animatar 1.0.1

A blazingly fast, deterministic animal avatar generator.
Documentation
pub mod colors;
pub mod random;
pub mod shapes;
pub mod svg;

use rand::{seq::IndexedRandom, Rng};
use random::seed_random;
use shapes::{
    brows::BROWS, ears::EARS, eyes::EYES, faces::FACES, hairs::HAIRS, muzzles::MUZZLES,
    patterns::PATTERNS,
};
use svg::{create_background, create_blackout, create_svg};

pub struct AvatarOptions<'a> {
    pub size: u32,
    pub round: bool,
    pub blackout: bool,
    pub avatar_colors: &'a [&'a str],
    pub background_colors: &'a [&'a str],
}

impl Default for AvatarOptions<'_> {
    fn default() -> Self {
        Self {
            size: 150,
            round: true,
            blackout: true,
            avatar_colors: &[
                "#d7b89c", "#b18272", "#ec8a90", "#a1Ac88", "#99c9bd", "#50c8c6",
            ],
            background_colors: &["#fcf7d1", "#ece2e1", "#e4e3cd", "#c4ddd6", "#b5f4bc"],
        }
    }
}

pub fn avatar(seed: &str, options: &AvatarOptions) -> String {
    let mut rng = seed_random(seed);

    let background_color = *options.background_colors.choose(&mut rng).unwrap();
    let avatar_color = *options.avatar_colors.choose(&mut rng).unwrap();

    let mut shapes_svg = String::new();

    shapes_svg.push_str(&FACES.choose(&mut rng).unwrap()(avatar_color));

    if rng.random() {
        shapes_svg.push_str(&PATTERNS.choose(&mut rng).unwrap()(avatar_color));
    }

    shapes_svg.push_str(&EARS.choose(&mut rng).unwrap()(avatar_color));

    if rng.random() {
        shapes_svg.push_str(&HAIRS.choose(&mut rng).unwrap()(avatar_color));
    }

    shapes_svg.push_str(&MUZZLES.choose(&mut rng).unwrap()(avatar_color));
    shapes_svg.push_str(&EYES.choose(&mut rng).unwrap()(avatar_color));
    shapes_svg.push_str(&BROWS.choose(&mut rng).unwrap()(avatar_color));

    let background = create_background(options.round, background_color);
    let blackout = if options.blackout {
        create_blackout(options.round)
    } else {
        String::new()
    };

    create_svg(options.size, &background, &shapes_svg, &blackout)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_avatar_is_deterministic() {
        let options = AvatarOptions::default();
        let seed = "my-test-seed";

        let svg1 = avatar(seed, &options);
        let svg2 = avatar(seed, &options);

        assert_eq!(svg1, svg2);
    }

    #[test]
    fn test_avatar_snapshot_default() {
        let options = AvatarOptions::default();
        let seed = "rust-test-seed";
        let generated_svg = avatar(seed, &options);

        // This is a "snapshot" of the expected output for the given seed.
        // If the generation logic changes, this test will fail, indicating an intended or unintended change.
        let expected_svg = r##"<svg
    xmlns="http://www.w3.org/2000/svg"
    version="1.1"
    width="150"
    height="150"
    viewBox="0 0 500 500"
>
    <rect
    width="500"
    height="500"
    rx="250"
    fill="#e4e3cd"
/>

    <path fill="#b18272" d="M440.7 280.2c0 6.7-.4 13.1-1.2 19.2-1.1 7.9-2.9 15.2-5.3 21.9a86.3 86.3 0 01-20.7 32.6C393 374.3 362 385.2 325 390.6c-23 3.3-48.3 4.5-74.9 4.5a529 529 0 01-75-4.6c-68-9.8-115.7-38.4-115.7-110.3 0-96.3 85.4-174.3 190.7-174.3 95.7 0 175 64.4 188.6 148.3a161 161 0 012.1 26z"/>

    <path fill="#b18272" d="M132.3 143a185.8 185.8 0 00-42.6 42.7c-28.4-20.8-36.5-43-31.4-66.4a86.2 86.2 0 018.4-22.1c33.5 4 56.8 18 65.6 45.9zM409.9 185.1a176.3 176.3 0 00-43.1-42.7c9-27.4 32.1-41.2 65.4-45.2 4 7.5 6.8 14.8 8.3 22.1 5.1 23.1-2.8 45.2-30.6 65.8z"/>
    <path fill="\#fec3aa" d="M103.9 168.2c-5.1 5.5-9.9 11.4-14.2 17.5-28.4-20.8-36.5-43-31.4-66.4 6.8 2.4 16.2 6.7 27 14.4a48.3 48.3 0 0118.6 34.5zM409.9 185.1c-4.5-6.3-9.4-12.3-14.7-18a47.9 47.9 0 0118.4-33.4c10.8-7.7 20.1-12 27-14.4 5 23.1-3 45.2-30.7 65.8z"/>

    <path fill="#936454" d="M297.8 78.3a4.8 4.8 0 01-4.2 5.2c-4.2.4-8.3 1-12.2 2-3.9 1-7.6 2.6-11 4.6a37.6 37.6 0 00-12.9 12.3l-1.5 2.7-.5.9a165.9 165.9 0 00-8.8 0l-1.5-3c-.9-.6-1.8-1.3-2.8-1.8-3-1.6-6.3-2.5-9.6-2.8-6.8-.6-13.7.6-20.5 2.4a3.8 3.8 0 11-1.5-7.5c3.7-.5 7.5-.9 11.3-1 3.8 0 7.7.3 11.5 1.1 2.2.5 4.4 1.2 6.5 2.2-2-2.4-4.3-4.6-6.9-6.5-6.9-5-15.4-7.3-24.1-7.6a5.7 5.7 0 11.4-11.5h.6c5.1.7 10.3 2 15.2 4a47.7 47.7 0 0123 20 65.6 65.6 0 00-11.5-38.4 6.7 6.7 0 1111.7-6.7v.1a76.7 76.7 0 017 44 42 42 0 0110.3-10c4-2.8 8.5-4.9 13-6.3 4.6-1.4 9.2-2.4 13.9-2.8 2.6-.2 5 1.8 5.1 4.4z"/>

    <path fill="\#fff" d="M210.3 362L193 359a3.2 3.2 0 01-2.6-3.4l1.1-18c.1-1.9 2-3.3 3.8-3l22 4c2 .4 3.2 2.5 2.5 4.3l-6 17.1c-.4 1.5-2 2.4-3.6 2.1zM289.8 362L307 359a3.2 3.2 0 002.6-3.4l-1.1-18c-.1-1.9-2-3.3-3.8-3l-22 4c-2 .4-3.2 2.5-2.5 4.3l6 17.1c.4 1.5 2 2.4 3.6 2.1z"/>
    <path fill="#936454" d="M275.7 273.1c-9.4 0-18.2-1.5-25.7 2-7.5-3.5-16.3-2-25.7-2-27.9 0-50.5 17.4-50.5 39 0 21.4 22.6 38.8 50.5 38.8 9.4 0 18.2-6.8 25.7-10.2 7.5 3.4 16.3 10.2 25.7 10.2 27.9 0 50.5-17.4 50.5-38.9s-22.6-38.9-50.5-38.9z"/>
    <path fill="\#15212a" d="M219.9 307.1c-.9 0-1.7 0-2.6-.2a4 4 0 01.5-8h2.1c2.6 0 4.6-1 5.5-5.8a4 4 0 014.7-3.2 4 4 0 013.2 4.7c-1.5 8-6.3 12.5-13.4 12.5zM281.1 307.1c.9 0 1.7 0 2.6-.2a4 4 0 00-.5-8h-2.1c-2.6 0-4.6-1-5.5-5.8a4 4 0 00-4.7-3.2 4 4 0 00-3.2 4.7c1.5 8 6.3 12.5 13.4 12.5z"/>

    <circle fill="\#15212a" cx="331.04" cy="253.21" r="22.21"/>
    <circle fill="\#fff" cx="338.81" cy="246.07" r="8.97"/>
    <circle fill="\#fff" cx="322.5" cy="263.97" r="5.01"/>
    <circle fill="\#15212a" cx="168.96" cy="253.21" r="22.21"/>
    <circle fill="\#fff" cx="161.19" cy="246.07" r="8.97"/>
    <circle fill="\#fff" cx="177.5" cy="263.97" r="5.01"/>

    <ellipse fill="\#15212a" cx="348" cy="190.6" rx="8.3" ry="19.4" transform="rotate(-57.5 348 190.6)"/>
    <ellipse fill="\#15212a" cx="152" cy="205.1" rx="19.4" ry="8.3" transform="rotate(-32.5 152 205)"/>

    <path
    d="M250,0a250,250 0 1,1 0,500"
    fill="\#15212a"
    fill-opacity="0.08"
/>
</svg>"##;

        assert_eq!(
            generated_svg.trim().replace("\n", "").replace(" ", ""),
            expected_svg.trim().replace("\n", "").replace(" ", "")
        );
    }

    #[test]
    fn test_avatar_different_options() {
        let options = AvatarOptions::<'_> {
            round: false,
            blackout: false,
            size: 400,
            ..Default::default()
        };

        let seed = "another-seed";
        let svg = avatar(seed, &options);

        // Check that the options were applied correctly
        assert!(svg.contains(r#"width="400""#));
        assert!(svg.contains(r#"rx="0""#)); // Not round
        assert!(!svg.contains(r#"fill-opacity="0.08""#)); // No blackout
    }
}