scroll-reel
Deterministic frame-stepped scroll recordings — single sweeps, choreographed section pans, and social formats.
scroll-reel is the workhorse generator, and it does one thing: record a page scrolling. Every
capture is frame-stepped — it drives a virtual clock, screenshots each frame, and pipes them
to ffmpeg — so output is frame-accurate, crisp (supersampled by deviceScaleFactor), parallelised
across workers, and byte-identical run-to-run.
{
name: "home",
url: "https://your-site.com",
generator: "scroll-reel",
}By default it eases a single top→bottom sweep. The options switch it into choreographed or
auto-section motion, emit light/dark and per-viewport variants, and reframe / re-encode the output
for social. Site cleanup (hide the cookie banner, block trackers, freeze the clock) lives in
settings.capture and applies to every URL capture.
Want a scripted click-through with a visible cursor, a clip cropped to one component, or a realtime recording of the live page (time-based hero animation, autoplay video)? That's the
interactiongenerator.
Config options
Everything is optional. Reference is the interactive view; TypeScript is the same shape in code, every option at its default.
outputobject
Pixel size, encoding, and output formats.
widthnumberdefault 1280Output width in CSS px.
heightnumberdefault 800Output height in CSS px.
deviceScaleFactornumberdefault 2Render scale, max 4 — higher captures crisper, then downscales into the video.
fpsnumberdefault 30Output frames per second (max 120).
crfnumberdefault 18x264 quality, 0–51 (lower = better quality / larger file).
fileNamestringOutput filename. Defaults to "<slug(asset name)>.mp4".
outputs('mp4' | 'gif' | 'webp' | 'poster')[]default ['mp4']Files to emit per variant; each becomes its own asset.
gifFpsnumberGIF / animated-WebP frame rate (max 50). Defaults to min(fps, 15).
pageobject
Page-load waiting + dwell at the ends of the scroll.
startDelayMsnumberdefault 500Dwell at the top before scrolling (ms).
endDwellMsnumberdefault 800Dwell at the bottom after scrolling (ms).
waitUntil'load' | 'domcontentloaded' | 'networkidle' | 'commit'default 'networkidle'Page-load milestone to wait for before recording.
waitForSelectorstringOptional element to wait for (visible) before recording, e.g. a hero section.
renderobject
Frame-stepped render tuning (parallelism, frame format, per-frame settling).
workersnumberParallel render workers, each its own browser context. Omit to auto-pick from cores + free memory.
frameFormat'jpeg' | 'png'default 'jpeg'Intermediate frame format. "jpeg" is faster; "png" is lossless.
settlePerFramebooleandefault trueWait for fonts + in-view images before each frame's screenshot. Off in --draft.
settleMaxMsnumberdefault 250Max time (ms) to wait per frame for settling before screenshotting anyway.
motionobject
How the scroll moves: duration/easing, loop, and choreography.
durationMsnumberdefault 6000Time to scroll from top to bottom (ms). Ignored when choreography / autoSections drives the length.
easing'linear' | 'ease-in' | 'ease-out' | 'ease-in-out' | 'ease-out-strong' | 'ease-in-out-strong'default 'ease-in-out'Easing for the default top→bottom scroll.
loop'none' | 'boomerang' | 'straight'default 'none'Loop style — works with the default sweep, choreography, and autoSections. "boomerang" plays the motion forward then back, retracing every stop; "straight" runs it once then glides straight back to the top so the clip loops.
choreographyChoreographyStep[]
Choreographed scroll: an ordered list of steps instead of one top→bottom sweep. Omit for the default single eased sweep. Clip length becomes startDelayMs + Σ(step travel + hold) + endDwellMs.
tonumber | stringrequiredTarget: a 0..1 number, an "NN%" string, or a CSS selector to bring into view.
durationMsnumberdefault 1200Travel time to this target (ms).
holdMsnumberdefault 800Hold time at this target after arriving (ms).
easingEasingdefault 'ease-in-out'Easing for the travel to this target.
autoSectionsboolean | AutoSections
Auto-choreograph: detect the page's sections and pan/hold through them within a fixed budget. true for defaults, or an object to tune. Ignored if choreography is set.
minHeightFractionnumberdefault 0.5Min element height (as a fraction of the viewport, max 2) to count as a section.
selectorstringExplicit section selector; overrides the heuristic. Omit to auto-detect.
headerSelectorstringSticky-header selector to measure the top inset from, for pages where the auto-detect picks the wrong element (e.g. a JS-fixed header). Sections land just below it. Omit to auto-detect; overridden by headerHeight.
headerHeightnumberSticky-header height in px, used verbatim as the top inset. Overrides the auto-detect and headerSelector. Omit to auto-detect.
holdMsnumberdefault 700Hold at each detected section (ms).
durationMsnumberdefault 12000Total clip length (ms) split across detected sections.
maxSectionsnumberdefault 8Cap on the number of sections.
constantVelocitybooleandefault trueDistribute travel time by distance for uniform scroll speed.
includeFooterbooleandefault falseScroll all the way to the page bottom (footer included). Off by default: footers aren't counted as sections and the reel ends at the last content section.
variantsobject
Variant matrix: each cell (color scheme × viewport) is emitted as its own asset.
colorScheme'light' | 'dark' | 'both'Force a color scheme. "both" emits a light AND a dark asset (<name>-light / <name>-dark). Omit to leave as-is.
themeClassstringAdd this class to <html> before capture, e.g. to trigger a CSS-class dark theme.
viewportsViewport[]
Also capture the reel at these viewports; each emits an asset (<name>-<viewport name>).
namestringrequiredLabel for this viewport — used in the asset id / filename (e.g. "desktop").
widthnumberrequiredViewport width in CSS px.
heightnumberrequiredViewport height in CSS px.
deviceScaleFactornumberOverride the generator-level deviceScaleFactor for this viewport. Omit to inherit it.
reframeobject
Reframe the output to a target aspect.
aspect'16:9' | '9:16' | '1:1' | { width, height }Reframe the output to a target aspect: a preset or an explicit pixel box. Omit to keep the capture aspect.
fit'cover' | 'contain'default 'cover'How to fit the capture into aspect: "cover" (scale + center-crop) or "contain" (scale + pad).
padColorstringdefault '#0b0b0f'Pad color used by "contain".
{
name: "home",
url: "https://your-site.com",
generator: "scroll-reel",
options: {
// --- sizing, encoding & output formats ---
output: {
width: 1280,
height: 800,
deviceScaleFactor: 2,
fps: 30,
crf: 18,
// fileName: "home.mp4", // optional; defaults to <slug(name)>.mp4
outputs: ["mp4"],
// gifFps: 15, // defaults to min(fps, 15)
},
// --- page load & end dwell ---
page: {
startDelayMs: 500,
endDwellMs: 800,
waitUntil: "networkidle",
// waitForSelector: "#hero", // optional; wait for an element first
},
// --- frame rendering ---
render: {
// workers: 4, // optional; auto-picks from cores + free memory
frameFormat: "jpeg",
settlePerFrame: true,
settleMaxMs: 250,
},
// --- motion (default is a single top→bottom sweep) ---
motion: {
durationMs: 6000,
easing: "ease-in-out",
loop: "none",
// choreography: [{ to: "#hero", holdMs: 1200 }, { to: "100%", durationMs: 1000 }],
// autoSections: { minHeightFraction: 0.5, holdMs: 700, durationMs: 12000, maxSections: 8, constantVelocity: true, includeFooter: false },
},
// --- variants (each emitted as its own asset) ---
variants: {
// colorScheme: "both", // optional; → <name>-light and <name>-dark
// themeClass: "dark", // optional; class added to <html>
// viewports: [{ name: "mobile", width: 390, height: 844 }],
},
// --- reframing ---
reframe: {
// aspect: "9:16", // optional; preset or { width, height }
fit: "cover",
padColor: "#0b0b0f",
},
},
}Every option also has hover docs in
pro-visu.config.ts— the authoring types are generated from the validation schema, so the editor always matches what the tool accepts.
Examples
Auto-sections
autoSections detects the page's sections and paces a pan/hold down the whole page within a fixed
time budget — hands-off, no choreography to author.
{name: "home",url: "https://your-site.com",generator: "scroll-reel",options: { motion: { autoSections: { durationMs: 9000, }, },},}Boomerang loop
loop: "boomerang" plays the capture forward then back, so the first and last frames match.
{name: "docs-home",generator: "scroll-reel",options: { output: { deviceScaleFactor: 3, }, motion: { loop: "boomerang", autoSections: { durationMs: 22000, holdMs: 1600, }, },},}Straight loop
loop: "straight" runs the motion once, then glides straight back to the top — no retraced
stops — so the clip's last frame lands where the first started and the output loops.
{name: "home",url: "https://your-site.com",generator: "scroll-reel",options: { motion: { loop: "straight", autoSections: { durationMs: 14000, }, },},}Vertical (social) reframe
aspect: "9:16" reframes the same page for a Reel or Story. Add outputs for gif/poster to
cover organic formats too.
{name: "home-vertical",url: "https://your-site.com",generator: "scroll-reel",options: { output: { width: 430, height: 932, }, motion: { durationMs: 5000, }, reframe: { aspect: "9:16", },},}

