OXYGENHUB
# fb420bbdb058 oxygenhub-android / rust-core / src / backdrop / ripples.rs
9.1 KiB · Rust · last changed The hub's living backgrounds in the app: the mark at sign-in, Your sky on the dashboard, live runs pulse (refs #23) 8 days ago source blame history raw
1dfa6314 Andy McCall ↶ 1
// Copied from OxygenHub crates/backdrop/src/ripples.rs (6dc080f6) by
2
// scripts/sync-backdrop.sh: change it there, never here.
3
//! Ripples (docs/backgrounds.md §2): a repository's Diffusion tab. The
4
//! repository in one margin, the upstreams it takes from beside it and the
5
//! consumers it diffuses to in the other; its changes flow out along arcs
6
//! to its consumers and its upstreams' flow in, each stream and node in
7
//! the status of its Diffusion pill, and a ripple widens from the
8
//! repository every few seconds. The data is the page's own.
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
/// Dots in the repository's knot and in each other's, as dense as a sky's
16
/// clusters.
17
const KNOT_REPO: usize = 90;
18
const KNOT: usize = 60;
19
/// Dots flowing along each stream, and their speed in CSS pixels a second:
20
/// a stream is a flow, quicker than the drift of marks.
21
const FLOW: usize = 28;
22
const FLOW_SPEED: f32 = 40.0;
23
/// A ripple every this many seconds, widening this far over this long.
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
/// One repository on the page: its name and the tone its dots take.
30
pub struct Node {
31
    pub name: String,
32
    pub tone: Tone,
33
}
34
35
pub struct Ripples {
36
    field: Field,
37
    /// The repository first, then its upstreams, then its consumers.
38
    nodes: Vec<Node>,
39
    ups: usize,
40
    pos: Vec<(f32, f32)>,
41
    /// Each knot's dots: its node, and its orbit.
42
    orbits: Vec<(usize, Orbit)>,
43
    /// Streams: from, to, and their dots' first index.
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
        // Upstreams flow into the repository; the repository flows out to
76
        // each consumer, the stream in the consumer's tone.
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
        // Streams: a gentle arc from each source to its destination, its
140
        // dots evenly spaced and moving along it, faded at the ends.
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
        // The ripple: a ring of dots widening from the repository.
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
}