---
name: product-film-motion-graphics
description: Build a finished, code-driven motion graphics product film (MP4 with synced original audio) for a real website or app. Use when asked for a launch video, product film, explainer animation, promo clip or "motion graphics video" of a product. Covers research on the live product, a deterministic HTML/CSS renderer, a central timeline, easing maths, a synthesised soundtrack, Playwright frame capture, an ffmpeg encode, and a self-critique loop. Do not use for text-to-video generation or for editing existing footage.
---

# Product film from code

You are making a real video file, not a storyboard. The picture is a web page you write. Chromium draws it frame by frame, and ffmpeg turns the frames into an MP4. Every property is a pure function of time, so the film is reproducible, editable and exact.

## Non-negotiables

1. **Real product, real data.** Inspect the live product before designing. Rebuild its UI from the real tokens (colours, fonts, spacing) and real content. Never invent metrics, users, reviews, rankings or screenshots. Leave out rotating ads/sponsored slots, or label them as sponsored.
2. **Deterministic.** `render(t)` must set *every* visual property on *every* call. No CSS animations or transitions, no `Date.now()`, no `Math.random()` (use a seeded PRNG), and no early `return` that skips resetting an element.
3. **One timeline.** All times live in `timeline.js` (pure data, no DOM). The renderer and the audio generator both import it.
4. **Look at your own output.** Render stills and contact sheets, view them, write down the 3 worst problems with timestamps, fix them in code, and re-render. Repeat.
5. **Be honest about limits.** You cannot hear the audio. Verify it with meters (LUFS, true peak, flat factor, silence detection) and a waveform/spectrogram, and tell the user to listen before publishing.

## Project layout

```
film/
  src/index.html      loads fonts.css + styles.css, builds DOM, exposes window.__render(t), window.__ready
  src/timeline.js     FPS, DURATION, SCENES, key times K, seeded keystroke schedules, cues()
  src/lib/motion.js   bezier(), ease presets, spring(), prog(), span(), bez2()
  src/data.js         real content captured from the product
  src/components.js   DOM builders (built once)
  src/render.js       director: render(t)
  scripts/research.mjs   Playwright session on the live product: screenshots + extracted text/images
  scripts/render.mjs     static server + N Playwright pages -> frames -> ffmpeg
  scripts/audio.mjs      synth score + SFX from cues() -> WAV -> loudnorm
  scripts/check-determinism.mjs, scripts/contact.py
  docs/  preproduction, storyboard, style guide, production notes
```

## Workflow

### A. Research (before any design)
- Drive the live site with Playwright: home, search, the results, tabs, a detail page and the signup/submit flow. Save screenshots, plus the extracted text and image URLs, to JSON.
- Read the codebase if you have it: design tokens, logo component, fonts.
- Download every image you will show into `assets/` and record each source URL in a manifest.
- Use `waitUntil: 'load'` plus a fixed delay, not `networkidle` (analytics never go idle). Click `:visible` inputs; responsive sites often hide duplicates.

### B. Pre-production docs
Write the creative direction (one paragraph), style guide (tokens, two type families max, motion principles), storyboard with exact times, asset inventory (real vs supporting graphics) and the technical plan.

### C. Motion system

**Canvas.** Use a 1920×1080 CSS viewport and render at `deviceScaleFactor: 2` to get 3840×2160. Build the product UI at its real CSS width (for example 1440 px) inside a `#cam` element and move it with one transform:

```
canvas = T + s · page          (transform-origin 0 0)
full-bleed:  s = 1920/1440 = 4/3, T = (0, 0)
focus a page point P at canvas point C:  T = C − s·P
```

Use a small number of camera states (full, push-in, split) and only interpolate between them. Interpolating `{s, x, y}` with the same eased progress works fine.

**Easing.**
```js
// CSS-identical cubic bezier: solve x(u) = t (Newton, then bisection fallback), return y(u)
ease.out   = bezier(.16, 1, .3, 1)    // arrivals
ease.inOut = bezier(.65, 0, .35, 1)   // camera, layout morphs
ease.in    = bezier(.55, 0, .9, .45)  // exits
spring(t)  = 1 − e^(−ζωt)(cos ω_d t + (ζω/ω_d) sin ω_d t),  ω = 2πf,  ω_d = ω√(1−ζ²)
prog(t, a, b, e) = e(clamp((t − a)/(b − a)))
```

**Typography.** Reveal type through masks: a `span.mask` with `overflow: hidden` around an inner span, moved with `translateY(108% → 0)` and staggered 60–90 ms per word. Exit upward the same way.

**Identity-preserving transitions.** An object should become the next object: fragments → a line → the search bar → the page (a `clip-path: inset(... round r)` growing from the bar's exact geometry); a result → its page; the header → the end card. When two elements morph together, offset their progress windows so they never cross (lead with one, trail the other by about 0.15–0.2 s).

**Cursor.** Use keyframes `[time, point]`, move with ease-in-out along a cubic Bézier with a slight perpendicular bow, add a 14% press scale on clicks plus a faint ripple. Click targets come from `offsetLeft/offsetTop` sums, which ignore transforms. Move the cursor off text after a click.

**Typing.** `t[i+1] = t[i] + b·(0.75 + 0.5·r()) + (space ? 0.35b : 0)`, with b ≈ 40–60 ms and a seeded `r`. The same times drive the keystroke SFX.

### D. Sound (`audio.mjs`)
- Synthesise everything (no samples, so no licensing questions): polyBLEP saw pads through a low-pass, FM plucks and bells (`sin(2πft + I·e^(−kt)·sin(2π·ratio·f·t))`), sine+saw bass, a pitch-swept sine kick that also writes a sidechain envelope, noise claps and hats, band-pass noise sweeps for whooshes, and short filtered noise for keys and clicks.
- Build the chord plan around scene times. Put the musical "drop" on the main reveal and drop the drums out for the "moment of clarity". The sonic logo is one note per logo element.
- Reverb is a Schroeder/Freeverb on a send bus. Use soft clipping (`tanh`), a 30 Hz high-pass and a fade to silence by the last frame.
- Master with two-pass `loudnorm` to I = −14 LUFS, TP = −1 dBTP. Verify with `ebur128`, `astats` (flat factor 0 = no clipping) and `silencedetect`, then render `showwavespic` and `showspectrumpic` and look at them.

### E. Capture and encode
```js
await page.evaluate((t) => window.__render(t), t);
await page.evaluate(() => new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r))));
await page.screenshot({ path, type: 'jpeg', quality: 95, animations: 'disabled', caret: 'hide' });
```
- Use 4–6 parallel pages that interleave frame indices; decode all images (`img.decode()`) and await `document.fonts.load(...)` before the first measurement.
- Chromium flags: `--font-render-hinting=none --disable-lcd-text --force-color-profile=srgb`.
- Encode with `-vf scale=out_color_matrix=bt709:out_range=tv,format=yuv420p -c:v libx264 -profile:v high -preset slow -crf 15 -r 60 -c:a aac -b:a 320k -movflags +faststart`. Then confirm the BT.709 tags. If primaries show as "unknown", remux with `-bsf:v h264_metadata=colour_primaries=1:transfer_characteristics=1:matrix_coefficients=1`.
- Render a 960×540 / 30 fps preview first (about 1 minute). Do the 4K60 only after the critique passes. Section re-renders (`--from/--to`) plus an encode-only step avoid redoing the whole film.

### F. Critique checklist (run on contact sheets every 0.5 s)
- Does frame 0 already move? Is the opening idea clear in under 2 s?
- Is the key UI text readable at phone size? If not, push the camera in rather than shrinking the type.
- Do any elements collide mid-morph? Check the frames at 25/50/75% of each transition.
- Are there stale elements left from earlier scenes? (Run the determinism check.)
- Any placeholders, broken images, invented numbers or unlabelled ads?
- Does each headline hold for at least about 1.5 s after it fully lands, and the end card for at least 2 s?
- Logo: exact letterforms and colours, with no distortion.

### G. QC before delivery
Report the exact duration, frame count (`-c copy -f null` frame=), resolution, fps, colour tags, audio LUFS and true peak, and file size. Extract the first and last frame and both sides of every scene boundary, and look at them.

## Deliver
The MP4, the source project, the storyboard, the style guide, a contact sheet, a poster frame (PNG at 4K), production notes (substitutions, real vs supporting graphics, limits), and the rebuild commands.
