A Snowstorm in Canvas

November 11, 2021

I wanted to get to know Canvas, and I thought a simple particle effect would make a nice first project.

The particles are stored as position, speed, and size values in a fixed length pool. Whenever a particle goes offscreen, its data is reset to place it at a random position at the start. A pool simplifies things a great deal - the alternative is spawning new particles and pushing them to an array, and filtering the array every frame to remove dead particles, which is far less efficient, in both speed and memory usage.

I initially spawned all particles along the top of an area much larger than the canvas to make the effect uniform across the render space, but that meant the majority of my particles were offscreen. That’s not as bad as it might sound from a performance perspective, but it did turn my particle count into a lie.

So instead, I decided to spawn all particles just outside the top and right edges of the canvas. I thought of two ways to do this:

const topratio = canvas.width / (canvas.width + canvas.height);
let rand = Math.random();

if (rand <= topratio) {
  particle.x = Math.random() * canvas.width;
  particle.y = -canvasSpawnOffset;
} else {
  particle.x = canvas.width + canvasSpawnOffset;
  particle.y = Math.random() * canvas.height;
}

This method uses a random value to determine the particle’s spawn position. If the random number is less than or equal to the ratio top/(top + side), then it spawns at the top. Otherwise, it spawns at the side.

flake.x = Math.random() * (canvas.width + canvas.height);
if (flake.x < canvas.width) {
  flake.y = -canvasSpawnOffset;
} else {
  flake.y = flake.x - canvas.width;
  flake.x = canvas.width + canvasSpawnOffset;
}

This one starts by choosing an x value as though the top and side are a single continuous horizontal line. If x is in the canvas.width part of that line, it’s placed along the top. If it’s in the canvas.height part of the line, x is converted to a y value and the particle is placed on the right side.

I went with the second method because it requires fewer variables, fewer calls to Math.random(), and just feels a bit more clever. It’s also a bit less clear, so if I come back to this in a few months and can’t understand what past-me was thinking, I might just change it to the ratio method.

I think the effect came out pretty nice! I might change it to use a soft circle image instead of drawing a rectangle; I tried using Canvas’s built-in arc and radial gradient functions, but they killed performance.