← back to Model Arena

data/artifacts/0e969e848172/gpt.html

375 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>2D Gravity Sandbox</title>
<style>
  html, body {
    margin: 0;
    padding: 0;
    overflow: hidden;
    background: #050711;
    color: #ddd;
    font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  }
  #ui {
    position: fixed;
    top: 8px;
    left: 8px;
    right: 8px;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
    z-index: 10;
    pointer-events: none;
  }
  #ui > * {
    pointer-events: auto;
  }
  button, input, label {
    font-size: 12px;
  }
  button {
    background: #222;
    color: #eee;
    border: 1px solid #555;
    padding: 4px 8px;
    border-radius: 4px;
    cursor: pointer;
  }
  button:hover {
    background: #333;
  }
  button:active {
    background: #444;
  }
  #massLabel, #velLabel {
    margin-right: 4px;
  }
  #canvas {
    display: block;
  }
  .panel {
    background: rgba(0,0,0,0.5);
    border-radius: 6px;
    padding: 4px 6px;
    border: 1px solid rgba(255,255,255,0.08);
  }
</style>
</head>
<body>
<div id="ui">
  <div class="panel">
    <button id="clearBtn">Clear</button>
    <button id="pauseBtn">Pause</button>
    <label style="margin-left:6px;">
      <input type="checkbox" id="trailToggle" checked>
      Trails
    </label>
    <label style="margin-left:6px;">
      <input type="checkbox" id="showVectors">
      Vel. vectors
    </label>
  </div>
  <div class="panel">
    <span id="massLabel">Mass:</span>
    <input id="massSlider" type="range" min="1" max="200" value="40">
    <span id="massValue">40</span>
  </div>
  <div class="panel">
    <span id="velLabel">Initial speed:</span>
    <input id="velSlider" type="range" min="0" max="400" value="120">
    <span id="velValue">120</span>
  </div>
  <div class="panel" style="font-size:11px;">
    Click &amp; drag to set direction and speed. Release to spawn a body.
  </div>
</div>
<canvas id="canvas"></canvas>
<script>
(function() {
  const canvas = document.getElementById('canvas');
  const ctx = canvas.getContext('2d');

  const clearBtn = document.getElementById('clearBtn');
  const pauseBtn = document.getElementById('pauseBtn');
  const trailToggle = document.getElementById('trailToggle');
  const showVectors = document.getElementById('showVectors');
  const massSlider = document.getElementById('massSlider');
  const massValue = document.getElementById('massValue');
  const velSlider = document.getElementById('velSlider');
  const velValue = document.getElementById('velValue');

  let width = window.innerWidth;
  let height = window.innerHeight;
  canvas.width = width;
  canvas.height = height;

  window.addEventListener('resize', () => {
    width = window.innerWidth;
    height = window.innerHeight;
    canvas.width = width;
    canvas.height = height;
    // Do not clear bodies; just redraw
  });

  massSlider.addEventListener('input', () => {
    massValue.textContent = massSlider.value;
  });
  velSlider.addEventListener('input', () => {
    velValue.textContent = velSlider.value;
  });

  let bodies = [];
  let lastTime = performance.now();
  let paused = false;

  // Physics constants
  const G = 80; // gravitational constant (tuned for visual behavior)
  const softening = 4; // softening length to avoid huge forces
  const maxTrailLength = 200;
  const maxBodies = 200; // simple cap to avoid runaway performance

  function randomColor() {
    const h = Math.floor(Math.random() * 360);
    const s = 60 + Math.random() * 30;
    const l = 50 + Math.random() * 10;
    return `hsl(${h},${s}%,${l}%)`;
  }

  function addBody(x, y, vx, vy, mass) {
    if (bodies.length >= maxBodies) bodies.shift();
    const radius = Math.sqrt(mass) * 0.6 + 1.5;
    bodies.push({
      x, y, vx, vy, mass,
      radius,
      color: randomColor(),
      trail: []
    });
  }

  clearBtn.onclick = () => {
    bodies = [];
    clearCanvas(true);
  };

  pauseBtn.onclick = () => {
    paused = !paused;
    pauseBtn.textContent = paused ? "Resume" : "Pause";
  };

  function clearCanvas(full) {
    if (trailToggle.checked && !full) {
      ctx.fillStyle = "rgba(5,7,17,0.28)";
      ctx.fillRect(0, 0, width, height);
    } else {
      ctx.fillStyle = "#050711";
      ctx.fillRect(0, 0, width, height);
    }
  }

  function update(dt) {
    // semi-implicit Euler
    const n = bodies.length;
    for (let i = 0; i < n; i++) {
      let ax = 0, ay = 0;
      const bi = bodies[i];
      for (let j = 0; j < n; j++) {
        if (i === j) continue;
        const bj = bodies[j];
        let dx = bj.x - bi.x;
        let dy = bj.y - bi.y;
        const distSq = dx*dx + dy*dy + softening*softening;
        const invDist = 1 / Math.sqrt(distSq);
        const invDist3 = invDist * invDist * invDist;
        const f = G * bj.mass * invDist3;
        ax += dx * f;
        ay += dy * f;
      }
      bi.vx += ax * dt;
      bi.vy += ay * dt;
    }

    for (let i = 0; i < n; i++) {
      const b = bodies[i];
      b.x += b.vx * dt;
      b.y += b.vy * dt;
      if (trailToggle.checked) {
        b.trail.push({x: b.x, y: b.y});
        if (b.trail.length > maxTrailLength) b.trail.shift();
      } else {
        b.trail.length = 0;
      }
    }
  }

  function draw() {
    clearCanvas(false);

    // trails
    for (const b of bodies) {
      if (!trailToggle.checked || b.trail.length < 2) continue;
      const len = b.trail.length;
      ctx.lineWidth = 1;
      for (let i = 1; i < len; i++) {
        const p0 = b.trail[i - 1];
        const p1 = b.trail[i];
        const alpha = i / len;
        ctx.strokeStyle = b.color.replace('hsl', 'hsla').replace('%)', `%,${alpha * 0.7})`);
        ctx.beginPath();
        ctx.moveTo(p0.x, p0.y);
        ctx.lineTo(p1.x, p1.y);
        ctx.stroke();
      }
    }

    // bodies
    for (const b of bodies) {
      const grad = ctx.createRadialGradient(b.x, b.y, 0, b.x, b.y, b.radius*1.4);
      grad.addColorStop(0, "#ffffff");
      grad.addColorStop(0.3, b.color);
      grad.addColorStop(1, "rgba(0,0,0,0)");
      ctx.fillStyle = grad;
      ctx.beginPath();
      ctx.arc(b.x, b.y, b.radius*1.4, 0, Math.PI*2);
      ctx.fill();

      ctx.fillStyle = b.color;
      ctx.beginPath();
      ctx.arc(b.x, b.y, Math.max(1, b.radius*0.5), 0, Math.PI*2);
      ctx.fill();

      if (showVectors.checked) {
        const scale = 0.15;
        const vx = b.vx * scale;
        const vy = b.vy * scale;
        ctx.strokeStyle = "#88f";
        ctx.lineWidth = 1;
        ctx.beginPath();
        ctx.moveTo(b.x, b.y);
        ctx.lineTo(b.x + vx, b.y + vy);
        ctx.stroke();
      }
    }
  }

  // Input for spawning bodies
  let isDragging = false;
  let dragStart = {x: 0, y: 0};
  let dragEnd = {x: 0, y: 0};

  function getMousePos(evt) {
    const rect = canvas.getBoundingClientRect();
    return {
      x: (evt.clientX - rect.left),
      y: (evt.clientY - rect.top)
    };
  }

  canvas.addEventListener('mousedown', (e) => {
    isDragging = true;
    dragStart = getMousePos(e);
    dragEnd = {...dragStart};
  });

  canvas.addEventListener('mousemove', (e) => {
    if (!isDragging) return;
    dragEnd = getMousePos(e);
  });

  canvas.addEventListener('mouseup', (e) => {
    if (!isDragging) return;
    isDragging = false;
    dragEnd = getMousePos(e);
    const dx = dragEnd.x - dragStart.x;
    const dy = dragEnd.y - dragStart.y;
    const speedFactor = parseFloat(velSlider.value) / 100;
    const vx = dx * 0.01 * speedFactor;
    const vy = dy * 0.01 * speedFactor;
    const mass = parseFloat(massSlider.value);
    addBody(dragStart.x, dragStart.y, vx, vy, mass);
  });

  canvas.addEventListener('mouseleave', () => {
    isDragging = false;
  });

  // Touch support
  canvas.addEventListener('touchstart', (e) => {
    const t = e.touches[0];
    isDragging = true;
    dragStart = getMousePos(t);
    dragEnd = {...dragStart};
    e.preventDefault();
  }, {passive:false});

  canvas.addEventListener('touchmove', (e) => {
    if (!isDragging) return;
    const t = e.touches[0];
    dragEnd = getMousePos(t);
    e.preventDefault();
  }, {passive:false});

  canvas.addEventListener('touchend', (e) => {
    if (!isDragging) return;
    isDragging = false;
    const dx = dragEnd.x - dragStart.x;
    const dy = dragEnd.y - dragStart.y;
    const speedFactor = parseFloat(velSlider.value) / 100;
    const vx = dx * 0.01 * speedFactor;
    const vy = dy * 0.01 * speedFactor;
    const mass = parseFloat(massSlider.value);
    addBody(dragStart.x, dragStart.y, vx, vy, mass);
    e.preventDefault();
  }, {passive:false});

  function drawDragVector() {
    if (!isDragging) return;
    ctx.save();
    ctx.setLineDash([4, 3]);
    ctx.strokeStyle = "#0f8";
    ctx.lineWidth = 1;
    ctx.beginPath();
    ctx.moveTo(dragStart.x, dragStart.y);
    ctx.lineTo(dragEnd.x, dragEnd.y);
    ctx.stroke();

    const dx = dragEnd.x - dragStart.x;
    const dy = dragEnd.y - dragStart.y;
    const len = Math.hypot(dx, dy);
    if (len > 4) {
      const nx = dx / len;
      const ny = dy / len;
      const arrowSize = 8;
      const ax = dragEnd.x;
      const ay = dragEnd.y;
      ctx.beginPath();
      ctx.moveTo(ax, ay);
      ctx.lineTo(ax - nx * arrowSize + ny * arrowSize * 0.5,
                 ay - ny * arrowSize - nx * arrowSize * 0.5);
      ctx.moveTo(ax, ay);
      ctx.lineTo(ax - nx * arrowSize - ny * arrowSize * 0.5,
                 ay - ny * arrowSize + nx * arrowSize * 0.5);
      ctx.stroke();
    }
    ctx.restore();
  }

  function loop(now) {
    const dt = Math.min(0.05, (now - lastTime) / 1000);
    lastTime = now;

    if (!paused) update(dt);
    draw();
    drawDragVector();

    requestAnimationFrame(loop);
  }

  clearCanvas(true);
  requestAnimationFrame(loop);
})();
</script>
</body>
</html>