缓动:把时间重新塑形
真实时间是均匀流逝的。缓动(easing)是一个
的函数,它不动起点也不动终点,只重新分配中途的快慢:先慢后快(ease-in)、先快后慢(ease-out)、两端慢中间快(ease-in-out)。经典的 power 与 circ、back 之外,@vega/anim 还带 sine(最温和)、expo(最强烈)、elastic(弹片往复)与
bounce(落地回弹)几族,easingFns 里共 23 条具名曲线。
值域并不限制在
。back 与 elastic 会越过 1 再回落,anticipate 先反向蓄力跌到 0 以下,这些过冲正是曲线形状的一部分——引擎不做截断,越界由调用方的值域自行承受。
1 · 曲线形状与运动节奏的对应
一条缓动曲线的横轴是时间进度,纵轴是值进度。曲线在某处越陡,对应时刻的运动就越快;曲线贴近水平,运动就近乎停滞。两颗小球同时出发、同时到达,差别只在中途。
2 · 三个朝向同源
ease-out 是 ease-in 的时间与值双翻转,easeOut(t) = 1 − easeIn(1 − t);ease-in-out 是把 ease-in 关于中点镜像。引擎据此每族只写一条曲线,另外两个朝向由 reverseEasing 与 mirrorEasing 两个高阶函数变换得到:23 条具名曲线里手写的实现只有 9 条,其余 14
条都是这两个变换的产物。
有一族是反过来的。bounce 的标准定义在 out 朝向——落地回弹这个动作本身就是「先大后小」的,写成 in 反而别扭——所以 easing.ts 里手写的是 bounceOut,bounceIn 由 reverseEasing(bounceOut) 派生,bounceInOut 再由它镜像。anticipate
则两者都不是:它把前半段的 backIn 与后半段的 easeOut 拼在一起,两段在中点都取 0.5 而连续衔接,终点回到 1 且不过冲。
建议 · 这条同源关系也解释了为什么对称的缓动(easeInOut、linear)看不出 reverse 与 mirror 两种重复方向的区别——两种变换作用在对称函数上结果相同。要观察差别得先把缓动换成单向的 easeOut,见时序。
3 · cubic-bezier 的反解
cubic-bezier(x1,y1,x2,y2) 不是一个
的函数,而是一条以参数
描述的曲线:
与
各自是
的三次多项式。动画引擎手上只有时间进度
,要先求出满足
的
,才能代入求
。这一步没有闭式解,引擎用 Newton-Raphson 迭代求根,导数过小时退回二分——浏览器内部对 cubic-bezier() 做的是同一件事。
steps(n) 走另一条路:把进度切成
级台阶,值在台阶之间不连续地跳变,用于逐帧动画与打字机效果。
import { Animation, cubicBezier } from '@vega/anim';
const anim = new Animation({
keyframes: [0, 1],
duration: 1500,
ease: cubicBezier(0.42, 0, 0.58, 1), // 或具名 'easeInOut'
repeat: Infinity,
onUpdate: (value) => { // value = ease(进度)
ball.style.transform = `translateX(${value * maxX}px)`;
},
});
手动指定四个控制点的效果见缓动曲线编辑器。曲线作用的「值」从何而来见插值;把这套预设塑形整个换成物理系统,见弹簧;曲线如何被一个可暂停、可反向的游标驱动见播放控制。