← back to Model Arena

data/artifacts/2ca024f41f3c/gpt.html

324 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Canvas Solar System Orrery</title>
<style>
  html, body {
    margin: 0;
    padding: 0;
    background: radial-gradient(circle at top, #050814 0%, #02030a 40%, #000 100%);
    color: #eee;
    font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
    height: 100%;
    overflow: hidden;
  }
  #container {
    display: flex;
    flex-direction: column;
    height: 100vh;
  }
  #controls {
    display: flex;
    align-items: center;
    gap: 1rem;
    padding: 0.75rem 1rem;
    background: rgba(0,0,0,0.75);
    border-bottom: 1px solid #222;
    box-sizing: border-box;
    z-index: 10;
  }
  #controls label {
    font-size: 0.9rem;
    color: #ccc;
    white-space: nowrap;
  }
  #controls input[type="range"] {
    flex: 1;
  }
  #controls button {
    background: #222;
    border: 1px solid #555;
    border-radius: 4px;
    padding: 0.35rem 0.75rem;
    color: #eee;
    cursor: pointer;
    font-size: 0.85rem;
    transition: background 0.15s, border-color 0.15s;
  }
  #controls button:hover {
    background: #333;
    border-color: #777;
  }
  #controls span.value {
    min-width: 3rem;
    text-align: right;
    font-variant-numeric: tabular-nums;
    color: #aaa;
    font-size: 0.8rem;
  }
  #canvas-container {
    flex: 1;
    position: relative;
  }
  canvas {
    display: block;
    width: 100%;
    height: 100%;
    background: transparent;
  }
  #tooltip {
    position: absolute;
    pointer-events: none;
    padding: 2px 6px;
    font-size: 0.8rem;
    border-radius: 3px;
    background: rgba(0,0,0,0.8);
    color: #fff;
    border: 1px solid rgba(255,255,255,0.2);
    white-space: nowrap;
    transform: translate(8px, -20px);
    opacity: 0;
    transition: opacity 0.08s;
  }
</style>
</head>
<body>
<div id="container">
  <div id="controls">
    <label for="speed">Speed</label>
    <input id="speed" type="range" min="0" max="3" step="0.01" value="1">
    <span class="value" id="speedValue">1.00×</span>
    <button id="pauseBtn">Pause</button>
  </div>
  <div id="canvas-container">
    <canvas id="orrery"></canvas>
    <div id="tooltip"></div>
  </div>
</div>

<script>
(function() {
  const canvas = document.getElementById('orrery');
  const ctx = canvas.getContext('2d');
  const tooltip = document.getElementById('tooltip');
  const speedSlider = document.getElementById('speed');
  const speedValueLabel = document.getElementById('speedValue');
  const pauseBtn = document.getElementById('pauseBtn');

  let width = 0, height = 0, cx = 0, cy = 0;
  let running = true;
  let baseTime = performance.now();
  let lastFrameTime = baseTime;
  let simTime = 0;

  const AU = 50; // visual scale unit for Earth's orbit radius

  // Orbital periods in Earth days (approx); used proportionally
  const EARTH_PERIOD = 365.25;
  const planets = [
    { name: 'Mercury', color: '#c9c9c9', radius: 3, orbitRadius: 0.39 * AU, periodDays: 88 },
    { name: 'Venus',   color: '#f5d28a', radius: 4, orbitRadius: 0.72 * AU, periodDays: 225 },
    { name: 'Earth',   color: '#4fa9ff', radius: 4, orbitRadius: 1.0 * AU, periodDays: EARTH_PERIOD },
    { name: 'Mars',    color: '#ff6f5e', radius: 3.5, orbitRadius: 1.52 * AU, periodDays: 687 },
    { name: 'Jupiter', color: '#f0e0c0', radius: 7, orbitRadius: 5.2 * AU, periodDays: 4331 },
    { name: 'Saturn',  color: '#f6f1d6', radius: 6, orbitRadius: 9.5 * AU, periodDays: 10747 },
    { name: 'Uranus',  color: '#9be3ff', radius: 5, orbitRadius: 19.2 * AU, periodDays: 30589 },
    { name: 'Neptune', color: '#497cff', radius: 5, orbitRadius: 30.1 * AU, periodDays: 59800 }
  ];

  // Precompute angular velocities relative to Earth
  planets.forEach(p => {
    const rel = EARTH_PERIOD / p.periodDays; // Mercury fastest > 1, Neptune < 1
    p.angularVel = rel * 0.4; // base radians per simulated year; scaled visually
    p.angle = Math.random() * Math.PI * 2;
  });

  function resize() {
    const rect = canvas.parentElement.getBoundingClientRect();
    const dpr = window.devicePixelRatio || 1;
    width = rect.width;
    height = rect.height;
    canvas.width = width * dpr;
    canvas.height = height * dpr;
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    cx = width / 2;
    cy = height / 2;

    // Auto-scale AU to fit within viewport while preserving relative distances
    const maxOrbit = planets[planets.length - 1].orbitRadius; // Neptune
    const margin = 30;
    const maxRadius = Math.min(width, height) / 2 - margin;
    const scale = maxRadius / maxOrbit;
    planets.forEach(p => p.visualOrbit = p.orbitRadius * scale);
  }

  window.addEventListener('resize', resize);
  resize();

  function drawBackground() {
    ctx.save();
    ctx.fillStyle = '#000000';
    ctx.fillRect(0, 0, width, height);

    // Sparse stars
    ctx.fillStyle = 'rgba(255,255,255,0.5)';
    const starCount = Math.floor((width * height) / 15000);
    for (let i = 0; i < starCount; i++) {
      const x = Math.random() * width;
      const y = Math.random() * height;
      const r = Math.random() * 0.6 + 0.2;
      ctx.beginPath();
      ctx.arc(x, y, r, 0, Math.PI * 2);
      ctx.fill();
    }
    ctx.restore();
  }

  // Draw static background once (stars)
  drawBackground();

  function render() {
    // Clear dynamic layer (but keep the starry background by redrawing quickly)
    ctx.clearRect(0, 0, width, height);
    drawBackground();

    // Draw sun
    const sunRadius = 12;
    const gradient = ctx.createRadialGradient(cx, cy, 0, cx, cy, sunRadius * 3);
    gradient.addColorStop(0, '#fff8c2');
    gradient.addColorStop(0.3, '#ffd866');
    gradient.addColorStop(1, 'rgba(0,0,0,0)');
    ctx.fillStyle = gradient;
    ctx.beginPath();
    ctx.arc(cx, cy, sunRadius * 3, 0, Math.PI * 2);
    ctx.fill();

    ctx.fillStyle = '#ffeb7f';
    ctx.beginPath();
    ctx.arc(cx, cy, sunRadius, 0, Math.PI * 2);
    ctx.fill();

    // Orbits
    ctx.save();
    ctx.strokeStyle = 'rgba(180,180,200,0.25)';
    ctx.lineWidth = 1;
    ctx.setLineDash([4, 4]);
    planets.forEach(p => {
      ctx.beginPath();
      ctx.arc(cx, cy, p.visualOrbit, 0, Math.PI * 2);
      ctx.stroke();
    });
    ctx.restore();

    const hovered = getHoveredPlanet(lastMouse.x, lastMouse.y);
    // Planets
    planets.forEach(p => {
      const x = cx + Math.cos(p.angle) * p.visualOrbit;
      const y = cy + Math.sin(p.angle) * p.visualOrbit;
      p.screenX = x;
      p.screenY = y;

      ctx.beginPath();
      ctx.fillStyle = p === hovered ? '#ffffff' : p.color;
      ctx.arc(x, y, p.radius, 0, Math.PI * 2);
      ctx.fill();

      // Simple glow for outer planets for visibility
      if (p === hovered) {
        ctx.save();
        ctx.strokeStyle = 'rgba(255,255,255,0.6)';
        ctx.lineWidth = 1.5;
        ctx.beginPath();
        ctx.arc(x, y, p.radius + 3, 0, Math.PI * 2);
        ctx.stroke();
        ctx.restore();
      }
    });
  }

  function update(dt) {
    const speed = parseFloat(speedSlider.value);
    simTime += dt * speed;
    planets.forEach(p => {
      p.angle += p.angularVel * dt * speed;
      p.angle %= Math.PI * 2;
    });
  }

  function loop(now) {
    const dt = (now - lastFrameTime) / 1000; // seconds
    lastFrameTime = now;

    if (running) {
      update(dt);
    }
    render();
    requestAnimationFrame(loop);
  }
  requestAnimationFrame(loop);

  // Interactivity
  speedSlider.addEventListener('input', () => {
    const speed = parseFloat(speedSlider.value);
    speedValueLabel.textContent = speed.toFixed(2) + '×';
  });

  pauseBtn.addEventListener('click', () => {
    running = !running;
    pauseBtn.textContent = running ? 'Pause' : 'Resume';
  });

  const lastMouse = { x: -1000, y: -1000, inside: false };

  function getHoveredPlanet(x, y) {
    if (!lastMouse.inside) return null;
    const hitRadius = 8;
    let closest = null;
    let closestDist = Infinity;
    planets.forEach(p => {
      const dx = x - p.screenX;
      const dy = y - p.screenY;
      const d = Math.sqrt(dx*dx + dy*dy);
      if (d < p.radius + hitRadius && d < closestDist) {
        closest = p;
        closestDist = d;
      }
    });
    return closest;
  }

  function updateTooltip(x, y) {
    const planet = getHoveredPlanet(x, y);
    if (planet) {
      tooltip.textContent = planet.name;
      const rect = canvas.getBoundingClientRect();
      const tx = x - rect.left;
      const ty = y - rect.top;
      tooltip.style.left = tx + 'px';
      tooltip.style.top = ty + 'px';
      tooltip.style.opacity = '1';
    } else {
      tooltip.style.opacity = '0';
    }
  }

  canvas.addEventListener('mousemove', (e) => {
    const rect = canvas.getBoundingClientRect();
    lastMouse.x = e.clientX - rect.left;
    lastMouse.y = e.clientY - rect.top;
    lastMouse.inside = true;
    updateTooltip(lastMouse.x, lastMouse.y);
  });

  canvas.addEventListener('mouseleave', () => {
    lastMouse.inside = false;
    tooltip.style.opacity = '0';
  });

  // Ensure initial label
  speedValueLabel.textContent = parseFloat(speedSlider.value).toFixed(2) + '×';
})();
</script>
</body>
</html>