16. Atari · 1984
I, Robot polygons
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);
})();