资讯动态

用代码画白板视频:rough.js + Playwright + ffmpeg 全流程

发布时间:2026/10/29 17:47:30 来源:尧图企业网站定制
1. 这套方案到底在解决什么问题第一次看到“每一笔都是代码画的白板视频”这个说法我脑子里蹦出来的不是某个炫酷的动画软件而是一条完全可编程、可复现、可批量生产的内容流水线。传统做白板视频的路子无非两种要么用万彩手影、VideoScribe 这类工具手动拖拽要么找设计师逐帧画好再合成。前者效率低、风格受限后者成本高、改一版就要重来。而用代码画白板视频本质上是把“画面”当成数据来生成把“动画”当成时间轴上的函数来求值最后用渲染引擎把每一帧截出来再用编码工具串成视频。这套方案的核心关键词是whiteboard-video、rough.js、Playwright、ffmpeg、Excalidraw。rough.js 负责画出那种手绘风格的线条和填充Playwright 负责驱动浏览器把每一帧画面稳定地截下来ffmpeg 负责把成千上万张图片按指定帧率编码成视频。Excalidraw 则是一个很好的参考对象它本身就是用 rough.js 做手绘渲染的场景数据是结构化的 JSON这给了我们一个重要的启示只要能把画面描述成结构化的数据就能用代码控制它的每一笔。适合谁来参考如果你是会一点 JavaScript 或 TypeScript 的前端、想做知识类短视频但不想学剪辑软件的开发者、需要批量生成教学动画的教育内容生产者这套思路都能直接抄。它不要求你会画画但要求你愿意把“画面”拆成“元素 时间 变换”这三件事。我实测下来一条 60 秒、30fps 的白板视频大概会产生 1800 帧 PNG用 ffmpeg 编码成 H.264 大概几十秒就能跑完整个流程完全可以在本地或者一台普通云主机上完成。2. 整体设计思路与工具选型拆解2.1 为什么是 rough.js 而不是 Canvas 原生绘图Canvas 原生 API 画出来的线是数学上完美的直线和曲线看起来太“干净”没有白板那种手写的不规则感。rough.js 的做法是在你给定的路径上叠加随机扰动模拟手绘时手腕的抖动和笔触的粗细变化。它提供了roughCanvas和roughSVG两种渲染器前者直接画在 Canvas 上后者生成 SVG 路径。做视频截图时我推荐用 Canvas 版本因为 Playwright 截取 Canvas 内容非常直接不需要处理 SVG 的序列化和字体加载问题。rough.js 的几个关键参数决定了最终风格roughness控制线条的粗糙程度值越大抖动越明显bowing控制线条的弯曲程度strokeWidth是笔触宽度。我一般会把roughness设在 1.5 到 2.5 之间bowing设在 1 到 2 之间这样出来的效果既像手写又不会太乱。还有一个容易被忽略的参数是seed它决定了随机扰动的具体形态。如果你希望同一段动画每次渲染出来的抖动完全一致就必须固定 seed否则每次刷新页面线条都会变截出来的帧序列就会闪烁。2.2 Playwright 在这里扮演什么角色很多人对 Playwright 的印象停留在自动化测试和爬虫但它的截图能力其实非常适合做“渲染农场”。它的工作流程是这样的启动一个 Chromium 实例打开一个本地 HTML 页面页面里用 Canvas 画好当前时刻的画面然后调用page.screenshot()把 Canvas 区域截下来。相比用 Node.js 的canvas库直接生成图片Playwright 的优势在于你可以用完整的浏览器环境字体渲染、CSS 变换、甚至 WebGL 都能用而且截图结果和你在浏览器里看到的完全一致。这里有一个关键点不要用page.screenshot()截整个页面再裁剪而是给 Canvas 元素单独截图。Playwright 支持elementHandle.screenshot()直接定位到 Canvas 元素截出来的就是纯画面没有页面边距和滚动条干扰。另外截图格式建议用 PNG虽然文件大一点但它是无损的后续 ffmpeg 编码时不会因为 JPEG 压缩产生块状伪影。如果你对磁盘空间敏感可以在 ffmpeg 编码时用-crf参数控制质量而不是在截图阶段牺牲画质。2.3 ffmpeg 的编码参数怎么定ffmpeg 把图片序列编码成视频核心参数就几个-framerate指定输入帧率-i指定输入模式-c:v libx264指定编码器-pix_fmt yuv420p保证兼容性-crf控制质量。我一般用 30fps-crf 18这样出来的视频在大多数平台上都能直接上传画质也足够清晰。如果你要做 60fps 的丝滑动画帧数翻倍编码时间也会翻倍但白板视频本身节奏不快30fps 完全够用。还有一个细节图片序列的命名要规范。ffmpeg 支持%04d这种模式比如frame_0001.png、frame_0002.png。如果你用 Playwright 截图时命名混乱ffmpeg 就没办法按顺序读取。我习惯在截图循环里用String(index).padStart(4, 0)来生成文件名这样即使帧数超过 9999 也不会乱序。2.4 Excalidraw 的场景数据给了什么启发Excalidraw 的场景数据是一个 JSON 数组每个元素描述一个图形类型、位置、尺寸、角度、样式、以及绑定的文字。这种结构化的描述方式意味着你可以用代码动态生成场景而不是手动去画。比如你要做一个“二叉树插入节点”的动画你可以用代码计算出每个节点的坐标然后生成对应的矩形和文字元素再按时间顺序控制它们的出现和移动。Excalidraw 本身也支持导出 PNG但它的导出是静态的要做动画还是得自己控制每一帧的渲染。我的做法是借鉴 Excalidraw 的数据结构但不用它的渲染器而是自己用 rough.js 画。这样我可以完全控制每一帧的绘制逻辑比如让一个矩形从透明渐变到不透明或者让一条线从起点逐渐画到终点。这种“逐帧控制”的能力才是代码画白板视频的真正威力。3. 核心细节解析与实操要点3.1 手绘风格的参数调优rough.js 的默认参数出来的效果比较温和如果你想要更明显的“粉笔”或“马克笔”质感需要调整几个地方。第一是stroke颜色不要用纯黑用#1e1e1e或者#2d2d2d这样看起来更像真实笔迹。第二是strokeWidth可以稍微大一点2 到 3 像素配合roughness: 2会有明显的笔触感。第三是填充风格rough.js 支持hachure、solid、zigzag等填充方式白板视频里常用hachure来模拟马克笔涂色hachureGap控制线条间距一般设 8 到 12 比较自然。还有一个技巧给不同的元素设置不同的 seed。如果你所有元素都用同一个 seed它们的抖动方向会一致看起来像整体在晃。给每个元素分配一个基于其 ID 的 seed抖动就会各自独立更像人手一笔一笔画出来的。我在代码里通常用一个简单的哈希函数把元素 ID 转成数字然后传给 rough.js 的seed选项。3.2 动画的时间轴设计白板视频的动画本质上就是“在什么时间点画什么东西”。我习惯把整个视频拆成若干个“场景”每个场景有一个开始时间和持续时间。场景内部再拆成“动作”比如“画矩形”、“写文字”、“画箭头”。每个动作有一个进度值t从 0 到 1表示这个动作完成了多少。rough.js 本身不提供动画能力你需要自己根据t来计算当前帧应该画到什么程度。举个例子画一条线从 A 到 B你可以用线性插值计算当前终点currentX A.x (B.x - A.x) * tcurrentY A.y (B.y - A.y) * t然后每一帧都用 rough.js 画从 A 到当前终点的线。这样线条就会看起来像被一笔画出来。文字的动画可以用透明度或者裁剪区域来实现比如让文字从左到右逐渐显示。Playwright 截图时你只需要在每一帧设置好t然后触发重绘再截图。3.3 Playwright 截图的性能优化逐帧截图最大的瓶颈是速度。如果每一帧都要重新加载页面、重新执行 JavaScript那 1800 帧可能要跑几个小时。我的优化方案是页面只加载一次通过page.evaluate()调用页面内的渲染函数来更新画面。具体来说页面里暴露一个window.renderFrame(t)函数Playwright 在循环里调用它然后截图。这样避免了重复加载和初始化速度能提升一个数量级。另一个优化点是关闭不必要的浏览器功能。启动 Chromium 时加上--disable-gpu、--disable-dev-shm-usage、--no-sandbox等参数可以减少资源占用。如果你在 Docker 里跑--disable-dev-shm-usage尤其重要否则共享内存不够会导致浏览器崩溃。截图时设置clip区域只截 Canvas 部分也能减少图像处理时间。3.4 ffmpeg 编码的常见坑第一个坑是帧率不匹配。如果你截图时按 30fps 的时间间隔生成帧但 ffmpeg 输入时用了默认的 25fps视频就会变慢或变快。一定要在-framerate里明确指定和截图一致的帧率。第二个坑是图片尺寸不一致。如果某一帧截图时 Canvas 尺寸变了ffmpeg 会报错。解决办法是在页面里固定 Canvas 的宽高并且在 Playwright 截图时指定clip的宽高。第三个坑是颜色空间。PNG 是 RGBH.264 需要 YUV-pix_fmt yuv420p就是做这个转换的。如果不加这个参数有些播放器会显示颜色异常。还有一个实用技巧如果你要加背景音乐可以用 ffmpeg 的-i分别输入视频和音频然后用-shortest让视频长度对齐音频。但白板视频通常不需要背景音乐或者只需要很轻的环境音这个看具体需求。4. 完整实操流程与关键环节实现4.1 环境准备与依赖安装先建一个空目录初始化 npm 项目然后安装三个核心依赖roughjs、playwright、ffmpeg系统级安装。Playwright 安装时要注意npx playwright install chromium会下载一个 Chromium 二进制包国内网络可能会失败。如果失败可以设置PLAYWRIGHT_DOWNLOAD_HOST环境变量指向一个可用的镜像源或者手动下载后放到缓存目录。ffmpeg 的安装更简单Windows 上直接下载压缩包解压后把bin目录加到 PATHmacOS 用brew install ffmpegLinux 用包管理器安装即可。安装完成后验证一下npx playwright --version能输出版本号ffmpeg -version能输出编码器信息。如果 ffmpeg 提示找不到libx264说明你装的是精简版需要重新安装完整版或者用libopenh264替代。我一般推荐用完整版因为libx264的压缩效率和兼容性都更好。4.2 页面结构与渲染函数创建一个index.html里面放一个canvas元素宽高设为 1920x1080。然后在script里引入 rough.js定义window.renderFrame(t)函数。这个函数接收一个全局时间t单位秒根据t计算出当前应该显示哪些元素、每个元素的动画进度是多少然后清空 Canvas 并重新绘制。这里的关键是把场景数据和时间轴逻辑分离。场景数据是一个数组每个元素包含图形类型、坐标、尺寸、样式、出现时间、持续时间。时间轴逻辑负责遍历场景数据计算每个元素在当前t时刻的进度然后调用 rough.js 的绘制方法。这样你改动画只需要改时间轴参数改画面只需要改场景数据互不干扰。4.3 逐帧截图脚本用 Playwright 写一个 Node.js 脚本启动 Chromium打开index.html然后循环调用page.evaluate((t) window.renderFrame(t), t)再对 Canvas 元素截图。循环的步长是1 / fps比如 30fps 就是1/30秒。总帧数等于视频时长乘以 fps。截图保存到frames/目录文件名用frame_0001.png这种格式。这里有一个细节截图前要等待渲染完成。虽然page.evaluate()是同步执行的但 Canvas 的绘制是异步的浏览器会在下一帧合成。稳妥的做法是在renderFrame里返回一个 Promise在requestAnimationFrame回调里 resolve然后 Playwright 等待这个 Promise。或者简单一点在evaluate之后加一个很小的waitForTimeout(10)给浏览器一点时间完成绘制。我实测 10ms 足够再短可能会截到上一帧的内容。4.4 ffmpeg 编码命令截图完成后用 ffmpeg 把frames/目录下的 PNG 序列编码成 MP4。命令如下ffmpeg -framerate 30 -i frames/frame_%04d.png -c:v libx264 -pix_fmt yuv420p -crf 18 -preset medium output.mp4-framerate 30指定输入帧率-i frames/frame_%04d.png指定输入模式%04d匹配四位数字的文件名。-c:v libx264用 H.264 编码-pix_fmt yuv420p保证兼容性-crf 18是质量参数数值越小质量越高文件越大18 是一个视觉上无损的甜点值。-preset medium是编码速度预设medium 在速度和质量之间比较平衡如果你追求更小文件可以用slow但编码时间会明显增加。4.5 参数计算与选择过程假设你要做一条 60 秒、30fps、1920x1080 的白板视频。总帧数 60 × 30 1800 帧。每帧 PNG 大约 200KB 到 500KB取决于画面复杂度1800 帧大概 360MB 到 900MB。ffmpeg 编码后的 MP4用-crf 18大概 20MB 到 50MB。截图阶段Playwright 每帧大概耗时 50ms 到 100ms1800 帧需要 90 秒到 180 秒。编码阶段ffmpeg 用 medium 预设大概 30 秒到 60 秒。整个流程从零到出片大概 3 到 5 分钟。如果你要批量生成多条视频可以把场景数据参数化用脚本循环生成不同的 HTML 和截图序列然后并行跑多个 ffmpeg 编码任务。但要注意 Playwright 实例不要开太多每个 Chromium 实例占几百 MB 内存开太多会爆内存。我一般同时跑 2 到 3 个实例再多就排队。5. 常见问题与排查技巧实录5.1 截图出来的画面闪烁怎么办闪烁通常是因为 rough.js 的随机种子没有固定。每次调用renderFrame时如果 rough.js 重新生成随机扰动线条的抖动形态就会变连续帧之间看起来就像在抖。解决办法是在创建 rough.js 实例时传入固定的seed或者给每个元素单独指定 seed。另外如果你用了Math.random()来生成元素位置或颜色也要改成基于元素 ID 的确定性函数。5.2 Playwright 截图速度太慢怎么优化首先确认你没有在每一帧重新加载页面。如果用了page.goto()在循环里那速度肯定慢。正确的做法是页面只加载一次循环里只用page.evaluate()。其次关闭图片加载和字体加载在启动浏览器时加上--blink-settingsimagesEnabledfalse或者用路由拦截把不必要的请求 block 掉。第三截图时用type: png但不要设置quality因为 PNG 不支持 quality 参数设了也没用。第四如果 Canvas 很大可以考虑降低分辨率比如用 1280x720 截图最后 ffmpeg 再放大到 1920x1080速度会快很多。5.3 ffmpeg 报错 “Invalid data found when processing input”这个错误通常是图片文件名不连续或者格式不对。检查frames/目录下是不是从frame_0001.png开始中间有没有缺号。ffmpeg 的%04d模式要求文件名严格连续缺一帧就会报错。另外确认图片确实是 PNG 格式有些截图工具会保存成 WebP 但扩展名还是.pngffmpeg 读不了。可以用file frames/frame_0001.png确认一下。5.4 视频播放时颜色发灰或发绿这是颜色空间转换的问题。PNG 是 RGB 颜色空间H.264 默认用 YUV。如果不指定-pix_fmt yuv420pffmpeg 可能会用yuv444p或其他格式导致某些播放器解码异常。加上-pix_fmt yuv420p就能解决。另外如果你在 ffmpeg 命令里加了-vf滤镜注意滤镜链的顺序颜色转换要在缩放之后。5.5 常见问题速查表问题现象可能原因解决办法画面闪烁rough.js seed 不固定固定 seed 或按元素 ID 生成 seed截图速度慢每帧重新加载页面页面只加载一次用 evaluate 更新ffmpeg 报错图片序列不连续检查文件名编号是否连续颜色异常未指定 pix_fmt加-pix_fmt yuv420p视频变慢/变快帧率不匹配-framerate与截图帧率一致浏览器崩溃共享内存不足加--disable-dev-shm-usage文字显示不全字体未加载用document.fonts.ready等待5.6 独家避坑技巧第一个技巧在截图循环里加进度日志。每 100 帧打印一次进度和耗时这样你能预估总时间也能及时发现卡住的情况。第二个技巧把场景数据和时间轴逻辑写成纯函数不依赖任何全局状态这样你可以单独测试每一帧的渲染结果不用跑整个截图流程。第三个技巧先用低分辨率和高帧率步长做预览比如 640x360、每 5 帧截一张快速生成一个低质量预览视频确认动画节奏和画面布局没问题再跑全量。这个习惯能帮你省下大量等待时间。6. 这套方案的扩展玩法6.1 批量生成不同主题的视频把场景数据抽成 JSON 配置文件每个视频一个配置。写一个脚本读取配置生成对应的 HTML跑截图和编码输出 MP4。这样你可以用同一套代码生成几十条不同内容的白板视频适合做系列课程或者批量内容运营。配置里可以参数化颜色主题、字体、动画速度甚至背景纹理。6.2 结合数据动态生成画面如果你的视频内容是基于数据的比如“展示过去一年每月的销售趋势”你可以用代码读取 CSV 或 JSON 数据动态计算每个柱状图的高度和位置然后生成场景数据。这样每次数据更新视频也能自动重新生成不需要手动调整。这种“数据驱动动画”的思路在商业报告和新闻可视化里非常实用。6.3 加入转场和镜头运动目前这套方案是固定视角的但你可以通过 Canvas 的translate和scale来实现镜头平移和缩放。比如在场景切换时让整个画布向左平移或者放大到某个局部。rough.js 绘制的元素会跟着变换效果就像摄像机在移动。注意变换矩阵要在绘制每个元素之前设置好否则不同元素的变换会叠加。6.4 用 SVG 替代 Canvas 做矢量输出如果你需要无限缩放的矢量视频可以把 rough.js 的渲染器换成roughSVG生成 SVG 字符串然后用 Playwright 截取 SVG 元素。SVG 的好处是文件小、可编辑但截图时字体渲染可能和 Canvas 有差异。另外SVG 动画可以用 CSS 或 SMIL 实现不一定需要逐帧截图但那样就失去了“每一帧都是代码控制”的精确性。我个人的选择是要精确控制就用 Canvas 逐帧要矢量输出就用 SVG 加 CSS 动画。6.5 性能监控与自动化如果你要长期跑这套流水线建议加一个简单的性能监控记录每条视频的截图耗时、编码耗时、文件大小输出到日志。这样你能发现性能退化比如某次更新后截图变慢了可能是页面里加了新的字体或者复杂的绘制逻辑。自动化方面可以用 GitHub Actions 或者本地的 cron 任务定时拉取最新配置生成视频上传到目标平台。整个流程完全不需要人工干预。我在实际使用中发现这套方案最大的价值不是“省了剪辑软件的钱”而是把视频内容变成了可版本控制的代码。你可以用 Git 管理场景数据用 diff 看每次改了什么用 CI 自动生成预览。这种工程化的内容生产方式一旦跑通效率提升是数量级的。踩过几次坑之后我现在的习惯是任何需要重复做的视频先问自己能不能用代码画能画就绝不手动拖。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑