| 1dfa6314 Andy McCall ↶ |
1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
|
| 7 |
|
| 8 |
|
| 9 |
|
| 10 |
use crate::backdrop::field::{knot, margins, unit, Dot, Field, Named, Orbit, Tone, ZOOM}; |
| 11 |
use crate::backdrop::scene::Scene; |
| 12 |
use crate::backdrop::sky::Colours; |
| 13 |
use std::f32::consts::{PI, TAU}; |
| 14 |
|
| 15 |
|
| 16 |
|
| 17 |
const KNOT_REPO: usize = 90; |
| 18 |
const KNOT: usize = 60; |
| 19 |
|
| 20 |
|
| 21 |
const FLOW: usize = 28; |
| 22 |
const FLOW_SPEED: f32 = 40.0; |
| 23 |
|
| 24 |
const RIPPLE_EVERY: f32 = 5.0; |
| 25 |
const RIPPLE_R: f32 = 280.0; |
| 26 |
const RIPPLE_S: f32 = 3.2; |
| 27 |
const RIPPLE_DOTS: usize = 72; |
| 28 |
|
| 29 |
|
| 30 |
pub struct Node { |
| 31 |
pub name: String, |
| 32 |
pub tone: Tone, |
| 33 |
} |
| 34 |
|
| 35 |
pub struct Ripples { |
| 36 |
field: Field, |
| 37 |
|
| 38 |
nodes: Vec<Node>, |
| 39 |
ups: usize, |
| 40 |
pos: Vec<(f32, f32)>, |
| 41 |
|
| 42 |
orbits: Vec<(usize, Orbit)>, |
| 43 |
|
| 44 |
streams: Vec<(usize, usize, usize)>, |
| 45 |
ripple_at: usize, |
| 46 |
w: f32, |
| 47 |
h: f32, |
| 48 |
t: f32, |
| 49 |
} |
| 50 |
|
| 51 |
impl Ripples { |
| 52 |
pub fn new(repo: String, upstreams: Vec<Node>, consumers: Vec<Node>, still: bool) -> Ripples { |
| 53 |
let ups = upstreams.len(); |
| 54 |
let mut nodes = vec![Node { name: repo, tone: Tone::Mark }]; |
| 55 |
nodes.extend(upstreams); |
| 56 |
nodes.extend(consumers); |
| 57 |
let mut field = Field::new(still); |
| 58 |
let mut orbits = Vec::new(); |
| 59 |
for (i, n) in nodes.iter().enumerate() { |
| 60 |
let mut core = Dot::new(0.0, 0.0, if i == 0 { 9.0 } else { 7.0 }, 4.0); |
| 61 |
core.tone = n.tone; |
| 62 |
field.dots.push(core); |
| 63 |
} |
| 64 |
for (i, n) in nodes.iter().enumerate() { |
| 65 |
for (k, o) in knot(&n.name, if i == 0 { KNOT_REPO } else { KNOT }).into_iter().enumerate() { |
| 66 |
let mut d = Dot::new(0.0, 0.0, 2.0 * ZOOM, if k % 9 == 0 { 1.0 } else { 0.0 }); |
| 67 |
d.tone = n.tone; |
| 68 |
d.group = i as u32; |
| 69 |
d.order = k as u32; |
| 70 |
d.bright = 0.55 + 0.45 * unit(crate::backdrop::sky::hash(&format!("{}@{k}", n.name)), 0); |
| 71 |
field.dots.push(d); |
| 72 |
orbits.push((i, o)); |
| 73 |
} |
| 74 |
} |
| 75 |
|
| 76 |
|
| 77 |
let mut streams = Vec::new(); |
| 78 |
for (i, node) in nodes.iter().enumerate() { |
| 79 |
let (from, to) = match i { |
| 80 |
0 => continue, |
| 81 |
i if i <= ups => (i, 0), |
| 82 |
i => (0, i), |
| 83 |
}; |
| 84 |
streams.push((from, to, field.dots.len())); |
| 85 |
for _ in 0..FLOW { |
| 86 |
let mut d = Dot::new(0.0, 0.0, 1.8 * ZOOM, 0.0); |
| 87 |
d.tone = node.tone; |
| 88 |
field.dots.push(d); |
| 89 |
} |
| 90 |
} |
| 91 |
let ripple_at = field.dots.len(); |
| 92 |
for _ in 0..RIPPLE_DOTS { |
| 93 |
let mut d = Dot::new(0.0, 0.0, 1.6 * ZOOM, 0.0); |
| 94 |
d.bright = 0.0; |
| 95 |
field.dots.push(d); |
| 96 |
} |
| 97 |
let mut r = Ripples { field, nodes, ups, pos: Vec::new(), orbits, streams, ripple_at, w: 0.0, h: 0.0, t: 0.0 }; |
| 98 |
r.layout(None); |
| 99 |
r |
| 100 |
} |
| 101 |
|
| 102 |
fn layout(&mut self, avoid: Option<(f32, f32)>) { |
| 103 |
let (w, h) = (self.w, self.h); |
| 104 |
let n = self.nodes.len(); |
| 105 |
let cons = n - 1 - self.ups; |
| 106 |
let spread = |i: usize, of: usize, top: f32, bottom: f32| { |
| 107 |
if of <= 1 { |
| 108 |
(top + bottom) / 2.0 |
| 109 |
} else { |
| 110 |
top + (bottom - top) * i as f32 / (of - 1) as f32 |
| 111 |
} |
| 112 |
}; |
| 113 |
let (repo_x, up_x, con_x) = match margins(w, avoid) { |
| 114 |
Some((l, x1)) => (l * 0.62, l * 0.22, x1 + (w - x1) * 0.45), |
| 115 |
None => (w * 0.12, w * 0.04, w * 0.88), |
| 116 |
}; |
| 117 |
self.pos = (0..n) |
| 118 |
.map(|i| match i { |
| 119 |
0 => (repo_x, h * 0.5), |
| 120 |
i if i <= self.ups => (up_x, spread(i - 1, self.ups, h * 0.25, h * 0.75)), |
| 121 |
i => (con_x, spread(i - 1 - self.ups, cons, h * 0.15, h * 0.85)), |
| 122 |
}) |
| 123 |
.collect(); |
| 124 |
self.field.named = |
| 125 |
self.nodes.iter().zip(&self.pos).map(|(nd, &(x, y))| Named { name: nd.name.clone(), x, y, reach: 90.0 }).collect(); |
| 126 |
} |
| 127 |
|
| 128 |
fn place(&mut self) { |
| 129 |
let n = self.nodes.len(); |
| 130 |
for (i, &(x, y)) in self.pos.iter().enumerate() { |
| 131 |
self.field.dots[i].x = x; |
| 132 |
self.field.dots[i].y = y; |
| 133 |
} |
| 134 |
for (k, (node, o)) in self.orbits.iter().enumerate() { |
| 135 |
let (x, y) = o.at(self.pos[*node], self.t); |
| 136 |
self.field.dots[n + k].x = x; |
| 137 |
self.field.dots[n + k].y = y; |
| 138 |
} |
| 139 |
|
| 140 |
|
| 141 |
for &(from, to, first) in &self.streams { |
| 142 |
let (a, b) = (self.pos[from], self.pos[to]); |
| 143 |
let len = (b.0 - a.0).hypot(b.1 - a.1).max(1.0); |
| 144 |
let ctrl = ((a.0 + b.0) / 2.0, (a.1 + b.1) / 2.0 - len * 0.18); |
| 145 |
for k in 0..FLOW { |
| 146 |
let s = (k as f32 / FLOW as f32 + self.t * FLOW_SPEED / len).fract(); |
| 147 |
let u = 1.0 - s; |
| 148 |
let d = &mut self.field.dots[first + k]; |
| 149 |
d.x = u * u * a.0 + 2.0 * u * s * ctrl.0 + s * s * b.0; |
| 150 |
d.y = u * u * a.1 + 2.0 * u * s * ctrl.1 + s * s * b.1; |
| 151 |
d.bright = (s * PI).sin(); |
| 152 |
} |
| 153 |
} |
| 154 |
|
| 155 |
let p = (self.t % RIPPLE_EVERY) / RIPPLE_S; |
| 156 |
let (x, y) = self.pos[0]; |
| 157 |
for k in 0..RIPPLE_DOTS { |
| 158 |
let d = &mut self.field.dots[self.ripple_at + k]; |
| 159 |
let a = k as f32 / RIPPLE_DOTS as f32 * TAU; |
| 160 |
let r = 40.0 + RIPPLE_R * p.min(1.0); |
| 161 |
d.x = x + a.cos() * r; |
| 162 |
d.y = y + a.sin() * r; |
| 163 |
d.bright = if self.field.still || p >= 1.0 { 0.0 } else { 0.8 * (1.0 - p) }; |
| 164 |
} |
| 165 |
} |
| 166 |
} |
| 167 |
|
| 168 |
impl Scene for Ripples { |
| 169 |
fn resize(&mut self, w: f32, h: f32, avoid: Option<(f32, f32)>) { |
| 170 |
self.w = w; |
| 171 |
self.h = h; |
| 172 |
self.layout(avoid); |
| 173 |
self.place(); |
| 174 |
} |
| 175 |
fn point(&mut self, at: Option<(f32, f32)>) { |
| 176 |
self.field.point(at); |
| 177 |
} |
| 178 |
fn step(&mut self, dt: f32) { |
| 179 |
if !self.field.still { |
| 180 |
self.t += dt.clamp(0.0, 0.1); |
| 181 |
} |
| 182 |
self.place(); |
| 183 |
self.field.light(dt); |
| 184 |
} |
| 185 |
fn instances(&self, c: &Colours, out: &mut Vec<f32>) { |
| 186 |
self.field.instances(c, out) |
| 187 |
} |
| 188 |
fn links(&self, c: &Colours, out: &mut Vec<f32>) { |
| 189 |
self.field.links(c, out) |
| 190 |
} |
| 191 |
fn label(&self) -> Option<(&str, f32, f32)> { |
| 192 |
self.field.label() |
| 193 |
} |
| 194 |
fn busy(&self) -> bool { |
| 195 |
self.field.easing() |
| 196 |
} |
| 197 |
fn still(&self) -> bool { |
| 198 |
self.field.still |
| 199 |
} |
| 200 |
fn size(&self) -> (f32, f32) { |
| 201 |
(self.w, self.h) |
| 202 |
} |
| 203 |
} |
| 204 |
|
| 205 |
#[cfg(test)] |
| 206 |
mod tests { |
| 207 |
use super::*; |
| 208 |
|
| 209 |
fn node(name: &str, tone: Tone) -> Node { |
| 210 |
Node { name: name.into(), tone } |
| 211 |
} |
| 212 |
|
| 213 |
#[test] |
| 214 |
fn upstreams_flow_in_and_consumers_out_in_their_pills_tones() { |
| 215 |
let mut r = Ripples::new( |
| 216 |
"andy/api".into(), |
| 217 |
vec![node("andy/base", Tone::Ok)], |
| 218 |
vec![node("andy/game", Tone::Ok), node("andy/other", Tone::Bad)], |
| 219 |
false, |
| 220 |
); |
| 221 |
r.resize(2560.0, 1270.0, Some((707.0, 1838.0))); |
| 222 |
assert!(r.pos[0].0 < 707.0 && r.pos[1].0 < r.pos[0].0, "the repository and its upstream in the left margin"); |
| 223 |
assert!(r.pos[2].0 > 1838.0 && r.pos[3].0 > 1838.0, "its consumers in the right"); |
| 224 |
let knots = 4 + KNOT_REPO + 3 * KNOT; |
| 225 |
assert_eq!(r.streams, vec![(1, 0, knots), (0, 2, knots + FLOW), (0, 3, knots + 2 * FLOW)]); |
| 226 |
let bad_stream = r.streams[2].2; |
| 227 |
assert!(r.field.dots[bad_stream..bad_stream + FLOW].iter().all(|d| d.tone == Tone::Bad), "a failing consumer's stream is red"); |
| 228 |
let before = r.field.dots[bad_stream].x; |
| 229 |
r.step(0.1); |
| 230 |
assert_ne!(before, r.field.dots[bad_stream].x, "streams flow"); |
| 231 |
r.point(Some(r.pos[3])); |
| 232 |
for _ in 0..30 { |
| 233 |
r.step(0.02); |
| 234 |
} |
| 235 |
assert_eq!(r.label().map(|l| l.0), Some("andy/other")); |
| 236 |
} |
| 237 |
|
| 238 |
#[test] |
| 239 |
fn still_keeps_the_ripple_away() { |
| 240 |
let mut r = Ripples::new("andy/api".into(), vec![], vec![node("andy/game", Tone::Mark)], true); |
| 241 |
r.resize(1600.0, 900.0, None); |
| 242 |
r.step(0.1); |
| 243 |
assert!(r.field.dots[r.ripple_at..].iter().all(|d| d.bright == 0.0)); |
| 244 |
} |
| 245 |
} |