# Video Scroll Hero

> Build a scroll-scrubbed VIDEO hero section — a prepared video file is REQUIRED. The visitor's scrollbar becomes a film scrubber (forward and backward, butter-smooth on phones), with an optional mid-film "message chapter" where the film freezes and scrolling walks through glass-card messages, ending in a baked-blur dissolve into ANY static end content (a signup form, a product panel, whatever the user wants). Use when the user says "scroll video hero", "scrollytelling video landing", "video scrub hero like TakeonAI", or has a hero video and wants scrolling to play it. NOT for 3-D/WebGL scenes, model viewers, or pages without a real video.

- Skill: `idealockin/video-scroll-hero` (Agent Skill)
- Install (CLI): `npx skillmds@latest add idealockin/video-scroll-hero`
- Raw SKILL.md: https://api.skillmd.com/api/skills/idealockin/video-scroll-hero/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: idealockin (https://skillmd.com/u/idealockin)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/idealockin/video-scroll-hero

---


# video-scroll-hero — a film the visitor scrubs

This skill turns one prepared video into a cinematic landing hero: scrolling
plays the film, rewinding plays it backward, an optional frozen-scene chapter
makes visitors actually read your key messages, and the film's final frames
dissolve into whatever real content the site needs. It was distilled from the
production hero at [TakeonAI.com](https://TakeonAI.com) (by
[@ideaLOCKIN](https://x.com/ideaLOCKIN)) after weeks of on-device tuning; every
rule below earned its place by fixing a real, shipped failure. You (the AI
reading this) are the engineer — this skill hands you the laws, the recipes,
and the numbers that survived production. Adapt freely; verify with the probes.

**Hard requirement: the user must already have a video.** One continuous shot
with a clear journey works best. If they don't have a video, or they want a
3-D/WebGL environment, this is the wrong skill — say so and stop.

## 1 — Interview the user first (always)

Ask, in one batch, and don't build until answered:

1. **The video**: file path, duration, fps, resolution. Any length works —
   everything below scales from `DURATION × FPS = total frames`.
2. **Mobile**: a real vertical (9:16) companion video, or a center-crop of the
   main one? (Center-crop is fine when the action is center-framed; never
   auto-pan the crop — moving crops read as nauseating camera drift.)
3. **The message chapter (optional)**: do they want the film to pause at one
   meaningful frame while scrolling walks through 3–7 short messages? If yes:
   which moment of the film (describe it; you'll find the exact frame), and
   what are the messages (offer to draft them — punchy line + one supporting
   sentence each works well)?
4. **The end content**: what should the film dissolve into? A form, a CTA
   panel, a product embed — THEIR content. Never invent it; build a clean
   placeholder if they haven't decided.
5. **Feel choices**: free two-way scrubbing (recommended) — and whether the
   opening should show a hook line + a "scroll" cue.

## 2 — The asset pipeline (video → frames)

Frames beat `<video>` seeking for scrubbing: deterministic both directions,
park-anywhere, no decoder quirks.

- Extract master PNGs with ffmpeg, then encode **two WebP sets from the
  master** (never from intermediate JPEGs): desktop ≈ 1440-wide at the video's
  native fps, quality ≈ 62; mobile ≈ 480-wide center-crop at ~⅔ the fps
  (e.g. 24 → 16), quality ≈ 28–36. Budget guide: desktop ≈ 10–13 MB total,
  mobile ≈ 2–4 MB.
- **Bake the end-blur into the frames.** For the final ~12% of both sets,
  apply a gaussian blur that ramps from 0 to heavy (σ≈18 at ~810px tall) at
  encode time. The runtime then NEVER runs a CSS blur filter — the dissolve
  under the end content costs nothing but a cheap composited `scale()` zoom.
  This is the single biggest processing-power saver in the whole design.
- Serve frames from a **versioned folder** (`/frames/v1/…`) with
  `public, max-age=31536000, immutable` caching, and bump the version on EVERY
  re-encode — changed bytes must live at a new URL or returning visitors get
  mixed films.
- Export a small `poster.jpg` for the no-JS/failure fallback.
- Choose the set once per mount by the smallest CSS viewport dimension
  (≤ 520px → mobile set; landscape phones keep the vertical set — frames never
  rotate; tablets get desktop and fill by zoom, letterboxing only at extreme
  aspects, centered, on the page's night color). Preload the first ~12 frames
  of each set with media queries that mirror the same breakpoint.

## 3 — The engine laws

A single `<canvas>` in a sticky, viewport-sized stage draws the current frame;
a tall scroll driver supplies the timeline. These laws are ordered by how much
pain they prevent:

1. **Scroll sets a TARGET; a rAF loop chases it under a sweep cap.** Lerp
   (`step = (target − playhead) × 0.12`) capped at `fps × ~2.5` (mobile) /
   `~2.8` (desktop) film-frames per second. Slowing the cap does NOT smooth
   anything — perceived smoothness is distinct images per second (law 4).
2. **Never draw far from the playhead.** Any stand-in frame (decoded or raw)
   must be within ~6 frames of the requested one; otherwise keep the last
   drawn frame. Violating this shows wrong-scene flashes.
3. **The playhead never enters undownloaded film.** Fold a supply frontier
   into the lerp bound so buffering EASES the film to a halt and back (never a
   per-frame clamp stutter). Urgent-fetch what the playhead needs now
   (fetchPriority high, small concurrency), give every frame a 2-retry budget
   then a permanent failed set, and run a dead-network watchdog (~25s of held
   playhead with ZERO deliveries → swap to poster + end content, so the page's
   purpose is never stranded).
4. **The decode economy is budgeted and truly off-thread.**
   `createImageBitmap(HTMLImageElement)` decodes ON the main thread in
   Chromium — fetch frames as **Blobs** and decode `createImageBitmap(Blob)`
   so decodes ride the worker pool. All decode work draws from ONE per-tick
   budget (~4 issues/tick) with priority (imminent playhead first). Decode a
   stride grid while sweeping fast (`stride = clamp(floor(sweepSpeed/20), 1,
   2 mobile / 4 desktop)`) and backfill at rest. Keep a sliding bitmap window
   (~2s ahead, ~3s kept) and evict outside it — phones enforce real decoded-
   image memory ceilings.
5. **The root document never scrolls.** Put ALL scrolling inside a fixed,
   full-viewport container: mobile browser bars then never collapse, expand,
   or rubber-band, and the viewport height never changes mid-gesture. Make the
   container **1 CSS pixel short** of the viewport — Chrome Android promotes
   an exactly-viewport-sized scroller to "implicit root scroller", which
   re-enables URL-bar hiding; the 1px opt-out keeps the bars parked. Size the
   stage with `100dvh` (never lvh — the largest-viewport unit overhangs and
   "slams" the header at scroll extremes). Ship `theme-color` matching the
   film's darkest tone + `viewport-fit=cover`, and paint the page underlay
   that same color so nothing can ever flash white behind the film.
6. **The message chapter is a flat segment in the scroll→film curve — not a
   lock.** Piecewise-map scroll: film act one → message zone (film holds at
   the chosen frame, a zone-progress value runs 0→1) → film act two → a short
   runway where the film holds its final frame. Fully two-way, no input
   locking, every other law rides through untouched.
7. **Overlays gate on the right clock.** Overlays that belong to a FILM MOMENT
   (the message layer over the frozen scene) gate on the PLAYHEAD being there;
   overlays that belong to a SCROLL RANGE gate on scroll. Mixing them makes
   cards flash over mid-flight footage during fast flings.
8. **Messages are unblastable FORWARD — and never paced backward.** The
   message column follows a rate-limited progress value (~0.25–0.4 zone/s)
   that only ADVANCES while the film is genuinely parked on the chapter
   frame — and the FILM's forward bound waits at that frame until the
   column completes. A mega-fling parks the film, plays the copy at reading
   pace, then releases. But on ANY reversal the column snaps to the
   scroll-derived target instantly: a paced rewind reads as an ignored
   gesture (this shipped both-directions once, users hated the
   direction-change delay, and it was rolled back). While the column is
   still behind the scroll ("story debt"), exempt the forward leash of
   law 9 — it otherwise trims the banked fling and freezes the story
   partway through, waiting for a swipe the visitor never owed. Wheels are
   deliberate; consider pacing touch devices only and leaving desktop's
   column a pure function of scroll.
9. **Freshness — never let scrolling feel "committed", and never write the
   scrollbar mid-gesture.** Raw scroll can bank unbounded lead over the
   capped film; reversing then feels ignored until the lead unwinds. Two
   rules: **cash-out** — on any reversal against the lead, re-anchor to the
   PRESENTED state (film + column), preserving that gesture's own delta;
   **forward leash** — the scroll may never run more than ~2.5 screens
   ahead of the presented state. Do NOT leash backward: reversal freshness
   is cash-out's job, and a backward leash fights the zone's state-coupled
   inverse mapping. Deliver BOTH as a **virtual mapping offset**
   (`effective = physical + offset`, consumed by the scroll→film mapping),
   never as a programmatic `scrollTo` — every iOS browser is WebKit, and
   any scrollTop write mid-momentum kills the fling dead. Fold the offset
   into the physical scrollbar with ONE real write only at true idle
   (~250ms of scroll stillness, no finger or button down), and ZERO it
   when the fold lands on a track edge — carried edge residue is a
   permanent mapping shift. Finally, **rail-drain the offset continuously,
   in the mapping itself**: the effective position may never sit beyond
   the track's ends, and a physical rail must always mean the matching
   film end — discard excess the moment a clamp would eat it (pure state
   math, gesture-safe). Idle-only cleanup cannot rescue an ACTIVE scrubber:
   chained corrections once banked an offset nearly the whole track long,
   and the visitor scrolled ~100% of a dead scrollbar while the film sat
   frozen. Between-gestures discipline applies to scroll WRITES; virtual
   state may adjust any time.
10. **Designed stops land with a brake, and pre-decode their zone.**
    Approach at full sweep, then a constant-deceleration tail
    (`v = vmax·√(d/D)`, D ≈ 0.45s of film) — never an exponential crawl,
    never a dead wall. Pre-decode every frame of the stop's brake window,
    TRICKLED a few frames per tick starting well out — a bulk decode burst is
    its own jank (and the chapter frame is usually the film's most expensive
    imagery).
11. **Floor every mid-sweep overlay at 0.002 opacity.** At literal opacity 0,
    Android Chrome drops composited layers despite `will-change`, then
    re-rasters big blurred text mid-scroll (a 200ms visible stutter on real
    devices). An invisible epsilon keeps layers rastered from boot, where the
    opening hold's idle absorbs the cost.
12. **Opening hold, honestly.** Lock input ~1s (load-gated up to a few more
    seconds until the opening ~2s of film has arrived), cap the loadbar at
    92% while locked, and reveal the scroll cue at the ACTUAL release. Use a
    **ghost gesture** cue that PERFORMS the input (a mouse glyph with a
    rolling wheel-dot on desktop; a fingertip comet in the correct physical
    direction on touch) — chevrons under-teach the gesture. A branded
    "loading" screen is optional and often worse than nothing: the hook
    line itself can BE the load-in — stagger its words visible one by one
    (opacity-only keyframes, ~0.15s apart, ~0.55s fades, `fill: both`)
    over the page's night underlay while the opening frames arrive. The
    words carry the wait; nothing says "loading".
13. **Measure, never eyeball.** Sample the canvas (~30ms cadence, small
    downscale, mean-abs pixel diff) to build motion profiles: smooth = a
    clean speed ramp; broken = freeze-then-leap. Test at 4×–6× CPU throttle
    AND on slow network (the two conditions that separate dev machines from
    real phones). Ship a dev contract: `?jump=<scrollY>` lands pre-scrolled
    skipping the hold, a param to disable freshness reconciliation (tests
    teleport with atomic jumps real gestures never produce), a build-version
    global, and a tiny optional on-screen diagnostic overlay — on-device
    screenshots of it will solve bugs emulation cannot reproduce. Harness
    gotchas that masquerade as engine bugs: a background/unfocused tab
    throttles rAF to ~2Hz (focus the tab before any speed-dependent probe);
    per-mount decisions like frame-set choice need a RELOAD after the
    harness resizes the viewport; pixel-luminance probes are blind to
    dark-on-dark wrong content — a phone screen recording (provable frame
    range + scrollbar position) out-diagnoses emulation for supply bugs.

## 4 — The message chapter, concretely

- Find the chapter frame by CONTENT (screenshot candidate frames and pick the
  strongest composition — ideally an index that divides both frame sets
  evenly). Zone length ≈ 55vh of scroll per message.
- Cards: alternate left/right inside a centered track (~max-w-3xl desktop;
  ~76% width on phones so the scene stays visible). Frost (backdrop blur
  ~22–28px, a faint dark tint, an `@supports` solid fallback) works
  beautifully over a frozen scene — the blur is nearly free because the
  backdrop never changes while the zone is active. A dark veil under the
  cards is optional taste; frost alone often carries readability.
- **The backdrop-root trap:** if ANY ancestor between a backdrop-filter
  card and the film's canvas carries a `mask-image`, `clip-path`,
  `filter`, or animated `opacity < 1`, that ancestor forms a backdrop root
  and the blur samples its own (empty) subtree instead of the footage —
  the card renders as bare text. Keep every ancestor unmasked at opacity 1
  and gate the CARDS individually. So no mask-gradient card viewports:
  cards enter and exit by viewport CLIPPING, like physical panes.
- **Never opacity-fade a frost card** — fading the card dilutes its own
  blur, so the frost only reads at full opacity and visibly "pops".
  Clip, don't fade. Drop cards to true opacity 0 while the chapter is
  closed (standing blur layers are real GPU cost on phones), and land
  their first paint on the parked frame, never mid-sweep.
- The film freezing + cards sliding = the "pause" — visitors never feel a
  lock because there isn't one.
- **Offer the chapter eyes-open.** It is the highest-maintenance feature
  in this skill: scroll runway alone loses to phone momentum (one moderate
  flick can blast the whole zone), pacing invites the reversal-delay trap
  (law 8), and the overlay demands the frost laws above. The production
  site this skill was distilled from iterated its chapter nine ways and
  ultimately shipped film-only. It can be gorgeous — budget real
  on-device time for it, or skip it.

## 5 — The ending

The baked-blur tail IS the transition: as the film's last frames play, the
image is already dissolving. Rise the end content over it (opacity +
translate), make it interactive ONLY when the film is truly done (p ≥ ~0.99;
`inert` + pointer-events none while hidden; reset its inner scroll on
deactivate; hide its inner scrollbar so activation doesn't shift the layout),
and wrap any dynamically-loaded end content in an error boundary so a failed
chunk can never take down the page the film worked so hard to deliver. Mount
heavy end content early (during the message chapter's idle if there is one —
never mid-sweep, where the mount hitch is visible on phones).

## 6 — What NOT to do

- No `<video>`-element seeking for the scrub, no scroll-hijack libraries, no
  auto-scroll, no fake scrollbars.
- No CSS blur/filter animation over the canvas — bake it into frames instead.
- No moving crops to chase off-center subjects on mobile.
- No timed text locks — if content must be read, make it a scroll segment.
- No scroll-position adjustments while a finger or mouse button is down.
- Don't relitigate these laws when a symptom moves after a fix — find which
  law the new location violates; the classes recur, the locations don't.

Build it, verify it with the probes at throttle, and hand the user a hero
their visitors will scrub twice.

