← Home
01Scrollytelling · 2026

The Long
Way Round

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.

Vite · React 18 · React Three Fiber · three.js · zustand 40,075 km · 18 milestones · ~320 million pixels the-long-way-around.sankettambare.in
02The idea

Scroll is distance. The only variable is cost.

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 rules

  • Scroll maps linearly to distance — 8 pixels per metre
  • Three numbers per mover: time, calories, food
  • No scroll-jacking, ever — native scroll throughout
  • Zero onboarding: one line that fades after 20 metres

What it is not

  • Not a calorie calculator — there is nothing to input
  • Not a training tool, and not weight-loss framing
  • No sign-up, no tracking, one generic person
  • Jokes aim at physics and time, never at bodies

The pitch fits on one line: one lap of the Earth, three ways to pay for it. Please do not scroll to the end.

03The hook

One pizza slice buys 4.4 km

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.

Run
4.4 km
65.1 kcal/km
6 min per km
Walk
5.4 km
53.2 kcal/km
12 min per km
Cycle
10.2 km
28.0 kcal/km
3 min per km

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.

04The model

One imaginary 70 kg adult, on flat ground

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.

The three modes

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.

The caveat that had to be stated

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.

05The core problem

The page is ten times taller than a browser allows

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.

Firefox cap17.9M px
Chrome cap33.5M px
One marathon0.34M px
Our journey320.6M px

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.

06The mechanism

One tall spacer, quietly sliding underneath

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.

The whole trick

distanceM = offsetM + scrollY / 8

// once scrolling has been idle 160ms
if (scrollY > 3_000_000) {
  offsetM += 250_000
  scrollTo(0, scrollY - 2_000_000)
}

Why the idle wait is load-bearing

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.

07Architecture

The counters never touch React state

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.

What would have happened

  • Every counter tick re-renders the component tree
  • Reconciliation competes with the render loop for frame budget
  • Mid-range Android drops below 45 fps and stays there

What it does instead

  • A mutable singleton holds distanceM, outside React entirely
  • One rAF writes straight to DOM nodes and object refs
  • React state is touched only when a discrete value changes

This 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.

08Rendering

The flat version paints first, then upgrades in place

three.js is 800 KB. Nobody should look at a blank screen while it loads, and some readers should never receive it at all.

Full

Desktops and recent phones. Perspective 3D, up to 2,000 calorie particles, device pixel ratio up to 2.

Lite

Mobile default, or anything an FPS watchdog catches below 45 fps. Same scene, 300 particles, pixel ratio 1. Downgrades once, never flaps.

Flat

No WebGL, or prefers-reduced-motion. Canvas 2D lanes, no particles, counters jump rather than ease. A complete experience, not a stub.

60 KB
entry, gzipped
3 s
first paint budget
45 fps
downgrade floor
1
canvas in flat tier
09Accessibility

The ribbon shapes are not decoration

Each mover's trail encodes its own data, and each is a different shape — so the three stay tellable apart with colour removed entirely.

WalkOne dot per 0.704 m step. No gaps — a walker always has a foot down.
RunOne dash per 1.04 m stride. The gap is airtime.
CycleA continuous line, ticked once per 2.1 m wheel turn.

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.

10What is in it

Eighteen milestones, and a lot of quiet road

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.

The journey

  • 18 hero cards — Bolt at 100 m to a full lap at 40,075 km
  • 27 whispers — one line each, no card, no pause
  • Ghosts: Sawe's sub-two marathon, Ganna's hour record, a car at Pune
  • Sky follows the walker's clock from 06:00 — dark by km 60

Getting around

  • Skip pill, plus N / P keys
  • An 18-dot rail on a log scale, so the first kilometre isn't crushed
  • Auto-travel at 1×, 100× and 10,000×
  • Sources drawer, shareable ?km= links, screenshot-ready ending
334
walker’s days
167
runner’s days
83
cyclist’s days
7,481
slices, walking
11What the build caught

Deriving numbers finds what re-reading cannot

Card 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.

83 days, not 84

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.

Three more, the same way

  • Share text said “7,107” beside a card saying “7,481” — one walker, two units
  • Two rail dots sat 2.9 px apart, making one unclickable
  • At 360×640 a card covered the controls once the copy wrapped

Prose numbers are unverified claims. Derive them and they become assertions — 19 of them, run in under a second.

12Where it stands

Real science, unrealistic people

The stack

ViteReact 18React Three Fiber three.jszustandvitest

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.

Honest about what is unsettled

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.

40,075
km, one lap
18
hero milestones
19
model assertions
3
rendering tiers
Scroll it → Source on GitHub Please do not scroll to the end.