clickable, live-demo web app.
🌐 Live demo: https://sen.ltd/portfolio/boids-flocking/
📦 GitHub: https://github.com/sen-ltd/boids-flocking
Boids flocking
Emergence from three local rules
Craig Reynolds' 1986 "boids" showed that flocking needs no leader and no global
plan. Each agent looks only at its nearby neighbours and applies three steering
rules; the coordinated flock is an emergent result, not a programmed one:
-
Separation — steer away from neighbours that are too close.
-
Alignment — steer toward the average heading of nearby boids.
-
Cohesion — steer toward the average position of nearby boids.
Each rule produces a Reynolds steering force — the desired velocity (at top
speed) minus the current one, clamped to a maximum:
function steer(desired: Vec, vel: Vec, p: Params): Vec {
if (mag(desired) === 0) return { x: 0, y: 0 };
return limit(sub(setMag(desired, p.maxSpeed), vel), p.maxForce);
}
The three forces are computed against neighbours found by distance and summed
with the weights from the sliders — with separation weighted by inverse distance
so the closest neighbours push hardest.
The engine is pure and tested
All the vector and steering math lives in src/boids.ts with no reference to the
DOM, so it's unit-tested directly (12 vitest cases). The tests pin the direction
of each rule (two close boids push apart; a boid turns toward its neighbours'
heading; a diffuse ring contracts under cohesion), that step keeps every boid
inside the toroidal world and under the speed cap, and that it never mutates its
input:
it('a diffuse cloud contracts under cohesion', () => {
// ...60 ticks of cohesion-only steering...
expect(spread(world)).toBeLessThan(spread(boids));
});
That "pure core, thin shell" split means the flocking behaviour can be verified
in CI without launching a browser.
The canvas shell is thin
The front-end in src/main.ts owns only the animation loop, rendering (each boid
a triangle coloured by heading, with motion trails), the control panel, and
drag-to-scatter interaction; the simulation itself is the imported pure function.
One bug worth noting: a devicePixelRatio canvas-scaling mistake made drawing
fill only the top-left quarter on Retina. Setting both canvas.width/height
(the backing resolution) and canvas.style.width/height (the logical size) fixed
it — a classic hi-DPI trap, caught by screenshotting at deviceScaleFactor 1 and 2.
src/boids.ts — pure model: vectors, the three rules, toroidal step()
src/boids.test.ts — 12 vitest cases for the steering math
src/main.ts — canvas render loop, controls, pointer interaction
Takeaway
Boids is the classic demonstration that global order can emerge from simple
local rules. The keys are expressing each rule as a steering force and
factoring the model into a pure, testable engine. After a run of CLIs, this
one is a clickable live-demo web app in TypeScript + Canvas — go drag the
demo and scatter the flock.
🌐 Live demo: https://sen.ltd/portfolio/boids-flocking/
📦 GitHub: https://github.com/sen-ltd/boids-flocking