HTML5 Canvas Particle Animation

Posted by in Code

UPDATE: yes, I know some people out there will see some flickering when viewing the demo. This is because a) I didn’t implement a double buffer, and b) there is no built-in canvas support for double buffering. There is a very simple solution, but I’ll save that for another time.

Having never really been a user of Apple products, I guess it’s not all that surprising that I’ve never been to the MobileMe website. However, after Googling something like the lines of “most popular e-mail providers,” I came across its login page and was blown away.

Holy moly, that looks amazing. Not only is the design great and the colors are just about perfect, but those little sparkles are crazy! They move in what appears to be 3D space, pulsing from behind the cloud to further into the background amongst and into (while highlighting) the iPads and iPhones, and follow your mouse movements for extra fun.

After poking around the source and giving up on 100% comprehension, I decided it might be fun to take a whack at my own thoroughly underwhelming homage to the MobileMe page.

The Setup

For some reason or another, I decided to make a night scene of some mountains and grass for some stars to streak across (I also threw in some parallax, but that’s for another post). Here’s what I’m starting out with prior to any canvas magic:

And here’s the accompanying code to make that happen (it’s pretty simple, but I’m including it anyways so as little is left out as possible):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#container {
    overflow:hidden;
    position:relative;
}
#pixie {
    z-index:0;
    background:-moz-linear-gradient(top, #040429, #257eb7);
    background:-webkit-gradient(linear, left top, left bottom, color-stop(0%,#040429), color-stop(100%,#257eb7));
}
#mountains, #grass {
    width:100%;
    position:absolute;
    bottom:0;
}
#mountains {
    height:156px;
    z-index:1;
    background:url(mountains.png) repeat-x 0 0;
}
#grass {
    height:62px;
    z-index:2;
    background:url(grass.png) repeat-x left 10px;
}
1
2
3
4
5
<div id="container">
    <canvas id="pixie"></canvas>
    <div id="mountains"></div>
    <div id="grass"></div>
</div>

But that’s child’s play compared to what you really came here for: HTML5 canvas! (You should note, though, that the actual height and width attributes of the canvas element must be set in the HTML or dynamically through JavaScript and not just the CSS, otherwise you’ll get something…weird)

The JavaScript

What first needs to happen is setting up the drawing environment. This includes grabbing the appropriate elements and getting the drawing dimensions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var WIDTH = window.innerWidth,
    HEIGHT = window.innerHeight,
    MAX_PARTICLES = 100,
    DRAW_INTERVAL = 60,
    container = document.querySelector('#container'),
    canvas = document.querySelector('#pixie'),
    context = canvas.getContext('2d'),
    gradient = null,
    pixies = new Array();

function setDimensions(e) {
    WIDTH = window.innerWidth;
    HEIGHT = window.innerHeight;
    container.style.width = WIDTH+'px';
    container.style.height = HEIGHT+'px';
    canvas.width = WIDTH;
    canvas.height = HEIGHT;
}
setDimensions();
window.addEventListener('resize', setDimensions);

This ensures that on window resizes, everything gets set again and updated to the new dimensions. You can also tinker with MAX_PARTICLES and DRAW_INTERVAL to your liking.

Next up is the Circle object. It contains the bulk of the logic of how the particles will behave.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
function Circle() {
    this.settings = {ttl:8000, xmax:5, ymax:2, rmax:10, rt:1, xdef:960, ydef:540, xdrift:4, ydrift: 4, random:true, blink:true};

    this.reset = function() {
        this.x = (this.settings.random ? WIDTH*Math.random() : this.settings.xdef);
        this.y = (this.settings.random ? HEIGHT*Math.random() : this.settings.ydef);
        this.r = ((this.settings.rmax-1)*Math.random()) + 1;
        this.dx = (Math.random()*this.settings.xmax) * (Math.random() < .5 ? -1 : 1);
        this.dy = (Math.random()*this.settings.ymax) * (Math.random() < .5 ? -1 : 1);
        this.hl = (this.settings.ttl/DRAW_INTERVAL)*(this.r/this.settings.rmax);
        this.rt = Math.random()*this.hl;
        this.settings.rt = Math.random()+1;
        this.stop = Math.random()*.2+.4;
        this.settings.xdrift *= Math.random() * (Math.random() < .5 ? -1 : 1);
        this.settings.ydrift *= Math.random() * (Math.random() < .5 ? -1 : 1);
    }

    this.fade = function() {
        this.rt += this.settings.rt;
    }

    this.draw = function() {
        if(this.settings.blink && (this.rt <= 0 || this.rt >= this.hl)) {
            this.settings.rt = this.settings.rt*-1;
        } else if(this.rt >= this.hl) {
            this.reset();
        }

        var newo = 1-(this.rt/this.hl);
        context.beginPath();
        context.arc(this.x, this.y, this.r, 0, Math.PI*2, true);
        context.closePath();

        var cr = this.r*newo;
        gradient = context.createRadialGradient(this.x, this.y, 0, this.x, this.y, (cr <= 0 ? 1 : cr));
        gradient.addColorStop(0.0, 'rgba(255,255,255,'+newo+')');
        gradient.addColorStop(this.stop, 'rgba(77,101,181,'+(newo*.6)+')');
        gradient.addColorStop(1.0, 'rgba(77,101,181,0)');
        context.fillStyle = gradient;
        context.fill();
    }

    this.move = function() {
        this.x += (this.rt/this.hl)*this.dx;
        this.y += (this.rt/this.hl)*this.dy;
        if(this.x > WIDTH || this.x < 0) this.dx *= -1;
        if(this.y > HEIGHT || this.y < 0) this.dy *= -1;
    }

    this.getX = function() { return this.x; }
    this.getY = function() { return this.y; }
}

The first line is some settings for each particle, each of which is named in a pretty self-explanatory manner.

  • time_to_live – used to calculate hl–or the half-life–of each particle
  • x_maxspeed and y_maxspeed – defines the maximum number of pixels a particle can move each frame
  • radius_max – maximum radius a particle can achieve
  • rt – used in conjunction with hl to determine how the ratio of maximum speed and full opacity of each particle in each frame

The reset() function just sets up the particle in a new location if it’s the first iteration or if random is set to true and the move() function just moves the particle according to the settings (you’ll notice I have them moving faster as they get smaller and more transparent). The good stuff comes in the draw() function.

Within draw(), you see that a new path is started for each particle and drawn into a circle with the arc() function. Normally you would use this just to draw an arc, but by supplying 2Π, or the radian equivalent to a full circle, as the fifth argument, you get a circle.

Then you can call createRadialGradient() to fill in the circles we just created with color, which I have set with three color stops at various opacities to really make the particles look like they’re glowing. You should note, though, that the con and g variables are set outside of the Circle object and treated as member variables.

Animating the Particles

Finally, you need to create all of the particles and iterate through each one to move and render:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
for (var i = 0; i < MAX_PARTICLES; i++) {
    pixies.push(new Circle());
    pixies[i].reset();
}

function draw() {
    context.clearRect(0, 0, WIDTH, HEIGHT);
    for(var i = 0; i < pixies.length; i++) {
        pixies[i].fade();
        pixies[i].move();
        pixies[i].draw();
    }
}

setInterval(draw, DRAW_INTERVAL);

The first part creates each new particle (or Circle() in this case) and stores it in an array. We then set a function to run at a certain interval based on what frames per second we desire to run at.

The draw() function simply iterates over the array and calls each necessary function of each particle to animate its movement. It’s necessary, though, to point out the clearRect() call. Without it, you wouldn’t get an animation of moving stars in the night sky but rather streaking purple circles.

With any luck, you should get something like this:

This is a very base attempt at particle animation with HTML5′s canvas element. You can do a lot of other, crazier things with canvas, but this is a good place to start understanding how it works and how to get animated things moving with JavaScript.

You can also click and drag left and right on the screen to get some parallax going between the mountains and the grass. I’ll go into detail on how to achieve that effect later on in life at some point maybe perhaps.

View the demo (requires you to be running an HTML5-capable browser).