← Physics Playground results

Physics Playground

nex agi Nex N2.5 mini GGUF · typescript

Initial view of Physics Playground

Reviews & generation details

Reviews

No human review yet. Unreviewed does not mean unsuccessful.

Generation context

Imported historical artifact. Its exact original instructions and lineage are not known.

Generation prompt · exact text unavailable

See the current prompt for reference. It may differ from the instructions used to generate this artifact.

Inspect original source 39,659 bytes · SHA-256 08e6aa612763
<!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: playground ── */
:root { color-scheme: dark; }
* { box-sizing: border-box; }
html, body { margin: 0; width: 100%; height: 100%; overflow: hidden; background: #0b0f17; font-family: Inter, ui-sans-serif, system-ui, -apple-system, sans-serif; }
body { color: #e8edf7; }
#playground { position: fixed; inset: 0; display: block; touch-action: none; cursor: crosshair; }
.hud-top { position: fixed; top: 18px; left: 22px; right: 22px; display: flex; justify-content: space-between; align-items: flex-start; pointer-events: none; z-index: 3; }
.brand { display: flex; align-items: center; gap: 10px; letter-spacing: .18em; font-size: 13px; font-weight: 750; color: #f4f7fb; text-shadow: 0 1px 12px #000; }
.brand span:last-child span { color: #5ee8ff; }
.brand-mark { width: 11px; height: 11px; border-radius: 50%; border: 2px solid #5ee8ff; box-shadow: 0 0 16px #5ee8ff66, inset 0 0 7px #5ee8ff55; }
.stats { display: flex; gap: 8px; font: 600 10px/1.2 ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: .04em; color: #9eabc2; }
.stats span { border: 1px solid #ffffff1c; border-radius: 5px; padding: 7px 8px; background: #ffffff08; backdrop-filter: blur(10px); }
.toolbar { position: fixed; top: 66px; left: 50%; transform: translateX(-50%); display: flex; align-items: center; gap: 9px; padding: 8px 10px; border: 1px solid #ffffff19; border-radius: 12px; background: #101827d9; box-shadow: 0 10px 32px #0005, inset 0 1px #ffffff12; backdrop-filter: blur(14px); color: #cbd6e7; z-index: 4; }
.toolbar::after { content: ''; position: absolute; top: -4px; left: 50%; width: 46px; height: 1px; background: #5ee8ff55; transform: translateX(-50%); }
.tool, .mini-toggle, .pill-toggle { border: 1px solid transparent; color: #9aa8bf; background: transparent; font: 700 10px/1 ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: .07em; border-radius: 6px; cursor: pointer; transition: .18s ease; }
.tool { padding: 9px 11px; text-transform: uppercase; }
.tool:hover, .mini-toggle:hover, .pill-toggle:hover { color: #f1f7ff; border-color: #ffffff1e; background: #ffffff0d; }
.tool.active, .mini-toggle.active { color: #081018; background: #5ee8ff; border-color: #86f3ff; box-shadow: 0 0 16px #5ee8ff3a; }
.pill-toggle { padding: 8px 10px; }
.pill-toggle.danger { color: #ff9eab; }
.pill-toggle.active { color: #a5f4ff; border-color: #5ee8ff33; background: #5ee8ff12; }
.divider { width: 1px; height: 20px; background: #ffffff1d; }
.mode-group { display: flex; gap: 3px; }
.control { display: flex; flex-direction: column; gap: 5px; min-width: 100px; }
.control label { display: flex; justify-content: space-between; gap: 12px; font: 700 9px/1 ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: .09em; color: #8795aa; }
.control output { color: #c3e7ef; font-weight: 600; }
input[type=range] { width: 100%; accent-color: #5ee8ff; cursor: pointer; }
.mini-toggle { padding: 8px 7px; font-size: 8px; }
.hint { position: fixed; bottom: 18px; left: 50%; transform: translateX(-50%); padding: 8px 12px; border: 1px solid #ffffff11; border-radius: 7px; color: #6f7e96; font: 600 9px/1 ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: .08em; background: #0a0f1899; z-index: 3; pointer-events: none; }
.toast { position: fixed; top: 42px; left: 50%; opacity: 0; transform: translate(-50%, -8px); padding: 7px 11px; border-radius: 6px; color: #d9fbff; background: #5ee8ff1a; border: 1px solid #5ee8ff30; font: 600 10px/1 ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: .07em; z-index: 5; pointer-events: none; transition: .22s ease; }
.toast.show { opacity: 1; transform: translate(-50%, 0); }
@media (max-width: 760px) { .toolbar { flex-wrap: wrap; justify-content: center; max-width: calc(100vw - 24px); } .control { min-width: 86px; } .hint { display: none; } }
</style>
</head>
<body>
<canvas id="playground"></canvas>
<div class="hud-top">
  <div class="brand"><span class="brand-mark"></span><span>SOFT<span>/</span>BODY</span></div>
  <div class="stats"><span id="fps">60 FPS</span><span id="step-readout">0.00 MS</span><span id="body-readout">0 BODIES</span></div>
</div>
<div class="toolbar" id="toolbar">
  <div class="tool-group mode-group" id="mode-group">
    <button class="tool active" data-mode="select">Grab</button>
    <button class="tool" data-mode="rope">Rope</button>
    <button class="tool" data-mode="cloth">Cloth</button>
    <button class="tool" data-mode="blob">Blob</button>
    <button class="tool" data-mode="ball">Ball</button>
  </div>
  <div class="divider"></div>
  <div class="tool-group">
    <button id="pin-corners" class="mini-toggle active" title="Pin both cloth corners">2 CORNERS</button>
    <button id="pin-edge" class="mini-toggle" title="Pin entire top edge">FULL EDGE</button>
  </div>
  <div class="divider"></div>
  <div class="control">
    <label>GRAVITY <output id="gravity-value">1.00</output></label>
    <input id="gravity" type="range" min="0" max="2000" step="20" value="980">
  </div>
  <div class="control">
    <label>STIFFNESS <output id="iteration-value">8</output></label>
    <input id="iterations" type="range" min="2" max="12" step="1" value="8">
  </div>
  <button id="gravity-toggle" class="pill-toggle active">GRAVITY</button>
  <button id="wind-toggle" class="pill-toggle">WIND</button>
  <button id="stress-toggle" class="pill-toggle">STRESS</button>
  <div class="divider"></div>
  <button id="pause" class="pill-toggle">PAUSE</button>
  <button id="step" class="pill-toggle">STEP</button>
  <button id="reset" class="pill-toggle danger">RESET</button>
</div>
<div class="hint" id="hint">DRAG TO DRAW · RIGHT-DRAG TO CUT · SHIFT-CLICK TO PIN</div>
<div class="toast" id="toast"></div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./config":"src/config.ts","./physics":"src/physics.ts","./interaction":"src/interaction.ts","./render":"src/render.ts"},"src/physics.ts":{"./config":"src/config.ts"},"src/interaction.ts":{"./config":"src/config.ts"},"src/render.ts":{"./config":"src/config.ts"}};
function __require(id) {
  if (__cache[id]) return __cache[id].exports;
  var module = __cache[id] = { exports: {} };
  var factory = __mods[id];
  if (!factory) throw new Error("Module not found: " + id);
  factory(module.exports, function (spec) {
    var target = (__map[id] && __map[id][spec]) || spec;
    return __require(target);
  }, module);
  return module.exports;
}

// ── module: src/main.ts ──
__mods["src/main.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const config_1 = require("./config");
const physics_1 = require("./physics");
const interaction_1 = require("./interaction");
const render_1 = require("./render");
const canvas = document.getElementById('playground');
const fpsEl = document.getElementById('fps');
const stepEl = document.getElementById('step-readout');
const bodyEl = document.getElementById('body-readout');
const gravitySlider = document.getElementById('gravity');
const gravityValue = document.getElementById('gravity-value');
const iterationsSlider = document.getElementById('iterations');
const iterationsValue = document.getElementById('iteration-value');
const gravityToggle = document.getElementById('gravity-toggle');
const windToggle = document.getElementById('wind-toggle');
const stressToggle = document.getElementById('stress-toggle');
const pauseButton = document.getElementById('pause');
const stepButton = document.getElementById('step');
const resetButton = document.getElementById('reset');
const pinCorners = document.getElementById('pin-corners');
const pinEdge = document.getElementById('pin-edge');
const hint = document.getElementById('hint');
const toast = document.getElementById('toast');
let width = 0, height = 0, dpr = 1, lastTime = performance.now(), lastFps = lastTime, fpsSamples = 0;
let physicsTime = 0;
let stressMode = true;
let toastTimer = 0;
function resize() {
    dpr = Math.min(window.devicePixelRatio || 1, 2);
    width = window.innerWidth;
    height = window.innerHeight;
    canvas.width = Math.round(width * dpr);
    canvas.height = Math.round(height * dpr);
    canvas.style.width = `${width}px`;
    canvas.style.height = `${height}px`;
    config_1.CONFIG.bounds[0] = 0;
    config_1.CONFIG.bounds[1] = 0;
    config_1.CONFIG.bounds[2] = width;
    config_1.CONFIG.bounds[3] = height;
}
function showToast(message) {
    toast.textContent = message;
    toast.classList.add('show');
    window.clearTimeout(toastTimer);
    toastTimer = window.setTimeout(() => toast.classList.remove('show'), 1600);
}
function updateStats(fps, now) {
    fpsEl.textContent = `${Math.round(fps)} FPS`;
    stepEl.textContent = `${physics_1.sim.lastStepMs.toFixed(2)} MS`;
    bodyEl.textContent = `${physics_1.sim.bodies.length} BODIES · ${physics_1.sim.points.length} PTS`;
    gravityValue.value = `${(config_1.CONFIG.gravity / 980).toFixed(2)}`;
    iterationsValue.value = String(config_1.CONFIG.iterations);
}
function applyGravity() { config_1.CONFIG.gravityOn = config_1.CONFIG.gravity > 0; gravityToggle.classList.toggle('active', config_1.CONFIG.gravityOn); }
function applyIterations() { config_1.CONFIG.iterations = Number(iterationsSlider.value); }
function togglePause(force) { physics_1.sim.paused = force !== undefined ? force : !physics_1.sim.paused; pauseButton.classList.toggle('active', physics_1.sim.paused); pauseButton.textContent = physics_1.sim.paused ? 'RUN' : 'PAUSE'; if (!physics_1.sim.paused)
    lastTime = performance.now(); }
function setStaticCircle(x, y, r) { physics_1.sim.solidCircles = [{ x, y, r }]; }
function resetWorld() {
    physics_1.sim.paused = true;
    (0, physics_1.clearWorld)();
    (0, physics_1.buildWorld)();
    physics_1.sim.paused = false;
    physicsTime = 0;
    lastTime = performance.now();
    togglePause(false);
    resize();
    showToast('PLAYGROUND RESET');
}
function resetSim(simRef) {
    simRef.paused = true;
    (0, physics_1.clearWorld)();
    (0, physics_1.buildWorld)();
    simRef.paused = false;
    physicsTime = 0;
    lastTime = performance.now();
}
function configureUI() {
    gravitySlider.addEventListener('input', () => { config_1.CONFIG.gravity = Number(gravitySlider.value); applyGravity(); });
    iterationsSlider.addEventListener('input', applyIterations);
    gravityToggle.addEventListener('click', () => { config_1.CONFIG.gravityOn = !config_1.CONFIG.gravityOn; applyGravity(); });
    windToggle.addEventListener('click', () => { config_1.CONFIG.windOn = !config_1.CONFIG.windOn; windToggle.classList.toggle('active', config_1.CONFIG.windOn); });
    stressToggle.addEventListener('click', () => { stressMode = !stressMode; stressToggle.classList.toggle('active', stressMode); });
    pauseButton.addEventListener('click', () => togglePause());
    stepButton.addEventListener('click', () => { if (!physics_1.sim.paused)
        togglePause(true); (0, physics_1.step)(); });
    resetButton.addEventListener('click', resetWorld);
    pinCorners.addEventListener('click', () => { pinCorners.classList.add('active'); pinEdge.classList.remove('active'); (0, interaction_1.setPinClothEdge)(false); showToast('PIN PATTERN · CORNERS'); });
    pinEdge.addEventListener('click', () => { pinEdge.classList.add('active'); pinCorners.classList.remove('active'); (0, interaction_1.setPinClothEdge)(true); showToast('PIN PATTERN · FULL EDGE'); });
    document.querySelectorAll('.tool').forEach(button => button.addEventListener('click', () => {
        document.querySelectorAll('.tool').forEach(item => item.classList.remove('active'));
        button.classList.add('active');
        (0, interaction_1.setMode)(button.dataset.mode);
        showToast(`${button.textContent || 'TOOL'}`.toUpperCase());
    }));
}
function frame(now) {
    const delta = Math.min(1 / 20, Math.max(0, (now - lastTime) / 1000));
    lastTime = now;
    fpsSamples += 1;
    if (now - lastFps >= 250)
        updateStats(fpsSamples * 1000 / Math.max(1, now - lastFps), now);
    lastFps = now;
    if (!physics_1.sim.paused) {
        physicsTime += delta;
        const maxStep = 1 / 20;
        while (physicsTime >= config_1.CONFIG.dt && physics_1.sim.frame < 1000000) {
            (0, physics_1.step)();
            physicsTime -= config_1.CONFIG.dt;
        }
    }
    (0, render_1.draw)(physics_1.sim, canvas);
    requestAnimationFrame(frame);
}
window.addEventListener('resize', () => {
    resize();
    if (physics_1.sim.points.length === 0)
        (0, physics_1.buildWorld)();
    if (physics_1.sim.points.length > 0) {
        for (const p of physics_1.sim.points) {
            if (p.pinned)
                continue;
            const r = p.radius || 0;
            if (p.x < r)
                p.x = r;
            if (p.x > width - r)
                p.x = width - r;
            if (p.y < r)
                p.y = r;
            if (p.y > height - 1 - r)
                p.y = height - 1 - r;
        }
    }
});
resize();
configureUI();
applyGravity();
applyIterations();
requestAnimationFrame(frame);
// Headless API: stable stepping and a few diagnostics used by browser probes.
Object.assign(window, {
    setTool: interaction_1.setMode,
    reset: () => { physics_1.sim.paused = true; (0, physics_1.clearWorld)(); (0, physics_1.buildWorld)(); physics_1.sim.paused = false; physicsTime = 0; lastTime = performance.now(); togglePause(false); resize(); },
    togglePause: () => togglePause(),
    setGravity: (value) => { gravitySlider.value = String(value); config_1.CONFIG.gravity = Number(gravitySlider.value); applyGravity(); },
    setIterations: (value) => { iterationsSlider.value = String(value); config_1.CONFIG.iterations = Number(iterationsSlider.value); },
    setPinClothEdge: interaction_1.setPinClothEdge,
    setStaticCircle,
    sim: physics_1.sim,
    simulate: physics_1.step,
    step: physics_1.step,
    addConstraint: physics_1.addConstraint,
    addPoint: physics_1.addPoint,
    cut: (a, b) => {
        const dx = b.x - a.x, dy = b.y - a.y, len2 = dx * dx + dy * dy;
        if (len2 > 1) {
            for (const c of physics_1.sim.constraints) {
                if (c.kind !== 'distance')
                    continue;
                const p0 = physics_1.sim.points[c.a], p1 = physics_1.sim.points[c.b];
                if (!p0 || !p1 || c.dead)
                    continue;
                const ex = p0.x - a.x, ey = p0.y - a.y;
                const t = (ex * dx + ey * dy) / len2;
                const cx = a.x + Math.max(0, Math.min(1, t)) * dx, cy = a.y + Math.max(0, Math.min(1, t)) * dy;
                const qdx = cx - p1.x, qdy = cy - p1.y;
                if (qdx * qdx + qdy * qdy < 81)
                    c.dead = true;
            }
        }
    },
    getKineticEnergy: () => physics_1.sim.points.reduce((sum, p) => { const vx = p.x - p.px, vy = p.y - p.py; return sum + vx * vx + vy * vy; }, 0),
    getFloor: () => height - 1,
});
window.addEventListener('pointermove', event => (0, render_1.setCursorPosition)(event.clientX, event.clientY));
window.addEventListener('pointerdown', event => {
    if (event.target.closest('.toolbar'))
        return;
    (0, interaction_1.applyInteractions)(physics_1.sim, event, { shift: event.shiftKey });
});
window.addEventListener('pointermove', event => { if (!event.target.closest('.toolbar'))
    (0, interaction_1.applyInteractions)(physics_1.sim, event, { shift: event.shiftKey }); });
window.addEventListener('pointerup', event => { if (!event.target.closest('.toolbar'))
    (0, interaction_1.applyInteractions)(physics_1.sim, event, { shift: event.shiftKey }); });
window.addEventListener('pointercancel', event => { if (!event.target.closest('.toolbar'))
    (0, interaction_1.applyInteractions)(physics_1.sim, event, { shift: event.shiftKey }); });
window.addEventListener('blur', () => { physicsTime = 0; lastTime = performance.now(); });
window.addEventListener('keydown', event => {
    if (event.code === 'Space') {
        event.preventDefault();
        togglePause();
    }
    if (event.code === 'ArrowRight' && physics_1.sim.paused) {
        (0, physics_1.step)();
    }
    if (event.code === 'KeyR')
        resetWorld();
});
};

// ── module: src/config.ts ──
__mods["src/config.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CONFIG = void 0;
exports.clamp = clamp;
exports.distance2 = distance2;
exports.isFiniteVec = isFiniteVec;
exports.setPin = setPin;
exports.CONFIG = {
    gravity: 980,
    gravityOn: true,
    damping: 0.985,
    dt: 1 / 60,
    iterations: 8,
    wind: 68,
    windOn: false,
    pinClothEdge: false,
    tearRadius: 7,
    cutRadius: 9,
    bounds: [0, 0, 0, 0],
    colors: ['#5ee8ff', '#ffca65', '#7dffa1', '#ff7d9d', '#b99aff', '#80e4ff'],
    staticCircles: [],
};
function clamp(value, min, max) { return Math.max(min, Math.min(max, value)); }
function distance2(a, b) { const dx = b.x - a.x; const dy = b.y - a.y; return dx * dx + dy * dy; }
function isFiniteVec(v) { return Number.isFinite(v.x) && Number.isFinite(v.y) && Number.isFinite(v.px) && Number.isFinite(v.py); }
function setPin(points, index, pinned) { if (Number.isInteger(index) && index >= 0 && index < points.length)
    points[index].pinned = pinned; }
};

// ── module: src/physics.ts ──
__mods["src/physics.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.sim = void 0;
exports.step = step;
exports.simulateFrame = simulateFrame;
exports.addPoint = addPoint;
exports.addConstraint = addConstraint;
exports.clearWorld = clearWorld;
exports.buildWorld = buildWorld;
const config_1 = require("./config");
let nextId = 1;
const maxCoordinate = 50000;
const maxVelocity = 4000;
function valid(v) { return Number.isFinite(v.x) && Number.isFinite(v.y) && Number.isFinite(v.px) && Number.isFinite(v.py); }
function clampPoint(point) {
    if (!valid(point)) {
        point.x = config_1.CONFIG.bounds[0];
        point.y = config_1.CONFIG.bounds[1];
        point.px = point.x;
        point.py = point.y;
        return;
    }
    point.x = (0, config_1.clamp)(point.x, -maxCoordinate, maxCoordinate);
    point.y = (0, config_1.clamp)(point.y, -maxCoordinate, maxCoordinate);
    point.px = (0, config_1.clamp)(point.px, -maxCoordinate, maxCoordinate);
    point.py = (0, config_1.clamp)(point.py, -maxCoordinate, maxCoordinate);
    const vx = (point.x - point.px) * config_1.CONFIG.damping;
    const vy = (point.y - point.py) * config_1.CONFIG.damping;
    point.px = point.x - (0, config_1.clamp)(vx, -maxVelocity, maxVelocity);
    point.py = point.y - (0, config_1.clamp)(vy, -maxVelocity, maxVelocity);
}
function solveDistance(c) {
    const a = sim.points[c.a];
    const b = sim.points[c.b];
    if (!a || !b || c.dead)
        return;
    const dx = b.x - a.x;
    const dy = b.y - a.y;
    const dist = Math.sqrt(dx * dx + dy * dy);
    if (!Number.isFinite(dist) || dist < 1e-6)
        return;
    const delta = (dist - c.rest) / dist;
    const stiffness = 0.5;
    let ax = 0, ay = 0, bx = 0, by = 0;
    if (!a.pinned && !a.grab) {
        ax = dx * delta * stiffness;
        ay = dy * delta * stiffness;
        a.x -= ax;
        a.y -= ay;
    }
    if (!b.pinned && !b.grab) {
        bx = -dx * delta * stiffness;
        by = -dy * delta * stiffness;
        b.x += bx;
        b.y += by;
    }
    c.stress = dist / c.rest;
}
function solvePin(c) { const p = sim.points[c.a]; if (p)
    p.pinned = true; }
function integrate(dt) {
    const dt2 = dt * dt;
    const gx = config_1.CONFIG.windOn ? config_1.CONFIG.wind : 0;
    const gy = config_1.CONFIG.gravityOn ? config_1.CONFIG.gravity : 0;
    for (let i = 0; i < sim.points.length; i++) {
        const p = sim.points[i];
        if (p.pinned)
            continue;
        const nx = p.x + (p.x - p.px) * config_1.CONFIG.damping + gx * dt2;
        const ny = p.y + (p.y - p.py) * config_1.CONFIG.damping + gy * dt2;
        p.px = p.x;
        p.py = p.y;
        p.x = nx;
        p.y = ny;
        clampPoint(p);
    }
}
function settleSleep(points, epsilon = 0.12) {
    for (const p of points) {
        const velocity = Math.sqrt((p.x - p.px) ** 2 + (p.y - p.py) ** 2);
        if (velocity <= epsilon) {
            p.px = p.x;
            p.py = p.y;
        }
    }
}
function resetGrabbedPoints() {
    for (const p of sim.points)
        if (p.grab) {
            p.px = p.x;
            p.py = p.y;
        }
}
function collide() {
    const [w, h] = [innerWidth, innerHeight];
    const floor = h - 1;
    for (const p of sim.points) {
        if (p.pinned || p.grab)
            continue;
        const r = p.radius || 0;
        if (p.x < r)
            p.x = r;
        if (p.x > w - r)
            p.x = w - r;
        if (p.y < r)
            p.y = r;
        if (p.y > floor - r) {
            p.y = floor - r;
            p.px = p.x;
            p.py = p.y + (p.py - p.y) * 0.3;
        }
        for (const shape of sim.solidCircles) {
            const dx = p.x - shape.x;
            const dy = p.y - shape.y;
            const dist = Math.sqrt(dx * dx + dy * dy);
            const minDist = shape.r + r;
            if (!Number.isFinite(dist) || dist < minDist) {
                const nx = dist > 1e-6 ? dx / dist : 0, ny = dist > 1e-6 ? dy / dist : 1;
                p.x = shape.x + nx * minDist;
                p.y = shape.y + ny * minDist;
                if (dist > 1e-6) {
                    const vx = (p.x - p.px) * 0.6, vy = (p.y - p.py) * 0.6;
                    p.px = p.x - vx;
                    p.py = p.y - vy;
                }
            }
        }
    }
}
function step() {
    const start = performance.now();
    simulateFrame(config_1.CONFIG.dt);
    sim.time += config_1.CONFIG.dt;
    sim.frame++;
    sim.lastStepMs = performance.now() - start;
}
function simulateFrame(dt) {
    integrate(dt);
    for (let i = 0; i < config_1.CONFIG.iterations; i++) {
        for (const c of sim.constraints)
            if (!c.dead && c.kind === 'distance')
                solveDistance(c);
        for (const c of sim.constraints)
            if (!c.dead && c.kind === 'pin')
                solvePin(c);
    }
    collide();
    resetGrabbedPoints();
    settleSleep(sim.points);
}
function addPoint(x, y, pinned = false, radius, group = '') {
    const p = { x, y, px: x, py: y, pinned, grab: false, radius, group };
    clampPoint(p);
    sim.points.push(p);
    return sim.points.length - 1;
}
function addConstraint(a, b, rest, kind = 'distance') {
    if (!Number.isInteger(a) || !Number.isInteger(b) || a === b || a < 0 || b < 0 || a >= sim.points.length || b >= sim.points.length)
        return;
    const pa = sim.points[a], pb = sim.points[b];
    if (!pa || !pb)
        return;
    let d = rest;
    if (d === undefined) {
        const dx = pb.x - pa.x;
        const dy = pb.y - pa.y;
        d = Math.sqrt(dx * dx + dy * dy);
    }
    if (!Number.isFinite(d) || d <= 0)
        d = 1;
    sim.constraints.push({ a, b, rest: d, stress: 1, dead: false, kind });
}
function clearWorld() { sim.points = []; sim.constraints = []; sim.bodies = []; sim.grabIndex = undefined; sim.grabPrev = null; }
function buildWorld() {
    clearWorld();
    addPoint(130, 145, true);
    addPoint(250, 145, true);
    const rope = [];
    let prev = addPoint(130, 145, true);
    rope.push(prev);
    for (let i = 1; i < 9; i++) {
        const y = 145 + i * 18;
        const x = 130 + Math.sin(i * 1.2) * 3;
        const n = addPoint(x, y, i === 0);
        rope.push(n);
        addConstraint(prev, n);
        prev = n;
    }
    const blobPts = [];
    for (let i = 0; i < 8; i++) {
        const a = Math.PI * 2 * i / 8 - Math.PI / 2;
        blobPts.push(addPoint(410 + Math.cos(a) * 58, 240 + Math.sin(a) * 58, true));
    }
    for (let i = 0; i < blobPts.length; i++) {
        const j = (i + 1) % blobPts.length;
        addConstraint(blobPts[i], blobPts[j]);
        if (i % 2 === 0)
            addConstraint(blobPts[i], blobPts[(i + 3) % blobPts.length]);
    }
    sim.bodies.push({ id: `body-${nextId++}`, name: 'Demo Blob', hue: 158, points: blobPts, constraints: sim.constraints.map((_, i) => i), pinned: blobPts.map(() => false), shape: 'blob' });
    for (let i = 0; i < 3; i++)
        addConstraint(115 + i * 13, 125 + i * 13, 11, 'pin');
    const cloth = [];
    const cols = 7, rows = 5;
    const ox = 500, oy = 230, sx = 24, sy = 24;
    for (let row = 0; row < rows; row++)
        for (let col = 0; col < cols; col++) {
            const x = ox + col * sx;
            const y = oy + row * sy;
            const idx = addPoint(x, y, row === 0 && col % 2 === 0);
            cloth.push(idx);
        }
    const id = (r, c) => r * cols + c;
    for (let r = 0; r < rows; r++)
        for (let c = 0; c < cols; c++) {
            const i = id(r, c);
            if (c < cols - 1)
                addConstraint(i, id(r, c + 1));
            if (r < rows - 1)
                addConstraint(i, id(r + 1, c));
            if (r < rows - 1 && c < cols - 1)
                addConstraint(i, id(r + 1, c + 1));
            if (r < rows - 1 && c > 0)
                addConstraint(i, id(r + 1, c - 1));
        }
    sim.bodies.push({ id: `body-${nextId++}`, name: 'Demo Cloth', hue: 204, points: cloth, constraints: sim.constraints.map((_, i) => i), pinned: cloth.map((_, i) => i === 0 || i === cols - 1), shape: 'cloth' });
    sim.solidCircles = [{ x: 570, y: 300, r: 58 }];
}
let sim = { points: [], constraints: [], bodies: [], solidCircles: [], running: true, paused: false, time: 0, fps: 60, frame: 0, grabPrev: null, lastStepMs: 0, step: null, simulateFrame: null, clearWorld: null, buildWorld: null };
exports.sim = sim;
sim.points = [];
sim.constraints = [];
sim.bodies = [];
sim.solidCircles = [];
buildWorld();
sim.step = step;
sim.simulateFrame = simulateFrame;
sim.clearWorld = clearWorld;
sim.buildWorld = buildWorld;
Object.assign(window, { sim });
Object.assign(window, { step });
};

// ── module: src/interaction.ts ──
__mods["src/interaction.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.cutState = exports.pinClothEdge = exports.mode = void 0;
exports.cutLine = cutLine;
exports.onPointerDown = onPointerDown;
exports.onPointerMove = onPointerMove;
exports.onPointerUp = onPointerUp;
exports.spawnAt = spawnAt;
exports.setMode = setMode;
exports.setPinClothEdge = setPinClothEdge;
exports.updateGrab = updateGrab;
exports.applyInteractions = applyInteractions;
const config_1 = require("./config");
let sim = null;
let mode = 'select';
exports.mode = mode;
let pinClothEdge = false;
exports.pinClothEdge = pinClothEdge;
const cutState = { active: false, start: { x: 0, y: 0 }, last: { x: 0, y: 0 } };
exports.cutState = cutState;
function nearestPoint(x, y, radius = 22) {
    if (!sim || !sim.points)
        return null;
    let best = null;
    for (let i = 0; i < sim.points.length; i++) {
        const p = sim.points[i];
        const dx = p.x - x;
        const dy = p.y - y;
        const d = Math.sqrt(dx * dx + dy * dy);
        if (best === null || d < best.distance)
            best = { index: i, distance: d };
    }
    return best && best.distance <= radius ? best : null;
}
function setGrab(index, pointer) {
    if (!sim)
        return;
    if (sim.grabIndex !== undefined)
        sim.points[sim.grabIndex].grab = false;
    if (index === undefined) {
        sim.grabIndex = undefined;
        sim.grabPrev = null;
        return;
    }
    const p = sim.points[index];
    if (!p || p.pinned)
        return;
    p.grab = true;
    sim.grabIndex = index;
    sim.grabPrev = { x: pointer.x, y: pointer.y };
}
function cutLine(a, b) {
    if (!sim)
        return;
    const dx = b.x - a.x, dy = b.y - a.y;
    const len2 = dx * dx + dy * dy;
    if (len2 < 1)
        return;
    for (const c of sim.constraints) {
        if (c.kind !== 'distance')
            continue;
        const p0 = sim.points[c.a], p1 = sim.points[c.b];
        if (!p0 || !p1 || c.dead)
            continue;
        const ex = p0.x - a.x, ey = p0.y - a.y;
        const t = (ex * dx + ey * dy) / len2;
        const cx = a.x + Math.max(0, Math.min(1, t)) * dx, cy = a.y + Math.max(0, Math.min(1, t)) * dy;
        const qdx = cx - p1.x, qdy = cy - p1.y;
        if (qdx * qdx + qdy * qdy < config_1.CONFIG.cutRadius * config_1.CONFIG.cutRadius)
            c.dead = true;
    }
}
function onPointerDown(x, y, button, modifiers) {
    if (!sim)
        return;
    if (button === 2 || modifiers.shift) {
        const hit = nearestPoint(x, y, 18);
        if (hit)
            sim.points[hit.index].pinned = !sim.points[hit.index].pinned;
        setGrab(sim.grabIndex, { x, y });
        if (button === 2) {
            cutState.active = true;
            cutState.start = { x, y };
            cutState.last = { x, y };
        }
        return;
    }
    if (button !== 0)
        return;
    setGrab(undefined, { x, y });
    const hit = nearestPoint(x, y, mode === 'select' ? 24 : 0);
    if (mode === 'select' && hit) {
        setGrab(hit.index, { x, y });
        return;
    }
    spawnAt(x, y);
}
function onPointerMove(x, y, modifiers) {
    if (!sim)
        return;
    if (cutState.active) {
        cutState.last = { x, y };
        cutLine(cutState.start, cutState.last);
        return;
    }
    if (modifiers.shift) {
        const hit = nearestPoint(x, y, 18);
        if (hit)
            sim.hoverIndex = hit.index;
    }
    if (sim.grabIndex !== undefined) {
        const p = sim.points[sim.grabIndex];
        if (p) {
            p.px = p.x;
            p.py = p.y;
            p.x = x;
            p.y = y;
        }
    }
}
function onPointerUp() {
    if (cutState.active)
        cutState.active = false;
    if (sim && sim.grabIndex !== undefined) {
        const p = sim.points[sim.grabIndex];
        if (p) {
            p.px = p.x;
            p.py = p.y;
        }
    }
    setGrab(undefined, { x: 0, y: 0 });
}
function spawnAt(x, y) {
    if (!sim)
        return;
    if (mode === 'ball') {
        sim.points.push({ x, y, px: x - 1, py: y, pinned: false, grab: false, radius: 18, group: 'ball' });
        return;
    }
    if (mode === 'rope') {
        let prev = sim.points.push({ x, y, px: x, py: y, pinned: true, grab: false, group: 'rope' }) - 1;
        for (let i = 1; i < 10; i++) {
            const p = sim.points.push({ x: x + i * 1.5, y: y + i * 22, px: x + i * 1.5, py: y + i * 22, pinned: false, grab: false, group: 'rope' }) - 1;
            sim.constraints.push({ a: prev, b: p, rest: 22.1, stress: 1, dead: false, kind: 'distance' });
            prev = p;
        }
        return;
    }
    if (mode === 'blob') {
        const pts = [];
        for (let i = 0; i < 8; i++) {
            const a = Math.PI * 2 * i / 8 - Math.PI / 2;
            const px = x + Math.cos(a) * 48, py = y + Math.sin(a) * 48;
            pts.push(sim.points.push({ x: px, y: py, px, py, pinned: false, grab: false, group: 'blob' }) - 1);
        }
        for (let i = 0; i < pts.length; i++) {
            const j = (i + 1) % pts.length;
            sim.constraints.push({ a: pts[i], b: pts[j], rest: 48 * Math.SQRT2, stress: 1, dead: false, kind: 'distance' });
            if (i % 2 === 0)
                sim.constraints.push({ a: pts[i], b: pts[(i + 3) % pts.length], rest: 96 * Math.SQRT2 / 2, stress: 1, dead: false, kind: 'distance' });
        }
        sim.bodies.push({ id: `body-${Date.now()}`, name: 'Blob', hue: 158, points: pts, constraints: [], pinned: pts.map(() => false), shape: 'blob' });
        return;
    }
    const cols = 6, rows = 4, ox = x - (cols - 1) * 18, oy = y - (rows - 1) * 18, pts = [];
    for (let r = 0; r < rows; r++)
        for (let c = 0; c < cols; c++) {
            const px = ox + c * 18, py = oy + r * 18;
            pts.push(sim.points.push({ x: px, y: py, px, py, pinned: pinClothEdge || (r === 0 && c % 2 === 0), grab: false, group: 'cloth' }) - 1);
        }
    const id = (r, c) => r * cols + c;
    for (let r = 0; r < rows; r++)
        for (let c = 0; c < cols; c++) {
            const i = id(r, c);
            if (c < cols - 1)
                sim.constraints.push({ a: i, b: id(r, c + 1), rest: 18, stress: 1, dead: false, kind: 'distance' });
            if (r < rows - 1)
                sim.constraints.push({ a: i, b: id(r + 1, c), rest: 18, stress: 1, dead: false, kind: 'distance' });
            if (r < rows - 1 && c < cols - 1)
                sim.constraints.push({ a: i, b: id(r + 1, c + 1), rest: 25.45, stress: 1, dead: false, kind: 'distance' });
            if (r < rows - 1 && c > 0)
                sim.constraints.push({ a: i, b: id(r + 1, c - 1), rest: 25.45, stress: 1, dead: false, kind: 'distance' });
        }
    sim.bodies.push({ id: `body-${Date.now()}`, name: 'Cloth', hue: 204, points: pts, constraints: [], pinned: pts.map(() => false), shape: 'cloth' });
}
function setMode(value) { exports.mode = mode = value; }
function setPinClothEdge(value) { exports.pinClothEdge = pinClothEdge = value; }
function updateGrab(pointer) { if (sim && sim.grabIndex !== undefined) {
    sim.points[sim.grabIndex].x = pointer.x;
    sim.points[sim.grabIndex].y = pointer.y;
} }
function applyInteractions(simRef, event, modifiers) { sim = simRef; const x = event.clientX, y = event.clientY; if (event.type === 'pointerdown')
    onPointerDown(x, y, event.button, modifiers);
else if (event.type === 'pointermove')
    onPointerMove(x, y, modifiers);
else if (event.type === 'pointerup')
    onPointerUp(); }
};

// ── module: src/render.ts ──
__mods["src/render.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.draw = draw;
exports.setCursorPosition = setCursorPosition;
function draw(sim, canvas) {
    const ctx = canvas.getContext('2d');
    if (!ctx)
        return;
    const w = canvas.width, h = canvas.height;
    ctx.clearRect(0, 0, w, h);
    ctx.save();
    ctx.strokeStyle = '#ffffff07';
    ctx.lineWidth = 1;
    const grid = 40;
    for (let x = grid; x < w; x += grid) {
        ctx.beginPath();
        ctx.moveTo(x, 0);
        ctx.lineTo(x, h);
        ctx.stroke();
    }
    for (let y = grid; y < h; y += grid) {
        ctx.beginPath();
        ctx.moveTo(0, y);
        ctx.lineTo(w, y);
        ctx.stroke();
    }
    ctx.restore();
    ctx.save();
    ctx.strokeStyle = '#5ee8ff33';
    ctx.lineWidth = 2;
    ctx.shadowColor = '#5ee8ff55';
    ctx.shadowBlur = 12;
    ctx.beginPath();
    ctx.moveTo(0, h - 1);
    ctx.lineTo(w, h - 1);
    ctx.stroke();
    ctx.restore();
    ctx.save();
    ctx.strokeStyle = '#ffffff12';
    ctx.lineWidth = 1;
    ctx.beginPath();
    ctx.moveTo(0, 0);
    ctx.lineTo(0, h);
    ctx.moveTo(w, 0);
    ctx.lineTo(w, h);
    ctx.stroke();
    ctx.restore();
    for (const shape of sim.solidCircles) {
        ctx.save();
        const grad = ctx.createRadialGradient(shape.x - shape.r * .3, shape.y - shape.r * .35, 2, shape.x, shape.y, shape.r);
        grad.addColorStop(0, '#3d4c62');
        grad.addColorStop(1, '#131b28');
        ctx.fillStyle = grad;
        ctx.strokeStyle = '#5ee8ff40';
        ctx.lineWidth = 2;
        ctx.shadowColor = '#5ee8ff28';
        ctx.shadowBlur = 18;
        ctx.beginPath();
        ctx.arc(shape.x, shape.y, shape.r, 0, Math.PI * 2);
        ctx.fill();
        ctx.stroke();
        ctx.shadowBlur = 0;
        ctx.strokeStyle = '#ffffff14';
        ctx.lineWidth = 1;
        ctx.beginPath();
        ctx.arc(shape.x, shape.y, shape.r * .52, 0, Math.PI * 2);
        ctx.stroke();
        ctx.restore();
    }
    drawConstraints(ctx, sim);
    drawBodies(ctx, sim);
    drawPoints(ctx, sim);
    drawCursor(ctx, sim, canvas);
}
function drawConstraints(ctx, sim) {
    for (const c of sim.constraints) {
        if (c.dead)
            continue;
        const a = sim.points[c.a], b = sim.points[c.b];
        if (!a || !b)
            continue;
        const ratio = c.stress || 1;
        const color = ratio > 1.24 ? '#ff6f7f' : ratio > 1.08 ? '#ffd06a' : '#5ee8ff';
        ctx.strokeStyle = color + (c.kind === 'pin' ? '80' : '4d');
        ctx.lineWidth = c.kind === 'pin' ? 2 : 1.1;
        ctx.beginPath();
        ctx.moveTo(a.x, a.y);
        ctx.lineTo(b.x, b.y);
        ctx.stroke();
    }
}
function bodyHue(hue) { return `hsl(${hue} 90% 62%)`; }
function drawBodies(ctx, sim) {
    for (const body of sim.bodies) {
        const pts = body.points.map(i => sim.points[i]).filter((p) => !!p);
        if (!pts.length)
            continue;
        ctx.save();
        if (body.shape === 'blob' || body.shape === 'cloth') {
            ctx.fillStyle = bodyHue(body.hue) + '1d';
            ctx.strokeStyle = bodyHue(body.hue) + '88';
            ctx.lineWidth = 1.3;
            ctx.beginPath();
            for (let i = 0; i < pts.length; i++) {
                if (i === 0)
                    ctx.moveTo(pts[i].x, pts[i].y);
                else
                    ctx.lineTo(pts[i].x, pts[i].y);
            }
            if (body.shape === 'blob')
                ctx.closePath();
            ctx.fill();
            ctx.stroke();
        }
        else if (body.shape === 'rope') {
            ctx.strokeStyle = bodyHue(body.hue) + 'aa';
            ctx.lineWidth = 1.8;
            ctx.beginPath();
            pts.forEach((p, i) => i === 0 ? ctx.moveTo(p.x, p.y) : ctx.lineTo(p.x, p.y));
            ctx.stroke();
        }
        ctx.restore();
    }
}
function drawPoints(ctx, sim) {
    for (const p of sim.points) {
        if (p.group === 'cloth' || p.group === 'blob')
            continue;
        const selected = sim.hoverIndex === sim.points.indexOf(p) || sim.grabIndex === sim.points.indexOf(p);
        ctx.fillStyle = selected ? '#ffffff' : '#9ee8ff';
        ctx.shadowColor = '#5ee8ff77';
        ctx.shadowBlur = selected ? 12 : 5;
        ctx.beginPath();
        ctx.arc(p.x, p.y, selected ? 3.4 : 2.1, 0, Math.PI * 2);
        ctx.fill();
        ctx.shadowBlur = 0;
    }
}
function drawCursor(ctx, sim, canvas) {
    const rect = canvas.getBoundingClientRect();
    const x = rect.left + (window.__cursorX ?? rect.width / 2);
    const y = rect.top + (window.__cursorY ?? rect.height / 2);
    const active = sim.grabIndex !== undefined || sim.hoverIndex !== undefined;
    ctx.save();
    ctx.strokeStyle = active ? '#ffffff' : '#5ee8ff';
    ctx.lineWidth = 1.2;
    ctx.globalAlpha = .65;
    ctx.beginPath();
    ctx.arc(x, y, active ? 12 : 8, 0, Math.PI * 2);
    ctx.stroke();
    ctx.beginPath();
    ctx.moveTo(x - 4, y);
    ctx.lineTo(x + 4, y);
    ctx.moveTo(x, y - 4);
    ctx.lineTo(x, y + 4);
    ctx.stroke();
    ctx.restore();
}
function setCursorPosition(x, y) { window.__cursorX = x; window.__cursorY = y; }
};

// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"bartowski/nex-agi_Nex-N2.5-mini-GGUF","provider":"brigid","persona":"typescript","sessionId":"d09e1ae8-f4c4-4095-a4ee-53c39872ba66","tokensIn":8200959,"tokensOut":51184,"tokensTotal":8252143,"cost":0,"turns":135,"toolCalls":163,"failedToolCalls":18,"timestamp":"2026-09-14T20:54:33.614Z"} -->