Print.css but not how you know it – Creating a 3D CSS Printer
Tags: webdev, tutorial, javascript, css
Published: 2021-11-08 18:39:56

For a while now I've been creating these 3D scenes with CSS for fun. Usually on my [live stream](https://twitch.tv/jh3yy).

{% twitter 1375639377150410758 %}


Each demo is an opportunity to try something different or work out ways to do things with CSS. One thing I often do is take suggestions for what we should try and make on the stream. A recent suggestion was a 3D printer. As in a "3D" printer as opposed to an ink/laserjet. And here's what I put together!

{% twitter 1385237899126480901 %}

## Making things 3D with CSS

I've wrote about [making things 3D with CSS before](https://css-tricks.com/css-in-3d-learning-to-think-in-cubes-instead-of-boxes/). The general gist is that most scenes are a composition of cuboids.

To make a cuboid, we can use CSS transforms to position the sides of a cuboid. The magic property being `transform-style`. Setting this to `preserve-3d` allows us to transform elements on the third dimension.

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

{% youtube 63ZFh6T_iXc %}

Once you create a few of these scenes, you start picking up ways to speed things up. I like to use Pug as a HTML preprocessor. The mixin ability gives me a way to create cuboids speedier. The markup examples in this article use Pug. But, for each CodePen demo you can use the “View Compiled HTML” option to see the HTML output.

```pug
mixin cuboid()
  .cuboid(class!=attributes.class)
    - let s = 0
    while s < 6
      .cuboid__side
      - s++
```

Using this code

```pug
+cuboid()(class="printer__top")
```

Would produce

```html
<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>
```

Then I have a set block of CSS I use to lay out the cuboids. The joy here is that we can leverage CSS custom properties to define the properties of a cuboid. As shown in the video above.

```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%;
}
```

Using custom properties, we can control various characteristics of the cuboids, etc.

- `--width`: The width of a cuboid on the plane
- `--height`: The height of a cuboid on the plane
- `--depth`: The depth of a cuboid on the plane
- `--x`: The X position on the plane
- `--y`: The Y position on the plane

This isn't very impressive until we put the cuboid into a scene and rotate it. Again, I use custom properties to manipulate the scene whilst I work on making something. Dat.GUI comes in super handy here.

{% codepen https://codepen.io/jh3y/pen/BaKqQLJ %}

If you inspect the demo, using the control panel updates custom CSS properties on the scene. This scoping of CSS custom properties saves a lot of repeated code and keeps things DRY.

## More Than One Way

Much like many things in CSS, there's more than one way to do it. Often you can compose a scene from cuboids and position things as and when you need. It can get tricky to manage though. Often there's a need to group things or add some type of container.

Consider this example where the chair is it’s own sub-scene that can be moved around.

{% codepen https://codepen.io/jh3y/pen/QWELerg %}

Many recent examples aren’t as complex. I've been reaching for extrusion. This means I'm able to map out whatever I'm making in 2D elements. For example, a helicopter I recently created.

```pug
.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
```

Then we can drop cuboids into all the containers using the mixin. Then apply a required "thickness" to each cuboid. The thickness becomes dictated by scoped custom properties. This demo toggles the `--thickness` property for cuboids that make the helicopter. It gives an idea of what the 2D mapping looked like to start with.

{% codepen https://codepen.io/jh3y/pen/yLMvXQG %}

That's the gist of how to go about making 3D things with CSS. Digging into the code will unveil some tricks for sure. But, in general, scaffold a scene, populate with cuboids, and color the cuboids. You'll often want some different shades of a color so we can differentiate the sides of a cuboid. Any extra details are either things that we can add to a cuboid side or transforms we can apply to a cuboid. For example, rotating and moving on the Z axis.

Let’s consider a stripped down example.

```pug
.scene
  .extrusion
    +cuboid()(class="extrusion__cuboid")
```

The new CSS for creating a cuboid with extrusion could look like this. Note how we're including scoped custom properties for the color of each side too. It would be wise to drop some defaults under the `:root` here or fallback values.

```css
.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;
}
```

We've gone with three shades for this example. But, sometimes you may need more. This demo puts that together but allows you to change scoped custom properties. The "thickness" value will change the extrusion of the cuboid. The transforms and dimensions will affect the containing element with the class "extrusion".

{% codepen https://codepen.io/jh3y/pen/JjWpMWb %}

## Scaffolding a Printer

To start, we can scaffold out all the pieces we need. With practice this becomes more obvious. But, the general rule is trying to visualise everything as boxes. That gives you a good idea of how to break something up.

```pug
.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
```

If you visualise the aim of what we're going for here. The two side pieces leave a gap in the middle. Then we have a cuboid that sits across the top and one that fills the back. Then two cuboids to make up the paper tray.

Once you're at that stage, it's a case of populating the cuboids which looks like this.

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

Note how we're able to reuse the classnames such as `cuboid--side`. These cuboids are likely to be the same thickness and use the same colors. Their position and size gets dictated by the containing element.

Piecing it together, we can can get something like this.

{% codepen https://codepen.io/jh3y/pen/YzZaqrR %}

Exploding the demo shows the different cuboids that make up the printer. If you turn off the extrusion, you can see the flat containing elements.

## Adding Some Detail

Now. You may have noticed that there's more detail than what adding colors to each side would provide. And this comes down to finding ways to add extra detail. We've got different options depending on what we want to add.

If it's an image or some basic color changes, we can make use of `background-image` to layer up gradients, etc.

For example, the top of the printer has details and the opening of the printer. This code addresses the top side of the top cuboid. The gradient handles the opening of the printer and the details.

```css
.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);
}
```

For the bear logo, we could use a `background-image` or even reach for a pseudo-element and position it.

```css
.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);
}
```

If we need to add more extensive details, then we are likely going to have to break out of using our cuboid mixin. For example, the top of our printer is going to have a preview screen using an `img` element.

```pug
.printer__top
  .cuboid.cuboid--top
    .cuboid__side
    .cuboid__side
    .cuboid__side
    .cuboid__side
      .screen
        .screen__preview
          img.screen__preview-img
    .cuboid__side
    .cuboid__side
```

Add some more details and we're ready to get some paper in the mix!

{% codepen https://codepen.io/jh3y/pen/poeLEjq %}

## Paper Journey

What's a printer without some paper? We want to animate some paper flying into the printer and getting shot out the other end.

We want something like this demo. Click anywhere to see a piece of paper fed into the printer and printed.

{% codepen https://codepen.io/jh3y/pen/QWpmvdb %}

We can add a block of paper to the scene with a cuboid and then use a separate element to act as a single sheet of paper.

```pug
.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
```

But, animating the paper flying into the printer takes some trial and error. It's wise to play with different transforms in the DevTools inspector. This is a good way to see how things will look. Often, it’s easier to use wrapper elements too. We use the `.paper` element to make the transfer and then use `.paper__flyer` to animate feeding the paper.

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

You'll notice that there's a fair bit of `calc` usage in there. To compose the animation timeline we can make use of CSS custom properties. Referring to a property, we can calculate the correct delays for each animation in the chain. The paper transfers and flies at the same time. One animation handles moving the container, another handles rotating the paper. Once those animations end, the paper gets fed into the printer with the `feed` animation. The animation delay is equal to the duration of the first two animations that run at the same time.

Run this demo where I’ve colored the container elements red and green. We make use of `.paper__flyer`'s pseudo-element to represent the piece of paper. But, the container elements do the hard work.

{% codepen https://codepen.io/jh3y/pen/XWMEgjB %}

You may be wondering when the paper comes out at the other end. But, in fact, the paper isn't the same element throughout. We use one element to go into the printer. And another element for the paper when it flies out of the printer. Another instance where extra elements will make our life easier.

The paper uses more than one element to do the loop and then the paper gets positioned to the edge of that element. Running this demo with more colored container elements shows how it's working.

{% codepen https://codepen.io/jh3y/pen/YzZaQEX %}

Once again, it's a bit of trial and error plus thinking about how we can leverage the use of container elements. Having a container with an offset `transform-origin` allows us to create the loop.

## Printing

We have everything in place. Now it's a case of actually printing something. To do this, we're going to add a form that allows users to pass in the URL of an image.

```pug
form.customer-form
  label(for="print") Print URL
  input#print(type='url' required placeholder="URL for Printing")
  input(type="submit" value="Print")
```

With some styling, we get something like this.

{% codepen https://codepen.io/jh3y/pen/wvJmqLO %}

The native behavior of forms and the use of `required` and `type="url"` means we only accept a URL. We could take this further with a `pattern` and check for certain image types. But, some good URL for random images don't include the image type. For example, "https://source.unsplash.com/random".

Submitting our form doesn't behave as we want and also the printing animation runs once on load. A way around this would be to only run the animation when a certain class gets applied to the printer.

When we submit the form, we can make a request for the URL and then set the `src` for images in our scene. One image being the screen preview on the printer. The other being an image on one side of the paper. In fact, when we print, we are going to add a new element for each printed piece of paper. That way each print looks like it gets added to a pile. We can remove the piece of paper we have on load.

Let’s start by handling the form submission. We are going to prevent the default event and call a `PROCESS` function.

```javascript
const PRINT = e => {
  e.preventDefault()
  PROCESS()
}

const PRINT_FORM = document.querySelector('form')
PRINT_FORM.addEventListener('submit', PRINT)
```

This function will handle making the request for our image source.

```javascript
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 = ''
}
```

We also set a `printing` variable to `true` which we will use to track current state, and disable the form’s button.

The reason we make a request for the image instead of setting it on the image? We want an absolute URL to an image. If we use the "unsplash" URL mentioned above and then share it between the images, this might not work. That's because we can run into scenarios where we have different images displayed.

Once we have the image source, we set the preview image source to that URL and reset the form’s input value.

To trigger the animation, we can hook into the "load" event of our preview image. When the event fires, we create a new element for the piece of paper to print and append it to the `printer` element. At the same time, we add a `printing` class to our printer. We can use this to trigger the first part of our paper animation.

```javascript
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)
})
```

After a set amount of time, we can reset the state. An alternative approach would be to debounce a bubbling `animationend` event. But, we can use a `setTimeout` as we know how long the animation will take.

{% codepen https://codepen.io/jh3y/pen/VwpXMdV %}

Our printing isn't to the correct scale though. And that's because we need to scale the image to the piece of paper. We need a small piece of CSS for this.

```css
.printed__image {
  height: 100%;
  width: 100%;
  object-fit: cover;
}
```

It would also be neat if the lights on the front of the printer communicated that the printer is busy. We could adjust the hue of one of the lights when the printer is printing.

```css
.progress-light {
  background: hsla(var(--progress-hue, 104), 80%, 50%);
}
.printing {
  --progress-hue: 10; /* Equates to red */
}
```

Put that together and we’ve got a “working” printer made with CSS and a smidge of JavaScript.

{% codepen https://codepen.io/jh3y/pen/dyvmVjQ %}

## That’s It!

A look at how we can make a functional 3D Printer with CSS, a smidge of JavaScript, and leveraging Pug.

{% codepen https://codepen.io/jh3y/pen/BaWrwEo %}

We covered a bunch of different things to achieve this. Some of the things we covered:

- How to make 3D things with CSS
- Using Pug mixins
- Using scoped custom CSS properties to keep things DRY
- Using extrusion to create 3D scenes
- Handling forms with JavaScript
- Composing animation timelines with custom properties

The joy of creating these demos is that many of them pose different problems to overcome. How to create certain shapes or construct certain animations. There’s often more than one way to do something.

What cool things could you make with 3D CSS? I’d love to see!

As always, thanks for reading. Wanna see more? Come find me on [Twitter](https://twitter.com/intent/follow?screen_name=jh3yy) or check out my [live stream](https://twitch.tv/jh3yy)!

**Stay Awesome! ʕ •ᴥ•ʔ**