Home

27. Braben and Bell · 1984 · Retro techniques

Elite planet

Demos are animating. Following your system setting.
Colors and knobs

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

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

Elite, David Braben and Ian Bell, Acornsoft 1984

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