← Physics Playground results

Physics Playground

Deepseek v4 Flash 0731 · 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 47,910 bytes · SHA-256 bb6bf6bb50ce
<!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 ── */
:root { color-scheme: dark; }
* { box-sizing: border-box; }
html, body { margin: 0; height: 100%; }
body {
  background: #0b0d12;
  color: #cfd6e4;
  font: 13px/1.4 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  overflow: hidden;
  font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
}
#app { display: flex; flex-direction: column; height: 100%; }

#toolbar {
  display: flex; align-items: center; gap: 8px;
  flex-wrap: wrap;
  padding: 7px 10px;
  background: linear-gradient(#161a22, #12151c);
  border-bottom: 1px solid #232836;
  user-select: none;
  z-index: 5;
}
.tgroup { display: flex; gap: 4px; }
.tool, #btn-pause, #btn-reset, select {
  background: #1d2230; color: #b9c2d4;
  border: 1px solid #2b3244; border-radius: 6px;
  padding: 4px 9px; cursor: pointer;
}
.tool:hover, #btn-pause:hover, #btn-reset:hover { background: #252c3d; }
.tool.on { background: #3b4f8a; color: #fff; border-color: #5b76c4; }
.sep { width: 1px; height: 18px; background: #2a3040; margin: 0 2px; }
.ck { display: flex; align-items: center; gap: 5px; cursor: pointer; white-space: nowrap; }
.ck input { accent-color: #5b76c4; }
.sl { display: flex; align-items: center; gap: 5px; white-space: nowrap; }
.sl input { width: 90px; accent-color: #5b76c4; }
#pinpat { background: #1d2230; color: #b9c2d4; border: 1px solid #2b3244; border-radius: 6px; }

#cv { display: block; flex: 1 1 auto; width: 100%; cursor: crosshair; }

#hud {
  position: fixed; right: 10px; bottom: 9px;
  display: flex; gap: 14px;
  background: rgba(10,12,18,0.62); border: 1px solid #232836;
  padding: 5px 10px; border-radius: 8px;
  font-size: 11.5px; color: #8fa0bd; pointer-events: none;
  backdrop-filter: blur(3px);
  z-index: 6;
}
#hud-hint { color: #e6c66a; }
#hud span b { color: #dbe3f2; font-weight: 600; }
</style>
</head>
<body>
<div id="app">
  <div id="toolbar">
    <div class="tgroup" id="tools">
      <button class="tool on" data-tool="grab" title="Select / grab (left-drag to grab, Shift+click to pin, right-drag to tear)">Grab</button>
      <button class="tool" data-tool="rope" title="Draw a hanging rope">Rope</button>
      <button class="tool" data-tool="cloth" title="Drag a cloth rectangle">Cloth</button>
      <button class="tool" data-tool="soft" title="Drag to size a soft body">Soft body</button>
      <button class="tool" data-tool="ball" title="Click to drop a ball">Ball</button>
    </div>
    <div class="sep"></div>
    <label class="ck"><input type="checkbox" id="ck-grav" checked=""> Gravity</label>
    <label class="ck"><input type="checkbox" id="ck-wind"> Wind</label>
    <label class="ck"><input type="checkbox" id="ck-stress"> Stress</label>
    <label class="ck"><input type="checkbox" id="ck-trail"> Trail</label>
    <div class="sep"></div>
    <label class="sl">G <input type="range" id="sl-grav" min="0" max="2500" step="50" value="900"></label>
    <label class="sl">Iter <input type="range" id="sl-iter" min="1" max="12" step="1" value="5"></label>
    <select id="pinpat" title="Cloth top pin pattern">
      <option value="full">Full edge</option>
      <option value="corners" selected="">Corners</option>
    </select>
    <div class="sep"></div>
    <button id="btn-pause" title="Space">Pause</button>
    <button id="btn-reset" title="Clear &amp; rebuild demo">Reset</button>
  </div>
  <canvas id="cv"></canvas>
  <div id="hud">
    <span id="hud-fps">-- fps</span>
    <span id="hud-ms">-- ms/step</span>
    <span id="hud-pts">0 pts</span>
    <span id="hud-hint"></span>
  </div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./world":"src/world.ts","./objects":"src/objects.ts","./engine":"src/engine.ts","./render":"src/render.ts","./input":"src/input.ts","./ui":"src/ui.ts"},"src/world.ts":{"./types":"src/types.ts"},"src/objects.ts":{"./world":"src/world.ts","./types":"src/types.ts"},"src/engine.ts":{"./world":"src/world.ts","./types":"src/types.ts"},"src/render.ts":{"./world":"src/world.ts","./interact":"src/interact.ts","./types":"src/types.ts"},"src/input.ts":{"./world":"src/world.ts","./interact":"src/interact.ts","./engine":"src/engine.ts","./objects":"src/objects.ts","./render":"src/render.ts","./types":"src/types.ts"},"src/ui.ts":{"./world":"src/world.ts","./interact":"src/interact.ts","./objects":"src/objects.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";
// ---------------------------------------------------------------
// Entry: wiring + the render/physics loop with a fixed-timestep
// accumulator. Exposes the sim on window for headless probing.
// ---------------------------------------------------------------
Object.defineProperty(exports, "__esModule", { value: true });
exports.start = start;
const world_1 = require("./world");
const objects_1 = require("./objects");
const engine_1 = require("./engine");
const render_1 = require("./render");
const input_1 = require("./input");
const ui_1 = require("./ui");
function start() {
    const canvas = document.getElementById('cv');
    (0, render_1.initRender)(canvas);
    (0, input_1.initInput)(canvas);
    (0, ui_1.initUI)();
    (0, render_1.resize)();
    addStaticGeometry();
    (0, objects_1.initScene)(world_1.cfg.width, world_1.cfg.height);
    window.addEventListener('resize', render_1.resize);
    window.addEventListener('keydown', (e) => {
        if (e.code === 'ArrowRight' && world_1.physics.paused) {
            (0, engine_1.stepFrame)(world_1.cfg.dt);
            (0, input_1.applyGrab)();
            (0, render_1.draw)(0);
        }
    });
    exposeSim();
    // ---- main loop ------------------------------------------------
    let acc = 0;
    let last = performance.now();
    let fpsSm = 0, fpsFrame = 0, fpsLast = performance.now();
    let stepAcc = 0, stepCnt = 0;
    function frame(t) {
        const dt = Math.min(world_1.cfg.maxFrameDelta, (t - last) / 1000);
        last = t;
        if (!world_1.physics.paused) {
            acc += dt;
            const s0 = performance.now();
            let guard = 0;
            while (acc >= world_1.cfg.dt && guard < 4) {
                (0, engine_1.stepFrame)(world_1.cfg.dt);
                (0, input_1.applyGrab)();
                acc -= world_1.cfg.dt;
                guard++;
            }
            stepAcc += performance.now() - s0;
            stepCnt++;
        }
        (0, render_1.draw)(dt);
        // fps + step-time gauges
        fpsFrame++;
        fpsSm += dt;
        if (fpsSm >= 0.5) {
            const fps = fpsFrame / fpsSm;
            (0, ui_1.updateHud)(fps, stepCnt > 0 ? stepAcc / stepCnt : 0);
            fpsFrame = 0;
            fpsSm = 0;
            stepAcc = 0;
            stepCnt = 0;
        }
        requestAnimationFrame(frame);
    }
    requestAnimationFrame(frame);
}
function addStaticGeometry() {
    world_1.world.circles.length = 0;
    world_1.world.boxes.length = 0;
}
function exposeSim() {
    const sim = {
        points: world_1.world.points,
        constraints: world_1.world.constraints,
        objs: world_1.world.objs,
        world: world_1.world,
        cfg: world_1.cfg,
        physics: world_1.physics,
        width: render_1.worldWidth,
        height: render_1.worldHeight,
        step: () => (0, engine_1.stepFrame)(world_1.cfg.dt),
        stepN: (n) => { for (let i = 0; i < n; i++)
            (0, engine_1.stepFrame)(world_1.cfg.dt); },
        kineticEnergy: engine_1.kineticEnergy,
        tearConstraintAt: engine_1.tearConstraintAt,
        cutSegment: engine_1.cutSegment,
        clear: () => { world_1.world.points.length = 0; world_1.world.constraints.length = 0; world_1.world.objs.length = 0; },
        addPoint: world_1.addPoint,
        addConstraint: world_1.addConstraint,
        spawnRope: objects_1.spawnRope,
        spawnCloth: objects_1.spawnCloth,
        spawnBlob: objects_1.spawnBlob,
        spawnBall: objects_1.spawnBall,
    };
    window.sim = sim;
}
start();
};

// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
// ---------------------------------------------------------------
// Core data structures for the Verlet soft-body simulation.
// ---------------------------------------------------------------
Object.defineProperty(exports, "__esModule", { value: true });
exports.clamp = clamp;
function clamp(v, lo, hi) {
    return v < lo ? lo : v > hi ? hi : v;
}
};

// ── module: src/world.ts ──
__mods["src/world.ts"] = function (exports, require, module) {
"use strict";
// ---------------------------------------------------------------
// Global sim state: tunable constants + the world (points,
// constraints, objects, static geometry).
// ---------------------------------------------------------------
Object.defineProperty(exports, "__esModule", { value: true });
exports.physics = exports.world = exports.cfg = void 0;
exports.addPoint = addPoint;
exports.addConstraint = addConstraint;
exports.makeObj = makeObj;
// ---- Tunable constants (all in px / seconds) --------------------
exports.cfg = {
    width: 1280, // world/screen size (set on resize)
    height: 720,
    gravity: 900, // px/s^2 downward
    gravityOn: true,
    damping: 0.99, // global velocity retention per step
    iterations: 5, // constraint relaxation iterations
    dt: 1 / 60, // fixed physics timestep
    maxFrameDelta: 0.05, // clamp render delta so tab-switch won't detonate
    tearRadius: 16, // px around drag path that severs constraints
    grabRadius: 18, // px around cursor that catches a point
    windOn: false,
    windAmp: 220, // horizontal acceleration amplitude
    windSpeed: 1.1, // wind oscillation frequency
    friction: 0.45, // tangential velocity removed at collision (0..1)
    spacing: 9, // default point spacing for spawned objects
};
exports.world = {
    points: [],
    constraints: [],
    objs: [],
    circles: [],
    boxes: [],
    nextId: 1,
};
exports.physics = {
    time: 0,
    windPhase: 0,
    paused: false,
    frameCount: 0,
    stepMs: 0,
    lastStepElapsed: 0,
};
// ---- Factory helpers ---------------------------------------------
function addPoint(x, y, radius = 2.5, pinned = false, bounce = 0.05, weight = 1) {
    const idx = exports.world.points.length;
    exports.world.points.push({
        id: exports.world.nextId++, x, y, px: x, py: y,
        pinned, radius, bounce, weight, active: true,
    });
    return idx;
}
/** Add a distance constraint between two point indices. */
function addConstraint(a, b, kind, stiffness = 1) {
    const pa = exports.world.points[a];
    const pb = exports.world.points[b];
    const restLen = Math.hypot(pa.x - pb.x, pa.y - pb.y) || 1e-3;
    const idx = exports.world.constraints.length;
    exports.world.constraints.push({ id: exports.world.nextId++, a, b, restLen, active: true, stiffness, kind, obj: -1 });
    return idx;
}
function makeObj(kind, color, glow) {
    const obj = { id: exports.world.nextId++, kind, color, glow, points: [], constraints: [] };
    exports.world.objs.push(obj);
    return obj;
}
};

// ── module: src/objects.ts ──
__mods["src/objects.ts"] = function (exports, require, module) {
"use strict";
// ---------------------------------------------------------------
// Spawners: rope, cloth, soft body blob, ball.
// ---------------------------------------------------------------
Object.defineProperty(exports, "__esModule", { value: true });
exports.spawnRope = spawnRope;
exports.spawnCloth = spawnCloth;
exports.spawnBlob = spawnBlob;
exports.spawnBall = spawnBall;
exports.clearWorld = clearWorld;
exports.initScene = initScene;
const world_1 = require("./world");
const types_1 = require("./types");
/** Hanging rope: a chain pinned at its start point. */
function spawnRope(x1, y1, x2, y2) {
    const spacing = world_1.cfg.spacing;
    const len = Math.hypot(x2 - x1, y2 - y1);
    const n = Math.max(2, Math.round(len / spacing));
    const ax = (x2 - x1) / n, ay = (y2 - y1) / n;
    const obj = (0, world_1.makeObj)('rope', '#ff6b6b', 'rgba(255,107,107,0.35)');
    let prev = -1;
    for (let i = 0; i <= n; i++) {
        const idx = (0, world_1.addPoint)(x1 + ax * i, y1 + ay * i, 2.4, i === 0, 0.05);
        obj.points.push(idx);
        if (prev >= 0) {
            const c = (0, world_1.addConstraint)(prev, idx, 'rope', 1);
            obj.constraints.push(c);
        }
        prev = idx;
    }
    return obj;
}
/** Cloth: a rectangular grid of points. Pins along the top edge. */
function spawnCloth(x0, y0, x1, y1, pin) {
    const spacing = world_1.cfg.spacing;
    const minX = Math.min(x0, x1), maxX = Math.max(x0, x1);
    const minY = Math.min(y0, y1), maxY = Math.max(y0, y1);
    const cols = Math.max(2, Math.round((maxX - minX) / spacing) + 1);
    const rows = Math.max(2, Math.round((maxY - minY) / spacing) + 1);
    const obj = (0, world_1.makeObj)('cloth', '#3ecfae', 'rgba(62,207,174,0.28)');
    obj.cols = cols;
    obj.rows = rows;
    // build grid
    const g = [];
    for (let r = 0; r < rows; r++) {
        g.push([]);
        for (let c = 0; c < cols; c++) {
            const x = minX + (maxX - minX) * c / (cols - 1);
            const y = minY + (maxY - minY) * r / (rows - 1);
            const isTop = r === 0;
            const pinned = pin === 'full' ? isTop : (isTop && (c === 0 || c === cols - 1));
            g[r].push((0, world_1.addPoint)(x, y, 2.1, pinned, 0.03));
        }
    }
    const C = (r, c) => g[r][c];
    // structural + shear + vertical bend
    for (let r = 0; r < rows; r++) {
        for (let c = 0; c < cols; c++) {
            if (c + 1 < cols)
                obj.constraints.push((0, world_1.addConstraint)(C(r, c), C(r, c + 1), 'struct', 1));
            if (r + 1 < rows)
                obj.constraints.push((0, world_1.addConstraint)(C(r, c), C(r + 1, c), 'struct', 1));
            if (r + 1 < rows && c + 1 < cols)
                obj.constraints.push((0, world_1.addConstraint)(C(r, c), C(r + 1, c + 1), 'shear', 0.9));
            if (r + 1 < rows && c - 1 >= 0)
                obj.constraints.push((0, world_1.addConstraint)(C(r, c), C(r + 1, c - 1), 'shear', 0.9));
            if (r + 2 < rows)
                obj.constraints.push((0, world_1.addConstraint)(C(r, c), C(r + 2, c), 'bend', 0.25));
            if (c + 2 < cols)
                obj.constraints.push((0, world_1.addConstraint)(C(r, c), C(r, c + 2), 'bend', 0.25));
        }
    }
    for (let r = 0; r < rows; r++)
        for (let c = 0; c < cols; c++)
            obj.points.push(g[r][c]);
    return obj;
}
/** Soft body: a ring of points around a center, edge + brace constraints. */
function spawnBlob(cx, cy, r) {
    const N = 14;
    const obj = (0, world_1.makeObj)('soft', '#ffd93d', 'rgba(255,217,61,0.3)');
    const center = (0, world_1.addPoint)(cx, cy, 3, false, 0.08);
    obj.center = center;
    const ring = [];
    for (let i = 0; i < N; i++) {
        const a = (i / N) * Math.PI * 2;
        ring.push((0, world_1.addPoint)(cx + Math.cos(a) * r, cy + Math.sin(a) * r, 3, false, 0.08));
    }
    obj.ring = ring;
    obj.points = [center, ...ring];
    // perimeter
    for (let i = 0; i < N; i++)
        obj.constraints.push((0, world_1.addConstraint)(ring[i], ring[(i + 1) % N], 'edge', 1));
    // center spokes (internal brace)
    for (let i = 0; i < N; i++)
        obj.constraints.push((0, world_1.addConstraint)(center, ring[i], 'brace', 0.8));
    // long chords for rigidity
    for (let i = 0; i < N; i += 2)
        obj.constraints.push((0, world_1.addConstraint)(ring[i], ring[(i + 5) % N], 'brace', 0.6));
    return obj;
}
/** Ball: a single heavy point that bounces. */
function spawnBall(x, y, r = 12) {
    const obj = (0, world_1.makeObj)('ball', '#a78bfa', 'rgba(167,139,250,0.4)');
    const idx = (0, world_1.addPoint)(x, y, r, false, 0.7, 1);
    obj.points.push(idx);
    return obj;
}
/** Remove every dynamic point/constraint (keeps static geometry). */
function clearWorld() {
    world_1.world.points.length = 0;
    world_1.world.constraints.length = 0;
    world_1.world.objs.length = 0;
}
/** Build the initial demo scene. */
function initScene(w, h) {
    // static circle to drape / bounce off
    const cx = Math.round(w * 0.66), cy = Math.round(h * 0.42);
    world_1.world.circles.push({ x: cx, y: cy, r: 56, color: '#2f3b55' });
    // a hanging pendulum rope
    spawnRope(w * 0.2, 54, w * 0.2, h * 0.42);
    // a soft body blob dropped near the top to settle on the floor
    spawnBlob(w * 0.5, h * 0.2, 34);
    // a cloth pinned along the top edge so it drapes over the static circle
    const cw = 150, ch = 170;
    spawnCloth(cx - 40 - cw, 52, cx - 40, 52 + ch, 'corners');
    // a ball resting for contrast
    spawnBall(w * 0.84, (0, types_1.clamp)(h * 0.5, 60, h - 40), 12);
}
};

// ── module: src/engine.ts ──
__mods["src/engine.ts"] = function (exports, require, module) {
"use strict";
// ---------------------------------------------------------------
// Engine: fixed-timestep Verlet integration + PBD constraint
// relaxation + point-vs-static-shape collision with friction.
// ---------------------------------------------------------------
Object.defineProperty(exports, "__esModule", { value: true });
exports.stepFrame = stepFrame;
exports.tearConstraintAt = tearConstraintAt;
exports.cutSegment = cutSegment;
exports.kineticEnergy = kineticEnergy;
const world_1 = require("./world");
const types_1 = require("./types");
const EPS = 1e-6;
/** Advance the simulation by one fixed timestep. */
function stepFrame(dt) {
    world_1.physics.time += dt;
    world_1.physics.frameCount++;
    world_1.physics.windPhase += dt * world_1.cfg.windSpeed;
    integrate(dt);
    for (let i = 0; i < world_1.cfg.iterations; i++)
        solveConstraints();
    collide();
}
function integrate(dt) {
    const g = world_1.cfg.gravityOn ? world_1.cfg.gravity : 0;
    const wx = world_1.cfg.windOn ? Math.sin(world_1.physics.windPhase) * world_1.cfg.windAmp : 0;
    const a2 = dt * dt;
    const dmp = world_1.cfg.damping;
    const pts = world_1.world.points;
    for (let i = 0; i < pts.length; i++) {
        const p = pts[i];
        if (p.pinned) {
            p.px = p.x;
            p.py = p.y;
            continue;
        }
        const vx = (p.x - p.px) * dmp;
        const vy = (p.y - p.py) * dmp;
        p.px = p.x;
        p.py = p.y;
        p.x += vx + wx * a2;
        p.y += vy + g * a2;
    }
}
function solveConstraints() {
    const cons = world_1.world.constraints;
    const pts = world_1.world.points;
    for (let i = 0; i < cons.length; i++) {
        const c = cons[i];
        if (!c.active)
            continue;
        const pa = pts[c.a], pb = pts[c.b];
        if (pa.pinned && pb.pinned)
            continue;
        let dx = pb.x - pa.x, dy = pb.y - pa.y;
        let distSq = dx * dx + dy * dy;
        if (distSq < EPS)
            continue; // coincident points: skip (no NaN)
        const dist = Math.sqrt(distSq);
        // inverse masses: pinned points have 0 (they never move)
        const invA = pa.pinned ? 0 : 1 / pa.weight;
        const invB = pb.pinned ? 0 : 1 / pb.weight;
        const invSum = invA + invB;
        if (invSum <= 0)
            continue;
        const corr = (dist - c.restLen) / dist * c.stiffness;
        const offX = dx * corr;
        const offY = dy * corr;
        const fA = invA / invSum, fB = invB / invSum;
        if (!pa.pinned) {
            pa.x += offX * fA;
            pa.y += offY * fA;
        }
        if (!pb.pinned) {
            pb.x -= offX * fB;
            pb.y -= offY * fB;
        }
    }
}
// ---- Collision ----------------------------------------------------
function settle(p, nx, ny, t = 1) {
    const vx = p.x - p.px, vy = p.y - p.py;
    const vn = vx * nx + vy * ny;
    if (vn <= 0)
        return; // already moving away on the normal
    const tx = -ny, ty = nx;
    const vt = vx * tx + vy * ty;
    const vtNew = vt * (1 - world_1.cfg.friction * t);
    const vnNew = -vn * p.bounce;
    p.px = p.x - (nx * vnNew + tx * vtNew);
    p.py = p.y - (ny * vnNew + ty * vtNew);
}
function collide() {
    const W = world_1.cfg.width, H = world_1.cfg.height;
    const th = 6; // wall thickness
    const pts = world_1.world.points;
    for (let i = 0; i < pts.length; i++) {
        const p = pts[i];
        if (p.pinned)
            continue;
        const r = p.radius;
        if (p.x < th + r) {
            p.x = th + r;
            settle(p, 1, 0);
        }
        else if (p.x > W - th - r) {
            p.x = W - th - r;
            settle(p, -1, 0);
        }
        const floorY = H - th;
        if (p.y > floorY - r) {
            p.y = floorY - r;
            settle(p, 0, -1);
        }
        if (p.y < th + r) {
            p.y = th + r;
            settle(p, 0, 1);
        }
        for (let ci = 0; ci < world_1.world.circles.length; ci++) {
            const c = world_1.world.circles[ci];
            const dx = p.x - c.x, dy = p.y - c.y;
            const rr = r + c.r;
            const d2 = dx * dx + dy * dy;
            if (d2 < rr * rr && d2 > EPS) {
                const d = Math.sqrt(d2);
                settle(p, dx / d, dy / d);
                p.x = c.x + (dx / d) * rr;
                p.y = c.y + (dy / d) * rr;
            }
        }
        for (let bi = 0; bi < world_1.world.boxes.length; bi++) {
            const b = world_1.world.boxes[bi];
            const bx = (0, types_1.clamp)(p.x, b.x, b.x + b.w);
            const by = (0, types_1.clamp)(p.y, b.y, b.y + b.h);
            const dx = p.x - bx, dy = p.y - by;
            const d2 = dx * dx + dy * dy;
            if (d2 < r * r) {
                if (d2 > EPS) {
                    const d = Math.sqrt(d2);
                    settle(p, dx / d, dy / d);
                    p.x = bx + (dx / d) * r;
                    p.y = by + (dy / d) * r;
                }
                else {
                    // centre of point is inside box: push out along nearest face
                    const left = p.x - b.x, right = b.x + b.w - p.x;
                    const top = p.y - b.y, bot = b.y + b.h - p.y;
                    const m = Math.min(left, right, top, bot);
                    let nx = 0, ny = 0;
                    if (m === left)
                        nx = -1;
                    else if (m === right)
                        nx = 1;
                    else if (m === top)
                        ny = -1;
                    else
                        ny = 1;
                    settle(p, nx, ny);
                    p.x += nx * (r + 0.05);
                    p.y += ny * (r + 0.05);
                }
            }
        }
    }
}
// ---- Probing / exposed helpers ------------------------------------
/** Deactivate the i-th (by obj) rope constraint index across whole model. */
function tearConstraintAt(ea, eb) {
    const cons = world_1.world.constraints;
    for (let i = 0; i < cons.length; i++) {
        const c = cons[i];
        if (c.active && ((c.a === ea && c.b === eb) || (c.a === eb && c.b === ea))) {
            c.active = false;
            return true;
        }
    }
    return false;
}
/** Cut every constraint that intersects a segment (ax,ay)-(bx,by). */
function cutSegment(ax, ay, bx, by) {
    let cut = 0;
    const cons = world_1.world.constraints;
    const pts = world_1.world.points;
    for (let i = 0; i < cons.length; i++) {
        const c = cons[i];
        if (!c.active)
            continue;
        const pa = pts[c.a], pb = pts[c.b];
        if (Math.min(pa.x, pb.x) > Math.max(ax, bx) + world_1.cfg.tearRadius)
            continue;
        if (Math.max(pa.x, pb.x) < Math.min(ax, bx) - world_1.cfg.tearRadius)
            continue;
        if (Math.min(pa.y, pb.y) > Math.max(ay, by) + world_1.cfg.tearRadius)
            continue;
        if (Math.max(pa.y, pb.y) < Math.min(ay, by) - world_1.cfg.tearRadius)
            continue;
        const midx = (pa.x + pb.x) / 2, midy = (pa.y + pb.y) / 2;
        // distance from midpoint to segment
        if (distToSegment(midx, midy, ax, ay, bx, by) <= world_1.cfg.tearRadius) {
            c.active = false;
            cut++;
        }
    }
    return cut;
}
function distToSegment(px, py, ax, ay, bx, by) {
    const dx = bx - ax, dy = by - ay;
    const l2 = dx * dx + dy * dy;
    let tt = 0;
    if (l2 > EPS) {
        tt = (0, types_1.clamp)(((px - ax) * dx + (py - ay) * dy) / l2, 0, 1);
    }
    return Math.hypot(px - (ax + dx * tt), py - (ay + dy * tt));
}
/** Total kinetic energy (point masses all equal to 1). */
function kineticEnergy() {
    const dt = world_1.cfg.dt;
    let ke = 0;
    const pts = world_1.world.points;
    for (let i = 0; i < pts.length; i++) {
        const p = pts[i];
        const vx = (p.x - p.px) / dt, vy = (p.y - p.py) / dt;
        ke += 0.5 * (vx * vx + vy * vy);
    }
    return ke / (60 * 60 * 4); // scale to pleasant magnitude (per-frame px^2)
}
};

// ── module: src/interact.ts ──
__mods["src/interact.ts"] = function (exports, require, module) {
"use strict";
// ---------------------------------------------------------------
// Mutable UI / interaction state shared by input, render, ui, main.
// Kept dependency-free so nobody needs to import renderers.
// ---------------------------------------------------------------
Object.defineProperty(exports, "__esModule", { value: true });
exports.ptr = exports.ui = void 0;
exports.spark = spark;
exports.ui = {
    mode: 'grab',
    pinPattern: 'corners',
    stress: false,
    trail: true,
    trailAlpha: 0.32,
};
exports.ptr = {
    mx: 0, my: 0,
    grabbed: -1, // point index under drag
    gx: 0, gy: 0, // cursor pos fed to the grabbed point
    pGX: 0, pGY: 0, // previous cursor pos (throw velocity)
    hover: -1,
    tearing: false,
    sparks: [],
    downX: 0, downY: 0,
    dragging: false, // a rect/preview drag is in progress
    preview: null,
};
/** Add a small tear spark at a coordinate (for feedback). */
function spark(x, y) {
    for (let i = 0; i < 6; i++) {
        const a = Math.random() * Math.PI * 2;
        const s = 40 + Math.random() * 90;
        exports.ptr.sparks.push({ x, y, vx: Math.cos(a) * s, vy: Math.sin(a) * s, t: 0.5 });
    }
}
};

// ── module: src/render.ts ──
__mods["src/render.ts"] = function (exports, require, module) {
"use strict";
// ---------------------------------------------------------------
// Canvas rendering: world, objects (as meshes / lines / polygons),
// constraints (optional stress coloring), pins, grab highlight,
// motion-trail, preview and tear sparks.
// ---------------------------------------------------------------
Object.defineProperty(exports, "__esModule", { value: true });
exports.initRender = initRender;
exports.resize = resize;
exports.worldWidth = worldWidth;
exports.worldHeight = worldHeight;
exports.draw = draw;
exports.moveCursor = moveCursor;
exports.nearestPoint = nearestPoint;
const world_1 = require("./world");
const interact_1 = require("./interact");
const types_1 = require("./types");
let cv;
let ctx;
let dpr = 1;
const BG = '#0b0d12';
function initRender(canvas) {
    cv = canvas;
    ctx = canvas.getContext('2d');
    resize();
}
function resize() {
    dpr = Math.min(window.devicePixelRatio || 1, 2);
    const w = cv.clientWidth, h = cv.clientHeight;
    cv.width = Math.max(1, Math.round(w * dpr));
    cv.height = Math.max(1, Math.round(h * dpr));
    world_1.cfg.width = w;
    world_1.cfg.height = h;
}
const ctx2d = () => ctx;
function worldWidth() { return world_1.cfg.width; }
function worldHeight() { return world_1.cfg.height; }
function draw(dt) {
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    // motion trail or clear
    if (interact_1.ui.trail) {
        ctx.fillStyle = BG;
        ctx.globalAlpha = interact_1.ui.trailAlpha;
        ctx.fillRect(0, 0, world_1.cfg.width, world_1.cfg.height);
        ctx.globalAlpha = 1;
    }
    else {
        ctx.fillStyle = BG;
        ctx.fillRect(0, 0, world_1.cfg.width, world_1.cfg.height);
    }
    drawStatic();
    for (const obj of world_1.world.objs)
        drawObj(obj);
    drawPins();
    drawGrab();
    drawSparks(dt);
    drawPreview();
}
// ---- static geometry ---------------------------------------------
function drawStatic() {
    const th = 6;
    const W = world_1.cfg.width, H = world_1.cfg.height;
    ctx.strokeStyle = '#1d2433';
    ctx.lineWidth = 1;
    ctx.strokeRect(0.5, 0.5, W - 1, H - 1);
    // floor highlight
    ctx.fillStyle = '#141926';
    ctx.fillRect(0, H - th, W, th);
    for (const c of world_1.world.circles) {
        ctx.fillStyle = '#222b3f';
        ctx.beginPath();
        ctx.arc(c.x, c.y, c.r, 0, Math.PI * 2);
        ctx.fill();
        ctx.fillStyle = '#2d3a57';
        ctx.beginPath();
        ctx.arc(c.x - c.r * 0.28, c.y - c.r * 0.28, c.r * 0.62, 0, Math.PI * 2);
        ctx.fill();
        ctx.strokeStyle = '#3a4a70';
        ctx.lineWidth = 1.5;
        ctx.beginPath();
        ctx.arc(c.x, c.y, c.r, 0, Math.PI * 2);
        ctx.stroke();
    }
    for (const b of world_1.world.boxes) {
        ctx.fillStyle = '#222b3f';
        ctx.fillRect(b.x, b.y, b.w, b.h);
        ctx.strokeStyle = '#3a4a70';
        ctx.lineWidth = 1.5;
        ctx.strokeRect(b.x, b.y, b.w, b.h);
    }
}
// ---- objects -------------------------------------------------------
function drawObj(obj) {
    // cloth: filled mesh
    if (obj.kind === 'cloth')
        drawCloth(obj);
    else if (obj.kind === 'soft')
        drawSoft(obj);
    else if (obj.kind === 'ball')
        drawBall(obj);
    else
        drawRope(obj);
}
function stressColor(stretch) {
    const t = (0, types_1.clamp)((stretch - 1) * 4, 0, 1); // 1 at +25% stretch
    // green -> yellow -> red
    if (!interact_1.ui.stress)
        return '#000';
    const r = Math.round(60 + t * 195);
    const g = Math.round(210 - t * 180);
    const b = 90;
    return `rgb(${r},${g},${b})`;
}
function line(a, b, stroke, width = 1.5, alpha = 1) {
    const pa = world_1.world.points[a], pb = world_1.world.points[b];
    ctx.globalAlpha = alpha;
    ctx.strokeStyle = stroke;
    ctx.lineWidth = width;
    ctx.beginPath();
    ctx.moveTo(pa.x, pa.y);
    ctx.lineTo(pb.x, pb.y);
    ctx.stroke();
    ctx.globalAlpha = 1;
}
function drawRope(obj) {
    ctx.lineCap = 'round';
    for (const ci of obj.constraints) {
        const c = world_1.world.constraints[ci];
        if (!c.active)
            continue;
        const pa = world_1.world.points[c.a], pb = world_1.world.points[c.b];
        if (interact_1.ui.stress) {
            const stretch = (Math.hypot(pb.x - pa.x, pb.y - pa.y)) / c.restLen;
            line(c.a, c.b, stressColor(stretch), 2.4);
        }
        else {
            line(c.a, c.b, obj.color, 2.4, 0.95);
        }
    }
    drawPoints(obj, obj.color, 2.2);
}
function drawCloth(obj) {
    const cols = obj.cols, rows = obj.rows;
    // fill each cell as two triangles
    ctx.fillStyle = obj.glow;
    ctx.strokeStyle = obj.color;
    ctx.lineWidth = 1;
    for (let r = 0; r < rows - 1; r++) {
        for (let c = 0; c < cols - 1; c++) {
            const i = r * cols + c;
            const a = world_1.world.points[obj.points[i]];
            const b = world_1.world.points[obj.points[i + 1]];
            const d = world_1.world.points[obj.points[i + cols]];
            const e = world_1.world.points[obj.points[i + cols + 1]];
            ctx.beginPath();
            ctx.moveTo(a.x, a.y);
            ctx.lineTo(b.x, b.y);
            ctx.lineTo(d.x, d.y);
            ctx.closePath();
            ctx.fill();
            ctx.beginPath();
            ctx.moveTo(b.x, b.y);
            ctx.lineTo(e.x, e.y);
            ctx.lineTo(d.x, d.y);
            ctx.closePath();
            ctx.fill();
        }
    }
    // structural lines
    for (const ci of obj.constraints) {
        const c = world_1.world.constraints[ci];
        if (!c.active)
            continue;
        if (c.kind !== 'struct' && c.kind !== 'shear')
            continue;
        const pa = world_1.world.points[c.a], pb = world_1.world.points[c.b];
        const stretch = (Math.hypot(pb.x - pa.x, pb.y - pa.y)) / c.restLen;
        const col = interact_1.ui.stress ? stressColor(stretch) : obj.color;
        ctx.strokeStyle = col;
        ctx.lineWidth = c.kind === 'shear' ? 0.5 : 1;
        ctx.globalAlpha = c.kind === 'shear' ? 0.25 : 0.5;
        ctx.beginPath();
        ctx.moveTo(pa.x, pa.y);
        ctx.lineTo(pb.x, pb.y);
        ctx.stroke();
        ctx.globalAlpha = 1;
    }
}
function drawSoft(obj) {
    const ring = obj.ring;
    // filled ring polygon
    ctx.fillStyle = obj.glow;
    ctx.beginPath();
    for (let i = 0; i < ring.length; i++) {
        const p = world_1.world.points[ring[i]];
        if (i === 0)
            ctx.moveTo(p.x, p.y);
        else
            ctx.lineTo(p.x, p.y);
    }
    ctx.closePath();
    ctx.fill();
    // perimeter
    for (let i = 0; i < ring.length; i++) {
        const a = ring[i], b = ring[(i + 1) % ring.length];
        line(a, b, obj.color, 2.2, 0.95);
    }
    // internal braces faint
    ctx.strokeStyle = obj.color;
    ctx.lineWidth = 0.8;
    ctx.globalAlpha = 0.22;
    ctx.beginPath();
    for (const ci of obj.constraints) {
        const c = world_1.world.constraints[ci];
        if (!c.active)
            continue;
        if (c.kind === 'edge')
            continue;
        const pa = world_1.world.points[c.a], pb = world_1.world.points[c.b];
        ctx.moveTo(pa.x, pa.y);
        ctx.lineTo(pb.x, pb.y);
    }
    ctx.stroke();
    ctx.globalAlpha = 1;
    drawPoints(obj, '#e8cf5a', 2.6);
}
function drawBall(obj) {
    const p = world_1.world.points[obj.points[0]];
    const grad = ctx.createRadialGradient(p.x - p.radius * 0.3, p.y - p.radius * 0.3, p.radius * 0.2, p.x, p.y, p.radius);
    grad.addColorStop(0, '#d8c7ff');
    grad.addColorStop(1, obj.color);
    ctx.fillStyle = grad;
    ctx.beginPath();
    ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
    ctx.fill();
    ctx.strokeStyle = 'rgba(255,255,255,0.35)';
    ctx.lineWidth = 1.5;
    ctx.stroke();
}
function drawPoints(obj, color, r) {
    ctx.fillStyle = color;
    for (const pi of obj.points) {
        const p = world_1.world.points[pi];
        ctx.beginPath();
        ctx.arc(p.x, p.y, r * 0.45, 0, Math.PI * 2);
        ctx.fill();
    }
}
// ---- pins / grab / sparks / preview -------------------------------
function drawPins() {
    ctx.fillStyle = '#7ee0ff';
    for (const obj of world_1.world.objs) {
        // draw pinned points of cloth as small pins
        for (const pi of obj.points) {
            const p = world_1.world.points[pi];
            if (!p.pinned || interact_1.ptr.grabbed === pi)
                continue;
            ctx.fillStyle = 'rgba(20,26,38,0.9)';
            ctx.beginPath();
            ctx.arc(p.x, p.y, 3.6, 0, Math.PI * 2);
            ctx.fill();
            ctx.fillStyle = '#7ee0ff';
            ctx.beginPath();
            ctx.arc(p.x, p.y, 2, 0, Math.PI * 2);
            ctx.fill();
        }
    }
}
function drawGrab() {
    if (interact_1.ptr.grabbed >= 0) {
        const p = world_1.world.points[interact_1.ptr.grabbed];
        ctx.strokeStyle = '#7ee0ff';
        ctx.lineWidth = 2;
        ctx.beginPath();
        ctx.arc(p.x, p.y, 9, 0, Math.PI * 2);
        ctx.stroke();
        ctx.strokeStyle = 'rgba(126,224,255,0.35)';
        ctx.beginPath();
        ctx.arc(p.x, p.y, 3, 0, Math.PI * 2);
        ctx.stroke();
    }
    else if (interact_1.ptr.hover >= 0) {
        const p = world_1.world.points[interact_1.ptr.hover];
        ctx.strokeStyle = 'rgba(126,224,255,0.5)';
        ctx.lineWidth = 1.5;
        ctx.beginPath();
        ctx.arc(p.x, p.y, 7, 0, Math.PI * 2);
        ctx.stroke();
    }
}
function drawSparks(dt) {
    const arr = interact_1.ptr.sparks;
    ctx.fillStyle = '#ffd93d';
    for (let i = arr.length - 1; i >= 0; i--) {
        const s = arr[i];
        s.x += s.vx * dt;
        s.y += s.vy * dt;
        s.vx *= 0.9;
        s.vy = s.vy * 0.9 + 300 * dt;
        s.t -= dt;
        if (s.t <= 0) {
            arr.splice(i, 1);
            continue;
        }
        ctx.globalAlpha = Math.max(s.t * 2, 0);
        ctx.fillRect(s.x - 1.2, s.y - 1.2, 2.4, 2.4);
    }
    ctx.globalAlpha = 1;
}
function drawPreview() {
    if (!interact_1.ptr.preview)
        return;
    const pr = interact_1.ptr.preview;
    ctx.save();
    ctx.setLineDash([6, 5]);
    ctx.strokeStyle = 'rgba(140,170,230,0.85)';
    ctx.lineWidth = 1.5;
    if (interact_1.ui.mode === 'cloth') {
        ctx.strokeRect(pr.x0, pr.y0, pr.x1 - pr.x0, pr.y1 - pr.y0);
    }
    else if (interact_1.ui.mode === 'rope') {
        ctx.beginPath();
        ctx.moveTo(pr.x0, pr.y0);
        ctx.lineTo(pr.x1, pr.y1);
        ctx.stroke();
    }
    else if (interact_1.ui.mode === 'soft') {
        const rr = Math.hypot(pr.x1 - pr.x0, pr.y1 - pr.y0);
        ctx.beginPath();
        ctx.arc(pr.x0, pr.y0, rr, 0, Math.PI * 2);
        ctx.stroke();
    }
    ctx.restore();
}
function moveCursor(x, y) {
    interact_1.ptr.mx = x;
    interact_1.ptr.my = y;
}
/** Find nearest point within grabRadius of (x,y). */
function nearestPoint(x, y) {
    const r2 = world_1.cfg.grabRadius * world_1.cfg.grabRadius;
    let best = -1, bestD = r2;
    const pts = world_1.world.points;
    for (let i = 0; i < pts.length; i++) {
        const p = pts[i];
        const dx = p.x - x, dy = p.y - y;
        const d = dx * dx + dy * dy;
        if (d < bestD) {
            bestD = d;
            best = i;
        }
    }
    return best;
}
};

// ── module: src/input.ts ──
__mods["src/input.ts"] = function (exports, require, module) {
"use strict";
// ---------------------------------------------------------------
// Input handling: grab/throw, tear, pin, and tool-driven spawning.
// Attached to the canvas by main.
// ---------------------------------------------------------------
Object.defineProperty(exports, "__esModule", { value: true });
exports.initInput = initInput;
exports.applyGrab = applyGrab;
const world_1 = require("./world");
const interact_1 = require("./interact");
const engine_1 = require("./engine");
const objects_1 = require("./objects");
const render_1 = require("./render");
const types_1 = require("./types");
let cv;
let pinMode = false; // shift held
function initInput(canvas) {
    cv = canvas;
    cv.addEventListener('pointerdown', (e) => {
        cv.setPointerCapture(e.pointerId);
        const x = e.offsetX, y = e.offsetY;
        (0, render_1.moveCursor)(x, y);
        if (e.button === 2) {
            // tear
            interact_1.ptr.tearing = true;
            interact_1.ptr.downX = x;
            interact_1.ptr.downY = y;
            (0, engine_1.cutSegment)(x - 1, y - 1, x, y); // nick right away
            (0, interact_1.spark)(x, y);
            e.preventDefault();
            return;
        }
        interact_1.ptr.dragging = true;
        interact_1.ptr.downX = x;
        interact_1.ptr.downY = y;
        if (interact_1.ui.mode === 'rope') {
            interact_1.ptr.preview = { x0: x, y0: y, x1: x, y1: y };
            return;
        }
        if (interact_1.ui.mode === 'cloth') {
            interact_1.ptr.preview = { x0: x, y0: y, x1: x, y1: y };
            return;
        }
        if (interact_1.ui.mode === 'soft') {
            interact_1.ptr.preview = { x0: x, y0: y, x1: x, y1: y };
            return;
        }
        if (interact_1.ui.mode === 'ball') {
            (0, objects_1.spawnBall)(x, y, 12);
            interact_1.ptr.dragging = false;
            return;
        }
        // grab mode
        if (e.shiftKey) {
            pinMode = true;
            togglePin(x, y);
            interact_1.ptr.dragging = false;
            return;
        }
        const idx = (0, render_1.nearestPoint)(x, y);
        if (idx >= 0) {
            startGrab(idx, x, y);
        }
        else
            interact_1.ptr.dragging = false;
    });
    cv.addEventListener('pointermove', (e) => {
        const x = e.offsetX, y = e.offsetY;
        (0, render_1.moveCursor)(x, y);
        if (interact_1.ptr.tearing) {
            (0, engine_1.cutSegment)(interact_1.ptr.downX, interact_1.ptr.downY, x, y);
            (0, interact_1.spark)((interact_1.ptr.downX + x) / 2, (interact_1.ptr.downY + y) / 2);
            interact_1.ptr.downX = x;
            interact_1.ptr.downY = y;
            return;
        }
        if (interact_1.ptr.grabbed >= 0) {
            interact_1.ptr.pGX = interact_1.ptr.gx;
            interact_1.ptr.pGY = interact_1.ptr.gy;
            interact_1.ptr.gx = (0, types_1.clamp)(x, 2, world_1.cfg.width - 2);
            interact_1.ptr.gy = (0, types_1.clamp)(y, 2, world_1.cfg.height - 2);
            return;
        }
        if (interact_1.ptr.preview) {
            interact_1.ptr.preview.x1 = x;
            interact_1.ptr.preview.y1 = y;
            return;
        }
        if (interact_1.ui.mode === 'grab' && !pinMode)
            interact_1.ptr.hover = (0, render_1.nearestPoint)(x, y);
    });
    const end = (e) => {
        const x = e.offsetX, y = e.offsetY;
        if (interact_1.ptr.tearing) {
            interact_1.ptr.tearing = false;
            return;
        }
        if (interact_1.ptr.grabbed >= 0) {
            releaseGrab();
            return;
        }
        if (interact_1.ptr.preview) {
            const pr = interact_1.ptr.preview;
            const w = Math.abs(pr.x1 - pr.x0), h = Math.abs(pr.y1 - pr.y0);
            if (interact_1.ui.mode === 'rope') {
                if (Math.hypot(pr.x1 - pr.x0, pr.y1 - pr.y0) > 6)
                    (0, objects_1.spawnRope)(pr.x0, pr.y0, pr.x1, pr.y1);
            }
            else if (interact_1.ui.mode === 'cloth') {
                if (w > 6 && h > 6)
                    (0, objects_1.spawnCloth)(pr.x0, pr.y0, pr.x1, pr.y1, interact_1.ui.pinPattern);
            }
            else if (interact_1.ui.mode === 'soft') {
                const rr = Math.hypot(pr.x1 - pr.x0, pr.y1 - pr.y0);
                if (rr > 8)
                    (0, objects_1.spawnBlob)(pr.x0, pr.y0, rr);
            }
            interact_1.ptr.preview = null;
        }
        interact_1.ptr.dragging = false;
        pinMode = false;
    };
    cv.addEventListener('pointerup', end);
    cv.addEventListener('pointercancel', end);
    cv.addEventListener('contextmenu', (e) => e.preventDefault());
    window.addEventListener('keydown', (e) => {
        if (e.code === 'ShiftLeft' || e.code === 'ShiftRight')
            pinMode = true;
    });
    window.addEventListener('keyup', (e) => {
        if (e.code === 'ShiftLeft' || e.code === 'ShiftRight')
            pinMode = false;
    });
}
function togglePin(x, y) {
    const idx = (0, render_1.nearestPoint)(x, y);
    if (idx < 0)
        return;
    const p = world_1.world.points[idx];
    p.pinned = !p.pinned;
    if (p.pinned) {
        p.px = p.x;
        p.py = p.y;
    }
}
function startGrab(idx, x, y) {
    interact_1.ptr.grabbed = idx;
    interact_1.ptr.gx = (0, types_1.clamp)(x, 2, world_1.cfg.width - 2);
    interact_1.ptr.gy = (0, types_1.clamp)(y, 2, world_1.cfg.height - 2);
    interact_1.ptr.pGX = world_1.world.points[idx].x;
    interact_1.ptr.pGY = world_1.world.points[idx].y;
    interact_1.ptr.hover = -1;
}
function releaseGrab() {
    // Velocity is already captured in px/py from the last grab update.
    interact_1.ptr.grabbed = -1;
}
/** Snaps the grabbed point to the cursor at the END of a physics step,
 *  carrying the cursor's previous position as throw velocity. */
function applyGrab() {
    if (interact_1.ptr.grabbed < 0)
        return;
    const p = world_1.world.points[interact_1.ptr.grabbed];
    p.px = interact_1.ptr.pGX;
    p.py = interact_1.ptr.pGY;
    p.x = interact_1.ptr.gx;
    p.y = interact_1.ptr.gy;
}
};

// ── module: src/ui.ts ──
__mods["src/ui.ts"] = function (exports, require, module) {
"use strict";
// ---------------------------------------------------------------
// Toolbar wiring + HUD readout. Pure DOM glue.
// ---------------------------------------------------------------
Object.defineProperty(exports, "__esModule", { value: true });
exports.initUI = initUI;
exports.togglePause = togglePause;
exports.updateHud = updateHud;
const world_1 = require("./world");
const interact_1 = require("./interact");
const objects_1 = require("./objects");
let pauseBtn;
function initUI() {
    // tool buttons
    const tools = Array.from(document.querySelectorAll('.tool'));
    tools.forEach((b) => {
        b.addEventListener('click', () => {
            interact_1.ui.mode = b.dataset.tool;
            tools.forEach((o) => o.classList.toggle('on', o === b));
        });
    });
    // checkboxes
    bindCheck('ck-grav', (v) => { world_1.cfg.gravityOn = v; });
    bindCheck('ck-wind', (v) => { world_1.cfg.windOn = v; });
    bindCheck('ck-stress', (v) => { interact_1.ui.stress = v; });
    bindCheck('ck-trail', (v) => { interact_1.ui.trail = v; });
    // sliders
    const sg = document.getElementById('sl-grav');
    sg.addEventListener('input', () => { world_1.cfg.gravity = Number(sg.value); });
    const si = document.getElementById('sl-iter');
    si.addEventListener('input', () => { world_1.cfg.iterations = Number(si.value); });
    // pin pattern
    const pin = document.getElementById('pinpat');
    pin.addEventListener('change', () => { interact_1.ui.pinPattern = pin.value; });
    pauseBtn = document.getElementById('btn-pause');
    pauseBtn.addEventListener('click', togglePause);
    const resetBtn = document.getElementById('btn-reset');
    resetBtn.addEventListener('click', () => {
        (0, objects_1.clearWorld)();
        world_1.world.circles.length = 0;
        world_1.world.boxes.length = 0;
        (0, objects_1.initScene)(world_1.cfg.width, world_1.cfg.height);
    });
    window.addEventListener('keydown', (e) => {
        if (e.code === 'Space') {
            e.preventDefault();
            togglePause();
        }
    });
}
function bindCheck(id, fn) {
    const el = document.getElementById(id);
    el.addEventListener('change', () => fn(el.checked));
}
function togglePause() {
    world_1.physics.paused = !world_1.physics.paused;
    if (pauseBtn)
        pauseBtn.textContent = world_1.physics.paused ? 'Play' : 'Pause';
}
function updateHud(fps, stepMs) {
    const f = document.getElementById('hud-fps');
    const m = document.getElementById('hud-ms');
    const p = document.getElementById('hud-pts');
    if (f)
        f.innerHTML = `<b>${Math.round(fps)}</b> fps`;
    if (m)
        m.innerHTML = `<b>${stepMs.toFixed(2)}</b> ms/step`;
    if (p)
        p.innerHTML = `<b>${world_1.world.points.length}</b> pts`;
}
};

// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"deepseek/deepseek-v4-flash-0731","provider":"openrouter","persona":"typescript","sessionId":"3417656d-0a68-4e85-b16b-9dffb4438213","tokensIn":4078971,"tokensOut":79555,"tokensTotal":4158526,"cost":0.103942746,"turns":73,"toolCalls":76,"failedToolCalls":0,"timestamp":"2026-08-01T17:15:25.236Z"} -->