三维CSS:学习用立方体而非盒子思考
由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!
我学习 CSS 的道路有点非同寻常。我并非从前端开发起步,而是一名 Java 开发人员。事实上,我对 CSS 最早的记忆还是在 Visual Studio 里给各种元素选择颜色。
直到后来,我才真正接触并爱上了前端开发。而探索 CSS 则是之后的事。那时,CSS3 正风靡一时。3D 和动画是当时最热门的领域,它们几乎塑造了我学习 CSS 的方式。它们深深吸引了我,并塑造了我对 CSS 的理解(此处双关),其影响甚至超过了布局、颜色等其他方面。
我想说的是,我研究 3D CSS 已经有一段时间了。就像任何你投入大量时间去做的事情一样,随着时间的推移,你会不断完善你的工作流程,精进你的技能。这篇文章将介绍我目前使用 3D CSS 的方法,并分享一些可能对你有帮助的技巧和窍门!
万物皆长方体
大多数情况下,我们可以用长方体来表示物体。当然,我们也可以创建更复杂的形状,但这通常需要更多考虑。曲线比较难处理,有一些技巧可以掌握(稍后会详细介绍)。
我们不会详细讲解如何在 CSS 中创建长方体。不过,我会在这个视频教程中讲解如何创建可配置的长方体 👍
其核心在于,我们使用一个元素包裹长方体,然后对内部的六个元素进行变换。每个元素都作为长方体的一个侧面。务必应用此方法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()
如果你观看这条推文中的延时视频,你会注意到我在搭建场景的过程中多次旋转飞机。
这段dat.GUI代码有点重复。我们可以创建一些函数,这些函数会接收一个配置并生成控制器。这需要一些调整才能满足你的需求。我在这个演示中开始尝试动态生成控制器。
定心
你可能已经注意到,默认情况下每个长方体一半在平面下方,一半在平面上方。这是有意为之,也是我最近才开始采用的做法。为什么呢?因为我们希望使用长方体的容器元素作为长方体的中心。这样可以简化动画制作,尤其是在考虑绕 Z 轴旋转时。我在创建“CSS is Cake”时发现了这一点。完成之后,我又想让每个切片都具有交互性。于是我不得不修改我的实现,以修正翻转切片的旋转中心。
在这里,我将该演示分解开来,以展示中心点以及中心偏移会对演示产生怎样的影响。
定位
如果场景比较复杂,我们可以将其拆分成不同的部分。这时子平面的概念就派上用场了。请看这个演示,我在这里重现了我的个人工作区。
这里涉及的物体很多,很难记住所有的长方体。为此,我们可以引入子平面。让我们来分析一下这个演示。椅子有它自己的子平面。这使得我们可以更轻松地在场景中移动和旋转它——以及其他操作——而不会影响到其他物体。事实上,我们甚至可以在不移动椅脚的情况下旋转陀螺!
美学
一旦有了结构,就该着手美化了。这完全取决于你要制作什么。但你可以利用一些技巧快速取得一些成效。我通常先把东西弄得“丑陋”一些,然后再回头为所有颜色创建 CSS 变量并应用它们。例如,用三种不同的色调来区分长方体的各个面。以烤面包机为例。三种色调分别代表了烤面包机的三个侧面:
我们之前提到的 Pug mixin 允许我们为长方体定义类名。给长方体的一面着色通常看起来像这样:
/* 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);
}
在我们的巴哥犬混合模型中添加额外元素有点棘手。但别忘了,我们长方体的每个面都提供了两个伪元素。我们可以用它们来添加各种细节。例如,烤面包机插槽和侧面的把手插槽就是伪元素。
另一个技巧是利用background-image伪元素添加细节。例如,考虑一下 3D 工作区。我们可以使用背景图层来创建阴影。我们可以使用实际图像来创建纹理表面。地板和地毯是重复的background-image。事实上,使用伪元素来处理纹理非常棒,因为这样我们就可以在需要时对其进行变换,例如旋转平铺图像。我还发现,在某些情况下,直接操作长方体侧面会出现闪烁现象。
使用图像作为纹理的一个问题是如何创建不同的色调。我们需要色调来区分不同的面。这时,filter属性就能派上用场了。brightness()对长方体的不同面应用滤镜可以使其变亮或变暗。考虑一下这个 CSS 翻转桌。所有表面都使用了纹理图像。但为了区分各个面,应用了亮度滤镜。
烟雾与镜子的视角
如果要用有限的元素集合来创建看似不可能的形状或特征,该怎么办呢?有时我们可以用一些技巧来欺骗眼睛,营造出一种“伪”的3D效果。Zdog库在这方面做得很好,就是一个很好的例子。
想象一下这束气球。系住它们的绳子透视角度正确,每个气球都有自己的旋转、倾斜等角度。但气球本身是扁平的。如果我们旋转这个平面,气球会保持反向旋转。这就产生了那种“伪”3D效果。试试演示,关闭反向旋转功能。
有时候需要跳出固有思维模式。我在搭建3D工作空间时,有人建议我用盆栽植物。我的房间里正好有几盆。我最初的想法是:“不行,我可以做一个方形花盆,但叶子怎么做出来呢?” 其实,我们也可以运用一些视觉技巧。找一张叶子或植物的图片素材。用类似remove.bg的工具去除背景。然后把多张图片放在同一个位置,但每张图片都旋转一定的角度。这样,旋转之后,我们就得到了3D植物的视觉效果。
应对不规则形状
形状不规则的图形很难用通用方法处理。每个作品都有其独特的挑战。不过,以下几个例子或许能给你一些启发。我最近读到一篇关于乐高界面面板用户体验的文章。事实上,把 3D CSS 工作比作搭建乐高积木是个不错的想法。乐高界面面板的形状我们可以用 CSS 来创建(除了凸点——我最近才知道它们叫这个)。它最初是一个长方体。然后我们可以裁剪顶面,将底面设为透明,并旋转一个伪元素将它们连接起来。我们可以使用这个伪元素添加一些背景图层来表现细节。试试在下面的演示中打开和关闭线框。如果我们想要精确地计算各个面的高度和角度,可以用一些数学公式来计算斜边等等。
另一个棘手的问题是曲线。球形并非 CSS 的强项。目前我们有几种选择。一种方法是接受这个事实,创建边数有限的多边形。另一种方法是创建圆形,并使用我们之前在处理植物时提到的旋转方法。这些方法都可行,但具体情况具体分析。每种方法都有其优缺点。使用多边形,我们要么放弃曲线,要么使用过多的元素来勉强得到一个近似曲线的形状。后者可能会导致性能问题。使用透视技巧,也可能出现性能问题,具体取决于实际情况。此外,由于形状没有边,我们也无法为其设置样式。
Z格斗
最后,值得一提的是“Z轴冲突”。这是指同一平面上的某些元素可能会重叠或导致不必要的闪烁。很难给出具体的例子,也没有通用的解决方案,需要根据具体情况逐一解决。主要的策略是调整DOM中元素的顺序,但有时这并非唯一的问题所在。
过于精确有时反而会带来问题。我们再来看看3D工作区。假设墙上有一块画布,阴影是一个伪元素。如果我们把画布紧贴着墙面放置,就会出现问题。这样一来,阴影和墙面就会争夺视觉中心。为了解决这个问题,我们可以稍微平移一下。这样就能解决问题,并明确哪个元素应该位于前面。
尝试启用和禁用“画布偏移”来调整此演示的大小。注意当没有偏移时,阴影会闪烁吗?这是因为阴影和墙壁争夺了视觉空间。偏移量将画布偏移量设置为我们指定值--x的一部分。这是一个用于此创建的响应式单位。1vmin--cm
就是这样”!
让你的 CSS 更上一层楼。运用我的技巧,创造你自己的技巧,分享它们,并展示你的 3D 作品!没错,用 CSS 制作 3D 对象可能很困难,而且这绝对是一个可以不断改进的过程。不同的方法适用于不同的人,耐心是必不可少的。我很期待看到你的创作思路!
最重要的是什么?享受过程!
文章来源:https://dev.to/jh3y/css-in-3d-learning-to-think-in-cubes-instead-of-boxes-4ank