The Libration
Brief kinetic-type × curiosity × audio-visual-synesthetic about lunar libration — the first experience built under the new brief-first / Craft Tests / AD Lock workflow.
Inspired by Libration on Wikipedia
Built with Pure Canvas 2D · Fraunces (display) + Space Grotesk (micro) · Web Audio API · ~350 lines of JS
Techniques Sinusoidal libration math (±7.55° longitude, ±6.7° latitude) · Per-letter phase-offset tilt · Chromatic fringe rendering (warm / slate / ink stacked via
globalCompositeOperation = 'screen') · Gold shadow halo following longitude velocity · Binary-search font fitting · Two-oscillator sine pad with StereoPanner + BiquadFilter modulation · Synthesized chime (880 Hz + 5th + 7th partials)Direction Cold blue-black against warm regolith-cream. Fraunces 900 set huge. Two sine tones beating in perfect fifth, pan and filter driven by the libration itself. A piece that rewards patience — do nothing and the moon nods through a full cycle in ~27 seconds, revealing six kinetic words and resolving on a quiet italic card.
Result A single enormous serif word tilts imperceptibly. You start to notice serif edges you couldn’t see a moment ago. A chime, another word. The piece ends. Fifty-nine percent. Not fifty. Not still.
The Story
The moon is famously “tidally locked.” It presents the same face to Earth through every orbit, because its rotation period equals its orbital period. For thousands of years that was the assumption — the moon is a fixed shape in the sky, hung in place.
Galileo, in 1637, looked through his telescope long enough to notice otherwise. The moon was nodding. Not by much — the effect is subtle, revealed only by patient comparison across several nights — but unambiguous. Features near the limb drift into view, then out again. Craters appear over the edge that weren’t visible last week.
The phenomenon is called libration, from the Latin librare, “to weigh or balance.” It has three causes layered on top of each other:
- Libration in longitude (±7.55°) — the moon rotates uniformly but orbits elliptically. Kepler’s second law means it travels faster near perigee and slower near apogee, so its orbital position races ahead of and falls behind its steady rotation. From Earth, the face appears to rock east-to-west.
- Libration in latitude (±6.7°) — the moon’s rotation axis is tilted ~1.5° from its orbital plane’s normal, so as we view it from different heights along the orbit we alternately see over its north pole and its south pole. A slow nod.
- Diurnal libration (~1°) — pure parallax from Earth’s own rotation. In a single night, moonrise to moonset, we shift our vantage point by the diameter of the Earth and see slightly different angles of the face.
The three effects combine. Over a full lunar month, the observer on Earth sees roughly 59% of the moon’s surface, not 50%. The supposedly fixed face is breathing.
That’s the human truth underneath. The thing we were most certain was unchanging — the face of the moon — is in fact continuously, imperceptibly different. It takes sustained attention to see it. And when you do, the world you thought you knew gets bigger by nine percent.
The Take
The piece opens on darkness. One letter at a time, F-I-X-E-D scribes itself in gold and settles into warm ink-cream. Three feet tall, perfectly upright. Click to begin. pulses softly below.
You click. A chime. The word begins to tilt.
At first the tilt is almost nothing — a fraction of a degree to the right. A beat later: maybe a full degree. Small chromatic fringes appear at each letter’s edges: a pale warm ghost on one side, a slate-blue ghost on the other. A gold halo bleeds from the serifs in the direction the letter is leaning, as if light is emerging from a hidden edge.
The baseline reaches +7.55°, holds, reverses. Halfway through the nod, FIXED crossfades into STILL — a second chime. The tilt continues its slow sine, this time dragging STILL through its full sway. Then SAME. NEW. FACE. RETURNS.
Each word’s letters don’t all tilt in lockstep. Each has a small phase offset — as if the letter in the middle of the word is a moment ahead of the letter at the end. The effect is that the word nods AS A WORD while each letter leans slightly differently. The motion feels organic, not mechanical. Not like a computer rotation; like a slow exhale.
In your ears: two sine tones, 110 Hz and 165 Hz — a perfect fifth that beats slowly against itself. The tones pan in stereo as the libration moves east and west. A low-pass filter opens and closes with the latitude nod — north-up the sound brightens, south-down it dims. It’s ambient, but it’s following the math exactly.
If you want, you can scrub. Move the cursor left: the libration jumps to the shadowed eastern limb. Move it right: the bright western limb. But the piece doesn’t need you to drive it. If you hold still, it proceeds on its own. The patient observer sees the full cycle.
At the end: a quiet card in Fraunces italic 300.
Fifty-nine percent. Not fifty. Not still.
The Tech
Brief-first workflow
This is the first experience built under a workflow overhaul that replaces neutral-topic seeds with opinionated [POV × Feeling × Medium × Subject] briefs. The brief produced by scripts/brief-seed.mjs for this piece was:
kinetic-type × curiosity × audio-visual-synesthetic about the lunar libration
Every downstream decision — typography, sound, pacing, palette — flowed from locking that brief first. A related new step, the Art Direction Lock, committed specific hex values, font sources, and synthesis parameters before any code file was opened.
Pure Canvas 2D + Web Audio
No Three.js, no WebGL, no shader pipeline. The subject is typographic; the tech is what the concept demands, nothing more. The entire experience lives in ~350 lines of JS.
Libration math
Real lunar libration parameters are used directly, with time compressed from ~27.55 days to 27 seconds:
const T_LON = 27.0; // seconds per longitude cycle
const T_LAT = 27.0 * 1.088; // slight asynchrony between axes
const LON_AMP = 7.55; // actual lunar longitude amplitude
const LAT_AMP = 6.7; // actual lunar latitude amplitude
function librationLon(t) {
return LON_AMP * Math.sin(2 * Math.PI * t / T_LON);
}
function librationLat(t) {
return LAT_AMP * Math.sin(2 * Math.PI * t / T_LAT + Math.PI / 3);
}
The π/3 phase offset between the axes means longitude and latitude don’t peak simultaneously — the nod feels composite, not scripted.
Per-letter phase offset
Each letter’s tilt is longitudeTilt + latitudeTilt × 0.22 × sin(i × π/8 + 1.1). That means letters within a word don’t tilt in lockstep — each has a slightly different lean based on its index. At small amplitudes this reads as a word with character, not a rigid block being rotated.
Chromatic fringes (moon’s bright and shadowed limbs)
For each letter, three rendering passes stacked via globalCompositeOperation = 'screen':
// Warm fringe (lit limb)
ctx.globalCompositeOperation = 'screen';
ctx.globalAlpha = 0.30;
ctx.fillStyle = WARM; // #E9D3A4
ctx.fillText(char, +fringe, 0);
// Cool fringe (shadowed limb)
ctx.fillStyle = SLATE; // #2E3A52
ctx.fillText(char, -fringe, 0);
// Gold emergence halo (shadow offset with libration velocity)
ctx.globalCompositeOperation = 'source-over';
ctx.shadowColor = 'rgba(195, 154, 79, 0.55)';
ctx.shadowBlur = size * 0.065;
ctx.shadowOffsetX = velLon * 0.35;
// Main ink
ctx.fillStyle = INK; // #F4EDE0
ctx.fillText(char, 0, 0);
The fringe offset scales with |longitude velocity|, so the chromatic aberration is strongest when the moon is moving fastest (around the midpoint of its sway) and disappears at the extremes. The gold shadow offsets forward in the direction of motion, creating a subtle “serifs emerging into light” effect.
Two-sine pad + modulation
const osc1 = aCtx.createOscillator(); // 110 Hz (A2)
const osc2 = aCtx.createOscillator(); // 165 Hz (E3, 3:2 perfect fifth)
// Longitude → pan
a.panner.pan.setTargetAtTime(0.6 * lon / LON_AMP, tc, 0.08);
// Latitude → LPF cutoff (400-1200 Hz)
a.lpf.frequency.setTargetAtTime(400 + 400 * (1 + lat / LAT_AMP), tc, 0.12);
The setTargetAtTime calls smooth the modulation so there’s no zippering as values update each frame. The 3:2 interval produces slow beats (~5 Hz) that feel tidal rather than musical.
Synthesized chime
One chime fires on each word transition — not a sample, but an envelope over three sine partials:
const partials = [
{ f: 880, g: 1.0 }, // fundamental
{ f: 1319, g: 0.4 }, // perfect 5th
{ f: 1540, g: 0.2 }, // minor 7th
];
env.gain.setValueAtTime(0, t);
env.gain.linearRampToValueAtTime(0.30, t + 0.008); // 8 ms attack
env.gain.exponentialRampToValueAtTime(0.0001, t + 1.2); // 1.2 s decay
Three sines with that envelope produce a glass-bell tone. No reverb in this build (kept dry to maintain presence against the sine pad). If audio craft iteration is needed, a small convolution reverb would fit.
Binary-search font fitting
Used to scale each word to fit 75% of viewport width regardless of letter count:
function fitText(text, maxWidth, fontTmpl) {
let lo = 14, hi = Math.min(520, H * 0.55);
while (hi - lo > 1) {
const mid = (lo + hi) >> 1;
ctx.font = fontTmpl.replace('{S}', mid);
if (ctx.measureText(text).width > maxWidth) hi = mid;
else lo = mid;
}
return lo; // largest size that fits
}
Results cached by (text, maxWidth, fontTmpl) key. So FIXED (5 letters) lands at ~280 px on desktop while RETURNS (7 letters) lands at ~220 px — the natural rhythm of word lengths becomes its own visual music.
Progress vs scrub
Progress advances linearly with dt during the active phase. When the cursor moves, a scrubWeight value rises toward 1 and blends the libration angle between the auto-driven value (time-based) and the cursor-driven value (cursorX / width × T_LON). When cursor stillness returns (>400 ms without movement), scrubWeight decays back to 0.
Crucially: scrubbing does NOT advance progress. Progress only advances with time. So a user can explore the libration manually for as long as they like without the piece prematurely completing — but if they stop moving, it quietly resumes its own cycle.
Finite shape
At progress >= 27.0 (one full cycle), the phase transitions to complete and the completion card fades in. The piece has a natural period; the experience has one too. Not an eternal loop.
This blog post was AI generated with Claude Code. Authored by Artificial Noodles.