為了清楚起見,我還沒有在 CSS 中重建 DOOM...。
不,這簡單得多:
渲染 DOOM 的輸出
放入一個 div 中
使用單一background-image: linear-gradient
塊。
所有客戶端都在瀏覽器中!
這很傻嗎?是的
我為什麼要這麼做?我認為您以前從未讀過我的文章,我利用網路技術做一些愚蠢的事情來學習東西...
為什麼要讀這些廢話? - 首先你可以玩用 CSS 渲染的 DOOM!
但說真的,程式碼可以向您展示一些有關與 WASM 互動以及從<canvas>
縮放圖像的有趣的事情。
我還深入研究了從一維陣列中平均像素值,因此如果您對此感興趣,這也可能會有用!
哦,也要特別感謝 Cornelius Diekmann,他為將DOOM 移植到 WASM做出了巨大的貢獻
好了,序言已經講完了,以下是玩「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