← back to Model Arena

data/artifacts/0e969e848172/grok.html

339 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>2D Gravity Sandbox</title>
<style>
  * { margin: 0; padding: 0; box-sizing: border-box; }
  html, body { width: 100%; height: 100%; overflow: hidden; background: #0a0a12; font-family: system-ui, sans-serif; }
  canvas { display: block; width: 100%; height: 100%; cursor: crosshair; }
  #ui {
    position: fixed; top: 12px; left: 12px; z-index: 10;
    display: flex; flex-wrap: wrap; gap: 8px; align-items: center;
  }
  #ui button, #ui label {
    background: rgba(20, 24, 40, 0.85);
    border: 1px solid rgba(120, 140, 200, 0.35);
    color: #c8d0e8;
    padding: 8px 14px;
    border-radius: 6px;
    font-size: 13px;
    cursor: pointer;
    backdrop-filter: blur(6px);
    user-select: none;
  }
  #ui button:hover { background: rgba(40, 50, 80, 0.9); border-color: rgba(150, 170, 230, 0.5); }
  #ui label { display: flex; align-items: center; gap: 6px; cursor: pointer; }
  #ui input[type="checkbox"] { accent-color: #6af; cursor: pointer; }
  #hint {
    position: fixed; bottom: 12px; left: 50%; transform: translateX(-50%);
    color: rgba(180, 190, 220, 0.5); font-size: 12px; pointer-events: none;
    text-align: center; white-space: nowrap;
  }
  #mass {
    position: fixed; top: 12px; right: 12px; z-index: 10;
    background: rgba(20, 24, 40, 0.85);
    border: 1px solid rgba(120, 140, 200, 0.35);
    color: #c8d0e8;
    padding: 8px 14px;
    border-radius: 6px;
    font-size: 13px;
    backdrop-filter: blur(6px);
    display: flex; align-items: center; gap: 8px;
  }
  #mass input[type="range"] { width: 100px; accent-color: #6af; cursor: pointer; }
</style>
</head>
<body>
<div id="ui">
  <button id="clearBtn">Clear</button>
  <label><input type="checkbox" id="trailToggle" checked> Trails</label>
  <label><input type="checkbox" id="softToggle" checked> Soften</label>
</div>
<div id="mass">
  <span>Mass</span>
  <input type="range" id="massSlider" min="1" max="100" value="20">
  <span id="massVal">20</span>
</div>
<div id="hint">Click to place bodies · Drag slightly to impart velocity</div>
<canvas id="c"></canvas>
<script>
(function () {
  const canvas = document.getElementById('c');
  const ctx = canvas.getContext('2d');
  const G = 0.4;
  const MIN_DIST = 4;
  const TRAIL_MAX = 180;
  const DT = 1;

  let W, H, dpr;
  let bodies = [];
  let showTrails = true;
  let soften = true;
  let hueCounter = 0;

  // Drag-to-velocity state
  let placing = false;
  let placeX = 0, placeY = 0;
  let placeStartX = 0, placeStartY = 0;

  function resize() {
    dpr = window.devicePixelRatio || 1;
    W = window.innerWidth;
    H = window.innerHeight;
    canvas.width = W * dpr;
    canvas.height = H * dpr;
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  }
  window.addEventListener('resize', resize);
  resize();

  function radiusFromMass(m) {
    return Math.max(3, Math.sqrt(m) * 2.2);
  }

  function nextHue() {
    hueCounter = (hueCounter + 47) % 360;
    return hueCounter;
  }

  function addBody(x, y, vx, vy, mass) {
    const hue = nextHue();
    bodies.push({
      x, y, vx, vy,
      mass,
      r: radiusFromMass(mass),
      hue,
      trail: [],
      color: `hsl(${hue}, 75%, 60%)`,
      glow: `hsla(${hue}, 80%, 55%, 0.35)`
    });
  }

  // Seed a few bodies for immediate interest
  function seed() {
    const cx = W / 2, cy = H / 2;
    addBody(cx, cy, 0, 0, 80);
    addBody(cx + 160, cy, 0, 1.6, 12);
    addBody(cx - 220, cy + 40, 0, -1.2, 8);
    addBody(cx + 60, cy - 200, 1.4, 0.3, 5);
  }
  seed();

  document.getElementById('clearBtn').addEventListener('click', () => {
    bodies = [];
  });
  document.getElementById('trailToggle').addEventListener('change', (e) => {
    showTrails = e.target.checked;
    if (!showTrails) bodies.forEach(b => { b.trail = []; });
  });
  document.getElementById('softToggle').addEventListener('change', (e) => {
    soften = e.target.checked;
  });
  const massSlider = document.getElementById('massSlider');
  const massVal = document.getElementById('massVal');
  massSlider.addEventListener('input', () => { massVal.textContent = massSlider.value; });

  canvas.addEventListener('pointerdown', (e) => {
    placing = true;
    placeX = placeStartX = e.clientX;
    placeY = placeStartY = e.clientY;
  });
  canvas.addEventListener('pointermove', (e) => {
    if (!placing) return;
    placeX = e.clientX;
    placeY = e.clientY;
  });
  function finishPlace(e) {
    if (!placing) return;
    placing = false;
    const x = placeStartX;
    const y = placeStartY;
    const dx = (e.clientX !== undefined ? e.clientX : placeX) - placeStartX;
    const dy = (e.clientY !== undefined ? e.clientY : placeY) - placeStartY;
    // Velocity from drag (scale down)
    const vx = dx * 0.035;
    const vy = dy * 0.035;
    const mass = parseFloat(massSlider.value) || 20;
    addBody(x, y, vx, vy, mass);
  }
  canvas.addEventListener('pointerup', finishPlace);
  canvas.addEventListener('pointercancel', () => { placing = false; });
  canvas.addEventListener('pointerleave', (e) => {
    if (placing) finishPlace(e);
  });

  function step() {
    const n = bodies.length;
    // Accelerations
    const ax = new Float64Array(n);
    const ay = new Float64Array(n);

    for (let i = 0; i < n; i++) {
      for (let j = i + 1; j < n; j++) {
        const a = bodies[i], b = bodies[j];
        let dx = b.x - a.x;
        let dy = b.y - a.y;
        let distSq = dx * dx + dy * dy;
        if (soften) distSq += MIN_DIST * MIN_DIST;
        else if (distSq < 1e-6) continue;
        const dist = Math.sqrt(distSq);
        const force = G / (distSq * dist); // G * m1*m2 / r^2, then /m later → G*other / r^3 * r_vec
        const fx = force * dx;
        const fy = force * dy;
        ax[i] += fx * b.mass;
        ay[i] += fy * b.mass;
        ax[j] -= fx * a.mass;
        ay[j] -= fy * a.mass;
      }
    }

    // Integrate + merge on collision
    const merged = new Set();
    for (let i = 0; i < n; i++) {
      if (merged.has(i)) continue;
      const b = bodies[i];
      b.vx += ax[i] * DT;
      b.vy += ay[i] * DT;
      b.x += b.vx * DT;
      b.y += b.vy * DT;

      if (showTrails) {
        b.trail.push(b.x, b.y);
        if (b.trail.length > TRAIL_MAX * 2) {
          b.trail.splice(0, b.trail.length - TRAIL_MAX * 2);
        }
      }
    }

    // Simple inelastic merge when overlapping
    for (let i = 0; i < bodies.length; i++) {
      if (merged.has(i)) continue;
      for (let j = i + 1; j < bodies.length; j++) {
        if (merged.has(j)) continue;
        const a = bodies[i], b = bodies[j];
        const dx = b.x - a.x, dy = b.y - a.y;
        const dist = Math.sqrt(dx * dx + dy * dy);
        if (dist < a.r + b.r * 0.5) {
          // Merge j into i
          const total = a.mass + b.mass;
          a.vx = (a.vx * a.mass + b.vx * b.mass) / total;
          a.vy = (a.vy * a.mass + b.vy * b.mass) / total;
          a.x = (a.x * a.mass + b.x * b.mass) / total;
          a.y = (a.y * a.mass + b.y * b.mass) / total;
          a.mass = total;
          a.r = radiusFromMass(total);
          // Keep heavier color influence
          if (b.mass > a.mass - b.mass) {
            a.hue = b.hue;
            a.color = b.color;
            a.glow = b.glow;
          }
          a.trail = [];
          merged.add(j);
        }
      }
    }
    if (merged.size) {
      bodies = bodies.filter((_, idx) => !merged.has(idx));
    }
  }

  function draw() {
    ctx.fillStyle = 'rgba(10, 10, 18, 0.35)';
    ctx.fillRect(0, 0, W, H);

    // Soft starfield residual: faint vignette
    // Trails
    if (showTrails) {
      for (const b of bodies) {
        const t = b.trail;
        if (t.length < 4) continue;
        ctx.beginPath();
        ctx.moveTo(t[0], t[1]);
        for (let i = 2; i < t.length; i += 2) {
          ctx.lineTo(t[i], t[i + 1]);
        }
        const len = t.length / 2;
        ctx.strokeStyle = b.color;
        ctx.globalAlpha = 0.55;
        ctx.lineWidth = Math.max(1, b.r * 0.35);
        ctx.lineCap = 'round';
        ctx.lineJoin = 'round';
        ctx.stroke();
        ctx.globalAlpha = 1;
      }
    }

    // Bodies
    for (const b of bodies) {
      // Glow
      const grd = ctx.createRadialGradient(b.x, b.y, 0, b.x, b.y, b.r * 3.5);
      grd.addColorStop(0, b.glow);
      grd.addColorStop(1, 'transparent');
      ctx.fillStyle = grd;
      ctx.beginPath();
      ctx.arc(b.x, b.y, b.r * 3.5, 0, Math.PI * 2);
      ctx.fill();

      // Core
      const core = ctx.createRadialGradient(
        b.x - b.r * 0.3, b.y - b.r * 0.3, b.r * 0.1,
        b.x, b.y, b.r
      );
      core.addColorStop(0, `hsl(${b.hue}, 60%, 88%)`);
      core.addColorStop(0.5, b.color);
      core.addColorStop(1, `hsl(${b.hue}, 80%, 35%)`);
      ctx.fillStyle = core;
      ctx.beginPath();
      ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2);
      ctx.fill();
    }

    // Placement preview
    if (placing) {
      const mass = parseFloat(massSlider.value) || 20;
      const r = radiusFromMass(mass);
      ctx.beginPath();
      ctx.arc(placeStartX, placeStartY, r, 0, Math.PI * 2);
      ctx.strokeStyle = 'rgba(200, 220, 255, 0.7)';
      ctx.lineWidth = 1.5;
      ctx.setLineDash([4, 4]);
      ctx.stroke();
      ctx.setLineDash([]);
      // Velocity arrow
      ctx.beginPath();
      ctx.moveTo(placeStartX, placeStartY);
      ctx.lineTo(placeX, placeY);
      ctx.strokeStyle = 'rgba(150, 200, 255, 0.8)';
      ctx.lineWidth = 2;
      ctx.stroke();
      // Arrow head
      const ang = Math.atan2(placeY - placeStartY, placeX - placeStartX);
      const ah = 8;
      ctx.beginPath();
      ctx.moveTo(placeX, placeY);
      ctx.lineTo(placeX - ah * Math.cos(ang - 0.4), placeY - ah * Math.sin(ang - 0.4));
      ctx.lineTo(placeX - ah * Math.cos(ang + 0.4), placeY - ah * Math.sin(ang + 0.4));
      ctx.closePath();
      ctx.fillStyle = 'rgba(150, 200, 255, 0.8)';
      ctx.fill();
    }
  }

  function loop() {
    // Substeps for stability with many bodies
    const sub = bodies.length > 25 ? 2 : 1;
    for (let s = 0; s < sub; s++) step();
    draw();
    requestAnimationFrame(loop);
  }
  // Initial clear
  ctx.fillStyle = '#0a0a12';
  ctx.fillRect(0, 0, W, H);
  requestAnimationFrame(loop);
})();
</script>
</body>
</html>