打造一個真正能列印的3D CSS印表機!

一段時間以來,我一直用CSS創建這些3D場景作為樂趣——通常在我的直播中進行。

每個演示都是嘗試不同事物或用CSS找出做事方法的機會。我經常做的一件事是接受建議,決定我們在直播中應該嘗試製作什麼。最近的一個建議是製作一個“3D”打印機。這就是我組合出來的!

用CSS製作3D物件

I’ve written about making things 3D with CSS before. The general gist is that most scenes are a composition of cuboids.

要製作一個立方體,我們可以使用CSS變換來定位立方體的各面——神奇的屬性是transform-style。將其設置為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原則。

不止一種方法

與CSS中的許多事物一樣,實現方法多種多樣。通常,你可以從立方體開始構建場景,並根據需要定位元素。然而,這種方式管理起來可能相當複雜。往往需要將元素分組或添加某種容器。

以這個例子為例,椅子作為一個可移動的子場景。

許多近期的案例並不那麼複雜。我傾向於使用擠出(extrusion)技術。這意味著我能夠用2D元素勾勒出我所製作的任何物體。例如,這是我最近創建的直升機:

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

注意我們如何能夠重用類名,例如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);
}

若需增添更豐富的細節,我們可能得放棄使用立方體混入。例如,印表機頂部將有一個使用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元素進行傳輸,然後使用.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);
  }
}

你會注意到其中使用了相當多的calc。為了組合動畫時間軸,我們可以利用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")

經過一些樣式設計,我們得到了如下的界面。

表單的默認行為以及使用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變數為true,用於追蹤當前狀態並禁用表單按鈕。

為何我們要請求圖像而不是直接設置在圖像上?因為我們需要一個圖像的絕對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)
  // 在設定的時間後重置狀態
  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; /* 相當於紅色 */
}

結合這一切,我們就擁有了一台用CSS和一點點JavaScript製作的“工作”打印機。

就是這樣!

我們已經探索了如何使用CSS、一點點JavaScript和利用Pug來製作一個功能性的3D打印機。試著在URL欄位中添加以下圖片鏈接,或者選擇你喜歡的另一個,然後試試看!

https://source.unsplash.com/random

我們涵蓋了許多不同的內容來實現這一點,包括:

  • 如何使用CSS製作3D物品
  • 使用Pug混入
  • 使用作用域自定義CSS屬性以保持代碼的乾淨
  • 使用擠出技術創建3D場景
  • 使用JavaScript處理表單
  • 使用自定義屬性組合動畫時間線

創建這些演示的樂趣在於,它們中的許多都提出了需要克服的不同問題,例如如何創建特定形狀或構建特定動畫。通常有不止一種方法來做某件事。

你可以用3D CSS製作什麼酷炫的東西呢?我很想看看!

一如既往,感謝閱讀。想看更多嗎?來Twitter找我或查看我的直播流!Twitter直播流

關於3D CSS打印機的常見問題(FAQs)

什麼是3D CSS打印機?

A 3D CSS Printer is a unique concept that uses Cascading Style Sheets (CSS), a style sheet language used for describing the look and formatting of a document written in HTML, to create a 3D representation of a printer. This innovative approach allows developers to create interactive and visually appealing web elements that can enhance user experience.

3D CSS打印機是如何工作的?

A 3D CSS Printer works by using CSS properties to create a 3D model of a printer. It uses properties such as transform, perspective, and animation to create the 3D effect. The printer is made up of multiple elements, each styled and positioned to create the overall 3D effect. The animation property is then used to create the printing effect.

我可以自定義3D CSS打印機嗎?

是的,您可以自訂3D CSS印表機。您可以更改顏色、尺寸,甚至動畫速度。這是通過修改印表機的CSS屬性來實現的。例如,您可以通過修改印表機元素的背景顏色屬性來更改顏色。

我如何將3D CSS印表機整合到我的網站中?

將3D CSS印表機整合到您的網站涉及將CSS和HTML代碼複製到您的網站代碼中。您需要確保CSS代碼包含在HTML文件的head部分,而HTML代碼則放置在您希望印表機出現的網頁位置。

是否可以對3D CSS印表機進行動畫處理?

是的,可以對3D CSS印表機進行動畫處理。動畫是通過使用CSS動畫屬性實現的。此屬性允許您創建關鍵幀,定義動畫的開始和結束狀態,以及任何中間步驟。

哪些瀏覽器支持3D CSS印表機?

3D CSS印表機應該可以在所有支持CSS transform和animation屬性的現代瀏覽器上運行。這包括Google Chrome、Mozilla Firefox、Safari和Microsoft Edge等瀏覽器。

我可以用3D CSS印表機進行商業用途嗎?

是的,您可以使用3D CSS印表機進行商業用途。然而,檢查您使用的任何代碼的許可條款以確保合規始終是個好主意。

我需要哪些技能來創建一個3D CSS印表機?

要打造一台3D CSS印表機,你需要對HTML和CSS有深入的了解。熟悉transform、perspective和animation等CSS屬性是必要的。具備基本的3D建模知識也會有所幫助,但並非必須。

我能否在3D CSS印表機中使用JavaScript?

當然可以,在3D CSS印表機中使用JavaScript是可行的。雖然僅用CSS就能建立印表機,但JavaScript可以用來增添互動性,例如根據使用者行為啟動或停止動畫。

有沒有更多學習3D CSS印表機的資源?

網路上有許多學習3D CSS印表機的資源。像是SitePoint、CSS-Tricks和MDN Web Docs等網站提供了豐富的CSS動畫和3D變換教程與指南。YouTube上也有許多相關主題的視頻教程。

Source:
https://www.sitepoint.com/3d-css-printer/