在过去的几个月里,我成了 Framer Motion 的忠实粉丝。在研究了如何用它来为我的 styled-components 添加动画效果后,我一直在捣鼓弹簧动画,并重建了好几个 UI 项目中几乎所有组件的过渡和动画效果。
当向一些开发同行展示成果时,他们对我用来设置弹簧动画的一些术语和选项(如质量、刚度和阻尼)的含义提出了一些问题。他们中的大多数人都是在不太清楚这些参数如何影响最终动画效果的情况下进行设置的。
幸运的是,我大学时学过数学和物理,能够解释这类动画背后的物理原理。本文旨在解释像 Framer Motion 这样的库中的弹簧动画是如何工作的,其背后的物理定律,以及你可以为弹簧动画设置的不同选项之间的关系。
胡克定律:弹簧动画的基础
首先,弹簧动画得名于其运动轨迹遵循弹簧的物理特性,亦即我们所说的简谐振荡器。这个术语及其相关数学原理可能看似复杂骇人,但请稍安勿躁,我将用最浅显的方式逐一解析。
在大学物理中,我们对简谐振荡器的定义如下:
当偏离平衡位置时,该系统会受到与位移 x 成正比的力 (F) 的作用。
这种力的公式被称为胡克定律,其定义如下:
F = -k × x
其中:
F是力k是劲度系数(一个正常数)x是位移
我们也可以将其表述为:
力 = -劲度系数 × 位移
这意味着:
- 如果我们拉伸弹簧(即
x > 0),使其偏离平衡位置一定距离,它就会开始运动 - 如果我们不拉伸它,它就不会运动(即
x = 0)
从力到加速度
不过,你可能在学校或某个科普 YouTube 频道上听说过,力等于物体的质量乘以加速度,这可以用以下公式表示:
F = m × a
其中:
m表示质量a表示加速度
因此,根据这个公式和胡克定律,我们可以推导出:
m × a = -k × x
这等价于:
a = -k × x / m
或者说:
加速度 = -刚度 × 位移 / 质量
从加速度到位置
现在我们得到了一个方程,可以根据弹簧的位移和附着在弹簧上的物体质量来定义加速度。从加速度我们可以推导出:
- 物体在任意时刻的速度
- 物体在任意时刻的位置
要获取物体的速度,需要将加速度与先前记录的速度相加:
v2 = v1 + a × t
即:
速度 = 原速度 + 加速度 × 时间间隔
最后,我们可以根据类似原理获取位置:
p2 = p1 + v × t
即:
位置 = 原位置 + 速度 × 时间间隔
关于时间间隔
在时间间隔方面,作为前端开发者,我们可能更熟悉帧率或"每秒帧数"的概念。考虑到 Framer Motion 动画的流畅性,我们可以推断其弹性动画以每秒 60 帧运行,这意味着时间间隔是恒定且等于 1/60 或 0.01666 的。
将数学公式转换为 JavaScript
完成数学计算后,你会发现只要知道物体的质量、弹簧的劲度系数和位移量,就能确定任意时刻(即任意帧)弹簧所系物体的位置。
我们可以将上述所有方程转化为 JavaScript 代码,针对特定位移量计算出物体在 600 帧(即 10 秒)内的所有位置。
基础弹簧动画函数
以下是返回物体沿弹簧运动轨迹位置的函数:
1const loop = (stiffness, mass) => {2 /* 弹簧长度,为简化计算设为 1 */3 let springLength = 1;4
5 /* 物体位置和速度 */6 let x = 2;7 let v = 0;8
9 /* 弹簧刚度,单位 kg / s^2 */10 let k = -stiffness;11
12 /* 帧率:我们期望 60 fps,因此帧率为 1/60 */13 let frameRate = 1 / 60;14
15 /* 初始化位置数组和当前帧数 i 为 0 */16 let positions = [];17 let i = 0;18
19 /* 循环 600 次,即 600 帧,相当于 10 秒 */20 while (i < 600) {21 let Fspring = k * (x - springLength);22
23 let a = Fspring / mass;24 v += a * frameRate;25 x += v * frameRate;26
27 i++;28
29 positions.push({30 position: x,31 frame: i,32 });33 }34
35 /**36 * positions 是一个数字数组,其中每个数字37 * 代表物体在特定帧的弹簧运动位置38 *39 * 我们使用这个数组来绘制物体在40 * 10 秒内的所有位置41 */42 return positions;43};
考虑阻尼效应:让动画更自然
在观察实际效果时,你或许会疑惑:为何这个弹簧动画永无止境,与你使用 Framer Motion 时体验到的效果截然不同?
这是因为我们用于生成物体位置的数学公式未考虑摩擦力与热能因素。若想实现自然流畅的弹簧动画,应当观察到物体运动随时间推移逐渐减速,直至完全静止——这正是阻尼概念登场之时。
什么是阻尼?
当你在查阅 Framer Motion 文档时可能见过这个术语,并好奇其含义及对弹簧动画的影响。以下是我们的定义:
阻尼是一种通过消耗能量来减缓并最终停止振荡的力。
其公式为:
Fd = -d × v
其中:
Fd是阻尼力d是阻尼比v是速度
即:
阻尼力 = -阻尼系数 × 速度
更新加速度公式
考虑阻尼因素后,我们需对第一部分建立的加速度公式进行调整。已知:
F = m × a
然而,此处的 F 等于弹簧力与阻尼力之和,而非仅弹簧力,因此:
Fs + Fd = m × a
推导得:
a = (Fs + Fd) / m
包含阻尼的完整函数
现在我们可以将这个新公式添加到之前的 JavaScript 代码中:
1const loop = (stiffness, mass, damping) => {2 /* 弹簧长度,为简化计算设为 1 */3 let springLength = 1;4
5 /* 物体位置和速度 */6 let x = 2;7 let v = 0;8
9 /* 弹簧刚度,单位 kg / s^2 */10 let k = -stiffness;11
12 /* 阻尼常数,单位 kg / s */13 let d = -damping;14
15 /* 帧率:我们期望 60 fps,因此帧率为 1/60 */16 let frameRate = 1 / 60;17
18 let positions = [];19 let i = 0;20
21 /* 循环 600 次,即 600 帧,相当于 10 秒 */22 while (i < 600) {23 let Fspring = k * (x - springLength);24 let Fdamping = d * v;25
26 let a = (Fspring + Fdamping) / mass;27 v += a * frameRate;28 x += v * frameRate;29
30 i++;31
32 positions.push({33 position: x,34 frame: i,35 });36 }37
38 return positions;39};
我们现在有了一个弹簧动画,由于阻尼作用将能量从系统中耗散,它最终会停止。物体会向最终"静止位置"收敛。
阻尼值的影响
调整阻尼参数时,你可以观察到:
- 低阻尼值:物体会多次振荡后才停止,动画更有弹性
- 高阻尼值:物体快速收敛至静止位置,减少振荡,动画更平滑
- 临界阻尼:物体以最快速度到达静止位置且不产生振荡
Framer Motion 中的实际应用
默认情况下,根据文档说明,Framer Motion 将弹簧动画的参数设置为:
- 刚度 (stiffness):
100 - 阻尼 (damping):
10 - 质量 (mass):
1
实际案例:动画按钮
下面是一个更接近实际应用场景的动画组件示例,您可能会在 UI 项目中用到它:
1import { motion } from 'framer-motion';2
3const AnimatedButton = () => {4 return (5 <motion.button6 whileHover={{ scale: 1.05 }}7 whileTap={{ scale: 0.95 }}8 transition={{9 type: "spring",10 stiffness: 400,11 damping: 17,12 mass: 113 }}14 style={{15 padding: '12px 24px',16 borderRadius: '8px',17 border: 'none',18 background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',19 color: 'white',20 fontSize: '16px',21 cursor: 'pointer',22 }}23 >24 点击我25 </motion.button>26 );27};
参数调整指南
既然您已经了解了质量、刚度和阻尼的含义,现在可以更有针对性地调整弹簧动画:
刚度 (Stiffness)
- 低值 (50-100):柔和、缓慢的动画,适合大型元素
- 中值 (150-300):平衡的动画,适合大多数 UI 交互
- 高值 (400-600):快速、紧凑的动画,适合小型元素或按钮
阻尼 (Damping)
- 低值 (5-10):更多振荡,更有弹性,活泼的感觉
- 中值 (15-25):适度振荡,自然的感觉
- 高值 (30-50):几乎无振荡,平滑的感觉
质量 (Mass)
- 低值 (0.5-1):轻盈、快速的响应
- 高值 (2-5):沉重、缓慢的响应,适合大型或"重"的元素
其他弹簧动画选项
为保持文章简洁,我略去了 Framer Motion 为弹性动画提供的其他选项,例如:
- velocity(速度):在上述示例中,我将初始速度视为等于 0
- restSpeed(静止速度):当速度低于此值时,动画被视为完成
- restDelta(剩余差值):当位置与目标位置的差值小于此值时,动画被视为完成
这些参数都在文档中有详细定义,我建议您在理解了基础物理原理后,尝试调整这些参数,观察它们如何影响最终动画效果。
调试技巧
在调整弹簧动画参数时,以下技巧可能会有帮助:
- 使用浏览器开发工具的慢动作功能,更清楚地观察动画效果
- 从默认值开始,逐个调整参数,理解每个参数的影响
- 参考其他应用的动画效果,尝试复现类似的感觉
- 保持一致性,在整个应用中使用相似的动画参数集
性能考虑
弹簧动画相比传统的缓动函数(easing functions)有一些性能特点:
- 弹簧动画需要实时计算每一帧,而不是使用预定义的曲线
- 适当的阻尼值可以减少动画时长,提高性能
- 避免同时运行过多的弹簧动画
总结
理解弹簧动画背后的物理原理,能让你:
- 更精确地控制动画效果,而不是盲目调参
- 创建更自然的用户体验,符合用户的物理直觉
- 提高开发效率,快速实现想要的动画效果
- 与设计师更好地沟通,用准确的术语讨论动画细节
弹簧动画是现代 UI 设计中不可或缺的一部分。从简单的按钮交互到复杂的页面过渡,掌握这些物理原理将帮助你创建更加出色的用户体验。
现在,打开你的代码编辑器,开始尝试调整这些参数,感受物理学如何让你的界面"活"起来吧!
希望这篇文章能帮助你更好地理解和使用 Framer Motion 的弹簧动画。如果你有任何问题或想法,欢迎交流讨论。
译者注:本文译自 The Physics Behind Spring Animations,有删改。
QiuYeDx
一个热爱技术、追求品味的开发者,专注于前端开发、交互动效、二次摄影等领域。