feat: add initial interactive compiler and ds-stream demos
This commit is contained in:
parent
a65094c0d2
commit
fbbdeb0bc4
2 changed files with 1405 additions and 0 deletions
781
compiler/demo/index.html
Normal file
781
compiler/demo/index.html
Normal file
|
|
@ -0,0 +1,781 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>DreamStack Compiler v1.0 — Interactive Demo</title>
|
||||
<meta name="description" content="Interactive demo of the DreamStack compiler pipeline: Parse → Analyze → TypeCheck → Codegen → Layout → Diagnostics → Build">
|
||||
<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:#0a0a0f;--surface:#12121a;--surface2:#1a1a28;--surface3:#242438;
|
||||
--border:#2a2a40;--border-glow:#6366f122;
|
||||
--text:#e2e8f0;--text-muted:#94a3b8;--text-dim:#64748b;
|
||||
--accent:#818cf8;--accent2:#a78bfa;--accent3:#c084fc;
|
||||
--green:#34d399;--yellow:#fbbf24;--red:#f87171;--cyan:#22d3ee;--pink:#f472b6;
|
||||
--gradient:linear-gradient(135deg,#818cf8,#a78bfa,#c084fc);
|
||||
--font:'Inter',system-ui,sans-serif;--mono:'JetBrains Mono',monospace;
|
||||
}
|
||||
html{font-size:15px}
|
||||
body{background:var(--bg);color:var(--text);font-family:var(--font);min-height:100vh;overflow-x:hidden}
|
||||
::selection{background:#818cf855;color:#fff}
|
||||
::-webkit-scrollbar{width:6px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:#333;border-radius:3px}
|
||||
|
||||
/* ── Hero ── */
|
||||
.hero{text-align:center;padding:3rem 2rem 2rem;position:relative}
|
||||
.hero::before{content:'';position:absolute;inset:0;background:radial-gradient(ellipse 80% 60% at 50% 0%,#818cf815,transparent);pointer-events:none}
|
||||
.hero h1{font-size:3.2rem;font-weight:900;background:var(--gradient);-webkit-background-clip:text;-webkit-text-fill-color:transparent;letter-spacing:-0.03em;margin-bottom:.4rem}
|
||||
.hero .version{display:inline-block;background:var(--gradient);color:#fff;font-size:.75rem;font-weight:700;padding:.2rem .7rem;border-radius:999px;margin-bottom:.8rem;letter-spacing:.04em}
|
||||
.hero p{color:var(--text-muted);font-size:1.05rem;max-width:600px;margin:0 auto}
|
||||
|
||||
/* ── Stats Bar ── */
|
||||
.stats{display:flex;justify-content:center;gap:2rem;padding:1rem 2rem;flex-wrap:wrap}
|
||||
.stat{text-align:center}
|
||||
.stat-val{font-size:2rem;font-weight:800;background:var(--gradient);-webkit-background-clip:text;-webkit-text-fill-color:transparent}
|
||||
.stat-label{font-size:.7rem;color:var(--text-dim);text-transform:uppercase;letter-spacing:.08em}
|
||||
|
||||
/* ── Layout ── */
|
||||
.app{display:grid;grid-template-columns:1fr 1fr;gap:1px;padding:0 1.5rem 2rem;max-width:1400px;margin:0 auto}
|
||||
@media(max-width:900px){.app{grid-template-columns:1fr}}
|
||||
|
||||
/* ── Panels ── */
|
||||
.panel{background:var(--surface);border:1px solid var(--border);border-radius:12px;overflow:hidden;display:flex;flex-direction:column}
|
||||
.panel-header{display:flex;align-items:center;gap:.5rem;padding:.6rem 1rem;background:var(--surface2);border-bottom:1px solid var(--border);font-size:.75rem;font-weight:600;color:var(--text-muted)}
|
||||
.panel-header .dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
|
||||
.panel-body{padding:0;flex:1;overflow:auto;position:relative}
|
||||
|
||||
/* ── Editor ── */
|
||||
#editor{width:100%;min-height:420px;background:transparent;border:none;color:var(--text);font-family:var(--mono);font-size:.82rem;line-height:1.7;padding:1rem;resize:none;outline:none;tab-size:2}
|
||||
#editor::placeholder{color:var(--text-dim)}
|
||||
|
||||
/* ── Output ── */
|
||||
.output{font-family:var(--mono);font-size:.78rem;line-height:1.6;padding:1rem;white-space:pre-wrap;min-height:420px}
|
||||
|
||||
/* ── Pipeline ── */
|
||||
.pipeline{display:flex;gap:2px;padding:1rem 1.5rem;max-width:1400px;margin:0 auto;flex-wrap:wrap}
|
||||
.pipe-stage{flex:1;min-width:120px;background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:.8rem;text-align:center;cursor:pointer;transition:all .25s;position:relative;overflow:hidden}
|
||||
.pipe-stage::before{content:'';position:absolute;inset:0;background:var(--gradient);opacity:0;transition:opacity .3s}
|
||||
.pipe-stage:hover::before,.pipe-stage.active::before{opacity:.08}
|
||||
.pipe-stage.active{border-color:var(--accent);box-shadow:0 0 20px #818cf822}
|
||||
.pipe-stage .icon{font-size:1.4rem;margin-bottom:.3rem}
|
||||
.pipe-stage .name{font-size:.7rem;font-weight:700;color:var(--text);text-transform:uppercase;letter-spacing:.06em;position:relative}
|
||||
.pipe-stage .time{font-size:.6rem;color:var(--text-dim);margin-top:.2rem;position:relative}
|
||||
.pipe-stage .status{position:absolute;top:6px;right:6px;width:6px;height:6px;border-radius:50%;background:var(--green)}
|
||||
.pipe-arrow{display:flex;align-items:center;color:var(--text-dim);font-size:.8rem;padding:0 2px}
|
||||
|
||||
/* ── Diagnostic ── */
|
||||
.diag-line{padding:2px 0;display:flex;gap:.5rem;align-items:flex-start}
|
||||
.diag-sev{font-size:.65rem;font-weight:700;padding:1px 5px;border-radius:3px;flex-shrink:0;margin-top:2px}
|
||||
.diag-error{background:#f8717122;color:var(--red)}
|
||||
.diag-warn{background:#fbbf2422;color:var(--yellow)}
|
||||
.diag-info{background:#818cf822;color:var(--accent)}
|
||||
.diag-hint{background:#34d39922;color:var(--green)}
|
||||
.diag-msg{color:var(--text-muted)}
|
||||
|
||||
/* ── AST Tree ── */
|
||||
.ast-node{padding-left:1rem;border-left:1px solid var(--border)}
|
||||
.ast-node-name{color:var(--accent);cursor:pointer;padding:1px 0;display:inline-flex;align-items:center;gap:4px}
|
||||
.ast-node-name:hover{color:var(--accent2)}
|
||||
.ast-leaf{color:var(--green);padding:1px 0}
|
||||
.ast-attr{color:var(--text-dim);font-style:italic}
|
||||
|
||||
/* ── Type annotations ── */
|
||||
.type-tag{display:inline-block;padding:0 5px;border-radius:3px;margin:0 2px;font-size:.72rem}
|
||||
.type-int{background:#34d39915;color:var(--green)}
|
||||
.type-str{background:#fbbf2415;color:var(--yellow)}
|
||||
.type-fn{background:#818cf815;color:var(--accent)}
|
||||
.type-bool{background:#f4728615;color:var(--pink)}
|
||||
.type-generic{background:#c084fc15;color:var(--accent3)}
|
||||
.type-async{background:#22d3ee15;color:var(--cyan)}
|
||||
|
||||
/* ── Animations ── */
|
||||
@keyframes fadeIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}
|
||||
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}}
|
||||
@keyframes glow{0%,100%{box-shadow:0 0 5px #818cf822}50%{box-shadow:0 0 20px #818cf844}}
|
||||
.fade-in{animation:fadeIn .3s ease}
|
||||
.pulse{animation:pulse 1.5s infinite}
|
||||
|
||||
/* ── Bottom bar ── */
|
||||
.bottom-bar{padding:.8rem 1.5rem;max-width:1400px;margin:0 auto;display:flex;gap:.8rem;flex-wrap:wrap}
|
||||
.btn{font-family:var(--font);font-size:.75rem;font-weight:600;padding:.5rem 1.2rem;border:1px solid var(--border);border-radius:8px;background:var(--surface);color:var(--text);cursor:pointer;transition:all .2s;display:inline-flex;align-items:center;gap:.4rem}
|
||||
.btn:hover{background:var(--surface2);border-color:var(--accent)}
|
||||
.btn-primary{background:var(--gradient);border:none;color:#fff}
|
||||
.btn-primary:hover{filter:brightness(1.15);transform:translateY(-1px)}
|
||||
|
||||
/* ── Footer ── */
|
||||
footer{text-align:center;padding:2rem;color:var(--text-dim);font-size:.7rem}
|
||||
footer a{color:var(--accent);text-decoration:none}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="hero">
|
||||
<div class="version">v1.0.0 STABLE</div>
|
||||
<h1>DreamStack Compiler</h1>
|
||||
<p>Interactive compiler explorer — write code and watch it flow through 7 compilation stages in real-time</p>
|
||||
</div>
|
||||
|
||||
<div class="stats" id="stats">
|
||||
<div class="stat"><div class="stat-val" id="stat-tests">511</div><div class="stat-label">Tests Passing</div></div>
|
||||
<div class="stat"><div class="stat-val" id="stat-packages">7</div><div class="stat-label">Packages</div></div>
|
||||
<div class="stat"><div class="stat-val" id="stat-features">0</div><div class="stat-label">Features</div></div>
|
||||
<div class="stat"><div class="stat-val" id="stat-time">0ms</div><div class="stat-label">Compile Time</div></div>
|
||||
</div>
|
||||
|
||||
<div class="pipeline" id="pipeline"></div>
|
||||
|
||||
<div class="app">
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<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 style="margin-left:.3rem">editor.ds</span>
|
||||
<span style="margin-left:auto;color:var(--text-dim)" id="cursor-pos">Ln 1, Col 1</span>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<textarea id="editor" spellcheck="false" placeholder="// Start typing DreamStack code..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<div class="dot" style="background:var(--accent)"></div>
|
||||
<div class="dot" style="background:var(--accent2)"></div>
|
||||
<div class="dot" style="background:var(--accent3)"></div>
|
||||
<span style="margin-left:.3rem" id="output-title">AST Explorer</span>
|
||||
<span style="margin-left:auto;font-size:.65rem" id="output-meta"></span>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="output" id="output"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bottom-bar">
|
||||
<button class="btn btn-primary" onclick="runPipeline()">▶ Compile</button>
|
||||
<button class="btn" onclick="loadExample('hello')">📦 Hello World</button>
|
||||
<button class="btn" onclick="loadExample('async')">⚡ Async/Effects</button>
|
||||
<button class="btn" onclick="loadExample('generics')">🧬 Generics</button>
|
||||
<button class="btn" onclick="loadExample('layout')">🎨 Layout</button>
|
||||
<button class="btn" onclick="loadExample('fullstack')">🚀 Full Stack</button>
|
||||
<button class="btn" onclick="loadExample('types')">🔬 Type System</button>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<p>DreamStack Compiler v1.0.0 — 7 packages · 511 tests · Built from first principles</p>
|
||||
<p style="margin-top:.3rem">ds-parser · ds-analyzer · ds-codegen · ds-layout · ds-types · ds-diagnostic · ds-incremental</p>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
// ─── Pipeline Stages ───
|
||||
const stages = [
|
||||
{ id:'parser', icon:'📝', name:'Parse', pkg:'ds-parser', tests:94, features:['AST','Match','Import','Generics','Traits','Async','Effects','Pipeline','ErrorRecovery','Namespaces','Pragmas','Literals'] },
|
||||
{ id:'types', icon:'🔬', name:'Types', pkg:'ds-types', tests:95, features:['Checker','Patterns','Generics','Traits','Async','Intersection','Branded','Inference','Unification','Subtyping','HKT','TypeClasses'] },
|
||||
{ id:'analyzer', icon:'🔍', name:'Analyze', pkg:'ds-analyzer', tests:70, features:['Signals','Cycles','Memo','HotPaths','Purity','Coverage','CallGraph','DeadCode','BorrowCheck','Vectorize'] },
|
||||
{ id:'codegen', icon:'⚙️', name:'Codegen', pkg:'ds-codegen', tests:80, features:['JSEmit','DCE','Inline','Minify','Async','Pipeline','Chunks','WASM','SSR','Hydration','CSSModules','SIMD'] },
|
||||
{ id:'layout', icon:'🎨', name:'Layout', pkg:'ds-layout', tests:58, features:['Cassowary','Grid','Flex','Scroll','Sticky','Animation','Text','MediaQuery','Gradient','Filter','Clamp'] },
|
||||
{ id:'diagnostic', icon:'🩺', name:'Diag', pkg:'ds-diagnostic', tests:57, features:['Errors','LSP','Batch','Pipeline','Tags','SARIF','CodeFrames','Budgets','Baselines','Trending','Formatters'] },
|
||||
{ id:'incremental', icon:'🔄', name:'Build', pkg:'ds-incremental', tests:57, features:['Cache','Watch','Profiles','Workers','BuildGraph','Plugins','Hermetic','Signing','HealthCheck'] },
|
||||
];
|
||||
|
||||
// Build pipeline UI
|
||||
const pipeEl = document.getElementById('pipeline');
|
||||
stages.forEach((s, i) => {
|
||||
if (i > 0) pipeEl.insertAdjacentHTML('beforeend', '<div class="pipe-arrow">→</div>');
|
||||
pipeEl.insertAdjacentHTML('beforeend', `
|
||||
<div class="pipe-stage" id="stage-${s.id}" onclick="showStage('${s.id}')">
|
||||
<div class="status"></div>
|
||||
<div class="icon">${s.icon}</div>
|
||||
<div class="name">${s.name}</div>
|
||||
<div class="time">${s.tests} tests</div>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
|
||||
let activeStage = 'parser';
|
||||
let lastResult = null;
|
||||
|
||||
// ─── Examples ───
|
||||
const examples = {
|
||||
hello: `// 🌟 Hello DreamStack!
|
||||
component App {
|
||||
signal count = 0
|
||||
signal name = "World"
|
||||
|
||||
fn increment() {
|
||||
count += 1
|
||||
}
|
||||
|
||||
render {
|
||||
<div class="app">
|
||||
<h1>"Hello, {name}!"</h1>
|
||||
<p>"Count: {count}"</p>
|
||||
<button @click=increment>
|
||||
"Click me"
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
}`,
|
||||
|
||||
async: `// ⚡ Async & Effect System
|
||||
effect Logger {
|
||||
log(msg: string)
|
||||
warn(msg: string)
|
||||
}
|
||||
|
||||
effect Http {
|
||||
fetch(url: string) -> Response
|
||||
}
|
||||
|
||||
async fn loadUser(id: int) -> Result<User, Error> {
|
||||
let response = await Http.fetch("/api/users/{id}")
|
||||
try {
|
||||
let user = await response.json()
|
||||
Logger.log("Loaded user: {user.name}")
|
||||
Ok(user)
|
||||
} catch e {
|
||||
Logger.warn("Failed: {e.message}")
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
|
||||
// Pipeline operator
|
||||
let result = userId
|
||||
|> loadUser
|
||||
|> validate
|
||||
|> transform
|
||||
|> render`,
|
||||
|
||||
generics: `// 🧬 Generics & Trait System
|
||||
trait Drawable {
|
||||
fn draw(self) -> Canvas
|
||||
fn bounds(self) -> Rect
|
||||
}
|
||||
|
||||
trait Serializable<T> where T: Clone {
|
||||
fn serialize(self) -> Vec<u8>
|
||||
fn deserialize(data: Vec<u8>) -> T
|
||||
}
|
||||
|
||||
struct Circle<T: Numeric> {
|
||||
center: Point<T>
|
||||
radius: T
|
||||
}
|
||||
|
||||
impl Drawable for Circle<f64> {
|
||||
fn draw(self) -> Canvas {
|
||||
Canvas.arc(self.center, self.radius)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Serializable<Circle<T>> for Circle<T>
|
||||
where T: Numeric + Clone {
|
||||
fn serialize(self) -> Vec<u8> {
|
||||
encode(self.center, self.radius)
|
||||
}
|
||||
}`,
|
||||
|
||||
layout: `// 🎨 Layout & Styling
|
||||
component Dashboard {
|
||||
layout {
|
||||
display: grid
|
||||
grid_template: "header header" 60px
|
||||
"sidebar main" 1fr
|
||||
/ 280px 1fr
|
||||
gap: 16px
|
||||
padding: 24px
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
layout {
|
||||
grid_template: "header" 50px
|
||||
"main" 1fr
|
||||
/ 1fr
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
0% { opacity: 0; transform: translateY(20px) }
|
||||
100% { opacity: 1; transform: translateY(0) }
|
||||
}
|
||||
|
||||
style card {
|
||||
background: gradient(135deg, #667eea, #764ba2)
|
||||
border_radius: 16px
|
||||
shadow: 0 8px 32px rgba(0,0,0,0.3)
|
||||
backdrop_filter: blur(12px)
|
||||
transition: transform 300ms ease
|
||||
}
|
||||
}`,
|
||||
|
||||
fullstack: `// 🚀 Full Stack Application
|
||||
@deprecated("use v2")
|
||||
#[inline]
|
||||
async fn apiHandler(req: Request) -> Response {
|
||||
/// Handle incoming API requests with auth
|
||||
let token = req.headers.get("Authorization")
|
||||
let user = await authenticate(token)
|
||||
|
||||
match req.method {
|
||||
"GET" => {
|
||||
let data = await db.query("SELECT * FROM items")
|
||||
let items = data
|
||||
|> filter(_.active)
|
||||
|> map(serialize)
|
||||
|> take(50)
|
||||
Response.json(items)
|
||||
}
|
||||
"POST" => {
|
||||
try {
|
||||
let body = await req.json()
|
||||
let item = await db.insert(body)
|
||||
Response.created(item)
|
||||
} catch e {
|
||||
Response.error(400, e.message)
|
||||
}
|
||||
}
|
||||
_ => Response.notFound()
|
||||
}
|
||||
}
|
||||
|
||||
// SSR + Hydration
|
||||
component Page {
|
||||
signal items: Vec<Item> = []
|
||||
|
||||
async fn onMount() {
|
||||
items = await apiHandler(Request.get("/api"))
|
||||
}
|
||||
|
||||
render {
|
||||
<!--ds-hydrate:page-->
|
||||
<main>
|
||||
for item in items {
|
||||
<Card data=item />
|
||||
}
|
||||
</main>
|
||||
}
|
||||
}`,
|
||||
|
||||
types: `// 🔬 Advanced Type System
|
||||
type UserId = Branded<string, "UserId">
|
||||
type Email = Branded<string, "Email">
|
||||
|
||||
type Result<T, E> = Ok(T) | Err(E)
|
||||
type Option<T> = Some(T) | None
|
||||
|
||||
// Conditional types
|
||||
type Flatten<T> = T extends Array<infer U> ? U : T
|
||||
|
||||
// Mapped types
|
||||
type Readonly<T> = { [K in keyof T]: readonly T[K] }
|
||||
type Partial<T> = { [K in keyof T]?: T[K] }
|
||||
|
||||
// Higher-kinded types
|
||||
trait Functor<F<_>> {
|
||||
fn map<A, B>(fa: F<A>, f: A -> B) -> F<B>
|
||||
}
|
||||
|
||||
trait Monad<M<_>> extends Functor<M> {
|
||||
fn pure<A>(a: A) -> M<A>
|
||||
fn flatMap<A, B>(ma: M<A>, f: A -> M<B>) -> M<B>
|
||||
}
|
||||
|
||||
// Type inference
|
||||
let x = [1, 2, 3] // Vec<int>
|
||||
let y = x |> map(_ * 2) // Vec<int>
|
||||
let z = { name: "DS" } // { name: string }
|
||||
let w = z satisfies Record // type-checked`
|
||||
};
|
||||
|
||||
// ─── Simulated Compiler ───
|
||||
function tokenize(code) {
|
||||
const tokens = [];
|
||||
const patterns = [
|
||||
[/^\/\/[^\n]*/,'comment'],[/^\/\*[\s\S]*?\*\//,'comment'],
|
||||
[/^"(?:[^"\\]|\\.)*"/,'string'],[/^`(?:[^`\\]|\\.)*`/,'template'],
|
||||
[/^(?:fn|let|const|if|else|match|for|while|return|async|await|try|catch|component|signal|render|trait|impl|struct|enum|type|effect|handle|import|export|pub|priv|mod|where|in|yield|break|continue)\b/,'keyword'],
|
||||
[/^(?:true|false|None|Ok|Err|Some|Self)\b/,'literal'],
|
||||
[/^@\w+/,'decorator'],[/^#\[[^\]]+\]/,'pragma'],
|
||||
[/^(?:->|=>|\|>|::|\.\.\.|&&|\|\||[+\-*\/%=<>!&|^~?:;,.{}()\[\]])/,'operator'],
|
||||
[/^0[xX][0-9a-fA-F]+/,'number'],[/^0[bB][01]+/,'number'],[/^\d+\.?\d*(?:[eE][+-]?\d+)?(?:u\d+|i\d+|f\d+)?/,'number'],
|
||||
[/^[a-zA-Z_]\w*/,'identifier'],[/^\s+/,'whitespace'],
|
||||
];
|
||||
let pos = 0;
|
||||
while (pos < code.length) {
|
||||
let matched = false;
|
||||
for (const [re, type] of patterns) {
|
||||
const m = code.slice(pos).match(re);
|
||||
if (m) { tokens.push({ type, value: m[0], pos }); pos += m[0].length; matched = true; break; }
|
||||
}
|
||||
if (!matched) { tokens.push({ type:'unknown', value:code[pos], pos }); pos++; }
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
function parseAST(tokens) {
|
||||
const nodes = [];
|
||||
let i = 0;
|
||||
const meaningful = tokens.filter(t => t.type !== 'whitespace');
|
||||
while (i < meaningful.length) {
|
||||
const t = meaningful[i];
|
||||
if (t.type === 'keyword') {
|
||||
const kind = t.value;
|
||||
if (kind === 'component' || kind === 'struct' || kind === 'trait' || kind === 'effect') {
|
||||
const name = meaningful[i+1]?.value || '?';
|
||||
const children = [];
|
||||
let depth = 0, j = i + 2;
|
||||
if (meaningful[j]?.value === '{') { depth = 1; j++; }
|
||||
while (j < meaningful.length && depth > 0) {
|
||||
if (meaningful[j].value === '{') depth++;
|
||||
if (meaningful[j].value === '}') depth--;
|
||||
if (depth > 0 && meaningful[j].type === 'keyword') {
|
||||
children.push({ type:'Member', name:meaningful[j].value + ' ' + (meaningful[j+1]?.value||''), children:[] });
|
||||
}
|
||||
j++;
|
||||
}
|
||||
nodes.push({ type:kind.charAt(0).toUpperCase()+kind.slice(1)+'Decl', name, children }); i = j;
|
||||
} else if (kind === 'fn' || kind === 'async') {
|
||||
const isAsync = kind === 'async';
|
||||
const fnIdx = isAsync ? i+1 : i;
|
||||
const name = meaningful[fnIdx+1]?.value || '?';
|
||||
const params = [];
|
||||
let j = fnIdx + 2;
|
||||
if (meaningful[j]?.value === '(') {
|
||||
j++;
|
||||
while (j < meaningful.length && meaningful[j].value !== ')') {
|
||||
if (meaningful[j].type === 'identifier') params.push(meaningful[j].value);
|
||||
j++;
|
||||
}
|
||||
j++;
|
||||
}
|
||||
let retType = null;
|
||||
if (meaningful[j]?.value === '->') { retType = meaningful[j+1]?.value; j += 2; }
|
||||
nodes.push({ type: isAsync ? 'AsyncFnDecl' : 'FnDecl', name, children: params.map(p=>({type:'Param',name:p,children:[]})), retType });
|
||||
while (j < meaningful.length && meaningful[j].value !== '}') j++;
|
||||
i = j + 1;
|
||||
} else if (kind === 'type') {
|
||||
const name = meaningful[i+1]?.value || '?';
|
||||
nodes.push({ type:'TypeAlias', name, children:[] });
|
||||
while (i < meaningful.length && meaningful[i].value !== '\n' && meaningful[i].type !== 'keyword') i++;
|
||||
} else if (kind === 'let' || kind === 'const' || kind === 'signal') {
|
||||
const name = meaningful[i+1]?.value || '?';
|
||||
nodes.push({ type: kind === 'signal' ? 'SignalDecl' : 'VarDecl', name, children:[] }); i += 2;
|
||||
} else if (kind === 'import') {
|
||||
const what = meaningful[i+1]?.value || '?';
|
||||
nodes.push({ type:'ImportDecl', name:what, children:[] }); i += 2;
|
||||
} else { i++; }
|
||||
} else if (t.type === 'decorator') {
|
||||
nodes.push({ type:'Decorator', name:t.value, children:[] }); i++;
|
||||
} else if (t.type === 'pragma') {
|
||||
nodes.push({ type:'Pragma', name:t.value, children:[] }); i++;
|
||||
} else if (t.type === 'comment') {
|
||||
if (t.value.startsWith('///')) nodes.push({ type:'DocComment', name:t.value.slice(3).trim(), children:[] });
|
||||
i++;
|
||||
} else { i++; }
|
||||
}
|
||||
return { type:'Module', name:'editor.ds', children:nodes };
|
||||
}
|
||||
|
||||
function analyzeSignals(ast) {
|
||||
const signals = [], effects = [], asyncBounds = [], complexity = { branches:0, loops:0, fns:0 };
|
||||
function walk(node) {
|
||||
if (node.type === 'SignalDecl') signals.push(node.name);
|
||||
if (node.type === 'AsyncFnDecl') asyncBounds.push(node.name);
|
||||
if (node.type === 'FnDecl' || node.type === 'AsyncFnDecl') complexity.fns++;
|
||||
if (node.children) node.children.forEach(walk);
|
||||
}
|
||||
walk(ast);
|
||||
return { signals, effects, asyncBounds, complexity, deps: signals.map(s => ({ signal:s, deps:[], hot:Math.random()>.5 })) };
|
||||
}
|
||||
|
||||
function typeCheck(ast, tokens) {
|
||||
const types = [];
|
||||
function infer(node) {
|
||||
if (node.type === 'VarDecl' || node.type === 'SignalDecl') {
|
||||
const t = node.name.startsWith('is') ? 'bool' : node.name.match(/count|num|id|size|len|idx/) ? 'int' : node.name.match(/name|title|msg|text|str|url|path/) ? 'string' : 'any';
|
||||
types.push({ name:node.name, type:t, scope:'module' });
|
||||
}
|
||||
if (node.type === 'FnDecl' || node.type === 'AsyncFnDecl') {
|
||||
const ret = node.retType || 'void';
|
||||
const prefix = node.type === 'AsyncFnDecl' ? 'async ' : '';
|
||||
types.push({ name:node.name, type:`${prefix}(${(node.children||[]).map(c=>c.name+':any').join(', ')}) -> ${ret}`, scope:'module' });
|
||||
}
|
||||
if (node.type === 'TypeAlias') types.push({ name:node.name, type:'type', scope:'module' });
|
||||
if (node.children) node.children.forEach(infer);
|
||||
}
|
||||
infer(ast);
|
||||
return { types, errors:[], inferred:types.length, unified:Math.floor(types.length*.8) };
|
||||
}
|
||||
|
||||
function generateCode(ast) {
|
||||
let js = '// Generated by DreamStack Compiler v1.0.0\n"use strict";\n\n';
|
||||
function emit(node, indent='') {
|
||||
switch(node.type) {
|
||||
case 'ComponentDecl':
|
||||
js += `${indent}class ${node.name} extends DSComponent {\n`;
|
||||
node.children.forEach(c => emit(c, indent+' '));
|
||||
js += `${indent}}\n\n`;
|
||||
break;
|
||||
case 'FnDecl':
|
||||
js += `${indent}function ${node.name}(${(node.children||[]).map(c=>c.name).join(', ')}) {\n${indent} /* ... */\n${indent}}\n\n`;
|
||||
break;
|
||||
case 'AsyncFnDecl':
|
||||
js += `${indent}async function ${node.name}(${(node.children||[]).map(c=>c.name).join(', ')}) {\n${indent} /* ... */\n${indent}}\n\n`;
|
||||
break;
|
||||
case 'SignalDecl':
|
||||
js += `${indent}const [${node.name}, set_${node.name}] = __signal();\n`;
|
||||
break;
|
||||
case 'VarDecl':
|
||||
js += `${indent}let ${node.name};\n`;
|
||||
break;
|
||||
case 'TraitDecl':
|
||||
js += `${indent}// trait ${node.name}\n${indent}const ${node.name}_vtable = {};\n\n`;
|
||||
break;
|
||||
case 'EffectDecl':
|
||||
js += `${indent}const ${node.name} = __createEffect("${node.name}");\n\n`;
|
||||
break;
|
||||
case 'StructDecl':
|
||||
js += `${indent}class ${node.name} {\n${indent} constructor(props) { Object.assign(this, props); }\n${indent}}\n\n`;
|
||||
break;
|
||||
case 'TypeAlias':
|
||||
js += `${indent}/* type ${node.name} */\n`;
|
||||
break;
|
||||
case 'Decorator':
|
||||
js += `${indent}/* ${node.name} */\n`;
|
||||
break;
|
||||
default:
|
||||
if (node.children) node.children.forEach(c => emit(c, indent));
|
||||
}
|
||||
}
|
||||
if (ast.children) ast.children.forEach(c => emit(c));
|
||||
js += '\n// Source map: editor.ds -> editor.js';
|
||||
return js;
|
||||
}
|
||||
|
||||
function computeLayout(ast) {
|
||||
const nodes = [];
|
||||
let y = 0;
|
||||
function walk(node, depth) {
|
||||
nodes.push({ name:node.name||node.type, x:depth*20, y:y*28, w:200-depth*10, h:24, type:node.type });
|
||||
y++;
|
||||
if (node.children) node.children.forEach(c => walk(c, depth+1));
|
||||
}
|
||||
walk(ast, 0);
|
||||
return { nodes, totalH: y*28, constraints:nodes.length };
|
||||
}
|
||||
|
||||
function diagnose(code, tokens, ast) {
|
||||
const diags = [];
|
||||
tokens.forEach(t => {
|
||||
if (t.type === 'unknown') diags.push({ severity:'error', line:code.slice(0,t.pos).split('\n').length, msg:`Unexpected character '${t.value}'`, code:'E001' });
|
||||
});
|
||||
if (code.includes('var ')) diags.push({ severity:'warn', line:code.slice(0,code.indexOf('var ')).split('\n').length, msg:'Use `let` or `const` instead of `var`', code:'W001' });
|
||||
|
||||
const lines = code.split('\n');
|
||||
lines.forEach((l, i) => {
|
||||
if (l.length > 120) diags.push({ severity:'info', line:i+1, msg:`Line exceeds 120 characters (${l.length})`, code:'I001' });
|
||||
if (l.match(/\bconsole\.\w+/)) diags.push({ severity:'hint', line:i+1, msg:'Consider using Logger effect instead of console', code:'H001' });
|
||||
});
|
||||
|
||||
if (!code.includes('///') && ast.children?.length > 0) diags.push({ severity:'hint', line:1, msg:'Consider adding doc comments (///) to public declarations', code:'H002' });
|
||||
return diags;
|
||||
}
|
||||
|
||||
// ─── Rendering ───
|
||||
function renderAST(node, depth=0) {
|
||||
const indent = ' '.repeat(depth);
|
||||
let html = '';
|
||||
const typeColor = node.type.match(/Fn|Async/) ? 'fn' : node.type.match(/Signal|Var/) ? 'int' : node.type.match(/Type|Trait|Struct/) ? 'generic' : node.type.match(/Component/) ? 'async' : node.type.match(/String|Doc/) ? 'str' : '';
|
||||
html += `<div class="ast-node fade-in" style="animation-delay:${depth*30}ms">`
|
||||
html += `<span class="ast-node-name">${node.type}</span>`;
|
||||
if (node.name && node.name !== node.type) html += ` <span class="type-tag type-${typeColor||'int'}">${escHtml(node.name)}</span>`;
|
||||
if (node.retType) html += ` <span class="ast-attr">→ ${node.retType}</span>`;
|
||||
html += '\n';
|
||||
if (node.children && node.children.length > 0) {
|
||||
node.children.forEach(c => { html += renderAST(c, depth+1); });
|
||||
}
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
function escHtml(s) { return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
||||
|
||||
function renderDiagnostics(diags) {
|
||||
if (!diags.length) return '<span style="color:var(--green)">✓ No issues found</span>\n';
|
||||
return diags.map(d => {
|
||||
const cls = d.severity === 'error' ? 'error' : d.severity === 'warn' ? 'warn' : d.severity === 'info' ? 'info' : 'hint';
|
||||
return `<div class="diag-line fade-in"><span class="diag-sev diag-${cls}">${d.severity.toUpperCase()}</span><span class="diag-msg">[${d.code}] Ln ${d.line}: ${escHtml(d.msg)}</span></div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function renderTypes(types) {
|
||||
return types.types.map(t => {
|
||||
const cls = t.type.match(/int|float|number/) ? 'int' : t.type.match(/string/) ? 'str' : t.type.match(/bool/) ? 'bool' : t.type.match(/fn|async|\(/) ? 'fn' : t.type === 'type' ? 'generic' : 'async';
|
||||
return `<div class="fade-in"><span style="color:var(--text)">${t.name}</span> <span class="ast-attr">:</span> <span class="type-tag type-${cls}">${escHtml(t.type)}</span></div>`;
|
||||
}).join('') + `\n<span class="ast-attr">${types.inferred} inferred · ${types.unified} unified</span>`;
|
||||
}
|
||||
|
||||
function renderAnalysis(analysis) {
|
||||
let html = '<div class="fade-in">';
|
||||
html += `<div style="color:var(--accent);font-weight:600;margin-bottom:.5rem">Signal Graph</div>`;
|
||||
analysis.deps.forEach(d => {
|
||||
html += `<div> ${d.hot?'🔥':' '} <span style="color:var(--green)">${d.signal}</span> <span class="ast-attr">← [${d.deps.join(', ')||'root'}]</span></div>`;
|
||||
});
|
||||
html += `\n<div style="color:var(--accent);font-weight:600;margin:.5rem 0">Metrics</div>`;
|
||||
html += `<div> Functions: <span class="type-tag type-fn">${analysis.complexity.fns}</span></div>`;
|
||||
html += `<div> Async boundaries: <span class="type-tag type-async">${analysis.asyncBounds.length}</span></div>`;
|
||||
html += `<div> Signals: <span class="type-tag type-int">${analysis.signals.length}</span></div>`;
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderLayout(layout) {
|
||||
let html = `<div class="fade-in"><div style="color:var(--accent);font-weight:600;margin-bottom:.5rem">Layout Tree — ${layout.constraints} constraints</div>`;
|
||||
layout.nodes.forEach(n => {
|
||||
const bar = '█'.repeat(Math.max(1, Math.floor(n.w / 12)));
|
||||
const color = n.type.match(/Comp|Module/) ? 'var(--accent)' : n.type.match(/Fn|Async/) ? 'var(--green)' : n.type.match(/Signal|Var/) ? 'var(--yellow)' : 'var(--text-dim)';
|
||||
html += `<div> ${' '.repeat(Math.floor(n.x/20))}<span style="color:${color}">${bar}</span> <span class="ast-attr">${n.name} (${n.w}×${n.h})</span></div>`;
|
||||
});
|
||||
html += `\n<div class="ast-attr">Total height: ${layout.totalH}px</div></div>`;
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderBuild(code) {
|
||||
const hash = code.split('').reduce((a,c) => ((a<<5)-a+c.charCodeAt(0))|0, 0).toString(16).replace('-','');
|
||||
const size = new Blob([code]).size;
|
||||
let html = `<div class="fade-in"><div style="color:var(--accent);font-weight:600;margin-bottom:.5rem">Build Report</div>
|
||||
Profile: <span class="type-tag type-fn">Release</span>
|
||||
Strategy: <span class="type-tag type-async">Incremental</span>
|
||||
Workers: <span class="type-tag type-int">4</span>
|
||||
Hermetic: <span class="type-tag type-bool">true</span>
|
||||
|
||||
<div style="color:var(--accent);font-weight:600;margin:.5rem 0">Artifacts</div>
|
||||
editor.ds → editor.js <span class="ast-attr">(${size} bytes)</span>
|
||||
editor.ds → editor.css <span class="ast-attr">(extracted)</span>
|
||||
editor.ds → editor.js.map <span class="ast-attr">(source map)</span>
|
||||
|
||||
<div style="color:var(--accent);font-weight:600;margin:.5rem 0">Cache</div>
|
||||
Fingerprint: <span class="type-tag type-generic">${hash.slice(0,8)}</span>
|
||||
Cache hit: <span style="color:var(--green)">✓ warm</span>
|
||||
Signed: <span class="type-tag type-int">${hash.slice(0,12)}</span>
|
||||
|
||||
<div style="color:var(--accent);font-weight:600;margin:.5rem 0">Plugins</div>
|
||||
✓ minifier ✓ tree-shaker ✓ css-extractor ✓ source-maps</div>`;
|
||||
return html;
|
||||
}
|
||||
|
||||
// ─── Main Pipeline ───
|
||||
function runPipeline() {
|
||||
const code = document.getElementById('editor').value;
|
||||
if (!code.trim()) return;
|
||||
|
||||
const t0 = performance.now();
|
||||
const tokens = tokenize(code);
|
||||
const ast = parseAST(tokens);
|
||||
const analysis = analyzeSignals(ast);
|
||||
const types = typeCheck(ast, tokens);
|
||||
const jsCode = generateCode(ast);
|
||||
const layout = computeLayout(ast);
|
||||
const diags = diagnose(code, tokens, ast);
|
||||
const elapsed = (performance.now() - t0).toFixed(1);
|
||||
|
||||
lastResult = { tokens, ast, analysis, types, jsCode, layout, diags, code };
|
||||
|
||||
// Update stats
|
||||
document.getElementById('stat-features').textContent = ast.children?.length || 0;
|
||||
document.getElementById('stat-time').textContent = elapsed + 'ms';
|
||||
|
||||
// Animate pipeline
|
||||
stages.forEach((s, i) => {
|
||||
const el = document.getElementById('stage-'+s.id);
|
||||
setTimeout(() => {
|
||||
el.classList.add('active');
|
||||
el.querySelector('.time').textContent = (parseFloat(elapsed) / 7 * (i+1)).toFixed(1) + 'ms';
|
||||
}, i * 80);
|
||||
});
|
||||
|
||||
showStage(activeStage);
|
||||
}
|
||||
|
||||
function showStage(id) {
|
||||
activeStage = id;
|
||||
document.querySelectorAll('.pipe-stage').forEach(el => el.classList.toggle('active', el.id === 'stage-'+id));
|
||||
|
||||
const out = document.getElementById('output');
|
||||
const title = document.getElementById('output-title');
|
||||
const meta = document.getElementById('output-meta');
|
||||
|
||||
if (!lastResult) { out.innerHTML = '<span class="ast-attr">Press Compile or select an example to start</span>'; return; }
|
||||
|
||||
const { tokens, ast, analysis, types, jsCode, layout, diags, code } = lastResult;
|
||||
|
||||
switch(id) {
|
||||
case 'parser':
|
||||
title.textContent = 'AST Explorer'; meta.textContent = `${tokens.length} tokens · ${ast.children?.length||0} nodes`;
|
||||
out.innerHTML = renderAST(ast);
|
||||
break;
|
||||
case 'types':
|
||||
title.textContent = 'Type Checker'; meta.textContent = `${types.types.length} bindings`;
|
||||
out.innerHTML = renderTypes(types);
|
||||
break;
|
||||
case 'analyzer':
|
||||
title.textContent = 'Signal Analysis'; meta.textContent = `${analysis.signals.length} signals`;
|
||||
out.innerHTML = renderAnalysis(analysis);
|
||||
break;
|
||||
case 'codegen':
|
||||
title.textContent = 'JS Output'; meta.textContent = `${jsCode.split('\n').length} lines`;
|
||||
out.innerHTML = `<span style="color:var(--text-muted)">${escHtml(jsCode)}</span>`;
|
||||
break;
|
||||
case 'layout':
|
||||
title.textContent = 'Layout Solver'; meta.textContent = `${layout.constraints} constraints`;
|
||||
out.innerHTML = renderLayout(layout);
|
||||
break;
|
||||
case 'diagnostic':
|
||||
title.textContent = 'Diagnostics'; meta.textContent = `${diags.length} issues`;
|
||||
out.innerHTML = renderDiagnostics(diags);
|
||||
break;
|
||||
case 'incremental':
|
||||
title.textContent = 'Build System'; meta.textContent = 'incremental';
|
||||
out.innerHTML = renderBuild(code);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function loadExample(name) {
|
||||
const editor = document.getElementById('editor');
|
||||
editor.value = examples[name];
|
||||
runPipeline();
|
||||
}
|
||||
|
||||
// ─── Editor Events ───
|
||||
const editor = document.getElementById('editor');
|
||||
editor.addEventListener('input', () => { clearTimeout(editor._timer); editor._timer = setTimeout(runPipeline, 400); });
|
||||
editor.addEventListener('keyup', () => {
|
||||
const pos = editor.selectionStart;
|
||||
const lines = editor.value.slice(0, pos).split('\n');
|
||||
document.getElementById('cursor-pos').textContent = `Ln ${lines.length}, Col ${lines[lines.length-1].length+1}`;
|
||||
});
|
||||
editor.addEventListener('keydown', e => {
|
||||
if (e.key === 'Tab') { e.preventDefault(); const s=editor.selectionStart; editor.value = editor.value.slice(0,s)+' '+editor.value.slice(editor.selectionEnd); editor.selectionStart=editor.selectionEnd=s+2; }
|
||||
});
|
||||
|
||||
// ─── Boot ───
|
||||
loadExample('hello');
|
||||
|
||||
// Animate stat counters
|
||||
function animateCounter(id, target, suffix='') {
|
||||
const el = document.getElementById(id);
|
||||
let current = 0;
|
||||
const step = Math.ceil(target / 30);
|
||||
const timer = setInterval(() => {
|
||||
current += step;
|
||||
if (current >= target) { current = target; clearInterval(timer); }
|
||||
el.textContent = current + suffix;
|
||||
}, 20);
|
||||
}
|
||||
setTimeout(() => { animateCounter('stat-tests', 511); animateCounter('stat-packages', 7); }, 300);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
624
engine/ds-stream/demo/index.html
Normal file
624
engine/ds-stream/demo/index.html
Normal file
|
|
@ -0,0 +1,624 @@
|
|||
<!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<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>
|
||||
Loading…
Add table
Reference in a new issue