/ references / video-export.md
video-export.md
  1  # Video Export: HTML Animation to MP4/GIF
  2  
  3  After completing an animation HTML, users often want to export it as video. This guide covers the complete pipeline.
  4  
  5  ## When to Export
  6  
  7  **Export timing**:
  8  - Animation runs correctly end-to-end, visually verified (Playwright screenshots confirm each keyframe)
  9  - User has watched it in the browser at least once and confirmed it looks good
 10  - **Do not** export while animation bugs are still being fixed -- fixing issues after video export is more expensive
 11  
 12  **Trigger phrases users may say**:
 13  - "Can you export this as video?"
 14  - "Convert to MP4"
 15  - "Make a GIF"
 16  - "60fps"
 17  
 18  ## Output Specs
 19  
 20  By default, deliver three formats and let the user choose:
 21  
 22  | Format | Spec | Best for | Typical size (30s) |
 23  |---|---|---|---|
 24  | MP4 25fps | 1920x1080, H.264, CRF 18 | Social media embeds, video platforms, YouTube | 1-2 MB |
 25  | MP4 60fps | 1920x1080, minterpolate frame interpolation, H.264, CRF 18 | High-framerate showcases, portfolios | 1.5-3 MB |
 26  | GIF | 960x540, 15fps, palette optimized | Twitter/X, README, Slack previews | 2-4 MB |
 27  
 28  ## Toolchain
 29  
 30  Two scripts in `scripts/`:
 31  
 32  ### 1. `render-video.js` -- HTML to MP4
 33  
 34  Records a 25fps MP4 base version. Requires global playwright.
 35  
 36  ```bash
 37  NODE_PATH=$(npm root -g) node /path/to/cc-design/scripts/render-video.js <html-file>
 38  ```
 39  
 40  Optional parameters:
 41  - `--duration=30` Animation duration (seconds)
 42  - `--width=1920 --height=1080` Resolution
 43  - `--trim=2.2` Seconds to trim from start (removes reload + font load time)
 44  - `--fontwait=1.5` Font load wait time (seconds), increase when using many fonts
 45  
 46  Output: Same directory as HTML, same name with `.mp4`.
 47  
 48  ### 2. `add-music.sh` -- MP4 + BGM to MP4
 49  
 50  Mixes background music into a silent MP4. Selects from built-in BGM library by mood, or accepts custom audio. Auto-matches duration, adds fade in/out.
 51  
 52  ```bash
 53  bash add-music.sh <input.mp4> [--mood=<name>] [--music=<path>] [--out=<path>]
 54  ```
 55  
 56  **Built-in BGM library** (in `assets/bgm-<mood>.mp3`):
 57  
 58  | `--mood=` | Style | Best for |
 59  |-----------|-------|---------|
 60  | `tech` (default) | Apple Silicon / Apple keynote vibe, minimal synth + piano | Product launches, AI tools, skill promos |
 61  | `ad` | Upbeat modern electronic, has build + drop | Social media ads, product teasers, promos |
 62  | `educational` | Warm, bright, light guitar/electric piano, inviting | Science communication, tutorial intros, course previews |
 63  | `educational-alt` | Alternative option for the same mood | Same as above |
 64  | `tutorial` | Lo-fi ambient, barely noticeable | Software demos, coding tutorials, long walkthroughs |
 65  | `tutorial-alt` | Alternative option | Same as above |
 66  
 67  **Behavior**:
 68  - Music trimmed to video duration
 69  - 0.3s fade in + 1s fade out (avoids hard cuts)
 70  - Video stream `-c:v copy` (no re-encode), audio AAC 192k
 71  - `--music=<path>` takes priority over `--mood`, use any external audio file
 72  - Invalid mood name lists all available options instead of failing silently
 73  
 74  **Typical pipeline** (animation export trio + music):
 75  ```bash
 76  node render-video.js animation.html                        # Screen record
 77  bash convert-formats.sh animation.mp4                      # Derive 60fps + GIF
 78  bash add-music.sh animation-60fps.mp4                      # Add default tech BGM
 79  # Or for different moods:
 80  bash add-music.sh tutorial-demo.mp4 --mood=tutorial
 81  bash add-music.sh product-promo.mp4 --mood=ad --out=promo-final.mp4
 82  ```
 83  
 84  ### 3. `convert-formats.sh` -- MP4 to 60fps MP4 + GIF
 85  
 86  Generates 60fps version and GIF from an existing MP4.
 87  
 88  ```bash
 89  bash /path/to/cc-design/scripts/convert-formats.sh <input.mp4> [gif_width] [--minterpolate]
 90  ```
 91  
 92  Output (same directory as input):
 93  - `<name>-60fps.mp4` -- Default uses `fps=60` frame duplication (wide compatibility); add `--minterpolate` for high-quality frame interpolation
 94  - `<name>.gif` -- Palette-optimized GIF (default 960 wide, configurable)
 95  
 96  **60fps mode selection**:
 97  
 98  | Mode | Command | Compatibility | Use case |
 99  |---|---|---|---|
100  | Frame duplication (default) | `convert-formats.sh in.mp4` | QuickTime/Safari/Chrome/VLC all work | General delivery, platform uploads, social media |
101  | minterpolate interpolation | `convert-formats.sh in.mp4 --minterpolate` | macOS QuickTime/Safari may refuse to open | Platforms that need true interpolated frames. **Must test locally** before delivery |
102  
103  Why frame duplication is now the default: minterpolate outputs an H.264 elementary stream with a known compat bug -- previous default of minterpolate hit "macOS QuickTime won't open" repeatedly. See `animation-pitfalls.md` rule 14.
104  
105  `gif_width` parameter:
106  - 960 (default) -- General social platform use
107  - 1280 -- Sharper but larger file
108  - 600 -- Twitter/X priority loading
109  
110  ## Complete Pipeline (Standard Recommended)
111  
112  After the user says "export video":
113  
114  ```bash
115  cd <project-directory>
116  
117  # Assuming $SKILL points to this skill's root directory (adjust to your install location)
118  
119  # 1. Record 25fps base MP4
120  NODE_PATH=$(npm root -g) node "$SKILL/scripts/render-video.js" my-animation.html
121  
122  # 2. Derive 60fps MP4 and GIF
123  bash "$SKILL/scripts/convert-formats.sh" my-animation.mp4
124  
125  # Output files:
126  # my-animation.mp4         (25fps, 1-2 MB)
127  # my-animation-60fps.mp4   (60fps, 1.5-3 MB)
128  # my-animation.gif         (15fps, 2-4 MB)
129  ```
130  
131  ## Technical Details (For Troubleshooting)
132  
133  ### Playwright recordVideo Gotchas
134  
135  - Framerate fixed at 25fps, cannot directly record 60fps (Chromium headless compositor limit)
136  - Recording starts from context creation, must use `trim` to cut front load time
137  - Default webm format, needs ffmpeg conversion to H.264 MP4 for universal playback
138  
139  `render-video.js` handles all of these.
140  
141  ### ffmpeg minterpolate Parameters
142  
143  Current config: `minterpolate=fps=60:mi_mode=mci:mc_mode=aobmc:me_mode=bidir:vsbmc=1`
144  
145  - `mi_mode=mci` -- Motion compensation interpolation
146  - `mc_mode=aobmc` -- Adaptive overlapped block motion compensation
147  - `me_mode=bidir` -- Bidirectional motion estimation
148  - `vsbmc=1` -- Variable-size block motion compensation
149  
150  Works well for CSS **transform animations** (translate/scale/rotate).
151  May produce slight ghosting on **pure fade** transitions -- if user complains, fall back to simple frame duplication:
152  
153  ```bash
154  ffmpeg -i input.mp4 -r 60 -c:v libx264 ... output.mp4
155  ```
156  
157  ### Why GIF Palette Needs Two Passes
158  
159  GIF supports only 256 colors. A single-pass GIF compresses the entire animation's colors into a generic 256-color palette, which ruins subtle palettes like warm beige + orange accent.
160  
161  Two-stage approach:
162  1. `palettegen=stats_mode=diff` -- Scans the entire clip, generates an **optimal palette for this specific animation**
163  2. `paletteuse=dither=bayer:bayer_scale=5:diff_mode=rectangle` -- Encodes using this palette, rectangle diff only updates changed regions, dramatically reducing file size
164  
165  Using `dither=bayer` for fade transitions produces smoother results than `none`, at slightly larger file size.
166  
167  ## Pre-flight Check (Before Export)
168  
169  30-second self-check before exporting:
170  
171  - [ ] HTML has played through completely in the browser, no console errors
172  - [ ] Animation frame 0 is a complete initial state (not a blank loading screen)
173  - [ ] Animation's last frame is a stable final state (not mid-animation)
174  - [ ] All fonts/images/emoji render correctly (see `animation-pitfalls.md`)
175  - [ ] Duration parameter matches the actual animation length in the HTML
176  - [ ] HTML's Stage detects `window.__recording` to force loop=false (must-check for hand-written Stage; `assets/animations.jsx` includes this)
177  - [ ] Final Sprite has `fadeOut={0}` (video end frame should not fade out)
178  - [ ] Remove any attribution watermarks that aren't the user's brand (see SKILL.md for watermark policy)
179  
180  ## Delivery Notes
181  
182  Standard format to include when delivering exports:
183  
184  ```
185  **Complete Delivery**
186  
187  | File | Format | Spec | Size |
188  |---|---|---|---|
189  | foo.mp4 | MP4 | 1920x1080, 25fps, H.264 | X MB |
190  | foo-60fps.mp4 | MP4 | 1920x1080, 60fps (motion interpolation), H.264 | X MB |
191  | foo.gif | GIF | 960x540, 15fps, palette optimized | X MB |
192  
193  **Notes**
194  - 60fps uses minterpolate motion estimation interpolation, works well with transform animations
195  - GIF uses palette optimization, 30s animation can compress to ~3MB
196  
197  Let me know if you need different dimensions or framerates.
198  ```
199  
200  ## Common Follow-up Requests
201  
202  | User says | Response |
203  |---|---|
204  | "Too large" | MP4: increase CRF to 23-28; GIF: reduce resolution to 600 or fps to 10 |
205  | "GIF is too blurry" | Increase `gif_width` to 1280; or suggest MP4 instead (most platforms support it now) |
206  | "Need vertical 9:16" | Modify HTML source with `--width=1080 --height=1920`, re-record |
207  | "Add watermark" | Use ffmpeg `-vf "drawtext=..."` or `overlay=` a PNG |
208  | "Need transparent background" | MP4 doesn't support alpha; use WebM VP9 + alpha or APNG |
209  | "Need lossless" | Set CRF to 0 + preset veryslow (file will be ~10x larger) |