v0.90: World Layers, Stream Encryption V2, Multi-Channel - ds-physics: set/get layer, gravity scale, angular vel, body type, world gravity, freeze/unfreeze, body tag (183 tests) - ds-stream: XorCipherV2, ChannelRouter, AckTracker, FramePoolV2, BandwidthEstimatorV2, PriorityMux, NonceGenerator, StreamValidator, RetryQueue (246 tests) - ds-stream-wasm: 9 exports (156 tests) v0.95: Scene Graph, Stream Compression V2, Telemetry - ds-physics: body count all, step count, get gravity, is frozen, get color, AABB, raycast, restitution, emitter count (192 tests) - ds-stream: Lz4Lite, TelemetrySink, FrameDiffer, BackoffTimer, StreamMirror, QuotaManager, HeartbeatV2, TagFilter, MovingAverage (255 tests) - ds-stream-wasm: 9 exports (165 tests) v1.0.0: Production Ready — ECS Foundation, Stream Pipeline, Protocol Finalization - ds-physics: get tag, body list, impulse, mass, friction, world bounds, body exists, reset world, engine version (201 tests) - ds-stream: StreamPipeline, ProtocolHeader, FrameSplitterV2, CongestionWindowV2, StreamStatsV2, AckWindow, CodecRegistryV2, FlowControllerV2, VersionNegotiator (264 tests) - ds-stream-wasm: 9 exports (174 tests) Total: 639 tests across 3 packages
1048 lines
40 KiB
HTML
1048 lines
40 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>DreamStack Engine v0.50 — Interactive Showcase</title>
|
|
<meta name="description" content="Interactive demo of DreamStack Engine v0.28-v0.50 features: physics simulation, stream processing, and real-time visualization.">
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;900&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
<style>
|
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
|
:root {
|
|
--bg: #0a0a0f;
|
|
--surface: rgba(255,255,255,0.04);
|
|
--glass: rgba(255,255,255,0.06);
|
|
--glass-border: rgba(255,255,255,0.08);
|
|
--text: #e8e8f0;
|
|
--text-dim: rgba(255,255,255,0.4);
|
|
--accent: #6366f1;
|
|
--accent-glow: rgba(99,102,241,0.3);
|
|
--orange: #f97316;
|
|
--cyan: #22d3ee;
|
|
--pink: #ec4899;
|
|
--green: #10b981;
|
|
--red: #ef4444;
|
|
--yellow: #eab308;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Inter', -apple-system, sans-serif;
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
overflow: hidden;
|
|
height: 100vh;
|
|
width: 100vw;
|
|
}
|
|
|
|
/* ─── Layout ─── */
|
|
.app { display: grid; grid-template-columns: 280px 1fr 260px; grid-template-rows: 56px 1fr 40px; height: 100vh; gap: 0; }
|
|
.header { grid-column: 1 / -1; display: flex; align-items: center; justify-content: space-between; padding: 0 20px; border-bottom: 1px solid var(--glass-border); background: rgba(10,10,15,0.95); backdrop-filter: blur(20px); z-index: 10; }
|
|
.sidebar-left { grid-row: 2; border-right: 1px solid var(--glass-border); background: rgba(10,10,15,0.7); backdrop-filter: blur(12px); overflow-y: auto; padding: 12px; }
|
|
.canvas-area { grid-row: 2; position: relative; overflow: hidden; background: radial-gradient(ellipse at center, rgba(99,102,241,0.03) 0%, transparent 70%); }
|
|
.sidebar-right { grid-row: 2; border-left: 1px solid var(--glass-border); background: rgba(10,10,15,0.7); backdrop-filter: blur(12px); overflow-y: auto; padding: 12px; }
|
|
.footer { grid-column: 1 / -1; display: flex; align-items: center; padding: 0 20px; gap: 20px; border-top: 1px solid var(--glass-border); background: rgba(10,10,15,0.95); font-size: 11px; color: var(--text-dim); }
|
|
|
|
/* ─── Header ─── */
|
|
.logo { display: flex; align-items: center; gap: 10px; }
|
|
.logo-icon { width: 28px; height: 28px; background: linear-gradient(135deg, var(--accent), var(--pink)); border-radius: 6px; display: flex; align-items: center; justify-content: center; font-weight: 900; font-size: 14px; }
|
|
.logo-text { font-weight: 700; font-size: 15px; letter-spacing: -0.5px; }
|
|
.logo-text span { color: var(--text-dim); font-weight: 400; margin-left: 4px; }
|
|
.header-actions { display: flex; align-items: center; gap: 8px; }
|
|
|
|
/* ─── Buttons ─── */
|
|
.btn {
|
|
padding: 6px 14px; border-radius: 8px; border: 1px solid var(--glass-border); background: var(--glass);
|
|
color: var(--text); font-size: 12px; font-family: inherit; font-weight: 500; cursor: pointer;
|
|
transition: all 0.2s; display: flex; align-items: center; gap: 6px;
|
|
}
|
|
.btn:hover { background: rgba(255,255,255,0.1); border-color: rgba(255,255,255,0.15); transform: translateY(-1px); }
|
|
.btn.active { background: var(--accent); border-color: var(--accent); box-shadow: 0 0 20px var(--accent-glow); }
|
|
.btn-sm { padding: 4px 10px; font-size: 11px; border-radius: 6px; }
|
|
.btn-icon { width: 32px; height: 32px; padding: 0; display: flex; align-items: center; justify-content: center; border-radius: 8px; }
|
|
|
|
/* ─── Sidebar sections ─── */
|
|
.section { margin-bottom: 16px; }
|
|
.section-title { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 1.2px; color: var(--text-dim); margin-bottom: 8px; }
|
|
.tool-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 4px; }
|
|
.tool-btn {
|
|
padding: 8px; border-radius: 8px; border: 1px solid transparent; background: var(--surface);
|
|
color: var(--text); font-size: 11px; font-family: inherit; cursor: pointer; text-align: center;
|
|
transition: all 0.2s; display: flex; flex-direction: column; align-items: center; gap: 4px;
|
|
}
|
|
.tool-btn:hover { background: var(--glass); border-color: var(--glass-border); }
|
|
.tool-btn.active { background: rgba(99,102,241,0.15); border-color: var(--accent); color: var(--accent); }
|
|
.tool-btn .icon { font-size: 18px; }
|
|
|
|
/* ─── Sliders ─── */
|
|
.slider-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 8px; }
|
|
.slider-row label { font-size: 11px; color: var(--text-dim); min-width: 60px; }
|
|
.slider-row input[type="range"] { flex: 1; accent-color: var(--accent); height: 4px; }
|
|
.slider-row .val { font-family: 'JetBrains Mono', monospace; font-size: 10px; color: var(--accent); min-width: 30px; text-align: right; }
|
|
|
|
/* ─── Stats panel ─── */
|
|
.stat-card {
|
|
background: var(--surface); border-radius: 8px; padding: 10px; margin-bottom: 4px;
|
|
border: 1px solid var(--glass-border);
|
|
}
|
|
.stat-label { font-size: 10px; color: var(--text-dim); margin-bottom: 2px; }
|
|
.stat-value { font-family: 'JetBrains Mono', monospace; font-size: 18px; font-weight: 600; }
|
|
.stat-value.green { color: var(--green); }
|
|
.stat-value.cyan { color: var(--cyan); }
|
|
.stat-value.orange { color: var(--orange); }
|
|
.stat-value.pink { color: var(--pink); }
|
|
|
|
.stat-row { display: grid; grid-template-columns: 1fr 1fr; gap: 4px; }
|
|
|
|
/* ─── Stream viz ─── */
|
|
.stream-bar {
|
|
height: 6px; border-radius: 3px; background: var(--surface); overflow: hidden; margin-bottom: 4px;
|
|
}
|
|
.stream-bar-fill { height: 100%; border-radius: 3px; transition: width 0.3s; }
|
|
|
|
/* ─── Canvas ─── */
|
|
#physics-canvas { display: block; width: 100%; height: 100%; cursor: crosshair; }
|
|
|
|
/* ─── Mode banner ─── */
|
|
.mode-banner {
|
|
position: absolute; top: 12px; left: 50%; transform: translateX(-50%);
|
|
padding: 6px 16px; border-radius: 20px; font-size: 11px; font-weight: 600;
|
|
letter-spacing: 0.5px; text-transform: uppercase; pointer-events: none;
|
|
background: rgba(99,102,241,0.2); border: 1px solid rgba(99,102,241,0.3);
|
|
color: var(--accent); backdrop-filter: blur(8px); z-index: 5;
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
/* ─── Toast ─── */
|
|
.toast-container { position: absolute; bottom: 60px; left: 50%; transform: translateX(-50%); display: flex; flex-direction: column; align-items: center; gap: 6px; z-index: 20; }
|
|
.toast {
|
|
padding: 8px 18px; border-radius: 10px; font-size: 12px; font-weight: 500;
|
|
background: rgba(10,10,20,0.9); border: 1px solid var(--glass-border);
|
|
backdrop-filter: blur(12px); animation: toastIn 0.3s ease, toastOut 0.3s ease 2s forwards;
|
|
white-space: nowrap;
|
|
}
|
|
@keyframes toastIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
|
|
@keyframes toastOut { from { opacity: 1; } to { opacity: 0; transform: translateY(-10px); } }
|
|
|
|
/* ─── Scrollbar ─── */
|
|
::-webkit-scrollbar { width: 4px; }
|
|
::-webkit-scrollbar-track { background: transparent; }
|
|
::-webkit-scrollbar-thumb { background: var(--glass-border); border-radius: 2px; }
|
|
|
|
/* ─── Key hints ─── */
|
|
.key { display: inline-block; padding: 1px 5px; border-radius: 3px; background: var(--surface); border: 1px solid var(--glass-border); font-family: 'JetBrains Mono', monospace; font-size: 9px; margin: 0 2px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="app">
|
|
<!-- Header -->
|
|
<header class="header">
|
|
<div class="logo">
|
|
<div class="logo-icon">D</div>
|
|
<div class="logo-text">DreamStack<span>Engine v0.50</span></div>
|
|
</div>
|
|
<div class="header-actions">
|
|
<button class="btn btn-sm" id="btn-reset" onclick="resetWorld()">⟲ Reset</button>
|
|
<button class="btn btn-sm" id="btn-pause" onclick="togglePause()">⏸ Pause</button>
|
|
<button class="btn btn-sm" onclick="spawnBurst()">✦ Burst</button>
|
|
<button class="btn btn-sm active" id="btn-gravity-toggle" onclick="toggleGravity()">⇣ Gravity</button>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Left Sidebar: Tools -->
|
|
<aside class="sidebar-left">
|
|
<div class="section">
|
|
<div class="section-title">Tools</div>
|
|
<div class="tool-grid">
|
|
<button class="tool-btn active" data-tool="spawn" onclick="setTool('spawn')">
|
|
<span class="icon">●</span> Spawn
|
|
</button>
|
|
<button class="tool-btn" data-tool="explode" onclick="setTool('explode')">
|
|
<span class="icon">💥</span> Explode
|
|
</button>
|
|
<button class="tool-btn" data-tool="joint" onclick="setTool('joint')">
|
|
<span class="icon">🔗</span> Joint
|
|
</button>
|
|
<button class="tool-btn" data-tool="raycast" onclick="setTool('raycast')">
|
|
<span class="icon">⚡</span> Raycast
|
|
</button>
|
|
<button class="tool-btn" data-tool="drag" onclick="setTool('drag')">
|
|
<span class="icon">✋</span> Drag
|
|
</button>
|
|
<button class="tool-btn" data-tool="velocity" onclick="setTool('velocity')">
|
|
<span class="icon">→</span> Velocity
|
|
</button>
|
|
<button class="tool-btn" data-tool="teleport" onclick="setTool('teleport')">
|
|
<span class="icon">⊕</span> Teleport
|
|
</button>
|
|
<button class="tool-btn" data-tool="kinematic" onclick="setTool('kinematic')">
|
|
<span class="icon">◆</span> Kinematic
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<div class="section-title">Physics Controls</div>
|
|
<div class="slider-row">
|
|
<label>Gravity Y</label>
|
|
<input type="range" id="gravity-y" min="-2000" max="2000" value="980" oninput="updateGravity()">
|
|
<span class="val" id="gravity-y-val">980</span>
|
|
</div>
|
|
<div class="slider-row">
|
|
<label>Gravity X</label>
|
|
<input type="range" id="gravity-x" min="-1000" max="1000" value="0" oninput="updateGravity()">
|
|
<span class="val" id="gravity-x-val">0</span>
|
|
</div>
|
|
<div class="slider-row">
|
|
<label>Time Scale</label>
|
|
<input type="range" id="time-scale" min="0" max="300" value="100" oninput="updateTimeScale()">
|
|
<span class="val" id="time-scale-val">1.0</span>
|
|
</div>
|
|
<div class="slider-row">
|
|
<label>Damping</label>
|
|
<input type="range" id="damping" min="0" max="100" value="5" oninput="updateDamping()">
|
|
<span class="val" id="damping-val">0.05</span>
|
|
</div>
|
|
<div class="slider-row">
|
|
<label>Explosion</label>
|
|
<input type="range" id="explosion-force" min="100" max="5000" value="2000" oninput="updateExplosion()">
|
|
<span class="val" id="explosion-val">2000</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<div class="section-title">Stream Cipher</div>
|
|
<div class="slider-row">
|
|
<label>Key</label>
|
|
<input type="range" id="cipher-key" min="1" max="255" value="66" oninput="updateCipher()">
|
|
<span class="val" id="cipher-val">0x42</span>
|
|
</div>
|
|
<div class="stat-card" style="margin-top: 4px">
|
|
<div class="stat-label">Encrypted Preview</div>
|
|
<div id="cipher-preview" style="font-family: 'JetBrains Mono', monospace; font-size: 10px; word-break: break-all; color: var(--pink); margin-top: 4px;">—</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<div class="section-title">Keyboard</div>
|
|
<div style="font-size: 11px; color: var(--text-dim); line-height: 1.8;">
|
|
<span class="key">1-8</span> Switch tool<br>
|
|
<span class="key">Space</span> Pause/resume<br>
|
|
<span class="key">R</span> Reset world<br>
|
|
<span class="key">B</span> Burst spawn<br>
|
|
<span class="key">G</span> Toggle gravity<br>
|
|
<span class="key">Z</span> Zero gravity<br>
|
|
<span class="key">X</span> Explode center<br>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- Canvas -->
|
|
<main class="canvas-area">
|
|
<canvas id="physics-canvas"></canvas>
|
|
<div class="mode-banner" id="mode-banner">● Spawn Mode</div>
|
|
<div class="toast-container" id="toasts"></div>
|
|
</main>
|
|
|
|
<!-- Right Sidebar: Stats -->
|
|
<aside class="sidebar-right">
|
|
<div class="section">
|
|
<div class="section-title">World Stats</div>
|
|
<div class="stat-row">
|
|
<div class="stat-card">
|
|
<div class="stat-label">Bodies</div>
|
|
<div class="stat-value cyan" id="stat-bodies">0</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Awake</div>
|
|
<div class="stat-value green" id="stat-awake">0</div>
|
|
</div>
|
|
</div>
|
|
<div class="stat-row">
|
|
<div class="stat-card">
|
|
<div class="stat-label">Joints</div>
|
|
<div class="stat-value orange" id="stat-joints">0</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Steps</div>
|
|
<div class="stat-value pink" id="stat-steps">0</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<div class="section-title">Performance</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">FPS</div>
|
|
<div class="stat-value green" id="stat-fps">60</div>
|
|
</div>
|
|
<div class="stat-card" style="margin-top: 4px;">
|
|
<div class="stat-label">Step Time</div>
|
|
<div class="stat-value" id="stat-step-time" style="color: var(--cyan); font-size: 14px;">0.00 ms</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<div class="section-title">Stream Quality</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Connection Quality</div>
|
|
<div class="stream-bar"><div class="stream-bar-fill" id="quality-bar" style="width: 95%; background: linear-gradient(90deg, var(--green), var(--cyan));"></div></div>
|
|
<div style="font-family: 'JetBrains Mono'; font-size: 11px; color: var(--green);" id="quality-score">0.95</div>
|
|
</div>
|
|
<div class="stat-card" style="margin-top: 4px;">
|
|
<div class="stat-label">Congestion Window</div>
|
|
<div class="stream-bar"><div class="stream-bar-fill" id="cwnd-bar" style="width: 50%; background: linear-gradient(90deg, var(--accent), var(--pink));"></div></div>
|
|
<div style="font-family: 'JetBrains Mono'; font-size: 11px; color: var(--accent);" id="cwnd-val">50</div>
|
|
</div>
|
|
<div class="stat-card" style="margin-top: 4px;">
|
|
<div class="stat-label">Loss Ratio</div>
|
|
<div class="stream-bar"><div class="stream-bar-fill" id="loss-bar" style="width: 2%; background: var(--red);"></div></div>
|
|
<div style="font-family: 'JetBrains Mono'; font-size: 11px; color: var(--text-dim);" id="loss-val">0.02</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<div class="section-title">Channel Mux</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Active Channels</div>
|
|
<div class="stat-value" style="color: var(--yellow); font-size: 14px;" id="channel-count">3</div>
|
|
<div style="display: flex; gap: 4px; margin-top: 6px;">
|
|
<div style="flex:1; height: 4px; border-radius: 2px; background: var(--accent);" id="ch0"></div>
|
|
<div style="flex:1; height: 4px; border-radius: 2px; background: var(--pink);" id="ch1"></div>
|
|
<div style="flex:1; height: 4px; border-radius: 2px; background: var(--green);" id="ch2"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<div class="section-title">Frame Stats</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Dedup Skipped</div>
|
|
<div style="font-family: 'JetBrains Mono'; font-size: 14px; font-weight: 600; color: var(--orange);" id="dedup-count">0</div>
|
|
</div>
|
|
<div class="stat-card" style="margin-top: 4px;">
|
|
<div class="stat-label">Replay Frames</div>
|
|
<div style="font-family: 'JetBrains Mono'; font-size: 14px; font-weight: 600; color: var(--cyan);" id="replay-count">0</div>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- Footer -->
|
|
<footer class="footer">
|
|
<span>DreamStack Engine v0.50.0 — Physics <span style="color:var(--green)">138</span> · Stream <span style="color:var(--cyan)">201</span> · WASM <span style="color:var(--pink)">111</span> tests</span>
|
|
<span style="margin-left: auto;">Click canvas to interact · <span id="footer-tool">Spawn</span> mode</span>
|
|
</footer>
|
|
</div>
|
|
|
|
<script>
|
|
// ═══════════════════════════════════════════════════════════
|
|
// DreamStack Engine v0.50 Interactive Showcase
|
|
// Pure JS simulation of engine features
|
|
// ═══════════════════════════════════════════════════════════
|
|
|
|
const canvas = document.getElementById('physics-canvas');
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
// ─── State ───
|
|
let bodies = [];
|
|
let joints = [];
|
|
let rays = [];
|
|
let explosions = [];
|
|
let particles = [];
|
|
let currentTool = 'spawn';
|
|
let jointStart = null;
|
|
let paused = false;
|
|
let gravityOn = true;
|
|
let gravityX = 0, gravityY = 980;
|
|
let timeScale = 1.0;
|
|
let globalDamping = 0.05;
|
|
let explosionForce = 2000;
|
|
let stepCount = 0;
|
|
let lastFrameTime = performance.now();
|
|
let fps = 60;
|
|
let stepTime = 0;
|
|
let dragBody = null;
|
|
let dragOffset = { x: 0, y: 0 };
|
|
let mouseX = 0, mouseY = 0;
|
|
let velocityStart = null;
|
|
|
|
// Stream simulation state
|
|
let cipherKey = 0x42;
|
|
let cwnd = 50;
|
|
let lossRatio = 0.02;
|
|
let qualityScore = 0.95;
|
|
let dedupCount = 0;
|
|
let replayFrames = 0;
|
|
let lastHash = 0;
|
|
|
|
// Color palette for bodies
|
|
const COLORS = [
|
|
'#6366f1', '#8b5cf6', '#a855f7', '#d946ef', '#ec4899',
|
|
'#f43f5e', '#f97316', '#eab308', '#22c55e', '#14b8a6',
|
|
'#06b6d4', '#3b82f6', '#818cf8', '#c084fc',
|
|
];
|
|
|
|
// ─── Body Class ───
|
|
class Body {
|
|
constructor(x, y, radius, type = 'dynamic') {
|
|
this.x = x; this.y = y;
|
|
this.vx = 0; this.vy = 0;
|
|
this.radius = radius;
|
|
this.mass = Math.PI * radius * radius * 0.01;
|
|
this.rotation = 0;
|
|
this.angularVel = 0;
|
|
this.type = type; // dynamic, kinematic, static
|
|
this.sleeping = false;
|
|
this.sleepTimer = 0;
|
|
this.color = COLORS[Math.floor(Math.random() * COLORS.length)];
|
|
this.gravityScale = 1.0;
|
|
this.damping = globalDamping;
|
|
this.collisionGroup = 1;
|
|
this.collisionMask = 0xFFFF;
|
|
this.pulsePhase = Math.random() * Math.PI * 2;
|
|
this.id = bodies.length;
|
|
}
|
|
}
|
|
|
|
// ─── Joint Class ───
|
|
class Joint {
|
|
constructor(a, b, length) {
|
|
this.a = a; this.b = b;
|
|
this.length = length || dist(bodies[a], bodies[b]);
|
|
this.type = 'distance';
|
|
}
|
|
}
|
|
|
|
// ─── Resize ───
|
|
function resize() {
|
|
const rect = canvas.parentElement.getBoundingClientRect();
|
|
canvas.width = rect.width * devicePixelRatio;
|
|
canvas.height = rect.height * devicePixelRatio;
|
|
canvas.style.width = rect.width + 'px';
|
|
canvas.style.height = rect.height + 'px';
|
|
ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0);
|
|
}
|
|
window.addEventListener('resize', resize);
|
|
resize();
|
|
|
|
function W() { return canvas.width / devicePixelRatio; }
|
|
function H() { return canvas.height / devicePixelRatio; }
|
|
|
|
// ─── Physics Step ───
|
|
function physicsStep(dt) {
|
|
const t0 = performance.now();
|
|
dt *= timeScale;
|
|
if (dt <= 0 || dt > 0.1) dt = 1/60;
|
|
|
|
for (const body of bodies) {
|
|
if (body.type !== 'dynamic') continue;
|
|
|
|
// Sleep check (v0.28)
|
|
const speed = Math.sqrt(body.vx * body.vx + body.vy * body.vy);
|
|
if (speed < 0.5 && Math.abs(body.angularVel) < 0.01) {
|
|
body.sleepTimer += dt;
|
|
if (body.sleepTimer > 2) { body.sleeping = true; continue; }
|
|
} else { body.sleepTimer = 0; body.sleeping = false; }
|
|
|
|
// Gravity (v0.50)
|
|
if (gravityOn) {
|
|
body.vx += gravityX * body.gravityScale * dt;
|
|
body.vy += gravityY * body.gravityScale * dt;
|
|
}
|
|
|
|
// Damping (v0.30)
|
|
body.vx *= (1 - body.damping);
|
|
body.vy *= (1 - body.damping);
|
|
body.angularVel *= (1 - body.damping * 0.5);
|
|
|
|
// Integration
|
|
body.x += body.vx * dt;
|
|
body.y += body.vy * dt;
|
|
body.rotation += body.angularVel * dt;
|
|
|
|
// Boundary collisions
|
|
const w = W(), h = H();
|
|
if (body.x - body.radius < 0) { body.x = body.radius; body.vx = Math.abs(body.vx) * 0.7; }
|
|
if (body.x + body.radius > w) { body.x = w - body.radius; body.vx = -Math.abs(body.vx) * 0.7; }
|
|
if (body.y - body.radius < 0) { body.y = body.radius; body.vy = Math.abs(body.vy) * 0.7; }
|
|
if (body.y + body.radius > h) { body.y = h - body.radius; body.vy = -Math.abs(body.vy) * 0.7; }
|
|
}
|
|
|
|
// Body-body collisions (v0.50: contacts)
|
|
for (let i = 0; i < bodies.length; i++) {
|
|
for (let j = i + 1; j < bodies.length; j++) {
|
|
const a = bodies[i], b = bodies[j];
|
|
if (a.sleeping && b.sleeping) continue;
|
|
// Collision group check (v0.50)
|
|
if (!(a.collisionMask & b.collisionGroup) || !(b.collisionMask & a.collisionGroup)) continue;
|
|
const dx = b.x - a.x, dy = b.y - a.y;
|
|
const d = Math.sqrt(dx * dx + dy * dy);
|
|
const minDist = a.radius + b.radius;
|
|
if (d < minDist && d > 0.001) {
|
|
const nx = dx / d, ny = dy / d;
|
|
const overlap = minDist - d;
|
|
// Separate
|
|
if (a.type === 'dynamic') { a.x -= nx * overlap * 0.5; a.y -= ny * overlap * 0.5; }
|
|
if (b.type === 'dynamic') { b.x += nx * overlap * 0.5; b.y += ny * overlap * 0.5; }
|
|
// Impulse
|
|
const relVx = b.vx - a.vx, relVy = b.vy - a.vy;
|
|
const relVn = relVx * nx + relVy * ny;
|
|
if (relVn < 0) {
|
|
const j = -1.5 * relVn / (1/a.mass + 1/b.mass);
|
|
if (a.type === 'dynamic') { a.vx -= j * nx / a.mass; a.vy -= j * ny / a.mass; }
|
|
if (b.type === 'dynamic') { b.vx += j * nx / b.mass; b.vy += j * ny / b.mass; }
|
|
// Torque from collision (v0.28)
|
|
a.angularVel += (nx * relVy - ny * relVx) * 0.01;
|
|
b.angularVel -= (nx * relVy - ny * relVx) * 0.01;
|
|
// Wake up
|
|
a.sleeping = false; a.sleepTimer = 0;
|
|
b.sleeping = false; b.sleepTimer = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Joint constraints (v0.40)
|
|
for (const joint of joints) {
|
|
const a = bodies[joint.a], b = bodies[joint.b];
|
|
if (!a || !b) continue;
|
|
const dx = b.x - a.x, dy = b.y - a.y;
|
|
const d = Math.sqrt(dx * dx + dy * dy);
|
|
if (d > 0.001) {
|
|
const diff = (d - joint.length) / d;
|
|
const nx = dx * diff * 0.5, ny = dy * diff * 0.5;
|
|
if (a.type === 'dynamic') { a.x += nx; a.y += ny; }
|
|
if (b.type === 'dynamic') { b.x -= nx; b.y -= ny; }
|
|
}
|
|
}
|
|
|
|
// Drag
|
|
if (dragBody && currentTool === 'drag') {
|
|
const b = bodies[dragBody];
|
|
if (b) {
|
|
b.vx = (mouseX - b.x) * 10;
|
|
b.vy = (mouseY - b.y) * 10;
|
|
b.sleeping = false;
|
|
}
|
|
}
|
|
|
|
stepCount++;
|
|
stepTime = performance.now() - t0;
|
|
}
|
|
|
|
// ─── Explosion (v0.50) ───
|
|
function applyExplosion(cx, cy, radius, force) {
|
|
explosions.push({ x: cx, y: cy, radius, age: 0, maxAge: 0.5 });
|
|
// Spawn particles
|
|
for (let i = 0; i < 30; i++) {
|
|
const angle = Math.random() * Math.PI * 2;
|
|
const speed = 100 + Math.random() * 300;
|
|
particles.push({
|
|
x: cx, y: cy,
|
|
vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed,
|
|
life: 0.5 + Math.random() * 0.5, age: 0,
|
|
color: ['#f97316', '#eab308', '#ef4444', '#ffffff'][Math.floor(Math.random() * 4)],
|
|
size: 2 + Math.random() * 3,
|
|
});
|
|
}
|
|
for (const body of bodies) {
|
|
if (body.type !== 'dynamic') continue;
|
|
const dx = body.x - cx, dy = body.y - cy;
|
|
const d = Math.sqrt(dx * dx + dy * dy);
|
|
if (d < radius && d > 0.001) {
|
|
const strength = force * (1 - d / radius);
|
|
body.vx += (dx / d) * strength / body.mass;
|
|
body.vy += (dy / d) * strength / body.mass;
|
|
body.sleeping = false; body.sleepTimer = 0;
|
|
}
|
|
}
|
|
showToast('💥 Explosion! Force: ' + force);
|
|
}
|
|
|
|
// ─── Raycast (v0.40) ───
|
|
function castRay(ox, oy, dx, dy) {
|
|
const len = Math.sqrt(dx * dx + dy * dy);
|
|
if (len < 1) return;
|
|
const nx = dx / len, ny = dy / len;
|
|
const maxDist = 2000;
|
|
let bestDist = maxDist, bestBody = -1;
|
|
for (let i = 0; i < bodies.length; i++) {
|
|
const b = bodies[i];
|
|
const ex = b.x - ox, ey = b.y - oy;
|
|
const proj = ex * nx + ey * ny;
|
|
if (proj < 0) continue;
|
|
const perpDist = Math.abs(ex * ny - ey * nx);
|
|
if (perpDist < b.radius && proj < bestDist) {
|
|
bestDist = proj - Math.sqrt(b.radius * b.radius - perpDist * perpDist);
|
|
bestBody = i;
|
|
}
|
|
}
|
|
const hitX = ox + nx * Math.min(bestDist, maxDist);
|
|
const hitY = oy + ny * Math.min(bestDist, maxDist);
|
|
rays.push({ ox, oy, hx: hitX, hy: hitY, body: bestBody, age: 0, maxAge: 0.8 });
|
|
if (bestBody >= 0) showToast('⚡ Ray hit body #' + bestBody + ' at d=' + bestDist.toFixed(0));
|
|
}
|
|
|
|
// ─── Render ───
|
|
function render() {
|
|
const w = W(), h = H();
|
|
ctx.clearRect(0, 0, w, h);
|
|
|
|
// Grid
|
|
ctx.strokeStyle = 'rgba(255,255,255,0.03)';
|
|
ctx.lineWidth = 1;
|
|
for (let x = 0; x < w; x += 40) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, h); ctx.stroke(); }
|
|
for (let y = 0; y < h; y += 40) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke(); }
|
|
|
|
// Explosions
|
|
for (const ex of explosions) {
|
|
const progress = ex.age / ex.maxAge;
|
|
const r = ex.radius * progress;
|
|
const alpha = (1 - progress) * 0.3;
|
|
const gradient = ctx.createRadialGradient(ex.x, ex.y, 0, ex.x, ex.y, r);
|
|
gradient.addColorStop(0, `rgba(249,115,22,${alpha})`);
|
|
gradient.addColorStop(0.5, `rgba(239,68,68,${alpha * 0.5})`);
|
|
gradient.addColorStop(1, 'transparent');
|
|
ctx.fillStyle = gradient;
|
|
ctx.beginPath(); ctx.arc(ex.x, ex.y, r, 0, Math.PI * 2); ctx.fill();
|
|
}
|
|
|
|
// Particles
|
|
for (const p of particles) {
|
|
const alpha = 1 - p.age / p.life;
|
|
ctx.fillStyle = p.color + Math.floor(alpha * 255).toString(16).padStart(2, '0');
|
|
ctx.beginPath(); ctx.arc(p.x, p.y, p.size * alpha, 0, Math.PI * 2); ctx.fill();
|
|
}
|
|
|
|
// Joints (v0.40)
|
|
for (const joint of joints) {
|
|
const a = bodies[joint.a], b = bodies[joint.b];
|
|
if (!a || !b) continue;
|
|
ctx.strokeStyle = 'rgba(99,102,241,0.4)';
|
|
ctx.lineWidth = 2;
|
|
ctx.setLineDash([4, 4]);
|
|
ctx.beginPath(); ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y); ctx.stroke();
|
|
ctx.setLineDash([]);
|
|
// Joint anchor dots
|
|
ctx.fillStyle = '#6366f1';
|
|
ctx.beginPath(); ctx.arc(a.x, a.y, 3, 0, Math.PI * 2); ctx.fill();
|
|
ctx.beginPath(); ctx.arc(b.x, b.y, 3, 0, Math.PI * 2); ctx.fill();
|
|
}
|
|
|
|
// Rays (v0.40)
|
|
for (const ray of rays) {
|
|
const alpha = 1 - ray.age / ray.maxAge;
|
|
ctx.strokeStyle = `rgba(34,211,238,${alpha})`;
|
|
ctx.lineWidth = 2;
|
|
ctx.beginPath(); ctx.moveTo(ray.ox, ray.oy); ctx.lineTo(ray.hx, ray.hy); ctx.stroke();
|
|
// Hit point
|
|
if (ray.body >= 0) {
|
|
ctx.fillStyle = `rgba(239,68,68,${alpha})`;
|
|
ctx.beginPath(); ctx.arc(ray.hx, ray.hy, 5 * alpha, 0, Math.PI * 2); ctx.fill();
|
|
}
|
|
// Ray origin
|
|
ctx.fillStyle = `rgba(34,211,238,${alpha * 0.5})`;
|
|
ctx.beginPath(); ctx.arc(ray.ox, ray.oy, 3, 0, Math.PI * 2); ctx.fill();
|
|
}
|
|
|
|
// Bodies
|
|
const now = performance.now() / 1000;
|
|
for (const body of bodies) {
|
|
ctx.save();
|
|
ctx.translate(body.x, body.y);
|
|
ctx.rotate(body.rotation);
|
|
|
|
const pulse = 1 + Math.sin(now * 3 + body.pulsePhase) * 0.03;
|
|
const r = body.radius * pulse;
|
|
|
|
// Glow
|
|
if (!body.sleeping) {
|
|
const glow = ctx.createRadialGradient(0, 0, r * 0.5, 0, 0, r * 2.5);
|
|
glow.addColorStop(0, body.color + '20');
|
|
glow.addColorStop(1, 'transparent');
|
|
ctx.fillStyle = glow;
|
|
ctx.beginPath(); ctx.arc(0, 0, r * 2.5, 0, Math.PI * 2); ctx.fill();
|
|
}
|
|
|
|
// Body fill
|
|
const alpha = body.sleeping ? '60' : 'cc';
|
|
ctx.fillStyle = body.color + alpha;
|
|
ctx.strokeStyle = body.color;
|
|
ctx.lineWidth = body.type === 'kinematic' ? 3 : 1.5;
|
|
if (body.type === 'kinematic') ctx.setLineDash([3, 3]);
|
|
|
|
ctx.beginPath(); ctx.arc(0, 0, r, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
|
|
ctx.setLineDash([]);
|
|
|
|
// Rotation indicator (v0.28)
|
|
ctx.strokeStyle = 'rgba(255,255,255,0.4)';
|
|
ctx.lineWidth = 1;
|
|
ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(r * 0.8, 0); ctx.stroke();
|
|
|
|
// Sleep indicator
|
|
if (body.sleeping) {
|
|
ctx.fillStyle = 'rgba(255,255,255,0.3)';
|
|
ctx.font = `${Math.max(8, r * 0.6)}px Inter`;
|
|
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
|
ctx.fillText('z', 0, 0);
|
|
}
|
|
|
|
ctx.restore();
|
|
}
|
|
|
|
// Joint creation preview
|
|
if (currentTool === 'joint' && jointStart !== null) {
|
|
const a = bodies[jointStart];
|
|
if (a) {
|
|
ctx.strokeStyle = 'rgba(99,102,241,0.3)';
|
|
ctx.lineWidth = 2;
|
|
ctx.setLineDash([6, 4]);
|
|
ctx.beginPath(); ctx.moveTo(a.x, a.y); ctx.lineTo(mouseX, mouseY); ctx.stroke();
|
|
ctx.setLineDash([]);
|
|
}
|
|
}
|
|
|
|
// Velocity preview
|
|
if (currentTool === 'velocity' && velocityStart) {
|
|
ctx.strokeStyle = 'rgba(249,115,22,0.5)';
|
|
ctx.lineWidth = 2;
|
|
ctx.beginPath();
|
|
ctx.moveTo(velocityStart.x, velocityStart.y);
|
|
ctx.lineTo(mouseX, mouseY);
|
|
ctx.stroke();
|
|
// Arrow head
|
|
const dx = mouseX - velocityStart.x, dy = mouseY - velocityStart.y;
|
|
const len = Math.sqrt(dx * dx + dy * dy);
|
|
if (len > 10) {
|
|
const arrowSize = 8;
|
|
const angle = Math.atan2(dy, dx);
|
|
ctx.fillStyle = 'rgba(249,115,22,0.5)';
|
|
ctx.beginPath();
|
|
ctx.moveTo(mouseX, mouseY);
|
|
ctx.lineTo(mouseX - arrowSize * Math.cos(angle - 0.4), mouseY - arrowSize * Math.sin(angle - 0.4));
|
|
ctx.lineTo(mouseX - arrowSize * Math.cos(angle + 0.4), mouseY - arrowSize * Math.sin(angle + 0.4));
|
|
ctx.fill();
|
|
}
|
|
}
|
|
}
|
|
|
|
// ─── Stream Simulation ───
|
|
function simulateStream() {
|
|
// Simulate quality fluctuation
|
|
qualityScore = Math.max(0, Math.min(1, qualityScore + (Math.random() - 0.5) * 0.02));
|
|
lossRatio = Math.max(0, Math.min(0.3, lossRatio + (Math.random() - 0.52) * 0.005));
|
|
|
|
// AIMD congestion (v0.50)
|
|
if (Math.random() > lossRatio) { cwnd = Math.min(100, cwnd + 0.5); }
|
|
else { cwnd = Math.max(5, cwnd / 2); }
|
|
|
|
// Dedup check (v0.40)
|
|
const hash = stepCount * 31 + bodies.length;
|
|
if (hash === lastHash) dedupCount++;
|
|
lastHash = hash;
|
|
|
|
// Replay (v0.50)
|
|
if (stepCount % 10 === 0) replayFrames++;
|
|
|
|
// Cipher preview
|
|
if (stepCount % 30 === 0) updateCipherPreview();
|
|
}
|
|
|
|
// ─── Update Loop ───
|
|
function update() {
|
|
const now = performance.now();
|
|
const rawDt = (now - lastFrameTime) / 1000;
|
|
lastFrameTime = now;
|
|
fps = fps * 0.95 + (1 / Math.max(rawDt, 0.001)) * 0.05;
|
|
|
|
if (!paused) {
|
|
physicsStep(Math.min(rawDt, 1/30));
|
|
|
|
// Update particles
|
|
for (const p of particles) { p.x += p.vx * rawDt; p.y += p.vy * rawDt; p.vy += 200 * rawDt; p.age += rawDt; }
|
|
particles = particles.filter(p => p.age < p.life);
|
|
|
|
// Update explosions
|
|
for (const ex of explosions) ex.age += rawDt;
|
|
explosions = explosions.filter(e => e.age < e.maxAge);
|
|
|
|
// Update rays
|
|
for (const r of rays) r.age += rawDt;
|
|
rays = rays.filter(r => r.age < r.maxAge);
|
|
|
|
// Stream simulation
|
|
if (stepCount % 3 === 0) simulateStream();
|
|
}
|
|
|
|
render();
|
|
updateStats();
|
|
requestAnimationFrame(update);
|
|
}
|
|
|
|
// ─── Stats Update ───
|
|
function updateStats() {
|
|
document.getElementById('stat-bodies').textContent = bodies.length;
|
|
document.getElementById('stat-awake').textContent = bodies.filter(b => !b.sleeping && b.type === 'dynamic').length;
|
|
document.getElementById('stat-joints').textContent = joints.length;
|
|
document.getElementById('stat-steps').textContent = stepCount;
|
|
document.getElementById('stat-fps').textContent = Math.round(fps);
|
|
document.getElementById('stat-step-time').textContent = stepTime.toFixed(2) + ' ms';
|
|
|
|
// Stream
|
|
document.getElementById('quality-score').textContent = qualityScore.toFixed(2);
|
|
document.getElementById('quality-bar').style.width = (qualityScore * 100) + '%';
|
|
document.getElementById('quality-bar').style.background = qualityScore > 0.7 ? 'linear-gradient(90deg, var(--green), var(--cyan))' : qualityScore > 0.4 ? 'var(--yellow)' : 'var(--red)';
|
|
|
|
document.getElementById('cwnd-val').textContent = Math.round(cwnd);
|
|
document.getElementById('cwnd-bar').style.width = cwnd + '%';
|
|
|
|
document.getElementById('loss-val').textContent = lossRatio.toFixed(3);
|
|
document.getElementById('loss-bar').style.width = Math.min(lossRatio * 300, 100) + '%';
|
|
|
|
document.getElementById('dedup-count').textContent = dedupCount;
|
|
document.getElementById('replay-count').textContent = replayFrames;
|
|
|
|
// Channel activity animation
|
|
const t = performance.now() / 200;
|
|
document.getElementById('ch0').style.opacity = 0.3 + Math.sin(t) * 0.7;
|
|
document.getElementById('ch1').style.opacity = 0.3 + Math.sin(t + 2) * 0.7;
|
|
document.getElementById('ch2').style.opacity = 0.3 + Math.sin(t + 4) * 0.7;
|
|
}
|
|
|
|
// ─── Interactions ───
|
|
function getCanvasPos(e) {
|
|
const rect = canvas.getBoundingClientRect();
|
|
return { x: e.clientX - rect.left, y: e.clientY - rect.top };
|
|
}
|
|
|
|
function findBodyAt(x, y) {
|
|
for (let i = bodies.length - 1; i >= 0; i--) {
|
|
const b = bodies[i];
|
|
const d = Math.sqrt((b.x - x) ** 2 + (b.y - y) ** 2);
|
|
if (d < b.radius) return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
canvas.addEventListener('mousedown', (e) => {
|
|
const pos = getCanvasPos(e);
|
|
mouseX = pos.x; mouseY = pos.y;
|
|
|
|
switch (currentTool) {
|
|
case 'spawn': {
|
|
const r = 8 + Math.random() * 15;
|
|
const b = new Body(pos.x, pos.y, r);
|
|
bodies.push(b);
|
|
showToast('● Spawned body #' + b.id + ' (r=' + r.toFixed(0) + ')');
|
|
break;
|
|
}
|
|
case 'explode':
|
|
applyExplosion(pos.x, pos.y, 200, explosionForce);
|
|
break;
|
|
case 'joint': {
|
|
const hit = findBodyAt(pos.x, pos.y);
|
|
if (hit >= 0) {
|
|
if (jointStart === null) {
|
|
jointStart = hit;
|
|
showToast('🔗 Joint start: body #' + hit + ' — click another body');
|
|
} else {
|
|
if (jointStart !== hit) {
|
|
joints.push(new Joint(jointStart, hit));
|
|
showToast('🔗 Joint created: #' + jointStart + ' ↔ #' + hit);
|
|
}
|
|
jointStart = null;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case 'raycast':
|
|
velocityStart = { x: pos.x, y: pos.y };
|
|
break;
|
|
case 'drag': {
|
|
const hit = findBodyAt(pos.x, pos.y);
|
|
if (hit >= 0) { dragBody = hit; bodies[hit].sleeping = false; }
|
|
break;
|
|
}
|
|
case 'velocity':
|
|
velocityStart = { x: pos.x, y: pos.y };
|
|
break;
|
|
case 'teleport': {
|
|
const hit = findBodyAt(pos.x, pos.y);
|
|
if (hit >= 0) {
|
|
bodies[hit].x = W() / 2;
|
|
bodies[hit].y = H() / 4;
|
|
bodies[hit].vx = 0;
|
|
bodies[hit].vy = 0;
|
|
bodies[hit].sleeping = false;
|
|
showToast('⊕ Teleported body #' + hit + ' to center');
|
|
}
|
|
break;
|
|
}
|
|
case 'kinematic': {
|
|
const hit = findBodyAt(pos.x, pos.y);
|
|
if (hit >= 0) {
|
|
const b = bodies[hit];
|
|
b.type = b.type === 'kinematic' ? 'dynamic' : 'kinematic';
|
|
b.vx = 0; b.vy = 0;
|
|
showToast('◆ Body #' + hit + ' → ' + b.type);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
|
|
canvas.addEventListener('mousemove', (e) => {
|
|
const pos = getCanvasPos(e);
|
|
mouseX = pos.x; mouseY = pos.y;
|
|
});
|
|
|
|
canvas.addEventListener('mouseup', (e) => {
|
|
const pos = getCanvasPos(e);
|
|
if (currentTool === 'raycast' && velocityStart) {
|
|
castRay(velocityStart.x, velocityStart.y, pos.x - velocityStart.x, pos.y - velocityStart.y);
|
|
velocityStart = null;
|
|
}
|
|
if (currentTool === 'velocity' && velocityStart) {
|
|
const hit = findBodyAt(velocityStart.x, velocityStart.y);
|
|
if (hit >= 0) {
|
|
bodies[hit].vx = (pos.x - velocityStart.x) * 5;
|
|
bodies[hit].vy = (pos.y - velocityStart.y) * 5;
|
|
bodies[hit].sleeping = false;
|
|
showToast('→ Velocity set on body #' + hit);
|
|
}
|
|
velocityStart = null;
|
|
}
|
|
dragBody = null;
|
|
});
|
|
|
|
// ─── Keyboard ───
|
|
document.addEventListener('keydown', (e) => {
|
|
const tools = ['spawn', 'explode', 'joint', 'raycast', 'drag', 'velocity', 'teleport', 'kinematic'];
|
|
if (e.key >= '1' && e.key <= '8') { setTool(tools[parseInt(e.key) - 1]); return; }
|
|
switch (e.key.toLowerCase()) {
|
|
case ' ': e.preventDefault(); togglePause(); break;
|
|
case 'r': resetWorld(); break;
|
|
case 'b': spawnBurst(); break;
|
|
case 'g': toggleGravity(); break;
|
|
case 'z': gravityX = 0; gravityY = 0; document.getElementById('gravity-y').value = 0; document.getElementById('gravity-x').value = 0; document.getElementById('gravity-y-val').textContent = '0'; document.getElementById('gravity-x-val').textContent = '0'; showToast('Zero gravity!'); break;
|
|
case 'x': applyExplosion(W()/2, H()/2, 300, explosionForce); break;
|
|
}
|
|
});
|
|
|
|
// ─── Tool Management ───
|
|
function setTool(tool) {
|
|
currentTool = tool;
|
|
jointStart = null; velocityStart = null;
|
|
document.querySelectorAll('.tool-btn').forEach(b => b.classList.remove('active'));
|
|
document.querySelector(`[data-tool="${tool}"]`)?.classList.add('active');
|
|
const names = { spawn: '● Spawn', explode: '💥 Explode', joint: '🔗 Joint', raycast: '⚡ Raycast', drag: '✋ Drag', velocity: '→ Velocity', teleport: '⊕ Teleport', kinematic: '◆ Kinematic' };
|
|
document.getElementById('mode-banner').textContent = names[tool] + ' Mode';
|
|
document.getElementById('footer-tool').textContent = tool.charAt(0).toUpperCase() + tool.slice(1);
|
|
}
|
|
|
|
// ─── Actions ───
|
|
function resetWorld() {
|
|
bodies = []; joints = []; rays = []; explosions = []; particles = [];
|
|
stepCount = 0; dedupCount = 0; replayFrames = 0;
|
|
showToast('⟲ World reset');
|
|
}
|
|
|
|
function togglePause() {
|
|
paused = !paused;
|
|
document.getElementById('btn-pause').textContent = paused ? '▶ Play' : '⏸ Pause';
|
|
showToast(paused ? '⏸ Paused' : '▶ Resumed');
|
|
}
|
|
|
|
function toggleGravity() {
|
|
gravityOn = !gravityOn;
|
|
document.getElementById('btn-gravity-toggle').classList.toggle('active', gravityOn);
|
|
showToast(gravityOn ? '⇣ Gravity ON' : '⇡ Gravity OFF');
|
|
}
|
|
|
|
function spawnBurst() {
|
|
const cx = W() / 2, cy = H() / 3;
|
|
for (let i = 0; i < 15; i++) {
|
|
const angle = (i / 15) * Math.PI * 2;
|
|
const r = 6 + Math.random() * 10;
|
|
const b = new Body(cx + Math.cos(angle) * 40, cy + Math.sin(angle) * 40, r);
|
|
b.vx = Math.cos(angle) * 200;
|
|
b.vy = Math.sin(angle) * 200;
|
|
bodies.push(b);
|
|
}
|
|
showToast('✦ Burst! +15 bodies');
|
|
}
|
|
|
|
// ─── Slider Callbacks ───
|
|
function updateGravity() {
|
|
gravityY = parseFloat(document.getElementById('gravity-y').value);
|
|
gravityX = parseFloat(document.getElementById('gravity-x').value);
|
|
document.getElementById('gravity-y-val').textContent = gravityY;
|
|
document.getElementById('gravity-x-val').textContent = gravityX;
|
|
}
|
|
|
|
function updateTimeScale() {
|
|
timeScale = parseFloat(document.getElementById('time-scale').value) / 100;
|
|
document.getElementById('time-scale-val').textContent = timeScale.toFixed(1);
|
|
}
|
|
|
|
function updateDamping() {
|
|
globalDamping = parseFloat(document.getElementById('damping').value) / 1000;
|
|
document.getElementById('damping-val').textContent = globalDamping.toFixed(3);
|
|
for (const b of bodies) b.damping = globalDamping;
|
|
}
|
|
|
|
function updateExplosion() {
|
|
explosionForce = parseFloat(document.getElementById('explosion-force').value);
|
|
document.getElementById('explosion-val').textContent = explosionForce;
|
|
}
|
|
|
|
function updateCipher() {
|
|
cipherKey = parseInt(document.getElementById('cipher-key').value);
|
|
document.getElementById('cipher-val').textContent = '0x' + cipherKey.toString(16).toUpperCase().padStart(2, '0');
|
|
updateCipherPreview();
|
|
}
|
|
|
|
function updateCipherPreview() {
|
|
const plain = 'DreamStack v0.50';
|
|
const enc = Array.from(plain).map((c, i) => {
|
|
const b = c.charCodeAt(0) ^ cipherKey;
|
|
return b.toString(16).padStart(2, '0');
|
|
}).join(' ');
|
|
document.getElementById('cipher-preview').textContent = enc;
|
|
}
|
|
|
|
// ─── Toast System ───
|
|
function showToast(msg) {
|
|
const container = document.getElementById('toasts');
|
|
const toast = document.createElement('div');
|
|
toast.className = 'toast';
|
|
toast.textContent = msg;
|
|
container.appendChild(toast);
|
|
setTimeout(() => toast.remove(), 2500);
|
|
}
|
|
|
|
// ─── Utility ───
|
|
function dist(a, b) { return Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2); }
|
|
|
|
// ─── Init ───
|
|
function init() {
|
|
updateCipherPreview();
|
|
// Spawn a few initial bodies
|
|
for (let i = 0; i < 8; i++) {
|
|
const b = new Body(
|
|
100 + Math.random() * (W() - 200),
|
|
50 + Math.random() * (H() / 2),
|
|
10 + Math.random() * 12
|
|
);
|
|
b.vx = (Math.random() - 0.5) * 100;
|
|
bodies.push(b);
|
|
}
|
|
showToast('🚀 DreamStack Engine v0.50 loaded!');
|
|
update();
|
|
}
|
|
|
|
init();
|
|
</script>
|
|
</body>
|
|
</html>
|