# scroll-reel

URL: /docs/generators/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**.

```ts
{
  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`](/docs/configuration#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
> [`interaction`](/docs/generators/interaction) generator.

## Config options

Everything is optional. **Reference** is the interactive view; **TypeScript** is the same shape
in code, every option at its default.

**Reference**

| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `output` | `object` |  | Pixel size, encoding, and output formats. |
| `output.width` | `number` | `1280` | Output width in CSS px. |
| `output.height` | `number` | `800` | Output height in CSS px. |
| `output.deviceScaleFactor` | `number` | `2` | Render scale, max 4 — higher captures crisper, then downscales into the video. |
| `output.fps` | `number` | `30` | Output frames per second (max 120). |
| `output.crf` | `number` | `18` | x264 quality, 0–51 (lower = better quality / larger file). |
| `output.fileName` | `string` |  | Output filename. Defaults to "<slug(asset name)>.mp4". |
| `output.outputs` | `('mp4' \| 'gif' \| 'webp' \| 'poster')[]` | `['mp4']` | Files to emit per variant; each becomes its own asset. |
| `output.gifFps` | `number` |  | GIF / animated-WebP frame rate (max 50). Defaults to min(fps, 15). |
| `page` | `object` |  | Page-load waiting + dwell at the ends of the scroll. |
| `page.startDelayMs` | `number` | `500` | Dwell at the top before scrolling (ms). |
| `page.endDwellMs` | `number` | `800` | Dwell at the bottom after scrolling (ms). |
| `page.waitUntil` | `'load' \| 'domcontentloaded' \| 'networkidle' \| 'commit'` | `'networkidle'` | Page-load milestone to wait for before recording. |
| `page.waitForSelector` | `string` |  | Optional element to wait for (visible) before recording, e.g. a hero section. |
| `render` | `object` |  | Frame-stepped render tuning (parallelism, frame format, per-frame settling). |
| `render.workers` | `number` |  | Parallel render workers, each its own browser context. Omit to auto-pick from cores + free memory. |
| `render.frameFormat` | `'jpeg' \| 'png'` | `'jpeg'` | Intermediate frame format. "jpeg" is faster; "png" is lossless. |
| `render.settlePerFrame` | `boolean` | `true` | Wait for fonts + in-view images before each frame's screenshot. Off in --draft. |
| `render.settleMaxMs` | `number` | `250` | Max time (ms) to wait per frame for settling before screenshotting anyway. |
| `motion` | `object` |  | How the scroll moves: duration/easing, loop, and choreography. |
| `motion.durationMs` | `number` | `6000` | Time to scroll from top to bottom (ms). Ignored when choreography / autoSections drives the length. |
| `motion.easing` | `'linear' \| 'ease-in' \| 'ease-out' \| 'ease-in-out' \| 'ease-out-strong' \| 'ease-in-out-strong'` | `'ease-in-out'` | Easing for the default top→bottom scroll. |
| `motion.loop` | `'none' \| 'boomerang' \| 'straight'` | `'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. |
| `motion.choreography` | `ChoreographyStep[]` |  | 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. |
| `motion.choreography.to` | `number \| string` |  | Target: a 0..1 number, an "NN%" string, or a CSS selector to bring into view. _(required)_ |
| `motion.choreography.durationMs` | `number` | `1200` | Travel time to this target (ms). |
| `motion.choreography.holdMs` | `number` | `800` | Hold time at this target after arriving (ms). |
| `motion.choreography.easing` | `Easing` | `'ease-in-out'` | Easing for the travel to this target. |
| `motion.autoSections` | `boolean \| 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. |
| `motion.autoSections.minHeightFraction` | `number` | `0.5` | Min element height (as a fraction of the viewport, max 2) to count as a section. |
| `motion.autoSections.selector` | `string` |  | Explicit section selector; overrides the heuristic. Omit to auto-detect. |
| `motion.autoSections.headerSelector` | `string` |  | Sticky-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. |
| `motion.autoSections.headerHeight` | `number` |  | Sticky-header height in px, used verbatim as the top inset. Overrides the auto-detect and headerSelector. Omit to auto-detect. |
| `motion.autoSections.holdMs` | `number` | `700` | Hold at each detected section (ms). |
| `motion.autoSections.durationMs` | `number` | `12000` | Total clip length (ms) split across detected sections. |
| `motion.autoSections.maxSections` | `number` | `8` | Cap on the number of sections. |
| `motion.autoSections.constantVelocity` | `boolean` | `true` | Distribute travel time by distance for uniform scroll speed. |
| `motion.autoSections.includeFooter` | `boolean` | `false` | Scroll 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. |
| `variants` | `object` |  | Variant matrix: each cell (color scheme × viewport) is emitted as its own asset. |
| `variants.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. |
| `variants.themeClass` | `string` |  | Add this class to <html> before capture, e.g. to trigger a CSS-class dark theme. |
| `variants.viewports` | `Viewport[]` |  | Also capture the reel at these viewports; each emits an asset (<name>-<viewport name>). |
| `variants.viewports.name` | `string` |  | Label for this viewport — used in the asset id / filename (e.g. "desktop"). _(required)_ |
| `variants.viewports.width` | `number` |  | Viewport width in CSS px. _(required)_ |
| `variants.viewports.height` | `number` |  | Viewport height in CSS px. _(required)_ |
| `variants.viewports.deviceScaleFactor` | `number` |  | Override the generator-level deviceScaleFactor for this viewport. Omit to inherit it. |
| `reframe` | `object` |  | Reframe the output to a target aspect. |
| `reframe.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. |
| `reframe.fit` | `'cover' \| 'contain'` | `'cover'` | How to fit the capture into aspect: "cover" (scale + center-crop) or "contain" (scale + pad). |
| `reframe.padColor` | `string` | `'#0b0b0f'` | Pad color used by "contain". |

**TypeScript**

```ts
{
  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.

```ts
{
name: "home",
url: "https://your-site.com",
generator: "scroll-reel",
options: {
  motion: {
    autoSections: {
      durationMs: 9000,
    },
  },
},
}
```

*Output: autoSections walks the home page top to bottom, holding briefly on each detected section.*

### Boomerang loop

`loop: "boomerang"` plays the capture forward then back, so the first and last frames match.

```ts
{
name: "docs-home",
generator: "scroll-reel",
options: {
  output: {
    deviceScaleFactor: 3,
  },
  motion: {
    loop: "boomerang",
    autoSections: {
      durationMs: 22000,
      holdMs: 1600,
    },
  },
},
}
```

*Output: A boomerang loop over an auto-sections pan — supersampled at deviceScaleFactor 3 for a crisp hero clip.*

### 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.

```ts
{
name: "home",
url: "https://your-site.com",
generator: "scroll-reel",
options: {
  motion: {
    loop: "straight",
    autoSections: {
      durationMs: 14000,
    },
  },
},
}
```

*Output: One auto-sections pass down the page, then a single glide home — a lighter-touch loop than boomerang.*

### 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.

```ts
{
name: "home-vertical",
url: "https://your-site.com",
generator: "scroll-reel",
options: {
  output: {
    width: 430,
    height: 932,
  },
  motion: {
    durationMs: 5000,
  },
  reframe: {
    aspect: "9:16",
  },
},
}
```

*Output: The same scroll, reframed vertical for social.*
