1use std::fmt::Write as _;
29
30const EDGE: f32 = 1024.0;
33const CENTER: f32 = EDGE / 2.0;
34
35pub 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
46struct 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 fn f(&mut self) -> f32 {
60 (self.next() >> 40) as f32 / (1u64 << 24) as f32
61 }
62 fn range(&mut self, lo: f32, hi: f32) -> f32 {
64 lo + self.f() * (hi - lo)
65 }
66 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
75fn 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
94struct 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 let offset = match rng.pick(4) {
110 0 => {
111 if rng.chance(0.5) { 30.0 } else { -30.0 }
113 }
114 1 => 180.0, 2 => {
116 if rng.chance(0.5) { 150.0 } else { 210.0 }
118 }
119 _ => {
120 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
163struct 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 Ring {
181 cx: f32,
182 cy: f32,
183 r: f32,
184 width: f32,
185 dash: f32,
186 rot: f32,
187 },
188 Rect {
190 cx: f32,
191 cy: f32,
192 w: f32,
193 h: f32,
194 rx: f32,
195 rot: f32,
196 },
197 Semi {
199 cx: f32,
200 cy: f32,
201 r: f32,
202 rot: f32,
203 },
204}
205
206impl Shape {
207 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
276fn 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
321fn compose(rng: &mut Rng, p: &Palette) -> Vec<Shape> {
325 let mut shapes = Vec::new();
326 match rng.pick(5) {
327 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 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 1 => {
368 let n = 3 + rng.pick(4); let orbit = rng.range(190.0, 240.0);
370 let pr = rng.range(92.0, 128.0) * (4.5 / n as f32).sqrt();
373 let petal = if n >= 4 { rng.pick(3) } else { rng.pick(2) * 2 };
376 let alternate = rng.chance(0.45);
377 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 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 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 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 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 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 _ => {
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
572pub 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 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 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 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 assert!(png.len() > 200, "seed {seed} rendered nearly nothing");
699 }
700 }
701}