Home

12. id Software · 1991

Wolfenstein raycaster

Colors and knobs

These rewrite the live file. Copy HTML picks up whatever you set.

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);
  }
})();