The KIRUPA orange logo! A stylized orange made to look like a glass of orange juice! Tutorials Books Videos Forums

Change the theme! Search!
Rambo ftw!

Customize Theme


Color

Background


Done

Deconstruction: Irritated Bubbles

by kirupa   |   28 July 2012

Making things move on the screen using code is one of my favorite hobbies. It is what got me into computers / design / programming so many years ago, and it is what still keeps me there. Most of the time, I just create stuff on my own animations from playing around with some code. Rarely, I re-create static animations that others have created. This is one such rare time.

A while ago, the very talented Pasquale D'Silva created an animated GIF of bubbles moving. With his permission, I decided to re-create that animation using code and then deconstruct how it was done with all of you.

You can see my version here (or by clicking on the image below):

example of bubbles

[ go here to see my live version. I know you want to! ]

In this deconstruction, you will learn how I created this animation. You will learn how the big chunks of code come together to create the final result that you see. The goal of this exercise, though, is not just recreating this animation. It is something bigger and (hopefully) more useful.

What I want you to learn is how to deconstruct the many parts of an animation. It's about taking something that is animated, understanding the subtle details, and faithfully trying to reproduce it. You learning about the JavaScript that makes it all work is just the icing...the delicious, frosty icing.

That's not easy, but few things worth doing actually are. Let's get started.

It Could Get Tricky

If you've never animated anything using the canvas before, this tutorial may be a little bit on the advanced side of things. I do encourage you to read on, but if you ever find yourself getting stuck, check out the Animations in the HTML5 Canvas and Animating Many Things on a Canvas tutorials.

Let's Deconstruct This

The code is the easy part. I know that sounds strange, but it really is. The difficult part is in breaking your animation down into the individual details. In this section, without talking about a single line of code, let's dissect these details

To help with this, I've pasted a copy of Pasquale's original GIF below:

This may sound weird, but just the stare at the animation for about a minute. Pay attention to the details. Once you've stared at it long enough where closing your eyes leaves an imprint of the animation on the inside of your eyelids, it is time to move on :P

The next step is to figure out how to break this animation down. This animation, similar to all animations, can be broadly divided into two categories: how it looks and how it changes.

How it Looks

This is pretty obvious. At first glance, you can see that the things that are moving - the things that I call bubbles - are nothing more than circles:

blue circle

Next, notice that each bubble has a color. These colors fall within a narrow range of blues and purples. I took a screenshot of Pasquale's GIF and used a color picker (found in almost every image editor) to come up with the following colors for the bubbles:

the colors

[ the finite list of colors I found ]

After the color, the last thing to look at is the size of the circles themselves. Notice that they are all not the same size. Instead, they each try to be slightly different in how big they are. The size each circle has doesn't stay fixed either. Their size shrinks the higher they float to the top:

the size of the circle

Now that we are done with the visuals, let's look at the changes caused by our animation.

Looking at the Changes

You can't have an animation without something that changes over a period of time. In our case, the thing that changes is the position of our circles. In general, everything starts off screen at the bottom and moves all the way up. Seems simple enough. Notice how the circles go up, though. They don't go up in a straight fashion. Instead, they go up with a slight bounce:

circles bob and weave up

As they are going up, some bubbles lean or tilt a little bit to the right or to the left:

lean a bit left or right

After all, it would simply be too easy for us if the bubbles did not do that. With that said, bobbing, weaving, and tilting isn't all that these bubbles do. They also rotate with a random radius as they move:

ball rotating

Because the circles are moving up while they rotate, this ends up looking like it is wiggling horizontally.

The last thing that I am going to call out is how everything starts. At the beginning, only a handful of the bubbles appear:

start of the irritated bubbles

These bubbles start towards the bottom-middle and radiate outwards. After a few seconds, the floodgates open and a boatload of bubbles start to appear:

there are a lot of bubbles now

These bubbles don't start from the bottom-middle and radiate outward like the initial bubbles that you saw. Instead, they come fast and furious from all over the place.

And with that, we have just looked at how the bubbles look and how they move. This takes us to the next section...

Deconstructing the Code

Now that you have an idea of what we are trying to re-create, all that is left is actually seeing how that has been translated into code. As you will see shortly, despite all of the interesting movements our bubbles make, coding it all up is reasonably straightforward.

The code for this can be broken up into the following chunks:

  1. Setting up the stage.
  2. Defining the bubble
  3. Creating the bubbles
  4. Drawing and animating the bubbles
    1. Ensuring the first group of bubbles move more predictably
    2. Ensuring the rest of the bubbles are their crazy, random selfs
    3. Resetting the position of bubbles that disappear or go out of bounds
    4. Telling our canvas to actually draw the bubbles

In the following sections, let's look at the code in greater detail. The easiest way for you to follow along is to simply view the source for the Irritated Bubbles example in a separate window, tab, or even monitor.

Let the deconstruction begin!

Setting Up the Stage

As you can see from looking at the Irritated Bubbles source, the first thing I do is just declare some global variables that almost all of my code will be using:

 var mainCanvas = document.getElementById("myCanvas");
var mainContext = mainCanvas.getContext('2d');

var canvasWidth = mainCanvas.width;
var canvasHeight = mainCanvas.height;

// this array contains a reference to every bubble that you create
var bubbles = new Array();

The first four variables are specific to our canvas object and its size. They should be self-explanatory in what they do, and you'll see the  canvasWidth, and canvasHeight variables used frequently in our code.

In the last line, I declare an array called bubbles. This array will store every single bubble that you create. Yeah...this bubbles Array object is kinda a big deal in our code.

Defining the Bubble

Setting up the stage by declaring a few global variables is the easy/boring part. The real fun happens once you start getting your hands dirty with creating the bubble.

All of our bubbles are instances of the Bubble object, and below is the constructor for it:

function Bubble(rotationRadius, sign, speed, xIncrement, yIncrement, bubbleRadius, initialX, initialY, color) {
    this.rotationRadius = rotationRadius;
    this.bubbleRadius = bubbleRadius;
    this.initialX = this.currentX = initialX;
    this.initialY = this.currentY = initialY;
    this.color = color;

    this.counter = 0;
    this.angle = 0;
    this.xIncrement = xIncrement;
    this.yIncrement = yIncrement;
    this.sign = sign;

    this.speed = speed;
}

Knowing About Objects

If you are not familiar with objects and how to work with them in JavaScript, you may want to read my Objects and Classes tutorial first.

Besides taking a boatload of arguments, our constructor is very simple. It just takes the arguments that are passed to it and assigns them to properties that live on the Bubble object itself.

Let's look at these arguments in greater detail:

  1. rotationRadius
    Each bubble rotates around a fixed axis as it is moving. The radius of this rotation is specified by this argument.
  2. sign
    Does your bubble tilt left or does it tilt to the right? The value of this argument determines that.
  3. speed
    This variable specifies the initial speed at which our bubble moves.
  4. xIncrement
    This variable specifies the rate at which the horizontal speed changes.
  5. yIncrement
    This variable specifies the rate at which the vertical speed changes.
  6. bubbleRadius
    The radius of your bubble is specified here in pixel values. The larger this value, the bigger your bubble will be.
  7. initialX
    As its name implies, this specifies the initial horizontal position of the bubble.
  8. initialY
    The close friend of initialX. This variable specifies the initial vertical position of the bubble.
  9. color
    This one is simple. It specifies the color of our bubble.

As you can probably infer, the values of these arguments play an important role in determining how your bubbles look and move. You'll see shortly all of these values being specified when we are actually creating the Bubble objects. In fact, you'll see it next!

Creating the Bubble

The Bubble object constructor you saw is pretty cool, but it really doesn't have much going for it right now outside being this container for properties we want it to store. Let's take a few more steps forward and actually create the bubbles and put our object constructor to use.

The creation of the bubbles is handled by the aptly named createBubbles function. At the very top of this function, you will see this large list of additions to our bubbles array:

 bubbles.push(new Bubble(getRadius(), 1, .1, 4, .09, 7, canvasWidth * .5 - 30, canvasHeight + 140, getColor()));
bubbles.push(new Bubble(getRadius(), 1, .3, 3, .11, 8, canvasWidth * .5 - 20, canvasHeight + 140, getColor()));
bubbles.push(new Bubble(getRadius(), 1, .6, 2, .19, 9, canvasWidth * .5 - 10, canvasHeight + 140, getColor()));
bubbles.push(new Bubble(getRadius(), 1, .8, .5, .23, 5, canvasWidth * .5 - 5, canvasHeight + 140, getColor()));
bubbles.push(new Bubble(getRadius(), -1, .9, .56, .26, 6, canvasWidth * .5 + 5, canvasHeight + 140, getColor()));
bubbles.push(new Bubble(getRadius(), -1, .7, 1, .13, 7, canvasWidth * .5 + 10, canvasHeight + 140, getColor()));
bubbles.push(new Bubble(getRadius(), -1, .5, 3, .10, 8, canvasWidth * .5 + 20, canvasHeight + 140, getColor()));
bubbles.push(new Bubble(getRadius(), -1, .2, 4, .07, 9, canvasWidth * .5 + 30, canvasHeight + 140, getColor()));

bubbles.push(new Bubble(getRadius(), 1, .1, 4, .09, 7, canvasWidth * .5 - 30, canvasHeight + 240, getColor()));
bubbles.push(new Bubble(getRadius(), 1, .3, 3, .11, 8, canvasWidth * .5 - 20, canvasHeight + 240, getColor()));
bubbles.push(new Bubble(getRadius(), 1, .6, 2, .19, 9, canvasWidth * .5 - 10, canvasHeight + 240, getColor()));
bubbles.push(new Bubble(getRadius(), 1, .8, .5, .23, 5, canvasWidth * .5 - 5, canvasHeight + 240, getColor()));
bubbles.push(new Bubble(getRadius(), -1, .9, .56, .26, 6, canvasWidth * .5 + 5, canvasHeight + 240, getColor()));
bubbles.push(new Bubble(getRadius(), -1, .7, 1, .13, 7, canvasWidth * .5 + 10, canvasHeight + 240, getColor()));
bubbles.push(new Bubble(getRadius(), -1, .5, 3, .10, 8, canvasWidth * .5 + 20, canvasHeight + 240, getColor()));
bubbles.push(new Bubble(getRadius(), -1, .2, 4, .07, 9, canvasWidth * .5 + 30, canvasHeight + 240, getColor()));

In each line, I am creating a new Bubble object and painstakingly filling in all of the various arguments the constructor takes. As you will see shortly, much of our code for creating bubbles relies on using random values for all of the bubble's look and animation properties. One of our goals was to ensure that, at the start of the animation, a handful of bubbles move in a more prescribed way starting from the bottom-middle and then radiating outward.

The rest of the bubbles are added in a way that you would probably expect - using a loop that does away with manually having to define each bubble as you saw earlier:

 // For the remaining bubbles, create them and specify their properties in a more random fashion
for (var i = 0; i < 50; i++) {
	var initialX = getXPosition();
	var initialY = 600 + 100 * i + getYPosition();
	var speed = getSpeed();
	var bubbleRadius = getBubbleRadius();
	var rotationRadius = getRotationRadius();
	var color = getColor();
	var sign;

	var signHelper = Math.floor(Math.random() * 2);

	// Randomly specify whether the circle will be moving left or moving right
	if (signHelper == 1) {
		sign = -1;
	} else {
		sign = 1;
	}

	// create the Bubble object that will bring your bubble to life (and the award for cheesiest comment goes to...)
	var bubble = new Bubble(rotationRadius, sign, speed, getIncrement(), getIncrement() * .5, bubbleRadius, initialX, initialY, color);
	bubbles.push(bubble);
}

Each iteration of the loop creates a new bubble with its own set of (mostly unique) values that get passed in as an argument to the constructor. What I specified manually earlier, I have some simple functions that do the work for me here. If you look at what the getXPosition, getYPosition, getSpeed, getBubbleRadius, getRotationRadius, and getColor functions do, they all simply return a random value that falls within a certain range.

The end result is that your bubbles array now has the bubbles you defined manually along with the 50 bubbles created using this for loop. All that is left is to call our draw function:

 setInterval(draw, 20);

We use the setInterval function to call our draw function once every 20 milliseconds. You can learn more about this approach for creating smooth animations from the Frame Rates and HTML/JavaScript tutorial.

Let's look at the draw function next.

Drawing and Animating the Bubbles

The place where all of the action starts is with the draw function:

function draw() {
	mainContext.clearRect(0, 0, canvasWidth, canvasHeight);
	mainContext.fillStyle = '#CFF9FE';
	mainContext.fillRect(0, 0, canvasWidth, canvasHeight);

	for (var i = 0; i < bubbles.length; i++) {
		var myCircle = bubbles[i];
		myCircle.update();
	}
}

As you saw earlier, this function repeatedly gets called every 20 milliseconds, and it is responsible for painting the background and telling each bubble to update its position.

The drawing aspect of it is handled by the following code:

mainContext.clearRect(0, 0, canvasWidth, canvasHeight);
mainContext.fillStyle = '#CFF9FE';
mainContext.fillRect(0, 0, canvasWidth, canvasHeight);

The code for updating each of our bubbles is handled by the for loop that calls the update method on each Bubble object that lives in your bubbles array:

for (var i = 0; i < bubbles.length; i++) {
	var bubble = bubbles[i];
	bubble.update();
}

This update method is responsible for ensuring your Bubble's position and appearance is modified each time your draw method is called to create the animation that you see. As such, we'll spend a bit of time looking at its contents in great detail.

This update method looks as follows:

Bubble.prototype.update = function () {
    this.counter += 1;
    this.angle += this.yIncrement;

    // move the bubble up
    this.currentY -= (this.speed + (this.counter * this.yIncrement) / 100 + Math.sin(this.angle) + 1);

    // move the bubble to the left or the right depending on the value for 'sign'
    if (this.currentY < canvasHeight) {
        if (this.sign > 0) {
            this.currentX += (this.speed * this.xIncrement + this.radius * Math.cos(this.angle));
        } else {
            this.currentX -= (this.speed * this.xIncrement - this.radius * Math.cos(this.angle));
        }
    }

    // as the bubble starts to go higher, start reducing its size ever so slightly
    if (this.currentY * 2 < canvasHeight) {
        this.bubbleRadius *= .95;
    }

    // if the width of your bubble becomes less than 2 pixels, reset the position of the bubble
    if (this.width < 2) {
        this.resetPosition();
    }

    // if the bubble floats well beyond the visible portion of the canvas, reset the position of the bubble
    if (this.currentY < -100) {
        this.resetPosition();
    }

    // if the bubble goes too far to the left or to the right, reset the position of the bubble
    if ((this.currentX > (canvasWidth + 100)) || (this.currentX < -100)) {
        this.resetPosition();
    }

    // The following code is responsible for actually drawing the bubble on the screen
    mainContext.beginPath();
    mainContext.arc(this.currentX, this.currentY, this.bubbleRadius, 0, Math.PI * 2, false);
    mainContext.closePath();
    mainContext.fillStyle = this.color;
    mainContext.fill();
};        

This looks like a lot of code - and to a certain extent, it is. Before explaining this code, let's take a step back for a second. The end goal of everything you are doing is to draw a bubble. Each time the draw function is called, each bubble is drawn in a slightly different position. You repeat all of this once every 20 milliseconds, and you have an animation.

Ultimately, though, it all starts and ends with just drawing the bubble. All it takes to draw a bubble is just the X position, Y position, and the bubble's radius:

the three things we care about

All of the code you see above and will see analyzed shortly is about making sure these three properties are available to draw the bubble at the right location and size. Now that you know this, let's see how all of this code helps with that.

The Incrementers

All awesome animations have them. Well...at least this one does, and I hope you think it is awesome! An animation is a smooth change in some property value over a period of time. This "smooth change" is often handled by a handful of properties that are incremented by an amount.

In our case, that is the counter and angle properties:

 this.counter += 1;
this.angle += this.yIncrement;

The counter property is incremented by 1 at each tick, and the angle is incremented by the value of the yIncrement property. You'll see the counter and angle properties used shortly when specifying the position of our bubble.

Movement in the Y Direction / Vertical Movement

The vertical position of your element is stored by the currentY property. In the following line, the value of that property is adjusted:

this.currentY -= (this.speed + (this.counter * this.yIncrement) / 100 + Math.sin(this.angle) + 1);

All of this is just a simple manipulation of numbers. Notice that our incrementers counter and angle have made their first guest appearance. In the end, this code ensures our bubbles move up with a slight oscillation that gets bigger as the bubble goes further up.

Movement in the X Direction / Horizontal Movement

The code for moving your bubbles horizontally is next:

if (this.currentY < canvasHeight) {
    if (this.sign > 0) {
        this.currentX += (this.speed * this.xIncrement + this.rotationRadius * Math.cos(this.angle));
    } else {
        this.currentX -= (this.speed * this.xIncrement - this.rotationRadius * Math.cos(this.angle));
    }
}

The currentX property stores the current horizontal position of the bubble. The sign property, like I mentioned earlier, specifies whether the bubble will move left or whether it will move right.

Depending on what direction the bubble will be moving, you have a different mathematical expression for specifying the movement direction. Just like with the vertical case, I am just manipualating numbers to give the desired visual effect when everything is put together.

Scaling your Circle's Size

The last movement related piece of code that we have is the one that makes your bubble's size shrink the higher it floats:

if (this.currentY * 2 < canvasHeight) {
    this.bubbleRadius *= .95;
}

Once the bubble's current position goes above a certain threshold, it's size is decreased ever so slightly. Remember, because this code gets called once every 20 milliseconds, "ever so slightly" can add up to something substantial.

Resetting the Position

When a bubble is no longer visible, almost everything about it is reset so that it can start its journey all over again.

The code for doing all of that can be found here:

// if the width of your bubble becomes less than 2 pixels, reset the position of the bubble
if (this.bubbleRadius < 2) {
    this.resetPosition();
}

// if the bubble floats well beyond the visible portion of the canvas, reset the position of the bubble
if (this.currentY < -100) {
    this.resetPosition();
}

// if the bubble goes too far to the left or to the right, reset the position of the bubble
if ((this.currentX > (canvasWidth + 100)) || (this.currentX < -100)) {
    this.resetPosition();
}

The cases where we reset a bubble to a new beginning are when it becomes so small that it is virtually invisible, when it floats well beyond the top part of the canvas, or when it goes outside of either the left or right boundaries. These three cases are outlined in the code above, and in all of them, the resetPosition method is called to set our wayward bubble back on the right path.

Drawing the Bubble (For Real This Time)

Finally, after all of this, we get to the point where the bubble is actually drawn to your canvas. The code for that rounds out our update method:

// The following code is responsible for actually drawing the bubble on the screen
mainContext.beginPath();
mainContext.arc(this.currentX, this.currentY, this.bubbleRadius, 0, Math.PI * 2, false);
mainContext.closePath();
mainContext.fillStyle = this.color;
mainContext.fill();

I've highlighted the two lines where the currentX, currentY, bubbleRadius, and color properties are used to actually draw what we are planning on drawing. To reiterate what I mentioned earlier, all of the work you've done is just to ensure these properties are set appropriately to draw your bubble in the right location.

Conclusion

Well, that is all there is to it. Feel free to take my source code and make tweaks to it to create your own unique take on this animation. If you create something really cool, do share it in the Irritated Bubbles forum thread - a thread which I will watch like a hawk for cool examples!

Just a final word before we wrap up. If you have a question and/or want to be part of a friendly, collaborative community of over 220k other developers like yourself, post on the forums for a quick response!

Kirupa's signature!

The KIRUPA Newsletter

Thought provoking content that lives at the intersection of design 🎨, development 🤖, and business 💰 - delivered weekly to over a bazillion subscribers!

SUBSCRIBE NOW

Serving you freshly baked content since 1998!
Killer icons by Dark Project Studios

Twitter Youtube Facebook Pinterest Instagram Github