算法与数据结构 / 动画引擎原理 · 从插值到播放控制 / SVG 描边:让线条自己画出来 待审核 38 / 47
stroke-dasharray · stroke-dashoffset · getTotalLength

SVG 描边:让线条自己画出来

最经典的 SVG 动画只动一个数。把一条路径的虚线段长设成它自身的总长,整条线就成了「一段实线加一段等长空白」;再让虚线偏移 stroke-dashoffset 从总长滑到 0,实线段便像被一只笔逐渐推出来。签名重放、对号打勾、loading 圈都是这一招。

dashoffset=L(1g)\text{dashoffset} = L \cdot (1 - g)

其中 LLgetTotalLength() 给出的总长,gg 是引擎插出来的进度。

1 · 描边自绘

图 1-1 · 三条子路径的描边自绘。可切换缓动与时长,或关掉顺序绘制让三笔同时推进;代码面板随控件同步。

2 · 为什么是 1 减进度

dashoffset 把虚线图案沿路径整体平移。偏移等于总长时,那唯一的实线段恰好被平移到路径起点之前,屏幕上什么都看不到;偏移归 0 时实线段回到原位、完整显现。所以进度 010 \to 1 对应偏移 L0L \to 0,中间任意时刻露出的正是前 gLg L 那一段。

警示 · dasharray 必须恰好等于该路径自身的总长,不能取一个「足够大的数」凑。虚线图案是「实线段 + 空白段」循环平铺的,段长小于总长时后面会露出第二段实线;段长大于总长时前几帧的偏移落在图案的空白区里,起笔会延迟。三条子路径各有各的长度,所以要各设各的 dasharray,不能共用一个值。

3 · 顺序绘制的轴是长度

三笔依次画出,用的不是三个错开的动画,而是把各子路径按长度首尾接成一条总长轴,用同一个全局进度去点亮:第 ii 笔的局部进度是 clamp((gLtotalsi)/Li)\text{clamp}\left((gL_\text{total} - s_i) / L_i\right),其中 sis_i 是它在这条轴上的起点。

这与交错同源,只是那里的轴是时间、每个单位一个延迟,而本页的轴是路径长度——长笔自然占用更多进度,短笔一闪而过,节奏由几何决定而非人为指定。

const path = svg.querySelector('path');
const L = path.getTotalLength();
path.style.strokeDasharray = L;

new Animation({
  keyframes: [0, 1],
  duration: 2000,
  ease: 'easeInOut',
  onUpdate: (g) => {
    path.style.strokeDashoffset = L * (1 - g);
  },
});

描边动的是笔触本身;让一个物体沿这条路径行进并朝向前进方向,见沿路径运动。进度本身只是一次普通的插值,被缓动塑形后接到 dashoffset 上。