Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
8.2 kB · 211 lines
Rust
at dev
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212//! 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<String>, /// 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#"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 {SIZE} {SIZE}' width='{w}' height='{h}' role='img' aria-label='Constellation of {title}'>"#, title = esc(&opts.title) ) .unwrap();
// Night sky background with a subtle radial falloff. s.push_str(concat!( "<defs>", "<radialGradient id='sky' cx='50%' cy='50%' r='70%'>", "<stop offset='0%' stop-color='#0b1026'/><stop offset='100%' stop-color='#05060f'/>", "</radialGradient>", "<filter id='soft'><feGaussianBlur stdDeviation='1.2'/></filter>", "<filter id='shimmer'><feGaussianBlur stdDeviation='3.5'/></filter>", "</defs>", )); write!(s, "<rect width='{SIZE}' height='{SIZE}' fill='url(#sky)'/>").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("<g class='edges'>"); 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, "<line x1='{x1:.1}' y1='{y1:.1}' x2='{x2:.1}' y2='{y2:.1}' stroke='{color}' stroke-width='0.8' opacity='{opacity:.2}'/>" ) .unwrap(); } s.push_str("</g>");
// ---- 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("<g class='stars'>"); 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!( "<g class='dark-star' data-term='{term}'>", "<circle cx='{x:.1}' cy='{y:.1}' r='{ring:.1}' fill='none' stroke='#c9d4f0' stroke-width='1.2' opacity='0.85' stroke-dasharray='3 2'/>", "<circle cx='{x:.1}' cy='{y:.1}' r='{r:.1}' fill='#05060f' stroke='#c9d4f0' stroke-width='1.5'/>", "<title>{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.</title>", "</g>" ), 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!( "<circle cx='{x1:.1}' cy='{y:.1}' r='{sr:.1}' fill='#c96a6a' opacity='{o:.2}' filter='url(#shimmer)'/>", "<circle cx='{x2:.1}' cy='{y:.1}' r='{sr:.1}' fill='#6a9ac9' opacity='{o:.2}' filter='url(#shimmer)'/>" ), 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!( "<g class='star' data-term='{term}'>", "<circle cx='{x:.1}' cy='{y:.1}' r='{glow:.1}' fill='{color}' opacity='0.25' filter='url(#soft)'/>", "<circle cx='{x:.1}' cy='{y:.1}' r='{r:.1}' fill='{color}'{stroke}/>", "<title>{term}</title>", "</g>" ), 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, "<text x='{x:.1}' y='{y:.1}' font-family='Georgia,serif' font-size='{size}' fill='#aab6d8' opacity='0.9'>{term}</text>", x = n.x + r + 4.0, y = n.y + 4.0, term = term, ) .unwrap(); } } s.push_str("</g>");
// ---- 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, "<text x='500' y='60' text-anchor='middle' font-family='Georgia,serif' font-size='19' fill='#8f9bbd' opacity='0.95'>{}</text>", 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, "<text x='500' y='985' text-anchor='middle' font-family='ui-monospace,monospace' font-size='12' fill='#5d6784'>{}</text>", esc(&frame) ) .unwrap();
s.push_str("</svg>"); s}