12. id Software · 1991
Wolfenstein raycaster
About
Wolfenstein 3D's 2.5D engine: for each column, a ray steps a grid with DDA until it hits a wall, then a vertical strip is drawn at 1/distance. No polygons. Ceiling and floor are flats. A tiny map sits in the corner. It patrols the maze until you take over.
WASD or arrows to walk. Move the pointer to look. A/D strafe.
Browser APIs
- Canvas 2D
- Keyboard Events
- Pointer Events
- requestAnimationFrame
- matchMedia
If WebGL is missing, the demo draws a message on the canvas instead of a blank frame. prefers-reduced-motion freezes the first still.
Source
(function () {
const PARAMS = {
color: "#c45c3e",
speed: 1,
fov: 1
};
function hexToRgb(hex) {
const n = parseInt(String(hex).replace("#", ""), 16);
if (isNaN(n)) return [196, 92, 62];
return [n >> 16 & 255, n >> 8 & 255, n & 255];
}
function mix(a, b, t) {
return [
a[0] + (b[0] - a[0]) * t,
a[1] + (b[1] - a[1]) * t,
a[2] + (b[2] - a[2]) * t
];
}
function css(rgb, a) {
return "rgba(" + (rgb[0] | 0) + "," + (rgb[1] | 0) + "," + (rgb[2] | 0) + "," + a + ")";
}
const canvas = document.getElementById("c");
const fallback = document.getElementById("fallback");
const ctx = canvas.getContext("2d");
if (!ctx) {
fallback.hidden = false;
fallback.textContent = "Canvas 2D is not available in this browser.";
return;
}
const WMAP = [
"1111111111111111",
"1..............1",
"1..22......33..1",
"1..............1",
"11....44....11.1",
"1..............1",
"1.222....333...1",
"1.2..........3.1",
"1.2..11..11..3.1",
"1..............1",
"1....1....1....1",
"1111111111111111"
];
const MW = WMAP[0].length;
const MH = WMAP.length;
function cell(x, y) {
if (x < 0 || y < 0 || x >= MW || y >= MH) return "1";
return WMAP[y | 0][x | 0];
}
const PAL = {
"1": [196, 92, 62],
"2": [156, 118, 62],
"3": [72, 118, 92],
"4": [92, 86, 120]
};
let px = 2.5, py = 9.5, ang = 0;
const keys = new Set();
let manual = false;
let autoTurn = 0;
let last = 0;
let raf = 0;
const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const RAD = 0.18;
function onKey(ev, down) {
keys[down ? "add" : "delete"](ev.code);
if (down && (ev.code === "KeyW" || ev.code === "KeyA" || ev.code === "KeyS" || ev.code === "KeyD" ||
ev.code === "ArrowLeft" || ev.code === "ArrowRight" || ev.code === "ArrowUp" || ev.code === "ArrowDown")) {
manual = true;
ev.preventDefault();
}
}
window.addEventListener("keydown", function (e) { onKey(e, true); });
window.addEventListener("keyup", function (e) { onKey(e, false); });
window.addEventListener("blur", function () { keys.clear(); });
let lookOff = 0;
canvas.addEventListener("pointermove", function (ev) {
const r = canvas.getBoundingClientRect();
lookOff = ((ev.clientX - r.left) / r.width - 0.5) * 0.7;
if (ev.buttons) {
ang += ev.movementX * 0.005;
manual = true;
}
});
canvas.addEventListener("pointerleave", function () { lookOff = 0; });
function tryMove(dx, dy) {
const nx = px + dx, ny = py + dy;
if (cell(nx + Math.sign(dx) * RAD, py) === ".") px = nx;
if (cell(px, ny + Math.sign(dy) * RAD) === ".") py = ny;
}
function resize() {
const dpr = Math.min(window.devicePixelRatio || 1, 2);
const w = Math.max(1, Math.floor(canvas.clientWidth * dpr));
const h = Math.max(1, Math.floor(canvas.clientHeight * dpr));
if (canvas.width !== w || canvas.height !== h) {
canvas.width = w;
canvas.height = h;
}
}
function cast(dirX, dirY) {
let mapX = px | 0, mapY = py | 0;
const deltaX = Math.abs(1 / (dirX || 1e-8));
const deltaY = Math.abs(1 / (dirY || 1e-8));
const stepX = dirX < 0 ? -1 : 1;
const stepY = dirY < 0 ? -1 : 1;
let sideX = dirX < 0 ? (px - mapX) * deltaX : (mapX + 1 - px) * deltaX;
let sideY = dirY < 0 ? (py - mapY) * deltaY : (mapY + 1 - py) * deltaY;
let side = 0, hit = ".";
for (let i = 0; i < 48; i++) {
if (sideX < sideY) { sideX += deltaX; mapX += stepX; side = 0; }
else { sideY += deltaY; mapY += stepY; side = 1; }
hit = cell(mapX, mapY);
if (hit !== ".") break;
}
const dist = side === 0
? (mapX - px + (1 - stepX) * 0.5) / dirX
: (mapY - py + (1 - stepY) * 0.5) / dirY;
const wallX = side === 0 ? py + dist * dirY : px + dist * dirX;
return { dist: Math.max(0.12, dist), side: side, hit: hit, wallX: wallX - Math.floor(wallX) };
}
function draw(w, h) {
const rgb = hexToRgb(PARAMS.color);
const ceil = mix([11, 12, 14], rgb, 0.08);
const floor = mix([18, 16, 14], rgb, 0.16);
ctx.fillStyle = css(ceil, 1);
ctx.fillRect(0, 0, w, h * 0.5);
ctx.fillStyle = css(floor, 1);
ctx.fillRect(0, h * 0.5, w, h * 0.5);
const view = ang + lookOff;
const dirX = Math.cos(view);
const dirY = Math.sin(view);
const planeX = -dirY * 0.66 * PARAMS.fov;
const planeY = dirX * 0.66 * PARAMS.fov;
const cols = Math.min(w, 320);
const step = w / cols;
for (let i = 0; i < cols; i++) {
const cam = (i / cols) * 2 - 1;
const hit = cast(dirX + planeX * cam, dirY + planeY * cam);
const perp = hit.dist;
const lineH = Math.min(h * 1.8, h / perp);
const y0 = (h - lineH) * 0.5;
const base = PAL[hit.hit] || rgb;
const tint = mix(base, rgb, 0.35);
const stripe = ((hit.wallX * 8) | 0) & 1 ? 0.78 : 1;
const sideK = hit.side ? 0.55 : 0.88;
const fog = 1 / (1 + perp * 0.28);
const c = [
tint[0] * stripe * sideK * fog,
tint[1] * stripe * sideK * fog,
tint[2] * stripe * sideK * fog
];
ctx.fillStyle = css(c, 1);
ctx.fillRect((i * step) | 0, y0 | 0, Math.ceil(step), lineH | 0);
}
const ms = Math.max(3, (Math.min(w, h) / 90) | 0);
const mx = 8, my = 8;
for (let y = 0; y < MH; y++) {
for (let x = 0; x < MW; x++) {
const t = WMAP[y][x];
if (t === ".") ctx.fillStyle = "rgba(20,22,24,0.7)";
else ctx.fillStyle = css(mix(PAL[t] || rgb, rgb, 0.25), 0.85);
ctx.fillRect(mx + x * ms, my + y * ms, ms - 1, ms - 1);
}
}
ctx.fillStyle = css(rgb, 1);
ctx.fillRect(mx + px * ms - 1.5, my + py * ms - 1.5, 3, 3);
ctx.strokeStyle = css(rgb, 0.9);
ctx.beginPath();
ctx.moveTo(mx + px * ms, my + py * ms);
ctx.lineTo(mx + (px + Math.cos(view) * 1.4) * ms, my + (py + Math.sin(view) * 1.4) * ms);
ctx.stroke();
}
function step(dt) {
const dirX = Math.cos(ang);
const dirY = Math.sin(ang);
const planeX = -dirY;
const planeY = dirX;
const sp = 2.4 * PARAMS.speed * dt;
const turn = 1.7 * dt;
let mx = 0, my = 0, yaw = 0;
if (keys.has("KeyW") || keys.has("ArrowUp")) { mx += dirX; my += dirY; }
if (keys.has("KeyS") || keys.has("ArrowDown")) { mx -= dirX; my -= dirY; }
if (keys.has("KeyD")) { mx += planeX; my += planeY; }
if (keys.has("KeyA")) { mx -= planeX; my -= planeY; }
if (keys.has("ArrowLeft")) yaw -= 1;
if (keys.has("ArrowRight")) yaw += 1;
if (manual) {
if (mx || my) {
const len = Math.hypot(mx, my) || 1;
tryMove((mx / len) * sp, (my / len) * sp);
}
ang += yaw * turn;
} else {
const hit = cell(px + dirX * 0.6, py + dirY * 0.6);
if (hit !== ".") autoTurn = 1.2;
if (autoTurn > 0) {
ang += 1.8 * dt;
autoTurn -= dt;
} else {
tryMove(dirX * sp * 0.55, dirY * sp * 0.55);
}
}
}
function frame(now) {
const dt = Math.min(0.05, last ? (now - last) * 0.001 : 0.016);
last = now;
if (!reduced) step(dt);
resize();
draw(canvas.width, canvas.height);
if (!reduced) raf = requestAnimationFrame(frame);
}
window.__controlsTest = {
getYaw: function () { return ang; },
getX: function () { return px; },
getY: function () { return py; },
setKeys: function (codes) {
keys.clear();
manual = true;
for (let i = 0; i < codes.length; i++) keys.add(codes[i]);
}
};
if (reduced) {
resize();
draw(canvas.width, canvas.height);
} else {
raf = requestAnimationFrame(frame);
}
})();