Physics Playground
GPT 5.6 terra · typescript

Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 15,852 bytes · SHA-256 0e71f6887d45
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Soft-Body Physics Playground</title>
<style>
/* ── css: layout ── */
*{box-sizing:border-box}html,body,#app,#playground{margin:0;width:100%;height:100%;overflow:hidden;background:#081016;color:#e5f5f3;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}#world{display:block;width:100%;height:100%;cursor:crosshair;background:radial-gradient(circle at 50% 0,#112b31 0,#081016 58%)}#panel{position:fixed;z-index:2;left:20px;top:18px;width:min(760px,calc(100vw - 40px));padding:12px 14px 10px;border:1px solid #2e5557;background:linear-gradient(110deg,#0d1c22eF,#0a1219eD);box-shadow:0 12px 40px #0008;border-radius:8px;backdrop-filter:blur(10px);display:flex;gap:15px;align-items:center;flex-wrap:wrap}.brand{display:flex;align-items:center;gap:9px;letter-spacing:.08em}.brand b{font-size:14px;color:#8affdc}.brand small{display:block;font-size:8px;opacity:.55;margin-top:3px}.orb{width:18px;height:18px;border-radius:50%;background:#56e5cc;box-shadow:0 0 16px #56e5cc}.tools,.actions{display:flex;gap:5px}.tool,button,select{font:inherit;font-size:10px;letter-spacing:.03em;border:1px solid #31545a;background:#10242a;color:#bdd5d3;border-radius:4px;padding:7px 8px;cursor:pointer}.tool:hover,button:hover{border-color:#69dbc6;color:#efffff}.tool.active{background:#245c5b;color:#baffef;border-color:#6ae8d0}.tool.danger.active{background:#682f3e;border-color:#ff6e88}.settings{display:flex;gap:10px;align-items:center;margin-left:auto}.settings label{font-size:9px;color:#90aaa9;display:flex;align-items:center;gap:5px}.settings input{width:70px;accent-color:#64e5cb}.settings output{color:#baffef;width:22px}.settings select{padding:4px;font-size:9px}.actions button{padding:6px}.readout{display:flex;gap:11px;font-size:9px;color:#9ab4b2;margin-left:auto}.readout i{display:inline-block;width:6px;height:6px;border-radius:50%;background:#55e3bd;box-shadow:0 0 7px #55e3bd}.hint{width:100%;margin:0;font-size:9px;color:#73908e}.hint b{color:#b8e8df}#toast{position:fixed;z-index:3;pointer-events:none;top:115px;left:50%;transform:translateX(-50%) scale(.7);opacity:0;padding:7px 15px;border:1px solid #ff6680;color:#ff91a3;background:#351722;border-radius:99px;font-weight:bold;font-size:11px;letter-spacing:.16em;transition:.25s}#toast.show{opacity:1;transform:translateX(-50%) scale(1)}@media(max-width:740px){#panel{left:10px;top:10px;width:calc(100% - 20px);gap:8px}.brand{display:none}.settings{margin-left:0}.tool span{display:none}.hint{display:none}}
</style>
</head>
<body>
<main id="playground"><canvas id="world"></canvas><section id="panel"><div class="brand"><span class="orb"></span><div><b>SOFT / LAB</b><small>VERLET PLAYGROUND</small></div></div><div class="tools" role="toolbar"><button class="tool active" data-tool="grab">↖ <span>Grab</span></button><button class="tool" data-tool="rope">⌇ <span>Rope</span></button><button class="tool" data-tool="cloth">▦ <span>Cloth</span></button><button class="tool" data-tool="blob">◉ <span>Blob</span></button><button class="tool" data-tool="ball">● <span>Ball</span></button><button class="tool danger" data-tool="cut">✂ <span>Cut</span></button></div><div class="settings"><label>Gravity <input id="gravity" type="range" min="0" max="1800" value="980"><output id="gravityOut">980</output></label><label>Solver <input id="iterations" type="range" min="2" max="12" value="7"><output id="iterationsOut">7</output></label><label>Pin <select id="pinMode"><option value="corners">corners</option><option value="edge">full edge</option></select></label></div><div class="actions"><button id="pause">Ⅱ Pause</button><button id="step">→ Step</button><button id="stress">Stress</button><button id="reset">↻ Reset</button></div><div class="readout"><span id="status"><i></i> RUNNING</span><span id="stats">60 FPS · 0.0 ms</span></div><p class="hint">Drag to create · <b>Shift + click</b> pin · <b>Right-drag</b> cut · <b>Space</b> pause</p></section><div id="toast">CUT</div></main>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {};
function __require(id) {
if (__cache[id]) return __cache[id].exports;
var module = __cache[id] = { exports: {} };
var factory = __mods[id];
if (!factory) throw new Error("Module not found: " + id);
factory(module.exports, function (spec) {
var target = (__map[id] && __map[id][spec]) || spec;
return __require(target);
}, module);
return module.exports;
}
// ── module: src/main.ts ──
__mods["src/main.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
// SOFT / LAB — fixed-step Verlet + position based constraint relaxation
const canvas = document.querySelector('#world'), ctx = canvas.getContext('2d');
const $ = (s) => document.querySelector(s);
const gravity = $('#gravity'), its = $('#iterations'), gOut = $('#gravityOut'), iOut = $('#iterationsOut');
const floorPad = 28, DT = 1 / 60, DAMP = .992, MAX_DELTA = .05, COLORS = { rope: '#ffc85a', cloth: '#5ce3d0', blob: '#bd82ff', ball: '#ff6689' };
let W = 0, H = 0, tool = 'grab', paused = false, stress = false, wind = 0, last = performance.now(), acc = 0, frameN = 0, frameT = last, fps = 60, ms = 0;
const points = [];
const links = [];
const circles = [{ x: 0, y: 0, r: 74 }, { x: 0, y: 0, r: 43 }];
function resize() { W = canvas.width = innerWidth * devicePixelRatio; H = canvas.height = innerHeight * devicePixelRatio; canvas.style.width = innerWidth + 'px'; canvas.style.height = innerHeight + 'px'; ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0); circles[0].x = innerWidth * .52; circles[0].y = innerHeight * .69; circles[1].x = innerWidth * .79; circles[1].y = innerHeight * .80; }
addEventListener('resize', resize);
resize();
function pt(x, y, r, kind, pin = false) { const p = { x, y, px: x, py: y, r, kind, pin }; points.push(p); return p; }
function link(a, b, kind) { links.push({ a, b, len: Math.max(1, Math.hypot(a.x - b.x, a.y - b.y)), kind }); }
function rope(x, y, x2, y2) { const n = Math.max(5, Math.min(30, Math.ceil(Math.hypot(x2 - x, y2 - y) / 17))), arr = []; for (let i = 0; i < n; i++) {
const t = i / (n - 1);
arr.push(pt(x + (x2 - x) * t, y + (y2 - y) * t, 4, 'rope', i === 0));
} for (let i = 1; i < n; i++)
link(arr[i - 1], arr[i], 'rope'); }
function cloth(x, y, x2, y2) { const cols = Math.max(5, Math.min(22, Math.round(Math.abs(x2 - x) / 18))), rows = Math.max(4, Math.min(16, Math.round(Math.abs(y2 - y) / 18))), dx = (x2 - x) / (cols - 1), dy = (y2 - y) / (rows - 1), a = []; for (let j = 0; j < rows; j++) {
a[j] = [];
for (let i = 0; i < cols; i++)
a[j][i] = pt(x + i * dx, y + j * dy, 3, 'cloth', j === 0 && ($('#pinMode').value === 'edge' || i === 0 || i === cols - 1));
} for (let j = 0; j < rows; j++)
for (let i = 0; i < cols; i++) {
if (i)
link(a[j][i - 1], a[j][i], 'cloth');
if (j)
link(a[j - 1][i], a[j][i], 'cloth');
if (i && j)
link(a[j - 1][i - 1], a[j][i], 'cloth');
if (i < cols - 1 && j)
link(a[j - 1][i + 1], a[j][i], 'cloth');
} }
function blob(x, y, x2, y2) { const r = Math.max(18, Math.min(70, Math.hypot(x2 - x, y2 - y))), n = 12, a = []; const c = pt(x, y, 4, 'blob'); for (let i = 0; i < n; i++)
a.push(pt(x + Math.cos(i / n * Math.PI * 2) * r, y + Math.sin(i / n * Math.PI * 2) * r, 5, 'blob')); for (let i = 0; i < n; i++) {
link(a[i], a[(i + 1) % n], 'blob');
link(a[i], c, 'blob');
link(a[i], a[(i + 2) % n], 'blob');
} }
function ball(x, y, x2, y2) { pt(x, y, Math.max(10, Math.min(32, Math.hypot(x2 - x, y2 - y) || 16)), 'ball'); }
function collide(p) { const minX = p.r + 5, maxX = innerWidth - p.r - 5, minY = p.r + 5, maxY = innerHeight - floorPad - p.r; if (p.x < minX) {
p.x = minX;
p.px = p.x + (p.x - p.px) * .25;
} if (p.x > maxX) {
p.x = maxX;
p.px = p.x + (p.x - px(p)) * .25;
} if (p.y < minY) {
p.y = minY;
p.py = p.y + (p.y - p.py) * .25;
} if (p.y > maxY) {
p.y = maxY;
p.py = p.y + (p.y - p.py) * .22;
} for (const c of circles) {
const dx = p.x - c.x, dy = p.y - c.y, d = Math.hypot(dx, dy), min = c.r + p.r;
if (d < min) {
const q = d > .001 ? d : 1, nx = q === 1 ? 1 : dx / q, ny = q === 1 ? 0 : dy / q;
p.x = c.x + nx * min;
p.y = c.y + ny * min;
const vx = p.x - p.px, vy = p.y - p.py;
p.px = p.x - vx * .55;
p.py = p.y - vy * .55;
}
} }
function px(p) { return p.px; }
let grab = null, mx = 0, my = 0, pmx = 0, pmy = 0, down = false, cutLast = null;
function step() { const t0 = performance.now(), g = +gravity.value; for (const p of points) {
if (p.pin)
continue;
const vx = (p.x - p.px) * DAMP, vy = (p.y - p.py) * DAMP;
p.px = p.x;
p.py = p.y;
p.x += vx + wind * DT * DT;
p.y += vy + g * DT * DT;
} if (grab) {
grab.x = mx;
grab.y = my;
grab.px = mx - (mx - pmx);
grab.py = my - (my - pmy);
} for (let k = 0; k < +its.value; k++) {
for (const l of links) {
const dx = l.b.x - l.a.x, dy = l.b.y - l.a.y, d = Math.hypot(dx, dy);
if (!Number.isFinite(d) || d < .0001)
continue;
const f = (d - l.len) / d * .5, ax = l.a.pin ? 0 : 1, bx = l.b.pin ? 0 : 1, div = ax + bx || 1;
if (ax) {
l.a.x += dx * f / div;
l.a.y += dy * f / div;
}
if (bx) {
l.b.x -= dx * f / div;
l.b.y -= dy * f / div;
}
}
for (const p of points)
if (!p.pin)
collide(p);
if (grab) {
grab.x = mx;
grab.y = my;
}
} ms = performance.now() - t0; }
function segmentHit(a, b, c, d) { const den = (b.x - a.x) * (d.y - c.y) - (b.y - a.y) * (d.x - c.x); if (Math.abs(den) < .001)
return false; const u = ((c.x - a.x) * (b.y - a.y) - (c.y - a.y) * (b.x - a.x)) / den, v = ((c.x - a.x) * (d.y - c.y) - (c.y - a.y) * (d.x - c.x)) / den; return u >= 0 && u <= 1 && v >= 0 && v <= 1; }
function cut(a, b) { let n = 0; for (let i = links.length - 1; i >= 0; i--)
if (segmentHit(a, b, links[i].a, links[i].b)) {
links.splice(i, 1);
n++;
} if (n) {
const t = $('#toast');
t.classList.add('show');
setTimeout(() => t.classList.remove('show'), 280);
} }
function draw() { ctx.clearRect(0, 0, innerWidth, innerHeight); ctx.fillStyle = '#0c1c20'; ctx.fillRect(0, innerHeight - floorPad, innerWidth, floorPad); ctx.strokeStyle = '#214147'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(0, innerHeight - floorPad + .5); ctx.lineTo(innerWidth, innerHeight - floorPad + .5); ctx.stroke(); for (const c of circles) {
const gr = ctx.createRadialGradient(c.x - c.r * .3, c.y - c.r * .35, 2, c.x, c.y, c.r);
gr.addColorStop(0, '#2b5a63');
gr.addColorStop(1, '#142d34');
ctx.fillStyle = gr;
ctx.beginPath();
ctx.arc(c.x, c.y, c.r, 0, 7);
ctx.fill();
ctx.strokeStyle = '#467780';
ctx.stroke();
} ctx.lineCap = 'round'; for (const l of links) {
const d = Math.hypot(l.a.x - l.b.x, l.a.y - l.b.y), ratio = d / l.len;
ctx.strokeStyle = stress ? `hsl(${Math.max(0, 125 - (ratio - 1) * 520)},85%,60%)` : COLORS[l.kind];
ctx.globalAlpha = l.kind === 'cloth' ? .53 : .9;
ctx.lineWidth = l.kind === 'blob' ? 3 : l.kind === 'rope' ? 2.6 : 1;
ctx.beginPath();
ctx.moveTo(l.a.x, l.a.y);
ctx.lineTo(l.b.x, l.b.y);
ctx.stroke();
} ctx.globalAlpha = 1; for (const p of points) {
if (p.kind === 'cloth' && !p.pin)
continue;
ctx.fillStyle = p.pin ? '#f7fff2' : COLORS[p.kind];
ctx.beginPath();
ctx.arc(p.x, p.y, p.pin ? 5 : p.r * .55, 0, 7);
ctx.fill();
} if (down && (tool !== 'grab' && tool !== 'cut')) {
ctx.strokeStyle = COLORS[tool];
ctx.setLineDash([5, 5]);
ctx.strokeRect(Math.min(mx, pmx), Math.min(my, pmy), Math.abs(mx - pmx), Math.abs(my - pmy));
ctx.setLineDash([]);
} ctx.fillStyle = '#bfffee'; ctx.font = '10px ui-monospace'; ctx.fillText(`${points.length} particles · ${links.length} links`, 18, innerHeight - 11); }
function loop(now) { const delta = Math.min(MAX_DELTA, (now - last) / 1000); last = now; if (!paused) {
acc += delta;
while (acc >= DT) {
step();
acc -= DT;
}
} frameN++; if (now - frameT > 500) {
fps = Math.round(frameN * 1000 / (now - frameT));
frameN = 0;
frameT = now;
$('#stats').textContent = `${fps} FPS · ${ms.toFixed(1)} ms`;
} draw(); requestAnimationFrame(loop); }
canvas.addEventListener('contextmenu', e => e.preventDefault());
canvas.addEventListener('pointerdown', e => { const r = canvas.getBoundingClientRect(); mx = pmx = e.clientX - r.left; my = pmy = e.clientY - r.top; down = true; canvas.setPointerCapture(e.pointerId); if (e.button === 2 || tool === 'cut') {
tool === 'cut';
cutLast = { x: mx, y: my };
return;
} if (e.shiftKey) {
let near = nearest(mx, my, 18);
if (near)
near.pin = !near.pin;
return;
} if (tool === 'grab')
grab = nearest(mx, my, 28); });
canvas.addEventListener('pointermove', e => { const r = canvas.getBoundingClientRect(); const ox = mx, oy = my; mx = e.clientX - r.left; my = e.clientY - r.top; if (cutLast) {
cut(cutLast, { x: mx, y: my });
cutLast = { x: mx, y: my };
} if (!down) {
pmx = mx;
pmy = my;
}
else if (grab) {
pmx = ox;
pmy = oy;
} });
canvas.addEventListener('pointerup', () => { if (down && !grab && !cutLast) {
const dx = mx - pmx, dy = my - pmy;
if (Math.hypot(dx, dy) > 5) {
if (tool === 'rope')
rope(pmx, pmy, mx, my);
if (tool === 'cloth')
cloth(pmx, pmy, mx, my);
if (tool === 'blob')
blob(pmx, pmy, mx, my);
if (tool === 'ball')
ball(pmx, pmy, mx, my);
}
} grab = null; cutLast = null; down = false; });
function nearest(x, y, rad) { let best = null, d = rad; for (const p of points) {
const q = Math.hypot(p.x - x, p.y - y);
if (q < d) {
d = q;
best = p;
}
} return best; }
document.querySelectorAll('.tool').forEach(b => b.onclick = () => { tool = b.dataset.tool; document.querySelector('.tool.active')?.classList.remove('active'); b.classList.add('active'); canvas.style.cursor = tool === 'grab' ? 'grab' : 'crosshair'; });
gravity.oninput = () => gOut.value = gravity.value;
its.oninput = () => iOut.value = its.value;
$('#pause').onclick = () => { paused = !paused; $('#pause').textContent = paused ? '▶ Resume' : 'Ⅱ Pause'; $('#status').innerHTML = `<i style="background:${paused ? '#ffca5b' : '#55e3bd'}"></i> ${paused ? 'PAUSED' : 'RUNNING'}`; };
$('#step').onclick = () => { if (paused)
step(); };
$('#stress').onclick = () => { stress = !stress; $('#stress').style.color = stress ? '#baffef' : ''; };
$('#reset').onclick = () => { points.length = links.length = 0; seed(); };
addEventListener('keydown', e => { if (e.code === 'Space') {
e.preventDefault();
$('#pause').click();
} if (e.code === 'ArrowRight' && paused)
step(); });
function seed() { rope(130, 110, 315, 215); cloth(410, 95, 650, 270); blob(900, 120, 945, 120); ball(1080, 160, 1098, 160); }
seed();
window.sim = { points, links, step, cutLine: cut, reset: () => { points.length = links.length = 0; }, get paused() { return paused; } };
requestAnimationFrame(loop);
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"openai/gpt-5.6-terra","provider":"openrouter","persona":"typescript","sessionId":"efb059e0-e6a5-439f-9209-ca76924c6204","tokensIn":124649,"tokensOut":5796,"tokensTotal":130445,"turns":15,"toolCalls":14,"failedToolCalls":2,"timestamp":"2026-07-31T17:52:48.054Z"} -->