高效视差

Paul Lewis
罗伯特·弗拉克
Robert Flack

是爱也可能讨厌,视差技术将不可或缺。如果谨慎使用,它可以为 Web 应用增加深度和精细度。不过,问题在于,以高性能方式实现视差功能可能颇具挑战性。在本文中,我们将讨论一种既高性能又(同样重要的是可跨浏览器运行)的解决方案。

视差插图。

要点

  • 请勿使用滚动事件或 background-position 制作视差动画。
  • 使用 CSS 3D 转换来制作更准确的视差效果。
  • 对于移动版 Safari,请使用 position: sticky 来确保传播视差效果。

如果您想获取普适性解决方案,请前往 UI Element Samples GitHub 代码库,并获取视差帮助程序 JS! 您可以在 GitHub 代码库中查看视差滚动条的实时演示

问题视差

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

不佳:使用滚动事件

视差的主要要求是它应该是滚动耦合的;对于页面滚动位置的每一次更改,视差元素的位置都应更新。虽然这听起来很简单,但现代浏览器的一个重要机制是它们能够异步工作。这适用于滚动事件,尤其是滚动事件。在大多数浏览器中,滚动事件都以“尽力而为”的方式传递,并不保证会在滚动动画的每一帧上都传递!

这条重要信息告诉我们,为什么我们需要避免使用基于 JavaScript 的解决方案,该解决方案会根据滚动事件移动元素:JavaScript 并不能保证视差会与页面的滚动位置保持一致。在旧版 Mobile Safari 中,滚动事件实际上是在滚动结束时传送,因此无法实现基于 JavaScript 的滚动效果。较新版本的确实会在动画播放期间提供滚动事件,但与 Chrome 类似,会“尽力而为”。如果主线程正忙于执行任何其他工作,滚动事件不会立即传递,这意味着视差效果将会丢失。

糟糕:正在更新“background-position

我们想要避免的另一种情况是,每一帧都进行绘制。许多解决方案会尝试更改 background-position 以提供视差外观,这会导致浏览器在滚动时重新绘制页面的受影响部分,并且成本可能足以导致动画出现严重卡顿。

如果想兑现视差运动的承诺,我们需要一些可以作为加速属性(现在意味着坚持变形和不透明度)并且不依赖于滚动事件的元素。

CSS 3D 效果

Scott KellumKeith Clark 在使用 CSS 3D 实现视差运动方面都做过重大工作,他们使用的技术如下:

  • 设置包含元素以使用 overflow-y: scroll(可能是 overflow-x: hidden)进行滚动。
  • 对同一元素应用 perspective 值,以及设置为 top left0 0perspective-origin
  • 为该元素的子元素应用 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>

根据透视调整比例

将子元素推回后会导致其与视角值成比例变小。您可以使用以下公式计算需要放大的比例:(perspective - 距离) / 透视。由于我们很可能希望视差元素采用视差元素,但要以我们编写时的大小显示,因此需要以这种方式进行放大,而不是保留原样。

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

对于未应用 translateZ 值的任何内容,您可以将值替换为 0。这意味着缩放比例为 (perspective - 0) / perspective,净值设为 1,表示既未增加,也没有缩小。这真的非常方便。

此方法的工作原理

您必须清楚了解其运作原理,因为我们稍后会用到这些信息。滚动实际上是一种转换,这就是它能够加速的原因;它主要涉及使用 GPU 来移动层。在没有任何透视概念的典型滚动中,在比较滚动元素及其子元素时,将按 1:1 的方式进行滚动。如果将某个元素向下滚动 300px,其子元素也会向上转换相同的量:300px

但是,将透视值应用于滚动元素会干扰该过程;这会改变支撑滚动转换的矩阵。现在,滚动 300px 可能只会将子项移动 150px,具体取决于您选择的 perspectivetranslateZ 值。如果某个元素的 translateZ 值为 0,该元素将按 1:1 的比例滚动(和以前一样),但是在 Z 轴上从透视原点推移的子项将以不同的速率滚动!最终结果:视差运动。而且,非常重要的一点是,系统会在浏览器内部滚动机制中自动处理此操作,这意味着无需监听 scroll 事件或更改 background-position

亮点:Mobile 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;
}

不过,就 Mobile Safari 来说,情况要复杂一些。从技术上讲,对容器元素应用 overflow-y: scroll 可以实现,但代价是快速滑动滚动元素。解决方案是添加 -webkit-overflow-scrolling: touch,但这也会展平 perspective,并且我们不会产生任何视差。

从渐进式增强的角度来看,这也许不是什么大问题。如果我们无法在任何情况下都实现视差效果,应用仍然可以运行,不过不妨想出一种权宜之计。

position: sticky轮到您了!

事实上,position: sticky 形式可以提供一些帮助,它允许元素在滚动期间“固定”在视口或给定父元素的顶部。与大多数规范一样,该规范相当繁琐,但它包含以下实用小技巧:

乍一看,这可能没有太大意义,但该语句中的关键点在于它如何确切地计算元素的粘性:“偏移的计算参考了具有滚动框的最近祖先实体”。换言之,系统会在应用任何其他转换之前(而不是之后)计算粘性元素的移动距离(使其看起来已附加到另一个元素或视口)。这意味着,与前面的滚动示例非常相似,如果按 300px 计算偏移值,则有新的机会使用透视(或任何其他转换)来操控 300px 偏移值,然后再将其应用于任何粘性元素。

通过将 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);
}

这会恢复 Mobile 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 将粘性与透视转换结合使用时会出现绘制错误。在这种情况下,建议添加一些代码,以便仅在需要时添加 position: sticky(带有 -webkit- 前缀的版本),这仅适用于移动 Safari。
  • 这种效果并不仅仅在 Edge 中“起作用”。Edge 会尝试在操作系统级别处理滚动,这通常是件好事,但在这种情况下,它会阻止它在滚动期间检测到视角变化。若要解决此问题,您可以添加固定位置元素,因为这似乎会将 Edge 切换为 非操作系统滚动方法,并确保它能够考虑到视角变化。
  • “网页内容变得超多了!”在决定网页内容的大小时,许多浏览器都会考虑这种规模,但遗憾的是,Chrome 和 Safari 未考虑到视角。因此,如果对某个元素应用了 3 倍的缩放,即使该元素处于应用 perspective 后的 1 倍,系统也可能会显示滚动条等。可以通过从右下角缩放元素(使用 transform-origin: bottom right)来解决此问题,此方法之所以有效,是因为这样会导致超大元素扩展到可滚动区域的“负区域”(通常是左上角)中;可滚动区域绝不会让您看到或滚动到负区域中的内容。

总结

谨慎使用视差功能是一种有趣的效果。如您所见,我们可以通过一种高性能、滚动耦合、跨浏览器的方式来实现此功能。由于需要进行一些数学调整和少量样板才能达到预期效果,因此我们封装了一个小型帮助程序库和示例,您可以在我们的界面元素示例 GitHub 代码库中找到它们。

玩玩一玩,让我们了解您的进展。