Inspiration

I did not want to build a 3D website that was 3D for its own sake. I wanted scrolling to mean something — for moving down the page to be the same gesture as turning a page in an argument.

Two things gave me that.

The first was a 2008 PNAS paper by Ballerini et al. They reconstructed the position of every bird in starling flocks above Rome, frame by frame, expecting each bird to track everything within a fixed distance. It doesn't. It tracks a fixed number — its six or seven nearest neighbours — no matter how far away they drift. Topological, not metric. That one distinction is why a flock holds together when it stretches, and why a decision made on one edge crosses ten thousand birds before it has finished being made. I wanted to put that fact on a screen in a way you could feel instead of read.

The second was rage at a homepage. Dulux sells over 1,200 colours. Its homepage opens on a full-screen cookie dialog over a dimmed page, a beige photograph of somebody else's living room, and a tagline in low-contrast grey. Their own history page notes that across two decades of Colour of the Year there has been "a dramatic shift towards neutral, natural colours." The beige isn't an accident — it's a stated drift. A company with twelve hundred colours has spent twenty years talking itself quieter.

So: one essay about emergence, and one argument that a paint company's website should be made of paint.

What it does

Murmuration is a five-chapter essay you scroll through. 16,384 starlings are simulated live — not baked, not a video.

  • The rule of three — separation, alignment and cohesion switch on one at a time as you scroll. The numbers printed beside each rule are the actual shader uniforms. You watch a gas of independent particles become a flock, one rule at a time.
  • Seven neighbours — the camera flies inside the flock.
  • The hawk — your cursor becomes a peregrine. A void opens around it and closes behind it, and a readout counts how many birds are genuinely disturbed.
  • One mind — the flock condenses into a sphere, then spells its own name.
  • Roost — night falls and the shape gives out.

Sound is synthesised from the simulation's own state, and the piece opens on a gate: enter with sound, or enter silently.

Colour Is the Point is an unofficial concept redesign of dulux.co.uk. The background is a live GPU fluid — the page is made of wet paint. Drag your cursor and it smears. Scroll and the wall dries to whichever colour you're reading about. A visualiser repaints a real room to any of thirty-six colours on the frame.

How I built it

Everything on screen is generated in the browser. No 3D models, no photography, no stock anything.

The flock is 16,384 agents living entirely on the GPU. Position, velocity and bank are three floating-point textures ping-ponged through GPUComputationRenderer every frame. The birds are one instanced mesh of nine vertices; heading, banking into turns and wingbeat are all reconstructed in the vertex shader. No per-bird data ever touches the CPU.

Topological coupling, implemented literally. Instead of an O(n²) neighbour search, each agent reads only its 7×7 texture-space neighbourhood. Because neighbourhoods overlap, influence crosses the flock as a travelling wave rather than a broadcast — which is exactly why an alarm started on one edge reaches the far side faster than any individual agent reacts. The rendering technique and the biology turned out to be the same idea.

The paint is a semi-Lagrangian advection pair: a velocity field that carries itself, forced by curl noise (divergence-free, so it swirls rather than blooms) and pushed by cursor impulses; and a dye field carried by that velocity, settling toward the section's colour. No pressure-projection step — the point is paint, not incompressible flow.

The visualiser is a shader, not a gimmick. It converts the room to HSV, finds the wall by hue and saturation, and swaps the hue while keeping the original luminance — so the raking light on the plaster and the shadow under the table survive the repaint, and the floor and timber are left alone.

Scroll speed is an input, not just a position. Fling the page and the flock streams — wind rises 4× and airspeed by half again. On the paint site the swirl goes from 0.34 to 1.58 under a fling. Scrolling became weather instead of a scrubber.

One anime.js v4 timeline drives every continuous value on Murmuration — camera, all five flocking weights, sky palette, bloom, lens dispersion — linked to a ScrollObserver with eased sync. Keyframe times are measured from real DOM section offsets at runtime, so the choreography lands on the right paragraph at any viewport size.

The four interiors on the paint site were generated with Gemini and are the only imagery in either project.

Challenges

centroid is a reserved word in GLSL. One variable name took down the entire simulation with a shader-compile error.

Nothing could ever land on its target. The wordmark formation smeared into an illegible blob for a long time. The cause was a minimum-airspeed clamp — correct for a bird, fatal for a formation — that forced every agent to keep moving, so they orbited their targets forever.

The falcon hit nothing. The interaction silently did nothing and the readout sat at zero. Instrumenting it showed the flock had self-organised into a hollow shell — the nearest bird to the centre was 26 units away — so aiming the predator at the flock's centre put it in the hole. Now the cursor ray strikes the shell surface, which is also where a stooping peregrine actually meets a roost.

An undefined uniform is invisible, not obviously broken. During a redesign a renamed value never reached the state object, so every bird's alpha became NaN and the entire flock silently disappeared. It looked like a tuning problem for three iterations before I checked the object rather than the numbers.

The bug I'm most glad I caught: anime.js was warning No target found seven to nine times per page and I'd been ignoring it. splitText's .lines array is empty synchronously — line-splitting needs measured layout — so animate() received zero targets and every body-copy reveal on both sites had never run. It was invisible in every screenshot, because untouched text just looks like finished text.

What I learned

That the constraint which makes a technique fast can be the same constraint that makes it true. I chose texture-local neighbour sampling to avoid an O(n²) search. It turned out to be a better model of real starling behaviour than the "expensive" version would have been.

And that screenshots are not verification. Almost every real defect in this project — the dead reveals, the invisible flock, the falcon hitting nothing — looked completely fine in a screenshot. The ones I found, I found by chasing a warning, reading back GPU pixels, or measuring a number. Looking at the picture only ever caught the problems that were already obvious.

What's next

A shareable permalink for any camera position in the flock, and a WebGPU compute path so the agent count can go up an order of magnitude.

Built With

Share this project:

Updates

Submission history