Print.css,但不是你所知道的那种方式——创建一台3D CSS打印机
由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!
我最近一直在用CSS制作这些3D场景,纯粹是为了好玩。通常是在我的直播中展示。
每次演示都是一次尝试新事物或探索 CSS 实现方法的良机。我经常会接受大家的建议,告诉我们应该在直播中尝试制作什么。最近有人建议制作一台 3D 打印机。这里指的是真正的“3D”打印机,而不是喷墨/激光打印机。这就是我最终的成果!
使用 CSS 创建 3D 对象
我之前写过关于用 CSS 创建 3D 图形的文章。总的来说,大多数场景都是由长方体构成的。
要创建一个长方体,我们可以使用 CSS 变换来定位长方体的边。关键属性是 `transform` transform-style。将其设置为 `true`preserve-3d可以让我们沿第三个维度变换元素。
* {
transform-style: preserve-3d;
}
当你创建了一些这样的场景后,你就会开始掌握一些加速的方法。我喜欢使用 Pug 作为 HTML 预处理器。它的 mixin 功能让我能够更快地创建长方体。本文中的标记示例使用了 Pug。但是,对于每个 CodePen 演示,你都可以使用“查看编译后的 HTML”选项来查看 HTML 输出。
mixin cuboid()
.cuboid(class!=attributes.class)
- let s = 0
while s < 6
.cuboid__side
- s++
使用此代码
+cuboid()(class="printer__top")
会产生
<div class="cuboid printer__top">
<div class="cuboid__side"></div>
<div class="cuboid__side"></div>
<div class="cuboid__side"></div>
<div class="cuboid__side"></div>
<div class="cuboid__side"></div>
<div class="cuboid__side"></div>
</div>
然后我使用了一组 CSS 代码块来布局长方体。妙处在于我们可以利用 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%;
}
利用自定义属性,我们可以控制长方体的各种特性等等。
--width平面上长方体的宽度--height平面上长方体的高度--depth长方体在平面上的深度--x平面上的 X 坐标--y平面上的 Y 坐标
只有把长方体放到场景中并旋转它之后,它才会变得特别引人注目。同样,我在制作过程中会使用自定义属性来操控场景。Dat.GUI 在这里就派上了用场。
如果您查看演示,会发现使用控制面板可以更新场景中的自定义 CSS 属性。这种 CSS 自定义属性的作用域划分方式可以节省大量重复代码,并保持代码的 DRY(Don't Repeat Yourself,不要重复自己)原则。
不止一种方法
就像 CSS 中的许多事情一样,实现方法不止一种。通常,你可以用长方体构建场景,并根据需要调整元素的位置。但这样管理起来可能会比较棘手。很多时候,你需要对元素进行分组或添加某种容器。
考虑以下示例,其中椅子是一个可以移动的独立子场景。
最近很多例子都不太复杂。我一直在使用拉伸技术。这意味着我可以把要制作的任何东西都用二维元素来表示。例如,我最近制作的直升机。
.helicopter
.helicopter__rotor
.helicopter__cockpit
.helicopter__base-light
.helicopter__chair
.helicopter__chair-back
.helicopter__chair-bottom
.helicopter__dashboard
.helicopter__tail
.helicopter__fin
.helicopter__triblade
.helicopter__tail-light
.helicopter__stabilizer
.helicopter__skids
.helicopter __skid--left.helicopter__ skid
.helicopter __skid--right.helicopter__ skid
.helicopter__wing
.helicopter __wing-light.helicopter__ wing-light--left
.helicopter __wing-light.helicopter__ wing-light--right
.helicopter__launchers
.helicopter __launcher.helicopter__ launcher--left
.helicopter __launcher.helicopter__ launcher--right
.helicopter__blades
然后我们可以使用 mixin 将长方体放入所有容器中。接着为每个长方体应用所需的“厚度”。厚度由作用域自定义属性决定。此演示切换--thickness构成直升机的长方体的属性。它展示了最初的 2D 映射效果。
这就是使用 CSS 创建 3D 物体的基本方法。深入研究代码肯定会发现一些技巧。但总的来说,首先搭建一个场景框架,填充长方体,然后给长方体着色。通常需要使用不同的颜色深浅来区分长方体的各个面。任何额外的细节都可以添加到长方体的面上,或者可以应用到长方体的变换上。例如,绕 Z 轴旋转或移动。
让我们来看一个简化的例子。
.scene
.extrusion
+cuboid()(class="extrusion__cuboid")
用于创建挤压长方体的新 CSS 代码可能如下所示。请注意,我们还为每个面的颜色添加了作用域自定义属性。最好在:root此处添加一些默认值或备用值。
.cuboid {
width: 100%;
height: 100%;
position: relative;
}
.cuboid__side:nth-of-type(1) {
background: var(--shade-one);
height: calc(var(--thickness) * 1vmin);
width: 100%;
position: absolute;
top: 0;
transform: translate(0, -50%) rotateX(90deg);
}
.cuboid__side:nth-of-type(2) {
background: var(--shade-two);
height: 100%;
width: calc(var(--thickness) * 1vmin);
position: absolute;
top: 50%;
right: 0;
transform: translate(50%, -50%) rotateY(90deg);
}
.cuboid__side:nth-of-type(3) {
background: var(--shade-three);
width: 100%;
height: calc(var(--thickness) * 1vmin);
position: absolute;
bottom: 0;
transform: translate(0%, 50%) rotateX(90deg);
}
.cuboid__side:nth-of-type(4) {
background: var(--shade-two);
height: 100%;
width: calc(var(--thickness) * 1vmin);
position: absolute;
left: 0;
top: 50%;
transform: translate(-50%, -50%) rotateY(90deg);
}
.cuboid__side:nth-of-type(5) {
background: var(--shade-three);
height: 100%;
width: 100%;
transform: translate3d(0, 0, calc(var(--thickness) * 0.5vmin));
position: absolute;
top: 0;
left: 0;
}
.cuboid__side:nth-of-type(6) {
background: var(--shade-one);
height: 100%;
width: 100%;
transform: translate3d(0, 0, calc(var(--thickness) * -0.5vmin)) rotateY(180deg);
position: absolute;
top: 0;
left: 0;
}
在这个示例中,我们使用了三种颜色。但有时您可能需要更多颜色。此演示将这些颜色组合在一起,并允许您更改作用域内的自定义属性。“厚度”值将改变长方体的拉伸程度。变换和尺寸将影响类名为“extrusion”的包含元素。
为打印机搭建脚手架
首先,我们可以先搭建一个框架,把所有需要的组件都列出来。熟能生巧,这一点会越来越明显。但总的来说,要把所有东西都想象成一个个盒子。这样就能很好地理解如何分解它们。
.scene
.printer
.printer __side.printer__ side--left
.printer __side.printer__ side--right
.printer __tray.printer__ tray--bottom
.printer __tray.printer__ tray--top
.printer__top
.printer__back
如果你想象一下我们想要达到的效果,就会发现两侧的部件在中间留出了空隙。然后,顶部放了一个长方体,背面也放了一个长方体。最后,两个长方体组成了纸托盘。
到了那个阶段,接下来就是填充长方体,看起来是这样的。
.scene
.printer
.printer __side.printer__ side--left
+cuboid()(class="cuboid--side")
.printer __side.printer__ side--right
+cuboid()(class="cuboid--side")
.printer __tray.printer__ tray--bottom
+cuboid()(class="cuboid--tray")
.printer __tray.printer__ tray--top
+cuboid()(class="cuboid--tray")
.printer__top
+cuboid()(class="cuboid--top")
.printer__back
+cuboid()(class="cuboid--back")
注意我们是如何复用类名的,例如 `<c> cuboid--side`。这些长方体很可能具有相同的厚度并使用相同的颜色。它们的位置和大小由包含元素决定。
把它们拼凑起来,我们就能得到类似这样的结果。
演示中,爆炸效果会显示构成打印机的不同长方体。如果关闭挤出功能,则可以看到包含这些元件的平面。
添加一些细节
现在,您可能已经注意到,仅仅在每一面添加颜色并不能展现出所有细节。这归根结底在于如何添加更多细节。根据我们想要添加的内容,我们有不同的选择。
如果是图像或一些基本的颜色变化,我们可以利用background-image渐变等效果进行叠加。
例如,打印机的顶部包含细节和打印机的开口。这段代码处理的是顶部长方体的顶面。渐变效果则用于处理打印机的开口和细节。
.cuboid--top {
--thickness: var(--depth);
--shade-one: linear-gradient(#292929, #292929) 100% 50%/14% 54% no-repeat, linear-gradient(var(--p-7), var(--p-7)) 40% 50%/12% 32% no-repeat, linear-gradient(var(--p-7), var(--p-7)) 30% 50%/2% 12% no-repeat, linear-gradient(var(--p-3), var(--p-3)) 0% 50%/66% 50% no-repeat, var(--p-1);
}
对于熊的标志,我们可以使用background-image伪元素,甚至可以使用伪元素并将其定位。
.cuboid--top > div:nth-of-type(1):after {
content: '';
position: absolute;
top: 7%;
left: 10%;
height: calc(var(--depth) * 0.12vmin);
width: calc(var(--depth) * 0.12vmin);
background: url("https://assets.codepen.io/605876/avatar.png");
background-size: cover;
transform: rotate(90deg);
filter: grayscale(0.5);
}
如果需要添加更多细节,我们可能就需要放弃使用立方体 mixin 了。例如,打印机的顶部会使用一个img元素显示预览屏幕。
.printer__top
.cuboid.cuboid--top
.cuboid__side
.cuboid__side
.cuboid__side
.cuboid__side
.screen
.screen__preview
img.screen__preview-img
.cuboid__side
.cuboid__side
再添加一些细节,我们就可以开始准备纸张了!
纸上之旅
打印机没有纸还能用吗?我们想制作一个动画,展示纸张飞进打印机,然后从另一端射出来。
我们想要类似这样的演示。点击任意位置即可看到纸张被送入打印机并打印出来。
我们可以用长方体在场景中添加一块纸,然后用一个单独的元素来模拟一张纸。
.paper-stack.paper-stack--bottom
+cuboid()(class="cuboid--paper")
.paper-stack.paper-stack--top
.cuboid.cuboid--paper
.cuboid__side
.paper
.paper__flyer
.cuboid__side
.cuboid__side
.cuboid__side
.cuboid__side
.cuboid__side
但是,要实现纸张飞入打印机的动画效果需要一些尝试和调整。最好在开发者工具的检查器中尝试不同的变换。这样可以很好地预览最终效果。通常,使用包装元素也会更方便。我们使用该.paper元素进行转印,然后用它.paper__flyer来实现送纸动画。
:root {
--load-speed: 2;
}
.paper-stack--top .cuboid--paper .paper {
animation: transfer calc(var(--load-speed) * 0.5s) ease-in-out forwards;
}
.paper-stack--top .cuboid--paper .paper__flyer {
animation: fly calc(var(--load-speed) * 0.5s) ease-in-out forwards;
}
.paper-stack--top .cuboid--paper .paper__flyer:after {
animation: feed calc(var(--load-speed) * 0.5s) calc(var(--load-speed) * 0.5s) forwards;
}
@keyframes transfer {
to {
transform: translate(0, -270%) rotate(22deg);
}
}
@keyframes feed {
to {
transform: translate(100%, 0);
}
}
@keyframes fly {
0% {
transform: translate3d(0, 0, 0) rotateY(0deg) translate(0, 0);
}
50% {
transform: translate3d(140%, 0, calc(var(--height) * 1.2)) rotateY(-75deg) translate(180%, 0);
}
100% {
transform: translate3d(140%, 0, var(--height)) rotateY(-75deg) translate(0%, 0) rotate(-180deg);
}
}
你会注意到这里用到了不少calcCSS 自定义属性。为了构建动画时间轴,我们可以使用 CSS 自定义属性。通过引用属性,我们可以计算出动画链中每个动画的正确延迟。纸张的传输和飞行是同时进行的。一个动画负责移动容器,另一个动画负责旋转纸张。这些动画结束后,纸张会连同动画一起送入打印机feed。动画延迟等于同时运行的前两个动画的持续时间之和。
运行这个演示,我把容器元素分别涂成了红色和绿色。我们使用.paper__flyer伪元素来表示纸张。但是,真正起作用的是容器元素。
你可能想知道纸张什么时候会从另一端出来。但实际上,纸张在整个过程中并不是同一个元素。我们用一个元素来处理送入打印机的纸张,而用另一个元素来处理从打印机飞出的纸张。这又是一个利用额外元素让我们的工作更轻松的例子。
该纸张使用多个元素来实现循环,然后将纸张定位到每个元素的边缘。使用更多彩色容器元素运行此演示,即可了解其工作原理。
这又需要一些反复试验,还要思考如何利用容器元素。使用带有偏移量的容器transform-origin可以让我们创建循环。
印刷
一切准备就绪。现在就差实际打印了。为此,我们将添加一个表单,允许用户输入图片的URL。
form.customer-form
label(for="print") Print URL
input#print(type='url' required placeholder="URL for Printing")
input(type="submit" value="Print")
经过一些造型设计,我们就能得到类似这样的效果。
required表单的原生行为以及`and`的使用type="url"意味着我们只接受 URL。我们可以进一步添加 `and`pattern并检查某些图像类型。但是,一些用于随机图像的 URL 并不包含图像类型。例如,“ https://source.unsplash.com/random ”。
表单提交后的行为不符合预期,而且打印动画只在页面加载时播放一次。一种解决方法是,仅在打印机应用特定类时才播放动画。
提交表单后,我们可以请求 URL,然后src在场景中设置图像。其中一张图片是打印机上的屏幕预览,另一张图片是纸张单面的图像。实际上,打印时,我们会为每张打印好的纸添加一个新元素。这样,每张打印件看起来就像被添加到一堆中一样。我们可以移除加载时打印的纸张。
我们先来处理表单提交。我们将阻止默认事件并调用一个PROCESS函数。
const PRINT = e => {
e.preventDefault()
PROCESS()
}
const PRINT_FORM = document.querySelector('form')
PRINT_FORM.addEventListener('submit', PRINT)
该函数将负责向我们的图像源发出请求。
let printing = false
const PREVIEW = document.querySelector('img.screen__preview-img')
const SUBMIT = document.querySelector('[type="submit"]')
const URL_INPUT = document.querySelector('[type="url"]')
const PROCESS = async () => {
if (printing) return
printing = true
SUBMIT.disabled = true
const res = await fetch(URL_INPUT.value)
PREVIEW.src = res.url
URL_INPUT.value = ''
}
我们还设置了一个printing变量,用于true跟踪当前状态,并禁用表单的按钮。
我们为什么请求图片而不是直接在图片上设置 URL 呢?因为我们需要的是图片的绝对 URL。如果我们使用上面提到的“unsplash”URL,然后在不同的图片之间共享它,可能会出现问题。这是因为我们可能会遇到需要显示不同图片的情况。
获取图像源后,我们将预览图像源设置为该 URL,并重置表单的输入值。
为了触发动画,我们可以监听预览图像的“load”事件。当该事件触发时,我们创建一个新的元素来放置要打印的纸张,并将其添加到当前printer元素中。同时,我们printing为打印机添加一个类。我们可以利用这个类来触发纸张动画的第一部分。
PREVIEW.addEventListener('load', () => {
PRINTER.classList.add('printing')
const PRINT = document.createElement('div')
PRINT.className = 'printed'
PRINT.innerHTML = `
<div class="printed__spinner">
<div class="printed__paper">
<div class="printed__papiere">
<img class="printed__image" src=${PREVIEW.src}/>
</div>
</div>
<div class="printed__paper-back"></div>
</div>
`
PRINTER.appendChild(PRINT)
// After a set amount of time reset the state
setTimeout(() => {
printing = false
SUBMIT.removeAttribute('disabled')
PRINTER.classList.remove('printing')
}, 4500)
})
经过一段时间后,我们可以重置状态。另一种方法是对冒泡事件进行防抖处理。但是,由于我们知道动画需要多长时间,所以animationend我们可以使用一个默认的延迟时间。setTimeout
但是我们的打印尺寸不正确。这是因为我们需要将图像缩放到纸张大小。我们需要一小段 CSS 代码来实现这一点。
.printed__image {
height: 100%;
width: 100%;
object-fit: cover;
}
如果打印机前面板上的指示灯能显示打印机是否正在工作,那就更好了。我们可以调整其中一个指示灯的颜色,以便在打印机打印时显示不同的颜色。
.progress-light {
background: hsla(var(--progress-hue, 104), 80%, 50%);
}
.printing {
--progress-hue: 10; /* Equates to red */
}
把这些结合起来,我们就得到了一个用 CSS 和少量 JavaScript 编写的“可运行”的打印机。
就是这样!
本文将介绍如何使用 CSS、少量 JavaScript 和 Pug 来制作一台功能齐全的 3D 打印机。
为了实现这个目标,我们研究了很多不同的方法。其中包括:
- 如何使用 CSS 创建 3D 物体
- 使用巴哥犬混种
- 使用作用域自定义 CSS 属性来保持代码的 DRY 特性。
- 利用挤压技术创建3D场景
- 使用 JavaScript 处理表单
- 使用自定义属性组合动画时间线
制作这些演示的乐趣在于,它们往往提出了不同的问题需要解决。例如,如何创建特定的形状或构建特定的动画。通常,实现某个目标的方法不止一种。
用 3D CSS 你能做出什么很酷的东西?我很想看看!
一如既往,感谢阅读。想看更多内容?欢迎在推特上关注我,或者观看我的直播!
保持精彩!ʕ •ᴥ•ʔ
文章来源:https://dev.to/jh3y/printcss-but-not-how-you-know-it-creating-a-3d-css-printer-1o0k