Field note · Making of

How this was built

The Cylinder Room is a single self-contained HTML page. No framework, no build step, no audio files — just a shader, a synthesizer, and a lot of typewriter ribbon. Here is what is actually happening.

three.js r160GLSL shader WebAudioPlayfair SC + Special Elite nano-banana still

The wax cylinder

The hero is one open-ended CylinderGeometry wearing a custom ShaderMaterial. There is no groove geometry — modelling 210 turns of spiral would drown the GPU. Instead the fragment shader fakes the grooves by perturbing the surface normal along the cylinder's axis, so light rakes across ridges that do not exist.

// helical groove: phase runs along the axis and around the barrel
float phase = (vUv.y * 210.0 + vUv.x) * 6.2831853;
float dg    = cos(phase) * 0.9;        // groove slope
vec3  N     = normalize(vWNormal + vWAxis * dg * 0.55);

A tight specular term (pow(…, 90.)) gives the hairline glints that make it read as wax rather than corduroy; a broad low-exponent term adds the soft sheen. A hash-based grain mottles the colour so no two rings look identical.

Lowering the needle

Nothing makes a sound until you click. On that click a single AudioContext is built and every layer of a phonograph is synthesized live — no audio files ship with this page:

Meanwhile the shader's uNeedleT uniform advances, drawing a faint bright band that tracks across the wax exactly where the on-screen needle arm is resting.

The catalogue & the paper

The accession cards are plain HTML set in Special Elite, a typewriter face, over a repeating linear-gradient that draws the ruled lines. A punched hole, a rotation of half a degree, and a red rubber-stamp "played once" on hover do the rest. Every recording described is invented; every place name is fictional.

The archive photograph

The one image on the site — the vault anteroom behind the preservation section — was generated with a nano-banana still (an Edwardian shelf of labelled cylinder boxes under a single brass lamp), then compressed to WebP and dropped to 42% opacity behind a warm vignette so the type stays legible.

Motion with manners

The render loop follows a settle pattern: draw one frame, wait 1.5 s, then start requestAnimationFrame — which keeps screenshot tools from hanging on the live WebGL. The loop pauses on document.hidden, reveals honour IntersectionObserver, and prefers-reduced-motion freezes the cylinder into a still portrait and silences all drift.