← Back to VESPERS

How a flock
becomes one animal

VESPERS is a live starling murmuration rendered in canvas 2D. Three thousand birds run real flocking rules; your cursor is a peregrine they ball up and shear away from. Here is how it was built.

The concept

A winter roost at dusk, made from arithmetic instead of choreography.

VESPERS is a fictional field study of the starlings that roost beneath the Old Chain Pier. The single job of the page is to make one idea felt in the body before it is read: that the vast, turning shape overhead has no author — it is thousands of birds each obeying three tiny rules, and fear (your cursor) rewriting them in real time. Everything else on the page stays quiet so the sky can be loud.

Palette · why these four

Dusk, reserved for the sky

#2A3244
Sky slate
The cold upper air just after sunset. Anchors the whole scene and the reading surfaces below the fold.
#C97B84
Dusk rose
The band of afterlight at the horizon. Used for the italic display accent — warmth without shouting.
#E8A85C
Ember
The last of the sun and the peregrine's lock-on ring. Reserved for lines and marks, never small body text.
#12141C
Starling ink
One bird against the light. Density — not shading — is the only tonal control in the flock.

Because ember is bright enough to fail contrast as text, the interface carries two dedicated ink tokens — a lighter ember #F0B978 and rose #E4A4AB — for any accent that has to be read. Muted labels sit at 58% paper over slate, which clears 4.5:1.

Type · Fraunces + Inter

A soft-serif for the birds, a grotesk for the facts

Aa
Fraunces — displayAn "old-style" soft serif with high optical-size contrast. Set large it has the hand-drawn warmth of a naturalist's plate; the italic (used for dark animal and fear) does the emotional work.
Aa
Fraunces Italic — the brave lineThe one piece of typographic bravery: fear set at ~150px in italic over the bold rose "rewrites the rules". A whole section given to five words.
Aa
Inter — everything measurableNumerals, labels, nav and body. Tabular figures in the stat block so counts line up. It keeps the data calm while Fraunces carries the poetry.
Techniques

The signature: a real flock, not a loop

Every bird is a point with a position and velocity in flat Float32Array buffers. Each frame, all birds are hashed into a uniform spatial grid (cell = perception radius) using an allocation-free linked-list (a head/next pair), so a bird only ever tests neighbours in its own cell and the eight around it — the difference between an O(n²) toy and three thousand birds at sixty frames.

From those neighbours each bird computes the three classic Reynolds steers — alignment (match nearby headings), cohesion (drift to the local centre) and separation (hold a wingspan) — plus a slow wandering wind so the idle flock wheels. The peregrine is your cursor: birds within its fear radius get a flee force and a local rewrite of the weights — cohesion is multiplied up and separation relaxed, so the flock genuinely packs into a dark ball on the far side, then shears open along the falcon's line and heals behind it. A click triggers a stoop that punches harder. None of it is scripted; the shapes are emergent.

Canvas 2D — sky gradient, ember afterglow, haze, pier and 3,000 birds batched into two Path2D fills per frame.
Spatial hash — uniform grid + linked-list buckets, rebuilt each frame, zero allocations.
Boids — alignment / cohesion / separation with predator-driven weight rewrites.
Perf — DPR capped at 2, count scaled to viewport area, rAF paused on hidden and off-screen (IntersectionObserver), clamped delta on resume.
Reduced motion — runs 120 silent steps then renders one settled knot; no rAF.
No libraries — Google Fonts only. No three.js, GSAP or matter-js needed.
The three passes

What changed, honestly

01

Craft

  • Caught the recurring failure: the muted label token was 40% paper, computing to ~3.3:1 over slate. Raised to 58% (≈5:1) and lifted the bravery caption to 64%.
  • Reserved ember strictly for lines and marks; gave readable accents their own lighter ink tokens.
  • Softened the pier: pilings were reading like a fence. Replaced flat black with a graded deck, receding piling opacity, and a warm horizon haze so birds no longer meet a hard edge.
  • Added a text-shadow to labels sitting over the animated sky.
02

Depth

  • Enriched the signature with a fading ember dive-trail behind the peregrine, so a fast sweep reads as a stoop.
  • Second-read detail: a handful of birds perched on the rail that occasionally lift off and rejoin the flock — noticed only if you linger.
  • Micro-interaction: the rule cards grow an ember underline and shift their numeral to rose on hover.
  • Tuned the fear response so cohesion overpowers separation under threat — the ball now forms and shears cleanly.
03

Hardening

  • 375px: the nav overran the viewport but was masked by overflow-x — the classic false pass. Section anchors now hide below 560px, leaving Guide + Index fully in-box (verified by geometry, not scrollWidth).
  • Confirmed the reduced-motion path renders a settled, non-blank knot and never starts the loop.
  • Verified DPR cap, resize re-population, visibility + off-screen pause with clamped delta on resume.
  • Chanel rule — removed the hard sun disc; the radial afterglow alone reads more like a sun that has just gone.
  • Final copy read: specific nouns, active voice, no filler.