- 前端
- 图形学
【免费下载链接】bloub
SVG recreation of the x.ai bot avatar. One shape morphing through 14 states, measured off the reference video frame by frame.
bloub 是一个用 SVG 复现 x.ai 机器人头像动画的开源项目:一个黑色填充形状在 14 种状态间平滑变形,眼睛由两个独立的白色形状驱动。它能把头像导出为 SVG、PNG、GIF 和 MP4。正是这套导出功能,催生了一对一整套无 DOM 测试 + happy-dom的验证体系:13 个测试文件里只有一个文件声明@vitest-environment happy-dom,其余全部跑在纯 node 环境里。而这套体系真正抓住的,是只能靠逐帧翻看 MP4 才能发现的导出缺陷。
bloub 的 14 种状态并排冻结展示(docs/states.png),由引擎按时间采样渲染,而非浏览器截图:
1. 三层测试金字塔:node 打底,happy-dom 收口
先看清测试体系的分层,理解"为什么只有 1 个文件需要 DOM":
| 层 | 代表文件 | 运行环境 | 验证什么 |
|---|---|---|---|
| 纯逻辑层 | engine.test.ts、cycles.test.ts、skins.test.ts | node | 引擎纯函数、时间线规则、形状/表情常量 |
| 纯数据层 | export.test.ts、anime.test.ts、i18n.test.ts | node | 导出画框、文件名、GIF 调色板与 LZW 编码 |
| DOM 层 | capture.test.ts | happy-dom | 离屏播放器渲染的 SVG 必须与引擎完全一致 |
分层的关键在 vitest.config.ts:
environment: 'node'是全局默认,DOM 是按文件申请的——在需要的文件头部写一行// @vitest-environment happy-dom即可;- 配置里特意只挂了一个 Vue 插件,它只为
capture.test.ts这一个文件服务(要挂载BloubBot.vue组件才能被 Vite 转换); - 注释直接点明动机:全局 DOM 环境会拖慢整套测试,"把测试套件保持在几秒钟"(原文:
C'est ce qui garde la suite a quelques secondes)。
也就是说:happy-dom 不是这个项目的底座,而是精确投放的一小块 DOM——只为验证"导出的渲染必须来自组件本体,而不是旁边重建的第二份画"。
2. 一个文件声明 happy-dom,其余 12 个文件跑在 node
唯一的 DOM 请求就在 capture.test.ts 的第一行(capture.test.ts#L1):
// @vitest-environment happy-dom文件头部的注释(capture.test.ts#L11-L21)说得很直白:
这是全仓库唯一需要 DOM 的测试,所以环境声明放在文件头部:其余测试跑在
node环境,这让capture.ts——连同整条导出链路——本来完全无法测试。它能抓住的问题,除了逐帧检查 MP4 之外没有任何其他途径可见,而这正是这些缺陷当初被放行的原因:三个缺陷长期悄悄劣化着每一份导出的视频。
为什么capture.ts无法在 node 里测试?因为它是导出链路的 DOM 层(capture.ts#L1-L11):需要 canvas 光栅化、需要document.createElement、需要剪贴板。与之对应,export.ts里的画框计算、目录、文件名都是纯函数,留在 node 里由 export.test.ts 覆盖。一条导出链路被精确地切成了"可纯测的部分"和"必须挂 DOM 的部分"。
离屏播放器本身的实现也值得看一眼(capture.ts#L324-L377):在document.body里挂一个position:fixed; left:-99999px的宿主 div,用createApp把BloubBot以frozenAt: 0和首个状态的state挂载,然后逐帧调用组件的rendAt(t)推进时间。这一步需要真实的组件挂载与渲染,正是 happy-dom 的用武之地——但它比 jsdom 更轻,且完全够用,因为断言的内容全是不依赖布局与像素的字符串:身体路径的d属性、眼睛的transform矩阵、viewBox的值。
3. 纯函数引擎:无 DOM 测试的地基
这套体系成立的前提写在 docs/architecture.md 里:
engine.sample(t)是时间的纯函数。这正是frozenAt属性、冻结状态板和无 DOM 测试套件得以存在的原因。
因此src/bot/有一条铁律:不得出现依赖真实时间的内部状态,不得用Date.now(),不得引入任何 Vue 依赖。暂停、续播、跳到任意日期、跑测试,都会得到同一张图像。
测试正是利用这一点建立了"双轨对照":capture.test.ts里有一个辅助函数moteurAuMemeInstant(capture.test.ts#L36-L47),独立于组件构造一个BotEngine,按与组件完全相同的方式给每个区块的时间打上绝对偏移,然后逐时刻与离屏播放器输出的 SVG 逐字节比对:
expect(corpsDe(svg), `t=${t.toFixed(2)}`).toBe(attendu.bodyPath) expect(yeuxDe(svg), `t=${t.toFixed(2)}`).toEqual(attendu.eyes.map((e) => e.matrix))身体路径、眼睛矩阵都是纯字符串,不需要任何渲染引擎,也不关心 DOM 长什么样——这正是"DOM 只负责挂载,断言只碰文本"的无 DOM 测试思路。
4. 四个被 happy-dom 抓到的导出缺陷
CHANGELOG(CHANGELOG.md,0.1.1 版本)记录了三个"不逐帧看 MP4 就发现不了"的图像缺陷,加上一个回归防线,恰好对应capture.test.ts的四个用例:
缺陷一:默认 GIF 开场是一颗没有眼睛的球。GIF 导出需要两遍扫描:第一遍统计颜色建调色板,第二遍编码。这要求重放序列必须逐帧一致,否则第一遍统计出的调色板对第二遍无效。但离屏播放器会记住"最后一次设置的区块",而引擎为淡入淡出保留"上一状态"——完整播放一遍再回到 t=0,第一个状态会与最后一个状态混合。默认循环的结尾是彗星状态,其eyeAlpha恰好为 0,于是 GIF 第一帧变成无眼球体。锁定用例:capture.test.ts#L86-L104,重放完整序列后回到 t=0,身体路径与眼睛矩阵必须与首帧完全相等。
缺陷二:每个区块接缝处都是坏帧。区块交界处,组件确实把状态设置到了绝对偏移再采样;但state的 watcher 队列会被导出流程的nextTick排空,绕过后一次本应惰性的redrawFrozen()。结果:setState之后立刻sample(0),混合比例为零,渲染出的是上一个状态的姿势——默认蒙太奇里有 13 处接缝,每处一帧坏图(0.1.1 修复前眼睛轨迹最多跳 53.4 个单位)。锁定用例:capture.test.ts#L116-L134,在每个区块的起点与中点时刻逐一对照引擎直出的姿势。
缺陷三:导出画面不是从蒙太奇的第一个状态开始。离屏播放器挂载时未传stateprop,模型落到默认值idle,引擎便构建在"静止"之上:一个以轨道状态开头的蒙太奇,导出第 0 帧却是静止球体朝三角形 morph 了 0.6 秒的过渡画面;而屏幕上的播放从不这样,它用当前区块启动状态。修复是在ouvreCycle里显式传入首状态(capture.ts#L341-L350),测试对orbit、egg、hexagon三种开头各断言一次 t=0 帧(capture.test.ts#L144-L158)。
缺陷四(回归防线):导出画框与组件画框脱节。DEMI_ECRAN曾以 158 硬编码,与组件里手写的VB = 158之间没有任何引用关系。想象回归场景:有人为环形更大的新状态加宽 viewBox,导出却仍用旧画框,每条视频多出空白边带,而所有测试都是绿的。用例直接断言导出 SVG 的viewBox等于组件实际发射的值(capture.test.ts#L62-L72);而DEMI_ECRAN与组件画框如今同源于 export.ts,画框包含所有形状等约束则留在 node 环境由 export.test.ts 把守。
5. 这套体系给前端测试的三点启示
- DOM 是资源,按文件申请,而不是全局默认。一行
// @vitest-environment happy-dom把 DOM 成本压到一个文件,整套测试保持在秒级。 - 把"纯函数内核"当成测试资产。
sample(t)不碰时钟、不碰框架,于是测试可以独立构造参照系,与 UI 路径逐字节对照。 - 肉眼不可见的缺陷,靠"复现 + 断言"固化。三个导出缺陷都是"看视频才能发现"的,而修复之后,锁定它们的测试把逐帧一致性变成了每次
pnpm test都能重跑的断言。
| 想读什么 | 去哪里 |
|---|---|
| 无 DOM 测试与冻结状态板的由来 | docs/architecture.md |
| 导出链路(画框、GIF 两遍扫描、MP4) | docs/export.md |
| 唯一的 happy-dom 测试 | src/ui/capture.test.ts |
| 离屏播放器实现 | src/ui/capture.ts |
| 环境配置与 Vue 插件的用途 | vitest.config.ts |
| 缺陷修复记录 | CHANGELOG.md |
无 DOM 测试不是"没有浏览器"的妥协,而是一次主动的架构选择:纯函数引擎负责正确性,happy-dom 只负责让组件本体被挂载、被读走。正是这 12:1 的环境配比,让 bloub 在几秒内跑完全部测试,同时把导出链路里最隐蔽的三个缺陷永久锁在了 CI 里。
- 前端
- 图形学
【免费下载链接】bloub
SVG recreation of the x.ai bot avatar. One shape morphing through 14 states, measured off the reference video frame by frame.
相关推荐
Docker-Jellyfin硬件加速配置:Intel、NVIDIA与树莓派优化指南
Docker Jellyfin硬件加速配置:Intel、NVIDIA与树莓派优化指南 想要让您的Jellyfin媒体服务器流畅播放4K HDR视频?硬件加速是关
终极指南:如何为pack CLI执行全面测试与验证
终极指南:如何为pack CLI执行全面测试与验证 pack是一款基于Cloud Native Buildpacks的强大CLI工具,它能帮助开发者轻松构建容器
Gumbo-Parser重构验证:如何构建完整的测试体系
Gumbo Parser是一个用纯C99编写的HTML5解析库,它为开发者提供了强大的HTML解析功能。在项目重构过程中,构建完整的测试体系是确保代码质量和功能
后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考