JavaScript 中有一个用于动画的原生 API,称为 Web 动画 API。 在本文中,我们将其称为 WAAPI。 MDN 对其有 良好的文档,Dan Wilson 有一篇 很棒的文章系列。
在本文中,我们将比较 WAAPI 和 CSS 中完成的动画。
关于浏览器支持的说明
WAAPI 有一个全面且健壮的 polyfill,使其在当今的生产环境中可用,即使浏览器支持有限。
和以往一样,您可以查看 Can I Use 以获取浏览器支持数据。 但是,它没有提供有关 WAAPI 所有子功能支持的非常好的信息。 这里有一个检查器可以做到这一点
查看 Dan Wilson 在 CodePen 上的 WAAPI 浏览器支持测试 笔记 (@danwilson)
要使用所有功能进行实验而无需使用 polyfill,请使用 Firefox Nightly。
WAAPI 的基础知识
如果您以前使用过 jQuery 的 .animate()
,则 WAAPI 的基本语法应该看起来非常熟悉。
var element = document.querySelector('.animate-me');
element.animate(keyframes, 1000);
animate
方法接受两个参数:关键帧和持续时间。 与 jQuery 相比,它不仅有内置于浏览器的优点,而且性能也更高。
第一个参数,关键帧,应该是对象数组。 每个对象都是我们动画中的一个关键帧。 这是一个简单的例子
var keyframes = [
{ opacity: 0 },
{ opacity: 1 }
];
第二个参数,持续时间,是我们希望动画持续的时间。 在上面的示例中,它是 1000 毫秒。 让我们看一个更令人兴奋的例子。
使用 WAAPI 重新创建 animista CSS 动画
这里有一些 CSS 代码,我从很棒的 animista 中提取出来,用于一个名为“slide-in-blurred-top”的入口动画。 它看起来很不错。

以下是 CSS 中的这些关键帧
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);
为了保持示例的简单性,我只指定了持续时间。 但是,我们可以使用第二个参数传递更多选项。 至少,我们还应该指定一个缓动。 以下是具有示例值的可用选项的完整列表
var options = {
iterations: Infinity,
iterationStart: 0,
delay: 0,
endDelay: 0,
direction: 'alternate',
duration: 700,
fill: 'forwards',
easing: 'ease-out',
}
element.animate(keyframes, options);
使用这些选项,我们的动画将从开头开始,没有延迟,并且无限循环,在正向播放和反向播放之间交替。
查看 CSS GRID 在 CodePen 上的 运动模糊 waapi 圆形 笔记 (@cssgrid)
令人讨厌的是,对于那些熟悉 CSS 动画的人来说,一些术语与我们习惯的术语不同。 尽管从好的方面来说,输入速度快了很多!
- 它是
easing
而不是animation-timing-function
- 而不是
animation-iteration-count
,它是iterations
。 如果我们希望动画无限重复,它是Infinity
而不是无限。 有点令人困惑的是,Infinity 不带引号。Infinity
是一个 JavaScript 关键字,而其他值是字符串。 - 我们使用毫秒而不是秒,这对于那些以前写过很多 JavaScript 代码的人来说应该很熟悉。(您也可以在 CSS 动画中使用毫秒,但很少有人这样做。)
让我们仔细看看其中一个选项:iterationStart
。
当我第一次遇到 iterationStart
时,我感到困惑。 为什么您要从指定的迭代开始,而不是仅仅减少迭代次数? 这个选项在您使用小数时最有帮助。 例如,您可以将其设置为 .5
,动画将从中间开始播放。 要形成一个整体需要两个半,因此如果您的迭代次数设置为 1 并且您的 iterationStart 设置为 .5
,动画将从中间开始播放,直到动画结束,然后从动画开头开始播放,并在中间结束!
值得注意的是,您还可以将迭代总数设置为小于 1。 例如
var option = {
iterations: .5,
iterationStart: .5
}
这将从中间播放动画,直到结束。
endDelay:endDelay 在您希望将多个动画串联起来,但希望在第一个动画结束和任何后续动画开始之间留出间隔时很有用。 这里有一个来自 Patrick Brosset 的有用视频来解释。
缓动
缓动是任何动画中最重要的元素之一。 WAAPI 为我们提供了两种不同的方式来设置缓动——在关键帧数组中或在选项对象中。
在 CSS 中,如果您应用了 animation-timing-function: ease-in-out
,您可能会认为动画的开头会缓入,动画的结尾会缓出。 事实上,缓动应用于关键帧之间,而不是整个动画。 这可以对动画的感觉进行细粒度的控制。 WAAPI 也提供了这种能力。
var keyframes = [
{ opacity: 0, easing: 'ease-in' },
{ opacity: 0.5, easing: 'ease-out' },
{ opacity: 1 }
]
值得注意的是,在 CSS 和 WAAPI 中,您都不应该为最后一帧传递缓动值,因为这将没有效果。 这是一个很多人都会犯的错误。
有时,在整个动画中添加缓动会更直观。 这在 CSS 中是不可能的,但现在可以使用 WAAPI 来实现。
var options = {
duration: 1000,
easing: 'ease-in-out',
}
您可以在这个 Pen 中看到这两种缓动之间的区别
查看 CSS GRID 在 CodePen 上的 相同的动画,不同的缓动 笔记 (@cssgrid)
缓动与线性
值得注意的是 CSS 动画和 WAAPI 之间的另一个区别:CSS 的默认值为 ease
,而 WAAPI 的默认值为 linear
。 缓动实际上是 ease-in-out
的一个版本,如果您感到懒惰,这是一个不错的选择。 同时,线性非常单调乏味——一致的速度看起来机械且不自然。 它可能被选择为默认值,因为它是最中性的选项。 但是,这使得在使用 WAAPI 时比使用 CSS 时应用缓动更重要,否则您的动画会显得乏味且机械。
性能
WAAPI 提供与 CSS 动画相同的性能改进,但这并不意味着流畅的动画是不可避免的。
我希望这个 API 的性能优化意味着我们可以摆脱 will-change
和完全 hacky 的 translateZ
——最终,它可能可以。 但是,至少在当前的浏览器实现中,这些属性仍然可以在处理抖动问题时有所帮助和必要。
但是,至少如果您对动画有延迟,您就不必担心使用 will-change
。 Web 动画规范的主要作者在 Animation for Work Slack 社区 上提供了一些有趣建议,希望他不介意我在这里重复一下:
如果您有正延迟,则不需要
will-change
,因为浏览器将在延迟开始时进行分层,并且当动画开始时,它将准备就绪。
WAAPI 与 CSS 动画的比较?
WAAPI 为我们在 JavaScript 中提供了一种语法,可以实现我们在样式表中已经能够实现的功能。 然而,它们不应该被视为竞争对手。 如果我们决定坚持使用 CSS 进行动画和过渡,我们就可以使用 WAAPI 与这些动画进行交互。
动画对象
.animate()
方法不仅仅动画化我们的元素,它还**返回**某些东西。
var myAnimation = element.animate(keyframes, options);

如果我们在控制台中查看返回值,我们会发现它是一个动画对象。这为我们提供了各种功能,其中一些非常容易理解,比如 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()
此方法将返回我们使用 WAAPI 定义的任何动画对象(以及任何 CSS 过渡或动画)的数组。
element.getAnimations() // returns any animations or transitions applied to our element using CSS or WAAPI
如果您觉得使用 CSS 来定义和应用动画很舒服,getAnimations()
允许您将 API 与 @keyframe
配合使用。您可以继续使用 CSS 完成大部分动画工作,并在需要时仍然可以使用 API。让我们看看这有多简单。
即使 DOM 元素只应用了一个动画,getAnimations()
始终会返回一个数组。让我们获取该单个动画对象以进行操作。
var h2 = document.querySelector("h2");
var myCSSAnimation = h2.getAnimations()[0];
现在我们可以对我们的 CSS 动画使用 Web 动画 API :)
myCSSAnimation.playbackRate = 4;
myCSSAnimation.reverse();
Promise 和事件
我们已经可以使用 CSS 触发各种事件,并在 JavaScript 代码中使用它们:animationstart
、animationend
、animationiteration
和 transitionend
。我经常需要监听动画或过渡的结束,以便随后从 DOM 中移除它所应用的元素。
在 WAAPI 中使用与 animationend
或 transitionend
等效的方法来实现此目的,同样需要使用动画对象。
myAnimation.onfinish = function() {
element.remove();
}
WAAPI 为我们提供了使用事件和 promise 的选择。动画对象的 .finished
属性将返回一个 promise,该 promise 在动画结束时解析。以下是上面示例使用 promise 的情况。
myAnimation.finished.then(() =>
element.remove())
让我们看一个稍微复杂一点的示例,它来自 Mozilla 开发者网络。Promise.all 期待一个 promise 数组,并且只有在所有这些 promise 解析后才会运行我们的回调函数。正如我们已经看到的,element.getAnimations()
返回一个动画对象的数组。我们可以遍历数组中的所有动画对象,对每个对象调用 .finished
,从而得到我们需要的 promise 数组。
在这个示例中,只有在页面上的所有动画都完成之后,我们的函数才会运行。
Promise.all(document.getAnimations().map(animation =>
animation.finished)).then(function() {
// do something cool
})
未来
本文中提到的功能仅仅是开始。当前的规范和实现看起来是伟大事情的开始。
这太棒了。声明性过渡非常适合 CSS,但动画一直感觉很笨拙,因为它们的命令式性质(例如,移除和重新添加一个类以再次触发它们)。过渡是隐式的,动画是命令式的。我对定义关键帧属性的非 CSS 语法感到不安,但很高兴看到我们能够在 CSS 的自然上下文中定义关键帧,然后在 JavaScript 中以命令式的方式触发和响应它们。这似乎是两全其美。
我对 JavaScript API 的唯一问题是 - 当我想使用它而不是过渡时 - 我仍然需要在动画完成后手动设置所有 CSS 属性,因为它会进行动画,然后恢复到状态 0。
如果你将时间选项对象中的
fill
属性设置为forwards
,这将有所帮助。很棒的文章……快速提问……有没有办法像 CSS 关键帧那样在关键帧上设置百分比?在您给出的示例中,您只有两个关键帧……一个用于 0%,另一个用于 100%……如果您有三个关键帧,并且希望它们分别在 10%、70% 和 100% 处运行,例如……有没有办法用 WAAPI 设置这个?
您可以使用关键帧上的
offset
。例如。
默认情况下,所有关键帧的间距相等。正如 TLGregg 所说,使用偏移量。
关于 CSS 动画的不错的文章。
我们有朝一日能够在动画中进行擦除吗?
我 91% 确定您可以在 DevTools(Firefox 或 Chrome)中擦除 WAAPI 中完成的动画。
哦,是的,我对此在 DevTools 中的确定性大约为 92%,但我更多地是在考虑在我们的代码中编写它,以便我们可以使用它将动画的进度与我们的滚动位置关联起来,就像视差一样 :D
是的,Chris 是对的。动画 DevTools 的时间轴功能也适用于 WAAPI 动画。非常有用。
嗨,Chris。
currentTime
属性可能会帮到你。https://mdn.org.cn/en-US/docs/Web/API/Animation/currentTime
您需要以某种方式将它链接到滚动位置。
看起来,与滚动相关的简单动画最终将会被实现。
https://wicg.github.io/scroll-animations/
看看 Dan Wilson 的这个非常棒的示例 https://codepen.io/danwilson/pen/JJEoxq/
我认为这正是我要找的东西,谢谢 Oliver。我创建了一个 React 组件来处理视差,我想试一试它 :)