Physics Playground
GPT 5.6 Luna Pro · full

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 16,344 bytes · SHA-256 bdca635ec9e2
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Softbody Lab</title>
<style>
:root{--bg:#08111f;--panel:#101c2e;--line:#263a55;--muted:#8496af;--text:#e9f1ff;--cyan:#55e6ff;--pink:#ff668f;--gold:#ffc857;--green:#7ef0b4}
*{box-sizing:border-box}html,body{margin:0;width:100%;height:100%;overflow:hidden;background:var(--bg);font-family:Inter,ui-sans-serif,system-ui,sans-serif;color:var(--text)}
#app{position:relative;width:100%;height:100%}canvas{display:block;width:100%;height:100%;cursor:crosshair}
header{position:absolute;z-index:3;left:18px;top:16px;display:flex;align-items:center;gap:14px;pointer-events:none}h1{font-size:17px;letter-spacing:.1em;text-transform:uppercase;margin:0;font-weight:700}h1 span{color:var(--cyan)}.tag{font-size:11px;color:#8fa6c4;border:1px solid #29425f;padding:5px 8px;border-radius:20px;background:#0b1728aa}
.panel{position:absolute;z-index:4;background:#0d1929e8;border:1px solid #233953;border-radius:13px;box-shadow:0 14px 34px #0005;backdrop-filter:blur(10px)}#tools{top:58px;left:18px;padding:9px;display:flex;gap:7px;align-items:center;flex-wrap:wrap;max-width:470px}.tool,.action{font:inherit;color:#c7d7ee;background:#13253a;border:1px solid #294360;border-radius:8px;padding:8px 10px;font-size:12px;cursor:pointer;transition:.15s}.tool:hover,.action:hover{border-color:#5aa8c7;color:#fff}.tool.active{background:#123c4d;border-color:var(--cyan);color:#e7fbff;box-shadow:0 0 16px #55e6ff22}.sep{width:1px;height:25px;background:#28415c;margin:0 2px}.icon{font-size:15px}.controls{position:absolute;right:18px;top:16px;padding:10px 12px;display:flex;gap:12px;align-items:center}.control{display:flex;gap:7px;align-items:center;font-size:11px;color:var(--muted);white-space:nowrap}.control strong{color:#d9e8ff;font-weight:600}.control input{accent-color:var(--cyan);width:84px}.stats{position:absolute;right:18px;bottom:17px;padding:9px 12px;min-width:185px;font:11px ui-monospace,monospace;color:#8da5c1;line-height:1.7}.stats b{color:#eaf5ff;font-weight:500}.hint{position:absolute;left:20px;bottom:18px;color:#68809f;font-size:11px;pointer-events:none}.mode-note{color:var(--gold);font-size:11px;margin:0 3px}
@media(max-width:720px){.controls{top:auto;bottom:16px;right:14px}.stats{bottom:72px;right:14px}.hint{display:none}#tools{left:10px;top:52px;max-width:calc(100% - 20px)}}
</style></head>
<body><div id="app"><canvas id="canvas"></canvas>
<header><h1><span>SOFT</span>BODY / LAB</h1><div class="tag">VERLET PLAYGROUND</div></header>
<div id="tools" class="panel"><button class="tool active" data-mode="grab">✦ Grab</button><button class="tool" data-mode="rope">⌁ Rope</button><button class="tool" data-mode="cloth">▦ Cloth</button><button class="tool" data-mode="blob">◉ Blob</button><button class="tool" data-mode="ball">● Ball</button><button class="tool" data-mode="cut">✂ Cut</button><span class="sep"></span><button id="pause" class="action">Ⅱ Pause</button><button id="step" class="action">› Step</button><button id="reset" class="action">↺ Reset</button></div>
<div class="panel controls"><label class="control">Gravity <input id="gravity" type="range" min="0" max="1800" value="760"><strong id="gval">760</strong></label><label class="control">Relax <input id="iters" type="range" min="2" max="10" value="6"><strong id="ival">6</strong></label><label class="control"><input id="stress" type="checkbox" checked> stress</label></div>
<div class="panel stats"><div>FPS <b id="fps">60</b> · STEP <b id="stepms">0.00</b>ms</div><div>POINTS <b id="pcount">0</b> · LINKS <b id="lcount">0</b></div><div>MODE <b id="mval">GRAB</b></div></div><div class="hint">drag to interact · right-drag / ✂ to cut · shift-click pins</div>
</div>
<script>
/* ================================================================
SOFTBODY LAB — tunable constants and position-based dynamics
================================================================ */
const CFG={dt:1/60,maxFrame:.05,damping:.992,gravity:760,iterations:6,friction:.78,tearRadius:14,maxSpeed:1800,pointRadius:5};
const canvas=document.getElementById('canvas'),ctx=canvas.getContext('2d');let W=0,H=0,dpr=1;
function resize(){dpr=Math.min(devicePixelRatio||1,2);W=innerWidth;H=innerHeight;canvas.width=W*dpr;canvas.height=H*dpr;ctx.setTransform(dpr,0,0,dpr,0,0);world.floor=H-30;world.right=W-18}addEventListener('resize',resize);
const world={floor:600,right:1000,points:[],links:[],shapes:[],objects:[],time:0,paused:false,acc:0,grab:null,cutPath:[],mode:'grab',lastMouse:{x:0,y:0}};
let uid=1;
class Point{constructor(x,y,opt={}){this.x=x;this.y=y;this.px=x;this.py=y;this.pin=!!opt.pin;this.r=opt.r||CFG.pointRadius;this.kind=opt.kind||'mesh';this.color=opt.color||'#55e6ff';this.id=uid++;world.points.push(this)}get vx(){return (this.x-this.px)/CFG.dt}get vy(){return (this.y-this.py)/CFG.dt}}
class Link{constructor(a,b,opt={}){this.a=a;this.b=b;this.len=opt.len??Math.hypot(a.x-b.x,a.y-b.y);this.stiff=opt.stiff??1;this.kind=opt.kind||'line';this.color=opt.color||a.color;this.dead=false;world.links.push(this)}
}
function link(a,b,opt){return new Link(a,b,opt)}
function pin(p){p.pin=true;p.x=p.px;p.py=p.y}
function addShape(s){world.shapes.push(s)}
function reset(){world.points=[];world.links=[];world.objects=[];world.shapes=[];world.time=0;world.acc=0;world.grab=null;makeStatics();seedScene()}
function makeStatics(){addShape({type:'circle',x:.72*W,y:world.floor-105,r:73});addShape({type:'circle',x:.28*W,y:world.floor-62,r:38});addShape({type:'box',x:.49*W,y:world.floor-58,w:150,h:26});}
function seedScene(){spawnCloth(.16*W,120,8,5,38,32);spawnRope(.78*W,110,15,19);spawnBlob(.51*W,145,52,7);spawnBall(.88*W,180,14)}
function spawnRope(x,y,n=16,spacing=19){let arr=[];for(let i=0;i<n;i++)arr.push(new Point(x+i*spacing,y+i*spacing*.4,{color:'#ffc857'}));pin(arr[0]);for(let i=1;i<n;i++)link(arr[i-1],arr[i],{color:'#ffc857'});world.objects.push({type:'rope',points:arr,color:'#ffc857'});return arr}
function spawnCloth(x,y,cols=9,rows=6,dx=36,dy=30){let a=[];for(let j=0;j<rows;j++){a[j]=[];for(let i=0;i<cols;i++){let p=new Point(x+i*dx,y+j*dy,{color:'#55e6ff'});a[j][i]=p;if(j===0&&(i===0||i===cols-1))pin(p)}}for(let j=0;j<rows;j++)for(let i=0;i<cols;i++){if(i)link(a[j][i-1],a[j][i],{color:'#55e6ff'});if(j)link(a[j-1][i],a[j][i],{color:'#55e6ff'});if(i&&j)link(a[j-1][i-1],a[j][i],{color:'#55e6ff',stiff:.85,kind:'shear'});if(i<cols-1&&j)link(a[j-1][i+1],a[j][i],{color:'#55e6ff',stiff:.85,kind:'shear'})}world.objects.push({type:'cloth',grid:a,color:'#55e6ff'});return a.flat()}
function spawnBlob(cx,cy,r=55,n=8){let pts=[];for(let i=0;i<n;i++){let a=i*Math.PI*2/n;pts.push(new Point(cx+Math.cos(a)*r,cy+Math.sin(a)*r,{color:'#ff668f'}))}let center=new Point(cx,cy,{color:'#ff668f'});for(let i=0;i<n;i++){link(pts[i],pts[(i+1)%n],{color:'#ff668f'});link(center,pts[i],{color:'#ff668f',stiff:.76})}world.objects.push({type:'blob',points:pts,center,color:'#ff668f'});return pts}
function spawnBall(x,y,r=15){let p=new Point(x,y,{r,kind:'ball',color:'#7ef0b4'});world.objects.push({type:'ball',point:p,color:'#7ef0b4'});return p}
function clampPoint(p){p.x=Math.max(10+p.r,Math.min(world.right-p.r,p.x));p.y=Math.min(world.floor-p.r,p.y);if(p.y>=world.floor-p.r-0.01){let vy=p.y-p.py;if(vy>0)p.py=p.y+vy*CFG.friction;p.x+=(p.x-p.px)*.03}}
function solveLink(l){if(l.dead)return;let dx=l.b.x-l.a.x,dy=l.b.y-l.a.y,d=Math.hypot(dx,dy);if(!isFinite(d)||d<1e-8)return;let err=(d-l.len)/d*.5*l.stiff, ax=dx*err,ay=dy*err;if(!l.a.pin){l.a.x+=ax;l.a.y+=ay}if(!l.b.pin){l.b.x-=ax;l.b.y-=ay}}
function collide(p){for(const s of world.shapes){if(s.type==='circle'){let dx=p.x-s.x,dy=p.y-s.y,d=Math.hypot(dx,dy),min=s.r+p.r;if(d<min){if(d<1e-7){dx=1;dy=0;d=1}let nx=dx/d,ny=dy/d;p.x=s.x+nx*min;p.y=s.y+ny*min;let vx=p.x-p.px,vy=p.y-p.py;let vn=vx*nx+vy*ny;if(vn<0){vx-=vn*nx*(1+CFG.friction);vy-=vn*ny*(1+CFG.friction)}p.px=p.x-vx*.86;p.py=p.y-vy*.86}}}clampPoint(p)}
function physicsStep(){let g=+document.getElementById('gravity').value;CFG.gravity=g;CFG.iterations=+document.getElementById('iters').value;for(const p of world.points){if(p.pin)continue;let vx=(p.x-p.px)*CFG.damping,vy=(p.y-p.py)*CFG.damping;p.px=p.x;p.py=p.y;p.x+=Math.max(-CFG.maxSpeed*CFG.dt,Math.min(CFG.maxSpeed*CFG.dt,vx));p.y+=Math.max(-CFG.maxSpeed*CFG.dt,Math.min(CFG.maxSpeed*CFG.dt,vy+g*CFG.dt*CFG.dt));if(!isFinite(p.x)||!isFinite(p.y)){p.x=p.px=200;p.y=p.py=100}}for(let k=0;k<CFG.iterations;k++){for(const l of world.links)solveLink(l);for(const p of world.points)collide(p);if(world.grab) {world.grab.p.x=world.grab.x;world.grab.p.y=world.grab.y}}world.time+=CFG.dt}
function cutSegment(x1,y1,x2,y2){for(const l of world.links){if(l.dead)continue;let d=segDist(l.a.x,l.a.y,l.b.x,l.b.y,x1,y1,x2,y2);if(d<CFG.tearRadius){l.dead=true;burst((l.a.x+l.b.x)/2,(l.a.y+l.b.y)/2,l.color)}}}
function segDist(ax,ay,bx,by,cx,cy,dx,dy){let ux=bx-ax,uy=by-ay,vx=dx-cx,vy=dy-cy,w=cx-ax, z=cy-ay;let den=ux*ux+uy*uy;let t=den?(w*ux+z*uy)/den:0;t=Math.max(0,Math.min(1,t));let px=ax+t*ux,py=ay+t*uy;let q=px-cx,r=py-cy,vden=vx*vx+vy*vy,s=vden?(q*vx+r*vy)/vden:0;s=Math.max(0,Math.min(1,s));let ex=cx+s*vx,ey=cy+s*vy;return Math.hypot(px-ex,py-ey)}
const sparks=[];function burst(x,y,c){for(let i=0;i<8;i++)sparks.push({x,y,vx:(Math.random()-.5)*100,vy:(Math.random()-.8)*100,life:.45,c})}
function draw(){ctx.clearRect(0,0,W,H);let bg=ctx.createLinearGradient(0,0,0,H);bg.addColorStop(0,'#081321');bg.addColorStop(1,'#0b1727');ctx.fillStyle=bg;ctx.fillRect(0,0,W,H);drawGrid();drawStatics();let stress=document.getElementById('stress').checked;for(const l of world.links){if(l.dead)continue;let ratio=Math.hypot(l.a.x-l.b.x,l.a.y-l.b.y)/l.len;ctx.strokeStyle=stress?stressColor(ratio):l.color;ctx.globalAlpha=l.kind==='shear'?.42:.8;ctx.lineWidth=l.kind==='shear'?1:1.7;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 o of world.objects)drawObject(o);for(const p of world.points){if(p.kind==='mesh'&&world.objects.some(o=>o.type==='cloth'&&o.grid?.flat().includes(p)))continue;ctx.fillStyle=p.color;ctx.beginPath();ctx.arc(p.x,p.y,p.kind==='ball'?p.r:3,0,Math.PI*2);ctx.fill();if(p.pin){ctx.strokeStyle='#fff';ctx.lineWidth=1;ctx.stroke()}}if(world.grab){ctx.strokeStyle='#fff';ctx.globalAlpha=.8;ctx.lineWidth=2;ctx.beginPath();ctx.arc(world.grab.p.x,world.grab.p.y,13+Math.sin(world.time*8)*2,0,7);ctx.stroke();ctx.globalAlpha=1}for(let i=sparks.length-1;i>=0;i--){let s=sparks[i];s.life-=CFG.dt;s.x+=s.vx*CFG.dt;s.y+=s.vy*CFG.dt;s.vy+=220*CFG.dt;ctx.globalAlpha=Math.max(0,s.life*2);ctx.fillStyle=s.c;ctx.fillRect(s.x,s.y,2,2);if(s.life<=0)sparks.splice(i,1)}ctx.globalAlpha=1}
function stressColor(r){let t=Math.max(0,Math.min(1,(r-0.92)*5));return `rgb(${Math.round(90+165*t)},${Math.round(220-150*t)},${Math.round(135-75*t)})`}
function drawGrid(){ctx.strokeStyle='#ffffff07';ctx.lineWidth=1;let size=42;for(let x=0;x<W;x+=size){ctx.beginPath();ctx.moveTo(x,0);ctx.lineTo(x,H);ctx.stroke()}for(let y=0;y<H;y+=size){ctx.beginPath();ctx.moveTo(0,y);ctx.lineTo(W,y);ctx.stroke()}ctx.fillStyle='#0c2030';ctx.fillRect(0,world.floor,W,H-world.floor);ctx.strokeStyle='#335a70';ctx.beginPath();ctx.moveTo(0,world.floor);ctx.lineTo(W,world.floor);ctx.stroke()}
function drawStatics(){for(const s of world.shapes){ctx.fillStyle='#182d42';ctx.strokeStyle='#41627b';ctx.lineWidth=2;if(s.type==='circle'){ctx.beginPath();ctx.arc(s.x,s.y,s.r,0,7);ctx.fill();ctx.stroke();ctx.strokeStyle='#29465e';ctx.beginPath();ctx.arc(s.x-12,s.y-15,s.r*.65,3.7,5.2);ctx.stroke()}else{ctx.fillRect(s.x-s.w/2,s.y-s.h/2,s.w,s.h);ctx.strokeRect(s.x-s.w/2,s.y-s.h/2,s.w,s.h)}}}
function drawObject(o){if(o.type==='cloth'){let g=o.grid;ctx.fillStyle='#55e6ff18';ctx.beginPath();for(let j=0;j<g.length;j++){for(let i=0;i<g[j].length;i++){let p=g[j][i];if(i===0&&j===0)ctx.moveTo(p.x,p.y);else if(i===0)ctx.lineTo(p.x,p.y);else ctx.lineTo(p.x,p.y)}}for(let j=g.length-1;j>=0;j--)ctx.lineTo(g[j][g[j].length-1].x,g[j][g[j].length-1].y);ctx.closePath();ctx.fill()}if(o.type==='blob'){let pts=o.points;ctx.fillStyle='#ff668f22';ctx.strokeStyle='#ff668faa';ctx.lineWidth=2;ctx.beginPath();pts.forEach((p,i)=>i?ctx.lineTo(p.x,p.y):ctx.moveTo(p.x,p.y));ctx.closePath();ctx.fill();ctx.stroke()}if(o.type==='ball'){ctx.fillStyle='#7ef0b444';ctx.strokeStyle='#7ef0b4';ctx.lineWidth=2;ctx.beginPath();ctx.arc(o.point.x,o.point.y,o.point.r,0,7);ctx.fill();ctx.stroke()}}
let last=performance.now(),fps=60;function frame(now){let delta=Math.min(CFG.maxFrame,(now-last)/1000);last=now;fps=fps*.9+(1/Math.max(delta,.001))*.1;if(!world.paused){world.acc+=delta;while(world.acc>=CFG.dt){physicsStep();world.acc-=CFG.dt}}draw();document.getElementById('fps').textContent=Math.round(Math.min(99,fps));document.getElementById('stepms').textContent=(CFG.dt*1000).toFixed(2);document.getElementById('pcount').textContent=world.points.length;document.getElementById('lcount').textContent=world.links.filter(l=>!l.dead).length;requestAnimationFrame(frame)}
function pos(e){let r=canvas.getBoundingClientRect();return{x:e.clientX-r.left,y:e.clientY-r.top}}
function nearest(x,y){let best=null,bd=28;for(const p of world.points){let d=Math.hypot(p.x-x,p.y-y);if(d<bd){bd=d;best=p}}return best}
let drawing=null;canvas.addEventListener('pointerdown',e=>{let q=pos(e);canvas.setPointerCapture(e.pointerId);world.lastMouse=q;if(e.button===2||world.mode==='cut'){drawing=q;world.cutPath=[q];return}if(world.mode==='grab'){let p=nearest(q.x,q.y);if(p){if(e.shiftKey){p.pin=!p.pin;p.px=p.x;p.py=p.y}else world.grab={p,x:q.x,y:q.y,lastX:q.x,lastY:q.y}}else if(e.shiftKey){spawnBall(q.x,q.y,13)}}else if(world.mode==='rope'){drawing={start:q,last:q,pts:[]};drawing.pts=spawnRope(q.x,q.y,1,20);world.objects[world.objects.length-1].points=drawing.pts}else if(world.mode==='cloth'){spawnCloth(q.x-130,q.y-70,8,5,36,28)}else if(world.mode==='blob'){spawnBlob(q.x,q.y,48,8)}else if(world.mode==='ball'){spawnBall(q.x,q.y,15)}});
canvas.addEventListener('pointermove',e=>{let q=pos(e);if(world.grab){let g=world.grab;g.lastX=g.x;g.lastY=g.y;g.x=q.x;g.y=q.y;g.p.x=q.x;g.p.y=q.y;g.p.px=q.x-(q.x-g.lastX)*.35;g.p.py=q.y-(q.y-g.lastY)*.35}if(drawing){if(world.mode==='cut'||e.buttons===2){let a=world.cutPath[world.cutPath.length-1];cutSegment(a.x,a.y,q.x,q.y);world.cutPath.push(q)}else if(world.mode==='rope'){let d=Math.hypot(q.x-drawing.last.x,q.y-drawing.last.y);if(d>14){let p=new Point(q.x,q.y,{color:'#ffc857'});let prev=drawing.pts[drawing.pts.length-1];link(prev,p,{color:'#ffc857'});drawing.pts.push(p);drawing.last=q}}world.lastMouse=q}});
canvas.addEventListener('pointerup',e=>{if(world.grab){world.grab=null}drawing=null;world.cutPath=[]});canvas.addEventListener('contextmenu',e=>e.preventDefault());
document.querySelectorAll('.tool').forEach(b=>b.onclick=()=>{document.querySelectorAll('.tool').forEach(x=>x.classList.remove('active'));b.classList.add('active');world.mode=b.dataset.mode;document.getElementById('mval').textContent=world.mode.toUpperCase()});document.getElementById('gravity').oninput=e=>document.getElementById('gval').textContent=e.target.value;document.getElementById('iters').oninput=e=>document.getElementById('ival').textContent=e.target.value;document.getElementById('pause').onclick=()=>{world.paused=!world.paused;document.getElementById('pause').textContent=world.paused?'▶ Play':'Ⅱ Pause'};document.getElementById('step').onclick=()=>{if(world.paused)physicsStep()};document.getElementById('reset').onclick=reset;addEventListener('keydown',e=>{if(e.code==='Space'){e.preventDefault();document.getElementById('pause').click()}if(e.code==='ArrowRight'&&world.paused)physicsStep()});
resize();reset();window.sim={world,get points(){return world.points},get links(){return world.links},step:physicsStep,reset,spawnRope,spawnCloth,spawnBlob,spawnBall,tear:(l)=>{if(l)l.dead=true},get kineticEnergy(){return world.points.reduce((s,p)=>s+(p.vx*p.vx+p.vy*p.vy),0)}};requestAnimationFrame(frame);
</script></body></html>
<!-- agent-meta {"model":"openai/gpt-5.6-luna-pro","provider":"openrouter","persona":"full","sessionId":"bb5042bb-6654-42d2-80cc-3e7dcc331902","tokensIn":385177,"tokensOut":25401,"tokensTotal":410578,"turns":10,"toolCalls":9,"failedToolCalls":0,"timestamp":"2026-07-31T00:02:20.341Z"} -->