← Physics Playground results

Physics Playground

Qwen 3.5 122B A10B MTP GGUF · typescript

Initial view of Physics Playground

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 48,901 bytes · SHA-256 ba508ad3546f
<!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: #1a1a2e; font-family: 'Segoe UI', sans-serif; }
#canvas { display: block; width: 100%; height: 100%; }
#toolbar { position: absolute; top: 10px; left: 50%; transform: translateX(-50%); display: flex; gap: 8px; background: rgba(0,0,0,0.6); padding: 8px 12px; border-radius: 8px; backdrop-filter: blur(4px); }
#toolbar button { background: #4a4a6a; color: #fff; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; font-size: 14px; transition: background 0.2s; }
#toolbar button:hover { background: #5a5a8a; }
#toolbar button.active { background: #4a9eff; }
#controls { position: absolute; top: 10px; left: 10px; display: flex; flex-direction: column; gap: 8px; background: rgba(0,0,0,0.6); padding: 12px; border-radius: 8px; backdrop-filter: blur(4px); color: #fff; font-size: 13px; }
#controls label { display: flex; flex-direction: column; gap: 4px; }
#controls input[type="range"] { width: 120px; }
#controls select { padding: 4px; background: #2a2a4a; color: #fff; border: 1px solid #4a4a6a; border-radius: 4px; }
#hud { position: absolute; top: 10px; right: 10px; background: rgba(0,0,0,0.6); padding: 8px 12px; border-radius: 8px; color: #88ff88; font-family: monospace; font-size: 13px; backdrop-filter: blur(4px); }
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<div id="toolbar">
  <button id="btn-rope">Rope</button>
  <button id="btn-cloth">Cloth</button>
  <button id="btn-softbody">Soft Body</button>
  <button id="btn-ball">Ball</button>
  <button id="btn-pause">Pause</button>
  <button id="btn-reset">Reset</button>
</div>
<div id="controls">
  <label>Gravity: <input type="range" id="gravity" min="-1" max="2" step="0.1" value="0.5"></label>
  <label>Iterations: <input type="range" id="iterations" min="1" max="15" step="1" value="5"></label>
  <label>Mode: <select id="mode"><option value="grab">Grab</option><option value="tear">Tear</option><option value="pin">Pin</option></select></label>
</div>
<div id="hud"><span id="fps">FPS: 0</span> | <span id="points">Points: 0</span></div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./physics.js":"src/physics.ts","./renderer.js":"src/renderer.ts","./input.js":"src/input.ts","./ui.js":"src/ui.ts","./types.js":"src/types.ts"},"src/physics.ts":{"./types.js":"src/types.ts"},"src/renderer.ts":{"./physics.js":"src/physics.ts","./types.js":"src/types.ts"},"src/input.ts":{"./physics.js":"src/physics.ts","./types.js":"src/types.ts"},"src/ui.ts":{"./input.js":"src/input.ts","./physics.js":"src/physics.ts","./types.js":"src/types.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";
// ============== MAIN ENTRY POINT ==============
Object.defineProperty(exports, "__esModule", { value: true });
exports.Simulation = void 0;
exports.start = start;
const physics_js_1 = require("./physics.js");
const renderer_js_1 = require("./renderer.js");
const input_js_1 = require("./input.js");
const ui_js_1 = require("./ui.js");
const types_js_1 = require("./types.js");
// Main simulation class that ties everything together
class Simulation {
    constructor() {
        this.lastTime = 0;
        this.accumulator = 0;
        this.fpsTime = 0;
        this.frameCount = 0;
        this.currentFPS = 0;
        this.canvas = document.getElementById('canvas');
        this.sim = new physics_js_1.PhysicsEngine();
        this.renderer = new renderer_js_1.Renderer(this.canvas);
        this.input = new input_js_1.InputHandler(this.sim);
        this.ui = new ui_js_1.UIController(this.sim, this.input);
        // Expose for testing/probing
        window.sim = this.sim;
        window.simulation = this;
    }
    start() {
        // Initialize canvas size and static bodies
        this.sim.setCanvasSize(this.canvas.width, this.canvas.height);
        // Attach input handlers
        this.input.attach(this.canvas);
        // Start game loop
        requestAnimationFrame((t) => this.loop(t));
        // Add some initial objects for demonstration
        this.addDemoObjects();
    }
    addDemoObjects() {
        const w = this.canvas.width;
        const h = this.canvas.height;
        // Initial rope hanging from top-left
        this.sim.createObject('rope', 100, 80, {
            segments: 12,
            segmentLength: 20
        });
        // Initial cloth in the middle
        this.sim.createObject('cloth', w * 0.4, 50, {
            width: 150,
            height: 120,
            spacing: 20
        });
    }
    loop(timestamp) {
        if (!this.lastTime)
            this.lastTime = timestamp;
        const deltaTime = Math.min((timestamp - this.lastTime) / 1000, 0.25); // Cap at 0.25s
        this.lastTime = timestamp;
        // Fixed timestep physics
        const fixedDt = types_js_1.CONSTANTS.DT;
        this.accumulator += deltaTime;
        const maxSubsteps = 10;
        let substeps = 0;
        while (this.accumulator >= fixedDt && substeps < maxSubsteps) {
            this.sim.step(fixedDt);
            this.accumulator -= fixedDt;
            substeps++;
        }
        // Render
        this.renderer.render(this.sim, this.input.getDragState(), this.input.getHoveredPoint(), this.input.getTealState());
        // Update UI stats
        this.updateStats();
        // Continue loop
        requestAnimationFrame((t) => this.loop(t));
    }
    updateStats() {
        this.frameCount++;
        const now = performance.now();
        if (now - this.fpsTime >= 500) {
            this.currentFPS = Math.round(this.frameCount * 1000 / (now - this.fpsTime));
            this.frameCount = 0;
            this.fpsTime = now;
            this.ui.updateStats(this.currentFPS);
        }
    }
    // Public methods for testing
    pause() {
        this.input.pause();
    }
    play() {
        this.input.play();
    }
    togglePause() {
        this.input.togglePause();
    }
    step() {
        this.sim.step();
    }
    spawn(type, x, y, params) {
        this.sim.createObject(type, x, y, params);
    }
    reset() {
        this.sim.clearAll();
        this.sim.setCanvasSize(this.canvas.width, this.canvas.height);
        this.addDemoObjects();
    }
    getFPS() {
        return this.currentFPS;
    }
    getPointCount() {
        return this.sim.points.length;
    }
}
exports.Simulation = Simulation;
// Start the simulation when the DOM is ready
function start() {
    const sim = new Simulation();
    sim.start();
}
// Start if not already started
if (typeof window !== 'undefined') {
    // Will be called by the entry point
}
};

// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
// ============== TYPES & CONSTANTS ==============
Object.defineProperty(exports, "__esModule", { value: true });
exports.CONSTANTS = void 0;
exports.vecSub = vecSub;
exports.vecAdd = vecAdd;
exports.vecMult = vecMult;
exports.vecLen = vecLen;
exports.vecNorm = vecNorm;
exports.vecDot = vecDot;
exports.clamp = clamp;
exports.distance = distance;
exports.distanceSquared = distanceSquared;
// Tunable constants
exports.CONSTANTS = {
    DT: 1 / 60,
    DAMPING: 0.99,
    DEFAULT_ITERATIONS: 5,
    GRAVITY: 0.5,
    AIR_FRICTION: 0.995,
    FRICTION: 0.95,
    BOUNCE: 0.7,
    TEAR_DISTANCE: 30,
    TEAR_FORCE_THRESHOLD: 2.5,
    GRAB_RADIUS: 20,
    SUBSTEPS: 1,
    // Colors
    COLORS: {
        ROPE: '#4a9eff',
        CLOTH: '#ff6b4a',
        SOFTBODY: '#4aff9f',
        BALL: '#ffea4a',
        CONSTRAINT: '#666688',
        CONSTRAINT_STRESSED: '#ff4444',
        CONSTRAINT_NORMAL: '#44ff44',
        PINNED: '#ffffff',
        STATIC: '#333355',
        GRAB_HIGHLIGHT: '#ffff00',
    },
    // Object defaults
    ROPE: { segmentLength: 20, segments: 20 },
    CLOTH: { spacing: 25, width: 10, height: 8 },
    SOFTBODY: { radius: 60, segments: 8 },
};
// Helper math functions
function vecSub(a, b) {
    return { x: a.x - b.x, y: a.y - b.y };
}
function vecAdd(a, b) {
    return { x: a.x + b.x, y: a.y + b.y };
}
function vecMult(v, s) {
    return { x: v.x * s, y: v.y * s };
}
function vecLen(v) {
    return Math.sqrt(v.x * v.x + v.y * v.y);
}
function vecNorm(v) {
    const len = vecLen(v);
    if (len < 1e-8)
        return { x: 0, y: 0 };
    return { x: v.x / len, y: v.y / len };
}
function vecDot(a, b) {
    return a.x * b.x + a.y * b.y;
}
function clamp(v, min, max) {
    return Math.max(min, Math.min(max, v));
}
function distance(p1, p2) {
    const dx = p2.x - p1.x;
    const dy = p2.y - p1.y;
    return Math.sqrt(dx * dx + dy * dy);
}
function distanceSquared(p1, p2) {
    const dx = p2.x - p1.x;
    const dy = p2.y - p1.y;
    return dx * dx + dy * dy;
}
};

// ── module: src/physics.ts ──
__mods["src/physics.ts"] = function (exports, require, module) {
"use strict";
// ============== PHYSICS ENGINE ==============
Object.defineProperty(exports, "__esModule", { value: true });
exports.PhysicsEngine = void 0;
const types_js_1 = require("./types.js");
class PhysicsEngine {
    constructor() {
        this.points = [];
        this.constraints = [];
        this.objects = [];
        this.staticBodies = [];
        this.nextPointId = 0;
        this.nextConstraintId = 0;
        this.nextObjectId = 0;
        this.gravity = types_js_1.CONSTANTS.GRAVITY;
        this.iterations = types_js_1.CONSTANTS.DEFAULT_ITERATIONS;
        this.paused = false;
        this.initStaticBodies();
    }
    initStaticBodies() {
        // Will be initialized after we know canvas size
    }
    setCanvasSize(width, height) {
        this.staticBodies = [
            // Floor
            { type: 'floor', x: 0, y: height - 20, color: types_js_1.CONSTANTS.COLORS.STATIC },
            // Left wall
            { type: 'wall', x: 10, y: 0, width: 20, height: height, color: types_js_1.CONSTANTS.COLORS.STATIC },
            // Right wall
            { type: 'wall', x: width - 30, y: 0, width: 20, height: height, color: types_js_1.CONSTANTS.COLORS.STATIC },
            // Optional ceiling (commented out for open top)
            // { type: 'wall', x: 0, y: 0, width: width, height: 20, color: COLORS.STATIC },
        ];
        // Add some interactive static circles
        this.staticBodies.push({ type: 'circle', x: width * 0.3, y: height * 0.6, radius: 50, color: types_js_1.CONSTANTS.COLORS.STATIC }, { type: 'circle', x: width * 0.7, y: height * 0.5, radius: 40, color: types_js_1.CONSTANTS.COLORS.STATIC });
        // Add a static box in the middle
        this.staticBodies.push({ type: 'box', x: width * 0.5 - 40, y: height * 0.55, width: 80, height: 40, color: types_js_1.CONSTANTS.COLORS.STATIC });
    }
    createPoint(x, y, pinned = false, radius = 3, mass = 1) {
        const point = {
            x, y,
            px: x, py: y,
            pinned,
            radius,
            mass,
            id: this.nextPointId++
        };
        this.points.push(point);
        return point;
    }
    createConstraint(p1, p2, stiffness = 1.0, visible = true) {
        const dist = (0, types_js_1.distance)(p1, p2);
        if (dist < 1e-6) {
            // Prevent zero-length constraints
            return { p1, p2, length: 1, stiffness, id: this.nextConstraintId++, visible };
        }
        const constraint = {
            p1, p2,
            length: dist,
            stiffness,
            id: this.nextConstraintId++,
            visible
        };
        this.constraints.push(constraint);
        return constraint;
    }
    createObject(type, x, y, params) {
        const obj = {
            id: this.nextObjectId++,
            type,
            points: [],
            constraints: [],
            color: this.getObjectColor(type),
            bounds: undefined
        };
        switch (type) {
            case 'rope':
                this.createRope(obj, x, y, params);
                break;
            case 'cloth':
                this.createCloth(obj, x, y, params);
                break;
            case 'softbody':
                this.createSoftBody(obj, x, y, params);
                break;
            case 'ball':
                this.createBall(obj, x, y, params);
                break;
        }
        this.objects.push(obj);
        return obj;
    }
    getObjectColor(type) {
        switch (type) {
            case 'rope': return types_js_1.CONSTANTS.COLORS.ROPE;
            case 'cloth': return types_js_1.CONSTANTS.COLORS.CLOTH;
            case 'softbody': return types_js_1.CONSTANTS.COLORS.SOFTBODY;
            case 'ball': return types_js_1.CONSTANTS.COLORS.BALL;
            default: return '#ffffff';
        }
    }
    createRope(obj, startX, startY, params) {
        const segments = params?.segments ?? types_js_1.CONSTANTS.ROPE.segments;
        const segmentLength = params?.segmentLength ?? types_js_1.CONSTANTS.ROPE.segmentLength;
        const startXFixed = Math.max(50, Math.min(startX, 100));
        // Create points hanging down
        for (let i = 0; i <= segments; i++) {
            const x = startXFixed;
            const y = startY + i * segmentLength;
            const pinned = i === 0;
            const point = this.createPoint(x, y, pinned);
            obj.points.push(point);
        }
        // Create constraints between consecutive points
        for (let i = 0; i < obj.points.length - 1; i++) {
            this.createConstraint(obj.points[i], obj.points[i + 1], 1.0, true);
            obj.constraints.push(this.constraints[this.constraints.length - 1]);
        }
    }
    createCloth(obj, startX, startY, params) {
        const width = params?.width ?? types_js_1.CONSTANTS.CLOTH.width;
        const height = params?.height ?? types_js_1.CONSTANTS.CLOTH.height;
        const spacing = params?.spacing ?? types_js_1.CONSTANTS.CLOTH.spacing;
        const cols = Math.floor(width / spacing) + 1;
        const rows = Math.floor(height / spacing) + 1;
        const actualSpacing = width / (cols - 1);
        // Create grid of points
        for (let row = 0; row < rows; row++) {
            for (let col = 0; col < cols; col++) {
                const x = startX + col * actualSpacing;
                const y = startY + row * actualSpacing;
                const pinned = row === 0; // Pin top row
                const point = this.createPoint(x, y, pinned);
                obj.points.push(point);
            }
        }
        // Structural constraints (horizontal and vertical)
        for (let row = 0; row < rows; row++) {
            for (let col = 0; col < cols; col++) {
                const idx = row * cols + col;
                // Right neighbor
                if (col < cols - 1) {
                    const idx2 = idx + 1;
                    this.createConstraint(obj.points[idx], obj.points[idx2], 1.0, false);
                    obj.constraints.push(this.constraints[this.constraints.length - 1]);
                }
                // Bottom neighbor
                if (row < rows - 1) {
                    const idx2 = idx + cols;
                    this.createConstraint(obj.points[idx], obj.points[idx2], 1.0, false);
                    obj.constraints.push(this.constraints[this.constraints.length - 1]);
                }
                // Shear constraints (diagonal)
                if (col < cols - 1 && row < rows - 1) {
                    const idx2 = idx + cols + 1;
                    this.createConstraint(obj.points[idx], obj.points[idx2], 0.5, false);
                    obj.constraints.push(this.constraints[this.constraints.length - 1]);
                }
                if (col > 0 && row < rows - 1) {
                    const idx2 = idx + cols - 1;
                    this.createConstraint(obj.points[idx], obj.points[idx2], 0.5, false);
                    obj.constraints.push(this.constraints[this.constraints.length - 1]);
                }
            }
        }
    }
    createSoftBody(obj, centerX, centerY, params) {
        const radius = params?.radius ?? types_js_1.CONSTANTS.SOFTBODY.radius;
        const segments = params?.segments ?? types_js_1.CONSTANTS.SOFTBODY.segments;
        // Create outer ring of points
        const outerPoints = [];
        for (let i = 0; i < segments; i++) {
            const angle = (i / segments) * Math.PI * 2 - Math.PI / 2;
            const x = centerX + Math.cos(angle) * radius;
            const y = centerY + Math.sin(angle) * radius;
            const point = this.createPoint(x, y, false, 4, 1.5);
            obj.points.push(point);
            outerPoints.push(point);
        }
        // Add center point
        const centerPoint = this.createPoint(centerX, centerY, false, 4, 2);
        obj.points.push(centerPoint);
        // Connect outer ring
        for (let i = 0; i < segments; i++) {
            const next = (i + 1) % segments;
            this.createConstraint(outerPoints[i], outerPoints[next], 1.0, true);
            obj.constraints.push(this.constraints[this.constraints.length - 1]);
        }
        // Connect each outer point to center
        for (let point of outerPoints) {
            this.createConstraint(point, centerPoint, 0.8, false);
            obj.constraints.push(this.constraints[this.constraints.length - 1]);
        }
        // Add cross-bracing for stability
        for (let i = 0; i < segments; i++) {
            const opposite = (i + Math.floor(segments / 2)) % segments;
            this.createConstraint(outerPoints[i], outerPoints[opposite], 0.3, false);
            obj.constraints.push(this.constraints[this.constraints.length - 1]);
        }
    }
    createBall(obj, x, y, params) {
        const radius = params?.radius ?? 15;
        const point = this.createPoint(x, y, false, radius, 2);
        obj.points.push(point);
    }
    step(dt = types_js_1.CONSTANTS.DT) {
        if (this.paused)
            return;
        // Sub-stepping for stability
        const subSteps = types_js_1.CONSTANTS.SUBSTEPS;
        const subDt = dt / subSteps;
        for (let s = 0; s < subSteps; s++) {
            this.integrate(subDt);
            this.solveConstraints();
            this.applyCollisions();
        }
    }
    integrate(dt) {
        const dt2 = dt * dt;
        for (const point of this.points) {
            if (point.pinned)
                continue;
            const vx = (point.x - point.px) * types_js_1.CONSTANTS.DAMPING;
            const vy = (point.y - point.py) * types_js_1.CONSTANTS.DAMPING;
            point.px = point.x;
            point.py = point.y;
            point.x += vx + 0 * dt2; // No horizontal gravity
            point.y += vy + this.gravity * dt2;
        }
    }
    solveConstraints() {
        for (let iter = 0; iter < this.iterations; iter++) {
            // Solve distance constraints
            for (const c of this.constraints) {
                const dx = c.p2.x - c.p1.x;
                const dy = c.p2.y - c.p1.y;
                const distSq = dx * dx + dy * dy;
                if (distSq < 1e-12)
                    continue; // Skip zero-length
                const dist = Math.sqrt(distSq);
                const diff = (dist - c.length) / dist * c.stiffness;
                const offsetX = dx * diff * 0.5;
                const offsetY = dy * diff * 0.5;
                if (!c.p1.pinned) {
                    c.p1.x += offsetX;
                    c.p1.y += offsetY;
                }
                if (!c.p2.pinned) {
                    c.p2.x -= offsetX;
                    c.p2.y -= offsetY;
                }
            }
            // Pin pinned points back (important for stability)
            for (const point of this.points) {
                if (point.pinned) {
                    // These stay exactly where they were set
                }
            }
        }
    }
    applyCollisions() {
        const canvasWidth = window.innerWidth;
        const canvasHeight = window.innerHeight;
        for (const point of this.points) {
            if (point.pinned)
                continue;
            let vx = point.x - point.px;
            let vy = point.y - point.py;
            // Static body collisions
            for (const body of this.staticBodies) {
                if (body.type === 'floor') {
                    const floorY = body.y;
                    if (point.y + point.radius > floorY) {
                        const overlap = point.y + point.radius - floorY;
                        point.y = floorY - point.radius;
                        point.px = point.x - vx * types_js_1.CONSTANTS.BOUNCE;
                        vx = vx * types_js_1.CONSTANTS.FRICTION;
                    }
                }
                else if (body.type === 'wall' && body.x !== undefined && body.height !== undefined) {
                    const left = body.x;
                    const right = body.x + (body.width ?? 0);
                    const bottom = body.y + body.height;
                    if (point.x + point.radius > left && point.x - point.radius < right) {
                        if (point.y + point.radius > body.y) {
                            if (point.x < left + point.radius) {
                                point.x = left + point.radius;
                                vx = -vx * types_js_1.CONSTANTS.BOUNCE;
                            }
                            if (point.x > right - point.radius) {
                                point.x = right - point.radius;
                                vx = -vx * types_js_1.CONSTANTS.BOUNCE;
                            }
                        }
                    }
                }
                else if (body.type === 'circle' && body.radius !== undefined) {
                    const dx = point.x - body.x;
                    const dy = point.y - body.y;
                    const dist = Math.sqrt(dx * dx + dy * dy);
                    const minDist = body.radius + point.radius;
                    if (dist < minDist && dist > 0) {
                        const nx = dx / dist;
                        const ny = dy / dist;
                        point.x = body.x + nx * minDist;
                        point.y = body.y + ny * minDist;
                        // Reflect velocity
                        const dot = vx * nx + vy * ny;
                        vx = (vx - 2 * dot * nx) * types_js_1.CONSTANTS.BOUNCE;
                        vy = (vy - 2 * dot * ny) * types_js_1.CONSTANTS.BOUNCE;
                        point.px = point.x - vx * 0.9;
                        point.py = point.y - vy * 0.9;
                    }
                }
                else if (body.type === 'box' && body.width != null && body.height != null) {
                    const left = body.x;
                    const right = body.x + body.width;
                    const top = body.y;
                    const bottom = body.y + body.height;
                    // Find closest point on box
                    let closestX = (0, types_js_1.clamp)(point.x, left, right);
                    let closestY = (0, types_js_1.clamp)(point.y, top, bottom);
                    const dx = point.x - closestX;
                    const dy = point.y - closestY;
                    const dist = Math.sqrt(dx * dx + dy * dy);
                    if (dist < point.radius && dist > 0) {
                        const nx = dx / dist;
                        const ny = dy / dist;
                        point.x = closestX + nx * point.radius;
                        point.y = closestY + ny * point.radius;
                        const dot = vx * nx + vy * ny;
                        vx = (vx - 2 * dot * nx) * types_js_1.CONSTANTS.BOUNCE;
                        vy = (vy - 2 * dot * ny) * types_js_1.CONSTANTS.BOUNCE;
                        point.px = point.x - vx * 0.9;
                        point.py = point.y - vy * 0.9;
                    }
                }
            }
            // Self-collision for soft bodies (simplified)
            // Only check points from the same object
            point.px = point.x - vx;
            point.py = point.y - vy;
        }
    }
    tearConstraintNear(x, y, maxDistance = types_js_1.CONSTANTS.TEAR_DISTANCE) {
        const torn = [];
        const maxDistSq = maxDistance * maxDistance;
        for (let i = this.constraints.length - 1; i >= 0; i--) {
            const c = this.constraints[i];
            // Check if constraint is near the click point
            const midX = (c.p1.x + c.p2.x) / 2;
            const midY = (c.p1.y + c.p2.y) / 2;
            const distSq = (midX - x) ** 2 + (midY - y) ** 2;
            if (distSq <= maxDistSq) {
                // Check if this constraint belongs to any of our objects
                const objIndex = this.objects.findIndex(obj => obj.constraints.includes(c));
                if (objIndex >= 0) {
                    const obj = this.objects[objIndex];
                    obj.constraints.splice(obj.constraints.indexOf(c), 1);
                    torn.push(c);
                }
                this.constraints.splice(i, 1);
            }
        }
        return torn;
    }
    findNearestPoint(x, y, radius = types_js_1.CONSTANTS.GRAB_RADIUS) {
        let nearest = null;
        let nearestDistSq = radius * radius;
        for (const point of this.points) {
            const distSq = (point.x - x) ** 2 + (point.y - y) ** 2;
            if (distSq < nearestDistSq) {
                nearestDistSq = distSq;
                nearest = point;
            }
        }
        return nearest;
    }
    togglePin(point) {
        point.pinned = !point.pinned;
    }
    removeObject(obj) {
        // Remove all points
        for (const point of obj.points) {
            const idx = this.points.indexOf(point);
            if (idx >= 0)
                this.points.splice(idx, 1);
        }
        // Remove all constraints
        for (const constraint of obj.constraints) {
            const idx = this.constraints.indexOf(constraint);
            if (idx >= 0)
                this.constraints.splice(idx, 1);
        }
        // Remove object
        const idx = this.objects.indexOf(obj);
        if (idx >= 0)
            this.objects.splice(idx, 1);
    }
    clearAll() {
        this.points = [];
        this.constraints = [];
        this.objects = [];
        this.nextPointId = 0;
        this.nextConstraintId = 0;
        this.nextObjectId = 0;
    }
    getKineticEnergy() {
        let energy = 0;
        for (const point of this.points) {
            const vx = point.x - point.px;
            const vy = point.y - point.py;
            energy += 0.5 * point.mass * (vx * vx + vy * vy);
        }
        return energy;
    }
    getMinY() {
        let minY = Infinity;
        for (const point of this.points) {
            if (point.y < minY)
                minY = point.y;
        }
        return minY;
    }
}
exports.PhysicsEngine = PhysicsEngine;
};

// ── module: src/renderer.ts ──
__mods["src/renderer.ts"] = function (exports, require, module) {
"use strict";
// ============== RENDERER ==============
Object.defineProperty(exports, "__esModule", { value: true });
exports.Renderer = void 0;
const types_js_1 = require("./types.js");
class Renderer {
    constructor(canvas) {
        this.showStress = true;
        this.showPoints = false;
        this.trailLength = 10;
        this.canvas = canvas;
        this.ctx = canvas.getContext('2d');
        this.resize();
        window.addEventListener('resize', () => this.resize());
    }
    resize() {
        this.canvas.width = window.innerWidth;
        this.canvas.height = window.innerHeight;
    }
    render(sim, draggedPoint, hoveredPoint, tearStart) {
        const ctx = this.ctx;
        const width = this.canvas.width;
        const height = this.canvas.height;
        // Clear background
        ctx.fillStyle = '#1a1a2e';
        ctx.fillRect(0, 0, width, height);
        // Draw static bodies
        this.drawStaticBodies(sim);
        // Draw objects
        for (const obj of sim.objects) {
            switch (obj.type) {
                case 'rope':
                case 'softbody':
                    this.drawObjectWithConstraints(sim, obj, draggedPoint);
                    break;
                case 'cloth':
                    this.drawCloth(sim, obj, draggedPoint);
                    break;
                case 'ball':
                    this.drawBall(sim, obj, draggedPoint);
                    break;
            }
        }
        // Draw tear line if active
        if (tearStart) {
            ctx.strokeStyle = '#ff4444';
            ctx.lineWidth = 2;
            ctx.setLineDash([5, 5]);
            ctx.beginPath();
            ctx.moveTo(tearStart.x, tearStart.y);
            ctx.lineTo(tearStart.x, tearStart.y);
            ctx.stroke();
            ctx.setLineDash([]);
            // Draw tear radius indicator
            ctx.strokeStyle = 'rgba(255, 68, 68, 0.3)';
            ctx.lineWidth = 1;
            ctx.beginPath();
            ctx.arc(tearStart.x, tearStart.y, types_js_1.CONSTANTS.TEAR_DISTANCE, 0, Math.PI * 2);
            ctx.stroke();
        }
        // Draw drag line if dragging
        if (draggedPoint) {
            ctx.strokeStyle = types_js_1.CONSTANTS.COLORS.GRAB_HIGHLIGHT;
            ctx.lineWidth = 2;
            ctx.beginPath();
            ctx.moveTo(draggedPoint.point.x, draggedPoint.point.y);
            ctx.lineTo(draggedPoint.point.x + draggedPoint.offsetX, draggedPoint.point.y + draggedPoint.offsetY);
            ctx.stroke();
        }
        // Draw hovered point highlight
        if (hoveredPoint && !draggedPoint) {
            ctx.fillStyle = types_js_1.CONSTANTS.COLORS.GRAB_HIGHLIGHT;
            ctx.beginPath();
            ctx.arc(hoveredPoint.x, hoveredPoint.y, types_js_1.CONSTANTS.GRAB_RADIUS, 0, Math.PI * 2);
            ctx.fill();
        }
    }
    drawStaticBodies(sim) {
        const ctx = this.ctx;
        for (const body of sim.staticBodies) {
            ctx.fillStyle = body.color;
            ctx.strokeStyle = '#444466';
            ctx.lineWidth = 2;
            if (body.type === 'floor') {
                ctx.fillRect(0, body.y - 5, window.innerWidth, 20);
                ctx.beginPath();
                ctx.moveTo(0, body.y);
                ctx.lineTo(window.innerWidth, body.y);
                ctx.stroke();
            }
            else if (body.type === 'wall' && body.width && body.height) {
                ctx.fillRect(body.x, body.y, body.width, body.height);
                ctx.strokeRect(body.x, body.y, body.width, body.height);
            }
            else if (body.type === 'circle' && body.radius) {
                ctx.beginPath();
                ctx.arc(body.x, body.y, body.radius, 0, Math.PI * 2);
                ctx.fill();
                ctx.stroke();
            }
            else if (body.type === 'box' && body.width && body.height) {
                ctx.fillRect(body.x, body.y, body.width, body.height);
                ctx.strokeRect(body.x, body.y, body.width, body.height);
            }
        }
    }
    drawObjectWithConstraints(sim, obj, draggedPoint) {
        const ctx = this.ctx;
        // Draw constraints
        for (const c of obj.constraints) {
            if (c.visible === false)
                continue;
            const stretch = (0, types_js_1.distance)(c.p1, c.p2) / c.length;
            let strokeColor = types_js_1.CONSTANTS.COLORS.CONSTRAINT;
            if (this.showStress && stretch > 1.05) {
                const stressRatio = Math.min((stretch - 1) / 0.5, 1);
                strokeColor = this.mixColors(types_js_1.CONSTANTS.COLORS.CONSTRAINT_NORMAL, types_js_1.CONSTANTS.COLORS.CONSTRAINT_STRESSED, stressRatio);
            }
            ctx.strokeStyle = strokeColor;
            ctx.lineWidth = 2;
            ctx.beginPath();
            ctx.moveTo(c.p1.x, c.p1.y);
            ctx.lineTo(c.p2.x, c.p2.y);
            ctx.stroke();
        }
        // Draw points
        if (this.showPoints || obj.type === 'rope') {
            for (const point of obj.points) {
                ctx.fillStyle = point.pinned ? types_js_1.CONSTANTS.COLORS.PINNED : obj.color;
                ctx.beginPath();
                ctx.arc(point.x, point.y, point.radius + 1, 0, Math.PI * 2);
                ctx.fill();
            }
        }
        // Fill soft body
        if (obj.type === 'softbody') {
            ctx.fillStyle = obj.color + '40'; // 25% alpha
            ctx.beginPath();
            ctx.moveTo(obj.points[0].x, obj.points[0].y);
            for (let i = 1; i < obj.points.length - 1; i++) {
                ctx.lineTo(obj.points[i].x, obj.points[i].y);
            }
            ctx.closePath();
            ctx.fill();
        }
    }
    drawCloth(sim, obj, draggedPoint) {
        const ctx = this.ctx;
        // Draw filled cloth mesh
        const points = obj.points;
        const cols = Math.round(Math.sqrt(points.length * 2));
        // Create a filled polygon from the cloth
        if (points.length > 2) {
            ctx.fillStyle = obj.color + '60'; // 40% alpha
            ctx.beginPath();
            // Top edge left to right
            for (let c = 0; c < cols; c++) {
                if (c < points.length) {
                    ctx.lineTo(points[c].x, points[c].y);
                }
            }
            // Right edge top to bottom
            for (let r = 1; r < Math.ceil(points.length / cols); r++) {
                const idx = r * cols + (cols - 1);
                if (idx < points.length) {
                    ctx.lineTo(points[idx].x, points[idx].y);
                }
            }
            // Bottom edge right to left
            const rows = Math.ceil(points.length / cols);
            for (let c = cols - 2; c >= 0; c--) {
                const idx = (rows - 1) * cols + c;
                if (idx >= 0 && idx < points.length) {
                    ctx.lineTo(points[idx].x, points[idx].y);
                }
            }
            // Left edge bottom to top
            for (let r = rows - 2; r >= 1; r--) {
                const idx = r * cols;
                if (idx >= 0 && idx < points.length) {
                    ctx.lineTo(points[idx].x, points[idx].y);
                }
            }
            ctx.closePath();
            ctx.fill();
        }
        // Draw grid lines
        ctx.strokeStyle = obj.color + '80';
        ctx.lineWidth = 1;
        ctx.beginPath();
        // Horizontal lines
        const rows = Math.ceil(points.length / cols);
        for (let r = 0; r < rows; r++) {
            const startIdx = r * cols;
            if (startIdx < points.length) {
                ctx.moveTo(points[startIdx].x, points[startIdx].y);
                for (let c = 1; c < cols; c++) {
                    const idx = startIdx + c;
                    if (idx < points.length) {
                        ctx.lineTo(points[idx].x, points[idx].y);
                    }
                }
            }
        }
        // Vertical lines
        for (let c = 0; c < cols; c++) {
            ctx.moveTo(points[c].x, points[c].y);
            for (let r = 1; r < rows; r++) {
                const idx = r * cols + c;
                if (idx < points.length) {
                    ctx.lineTo(points[idx].x, points[idx].y);
                }
            }
        }
        ctx.stroke();
        // Draw pinned points
        for (const point of obj.points) {
            if (point.pinned) {
                ctx.fillStyle = types_js_1.CONSTANTS.COLORS.PINNED;
                ctx.beginPath();
                ctx.arc(point.x, point.y, 5, 0, Math.PI * 2);
                ctx.fill();
            }
        }
    }
    drawBall(sim, obj, draggedPoint) {
        const ctx = this.ctx;
        const point = obj.points[0];
        // Ball body
        const gradient = ctx.createRadialGradient(point.x - point.radius / 3, point.y - point.radius / 3, point.radius / 4, point.x, point.y, point.radius);
        gradient.addColorStop(0, '#ffffff');
        gradient.addColorStop(0.3, obj.color);
        gradient.addColorStop(1, this.darkenColor(obj.color, 0.3));
        ctx.fillStyle = gradient;
        ctx.beginPath();
        ctx.arc(point.x, point.y, point.radius, 0, Math.PI * 2);
        ctx.fill();
        // Highlight
        ctx.strokeStyle = types_js_1.CONSTANTS.COLORS.GRAB_HIGHLIGHT;
        ctx.lineWidth = 2;
        ctx.beginPath();
        ctx.arc(point.x, point.y, point.radius, 0, Math.PI * 2);
        ctx.stroke();
    }
    mixColors(color1, color2, ratio) {
        const r1 = parseInt(color1.slice(1, 3), 16);
        const g1 = parseInt(color1.slice(3, 5), 16);
        const b1 = parseInt(color1.slice(5, 7), 16);
        const r2 = parseInt(color2.slice(1, 3), 16);
        const g2 = parseInt(color2.slice(3, 5), 16);
        const b2 = parseInt(color2.slice(5, 7), 16);
        const r = Math.round(r1 + (r2 - r1) * ratio);
        const g = Math.round(g1 + (g2 - g1) * ratio);
        const b = Math.round(b1 + (b2 - b1) * ratio);
        return `rgb(${r}, ${g}, ${b})`;
    }
    darkenColor(color, amount) {
        const r = parseInt(color.slice(1, 3), 16);
        const g = parseInt(color.slice(3, 5), 16);
        const b = parseInt(color.slice(5, 7), 16);
        const dr = Math.round(r * (1 - amount));
        const dg = Math.round(g * (1 - amount));
        const db = Math.round(b * (1 - amount));
        return `rgb(${dr}, ${dg}, ${db})`;
    }
}
exports.Renderer = Renderer;
};

// ── module: src/input.ts ──
__mods["src/input.ts"] = function (exports, require, module) {
"use strict";
// ============== INPUT HANDLER ==============
Object.defineProperty(exports, "__esModule", { value: true });
exports.InputHandler = void 0;
class InputHandler {
    constructor(sim) {
        this.mode = 'grab';
        this.dragged = null;
        this.tearState = null;
        this.hoveringPoint = null;
        // Spawn creation state
        this.spawning = null;
        this.sim = sim;
    }
    attach(canvas) {
        // Mouse down
        canvas.addEventListener('mousedown', (e) => this.onMouseDown(e));
        // Mouse move
        canvas.addEventListener('mousemove', (e) => this.onMouseMove(e));
        // Mouse up
        canvas.addEventListener('mouseup', (e) => this.onMouseUp(e));
        // Context menu (prevent for right-click tearing)
        canvas.addEventListener('contextmenu', (e) => e.preventDefault());
        // Keyboard
        document.addEventListener('keydown', (e) => this.onKeyDown(e));
        document.addEventListener('keyup', (e) => this.onKeyUp(e));
    }
    onMouseDown(e) {
        const target = e.currentTarget;
        const rect = target.getBoundingClientRect();
        const x = e.clientX - rect.left;
        const y = e.clientY - rect.top;
        // Right-click for tearing
        if (e.button === 2) {
            this.tearState = { startX: x, startY: y, currentX: x, currentY: y };
            return;
        }
        // Find nearest point
        const point = this.sim.findNearestPoint(x, y);
        if (this.mode === 'grab') {
            if (point) {
                this.dragged = {
                    point,
                    startX: point.x,
                    startY: point.y,
                    offsetX: x - point.x,
                    offsetY: y - point.y
                };
            }
            else {
                // Start spawning a rope from empty click
                this.spawning = { type: 'rope', startX: x, startY: y };
            }
        }
        else if (this.mode === 'pin') {
            if (point) {
                this.sim.togglePin(point);
            }
        }
        else if (this.mode === 'tear') {
            this.tearState = { startX: x, startY: y, currentX: x, currentY: y };
        }
    }
    onMouseMove(e) {
        const target = e.currentTarget;
        const rect = target.getBoundingClientRect();
        const x = e.clientX - rect.left;
        const y = e.clientY - rect.top;
        // Update tear state
        if (this.tearState) {
            this.tearState.currentX = x;
            this.tearState.currentY = y;
            // Tear constraints in the drag path
            this.sim.tearConstraintNear(x, y, 15);
        }
        // Update dragged point position
        if (this.dragged) {
            this.dragged.point.x = x;
            this.dragged.point.y = y;
        }
        // Update hovering point
        this.hoveringPoint = this.sim.findNearestPoint(x, y);
        // Update spawning preview
        if (this.spawning) {
            // Just track the mouse for now, will create on mouseup
            this.spawning = { ...this.spawning };
        }
    }
    onMouseUp(e) {
        const target = e.currentTarget;
        const rect = target.getBoundingClientRect();
        const x = e.clientX - rect.left;
        const y = e.clientY - rect.top;
        // Finish spawning
        if (this.spawning) {
            this.spawnObject(this.spawning.type, this.spawning.startX, this.spawning.startY);
            this.spawning = null;
        }
        // Release dragged point with velocity
        if (this.dragged) {
            const point = this.dragged.point;
            const vx = (point.x - this.dragged.startX);
            const vy = (point.y - this.dragged.startY);
            // Set previous position to give throw velocity
            point.px = point.x - vx * 1.5;
            point.py = point.y - vy * 1.5;
            this.dragged = null;
        }
        // Finish tearing
        if (this.tearState) {
            // Final tear at end point
            this.sim.tearConstraintNear(this.tearState.currentX, this.tearState.currentY, 20);
            this.tearState = null;
        }
    }
    spawnObject(type, x, y) {
        const canvas = document.getElementById('canvas');
        const height = canvas.height;
        switch (type) {
            case 'rope':
                // Spawn rope hanging from top, starting near click
                this.sim.createObject('rope', x, Math.max(50, y - 200), {
                    segments: 15,
                    segmentLength: 18
                });
                break;
            case 'cloth':
                // Spawn cloth pinned at top
                this.sim.createObject('cloth', Math.max(50, x - 100), Math.max(30, y - 150), {
                    width: 200,
                    height: 150,
                    spacing: 20
                });
                break;
            case 'softbody':
                // Spawn soft body at click position
                this.sim.createObject('softbody', x, Math.max(100, y - 100), {
                    radius: 50,
                    segments: 10
                });
                break;
            case 'ball':
                // Spawn ball at click
                this.sim.createObject('ball', x, Math.max(50, y - 50), {
                    radius: 20
                });
                break;
        }
    }
    onKeyDown(e) {
        switch (e.code) {
            case 'Space':
                e.preventDefault();
                this.sim.paused = !this.sim.paused;
                this.updatePauseButton();
                break;
            case 'ArrowUp':
            case 'ArrowRight':
                if (this.sim.paused) {
                    this.sim.step();
                }
                break;
        }
    }
    onKeyUp(e) {
        // Reset keys
    }
    updatePauseButton() {
        const btn = document.getElementById('btn-pause');
        if (btn) {
            btn.textContent = this.sim.paused ? 'Play' : 'Pause';
            btn.classList.toggle('active', this.sim.paused);
        }
    }
    setMode(mode) {
        this.mode = mode;
    }
    pause() {
        this.sim.paused = true;
        this.updatePauseButton();
    }
    play() {
        this.sim.paused = false;
        this.updatePauseButton();
    }
    togglePause() {
        if (this.sim.paused) {
            this.play();
        }
        else {
            this.pause();
        }
    }
    step() {
        this.sim.step();
    }
    getDragState() {
        if (!this.dragged)
            return null;
        return {
            point: this.dragged.point,
            offsetX: 0,
            offsetY: 0
        };
    }
    getTealState() {
        if (!this.tearState)
            return null;
        return { x: this.tearState.currentX, y: this.tearState.currentY };
    }
    getHoveredPoint() {
        return this.hoveringPoint;
    }
}
exports.InputHandler = InputHandler;
};

// ── module: src/ui.ts ──
__mods["src/ui.ts"] = function (exports, require, module) {
"use strict";
// ============== UI CONTROLLER ==============
Object.defineProperty(exports, "__esModule", { value: true });
exports.UIController = void 0;
class UIController {
    constructor(sim, input) {
        this.frameCount = 0;
        this.lastTime = performance.now();
        this.sim = sim;
        this.input = input;
        this.fpsEl = document.getElementById('fps');
        this.pointsEl = document.getElementById('points');
        this.gravitySlider = document.getElementById('gravity');
        this.iterationsSlider = document.getElementById('iterations');
        this.modeSelect = document.getElementById('mode');
        this.btnPause = document.getElementById('btn-pause');
        this.btnReset = document.getElementById('btn-reset');
        this.toolbarButtons = new Map();
        this.toolbarButtons.set('rope', document.getElementById('btn-rope'));
        this.toolbarButtons.set('cloth', document.getElementById('btn-cloth'));
        this.toolbarButtons.set('softbody', document.getElementById('btn-softbody'));
        this.toolbarButtons.set('ball', document.getElementById('btn-ball'));
        this.attachListeners();
    }
    attachListeners() {
        // Gravity slider
        this.gravitySlider.addEventListener('input', (e) => {
            const value = parseFloat(e.target.value);
            this.sim.gravity = value;
        });
        // Iterations slider
        this.iterationsSlider.addEventListener('input', (e) => {
            const value = parseInt(e.target.value);
            this.sim.iterations = value;
        });
        // Mode select
        this.modeSelect.addEventListener('change', (e) => {
            const mode = e.target.value;
            this.input.setMode(mode);
            this.updateToolbarHighlight('spawn');
        });
        // Pause button
        this.btnPause.addEventListener('click', () => {
            this.input.togglePause();
        });
        // Reset button
        this.btnReset.addEventListener('click', () => {
            this.sim.clearAll();
            this.sim.setCanvasSize(window.innerWidth, window.innerHeight);
        });
        // Toolbar buttons
        this.toolbarButtons.forEach((btn, type) => {
            btn.addEventListener('click', () => {
                this.selectSpawnType(type);
            });
        });
    }
    selectSpawnType(type) {
        this.updateToolbarHighlight(type);
        this.input.setMode('grab');
        this.modeSelect.value = 'grab';
    }
    updateToolbarHighlight(activeType) {
        this.toolbarButtons.forEach((btn, type) => {
            btn.classList.toggle('active', type === activeType);
        });
    }
    updateStats(fps) {
        this.frameCount++;
        const now = performance.now();
        const delta = now - this.lastTime;
        if (delta >= 1000) {
            this.fpsEl.textContent = `FPS: ${Math.round((this.frameCount * 1000) / delta)}`;
            this.pointsEl.textContent = `Points: ${this.sim.points.length}`;
            this.frameCount = 0;
            this.lastTime = now;
        }
    }
    getSpawnMode() {
        // Check if any toolbar button is active
        for (const [type, btn] of this.toolbarButtons) {
            if (btn.classList.contains('active')) {
                return type;
            }
        }
        return null;
    }
}
exports.UIController = UIController;
};

// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>