如何使用 CSS 自定义属性播放和暂停 CSS 动画

Avatar of Mads Stoumann
Mads Stoumann

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费积分!

让我们来看看 CSS @keyframes 动画,特别是如何 **暂停** 以及其他控制方式。有一个专门用于此的 CSS 属性,可以通过 JavaScript 控制,但细节中有很多细微差别。我们还将看看我首选的设置方法,它提供了很多控制。提示:它涉及 CSS 自定义属性。

暂停动画的重要性

最近,在使用 CSS 制作的幻灯片(您将在本文后面看到)时,我检查了 DevTools 的 **图层** 面板中的动画。我注意到了一些之前从未想过的事情:当前不在视窗内的 动画仍在运行!

也许这并不那么令人意外。我们知道视频就是这样做的。视频会一直播放,直到您暂停它。但这让我开始思考这些正在播放的动画是否仍在使用 CPU/GPU?它们是否会消耗不必要的处理能力,从而降低页面其他部分的速度?

在 DevTools 中检查 **性能** 面板中的帧没有提供更多关于此的信息,因为我无法看到“屏幕外”帧。但是,当我向下滚动离开我的“仅使用 CSS 的幻灯片”的第一个幻灯片,然后等待并向后滚动时,它处于第五个幻灯片。动画并没有暂停。动画会一直运行,直到您暂停它们。

因此,我开始研究如何为什么以及何时应该暂停动画。鉴于上面的发现,性能是一个明显的原因。另一个原因是控制。用户不仅喜欢拥有控制权,而且应该拥有控制权。几年前,我的妻子遭受了严重的脑震荡。从那以后,她一直在避免带有太多动画的网页,因为这些动画会让她头晕。因此,我认为可访问性可能是允许动画暂停的最重要的原因。

总而言之,这些都是很重要的事情。我们专门讨论 CSS 关键帧动画,但从广义上讲,这意味着我们正在讨论

  1. 性能
  2. 控制
  3. 可访问性

暂停动画的基础知识

在 CSS 中真正暂停动画的唯一方法是使用 animation-play-state 属性,其值为 paused

.paused {
  animation-play-state: paused;
}

在 JavaScript 中,该属性为“驼峰式命名法”的 animationPlayState,设置方法如下

element.style.animationPlayState = 'paused';

我们可以通过读取 animationPlayState 的当前值来创建一个播放和暂停动画的切换按钮

const running = element.style.animationPlayState === 'running';

…然后将其设置为相反的值

element.style.animationPlayState = running ? 'paused' : 'running';

设置持续时间

暂停动画的另一种方法是将 animation-duration 设置为 0s。动画实际上在运行,但由于它没有持续时间,您不会看到任何动作。

但如果我们将其值改为 3s

它有效,但有一个主要缺点:动画在技术上仍在运行。动画只是在其初始位置和序列中的下一个位置之间切换。

直接移除动画

我们可以完全移除动画,并通过类将其添加回来,但与 animation-duration 一样,这实际上不会暂停动画。

.remove-animation {
  animation: none !important;
}

由于我们真正想要的是真正的暂停,让我们坚持使用 animation-play-state 并研究其他使用方法。

使用数据属性和 CSS 自定义属性

让我们在 CSS 中使用 数据属性 作为选择器。我们可以随意命名它们,因此我将在所有想要播放/暂停动画的元素上使用 [data-animation] 属性。这样,它可以与其他动画区分开来

<div data-animation></div>

该属性是选择器,animation 简写是我们将设置所有内容的属性。我们将添加一些 CSS 自定义属性(使用 Emmet 简写)作为值

[data-animation] {
  animation:
    var(--animn, none)
    var(--animdur, 1s)
    var(--animtf, linear)
    var(--animdel, 0s)
    var(--animic, infinite)
    var(--animdir, alternate)
    var(--animfm, none)
    var(--animps, running);
}

有了它,任何具有该数据属性的动画都将完全准备好接受动画,并且我们可以 使用自定义属性控制动画的各个方面。一些动画将有一些共同点(例如持续时间、缓动类型等),因此自定义属性上也设置了回退值。

为什么要使用 CSS 自定义属性?首先,它们可以在 CSS 和 JavaScript 中读取和设置。其次,它们可以显著减少我们需要编写的 CSS 量。而且,由于我们可以在 @keyframes 中设置它们(至少在撰写本文时在 Chrome 中可以这样做),它们提供了全新的工作方式来处理动画!

对于动画本身,我使用类选择器并更新来自 [data-animation] 选择器的变量

<div class="circle a-slide" data-animation></div>

为什么要使用类数据属性?在此阶段,data-animation 属性也可以是普通类,但我们将在后面以更高级的方式使用它。请注意,.circle 类名实际上与动画无关 - 它只是一个用于为元素设置样式的类。

/* Animation classes */
.a-pulse {
  --animn: pulse;
}
.a-slide {
  --animdur: 3s;
  --animn: slide;
}

/* Keyframes */
@keyframes pulse {
  0% { transform: scale(1); }
  25% { transform: scale(.9); }
  50% { transform: scale(1); }
  75% { transform: scale(1.1); }
  100% { transform: scale(1); }
}
@keyframes slide {
  from { margin-left: 0%; }
  to { margin-left: 150px; }
}

我们只需要更新要更改的值,因此如果我们在 data-animation 选择器的回退值中使用一些通用值,我们只需要更新动画自定义属性 --animn 的名称。

示例:使用复选框技巧暂停

为了使用老式的 复选框技巧 暂停所有动画,让我们在动画之前创建一个复选框

<input type="checkbox" data-animation-pause />

并在 checked 时更新 --animps 属性

[data-animation-pause]:checked ~ [data-animation] {
  --animps: paused;
}

就是这样!单击复选框时,动画会在播放和暂停之间切换 - 无需 JavaScript。

仅使用 CSS 的幻灯片

让我们将这些想法付诸实践!

我最近一直在使用 <details> 标签。它是制作 手风琴 的显而易见的候选者,但它也可以用于制作 工具提示切换提示、下拉菜单(设计成 <select> 的外观) 、大型菜单……应有尽有。毕竟,它是官方的 HTML 公开元素。除了所有 HTML 元素都接受的全局属性和全局事件之外,<details> 只有一个 open 属性和一个 toggle 事件。因此,与复选框技巧一样,它非常适合切换状态 - 但更加简单

details[open] {
  --state: 1;
}
details:not([open]) {
  --state: 0;
}

我决定做一个幻灯片,其中幻灯片通过称为 autoplay 的主要动画自动切换,并且每个单独的幻灯片都有自己独特的次要动画。animation-play-state--animps 属性控制。每个单独的幻灯片都可以有自己独特的动画,在 --animn 属性中定义

<figure style="--animn:kenburns-top;--index:0;">
  <img src="some-slide-image.jpg" />
  <figcaption>Caption</figcaption>
</figure>

次要动画的 animation-play-state--img-animps 属性控制。我在 Animista 中找到了一些不错的 Ken Burns 风格的动画,并在幻灯片的 --animn 属性中切换它们。

从另一个动画暂停动画

为了防止 GPU 过载,理想情况下,主动画应该暂停任何辅助动画。我们之前简要提到了这一点,但只有 Chrome(在撰写本文时,而且它确实有点不稳定)可以从 @keyframe 动画更新 CSS 自定义属性——你可以在以下示例中看到,其中 --bgc 属性和 --counter 属性在不同的帧中被修改

辅助动画的初始状态,--img-animps 属性,需要处于 paused 状态,即使主动画正在运行

details[open] ~ .c-mm__inner .c-mm__frame {
  --animps: running;
  --img-animps: paused;
}

然后,在主动画 @keyframes 中,该属性更新为 running

@keyframes autoplay {
  0.1% {
    --img-animps: running; /* START */
    opacity: 0;
    z-index: calc(var(--z) + var(--slides))
  }
  5% { opacity: 1 }
  50% { opacity: 1 }
  51% { --img-animps: paused } /* STOP! */
  100% {
    opacity: 0;
    z-index: var(--z)
  }
}

为了使此功能在除 Chrome 之外的其他浏览器中也能正常运行,初始值需要设置为 running,因为它们无法从 @keyframe 更新 CSS 自定义属性。

这是一个幻灯片,带有一个“细节技巧”播放/暂停按钮——不需要 JavaScript

启用 prefers-reduced-motion

有些人不喜欢动画,或者至少不喜欢过多的动画。这可能仅仅是个人喜好,但也可能是因为存在医疗状况。我们在这篇文章的开头就谈到了动画的无障碍性。

macOS 和 Windows 都有选项,允许用户告知浏览器他们更喜欢在网站上减少动画。这使我们能够使用 prefers-reduced-motion 特性查询,Eric Bailey 对此进行了全面介绍.

@media (prefers-reduced-motion) { ... }

让我们使用 [data-animation] 选择器来减少动画,通过为它提供不同的值来应用于启用 prefers-reduced-motion 时的情况:

  • alternate = 运行不同的动画
  • once = 将 animation-iteration-count 设置为 1
  • slow = 更改 animation-duration 属性
  • stop = 将 animation-play-state 设置为 paused

这些只是建议,它们实际上可以是任何你想要的东西。

<div class="circle a-slide" data-animation="alternate"></div>
<div class="circle a-slide" data-animation="once"></div>
<div class="circle a-slide" data-animation="slow"></div>
<div class="circle a-slide" data-animation="stop"></div>

更新后的媒体查询

@media (prefers-reduced-motion) {
  [data-animation="alternate"] {
   /* Change animation duration AND name */
    --animdur: 4s;
    --animn: opacity;
  }
  [data-animation="slow"] {
    /* Change animation duration */
    --animdur: 10s;
  }
  [data-animation="stop"] {
    /* Stop the animation */
    --animps: paused;
  }
}

如果这太笼统了,而你更喜欢对每个动画类都有唯一的备用动画,请像这样对选择器进行分组

.a-slide[data-animation="alternate"] { /* etc. */ }

这是一个使用复选框模拟 prefers-reduced-motion 的 Pen。在 Pen 中向下滚动以查看每个圆圈的行为变化

使用 JavaScript 暂停

要使用 JavaScript 重新创建“暂停所有动画”复选框,请遍历所有 [data-animation] 元素并切换相同的 --animps 自定义属性

<button id="js-toggle" type="button">Toggle Animations</button>
const animations = document.querySelectorAll('[data-animation');
const jstoggle = document.getElementById('js-toggle');

jstoggle.addEventListener('click', () => {
  animations.forEach(animation => {
    const running = getComputedStyle(animation).getPropertyValue("--animps") || 'running';
    animation.style.setProperty('--animps', running === 'running' ? 'paused' : 'running');
  })
});

这与复选框技巧完全相同,使用相同的自定义属性:--animps,只是由 JavaScript 而不是 CSS 设置。如果我们想要支持旧版本的浏览器,我们可以切换一个类,它将更新 animation-play-state

使用 IntersectionObserver

为了自动播放和暂停所有 [data-animation] 动画——从而避免不必要地过载 GPU——我们可以使用 IntersectionObserver

首先,我们需要确保没有任何动画正在运行

[data-animation] {
  /* Change 'running' to 'paused' */
  animation: var(--animps, paused); 
}

然后,我们将创建观察器并在元素在视窗中处于 25% 或 75% 时触发它。如果匹配到后者,动画将开始播放;否则它将暂停。

默认情况下,所有具有 [data-animation] 属性的元素都将被观察,但如果启用了 prefers-reduced-motion(设置为“reduce”),则具有 [data-animation="stop"] 的元素将被忽略。

const IO = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      const state = (entry.intersectionRatio >= 0.75) ? 'running' : 'paused';
      entry.target.style.setProperty('--animps', state);
    }
  });
}, {
  threshold: [0.25, 0.75]
});

const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
const elements = mediaQuery?.matches ? document.querySelectorAll(`[data-animation]:not([data-animation="stop"]`) : document.querySelectorAll('[data-animation]');

elements.forEach(animation => {
  IO.observe(animation);
});

你必须调整 threshold 值,以及是否需要在动画触发后取消观察某些动画等。如果你动态加载新内容或动画,你可能需要重写观察器的一些部分。不可能涵盖所有场景,但将此用作基础应该可以帮助你开始使用自动播放和暂停 CSS 动画!

奖励:使用最少的 JavaScript 将 <audio> 添加到幻灯片

这是一个将音乐添加到我们构建的幻灯片的想法。首先,添加一个 audio 标签

<audio src="/asset/audio/slideshow.mp3" hidden loop></audio>

然后,在 JavaScript 中

const audio = document.querySelector('your-audio-selector');
const details = document.querySelector('your-details-selector');
details.addEventListener('toggle', () => {
  details.open ? audio.play() : audio.pause();
})

很简单,对吧?

我在这里做了一个“无声电影”(带音频)的演示,让你了解我极客的过去。🙂