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