nhdesigngen 1.1.3

An Animal Crossing: New Horizons design generator
Documentation
use exoquant::Color;
use serde::Serialize;
use std::cmp::Ordering;

/// Simple full-range HSV+alpha for conversions
#[derive(Debug, Clone, Serialize)]
pub struct HSVA {
    /// Hue, in degrees [0, 360)
    pub h: f32,
    /// Saturation [0, 1]
    pub s: f32,
    /// Value [0, 1]
    pub v: f32,
    /// Alpha [0, 1]
    pub a: f32,
}

/// Palette item allowing HSV or transparent
#[derive(Debug, Clone, Serialize, Eq)]
pub enum NHPaletteItem {
    Color{
        h: u8,
        s: u8,
        v: u8,
    },
    Transparent,
}

impl Ord for NHPaletteItem {
    fn cmp(&self, other: &Self) -> Ordering {
        match (self, other) {
            (NHPaletteItem::Transparent, NHPaletteItem::Transparent) => Ordering::Equal,
            (NHPaletteItem::Color{..}, NHPaletteItem::Transparent) => Ordering::Less,
            (NHPaletteItem::Transparent, NHPaletteItem::Color{..}) => Ordering::Greater,
            (NHPaletteItem::Color{h: lh, s: ls, v:lv, ..}, NHPaletteItem::Color{h: rh, s: rs, v:rv, ..}) => {
                match lh.cmp(rh) {
                    Ordering::Equal => {
                        match ls.cmp(rs) {
                            Ordering::Equal => lv.cmp(rv),
                            o => o,
                        }
                    },
                    o => o,
                }
            },
        }
    }
}

impl PartialOrd for NHPaletteItem {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl PartialEq for NHPaletteItem {
    fn eq(&self, other: &Self) -> bool {
        if let Ordering::Equal = self.cmp(other) {
            true
        } else {
            false
        }
    }
}

/// Palette item allowing HSV in NH format, combined with RGBA in [0, 255] range
#[derive(Debug, Clone, Serialize)]
pub struct HSVRGBA {
    pub h: u8,
    pub s: u8,
    pub v: u8,
    pub r: u8,
    pub g: u8,
    pub b: u8,
    pub a: u8,
}

impl Default for HSVA {
    fn default() -> Self {
        HSVA {
            h: 0.0,
            s: 0.0,
            v: 0.0,
            a: 1.0,
        }
    }
}

impl Default for NHPaletteItem {
    fn default() -> Self {
        NHPaletteItem::Color{
            h: 0,
            s: 0,
            v: 0,
        }
    }
}

impl From<&Color> for HSVA {
    fn from(color: &Color) -> Self {
        let a = color.a as f32 / 255.0;

        let r_prime = color.r as f32 / 255.0;
        if color.r == color.g && color.g == color.b {
            return HSVA {
                h: 0.0,
                s: 0.0,
                v: r_prime,
                a,
            };
        }
        let g_prime = color.g as f32 / 255.0;
        let b_prime = color.b as f32 / 255.0;
        let c_max = r_prime.max(g_prime).max(b_prime);
        let c_min = r_prime.min(g_prime).min(b_prime);
        let delta = c_max - c_min;
        let mut hue = 60.0
            * if c_max == r_prime {
                (g_prime - b_prime) / delta
            } else if c_max == g_prime {
                (b_prime - r_prime) / delta + 2.0
            } else {
                (r_prime - g_prime) / delta + 4.0
            };
        while hue < 0.0 {
            hue += 360.0;
        }
        while hue >= 360.0 {
            hue -= 360.0;
        }
        let saturation = if c_max == 0.0 { 0.0 } else { delta / c_max };
        HSVA {
            h: hue,
            s: saturation,
            v: c_max,
            a,
        }
    }
}

impl From<&HSVA> for Color {
    fn from(hsv: &HSVA) -> Self {
        let a = (hsv.a * 255.0).round() as u8;
        if hsv.v == 0.0 {
            return Color::new(0, 0, 0, a);
        } else if hsv.s == 0.0 {
            let v = (hsv.v * 255.0).round() as u8;
            return Color::new(v, v, v, a);
        }
        let chroma = hsv.v * hsv.s;
        let h_prime = hsv.h / 60.0;
        let x = chroma * (1.0 - (h_prime % 2.0 - 1.0).abs());
        let (r_prime, g_prime, b_prime) = if h_prime >= 0.0 && h_prime <= 1.0 {
            (chroma, x, 0.0)
        } else if h_prime <= 2.0 {
            (x, chroma, 0.0)
        } else if h_prime <= 3.0 {
            (0.0, chroma, x)
        } else if h_prime <= 4.0 {
            (0.0, x, chroma)
        } else if h_prime <= 5.0 {
            (x, 0.0, chroma)
        } else if h_prime <= 6.0 {
            (chroma, 0.0, x)
        } else {
            panic!("this should never ever happen");
        };
        let m = hsv.v - chroma;
        Color {
            r: ((r_prime + m) * 255.0).round() as u8,
            g: ((g_prime + m) * 255.0).round() as u8,
            b: ((b_prime + m) * 255.0).round() as u8,
            a,
        }
    }
}

impl From<&HSVA> for NHPaletteItem {
    fn from(hsv: &HSVA) -> Self {
        if hsv.a == 0.0 {
            NHPaletteItem::Transparent
        } else {
            // Saturation is [0, 14], and doesn't actually allow full saturation, so we
            // scale to [0, 15] and have to drop the top.  Later, an option for global
            // desaturation may be made possible, but until then, it will have to be manual.
            let v = ((hsv.v * 15.0).round() as u8).min(14).max(0);

            // If value is 0, saturation is meaningless, so lock it to 0.
            let s = if v == 0 {
                0
            } else {
                ((hsv.s * 15.0).round() as u8).min(14).max(0)
            };
            // Hue is [0, 29] for 30 full colors.  30 is full red, and has wraparound, so we scale
            // from 0-30 and set a modulus.  Hue is meaningless with either 0 saturation or value,
            // so lock it to 0 in those cases.  If v == 0, then s == 0.
            let h = if s == 0 {
                0
            } else {
                ((hsv.h / 360.0 * 30.0).round() as u8) % 30
            };
            NHPaletteItem::Color{
                h,
                s,
                v,
            }
        }
    }
}

impl From<&NHPaletteItem> for HSVA {
    fn from(palette_item: &NHPaletteItem) -> Self {
        match palette_item {
            NHPaletteItem::Transparent => HSVA {
                h: 0.0,
                s: 0.0,
                v: 0.0,
                a: 0.0,
            },
            NHPaletteItem::Color{h, s, v, ..} => HSVA {
                h: *h as f32 * (360.0 / 30.0),
                s: *s as f32 / 15.0,
                v: *v as f32 / 15.0,
                a: 1.0,
            },
        }
    }
}

impl From<&Color> for NHPaletteItem {
    fn from(color: &Color) -> Self {
        let hsv: HSVA = color.into();
        (&hsv).into()
    }
}

impl From<&NHPaletteItem> for Color {
    fn from(palette_item: &NHPaletteItem) -> Self {
        let hsv: HSVA = palette_item.into();
        (&hsv).into()
    }
}

impl From<&HSVRGBA> for NHPaletteItem {
    fn from(color: &HSVRGBA) -> Self {
        if color.a == 0 {
            NHPaletteItem::Transparent
        } else {
            NHPaletteItem::Color{
                h: color.h,
                s: color.s,
                v: color.v,
            }
        }
    }
}

impl From<&NHPaletteItem> for HSVRGBA {
    fn from(palette_item: &NHPaletteItem) -> Self {
        let color: Color = palette_item.into();
        let (h, s, v) = match palette_item {
            NHPaletteItem::Transparent => (0, 0, 0),
            NHPaletteItem::Color{h, s, v, ..} => (*h, *s, *v),
        };

        HSVRGBA {
            h,
            s,
            v,
            r: color.r,
            g: color.g,
            b: color.b,
            a: color.a,
        }
    }
}

// Convenience conversion traits for moves

impl From<Color> for HSVA {
    fn from(color: Color) -> Self {
        (&color).into()
    }
}

impl From<HSVA> for Color {
    fn from(hsv: HSVA) -> Self {
        (&hsv).into()
    }
}

impl From<HSVA> for NHPaletteItem {
    fn from(hsv: HSVA) -> Self {
        (&hsv).into()
    }
}

impl From<NHPaletteItem> for HSVA {
    fn from(palette_item: NHPaletteItem) -> Self {
        (&palette_item).into()
    }
}

impl From<Color> for NHPaletteItem {
    fn from(color: Color) -> Self {
        (&color).into()
    }
}

impl From<NHPaletteItem> for Color {
    fn from(palette_item: NHPaletteItem) -> Self {
        (&palette_item).into()
    }
}

impl From<HSVRGBA> for NHPaletteItem {
    fn from(color: HSVRGBA) -> Self {
        (&color).into()
    }
}

impl From<NHPaletteItem> for HSVRGBA {
    fn from(palette_item: NHPaletteItem) -> Self {
        (&palette_item).into()
    }
}