高效视差

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

总结

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

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