大多数聊天软件和社交平台不认SVG动画,这是个老问题。Simon Willison常年用"一只骑自行车的鹈鹕"当LLM画图能力的非正式测试题,自己博客里堆满了SVG代码,却发现这些会动的矢量图几乎没地方发。他给自己写的markdown-svg-renderer工具新添了一个MP4标签页——本质是把一整套FFmpeg编译成WebAssembly塞进浏览器,现场把SVG动画拆帧、编码,吐出一段任何平台都能播的视频。听着像个小功能,拆开看,是一套完整的浏览器端视频工程。
粘贴一段Markdown,自动长出五个标签页
工具用法很简单:把含SVG代码块的Markdown粘进去,或者丢一个CORS友好的URL、一个GitHub Gist链接,页面就会把原本的SVG代码块替换成一个带标签的面板——Rendered、PNG、JPEG、MP4、Code五个切换项,URL方式还能生成可收藏的链接。
PNG和JPEG两个标签页完全不依赖ffmpeg:用image/svg+xml的blob URL加载原始SVG,按显示宽度用canvas栅格化,画一层白色背景把透明通道拍平,JPEG质量设成0.9。两个格式都是首次点开标签页才生成,不点不算。这是最省事的一半功能,真正有意思的是MP4。
MP4是怎么"无中生有"的
工具先要判断这段SVG到底有没有动画。判断依据分两路:SMIL标签(animate、set、animateTransform、animateMotion)和CSS动画(必须同时出现@keyframes和animation相关属性)。查不到任何一种,直接不生成MP4标签页,免得白跑一趟。
确定有动画之后,要估算循环时长。工具解析SMIL的dur、begin、repeatDur、repeatCount,再解析CSS的duration、delay、iteration-count,取其中最大值,四舍五入到两位小数,上限封顶30秒;实在判断不出来就默认2秒,用户可以在0.1到30秒之间手动改。
导出第一帧时还有个细节:冻结帧不能简单截图。CSS动画靠负值animation-delay加暂停状态定格;SMIL动画则要把begin值按时间戳反向偏移,再把SVG序列化、丢进Image对象、画到白底canvas上导出——两套逻辑,一套代码。
拆帧、渲染都做完,最后一步才是真正的重活:用libx264编码器,veryfast预设,CRF 26,长边缩放到720像素,宽高取偶数以适配yuv420p,再加faststart标志方便流式播放。这些数字不是随手拍的,是在浏览器算力和文件体积之间找的一个折中点——放大分辨率或拉长时长,浏览器端编码的耗时和内存都会跟着涨。
浏览器里跑FFmpeg,图的是什么
真正让人愣一下的是这句话:整个编码过程不需要服务器。工具用的是@ffmpeg/core 0.12.10,从jsDelivr的CDN拉取,体积超过31MB,但只在用户第一次点开MP4标签页时才下载,不点不加载。为了绕开跨域worker的限制,它用同源的blob worker来跑ffmpeg.wasm——代价是所有worker共享同一套虚拟文件系统,多个编码任务只能排队串行,没法并发。
- 结论.整套流程SVG内容全程留在浏览器本地,不经过任何服务器,对分享带隐私顾虑的图形是个加分项。
这套设计的价值不在"能转格式",而在它把一件通常交给服务器和专业编码库干的活,原样搬到了本地。麻雀虽小,五脏俱全——检测、估时、冻结帧、编码,一样不少,而且每个环节的参数选择都能看出取舍的痕迹:30秒封顶是怕浏览器扛不住长循环,720像素是怕内存爆掉,CRF 26是画质和体积的平衡点。这些经验值不是万能公式,遇到超长循环或高频闪烁的动画,大概率会撞到边界——工具目前也没交代失败时会怎样。
这类判断也该说清楚它的局限:目前能看到的所有信息都来自Simon Willison本人的博客和代码仓库,没有第三方对性能、兼容性或安全性做过评测。工具要渲染任意用户提供的SVG,理论上存在脚本注入的攻击面,这一点原文没有讨论。
- 风险.分享或粘贴他人提供的SVG源码时,渲染环节本质是执行了一段不受信任的矢量图形代码,谨慎起见不要贴入来源不明的内容。
一个人的博客配图需求,逼出了一整套浏览器端视频编码工程。
鹈鹕会不会骑自行车暂且不论,至少现在,它骑起来的样子能被录成一段MP4,发到任何一个不认SVG的平台上了。
