/// JavaScript emitter — generates executable JS from DreamStack AST + signal graph. use std::collections::HashSet; use ds_parser::*; use ds_analyzer::{SignalGraph, SignalKind, AnalyzedView, InitialValue}; pub struct JsEmitter { output: String, indent: usize, node_id_counter: usize, /// Non-signal local variables (e.g., for-in loop vars) local_vars: HashSet, } impl JsEmitter { pub fn new() -> Self { Self { output: String::new(), indent: 0, node_id_counter: 0, local_vars: HashSet::new(), } } /// Generate a complete HTML page with embedded runtime and compiled app. pub fn emit_html(program: &Program, graph: &SignalGraph, views: &[AnalyzedView]) -> String { let mut emitter = Self::new(); let app_js = emitter.emit_program(program, graph, views); format!( r#" DreamStack App
"# ) } /// Generate the application JS module. pub fn emit_program(&mut self, program: &Program, graph: &SignalGraph, views: &[AnalyzedView]) -> String { self.output.clear(); self.emit_line("// DreamStack compiled output"); self.emit_line("// Generated by dreamstack compiler v0.1.0"); self.emit_line(""); self.emit_line("(function() {"); self.indent += 1; // Phase 1: Create all signals self.emit_line("// ── Signals ──"); for node in &graph.nodes { match &node.kind { SignalKind::Source => { let init = match &node.initial_value { Some(InitialValue::Int(n)) => format!("{n}"), Some(InitialValue::Float(n)) => format!("{n}"), Some(InitialValue::Bool(b)) => format!("{b}"), Some(InitialValue::String(s)) => format!("\"{s}\""), None => { // Fall back to the let declaration expression // (handles List, Record, etc.) if let Some(expr) = self.find_let_expr(program, &node.name) { // Check if it's a spring() call — Spring is already signal-compatible if matches!(expr, Expr::Call(name, _) if name == "spring") { let js_expr = self.emit_expr(expr); self.emit_line(&format!("const {} = {};", node.name, js_expr)); continue; } self.emit_expr(expr) } else { "null".to_string() } } }; self.emit_line(&format!("const {} = DS.signal({});", node.name, init)); } SignalKind::Derived => { // Find the let declaration to get the expression let expr = self.find_let_expr(program, &node.name); if let Some(expr) = expr { let js_expr = self.emit_expr(expr); self.emit_line(&format!( "const {} = DS.derived(() => {});", node.name, js_expr )); } } SignalKind::Handler { .. } => {} // Handled later } } self.emit_line(""); // Phase 2a: Component functions self.emit_line("// ── Components ──"); for decl in &program.declarations { if let Declaration::Component(comp) = decl { let params = comp.props.iter() .map(|p| p.name.clone()) .collect::>() .join(", "); self.emit_line(&format!("function component_{}({}) {{", comp.name, params)); self.indent += 1; let child_var = self.emit_view_expr(&comp.body, graph); self.emit_line(&format!("return {};", child_var)); self.indent -= 1; self.emit_line("}"); self.emit_line(""); } } // Phase 2b: Build views self.emit_line("// ── Views ──"); for decl in &program.declarations { if let Declaration::View(view) = decl { self.emit_view(view, graph); } } // Phase 3: Event handlers self.emit_line(""); self.emit_line("// ── Handlers ──"); for decl in &program.declarations { if let Declaration::OnHandler(handler) = decl { self.emit_handler(handler); } } // Phase 4: Route view functions let routes: Vec<_> = program.declarations.iter() .filter_map(|d| if let Declaration::Route(r) = d { Some(r) } else { None }) .collect(); if !routes.is_empty() { self.emit_line(""); self.emit_line("// ── Routes ──"); for (i, route) in routes.iter().enumerate() { self.emit_line(&format!("function route_view_{}() {{", i)); self.indent += 1; let child = self.emit_view_expr(&route.body, graph); self.emit_line(&format!("return {};", child)); self.indent -= 1; self.emit_line("}"); } } // Phase 5: Mount to DOM self.emit_line(""); self.emit_line("// ── Mount ──"); if !routes.is_empty() { // Router-based mounting self.emit_line("const __root = document.getElementById('ds-root');"); self.emit_line("let __currentView = null;"); // Emit any non-route views as layout (e.g., nav bar) for view in views { self.emit_line(&format!("__root.appendChild(view_{}());", view.name)); } self.emit_line("const __routeContainer = document.createElement('div');"); self.emit_line("__routeContainer.className = 'ds-route-container';"); self.emit_line("__root.appendChild(__routeContainer);"); self.emit_line(""); self.emit_line("DS.effect(() => {"); self.indent += 1; self.emit_line("const path = DS.route.value;"); self.emit_line("__routeContainer.innerHTML = '';"); for (i, route) in routes.iter().enumerate() { let branch = if i == 0 { "if" } else { "} else if" }; self.emit_line(&format!( "{} (DS.matchRoute(\"{}\", path)) {{", branch, route.path )); self.indent += 1; self.emit_line(&format!("__routeContainer.appendChild(route_view_{}());", i)); self.indent -= 1; } self.emit_line("}"); self.indent -= 1; self.emit_line("});"); } else if let Some(view) = views.first() { self.emit_line(&format!( "document.getElementById('ds-root').appendChild(view_{}());", view.name )); } // Phase 6: Constraints let constraints: Vec<_> = program.declarations.iter() .filter_map(|d| if let Declaration::Constrain(c) = d { Some(c) } else { None }) .collect(); if !constraints.is_empty() { self.emit_line(""); self.emit_line("// ── Constraints ──"); for c in &constraints { let expr_js = self.emit_expr(&c.expr); // Use view element name or fall back to querySelector self.emit_line(&format!( "DS.constrain(document.querySelector('[data-ds=\"{}\"]:not(.x)') || document.getElementById('ds-root'), '{}', () => {});", c.element, c.prop, expr_js )); } } // Phase 7: Stream initialization let streams: Vec<_> = program.declarations.iter() .filter_map(|d| if let Declaration::Stream(s) = d { Some(s) } else { None }) .collect(); if !streams.is_empty() { self.emit_line(""); self.emit_line("// ── Bitstream Streaming ──"); for stream in &streams { let mode = match stream.mode { StreamMode::Pixel => "pixel", StreamMode::Delta => "delta", StreamMode::Signal => "signal", }; let url = self.emit_expr(&stream.relay_url); self.emit_line(&format!( "DS._initStream({}, '{}');", url, mode )); } } self.indent -= 1; self.emit_line("})();"); self.output.clone() } fn emit_view(&mut self, view: &ViewDecl, graph: &SignalGraph) { self.emit_line(&format!("function view_{}() {{", view.name)); self.indent += 1; let root_id = self.emit_view_expr(&view.body, graph); self.emit_line(&format!("return {};", root_id)); self.indent -= 1; self.emit_line("}"); } /// Emit a view expression and return the variable name of the created DOM node. fn emit_view_expr(&mut self, expr: &Expr, graph: &SignalGraph) -> String { match expr { Expr::Container(container) => { // ─── PHYSICS SCENE ─── if matches!(container.kind, ContainerKind::Scene) { return self.emit_scene(container, graph); } let node_var = self.next_node_id(); let tag = match &container.kind { ContainerKind::Column => "div", ContainerKind::Row => "div", ContainerKind::Stack => "div", ContainerKind::Panel => "div", ContainerKind::Form => "form", ContainerKind::List => "ul", ContainerKind::Scene => "canvas", // handled above ContainerKind::Custom(s) => s, }; let class = match &container.kind { ContainerKind::Column => "ds-column", ContainerKind::Row => "ds-row", ContainerKind::Stack => "ds-stack", ContainerKind::Panel => "ds-panel", _ => "", }; self.emit_line(&format!("const {} = document.createElement('{}');", node_var, tag)); if !class.is_empty() { self.emit_line(&format!("{}.className = '{}';", node_var, class)); } // Handle container props for (key, val) in &container.props { let js_val = self.emit_expr(val); if graph.name_to_id.contains_key(&js_val) || self.is_signal_ref(&js_val) { self.emit_line(&format!( "DS.effect(() => {{ {}.style.{} = {}.value + 'px'; }});", node_var, key, js_val )); } else { self.emit_line(&format!("{}.style.{} = {};", node_var, key, js_val)); } } // Emit children for child in &container.children { let child_var = self.emit_view_expr(child, graph); self.emit_line(&format!("{}.appendChild({});", node_var, child_var)); } node_var } Expr::Element(element) => { let node_var = self.next_node_id(); let html_tag = match element.tag.as_str() { "text" => "span", "button" => "button", "input" => "input", "image" | "avatar" => "img", "link" => "a", "label" => "label", "spinner" => "div", "skeleton" => "div", _ => "div", }; self.emit_line(&format!("const {} = document.createElement('{}');", node_var, html_tag)); self.emit_line(&format!("{}.className = 'ds-{}';", node_var, element.tag)); // Handle text content / arguments for arg in &element.args { match arg { Expr::StringLit(s) => { if let Some(StringSegment::Literal(text)) = s.segments.first() { self.emit_line(&format!( "{}.textContent = \"{}\";", node_var, text.replace('"', "\\\"") )); } } Expr::Ident(name) => { if self.local_vars.contains(name) { // Non-reactive local variable (e.g., for-in loop var) self.emit_line(&format!( "{}.textContent = {};", node_var, name )); } else { // Reactive text binding! self.emit_line(&format!( "DS.effect(() => {{ {}.textContent = {}.value; }});", node_var, name )); } } _ => { let js = self.emit_expr(arg); self.emit_line(&format!("{}.textContent = {};", node_var, js)); } } } // Handle props (event handlers, attributes) for (key, val) in &element.props { match key.as_str() { "click" | "input" | "change" | "submit" | "keydown" | "keyup" | "mousedown" | "mouseup" => { let handler_js = self.emit_event_handler_expr(val); let dom_event = match key.as_str() { "click" => "click", "input" => "input", "change" => "change", "submit" => "submit", "keydown" => "keydown", "keyup" => "keyup", "mousedown" => "mousedown", "mouseup" => "mouseup", _ => key, }; self.emit_line(&format!( "{}.addEventListener('{}', (e) => {{ {}; DS.flush(); }});", node_var, dom_event, handler_js )); } "bind" => { // Two-way binding: signal <-> input // Use raw signal name (not emit_expr which adds .value) let signal_name = match val { Expr::Ident(name) => name.clone(), _ => self.emit_expr(val), }; // Signal -> input self.emit_line(&format!( "DS.effect(() => {{ {}.value = {}.value; }});", node_var, signal_name )); // Input -> signal self.emit_line(&format!( "{}.addEventListener('input', (e) => {{ {}.value = e.target.value; DS.flush(); }});", node_var, signal_name )); } "class" => { let js = self.emit_expr(val); self.emit_line(&format!("{}.className += ' ' + {};", node_var, js)); } "placeholder" => { let js = self.emit_expr(val); self.emit_line(&format!("{}.placeholder = {};", node_var, js)); } "value" => { let js = self.emit_expr(val); self.emit_line(&format!( "DS.effect(() => {{ {}.value = {}.value; }});", node_var, js )); } "style" => { let js = self.emit_expr(val); self.emit_line(&format!("{}.style.cssText = {};", node_var, js)); } "disabled" => { let js = self.emit_expr(val); self.emit_line(&format!( "DS.effect(() => {{ {}.disabled = {}.value; }});", node_var, js )); } _ => { let js = self.emit_expr(val); self.emit_line(&format!("{}.setAttribute('{}', {});", node_var, key, js)); } } } node_var } Expr::When(cond, body) => { let anchor_var = self.next_node_id(); let container_var = self.next_node_id(); let cond_js = self.emit_expr(cond); self.emit_line(&format!("const {} = document.createComment('when');", anchor_var)); self.emit_line(&format!("let {} = null;", container_var)); // Build the conditional child self.emit_line("DS.effect(() => {"); self.indent += 1; self.emit_line(&format!("const show = {};", cond_js)); self.emit_line(&format!("if (show && !{}) {{", container_var)); self.indent += 1; let child_var = self.emit_view_expr(body, graph); self.emit_line(&format!("{} = {};", container_var, child_var)); self.emit_line(&format!( "{}.parentNode.insertBefore({}, {}.nextSibling);", anchor_var, container_var, anchor_var )); self.indent -= 1; self.emit_line(&format!("}} else if (!show && {}) {{", container_var)); self.indent += 1; self.emit_line(&format!("{}.remove();", container_var)); self.emit_line(&format!("{} = null;", container_var)); self.indent -= 1; self.emit_line("}"); self.indent -= 1; self.emit_line("});"); anchor_var } // ForIn reactive list: `for item in items -> body` Expr::ForIn { item, index, iter, body } => { let container_var = self.next_node_id(); let iter_js = self.emit_expr(iter); let iter_var = self.next_node_id(); // unique name to avoid shadowing self.emit_line(&format!("const {} = document.createElement('div');", container_var)); self.emit_line(&format!("{}.className = 'ds-for-list';", container_var)); // Reactive effect that re-renders the list when the iterable changes self.emit_line("DS.effect(() => {"); self.indent += 1; self.emit_line(&format!("const {} = {};", iter_var, iter_js)); self.emit_line(&format!("{}.innerHTML = '';", container_var)); let idx_var = index.as_deref().unwrap_or("_idx"); self.emit_line(&format!("const __list = ({0} && {0}.value !== undefined) ? {0}.value : (Array.isArray({0}) ? {0} : []);", iter_var)); self.emit_line(&format!("__list.forEach(({item}, {idx_var}) => {{")); self.indent += 1; // Mark loop vars as non-reactive so text bindings use plain access self.local_vars.insert(item.clone()); if let Some(idx) = index { self.local_vars.insert(idx.clone()); } let child_var = self.emit_view_expr(body, graph); self.emit_line(&format!("{}.appendChild({});", container_var, child_var)); // Clean up self.local_vars.remove(item); if let Some(idx) = index { self.local_vars.remove(idx); } self.indent -= 1; self.emit_line("});"); self.indent -= 1; self.emit_line("});"); container_var } // Component usage: `` Expr::ComponentUse { name, props, children } => { let args = props.iter() .map(|(k, v)| self.emit_expr(v)) .collect::>() .join(", "); let node_var = self.next_node_id(); self.emit_line(&format!("const {} = component_{}({});", node_var, name, args)); node_var } Expr::Match(scrutinee, arms) => { let container_var = self.next_node_id(); self.emit_line(&format!("const {} = document.createElement('div');", container_var)); self.emit_line(&format!("{}.className = 'ds-match';", container_var)); let scrutinee_js = self.emit_expr(scrutinee); self.emit_line("DS.effect(() => {"); self.indent += 1; self.emit_line(&format!("const val = {};", scrutinee_js)); self.emit_line(&format!("{}.innerHTML = '';", container_var)); for (i, arm) in arms.iter().enumerate() { let prefix = if i == 0 { "if" } else { "} else if" }; let pattern_js = self.emit_pattern_check(&arm.pattern, "val"); self.emit_line(&format!("{} ({}) {{", prefix, pattern_js)); self.indent += 1; let child = self.emit_view_expr(&arm.body, graph); self.emit_line(&format!("{}.appendChild({});", container_var, child)); self.indent -= 1; } self.emit_line("}"); self.indent -= 1; self.emit_line("});"); container_var } // Fallback: just create a text node _ => { let node_var = self.next_node_id(); let js = self.emit_expr(expr); self.emit_line(&format!( "const {} = document.createTextNode({});", node_var, js )); node_var } } } fn emit_handler(&mut self, handler: &OnHandler) { let handler_js = self.emit_event_handler_expr(&handler.body); if let Some(param) = &handler.param { self.emit_line(&format!( "DS.onEvent('{}', ({}) => {{ {}; DS.flush(); }});", handler.event, param, handler_js )); } else { self.emit_line(&format!( "DS.onEvent('{}', () => {{ {}; DS.flush(); }});", handler.event, handler_js )); } } // ── Expression emitters ───────────────────────────── fn emit_expr(&self, expr: &Expr) -> String { match expr { Expr::IntLit(n) => format!("{n}"), Expr::FloatLit(n) => format!("{n}"), Expr::BoolLit(b) => format!("{b}"), Expr::StringLit(s) => { if s.segments.len() == 1 { if let StringSegment::Literal(text) = &s.segments[0] { return format!("\"{}\"", text.replace('"', "\\\"")); } } // Template literal with interpolation let mut parts = Vec::new(); for seg in &s.segments { match seg { StringSegment::Literal(text) => parts.push(text.clone()), StringSegment::Interpolation(expr) => { parts.push(format!("${{{}}}", self.emit_expr(expr))); } } } format!("`{}`", parts.join("")) } Expr::Ident(name) => format!("{name}.value"), Expr::DotAccess(base, field) => { let base_js = self.emit_expr(base); format!("{base_js}.{field}") } Expr::BinOp(left, op, right) => { let l = self.emit_expr(left); let r = self.emit_expr(right); let op_str = match op { BinOp::Add => "+", BinOp::Sub => "-", BinOp::Mul => "*", BinOp::Div => "/", BinOp::Mod => "%", BinOp::Eq => "===", BinOp::Neq => "!==", BinOp::Lt => "<", BinOp::Gt => ">", BinOp::Lte => "<=", BinOp::Gte => ">=", BinOp::And => "&&", BinOp::Or => "||", }; format!("({l} {op_str} {r})") } Expr::UnaryOp(op, inner) => { let inner_js = self.emit_expr(inner); match op { UnaryOp::Neg => format!("(-{inner_js})"), UnaryOp::Not => format!("(!{inner_js})"), } } Expr::Call(name, args) => { let args_js: Vec = args.iter().map(|a| self.emit_expr(a)).collect(); // Built-in functions map to DS.xxx() let fn_name = match name.as_str() { "navigate" => "DS.navigate", "spring" => "DS.spring", _ => name, }; format!("{}({})", fn_name, args_js.join(", ")) } Expr::If(cond, then_b, else_b) => { let c = self.emit_expr(cond); let t = self.emit_expr(then_b); let e = self.emit_expr(else_b); format!("({c} ? {t} : {e})") } Expr::Lambda(params, body) => { let body_js = self.emit_expr(body); format!("({}) => {}", params.join(", "), body_js) } Expr::Record(fields) => { let fields_js: Vec = fields .iter() .map(|(k, v)| format!("{}: {}", k, self.emit_expr(v))) .collect(); format!("{{ {} }}", fields_js.join(", ")) } Expr::List(items) => { let items_js: Vec = items.iter().map(|i| self.emit_expr(i)).collect(); format!("[{}]", items_js.join(", ")) } Expr::StreamFrom { source, .. } => { format!("DS._connectStream(\"{}\")", source) } _ => "null".to_string(), } } /// Emit an event handler expression (assignment, etc.) fn emit_event_handler_expr(&self, expr: &Expr) -> String { match expr { Expr::Assign(target, op, value) => { let target_js = match target.as_ref() { Expr::Ident(name) => name.clone(), Expr::DotAccess(base, field) => { format!("{}.{}", self.emit_expr(base), field) } _ => self.emit_expr(target), }; let value_js = self.emit_expr(value); let assign = match op { AssignOp::Set => format!("{target_js}.value = {value_js}"), AssignOp::AddAssign => format!("{target_js}.value += {value_js}"), AssignOp::SubAssign => format!("{target_js}.value -= {value_js}"), }; // Stream diff: broadcast signal change if streaming is active format!("{}; DS._streamDiff(\"{}\", {}.value)", assign, target_js, target_js) } Expr::Block(exprs) => { let stmts: Vec = exprs.iter().map(|e| self.emit_event_handler_expr(e)).collect(); stmts.join("; ") } _ => self.emit_expr(expr), } } /// Emit a pattern matching condition check. fn emit_pattern_check(&self, pattern: &Pattern, scrutinee: &str) -> String { match pattern { Pattern::Wildcard => "true".to_string(), Pattern::Ident(name) => { // Bind: always true, but assign format!("(({name} = {scrutinee}), true)") } Pattern::Constructor(name, _fields) => { format!("{scrutinee} === '{name}'") } Pattern::Literal(expr) => { let val = self.emit_expr(expr); format!("{scrutinee} === {val}") } } } // ── Helpers ────────────────────────────────────────── fn next_node_id(&mut self) -> String { let id = self.node_id_counter; self.node_id_counter += 1; format!("n{id}") } fn emit_line(&mut self, line: &str) { for _ in 0..self.indent { self.output.push_str(" "); } self.output.push_str(line); self.output.push('\n'); } fn find_let_expr<'a>(&self, program: &'a Program, name: &str) -> Option<&'a Expr> { for decl in &program.declarations { if let Declaration::Let(let_decl) = decl { if let_decl.name == name { return Some(&let_decl.value); } } } None } fn is_signal_ref(&self, expr: &str) -> bool { // Must start with a letter/underscore (not a digit) and contain only ident chars !expr.is_empty() && expr.starts_with(|c: char| c.is_ascii_alphabetic() || c == '_') && expr.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') } /// Emit a physics scene — canvas + WASM physics engine + animation loop fn emit_scene(&mut self, container: &Container, graph: &SignalGraph) -> String { let wrapper_var = self.next_node_id(); let canvas_var = self.next_node_id(); // Extract scene props let mut scene_width = "800".to_string(); let mut scene_height = "500".to_string(); let mut gravity_x_expr = None; let mut gravity_y_expr = None; let mut gx_is_signal = false; let mut gy_is_signal = false; for (key, val) in &container.props { match key.as_str() { "width" => scene_width = self.emit_expr(val), "height" => scene_height = self.emit_expr(val), "gravity_x" => { gx_is_signal = matches!(val, Expr::Ident(_)); gravity_x_expr = Some(if let Expr::Ident(name) = val { name.clone() } else { self.emit_expr(val) }); } "gravity_y" => { gy_is_signal = matches!(val, Expr::Ident(_)); gravity_y_expr = Some(if let Expr::Ident(name) = val { name.clone() } else { self.emit_expr(val) }); } _ => {} } } // Create wrapper div self.emit_line(&format!("const {} = document.createElement('div');", wrapper_var)); self.emit_line(&format!("{}.className = 'ds-scene-wrapper';", wrapper_var)); // Create canvas let w_val = if graph.name_to_id.contains_key(&scene_width) || self.is_signal_ref(&scene_width) { format!("{}.value", scene_width) } else { scene_width.clone() }; let h_val = if graph.name_to_id.contains_key(&scene_height) || self.is_signal_ref(&scene_height) { format!("{}.value", scene_height) } else { scene_height.clone() }; self.emit_line(&format!("const {} = document.createElement('canvas');", canvas_var)); self.emit_line(&format!("{}.width = {};", canvas_var, w_val)); self.emit_line(&format!("{}.height = {};", canvas_var, h_val)); self.emit_line(&format!("{}.style.width = {} + 'px';", canvas_var, w_val)); self.emit_line(&format!("{}.style.height = {} + 'px';", canvas_var, h_val)); self.emit_line(&format!("{}.style.borderRadius = '16px';", canvas_var)); self.emit_line(&format!("{}.style.background = 'rgba(255,255,255,0.02)';", canvas_var)); self.emit_line(&format!("{}.style.border = '1px solid rgba(255,255,255,0.06)';", canvas_var)); self.emit_line(&format!("{}.style.cursor = 'pointer';", canvas_var)); self.emit_line(&format!("{}.appendChild({});", wrapper_var, canvas_var)); // Async IIFE to load WASM and set up physics self.emit_line("(async () => {"); self.indent += 1; self.emit_line("const { default: init, PhysicsWorld } = await import('./pkg/ds_physics.js');"); self.emit_line("await init();"); self.emit_line(&format!("const _sceneW = {};", w_val)); self.emit_line(&format!("const _sceneH = {};", h_val)); self.emit_line("const _world = new PhysicsWorld(_sceneW, _sceneH);"); self.emit_line(&format!("const _ctx = {}.getContext('2d');", canvas_var)); // Create bodies from child elements for child in &container.children { if let Expr::Element(element) = child { match element.tag.as_str() { "circle" => self.emit_scene_circle(element, graph), "rect" => self.emit_scene_rect(element, graph), _ => {} // skip unknown elements } } } // Wire reactive gravity if let Some(ref gx) = gravity_x_expr { if gx_is_signal { if let Some(ref gy) = gravity_y_expr { if gy_is_signal { self.emit_line(&format!( "DS.effect(() => {{ _world.set_gravity({}.value, {}.value); }});", gx, gy )); } else { self.emit_line(&format!( "DS.effect(() => {{ _world.set_gravity({}.value, {}); }});", gx, gy )); } } else { self.emit_line(&format!( "DS.effect(() => {{ _world.set_gravity({}.value, 980); }});", gx )); } } } else if let Some(ref gy) = gravity_y_expr { if gy_is_signal { self.emit_line(&format!( "DS.effect(() => {{ _world.set_gravity(0, {}.value); }});", gy )); } else { self.emit_line(&format!("_world.set_gravity(0, {});", gy)); } } // Drag interaction self.emit_line("let _dragBody = -1, _dragOffX = 0, _dragOffY = 0;"); self.emit_line("let _lastMX = 0, _lastMY = 0, _velX = 0, _velY = 0;"); self.emit_line(&format!( "{}.addEventListener('mousedown', (e) => {{", canvas_var )); self.indent += 1; self.emit_line(&format!("const rect = {}.getBoundingClientRect();", canvas_var)); self.emit_line("const mx = e.clientX - rect.left, my = e.clientY - rect.top;"); self.emit_line("_lastMX = mx; _lastMY = my;"); self.emit_line("for (let b = 0; b < _world.body_count(); b++) {"); self.indent += 1; self.emit_line("const c = _world.get_body_center(b);"); self.emit_line("const dx = mx - c[0], dy = my - c[1];"); self.emit_line("if (Math.sqrt(dx*dx + dy*dy) < 80) { _dragBody = b; _dragOffX = dx; _dragOffY = dy; e.preventDefault(); return; }"); self.indent -= 1; self.emit_line("}"); self.indent -= 1; self.emit_line("});"); self.emit_line("window.addEventListener('mousemove', (e) => {"); self.indent += 1; self.emit_line(&format!("const rect = {}.getBoundingClientRect();", canvas_var)); self.emit_line("const mx = e.clientX - rect.left, my = e.clientY - rect.top;"); self.emit_line("_velX = mx - _lastMX; _velY = my - _lastMY; _lastMX = mx; _lastMY = my;"); self.emit_line("if (_dragBody >= 0) { const c = _world.get_body_center(_dragBody); _world.apply_impulse(_dragBody, (mx - _dragOffX - c[0]) * 5000, (my - _dragOffY - c[1]) * 5000); }"); self.indent -= 1; self.emit_line("});"); self.emit_line("window.addEventListener('mouseup', () => { if (_dragBody >= 0) { _world.apply_impulse(_dragBody, _velX * 3000, _velY * 3000); _dragBody = -1; } });"); // Event listener for physics impulse self.emit_line("DS.onEvent('physics_impulse', (data) => { if (data && data.body !== undefined) _world.apply_impulse(data.body, data.fx || 0, data.fy || 0); });"); // Animation loop with draw self.emit_line("function _sceneDraw() {"); self.indent += 1; self.emit_line("_ctx.clearRect(0, 0, _sceneW, _sceneH);"); // Grid self.emit_line("_ctx.strokeStyle = 'rgba(255,255,255,0.02)'; _ctx.lineWidth = 1;"); self.emit_line("for (let x = 0; x < _sceneW; x += 40) { _ctx.beginPath(); _ctx.moveTo(x,0); _ctx.lineTo(x,_sceneH); _ctx.stroke(); }"); self.emit_line("for (let y = 0; y < _sceneH; y += 40) { _ctx.beginPath(); _ctx.moveTo(0,y); _ctx.lineTo(_sceneW,y); _ctx.stroke(); }"); // Bodies self.emit_line("for (let b = 0; b < _world.body_count(); b++) {"); self.indent += 1; self.emit_line("const pos = _world.get_body_positions(b), col = _world.get_body_color(b), bt = _world.get_body_type(b);"); self.emit_line("const r = col[0]*255|0, g = col[1]*255|0, bl = col[2]*255|0, a = col[3];"); self.emit_line("const fill = `rgba(${r},${g},${bl},${a*0.5})`, stroke = `rgba(${r},${g},${bl},${a*0.9})`, glow = `rgba(${r},${g},${bl},0.4)`;"); // Rect self.emit_line("if (bt === 1 && pos.length >= 8) {"); self.indent += 1; self.emit_line("_ctx.beginPath(); _ctx.moveTo(pos[0],pos[1]); _ctx.lineTo(pos[2],pos[3]); _ctx.lineTo(pos[4],pos[5]); _ctx.lineTo(pos[6],pos[7]); _ctx.closePath();"); self.emit_line("_ctx.shadowColor = glow; _ctx.shadowBlur = 18; _ctx.fillStyle = fill; _ctx.fill(); _ctx.shadowBlur = 0; _ctx.strokeStyle = stroke; _ctx.lineWidth = 2; _ctx.stroke();"); self.indent -= 1; self.emit_line("} else {"); self.indent += 1; // Circle self.emit_line("const pc = _world.get_body_perimeter_count(b);"); self.emit_line("if (pc >= 3) { _ctx.beginPath(); _ctx.moveTo(pos[0],pos[1]); for (let i=1;i= (pc+1)*2) { _ctx.beginPath(); _ctx.arc(pos[pc*2],pos[pc*2+1],2.5,0,Math.PI*2); _ctx.fillStyle = stroke; _ctx.fill(); } }"); self.indent -= 1; self.emit_line("}"); self.indent -= 1; self.emit_line("}"); // Boundary self.emit_line("_ctx.strokeStyle = 'rgba(99,102,241,0.06)'; _ctx.lineWidth = 2; _ctx.strokeRect(1,1,_sceneW-2,_sceneH-2);"); self.indent -= 1; self.emit_line("}"); // Loop self.emit_line(&format!( "function _sceneLoop() {{ _world.step(1/60); _sceneDraw(); if (DS._streamWs) DS._streamSceneState(_world, {}, {}); requestAnimationFrame(_sceneLoop); }}", scene_width, scene_height )); self.emit_line("requestAnimationFrame(_sceneLoop);"); self.indent -= 1; self.emit_line("})();"); wrapper_var } /// Emit a circle body creation inside a scene fn emit_scene_circle(&mut self, element: &Element, _graph: &SignalGraph) { let mut x = "200".to_string(); let mut y = "100".to_string(); let mut radius = "30".to_string(); let mut color = None; let mut segments = "16".to_string(); let mut pinned = false; for (key, val) in &element.props { let js = self.emit_expr(val); match key.as_str() { "x" => x = js, "y" => y = js, "radius" => radius = js, "color" => color = Some(js), "segments" => segments = js, "pinned" => pinned = js == "true", _ => {} } } let body_var = self.next_node_id(); self.emit_line(&format!( "const {} = _world.create_soft_circle({}, {}, {}, {}, 50);", body_var, x, y, radius, segments )); if let Some(c) = color { self.emit_scene_set_color(&body_var, c); } if pinned { self.emit_line(&format!("_world.pin_particle({}, true);", body_var)); } } /// Emit a rect body creation inside a scene fn emit_scene_rect(&mut self, element: &Element, _graph: &SignalGraph) { let mut x = "200".to_string(); let mut y = "100".to_string(); let mut width = "60".to_string(); let mut height = "40".to_string(); let mut color = None; let mut pinned = false; for (key, val) in &element.props { let js = self.emit_expr(val); match key.as_str() { "x" => x = js, "y" => y = js, "width" => width = js, "height" => height = js, "color" => color = Some(js), "pinned" => pinned = js == "true", _ => {} } } let body_var = self.next_node_id(); self.emit_line(&format!( "const {} = _world.create_soft_rect({}, {}, {}, {}, 4, 3, 30);", body_var, x, y, width, height )); if let Some(c) = color { self.emit_scene_set_color(&body_var, c); } if pinned { self.emit_line(&format!("_world.pin_particle({}, true);", body_var)); } } /// Emit set_body_color from a hex color string fn emit_scene_set_color(&mut self, body_var: &str, color_str: String) { // Parse hex color at compile time or emit runtime parser let clean = color_str.replace('"', "").replace('\\', ""); if clean.starts_with('#') && clean.len() == 7 { let r = u8::from_str_radix(&clean[1..3], 16).unwrap_or(128); let g = u8::from_str_radix(&clean[3..5], 16).unwrap_or(128); let b = u8::from_str_radix(&clean[5..7], 16).unwrap_or(128); self.emit_line(&format!( "_world.set_body_color({}, {:.3}, {:.3}, {:.3}, 1.0);", body_var, r as f64 / 255.0, g as f64 / 255.0, b as f64 / 255.0 )); } } } impl Default for JsEmitter { fn default() -> Self { Self::new() } } /// Minimal CSS reset and layout classes. const CSS_RESET: &str = r#" * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif; background: #0a0a0f; color: #e2e8f0; display: flex; justify-content: center; align-items: center; min-height: 100vh; } #ds-root { width: 100%; max-width: 800px; padding: 2rem; } .ds-column { display: flex; flex-direction: column; gap: 1rem; } .ds-row { display: flex; flex-direction: row; gap: 1rem; align-items: center; } .ds-stack { position: relative; } .ds-panel { position: absolute; } .ds-text, .ds-text span { font-size: 1.25rem; color: #e2e8f0; } .ds-button, button.ds-button { background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); color: white; border: none; padding: 0.75rem 1.5rem; border-radius: 12px; font-size: 1rem; font-weight: 600; cursor: pointer; transition: all 0.2s ease; box-shadow: 0 4px 15px rgba(99, 102, 241, 0.3); } .ds-button:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(99, 102, 241, 0.4); } .ds-button:active { transform: translateY(0); } .ds-input, input.ds-input { background: rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.1); color: #e2e8f0; padding: 0.75rem 1rem; border-radius: 12px; font-size: 1rem; outline: none; transition: border-color 0.2s; } .ds-input:focus { border-color: #6366f1; box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15); } .ds-spinner { width: 2rem; height: 2rem; border: 3px solid rgba(255,255,255,0.1); border-top-color: #6366f1; border-radius: 50%; animation: ds-spin 0.8s linear infinite; } @keyframes ds-spin { to { transform: rotate(360deg); } } @keyframes ds-fade-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } } .ds-fade-in { animation: ds-fade-in 0.3s ease-out; } "#; /// The DreamStack client-side reactive runtime (~3KB). const RUNTIME_JS: &str = r#" const DS = (() => { // ── Signal System ── let currentEffect = null; let batchDepth = 0; let pendingEffects = new Set(); class Signal { constructor(initialValue) { this._value = initialValue; this._subscribers = new Set(); } get value() { if (currentEffect) { this._subscribers.add(currentEffect); } return this._value; } set value(newValue) { if (this._value === newValue) return; this._value = newValue; if (batchDepth > 0) { for (const sub of this._subscribers) { pendingEffects.add(sub); } } else { for (const sub of [...this._subscribers]) { sub._run(); } } } } class Derived { constructor(fn) { this._fn = fn; this._value = undefined; this._dirty = true; this._subscribers = new Set(); this._effect = new Effect(() => { this._value = this._fn(); this._dirty = false; // Notify our subscribers if (batchDepth > 0) { for (const sub of this._subscribers) { pendingEffects.add(sub); } } else { for (const sub of [...this._subscribers]) { sub._run(); } } }); this._effect._run(); } get value() { if (currentEffect) { this._subscribers.add(currentEffect); } return this._value; } } class Effect { constructor(fn) { this._fn = fn; this._disposed = false; } _run() { if (this._disposed) return; const prev = currentEffect; currentEffect = this; try { this._fn(); } finally { currentEffect = prev; } } dispose() { this._disposed = true; } } // ── Public API ── function signal(value) { return new Signal(value); } function derived(fn) { return new Derived(fn); } function effect(fn) { const eff = new Effect(fn); eff._run(); return eff; } function batch(fn) { batchDepth++; try { fn(); } finally { batchDepth--; if (batchDepth === 0) { flush(); } } } function flush() { const effects = [...pendingEffects]; pendingEffects.clear(); for (const eff of effects) { eff._run(); } } // ── Event System ── const eventHandlers = {}; function onEvent(name, handler) { if (!eventHandlers[name]) eventHandlers[name] = []; eventHandlers[name].push(handler); } function emit(name, data) { const handlers = eventHandlers[name]; if (handlers) { batch(() => { for (const h of handlers) { h(data); } }); } } // ── Keyed List Reconciliation ── function keyedList(container, getItems, keyFn, renderFn) { let nodeMap = new Map(); const eff = effect(() => { const raw = getItems(); const items = (raw && raw.value !== undefined) ? raw.value : (Array.isArray(raw) ? raw : []); const newMap = new Map(); const frag = document.createDocumentFragment(); items.forEach((item, idx) => { const key = keyFn ? keyFn(item, idx) : idx; let node = nodeMap.get(key); if (!node) { node = renderFn(item, idx); } newMap.set(key, node); frag.appendChild(node); }); container.innerHTML = ''; container.appendChild(frag); nodeMap = newMap; }); return eff; } // ── Router ── const _route = new Signal(window.location.hash.slice(1) || '/'); window.addEventListener('hashchange', () => { _route.value = window.location.hash.slice(1) || '/'; }); function navigate(path) { window.location.hash = path; } function matchRoute(pattern, path) { if (pattern === path) return {}; const patParts = pattern.split('/'); const pathParts = path.split('/'); if (patParts.length !== pathParts.length) return null; const params = {}; for (let i = 0; i < patParts.length; i++) { if (patParts[i].startsWith(':')) { params[patParts[i].slice(1)] = pathParts[i]; } else if (patParts[i] !== pathParts[i]) { return null; } } return params; } // ── Async Resources ── function resource(fetcher) { const state = new Signal({ status: 'loading', data: null, error: null }); const eff = effect(() => { state.value = { status: 'loading', data: state._value.data, error: null }; Promise.resolve(fetcher()) .then(data => { state.value = { status: 'ok', data, error: null }; }) .catch(err => { state.value = { status: 'error', data: null, error: err.message || String(err) }; }); }); return state; } function fetchJSON(url) { return resource(() => fetch(url).then(r => r.json())); } // ── Spring Physics Engine ── const _activeSprings = new Set(); let _rafId = null; let _lastTime = 0; class Spring { constructor({ value = 0, target, stiffness = 170, damping = 26, mass = 1 } = {}) { this._signal = new Signal(value); this._velocity = 0; this._target = target !== undefined ? target : value; this.stiffness = stiffness; this.damping = damping; this.mass = mass; this._settled = true; } get value() { return this._signal.value; } set value(v) { // Assignment animates to new target (not instant set) this.target = v; } get target() { return this._target; } set target(t) { this._target = t; this._settled = false; _activeSprings.add(this); _startSpringLoop(); } set(v) { this._signal.value = v; this._target = v; this._velocity = 0; this._settled = true; _activeSprings.delete(this); } _step(dt) { const pos = this._signal._value; const vel = this._velocity; const k = this.stiffness, d = this.damping, m = this.mass; const accel = (p, v) => (-k * (p - this._target) - d * v) / m; const k1v = accel(pos, vel), k1p = vel; const k2v = accel(pos + k1p*dt/2, vel + k1v*dt/2), k2p = vel + k1v*dt/2; const k3v = accel(pos + k2p*dt/2, vel + k2v*dt/2), k3p = vel + k2v*dt/2; const k4v = accel(pos + k3p*dt, vel + k3v*dt), k4p = vel + k3v*dt; this._velocity = vel + (dt/6)*(k1v + 2*k2v + 2*k3v + k4v); this._signal.value = pos + (dt/6)*(k1p + 2*k2p + 2*k3p + k4p); if (Math.abs(this._velocity) < 0.01 && Math.abs(this._signal._value - this._target) < 0.01) { this._signal.value = this._target; this._velocity = 0; this._settled = true; _activeSprings.delete(this); } } } function _startSpringLoop() { if (_rafId !== null) return; _lastTime = performance.now(); _rafId = requestAnimationFrame(_springLoop); } function _springLoop(now) { const dt = Math.min((now - _lastTime) / 1000, 0.064); _lastTime = now; batch(() => { for (const s of _activeSprings) { const steps = Math.ceil(dt / (1/120)); const subDt = dt / steps; for (let i = 0; i < steps; i++) s._step(subDt); } }); if (_activeSprings.size > 0) _rafId = requestAnimationFrame(_springLoop); else _rafId = null; } function spring(opts) { return new Spring(typeof opts === 'object' ? opts : { value: opts, target: opts }); } // ── Constraint Solver ── function constrain(element, prop, fn) { return effect(() => { const val = fn(); if (prop === 'width' || prop === 'height' || prop === 'left' || prop === 'top' || prop === 'right' || prop === 'bottom' || prop === 'maxWidth' || prop === 'minWidth' || prop === 'maxHeight' || prop === 'minHeight' || prop === 'fontSize' || prop === 'padding' || prop === 'margin' || prop === 'gap') { element.style[prop] = typeof val === 'number' ? val + 'px' : val; } else if (prop === 'opacity' || prop === 'scale') { element.style[prop] = val; } else { element.style[prop] = val; } }); } // Viewport signal for responsive constraints const _viewport = { width: new Signal(window.innerWidth), height: new Signal(window.innerHeight) }; window.addEventListener('resize', () => { _viewport.width.value = window.innerWidth; _viewport.height.value = window.innerHeight; }); // ── 2D Scene Rendering Engine ── function scene(width, height) { const canvas = document.createElement('canvas'); canvas.width = width || 600; canvas.height = height || 400; canvas.style.borderRadius = '16px'; canvas.style.background = 'rgba(255,255,255,0.03)'; canvas.style.border = '1px solid rgba(255,255,255,0.08)'; canvas.style.display = 'block'; canvas.style.margin = '8px 0'; const ctx = canvas.getContext('2d'); const shapes = []; let _dirty = false; function _render() { ctx.clearRect(0, 0, canvas.width, canvas.height); for (const s of shapes) s._draw(ctx); } function _scheduleRender() { if (!_dirty) { _dirty = true; queueMicrotask(() => { _dirty = false; _render(); }); } } return { canvas, ctx, shapes, _render, _scheduleRender }; } function circle(scn, opts) { const shape = { type: 'circle', _draw(ctx) { const x = typeof opts.x === 'object' && 'value' in opts.x ? opts.x.value : (typeof opts.x === 'function' ? opts.x() : opts.x); const y = typeof opts.y === 'object' && 'value' in opts.y ? opts.y.value : (typeof opts.y === 'function' ? opts.y() : opts.y); const r = typeof opts.r === 'object' && 'value' in opts.r ? opts.r.value : (typeof opts.r === 'function' ? opts.r() : (opts.r || 20)); const fill = opts.fill || '#8b5cf6'; ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); // Gradient fill for nice look const grad = ctx.createRadialGradient(x - r*0.3, y - r*0.3, r*0.1, x, y, r); grad.addColorStop(0, '#a78bfa'); grad.addColorStop(1, fill); ctx.fillStyle = grad; ctx.fill(); // Glow ctx.shadowColor = fill; ctx.shadowBlur = 20; ctx.fill(); ctx.shadowBlur = 0; } }; scn.shapes.push(shape); // Reactive: re-render when signal deps change effect(() => { shape._draw; // trigger reads if (opts.x && typeof opts.x === 'object' && 'value' in opts.x) opts.x.value; if (opts.y && typeof opts.y === 'object' && 'value' in opts.y) opts.y.value; if (opts.r && typeof opts.r === 'object' && 'value' in opts.r) opts.r.value; scn._scheduleRender(); }); return shape; } function rect(scn, opts) { const shape = { type: 'rect', _draw(ctx) { const x = typeof opts.x === 'object' && 'value' in opts.x ? opts.x.value : (typeof opts.x === 'function' ? opts.x() : opts.x); const y = typeof opts.y === 'object' && 'value' in opts.y ? opts.y.value : (typeof opts.y === 'function' ? opts.y() : opts.y); const w = typeof opts.w === 'object' && 'value' in opts.w ? opts.w.value : (typeof opts.w === 'function' ? opts.w() : (opts.w || 40)); const h = typeof opts.h === 'object' && 'value' in opts.h ? opts.h.value : (typeof opts.h === 'function' ? opts.h() : (opts.h || 40)); const fill = opts.fill || '#6366f1'; const r = opts.radius || 8; ctx.beginPath(); ctx.roundRect(x, y, w, h, r); ctx.fillStyle = fill; ctx.fill(); } }; scn.shapes.push(shape); effect(() => { if (opts.x && typeof opts.x === 'object' && 'value' in opts.x) opts.x.value; if (opts.y && typeof opts.y === 'object' && 'value' in opts.y) opts.y.value; if (opts.w && typeof opts.w === 'object' && 'value' in opts.w) opts.w.value; if (opts.h && typeof opts.h === 'object' && 'value' in opts.h) opts.h.value; scn._scheduleRender(); }); return shape; } function line(scn, opts) { const shape = { type: 'line', _draw(ctx) { const x1 = typeof opts.x1 === 'object' && 'value' in opts.x1 ? opts.x1.value : opts.x1; const y1 = typeof opts.y1 === 'object' && 'value' in opts.y1 ? opts.y1.value : opts.y1; const x2 = typeof opts.x2 === 'object' && 'value' in opts.x2 ? opts.x2.value : opts.x2; const y2 = typeof opts.y2 === 'object' && 'value' in opts.y2 ? opts.y2.value : opts.y2; ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.strokeStyle = opts.stroke || 'rgba(139,92,246,0.3)'; ctx.lineWidth = opts.width || 2; ctx.stroke(); } }; scn.shapes.push(shape); effect(() => { if (opts.x1 && typeof opts.x1 === 'object' && 'value' in opts.x1) opts.x1.value; if (opts.y1 && typeof opts.y1 === 'object' && 'value' in opts.y1) opts.y1.value; if (opts.x2 && typeof opts.x2 === 'object' && 'value' in opts.x2) opts.x2.value; if (opts.y2 && typeof opts.y2 === 'object' && 'value' in opts.y2) opts.y2.value; scn._scheduleRender(); }); return shape; } // ── Bitstream Streaming ── const HEADER_SIZE = 16; let _streamWs = null; let _streamSeq = 0; let _streamMode = 'signal'; let _streamStart = 0; let _prevSignals = null; function _encodeHeader(type, flags, seq, ts, w, h, len) { const b = new ArrayBuffer(HEADER_SIZE); const v = new DataView(b); v.setUint8(0, type); v.setUint8(1, flags); v.setUint16(2, seq, true); v.setUint32(4, ts, true); v.setUint16(8, w, true); v.setUint16(10, h, true); v.setUint32(12, len, true); return new Uint8Array(b); } function _decodeHeader(buf) { const v = new DataView(buf.buffer || buf, buf.byteOffset || 0); return { type: v.getUint8(0), flags: v.getUint8(1), seq: v.getUint16(2, true), timestamp: v.getUint32(4, true), width: v.getUint16(8, true), height: v.getUint16(10, true), length: v.getUint32(12, true) }; } function _initStream(url, mode) { _streamMode = mode || 'signal'; _streamStart = performance.now(); _streamWs = new WebSocket(url); _streamWs.binaryType = 'arraybuffer'; _streamWs.onmessage = function(e) { if (!(e.data instanceof ArrayBuffer) || e.data.byteLength < HEADER_SIZE) return; var bytes = new Uint8Array(e.data); var view = new DataView(bytes.buffer); var type = view.getUint8(0); var flags = view.getUint8(1); if (flags & 0x01) _handleRemoteInput(type, bytes.subarray(HEADER_SIZE)); }; _streamWs.onclose = function() { setTimeout(function() { _initStream(url, mode); }, 2000); }; console.log('[ds-stream] Source connected:', url, 'mode:', mode); } function _streamSend(type, flags, payload) { if (!_streamWs || _streamWs.readyState !== 1) return; var ts = (performance.now() - _streamStart) | 0; var msg = new Uint8Array(HEADER_SIZE + payload.length); var v = new DataView(msg.buffer); v.setUint8(0, type); v.setUint8(1, flags); v.setUint16(2, (_streamSeq++) & 0xFFFF, true); v.setUint32(4, ts, true); v.setUint32(12, payload.length, true); msg.set(payload, HEADER_SIZE); _streamWs.send(msg.buffer); } function _streamDiff(name, value) { if (!_streamWs || _streamMode !== 'signal') return; var obj = {}; obj[name] = (typeof value === 'object' && value !== null && 'value' in value) ? value.value : value; _streamSend(0x31, 0, new TextEncoder().encode(JSON.stringify(obj))); } function _streamSync(signals) { var state = {}; for (var name in signals) { var sig = signals[name]; state[name] = (typeof sig === 'object' && sig !== null && '_value' in sig) ? sig._value : sig; } _streamSend(0x30, 0x02, new TextEncoder().encode(JSON.stringify(state))); } function _streamSceneState(world, w, h) { if (_streamMode === 'signal') { var bodies = []; for (var b = 0; b < world.body_count(); b++) { var p = world.get_body_center(b); bodies.push({ x: p[0] | 0, y: p[1] | 0 }); } _streamSend(0x31, 0, new TextEncoder().encode(JSON.stringify({ _bodies: bodies }))); } } function _handleRemoteInput(type, payload) { if (payload.length < 4) return; var view = new DataView(payload.buffer, payload.byteOffset); switch (type) { case 0x01: case 0x02: emit('remote_pointer', { x: view.getUint16(0, true), y: view.getUint16(2, true), buttons: payload.length > 4 ? view.getUint8(4) : 0, type: type === 0x02 ? 'down' : 'move' }); break; case 0x03: emit('remote_pointer', { x: 0, y: 0, buttons: 0, type: 'up' }); break; case 0x10: emit('remote_key', { keyCode: view.getUint16(0, true), type: 'down' }); break; case 0x11: emit('remote_key', { keyCode: view.getUint16(0, true), type: 'up' }); break; case 0x50: emit('remote_scroll', { dx: view.getInt16(0, true), dy: view.getInt16(2, true) }); break; } } function _connectStream(url) { var state = signal(null); var ws = new WebSocket(url); ws.binaryType = 'arraybuffer'; ws.onmessage = function(e) { if (!(e.data instanceof ArrayBuffer) || e.data.byteLength < HEADER_SIZE) return; var bytes = new Uint8Array(e.data); var view = new DataView(bytes.buffer); var type = view.getUint8(0); var payloadLen = view.getUint32(12, true); var pl = bytes.subarray(HEADER_SIZE, HEADER_SIZE + payloadLen); if (type === 0x30 || type === 0x31) { try { var newState = JSON.parse(new TextDecoder().decode(pl)); state.value = Object.assign(state._value || {}, newState); } catch(ex) {} } }; ws.onclose = function() { setTimeout(function() { _connectStream(url); }, 2000); }; return state; } var _ds = { signal: signal, derived: derived, effect: effect, batch: batch, flush: flush, onEvent: onEvent, emit: emit, keyedList: keyedList, route: _route, navigate: navigate, matchRoute: matchRoute, resource: resource, fetchJSON: fetchJSON, spring: spring, constrain: constrain, viewport: _viewport, scene: scene, circle: circle, rect: rect, line: line, _initStream: _initStream, _streamDiff: _streamDiff, _streamSync: _streamSync, _streamSceneState: _streamSceneState, _connectStream: _connectStream, Signal: Signal, Derived: Derived, Effect: Effect, Spring: Spring }; Object.defineProperty(_ds, '_streamWs', { get: function() { return _streamWs; } }); return _ds; })(); "#;