说一说基于JavaScript的动画


按理来说,使用JavaScript来制作动画其实已经是老生常谈的一件事情了。不过无奈本人还是个新手,并不是老生。曾想起大学时曾学过的各类专业课,例如Flash、After Effects等等,这些都是一些用于制作动画内容的软件。那在网页中使用JavaScript驱动的的动画到底如何实现呢? 简易Demo:es/js_animation_test.html Anikyu源代码仓库:anikyu

论网页与传统动画的联系

本人认为,网页与传统动画一样,都是能够令人产生愉悦的可以用于观赏的产品;但比传统动画更强大的是,网页能够与用户直接产生交互。

专业动画设计软件里,动画的实现一般是逐帧动画或是关键帧动画。

  • 逐帧动画

创作者每一帧绘制一次,最终将多帧连续播放,实现动画。

  • 关键帧动画

创作者在每个关键点绘制一帧,最终由程序在两个关键帧之间进行插值,得到中间帧。插值可以是线性插值,也可以是包含缓动的插值。

对于网页动画,在早期,若动画效果很复杂,甚是精美,一般都直接使用Flash等插件来制作;简单的动画则使用JavaScript中的setInterval()来实现,即每隔若干毫秒(保持性能的同时不让用户感到卡顿)对某一对象某一数值进行一次增加,请求一次动画帧,直到该对象该值的量到达预想值,由此实现动画。jQuery中的animate()对其进行了封装。同时还有SVG动画,使用animate标签来进行实现。随着CSS3的出现,DOM元素动画/过渡效果可以使用CSS实现。

当然,CSS动画的使用也有一些局限性:

  1. 不能够灵活控制动画播放进度
  2. 仅可以支持DOM元素动画

最近在学习Three.js,其核心库似乎不包含动画。其官方示例有使用Tween.js这个动画库,但由于我比较想试着自己实现一个类似的动画库(其实是因为它的用法和我的想法不太相符)。

因此,我自己封装了一个基于JavaScript实现的动画库 —— Anikyu 。

当然本文先并不着重介绍我所封装的库。对于其使用方法,请直接看anikyu的仓库。

网页的维度

我们都知道,网页有两个维度:宽度和高度。 如果要用到动画,那就再加个时间。 (好吧,如果非要说WebGL或是CSS 3D等等三维技术,那再加个深度;此处不讨论)。

要让一个对象会动,就必须引入时间这个概念;因为时间点是一个点,其中包含的是对象在在当前时间点的状态;既然要让对象会动,就得找下一个时间点,最终连点成线,就产生了一个时间轴,这样就有了时间这个维度。

如何在两个时间点之间进行基本的补间?

现在我们把两个时间点之间的变化过程看成一个整体,开始时间点动画进度为0,结束时间点动画进度为1。

计算过程

设开始时间为64,结束时间为1024,当前时间为256,那么:

总时间差 = 结束时间 – 开始时间 = 1024 – 64 = 960

当前动画进度 = (当前时间 – 开始时间)/总时间差 = (256 – 64)/960 = 0.2 ;

设开始值为222,结束值为666,那么:

当前时间点(相对于开始值)的增量 = (结束值 – 开始值)当前动画进度 = (666 – 222)0.2 = 88.8

因此在该时间点下:

当前对象的值 = 开始值 + 当前时间的增量 = 222 + 88.8 = 310.8

之后使用定时器执行上方的步骤,每次时间改变,重新计算一次进度和时间点增量,并赋值给目标对象,直到当前动画进度达到1,此时动画完成。

加入缓动效果

在上面的代码中,我们已经实现了最简单的补间,即匀速补间。但在很多情况下,仅有匀速补间是不够的,例如我们可能要实现一个动画运行先快(慢)后慢(快)/中间快(慢)两头慢(快)/稍微出去一点再回来/弹跳等诸如此类的小效果。在After Effects、Cinema 4D等设计类软件时间轴面板上,我们可以对缓动函数进行可视化的编辑,从而我们可以直观地看到缓动函数的曲线。

image

与此类似,借助CSS3所提供的transition-timing-function属性,我们也可以直接在CSS中实现缓动动画,其属性值包括:linear、ease、ease-in、ease-out、ease-in-out,外加这五个属性值所基于的cubic-bezier(n,n,n,n) —— 三次贝塞尔曲线。

CSS3 所提供的缓动函数已经基本满足我们日常的一些动画需求,即上文所提及的先快(慢)后慢(快)/中间快(慢)两头慢(快)/稍微出去一点再回来;但对于弹跳效果,由于CSS3 缓动动画仅支持两点之间的简单缓动,而弹跳效果在动画过程中无法使用CSS3 所支持的三次贝塞尔曲线来表达(弹跳动画包含多个转折点),因此无法使用纯CSS来实现弹跳。

但在JavaScript中,我们可以自己编写缓动函数。

缓动函数参见 ECharts 示例页面

InOutInOut
quadraticInquadraticOutquadraticInOut
cubicIncubicOutcubicInOut
quarticInquarticOutquarticInOut
quinticInquinticOutquinticInOut
sinusoidalInsinusoidalOutsinusoidalInOut
exponentialInexponentialOutexponentialInOut
circularIncircularOutcircularInOut
elasticInelasticOutelasticInOut
backInbackOutbackInOut
bounceInbounceOutbounceInOut

函数曲线示例:

image

如果自行百度,可以发现这些补间函数基本都是大同小异的。它们都接受一个值 当前动画播放实际进度k,返回一个经过处理的进度值 k2 用以供计算函数算出当前时间点的变化量。

接上一步的计算过程

假设当前动画状态以及所给定值和上一步一样,缓动函数使用的是bounceIn,则:

当前时间点(相对于开始值)的增量 = (结束值 – 开始值) * 缓动函数(当前动画进度) = (666 – 222)*bounceOut(0.2) = 134.31

因此在该时间点下:

当前对象的值 = 开始值 + 当前时间的增量 = 222 + 134.31 = 356.31

加入定时器以实现动画

正如上文所提及,动画的存在依赖于时间这个维度。一个时间点表示一个时间点的状态,那如何得到动画播放期间每一个时间点的状态呢?

答案就是定时器。定时器可以让浏览器每间隔一段时间来执行一段函数。在早期浏览器(IE9或更低版本)中仅能够使用setInterval()来实现不断地请求动画,新版本浏览器中则引入了专门用于创建动画的requestAnimationFrame()。

requestAnimationFrame与setInterval区别在于:

  1. requestAnimationFrame无需手动指定更新间隔时间,其更新跟随屏幕的更新而更新(由屏幕刷新率来确定),这种更新机制类似CSS3 动画的更新;setInterval需要手动指定更新间隔时间,其更新由定时器来控制。这使得使用setInterval请求动画时可能会因来不及绘制而发生丢帧或过度绘制而产生性能问题,而requestAnimationFrame能够更加精准控制动画的绘制。
  2. 在页面不可见时,requestAnimationFrame会暂停执行,而setInterval只要不被清除将会一直执行。这导致setInterval的运行将十分耗电。

Anikyu包含了requestAnimationFrame的 Polyfill,以用于支持IE9浏览器。

通过定时器不断执行下列操作:

获取到当前时间 – 将当前时间和开始时间进行一系列计算得到当前时间点状态 – 将状态赋值给原始对象

即可实现动画。

代码

先定义两个函数

function clamp(value, min, max) {
    return Math.max(min, Math.min(max, value));
}

clamp函数用于钳制数值范围。由于当前动画进度的值必定是一个[0,1]区间的值,因此我们必须让动画进度限制在该范围内,否则可能会导致最终效果值小于/大于预期值

function bounceOut(k) {
    if (k < (1 / 2.75)) { return 7.5625 * k * k; }
    else if (k < (2 / 2.75)) { return 7.5625 * (k -= (1.5 / 2.75)) * k + 0.75; }
    else if (k < (2.5 / 2.75)) { return 7.5625 * (k -= (2.25 / 2.75)) * k + 0.9375; }
    else { return 7.5625 * (k -= (2.625 / 2.75)) * k + 0.984375; }
}

bounceOut函数是上文所提及的30个缓动函数之一,可在动画进度快要结束时产生弹回的效果。将当前动画进度传入后,可以获得一个经过处理的进度,在实际计算时根据该进度得到当前时间点状态,实现变速运动。若当前动画进度不经过该函数处理,则动画为匀速运动。

获取动画对象,并定义初始值、结束值,获取动画起始时间

let el = document.getElementById('el')
​
let init = 222;
let end = 666;
let timeDelta = 960;
​
let startTime = Date.now()

编写动画函数

function animate() {
    interval = requestAnimationFrame(animate)
    let loop = () => {
        let currentProgress = (Date.now() - startTime) / timeDelta
​
        currentProgress = clamp(currentProgress, 0, 1)
​
        let sumNumber = (end - init) * bounceOut(currentProgress)
​
        // let sumNumber = (end - init) * currentProgress
​
        let currentStatus = init + sumNumber
​
        if (currentProgress === 1) {
            cancelAnimationFrame(interval)
            el.style.transform = `translate(${end}px)`
        } else {
            el.style.transform = `translate(${currentStatus}px)`
        }
    }
    loop()
}
animate()

以上就是我对JavaScript动画的理解。借此机会,我也封装了一个上文所提及的动画库,Anikyu。

Anikyu是本人春节期间在家,正好也是疫情期间,做这个动画demo时突然想封装的一个库。正巧借此机会也学习了一下ES6没用过的特性、Webpack、Babel,同时也将该库发布到了 npm

各位要是觉得靠谱不妨来用一下,支持IE 9+浏览器,支持Node.js环境。

我也尝试将Anikyu代码结构、开发过程写一篇新的博客:#7

可能有点多,我想起来的时候慢慢补充。


评论