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