这是用户在 2024-6-21 11:33 为 https://blog.maximeheckel.com/posts/the-physics-behind-spring-animations/ 保存的双语快照页面,由 沉浸式翻译 提供双语支持。了解如何保存?
@MaximeHeckel

The physics behind spring animations
弹簧动画背后的物理原理

June 23, 2020 / 9 min read
2020 年 6 月 23 日 / 阅读时间 9 分钟

Last Updated: August 4, 2022 最后更新时间:2022 年 8 月 4 日

In the past few months, I've become a big fan of Framer Motion. After looking at how I could use it to animate my styled-components, I've been tinkering around spring animations and rebuilt almost all the transitions and animations of components in several UI projects. While showcasing the result to some fellow developers I got some questions around the meaning of some of the terms and options used to set up a spring animation like mass, stiffness, and damping. Most of them were setting them without really knowing how they were influencing the resulting animation. Lucky for them, I used to study maths and physics in college and was able to bring light on the physics behind this type of animation.
在过去的几个月里,我已经成为 Framer Motion 的忠实粉丝。在研究了如何使用它来为我的样式化组件设置动画之后,我一直在修补弹簧动画,并在几个 UI 项目中重建了组件的几乎所有过渡和动画。在向一些开发人员展示结果时,我对用于设置弹簧动画(如质量、刚度和阻尼)的一些术语和选项的含义提出了一些问题。他们中的大多数人在设置它们时并没有真正知道它们如何影响最终的动画。对他们来说幸运的是,我曾经在大学学习数学和物理,并且能够阐明此类动画背后的物理原理。

This article aims to explain how a spring animation in a library like Framer Motion works, the laws of physics that are behind it, and the relation between the different options you can set for a spring animation.
本文旨在解释像 Framer Motion 这样的库中的弹簧动画如何工作、其背后的物理定律,以及可以为弹簧动画设置的不同选项之间的关系。

Hooke's Law 胡克定律

First of all, a spring animation has this name because the animation itself follows the physics of a spring or what we also call a Harmonic Oscillator. This term and the math surrounding it might seem very scary and complicated but bare with me, I'll break down everything as simply as possible. When I was in college, we defined a Harmonic Oscillator as follows:
首先,弹簧动画之所以有这个名字,是因为动画本身遵循弹簧或我们也称为谐波振荡器的物理原理。这个术语和围绕它的数学可能看起来非常可怕和复杂,但对我来说,我会尽可能简单地分解所有内容。当我上大学时,我们定义谐波振荡器如下:

a system that experiences a force (F) proportional to a displacement x when displaced from its equilibrium.
当偏离平衡时,系统会受到与位移 x 成比例的力 (F)。

The formula for such force is called Hooke's Law and it is defined as follows:
这种力的公式称为胡克定律,定义如下:

1
F = -k*x

where k is a positive constant called stiffness which we can also write as:
其中 k 是一个称为刚度的正常数,我们也可以将其写为:

What that means is that:
这意味着:

  • ArrowAn icon representing an arrow
    if we pull the spring (i.e. x > 0 ) to a certain distance away from its equilibrium, it will start to move
    如果我们将弹簧(即 x > 0 )拉到远离其平衡状态一定距离,它将开始移动
  • ArrowAn icon representing an arrow
    if we don't pull it, it won't move (i.e. x = 0)
    如果我们不拉它,它就不会移动(即 x = 0)

However, maybe you might have heard at school or on one of the many science-focused Youtube channels on that force is the object's mass times its acceleration, which translates to the following formula:
然而,也许您可​​能在学校或许多以科学为重点的 Youtube 频道之一听说过,力是物体的质量乘以加速度,可转换为以下公式:

1
F = m*a

where m is the mass and a is the acceleration.
其中 m 是质量, a 是加速度。

Thus given this formula and the formula above, we can deduct that:
因此,根据这个公式和上面的公式,我们可以推出:

1
m*a = -k*x

which is equivalent to 这相当于

1
a = -k *x / m

We now have an equation from which we define the acceleration based on the displacement of our spring and the mass of the object attached to that spring. From the acceleration we can deduct the following:
现在我们有了一个方程,根据弹簧的位移和连接到该弹簧的物体的质量来定义加速度。从加速度我们可以推导出以下内容:

  • ArrowAn icon representing an arrow
    the velocity of the object at any given time
    任何给定时间物体的速度
  • ArrowAn icon representing an arrow
    the position of the object at any given time
    任何给定时间物体的位置

To get the velocity of the object, you need to add the acceleration rate to the previously recorded velocity, which can translate to the following equation:
要获得物体的速度,您需要将加速度与之前记录的速度相加,可以转化为以下等式:

1
v2 = v1 + a*t

Finally, we can get the position as it follows a similar principle: the position of the object is equal to the previously recorded position to which we add the velocity:
最后,我们可以得到位置,因为它遵循类似的原理:物体的位置等于之前记录的位置加上速度:

1
p2 = p1 + v*t

For the time interval, as frontend developers, we might know it better as a frame rate or "frames per second". Considering the smoothness of Framer Motion's animations we can assume that its spring animations runs at 60 frames per second thus a time interval that is constant and equal to 1/60 or 0.01666.
对于时间间隔,作为前端开发人员,我们可能更了解它作为帧速率或“每秒帧数”。考虑到 Framer Motion 动画的平滑度,我们可以假设其弹簧动画以每秒 60 帧的速度运行,因此时间间隔是恒定的且等于 1/600.01666

Translating the maths to Javascript
将数学转换为 JavaScript

Now that we've done the math, you can see that by knowing the mass of the object, the stiffness and the displacement of our spring, we can know the position of the object attached to that spring at any given time, i.e at any given frame. We can translate all the equations above in Javascript, and for a given displacement calculate all the positions of an object for 600 frames, i.e. 10 seconds:
现在我们已经完成了数学计算,您可以看到,通过了解物体的质量、弹簧的刚度和位移,我们可以知道在任何给定时间(即在任何时刻)连接到该弹簧的物体的位置给定的框架。我们可以用 Javascript 翻译上面的所有方程,并且对于给定的位移,计算 600 帧(即 10 秒)内对象的所有位置:

Function that returns the positions of an object following the motion of a spring
返回物体跟随弹簧运动的位置的函数

1
const loop = (stiffness, mass) => {
2
/* Spring Length, set to 1 for simplicity */
3
let springLength = 1;
4
5
/* Object position and velocity. */
6
let x = 2;
7
let v = 0;
8
9
/* Spring stiffness, in kg / s^2 */
10
let k = -stiffness;
11
12
/* Framerate: we want 60 fps hence the framerate here is at 1/60 */
13
let frameRate = 1 / 60;
14
15
/* Initiate the array of position and the current framerate i to 0 */
16
let positions = [];
17
let i = 0;
18
19
/* We loop 600 times, i.e. for 600 frames which is equivalent to 10s*/
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 is an array of number where each number
37
* represents the position of the object in a spring
38
* motion at a specific frame
39
*
40
* We use this array to plot all the position of the
41
* object for 10 seconds.
42
*/
43
return positions;
44
};

I built this small playground below with a graph representation of the positions that are returned by the function above a component animated by Framer Motion that has the same mass and stiffness. You can tune the mass and the stiffness with the range inputs above the graph and observe how each variable influences the animated component and the graph of positions.
我在下面构建了这个小游乐场,用图形表示位置,这些位置是由具有相同质量和刚度的 Framer Motion 动画的组件上方的函数返回的。您可以使用图表上方的范围输入来调整质量和刚度,并观察每个变量如何影响动画组件和位置图表。

Taking damping into account
考虑阻尼

While observing the visualization above, you might have wondered why is the spring animation never-ending as opposed to the ones you might have tried yourself with Framer Motion. That is because the math formulas we used to generate the position of the object were not taking into account friction and heat. If we want to obtain a spring animation that feels natural, we should see the movement of the object slowing down as time passes to eventually stop moving. That is where the damping comes into the picture. You might have seen this term when looking at the documentation of Framer Motion and wondered what it meant or does to the resulting spring animation, here's how we will define it:
在观察上面的可视化效果时,您可能想知道为什么弹簧动画永远不会结束,而不是您自己使用 Framer Motion 尝试过的动画。这是因为我们用来生成物体位置的数学公式没有考虑摩擦和热量。如果我们想要获得感觉自然的弹簧动画,我们应该看到对象的运动随着时间的推移而减慢,最终停止移动。这就是阻尼发挥作用的地方。您可能在查看 Framer Motion 文档时看到过这个术语,并想知道它对生成的弹簧动画意味着什么或有何作用,我们将如何定义它:

Damping is the force that slows down and eventually stops an oscillation by dissipating energy
阻尼是通过耗散能量来减慢并最终停止振荡的力

Its formula is: 其公式为:

1
Fd = -d * v

where d is the damping ratio and v the velocity
其中 d 是阻尼比, v 是速度

Taking damping into account will bring some changes to the acceleration formula we established in the first part. We know that
考虑阻尼会给我们在第一部分中建立的加速度公式带来一些变化。我们知道

1
F = m*a

However, F here is equal to the spring force and the damping force, instead of just the spring force, thus:
然而,这里的 F 等于弹簧力和阻尼力,而不仅仅是弹簧力,因此:

1
Fs + Fd = m*a -> a = (Fs + Fd)/m

We can now add this new formula to the Javascript code we've showcased in the previous part (I highlighted the additions I've made to the code compared to the previous implementation):
我们现在可以将这个新公式添加到我们在上一部分中展示的 Javascript 代码中(我突出显示了与之前的实现相比我对代码所做的添加):

Updated function that takes into account the damping ratio
更新了考虑阻尼比的函数

1
const loop = (stiffness, mass, damping) => {
2
/* Spring Length, set to 1 for simplicity */
3
let springLength = 1;
4
5
/* Object position and velocity. */
6
let x = 2;
7
let v = 0;
8
9
/* Spring stiffness, in kg / s^2 */
10
let k = -stiffness;
11
12
/* Damping constant, in kg / s */
13
let d = -damping;
14
15
/* Framerate: we want 60 fps hence the framerate here is at 1/60 */
16
let frameRate = 1 / 60;
17
18
let positions = [];
19
let i = 0;
20
21
/* We loop 600 times, i.e. for 600 frames which is equivalent to 10s*/
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
};

And finally, represent the resulting position data in the playground:
最后,表示游乐场中的结果位置数据:

As you can see we now have a spring animation that eventually stops due to damping dissipating the energy out of the system. The chart above showcases this by converging towards a final "resting position". By increasing the damping slider to a high value you can observe that the object subject to spring animation tends to converge to the "resting position" way faster than for a lower damping value.
正如您所看到的,我们现在有一个弹簧动画,由于阻尼将能量耗散出系统,该动画最终停止。上图通过向最终“静止位置”收敛来展示这一点。通过将阻尼滑块增加到较高的值,您可以观察到受弹簧动画影响的对象往往会比较低阻尼值更快地收敛到“静止位置”。

A real-life example 一个现实生活中的例子

By default, Framer Motion sets the stiffness of the spring animation to 100, the damping to 10, and the mass to 1 according to the docs. Below, I wrote an animated Button component that is closer to a real-life example that you might want to implement in your UI projects. Now that you know what mass, stiffness and damping you can try to fine-tune your spring animation.
默认情况下,Framer Motion 根据文档将弹簧动画的刚度设置为 100,阻尼设置为 10,质量设置为 1。下面,我编写了一个动画 Button 组件,它更接近您可能想要在 UI 项目中实现的现实示例。现在您知道了质量、刚度和阻尼,您可以尝试微调弹簧动画。

import { motion } from 'framer-motion';
import './scene.css';

const Example = () => {
return (
<motion.button
style={{
background:
'linear-gradient(180deg, #ff008c 0%, rgb(211, 9, 225) 100%)',
color: 'white',
height: '50px',
width: '200px',
borderRadius: '10px',
border: 'none',
boxShadow: 'none',
outline: 'none',
cursor: 'pointer',
}}
whileTap={{
scale: 1.3,
borderRadius: '6px',
}}
transition={{ type: 'spring', stiffness: 100, damping: 10, mass: 1 }}
>
Click me!
</motion.button>
);
};

export default Example;

Want to learn more the math/physics behind animations you see on a day-to-day basis?
想要了解更多日常看到的动画背后的数学/物理知识吗?

I dedicated an entire blog post about the math behind Cubic Béziers that contains slick visualizations that easily explain how the motion these complex formulas define is obtained!
我专门写了一篇关于三次贝塞尔曲线背后的数学的博文,其中包含流畅的可视化效果,可以轻松解释如何获得这些复杂公式定义的运动!

Want to learn more about Framer Motion?
想要了解有关 Framer Motion 的更多信息?

Checkout my blog post Guide to creating animations that spark joy with Framer Motion!
查看我的博客文章使用 Framer Motion 创建激发欢乐的动画指南!

Liked this article? Share it with a friend on Twitter or support me to take on more ambitious projects to write about. Have a question, feedback or simply wish to contact me privately? Shoot me a DM and I'll do my best to get back to you.
喜欢这篇文章吗?在 Twitter 上与朋友分享,或者支持我承担更雄心勃勃的项目来撰写。有疑问、反馈或只是想私下联系我?请给我发私信,我会尽力回复您。

Have a wonderful day. 祝你有美好的一天。

– Maxime ——马克西姆

A deep dive into the inner workings of spring animations in Framer Motion.

Drag to outliner or Upload
Close