Skip to main content

day_vector/
icongen.rs

1// Copyright © The Daybrite Project
2// SPDX-License-Identifier: MPL-2.0
3
4//! Seeded app-icon generator (docs/icons.md): one `u64` seed → a deterministic layered SVG
5//! master (`day:background` / `day:foreground` / `day:monochrome`, the contract `day icon`
6//! consumes), designed to read well through every downstream form — iOS squircle, Android
7//! adaptive + themed monochrome, plain PNG.
8//!
9//! The compositions encode the published icon-design guidance rather than free-form noise:
10//!
11//! * **One or two focal points in simple geometry** — icons are judged at small sizes, so a
12//!   single dominant motif with at most a couple of supporting accents (Apple HIG).
13//! * **Safe-zone placement** — primary content stays inside the central region so the iOS
14//!   squircle and Android circle masks never clip it; the backdrop alone bleeds full-canvas.
15//! * **A limited, harmonious palette** — a background tone plus at most two accent hues,
16//!   drawn from the classic color-harmony schemes (analogous, complementary,
17//!   split-complementary, triadic) with saturation/lightness held to bands that keep
18//!   figure-ground contrast high on both dark and light backdrops.
19//! * **Flat or subtly gradient backgrounds** — a gentle vertical two-stop gradient of one
20//!   hue (the HIG's "subtle top-to-bottom gradient adds depth without looking dated").
21//! * **Balance** — compositions are either symmetric (centered, rotational) or
22//!   golden-section asymmetric with a small counterweight, the two classical routes to
23//!   visual equilibrium.
24//!
25//! Determinism is part of the contract: `day new` seeds from the app id so the same id
26//! always regenerates the same icon, and `day icon --generate --seed N` reproduces exactly.
27
28use std::fmt::Write as _;
29
30/// Canvas edge in user units. All downstream renders scale from the viewBox, so the exact
31/// number only sets the coordinate vocabulary below.
32const EDGE: f32 = 1024.0;
33const CENTER: f32 = EDGE / 2.0;
34
35/// Hash an arbitrary string (an app id, a pet name) into a seed — FNV-1a 64, hand-rolled so
36/// the mapping is stable across Rust versions (std's hashers are randomly keyed).
37pub fn seed_from_str(s: &str) -> u64 {
38    let mut h: u64 = 0xcbf2_9ce4_8422_2325;
39    for b in s.as_bytes() {
40        h ^= u64::from(*b);
41        h = h.wrapping_mul(0x0000_0100_0000_01b3);
42    }
43    h
44}
45
46/// splitmix64 — tiny, well-distributed, and dependency-free; every aesthetic choice below
47/// draws from this stream in a fixed order, which is what makes a seed reproducible.
48struct Rng(u64);
49
50impl Rng {
51    fn next(&mut self) -> u64 {
52        self.0 = self.0.wrapping_add(0x9e37_79b9_7f4a_7c15);
53        let mut z = self.0;
54        z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
55        z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
56        z ^ (z >> 31)
57    }
58    /// Uniform in `[0, 1)`.
59    fn f(&mut self) -> f32 {
60        (self.next() >> 40) as f32 / (1u64 << 24) as f32
61    }
62    /// Uniform in `[lo, hi)`.
63    fn range(&mut self, lo: f32, hi: f32) -> f32 {
64        lo + self.f() * (hi - lo)
65    }
66    /// Uniform integer in `[0, n)`.
67    fn pick(&mut self, n: u32) -> u32 {
68        (self.next() % u64::from(n)) as u32
69    }
70    fn chance(&mut self, p: f32) -> bool {
71        self.f() < p
72    }
73}
74
75/// HSL (h in degrees, s/l in 0..1) → `#rrggbb`. Hand-rolled: palettes are authored in HSL
76/// because the harmony schemes are angle arithmetic on the hue wheel.
77fn hsl(h: f32, s: f32, l: f32) -> String {
78    let h = h.rem_euclid(360.0);
79    let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
80    let x = c * (1.0 - ((h / 60.0) % 2.0 - 1.0).abs());
81    let m = l - c / 2.0;
82    let (r, g, b) = match (h / 60.0) as u32 {
83        0 => (c, x, 0.0),
84        1 => (x, c, 0.0),
85        2 => (0.0, c, x),
86        3 => (0.0, x, c),
87        4 => (x, 0.0, c),
88        _ => (c, 0.0, x),
89    };
90    let q = |v: f32| ((v + m).clamp(0.0, 1.0) * 255.0).round() as u8;
91    format!("#{:02x}{:02x}{:02x}", q(r), q(g), q(b))
92}
93
94/// The palette: a background gradient pair plus two accents and an "ink" (the near-neutral
95/// detail color). Accent lightness bands are chosen against the backdrop so figure-ground
96/// contrast holds by construction.
97struct Palette {
98    bg_top: String,
99    bg_bottom: String,
100    a: String,
101    b: String,
102    ink: String,
103    dark: bool,
104}
105
106fn palette(rng: &mut Rng) -> Palette {
107    let base = rng.f() * 360.0;
108    // Classic harmony schemes: the second accent's hue offset from the first.
109    let offset = match rng.pick(4) {
110        0 => {
111            // Analogous: adjacent hues, cohesive and calm.
112            if rng.chance(0.5) { 30.0 } else { -30.0 }
113        }
114        1 => 180.0, // complementary: maximum hue tension, still consonant
115        2 => {
116            // Split-complementary: the softer complement.
117            if rng.chance(0.5) { 150.0 } else { 210.0 }
118        }
119        _ => {
120            // Triadic.
121            if rng.chance(0.5) { 120.0 } else { 240.0 }
122        }
123    };
124    let dark = rng.chance(0.62);
125    let drift = rng.range(-14.0, 14.0);
126    let (bg_top, bg_bottom) = if dark {
127        let s = rng.range(0.30, 0.55);
128        (
129            hsl(base + drift, s, rng.range(0.24, 0.32)),
130            hsl(base, s, rng.range(0.12, 0.18)),
131        )
132    } else {
133        let s = rng.range(0.25, 0.50);
134        (
135            hsl(base + drift, s, rng.range(0.93, 0.97)),
136            hsl(base, s, rng.range(0.84, 0.90)),
137        )
138    };
139    let acc = |rng: &mut Rng, h: f32| {
140        if dark {
141            hsl(h, rng.range(0.62, 0.85), rng.range(0.56, 0.70))
142        } else {
143            hsl(h, rng.range(0.55, 0.80), rng.range(0.38, 0.50))
144        }
145    };
146    let a = acc(rng, base);
147    let b = acc(rng, base + offset);
148    let ink = if dark {
149        hsl(base, rng.range(0.08, 0.20), rng.range(0.92, 0.97))
150    } else {
151        hsl(base, rng.range(0.30, 0.50), rng.range(0.16, 0.26))
152    };
153    Palette {
154        bg_top,
155        bg_bottom,
156        a,
157        b,
158        ink,
159        dark,
160    }
161}
162
163/// One drawable element of the composition. `core` marks the shapes that carry the icon's
164/// identity — the monochrome layer re-emits exactly those as a single-color silhouette and
165/// drops the decorative rest (halos, glows, low-alpha accents).
166struct Shape {
167    kind: Kind,
168    fill: String,
169    opacity: f32,
170    core: bool,
171}
172
173enum Kind {
174    Circle {
175        cx: f32,
176        cy: f32,
177        r: f32,
178    },
179    /// Stroked circle; `dash` < 1.0 leaves a gap (an open arc), rotated by `rot`.
180    Ring {
181        cx: f32,
182        cy: f32,
183        r: f32,
184        width: f32,
185        dash: f32,
186        rot: f32,
187    },
188    /// Rounded rect centered at (cx, cy), rotated by `rot` degrees.
189    Rect {
190        cx: f32,
191        cy: f32,
192        w: f32,
193        h: f32,
194        rx: f32,
195        rot: f32,
196    },
197    /// Semicircle (flat edge through the center line), rotated by `rot`.
198    Semi {
199        cx: f32,
200        cy: f32,
201        r: f32,
202        rot: f32,
203    },
204}
205
206impl Shape {
207    /// Emit as SVG. `mono` overrides every color with black and squashes opacity to 1 —
208    /// the themed-icon silhouette (the platform supplies the tint).
209    fn svg(&self, mono: bool) -> String {
210        let fill = if mono { "#000000" } else { self.fill.as_str() };
211        let op = if mono || self.opacity >= 0.999 {
212            String::new()
213        } else {
214            format!(" opacity=\"{:.2}\"", self.opacity)
215        };
216        match self.kind {
217            Kind::Circle { cx, cy, r } => {
218                format!("<circle cx=\"{cx:.1}\" cy=\"{cy:.1}\" r=\"{r:.1}\" fill=\"{fill}\"{op}/>")
219            }
220            Kind::Ring {
221                cx,
222                cy,
223                r,
224                width,
225                dash,
226                rot,
227            } => {
228                let circ = std::f32::consts::TAU * r;
229                let dasharray = if dash < 0.999 {
230                    format!(
231                        " stroke-dasharray=\"{:.1} {:.1}\" stroke-linecap=\"round\"",
232                        circ * dash,
233                        circ,
234                    )
235                } else {
236                    String::new()
237                };
238                format!(
239                    "<circle cx=\"{cx:.1}\" cy=\"{cy:.1}\" r=\"{r:.1}\" fill=\"none\" \
240                     stroke=\"{fill}\" stroke-width=\"{width:.1}\"{dasharray}{op} \
241                     transform=\"rotate({rot:.1} {cx:.1} {cy:.1})\"/>"
242                )
243            }
244            Kind::Rect {
245                cx,
246                cy,
247                w,
248                h,
249                rx,
250                rot,
251            } => {
252                let x = cx - w / 2.0;
253                let y = cy - h / 2.0;
254                let t = if rot.abs() > 0.01 {
255                    format!(" transform=\"rotate({rot:.1} {cx:.1} {cy:.1})\"")
256                } else {
257                    String::new()
258                };
259                format!(
260                    "<rect x=\"{x:.1}\" y=\"{y:.1}\" width=\"{w:.1}\" height=\"{h:.1}\" \
261                     rx=\"{rx:.1}\" fill=\"{fill}\"{op}{t}/>"
262                )
263            }
264            Kind::Semi { cx, cy, r, rot } => {
265                format!(
266                    "<path d=\"M {} {cy:.1} A {r:.1} {r:.1} 0 0 1 {} {cy:.1} Z\" \
267                     fill=\"{fill}\"{op} transform=\"rotate({rot:.1} {cx:.1} {cy:.1})\"/>",
268                    cx - r,
269                    cx + r,
270                )
271            }
272        }
273    }
274}
275
276/// The dominant-motif vocabulary: filled, compact, and legible at 16 px.
277fn motif(rng: &mut Rng, cx: f32, cy: f32, r: f32, fill: &str) -> Shape {
278    let kind = match rng.pick(5) {
279        0 => Kind::Circle { cx, cy, r },
280        1 => Kind::Ring {
281            cx,
282            cy,
283            r: r * 0.82,
284            width: r * 0.36,
285            dash: 1.0,
286            rot: 0.0,
287        },
288        2 => Kind::Rect {
289            cx,
290            cy,
291            w: r * 1.84,
292            h: r * 1.84,
293            rx: r * 0.55,
294            rot: if rng.chance(0.3) { 45.0 } else { 0.0 },
295        },
296        3 => Kind::Rect {
297            cx,
298            cy,
299            w: r * 2.0,
300            h: r * 1.15,
301            rx: r * 0.575,
302            rot: rng.range(-30.0, 30.0),
303        },
304        _ => Kind::Semi {
305            cx,
306            cy: cy + r * 0.25,
307            r: r * 1.05,
308            rot: *[0.0, 180.0, -90.0, 90.0]
309                .get(rng.pick(4) as usize)
310                .unwrap_or(&0.0),
311        },
312    };
313    Shape {
314        kind,
315        fill: fill.to_string(),
316        opacity: 1.0,
317        core: true,
318    }
319}
320
321/// Compose the foreground: a `Vec<Shape>` whose `core` subset is also the monochrome
322/// silhouette. Templates are the two classical balance strategies — symmetry (centered,
323/// rotational, stacked) and golden-section asymmetry with a counterweight.
324fn compose(rng: &mut Rng, p: &Palette) -> Vec<Shape> {
325    let mut shapes = Vec::new();
326    match rng.pick(5) {
327        // Centered: one dominant motif, optional halo ring behind it.
328        0 => {
329            let r = rng.range(240.0, 300.0);
330            if rng.chance(0.55) {
331                shapes.push(Shape {
332                    kind: Kind::Ring {
333                        cx: CENTER,
334                        cy: CENTER,
335                        r: r + rng.range(60.0, 90.0),
336                        width: rng.range(20.0, 34.0),
337                        dash: if rng.chance(0.5) {
338                            rng.range(0.55, 0.8)
339                        } else {
340                            1.0
341                        },
342                        rot: rng.f() * 360.0,
343                    },
344                    fill: p.b.clone(),
345                    opacity: 0.85,
346                    core: false,
347                });
348            }
349            shapes.push(motif(rng, CENTER, CENTER, r, &p.a));
350            if rng.chance(0.5) {
351                // A small satellite where the halo would sit — the second focal point.
352                let ang = rng.f() * std::f32::consts::TAU;
353                let d = rng.range(0.78, 0.95) * (r + 70.0);
354                shapes.push(Shape {
355                    kind: Kind::Circle {
356                        cx: CENTER + ang.cos() * d,
357                        cy: CENTER + ang.sin() * d,
358                        r: rng.range(42.0, 66.0),
359                    },
360                    fill: p.ink.clone(),
361                    opacity: 1.0,
362                    core: true,
363                });
364            }
365        }
366        // Rotational symmetry: N petals on a circle, optional center dot — mandala-adjacent.
367        1 => {
368            let n = 3 + rng.pick(4); // 3..=6
369            let orbit = rng.range(190.0, 240.0);
370            // Fewer petals get proportionally bigger ones, so a sparse ring still fills the
371            // composition instead of floating.
372            let pr = rng.range(92.0, 128.0) * (4.5 / n as f32).sqrt();
373            // Sparse rings of capsules read scattered; below four petals stay with the
374            // compact shapes.
375            let petal = if n >= 4 { rng.pick(3) } else { rng.pick(2) * 2 };
376            let alternate = rng.chance(0.45);
377            // Anchored just off "12 o'clock": a recognizably upright arrangement still
378            // varies seed-to-seed without ever reading as randomly strewn.
379            let phase = -90.0 + rng.range(-16.0, 16.0);
380            for i in 0..n {
381                let ang = phase + 360.0 * i as f32 / n as f32;
382                let rad = ang.to_radians();
383                let (cx, cy) = (CENTER + rad.cos() * orbit, CENTER + rad.sin() * orbit);
384                let fill = if alternate && i % 2 == 1 { &p.b } else { &p.a };
385                let kind = match petal {
386                    0 => Kind::Circle { cx, cy, r: pr },
387                    1 => Kind::Rect {
388                        cx,
389                        cy,
390                        w: pr * 2.0,
391                        h: pr * 1.25,
392                        rx: pr * 0.62,
393                        rot: ang + 90.0,
394                    },
395                    _ => Kind::Ring {
396                        cx,
397                        cy,
398                        r: pr * 0.72,
399                        width: pr * 0.5,
400                        dash: 1.0,
401                        rot: 0.0,
402                    },
403                };
404                shapes.push(Shape {
405                    kind,
406                    fill: fill.clone(),
407                    opacity: 1.0,
408                    core: true,
409                });
410            }
411            if rng.chance(0.7) {
412                shapes.push(Shape {
413                    kind: Kind::Circle {
414                        cx: CENTER,
415                        cy: CENTER,
416                        r: rng.range(58.0, 92.0),
417                    },
418                    fill: p.ink.clone(),
419                    opacity: 1.0,
420                    core: true,
421                });
422            }
423        }
424        // Golden-section asymmetry: dominant motif near a golden point, a clear counterweight
425        // pulled in along the diagonal toward the opposite one — balance without symmetry,
426        // and the shared axis is what makes the pair read as designed rather than scattered.
427        2 => {
428            let lo = EDGE * 0.382;
429            let hi = EDGE * 0.618;
430            let (gx, gy) = match rng.pick(4) {
431                0 => (lo, lo),
432                1 => (hi, lo),
433                2 => (lo, hi),
434                _ => (hi, hi),
435            };
436            // Ease both anchors toward the center: cohesion beats literal golden points.
437            let mx = CENTER + (gx - CENTER) * 0.72;
438            let my = CENTER + (gy - CENTER) * 0.72;
439            let (ox, oy) = (CENTER + (CENTER - gx) * 0.62, CENTER + (CENTER - gy) * 0.62);
440            let r = rng.range(205.0, 250.0);
441            // Compact, rotation-stable motifs only — a tilted capsule off-center reads as
442            // clutter, not asymmetry.
443            let kind = match rng.pick(3) {
444                0 => Kind::Circle { cx: mx, cy: my, r },
445                1 => Kind::Ring {
446                    cx: mx,
447                    cy: my,
448                    r: r * 0.82,
449                    width: r * 0.36,
450                    dash: 1.0,
451                    rot: 0.0,
452                },
453                _ => Kind::Rect {
454                    cx: mx,
455                    cy: my,
456                    w: r * 1.84,
457                    h: r * 1.84,
458                    rx: r * 0.55,
459                    rot: 0.0,
460                },
461            };
462            shapes.push(Shape {
463                kind,
464                fill: p.a.clone(),
465                opacity: 1.0,
466                core: true,
467            });
468            let cr = rng.range(72.0, 100.0);
469            shapes.push(Shape {
470                kind: if rng.chance(0.5) {
471                    Kind::Circle {
472                        cx: ox,
473                        cy: oy,
474                        r: cr,
475                    }
476                } else {
477                    Kind::Ring {
478                        cx: ox,
479                        cy: oy,
480                        r: cr * 0.85,
481                        width: cr * 0.42,
482                        dash: 1.0,
483                        rot: 0.0,
484                    }
485                },
486                fill: p.b.clone(),
487                opacity: 1.0,
488                core: true,
489            });
490            if rng.chance(0.45) {
491                // A third beat on the same diagonal — rhythm, and it ties the pair together.
492                shapes.push(Shape {
493                    kind: Kind::Circle {
494                        cx: (mx + ox) / 2.0,
495                        cy: (my + oy) / 2.0,
496                        r: rng.range(30.0, 44.0),
497                    },
498                    fill: p.ink.clone(),
499                    opacity: 1.0,
500                    core: true,
501                });
502            }
503        }
504        // Stacked bars: 2–3 descending capsules — abstract "text", mirror-balanced.
505        3 => {
506            let n = 2 + rng.pick(2);
507            let h = rng.range(88.0, 112.0);
508            let gap = rng.range(56.0, 76.0);
509            let total = n as f32 * h + (n - 1) as f32 * gap;
510            let top = CENTER - total / 2.0 + h / 2.0;
511            let centered = rng.chance(0.5);
512            let left = CENTER - 250.0;
513            let widths = [500.0, rng.range(320.0, 400.0), rng.range(180.0, 260.0)];
514            for i in 0..n {
515                let w = widths[i as usize];
516                let cx = if centered { CENTER } else { left + w / 2.0 };
517                let fill = match i {
518                    0 => &p.a,
519                    1 => &p.b,
520                    _ => &p.ink,
521                };
522                shapes.push(Shape {
523                    kind: Kind::Rect {
524                        cx,
525                        cy: top + i as f32 * (h + gap),
526                        w,
527                        h,
528                        rx: h / 2.0,
529                        rot: 0.0,
530                    },
531                    fill: fill.clone(),
532                    opacity: 1.0,
533                    core: true,
534                });
535            }
536        }
537        // Orbit: dominant circle, an open arc around it, a satellite on the arc.
538        _ => {
539            let r = rng.range(180.0, 230.0);
540            let ring = r + rng.range(90.0, 130.0);
541            let rot = rng.f() * 360.0;
542            shapes.push(motif(rng, CENTER, CENTER, r, &p.a));
543            shapes.push(Shape {
544                kind: Kind::Ring {
545                    cx: CENTER,
546                    cy: CENTER,
547                    r: ring,
548                    width: rng.range(26.0, 38.0),
549                    dash: rng.range(0.6, 0.85),
550                    rot,
551                },
552                fill: p.b.clone(),
553                opacity: 1.0,
554                core: true,
555            });
556            let rad = rot.to_radians();
557            shapes.push(Shape {
558                kind: Kind::Circle {
559                    cx: CENTER + rad.cos() * ring,
560                    cy: CENTER + rad.sin() * ring,
561                    r: rng.range(40.0, 58.0),
562                },
563                fill: p.ink.clone(),
564                opacity: 1.0,
565                core: true,
566            });
567        }
568    }
569    shapes
570}
571
572/// Generate the master SVG for `seed`. Deterministic: the same seed always yields the same
573/// bytes (the `day new` app-id contract).
574pub fn generate(seed: u64) -> String {
575    let mut rng = Rng(seed);
576    let p = palette(&mut rng);
577    let shapes = compose(&mut rng, &p);
578
579    // Background decoration lives in the background LAYER: the adaptive-icon pipeline
580    // tightens the foreground to its content box, so full-bleed depth cues (glow, gloss)
581    // must not inflate the foreground's extent.
582    let glow = rng.chance(0.7);
583    let gloss = p.dark && rng.chance(0.35);
584
585    let mut svg = String::with_capacity(4096);
586    let _ = write!(
587        svg,
588        "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 1024 1024\">\
589         <defs>\
590         <linearGradient id=\"day-bg\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\
591         <stop offset=\"0\" stop-color=\"{}\"/>\
592         <stop offset=\"1\" stop-color=\"{}\"/>\
593         </linearGradient>\
594         <radialGradient id=\"day-glow\">\
595         <stop offset=\"0\" stop-color=\"{}\" stop-opacity=\"{}\"/>\
596         <stop offset=\"1\" stop-color=\"{}\" stop-opacity=\"0\"/>\
597         </radialGradient>\
598         </defs>",
599        p.bg_top,
600        p.bg_bottom,
601        p.a,
602        if p.dark { "0.30" } else { "0.18" },
603        p.a,
604    );
605    let _ = write!(
606        svg,
607        "<g id=\"day:background\">\
608         <rect width=\"1024\" height=\"1024\" fill=\"url(#day-bg)\"/>"
609    );
610    if glow {
611        let _ = write!(
612            svg,
613            "<circle cx=\"512\" cy=\"512\" r=\"470\" fill=\"url(#day-glow)\"/>"
614        );
615    }
616    if gloss {
617        let _ = write!(
618            svg,
619            "<ellipse cx=\"512\" cy=\"-120\" rx=\"820\" ry=\"560\" fill=\"#ffffff\" opacity=\"0.06\"/>"
620        );
621    }
622    svg.push_str("</g>");
623
624    svg.push_str("<g id=\"day:foreground\">");
625    for s in &shapes {
626        svg.push_str(&s.svg(false));
627    }
628    svg.push_str("</g>");
629
630    // Hidden in the master so plain viewers (and the composite) show the icon as shipped;
631    // the pipeline's monochrome-only document re-enables the layer (icon.rs unhide_layer).
632    svg.push_str("<g id=\"day:monochrome\" display=\"none\">");
633    for s in shapes.iter().filter(|s| s.core) {
634        svg.push_str(&s.svg(true));
635    }
636    svg.push_str("</g>");
637
638    svg.push_str("</svg>");
639    svg
640}
641
642#[cfg(test)]
643mod tests {
644    use super::*;
645
646    #[test]
647    fn deterministic() {
648        assert_eq!(generate(42), generate(42));
649        assert_eq!(
650            generate(seed_from_str("dev.example.app")),
651            generate(seed_from_str("dev.example.app"))
652        );
653    }
654
655    #[test]
656    fn distinct_across_seeds() {
657        let mut seen = std::collections::HashSet::new();
658        for seed in 0..96u64 {
659            assert!(seen.insert(generate(seed)), "seed {seed} collided");
660        }
661    }
662
663    #[test]
664    fn carries_the_master_layer_contract() {
665        let svg = generate(7);
666        for id in ["day:background", "day:foreground", "day:monochrome"] {
667            assert!(svg.contains(id), "missing {id}");
668        }
669        assert!(!svg.contains("<text"), "text must be outlined");
670    }
671
672    #[test]
673    fn monochrome_stays_inside_the_vectordrawable_subset() {
674        // Android's themed icon ships the monochrome layer as a VectorDrawable only when it
675        // fits the subset (docs/icons.md) — generated masters must never fall back to the
676        // bitmap mask. Reconstructs the pipeline's monochrome-only doc from the authored
677        // layer markers.
678        for seed in [0u64, 1, 7, 42, 99, 3_427_929_162_618_665_977] {
679            let svg = generate(seed);
680            let header_end = svg.find("<g id=\"day:background\"").expect("bg layer");
681            let mono_start = svg.find("<g id=\"day:monochrome\"").expect("mono layer");
682            let mono = format!("{}{}", &svg[..header_end], &svg[mono_start..])
683                .replace(" display=\"none\"", "");
684            let tree = crate::parse(mono.as_bytes()).expect("mono parses");
685            if let Err(e) = crate::to_vector_drawable(&tree) {
686                panic!("seed {seed}: monochrome left the VectorDrawable subset: {e:?}");
687            }
688        }
689    }
690
691    #[test]
692    fn every_seed_parses_and_renders_content() {
693        for seed in [0u64, 1, 17, 0xdead_beef, u64::MAX] {
694            let svg = generate(seed);
695            let tree = crate::parse(svg.as_bytes()).expect("parses");
696            let png = crate::render_png(&tree, 64).expect("renders");
697            // A generated icon is never blank: the opaque backdrop alone guarantees pixels.
698            assert!(png.len() > 200, "seed {seed} rendered nearly nothing");
699        }
700    }
701}