高效视差

无论您喜欢与否,视差效果都将继续存在。如果使用得当,它可以为 Web 应用增添深度和微妙感。不过,以高性能方式实现视差效果可能颇具挑战性。在本文中,我们将讨论一种既高效又(同样重要的是)可跨浏览器使用的解决方案。

视差插图。

要点

  • 请勿使用滚动事件或 background-position 来创建视差动画。
  • 使用 CSS 3D 转换来创建更准确的视差效果。
  • 对于 Mobile Safari,请使用 position: sticky 以确保视差效果得到传播。

如果您想要即插即用的解决方案,请前往 UI 元素示例 GitHub 代码库,然后获取 Parallax 辅助 JS!您可以在 GitHub 代码库中查看视差滚动器的实时演示

问题视差

首先,我们来看看实现视差效果的两种常见方式,特别是它们为何不适合我们的用途。

反面示例:使用滚动事件

视差效果的关键要求是它应与滚动相关联;对于网页滚动位置的每一次变化,视差元素的位置都应更新。虽然这听起来很简单,但现代浏览器的一个重要机制是能够异步工作。在我们的特定情况下,这适用于滚动事件。在大多数浏览器中,滚动事件是“尽力而为”的,无法保证在滚动动画的每一帧中都传递。

这项重要信息告诉我们,为什么需要避免使用基于 JavaScript 的解决方案来根据滚动事件移动元素:JavaScript 无法保证视差效果与网页的滚动位置保持同步。在旧版 Mobile Safari 中,滚动事件实际上是在滚动结束时传递的,这使得无法实现基于 JavaScript 的滚动效果。较新版本确实会在动画期间传递滚动事件,但与 Chrome 类似,也是尽力而为。如果主线程正忙于处理任何其他工作,滚动事件将不会立即传递,这意味着视差效果将丢失。

错误:正在更新 background-position

我们还希望避免在每个帧上都进行绘制。许多解决方案试图更改 background-position 以提供视差效果,这会导致浏览器在滚动时重新绘制页面的受影响部分,而这可能会非常耗费资源,导致动画出现明显的卡顿。

如果我们想实现视差运动效果,就需要一种可以作为加速属性(目前这意味着坚持使用 transform 和 opacity)应用,并且不依赖于滚动事件的属性。

3D CSS

Scott KellumKeith Clark 在使用 CSS 3D 实现视差运动方面都做了大量工作,他们使用的技术实际上是:

  • 设置一个包含元素,使其通过 overflow-y: scroll(可能还有 overflow-x: hidden)滚动。
  • 对同一元素应用 perspective 值,并将 perspective-origin 设置为 top left0 0
  • 对相应元素的子元素应用 Z 轴平移,并将其缩放回原样,以提供视差运动,而不会影响其在屏幕上的大小。

此方法的 CSS 如下所示:

.container {
  width: 100%;
  height: 100%;
  overflow-x: hidden;
  overflow-y: scroll;
  perspective: 1px;
  perspective-origin: 0 0;
}

.parallax-child {
  transform-origin: 0 0;
  transform: translateZ(-2px) scale(3);
}

假设 HTML 代码段如下所示:

<div class="container">
    <div class="parallax-child"></div>
</div>

调整透视比例

将子元素向后推会使其按透视值比例缩小。您可以使用以下公式计算需要放大多少倍:(透视 - 距离)/ 透视。由于我们很可能希望视差元素产生视差效果,但以我们创作时的大小显示,因此需要以这种方式放大该元素,而不是保持原样。

在上述代码中,透视效果为 1pxparallax-child 的 Z 距离为 -2px。这意味着,元素需要放大 3 倍,您可以看到,这是插入到代码中的值:scale(3)

对于未应用 translateZ 值的任何内容,您都可以替换为零值。这意味着缩放比例为(透视 - 0)/ 透视,结果为 1,表示未放大或缩小。非常实用。

此方法的运作方式

务必清楚了解这种方法为何有效,因为我们很快就会用到相关知识。滚动实际上是一种转换,因此可以加速;它主要涉及使用 GPU 移动图层。在没有任何透视概念的典型滚动中,滚动元素及其子元素的滚动方式是 1:1 的。如果您将某个元素向下滚动 300px,则其子元素会向上转换相同的量:300px

不过,将透视值应用于滚动元素会扰乱此过程;它会更改作为滚动转换基础的矩阵。现在,滚动 300 像素可能只会使子元素移动 150 像素,具体取决于您选择的 perspectivetranslateZ 值。如果元素的 translateZ 值为 0,则会以 1:1 的比例滚动(与之前一样),但沿 Z 轴方向从透视原点向外推送的子元素将以不同的速率滚动!最终效果:视差运动。非常重要的一点是,这是作为浏览器内部滚动机制的一部分自动处理的,这意味着无需监听 scroll 事件或更改 background-position

美中不足:移动版 Safari

每种效果都有注意事项,对于转换而言,一个重要的注意事项是关于保留对子元素的 3D 效果。如果层次结构中具有透视效果的元素与其视差子元素之间存在其他元素,则 3D 透视效果会“变平”,也就是说效果会消失。

<div class="container">
    <div class="parallax-container">
    <div class="parallax-child"></div>
    </div>
</div>

在上面的 HTML 中,.parallax-container 是新添加的,它会有效地使 perspective 值变平,从而失去视差效果。在大多数情况下,解决方案非常简单:您只需向元素添加 transform-style: preserve-3d,即可使其传播已应用于树中更上层的任何 3D 效果(例如我们的透视值)。

.parallax-container {
  transform-style: preserve-3d;
}

不过,对于移动版 Safari,情况会稍微复杂一些。 从技术上讲,将 overflow-y: scroll 应用于容器元素是可行的,但代价是无法快速滑动滚动元素。解决方案是添加 -webkit-overflow-scrolling: touch,但它也会使 perspective 变平,并且我们不会获得任何视差效果。

从渐进增强的角度来看,这可能不是什么大问题。即使在所有情况下都无法实现视差效果,我们的应用仍然可以正常运行,但最好还是找到一种解决方法。

position: sticky来助你一臂之力!

实际上,position: sticky 可以提供一些帮助,它允许元素在滚动期间“粘附”到视口顶部或给定的父元素。与大多数规范一样,该规范相当庞大,但其中包含一个有用的亮点:

乍一看,这句话似乎没什么特别之处,但其中的关键点在于它提到了如何准确计算元素的粘滞性:“偏移量是参照具有滚动框的最近祖先计算的”。换句话说,移动粘性元素的距离(使其看起来附着在另一个元素或视口上)是在应用任何其他转换之前计算的,而不是之后。这意味着,与之前的滚动示例非常相似,如果在 300 像素处计算出偏移量,那么在将该偏移量值应用于任何粘性元素之前,您有机会使用透视(或任何其他转换)来操纵该偏移量值。

通过将 position: -webkit-sticky 应用于视差元素,我们可以有效地“反转”-webkit-overflow-scrolling: touch 的扁平化效果。这样可确保视差元素引用具有滚动框的最近祖先,在本例中为 .container。然后,与之前类似,.parallax-container 应用 perspective 值,这会更改计算出的滚动偏移量并创建视差效果。

<div class="container">
    <div class="parallax-container">
    <div class="parallax-child"></div>
    </div>
</div>
.container {
  overflow-y: scroll;
  -webkit-overflow-scrolling: touch;
}

.parallax-container {
  perspective: 1px;
}

.parallax-child {
  position: -webkit-sticky;
  top: 0px;
  transform: translate(-2px) scale(3);
}

这会恢复移动版 Safari 的视差效果,这绝对是个好消息!

粘性定位注意事项

不过,这里存在一个区别:position: sticky 改变视差机制。粘性定位会尝试将元素粘附到滚动容器,而非粘性版本则不会。这意味着,具有粘性结束位置的视差最终会成为没有粘性结束位置的视差的逆视差:

  • 对于 position: sticky,元素越靠近 z=0,移动的距离就越
  • 如果没有 position: sticky,元素越靠近 z=0,移动的距离就越远

如果上述内容看起来有点抽象,不妨观看 Robert Flack 制作的这个演示,其中展示了元素在采用和未采用粘性定位时的不同行为。如需查看差异,您需要使用 Chrome Canary(撰写本文时为第 56 版)或 Safari。

视差透视效果屏幕截图

Robert Flack 演示position: sticky 如何影响视差滚动。

各种 bug 和解决方法

不过,与任何事物一样,仍有一些需要解决的问题:

  • 粘性支持不一致。Chrome 仍在实现支持,Edge 完全不支持,而 Firefox 在将 sticky 与透视转换相结合时存在绘制 bug。在这种情况下,值得添加少量代码,以便仅在需要时(即仅针对移动版 Safari)添加 position: sticky(带有 -webkit- 前缀的版本)。
  • 该效果在 Edge 中无法“正常运行”。Edge 尝试在操作系统级别处理滚动,这通常是一件好事,但在这种情况下,它会阻止 Edge 检测滚动期间的透视变化。如需解决此问题,您可以添加一个固定位置元素,因为这似乎会将 Edge 切换为非操作系统滚动方法,并确保其考虑透视变化。
  • “网页内容突然变大了!”许多浏览器在决定网页内容的大小的时候都会考虑缩放比例,但遗憾的是,Chrome 和 Safari 不会考虑透视效果。因此,如果某个元素应用了 3 倍的缩放比例,即使在应用 perspective 后该元素处于 1 倍的缩放比例,您也可能会看到滚动条等。可以通过从右下角缩放元素 (transform-origin: bottom right) 来解决此问题,因为这样会导致过大的元素扩展到可滚动区域的“负区域”(通常是左上角);可滚动区域永远不会让您看到或滚动到负区域中的内容。

总结

如果使用得当,视差效果会非常有趣。如您所见,我们可以以高性能、滚动耦合且跨浏览器的方式实现它。由于它需要进行一些数学运算,并且需要少量样板代码才能获得所需的效果,因此我们封装了一个小型辅助库和示例,您可以在我们的 UI 元素示例 GitHub 代码库中找到它们。

试用一下,然后告诉我们您的体验。