什么是缓动动画

自然界中的物体从起点移动到终点时,速度从来不是一成不变的。汽车启动时速度会由慢变快,停止时则由快变慢;篮球落地时会在地上来回反弹,并逐渐停止运动。大家都期待事物的呈现遵循一定的运动规律,所以,在网页中适当的使用动画能让用户得到更舒适的体验。

要制作出更加自然的动画,就需要理解什么是缓动函数。简单来说,缓动函数用于控制动画从初始值运动到最终值的速率。业界已经整理出了一些常用的缓动函数曲线,本文将给大家介绍如何在 CSS 与 JavaScript 里使用这些缓动函数。

常用缓动函数曲线

缓动动画曲线

在css中使用缓动动画

CSS 提供了四种基础的缓动函数:

  • linear 表示线性动画,动画从开始到结束一直是同样的速度,看起来不是很自然。 ease-in
  • 表示缓入动画,动画的速度先慢后快,就好像汽车启动时一样。缓入动画会在速度最快时停止,这会让动画结束得很突然,因为自然界中的运动总是慢慢减速后才停止的。
  • ease-out 表示缓出动画,与缓入动画正好相反,缓出动画的速度先快后慢,就好像汽车慢慢停止一样。
  • ease-in-out 表示缓入缓出动画,它的速度由慢变快,最后再变慢,就好像汽车启动、加速、然后停下来一样。

总的来说,线性动画与缓入动画不太符合自然运动规律,缓入缓出动画更符合自然界的运动规律。

文章开头提到的常用缓动函数也是以这种规则命名的,所以很容易就能区分。虽然 CSS 不提供这些缓动函数,但可以通过贝塞尔曲线来定义这些缓动函数。例如,要让一个元素的高度用名为 easeInOutCubic 的缓动函数来变化,可以这样写:

div { transition: height 0.2s cubic-bezier(0.645, 0.045, 0.355, 1) }

cubic-bezier() 内定义的四个数字实际上是两个点的坐标,用这两个点就可以确定缓动函数的运动曲线了
在线制作贝塞尔曲线

在JavaScript中使用缓动函数

如果你仔细看过了前文提到的常用缓动函数的网站,你会发现有一些缓动函数没法在 CSS 中使用,只能用 jQuery 加上 jQuery Easing 插件实现,例如刚才提到的“弹簧”动画,在jQuery中的例子为:

$div.animate({ top: '-=100px' }, 600, 'easeOutElastic')

但是仅仅为了制作一个动画就在项目里引入两个依赖太不值得了,所以这里介绍一下原生JavaScript实现缓动函数。

查看 jQuery Easing 插件的源码就会发现,JavaScript 里的缓动函数是真的“函数”。
jQuery Easing 插件源码
其中 easeOutElastic 函数的定义如下:

const c4 = (2 * Math.PI) / 3
function easeOutElastic (x) {
  return x === 0 ? 0 : x === 1 ? 1 :
    Math.pow(2, -10 * x) * Math.sin((x * 10 - 0.75) * c4) + 1
}

看不懂?没关系,我也看不懂。一般情况下,我们不需要深入了解每个缓动函数是如何实现的,只需要知道它接收一个参数 x,这个参数代表当前动画的运动时间点,并返回这个时间点动画所处的位置。

简单解释一下这里所说的动画的「运动时间点」与「运动位置」。

我们可以将一个动画的开始到结束理解为动画从 0% 的时间点运动到了 100% 的时间点(即缓动函数曲线的 X 轴),所以动画开始时,x 的值就是 0,运行到一半时,x 的值就是 0.5,结束时,x 的值就是 1,依此类推。当动画的时间点从 0% 运行到 100% 的时候,动画的位置也同样从 0% 运动到了 100%(即缓动函数曲线的 Y 轴),这两个轴从 0 出发运动到 1 时所形成的点就组成了「缓动函数曲线」。

这样理解的话,线性动画的缓动函数曲线是一条直线也就不难理解了——它的运动时间对应的运动位置总是相同的。

现在,假设我们需要使用 easeOutElastic 函数在两秒钟内将一个 div 元素的高度从 100px 运动到 400px,我们可以这样写:

const div = document.getElementsByTagName('div')[0] // 要变化高度的 div
const startValue = 100                              // div 的初始高度
const endValue = 400                                // div 的最终高度
const changeValue = endValue - startValue           // div 变化了这么多高度
const during = 2000                                 // 动画持续 2 秒钟
// 为了让动画足够流畅,我们需要达到 60 帧/每秒的动画速率
const updateTime = 1000 / 60                        //计算帧间隔时间
const updateCount = during / updateTime             // 计算出两秒内我们需要更新动画的状态多少次
// 我们需要一个在下一帧更新动画状态的函数
const rAF = window.requestAnimationFrame || function(cb) {
  setTimeout(cb, updateTime)
}
const startPosition = 0                             // 动画的开始时间点是 0%
const endPosition = 1                               // 动画的结束时间点是 100%
// 因为我们要在动画从 0% 运动到 100% 时更新updateCount次动画,
// 所以要计算出每次更新动画时动画经过的时间
const perUpdateDistance = endPosition / updateCount
let position = startPosition                        // 记录动画的当前时间点

function step () {
  // 计算 div 在当前时间点的高度
  const height = startValue + changeValue * easeOutElastic(position)
  div.style.height = height + 'px'                   // 更新 div 的高度
  position += perUpdateDistance
  // 如果动画还没结束,则准备在下一帧更新动画
  if (position < endPosition) {
    rAF(step)
  } else {
    console.log('动画结束')
  }
}

step()                                               // 开始运行动画

理解了动画运行的过程之后,上面的代码很容易就能封装成一个可以重复使用的 animate 函数:

const updateTime = 1000 / 60  //设定动画为每秒60帧
const rAF = window.requestAnimationFrame || function(cb) {
  setTimeout(cb, updateTime)
}

/*
 * 简单的执行缓动函数的方法
 * @param {number} startValue - 初始值
 * @param {number} endValue - 最终值
 * @param {number} during - 持续时间
 * @param {function} easingFunc - 缓动函数
 * @param {function} stepCb - 每次更新动画状态后执行的函数
 * @return {Promise}
 */
function animate (startValue, endValue, during, easingFunc, stepCb) {
  const changeValue = endValue - startValue
  const updateCount = during / updateTime
  const perUpdateDistance = 1 / updateCount
  let position = 0

  return new Promise(resolve => {
    function step () {
      const state = startValue + changeValue * easingFunc(position)
      stepCb(state)
      position += perUpdateDistance
      if (position < 1) {
        rAF(step)
      } else {
        resolve()
      }
    }
    step()
  })
}

例如使用 animate 函数实现弹簧效果可以这样写:

const div = document.getElementsByTagName('div')[0]

animate(100, 400, 2000, easeOutElastic, height => {
  div.style.height = height + 'px'
}).then(() => console.log('动画结束'))

现在,你就可以代入 jQuery Easing 插件中所有的缓动函数并尝试不同的动画效果了。

附言

jQuery Easing 插件在 v1.3.2 及之前的版本实现的缓动函数有四个参数,但这种实现方式参杂了运动的过程所以无法细粒度的控制动画的进度,不建议使用。

文中实现的 animate 函数比较粗糙,并且一次只能变化一个属性。
参考资料:使用缓动函数制作更自然的动画

Last modification:October 28, 2019
If you think my article is useful to you, please feel free to appreciate