弹簧与状态机
角色里每一个连续量都是阻尼弹簧。没有补间,也没有关键帧。
积分器
半隐式欧拉,固定子步 1/120 秒。在这些频率下半隐式是稳定的,代价只有 RK4 的三分之一,当每帧有几百根弹簧在走时这很重要。
为什么要固定子步
直接用帧间隔积分会让模拟依赖帧率:同一根弹簧在 144 Hz 显示器上比在 60 Hz 上收敛更快,帧率不稳时还会明显抖动。
累积固定子步则意味着 144 Hz 和 60 Hz 显示器在同一墙钟时刻收敛到同一值。
| rate | value after 1s |
|---|---|
| 60 Hz | 0.980837 |
| 144 Hz | 0.981133 |
阻尼
ζ = 1 是临界阻尼:最快接近且不超调。低于 1 弹簧会超调并振荡,这就是 squash 有橡胶感、通知徽标有弹出感的原因。
blue ζ=1, green ζ=0.6, red ζ=0.3, all ω=10
脉冲
有些运动更适合表达成一脚,而不是一条曲线。不要移动目标,直接给速度加值:
弹簧吸收这一下并自行收敛。angry 每 1.8 到 3.2 秒做一次,产生的回弹感如果要写成显式动画会非常繁琐,而且两次看起来永远不会机械地相同。
Behaviour
一个 behaviour 每帧返回四个目标。idle 是三个频率互不相关的正弦波:
因为 0.5、0.17、0.27 和 0.85 没有公约周期,合成运动在人类时间尺度上不会重复。这就是侧边栏里待着的角色像活的、而不是一段循环的区别。
把弹簧导出成 CSS
有些运动必须作为 CSS 过渡运行,而不是走 JS ticker。springToLinearEasing 把弹簧采样成 linear() 缓动,让曲线对上:
linear(0, 0.1154, 0.3501, 0.592, 0.7878, 0.9221, 1.0004, 1.0367, 1.046, 1.0409, 1.0302, 1.019, 1.0099)减少动态效果
当读者偏好减少动态效果时,弹簧被直接落到目标,而不是步进;只画一帧,不启动循环。角色是正确且静止的,而不是缺席的。