Physics Playground
GPT 5.6 Luna Pro · 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 21,411 bytes · SHA-256 cf170906302d
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Verlet Atelier — Soft-Body Playground</title>
<style>
/* ── css: main ── */
*{box-sizing:border-box}html,body{margin:0;background:#0d1018;color:#e7e9f2;font-family:Inter,ui-sans-serif,system-ui,sans-serif;height:100%;overflow:hidden}body{background:radial-gradient(circle at 70% 0%,#192338 0,#0d1018 55%)}.shell{height:100vh;display:flex;flex-direction:column;padding:20px 28px 14px;max-width:1500px;margin:auto}.topbar{height:68px;display:flex;justify-content:space-between;align-items:center}.brand{display:flex;gap:13px;align-items:center}.brand-mark{font-size:31px;color:#72e0c1;text-shadow:0 0 20px #44ceb0}.brand h1{font-size:16px;letter-spacing:3px;margin:0;font-weight:800}.brand p{font-size:11px;color:#727b91;letter-spacing:1.5px;margin:4px 0}.readouts{display:flex;gap:30px;text-align:right}.readouts div{display:flex;flex-direction:column}.readouts b{font-size:18px;color:#72e0c1;font-variant-numeric:tabular-nums}.readouts span,footer{font-size:9px;letter-spacing:1.5px;color:#727b91}.workspace{position:relative;flex:1;min-height:0;border:1px solid #273149;border-radius:10px;background:#101725;box-shadow:0 20px 50px #0005;overflow:hidden}canvas{width:100%;height:100%;display:block;cursor:crosshair}.panel{position:absolute;top:16px;right:16px;width:208px;background:#131b2bdf;border:1px solid #33425e;border-radius:9px;backdrop-filter:blur(14px);padding:14px;box-shadow:0 12px 30px #0006}.panel-head{font-size:10px;letter-spacing:2px;color:#95a0b8;display:flex;justify-content:space-between;margin-bottom:12px}.live-dot{color:#70e0bd;font-size:9px}.tools{display:grid;grid-template-columns:1fr 1fr;gap:6px}.tool,.actions button{border:1px solid #2c3952;background:#182237;color:#9ca8c0;border-radius:6px;padding:8px 5px;text-align:left;cursor:pointer;transition:.15s}.tool:hover,.tool.active{border-color:#65d4b8;background:#203548;color:#eafff9}.tool i{font-style:normal;font-size:18px;display:block;color:#65d4b8;line-height:18px}.tool span{font-size:10px;font-weight:700;letter-spacing:1px}.tool small{display:block;font-size:8px;color:#718099;margin-top:2px}.section{border-top:1px solid #29364d;margin-top:14px;padding-top:12px}.row{display:flex;justify-content:space-between;font-size:9px;letter-spacing:1.2px;color:#8994aa}.row b{color:#e6ebf5;font-size:10px}input[type=range]{width:100%;accent-color:#6addbd;margin:9px 0 5px}.toggle{font-size:9px;color:#8691a6;letter-spacing:1px;display:flex;align-items:center;gap:7px}.toggle input{display:none}.toggle span{width:25px;height:14px;border-radius:10px;background:#344057;position:relative}.toggle span:after{content:'';position:absolute;width:10px;height:10px;left:2px;top:2px;border-radius:50%;background:#8893a8;transition:.15s}.toggle input:checked+span{background:#286957}.toggle input:checked+span:after{left:13px;background:#72e0c1}.hint{font-size:9px;line-height:23px;color:#7c889f}.key{border:1px solid #3a4963;border-radius:3px;padding:3px 5px;color:#c4ccdb;font-size:8px}.actions{display:flex;gap:6px;margin-top:14px}.actions button{font-size:10px;letter-spacing:1px;flex:1;text-align:center}.actions .primary{background:#2a806a;border-color:#62d9b7;color:#edfff9}.tip{position:absolute;bottom:13px;left:18px;color:#77849c;font-size:11px;background:#141c2cbb;padding:7px 10px;border-radius:5px}.workspace:after{content:'';position:absolute;inset:0;pointer-events:none;background:linear-gradient(120deg,#6de0bf08,transparent 40%)}footer{height:28px;display:flex;justify-content:space-between;align-items:end}.legend{display:inline-block;width:7px;height:7px;border-radius:50%;margin:0 5px 0 14px}.legend.rope{background:#f4bd62}.legend.cloth{background:#7ba7ff}.legend.blob{background:#e879ba}.legend.ball{background:#79d6ff}@media(max-width:700px){.shell{padding:8px}.panel{width:180px;right:8px;top:8px}.readouts{gap:8px}.brand h1{font-size:12px}.tip,footer{display:none}}
/* ── css: canvas ── */
canvas{touch-action:none}
</style>
</head>
<body>
<main class="shell"><header class="topbar"><div class="brand"><span class="brand-mark">◈</span><div><h1>VERLET ATELIER</h1><p>soft-body physics playground</p></div></div><div class="readouts"><div><b id="fps">60</b><span>FPS</span></div><div><b id="stepMs">0.0</b><span>STEP MS</span></div><div><b id="objectCount">0</b><span>OBJECTS</span></div></div></header><section class="workspace"><canvas id="canvas" width="1200" height="720"></canvas><aside class="panel"><div class="panel-head"><span>TOOLKIT</span><span class="live-dot">● LIVE</span></div><div class="tools"><button class="tool active" data-tool="grab"><i>✥</i><span>GRAB</span><small>drag points</small></button><button class="tool" data-tool="rope"><i>⌁</i><span>ROPE</span><small>draw chain</small></button><button class="tool" data-tool="cloth"><i>▦</i><span>CLOTH</span><small>drag rectangle</small></button><button class="tool" data-tool="blob"><i>◉</i><span>BLOB</span><small>spawn soft body</small></button><button class="tool" data-tool="ball"><i>●</i><span>BALL</span><small>drop a bouncer</small></button><button class="tool" data-tool="cut"><i>/</i><span>CUT</span><small>slice constraints</small></button></div><div class="section"><label class="row"><span>GRAVITY</span><b id="gravityVal">720</b></label><input id="gravity" type="range" min="0" max="1200" value="720" step="10"><label class="toggle"><input id="gravityOn" type="checkbox" checked=""><span></span> ENABLED</label></div><div class="section"><label class="row"><span>RELAXATION</span><b id="iterVal">6×</b></label><input id="iterations" type="range" min="2" max="10" value="6"><label class="row"><span>DAMPING</span><b id="dampVal">0.992</b></label><input id="damping" type="range" min="0.96" max="0.999" value="0.992" step="0.001"></div><div class="section hint"><span class="key">SPACE</span> pause <span class="key">← →</span> step <br><span class="key">SHIFT</span> pin / unpin <span class="key">RMB</span> cut</div><div class="actions"><button id="pause" class="primary">Ⅱ PAUSE</button><button id="reset">↺ RESET</button></div></aside><div class="tip" id="tip">Choose a tool, then drag on the canvas</div></section><footer><span><i class="legend rope"></i>ROPE <i class="legend cloth"></i>CLOTH <i class="legend blob"></i>SOFT BODY <i class="legend ball"></i>BALL</span><span>FIXED TIMESTEP 60HZ · CONSTRAINT SOLVER</span></footer></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 });
// VERLET ATELIER — fixed-step position based physics playground
const C = { dt: 1 / 60, gravity: 720, damping: .992, iterations: 6, floor: 670, tear: 22, wall: 16, maxPoints: 1200 };
const canvas = document.querySelector('#canvas'), ctx = canvas.getContext('2d');
const pts = [];
const links = [];
const objs = [];
let nextObj = 0;
let paused = false, acc = 0, last = performance.now(), grab = -1, grabX = 0, grabY = 0, grabVX = 0, grabVY = 0, dragLastX = 0, dragLastY = 0, tool = 'grab';
let cutStart = null, draft = null, fps = 60, stepMs = 0;
const palette = { rope: '#f5bd65', cloth: '#78a9ff', blob: '#ea78b8', ball: '#78d9ff' };
const $ = (s) => document.querySelector(s);
function addPoint(x, y, kind, obj, r = 3, pinned = false) { const p = { x, y, px: x, py: y, ox: x, oy: y, pinned, r, kind, obj }; pts.push(p); return pts.length - 1; }
function link(a, b, obj, rest) { const l = links.push({ a, b, obj, len: rest ?? dist(pts[a], pts[b]), rest: rest ?? dist(pts[a], pts[b]), alive: true }) - 1; objs[obj].links.push(l); return l; }
function dist(a, b) { return Math.hypot(a.x - b.x, a.y - b.y); }
function makeObj(kind, color = palette[kind]) { const o = { kind, points: [], links: [], color }; objs.push(o); return objs.length - 1; }
function rope(x, y, ex, ey) { const o = makeObj('rope'); const n = Math.max(6, Math.min(32, Math.round(Math.hypot(ex - x, ey - y) / 18))); for (let i = 0; i < n; i++) {
const t = i / (n - 1), id = addPoint(x + (ex - x) * t, y + (ey - y) * t, 'rope', o, 3, i === 0);
objs[o].points.push(id);
if (i)
link(id - 1, id, o);
} return o; }
function cloth(x, y, w, h) { const o = makeObj('cloth'); const cols = Math.max(4, Math.min(15, Math.round(w / 22))), rows = Math.max(3, Math.min(11, Math.round(h / 22))); objs[o].cols = cols; objs[o].rows = rows; for (let j = 0; j < rows; j++)
for (let i = 0; i < cols; i++) {
const id = addPoint(x + i * w / (cols - 1), y + j * h / (rows - 1), 'cloth', o, 2, j === 0 && (i === 0 || i === cols - 1));
objs[o].points.push(id);
} for (let j = 0; j < rows; j++)
for (let i = 0; i < cols; i++) {
const id = objs[o].points[j * cols + i];
if (i)
link(id - 1, id, o);
if (j)
link(objs[o].points[(j - 1) * cols + i], id, o);
if (i && j)
link(objs[o].points[(j - 1) * cols + i - 1], id, o, Math.hypot(w / (cols - 1), h / (rows - 1)));
if (i < cols - 1 && j)
link(objs[o].points[(j - 1) * cols + i + 1], id, o, Math.hypot(w / (cols - 1), h / (rows - 1)));
} return o; }
function blob(x, y) { const o = makeObj('blob'), n = 12, rad = 58, center = addPoint(x, y, 'blob', o, 5); objs[o].points.push(center); for (let i = 0; i < n; i++) {
const a = i * Math.PI * 2 / n, id = addPoint(x + Math.cos(a) * rad, y + Math.sin(a) * rad, 'blob', o, 4);
objs[o].points.push(id);
} for (let i = 0; i < n; i++) {
const id = objs[o].points[1 + i];
link(center, id, o, rad);
link(id, objs[o].points[1 + (i + 1) % n], o, 2 * rad * Math.sin(Math.PI / n));
} return o; }
function ball(x, y) { const o = makeObj('ball'), id = addPoint(x, y, 'ball', o, 14); objs[o].points.push(id); return o; }
function reset() { pts.length = 0; links.length = 0; objs.length = 0; rope(190, 105, 320, 360); cloth(430, 125, 250, 240); blob(805, 155); ball(990, 120); for (const p of pts) {
p.px = p.x;
p.py = p.y;
} }
function integrate() { const g = $('#gravityOn').checked ? Number(($('#gravity').value)) : 0; for (const p of pts) {
if (p.pinned) {
p.px = p.x;
p.py = p.y;
continue;
}
const vx = (p.x - p.px) * C.damping, vy = (p.y - p.py) * C.damping;
p.px = p.x;
p.py = p.y;
p.x += vx;
p.y += vy + g * C.dt * C.dt;
if (!Number.isFinite(p.x) || !Number.isFinite(p.y)) {
p.x = p.ox;
p.y = p.oy;
p.px = p.x;
p.py = p.y;
}
} }
function solve() {
for (let k = 0; k < C.iterations; k++) {
for (const l of links) {
if (!l.alive)
continue;
const a = pts[l.a], b = pts[l.b], dx = b.x - a.x, dy = b.y - a.y, d = Math.hypot(dx, dy) || .0001;
const err = (d - l.rest) / d * .48;
const ax = dx * err, ay = dy * err;
if (!a.pinned) {
a.x += ax;
a.y += ay;
}
if (!b.pinned) {
b.x -= ax;
b.y -= ay;
}
}
for (const p of pts) {
if (p.pinned)
continue;
const r = p.r;
if (p.x < r + 10)
p.x = r + 10;
if (p.x > canvas.width - r - 10)
p.x = canvas.width - r - 10;
if (p.y > C.floor - r) {
p.y = C.floor - r;
const vy = p.y - p.py;
if (vy > 0)
p.py = p.y + vy * .28;
p.px = p.x - (p.x - p.px) * .35;
}
if (p.y < r + 10)
p.y = r + 10; // static round bumper
const dx = p.x - 690, dy = p.y - 500, d = Math.hypot(dx, dy) || .001, rr = 72 + r;
if (d < rr) {
p.x = 690 + dx / d * rr;
p.y = 500 + dy / d * rr;
}
}
}
}
function step() { integrate(); solve(); for (const p of pts) {
p.ox = p.x;
p.oy = p.y;
} }
function strokeColor(l) { const a = pts[l.a], b = pts[l.b], ratio = dist(a, b) / l.rest; return ratio > 1.25 ? '#f06f78' : ratio > 1.08 ? '#f4bd65' : objs[l.obj].color; }
function render() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
const bg = ctx.createLinearGradient(0, 0, 0, canvas.height);
bg.addColorStop(0, '#101827');
bg.addColorStop(1, '#0e1520');
ctx.fillStyle = bg;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.strokeStyle = '#1c2b40';
ctx.lineWidth = 1;
for (let x = 0; x < canvas.width; x += 40) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, canvas.height);
ctx.stroke();
}
for (let y = 0; y < canvas.height; y += 40) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(canvas.width, y);
ctx.stroke();
}
ctx.fillStyle = '#161f2c';
ctx.fillRect(0, C.floor, canvas.width, canvas.height - C.floor);
ctx.strokeStyle = '#37506a';
ctx.beginPath();
ctx.moveTo(0, C.floor + .5);
ctx.lineTo(canvas.width, C.floor + .5);
ctx.stroke();
ctx.fillStyle = '#253348';
ctx.beginPath();
ctx.arc(690, 500, 72, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = '#49617c';
ctx.stroke();
for (const o of objs) {
if (o.kind === 'cloth') {
const c = o.cols, r = o.rows;
for (let j = 0; j < r - 1; j++)
for (let i = 0; i < c - 1; i++) {
const q = [pts[o.points[j * c + i]], pts[o.points[j * c + i + 1]], pts[o.points[(j + 1) * c + i + 1]], pts[o.points[(j + 1) * c + i]]];
ctx.fillStyle = 'rgba(89,143,255,.16)';
ctx.beginPath();
ctx.moveTo(q[0].x, q[0].y);
q.slice(1).forEach(p => ctx.lineTo(p.x, p.y));
ctx.closePath();
ctx.fill();
}
}
if (o.kind === 'blob') {
ctx.fillStyle = o.color + '32';
ctx.strokeStyle = o.color;
ctx.lineWidth = 2;
const q = o.points.slice(1).map(i => pts[i]);
ctx.beginPath();
ctx.moveTo(q[0].x, q[0].y);
q.forEach(p => ctx.lineTo(p.x, p.y));
ctx.closePath();
ctx.fill();
ctx.stroke();
}
}
for (const l of links) {
if (!l.alive)
continue;
const a = pts[l.a], b = pts[l.b];
ctx.strokeStyle = strokeColor(l);
ctx.lineWidth = objs[l.obj].kind === 'cloth' ? 1.1 : 2;
ctx.beginPath();
ctx.moveTo(a.x, a.y);
ctx.lineTo(b.x, b.y);
ctx.stroke();
}
for (const p of pts) {
if (p.kind === 'cloth')
continue;
ctx.fillStyle = palette[p.kind];
ctx.beginPath();
ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
ctx.fill();
if (p.pinned) {
ctx.strokeStyle = '#eafff7';
ctx.lineWidth = 1;
ctx.stroke();
}
}
if (grab >= 0) {
const p = pts[grab];
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(p.x, p.y, 12 + Math.sin(performance.now() / 100) * 2, 0, Math.PI * 2);
ctx.stroke();
}
if (draft) {
ctx.strokeStyle = '#73e0c0';
ctx.setLineDash([5, 5]);
ctx.beginPath();
ctx.moveTo(draft.x, draft.y);
ctx.lineTo(grabX, grabY);
ctx.stroke();
ctx.setLineDash([]);
}
if (cutStart) {
ctx.strokeStyle = '#f06f78';
ctx.setLineDash([4, 4]);
ctx.beginPath();
ctx.moveTo(cutStart.x, cutStart.y);
ctx.lineTo(grabX, grabY);
ctx.stroke();
ctx.setLineDash([]);
}
}
function pos(e) { const r = canvas.getBoundingClientRect(); return { x: (e.clientX - r.left) * canvas.width / r.width, y: (e.clientY - r.top) * canvas.height / r.height }; }
function nearest(x, y) { let best = -1, bd = 28; pts.forEach((p, i) => { const d = Math.hypot(p.x - x, p.y - y); if (d < bd) {
bd = d;
best = i;
} }); return best; }
function cut(a, b) { for (const l of links) {
if (!l.alive)
continue;
const p = pts[l.a], q = pts[l.b], d = segDist(a, b, p, q);
if (d < C.tear)
l.alive = false;
} }
function segDist(a, b, p, q) { const ux = b.x - a.x, uy = b.y - a.y, vx = q.x - p.x, vy = q.y - p.y; let t = ((p.x - a.x) * ux + (p.y - a.y) * uy) / (ux * ux + uy * uy || 1); t = Math.max(0, Math.min(1, t)); let s = ((a.x + t * ux - p.x) * vx + (a.y + t * uy - p.y) * vy) / (vx * vx + vy * vy || 1); s = Math.max(0, Math.min(1, s)); return Math.hypot(a.x + t * ux - (p.x + s * vx), a.y + t * uy - (p.y + s * vy)); }
function pointerDown(e) { const p = pos(e); grabX = p.x; grabY = p.y; if (e.button === 2 || tool === 'cut') {
cutStart = p;
canvas.setPointerCapture(e.pointerId);
return;
} if (tool === 'rope' || tool === 'cloth') {
draft = p;
canvas.setPointerCapture(e.pointerId);
return;
} const id = nearest(p.x, p.y); if (id >= 0) {
if (e.shiftKey) {
pts[id].pinned = !pts[id].pinned;
pts[id].px = pts[id].x;
pts[id].py = pts[id].y;
}
else {
grab = id;
dragLastX = p.x;
dragLastY = p.y;
canvas.setPointerCapture(e.pointerId);
}
}
else if (tool === 'blob')
blob(p.x, p.y);
else if (tool === 'ball')
ball(p.x, p.y); }
function pointerMove(e) { const p = pos(e); grabX = p.x; grabY = p.y; if (grab >= 0) {
grabVX = p.x - dragLastX;
grabVY = p.y - dragLastY;
pts[grab].x = p.x;
pts[grab].y = p.y;
pts[grab].px = p.x - grabVX * 2;
pts[grab].py = p.y - grabVY * 2;
dragLastX = p.x;
dragLastY = p.y;
} render(); }
function pointerUp(e) { const p = pos(e); if (cutStart) {
cut(cutStart, p);
cutStart = null;
}
else if (draft) {
if (tool === 'rope')
rope(draft.x, draft.y, p.x, p.y);
else
cloth(Math.min(draft.x, p.x), Math.min(draft.y, p.y), Math.abs(p.x - draft.x) || 160, Math.abs(p.y - draft.y) || 140);
draft = null;
}
else if (grab >= 0) {
pts[grab].px = pts[grab].x - grabVX * 3;
pts[grab].py = pts[grab].y - grabVY * 3;
grab = -1;
} canvas.releasePointerCapture(e.pointerId); }
function loop(now) { const frame = Math.min(.05, (now - last) / 1000); last = now; acc += frame; let n = 0, t = performance.now(); while (acc >= C.dt) {
if (!paused || n === 0 && false)
step();
acc -= C.dt;
n++;
} stepMs = performance.now() - t; fps = fps * .92 + (.08 / (frame || .016)); render(); $('#fps').textContent = Math.round(Math.min(60, 1 / frame)).toString(); $('#stepMs').textContent = stepMs.toFixed(2); $('#objectCount').textContent = objs.length.toString(); requestAnimationFrame(loop); }
function setup() { reset(); canvas.addEventListener('pointerdown', pointerDown); canvas.addEventListener('pointermove', pointerMove); canvas.addEventListener('pointerup', pointerUp); canvas.addEventListener('contextmenu', e => e.preventDefault()); document.querySelectorAll('.tool').forEach(b => b.addEventListener('click', () => { tool = b.getAttribute('data-tool'); document.querySelectorAll('.tool').forEach(x => x.classList.toggle('active', x === b)); $('#tip').textContent = tool === 'grab' ? 'Drag any point · Shift-click to pin' : tool === 'cut' ? 'Drag across links to tear' : 'Drag on canvas to place ' + tool; })); $('#gravity').addEventListener('input', () => $('#gravityVal').textContent = $('#gravity').value); $('#gravityOn').addEventListener('change', () => { }); $('#iterations').addEventListener('input', () => { C.iterations = Number($('#iterations').value); $('#iterVal').textContent = C.iterations + '×'; }); $('#damping').addEventListener('input', () => { C.damping = Number($('#damping').value); $('#dampVal').textContent = C.damping.toFixed(3); }); $('#pause').addEventListener('click', () => { paused = !paused; $('#pause').textContent = paused ? '▶ RESUME' : 'Ⅱ PAUSE'; }); $('#reset').addEventListener('click', reset); window.addEventListener('keydown', e => { if (e.code === 'Space') {
e.preventDefault();
paused = !paused;
$('#pause').textContent = paused ? '▶ RESUME' : 'Ⅱ PAUSE';
} if (paused && (e.code === 'ArrowRight' || e.code === 'ArrowLeft'))
step(); }); window.sim = { points: pts, constraints: links, objects: objs, step, reset, tear: cut, config: C }; requestAnimationFrame(loop); }
setup();
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"openai/gpt-5.6-luna-pro","provider":"openrouter","persona":"typescript","sessionId":"05303990-c7e4-4e36-a0fe-cb1c29fd7560","tokensIn":445195,"tokensOut":25505,"tokensTotal":470700,"turns":13,"toolCalls":18,"failedToolCalls":1,"timestamp":"2026-07-31T02:57:35.216Z"} -->