03 — Tech & motion
Three engines, one rAF heart
A — The velocity engine
One requestAnimationFrame loop measures scroll displacement per
millisecond. The raw signal is asymmetrically smoothed — it rises fast
(k = 0.16) and decays slow (k = 0.045), so type snaps into
motion and settles like a needle. The normalized value maps onto the font axes of
every letter in the headline, phase-shifted per letter to ripple like a wave through
the line.
raw = (scrollY − lastY) / dt // px per ms
smooth = lerp(smooth, |raw|, raw > smooth ? 0.16 : 0.045)
norm = clamp(smooth / 2.6, 0, 1) // 2.6 px/ms ≈ flat out
// per letter i — a travelling wave keeps it organic:
phase = 0.75 + 0.25 · sin(t·0.004 + i·0.9)
wght = 620 + 180 · norm · phase // mass increases
wdth = 94 − 19 · norm · phase // width compresses
B — Split-flap board mechanics
Each character cell owns a position on a drum: A–Z −. When a row
enters the viewport, every cell starts flipping at a fixed mechanical tick of
42 ms, offset by 26 ms per column and 140 ms per row.
A cell's start position is derived backwards from its target letter, so flip counts
vary 6–15 — cells land at different moments, exactly like a real departures board.
Every tick collapses the glyph vertically (scaleY 1 → 0.35 → 1) via the
Web Animations API to fake the flap without images.
startIdx = (targetIdx − flips) mod drum.length
every 42ms: cell.text = drum[(startIdx + step) % drum.length]
cell.animate(scaleY: [1, 0.35, 1], 42ms, linear)
stop when step = distance(startIdx → targetIdx)
C — Inertia marquee physics
The bands integrate three forces per frame: a constant brand-drift, a coupling term
fed by page scroll velocity (the whole site shares one bloodstream), and thrown
momentum from pointer flicks, decaying with exponential friction. Position wraps
modulo one chunk width, so the band is infinite in both directions.
if grabbed: pos += Δpointer; vel = smooth(Δpointer)
on release: momentum = clamp(vel, −60, 60)
each frame: pos −= dir · base · (1 + scrollNorm · gain) · dt
pos += momentum; momentum ×= 0.94
wrap: pos = pos mod chunkWidth
D — Scatter, magnetism, restraint
Section titles are split into letters at build time (real spans, zero CLS,
screen-reader copy preserved). Each letter carries a deterministic scatter transform
seeded from its index; an IntersectionObserver flips one class and CSS transitions
— cubic-bezier(0.16, 1, 0.3, 1), 28 ms stagger — reassemble the
line. The contact CTA measures pointer distance per letter and converts proximity
into weight and displacement through a smoothstep curve. And when
prefers-reduced-motion is set, every engine stays off: the site renders
as a finished, static, fully legible poster.