项目详情
Bloub
用测量学复刻 x.ai bot 头像:一个黑色形状 morph 出 14 个状态,零动画库
更新于 2026-09-16 · GitHub 数据采集同日
← 返回汇总
项目速览
一句话定位
把参考视频逐帧测量,用纯 SVG + 时间纯函数复刻 x.ai bot 头像的每一个像素级行为
一个填充黑色形状在 14 个状态间 morph,两只白色眼睛独立 morph,纯色背景。没有 GSAP、没有 framer-motion、没有弹簧引擎——所有常量都是从 10 fps 切帧的参考视频里测量出来的。适合研究「测量驱动的前端动画」「无时钟引擎设计」「SVG morph 数学」的开发者。
项目速览
核心数据
2026-08-15 建仓 · 08-17 发布 v0.1.1 后停更 · 30 commits · 仓库体积约 1.2 MB · 火于独立创作者社区的「逆向测量」叙事
测量反直觉
五个「你以为」全是错的
| 直觉以为 | 视频实测 |
眼睛倾斜 // | 倾斜 \\,偏离竖直约 26° |
| 身体是 squircle(超椭圆) | 正圆,径向偏差 < 0.7% |
| 过渡用弹簧(spring) | 指数 ease-out,身体从不 overshoot;全项目唯一弹簧是通知贴纸 +14% 弹出 |
| 彗星态:点划过屏幕 | 点不动,尾巴绕着它转 |
| 静止时头像上下浮动 | 不浮动(中心稳定 ±0.003),生命感全靠视线漂移 + 眨眼 |
彩蛋:favicon.svg 不是近似画——圆 + 两个眼矩阵 = engine.sample(1) 对 idle 态的字节级输出,右眼比左眼窄(0.64 vs 0.87)是球面深度压缩,不是 bug。
核心理念
常量是测量值,不是设计值
取整一个常量,就毁掉一分相似度——而相似度是这个项目唯一的成功标准
- 凝视角度、眼尺寸、半径、时长、颜色:全部来自逐帧分析,README 明确警告「不要把它们修好看」
- 数字看着随意,是因为测量本来就不整齐:如远近眼宽比 0.69(源)、0.674(原始视频)、0.708(模型推算)三个值并存,测试容差覆盖其差
- 静止不浮动也是测量结论:代码里刻意保留千分之几的漂移 + 0.5% 呼吸,只为图像不至于完全冻结
🔥 测量管线
从视频到 profiles.ts
绿框 = 测量路径 · 黄框 = 本地输入 / 生成脚本(reference.mp4 与 frames/ 不入库,完整复现需自备视频)
🔥 架构
无框架、无时钟的核心层
实线绿框 = 核心层(纯函数,可 DOM-less 测试)· 虚线灰框 = UI 层(Vue)· 跨层共享的 Vue 代码放 src/ui/,绝不进 src/bot/
核心机制
engine.sample(t):时间纯函数
- 同输入 → 同帧:暂停、恢复、跳转任意日期、测试,图像完全一致——这是冻结状态板和 DOM-less 测试的全部前提
- montage 回放字节级稳定:v0.1.1 的 fade 修复验证跑了 1921 帧 + 15 态状态板 + 任意日期重读,输出与修复前逐字节一致
- sample() 禁止 mutate:曾在形状 morph 里「顺手」清除过期旧状态,导致 fade 结束前回读旧日期找不到数据——现已加专属回归测试
- fade 中途切状态:从冻结的合成 pose 起混合,而不是旧状态的完整 pose——把 26–43px 跳变压回 10–14px 量级
// 只在 fade 进行中冻结合成 pose
// (每帧都冻结会让 alert 的「!」
// 半路停摆)
setState(s) {
if (fadeInProgress)
origin = freeze(currentPose)
blend(origin, s, easeOut)
}
// 时间纯函数(伪码)
engine.sample(t) → Frame // 无副作用
🔥 核心机制
64 角采样,morph = 半径线性插值
- 所有轮廓在相同 64 个角度采样(PROFILE_SAMPLES):任意两形状点集天然一一对应,过渡退化为半径线性插值——这就是「不需要 path-morphing 库」的原因
- 两种形状来源不许混:profiles.ts(视频提取,驱动动画态)vs skins.ts(解析构造,自定义皮肤);新形状要么走径向 profile,要么 profileFromPolygon
- 皮肤只替换 baseBody 态的身体(idle/wink/wide/notify/swirl);其余状态轮廓即动画本身,不可覆盖
核心机制
眼睛是 mask 上的洞,球面上的点
- 不是叠白椭圆:眼睛是
<mask> 挖的洞,滑向轮廓边缘时自动裁剪,零裁剪代码
- 球面模型:远眼宽 = 近眼 0.69 倍(面积 0.663),正是球面上该点的深度因子——压缩、倾斜、转到背面全由切空间正交投影自然涌现,拟合残差约 1px(190px 球)
- body 背后垫不透明 paper 底:环和爆散粒子的后半段画在 body 之下被遮挡;没有这层底,环会从眼睛洞里漏出来
- 贴边元素都要走 radiusAtAngle:眼睛和通知贴纸按局部半径 pro-rata 缩放,否则非圆形状上直接出界
最深的坑
eyefit:为什么不能在渲染循环里解算
失败路径 × 7 个版本
每版都在渲染循环里实时解算眼睛位置,每版都产生可见运动伪影——因为解算器读的输入(静止视线漂移、指针、表情 morph 中间态、最近边缘、最紧的眼)全都在以 60fps 移动:
- active-set 切换:最近边缘跳变 → 26 单位回退
- 非光滑目标函数:min 是 C0 不是 C1
- chattering:逐帧反馈回路高频抖动
结论:缺陷不在任何一版的几何里,而在「逐帧求解」这个动作本身。
正解:查表(pose space deformation)
- eyefit.ts 在 import 时一次性解算,产出一个 (shape × base-body 态 × 表情) 补偿表——即角色绑定的 PSD 思路(Lewis/Cordner/Fong, SIGGRAPH 2000)
- 引擎只在 morph 边界查表(shapePrev/shape 等成对端点值),再用该 morph 自己的 easeOutQuint 插值——绝不读插值中间值
- 不再跑在动画里 → 无连续性要求 → 可以迭代收敛、对所有变量取最坏情况(含静止视线漂移的解析边界)
- 补偿是纯平移(等距变换):间距、大小、倾斜逐像素保持,脸在窄体上「坐低一点」——正是人手会做的手势
失败复盘
六种「看起来可行」的修法,全部实测翻车
| 修法 | 为什么翻车(全部有测量) |
| 每只眼各自避让 | 两眼朝向不同、退缩量不同 → 眼距散开,扭曲的脸比裁剪更难读 |
| 沿半径向中心退 | 走长对角线换少量竖直空间 → 双眼被拖到中间并成一团,283/680 组合 |
| 整脸缩放 | 比例全保但眼睛明显变小,换表情时还会把缩放动画化 |
| 取最差眼 | Math.min 是离散选择,morph 中途约束眼切换 → 推力方向翻转,三角上 11.6 单位 |
| 每形状一条、对表情取最坏 | 胶囊上 neutre 眼位高需下移、effrayé 眼位低需上移,单个平移无法同时满足,实测仍 4 组合溢出 |
| 放不下就放弃 | wide 态 87 单位眼 + 三角/泪滴必溢出;规则改为「追求最不坏」,不许留真实溢出不改 |
680 个 组合逐一扫过,且不只扫组合还扫时间——曾因只测单时刻,让 capsule+frightened 靠静止漂移「漂」过了边缘。skins.test.ts 锁死全部行为:测「来回抖」而非速度,区分永久性抖动(静止漂移/形状 morph:不许多于正圆)与换表情这类合法移动。补偿瞄准的是原始 profile 的贴边余量,不是安全间隙——正圆上外眼本来就擦边(体积感的来源),追求 clearance 会让眼睛恰好相切、看得出来。圆上两轮廓重合 → 补偿恒为 (0,0),favicon 一像素不动。
核心机制
假 3D 与真时长:montage 不缩放时间
- 环是正交投影的 3D 圆:z 分量把每条弧劈成两半,后半段先画、被 body 遮挡——深度排序让环读作「绕球」而非平面画
- 状态声明几何(ArcSpec,球半径单位),引擎统一光栅化:viewBox 缩放只存在于引擎一处
- montage 只 hold / cut,从不倍速:拉长块 = 让循环态多转几圈、非循环态保持终态;缩短 = 切断。乘速度因子会一次性毁掉所有测量时长
- MIN_BLOCK 是派生值:从目录里最长 morph(orbit 0.6s)推导,不手写——曾经手写的 0.6 纯属巧合
- 弹簧是局部且刻意的:全项目无弹簧引擎,唯一的 NOTIF_POP = 1.14(通知贴纸弹出);新的弹跳效果应写进需要它的那个状态
- 形状变化藏在眨眼里:morph-damping 机制用 blinkIn 复现——强制眨眼 0.2s,例行 idle 眨眼 0.18s
工程文化
测量驱动的修复文化
先复现,再修
CHANGELOG 每条 fix 都「先复现并测量,测量在修复 commit 里」:montage 接缝处眼睛轨迹 53.4 单位跳变 → 修后 0.6;MP4 编码器失败泄漏 → 后续导出启动即死;localStorage 被禁抛 SecurityError 白屏 → 持久化改为可选。
测试即重放
纯函数引擎使测试无 DOM:默认周期 1921 帧、15 态状态板、任意日期重读逐字节回归;eyefit 表由 skins.test.ts 全量锁定(扫组合 × 扫时间)。vue-tsc 是唯一门禁,无 ESLint/Prettier。
细节即证据
favicon = sample(1) 字节级;竖直「!」是锥形杆(上下比 1.76),斜「!」是胶囊杆且点是泪滴;「#etat= 重复动画死循环」源于 hashchange 被误读为导航、lookup 取首次出现——一点击即触发。
产品面
不只是复刻:一个可玩的头像工坊
- Customise:8 种身体形状 × 12 色 × 16 种静止表情,跨访问保持
- Animations:小时间线编辑器——排列状态、设保持时长、保存结果
- 导出一切:头像 → SVG/PNG/动画 GIF,整条时间线 → GIF/MP4;静态格式零依赖,视频编码器首次用到才加载
- 两个深链 URL:
#planche 14 态并排冻结;#etat=orbit&stop 直达单态暂停
- 三语界面:法/英/中,手写翻译层
// 当 Vue 组件用
<BloubBot v-model:block="block"
v-model:state="state"
v-model:playing="playing" />
<BloubBot state="orbit" :size="120"
:frozen-at="1.2" /> // 渲染单帧,无动画循环
// props: size/shape/color/expression/
// paper/frozenAt/cycle/follow/gaze
// models: block/state/playing/elapsed
风险提示
需要清醒的地方
- 商标与设计权不在代码许可内:README 明确「与 x.ai 无关、非官方、不受其背书」,MIT 只覆盖本仓库代码,不覆盖它模仿的设计;商用/高曝光使用前需自行评估
- 单人项目,活跃窗口极短:08-15 建仓 → 08-17 v0.1.1 后无 push;3 天 1.5k stars 的热度不等于持续维护
- profiles 部分不可复现:reference.mp4 与 frames/ 均不入库,egg/hexagon/triangle 三个轮廓无法从仓库再生,只能信生成产物
- 工程门禁偏窄:无 ESLint/Prettier,vue-tsc 单门禁;对想贡献的人是低门槛也是低约束
在线体验
Playground:径向轮廓 Morph 实验台
本地复现 bloub 的核心机制——选两个形状拖 morph 进度条,看 64 角采样点半径逐项插值;再拖眼睛滑向轮廓边缘,亲眼看 mask 洞被「切开」——正是 v0.1.1 修掉的那个 bug,开 eyefit 补偿开关看表驱动平移如何救场。零网络、纯几何。