Home

27. Braben · 1984

Elite planet

Colors and knobs

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

About

Elite's worlds were spheres of dots with the back culled, a Lambert term for the terminator, and a cheap ring. 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.

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: "#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;
  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.006;
    pitch = Math.max(-0.9, Math.min(0.9, pitch));
    lastX = e.clientX; lastY = e.clientY;
  });

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

  if (reduced) { resize(); draw(canvas.width, canvas.height); }
  else requestAnimationFrame(frame);
})();