feat(web): every scene as dense as the sky: knots, fuller streams, 1.4x (refs #167) f993f792537e
Andy: the sky's density is right, the rest should match. Ripples' and the swarm's repositories and runners are knots laid out like a sky's clusters (field::knot: the sky's lanes, sizes and speeds), 90 dots for the repository and 60 for each other, six for each image a node holds; streams carry 28 dots (Diffusion) and 20 chunks (the swarm); the ripple 72; adrift's cluster 160; the mark 900 dots. Every scene's dots are drawn at the sky's 1.4x.
- Author
- Andy McCall @andymccall <[email protected]>
- When
- · 2026-10-13 20:26 UTC
- Signature
- ✔ signed by Andy McCall
- Hash
f993f792537e56020d40e4b598c6c384c78b10e529f307f77ef0d1304d36f68a- Depends on
- 17ca6cddc534 6246d1e51f4f
- Depended on by
- 5501b1c657b2
- In bookmarks
- api-unknown-fields cancel-landed-runs diffusion-pin-bookmark draft-ready main oh-comment-body-file proposal-list-fast
- In tags
- v0.5.0 v0.5.1 v0.5.2 v0.6.0 v0.6.1 v0.7.0 v0.8.0 v0.9.0 v0.9.1 v0.9.11 v0.9.15 v0.9.16 v0.9.19 v0.9.2 v0.9.20 v0.9.21 v0.9.22 v0.9.23 v0.9.24 v0.9.3 v0.9.4 v0.9.5 v0.9.6 v0.9.7 v0.9.8 v0.9.9
- Edits
- 52 atoms, 0 tree operations, 7 files
Browse the tree at this change
modified assets/backdrop/backdrop.js +3 −3
| … | 338 unchanged lines |
||
| 339 | 339 | arg0.viewport(arg1, arg2, arg3, arg4); |
|
| 340 | 340 | }, |
|
| 341 | 341 | __wbindgen_generic_0000000000000001: function(arg0, arg1) { |
|
| 342 | - | // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [F64], shim_idx: 6, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. |
|
| 342 | + | // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [F64], shim_idx: 8, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. |
|
| 343 | 343 | const ret = makeMutClosure(arg0, arg1, wasm_bindgen_ef67fbc5f0a8b406___convert__closures_____invoke___f64______true_); |
|
| 344 | 344 | return ret; |
|
| 345 | 345 | }, |
|
| 346 | 346 | __wbindgen_generic_0000000000000002: function(arg0, arg1) { |
|
| 347 | - | // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("MouseEvent")], shim_idx: 5, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. |
|
| 347 | + | // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("MouseEvent")], shim_idx: 7, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. |
|
| 348 | 348 | const ret = makeMutClosure(arg0, arg1, wasm_bindgen_ef67fbc5f0a8b406___convert__closures_____invoke___web_sys_793942649ef82f98___features__gen_MouseEvent__MouseEvent______true_); |
|
| 349 | 349 | return ret; |
|
| 350 | 350 | }, |
|
| 351 | 351 | __wbindgen_generic_0000000000000003: function(arg0, arg1) { |
|
| 352 | - | // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 4, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. |
|
| 352 | + | // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 6, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. |
|
| 353 | 353 | const ret = makeMutClosure(arg0, arg1, wasm_bindgen_ef67fbc5f0a8b406___convert__closures_____invoke_______true_); |
|
| 354 | 354 | return ret; |
|
| 355 | 355 | }, |
|
| … | 292 unchanged lines |
replaced assets/backdrop/backdrop_bg.wasm +0 −0
No line changes.
modified crates/backdrop/src/adrift.rs +11 −20
| … | 2 unchanged lines |
||
| 3 | 3 | //! the page drifted from what was asked for. It drifts for a minute, fades |
|
| 4 | 4 | //! and sets out again. No data. |
|
| 5 | 5 | ||
| 6 | - | use crate::field::{margins, unit, Dot, Field}; |
|
| 6 | + | use crate::field::{knot, margins, unit, Dot, Field, Orbit, ZOOM}; |
|
| 7 | 7 | use crate::scene::Scene; |
|
| 8 | - | use crate::sky::{hash, Colours, MAX_SPEED}; |
|
| 9 | - | use std::f32::consts::TAU; |
|
| 8 | + | use crate::sky::{hash, Colours}; |
|
| 10 | 9 | ||
| 11 | - | const CLUSTER: usize = 70; |
|
| 10 | + | const CLUSTER: usize = 160; |
|
| 12 | 11 | /// The lost mark's speed, and how long one drift lasts, fade included. |
|
| 13 | 12 | const DRIFT_SPEED: f32 = 7.0; |
|
| 14 | 13 | const DRIFT_S: f32 = 70.0; |
|
| 15 | 14 | const FADE_S: f32 = 5.0; |
|
| 16 | 15 | ||
| 17 | - | struct Orbit { |
|
| 18 | - | radius: f32, |
|
| 19 | - | angle: f32, |
|
| 20 | - | speed: f32, |
|
| 21 | - | } |
|
| 22 | - | ||
| 23 | 16 | pub struct Adrift { |
|
| 24 | 17 | field: Field, |
|
| 25 | 18 | orbits: Vec<Orbit>, |
|
| … | 6 unchanged lines |
||
| 32 | 25 | impl Adrift { |
|
| 33 | 26 | pub fn new(still: bool) -> Adrift { |
|
| 34 | 27 | let mut field = Field::new(still); |
|
| 35 | - | let mut orbits = Vec::new(); |
|
| 28 | + | let orbits = knot("adrift", CLUSTER); |
|
| 36 | 29 | for k in 0..CLUSTER { |
|
| 37 | - | let h = hash(&format!("adrift#{k}")); |
|
| 38 | - | let radius = (14.0 + (k % 8) as f32 * 6.5 + unit(h, 0) * 4.0) * 1.4; |
|
| 39 | - | orbits.push(Orbit { radius, angle: unit(h, 16) * TAU, speed: (MAX_SPEED / radius).min(0.25) * (0.5 + 0.5 * unit(h, 32)) }); |
|
| 40 | - | let mut d = Dot::new(0.0, 0.0, 2.6, if k % 7 == 0 { 2.0 } else { 0.0 }); |
|
| 30 | + | let h = hash(&format!("adrift@{k}")); |
|
| 31 | + | let mut d = Dot::new(0.0, 0.0, 2.0 * ZOOM, if k % 7 == 0 { 2.0 } else { 0.0 }); |
|
| 41 | 32 | d.bright = 0.5 + 0.5 * unit(h, 48); |
|
| 42 | 33 | d.group = 0; |
|
| 43 | 34 | d.order = k as u32; |
|
| … | 1 unchanged lines |
||
| 45 | 36 | } |
|
| 46 | 37 | // The cluster's core, and the lost mark, a little larger. |
|
| 47 | 38 | field.dots.push(Dot::new(0.0, 0.0, 8.0, 4.0)); |
|
| 48 | - | let mut lost = Dot::new(0.0, 0.0, 4.2, 0.0); |
|
| 39 | + | let mut lost = Dot::new(0.0, 0.0, 3.4 * ZOOM, 0.0); |
|
| 49 | 40 | lost.bright = 1.0; |
|
| 50 | 41 | field.dots.push(lost); |
|
| 51 | 42 | let mut a = Adrift { field, orbits, centre: (0.0, 0.0), w: 0.0, h: 0.0, t: 12.0 }; |
|
| … | 4 unchanged lines |
||
| 56 | 47 | fn place(&mut self) { |
|
| 57 | 48 | let (cx, cy) = self.centre; |
|
| 58 | 49 | for (i, o) in self.orbits.iter().enumerate() { |
|
| 59 | - | let a = o.angle + o.speed * self.t; |
|
| 60 | - | self.field.dots[i].x = cx + a.cos() * o.radius; |
|
| 61 | - | self.field.dots[i].y = cy + a.sin() * o.radius; |
|
| 50 | + | let (x, y) = o.at((cx, cy), self.t); |
|
| 51 | + | self.field.dots[i].x = x; |
|
| 52 | + | self.field.dots[i].y = y; |
|
| 62 | 53 | } |
|
| 63 | 54 | let core = &mut self.field.dots[CLUSTER]; |
|
| 64 | 55 | core.x = cx; |
|
| … | 2 unchanged lines |
||
| 67 | 58 | // each drift and setting out again from the cluster's edge. |
|
| 68 | 59 | let p = self.t % DRIFT_S; |
|
| 69 | 60 | let dir: f32 = -0.35; |
|
| 70 | - | let from = 70.0 * 1.4; |
|
| 61 | + | let from = self.orbits.iter().map(|o| o.radius).fold(0.0, f32::max) + 14.0; |
|
| 71 | 62 | let lost = &mut self.field.dots[CLUSTER + 1]; |
|
| 72 | 63 | lost.x = cx + dir.cos() * (from + p * DRIFT_SPEED); |
|
| 73 | 64 | lost.y = cy + dir.sin() * (from + p * DRIFT_SPEED); |
|
| … | 68 unchanged lines |
modified crates/backdrop/src/field.rs +35 −0
| … | 195 unchanged lines |
||
| 196 | 196 | ((h >> shift) & 0xffff) as f32 / 65536.0 |
|
| 197 | 197 | } |
|
| 198 | 198 | ||
| 199 | + | /// One dot of a knot: its orbit about the knot's centre. |
|
| 200 | + | #[derive(Clone, Copy, Debug)] |
|
| 201 | + | pub struct Orbit { |
|
| 202 | + | pub radius: f32, |
|
| 203 | + | pub angle: f32, |
|
| 204 | + | /// Radians a second, signed. |
|
| 205 | + | pub speed: f32, |
|
| 206 | + | } |
|
| 207 | + | ||
| 208 | + | impl Orbit { |
|
| 209 | + | pub fn at(&self, centre: (f32, f32), t: f32) -> (f32, f32) { |
|
| 210 | + | let a = self.angle + self.speed * t; |
|
| 211 | + | (centre.0 + a.cos() * self.radius, centre.1 + a.sin() * self.radius) |
|
| 212 | + | } |
|
| 213 | + | } |
|
| 214 | + | ||
| 215 | + | /// `n` dots as a knot like a sky's cluster (sky::Sky, Andy's 2026-10-13 |
|
| 216 | + | /// tuning): close lanes, more of them the busier, drawn 1.4 times larger, |
|
| 217 | + | /// turning one way at the sky's slow speed. `seed` makes it stable. |
|
| 218 | + | pub fn knot(seed: &str, n: usize) -> Vec<Orbit> { |
|
| 219 | + | let lanes = (6 + n / 30).min(14) as u32; |
|
| 220 | + | let dir = if crate::sky::hash(seed) & 1 == 0 { 1.0 } else { -1.0 }; |
|
| 221 | + | (0..n as u32) |
|
| 222 | + | .map(|k| { |
|
| 223 | + | let h = crate::sky::hash(&format!("{seed}#{k}")); |
|
| 224 | + | let radius = (16.0 + (k % lanes) as f32 * 6.5 + unit(h, 0) * 4.0) * 1.4; |
|
| 225 | + | let speed = dir * (crate::sky::MAX_SPEED / radius).min(0.25) * (0.5 + 0.5 * unit(h, 16)); |
|
| 226 | + | Orbit { radius, angle: unit(h, 32) * std::f32::consts::TAU, speed } |
|
| 227 | + | }) |
|
| 228 | + | .collect() |
|
| 229 | + | } |
|
| 230 | + | ||
| 231 | + | /// A dot's size drawn as the sky draws its marks (1.4 times larger). |
|
| 232 | + | pub const ZOOM: f32 = 1.4; |
|
| 233 | + | ||
| 199 | 234 | /// The page's margins beside its content column, when both are wide |
|
| 200 | 235 | /// enough to hold something: (left width, right start). |
|
| 201 | 236 | pub fn margins(w: f32, avoid: Option<(f32, f32)>) -> Option<(f32, f32)> { |
|
| … | 44 unchanged lines |
modified crates/backdrop/src/mark.rs +3 −3
| … | 2 unchanged lines |
||
| 3 | 3 | //! stream runs through the middle like the mark's infinity, each with its |
|
| 4 | 4 | //! core. Brand only: no data. |
|
| 5 | 5 | ||
| 6 | - | use crate::field::{unit, Dot, Field}; |
|
| 6 | + | use crate::field::{unit, Dot, Field, ZOOM}; |
|
| 7 | 7 | use crate::scene::Scene; |
|
| 8 | 8 | use crate::sky::{hash, Colours}; |
|
| 9 | 9 | use std::f32::consts::TAU; |
|
| 10 | 10 | ||
| 11 | 11 | /// Dots on the two rings. |
|
| 12 | - | const DOTS: usize = 560; |
|
| 12 | + | const DOTS: usize = 900; |
|
| 13 | 13 | /// How fast the rings turn, at their rim, in CSS pixels a second. |
|
| 14 | 14 | const RIM_SPEED: f32 = 9.0; |
|
| 15 | 15 | ||
| … | 23 unchanged lines |
||
| 39 | 39 | // Off the ring's line by a bell-ish spread, most near it. |
|
| 40 | 40 | let off = (unit(h, 16) + unit(h, 32) - 1.0) * 18.0; |
|
| 41 | 41 | beads.push(Bead { ring, angle: unit(h, 0) * TAU, off, phase: unit(h, 48) * TAU }); |
|
| 42 | - | let mut d = Dot::new(0.0, 0.0, 1.5 + unit(h, 40) * 1.6, if i % 11 == 0 { 1.0 } else { 0.0 }); |
|
| 42 | + | 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 }); |
|
| 43 | 43 | d.bright = 0.55 + 0.45 * unit(h, 24); |
|
| 44 | 44 | d.group = ring as u32; |
|
| 45 | 45 | field.dots.push(d); |
|
| … | 102 unchanged lines |
modified crates/backdrop/src/ripples.rs +32 −28
| … | 4 unchanged lines |
||
| 5 | 5 | //! the status of its Diffusion pill, and a ripple widens from the |
|
| 6 | 6 | //! repository every few seconds. The data is the page's own. |
|
| 7 | 7 | ||
| 8 | - | use crate::field::{margins, unit, Dot, Field, Named, Tone}; |
|
| 8 | + | use crate::field::{knot, margins, unit, Dot, Field, Named, Orbit, Tone, ZOOM}; |
|
| 9 | 9 | use crate::scene::Scene; |
|
| 10 | - | use crate::sky::{hash, Colours}; |
|
| 10 | + | use crate::sky::Colours; |
|
| 11 | 11 | use std::f32::consts::{PI, TAU}; |
|
| 12 | 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; |
|
| 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; |
|
| 17 | 17 | /// Dots flowing along each stream, and their speed in CSS pixels a second: |
|
| 18 | 18 | /// a stream is a flow, quicker than the drift of marks. |
|
| 19 | - | const FLOW: usize = 10; |
|
| 19 | + | const FLOW: usize = 28; |
|
| 20 | 20 | const FLOW_SPEED: f32 = 40.0; |
|
| 21 | 21 | /// A ripple every this many seconds, widening this far over this long. |
|
| 22 | 22 | const RIPPLE_EVERY: f32 = 5.0; |
|
| 23 | 23 | const RIPPLE_R: f32 = 280.0; |
|
| 24 | 24 | const RIPPLE_S: f32 = 3.2; |
|
| 25 | - | const RIPPLE_DOTS: usize = 40; |
|
| 25 | + | const RIPPLE_DOTS: usize = 72; |
|
| 26 | 26 | ||
| 27 | 27 | /// One repository on the page: its name and the tone its dots take. |
|
| 28 | 28 | pub struct Node { |
|
| … | 7 unchanged lines |
||
| 36 | 36 | nodes: Vec<Node>, |
|
| 37 | 37 | ups: usize, |
|
| 38 | 38 | pos: Vec<(f32, f32)>, |
|
| 39 | + | /// Each knot's dots: its node, and its orbit. |
|
| 40 | + | orbits: Vec<(usize, Orbit)>, |
|
| 39 | 41 | /// Streams: from, to, and their dots' first index. |
|
| 40 | 42 | streams: Vec<(usize, usize, usize)>, |
|
| 41 | 43 | ripple_at: usize, |
|
| … | 9 unchanged lines |
||
| 51 | 53 | nodes.extend(upstreams); |
|
| 52 | 54 | nodes.extend(consumers); |
|
| 53 | 55 | let mut field = Field::new(still); |
|
| 56 | + | let mut orbits = Vec::new(); |
|
| 54 | 57 | for (i, n) in nodes.iter().enumerate() { |
|
| 55 | 58 | let mut core = Dot::new(0.0, 0.0, if i == 0 { 9.0 } else { 7.0 }, 4.0); |
|
| 56 | 59 | core.tone = n.tone; |
|
| 57 | 60 | field.dots.push(core); |
|
| 58 | - | for k in 0..RING { |
|
| 59 | - | let mut d = Dot::new(0.0, 0.0, 2.2, 0.0); |
|
| 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 }); |
|
| 60 | 65 | d.tone = n.tone; |
|
| 61 | 66 | d.group = i as u32; |
|
| 62 | 67 | d.order = k as u32; |
|
| 63 | - | d.bright = 0.6 + 0.4 * unit(hash(&format!("{}#{k}", n.name)), 0); |
|
| 68 | + | d.bright = 0.55 + 0.45 * unit(crate::sky::hash(&format!("{}@{k}", n.name)), 0); |
|
| 64 | 69 | field.dots.push(d); |
|
| 70 | + | orbits.push((i, o)); |
|
| 65 | 71 | } |
|
| 66 | 72 | } |
|
| 67 | 73 | // Upstreams flow into the repository; the repository flows out to |
|
| … | 7 unchanged lines |
||
| 75 | 81 | }; |
|
| 76 | 82 | streams.push((from, to, field.dots.len())); |
|
| 77 | 83 | for _ in 0..FLOW { |
|
| 78 | - | let mut d = Dot::new(0.0, 0.0, 2.0, 0.0); |
|
| 84 | + | let mut d = Dot::new(0.0, 0.0, 1.8 * ZOOM, 0.0); |
|
| 79 | 85 | d.tone = node.tone; |
|
| 80 | 86 | field.dots.push(d); |
|
| 81 | 87 | } |
|
| 82 | 88 | } |
|
| 83 | 89 | let ripple_at = field.dots.len(); |
|
| 84 | 90 | for _ in 0..RIPPLE_DOTS { |
|
| 85 | - | let mut d = Dot::new(0.0, 0.0, 1.8, 0.0); |
|
| 91 | + | let mut d = Dot::new(0.0, 0.0, 1.6 * ZOOM, 0.0); |
|
| 86 | 92 | d.bright = 0.0; |
|
| 87 | 93 | field.dots.push(d); |
|
| 88 | 94 | } |
|
| 89 | - | let mut r = Ripples { field, nodes, ups, pos: Vec::new(), streams, ripple_at, w: 0.0, h: 0.0, t: 0.0 }; |
|
| 95 | + | let mut r = Ripples { field, nodes, ups, pos: Vec::new(), orbits, streams, ripple_at, w: 0.0, h: 0.0, t: 0.0 }; |
|
| 90 | 96 | r.layout(None); |
|
| 91 | 97 | r |
|
| 92 | 98 | } |
|
| … | 21 unchanged lines |
||
| 114 | 120 | }) |
|
| 115 | 121 | .collect(); |
|
| 116 | 122 | 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(); |
|
| 123 | + | self.nodes.iter().zip(&self.pos).map(|(nd, &(x, y))| Named { name: nd.name.clone(), x, y, reach: 90.0 }).collect(); |
|
| 118 | 124 | } |
|
| 119 | 125 | ||
| 120 | 126 | fn place(&mut self) { |
|
| 121 | - | let mut at = 0; |
|
| 122 | - | let spin = RING_SPEED / RING_R * self.t; |
|
| 127 | + | let n = self.nodes.len(); |
|
| 123 | 128 | 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 | - | } |
|
| 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; |
|
| 133 | 136 | } |
|
| 134 | 137 | // Streams: a gentle arc from each source to its destination, its |
|
| 135 | 138 | // dots evenly spaced and moving along it, faded at the ends. |
|
| … | 16 unchanged lines |
||
| 152 | 155 | for k in 0..RIPPLE_DOTS { |
|
| 153 | 156 | let d = &mut self.field.dots[self.ripple_at + k]; |
|
| 154 | 157 | let a = k as f32 / RIPPLE_DOTS as f32 * TAU; |
|
| 155 | - | let r = RING_R + RIPPLE_R * p.min(1.0); |
|
| 158 | + | let r = 40.0 + RIPPLE_R * p.min(1.0); |
|
| 156 | 159 | d.x = x + a.cos() * r; |
|
| 157 | 160 | d.y = y + a.sin() * r; |
|
| 158 | 161 | d.bright = if self.field.still || p >= 1.0 { 0.0 } else { 0.8 * (1.0 - p) }; |
|
| … | 57 unchanged lines |
||
| 216 | 219 | r.resize(2560.0, 1270.0, Some((707.0, 1838.0))); |
|
| 217 | 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"); |
|
| 218 | 221 | 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)]); |
|
| 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)]); |
|
| 220 | 224 | let bad_stream = r.streams[2].2; |
|
| 221 | 225 | assert!(r.field.dots[bad_stream..bad_stream + FLOW].iter().all(|d| d.tone == Tone::Bad), "a failing consumer's stream is red"); |
|
| 222 | 226 | let before = r.field.dots[bad_stream].x; |
|
| … | 17 unchanged lines |
modified crates/backdrop/src/swarm.rs +23 −28
| … | 3 unchanged lines |
||
| 4 | 4 | //! to it from every holder, the hub and its peers alike, as Jetstream's |
|
| 5 | 5 | //! swarm fetches them. The data is the page's own snapshot. |
|
| 6 | 6 | ||
| 7 | - | use crate::field::{margins, unit, Dot, Field, Named}; |
|
| 7 | + | use crate::field::{knot, margins, Dot, Field, Named, Orbit, ZOOM}; |
|
| 8 | 8 | use crate::scene::Scene; |
|
| 9 | - | use crate::sky::{hash, Colours, MAX_SPEED}; |
|
| 10 | - | use std::f32::consts::{PI, TAU}; |
|
| 9 | + | use crate::sky::Colours; |
|
| 10 | + | use std::f32::consts::PI; |
|
| 11 | 11 | ||
| 12 | + | /// Dots for each image a node holds (its chunks, drawn as a sky's knot). |
|
| 13 | + | const PER_IMAGE: usize = 6; |
|
| 12 | 14 | /// Chunks streaming along each holder-to-fetcher path, their speed, and |
|
| 13 | 15 | /// the most streams drawn. |
|
| 14 | - | const CHUNKS: usize = 8; |
|
| 16 | + | const CHUNKS: usize = 20; |
|
| 15 | 17 | const CHUNK_SPEED: f32 = 40.0; |
|
| 16 | 18 | const MAX_STREAMS: usize = 40; |
|
| 17 | 19 | ||
| … | 5 unchanged lines |
||
| 23 | 25 | ||
| 24 | 26 | struct Held { |
|
| 25 | 27 | node: usize, |
|
| 26 | - | radius: f32, |
|
| 27 | - | angle: f32, |
|
| 28 | - | speed: f32, |
|
| 28 | + | orbit: Orbit, |
|
| 29 | 29 | } |
|
| 30 | 30 | ||
| 31 | 31 | pub struct Swarm { |
|
| … | 16 unchanged lines |
||
| 48 | 48 | field.dots.push(Dot::new(0.0, 0.0, if i == 0 { 10.0 } else { 8.0 }, 4.0)); |
|
| 49 | 49 | } |
|
| 50 | 50 | let mut held = Vec::new(); |
|
| 51 | - | let mut count = vec![0u32; names.len()]; |
|
| 52 | - | for (n, img) in images.iter().enumerate() { |
|
| 51 | + | let mut count = vec![0usize; names.len()]; |
|
| 52 | + | for img in &images { |
|
| 53 | 53 | for &node in img.held.iter().filter(|&&n| n < names.len()) { |
|
| 54 | - | let k = count[node]; |
|
| 55 | 54 | count[node] += 1; |
|
| 56 | - | let h = hash(&format!("{}#{n}", names[node])); |
|
| 57 | - | let radius = (16.0 + (k % 5) as f32 * 7.0 + unit(h, 0) * 4.0) * 1.4; |
|
| 58 | - | held.push(Held { |
|
| 59 | - | node, |
|
| 60 | - | radius, |
|
| 61 | - | angle: unit(h, 16) * TAU, |
|
| 62 | - | speed: (MAX_SPEED / radius).min(0.2) * (0.5 + 0.5 * unit(h, 32)), |
|
| 63 | - | }); |
|
| 64 | - | let mut d = Dot::new(0.0, 0.0, 2.6, 0.0); |
|
| 55 | + | } |
|
| 56 | + | } |
|
| 57 | + | for (node, name) in names.iter().enumerate() { |
|
| 58 | + | for (k, orbit) in knot(name, count[node] * PER_IMAGE).into_iter().enumerate() { |
|
| 59 | + | let mut d = Dot::new(0.0, 0.0, 2.0 * ZOOM, if k % PER_IMAGE == 0 { 5.0 } else { 0.0 }); |
|
| 65 | 60 | d.group = node as u32; |
|
| 66 | - | d.order = k; |
|
| 67 | - | d.bright = 0.7; |
|
| 61 | + | d.order = k as u32; |
|
| 62 | + | d.bright = 0.75; |
|
| 68 | 63 | field.dots.push(d); |
|
| 64 | + | held.push(Held { node, orbit }); |
|
| 69 | 65 | } |
|
| 70 | 66 | } |
|
| 71 | 67 | let mut streams = Vec::new(); |
|
| … | 5 unchanged lines |
||
| 77 | 73 | } |
|
| 78 | 74 | streams.push((from, to, field.dots.len())); |
|
| 79 | 75 | for _ in 0..CHUNKS { |
|
| 80 | - | let mut d = Dot::new(0.0, 0.0, 2.2, 5.0); |
|
| 76 | + | let mut d = Dot::new(0.0, 0.0, 1.8 * ZOOM, 5.0); |
|
| 81 | 77 | d.bright = 0.0; |
|
| 82 | 78 | field.dots.push(d); |
|
| 83 | 79 | } |
|
| … | 25 unchanged lines |
||
| 109 | 105 | .zip(&self.pos) |
|
| 110 | 106 | .enumerate() |
|
| 111 | 107 | .map(|(i, (name, &(x, y)))| { |
|
| 112 | - | let reach = self.held.iter().filter(|h| h.node == i).map(|h| h.radius).fold(12.0, f32::max); |
|
| 108 | + | let reach = self.held.iter().filter(|h| h.node == i).map(|h| h.orbit.radius).fold(12.0, f32::max); |
|
| 113 | 109 | Named { name: name.clone(), x, y, reach } |
|
| 114 | 110 | }) |
|
| 115 | 111 | .collect(); |
|
| … | 6 unchanged lines |
||
| 122 | 118 | self.field.dots[i].y = y; |
|
| 123 | 119 | } |
|
| 124 | 120 | for (k, hd) in self.held.iter().enumerate() { |
|
| 125 | - | let (x, y) = self.pos[hd.node]; |
|
| 126 | - | let a = hd.angle + hd.speed * self.t; |
|
| 127 | - | self.field.dots[n + k].x = x + a.cos() * hd.radius; |
|
| 128 | - | self.field.dots[n + k].y = y + a.sin() * hd.radius; |
|
| 121 | + | let (x, y) = hd.orbit.at(self.pos[hd.node], self.t); |
|
| 122 | + | self.field.dots[n + k].x = x; |
|
| 123 | + | self.field.dots[n + k].y = y; |
|
| 129 | 124 | } |
|
| 130 | 125 | for &(from, to, first) in &self.streams { |
|
| 131 | 126 | let (a, b) = (self.pos[from], self.pos[to]); |
|
| … | 62 unchanged lines |
||
| 194 | 189 | let mut s = Swarm::new(names, images, false); |
|
| 195 | 190 | s.resize(2560.0, 1270.0, Some((707.0, 1838.0))); |
|
| 196 | 191 | assert!(s.pos[0].0 < 707.0 && s.pos[1].0 > 1838.0 && s.pos[2].0 > 1838.0, "the hub on one side, runners on the other"); |
|
| 197 | - | assert_eq!(s.held.len(), 5, "a dot for every image each node holds"); |
|
| 192 | + | assert_eq!(s.held.len(), 5 * PER_IMAGE, "dots for every image each node holds"); |
|
| 198 | 193 | assert_eq!(s.streams.iter().map(|&(a, b, _)| (a, b)).collect::<Vec<_>>(), vec![(0, 2), (1, 2)], "from the hub and the peer"); |
|
| 199 | 194 | let first = s.streams[0].2; |
|
| 200 | 195 | let before = s.field.dots[first].x; |
|
| … | 9 unchanged lines |