CSS Deep-Dive - 利用 matrix3d() 实现完美帧效果自定义滚动条

自定义滚动条极为罕见,这主要是因为 滚动条是网络上仅有的一小部分 无法风格(我正在看您,日期选择器)。 你可以使用 JavaScript 构建自己的代码,但成本高、低 保真度和延迟。在本文中,我们将介绍 使用非常规 CSS 矩阵构建自定义滚动条, 只需编写一些设置代码即可。

要点

你不关心小事?您只需要看一下 彩虹猫演示 获取该库?您可以在我们的 GitHub 代码库

LAM;WRA(长篇幅,数学格式;无论如何都会读)

不久之前,我们开发了视差滚动条(您读过 这篇文章? 非常棒,值得您花时间学习!)。使用 CSS 3D 将元素推回原位 所以元素的移动速度比实际滚动速度

回顾

我们先回顾一下视差滚动条的工作原理。

如动画中所示,我们通过推送元素来实现视差效果 沿着 Z 轴向后移动。滚动文档实际上是一种 沿 Y 轴的平移量。因此,如果我们向下滚动,例如每隔 100 像素, 元素将向上移动 100px。这适用于所有元素, 甚至是“往后”查询。但由于他们离您较远 那么他们在屏幕上观察到的移动将小于 100 像素, 所需的视差效果

当然,将元素移回空间后也会使其看起来更小, 我们可以通过放大元素来修正错误我们计算出确切的数学公式 是构建 视差滚动条 所以我不会重复所有细节

第 0 步:我们想要做什么?

滚动条。这就是我们要构建的内容。但你有没有想过 会发生什么?我当然没有。滚动条是 有多少可用内容目前可见,以及进度 您和读者所做的努力。如果您向下滚动,滚动条也会随之 表明您正在朝最后迈进。如果所有内容都适合 进入视口时,滚动条通常会隐藏起来。 如果内容的高度是视口高度的 2 倍, 滚动条填充视口高度的 1⁄2。内容价值是广告高度的 3 倍 视口会将滚动条缩放到视口的 1⁄3,以此类推。您可以看到该模式。 除了滚动鼠标以外,您还可以点击并拖动滚动条 加快网站访问速度对于不显眼的 这样的元素一起战斗吧。

第 1 步:颠倒顺序

我们可以使用 CSS 3D 让元素的移动速度慢于滚动速度 如视差滚动文章中所述。我们能否撤消此操作 方向?事实证明,我们可以这样 构建 自定义滚动条,完美呈现每一帧画面要了解其工作原理,我们需要介绍 请先了解几项 CSS 3D 基础知识

要获得任何一种数学意义上的透视投影, 可能最终使用的是 齐全坐标。 我不详细介绍它们是什么,以及它们为什么有效。 它们就像 3D 坐标和一个名为 w 的附加第四个坐标。本次 坐标应为 1,除非您希望透视失真。周三 不必担心 w 的细节,因为我们不会使用任何 设置为 1 以外的值。因此,从现在开始,所有点都是四维矢量 [x, y, z, w=1],因此矩阵需要 也设为 4x4

您有时会看到 CSS 在 使用 matrix3d() 函数。matrix3d 接受 16 个参数(因为矩阵是 4x4),其顺序依次指定。因此,我们可以使用此函数 还可以手动指定旋转、翻译等 w 坐标混乱了!

在使用 matrix3d() 之前,我们需要一个 3D 上下文,因为如果没有 3D 环境不会有任何透视失真,也不需要 齐全的坐标。要创建 3D 背景,我们需要一个 perspective 以及其中一些可以在新资源中进行转换的元素 创造了 3D 空间。对于 示例

一段 CSS 代码,使用 CSS 的
    透视图属性。

透视容器内的元素由 CSS 引擎处理 如下所示:

  • 将元素的每个角(顶点)转成同构坐标 [x,y,z,w](相对于透视图容器)。
  • 从右到左以矩阵形式应用元素的所有转换。
  • 如果透视元素可滚动,则应用滚动矩阵。
  • 应用透视矩阵。

滚动矩阵是沿 y 轴的平移操作。如果我们向下滚动 400 像素,则需要将所有元素上移 400 像素。透视矩阵 该矩阵在 3D 图像中“拉”离消失点越远的点, 空间这样既能缩小内容 离得比较远,在翻译时也就“移动较慢”。 因此,如果某个元素被推回,则 400px 的平移将会导致该元素 在屏幕上只移动 300 像素

如果您想知道所有详细信息,请参阅 关于 CSS 提供商的规范 但在本文中,我简化了 算法。

我们的方框位于透视容器内,perspective 的值为 p。 属性,我们假设该容器可滚动,并且向下滚动 n 像素。

透视矩阵乘以滚动矩阵乘元素转换矩阵
  等于 4x4 单位矩阵,在第四行中, p 上减一
  第三列乘以 4x4 单位矩阵,在第二列中减去 n
  第 4 列乘以元素转换矩阵。

第一个矩阵是透视矩阵,第二个矩阵是卷轴 模型。总结一下:滚动矩阵的作用是使某个元素向上移动向下滚动,因此出现负号。

而对于滚动条,我们希望元素与滚动条相反 我们可以运用以下技巧: 反转框的角的 w 坐标。如果 w 坐标为 -1,则所有翻译都将以相反的方向生效。那么,我们如何 ?CSS 引擎负责将方框的角转换为 同构坐标,并将 w 设置为 1。是时候让 matrix3d() 大放异彩了!

.box {
  transform:
    matrix3d(
      1, 0, 0, 0,
      0, 1, 0, 0,
      0, 0, 1, 0,
      0, 0, 0, -1
    );
}

此矩阵除了对 w 执行求反操作外,不会执行任何其他操作。因此,当 CSS 引擎 将每个角转换为 [x,y,z,1] 形式的向量,矩阵将 将其转换为 [x,y,z,-1]

第四行的四乘四位单位矩阵,在 p 上减一
  第三列乘以 4x4 单位矩阵,在第二列中减去 n
  行第四列乘以 4x4 单位矩阵,并且在
  第四行第四列乘以四维向量 x, y, z, 1 等于四
  由四个恒等式矩阵在第四行第三列中减一 p,
  第二行中的减 n(第四列),减号(第四行)
  第四列等于四维矢量 x,y + n、z,再减去 z
  p 减 1。

我列出了一个中间步骤,用于展示元素转换的效果 模型。如果您对矩阵数学不舒服,也没有关系。尤里卡 在最后一行中,我们最终在 y 轴上添加了滚动偏移 n, 而不是减去该坐标。元素将向下平移

不过,如果我们只是将此矩阵放入 示例, 该元素将不会显示。这是因为 CSS 规范要求 带 w < 的顶点0 会阻止呈现元素。由于我们的 z 坐标目前为 0,p 为 1,w 将为 -1。

幸运的是,我们可以选择 z 的值!为了确保最终 w=1,我们需要 设置 z = -2。

.box {
  transform:
    matrix3d(
      1, 0, 0, 0,
      0, 1, 0, 0,
      0, 0, 1, 0,
      0, 0, 0, -1
    )
    translateZ(-2px);
}

瞧,我们的 回来了

第 2 步:移动数据

现在,我们的盒子就有了,并且看上去和不使用任何 API 时的样子 转换。目前,透视容器是不可滚动的 看到它,但我们知道元素会朝着另一个方向移动 。那么,让我们滚动容器,对吧?我们只需添加一个 占据空间的分隔符:

<div class="container">
    <div class="box"></div>
    <span class="spacer"></span>
</div>

<style>
/* … all the styles from the previous example … */
.container {
    overflow: scroll;
}
.spacer {
    display: block;
    height: 500px;
}
</style>

现在 滚动方框! 红色框会向下移动。

第 3 步:指定尺寸

我们有一个元素会在页面向下滚动时向下移动。这有点难 确实如此。现在我们需要设置滚动条的样式 使其更具互动性

滚动条通常由“滑块”和“滑道”组成, 始终可见。拇指的高度与 内容可见。

<script>
    const scroller = document.querySelector('.container');
    const thumb = document.querySelector('.box');
    const scrollerHeight = scroller.getBoundingClientRect().height;
    thumb.style.height = /* ??? */;
</script>

scrollerHeight 是可滚动元素的高度,而 scroller.scrollHeight 是可滚动内容的总高度。 scrollerHeight/scroller.scrollHeight 是 可见。拇指盖的垂直空间之比应与 可见内容的比例:

拇指圆点样式,点高度与滚动条高度相同
  当且仅当拇指点样式点高度时,滚动条点滚动高度上
  等于滚动条高度乘以滚动条点滚动高度
  高度。
<script>
    // …
    thumb.style.height =
    scrollerHeight * scrollerHeight / scroller.scrollHeight + 'px';
    // Accommodate for native scrollbars
    thumb.style.right =
    (scroller.clientWidth - scroller.getBoundingClientRect().width) + 'px';
</script>

拇指的大小为 看起来没有问题, 但速度太快了在这里,我们可以从 视差滚动条。如果我们进一步向后移动元素, 滚动。我们可以放大来修正该尺寸。但是,我们应该 准确地将其还原?猜对了,让我们来做一些数学题吧!这是我最后一次了 promise。

关键点在于我们需要拇指的下边缘 一直滚动时,与可滚动元素的下边缘对齐 。换句话说:如果我们滚动 scroller.scrollHeight - scroller.height 像素,我们希望缩略图 由scroller.height - thumb.height翻译。对于滚动条的每个像素 希望我们的拇指移动 1/1 像素:

系数等于滚动条圆点高度减去拇指圆点高度
  点滚动高度减去滚动条点高度。

这是我们的调整系数。现在我们需要将缩放比例转换为 沿 z 轴的平移,我们在视差滚动中已经这样做了 一篇文章。根据 规范中的相关部分: 缩放比例等于 p/(p - z)。我们可以将 z 求解方程式解为 计算我们需要将拇指沿 z 轴移动多少。但保留 请记住,由于 W 坐标的危险性,我们需要将 沿 z 线的额外-2px。另请注意,元素的转换 也就是说,我们的特殊矩阵前面的所有译文都不会 反转,但是特殊矩阵之后的所有译文都会如此!让我们 对其进行编码!

<script>
    // ... code from above...
    const factor =
    (scrollerHeight - thumbHeight)/(scroller.scrollHeight - scrollerHeight);
    thumb.style.transform = `
    matrix3d(
        1, 0, 0, 0,
        0, 1, 0, 0,
        0, 0, 1, 0,
        0, 0, 0, -1
    )
    scale(${1/factor})
    translateZ(${1 - 1/factor}px)
    translateZ(-2px)
    `;
</script>

我们有 滚动条! 它只是一个我们可以随意设置样式的 DOM 元素。有一点需要注意 就无障碍功能而言,很重要的一点是让拇指对 因为许多用户习惯于采用这种方式与滚动条互动。 为了避免这篇博文再长一些,我就不作说明了, 该部分的详细信息请参阅 库代码 如果您想了解具体做法

iOS 呢?

啊,我的老朋友 iOS Safari与视差滚动操作一样, 此处的问题。由于我们是在元素上滚动,因此需要指定 -webkit-overflow-scrolling: touch,但这会导致 3D 扁平化, 滚动效果停止运行。我们在视差滚动条中解决了这个问题 检测 iOS Safari 并依赖 position: sticky 作为权宜解决方法; 在这里我们要做的就是同样的操作。请参阅 与视差相关的文章 来刷新记忆

浏览器滚动条会怎么样?

在某些系统中,我们不得不处理永久性的原生滚动条。 滚动条一直无法隐藏(除非使用滚动条 非标准伪选择器)。 因此,为了隐藏这些漏洞,我们不得不采取一些(无数学题)的黑客手段。我们将 使用 overflow-x: hidden 在容器中滚动元素,并使 宽度大于容器宽度的滚动元素。浏览器的原生滚动条 现在已经不在视野范围内了

金融

综上,我们现在可以构建完美的 就像我们的 彩虹猫演示

如果您看不到 Nyan 猫,则表示您遇到了 发现并提交的错误 (点击拇指图标,让 Nyan 猫出现)。 Chrome 确实可以有效避免不必要的工作 例如为屏幕外的内容绘制或添加动画效果。但遗憾的是,我们的 黑客帝国的恶作剧让 Chrome 认为彩虹猫 GIF 实际上不在屏幕内。 希望此问题能尽快得到解决。

没问题。工作量非常大。感谢大家阅读了 这个词。这是一些 这相当棘手,可能很少值得费心费力 除非自定义滚动条是体验的重要组成部分时。但是 知道这是可能的,不是吗?事实上,要做到这一点很难 自定义滚动条显示需要对 CSS 一端进行改进。但不必担心! 未来, HoudiniAnimationWorklet 将 像这样更容易制作出精美的滚动链接效果