使用 Web Animations API

Web Animations API 使我们能够使用 JavaScript 构建动画并控制其播放。本文将通过有趣的演示和以爱丽丝梦游仙境为主题的教程,帮助您入门。

认识 Web Animations API

Web Animations API 将浏览器的动画引擎开放给开发人员,并允许 JavaScript 进行操作。该 API 被设计为 CSS 动画和 CSS 过渡实现的基础,并为未来的动画效果敞开了大门。它是 Web 上最有效的动画方式之一,它让浏览器进行自己的内部优化,无需使用黑客手段、强制转换或 Window.requestAnimationFrame()。

使用 Web Animations API,我们可以将交互式动画从样式表转移到 JavaScript 中,从而将表现形式与行为分离。我们不再需要依赖于 DOM 密集型技术,例如编写 CSS 属性和将类作用域到元素以控制播放方向。与纯声明性 CSS 不同,JavaScript 还允许我们动态地设置从属性到持续时间的各种值。对于构建自定义动画库和创建交互式动画,Web Animations API 可能是最佳工具。让我们看看它能做什么!

使用 Web Animations API 编写 CSS 动画

学习 Web Animations API 的一种较为常见的方法是从大多数 Web 开发人员之前已经接触过的内容开始:CSS 动画。CSS 动画具有熟悉的语法,非常适合演示目的。

CSS 版本

这是一个用 CSS 编写的翻滚动画,展示了爱丽丝掉入通往仙境兔子洞的场景(查看 Codepen 上的完整代码)。

Alice Tumbling down the rabbit's hole.

请注意,背景移动,爱丽丝旋转,她的颜色在旋转时发生变化。在本教程中,我们将重点关注爱丽丝。以下简化的 CSS 控制了爱丽丝的动画。

css
#alice {
  animation: aliceTumbling infinite 3s linear;
}

@keyframes aliceTumbling {
  0% {
    color: #000;
    transform: rotate(0) translate3D(-50%, -50%, 0);
  }
  30% {
    color: #431236;
  }
  100% {
    color: #000;
    transform: rotate(360deg) translate3D(-50%, -50%, 0);
  }
}

这会在 3 秒内以恒定(线性)速率更改爱丽丝的颜色和变换旋转,并无限循环。在 @keyframes 块中,我们可以看到,在每次循环的 30% 处(大约 0.9 秒),爱丽丝的颜色从黑色变为深酒红色,然后在循环结束前变回黑色。

迁移到 JavaScript

现在让我们尝试使用 Web Animations API 创建相同的动画。

表示关键帧

首先,我们需要创建一个 Keyframe 对象,对应于我们的 CSS @keyframes 块。

js
const aliceTumbling = [
  { transform: "rotate(0) translate3D(-50%, -50%, 0)", color: "#000" },
  { color: "#431236", offset: 0.3 },
  { transform: "rotate(360deg) translate3D(-50%, -50%, 0)", color: "#000" },
];

这里我们使用了一个包含多个对象的数组。每个对象表示原始 CSS 中的一个键。但是,与 CSS 不同的是,Web Animations API 不需要明确告知动画中的每个键在动画中出现的百分比。它会根据您提供的键的数量自动将动画分成相等的部分。这意味着,具有三个键的 Keyframe 对象会在每次循环的 50% 处播放中间键,除非另有说明。

当我们想要明确设置键相对于其他键的偏移量时,可以在对象中直接指定偏移量,并用逗号与声明隔开。在上面的示例中,为了确保爱丽丝的颜色在 30% 处发生变化(而不是 50%),我们为它提供了 offset: 0.3。

目前,至少应指定两个关键帧(表示动画序列的起始状态和结束状态)。如果您的关键帧列表中只有一个条目,Element.animate() 可能会在某些浏览器中抛出 NotSupportedError DOMException,直到这些浏览器更新。

因此,简而言之,默认情况下,除非您在键上指定偏移量,否则键的间隔是相等的。方便吧?

表示时间属性

我们还需要创建一个时间属性对象,对应于爱丽丝动画中的值。

js
const aliceTiming = {
  duration: 3000,
  iterations: Infinity,
};

您会注意到这里与 CSS 中表示等效值的几种差异。

  • 首先,持续时间以毫秒为单位,而不是秒——3000 而不是 3 秒。与 setTimeout() 和 Window.requestAnimationFrame() 一样,Web Animations API 只接受毫秒。
  • 您还会注意到是 iterations,而不是 iteration-count。

注意:CSS 动画中使用的术语与 Web 动画中使用的术语之间存在一些细微差异。例如,Web 动画不使用字符串“infinite”,而是使用 JavaScript 关键字 Infinity。此外,我们使用 easing 而不是 timing-function。这里没有列出 easing 值,因为与 CSS 动画不同,CSS 动画的默认 animation-timing-function 是 ease,而在 Web Animations API 中,默认的缓动方式是 linear,这正是我们想要的效果。

整合各部分

现在是时候使用 Element.animate() 方法将它们整合在一起了。

js
document.getElementById("alice").animate(aliceTumbling, aliceTiming);

这样,动画就开始播放了(查看 Codepen 上的完成版本)。

animate() 方法可以调用任何可以用 CSS 动画的 DOM 元素。它可以用多种方式编写。我们可以直接传递关键帧和时间属性的值,而不是为它们创建对象,如下所示。

js
document.getElementById("alice").animate(
  [
    { transform: "rotate(0) translate3D(-50%, -50%, 0)", color: "#000" },
    { color: "#431236", offset: 0.3 },
    { transform: "rotate(360deg) translate3D(-50%, -50%, 0)", color: "#000" },
  ],
  {
    duration: 3000,
    iterations: Infinity,
  },
);

此外,如果我们只想指定动画的持续时间,而不指定迭代次数(默认情况下,动画迭代一次),可以只传入毫秒数。

js
document.getElementById("alice").animate(
  [
    { transform: "rotate(0) translate3D(-50%, -50%, 0)", color: "#000" },
    { color: "#431236", offset: 0.3 },
    { transform: "rotate(360deg) translate3D(-50%, -50%, 0)", color: "#000" },
  ],
  3000,
);

使用 play()、pause()、reverse() 和 updatePlaybackRate() 控制播放

虽然我们可以使用 Web Animations API 编写 CSS 动画,但 API 的真正用武之地在于操作动画的播放。Web Animations API 提供了多种有用的方法来控制播放。让我们看看如何在“爱丽丝变大变小”游戏中暂停和播放动画(查看 Codepen 上的完整代码)。

Playing the growing and shrinking game with Alice.

在这个游戏中,爱丽丝有一个动画,让她从变小变大,我们可以通过一个瓶子和一个蛋糕来控制。两者都有自己的动画。

暂停和播放动画

我们稍后会详细介绍爱丽丝的动画,但现在让我们仔细看看蛋糕的动画。

js
const nommingCake = document
  .getElementById("eat-me_sprite")
  .animate(
    [{ transform: "translateY(0)" }, { transform: "translateY(-80%)" }],
    {
      fill: "forwards",
      easing: "steps(4, end)",
      duration: aliceChange.effect.getComputedTiming().duration / 2,
    },
  );

Element.animate() 方法将在调用后立即运行。为了防止蛋糕在用户点击它之前就吃掉自己,我们会在定义它之后立即调用 Animation.pause(),如下所示。

js
nommingCake.pause();

现在,我们可以使用 Animation.play() 方法在准备好时运行它。

js
nommingCake.play();

具体而言,我们希望将其与爱丽丝的动画关联起来,这样当蛋糕被吃掉时,爱丽丝就会变大。我们可以通过以下函数来实现。

js
const growAlice = () => {
  // Play Alice's animation.
  aliceChange.play();

  // Play the cake's animation.
  nommingCake.play();
};

当用户按住鼠标或在触摸屏上按住手指时,现在可以调用 growAlice 让所有动画播放。

js
cake.addEventListener("mousedown", growAlice, false);
cake.addEventListener("touchstart", growAlice, false);

其他有用的方法

除了暂停和播放之外,我们还可以使用以下 Animation 方法。

  • Animation.finish() 跳到动画的末尾。
  • Animation.cancel() 中断动画并删除其效果。
  • Animation.reverse() 将动画的播放速率 (Animation.playbackRate) 设置为负值,使其反向运行。

让我们先来看看 playbackRate——负的 playbackRate 会导致动画反向运行。当爱丽丝从瓶子里喝水时,她会变小。这是因为瓶子将她的动画播放速率从 1 更改为 -1。

js
const shrinkAlice = () => {
  aliceChange.playbackRate = -1;
  aliceChange.play();
};

bottle.addEventListener("mousedown", shrinkAlice, false);
bottle.addEventListener("touchstart", shrinkAlice, false);

在《镜中奇遇记》中,爱丽丝进入了一个必须奔跑才能保持原地的世界——而且必须快跑两倍才能向前移动!在红皇后竞赛示例中,爱丽丝和红皇后正在奔跑以保持原位(查看 Codepen 上的完整代码)。

Alice and the Red Queen race to get to the next square in this game.

因为小孩子很容易累,不像自动的棋子,爱丽丝一直在减速。我们可以通过在她的动画 playbackRate 上设置衰减来实现。我们使用 updatePlaybackRate() 而不是直接设置 playbackRate,因为这会产生平滑的更新。

js
setInterval(() => {
  // Make sure the playback rate never falls below .4
  if (redQueen_alice.playbackRate > 0.4) {
    redQueen_alice.updatePlaybackRate(redQueen_alice.playbackRate * 0.9);
  }
}, 3000);

但是,通过点击或轻触来敦促她们继续前进会让她们通过将 playbackRate 乘以一个值来加速。

js
const goFaster = () => {
  redQueen_alice.updatePlaybackRate(redQueen_alice.playbackRate * 1.1);
};

document.addEventListener("click", goFaster);
document.addEventListener("touchstart", goFaster);

背景元素也具有playbackRate,这些元素在您点击或轻触时会受到影响。当您让爱丽丝和红皇后跑得快两倍时会发生什么?当您让她们慢下来时会发生什么?

持久化动画样式

在对元素进行动画处理时,一个常见的用例是在动画完成后保留动画的最终状态。有时为此使用的一种方法是将动画的填充模式设置为forwards。但是,出于两个原因,不建议使用填充模式无限期地保留动画效果

  • 浏览器必须在动画处于活动状态时保持动画状态,因此动画会继续消耗资源,即使它不再动画。请注意,这在一定程度上可以通过浏览器自动移除填充动画来缓解。
  • 动画应用的样式在层叠顺序中比指定的样式具有更高的优先级,因此在需要时可能难以覆盖它们。

更好的方法是使用Animation.commitStyles()方法。这将动画当前样式的计算值写入其目标元素的style属性,之后可以正常重新设置元素的样式。

自动删除填充动画

可以在同一个元素上触发大量的动画。如果它们是无限期的(即,向前填充),这会导致巨大的动画列表,这可能会导致内存泄漏。出于这个原因,浏览器会自动删除填充动画,在它们被更新的动画替换后,除非开发人员明确指定保留它们。

当以下所有条件都满足时,动画将被删除

  • 动画正在填充(如果它正在向前播放,则其fillforwards,如果它正在向后播放,则其fillbackwards,或者both)。
  • 动画已完成。(请注意,由于fill,它仍然会生效。)
  • 动画的时间线是单调递增的。(这对于DocumentTimeline始终为真;其他时间线,例如scroll-timeline可以向后运行。)
  • 动画没有受到声明式标记(如 CSS)的控制。
  • 动画的AnimationEffect的所有样式效果都被另一个也满足上述所有条件的动画所覆盖。(通常,当两个动画设置同一个元素的相同样式属性时,最后创建的动画会覆盖另一个动画。)

前四个条件确保,在没有 JavaScript 代码干预的情况下,动画效果永远不会改变或结束。最后一个条件确保动画永远不会真正影响任何元素的样式:它已被完全替换。

当动画被自动删除时,动画的remove事件会触发。

要阻止浏览器自动删除动画,请调用动画的persist()方法。

如果动画已被删除,则动画的animation.replaceState属性将为removed,如果您已对动画调用persist(),则为persisted,否则为active

从动画中获取信息

想象一下我们可以使用playbackRate的其他方式,例如通过让用户减慢整个网站上的动画速度来提高患有前庭疾病用户的可访问性。这在没有重新计算每个 CSS 规则中的持续时间的情况下,使用 CSS 是不可能实现的,但是使用 Web Animations API,我们可以使用Document.getAnimations方法循环遍历页面上的每个动画,并将它们的playbackRate减半,如下所示

js
document.getAnimations().forEach((animation) => {
  animation.updatePlaybackRate(animation.playbackRate * 0.5);
});

使用 Web Animations API,您只需要更改一个属性即可!

另一个使用 CSS 动画单独难以做到的事情是创建对其他动画提供的值的依赖。例如,在“爱丽丝变大和变小”游戏示例中,您可能已经注意到蛋糕的持续时间有些奇怪

js
document.getElementById("eat-me_sprite").animate([], {
  duration: aliceChange.effect.timing.duration / 2,
});

为了理解这里发生了什么,让我们看一下爱丽丝的动画

js
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 秒内从一半大小变为两倍大小。然后我们暂停了她

js
aliceChange.pause();

如果我们让她在动画开始时暂停,她会从一半的完整大小开始,就像她已经喝了整瓶一样!我们希望将她的动画“播放头”设置在中间,这样她已经完成了一半。我们可以通过将她的Animation.currentTime设置为 4 秒来实现,如下所示

js
aliceChange.currentTime = 4000;

但是在制作这个动画时,我们可能会多次更改爱丽丝的持续时间。如果我们动态设置她的currentTime,是不是更好,这样我们就不用同时进行两次更新了?实际上,我们可以通过引用aliceChange的Animation.effect属性来实现,该属性返回一个包含爱丽丝上活动效果的所有细节的对象

js
aliceChange.currentTime = aliceChange.effect.getComputedTiming().duration / 2;

effect让我们可以访问动画的关键帧和计时属性——aliceChange.effect.getComputedTiming()指向爱丽丝的计时对象——它包含她的duration。我们可以将她的持续时间减半以获得她动画时间线的中间点,使她恢复正常高度。现在我们可以反转并播放她的动画以使她变小或变大!

我们也可以在设置蛋糕和瓶子的持续时间时做同样的事情

js
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来找出她是处于动画的较大端还是较小端

js
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
    // …
  }
};

注意:截至撰写本文时,getAnimations()effect并非在所有浏览器中都已发布,但 polyfill 今天支持它们。

回调函数和 Promise

CSS 动画和过渡有自己的事件监听器,这些事件监听器在 Web Animations API 中也是可能的

  • onfinishfinish事件的事件处理程序,可以使用finish()手动触发。
  • oncancelcancel事件的事件处理程序,可以使用cancel()触发。

在这里,我们为蛋糕、瓶子和爱丽丝设置回调,以触发endGame函数

js
// 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 的基本功能。到目前为止,您应该已经准备好“跳入兔子洞”在浏览器中进行动画处理,并准备好编写您自己的动画实验!

另请参阅