/ 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) |