An Introduction to Particle Systems/Generators using the HTML5 Canvas object

November 18, 2012

In its most elemental form, particle systems are composed by two main objects: A particle object, which will represent every individual particle, and a generator, which will generate particles themselves.

For the sake of simplicity we're going to approach some things unconventionally. In reality, a particle system can be an inmensely complex system.

So what exactly is a "particle"? In computer science and game development a particle could be described as a small localized object that stores physical properties, such as volume or mass, and dynamic properties such as its position in the X and Y axis (and also Z, if you're working in 3D) as well as its velocity in all two or three axis.

The main purpose of a particle generator is to generate the particles themselves (well, D'Oh!) and to control some properties about the generation, such as the number of particles to generate, their initial speed, spread, etc.

This two definitions will form the basis of our particle system, so let's translate them into a practical and real-world context.

First of all, we need to setup the scene where the magic will happen.

All we need is just an empty HTML and a Canvas object, that's it.

<!DOCTYPE html>
<html>
    <head>
        <style>
            html { height: 100%; }
            body {
                margin: 0;
                padding: 0;
                background-color: #222;
                height: 100%;
            }
        </style>
        <script src="particles.js"></script>
    </head>
    <body>
        <canvas></canvas>
    </body>
</html>

Now we're going to create a file called particles.js and use the following code:

// particles.js
(function() {
	// shim layer with setTimeout fallback by Paul Irish
	// Used as an efficient and browser-friendly 
	// replacement for setTimeout or setInterval
	window.requestAnimFrame = (function(){
		return  window.requestAnimationFrame ||
		window.webkitRequestAnimationFrame   ||
		window.mozRequestAnimationFrame      ||
		window.oRequestAnimationFrame        ||
		window.msRequestAnimationFrame       ||
		function (callback) {
			window.setTimeout(callback, 1000 / 60);
		};
	})();

	// Call the init() function on load
	document.addEventListener('DOMContentLoaded', init);

	function init() {
		// Get a reference to the canvas object in the HTML
		var cobj = document.getElementsByTagName('canvas')[0],
			  c = cobj.getContext('2d');

		// Make the canvas have the same size as
		// the browser window
		cobj.width = document.body.clientWidth;
		cobj.height = document.body.clientHeight;

		// Call the paint method
		paint();

		function paint() {
			// Call paint() again, recursively
			requestAnimFrame(paint);
		}
	}
})();

That's it for the moment. Now, we've discussed that particles store physical properties, which we're not going to worry about now, and dynamic properties, such as their position in the X and Y axis as well as their velocity. So let's create a class called "Particle" that does that for us:

var Particle = function(x, y, vx, vy) {
	this.x = x || 0;
	this.y = y || 0;
	this.vx = vx || 0;
	this.vy = vy || 0;
	
	this.update = function (vx, vy) {
		vx = vx || 0,
		vy = vy || 0;

		this.x += this.vx + vx;
		this.y += this.vy + vy;
	};
};

The concept is simple: Every time that the update() method of Particle objects gets called, the velocity is added to the position. As pictured below, if the velocity of both axis is positive, the particle would move down and to the right, and if it's negative it would move the left and up and so on.

Our generator will be in charge of primarily two main tasks; The first one will be to instantiate, store and manage particles themselves and the second will be to call each particle's update() method.

In order to simplify things a bit, we're also going to be specifying a reference to the canvas where particles are going to be displayed.

var ParticleSystem = function(container, center, count) {
	var i = 0,
			c = container.getContext('2d');

	count = count || 0;

	this.particles = [];

	this.center = {
		x: center.x || 0,
		y: center.y || 0
	};

	// Initialization
	for ( ; i < count ; ++i ) {
		var x = this.center.x,
				y = this.center.y,
				vx = 1,
				vy = 1;

		this.particles.push(new Particle(x, y, vx, vy));
	}

	this.update = function() {
		for ( i = 0 ; i < count ; ++i ) {

			// We don't want to process particles 
			// that we can't see anymore
			if (this.particles[i].x > 0 &&
				this.particles[i].x < container.width &&
				this.particles[i].y > 0 &&
				this.particles[i].y < container.height) {

				this.particles[i].update();

				c.fillRect(this.particles[i].x, this.particles[i].y, 5, 5);
			}
		}
	};
};

Finally, inside the init() function we're going to; First, instanciate a new ParticleSystem object and second, call its update() method from within the paint() function.

So, let's put it all together:

(function() {

  var Particle = function(x, y, vx, vy) {
    this.x = x || 0;
    this.y = y || 0;
    this.vx = vx || 0;
    this.vy = vy || 0;
    
    this.update = function (vx, vy) {
      vx = vx || 0,
      vy = vy || 0;

      this.x += this.vx + vx;
      this.y += this.vy + vy;
    };
  };

  var ParticleSystem = function(container, center, count) {
    var i = 0,
        c = container.getContext('2d');

    count = count || 0;

    this.particles = [];

    this.center = {
      x: center.x || 0,
      y: center.y || 0
    };

    // Initialization
    for ( ; i < count ; ++i ) {
      var x = this.center.x,
          y = this.center.y,
          vx = 1,
          vy = 1;

      this.particles.push(new Particle(x, y, vx, vy));
    }

    this.update = function() {
      for ( i = 0 ; i < count ; ++i ) {

        // We don't want to process particles that
        // we can't see anymore
        if (this.particles[i].x > 0 &&
          this.particles[i].x < container.width &&
          this.particles[i].y > 0 &&
          this.particles[i].y < container.height) {

          this.particles[i].update();

          c.fillRect(this.particles[i].x, this.particles[i].y, 5, 5);
        }
      }
    };
  };


  // shim layer with setTimeout fallback by Paul Irish
  // Used as an efficient and browser-friendly 
  // replacement for setTimeout or setInterval
  window.requestAnimFrame = (function(){
    return  window.requestAnimationFrame ||
    window.webkitRequestAnimationFrame   ||
    window.mozRequestAnimationFrame      ||
    window.oRequestAnimationFrame        ||
    window.msRequestAnimationFrame       ||
    function (callback) {
      window.setTimeout(callback, 1000 / 60);
    };
  })();

  // Call the init() function on load
  document.addEventListener('DOMContentLoaded', init);

  function init() {
    // Get a reference to the canvas object in the HTML
    var cobj = document.getElementsByTagName('canvas')[0],
        c = cobj.getContext('2d'),
        p = null;

    // Make the canvas have the same size as
    // the browser window
    cobj.width = document.body.clientWidth;
    cobj.height = document.body.clientHeight;

		// Create a new particle system that generates 
		// 100 particles on the center of the screen
    p = new ParticleSystem(cobj, { x: cobj.width / 2, y: cobj.height / 2 }, 100);

    // Call the paint method
    paint();

    function paint() {
      p.update();

      // Call paint() again, recursively
      requestAnimFrame(paint);
    }
  }
})();

If you try it, the code above generates the following output:

Well, it's not exactly what I would call impressive, innit?

Let's brake it down and see what's going on here. First, you'll notice that a line is being formed - the reason why this happens is quite simple; Remember that the HTML5 Canvas object works in immediate mode, which roughly translates to "whatever you paint, it stays there until you decide to clean it". So we are actually watching the particles moving, the problem is that they are not "cleaning" after themselves.
The second problem is that all the particles are black. This happens because we haven't set a fill style, and the default fillStyle is 0x000000, or well... you know, black.

Finally, it seems that all the particles are moving in the same direction, as if we were only displaying a single particle. The reason why this happens is because if you pay attention to the initilisation routine our initial velocity for both the X as well as the Y axis is 1, which means that particles will move to the right and down. In order to fix that, we'll need to add a random value.

The idea is to generate a range between a negative number and a positive number. To do that, we'll be doing this:

vx = Math.random() * 3 - 1.5;
vy = Math.random() * 3 - 1.5;

Which will generate a number between -1.5 and 1.5. Let's apply all these changes and see what comes out of it then!

Now that's much better! However, you'll notice that after the first "explosion" the particles get out of the screen and nothing else happens. The reason why this is, is because of the following four lines:

// We don't want to process particles that
// we can't see anymore
if (this.particles[i].x > 0 &&
  this.particles[i].x < container.width &&
  this.particles[i].y > 0 &&
  this.particles[i].y < container.height) {
  
}

All that we need to do then, is to add an else, which will handle what happens when particles are no longer inside the visible area. In this case, we could take two possible paths:

The first one would be to create a new particle instance replacing the old one, but while this could look like a good idea it's not very GC-friendly. By GC I'm of course referring to the browser's Garbage Collector that deletes unused objects from memory - in this case we would be instantiating many objects every second, which would mean that we would be generating thousands of objects that would only last one "pass".

The other option, the one we're going to take, is to reset the position of the particle back to the origin. So let's do that!

// We don't want to process particles that
// we can't see anymore
if (this.particles[i].x > 0 &&
  this.particles[i].x < container.width &&
  this.particles[i].y > 0 &&
  this.particles[i].y < container.height) {

  this.particles[i].update();

  c.fillRect(this.particles[i].x, this.particles[i].y, 5, 5);
} else {
  this.particles[i].x = this.center.x;
  this.particles[i].y = this.center.y;
}

And VOILÁ! If you run that example you'll see that we now have a fully functional particle generator.

Here's what you should have so far:

// particles.js
(function() {

  var Particle = function(x, y, vx, vy) {
    this.x = x || 0;
    this.y = y || 0;
    this.vx = vx || 0;
    this.vy = vy || 0;
    
    this.update = function (vx, vy) {
      vx = vx || 0,
      vy = vy || 0;

      this.x += this.vx + vx;
      this.y += this.vy + vy;
    };
  };

  var ParticleSystem = function(container, center, count) {
    var i = 0,
        c = container.getContext('2d');

    count = count || 0;

    this.particles = [];

    this.center = {
      x: center.x || 0,
      y: center.y || 0
    };

    // Initialization
    for ( ; i < count ; ++i ) {
      var x = this.center.x,
          y = this.center.y,
          vx = Math.random() * 3 - 1.5,
          vy = Math.random() * 3 - 1.5;

      this.particles.push(new Particle(x, y, vx, vy));
    }

    this.update = function() {
      for ( i = 0 ; i < count ; ++i ) {

        // We don't want to process particles that
        // we can't see anymore
        if (this.particles[i].x > 0 &&
          this.particles[i].x < container.width &&
          this.particles[i].y > 0 &&
          this.particles[i].y < container.height) {

          this.particles[i].update();

          c.fillRect(this.particles[i].x, this.particles[i].y, 5, 5);
        } else {
          this.particles[i].x = this.center.x;
          this.particles[i].y = this.center.y;
        }
      }
    };
  };


  // shim layer with setTimeout fallback by Paul Irish
  // Used as an efficient and browser-friendly
  // replacement for setTimeout or setInterval
  window.requestAnimFrame = (function(){
    return  window.requestAnimationFrame ||
    window.webkitRequestAnimationFrame   ||
    window.mozRequestAnimationFrame      ||
    window.oRequestAnimationFrame        ||
    window.msRequestAnimationFrame       ||
    function (callback) {
      window.setTimeout(callback, 1000 / 60);
    };
  })();

  // Call the init() function on load
  document.addEventListener('DOMContentLoaded', init);

  function init() {
    // Get a reference to the canvas object in the HTML
    var cobj = document.getElementsByTagName('canvas')[0],
        c = cobj.getContext('2d'),
        p = null;

    // Make the canvas have the same size as
    // the browser window
    cobj.width = document.body.clientWidth;
    cobj.height = document.body.clientHeight;

    // Set the colour to white
    c.fillStyle = '#FFFFFF';

    p = new ParticleSystem(cobj, { x: cobj.width/2, y: cobj.height/2 }, 1000);

    // Call the paint method
    paint();

    function paint() {
      c.clearRect(0, 0, cobj.width, cobj.height);

      p.update();

      // Call paint() again, recursively
      requestAnimFrame(paint);
    }
  }
})();

Now that we have this thing working it's time to have fun with it. For your convenience, I've made some variations of the code shown above and uploaded them as jsfiddles.

I'd love to see your creations! Post them in the comments below!