关于前端的Web Animation API,你应该了解的

424 阅读11分钟

一、引言

最近在优化一个项目的动画性能的时候,偶然发现了这个其实已经出现了好几年的东西,它的优点有很多,比如性能基本接近css3动画,缺点则是相对于gsap库,缺少时间线这样的队列动画,以及对低版本设备的兼容性问题,好在老大觉得没问题,可以大胆用。不过在使用过程中,踩了不少坑,所以在这里发篇文章,警醒后人。

二、animation的使用

Web Animation这个API总的来说,使用上很接近原先的css3中的animation相关参数,大体上的调用方式如下:

    Element.animate(keyframes, options);

从这里开始,我们去写一个小球弹跳的动画,一步一步完善它的表现,来对比css3和AnimationAPI的一些异同。

我们去画两个小球,分别用css3和Animation API去实现让小球下落并回弹的动画: 用css3来写,很简单:

@keyframes Jump {
    0% {
        transform: translate(0, 0);
        background-color: blue;
    }
    100% {
        transform: translate(0, calc(50vh - 25px));
        background-color: lightblue;
    }
}
#ball2 {
    animation: Jump 0.75s forwards infinite alternate;
}

以上代码实现了一个id为ball2的元素,动画为垂直下落再回弹,并且小球的颜色在从蓝变成浅蓝再变回来。接下来,我们把他改写成Animation API的形式。

animation这个函数有两个入参,分别是keyframes 和 options,前者类似于css3的@keyframs,后者则是类似于css3的animation相关属性参数,改写以后的js代码如下:

const element = document.createElement("div")
element.id = "ball1"
const animation = element.animate(
    [
        {
            transform: 'translate(0,0)',
            backgroundColor: 'red',
        },
        {
            transform: 'translate(0,calc(50vh - 25px))',
            backgroundColor: 'pink',
        },
    ],
    {
        fill: "forwards",
        duration: 750,
        direction: "alternate",
        iterations: Infinity,
    }
);
document.getElementById("app").append(element);

下面开始详细的解释每个参数的具体用法。

1. Element

简单说就是一个Dom对象,你想要添加动画的元素,可以是通过document.getElementById获取到的具体的某个元素,也可以是通过类似document.querySelector(".ball")获取到的多个元素。而在Vue中,也可以是ref定义的元素。

2. keyframes

keyframs,含义为关键帧,即代表元素在设定的某个时刻的状态,一般情况下是个数组。数组中对象的参数则是直接使用Element.style中的驼峰定义的属性名,比如css的background-color在此处则应当使用backgroundColor

    const keyframes = [ 
        { opacity: 1 },
        { opacity: 0 } 
    ];

这就实现了一个最简单的从透明度1到透明度0的渐隐动画。在数组只有两个对象的情况下,相当于css中@keyframs中的from-to

而当数组的数量大于2个的时候,默认的关键帧节点会被均分,比如

const keyframes = [ 
        { opacity: 1 },
        { opacity: 0.2 },
        { opacity: 0 } 
    ];

等价于

    @keyframes fadeOut {
        0%{
            opacity:1;
        }
        50%{
            opacity:0.2;
        }
        100%{
            opacity:0;
        }
    }

一些简单的动画可能这样就足够了,但是在实际中,我们可能需要非等分时间轴的动画,比如css中的10% 15% 85% 100%这样的节点,这种情况下,我们就需要用到offset属性,它的取值区间为0~1,对应了css3的关键帧的百分比。

例如:

const keyframes = [ 
        { opacity: 1 , offset:0},
        { opacity: 0.2 , offset:0.3},
        { opacity: 0 , offset:1} 
    ];

等价于

    @keyframes fadeOut {
        0%{
            opacity:1;
        }
        30%{
            opacity:0.2;
        }
        100%{
            opacity:0;
        }
    }

除了数组的形式外,关键帧也可以以对象的形式进行赋值,当动画执行的阶段比较多,或者有重复的值的时候,这种方式就书写起来十分简便了,在没有指定offset的情况下,各个属性会按照值的数量均分整个播放过程。

如果指定offset,属性的值会和offset数组内的值相互对应。而属性数组的值比offset数组值多的情况下,真实offset则会自动均分填充补足缺省的部分。

const keyframes = {
    opacity:[0,0.4,1],
    backgroundColor: ["red", "yellow", "green"],
    offset:[0,0.4] //0,0.4,1的缺省写法
}

除了offset以外,还有一个固有属性easing用于控制到下一个关键帧之间的动画运动曲线,其属性和css的animation-timing-function用法相同。

3.options

options也有两种写法,一种是number类型,属于持续时间的简写形式,比如2000,单位是毫秒(ms),另一种,则是我们接下来要说的,也是主要使用的对象形式。 其中包含了如下属性

  • delay

类似于animation-delay属性,是延迟动画开始的毫秒数,默认为0。

  • direction

类似于animation-direction属性,是动画的播放方向,有:正向运行normal、反向运行reverse、先正向一个周期再反向一个周期alternate、先反向一个周期再正向一个周期alternate-reverse,默认是normal

  • duration

类似于animation-duration属性,是动画运行一个周期的所需要的毫秒数,默认为0,需要注意的是,设置为0的动画不会播放。

  • easing

是动画整体随时间的变化速率的表现,支持css属性使用的固定值:linear、ease、ease-in、ease-out、ease-in-out等,也支持cubic-bezier(0.42, 0.0, 1.0, 1.0)这样的贝塞尔曲线的形式。

需要注意的是,这个属性不同animation-timing-function属性。

  • endDelay

动画结束后延迟的毫秒数,一般用于多个动画播放周期的差值校正。

  • fill

类似于animation-fill-mode,决定了动画执行后元素的样式,有backwardsforwardsbothnone

  • iterationStart

描述动画应在不同动画周期中的哪个时间点开始。例如,0.5 表示在第一次动画的中途开始,设置此值后,具有 2 个周期的动画将在第三个周期的中途结束。默认值为 0.0。一般用于处理一些direction设置了alternate的属性后,想要动画开始于比如返回的途中等场景。

  • iterations

动画应重复的次数。默认值为 1,还可以取值Infinity,使动画在元素存在时重复播放。

  • composite

确定如何在此动画和其他单独的动画之间组合。默认为replace

add:动画将被追加到原有的属性上。比如对一个原先属性为

transform:translateX(50px) rotate(45deg);的元素,组合上transform:translateX(100px);,则动画执行时会变成transform:translateX(50px) rotate(45deg) translateX(100px);

accumulate:动画将被合并到原有属性上。比如对一个原先属性为transform:translateX(50px) rotate(45deg);的元素,组合上transform:translateX(100px);,则动画执行时会变成transform:translateX(150px) rotate(45deg);

replace:替换原有动画内容,很容易理解。

  • iterationComposite

取值和composite属性一致,确定每个动画周期后,属性的值如何组合,比如可以让一个元素越来越低。

  • pseudoElement

确定伪元素,可以填入类似::after之类的伪元素字符串,如果填入,动画则不会作用于元素本身,而是作用在伪元素上。

4.keyframes中的easing属性

了解了这些基本属性以后,我们可以开始完善我们的小球了。为了让我们的小球更接近真实视觉效果,我们这边准备给小球添加一个垂直方向的scale变形以及下落逐渐加速,回弹逐渐减速的效果。

用css来做的话,我们这时候就会用animation-timing-function去设置一个ease-in曲线,因为小球的回弹是反向的动画,所以正好实现了正周期加速,逆周期减速的效果。 但是用js的方式,应该怎么写呢?

其实很简单,我们只需要在关键帧中添加一个easing属性,取值和animation-timing-function相同,这个属性可以设置向下一个关键帧的运动速度曲线,由于每个关键帧都可以单独设置曲线,这一点其实比css只能设置统一的动画速度曲线自由的多。

我们设置上属性后,去做一个css做一个对比,可以看到,效果是完全一致的。 实现效果如下:

三、Animation的方法

Element.animate()方法会返回Animation的实例对象,而这个对象中有一些默认的事件回调方法,也提供了一些基本的方法调用。以下是一些常用函数的简介。

1.finish事件

当一个动画执行结束后,会触发finish事件,在此事件中,你可以开启另一个动画,或者清空播放结束的元素等等。此事件的回调,解决了css动画的没有回调,需要手动区分且监听dom元素的动画执行事件,才能获取播放结束的时点的问题。事件的调用是类似一个Promise的结构,范例如下:

    const animation = Element.animate(
       {
           opacity:[0,1]
       },
       {
           fill:'forwards',
           duration:1000,
       }
   ),
   animation.onfinish = (event) =>{
       console.log(event);
       Element.remove();
   }

2.cancel事件和remove事件

这两个事件放在一起说,是因为比较类似,都是在Animation对象被销毁时触发的,区别在于,前者是通过方法调用手动清除触发,后者则是当其寄生的Dom对象销毁时,被一起关联销毁时触发。使用方式与finish事件相同,因此不举例了,事件名分别是oncancelonremove

3.play(),pause()

Animation对象同样有一些可以主动使用的方法。 这个方法基本等同于animation-play-stateruningpause,用于播放处于暂停状态的动画。 例如:

    const animation = Element.animate(
        {
            opacity:[0,1]
        },
        {
            fill:'forwards',
            duration:1000,
        }
    )
    animation.pause();
    setTimeout(()=>{
        animation.play()
    },1000)

4.cancel()

由于Animation对象即使只播放一次动画,也不会在执行结束后自己销毁,而当我们对一个元素多次调用animate方法后,会发现元素挂在的Animation对象越来越多,而cancel方法会删除这个Animation对象,对于播放结束的非重复使用动画,应当尽早进行清理,以免造成内存泄漏。 获取某个元素挂载的对象可以用getAnimations()这个方法,获取到的是一个Animation对象的数组,对其遍历,进行cancel即可,当然移除这个Dom元素也是可以关联消除Animation对象的。

4.finish()

结束动画的播放,本质上等于将动画的时间轴重置为0。

四、特殊注意项

1.options.easing和keyframes中easing的差异

在上面的过程中,我们主要用到了关键帧keyframes中的easing属性,但是文章中还有一个easing属性是设置在options中的,这两者有什么区别么?

这两者的区别其实很大,我绘制了一个动画,让一个点从0移动至200,再返回0,将其轨迹和时间关联绘制了一个曲线来表明区别。

这是options.easing设置成linear的线性移动的情况:

image.png

这是在options中设置easeing:'ease-in'的情况:

image.png

这是则是我在keyframes中easeing:'ease-in'的情况:

image.png

最后则是options中设置easeing:'ease-in'且keyframes中easeing:'ease-in'的情况:

image.png

合成的方法有点复杂,暂时没法给出完整的推论,但是从以上曲线之间对比,我们可以得出几个结论:

  1. keyframs中的ease属性改变了关键帧offset的排布。
  2. options中的ease会进一步改变keyframesease所设置好的时间曲线。

因此在使用options中的ease属性时,请务必慎重,随意使用会出现一些预料外的效果,当然,我们也可以借此去实现一些原本css3中的时间函数不好实现的效果,比如,下面的小球运动效果,就是用了options中的ease-in-out去实现的,虽然效果是一致的,但是不需要设置反转之类的属性,更符合弹跳是一个运动周期的直觉。

作为题外话,这里顺便绘制了设置offset的情况下,options的ease会怎么作用于关键帧排布的曲线:

这个是options.ease = “ease-in-out”

image.png

这个则是,在options.ease = “ease-in-out”基础上对offset设置了0,0.3,1的情况。

image.png

2.Animation的重复使用

Element.animate()方法是可以通过多次调用,对同一个元素叠加动画的。其叠加的方式参考options中的composite属性,默认情况下冲突的属性是会覆盖(replace)原先动画的属性,不冲突的属性则继续播放。使用这个方式我们可以在比如click事件中给元素添加Animation,来让一个运行中的动画中途改变样式。

五、总结

本文简略的描述了下Web Animation API的一些常见用法和注意事项,如有错误,还望各位在评论中指出。Web Animation API最优秀的部分肯定还是调用方式的简单以及原生CSS的高性能表现,其适配版本分别是Chrome 75(2019-06-04)FireFox 48(2016-08-02)Safari 13.1(2020-03-24)到目前位置大概三年多的时间,兼容性方面应该算是比较成熟了,至于真的不支持的设备,可以通过类似if(!window.Animation){}的方式去验证浏览器是否支持此API,相对于其性能的优越性,个人觉得很多js动画库也确实该做出一些变革了。

最后,也希望此文能对更多苦于写css动画感到繁琐的前端同学有一些帮助。