Scrubbing an AI-generated video with scroll: one timeline, six chapters
I wanted to try one thing: drive a video from start to finish with scroll alone — no play button. How far you scroll is where the film seeks; stop and it freezes. The subject is a short AI-generated film introducing the jirai-kei subculture, split into six chapters with Traditional-Chinese + Japanese captions, with Lenis for smooth scroll.
👉 See the live page — scroll slowly and the film tracks your motion.
Scroll progress = the film’s timeline
The whole page is a single video pinned centre-screen, backed by 700vh of scroll length as a “scrub track”. Each frame I read scroll progress p (0 to 1):
const total = scrollEl.offsetHeight - innerHeight; // scrollable distance
const p = clamp(-scrollEl.getBoundingClientRect().top / total, 0, 1);
Multiply p by the film’s duration and that’s the time to seek to. No timeline library, no play/pause/reverse — state is purely a function of scroll position, so scrolling back up rewinds the film for free.
Eased seeking is what makes the scrub smooth
Setting film.currentTime = p * duration directly makes the scrub jump frame to frame — harsh. Instead I ease (lerp) toward the target each frame:
const target = p * film.duration;
currentTime += (target - currentTime) * 0.14; // 0.14 ≈ smoothness
if (film.readyState >= 2) film.currentTime = clamp(currentTime, 0, duration - 0.04);
0.14 is the balance between responsiveness and smoothness — higher jumps, lower drags. With Lenis smoothing the wheel on top, the scroll itself feels fluid, and the film reads like “holding a timeline” rather than “dragging a scrubber”.
Tying six captions to the film
Captions don’t morph continuously — they switch in discrete chapters: Math.floor(p * 6) slices progress into six bands, each toggling its .cap and the matching chapter dot’s on class, while the top progress bar width is simply p × 100%. The same p drives three things at once — film time, current chapter, progress bar — so they always agree.
What I learned
1. To scrub a video, you have to buffer it. <video> with preload="auto" plus playsinline muted is what allows instant seeking; on phones the file must buffer enough to scrub smoothly, so a single 720p, 30-second file is easier to manage than per-chapter clips.
2. Poster + preloader avoid the blank flash. While the video isn’t ready, the chapter-1 poster shows (poster="ch1.jpg"), and the preloader hides on canplay (or after load) — no black gap.
3. Always honour prefers-reduced-motion. For reduced-motion users: skip Lenis and set currentTime directly (no 0.14 easing) — the film still tracks scroll, just without the extra inertia and smoothing.
4. Scrub smoothness vs. sharpness is a trade-off. Seeking a too-large file stutters on slow machines; too small loses detail. A single 720p file splits the difference.
Wrap-up
From “drive a video with scroll alone” to a page where the film tracks your motion and six captions surface as you go, the biggest takeaway: bind the film’s timeline to scroll and the viewer shifts from “watching a video” to “holding the timeline”. They control the pace, but every scroll position lands on a frame.