Physics Playground
ling 3.0 tiny:free · 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 23,843 bytes · SHA-256 95e85d871d14
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>2D Soft-Body Physics Playground</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{background:#0d0d0f;overflow:hidden;font-family:'Segoe UI',sans-serif;user-select:none}
#toolbar{position:fixed;top:0;left:0;right:0;background:rgba(10,10,12,0.92);border-bottom:1px solid rgba(255,255,255,0.08);z-index:10;display:flex;align-items:center;gap:8px;padding:6px 12px;flex-wrap:wrap;backdrop-filter:blur(10px)}
.toolbar label{color:#8899aa;font-size:11px;display:flex;align-items:center;gap:3px}
.toolbar input[type=range]{width:70px;accent-color:#FF8C00;cursor:pointer}
.toolbar select{background:#1a1a1e;color:#ccddee;border:1px solid #333;border-radius:4px;padding:2px 4px;font-size:11px;cursor:pointer}
.toolbar button{background:#1e1e24;color:#ccddee;border:1px solid #333;border-radius:4px;padding:4px 10px;font-size:11px;cursor:pointer;transition:background .15s}
.toolbar button:hover{background:#2a2a32}
.toolbar button.active{background:#2a2a3a;color:#FF8C00;border-color:#FF8C00}
.toolbar button.danger{background:#3a1a1a;color:#ff6666;border-color:#551111}
.toolbar button.danger:hover{background:#551111}
.toolbar button.green{background:#1a3a1a;color:#66ff66;border-color:#2a5a2a}
.toolbar button.green:hover{background:#2a5a2a}
#canvasWrap{position:fixed;top:0;left:0;width:100vw;height:100vh}
canvas{display:block;width:100%;height:100%}
#fps{position:fixed;bottom:8px;left:8px;color:#556677;font-size:12px;background:rgba(0,0,0,0.5);padding:3px 8px;border-radius:4px;pointer-events:none;z-index:10}
#status{position:fixed;bottom:8px;right:8px;color:#556677;font-size:11px;background:rgba(0,0,0,0.5);padding:3px 8px;border-radius:4px;pointer-events:none;z-index:10}
</style>
</head>
<body>
<div id="toolbar">
<label>Gravity <input type="range" id="gSlider" min="0" max="20" step="0.1" value="980"></label>
<label>Iter. <input type="range" id="iSlider" min="2" max="8" step="1" value="4"></label>
<label>Damping <input type="range" id="dSlider" min="0.9" max="1" step="0.001" value="0.995"></label>
<button id="btnPlay" class="active">Play</button>
<button id="btnStep">Step</button>
<button id="btnReset">Reset</button>
<button id="btnSpawnRope">Rope</button>
<button id="btnSpawnCloth">Cloth</button>
<button id="btnSpawnSoft">Soft Body</button>
<button id="btnSpawnBall">Ball</button>
<button id="btnTear" class="danger">Tear</button>
<button id="btnPin" class="green">Pin</button>
</div>
<div id="canvasWrap"><canvas id="c"></canvas></div>
<div id="fps">-- FPS</div>
<div id="status">Ready</div>
<script>
// ============================================================
// 2D Soft-Body Physics Playground
// Verlet integration with constraint relaxation
// ============================================================
// ---- Configuration ----
const CFG = {
gravity: 980,
damping: 0.995,
iterations: 4,
timestep: 1 / 60,
maxDelta: 1 / 10,
maxVelocity: 600,
wallMargin: 50,
grabRadius: 14,
pinRadius: 10,
tearRadius: 25,
staticCircle: { x: 600, y: 400, radius: 130 },
wallColor: '#2a2a30',
objColors: {
rope: '#FF8C00',
cloth: '#00BFFF',
softBody: '#FF3366',
ball: '#00FF88',
},
pinColor: '#888899',
constraintColor: '#666688',
meshFillAlpha: 0.15,
meshEdgeAlpha: 0.6,
trailAlpha: 0.12,
};
// ---- Canvas ----
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
let W = 0, H = 0;
function resize() {
const wrap = document.getElementById('canvasWrap');
const rect = wrap.getBoundingClientRect();
W = rect.width; H = rect.height;
canvas.width = W; canvas.height = H;
}
window.addEventListener('resize', resize);
resize();
// ---- Simulation State ----
const sim = {
points: [],
constraints: [],
objects: [],
mouse: { x: -999, y: -999, down: false, rightDown: false },
grabbed: null,
dragging: null,
paused: false,
frameCount: 0,
fps: 0,
fpsTimer: 0,
accumulator: 0,
lastTime: 0,
staticShapes: [],
particles: [],
staticCircle: { x: 600, y: 400, radius: 130, mass: 0 },
};
// ---- Utility ----
function dist(x1, y1, x2, y2) { return Math.sqrt((x1-x2)**2 + (y1-y2)**2); }
function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
function rand(a, b) { return a + Math.random() * (b - a); }
// ---- Point ----
let _pid = 0;
class Point {
constructor(x, y, pinned) {
this.x = x; this.y = y;
this.xPrev = x; this.yPrev = y;
this.pinned = pinned; this.id = _pid++;
this.meshIdx = -1;
}
velocity() { return { x: (this.x - this.xPrev), y: (this.y - this.yPrev) }; }
setFrom(p) { this.x = p.x; this.y = p.y; this.xPrev = p.x; this.yPrev = p.y; }
isPinned() { return this.pinned; }
}
// ---- Constraint ----
class Constraint {
constructor(a, b, restLength) {
this.a = a; this.b = b; this.restLength = restLength;
this.active = true;
this.removed = false;
}
get midpoint() {
return { x: (this.a.x + this.b.x) / 2, y: (this.a.y + this.b.y) / 2 };
}
get length() { return dist(this.a.x, this.a.y, this.b.x, this.b.y); }
get stretch() { return this.length / Math.max(1, this.restLength); }
}
// ---- Object Factory ----
function createRope(startX, startY, numPoints, restLength) {
const pts = [];
const conns = [];
let x = startX, y = startY;
for (let i = 0; i < numPoints; i++) {
pts.push(new Point(x, y, i === 0));
}
for (let i = 0; i < numPoints - 1; i++) {
const r = restLength || 30;
const dx = pts[i+1].x - pts[i].x;
const dy = pts[i+1].y - pts[i].y;
conns.push(new Constraint(pts[i], pts[i+1], r));
}
// For the first point, it's pinned
return { type: 'rope', points: pts, constraints: conns, pinned: [true, ...Array(numPoints-1).fill(false)] };
}
function createCloth(width, height, restLength, pinPattern) {
// pinPattern: 'corners', 'edge'
const pts = [];
const conns = [];
const pinMap = [];
const margin = 40;
const spacing = restLength || 35;
const cx = 300, cy = 400;
for (let j = 0; j <= height; j++) {
for (let i = 0; i <= width; i++) {
const x = cx - width * spacing / 2 + i * spacing;
const y = cy - height * spacing / 2 + j * spacing;
const pinned = (j === 0 && pinPattern === 'corners') ||
(j === 0 && pinPattern === 'edge') ||
(pinPattern === 'full' && j === 0);
pts.push(new Point(x, y, pinned));
pinMap.push(pinned);
}
}
// Add constraints
for (let j = 0; j <= height; j++) {
for (let i = 0; i < width; i++) {
const idx = j * (width + 1) + i;
// horizontal
if (i < width) conns.push(new Constraint(pts[idx], pts[idx + 1], spacing));
// vertical
if (j < height) conns.push(new Constraint(pts[idx], pts[idx + width + 1], spacing));
// shear
if (i < width && j < height) conns.push(new Constraint(pts[idx], pts[idx + width + 1], spacing));
}
}
return { type: 'cloth', points: pts, constraints: conns, pinMap: pinMap };
}
function createSoftBody(cx, cy, radius, numPoints) {
const pts = [];
const conns = [];
const r = radius || 60;
const num = numPoints || 64;
for (let i = 0; i < num; i++) {
const angle = (i / num) * Math.PI * 2;
const x = cx + Math.cos(angle) * radius;
const y = cy + Math.sin(angle) * radius;
pts.push(new Point(x, y, i === 0));
}
// Edge constraints
for (let i = 0; i < num; i++) {
const next = (i + 1) % num;
conns.push(new Constraint(pts[i], pts[next], radius));
}
// Internal brace constraints (diagonals for stability)
const half = Math.floor(num / 2);
for (let i = 0; i < half; i++) {
const next = (i + half) % num;
conns.push(new Constraint(pts[i], pts[next], radius));
}
return { type: 'softBody', points: pts, constraints: conns, pinned: Array(num).fill(false) };
}
function createBall(x, y) {
return { type: 'ball', points: [new Point(x, y, false)], constraints: [], pinned: [] };
}
// ---- Static Shapes ----
function createStaticCircle(cx, cy, r) {
return { type: 'circle', cx, cy, radius: r, mass: 0 };
}
// ---- Physics Engine ----
function verletStep(dt, damping) {
for (const p of sim.points) {
if (p.pinned) continue;
const vx = (p.x - p.xPrev) * damping;
const vy = (p.y - p.yPrev) * damping;
p.x += vx + CFG.gravity * dt * dt;
p.y += vy + CFG.gravity * dt * dt;
p.xPrev = p.x;
p.yPrev = p.y;
}
}
function solveConstraints(iterations) {
for (let iter = 0; iter < iterations; iter++) {
for (const c of sim.constraints) {
if (!c.active) continue;
const p1 = c.a, p2 = c.b;
const dx = p2.x - p1.x;
const dy = p2.y - p1.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 1e-6) continue;
const diff = (c.restLength - dist) / dist;
const offset = diff * 0.5;
if (!p1.pinned) { p1.x += dx * offset; p1.y += dy * offset; }
if (!p2.pinned) { p2.x -= dx * offset; p2.y -= dy * offset; }
}
}
}
function clampPositions() {
for (const p of sim.points) {
if (p.pinned) continue;
p.x = clamp(p.x, CFG.wallMargin, W - CFG.wallMargin);
p.y = clamp(p.y, CFG.wallMargin, H - CFG.wallMargin);
}
}
function resolveCollisions() {
// Wall collisions
const wm = CFG.wallMargin;
for (const p of sim.points) {
if (p.pinned) continue;
// Floor
if (p.y > H - wm) { p.y = H - wm; }
// Ceiling
if (p.y < wm) { p.y = wm; }
// Left wall
if (p.x < wm) { p.x = wm; }
// Right wall
if (p.x > W - wm) { p.x = W - wm; }
}
// Static circle collision
const sc = CFG.staticCircle;
for (const p of sim.points) {
if (p.pinned) continue;
const dx = p.x - sc.cx;
const dy = p.y - sc.cy;
const d = Math.sqrt(dx * dx + dy * dy);
if (d < sc.radius) {
const overlap = sc.radius - d;
if (overlap > 1e-6) {
const nx = dx / d;
const ny = dy / d;
p.x += nx * overlap;
p.y += ny * overlap;
}
}
}
// Static box collision
const bw = 200, bh = 150, bx = 500, by = 250;
for (const p of sim.points) {
if (p.pinned) continue;
if (p.x < bx + bw && p.x > bx && p.y < by + bh && p.y > by) {
const dx = p.x - (bx + bw / 2);
const dy = p.y - (by + bh / 2);
const d = Math.sqrt(dx * dx + dy * dy);
if (d < 0.01) continue;
const overlap = Math.max(bw / 2 - Math.abs(dx), bh / 2 - Math.abs(dy));
if (overlap > 1e-6) {
const nx = dx / d;
const ny = dy / d;
p.x += nx * overlap;
p.y += ny * overlap;
}
}
}
}
function stepPhysics(dt) {
verletStep(dt, CFG.damping);
solveConstraints(CFG.iterations);
clampPositions();
resolveCollisions();
}
// ---- Simulation Update ----
function updateSimulation() {
const dt = CFG.timestep;
sim.accumulator += dt;
let steps = 0;
while (sim.accumulator >= dt && steps < 8) {
stepPhysics(dt);
sim.accumulator -= dt;
steps++;
}
if (sim.accumulator > dt) sim.accumulator = 0;
}
// ---- Point Search ----
function findNearestPoint(mx, my, radius) {
let best = null, bestDist = radius || CFG.grabRadius;
for (const p of sim.points) {
const d = dist(mx, my, p.x, p.y);
if (d < bestDist) { bestDist = d; best = p; }
}
return best;
}
// ---- Interaction ----
function onMouseDown(e) {
const rect = canvas.getBoundingClientRect();
const mx = e.clientX - rect.left;
const my = e.clientY - rect.top;
sim.mouse.down = true;
sim.mouse.x = mx; sim.mouse.y = my;
if (e.button === 2) {
sim.mouse.rightDown = true;
const p = findNearestPoint(mx, my, CFG.tearRadius);
if (p) {
// Tear constraint to this point
for (const c of sim.constraints) {
if (!c.active) continue;
const mid = c.midpoint;
if (dist(mx, my, mid.x, mid.y) < CFG.tearRadius) {
c.active = false;
c.removed = true;
}
}
}
} else {
const p = findNearestPoint(mx, my, CFG.grabRadius);
if (p) {
sim.grabbed = p;
sim.dragging = p;
// Save previous position for velocity
p.xPrev = p.x;
p.yPrev = p.y;
} else {
// Try to spawn a new object
spawnObject(mx, my);
}
}
}
function onMouseMove(e) {
const rect = canvas.getBoundingClientRect();
const mx = e.clientX - rect.left;
const my = e.clientY - rect.top;
sim.mouse.x = mx; sim.mouse.y = my;
if (sim.dragging && sim.grabbed) {
// Move grabbed point to cursor
sim.dragging.x = mx;
sim.dragging.y = my;
sim.dragging.xPrev = mx - (sim.dragging.x - sim.dragging.xPrev) * 0.5;
sim.dragging.yPrev = my - (sim.dragging.y - sim.dragging.yPrev) * 0.5;
}
}
function onMouseUp(e) {
const rect = canvas.getBoundingClientRect();
const mx = e.clientX - rect.left;
const my = e.clientY - rect.top;
sim.mouse.down = false;
sim.mouse.rightDown = false;
if (sim.dragging) {
// Release: compute velocity and let physics take over
const vx = sim.dragging.x - sim.dragging.xPrev;
const vy = sim.dragging.y - sim.dragging.yPrev;
sim.dragging.xPrev = sim.dragging.x;
sim.dragging.yPrev = sim.dragging.y;
// Add velocity to Verlet
// (we'll handle this by keeping the previous position slightly behind)
sim.dragging = null;
}
sim.grabbed = null;
}
function spawnObject(mx, my) {
// Spawn at click position
const types = ['rope', 'cloth', 'softBody', 'ball'];
const type = types[Math.floor(Math.random() * types.length)];
let obj;
switch (type) {
case 'rope':
obj = createRope(mx, my, 20, 35);
break;
case 'cloth':
obj = createCloth(16, 16, 35, 'corners');
break;
case 'softBody':
obj = createSoftBody(mx, my, 50, 64);
break;
case 'ball':
obj = createBall(mx, my);
break;
}
sim.objects.push(obj);
// Add points to sim.points
for (const p of obj.points) {
sim.points.push(p);
}
// Rebuild constraints
sim.constraints = [];
for (const c of obj.constraints) {
sim.constraints.push(c);
}
}
// ---- Pin Toggle ----
function togglePin(point) {
if (point && !point.pinned) {
point.pinned = true;
} else if (point && point.pinned) {
point.pinned = false;
}
}
// ---- Event Listeners ----
canvas.addEventListener('mousedown', onMouseDown);
canvas.addEventListener('mousemove', onMouseMove);
canvas.addEventListener('mouseup', onMouseUp);
canvas.addEventListener('mouseleave', onMouseUp);
canvas.addEventListener('contextmenu', e => e.preventDefault());
canvas.addEventListener('touchstart', e => { e.preventDefault(); const t = e.touches[0]; onMouseDown({ clientX: t.clientX, clientY: t.clientY, button: 0 }); }, { passive: false });
canvas.addEventListener('touchmove', e => { e.preventDefault(); const t = e.touches[0]; onMouseMove({ clientX: t.clientX, clientY: t.clientY }); }, { passive: false });
canvas.addEventListener('touchend', e => { e.preventDefault(); onMouseUp({ clientX: e.changedTouches[0].clientX, clientY: e.changedTouches[0].clientY, button: 0 }); }, { passive: false });
// ---- Toolbar Buttons ----
document.getElementById('btnPlay').addEventListener('click', () => { sim.paused = !sim.paused; document.getElementById('btnPlay').textContent = sim.paused ? 'Pause' : 'Play'; document.getElementById('btnPlay').classList.toggle('active', !sim.paused); });
document.getElementById('btnStep').addEventListener('click', () => { if (sim.paused) stepPhysics(CFG.timestep); });
document.getElementById('btnReset').addEventListener('click', () => {
sim.points = []; sim.constraints = []; sim.objects = []; sim.particles = [];
sim.accumulator = 0;
// Remove all objects
sim.objects = [];
// Clear the canvas
ctx.clearRect(0, 0, W, H);
});
document.getElementById('btnSpawnRope').addEventListener('click', () => spawnObject(W / 2, H / 2));
document.getElementById('btnSpawnCloth').addEventListener('click', () => spawnObject(W / 2, H / 2));
document.getElementById('btnSpawnSoft').addEventListener('click', () => spawnObject(W / 2, H / 2));
document.getElementById('btnSpawnBall').addEventListener('click', () => spawnObject(W / 2, H / 2));
document.getElementById('btnTear').addEventListener('click', () => {
// Right-click to tear
const p = findNearestPoint(sim.mouse.x, sim.mouse.y, CFG.tearRadius);
if (p) {
for (const c of sim.constraints) {
if (!c.active) continue;
const mid = c.midpoint;
if (dist(sim.mouse.x, sim.mouse.y, mid.x, mid.y) < CFG.tearRadius) {
c.active = false;
c.removed = true;
}
}
}
});
document.getElementById('btnPin').addEventListener('click', () => {
if (sim.grabbed) {
togglePin(sim.grabbed);
}
});
// ---- Gravity & Iterations Sliders ----
document.getElementById('gSlider').addEventListener('input', e => { CFG.gravity = parseFloat(e.target.value); });
document.getElementById('iSlider').addEventListener('input', e => { CFG.iterations = parseInt(e.target.value); });
document.getElementById('dSlider').addEventListener('input', e => { CFG.damping = parseFloat(e.target.value); });
// ---- Rendering ----
function drawPoint(p) {
if (p.pinned) {
ctx.fillStyle = CFG.pinColor;
ctx.beginPath();
ctx.arc(p.x, p.y, 2, 0, Math.PI * 2);
ctx.fill();
return;
}
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.arc(p.x, p.y, 2, 0, Math.PI * 2);
ctx.fill();
}
function drawConstraint(c, color) {
ctx.strokeStyle = color;
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(c.a.x, c.a.y);
ctx.lineTo(c.b.x, c.b.y);
ctx.stroke();
}
function drawMesh(points, constraints) {
if (points.length < 3) return;
// Draw edge lines
for (const c of constraints) {
if (!c.active) continue;
const isH = (c.a.y === c.b.y);
const isV = (c.a.x === c.b.x);
const isDiag = !isH && !isV;
const isEdge = isH || isV;
const isShear = isDiag;
const alpha = isEdge ? CFG.meshEdgeAlpha : 0.2;
ctx.strokeStyle = `rgba(255,255,255,${alpha})`;
ctx.lineWidth = 0.5;
ctx.beginPath();
ctx.moveTo(c.a.x, c.a.y);
ctx.lineTo(c.b.x, c.b.y);
ctx.stroke();
}
// Draw filled mesh
ctx.fillStyle = `rgba(255,255,255,${CFG.meshFillAlpha})`;
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
for (const p of points) ctx.lineTo(p.x, p.y);
ctx.closePath();
ctx.fill();
}
function drawObject(obj) {
if (obj.type === 'rope') {
ctx.strokeStyle = CFG.objColors.rope;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(obj.points[0].x, obj.points[0].y);
for (let i = 1; i < obj.points.length; i++) {
ctx.lineTo(obj.points[i].x, obj.points[i].y);
}
ctx.stroke();
// Draw points
for (const p of obj.points) drawPoint(p);
} else if (obj.type === 'cloth') {
drawMesh(obj.points, obj.constraints);
for (const p of obj.points) drawPoint(p);
} else if (obj.type === 'softBody') {
drawMesh(obj.points, obj.constraints);
for (const p of obj.points) drawPoint(p);
} else if (obj.type === 'ball') {
// Draw ball as circle
const r = 8;
ctx.fillStyle = CFG.objColors.ball;
ctx.beginPath();
ctx.arc(obj.points[0].x, obj.points[0].y, r, 0, Math.PI * 2);
ctx.fill();
// Inner ring
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(obj.points[0].x, obj.points[0].y, r * 0.7, 0, Math.PI * 2);
ctx.stroke();
drawPoint(obj.points[0]);
}
}
function drawStatic() {
// Walls
ctx.strokeStyle = CFG.wallColor;
ctx.lineWidth = 3;
ctx.strokeRect(0, 0, W, H);
ctx.strokeRect(0, H - 50, W, 50);
ctx.strokeRect(W - 50, 0, 50, H);
// Static circle
const sc = CFG.staticCircle;
ctx.fillStyle = '#333344';
ctx.beginPath();
ctx.arc(sc.cx, sc.cy, sc.radius, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = '#555566';
ctx.lineWidth = 2;
ctx.stroke();
// Label
ctx.fillStyle = '#556677';
ctx.font = '11px monospace';
ctx.fillText('Static Circle', sc.cx - 40, sc.cy - sc.radius - 8);
}
function drawTearEffect() {
// Draw fading lines for torn constraints
for (const c of sim.constraints) {
if (!c.active) continue;
ctx.strokeStyle = 'rgba(255,100,100,0.3)';
ctx.lineWidth = 1;
ctx.setLineDash([4, 4]);
ctx.beginPath();
ctx.moveTo(c.a.x, c.a.y);
ctx.lineTo(c.b.x, c.b.y);
ctx.stroke();
ctx.setLineDash([]);
}
}
function drawFPS() {
sim.fpsTimer++;
if (sim.fpsTimer >= 30) {
sim.fps = Math.round(sim.frameCount / (sim.fpsTimer / 16.67));
sim.frameCount = 0;
sim.fpsTimer = 0;
}
}
// ---- Main Render ----
function render() {
ctx.clearRect(0, 0, W, H);
// Background
ctx.fillStyle = '#0d0d0f';
ctx.fillRect(0, 0, W, H);
// Subtle grid
ctx.strokeStyle = 'rgba(255,255,255,0.03)';
ctx.lineWidth = 0.5;
const gridSize = 50;
for (let x = 0; x < W; x += gridSize) {
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke();
}
for (let y = 0; y < H; y += gridSize) {
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke();
}
// Draw static shapes
drawStatic();
// Draw objects
for (const obj of sim.objects) drawObject(obj);
// Draw torn constraints
drawTearEffect();
// Draw points
for (const p of sim.points) {
if (p.pinned) continue;
// Trail effect for fast-moving points
const vel = p.velocity();
const speed = Math.sqrt(vel.x * vel.x + vel.y * vel.y);
if (speed > 5) {
ctx.fillStyle = `rgba(255,255,255,${CFG.trailAlpha * Math.min(speed / 100, 1)})`;
ctx.beginPath();
ctx.arc(p.x, p.y, 1.5, 0, Math.PI * 2);
ctx.fill();
}
}
// Draw grabbed point
if (sim.grabbed) {
const g = sim.grabbed;
ctx.fillStyle = '#FF8C00';
ctx.beginPath();
ctx.arc(g.x, g.y, 5, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.arc(g.x, g.y, 8, 0, Math.PI * 2);
ctx.stroke();
}
// Draw constraint stretch visualization (color by stretch)
// Draw all active constraints with color
for (const c of sim.constraints) {
if (!c.active) continue;
const stretch = c.stretch;
let r, g, b;
if (stretch < 0.85) {
r = 0; g = 180; b = 120; // green
} else if (stretch < 1.0) {
r = 255; g = 220; b = 100; // yellow
} else if (stretch < 1.2) {
r = 255; g = 100; b = 50; // orange
} else {
r = 255; g = 50; b = 50; // red
}
ctx.strokeStyle = `rgba(${r},${g},${b},${CFG.meshEdgeAlpha})`;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(c.a.x, c.a.y);
ctx.lineTo(c.b.x, c.b.y);
ctx.stroke();
}
// Draw mouse cursor
if (sim.mouse.down || sim.mouse.rightDown) {
ctx.fillStyle = 'rgba(255,255,255,0.4)';
ctx.beginPath();
ctx.arc(sim.mouse.x, sim.mouse.y, 6, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 1;
ctx.stroke();
}
// FPS
drawFPS();
}
// ---- Main Loop ----
let lastTimestamp = 0;
function loop(timestamp) {
if (!lastTimestamp) lastTimestamp = timestamp;
const dt = Math.min((timestamp - lastTimestamp) / 1000, CFG.maxDelta);
lastTimestamp = timestamp;
sim.frameCount++;
if (!sim.paused) {
updateSimulation();
}
render();
requestAnimationFrame(loop);
}
// ---- Expose API ----
window.sim = sim;
window.sim.step = function() { stepPhysics(CFG.timestep); };
window.sim.points = sim.points;
// ---- Initialize ----
// Pre-spawn a few objects for visual interest
sim.objects.push(createRope(300, 700, 25, 30));
sim.objects.push(createSoftBody(600, 400, 50, 64));
sim.objects.push(createBall(300, 600));
sim.objects.push(createCloth(16, 16, 35, 'corners'));
// Start loop
requestAnimationFrame(loop);
</script>
</body>
</html>