换向缓动:reverse 时别把 ease 倒过来
播放控制里的 reverse 只是给 speed 取负,游标从此递减、纯函数原样重采样。代价藏在缓动里:曲线也被一起时间反演了。一条 expoOut(慢收尾)正向播放是「先快后慢」,倒放就成了 expoIn 的「先慢后快」——回退段因此显得生硬。
easeReverse 换一种做法:换向那一刻记下当前位置作锚点,对剩余距离重新施加一条正向缓动,于是回退段仍保有 ease-out 的减速收尾。
1 · 两条曲线的分岔
默认在播放头走到 duration 一半时翻向。注意 expoOut 是先快后慢,此刻值已约 0.97,球看着已近终点——时间的一半不是运动的一半。反向之后两条曲线随即分岔:灰蓝的 naive 把上升曲线镜像着倒回来,上升快的区段回落也陡;主色的 easeReverse 从锚点起另画一段减速收尾的新曲线。
2 · 锚点的算法
换向瞬间冻结一对锚点 ,之后每帧按
重算,其中 是「换向后经过的进度除以剩余进度」,恒为 。 时 ,保证换向连续不跳变; 时精确落到端点。
这与时序里的 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),用于「进场活泼、退场克制」这类不对称的手感。承接进行中速度的另一条路线见中断与重定向与飞行中重定向。