高效视差

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;
}

不过,对于 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 代码库中找到该库。

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