HTML5 Canvas Particle Animation

19 Jan

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.

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)

Circle Object

What first needs to happen is we need to create a particle object in JavaScript.

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
function Circle() {
    this.settings = {time_to_live:8000, x_maxspeed:5, y_maxspeed:2, radius_max:10, rt:1, x_origin:960, y_origin:540, random:true, blink:true};

    this.reset = function() {
        this.x = (this.settings.random ? WIDTH*Math.random() : this.settings.x_origin);
        this.y = (this.settings.random ? HEIGHT*Math.random() : this.settings.y_origin);
        this.r = ((this.settings.radius_max-1)*Math.random()) + 1;
        this.dx = (Math.random()*this.settings.x_maxpseed) * (Math.random() < .5 ? -1 : 1);
        this.dy = (Math.random()*this.settings.y_maxpseed) * (Math.random() < .5 ? -1 : 1);
        this.hl = (this.settings.time_to_live/rint)*(this.r/this.settings.radius_max);
        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 new_opacity = 1-(this.rt/this.hl);
        con.beginPath();
        con.arc(this.x,this.y,this.r,0,Math.PI*2,true);
        con.closePath();
        g = con.createRadialGradient(this.x,this.y,0,this.x,this.y,this.r*new_opacity);
        g.addColorStop(0.0, 'rgba(255,255,255,'+new_opacity+')');
        g.addColorStop(this.stop, 'rgba(77,101,181,'+(new_opacity*.6)+')');
        g.addColorStop(1.0, 'rgba(77,101,181,0)');
        con.fillStyle = g;
        con.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;
    }
}

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
16
17
18
19
$(document).ready(function(){
    WIDTH = 1680;
    HEIGHT = 1050;
    canvas = document.getElementById('pixie');
    con = canvas.getContext('2d');
    for(var i = 0; i < 100; i++) {
        pxs[i] = new Circle();
        pxs[i].reset();
    }
    setInterval(draw,rint);
});
function draw() {
    con.clearRect(0,0,WIDTH,HEIGHT);
    for(var i = 0; i < pxs.length; i++) {
        pxs[i].fade();
        pxs[i].move();
        pxs[i].draw();
    }
}

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).

21 Responses to “HTML5 Canvas Particle Animation”

  1. kyle 22. Feb, 2011 at 4:47 pm #

    Excellent! So how would you control where the particles originate from and where they burn out? Similar to what Mobile Me did? The particles originate behind the logo and radiate outward. Any idea?

    • Tim 23. Feb, 2011 at 1:37 pm #

      Thanks! It’s pretty simple to get them all to originate from one point (or a region, if you would prefer). In fact, I have the random bool value in the this.s (settings) of the Circle object so you can switch that to false and every point will radiate from the (xdef, ydef) coordinate.

      I also have the blink bool value to indicate whether they continue to drift around and fade in again after they fade all the way out or, if set to false, will reset to the origin.

      Like I said, pretty simple to get that to happen! However, you’ll notice in the MobileMe site that the particles have random amounts of drift in both the x and y directions so they don’t move in straight lines and have “target tendencies,” meaning they all aim for a particular iDevice in the background. Getting that to happen is a lot more more and easily triples the amount of code necessary to get that working. If you want to know more, though, let me know and I’ll write up something about that, too. Thanks again!

  2. Arthwarr 11. Apr, 2011 at 5:08 am #

    This is absolutely fantastic.

    Thanks a lot for writing this piece of code, i couldnt understand how it worked on apple website

    • Tim 11. Apr, 2011 at 7:21 am #

      No problem. Thanks for reading!

  3. John 18. May, 2011 at 12:14 pm #

    Wow, that looks amazing! I’ve been thinking about creating my own snazzy website in a similar vein :) am I allowed to use your code or night scene image or is this only intended as an example?

    Thanks,

    J

    • Tim 18. May, 2011 at 1:07 pm #

      Of course you’re allowed! Feel free to use it all if you so desire, I’m just glad you like it. A little head nod my way when you’re done would be great. I’d love to see what you create.

  4. Tobias 17. Aug, 2011 at 10:45 am #

    Thank you so much for this awesome script. I am trying to implement it on my website but just found out that it does not work in Internet Explorer, is there some kind of work around?

    Thanks,

    Tobias

    • Tim 18. Aug, 2011 at 8:30 am #

      In general, there is an error that returns in just about every browser. It’s an INDEX_SIZE_ERR (DOM Exception 1) which means that somewhere in the code, there is a negative value being put into a canvas method. In this case, on line 110, there are occasions when the circle’s radius will go into negative territory.

      In Chrome and whatnot, these errors are tolerated and the animation will continue. In IE, however, the whole thing just halts. The fix is so ridiculously simple and rectifies both the DOM errors and the flashing/stuttering some users have noticed that I don’t know why I never got around to it before. Replace line 110 with the following two lines:

      var cr = this.r * newo;
      g = con.createRadialGradient(this.x,this.y,0,this.x,this.y,(cr <= 0 ? 1 : cr));

      If you’re talking about an IE version that’s less than 9, though, then that’s because those versions don’t support the canvas element.

      And if you’re talking about the missing background gradient, that’s because I didn’t put in those stupid trashy DX filters. I was hoping instead that IE9 would eventually be patched up to IE10 and use some sort of vendor prefix for CSS3 linear gradients.

  5. Shannon 12. Sep, 2011 at 7:04 pm #

    Sorry For Being A Total Newb! :P

    But In What Part Of The HTML File,
    Do I Stick My Website Content Into?

    Because Everytime I Try To Stick It Into A Different Div Tag, It Either Shows At The Bottom Or Top Of The Page.

    Thanks :D

    • Shannon 12. Sep, 2011 at 7:06 pm #

      & In This Case Is Their Like Some CSS You Need To Do This?

    • Tim 14. Sep, 2011 at 3:26 pm #

      I guess that’s because there really isn’t a place to do so. This wasn’t really made to be anything to house content, but that doesn’t mean you can’t!

      Just add another

      to the #container and give it the following CSS rules:

      #content {
      position:absolute;
      width:50%;
      height:50%;
      top:25%;
      left:25;
      border:1px solid #FFF;
      }

      That should get you started with a white-bordered box in the center of the screen that is half the width and height of the window. You’ll have to do more stuff if you want some scrolling content, though.

      Good luck, and thanks for reading!

  6. dhaval 18. Dec, 2011 at 12:53 am #

    how can I add text on the top?

  7. Ashley 04. Jan, 2012 at 9:05 am #

    This is absolutely fantastic. You should be proud.

    Well done!

  8. Kelli 17. Jan, 2012 at 8:49 pm #

    I noticed when you make the window larger, you are left with white space. Is there a way to make it all expand with the window?
    Awesome work btw!

    • Tim 17. Jan, 2012 at 9:54 pm #

      Thanks! And there absolutely is a way!

      Unfortunately, it’s not as simple as setting both the container <div> and the <canvas> element with width=”100%” and height=”100%”. You can go ahead and do that with the <div>, but <canvas> requires hard-set widths and heights to draw anything (in Chrome anyways).

      What you need to do is add an event listener for window.onresize and set it to something like:

      1
      2
      3
      4
      5
      6
      window.onresize = function(e){
          WIDTH = window.innerWidth;
          HEIGHT = window.innerHeight;
          $('#canvas').attr('width',WIDTH).attr('height',HEIGHT);
          $('#container').width(WIDTH).height(HEIGHT);
      };

      Although I think in my code, I actually set the ID for the <canvas> element to “#pixie.” But either way, just set an event listener for window.onresize so you can set the appropriate width and heights for the container, <canvas>, and the application variables (in this case, WIDTH and HEIGHT). Thanks again for reading!

    • Kelli 18. Jan, 2012 at 2:31 am #

      Awesome! Thanks. Still learning javascript :) . I tweaked it a little since it wasn’t redrawing the stars and came up with this:

      window.onresize = function(e){
      WIDTH = window.innerWidth;
      HEIGHT = window.innerHeight;
      canvas = document.getElementById(‘pixie’);
      $(canvas).attr(‘width’, WIDTH).attr(‘height’,HEIGHT);
      $(‘#container’).width(WIDTH).height(HEIGHT);
      for(var i = 0; i < 100; i++) {
      pxs[i] = new Circle();
      pxs[i].reset();
      }
      setInterval(draw,rint);
      };

      Works perfect for me now :)

    • Tim 18. Jan, 2012 at 2:46 am #

      That’s great! Seems like you’re doing fine to me :0)

    • Kelli 18. Jan, 2012 at 2:38 am #

      oops….. scratch that

      setInterval(draw,rint);

      at the end there. Don’t need to run it twice unless i ant crazy fast stars haha.

  9. Valentines Day Gifts 30. Jan, 2012 at 1:41 am #

    A video game that’s realistic? All I? can think of are driving games.

Trackbacks/Pingbacks

  1. How to make a Javascript Particle Engine | Flyers Blog - 14. Nov, 2011

    [...] de particule basé sur l’animation de simples éléments DIVs. Vous pouvez voir un exemple de moteur de particule en HTML 5 pour approfondir le sujet mais ce n’est pas l’orientation du moteur que je vais vous [...]

Leave a Reply