21. Amiga · 1984
Boing ball
About
Commodore's 1984 show ball: a sphere with longitude/latitude checkers, Lambert-shaded, a flattened shadow on a perspective grid. It bounces with a little energy loss and rolls with its spin. Click to kick it.
Click the preview to kick the ball.
Browser APIs
- Canvas 2D
- ImageData
- 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: "#d8241c",
speed: 1,
checks: 8
};
function hexToRgb(hex) {
const n = parseInt(String(hex).replace("#", ""), 16);
if (isNaN(n)) return [216, 36, 28];
return [n >> 16 & 255, n >> 8 & 255, n & 255];
}
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 x = 0, y = 1.8, vx = 1.15, vy = 0, spin = 0.4, last = 0;
let poke = 0;
const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const G = 9.4, FLOOR = 0, R = 0.72;
canvas.addEventListener("pointerdown", function (ev) {
const rect = canvas.getBoundingClientRect();
const nx = ((ev.clientX - rect.left) / rect.width - 0.5) * 4.4;
vx = (nx - x) * 1.4;
vy = 4.2;
poke = 1;
});
function project(px, py, pz, w, h) {
const z = pz + 5.2;
const f = Math.min(w, h) * 0.95 / z;
return { x: w * 0.5 + px * f, y: h * 0.62 - py * f, s: f };
}
function drawGrid(w, h, rgb) {
ctx.strokeStyle = "rgba(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + ",0.22)";
ctx.lineWidth = 1;
for (let i = -6; i <= 6; i++) {
const a = project(i * 0.55, FLOOR, -3.2, w, h);
const b = project(i * 0.55, FLOOR, 3.4, w, h);
ctx.beginPath(); ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y); ctx.stroke();
const c = project(-3.4, FLOOR, i * 0.55, w, h);
const d = project(3.4, FLOOR, i * 0.55, w, h);
ctx.beginPath(); ctx.moveTo(c.x, c.y); ctx.lineTo(d.x, d.y); ctx.stroke();
}
}
function drawBall(w, h, rgb) {
const p = project(x, y + R, 0, w, h);
const rad = R * p.s;
const sh = project(x + 0.35, FLOOR + 0.01, 0.15, w, h);
const lift = Math.max(0.15, 1 - (y / 2.6));
ctx.fillStyle = "rgba(0,0,0," + (0.38 * lift) + ")";
ctx.beginPath();
ctx.ellipse(sh.x, sh.y, rad * 0.85, rad * 0.22, 0, 0, Math.PI * 2);
ctx.fill();
const nCheck = Math.max(4, PARAMS.checks | 0);
const xmin = Math.max(0, (p.x - rad) | 0);
const xmax = Math.min(w - 1, (p.x + rad) | 0);
const ymin = Math.max(0, (p.y - rad) | 0);
const ymax = Math.min(h - 1, (p.y + rad) | 0);
const id = ctx.getImageData(xmin, ymin, xmax - xmin + 1, ymax - ymin + 1);
const data = id.data;
const cw = xmax - xmin + 1;
const cs = Math.cos(spin), sn = Math.sin(spin);
const white = [236, 232, 224];
for (let py = ymin; py <= ymax; py++) {
for (let px = xmin; px <= xmax; px++) {
const nx = (px - p.x) / rad;
const ny = (py - p.y) / rad;
const r2 = nx * nx + ny * ny;
if (r2 > 1) continue;
const nz = Math.sqrt(1 - r2);
const rx = nx * cs + nz * sn;
const rz = -nx * sn + nz * cs;
const lon = Math.atan2(rx, rz);
const lat = Math.asin(Math.max(-1, Math.min(1, ny)));
const u = Math.floor(((lon + Math.PI) / (Math.PI * 2)) * nCheck);
const v = Math.floor(((lat + Math.PI / 2) / Math.PI) * (nCheck * 0.5));
const on = (u + v) & 1;
const wrap = 0.18 + 0.85 * Math.max(0, 0.35 + 0.75 * (nx * 0.4 + ny * -0.5 + nz * 0.7));
const col = on ? rgb : white;
const i = ((py - ymin) * cw + (px - xmin)) * 4;
data[i] = col[0] * wrap;
data[i + 1] = col[1] * wrap;
data[i + 2] = col[2] * wrap;
data[i + 3] = 255;
}
}
ctx.putImageData(id, xmin, ymin);
}
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.032, last ? (now - last) * 0.001 : 0.016);
last = now;
if (!reduced) {
vy -= G * dt * PARAMS.speed;
y += vy * dt * PARAMS.speed;
x += vx * dt * PARAMS.speed;
spin += vx * dt * 0.85 * PARAMS.speed;
if (y < FLOOR) {
y = FLOOR;
vy = Math.abs(vy) * 0.84;
if (vy < 0.4) vy = 3.6 + poke * 1.2;
}
if (x > 2.1) { x = 2.1; vx = -Math.abs(vx); }
if (x < -2.1) { x = -2.1; vx = Math.abs(vx); }
poke *= 0.9;
}
resize();
const w = canvas.width, h = canvas.height;
ctx.fillStyle = "#0b0c0e";
ctx.fillRect(0, 0, w, h);
const rgb = hexToRgb(PARAMS.color);
drawGrid(w, h, rgb);
drawBall(w, h, rgb);
if (!reduced) requestAnimationFrame(frame);
}
if (reduced) {
y = 0.8; x = 0;
resize();
ctx.fillStyle = "#0b0c0e";
ctx.fillRect(0, 0, canvas.width, canvas.height);
const rgb = hexToRgb(PARAMS.color);
drawGrid(canvas.width, canvas.height, rgb);
drawBall(canvas.width, canvas.height, rgb);
} else requestAnimationFrame(frame);
})();