CineScroll
scrollcanvascinematicimage-sequencescrollytellingScroll is the camera. Pin the viewport, feed it frames, and the page becomes one unbroken shot.
Scroll is the camera: a pinned viewport scrubs a frame sequence with double-exposure blends, velocity whip blur and DOM chapter overlays. Bring your own frames and chapters — photos, renders or locally generated ComfyUI plates.
Playground
Tweak the props in the toolbox — the preview updates in realtime. “Copy code” gives you the exact JSX for the current tweaks.
Installation
Requires the shadcn CLI. Run this from your project root — it writes the component source, then you import and go.
npx shadcn@latest add "https://webcules.com/r/cine-scroll.json"
The command installs the component source (plus its engine / type files when the component ships one). No other dependencies — everything is dependency-free canvas, SVG and CSS.
Usage
Import the component and drop it in — every prop in the toolbox is a regular React prop:
import { CineScroll, CineIndexRows } from "@webcules/ui/components/ui/cine-scroll";const frames = ["/plates/shot-01.webp","/plates/shot-02.webp","/plates/shot-03.webp","/plates/shot-04.webp",];export function FilmHero() {return (<CineScrollframes={frames}height="500vh"blend="exposure"motionBlur={0.6}chapters={[{from: 0,to: 0.2,align: "center",content: (<div className="text-center text-white"><p className="text-[11px] uppercase tracking-[0.4em]">A film you scroll through</p><h1 className="mt-3 text-6xl font-semibold">STUDIO®</h1></div>),},{from: 0.3,to: 0.55,align: "left",content: <p className="text-xl text-white">Impossible scenes, told like they happened.</p>,},{from: 0.65,to: 0.9,align: "right",content: (<CineIndexRowsrows={[{ year: "2025", title: "Hayate", note: "No detours" },{ year: "2024", title: "Tsuki", note: "One last train" },]}/>),},]}/>);}
Highlights
- Scroll is the camera: a tall section pins its stage while the scrollbar scrubs an ordered frame sequence — photos, renders or AI plates, your media
- Adjacent plates double-expose into each other; fast scrolling adds a directional whip blur, slow scrolling breathes with a Ken Burns push-in
- Chapters are real DOM (selectable, indexable) that fade and rise inside progress windows — pair with the exported CineIndexRows and CineStat primitives
- Works as a page hero (window scroll) or embedded: set stageHeight to the container viewport and it scrubs inside any scroll box
- Canvas 2D, zero deps; one RAF parked off-screen/hidden-tab; reduced-motion keeps scroll→frame direct manipulation with no autonomous motion
Example — Make the plates with local ComfyUI
Generate a consistent camera move: plate 1 from text, every next plate as img2img off the previous one (denoise ≈0.5) with a per-beat camera prompt. Same style + cast block verbatim in every prompt is what makes the scrub read as one scene.
// research/generate-plates.ts in the social-forge project —// beats: wide → tumble close-up → car pass → whip close → slide → leapconst style = "1990s Japanese anime film still, cel animation, dusk city, …";const beats = ["sprinting toward camera, wide", "close-up, dutch angle", "…"];// plate 1: txt2img · plate n: img2img(plate n−1, denoise 0.5)
Example — Stat counters over the final shot
Export CineStat gives the small-caps label over the huge serif-italic numeral from the reference.
import { CineScroll, CineStat } from "@webcules/ui/components/ui/cine-scroll";<CineScrollframes={frames}chapters={[{from: 0.84,to: 1,align: "center",at: "bottom",content: (<div className="flex gap-14"><CineStat label="Clients" value="73" /><CineStat label="Sets built" value="208" /></div>),},]}/>
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| video | string | "" | Optional clip URL — the scrub seeks the video with scroll instead of the frames. Leave empty for the SHŪDEN frame set. |
| blend | "exposure" | "crossfade" | "exposure" | exposure = lighten double-exposure between plates (the reference look); crossfade = plain alpha. No effect in video mode. |
| motionBlur | number (0–1) | 0.6 | Velocity-scaled directional blur while you scroll fast — the 'one unbroken camera move' feel. |
| drift | number (0–0.15) | 0 | Ken Burns push-in/pan per plate so stills breathe. Off by default — video frames are already alive. |
| scrollSmoothing | number (0–0.4) | 0.14 | Lerped scrub lag. 0 = raw scrollbar, ~0.14 = filmic. |
| vignette | number (0–0.8) | 0.35 | Edge darkening — unifies mixed-source plates into one grade. |
| grain | number (0–0.4) | 0.12 | Animated film grain (deterministic per frame). |
Notes
- Bring your own media: pass frames (any image URLs, ≈8–16 plates, webp, 16:9) or a single video URL as video — the scrub then seeks the clip with scroll. The SHŪDEN demo frames are cut from one locally generated Wan 2.2 image→video camera move; the reference site runs a real-time 3D scene, the scrub pattern is identical.
- Chapters are progress windows (0–1). A chapter starting at from: 0 is visible before any scroll; others fade/rise inside their window.
- Chapters are progress windows (0–1). A chapter starting at from: 0 is visible before any scroll; others fade/rise inside their window.
- The canvas is aria-hidden; chapter copy is real DOM and stays readable by screen readers at every scroll position.
- Prefer reduced motion: scrubbing becomes direct manipulation — scroll still swaps plates instantly, with no smoothing, blur, drift or grain animation.
- Plates are decoded once into offscreen canvases (capped 1600px wide) and DPR is capped at 2; the RAF loop parks when the section is off-screen or the tab is hidden.
