OXYGENHUB

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