# Gm Reel Motion

> Motion timing standard for every vertical reel — count-ups, wipes, staggers, easings. Use when building or reviewing any reel so the whole series moves identically.

- Skill: `crafty-arl/gm-reel-motion` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add crafty-arl/gm-reel-motion`
- Raw SKILL.md: https://api.skillmd.com/api/skills/crafty-arl/gm-reel-motion/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: crafty-arl (https://skillmd.com/u/crafty-arl)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/crafty-arl/gm-reel-motion

---


# GM! Carl reel motion standard
Locked 2026-09-12. Every reel from here uses this. Source: `reel-captions.html`
(the `TIMING` + `EASE` block is the single source of truth), worked example in `reel-count/`.

## The rules

| Element | Motion | Timing |
|---|---|---|
| **Big number** | counts up 0 → value, easeOutExpo | starts 0.06s, runs 0.92s |
| **Big number** | scale 0.88 → 1.0, easeOutBack (slight overshoot) + fade | 0.00s, 0.38s |
| **Gold rule** | wipes out from centre, 0 → 220px, easeOutCubic | 0.30s, 0.45s |
| **Supporting line** | fade + rise 26px, easeOutCubic | 0.52s, 0.40s |
| **Body lines** | fade + rise 30px, **staggered 0.09s per line** | 0.00s, 0.46s each |
| **Source credit** | fade in last, always | 0.70s, 0.40s |

**Why count-up and not a cut.** The whole campaign is called a count. A number that
rolls up reads as data being gathered; a number that cuts in reads as a claim. It also
buys ~1s of attention on a stat that would otherwise be read and dismissed.

**Never animate the source credit's text**, only its opacity. A moving citation looks
like decoration; a still one looks like evidence.

## Render pipeline

Only the **animated head** (1.25s = 38 frames @30fps) is screenshotted. ffmpeg clones
the last frame for the hold — `tpad=stop_mode=clone:stop_duration=N`. Rendering full
windows would be ~320 screenshots instead of 114 for identical output.

```bash
# 1. frames  (Playwright steps window.setFrame(cardId, t) — deterministic, not CSS timing)
# 2. alpha clips
ffmpeg -framerate 30 -i seq/c1-%04d.png -vf "tpad=stop_mode=clone:stop_duration=1.98" -c:v qtrle c1.mov
# 3. composite: offset each card with setpts, gate with enable
[1:v]setpts=PTS+0.10/TB[a];
[0:v][a]overlay=0:0:eof_action=pass:repeatlast=0:enable='between(t,0.10,3.34)'[v1]
```

## Hard-won gotchas

- **qtrle** (in a `.mov`) is the alpha codec that works here. PNG sequences alone can't
  be offset on a timeline.
- **Never `apad` with `-shortest`** — `-shortest` does not constrain `filter_complex`
  outputs, so apad emits infinite audio and the encode never terminates. Bound with `-t`.
- `-preset slow` at 1080x1920 is too heavy on this box. `veryfast` + `crf 20` is clean.
- Captions need the baked-in scrim. Bright plates swallow cream text.
- Drive animation **per frame from JS**, never CSS `animation:` — a screenshotter has no
  reliable way to pin CSS animation time, and you get torn frames.

## Caption timing comes from the audio, not from taste

Render the VO first, then:
```bash
ffmpeg -i vo.wav -af "silencedetect=noise=-40dB:d=0.07" -f null /dev/null
```
Use the detected gaps as card boundaries. For a sentence with no internal pause, split
by syllable weight, not character count — spoken numbers run far longer than they read.

## Voice
Qwen3-TTS `YOUR_TTS_HOST:5005`, voice **YOUR_VOICE_ID** (Carl's real clone, carl-ref.wav).
Verify against `GET /v1/voices` every time — that map was inverted once and shipped
months of audio in the wrong voice. Normalise to `-16 LUFS` for social.

