算法与数据结构 / 动画引擎原理 · 从插值到播放控制 / 缓动曲线编辑器:拖控制点 待审核 4 / 47
cubic-bezier · 控制点 · Newton 反解

缓动曲线编辑器:拖控制点

cubic-bezier(x1,y1,x2,y2) 只有四个数。两个端点被固定在 (0,0)(0,0)(1,1)(1,1)——缓动必须从 0 开始、到 1 结束——形状全部交给两个控制点 P1=(x1,y1)P_1 = (x_1, y_1)P2=(x2,y2)P_2 = (x_2, y_2)。Bézier 曲线是控制点的加权混合,P1P_1P2P_2 像两块磁铁,把曲线从对角线(linear)拽向自己。

控制点的 xx 被锁在 [0,1][0,1] 内,因为时间必须单调推进;yy 不设限,拉到 1 以上或 0 以下就得到越过端点的过冲与回弹。

1 · 控制点与曲线形状

手柄线(P0P1P_0 \to P_1P3P2P_3 \to P_2)的斜率就是曲线在两端的瞬时速度。起点手柄越平,启动越慢,得到 ease-in;终点手柄越平,收尾越慢,得到 ease-out。CSS 的四个关键字 ease / ease-in / ease-out / ease-in-out 各自对应一组固定的控制点,其中 ease(0.25,0.1,0.25,1)(0.25, 0.1, 0.25, 1) 是唯一让两个控制点共用同一个 xx 的一组。

图 1-1 · cubic-bezier 的两个控制点与它塑出的运动。可拖动 P1、P2 实时重塑曲线,或点四个预设按钮对照 CSS 关键字对应的控制点。

2 · 参数曲线与反解

cubic-bezier 不是 y=f(x)y = f(x),而是一条以参数 u[0,1]u \in [0,1] 描述的曲线:xxyy 都是 uu 的三次多项式。这带来一个容易被忽略的分工——画这条曲线与用这条曲线,走的不是同一条代码路径。

画曲线时对 uu 均匀采样,逐点取 (x(u),y(u))(x(u), y(u)) 连成折线,不需要任何求解;上面那块画布正是这么描点的。而动画引擎手上只有时间进度 xx,必须先求出满足 x(u)=xx(u) = xuu,才能代入 y(u)y(u)。引擎用 Newton-Raphson 迭代求根,导数过小时退回二分——浏览器实现 cubic-bezier() 时做的是同一件事。

注 · 沿 uu 均匀采样得到的点在 xx 轴上并不均匀:P1P_1P2P_2xx 挤在一侧时,曲线的采样点也随之疏密不均。描点画图看不出来(折线足够密),但若拿这批点当查找表反查 yy,误差会集中在 xx 分布最稀的那一段。这正是引擎不用查表而用迭代的原因。

import { Animation, cubicBezier } from '@vega/anim';

// 编辑器拖出来的两个控制点
const ease = cubicBezier(x1, y1, x2, y2);

const anim = new Animation({
  keyframes: [0, 1],
  duration: 1500,
  ease,                  // 引擎给定时间 x, Newton 反解 u 再求 y
  repeat: Infinity,
  onUpdate: (value) => {
    ball.style.transform = `translateX(${value * maxX}px)`;
  },
});

具名缓动与三个朝向同源派生的原理见缓动。若要的不是一条预设曲线,而是有质量与初速度、时长由参数自己决定的物理回弹,换成弹簧