滚动驱动动画

见过太多随着网页文档,随着滚动下滑文档包含某些进度条, 或者某些产品的官网页, 包含图片缩放平移旋转的效果. 很典型的rspack文档,蓝湖,南孚国际,webflow 等. 不论他们是否使用js, 都存在随着滚动产生相对应的动画效果. 网页档次就上来了

参考资料

  1. scroll-driven animations - MDN
  2. 滚动驱动动画 - MDN
  3. Scroll-driven animation timelines - MDN
  4. 滚动驱动动画案例

前言

滚动驱动动画是2023年提出并被大部分主流浏览器所支持的. 除了滚动驱动动画, 在实际使用中可能还需要结合 滚动吸附1, 容器查询2(特别是容器状态查询).

在阅读本文前, 最好具备 css 动画 的知识储备. 因为滚动驱动动画是对 css 动画 模块的一个拓展.

什么是滚动驱动动画

css 滚动驱动动画模块定义了使 css 动画关键帧 随文档滚动进度相关联的一组属性.

传统上的css动画是随着现实世界的时间流逝自动播放的. css 驱动动画最大的不同, 就是将动画的播放进度和滚动进度相互关联.

当滚动停止时, 动画播放进度也会停止, 类似于设置了css规则 animation-play-state: pause.

传统上, 要使滚动进度和动画关联需要结合 scroll js事件, 并结合复杂计算实现动画的播放进度. 任何时候,如果依赖主线程来渲染 JavaScript 效果,都存在阻塞主线程的风险,这会导致页面无响应、用户体验不佳,甚至出现卡顿。

动画时间线

使用 animation-timeline 定义动画的时间线. 如果该属性不设置, 则默认采用现实世界的时间线, 而不是滚动进度时间线或滚动可视区域的时间线.

匿名时间线与命名时间线

animation-timeline 可以使用内置的 scroll() 函数或 view() 函数作为其值, 通过函数指定时间线的方式通常也称为匿名时间线. 除匿名时间线外, 也可以使用自定义的时间线.

自定义的时间线具有时间线名称, 属性包含 view-timelinescroll-timeline 两类, 分别与匿名时间线 view() 函数和 scroll() 函数对应.

通过将自定义的时间线名称赋值给animation-timeline属性, 可达到使用动画的元素与任意滚动元素的滚动进度关联, 以驱动元素动画.

在实际使用中可结合实际场景结合适用匿名时间线或命名时间线. 通常, 在复杂场景中都是用命名时间线.

时间线与动画帧的关系

时间线与动画帧的关联, 主要区别在是与滚动容器关联, 还是与元素在滚动容器内可视区域关联.

一类是容器的滚动时间线. 此类时间线和滚动容器的从头开始滚动, 到滚动到底部这一段距离就是滚动时间线, 即容器的滚动长度. 从容器顶部没有开始向下滚动算动画帧的from, 即动画起点, 容器完全滚动到底部不能再次滚动时, 为动画帧的to, 即终点.

一类是可见性时间线. 元素自身在滚动过程中, 出现在滚动可视区域的一段距离, 即元素在滚动容器可视区域内的滚动长度, 从元素边界沿着滚动轴开始进入区域开始, 到沿着滚动轴离开可视区域得过程为一个完整时间线. 开始进入为动画帧的from, 即起点, 离开可视区域为动画帧的to, 即终点.

不同时间线的适用场景.

第一类滚动时间线, 即基于容器的滚动长度的时间线, 一般是针对容器自身一些滚动全过程可见的元素使用的, 例如阅读进度提示元素. 而第二类滚动时间线, 即子元素出现在滚动容器可视区域的滚动长度, 一般是针对子元素自身的动画效果的, 例如动画要从开始进入可视区域触发开始动画, 在完全离开时可视区域后完成所有的动画帧, 这类动画通常是对设置子元素的注意力或表现力的需要.

滚动时间线案例

使用滚动进度时间轴,时间轴会根据可滚动元素(滚动条)从上到下(或从左到右)再返回的滚动情况而推进。默认情况下,滚动范围内的位置会转换为进度百分比——0%在开始和100%结束时分别显示。

要创建滚动进度时间线,该animation-timeline值必须引用滚动器,滚动器可以是命名的,也可以是匿名的。

命名滚动进度时间线

命名滚动进度时间轴是指使用 scroll-timeline-name 属性(或其scroll-timeline简写形式)显式命名时间线。名称是一个<dashed-ident> 值。 当声明此属性的滚动容器所在的滚动进度要与动画关联时, 通过 animation-timeline 与其scroll-timeline-name值关联即可实现滚动时间线关联.

此处的HTML包含三个元素:将为其设置动画的item、将在 scroller 中滚动的子元素 container。container 的大小需要足够大,以使其内容超出scroller, 以出现滚动条:如果没有滚动条,则不会有滚动时间线。

<main class="scroller">
  <div class="container">
    <span class="item"></span>
  </div>
</main>

我们提供了一些基本样式。其中重要的包括设置容器高度使其高于滚动条,然后设置溢出属性以允许滚动:

.scroller {
  width: 400px;
  height: 100px;
  overflow: scroll;
}
.container {
  height: 200px;
}

在动画元素item上设置与祖先元素的scroll-timeline-name相匹配的 animation-timeline ,即可创建命名滚动进度时间线。我们还必须包含一个动画,这可以通过将动画简写中的animation-name组件的值设置为关键帧动画的<custom-ident>名称来实现:

.scroller {
  scroll-timeline-name: --rotate;
}
.item {
  animation: action 1ms linear;
  animation-timeline: --rotate;
}

在这种情况下,动画进程是由溢出滚动条的滚动控制的,而滚动条不像现实世界时间那样会过期。

在滚动之前,容器位于滚动条的顶部,动画处于 0% 关键帧。尝试向下滚动。随着滚动,动画会沿着时间轴旋转 720 度。当无法继续滚动时,动画的进度到达 100% 关键帧。除非将滚动to条滚动回顶部,否则动画元素不会恢复到默认旋转状态。

你可能已经注意到,动画简写中的animation-duration组件被设置为1毫秒。在创建CSS滚动驱动的动画时,指定animation-duration值并不会影响动画的持续时间,而且也没有必要。然而,持续时间会影响非线性视图进度动画的时间线,并且Firefox需要非零的animation-duration才能将动画应用于元素。出于这些原因,通常的做法是将animation-duration设置为1毫秒。

将animation-duration设置为1毫秒可确保动画在Firefox中正常工作,使动画效果在所有浏览器中保持一致,并且如果浏览器不支持视图进度动画时间线,则动画会被隐藏。如果浏览器支持关键帧动画,则用户将看不到动画。但是,动画仍然会发生,并且会触发动画事件。

匿名滚动时间线

您不必为滚动进度时间线命名。您可以将匿名滚动进度时间线与动画关联起来。在这种情况下,animation-timeline要添加动画的元素的属性被设置为一个scroll()函数。该函数会根据您传递给它的可选参数,选择提供滚动进度时间线的滚动条以及要使用的滚动轴。其中一个参数是<scroller>定义滚动条元素与当前元素关系的关键字( nearestrootself)。另一个参数是滚动条的<axis>值(blockinliney、或 x)。

本示例使用的 CSS 与上一个示例完全相同,只是将元素animation-timeline属性设置为一个scroll()函数。此外,我们还覆盖了容器的大小以改变滚动方向.

.item {
  animation: action 1ms linear;
  animation-timeline: scroll(nearest inline);
}
.container {
  inline-size: 800px;
  block-size: 100%;
}

可见性时间线案例

您还可以根据滚动条内元素的可见性变化来推进动画——这可以通过视图进度时间线来实现。视图进度时间线不跟踪滚动容器的滚动偏移量,而是跟踪元素(称为“主体”)在滚动端口内的相对位置。动画关键帧的推进取决于主体在滚动条内的可见性。与滚动进度时间线不同,使用视图进度时间线时,您无法指定滚动条——主体 的可见性始终在其最近的祖先滚动条内跟踪。

视图进度时间轴动画仅在元素位于其滚动区域内可见时才会发生。时间轴进度从0%跟踪对象开始与滚动区域在块或行内结束边缘相交时开始。进度100%结束于对象从滚动区域在块或行内开始边缘离开滚动区域时。

由于元素通常在离开视口时就能达到100%的进度,因此你可能希望在动画结束前很早的一个关键帧块中设置动画的最终效果。你可以在20%、50%或80%的关键帧块内设置完成的效果,而不是使用to或100%的关键帧,以确保元素在动画结束时仍在视图中。

通过视图进度时间线,您可以调整视图进度的可见范围。使用view-timeline-inset(view-timeline简写的一部分)来调整何时将主题视为在视图中。默认值为auto。任何非auto inset值的效果都类似于移动滚动端口的边缘:正inset值表示向内调整,负值表示向外调整。

与滚动进度时间线类似,视图进度时间线也可以命名或保持匿名。

命名时间线

命名视图进度时间线是指通过使用view-timeline-name属性(这是view-timeline简写的一个组成部分)明确命名主题的时间线。然后,通过将<dashed-ident>名称指定为该元素animation-timeline属性的值,将该名称链接到要设置动画的元素。

使用命名视图进度时间轴,要进行动画处理的元素不必与主体元素相同。换句话说,控制时间轴的元素不必与被动画处理的元素相同。这意味着您可以根据另一个元素在其可滚动容器内的移动来控制一个元素的动画。

这里我们使用该view-timeline-name属性为元素命名,并将该元素标识为视图进度时间线的来源。然后,我们将该名称设置为该animation-timeline属性的值。

.item {
  animation: action 1ms linear;

  view-timeline-name: --a-name;
  animation-timeline: --a-name;
}

我们在 animation-time 之前应用了动画 如果animation-time之后设置animation,animation会将动画时间线重置为自动。

该动画与之前的示例略有不同,其旋转效果从动画进程的20%开始,到80%结束;这意味着元素在首次进入视野时不会处于活跃旋转状态,且会在完全消失前停止旋转。

@keyframes action {
  0%,
  20% {
    rotate: 45deg;
  }
  80%,
  100% {
    rotate: 720deg;
  }
}

匿名时间线

或者,view()可以将一个函数设置为该属性的值animation-timeline,以指定元素的动画时间线为匿名视图进度时间线。这样,元素将根据其在其最近的父滚动条内的位置进行动画播放。

该view()函数会创建一个视图时间轴。您可以使用该属性将时间轴附加到要添加动画效果的元素上animation-timeline。该函数会为选择器匹配的每个元素创建一个视图时间轴。

在这个例子中,我们再次在animation-timeline之前定义了 animation,这样时间线就不会重置。然后我们引入了一个无参view()函数。我们没有指定滚动条,因为根据定义,主题的可见性由其最近的祖先滚动条跟踪。

.item {
  animation: action 1ms linear;
  animation-timeline: view();
}

时间线附加属性

在初步了解了时间线如何与动画帧关联后, 现在可以了解对时间线(滚动区域)长度进行微调.

时间线长度分为两级, 第一级使用匿名函数(view()和scroll())定义, 此函数定义了可容器可视区域的边界. 是从滚动容器角度, 而滚动容器就是绝对的长度, 类似于动画的胶卷长度. 而还有一类是从滚动容器角度(匿名函数)无法实现的, animation-range属性用来解决滚动容器无法感知元素是否完全进入可视区域.

胶卷的长度

动画的时间线因为动画时间线的定义点不同而不同, 但是我们可以确定 通过匿名函数定义的时间线长度就是胶卷的所有长度.

模型情况下, scroll匿名函数的所定义的滚动长度等于其滚动容器的原始长度, scroll含有两个参数, 可以决定滚动容器的查找范围和滚动轴.

而 view 函数就不太一样了, 通常可以指定三个参数, 用于设置view-timeline-axisview-timeline-inset属性的值。

  • 参数数量为零或一个<axis>。如果设置,则指定动画滚动的方向轴。
  • 可以是关键字参数auto,也可以是零、一或两个<length-percentage> 值。如果设置了这些值,则它们指定滚动容器可视区域的起始偏移量和/或结束偏移量。

声明 view() 相当于调用 view(block auto),后者将 block 定义为提供时间线和滚动填充的父元素的轴,scroll-padding 通常默认为 0,作为动画在可见区域内开始和结束的边距。

view-timeline-inset 参数指定了调整滚动端口起始和结束位置的边距(若为正数)或偏移量(若为负数)。这些参数用于确定元素被视为“在视图中”的滚动位置,从而决定动画时间线的长度。换言之,动画不是从滚动端口的起始边缘开始到结束边缘结束,而是在经过边距调整后的视图起始和结束位置发生。

与滚动时间线的scroll()函数不同,view()函数中没有<scroller>参数,因为视图时间线始终在其最近的祖先滚动容器内跟踪 滚动可视区域。

在这个例子中,由于我们使用的是插入值,因此可以使用from和to关键帧选择器。

@keyframes action {
  from {
    rotate: 45deg;
  }
  to {
    rotate: 720deg;
  }
}

.item {
  animation: action 1ms linear;
  animation-timeline: view(block 20% 20%);
}

可以看到, 和之前案例中的行为是一致的. 此时的可滚动区域被滚动时间线限制了. 而不是从关键帧中限制的.

胶卷一段

在获得了胶卷的绝对时间线长度后, 可以通过 animation-range 定义在胶卷中使用哪一段放置动画, 好比之前动画内容是从第一张胶卷开始的. 现在通过 animation-range 将动画部分挪到更里面的一段中去.

其属性值: entry, exit, contain

参考资料

可见性时间线高阶用法

将声明 view-timeline 的元素作为滚动容器的观察者, 将此观察状态储存(挂载)到滚动容器之外的更大的父级之上, 比如html, 然后需要动画元素, 取得这个储存得状态得进度应用到自身动画时间线上(将名称赋值给 animation-timeline属性)

或者,可以使用animation-timeline属性来明确说明使用默认文档时间线,或者指定动画没有时间线,因此根本不应发生。

参考资料

Footnotes

  1. 滚动吸附

  2. 滚动吸附