配了三年动画,今天才发现 cubic-bezier() 一直在骗你——linear() 把弹簧动画从 JS 库里彻底救出来了

你的动画为什么总感觉「不对」?可能是 cubic-bezier() 欺骗了你。

这个问题藏得很深。设计师说「要有弹性」,前端工程师打开 MDN 翻 cubic-bezier(),找到一个 cubic-bezier(0.34, 1.56, 0.64, 1) 觉得差不多能用,然后交差了。界面确实「动了」,但用户总觉得哪里不对——按钮按下去像按在塑料上,弹层弹出来像纸片飘落,切换动画软趴趴的没有重量感。

根因不在代码质量,在于 cubic-bezier() 这个工具本身的数学局限。

cubic-bezier() 只能画 S 曲线,它画不出弹簧

cubic-bezier() 用四个控制点描述一条贝塞尔曲线。这条曲线从 (0,0) 到 (1,1),中间的两个控制点决定了曲率。因为它是多项式函数,曲线不能和自身交叉——也就是说,它只能画出光滑的 S 形,无法出现「冲过去再弹回来」的效果。

这就是为什么你用 cubic-bezier() 做的「弹性动画」永远看起来假。真正的弹簧运动有物理参数:质量(mass)、刚度(stiffness)、阻尼(damping)。当弹簧释放时,它会冲过平衡点,然后振荡几次才停下来。cubic-bezier() 的 S 曲线根本装不下这个过程。

那以前想做一个真正的弹簧动画怎么办?只能引入 GSAP 的 elastic.out()、Framer Motion 的 spring、或者 anime.js 之类的 JS 动画库,在运行时用 JavaScript 模拟物理方程。

2023 年之后,这件事变了。

linear() 的真正用法:它不是线性,它是分段线性插值

很多人看到 linear() 这个名字就跳过了——「线性?那不就是匀速运动吗,丑死了。」

这是一个误解。linear() 的核心能力不是「匀速」,而是「允许值超出 0-1 范围」。

linear() 的语法是:

transition-timing-function: linear(点1, 点2, ..., 点N);

每个点的格式是 值 进度%(进度是可选的)。关键在于:值可以小于 0,可以大于 1。当值超过 1,就产生了回弹/超调(overshoot)——元素会「冲过头」,然后浏览器继续按线性插值往后走,于是就形成了真实的振荡衰减。

看一个具体的弹簧曲线(质量 1、刚度 200、阻尼 15):

:root {
  --ease-spring: linear(
    0,
    0.18,
    0.42,
    0.72,
    0.95,
    1.06,
    1.04,
    1.01,
    0.99,
    1
  );
}

.button:active {
  transform: scale(0.95);
  transition: transform 0.6s var(--ease-spring);
}

注意这些值:0.95 → 1.06 → 1.04 → 1.01 → 0.99 → 1。动画过程中 transform 先从 0.95 冲到 1.06(超调),然后回落,经过几次振荡最终停在 1。这就是一个真实物理弹簧的压缩-释放过程,全部在 CSS 里完成,运行时零 JavaScript。

你不需要手写这些数字

手调 linear() 的参数不现实。正确的做法是用工具把物理参数转换成 CSS。

推荐工具链:

  1. spring.computer(Sam Selikoff):输入 mass/stiffness/damping,导出 linear() 曲线。支持预览,导出的代码直接可用。

  2. linear-easing-generator.netlify.app(Jake Archibald):输入 CSS linear() 字符串,生成可视化曲线图;也可以反向从曲线推参数。

  3. Chrome DevTools:在 Animations 面板里可以直接看到 linear() 曲线的可视化,确认超调量是否符合预期。

实际工作中,你不需要理解每个数字的含义。你只需要知道三件事:

  • 刚度(stiffness)越高,弹簧越「硬」,振荡越快
  • 阻尼(damping)越高,弹簧越快停下来,超调越小
  • 质量(mass)越大,弹簧越「重」,响应越慢

打开 spring.computer,调到你满意的弹性效果,复制导出结果到 CSS 里。这就是工程上正确的用法。

真实数据:87% 覆盖,Safari 18+ 完全支持

linear() 函数是 CSS Easing Functions Level 1 规范的一部分,属于 Baseline Widely Available 级别。

各浏览器支持情况(2026 年 8 月):

浏览器 支持版本 稳定版时间
Chrome / Edge 113+ 2023 年 12 月
Firefox 112+ 2023 年 12 月
Safari 17.2+ 2024 年 1 月
Safari iOS 17.2+ 2024 年 1 月

全球覆盖率约 87%。剩余的 13% 主要是很老的移动设备。对于大多数项目,@supports (transition-timing-function: linear(0, 1)) 的渐进增强策略就够了——不支持的浏览器会回退到 ease,动画失去弹性但功能正常。

如果你在用 GSAP 或 Framer Motion 仅仅是为了获得弹簧效果,linear() 是直接的替代方案。迁移路径很简单:

/* 之前 */
transition: transform 0.6s cubic-bezier(0.34, 1.56, 0.64, 1);

/* 之后 */
transition: transform 0.6s linear(
  0,
  0.009, 0.035 2.1%,
  0.141, 0.281 6.7%,
  0.723 12.9%,
  0.938 16.7%,
  1.017, 1.077, 1.121, 1.149 24.3%,
  1.163, 1.161, 1.154 29.9%,
  1.129 32.8%,
  1.051 39.6%,
  1.017 43.1%,
  0.991, 0.977 51%,
  1 100%
);

后者是真正的物理弹簧,前者是「S 形曲线假装有弹性」。

什么时候仍然需要 JS 动画库

linear() 能做弹簧,但不能做所有事。以下场景 JS 动画库仍有优势:

  • 物理参数需要动态计算:比如拖拽释放时根据速度计算弹道终点
  • 弹性效果需要交互反馈:用户拖动元素,松手后根据拖动方向和速度决定弹到哪里
  • 复杂的编排序列:多个动画之间有时序依赖关系

但如果你的场景是「按钮点击反馈」「弹层出现」「卡片切换」这类固定路径的 UI 动效,linear() 完全够用,而且性能更好(纯 CSS,不占用主线程)。

下一步

  1. 打开 spring.computer,把 stiffness 设为 200、damping 设为 15,看生成的 linear() 曲线
  2. 找一个现有项目里的 cubic-bezier() 弹性效果,替换成工具生成的 linear() 曲线
  3. 在 Chrome DevTools → Animations 面板里验证曲线的超调量是否符合设计预期
  4. 加上 @supports 渐进增强:.button { transition: transform 0.6s ease; } @supports (transition-timing-function: linear(0, 1)) { .button { transition-timing-function: linear(0, 0.18, 0.42, 0.72, 0.95, 1.06, 1.04, 1.01, 0.99, 1); } }

弹性这件事,以前靠 20-60 KB 的 JS 库才能做到。现在四行 CSS 够了。

评论区

0 条评论

登录后可评论。

阿柯·前端架构 10 阅读