//! Server-rendered SVG sky (docs/06). Terms are stars; luminosity = centrality; //! edges above a confidence threshold are the faint lines of a star chart; //! ensemble disagreement renders as chromatic blur; the most central term is //! the dark star — a black disc ringed by everything that orbits it, because //! the organizing element has the least positive content. //! //! SVG attributes are single-quoted throughout so hex colors survive Rust raw //! string delimiters. use std::fmt::Write; use super::layout::{Layout, SIZE}; /// Edge weight below which lines are not drawn. const EDGE_THRESHOLD: f64 = 0.15; fn esc(s: &str) -> String { s.replace('&', "&") .replace('<', "<") .replace('>', ">") .replace('\'', "'") } /// Community hues, spaced around the wheel. fn star_color(community: i64, lum: f64) -> String { let hue = (community as f64 * 67.0) % 360.0; let light = 62.0 + lum * 28.0; format!("hsl({hue:.0},72%,{light:.0}%)") } pub struct SvgOptions { pub width: u32, pub height: u32, /// Term to visually emphasize (term-focus fragment / comparison view). pub focus: Option, /// Card mode: bigger type. pub card: bool, /// Title shown in the frame (handle). pub title: String, } pub fn render_sky(layout: &Layout, opts: &SvgOptions) -> String { let mut s = String::with_capacity(64 * 1024); let (w, h) = (opts.width, opts.height); write!( s, r#""#, title = esc(&opts.title) ) .unwrap(); // Night sky background with a subtle radial falloff. s.push_str(concat!( "", "", "", "", "", "", "", )); write!(s, "").unwrap(); let pos: std::collections::HashMap<&str, (f64, f64)> = layout .nodes .iter() .map(|n| (n.term.as_str(), (n.x, n.y))) .collect(); // ---- edges: faint star-chart lines, drawn under the stars ---- s.push_str(""); for e in &layout.edges { if e.weight < EDGE_THRESHOLD { continue; } let (Some(&(x1, y1)), Some(&(x2, y2))) = (pos.get(e.src.as_str()), pos.get(e.dst.as_str())) else { continue; }; let opacity = (0.10 + e.weight * 0.25).min(0.4); // Antagonistic relations are drawn warm; kinships cool. let color = if e.sign < -0.15 { "#b0543a" } else { "#7d92c9" }; write!( s, "" ) .unwrap(); } s.push_str(""); // ---- stars ---- // The dark star is the signature of a *quilted* sky. A deflationary or // dimensional sky renders its most central term as an ordinary star: // showing a hollow center the data doesn't support would be a horoscope. let show_dark_star = layout.verdict == "quilted"; s.push_str(""); for n in &layout.nodes { let r = 2.0 + n.lum * 9.0; let focused = opts.focus.as_deref() == Some(n.term.as_str()); let term = esc(&n.term); if n.is_master && show_dark_star { // The dark star: labeled but unfilled, ringed by its orbit. write!( s, concat!( "", "", "", "{term} — your most central term. Why is it hollow? Because what organizes a field of meaning is the term with the least positive content of its own.", "" ), x = n.x, y = n.y, ring = r + 6.0, r = r, term = term, ) .unwrap(); } else { // Chromatic blur for contested terms: shimmer halo in offset hues, // crisp point for settled ones. Uncertainty as atmosphere. if n.blur > 0.05 { let spread = 1.5 + n.blur * 6.0; write!( s, concat!( "", "" ), x1 = n.x - spread * 0.6, x2 = n.x + spread * 0.6, y = n.y, sr = r + spread, o = 0.18 + n.blur * 0.25, ) .unwrap(); } write!( s, concat!( "", "", "", "{term}", "" ), x = n.x, y = n.y, glow = r * 2.2, r = r, color = star_color(n.community, n.lum), stroke = if focused { " stroke='#ffffff' stroke-width='2'" } else { "" }, term = term, ) .unwrap(); } // Label the brighter stars (and always the focus + master). if n.lum > 0.35 || n.is_master || focused { let size = if opts.card { 22.0 } else { 15.0 }; write!( s, "{term}", x = n.x + r + 4.0, y = n.y + 4.0, term = term, ) .unwrap(); } } s.push_str(""); // ---- the deflationary sky is a first-class design target ---- if layout.verdict == "deflationary" || layout.verdict == "insufficient" { let msg = if layout.verdict == "insufficient" { "Not enough sky yet. Keep completing squares." } else { "Your completions don't yet show a stable center. This may mean more sessions — or it may mean you don't have one." }; write!( s, "{}", esc(msg) ) .unwrap(); } // ---- epistemic frame: every sky carries its status ---- let frame = format!( "{} · {} · {} completions · {} terms · gini {:.2} · ∅-corr {:+.2} · mix-entropy {:.2} · ensemble [{}] · {}…", layout.verdict, layout.version, layout.fit.n_completions, layout.fit.n_terms, layout.fit.gini, layout.fit.empty_corr, layout.fit.posterior_entropy, layout.interpreter_versions.join(", "), &layout.input_hash[..12.min(layout.input_hash.len())], ); write!( s, "{}", esc(&frame) ) .unwrap(); s.push_str(""); s }