文本动画:逐字、逐词、打字机
一句话整体淡入只是一个动画。要让文字像波浪一样依次浮现,得先把它拆开:split-text 用 JS 把文本切成字、词或行,每个单位包进独立的 <span>,再给它们一段相同的入场动画,只在起始延迟上拉开差距——那正是 stagger 做的事。
打字机是另一条路。它不拆 DOM,也不给谁加延迟,而是让「已显现的字符数」这一个整数随时间增长。两种做法在实现上几乎没有共同点。
1 · 拆分粒度与波的方向
粒度决定拆成什么单位,from 决定波从哪里发起。每个单位是一个独立的 Animation,延迟由 stagger 按它到发起点的距离算出。
警示 · 包裹用的 <span> 必须 display: inline-block,否则 translateY 对纯 inline 元素不生效——整段会「淡入但不上浮」,且没有任何报错。按字拆时这一条尤其容易漏。按词拆时空格要单独保留成文本节点(或给单位加
white-space: pre),否则相邻两个 inline-block 之间的空白会被折叠,词粘在一起。
拆分粒度的取舍很直接:按字最细、波最连贯,但会切开单词与连字,英文长句慎用;按词保留单词完整;按行最稳,适合大段排版。
2 · 打字机
把「显现到第几个字」建模成一个 keyframes: [0, total]、ease: 'linear' 的动画,onUpdate 里 Math.floor 取整后 slice 写回。速度参数是字每秒,换算成每字耗时后乘字数即得总时长。光标是一个常驻元素,闪烁交给 CSS 的 steps(1)。
注 · 用 ease: 'linear' 不是随手选的默认值。被补间的「值」是字符下标,而 onUpdate 里的 Math.floor 把连续值量化成整数——非线性缓动会让相邻两字的间隔忽长忽短,听感上像敲字的人时快时慢。真要模拟人手,该调的是每字耗时的随机抖动,而不是整段的缓动曲线。
3 · 两种 reveal 的分野
split-text 是 个动画加 个延迟,所有单位都参与运动,只是先后错开;typewriter 是 1 个动画加一个递增的整数游标,运动的不是字符而是「显现到第几个」。
代价也各在一边。split-text 要真的改 DOM 结构,字数一多就是几百个 <span> 与几百个 Animation;typewriter 只动一个文本节点,但每帧重设 textContent 会让整行重新布局,长文本同样不便宜。
import { Animation, stagger } from '@vega/anim';
// split-text: 拆成单位, 每个包进 inline-block 的 span, 再配一个递增 delay
const delayFor = stagger(55, { from: 'first', ease: 'easeOut' });
units.forEach((el, i) => {
new Animation({
keyframes: [0, 1], duration: 420, ease: 'easeOut',
delay: delayFor(i, units.length), // 第 i 个晚一点起
onUpdate: (p) => {
el.style.opacity = p;
el.style.transform = `translateY(${(1 - p) * 8}px)`;
},
});
});
// typewriter: 显现字符数 0 → total 线性增长, 截取 slice 写回
new Animation({
keyframes: [0, text.length],
duration: (1000 / cps) * text.length,
ease: 'linear', // 匀速敲字
onUpdate: (value) => { host.textContent = text.slice(0, Math.floor(value)); },
});
入场那条「上浮」轨迹的编排细节见关键帧与编排;每个单位的透明度与位移如何逐帧算出见插值。让字形本身在两个字之间连续变形,则是另一套做法,见文字形变。