高效视差

Paul Lewis
Robert Flack
Robert Flack

无论爱是爱还是讨厌,视差功能都会持续下去。若能谨慎使用,就能让 Web 应用更显深度和微妙。不过,问题在于,以高性能方式实现视差可能颇具挑战性。在本文中,我们将讨论一个不仅性能出色,而且同样重要的是可跨浏览器运行的解决方案。

视差图示。

要点

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

如果您需要普适性解决方案,请前往 UI 元素示例 GitHub 代码库,获取视差辅助程序 JS! 您可以在 GitHub 代码库中查看视差滚动条的实时演示

问题视差器

首先,我们来看一下实现视差效果的两种常见方法,尤其是它们为什么不适合我们的目的。

坏:使用滚动事件

视差的关键要求是滚动耦合;对于页面滚动位置的每一次更改,视差元素的位置都应更新。虽然这听起来很简单,但现代浏览器的一个重要机制是异步工作的能力。这适用于滚动事件(在特定情况下)。在大多数浏览器中,滚动事件都是“尽力而为”传送的,不能保证会在滚动动画的每一帧中传送!

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

错误:正在更新“background-position

我们要避免的另一种情况是在每一帧上绘制。许多解决方案会尝试通过更改 background-position 来提供视差外观,这会导致浏览器在滚动时重新绘制页面中受影响的部分,而这会产生高昂的成本,导致动画出现明显卡顿。

如果我们想要实现视差运动的承诺,那么我们需要一些可以作为加速属性(如今意味着坚持使用转换和不透明度)且不依赖于滚动事件的特性。

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。这意味着缩放比例为 (perspective - 0) / perspective,其值为 1,这意味着它既不调高也不缩小。真的非常方便。

此方法的工作原理

必须要明确它为什么能发挥作用,因为我们稍后会运用这一知识。滚动实际上是一种转换,这就是它能够加速的原因;它主要涉及到随 GPU 移动层。在没有任何角度概念的典型滚动中,滚动在比较滚动元素及其子项时以 1 对 1 的方式发生。如果您将某个元素向下滚动 300px,则其子元素将向上转换相同的量:300px

不过,对滚动元素应用透视值会使这一过程混淆;因为这会更改支持滚动转换的矩阵。现在,根据您选择的 perspectivetranslateZ 值,300 像素的滚动可能只能将子元素移动 150 像素。如果某个元素的 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 的形式有一些帮助,它的存在可让元素在滚动期间“固定”在视口顶部或给定的父元素上。与大多数规范一样,该规范相当庞大,但包含以下实用小提示:

乍一看,这可能并不意味着很大,但该语句的一个关键点是,它指的是如何确切地计算元素粘度的方式:“偏移是参考最近的祖先实体(带滚动框)计算的”。换句话说,移动粘性元素的距离(为了让粘性元素显示在其他元素或视口中而移动的距离)是在应用任何其他转换之前(而不是之后)计算的。这意味着,与前面的滚动示例非常相似,如果将偏移量计算为 300 像素,则可以利用视角(或任何其他转换)在 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);
}

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

粘性定位注意事项

不过,这两者之间有一个区别:position: sticky 会改变视差机制。粘性定位会尝试将元素粘在滚动容器上,而非粘性版本则不会。这意味着,具有粘性的视差最终与没有以下特征的视差相反:

  • 如果使用 position: sticky,元素移动的距离越接近 z=0,移动越小。
  • 如果没有 position: sticky,元素越接近 z=0,它就会移动得越多。

如果这一切看起来有点抽象,请查看 Robert Flack 的这个演示,它演示了元素在使用和不使用粘性定位时的行为方式。要查看差异,您需要使用 Chrome Canary(在撰写本文时,版本为 56)或 Safari。

视差透视屏幕截图

Robert Flack 的演示,其中展示了 position: sticky 对视差滚动的影响。

各种错误和解决方法

但和任何事物一样,仍有一些肿块和隆起需要平滑处理:

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

总结

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

来玩一把,告诉我们您的进展情况。