← Physics Playground results

Physics Playground

lfm2 24b a2b · basic

Initial view of Physics Playground

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"} -->