Repository navigation
Releases: wx4766/html-to-video
Release list
v1.4.0 · 11 个模板全部换得了配色,而且不用改文件
v1.4.0 · 11 个模板全部换得了配色,而且不用改文件
这一版补齐一件一直「只说了一半」的能力:换配色。
之前只有 5 个模板认 CFG.theme,6 个图表模板完全不认 —— 文档里却写着「改一个字段就能换配色」。
现在 11 个全接上,并且多了一条命令行的路子:HTML 一个字不改,同一个文件渲出三套配色。
新增:--theme=<名>
node scripts/render-video-seek.js bar-chart.html --theme=paper --out=paper.mp4
node scripts/render-video-seek.js bar-chart.html --theme=neon --out=neon.mp4
node scripts/render-video-seek.js bar-chart.html --out=midnight.mp4内置三套:midnight(默认深色)、paper(浅底纸张)、neon(霓虹)。
--batch 里每个任务也能各用一套,适合一次出一组风格统一的系列片:
[{ "html": "bar-chart.html", "duration": 6, "theme": "midnight", "out": "bar-dark.mp4" },
{ "html": "bar-chart.html", "duration": 6, "theme": "paper", "out": "bar-light.mp4" }]新增:--out=<文件>
这个不是顺手加的 —— 是「同一份 HTML 出三套配色」绕不过去的配套。
以前成片名是写死的:<HTML 名>.mp4,紧挨着 HTML。想渲第二个配色,
就会把第一个盖掉,你得自己 mv 一下再渲。三个主题就是三次 mv。
node scripts/render-video-seek.js bar-chart.html --theme=midnight --out=out/bar-dark.mp4
node scripts/render-video-seek.js bar-chart.html --theme=paper --out=out/bar-light.mp4
node scripts/render-video-seek.js bar-chart.html --theme=neon --out=out/bar-neon.mp4out/ 不存在会自动建。默认行为一个字没变:不给 --out 时仍然是同目录同名,
--from/--to 仍然是 <名>.<起>-<止>s.mp4。
扩展名和编码器对不上时会提醒(比如 --alpha 出的是 ProRes 4444,你写了 .mp4),
但不会拦着不让你渲 —— 文件名终究是你的选择。
拼错主题名不会静默变成默认色
这是这一版刻意多花的一点功夫。渲染器在页面加载完成后回读实际生效的主题
(window.__h2vThemeResolved),而不是假定「参数传进去了就一定生效」:
⚠ --theme=papr 未被识别,已回退到 midnight(可用主题名见 assets/templates/themes.js 的 H2V_THEMES)
同样地,如果你的页面根本没引入 themes.js,它会直接说「--theme 不生效」,
而不是渲出一片默认色让你回去对比才知道选错了。
新增:h2vRgba(hex, alpha) 与 5 个可选主题字段
光晕、阴影、渐隐都需要「同一主题色的不同透明度」。以前每个模板自己拆 hex 拼 rgba(),
换主题时最容易漏掉的就是这些手拼的字符串。现在统一走 h2vRgba。
主题还多了 5 个可选字段,专门承接深底与浅底无法共用的那部分:
| 字段 | 管什么 | 为什么需要 |
|---|---|---|
bgFar |
背景渐变的远端 | 深底渐变到近黑,浅底得渐变到米白 |
accentDeep |
强调色深一档 | 光晕内圈、按下态 |
accentSoft |
次一级数据色 | 双系列图的第二条线 / 第二根柱 |
sweep |
环境光扫过的 RGB | 深色底上白扫光好看,浅底上用白扫等于没画 |
onAccent |
强调色块上的字色 | 深色主题写深字,浅色主题写白字 |
关键设计:midnight 只带 11 个基础字段,可选字段留空。模板统一写成
TH.accentDeep || '<历史常量>' —— 所以默认主题走的还是原来那些常量,
默认输出的像素不变。新字段只在你真的换了主题时才出现。
变更:6 个模板接入主题
bar-chart / line-chart / number-counter / timeline / title-card / vertical-card。
同时修正了原先已接入的 5 个模板:它们只读 CFG.theme,从来不看 window.__h2vTheme,
也就是说命令行换主题对它们其实是空操作 —— 名义上支持,实际只在配置文件里生效。
这种「登记了能力但只做了一半」的问题现在由契约测试挡住:
模板只要在能力表里登记了 theme: true,就必须真的读 window.__h2vTheme。
底色也跟着统一了:这 6 个模板原来用 #080A10,另 5 个用 #0A0C12,现在统一走主题值。
改造前后逐像素比对(同一时间点、960×540 静帧):
| 模板 | 最大通道差 | 平均差 |
|---|---|---|
| bar-chart | 2 | 1.35 |
| line-chart | 2 | 1.36 |
| number-counter | 2 | 1.27 |
| timeline | 2 | 1.35 |
| title-card | 2 | 1.24 |
| vertical-card | 2 | 1.20 |
最大通道差 2/255、平均差 1.2–1.4,差异全部来自那 2/255 的底色微调,肉眼不可分辨。
修复:测试拷模板时漏拷 themes.js
模板开始 import ./themes.js 之后,有 10 个渲染用例集体误报 —— 测试只把 HTML
单文件拷到临时目录,themes.js 404,而「有页面报错就判失败」的闸门如实拦下了它。
新增 copyTemplate():按源码里真实出现的 ./xxx.js 把同目录脚本一起拷过去。
以后再加共用脚本,测试不用跟着改。
顺带修掉三处文档自相矛盾(都是本轮逐行核对代码发现的):
模板数写「10 个」而实际 11 个;--alpha 覆盖数写 5 个而实际 6 个;
ProRes 位深写 yuva444p12le,代码实际是 yuva444p10le。
还有一句流传很久的错误说法:「只拷 HTML 也能跑,只是换不了主题」。
实际是 404 会被判成页面报错 → 渲染失败。文档已改成准确描述。
实测
npm test104/104,exit 0,89 秒(v1.3.1 是 100)。- 新增
test/theme.test.mjs(3 条):两套配色渲出来的文件必须不同(只验参数被接受不算数)、
--out真落在指定路径且不再往 HTML 旁边另写一份、主题名拼错时照常出片但明确告警。 - 契约测试新增 3 类断言:能力表登记必须配套读
__h2vTheme、midnight不得定义可选字段、
非默认主题必须定义全部 5 个可选字段。 - 三套配色 × 11 个模板逐一截图核对,
paper浅底上的字色与柱色都过对比度(人工目检)。
已知限制
- 换主题只管颜色,不管版式。浅底上同样的字号和细线观感会偏弱,
个别模板在paper下比midnight显得平一些。需要更细的对比度可以传对象覆盖单个字段。 - 可变透明度(呼吸、淡入)走
h2vRgba,不走主题里已带 alpha 的glow字段。
兼容性
命令行参数、渲染契约对 v1.3.1 完全兼容,--theme 与 --out 都是纯增量。
默认主题下输出像素不变(差异 ≤2/255,仅底色);不给 --out 时成片位置和命名一个字没变。
注意:现在模板依赖同目录的 themes.js。拷贝模板时请连它一起拷 ——
只拷 HTML 会 404,而 404 属于页面报错,渲染会被判失败(要强行出片可加 --allow-errors)。
v1.3.1 · 把成片放大到 2~3 倍,修掉看得见的毛病
v1.3.1 · 把成片放大到 2~3 倍,修掉看得见的毛病
这一版只做一件事:把渲出来的片子放大到 2~3 倍,一帧一帧看,修掉肉眼能看见的毛病。
另外顺手修掉两个会让渲染中途失败的故障 —— 磁盘余量和端口。
新增:--tmpdir=<目录>,以及开工前的磁盘余量预检
node scripts/render-video-seek.js explainer.html --duration=60 --tmpdir=D:\render-tmp1080p 一分钟大约要 2.4 GB 逐帧 PNG。系统盘经常没这个余量,而旧版会在渲到一半时
ENOSPC 崩掉,留下一句看不懂的报错 —— 你已经等了十几分钟,还要自己去猜是哪里写满了。
现在开工前先查余量,不够就直说该改什么:换盘(--tmpdir)/ 缩短时长 / 降分辨率。
这条是拿真实事故换来的。写这一版的过程中本机系统盘被渲到 0 字节可用,
连临时目录都建不出来。
修复:随机撞上 Chromium「不安全端口」
本地静态服务器用 listen(0) 让系统分端口,而 Chromium 会拒绝访问一批端口
(实测撞到过 3659),返回 net::ERR_UNSAFE_PORT。这个报错和「端口分配」看不出任何关系,
排查时极易误判成页面写坏了、或者 Playwright 装错了。
现在分到黑名单端口就自动重来(最多 8 次),并补了 test/local-server.test.mjs 回归。
修复:人物细节返工
以下全部是把静帧裁出来、放到 2~3 倍才看出来的 —— 正常观看距离下它们只是「说不上哪里怪」。
| 症状 | 根因 | 处理 |
|---|---|---|
| 头顶露一圈肤色月牙 | 头是 headR × 1.06headR 的椭圆,发壳却是 headR*1.04 的正圆,顶部比头低 0.02R |
发壳改成 1.06R × 1.14R 椭圆,并按「弧终点 = 发际线曲线终点」收口(closePath 会补一条直线弦,鬓角处留一道斜切边) |
| 手是一颗带灰边的鹅卵石 | shade(skin,-0.30) 描边本意是做体积,放大后就是脏边 |
去掉描边,改成沿小臂方向外移 0.016,手从袖口「伸出来」 |
| 两条腿粘成一块布 | 大腿 0.080 时内缘重叠,从衬衫下摆出来像裙子 | 收到 0.070,留出 0.034 裆缝;后腿压暗 0.07 分前后 |
| 脖子像柱子插在衣服上 | 脖子画在衬衫之后,领口盖不住 | 脖子改为在衬衫之前画,领口自然盖住脖根;领口中间再凹 0.014,脖子是「坐」在领口里 |
| 肩线两个鼓包 | 袖口圆头半径 armW/2 比肩线还高 0.004 |
肩点下移 0.008 |
| 鞋提亮成浅灰反而更丑 | 深色裤腿下两只浅灰拖鞋 | 退回深色,靠形状和投影区分 |
| 躯干是一块板 | 纯色梯形没有任何明暗 | 加一层横向渐变 |
变更
- 删掉对话镜右上角的图标徽章。82px 的小卡片孤零零飘在角上,既不指向内容也不解释内容,
放大看就是块没用的装饰。留白比乱填好看。 - 竖屏片尾的标记行改成跟着副标题走(
subY + 110)。原先写死0.60H,
而副标题在 0.47H —— 中间空出一大块,那排图标看着像「忘了摆的第二组内容」。
片尾主标题同时下移到 0.40H。
实测
npm test100/100(v1.3.0 是 96,本版新增端口回归与人物几何判据)。- 演示片
docs/demo-explainer.mp43.94 MB / 17.6s / 1920×1080,
GIF 1.63 MB,封面 0.82 MB。
兼容性
命令行参数和行为对 v1.3.0 完全兼容,--tmpdir 是纯增量。
本版改的都是模板内部的版式与绘制实现,渲染契约没动。
v1.3.0 · 多镜头、持续运镜、素材库
v1.3.0 · 多镜头、持续运镜、素材库,以及「渲一次,发得到处都是」
这一版把工具从「渲一段动画」推到「出一支片子」。
v1.2.0 解决的是「这一帧画得对不对」,v1.3.0 解决的是「这一支片子由谁串起来」:
镜头之间怎么接、观众的眼睛往哪走、同一份素材怎么复用到下一支片子、
以及渲完之后 GIF / 封面 / 竖屏版是不是还得再开一次软件。
新增:多镜头编排
一份 HTML 里写一组镜头,镜头之间带转场,一条命令出整支片子 —— 不用渲几段再拼,也不用开剪辑软件。
node scripts/render-video-seek.js explainer.html --duration=17.6 --fps=30 --gif --poster=1.2可用转场 8 种:fade(溶解)· dip(经底色)· slide(推入)· push(推拉)·
rise(上推)· zoom(变焦穿过)· wipe(扫擦)· cut(硬切)。
转场是重叠的:总时长 = Σ镜头 − Σ转场。给一支片子加转场,片子不会因此变长。
非转场帧走快路径直接画,只有转场窗口才用离屏合成,不额外拖慢渲染。
转场纪律写进了代码,不只是写进文档
最初的做法是「把两个镜头各画一遍,按 alpha 叠」—— 结果 slide / push / wipe / zoom
会把两镜的标题压在同一位置,叠成一片乱码,看着像画面坏了,实际是两段文字同时在场。
现在 SC.paintShot 把 role: { side: 'a' | 'b', p } 交给镜头:退场的一方先撤内容,
进场的一方晚 0.22×转场时长起步。两镜内容永不同时在场。同时把默认策略也定死了 ——
文字类镜头只用 fade / dip,位移类转场留给满幅图形。slide 的分隔亮线从 0.35 降到 0.16,
wipe 的扫描亮线从 0.22 降到 0.10,亮线不再抢戏。
新增:explainer.html 讲解片模板
第一个多镜头模板,五种镜头开箱即用:片头 / 人物 + 气泡 / 分条要点 / 条形对比 / 片尾。
内置人物、图标、运镜、字幕。模板总数 10 → 11。
排版按 1080p 观看距离重标,而不是按电脑屏幕:气泡正文 30→38px、镜内标题 58→64px、
条目标题 38→42px、片尾主标题 76→84px。原来那套字号在自己屏幕上「看着还行」,
放到手机上一句话得眯着眼读 —— 视频的观看距离比网页远得多,字号要按这个重算。
新增:持续运镜 camera.js
kenburns(缓慢推拉)· breath(呼吸缩放,治「画面死了」)· handheld(确定性伪噪声手持微抖)·
shake(衰减式撞击震动),可以 stack 叠加。
手持抖动是用固定种子的正弦叠加算出来的,不是 Math.random()。因为多 worker 并行渲染时,
Math.random() 会让同一帧在不同 worker 上算出不同结果,拼起来就抖成故障。
确定性是这个项目的底线:同一条命令渲两次,必须逐像素一致。
新增:素材库
shapes.js —— 40+ 线性图标(纯 path 数据,24 网格)、卡片 / 胶囊 / 箭头 / 正交连接线 /
虚线框 / 进度弧、网格 / 点阵 / 柔光 / 暗角 / 流动极光背景 / 胶片颗粒(固定种子 LCG)。
全矢量、零依赖、零版权风险。
figures.js —— 参数化人物:头身比例、发型(6 种)、表情(6 种)、姿势(6 种)、配色全可调,
另附 4 个开箱预设和自带对话气泡。
刻意不做人物位图包和建筑照片包,三个理由:版权说不清、风格和用户的画面打架、
仓库会被撑大几十 MB。参数化人物丑一点,但能用、能改、能进商业项目。
新增:渲一次,发得到处都是
| 参数 | 作用 |
|---|---|
--gif[=fps,width] |
出片后顺手产 GIF(两阶段 palette 优化),不重渲 |
--poster=<秒> |
截一帧当封面 |
--batch=<file.json> |
一串任务按顺序渲,最后打汇总表;单个失败不连坐已渲好的,但整体非零退出(CI 能拦住) |
--story=<file> |
镜头表外置成 JSON,一组镜头换掉整份剧本 |
--scene-log=<file> |
导出镜头时间轴 index,id,start,end,dur,trans_in,trans_dur |
--story 的理由很实际:让 LLM 写 JSON 比让它写 500 行 canvas 代码可靠得多。
镜头表总时长和 --duration 对不上时会告警 —— 否则结尾会静默多出一截静止画面,
你看成片时只觉得「最后有点闷」,查起来要一帧一帧数。
新增:--concurrency=auto、--headed、性能基线
--concurrency=auto按 CPU 核数与可用内存挑 worker 数(上限 8),日志里写明是 auto 选的。--headed开着浏览器窗口渲,调试时能看见画面;并发自动降到 1,不会弹出一排窗口。scripts/bench.js量出s/帧并据此估算长片耗时,产物bench/bench.md。
文档里不再写「约几分钟」这种拍脑袋的数字 —— 任何人可以在自己机器上量一次。
改对的地方
- 人物立体感:
FIG.shade(hex, k)取同色系深浅一档;袖子按上衣色暗 0.14 绘制
(同色时手臂整条融进衣服,只剩一只手浮在胯边像块色斑);垂手外撇 0.05 让袖口露在躯干轮廓外。 - 发型参数语义改成「发际线高度」(
fringe,单位 headR,负值在上)。旧参数是「往下压多深」,
算出来发际线落在眼睛下方,渲出来像戴了顶压到眼皮的黑头盔;同时鬓角只收到头心高度,
两侧露头皮,又像一顶浮在头顶的小帽子。 shapes.js新增E(椭圆)指令,database图标改用扁椭圆做顶面 —— 用整圆会画成一颗药丸。- 竖屏版式重做:气泡贴到人物头顶(原来
by写死0.17H,气泡和脑袋之间空出一条 15% 的无人区,
尾巴悬空指不到人);人物放大到0.58H撑住画幅;tailX改成传画布绝对坐标
(原来传bw*0.5,尾巴跑到画面 43% 处);片头补淡图标水印 + 取景角标,片尾按比例摊开。 - 横屏三条要点改三栏并排:竖排三张满宽卡片在 16:9 里就是一张表格,每张右边空掉一半。
- 条形对比镜头:顶部高光条换成同色相竖向渐变(硬边压出「糖果按钮」感);
圆角取min(条高/2, 条长/2),短条不会退化成圆球;标签数值按条高对齐
(原来按行高算,条一变高数字就掉到条下面)。
实测
- 1920×1080 / 17.6s / 528 帧 / 4 workers:12 分 09 秒,成片 4.0 MB。
- 演示片
docs/demo-explainer.mp43.94 MB、docs/demo-explainer.gif1.63 MB(3.2s 起切 6s、10fps、480px)。 - 测试 55 → 96 个。
已知限制
- 转场要求每个镜头自己把背景画满:转场是按 alpha 叠两张画,镜头留透明会在交叉溶解时
透出底色,看着像闪一下。 --headed依赖本机有可用显示环境;无头 CI 里请用默认模式。scenes.js的多镜头是同一页面内切换,不是把多段 mp4 拼接。如果各镜头依赖不同的外部资源加载,
仍建议分开渲再在剪辑软件里合。
兼容性
渲染契约(window.__seek / __ready / __seekRender)与模板 CFG 结构没动。
所有新参数都是纯增量,v1.2.0 的命令一行不用改。
v1.2.0 · 透明层、字幕时间轴,以及 4 个新模板
v1.2.0 · 透明层、字幕时间轴、以及 4 个新模板
这一版把「能出片」推到「能进生产流程」:文字层能直接叠到实拍上,字幕能跟着配音走,
模板从 6 个变成 10 个,并且第一次有了可选配色。
新增:--alpha 透明输出
node scripts/render-video-seek.js overlay.html --duration=6 --fps=30 --alpha
# → overlay.mov(ProRes 4444 / yuva444p10le,背景透明)AI 生成的背景 + 代码生成的文字层,是现在性价比最高的组合 —— 模型画文字必糊,代码画文字精确到像素。
模板侧要做一件事:读到 window.__transparent 时把底色连同全屏光斑一起跳过。
实测只跳底色、留着 5% 的光斑,四角 alpha 是 12 而不是 0,叠到实拍上整片泛白,
剪辑师第一反应是「这素材脏」。全跳之后四角 alpha=0,只有 3.7% 的像素不透明(全是文字)。
体积(960×540 / 2s):H.264 约 118 KB,ProRes 4444 约 760 KB,6.5 倍。
为什么没有 VP9 alpha:把 PNG 序列编成 -pix_fmt yuva420p -auto-alt-ref 0,
文件确实从 7.8 KB 涨到 16.8 KB,alpha 数据写进去了,但 Chromium 播出来整帧 alpha 全 0 ——
连文字都透明。lossless、固定码率、-lag-in-frames 0 结果一样,ffmpeg 解码也还原不了
webm 的 BlockAdditional alpha。既然没法自证正确,就不作为选项发布,传 --alpha-codec=vp9
会直接报错并说明原因。
新增:4 个高频模板
| 模板 | 用途 |
|---|---|
compare.html |
前后对比:左右两栏对照 + 底部结论条 |
flow-steps.html |
流程图动画:步骤依次点亮,当前步高亮外扩 |
subtitle-card.html |
逐句解说卡:一句话一句地出 + 分段进度条 |
vertical-subtitle.html |
竖屏字幕卡:**关键词** 自动高亮,原生 9:16 |
四个都过了六维验收(运动 / 静止 / 跳变 / 确定性 / 框景 / 报错),也都有独立竖屏版式 ——
不是把横屏拉长,而是按 1080 宽重排。
竖屏版式这一版才真正做对:横屏的 SY(纵向缩放比)在 9:16 下是 1.78,
直接拿它算行距会把整栏撑开。新增 VSY = PORTRAIT ? SF : SY 与竖屏字号档位,
四个新模板的行距、卡片高度、结语位置全部按这套重算。
新增:主题参数化
assets/templates/themes.js 给三套调色板,换主题只改 CFG.theme 一行:
const CFG = { theme: 'paper', /* midnight(默认)| paper(浅色)| neon(黑紫霓虹) */ };也可以传对象只覆盖要改的字段。四个新模板已接入。
新增:--srt 外部字幕时间轴
node scripts/render-video-seek.js subtitle-card.html --duration=8 --srt=voice.srt --cue-log=cues.csv支持 SRT / WebVTT / JSON。解析一次、注入 window.__cues,模板按当前 t 自己查句;
没有字幕文件就回退到模板自带的 CFG.lines。
--cue-log 输出 frame,t,cue_index,cue_start,cue_end 的 CSV,把「字幕时间对不对」
变成能算的数。实测 4 句 / 80 帧,每句首帧与 SRT 起点的偏差 0 ms,帧覆盖数与理论值完全一致。
修复:QA 的两个真实盲区
① 看不见离屏画布里的文字。 文字先画进离屏 canvas 再 drawImage 贴到主画布时,
旧版只 hook fillText / strokeText,看到的是「一块贴图」,半截字完全查不出来。
现在 drawImage 会做坐标映射,把源图上的文字外框追到目标画布 —— 对照页从「0 条线索」
变成能报出「半截字:离屏文字应该被追上」。
② DOM 文字必然误报「叠字」。 按文本节点取所有行框的并集时,多行段落的并集框会把
行内 <span> 整个包住,段落和自己的强调词 100% 被判成叠字。改为按视觉行分块
(同一行的片段合并、跨行不合并)后,同一份对照页的框景线索从 6 条降到 1 条 ——
剩下那条是真的出界。
工程
- 判据可单测:
frameIssues/persistent/mergeLines/ 统计函数移到
scripts/lib/qa-rules.cjs。页面里跑的和单测跑的是同一个函数体
(mergeLines.toString()注入),不会出现两份实现漂移。 - 模板契约测试升级成能力登记表:新模板必须先登记它支持哪些能力(主题 / 字幕 / 透明),
源码和能力表双向对齐。能力被悄悄删掉、或新能力没登记,CI 都会失败。 - 测试从 14 个涨到 55 个:新增 judge 单测、离屏映射、DOM 分块、字幕对齐。
CHANGELOG.md:这一版起有正式的更新日志和版本号规则。
兼容性
老用法一行都不用改:模板的 CFG 结构没动,渲染契约(__seek / __ready / __seekRender)
没动,--alpha / --srt / CFG.theme 都是纯增量。
默认输出与 v1.1.0 逐像素一致(非 alpha 路径不传 omitBackground,PNG 编码路径不变)。
v1.1.0 · 横竖屏都能渲,断了能接着渲
v1.1.0 · 横竖屏都能渲,断了能接着渲
这一版解决三个「用过才知道疼」的问题。
模板不再写死 1920×1080
以前只有 1920×1080 好用,改 --width/--height 会被静默裁掉右下角 —— 画面照样出,只是缺一块,
不报错,最容易白跑一晚上。现在 6 个模板全部跟随视口自适应:
| 场景 | 参数 |
|---|---|
| 横屏 1080p | --width=1920 --height=1080(默认,与旧版逐像素一致) |
| 竖屏 9:16 | --width=1080 --height=1920 |
| 小尺寸试片 | --width=960 --height=540 |
三档等比缩放:横向尺寸跟宽度、纵向尺寸跟高度、字号线宽跟几何平均 ——
1920×1080 下三者恒为 1,所以默认输出和旧版一个像素都不差(已用 PSNR 逐帧回归,average:inf)。
竖屏不是把横屏拉长,字号不缩、只把版式铺开。
顺手修掉那个最阴的坑:画布比视口小的时候,渲染器现在会明确告警「截图只取左上角,右下会被裁掉」,
而不是默默出一部缺角的片子。
新增竖屏模板 vertical-card.html
原生 9:16 版式:kicker → 多行大标题 → 强调短线 → 副标题 → 大数字滚动 → 脚注(避开底部 15% 字幕带)。
不是把 16:9 拉伸,是以 1080 宽为设计基准重排的。
断点续渲 --resume
1800 帧的片子跑到 80% 崩了,不用从零开始:
node scripts/render-video-seek.js animation.html --fps=60 --duration=30 --resume
# ▸ resume: 已复用 1432 帧,还需渲 368 帧三条规则都是为了保证续出来的片子和一次渲完的片子逐像素一致:
- 帧先写
.part再改名 —— 被 kill 的那一刻不会留下半张图冒充「已完成」 - 参数变了(fps / 尺寸 / 范围)直接拒绝并打印「上次 vs 这次」—— 两批不同参数的帧混着编码,
片子前后节奏会不一致,而且不报错 - 编码前按帧号逐个核对连续覆盖 —— 缺一张 ffmpeg 会静默提前收尾,成片莫名变短
实测:960×540 / 80 帧,跑到 66% 强杀 → 53/53 帧 100% 复用,续渲结果 80/80 帧 PSNR=inf、
MP4 的 MD5 与全量渲染完全相同。
有了能跑的测试和 CI
npm test—— 14 个用例:出片链路、横竖屏分辨率、续渲三条底线、模板契约、QA 正反两组.github/workflows/ci.yml—— Ubuntu + Node 20,装 ffmpeg、装 Chromium、断言libx264可用、跑全量测试CONTRIBUTING.md+ Issue / PR 模板 —— 加模板的五条硬约定、提 PR 要回答的三个问题
QA 的对照组值得一提:test/fixtures/broken.html 里塞了六类真实缺陷(随机数、半截字、叠字、
进字幕带、中间几秒近空白、控制台报错),测试要求 QA 必须判它失败并报出线索 ——
只测「正常能过」的验收工具,等于没验收。
兼容性
- 老用法完全不变:
bash assets/render.sh animation.html出的片子与 v1.0.0 逐像素一致 - 自写动画不受影响;只是画布写死尺寸的动画会在尺寸不匹配时收到告警
v1.0.0 · 用代码逐帧做视频
v1.0.0 · 用代码逐帧做视频
把 HTML / Canvas 动画逐帧渲染成 MP4(可配乐、可出 GIF)。
不是 AI 文生视频,而是:
写 HTML/Canvas,把画面写成 frame(t) 纯函数
→ 无头浏览器逐帧 seek 到时刻 t,截图
→ ffmpeg 编成 H.264 MP4
这次有什么
渲染管线 scripts/render-video-seek.js
- 冻结时钟 + 逐帧 seek,同一时刻永远渲出同一张图
- 原生任意帧率(真 60fps,不是插帧),因为没有录屏,不存在锁 25fps 的坑
- 多 worker 并行出帧,帧号全局化,片段渲染与全片同刻逐像素一致
- 页面报错即失败:
pageerror/console.error归并去重后非零退出,不再出现「黑屏却报成功」 - 支持
--from/--to片段渲染、--stills静帧、--audio混音、--gop关键帧
质量门 scripts/qa.js
六维自动化验收:确定性 / 动感 / 流畅 / 框景 / 字体 / 报错。
找得出「半截字」「叠字」「文字落进字幕带」「近空白帧」「孤立跳变」「字体静默回退」。
纯 Node 零新依赖 —— 像素运算借 Chromium 自己解码,不引 numpy / PIL / sharp。
产出 qa.md + qa.json + 运动热图。
本地 http server scripts/lib/local-server.cjs
file:// 下 <script type="module"> 和 fetch() 会被 CORS 拦死。默认改走本地 http 服务,
让 Vite / esbuild 打包出来的动画项目也能直接渲。
动效库 assets/lib/motion.js
33+ 个缓动 / 弹簧 / 步进 / 错开 / 拍号纯函数,跟「冻结时钟 + __seek(t)」天然契合。
5 个开箱即用模板 assets/templates/
标题卡 / 条形图 / 折线图 / 数字滚动 / 时间线。单文件自包含,改顶部 CFG 配置块就出片。
三分钟上手
# 1. 装依赖(探测浏览器缓存 → 挑匹配的 playwright 版本 → 装完整版 ffmpeg)
bash scripts/install-deps.sh
# 2. 拷贝一个模板,改文案
cp assets/templates/bar-chart.html my-video.html
# 3. 出片
bash assets/render.sh my-video.html --duration=6 --fps=30 --width=1920 --height=1080
# 4. 过质量门
bash assets/qa.sh my-video.html --duration=6 --fps=30已知限制
- 页面必须暴露
window.__seek(t)与window.__ready(「渲染契约」,见assets/animation-skeleton.html)。
纯 CSS@keyframes/ Lottie / Three.js 手写非确定动画不吃__seek。 - 逐帧 PNG 先落盘再编码,720 帧约 316 MB。
- 只出 H.264 yuv420p,没有 alpha 通道,做不了透明背景叠加层。
- 没有断点续渲:中途挂了只能重来(ffmpeg 那步会保留 PNG)。
install-deps.sh是 bash 脚本,Windows 需要 Git Bash / WSL。
详细的能力边界与取舍说明见 README.md。