阿川私房教材:
學 JavaScript 前端,帶作品集去面試!

63 個專案實戰,寫出作品集,讓面試官眼前一亮!

立即開始免費試讀!

為了清楚起見,我還沒有在 CSS 中重建 DOOM...。

不,這簡單得多:

  • 渲染 DOOM 的輸出

  • 放入一個 div 中

  • 使用單一background-image: linear-gradient塊。

  • 所有客戶端都在瀏覽器中!

這很傻嗎?是的

我為什麼要這麼做?我認為您以前從未讀過我的文章,我利用網路技術做一些愚蠢的事情來學習東西...

為什麼要讀這些廢話? - 首先你可以玩用 CSS 渲染的 DOOM!

但說真的,程式碼可以向您展示一些有關與 WASM 互動以及從<canvas>縮放圖像的有趣的事情。

我還深入研究了從一維陣列中平均像素值,因此如果您對此感興趣,這也可能會有用!

哦,也要特別感謝 Cornelius Diekmann,他為將DOOM 移植到 WASM做出了巨大的貢獻

好了,序言已經講完了,以下是玩「CSS* 中的 Doom」的流程。

使用 CSS渲染的Doom

在行動裝置上,控制位於遊戲下方,而對於 PC,控制則在筆中進行了說明。

您必須點擊遊戲才能辨識輸入

(此外,如果您在 PC 上點擊遊戲下方的按鈕將不起作用,因為它們僅適用於行動裝置)。

注意:codepen 在某些裝置上似乎不支援此功能,如果發生這種情況,您可以在我的伺服器上播放它: grahamthe.dev/demos/doom/

{% codepen https://codepen.io/GrahamTheDev/pen/yyyjyBK %}

那麼這裡發生了什麼事?

它看起來就像是低品質的《毀滅戰士》對吧?

但是- 如果你敢檢查輸出,你可能會讓 chrome 崩潰...

您看,我們正在做以下事情:

  • doom.wasm取得輸出並將其放在<canvas>元素上。

  • 我們隱藏畫布元素,然後使用 JS 收集像素資料

  • 我們獲取該像素資料,找到每 4 個像素的平均值以將解析度減半。

  • 然後我們將這些新像素轉換為 CSS 線性漸層。

  • 我們使用background-image: linear-gradient將線性漸層應用於遊戲 div

產生的單一線性漸變超過 1MB CSS(實際上是 2MB),因此很遺憾我無法在這裡向您展示它的樣子(或在 CodePen 上!)因為它太大了!

我們每秒建立 60 多次...Web 瀏覽器和 CSS 解析能夠處理這一點非常令人印象深刻!

現在我不會涵蓋所有內容,但有一件有趣的事情是將<canvas>資料轉換為陣列,然後獲取用於重新縮放的像素資料,所以讓我們來介紹一下:

取得平均像素顏色的簡單方法

我遇到了一個問題。

使用 CSS 以 640*400 渲染遊戲讓 Web 瀏覽器哭了!

所以我需要將圖像縮小到 320*200。

有很多方法可以做到這一點,但我選擇了一種簡單的像素平均方法。

程式碼中有一些有趣的東西,但我認為調整大小功能是最有趣的功能之一,在某些時候可能會對您有用。

如果您以前從未處理過像素資料陣列,那麼它會特別有趣(因為它是 2D 影像的 1D 表示,所以遍歷它很有趣!)。

以下是取得像素資料平均值的程式碼以供參考:

function rgbaToHex(r, g, b) {
  return (
    "#" +
    [r, g, b].map((x) => Math.round(x).toString(16).padStart(2, "0")).join("")
  )
}

function averageBlockColour(data, startX, startY, width, blockSize) {
  let r = 0, g = 0, b = 0;

  for (let y = startY; y < startY + blockSize; y++) {
    for (let x = startX; x < startX + blockSize; x++) {
      const i = (y * width + x) * 4;
      r += data[i];
      g += data[i + 1];
      b += data[i + 2];
    }
  }

  const size = blockSize * blockSize;
  return rgbaToHex(r / size, g / size, b / size);
}

如果您想要對影像進行簡單的大小調整(例如縮圖),則此averageBlockColour函數非常有用。

它僅限於乾淨的倍數(2 像素、3 像素塊大小等),但可以很好地說明如何獲取一組像素的平均顏色。

有趣的部分是const i = (y * width + x) * 4

這是因為我們使用了Uint8ClampedArray ,其中每個像素由 4 個位元組表示,1 個位元組代表紅色,1 個位元組代表綠色,1 個位元組代表藍色,1 個位元組代表 Alpha 通道。

我們使用它是因為我們需要移動一維陣列並抓取二維像素資料。

像素資料解釋

我們需要能夠以塊為單位移動來平均顏色。

這些區塊的寬度為 X 像素,高度為 X 像素。

這意味著跳過圖像行的其餘部分來獲取第二行(或第三行、第四行…)資料,因為所有內容都儲存在一條長行中。

讓我嘗試用一個簡短的“圖表”來解釋:

Image (3x2 pixels):

Row 0:  RGBA0: (0,0) RGBA1: (1,0) RGBA2: (2,0)
Row 1:  RGBA3: (0,1) RGBA4: (1,1) RGBA5: (2,1)

Array data:   [ RGBA0 | RGBA1 | RGBA2 | RGBA3 | RGBA4 | RGBA5 ]
Image pos:      (0,0)   (1,0)   (2,0)   (0,1)   (1,1)   (2,1)
Array pos:       0-3     4-7     8-11   12-15   16-19   20-23

現在您可以看到我們的圖像的每一行是如何一個接一個地堆疊的,您可以明白為什麼我們需要向前跳。

所以我們的函數需要:

  • 資料:我們的像素資料陣列,

  • startX:我們想要資料的像素最左邊的位置(二維)

  • startY:我們想要資料的像素的最頂部位置(二維)

  • 寬度:圖像資料的總寬度(因此我們可以跳過行)

  • blockSize:我們想要平均的像素數的高度和寬度。

如果我們想要得到第一個 2 x 2 像素區塊的平均值,我們可以這樣傳遞:

  • 資料: [ RGBA0 | RGBA1 | RGBA2 | RGBA3 | RGBA4 | RGBA5 ]

  • 起始X: 0

  • 開始: 0

  • 寬度: 3

  • 區塊大小: 2

在我們的循環中我們得到:

//const i = (y * w + x) * 4;

  const i = (0 * 3 + 0) * 4 = start at array position 0: RGBA0
  const i = (1 * 3 + 0) * 4 = start at array position 12: RGBA3
  const i = (0 * 3 + 1) * 4 = start at array position 4: RGBA1
  const i = (1 * 3 + 1) * 4 = start at array position 16: RGBA4

像素資料如下:

(0,0, 1,0)

(0,1,1,1)

然後,如果我們想獲得接下來 4 個像素的平均值,我們只需傳遞:

  • 資料: [ RGBA0 | RGBA1 | RGBA2 | RGBA3 | RGBA4 | RGBA5 ]

  • startX: 1 <-- 將起始位置加 1

  • 開始: 0

  • 寬度: 3

  • 區塊大小: 2

在我們的循環中我們現在得到:

//const i = (y * w + x) * 4;

  const i = (0 * 3 + 1) * 4 = start at array position 4: RGBA1
  const i = (1 * 3 + 1) * 4 = start at array position 16: RGBA4
  const i = (0 * 3 + 2) * 4 = start at array position 8: RGBA2
  const i = (1 * 3 + 2) * 4 = start at array position 20: RGBA5

像素資料如下:

(1,0, 2,0)

(1,1,2,1)

現在我們有了原始像素資料

其餘過程更容易理解

我們有一些像素的 RGBA 資料 - 可能看起來像[200,57,83,255]

我們只需將每個部分的值加起來:

  r += data[i]; //red
  g += data[i + 1]; //green
  b += data[i + 2]; //blue
  //we deliberately don't grab the "a" (alpha) channel as it will always be 255 - the same as opacity: 1 or non-transparent.

一旦我們對 4 個像素(y 和 x 的 2 個循環)完成此操作,我們將得到這 4 個像素的總 R、G 和 B 值(在第一個實例中 y 為 0 和 1,x 為 0 和 1,在第二個實例中 y 為 0 和 1,現在 x 為 1 和 2)。

然後我們取它們的平均值:

  const size = blockSize * blockSize; // (2 * 2)
  //               avg red,    avg green,  avg blue
  return rgbaToHex(r / size,   g / size,   b / size);    

然後我們將其傳遞給函數,該函數將紅色、綠色和藍色的變數轉換為有效的十六進位值。

function rgbaToHex(r, g, b) {
  return (
    "#" +
    [r, g, b].map((x) => Math.round(x).toString(16).padStart(2, "0")).join("")
  )
}

讓我們將其分解為幾個步驟:

  • #開頭

  • 依序取得每個 R、G、B 值並執行以下操作:

  • 對數值進行四捨五入(因為我們有平均值,所以需要整數)

  • 將原始值轉換為十六進位(0-9A-F 將字串更改為 base16)

  • 確保較小的數字(0-15)用 0 填充,這樣我們總是能得到 R、G 和 B 值各 2 位數字(因此我們總是能得到總共 6 個字元(

  • 將 R、G 和 B 十六進位值連接在一起。

因此,如果我們將 [200.2,6.9,88.4] 作為我們的 R、G 和 B 值,我們將得到:

A = 10, B = 11, C = 12, D = 13, E = 14, F = 15

- Start -> "#"
- 200.2 -> round (200) -> (12 * 16) + 8 = C8 
- 6.9   -> round (7)   -> (0 * 16)  + 7 =  7
- 88.4  -> round (88)  -> (5 * 16)  + 8 = 58
- Pad   -> C8, 07, 58
- Join  -> #C80758

我們有它,R200,G7,B88 是十六進位程式碼#c80758。

就這樣結束了

其中有一些關於向 WASM 應用程式發送命令的非常有趣的部分,我鼓勵您自己去探索這些部分,以及我之前提到的 Cornelius Diekmann 撰寫的關於將 DOOM 移植到 WASM 的超級有趣的文章。


如果您發現這篇文章很有趣(或令人分心…哈哈),那麼請不要忘記按讚並與他人分享。

它確實幫助了我!


下期再見,祝大家週末愉快


原文出處:https://dev.to/grahamthedev/doomrendered-using-a-single-div-and-css-1fal


共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。

阿川私房教材:
學 JavaScript 前端,帶作品集去面試!

63 個專案實戰,寫出作品集,讓面試官眼前一亮!

立即開始免費試讀!