Guide · How it was made
Flying through a minimal surface that keeps changing its mind
Gyroid is a single-page study of the triply periodic minimal surfaces — the soap-film geometries that divide space into two perfect interlocking labyrinths. The page's one job: put the visitor inside the surface, mid-flight, while it morphs on a slow cycle through the three classical TPMS — gyroid, Schwarz P, diamond — and let them steer the morph themselves.
Palette
Butterfly wing scales grow real gyroids and get their green from geometry, not pigment — so the palette refuses flat colour: the two channels are tinted teal and violet, and every surface reads through an angle-dependent sheen between them. The bright accents are reserved for lines, marks and canvas light; body copy sits in bone ink for contrast.
Type
- Syne — display
Wide, slightly alien geometric display for the wordmark and headings. Its heavy O's echo the lattice openings; the hero wordmark carries an iridescent gradient sheen, the one moment of typographic bravery.
- Space Mono — everything else
Instrument-panel body: readouts, formulae, captions and copy. The page is a cockpit flying through mathematics; a mono keeps every number honest and tabular.
Techniques
The scene is a vanilla three.js fullscreen RawShaderMaterial on a single triangle — no geometry, no lights, everything happens in the fragment shader. Each TPMS is an implicit field (sin x cos y + sin y cos z + sin z cos x for the gyroid, and the P and D identities). The three fields are blended by a weight uniform driven from JS, the blend is shelled (|f| − 0.3) into a thin wall, and the result is sphere-traced with 92 bounded steps, a relaxed step factor and a hard far cap so scroll stays smooth (measured ~240 fps at 1440×900 while scrolling, DPR-capped and pixel-budgeted). A smooth-subtracted bubble around the camera carves a clean opening through any wall the flight path crosses, so the drift never clips. At the hit point the sign of the field says which of the two channels the face borders — teal side or violet side — and a fresnel term slides each face toward its opposite tint at grazing angles: structural colour, the butterfly's trick. The morph cycle (hold 7 s, morph 3.5 s) is exposed on window.__gyroid, the same hook the three surface cards use to steer the cycle, and the DOM readout reports surface, morph progress and the lattice cell the camera currently occupies. Reduced motion renders one settled mid-morph frame and stops; a lost WebGL context swaps in a layered CSS-gradient scene.
Iteration log
-
Pass 1 · Craft
The first render was a black void with rims
Driving the actual morph (screenshots at gyroid-hold and Schwarz-P-hold) showed the field frequency was so low the camera saw one wall, nearly unlit — only fresnel edges. Raised the lattice frequency 2.35×, thinned the shell (0.42 → 0.30), shrank the camera bubble (1.35 → 0.42), widened the FOV, lifted base diffuse and added a violet counter-light so both channels read as material, not silhouette. Verified the wing-section body ink was true #E4E8EC (a screenshot artefact had suggested a violet cast) and that misses fade to a faint two-tone haze instead of a hard black cutout.
-
Pass 2 · Depth
Steering, and a surface with no name
Made the three surface cards live controls: clicking one tweens the cycle to that surface (same
jumpTothe QA harness drives), with the on-screen card badged andaria-pressed. Added the second-read detail: for the middle sliver of every morph the readout drops the arrow and admits UNNAMED — mid-blend you are flying through a surface that has no name, which is exactly what the section copy claims. The readout also tracks the lattice cell the camera occupies, ticking as you cross walls. -
Pass 3 · Hardening
Probes, not promises
Headless runs asserted the signature end-to-end: cycle parameter advances on its own,
jumpTo('SCHWARZ P')lands weights at (0,1,0) with the DOM readout matching, a real card click reaches DIAMOND, and two screenshots 1.3 s apart always differ. Reduced-motion emulation produced a genuinely static, fully painted settled frame; forcingWEBGL_lose_contextswapped in the CSS fallback. 375 px: no horizontal overflow, nav and wordmark inside the viewport, cards stacking to one column. Chanel rule: deleted a dead formula-chip style that never earned its place.