gfx-html
图形是由无头Chrome逐帧渲染并编码为Alpha影片的HTML页面。完整请求文本位于$ARGUMENTS中;第一个标记通常是项目slug。动词名称遵循构建合约;如有冲突,以python engine/ve.py --help为准。
使这条通道生效的规则
每个图形都是 ?t=<秒数> 的纯函数。 没有CSS动画、没有GSAP时间线、没有requestAnimationFrame、没有过渡效果。页面从location.search读取t,计算布局,并渲染一个静态帧。
这不是风格偏好。chrome --headless --screenshot 在“页面看起来已加载”时触发——此构建中没有虚拟时钟,没有Emulation.setVirtualTimePolicy,没有beginFrame。使用CSS动画的页面若以这种方式采样,每次运行都会产生不同的像素点。这会破坏内容寻址缓存(相同的键,不同的像素)并产生可见的抖动。
由相同朴素循环采样的t的纯函数是确定性和可缓存的,这就是全部区别。
提前说明的预算
Chrome稳定状态下的渲染速度约为每帧430-470毫秒,冷启动需7.9秒。一个5秒的图形以30 fps计算,共150帧,单线程大约需要65秒。引擎并行化四个Chrome进程并缓存每帧PNG,因此未更改的图形可以免费重新渲染——但首次渲染并非瞬间完成。
在开始长时间渲染前,告知用户帧数和预估时间。优先使用短图形:一个停留6秒的底栏并不需要6秒的运动,它只需要0.4秒入场、停留和0.4秒出场。
第一步——选择或编写模板
模板位于presets/graphics/*.html。优先复用再编写。模板是一个独立的HTML文件,需满足:
- 从
location.search读取t(秒)及其属性。 - 使用纯算术从
t计算所有位置/不透明度。缓动函数也可以——它们本身就是纯函数。 - 在透明背景上渲染。绝不能绘制背景颜色。
- 使用来自
assets/fonts/的字体,通过file:///URL的@font-face加载。不要依赖系统字体——此Chrome解析到的字体与QC控制渲染假设的不同。 - 在1920x1080分辨率下清晰可读,并位于平台的标题安全区域内。检查
output.platform:位于底部三分之一的底栏会被TikTok UI遮挡。
布局细节、缓动约定和属性合约见docs/graphics.md。此处不再赘述。
第二步——渲染帧
python engine/ve.py gfx <slug>
每帧引擎将Chrome作为参数列表调用,大致如下:
chrome.exe --headless=new --disable-gpu --no-sandbox --hide-scrollbars
--user-data-dir=<win-path>\prof --default-background-color=00000000
--window-size=1920,1080 --screenshot=<win-path>\frame_0007.png
"file:///C:/.../template.html?t=0.233&title=..."
两个标志决定了整个流程:
--default-background-color=00000000用于生成真实的Alpha通道。已验证:角落像素读取为0 0 0 0。缺少该标志会产生不透明的黑色,叠加层会覆盖视频。file:///URL必须是真实的Windows路径。Git Bash的$PWD路径会静默失败——三个不同的HTML文件产生了完全相同的md5值(一个空白深色模式错误页面),退出码为0,文件大小也合理。这就是为什么QC检查(g)需要与已知空白哈希值进行比较。
帧缓存在.cache/html/下,键为sha256(html_content + t + params + renderer + renderer_version)。更改模板会使所有帧失效,这是正确的行为。
第三步——保留Alpha编码
同一命令。ve gfx一次性渲染帧并进行编码——没有单独的编码动词,因为仅有PNG序列而没有配套影片的产物是半成品,下游无法使用。
PNG序列编码为QuickTime RLE(qtrle,argb)。ProRes 4444和原始PNG序列也能正确保留Alpha通道。此构建中,VP9-in-WebM静默丢弃Alpha通道,即使使用-metadata alpha_mode=1也是如此——不要使用它,也不要未经检测就接受用户提供的此类素材。
输出文件位于.cache/ovl/<xx>/<key>.mov,并附带<key>.json副产物。
第四步——锚定叠加层
编写overlays[]条目:id、kind: "html"、template、props、anchor、duration_frames、layer、render。
anchor始终为{segment_id, offset_frames}——绝不能使用绝对时间线位置。 时间线位置由已启用片段的累计和推导得出。一旦有人禁用了上游的某个片段,所有绝对位置都会出错,每个图形都会漂移。锚定叠加层会随其片段自动移动,这正是编辑器期望的行为。
layer控制合成顺序,数值越大越靠上。字幕最后合成,位于所有图形之上。
第五步——验证完成后再宣布完成
三项检查,均成本低廉:
- Alpha探测。 从角落帧读取一个原始rgba像素,断言
a = 0。QC(d)。 - 空白页检查。 断言帧不是每帧都是纯色。QC(g)。这能捕获错误的
file:///URL、字体加载失败和JS错误,这些情况否则都会以退出码0和看似合理的文件大小结束。它采样第一帧、中间帧和四分之三帧,仅当所有帧都是均匀颜色时才失败——此处每个模板都以不透明度0开始,因此正常工作图形的第0帧确实应为空白,仅凭此判断会错误地判定正确渲染。
ve gfx会自动对其生成的产物运行这两项检查,如果失败则退出码为2。
3. 确定性。 将同一帧渲染两次并比较哈希值。如果不同,模板中就存在不是t纯函数的内容——找出来。此项检查的存在是因为它捕获的故障是不可见的,直到缓存渲染与全新渲染出现差异。
然后向用户展示中点静帧。当截图只需一条命令即可获得时,不要用文字描述图形。
故障模式
| 症状 | 原因 | 修复方法 |
|---|---|---|
| 叠加层以黑色覆盖视频 | 缺少--default-background-color,或模板绘制了背景 | 添加标志;使主体透明 |
| 每帧相同,文件大小看似合理 | 错误的file:/// URL导致空白错误页 | 使用真实Windows路径;针对空白哈希值进行断言 |
| 相同键,不同运行间像素不同 | 模板不是t的纯函数 | 移除所有CSS动画和rAF |
| 文本以错误的字体渲染 | 系统字体回退 | 通过file:/// URL在assets/fonts/中使用@font-face |
| 编码后Alpha丢失 | VP9/WebM | 使用qtrle argb或ProRes 4444 |
| 上游编辑后图形漂移 | 绝对时间线锚点 | 锚定到{segment_id, offset_frames} |
| 渲染耗时数分钟 | 430毫秒/帧是下限 | 缩短图形或接受此耗时;缓存使第二次运行免费 |