27. Braben and Bell · 1984 · Retro techniques
Elite planet
Demos are animating. Following your system setting.
About
Elite's worlds were wireframe circles with a line or a crater on a BBC Micro; the dotted sphere with a lit terminator and a ring here is how its planets are remembered rather than how they were drawn, so treat it as an interpretation. No textures. Drag to spin. It keeps a slow auto-rotate until you take over.
Drag on the preview to spin the planet. Auto-rotation stops after the first drag.
Reference
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. By default a demo holds a still frame when your system asks for reduced motion; the Motion buttons override that, and a standalone copy animates with #motion=play on its URL.
Source
(function () {
const PARAMS = {
color: "#c8d2dc",
speed: 1,
density: 1
};
function hexToRgb(hex) {
const n = parseInt(String(hex).replace("#", ""), 16);
if (isNaN(n)) return [200, 210, 220];
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;
}
const stars = [];
for (let i = 0; i < 180; i++) {
stars.push({ x: Math.random(), y: Math.random(), a: 0.25 + Math.random() * 0.7, s: 0.7 + Math.random() * 1.4 });
}
let yaw = 0.6, pitch = 0.25, auto = true, dragging = false, lastX = 0, lastY = 0, last = 0;
let raf = 0;
const motionQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
let reduced = motionQuery.matches;
canvas.addEventListener("pointerdown", function (e) {
dragging = true; auto = false; lastX = e.clientX; lastY = e.clientY;
canvas.setPointerCapture(e.pointerId);
wake();
});
canvas.addEventListener("pointerup", function () { dragging = false; wake(); });
canvas.addEventListener("pointermove", function (e) {
if (!dragging) return;
yaw += (e.clientX - lastX) * 0.008;
pitch += (e.clientY - lastY) * 0.006;
pitch = Math.max(-0.9, Math.min(0.9, pitch));
lastX = e.clientX; lastY = e.clientY;
wake();
});
function rot(x, y, z) {
const cx = Math.cos(pitch), sx = Math.sin(pitch);
const cy = Math.cos(yaw), sy = Math.sin(yaw);
const y2 = y * cx - z * sx; z = y * sx + z * cx; y = y2;
const x2 = x * cy + z * sy; z = -x * sy + z * cy;
return [x2, y, z];
}
function draw(w, h) {
ctx.fillStyle = "#000";
ctx.fillRect(0, 0, w, h);
const rgb = hexToRgb(PARAMS.color);
for (let i = 0; i < stars.length; i++) {
const s = stars[i];
ctx.fillStyle = "rgba(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + "," + s.a + ")";
ctx.fillRect(s.x * w, s.y * h, s.s, s.s);
}
const R = Math.min(w, h) * 0.32;
const cx = w * 0.5, cy = h * 0.52;
const latN = Math.max(10, (16 * PARAMS.density) | 0);
const lonN = Math.max(16, (28 * PARAMS.density) | 0);
for (let i = 0; i <= latN; i++) {
const lat = (i / latN) * Math.PI - Math.PI / 2;
const cl = Math.cos(lat), sl = Math.sin(lat);
for (let j = 0; j < lonN; j++) {
const lon = (j / lonN) * Math.PI * 2;
const x = cl * Math.cos(lon), y = sl, z = cl * Math.sin(lon);
const p = rot(x, y, z);
if (p[2] < 0.04) continue;
const lambert = Math.max(0.12, p[2] * 0.7 + 0.25);
const size = 1.1 + p[2] * 1.1;
ctx.fillStyle = "rgba(" + (rgb[0] * lambert | 0) + "," + (rgb[1] * lambert | 0) + "," + (rgb[2] * lambert | 0) + ",0.95)";
ctx.fillRect(cx + p[0] * R - size * 0.5, cy - p[1] * R - size * 0.5, size, size);
}
}
ctx.strokeStyle = "rgba(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + ",0.28)";
ctx.lineWidth = 1;
ctx.beginPath();
for (let k = 0; k <= 64; k++) {
const a = (k / 64) * Math.PI * 2;
const p = rot(Math.cos(a) * 1.35, 0.08, Math.sin(a) * 1.35);
const sx = cx + p[0] * R, sy = cy - p[1] * R;
if (k === 0) ctx.moveTo(sx, sy);
else ctx.lineTo(sx, sy);
}
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.22 * PARAMS.speed;
resize();
draw(canvas.width, canvas.height);
if (!reduced) raf = requestAnimationFrame(frame);
}
// Motion. The system's reduced-motion preference decides by default. A
// standalone copy animates anyway with "#motion=play" in its URL (or holds a
// frame with "#motion=still"); an embedding page can post {motion: "play"}.
let motionMode = "auto";
function applyMotion() {
const next = motionMode === "still" || (motionMode === "auto" && motionQuery.matches);
if (next === reduced) return;
reduced = next;
last = 0;
if (reduced) { cancelAnimationFrame(raf); raf = 0; frame(performance.now()); }
else if (!raf) raf = requestAnimationFrame(frame);
}
function setMotion(mode) {
if (mode === "play" || mode === "still" || mode === "auto") { motionMode = mode; applyMotion(); }
}
function readMotionHash() {
const m = /motion=(play|still|auto)/.exec(location.hash);
let mode = m ? m[1] : "auto";
// Embedded in the gallery, the page sets window.__demoMotion before any frame loads.
try { if (!m && window.parent !== window && typeof window.parent.__demoMotion === "string") mode = window.parent.__demoMotion; } catch (e) { /* cross-origin parent */ }
setMotion(mode);
}
function wake() { if (reduced) frame(performance.now()); }
if (motionQuery.addEventListener) motionQuery.addEventListener("change", applyMotion);
else if (motionQuery.addListener) motionQuery.addListener(applyMotion);
window.addEventListener("hashchange", readMotionHash);
window.addEventListener("message", function (e) { if (e.data && typeof e.data.motion === "string") setMotion(e.data.motion); });
window.addEventListener("resize", wake);
if (reduced) frame(performance.now());
else raf = requestAnimationFrame(frame);
readMotionHash();
})();