← Back to VERSE

How it’s made

Four poems. Four small physics engines.

No animation library, no easing package, no particle plugin — every misbehaving letter in VERSE is pushed around by plain JavaScript running its own arithmetic, once per movement, tuned by eye until it stopped looking like code and started looking like weight.

I — GravityMass, drag, and a floor that remembers

Each poem line is split at build time into <span class="word"> elements. Before anything else happens, every word’s canonical home position is measured once with getBoundingClientRect(), while the whole poem still sits in normal flow. Click a word and it becomes position:absolute, anchored at that home coordinate; a small integrator then adds gravity to its velocity every frame (vy += g·dt), lets it drift sideways on a sine flutter sized inversely to its letter count, and bounces it off a floor line with mass-scaled restitution — short words bounce more, long ones thud and stay. Landed words are tracked in a list so a new arrival can shove any word it overlaps, waking it back into the simulation for a second, smaller bounce. Lift reverses the whole thing as a critically-damped spring (a = -k·x - c·v) pulling every fallen word back toward its untouched home, staggered a few dozen milliseconds apart so the return reads as buoyant, not instant.

II — MagnetA spring that never stops chasing zero

Here the split goes to individual <span class="letter"> characters. Every letter keeps a target offset of (0,0) unless the pointer enters its 96px radius, in which case the target becomes a vector pointing away from the cursor, scaled by (1 − distance/radius)². A spring integrator chases that target every frame regardless of whether it’s zero or not — so release is exactly as physical as repulsion, just running backwards. The ring cursor is a single fixed-position div following pointermove, shown only under (hover:hover) and (pointer:fine); touch devices never see it.

III — AssemblyFLIP, without the measuring step

A tiny seeded linear-congruential generator scatters every letter to a polar-random offset on load — same shape every reload, tuned rather than left to raw Math.random(). An IntersectionObserver fires once when the poem crosses 30% into view; each letter’s stored scatter distance sets its transition-delay and duration, so letters that traveled furthest also arrive last, and the whole poem closes like a slow exhale rather than snapping into place at once.

IV — ErosionScroll as a straight razor

The section is a 340vh scroll track holding a position: sticky poem. A scroll listener (throttled to one requestAnimationFrame per tick) turns the sticky element’s getBoundingClientRect() into a 0–1 progress value. Each line gets a reading window around its position in that range; once progress passes a line, its non-essential letters fade, drift upward, rotate, and blur in proportion — a smoothstepped erosion amount, not a linear one — while the word marked to survive (set via a data-keep attribute) stays lit in gold. Scroll back up and the ash reassembles, because the whole thing is a pure function of scroll position, not a one-shot animation.

ProcessThree passes, mostly by eye

Pass one built the four engines against the brief. Pass two was tuning by screenshot and by hand: gravity’s restitution felt too bouncy for long words, the magnet radius read as jittery until the spring constant dropped, and the assembly stagger needed a distance curve instead of a flat index-based delay to avoid a mechanical typewriter feel. Pass three checked both viewports, prefers-reduced-motion (which renders every poem as still type, with Erosion’s essential words permanently gold), and keyboard access — every fallen word is a focusable, Enter/Space-operable button, not just a click target.