Field Notes · 128

How the ocean conveyor was drawn, one particle at a time.

Gyre renders an invisible field — the global thermohaline circulation — as a living map made of drifting water. Here is the palette, the type, the shader, and the three passes that took it from a scatter of dots to a flowing sea.

The concept / 01

An invisible force, given a body.

The ocean turns over in a single circuit roughly a thousand years long: warm water riding poleward at the surface, cooling, sinking, and creeping back through the abyss. It shapes climate and coastline, yet no one has ever seen it. Gyre is a field piece for the merely curious — a quiet, hypnotic map whose only job is to make that circuit legible and beautiful. A fictional study in the Generative Assets collection, built as a showcase of procedural craft.

Palette / 02

Colour argued from the sea, not chosen for taste.

Two currents, two temperatures, one abyss. The bright teal is reserved for lines and marks; the body copy is set in a derived foam-light ink so small text always clears 4.5:1 on the dark ground.

Abyss
#06121A
The unlit deep. The ground everything floats on.
Current · line/mark
#37B6A6
Cold, oxygen-rich deep return. Used only for currents, rules and glyph strokes.
Warm drift
#D98A3D
Sun-warmed surface water thrown poleward. The amber of the western jets.
Foam
#E6EEF0
Headings and the wordmark. Sea-spray white.
Body ink · derived
#C4D2D4
The legible reading ink — ~12:1 on abyss.
Dim ink
#8FA3A8
Labels and meta. Still ~6:1 — never the bright teal for text.

Type / 03

A calm instrument face against a soft display.

Gyre
Sora · Display

Soft-cornered geometric humanist. Set at 800 for the wordmark and 300 for headlines — the weight jump does the drama so nothing else has to.

17.2 Sv
JetBrains Mono · Readouts

Every number — overturning, petawatts, years — is monospaced and tabular, so the piece reads like a quiet instrument panel.

The signature / 04

65,536 particles, advected on the GPU.

The field is a WebGL2 ping-pong simulation. Every particle's position lives in one texel of a 256×256 RGBA16F float texture. Each frame a fragment shader reads that texture, samples an analytic velocity field at each particle, integrates one step, and writes to a second texture; the two swap. No positions ever leave the graphics card.

The velocity field is pure GLSL: six overlapping gyre terms (clockwise north, anticlockwise south) give the great rotating lenses; two octaves of the curl of value-noise add the organic meander so the sea is never twice the same; a gentle baseline keeps every region alive. A separate point pass draws all 65,536 particles additively into a persistent trail buffer that fades a little each frame — that fade is what turns moving dots into flowing streams. Colour is a temperature field: amber where the water runs warm and shallow, teal where it returns cold and deep. Move a cursor and a soft eddy answers, then relaxes — the invisible field responding to your presence.

Iteration log / 05

Three passes, driving the field each time.

  • Craft spacing · streams · legibility

    The first build read as static noise: the trail fade was too aggressive (dots, not streams) and a sinusoidal zonal drift cut hard shear seams across the frame. Fixed by dropping the fade from 0.052 to 0.020, lifting the speed, scaling the gyres ×1.9 so they read as spirals, and replacing the drift band with dual-octave curl-of-noise plus a small baseline so no region stagnates. Added text-shadow scrims and a mobile-specific gradient so hero copy clears the bright field.

  • Depth second-read detail

    Added the cursor eddy — a rotational perturbation injected at the pointer that ramps in and decays back to zero when you stop moving, so the resting state (and the thumbnail) stay a pure, untouched conveyor. The stat readouts count up on load. The result rewards a second visit: the sea quietly bends around you.

  • Hardening 375 · reduced-motion · fallbacks

    Verified the 375px layout with the nav fully inside the viewport. Reduced-motion warms the simulation 160 steps then freezes a settled, non-blank frame. Confirmed :focus-visible, a devicePixelRatio cap of 2, resize rebuilding the trail buffer, the rAF loop pausing on document.hidden, and a CSS-gradient fallback for WebGL context loss. Removed the film-grain overlay — imperceptible over the field, and one ornament too many.

Elsewhere / 06

See it move.