Physics Playground
Deepseek v4 Flash 0731 · 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 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 & 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"} -->