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