product-motion-ad

Make a motion ad, launch film or product explainer video from real captures of your product, rendered frame-exactly to MP4 for a landing page (16:9) and phones or social feeds (4:5).

skill28 filesMIT
Download .tar

Install

curl -fsSL https://vzdesenko.xyz/skills/product-motion-ad.tar | tar -x -C ~/.claude/skills/

SKILL.md

---
name: product-motion-ad
description: Make a motion ad, launch film or product explainer video from real captures of your product, rendered frame-exactly to MP4 for a landing page (16:9) and phones or social feeds (4:5). Use this whenever someone asks for a promo video, motion ad, demo video, hero or landing-page video, launch video or an animated explainer of their app, SaaS or site, even if they only say "make a video for my landing page", "something that explains what my app does" or paste another video-making skill. Covers an 18-second sound-off ad and a 45–70 second Apple-style launch film, capturing the real UI, storyboard and copy, a GSAP timeline, rendering with a bundled ffmpeg, stills and video QA, versioned delivery and embedding; optionally AI footage (Nano Banana stills animated with Veo) and music, sound effects and voices with ElevenLabs.
license: MIT
---

# Product motion ad

Turns a product into a video built from **captures of the real product**: one HTML page driven by
a paused GSAP timeline, rendered frame by frame in Chrome and encoded to H.264. Two formats share
the same renderer:

| Format | Length | Start from | When |
|---|---|---|---|
| **Motion ad** | 18 s, sound off | `assets/template.html` | a landing-page hero loop or a feed ad: five beats, one headline each |
| **Launch film** | 45–70 s | `assets/film-template.html` + `assets/film-kit.js` | a launch: the product used for real, big statements between demo moments, optional footage and sound. Read `references/launch-film.md` |

Optional layers, each only when the person wants it and each gated on their go-ahead because it
costs money: **AI footage** (Nano Banana stills, Veo clips: `references/ai-footage.md`) and
**sound** (ElevenLabs music, sound effects and voices: `references/sound.md`).

Motion ad, default story (five beats, each one headline):

| Time | Beat | What moves |
|---|---|---|
| 0–3.4s | **hook** | a phone rises showing the product's key screen, which scrolls |
| 3.4–7.2s | **ui** | the real desktop UI slides in; a cursor clicks, the state changes, highlights appear |
| 7.2–10.6s | **gallery** | a tilted wall of real outputs; the phone re-dresses the same content in other looks |
| 10.6–14s | **proof** | one value changes on the live page; a callout points at it (before → after) |
| 14–18s | **cta** | brand-colour wipe, logo, promise, "free" chip, a button being clicked, URL |

Change, cut or reorder beats when the product needs it (a CLI tool has no phone beat; a
marketplace's gallery is listings, not templates). Beat start times sit in one `T` table.

## Principles

- **Real product, real claims.** Every screen is a capture (or the live components) of the actual
  product with realistic demo data. Every claim (price, "free", limits, counts, speed) must appear
  on the page the video lives on or in the product itself. Never invent customers, ratings, logos
  or results. Mask anything not public yet (a domain in an embed snippet becomes `".../embed.js"`;
  an undecided product name stays out, the logo mark can stay).
- **Benefit, not mechanism.** Viewers must get each beat with the sound off in about a second; see
  `references/storyboard-and-copy.md` for the rules that came out of real review rounds.
- **Match the brand, not the template.** Colours, fonts (titles in the landing page's display
  face), logo and tone come from the product's own CSS. Nothing in the templates is a brand; replace
  every token. No hype, no exclamation marks, no grey text on video.
- **Restraint.** Every element has to explain something. Footage and statements punctuate the demo;
  they don't replace it.
- **Paid steps wait for the person.** Generate options, show them, stop. Animate only the stills
  they picked, compose music only after the silent cut is approved, add voices only if they want
  them. Never print API keys.
- **Deliver versions, never overwrite.** `~/Downloads/<product>-<topic>-<format>-v<N>.mp4`, plus a
  `-web.mp4` encode for pages and a `-music.mp4` when there is sound.

## Workflow

```
- [ ] 1. Research the product and the page the video will sit on
- [ ] 2. Pick the format (motion ad or launch film) and set up the work directory
- [ ] 3. Capture the real UI (or mount the live components)
- [ ] 4. Storyboard and copy
- [ ] 5. Optional: AI footage, only after a yes (stills → their picks → clips → their trims)
- [ ] 6. Fill CONFIG and build
- [ ] 7. Review stills, fix, repeat
- [ ] 8. Render, QA the encoded file, deliver
- [ ] 9. Optional: music, sound effects, voices (last, on the approved cut)
- [ ] 10. 4:5 cut for phones (usually wanted once it's on a site)
- [ ] 11. Embed on the site, if asked
```

### 1. Research

Read the destination page (source in the repo, or fetch the URL) and pull: the headline promise,
what is free and what is paid, real limits, the three or four features that matter, and the brand
tokens (accent, ink, surface colours, display and body fonts, logo). Read the product's config for
facts the video may state (number of voices, languages, integrations) instead of guessing. Ask only
what can't be inferred: where the video goes (landing page → 16:9; feed/mobile → 4:5; stories →
9:16), which format, and whether they want footage or sound. A design doc that says "no motion" on
the landing page still yields to an explicit request for a video; mention the reduced-motion
trade-off once when embedding.

### 2. Work directory

```bash
S=<path to this skill>                       # e.g. ~/.claude/skills/product-motion-ad
W=<scratch dir>/video && mkdir -p $W/a && cd $W
(cd $S/scripts && npm install)               # once: playwright-core, gsap, ffmpeg-static (no browser download)
cp $S/scripts/node_modules/gsap/dist/gsap.min.js $S/scripts/node_modules/gsap/dist/MotionPathPlugin.min.js .
cp $S/assets/template.html index.html                                   # motion ad
# or: cp $S/assets/film-template.html index.html && cp $S/assets/film-kit.js .   # launch film
```

Run the scripts from the skill folder (`node $S/scripts/render.cjs …`, `python3 $S/scripts/…`)
with the work directory as the current directory.

### 3. Capture the real UI

Follow `references/capturing-real-ui.md`. The motion ad expects:

| CONFIG field | Capture |
|---|---|
| `hook.screen` | the key screen at phone width: element screenshot, 390 CSS px wide, DPR 3, full height |
| `proof.after` | the same capture with **one** value changed (price, status, count) |
| `ui.states` | two desktop captures, before and after a click, 1600×1000 CSS at DPR 2 |
| `gallery.wall` | 12–30 real outputs (templates, pages, reports), portrait crops around 3:4 |
| `gallery.variants` | up to three phone captures of the *same* content in other looks |
| `proof.card.image` | a QR code for the real URL, a receipt, a share sheet… or `card: null` |

`scripts/capture.cjs` screenshots a URL or one element with dev overlays hidden, first-run tours
skipped (`--storage`) and clicks performed first (`--click`). `scripts/diff-box.py before after
--dpr 3` returns the CSS box of the changed value for `proof.box`. When pages sit behind a login,
add a temporary route that renders the real components with demo data, capture, then delete it.
For a launch film, anything that moves (a chat, a widget, a dashboard changing state) is better as
the app's own markup and compiled CSS inside a shadow root, driven by the timeline
(`references/launch-film.md`, "The page").

### 4. Storyboard and copy

Motion ad: five headlines (two lines, ≤ ~16 characters per line in 16:9), up to three pills per
beat, the callout and the end card. Launch film: the scene table in `references/launch-film.md`,
statements of two to five words, every scene a multiple of 0.5 s on a 120 BPM grid. Read
`references/storyboard-and-copy.md` first; it lists the copy patterns reviewers rejected.

### 5. Optional: AI footage

Only if they want cinematic filler. `references/ai-footage.md`: prompts that come out realistic,
`scripts/gen-images.py` (Nano Banana), their picks, `scripts/animate.py` (Veo, cheapest model that
looks right), their trims, then `scripts/prep-plates.py` into graded 60 fps plates the page shows
frame-exactly.

### 6. Fill CONFIG and build

Edit the `CONFIG` object at the top of `index.html`; swap the Google Fonts `<link>` together with
the font names. Motion ad: `*words*` inside a headline line use the accent colour; coordinates
(`ui.click`, `ui.rest`, `ui.rings`, `proof.box`) are CSS pixels of the capture they refer to. Launch
film: scenes are `shot`, `slide`, `roll` and `type` blocks; extend `build` with the product's own
moments using the helpers in `film-kit.js`. Open `index.html` in a browser to watch it loop while
editing (the `?render` guard keeps render runs paused). Keep `window.seek`, `window.DURATION` and
`window.ready` intact.

### 7. Stills review

```bash
node $S/scripts/render.cjs --stills 1.8,3.6,5.6,7.6,9.4,10.9,12.2,13.3,14.3,17.2
python3 $S/scripts/contact-sheet.py "still-*.jpg" -o stills.jpg
```

Read `stills.jpg`, then single stills at full size. Mid-beat frames check layout; frames 1–3 apart
around every transition check that nothing pops, overlaps or flashes. Checklists:
`references/animation-craft.md` and, for films, the bug table in `references/launch-film.md`.
Delete old stills with `find . -maxdepth 1 -name 'still-*.jpg' -delete`.

### 8. Render, QA, deliver

```bash
node $S/scripts/render.cjs --out ad-16x9.mp4 --web --poster 9.4      # films: --fps 60
python3 $S/scripts/qa-video.py ad-16x9.mp4                            # hard cuts and blinks
```

`--web` adds `-web.mp4` (crf 25, about half the bytes) for pages; `--poster` grabs a still. The
hard cuts `qa-video.py` reports must be exactly the cuts you planned; `--hold` checks that text
which should be still really is. Spot-check frames from the encoded file, not just the page
(`ffmpeg -ss 12.8 -i ad-16x9.mp4 -frames:v 1 f.jpg`; the bundled binary is
`node -e "console.log(require('ffmpeg-static'))"` inside `scripts/`). Copy to `~/Downloads` under
the next free version number.

Report: file paths and sizes, the beat-by-beat copy, and caveats (claims that only appear on a
secondary page, demo URLs that don't resolve, captures you altered such as removed tags).

### 9. Optional: music, sound effects, voices

Last, on the approved cut. `references/sound.md`: ElevenLabs music composed section by section to
the scene table (`scripts/music.py`), measured and aligned to the beat grid
(`scripts/audio-analyze.py`), edited by whole bars when the cut changes (`scripts/music-edit.py`),
sound effects (`scripts/sfx.py`), optional voices with checked pronunciation and caption sync
(`scripts/voice.py`), and one mix with fixed gain, ducking under speech and a tick on every click
(`scripts/mix.py`). Offer two or three music moods; deliver the music version next to the silent one.

### 10. 4:5 cut

A 16:9 video on a phone is ~350 px wide, so its type becomes unreadable. Render the motion ad with
`--format 4x5`: 104 px headlines, the phone bleeding off the bottom edge, the UI window bleeding
right, no corner brand mark (small frames feel cramped with it; the end card carries the logo).
Review stills the same way, deliver as `-4x5-v<N>.mp4`.

### 11. Embed

`references/shipping-to-a-landing-page.md`: autoplaying muted loop, one `<video>` with a
`<source media>` per cut, a visible frame, the web encode, new file names on immutable CDNs, and a
Playwright check that the right file plays and loops.

## Iterating

Feedback arrives as "change this badge", "that transition is off", "I can't read the chat". Edit
`index.html`, re-render stills for the affected part, then the whole video, and bump the version.
Keep a copy of the previous source (`cp index.html index.v2.html`) before bigger edits. When a user
corrects copy or taste, apply the rule everywhere, not just where they pointed (clipped descenders
were on every headline; a slide without an exit animation meant every slide needed one). When a
timing change shifts the film, re-time the music by whole bars instead of composing a new track.

## Requirements

Node 18+, Google Chrome (or `npx playwright install chromium` in `scripts/` and pass
`--channel chromium`), Python 3 with Pillow (review helpers, footage) and numpy (sound). ffmpeg
comes from `ffmpeg-static`; nothing needs a system install. Optional keys, read from the
environment: `GEMINI_API_KEY` (Nano Banana, Veo) and `ELEVENLABS_API_KEY` (with the Music, Sound
Effects, Text to Speech and Speech to Text permissions you use).

| Symptom | Fix |
|---|---|
| `Cannot find module 'playwright-core'` / "ffmpeg not found" | `cd $S/scripts && npm install`; run scripts by their path in the skill folder |
| `Failed to launch chrome` | install Chrome, or install Chromium as above and pass `--channel chromium` |
| zsh: `no matches found: still-*.jpg` | quote globs; clean up with `find . -maxdepth 1 -name 'still-*.jpg' -delete` |
| A frame shows the previous scene's leftovers | a tween used `.from()` or a callback; use `gsap.set` + `.to()` only |
| Text looks fine in the page but blurry in the MP4 | captures below 2× DPR; recapture at DPR 2–3 |
| Text shimmers while zooming | slow scale tweens re-rasterize text; enlarge statically, move fast |
| `page.goto: ERR_FILE_NOT_FOUND` | run from the work directory or pass `--page path/to/index.html` |
| Veo returns 429 after a few clips | its rate limit after a burst; `animate.py` waits and retries, or switch to Lite |
| "missing the permission music_generation" | enable that permission on the ElevenLabs key |
| The music drops for seconds after a loud hit | dynamic loudness normalisation; `mix.py` uses one fixed gain and a limiter |

## Files

- `assets/template.html`: the motion ad; CONFIG, beats, 16:9 and 4:5 layouts
- `assets/film-template.html`, `assets/film-kit.js`: the launch film; CONFIG-driven scenes and the
  transition, typing, cursor, footage and state helpers
- `scripts/render.cjs`: stills, MP4, web encode, poster
- `scripts/capture.cjs`, `scripts/diff-box.py`, `scripts/contact-sheet.py`: captures and review
- `scripts/qa-video.py`: hard cuts, blinks and shimmer in the encoded file
- `scripts/gen-images.py`, `scripts/animate.py`, `scripts/prep-plates.py`: optional AI footage
- `scripts/music.py`, `scripts/audio-analyze.py`, `scripts/music-edit.py`, `scripts/sfx.py`,
  `scripts/voice.py`, `scripts/mix.py`: optional sound (`scripts/common.py` holds shared helpers)
- `references/storyboard-and-copy.md`, `references/capturing-real-ui.md`,
  `references/animation-craft.md`, `references/launch-film.md`, `references/ai-footage.md`,
  `references/sound.md`, `references/shipping-to-a-landing-page.md`

Credit: the paused-timeline-to-MP4 approach follows a community "Motion Ad" skill; this is an
independent implementation built around real product captures and landing-page delivery.

Files

28 files

product-motion-ad/