创建一个实际打印的3D CSS打印机

2021-07-31

有一段时间以来,我一直在用CSS创建这些3D场景来取乐——通常是在我的直播 .


每一个演示都是一个尝试不同的东西的机会,或者找到用CSS做事情的方法。我经常做的一件事就是对我们应该尝试和提出的建议提出建议。最近的一个建议是用3D打印的打印机。这是我的拼图!


用CSS制作三维物体

我写过以前用CSS制作3D的东西. 总的要点是大多数场景都是由长方体组成的。

为了制作一个长方体,我们可以使用CSS变换来定位长方体的边-神奇的属性是transform-style. 将此设置为保留-3d允许我们转换三维元素:

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

一旦你创造了一些这样的场景,你就开始寻找加速的方法。我喜欢用Pug作为HTML预处理器。混合能力给了我一种更快地创建长方体的方法。本文中的标记示例使用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位置

直到我们将长方体放入场景中并旋转它,这并不是很令人印象深刻。同样,我使用自定义属性来操纵场景,同时我在制作一些东西。 数据图形用户界面这里非常方便


如果您检查演示,使用控制面板更新场景中的自定义CSS属性。CSS自定义属性的这种作用域可以节省大量重复代码,并保持干燥。

不止一种方式

就像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制造直升机的长方体的属性。它提供了一个二维映射开始时是什么样子的概念。


这就是如何使用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;
}

我们在这个例子中使用了三种色调。但有时你可能需要更多。这个演示将它们组合在一起,但允许您更改作用域的自定义属性。“厚度”值将更改长方体的挤出。变换和尺寸将影响类为“拉伸”的包含元素。


搭建打印机

首先,我们可以把我们需要的东西都搭成脚手架。随着实践,这一点变得更加明显。但总的原则是把所有的东西都想象成盒子。这给了你一个很好的方法来分解一些东西:

.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")      

注意如何重用类名,例如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

但是,设置纸张飞入打印机的动画需要一些尝试和错误。在DevTools检查器中使用不同的转换是明智的。这是一个很好的方式来看看事情会怎样。通常,使用包装元素也更容易。我们使用.paper元素进行传输,然后使用.纸片传单要设置送纸动画:

: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);
  }
}

你会注意到calc那里的用法。我们可以使用CSS自定义属性来组成动画时间线。参照属性,我们可以计算链中每个动画的正确延迟。纸张同时传送和飞行。一个动画控制移动容器,另一个控制旋转纸张。一旦这些动画结束,纸张将通过喂养动画。动画延迟等于同时运行的前两个动画的持续时间。

运行这个演示,我把容器元素涂成红色和绿色。我们利用.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")

通过一些造型,我们得到了这样的东西。


形式的固有行为和使用requiredtype="url"意味着我们只接受一个URL。我们可以用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变量到是的,我们将使用它来跟踪当前状态,并禁用窗体的按钮。

为什么我们要请求图像而不是在图像上设置它?我们需要一个图像的绝对URL。如果我们使用上面提到的“Unsplash”URL,然后在图像之间共享它,这可能行不通。这是因为我们可能会遇到一些场景,我们会显示不同的图像。

有了图像源之后,我们将预览图像源设置为该URL并重置表单的输入值。

为了触发动画,我们可以钩住预览图像的“加载”事件。当事件激发时,我们为要打印的纸张创建一个新元素,并将其附加到printer元素。同时,我们添加了一个印刷给我们的打印机上课。我们可以使用此选项来触发纸张动画的第一部分:

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事件。但我们可以用 设置超时,我们知道动画需要多长时间。


不过,我们的印刷品没有达到正确的比例。这是因为我们需要将图像缩放到一张纸上。我们需要一小块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打印机。尝试在URL字段中添加以下图像链接,或者您选择的另一个链接,然后进行旋转!

https://source.unsplash.com/random


为了实现这一目标,我们介绍了许多不同的方法,包括:

  • 如何用CSS制作三维物体
  • 使用哈巴狗混合蛋白
  • 使用作用域自定义CSS属性保持干燥
  • 使用挤出创建三维场景
  • 使用JavaScript处理表单
  • 使用自定义特性组合动画时间线

创建这些演示的乐趣在于它们中的许多都提出了不同的问题需要克服,例如如何创建某些形状或构造某些动画。做某事的方法往往不止一种。

你能用3D CSS做什么酷的东西?我很想看看!

一如既往,谢谢你的阅读。想看更多吗?快来找我推特或者看看我的直播 !