OXYGENHUB
◆ v0.1.4 oxygenhub-android / rust-core / src / backdrop / mark.rs
5.0 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/mark.rs (6dc080f6) by
2
// scripts/sync-backdrop.sh: change it there, never here.
3
//! The mark (docs/backgrounds.md §2): sign in, register and device sign-in.
4
//! The Oxygen mark's two rings drawn in dots, turning opposite ways so the
5
//! stream runs through the middle like the mark's infinity, each with its
6
//! core. Brand only: no data.
7
8
use crate::backdrop::field::{unit, Dot, Field, ZOOM};
9
use crate::backdrop::scene::Scene;
10
use crate::backdrop::sky::{hash, Colours};
11
use std::f32::consts::TAU;
12
13
/// Dots on the two rings.
14
const DOTS: usize = 1400;
15
/// How fast the rings turn, at their rim, in CSS pixels a second.
16
const RIM_SPEED: f32 = 9.0;
17
18
struct Bead {
19
    ring: usize,
20
    angle: f32,
21
    /// Its distance off the ring's line, and its breathing's phase.
22
    off: f32,
23
    phase: f32,
24
}
25
26
pub struct Mark {
27
    field: Field,
28
    beads: Vec<Bead>,
29
    w: f32,
30
    h: f32,
31
    t: f32,
32
}
33
34
impl Mark {
35
    pub fn new(still: bool) -> Mark {
36
        let mut field = Field::new(still);
37
        let mut beads = Vec::new();
38
        for i in 0..DOTS {
39
            let h = hash(&format!("mark#{i}"));
40
            let ring = i % 2;
41
            // Off the ring's line by a bell-ish spread, most near it.
42
            let off = (unit(h, 16) + unit(h, 32) - 1.0) * 18.0;
43
            beads.push(Bead { ring, angle: unit(h, 0) * TAU, off, phase: unit(h, 48) * TAU });
44
            let mut d = Dot::new(0.0, 0.0, (1.5 + unit(h, 40) * 1.6) * ZOOM, if i % 11 == 0 { 1.0 } else { 0.0 });
45
            d.bright = 0.55 + 0.45 * unit(h, 24);
46
            d.group = ring as u32;
47
            field.dots.push(d);
48
        }
49
        // Join each ring's beads in the order round it.
50
        for ring in 0..2 {
51
            let mut idx: Vec<usize> = (0..DOTS).filter(|&i| beads[i].ring == ring).collect();
52
            idx.sort_by(|&a, &b| beads[a].angle.total_cmp(&beads[b].angle));
53
            for (k, i) in idx.into_iter().enumerate() {
54
                field.dots[i].order = k as u32;
55
            }
56
        }
57
        // The two cores.
58
        for _ in 0..2 {
59
            field.dots.push(Dot::new(0.0, 0.0, 7.0, 0.0));
60
        }
61
        let mut m = Mark { field, beads, w: 0.0, h: 0.0, t: 0.0 };
62
        m.place();
63
        m
64
    }
65
66
    /// The rings' radius and centres for this view: large enough to frame
67
    /// the page, out into its margins, their centres 1.1 radii either side
68
    /// of the middle, as the mark draws them.
69
    fn rings(&self) -> (f32, [(f32, f32); 2]) {
70
        let r = (self.w * 0.2).min(self.h * 0.42).clamp(110.0, 560.0);
71
        let (cx, cy) = (self.w / 2.0, self.h / 2.0);
72
        (r, [(cx - r * 1.1, cy), (cx + r * 1.1, cy)])
73
    }
74
75
    fn place(&mut self) {
76
        let (r, centres) = self.rings();
77
        let spin = RIM_SPEED / r * self.t;
78
        for (i, b) in self.beads.iter().enumerate() {
79
            // The left ring turns one way and the right the other.
80
            let a = b.angle + if b.ring == 0 { spin } else { -spin };
81
            let rr = r + b.off + 3.0 * (self.t * 0.4 + b.phase).sin();
82
            let (cx, cy) = centres[b.ring];
83
            self.field.dots[i].x = cx + a.cos() * rr;
84
            self.field.dots[i].y = cy + a.sin() * rr;
85
        }
86
        for (k, (cx, cy)) in centres.iter().enumerate() {
87
            let d = &mut self.field.dots[DOTS + k];
88
            d.x = *cx;
89
            d.y = *cy;
90
        }
91
    }
92
}
93
94
impl Scene for Mark {
95
    fn resize(&mut self, w: f32, h: f32, _avoid: Option<(f32, f32)>) {
96
        self.w = w;
97
        self.h = h;
98
        self.place();
99
    }
100
    fn point(&mut self, at: Option<(f32, f32)>) {
101
        self.field.point(at);
102
    }
103
    fn step(&mut self, dt: f32) {
104
        if !self.field.still {
105
            self.t += dt.clamp(0.0, 0.1);
106
        }
107
        self.place();
108
        self.field.light(dt);
109
    }
110
    fn instances(&self, c: &Colours, out: &mut Vec<f32>) {
111
        self.field.instances(c, out)
112
    }
113
    fn links(&self, c: &Colours, out: &mut Vec<f32>) {
114
        self.field.links(c, out)
115
    }
116
    fn label(&self) -> Option<(&str, f32, f32)> {
117
        None
118
    }
119
    fn busy(&self) -> bool {
120
        self.field.easing()
121
    }
122
    fn still(&self) -> bool {
123
        self.field.still
124
    }
125
    fn size(&self) -> (f32, f32) {
126
        (self.w, self.h)
127
    }
128
}
129
130
#[cfg(test)]
131
mod tests {
132
    use super::*;
133
134
    #[test]
135
    fn two_rings_turn_opposite_ways_about_their_cores() {
136
        let mut m = Mark::new(false);
137
        m.resize(1600.0, 900.0, None);
138
        let (r, centres) = m.rings();
139
        assert!(centres[0].0 < 800.0 && centres[1].0 > 800.0);
140
        for (i, b) in m.beads.iter().enumerate() {
141
            let (cx, cy) = centres[b.ring];
142
            let d = (m.field.dots[i].x - cx).hypot(m.field.dots[i].y - cy);
143
            assert!((d - r).abs() <= 22.0, "on its ring: {d} against {r}");
144
        }
145
        let before = (m.field.dots[0].x, m.field.dots[0].y);
146
        m.step(0.1);
147
        let moved = (m.field.dots[0].x - before.0).hypot(m.field.dots[0].y - before.1);
148
        assert!(moved > 0.0 && moved < 2.0, "slow: {moved} px in a tenth of a second");
149
        assert!(m.label().is_none(), "brand only: nothing to name");
150
    }
151
}