动画引擎原理 总览待审核
text morph · 槽位对齐 · 一个时钟

文字变形:一个词逐字长成另一个词

「十二个月」不断切换的那种标题动画,字不是整块换掉的:每个字母各自滑向新位置、在半路换成 另一张脸,长出来的字母从串尾外侧挤进来。文本动画那页把一句话 拆开做入场,这里更进一步——两串字之间要先对齐:谁只是搬家 (keep)、谁淡出 (out)、 谁淡入 (in)。对齐排出一组槽位后,剩下的又全是老朋友:一段 插值、一条缓动、 一把交错的延迟。

整页只有一个 Animation 在跑,它的值就是「当前是第几毫秒」;每个字的位置与 不透明度都由这一个数经纯函数算出。于是 seek / 反向 / 变速全部免费——拖播放头能停在任意 中间帧,倒放能看着 September 缩回 August。CSS 侧的同一效果 (由 transition 驱动、 每字一条 transition-delay) 见 CSS Animation · Text Morph, 两页共用同一套对齐算法,差别只在「值由谁算、落到哪」。

一个时钟,一串纯函数

点词切换,或直接拖播放头把这次 morph 停在任意时刻。下方甘特图一条 lane 是一个字, 色块是它的 [delay, delay + dur] 区间——红竖线扫过谁,谁就在动。 把 each 拉大,色块阶梯错得越开,过渡中字距被拉开得越夸张:那些 「Janua ry」「Oc to ber」的瞬间全部来自这里。为看清角色,页内把淡出的字标红、 淡入的字标紫,只是搬家的字 (keep) 保持墨色——真实使用时它们当然是同一个颜色。

currentTime 0 ms 总时长 0 ms speed 1× 槽位 0 keep / out / in 0 / 0 / 0
为什么值得只用一个 Animation。十几个字大可各建一个 Animation (那正是 stagger 的常规做法),但那样「整段 morph 现在走到哪」就散在 十几个播放器里,想统一 seek 得逐个 seek。这里换个切法:唯一的 Animation 的值 就是毫秒数,每个字的 delay 只是 render(t) 里的一次减法 (p = (t − delay) / dur)。交错从「一堆播放器的起跑时间」降级成一个纯函数的参数, 于是 seek / 反向 / 变速这些播放控制只需作用在那一个游标上。 反向不是把画面倒着播一遍那么简单吗? 是,而这正是重点:因为 rendert → 画面纯函数,倒放只是让 t 往回走。 淡出的字会重新亮起、淡入的字缩回去,全程不需要任何「撤销」逻辑。对照 CSS 版: transition 没有播放头可倒,要看中间帧得靠 animation-play-state: paused负的 animation-delay 硬凑。

中断:一次 morph 还没播完,词又变了

真实场景里没人等你播完——用户连点、数据连续刷新。此时新的一次 morph 该从哪儿起步? 左边按算法给的起点 (旧串的静止位置) 重新开始:字会瞬间跳回去,过渡断成两截。 右边先读出每个字此刻真正渲染在哪、多透明,拿它当新一段的起点,画面连续。 点「⚡ 连点 3 次」看得最清楚。

✗ 按算法起点重来

✓ 接住当前渲染状态

打断时最大跳变 px 接住后最大跳变 px
这件事 CSS 版是白送的。transition 的语义就是「从当前计算值过渡到新值」—— 半路改目标,浏览器自动从中间值接着走,不需要一行代码。引擎版把插值权拿回自己手里, 这份「接住」就得自己实现:每帧记下渲染值,新一段开始时用它覆盖 slot.from。 反过来,CSS 版拿不到播放头、没法 seek 与反向,那是引擎版白送的。两边各自免费的东西, 恰好是对方要手写的东西——这也是选型时真正该问的问题。 还能更进一步:带速度接管。本页的「接住」只对齐了位置,速度仍从新缓动的起点 重新算 (easeOut 起步最快, 所以观感尚可)。若要连速度也连续,就把每个字的 x 换成弹簧值源、 用 animateTo 在飞行中改目标——做法与 飞行中重定向中断与重定向 两页完全一致,代价是放弃「一个时钟」的可 seek 性 (弹簧的时长是结果而非给定)。 可 seek可无缝打断在这里是一对需要取舍的性质。
@vega/anim — 一个时钟 + render 纯函数

  
打断时接住当前渲染状态

  

对齐用的最长公共子序列就是 diff 的内核,展开见序列对齐 DP列表 diff;把同一套「量 → 对齐 → 交错」交给 transition 表达 (以及无障碍、降级、性能上的取舍),见 CSS 侧那一页