// Copied from OxygenHub crates/backdrop/src/ripples.rs (6dc080f6) by // scripts/sync-backdrop.sh: change it there, never here. //! Ripples (docs/backgrounds.md ยง2): a repository's Diffusion tab. The //! repository in one margin, the upstreams it takes from beside it and the //! consumers it diffuses to in the other; its changes flow out along arcs //! to its consumers and its upstreams' flow in, each stream and node in //! the status of its Diffusion pill, and a ripple widens from the //! repository every few seconds. The data is the page's own. use crate::backdrop::field::{knot, margins, unit, Dot, Field, Named, Orbit, Tone, ZOOM}; use crate::backdrop::scene::Scene; use crate::backdrop::sky::Colours; use std::f32::consts::{PI, TAU}; /// Dots in the repository's knot and in each other's, as dense as a sky's /// clusters. const KNOT_REPO: usize = 90; const KNOT: usize = 60; /// Dots flowing along each stream, and their speed in CSS pixels a second: /// a stream is a flow, quicker than the drift of marks. const FLOW: usize = 28; const FLOW_SPEED: f32 = 40.0; /// A ripple every this many seconds, widening this far over this long. const RIPPLE_EVERY: f32 = 5.0; const RIPPLE_R: f32 = 280.0; const RIPPLE_S: f32 = 3.2; const RIPPLE_DOTS: usize = 72; /// One repository on the page: its name and the tone its dots take. pub struct Node { pub name: String, pub tone: Tone, } pub struct Ripples { field: Field, /// The repository first, then its upstreams, then its consumers. nodes: Vec, ups: usize, pos: Vec<(f32, f32)>, /// Each knot's dots: its node, and its orbit. orbits: Vec<(usize, Orbit)>, /// Streams: from, to, and their dots' first index. streams: Vec<(usize, usize, usize)>, ripple_at: usize, w: f32, h: f32, t: f32, } impl Ripples { pub fn new(repo: String, upstreams: Vec, consumers: Vec, still: bool) -> Ripples { let ups = upstreams.len(); let mut nodes = vec![Node { name: repo, tone: Tone::Mark }]; nodes.extend(upstreams); nodes.extend(consumers); let mut field = Field::new(still); let mut orbits = Vec::new(); for (i, n) in nodes.iter().enumerate() { let mut core = Dot::new(0.0, 0.0, if i == 0 { 9.0 } else { 7.0 }, 4.0); core.tone = n.tone; field.dots.push(core); } for (i, n) in nodes.iter().enumerate() { for (k, o) in knot(&n.name, if i == 0 { KNOT_REPO } else { KNOT }).into_iter().enumerate() { let mut d = Dot::new(0.0, 0.0, 2.0 * ZOOM, if k % 9 == 0 { 1.0 } else { 0.0 }); d.tone = n.tone; d.group = i as u32; d.order = k as u32; d.bright = 0.55 + 0.45 * unit(crate::backdrop::sky::hash(&format!("{}@{k}", n.name)), 0); field.dots.push(d); orbits.push((i, o)); } } // Upstreams flow into the repository; the repository flows out to // each consumer, the stream in the consumer's tone. let mut streams = Vec::new(); for (i, node) in nodes.iter().enumerate() { let (from, to) = match i { 0 => continue, i if i <= ups => (i, 0), i => (0, i), }; streams.push((from, to, field.dots.len())); for _ in 0..FLOW { let mut d = Dot::new(0.0, 0.0, 1.8 * ZOOM, 0.0); d.tone = node.tone; field.dots.push(d); } } let ripple_at = field.dots.len(); for _ in 0..RIPPLE_DOTS { let mut d = Dot::new(0.0, 0.0, 1.6 * ZOOM, 0.0); d.bright = 0.0; field.dots.push(d); } let mut r = Ripples { field, nodes, ups, pos: Vec::new(), orbits, streams, ripple_at, w: 0.0, h: 0.0, t: 0.0 }; r.layout(None); r } fn layout(&mut self, avoid: Option<(f32, f32)>) { let (w, h) = (self.w, self.h); let n = self.nodes.len(); let cons = n - 1 - self.ups; let spread = |i: usize, of: usize, top: f32, bottom: f32| { if of <= 1 { (top + bottom) / 2.0 } else { top + (bottom - top) * i as f32 / (of - 1) as f32 } }; let (repo_x, up_x, con_x) = match margins(w, avoid) { Some((l, x1)) => (l * 0.62, l * 0.22, x1 + (w - x1) * 0.45), None => (w * 0.12, w * 0.04, w * 0.88), }; self.pos = (0..n) .map(|i| match i { 0 => (repo_x, h * 0.5), i if i <= self.ups => (up_x, spread(i - 1, self.ups, h * 0.25, h * 0.75)), i => (con_x, spread(i - 1 - self.ups, cons, h * 0.15, h * 0.85)), }) .collect(); self.field.named = self.nodes.iter().zip(&self.pos).map(|(nd, &(x, y))| Named { name: nd.name.clone(), x, y, reach: 90.0 }).collect(); } fn place(&mut self) { let n = self.nodes.len(); for (i, &(x, y)) in self.pos.iter().enumerate() { self.field.dots[i].x = x; self.field.dots[i].y = y; } for (k, (node, o)) in self.orbits.iter().enumerate() { let (x, y) = o.at(self.pos[*node], self.t); self.field.dots[n + k].x = x; self.field.dots[n + k].y = y; } // Streams: a gentle arc from each source to its destination, its // dots evenly spaced and moving along it, faded at the ends. for &(from, to, first) in &self.streams { let (a, b) = (self.pos[from], self.pos[to]); let len = (b.0 - a.0).hypot(b.1 - a.1).max(1.0); let ctrl = ((a.0 + b.0) / 2.0, (a.1 + b.1) / 2.0 - len * 0.18); for k in 0..FLOW { let s = (k as f32 / FLOW as f32 + self.t * FLOW_SPEED / len).fract(); let u = 1.0 - s; let d = &mut self.field.dots[first + k]; d.x = u * u * a.0 + 2.0 * u * s * ctrl.0 + s * s * b.0; d.y = u * u * a.1 + 2.0 * u * s * ctrl.1 + s * s * b.1; d.bright = (s * PI).sin(); } } // The ripple: a ring of dots widening from the repository. let p = (self.t % RIPPLE_EVERY) / RIPPLE_S; let (x, y) = self.pos[0]; for k in 0..RIPPLE_DOTS { let d = &mut self.field.dots[self.ripple_at + k]; let a = k as f32 / RIPPLE_DOTS as f32 * TAU; let r = 40.0 + RIPPLE_R * p.min(1.0); d.x = x + a.cos() * r; d.y = y + a.sin() * r; d.bright = if self.field.still || p >= 1.0 { 0.0 } else { 0.8 * (1.0 - p) }; } } } impl Scene for Ripples { fn resize(&mut self, w: f32, h: f32, avoid: Option<(f32, f32)>) { self.w = w; self.h = h; self.layout(avoid); self.place(); } fn point(&mut self, at: Option<(f32, f32)>) { self.field.point(at); } fn step(&mut self, dt: f32) { if !self.field.still { self.t += dt.clamp(0.0, 0.1); } self.place(); self.field.light(dt); } fn instances(&self, c: &Colours, out: &mut Vec) { self.field.instances(c, out) } fn links(&self, c: &Colours, out: &mut Vec) { self.field.links(c, out) } fn label(&self) -> Option<(&str, f32, f32)> { self.field.label() } fn busy(&self) -> bool { self.field.easing() } fn still(&self) -> bool { self.field.still } fn size(&self) -> (f32, f32) { (self.w, self.h) } } #[cfg(test)] mod tests { use super::*; fn node(name: &str, tone: Tone) -> Node { Node { name: name.into(), tone } } #[test] fn upstreams_flow_in_and_consumers_out_in_their_pills_tones() { let mut r = Ripples::new( "andy/api".into(), vec![node("andy/base", Tone::Ok)], vec![node("andy/game", Tone::Ok), node("andy/other", Tone::Bad)], false, ); r.resize(2560.0, 1270.0, Some((707.0, 1838.0))); assert!(r.pos[0].0 < 707.0 && r.pos[1].0 < r.pos[0].0, "the repository and its upstream in the left margin"); assert!(r.pos[2].0 > 1838.0 && r.pos[3].0 > 1838.0, "its consumers in the right"); let knots = 4 + KNOT_REPO + 3 * KNOT; assert_eq!(r.streams, vec![(1, 0, knots), (0, 2, knots + FLOW), (0, 3, knots + 2 * FLOW)]); let bad_stream = r.streams[2].2; assert!(r.field.dots[bad_stream..bad_stream + FLOW].iter().all(|d| d.tone == Tone::Bad), "a failing consumer's stream is red"); let before = r.field.dots[bad_stream].x; r.step(0.1); assert_ne!(before, r.field.dots[bad_stream].x, "streams flow"); r.point(Some(r.pos[3])); for _ in 0..30 { r.step(0.02); } assert_eq!(r.label().map(|l| l.0), Some("andy/other")); } #[test] fn still_keeps_the_ripple_away() { let mut r = Ripples::new("andy/api".into(), vec![], vec![node("andy/game", Tone::Mark)], true); r.resize(1600.0, 900.0, None); r.step(0.1); assert!(r.field.dots[r.ripple_at..].iter().all(|d| d.bright == 0.0)); } }