Skip to content

Instantly share code, notes, and snippets.

@DerGoogler
Created May 18, 2025 13:24
Show Gist options
  • Save DerGoogler/7720c7c944f9b92f73ee7c3ee7d4e640 to your computer and use it in GitHub Desktop.
Save DerGoogler/7720c7c944f9b92f73ee7c3ee7d4e640 to your computer and use it in GitHub Desktop.
WebUI X Snake WebGPU
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, user-scalable=no"
/>
<title>Snake Game</title>
<!-- Window Safe Area Insets -->
<link rel="stylesheet" type="text/css" href="https://mui.kernelsu.org/internal/insets.css" />
<!-- App Theme which the user has currently selected -->
<link rel="stylesheet" type="text/css" href="https://mui.kernelsu.org/internal/colors.css" />
<style>
html,
body {
margin: 0;
padding: 0;
overflow: hidden;
background: var(--background);
width: 100%;
height: 100%;
}
canvas {
display: block;
width: 100vw;
height: calc(
100vh - var(--window-inset-top) - var(--window-inset-bottom)
);
margin-top: var(--window-inset-top);
margin-bottom: var(--window-inset-bottom);
outline-style: dotted;
outline-color: var(--primary);
outline-width: 5px;
}
.score {
margin-top: var(--window-inset-top);
position: absolute;
z-index: 10;
font-family: Arial, sans-serif;
top: 10px;
left: 10px;
font-size: 20pt;
color: var(--onBackground);
display: flex;
flex-direction: row;
justify-content: space-between;
justify-items: flex-end;
align-items: center;
width: calc(100% - 20px);
}
</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>
<div class="score">
<div id="score"></div>
<svg
id="restart"
xmlns="http://www.w3.org/2000/svg"
width="30"
height="30"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="icon icon-tabler icons-tabler-outline icon-tabler-rotate-rectangle"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M10.09 4.01l.496 -.495a2 2 0 0 1 2.828 0l7.071 7.07a2 2 0 0 1 0 2.83l-7.07 7.07a2 2 0 0 1 -2.83 0l-7.07 -7.07a2 2 0 0 1 0 -2.83l3.535 -3.535h-3.988"
/>
<path d="M7.05 11.038v-3.988" />
</svg>
</div>
<script type="module">
const root = document.documentElement;
const topInset = getComputedStyle(root)
.getPropertyValue("--window-inset-top")
.trim();
const bottomInset = getComputedStyle(root)
.getPropertyValue("--window-inset-bottom")
.trim();
const backgroundColor = getComputedStyle(root)
.getPropertyValue("--background")
.trim();
const primaryColor = getComputedStyle(root)
.getPropertyValue("--primary")
.trim();
const errorContainerColor = getComputedStyle(root)
.getPropertyValue("--errorContainer")
.trim();
const topInsetInt = parseInt(topInset.replace("px", ""));
const bottomInsetInt = parseInt(bottomInset.replace("px", ""));
async function initWebGPU() {
if (!navigator.gpu) {
alert("WebGPU is not supported on this device.");
return null;
}
const restartButton = document.getElementById("restart");
restartButton.addEventListener("click", () => {
initGame();
});
const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice();
const canvas = document.getElementById("gameCanvas");
const context = canvas.getContext("webgpu");
const format = navigator.gpu.getPreferredCanvasFormat();
context.configure({
device,
format,
alphaMode: "opaque",
});
const shaderModule = device.createShaderModule({
code: `
struct VertexOutput {
@builtin(position) Position : vec4<f32>,
@location(0) fragColor : vec3<f32>,
};
@vertex
fn vmain(@location(0) position: vec2<f32>,
@location(1) color: vec3<f32>) -> VertexOutput {
var output: VertexOutput;
output.Position = vec4<f32>(position, 0.0, 1.0);
output.fragColor = color;
return output;
}
@fragment
fn pmain(@location(0) color: vec3<f32>) -> @location(0) vec4<f32> {
return vec4<f32>(color, 1.0);
}
`,
});
const pipeline = device.createRenderPipeline({
layout: "auto",
vertex: {
module: shaderModule,
entryPoint: "vmain",
buffers: [
{
arrayStride: 5 * 4,
attributes: [
{ shaderLocation: 0, offset: 0, format: "float32x2" },
{ shaderLocation: 1, offset: 2 * 4, format: "float32x3" },
],
},
],
},
fragment: {
module: shaderModule,
entryPoint: "pmain",
targets: [{ format }],
},
primitive: {
topology: "triangle-list",
},
});
const gpuBuffers = [];
function createRectangleVertices(x, y, width, height, color) {
const x1 = (x / canvas.width) * 2 - 1;
const y1 = 1 - (y / canvas.height) * 2;
const x2 = ((x + width) / canvas.width) * 2 - 1;
const y2 = 1 - ((y + height) / canvas.height) * 2;
return new Float32Array([
x1,
y1,
...color,
x2,
y1,
...color,
x1,
y2,
...color,
x1,
y2,
...color,
x2,
y1,
...color,
x2,
y2,
...color,
]);
}
const createObject = (id, x, y, w, h, c) => {
const obj = createRectangleVertices(x, y, w, h, c);
const buffer = device.createBuffer({
size: obj.byteLength,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(buffer, 0, obj);
gpuBuffers[id] = { buffer, length: obj.length, color: c };
};
const updateObject = (id, x, y, w, h, c) => {
const obj = createRectangleVertices(x, y, w, h, c);
device.queue.writeBuffer(gpuBuffers[id].buffer, 0, obj);
};
const drawObjects = () => {
const commandEncoder = device.createCommandEncoder();
const textureView = context.getCurrentTexture().createView();
const renderPass = commandEncoder.beginRenderPass({
colorAttachments: [
{
view: textureView,
clearValue: hexToClearValue(backgroundColor),
loadOp: "clear",
storeOp: "store",
},
],
});
renderPass.setPipeline(pipeline);
for (let key in gpuBuffers) {
renderPass.setVertexBuffer(0, gpuBuffers[key].buffer);
renderPass.draw(gpuBuffers[key].length / 5);
}
renderPass.end();
device.queue.submit([commandEncoder.finish()]);
};
return { device, createObject, updateObject, drawObjects };
}
const canvas = document.getElementById("gameCanvas");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight - (topInsetInt + bottomInsetInt);
window.addEventListener("resize", () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight - (topInsetInt + bottomInsetInt);
});
const { device, createObject, updateObject, drawObjects } =
await initWebGPU();
// Game logic
const GRID_SIZE = 20;
let snake = [];
let food = {};
let direction = "right";
let changingDirection = false;
let score = 0;
let gameOver = false;
const FOOD_COLOR = hexToRGBAArray(errorContainerColor);
const SNAKE_COLOR = hexToRGBAArray(primaryColor);
function generateFoodPosition() {
let position;
do {
position = {
x: Math.floor(Math.random() * (canvas.width / GRID_SIZE)),
y: Math.floor(Math.random() * (canvas.height / GRID_SIZE)),
};
} while (snake.some((p) => p.x === position.x && p.y === position.y));
return position;
}
function moveSnake() {
for (let i = snake.length - 1; i > 0; i--) {
snake[i] = { ...snake[i - 1] };
}
switch (direction) {
case "up":
snake[0].y--;
break;
case "down":
snake[0].y++;
break;
case "left":
snake[0].x--;
break;
case "right":
snake[0].x++;
break;
}
changingDirection = false;
}
function isGameOver() {
const head = snake[0];
if (
head.x < 0 ||
head.y < 0 ||
head.x >= Math.floor(canvas.width / GRID_SIZE) ||
head.y >= Math.floor(canvas.height / GRID_SIZE) ||
snake.slice(1).some((p) => p.x === head.x && p.y === head.y)
) {
return true;
}
return false;
}
function updateGame() {
if (gameOver) return;
moveSnake();
if (snake[0].x === food.x && snake[0].y === food.y) {
snake.push({ ...snake[snake.length - 1] });
score += 10;
food = generateFoodPosition();
}
if (isGameOver()) {
gameOver = true;
alert("Game Over! Wanna restart the game?");
}
drawGame();
}
function drawGame() {
updateObject(
"FOOD",
food.x * GRID_SIZE,
food.y * GRID_SIZE,
GRID_SIZE,
GRID_SIZE,
FOOD_COLOR
);
snake.forEach((part, i) => {
updateObject(
"BODY_" + i,
part.x * GRID_SIZE,
part.y * GRID_SIZE,
GRID_SIZE,
GRID_SIZE,
SNAKE_COLOR
);
});
document.getElementById("score").innerText = `Score: ${score}`;
drawObjects();
}
function initGame() {
for (let i = 0; i < 100; i++) {
createObject(
"BODY_" + i,
-100,
-100,
GRID_SIZE,
GRID_SIZE,
SNAKE_COLOR
);
}
createObject("FOOD", -100, -100, GRID_SIZE, GRID_SIZE, FOOD_COLOR);
snake = [
{ x: 5, y: 5 },
{ x: 4, y: 5 },
{ x: 3, y: 5 },
];
food = generateFoodPosition();
score = 0;
gameOver = false;
setInterval(updateGame, 150);
}
// Keyboard controls
document.addEventListener("keydown", (e) => {
if (changingDirection) return;
changingDirection = true;
switch (e.key) {
case "ArrowUp":
if (direction !== "down") direction = "up";
break;
case "ArrowDown":
if (direction !== "up") direction = "down";
break;
case "ArrowLeft":
if (direction !== "right") direction = "left";
break;
case "ArrowRight":
if (direction !== "left") direction = "right";
break;
}
});
// Swipe controls
let touchStartX = 0,
touchStartY = 0;
canvas.addEventListener("touchstart", (e) => {
touchStartX = e.touches[0].clientX;
touchStartY = e.touches[0].clientY;
});
canvas.addEventListener("touchend", (e) => {
const dx = e.changedTouches[0].clientX - touchStartX;
const dy = e.changedTouches[0].clientY - touchStartY;
if (Math.abs(dx) > Math.abs(dy)) {
if (dx > 30 && direction !== "left") direction = "right";
else if (dx < -30 && direction !== "right") direction = "left";
} else {
if (dy > 30 && direction !== "up") direction = "down";
else if (dy < -30 && direction !== "down") direction = "up";
}
});
initGame();
function hexToClearValue(hex) {
if (!/^#([0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(hex)) {
throw new Error(
"Invalid hex color. Use #rrggbb or #rrggbbaa format."
);
}
// Remove the '#' and split into parts
const value = hex.slice(1);
const hasAlpha = value.length === 8;
const r = parseInt(value.slice(0, 2), 16) / 255;
const g = parseInt(value.slice(2, 4), 16) / 255;
const b = parseInt(value.slice(4, 6), 16) / 255;
const a = hasAlpha ? parseInt(value.slice(6, 8), 16) / 255 : 1;
return { r, g, b, a };
}
function hexToRGBAArray(hex) {
if (!/^#([0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(hex)) {
throw new Error(
"Invalid hex color. Use #rrggbb or #rrggbbaa format."
);
}
const value = hex.slice(1);
const r = parseInt(value.slice(0, 2), 16) / 255;
const g = parseInt(value.slice(2, 4), 16) / 255;
const b = parseInt(value.slice(4, 6), 16) / 255;
return [r, g, b];
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment