A walker, a runner and a cyclist set off together and go once around the Earth. They are always at the same distance, so the race is never about position — it is about what the distance cost each of them.
Most comparison pieces race things against each other. This one refuses to. All three movers sit at the same kilometre for the whole journey, which turns the question from who is ahead into something more interesting: what did getting here take?
The pitch fits on one line: one lap of the Earth, three ways to pay for it. Please do not scroll to the end.
Food is the currency, because kilocalories are abstract and a slice is not. One 285 kcal cheese slice, converted at each mover's rate, is the whole idea in one number.
The runner gets there first and pays most. The cyclist covers more than twice the ground on the same slice — which is the finale's argument, arrived at by scrolling rather than asserted up front.
Every number on screen comes from one formula —
kcal = MET × kg × hours — using 2024 Compendium of Physical
Activities values. Nothing is tuned to make the story better.
walk 5 km/h 3.8 MET 53.2 kcal/km 1,421 steps/km run 10 km/h 9.3 MET 65.1 kcal/km 960 steps/km cycle 20 km/h 8.0 MET 28.0 kcal/km 240 strokes/km
Compendium codes 17190, 12050 and 01030.
These are gross calories — they include the ~70 kcal/hour this person burns doing nothing, the way a fitness watch reports it.
So running costs only 1.2× walking per kilometre here, where textbooks say about 2×. That figure is net. Both are right, and the HUD carries a one-line caption saying so.
No hills, no wind, no rest stops, no sleep, no eating. The movers are not realistic. They are consistent, which is the only property the piece needs.
At 8 pixels per metre, a full lap of the Earth is about 320 million pixels. Browsers do not go that far — and they fail quietly, by clamping, not by erroring.
Custom wheel handling would solve it and ruin everything else — scroll-jacking is the single most common accessibility and mobile complaint about pieces like this. The fix had to keep native scroll intact.
The page holds a single 4,000,000 px spacer — 500 km worth — with the canvas fixed behind it. A virtual offset does the rest.
distanceM = offsetM + scrollY / 8
// once scrolling has been idle 160ms
if (scrollY > 3_000_000) {
offsetM += 250_000
scrollTo(0, scrollY - 2_000_000)
}
Moving scrollY mid-gesture kills iOS momentum dead. Waiting for
160 ms of quiet means the swap only ever happens between flicks,
where nobody can feel it.
Verified in a real browser: scrollY jumps 2,900,000 → 1,100,000 while distance continues 362,500 → 387,500 m, monotonic throughout.
The wheel is never touched. Keyboard, trackpad, screen reader and momentum all behave exactly as the browser intends.
distanceM changes every frame, and the HUD, whisper, rail, sky
and camera all read it. Routing that through useState re-renders the tree sixty
times a second.
distanceM, outside React entirelyThis is an architecture decision, not an optimisation. Retrofitting it would mean rewriting every component that reads the value — so it was settled before a line of UI was written.
three.js is 800 KB. Nobody should look at a blank screen while it loads, and some readers should never receive it at all.
Desktops and recent phones. Perspective 3D, up to 2,000 calorie particles, device pixel ratio up to 2.
Mobile default, or anything an FPS watchdog catches below 45 fps. Same scene, 300 particles, pixel ratio 1. Downgrades once, never flaps.
No WebGL, or prefers-reduced-motion. Canvas 2D lanes, no
particles, counters jump rather than ease. A complete experience, not a stub.
Each mover's trail encodes its own data, and each is a different shape — so the three stay tellable apart with colour removed entirely.
A colour-blindness review passed the palette conditional on
keeping this redundancy — yellow and pink converge for red-green viewers. Collapsing to
colour alone would silently fail a check that had already been signed off, so it is written
into the repo's CLAUDE.md as a rule.
The pacing is deliberate: long empty stretches make the payoffs land. Hero cards are the only skip targets; everything else is a whisper printed flat on the road as you pass over it.
N / P keys?km= links, screenshot-ready endingCard copy uses tokens resolved against the model at each card's own distance, so text can never drift from the numbers. That rule reads like tidiness. It is a correctness mechanism.
The spec said 84. So did its 49-claim fact-check. So did the design. 40,075 ÷ 20 km/h is 2,003.75 hours — 83.49 days.
The walker (333.96) and runner (166.98) genuinely do round up. The same instinct was applied once too often, and three review passes agreed with each other.
Prose numbers are unverified claims. Derive them and they become assertions — 19 of them, run in under a second.
No framework for the scroll — it is 180 lines of plain rAF and one spacer. The heavy dependency is three.js, and it is the one thing that lazy-loads.
Five figures are flagged on their own cards and in the sources drawer rather than quietly rounded — two expressway lengths, NH 44's true distance, the Comrades route, and a 1970 efficiency ranking.
Every card carries its source. The drawer builds itself from them.