dreamstack/engine/ds-stream/demo/index.html

624 lines
28 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DreamStack Stream — Interactive Receiver</title>
<meta name="description" content="Live interactive demo of the DreamStack Universal Bitstream Protocol — pixel streaming, delta compression, signal sync, and bidirectional input">
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&family=Inter:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet">
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root{
--bg:#050510;--surface:#0c0c1a;--surface2:#141428;--surface3:#1e1e36;
--border:#252545;--glow:#818cf822;
--text:#e2e8f0;--muted:#94a3b8;--dim:#64748b;
--accent:#818cf8;--accent2:#a78bfa;--green:#34d399;--yellow:#fbbf24;
--red:#f87171;--cyan:#22d3ee;--pink:#f472b6;--orange:#fb923c;
--gradient:linear-gradient(135deg,#818cf8,#6366f1);
--font:'Inter',system-ui,sans-serif;--mono:'JetBrains Mono',monospace;
}
html{font-size:14px}
body{background:var(--bg);color:var(--text);font-family:var(--font);min-height:100vh;overflow-x:hidden}
::selection{background:#818cf855}
::-webkit-scrollbar{width:5px}::-webkit-scrollbar-thumb{background:#333;border-radius:3px}
.hero{text-align:center;padding:2rem 2rem 1rem;position:relative}
.hero::before{content:'';position:absolute;inset:0;background:radial-gradient(ellipse 70% 50% at 50% 0%,#6366f112,transparent);pointer-events:none}
.hero h1{font-size:2.4rem;font-weight:900;background:linear-gradient(135deg,#22d3ee,#818cf8,#a78bfa);-webkit-background-clip:text;-webkit-text-fill-color:transparent;letter-spacing:-0.03em}
.badge{display:inline-block;background:linear-gradient(135deg,#22d3ee,#6366f1);color:#fff;font-size:.65rem;font-weight:700;padding:.15rem .6rem;border-radius:999px;margin-bottom:.5rem;letter-spacing:.05em}
.hero p{color:var(--muted);font-size:.9rem;max-width:620px;margin:0 auto}
/* Grid */
.grid{display:grid;grid-template-columns:1fr 340px;gap:1px;padding:1rem;max-width:1400px;margin:0 auto}
@media(max-width:900px){.grid{grid-template-columns:1fr}}
/* Panels */
.panel{background:var(--surface);border:1px solid var(--border);border-radius:10px;overflow:hidden;display:flex;flex-direction:column}
.panel-h{display:flex;align-items:center;gap:.4rem;padding:.5rem .8rem;background:var(--surface2);border-bottom:1px solid var(--border);font-size:.7rem;font-weight:600;color:var(--muted)}
.panel-h .dot{width:7px;height:7px;border-radius:50%}
.panel-b{flex:1;overflow:auto;position:relative}
/* Canvas area */
.canvas-wrap{position:relative;background:#000;aspect-ratio:16/10;cursor:crosshair}
.canvas-wrap canvas{display:block;width:100%;height:100%;image-rendering:pixelated}
.canvas-overlay{position:absolute;top:8px;left:8px;display:flex;gap:6px;pointer-events:none}
.canvas-badge{background:#00000099;backdrop-filter:blur(8px);border:1px solid #ffffff15;border-radius:6px;padding:3px 8px;font-family:var(--mono);font-size:.6rem;color:#fff}
.canvas-badge.live{border-color:var(--red);color:var(--red)}
.mouse-pos{position:absolute;bottom:8px;right:8px;background:#00000099;backdrop-filter:blur(8px);border-radius:6px;padding:3px 8px;font-family:var(--mono);font-size:.6rem;color:var(--muted)}
/* Stats bar */
.stats-bar{display:grid;grid-template-columns:repeat(6,1fr);gap:1px;padding:.5rem .8rem;background:var(--surface2);border-top:1px solid var(--border)}
.stat-box{text-align:center}
.stat-v{font-size:.95rem;font-weight:800;font-family:var(--mono)}
.stat-l{font-size:.55rem;color:var(--dim);text-transform:uppercase;letter-spacing:.06em}
/* Sidebar */
.sidebar{display:flex;flex-direction:column;gap:1px}
/* Protocol inspector */
.proto-scroll{max-height:260px;overflow-y:auto;font-family:var(--mono);font-size:.68rem;padding:.5rem}
.proto-msg{display:flex;gap:6px;padding:3px 0;border-bottom:1px solid #ffffff06;align-items:center;animation:fadeSlide .2s ease}
.proto-dir{font-size:.55rem;font-weight:700;padding:1px 4px;border-radius:3px;flex-shrink:0}
.proto-out{background:#818cf822;color:var(--accent)}
.proto-in{background:#34d39922;color:var(--green)}
.proto-type{color:var(--cyan);min-width:85px}
.proto-seq{color:var(--dim)}
.proto-size{color:var(--muted);margin-left:auto}
@keyframes fadeSlide{from{opacity:0;transform:translateX(-6px)}to{opacity:1;transform:translateX(0)}}
/* Hex viewer */
.hex-view{font-family:var(--mono);font-size:.62rem;padding:.5rem;color:var(--muted);line-height:1.5;max-height:140px;overflow-y:auto}
.hex-offset{color:var(--dim)}
.hex-byte{color:var(--accent)}
.hex-header{color:var(--cyan)}
.hex-payload{color:var(--green)}
/* Controls */
.controls{padding:.6rem .8rem;display:flex;flex-wrap:wrap;gap:.4rem}
.btn{font-family:var(--font);font-size:.68rem;font-weight:600;padding:.4rem .8rem;border:1px solid var(--border);border-radius:6px;background:var(--surface2);color:var(--text);cursor:pointer;transition:all .15s;display:inline-flex;align-items:center;gap:.3rem}
.btn:hover{background:var(--surface3);border-color:var(--accent)}
.btn.active{background:var(--accent);border-color:var(--accent);color:#fff}
.btn-sm{padding:.25rem .5rem;font-size:.6rem}
/* Quality indicator */
.quality{display:flex;gap:4px;padding:.5rem .8rem;background:var(--surface2);border-top:1px solid var(--border)}
.q-bar{flex:1;height:6px;border-radius:3px;background:var(--surface3)}
.q-fill{height:100%;border-radius:3px;transition:width .3s}
/* Header diagram */
.header-dia{padding:.6rem;font-family:var(--mono);font-size:.62rem;line-height:1.6;background:var(--surface2);border-top:1px solid var(--border)}
.hdr-field{display:inline-block;padding:1px 5px;border-radius:3px;margin:0 1px}
.hdr-type{background:#22d3ee18;color:var(--cyan)}
.hdr-flags{background:#fbbf2418;color:var(--yellow)}
.hdr-seq{background:#818cf818;color:var(--accent)}
.hdr-ts{background:#f4728618;color:var(--pink)}
.hdr-dim{background:#34d39918;color:var(--green)}
.hdr-len{background:#fb923c18;color:var(--orange)}
/* Compression chart */
.comp-chart{display:flex;align-items:flex-end;gap:2px;height:60px;padding:.5rem .8rem}
.comp-bar{flex:1;background:var(--accent);border-radius:2px 2px 0 0;min-height:2px;transition:height .2s;position:relative}
.comp-bar::after{content:attr(data-ratio);position:absolute;top:-14px;left:50%;transform:translateX(-50%);font-size:.5rem;color:var(--muted);white-space:nowrap}
/* Signal graph */
.signal-list{padding:.5rem .8rem;font-family:var(--mono);font-size:.68rem}
.sig-row{display:flex;justify-content:space-between;padding:2px 0;border-bottom:1px solid #ffffff06}
.sig-name{color:var(--cyan)}
.sig-val{color:var(--green)}
.sig-changed{animation:sigPulse .4s ease}
@keyframes sigPulse{0%{color:var(--yellow)}100%{color:var(--green)}}
footer{text-align:center;padding:1.5rem;color:var(--dim);font-size:.65rem}
</style>
</head>
<body>
<div class="hero">
<div class="badge">UNIVERSAL BITSTREAM PROTOCOL v12</div>
<h1>ds-stream Interactive Receiver</h1>
<p>Live pixel streaming with XOR+RLE delta compression, signal sync, adaptive quality, and bidirectional input — all in the browser</p>
</div>
<div class="grid">
<!-- Main: Canvas + Stats -->
<div style="display:flex;flex-direction:column;gap:1px">
<div class="panel">
<div class="panel-h">
<div class="dot" style="background:var(--red)"></div>
<div class="dot" style="background:var(--yellow)"></div>
<div class="dot" style="background:var(--green)"></div>
<span>receiver viewport</span>
<span style="margin-left:auto" id="resolution">320×200</span>
<span id="stream-status" style="color:var(--green)">● LIVE</span>
</div>
<div class="panel-b">
<div class="canvas-wrap" id="canvas-wrap">
<canvas id="display" width="320" height="200"></canvas>
<div class="canvas-overlay">
<div class="canvas-badge live" id="badge-live">● LIVE</div>
<div class="canvas-badge" id="badge-fps">0 FPS</div>
<div class="canvas-badge" id="badge-frame">K #0</div>
</div>
<div class="mouse-pos" id="mouse-pos">0, 0</div>
</div>
</div>
<div class="stats-bar">
<div class="stat-box"><div class="stat-v" id="s-fps" style="color:var(--green)">0</div><div class="stat-l">FPS</div></div>
<div class="stat-box"><div class="stat-v" id="s-frames" style="color:var(--cyan)">0</div><div class="stat-l">Frames</div></div>
<div class="stat-box"><div class="stat-v" id="s-bytes" style="color:var(--accent)">0</div><div class="stat-l">KB Sent</div></div>
<div class="stat-box"><div class="stat-v" id="s-ratio" style="color:var(--yellow)">0%</div><div class="stat-l">Savings</div></div>
<div class="stat-box"><div class="stat-v" id="s-rtt" style="color:var(--pink)">0ms</div><div class="stat-l">RTT</div></div>
<div class="stat-box"><div class="stat-v" id="s-quality" style="color:var(--green)">Full</div><div class="stat-l">Quality</div></div>
</div>
</div>
<!-- Controls -->
<div class="panel">
<div class="panel-h"><span>stream controls</span></div>
<div class="controls">
<button class="btn active" id="btn-play" onclick="toggleStream()">⏸ Pause</button>
<button class="btn" onclick="sendKeyframe()">🔑 Force Keyframe</button>
<button class="btn" onclick="cycleFPS()">🎯 <span id="fps-label">30 FPS</span></button>
<button class="btn" onclick="cycleScene()">🎬 <span id="scene-label">Waves</span></button>
<button class="btn" onclick="toggleDelta()">📐 <span id="delta-label">Delta: ON</span></button>
<button class="btn" onclick="toggleSignals()">📡 <span id="sig-label">Signals: ON</span></button>
<button class="btn btn-sm" onclick="simulateLatency()">🌐 +100ms RTT</button>
<button class="btn btn-sm" onclick="resetStats()">↻ Reset</button>
</div>
</div>
<!-- Compression chart -->
<div class="panel">
<div class="panel-h"><span>compression ratio per frame</span><span style="margin-left:auto" id="avg-ratio">avg: 0%</span></div>
<div class="comp-chart" id="comp-chart"></div>
</div>
</div>
<!-- Sidebar -->
<div class="sidebar">
<!-- Header Format -->
<div class="panel">
<div class="panel-h"><span>16-byte header format</span></div>
<div class="header-dia" id="header-dia">
<div style="margin-bottom:4px;color:var(--muted)">┌──────┬───────┬──────┬───────────┬───────┬────────┬────────┐</div>
<div><span class="hdr-field hdr-type">type</span><span class="hdr-field hdr-flags">flags</span><span class="hdr-field hdr-seq">seq</span><span class="hdr-field hdr-ts">timestamp</span><span class="hdr-field hdr-dim">width</span><span class="hdr-field hdr-dim">height</span><span class="hdr-field hdr-len">length</span></div>
<div style="margin-top:4px;color:var(--muted)"><span style="color:var(--cyan)">u8</span><span style="color:var(--yellow)">u8</span><span style="color:var(--accent)">u16</span><span style="color:var(--pink)">u32</span><span style="color:var(--green)">u16</span><span style="color:var(--green)">u16</span><span style="color:var(--orange)">u32</span></div>
<div style="margin-top:4px;color:var(--muted)">└──────┴───────┴──────┴───────────┴───────┴────────┴────────┘</div>
<div id="hdr-live" style="margin-top:6px;color:var(--text)"></div>
</div>
</div>
<!-- Protocol Inspector -->
<div class="panel" style="flex:1">
<div class="panel-h"><span>protocol inspector</span><span style="margin-left:auto" id="msg-count">0 msgs</span></div>
<div class="proto-scroll" id="proto-log"></div>
</div>
<!-- Hex View -->
<div class="panel">
<div class="panel-h"><span>last frame hex dump</span></div>
<div class="hex-view" id="hex-view">Waiting for frames...</div>
</div>
<!-- Signals -->
<div class="panel">
<div class="panel-h"><span>signal state</span><span style="margin-left:auto" id="sig-count">0 signals</span></div>
<div class="signal-list" id="signal-list"></div>
</div>
<!-- Quality -->
<div class="panel">
<div class="panel-h"><span>adaptive quality</span></div>
<div class="quality">
<div class="q-bar"><div class="q-fill" id="q-fill" style="width:100%;background:var(--green)"></div></div>
</div>
<div style="padding:.3rem .8rem;font-size:.6rem;color:var(--muted);display:flex;justify-content:space-between">
<span>Full (RTT&lt;50ms)</span><span>Reduced</span><span>Minimal</span>
</div>
</div>
</div>
</div>
<footer>DreamStack Universal Bitstream Protocol v12 — ds-stream · 16-byte headers · XOR+RLE delta · Adaptive quality · Stream mux · 24 frame types</footer>
<script>
// ─── Protocol Constants ───
const HEADER_SIZE = 16;
const FLAG_INPUT = 0x01, FLAG_KEYFRAME = 0x02, FLAG_COMPRESSED = 0x04;
const FRAME_TYPES = {
0x01:'Pixels', 0x02:'CompressedPx', 0x03:'DeltaPixels',
0x10:'AudioPCM', 0x11:'AudioOpus',
0x20:'Haptic', 0x21:'Actuator', 0x22:'LedMatrix',
0x30:'SignalSync', 0x31:'SignalDiff', 0x32:'SchemaAnnounce', 0x33:'SubscribeFilter',
0x40:'NeuralFrame', 0x41:'NeuralAudio', 0x42:'NeuralActuator', 0x43:'NeuralLatent',
0x50:'StateSync', 0x51:'Replay', 0x52:'Compressed',
0xF0:'Keyframe', 0x0F:'Auth', 0xFD:'Ack', 0xFE:'Ping', 0xFF:'End'
};
// ─── State ───
const W = 320, H = 200;
const canvas = document.getElementById('display');
const ctx = canvas.getContext('2d');
let running = true, useDelta = true, showSignals = true;
let seq = 0, frameCount = 0, totalBytes = 0, msgCount = 0;
let targetFPS = 30, currentScene = 0;
const scenes = ['Waves','Plasma','Rain','Matrix'];
let previousFrame = null, rttMs = 12;
let fpsCounter = 0, lastFpsTick = performance.now();
let compressionHistory = [];
let signals = { count: 0, time: 0, mouse_x: 0, mouse_y: 0, fps: 30, scene: 'waves', quality: 'full' };
// ─── Header encode/decode ───
function encodeHeader(type, flags, seq, ts, w, h, len) {
const buf = new ArrayBuffer(HEADER_SIZE);
const dv = new DataView(buf);
dv.setUint8(0, type); dv.setUint8(1, flags);
dv.setUint16(2, seq, true); dv.setUint32(4, ts, true);
dv.setUint16(8, w, true); dv.setUint16(10, h, true);
dv.setUint32(12, len, true);
return new Uint8Array(buf);
}
function decodeHeader(buf) {
const dv = new DataView(buf.buffer, buf.byteOffset);
return {
type: dv.getUint8(0), flags: dv.getUint8(1),
seq: dv.getUint16(2, true), timestamp: dv.getUint32(4, true),
width: dv.getUint16(8, true), height: dv.getUint16(10, true),
length: dv.getUint32(12, true)
};
}
// ─── XOR Delta ───
function computeDelta(current, previous) {
const delta = new Uint8Array(current.length);
for (let i = 0; i < current.length; i++) delta[i] = current[i] ^ previous[i];
return delta;
}
function applyDelta(previous, delta) {
const out = new Uint8Array(previous.length);
for (let i = 0; i < previous.length; i++) out[i] = previous[i] ^ delta[i];
return out;
}
// ─── RLE ───
function rleEncode(data) {
const out = [];
let i = 0;
while (i < data.length) {
if (data[i] === 0) {
let start = i;
while (i < data.length && data[i] === 0) i++;
let count = i - start;
while (count > 0) {
const chunk = Math.min(count, 65535);
out.push(0, chunk & 0xFF, (chunk >> 8) & 0xFF);
count -= chunk;
}
} else { out.push(data[i]); i++; }
}
return new Uint8Array(out);
}
function rleDecode(data) {
const out = [];
let i = 0;
while (i < data.length) {
if (data[i] === 0 && i + 2 < data.length) {
const count = data[i+1] | (data[i+2] << 8);
for (let j = 0; j < count; j++) out.push(0);
i += 3;
} else { out.push(data[i]); i++; }
}
return new Uint8Array(out);
}
// ─── Scene Generators ───
function generateWaves(t) {
const img = ctx.createImageData(W, H);
for (let y = 0; y < H; y++) for (let x = 0; x < W; x++) {
const i = (y * W + x) * 4;
const v1 = Math.sin(x*0.03 + t*2) * Math.cos(y*0.02 + t) * 0.5 + 0.5;
const v2 = Math.sin((x+y)*0.02 - t*1.5) * 0.5 + 0.5;
img.data[i] = (v1 * 100 + 30)|0;
img.data[i+1] = (v2 * 130 + 80)|0;
img.data[i+2] = (v1 * v2 * 255)|0;
img.data[i+3] = 255;
}
return img;
}
function generatePlasma(t) {
const img = ctx.createImageData(W, H);
for (let y = 0; y < H; y++) for (let x = 0; x < W; x++) {
const i = (y * W + x) * 4;
const v = Math.sin(x*0.05+t) + Math.sin(y*0.05+t*0.7) + Math.sin((x+y)*0.03+t*1.3) + Math.sin(Math.sqrt(x*x+y*y)*0.04);
const c = (v + 4) / 8;
img.data[i] = (Math.sin(c*Math.PI*2)*127+128)|0;
img.data[i+1] = (Math.sin(c*Math.PI*2+2.094)*127+128)|0;
img.data[i+2] = (Math.sin(c*Math.PI*2+4.189)*127+128)|0;
img.data[i+3] = 255;
}
return img;
}
let rainDrops = Array.from({length:60}, () => ({x:Math.random()*W, y:Math.random()*H, s:1+Math.random()*3, l:3+Math.random()*8}));
function generateRain(t) {
const img = ctx.createImageData(W, H);
// dark bg
for (let i = 0; i < img.data.length; i += 4) { img.data[i]=8; img.data[i+1]=10; img.data[i+2]=20; img.data[i+3]=255; }
rainDrops.forEach(d => {
d.y += d.s; if (d.y > H) { d.y = 0; d.x = Math.random()*W; }
for (let j = 0; j < d.l; j++) {
const py = (d.y - j)|0;
if (py >= 0 && py < H) {
const idx = (py * W + (d.x|0)) * 4;
const alpha = 1 - j/d.l;
img.data[idx] = (100 * alpha)|0;
img.data[idx+1] = (180 * alpha)|0;
img.data[idx+2] = (255 * alpha)|0;
}
}
});
return img;
}
let matrixCols = Array.from({length:W/6|0}, () => ({y:Math.random()*H, speed:2+Math.random()*5}));
function generateMatrix(t) {
const img = ctx.createImageData(W, H);
for (let i = 0; i < img.data.length; i += 4) { img.data[i]=0; img.data[i+1]=5; img.data[i+2]=0; img.data[i+3]=255; }
matrixCols.forEach((col, ci) => {
col.y += col.speed; if (col.y > H + 40) col.y = -20;
for (let j = 0; j < 15; j++) {
const py = (col.y - j*8)|0;
if (py >= 0 && py < H) {
const x = ci * 6;
const brightness = j === 0 ? 255 : Math.max(40, 200 - j*15);
for (let dx = 0; dx < 5 && x+dx < W; dx++) {
for (let dy = 0; dy < 7 && py+dy < H; dy++) {
const idx = ((py+dy)*W+(x+dx))*4;
if (Math.random() > 0.3) img.data[idx+1] = brightness;
}
}
}
}
});
return img;
}
const generators = [generateWaves, generatePlasma, generateRain, generateMatrix];
// ─── Protocol Message Logging ───
function logMessage(dir, type, seq, size, extra='') {
const log = document.getElementById('proto-log');
const name = FRAME_TYPES[type] || `0x${type.toString(16).padStart(2,'0')}`;
const dirClass = dir === 'OUT' ? 'proto-out' : 'proto-in';
const div = document.createElement('div');
div.className = 'proto-msg';
div.innerHTML = `<span class="proto-dir ${dirClass}">${dir}</span><span class="proto-type">${name}</span><span class="proto-seq">#${seq}</span>${extra?`<span style="color:var(--dim)">${extra}</span>`:''}<span class="proto-size">${size}B</span>`;
log.prepend(div);
while (log.children.length > 60) log.lastChild.remove();
msgCount++;
document.getElementById('msg-count').textContent = msgCount + ' msgs';
}
// ─── Hex Dump ───
function hexDump(header, payloadSize) {
const el = document.getElementById('hex-view');
const hdr = decodeHeader(header);
const bytes = Array.from(header).map(b => b.toString(16).padStart(2,'0'));
el.innerHTML =
`<span class="hex-offset">0000</span> <span class="hex-header">${bytes.slice(0,4).join(' ')}</span> <span class="hex-header">${bytes.slice(4,8).join(' ')}</span> <span class="hex-header">${bytes.slice(8,12).join(' ')}</span> <span class="hex-header">${bytes.slice(12,16).join(' ')}</span>\n` +
`<span style="color:var(--dim)"> type=<span class="hex-byte">0x${bytes[0]}</span> flags=<span class="hex-byte">0x${bytes[1]}</span> seq=<span class="hex-byte">${hdr.seq}</span> ts=<span class="hex-byte">${hdr.timestamp}</span></span>\n` +
`<span style="color:var(--dim)"> ${hdr.width}×${hdr.height} payload=<span class="hex-payload">${payloadSize} bytes</span></span>`;
// Live header display
document.getElementById('hdr-live').innerHTML = `Last: <span class="hdr-field hdr-type">0x${bytes[0]}</span> <span class="hdr-field hdr-flags">0x${bytes[1]}</span> <span class="hdr-field hdr-seq">${hdr.seq}</span> <span class="hdr-field hdr-ts">${hdr.timestamp}ms</span> <span class="hdr-field hdr-dim">${hdr.width}×${hdr.height}</span> <span class="hdr-field hdr-len">${payloadSize}B</span>`;
}
// ─── Update Signals ───
function updateSignals() {
if (!showSignals) return;
const el = document.getElementById('signal-list');
signals.time = (performance.now()/1000).toFixed(1);
signals.count = frameCount;
signals.fps = targetFPS;
signals.scene = scenes[currentScene].toLowerCase();
signals.quality = rttMs < 50 ? 'full' : rttMs < 150 ? 'reduced' : 'minimal';
el.innerHTML = Object.entries(signals).map(([k,v]) =>
`<div class="sig-row"><span class="sig-name">${k}</span><span class="sig-val">${v}</span></div>`
).join('');
document.getElementById('sig-count').textContent = Object.keys(signals).length + ' signals';
}
// ─── Compression Chart ───
function updateCompressionChart(ratio) {
compressionHistory.push(ratio);
if (compressionHistory.length > 40) compressionHistory.shift();
const el = document.getElementById('comp-chart');
el.innerHTML = compressionHistory.map(r => {
const h = Math.max(2, (1-r) * 60);
const c = r < 0.3 ? 'var(--green)' : r < 0.6 ? 'var(--yellow)' : 'var(--accent)';
return `<div class="comp-bar" style="height:${h}px;background:${c}" data-ratio="${((1-r)*100).toFixed(0)}%"></div>`;
}).join('');
const avg = compressionHistory.reduce((a,b)=>a+b,0) / compressionHistory.length;
document.getElementById('avg-ratio').textContent = `avg: ${((1-avg)*100).toFixed(0)}% saved`;
}
// ─── Quality Update ───
function updateQuality() {
const fill = document.getElementById('q-fill');
const el = document.getElementById('s-quality');
if (rttMs < 50) { fill.style.width = '100%'; fill.style.background = 'var(--green)'; el.textContent = 'Full'; el.style.color = 'var(--green)'; }
else if (rttMs < 150) { fill.style.width = '50%'; fill.style.background = 'var(--yellow)'; el.textContent = 'Med'; el.style.color = 'var(--yellow)'; }
else { fill.style.width = '15%'; fill.style.background = 'var(--red)'; el.textContent = 'Min'; el.style.color = 'var(--red)'; }
}
// ─── Main Loop ───
let t = 0;
function streamFrame() {
if (!running) return;
t += 0.016;
const timestamp = (performance.now())|0;
// Generate frame
const imageData = generators[currentScene](t);
const currentPixels = new Uint8Array(imageData.data.buffer);
let payloadBytes, frameType, flags, isKey = false;
if (!previousFrame || !useDelta || frameCount % 60 === 0) {
// Keyframe
payloadBytes = currentPixels;
frameType = 0x01; flags = FLAG_KEYFRAME;
isKey = true;
updateCompressionChart(1.0);
} else {
// Delta frame with RLE
const delta = computeDelta(currentPixels, previousFrame);
const compressed = rleEncode(delta);
const ratio = compressed.length / currentPixels.length;
payloadBytes = compressed;
frameType = 0x03; flags = FLAG_COMPRESSED;
updateCompressionChart(ratio);
}
// Encode header
const header = encodeHeader(frameType, flags, seq, timestamp, W, H, payloadBytes.length);
totalBytes += HEADER_SIZE + payloadBytes.length;
// Log
const extra = isKey ? 'KEY' : `${((1-payloadBytes.length/currentPixels.length)*100).toFixed(0)}% saved`;
logMessage('OUT', frameType, seq, HEADER_SIZE + payloadBytes.length, extra);
hexDump(header, payloadBytes.length);
// "Receive" — decode on receiver side
if (frameType === 0x03 && previousFrame) {
const decodedDelta = rleDecode(payloadBytes);
const reconstructed = applyDelta(previousFrame, decodedDelta);
const recImg = new ImageData(new Uint8ClampedArray(reconstructed.buffer), W, H);
ctx.putImageData(recImg, 0, 0);
previousFrame = reconstructed;
} else {
ctx.putImageData(imageData, 0, 0);
previousFrame = new Uint8Array(currentPixels);
}
// ACK back
if (frameCount % 5 === 0) {
const ackHeader = encodeHeader(0xFD, 0, seq, timestamp, 0, 0, 4);
logMessage('IN', 0xFD, seq, HEADER_SIZE + 4, `rtt=${rttMs}ms`);
}
// Signal sync
if (showSignals && frameCount % 30 === 0) {
const sigJson = JSON.stringify(signals);
logMessage('OUT', 0x30, seq, HEADER_SIZE + sigJson.length, `${Object.keys(signals).length} sigs`);
} else if (showSignals && frameCount % 10 === 0) {
logMessage('OUT', 0x31, seq, HEADER_SIZE + 20, 'diff');
}
// Ping
if (frameCount % 90 === 0) {
logMessage('OUT', 0xFE, seq, HEADER_SIZE, '♥');
}
seq = (seq + 1) & 0xFFFF;
frameCount++;
fpsCounter++;
// Update UI
document.getElementById('badge-frame').textContent = `${isKey?'K':'Δ'} #${frameCount}`;
document.getElementById('s-frames').textContent = frameCount;
document.getElementById('s-bytes').textContent = (totalBytes/1024).toFixed(0);
document.getElementById('s-rtt').textContent = rttMs + 'ms';
const savings = previousFrame && useDelta ? ((1-payloadBytes.length/(W*H*4))*100).toFixed(0) : '0';
document.getElementById('s-ratio').textContent = savings + '%';
updateSignals();
updateQuality();
}
// FPS counter
setInterval(() => {
const now = performance.now();
const fps = (fpsCounter / ((now - lastFpsTick) / 1000)).toFixed(0);
document.getElementById('s-fps').textContent = fps;
document.getElementById('badge-fps').textContent = fps + ' FPS';
fpsCounter = 0; lastFpsTick = now;
}, 1000);
// Stream loop
let streamTimer;
function startStream() {
streamTimer = setInterval(streamFrame, 1000 / targetFPS);
}
startStream();
// ─── Controls ───
function toggleStream() {
running = !running;
const btn = document.getElementById('btn-play');
btn.textContent = running ? '⏸ Pause' : '▶ Play';
btn.classList.toggle('active', running);
document.getElementById('badge-live').textContent = running ? '● LIVE' : '⏸ PAUSED';
document.getElementById('badge-live').style.color = running ? 'var(--red)' : 'var(--muted)';
document.getElementById('stream-status').textContent = running ? '● LIVE' : '⏸ PAUSED';
document.getElementById('stream-status').style.color = running ? 'var(--green)' : 'var(--muted)';
}
function sendKeyframe() { previousFrame = null; logMessage('IN', 0xF0, seq, HEADER_SIZE, 'forced'); }
function cycleFPS() {
const rates = [15, 30, 60];
const idx = (rates.indexOf(targetFPS) + 1) % rates.length;
targetFPS = rates[idx];
document.getElementById('fps-label').textContent = targetFPS + ' FPS';
clearInterval(streamTimer); startStream();
}
function cycleScene() {
currentScene = (currentScene + 1) % scenes.length;
document.getElementById('scene-label').textContent = scenes[currentScene];
previousFrame = null;
}
function toggleDelta() {
useDelta = !useDelta;
document.getElementById('delta-label').textContent = `Delta: ${useDelta?'ON':'OFF'}`;
}
function toggleSignals() {
showSignals = !showSignals;
document.getElementById('sig-label').textContent = `Signals: ${showSignals?'ON':'OFF'}`;
}
function simulateLatency() { rttMs = Math.min(300, rttMs + 50); updateQuality(); }
function resetStats() {
frameCount = 0; totalBytes = 0; seq = 0; msgCount = 0; rttMs = 12;
compressionHistory = []; previousFrame = null;
document.getElementById('proto-log').innerHTML = '';
document.getElementById('comp-chart').innerHTML = '';
}
// ─── Mouse tracking as input ───
const wrap = document.getElementById('canvas-wrap');
wrap.addEventListener('mousemove', e => {
const rect = wrap.getBoundingClientRect();
const x = ((e.clientX - rect.left) / rect.width * W)|0;
const y = ((e.clientY - rect.top) / rect.height * H)|0;
signals.mouse_x = x; signals.mouse_y = y;
document.getElementById('mouse-pos').textContent = `${x}, ${y}`;
if (running && frameCount % 3 === 0) logMessage('IN', 0x01, seq, HEADER_SIZE+5, `${x},${y}`);
});
wrap.addEventListener('click', e => {
const rect = wrap.getBoundingClientRect();
const x = ((e.clientX - rect.left) / rect.width * W)|0;
const y = ((e.clientY - rect.top) / rect.height * H)|0;
logMessage('IN', 0x02, seq, HEADER_SIZE+5, `click ${x},${y}`);
});
</script>
</body>
</html>