← Physics Playground results

Physics Playground

Qwen 3.5 122b a10b · 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 56,633 bytes · SHA-256 4b52af16f10e
<!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', Tahoma, Geneva, Verdana, sans-serif;
  color: #eee;
}

#canvas-container {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  z-index: 1;
}

#sim-canvas {
  display: block;
  width: 100%;
  height: 100%;
  cursor: crosshair;
}

#toolbar {
  position: fixed;
  top: 10px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 100;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 16px;
  background: rgba(0, 0, 0, 0.7);
  border-radius: 12px;
  backdrop-filter: blur(4px);
  flex-wrap: wrap;
  justify-content: center;
  max-width: 95vw;
}

#toolbar button {
  padding: 8px 14px;
  border: none;
  border-radius: 6px;
  background: #3d5afe;
  color: white;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.15s ease;
}

#toolbar button:hover {
  background: #536dfe;
  transform: translateY(-1px);
}

#toolbar button:active {
  transform: translateY(0);
}

#toolbar button.active {
  background: #00c853;
}

#toolbar .separator {
  width: 1px;
  height: 24px;
  background: rgba(255, 255, 255, 0.3);
  margin: 0 4px;
}

#toolbar label {
  display: flex;
  align-items: center;
  gap: 4px;
  font-size: 12px;
  color: #aaa;
}

#toolbar input[type="range"] {
  width: 60px;
  height: 4px;
  -webkit-appearance: none;
  appearance: none;
  background: rgba(255, 255, 255, 0.2);
  border-radius: 2px;
  outline: none;
}

#toolbar input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 12px;
  height: 12px;
  background: #3d5afe;
  border-radius: 50%;
  cursor: pointer;
}

#toolbar input[type="checkbox"] {
  width: 14px;
  height: 14px;
  cursor: pointer;
}

#info-panel {
  position: fixed;
  top: 10px;
  right: 10px;
  z-index: 100;
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 10px 14px;
  background: rgba(0, 0, 0, 0.7);
  border-radius: 8px;
  font-size: 12px;
  font-family: 'Consolas', monospace;
  backdrop-filter: blur(4px);
}

#help-text {
  position: fixed;
  bottom: 10px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 100;
  padding: 6px 16px;
  background: rgba(0, 0, 0, 0.6);
  border-radius: 6px;
  font-size: 11px;
  color: #888;
  backdrop-filter: blur(4px);
}
</style>
</head>
<body>
<div id="canvas-container">
  <canvas id="sim-canvas"></canvas>
</div>
<div id="toolbar">
  <button id="btn-rope" title="Draw Rope">Rope</button>
  <button id="btn-cloth" title="Draw Cloth">Cloth</button>
  <button id="btn-softbody" title="Spawn Soft Body">Soft Body</button>
  <button id="btn-ball" title="Spawn Ball">Ball</button>
  <div class="separator"></div>
  <button id="btn-pause" title="Pause (Space)">⏸</button>
  <button id="btn-step" title="Step (→)">⏯</button>
  <button id="btn-reset" title="Reset">↻</button>
  <div class="separator"></div>
  <label>Gravity: <input type="range" id="gravity-slider" min="-20" max="50" value="20"></label>
  <span id="gravity-value">20</span>
  <label>Iterations: <input type="range" id="iterations-slider" min="1" max="20" value="8"></label>
  <span id="iter-value">8</span>
  <label><input type="checkbox" id="stress-viz"> Stress</label>
  <label><input type="checkbox" id="trail-viz"> Trails</label>
</div>
<div id="info-panel">
  <div id="fps-counter">FPS: --</div>
  <div id="step-time">Step: -- ms</div>
  <div id="point-count">Points: 0</div>
</div>
<div id="help-text">
  Left-drag: Grab | Right-drag: Tear | Ctrl+Click: Pin/Unpin
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./config":"src/config.ts","./types":"src/types.ts","./physics":"src/physics.ts","./objects":"src/objects.ts","./render":"src/render.ts"},"src/types.ts":{"./config":"src/config.ts"},"src/physics.ts":{"./config":"src/config.ts","./types":"src/types.ts"},"src/objects.ts":{"./config":"src/config.ts","./types":"src/types.ts"},"src/render.ts":{"./config":"src/config.ts","./types":"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 - Physics Playground
 */
Object.defineProperty(exports, "__esModule", { value: true });
const config_1 = require("./config");
const types_1 = require("./types");
const physics_1 = require("./physics");
const objects_1 = require("./objects");
const render_1 = require("./render");
// Global simulation state (exposed on window for testing)
let sim = null;
/**
 * Initialize the simulation.
 */
function initSimulation() {
    const canvas = document.getElementById('sim-canvas');
    const ctx = canvas.getContext('2d');
    // Set canvas size to window size
    function resizeCanvas() {
        canvas.width = window.innerWidth;
        canvas.height = window.innerHeight;
        if (sim) {
            sim.width = canvas.width;
            sim.height = canvas.height;
            // Re-create boundary walls on resize
            sim.staticShapes = (0, objects_1.createBoundaryWalls)(canvas.width, canvas.height);
        }
    }
    resizeCanvas();
    window.addEventListener('resize', resizeCanvas);
    // Create initial objects
    const points = [];
    const constraints = [];
    const staticShapes = (0, objects_1.createBoundaryWalls)(canvas.width, canvas.height);
    const objects = [];
    // Add a rope
    const rope = (0, objects_1.createRope)(150, 100, 20, true);
    objects.push(rope);
    // Add cloth
    const cloth = (0, objects_1.createCloth)(400, 80, 14, 10, 'corners');
    objects.push(cloth);
    // Add soft body
    const softbody = (0, objects_1.createSoftBody)(700, 200, 50, 14);
    objects.push(softbody);
    // Add some balls
    const ball1 = (0, objects_1.createBall)(300, 100);
    const ball2 = (0, objects_1.createBall)(350, 100);
    objects.push(ball1, ball2);
    // Collect all points and constraints from objects
    for (const obj of objects) {
        if (obj.type === 'rope' || obj.type === 'cloth' || obj.type === 'softbody') {
            for (const p of obj.points)
                points.push(p);
            for (const c of obj.constraints)
                constraints.push(c);
        }
        else if (obj.type === 'ball') {
            points.push(obj.point);
        }
    }
    // Add a static circle obstacle
    staticShapes.push((0, objects_1.createStaticCircle)(canvas.width / 2, canvas.height - 150, 60));
    const state = {
        points,
        constraints,
        staticShapes,
        objects,
        grabbedPoint: null,
        grabOffset: { x: 0, y: 0 },
        isTearing: false,
        tearStart: null,
        paused: false,
        gravity: config_1.CONFIG.GRAVITY_DEFAULT,
        iterations: 8,
        showStress: false,
        showTrails: false,
        accumulator: 0,
        lastFrameTime: performance.now(),
        fps: 60,
        frameCount: 0,
        stepTime: 0,
        canvas,
        ctx,
        width: canvas.width,
        height: canvas.height,
    };
    return state;
}
/**
 * Find nearest point within grab radius.
 */
function findNearestPoint(state, x, y) {
    let nearest = null;
    let minDist = config_1.CONFIG.GRAB_RADIUS;
    for (const p of state.points) {
        const dist = types_1.Vec.dist({ x, y }, { x: p.x, y: p.y });
        if (dist < minDist) {
            minDist = dist;
            nearest = p;
        }
    }
    return nearest;
}
/**
 * Toggle pin on a point.
 */
function togglePin(state, point) {
    point.pinned = !point.pinned;
}
/**
 * Update physics with fixed timestep.
 */
function updatePhysics(state, dt) {
    // Clamp delta time to prevent spiral of death after tab switch
    dt = Math.min(dt, config_1.CONFIG.MAX_DELTA);
    state.accumulator += dt;
    // Maximum substeps to prevent spiral of death
    const maxSubsteps = config_1.CONFIG.MAX_SUBSTEPS;
    let substeps = 0;
    while (state.accumulator >= config_1.CONFIG.TIMESTEP && substeps < maxSubsteps) {
        const stepStart = performance.now();
        // Integrate positions
        (0, physics_1.integratePoints)(state, config_1.CONFIG.TIMESTEP);
        // Solve constraints multiple times for stability
        (0, physics_1.solveConstraints)(state);
        // Resolve collisions
        (0, physics_1.resolveCollisions)(state);
        state.stepTime = performance.now() - stepStart;
        state.accumulator -= config_1.CONFIG.TIMESTEP;
        substeps++;
    }
}
/**
 * Render the simulation.
 */
function render(state) {
    const ctx = state.ctx;
    const width = state.width;
    const height = state.height;
    // Clear canvas
    (0, render_1.clearCanvas)(ctx, width, height);
    // Draw trails first (faded)
    if (state.showTrails) {
        (0, render_1.drawTrails)(ctx, state.points);
    }
    // Draw static shapes
    (0, render_1.drawStaticShapes)(ctx, state.staticShapes);
    // Draw constraints
    (0, render_1.drawConstraints)(ctx, state.constraints, state.showStress, state.grabbedPoint);
    // Draw objects (triangles/meshes)
    for (const obj of state.objects) {
        if (obj.type === 'cloth') {
            (0, render_1.drawCloth)(ctx, obj);
        }
        else if (obj.type === 'softbody') {
            (0, render_1.drawSoftBody)(ctx, obj);
        }
    }
    // Draw points
    (0, render_1.drawPoints)(ctx, state.points, state.grabbedPoint);
    // Draw balls separately (they have custom radius)
    for (const obj of state.objects) {
        if (obj.type === 'ball') {
            (0, render_1.drawBall)(ctx, obj);
        }
    }
    // Draw tear indicator
    if (state.isTearing && state.tearStart) {
        const mouse = getMousePosition(state);
        (0, render_1.drawTearIndicator)(ctx, state.tearStart, mouse);
    }
    // Draw grab highlight
    if (state.grabbedPoint) {
        (0, render_1.drawGrabHighlight)(ctx, state.grabbedPoint.x, state.grabbedPoint.y);
    }
}
/**
 * Get current mouse position relative to canvas.
 */
function getMousePosition(state) {
    const rect = state.canvas.getBoundingClientRect();
    return {
        x: pointerState.x - rect.left,
        y: pointerState.y - rect.top,
    };
}
// Mouse/pointer state
const pointerState = {
    x: 0,
    y: 0,
    isDown: false,
    isRightDown: false,
    isShiftDown: false,
    isCtrlDown: false,
};
// Spawn mode
let spawnMode = 'none';
let spawnStart = null;
/**
 * Handle mouse down event.
 */
function handleMouseDown(e, state) {
    const rect = state.canvas.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const y = e.clientY - rect.top;
    pointerState.x = e.clientX;
    pointerState.y = e.clientY;
    pointerState.isCtrlDown = e.ctrlKey || e.metaKey;
    if (e.button === 2) {
        // Right click - start tearing
        pointerState.isRightDown = true;
        state.isTearing = true;
        state.tearStart = { x, y };
        return;
    }
    if (e.button === 0) {
        // Left click
        if (spawnMode !== 'none') {
            // Creating a new object
            spawnStart = { x, y };
            pointerState.isDown = true;
            return;
        }
        // Try to grab a point
        const nearest = findNearestPoint(state, x, y);
        if (nearest) {
            if (pointerState.isCtrlDown) {
                // Toggle pin
                togglePin(state, nearest);
            }
            else {
                state.grabbedPoint = nearest;
                state.grabOffset = { x: x - nearest.x, y: y - nearest.y };
                pointerState.isDown = true;
            }
        }
    }
}
/**
 * Handle mouse move event.
 */
function handleMouseMove(e, state) {
    const rect = state.canvas.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const y = e.clientY - rect.top;
    pointerState.x = e.clientX;
    pointerState.y = e.clientY;
    if (spawnMode !== 'none' && spawnStart && pointerState.isDown) {
        // Drawing preview could go here
        return;
    }
    if (state.isTearing && pointerState.isRightDown) {
        // Tear near current position
        const nearest = findNearestPoint(state, x, y);
        if (nearest) {
            (0, physics_1.tearNearPoint)(state, nearest, config_1.CONFIG.TEAR_DISTANCE);
        }
    }
    if (state.grabbedPoint && pointerState.isDown) {
        // Move grabbed point to cursor position
        state.grabbedPoint.x = x + state.grabOffset.x;
        state.grabbedPoint.y = y + state.grabOffset.y;
        state.grabbedPoint.oldX = state.grabbedPoint.x;
        state.grabbedPoint.y = state.grabbedPoint.y;
    }
}
/**
 * Handle mouse up event.
 */
function handleMouseUp(e, state) {
    if (e.button === 2) {
        pointerState.isRightDown = false;
        state.isTearing = false;
        state.tearStart = null;
    }
    if (e.button === 0) {
        if (spawnMode !== 'none' && spawnStart && pointerState.isDown) {
            // Complete object creation
            createObjectFromSpawn(state, spawnStart, { x: e.clientX - state.canvas.getBoundingClientRect().left, y: e.clientY - state.canvas.getBoundingClientRect().top });
            spawnMode = 'none';
            spawnStart = null;
            updateToolbarButtons();
        }
        pointerState.isDown = false;
        if (state.grabbedPoint) {
            // Throw the point - give it velocity based on drag
            const vx = (state.grabbedPoint.x - state.grabbedPoint.oldX);
            const vy = (state.grabbedPoint.y - state.grabbedPoint.oldY);
            // Apply throw velocity (multiply for more power)
            state.grabbedPoint.oldX = state.grabbedPoint.x - vx * 0.5;
            state.grabbedPoint.oldY = state.grabbedPoint.y - vy * 0.5;
            state.grabbedPoint = null;
        }
    }
}
/**
 * Create object based on spawn mode.
 */
function createObjectFromSpawn(state, start, end) {
    const dx = end.x - start.x;
    const dy = end.y - start.y;
    const length = Math.sqrt(dx * dx + dy * dy);
    if (spawnMode === 'rope') {
        const numSegments = Math.max(5, Math.floor(length / config_1.CONFIG.ROPE_SPACING));
        const rope = (0, objects_1.createRope)(start.x, start.y, numSegments, true);
        state.objects.push(rope);
        for (const p of rope.points)
            state.points.push(p);
        for (const c of rope.constraints)
            state.constraints.push(c);
    }
    else if (spawnMode === 'cloth') {
        const cols = Math.max(4, Math.floor(Math.abs(dx) / config_1.CONFIG.CLOTH_SPACING));
        const rows = Math.max(3, Math.floor(Math.abs(dy) / config_1.CONFIG.CLOTH_SPACING));
        const cloth = (0, objects_1.createCloth)(start.x, start.y, cols, rows, 'corners');
        state.objects.push(cloth);
        for (const p of cloth.points)
            state.points.push(p);
        for (const c of cloth.constraints)
            state.constraints.push(c);
    }
    else if (spawnMode === 'softbody') {
        const radius = Math.max(20, length / 2);
        const centerX = start.x + dx / 2;
        const centerY = start.y + dy / 2;
        const softbody = (0, objects_1.createSoftBody)(centerX, centerY, radius, 12);
        state.objects.push(softbody);
        for (const p of softbody.points)
            state.points.push(p);
        for (const c of softbody.constraints)
            state.constraints.push(c);
    }
    else if (spawnMode === 'ball') {
        const ball = (0, objects_1.createBall)(start.x, start.y);
        state.objects.push(ball);
        state.points.push(ball.point);
    }
}
/**
 * Update toolbar button states.
 */
function updateToolbarButtons() {
    for (const btn of document.querySelectorAll('#toolbar button')) {
        btn.classList.remove('active');
    }
    if (spawnMode !== 'none') {
        const btnId = `btn-${spawnMode}`;
        const btn = document.getElementById(btnId);
        if (btn)
            btn.classList.add('active');
    }
}
/**
 * Game loop.
 */
function gameLoop(currentTime) {
    if (!sim)
        return;
    const dt = (currentTime - sim.lastFrameTime) / 1000;
    sim.lastFrameTime = currentTime;
    // Calculate FPS
    sim.frameCount++;
    if (sim.frameCount % 30 === 0) {
        sim.fps = Math.round(1 / dt);
        updateInfoDisplay();
    }
    if (!sim.paused) {
        updatePhysics(sim, dt);
    }
    render(sim);
    requestAnimationFrame(gameLoop);
}
/**
 * Update info panel display.
 */
function updateInfoDisplay() {
    if (!sim)
        return;
    const fpsEl = document.getElementById('fps-counter');
    const stepEl = document.getElementById('step-time');
    const countEl = document.getElementById('point-count');
    if (fpsEl)
        fpsEl.textContent = `FPS: ${sim.fps}`;
    if (stepEl)
        stepEl.textContent = `Step: ${sim.stepTime.toFixed(2)} ms`;
    if (countEl)
        countEl.textContent = `Points: ${sim.points.length}`;
}
/**
 * Reset the simulation.
 */
function resetSimulation() {
    sim = initSimulation();
    setupEventListeners(sim);
    setupUIListeners(sim);
}
/**
 * Setup event listeners.
 */
function setupEventListeners(state) {
    const canvas = state.canvas;
    canvas.addEventListener('mousedown', (e) => handleMouseDown(e, state));
    canvas.addEventListener('mousemove', (e) => handleMouseMove(e, state));
    canvas.addEventListener('mouseup', (e) => handleMouseUp(e, state));
    canvas.addEventListener('contextmenu', (e) => e.preventDefault());
    // Keyboard controls
    document.addEventListener('keydown', (e) => {
        if (e.code === 'Space') {
            state.paused = !state.paused;
            const btn = document.getElementById('btn-pause');
            if (btn)
                btn.textContent = state.paused ? '▶' : '⏸';
        }
        else if (e.code === 'ArrowRight') {
            // Step one frame when paused
            if (state.paused) {
                updatePhysics(state, config_1.CONFIG.TIMESTEP);
                render(state);
            }
        }
        else if (e.code === 'KeyR') {
            resetSimulation();
        }
    });
}
/**
 * Setup UI control listeners.
 */
function setupUIListeners(state) {
    // Toolbar buttons
    document.getElementById('btn-rope').addEventListener('click', () => {
        spawnMode = spawnMode === 'rope' ? 'none' : 'rope';
        updateToolbarButtons();
    });
    document.getElementById('btn-cloth').addEventListener('click', () => {
        spawnMode = spawnMode === 'cloth' ? 'none' : 'cloth';
        updateToolbarButtons();
    });
    document.getElementById('btn-softbody').addEventListener('click', () => {
        spawnMode = spawnMode === 'softbody' ? 'none' : 'softbody';
        updateToolbarButtons();
    });
    document.getElementById('btn-ball').addEventListener('click', () => {
        spawnMode = spawnMode === 'ball' ? 'none' : 'ball';
        updateToolbarButtons();
    });
    document.getElementById('btn-pause').addEventListener('click', () => {
        state.paused = !state.paused;
        const btn = document.getElementById('btn-pause');
        btn.textContent = state.paused ? '▶' : '⏸';
    });
    document.getElementById('btn-step').addEventListener('click', () => {
        state.paused = true;
        updatePhysics(state, config_1.CONFIG.TIMESTEP);
        render(state);
        const btn = document.getElementById('btn-pause');
        btn.textContent = '⏸';
    });
    document.getElementById('btn-reset').addEventListener('click', () => {
        resetSimulation();
    });
    // Sliders
    const gravitySlider = document.getElementById('gravity-slider');
    const gravityValue = document.getElementById('gravity-value');
    gravitySlider.addEventListener('input', () => {
        state.gravity = parseFloat(gravitySlider.value);
        gravityValue.textContent = gravitySlider.value;
    });
    const iterSlider = document.getElementById('iterations-slider');
    const iterValue = document.getElementById('iter-value');
    iterSlider.addEventListener('input', () => {
        state.iterations = parseInt(iterSlider.value);
        iterValue.textContent = iterSlider.value;
    });
    // Checkboxes
    document.getElementById('stress-viz').addEventListener('change', (e) => {
        state.showStress = e.target.checked;
    });
    document.getElementById('trail-viz').addEventListener('change', (e) => {
        state.showTrails = e.target.checked;
    });
}
/**
 * Main initialization.
 */
function main() {
    sim = initSimulation();
    setupEventListeners(sim);
    setupUIListeners(sim);
    // Expose simulation on window for testing
    window.sim = {
        get state() { return sim; },
        step() {
            if (sim) {
                updatePhysics(sim, config_1.CONFIG.TIMESTEP);
                render(sim);
            }
        },
        reset() { resetSimulation(); },
        pause() { if (sim)
            sim.paused = true; },
        resume() { if (sim)
            sim.paused = false; },
        addRope(x, y, len = 15) {
            if (!sim)
                return;
            const rope = (0, objects_1.createRope)(x, y, len, true);
            sim.objects.push(rope);
            for (const p of rope.points)
                sim.points.push(p);
            for (const c of rope.constraints)
                sim.constraints.push(c);
        },
        addCloth(x, y, cols = 10, rows = 6) {
            if (!sim)
                return;
            const cloth = (0, objects_1.createCloth)(x, y, cols, rows, 'corners');
            sim.objects.push(cloth);
            for (const p of cloth.points)
                sim.points.push(p);
            for (const c of cloth.constraints)
                sim.constraints.push(c);
        },
        addSoftBody(x, y, radius = 40) {
            if (!sim)
                return;
            const sb = (0, objects_1.createSoftBody)(x, y, radius, 12);
            sim.objects.push(sb);
            for (const p of sb.points)
                sim.points.push(p);
            for (const c of sb.constraints)
                sim.constraints.push(c);
        },
        addBall(x, y) {
            if (!sim)
                return;
            const ball = (0, objects_1.createBall)(x, y);
            sim.objects.push(ball);
            sim.points.push(ball.point);
        },
        tearPoint(point) {
            if (!sim)
                return;
            (0, physics_1.tearNearPoint)(sim, point, 50);
        },
        // Debug helpers
        getTotalKineticEnergy: () => sim ? (0, physics_1.getTotalKineticEnergy)(sim.points) : 0,
        hasNaNValues: () => sim ? (0, physics_1.hasNaNValues)(sim) : false,
        getMinY: () => sim ? (0, physics_1.getMinY)(sim.points) : Infinity,
    };
    requestAnimationFrame(gameLoop);
}
// Start the simulation
main();
};

// ── module: src/config.ts ──
__mods["src/config.ts"] = function (exports, require, module) {
"use strict";
/**
 * Physics Configuration - Tunable Constants
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.CONFIG = void 0;
exports.CONFIG = {
    // Physics timing
    TIMESTEP: 1 / 60, // Fixed timestep (seconds)
    MAX_SUBSTEPS: 10, // Max substeps per frame to prevent spiral of death
    MAX_DELTA: 0.25, // Maximum delta time clamp (prevents tab-switch explosion)
    // Verlet integration
    DAMPING: 0.95, // Velocity damping (air resistance) - lower = more damping
    GRAVITY_DEFAULT: 20, // Default gravity (pixels/s²)
    // Constraint solving
    ITERATIONS_DEFAULT: 8, // Number of constraint iterations per step
    MIN_ITERATIONS: 1,
    MAX_ITERATIONS: 20,
    // Collision
    FRICTION: 0.8, // Friction coefficient on collision
    BOUNCE: 0.15, // Restitution (bounciness) - lower = less bouncy
    WALL_BOUNCE: 0.2,
    // Interaction
    GRAB_RADIUS: 25, // Radius for grabbing points
    TEAR_DISTANCE: 40, // Distance threshold for tearing when right-dragging
    TEAR_FORCE_THRESHOLD: 3, // Force multiplier to tear a constraint
    // Rendering
    POINT_RADIUS: 3,
    CONSTRAINT_WIDTH: 1,
    STRESS_COLOR_MIN: [0, 255, 0], // Green for no stress
    STRESS_COLOR_MAX: [255, 0, 0], // Red for max stress
    TRAIL_LENGTH: 5, // Trail history length
    TRAIL_FADE: 0.15, // Trail fade factor
    // Object defaults
    ROPE_SPACING: 15,
    CLOTH_SPACING: 18,
    SOFTBODY_RADIUS: 40,
    BALL_RADIUS: 12,
    // Colors (HSL values for vibrant materials)
    COLORS: {
        ROPE: [220, 80, 60], // Blue-ish
        CLOTH: [160, 70, 55], // Cyan-ish
        SOFTBODY: [280, 75, 60], // Purple-ish
        BALL: [45, 90, 60], // Yellow-ish
        PINNED: [0, 0, 80], // Dark gray
        CONSTRAINT: [200, 30, 40], // Dim red
        STATIC: [100, 10, 30], // Very dim
        GRABBED: [120, 100, 70], // Bright green
        TEARING: [0, 100, 70], // Bright cyan
    },
    // Canvas
    CANVAS_BG: '#1a1a2e',
    WALL_COLOR: 'rgba(100, 100, 150, 0.3)',
};
};

// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
/**
 * Type Definitions for the Physics Engine
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.Vec = void 0;
exports.createPoint = createPoint;
exports.createConstraint = createConstraint;
const config_1 = require("./config");
/** Helper to create a point */
function createPoint(x, y, pinned = false) {
    return {
        x, y,
        oldX: x, oldY: y,
        pinned,
        radius: config_1.CONFIG.POINT_RADIUS,
        mass: 1,
        trail: [],
    };
}
/** Helper to create a constraint */
function createConstraint(p1, p2, visible = true) {
    const dx = p2.x - p1.x;
    const dy = p2.y - p1.y;
    const length = Math.sqrt(dx * dx + dy * dy);
    return {
        p1, p2,
        length: isNaN(length) ? 0 : length,
        stiffness: 1,
        visible,
        currentLength: length,
    };
}
/** Vector operations */
exports.Vec = {
    add: (a, b) => ({ x: a.x + b.x, y: a.y + b.y }),
    sub: (a, b) => ({ x: a.x - b.x, y: a.y - b.y }),
    mul: (v, s) => ({ x: v.x * s, y: v.y * s }),
    dot: (a, b) => a.x * b.x + a.y * b.y,
    len: (v) => Math.sqrt(v.x * v.x + v.y * v.y),
    norm: (v) => {
        const len = Math.sqrt(v.x * v.x + v.y * v.y);
        return len > 0.0001 ? { x: v.x / len, y: v.y / len } : { x: 0, y: 0 };
    },
    dist: (a, b) => {
        const dx = a.x - b.x;
        const dy = a.y - b.y;
        return Math.sqrt(dx * dx + dy * dy);
    },
};
};

// ── module: src/physics.ts ──
__mods["src/physics.ts"] = function (exports, require, module) {
"use strict";
/**
 * Physics Engine - Verlet Integration with Constraint Relaxation
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.integratePoints = integratePoints;
exports.solveConstraints = solveConstraints;
exports.resolveCollisions = resolveCollisions;
exports.tearNearPoint = tearNearPoint;
exports.getTotalKineticEnergy = getTotalKineticEnergy;
exports.hasNaNValues = hasNaNValues;
exports.getMinY = getMinY;
const config_1 = require("./config");
const types_1 = require("./types");
/**
 * Integrate all points using Verlet integration.
 * Formula: x' = x + (x - xPrev) * damping + a * dt²
 */
function integratePoints(state, dt) {
    const gravity = state.gravity;
    const damping = config_1.CONFIG.DAMPING;
    const dt2 = dt * dt;
    const MAX_VELOCITY = 500; // Clamp max velocity to prevent explosion
    for (const p of state.points) {
        if (p.pinned)
            continue;
        // Calculate displacement (this IS velocity * dt, damped)
        let dispX = (p.x - p.oldX) * damping;
        let dispY = (p.y - p.oldY) * damping;
        // Clamp velocity (disp/dt = velocity)
        const velX = dispX / dt;
        const velY = dispY / dt;
        const velSq = velX * velX + velY * velY;
        if (velSq > MAX_VELOCITY * MAX_VELOCITY) {
            const scale = MAX_VELOCITY / Math.sqrt(velSq);
            dispX *= scale;
            dispY *= scale;
        }
        // Store old position BEFORE updating
        const prevX = p.x;
        const prevY = p.y;
        // Update position: x' = x + displacement + gravity * dt²
        p.x += dispX;
        p.y += dispY + gravity * dt2;
        // Update old position for next step
        p.oldX = prevX;
        p.oldY = prevY;
        // Track trail (use prevX/prevY which is the position before this step)
        if (state.showTrails && p.trail.length < config_1.CONFIG.TRAIL_LENGTH) {
            p.trail.push({ x: prevX, y: prevY });
        }
        else if (state.showTrails && p.trail.length >= config_1.CONFIG.TRAIL_LENGTH) {
            p.trail.shift();
            p.trail.push({ x: prevX, y: prevY });
        }
    }
}
/**
 * Solve all distance constraints using relaxation.
 * Iterates N times for stability.
 */
function solveConstraints(state) {
    const iterations = state.iterations;
    const points = state.points;
    const constraints = state.constraints;
    for (let iter = 0; iter < iterations; iter++) {
        for (const c of constraints) {
            const p1 = c.p1;
            const p2 = c.p2;
            // Skip if both points are pinned
            if (p1.pinned && p2.pinned)
                continue;
            const dx = p2.x - p1.x;
            const dy = p2.y - p1.y;
            const distSq = dx * dx + dy * dy;
            // Guard against zero-length or NaN
            if (!isFinite(distSq) || distSq < 0.0001) {
                // Points are coincident - skip to avoid division by zero
                c.currentLength = 0;
                continue;
            }
            const dist = Math.sqrt(distSq);
            c.currentLength = dist;
            // Calculate how much we need to move the points
            const diff = (dist - c.length) / dist;
            const offsetX = dx * diff * 0.5;
            const offsetY = dy * diff * 0.5;
            // Apply stiffness factor
            const stiff = c.stiffness;
            // Move points toward/away from each other
            if (!p1.pinned) {
                p1.x += offsetX;
                p1.y += offsetY;
            }
            if (!p2.pinned) {
                p2.x -= offsetX;
                p2.y -= offsetY;
            }
        }
    }
}
/**
 * Resolve collisions between points and static shapes.
 */
function resolveCollisions(state) {
    const shapes = state.staticShapes;
    const bounce = config_1.CONFIG.BOUNCE;
    const friction = config_1.CONFIG.FRICTION;
    for (const p of state.points) {
        if (p.pinned)
            continue;
        for (const shape of shapes) {
            if (shape.type === 'wall') {
                resolveWallCollision(p, shape, bounce, friction);
            }
            else if (shape.type === 'circle') {
                resolveCircleCollision(p, shape, bounce, friction);
            }
            else if (shape.type === 'box') {
                resolveBoxCollision(p, shape, bounce, friction);
            }
        }
    }
}
/**
 * Resolve collision with a wall (infinite line).
 */
function resolveWallCollision(p, wall, bounce, friction) {
    // Vector from wall point to particle
    const dx = p.x - wall.x;
    const dy = p.y - wall.y;
    // Distance to wall (dot product with normal)
    const dist = dx * wall.nx + dy * wall.ny;
    // Check if on the wrong side of the wall
    if (dist < -p.radius) {
        // Penetration depth
        const penetration = -dist - p.radius;
        // Push out along normal
        p.x += wall.nx * penetration;
        p.y += wall.ny * penetration;
        // Calculate velocity
        const vx = p.x - p.oldX;
        const vy = p.y - p.oldY;
        // Velocity component along normal
        const vn = vx * wall.nx + vy * wall.ny;
        // Only bounce if moving toward the wall
        if (vn < 0) {
            // Tangent vector
            const tx = -wall.ny;
            const ty = wall.nx;
            // Velocity component along tangent
            const vt = vx * tx + vy * ty;
            // Reflect: new velocity = -bounce * vn * n + friction * vt * t
            const newVn = -bounce * vn;
            const newVt = friction * vt;
            // Convert back to world coordinates
            const newVx = newVn * wall.nx + newVt * tx;
            const newVy = newVn * wall.ny + newVt * ty;
            // Update old position to create new velocity
            p.oldX = p.x - newVx * config_1.CONFIG.TIMESTEP;
            p.oldY = p.y - newVy * config_1.CONFIG.TIMESTEP;
        }
    }
}
/**
 * Resolve collision with a circle.
 */
function resolveCircleCollision(p, circle, bounce, friction) {
    const dx = p.x - circle.x;
    const dy = p.y - circle.y;
    const distSq = dx * dx + dy * dy;
    const minDist = circle.radius + p.radius;
    if (distSq < minDist * minDist) {
        const dist = Math.sqrt(distSq);
        // Normal pointing from circle center to particle
        let nx = dx / dist;
        let ny = dy / dist;
        // Handle coincident points
        if (dist < 0.0001) {
            nx = 0;
            ny = -1;
        }
        // Push out
        const penetration = minDist - dist;
        p.x += nx * penetration;
        p.y += ny * penetration;
        // Velocity
        const vx = p.x - p.oldX;
        const vy = p.y - p.oldY;
        // Normal velocity
        const vn = vx * nx + vy * ny;
        if (vn < 0) {
            // Tangent
            const tx = -ny;
            const ty = nx;
            const vt = vx * tx + vy * ty;
            const newVn = -bounce * vn;
            const newVt = friction * vt;
            const newVx = newVn * nx + newVt * tx;
            const newVy = newVn * ny + newVt * ty;
            p.oldX = p.x - newVx * config_1.CONFIG.TIMESTEP;
            p.oldY = p.y - newVy * config_1.CONFIG.TIMESTEP;
        }
    }
}
/**
 * Resolve collision with an axis-aligned box.
 */
function resolveBoxCollision(p, box, bounce, friction) {
    // Find closest point on box to particle
    const closestX = Math.max(box.x, Math.min(p.x, box.x + box.width));
    const closestY = Math.max(box.y, Math.min(p.y, box.y + box.height));
    const dx = p.x - closestX;
    const dy = p.y - closestY;
    const distSq = dx * dx + dy * dy;
    if (distSq < p.radius * p.radius) {
        const dist = Math.sqrt(distSq);
        // Normal from closest point to particle
        let nx = dx / dist;
        let ny = dy / dist;
        // Handle center case
        if (dist < 0.0001) {
            // Particle is inside - push to nearest edge
            const leftDist = p.x - box.x;
            const rightDist = box.x + box.width - p.x;
            const topDist = p.y - box.y;
            const bottomDist = box.y + box.height - p.y;
            const minEdge = Math.min(leftDist, rightDist, topDist, bottomDist);
            if (minEdge === leftDist) {
                nx = -1;
                ny = 0;
            }
            else if (minEdge === rightDist) {
                nx = 1;
                ny = 0;
            }
            else if (minEdge === topDist) {
                nx = 0;
                ny = -1;
            }
            else {
                nx = 0;
                ny = 1;
            }
        }
        // Push out
        const penetration = p.radius - dist;
        p.x += nx * penetration;
        p.y += ny * penetration;
        // Velocity response
        const vx = p.x - p.oldX;
        const vy = p.y - p.oldY;
        const vn = vx * nx + vy * ny;
        if (vn < 0) {
            const tx = -ny;
            const ty = nx;
            const vt = vx * tx + vy * ty;
            const newVn = -bounce * vn;
            const newVt = friction * vt;
            const newVx = newVn * nx + newVt * tx;
            const newVy = newVn * ny + newVt * ty;
            p.oldX = p.x - newVx * config_1.CONFIG.TIMESTEP;
            p.oldY = p.y - newVy * config_1.CONFIG.TIMESTEP;
        }
    }
}
/**
 * Tear constraints that are being stretched beyond threshold.
 * Called during right-drag tearing interaction.
 */
function tearNearPoint(state, point, tearRadius) {
    const constraints = state.constraints;
    const toRemove = [];
    for (const c of constraints) {
        // Check if either endpoint is near the tear point
        const dist1 = types_1.Vec.dist(point, { x: c.p1.x, y: c.p1.y });
        const dist2 = types_1.Vec.dist(point, { x: c.p2.x, y: c.p2.y });
        if (dist1 < tearRadius || dist2 < tearRadius) {
            // Check if constraint is stretched
            if (c.currentLength > c.length * config_1.CONFIG.TEAR_FORCE_THRESHOLD) {
                toRemove.push(c);
            }
        }
    }
    // Remove torn constraints
    for (const c of toRemove) {
        const idx = constraints.indexOf(c);
        if (idx >= 0)
            constraints.splice(idx, 1);
    }
}
/**
 * Get total kinetic energy of the system (for debugging/validation).
 */
function getTotalKineticEnergy(points) {
    let energy = 0;
    for (const p of points) {
        const vx = p.x - p.oldX;
        const vy = p.y - p.oldY;
        energy += 0.5 * p.mass * (vx * vx + vy * vy);
    }
    return energy;
}
/**
 * Check for any NaN values in the simulation.
 */
function hasNaNValues(state) {
    for (const p of state.points) {
        if (!isFinite(p.x) || !isFinite(p.y) || !isFinite(p.oldX) || !isFinite(p.oldY)) {
            return true;
        }
    }
    return false;
}
/**
 * Get minimum Y coordinate among all points (for floor check).
 */
function getMinY(points) {
    let minY = Infinity;
    for (const p of points) {
        if (p.y < minY)
            minY = p.y;
    }
    return minY;
}
};

// ── module: src/objects.ts ──
__mods["src/objects.ts"] = function (exports, require, module) {
"use strict";
/**
 * Physics Object Creation - Ropes, Cloth, Soft Bodies, Balls
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.createRope = createRope;
exports.createCloth = createCloth;
exports.createSoftBody = createSoftBody;
exports.createBall = createBall;
exports.createBoundaryWalls = createBoundaryWalls;
exports.createStaticCircle = createStaticCircle;
exports.createStaticBox = createStaticBox;
const config_1 = require("./config");
const types_1 = require("./types");
/**
 * Create a rope (hanging chain).
 * @param startX Starting X position
 * @param startY Starting Y position
 * @param length Number of segments
 * @param pinned Start point is pinned
 */
function createRope(startX, startY, length = 15, pinned = true) {
    const points = [];
    const constraints = [];
    const spacing = config_1.CONFIG.ROPE_SPACING;
    for (let i = 0; i <= length; i++) {
        const x = startX + i * spacing;
        const y = startY;
        const p = (0, types_1.createPoint)(x, y, pinned && i === 0);
        points.push(p);
        // Connect to previous point
        if (i > 0) {
            const c = (0, types_1.createConstraint)(points[i - 1], points[i]);
            constraints.push(c);
        }
    }
    return {
        type: 'rope',
        points,
        constraints,
        color: [...config_1.CONFIG.COLORS.ROPE],
    };
}
/**
 * Create cloth (grid with structural and shear constraints).
 * @param startX Top-left X
 * @param startY Top-left Y
 * @param cols Number of columns
 * @param rows Number of rows
 * @param pinPattern Pin pattern: 'corners' | 'full-top'
 */
function createCloth(startX, startY, cols = 12, rows = 8, pinPattern = 'corners') {
    const spacing = config_1.CONFIG.CLOTH_SPACING;
    const points = [];
    const constraints = [];
    const triangles = [];
    // Create grid of points
    for (let row = 0; row < rows; row++) {
        for (let col = 0; col < cols; col++) {
            const x = startX + col * spacing;
            const y = startY + row * spacing;
            // Pin top edge based on pattern
            let pinned = false;
            if (row === 0) {
                if (pinPattern === 'full-top') {
                    pinned = true;
                }
                else if (pinPattern === 'corners') {
                    pinned = (col === 0 || col === cols - 1);
                }
            }
            const p = (0, types_1.createPoint)(x, y, pinned);
            points.push(p);
        }
    }
    // Helper to get point index
    const idx = (col, row) => row * cols + col;
    // Structural constraints (horizontal and vertical)
    for (let row = 0; row < rows; row++) {
        for (let col = 0; col < cols; col++) {
            const currentIdx = idx(col, row);
            // Horizontal constraint
            if (col < cols - 1) {
                const rightIdx = idx(col + 1, row);
                constraints.push((0, types_1.createConstraint)(points[currentIdx], points[rightIdx]));
            }
            // Vertical constraint
            if (row < rows - 1) {
                const downIdx = idx(col, row + 1);
                constraints.push((0, types_1.createConstraint)(points[currentIdx], points[downIdx]));
            }
        }
    }
    // Shear constraints (diagonals for stability)
    for (let row = 0; row < rows - 1; row++) {
        for (let col = 0; col < cols - 1; col++) {
            const i1 = idx(col, row);
            const i2 = idx(col + 1, row + 1);
            const i3 = idx(col + 1, row);
            const i4 = idx(col, row + 1);
            // Cross diagonals
            constraints.push((0, types_1.createConstraint)(points[i1], points[i2], false));
            constraints.push((0, types_1.createConstraint)(points[i3], points[i4], false));
        }
    }
    // Create triangles for mesh rendering
    for (let row = 0; row < rows - 1; row++) {
        for (let col = 0; col < cols - 1; col++) {
            const i1 = idx(col, row);
            const i2 = idx(col + 1, row);
            const i3 = idx(col, row + 1);
            const i4 = idx(col + 1, row + 1);
            // Two triangles per quad
            triangles.push({ p1: points[i1], p2: points[i2], p3: points[i3] });
            triangles.push({ p1: points[i2], p2: points[i4], p3: points[i3] });
        }
    }
    return {
        type: 'cloth',
        points,
        constraints,
        triangles,
        color: [...config_1.CONFIG.COLORS.CLOTH],
        width: cols,
        height: rows,
    };
}
/**
 * Create a soft body (circle/blob shape with internal bracing).
 * @param centerX Center X
 * @param centerY Center Y
 * @param radius Outer radius
 * @param numPoints Number of perimeter points
 */
function createSoftBody(centerX, centerY, radius = config_1.CONFIG.SOFTBODY_RADIUS, numPoints = 12) {
    const points = [];
    const constraints = [];
    const triangles = [];
    const angleStep = (Math.PI * 2) / numPoints;
    // Create perimeter points
    for (let i = 0; i < numPoints; i++) {
        const angle = i * angleStep - Math.PI / 2; // Start from top
        const x = centerX + Math.cos(angle) * radius;
        const y = centerY + Math.sin(angle) * radius;
        const p = (0, types_1.createPoint)(x, y, false);
        points.push(p);
    }
    // Add center point for better deformation
    const centerPoint = (0, types_1.createPoint)(centerX, centerY, false);
    points.push(centerPoint);
    const centerIdx = numPoints;
    // Perimeter constraints (edges)
    for (let i = 0; i < numPoints; i++) {
        const next = (i + 1) % numPoints;
        constraints.push((0, types_1.createConstraint)(points[i], points[next]));
    }
    // Internal constraints (connect to center)
    for (let i = 0; i < numPoints; i++) {
        constraints.push((0, types_1.createConstraint)(points[i], centerPoint, false));
    }
    // Brace constraints (skip-one connections for stiffness)
    for (let i = 0; i < numPoints; i++) {
        const skip1 = (i + 2) % numPoints;
        const skip2 = (i + 3) % numPoints;
        constraints.push((0, types_1.createConstraint)(points[i], points[skip1], false));
        if (numPoints > 6) {
            constraints.push((0, types_1.createConstraint)(points[i], points[skip2], false));
        }
    }
    // Create triangles for filled rendering
    for (let i = 0; i < numPoints; i++) {
        const next = (i + 1) % numPoints;
        triangles.push({
            p1: centerPoint,
            p2: points[i],
            p3: points[next],
            color: undefined
        });
    }
    return {
        type: 'softbody',
        points,
        constraints,
        triangles,
        color: [...config_1.CONFIG.COLORS.SOFTBODY],
    };
}
/**
 * Create a bouncing ball (single point with radius).
 * @param x Initial X
 * @param y Initial Y
 */
function createBall(x, y) {
    const point = (0, types_1.createPoint)(x, y, false);
    point.radius = config_1.CONFIG.BALL_RADIUS;
    return {
        type: 'ball',
        point,
        color: [...config_1.CONFIG.COLORS.BALL],
    };
}
/**
 * Create static walls around the canvas bounds.
 * @param width Canvas width
 * @param height Canvas height
 */
function createBoundaryWalls(width, height) {
    const walls = [];
    const margin = 50;
    // Left wall (normal points right)
    walls.push({
        type: 'wall',
        x: margin,
        y: height / 2,
        nx: 1,
        ny: 0,
    });
    // Right wall (normal points left)
    walls.push({
        type: 'wall',
        x: width - margin,
        y: height / 2,
        nx: -1,
        ny: 0,
    });
    // Bottom wall/floor (normal points up)
    walls.push({
        type: 'wall',
        x: width / 2,
        y: height - margin,
        nx: 0,
        ny: -1,
    });
    // Top wall (normal points down) - optional ceiling
    walls.push({
        type: 'wall',
        x: width / 2,
        y: margin,
        nx: 0,
        ny: 1,
    });
    return walls;
}
/**
 * Create a static circle obstacle.
 * @param x Center X
 * @param y Center Y
 * @param radius Circle radius
 */
function createStaticCircle(x, y, radius) {
    return {
        type: 'circle',
        x,
        y,
        radius,
    };
}
/**
 * Create a static box obstacle.
 * @param x Top-left X
 * @param y Top-left Y
 * @param width Box width
 * @param height Box height
 */
function createStaticBox(x, y, width, height) {
    return {
        type: 'box',
        x,
        y,
        width,
        height,
    };
}
};

// ── module: src/render.ts ──
__mods["src/render.ts"] = function (exports, require, module) {
"use strict";
/**
 * Rendering - Canvas 2D Drawing
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.hslToRgb = hslToRgb;
exports.hslArrayToString = hslArrayToString;
exports.getStressColor = getStressColor;
exports.clearCanvas = clearCanvas;
exports.drawStaticShapes = drawStaticShapes;
exports.drawConstraints = drawConstraints;
exports.drawPoints = drawPoints;
exports.drawCloth = drawCloth;
exports.drawSoftBody = drawSoftBody;
exports.drawBall = drawBall;
exports.drawTrails = drawTrails;
exports.drawTearIndicator = drawTearIndicator;
exports.drawGrabHighlight = drawGrabHighlight;
exports.drawFpsCounter = drawFpsCounter;
const config_1 = require("./config");
/**
 * Convert HSL to RGB for canvas.
 */
function hslToRgb(h, s, l) {
    s /= 100;
    l /= 100;
    const c = (1 - Math.abs(2 * l - 1)) * s;
    const x = c * (1 - Math.abs((h / 60) % 2 - 1));
    const m = l - c / 2;
    let r = 0, g = 0, b = 0;
    if (h >= 0 && h < 60) {
        r = c;
        g = x;
        b = 0;
    }
    else if (h >= 60 && h < 120) {
        r = x;
        g = c;
        b = 0;
    }
    else if (h >= 120 && h < 180) {
        r = 0;
        g = c;
        b = x;
    }
    else if (h >= 180 && h < 240) {
        r = 0;
        g = x;
        b = c;
    }
    else if (h >= 240 && h < 300) {
        r = x;
        g = 0;
        b = c;
    }
    else if (h >= 300 && h < 360) {
        r = c;
        g = 0;
        b = x;
    }
    return [
        Math.round((r + m) * 255),
        Math.round((g + m) * 255),
        Math.round((b + m) * 255),
    ];
}
/**
 * Get color string from HSL array.
 */
function hslArrayToString(color, alpha = 1) {
    return `hsla(${color[0]}, ${color[1]}%, ${color[2]}%, ${alpha})`;
}
/**
 * Get stress-based color (green to red gradient).
 */
function getStressColor(stretchRatio) {
    // stretchRatio = currentLength / restLength
    // 1.0 = no stress (green), >1.5 = max stress (red)
    const t = Math.min(Math.max((stretchRatio - 1) * 2, 0), 1);
    const [r1, g1, b1] = config_1.CONFIG.STRESS_COLOR_MIN;
    const [r2, g2, b2] = config_1.CONFIG.STRESS_COLOR_MAX;
    const r = Math.round(r1 + (r2 - r1) * t);
    const g = Math.round(g1 + (g2 - g1) * t);
    const b = Math.round(b1 + (b2 - b1) * t);
    return `rgb(${r}, ${g}, ${b})`;
}
/**
 * Clear the canvas with background color.
 */
function clearCanvas(ctx, width, height) {
    ctx.fillStyle = config_1.CONFIG.CANVAS_BG;
    ctx.fillRect(0, 0, width, height);
}
/**
 * Draw static shapes (walls, obstacles).
 */
function drawStaticShapes(ctx, shapes) {
    ctx.strokeStyle = config_1.CONFIG.WALL_COLOR;
    ctx.lineWidth = 2;
    ctx.fillStyle = config_1.CONFIG.WALL_COLOR;
    for (const shape of shapes) {
        if (shape.type === 'wall') {
            // Draw wall as a line segment
            const len = 1000;
            const dx = -shape.ny * len;
            const dy = shape.nx * len;
            ctx.beginPath();
            ctx.moveTo(shape.x + dx, shape.y + dy);
            ctx.lineTo(shape.x - dx, shape.y - dy);
            ctx.stroke();
        }
        else if (shape.type === 'circle') {
            ctx.beginPath();
            ctx.arc(shape.x, shape.y, shape.radius, 0, Math.PI * 2);
            ctx.stroke();
            ctx.fill();
        }
        else if (shape.type === 'box') {
            ctx.fillRect(shape.x, shape.y, shape.width, shape.height);
            ctx.strokeRect(shape.x, shape.y, shape.width, shape.height);
        }
    }
}
/**
 * Draw all constraints.
 */
function drawConstraints(ctx, constraints, showStress, grabbedPoint) {
    ctx.lineWidth = config_1.CONFIG.CONSTRAINT_WIDTH;
    for (const c of constraints) {
        if (!c.visible)
            continue;
        if (showStress) {
            // Color by stress
            const ratio = c.length > 0 ? c.currentLength / c.length : 1;
            ctx.strokeStyle = getStressColor(ratio);
        }
        else {
            // Check if connected to grabbed point
            if (c.p1 === grabbedPoint || c.p2 === grabbedPoint) {
                ctx.strokeStyle = hslArrayToString(config_1.CONFIG.COLORS.GRABBED);
            }
            else if (c.color) {
                ctx.strokeStyle = hslArrayToString([c.color[0], c.color[1], c.color[2]]);
            }
            else {
                ctx.strokeStyle = hslArrayToString(config_1.CONFIG.COLORS.CONSTRAINT);
            }
        }
        ctx.beginPath();
        ctx.moveTo(c.p1.x, c.p1.y);
        ctx.lineTo(c.p2.x, c.p2.y);
        ctx.stroke();
    }
}
/**
 * Draw all points.
 */
function drawPoints(ctx, points, grabbedPoint) {
    const radius = config_1.CONFIG.POINT_RADIUS;
    for (const p of points) {
        // Skip drawing points for cloth (render as mesh instead)
        // But still draw for ropes and soft bodies
        if (p === grabbedPoint) {
            // Highlight grabbed point
            ctx.fillStyle = hslArrayToString(config_1.CONFIG.COLORS.GRABBED);
            ctx.beginPath();
            ctx.arc(p.x, p.y, radius * 2, 0, Math.PI * 2);
            ctx.fill();
        }
        else if (p.pinned) {
            // Pinned points are darker
            ctx.fillStyle = hslArrayToString(config_1.CONFIG.COLORS.PINNED);
            ctx.beginPath();
            ctx.arc(p.x, p.y, radius, 0, Math.PI * 2);
            ctx.fill();
        }
        else if (p.color) {
            ctx.fillStyle = hslArrayToString([p.color[0], p.color[1], p.color[2]]);
            ctx.beginPath();
            ctx.arc(p.x, p.y, radius, 0, Math.PI * 2);
            ctx.fill();
        }
        else {
            ctx.fillStyle = hslArrayToString(config_1.CONFIG.COLORS.CONSTRAINT);
            ctx.beginPath();
            ctx.arc(p.x, p.y, radius, 0, Math.PI * 2);
            ctx.fill();
        }
    }
}
/**
 * Draw cloth as filled triangles.
 */
function drawCloth(ctx, cloth) {
    ctx.lineWidth = 1;
    for (const tri of cloth.triangles) {
        // Semi-transparent fill
        ctx.fillStyle = hslArrayToString(cloth.color, 0.3);
        ctx.strokeStyle = hslArrayToString(cloth.color, 0.6);
        ctx.beginPath();
        ctx.moveTo(tri.p1.x, tri.p1.y);
        ctx.lineTo(tri.p2.x, tri.p2.y);
        ctx.lineTo(tri.p3.x, tri.p3.y);
        ctx.closePath();
        ctx.fill();
        ctx.stroke();
    }
}
/**
 * Draw soft body as filled polygon.
 */
function drawSoftBody(ctx, body) {
    ctx.lineWidth = 2;
    ctx.strokeStyle = hslArrayToString(body.color);
    ctx.fillStyle = hslArrayToString(body.color, 0.4);
    // Find perimeter points (points that have visible constraints)
    const perimeterPoints = [];
    const visited = new Set();
    for (const c of body.constraints) {
        if (c.visible) {
            if (!visited.has(c.p1)) {
                perimeterPoints.push(c.p1);
                visited.add(c.p1);
            }
            if (!visited.has(c.p2)) {
                perimeterPoints.push(c.p2);
                visited.add(c.p2);
            }
        }
    }
    if (perimeterPoints.length > 2) {
        ctx.beginPath();
        ctx.moveTo(perimeterPoints[0].x, perimeterPoints[0].y);
        for (let i = 1; i < perimeterPoints.length; i++) {
            ctx.lineTo(perimeterPoints[i].x, perimeterPoints[i].y);
        }
        ctx.closePath();
        ctx.fill();
        ctx.stroke();
    }
    // Also draw internal triangles
    for (const tri of body.triangles) {
        ctx.fillStyle = hslArrayToString(body.color, 0.2);
        ctx.beginPath();
        ctx.moveTo(tri.p1.x, tri.p1.y);
        ctx.lineTo(tri.p2.x, tri.p2.y);
        ctx.lineTo(tri.p3.x, tri.p3.y);
        ctx.closePath();
        ctx.fill();
    }
}
/**
 * Draw a ball.
 */
function drawBall(ctx, ball) {
    const { point, color } = ball;
    ctx.fillStyle = hslArrayToString(color);
    ctx.strokeStyle = hslArrayToString(color);
    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.arc(point.x, point.y, point.radius, 0, Math.PI * 2);
    ctx.fill();
    ctx.stroke();
}
/**
 * Draw trails behind points.
 */
function drawTrails(ctx, points) {
    for (const p of points) {
        if (p.trail.length < 2)
            continue;
        for (let i = 0; i < p.trail.length - 1; i++) {
            const t1 = p.trail[i];
            const t2 = p.trail[i + 1];
            const alpha = (i + 1) / p.trail.length * config_1.CONFIG.TRAIL_FADE;
            ctx.strokeStyle = hslArrayToString(config_1.CONFIG.COLORS.CONSTRAINT, alpha);
            ctx.lineWidth = 1;
            ctx.beginPath();
            ctx.moveTo(t1.x, t1.y);
            ctx.lineTo(t2.x, t2.y);
            ctx.stroke();
        }
    }
}
/**
 * Draw tear indicator (line from start to current position).
 */
function drawTearIndicator(ctx, start, current) {
    ctx.strokeStyle = hslArrayToString(config_1.CONFIG.COLORS.TEARING);
    ctx.lineWidth = 2;
    ctx.setLineDash([5, 5]);
    ctx.beginPath();
    ctx.moveTo(start.x, start.y);
    ctx.lineTo(current.x, current.y);
    ctx.stroke();
    ctx.setLineDash([]);
}
/**
 * Draw grab cursor highlight.
 */
function drawGrabHighlight(ctx, x, y) {
    ctx.strokeStyle = hslArrayToString(config_1.CONFIG.COLORS.GRABBED, 0.5);
    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.arc(x, y, config_1.CONFIG.GRAB_RADIUS, 0, Math.PI * 2);
    ctx.stroke();
}
/**
 * Draw FPS counter text.
 */
function drawFpsCounter(ctx, fps, stepTime, pointCount) {
    // This is handled by DOM elements, but we could draw on canvas too
    // For now, just update DOM elements via main.ts
}
};

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