//! The swarm (docs/backgrounds.md §2): Admin › Runners › Images. The hub //! and the runners as nodes, each ringed by a dot for every image it //! holds; while a runner fetches an image, chunks (small squares) stream //! to it from every holder, the hub and its peers alike, as Jetstream's //! swarm fetches them. The data is the page's own snapshot. use crate::field::{knot, margins, Dot, Field, Named, Orbit, ZOOM}; use crate::scene::Scene; use crate::sky::Colours; use std::f32::consts::PI; /// Dots for each image a node holds (its chunks, drawn as a sky's knot). const PER_IMAGE: usize = 6; /// Chunks streaming along each holder-to-fetcher path, their speed, and /// the most streams drawn. const CHUNKS: usize = 20; const CHUNK_SPEED: f32 = 40.0; const MAX_STREAMS: usize = 40; /// One image: the nodes that hold it and those fetching it now. pub struct Image { pub held: Vec, pub fetching: Vec, } struct Held { node: usize, orbit: Orbit, } pub struct Swarm { field: Field, /// The hub first, then the runners. names: Vec, pos: Vec<(f32, f32)>, held: Vec, /// Streams: from, to, and their chunks' first index. streams: Vec<(usize, usize, usize)>, w: f32, h: f32, t: f32, } impl Swarm { pub fn new(names: Vec, images: Vec, still: bool) -> Swarm { let mut field = Field::new(still); for i in 0..names.len() { field.dots.push(Dot::new(0.0, 0.0, if i == 0 { 10.0 } else { 8.0 }, 4.0)); } let mut held = Vec::new(); let mut count = vec![0usize; names.len()]; for img in &images { for &node in img.held.iter().filter(|&&n| n < names.len()) { count[node] += 1; } } for (node, name) in names.iter().enumerate() { for (k, orbit) in knot(name, count[node] * PER_IMAGE).into_iter().enumerate() { let mut d = Dot::new(0.0, 0.0, 2.0 * ZOOM, if k % PER_IMAGE == 0 { 5.0 } else { 0.0 }); d.group = node as u32; d.order = k as u32; d.bright = 0.75; field.dots.push(d); held.push(Held { node, orbit }); } } let mut streams = Vec::new(); 'images: for img in &images { for &to in img.fetching.iter().filter(|&&n| n < names.len()) { for &from in img.held.iter().filter(|&&n| n < names.len() && n != to) { if streams.len() == MAX_STREAMS { break 'images; } streams.push((from, to, field.dots.len())); for _ in 0..CHUNKS { let mut d = Dot::new(0.0, 0.0, 1.8 * ZOOM, 5.0); d.bright = 0.0; field.dots.push(d); } } } } let mut s = Swarm { field, names, pos: Vec::new(), held, streams, w: 0.0, h: 0.0, t: 0.0 }; s.layout(None); s } fn layout(&mut self, avoid: Option<(f32, f32)>) { let (w, h) = (self.w, self.h); let runners = self.names.len().saturating_sub(1); let (hub_x, run_x) = match margins(w, avoid) { Some((l, x1)) => (l * 0.5, x1 + (w - x1) * 0.5), None => (w * 0.1, w * 0.9), }; self.pos = (0..self.names.len()) .map(|i| match i { 0 => (hub_x, h * 0.5), _ if runners <= 1 => (run_x, h * 0.5), i => (run_x, h * 0.2 + h * 0.6 * (i - 1) as f32 / (runners - 1) as f32), }) .collect(); self.field.named = self .names .iter() .zip(&self.pos) .enumerate() .map(|(i, (name, &(x, y)))| { let reach = self.held.iter().filter(|h| h.node == i).map(|h| h.orbit.radius).fold(12.0, f32::max); Named { name: name.clone(), x, y, reach } }) .collect(); } fn place(&mut self) { let n = self.names.len(); for (i, &(x, y)) in self.pos.iter().enumerate() { self.field.dots[i].x = x; self.field.dots[i].y = y; } for (k, hd) in self.held.iter().enumerate() { let (x, y) = hd.orbit.at(self.pos[hd.node], self.t); self.field.dots[n + k].x = x; self.field.dots[n + k].y = y; } 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); // Peers' streams bow one way, the hub's the other, so they // read as separate sources. let bow = if from == 0 { -0.12 } else { 0.12 }; let ctrl = ((a.0 + b.0) / 2.0, (a.1 + b.1) / 2.0 + len * bow); for k in 0..CHUNKS { let s = (k as f32 / CHUNKS as f32 + self.t * CHUNK_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(); } } } } impl Scene for Swarm { 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::*; #[test] fn chunks_stream_to_a_fetching_runner_from_every_holder() { let names = vec!["hub".to_string(), "earth-1".to_string(), "plex-1".to_string()]; let images = vec![Image { held: vec![0, 1], fetching: vec![2] }, Image { held: vec![0, 1, 2], fetching: vec![] }]; let mut s = Swarm::new(names, images, false); s.resize(2560.0, 1270.0, Some((707.0, 1838.0))); assert!(s.pos[0].0 < 707.0 && s.pos[1].0 > 1838.0 && s.pos[2].0 > 1838.0, "the hub on one side, runners on the other"); assert_eq!(s.held.len(), 5 * PER_IMAGE, "dots for every image each node holds"); assert_eq!(s.streams.iter().map(|&(a, b, _)| (a, b)).collect::>(), vec![(0, 2), (1, 2)], "from the hub and the peer"); let first = s.streams[0].2; let before = s.field.dots[first].x; s.step(0.1); assert_ne!(before, s.field.dots[first].x); s.point(Some(s.pos[1])); for _ in 0..30 { s.step(0.02); } assert_eq!(s.label().map(|l| l.0), Some("earth-1")); } }