Launch video
You are the director and the product storyteller. The editor comes second. Every shot has to earn its place by moving the viewer one step closer to "I get it, I want it."
The video is a web page with a single clock. window.seek(T) draws global second T. Playwright screenshots it at 30fps, and ffmpeg encodes the frames together with a voiceover and a music bed. Nothing depends on wall-clock time, so any frame can be re-rendered on its own. That lets you fix one scene without touching the rest.
Starter files are in template/ and scripts/, and the README explains how to run them.
1. Story first (before any pixels)
Write the script as sentences keyed A, B, C and so on. Each sentence becomes one caption and one beat. Use this four-beat structure:
- Hook (0–8s). Name the pain in the viewer's words. No logo wall, no "introducing". The first 8 seconds decide whether anyone watches the rest.
- Product in action (8–45s). One problem, then the feature that solves it, then the result. Show the real product doing the real thing: typing a prompt, getting an answer, hitting publish. Avoid a feature parade, and show at most 4–5 capabilities.
- Why it's different (45–75s). Show the architecture or integrations story, ideally with a diagram that moves. Explain who does what. For example: "We do X; Stripe does payments and taxes."
- Offer + close (last 10–15s). Give the price or plan, and end on one brand line. Make that last line short and memorable.
Aim for 80–95s total. A 30s ad cut and a 9:16 cut can be made later from the same timeline.
2. Visual rules (hard constraints)
- No browser chrome. Never show tabs, URL bars or bookmarks. Rebuild product screens as clean HTML cards, using the real recording only as a reference, and anonymize all data.
- Product images float. Give them rounded corners (16–18px) and soft long shadows. Keep generous padding so the card never fills the frame; about 75% of the width at most.
- No words on top of product images. Explanations go in callouts beside the UI or in captions below it, never across the screenshot.
- The accent color appears only on dark backgrounds. Never use it on a product image. If the brand line needs the accent, put the end card on dark brand color.
- Don't make everything one hue. Use one or two complementary pops, for example a red like #C8323C on an alert dot or a purple for a partner brand, sparingly.
- Focus one area at a time. To spotlight a feature, the cursor moves there, double-clicks, the camera pushes in, and the rest of the UI greys or blurs out.
- Callouts are small white pills with a colored dot, anchored to real DOM elements. They pop in with a slight overshoot, at most three on screen, and they disappear before the shot exits.
- Background: slow-drifting, blurred brand-color blobs plus a 6% grain. It should be calm and never compete with the product. Don't copy a banner design verbatim.
3. Motion vocabulary
- Shot transition = blur-morph. The shot enters at scale 0.92 with an 18px blur fading to 0, eased out over about 0.7s. It exits with scale +5%, blur 18px and opacity to 0 over 0.55s. Overlap the shots by about 0.1s. Avoid gimmicky transitions such as pixel wipes or fabric pulls; they read as cheap.
- Slow push-in of 3–5% across each shot, so nothing is ever static.
- Element entrances use ease-out over 300–650ms, staggered 80–150ms apart (
stg()). - Cursor: smooth eased moves, a press animation on click (scale 0.82 for 120ms), and holds of about 0.3s before clicking.
- Dock intro (0–2.5s): a frosted macOS dock with real Apple app icons (take them from an iOS/macOS UI kit, not hand-drawn ones) and your app icon in the middle. The app's slot grows, the camera zooms toward it, the cursor clicks, the icon bounces with a running dot, and the scene blur-morphs into the app.
- Cadence: let each beat breathe. Leave 0.8–2s of visual hold after a VO sentence before the next idea. If it feels rushed, it is. Re-space the VO before cutting the visuals.
4. Audio
- VO: generate the voiceover with the founder's ElevenLabs voice clone, one take of the whole script. Transcribe it (Scribe) to get word-level timings, then re-space it: cut at the silences between sentences and insert extra silence where a beat needs room. Write the sentence start and end times to
timeline.jsasTL={A:[start,end,text],...}, and key every animation offTLrather than hard-coded seconds. - To change one line, generate just that line, splice it in at a silence, and update its
TLentry. - Music: an instrumental bed. Check that the transcript is empty to make sure it has no vocals. Mix it at volume 0.3 with sidechain ducking under the VO, and fade it out over the last 3–4s (see
scripts/mix.sh).
5. Pulling an animation from your website
Scroll-driven site animations can be captured frame by frame (scripts/capture-scroll.js):
- Hide the nav, cookie banners and the section's text so only the visual remains.
- Capture densely, using 4px scroll steps. A 16px step played back over 10s works out to about 6fps and looks laggy.
- In the canvas, blend adjacent frames by fractional index for sub-frame smoothness.
- If you split a capture across parallel browsers, warm each one up by scrolling from the same start position first. Scroll animations are stateful, so a fresh load in the middle of the page jumps at the seams.
- Check for seams by scanning the mean absolute difference between consecutive frames. Spikes well above the median mean a jump.
6. Pipeline
- Write the script, then generate the VO, then transcribe it, re-space it and write
timeline.js. - Build scenes in
scenes.js. Each one isS({tIn,tOut,s0,push,ox,oy,build(){...},update(T){...}}). - Check stills with
node scripts/preview.js page.html out.png T ...at the key beats. - Render with
scripts/render-parallel.sh. Re-render only the time ranges you changed. - Mix with
scripts/mix.sh, then encode withscripts/encode.sh(libx264 crf 17, yuv420p, AAC 192k, faststart). - QA: pull frames from the encoded mp4 at every scene, and check the brand line, the callouts and the seams. Listen through once at full speed before sending.
7. Gotchas learned the hard way
- Run renders as a single tracked background job that waits on its workers. Detached
nohupjobs get killed and leave partial frames behind. - Preloading hundreds of large images per page can crash Chromium with "Target crashed". If a page preloads a big image sequence, use at most 2 parallel workers. Always re-check the frame count and the worker logs before encoding, because a crashed worker silently leaves stale frames behind.
- Class names can collide with the app's CSS. For example,
.darkin the app stylesheet made a card 1080px tall, so prefix scene classes or use unusual names. - Callouts must clamp to the shot's exit time, or they linger over the next scene.
- Don't overstate technical claims in callouts. Write "No API round trip · decided in-process", not "0 ms".
- When changing the outro line, also remove its caption if the end card already shows the same words.