使用滚动驱动的动画,在滚动时为元素添加动画效果

了解如何使用滚动时间轴和查看时间轴,以声明方式创建滚动驱动的动画。

滚动条驱动的动画

浏览器支持

  • Chrome:115。 <ph type="x-smartling-placeholder">
  • 边缘:115。 <ph type="x-smartling-placeholder">
  • Firefox:背后有旗帜。
  • Safari:不支持。 <ph type="x-smartling-placeholder">

来源

滚动条驱动的动画是 Web 中的一种常见用户体验模式。滚动驱动的动画与滚动容器的滚动位置相关联。也就是说,当您向上或向下滚动时,关联的动画会在直接做出响应时向前或向后拖动。例如,视差背景图片或阅读指示器之类的效果会随着您滚动而移动。

<ph type="x-smartling-placeholder">
</ph>
文档上方的阅读指示器,由滚动驱动。

与滚动驱动的动画类似,这种动画会关联到元素在其滚动容器中的位置。例如,使用它时,元素可以在进入用户视野范围内时淡入。

<ph type="x-smartling-placeholder">
</ph>
此页面上的图片会在屏幕上显示时淡入。

实现这类效果的典型方式是在主线程上响应滚动事件,而这会导致两个主要问题:

  • 现代浏览器在单独的进程中执行滚动,因此异步传递滚动事件。
  • 主线程动画受卡顿影响

这使得创建与滚动同步的高性能滚动驱动动画变得不可能或非常困难。

从 Chrome 115 版开始,您可以使用一组新的 API 和概念来启用声明式滚动驱动型动画:滚动时间轴和查看时间轴。

这些新概念与现有 Web Animations API (WAAPI)CSS Animations API 集成,使其可以继承这些现有 API 的优势。这包括能够让滚动驱动的动画在主线程之外运行。没错,读对了。现在,您只需再添加几行代码,就能获得由滚动驱动、在主线程之外运行且丝滑流畅的动画。有什么不好吃的?!

网络上的动画,简短回顾

使用 CSS 在网页上制作动画

若要在 CSS 中制作动画,请使用 @keyframes at-rule 定义一组关键帧。使用 animation-name 属性将其关联到某个元素,同时通过设置 animation-duration 来确定动画播放时长。还有更多 animation-* 长手属性(animation-easing-functionanimation-fill-mode 等)可以使用,所有这些属性都可以在 animation 简写形式中组合使用。

例如,下面的动画会在 X 轴上放大某个元素,同时更改其背景颜色:

@keyframes scale-up {
  from {
    background-color: red;
    transform: scaleX(0);
  }
  to {
    background-color: darkred;
    transform: scaleX(1);
  }
}

#progressbar {
  animation: 2.5s linear forwards scale-up;
}

使用 JavaScript 在网页上实现动画

在 JavaScript 中,可以使用 Web Animations API 实现完全相同的效果。为此,您可以创建新的 AnimationKeyFrameEffect 实例,也可以使用更短的 Element animate() 方法

document.querySelector('#progressbar').animate(
  {
    backgroundColor: ['red', 'darkred'],
    transform: ['scaleX(0)', 'scaleX(1)'],
  },
  {
    duration: 2500,
    fill: 'forwards',
    easing: 'linear',
   }
);

上述 JavaScript 代码段的此可视化结果与之前的 CSS 版本完全相同。

动画时间轴

默认情况下,附加到元素的动画在文档时间轴上运行。其初始时间在网页加载时从 0 开始,并随着时钟时间的流逝而开始向前倒流。这是默认的动画时间轴,并且到目前为止,这是您有权访问的唯一动画时间轴。

滚动驱动的动画规范定义了您可以使用的两种新型时间轴:

  • 滚动进度时间轴:与滚动容器沿特定轴的滚动位置相关联的时间轴。
  • 查看进度时间轴:与特定元素在其滚动容器中相对位置的时间轴。

滚动进度时间轴

滚动进度时间轴是一种动画时间轴,它与滚动容器(也称为“scrollport”或“滚动条”)沿特定轴的滚动位置进度相关联。它会将滚动范围内的位置转换为进度百分比。

开始滚动位置表示进度为 0%,滚动结束位置表示进度为 100%。在下面的可视化图表中,您可以看到,随着滚动条从上到下滚动,进度从 0% 增加到 100%。

<ph type="x-smartling-placeholder">
</ph>
滚动进度时间轴的可视化。当您向下滚动到滚动条的底部时,进度值会从 0% 增加到 100%。

✨ 亲自尝试一下

滚动进度时间线通常缩写为“滚动时间线”。

查看进度时间轴

这种类型的时间轴与滚动容器中特定元素的相对进度相关联。与滚动进度时间轴一样,系统还会跟踪滚动条的滚动偏移量。与滚动进度时间轴不同的是,进度是由正文在该滚动条中的相对位置决定的。

这与 IntersectionObserver 的工作原理大致类似,后者可以跟踪元素在滚动条中的可见程度。如果该元素在滚动条中不可见,则不会交叉。如果它在滚动条内可见(即使是最小部分),则会交叉。

查看进度时间轴从主题开始与滚动条相交开始,到主题停止与滚动条交叉时结束。在下面的可视化图表中,您可以看到,当对象进入滚动容器时,进度从 0% 开始计数,在对象离开滚动容器的那一刻计数到 100%。

<ph type="x-smartling-placeholder">
</ph>
查看进度时间轴的可视化。当主题(绿色框)越过滚动条时,进度会从 0% 增加到 100%。

✨ 亲自尝试一下

“查看进度时间轴”通常简称为“查看时间轴”。您可以根据正文的大小来定位 View Timeline 的特定部分,但稍后会进行更多定位。

掌握滚动进度时间轴的实际应用

在 CSS 中创建匿名滚动进度时间轴

若要在 CSS 中创建滚动时间轴,最简单的方法是使用 scroll() 函数。这将创建一个匿名滚动时间轴,您可以将其设为新 animation-timeline 属性的值。

示例:

@keyframes animate-it { … }

.subject {
  animation: animate-it linear;
  animation-timeline: scroll(root block);
}

scroll() 函数接受 <scroller><axis> 参数。

<scroller> 参数接受的值如下:

  • nearest:使用最近的祖先滚动容器(默认)
  • root:将文档视口用作滚动容器。
  • self:将元素本身用作滚动容器。

<axis> 参数接受的值如下:

  • block:使用滚动容器的块轴上的进度衡量(默认)
  • inline:用于衡量沿滚动容器内嵌轴的进度。
  • y:用于衡量沿滚动容器 y 轴的进度。
  • x:用于衡量沿滚动容器 x 轴的进度。

例如,如需将动画绑定到块轴上的根滚动条,要传入 scroll() 的值为 rootblock。加起来,该值为 scroll(root block)

演示:朗读进度指示器

此演示版视口顶部有一个固定的阅读进度指示器。在页面中向下滚动时,进度条会不断增大,直到达到文档末尾并占据整个视口宽度为止。匿名滚动进度时间轴用于驱动动画。

<ph type="x-smartling-placeholder">
</ph>
演示:朗读进度指示器

✨ 亲自尝试一下

使用“位置固定”功能,阅读进度指示器位于页面顶部。为了利用合成动画,不是为 width 添加动画效果,而是使用 transform 在 x 轴上缩小元素。

<body>
  <div id="progress"></div>
  …
</body>
@keyframes grow-progress {
  from { transform: scaleX(0); }
  to { transform: scaleX(1); }
}

#progress {
  position: fixed;
  left: 0; top: 0;
  width: 100%; height: 1em;
  background: red;

  transform-origin: 0 50%;
  animation: grow-progress auto linear;
  animation-timeline: scroll();
}

#progress 元素上的动画 grow-progress 的时间轴设置为使用 scroll() 创建的匿名时间轴。scroll() 没有提供任何参数,因此将回退到其默认值。

要跟踪的默认滚动条是 nearest,默认轴是 block。这样可以有效地定位根滚动条,因为它是 #progress 元素最近的滚动条,同时跟踪其阻止方向。

在 CSS 中创建命名的滚动进度时间轴

定义滚动进度时间轴的另一种方法是使用已命名的时间轴。这种方法比较冗长,但在以下情况下,它可能会派上用场:您不是以父滚动条或根滚动条为目标,或者页面使用多个时间轴或自动查找功能不起作用。这样,您就可以通过您指定的名称来识别滚动进度时间轴。

如需在元素上创建已命名的滚动进度时间轴,请将滚动容器上的 scroll-timeline-name CSS 属性设置为您喜欢的标识符。该值必须以 -- 开头。

如需调整要跟踪的轴,请同时声明 scroll-timeline-axis 属性。允许的值与 scroll()<axis> 实参相同。

最后,如需将动画关联到滚动进度时间轴,请为需要添加动画效果的元素设置 animation-timeline 属性,使其值与用于 scroll-timeline-name 的标识符相同。

代码示例:

@keyframes animate-it { … }

.scroller {
  scroll-timeline-name: --my-scroller;
  scroll-timeline-axis: inline;
}

.scroller .subject {
  animation: animate-it linear;
  animation-timeline: --my-scroller;
}

如果需要,您可以在 scroll-timeline 简写形式中组合 scroll-timeline-namescroll-timeline-axis。例如:

scroll-timeline: --my-scroller inline;

此演示版在每个图片轮播界面上方显示了一个步骤指示器。如果轮播界面包含三张图片,则指示器栏的宽度为 33%,表示您当前正在查看三张图片中的一幅。当最后一张图片出现在视图中时(由滚动条滚动到末尾处确定),指示器会占据滚动条的整个宽度。命名的滚动进度时间轴用于驱动动画。

<ph type="x-smartling-placeholder">
</ph>
演示:水平轮播步骤指示器

✨ 亲自尝试一下

图库的基本标记如下所示:

<div class="gallery" style="--num-images: 2;">
  <div class="gallery__scrollcontainer">
    <div class="gallery__progress"></div>
    <div class="gallery__entry">…</div>
    <div class="gallery__entry">…</div>
  </div>
</div>

.gallery__progress 元素绝对位于 .gallery 封装容器元素中。其初始大小由 --num-images 自定义属性决定。

.gallery {
  position: relative;
}


.gallery__progress {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 1em;
  transform: scaleX(calc(1 / var(--num-images)));
}

.gallery__scrollcontainer 会水平布局包含的 .gallery__entry 元素,并且是会滚动的元素。通过跟踪其滚动位置,.gallery__progress 会获得动画效果。通过引用已命名的滚动进度时间轴 --gallery__scrollcontainer 可以做到这一点。

@keyframes grow-progress {
  to { transform: scaleX(1); }
}

.gallery__scrollcontainer {
  overflow-x: scroll;
  scroll-timeline: --gallery__scrollcontainer inline;
}
.gallery__progress {
  animation: auto grow-progress linear forwards;
  animation-timeline: --gallery__scrollcontainer;
}

使用 JavaScript 创建滚动进度时间轴

如需使用 JavaScript 创建滚动时间轴,请创建 ScrollTimeline 类的新实例。传入包含您要跟踪的 sourceaxis 的属性包。

  • source:对您要跟踪的滚动条所在元素的引用。使用 document.documentElement 定位到根滚动条。
  • axis:确定要跟踪的轴。与 CSS 变体类似,接受的值包括 blockinlinexy
const tl = new ScrollTimeline({
  source: document.documentElement,
});

如需将其附加到网页动画,请将其作为 timeline 属性传入,并忽略任何 duration(如果有)。

$el.animate({
  opacity: [0, 1],
}, {
  timeline: tl,
});

演示:朗读进度指示器,已重温

要使用 JavaScript 使用相同的标记重新创建阅读进度指示器,请使用以下 JavaScript 代码:

const $progressbar = document.querySelector('#progress');

$progressbar.style.transformOrigin = '0% 50%';
$progressbar.animate(
  {
    transform: ['scaleX(0)', 'scaleX(1)'],
  },
  {
    fill: 'forwards',
    timeline: new ScrollTimeline({
      source: document.documentElement,
    }),
  }
);

视觉效果在 CSS 版本中是相同的:创建的 timeline 会跟踪根滚动条,并在您滚动页面时在 x 轴上将 #progress 从 0% 放大到 100%。

✨ 亲自尝试一下

掌握查看进度时间轴的实际应用

在 CSS 中创建匿名视图进度时间轴

如需创建查看进度时间轴,请使用 view() 函数。它接受的参数为 <axis><view-timeline-inset>

  • <axis> 与滚动进度时间轴相同,它定义了要跟踪的轴。默认值为 block
  • 借助 <view-timeline-inset>,您可以指定偏移量(正值或负值),以便在元素被视为可见或不可见时调整边界。该值必须是百分比或 auto,默认值为 auto

例如,如需将动画绑定到与其滚动条在块轴上与其滚动条相交的元素,请使用 view(block)。与 scroll() 类似,将此属性设置为 animation-timeline 属性的值,并且不要忘记将 animation-duration 设置为 auto

使用以下代码,当您滚动时,每个 img 都会在跨越视口时淡入。

@keyframes reveal {
  from { opacity: 0; }
  to { opacity: 1; }
}

img {
  animation: reveal linear;
  animation-timeline: view();
}

Intermezzo:查看时间轴范围

默认情况下,关联到“查看时间轴”的动画会附加到整个时间轴范围。此过程从对象即将进入滚动端口的那一刻开始,到对象完全离开滚动端口时结束。

您还可以通过指定应附加到的范围,将该视图链接到视图时间轴的特定部分。例如,这可以是仅当主题进入滚动条时的情况。在下面的可视化图表中,当主题进入滚动容器时,进度会从 0% 开始计数,但从完全交叉的那一刻起,进度便达到 100%。

<ph type="x-smartling-placeholder">
</ph>
设置为跟踪主题的条目范围的视图时间轴。仅当主题进入滚动端口时,动画才会运行。

可供定位的数据视图时间轴范围如下:

  • cover:表示查看进度时间轴的完整范围。
  • entry:表示主账号框进入查看进度可见性范围的范围。
  • exit:表示主账号框退出查看进度可见性范围的范围。
  • entry-crossing:表示主框跨越末端边框的范围。
  • exit-crossing:表示主框跨越起始边框边缘的范围。
  • contain:表示主框完全包含在滚动端口中或完全覆盖其视图进度可见性范围的范围。这取决于主题比滚动条更高还是更短。

如需定义范围,您必须设置范围起始值和范围结束值。每个参数都包含范围名称(参见上文中的列表)和范围偏移量,用于确定该范围名称中的位置。范围偏移量通常是从 0%100% 的百分比,但您也可以指定固定长度,例如 20em

例如,如果您想从正文进入的那一刻起运行动画,请选择 entry 0% 作为范围起始值。如需在输入对象之前完成任务,请选择 entry 100% 作为范围结束值。

在 CSS 中,您可以使用 animation-range 属性进行设置。示例:

animation-range: entry 0% entry 100%;

在 JavaScript 中,使用 rangeStartrangeEnd 属性。

$el.animate(
  keyframes,
  {
    timeline: tl,
    rangeStart: 'entry 0%',
    rangeEnd: 'entry 100%',
  }
);

使用下面嵌入的工具查看每个范围名称所代表的含义以及百分比对起始位置和结束位置的影响。尝试将范围起始值设为 entry 0%,将范围结束值设为 cover 50%,然后拖动滚动条以查看动画结果。

<ph type="x-smartling-placeholder">
</ph>
View Timeline Ranges 可视化工具,位于 https://goo.gle/view-timeline-range-tool

观看录制内容

正如您在玩转此“查看时间轴范围”工具时,您可能会注意到,某些范围可以通过两种不同的“范围名称 + 范围偏移”组合来定位。例如,entry 0%entry-crossing 0%cover 0% 均定位到同一区域。

当范围起始值和范围结束值定位到相同的范围名称并跨越整个范围(从 0% 到 100%)时,您可以将该值缩短为只是范围名称。例如,可以将 animation-range: entry 0% entry 100%; 重写为更短的 animation-range: entry

演示:图片显示

此演示版图片会在图片进入滚动端口时淡入。此操作是使用匿名视图时间轴来实现的。调整了动画范围,使每张图片在滚动至一半时都处于完全不透明度。

<ph type="x-smartling-placeholder">
</ph>
演示:图片显示

✨ 亲自尝试一下

展开效果是通过使用动画裁剪路径来实现的。用于该效果的 CSS 如下所示:

@keyframes reveal {
  from { opacity: 0; clip-path: inset(0% 60% 0% 50%); }
  to { opacity: 1; clip-path: inset(0% 0% 0% 0%); }
}

.revealing-image {
  animation: auto linear reveal both;
  animation-timeline: view();
  animation-range: entry 25% cover 50%;
}

在 CSS 中创建命名的查看进度时间轴

与滚动时间轴具有命名版本的方式类似,您也可以创建命名的“查看时间轴”。您将使用带有 view-timeline- 前缀的变体(即 view-timeline-nameview-timeline-axis),而不是 scroll-timeline-* 属性。

系统会应用相同类型的值,以及查询已命名时间轴的相同规则。

演示:显示图片,重新查看

重新处理之前的图片显示演示,修改后的代码如下所示:

.revealing-image {
  view-timeline-name: --revealing-image;
  view-timeline-axis: block;

  animation: auto linear reveal both;
  animation-timeline: --revealing-image;
  animation-range: entry 25% cover 50%;
}

使用 view-timeline-name: revealing-image 时,系统将在其最近的滚动条中跟踪元素。然后,系统会将相同的值用作 animation-timeline 属性的值。视觉输出与之前完全相同。

✨ 亲自尝试一下

在 JavaScript 中创建视图进度时间轴

如需使用 JavaScript 创建视图时间轴,请创建 ViewTimeline 类的新实例。传入包含要跟踪的 subjectaxisinset 的属性包。

  • subject:对您要在自己的滚动条中跟踪的元素的引用。
  • axis:要跟踪的轴。与 CSS 变体类似,接受的值包括 blockinlinexy
  • inset:确定框是否位于视图中时,滚动端口的边衬区(正)或边衬区(负)调整。
const tl = new ViewTimeline({
  subject: document.getElementById('subject'),
});

如需将其附加到网页动画,请将其作为 timeline 属性传入,并忽略任何 duration(如果有)。(可选)使用 rangeStartrangeEnd 属性传入范围信息。

$el.animate({
  opacity: [0, 1],
}, {
  timeline: tl,
  rangeStart: 'entry 25%',
  rangeEnd: 'cover 50%',
});

✨ 亲自尝试一下

更多值得一试的功能

使用一组关键帧附加到多个视图时间轴范围

我们来看这个联系人列表演示,其中列表条目以动画形式呈现。当列表条目从底部进入滚动端口时,它会滑入并淡入,当退出顶部的滚动端口时,则滑动并淡出。

<ph type="x-smartling-placeholder">
</ph>
演示:联系人列表

✨ 亲自尝试一下

在本演示中,每个元素都用一个 View Timeline 进行装饰,它会在元素越过其滚动端口时跟踪它,但还附加到了两个滚动驱动的动画。animate-in 动画会附加到时间轴的 entry 范围,而 animate-out 动画会附加到时间轴的 exit 范围。

@keyframes animate-in {
  0% { opacity: 0; transform: translateY(100%); }
  100% { opacity: 1; transform: translateY(0); }
}
@keyframes animate-out {
  0% { opacity: 1; transform: translateY(0); }
  100% { opacity: 0; transform: translateY(-100%); }
}

#list-view li {
  animation: animate-in linear forwards,
             animate-out linear forwards;
  animation-timeline: view();
  animation-range: entry, exit;
}

您也可以创建一组已包含范围信息的关键帧,而无需运行附加到两个不同范围的两个不同动画。

@keyframes animate-in-and-out {
  entry 0%  {
    opacity: 0; transform: translateY(100%);
  }
  entry 100%  {
    opacity: 1; transform: translateY(0);
  }
  exit 0% {
    opacity: 1; transform: translateY(0);
  }
  exit 100% {
    opacity: 0; transform: translateY(-100%);
  }
}

#list-view li {
  animation: linear animate-in-and-out;
  animation-timeline: view();
}

由于关键帧包含范围信息,因此您无需指定 animation-range。结果与之前完全相同。

✨ 亲自尝试一下

附加到非祖先滚动时间轴

已命名的滚动时间轴和命名视图时间轴的查询机制仅限于滚动祖先实体。然而,很多情况下,需要设置动画效果的元素并不是需要跟踪的滚动条的子级。

为此,可以使用 timeline-scope 属性。您可以使用此属性来声明具有该名称的时间轴,而无需实际创建它。这样一来,具有该名称的时间轴就会范围更广。实际上,您可以对共享的父元素使用 timeline-scope 属性,以便子滚动条的时间轴可以附加到该元素上。

例如:

.parent {
  timeline-scope: --tl;
}
.parent .scroller {
  scroll-timeline: --tl;
}
.parent .scroller ~ .subject {
  animation: animate linear;
  animation-timeline: --tl;
}

在此代码段中:

  • .parent 元素声明一个名为 --tl 的时间轴。它的任何子级都可以找到它并将其用作 animation-timeline 属性的值。
  • .scroller 元素实际上定义了一个名为 --tl 的滚动时间轴。默认情况下,它仅对其子级可见,但由于 .parent 已将其设置为 scroll-timeline-root,因此它会附加到它。
  • .subject 元素使用 --tl 时间轴。它会沿着其祖先树遍历,并在 .parent 上找到 --tl。由于 .parent 上的 --tl 指向 .scroller--tl,因此 .subject 实质上会跟踪 .scroller 的滚动进度时间轴。

换句话说,您可以使用 timeline-root 将时间轴向上移动至祖先实体(也称为提升),以便祖先实体的所有子项都可以访问时间轴。

timeline-scope 属性可与滚动时间轴和视图时间轴一起使用。

更多演示和资源

本文中介绍的所有演示(针对 scroll-driven-animations.style mini-site)进行了说明。该网站提供更多演示,以突出滚动驱动的动画的强大功能。

此专辑封面列表就是另一个演示。每个封面都会随着焦点在中心的聚光灯下旋转 3D。

<ph type="x-smartling-placeholder">
</ph>
演示:翻唱流程

✨ 亲自尝试一下

或者,这是利用 position: sticky 的堆叠卡片演示。随着卡片堆叠在一起,已经卡住的卡片会按比例缩小,从而营造出一种不错的深度效果。最后,整个堆栈以一组的形式滑出视图。

<ph type="x-smartling-placeholder">
</ph>
演示:堆叠卡片

✨ 亲自尝试一下

scroll-driven-animations.style 也提供了一些工具,例如本博文前面部分介绍的“查看时间轴范围进度”可视化功能。

滚动条驱动的动画也在 2023 年 Google I/O 大会的 Web 动画的新变化中进行了介绍。