← 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 &nbsp;·&nbsp; Click &amp; drag to fling it &nbsp;·&nbsp; 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>