Js Particles

An Intro to JS Particles, Pt. 1

Full vanilla javascript particle walkthrough and implementation

About

This article will describe how you can implement a particle system in pure/vanilla javascript for use in web/html games or other web-based/browser projects.

Meet Pete

So Pete the Particle is a happy little particle running in the canvas shown above. Feel free to poke him if he isn’t showing up very well. But you may be wondering… What exactly is a particle and why would I want to implement my own? First, in my perspective at least, a particle is simply a procedurally-generated visual effect that can be added to a game (or any other visual project). Think of a shower of sparks when you shoot an enemy, a trail of dust that kicks up when your character runs, even footsteps that your character may leave in sand or snow. These can all be implemented through particles. And while there are other ways to implement visual effects, I’ll present you some advantages to using a particle system, which allows you to fire and forget visual effects so that your game logic doesn’t need to keep track of animation or effect state.

Game Hooks

Let’s take a closer look at Pete and how Pete is integrated into the game loop. To understand how the particle logic works, we need to understand how it will be hooked into your game. The game model I’m using here assumes that all logic is tied to Animation Frames. In each frame, you are making updates to your game model and then rendering out the results to display in your browser. The particle logic will follow this same model.

I’ll assume that your game loop looks something like this:

var lastUpdate = performance.now();
const maxDeltaTime = 100;
const updateCtx = { deltaTime: 0 };
const canvas = document.getElementById("canvas");
const renderCtx = canvas.getContext("2d");
function loop(hts) {
    updateCtx.deltaTime = Math.min(maxDeltaTime, hts - lastUpdate);
    lastUpdate = hts;
    update(updateCtx);
    render(renderCtx);
    loopID = window.requestAnimationFrame(loop);
}

Where update(updateCtx) and render(renderCtx) are your entry points to your game logic. We’ll use these same entry points for operations on particles. While you could get away with a single function call, there’s reasons breaking them out makes sense. Specifically there may be cases where you want to update the state of particles (e.g.: position and such), but don’t want to render it (e.g.: offscreen or not visible). Also note that I’m passing in a few variables which will be important to particle operations. updateCtx is an object that contains a deltaTime attribute, and renderCtx is the canvas context onto which stuff will be rendered. We’ll see how these are used when we dive deeper into what makes Pete tick. Let’s assume that your main update and render methods have been updated to call the particle’s entry points, as shown here:

const pete = new PeteParticle();
update(updateCtx) {
    ...
    pete.update(updateCtx);
    ...
}
render(renderCtx) {
    ...
    pete.render(renderCtx);
    ...
}

Particle Logic

To implement Pete, we are going to introduce a class to store some data and define the game hooks to allow updates and rendering of the particle. What data is needed to model the behaviour we are seeing? Well, there’s a position on the screen to start with (let’s call that x,y coordinates), and the shape of the particle, which is a filled circle which can simply be represented by a radius (size) as well as a color associated with the particle (color). Those are all static properties, but what about the behaviour? Well, the particle is moving. There’s lots of ways we could represent that, but the easiest is to just consider the change of position based on x and y axis over time. Let’s call the change in x, delta X and the change in y, delta Y (dx and dy for short). Finally, when the particle hits the edge of the canvas it changes direction. We need some logic to keep track of the bounds of the canvas. This can be represented by a minimum and maximum x value (minx, maxx) and same for a y value (miny, maxy). Putting that into a class structure and initializing looks like:

class PeteParticle {
    constructor(spec={}) {
        // current position
        this.x = spec.x || 0;
        this.y = spec.y || 0;
        // deltas for x,y - represents velocity in pixels per second
        this.dx = (spec.dx || 50) * .001;
        this.dy = (spec.dy || 50) * .001;
        // size of particle
        this.size = spec.size || 5;
        // color of particle
        this.color = spec.color || "red";
        // bounds
        this.minx = spec.minx || 0;
        this.miny = spec.miny || 0;
        this.maxx = spec.maxx || 400;
        this.maxy = spec.maxy || 400;
    }
}

Two notes here:

  1. I like to use spec objects in constructors. It allows you to easily change the calling parameters for object creation (which we will do later) as well as allows you a way to easily specify defaults for object creation, which I’ve done here (spec.x || 0 becomes undefined || 0 which becomes 0 when spec.x is undefined). So you can make a PeteParticle just by calling new PeteParticle() and you will get something that works.
  2. You’ll note a .001 multipled to the dx and dy values. If you look back at the game loop, the deltaTime attribute being passed to the update function is computed in milliseconds. I like to think of velocity in terms of pixels per second, so to get to pixels per millisecond, multiply by .001.

Now that we have data, let’s take a closer look at the update and render methods that are needed to get Pete moving and rendering. If you remember, update is being passed a context that includes a deltaTime attribute which represents the amount of time in milliseconds since the last update was called. This is the elapsed time since we’ve last made any changes to Pete. In the data for Pete, we expressed movement by keeping track of a delta x and y which represented the number of pixels of movement per unit time (millisecond in our case, remembering we converted dx and dy in seconds by multiplying by .001 converting to milliseconds). So to get how far Pete should have traveled since the last update, all we need to do is multiply the dx and dy values by deltaTime and then store the results. Take a look:

update(updateCtx) {
    let dt = updateCtx.deltaTime;
    // update position
    this.x += Math.round((this.dx * dt));
    this.y += Math.round((this.dy * dt));
}

Pretty simple right? Note: I’m rounding the change in position I’m adding to x and y as a best practice to keep x and y as integers. Floats work too, but are a bit more computationally heavy when rendering. This is enough to get Pete moving, but Pete would quickly become lost as he wanders off the screen we’ve provided. To keep Pete safe (and always visible) we’re going to add a fence that tells Pete how far he can go in any direction. If he goes to far in any direction, we’ll tell him to turn around and go in the opposite direction at the same speed. From our data model for Pete the fence was represented by the minx, miny, maxx, maxy variables. Let’s take a look at how to implement the fence:

update(updateCtx) {
    let dt = updateCtx.deltaTime;
    // update position
    this.x += Math.round((this.dx * dt));
    this.y += Math.round((this.dy * dt));
    // update direction
    if (this.x <= this.minx) this.dx = Math.abs(this.dx);
    if (this.y <= this.miny) this.dy = Math.abs(this.dy);
    if (this.x >= this.maxx) this.dx = -(Math.abs(this.dx));
    if (this.y >= this.maxy) this.dy = -(Math.abs(this.dy));
}

The logic for the fence is pretty straightforward. If Pete’s position ever drops below a minimum or above a maximum, we change the corresponding position delta to be negative or positive to turn Pete around. Implementation Note: using Math.abs is required here vs. just flipping the sign of the delta (e.g.: this.dx = -this.dx). Remember that the delta time passed in is variable as it is the actual milliseconds since last call. For example, say your dx is -5 and current x position is 5 and you are passed a deltaTime of 10. Your new position would be calculated as this.x += -5*10 or this.x = 5 - 50 or this.x = -45. Assuming our minx is 0, -45 is well under this, so we would swap the sign of dx which would now be 5. Now say the next frame’s deltaTime is 5. Using the same computation, this.x += 5*5 or this.x = -45 + 25 or this.x = -20. Uh oh… this is still below our minumum minx, so if we were just swapping the sign, we would send the particle back in the wrong direction. I actually had this bug when I first implemented ;p.

Final step is to figure how to actually draw Pete. Using Javascript primitives, this is actually pretty easy. Pete is just a filled circle with a specific color, so we’ll use the Arc primitive:

render(renderCtx) {
    renderCtx.beginPath();
    renderCtx.arc(this.x, this.y, this.size, 0, Math.PI*2);
    renderCtx.fillStyle = this.color;
    renderCtx.fill();
}

Let’s break this down. First renderCtx.beginPath() (reference: BeginPath) is used to start a new rendering path in Javascript. Rendering paths in Javascript allow you to build out a shape using multiple primitives (like lines, arcs, rectangles, etc) before doing a single render/draw call for the entire path you’ve laid out. Here, we’re only using a single primitive, so it doesn’t buy us much, but is still needed to setup our render state. renderCtx.arc(this.x, this.y, this.size, 0, Math.PI*2) is where we are telling Javascript to draw our filled circle. We pass in Pete’s position using this.x, this.y, this.size is the radius of the circle (in pixels) which we set in our constructor, and the 0, MathPI*2 is used to identify the start and end angles (in radians) of the arc to draw (0 to 2*PI is a full circle). renderCtx.fillstyle = this.color is used to set the color of the circle (reference: FillStyle), which can be named colors like red or black, RGB hex values like #808080 for a gray, or a RGB color string like rgb(127,127,127) (reference: CSS Color Value). And finally, the renderCtx.fill() call renders the path, our circle, to the canvas (reference: Fill).

Putting It Together

The last piece of the puzzle is the initialization code for Pete. You’ll note that in the first introduction to Pete and the canvas below, Pete starts at a random location and heads off in a random direction and a fixed speed. We also need to define what the fence parameters should be. Let’s start with the fence, as it’s easiest. We will set Pete’s boundaries to simply be the boundaries of the canvas we are using. So spec = { minx: 0, miny: 0, maxx: canvas.width, maxy: canvas.height } will do fine. Starting position also is easy. We’ll pick a random number between 0 and 1 (using Math.random()) and multiply by the canvas height and width, as that’s the bounds we are using for Pete. For handling movement in a random direction but at constant speed. I pick a speed, say 200 (measured in pixels per second), then pick a random angle. To get the x, y deltas, I simply use Math.sin(angle) and Math.cos(angle). Let’s put this all in a function:

function makePete() {
  let speed = 200;
  let angle = Math.random() * Math.PI * 2;  // angle is in radians, so full circle is PI * 2
  petes.push(
    new PeteParticle({
      x: Math.random() * width,             // random starting location
      y: Math.random() * height,
      dx: speed * Math.cos(angle),          // starting deltas/movement based on angle
      dy: speed * Math.sin(angle),
      color: "#A9BCD0",                     // picking a color
      maxx: width,                          // setting fence max values (min default to 0)
      maxy: height
    })
  );
}

By putting this in a function, we can now call it multiple times to get multiple particles, all following the same rules. The only thing we haven’t accounted for is keeping track of our Pete particles. In the above function there is a petes.push() call. I’m using an array named petes, allocated by using var petes = [];. So that will need to be declared prior to calling our makePete() function:

var petes = [];
for (let i=0; i<5; i++) {
    makePete();
}

This handles creating five Pete particles (yay, he now has friends!). But we need to update our update() and render() functions accordingly:

update(updateCtx) {
    for (const pete of petes) {
        pete.update(updateCtx);
    }
}
render(renderCtx) {
    for (const pete of petes) {
        pete.update(renderCtx);
    }
}

So now we have Pete and a couple of friends. Use the +friend button to add more.

I’ll stop here for now, as I ended up being long winded in explaining everything. And there’s still quite a bit of ground to cover. Hopefully I’ve outlined some basic concepts: a particle really is some data and rules for updating and rendering. Logic for initialization lies outside the particle and that we need to be able to hook to the main game loop. We still need to get to the “fire and forget” point I raised at the beginning of the article. And I still want to cover more on the creation and expiration of particles and ways to handle that. And while I’m sure Pete is a fine particle, there’s a lot more we can do and I’ll provide some more exciting examples. Look for these topics to be covered in more detail in coming posts.

The full code for this example is below, or you can play around with it for yourself using this CodePen.

var lastUpdate = performance.now();
const maxDeltaTime = 100;
const updateCtx = { deltaTime: 0 };
const canvas = document.getElementById("canvas");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const width = canvas.width;
const height = canvas.height;
const renderCtx = canvas.getContext("2d");
const petes = [];
function loop(hts) {
  updateCtx.deltaTime = Math.min(maxDeltaTime, hts - lastUpdate);
  lastUpdate = hts;
  update(updateCtx);
  render(renderCtx);
  window.requestAnimationFrame(loop);
}
function update(updateCtx) {
  for (const pete of petes) {
    pete.update(updateCtx);
  }
}

function render(renderCtx) {
  renderCtx.clearRect(0, 0, width, height);
  for (const pete of petes) {
    pete.render(renderCtx);
  }
}

window.requestAnimationFrame(loop);

class PeteParticle {
  constructor(spec = {}) {
    // current position
    this.x = spec.x || 0;
    this.y = spec.y || 0;
    // deltas for x,y - represents velocity in pixels per second
    this.dx = (spec.dx || 50) * 0.001;
    this.dy = (spec.dy || 50) * 0.001;
    // size of particle
    this.size = spec.size || 5;
    // color of particle
    this.color = spec.color || "red";
    // bounds
    this.minx = spec.minx || 0;
    this.miny = spec.miny || 0;
    this.maxx = spec.maxx || 400;
    this.maxy = spec.maxy || 400;
  }
  update(updateCtx) {
    let dt = updateCtx.deltaTime;
    // update position
    this.x += Math.round(this.dx * dt);
    this.y += Math.round(this.dy * dt);
    // update direction
    if (this.x <= this.minx) this.dx = Math.abs(this.dx);
    if (this.y <= this.miny) this.dy = Math.abs(this.dy);
    if (this.x >= this.maxx) this.dx = -Math.abs(this.dx);
    if (this.y >= this.maxy) this.dy = -Math.abs(this.dy);
  }

  render(renderCtx) {
    renderCtx.beginPath();
    renderCtx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
    renderCtx.fillStyle = this.color;
    renderCtx.fill();
  }
}

function makePete() {
  if (petes.length >= 50) return;
  let speed = 200;
  let angle = Math.random() * Math.PI * 2;
  petes.push(
    new PeteParticle({
      x: Math.random() * width,
      y: Math.random() * height,
      dx: speed * Math.cos(angle),
      dy: speed * Math.sin(angle),
      color: "#A9BCD0",
      maxx: width,
      maxy: height
    })
  );
}

for (let i = 0; i < 25; i++) {
  makePete();
}

GENERAL
gamedev, js, particles, procgen