DOOM……使用单个 DIV 和 CSS 渲染!🤯🔫💥
为了清楚起见,我还没有在 CSS 中重建 DOOM...。
不,这简单得多:
- 渲染DOOM 的输出
- 放入一个 div 中
- 使用单个
background-image: linear-gradient
块。 - 所有客户端都在浏览器中!
傻吗?是的
我为什么要这么做?我猜你之前肯定没读过我的文章,我经常用 Web 技术做一些傻事来学习……
你为什么要读这些废话? ——好吧,你可以先玩用 CSS 渲染的 DOOM!
但说真的,代码可以向您展示一些有关与 WASM 交互以及从 缩放图像的有趣的事情<canvas>
。
我还深入研究了从一维数组中平均像素值,因此如果您对此感兴趣,这也可能会有用!
哦,还要特别感谢 Cornelius Diekmann,他为将DOOM 移植到 WASM做出了巨大的贡献
好了,序言已经讲完了,下面是玩“CSS* 中的 Doom”的流程。
使用 CSS渲染的Doom
在移动设备上,控制位于游戏下方,而对于 PC,控制则在笔中进行了说明。
您必须点击游戏才能识别输入
(此外,如果您在 PC 上单击游戏下方的按钮将不起作用,因为它们仅适用于移动设备)。
注意:codepen 在某些设备上似乎不支持此功能,如果发生这种情况,您可以在我的服务器上玩它:grahamthe.dev/demos/doom/
那么这里发生了什么?
它看起来就像是低质量的《毁灭战士》对吧?
但是- 如果你敢检查输出,你可能会让 chrome 崩溃......
您看,我们正在做以下事情:
- 从元素中获取输出
doom.wasm
并将其放在<canvas>
元素上。 - 我们隐藏画布元素,然后使用 JS 收集像素数据
- 我们获取该像素数据,找到每 4 个像素的平均值以将分辨率减半。
- 然后我们将这些新像素转换为 CSS 线性渐变。
- 我们将线性渐变应用到游戏 div 上,使用
background-image: linear-gradient
生成的单个线性渐变超过 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
- 起始Y: 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
- 起始Y: 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