Loading...
Loading...
Build scene-by-scene chapters that advance left to right on one fixed stage while the user scrolls vertically, with native scroll as the only input. One pure function turns scrollY into scene, dwell and move, so forward, reverse, scrollbar drags, fast skips and reload at depth all land on the right frame. Covers dwell versus transition budgets, a burn-wipe or pan between scenes, chapter index and progress UI, glide-to-scene links that skip intermediate scenes, keyboard and reduced-motion behaviour. Use for horizontal scrollytelling, multi-scene product tours, "scroll down to travel sideways", chapter carousels driven by the wheel, or any piece where a wheel-jacked slider keeps breaking on touch, keys, refresh or back.
npx skill4agent add mengto/skills horizontal-scroll-scenesscrollY / (scrollHeight - innerHeight){scene, dwell 0→1, move 0→1}translateX1742vhscroll-world-storytellingbuild-threejs-scroll-worldsgsap-scrolltrigger-storytellingscroll-progress-timelineDWELLMOVE| constant | value | meaning |
|---|---|---|
| 1.15 | scroll spent inside a scene while its craft advances (about 1,030 px at 900 tall) |
| 0.85 | scroll spent crossing to the next scene (about 760 px); the last scene has none |
| 2.00 | one scene per two screens |
| | 8 scenes: 15.15 plus a 1.35 footer = 16.5; demo, 5 scenes: 9.15 |
| track | | so 1 unit ≈ 1 screen; Kibori is 1742vh for 16.5 |
function travel(sp){ // sp = scrollY / (scrollHeight - innerHeight)
const u = Math.min(SPAN - 1e-5, sp*SPAN);
const i = Math.min(N - 1, Math.floor(u/CYC));
const local = u - i*CYC;
const dw = Math.min(1, local/DWELL); // the scene's own clock
const f = i >= N-1 ? 0 : clamp01((local - DWELL)/MOVE); // the move
const rc = clamp01((f - 0.01)/0.97); // the line's own clock
return {i, u, dw, f, fe: sstep(0,1,f), L: -0.08 + 1.24*rc, // L: line, off-frame at both ends
live: Math.min(N-1, f < 0.39 ? i : i+1)}; // what the index shows
}scrollHeight - innerHeightinnerHeightcurrentSceneHomeEndureload()scroll-snapSpacePageDownu = 2000/(scrollHeight - vh)*SPANk1k < idwk == i0--p{k}--e{k}rc = (f-0.01)/0.97ii+1translateX(-(i+fe)/N*100%)linear-gradient(a, transparent (L-.01)%, #000 (L+.016)%)#000 (L-.062)%, transparent (L-.048)%a = 90deg270degsstep(1.00, 1.14, L)f < 0.390.550MOVE*0.80DWELL*0.62DWELL + MOVE*0.28MOVE*0.34arrivescrollTo(tgt-1)*CYC + DWELL*0.96(tgt+1)*CYC + 0.021-(1-x)^2.4tgt*CYC + DWELL*0.80clamp(700 + |dy|/vh*150, 900, 2400)html{scroll-behavior:smooth}scrollToautovisibility:hiddeninertscrollresizepageshowloadfonts.readyResizeObserverclipPathurl(#id)aria-current="step"position:fixedSpacePageDownHomeEndprefers-reduced-motion: reduceff = 0.64scrollTodocument.hidden03 / 05 Kigumirender()kibori-60fpstravel()| you see | cause |
|---|---|
| reload lands on scene 1 | state kept in JS; derive from |
| scene "sticks" between two beats | snap or wheel capture; delete it |
| two rooms overlap under the line | camera still travelling under the wipe |
| index shows the old scene as the title lands | flip threshold at 0.5 instead of 0.39 |
| jump link strobes several scenes | glide plays every move instead of the last |
| jump link overshoots or stutters | |
| second scene's clip does nothing | duplicated SVG |
| screen reader reads chapters that are not on screen | opacity 0 without |
| phone shows two titles spliced at the line | incoming chapter not held back until the line clears |
kibori.htmltravel()