auto-palette 0.9.0

🎨 A Rust library that extracts prominent color palettes from images automatically.
Documentation
use std::{fmt::Display, marker::PhantomData};

use num_traits::clamp;
#[cfg(feature = "wasm")]
use serde::{Deserialize, Serialize};

use crate::{
    color::{Hue, Luv, WhitePoint, D65},
    math::FloatNumber,
};

/// The LCHuv color representation.
///
/// See the following for more details:
/// [CIE LCHuv color space - Wikipedia](https://en.wikipedia.org/wiki/CIELUV#Cylindrical_representation_(CIELCh))
///
/// # Type Parameters
/// * `T` - The floating point type.
/// * `W` - The white point type.
///
/// # Fields
/// * `l` - The lightness component.
/// * `c` - The chroma component.
/// * `h` - The hue component.
///
/// # Examples
/// ```
/// use auto_palette::color::{LCHuv, Luv, D65};
///
/// let lchuv: LCHuv<_> = LCHuv::new(56.232, 50.875, 154.710);
/// assert_eq!(format!("{}", lchuv), "LCH(uv)(56.23, 50.88, 154.71)");
///
/// let luv: Luv<_> = (&lchuv).into();
/// assert_eq!(format!("{}", luv), "Luv(56.23, -46.00, 21.73)");
/// ```
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "wasm", derive(Serialize, Deserialize))]
pub struct LCHuv<T = f64, W = D65>
where
    T: FloatNumber,
    W: WhitePoint,
{
    pub l: T,
    pub c: T,
    pub h: Hue<T>,
    #[cfg_attr(feature = "wasm", serde(skip))]
    _marker: PhantomData<W>,
}

impl<T, W> LCHuv<T, W>
where
    T: FloatNumber,
    W: WhitePoint,
{
    /// Creates a new `LCHuv` instance.
    ///
    /// # Arguments
    /// * `l` - The lightness component.
    /// * `c` - The chroma component.
    /// * `h` - The hue component.
    ///
    /// # Returns
    /// A new `LCHuv` instance.
    #[must_use]
    pub fn new(l: T, c: T, h: T) -> Self {
        Self {
            l: clamp(l, T::zero(), T::from_u32(100)),
            c: clamp(c, T::zero(), T::from_u32(180)),
            h: Hue::from_degrees(h),
            _marker: PhantomData,
        }
    }
}

impl<T, W> Display for LCHuv<T, W>
where
    T: FloatNumber,
    W: WhitePoint,
{
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "LCH(uv)({:.2}, {:.2}, {:.2})",
            self.l,
            self.c,
            self.h.to_degrees()
        )
    }
}

impl<T, W> From<&Luv<T, W>> for LCHuv<T, W>
where
    T: FloatNumber,
    W: WhitePoint,
{
    fn from(luv: &Luv<T, W>) -> Self {
        // This implementation is based on the formulae from the following sources:
        // http://www.brucelindbloom.com/index.html?Eqn_Luv_to_LCH.html
        let l = luv.l;
        let c = (luv.u * luv.u + luv.v * luv.v).sqrt();
        let h = luv.v.atan2(luv.u).to_degrees();
        Self::new(l, c, h)
    }
}

#[cfg(test)]
mod tests {
    #[cfg(feature = "wasm")]
    use serde_test::{assert_de_tokens, assert_ser_tokens, Token};

    use super::*;
    use crate::{assert_approx_eq, color::Luv};

    #[test]
    fn test_new() {
        // Act
        let actual: LCHuv<_> = LCHuv::new(56.232, 50.875, 154.710);

        // Assert
        assert_eq!(
            actual,
            LCHuv {
                l: 56.232,
                c: 50.875,
                h: Hue::from_degrees(154.710),
                _marker: PhantomData,
            }
        );
    }

    #[test]
    #[cfg(feature = "wasm")]
    fn test_serialize() {
        // Act
        let lchuv: LCHuv<f64> = LCHuv::new(56.232, 50.875, 154.710);

        // Assert
        assert_ser_tokens(
            &lchuv,
            &[
                Token::Struct {
                    name: "LCHuv",
                    len: 3,
                },
                Token::Str("l"),
                Token::F64(56.232),
                Token::Str("c"),
                Token::F64(50.875),
                Token::Str("h"),
                Token::F64(154.710),
                Token::StructEnd,
            ],
        )
    }

    #[test]
    #[cfg(feature = "wasm")]
    fn test_deserialize() {
        // Act
        let lchuv: LCHuv<f64> = LCHuv::new(56.232, 50.875, 154.710);

        // Assert
        assert_de_tokens(
            &lchuv,
            &[
                Token::Struct {
                    name: "LCHuv",
                    len: 3,
                },
                Token::Str("l"),
                Token::F64(56.232),
                Token::Str("c"),
                Token::F64(50.875),
                Token::Str("h"),
                Token::F64(154.710),
                Token::StructEnd,
            ],
        )
    }

    #[test]
    fn test_fmt() {
        // Act
        let lchuv: LCHuv<_> = LCHuv::new(56.232, 50.875, 154.710);
        let actual = format!("{}", lchuv);

        // Assert
        assert_eq!(actual, "LCH(uv)(56.23, 50.88, 154.71)");
    }

    #[test]
    fn test_from_luv() {
        // Act
        let luv: Luv<f32> = Luv::new(56.232, -45.999, 21.734);
        let actual = LCHuv::from(&luv);

        // Assert
        assert_approx_eq!(actual.l, 56.231998);
        assert_approx_eq!(actual.c, 50.875087);
        assert_approx_eq!(actual.h.to_degrees(), 154.709808);
    }
}