horizontal-scroll-scenes
Horizontal Scroll Scenes
The mechanism: scroll position is the only state. One pure function turns scrollY / (scrollHeight - innerHeight) into {scene, dwell 0→1, move 0→1} every frame, and everything on screen (camera, wipe line, craft clock, text, index) is written from that triple. Nothing remembers where you were, so every beat is a place on the page.
This is what Kibori does, and it is not a translateX slider. Verified by running it: the page is a 1742vh native-scroll track (15,678 px at 1440×900); a fixed stage holds one WebGL world whose camera travels 0 → 49.7 world units left to right through eight stations; between stations a ragged burn line wipes from a frozen frame of the scene being left to the live scene already framed on the next. There is no wheel handler, no snap, no stored "current chapter". Reload at depth, scrollbar drag and anchor jumps work because state is derived, not accumulated.
Reach for scroll-world-storytelling or build-threejs-scroll-worlds for the story and 3D-world side, gsap-scrolltrigger-storytelling for pinned sticky sections, scroll-progress-timeline for a progress line. Reach for this when the deliverable is discrete chapters with a resting beat each, laid along an X axis, and the hard part is making that state machine survive reverse, skip and refresh. The demo is Vanilla JavaScript + DOM/CSS + inline SVG; the source is three.js. The state machine is identical.
The pace: two weights and a track height
Every number lives in DWELL, MOVE and the track height. One unit is one screen of scrolling.
| constant | value | meaning |
|---|---|---|
DWELL |
1.15 | scroll spent inside a scene while its craft advances (about 1,030 px at 900 tall) |
MOVE |
0.85 | scroll spent crossing to the next scene (about 760 px); the last scene has none |
CYC |
2.00 | one scene per two screens |
SPAN |
N*DWELL + (N-1)*MOVE |
8 scenes: 15.15 plus a 1.35 footer = 16.5; demo, 5 scenes: 9.15 |
| track | (SPAN + 1) * 100vh |
so 1 unit ≈ 1 screen; Kibori is 1742vh for 16.5 |