算法与数据结构 / 动画引擎原理 · 从插值到播放控制 / 时间重映射:局部慢放与速度曲线 待审核 28 / 47
time remap · slow-mo · speed ramp

时间重映射:局部慢放与速度曲线

播放头沿一条线性时间轴均匀推进。时间重映射不碰这条轴,而是在它之上再套一个「时间到时间」的映射 u=remap(u)u' = \text{remap}(u):某个子区间被拉伸,对应的运动就慢放;整段先快后慢就是速度曲线。

1 · 一段慢放

图 1-1 · 重映射曲线与它的斜率(速度曲线),下方两条轨道分别跟随原始播放头与重映射后的播放头。可调慢放区间与倍率。

两条轨道同时出发、同时到达,差别只在中段一条明显放慢。

2 · 省下的时间必须补回去

重映射后两端仍是 remap(0)=0\text{remap}(0) = 0remap(1)=1\text{remap}(1) = 1:总时长不变,只是把时间重新分配。慢放区省下来的输出量必须在区间外补回,所以两侧的局部速度必然大于 1。

设慢放区宽度为 w=esw = e - s、倍率为 rr,则区间外的补偿速度满足

wr+(1w)fast=1fast=1wr1ww r + (1 - w)\,\text{fast} = 1 \quad \Longrightarrow \quad \text{fast} = \frac{1 - w r}{1 - w}

倍率越低、慢放区越宽,补偿段就越快。

警示 · 这个式子在 w1w \to 1 时发散:慢放区几乎占满整条轴时,fast 趋于无穷。lab 里把区间止强制不小于起点加 0.05 正是为此——留下 5% 的补偿余量。真做速度曲线时这条约束要写进 UI,否则用户把区间拖满就会得到一段瞬移。

3 · 与缓动同源

缓动是把时间重塑成值进度(时间到进度),重映射是把时间重塑成时间(时间到时间)。两者都是对均匀时间轴再套一层映射,区别只在输出的量纲。

本页的 remap 是分段线性的,所以速度曲线是阶梯状,慢放区的进出是硬切。想要平滑的速度过渡,把分段常速换成一条连续的速度函数、再取它的归一化积分即可——那就是影视软件里的 speed ramp。

// remap: 时间 → 时间。慢放区速度 = rate, 其余补偿到 fast, 使 remap(1) = 1。
function makeRemap(s, e, rate) {
  const slow = e - s, fast = (1 - slow * rate) / (1 - slow);
  return (u) =>
    u <= s ? u * fast
    : u >= e ? (u - e) * fast + (s * fast + slow * rate)
    : s * fast + (u - s) * rate;
}

const remap = makeRemap(0.35, 0.65, 0.25);
new Animation({
  keyframes: [0, 1], duration: 2400,
  ease: 'linear',                 // 取均匀线性播放头 u
  repeat: Infinity,
  onUpdate: (u) => {
    const u2 = remap(u);          // 再套一层 时间→时间
    ball.style.transform = `translateX(${u2 * maxX}px)`;
  },
});

驱动这一切的仍只是一个可暂停、可任意 scrub 的线性播放头,见播放控制;把输出换成值进度就是缓动