Physics Playground
nex agi Nex N2.5 mini GGUF · typescript

Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 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"} -->