Physics Playground
Qwen 3.5 122B A10B MTP 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 48,901 bytes · SHA-256 ba508ad3546f
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Soft-Body Physics Playground</title>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #1a1a2e; font-family: 'Segoe UI', sans-serif; }
#canvas { display: block; width: 100%; height: 100%; }
#toolbar { position: absolute; top: 10px; left: 50%; transform: translateX(-50%); display: flex; gap: 8px; background: rgba(0,0,0,0.6); padding: 8px 12px; border-radius: 8px; backdrop-filter: blur(4px); }
#toolbar button { background: #4a4a6a; color: #fff; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; font-size: 14px; transition: background 0.2s; }
#toolbar button:hover { background: #5a5a8a; }
#toolbar button.active { background: #4a9eff; }
#controls { position: absolute; top: 10px; left: 10px; display: flex; flex-direction: column; gap: 8px; background: rgba(0,0,0,0.6); padding: 12px; border-radius: 8px; backdrop-filter: blur(4px); color: #fff; font-size: 13px; }
#controls label { display: flex; flex-direction: column; gap: 4px; }
#controls input[type="range"] { width: 120px; }
#controls select { padding: 4px; background: #2a2a4a; color: #fff; border: 1px solid #4a4a6a; border-radius: 4px; }
#hud { position: absolute; top: 10px; right: 10px; background: rgba(0,0,0,0.6); padding: 8px 12px; border-radius: 8px; color: #88ff88; font-family: monospace; font-size: 13px; backdrop-filter: blur(4px); }
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<div id="toolbar">
<button id="btn-rope">Rope</button>
<button id="btn-cloth">Cloth</button>
<button id="btn-softbody">Soft Body</button>
<button id="btn-ball">Ball</button>
<button id="btn-pause">Pause</button>
<button id="btn-reset">Reset</button>
</div>
<div id="controls">
<label>Gravity: <input type="range" id="gravity" min="-1" max="2" step="0.1" value="0.5"></label>
<label>Iterations: <input type="range" id="iterations" min="1" max="15" step="1" value="5"></label>
<label>Mode: <select id="mode"><option value="grab">Grab</option><option value="tear">Tear</option><option value="pin">Pin</option></select></label>
</div>
<div id="hud"><span id="fps">FPS: 0</span> | <span id="points">Points: 0</span></div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./physics.js":"src/physics.ts","./renderer.js":"src/renderer.ts","./input.js":"src/input.ts","./ui.js":"src/ui.ts","./types.js":"src/types.ts"},"src/physics.ts":{"./types.js":"src/types.ts"},"src/renderer.ts":{"./physics.js":"src/physics.ts","./types.js":"src/types.ts"},"src/input.ts":{"./physics.js":"src/physics.ts","./types.js":"src/types.ts"},"src/ui.ts":{"./input.js":"src/input.ts","./physics.js":"src/physics.ts","./types.js":"src/types.ts"}};
function __require(id) {
if (__cache[id]) return __cache[id].exports;
var module = __cache[id] = { exports: {} };
var factory = __mods[id];
if (!factory) throw new Error("Module not found: " + id);
factory(module.exports, function (spec) {
var target = (__map[id] && __map[id][spec]) || spec;
return __require(target);
}, module);
return module.exports;
}
// ── module: src/main.ts ──
__mods["src/main.ts"] = function (exports, require, module) {
"use strict";
// ============== MAIN ENTRY POINT ==============
Object.defineProperty(exports, "__esModule", { value: true });
exports.Simulation = void 0;
exports.start = start;
const physics_js_1 = require("./physics.js");
const renderer_js_1 = require("./renderer.js");
const input_js_1 = require("./input.js");
const ui_js_1 = require("./ui.js");
const types_js_1 = require("./types.js");
// Main simulation class that ties everything together
class Simulation {
constructor() {
this.lastTime = 0;
this.accumulator = 0;
this.fpsTime = 0;
this.frameCount = 0;
this.currentFPS = 0;
this.canvas = document.getElementById('canvas');
this.sim = new physics_js_1.PhysicsEngine();
this.renderer = new renderer_js_1.Renderer(this.canvas);
this.input = new input_js_1.InputHandler(this.sim);
this.ui = new ui_js_1.UIController(this.sim, this.input);
// Expose for testing/probing
window.sim = this.sim;
window.simulation = this;
}
start() {
// Initialize canvas size and static bodies
this.sim.setCanvasSize(this.canvas.width, this.canvas.height);
// Attach input handlers
this.input.attach(this.canvas);
// Start game loop
requestAnimationFrame((t) => this.loop(t));
// Add some initial objects for demonstration
this.addDemoObjects();
}
addDemoObjects() {
const w = this.canvas.width;
const h = this.canvas.height;
// Initial rope hanging from top-left
this.sim.createObject('rope', 100, 80, {
segments: 12,
segmentLength: 20
});
// Initial cloth in the middle
this.sim.createObject('cloth', w * 0.4, 50, {
width: 150,
height: 120,
spacing: 20
});
}
loop(timestamp) {
if (!this.lastTime)
this.lastTime = timestamp;
const deltaTime = Math.min((timestamp - this.lastTime) / 1000, 0.25); // Cap at 0.25s
this.lastTime = timestamp;
// Fixed timestep physics
const fixedDt = types_js_1.CONSTANTS.DT;
this.accumulator += deltaTime;
const maxSubsteps = 10;
let substeps = 0;
while (this.accumulator >= fixedDt && substeps < maxSubsteps) {
this.sim.step(fixedDt);
this.accumulator -= fixedDt;
substeps++;
}
// Render
this.renderer.render(this.sim, this.input.getDragState(), this.input.getHoveredPoint(), this.input.getTealState());
// Update UI stats
this.updateStats();
// Continue loop
requestAnimationFrame((t) => this.loop(t));
}
updateStats() {
this.frameCount++;
const now = performance.now();
if (now - this.fpsTime >= 500) {
this.currentFPS = Math.round(this.frameCount * 1000 / (now - this.fpsTime));
this.frameCount = 0;
this.fpsTime = now;
this.ui.updateStats(this.currentFPS);
}
}
// Public methods for testing
pause() {
this.input.pause();
}
play() {
this.input.play();
}
togglePause() {
this.input.togglePause();
}
step() {
this.sim.step();
}
spawn(type, x, y, params) {
this.sim.createObject(type, x, y, params);
}
reset() {
this.sim.clearAll();
this.sim.setCanvasSize(this.canvas.width, this.canvas.height);
this.addDemoObjects();
}
getFPS() {
return this.currentFPS;
}
getPointCount() {
return this.sim.points.length;
}
}
exports.Simulation = Simulation;
// Start the simulation when the DOM is ready
function start() {
const sim = new Simulation();
sim.start();
}
// Start if not already started
if (typeof window !== 'undefined') {
// Will be called by the entry point
}
};
// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
// ============== TYPES & CONSTANTS ==============
Object.defineProperty(exports, "__esModule", { value: true });
exports.CONSTANTS = void 0;
exports.vecSub = vecSub;
exports.vecAdd = vecAdd;
exports.vecMult = vecMult;
exports.vecLen = vecLen;
exports.vecNorm = vecNorm;
exports.vecDot = vecDot;
exports.clamp = clamp;
exports.distance = distance;
exports.distanceSquared = distanceSquared;
// Tunable constants
exports.CONSTANTS = {
DT: 1 / 60,
DAMPING: 0.99,
DEFAULT_ITERATIONS: 5,
GRAVITY: 0.5,
AIR_FRICTION: 0.995,
FRICTION: 0.95,
BOUNCE: 0.7,
TEAR_DISTANCE: 30,
TEAR_FORCE_THRESHOLD: 2.5,
GRAB_RADIUS: 20,
SUBSTEPS: 1,
// Colors
COLORS: {
ROPE: '#4a9eff',
CLOTH: '#ff6b4a',
SOFTBODY: '#4aff9f',
BALL: '#ffea4a',
CONSTRAINT: '#666688',
CONSTRAINT_STRESSED: '#ff4444',
CONSTRAINT_NORMAL: '#44ff44',
PINNED: '#ffffff',
STATIC: '#333355',
GRAB_HIGHLIGHT: '#ffff00',
},
// Object defaults
ROPE: { segmentLength: 20, segments: 20 },
CLOTH: { spacing: 25, width: 10, height: 8 },
SOFTBODY: { radius: 60, segments: 8 },
};
// Helper math functions
function vecSub(a, b) {
return { x: a.x - b.x, y: a.y - b.y };
}
function vecAdd(a, b) {
return { x: a.x + b.x, y: a.y + b.y };
}
function vecMult(v, s) {
return { x: v.x * s, y: v.y * s };
}
function vecLen(v) {
return Math.sqrt(v.x * v.x + v.y * v.y);
}
function vecNorm(v) {
const len = vecLen(v);
if (len < 1e-8)
return { x: 0, y: 0 };
return { x: v.x / len, y: v.y / len };
}
function vecDot(a, b) {
return a.x * b.x + a.y * b.y;
}
function clamp(v, min, max) {
return Math.max(min, Math.min(max, v));
}
function distance(p1, p2) {
const dx = p2.x - p1.x;
const dy = p2.y - p1.y;
return Math.sqrt(dx * dx + dy * dy);
}
function distanceSquared(p1, p2) {
const dx = p2.x - p1.x;
const dy = p2.y - p1.y;
return dx * dx + dy * dy;
}
};
// ── module: src/physics.ts ──
__mods["src/physics.ts"] = function (exports, require, module) {
"use strict";
// ============== PHYSICS ENGINE ==============
Object.defineProperty(exports, "__esModule", { value: true });
exports.PhysicsEngine = void 0;
const types_js_1 = require("./types.js");
class PhysicsEngine {
constructor() {
this.points = [];
this.constraints = [];
this.objects = [];
this.staticBodies = [];
this.nextPointId = 0;
this.nextConstraintId = 0;
this.nextObjectId = 0;
this.gravity = types_js_1.CONSTANTS.GRAVITY;
this.iterations = types_js_1.CONSTANTS.DEFAULT_ITERATIONS;
this.paused = false;
this.initStaticBodies();
}
initStaticBodies() {
// Will be initialized after we know canvas size
}
setCanvasSize(width, height) {
this.staticBodies = [
// Floor
{ type: 'floor', x: 0, y: height - 20, color: types_js_1.CONSTANTS.COLORS.STATIC },
// Left wall
{ type: 'wall', x: 10, y: 0, width: 20, height: height, color: types_js_1.CONSTANTS.COLORS.STATIC },
// Right wall
{ type: 'wall', x: width - 30, y: 0, width: 20, height: height, color: types_js_1.CONSTANTS.COLORS.STATIC },
// Optional ceiling (commented out for open top)
// { type: 'wall', x: 0, y: 0, width: width, height: 20, color: COLORS.STATIC },
];
// Add some interactive static circles
this.staticBodies.push({ type: 'circle', x: width * 0.3, y: height * 0.6, radius: 50, color: types_js_1.CONSTANTS.COLORS.STATIC }, { type: 'circle', x: width * 0.7, y: height * 0.5, radius: 40, color: types_js_1.CONSTANTS.COLORS.STATIC });
// Add a static box in the middle
this.staticBodies.push({ type: 'box', x: width * 0.5 - 40, y: height * 0.55, width: 80, height: 40, color: types_js_1.CONSTANTS.COLORS.STATIC });
}
createPoint(x, y, pinned = false, radius = 3, mass = 1) {
const point = {
x, y,
px: x, py: y,
pinned,
radius,
mass,
id: this.nextPointId++
};
this.points.push(point);
return point;
}
createConstraint(p1, p2, stiffness = 1.0, visible = true) {
const dist = (0, types_js_1.distance)(p1, p2);
if (dist < 1e-6) {
// Prevent zero-length constraints
return { p1, p2, length: 1, stiffness, id: this.nextConstraintId++, visible };
}
const constraint = {
p1, p2,
length: dist,
stiffness,
id: this.nextConstraintId++,
visible
};
this.constraints.push(constraint);
return constraint;
}
createObject(type, x, y, params) {
const obj = {
id: this.nextObjectId++,
type,
points: [],
constraints: [],
color: this.getObjectColor(type),
bounds: undefined
};
switch (type) {
case 'rope':
this.createRope(obj, x, y, params);
break;
case 'cloth':
this.createCloth(obj, x, y, params);
break;
case 'softbody':
this.createSoftBody(obj, x, y, params);
break;
case 'ball':
this.createBall(obj, x, y, params);
break;
}
this.objects.push(obj);
return obj;
}
getObjectColor(type) {
switch (type) {
case 'rope': return types_js_1.CONSTANTS.COLORS.ROPE;
case 'cloth': return types_js_1.CONSTANTS.COLORS.CLOTH;
case 'softbody': return types_js_1.CONSTANTS.COLORS.SOFTBODY;
case 'ball': return types_js_1.CONSTANTS.COLORS.BALL;
default: return '#ffffff';
}
}
createRope(obj, startX, startY, params) {
const segments = params?.segments ?? types_js_1.CONSTANTS.ROPE.segments;
const segmentLength = params?.segmentLength ?? types_js_1.CONSTANTS.ROPE.segmentLength;
const startXFixed = Math.max(50, Math.min(startX, 100));
// Create points hanging down
for (let i = 0; i <= segments; i++) {
const x = startXFixed;
const y = startY + i * segmentLength;
const pinned = i === 0;
const point = this.createPoint(x, y, pinned);
obj.points.push(point);
}
// Create constraints between consecutive points
for (let i = 0; i < obj.points.length - 1; i++) {
this.createConstraint(obj.points[i], obj.points[i + 1], 1.0, true);
obj.constraints.push(this.constraints[this.constraints.length - 1]);
}
}
createCloth(obj, startX, startY, params) {
const width = params?.width ?? types_js_1.CONSTANTS.CLOTH.width;
const height = params?.height ?? types_js_1.CONSTANTS.CLOTH.height;
const spacing = params?.spacing ?? types_js_1.CONSTANTS.CLOTH.spacing;
const cols = Math.floor(width / spacing) + 1;
const rows = Math.floor(height / spacing) + 1;
const actualSpacing = width / (cols - 1);
// Create grid of points
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
const x = startX + col * actualSpacing;
const y = startY + row * actualSpacing;
const pinned = row === 0; // Pin top row
const point = this.createPoint(x, y, pinned);
obj.points.push(point);
}
}
// Structural constraints (horizontal and vertical)
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
const idx = row * cols + col;
// Right neighbor
if (col < cols - 1) {
const idx2 = idx + 1;
this.createConstraint(obj.points[idx], obj.points[idx2], 1.0, false);
obj.constraints.push(this.constraints[this.constraints.length - 1]);
}
// Bottom neighbor
if (row < rows - 1) {
const idx2 = idx + cols;
this.createConstraint(obj.points[idx], obj.points[idx2], 1.0, false);
obj.constraints.push(this.constraints[this.constraints.length - 1]);
}
// Shear constraints (diagonal)
if (col < cols - 1 && row < rows - 1) {
const idx2 = idx + cols + 1;
this.createConstraint(obj.points[idx], obj.points[idx2], 0.5, false);
obj.constraints.push(this.constraints[this.constraints.length - 1]);
}
if (col > 0 && row < rows - 1) {
const idx2 = idx + cols - 1;
this.createConstraint(obj.points[idx], obj.points[idx2], 0.5, false);
obj.constraints.push(this.constraints[this.constraints.length - 1]);
}
}
}
}
createSoftBody(obj, centerX, centerY, params) {
const radius = params?.radius ?? types_js_1.CONSTANTS.SOFTBODY.radius;
const segments = params?.segments ?? types_js_1.CONSTANTS.SOFTBODY.segments;
// Create outer ring of points
const outerPoints = [];
for (let i = 0; i < segments; i++) {
const angle = (i / segments) * Math.PI * 2 - Math.PI / 2;
const x = centerX + Math.cos(angle) * radius;
const y = centerY + Math.sin(angle) * radius;
const point = this.createPoint(x, y, false, 4, 1.5);
obj.points.push(point);
outerPoints.push(point);
}
// Add center point
const centerPoint = this.createPoint(centerX, centerY, false, 4, 2);
obj.points.push(centerPoint);
// Connect outer ring
for (let i = 0; i < segments; i++) {
const next = (i + 1) % segments;
this.createConstraint(outerPoints[i], outerPoints[next], 1.0, true);
obj.constraints.push(this.constraints[this.constraints.length - 1]);
}
// Connect each outer point to center
for (let point of outerPoints) {
this.createConstraint(point, centerPoint, 0.8, false);
obj.constraints.push(this.constraints[this.constraints.length - 1]);
}
// Add cross-bracing for stability
for (let i = 0; i < segments; i++) {
const opposite = (i + Math.floor(segments / 2)) % segments;
this.createConstraint(outerPoints[i], outerPoints[opposite], 0.3, false);
obj.constraints.push(this.constraints[this.constraints.length - 1]);
}
}
createBall(obj, x, y, params) {
const radius = params?.radius ?? 15;
const point = this.createPoint(x, y, false, radius, 2);
obj.points.push(point);
}
step(dt = types_js_1.CONSTANTS.DT) {
if (this.paused)
return;
// Sub-stepping for stability
const subSteps = types_js_1.CONSTANTS.SUBSTEPS;
const subDt = dt / subSteps;
for (let s = 0; s < subSteps; s++) {
this.integrate(subDt);
this.solveConstraints();
this.applyCollisions();
}
}
integrate(dt) {
const dt2 = dt * dt;
for (const point of this.points) {
if (point.pinned)
continue;
const vx = (point.x - point.px) * types_js_1.CONSTANTS.DAMPING;
const vy = (point.y - point.py) * types_js_1.CONSTANTS.DAMPING;
point.px = point.x;
point.py = point.y;
point.x += vx + 0 * dt2; // No horizontal gravity
point.y += vy + this.gravity * dt2;
}
}
solveConstraints() {
for (let iter = 0; iter < this.iterations; iter++) {
// Solve distance constraints
for (const c of this.constraints) {
const dx = c.p2.x - c.p1.x;
const dy = c.p2.y - c.p1.y;
const distSq = dx * dx + dy * dy;
if (distSq < 1e-12)
continue; // Skip zero-length
const dist = Math.sqrt(distSq);
const diff = (dist - c.length) / dist * c.stiffness;
const offsetX = dx * diff * 0.5;
const offsetY = dy * diff * 0.5;
if (!c.p1.pinned) {
c.p1.x += offsetX;
c.p1.y += offsetY;
}
if (!c.p2.pinned) {
c.p2.x -= offsetX;
c.p2.y -= offsetY;
}
}
// Pin pinned points back (important for stability)
for (const point of this.points) {
if (point.pinned) {
// These stay exactly where they were set
}
}
}
}
applyCollisions() {
const canvasWidth = window.innerWidth;
const canvasHeight = window.innerHeight;
for (const point of this.points) {
if (point.pinned)
continue;
let vx = point.x - point.px;
let vy = point.y - point.py;
// Static body collisions
for (const body of this.staticBodies) {
if (body.type === 'floor') {
const floorY = body.y;
if (point.y + point.radius > floorY) {
const overlap = point.y + point.radius - floorY;
point.y = floorY - point.radius;
point.px = point.x - vx * types_js_1.CONSTANTS.BOUNCE;
vx = vx * types_js_1.CONSTANTS.FRICTION;
}
}
else if (body.type === 'wall' && body.x !== undefined && body.height !== undefined) {
const left = body.x;
const right = body.x + (body.width ?? 0);
const bottom = body.y + body.height;
if (point.x + point.radius > left && point.x - point.radius < right) {
if (point.y + point.radius > body.y) {
if (point.x < left + point.radius) {
point.x = left + point.radius;
vx = -vx * types_js_1.CONSTANTS.BOUNCE;
}
if (point.x > right - point.radius) {
point.x = right - point.radius;
vx = -vx * types_js_1.CONSTANTS.BOUNCE;
}
}
}
}
else if (body.type === 'circle' && body.radius !== undefined) {
const dx = point.x - body.x;
const dy = point.y - body.y;
const dist = Math.sqrt(dx * dx + dy * dy);
const minDist = body.radius + point.radius;
if (dist < minDist && dist > 0) {
const nx = dx / dist;
const ny = dy / dist;
point.x = body.x + nx * minDist;
point.y = body.y + ny * minDist;
// Reflect velocity
const dot = vx * nx + vy * ny;
vx = (vx - 2 * dot * nx) * types_js_1.CONSTANTS.BOUNCE;
vy = (vy - 2 * dot * ny) * types_js_1.CONSTANTS.BOUNCE;
point.px = point.x - vx * 0.9;
point.py = point.y - vy * 0.9;
}
}
else if (body.type === 'box' && body.width != null && body.height != null) {
const left = body.x;
const right = body.x + body.width;
const top = body.y;
const bottom = body.y + body.height;
// Find closest point on box
let closestX = (0, types_js_1.clamp)(point.x, left, right);
let closestY = (0, types_js_1.clamp)(point.y, top, bottom);
const dx = point.x - closestX;
const dy = point.y - closestY;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < point.radius && dist > 0) {
const nx = dx / dist;
const ny = dy / dist;
point.x = closestX + nx * point.radius;
point.y = closestY + ny * point.radius;
const dot = vx * nx + vy * ny;
vx = (vx - 2 * dot * nx) * types_js_1.CONSTANTS.BOUNCE;
vy = (vy - 2 * dot * ny) * types_js_1.CONSTANTS.BOUNCE;
point.px = point.x - vx * 0.9;
point.py = point.y - vy * 0.9;
}
}
}
// Self-collision for soft bodies (simplified)
// Only check points from the same object
point.px = point.x - vx;
point.py = point.y - vy;
}
}
tearConstraintNear(x, y, maxDistance = types_js_1.CONSTANTS.TEAR_DISTANCE) {
const torn = [];
const maxDistSq = maxDistance * maxDistance;
for (let i = this.constraints.length - 1; i >= 0; i--) {
const c = this.constraints[i];
// Check if constraint is near the click point
const midX = (c.p1.x + c.p2.x) / 2;
const midY = (c.p1.y + c.p2.y) / 2;
const distSq = (midX - x) ** 2 + (midY - y) ** 2;
if (distSq <= maxDistSq) {
// Check if this constraint belongs to any of our objects
const objIndex = this.objects.findIndex(obj => obj.constraints.includes(c));
if (objIndex >= 0) {
const obj = this.objects[objIndex];
obj.constraints.splice(obj.constraints.indexOf(c), 1);
torn.push(c);
}
this.constraints.splice(i, 1);
}
}
return torn;
}
findNearestPoint(x, y, radius = types_js_1.CONSTANTS.GRAB_RADIUS) {
let nearest = null;
let nearestDistSq = radius * radius;
for (const point of this.points) {
const distSq = (point.x - x) ** 2 + (point.y - y) ** 2;
if (distSq < nearestDistSq) {
nearestDistSq = distSq;
nearest = point;
}
}
return nearest;
}
togglePin(point) {
point.pinned = !point.pinned;
}
removeObject(obj) {
// Remove all points
for (const point of obj.points) {
const idx = this.points.indexOf(point);
if (idx >= 0)
this.points.splice(idx, 1);
}
// Remove all constraints
for (const constraint of obj.constraints) {
const idx = this.constraints.indexOf(constraint);
if (idx >= 0)
this.constraints.splice(idx, 1);
}
// Remove object
const idx = this.objects.indexOf(obj);
if (idx >= 0)
this.objects.splice(idx, 1);
}
clearAll() {
this.points = [];
this.constraints = [];
this.objects = [];
this.nextPointId = 0;
this.nextConstraintId = 0;
this.nextObjectId = 0;
}
getKineticEnergy() {
let energy = 0;
for (const point of this.points) {
const vx = point.x - point.px;
const vy = point.y - point.py;
energy += 0.5 * point.mass * (vx * vx + vy * vy);
}
return energy;
}
getMinY() {
let minY = Infinity;
for (const point of this.points) {
if (point.y < minY)
minY = point.y;
}
return minY;
}
}
exports.PhysicsEngine = PhysicsEngine;
};
// ── module: src/renderer.ts ──
__mods["src/renderer.ts"] = function (exports, require, module) {
"use strict";
// ============== RENDERER ==============
Object.defineProperty(exports, "__esModule", { value: true });
exports.Renderer = void 0;
const types_js_1 = require("./types.js");
class Renderer {
constructor(canvas) {
this.showStress = true;
this.showPoints = false;
this.trailLength = 10;
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.resize();
window.addEventListener('resize', () => this.resize());
}
resize() {
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
}
render(sim, draggedPoint, hoveredPoint, tearStart) {
const ctx = this.ctx;
const width = this.canvas.width;
const height = this.canvas.height;
// Clear background
ctx.fillStyle = '#1a1a2e';
ctx.fillRect(0, 0, width, height);
// Draw static bodies
this.drawStaticBodies(sim);
// Draw objects
for (const obj of sim.objects) {
switch (obj.type) {
case 'rope':
case 'softbody':
this.drawObjectWithConstraints(sim, obj, draggedPoint);
break;
case 'cloth':
this.drawCloth(sim, obj, draggedPoint);
break;
case 'ball':
this.drawBall(sim, obj, draggedPoint);
break;
}
}
// Draw tear line if active
if (tearStart) {
ctx.strokeStyle = '#ff4444';
ctx.lineWidth = 2;
ctx.setLineDash([5, 5]);
ctx.beginPath();
ctx.moveTo(tearStart.x, tearStart.y);
ctx.lineTo(tearStart.x, tearStart.y);
ctx.stroke();
ctx.setLineDash([]);
// Draw tear radius indicator
ctx.strokeStyle = 'rgba(255, 68, 68, 0.3)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(tearStart.x, tearStart.y, types_js_1.CONSTANTS.TEAR_DISTANCE, 0, Math.PI * 2);
ctx.stroke();
}
// Draw drag line if dragging
if (draggedPoint) {
ctx.strokeStyle = types_js_1.CONSTANTS.COLORS.GRAB_HIGHLIGHT;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(draggedPoint.point.x, draggedPoint.point.y);
ctx.lineTo(draggedPoint.point.x + draggedPoint.offsetX, draggedPoint.point.y + draggedPoint.offsetY);
ctx.stroke();
}
// Draw hovered point highlight
if (hoveredPoint && !draggedPoint) {
ctx.fillStyle = types_js_1.CONSTANTS.COLORS.GRAB_HIGHLIGHT;
ctx.beginPath();
ctx.arc(hoveredPoint.x, hoveredPoint.y, types_js_1.CONSTANTS.GRAB_RADIUS, 0, Math.PI * 2);
ctx.fill();
}
}
drawStaticBodies(sim) {
const ctx = this.ctx;
for (const body of sim.staticBodies) {
ctx.fillStyle = body.color;
ctx.strokeStyle = '#444466';
ctx.lineWidth = 2;
if (body.type === 'floor') {
ctx.fillRect(0, body.y - 5, window.innerWidth, 20);
ctx.beginPath();
ctx.moveTo(0, body.y);
ctx.lineTo(window.innerWidth, body.y);
ctx.stroke();
}
else if (body.type === 'wall' && body.width && body.height) {
ctx.fillRect(body.x, body.y, body.width, body.height);
ctx.strokeRect(body.x, body.y, body.width, body.height);
}
else if (body.type === 'circle' && body.radius) {
ctx.beginPath();
ctx.arc(body.x, body.y, body.radius, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
}
else if (body.type === 'box' && body.width && body.height) {
ctx.fillRect(body.x, body.y, body.width, body.height);
ctx.strokeRect(body.x, body.y, body.width, body.height);
}
}
}
drawObjectWithConstraints(sim, obj, draggedPoint) {
const ctx = this.ctx;
// Draw constraints
for (const c of obj.constraints) {
if (c.visible === false)
continue;
const stretch = (0, types_js_1.distance)(c.p1, c.p2) / c.length;
let strokeColor = types_js_1.CONSTANTS.COLORS.CONSTRAINT;
if (this.showStress && stretch > 1.05) {
const stressRatio = Math.min((stretch - 1) / 0.5, 1);
strokeColor = this.mixColors(types_js_1.CONSTANTS.COLORS.CONSTRAINT_NORMAL, types_js_1.CONSTANTS.COLORS.CONSTRAINT_STRESSED, stressRatio);
}
ctx.strokeStyle = strokeColor;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(c.p1.x, c.p1.y);
ctx.lineTo(c.p2.x, c.p2.y);
ctx.stroke();
}
// Draw points
if (this.showPoints || obj.type === 'rope') {
for (const point of obj.points) {
ctx.fillStyle = point.pinned ? types_js_1.CONSTANTS.COLORS.PINNED : obj.color;
ctx.beginPath();
ctx.arc(point.x, point.y, point.radius + 1, 0, Math.PI * 2);
ctx.fill();
}
}
// Fill soft body
if (obj.type === 'softbody') {
ctx.fillStyle = obj.color + '40'; // 25% alpha
ctx.beginPath();
ctx.moveTo(obj.points[0].x, obj.points[0].y);
for (let i = 1; i < obj.points.length - 1; i++) {
ctx.lineTo(obj.points[i].x, obj.points[i].y);
}
ctx.closePath();
ctx.fill();
}
}
drawCloth(sim, obj, draggedPoint) {
const ctx = this.ctx;
// Draw filled cloth mesh
const points = obj.points;
const cols = Math.round(Math.sqrt(points.length * 2));
// Create a filled polygon from the cloth
if (points.length > 2) {
ctx.fillStyle = obj.color + '60'; // 40% alpha
ctx.beginPath();
// Top edge left to right
for (let c = 0; c < cols; c++) {
if (c < points.length) {
ctx.lineTo(points[c].x, points[c].y);
}
}
// Right edge top to bottom
for (let r = 1; r < Math.ceil(points.length / cols); r++) {
const idx = r * cols + (cols - 1);
if (idx < points.length) {
ctx.lineTo(points[idx].x, points[idx].y);
}
}
// Bottom edge right to left
const rows = Math.ceil(points.length / cols);
for (let c = cols - 2; c >= 0; c--) {
const idx = (rows - 1) * cols + c;
if (idx >= 0 && idx < points.length) {
ctx.lineTo(points[idx].x, points[idx].y);
}
}
// Left edge bottom to top
for (let r = rows - 2; r >= 1; r--) {
const idx = r * cols;
if (idx >= 0 && idx < points.length) {
ctx.lineTo(points[idx].x, points[idx].y);
}
}
ctx.closePath();
ctx.fill();
}
// Draw grid lines
ctx.strokeStyle = obj.color + '80';
ctx.lineWidth = 1;
ctx.beginPath();
// Horizontal lines
const rows = Math.ceil(points.length / cols);
for (let r = 0; r < rows; r++) {
const startIdx = r * cols;
if (startIdx < points.length) {
ctx.moveTo(points[startIdx].x, points[startIdx].y);
for (let c = 1; c < cols; c++) {
const idx = startIdx + c;
if (idx < points.length) {
ctx.lineTo(points[idx].x, points[idx].y);
}
}
}
}
// Vertical lines
for (let c = 0; c < cols; c++) {
ctx.moveTo(points[c].x, points[c].y);
for (let r = 1; r < rows; r++) {
const idx = r * cols + c;
if (idx < points.length) {
ctx.lineTo(points[idx].x, points[idx].y);
}
}
}
ctx.stroke();
// Draw pinned points
for (const point of obj.points) {
if (point.pinned) {
ctx.fillStyle = types_js_1.CONSTANTS.COLORS.PINNED;
ctx.beginPath();
ctx.arc(point.x, point.y, 5, 0, Math.PI * 2);
ctx.fill();
}
}
}
drawBall(sim, obj, draggedPoint) {
const ctx = this.ctx;
const point = obj.points[0];
// Ball body
const gradient = ctx.createRadialGradient(point.x - point.radius / 3, point.y - point.radius / 3, point.radius / 4, point.x, point.y, point.radius);
gradient.addColorStop(0, '#ffffff');
gradient.addColorStop(0.3, obj.color);
gradient.addColorStop(1, this.darkenColor(obj.color, 0.3));
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(point.x, point.y, point.radius, 0, Math.PI * 2);
ctx.fill();
// Highlight
ctx.strokeStyle = types_js_1.CONSTANTS.COLORS.GRAB_HIGHLIGHT;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(point.x, point.y, point.radius, 0, Math.PI * 2);
ctx.stroke();
}
mixColors(color1, color2, ratio) {
const r1 = parseInt(color1.slice(1, 3), 16);
const g1 = parseInt(color1.slice(3, 5), 16);
const b1 = parseInt(color1.slice(5, 7), 16);
const r2 = parseInt(color2.slice(1, 3), 16);
const g2 = parseInt(color2.slice(3, 5), 16);
const b2 = parseInt(color2.slice(5, 7), 16);
const r = Math.round(r1 + (r2 - r1) * ratio);
const g = Math.round(g1 + (g2 - g1) * ratio);
const b = Math.round(b1 + (b2 - b1) * ratio);
return `rgb(${r}, ${g}, ${b})`;
}
darkenColor(color, amount) {
const r = parseInt(color.slice(1, 3), 16);
const g = parseInt(color.slice(3, 5), 16);
const b = parseInt(color.slice(5, 7), 16);
const dr = Math.round(r * (1 - amount));
const dg = Math.round(g * (1 - amount));
const db = Math.round(b * (1 - amount));
return `rgb(${dr}, ${dg}, ${db})`;
}
}
exports.Renderer = Renderer;
};
// ── module: src/input.ts ──
__mods["src/input.ts"] = function (exports, require, module) {
"use strict";
// ============== INPUT HANDLER ==============
Object.defineProperty(exports, "__esModule", { value: true });
exports.InputHandler = void 0;
class InputHandler {
constructor(sim) {
this.mode = 'grab';
this.dragged = null;
this.tearState = null;
this.hoveringPoint = null;
// Spawn creation state
this.spawning = null;
this.sim = sim;
}
attach(canvas) {
// Mouse down
canvas.addEventListener('mousedown', (e) => this.onMouseDown(e));
// Mouse move
canvas.addEventListener('mousemove', (e) => this.onMouseMove(e));
// Mouse up
canvas.addEventListener('mouseup', (e) => this.onMouseUp(e));
// Context menu (prevent for right-click tearing)
canvas.addEventListener('contextmenu', (e) => e.preventDefault());
// Keyboard
document.addEventListener('keydown', (e) => this.onKeyDown(e));
document.addEventListener('keyup', (e) => this.onKeyUp(e));
}
onMouseDown(e) {
const target = e.currentTarget;
const rect = target.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// Right-click for tearing
if (e.button === 2) {
this.tearState = { startX: x, startY: y, currentX: x, currentY: y };
return;
}
// Find nearest point
const point = this.sim.findNearestPoint(x, y);
if (this.mode === 'grab') {
if (point) {
this.dragged = {
point,
startX: point.x,
startY: point.y,
offsetX: x - point.x,
offsetY: y - point.y
};
}
else {
// Start spawning a rope from empty click
this.spawning = { type: 'rope', startX: x, startY: y };
}
}
else if (this.mode === 'pin') {
if (point) {
this.sim.togglePin(point);
}
}
else if (this.mode === 'tear') {
this.tearState = { startX: x, startY: y, currentX: x, currentY: y };
}
}
onMouseMove(e) {
const target = e.currentTarget;
const rect = target.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// Update tear state
if (this.tearState) {
this.tearState.currentX = x;
this.tearState.currentY = y;
// Tear constraints in the drag path
this.sim.tearConstraintNear(x, y, 15);
}
// Update dragged point position
if (this.dragged) {
this.dragged.point.x = x;
this.dragged.point.y = y;
}
// Update hovering point
this.hoveringPoint = this.sim.findNearestPoint(x, y);
// Update spawning preview
if (this.spawning) {
// Just track the mouse for now, will create on mouseup
this.spawning = { ...this.spawning };
}
}
onMouseUp(e) {
const target = e.currentTarget;
const rect = target.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// Finish spawning
if (this.spawning) {
this.spawnObject(this.spawning.type, this.spawning.startX, this.spawning.startY);
this.spawning = null;
}
// Release dragged point with velocity
if (this.dragged) {
const point = this.dragged.point;
const vx = (point.x - this.dragged.startX);
const vy = (point.y - this.dragged.startY);
// Set previous position to give throw velocity
point.px = point.x - vx * 1.5;
point.py = point.y - vy * 1.5;
this.dragged = null;
}
// Finish tearing
if (this.tearState) {
// Final tear at end point
this.sim.tearConstraintNear(this.tearState.currentX, this.tearState.currentY, 20);
this.tearState = null;
}
}
spawnObject(type, x, y) {
const canvas = document.getElementById('canvas');
const height = canvas.height;
switch (type) {
case 'rope':
// Spawn rope hanging from top, starting near click
this.sim.createObject('rope', x, Math.max(50, y - 200), {
segments: 15,
segmentLength: 18
});
break;
case 'cloth':
// Spawn cloth pinned at top
this.sim.createObject('cloth', Math.max(50, x - 100), Math.max(30, y - 150), {
width: 200,
height: 150,
spacing: 20
});
break;
case 'softbody':
// Spawn soft body at click position
this.sim.createObject('softbody', x, Math.max(100, y - 100), {
radius: 50,
segments: 10
});
break;
case 'ball':
// Spawn ball at click
this.sim.createObject('ball', x, Math.max(50, y - 50), {
radius: 20
});
break;
}
}
onKeyDown(e) {
switch (e.code) {
case 'Space':
e.preventDefault();
this.sim.paused = !this.sim.paused;
this.updatePauseButton();
break;
case 'ArrowUp':
case 'ArrowRight':
if (this.sim.paused) {
this.sim.step();
}
break;
}
}
onKeyUp(e) {
// Reset keys
}
updatePauseButton() {
const btn = document.getElementById('btn-pause');
if (btn) {
btn.textContent = this.sim.paused ? 'Play' : 'Pause';
btn.classList.toggle('active', this.sim.paused);
}
}
setMode(mode) {
this.mode = mode;
}
pause() {
this.sim.paused = true;
this.updatePauseButton();
}
play() {
this.sim.paused = false;
this.updatePauseButton();
}
togglePause() {
if (this.sim.paused) {
this.play();
}
else {
this.pause();
}
}
step() {
this.sim.step();
}
getDragState() {
if (!this.dragged)
return null;
return {
point: this.dragged.point,
offsetX: 0,
offsetY: 0
};
}
getTealState() {
if (!this.tearState)
return null;
return { x: this.tearState.currentX, y: this.tearState.currentY };
}
getHoveredPoint() {
return this.hoveringPoint;
}
}
exports.InputHandler = InputHandler;
};
// ── module: src/ui.ts ──
__mods["src/ui.ts"] = function (exports, require, module) {
"use strict";
// ============== UI CONTROLLER ==============
Object.defineProperty(exports, "__esModule", { value: true });
exports.UIController = void 0;
class UIController {
constructor(sim, input) {
this.frameCount = 0;
this.lastTime = performance.now();
this.sim = sim;
this.input = input;
this.fpsEl = document.getElementById('fps');
this.pointsEl = document.getElementById('points');
this.gravitySlider = document.getElementById('gravity');
this.iterationsSlider = document.getElementById('iterations');
this.modeSelect = document.getElementById('mode');
this.btnPause = document.getElementById('btn-pause');
this.btnReset = document.getElementById('btn-reset');
this.toolbarButtons = new Map();
this.toolbarButtons.set('rope', document.getElementById('btn-rope'));
this.toolbarButtons.set('cloth', document.getElementById('btn-cloth'));
this.toolbarButtons.set('softbody', document.getElementById('btn-softbody'));
this.toolbarButtons.set('ball', document.getElementById('btn-ball'));
this.attachListeners();
}
attachListeners() {
// Gravity slider
this.gravitySlider.addEventListener('input', (e) => {
const value = parseFloat(e.target.value);
this.sim.gravity = value;
});
// Iterations slider
this.iterationsSlider.addEventListener('input', (e) => {
const value = parseInt(e.target.value);
this.sim.iterations = value;
});
// Mode select
this.modeSelect.addEventListener('change', (e) => {
const mode = e.target.value;
this.input.setMode(mode);
this.updateToolbarHighlight('spawn');
});
// Pause button
this.btnPause.addEventListener('click', () => {
this.input.togglePause();
});
// Reset button
this.btnReset.addEventListener('click', () => {
this.sim.clearAll();
this.sim.setCanvasSize(window.innerWidth, window.innerHeight);
});
// Toolbar buttons
this.toolbarButtons.forEach((btn, type) => {
btn.addEventListener('click', () => {
this.selectSpawnType(type);
});
});
}
selectSpawnType(type) {
this.updateToolbarHighlight(type);
this.input.setMode('grab');
this.modeSelect.value = 'grab';
}
updateToolbarHighlight(activeType) {
this.toolbarButtons.forEach((btn, type) => {
btn.classList.toggle('active', type === activeType);
});
}
updateStats(fps) {
this.frameCount++;
const now = performance.now();
const delta = now - this.lastTime;
if (delta >= 1000) {
this.fpsEl.textContent = `FPS: ${Math.round((this.frameCount * 1000) / delta)}`;
this.pointsEl.textContent = `Points: ${this.sim.points.length}`;
this.frameCount = 0;
this.lastTime = now;
}
}
getSpawnMode() {
// Check if any toolbar button is active
for (const [type, btn] of this.toolbarButtons) {
if (btn.classList.contains('active')) {
return type;
}
}
return null;
}
}
exports.UIController = UIController;
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>