利用 box-shadow 畫出任何圖案


Posted by ArvinH on 2020-02-01

前言

大約在兩年前我曾經寫過一篇文章介紹如何用 CSS 繪圖 - 用 CSS 畫畫的小技巧,該文章的最後我有稍微提到我們能夠利用 CSS3 的 box-shadow 屬性來製造出 Pixel 風格的圖案。然而,所有圖案不都是由 pixel 組成的嗎?如果我們能夠用 box-shadow 畫出 Pixel Art,那只要 Pixel 數量足夠,size 夠細緻,應該是能夠繪製出任何圖形的吧?

不過在用 CSS 畫畫的小技巧這篇文章中,所製作的是比較簡單的文字,透過直接編輯 box-shadow 還在可接受範圍中,但如果是想要繪製複雜一點的人物角色,例如鋼鐵人src

iron-man

這如果一格一格手動對照,然後撰寫 box-shadow,比登天還難,更別提想繪製出比 Pixel Art 細緻一點的圖案了。

網路上對於 box-shadow 的運用,大多圍繞在 Pixel Art 的實作,例如 Una Kravets 的部落格 介紹了如何使用 SCSS 與陣列來產生 CSS Pixel Art;上面鋼鐵人原圖也是利用相同原理Pixelator 則是讓你能線上繪製自己喜歡的 Pixel Art,並且產生出對應的 box-shadow

好在 Codepen 上高手如雲,被我發現一篇利用 Angular 實作圖片轉 box-shadow 的版本,實作方式很有意思,我用 svelte 改寫了一個版本,今天就來分享一下實作細節!

先看個成果:

Gif Demo:
gif-demo

Live Demo:

可以下載這個範例圖來上傳,效果會比較好:demo-mario

Box-Shadow

在開始前,先複習一下 box-shadow,CSS3 的 box-shadow 屬性可以設定多個值,每個值代表著一個 box-shadow 的 x 位移(x-offset),y 位移(y-offset),陰影模糊半徑(shadow blur radii),陰影擴散半徑(shadow spread radii) 和顏色(color)。

由於 允許設置多個值可控制 X 與 Y 位移 這兩個特質,box-shadow 非常適合用來組合成圖片,尤其是 Pixel Art。以黑白相間的棋盤為例:

<div style="
    width: 40px;
    height: 40px;
    box-shadow: 0 40px #000, 40px 0 #000;
    border: 1px solid #000;
"></div>

box-shadow-explain

依照這個原理,就能組合成複雜一點的 Pixel Art:

但說實話,要手動撰寫 box-shadow 來組合出這個小愛心,大概就去掉半條命了,還是得依靠 Pixelator 來繪製並產生 CSS。

圖片轉 box-shadow 實作原理

在複習完 box-shadow 組成圖片的原理後,應該不難推斷出圖片轉 box-shadow 的做法。

概念上就是先定義出一個 grid system,將圖片切割成一塊一塊的單位,接著計算出每個單位區塊的 x-offset 與 y-offset,然後放入對應顏顏色,這樣就能組合出一個 unit 所對應的 box-shadow 值,依此類推把每個單位區塊都轉換完即可。

實作上的步驟比較繁瑣一些,但概念是相同的:

  • 利用 URL.createObjectURL(event.target.files[0]); 將圖片檔案轉換成 Image 物件。
  • 在 image onload 時,透過 canvas 2d context 的 drawImage() 函式將圖片繪製到 canvas 上。
  • 接著再以 canvas 2d context 的 getImageData 取得一個以一維陣列存放的圖片資訊。
  • 遍歷該一維陣列內的圖片資訊,組合出 box-shadow 的值。

上述步驟中,最關鍵的就是最後一點,canvas 2d context 的 getImageData 函式會回傳一個一維陣列 - Unit8ClampedArray,裡面包含了圖片每個 unit 的 RGBA 值(值段區間為 0 ~ 255)。

利用這個一維陣列,我們就可以知道載入的圖片有多少 unit(grid system),每個 unit 又各自是什麼顏色,進而推算出 box-shadow 每一個值的 x-offset、y-offset 與顏色。這也是為何我們需要先將圖片繪製到 Canvas 的原因。

關鍵程式碼如下:

const buildPixelArt = (pixelSize = 1, image, canvas, canvasContext) => {
  const width = image.width;
  const height = image.height;

  canvas.width = width;
  canvas.height = height;
  canvasContext.drawImage(image, 0, 0);

  const boxShadow = [];
  const { data: imageData } = canvasContext.getImageData(0, 0, width, height);

  for (let i = 0, n = imageData.length; i < n; i += 4) {
    var a = imageData[i + 3];
    if (a > 0) {
      const row = Math.ceil((i + 4) / 4 / width - 1);
      const col = (i + 4) / 4 - row * width + 1;
      boxShadow.push(
        col * pixelSize +
          "px " +
          row * pixelSize +
          "px " +
          getColor(imageData[i], imageData[i + 1], imageData[i + 2], a / 255)
      );
    }
  }

Unit8ClampedArray 陣列裡面,每四個 indices 為一單位,分別為該 unit 的 RGBA 值,所以在迴圈中我們以 4 為遞增單位,並以此為計算二維平面中 rowcol 的基礎。

計算出一維陣列內每個 unit 在二維平面上的行與列後,個別乘上定義好的 pixelSize,就能算出該 unit 在 box-shadow 值中的 x-offset 與 y-offset,然後聯同顏色值一起 push 到 boxShadow 陣列中。

最後利用預先寫好的 css template,將 boxShadow 整合進去即可產生需要的 css style:

 const generatedCss =
    "#pixel-art {\n" +
    "  height: " +
    height * pixelSize +
    "px;\n" +
    "  width: " +
    width * pixelSize +
    "px;\n" +
    "}\n" +
    "#pixel-art:after {\n" +
    '  content: "";\n' +
    "  position: absolute;\n" +
    "  width: " +
    pixelSize +
    "px;\n" +
    "  height: " +
    pixelSize +
    "px;\n" +
    "  box-shadow:\n" +
    "    " +
    boxShadow.join(",\n    ") +
    ";\n" +
    "}";
  return generatedCss;

載入產生的 CSS 後,就可以看到我們上傳的圖片重新以 box-shadow 的形式被重組在頁面上,單單一個 div 就能繪製出任何圖形!蠻酷的吧!

完整程式碼請到 CodeSandbox 上翻閱,大部分邏輯都在 PixelArtArea.svelte 元件與 buildPixelArt.js 這隻檔案,其餘 svelte 部分的程式碼也很好理解,不過我是第一次用 svelte,若有使用不當的地方歡迎指教!

注意事項

利用 box-shadow 繪圖基本上沒什麼實質意義,就只是好玩而已,千萬不要把這用到正式環境,效能之差會把使用者端的瀏覽器搞當機的。這次的範例也只能吃得下像素較小的圖片,若是上傳了較大的檔案,打開 Devtool 時就會發現你的頁面 crash 了...
另外,產生完的 CSS,範例中我是直接 append 到 head 下,所以若是在沒有重整頁面的狀況下上傳別的圖片,就會再度 append 新的 css 進去,久了以後 head 也會越來越肥。

結論

CSS 真的很有趣,能做出許多意料之外的事,雖然絕大多數沒什麼用處,但這種技術上的創意應用所帶來的興奮感,正是繁忙於日常的開發者們所需要的吧!

資料來源

  1. Una Kravets 的部落格
  2. Convert an image to CSS Box Shadows
  3. iron man

#css #box-shadow #drawing









Related Posts

(100Days Challenge of Python) Day5: For loop

(100Days Challenge of Python) Day5: For loop

初心者的計概--SESSION 與 COOKIE

初心者的計概--SESSION 與 COOKIE

【JS幼幼班】Step.04 基本語法:變數

【JS幼幼班】Step.04 基本語法:變數




Newsletter




Comments