CSS 三维:学会用立方体而不是盒子思考

Avatar of Jhey Tompkins
Jhey Tompkins

DigitalOcean 为您旅程的每个阶段提供云产品。 立即开始使用 $200 免费积分!

我学习 CSS 的道路有点非正统。 我最初不是前端开发人员。 我是 Java 开发人员。 事实上,我对 CSS 最早的记忆是在 Visual Studio 中为东西选择颜色。

直到后来我才开始接触并爱上了前端。 而探索 CSS 则更晚。 当我开始探索 CSS 时,正好是 CSS3 开始流行的时候。 三维和动画是当时最酷的家伙。 它们几乎塑造了我学习 CSS 的方式。 它们吸引了我,并 *塑造*(双关语)了我对 CSS 的理解,超过了其他东西,例如布局、颜色等。

我的意思是我已经做了一段时间的 3D CSS 了。 就像你花很多时间在任何事情上一样,随着时间的推移,你会不断改进你的流程,以磨练你的技能。 这篇文章探讨了我目前如何处理 3D CSS,并介绍了一些可能对你有帮助的技巧和窍门!

https://codepen.io/jh3y/pen/mLaXRe

一切都是长方体

对于大多数东西,我们可以使用长方体。 当然,我们可以创建更复杂的形状,但通常需要更多考虑。 曲线尤其难以处理,有一些技巧可以处理它们(但稍后会详细介绍)。

我们不会详细介绍如何在 CSS 中制作长方体。 我们可以参考 Ana Tudor 的文章,或者查看我制作长方体的这个屏幕录制

本质上,我们使用一个元素来包装我们的长方体,然后在里面转换六个元素。 每个元素充当我们长方体的一面。 重要的是要应用 transform-style: preserve-3d。 在任何地方应用它也并非坏事。 当事情变得更复杂时,我们很可能要处理嵌套的长方体。 试图在浏览器之间切换时调试一个丢失的 transform-style 会很痛苦。

* { transform-style: preserve-3d; }

对于你的 3D 作品,如果超过几个面,试着想象整个场景都是用长方体构建的。 举个实际例子,请考虑这个 3D 书籍的演示。 它由四个长方体组成。 每个封面上一个,脊柱上一个,页面上一个。 background-image 的使用为我们完成了剩下的工作。

设置场景

我们将像使用乐高积木一样使用长方体。 但是,我们可以通过设置场景并创建一个平面来让我们的生活更轻松一些。 那个平面将是我们作品的放置位置,并使我们更容易旋转和移动整个作品。

对我来说,当我创建一个场景时,我喜欢先在 X 和 Y 轴上旋转它。 然后,我用 rotateX(90deg) 将其平放。 这样,当我想在场景中添加一个新的长方体时,我将其添加到平面元素中。 我在这里还会做的一件事是在所有长方体上设置 position: absolute

.plane {
  transform: rotateX(calc(var(--rotate-x, -24) * 1deg)) rotateY(calc(var(--rotate-y, -24) * 1deg)) rotateX(90deg) translate3d(0, 0, 0);
}

从样板开始

创建各种大小的长方体并在平面上创建长方体会导致每个作品的重复工作很多。 出于这个原因,我使用 Pug 通过 mixin 创建我的长方体。 如果你不熟悉 Pug,我写了一篇 5 分钟的介绍

一个典型的场景如下所示

//- Front
//- Back
//- Right
//- Left
//- Top
//- Bottom
mixin cuboid(className)
  .cuboid(class=className)
    - let s = 0
    while s < 6
      .cuboid__side
      - s++
.scene
  //- Plane that all the 3D stuff sits on
  .plane
    +cuboid('first-cuboid')

至于 CSS,我的长方体类目前看起来像这样

.cuboid {
  // Defaults
  --width: 15;
  --height: 10;
  --depth: 4;
  height: calc(var(--depth) * 1vmin);
  width: calc(var(--width) * 1vmin);
  transform-style: preserve-3d;
  position: absolute;
  font-size: 1rem;
  transform: translate3d(0, 0, 5vmin);
}
.cuboid > div:nth-of-type(1) {
  height: calc(var(--height) * 1vmin);
  width: 100%;
  transform-origin: 50% 50%;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%) rotateX(-90deg) translate3d(0, 0, calc((var(--depth) / 2) * 1vmin));
}
.cuboid > div:nth-of-type(2) {
  height: calc(var(--height) * 1vmin);
  width: 100%;
  transform-origin: 50% 50%;
  transform: translate(-50%, -50%) rotateX(-90deg) rotateY(180deg) translate3d(0, 0, calc((var(--depth) / 2) * 1vmin));
  position: absolute;
  top: 50%;
  left: 50%;
}
.cuboid > div:nth-of-type(3) {
  height: calc(var(--height) * 1vmin);
  width: calc(var(--depth) * 1vmin);
  transform: translate(-50%, -50%) rotateX(-90deg) rotateY(90deg) translate3d(0, 0, calc((var(--width) / 2) * 1vmin));
  position: absolute;
  top: 50%;
  left: 50%;
}
.cuboid > div:nth-of-type(4) {
  height: calc(var(--height) * 1vmin);
  width: calc(var(--depth) * 1vmin);
  transform: translate(-50%, -50%) rotateX(-90deg) rotateY(-90deg) translate3d(0, 0, calc((var(--width) / 2) * 1vmin));
  position: absolute;
  top: 50%;
  left: 50%;
}
.cuboid > div:nth-of-type(5) {
  height: calc(var(--depth) * 1vmin);
  width: calc(var(--width) * 1vmin);
  transform: translate(-50%, -50%) translate3d(0, 0, calc((var(--height) / 2) * 1vmin));
  position: absolute;
  top: 50%;
  left: 50%;
}
.cuboid > div:nth-of-type(6) {
  height: calc(var(--depth) * 1vmin);
  width: calc(var(--width) * 1vmin);
  transform: translate(-50%, -50%) translate3d(0, 0, calc((var(--height) / 2) * -1vmin)) rotateX(180deg);
  position: absolute;
  top: 50%;
  left: 50%;
}

默认情况下,这会给我们这样的结果

由 CSS 变量驱动

你可能已经注意到里面有很多 CSS 变量(也称为自定义属性)。 这节省了大量时间。 我使用 CSS 变量来驱动我的长方体。

  • --width:长方体在平面上的宽度
  • --height:长方体在平面上的高度
  • --depth:长方体在平面上的深度
  • --x:平面上的 X 位置
  • --y:平面上的 Y 位置

我主要使用 vmin 作为我的尺寸单位,以保持一切响应。 如果我正在创建一个可缩放的东西,我可能会创建一个响应式单位。 我们在 之前的一篇文章 中提到了这种技术。 同样,我将平面平放。 现在我可以将长方体视为具有高度、宽度和深度。 这个演示展示了我们如何在平面周围移动长方体并改变其尺寸。

使用 dat.GUI 进行调试

你可能已经注意到我们涵盖的一些演示的右上角有一个小面板。 那就是 dat.GUI. 它是一个轻量级的 JavaScript 控制器库,非常适合调试 3D CSS。 不需要太多代码,我们就可以设置一个面板,允许我们在运行时更改 CSS 变量。 我喜欢做的一件事是用该面板在 X 和 Y 轴上旋转平面。 这样,就可以看到物体是如何对齐的,或者对一开始可能看不到的部分进行处理。


const {
  dat: { GUI },
} = window
const CONTROLLER = new GUI()
const CONFIG = {
  'cuboid-height': 10,
  'cuboid-width': 10,
  'cuboid-depth': 10,
  x: 5,
  y: 5,
  z: 5,
  'rotate-cuboid-x': 0,
  'rotate-cuboid-y': 0,
  'rotate-cuboid-z': 0,
}
const UPDATE = () => {
  Object.entries(CONFIG).forEach(([key, value]) => {
    document.documentElement.style.setProperty(`--${key}`, value)
  })
}
const CUBOID_FOLDER = CONTROLLER.addFolder('Cuboid')
CUBOID_FOLDER.add(CONFIG, 'cuboid-height', 1, 20, 0.1)
  .name('Height (vmin)')
  .onChange(UPDATE)
CUBOID_FOLDER.add(CONFIG, 'cuboid-width', 1, 20, 0.1)
  .name('Width (vmin)')
  .onChange(UPDATE)
CUBOID_FOLDER.add(CONFIG, 'cuboid-depth', 1, 20, 0.1)
  .name('Depth (vmin)')
  .onChange(UPDATE)
// You have a choice at this point. Use x||y on the plane
// Or, use standard transform with vmin.
CUBOID_FOLDER.add(CONFIG, 'x', 0, 40, 0.1)
  .name('X (vmin)')
  .onChange(UPDATE)
CUBOID_FOLDER.add(CONFIG, 'y', 0, 40, 0.1)
  .name('Y (vmin)')
  .onChange(UPDATE)
CUBOID_FOLDER.add(CONFIG, 'z', -25, 25, 0.1)
  .name('Z (vmin)')
  .onChange(UPDATE)
CUBOID_FOLDER.add(CONFIG, 'rotate-cuboid-x', 0, 360, 1)
  .name('Rotate X (deg)')
  .onChange(UPDATE)
CUBOID_FOLDER.add(CONFIG, 'rotate-cuboid-y', 0, 360, 1)
  .name('Rotate Y (deg)')
  .onChange(UPDATE)
CUBOID_FOLDER.add(CONFIG, 'rotate-cuboid-z', 0, 360, 1)
  .name('Rotate Z (deg)')
  .onChange(UPDATE)
UPDATE()

如果你观看这条推文中的延时视频。 你会注意到,我在构建场景时多次旋转了平面。

https://twitter.com/jh3yy/status/1312126353177673732?s=20

dat.GUI 代码有点重复。 我们可以创建函数来获取配置并生成控制器。 需要稍微调整一下才能满足你的需求。 我开始在 这个演示 中使用动态生成的控制器。

居中

你可能已经注意到,默认情况下,每个长方体都有一半在平面下方,一半在平面上方。 这是故意的。 也是我最近才开始做的事情。 为什么? 因为我们希望将长方体的包含元素用作长方体的中心。 这使得动画更容易。 特别是,如果我们考虑绕 Z 轴旋转。 当我创建“CSS is Cake”时,我发现了这一点。 在制作蛋糕后,我决定让每个切片都具有交互性。 然后我不得不回去更改我的实现,以修复翻转切片的旋转中心。

https://codepen.io/jh3y/pen/KKVGoGJ

在这里,我分解了那个演示,展示了中心以及偏移中心将如何影响演示。

定位

如果我们正在处理一个更复杂的场景,我们可能会将其分成不同的部分。 这就是子平面概念派上用场的地方。 考虑这个演示,我重新创建了我的个人工作空间。

https://twitter.com/jh3yy/status/1310658720746045440?s=20

这里发生了很多事情,很难跟踪所有长方体。 为此,我们可以引入子平面。 让我们分解那个演示。 椅子有它自己的子平面。 这使得更容易在场景周围移动它并旋转它——以及其他事情——而不会影响其他任何东西。 事实上,我们甚至可以旋转顶部而不会移动底部!

美学

一旦我们有了结构,就可以开始处理美观问题了。这完全取决于你正在制作什么。但你可以通过使用某些技巧来获得一些快速收益。我倾向于先把东西做“丑”,然后再回去为所有颜色设置 CSS 变量并应用它们。三种色调用于同一件事,可以让我们在视觉上区分长方体的侧面。考虑这个烤面包机示例。三种色调覆盖了烤面包机的侧面。

https://codepen.io/jh3y/pen/KKVjLrx

我们之前提到的 Pug 混合器允许我们为长方体定义类名。将颜色应用于侧面通常看起来像这样。

/* The front face uses a linear-gradient to apply the shimmer effect */
.toaster__body > div:nth-of-type(1) {
  background: linear-gradient(120deg, transparent 10%, var(--shine) 10% 20%, transparent 20% 25%, var(--shine) 25% 30%, transparent 30%), var(--shade-one);
}
.toaster__body > div:nth-of-type(2) {
  background: var(--shade-one);
}
.toaster__body > div:nth-of-type(3),
.toaster__body > div:nth-of-type(4) {
  background: var(--shade-three);
}
.toaster__body > div:nth-of-type(5),
.toaster__body > div:nth-of-type(6) {
  background: var(--shade-two);
}

用我们的 Pug 混合器包含额外的元素有点棘手。但别忘了,长方体的每个侧面都提供了两个伪元素。我们可以将它们用于各种细节。例如,烤面包机的插槽和侧面手柄的插槽都是伪元素。

另一个技巧是使用 background-image 添加细节。例如,考虑 3D 工作空间。我们可以使用背景图层来创建阴影。我们可以使用实际的图像来创建纹理表面。地板和地毯是重复的 background-image。事实上,对纹理使用伪元素很棒,因为这样我们可以在需要时对其进行转换,例如旋转平铺图像。我还发现,在某些情况下,直接使用长方体侧面会产生闪烁。

https://codepen.io/jh3y/pen/XWdQBRx

使用图像作为纹理的一个问题是如何创建不同的色调。我们需要色调来区分不同的侧面。这就是 filter 属性可以提供帮助的地方。将 brightness``() 过滤器应用于长方体的不同侧面可以使它们变亮或变暗。考虑这个 CSS 翻转表格。所有表面都使用纹理图像。但为了区分侧面,应用了亮度过滤器。

https://codepen.io/jh3y/pen/xJXvjP

烟雾和镜子透视

如何使用有限的元素集来创建形状或我们想要创建的看似不可能的特征?有时我们可以用一些烟雾和镜子来欺骗眼睛。我们可以提供一种“假”的 3D 感。the Zdog library 做得很好,是一个很好的例子。

考虑一下这一束气球。支撑它们的绳子使用了正确的透视,并且每个绳子都有自己的旋转、倾斜等。但气球本身是扁平的。如果我们旋转平面,气球会保持反向平面的旋转。这会给人一种“假”的 3D 印象。试试演示并关闭反向。

https://codepen.io/jh3y/pen/NWNVgJw

有时需要一些跳出框框的思考。在制作 3D 工作空间时,有人建议我制作一个盆栽。我房间里有一些盆栽。我最初的想法是,“不,我可以做一个方形花盆,我该如何制作所有叶子?”实际上,我们也可以在这个上面使用一些视觉技巧。获取一些树叶或植物的库存图像。使用像 remove.bg 这样的工具去除背景。然后将许多图像放置在同一个位置,但将它们分别旋转一定角度。现在,当它们旋转时,我们就会产生一个 3D 植物的印象。

解决棘手的形状

棘手的形状很难用通用的方式覆盖。每个创作都有自己的障碍。但是,有一些例子可以帮助你获得解决问题的想法。我最近读了一篇关于乐高界面面板的 UX 的文章。事实上,将 3D CSS 工作视为乐高积木并不是一个坏主意。但乐高界面面板是一种我们可以用 CSS 制作的形状(减去螺柱——我最近才知道它们叫什么)。它是一个长方体。然后我们可以剪裁顶部,使末端透明,并旋转一个伪元素将其连接起来。我们可以使用伪元素并添加一些背景图层来添加细节。尝试在下面的演示中打开和关闭线框。如果我们想要面部的精确高度和角度,我们可以使用一些数学来计算斜边等等。

另一个难以覆盖的东西是曲线。球形不在 CSS 的能力范围内。我们现在有几种选择。一种选择是接受这一事实,并创建具有有限边数的多边形。另一种是创建圆形并使用我们前面提到的植物旋转方法。这些选择都可以。但再次强调,这是基于用例的。每个都有优点和缺点。使用多边形,我们放弃曲线或使用太多元素,最终得到几乎是曲线。后者会导致性能问题。使用透视技巧,我们也可能最终遇到性能问题,具体取决于情况。我们还放弃了对形状的“侧面”进行样式设置的能力,因为根本不存在“侧面”。

Z 冲突

最后但并非最不重要的一点,值得一提的是“Z 冲突”。这是指平面上的某些元素可能重叠或导致不希望出现的闪烁。很难给出此问题的良好示例。没有通用的解决方案。这是一个需要逐案解决的问题。主要策略是根据需要在 DOM 中对事物进行排序。但有时这不是唯一的问题。

准确性有时会导致问题。让我们再次参考 3D 工作空间。考虑一下墙上的画布。阴影是一个伪元素。如果我们将画布完全贴在墙上,我们就会遇到问题。如果这样做,阴影和墙壁将争夺前置位置。为了解决这个问题,我们可以将事物稍微平移一点。这将解决问题并声明应该坐在前面的东西。

尝试调整此演示的大小,打开和关闭“画布偏移”。注意到当没有偏移量时,阴影会闪烁吗?这是因为阴影和墙壁正在争夺视图。偏移量将 --x 设置为 1vmin 的一小部分,我们将其命名为 --cm。这是一个用于该创作的响应式单位。

就是“它”!

将你的 CSS 带到另一个维度。使用我的一些技巧,创建自己的技巧,分享它们,分享你的 3D 作品!是的,在 CSS 中制作 3D 物体可能很困难,这绝对是一个我们可以不断完善的过程。不同的方法适合不同的人,耐心是必不可少的要素。我很想知道你会如何处理你的方法!

最重要的是?玩得开心!