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




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?
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!
This is absolutely fantastic.
Thanks a lot for writing this piece of code, i couldnt understand how it worked on apple website
No problem. Thanks for reading!
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
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.
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
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.
Sorry For Being A Total Newb!
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
& In This Case Is Their Like Some CSS You Need To Do This?
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
#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!
how can I add text on the top?
This is absolutely fantastic. You should be proud.
Well done!
I’ve done some benchmarking about Canvas animation on Mobile platform and it was clear that Canvas tecnology was really too heavy to be deployed on mobiles platform.
That’s why i’ve made a particle engine based on DIV animation who is really more speedy. Check it out http://www.flyers-web.org/blog/programmation/how-to-make-a-javascript-particle-engine-238
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!
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:
2
3
4
5
6
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!
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
That’s great! Seems like you’re doing fine to me :0)
oops….. scratch that
setInterval(draw,rint);
at the end there. Don’t need to run it twice unless i ant crazy fast stars haha.
A video game that’s realistic? All I? can think of are driving games.