← 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>