Andi Smith

Canvas Animated Particles 3D Effect in 5 Minutes

A couple of months ago, I gave a PechaKucha talk about creating a 3D animated particles effect with the canvas element.

For those who've not come across the concept of a PechaKucha before, the idea is to present on a topic for 20 slides with 20 seconds per slide. The event I attended was slightly different with just 5 minutes to show those 20 slides (so 15 seconds a slide).

Whenever I talk to web developers I find that the canvas element still remains a mystery to most, so I wanted to do a short presentation that could introduce developers who had not used the canvas element before to the concepts and show how it's not quite as scary as it seems.

It was my first PechaKucha, and during rehearsal I learnt that the format doesn't lend itself too well to explaining how to implement lines and lines of code. I tried to balance out the slides so there wasn't too much new information on each. The aim of the talk was less about teaching developers everything about canvas and more about showing how simple it can be to create an effect.

Below is a write up of the talk, and I'm going to purposefully keep it short (hopefully a 5 minute read) to allow you time to experiment. You can view the original presentation here, the slides move on every 15 seconds and the animated demos start themselves - each demo is deliberately self contained so you can dive in to the code. Please use, abuse and learn.

The 5 Minute 3D Effect

Quick note: this is a 3D effect in 2D, not a WebGL demo. The effect we are going for is a simple space effect with particles flying out of the center of the canvas element, it looks like the following:

3D Particles Effect

The Canvas element

  • Allows us to place a 2D drawing canvas on our page
  • Interfaced with JavaScript
  • Default dimensions: 300 x 150
  • Works in IE9+
<canvas id="particles">
  Stop running IE6-8 dude, it's not cool!
</canvas>

The Canvas Context

The context is like an artists toolbox. We use it to:

  • Draw lines, rectangles and arcs
  • Set colours and styles
  • Fill shapes
  • Clear the canvas
  • Get/use image data
var canvas = document.getElementById('particles');
var context = canvas.getContext('2d');

Set up our Drawing Area

We will create a function called draw() that will be called for each frame of our animation. This function will need to carry out a number of tasks:

  • Clear the canvas - as our canvas is a bitmap we cannot move items on it, so for each frame we have to redraw the canvas.
  • Translate our canvas so our point of origin (0, 0) is the center, not the top left.
  • We use save() and restore() to save/restore the state of the context (e.g. point of origin, colors). It does not save an image of the canvas.
function draw() {
  context.clearRect(0, 0, width, height);

  context.save();
  context.translate((width/2), (height/2));

  // draw particles

  context.restore();
}

Decide On a Amount of Particles

We need to decide how many particles we wish to show. Naturally in a presentation, the audience will always choose the highest number you offer (10,000) - but in real life consider number of particles vs. performance. Our particles will live in an array in JavaScript.

var AMOUNT = 10000;
var particles = [];

Create Those Particles!

Each particle is an object and for each particle we:

  • Make an object with x, y, z co-ordinates
  • Push it on to our particles array
function create() {
  var i = AMOUNT;
  var particle;

  while (i--) {
    particle = {
      x: 0,
      y: 0,
      z: 0
    };
    particles.push(particle);
  }
}

Generate Random Numbers

At the moment all of our particles are sitting at the same co-ordinates (the center), so we need to give them some random coordinates, which means we need a simple function to generate random numbers. This function:

  • Gets the difference between max and min
  • Multiplies the difference by Math.random()
  • Adds min back to our value to ensure it's within our range
function randomNumber(min, max) {
    return (Math.random() * (max - min)) + min;
}

Create Random Particles!

Now we've done that, we adjust our particle creation loop for each particle to:

  • Set x to a random number based on half the width (remember our origin is the center)
  • Set y to a random number based on half the height
  • Set z to 0 (for the moment)
particle = {
  x: randomNumber(-(width/2), (width/2)),
  y: randomNumber(-(height/2), (height/2)),
  z: 0
};

Draw Our Particles!

Now we have differing co-ordinates, we loop through our particles array and make each particle a circle using context.arc(x, y, radius, startAngle, endAngle, antiClockwise).

context.fillStyle='rgba(255,255,255,0.3)';
context.beginPath();
context.arc(particle.x, particle.y, radius, 0, Math.PI*2, true);
context.fill();

Here, we are choosing to color our particles white, during our arc and then filling it. I've added a nice semi-transparent effect to the color by specifying an alpha in our rgba color value. You can improve performance by only setting the colour once rather than for each particle.

Field of Vision

The z coordinate measures how close or far away our particle is from our view. If it gets too close, it will obscure our view so we want to simulate it going past us at some point. If it gets too far away, we are spending time rendering a particle that is practically invisible. So we use this value to tell our draw function when to draw the particle. It's up to you which value you use, ~250 seems to work well.

var FIELDOFVISION = 250;
particle = {
  x: randomNumber(-(width/2),(width/2)),
  y: randomNumber(-(height/2),(height/2)),
  z: randomNumber(-FIELDOFVISION,FIELDOFVISION)
};

Scaling Our Particles

Particles should be different sizes at different points on the z axis, so we use scale to position them and to change the radius of our circles.

scale = (FIELDOFVISION/(particle.z+FIELDOFVISION));
x = (particle.x * scale);
y = (particle.y * scale);
context.arc(x, y, scale, 0, Math.PI*2, true);

Animation

Our particles are now pretty, scaled and scattered, but still static. We use requestAnimationFrame to draw when the browser reflows, for the best performance.

Add a quick shim:

window.requestAnimationFrame ||
  (window.requestAnimationFrame = window.webkitRequestAnimationFrame || 
  window.mozRequestAnimationFrame || 
  window.oRequestAnimationFrame || 
  window.msRequestAnimationFrame || 
  function( callback ){
    window.setTimeout(callback, 1000 / 60);
  });

Then we call draw() like so:

requestAnimationFrame(draw);

Make Our Particles Move!

To make the particles move, for each frame we need to change the z co- ordinate.

SPEED = -4;

Within our draw loop:

particle.z += SPEED;

We also want to test for the field of vision and move our particles to the front or back when they are no longer in vision. This has the bonus of reducing the amount of array manipulation (we're just editing values rather than splicing and splitting arrays) improving performance.

if (particle.z < -FIELDOFVISION) {
  particle.z = FIELDOFVISION;
}

Done!

And that's it, we have particles that move! Simple, and nothing really to be scared of.

Check out my presentation for demos; a few more effects and a nice picture of a cat! Enjoy!