- 前端
- 图形学
【免费下载链接】bloub
SVG recreation of the x.ai bot avatar. One shape morphing through 14 states, measured off the reference video frame by frame.
🕐bloub是一个用 Vue 3 + TypeScript 从零实现的 x.ai 机器人头像 SVG 重制项目:一个黑色剪影在 14 种状态间平滑变形,双眼独立变化。它最核心的设计只有一句话——engine.sample(t)是一个纯函数:时间 t 是参数,不是时钟。这一行约定,让"冻结任意时刻"、"回看历史帧"和"无 DOM 单元测试"全部水到渠成。本文用最少代码,带你看懂这套纯函数引擎是怎么搭起来的。
它要解决什么问题?
先看效果:下面的 14 个头像全部是同一套引擎在固定时刻采样得到的静帧,没有任何动画循环参与。
普通动画的做法是:浏览器里起一个requestAnimationFrame循环,每帧累加一个内部计时器,用Date.now()推进。这套方式有个致命伤——画面由"内部时钟"驱动,于是:
- 想冻结某一帧?得停表、暂停循环,状态散落在各帧回调里;
- 想跳到第 7.3 秒?只能快进播放;
- 想写单元测试?必须 mock
Date、mock DOM,或者干脆测不了。
bloub 的答案很干脆:把时间从引擎里彻底请出去。
纯函数是什么?一分钟建立直觉
纯函数指:相同的输入必然得到相同的输出,且调用不改变外部世界。
// bloub 的整个渲染入口,src/bot/engine.ts frame = engine.sample(1.2) // 1.2 = 状态开始后的第 1.2 秒sample(t)满足三条纪律(见 engine.ts 的注释):
- 不读墙钟——没有时间参数进来,引擎自己绝不去问"现在几点了";
- 不读
Date.now(),不 import Vue——src/bot/ 目录与框架完全解耦; - 采样不产生副作用——
sample绝不修改引擎内部状态,重读同一时刻永远得到同一张图。
换句话说,引擎内部那些字段(当前状态、过渡起点、变形起始时刻……)都不是"实时演化的时钟",而是一份带时间戳的事件记录:setState('orbit', 2.0)只写下一条"2.0 秒时切到 orbit"的日志。真正"算画面"这件事,只发生在sample(t)被调用、且 t 传进来的那一刻。
冻结任意时刻:暂停、跳转、倒看全免费
因为画面是 t 的函数,以下操作天然成立,无需任何额外机制:
- 暂停= 不再调用
sample,画面停在最后一帧; - 跳到任意时刻= 直接
sample(任意 t); - 回看历史= 再次
sample(过去的时间)——引擎不会"清理"旧状态,所以 0.7 秒的中间帧在 2.5 秒之后照样能原样重算出来; - 确定性= 两个独立引擎实例对同一时刻的采样结果完全一致,位级相同。
项目用测试把上述性质钉死了(engine.test.ts):
- "两次读同一日期,结果必须一致"
- "状态切换的淡入过程中依然可重放"——这是最容易踩的坑:若在淡入结束后清除旧状态,再读淡入期间的时刻就会丢帧
- "读一个早于状态切换的时刻,不得越界外推"——ease-out 曲线对负数比例会 extrapolate,让剪影飞出 30 倍距离
外部输入怎么进来?——"带时间戳的 setter"
你可能会问:切换状态、换身体形状、鼠标指向眼睛,这些外部输入不也影响画面吗?
bloub 的答案是:所有外部输入必须通过"带日期的 setter"进入(setState(id, now)、setShape(radii, now)、setLook(look, now)),setter 只记录"什么时刻发生了什么",绝不立刻改变画面。sample(t)读到这条记录时,再按各自的缓动曲线(项目统一用从参考视频实测出的easeOutQuint)把过渡算出来。
这是纯函数纪律里最精细的一环:architecture 文档 专门记了一个真实教训——"在播放中清理过期状态,看起来是无害的优化,却会让引擎失去可重放性"。它已经真实翻过车,于是有了上面那条专门的测试。
实际收益:状态面板、缩略图与离线 GIF 导出
纯函数设计不是纸上谈兵,它在 App.vue 里有三个"白拿"的用法:
- 冻结状态板:URL 带上
#planche,14 个头像并排定格。实现就是给每个 BloubBot.vue 组件传frozen-at属性——组件拿到它后只调一次engine.sample(frozenAt),根本不启动动画循环; - 时间轴缩略图:TimelineTrack.vue 里每个事件块的小预览图同理,一个
frozen-at搞定,不用为 14 个缩略图开 14 个播放器; - GIF / MP4 导出:capture.ts 用一个"屏幕外引擎"逐帧
rendAt(t)渲染整个循环,比实时播放快得多——因为渲染不依赖墙钟,帧与帧之间的间隔可以无限小。
这就是 README 里那句 "framework-free and clock-free" 的全部含金量:同一套sample(t),白天是页面上的动画,晚上是导出管线的离屏渲染器,测试里是纯数据函数。
这套设计的三条底线(可以直接抄进你的项目)
- 时间永远是参数:任何模块内部出现
Date.now()或performance.now()读时间,纯函数链就断了; - 事件带时间戳,采样不带副作用:setter 只追加"何时发生"的记录,
sample只读不算账; - 旧状态不清理:清理历史是"看起来无害、实则毁掉可重放性"的典型优化,bloub 用一条专门测试把它挡在门外(docs/architecture.md)。
小结
bloub 把一个 SVG 头像做得像参考视频一样"逐帧可测量",靠的却不是逐帧录制,而是反过来——让engine.sample(t)成为时间的纯函数:暂停、跳转、回看、离线导出、无 DOM 测试,全部是同一个约定的免费赠品。如果你在做一个需要精确帧控制的动画项目,这套"时钟外置"的纯函数引擎设计,值得直接搬走。
- 核心引擎:src/bot/engine.ts
- 14 个状态的定义与实测时长:src/bot/states.ts
- 组件封装与
frozen-at用法:src/components/BloubBot.vue - 架构决策与踩坑记录:docs/architecture.md
- 前端
- 图形学
【免费下载链接】bloub
SVG recreation of the x.ai bot avatar. One shape morphing through 14 states, measured off the reference video frame by frame.
相关推荐
Flax函数式API:纯函数模块设计
Flax函数式API:纯函数模块设计 痛点:JAX生态中的状态管理难题 在深度学习框架设计中,状态管理一直是个棘手的问题。传统的命令式框架(如PyTorch)允
人工智能深度学习机器学习终极清单:org-modern的25个常见问题及解决方法
终极清单:org modern的25个常见问题及解决方法 org modern是一款为Emacs Org模式提供现代化样式的插件,能够显著提升Org文档的视觉体
WarriorJS与函数式编程:纯函数设计的AI实现
WarriorJS与函数式编程:纯函数设计的AI实现 在WarriorJS这款基于Node.js的编程游戏中,玩家需要通过编写JavaScript代码控制角色移
教育CLI
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考