{"slug":"elite","number":"27","title":"Elite planet","era":"Braben · 1984","summary":"A dotted sphere and a starfield. Drag to spin.","description":"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.","apis":["Canvas 2D","Pointer Events","requestAnimationFrame","matchMedia"],"tags":["wireframe","3d"],"hasGlsl":false,"interaction":"Drag on the preview to spin the planet. Auto-rotation stops after the first drag.","url":"https://3d-retro.com/experiments/elite","example_url":"https://3d-retro.com/examples/elite.html","api_url":"https://3d-retro.com/api/v1/experiments/elite","html":"<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"utf-8\" />\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n  <meta name=\"description\" content=\"A dotted Elite planet. Drag to spin.\" />\n  <link rel=\"canonical\" href=\"https://3d-retro.com/experiments/elite\" />\n  <title>Elite planet · 3d retro graphics</title>\n  <style>\n    html, body { margin: 0; width: 100%; height: 100%; overflow: hidden; background: #000000; color: #e8e6e1; position: absolute; inset: 0; }\n    canvas { display: block; position: absolute; inset: 0; width: 100%; height: 100%; touch-action: none; }\n    .fallback { position: absolute; inset: 0; display: grid; place-items: center; padding: 24px; font: 14px/1.5 ui-monospace, \"IBM Plex Mono\", monospace; text-align: center; color: #e8e6e1; background: #000; }\n    .fallback[hidden] { display: none; }\n  </style>\n</head>\n<body>\n<canvas id=\"c\"></canvas>\n<div id=\"fallback\" class=\"fallback\" hidden></div>\n<script>\n(function () {\n  const PARAMS = {\n    color: \"#c8d2dc\",\n    speed: 1,\n    density: 1\n  };\n  function hexToRgb(hex) {\n    const n = parseInt(String(hex).replace(\"#\", \"\"), 16);\n    if (isNaN(n)) return [200, 210, 220];\n    return [n >> 16 & 255, n >> 8 & 255, n & 255];\n  }\n\n  const canvas = document.getElementById(\"c\");\n  const fallback = document.getElementById(\"fallback\");\n  const ctx = canvas.getContext(\"2d\");\n  if (!ctx) {\n    fallback.hidden = false;\n    fallback.textContent = \"Canvas 2D is not available in this browser.\";\n    return;\n  }\n\n  const stars = [];\n  for (let i = 0; i < 180; i++) {\n    stars.push({ x: Math.random(), y: Math.random(), a: 0.25 + Math.random() * 0.7, s: 0.7 + Math.random() * 1.4 });\n  }\n\n  let yaw = 0.6, pitch = 0.25, auto = true, dragging = false, lastX = 0, lastY = 0, last = 0;\n  const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n\n  canvas.addEventListener(\"pointerdown\", function (e) {\n    dragging = true; auto = false; lastX = e.clientX; lastY = e.clientY;\n    canvas.setPointerCapture(e.pointerId);\n  });\n  canvas.addEventListener(\"pointerup\", function () { dragging = false; });\n  canvas.addEventListener(\"pointermove\", function (e) {\n    if (!dragging) return;\n    yaw += (e.clientX - lastX) * 0.008;\n    pitch += (e.clientY - lastY) * 0.006;\n    pitch = Math.max(-0.9, Math.min(0.9, pitch));\n    lastX = e.clientX; lastY = e.clientY;\n  });\n\n  function rot(x, y, z) {\n    const cx = Math.cos(pitch), sx = Math.sin(pitch);\n    const cy = Math.cos(yaw), sy = Math.sin(yaw);\n    const y2 = y * cx - z * sx; z = y * sx + z * cx; y = y2;\n    const x2 = x * cy + z * sy; z = -x * sy + z * cy;\n    return [x2, y, z];\n  }\n\n  function draw(w, h) {\n    ctx.fillStyle = \"#000\";\n    ctx.fillRect(0, 0, w, h);\n    const rgb = hexToRgb(PARAMS.color);\n    for (let i = 0; i < stars.length; i++) {\n      const s = stars[i];\n      ctx.fillStyle = \"rgba(\" + rgb[0] + \",\" + rgb[1] + \",\" + rgb[2] + \",\" + s.a + \")\";\n      ctx.fillRect(s.x * w, s.y * h, s.s, s.s);\n    }\n    const R = Math.min(w, h) * 0.32;\n    const cx = w * 0.5, cy = h * 0.52;\n    const latN = Math.max(10, (16 * PARAMS.density) | 0);\n    const lonN = Math.max(16, (28 * PARAMS.density) | 0);\n    for (let i = 0; i <= latN; i++) {\n      const lat = (i / latN) * Math.PI - Math.PI / 2;\n      const cl = Math.cos(lat), sl = Math.sin(lat);\n      for (let j = 0; j < lonN; j++) {\n        const lon = (j / lonN) * Math.PI * 2;\n        const x = cl * Math.cos(lon), y = sl, z = cl * Math.sin(lon);\n        const p = rot(x, y, z);\n        if (p[2] < 0.04) continue;\n        const lambert = Math.max(0.12, p[2] * 0.7 + 0.25);\n        const size = 1.1 + p[2] * 1.1;\n        ctx.fillStyle = \"rgba(\" + (rgb[0] * lambert | 0) + \",\" + (rgb[1] * lambert | 0) + \",\" + (rgb[2] * lambert | 0) + \",0.95)\";\n        ctx.fillRect(cx + p[0] * R - size * 0.5, cy - p[1] * R - size * 0.5, size, size);\n      }\n    }\n    ctx.strokeStyle = \"rgba(\" + rgb[0] + \",\" + rgb[1] + \",\" + rgb[2] + \",0.28)\";\n    ctx.lineWidth = 1;\n    ctx.beginPath();\n    for (let k = 0; k <= 64; k++) {\n      const a = (k / 64) * Math.PI * 2;\n      const p = rot(Math.cos(a) * 1.35, 0.08, Math.sin(a) * 1.35);\n      const sx = cx + p[0] * R, sy = cy - p[1] * R;\n      if (k === 0) ctx.moveTo(sx, sy);\n      else ctx.lineTo(sx, sy);\n    }\n    ctx.stroke();\n  }\n\n  function resize() {\n    const dpr = Math.min(window.devicePixelRatio || 1, 2);\n    const w = Math.max(1, Math.floor(canvas.clientWidth * dpr));\n    const h = Math.max(1, Math.floor(canvas.clientHeight * dpr));\n    if (canvas.width !== w || canvas.height !== h) { canvas.width = w; canvas.height = h; }\n  }\n\n  function frame(now) {\n    const dt = Math.min(0.05, last ? (now - last) * 0.001 : 0.016);\n    last = now;\n    if (auto && !reduced) yaw += dt * 0.22 * PARAMS.speed;\n    resize();\n    draw(canvas.width, canvas.height);\n    if (!reduced) requestAnimationFrame(frame);\n  }\n\n  if (reduced) { resize(); draw(canvas.width, canvas.height); }\n  else requestAnimationFrame(frame);\n})();\n</script>\n</body>\n</html>\n"}