CSS Animation 与 Web Animation API 之争

1 年前

原文链接 CSS Animations vs Web Animations API | CSS-Tricks

JavaScript 提供了 Web Animations API 原生动画 API ,在本篇文章中称它为 WAAPI。MDN 上有一篇关于它的文章很推荐,此外 Dan Wilson 有一系列高质量的文章

本篇文章中,我们将对比 WAAPI 和 与之对应的 CSS 动画。

浏览器支持情况

WAAPI 有一个完善且强大的 polyfill,使得我们现在可以在生产环境下使用它,即便是在浏览器受限的情况下。

通常,你可以在 Can I Use 上查看到浏览器支持的情况。然而,它却没有完整的提供 WAAPI 一些子特性的信息。下面是一个完整的报告:

Dan Wilson 已经在 CodePen 提供了 WAAPI 浏览器支持测试供大家参考。

如果想要在不使用 polyfill 的情况下测试所有的特性,可以使用 Firefox Nightly。

WAAPI 核心

如果你曾经用过 jQuery 中的 .animate(),WAAPI 的基本语法与之类似。

var element = document.querySelector('.animate-me');
element.animate(keyframes, 1000);

animate 方法接收两个参数:keyframes 和 duration。和 jQuery 不同的是,它是浏览器原生支持的,在性能生也有很大的优势。

第一个参数,keyframe 通常是一个对象数组。每一个对象都是我们动画中的一帧。下面是一个简单的示例:

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

第二个参数,duration 表示我们想要这个动画持续多久。上面的动画中是 1000 毫秒。下面我们看一个更有趣的例子。

使用 WAAPI 重构 CSS 动画

这里是我从 酷炫的 Animista 上找到的一个叫做 slide-in-blurred-top 的出场动画,看起来就很舒服。

真实效果 Animista 要比这张 GIF 动画好很多。

下面是 CSS 中的 keyframes:

0% {
  transform: translateY(-1000px) scaleY(2.5) scaleX(.2);
  transform-origin: 50% 0;
  filter: blur(40px);
  opacity: 0;
}
100% {
  transform: translateY(0) scaleY(1) scaleX(1);
  transform-origin: 50% 50%;
  filter: blur(0);
  opacity: 1;
}

下面是与之对应的 WAAPI 代码:

var keyframes = [
  { 
    transform: 'translateY(-1000px) scaleY(2.5) scaleX(.2)', 
    transformOrigin: '50% 0',
    filter: 'blur(40px)',
    opacity: 0 
  },
  { 
    transform: 'translateY(0) scaleY(1) scaleX(1)',
    transformOrigin: '50% 50%',
    filter: 'blur(0)',
    opacity: 1 
  }
];

我们已经看到将它应用到任何想要添加动画的元素上是多么的简单:

element.animate(keyframes, 700);

考虑到动画的简单,我仅仅为它指定了 duration。然而,我们还可以在第二个参数中传递更多的选项。至少,我们可以指定一个缓动方式。下面是一个完整的配置项列表:

var options = {
  iterations: Infinity,
  iterationStart: 0,
  delay: 0,
  endDelay: 0,
  direction: 'alternate',
  duration: 700,
  fill: 'forwards',
  easing: 'ease-out',
}
element.animate(keyframes, options);

借助这些配置项,我们完成了一个开始没有延迟并且循环出现的动画。

Dan Wilson 已经在 CodePen 提供了 motion blur waapi circle 供大家参考。

令人恼火的是,有一些专业的术语与我们熟悉的 CSS 变量有所不同。不过换个角度来看,这些变化也让我们更方便拼写!

  • 与 animation-timing-function 对应的是 easing;
  • 与 animation-iteration-count 对应的是 iterations。如果你想要让动画一直重复下去,请使用 Infinity 代替 infinite。费解的是,Infinity不需要使用逗号包裹,它是 JavaScript 的一个关键字,而其他值是字符串。
  • 我们使用 ms 替代了 s,这对于我们写过 JavaScript 的开发者来说会更容易接受。(实际上在 CSS 动画中也可使用 ms,不过基本没人会这样用。)

下面我们仔细看一下其中的一个配置项:iterationStart。

第一次看到 iterationStart 时我有点懵。为什么在一个特定的循环上开始动画而不是通过减少动画循环的次数呢?这个配置项当你使用一个小数是会非常的有用。比如,你可以将它设置为 .5,那么动画将会在后一半才开始。它由两半构成一个完整的动画,所以如果你设置循环次数为 1,并且 iterationStart 被设置为 .5,动画将会在完整动画的中间开始直到结束,然后再开始动画,最后在完整动画的中间结束!

值得注意的是,我们也可以将 iterations 设置为一个小于 1 的值。比如:

var option = {
  iterations: .5,
  iterationStart: .5
}

这就会有一个从中间开始的动画。

endDelay: 如果你想要将多个动画的首位串接在一起,这个配置项就很重要了。下面是 Patrick Brosset 的一个视频,很好地解释了它。

Easing

Easing 在任何动画中都非常的重要。 WAAPI 为我们提供了两个不同的方式来设置它——在 keyframes 数组中或者在我们的配置项对象中。

在 CSS 中,如果你设置了 animation-timing-function: ease-in-out,你或许以为在动画的开始会渐入效果,然后在结尾是淡出效果。实际上,easing 是应用在 keyframes 的,而不是整个动画。这可以更加细力度地控制动画。 WAAPI 也提供了这个能力。

var keyframes = [
  { opacity: 0, easing: 'ease-in' }, 
  { opacity: 0.5, easing: 'ease-out' }, 
  { opacity: 1 }
]

值得注意的是,无论是 CSS 或者 WAAPI 中,你都不必要在最后一个 frame 里设置 easing 值,因为它不会有效果的。这可能是很多人都容易犯的一个错误。

通常,直接将 easing 添加到整个动画更加的只管。这在 CSS 中是无法完成的,但我们可以用 WAAPI 实现它。

var options = {
  duration: 1000,
  easing: 'ease-in-out',
}

你可以看下一这两种的 easing 方式 的不同:

Dan Wilson 已经在 CodePen 提供了 Same animation, different easing 供大家参考。

对比 Ease 和 Linear

CSS animation 和 WAAPI 还有一个不太重要的区别:CSS 默认效果是 ease,而 WAAPI 则是 linear。Ease 实际上就是 ease-in-out 的一个版本,并且在你想偷懒时最好的一个方式。对比之下,linear 就有点单调乏味的感觉—— 一成不变的速度让人看起来很机械和不自然。它一般作为一个很中立的选择。所以,在使用 WAAPI 时使用恰当的 easing 更加的重要,以免让动画变得机械和乏味。

性能

WAAPI 有着和 CSS 动画一样的性能表现,虽然这不意味着你可以毫无顾忌的使用动画。

我曾经幻想这个 API 的性能优化让我们不再使用 will-change 并且可以完整的 hack hack translateZ - 成为可能。然而,至少在现在的浏览器实现中,这些属性依旧是有用的,并且在处理闪烁的问题上很必要。

然而,至少在当动画有延迟时,你没必要去使用 will-change。web animation 规范的主要作者有一些很有趣的建议在 Animation for Work Slack community,希望他不会介意我在这里引用:

如果您有一个确定的延迟,您不需要使用 will-change。因为浏览器将在延迟开始时进行分层,当动画启动时,它将准备就绪。

WAAPI 与 CSS 动画之争?

WAAPI 为开发者提供的 JavaScript 语法完全可以使用 CSS 来完成。然而,它们并不是竞争对手的关系。如果你依旧热衷于 css 动画,也可以试试将 css 与 WAAPI 组合起来,或许会有不一样的火花。

Animation 对象

animate 方法不仅仅为元素提供动画,它还有自己的返回值。

var myAnimation = element.animate(keyframes, options);

打印出来的 Animation 对象。

仔细看一些打印出来的值,会发现它一个 animation 对象。它给我们提供了多种功能,很多都是有自释义的,比如 myAnimation.pause()。我们通过修改 animation-play-state 方法已经可以获得一个与 CSS 动画类似的效果,不过 WAAPI 的预发要比 element.style.animationPlayState = "paused" 写法优雅的多。我们还可以通过 myAnimation.reverse() 方法轻松地反转动画。当然,这也是 animation-direction CSS 属性在脚本中的另一种实现方式。

然而,至少现在来说,使用 JavaScript 操作 @keyframe 绝不是件称得上简单的事。尽管有些事就像重启一个动画那样有点诀窍,就像 Chris Coyier 曾写到的)。借助 WAAPI 的 myAnimation.play() 我们可以很简单的重新开始一个动画,或者从我们上次停止的地方开始新的动画。

我们甚至可以很容易地改变动画的速度。

myAnimation.playbackRate = 2; // speed it up
myAnimation.playbackRate = .4; // use a number less than one to slow it down

getAnimations()

这个方法会返回一个由 animation 对象组成的数组,只要调用它的元素上定义了 WAAPI 动画,甚至是 CSS transitions 或者 animations。

element.getAnimations() // returns any animations or transitions applied to our element using CSS or WAAPI

如果您感觉使用 CSS 来定义和应用动画比较顺手,也可以将getAnimations() API 与 @keyframes 结合使用。你可以继续使用 CSS 完成大部分动画,并在需要的时候借助 WAAPI。让我们看看有多简单。

尽管 DOM 元素仅仅有一个动画,getAnimations() 依然会返回一个数组。我们先取出单个的 animation 对象。

var h2 = document.querySelector("h2");
var myCSSAnimation = h2.getAnimations()[0];

现在我们可以在 CSS 动画中使用 WAAPI 了。

myCSSAnimation.playbackRate = 4;
myCSSAnimation.reverse();

Promise 和 Event

在 CSS 动画中已经有了各种各样的事件触发方式,我们在 JavaScript 中可以用 animationstart、animationend、animationiteration 以及 transitionend 事件来监听到。我通常会监听一个动画或者过渡的结束,然后在 DOM 中删除动画对应的元素。

在 WAAPI 中可以使用 animation 对象的 animationend 或者 transitionend 方法达到相同的目的:

myAnimation.onfinish = function() {
  element.remove();
}

WWAPI 支持 event 和 promise 两种方式。animation 对象的 finish 属性会返回一个 promise,在动画结束的时候调用 resolve。下面是一个使用 promise 的示例:

myAnimation.finished.then(() =>
  element.remove())

我们粗略地看一下 Mozilla 开发者社区中稍显复杂的示例。Promise.all 需要一个 promise 组成的数组作为参数,并且只会在所有的 promise 都成功后才去调用回调函数。正如你所看到的,element.getAnimations() 返回了 animation 对象数组。我们可以在借助 map 调用每个 animation 对象的 finished,得到我们所需要的 promise 数组。

在这个示例中,仅仅会在页面中所有动画都结束后去调用我们定义的函数。

Promise.all(document.getAnimations().map(animation => 
  animation.finished)).then(function() {           
    // do something cool 
  })

未来

文中提到的这些特性仅仅是个开始。现有的规范和实现看起来更像是一项伟大事业的起点。

0
推荐阅读