{"slug":"irobot","number":"16","title":"I, Robot polygons","era":"Atari · 1984","summary":"Filled cubes and pyramids, painter's algorithm. Drag to orbit.","description":"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.","apis":["Canvas 2D","Pointer Events","requestAnimationFrame","matchMedia"],"tags":["polygon","3d"],"hasGlsl":false,"interaction":"Drag on the preview to orbit. Auto-rotation stops after the first drag.","url":"https://3d-retro.com/experiments/irobot","example_url":"https://3d-retro.com/examples/irobot.html","api_url":"https://3d-retro.com/api/v1/experiments/irobot","html":"<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"utf-8\" />\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n  <meta name=\"description\" content=\"Painter's-algorithm filled polygons, 1984 arcade style. Drag to orbit.\" />\n  <link rel=\"canonical\" href=\"https://3d-retro.com/experiments/irobot\" />\n  <title>I, Robot polygons · 3d retro graphics</title>\n  <style>\n    html, body { margin: 0; width: 100%; height: 100%; overflow: hidden; background: #000000; color: #e8e6e1; position: absolute; inset: 0; }\n    canvas { display: block; position: absolute; inset: 0; width: 100%; height: 100%; touch-action: none; }\n    .fallback { position: absolute; inset: 0; display: grid; place-items: center; padding: 24px; font: 14px/1.5 ui-monospace, \"IBM Plex Mono\", monospace; text-align: center; color: #e8e6e1; background: #000; }\n    .fallback[hidden] { display: none; }\n  </style>\n</head>\n<body>\n<canvas id=\"c\"></canvas>\n<div id=\"fallback\" class=\"fallback\" hidden></div>\n<script>\n(function () {\n  const PARAMS = {\n    color: \"#e23b2e\",\n    accent: \"#f0c43c\",\n    speed: 1\n  };\n  function hexToRgb(hex) {\n    const n = parseInt(String(hex).replace(\"#\", \"\"), 16);\n    if (isNaN(n)) return [226, 59, 46];\n    return [n >> 16 & 255, n >> 8 & 255, n & 255];\n  }\n  function css(rgb, k) {\n    return \"rgb(\" + ((rgb[0] * k) | 0) + \",\" + ((rgb[1] * k) | 0) + \",\" + ((rgb[2] * k) | 0) + \")\";\n  }\n\n  const canvas = document.getElementById(\"c\");\n  const fallback = document.getElementById(\"fallback\");\n  const ctx = canvas.getContext(\"2d\");\n  if (!ctx) {\n    fallback.hidden = false;\n    fallback.textContent = \"Canvas 2D is not available in this browser.\";\n    return;\n  }\n\n  let yaw = 0.6, pitch = 0.42, auto = true, dragging = false, lastX = 0, lastY = 0, last = 0;\n  const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n\n  canvas.addEventListener(\"pointerdown\", function (e) {\n    dragging = true; auto = false; lastX = e.clientX; lastY = e.clientY;\n    canvas.setPointerCapture(e.pointerId);\n  });\n  canvas.addEventListener(\"pointerup\", function () { dragging = false; });\n  canvas.addEventListener(\"pointermove\", function (e) {\n    if (!dragging) return;\n    yaw += (e.clientX - lastX) * 0.008;\n    pitch += (e.clientY - lastY) * 0.008;\n    pitch = Math.max(0.15, Math.min(1.1, pitch));\n    lastX = e.clientX; lastY = e.clientY;\n  });\n\n  function xform(x, y, z, w, h) {\n    const cy = Math.cos(yaw), sy = Math.sin(yaw);\n    const cx = Math.cos(pitch), sx = Math.sin(pitch);\n    const x1 = x * cy + z * sy;\n    const z1 = -x * sy + z * cy;\n    const y1 = y * cx - z1 * sx;\n    const z2 = y * sx + z1 * cx;\n    const zc = z2 + 16;\n    if (zc < 0.4) return null;\n    const fov = Math.min(w, h) * 0.95;\n    return { x: w * 0.5 + (x1 / zc) * fov, y: h * 0.52 - (y1 / zc) * fov, z: zc };\n  }\n\n  function face(pts, col, k, faces) {\n    const p = [];\n    let z = 0;\n    for (let i = 0; i < pts.length; i++) {\n      if (!pts[i]) return;\n      p.push(pts[i]);\n      z += pts[i].z;\n    }\n    const a = p[1].x - p[0].x, b = p[1].y - p[0].y;\n    const c = p[2].x - p[0].x, d = p[2].y - p[0].y;\n    if (a * d - b * c <= 0) return;\n    faces.push({ p: p, z: z / p.length, col: col, k: k });\n  }\n\n  function cube(cx, cy, cz, s, h, col, faces, w, hh) {\n    const p = [\n      xform(cx - s, cy, cz - s, w, hh),\n      xform(cx + s, cy, cz - s, w, hh),\n      xform(cx + s, cy, cz + s, w, hh),\n      xform(cx - s, cy, cz + s, w, hh),\n      xform(cx - s, cy + h, cz - s, w, hh),\n      xform(cx + s, cy + h, cz - s, w, hh),\n      xform(cx + s, cy + h, cz + s, w, hh),\n      xform(cx - s, cy + h, cz + s, w, hh)\n    ];\n    face([p[0], p[1], p[2], p[3]], col, 0.28, faces);\n    face([p[4], p[7], p[6], p[5]], col, 1, faces);\n    face([p[0], p[4], p[5], p[1]], col, 0.72, faces);\n    face([p[1], p[5], p[6], p[2]], col, 0.52, faces);\n    face([p[2], p[6], p[7], p[3]], col, 0.4, faces);\n    face([p[3], p[7], p[4], p[0]], col, 0.6, faces);\n  }\n\n  function pyramid(cx, cy, cz, s, h, col, faces, w, hh) {\n    const a = xform(cx - s, cy, cz - s, w, hh);\n    const b = xform(cx + s, cy, cz - s, w, hh);\n    const c = xform(cx + s, cy, cz + s, w, hh);\n    const d = xform(cx - s, cy, cz + s, w, hh);\n    const t = xform(cx, cy + h, cz, w, hh);\n    face([a, b, c, d], col, 0.3, faces);\n    face([a, t, b], col, 0.9, faces);\n    face([b, t, c], col, 0.65, faces);\n    face([c, t, d], col, 0.45, faces);\n    face([d, t, a], col, 0.75, faces);\n  }\n\n  function draw(w, h) {\n    ctx.fillStyle = \"#000\";\n    ctx.fillRect(0, 0, w, h);\n    const red = hexToRgb(PARAMS.color);\n    const gold = hexToRgb(PARAMS.accent);\n    const cyan = [70, 170, 190];\n    const faces = [];\n\n    for (let i = -6; i <= 6; i++) {\n      const a1 = xform(i, 0, -7, w, h), b1 = xform(i, 0, 7, w, h);\n      if (a1 && b1) faces.push({ line: true, a: a1, b: b1, z: (a1.z + b1.z) * 0.5, col: red, k: 0.35 });\n      const a2 = xform(-7, 0, i, w, h), b2 = xform(7, 0, i, w, h);\n      if (a2 && b2) faces.push({ line: true, a: a2, b: b2, z: (a2.z + b2.z) * 0.5, col: red, k: 0.35 });\n    }\n\n    for (let z = -5; z <= 5; z++) {\n      for (let x = -5; x <= 5; x++) {\n        if ((x + z) % 2 !== 0) continue;\n        const n = Math.sin(x * 1.7 + z * 0.9) * 0.5 + 0.5;\n        const ht = 0.35 + n * 1.8;\n        const col = n > 0.66 ? gold : n > 0.33 ? red : cyan;\n        if ((x * z) % 3 === 0 && Math.abs(x) + Math.abs(z) > 2) {\n          pyramid(x * 1.35, 0, z * 1.35, 0.42, ht + 0.4, col, faces, w, h);\n        } else {\n          cube(x * 1.35, 0, z * 1.35, 0.42, ht, col, faces, w, h);\n        }\n      }\n    }\n    cube(0, 0, 0, 0.28, 1.55, red, faces, w, h);\n    cube(0, 1.55, 0, 0.22, 0.22, gold, faces, w, h);\n\n    faces.sort(function (a, b) { return b.z - a.z; });\n    for (let i = 0; i < faces.length; i++) {\n      const f = faces[i];\n      if (f.line) {\n        if (!f.a || !f.b) continue;\n        ctx.strokeStyle = css(f.col, f.k);\n        ctx.lineWidth = 1;\n        ctx.beginPath();\n        ctx.moveTo(f.a.x, f.a.y);\n        ctx.lineTo(f.b.x, f.b.y);\n        ctx.stroke();\n        continue;\n      }\n      ctx.beginPath();\n      ctx.moveTo(f.p[0].x, f.p[0].y);\n      for (let k = 1; k < f.p.length; k++) ctx.lineTo(f.p[k].x, f.p[k].y);\n      ctx.closePath();\n      ctx.fillStyle = css(f.col, f.k);\n      ctx.fill();\n      ctx.strokeStyle = css(f.col, Math.min(1, f.k + 0.15));\n      ctx.lineWidth = 1;\n      ctx.stroke();\n    }\n  }\n\n  function resize() {\n    const dpr = Math.min(window.devicePixelRatio || 1, 2);\n    const w = Math.max(1, Math.floor(canvas.clientWidth * dpr));\n    const h = Math.max(1, Math.floor(canvas.clientHeight * dpr));\n    if (canvas.width !== w || canvas.height !== h) {\n      canvas.width = w;\n      canvas.height = h;\n    }\n  }\n\n  function frame(now) {\n    const dt = Math.min(0.05, last ? (now - last) * 0.001 : 0.016);\n    last = now;\n    if (auto && !reduced) yaw += dt * 0.28 * PARAMS.speed;\n    resize();\n    draw(canvas.width, canvas.height);\n    if (!reduced) requestAnimationFrame(frame);\n  }\n\n  if (reduced) { resize(); draw(canvas.width, canvas.height); }\n  else requestAnimationFrame(frame);\n})();\n</script>\n</body>\n</html>\n"}