CSS 3D - 沿 z 轴滚动

2025-06-07

CSS 3D - 沿 z 轴滚动

演示 gif

在本文中,我们将创建一个小型 3D 场景,用户可以在 z 轴上滚动。您可以在GitHub上找到本教程的最终代码,也可以点击此链接查看演示

本文假设你已经具备一些 CSS 和 JS 知识。我们将使用 CSS 自定义属性,如果你不熟悉 CSS 自定义属性,可以阅读CSS 自定义属性 - 速查表

CSS 3D 简介

当我们谈论 CSS 3D 时,我们实际上谈论的是 CSS3 transform 3D。此方法允许我们使用transformCSS 属性为 DOM 元素设置 z 轴上的透视或旋转。

transform CSS 属性允许您旋转、缩放、倾斜或平移元素。它修改了 CSS 视觉格式模型的坐标空间。

变换 - MDN

为了能够在 3D 空间中渲染我们的 Dom 元素,我们需要查看以下属性:

  • 看法
  • 透视原点
  • 变换 Z

看法

perspective是一个 CSS 属性,用于设置 z=0 和用户之间的距离。透视值越小,视角的扭曲程度就越大。(请尝试scenePerspective在下面的 codePen 示例中更改该值)。

.container-scene {
  perspective: 100px;
}
Enter fullscreen mode Exit fullscreen mode

的值perspective长度单位

透视图

尝试将下面的示例中的值设置scenePerspective为 0 和 70。您会注意到,如果将值设置为 0,我们的立方体将完全没有透视效果。如果将值设置为 70,您会看到立方体透视效果非常明显。透视值越小,透视效果越明显。

为了能够渲染 3D 空间,我们需要transform-style: preserve-3d;在子元素上指定。在上面的例子中,它设置为 our .cube。默认情况下,元素是扁平化的。

.container-scene {
  perspective: 400px;
}

.container-scene .cube {
  transform-style: preserve-3d;
}
Enter fullscreen mode Exit fullscreen mode

透视原点

perspective-originCSS 属性决定了观看者所注视的位置。它被 perspective 属性用作消失点。

MDN

这个属性基本上允许我们移动 3D 场景的消失点。

.container-scene {
  perspective: 400px;
  perspective-origin: 50% 100%; /*X position value, Y position value*/
}

.container-scene .cube {
  transform-style: preserve-3d;
}
Enter fullscreen mode Exit fullscreen mode

对于 x 和 y,我们可以使用百分比来设置位置。但我们也可以使用以下值:

  • x位置:
    • left= 0%
    • center= 50%
    • right= 100%
  • y 位置
    • top= 0%
    • center= 50%
    • bottom= 50%

透视原点图

perspectiveOriginX在下面的示例中,您可以更改和的值perspectiveOriginY

变换 Z

我们之前已经提到过,transformCSS 属性允许我们在 3D 空间中设置元素。

Transform 具有不同的功能来在 3D 中转换我们的元素:

  • rotateX(角度) - MDN
  • rotateY(角度) - MDN
  • rotateZ(角度) - MDN
  • 翻译Z(tz) - MDN
  • scaleZ(sz) - MDN

正如我们在本节插图中看到的perspectivetranslateZ()允许我们沿 3D 空间的 z 轴定位元素。或者,我们也可以使用translate3D(x, y, z)CSS 函数。

在下面的例子中,您可以通过改变和的值来调整.cube的 Z 轴位置.face-cubeTranslateZcubeFacesTranslateZ

沿 z 轴滚动

现在我们已经很好地了解了 CSS 3D 的工作原理,我们将创建一个 3D 场景,我们将能够在 z 轴上滚动。

设置场景

我们将创建一个页面,列出吉卜力工作室的所有电影。每部电影都将以卡片的形式放置在场景的 z 轴上。欢迎 fork 或下载以下 codepen 作为入门资料,以便继续学习。我使用axiosStudio Ghibli API来填充此页面。

如果您想跟随自己的内容,我们将需要以下标记:

<div class="viewport">
  <div class="scene3D-container">
    <div class="scene3D">
      <div>Card1</div>
      <div>Card2</div>
      <!--Etc.-->
    </div>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

造型

首先,我们要设置CSS 自定义属性(CSS 变量)。其中一些变量将使用 JS 进行转换。它们将帮助我们与场景进行交互。

:root {
  --scenePerspective: 1;
  --scenePerspectiveOriginX: 50;
  --scenePerspectiveOriginY: 30;
  --itemZ: 2; // Gap between each cards
  --cameraSpeed: 150; // Where 1 is the fastest, this var is a multiplying factor of --scenePerspective and --filmZ
  --cameraZ: 0; // Initial camera position
  --viewportHeight: 0; // Viewport height will allow us to set the depth of our scene
}
Enter fullscreen mode Exit fullscreen mode

.viewport这将允许我们设置窗口的高度。稍后我们将使用它来设置场景的深度,并使用滚动条在z轴上导航。

.viewport {
  height: calc(var(--viewportHeight) * 1px);
}
Enter fullscreen mode Exit fullscreen mode

.scene3D-container设置场景透视图和透视原点。它的位置是固定的,所以它始终在屏幕上。我们也要设置透视原点。

.viewport .scene3D-container {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  perspective: calc(var(--scenePerspective) * var(--cameraSpeed) * 1px);
  perspective-origin: calc(var(--scenePerspectiveOriginX) * 1%) calc(
      var(--scenePerspectiveOriginY) * 1%
    );
  will-change: perspective-origin;
  transform: translate3d(
    0,
    0,
    0
  ); //Allows Hardware-Accelerated CSS, so transitions are smoother
}
Enter fullscreen mode Exit fullscreen mode

.scene3D设置场景在 z 轴上的位置,这有点像在 z 轴上移动相机。但实际上我们移动的是场景,而相机(视口)是固定的。在本文的其余部分,我们将使用相机比较。.scene3D取视口的完整高度和宽度。

.viewport .scene3D-container .scene3D {
  position: absolute;
  top: 0;
  height: 100vh;
  width: 100%;
  transform-style: preserve-3d;
  transform: translateZ(calc(var(--cameraZ) * 1px));
  will-change: transform;
}
Enter fullscreen mode Exit fullscreen mode

最后,同样重要的是,我们要在场景中定位卡片。所有卡片都是绝对定位的。奇数卡片位于左侧,偶数卡片位于右侧。

我们使用 SCSS 以编程方式平移每个项目。在X轴和Y轴上,我们随机地将它们平移 -25% 到 25%(X 轴),-50% 到 50%(Y 轴)。我们使用循环,@for使每个项目在z轴上的平移量乘以其索引。

.viewport .scene3D-container .scene3D {
  > div {
    position: absolute;
    display: block;
    width: 100%;
    top: 40%;

    @media only screen and (min-width: 600px) {
      width: 45%;
    }

    &:nth-child(2n) {
      left: 0;
    }

    &:nth-child(2n + 1) {
      right: 0;
    }

    @for $i from 0 through 25 {
      &:nth-child(#{$i}) {
        transform: translate3D(
          random(50) - 25 * 1%,
          random(100) - 50 * 1%,
          calc(var(--itemZ) * var(--cameraSpeed) * #{$i} * -1px)
        );
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

CSS 现在已经完成,我们有了一个 3D 场景。在本文的后续部分,我们将编写一些 JavaScript 代码来实现场景导航。

沿 z 轴滚动(移动相机)

为了能够滚动,我们首先需要设置--viewportHeight模拟场景深度的值。

场景的深度等于下列各项的和:

  • 用户窗口的高度
    • 窗口内部高度
  • 视角.scene3D-container
    • var(--scenePerspective) * var(--cameraSpeed)
  • 我们最后一项的平移 z 值
    • var(--itemZ) * var(--cameraSpeed) * items.length

让我们创建一个函数来更新加载时setSceneHeight()的值。--viewportHeight

document.addEventListener("DOMContentLoaded", function() {
  setSceneHeight();
});

function setSceneHeight() {
  const numberOfItems = films.length; // Or number of items you have in `.scene3D`
  const itemZ = parseFloat(
    getComputedStyle(document.documentElement).getPropertyValue("--itemZ")
  );
  const scenePerspective = parseFloat(
    getComputedStyle(document.documentElement).getPropertyValue(
      "--scenePerspective"
    )
  );
  const cameraSpeed = parseFloat(
    getComputedStyle(document.documentElement).getPropertyValue("--cameraSpeed")
  );

  const height =
    window.innerHeight +
    scenePerspective * cameraSpeed +
    itemZ * cameraSpeed * numberOfItems;

  // Update --viewportHeight value
  document.documentElement.style.setProperty("--viewportHeight", height);
}
Enter fullscreen mode Exit fullscreen mode

我们的页面现在有了滚动条,但仍然无法滚动。我们需要添加一个事件监听器来监听用户的滚动操作。scroll 事件会调用一个moveCamera()函数,它会将window.pageYOffset--cameraZ的值更新为的值

document.addEventListener("DOMContentLoaded", function() {
  window.addEventListener("scroll", moveCamera);
  setSceneHeight();
});

function moveCamera() {
  document.documentElement.style.setProperty("--cameraZ", window.pageYOffset);
}

function setSceneHeight() {
  // ...
}
Enter fullscreen mode Exit fullscreen mode

移动摄像机角度

最后,让我们让场景更具动态效果。在mousemove 事件中,我们将更改scenePerspectiveOriginX和的值scenePerspectiveOriginY。这将产生相机移动的错觉。物品将保持在场景中的直线位置。如果您想让相机旋转得更逼真,可以在场景中应用rotate3d() 。

首先,我们将这两个变量的初始值存储在一个perspectiveOrigin对象中,并设置一个perspectiveOrigin.maxGap值来限制变量的最大值和最小值。例如,如果scenePerspectiveOriginY等于 50%。当鼠标移动时,新值将在 40% 到 60% 之间。

const perspectiveOrigin = {
  x: parseFloat(
    getComputedStyle(document.documentElement).getPropertyValue(
      "--scenePerspectiveOriginX"
    )
  ),
  y: parseFloat(
    getComputedStyle(document.documentElement).getPropertyValue(
      "--scenePerspectiveOriginY"
    )
  ),
  maxGap: 10
};
Enter fullscreen mode Exit fullscreen mode

如果用户光标位于屏幕中心,我们将设置--scenePerspectiveOriginX和的值--scenePerspectiveOriginX作为初始值。光标离中心越远,这些值的增/减幅度就越大。如果用户移动到左上角,值会增加;如果移动到右下角,值会减少。

moveCameraAngle()函数将更新以下值:

  • xGapyGap以百分比形式返回用户鼠标在 X 轴和 Y 轴上的位置,与窗口的中心进行比较。
  • newPerspectiveOriginXnewPerspectiveOriginY返回新的视角原点。
document.addEventListener("DOMContentLoaded", function() {
  window.addEventListener("scroll", moveCamera);
  window.addEventListener("mousemove", moveCameraAngle);
  setSceneHeight();
});

function moveCameraAngle(event) {
  const xGap =
    (((event.clientX - window.innerWidth / 2) * 100) /
      (window.innerWidth / 2)) *
    -1;
  const yGap =
    (((event.clientY - window.innerHeight / 2) * 100) /
      (window.innerHeight / 2)) *
    -1;
  const newPerspectiveOriginX =
    perspectiveOrigin.x + (xGap * perspectiveOrigin.maxGap) / 100;
  const newPerspectiveOriginY =
    perspectiveOrigin.y + (yGap * perspectiveOrigin.maxGap) / 100;

  document.documentElement.style.setProperty(
    "--scenePerspectiveOriginX",
    newPerspectiveOriginX
  );
  document.documentElement.style.setProperty(
    "--scenePerspectiveOriginY",
    newPerspectiveOriginY
  );
}
Enter fullscreen mode Exit fullscreen mode

我们的场景完成了🎉。希望你喜欢这篇文章😃

资源


文章来源:https://dev.to/vinceumo/css-3d---scrolling-on-the-z-axis-45i9
PREV
Vue.js 101 todo PWA 教程 Bonus - 离线渐进式 Web 应用程序 (PWA)
NEXT
以 React 开发人员身份学习 Vue