Home

16. Atari · 1984

I, Robot polygons

Colors and knobs

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

About

I, Robot was the first arcade game to fill polygons in real time. Faces are sorted back-to-front and painted. No z-buffer, no textures: a grid of platforms, a red tower, primary colors on black. Drag to orbit. It keeps a slow auto-spin until you take over.

Drag on the preview to orbit. Auto-rotation stops after the first drag.

Browser APIs

  • Canvas 2D
  • 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: "#e23b2e",
    accent: "#f0c43c",
    speed: 1
  };
  function hexToRgb(hex) {
    const n = parseInt(String(hex).replace("#", ""), 16);
    if (isNaN(n)) return [226, 59, 46];
    return [n >> 16 & 255, n >> 8 & 255, n & 255];
  }
  function css(rgb, k) {
    return "rgb(" + ((rgb[0] * k) | 0) + "," + ((rgb[1] * k) | 0) + "," + ((rgb[2] * k) | 0) + ")";
  }

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

  let yaw = 0.6, pitch = 0.42, auto = true, dragging = false, lastX = 0, lastY = 0, last = 0;
  const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

  canvas.addEventListener("pointerdown", function (e) {
    dragging = true; auto = false; lastX = e.clientX; lastY = e.clientY;
    canvas.setPointerCapture(e.pointerId);
  });
  canvas.addEventListener("pointerup", function () { dragging = false; });
  canvas.addEventListener("pointermove", function (e) {
    if (!dragging) return;
    yaw += (e.clientX - lastX) * 0.008;
    pitch += (e.clientY - lastY) * 0.008;
    pitch = Math.max(0.15, Math.min(1.1, pitch));
    lastX = e.clientX; lastY = e.clientY;
  });

  function xform(x, y, z, w, h) {
    const cy = Math.cos(yaw), sy = Math.sin(yaw);
    const cx = Math.cos(pitch), sx = Math.sin(pitch);
    const x1 = x * cy + z * sy;
    const z1 = -x * sy + z * cy;
    const y1 = y * cx - z1 * sx;
    const z2 = y * sx + z1 * cx;
    const zc = z2 + 16;
    if (zc < 0.4) return null;
    const fov = Math.min(w, h) * 0.95;
    return { x: w * 0.5 + (x1 / zc) * fov, y: h * 0.52 - (y1 / zc) * fov, z: zc };
  }

  function face(pts, col, k, faces) {
    const p = [];
    let z = 0;
    for (let i = 0; i < pts.length; i++) {
      if (!pts[i]) return;
      p.push(pts[i]);
      z += pts[i].z;
    }
    const a = p[1].x - p[0].x, b = p[1].y - p[0].y;
    const c = p[2].x - p[0].x, d = p[2].y - p[0].y;
    if (a * d - b * c <= 0) return;
    faces.push({ p: p, z: z / p.length, col: col, k: k });
  }

  function cube(cx, cy, cz, s, h, col, faces, w, hh) {
    const p = [
      xform(cx - s, cy, cz - s, w, hh),
      xform(cx + s, cy, cz - s, w, hh),
      xform(cx + s, cy, cz + s, w, hh),
      xform(cx - s, cy, cz + s, w, hh),
      xform(cx - s, cy + h, cz - s, w, hh),
      xform(cx + s, cy + h, cz - s, w, hh),
      xform(cx + s, cy + h, cz + s, w, hh),
      xform(cx - s, cy + h, cz + s, w, hh)
    ];
    face([p[0], p[1], p[2], p[3]], col, 0.28, faces);
    face([p[4], p[7], p[6], p[5]], col, 1, faces);
    face([p[0], p[4], p[5], p[1]], col, 0.72, faces);
    face([p[1], p[5], p[6], p[2]], col, 0.52, faces);
    face([p[2], p[6], p[7], p[3]], col, 0.4, faces);
    face([p[3], p[7], p[4], p[0]], col, 0.6, faces);
  }

  function pyramid(cx, cy, cz, s, h, col, faces, w, hh) {
    const a = xform(cx - s, cy, cz - s, w, hh);
    const b = xform(cx + s, cy, cz - s, w, hh);
    const c = xform(cx + s, cy, cz + s, w, hh);
    const d = xform(cx - s, cy, cz + s, w, hh);
    const t = xform(cx, cy + h, cz, w, hh);
    face([a, b, c, d], col, 0.3, faces);
    face([a, t, b], col, 0.9, faces);
    face([b, t, c], col, 0.65, faces);
    face([c, t, d], col, 0.45, faces);
    face([d, t, a], col, 0.75, faces);
  }

  function draw(w, h) {
    ctx.fillStyle = "#000";
    ctx.fillRect(0, 0, w, h);
    const red = hexToRgb(PARAMS.color);
    const gold = hexToRgb(PARAMS.accent);
    const cyan = [70, 170, 190];
    const faces = [];

    for (let i = -6; i <= 6; i++) {
      const a1 = xform(i, 0, -7, w, h), b1 = xform(i, 0, 7, w, h);
      if (a1 && b1) faces.push({ line: true, a: a1, b: b1, z: (a1.z + b1.z) * 0.5, col: red, k: 0.35 });
      const a2 = xform(-7, 0, i, w, h), b2 = xform(7, 0, i, w, h);
      if (a2 && b2) faces.push({ line: true, a: a2, b: b2, z: (a2.z + b2.z) * 0.5, col: red, k: 0.35 });
    }

    for (let z = -5; z <= 5; z++) {
      for (let x = -5; x <= 5; x++) {
        if ((x + z) % 2 !== 0) continue;
        const n = Math.sin(x * 1.7 + z * 0.9) * 0.5 + 0.5;
        const ht = 0.35 + n * 1.8;
        const col = n > 0.66 ? gold : n > 0.33 ? red : cyan;
        if ((x * z) % 3 === 0 && Math.abs(x) + Math.abs(z) > 2) {
          pyramid(x * 1.35, 0, z * 1.35, 0.42, ht + 0.4, col, faces, w, h);
        } else {
          cube(x * 1.35, 0, z * 1.35, 0.42, ht, col, faces, w, h);
        }
      }
    }
    cube(0, 0, 0, 0.28, 1.55, red, faces, w, h);
    cube(0, 1.55, 0, 0.22, 0.22, gold, faces, w, h);

    faces.sort(function (a, b) { return b.z - a.z; });
    for (let i = 0; i < faces.length; i++) {
      const f = faces[i];
      if (f.line) {
        if (!f.a || !f.b) continue;
        ctx.strokeStyle = css(f.col, f.k);
        ctx.lineWidth = 1;
        ctx.beginPath();
        ctx.moveTo(f.a.x, f.a.y);
        ctx.lineTo(f.b.x, f.b.y);
        ctx.stroke();
        continue;
      }
      ctx.beginPath();
      ctx.moveTo(f.p[0].x, f.p[0].y);
      for (let k = 1; k < f.p.length; k++) ctx.lineTo(f.p[k].x, f.p[k].y);
      ctx.closePath();
      ctx.fillStyle = css(f.col, f.k);
      ctx.fill();
      ctx.strokeStyle = css(f.col, Math.min(1, f.k + 0.15));
      ctx.lineWidth = 1;
      ctx.stroke();
    }
  }

  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 frame(now) {
    const dt = Math.min(0.05, last ? (now - last) * 0.001 : 0.016);
    last = now;
    if (auto && !reduced) yaw += dt * 0.28 * PARAMS.speed;
    resize();
    draw(canvas.width, canvas.height);
    if (!reduced) requestAnimationFrame(frame);
  }

  if (reduced) { resize(); draw(canvas.width, canvas.height); }
  else requestAnimationFrame(frame);
})();