使用 Web 动画 API
Web Animations API 允许我们使用 JavaScript 构建动画并控制其播放。本文将通过有趣的演示和爱丽丝梦游仙境主题的教程,引导您走上正确的学习道路。
了解 Web Animations API
Web Animations API 向开发者和 JavaScript 操作开放了浏览器的动画引擎。此 API 旨在成为 CSS Animations 和 CSS Transitions 两种实现的底层基础,并为未来的动画效果打开了大门。它是 Web 上执行动画最高效的方式之一,允许浏览器进行自己的内部优化,无需任何技巧、强制或 Window.requestAnimationFrame()。
借助 Web Animations API,我们可以将交互式动画从样式表移动到 JavaScript,将表现与行为分离。我们不再需要依赖 DOM 大量操作的技术,例如编写 CSS 属性和将作用域类添加到元素来控制播放方向。与纯声明式 CSS 不同,JavaScript 还允许我们动态设置从属性到持续时间的值。对于构建自定义动画库和创建交互式动画,Web Animations API 可能是完成这项工作的完美工具。让我们看看它能做什么!
此页面包含一套利用 Web Animations API 的示例,灵感来自《爱丽丝梦游仙境》。这些示例由 Rachel Nabors 创建并无偿分享。完整的示例套件可在 CodePen 上获取;这里我们展示了与我们文档相关的示例。
使用 Web Animations API 编写 CSS 动画
学习 Web Animations API 的一种更熟悉的方法是,从大多数 Web 开发者以前都玩过的东西开始:CSS 动画。CSS 动画具有熟悉的语法,非常适合演示。
CSS 版本
这是一个用 CSS 编写的翻滚动画,展示了爱丽丝掉进通往仙境的兔子洞
请注意,背景移动,爱丽丝旋转,并且她的颜色在与旋转偏移的位置发生变化。本教程中我们将只关注爱丽丝。您可以通过单击代码块上的“播放”来查看完整的源代码。这是控制爱丽丝动画的简化 CSS
#alice {
animation: alice-tumbling infinite 3s linear;
}
@keyframes alice-tumbling {
0% {
color: black;
transform: rotate(0) translate3d(-50%, -50%, 0);
}
30% {
color: #431236;
}
100% {
color: black;
transform: rotate(360deg) translate3d(-50%, -50%, 0);
}
}
这会在 3 秒内以恒定(线性)速度改变爱丽丝的颜色和她的变换旋转,并无限循环。在 @keyframes 块中,我们可以看到在每次循环的 30%(大约 0.9 秒时),爱丽丝的颜色从黑色变为深勃艮第,然后在循环结束时再次变回。
将其移动到 JavaScript
现在让我们尝试使用 Web Animations API 创建相同的动画。
表示关键帧
我们需要做的第一件事是创建一个与我们的 CSS @keyframes 块对应的关键帧对象
const aliceTumbling = [
{ transform: "rotate(0) translate3d(-50%, -50%, 0)", color: "black" },
{ color: "#431236", offset: 0.3 },
{ transform: "rotate(360deg) translate3d(-50%, -50%, 0)", color: "black" },
];
这里我们使用了一个包含多个对象的数组。每个对象代表原始 CSS 中的一个关键帧。然而,与 CSS 不同,Web Animations API 不需要明确告知每个关键帧在动画中出现的百分比。它将根据您提供的关键帧数量自动将动画分成相等的部分。这意味着,除非另有说明,具有三个关键帧的关键帧对象将在动画的每次循环中播放中间关键帧的 50%。
当我们想要明确设置一个关键帧相对于其他关键帧的偏移量时,我们可以在对象中直接指定偏移量,用逗号将其与声明分开。在上面的示例中,为了确保爱丽丝的颜色变化在 30%(而不是 50%)处发生,我们给它设置了 offset: 0.3。
目前,应至少指定两个关键帧(代表动画序列的开始和结束状态)。如果您的关键帧列表只有一个条目,在某些浏览器更新之前,Element.animate() 可能会抛出 NotSupportedError DOMException。
因此,总结一下,关键帧默认等距分布,除非您在某个关键帧上指定了偏移量。很方便,不是吗?
表示时间属性
我们还需要创建一个时间属性对象,对应于爱丽丝动画中的值
const aliceTiming = {
duration: 3000,
iterations: Infinity,
};
你会注意到这里与 CSS 中等效值的一些差异
- 首先,持续时间以毫秒为单位,而不是秒——3000 而不是 3 秒。像
setTimeout()和Window.requestAnimationFrame()一样,Web Animations API 只接受毫秒。 - 你会注意到的另一件事是它是
iterations,而不是iteration-count。
注意:CSS 动画和 Web 动画中使用的术语之间存在许多细微的差异。例如,Web 动画不使用字符串 "infinite",而是使用 JavaScript 关键字 Infinity。并且不使用 timing-function,而是使用 easing。我们这里没有列出 easing 值,因为与 CSS 动画中默认的 animation-timing-function 为 ease 不同,在 Web 动画 API 中,默认的缓动是 linear — 这正是我们想要的。
将这些片段组合起来
现在是时候使用 Element.animate() 方法将它们组合在一起了
document.getElementById("alice").animate(aliceTumbling, aliceTiming);
然后砰的一声:动画开始播放
animate() 方法可以在任何可以用 CSS 动画化的 DOM 元素上调用。它可以用几种方式编写。与其为关键帧和计时属性创建对象,我们可以直接传入它们的值,如下所示
document.getElementById("alice").animate(
[
{ transform: "rotate(0) translate3d(-50%, -50%, 0)", color: "black" },
{ color: "#431236", offset: 0.3 },
{ transform: "rotate(360deg) translate3d(-50%, -50%, 0)", color: "black" },
],
{
duration: 3000,
iterations: Infinity,
},
);
更重要的是,如果只想指定动画的持续时间而不指定其迭代次数(默认情况下,动画迭代一次),我们可以只传入毫秒数
document.getElementById("alice").animate(
[
{ transform: "rotate(0) translate3d(-50%, -50%, 0)", color: "black" },
{ color: "#431236", offset: 0.3 },
{ transform: "rotate(360deg) translate3d(-50%, -50%, 0)", color: "black" },
],
3000,
);
使用 play()、pause()、reverse() 和 updatePlaybackRate() 控制播放
虽然我们可以用 Web Animations API 编写 CSS 动画,但该 API 真正派上用场的地方是操纵动画的播放。Web Animations API 提供了几种有用的方法来控制播放。让我们看看在“追逐白兔”示例中暂停和播放动画
在这个例子中,白兔有一个动画,导致它掉进兔子洞。它只有在用户点击它时才会触发。
暂停和播放动画
我们可以像往常一样使用 animate() 方法来动画化兔子
const whiteRabbit = document.getElementById("rabbit");
const rabbitDownAnimation = whiteRabbit.animate(
[{ transform: "translateY(0%)" }, { transform: "translateY(100%)" }],
{ duration: 3000, fill: "forwards" },
);
Element.animate() 方法在被调用后会立即运行。为了防止蛋糕在用户有机会点击它之前就自己吃掉,我们立即在其定义后调用 Animation.pause(),如下所示
rabbitDownAnimation.pause();
注意: 另外,您也可以使用 Animation() 构造函数定义 rabbitDownAnimation,该构造函数在您调用 play() 之前不会开始播放。
现在我们可以随时使用 Animation.play() 方法来运行它。具体来说,我们希望将其与点击操作关联起来。我们可以通过以下方式实现这一点
whiteRabbit.addEventListener("click", downHeGoes);
whiteRabbit.addEventListener("touchstart", downHeGoes);
function downHeGoes(event) {
whiteRabbit.removeEventListener("click", downHeGoes);
whiteRabbit.removeEventListener("touchstart", downHeGoes);
rabbitDownAnimation.play();
}
当用户点击或触摸兔子时,我们现在可以调用 downHeGoes 来播放所有动画。
其他有用的方法
除了暂停和播放,我们还可以使用以下动画方法
Animation.finish()跳到动画的末尾。Animation.cancel()中止动画并移除其效果。Animation.reverse()将动画的播放速率 (Animation.playbackRate) 设置为负值,使其向后运行。
让我们首先看一下 playbackRate——负的 playbackRate 将导致动画反向运行。在 《爱丽丝镜中奇遇记》中,爱丽丝来到一个她必须奔跑才能保持原地的世界——并且必须跑得快两倍才能前进!在红皇后赛跑的例子中,爱丽丝和红皇后正在奔跑以保持原地
因为小孩子不像自动棋子那样容易疲劳,爱丽丝不断减速。我们可以通过设置她动画的 playbackRate 的衰减来实现这一点。我们使用 updatePlaybackRate() 而不是直接设置 playbackRate,因为这会产生平滑的更新
setInterval(() => {
// Make sure the playback rate never falls below .4
if (redQueenAlice.playbackRate > 0.4) {
redQueenAlice.updatePlaybackRate(redQueenAlice.playbackRate * 0.9);
}
adjustBackgroundPlayback();
}, 1000);
但是通过点击或轻触来催促它们会通过将其 playbackRate 相乘来加速它们
function goFaster() {
// But you can speed them up by giving the screen a click or a tap.
redQueenAlice.updatePlaybackRate(redQueenAlice.playbackRate * 1.1);
adjustBackgroundPlayback();
}
document.addEventListener("click", goFaster);
document.addEventListener("touchstart", goFaster);
背景元素的 playbackRate 也会在您点击或轻触时受到影响。它们的播放速率源自爱丽丝的,如下所示。当您让爱丽丝和红皇后跑得快两倍时会发生什么?当您让它们减速时会发生什么?
/* Alice tires so easily!
Every so many seconds, reduce their playback rate so they slow a little.
*/
const sceneries = [
foreground1Movement,
foreground2Movement,
background1Movement,
background2Movement,
];
function adjustBackgroundPlayback() {
// If Alice and the Red Queen are running at a speed of 0.8–1.2,
// the background doesn't move.
// But if they fall under 0.8, the background slides backwards
if (redQueenAlice.playbackRate < 0.8) {
sceneries.forEach((anim) => {
anim.updatePlaybackRate(-redQueenAlice.playbackRate / 2);
});
} else if (redQueenAlice.playbackRate > 1.2) {
sceneries.forEach((anim) => {
anim.updatePlaybackRate(redQueenAlice.playbackRate / 2);
});
} else {
sceneries.forEach((anim) => {
anim.updatePlaybackRate(0);
});
}
}
adjustBackgroundPlayback();
保留动画样式
在对元素进行动画处理时,常见的用例是在动画完成后保留动画的最终状态。有时用于此的一种方法是将动画的填充模式设置为 forwards。然而,不建议无限期地使用填充模式来保留动画效果,原因有二
- 浏览器必须在动画仍处于活动状态时维护动画状态,因此即使动画不再播放,它也会继续消耗资源。请注意,这在一定程度上通过浏览器自动移除填充动画而得到缓解。
- 动画应用的样式在级联中具有更高的优先级,因此在需要时可能难以覆盖它们。
更好的方法是使用 Animation.commitStyles() 方法。这会将动画当前样式的计算值写入其目标元素的 style 属性,之后该元素可以正常重新设置样式。
自动移除填充动画
可以在同一元素上触发大量动画。如果它们是无限的(即,向前填充),这可能导致巨大的动画列表,从而造成内存泄漏。因此,浏览器会在较新的动画替换旧的动画后自动移除填充动画,除非开发者明确指定保留它们。
当满足以下所有条件时,动画将被移除
- 动画正在填充(如果它向前播放,其
fill为forwards;如果它向后播放,则为backwards;或为both)。 - 动画已完成。(请注意,由于
fill,它仍然有效。) - 动画的时间线单调递增。(对于
DocumentTimeline来说这总是真的;其他时间线例如scroll-timeline可以反向运行。) - 动画不受声明性标记(如 CSS)控制。
- 动画的
AnimationEffect的每个样式效果都被另一个也满足上述所有条件的动画所覆盖。(通常,当两个动画设置同一元素的同一样式属性时,后创建的动画会覆盖另一个。)
前四个条件确保,在没有 JavaScript 代码干预的情况下,动画的效果永远不会改变或结束。最后一个条件确保动画永远不会真正影响任何元素的样式:它已被完全替换。
当动画自动移除时,动画的 remove 事件将被触发。
为了防止浏览器自动移除动画,请调用动画的 persist() 方法。
如果动画已被移除,则动画的 replaceState 属性将为 removed;如果您已对动画调用 persist(),则为 persisted;否则为 active。
从动画中获取信息
想象一下我们还可以如何使用 playbackRate,例如通过让患有前庭疾病的用户减慢整个网站的动画来改善可访问性。这在没有重新计算每个 CSS 规则中的持续时间的情况下是无法用 CSS 实现的,但使用 Web Animations API,我们可以使用 Document.getAnimations 方法遍历页面上的每个动画,并将其 playbackRate 减半,如下所示
document.getAnimations().forEach((animation) => {
animation.updatePlaybackRate(animation.playbackRate * 0.5);
});
使用 Web Animations API,您只需要更改一个小属性!
单独使用 CSS 动画很难做到的另一件事是创建对其他动画提供的值的依赖。例如,在“爱丽丝变大变小游戏”示例中,您可能注意到蛋糕的持续时间有些奇怪
document.getElementById("eat-me-sprite").animate([], {
duration: aliceChange.effect.getComputedTiming().duration / 2,
});
要了解这里发生了什么,让我们看一下爱丽丝的动画
const aliceChange = document
.getElementById("alice")
.animate(
[
{ transform: "translate(-50%, -50%) scale(.5)" },
{ transform: "translate(-50%, -50%) scale(2)" },
],
{
duration: 8000,
easing: "ease-in-out",
fill: "both",
},
);
爱丽丝的动画让她在 8 秒内从她的一半大小变为她的两倍大小。然后我们暂停她
aliceChange.pause();
如果我们在动画开始时就暂停了她,她就会从她的一半大小开始,就好像她已经喝光了整瓶药水一样!我们希望将她的动画“播放头”设置在中间,这样她就已经完成了一半。我们可以通过将她的 Animation.currentTime 设置为 4 秒来做到这一点,如下所示
aliceChange.currentTime = 4000;
但是,在制作此动画时,我们可能会经常改变爱丽丝的持续时间。如果我们动态设置她的 currentTime,这样我们就不用一次进行两次更新,那不是更好吗?实际上,我们可以通过引用 aliceChange 的 Animation.effect 属性来做到这一点,该属性返回一个包含爱丽丝身上所有活动效果详细信息的对象
aliceChange.currentTime = aliceChange.effect.getComputedTiming().duration / 2;
effect 允许我们访问动画的关键帧和时间属性——aliceChange.effect.getComputedTiming() 指向爱丽丝的计时对象——这包含她的 duration。我们可以将她的持续时间除以一半来获得她动画时间线的中点,将她设置为正常高度。现在我们可以反向播放她的动画,让她变小或变大!
我们在设置蛋糕和瓶子持续时间时也可以做同样的事情
const drinking = document
.getElementById("liquid")
.animate([{ height: "100%" }, { height: "0" }], {
fill: "forwards",
duration: aliceChange.effect.getComputedTiming().duration / 2,
});
drinking.pause();
现在,所有三个动画都只与一个持续时间相关联,我们可以轻松地从一个地方更改它。
我们还可以使用 Web Animations API 来计算动画的当前时间。游戏在您吃完蛋糕或喝光瓶子时结束。玩家看到的插曲取决于爱丽丝在动画中进展到何处,是她变得太大了无法进入小门,还是变得太小了无法拿到开门的钥匙。我们可以通过获取她动画的 currentTime 并将其除以她的 activeDuration 来确定她在动画中是处于大尺寸还是小尺寸
const endGame = () => {
// get Alice's timeline's playhead location
const alicePlayhead = aliceChange.currentTime;
const aliceTimeline = aliceChange.effect.getComputedTiming().activeDuration;
// stops Alice's and other animations
stopPlayingAlice();
// depending on which third it falls into
const aliceHeight = alicePlayhead / aliceTimeline;
if (aliceHeight <= 0.333) {
// Alice got smaller!
// …
} else if (aliceHeight >= 0.666) {
// Alice got bigger!
// …
} else {
// Alice didn't change significantly
// …
}
};
回调和 Promise
CSS 动画和过渡有自己的事件监听器,Web Animations API 也支持这些监听器
这里我们将蛋糕、瓶子和爱丽丝的回调设置为触发 endGame 函数
// When the cake or bottle runs out
nommingCake.onfinish = endGame;
drinking.onfinish = endGame;
// Alice reaches the end of her animation
aliceChange.onfinish = endGame;
更好的是,Web Animations API 还提供了一个 finished Promise,当动画完成时它会解析,如果动画被取消则会拒绝。
总结
这些是 Web Animations API 的基本功能。现在您应该已经准备好“跳进兔子洞”,在浏览器中进行动画制作,并准备好编写自己的动画实验了!
另见
- CodePen 上的《爱丽丝梦游仙境》演示完整套件,供您玩耍、分叉和分享。
- 使用 Element.animate 随意动画化 (2016) 解释了 Web Animations API 的背景以及它为何比其他 Web 动画方法性能更高。