什么是缓动函数?

61

在动画的上下文中,什么是缓动函数?似乎Dojo、jQuery、Silverlight、Flex和其他 UI 系统都有缓动函数的概念。我找不到一个好的缓动函数解释?有人能够解释一下缓动函数的概念或指出一个好的解释吗?我对概念感兴趣,而不是特定框架的具体细节。

缓动是否只用于位置,还是普遍适用于对象的任何属性?

5个回答

136
一个缓动函数通常是描述属性值在完成百分比时的函数。不同的框架使用略有不同的变化,但一旦你了解了这个概念,就很容易理解,但最好看几个例子。

首先让我们看看所有缓动函数都遵守的接口。

我们的缓动函数将采用几个参数:

  • percentComplete:(0.01.0)。
  • elapsedTime:动画运行的毫秒数
  • startValue:开始时的值(或完成百分比为0%时的值)
  • endValue:结束时的值(或完成百分比为100%时的值)
  • totalDuration:所需动画总长度(以毫秒为单位)

并且将返回一个数字,该数字表示应设置属性的值。

注意:这是jQuery用于其缓动函数的相同签名,我将借用其中的示例。

最容易理解的是线性缓动:

var linear = function(percent,elapsed,start,end,total) {
    return start+(end-start)*percent;
}

现在让我们来实际应用一下:

假设我们有一个动画,持续1000毫秒,应该从0开始到50结束。将这些值传入我们的缓动函数中,就可以告诉我们实际的值应该是多少:

linear(0, 0, 0,50, 1000)        // 0
linear(0.25, 250, 0, 50, 1000)  // 12.5
linear(0.5, 500, 0, 50, 1000)   // 25
linear(0.75, 750, 0, 50, 1000)  // 37.5
linear(1.0, 1000, 0, 50, 1000)  // 50

这是一个相当直接(没有双关语)的补间。它是一种简单的线性插值。如果您绘制值与时间的图形,它将是一条直线:

Linear ease

让我们来看一个稍微复杂一些的缓动函数,二次缓入:

var easeInQuad = function (x, t, b, c, d) {
    return c*(t/=d)*t + b;
}

让我们看看使用与之前相同的输入,得到的相同结果:

easeInQuad(0, 0, 0, 50, 1000)      // 0
easeInQuad(0.25, 250, 0, 50, 1000) // 3.125
easeInQuad(0.5, 500, 0, 50, 1000)  // 12.5
easeInQuad(0.75, 750, 0, 50, 1000) // 28.125
easeInQuad(1, 1000, 0, 50, 1000)   // 50

请注意,这些值与我们的线性缓动非常不同。它开始非常缓慢,然后加速到结束点。在动画完成50%时,它仅达到了12.5的值,这只是我们指定的startend值之间实际距离的四分之一。

如果我们将此函数绘制成图形,则会像这样:

Quad-Ease-In

现在让我们来看一个基本的缓出效果:
var easeOutQuad = function (x, t, b, c, d) {
    return -c *(t/=d)*(t-2) + b;
};

这实际上是一个相反的加速曲线,与缓入效果相反。它开始得很快,然后减速到结束值:

Ease out

还有一些函数可以使进入和退出更加容易:

var easeInOutQuad = function (x, t, b, c, d) {
    if ((t/=d/2) < 1) return c/2*t*t + b;
    return -c/2 * ((--t)*(t-2) - 1) + b;
};

EaseInOut

这个函数会从慢到快,然后再从快到慢,到中间时达到最大速度。

有许多缓动/插值函数可供使用:线性、二次方、三次方、四次方、五次方、正弦。还有一些特殊的缓动函数,如弹跳和弹性,它们有自己的特点。

例如,弹性缓动:

var easeInElastic = function (x, t, b, c, d) {
    var s=1.70158;var p=0;var a=c;
    if (t==0) return b;  if ((t/=d)==1) return b+c;  if (!p) p=d*.3;
    if (a < Math.abs(c)) { a=c; var s=p/4; }
    else var s = p/(2*Math.PI) * Math.asin (c/a);
    return -(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b;
},

Elastic ease in

也许有其他人能够解释插值背后的实际数学部分,因为老实说我不是一个数学高手。但这就是缓动函数本身的基本原理。
当您启动补间/动画时,动画引擎会记住您想要的起始和结束值。然后每次更新时,它都会计算经过了多长时间。它使用提供的缓动函数调用值来确定应将属性设置为的值。只要所有缓动函数都实现相同的签名,它们就可以轻松交换,核心动画引擎就不必知道差异(这使得关注点分离变得非常出色)。
您会注意到,我已经避免明确谈论x和y位置,因为缓动与位置没有任何特定关系。缓动函数只是定义了开始和结束值之间的转换。它们可以是x坐标,颜色或对象的透明度。
实际上,在理论上,您可以应用不同的缓动函数来插值不同的属性。希望这有助于阐明基本思想。
这里还有一个非常酷炫的示例(使用稍微不同的签名,但是原理相同),可以玩一下,了解缓动如何与位置相关。

编辑

这里是一个小的jsFiddle,我组合在一起来演示javascript中的一些基本用法。请注意,top属性使用弹跳进行缓动,left属性使用quad进行缓动。使用滑块模拟渲染循环。

由于easing对象中的所有函数具有相同的签名,因此您可以将它们中的任何一个替换为另一个。目前,大多数这些内容都是硬编码的(例如开始和结束值、使用的缓动函数以及动画的长度),但在动画助手的实际示例中,您需要传递以下属性:

  • 要更改的属性
  • 起始值(或如果左侧为undefined,则使用其当前值)
  • 结束值
  • 动画的长度
  • 您想要使用的缓动函数的引用。

动画引擎将在整个动画的持续时间内跟踪这些设置,并在每个更新周期期间使用缓动参数来计算属性的新值。


谢谢,这是一个很好的解释。您能否用一些伪代码详细说明动画引擎和缓动函数之间的接口,以及动画引擎如何使用多个缓动函数来控制多个属性? - ams
1
非常好的回答!因为到处都缺乏文档而感到困惑,但你真正澄清了一切。干杯! - SMBiggs

11

缓动(easing)函数是一种用于控制动画速度以达到期望效果(弹跳、放大慢放等)的算法。

如果需要更详细的了解,请查看 MSDN 的相关介绍


它控制动画函数运行的频率或正在进行动画处理的值,例如对象的x坐标。 - ams
@ams - 重点不在于函数运行的频率或被动画的值,而在于计算被动画变量值的函数本身。 - Justin Niessner
这与函数运行频率无关。就像Justin所说,缓动函数会随着时间计算属性的值。看一下我链接的图表,你会看到随着时间推移,值的变化情况。我们使用类似的方法来通过三次方程实现在3D空间中平滑移动。 - Chris Lacasse

10
我想即使这个问题已经有了一个被接受的答案,也希望能够发布我的答案。 32bitkid 已经做出了必要的解释。我将添加基本的实际实现,因为我找不到一个(我也发了一个问题)。
以这个简单的线性动画为例。我怀疑它不需要任何解释,因为代码是自说明的。我们计算一个恒定的增量值,它在时间上不会改变,并且在每次迭代时,我们增加盒子的位置。我们直接修改位置变量,然后将其应用于盒子。 JSFiddle

var box = document.getElementById("box");

var fps           = 60;
var duration      = 2;                                   // seconds
var iterations    = fps * duration;                      // 120 frames
var startPosition = 0;                                   // left end of the screen
var endPosition   = window.innerWidth - box.clientWidth; // right end of the screen
var distance      = endPosition - startPosition;         // total distance
var posIncrement  = distance / iterations;               // change per frame
var position      = startPosition;                       // current position

function move() {
  position += posIncrement;              // increase position
  if (position >= endPosition) {         // check if reached endPosition
    clearInterval(handler);              // if so, stop interval
    box.style.left = endPosition + "px"; // jump to endPosition
    return;                              // exit function
  }
  box.style.left = position + "px";      // move to the new position
}

var handler = setInterval(move, 1000/fps); // run move() every 16~ millisecond
body {
    background: gainsboro;
}
#box {
    width: 100px;
    height: 100px;
    background: white;
    box-shadow: 1px 1px 1px rgba(0,0,0,.2);
    position: absolute;
    left: 0;
}
<div id="box"></div>


现在,让我们添加缓动效果。我们从简单的使用linear(无缓动)开始。它将得到与上面相同的动画,但方法不同。这次,我们不会直接修改位置变量,而是会修改时间。
function linear(time, begin, change, duration) {
    return change * (time / duration) + begin;
}

首先,让我们谈谈参数。

  • time:已经过时间
  • begin:属性(宽度、左边距、外边距、不透明度等)的初始值
  • change:位移,(结束值 - 开始值)
  • duration:动画将花费的总时间

timeduration 直接相关。如果您有一个持续 2 秒的动画,您增加 time 并将其传递给缓动函数 linear。该函数将返回一个位置,表示在给定时间盒子应该处于该位置。

假设我正在将一个盒子从 0 移动到 100,在 2 秒内。如果我想获取盒子的位置,比如在第 700 毫秒,我会以以下方式调用 linear 函数:

linear(0.7, 0, 100, 2);

这将返回35。在动画开始后的700毫秒,方框的位置将在35像素处。让我们看看它的实际效果。

JSFiddle

var box = document.getElementById("box");

var fps           = 60;
var duration      = 2;                                   // seconds
var iterations    = fps * duration;                      // 120 frames
var startPosition = 0;                                   // left end of the screen
var endPosition   = window.innerWidth - box.clientWidth; // right end of the screen
var distance      = endPosition - startPosition;         // total distance
var timeIncrement = duration / iterations;
var position      = 0;
var time          = 0;

function move() {
    time += timeIncrement;
    position = linear(time, startPosition, distance, duration);
    if (position >= endPosition) {
        clearInterval(handler);
        box.style.left = endPosition + "px";
        return;
    }
    box.style.left = position + "px";
}

var handler = setInterval(move, 1000/fps);

function linear(time, begin, change, duration) {
    return change * (time / duration) + begin;
}
body {
    background: gainsboro;
}
#box {
    width: 100px;
    height: 100px;
    background: white;
    box-shadow: 1px 1px 1px rgba(0,0,0,.2);
    position: absolute;
    left: 0;
}
<div id="box"></div>


这段代码需要注意的部分是:
var timeIncrement = duration / iterations;
var time = 0;

function move() {
    time += timeIncrement;
    position = linear(time, startPosition, distance, duration);
    // ...

在第一个动画中,我们直接修改了位置变量。我们需要一个恒定的位置增量值。我们计算的方法是posIncrement = distance / iterations。使用缓动时,我们不再修改位置变量,而是修改时间变量。因此,我们需要一个时间增量值。我们以与位置增量相同的方式计算它,只是这一次我们将duration除以iterations。我们使用时间增量增加时间,并将时间传递给缓动函数,缓动函数返回下一个方框应占据的位置。
total distance / iterations (frames) = position change per frame
total duration / iterations (frames) = time change per frame

这里有一些关于眼睛的图表。

Ease function graph


最后,一个 easeInOutQuad 的例子。

JSFiddle

var box = document.getElementById("box");

var fps           = 60;
var duration      = 2;                                   // seconds
var iterations    = fps * duration;                      // 120 frames
var startPosition = 0;                                   // left end of the screen
var endPosition   = window.innerWidth - box.clientWidth; // right end of the screen
var distance      = endPosition - startPosition;         // total distance
var timeIncrement = duration / iterations;
var time          = 0;
var position      = 0;

function move() {
  time += timeIncrement;
  position = easeInOutQuad(time, startPosition, distance, duration);
  if (position >= endPosition) {
    clearInterval(handler);
    box.style.left = endPosition + "px";
    return;
  }
  box.style.left = position + "px";
}

var handler = setInterval(move, 1000 / fps);

function easeInOutQuad(t, b, c, d) {
  if ((t /= d / 2) < 1) {
    return c / 2 * t * t + b;
  } else {
    return -c / 2 * ((--t) * (t - 2) - 1) + b;
  }
}
body {
    background: gainsboro;
}
#box {
    width: 100px;
    height: 100px;
    background: white;
    box-shadow: 1px 1px 1px rgba(0,0,0,.2);
    position: absolute;
    left: 0;
}
<div id="box"></div>


1

TL;DR: 在0-1的范围内,基本实现了缓动函数并支持强度调整:

t = 归一化输入值,因此取值范围为0-1。

s = 强度,其中1为线性,数值越高,缓动效果越强。

# ease-in
result = t**s 

# ease-out
s = int(s)*2 + 1
result = s*((t-1)**s + 1)

# ease-inOut
result = t**s /(t**s + (1-t)**s)

这里的 ** 是什么?抱歉问了个愚蠢的问题。 - Confused
1
@Confused 表示 t 的 s 次方,因此 t**s == pow(t, s) - Kaaf
太好了!我也是这么想的。做事情的方式简单、整洁、有条理。谢谢你! - Confused

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接