dreamstack/engine/demo/showcase.html

1049 lines
40 KiB
HTML
Raw Normal View History

<!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>