feat(demo): pixel streaming demo with delta+RLE encoding

- Source (index.html): physics sim → pixel capture → delta XOR + RLE encode → WebSocket
- Receiver (receiver.html): WebSocket → decode delta/keyframe → render + click interaction + RTT
- Relay (relay.js): bidirectional WebSocket relay (pixels ↓ signals ↑)
- 97% compression (18KB/frame vs 675KB raw RGBA)
- Interactive: click on receiver → impulse force on source → round-trip latency measured
- Frame types: 0x11 keyframe (every 60 frames), 0x12 delta+RLE
- No buffer bloat: drops frames when pipe is busy
This commit is contained in:
enzotar 2026-03-10 20:49:35 -07:00
parent b0440e2e47
commit 9cc395d2a7
6 changed files with 1418 additions and 0 deletions

1
engine/demo/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
node_modules/

760
engine/demo/index.html Normal file
View file

@ -0,0 +1,760 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DreamStack — Pixel Stream Source</title>
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap"
rel="stylesheet">
<style>
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
--bg: #0a0a12;
--card: #12121e;
--surface: #1a1a2e;
--blue: #4f8fff;
--purple: #8b5cf6;
--green: #10b981;
--orange: #f59e0b;
--red: #ef4444;
--cyan: #06b6d4;
--text: #e2e8f0;
--dim: #64748b;
--border: #1e293b;
}
body {
font-family: 'Inter', sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
}
.header {
text-align: center;
padding: 1.5rem 1rem 1rem;
position: relative;
}
.header::after {
content: '';
position: absolute;
bottom: 0;
left: 10%;
width: 80%;
height: 1px;
background: linear-gradient(90deg, transparent, var(--blue), var(--purple), transparent);
}
.header h1 {
font-size: 1.6rem;
font-weight: 700;
background: linear-gradient(135deg, var(--blue), var(--purple));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.header .sub {
font-family: 'JetBrains Mono', monospace;
font-size: .7rem;
color: var(--green);
margin-top: .2rem;
letter-spacing: .05em;
}
.status-bar {
display: flex;
justify-content: center;
gap: 1.5rem;
padding: .8rem;
font-family: 'JetBrains Mono', monospace;
font-size: .7rem;
}
.status-item {
display: flex;
align-items: center;
gap: .4rem;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
.status-dot.on {
background: var(--green);
box-shadow: 0 0 8px var(--green);
}
.status-dot.off {
background: var(--red);
}
.layout {
display: flex;
gap: 1.5rem;
padding: 1rem 1.5rem;
max-width: 1200px;
margin: 0 auto;
}
.panel {
background: var(--card);
border: 1px solid var(--border);
border-radius: 12px;
overflow: hidden;
flex: 1;
}
.panel.source {
box-shadow: 0 0 20px rgba(79, 143, 255, 0.2);
}
.panel-label {
display: flex;
align-items: center;
gap: .5rem;
padding: .6rem 1rem;
font-size: .7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: .08em;
border-bottom: 1px solid var(--border);
color: var(--blue);
}
.panel-label .dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--blue);
animation: pulse 2s infinite;
}
@keyframes pulse {
0%,
100% {
opacity: 1
}
50% {
opacity: .3
}
}
canvas {
display: block;
width: 100%;
background: #08080f;
}
.stats {
display: flex;
gap: 1rem;
padding: .5rem 1rem;
font-family: 'JetBrains Mono', monospace;
font-size: .6rem;
color: var(--dim);
flex-wrap: wrap;
border-top: 1px solid var(--border);
}
.stat {
display: flex;
gap: .3rem;
}
.stat-v {
color: var(--green);
}
.controls {
display: flex;
gap: .5rem;
padding: .8rem 1.5rem;
max-width: 1200px;
margin: 0 auto;
flex-wrap: wrap;
justify-content: center;
}
.btn {
font-family: 'Inter', sans-serif;
font-size: .65rem;
font-weight: 600;
padding: .45rem .9rem;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--surface);
color: var(--dim);
cursor: pointer;
transition: all .2s;
text-transform: uppercase;
letter-spacing: .06em;
}
.btn:hover {
border-color: var(--blue);
color: var(--text);
transform: translateY(-1px);
}
.btn.active {
background: linear-gradient(135deg, var(--blue), var(--purple));
border-color: transparent;
color: white;
box-shadow: 0 0 12px rgba(79, 143, 255, .3);
}
.legend {
max-width: 1200px;
margin: .3rem auto .5rem;
padding: 0 1.5rem;
display: flex;
gap: .8rem;
flex-wrap: wrap;
justify-content: center;
}
.legend-item {
display: flex;
align-items: center;
gap: .3rem;
font-size: .6rem;
color: var(--dim);
}
.legend-dot {
width: 7px;
height: 7px;
border-radius: 50%;
}
/* Stream info panel */
.stream-panel {
min-width: 200px;
max-width: 220px;
display: flex;
flex-direction: column;
gap: .75rem;
padding-top: 1rem;
}
.info-card {
background: var(--card);
border: 1px solid var(--border);
border-radius: 8px;
padding: .5rem .75rem;
font-family: 'JetBrains Mono', monospace;
font-size: .55rem;
}
.info-card .title {
color: var(--cyan);
font-weight: 600;
text-transform: uppercase;
letter-spacing: .1em;
margin-bottom: .3rem;
font-size: .6rem;
}
.info-card .val {
color: var(--orange);
word-break: break-all;
line-height: 1.4;
}
.arrow {
text-align: center;
color: var(--green);
font-size: 1.2rem;
animation: flow 1.5s infinite;
}
@keyframes flow {
0% {
transform: translateY(-3px);
opacity: .3
}
50% {
transform: translateY(3px);
opacity: 1
}
100% {
transform: translateY(-3px);
opacity: .3
}
}
.arrow-label {
text-align: center;
font-size: .6rem;
color: var(--dim);
}
</style>
</head>
<body>
<div class="header">
<h1>DreamStack Engine — Source</h1>
<div class="sub">ds-physics v0.9.0 · pixel streaming via ds-stream</div>
</div>
<div class="status-bar">
<div class="status-item"><span class="status-dot off" id="wsDot"></span><span id="wsStatus"
style="color:var(--red)">Disconnected</span></div>
<div class="status-item"><span style="color:var(--dim)">Relay:</span> <span id="relayAddr"
style="color:var(--orange)">ws://localhost:9800/source</span></div>
</div>
<div class="controls">
<button class="btn active" onclick="setScene(0)">🔗 Joint Family</button>
<button class="btn" onclick="setScene(1)">🎯 Sensors</button>
<button class="btn" onclick="setScene(2)">💤 Sleeping</button>
<button class="btn" onclick="setScene(3)">⚙️ Motors</button>
<button class="btn" onclick="setScene(4)">💥 Collision Events</button>
</div>
<div class="legend" id="legend"></div>
<div class="layout">
<div class="panel source">
<div class="panel-label"><span class="dot"></span> Physics Simulation</div>
<canvas id="source" width="480" height="360"></canvas>
<div class="stats">
<div class="stat">FPS <span class="stat-v" id="fps">0</span></div>
<div class="stat">Bodies <span class="stat-v" id="bodyCount">0</span></div>
<div class="stat">Joints <span class="stat-v" id="jointCount">0</span></div>
<div class="stat">Events <span class="stat-v" id="eventCount">0</span></div>
</div>
</div>
<div class="stream-panel">
<div class="arrow-label">CAPTURE</div>
<div class="arrow"></div>
<div class="info-card">
<div class="title">ds-stream frame</div>
<div class="val" id="frameInfo"></div>
</div>
<div class="info-card">
<div class="title">Header (16B)</div>
<div class="val" id="frameHex"></div>
</div>
<div class="arrow"></div>
<div class="arrow-label">WebSocket</div>
<div class="arrow"></div>
<div class="info-card">
<div class="title">Streamed</div>
<div class="val" id="streamStats"></div>
</div>
</div>
</div>
<script>
// ─── ds-stream Protocol ───
const FRAME_KEYFRAME = 0x11;
const FRAME_DELTA = 0x12;
const SIGNAL_CLICK = 0x30;
const SIGNAL_PONG = 0x31;
const HEADER_SIZE = 16;
const KEYFRAME_INTERVAL = 60; // full frame every N frames
// Previous frame buffer for delta encoding
let prevFrameBuffer = null;
// Remote click flashes
const clickFlashes = [];
function encodeFrame(seq, timestamp, w, h, pixels) {
const totalPixels = w * h * 4;
const isKeyframe = (seq % KEYFRAME_INTERVAL === 0) || !prevFrameBuffer;
let payload, frameType;
if (isKeyframe) {
// Keyframe: send raw pixels (receiver resets its buffer)
frameType = FRAME_KEYFRAME;
payload = pixels;
// Save as previous
prevFrameBuffer = new Uint8Array(totalPixels);
prevFrameBuffer.set(pixels.subarray(0, totalPixels));
} else {
// Delta: XOR with previous frame, then RLE encode
frameType = FRAME_DELTA;
payload = encodeDeltaRLE(pixels, prevFrameBuffer, totalPixels);
// Update previous buffer
prevFrameBuffer.set(pixels.subarray(0, totalPixels));
}
// Build frame: [header 16B] [payload]
const frame = new Uint8Array(HEADER_SIZE + payload.length);
const v = new DataView(frame.buffer);
frame[0] = frameType;
frame[1] = 0x00;
v.setUint16(2, seq & 0xFFFF, true);
v.setUint32(4, timestamp, true);
v.setUint16(8, w, true);
v.setUint16(10, h, true);
v.setUint32(12, payload.length, true);
frame.set(payload, HEADER_SIZE);
return frame;
}
// RLE-encode XOR delta: [count_hi, count_lo, r, g, b, a] per run
// Runs of identical RGBA quads are collapsed
function encodeDeltaRLE(current, previous, totalBytes) {
const chunks = [];
let i = 0;
while (i < totalBytes) {
// XOR this pixel
const dr = current[i] ^ previous[i];
const dg = current[i + 1] ^ previous[i + 1];
const db = current[i + 2] ^ previous[i + 2];
const da = current[i + 3] ^ previous[i + 3];
// Count consecutive identical XOR values
let runLen = 1;
let j = i + 4;
while (j < totalBytes && runLen < 65535) {
if ((current[j] ^ previous[j]) !== dr ||
(current[j + 1] ^ previous[j + 1]) !== dg ||
(current[j + 2] ^ previous[j + 2]) !== db ||
(current[j + 3] ^ previous[j + 3]) !== da) break;
runLen++;
j += 4;
}
// Emit: [count_hi, count_lo, r, g, b, a]
chunks.push(runLen >> 8, runLen & 0xFF, dr, dg, db, da);
i = j;
}
return new Uint8Array(chunks);
}
// ─── Physics (same engine as before) ───
class Vec2 {
constructor(x = 0, y = 0) { this.x = x; this.y = y }
add(v) { return new Vec2(this.x + v.x, this.y + v.y) }
sub(v) { return new Vec2(this.x - v.x, this.y - v.y) }
scale(s) { return new Vec2(this.x * s, this.y * s) }
len() { return Math.sqrt(this.x * this.x + this.y * this.y) }
norm() { const l = this.len(); return l > 0 ? this.scale(1 / l) : new Vec2() }
dot(v) { return this.x * v.x + this.y * v.y }
}
class Body {
constructor(x, y, r, type = 'dynamic') {
this.pos = new Vec2(x, y); this.vel = new Vec2(); this.acc = new Vec2();
this.radius = r; this.width = r * 2; this.height = r * 2; this.shape = 'circle';
this.type = type; this.color = '#4f8fff'; this.sleeping = false; this.removed = false;
this.angularVel = 0; this.angle = 0; this.mass = r * 0.1; this.glowColor = null; this.label = '';
}
}
class Joint {
constructor(a, b, type, p = {}) { this.bodyA = a; this.bodyB = b; this.type = type; this.params = p; this.removed = false; this.color = '#64748b'; this.motorAngle = 0; }
}
class PhysicsWorld {
constructor(w, h) { this.width = w; this.height = h; this.bodies = []; this.joints = []; this.gravity = new Vec2(0, 120); this.events = []; this.prevContacts = new Set(); }
createCircle(x, y, r, t = 'dynamic') { const b = new Body(x, y, r, t); this.bodies.push(b); return this.bodies.length - 1; }
createSensor(x, y, r) { const i = this.createCircle(x, y, r, 'sensor'); this.bodies[i].color = 'rgba(16,185,129,0.25)'; this.bodies[i].glowColor = '#10b981'; return i; }
createJoint(a, b, t, p = {}) { this.joints.push(new Joint(a, b, t, p)); return this.joints.length - 1; }
step(dt) {
this.events = []; const contacts = new Set();
for (const b of this.bodies) {
if (b.removed || b.sleeping || b.type === 'kinematic' || b.type === 'sensor') continue;
b.vel = b.vel.add(this.gravity.scale(dt)).add(b.acc.scale(dt)); b.acc = new Vec2();
b.vel = b.vel.scale(0.998); b.pos = b.pos.add(b.vel.scale(dt)); b.angle += b.angularVel * dt;
if (b.pos.x - b.radius < 0) { b.pos.x = b.radius; b.vel.x *= -0.5 }
if (b.pos.x + b.radius > this.width) { b.pos.x = this.width - b.radius; b.vel.x *= -0.5 }
if (b.pos.y - b.radius < 0) { b.pos.y = b.radius; b.vel.y *= -0.5 }
if (b.pos.y + b.radius > this.height) { b.pos.y = this.height - b.radius; b.vel.y *= -0.5 }
}
for (let i = 0; i < this.bodies.length; i++) {
for (let j = i + 1; j < this.bodies.length; j++) {
const a = this.bodies[i], b = this.bodies[j];
if (a.removed || b.removed) continue;
const d = a.pos.sub(b.pos), dist = d.len(), minD = a.radius + b.radius;
if (dist < minD && dist > 0) {
contacts.add(`${i}:${j}`);
if (a.type !== 'sensor' && b.type !== 'sensor') {
const n = d.scale(1 / dist), ov = minD - dist;
if (a.type === 'dynamic' && b.type === 'dynamic') {
a.pos = a.pos.add(n.scale(ov * 0.5)); b.pos = b.pos.sub(n.scale(ov * 0.5));
const rv = a.vel.sub(b.vel).dot(n);
if (rv < 0) { a.vel = a.vel.sub(n.scale(rv * 0.8)); b.vel = b.vel.add(n.scale(rv * 0.8)) }
} else if (a.type === 'dynamic') { a.pos = a.pos.add(n.scale(ov)); const rv = a.vel.dot(n); if (rv < 0) a.vel = a.vel.sub(n.scale(rv * 1.5)) }
else if (b.type === 'dynamic') { b.pos = b.pos.sub(n.scale(ov)); const rv = b.vel.dot(n); if (rv > 0) b.vel = b.vel.sub(n.scale(rv * 1.5)) }
}
}
}
}
for (const k of contacts) if (!this.prevContacts.has(k)) { const [a, b] = k.split(':').map(Number); this.events.push({ bodyA: a, bodyB: b, started: true }) }
for (const k of this.prevContacts) if (!contacts.has(k)) { const [a, b] = k.split(':').map(Number); this.events.push({ bodyA: a, bodyB: b, started: false }) }
this.prevContacts = contacts;
for (const j of this.joints) {
if (j.removed) continue; const a = this.bodies[j.bodyA], b = this.bodies[j.bodyB];
const diff = b.pos.sub(a.pos), dist = diff.len(), norm = dist > 0 ? diff.scale(1 / dist) : new Vec2(1, 0);
switch (j.type) {
case 'spring': { const rest = j.params.restLength || 60, k = j.params.stiffness || 0.02, f = (dist - rest) * k; if (a.type === 'dynamic') a.vel = a.vel.add(norm.scale(f)); if (b.type === 'dynamic') b.vel = b.vel.sub(norm.scale(f)); break }
case 'fixed': { if (dist > 0.1) { const c = diff.sub(norm.scale(j.params.restLength || 0)); if (a.type === 'dynamic') a.pos = a.pos.add(c.scale(0.5)); if (b.type === 'dynamic') b.pos = b.pos.sub(c.scale(0.5)) } break }
case 'revolute': { const anchor = j.params.anchor || a.pos, arm = j.params.armLength || 50; const toB = b.pos.sub(anchor), d2 = toB.len(); if (d2 > 0) b.pos = anchor.add(toB.scale(arm / d2)); if (j.params.motorVel) { j.motorAngle += j.params.motorVel * dt; b.pos = new Vec2(anchor.x + Math.cos(j.motorAngle) * arm, anchor.y + Math.sin(j.motorAngle) * arm) } break }
case 'prismatic': { const ax = j.params.axis || new Vec2(1, 0), o = a.pos, toB2 = b.pos.sub(o), proj = toB2.dot(ax); b.pos = o.add(ax.scale(proj)); if (j.params.min !== undefined && proj < j.params.min) b.pos = o.add(ax.scale(j.params.min)); if (j.params.max !== undefined && proj > j.params.max) b.pos = o.add(ax.scale(j.params.max)); if (j.params.motorVel && b.type === 'dynamic') b.vel = b.vel.add(ax.scale(j.params.motorVel * 0.5)); break }
case 'rope': { const mxD = j.params.maxDistance || 100; if (dist > mxD) { const c = norm.scale(dist - mxD); if (b.type === 'dynamic') { b.pos = b.pos.sub(c); b.vel = b.vel.sub(norm.scale(b.vel.dot(norm) * 0.8)) } } break }
}
}
}
}
// ─── Scenes ───
let currentScene = 0, world, frameSeq = 0, totalBytes = 0, totalFrames = 0;
const scenes = [
{
name: 'Joint Family', legend: [{ color: '#f59e0b', label: 'Spring' }, { color: '#ef4444', label: 'Fixed' }, { color: '#8b5cf6', label: 'Revolute' }, { color: '#06b6d4', label: 'Prismatic' }, { color: '#10b981', label: 'Rope' }],
setup(w) {
w.gravity = new Vec2(0, 80);
const sa = w.createCircle(60, 80, 8, 'kinematic'); w.bodies[sa].color = '#334155';
const sb = w.createCircle(60, 160, 12); w.bodies[sb].color = '#f59e0b';
w.createJoint(sa, sb, 'spring', { restLength: 60, stiffness: 0.015 }); w.joints[w.joints.length - 1].color = '#f59e0b';
const fa = w.createCircle(160, 100, 8, 'kinematic'); w.bodies[fa].color = '#334155';
const fb = w.createCircle(190, 130, 12); w.bodies[fb].color = '#ef4444';
w.createJoint(fa, fb, 'fixed', { restLength: 42 }); w.joints[w.joints.length - 1].color = '#ef4444';
const ra = w.createCircle(280, 120, 8, 'kinematic'); w.bodies[ra].color = '#334155';
const rb = w.createCircle(330, 120, 12); w.bodies[rb].color = '#8b5cf6';
w.createJoint(ra, rb, 'revolute', { anchor: w.bodies[ra].pos, armLength: 50, motorVel: 2.5 }); w.joints[w.joints.length - 1].color = '#8b5cf6';
const pa = w.createCircle(80, 280, 8, 'kinematic'); w.bodies[pa].color = '#334155';
const pb = w.createCircle(160, 280, 14); w.bodies[pb].color = '#06b6d4';
w.createJoint(pa, pb, 'prismatic', { axis: new Vec2(1, 0), min: 20, max: 140, motorVel: 80 }); w.joints[w.joints.length - 1].color = '#06b6d4';
const rpa = w.createCircle(350, 60, 8, 'kinematic'); w.bodies[rpa].color = '#334155';
const rpb = w.createCircle(380, 180, 14); w.bodies[rpb].color = '#10b981';
w.createJoint(rpa, rpb, 'rope', { maxDistance: 130 }); w.joints[w.joints.length - 1].color = '#10b981';
}
},
{
name: 'Sensors', legend: [{ color: '#10b981', label: 'Sensor' }, { color: '#4f8fff', label: 'Dynamic' }, { color: '#f59e0b', label: 'Detected' }],
setup(w) {
w.gravity = new Vec2(0, 60);
w.createSensor(120, 200, 60); w.bodies[w.bodies.length - 1].label = 'SENSOR A';
w.createSensor(360, 200, 60); w.bodies[w.bodies.length - 1].label = 'SENSOR B';
for (let i = 0; i < 8; i++) { const x = 60 + Math.random() * 360; const idx = w.createCircle(x, -20 - i * 40, 8 + Math.random() * 8); w.bodies[idx].color = '#4f8fff'; w.bodies[idx].vel = new Vec2((Math.random() - 0.5) * 60, Math.random() * 30) }
}
},
{
name: 'Sleeping', legend: [{ color: '#4f8fff', label: 'Awake' }, { color: '#334155', label: 'Sleeping' }, { color: '#f59e0b', label: 'Waking' }],
setup(w) {
w.gravity = new Vec2(0, 100);
for (let r = 0; r < 4; r++)for (let c = 0; c < 5; c++) { const idx = w.createCircle(140 + c * 50, 300 - r * 35, 14); w.bodies[idx].sleeping = true; w.bodies[idx].color = '#334155'; w.bodies[idx].label = '💤' }
const wk = w.createCircle(240, 50, 16); w.bodies[wk].color = '#f59e0b'; w.bodies[wk].label = '⚡'; w.bodies[wk].vel = new Vec2(0, 80);
}
},
{
name: 'Motors', legend: [{ color: '#8b5cf6', label: 'Revolute' }, { color: '#06b6d4', label: 'Prismatic' }],
setup(w) {
w.gravity = new Vec2(0, 0);
const speeds = [1.5, -2.5, 3.5, -1.0], colors = ['#8b5cf6', '#a855f7', '#c084fc', '#7c3aed'];
for (let i = 0; i < 4; i++) {
const cx = 80 + i * 100; const a = w.createCircle(cx, 120, 6, 'kinematic'); w.bodies[a].color = '#1e293b';
for (let j = 1; j <= 3; j++) { const arm = w.createCircle(cx + j * 20, 120, 5 + j * 2); w.bodies[arm].color = colors[i]; w.createJoint(a, arm, 'revolute', { anchor: w.bodies[a].pos, armLength: j * 20, motorVel: speeds[i] * (1 + j * 0.3) }); w.joints[w.joints.length - 1].color = colors[i] + '60' }
}
for (let i = 0; i < 3; i++) { const cy = 250 + i * 40; const a = w.createCircle(60, cy, 5, 'kinematic'); w.bodies[a].color = '#1e293b'; const b = w.createCircle(100, cy, 10); w.bodies[b].color = '#06b6d4'; w.createJoint(a, b, 'prismatic', { axis: new Vec2(1, 0), min: 20, max: 350, motorVel: 60 + i * 40 }); w.joints[w.joints.length - 1].color = '#06b6d480' }
}
},
{
name: 'Collisions', legend: [{ color: '#4f8fff', label: 'Body' }, { color: '#ef4444', label: 'Start' }, { color: '#10b981', label: 'End' }],
setup(w) {
w.gravity = new Vec2(0, 100);
for (let i = 0; i < 12; i++) { const idx = w.createCircle(60 + Math.random() * 360, 30 + Math.random() * 100, 10 + Math.random() * 12); w.bodies[idx].color = '#4f8fff'; w.bodies[idx].vel = new Vec2((Math.random() - 0.5) * 100, 0) }
}
}
];
function setScene(idx) {
currentScene = idx; world = new PhysicsWorld(480, 360); scenes[idx].setup(world); frameSeq = 0;
document.querySelectorAll('.btn').forEach((b, i) => b.classList.toggle('active', i === idx));
document.getElementById('legend').innerHTML = scenes[idx].legend.map(l => `<div class="legend-item"><span class="legend-dot" style="background:${l.color}"></span>${l.label}</div>`).join('');
}
// ─── WebSocket ───
let ws = null;
let wsConnected = false;
function connectWS() {
ws = new WebSocket('ws://localhost:9800/source');
ws.binaryType = 'arraybuffer';
ws.onopen = () => {
wsConnected = true;
document.getElementById('wsDot').className = 'status-dot on';
document.getElementById('wsStatus').textContent = 'Connected';
document.getElementById('wsStatus').style.color = 'var(--green)';
};
ws.onclose = () => {
wsConnected = false;
document.getElementById('wsDot').className = 'status-dot off';
document.getElementById('wsStatus').textContent = 'Disconnected';
document.getElementById('wsStatus').style.color = 'var(--red)';
setTimeout(connectWS, 2000);
};
ws.onmessage = (evt) => {
// Handle interaction signals from receiver
const data = new Uint8Array(evt.data);
if (data[0] === SIGNAL_CLICK && data.length >= 20) {
const view = new DataView(evt.data);
const clickX = view.getFloat32(4, true);
const clickY = view.getFloat32(8, true);
const clickTs = view.getFloat64(12, true);
// Find nearest dynamic body and apply impulse
let nearest = -1, nearDist = Infinity;
for (let i = 0; i < world.bodies.length; i++) {
const b = world.bodies[i];
if (b.removed || b.type !== 'dynamic') continue;
const d = new Vec2(clickX - b.pos.x, clickY - b.pos.y).len();
if (d < nearDist) { nearDist = d; nearest = i; }
}
if (nearest >= 0 && nearDist < 100) {
const b = world.bodies[nearest];
const dir = new Vec2(b.pos.x - clickX, b.pos.y - clickY).norm();
b.vel = b.vel.add(dir.scale(200));
// Wake sleeping bodies
if (b.sleeping) {
b.sleeping = false; b.color = '#f59e0b'; b.label = '⚡';
setTimeout(() => { b.color = '#4f8fff'; b.label = ''; }, 500);
}
}
// Flash marker at click position
clickFlashes.push({ x: clickX, y: clickY, t: performance.now(), radius: 20 });
// Send pong with original timestamp for RTT
const pong = new ArrayBuffer(20);
const pv = new DataView(pong);
new Uint8Array(pong)[0] = SIGNAL_PONG;
pv.setFloat32(4, clickX, true);
pv.setFloat32(8, clickY, true);
pv.setFloat64(12, clickTs, true);
if (wsConnected && ws.readyState === 1) ws.send(pong);
}
};
ws.onerror = () => ws.close();
}
connectWS();
// ─── Render ───
const can = document.getElementById('source');
const ctx = can.getContext('2d');
function render() {
ctx.fillStyle = '#08080f'; ctx.fillRect(0, 0, 480, 360);
ctx.strokeStyle = '#ffffff08'; ctx.lineWidth = 1;
for (let x = 0; x < 480; x += 40) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, 360); ctx.stroke() }
for (let y = 0; y < 360; y += 40) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(480, y); ctx.stroke() }
for (const j of world.joints) {
if (j.removed) continue; const a = world.bodies[j.bodyA], b = world.bodies[j.bodyB];
ctx.strokeStyle = j.color; ctx.lineWidth = j.type === 'rope' ? 1 : 2;
if (j.type === 'spring') {
const dx = b.pos.x - a.pos.x, dy = b.pos.y - a.pos.y, len = Math.sqrt(dx * dx + dy * dy), nx = -dy / len, ny = dx / len;
ctx.beginPath(); ctx.moveTo(a.pos.x, a.pos.y);
for (let i = 1; i < 8; i++) { const t = i / 8, s = (i % 2 === 0 ? 1 : -1) * 6; ctx.lineTo(a.pos.x + dx * t + nx * s, a.pos.y + dy * t + ny * s) }
ctx.lineTo(b.pos.x, b.pos.y); ctx.stroke();
} else if (j.type === 'rope') { ctx.setLineDash([4, 4]); ctx.beginPath(); ctx.moveTo(a.pos.x, a.pos.y); ctx.lineTo(b.pos.x, b.pos.y); ctx.stroke(); ctx.setLineDash([]) }
else { ctx.beginPath(); ctx.moveTo(a.pos.x, a.pos.y); ctx.lineTo(b.pos.x, b.pos.y); ctx.stroke() }
}
for (let i = 0; i < world.bodies.length; i++) {
const b = world.bodies[i]; if (b.removed) continue;
if (b.type === 'sensor') {
ctx.save(); ctx.globalAlpha = 0.15 + Math.sin(Date.now() * 0.003) * 0.05; ctx.fillStyle = b.glowColor || '#10b981';
ctx.beginPath(); ctx.arc(b.pos.x, b.pos.y, b.radius, 0, Math.PI * 2); ctx.fill();
ctx.globalAlpha = 0.6; ctx.strokeStyle = b.glowColor || '#10b981'; ctx.lineWidth = 1; ctx.setLineDash([4, 4]); ctx.stroke(); ctx.setLineDash([]); ctx.restore();
let hasOv = false;
for (let j = 0; j < world.bodies.length; j++) { if (i === j || world.bodies[j].type === 'sensor') continue; if (b.pos.sub(world.bodies[j].pos).len() < b.radius + world.bodies[j].radius) { hasOv = true; world.bodies[j].glowColor = '#f59e0b' } }
if (hasOv) { ctx.save(); ctx.globalAlpha = 0.4; ctx.fillStyle = '#f59e0b'; ctx.beginPath(); ctx.arc(b.pos.x, b.pos.y, b.radius, 0, Math.PI * 2); ctx.fill(); ctx.restore() }
if (b.label) { ctx.fillStyle = '#10b981'; ctx.font = '500 9px Inter'; ctx.textAlign = 'center'; ctx.fillText(b.label, b.pos.x, b.pos.y + b.radius + 14) }
continue;
}
ctx.save(); ctx.shadowColor = b.glowColor || b.color; ctx.shadowBlur = b.sleeping ? 0 : 8; ctx.fillStyle = b.color; ctx.globalAlpha = b.sleeping ? 0.4 : 1;
ctx.beginPath(); ctx.arc(b.pos.x, b.pos.y, b.radius, 0, Math.PI * 2); ctx.fill(); ctx.restore();
if (b.label) { ctx.fillStyle = '#fff'; ctx.font = '10px Inter'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(b.label, b.pos.x, b.pos.y) }
for (const evt of world.events) { if (evt.bodyA === i || evt.bodyB === i) { ctx.save(); ctx.globalAlpha = 0.6; ctx.strokeStyle = evt.started ? '#ef4444' : '#10b981'; ctx.lineWidth = 3; ctx.beginPath(); ctx.arc(b.pos.x, b.pos.y, b.radius + 6, 0, Math.PI * 2); ctx.stroke(); ctx.restore() } }
}
ctx.fillStyle = '#ffffff30'; ctx.font = '500 10px Inter'; ctx.textAlign = 'left'; ctx.fillText(scenes[currentScene].name.toUpperCase(), 10, 350);
// Draw click flash markers
const now = performance.now();
for (let i = clickFlashes.length - 1; i >= 0; i--) {
const f = clickFlashes[i];
const age = (now - f.t) / 1000;
if (age > 0.5) { clickFlashes.splice(i, 1); continue; }
const alpha = 1 - age * 2;
const r = f.radius + age * 40;
ctx.save();
ctx.globalAlpha = alpha;
ctx.strokeStyle = '#ff6b6b';
ctx.lineWidth = 2;
ctx.beginPath(); ctx.arc(f.x, f.y, r, 0, Math.PI * 2); ctx.stroke();
// Crosshair
ctx.beginPath(); ctx.moveTo(f.x - 8, f.y); ctx.lineTo(f.x + 8, f.y); ctx.stroke();
ctx.beginPath(); ctx.moveTo(f.x, f.y - 8); ctx.lineTo(f.x, f.y + 8); ctx.stroke();
ctx.restore();
}
}
function streamFrame() {
// Drop frame if previous one hasn't flushed yet (no buffering)
if (wsConnected && ws.bufferedAmount > 0) return;
const sw = 480, sh = 360;
const imgData = ctx.getImageData(0, 0, sw, sh);
const pixels = new Uint8Array(imgData.data.buffer);
const ts = Math.floor(performance.now()) & 0xFFFFFFFF;
const frame = encodeFrame(frameSeq++, ts, sw, sh, pixels);
totalBytes += frame.length; totalFrames++;
if (wsConnected && ws.readyState === 1) {
ws.send(frame);
}
// Stats
const rawSize = sw * sh * 4;
const ratio = ((1 - frame.length / (rawSize + HEADER_SIZE)) * 100).toFixed(0);
const typeStr = frame[0] === FRAME_KEYFRAME ? 'KEY' : 'Δ';
const hdr = Array.from(frame.slice(0, HEADER_SIZE)).map(b => b.toString(16).padStart(2, '0')).join(' ');
document.getElementById('frameInfo').textContent = `${typeStr} seq=${(frameSeq - 1) & 0xFFFF} ${sw}×${sh}`;
document.getElementById('frameHex').textContent = hdr;
document.getElementById('streamStats').textContent = `${totalFrames} frames · ${fmtB(totalBytes)} · ${fmtB(frame.length)}/f (-${ratio}%)`;
}
function fmtB(b) { if (b < 1024) return b + 'B'; if (b < 1048576) return (b / 1024).toFixed(1) + 'KB'; return (b / 1048576).toFixed(1) + 'MB' }
// Sleeping wake logic
function tickSleeping() { if (currentScene !== 2) return; for (let i = 0; i < world.bodies.length; i++) { const b = world.bodies[i]; if (!b.sleeping) continue; for (let j = 0; j < world.bodies.length; j++) { if (i === j) continue; const o = world.bodies[j]; if (!o.sleeping && o.type === 'dynamic' && b.pos.sub(o.pos).len() < b.radius + o.radius + 10) { b.sleeping = false; b.color = '#f59e0b'; b.label = '⚡'; b.vel = new Vec2((Math.random() - 0.5) * 80, -40); setTimeout(() => { b.color = '#4f8fff'; b.label = '' }, 500) } } } }
let lastT = 0, fpsCnt = 0, fpsT = 0, dFps = 0;
function loop(t) {
const dt = Math.min((t - lastT) / 1000, 1 / 30); lastT = t;
fpsCnt++; fpsT += dt; if (fpsT >= 1) { dFps = fpsCnt; fpsCnt = 0; fpsT = 0 }
tickSleeping(); world.step(dt); render(); streamFrame();
document.getElementById('fps').textContent = dFps;
document.getElementById('bodyCount').textContent = world.bodies.filter(b => !b.removed).length;
document.getElementById('jointCount').textContent = world.joints.filter(j => !j.removed).length;
document.getElementById('eventCount').textContent = world.events.length;
requestAnimationFrame(loop);
}
setScene(0); requestAnimationFrame(loop);
</script>
</body>
</html>

37
engine/demo/package-lock.json generated Normal file
View file

@ -0,0 +1,37 @@
{
"name": "demo",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "demo",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"ws": "^8.19.0"
}
},
"node_modules/ws": {
"version": "8.19.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
}
}
}

15
engine/demo/package.json Normal file
View file

@ -0,0 +1,15 @@
{
"name": "demo",
"version": "1.0.0",
"main": "relay.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"ws": "^8.19.0"
}
}

536
engine/demo/receiver.html Normal file
View file

@ -0,0 +1,536 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DreamStack — Pixel Stream Receiver</title>
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap"
rel="stylesheet">
<style>
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0
}
:root {
--bg: #0a0a12;
--card: #12121e;
--surface: #1a1a2e;
--blue: #4f8fff;
--purple: #8b5cf6;
--green: #10b981;
--orange: #f59e0b;
--red: #ef4444;
--cyan: #06b6d4;
--text: #e2e8f0;
--dim: #64748b;
--border: #1e293b
}
body {
font-family: 'Inter', sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center
}
.header {
text-align: center;
padding: 1.5rem 1rem 1rem;
position: relative;
width: 100%
}
.header::after {
content: '';
position: absolute;
bottom: 0;
left: 10%;
width: 80%;
height: 1px;
background: linear-gradient(90deg, transparent, var(--purple), var(--blue), transparent)
}
.header h1 {
font-size: 1.6rem;
font-weight: 700;
background: linear-gradient(135deg, var(--purple), var(--blue));
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent
}
.header .sub {
font-family: 'JetBrains Mono', monospace;
font-size: .7rem;
color: var(--purple);
margin-top: .2rem;
letter-spacing: .05em
}
.status-bar {
display: flex;
justify-content: center;
gap: 1.5rem;
padding: .8rem;
font-family: 'JetBrains Mono', monospace;
font-size: .7rem
}
.status-item {
display: flex;
align-items: center;
gap: .4rem
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%
}
.status-dot.on {
background: var(--green);
box-shadow: 0 0 8px var(--green)
}
.status-dot.off {
background: var(--red)
}
.hint {
text-align: center;
font-size: .65rem;
color: var(--dim);
padding: .3rem;
font-style: italic
}
.hint em {
color: var(--orange);
font-style: normal
}
.content {
display: flex;
gap: 1.5rem;
padding: 1rem 1.5rem;
max-width: 900px;
width: 100%;
align-items: flex-start
}
.panel {
background: var(--card);
border: 1px solid var(--border);
border-radius: 12px;
overflow: hidden;
flex: 1;
box-shadow: 0 0 20px rgba(139, 92, 246, 0.2)
}
.panel-label {
display: flex;
align-items: center;
gap: .5rem;
padding: .6rem 1rem;
font-size: .7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: .08em;
border-bottom: 1px solid var(--border);
color: var(--purple)
}
.panel-label .dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--purple);
animation: pulse 2s infinite
}
@keyframes pulse {
0%,
100% {
opacity: 1
}
50% {
opacity: .3
}
}
canvas {
display: block;
width: 100%;
image-rendering: auto;
background: #08080f;
cursor: crosshair
}
.stats {
display: flex;
gap: 1rem;
padding: .5rem 1rem;
font-family: 'JetBrains Mono', monospace;
font-size: .55rem;
color: var(--dim);
flex-wrap: wrap;
border-top: 1px solid var(--border)
}
.stat {
display: flex;
gap: .3rem
}
.stat-v {
color: var(--green)
}
.stat-v.rtt {
color: var(--orange)
}
.stat-v.compress {
color: var(--cyan)
}
.side-panel {
display: flex;
flex-direction: column;
gap: .75rem;
min-width: 200px;
max-width: 220px;
padding-top: 1rem
}
.info-card {
background: var(--card);
border: 1px solid var(--border);
border-radius: 8px;
padding: .5rem .75rem;
font-family: 'JetBrains Mono', monospace;
font-size: .55rem
}
.info-card .title {
color: var(--cyan);
font-weight: 600;
text-transform: uppercase;
letter-spacing: .1em;
margin-bottom: .3rem;
font-size: .6rem
}
.info-card .val {
color: var(--orange);
word-break: break-all;
line-height: 1.4
}
.info-card.rtt-card {
border-color: var(--orange)
}
.info-card.rtt-card .title {
color: var(--orange)
}
.info-card.compress-card {
border-color: var(--cyan)
}
.info-card.compress-card .title {
color: var(--cyan)
}
.rtt-big {
font-size: 1.4rem;
font-weight: 700;
color: var(--orange);
text-align: center;
padding: .3rem 0
}
.compress-big {
font-size: 1.4rem;
font-weight: 700;
color: var(--cyan);
text-align: center;
padding: .3rem 0
}
.rtt-label {
font-size: .5rem;
color: var(--dim);
text-align: center
}
.waiting {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 360px;
gap: 1rem;
color: var(--dim)
}
.waiting .spinner {
width: 40px;
height: 40px;
border: 3px solid var(--border);
border-top-color: var(--purple);
border-radius: 50%;
animation: spin 1s linear infinite
}
@keyframes spin {
to {
transform: rotate(360deg)
}
}
</style>
</head>
<body>
<div class="header">
<h1>DreamStack — Receiver</h1>
<div class="sub">ds-stream delta receiver · interactive</div>
</div>
<div class="status-bar">
<div class="status-item"><span class="status-dot off" id="wsDot"></span><span id="wsStatus"
style="color:var(--red)">Disconnected</span></div>
<div class="status-item"><span style="color:var(--dim)">Relay:</span> <span
style="color:var(--orange)">ws://localhost:9800/stream</span></div>
</div>
<div class="hint">👆 <em>Click on the stream</em> to interact — forces sent via relay, round-trip latency measured
</div>
<div class="content">
<div class="panel">
<div class="panel-label"><span class="dot"></span> Pixel Stream · Click to Interact</div>
<div id="waitingMsg" class="waiting">
<div class="spinner"></div>
<div>Waiting for source stream...</div>
</div>
<canvas id="receiver" width="480" height="360" style="display:none"></canvas>
<div class="stats">
<div class="stat">Frames <span class="stat-v" id="frames">0</span></div>
<div class="stat">Bytes <span class="stat-v" id="bytes">0B</span></div>
<div class="stat">FPS <span class="stat-v" id="fps">0</span></div>
<div class="stat">Decode <span class="stat-v" id="latency"></span></div>
<div class="stat">RTT <span class="stat-v rtt" id="rttStat"></span></div>
<div class="stat">Saved <span class="stat-v compress" id="savedStat"></span></div>
</div>
</div>
<div class="side-panel">
<div class="info-card rtt-card">
<div class="title">⏱ Round-Trip Latency</div>
<div class="rtt-big" id="rttBig"></div>
<div class="rtt-label">click → source → render → back</div>
<div class="val" style="margin-top:.3rem;font-size:.5rem" id="rttHistory">Click to measure</div>
</div>
<div class="info-card compress-card">
<div class="title">📦 Compression</div>
<div class="compress-big" id="compressBig"></div>
<div class="rtt-label">delta + RLE vs raw RGBA</div>
<div class="val" style="margin-top:.3rem;font-size:.5rem" id="compressDetail"></div>
</div>
<div class="info-card">
<div class="title">Frame Type</div>
<div class="val" id="frameType"></div>
</div>
<div class="info-card">
<div class="title">Interactions</div>
<div class="val" id="clickCount">0 clicks sent</div>
</div>
</div>
</div>
<script>
const HEADER_SIZE = 16;
const FRAME_KEYFRAME = 0x11;
const FRAME_DELTA = 0x12;
const SIGNAL_CLICK = 0x30;
const SIGNAL_PONG = 0x31;
const canvas = document.getElementById('receiver');
const ctx = canvas.getContext('2d');
let ws = null;
let totalFrames = 0, totalBytes = 0, clicksSent = 0;
let fpsCnt = 0, displayFps = 0, lastFpsTime = performance.now();
const rttHistory = [];
// Frame buffer for delta decoding
let frameBuffer = null;
const RAW_SIZE = 480 * 360 * 4; // 691200 bytes
function decodeHeader(buffer) {
const frame = new Uint8Array(buffer);
const view = new DataView(buffer);
return {
type: frame[0],
seq: view.getUint16(2, true),
width: view.getUint16(8, true),
height: view.getUint16(10, true),
payloadLength: view.getUint32(12, true),
payload: new Uint8Array(buffer, HEADER_SIZE),
};
}
// Decode RLE delta and apply XOR to frame buffer
function applyDeltaRLE(payload, buffer, totalBytes) {
let pi = 0; // payload index
let bi = 0; // buffer index
while (pi < payload.length && bi < totalBytes) {
const runLen = (payload[pi] << 8) | payload[pi + 1];
const dr = payload[pi + 2];
const dg = payload[pi + 3];
const db = payload[pi + 4];
const da = payload[pi + 5];
pi += 6;
for (let r = 0; r < runLen && bi < totalBytes; r++) {
buffer[bi] ^= dr;
buffer[bi + 1] ^= dg;
buffer[bi + 2] ^= db;
buffer[bi + 3] ^= da;
bi += 4;
}
}
}
function fmtB(b) { if (b < 1024) return b + 'B'; if (b < 1048576) return (b / 1024).toFixed(1) + 'KB'; return (b / 1048576).toFixed(1) + 'MB'; }
function renderBuffer() {
const imgData = ctx.createImageData(480, 360);
imgData.data.set(frameBuffer);
ctx.putImageData(imgData, 0, 0);
}
// ─── Click → Signal ───
canvas.addEventListener('click', (e) => {
if (!ws || ws.readyState !== 1) return;
const rect = canvas.getBoundingClientRect();
const clickX = (e.clientX - rect.left) * (480 / rect.width);
const clickY = (e.clientY - rect.top) * (360 / rect.height);
const signal = new ArrayBuffer(20);
const sv = new DataView(signal);
new Uint8Array(signal)[0] = SIGNAL_CLICK;
sv.setFloat32(4, clickX, true);
sv.setFloat32(8, clickY, true);
sv.setFloat64(12, performance.now(), true);
ws.send(signal);
clicksSent++;
document.getElementById('clickCount').textContent = `${clicksSent} clicks sent`;
});
// ─── WebSocket ───
function connectWS() {
ws = new WebSocket('ws://localhost:9800/stream');
ws.binaryType = 'arraybuffer';
ws.onopen = () => {
document.getElementById('wsDot').className = 'status-dot on';
document.getElementById('wsStatus').textContent = 'Connected';
document.getElementById('wsStatus').style.color = 'var(--green)';
};
ws.onmessage = (evt) => {
const data = new Uint8Array(evt.data);
// Handle pong (RTT response)
if (data[0] === SIGNAL_PONG && evt.data.byteLength >= 20) {
const view = new DataView(evt.data);
const originalTs = view.getFloat64(12, true);
const rtt = performance.now() - originalTs;
rttHistory.push(rtt);
if (rttHistory.length > 20) rttHistory.shift();
const avg = rttHistory.reduce((a, b) => a + b, 0) / rttHistory.length;
document.getElementById('rttBig').textContent = rtt.toFixed(1) + 'ms';
document.getElementById('rttStat').textContent = rtt.toFixed(1) + 'ms';
document.getElementById('rttHistory').textContent =
`avg: ${avg.toFixed(1)}ms · min: ${Math.min(...rttHistory).toFixed(1)}ms · max: ${Math.max(...rttHistory).toFixed(1)}ms (n=${rttHistory.length})`;
return;
}
// Handle pixel frames
if (data[0] !== FRAME_KEYFRAME && data[0] !== FRAME_DELTA) return;
const t0 = performance.now();
const hdr = decodeHeader(evt.data);
const isKey = hdr.type === FRAME_KEYFRAME;
if (isKey) {
// Keyframe: replace entire buffer
frameBuffer = new Uint8Array(RAW_SIZE);
frameBuffer.set(hdr.payload.subarray(0, RAW_SIZE));
} else {
// Delta: apply RLE-decoded XOR to existing buffer
if (!frameBuffer) return; // can't apply delta without a keyframe first
applyDeltaRLE(hdr.payload, frameBuffer, RAW_SIZE);
}
document.getElementById('waitingMsg').style.display = 'none';
canvas.style.display = 'block';
renderBuffer();
totalFrames++;
totalBytes += evt.data.byteLength;
fpsCnt++;
const now = performance.now();
if (now - lastFpsTime >= 1000) {
displayFps = fpsCnt;
fpsCnt = 0;
lastFpsTime = now;
}
const decodeMs = (now - t0).toFixed(1);
const wireSize = evt.data.byteLength;
const saved = ((1 - wireSize / (RAW_SIZE + HEADER_SIZE)) * 100).toFixed(0);
document.getElementById('frames').textContent = totalFrames;
document.getElementById('bytes').textContent = fmtB(totalBytes);
document.getElementById('fps').textContent = displayFps;
document.getElementById('latency').textContent = decodeMs + 'ms';
document.getElementById('savedStat').textContent = saved + '%';
document.getElementById('frameType').textContent = isKey
? `KEY (0x11) · ${fmtB(wireSize)} raw`
: `Δ (0x12) · ${fmtB(wireSize)} compressed`;
document.getElementById('compressBig').textContent = isKey ? '0%' : saved + '%';
document.getElementById('compressDetail').textContent =
`${fmtB(wireSize)} wire vs ${fmtB(RAW_SIZE)} raw`;
};
ws.onclose = () => {
document.getElementById('wsDot').className = 'status-dot off';
document.getElementById('wsStatus').textContent = 'Disconnected';
document.getElementById('wsStatus').style.color = 'var(--red)';
setTimeout(connectWS, 2000);
};
ws.onerror = () => ws.close();
}
connectWS();
</script>
</body>
</html>

69
engine/demo/relay.js Normal file
View file

@ -0,0 +1,69 @@
// DreamStack ds-stream Pixel Relay Server (bidirectional)
// Source → Receiver: pixel frames
// Receiver → Source: interaction signals (clicks, drags)
const { WebSocketServer } = require('ws');
const http = require('http');
const PORT = 9800;
const sources = new Set();
const receivers = new Set();
let frameCount = 0;
let byteCount = 0;
let signalCount = 0;
const server = http.createServer((req, res) => {
res.writeHead(200, {
'Content-Type': 'text/plain',
'Access-Control-Allow-Origin': '*',
});
res.end(`ds-stream relay | sources: ${sources.size} | receivers: ${receivers.size} | frames: ${frameCount} | signals: ${signalCount} | bytes: ${byteCount}`);
});
const wss = new WebSocketServer({ server });
wss.on('connection', (ws, req) => {
const path = req.url || '/';
if (path.startsWith('/source')) {
sources.add(ws);
console.log(`[relay] source connected (${sources.size} total)`);
ws.on('message', (data) => {
frameCount++;
byteCount += data.length;
for (const recv of receivers) {
if (recv.readyState === 1) recv.send(data);
}
});
ws.on('close', () => {
sources.delete(ws);
console.log(`[relay] source disconnected (${sources.size} remaining)`);
});
} else {
receivers.add(ws);
console.log(`[relay] receiver connected (${receivers.size} total)`);
ws.on('message', (data) => {
// Forward interaction signals from receiver → source
signalCount++;
for (const src of sources) {
if (src.readyState === 1) src.send(data);
}
});
ws.on('close', () => {
receivers.delete(ws);
console.log(`[relay] receiver disconnected (${receivers.size} remaining)`);
});
}
});
server.listen(PORT, () => {
console.log(`\n ╔═══════════════════════════════════════╗`);
console.log(` ║ ds-stream relay on :${PORT}`);
console.log(` ║ Source: ws://localhost:${PORT}/source ║`);
console.log(` ║ Receiver: ws://localhost:${PORT}/stream ║`);
console.log(` ║ Mode: bidirectional (pixels + signals) ║`);
console.log(` ╚═══════════════════════════════════════╝\n`);
});