Back to experiments
CANVAS 2D

Skittish Shoal

A pool of tiny fish that swim in from the edges and drift on a noise-driven current. Sweep your pointer through and they dart away like fish from a finger — hold it still and they carry on, unbothered.

Sweep your pointer through the pool — the fish scatter from the motion, then rejoin the current when you hold still.
ABOUT
TECH STACK
JavaScript Canvas 2D API requestAnimationFrame Layered sine noise
YEAR
2026

What I did

I wanted a calm, self-contained motion piece that could sit behind a hero or stand on its own, with zero dependencies — just a single <canvas> and a bit of maths.

The build was about tuning: fish count, size, and turn rate until the movement felt like a shoal drifting on a current rather than random noise. The whole thing runs in well under 100 lines and stays smooth by keeping per-frame work cheap.

How it works

Each fish steers toward a divergence-free flow field — the curl of a smooth noise potential rather than the noise itself. Because an incompressible field neither squeezes nor spreads the flow, the shoal keeps an even spacing and never collapses into hard lines. Each fish turns gradually toward that direction with a little wander, so they drift like a real shoal.

The canvas is fully repainted every frame, and each fish is drawn as a small ellipse body with a short tail, angled to face wherever it's swimming. Near the pointer they're pushed away by a force with a quadratic gradient — barely felt at the edges but sharply stronger up close. Crucially, that force is scaled by how much the pointer is moving: a still cursor stirs nothing, like holding your finger still in the water, while sweeping it through scatters them — and because each re-reads the flow every frame, they settle back the moment you stop. New fish always swim in from off the edges, and the shoal holds a steady population — a fish is only removed once it drifts off the canvas, which frees a slot for the next one to enter.