← back to Model Arena
data/artifacts/0e969e848172/claude-code.html
440 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gravity Sandbox</title>
<style>
html, body {
margin: 0;
padding: 0;
height: 100%;
overflow: hidden;
background: #05060a;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
user-select: none;
-webkit-user-select: none;
}
#space {
display: block;
position: absolute;
inset: 0;
cursor: crosshair;
}
#hud {
position: absolute;
top: 12px;
left: 12px;
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
background: rgba(10, 14, 24, 0.72);
border: 1px solid rgba(120, 160, 255, 0.25);
border-radius: 10px;
padding: 10px 12px;
color: #cfe0ff;
backdrop-filter: blur(6px);
z-index: 10;
max-width: calc(100vw - 40px);
}
#hud .title {
font-weight: 700;
letter-spacing: 0.5px;
font-size: 13px;
color: #ffffff;
margin-right: 4px;
}
button {
background: #16223c;
color: #cfe0ff;
border: 1px solid rgba(120, 160, 255, 0.35);
border-radius: 7px;
padding: 6px 12px;
font-size: 12px;
cursor: pointer;
transition: background 0.15s, transform 0.05s;
}
button:hover { background: #1f3157; }
button:active { transform: scale(0.96); }
button.active {
background: #2f5fd0;
color: #fff;
border-color: #6f9bff;
}
label.ctl {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: #9fb6e6;
}
input[type=range] {
width: 90px;
accent-color: #5b8cff;
}
#hint {
position: absolute;
bottom: 12px;
left: 50%;
transform: translateX(-50%);
color: rgba(180, 200, 240, 0.55);
font-size: 12px;
background: rgba(10, 14, 24, 0.55);
padding: 6px 14px;
border-radius: 999px;
pointer-events: none;
z-index: 10;
white-space: nowrap;
}
#stats {
font-size: 11px;
color: #7e94c4;
margin-left: 4px;
}
</style>
</head>
<body>
<canvas id="space"></canvas>
<div id="hud">
<span class="title">GRAVITY SANDBOX</span>
<label class="ctl">Mass
<input type="range" id="mass" min="1" max="100" value="25">
<span id="massVal">25</span>
</label>
<button id="trailBtn" class="active" title="Toggle motion trails">Trails: ON</button>
<button id="pauseBtn" title="Pause / resume simulation">Pause</button>
<button id="clearBtn" title="Remove all bodies">Clear</button>
<span id="stats">0 bodies</span>
</div>
<div id="hint">Click to drop a body · Click & drag to fling it · Big mass = a star that anchors orbits</div>
<script>
(function () {
"use strict";
var canvas = document.getElementById("space");
var ctx = canvas.getContext("2d");
var DPR = Math.min(window.devicePixelRatio || 1, 2);
var W = 0, H = 0;
function resize() {
W = window.innerWidth;
H = window.innerHeight;
canvas.width = Math.floor(W * DPR);
canvas.height = Math.floor(H * DPR);
canvas.style.width = W + "px";
canvas.style.height = H + "px";
ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
paintBackground(1); // full repaint after resize clears the canvas
}
// ---------- simulation state ----------
var G = 900; // gravitational constant (tuned for pixels)
var SOFTEN = 12; // softening length to avoid singular slingshots
var bodies = [];
var trailsOn = true;
var paused = false;
var nextHue = 0;
function radiusFor(mass) {
return 2.5 + 2.2 * Math.cbrt(mass);
}
function makeBody(x, y, vx, vy, mass) {
nextHue = (nextHue + 47) % 360;
return {
x: x, y: y,
vx: vx, vy: vy,
ax: 0, ay: 0,
mass: mass,
r: radiusFor(mass),
hue: nextHue,
big: mass >= 60
};
}
// ---------- physics: velocity Verlet N-body ----------
function computeAccel() {
var i, j, n = bodies.length;
for (i = 0; i < n; i++) { bodies[i].ax = 0; bodies[i].ay = 0; }
for (i = 0; i < n; i++) {
var a = bodies[i];
for (j = i + 1; j < n; j++) {
var b = bodies[j];
var dx = b.x - a.x;
var dy = b.y - a.y;
var d2 = dx * dx + dy * dy + SOFTEN * SOFTEN;
var invD = 1 / Math.sqrt(d2);
var f = G * invD / d2; // G / d^2 * (unit vector handled below)
a.ax += f * b.mass * dx;
a.ay += f * b.mass * dy;
b.ax -= f * a.mass * dx;
b.ay -= f * a.mass * dy;
}
}
}
function mergeCollisions() {
for (var i = 0; i < bodies.length; i++) {
for (var j = i + 1; j < bodies.length; j++) {
var a = bodies[i], b = bodies[j];
var dx = b.x - a.x, dy = b.y - a.y;
var rr = (a.r + b.r) * 0.6;
if (dx * dx + dy * dy < rr * rr) {
// inelastic merge, conserve momentum
var m = a.mass + b.mass;
a.x = (a.x * a.mass + b.x * b.mass) / m;
a.y = (a.y * a.mass + b.y * b.mass) / m;
a.vx = (a.vx * a.mass + b.vx * b.mass) / m;
a.vy = (a.vy * a.mass + b.vy * b.mass) / m;
if (b.mass > a.mass) { a.hue = b.hue; }
a.mass = m;
a.r = radiusFor(m);
a.big = m >= 60;
bodies.splice(j, 1);
j--;
updateStats();
}
}
}
}
function step(dt) {
var i, n = bodies.length;
for (i = 0; i < n; i++) {
var b = bodies[i];
b.vx += b.ax * dt * 0.5;
b.vy += b.ay * dt * 0.5;
b.x += b.vx * dt;
b.y += b.vy * dt;
}
computeAccel();
for (i = 0; i < n; i++) {
var c = bodies[i];
c.vx += c.ax * dt * 0.5;
c.vy += c.ay * dt * 0.5;
}
mergeCollisions();
// cull bodies flung far off-screen
var margin = 3000;
for (i = bodies.length - 1; i >= 0; i--) {
var d = bodies[i];
if (d.x < -margin || d.x > W + margin || d.y < -margin || d.y > H + margin) {
bodies.splice(i, 1);
updateStats();
}
}
}
// ---------- starfield background ----------
var stars = [];
function seedStars() {
stars = [];
var count = Math.floor((W * H) / 6000);
for (var i = 0; i < count; i++) {
stars.push({
x: Math.random() * W,
y: Math.random() * H,
s: Math.random() * 1.3 + 0.3,
a: Math.random() * 0.5 + 0.15
});
}
}
function paintBackground(alpha) {
// trails work by painting a translucent dark layer each frame;
// alpha=1 gives a hard clear
ctx.fillStyle = "rgba(5, 6, 10," + alpha + ")";
ctx.fillRect(0, 0, W, H);
if (alpha === 1) {
for (var i = 0; i < stars.length; i++) {
var s = stars[i];
ctx.fillStyle = "rgba(200,215,255," + s.a + ")";
ctx.fillRect(s.x, s.y, s.s, s.s);
}
}
}
// ---------- rendering ----------
function draw() {
if (trailsOn) {
paintBackground(0.08); // fade — old positions linger as colored trails
} else {
paintBackground(1);
}
for (var i = 0; i < bodies.length; i++) {
var b = bodies[i];
var col = "hsl(" + b.hue + ", 90%, 65%)";
if (b.big) {
// star-like glow for heavy bodies
var g = ctx.createRadialGradient(b.x, b.y, 0, b.x, b.y, b.r * 4);
g.addColorStop(0, "hsla(" + b.hue + ", 95%, 75%, 0.55)");
g.addColorStop(1, "hsla(" + b.hue + ", 95%, 75%, 0)");
ctx.fillStyle = g;
ctx.beginPath();
ctx.arc(b.x, b.y, b.r * 4, 0, Math.PI * 2);
ctx.fill();
}
ctx.beginPath();
ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2);
ctx.fillStyle = col;
ctx.fill();
ctx.beginPath();
ctx.arc(b.x - b.r * 0.3, b.y - b.r * 0.3, b.r * 0.45, 0, Math.PI * 2);
ctx.fillStyle = "rgba(255,255,255,0.5)";
ctx.fill();
}
// drag preview: launch vector
if (dragging && dragStart) {
ctx.beginPath();
ctx.moveTo(dragStart.x, dragStart.y);
ctx.lineTo(dragNow.x, dragNow.y);
ctx.strokeStyle = "rgba(140,180,255,0.8)";
ctx.lineWidth = 1.5;
ctx.setLineDash([5, 5]);
ctx.stroke();
ctx.setLineDash([]);
var pm = +massSlider.value;
ctx.beginPath();
ctx.arc(dragStart.x, dragStart.y, radiusFor(pm), 0, Math.PI * 2);
ctx.strokeStyle = "rgba(200,220,255,0.9)";
ctx.stroke();
}
}
// ---------- main loop ----------
var last = performance.now();
function frame(now) {
var dt = Math.min((now - last) / 1000, 0.033); // clamp big tab-switch gaps
last = now;
if (!paused) {
// substep for stability at close encounters
var sub = 4;
for (var s = 0; s < sub; s++) step(dt / sub);
}
draw();
requestAnimationFrame(frame);
}
// ---------- input ----------
var massSlider = document.getElementById("mass");
var massVal = document.getElementById("massVal");
var dragging = false;
var dragStart = null;
var dragNow = { x: 0, y: 0 };
massSlider.addEventListener("input", function () {
massVal.textContent = massSlider.value;
});
function pos(e) {
var r = canvas.getBoundingClientRect();
var t = e.touches ? e.touches[0] : e;
return { x: t.clientX - r.left, y: t.clientY - r.top };
}
function down(e) {
if (e.target !== canvas) return;
dragging = true;
dragStart = pos(e);
dragNow = { x: dragStart.x, y: dragStart.y };
e.preventDefault();
}
function move(e) {
if (!dragging) return;
dragNow = pos(e.changedTouches ? { touches: e.changedTouches } : e);
}
function up(e) {
if (!dragging) return;
dragging = false;
var end = e.changedTouches ? { x: e.changedTouches[0].clientX - canvas.getBoundingClientRect().left,
y: e.changedTouches[0].clientY - canvas.getBoundingClientRect().top }
: pos(e);
var vx = (end.x - dragStart.x) * 1.6;
var vy = (end.y - dragStart.y) * 1.6;
bodies.push(makeBody(dragStart.x, dragStart.y, vx, vy, +massSlider.value));
dragStart = null;
updateStats();
}
canvas.addEventListener("mousedown", down);
window.addEventListener("mousemove", move);
window.addEventListener("mouseup", up);
canvas.addEventListener("touchstart", down, { passive: false });
window.addEventListener("touchmove", move, { passive: true });
window.addEventListener("touchend", up);
// ---------- HUD buttons ----------
var trailBtn = document.getElementById("trailBtn");
var pauseBtn = document.getElementById("pauseBtn");
var clearBtn = document.getElementById("clearBtn");
var stats = document.getElementById("stats");
trailBtn.addEventListener("click", function () {
trailsOn = !trailsOn;
trailBtn.textContent = "Trails: " + (trailsOn ? "ON" : "OFF");
trailBtn.classList.toggle("active", trailsOn);
if (!trailsOn) paintBackground(1);
});
pauseBtn.addEventListener("click", function () {
paused = !paused;
pauseBtn.textContent = paused ? "Resume" : "Pause";
pauseBtn.classList.toggle("active", paused);
});
clearBtn.addEventListener("click", function () {
bodies = [];
paintBackground(1);
updateStats();
});
function updateStats() {
stats.textContent = bodies.length + (bodies.length === 1 ? " body" : " bodies");
}
// ---------- boot: seed a small demo system ----------
function seedDemo() {
var cx = W / 2, cy = H / 2;
var star = makeBody(cx, cy, 0, 0, 90);
star.hue = 48; // warm star
bodies.push(star);
var dists = [110, 170, 240];
for (var i = 0; i < dists.length; i++) {
var d = dists[i];
var ang = Math.random() * Math.PI * 2;
var v = Math.sqrt(G * star.mass / d); // circular orbital speed
bodies.push(makeBody(
cx + Math.cos(ang) * d,
cy + Math.sin(ang) * d,
-Math.sin(ang) * v,
Math.cos(ang) * v,
3 + i * 4
));
}
computeAccel();
updateStats();
}
window.addEventListener("resize", function () {
resize();
seedStars();
paintBackground(1);
});
resize();
seedStars();
paintBackground(1);
seedDemo();
requestAnimationFrame(frame);
})();
</script>
</body>
</html>