Boids Murmuration
Up to three thousand six hundred birds, each one watching only what is within about thirty pixels of itself. Nobody leads, nobody can see the shape, and the flock thickens into sheets, folds along its own lanes and tears open anyway.
About this piece
Every bird on this plate runs the same three rules and knows nothing else. Separation: back away from anyone closer than 0.82 of the mean spacing. Alignment: match the average heading of everyone inside the perception radius. Cohesion: drift toward where those same neighbours are. That is the whole model, and it is Craig Reynolds’ from 1987. There is no leader, no target, no plan, and no bird holds a variable describing the flock — the shape you are watching exists only in your eye.
On this page’s stage the flock is 3,600 birds at a mean spacing of 16.0 px, with a perception radius of 2.15 spacings — 34.4 px. A gallery thumbnail runs the identical model at 10.5 px spacing and a few hundred birds, because the count follows canvas area and then every length is measured in spacings; a tile is a small flock rather than a crop out of a big one.
Neighbour lists come from a uniform spatial hash rebuilt every frame at exactly the perception radius, so a bird only ever tests the 3×3 block of cells around itself — 41.6 candidate birds on average, against 3,599 for the naïve loop. That is the difference between a plate that costs 5.4 ms a frame at full size and one that costs about four hundred.
Locally almost unanimous, globally barely agreed
The single most useful number for a flock is polarisation: average every bird’s heading as a unit vector and take the length of the result. One means a squadron in formation; zero means a milling swarm. Measured over 400 frames on this stage, this roost’s global polarisation wanders between 0.03 and 0.38 and never settles. Take the same measurement over each bird’s own neighbours instead and it is 0.87.
That gap is the piece. Any two birds standing near each other are pointing almost the same way; the flock as a whole is pointing nowhere. It is what a roost over its own wood looks like — birds milling, turning, doubling back — rather than a skein of geese crossing a county, which would read near 1.0 and would be a much duller picture.
Two things that are authored, and why
Plain boids does not make this picture, and it is worth being blunt about that. Run the three rules alone in a box and you get one of two disappointments, both of which were built and thrown away here: an evenly stirred soup with no structure at all, or — with cohesion turned up — the flock shattering into eight or twenty separate shoals with bare paper between them, each too far from the rest to ever find them again. So the plate adds exactly two things on top of Reynolds.
A steering field, applied as a rotation. Three slowly-drifting sinusoids in x, y and x+y produce a smooth number at every point, and that number is used to turn a bird’s already-chosen heading by up to 0.55 rad. The obvious version — making the field a direction and steering toward it — was written first and is wrong: a direction field is a conveyor, and wherever it diverges it empties that patch of sky permanently. Within a minute the roost had become a wreath with a hole in the middle. A rotation cannot pump birds anywhere. It only curves paths, which is enough to fold the flock into lanes and squeeze it into sheets.
A soft elliptical containment. The frame has to hold the flock, and the tempting way to do that is a pull toward the flock’s own centroid. That fails the same way: every bird then orbits one point, the orbits phase-lock, and you get the wreath again. The containment here is an ellipse at 0.385 of the frame on each axis whose force is exactly zero inside it. There is no centre to orbit. Birds feel it only on the way out, and 88.9% of them are inside it at any moment; the rest are the wisps hanging off the edge.
The hawk is the third addition and the only one that is theatre rather than structure. Every 620 frames — about 10.3 seconds — a repulsor crosses the roost on a chord whose bearing advances by the golden angle each pass, and it is out for 4.3 s of that. It tears a hole roughly a third of the frame across, and the flock closes it behind him.
Reading the plate: what to watch for
- The lanes are alignment, not a drawing. Where a run of birds is heading the same way their streaks lie parallel and read as a comb. Nothing draws a lane; a lane is what 0.87 local polarisation looks like from outside.
- The dark knots are real density, not a shading trick. Ink weight and alpha both follow how many neighbours a bird can see. A scatter at mean density would put 14.5 birds inside a perception disc. Measured here, the tenth percentile sees 17, the median 35–42 and the ninetieth 60–82 — so the sheets genuinely run about three times the flock’s own average density, and the wisps genuinely run below it.
- The wide opening that drifts across is the hawk. Give it ten seconds and it will happen again from a different bearing. If the hole is small and sits still, it is not the hawk — it is a divergence in the steering field, and it will close on its own.
- Coral is not a role. One bird in seventeen is coral, chosen once when the flock is seeded and never again. It marks nothing, leads nothing and behaves identically. It is there so the flock reads in this site’s ink family, and it doubles as a tracer — pick one and you can follow a single bird through a fold.
- Birds sit much closer than the mean spacing. The median bird’s nearest neighbour is 0.46 spacings away, not 1.0. Separation stops them touching; it does not spread them out. That is why the flock reads as a cloud with texture rather than as a lawn of evenly-planted dots.
- You never see the first frame. Sixty settling steps run before anything is painted, because a freshly seeded flock is unstructured dust and worth nobody’s time. Global polarisation climbs from 0.03 at seeding to about 0.25 by frame 60, which is roughly where the lanes become visible.
Colour, weight and how the picture is put together
A bird is drawn as a short streak, not a dot: a line from 3.6 velocities behind it to 0.8 in front. The streak is what makes a heading legible at all — a field of dots at this size carries no direction, and direction is the entire subject.
Alpha and line width both ramp with local density, which is what gives the flock a dark core and a wispy edge instead of one flat grey. The ramp is normalised near the measured ninetieth percentile (70 neighbours) rather than at the uniform expectation of 14.5; normalising at 14.5 clips nine birds in ten to full weight and paints exactly the flat grey it was meant to avoid.
The whole flock is painted in six batched passes, not bird by bird. Each bird is sorted into one of five density buckets plus a coral bucket, and each bucket is one beginPath() and one stroke(). Setting strokeStyle per bird would mean 3,600 state changes a frame; this way it is six.
Not one colour is named in the drawing code. Both ink triplets and the background are handed in by the caller, from the shared palette — so the embed follows the reader’s light or dark setting, the catalogue tile follows the tile frame, and the two cannot drift into different pictures.
Under prefers-reduced-motion no animation loop is scheduled at all. The sixty settling steps still run, so a visitor who asked for stillness is served a settled murmuration as a still frame rather than a blank plate or a sheet of unstructured dust.
Honest limits
Real starlings do not do it this way, and the difference is well documented. The STARFLAG group (Ballerini et al., PNAS 2008) reconstructed thousands of birds in three dimensions over Rome and found that a starling interacts with a fixed number of neighbours — six or seven — regardless of how far away they are. Interaction is topological, not metric. This plate is metric: a fixed radius, inside which a bird in a thick sheet sees 35–42 others and a bird on a wisp sees 17. That is not a small difference. Topological flocks hold together through density changes; metric ones fragment, which is precisely why this one needs a containment ellipse to stay in one piece and a real flock does not.
It is two-dimensional. A murmuration is a volume, and most of what makes a photograph of one beautiful — the shading where the flock is deep, birds occluding birds, the silhouette turning edge-on and nearly vanishing — is depth. There is no depth here. The density variation is real, but it is density in a plane.
The structure is helped. The lanes, the sheets and the tears come from the steering field as much as from the birds. Reynolds’ three rules are all really here and all really doing their share, but if you switch the field off you get the even soup described above. This is a plate that uses boids; it is not a demonstration that boids alone produces murmurations, and the literature is clear that in nature something closer to the opposite is true.
The hawk is a shape, not a predator. It is a moving circle of repulsion on a fixed schedule. It does not hunt, does not aim at the dense part of the flock, and nothing is ever caught. Real evasion waves propagate through a murmuration faster than any individual bird turns; nothing here does that.
The roost never lands. The best part of a real murmuration is the end, when the whole cloud pours into the reeds in twenty seconds. This one mills indefinitely. The loop has no beginning and no end, which makes it easy to leave running and a poor documentary.
Draw your own
The three rules are older than this site by decades. Craig Reynolds set them out in Flocks, Herds, and Schools: A Distributed Behavioral Model, published in Computer Graphics 21(4), the SIGGRAPH ’87 proceedings, pages 25–34; his own summary is to steer to avoid crowding local flockmates (separation), towards their average heading (alignment) and towards their average position (cohesion).
The snippet below is those three rules and nothing else, in 25 lines of HTML and plain JavaScript with no library: no steering field, no containment, no hawk. 120 boids live on a 400 × 260 px canvas whose edges wrap round, so a bird leaving on the right comes back on the left and measures its neighbours the short way across the seam. Each frame, every bird looks at the flockmates within R = 50 px (about 9 of them at the random start, about 25 once the flock has gathered), and changes its velocity by three terms:
- Alignment: 8% of the gap between its own velocity and the neighbours’ mean velocity (ALI = 0.08).
- Cohesion: 0.1% of the offset to the neighbours’ mean position (COH = 0.001), a pull of at most 0.05 px per frame, deliberately gentle so that birds drift together rather than collapse into a knot.
- Separation: a push of 0.05 px per frame away from each bird closer than 16 px (SEP = 0.05).
Then the speed is clamped to between 1.5 and 3 px per frame, so nobody stalls or bolts, and every bird is drawn as a stroke 3.5 px wide and six velocities long (9 to 18 px), pointing the way it flies. One bird in seventeen is coral, as on the plate. The cobalt, coral and ground are the ones this page’s animation reads from the site palette, light or dark to match your device. Save it as an .html file and open it.
<canvas id="flock" width="400" height="260"></canvas>
<script>
const dark = matchMedia('(prefers-color-scheme: dark)').matches; // follow the OS theme
const [bg, cobalt, coral] = dark ? ['#0B0D12', '150,180,255', '255,120,84']
: ['#E7E2D5', '40,72,205', '190,68,28'];
document.body.style.background = bg;
const ctx = document.getElementById('flock').getContext('2d');
const W = 400, H = 260, R = 50, SEP = 0.05, ALI = 0.08, COH = 0.001, MIN = 1.5, MAX = 3;
const B = Array.from({length: 120}, () => ({x: Math.random() * W, y: Math.random() * H, vx: Math.random() * 4 - 2, vy: Math.random() * 4 - 2}));
const wrap = (d, L) => d > L / 2 ? d - L : d < -L / 2 ? d + L : d; // shortest way across a wrapped edge
function step() {
for (const b of B) { let n = 0, sx = 0, sy = 0, ax = 0, ay = 0, cx = 0, cy = 0;
for (const o of B) { const dx = wrap(o.x - b.x, W), dy = wrap(o.y - b.y, H), d = Math.hypot(dx, dy);
if (o === b || d > R) continue; // only flockmates within R px count
n++; ax += o.vx; ay += o.vy; cx += dx; cy += dy; // their mean heading and mean offset
if (d < 16) { sx -= dx / (d || 1); sy -= dy / (d || 1); } } // separation: push off the too-close
if (n) { b.vx += (ax / n - b.vx) * ALI + cx / n * COH + sx * SEP; b.vy += (ay / n - b.vy) * ALI + cy / n * COH + sy * SEP; }
const s = Math.hypot(b.vx, b.vy) || 1, k = Math.min(MAX, Math.max(MIN, s)) / s; b.vx *= k; b.vy *= k; } // speed clamp
for (const b of B) { b.x = (b.x + b.vx + W) % W; b.y = (b.y + b.vy + H) % H; } // wrap-around edges
}
function draw() { ctx.clearRect(0, 0, W, H); ctx.lineWidth = 3.5; ctx.lineCap = 'round';
B.forEach((b, i) => { ctx.strokeStyle = 'rgb(' + (i % 17 ? cobalt : coral) + ')'; ctx.beginPath(); ctx.moveTo(b.x - 6 * b.vx, b.y - 6 * b.vy); ctx.lineTo(b.x, b.y); ctx.stroke(); }); }
const still = matchMedia('(prefers-reduced-motion: reduce)').matches; // still: settle 300 steps, draw once
if (still) { for (let i = 0; i < 300; i++) step(); draw(); } else (function frame() { step(); draw(); requestAnimationFrame(frame); })();
</script>
What to expect, measured by running the snippet’s own step() 300 times over, each from a fresh random start: the flock’s polarisation (the length of the average unit heading, the number described above) starts near 0.1 and passes 0.6 after a median of about 100 frames, under two seconds at 60 frames a second. By frame 300 the median run reads 0.99, the whole flock streaming one way. About one run in thirty is still below 0.6 at frame 300: the flock has split into bands crossing the wrapped canvas in different directions, and on a torus with no hawk or field to break them up they can travel like that for a long time. Under prefers-reduced-motion it runs 300 steps without drawing, paints that settled flock once and stops.
Where to take it: set ALI and COH to 0 and only separation is left; the birds keep their random headings for good and polarisation stays around 0.1. What the snippet does not do: every bird compares itself against every other, 14,280 distance checks a frame at 120 birds but nearly a million at 1,000, which is why the plate above sorts its 3,600 birds into a spatial hash first; it updates the birds one at a time in place, so later birds in the list see earlier birds’ new velocities this frame, a small bias a careful version removes by writing into a second array; its speed is counted in frames, so it runs twice as fast on a 120 Hz screen; it reads your light or dark setting once, when the page loads; and it draws at 400 CSS pixels without scaling for high-density screens, so the strokes are slightly soft on a phone or Retina display.
Sources: Craig Reynolds’ Boids page (the three rules, quoted above) and his online copy of the 1987 paper (its citation), both read 2026-10-01. The polarisation figures are measured from the snippet’s own step().
Curious how the loop and canvas fit together? Read how it works →
More from the gallery
- Plate 26 Contour Drift A survey of land that is not there, redrawn as it moves.
- Plate 27 Fourier Epicycles Forty-nine circles on one arm, drawing the shape they are made of.
- Plate 28 Rule Thirty Cascade One live cell and a three-bit rule, falling into a pattern that never repeats.
- Plate 29 Substrate Cracks Straight cracks that spawn cracks at right angles and stop dead on each other.
- Plate 30 Double Pendulum Two dozen double pendulums let go a hair apart, falling out of step into chaos.
- Plate 31 Game of Life Two rules on a soup of cells: births in coral, survivors in cobalt, the dead as fading ghosts.