← CSS Animation · 能力速览 / 用 linear() 把弹簧物理烘焙进纯 CSS 待审核 2 / 22
linear() easing · spring 物理 · 零运行时 JS

用 linear() 把弹簧物理烘焙进纯 CSS

最自然的运动是弹簧 spring:由 stiffness(劲度)、damping(阻尼)、mass(质量)三个物理量决定,会有恰到好处的过冲与回弹。问题在于 cubic-bezier 只有四个控制点,描述的是一条三次曲线,最多模拟一次过冲,无法表达 spring 那种「冲过头 → 荡回来 → 再小幅震荡」的多次回弹。本系列的好动画准则里,spring demo 走的是 JS 解方程 + element.animate()(WAAPI)的路子。

本页换一条路:把 spring 物理预先采样、烘焙进一条 linear() 缓动,之后的运动全部交给纯 CSS,运行时不跑一行 JS。原理来自 Carmen Ansio 的 Spring physics in CSS。关键认识:任何动画都是按进度(0→1)在两值间插值,缓动函数本质是一张查找表——把「时间进度」映射成「已完成进度」。cubic-bezier 是参数化的曲线;而 linear() 允许你在等间距的输入点上直接给出任意输出值,等于让你手写这张查找表。于是只要按时间采样弹簧方程、把每一帧的位移当作输出值塞进 linear(),物理精确的回弹就「焊死」在了缓动里。

1 · 把弹簧采样成 linear()

拖动三个物理参数,曲线图实时画出采样得到的缓动:横轴是时间进度,纵轴是已完成进度,等间距的采样点(图中紫点)就是 linear() 的控制值;超过 1 的部分就是过冲。蓝色虚线是带过冲的 cubic-bezier(.34, 1.56, .64, 1) 对照——它只能拱起一次,回不来也震不动。下方两颗球用同一时长、各自的缓动同场对比。

面板底部那段 CSS 随参数实时更新,正是页面真正使用的样式。注意动画本身是 transition 在跑,JS 只在你调参数时把弹簧解算一次、拼出 linear() 字符串写进自定义属性,之后浏览器自己插值。既然它只是一条普通缓动,就能像 ease-out 一样用在 transition / animation 的任意可动属性上。

阻尼越小,过冲越大、来回震荡越多次,采样点也就越「波浪」;阻尼足够大时直接收敛到位、不过冲。把采样点数拖小到个位数,会看到曲线退化成折线、回弹被「采漏」——这正说明 linear() 是离散查找表,点越多越逼近真实物理(代价是声明更长)。生产里常用工具(见文末)生成,并对相近段做精简。

2 · 同一条 linear() 作用于不同属性

缓动与「动哪个属性」无关。下面三个盒子共用同一条 spring 缓动,分别作用在 translate / scale / rotate 上,过冲与回弹一致——因为它们读的是同一张查找表。换一个 spring 预设,三者仍同步变化。

取舍。linear() 把物理「焊死」进了一条静态曲线:它不像 JS 弹簧库那样能从当前速度被打断接管(见可打断),也无法表达真正无限的震荡;采样点是定长的,极端欠阻尼会被截断。需要随手势速度连续接管、或 spring 参数运行时频繁变的,仍该用 WAAPI / Motion 等库(见性能与取舍)。但对「一次性入场、状态切换」这类最常见的回弹,linear() 用纯 CSS 就够,且能下放合成线程、尊重 prefers-reduced-motion(本页命中时直接到位、不回弹)。

3 · 相关链接