From f522cd69480ea7b16e99074445c254de1270b3d6 Mon Sep 17 00:00:00 2001 From: Mason Smith Date: Sat, 8 May 2021 23:42:04 -0700 Subject: [PATCH 1/3] implement CIELCHuv color space as Lchuv --- palette/src/equality.rs | 6 +- palette/src/hues.rs | 204 ++---- palette/src/lchuv.rs | 717 +++++++++++++++++++ palette/src/lib.rs | 2 + palette/src/luv.rs | 22 +- palette/tests/hsluv_dataset/hsluv_dataset.rs | 29 +- palette_derive/src/lib.rs | 3 +- 7 files changed, 842 insertions(+), 141 deletions(-) create mode 100644 palette/src/lchuv.rs diff --git a/palette/src/equality.rs b/palette/src/equality.rs index 94cbba802..ae25c34e6 100644 --- a/palette/src/equality.rs +++ b/palette/src/equality.rs @@ -2,7 +2,9 @@ use approx::{AbsDiffEq, RelativeEq, UlpsEq}; use crate::float::Float; use crate::white_point::WhitePoint; -use crate::{from_f64, FloatComponent, FromF64, Lab, LabHue, Lch, Luv, RgbHue, Xyz, Yxy}; +use crate::{ + from_f64, FloatComponent, FromF64, Lab, LabHue, Lch, Lchuv, Luv, LuvHue, RgbHue, Xyz, Yxy, +}; macro_rules! impl_eq { ( $self_ty: ident , [$($element: ident),+]) => { @@ -66,6 +68,7 @@ impl_eq!(Yxy, [y, x, luma]); impl_eq!(Lab, [l, a, b]); impl_eq!(Luv, [l, u, v]); impl_eq!(Lch, [l, chroma, hue]); +impl_eq!(Lchuv, [l, chroma, hue]); // For hues, the difference is calculated and compared to zero. However due to // the way floating point's work this is not so simple. @@ -147,3 +150,4 @@ macro_rules! impl_eq_hue { impl_eq_hue!(LabHue); impl_eq_hue!(RgbHue); +impl_eq_hue!(LuvHue); diff --git a/palette/src/hues.rs b/palette/src/hues.rs index a09f4850f..37149ee2d 100644 --- a/palette/src/hues.rs +++ b/palette/src/hues.rs @@ -20,7 +20,7 @@ macro_rules! make_hues { /// number (like `f32`). This makes many calculations easier, but may /// also have some surprising effects if it's expected to act as a /// linear number. - #[derive(Clone, Copy, Debug, Default)] + #[derive(Clone, Copy, Debug, Default)] #[cfg_attr(feature = "serializing", derive(Serialize, Deserialize))] #[repr(C)] pub struct $name(T); @@ -256,7 +256,7 @@ macro_rules! make_hues { Standard: Distribution, { fn sample(&self, rng: &mut R) -> $name { - $name(rng.gen() * from_f64(360.0)) + $name(rng.gen() * from_f64(360.0)) } } )+) @@ -293,146 +293,84 @@ fn normalize_angle_positive(deg: T) -> T { deg - ((deg / c360).floor() * c360) } -#[cfg(feature = "random")] -pub struct UniformLabHue -where - T: Float + FromF64 + SampleUniform, -{ - hue: Uniform, -} - -#[cfg(feature = "random")] -impl SampleUniform for LabHue -where - T: Float + FromF64 + SampleUniform, -{ - type Sampler = UniformLabHue; -} - -#[cfg(feature = "random")] -impl UniformSampler for UniformLabHue -where - T: Float + FromF64 + SampleUniform, -{ - type X = LabHue; - - fn new(low_b: B1, high_b: B2) -> Self - where - B1: SampleBorrow + Sized, - B2: SampleBorrow + Sized, - { - let low = *low_b.borrow(); - let normalized_low = LabHue::to_positive_degrees(low); - let high = *high_b.borrow(); - let normalized_high = LabHue::to_positive_degrees(high); - - let normalized_high = if normalized_low >= normalized_high && low.0 < high.0 { - normalized_high + from_f64(360.0) - } else { - normalized_high - }; - - UniformLabHue { - hue: Uniform::new(normalized_low, normalized_high), +macro_rules! impl_uniform { + ( $uni_ty: ident , $base_ty: ident) => { + #[cfg(feature = "random")] + pub struct $uni_ty + where + T: Float + FromF64 + SampleUniform, + { + hue: Uniform, } - } - fn new_inclusive(low_b: B1, high_b: B2) -> Self - where - B1: SampleBorrow + Sized, - B2: SampleBorrow + Sized, - { - let low = *low_b.borrow(); - let normalized_low = LabHue::to_positive_degrees(low); - let high = *high_b.borrow(); - let normalized_high = LabHue::to_positive_degrees(high); - - let normalized_high = if normalized_low >= normalized_high && low.0 < high.0 { - normalized_high + from_f64(360.0) - } else { - normalized_high - }; - - UniformLabHue { - hue: Uniform::new_inclusive(normalized_low, normalized_high), + #[cfg(feature = "random")] + impl SampleUniform for $base_ty + where + T: Float + FromF64 + SampleUniform, + { + type Sampler = $uni_ty; } - } - - fn sample(&self, rng: &mut R) -> LabHue { - LabHue::from(self.hue.sample(rng) * from_f64(360.0)) - } -} -#[cfg(feature = "random")] -pub struct UniformRgbHue -where - T: Float + FromF64 + SampleUniform, -{ - hue: Uniform, -} - -#[cfg(feature = "random")] -impl SampleUniform for RgbHue -where - T: Float + FromF64 + SampleUniform, -{ - type Sampler = UniformRgbHue; -} + #[cfg(feature = "random")] + impl UniformSampler for $uni_ty + where + T: Float + FromF64 + SampleUniform, + { + type X = $base_ty; + + fn new(low_b: B1, high_b: B2) -> Self + where + B1: SampleBorrow + Sized, + B2: SampleBorrow + Sized, + { + let low = *low_b.borrow(); + let normalized_low = $base_ty::to_positive_degrees(low); + let high = *high_b.borrow(); + let normalized_high = $base_ty::to_positive_degrees(high); + + let normalized_high = if normalized_low >= normalized_high && low.0 < high.0 { + normalized_high + from_f64(360.0) + } else { + normalized_high + }; + + $uni_ty { + hue: Uniform::new(normalized_low, normalized_high), + } + } -#[cfg(feature = "random")] -impl UniformSampler for UniformRgbHue -where - T: Float + FromF64 + SampleUniform, -{ - type X = RgbHue; - - fn new(low_b: B1, high_b: B2) -> Self - where - B1: SampleBorrow + Sized, - B2: SampleBorrow + Sized, - { - let low = *low_b.borrow(); - let normalized_low = RgbHue::to_positive_degrees(low); - let high = *high_b.borrow(); - let normalized_high = RgbHue::to_positive_degrees(high); - - let normalized_high = if normalized_low >= normalized_high && low.0 < high.0 { - normalized_high + from_f64(360.0) - } else { - normalized_high - }; - - UniformRgbHue { - hue: Uniform::new(normalized_low, normalized_high), - } - } + fn new_inclusive(low_b: B1, high_b: B2) -> Self + where + B1: SampleBorrow + Sized, + B2: SampleBorrow + Sized, + { + let low = *low_b.borrow(); + let normalized_low = $base_ty::to_positive_degrees(low); + let high = *high_b.borrow(); + let normalized_high = $base_ty::to_positive_degrees(high); + + let normalized_high = if normalized_low >= normalized_high && low.0 < high.0 { + normalized_high + from_f64(360.0) + } else { + normalized_high + }; + + $uni_ty { + hue: Uniform::new_inclusive(normalized_low, normalized_high), + } + } - fn new_inclusive(low_b: B1, high_b: B2) -> Self - where - B1: SampleBorrow + Sized, - B2: SampleBorrow + Sized, - { - let low = *low_b.borrow(); - let normalized_low = RgbHue::to_positive_degrees(low); - let high = *high_b.borrow(); - let normalized_high = RgbHue::to_positive_degrees(high); - - let normalized_high = if normalized_low >= normalized_high && low.0 < high.0 { - normalized_high + from_f64(360.0) - } else { - normalized_high - }; - - UniformRgbHue { - hue: Uniform::new_inclusive(normalized_low, normalized_high), + fn sample(&self, rng: &mut R) -> $base_ty { + $base_ty::from(self.hue.sample(rng) * from_f64(360.0)) + } } - } - - fn sample(&self, rng: &mut R) -> RgbHue { - RgbHue::from(self.hue.sample(rng) * from_f64(360.0)) - } + }; } +impl_uniform!(UniformLabHue, LabHue); +impl_uniform!(UniformRgbHue, RgbHue); +impl_uniform!(UniformLuvHue, LuvHue); + #[cfg(test)] mod test { use super::{normalize_angle, normalize_angle_positive}; diff --git a/palette/src/lchuv.rs b/palette/src/lchuv.rs new file mode 100644 index 000000000..aa26cf35d --- /dev/null +++ b/palette/src/lchuv.rs @@ -0,0 +1,717 @@ +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, Standard}; +#[cfg(feature = "random")] +use rand::Rng; + +use crate::convert::FromColorUnclamped; +use crate::encoding::pixel::RawPixel; +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, +}; + +/// CIE L\*C\*uv h°uv with an alpha component. See the [`Lchuva` implementation in +/// `Alpha`](crate::Alpha#Lchuva). +pub type Lchuva = Alpha, T>; + +/// CIE L\*C\*uv h°uv, a polar version of [CIE L\*u\*v\*](crate::Lab). +/// +/// L\*C\*uv h°uv shares its range and perceptual uniformity with L\*u\*v\*, but +/// it's a cylindrical color space, like [HSL](crate::Hsl) and +/// [HSV](crate::Hsv). This gives it the same ability to directly change +/// the hue and colorfulness of a color, while preserving other visual aspects. +#[derive(Debug, Pixel, FromColorUnclamped, WithAlpha)] +#[cfg_attr(feature = "serializing", derive(Serialize, Deserialize))] +#[palette( + palette_internal, + white_point = "Wp", + component = "T", + skip_derives(Luv, Lchuv) +)] +#[repr(C)] +pub struct Lchuv +where + T: FloatComponent, + Wp: WhitePoint, +{ + /// L\* is the lightness of the color. 0.0 gives absolute black and 100.0 + /// gives the brightest white. + pub l: T, + + /// C\*uv is the colorfulness of the color. It's similar to + /// saturation. 0.0 gives gray scale colors, and numbers around + /// 130-180 gives fully saturated colors, depending on the + /// hue. The upper limit of 180 should include the whole + /// L\*u\*v\*. + pub chroma: T, + + /// 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 white point associated with the color's illuminant and observer. + /// D65 for 2 degree observer is used by default. + #[cfg_attr(feature = "serializing", serde(skip))] + #[palette(unsafe_zero_sized)] + pub white_point: PhantomData, +} + +impl Copy for Lchuv +where + T: FloatComponent, + Wp: WhitePoint, +{ +} + +impl Clone for Lchuv +where + T: FloatComponent, + Wp: WhitePoint, +{ + fn clone(&self) -> Lchuv { + *self + } +} + +impl Lchuv +where + T: FloatComponent, +{ + /// CIE L\*C\*uv h°uv with white point D65. + pub fn new>>(l: T, chroma: T, hue: H) -> Lchuv { + Lchuv { + l, + chroma, + hue: hue.into(), + white_point: PhantomData, + } + } +} + +impl Lchuv +where + T: FloatComponent, + Wp: WhitePoint, +{ + /// CIE L\*C\*uv h°uv + pub fn with_wp>>(l: T, chroma: T, hue: H) -> Lchuv { + Lchuv { + l, + chroma, + hue: hue.into(), + white_point: PhantomData, + } + } + + /// Convert to a `(L\*, C\*uv, h°uv)` tuple. + pub fn into_components(self) -> (T, T, LuvHue) { + (self.l, self.chroma, self.hue) + } + + /// Convert from a `(L\*, C\*uv, h°uv)` tuple. + pub fn from_components>>((l, chroma, hue): (T, T, H)) -> Self { + Self::with_wp(l, chroma, hue) + } + + /// Return the `l` value minimum. + pub fn min_l() -> T { + T::zero() + } + + /// Return the `l` value maximum. + pub fn max_l() -> T { + from_f64(100.0) + } + + /// Return the `chroma` value minimum. + pub fn min_chroma() -> T { + T::zero() + } + + /// Return the `chroma` value maximum. + pub fn max_chroma() -> T { + from_f64(180.0) + } +} + +impl PartialEq for Lchuv +where + T: FloatComponent + PartialEq, + Wp: WhitePoint, +{ + fn eq(&self, other: &Self) -> bool { + self.l == other.l && self.chroma == other.chroma && self.hue == other.hue + } +} + +impl Eq for Lchuv +where + T: FloatComponent + Eq, + Wp: WhitePoint, +{ +} + +///[`Lchuva`](crate::Lchuva) implementations. +impl Alpha, A> +where + T: FloatComponent, + A: Component, +{ + /// CIE L\*C\*uv h°uv and transparency with white point D65. + pub fn new>>(l: T, chroma: T, hue: H, alpha: A) -> Self { + Alpha { + color: Lchuv::new(l, chroma, hue), + alpha, + } + } +} + +///[`Lchuva`](crate::Lchuva) implementations. +impl Alpha, A> +where + T: FloatComponent, + A: Component, + Wp: WhitePoint, +{ + /// CIE L\*C\*uv h°uv and transparency. + pub fn with_wp>>(l: T, chroma: T, hue: H, alpha: A) -> Self { + Alpha { + color: Lchuv::with_wp(l, chroma, hue), + alpha, + } + } + + /// Convert to a `(L\*, C\*uv, h°uv, alpha)` tuple. + pub fn into_components(self) -> (T, T, LuvHue, A) { + (self.l, self.chroma, self.hue, self.alpha) + } + + /// Convert from a `(L\*, C\*uv, h°uv, alpha)` tuple. + pub fn from_components>>((l, chroma, hue, alpha): (T, T, H, A)) -> Self { + Self::with_wp(l, chroma, hue, alpha) + } +} + +impl FromColorUnclamped> for Lchuv +where + Wp: WhitePoint, + T: FloatComponent, +{ + fn from_color_unclamped(color: Lchuv) -> Self { + color + } +} + +impl FromColorUnclamped> for Lchuv +where + Wp: WhitePoint, + T: FloatComponent, +{ + fn from_color_unclamped(color: Luv) -> Self { + Lchuv { + l: color.l, + chroma: color.u.hypot(color.v), + hue: color.get_hue().unwrap_or(LuvHue::from(T::zero())), + white_point: PhantomData, + } + } +} + +impl>> From<(T, T, H)> for Lchuv { + fn from(components: (T, T, H)) -> Self { + Self::from_components(components) + } +} + +impl Into<(T, T, LuvHue)> for Lchuv { + fn into(self) -> (T, T, LuvHue) { + self.into_components() + } +} + +impl>, A: Component> From<(T, T, H, A)> + for Alpha, A> +{ + fn from(components: (T, T, H, A)) -> Self { + Self::from_components(components) + } +} + +impl Into<(T, T, LuvHue, A)> + for Alpha, A> +{ + fn into(self) -> (T, T, LuvHue, A) { + self.into_components() + } +} + +impl Clamp for Lchuv +where + T: FloatComponent, + Wp: WhitePoint, +{ + fn is_within_bounds(&self) -> bool { + self.l >= Self::min_l() && self.l <= Self::max_l() && + self.chroma >= Self::min_chroma() && self.chroma <= Self::max_chroma() + } + + fn clamp(&self) -> Lchuv { + let mut c = *self; + c.clamp_self(); + c + } + + fn clamp_self(&mut self) { + self.l = clamp(self.l, Self::min_l(), Self::max_l()); + self.chroma = clamp(self.chroma, Self::min_chroma(), Self::max_chroma()); + } +} + +impl Mix for Lchuv +where + T: FloatComponent, + Wp: WhitePoint, +{ + type Scalar = T; + + fn mix(&self, other: &Lchuv, factor: T) -> Lchuv { + let factor = clamp(factor, T::zero(), T::one()); + let hue_diff: T = (other.hue - self.hue).to_degrees(); + Lchuv { + l: self.l + factor * (other.l - self.l), + chroma: self.chroma + factor * (other.chroma - self.chroma), + hue: self.hue + factor * hue_diff, + white_point: PhantomData, + } + } +} + +impl Shade for Lchuv +where + T: FloatComponent, + Wp: WhitePoint, +{ + type Scalar = T; + + fn lighten(&self, factor: T) -> Lchuv { + let difference = if factor >= T::zero() { + T::from_f64(100.0) - self.l + } else { + self.l + }; + + let delta = difference.max(T::zero()) * factor; + + Lchuv { + l: (self.l + delta).max(T::zero()), + chroma: self.chroma, + hue: self.hue, + white_point: PhantomData, + } + } + + fn lighten_fixed(&self, amount: T) -> Lchuv { + Lchuv { + l: (self.l + T::from_f64(100.0) * amount).max(T::zero()), + chroma: self.chroma, + hue: self.hue, + white_point: PhantomData, + } + } +} + +impl GetHue for Lchuv +where + T: FloatComponent, + Wp: WhitePoint, +{ + type Hue = LuvHue; + + fn get_hue(&self) -> Option> { + if self.chroma <= T::zero() { + None + } else { + Some(self.hue) + } + } +} + +impl Hue for Lchuv +where + T: FloatComponent, + Wp: WhitePoint, +{ + fn with_hue>(&self, hue: H) -> Lchuv { + Lchuv { + l: self.l, + chroma: self.chroma, + hue: hue.into(), + white_point: PhantomData, + } + } + + fn shift_hue>(&self, amount: H) -> Lchuv { + Lchuv { + l: self.l, + chroma: self.chroma, + hue: self.hue + amount.into(), + white_point: PhantomData, + } + } +} + +impl Saturate for Lchuv +where + T: FloatComponent, + Wp: WhitePoint, +{ + type Scalar = T; + + fn saturate(&self, factor: T) -> Lchuv { + let difference = if factor >= T::zero() { + Self::max_chroma() - self.chroma + } else { + self.chroma + }; + + let delta = difference.max(T::zero()) * factor; + + Lchuv { + l: self.l, + chroma: (self.chroma + delta).max(T::zero()), + hue: self.hue, + white_point: PhantomData, + } + } + + fn saturate_fixed(&self, amount: T) -> Lchuv { + Lchuv { + l: self.l, + chroma: (self.chroma + Self::max_chroma() * amount).max(T::zero()), + hue: self.hue, + white_point: PhantomData, + } + } +} + +impl Default for Lchuv +where + T: FloatComponent, + Wp: WhitePoint, +{ + fn default() -> Lchuv { + Lchuv::with_wp(T::zero(), T::zero(), LuvHue::from(T::zero())) + } +} + +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 AsRef

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

for Lchuv +where + T: FloatComponent, + Wp: WhitePoint, + P: RawPixel + ?Sized, +{ + fn as_mut(&mut self) -> &mut P { + self.as_raw_mut() + } +} + +impl RelativeContrast for Lchuv +where + Wp: WhitePoint, + T: FloatComponent, +{ + type Scalar = T; + + fn get_contrast_ratio(&self, other: &Self) -> T { + let xyz1 = Xyz::from_color(*self); + let xyz2 = Xyz::from_color(*other); + + contrast_ratio(xyz1.y, xyz2.y) + } +} + +#[cfg(feature = "random")] +impl Distribution> for Standard +where + T: FloatComponent, + Wp: WhitePoint, + Standard: Distribution, +{ + fn sample(&self, rng: &mut R) -> Lchuv { + Lchuv { + l: rng.gen() * from_f64(100.0), + chroma: crate::Float::sqrt(rng.gen()) * from_f64(180.0), + hue: rng.gen::>(), + white_point: PhantomData, + } + } +} + +#[cfg(feature = "random")] +pub struct UniformLchuv +where + T: FloatComponent + SampleUniform, + Wp: WhitePoint, +{ + l: Uniform, + chroma: Uniform, + hue: crate::hues::UniformLuvHue, + white_point: PhantomData, +} + +#[cfg(feature = "random")] +impl SampleUniform for Lchuv +where + T: FloatComponent + SampleUniform, + Wp: WhitePoint, +{ + type Sampler = UniformLchuv; +} + +#[cfg(feature = "random")] +impl UniformSampler for UniformLchuv +where + T: FloatComponent + SampleUniform, + Wp: WhitePoint, +{ + type X = Lchuv; + + fn new(low_b: B1, high_b: B2) -> Self + where + B1: SampleBorrow + Sized, + B2: SampleBorrow + Sized, + { + let low = *low_b.borrow(); + let high = *high_b.borrow(); + + UniformLchuv { + l: Uniform::new::<_, T>(low.l, high.l), + chroma: Uniform::new::<_, T>(low.chroma * low.chroma, high.chroma * high.chroma), + hue: crate::hues::UniformLuvHue::new(low.hue, high.hue), + white_point: PhantomData, + } + } + + fn new_inclusive(low_b: B1, high_b: B2) -> Self + where + B1: SampleBorrow + Sized, + B2: SampleBorrow + Sized, + { + let low = *low_b.borrow(); + let high = *high_b.borrow(); + + UniformLchuv { + l: Uniform::new_inclusive::<_, T>(low.l, high.l), + chroma: Uniform::new_inclusive::<_, T>( + low.chroma * low.chroma, + high.chroma * high.chroma, + ), + hue: crate::hues::UniformLuvHue::new_inclusive(low.hue, high.hue), + white_point: PhantomData, + } + } + + fn sample(&self, rng: &mut R) -> Lchuv { + Lchuv { + l: self.l.sample(rng), + chroma: crate::Float::sqrt(self.chroma.sample(rng)), + hue: self.hue.sample(rng), + white_point: PhantomData, + } + } +} + +#[cfg(test)] +mod test { + use crate::white_point::D65; + use crate::Lchuv; + + #[test] + fn ranges() { + assert_ranges! { + Lchuv; + clamped { + l: 0.0 => 100.0, + chroma: 0.0 => 180.0 + } + clamped_min { + } + unclamped { + hue: -360.0 => 360.0 + } + } + } + + raw_pixel_conversion_tests!(Lchuv: l, chroma, hue); + raw_pixel_conversion_fail_tests!(Lchuv: l, chroma, hue); + + #[test] + fn check_min_max_components() { + assert_relative_eq!(Lchuv::::min_l(), 0.0); + assert_relative_eq!(Lchuv::::max_l(), 100.0); + assert_relative_eq!(Lchuv::::min_chroma(), 0.0); + assert_relative_eq!(Lchuv::::max_chroma(), 180.0); + } + + #[cfg(feature = "serializing")] + #[test] + fn serialize() { + let serialized = ::serde_json::to_string(&Lchuv::new(80.0, 70.0, 130.0)).unwrap(); + + assert_eq!(serialized, r#"{"l":80.0,"chroma":70.0,"hue":130.0}"#); + } + + #[cfg(feature = "serializing")] + #[test] + fn deserialize() { + let deserialized: Lchuv = + ::serde_json::from_str(r#"{"l":70.0,"chroma":80.0,"hue":130.0}"#).unwrap(); + + assert_eq!(deserialized, Lchuv::new(70.0, 80.0, 130.0)); + } + + #[cfg(feature = "random")] + test_uniform_distribution! { + Lchuv as crate::Luv { + l: (0.0, 100.0), + u: (-80.0, 80.0), + v: (-80.0, 80.0), + }, + min: Lchuv::new(0.0f32, 0.0, 0.0), + max: Lchuv::new(100.0, 180.0, 360.0) + } +} diff --git a/palette/src/lib.rs b/palette/src/lib.rs index d50d51654..11796e959 100644 --- a/palette/src/lib.rs +++ b/palette/src/lib.rs @@ -242,6 +242,7 @@ pub use hsv::{Hsv, Hsva}; pub use hwb::{Hwb, Hwba}; pub use lab::{Lab, Laba}; pub use lch::{Lch, Lcha}; +pub use lchuv::{Lchuv, Lchuva}; pub use luma::{GammaLuma, GammaLumaa, LinLuma, LinLumaa, SrgbLuma, SrgbLumaa}; pub use luv::{Luv, Luva}; pub use rgb::{GammaSrgb, GammaSrgba, LinSrgb, LinSrgba, Packed, RgbChannels, Srgb, Srgba}; @@ -426,6 +427,7 @@ mod hsv; mod hwb; mod lab; mod lch; +mod lchuv; pub mod luma; mod luv; pub mod rgb; diff --git a/palette/src/luv.rs b/palette/src/luv.rs index 0691b1dbe..3c1a127f4 100644 --- a/palette/src/luv.rs +++ b/palette/src/luv.rs @@ -13,7 +13,7 @@ use crate::encoding::pixel::RawPixel; use crate::white_point::{WhitePoint, D65}; use crate::{ clamp, contrast_ratio, from_f64, Alpha, Clamp, Component, ComponentWise, FloatComponent, - GetHue, LuvHue, Mix, Pixel, RelativeContrast, Shade, Xyz, + GetHue, Lchuv, LuvHue, Mix, Pixel, RelativeContrast, Shade, Xyz, }; /// CIE L\*u\*v\* (CIELUV) with an alpha component. See the [`Luva` @@ -36,7 +36,7 @@ pub type Luva = Alpha, T>; palette_internal, white_point = "Wp", component = "T", - skip_derives(Xyz, Luv) + skip_derives(Xyz, Luv, Lchuv) )] #[repr(C)] pub struct Luv @@ -221,6 +221,18 @@ where } } +impl FromColorUnclamped> for Luv +where + Wp: WhitePoint, + T: FloatComponent, +{ + fn from_color_unclamped(color: Lchuv) -> Self { + let (sin_hue, cos_hue) = color.hue.to_radians().sin_cos(); + let chroma = color.chroma.max(T::zero()); + Luv::with_wp(color.l, chroma * cos_hue, chroma * sin_hue) + } +} + impl FromColorUnclamped> for Luv where Wp: WhitePoint, @@ -848,9 +860,9 @@ mod test { #[cfg(feature = "random")] test_uniform_distribution! { Luv { - l: (0.0, 100.0), - u: (-84.0, 176.0), - v: (-135.0, 108.0) + l: (0.0, 100.0), + u: (-84.0, 176.0), + v: (-135.0, 108.0) }, min: Luv::new(0.0f32, -84.0, -135.0), max: Luv::new(100.0, 176.0, 108.0) diff --git a/palette/tests/hsluv_dataset/hsluv_dataset.rs b/palette/tests/hsluv_dataset/hsluv_dataset.rs index ef95f31ec..0d352478d 100644 --- a/palette/tests/hsluv_dataset/hsluv_dataset.rs +++ b/palette/tests/hsluv_dataset/hsluv_dataset.rs @@ -7,12 +7,13 @@ use serde_json; use palette::convert::IntoColorUnclamped; use palette::white_point::D65; -use palette::{Luv, Xyz}; +use palette::{Lchuv, Luv, LuvHue, Xyz}; use std::collections::HashMap; #[derive(Clone, Debug)] struct HsluvExample { name: String, + lchuv: Lchuv, luv: Luv, xyz: Xyz, } @@ -34,6 +35,12 @@ fn load_data() -> Examples { .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() @@ -46,6 +53,7 @@ fn load_data() -> Examples { HsluvExample { name: k.clone(), luv: Luv::new(luv_data[0], luv_data[1], luv_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]), }, ) @@ -72,3 +80,22 @@ pub fn run_luv_to_xyz_tests() { assert_relative_eq!(to_xyz, v.xyz, epsilon = 0.001); } } + +#[test] +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); + } +} + +#[test] +pub fn run_luv_to_lchuv_tests() { + for (_, v) in TEST_DATA.iter() { + let mut to_lchuv: Lchuv = v.luv.into_color_unclamped(); + if to_lchuv.chroma < 1e-8 { + to_lchuv.hue = LuvHue::from_degrees(0.0); + } + assert_relative_eq!(to_lchuv, v.lchuv, epsilon = 0.001); + } +} diff --git a/palette_derive/src/lib.rs b/palette_derive/src/lib.rs index c143d2e5c..9662e2664 100644 --- a/palette_derive/src/lib.rs +++ b/palette_derive/src/lib.rs @@ -38,7 +38,7 @@ mod meta; mod util; const COLOR_TYPES: &[&str] = &[ - "Rgb", "Luma", "Hsl", "Hsv", "Hwb", "Lab", "Lch", "Luv", "Xyz", "Yxy", + "Rgb", "Luma", "Hsl", "Hsv", "Hwb", "Lab", "Lch", "Lchuv", "Luv", "Xyz", "Yxy", ]; const PREFERRED_CONVERSION_SOURCE: &[(&str, &str)] = &[ @@ -49,6 +49,7 @@ const PREFERRED_CONVERSION_SOURCE: &[(&str, &str)] = &[ ("Hwb", "Hsv"), ("Lab", "Xyz"), ("Lch", "Lab"), + ("Lchuv", "Luv"), ("Luv", "Xyz"), ("Yxy", "Xyz"), ]; From 41efda57bf59a049d0d7d6810e866357ae258bee Mon Sep 17 00:00:00 2001 From: Mason Smith Date: Mon, 10 May 2021 16:34:02 -0700 Subject: [PATCH 2/3] cargo fmt --- palette/src/lchuv.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/palette/src/lchuv.rs b/palette/src/lchuv.rs index aa26cf35d..10a9d8395 100644 --- a/palette/src/lchuv.rs +++ b/palette/src/lchuv.rs @@ -258,8 +258,10 @@ where Wp: WhitePoint, { fn is_within_bounds(&self) -> bool { - self.l >= Self::min_l() && self.l <= Self::max_l() && - self.chroma >= Self::min_chroma() && self.chroma <= Self::max_chroma() + self.l >= Self::min_l() + && self.l <= Self::max_l() + && self.chroma >= Self::min_chroma() + && self.chroma <= Self::max_chroma() } fn clamp(&self) -> Lchuv { From 384c6b63d5c3660044723aac9e95d49e5b74e0ba Mon Sep 17 00:00:00 2001 From: Mason Smith Date: Wed, 12 May 2021 10:20:26 -0700 Subject: [PATCH 3/3] formatting fix in hue.rs rustfmt is unopionated inside macros for now, so this commit manually adjusts some indentation to make it look better. --- palette/src/hues.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/palette/src/hues.rs b/palette/src/hues.rs index 37149ee2d..9b96e8d68 100644 --- a/palette/src/hues.rs +++ b/palette/src/hues.rs @@ -20,7 +20,7 @@ macro_rules! make_hues { /// number (like `f32`). This makes many calculations easier, but may /// also have some surprising effects if it's expected to act as a /// linear number. - #[derive(Clone, Copy, Debug, Default)] + #[derive(Clone, Copy, Debug, Default)] #[cfg_attr(feature = "serializing", derive(Serialize, Deserialize))] #[repr(C)] pub struct $name(T);