world_id_marbles/
lib.rs

1#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
2
3use anyhow::Result;
4use indoc::formatdoc;
5use resvg::{
6    tiny_skia::{Pixmap, Transform},
7    usvg::{self, TreeParsing},
8};
9use std::{fs, path::Path};
10use zkp_u256::U256;
11
12const COLORS: [&str; 36] = [
13    "#FF0000", "#FF2B00", "#FF5500", "#FF8000", "#FFAA00", "#FFD500", "#FFFF00", "#D4FF00",
14    "#AAFF00", "#80FF00", "#55FF00", "#2BFF00", "#00FF00", "#00FF2A", "#00FF2A", "#00FF80",
15    "#00FFAA", "#00FFD4", "#00FFFF", "#00D4FF", "#00AAFF", "#0080FF", "#0055FF", "#002AFF",
16    "#0000FF", "#2A00FF", "#0000FF", "#5500FF", "#8000FF", "#AA00FF", "#D500FF", "#FF00FF",
17    "#FF00D5", "#FF00AA", "#FF0080", "#FF0055",
18];
19
20pub struct Marble {
21    seed: U256,
22}
23
24impl Marble {
25    /// Create a new marble with the given seed.
26    pub fn new<T>(seed: T) -> Self
27    where
28        U256: From<T>,
29    {
30        Self {
31            seed: U256::from(seed),
32        }
33    }
34
35    fn random_number<T>(&mut self, max: T) -> u32
36    where
37        U256: From<T>,
38    {
39        let max = U256::from(max);
40
41        let result = self.seed.clone() % max.clone();
42        self.seed /= max;
43
44        result.as_u32()
45    }
46
47    fn random_color(&mut self) -> &str {
48        let index = self.random_number(COLORS.len()) as usize;
49
50        COLORS[index]
51    }
52
53    /// Build the SVG for the marble.
54    #[must_use]
55    pub fn build_svg(&mut self) -> String {
56        let mut shapes = vec![
57            formatdoc!(
58                r#"
59                <g filter="url(#blur)" opacity=".8">
60                    <path fill="{color}" d="M78.824-16.686c17.78 14.541 4.24 87.76-2.637 82.948-4.194-2.935-9.153-27.765-22.32-38.405-8.418-6.802-23.488-1.839-33.086-1.137-24.614 1.8 40.115-58.069 58.043-43.406Z"/>
61                </g>
62            "#,
63                color = self.random_color()
64            ),
65            formatdoc!(
66                r#"
67                <g filter="url(#blur)" opacity=".9">
68                    <ellipse cx="33.545" cy="32.494" fill="{color}" rx="33.545" ry="32.494" transform="matrix(-.48289 -.87568 .7985 -.602 9.46 74.034)"/>
69                </g>
70            "#,
71                color = self.random_color()
72            ),
73            formatdoc!(
74                r#"
75                <g filter="url(#blur)" opacity=".8">
76                    <ellipse cx="39.533" cy="39.042" fill="{color}" rx="39.533" ry="39.042" transform="matrix(-.2882 -.95757 .93652 -.35062 13.847 67.74)" />
77                </g>
78            "#,
79                color = self.random_color()
80            ),
81        ];
82
83        shapes.sort_by(|_, _| self.random_number(2).cmp(&1));
84
85        formatdoc!(
86            r##"
87                <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 80 80" transform="rotate({rotation} 40 40)">
88                    <g clip-path="url(#a)">
89                        <circle cx="40" cy="40" r="40" fill="#F8F8F8" />
90                        {shapes}
91                    </g>
92                    <defs>
93                        <filter id="blur" width="300" height="300" x="0" y="0" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
94                            <feGaussianBlur result="effect1_foregroundBlur_557_59789" stdDeviation="9.6" />
95                        </filter>
96                        <clipPath id="a">
97                            <rect width="80" height="80" fill="#fff" rx="40" />
98                        </clipPath>
99                    </defs>
100                </svg>
101        "##,
102            rotation = self.random_number(359),
103            shapes = shapes.join("")
104        )
105    }
106
107    /// Render the marble as a PNG.
108    /// The PNG is returned as a vector of bytes.
109    ///
110    /// # Errors
111    ///
112    /// This function will return an error if the marble cannot be rendered.
113    /// This can happen if the SVG fails to be parsed or the PNG cannot be encoded.
114    pub fn render_png(&mut self, width: u32, height: u32) -> Result<Vec<u8>> {
115        let svg = self.build_svg();
116        let tree = usvg::Tree::from_data(svg.as_bytes(), &usvg::Options::default())?;
117
118        let mut pixmap = Pixmap::new(width, height).ok_or_else(|| {
119            anyhow::anyhow!("Failed to create pixmap with size {}x{}", width, height)
120        })?;
121
122        resvg::render(
123            &tree,
124            resvg::FitTo::Size(width, height),
125            Transform::default(),
126            pixmap.as_mut(),
127        )
128        .ok_or_else(|| anyhow::anyhow!("Failed to render SVG"))?;
129
130        Ok(pixmap.encode_png()?)
131    }
132
133    /// Save the marble as a PNG.
134    ///
135    /// # Errors
136    ///
137    /// This function will return an error if the marble cannot be rendered or saved.
138    pub fn save_png<P: AsRef<Path>>(&mut self, width: u32, height: u32, path: P) -> Result<()> {
139        fs::write(path, self.render_png(width, height)?)?;
140
141        Ok(())
142    }
143}