缓动曲线在UI动画中的应用十分广泛。缓动曲线可以用来控制动画的运动速率,使其按照我们的意愿模拟真实物体的运动规律。举个例子,当我们往上抛出一个石块时,在不考虑空气阻力的情况下,石块会在重力的作用下,先匀减速上升,直至速度为零。而后,石块的速度又会从零开始匀加速下降。那么,如何模拟这样的运动过程呢?在高中物理课上,我们已经知道,石块的位移和时间之间满足平方关系:
h(t) = v_0t – \frac{1}{2}gt^2 + h_0
其中,h(t) 是石块在 t 时刻的高度,v_0 是石块的初速度,g 是重力加速度,h_0 是石块的初始高度。从这个式子可以看出,我们需要一条二次方的运动曲线(抛物线)来模拟这样的运动过程。而类似这样的一条曲线,就是本文要讨论的缓动曲线。
总的来说,缓动曲线包含四大类,分别是线性(linear)、缓入(ease in)、缓出(ease out)和缓入缓出(ease in and out)。除了线性类,其余三大类又可以细分出各种子类,比如二次方缓动曲线(Quadratic)就是其中的一个子类。以上文提到的石块为例,石块的运动曲线满足二次方缓动曲线,而上升过程满足缓出的过程(速度先快后慢),下降过程则满足缓入过程(速度先慢后快)。本文将对线性类和缓动类的 10 种子类共 11 种类别 31 种组合情况进行公式化整理,并绘制相应的缓动曲线图,供大家查阅。这 11 种类别分别为:
- Linear
- Sinusoidal
- Quadratic
- Cubic
- Quartic
- Quintic
- Exponential
- Circular
- Back
- Elastic
- Bounce
目录
缓动曲线的关系
曲线关系
缓动曲线的定义域是 [0, 1],而其值域满足前提
\begin{align}
f(0) &= 0,\newline
f(1) &= 1
\end{align}
从这个前提出发,我们将会看到,不论是直线,还是缓动曲线,都会遵循相同的计算方式。
假设以函数 f_b(x) 作为基础函数(一般可作为缓入函数),若缓入函数表示为
f(x) = f_b(x)
那么我们将会看到,所有的缓出曲线必然可以表示为
g(x) = 1 – f_b(1-x)
而缓入缓出曲线可以用分段函数表示
h(x) =
\begin{cases}
0.5\cdot f_b(2\cdot x)&,x \in [0, 0.5] \newline
\newline
0.5\cdot[2-f_b(2\cdot(1-x))]&,x\in(0.5, 1]
\end{cases}
举例
平方曲线
对于平方曲线,我们定义基础函数为
f_b(x) = x^2,\quad x\in[0, 1]
则缓入函数表示为
f(x) = f_b(x) = x^2,\quad x\in[0, 1]
缓出函数可以表示为
g(x) = 1 – f_b(1-x) = 1-(1-x)^2,\quad x\in[0, 1]
而缓入缓出曲线可以用分段函数表示
\begin{align}
h(x) &=
\begin{cases}
0.5\cdot f_b(2\cdot x)&,x \in [0, 0.5] \newline
\newline
0.5\cdot[2-f_b(2\cdot(1-x))]&,x\in(0.5, 1]
\end{cases}\newline
\newline
&=
\begin{cases}
0.5\cdot(2x)^2&,x \in [0, 0.5] \newline
\newline
0.5\cdot(2-[2(1-x)]^2)&,x\in(0.5, 1]
\end{cases}\newline
\newline
&=
\begin{cases}
2x^2&,x \in [0, 0.5] \newline
\newline
1-2(1-x)^2&,x\in(0.5, 1]
\end{cases}
\end{align}
直线
对于直线来说,上述的缓动曲线关系依然成立。令基础函数为
f_b(x) = x,\quad x\in[0, 1]
则缓入函数:
f(x) = f_b(x) = x,\quad x\in[0, 1]
缓出函数:
g(x) = 1 – f_b(1-x) = 1 – (1 – x) = x,\quad x\in[0, 1]
缓入缓出函数:
\begin{align}
h(x) &=
\begin{cases}
0.5\cdot f_b(2\cdot x)&,x \in [0, 0.5] \newline
\newline
0.5\cdot[2-f_b(2\cdot(1-x))]&,x\in(0.5, 1]
\end{cases}\newline
\newline
&=
\begin{cases}
0.5\cdot (2\cdot x)&,x \in [0, 0.5] \newline
\newline
0.5\cdot[2-(2\cdot(1-x))]&,x\in(0.5, 1]
\end{cases}\newline
\newline
&=
\begin{cases}
x&,x \in [0, 0.5] \newline
\newline
x&,x\in(0.5, 1]
\end{cases}\newline
\newline
&=x,\quad x\in[0, 1]
\end{align}
可见,三种函数都是一样的。这个结果是很容易理解的:既然是直线,那么就没有缓入或缓出的过程,即整个过程都是同样的速率,其导数必然为常数。在这里这个求导的常数就是 1,对应的结果就是
f(x) = g(x) = h(x) = x,\quad x\in[0, 1]
缓动曲线公式和曲线图
Linear
基础函数
f_b(x) = x, \quad x\in[0, 1]
公式
y(x) = x, \quad x\in[0, 1]
曲线图
Sinusoidal
基础函数
f_b(x) = 1-\cos(\frac{\pi}{2}\cdot x), \quad x\in[0, 1]
easeInSine
公式
y(x) = 1-\cos(\frac{\pi}{2}\cdot x), \quad x\in[0, 1]
曲线图
easeOutSine
公式
y(x) = \sin(\frac{\pi}{2}\cdot x), \quad x\in[0, 1]
曲线图
easeInOutSine
公式
y(x) =
\begin{cases}
0.5[1-\cos(\pi\cdot x)]&,x \in [0, 0.5] \newline
\newline
0.5{\sin[\pi\cdot(x-0.5)]+1}&,x\in(0.5, 1]
\end{cases}
曲线图
Quadratic
基础函数
f_b(x) = x^2,\quad x\in[0, 1]
easeInQuad
公式
y(x) = x^2,\quad x\in[0, 1]
曲线图
easeOutQuad
公式
y(x) = 1-(1-x)^2,\quad x\in[0, 1]
曲线图
easeInOutQuad
公式
y(x) =
\begin{cases}
2x^2&,x \in [0, 0.5] \newline
\newline
1-2(1-x)^2&,x\in(0.5, 1]
\end{cases}
曲线图
Cubic
基础函数
f_b(x) = x^3,\quad x\in[0, 1]
easeInCubic
公式
y(x) = x^3,\quad x\in[0, 1]
曲线图
easeOutCubic
公式
y(x) = 1-(1-x)^3,\quad x\in[0, 1]
曲线图
easeInOutCubic
公式
y(x) =
\begin{cases}
4x^3&,x \in [0, 0.5] \newline
\newline
1-4(1-x)^3&,x\in(0.5, 1]
\end{cases}
曲线图
Quartic
基础函数
f_b(x) = x^4,\quad x\in[0, 1]
easeInQuart
公式
y(x) = x^4,\quad x\in[0, 1]
曲线图
easeOutQuart
公式
y(x) = 1-(1-x)^4,\quad x\in[0, 1]
曲线图
easeInOutQuart
公式
y(x) =
\begin{cases}
8x^4&,x \in [0, 0.5] \newline
\newline
1-8(1-x)^4&,x\in(0.5, 1]
\end{cases}
曲线图
Quintic
基础函数
f_b(x) = x^5,\quad x\in[0, 1]
easeInQuint
公式
y(x) = x^5,\quad x\in[0, 1]
曲线图
easeOutQuint
公式
y(x) = 1-(1-x)^5,\quad x\in[0, 1]
曲线图
easeInOutQuint
公式
y(x) =
\begin{cases}
16x^5&,x \in [0, 0.5] \newline
\newline
1-16(1-x)^5&,x\in(0.5, 1]
\end{cases}
曲线图
Exponential
基础函数
f_b(x) = 2^{10\cdot(x – 1)},\quad x\in[0, 1]
easeInExpo
公式
y(x) = 2^{10\cdot(x – 1)},\quad x\in[0, 1]
曲线图
easeOutExpo
公式
y(x) = 1 – 2^{-10x},\quad x\in[0, 1]
曲线图
easeInOutExpo
公式
y(x) =
\begin{cases}
2^{10\cdot(2 x – 1) – 1}&,x \in [0, 0.5] \newline
\newline
1 – 2^{-10\cdot(2 x – 1) – 1}&,x\in(0.5, 1]
\end{cases}
曲线图
Circular
基础函数
f_b(x) = 1 – \sqrt{1 – x^2},\quad x\in[0, 1]
easeInCirc
公式
y(x) = 1 – \sqrt{1 – x^2},\quad x\in[0, 1]
曲线图
easeOutCirc
公式
y(x) = \sqrt{1 – (1 – x)^2},\quad x\in[0, 1]
曲线图
easeInOutCirc
公式
y(x) =
\begin{cases}
0.5\cdot(1 – \sqrt{1 – 4 x^2})&,x \in [0, 0.5] \newline
\newline
0.5\cdot(1 + \sqrt{1 – 4\cdot(1 – x)^2})&,x\in(0.5, 1]
\end{cases}
曲线图
接下来的Back和Elastic缓动曲线,需要更多的参数辅助控制曲线的形态。
Back
基础函数
f_b(x) = x^2\cdot[(1 + n)\cdot x – n],\quad x\in[0, 1]
easeInBack
公式
y(x) = x^2\cdot[(1 + n)\cdot x – n],\quad x\in[0, 1]
easeOutBack
公式
y(x) = 1 – (1 – x)^2\cdot[(1 + n)\cdot(1 – x) – n],\quad x\in[0, 1]
曲线图
easeInOutBack
公式
y(x) =
\begin{cases}
2x^2\cdot[(1 + n)\cdot 2 x – n]&,x \in [0, 0.5] \newline
\newline
1 – 2 (1 – x)^2\cdot[(1 + n)\cdot 2 (1 – x) – n]&,x\in(0.5, 1]
\end{cases}
曲线图
Elastic
基础函数
f_b(x) = 2^{10(x – 1)}\cdot\sin{[(2 n + \frac{1}{2})\pi\cdot x]},\quad x\in[0, 1]
easeInElastic
公式
y(x) = 2^{10(x – 1)}\cdot\sin{[(2 n + \frac{1}{2})\pi\cdot x]},\quad x\in[0, 1]
曲线图
easeOutElastic
公式
y(x) = 1 – 2^{10 (-x)}\cdot\sin{[(2 n + \frac{1}{2})\pi\cdot (1 – x)]},\quad x\in[0, 1]
曲线图
easeInOutElastic
公式
y(x) =
\begin{cases}
0.5\cdot 2^{10\cdot(2 x – 1)}\cdot\sin{[(2 n + \frac{1}{2})\pi\cdot(2x)]}&,x \in [0, 0.5] \newline
\newline
0.5\cdot{2-2^{10\cdot[2 (1 – x) – 1]}\cdot\sin{[(2 n + \frac{1}{2})\pi\cdot 2(1 – x)]}}&,x\in(0.5, 1]
\end{cases}
曲线图
Bounce
要处理 Bounce 曲线,我们需要更多的技巧。首先 Bounce 曲线处处连续,但并非处处可导,不可导的点处在 Bounce 的各个转折点。要构造这样的一个函数,我们就需要一定的技巧。在前面的例子中,基础函数都可以用一个直观的函数表达式来表示。在 Back 和 Elastic 曲线中,我们需要引入更多的参数来控制曲线的形态。而在 Bounce 曲线中,我们也需要两个参数来控制曲线的形态。所不同的是,这两个参数是需要根据自变量的值动态调整的。因此我们需要一个广义的函数。可以借助 C 语言来定义这个基础函数
float bounce(float progress)
{
for(float a=0, b=1; 1; a+=b,b/=2.0f) {
if (progress >= (7 - 4.0f * a) / 11.0f) {
return -powf((11-6*a-11*progress)/4, 2) + powf(b, 2);
}
}
}
进一步,我们需要定义 easeIn, easeOut 和 easeInOut 函数。这三个函数通过接收基础函数和给定的自变量值来获取函数值。我们可以通过函数指针来实现
typedef float (*curve)(float);
有了函数指针,我们的缓动函数就可以声明如下:
float easeIn(curve cv, float x);
float easeOut(curve cv, float x);
float easeInOut(curve cv, float x);
使用的时候,把 bounce 函数传入对应的缓动函数中即可。
easeInBounce
函数:
float easeIn(curve cv, float x)
{
return cv(x);
}
曲线图:
easeOutBounce
函数:
float easeOut(curve cv, float x)
{
return 1-cv(1-x);
}
easeInOutBounce
函数:
float easeInOut(curve cv, float x)
{
if (x <= 0.5f)
return 0.5f * cv(2.0f * x);
else
return (2.0f - cv(2.0f * (1.0f - x))) / 2.0f;
}
曲线图: