Particle System
Capstone: build a complete particle system combining everything you've learned.
What IS a Particle System?
A particle system is one of the most rewarding things you can build with Canvas. It manages hundreds or thousands of tiny objects called “particles” that move, change, and eventually disappear. Together, these particles create effects that look like fire, smoke, rain, sparks, confetti, stars, or magic dust.
You have already seen particle systems everywhere, even if you did not know the name:
Real-world examples of particle systems:
- Fireworks exploding in the sky
- Snow falling in a video game
- Sparks flying off a sword in an action game
- The “confetti rain” when you win something online
- Smoke rising from a campfire
- The cursor trail on a flashy website
- Stars streaming past in a space game
The magic: each individual particle is dead simple, just a dot that moves. But hundreds of simple dots following simple rules create complex, organic-looking effects.
The Particle Lifecycle
Every particle system follows the same pattern, every single frame. Think of it like a factory assembly line:
The particle lifecycle, running 60 times per second:
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ BIRTH │────→│ UPDATE │────→│ DEATH │────→│ DRAW │
│ │ │ │ │ │ │ │
│ Create │ │ Move it │ │ Remove │ │ Render │
│ new │ │ Apply │ │ if life │ │ surviving│
│ particle │ │ physics │ │ is <= 0 │ │ particles│
│ with │ │ (gravity,│ │ │ │ │
│ position │ │ wind, │ │ │ │ │
│ velocity │ │ drag) │ │ │ │ │
│ lifetime │ │ Age it │ │ │ │ │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
│ │
└──────────── next frame ───────────────────────────┘ A particle is just a plain JavaScript object with properties. Here is the anatomy of a typical particle:
{
x: 250, // position (where it is)
y: 200,
vx: 30, // velocity (how fast it moves)
vy: -100,
life: 1.0, // 1.0 = just born, 0.0 = dead
decay: 0.5, // how fast it dies (per second)
r: 5, // radius (size)
hue: 200, // color
} Step 1: Your First Particles
Let’s start with the simplest possible particle system: spawn particles at a fixed point, give them random velocities, apply gravity, fade them out, and remove dead ones. This is the foundation for everything else in this lesson.
Try this: Change how many particles spawn each frame: push 3 or 5 instead of 1. Then widen the velocity range to make particles spread further. Small tweaks here create very different visual effects.
Key takeaways from Step 1:
particlesis just a regular array, nothing fancy- Each frame: emit, update, filter dead, draw survivors
p.lifegoes from 1 to 0; we use it for opacity AND size- Delta time (
dt) makes movement speed-independent
Step 2: Mouse-Driven Emission
Instead of a fixed spawn point, let’s emit particles from wherever the mouse is. This instantly makes the effect feel interactive and alive. Notice how we use Math.cos(angle) and Math.sin(angle) to convert a random angle into horizontal and vertical velocity components; cosine gives the x-direction and sine gives the y-direction for any angle in radians.
Emitter Patterns
So far we have emitted from a single point. But real effects need different emission shapes. An emitter defines where and how particles are born. Here are the most common patterns:
Common emitter shapes:
POINT LINE CIRCLE CONE
All from Along a Around a Within an
one spot segment ring angle range
· ───── ╭───╮ · · ·
/|\ ↓↓↓↓↓↓↓ ↗ · · ↖ · · · · ·
/ | \ ↑ ↓ · · · · · · ·
↙ · · ↘ ▲
╰───╯ emitter Forces: Gravity, Wind, and Turbulence
Particles that move in straight lines look boring. Real-world particles are affected by forces: gravity pulls them down, wind pushes them sideways, and turbulence makes them wobble randomly. Adding forces is simple: you just modify velocity each frame.
How forces modify velocity:
Each frame: p.vy += gravity * dt; // gravity: constant downward pull p.vx += wind * dt; // wind: constant sideways push p.vx += (random-0.5) * turbulence * dt; // turbulence: random jitter p.vy += (random-0.5) * turbulence * dt; p.vx *= friction; // friction: gradual slowdown (0.98) p.vy *= friction; Think of velocity as "speed + direction". Forces change the speed and direction a little bit each frame. Over many frames, this creates natural-looking curves.
Attraction and Repulsion
One of the coolest forces is attraction, where particles are pulled toward a point (like gravity toward a planet). The opposite is repulsion, where particles are pushed away. The force gets stronger as particles get closer.
Explosion on Click
Click to trigger a burst of particles. Each explosion sends particles in all directions with fade trails for a dramatic look.
Connecting Particles with Lines (Constellation Effect)
One of the most popular web effects: particles drift gently, and when two particles are close enough, a line is drawn between them. The closer they are, the brighter the line. The result looks like a constellation or neural network.
The algorithm:
For each pair of particles (i, j):
distance = sqrt((xi-xj)^2 + (yi-yj)^2)
if distance < threshold:
opacity = 1 - (distance / threshold)
draw a line between them with that opacity
Note: checking every pair is O(n²). With 100 particles,
that is 4,950 checks per frame : fine for Canvas.
With 1000 particles, it would be 499,500: too slow!
Keep particle count moderate for this effect. Blending Modes (globalCompositeOperation)
By default, new drawings cover what is beneath them. But Canvas supports blending modes that combine overlapping colors in interesting ways. The most useful for particle effects is ‘lighter’, which adds colors together, creating a glowing effect where particles overlap.
Key blending modes for particles:
'source-over' → Default. New covers old. 'lighter' → Additive. Colors add up → bright overlaps. 'multiply' → Colors darken where they overlap. 'screen' → Opposite of multiply, lightens. 'lighter' is the secret ingredient for: - Glowing fire/spark effects - Neon trails - Energy/magic effects - Laser beams Usage: ctx.globalCompositeOperation = 'lighter'; // draw glowy things ctx.globalCompositeOperation = 'source-over'; // reset
Fire Effect
Fire combines upward velocity, warm colors (orange/yellow), size decay, turbulence, and fade trails. The additive blending from the previous section makes it glow. This demo also uses ctx.createRadialGradient() to give each particle a soft, glowing edge. A radial gradient transitions colors outward from a center point to an outer radius, which is ideal for simulating light falloff.
Snow and Rain
Weather effects use the same particle system with different parameters: snow falls slowly and drifts sideways, rain falls fast in straight lines.
Confetti Celebration
Confetti uses rectangular particles that rotate as they fall. The rotation is what makes it look like real paper confetti tumbling through the air. We vary the colors and sizes for a festive look. This demo uses ctx.save(), ctx.translate(), and ctx.rotate() from the transforms lesson to spin each confetti piece around its own center, then ctx.restore() to reset the transform before drawing the next piece.
Starfield (Flying Through Space)
The classic starfield effect simulates flying forward through space. Stars start small at the center and grow larger as they fly outward, creating a sense of depth and speed.
Object Pooling (Performance Concept)
When you spawn thousands of particles, creating new objects every frame and letting garbage collection clean up dead ones can cause performance stutters. Object pooling solves this by reusing dead particles instead of creating new ones.
How object pooling works:
Without pooling: emit() → push new object → GC cleans up dead objects emit() → push new object → GC cleans up (STUTTER!) emit() → push new object → ... With pooling: ┌────────────────────────────────────────────┐ │ Pool: [particle, particle, particle, ...] │ │ │ │ emit() → find a dead particle in pool │ │ → reset its properties │ │ → it is "alive" again │ │ │ │ No new objects created! │ │ No garbage collection needed! │ └────────────────────────────────────────────┘
The Complete System
Here is a full particle system with multiple switchable effects, gravity, trails, and mouse interaction. This combines everything from this lesson. Click to switch between effects.
Performance Tips
How to keep your particle system fast:
- Limit particle count. Set a max (e.g. 500-1000) and stop emitting or remove oldest when you hit it.
- Use object pooling (shown above) for high-throughput systems to avoid garbage collection pauses.
- Avoid per-particle gradients when possible. Flat circles are 5-10x faster than radial gradient circles.
- Use
fillRectinstead ofarcfor square particles; it skips the path building step. - Batch draws. Build one big path with many arcs, then fill once, instead of fill() per particle.
- Skip offscreen particles. Do not draw particles that are outside the canvas bounds.
- Reduce particle count on mobile. Check
navigator.hardwareConcurrencyor test frame rate. - Use
OffscreenCanvasin a Web Worker for heavy particle systems that should not block the UI thread.
What you learned in this lesson:
- A particle system manages arrays of short-lived objects with position, velocity, and lifetime
- The lifecycle: birth (emit) → update (physics) → death (filter) → draw
- Emitter patterns: point, line, circle, and cone shapes
- Forces: gravity, wind, turbulence, attraction, and repulsion
- Additive blending (
globalCompositeOperation = ‘lighter’) for glowing effects - Connecting nearby particles creates a constellation/network effect
- Object pooling recycles dead particles to avoid garbage collection
- Real effects: fire, snow, rain, confetti, starfield, explosions
Drift (Flow Field, macOS Screensaver Style)
One of the most hypnotic particle effects is a flow field. Instead of giving each particle its own velocity, you treat the canvas as an invisible vector field — a grid where every point has a direction. Particles read the direction at their current position and drift along it. The result is smooth, curving, organic trails. macOS’s “Drift” screensaver uses this exact technique with a noise-driven field.
How a flow field works:
Every (x, y) position has an angle. Angle changes smoothly as you move across the canvas. ↘ ↘ → → ↗ ↗ ↘ → → → ↗ ↑ → → ↗ ↑ ↑ ↑ → ↗ ↑ ↑ ↖ ↖ ↗ ↑ ↑ ↖ ↖ ← Each frame, for each particle: 1. Read the angle at its current (x, y) 2. Convert angle → velocity: (cos(a), sin(a)) * speed 3. Move particle 4. Draw a tiny line segment from old → new position The field itself slowly evolves over time, so the curves keep shifting. Particles wrap around edges, giving the illusion of an infinite drifting flow.
The macOS Catalina “Drift” screensaver looks like moving particles but actually isn’t — peek at sandydoo/flux (the open-source tribute) and you’ll see the trick: it’s a fixed grid of lines, where each line points along a velocity field and stretches based on its magnitude. The grid never moves. Everything you see is the field underneath flowing.
And here is the part I missed on my first attempt: that field is not noise. It’s a real fluid simulation — Jos Stam’s “Stable Fluids” algorithm running on a coarse grid. Each frame: noise injects swirling forces at scattered cells, diffusion spreads them, advection has the velocity carry itself (this is what makes a fluid look fluid: disturbances flow downstream instead of wiggling in place), and pressure projection enforces incompressibility (no sources, no sinks). Without advection and projection, you get sine-wave wobble. With them, you get currents, eddies, and the unmistakable plasma look.
Lines sample the velocity field bilinearly at their grid position, compute angle from atan2(vy, vx) and length from |v|, and ease toward those targets so they curl smoothly. The 80-cell-wide fluid grid is small enough that the CPU solver runs comfortably at 60fps in JS.
Try this: Comment out the project() calls in fluidStep and watch the flow lose its incompressibility — currents stop curling and instead pile up or vanish into nothing, exactly like the visual difference between Drift and a noise-only field. Push the force-injection count from 20 to 60 for a violent, hurricane-y fluid. Drop the damping from 0.995 to 0.999 and disturbances persist much longer, building up into massive vortices. Swap the PALETTE for cool blues ([200, 90, 30], [195, 85, 50], [180, 80, 65], [165, 70, 75], [210, 60, 85]) for the Poolside theme.
Where to Go Next
Congratulations. You have built a complete particle system from scratch and learned the core Canvas 2D API! Here are directions to explore next:
- Sprite sheets: animate characters using
drawImage()source cropping instead of drawing shapes - Tile maps: build game levels from grid data, combine with keyboard controls from Lesson 7
- Physics engines: Matter.js adds rigid body physics (bouncing, stacking, constraints)
- Canvas game frameworks: Phaser, PixiJS, Konva for production-grade games
- WebGL / Three.js: 3D graphics using the GPU, orders of magnitude more particles possible
- OffscreenCanvas: render in a Web Worker for better performance on the main thread
- Shader-based particles: move particle physics to the GPU with GLSL shaders for millions of particles
- Procedural generation: generate terrain, trees, caves using noise functions and Canvas
You now have all the building blocks for Canvas graphics. Every game, visualization, and creative coding project uses some combination of what you learned in these 8 lessons: shapes, color, transforms, text, images, animation, interaction, and particle systems. Go build something amazing.