fix(web): a sky's marks spread all the way round each cluster (refs #157) eb56c8bc4d0e
FNV-1a alone gave names differing only at the end alike high bits, so a cluster's marks fell at a few angles and drew fans. The hash now ends with splitmix64's finaliser, and a test checks every eighth of the circle holds a fair share.
- Author
- Andy McCall @andymccall <[email protected]>
- When
- · 2026-10-13 18:44 UTC
- Signature
- ✔ signed by Andy McCall
- Hash
eb56c8bc4d0eeb2db98c6b6f26515240d5142bac75c643b161102bcc9dcfc1d0- Depends on
- 17ca6cddc534 6907a989e82a
- Depended on by
- ffb67a81188c da8f7826a645 6246d1e51f4f
- 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
- 5 atoms, 0 tree operations, 2 files
Browse the tree at this change
replaced assets/backdrop/backdrop_bg.wasm +0 −0
No line changes.
modified crates/backdrop/src/sky.rs +20 −2
| … | 168 unchanged lines |
||
| 169 | 169 | link_fade: f32, |
|
| 170 | 170 | } |
|
| 171 | 171 | ||
| 172 | - | /// FNV-1a: a small, stable hash, so a repository's place never changes. |
|
| 172 | + | /// FNV-1a, then splitmix64's finaliser: a small, stable hash, so a |
|
| 173 | + | /// repository's place never changes. The finaliser spreads every bit: |
|
| 174 | + | /// FNV alone gives names that differ only at the end (`repo#1`, `repo#2`) |
|
| 175 | + | /// alike high bits, which drew a cluster's marks as fans at a few angles. |
|
| 173 | 176 | pub fn hash(s: &str) -> u64 { |
|
| 174 | 177 | let mut h: u64 = 0xcbf2_9ce4_8422_2325; |
|
| 175 | 178 | for b in s.as_bytes() { |
|
| 176 | 179 | h ^= *b as u64; |
|
| 177 | 180 | h = h.wrapping_mul(0x0100_0000_01b3); |
|
| 178 | 181 | } |
|
| 179 | - | h |
|
| 182 | + | h = (h ^ (h >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9); |
|
| 183 | + | h = (h ^ (h >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb); |
|
| 184 | + | h ^ (h >> 31) |
|
| 180 | 185 | } |
|
| 181 | 186 | ||
| 182 | 187 | /// A number from 0 to 1 from bits of a hash. |
|
| … | 401 unchanged lines |
||
| 584 | 589 | } |
|
| 585 | 590 | ||
| 586 | 591 | #[test] |
|
| 592 | + | fn a_clusters_marks_spread_all_the_way_round() { |
|
| 593 | + | let repos = vec![Repo { name: "doomsdayone/rstar-api".into() }]; |
|
| 594 | + | let events: Vec<Event> = (0..200).map(|_| Event { repo: 0, kind: Kind::Issue, age_days: 1.0, n: 1 }).collect(); |
|
| 595 | + | let s = Sky::new(&repos, &events, 1600.0, 900.0, true); |
|
| 596 | + | // Every eighth of the circle holds a fair share of the marks. |
|
| 597 | + | let mut octants = [0u32; 8]; |
|
| 598 | + | for m in s.marks.iter().filter(|m| m.kind == Kind::Issue) { |
|
| 599 | + | octants[((m.angle / TAU * 8.0) as usize).min(7)] += 1; |
|
| 600 | + | } |
|
| 601 | + | assert!(octants.iter().all(|&n| (12..=40).contains(&n)), "{octants:?}"); |
|
| 602 | + | } |
|
| 603 | + | ||
| 604 | + | #[test] |
|
| 587 | 605 | fn computed_colours_parse() { |
|
| 588 | 606 | assert_eq!(parse_colour("rgb(9, 105, 218)"), Some([9.0 / 255.0, 105.0 / 255.0, 218.0 / 255.0, 1.0])); |
|
| 589 | 607 | assert_eq!(parse_colour("rgba(0, 240, 255, 0.75)").map(|c| c[3]), Some(0.75)); |
|
| … | 5 unchanged lines |