Skip to content

Add MP4 export for animated SVGs using ffmpeg.wasm - #314

Merged
simonw merged 2 commits into
mainfrom
claude/mp4-export-animated-svg-xqzwhe
Aug 16, 2026
Merged

Add MP4 export for animated SVGs using ffmpeg.wasm#314
simonw merged 2 commits into
mainfrom
claude/mp4-export-animated-svg-xqzwhe

Conversation

@simonw

@simonw simonw commented Aug 16, 2026

Copy link
Copy Markdown
Owner

Explore feasibility of adding an MP4 export option to markdown-svg-renderer.html which only shows up for animated SVGs and detects how long the loop should be based on the SVG syntax and then dynamically loads the necessary libraries from a JavaScript CDN (I guess ffmpeg as wasm) and uses those to generate a mobile friendly small file size looping MP4 to both preview and download

Test it on this one: https://tools.simonwillison.net/markdown-svg-renderer#url=https%3A%2F%2Fgist.github.com%2Fsimonw%2Fa5cbc71950bff0940c9f5482205dc2fb

Any heavy dependencies should only be loaded the first time the user selects an MP4 tab

Claude Fable 5 PR

This PR adds support for exporting animated SVGs as looping H.264 MP4 videos. When an SVG contains animations (either SMIL or CSS), a new MP4 tab appears in the renderer allowing users to generate and download a video.

Key Changes

  • Animation Detection: Added detectSvgAnimation() to identify SMIL animations (<animate>, <set>, <animateTransform>, <animateMotion>) and CSS animations (@keyframes), with duration calculation for loop length estimation
  • SVG Snapshot Capability: Implemented snapshotSvgAtTime() to freeze an animated SVG at a specific time by shifting animation delays and begin offsets, enabling frame capture
  • ffmpeg.wasm Integration:
    • Lazy-loads ffmpeg.wasm from jsdelivr (~31 MB) only when the MP4 tab is first opened
    • Uses a same-origin worker blob to work around cross-origin restrictions
    • Manages a job queue to serialize encoding operations on the shared worker filesystem
  • MP4 Generation Pipeline:
    • Captures frames at 30 FPS for the specified duration
    • Scales output to fit 720px on longest side with even dimensions (yuv420p requirement)
    • Encodes with libx264 using mobile-friendly settings (profile:main, preset:veryfast, crf:26)
    • Enables faststart for streaming playback
  • UI Enhancements: Added MP4 tab with duration input, generation button, progress feedback, video preview, and download functionality
  • Comprehensive Tests: Added test coverage for static SVGs (no MP4 tab), SMIL animation detection, CSS animation detection, and full MP4 generation pipeline with ffmpeg.wasm

Implementation Details

  • Animation duration detection handles SMIL clock values (h, min, s, ms) and CSS time values, accounting for repeat counts and delays
  • Frame snapshotting uses negative animation-delay and begin offsets to position animations at specific times
  • ffmpeg.wasm worker communication uses a message-passing protocol with file I/O and progress callbacks
  • Output is constrained to 30 seconds maximum duration to manage file sizes and generation time

https://claude.ai/code/session_01RWMdMCX4SnoLjTkQ4MXvA2

claude added 2 commits August 16, 2026 17:25
Animated SVG blocks (SMIL or CSS animations) get an extra MP4 tab that
renders a looping H.264 video entirely in the browser:

- Detects animation from SMIL elements (animate, set, animateTransform,
  animateMotion) and CSS @keyframes/animation rules, and derives a
  suggested loop length from dur/repeatCount/repeatDur and
  animation-duration/iteration-count (editable before generating).
- Captures frames by freezing a copy of the SVG at each timestamp:
  negative animation-delay + paused for CSS, shifted begin offsets for
  SMIL, then rasterizes via the same blob-URL canvas approach as the
  PNG/JPEG tabs. SVG images draw at document time 0, so each shifted
  copy yields the frame at that time.
- Encodes with ffmpeg.wasm, lazy-loaded from jsdelivr (~31 MB, first
  use only). The @ffmpeg/ffmpeg wrapper's UMD build cannot start its
  worker cross-origin, so a small inline worker drives @ffmpeg/core
  directly via blob URLs.
- Output is mobile-friendly: max 720px, even dimensions, yuv420p,
  main profile, CRF 26, +faststart, previewed in a looping video
  element with a download button showing file size.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RWMdMCX4SnoLjTkQ4MXvA2
Selecting the MP4 tab now starts the ~31 MB ffmpeg.wasm download
immediately (once per page, shared across blocks) instead of waiting
for the Generate click, so it is usually ready by the time the user
has reviewed the loop length. Nothing is fetched for documents where
the MP4 tab is never opened, and the tests now assert that.

Verified against the animated nudibranch gist: the CSS animation
shorthands (4.6s-7s, infinite) detect as a 7s loop, and the default
7s x 30fps generation produces a 60 KB 720x596 H.264 MP4 in ~14s of
encode time.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RWMdMCX4SnoLjTkQ4MXvA2
@claude

claude Bot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Claude encountered an error —— View job


I'll analyze this and get back to you.

@simonw
simonw merged commit 73e0327 into main Aug 16, 2026
3 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants