Physics Playground
ThinkingCap Qwen3.6 27B GGUF · 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 38,217 bytes · SHA-256 c563b3e17203
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Soft-Body Physics Playground</title>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #111; font-family: 'Segoe UI', system-ui, sans-serif; }
#app { position: relative; width: 100%; height: 100%; }
#canvas { display: block; width: 100%; height: 100%; cursor: crosshair; }
#toolbar {
position: absolute; top: 8px; left: 50%; transform: translateX(-50%);
display: flex; align-items: center; gap: 4px;
background: rgba(20,20,30,0.85); backdrop-filter: blur(6px);
padding: 6px 12px; border-radius: 8px;
border: 1px solid rgba(255,255,255,0.08);
z-index: 10;
}
#toolbar button {
background: rgba(255,255,255,0.08); color: #ccc; border: 1px solid transparent;
padding: 5px 10px; border-radius: 5px; cursor: pointer; font-size: 14px;
transition: background 0.15s, border-color 0.15s;
}
#toolbar button:hover { background: rgba(255,255,255,0.15); }
#toolbar button.active { border-color: #5af; color: #5af; background: rgba(85,170,255,0.12); }
#toolbar .sep { color: rgba(255,255,255,0.2); margin: 0 4px; user-select: none; }
#toolbar label {
color: #999; font-size: 12px; display: flex; align-items: center; gap: 3px;
margin: 0 2px;
}
#toolbar input[type="range"] { width: 60px; accent-color: #5af; }
#toolbar input[type="checkbox"] { accent-color: #5af; }
#hud {
position: absolute; bottom: 8px; left: 8px;
color: rgba(255,255,255,0.5); font-size: 11px; line-height: 1.5;
pointer-events: none; user-select: none;
}
</style>
</head>
<body>
<div id="app">
<canvas id="canvas"></canvas>
<div id="toolbar">
<button data-tool="select" class="active">🖱 Select</button>
<button data-tool="rope">🪢 Rope</button>
<button data-tool="cloth">🧵 Cloth</button>
<button data-tool="softbody">🔵 Soft Body</button>
<button data-tool="ball">⚽ Ball</button>
<span class="sep">|</span>
<button id="btn-pause">⏸ Pause</button>
<button id="btn-step">⏭ Step</button>
<button id="btn-reset">🔄 Reset</button>
<span class="sep">|</span>
<label>Gravity <input type="range" id="grav-slider" min="0" max="2000" value="980"></label>
<label>Iterations <input type="range" id="iter-slider" min="1" max="20" value="6"></label>
<span class="sep">|</span>
<label><input type="checkbox" id="chk-stress"> Stress</label>
<label><input type="checkbox" id="chk-points"> Points</label>
</div>
<div id="hud">
<div id="fps">FPS: --</div>
<div id="physTime">Phys: --ms</div>
<div id="objCount">Objects: 0</div>
</div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./config":"src/config.ts","./physics":"src/physics.ts","./simulation":"src/simulation.ts","./renderer":"src/renderer.ts"},"src/physics.ts":{"./config":"src/config.ts"},"src/simulation.ts":{"./config":"src/config.ts","./physics":"src/physics.ts"},"src/renderer.ts":{"./config":"src/config.ts","./physics":"src/physics.ts","./simulation":"src/simulation.ts"}};
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 });
// Main entry: wire up canvas, input, toolbar, game loop
const config_1 = require("./config");
const physics_1 = require("./physics");
const simulation_1 = require("./simulation");
const renderer_1 = require("./renderer");
const canvas = document.getElementById('canvas');
const renderer = new renderer_1.Renderer(canvas);
const sim = new simulation_1.Simulation();
sim.setBounds(window.innerWidth, window.innerHeight);
// Add a static obstacle circle
sim.staticCircles.push({ cx: window.innerWidth / 2, cy: window.innerHeight * 0.5, r: 40 });
// Tool state
let currentTool = 'select';
let isDragging = false;
let isRightDrag = false;
let dragStartX = 0;
let dragStartY = 0;
let lastDragX = 0;
let lastDragY = 0;
let spawnPreview = null;
// FPS tracking
let lastTime = performance.now();
let fps = 0;
let frameCount = 0;
let fpsUpdateTime = 0;
let physStepTime = 0;
// HUD elements
const fpsEl = document.getElementById('fps');
const physTimeEl = document.getElementById('physTime');
const objCountEl = document.getElementById('objCount');
// Resize
function resize() {
const w = window.innerWidth;
const h = window.innerHeight;
renderer.resize(w, h);
sim.setBounds(w, h);
// Update static circle position
if (sim.staticCircles.length > 0) {
sim.staticCircles[0].cx = w / 2;
sim.staticCircles[0].cy = h * 0.5;
}
}
resize();
window.addEventListener('resize', resize);
// Toolbar buttons
const toolbar = document.getElementById('toolbar');
toolbar.querySelectorAll('[data-tool]').forEach(btn => {
btn.addEventListener('click', () => {
toolbar.querySelectorAll('[data-tool]').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
currentTool = btn.dataset.tool || 'select';
canvas.style.cursor = currentTool === 'select' ? 'default' : 'crosshair';
});
});
// Pause / Step / Reset
document.getElementById('btn-pause').addEventListener('click', () => {
sim.paused = !sim.paused;
document.getElementById('btn-pause').textContent = sim.paused ? '▶ Play' : '⏸ Pause';
});
document.getElementById('btn-step').addEventListener('click', () => {
sim.step(Number(gravSlider.value), Number(iterSlider.value));
renderer.render(sim);
});
document.getElementById('btn-reset').addEventListener('click', () => {
sim.removeObjects();
});
// Sliders & checkboxes
const gravSlider = document.getElementById('grav-slider');
const iterSlider = document.getElementById('iter-slider');
const chkStress = document.getElementById('chk-stress');
const chkPoints = document.getElementById('chk-points');
chkStress.addEventListener('change', () => { sim.showStress = chkStress.checked; });
chkPoints.addEventListener('change', () => { sim.showPoints = chkPoints.checked; });
// Keyboard
window.addEventListener('keydown', (e) => {
if (e.code === 'Space') {
e.preventDefault();
sim.paused = !sim.paused;
document.getElementById('btn-pause').textContent = sim.paused ? '▶ Play' : '⏸ Pause';
}
if (e.code === 'ArrowRight' && sim.paused) {
e.preventDefault();
sim.step(Number(gravSlider.value), Number(iterSlider.value));
renderer.render(sim);
}
});
// Mouse / pointer interaction
function getCanvasPos(e) {
const rect = canvas.getBoundingClientRect();
return { x: (e.clientX - rect.left) * (canvas.width / rect.width), y: (e.clientY - rect.top) * (canvas.height / rect.height) };
}
canvas.addEventListener('pointerdown', (e) => {
const pos = getCanvasPos(e);
isDragging = true;
dragStartX = pos.x;
dragStartY = pos.y;
lastDragX = pos.x;
lastDragY = pos.y;
spawnPreview = null;
if (e.button === 2 || e.getModifierState('ShiftKey')) {
// Right click or shift: tear mode
isRightDrag = true;
return;
}
if (e.getModifierState('ControlKey')) {
// Ctrl + click: toggle pin
sim.togglePin(pos.x, pos.y, config_1.CONFIG.grabRadius);
isDragging = false;
return;
}
if (currentTool === 'select') {
// Try to grab a point
if (!sim.grabPoint(pos.x, pos.y, config_1.CONFIG.grabRadius)) {
isDragging = false;
}
return;
}
// Start spawn drag
spawnPreview = { x: pos.x, y: pos.y };
});
canvas.addEventListener('pointermove', (e) => {
if (!isDragging)
return;
const pos = getCanvasPos(e);
if (isRightDrag) {
// Tear along drag path
sim.tearConstraints(lastDragX, lastDragY, pos.x, pos.y, config_1.CONFIG.tearRadius);
lastDragX = pos.x;
lastDragY = pos.y;
return;
}
if (sim.grabbedPoint) {
// Set grabbed point position (velocity will be computed from position delta)
sim.setGrabPosition(pos.x, pos.y);
lastDragX = pos.x;
lastDragY = pos.y;
return;
}
if (spawnPreview && currentTool !== 'select') {
spawnPreview = { x: pos.x, y: pos.y };
}
});
canvas.addEventListener('pointerup', (e) => {
if (isRightDrag) {
isRightDrag = false;
isDragging = false;
return;
}
if (sim.grabbedPoint) {
sim.releaseGrab();
isDragging = false;
return;
}
if (spawnPreview) {
const pos = getCanvasPos(e);
// Spawn object based on drag extent
const dx = Math.abs(pos.x - dragStartX);
const dy = Math.abs(pos.y - dragStartY);
if (currentTool === 'rope') {
const length = Math.sqrt(dx * dx + dy * dy) || config_1.CONFIG.ropeSegments * config_1.CONFIG.ropeSpacing;
const segs = Math.max(4, Math.round(length / config_1.CONFIG.ropeSpacing));
const angle = Math.atan2(dy, dx);
const segLen = length / segs;
const obj = (0, physics_1.createRope)(dragStartX, dragStartY, segs, segLen);
// Orient the rope along the drag direction
for (let i = 0; i < obj.points.length; i++) {
const t = i / (obj.points.length - 1);
obj.points[i].x = dragStartX + Math.cos(angle) * t * length;
obj.points[i].xPrev = obj.points[i].x;
obj.points[i].y = dragStartY + Math.sin(angle) * t * length;
obj.points[i].yPrev = obj.points[i].y;
}
sim.addObject(obj);
}
else if (currentTool === 'cloth') {
const w = Math.max(dx, 40);
const h = Math.max(dy, 40);
const cols = Math.max(4, Math.round(w / config_1.CONFIG.clothSpacing));
const rows = Math.max(3, Math.round(h / config_1.CONFIG.clothSpacing));
const obj = (0, physics_1.createCloth)(dragStartX, dragStartY, cols, rows, config_1.CONFIG.clothSpacing, 'corners');
sim.addObject(obj);
}
else if (currentTool === 'softbody') {
const r = Math.max(Math.sqrt(dx * dx + dy * dy), 30) / 2;
const obj = (0, physics_1.createSoftBody)(dragStartX, dragStartY, r, config_1.CONFIG.softBodyPoints);
sim.addObject(obj);
}
else if (currentTool === 'ball') {
const obj = (0, physics_1.createBall)(dragStartX, dragStartY, config_1.CONFIG.ballRadius);
sim.addObject(obj);
}
spawnPreview = null;
isDragging = false;
}
});
// Prevent context menu on canvas
canvas.addEventListener('contextmenu', (e) => e.preventDefault());
// Expose sim on window for headless probing
window.sim = sim;
// Spawn initial objects (leave 30px margin from walls)
const w = window.innerWidth;
const h = window.innerHeight;
const margin = 30;
sim.addObject((0, physics_1.createRope)(w * 0.2, 50, config_1.CONFIG.ropeSegments, config_1.CONFIG.ropeSpacing));
sim.addObject((0, physics_1.createCloth)(w * 0.5 - 90, 70, config_1.CONFIG.clothCols, config_1.CONFIG.clothRows, config_1.CONFIG.clothSpacing, 'corners'));
sim.addObject((0, physics_1.createSoftBody)(w * 0.8, 150, config_1.CONFIG.softBodyRadius, config_1.CONFIG.softBodyPoints));
sim.addObject((0, physics_1.createBall)(w * 0.4, 120, config_1.CONFIG.ballRadius));
// Main loop
function loop(now) {
// FPS calculation
frameCount++;
if (now - fpsUpdateTime > 500) {
fps = Math.round(frameCount * 1000 / (now - fpsUpdateTime));
frameCount = 0;
fpsUpdateTime = now;
}
const dt = Math.min((now - lastTime) / 1000, 0.1);
lastTime = now;
const t0 = performance.now();
sim.tick(dt, Number(gravSlider.value), Number(iterSlider.value));
physStepTime = performance.now() - t0;
renderer.render(sim);
// Draw spawn preview
if (spawnPreview && isDragging && currentTool !== 'select') {
const e_pos = getCanvasPos({ clientX: 0, clientY: 0 });
// We need the current pointer position — use spawnPreview as end
const ctx = renderer['ctx'];
ctx.setLineDash([4, 4]);
ctx.strokeStyle = 'rgba(255,255,255,0.3)';
ctx.lineWidth = 1;
if (currentTool === 'rope') {
ctx.beginPath();
ctx.moveTo(dragStartX, dragStartY);
ctx.lineTo(spawnPreview.x, spawnPreview.y);
ctx.stroke();
}
else if (currentTool === 'cloth') {
ctx.strokeRect(dragStartX, dragStartY, spawnPreview.x - dragStartX, spawnPreview.y - dragStartY);
}
else {
ctx.beginPath();
ctx.arc(dragStartX, dragStartY, Math.sqrt((spawnPreview.x - dragStartX) ** 2 + (spawnPreview.y - dragStartY) ** 2) / 2, 0, Math.PI * 2);
ctx.stroke();
}
ctx.setLineDash([]);
}
// HUD
fpsEl.textContent = `FPS: ${fps}`;
physTimeEl.textContent = `Phys: ${physStepTime.toFixed(1)}ms`;
objCountEl.textContent = `Objects: ${sim.objects.length} | Points: ${sim.getTotalPoints()}`;
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
};
// ── module: src/config.ts ──
__mods["src/config.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CONFIG = void 0;
// Tunable physics constants
exports.CONFIG = {
// Physics
gravity: 800,
damping: 0.95,
fixedDt: 1 / 60,
maxFramesPerTick: 4,
constraintIterations: 6,
maxDelta: 1 / 20, // clamp frame delta to avoid explosion on tab switch
// Interaction
grabRadius: 16,
tearRadius: 14,
throwDamping: 0.9,
// Objects
ropeSegments: 20,
ropeSpacing: 12,
clothCols: 16,
clothRows: 12,
clothSpacing: 12,
softBodyRadius: 50,
softBodyPoints: 16,
ballRadius: 20,
// Rendering
pointRadius: 2,
clothPointRadius: 0,
};
};
// ── module: src/physics.ts ──
__mods["src/physics.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createRope = createRope;
exports.createCloth = createCloth;
exports.createSoftBody = createSoftBody;
exports.createBall = createBall;
// Build a rope
function createRope(x, y, segments, spacing) {
const pts = [];
const cons = [];
for (let i = 0; i <= segments; i++) {
pts.push({ x: x, y: y + i * spacing, xPrev: x, yPrev: y + i * spacing, pinned: i === 0, mass: 1 });
}
for (let i = 0; i < pts.length - 1; i++) {
cons.push({ i, j: i + 1, restLength: spacing, broken: false });
}
return { type: 'rope', points: pts, constraints: cons, color: '#f95', fillColor: null };
}
// Build a cloth rectangle
function createCloth(x, y, cols, rows, spacing, pinMode) {
const pts = [];
const cons = [];
for (let row = 0; row <= rows; row++) {
for (let col = 0; col <= cols; col++) {
const px = x + col * spacing;
const py = y + row * spacing;
pts.push({ x: px, y: py, xPrev: px, yPrev: py, pinned: false, mass: 1 });
}
}
const pinRow = 0;
for (let col = 0; col <= cols; col++) {
const idx = pinRow * (cols + 1) + col;
if (pinMode === 'edge' || col === 0 || col === cols) {
pts[idx].pinned = true;
}
}
// Structural constraints (horizontal + vertical)
for (let row = 0; row <= rows; row++) {
for (let col = 0; col < cols; col++) {
const a = row * (cols + 1) + col;
const b = a + 1;
cons.push({ i: a, j: b, restLength: spacing, broken: false });
}
}
for (let row = 0; row < rows; row++) {
for (let col = 0; col <= cols; col++) {
const a = row * (cols + 1) + col;
const b = a + (cols + 1);
cons.push({ i: a, j: b, restLength: spacing, broken: false });
}
}
// Shear constraints (diagonal)
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
const a = row * (cols + 1) + col;
const b = a + cols + 2;
cons.push({ i: a, j: b, restLength: spacing * Math.SQRT2, broken: false });
const c = a + 1;
const d = a + cols + 1;
cons.push({ i: c, j: d, restLength: spacing * Math.SQRT2, broken: false });
}
}
return { type: 'cloth', points: pts, constraints: cons, color: '#5af', fillColor: null };
}
// Build a soft body (circle of points with internal bracing)
function createSoftBody(cx, cy, radius, numPoints) {
const pts = [];
const cons = [];
for (let i = 0; i < numPoints; i++) {
const a = (i / numPoints) * Math.PI * 2 - Math.PI / 2;
pts.push({
x: cx + Math.cos(a) * radius,
y: cy + Math.sin(a) * radius,
xPrev: cx + Math.cos(a) * radius,
yPrev: cy + Math.sin(a) * radius,
pinned: false,
mass: 1,
});
}
// Center point
pts.push({ x: cx, y: cy, xPrev: cx, yPrev: cy, pinned: false, mass: 1 });
const centerIdx = numPoints;
// Edge constraints
for (let i = 0; i < numPoints; i++) {
cons.push({ i, j: (i + 1) % numPoints, restLength: 2 * Math.sin(Math.PI / numPoints) * radius, broken: false });
cons.push({ i, j: centerIdx, restLength: radius, broken: false });
}
// Cross bracing for stability
for (let i = 0; i < numPoints; i++) {
cons.push({ i, j: (i + 3) % numPoints, restLength: 2 * Math.sin(3 * Math.PI / numPoints) * radius, broken: false });
}
return { type: 'softbody', points: pts, constraints: cons, color: '#5f5', fillColor: 'rgba(85,255,85,0.12)' };
}
// Build a ball
function createBall(x, y, radius) {
return {
type: 'ball',
points: [{ x, y, xPrev: x, yPrev: y, pinned: false, mass: 1 }],
constraints: [],
color: '#f5f',
fillColor: null,
};
}
};
// ── module: src/simulation.ts ──
__mods["src/simulation.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Simulation = void 0;
// Simulation engine: Verlet integration, constraint relaxation, collision
const config_1 = require("./config");
class Simulation {
constructor() {
this.objects = [];
this.staticRects = [];
this.staticCircles = [];
this.paused = false;
this.gravityOn = true;
this.showStress = false;
this.showPoints = false;
this.accumulator = 0;
this.grabbedPoint = null;
// Floor (will be overridden by setBounds)
this.staticRects.push({ x: -5000, y: 700, w: 10000, h: 20 });
}
setBounds(w, h) {
this.staticRects = [
{ x: -5000, y: h - 10, w: 10000, h: 20 }, // floor
{ x: -10, y: -5000, w: 20, h: h + 5000 }, // left wall
{ x: w - 10, y: -5000, w: 20, h: h + 5000 }, // right wall
];
}
addObject(obj) {
this.objects.push(obj);
}
removeObjects() {
this.objects = [];
}
// Verlet integration step
integrate(dt, gravity) {
const dt2 = dt * dt;
const damp = config_1.CONFIG.damping;
for (const obj of this.objects) {
for (const p of obj.points) {
if (p.pinned)
continue;
const vx = (p.x - p.xPrev) * damp;
const vy = (p.y - p.yPrev) * damp;
p.xPrev = p.x;
p.yPrev = p.y;
p.x += vx;
p.y += vy + (this.gravityOn ? gravity : 0) * dt2;
}
}
}
// Constraint relaxation
solveConstraints(iterations) {
for (const obj of this.objects) {
for (let iter = 0; iter < iterations; iter++) {
for (const c of obj.constraints) {
if (c.broken)
continue;
const pi = obj.points[c.i];
const pj = obj.points[c.j];
const dx = pj.x - pi.x;
const dy = pj.y - pi.y;
let dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 0.001) {
dist = 0.001;
}
const diff = (c.restLength - dist) / dist;
const mi = pi.pinned ? 0 : 1 / pi.mass;
const mj = pj.pinned ? 0 : 1 / pj.mass;
const total = mi + mj;
if (total < 0.001)
continue;
const wi = mi / total;
const wj = mj / total;
const ox = dx * diff;
const oy = dy * diff;
if (!pi.pinned) {
pi.x -= ox * wi;
pi.y -= oy * wi;
}
if (!pj.pinned) {
pj.x += ox * wj;
pj.y += oy * wj;
}
}
}
}
}
// Collision with static geometry — always clamp outside of rects
resolveCollisions() {
const friction = 0.8;
const restitution = 0.05;
for (const obj of this.objects) {
for (const p of obj.points) {
if (p.pinned)
continue;
const r = p.radius ?? 1.5;
for (const rect of this.staticRects) {
const rx = rect.x - r;
const ry = rect.y - r;
const rw = rect.w + 2 * r;
const rh = rect.h + 2 * r;
if (p.x < rx || p.x > rx + rw || p.y < ry || p.y > ry + rh)
continue;
const dLeft = p.x - rx;
const dRight = (rx + rw) - p.x;
const dTop = p.y - ry;
const dBottom = (ry + rh) - p.y;
const minD = Math.min(dLeft, dRight, dTop, dBottom);
// Velocity before collision
const vx = p.x - p.xPrev;
const vy = p.y - p.yPrev;
// Sleep threshold: zero velocity when barely moving on a surface
const sleepThreshold = 0.3;
if (minD === dTop) {
p.y = ry;
if (vy > sleepThreshold) {
p.yPrev = p.y + vy * restitution;
p.xPrev = p.x - vx * friction;
}
else {
p.yPrev = p.y; // sleep
p.xPrev = p.x - vx * friction;
}
}
else if (minD === dBottom) {
p.y = ry + rh;
if (vy < -sleepThreshold) {
p.yPrev = p.y + vy * restitution;
p.xPrev = p.x - vx * friction;
}
else {
p.yPrev = p.y;
p.xPrev = p.x - vx * friction;
}
}
else if (minD === dLeft) {
p.x = rx;
if (vx > sleepThreshold) {
p.xPrev = p.x + vx * restitution;
p.yPrev = p.y - vy * friction;
}
else {
p.xPrev = p.x;
p.yPrev = p.y - vy * friction;
}
}
else {
p.x = rx + rw;
if (vx < -sleepThreshold) {
p.xPrev = p.x + vx * restitution;
p.yPrev = p.y - vy * friction;
}
else {
p.xPrev = p.x;
p.yPrev = p.y - vy * friction;
}
}
}
// Circle collision
for (const circ of this.staticCircles) {
const dx = p.x - circ.cx;
const dy = p.y - circ.cy;
const dist = Math.sqrt(dx * dx + dy * dy);
const minDist = circ.r + r;
if (dist < minDist && dist > 0.001) {
const nx = dx / dist;
const ny = dy / dist;
p.x += nx * (minDist - dist);
p.y += ny * (minDist - dist);
const vx = p.x - p.xPrev;
const vy = p.y - p.yPrev;
const vn = vx * nx + vy * ny;
if (vn < 0) {
const tx = -ny;
const ty = nx;
const vt = vx * tx + vy * ty;
p.xPrev = p.x - (vn * restitution * nx + vt * friction * tx);
p.yPrev = p.y - (vn * restitution * ny + vt * friction * ty);
}
}
}
}
}
}
// NaN guard + hard boundary enforcement (tunneling fallback)
clampPoints() {
const BOUND = 50000;
const r = 1.5;
for (const obj of this.objects) {
for (const p of obj.points) {
if (!isFinite(p.x) || !isFinite(p.y)) {
p.x = 0;
p.y = 0;
}
if (!isFinite(p.xPrev) || !isFinite(p.yPrev)) {
p.xPrev = p.x;
p.yPrev = p.y;
}
p.x = Math.max(-BOUND, Math.min(BOUND, p.x));
p.y = Math.max(-BOUND, Math.min(BOUND, p.y));
p.xPrev = Math.max(-BOUND, Math.min(BOUND, p.xPrev));
p.yPrev = Math.max(-BOUND, Math.min(BOUND, p.yPrev));
// Hard clamp against static rects (catch tunneling)
if (p.pinned)
continue;
for (const rect of this.staticRects) {
const rx1 = rect.x - r;
const ry1 = rect.y - r;
const rx2 = rect.x + rect.w + r;
const ry2 = rect.y + rect.h + r;
if (p.x >= rx1 && p.x <= rx2) {
if (p.y > ry2) {
p.y = ry2;
if (p.y - p.yPrev > 0.3) {
p.yPrev = p.y + (p.y - p.yPrev) * 0.05;
}
else {
p.yPrev = p.y;
}
}
}
if (p.y >= ry1 && p.y <= ry2) {
if (p.x < rx1) {
p.x = rx1;
p.xPrev = p.x;
}
if (p.x > rx2) {
p.x = rx2;
p.xPrev = p.x;
}
}
}
}
}
}
step(gravity = config_1.CONFIG.gravity, iterations = config_1.CONFIG.constraintIterations) {
const dt = config_1.CONFIG.fixedDt;
this.integrate(dt, gravity);
// Run collision after each constraint pass for stability
for (let i = 0; i < iterations; i++) {
this.solveConstraints(1);
this.resolveCollisions();
}
this.clampPoints();
}
tick(deltaTime, gravity = config_1.CONFIG.gravity, iterations = config_1.CONFIG.constraintIterations) {
if (this.paused)
return;
let dt = Math.min(deltaTime, config_1.CONFIG.maxDelta);
this.accumulator += dt;
let count = 0;
while (this.accumulator >= config_1.CONFIG.fixedDt && count < config_1.CONFIG.maxFramesPerTick) {
this.step(gravity, iterations);
this.accumulator -= config_1.CONFIG.fixedDt;
count++;
}
if (this.accumulator >= config_1.CONFIG.fixedDt) {
this.accumulator = 0;
}
}
findNearestPoint(mx, my, radius) {
let best = null;
for (const obj of this.objects) {
for (let i = 0; i < obj.points.length; i++) {
const p = obj.points[i];
const dx = p.x - mx;
const dy = p.y - my;
const d = dx * dx + dy * dy;
if (d < radius * radius) {
if (!best || d < best.dist) {
best = { obj, idx: i, dist: d };
}
}
}
}
return best ? { obj: best.obj, idx: best.idx } : null;
}
grabPoint(mx, my, radius) {
const hit = this.findNearestPoint(mx, my, radius);
if (hit) {
this.grabbedPoint = hit;
return true;
}
return false;
}
setGrabPosition(mx, my) {
if (!this.grabbedPoint)
return;
const p = this.grabbedPoint.obj.points[this.grabbedPoint.idx];
if (p.pinned)
return;
p.x = mx;
p.y = my;
}
releaseGrab() {
this.grabbedPoint = null;
}
tearConstraints(x1, y1, x2, y2, radius) {
for (const obj of this.objects) {
for (const c of obj.constraints) {
if (c.broken)
continue;
const pi = obj.points[c.i];
const pj = obj.points[c.j];
const mx = (pi.x + pj.x) / 2;
const my = (pi.y + pj.y) / 2;
const dx = x2 - x1;
const dy = y2 - y1;
const len2 = dx * dx + dy * dy;
let t = 0;
if (len2 > 0.001) {
t = ((mx - x1) * dx + (my - y1) * dy) / len2;
t = Math.max(0, Math.min(1, t));
}
const px = x1 + t * dx;
const py = y1 + t * dy;
const d2 = (mx - px) * (mx - px) + (my - py) * (my - py);
if (d2 < radius * radius) {
c.broken = true;
}
}
}
}
togglePin(mx, my, radius) {
const hit = this.findNearestPoint(mx, my, radius);
if (hit) {
hit.obj.points[hit.idx].pinned = !hit.obj.points[hit.idx].pinned;
return true;
}
return false;
}
getKineticEnergy() {
let ke = 0;
const dt = config_1.CONFIG.fixedDt;
for (const obj of this.objects) {
for (const p of obj.points) {
const vx = (p.x - p.xPrev) / dt;
const vy = (p.y - p.yPrev) / dt;
ke += 0.5 * p.mass * (vx * vx + vy * vy);
}
}
return ke;
}
getTotalPoints() {
let n = 0;
for (const obj of this.objects)
n += obj.points.length;
return n;
}
hasNaN() {
for (const obj of this.objects) {
for (const p of obj.points) {
if (!isFinite(p.x) || !isFinite(p.y))
return true;
}
}
return false;
}
}
exports.Simulation = Simulation;
};
// ── module: src/renderer.ts ──
__mods["src/renderer.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Renderer = void 0;
// Renderer: Canvas 2D drawing for the physics playground
const config_1 = require("./config");
class Renderer {
constructor(canvas) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
}
resize(w, h) {
this.canvas.width = w;
this.canvas.height = h;
}
render(sim) {
const ctx = this.ctx;
const w = this.canvas.width;
const h = this.canvas.height;
// Background with slight motion blur
ctx.fillStyle = 'rgba(17, 17, 24, 0.92)';
ctx.fillRect(0, 0, w, h);
// Draw static geometry
this.drawStatics(sim);
// Draw objects
for (const obj of sim.objects) {
this.drawObject(ctx, obj, sim.showStress, sim.showPoints);
}
// Draw grab highlight
if (sim['grabbedPoint']) {
const gp = sim['grabbedPoint'];
const p = gp.obj.points[gp.idx];
ctx.beginPath();
ctx.arc(p.x, p.y, config_1.CONFIG.grabRadius + 4, 0, Math.PI * 2);
ctx.strokeStyle = 'rgba(255,255,100,0.6)';
ctx.lineWidth = 2;
ctx.stroke();
}
}
drawStatics(sim) {
const ctx = this.ctx;
// Floor and walls
ctx.fillStyle = 'rgba(50, 50, 70, 0.5)';
for (const r of sim.staticRects) {
ctx.fillRect(r.x, r.y, r.w, r.h);
}
// Floor/wall lines
ctx.strokeStyle = 'rgba(100, 100, 130, 0.4)';
ctx.lineWidth = 1;
for (const r of sim.staticRects) {
ctx.beginPath();
ctx.moveTo(r.x, r.y);
ctx.lineTo(r.x + r.w, r.y);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(r.x, r.y);
ctx.lineTo(r.x, r.y + r.h);
ctx.stroke();
}
// Static circles
for (const c of sim.staticCircles) {
ctx.fillStyle = 'rgba(50, 50, 70, 0.5)';
ctx.beginPath();
ctx.arc(c.cx, c.cy, c.r, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = 'rgba(100, 100, 130, 0.4)';
ctx.stroke();
}
}
drawObject(ctx, obj, showStress, showPoints) {
const pts = obj.points;
const cons = obj.constraints;
// Draw filled shape for soft bodies
if (obj.type === 'softbody' && obj.fillColor && pts.length > 2) {
const centerIdx = pts.length - 1;
ctx.beginPath();
ctx.moveTo(pts[0].x, pts[0].y);
for (let i = 1; i < centerIdx; i++) {
ctx.lineTo(pts[i].x, pts[i].y);
}
ctx.closePath();
ctx.fillStyle = obj.fillColor;
ctx.fill();
}
// Draw cloth as filled mesh
if (obj.type === 'cloth') {
this.drawClothMesh(ctx, obj);
}
// Draw ball
if (obj.type === 'ball' && pts.length === 1) {
const r = config_1.CONFIG.ballRadius;
ctx.beginPath();
ctx.arc(pts[0].x, pts[0].y, r, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(255,85,255,0.2)';
ctx.fill();
ctx.strokeStyle = obj.color;
ctx.lineWidth = 2;
ctx.stroke();
return;
}
// Draw constraints as lines
for (const c of cons) {
if (c.broken)
continue;
const pi = pts[c.i];
const pj = pts[c.j];
let color = obj.color;
if (showStress) {
const dx = pj.x - pi.x;
const dy = pj.y - pi.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (c.restLength > 0.001) {
const stretch = dist / c.restLength;
const t = Math.min(1, Math.max(0, (stretch - 0.9) * 5));
color = this.stretchColor(t);
}
}
ctx.beginPath();
ctx.moveTo(pi.x, pi.y);
ctx.lineTo(pj.x, pj.y);
ctx.strokeStyle = color;
ctx.lineWidth = obj.type === 'cloth' ? 0.8 : 1.5;
ctx.stroke();
}
// Draw points
if (showPoints) {
for (const p of pts) {
ctx.beginPath();
ctx.arc(p.x, p.y, config_1.CONFIG.pointRadius, 0, Math.PI * 2);
ctx.fillStyle = p.pinned ? '#ff0' : obj.color;
ctx.fill();
}
}
}
drawClothMesh(ctx, obj) {
const pts = obj.points;
const cols = config_1.CONFIG.clothCols;
const rows = config_1.CONFIG.clothRows;
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
const a = row * (cols + 1) + col;
const b = a + 1;
const c = a + (cols + 1);
const d = c + 1;
// Fill quadrilateral
ctx.beginPath();
ctx.moveTo(pts[a].x, pts[a].y);
ctx.lineTo(pts[b].x, pts[b].y);
ctx.lineTo(pts[d].x, pts[d].y);
ctx.lineTo(pts[c].x, pts[c].y);
ctx.closePath();
ctx.fillStyle = 'rgba(85,170,255,0.08)';
ctx.fill();
}
}
}
stretchColor(t) {
const r = Math.round(50 + 205 * t);
const g = Math.round(220 - 180 * t);
return `rgb(${r},${g},50)`;
}
}
exports.Renderer = Renderer;
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"ThinkingCap-Qwen3.6-27B-GGUF","provider":"brigid","persona":"typescript","sessionId":"c2155b86-1773-43d5-b721-c817a37e999c","tokensIn":3130814,"tokensOut":49377,"tokensTotal":3180191,"cost":0,"turns":82,"toolCalls":82,"failedToolCalls":1,"timestamp":"2026-08-03T21:35:39.497Z"} -->