Skip to content

Releases: wx4766/html-to-video

v1.4.0 · 11 个模板全部换得了配色,而且不用改文件

Choose a tag to compare

@wx4766 wx4766 released this 21 Oct 16:07

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.mp4

out/ 不存在会自动建。默认行为一个字没变:不给 --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 test 104/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 倍,修掉看得见的毛病

Choose a tag to compare

@wx4766 wx4766 released this 20 Oct 16:18

v1.3.1 · 把成片放大到 2~3 倍,修掉看得见的毛病

这一版只做一件事:把渲出来的片子放大到 2~3 倍,一帧一帧看,修掉肉眼能看见的毛病。
另外顺手修掉两个会让渲染中途失败的故障 —— 磁盘余量和端口。

新增:--tmpdir=<目录>,以及开工前的磁盘余量预检

node scripts/render-video-seek.js explainer.html --duration=60 --tmpdir=D:\render-tmp

1080p 一分钟大约要 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 test 100/100(v1.3.0 是 96,本版新增端口回归与人物几何判据)。
  • 演示片 docs/demo-explainer.mp4 3.94 MB / 17.6s / 1920×1080,
    GIF 1.63 MB,封面 0.82 MB。

兼容性

命令行参数和行为对 v1.3.0 完全兼容,--tmpdir 是纯增量。
本版改的都是模板内部的版式与绘制实现,渲染契约没动。

v1.3.0 · 多镜头、持续运镜、素材库

Choose a tag to compare

@wx4766 wx4766 released this 20 Oct 16:18

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.mp4 3.94 MB、docs/demo-explainer.gif 1.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 个新模板

Choose a tag to compare

@wx4766 wx4766 released this 17 Oct 19:48

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 · 横竖屏都能渲,断了能接着渲

Choose a tag to compare

@wx4766 wx4766 released this 17 Oct 17:26

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 · 用代码逐帧做视频

Choose a tag to compare

@wx4766 wx4766 released this 11 Oct 16:46

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。