算法与数据结构 / 动画引擎原理 · 从插值到播放控制 / 换向缓动:reverse 时别把 ease 倒过来 待审核 23 / 47
easeReverse · 换向缓动 · anchor

换向缓动:reverse 时别把 ease 倒过来

播放控制里的 reverse 只是给 speed 取负,游标从此递减、纯函数原样重采样。代价藏在缓动里:曲线也被一起时间反演了。一条 expoOut(慢收尾)正向播放是「先快后慢」,倒放就成了 expoIn 的「先慢后快」——回退段因此显得生硬。

easeReverse 换一种做法:换向那一刻记下当前位置作锚点,对剩余距离重新施加一条正向缓动,于是回退段仍保有 ease-out 的减速收尾。

1 · 两条曲线的分岔

图 1-1 · naive reverse 与 easeReverse 在同一次换向后的对照。默认到时间中点自动反向,也可关掉自动、在任意时刻手动打断。

默认在播放头走到 duration 一半时翻向。注意 expoOut 是先快后慢,此刻值已约 0.97,球看着已近终点——时间的一半不是运动的一半。反向之后两条曲线随即分岔:灰蓝的 naive 把上升曲线镜像着倒回来,上升快的区段回落也陡;主色的 easeReverse 从锚点起另画一段减速收尾的新曲线。

2 · 锚点的算法

换向瞬间冻结一对锚点 (anchorRatio,anchorQ)(\text{anchorRatio}, \text{anchorQ}),之后每帧按

ratio=anchorRatio+scaleease(τ)\text{ratio} = \text{anchorRatio} + \text{scale} \cdot \text{ease}(\tau)

重算,其中 τ\tau 是「换向后经过的进度除以剩余进度」,恒为 010 \to 1τ=0\tau = 0ratio=anchorRatio\text{ratio} = \text{anchorRatio},保证换向连续不跳变;τ=1\tau = 1 时精确落到端点。

这与时序里的 repeatType: 'reverse' 是两件事:那是每次迭代整体的时间反演,发生在迭代边界;本页则是单次播放中途换向,且对剩余距离重施正向缓动。

警示 · 输出依赖换向历史,纯函数性因此被打破了一半。sampleAt(t) 仍返回 anchor-off 的正向曲线,只有 live 播放值才体现锚点——这是有意的取舍:保住 sampleAt 的纯函数契约(时间轴缩略图、确定性测试都依赖它),代价是它与画面在换向后不再一致。要预览带锚点的曲线,只能记录实际播放轨迹。

const opts = { keyframes: [0, 1], duration: 1600, ease: 'expoOut' };

const naive = new Animation(opts);                       // reverse 会把 ease 一起倒放
const smart = new Animation({ ...opts, easeReverse: true }); // 回退对剩余距离重施正向 ease

smart.reverse();    // 任意时刻打断: 以当前位置为锚, τ:0→1 重走剩余段
smart.sampleAt(t);  // 仍是 anchor-off 的纯曲线 (不受换向历史影响)

easeReverse 还可以给一条不同于正向的曲线(控件里的 sineIn / easeIn / backIn),用于「进场活泼、退场克制」这类不对称的手感。承接进行中速度的另一条路线见中断与重定向飞行中重定向