Physics Playground
lfm2 24b a2b · basic

Browser check: Artifact runtime error. This is a technical check, not a quality review.
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,623 bytes · SHA-256 88d03d4a6324
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>2D Soft-Body Physics Playground</title>
<style>
body { margin:0; background:#0a0f1a; color:#fff; font-family:sans-serif; }
canvas { display:block; background:#111; }
#toolbar { position:fixed; top:10px; left:10px; z-index:10; }
#controls { display:flex; gap:12px; margin-bottom:8px; }
button, input[type=range] { background:#222; border:1px solid #444; padding:4px 8px; color:#eee; }
#fps, #time { font-size:0.9em; background:#111; padding:2px 6px; border-radius:3px; }
</style>
</head>
<body>
<canvas id="c"></canvas>
<div id="toolbar">
<div id="controls">
<button id="pauseBtn">Pause</button>
<button id="stepBtn">Step</button>
<button id="resetBtn">Reset</button>
<label>Gravity: <input type="range" id="gravity" min="0" max="2" step="0.01" value="1"></label>
<label>Damp: <input type="range" id="damp" min="0" max="0.99" step="0.001" value="0.98"></label>
<label>Iter: <input type="range" id="iter" min="3" max="8" step="1" value="5"></label>
<label>Tear Radius: <input type="range" id="tearR" min="2" max="12" step="1" value="6"></label>
</div>
<div id="stats">
<span id="fps">FPS: 0</span> | <span id="time">Time: 0</span>
</div>
</div>
<script>
/* ==== CONFIGURATION ====
* All physics constants at top for easy tuning
* Timestep: 1/60 s, fixed accumulator
* Gravity: 1.0 m/s² down
* Damping: 0.98
* Iterations per step: 5 (relaxation)
* Tear radius: 6 px
*/
const CONFIG = {
gravity: 1.0,
damping: 0.98,
iterations: 5,
timestep: 1/60,
maxDelta: 0.05,
tearRadius: 6,
floorY: 320,
wallLeft: 30, wallRight: 970
};
let sim;
let running = true;
/* ==== CANVAS SETUP ====
* 2D world with walls, floor, and static shapes
*/
const c = document.getElementById('c');
const ctx = c.getContext('2d');
const W = c.width = 1000, H = c.height = 600;
const WALL_W = 30, WALL_H = 60;
function drawFloor() {
ctx.fillStyle = '#222';
ctx.fillRect(WALL_LEFT, WALL_Y + H - WALL_H, W - WALL_LEFT*2, WALL_H);
}
function drawWalls() {
ctx.strokeStyle = '#444';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(WALL_LEFT, WALL_Y);
ctx.lineTo(WALL_LEFT, H - WALL_H);
ctx.lineTo(WALL_RIGHT, H - WALL_H);
ctx.lineTo(WALL_RIGHT, WALL_Y);
ctx.closePath();
ctx.stroke();
}
function drawStaticCircle(x, y, r, col) {
ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI*2); ctx.fillStyle = col; ctx.fill();
}
function drawStaticBox(x, y, w, h, col) {
ctx.fillStyle = col; ctx.fillRect(x, y, w, h);
}
/* ==== OBJECT MODELS ====
* Points, ropes, cloths, soft bodies
*/
class Point {
constructor(x, y, col='white') { this.x = x; this.y = y; this.vx = 0; this.vy = 0; this.px = x; this.py = y; this.col = col; this.fixed = false; this.prevX = x; this.prevY = y; }
update(dt) {
if (this.fixed) { this.prevX = this.x; this.prevY = this.y; return; }
const ax = this.x - this.prevX, ay = this.y - this.prevY;
const vx = ax * CONFIG.damping + this.vx, vy = ay * CONFIG.damping + this.vy;
this.vx = vx; this.vy = vy;
this.x += vx * dt; this.y += vy * dt;
this.prevX = this.x - vx * dt; this.prevY = this.y - vy * dt;
}
}
class Rope {
constructor(points, col='white') {
this.points = points.map(p => Object.assign({}, p));
this.col = col; this.constraints = [];
this.initConstraints();
}
initConstraints() {
for (let i=0;i<this.points.length-1;i++) {
const a = this.points[i], b = this.points[i+1];
const dx = b.x-a.x, dy = b.y-a.y, d = Math.hypot(dx,dy);
if (d===0) return;
this.constraints.push({a,b,dx,dy,d:0});
}
}
update(dt) {
for(let c of this.constraints) {
const p1 = c.a, p2 = c.b;
const dx = p2.x-p1.x, dy = p2.y-p1.y, d = Math.hypot(dx,dy);
if (d<=0.01) {c.d = 0; continue; }
const diff = d - CONFIG.tearRadius;
const f = diff/d * c.d * 0.5;
p1.x -= dx*f; p1.y -= dy*f;
p2.x += dx*f; p2.y += dy*f;
c.d = diff;
}
for(let i=0;i<this.points.length;i++) {
this.points[i].update(dt);
}
}
}
class Cloth {
constructor(pts, col='white', pinPattern='corners') {
this.pts = pts.map(p=>Object.assign({},p));
this.col = col; this.pins = [];
this.constraints = [];
this.initPins(pinPattern);
this.initEdges();
}
initPins(pattern) {
// pins on top edge
if (pattern==='corners') { this.pins = [0,this.pts.length-1]; }
else { this.pins = Array.from({length:this.pts.length}, (_,i)=>i); }
}
initEdges() {
// horizontal edges
for(let i=0;i<this.pts.length-1;i++) {
const a = this.pts[i], b = this.pts[i+1];
const dx = b.x-a.x, dy = b.y-a.y, d = Math.hypot(dx,dy);
if (d===0) continue;
this.constraints.push({a,b,dx,dy,d:0});
}
}
update(dt) {
for(let c of this.constraints) {
const p1 = c.a, p2 = c.b;
const dx = p2.x-p1.x, dy = p2.y-p1.y, d = Math.hypot(dx,dy);
if (d<=0.01) {c.d=0; continue;}
const diff = d - CONFIG.tearRadius;
const f = diff/d * c.d * 0.5;
p1.x -= dx*f; p1.y -= dy*f;
p2.x += dx*f; p2.y += dy*f;
c.d = diff;
}
for(let i=0;i<this.pts.length;i++) this.pts[i].update(dt);
}
}
class SoftBody {
constructor(center, radius, col='white') {
this.pts = [];
let r = radius;
for (let i=0;i<8;i++) {
const ang = Math.PI*2*i/8;
this.pts.push({x: center.x+Math.cos(ang)*r, y: center.y+Math.sin(ang)*r, vx:0, vy:0});
}
this.col = col; this.fixed = false; this.pins=[];
this.edges = [];
for (let i=0;i<4;i++) {
this.edges.push({a:i,i+1});
}
this.braces = [];
}
update(dt) {
for(let e of this.edges) {
const a = this.pts[e.a], b = this.pts[e.b];
const dx = b.x-a.x, dy = b.y-a.y, d = Math.hypot(dx,dy);
if (d===0) continue;
const midX = (a.x+b.x)/2, midY = (a.y+b.y)/2;
this.edges[e] = {a,b,dx,dy,d:Math.hypot(a.vx-a.vx+dx,a.vy-a.vy+dy)/dt};
}
for(let i=0;i<this.pts.length;i++) {
this.pts[i].update(dt);
}
}
}
class Ball {
constructor(x, y, r, col='white') {
this.x = x; this.y = y; this.r = r; this.vx=0; this.vy=0; this.col=col; this.fixed=false;
}
update(dt) {
if (this.fixed) { return; }
this.x += this.vx*dt; this.y += this.vy*dt;
}
}
/* ==== PHYSICS ENGINE ====
* Verlet integration, constraint relaxation
*/
function createSim() {
const points = [];
let ropes=[], cloths=[], softBodies=[], balls=[];
// floor & wall objects
const floor = {y: H-WALL_Y, h: WALL_H, col:'#222'};
const wall = {x:WALL_LEFT, y:0, w:H, h:WALL_H, col:'#444'};
// static circle
const statCircle = {x:W/2, y:20, r:25, col:'#ff6'};
// helper to add point
function addPoint(x,y,col) {
points.push(new Point(x,y,col));
}
// rope tool
let ropePoints=[];
let ropeStart=null;
// cloth tool
let clothPoints=[];
let clothTopPins=[];
// soft body tool
let sbCenter={x:W/2, y:W-80, r:30, col:'#ff0'};
// ball tool
let ball = new Ball(W/2, H-w-ball.r-10, ball.r, '#f00');
// add static floor/walls/circle
drawFloor(); drawWalls(); drawStaticCircle(statCircle.x, statCircle.y, statCircle.r, statCircle.col);
// helper to step physics
function step(dt) {
// fixed timestep accumulator
static accum = 0;
accum += dt;
if (accum>CONFIG.timestep) { accum-=CONFIG.timestep; }
// gravity
for (let p of points) if (!p.fixed) { p.vy += CONFIG.gravity*dt; }
// Verlet integration
for (let p of points) {
if (!p.fixed) {
const ax = (p.x - p.prevX)/CONFIG.timestep, ay = (p.y - p.prevY)/CONFIG.timestep;
p.x += p.vx + ax*dt*CONFIG.damping + CONFIG.damping*ax*dt;
p.y += p.vy + ay*dt*CONFIG.damage + CONFIG.damage*ay*dt;
p.prevX = p.x - p.vx*dt - ax*dt*CONFIG.damping;
p.prevY = p.y - p.vy*dt - ay*dt*CONFIG.damage;
}
}
// constraints relaxation
for (let n=0;n<CONFIG.iterations;n++) {
for (let r of ropes) r.update(dt);
for (let c of cloths) c.update(dt);
for (let sb of softBodies) sb.update(dt);
}
// clamp points
for (let p of points) { if (p.y>H-30) p.y=H-30; if (p.y<30) p.y=30; }
// draw
ctx.clearRect(0,0,c.width,c.height);
drawFloor(); drawWalls(); drawStaticCircle(statCircle.x, statCircle.y, statCircle.r, statCircle.col);
for (let p of points) ctx.beginPath(); else if (!p.fixed) { ctx.arc(p.x,p.y,p.r||5,'0',Math.PI*2); ctx.fillStyle=p.col; ctx.fill(); }
for (let r of ropes) {
ctx.strokeStyle='#66f'; ctx.lineWidth=3;
ctx.beginPath();
r.points.forEach((pt,idx)=>{
if (idx==0) ctx.moveTo(pt.x,pt.y); else ctx.lineTo(pt.x,pt.y);
});
ctx.stroke();
}
for (let c of cloths) {
ctx.fillStyle='#66f'; ctx.beginPath();
for (let i=0;i<c.points.length-1;i++) {
const a = c.points[i], b = c.points[i+1];
ctx.moveTo(a.x,a.y); ctx.lineTo(b.x,b.y);
}
ctx.fill();
}
for (let sb of softBodies) {
ctx.beginPath(); ctx.arc(sb.center.x, sb.center.y, sb.r, 0, Math.PI*2); ctx.fillStyle=sb.col; ctx.fill();
}
for (let b of balls) {
ctx.beginPath(); ctx.arc(b.x,b.y,b.r,'0',Math.PI*2); ctx.fillStyle=b.col; ctx.fill();
}
// FPS
let fps = Math.round(1/dt);
document.getElementById('fps').textContent='FPS: '+fps;
document.getElementById('time').textContent='Time: '+Math.round(dt*1000)+' ms';
}
// interaction helpers
function pickPoint(x,y) {
let best = null, d=Infinity;
for (let p of points) { const d2=(p.x-x)**2+(p.y-y)**2; if (d2<d && d2<CONFIG.tearRadius*CONFIG.tearRadius) {d=d2; best=p;} }
return best;
}
function getMouse() {
const mx = c.getBoundingClientRect().left + c.mouseX;
const my = c.getBoundingClientRect().top + c.mouseY;
return {x:mx, y:my};
}
// rope tool
c.addEventListener('mousedown', e=>{
if (e.button===0) {
const m = getMouse();
let p = pickPoint(m.x,m.y);
if (!p) { ropeStart=p; return; }
// start new rope
ropePoints.push(p); ropeStart=p;
} else if (e.button===2) {
// grab point
const m = getMouse();
let p = pickPoint(m.x,m.y);
if (p) {
// pin or drag
if (ropePoints.length) {
// pin
ropePoints.forEach(pp=>pp.fixed=true); ropePoints=[]; ropeStart=p;
} else {
// grab and move
ropePoints.push(p); ropeStart=p;
}
}
} else if (e.button===3) {
// right drag cut
const m = getMouse();
let p = pickPoint(m.x,m.y);
if (p && ropePoints.length) {
for (let i=ropePoints.length-1;i>=0;i--) {
if (Math.hypot(ropePoints[i].x-p.x,ropePoints[i].y-p.y)<CONFIG.tearRadius) {
// cut
ropePoints.splice(i,1);
// split rope into two
const a = ropePoints.slice(0,i+1).map(x=>Object.assign({},x));
const b = ropePoints.slice(i+1).map(x=>Object.assign({},x));
// rebuild
ropePoints = a; ropeStart=a[a.length-1];
ropePoints.push(...b); ropePoints.push(ropeStart);
}
}
}
}
});
c.addEventListener('mousemove', e=>{
if (e.buttons===0) return;
const m = getMouse();
if (ropeStart) {
// drag rope
for (let i=0;i<ropePoints.length;i++) {
ropePoints[i].x = m.x - Math.cos(ropePoints[i].y-m.y)*CONFIG.tearRadius;
ropePoints[i].y = m.y - Math.sin(ropePoints[i].y-m.y)*CONFIG.tearRadius;
}
ropeStart=pickPoint(m.x,m.y);
} else if (ropePoints.length) {
// grab
const p = pickPoint(m.x,m.y);
if (p) {
ropeStart = p;
ropePoints.push(p);
}
}
});
c.addEventListener('mouseup', e=>{
if (e.button===0 && ropeStart) {
// release
ropeStart.fixed=false; ropeStart=null;
ropePoints.forEach(p=>p.fixed=false);
} else if (ropePoints.length) {
// pin top corners of cloth
const m = getMouse();
let pts = clothTopPins.map(i=>cloth.pts[i]);
if (Math.hypot(pts[0].x-m.x,pts[0].y-m.y)<CONFIG.tearRadius) clothTopPins[0]=pts[0]; // pin
if (Math.hypot(pts[1].x-m.x,pts[1].y-m.y)<CONFIG.tearRadius) clothTopPins[1]=pts[1]; // pin
}
});
// step button
document.getElementById('stepBtn').onclick = ()=>{ if (running) step(CONFIG.timestep); };
// reset
document.getElementById('resetBtn').onclick = ()=>{
points.length=0; ropes=[]; cloths=[]; softBodies=[]; balls=[new Ball(W/2,H-ball.r-10,ball.r,'#f00')];
// spawn some objects
for (let i=0;i<4;i++) addPoint(W/2+Math.cos(i*Math.PI/2)*120,W/2+Math.sin(i*Math.PI/2)*120, '#fff');
// rope
addPoint(W/2-80,W/2, '#66f'); // start rope
// cloth top pins
for (let i=0;i<4;i++) clothTopPins.push(i);
// soft body
softBodies.push(new SoftBody({x:W/2, y:W/2-80, r:30}, '#ff0'));
// ball
balls.push(new Ball(W/2+40, H-ball.r-10, ball.r,'#f00'));
};
// pause toggle
document.getElementById('pauseBtn').onclick = ()=>{ running=!running; };
// gravity slider
document.getElementById('gravity').oninput = e=>CONFIG.gravity=+e.target.value;
// damping slider
document.getElementById('damp').oninput = e=>CONFIG.damping=+e.target.value;
// iterations slider
document.getElementById('iter').oninput = e=>CONFIG.iterations=+e.target.value;
// tear radius
document.getElementById('tearR').oninput = e=>CONFIG.tearRadius=+e.target.value;
// init points
for (let i=0;i<8;i++) addPoint(W/2+Math.cos(i*Math.PI/4)*90,W/2+Math.sin(i*Math.PI/4)*90, '#fff');
// init cloth top pins
clothTopPins.push(0,1,2,3);
// start loop
function loop() {
if (running) step(CONFIG.timestep);
requestAnimationFrame(loop);
}
loop();
}
// expose for debugging
window.sim = sim;
window.CONFIG = CONFIG;
// init initial scene
sim = createSim();
</script>
</body>
</html>
<!-- agent-meta {"model":"liquid/lfm2-24b-a2b","provider":"lmstudio","persona":"basic","sessionId":"198053f5-c029-4dfe-a8ea-930d5bc36d4b","tokensIn":1220,"tokensOut":5088,"tokensTotal":6308,"cost":0,"turns":1,"toolCalls":0,"failedToolCalls":0,"timestamp":"2026-08-01T04:51:04.206Z"} -->