高效视差

Paul Lewis
Robert Flack
Robert Flack

无论您喜不喜欢,视差效果已成为一种常态。谨慎使用时,它可以为 Web 应用增添深度和细腻感。不过,问题在于,以高性能的方式实现平移效果可能具有挑战性。在本文中,我们将讨论一种既高效又可跨浏览器使用的解决方案。

视差插图。

要点

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

如果您想要使用插入式解决方案,请前往 界面元素示例 GitHub 代码库,获取 Parallax 辅助 JS! 您可以在 GitHub 代码库中查看平行视差滚动条的实时演示

问题视差

首先,我们来看看实现视差效果的两种常用方法,尤其是它们不适合我们的用途的原因。

错误示例:使用滚动事件

Parallax 的关键要求是它应与滚动相关联;对于网页滚动位置的每次更改,Parallax 元素的位置都应更新。虽然这听起来很简单,但现代浏览器的一项重要机制是能够异步工作。在我们的具体示例中,这适用于滚动事件。在大多数浏览器中,滚动事件是“尽力而为”地传送的,并不能保证在滚动动画的每个帧中都会传送!

这条重要信息告诉我们,为什么需要避免使用基于 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 值的任何内容,您可以替换为值为零。这意味着,缩放比例为 (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;
}

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

总结

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

请试用一下,然后告诉我们您的进展。