- 前端
- 图形学
【免费下载链接】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 变形动画项目:一个纯黑的小机器人在14 种状态之间平滑 morphing,全程不借助任何路径变形库或动画库。它的核心技巧是径向轮廓采样——把每种形状变成一圈固定角度上的半径数据,让任意两个形状的轮廓点天然一一对应,形状变形就退化为 64 个数字的线性插值。本文带新手读者拆透这套原理,以及它为什么比调用现成 morphing 库更简单、更稳。
先看效果:一个形状,14 种状态
在 bloub 里,idle(待机)、眨眼、六边形、三角形、轨道、爆发……所有状态都是同一个填充形状在连续变形,而不是逐个切换贴图。把页面地址加上#planche,可以冻结看到全部 14 个状态并排排列:
对普通用户来说,只需pnpm install && pnpm dev就能在本地看到它;而想理解"变形是怎么发生的",关键全在 src/bot/ 这一层纯 TypeScript 代码里。
路径变形库的痛点:点不对应,曲线就错乱
直接拿两条 SVGpath做 morphing 有一个根本难题:两条路径的锚点如何一一对应?
- 一个圆通常由 2 段弧线指令描述,一个三角形由 3 条直线段描述——指令结构和点数都不同,插值无从下手;
- 现成的路径变形库(如基于路径分解的库)要么做复杂的重采样与拓扑匹配,要么对形状变化敏感、偶发扭曲;
- 即使点数凑齐,若点的"语义"不对齐(比如 A 形的第 5 个点在左边、B 形的第 5 个点在右边),插值过程就会出现自交和扭动。
bloub 的解法是绕开问题本身:不让"两条路径"互相变形,而是把每条路径先统一翻译成同一种数据格式。
径向轮廓采样:把任意形状变成 64 个数字
固定角度采样,点与点天然一一对应
项目约定所有轮廓都写成径向函数 r(θ):从中心出发,向一圈均匀分布的角度发射射线,记录每条射线到轮廓的距离。采样角度是全局统一常量PROFILE_SAMPLES = 64,定义在 profiles.ts,角度序列在 shape.ts 中一次性预计算好。
这就完成了路径变形库里最难的"点对应"工作:任何两个形状的第 k 个半径,永远描述同一个角度方向上的轮廓。对应关系不是算出来的,而是构造时就存在的。
前提是所有轮廓都是凸形(蛋形、六边形、圆……),这样 r(θ) 才能精确描述轮廓而不丢失信息。
变形退化为线性插值
对应关系确定后,morphing 就变成了对两个长度 64 的数组逐元素 lerp,核心实现是 shape.ts 中的blend:
dst.radii[i] = lerp(a.radii[i], b.radii[i], t)旋转角还会走"最短路径"插值(从 +170° 到 -170° 只转 20°,而不是 340°),再叠加缩放与平移的插值。整个引擎里没有一条与路径解析、拓扑匹配有关的代码——这就是"避免路径变形库"的全部原因。
轮廓数据从哪来:逐帧测量,不是手画
一个容易忽略的点:profiles.ts里的 64 个数字不是设计出来的,而是从参考视频逐帧量出来的。工具链是 tools/extract-profiles.py:
- 用 ffmpeg 把参考视频按 10 fps 切成帧;
- 找到画面中最大的黑色连通域,取质心;
- 沿 64 个均匀角度做亚像素射线投射,二分找到轮廓边界;
- 以静息球体半径(190 px)归一化,输出
r(θ)数组。
其中蛋形、六边形、三角形这三个"不规矩"的形状由脚本生成(见 profiles.ts 的注释:明确标注了对应帧号与实测包围盒);而圆和点被验证为完美圆(径向偏差 < 0.7%),"!" 的杆是两个圆的凸包,这些都在 shape.ts 里用解析式直接构造,比冻结一份带噪点的测量数据更精确。测量细节完整记录在 docs/measurements.md。
64 个点如何画得丝滑:Catmull-Rom 平滑收尾
拿到插值后的 64 个半径,toPoints 把它们投影成屏幕坐标(依次应用旋转、squash 缩放、平移),最后由 closedPath 把闭合折线转成Catmull-Rom 三次贝塞尔曲线。
64 个采样点在这个环节绰绰有余:即使放大到 600 px 显示,轮廓依然像素级平滑,而且生成的d字符串链很短,渲染开销极低。也就是说,"变形精度"和"视觉平滑"被拆成了两件事——前者由径向采样保证,后者由曲线拟合保证,互不干扰。
边角场景:处理写不成 r(θ) 的形状
径向采样假设中心在形状内部且轮廓凸。遇到例外时,bloub 的补集工具是 profileFromPolygon:对任意多边形从中心发射同一组 64 条射线,与每条边求交取最远点,把多边形"翻译"成径向轮廓——从此它也和其他所有形状一样参与线性插值。
自定义器里的 8 种身体形状(鹅卵石、云朵、水滴、胶囊……)全部走这条路解析式生成,集中在 skins.ts:超椭圆用 superellipseProfile、云朵用圆并集 unionOfCirclesProfile、圆角多边形用 Minkowski 和 regularPolygonProfile。而"画在身体上"的眼睛与通知圆点,则用 radiusAtAngle 查询当前轮廓在指定方向上的真实半径,保证眼睛贴边移动而不会穿出轮廓——眼睛本身就是<mask>里的孔,越界即被裁掉,无需额外裁剪代码(见 docs/architecture.md)。
工程收益:纯函数引擎,一帧都可复现
因为轮廓是"角度 → 半径"的确定性数据、变形只是插值,整个引擎做到engine.sample(t)是时间的纯函数(见 engine.ts 的blendPose):
- 暂停、恢复、跳转到任意时刻,同一时刻永远渲染出同一张图;
- 冻结状态板、GIF/MP4 导出(见 docs/export.md)都靠这一点成立;
- 测试套件无需 DOM,直接对帧数做逐帧断言。
换一条路径:如果用路径变形库,输出依赖库内部的重采样状态,"任意时刻可复现"这类性质就很难保证。
文件导览:沿着源码读懂这套 morphing 原理
| 想理解什么 | 看这里 |
|---|---|
| 径向轮廓、blend、Catmull-Rom 平滑 | src/bot/shape.ts |
| 64 个角度的测量数据(勿手改) | src/bot/profiles.ts |
| 从视频帧提取轮廓的脚本 | tools/extract-profiles.py |
| 自定义形状的解析式构造 | src/bot/skins.ts |
| 纯函数引擎与姿态插值 | src/bot/engine.ts |
| 架构决策(为何不用路径变形库) | docs/architecture.md |
| 每个常量的测量来源 | docs/measurements.md |
一句话总结:bloub 没有和"路径变形"正面搏斗,而是用统一角度的径向轮廓采样把任意形状归一化成同构的半径数组,让 morphing 变成一次平凡的线性插值——零库、可复现、逐帧可测试。这套思路对任何"多形状平滑过渡"的需求都可直接复用。
- 前端
- 图形学
【免费下载链接】bloub
SVG recreation of the x.ai bot avatar. One shape morphing through 14 states, measured off the reference video frame by frame.
相关推荐
WeChat聊天记录导出完整教程:10分钟用WeChatMsg把对话存成HTML和Word文件
WeChat聊天记录导出完整教程:10分钟用WeChatMsg把对话存成HTML和Word文件 年底复盘,我想找某张项目验收截图当初发在哪个工作群,微信里已经翻
深度解析开源AI引擎LocalAI:3种高效部署方案实战指南
深度解析开源AI引擎LocalAI:3种高效部署方案实战指南 你是否曾想过在本地环境中运行各种AI模型,却苦于GPU资源不足或云端服务成本高昂?LocalAI正
人工智能大模型模型推理服务本地部署LLM 网关多模态AI AgentRAGMCP 服务Hermes Agent完整配置指南:5步搭建你的智能AI代理开发环境
Hermes Agent完整配置指南:5步搭建你的智能AI代理开发环境 Hermes Agent是一款强大的AI代理开发框架,它能够随着你的需求而成长,帮助你快
AI Agent人工智能AI 应用工具调用Agent 记忆交互助手RAG任务调度MCP 服务
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考