//! The mark (docs/backgrounds.md ยง2): sign in, register and device sign-in. //! The Oxygen mark's two rings drawn in dots, turning opposite ways so the //! stream runs through the middle like the mark's infinity, each with its //! core. Brand only: no data. use crate::field::{unit, Dot, Field, ZOOM}; use crate::scene::Scene; use crate::sky::{hash, Colours}; use std::f32::consts::TAU; /// Dots on the two rings. const DOTS: usize = 1400; /// How fast the rings turn, at their rim, in CSS pixels a second. const RIM_SPEED: f32 = 9.0; struct Bead { ring: usize, angle: f32, /// Its distance off the ring's line, and its breathing's phase. off: f32, phase: f32, } pub struct Mark { field: Field, beads: Vec, w: f32, h: f32, t: f32, } impl Mark { pub fn new(still: bool) -> Mark { let mut field = Field::new(still); let mut beads = Vec::new(); for i in 0..DOTS { let h = hash(&format!("mark#{i}")); let ring = i % 2; // Off the ring's line by a bell-ish spread, most near it. let off = (unit(h, 16) + unit(h, 32) - 1.0) * 18.0; beads.push(Bead { ring, angle: unit(h, 0) * TAU, off, phase: unit(h, 48) * TAU }); 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 }); d.bright = 0.55 + 0.45 * unit(h, 24); d.group = ring as u32; field.dots.push(d); } // Join each ring's beads in the order round it. for ring in 0..2 { let mut idx: Vec = (0..DOTS).filter(|&i| beads[i].ring == ring).collect(); idx.sort_by(|&a, &b| beads[a].angle.total_cmp(&beads[b].angle)); for (k, i) in idx.into_iter().enumerate() { field.dots[i].order = k as u32; } } // The two cores. for _ in 0..2 { field.dots.push(Dot::new(0.0, 0.0, 7.0, 0.0)); } let mut m = Mark { field, beads, w: 0.0, h: 0.0, t: 0.0 }; m.place(); m } /// The rings' radius and centres for this view: large enough to frame /// the page, out into its margins, their centres 1.1 radii either side /// of the middle, as the mark draws them. fn rings(&self) -> (f32, [(f32, f32); 2]) { let r = (self.w * 0.2).min(self.h * 0.42).clamp(110.0, 560.0); let (cx, cy) = (self.w / 2.0, self.h / 2.0); (r, [(cx - r * 1.1, cy), (cx + r * 1.1, cy)]) } fn place(&mut self) { let (r, centres) = self.rings(); let spin = RIM_SPEED / r * self.t; for (i, b) in self.beads.iter().enumerate() { // The left ring turns one way and the right the other. let a = b.angle + if b.ring == 0 { spin } else { -spin }; let rr = r + b.off + 3.0 * (self.t * 0.4 + b.phase).sin(); let (cx, cy) = centres[b.ring]; self.field.dots[i].x = cx + a.cos() * rr; self.field.dots[i].y = cy + a.sin() * rr; } for (k, (cx, cy)) in centres.iter().enumerate() { let d = &mut self.field.dots[DOTS + k]; d.x = *cx; d.y = *cy; } } } impl Scene for Mark { fn resize(&mut self, w: f32, h: f32, _avoid: Option<(f32, f32)>) { self.w = w; self.h = h; self.place(); } fn point(&mut self, at: Option<(f32, f32)>) { self.field.point(at); } fn step(&mut self, dt: f32) { if !self.field.still { self.t += dt.clamp(0.0, 0.1); } self.place(); self.field.light(dt); } fn instances(&self, c: &Colours, out: &mut Vec) { self.field.instances(c, out) } fn links(&self, c: &Colours, out: &mut Vec) { self.field.links(c, out) } fn label(&self) -> Option<(&str, f32, f32)> { None } fn busy(&self) -> bool { self.field.easing() } fn still(&self) -> bool { self.field.still } fn size(&self) -> (f32, f32) { (self.w, self.h) } } #[cfg(test)] mod tests { use super::*; #[test] fn two_rings_turn_opposite_ways_about_their_cores() { let mut m = Mark::new(false); m.resize(1600.0, 900.0, None); let (r, centres) = m.rings(); assert!(centres[0].0 < 800.0 && centres[1].0 > 800.0); for (i, b) in m.beads.iter().enumerate() { let (cx, cy) = centres[b.ring]; let d = (m.field.dots[i].x - cx).hypot(m.field.dots[i].y - cy); assert!((d - r).abs() <= 22.0, "on its ring: {d} against {r}"); } let before = (m.field.dots[0].x, m.field.dots[0].y); m.step(0.1); let moved = (m.field.dots[0].x - before.0).hypot(m.field.dots[0].y - before.1); assert!(moved > 0.0 && moved < 2.0, "slow: {moved} px in a tenth of a second"); assert!(m.label().is_none(), "brand only: nothing to name"); } }