From 023cc71c21f911122784e6fe0e2acec9258e5c2b Mon Sep 17 00:00:00 2001 From: Mason Smith Date: Wed, 12 May 2021 15:01:26 -0700 Subject: [PATCH 1/5] implement HSLuv as `Hsluv` --- palette/src/convert.rs | 22 +- palette/src/equality.rs | 4 +- palette/src/hsluv.rs | 665 +++++++++++++++++++ palette/src/lchuv.rs | 23 +- palette/src/lib.rs | 3 + palette/src/luv_bounds.rs | 168 +++++ palette/tests/hsluv_dataset/hsluv_dataset.rs | 55 +- palette_derive/src/lib.rs | 3 +- 8 files changed, 918 insertions(+), 25 deletions(-) create mode 100644 palette/src/hsluv.rs create mode 100644 palette/src/luv_bounds.rs diff --git a/palette/src/convert.rs b/palette/src/convert.rs index 54cf7af4c..415e1e2e5 100644 --- a/palette/src/convert.rs +++ b/palette/src/convert.rs @@ -529,7 +529,7 @@ mod tests { use crate::encoding::linear::Linear; use crate::luma::{Luma, LumaStandard}; use crate::rgb::{Rgb, RgbSpace}; - use crate::{Alpha, Hsl, Hsv, Hwb, Lab, Lch, Luv, Xyz, Yxy}; + use crate::{Alpha, Hsl, Hsluv, Hsv, Hwb, Lab, Lch, Luv, Xyz, Yxy}; use crate::{Clamp, FloatComponent}; #[derive(FromColorUnclamped, WithAlpha)] @@ -684,6 +684,9 @@ mod tests { let hsl: Hsl<_, f64> = Default::default(); WithXyz::::from_color(hsl); + let hsluv: Hsluv<_, f64> = Default::default(); + WithXyz::::from_color(hsluv); + let hsv: Hsv<_, f64> = Default::default(); WithXyz::::from_color(hsv); @@ -721,6 +724,9 @@ mod tests { let hsl: Alpha, u8> = Alpha::from(Hsl::default()); WithXyz::::from_color(hsl); + let hsluv: Alpha, u8> = Alpha::from(Hsluv::default()); + WithXyz::::from_color(hsluv); + let hsv: Alpha, u8> = Alpha::from(Hsv::default()); WithXyz::::from_color(hsv); @@ -758,6 +764,9 @@ mod tests { let hsl: Hsl<_, f64> = Default::default(); Alpha::, u8>::from_color(hsl); + let hsluv: Hsluv<_, f64> = Default::default(); + Alpha::, u8>::from_color(hsluv); + let hsv: Hsv<_, f64> = Default::default(); Alpha::, u8>::from_color(hsv); @@ -795,6 +804,9 @@ mod tests { let hsl: Hsl<_, f64> = Default::default(); Alpha::, u8>::from_color(hsl); + let hsluv: Hsluv<_, f64> = Default::default(); + Alpha::, u8>::from_color(hsluv); + let hsv: Hsv<_, f64> = Default::default(); Alpha::, u8>::from_color(hsv); @@ -817,6 +829,7 @@ mod tests { let _luv: Luv<_, f64> = color.into_color(); let _rgb: Rgb<_, f64> = color.into_color(); let _hsl: Hsl<_, f64> = color.into_color(); + let _hsluv: Hsluv<_, f64> = color.into_color(); let _hsv: Hsv<_, f64> = color.into_color(); let _hwb: Hwb<_, f64> = color.into_color(); let _luma: Luma = color.into_color(); @@ -835,6 +848,7 @@ mod tests { let _luv: Luv<_, f64> = color.into_color(); let _rgb: Rgb<_, f64> = color.into_color(); let _hsl: Hsl<_, f64> = color.into_color(); + let _hsluv: Hsluv<_, f64> = color.into_color(); let _hsv: Hsv<_, f64> = color.into_color(); let _hwb: Hwb<_, f64> = color.into_color(); let _luma: Luma = color.into_color(); @@ -852,6 +866,7 @@ mod tests { let _luv: Alpha, u8> = color.into_color(); let _rgb: Alpha, u8> = color.into_color(); let _hsl: Alpha, u8> = color.into_color(); + let _hsluv: Alpha, u8> = color.into_color(); let _hsv: Alpha, u8> = color.into_color(); let _hwb: Alpha, u8> = color.into_color(); let _luma: Alpha, u8> = color.into_color(); @@ -870,6 +885,7 @@ mod tests { let _luv: Alpha, u8> = color.into_color(); let _rgb: Alpha, u8> = color.into_color(); let _hsl: Alpha, u8> = color.into_color(); + let _hsluv: Alpha, u8> = color.into_color(); let _hsv: Alpha, u8> = color.into_color(); let _hwb: Alpha, u8> = color.into_color(); let _luma: Alpha, u8> = color.into_color(); @@ -901,6 +917,9 @@ mod tests { let hsl: Hsl<_, f64> = Default::default(); WithoutXyz::::from_color(hsl); + let hsluv: Hsluv<_, f64> = Default::default(); + WithoutXyz::::from_color(hsluv); + let hsv: Hsv<_, f64> = Default::default(); WithoutXyz::::from_color(hsv); @@ -923,6 +942,7 @@ mod tests { let _luv: Luv = color.into_color(); let _rgb: Rgb<_, f64> = color.into_color(); let _hsl: Hsl<_, f64> = color.into_color(); + let _hsluv: Hsluv<_, f64> = color.into_color(); let _hsv: Hsv<_, f64> = color.into_color(); let _hwb: Hwb<_, f64> = color.into_color(); let _luma: Luma, f64> = color.into_color(); diff --git a/palette/src/equality.rs b/palette/src/equality.rs index ae25c34e6..9822ac2f5 100644 --- a/palette/src/equality.rs +++ b/palette/src/equality.rs @@ -3,7 +3,8 @@ use approx::{AbsDiffEq, RelativeEq, UlpsEq}; use crate::float::Float; use crate::white_point::WhitePoint; use crate::{ - from_f64, FloatComponent, FromF64, Lab, LabHue, Lch, Lchuv, Luv, LuvHue, RgbHue, Xyz, Yxy, + from_f64, FloatComponent, FromF64, Hsluv, Lab, LabHue, Lch, Lchuv, Luv, LuvHue, RgbHue, Xyz, + Yxy, }; macro_rules! impl_eq { @@ -69,6 +70,7 @@ impl_eq!(Lab, [l, a, b]); impl_eq!(Luv, [l, u, v]); impl_eq!(Lch, [l, chroma, hue]); impl_eq!(Lchuv, [l, chroma, hue]); +impl_eq!(Hsluv, [hue, saturation, l]); // For hues, the difference is calculated and compared to zero. However due to // the way floating point's work this is not so simple. diff --git a/palette/src/hsluv.rs b/palette/src/hsluv.rs new file mode 100644 index 000000000..fe757e32e --- /dev/null +++ b/palette/src/hsluv.rs @@ -0,0 +1,665 @@ +use core::marker::PhantomData; +use core::ops::{Add, AddAssign, Sub, SubAssign}; + +use crate::encoding::pixel::RawPixel; +use crate::luv_bounds::LuvBounds; +use crate::{ + clamp, contrast_ratio, + convert::FromColorUnclamped, + white_point::{WhitePoint, D65}, + Alpha, Clamp, Component, FloatComponent, GetHue, Hue, Lchuv, LuvHue, Mix, Pixel, + RelativeContrast, Saturate, Shade, Xyz, +}; + +/// HSLuv with an alpha component. See the [`Hsluva` implementation in +/// `Alpha`](crate::Alpha#Hsluva). +pub type Hsluva = Alpha, T>; + +/// HSLuv color space. +/// +/// The HSLuv color space can be seen as a cylindrical version of +/// [CIELUV](crate::luv::Luv), similar to +/// [LCHuv](crate::lchuv::Lchuv), with the additional benefit of +/// streching the chroma values to a uniform saturation range [0.0, +/// 100.0]. This makes HSLuv much more convenient for generating +/// colors than Lchuv, as the set of valid saturation values is +/// independent of lightness and hue. +#[derive(Debug, Pixel, FromColorUnclamped, WithAlpha)] +#[cfg_attr(feature = "serializing", derive(Serialize, Deserialize))] +#[palette( + palette_internal, + white_point = "Wp", + component = "T", + skip_derives(Lchuv, Hsluv) +)] +#[repr(C)] +pub struct Hsluv +where + T: FloatComponent, + Wp: WhitePoint, +{ + /// The hue of the color, in degrees. Decides if it's red, blue, purple, + /// etc. + #[palette(unsafe_same_layout_as = "T")] + pub hue: LuvHue, + + /// The colorfulness of the color, as a percentage of the maximum + /// available chroma. 0.0 gives gray scale colors and 100.0 will + /// give absolutely clear colors. + pub saturation: T, + + /// Decides how light the color will look. 0.0 will be black, 50.0 will give + /// a clear color, and 100.0 will give white. + pub l: T, + + /// The white point and RGB primaries this color is adapted to. The default + /// is the sRGB standard. + #[cfg_attr(feature = "serializing", serde(skip))] + #[palette(unsafe_zero_sized)] + pub white_point: PhantomData, +} + +impl Copy for Hsluv +where + T: FloatComponent, + Wp: WhitePoint, +{ +} + +impl Clone for Hsluv +where + T: FloatComponent, + Wp: WhitePoint, +{ + fn clone(&self) -> Hsluv { + *self + } +} + +impl Hsluv +where + T: FloatComponent, +{ + /// HSLuv with standard D65 whitepoint + pub fn new>>(hue: H, saturation: T, l: T) -> Hsluv { + Hsluv { + hue: hue.into(), + saturation, + l, + white_point: PhantomData, + } + } +} + +impl Hsluv +where + T: FloatComponent, + Wp: WhitePoint, +{ + /// HSLuv with custom whitepoint. + pub fn with_wp>>(hue: H, saturation: T, l: T) -> Hsluv { + Hsluv { + hue: hue.into(), + saturation, + l, + white_point: PhantomData, + } + } + + /// Convert to a `(hue, saturation, l)` tuple. + pub fn into_components(self) -> (LuvHue, T, T) { + (self.hue, self.saturation, self.l) + } + + /// Convert from a `(hue, saturation, l)` tuple. + pub fn from_components>>((hue, saturation, l): (H, T, T)) -> Self { + Self::with_wp(hue, saturation, l) + } + + /// Return the `saturation` value minimum. + pub fn min_saturation() -> T { + T::zero() + } + + /// Return the `saturation` value maximum. + pub fn max_saturation() -> T { + T::from_f64(100.0) + } + + /// Return the `l` value minimum. + pub fn min_l() -> T { + T::zero() + } + + /// Return the `l` value maximum. + pub fn max_l() -> T { + T::from_f64(100.0) + } +} + +impl PartialEq for Hsluv +where + T: FloatComponent + PartialEq, + Wp: WhitePoint, +{ + fn eq(&self, other: &Self) -> bool { + self.hue == other.hue && self.saturation == other.saturation && self.l == other.l + } +} + +impl Eq for Hsluv +where + T: FloatComponent + Eq, + Wp: WhitePoint, +{ +} + +///[`Hsluva`](crate::Hsluva) implementations. +impl Alpha, A> +where + T: FloatComponent, + A: Component, +{ + /// HSLuv and transparency with standard D65 whitepoint. + pub fn new>>(hue: H, saturation: T, l: T, alpha: A) -> Self { + Alpha { + color: Hsluv::new(hue, saturation, l), + alpha, + } + } +} + +///[`Hsluva`](crate::Hsluva) implementations. +impl Alpha, A> +where + T: FloatComponent, + A: Component, + Wp: WhitePoint, +{ + /// HSLuv and transparency. + pub fn with_wp>>(hue: H, saturation: T, l: T, alpha: A) -> Self { + Alpha { + color: Hsluv::with_wp(hue, saturation, l), + alpha, + } + } + + /// Convert to a `(hue, saturation, l, alpha)` tuple. + pub fn into_components(self) -> (LuvHue, T, T, A) { + (self.hue, self.saturation, self.l, self.alpha) + } + + /// Convert from a `(hue, saturation, l, alpha)` tuple. + pub fn from_components>>((hue, saturation, l, alpha): (H, T, T, A)) -> Self { + Self::with_wp(hue, saturation, l, alpha) + } +} + +impl FromColorUnclamped> for Hsluv +where + T: FloatComponent, + Wp: WhitePoint, +{ + fn from_color_unclamped(hsluv: Hsluv) -> Self { + hsluv + } +} + +impl FromColorUnclamped> for Hsluv +where + T: FloatComponent, + Wp: WhitePoint, +{ + fn from_color_unclamped(color: Lchuv) -> Self { + // convert the chroma to a saturation based on the max + // saturation at a particular hue. + let max_chroma = LuvBounds::from_lightness(color.l).max_chroma_at_hue(color.hue); + + Hsluv::with_wp( + color.hue, + color.chroma / max_chroma * T::from_f64(100.0), + color.l, + ) + } +} + +impl>> From<(H, T, T)> for Hsluv { + fn from(components: (H, T, T)) -> Self { + Self::from_components(components) + } +} + +impl Into<(LuvHue, T, T)> for Hsluv { + fn into(self) -> (LuvHue, T, T) { + self.into_components() + } +} + +impl>, A: Component> From<(H, T, T, A)> + for Alpha, A> +{ + fn from(components: (H, T, T, A)) -> Self { + Self::from_components(components) + } +} + +impl Into<(LuvHue, T, T, A)> + for Alpha, A> +{ + fn into(self) -> (LuvHue, T, T, A) { + self.into_components() + } +} + +impl Clamp for Hsluv +where + T: FloatComponent, + Wp: WhitePoint, +{ + #[rustfmt::skip] + fn is_within_bounds(&self) -> bool { + self.saturation >= Self::min_saturation() && self.saturation <= Self::max_saturation() && + self.l >= Self::min_l() && self.l <= Self::max_l() + } + + fn clamp(&self) -> Hsluv { + let mut c = *self; + c.clamp_self(); + c + } + + fn clamp_self(&mut self) { + self.saturation = clamp( + self.saturation, + Self::min_saturation(), + Self::max_saturation(), + ); + self.l = clamp(self.l, Self::min_l(), Self::max_l()); + } +} + +impl Mix for Hsluv +where + T: FloatComponent, + Wp: WhitePoint, +{ + type Scalar = T; + + fn mix(&self, other: &Hsluv, factor: T) -> Hsluv { + let factor = clamp(factor, T::zero(), T::one()); + let hue_diff: T = (other.hue - self.hue).to_degrees(); + + Hsluv { + hue: self.hue + factor * hue_diff, + saturation: self.saturation + factor * (other.saturation - self.saturation), + l: self.l + factor * (other.l - self.l), + white_point: PhantomData, + } + } +} + +impl Shade for Hsluv +where + T: FloatComponent, + Wp: WhitePoint, +{ + type Scalar = T; + + fn lighten(&self, factor: T) -> Hsluv { + let difference = if factor >= T::zero() { + Self::max_l() - self.l + } else { + self.l + }; + + let delta = difference.max(T::zero()) * factor; + + Hsluv { + hue: self.hue, + saturation: self.saturation, + l: (self.l + delta).max(T::zero()), + white_point: PhantomData, + } + } + + fn lighten_fixed(&self, amount: T) -> Hsluv { + Hsluv { + hue: self.hue, + saturation: self.saturation, + l: (self.l + Self::max_l() * amount).max(T::zero()), + white_point: PhantomData, + } + } +} + +impl GetHue for Hsluv +where + T: FloatComponent, + Wp: WhitePoint, +{ + type Hue = LuvHue; + + fn get_hue(&self) -> Option> { + if self.saturation <= T::zero() { + None + } else { + Some(self.hue) + } + } +} + +impl Hue for Hsluv +where + T: FloatComponent, + Wp: WhitePoint, +{ + fn with_hue>(&self, hue: H) -> Hsluv { + Hsluv { + hue: hue.into(), + saturation: self.saturation, + l: self.l, + white_point: PhantomData, + } + } + + fn shift_hue>(&self, amount: H) -> Hsluv { + Hsluv { + hue: self.hue + amount.into(), + saturation: self.saturation, + l: self.l, + white_point: PhantomData, + } + } +} + +impl Saturate for Hsluv +where + T: FloatComponent, + Wp: WhitePoint, +{ + type Scalar = T; + + fn saturate(&self, factor: T) -> Hsluv { + let difference = if factor >= T::zero() { + Self::max_saturation() - self.saturation + } else { + self.saturation + }; + + let delta = difference.max(T::zero()) * factor; + + Hsluv { + hue: self.hue, + saturation: clamp( + self.saturation + delta, + Self::min_saturation(), + Self::max_saturation(), + ), + l: self.l, + white_point: PhantomData, + } + } + + fn saturate_fixed(&self, amount: T) -> Hsluv { + Hsluv { + hue: self.hue, + saturation: clamp( + self.saturation + Self::max_saturation() * amount, + Self::min_saturation(), + Self::max_saturation(), + ), + l: self.l, + white_point: PhantomData, + } + } +} + +impl Default for Hsluv +where + T: FloatComponent, + Wp: WhitePoint, +{ + fn default() -> Hsluv { + Hsluv::with_wp(LuvHue::from(T::zero()), T::zero(), T::zero()) + } +} + +impl Add> for Hsluv +where + T: FloatComponent, + Wp: WhitePoint, +{ + type Output = Hsluv; + + fn add(self, other: Hsluv) -> Self::Output { + Hsluv { + hue: self.hue + other.hue, + saturation: self.saturation + other.saturation, + l: self.l + other.l, + standard: PhantomData, + } + } +} + +impl Add for Hsluv +where + T: FloatComponent, + Wp: WhitePoint, +{ + type Output = Hsluv; + + fn add(self, c: T) -> Self::Output { + Hsluv { + hue: self.hue + c, + saturation: self.saturation + c, + l: self.l + c, + standard: PhantomData, + } + } +} + +impl AddAssign> for Hsluv +where + T: FloatComponent + AddAssign, + Wp: WhitePoint, +{ + fn add_assign(&mut self, other: Hsluv) { + self.hue += other.hue; + self.saturation += other.saturation; + self.l += other.l; + } +} + +impl AddAssign for Hsluv +where + T: FloatComponent + AddAssign, + Wp: WhitePoint, +{ + fn add_assign(&mut self, c: T) { + self.hue += c; + self.saturation += c; + self.l += c; + } +} + +impl Sub> for Hsluv +where + T: FloatComponent, + Wp: WhitePoint, +{ + type Output = Hsluv; + + fn sub(self, other: Hsluv) -> Self::Output { + Hsluv { + hue: self.hue - other.hue, + saturation: self.saturation - other.saturation, + l: self.l - other.l, + standard: PhantomData, + } + } +} + +impl Sub for Hsluv +where + T: FloatComponent, + Wp: WhitePoint, +{ + type Output = Hsluv; + + fn sub(self, c: T) -> Self::Output { + Hsluv { + hue: self.hue - c, + saturation: self.saturation - c, + l: self.l - c, + standard: PhantomData, + } + } +} + +impl SubAssign> for Hsluv +where + T: FloatComponent + SubAssign, + Wp: WhitePoint, +{ + fn sub_assign(&mut self, other: Hsluv) { + self.hue -= other.hue; + self.saturation -= other.saturation; + self.l -= other.l; + } +} + +impl SubAssign for Hsluv +where + T: FloatComponent + SubAssign, + Wp: WhitePoint, +{ + fn sub_assign(&mut self, c: T) { + self.hue -= c; + self.saturation -= c; + self.l -= c; + } +} + +impl AsRef

for Hsluv +where + T: FloatComponent, + Wp: WhitePoint, + P: RawPixel + ?Sized, +{ + fn as_ref(&self) -> &P { + self.as_raw() + } +} + +impl AsMut

for Hsluv +where + T: FloatComponent, + Wp: WhitePoint, + P: RawPixel + ?Sized, +{ + fn as_mut(&mut self) -> &mut P { + self.as_raw_mut() + } +} + +impl RelativeContrast for Hsluv +where + T: FloatComponent, + Wp: WhitePoint, +{ + type Scalar = T; + + fn get_contrast_ratio(&self, other: &Self) -> T { + use crate::FromColor; + + let xyz1 = Xyz::from_color(*self); + let xyz2 = Xyz::from_color(*other); + + contrast_ratio(xyz1.y, xyz2.y) + } +} + +#[cfg(test)] +mod test { + use super::Hsluv; + use crate::{white_point::D65, FromColor, Lchuv, LuvHue, Saturate}; + + #[test] + fn lchuv_round_trip() { + for hue in (0..=20).map(|x| x as f64 * 18.0) { + for sat in (0..=20).map(|x| x as f64 * 5.0) { + for l in (1..=20).map(|x| x as f64 * 5.0) { + let hsluv = Hsluv::new(hue, sat, l); + let lchuv = Lchuv::from_color(hsluv); + let mut to_hsluv = Hsluv::from_color(lchuv); + if to_hsluv.l < 1e-8 { + to_hsluv.hue = LuvHue::from(0.0); + } + assert_relative_eq!(hsluv, to_hsluv, epsilon = 1e-5); + } + } + } + } + + #[test] + fn ranges() { + assert_ranges! { + Hsluv; + clamped { + saturation: 0.0 => 100.0, + l: 0.0 => 100.0 + } + clamped_min {} + unclamped { + hue: -360.0 => 360.0 + } + } + } + + #[test] + fn saturate() { + for sat in (0..=10).map(|s| s as f64 * 10.0) { + for a in (0..=10).map(|l| l as f64 * 10.0) { + let hsl = Hsluv::new(150.0, sat, a); + let hsl_sat_fixed = hsl.saturate_fixed(0.1); + let expected_sat_fixed = Hsluv::new(150.0, (sat + 10.0).min(100.0), a); + assert_relative_eq!(hsl_sat_fixed, expected_sat_fixed); + + let hsl_sat = hsl.saturate(0.1); + let expected_sat = Hsluv::new(150.0, (sat + (100.0 - sat) * 0.1).min(100.0), a); + assert_relative_eq!(hsl_sat, expected_sat); + } + } + } + + raw_pixel_conversion_tests!(Hsluv: hue, saturation, lightness); + raw_pixel_conversion_fail_tests!(Hsluv: hue, saturation, lightness); + + #[test] + fn check_min_max_components() { + assert_relative_eq!(Hsluv::::min_saturation(), 0.0); + assert_relative_eq!(Hsluv::::min_l(), 0.0); + assert_relative_eq!(Hsluv::::max_saturation(), 100.0); + assert_relative_eq!(Hsluv::::max_l(), 100.0); + } + + #[cfg(feature = "serializing")] + #[test] + fn serialize() { + let serialized = ::serde_json::to_string(&Hsluv::new(120.0, 80.0, 60.0)).unwrap(); + + assert_eq!( + serialized, + r#"{"hue":120.0,"saturation":80.0,"l":60.0}"# + ); + } + + #[cfg(feature = "serializing")] + #[test] + fn deserialize() { + let deserialized: Hsluv = + ::serde_json::from_str(r#"{"hue":120.0,"saturation":80.0,"l":60.0}"#).unwrap(); + + assert_eq!(deserialized, Hsluv::new(120.0, 80.0, 60.0)); + } +} diff --git a/palette/src/lchuv.rs b/palette/src/lchuv.rs index 10a9d8395..190925e3c 100644 --- a/palette/src/lchuv.rs +++ b/palette/src/lchuv.rs @@ -10,10 +10,11 @@ use rand::Rng; use crate::convert::FromColorUnclamped; use crate::encoding::pixel::RawPixel; +use crate::luv_bounds::LuvBounds; use crate::white_point::{WhitePoint, D65}; use crate::{ clamp, contrast_ratio, from_f64, Alpha, Clamp, Component, FloatComponent, FromColor, GetHue, - Hue, Luv, LuvHue, Mix, Pixel, RelativeContrast, Saturate, Shade, Xyz, + Hsluv, Hue, Luv, LuvHue, Mix, Pixel, RelativeContrast, Saturate, Shade, Xyz, }; /// CIE L\*C\*uv h°uv with an alpha component. See the [`Lchuva` implementation in @@ -32,7 +33,7 @@ pub type Lchuva = Alpha, T>; palette_internal, white_point = "Wp", component = "T", - skip_derives(Luv, Lchuv) + skip_derives(Luv, Lchuv, Hsluv) )] #[repr(C)] pub struct Lchuv @@ -224,6 +225,24 @@ where } } +impl FromColorUnclamped> for Lchuv +where + Wp: WhitePoint, + T: FloatComponent, +{ + fn from_color_unclamped(color: Hsluv) -> Self { + // Apply the given saturation as a percentage of the max + // chroma for that hue. + let max_chroma = LuvBounds::from_lightness(color.l).max_chroma_at_hue(color.hue); + + Lchuv::with_wp( + color.l, + color.saturation * max_chroma * T::from_f64(0.01), + color.hue, + ) + } +} + impl>> From<(T, T, H)> for Lchuv { fn from(components: (T, T, H)) -> Self { Self::from_components(components) diff --git a/palette/src/lib.rs b/palette/src/lib.rs index 11796e959..67d38d9dd 100644 --- a/palette/src/lib.rs +++ b/palette/src/lib.rs @@ -238,6 +238,7 @@ pub use blend::Blend; pub use gradient::Gradient; pub use hsl::{Hsl, Hsla}; +pub use hsluv::{Hsluv, Hsluva}; pub use hsv::{Hsv, Hsva}; pub use hwb::{Hwb, Hwba}; pub use lab::{Lab, Laba}; @@ -423,6 +424,7 @@ mod random_sampling; mod alpha; mod hsl; +mod hsluv; mod hsv; mod hwb; mod lab; @@ -442,6 +444,7 @@ mod component; pub mod convert; pub mod encoding; mod equality; +mod luv_bounds; mod relative_contrast; pub mod white_point; diff --git a/palette/src/luv_bounds.rs b/palette/src/luv_bounds.rs new file mode 100644 index 000000000..931c4c704 --- /dev/null +++ b/palette/src/luv_bounds.rs @@ -0,0 +1,168 @@ +//! Utility functions for computing in-gamut regions for CIELuv color space. +use crate::{FloatComponent, LuvHue}; +#[allow(unused)] +use num_traits::Float; +use num_traits::{Pow, ToPrimitive}; + +/// Boundary line in the u-v plane of the Luv color space. +struct BoundaryLine { + slope: f64, + intercept: f64, +} + +impl BoundaryLine { + /// Given array starting at the origin at angle theta, determine + /// the signed length at which the ray intersects with the + /// boundary. + fn intersect_length_at_angle(&self, theta: f64) -> Option { + let (sin_theta, cos_theta) = theta.to_f64().unwrap().sin_cos(); + let denom = sin_theta - self.slope * cos_theta; + if denom.abs() > 1.0e-6 { + Some(self.intercept / denom) + } else { + None + } + } + + /// Return the distance from this line to the origin. + #[allow(unused)] + fn distance_to_origin(&self) -> f64 { + self.intercept.abs() / (self.slope * self.slope + 1.0).sqrt() + } +} + +/// `LuvBounds` represents the convex polygon formed by the in-gamut +/// region in the uv plane at a given lightness. +pub(crate) struct LuvBounds { + bounds: [BoundaryLine; 6], +} + +const M: [[f64; 3]; 3] = [ + [3.240969941904521, -1.537383177570093, -0.498610760293], + [-0.96924363628087, 1.87596750150772, 0.041555057407175], + [0.055630079696993, -0.20397695888897, 1.056971514242878], +]; +const KAPPA: f64 = 903.2962962; +const EPSILON: f64 = 0.0088564516; + +impl LuvBounds { + pub fn from_lightness(l: T) -> Self { + let l: f64 = l.to_f64().unwrap(); + + let sub1 = (l + 16.0).pow(3.0) / 1560896.0; + let sub2 = if sub1 > EPSILON { sub1 } else { l / KAPPA }; + + let line = |c: usize, t: f64| { + let m: &[f64; 3] = &M[c]; + let top1 = (284517.0 * m[0] - 94839.0 * m[2]) * sub2; + let top2 = (838422.0 * m[2] + 769860.0 * m[1] + 731718.0 * m[0]) * l * sub2 + - 769860.0 * t * l; + let bottom = (632260.0 * m[2] - 126452.0 * m[1]) * sub2 + 126452.0 * t; + + BoundaryLine { + slope: top1 / bottom, + intercept: top2 / bottom, + } + }; + + Self { bounds: [line(0, 0.0), line(0, 1.0), + line(1, 0.0), line(1, 1.0), + line(2, 0.0), line(2, 1.0)]} + } + + /// Given a particular hue, return the distance to the boundary at + /// the angle determined by the hue. + pub fn max_chroma_at_hue(&self, hue: LuvHue) -> T { + let mut min_chroma = f64::MAX; + let h = hue.to_positive_radians().to_f64().unwrap(); + + // minimize the distance across all individual boundaries + for b in &self.bounds { + if let Some(t) = b.intersect_length_at_angle(h) { + if t >= 0.0 && min_chroma > t { + min_chroma = t; + } + } + } + T::from_f64(min_chroma) + } + + /// Return the minimum chroma such that, at any hue, the chroma is + /// in-gamut. + /// + /// This is equivalent to finding the minimum distance to the + /// origin across all boundaries. + /// + /// # Remarks + /// This is useful for a n HPLuv implementation. + #[allow(unused)] + pub fn max_safe_chroma(&self) -> T + where + T: FloatComponent, + { + let mut min_dist = f64::MAX; + + // minimize the distance across all individual boundaries + for b in &self.bounds { + let d = b.distance_to_origin(); + if min_dist > d { + min_dist = d; + } + } + T::from_f64(min_dist) + } +} + +#[cfg(test)] +mod tests { + use super::BoundaryLine; + + #[test] + fn boundary_intersect() { + let line = BoundaryLine { + slope: -1.0, + intercept: 1.0, + }; + assert_relative_eq!(line.intersect_length_at_angle(0.0).unwrap(), 1.0); + assert_relative_eq!( + line.intersect_length_at_angle(std::f64::consts::FRAC_PI_4) + .unwrap(), + std::f64::consts::FRAC_1_SQRT_2 + ); + assert_eq!( + line.intersect_length_at_angle(-std::f64::consts::FRAC_PI_4), + None + ); + + let line = BoundaryLine { + slope: 0.0, + intercept: 2.0, + }; + assert_eq!(line.intersect_length_at_angle(0.0), None); + assert_relative_eq!( + line.intersect_length_at_angle(std::f64::consts::FRAC_PI_2) + .unwrap(), + 2.0 + ); + assert_relative_eq!( + line.intersect_length_at_angle(2.0 * std::f64::consts::FRAC_PI_3) + .unwrap(), + 4.0 / 3.0f64.sqrt() + ); + } + + #[test] + fn line_distance() { + let line = BoundaryLine { + slope: 0.0, + intercept: 2.0, + }; + assert_relative_eq!(line.distance_to_origin(), 2.0); + + let line = BoundaryLine { + slope: 1.0, + intercept: 2.0, + }; + assert_relative_eq!(line.distance_to_origin(), std::f64::consts::SQRT_2); + } +} diff --git a/palette/tests/hsluv_dataset/hsluv_dataset.rs b/palette/tests/hsluv_dataset/hsluv_dataset.rs index 0d352478d..65992b8b2 100644 --- a/palette/tests/hsluv_dataset/hsluv_dataset.rs +++ b/palette/tests/hsluv_dataset/hsluv_dataset.rs @@ -7,13 +7,14 @@ use serde_json; use palette::convert::IntoColorUnclamped; use palette::white_point::D65; -use palette::{Lchuv, Luv, LuvHue, Xyz}; +use palette::{Hsluv, Lchuv, Luv, LuvHue, Xyz}; use std::collections::HashMap; #[derive(Clone, Debug)] struct HsluvExample { name: String, lchuv: Lchuv, + hsluv: Hsluv, luv: Luv, xyz: Xyz, } @@ -26,33 +27,28 @@ fn load_data() -> Examples { let raw_data: serde_json::Value = serde_json::from_str(&data_str).unwrap(); let m = raw_data.as_object().expect("failed to parse dataset"); + let to_vec = |c: &serde_json::Value| { + c.as_array() + .unwrap() + .iter() + .flat_map(|x| x.as_f64()) + .collect() + }; + m.iter() .map(|(k, v)| { let colors = v.as_object().unwrap(); - let luv_data: Vec = colors["luv"] - .as_array() - .unwrap() - .iter() - .flat_map(|x| x.as_f64()) - .collect(); - let lchuv_data: Vec = colors["lch"] - .as_array() - .unwrap() - .iter() - .flat_map(|x| x.as_f64()) - .collect(); - let xyz_data: Vec = colors["xyz"] - .as_array() - .unwrap() - .iter() - .flat_map(|x| x.as_f64()) - .collect(); + let luv_data: Vec = to_vec(&colors["luv"]); + let lchuv_data: Vec = to_vec(&colors["lch"]); + let hsluv_data: Vec = to_vec(&colors["hsluv"]); + let xyz_data: Vec = to_vec(&colors["xyz"]); ( k.clone(), HsluvExample { name: k.clone(), luv: Luv::new(luv_data[0], luv_data[1], luv_data[2]), + hsluv: Hsluv::new(hsluv_data[0], hsluv_data[1], hsluv_data[2]), lchuv: Lchuv::new(lchuv_data[0], lchuv_data[1], lchuv_data[2]), xyz: Xyz::new(xyz_data[0], xyz_data[1], xyz_data[2]), }, @@ -85,7 +81,7 @@ pub fn run_luv_to_xyz_tests() { pub fn run_lchuv_to_luv_tests() { for (_, v) in TEST_DATA.iter() { let to_luv: Luv = v.lchuv.into_color_unclamped(); - assert_relative_eq!(to_luv, v.luv, epsilon = 0.1); + assert_relative_eq!(to_luv, v.luv, epsilon = 0.001); } } @@ -99,3 +95,22 @@ pub fn run_luv_to_lchuv_tests() { assert_relative_eq!(to_lchuv, v.lchuv, epsilon = 0.001); } } + +#[test] +pub fn run_lchuv_to_hsluv_tests() { + for (_, v) in TEST_DATA.iter() { + let mut to_hsluv: Hsluv = v.lchuv.into_color_unclamped(); + if to_hsluv.l > 100.0 - 1e-5 { + to_hsluv.saturation = 0.0; + } + assert_relative_eq!(to_hsluv, v.hsluv, epsilon = 1e-5); + } +} + +#[test] +pub fn run_hsluv_to_lchuv_tests() { + for (_, v) in TEST_DATA.iter() { + let to_lchuv: Lchuv = v.hsluv.into_color_unclamped(); + assert_relative_eq!(to_lchuv, v.lchuv, epsilon = 1e-5); + } +} diff --git a/palette_derive/src/lib.rs b/palette_derive/src/lib.rs index 9662e2664..c9aae059b 100644 --- a/palette_derive/src/lib.rs +++ b/palette_derive/src/lib.rs @@ -38,13 +38,14 @@ mod meta; mod util; const COLOR_TYPES: &[&str] = &[ - "Rgb", "Luma", "Hsl", "Hsv", "Hwb", "Lab", "Lch", "Lchuv", "Luv", "Xyz", "Yxy", + "Rgb", "Luma", "Hsl", "Hsluv", "Hsv", "Hwb", "Lab", "Lch", "Lchuv", "Luv", "Xyz", "Yxy", ]; const PREFERRED_CONVERSION_SOURCE: &[(&str, &str)] = &[ ("Rgb", "Xyz"), ("Luma", "Xyz"), ("Hsl", "Rgb"), + ("Hsluv", "Lchuv"), ("Hsv", "Rgb"), ("Hwb", "Hsv"), ("Lab", "Xyz"), From 00eaccd6d2a10ad42acdad81ec977fdf611e2af2 Mon Sep 17 00:00:00 2001 From: Mason Smith Date: Thu, 13 May 2021 00:05:05 -0700 Subject: [PATCH 2/5] use macros for Add/Sub implementation for Luv, Lchuv, Hsluv --- palette/src/arithmetic.rs | 117 ++++++++++++++++++++++++++++++++ palette/src/hsluv.rs | 136 ++++++-------------------------------- palette/src/lchuv.rs | 136 ++++++-------------------------------- palette/src/lib.rs | 3 + palette/src/luv.rs | 134 ++++++------------------------------- 5 files changed, 181 insertions(+), 345 deletions(-) create mode 100644 palette/src/arithmetic.rs diff --git a/palette/src/arithmetic.rs b/palette/src/arithmetic.rs new file mode 100644 index 000000000..8edbf8f97 --- /dev/null +++ b/palette/src/arithmetic.rs @@ -0,0 +1,117 @@ +//! Macros to implement arithmetic traits on Color spaces. + +/// Implement `Add` and `AddAssign` traits for a color space. +/// +/// Both scalars and color arithmetic are implemented. +#[macro_export] +macro_rules! impl_color_add { + ($self_ty: ident , [$($element: ident),+], $phantom: ident) => { + impl Add<$self_ty> for $self_ty + where + T: FloatComponent, + Wp: WhitePoint, + { + type Output = $self_ty; + + fn add(self, other: $self_ty) -> Self::Output { + $self_ty { + $( $element: self.$element + other.$element ),+, + $phantom: PhantomData, + } + } + } + impl Add for $self_ty + where + T: FloatComponent, + Wp: WhitePoint, + { + type Output = $self_ty; + + fn add(self, c: T) -> Self::Output { + $self_ty { + $( $element: self.$element + c ),+, + $phantom: PhantomData, + } + } + } + + impl AddAssign<$self_ty> for $self_ty + where + T: FloatComponent + AddAssign, + Wp: WhitePoint, + { + fn add_assign(&mut self, other: $self_ty) { + $( self.$element += other.$element );+ + } + } + + impl AddAssign for $self_ty + where + T: FloatComponent + AddAssign, + Wp: WhitePoint, + { + fn add_assign(&mut self, c: T) { + $( self.$element += c );+ + } + } + } +} + +/// Implement `Sub` and `SubAssign` traits for a color space. +/// +/// Both scalars and color arithmetic are implemented. +#[macro_export] +macro_rules! impl_color_sub { + ($self_ty: ident , [$($element: ident),+], $phantom: ident) => { + + impl Sub<$self_ty> for $self_ty + where + T: FloatComponent, + Wp: WhitePoint, + { + type Output = $self_ty; + + fn sub(self, other: $self_ty) -> Self::Output { + $self_ty { + $( $element: self.$element - other.$element ),+, + $phantom: PhantomData, + } + } + } + + impl Sub for $self_ty + where + T: FloatComponent, + Wp: WhitePoint, + { + type Output = $self_ty; + + fn sub(self, c: T) -> Self::Output { + $self_ty { + $( $element: self.$element - c ),+, + $phantom: PhantomData, + } + } + } + + impl SubAssign<$self_ty> for $self_ty + where + T: FloatComponent + SubAssign, + Wp: WhitePoint, + { + fn sub_assign(&mut self, other: $self_ty) { + $( self.$element -= other.$element; )+ + } + } + + impl SubAssign for $self_ty + where + T: FloatComponent + SubAssign, + Wp: WhitePoint, + { + fn sub_assign(&mut self, c: T) { + $( self.$element -= c; )+ + } + } + } +} diff --git a/palette/src/hsluv.rs b/palette/src/hsluv.rs index fe757e32e..90c5e22a4 100644 --- a/palette/src/hsluv.rs +++ b/palette/src/hsluv.rs @@ -11,6 +11,8 @@ use crate::{ RelativeContrast, Saturate, Shade, Xyz, }; +use crate::{impl_color_add, impl_color_sub}; + /// HSLuv with an alpha component. See the [`Hsluva` implementation in /// `Alpha`](crate::Alpha#Hsluva). pub type Hsluva = Alpha, T>; @@ -424,121 +426,8 @@ where } } -impl Add> for Hsluv -where - T: FloatComponent, - Wp: WhitePoint, -{ - type Output = Hsluv; - - fn add(self, other: Hsluv) -> Self::Output { - Hsluv { - hue: self.hue + other.hue, - saturation: self.saturation + other.saturation, - l: self.l + other.l, - standard: PhantomData, - } - } -} - -impl Add for Hsluv -where - T: FloatComponent, - Wp: WhitePoint, -{ - type Output = Hsluv; - - fn add(self, c: T) -> Self::Output { - Hsluv { - hue: self.hue + c, - saturation: self.saturation + c, - l: self.l + c, - standard: PhantomData, - } - } -} - -impl AddAssign> for Hsluv -where - T: FloatComponent + AddAssign, - Wp: WhitePoint, -{ - fn add_assign(&mut self, other: Hsluv) { - self.hue += other.hue; - self.saturation += other.saturation; - self.l += other.l; - } -} - -impl AddAssign for Hsluv -where - T: FloatComponent + AddAssign, - Wp: WhitePoint, -{ - fn add_assign(&mut self, c: T) { - self.hue += c; - self.saturation += c; - self.l += c; - } -} - -impl Sub> for Hsluv -where - T: FloatComponent, - Wp: WhitePoint, -{ - type Output = Hsluv; - - fn sub(self, other: Hsluv) -> Self::Output { - Hsluv { - hue: self.hue - other.hue, - saturation: self.saturation - other.saturation, - l: self.l - other.l, - standard: PhantomData, - } - } -} - -impl Sub for Hsluv -where - T: FloatComponent, - Wp: WhitePoint, -{ - type Output = Hsluv; - - fn sub(self, c: T) -> Self::Output { - Hsluv { - hue: self.hue - c, - saturation: self.saturation - c, - l: self.l - c, - standard: PhantomData, - } - } -} - -impl SubAssign> for Hsluv -where - T: FloatComponent + SubAssign, - Wp: WhitePoint, -{ - fn sub_assign(&mut self, other: Hsluv) { - self.hue -= other.hue; - self.saturation -= other.saturation; - self.l -= other.l; - } -} - -impl SubAssign for Hsluv -where - T: FloatComponent + SubAssign, - Wp: WhitePoint, -{ - fn sub_assign(&mut self, c: T) { - self.hue -= c; - self.saturation -= c; - self.l -= c; - } -} +impl_color_add!(Hsluv, [hue, saturation, l], white_point); +impl_color_sub!(Hsluv, [hue, saturation, l], white_point); impl AsRef

for Hsluv where @@ -616,6 +505,23 @@ mod test { } } + /// Check that the arithmetic operations (add/sub) are all + /// implemented. + #[test] + fn test_arithmetic() { + let hsl = Hsluv::new(120.0, 40.0, 30.0); + let hsl2 = Hsluv::new(200.0, 30.0, 40.0); + let mut _hsl3 = hsl + hsl2; + _hsl3 += hsl2; + let mut _hsl4 = hsl2 + 0.3; + _hsl4 += 0.1; + + _hsl3 = hsl2 - hsl; + _hsl3 = _hsl4 - 0.1; + _hsl4 -= _hsl3; + _hsl3 -= 0.1; + } + #[test] fn saturate() { for sat in (0..=10).map(|s| s as f64 * 10.0) { diff --git a/palette/src/lchuv.rs b/palette/src/lchuv.rs index 190925e3c..81a7e19cc 100644 --- a/palette/src/lchuv.rs +++ b/palette/src/lchuv.rs @@ -8,6 +8,8 @@ use rand::distributions::{Distribution, Standard}; #[cfg(feature = "random")] use rand::Rng; +use crate::{impl_color_add, impl_color_sub}; + use crate::convert::FromColorUnclamped; use crate::encoding::pixel::RawPixel; use crate::luv_bounds::LuvBounds; @@ -432,121 +434,8 @@ where } } -impl Add> for Lchuv -where - T: FloatComponent, - Wp: WhitePoint, -{ - type Output = Lchuv; - - fn add(self, other: Lchuv) -> Self::Output { - Lchuv { - l: self.l + other.l, - chroma: self.chroma + other.chroma, - hue: self.hue + other.hue, - white_point: PhantomData, - } - } -} - -impl Add for Lchuv -where - T: FloatComponent, - Wp: WhitePoint, -{ - type Output = Lchuv; - - fn add(self, c: T) -> Self::Output { - Lchuv { - l: self.l + c, - chroma: self.chroma + c, - hue: self.hue + c, - white_point: PhantomData, - } - } -} - -impl AddAssign> for Lchuv -where - T: FloatComponent + AddAssign, - Wp: WhitePoint, -{ - fn add_assign(&mut self, other: Lchuv) { - self.l += other.l; - self.chroma += other.chroma; - self.hue += other.hue; - } -} - -impl AddAssign for Lchuv -where - T: FloatComponent + AddAssign, - Wp: WhitePoint, -{ - fn add_assign(&mut self, c: T) { - self.l += c; - self.chroma += c; - self.hue += c; - } -} - -impl Sub> for Lchuv -where - T: FloatComponent, - Wp: WhitePoint, -{ - type Output = Lchuv; - - fn sub(self, other: Lchuv) -> Self::Output { - Lchuv { - l: self.l - other.l, - chroma: self.chroma - other.chroma, - hue: self.hue - other.hue, - white_point: PhantomData, - } - } -} - -impl Sub for Lchuv -where - T: FloatComponent, - Wp: WhitePoint, -{ - type Output = Lchuv; - - fn sub(self, c: T) -> Self::Output { - Lchuv { - l: self.l - c, - chroma: self.chroma - c, - hue: self.hue - c, - white_point: PhantomData, - } - } -} - -impl SubAssign> for Lchuv -where - T: FloatComponent + SubAssign, - Wp: WhitePoint, -{ - fn sub_assign(&mut self, other: Lchuv) { - self.l -= other.l; - self.chroma -= other.chroma; - self.hue -= other.hue; - } -} - -impl SubAssign for Lchuv -where - T: FloatComponent + SubAssign, - Wp: WhitePoint, -{ - fn sub_assign(&mut self, c: T) { - self.l -= c; - self.chroma -= c; - self.hue -= c; - } -} +impl_color_add!(Lchuv, [l, chroma, hue], white_point); +impl_color_sub!(Lchuv, [l, chroma, hue], white_point); impl AsRef

for Lchuv where @@ -697,6 +586,23 @@ mod test { } } + /// Check that the arithmetic operations (add/sub) are all + /// implemented. + #[test] + fn test_arithmetic() { + let lchuv = Lchuv::new(120.0, 40.0, 30.0); + let lchuv2 = Lchuv::new(200.0, 30.0, 40.0); + let mut _lchuv3 = lchuv + lchuv2; + _lchuv3 += lchuv2; + let mut _lchuv4 = lchuv2 + 0.3; + _lchuv4 += 0.1; + + _lchuv3 = lchuv2 - lchuv; + _lchuv3 = _lchuv4 - 0.1; + _lchuv4 -= _lchuv3; + _lchuv3 -= 0.1; + } + raw_pixel_conversion_tests!(Lchuv: l, chroma, hue); raw_pixel_conversion_fail_tests!(Lchuv: l, chroma, hue); diff --git a/palette/src/lib.rs b/palette/src/lib.rs index 67d38d9dd..3524ec2b3 100644 --- a/palette/src/lib.rs +++ b/palette/src/lib.rs @@ -438,6 +438,9 @@ mod yxy; mod hues; +#[macro_use] +mod arithmetic; + pub mod chromatic_adaptation; mod color_difference; mod component; diff --git a/palette/src/luv.rs b/palette/src/luv.rs index 3c1a127f4..db09a76e3 100644 --- a/palette/src/luv.rs +++ b/palette/src/luv.rs @@ -15,6 +15,7 @@ use crate::{ clamp, contrast_ratio, from_f64, Alpha, Clamp, Component, ComponentWise, FloatComponent, GetHue, Lchuv, LuvHue, Mix, Pixel, RelativeContrast, Shade, Xyz, }; +use crate::{impl_color_add, impl_color_sub}; /// CIE L\*u\*v\* (CIELUV) with an alpha component. See the [`Luva` /// implementation in `Alpha`](crate::Alpha#Luva). @@ -428,121 +429,8 @@ where } } -impl Add> for Luv -where - T: FloatComponent, - Wp: WhitePoint, -{ - type Output = Luv; - - fn add(self, other: Luv) -> Self::Output { - Luv { - l: self.l + other.l, - u: self.u + other.u, - v: self.v + other.v, - white_point: PhantomData, - } - } -} - -impl Add for Luv -where - T: FloatComponent, - Wp: WhitePoint, -{ - type Output = Luv; - - fn add(self, c: T) -> Self::Output { - Luv { - l: self.l + c, - u: self.u + c, - v: self.v + c, - white_point: PhantomData, - } - } -} - -impl AddAssign> for Luv -where - T: FloatComponent + AddAssign, - Wp: WhitePoint, -{ - fn add_assign(&mut self, other: Luv) { - self.l += other.l; - self.u += other.u; - self.v += other.v; - } -} - -impl AddAssign for Luv -where - T: FloatComponent + AddAssign, - Wp: WhitePoint, -{ - fn add_assign(&mut self, c: T) { - self.l += c; - self.u += c; - self.v += c; - } -} - -impl Sub> for Luv -where - T: FloatComponent, - Wp: WhitePoint, -{ - type Output = Luv; - - fn sub(self, other: Luv) -> Self::Output { - Luv { - l: self.l - other.l, - u: self.u - other.u, - v: self.v - other.v, - white_point: PhantomData, - } - } -} - -impl Sub for Luv -where - T: FloatComponent, - Wp: WhitePoint, -{ - type Output = Luv; - - fn sub(self, c: T) -> Self::Output { - Luv { - l: self.l - c, - u: self.u - c, - v: self.v - c, - white_point: PhantomData, - } - } -} - -impl SubAssign> for Luv -where - T: FloatComponent + SubAssign, - Wp: WhitePoint, -{ - fn sub_assign(&mut self, other: Luv) { - self.l -= other.l; - self.u -= other.u; - self.v -= other.v; - } -} - -impl SubAssign for Luv -where - T: FloatComponent + SubAssign, - Wp: WhitePoint, -{ - fn sub_assign(&mut self, c: T) { - self.l -= c; - self.u -= c; - self.v -= c; - } -} +impl_color_add!(Luv, [l, u, v], white_point); +impl_color_sub!(Luv, [l, u, v], white_point); impl Mul> for Luv where @@ -827,6 +715,22 @@ mod test { unclamped {} } } + /// Check that the arithmetic operations (add/sub) are all + /// implemented. + #[test] + fn test_arithmetic() { + let luv = Luv::new(120.0, 40.0, 30.0); + let luv2 = Luv::new(200.0, 30.0, 40.0); + let mut _luv3 = luv + luv2; + _luv3 += luv2; + let mut _luv4 = luv2 + 0.3; + _luv4 += 0.1; + + _luv3 = luv2 - luv; + _luv3 = _luv4 - 0.1; + _luv4 -= _luv3; + _luv3 -= 0.1; + } raw_pixel_conversion_tests!(Luv: l, u, v); raw_pixel_conversion_fail_tests!(Luv: l, u, v); From 408edac685c2d76c5396dd305305bf0842b5e0d5 Mon Sep 17 00:00:00 2001 From: Mason Smith Date: Thu, 13 May 2021 22:25:46 -0700 Subject: [PATCH 3/5] cargo fmt --- palette/src/hsluv.rs | 25 +++++++++++-------------- palette/src/lchuv.rs | 22 +++++++++++----------- palette/src/luv.rs | 22 +++++++++++----------- palette/src/luv_bounds.rs | 21 ++++++++++++++------- 4 files changed, 47 insertions(+), 43 deletions(-) diff --git a/palette/src/hsluv.rs b/palette/src/hsluv.rs index 90c5e22a4..93eea4a83 100644 --- a/palette/src/hsluv.rs +++ b/palette/src/hsluv.rs @@ -509,17 +509,17 @@ mod test { /// implemented. #[test] fn test_arithmetic() { - let hsl = Hsluv::new(120.0, 40.0, 30.0); - let hsl2 = Hsluv::new(200.0, 30.0, 40.0); - let mut _hsl3 = hsl + hsl2; - _hsl3 += hsl2; - let mut _hsl4 = hsl2 + 0.3; - _hsl4 += 0.1; + let hsl = Hsluv::new(120.0, 40.0, 30.0); + let hsl2 = Hsluv::new(200.0, 30.0, 40.0); + let mut _hsl3 = hsl + hsl2; + _hsl3 += hsl2; + let mut _hsl4 = hsl2 + 0.3; + _hsl4 += 0.1; - _hsl3 = hsl2 - hsl; - _hsl3 = _hsl4 - 0.1; - _hsl4 -= _hsl3; - _hsl3 -= 0.1; + _hsl3 = hsl2 - hsl; + _hsl3 = _hsl4 - 0.1; + _hsl4 -= _hsl3; + _hsl3 -= 0.1; } #[test] @@ -554,10 +554,7 @@ mod test { fn serialize() { let serialized = ::serde_json::to_string(&Hsluv::new(120.0, 80.0, 60.0)).unwrap(); - assert_eq!( - serialized, - r#"{"hue":120.0,"saturation":80.0,"l":60.0}"# - ); + assert_eq!(serialized, r#"{"hue":120.0,"saturation":80.0,"l":60.0}"#); } #[cfg(feature = "serializing")] diff --git a/palette/src/lchuv.rs b/palette/src/lchuv.rs index 81a7e19cc..180f2c15a 100644 --- a/palette/src/lchuv.rs +++ b/palette/src/lchuv.rs @@ -590,17 +590,17 @@ mod test { /// implemented. #[test] fn test_arithmetic() { - let lchuv = Lchuv::new(120.0, 40.0, 30.0); - let lchuv2 = Lchuv::new(200.0, 30.0, 40.0); - let mut _lchuv3 = lchuv + lchuv2; - _lchuv3 += lchuv2; - let mut _lchuv4 = lchuv2 + 0.3; - _lchuv4 += 0.1; - - _lchuv3 = lchuv2 - lchuv; - _lchuv3 = _lchuv4 - 0.1; - _lchuv4 -= _lchuv3; - _lchuv3 -= 0.1; + let lchuv = Lchuv::new(120.0, 40.0, 30.0); + let lchuv2 = Lchuv::new(200.0, 30.0, 40.0); + let mut _lchuv3 = lchuv + lchuv2; + _lchuv3 += lchuv2; + let mut _lchuv4 = lchuv2 + 0.3; + _lchuv4 += 0.1; + + _lchuv3 = lchuv2 - lchuv; + _lchuv3 = _lchuv4 - 0.1; + _lchuv4 -= _lchuv3; + _lchuv3 -= 0.1; } raw_pixel_conversion_tests!(Lchuv: l, chroma, hue); diff --git a/palette/src/luv.rs b/palette/src/luv.rs index db09a76e3..3d6815d51 100644 --- a/palette/src/luv.rs +++ b/palette/src/luv.rs @@ -719,17 +719,17 @@ mod test { /// implemented. #[test] fn test_arithmetic() { - let luv = Luv::new(120.0, 40.0, 30.0); - let luv2 = Luv::new(200.0, 30.0, 40.0); - let mut _luv3 = luv + luv2; - _luv3 += luv2; - let mut _luv4 = luv2 + 0.3; - _luv4 += 0.1; - - _luv3 = luv2 - luv; - _luv3 = _luv4 - 0.1; - _luv4 -= _luv3; - _luv3 -= 0.1; + let luv = Luv::new(120.0, 40.0, 30.0); + let luv2 = Luv::new(200.0, 30.0, 40.0); + let mut _luv3 = luv + luv2; + _luv3 += luv2; + let mut _luv4 = luv2 + 0.3; + _luv4 += 0.1; + + _luv3 = luv2 - luv; + _luv3 = _luv4 - 0.1; + _luv4 -= _luv3; + _luv3 -= 0.1; } raw_pixel_conversion_tests!(Luv: l, u, v); diff --git a/palette/src/luv_bounds.rs b/palette/src/luv_bounds.rs index 931c4c704..159dd4229 100644 --- a/palette/src/luv_bounds.rs +++ b/palette/src/luv_bounds.rs @@ -52,22 +52,29 @@ impl LuvBounds { let sub1 = (l + 16.0).pow(3.0) / 1560896.0; let sub2 = if sub1 > EPSILON { sub1 } else { l / KAPPA }; - let line = |c: usize, t: f64| { + let line = |c: usize, t: f64| { let m: &[f64; 3] = &M[c]; let top1 = (284517.0 * m[0] - 94839.0 * m[2]) * sub2; - let top2 = (838422.0 * m[2] + 769860.0 * m[1] + 731718.0 * m[0]) * l * sub2 - - 769860.0 * t * l; + let top2 = + (838422.0 * m[2] + 769860.0 * m[1] + 731718.0 * m[0]) * l * sub2 - 769860.0 * t * l; let bottom = (632260.0 * m[2] - 126452.0 * m[1]) * sub2 + 126452.0 * t; BoundaryLine { slope: top1 / bottom, intercept: top2 / bottom, } - }; + }; - Self { bounds: [line(0, 0.0), line(0, 1.0), - line(1, 0.0), line(1, 1.0), - line(2, 0.0), line(2, 1.0)]} + Self { + bounds: [ + line(0, 0.0), + line(0, 1.0), + line(1, 0.0), + line(1, 1.0), + line(2, 0.0), + line(2, 1.0), + ], + } } /// Given a particular hue, return the distance to the boundary at From 088a31f591e6b0d281163b93262401c317359da1 Mon Sep 17 00:00:00 2001 From: Mason Smith Date: Thu, 20 May 2021 22:44:09 -0700 Subject: [PATCH 4/5] move add/sub macros to macros module --- palette/src/arithmetic.rs | 117 -------------------------------------- palette/src/hsluv.rs | 2 - palette/src/lchuv.rs | 2 - palette/src/lib.rs | 3 - palette/src/luv.rs | 2 +- palette/src/macros.rs | 111 ++++++++++++++++++++++++++++++++++++ 6 files changed, 112 insertions(+), 125 deletions(-) delete mode 100644 palette/src/arithmetic.rs diff --git a/palette/src/arithmetic.rs b/palette/src/arithmetic.rs deleted file mode 100644 index 8edbf8f97..000000000 --- a/palette/src/arithmetic.rs +++ /dev/null @@ -1,117 +0,0 @@ -//! Macros to implement arithmetic traits on Color spaces. - -/// Implement `Add` and `AddAssign` traits for a color space. -/// -/// Both scalars and color arithmetic are implemented. -#[macro_export] -macro_rules! impl_color_add { - ($self_ty: ident , [$($element: ident),+], $phantom: ident) => { - impl Add<$self_ty> for $self_ty - where - T: FloatComponent, - Wp: WhitePoint, - { - type Output = $self_ty; - - fn add(self, other: $self_ty) -> Self::Output { - $self_ty { - $( $element: self.$element + other.$element ),+, - $phantom: PhantomData, - } - } - } - impl Add for $self_ty - where - T: FloatComponent, - Wp: WhitePoint, - { - type Output = $self_ty; - - fn add(self, c: T) -> Self::Output { - $self_ty { - $( $element: self.$element + c ),+, - $phantom: PhantomData, - } - } - } - - impl AddAssign<$self_ty> for $self_ty - where - T: FloatComponent + AddAssign, - Wp: WhitePoint, - { - fn add_assign(&mut self, other: $self_ty) { - $( self.$element += other.$element );+ - } - } - - impl AddAssign for $self_ty - where - T: FloatComponent + AddAssign, - Wp: WhitePoint, - { - fn add_assign(&mut self, c: T) { - $( self.$element += c );+ - } - } - } -} - -/// Implement `Sub` and `SubAssign` traits for a color space. -/// -/// Both scalars and color arithmetic are implemented. -#[macro_export] -macro_rules! impl_color_sub { - ($self_ty: ident , [$($element: ident),+], $phantom: ident) => { - - impl Sub<$self_ty> for $self_ty - where - T: FloatComponent, - Wp: WhitePoint, - { - type Output = $self_ty; - - fn sub(self, other: $self_ty) -> Self::Output { - $self_ty { - $( $element: self.$element - other.$element ),+, - $phantom: PhantomData, - } - } - } - - impl Sub for $self_ty - where - T: FloatComponent, - Wp: WhitePoint, - { - type Output = $self_ty; - - fn sub(self, c: T) -> Self::Output { - $self_ty { - $( $element: self.$element - c ),+, - $phantom: PhantomData, - } - } - } - - impl SubAssign<$self_ty> for $self_ty - where - T: FloatComponent + SubAssign, - Wp: WhitePoint, - { - fn sub_assign(&mut self, other: $self_ty) { - $( self.$element -= other.$element; )+ - } - } - - impl SubAssign for $self_ty - where - T: FloatComponent + SubAssign, - Wp: WhitePoint, - { - fn sub_assign(&mut self, c: T) { - $( self.$element -= c; )+ - } - } - } -} diff --git a/palette/src/hsluv.rs b/palette/src/hsluv.rs index 93eea4a83..a429a3bb3 100644 --- a/palette/src/hsluv.rs +++ b/palette/src/hsluv.rs @@ -11,8 +11,6 @@ use crate::{ RelativeContrast, Saturate, Shade, Xyz, }; -use crate::{impl_color_add, impl_color_sub}; - /// HSLuv with an alpha component. See the [`Hsluva` implementation in /// `Alpha`](crate::Alpha#Hsluva). pub type Hsluva = Alpha, T>; diff --git a/palette/src/lchuv.rs b/palette/src/lchuv.rs index 180f2c15a..774733a30 100644 --- a/palette/src/lchuv.rs +++ b/palette/src/lchuv.rs @@ -8,8 +8,6 @@ use rand::distributions::{Distribution, Standard}; #[cfg(feature = "random")] use rand::Rng; -use crate::{impl_color_add, impl_color_sub}; - use crate::convert::FromColorUnclamped; use crate::encoding::pixel::RawPixel; use crate::luv_bounds::LuvBounds; diff --git a/palette/src/lib.rs b/palette/src/lib.rs index 3524ec2b3..67d38d9dd 100644 --- a/palette/src/lib.rs +++ b/palette/src/lib.rs @@ -438,9 +438,6 @@ mod yxy; mod hues; -#[macro_use] -mod arithmetic; - pub mod chromatic_adaptation; mod color_difference; mod component; diff --git a/palette/src/luv.rs b/palette/src/luv.rs index 3d6815d51..988007798 100644 --- a/palette/src/luv.rs +++ b/palette/src/luv.rs @@ -15,7 +15,7 @@ use crate::{ clamp, contrast_ratio, from_f64, Alpha, Clamp, Component, ComponentWise, FloatComponent, GetHue, Lchuv, LuvHue, Mix, Pixel, RelativeContrast, Shade, Xyz, }; -use crate::{impl_color_add, impl_color_sub}; + /// CIE L\*u\*v\* (CIELUV) with an alpha component. See the [`Luva` /// implementation in `Alpha`](crate::Alpha#Luva). diff --git a/palette/src/macros.rs b/palette/src/macros.rs index a97b877da..0b6ec5fa1 100644 --- a/palette/src/macros.rs +++ b/palette/src/macros.rs @@ -256,3 +256,114 @@ macro_rules! test_uniform_distribution { } }; } + +macro_rules! impl_color_add { + ($self_ty: ident , [$($element: ident),+], $phantom: ident) => { + impl Add<$self_ty> for $self_ty + where + T: FloatComponent, + Wp: WhitePoint, + { + type Output = $self_ty; + + fn add(self, other: $self_ty) -> Self::Output { + $self_ty { + $( $element: self.$element + other.$element ),+, + $phantom: PhantomData, + } + } + } + impl Add for $self_ty + where + T: FloatComponent, + Wp: WhitePoint, + { + type Output = $self_ty; + + fn add(self, c: T) -> Self::Output { + $self_ty { + $( $element: self.$element + c ),+, + $phantom: PhantomData, + } + } + } + + impl AddAssign<$self_ty> for $self_ty + where + T: FloatComponent + AddAssign, + Wp: WhitePoint, + { + fn add_assign(&mut self, other: $self_ty) { + $( self.$element += other.$element );+ + } + } + + impl AddAssign for $self_ty + where + T: FloatComponent + AddAssign, + Wp: WhitePoint, + { + fn add_assign(&mut self, c: T) { + $( self.$element += c );+ + } + } + } +} + +/// Implement `Sub` and `SubAssign` traits for a color space. +/// +/// Both scalars and color arithmetic are implemented. +macro_rules! impl_color_sub { + ($self_ty: ident , [$($element: ident),+], $phantom: ident) => { + + impl Sub<$self_ty> for $self_ty + where + T: FloatComponent, + Wp: WhitePoint, + { + type Output = $self_ty; + + fn sub(self, other: $self_ty) -> Self::Output { + $self_ty { + $( $element: self.$element - other.$element ),+, + $phantom: PhantomData, + } + } + } + + impl Sub for $self_ty + where + T: FloatComponent, + Wp: WhitePoint, + { + type Output = $self_ty; + + fn sub(self, c: T) -> Self::Output { + $self_ty { + $( $element: self.$element - c ),+, + $phantom: PhantomData, + } + } + } + + impl SubAssign<$self_ty> for $self_ty + where + T: FloatComponent + SubAssign, + Wp: WhitePoint, + { + fn sub_assign(&mut self, other: $self_ty) { + $( self.$element -= other.$element; )+ + } + } + + impl SubAssign for $self_ty + where + T: FloatComponent + SubAssign, + Wp: WhitePoint, + { + fn sub_assign(&mut self, c: T) { + $( self.$element -= c; )+ + } + } + } +} From 11954d1ed22233f099da983103ef18a40a11fd08 Mon Sep 17 00:00:00 2001 From: Mason Smith Date: Thu, 20 May 2021 22:44:39 -0700 Subject: [PATCH 5/5] hsluv uniform sampling --- palette/src/hsluv.rs | 87 ++++++++++++++++++++++++++ palette/src/luv.rs | 1 - palette/src/random_sampling/cone.rs | 97 +++++++++++++++++++++++++++-- 3 files changed, 179 insertions(+), 6 deletions(-) diff --git a/palette/src/hsluv.rs b/palette/src/hsluv.rs index a429a3bb3..eaad058b1 100644 --- a/palette/src/hsluv.rs +++ b/palette/src/hsluv.rs @@ -1,6 +1,13 @@ use core::marker::PhantomData; use core::ops::{Add, AddAssign, Sub, SubAssign}; +#[cfg(feature = "random")] +use rand::distributions::uniform::{SampleBorrow, SampleUniform, Uniform, UniformSampler}; +#[cfg(feature = "random")] +use rand::distributions::Distribution; +#[cfg(feature = "random")] +use rand::Rng; + use crate::encoding::pixel::RawPixel; use crate::luv_bounds::LuvBounds; use crate::{ @@ -466,6 +473,86 @@ where } } +#[cfg(feature = "random")] +pub struct UniformHsluv +where + T: FloatComponent + SampleUniform, + Wp: WhitePoint, +{ + hue: crate::hues::UniformLuvHue, + u1: Uniform, + u2: Uniform, + space: PhantomData, +} + +#[cfg(feature = "random")] +impl SampleUniform for Hsluv +where + T: FloatComponent + SampleUniform, + Wp: WhitePoint, +{ + type Sampler = UniformHsluv; +} + +#[cfg(feature = "random")] +impl UniformSampler for UniformHsluv +where + T: FloatComponent + SampleUniform, + Wp: WhitePoint, +{ + type X = Hsluv; + + fn new(low_b: B1, high_b: B2) -> Self + where + B1: SampleBorrow + Sized, + B2: SampleBorrow + Sized, + { + use crate::random_sampling::invert_hsluv_sample; + + let low = *low_b.borrow(); + let high = *high_b.borrow(); + + let (r1_min, r2_min): (T, T) = invert_hsluv_sample(low); + let (r1_max, r2_max): (T, T) = invert_hsluv_sample(high); + + UniformHsluv { + hue: crate::hues::UniformLuvHue::new(low.hue, high.hue), + u1: Uniform::new::<_, T>(r1_min, r1_max), + u2: Uniform::new::<_, T>(r2_min, r2_max), + space: PhantomData, + } + } + + fn new_inclusive(low_b: B1, high_b: B2) -> Self + where + B1: SampleBorrow + Sized, + B2: SampleBorrow + Sized, + { + use crate::random_sampling::invert_hsluv_sample; + + let low = *low_b.borrow(); + let high = *high_b.borrow(); + + let (r1_min, r2_min) = invert_hsluv_sample(low); + let (r1_max, r2_max) = invert_hsluv_sample(high); + + UniformHsluv { + hue: crate::hues::UniformLuvHue::new_inclusive(low.hue, high.hue), + u1: Uniform::new_inclusive::<_, T>(r1_min, r1_max), + u2: Uniform::new_inclusive::<_, T>(r2_min, r2_max), + space: PhantomData, + } + } + + fn sample(&self, rng: &mut R) -> Hsluv { + crate::random_sampling::sample_hsluv( + self.hue.sample(rng), + self.u1.sample(rng), + self.u2.sample(rng), + ) + } +} + #[cfg(test)] mod test { use super::Hsluv; diff --git a/palette/src/luv.rs b/palette/src/luv.rs index 988007798..9fb3b4682 100644 --- a/palette/src/luv.rs +++ b/palette/src/luv.rs @@ -16,7 +16,6 @@ use crate::{ GetHue, Lchuv, LuvHue, Mix, Pixel, RelativeContrast, Shade, Xyz, }; - /// CIE L\*u\*v\* (CIELUV) with an alpha component. See the [`Luva` /// implementation in `Alpha`](crate::Alpha#Luva). pub type Luva = Alpha, T>; diff --git a/palette/src/random_sampling/cone.rs b/palette/src/random_sampling/cone.rs index 6e7b427d8..4e1f0fcfc 100644 --- a/palette/src/random_sampling/cone.rs +++ b/palette/src/random_sampling/cone.rs @@ -1,9 +1,10 @@ use core::marker::PhantomData; use crate::float::Float; -use crate::hues::RgbHue; +use crate::hues::{LuvHue, RgbHue}; use crate::rgb::RgbStandard; -use crate::{from_f64, FloatComponent, Hsl, Hsv}; +use crate::white_point::WhitePoint; +use crate::{from_f64, FloatComponent, Hsl, Hsluv, Hsv}; // Based on https://stackoverflow.com/q/4778147 and https://math.stackexchange.com/q/18686, // picking A = (0, 0), B = (0, 1), C = (1, 1) gives us: @@ -60,6 +61,35 @@ where } } +pub fn sample_hsluv(hue: LuvHue, r1: T, r2: T) -> Hsluv +where + T: FloatComponent, + Wp: WhitePoint, +{ + let (saturation, l) = if r1 <= from_f64::(0.5) { + // Scale it up to [0, 1] + let r1 = r1 * from_f64::(2.0); + let h = Float::cbrt(r1); + let r = Float::sqrt(r2) * from_f64::(100.0); + // Scale the lightness back to [0, 0.5] + (r, h * from_f64::(50.0)) + } else { + // Scale and shift it to [0, 1). + let r1 = (from_f64::(1.0) - r1) * from_f64::(2.0); + let h = Float::cbrt(r1); + let r = Float::sqrt(r2) * from_f64::(100.0); + // Turn the cone upside-down and scale the lightness back to (0.5, 1.0] + (r, (from_f64::(2.0) - h) * from_f64::(50.0)) + }; + + Hsluv { + hue, + saturation, + l, + white_point: PhantomData, + } +} + pub fn invert_hsl_sample(color: Hsl) -> (T, T) where T: FloatComponent, @@ -82,12 +112,36 @@ where (r1, r2) } +pub fn invert_hsluv_sample(color: Hsluv) -> (T, T) +where + T: FloatComponent, + Wp: WhitePoint, +{ + let lightness: T = color.l / from_f64::(100.0); + let r1 = if lightness <= from_f64::(0.5) { + // ((x * 2)^3) / 2 = x^3 * 4. + // l is multiplied by 2 to scale it up to [0, 1], becoming h. + // h is cubed to make it r1. r1 is divided by 2 to take it back to [0, 0.5]. + lightness * lightness * lightness * from_f64::(4.0) + } else { + let x = lightness - from_f64::(1.0); + x * x * x * from_f64::(4.0) + from_f64::(1.0) + }; + + // saturation is first multiplied, then divided by h before squaring. + // h can be completely eliminated, leaving only the saturation. + let r2 = (color.saturation / from_f64::(100.0)).powi(2); + + (r1, r2) +} + #[cfg(test)] mod test { - use super::{invert_hsl_sample, sample_hsl, sample_hsv}; + use super::{invert_hsl_sample, invert_hsluv_sample, sample_hsl, sample_hsluv, sample_hsv}; use crate::encoding::Srgb; - use crate::hues::RgbHue; - use crate::{Hsl, Hsv}; + use crate::hues::{LuvHue, RgbHue}; + use crate::white_point::D65; + use crate::{Hsl, Hsluv, Hsv}; #[cfg(feature = "random")] #[test] @@ -100,6 +154,10 @@ mod test { let b = sample_hsl(RgbHue::from(360.0), 1.0, 1.0); assert_relative_eq!(Hsl::new(0.0, 0.0, 0.0), a); assert_relative_eq!(Hsl::new(360.0, 1.0, 1.0), b); + let a = sample_hsluv(LuvHue::from(0.0), 0.0, 0.0); + let b = sample_hsluv(LuvHue::from(360.0), 1.0, 1.0); + assert_relative_eq!(Hsluv::new(0.0, 0.0, 0.0), a); + assert_relative_eq!(Hsluv::new(360.0, 100.0, 100.0), b); } #[cfg(feature = "random")] @@ -130,4 +188,33 @@ mod test { test_hsl!(0.1666129293, 0.4396910574); test_hsl!(0.6190216210, 0.7175675180); } + + #[cfg(feature = "random")] + #[test] + fn hsluv_sampling() { + // Sanity check that sampling and inverting from sample are equivalent + macro_rules! test_hsluv { + ( $x:expr, $y:expr ) => {{ + let a = invert_hsluv_sample::(sample_hsluv(LuvHue::from(0.0), $x, $y)); + assert_relative_eq!(a.0, $x); + assert_relative_eq!(a.1, $y); + }}; + } + + test_hsluv!(0.8464721407, 0.8271899200); + test_hsluv!(0.8797234442, 0.4924621591); + test_hsluv!(0.9179406120, 0.8771350605); + test_hsluv!(0.5458023108, 0.1154283005); + test_hsluv!(0.2691241774, 0.7881780600); + test_hsluv!(0.2085030453, 0.9975406626); + test_hsluv!(0.8483632811, 0.4955013942); + test_hsluv!(0.0857919040, 0.0652214785); + test_hsluv!(0.7152662838, 0.2788421565); + test_hsluv!(0.2973598808, 0.5585230243); + test_hsluv!(0.0936619602, 0.7289450731); + test_hsluv!(0.4364395449, 0.9362269009); + test_hsluv!(0.9802381158, 0.9742974964); + test_hsluv!(0.1666129293, 0.4396910574); + test_hsluv!(0.6190216210, 0.7175675180); + } }