Transform 实验室

贝塞尔曲线:从数学原理到动画缓动实践

2025-03-02 wangjun 14 min read
T()

为什么动画都要用贝塞尔?

如果动画匀速(linear)进行,观感是"死板"的。现实中物体运动总是先加速后减速。贝塞尔曲线用几个控制点就能表达这种"快慢变化",因此在 CSS、SVG、游戏引擎中无处不在。

从线性插值说起

两点之间做线性插值:

P(t) = P0 + t * (P1 - P0),  t ∈ [0, 1]

t 匀速增大时,点匀速移动,这就是 linear 动画。

de Casteljau 递推:多阶贝塞尔

贝塞尔曲线通过"逐层插值"构造。以二次(3 点)为例:先在 P0→P1P1→P2 上取插值点 A、B,再在 A→B 上插值得到曲线点:

P0 ──A── P1 ──B── P2
        └────Q────┘   Q = 曲线上的点

三次贝塞尔(4 个点,CSS 默认)同理,只是多插值一层:

function bezier(t, p0, p1, p2, p3) {
  const u = 1 - t;
  return u*u*u*p0 + 3*u*u*t*p1
       + 3*u*t*t*p2 + t*t*t*p3;
}

CSS cubic-bezier() 的秘密

CSS 的 transition-timing-function: cubic-bezier(x1, y1, x2, y2) 正是三次贝塞尔:(x1,y1)(x2,y2) 是两个控制点,起点固定在 (0,0),终点固定在 (1,1),曲线输出就是"进度 → 完成度"的映射:

.card { transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); }
/* 常用缓动对照 */
/* ease-in     = cubic-bezier(0.42, 0, 1, 1)     先慢后快 */
/* ease-out    = cubic-bezier(0, 0, 0.58, 1)     先快后慢 */
/* ease-in-out = cubic-bezier(0.42, 0, 0.58, 1)  两端慢中间快 */
/* back-out    = cubic-bezier(0.34, 1.56, 0.64, 1) 带回弹 */

JS 手写缓动函数

很多动画库需要拿到具体进度值。三次贝塞尔求"给定进度 x,求 y",需要解一元三次方程(或牛顿迭代):

function cubicBezierY(x, x1, y1, x2, y2) {

  let t = x;
  for (let i = 0; i < 8; i++) {
    const xt = bezierX(t, x1, x2);
    const dxt = 3*(1-t)*(1-t)*(x1-x) + 6*(1-t)*t*(x2-x1) + 3*t*t*(x-x2);
    t -= (xt - x) / dxt;
  }
  return bezierY(t, y1, y2);
}

有了进度函数,自定义动画就只剩一行:style.transform = scale(progress)

贝塞尔在 SVG 中的角色

SVG 路径 path d="M 0 0 C 100 0 200 100 300 300" 里的 C 命令就是三次贝塞尔曲线,S 是平滑延伸。设计稿导出的图标路径,几乎全是贝塞尔片段的拼接 -- 你每天看的矢量图标,都是贝塞尔的产物。

贝塞尔曲线把"快慢"翻译成数学,把数学翻译成视觉 -- 理解它,动画就不再是玄学。