diff --git a/palette/src/hsl.rs b/palette/src/hsl.rs index e0859eb59..6bfa3fb86 100644 --- a/palette/src/hsl.rs +++ b/palette/src/hsl.rs @@ -755,6 +755,8 @@ mod test { use super::Hsl; use crate::{FromColor, Hsv, Srgb}; + test_convert_into_from_xyz!(Hsl); + #[test] fn red() { let a = Hsl::from_color(Srgb::new(1.0, 0.0, 0.0)); diff --git a/palette/src/hsluv.rs b/palette/src/hsluv.rs index 7ddb878ee..e32ca1854 100644 --- a/palette/src/hsluv.rs +++ b/palette/src/hsluv.rs @@ -479,6 +479,8 @@ mod test { use super::Hsluv; use crate::{white_point::D65, FromColor, Lchuv, LuvHue, Saturate}; + test_convert_into_from_xyz!(Hsluv); + #[test] fn lchuv_round_trip() { for hue in (0..=20).map(|x| x as f64 * 18.0) { diff --git a/palette/src/hsv.rs b/palette/src/hsv.rs index 9b6065e4c..0503dfef2 100644 --- a/palette/src/hsv.rs +++ b/palette/src/hsv.rs @@ -754,6 +754,8 @@ mod test { use super::Hsv; use crate::{FromColor, Hsl, Srgb}; + test_convert_into_from_xyz!(Hsv); + #[test] fn red() { let a = Hsv::from_color(Srgb::new(1.0, 0.0, 0.0)); diff --git a/palette/src/hues.rs b/palette/src/hues.rs index 5e7645ad9..e881aea7e 100644 --- a/palette/src/hues.rs +++ b/palette/src/hues.rs @@ -3,7 +3,7 @@ use core::ops::Mul; use core::{ cmp::PartialEq, - ops::{Add, AddAssign, Sub, SubAssign}, + ops::{Add, AddAssign, Neg, Sub, SubAssign}, }; #[cfg(feature = "approx")] @@ -19,14 +19,15 @@ use rand::{ }; #[cfg(feature = "approx")] -use crate::angle::HalfRotation; -use crate::num::Zero; +use crate::{angle::HalfRotation, num::Zero}; #[cfg(feature = "random")] use crate::angle::FullRotation; -use crate::angle::{AngleEq, FromAngle, RealAngle, SignedAngle, UnsignedAngle}; -use crate::num::{Arithmetics, Trigonometry}; +use crate::{ + angle::{AngleEq, FromAngle, RealAngle, SignedAngle, UnsignedAngle}, + num::Trigonometry, +}; macro_rules! make_hues { ($($(#[$doc:meta])+ struct $name:ident;)+) => ($( @@ -132,6 +133,32 @@ macro_rules! make_hues { } } + impl $name { + /// Returns a hue from `a` and `b`, normalized to `[0°, 360°)`. + /// + /// If `a` and `b` are both `0`, returns `0`, + #[inline(always)] + pub fn from_cartesian(a: T, b: T) -> Self where T: Add + Neg { + // atan2 returns values in the interval [-π, π] + // instead of + // let hue_rad = T::atan2(b,a); + // use negative a and be and rotate, to ensure the hue is normalized, + let hue_rad = T::from_f64(core::f64::consts::PI) + T::atan2(-b, -a); + Self::from_radians(hue_rad) + } + + /// Returns `a` and `b` values for this hue, normalized to `[-1, + /// 1]`. + /// + /// They will have to be multiplied by a radius values, such as + /// saturation, value, chroma, etc., to represent a specific color. + #[inline(always)] + pub fn into_cartesian(self) -> (T, T) { + let (b, a) = self.into_raw_radians().sin_cos(); + (a, b) // Note the swapped order compared to above + } + } + impl From for $name { #[inline] fn from(degrees: T) -> $name { @@ -518,36 +545,6 @@ impl_uniform!(UniformRgbHue, RgbHue); impl_uniform!(UniformLuvHue, LuvHue); impl_uniform!(UniformOklabHue, OklabHue); -impl OklabHue -where - T: RealAngle + Zero + Arithmetics + Trigonometry + Clone + PartialEq, -{ - /// Returns `a` and `b` values for this hue at the given `croma` - #[inline(always)] - pub fn ab(self, chroma: T) -> (T, T) { - let hue_rad = self.into_raw_radians(); - let (sin, cos) = hue_rad.sin_cos(); - (chroma.clone() * cos, chroma * sin) - } - - /// Returns a hue from `a` and `b`, with a normalized angle, i.e. an angle in the half - /// open interval [0° .. 360°). - /// If `a` and `b` are both `zero`, returns `None` - #[inline(always)] - pub fn from_ab(a: T, b: T) -> Option { - if a == T::zero() && b == T::zero() { - None - } else { - // atan2 returns values in the interval [-π .. π] - // instead of - // let hue_rad = T::atan2(b,a); - // use negative a and be and rotate, to ensure the hue is normalized, - let hue_rad = T::from_f64(core::f64::consts::PI) + T::atan2(-b, -a); - Some(Self::from_radians(hue_rad)) - } - } -} - #[cfg(test)] mod test { use crate::{ @@ -559,8 +556,8 @@ mod test { fn oklabhue_ab_roundtrip() { for degree in [0.0_f64, 90.0, 30.0, 330.0, 120.0, 240.0] { let hue = OklabHue::from_degrees(degree); - let (a, b) = hue.ab(10000.0); - let roundtrip_hue = OklabHue::from_ab(a, b).unwrap(); + let (a, b) = hue.into_cartesian(); + let roundtrip_hue = OklabHue::from_cartesian(a * 10000.0, b * 10000.0); assert_abs_diff_eq!(roundtrip_hue, hue); } } diff --git a/palette/src/hwb.rs b/palette/src/hwb.rs index 8aa0b189e..0c14b4c93 100644 --- a/palette/src/hwb.rs +++ b/palette/src/hwb.rs @@ -787,6 +787,8 @@ mod test { use super::Hwb; use crate::{Clamp, FromColor, Srgb}; + test_convert_into_from_xyz!(Hwb); + #[test] fn red() { let a = Hwb::from_color(Srgb::new(1.0, 0.0, 0.0)); diff --git a/palette/src/lab.rs b/palette/src/lab.rs index 204744f64..c4b8b26d9 100644 --- a/palette/src/lab.rs +++ b/palette/src/lab.rs @@ -1,6 +1,6 @@ use core::{ marker::PhantomData, - ops::{Add, AddAssign, BitAnd, BitOr, Div, DivAssign, Mul, MulAssign, Sub, SubAssign}, + ops::{Add, AddAssign, BitAnd, BitOr, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign}, }; #[cfg(feature = "approx")] @@ -295,12 +295,12 @@ impl_premultiply!(Lab {l, a, b} phantom: white_point); impl GetHue for Lab where - T: RealAngle + Trigonometry + Clone, + T: RealAngle + Trigonometry + Add + Neg + Clone, { type Hue = LabHue; fn get_hue(&self) -> LabHue { - LabHue::from_radians(self.b.clone().atan2(self.a.clone())) + LabHue::from_cartesian(self.a.clone(), self.b.clone()) } } @@ -469,6 +469,8 @@ mod test { use crate::white_point::D65; use crate::{FromColor, LinSrgb}; + test_convert_into_from_xyz!(Lab); + #[test] fn red() { let a = Lab::from_color(LinSrgb::new(1.0, 0.0, 0.0)); diff --git a/palette/src/lch.rs b/palette/src/lch.rs index 76ed8b980..9fd246cc2 100644 --- a/palette/src/lch.rs +++ b/palette/src/lch.rs @@ -497,6 +497,8 @@ mod test { use crate::white_point::D65; use crate::Lch; + test_convert_into_from_xyz!(Lch); + #[test] fn ranges() { assert_ranges! { diff --git a/palette/src/lchuv.rs b/palette/src/lchuv.rs index b9837980e..4db2911e3 100644 --- a/palette/src/lchuv.rs +++ b/palette/src/lchuv.rs @@ -479,6 +479,8 @@ mod test { use crate::white_point::D65; use crate::Lchuv; + test_convert_into_from_xyz!(Lchuv); + #[test] fn ranges() { assert_ranges! { diff --git a/palette/src/luma/luma.rs b/palette/src/luma/luma.rs index 5de316764..739b25d30 100644 --- a/palette/src/luma/luma.rs +++ b/palette/src/luma/luma.rs @@ -1189,6 +1189,8 @@ mod test { use crate::encoding::Srgb; use crate::Luma; + test_convert_into_from_xyz!(Luma); + #[test] fn ranges() { assert_ranges! { diff --git a/palette/src/luv.rs b/palette/src/luv.rs index c67059a05..ff63c0534 100644 --- a/palette/src/luv.rs +++ b/palette/src/luv.rs @@ -1,6 +1,6 @@ use core::{ marker::PhantomData, - ops::{Add, AddAssign, BitAnd, Div, DivAssign, Mul, MulAssign, Sub, SubAssign}, + ops::{Add, AddAssign, BitAnd, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign}, }; #[cfg(feature = "approx")] @@ -302,12 +302,12 @@ impl_premultiply!(Luv {l, u, v} phantom: white_point); impl GetHue for Luv where - T: RealAngle + Trigonometry + Clone, + T: RealAngle + Trigonometry + Add + Neg + Clone, { type Hue = LuvHue; fn get_hue(&self) -> LuvHue { - LuvHue::from_radians(self.v.clone().atan2(self.u.clone())) + LuvHue::from_cartesian(self.u.clone(), self.v.clone()) } } @@ -451,6 +451,8 @@ mod test { use crate::white_point::D65; use crate::{FromColor, LinSrgb}; + test_convert_into_from_xyz!(Luv); + #[test] fn red() { let u = Luv::from_color(LinSrgb::new(1.0, 0.0, 0.0)); diff --git a/palette/src/macros.rs b/palette/src/macros.rs index d45fd434f..ef4a0bd4f 100644 --- a/palette/src/macros.rs +++ b/palette/src/macros.rs @@ -21,6 +21,8 @@ mod lazy_select; mod simd; #[macro_use] mod clamp; +#[macro_use] +mod convert; #[cfg(feature = "random")] #[macro_use] diff --git a/palette/src/macros/convert.rs b/palette/src/macros/convert.rs new file mode 100644 index 000000000..644db8c41 --- /dev/null +++ b/palette/src/macros/convert.rs @@ -0,0 +1,19 @@ +/// Check that traits for converting to and from XYZ have been implemented. +#[cfg(test)] +macro_rules! test_convert_into_from_xyz { + ($ty:ty) => { + #[test] + fn convert_from_xyz() { + use crate::FromColor; + + let _: $ty = <$ty>::from_color(crate::Xyz::default()); + } + + #[test] + fn convert_into_xyz() { + use crate::FromColor; + + let _: crate::Xyz = crate::Xyz::from_color(<$ty>::default()); + } + }; +} diff --git a/palette/src/ok_utils.rs b/palette/src/ok_utils.rs index 1b02d7e11..6e9a180c2 100644 --- a/palette/src/ok_utils.rs +++ b/palette/src/ok_utils.rs @@ -1,14 +1,13 @@ //! Traits and functions used in Ok* color spaces + #[cfg(test)] -use crate::angle::RealAngle; -use crate::convert::IntoColorUnclamped; -use crate::num::{ - Arithmetics, Cbrt, FromScalar, IsValidDivisor, MinMax, One, Powi, Real, Recip, Sqrt, - Trigonometry, Zero, +use crate::{angle::RealAngle, num::Trigonometry, OklabHue}; + +use crate::{ + convert::IntoColorUnclamped, + num::{Arithmetics, Cbrt, MinMax, One, Powi, Real, Sqrt, Zero}, + HasBoolMask, LinSrgb, Oklab, }; -#[cfg(test)] -use crate::OklabHue; -use crate::{HasBoolMask, LinSrgb, Oklab}; /// Finds intersection of the line defined by /// @@ -17,101 +16,77 @@ use crate::{HasBoolMask, LinSrgb, Oklab}; /// C = t * c1; /// /// a and b must be normalized so a² + b² == 1 -fn find_gamut_intersection(a: T, b: T, l1: T, c1: T, l0: T, cusp: Option>) -> T +fn find_gamut_intersection(a: T, b: T, l1: T, c1: T, l0: T, cusp: LC) -> T where - T: Real - + One - + Zero - + Arithmetics - + Sqrt - + MinMax - + Copy - + PartialEq - + PartialOrd - + HasBoolMask - + Powi - + Cbrt - + Trigonometry - + FromScalar, - T::Scalar: Real - + Zero - + One - + Recip - + IsValidDivisor - + Arithmetics - + Clone - + FromScalar, + T: Real + One + Zero + Arithmetics + MinMax + HasBoolMask + PartialOrd + Clone, { - // Find the cusp of the gamut triangle - let cusp = cusp.unwrap_or_else(|| LC::find_cusp(a, b)); - // Find the intersection for upper and lower half separately - if ((l1 - l0) * cusp.chroma - (cusp.lightness - l0) * c1) <= T::zero() { + if ((l1.clone() - &l0) * &cusp.chroma - (cusp.lightness.clone() - &l0) * &c1) <= T::zero() { // Lower half - cusp.chroma * l0 / (c1 * cusp.lightness + cusp.chroma * (l0 - l1)) + cusp.chroma.clone() * &l0 / (c1 * cusp.lightness + cusp.chroma * (l0 - l1)) } else { // Upper half // First intersect with triangle - let t = cusp.chroma * (l0 - T::one()) - / (c1 * (cusp.lightness - T::one()) + cusp.chroma * (l0 - l1)); + let t = cusp.chroma.clone() * (l0.clone() - T::one()) + / (c1.clone() * (cusp.lightness - T::one()) + cusp.chroma * (l0.clone() - &l1)); // Then one step Halley's method { - let dl = l1 - l0; - let dc = c1; + let dl = l1.clone() - &l0; + let dc = c1.clone(); - let k_l = T::from_f64(0.3963377774) * a + T::from_f64(0.2158037573) * b; - let k_m = -T::from_f64(0.1055613458) * a - T::from_f64(0.0638541728) * b; + let k_l = T::from_f64(0.3963377774) * &a + T::from_f64(0.2158037573) * &b; + let k_m = -T::from_f64(0.1055613458) * &a - T::from_f64(0.0638541728) * &b; let k_s = -T::from_f64(0.0894841775) * a - T::from_f64(1.2914855480) * b; - let l_dt = dl + dc * k_l; - let m_dt = dl + dc * k_m; - let s_dt = dl + dc * k_s; + let l_dt = dl.clone() + dc.clone() * &k_l; + let m_dt = dl.clone() + dc.clone() * &k_m; + let s_dt = dl + dc * &k_s; // If higher accuracy is required, 2 or 3 iterations of the following block can be used: { - let lightness = l0 * (T::one() - t) + t * l1; - let chroma = t * c1; + let lightness = l0 * (T::one() - &t) + t.clone() * l1; + let chroma = t.clone() * c1; - let l_ = lightness + chroma * k_l; - let m_ = lightness + chroma * k_m; + let l_ = lightness.clone() + chroma.clone() * k_l; + let m_ = lightness.clone() + chroma.clone() * k_m; let s_ = lightness + chroma * k_s; - let l = l_ * l_ * l_; - let m = m_ * m_ * m_; - let s = s_ * s_ * s_; + let l = l_.clone() * &l_ * &l_; + let m = m_.clone() * &m_ * &m_; + let s = s_.clone() * &s_ * &s_; - let ldt = T::from_f64(3.0) * l_dt * l_ * l_; - let mdt = T::from_f64(3.0) * m_dt * m_ * m_; - let sdt = T::from_f64(3.0) * s_dt * s_ * s_; + let ldt = T::from_f64(3.0) * &l_dt * &l_ * &l_; + let mdt = T::from_f64(3.0) * &m_dt * &m_ * &m_; + let sdt = T::from_f64(3.0) * &s_dt * &s_ * &s_; - let ldt2 = T::from_f64(6.0) * l_dt * l_dt * l_; - let mdt2 = T::from_f64(6.0) * m_dt * m_dt * m_; - let sdt2 = T::from_f64(6.0) * s_dt * s_dt * s_; + let ldt2 = T::from_f64(6.0) * &l_dt * l_dt * l_; + let mdt2 = T::from_f64(6.0) * &m_dt * m_dt * m_; + let sdt2 = T::from_f64(6.0) * &s_dt * s_dt * s_; - let r = T::from_f64(4.0767416621) * l - T::from_f64(3.3077115913) * m - + T::from_f64(0.2309699292) * s + let r = T::from_f64(4.0767416621) * &l - T::from_f64(3.3077115913) * &m + + T::from_f64(0.2309699292) * &s - T::one(); - let r1 = T::from_f64(4.0767416621) * ldt - T::from_f64(3.3077115913) * mdt - + T::from_f64(0.2309699292) * sdt; - let r2 = T::from_f64(4.0767416621) * ldt2 - T::from_f64(3.3077115913) * mdt2 - + T::from_f64(0.2309699292) * sdt2; + let r1 = T::from_f64(4.0767416621) * &ldt - T::from_f64(3.3077115913) * &mdt + + T::from_f64(0.2309699292) * &sdt; + let r2 = T::from_f64(4.0767416621) * &ldt2 - T::from_f64(3.3077115913) * &mdt2 + + T::from_f64(0.2309699292) * &sdt2; - let u_r = r1 / (r1 * r1 - T::from_f64(0.5) * r * r2); - let mut t_r = -r * u_r; + let u_r = r1.clone() / (r1.clone() * r1 - T::from_f64(0.5) * &r * r2); + let mut t_r = -r * &u_r; - let g = -T::from_f64(1.2684380046) * l + T::from_f64(2.6097574011) * m - - T::from_f64(0.3413193965) * s + let g = -T::from_f64(1.2684380046) * &l + T::from_f64(2.6097574011) * &m + - T::from_f64(0.3413193965) * &s - T::one(); - let g1 = -T::from_f64(1.2684380046) * ldt + T::from_f64(2.6097574011) * mdt - - T::from_f64(0.3413193965) * sdt; - let g2 = -T::from_f64(1.2684380046) * ldt2 + T::from_f64(2.6097574011) * mdt2 - - T::from_f64(0.3413193965) * sdt2; + let g1 = -T::from_f64(1.2684380046) * &ldt + T::from_f64(2.6097574011) * &mdt + - T::from_f64(0.3413193965) * &sdt; + let g2 = -T::from_f64(1.2684380046) * &ldt2 + T::from_f64(2.6097574011) * &mdt2 + - T::from_f64(0.3413193965) * &sdt2; - let u_g = g1 / (g1 * g1 - T::from_f64(0.5) * g * g2); - let mut t_g = -g * u_g; + let u_g = g1.clone() / (g1.clone() * g1 - T::from_f64(0.5) * &g * g2); + let mut t_g = -g * &u_g; let b = -T::from_f64(0.0041960863) * l - T::from_f64(0.7034186147) * m + T::from_f64(1.7076147010) * s @@ -121,14 +96,22 @@ where let b2 = -T::from_f64(0.0041960863) * ldt2 - T::from_f64(0.7034186147) * mdt2 + T::from_f64(1.7076147010) * sdt2; - let u_b = b1 / (b1 * b1 - T::from_f64(0.5) * b * b2); - let mut t_b = -b * u_b; + let u_b = b1.clone() / (b1.clone() * b1 - T::from_f64(0.5) * &b * b2); + let mut t_b = -b * &u_b; // flt_max really is a constant, but cannot be defined as one due to the T::from_f64 function let flt_max = T::from_f64(10e5); - t_r = if u_r >= T::zero() { t_r } else { flt_max }; - t_g = if u_g >= T::zero() { t_g } else { flt_max }; + t_r = if u_r >= T::zero() { + t_r + } else { + flt_max.clone() + }; + t_g = if u_g >= T::zero() { + t_g + } else { + flt_max.clone() + }; t_b = if u_b >= T::zero() { t_b } else { flt_max }; t + T::min(t_r, T::min(t_g, t_b)) @@ -149,56 +132,58 @@ where + One + Zero + Arithmetics - + Sqrt + MinMax - + Copy - + PartialOrd - + HasBoolMask - + Powi + Cbrt - + Trigonometry - + FromScalar, - T::Scalar: Real - + Zero - + One - + Recip - + IsValidDivisor - + Arithmetics + + Sqrt + + Powi + Clone - + FromScalar, + + HasBoolMask + + PartialOrd, + Oklab: IntoColorUnclamped>, { pub fn from_normalized(lightness: T, a_: T, b_: T) -> Self { - let cusp = LC::find_cusp(a_, b_); - - let max_chroma = - find_gamut_intersection(a_, b_, lightness, T::one(), lightness, Some(cusp)); + let cusp = LC::find_cusp(a_.clone(), b_.clone()); + + let max_chroma = find_gamut_intersection( + a_.clone(), + b_.clone(), + lightness.clone(), + T::one(), + lightness.clone(), + cusp.clone(), + ); let st_max = ST::from(cusp); // Scale factor to compensate for the curved part of gamut shape: - let k = max_chroma / T::min(lightness * st_max.s, (T::one() - lightness) * st_max.t); + let k = max_chroma.clone() + / T::min( + lightness.clone() * st_max.s, + (T::one() - &lightness) * st_max.t, + ); let c_mid = { let st_mid = ST::mid(a_, b_); // Use a soft minimum function, instead of a sharp triangle shape to get a smooth value for chroma. - let c_a = lightness * st_mid.s; - let c_b = (T::one() - lightness) * st_mid.t; + let c_a = lightness.clone() * st_mid.s; + let c_b = (T::one() - &lightness) * st_mid.t; T::from_f64(0.9) * k * T::sqrt(T::sqrt( T::one() - / (T::one() / (c_a * c_a * c_a * c_a) + T::one() / (c_b * c_b * c_b * c_b)), + / (T::one() / (c_a.clone() * &c_a * &c_a * &c_a) + + T::one() / (c_b.clone() * &c_b * &c_b * &c_b)), )) }; let c_0 = { // for C_0, the shape is independent of hue, so ST are constant. // Values picked to roughly be the average values of ST. - let c_a = lightness * T::from_f64(0.4); + let c_a = lightness.clone() * T::from_f64(0.4); let c_b = (T::one() - lightness) * T::from_f64(0.8); // Use a soft minimum function, instead of a sharp triangle shape to get a smooth value for chroma. - T::sqrt(T::one() / (T::one() / (c_a * c_a) + T::one() / (c_b * c_b))) + T::sqrt(T::one() / (T::one() / (c_a.clone() * c_a) + T::one() / (c_b.clone() * c_b))) }; Self { zero: c_0, @@ -240,44 +225,32 @@ pub(crate) const MAX_SRGB_SATURATION_INACCURACY: f64 = 1e-6; impl LC where - T: Real - + PartialOrd - + HasBoolMask - + MinMax - + Copy - + Powi - + Sqrt - + Cbrt - + Arithmetics - + Trigonometry - + Zero - + One - + FromScalar, - T::Scalar: Real - + Zero - + One - + Recip - + IsValidDivisor - + Arithmetics - + Clone - + FromScalar, + T: Real + One + Arithmetics + Powi + HasBoolMask + PartialOrd + Clone, { /// Returns the cusp of the geometrical shape of representable `sRGB` colors for /// normalized `a` and `b` values of an `OKlabHue`, where "normalized" means, `a² + b² == 1`. /// /// The cusp solely depends on the maximum saturation of the hue, but is expressed as a /// combination of lightness and chroma. - pub fn find_cusp(a: T, b: T) -> Self { + pub fn find_cusp(a: T, b: T) -> Self + where + T: MinMax + Cbrt, + Oklab: IntoColorUnclamped>, + { // First, find the maximum saturation (saturation S = C/L) - let max_saturation = Self::max_saturation(a, b); + let max_saturation = Self::max_saturation(a.clone(), b.clone()); // Convert to linear sRGB to find the first point where at least one of r,g or b >= 1: - let rgb_at_max: LinSrgb = - Oklab::new(T::one(), max_saturation * a, max_saturation * b).into_color_unclamped(); + let rgb_at_max: LinSrgb = Oklab::new( + T::one(), + max_saturation.clone() * a, + max_saturation.clone() * b, + ) + .into_color_unclamped(); let max_lightness = T::cbrt(T::one() / T::max(T::max(rgb_at_max.red, rgb_at_max.green), rgb_at_max.blue)); Self { - lightness: max_lightness, + lightness: max_lightness.clone(), chroma: max_lightness * max_saturation, } } @@ -297,7 +270,7 @@ where // wl, wm and ws are coefficients for https://en.wikipedia.org/wiki/LMS_color_space // -- the color space modelling human perception. let (k0, k1, k2, k3, k4, wl, wm, ws) = - if T::from_f64(-1.88170328) * a - T::from_f64(0.80936493) * b > T::one() { + if T::from_f64(-1.88170328) * &a - T::from_f64(0.80936493) * &b > T::one() { // red component at zero first ( T::from_f64(1.19086277), @@ -309,7 +282,7 @@ where T::from_f64(-3.3077115913), T::from_f64(0.2309699292), ) - } else if T::from_f64(1.81444104) * a - T::from_f64(1.19445276) * b > T::one() { + } else if T::from_f64(1.81444104) * &a - T::from_f64(1.19445276) * &b > T::one() { //green component at zero first ( T::from_f64(0.73956515), @@ -336,40 +309,41 @@ where }; // Approximate max saturation using a polynomial - let mut approx_max_saturation = k0 + k1 * a + k2 * b + k3 * a.powi(2) + k4 * a * b; + let mut approx_max_saturation = + k0 + k1 * &a + k2 * &b + k3 * a.clone().powi(2) + k4 * &a * &b; // Get closer with Halley's method - let k_l = T::from_f64(0.3963377774) * a + T::from_f64(0.2158037573) * b; - let k_m = T::from_f64(-0.1055613458) * a - T::from_f64(0.0638541728) * b; + let k_l = T::from_f64(0.3963377774) * &a + T::from_f64(0.2158037573) * &b; + let k_m = T::from_f64(-0.1055613458) * &a - T::from_f64(0.0638541728) * &b; let k_s = T::from_f64(-0.0894841775) * a - T::from_f64(1.2914855480) * b; for _i in 0..MAX_SRGB_SATURATION_SEARCH_MAX_ITER { - let l_ = T::one() + approx_max_saturation * k_l; - let m_ = T::one() + approx_max_saturation * k_m; - let s_ = T::one() + approx_max_saturation * k_s; + let l_ = T::one() + approx_max_saturation.clone() * &k_l; + let m_ = T::one() + approx_max_saturation.clone() * &k_m; + let s_ = T::one() + approx_max_saturation.clone() * &k_s; - let l = l_.powi(3); - let m = m_.powi(3); - let s = s_.powi(3); + let l = l_.clone().powi(3); + let m = m_.clone().powi(3); + let s = s_.clone().powi(3); // first derivative components - let l_ds = T::from_f64(3.0) * k_l * l_.powi(2); - let m_ds = T::from_f64(3.0) * k_m * m_.powi(2); - let s_ds = T::from_f64(3.0) * k_s * s_.powi(2); + let l_ds = T::from_f64(3.0) * &k_l * l_.clone().powi(2); + let m_ds = T::from_f64(3.0) * &k_m * m_.clone().powi(2); + let s_ds = T::from_f64(3.0) * &k_s * s_.clone().powi(2); // second derivative components - let l_ds2 = T::from_f64(6.0) * k_l.powi(2) * l_; - let m_ds2 = T::from_f64(6.0) * k_m.powi(2) * m_; - let s_ds2 = T::from_f64(6.0) * k_s.powi(2) * s_; + let l_ds2 = T::from_f64(6.0) * k_l.clone().powi(2) * l_; + let m_ds2 = T::from_f64(6.0) * k_m.clone().powi(2) * m_; + let s_ds2 = T::from_f64(6.0) * k_s.clone().powi(2) * s_; // let x be the approximate maximum saturation and // i the current iteration // f = f(x_i), f1 = f'(x_i), f2 = f''(x_i) for - let f = wl * l + wm * m + ws * s; - let f1 = wl * l_ds + wm * m_ds + ws * s_ds; - let f2 = wl * l_ds2 + wm * m_ds2 + ws * s_ds2; + let f = wl.clone() * l + wm.clone() * m + ws.clone() * s; + let f1 = wl.clone() * l_ds + wm.clone() * m_ds + ws.clone() * s_ds; + let f2 = wl.clone() * l_ds2 + wm.clone() * m_ds2 + ws.clone() * s_ds2; approx_max_saturation = - approx_max_saturation - f * f1 / (f1.powi(2) - T::from_f64(0.5) * f * f2); + approx_max_saturation - f.clone() * &f1 / (f1.powi(2) - T::from_f64(0.5) * f * f2); } approx_max_saturation } @@ -378,34 +352,26 @@ where #[cfg(test)] impl OklabHue where - T: Real - + RealAngle - + PartialOrd - + HasBoolMask - + MinMax - + Copy - + Powi - + Sqrt - + Cbrt - + Arithmetics - + Trigonometry - + Zero - + One - + FromScalar, - T::Scalar: Real - + Zero + T: RealAngle + One - + Recip - + IsValidDivisor + Arithmetics - + Clone - + FromScalar, + + Trigonometry + + MinMax + + Cbrt + + Powi + + HasBoolMask + + PartialOrd + + Clone, + Oklab: IntoColorUnclamped>, { pub(crate) fn srgb_limits(self) -> (LC, T, T) { - let normalized_hue_vector = self.ab(T::one()); - let lc = LC::find_cusp(normalized_hue_vector.0, normalized_hue_vector.1); - let a = lc.chroma.clone() * normalized_hue_vector.0.clone(); - let b = lc.chroma.clone() * normalized_hue_vector.1.clone(); + let normalized_hue_vector = self.into_cartesian(); + let lc = LC::find_cusp( + normalized_hue_vector.0.clone(), + normalized_hue_vector.1.clone(), + ); + let a = lc.chroma.clone() * normalized_hue_vector.0; + let b = lc.chroma.clone() * normalized_hue_vector.1; (lc, a, b) } } @@ -427,11 +393,11 @@ pub(crate) struct ST { impl From> for ST where - T: Arithmetics + One + Copy, + T: Arithmetics + One + Clone, { fn from(lc: LC) -> Self { ST { - s: lc.chroma / lc.lightness, + s: lc.chroma.clone() / &lc.lightness, t: lc.chroma / (T::one() - lc.lightness), } } @@ -439,7 +405,7 @@ where impl ST where - T: Real + Arithmetics + Copy + One, + T: Real + Arithmetics + One + Clone, { /// Returns a smooth approximation of the location of the cusp. /// @@ -450,24 +416,22 @@ where /// /// `T_mid < T_max` #[rustfmt::skip] - fn mid(a_: T, b_: T) -> ST - - { + fn mid(a_: T, b_: T) -> ST { let s = T::from_f64(0.11516993) + T::one() / ( T::from_f64(7.44778970) - + T::from_f64(4.15901240) * b_ - + a_ * (T::from_f64(-2.19557347)+ T::from_f64(1.75198401) * b_ - + a_ * (T::from_f64(-2.13704948) - T::from_f64(10.02301043) * b_ - + a_ * (T::from_f64(-4.24894561) + T::from_f64(5.38770819) * b_+ T::from_f64(4.69891013) * a_ + + T::from_f64(4.15901240) * &b_ + + a_.clone() * (T::from_f64(-2.19557347)+ T::from_f64(1.75198401) * &b_ + + a_.clone() * (T::from_f64(-2.13704948) - T::from_f64(10.02301043) * &b_ + + a_.clone() * (T::from_f64(-4.24894561) + T::from_f64(5.38770819) * &b_+ T::from_f64(4.69891013) * &a_ ))) ); let t = T::from_f64(0.11239642)+ T::one()/ ( - T::from_f64(1.61320320) - T::from_f64(0.68124379) * b_ - + a_ * (T::from_f64(0.40370612) - + T::from_f64(0.90148123) * b_ - + a_ * (T::from_f64(-0.27087943) + T::from_f64(0.61223990) * b_ - + a_ * (T::from_f64(0.00299215) - T::from_f64(0.45399568) * b_ - T::from_f64(0.14661872) * a_ + T::from_f64(1.61320320) - T::from_f64(0.68124379) * &b_ + + a_.clone() * (T::from_f64(0.40370612) + + T::from_f64(0.90148123) * &b_ + + a_.clone() * (T::from_f64(-0.27087943) + T::from_f64(0.61223990) * &b_ + + a_.clone() * (T::from_f64(0.00299215) - T::from_f64(0.45399568) * b_ - T::from_f64(0.14661872) * a_ ))) ); ST { s, t } @@ -485,7 +449,7 @@ where /// [D65](https://en.wikipedia.org/wiki/Illuminant_D65) reference white luminance. /// Mapping `1` to that luminance is just a matter of definition. But is say `0.8` `Oklab` /// lightness equal to `0.5` or `0.9` `sRGB` luminance? -/// +/// /// The shape and weights of `L_r` are chosen to closely matches the lightness estimate of /// the `CIELab` color space and be nearly equal at `0.5`. /// @@ -495,15 +459,15 @@ where /// https://bottosson.github.io/posts/colorpicker/#intermission---a-new-lightness-estimate-for-oklab pub(crate) fn toe(oklab_lightness: T) -> T where - T: Real + Copy + Powi + Sqrt + Arithmetics + One, + T: Real + Powi + Sqrt + Arithmetics + One + Clone, { let k_1 = T::from_f64(0.206); let k_2 = T::from_f64(0.03); - let k_3 = (T::one() + k_1) / (T::one() + k_2); + let k_3 = (T::one() + &k_1) / (T::one() + &k_2); T::from_f64(0.5) - * (k_3 * oklab_lightness - k_1 + * (k_3.clone() * &oklab_lightness - &k_1 + T::sqrt( - (k_3 * oklab_lightness - k_1).powi(2) + (k_3.clone() * &oklab_lightness - k_1).powi(2) + T::from_f64(4.0) * k_2 * k_3 * oklab_lightness, )) } @@ -513,12 +477,12 @@ where /// Inverse of [`toe`] pub(crate) fn toe_inv(l_r: T) -> T where - T: Real + Copy + Powi + Arithmetics + One, + T: Real + Powi + Arithmetics + One + Clone, { let k_1 = T::from_f64(0.206); let k_2 = T::from_f64(0.03); - let k_3 = (T::one() + k_1) / (T::one() + k_2); - (l_r.powi(2) + k_1 * l_r) / (k_3 * (l_r + k_2)) + let k_3 = (T::one() + &k_1) / (T::one() + &k_2); + (l_r.clone().powi(2) + k_1 * &l_r) / (k_3 * (l_r + k_2)) } #[cfg(test)] @@ -597,7 +561,13 @@ mod tests { max_b = f64::max(max_b, b); min_b = f64::min(min_b, b); } - let (max_chroma_a, max_chroma_b) = max_chroma.hue.ab(max_chroma.lc.chroma); + + let (normalized_a, normalized_b) = max_chroma.hue.into_cartesian(); + let (max_chroma_a, max_chroma_b) = ( + normalized_a * max_chroma.lc.chroma, + normalized_b * max_chroma.lc.chroma, + ); + println!( "Min chroma {} at hue {:?}°.", min_chroma.lc.chroma, min_chroma.hue, @@ -615,7 +585,10 @@ mod tests { fn max_saturation_f64_eq_f32() { let lin_srgb = LinSrgb::new(0.0, 0.0, 1.0); let oklab_64 = Oklab::::from_color_unclamped(lin_srgb); - let (normalized_a, normalized_b) = oklab_64.chroma_and_normalized_ab().1.unwrap(); + let (normalized_a, normalized_b) = ( + oklab_64.a / oklab_64.get_chroma(), + oklab_64.b / oklab_64.get_chroma(), + ); let saturation_64 = LC::max_saturation(normalized_a, normalized_b); let saturation_32 = LC::max_saturation(normalized_a as f32, normalized_b as f32); diff --git a/palette/src/okhsl.rs b/palette/src/okhsl.rs index 61e090e4d..e2ee01e08 100644 --- a/palette/src/okhsl.rs +++ b/palette/src/okhsl.rs @@ -1,14 +1,13 @@ pub use alpha::Okhsla; -use crate::num::{FromScalar, Hypot, Powi, Recip, Sqrt}; -use crate::ok_utils::{toe, ChromaValues}; -use crate::white_point::D65; use crate::{ - angle::{FromAngle, RealAngle}, - convert::FromColorUnclamped, - num::{Arithmetics, Cbrt, IsValidDivisor, MinMax, One, Real, Trigonometry, Zero}, + angle::FromAngle, + convert::{FromColorUnclamped, IntoColorUnclamped}, + num::{Arithmetics, Cbrt, Hypot, IsValidDivisor, MinMax, One, Powi, Real, Sqrt, Zero}, + ok_utils::{toe, ChromaValues}, stimulus::{FromStimulus, Stimulus}, - HasBoolMask, Oklab, OklabHue, + white_point::D65, + GetHue, HasBoolMask, LinSrgb, Oklab, OklabHue, }; mod alpha; @@ -36,7 +35,7 @@ mod visual_eq; )] #[repr(C)] pub struct Okhsl { - /// The hue of the color, in degrees of a circle, where for all `h`: `h+n*360 == h`. + /// The hue of the color, in degrees of a circle. /// /// For fully saturated, bright colors /// * 0° corresponds to a kind of magenta-pink (RBG #ff0188), @@ -57,7 +56,7 @@ pub struct Okhsl { /// For v == 0 the saturation is irrelevant. pub saturation: T, - /// The relative luminance of the color, where + /// The relative luminance of the color, where /// * `0.0` corresponds to pure black /// * `1.0` corresponds to white /// @@ -155,35 +154,25 @@ where + One + Zero + Arithmetics + + Powi + Sqrt + + Hypot + MinMax - + Copy - + PartialOrd - + HasBoolMask - + Powi + Cbrt - + Hypot - + Trigonometry - + RealAngle - + FromScalar, - T::Scalar: Real - + Zero - + One - + Recip + IsValidDivisor - + Arithmetics - + Clone - + FromScalar, + + HasBoolMask + + PartialOrd + + Clone, + Oklab: GetHue> + IntoColorUnclamped>, { fn from_color_unclamped(lab: Oklab) -> Self { // refer to the SRGB reference-white-based lightness L_r as l for consistency with HSL - let l = toe(lab.l); + let l = toe(lab.l.clone()); - if let Some(h) = lab.try_hue() { - let (chroma, normalized_ab) = lab.chroma_and_normalized_ab(); - let (a_, b_) = - normalized_ab.expect("There is a hue, thus there also are normalized a and b"); - let cs = ChromaValues::from_normalized(lab.l, a_, b_); + let chroma = lab.get_chroma(); + let hue = lab.get_hue(); + if chroma.is_valid_divisor() { + let cs = ChromaValues::from_normalized(lab.l, lab.a / &chroma, lab.b / &chroma); // Inverse of the interpolation in okhsl_to_srgb: @@ -191,21 +180,21 @@ where let mid_inv = T::from_f64(1.25); let s = if chroma < cs.mid { - let k_1 = mid * cs.zero; - let k_2 = T::one() - k_1 / cs.mid; + let k_1 = mid.clone() * cs.zero; + let k_2 = T::one() - k_1.clone() / cs.mid; - let t = chroma / (k_1 + k_2 * chroma); + let t = chroma.clone() / (k_1 + k_2 * chroma); t * mid } else { - let k_0 = cs.mid; - let k_1 = (T::one() - mid) * (cs.mid * mid_inv).powi(2) / cs.zero; - let k_2 = T::one() - (k_1) / (cs.max - cs.mid); + let k_0 = cs.mid.clone(); + let k_1 = (T::one() - &mid) * (cs.mid.clone() * mid_inv).powi(2) / cs.zero; + let k_2 = T::one() - k_1.clone() / (cs.max - cs.mid); - let t = (chroma - k_0) / (k_1 + k_2 * (chroma - k_0)); - mid + (T::one() - mid) * t + let t = (chroma.clone() - &k_0) / (k_1 + k_2 * (chroma - k_0)); + mid.clone() + (T::one() - mid) * t }; - Self::new(h, s, l) + Self::new(hue, s, l) } else { // `a` describes how green/red the color is, `b` how blue/yellow the color is // both are zero -> the color is totally desaturated. @@ -250,6 +239,8 @@ mod tests { use crate::visual::{VisualColor, VisuallyEqual}; use crate::{encoding, LinSrgb, Okhsl, Oklab, Srgb}; + test_convert_into_from_xyz!(Okhsl); + #[test] fn test_roundtrip_okhsl_oklab_is_original() { let colors = [ diff --git a/palette/src/okhsv.rs b/palette/src/okhsv.rs index 75848e5e8..8194d90aa 100644 --- a/palette/src/okhsv.rs +++ b/palette/src/okhsv.rs @@ -4,18 +4,17 @@ pub use alpha::Okhsva; #[cfg(feature = "random")] pub use random::UniformOkhsv; -use crate::angle::FromAngle; -use crate::convert::IntoColorUnclamped; -use crate::num::{ - Arithmetics, Cbrt, FromScalar, Hypot, IsValidDivisor, MinMax, One, Powi, Real, Recip, Sqrt, - Trigonometry, Zero, -}; -use crate::ok_utils::{LC, ST}; -use crate::stimulus::{FromStimulus, Stimulus}; -use crate::white_point::D65; use crate::{ - angle::RealAngle, convert::FromColorUnclamped, ok_utils, HasBoolMask, LinSrgb, Okhwb, Oklab, - OklabHue, + angle::FromAngle, + bool_mask::LazySelect, + convert::{FromColorUnclamped, IntoColorUnclamped}, + num::{ + Arithmetics, Cbrt, Hypot, IsValidDivisor, MinMax, One, Powi, Real, Sqrt, Trigonometry, Zero, + }, + ok_utils::{self, LC, ST}, + stimulus::{FromStimulus, Stimulus}, + white_point::D65, + GetHue, HasBoolMask, LinSrgb, Okhwb, Oklab, OklabHue, }; mod alpha; @@ -30,8 +29,8 @@ mod visual_eq; /// /// Allows /// * changing lightness/chroma/saturation while keeping perceived Hue constant -/// (like HSV promises but delivers only partially) -/// * finding the strongest color (maximum chroma) at s == 1 (like HSV) +/// (like HSV promises but delivers only partially) +/// * finding the strongest color (maximum chroma) at s == 1 (like HSV) #[derive(Debug, Copy, Clone, ArrayCast, FromColorUnclamped, WithAlpha)] #[cfg_attr(feature = "serializing", derive(Serialize, Deserialize))] #[palette( @@ -42,7 +41,7 @@ mod visual_eq; )] #[repr(C)] pub struct Okhsv { - /// The hue of the color, in degrees of a circle, where for all `h`: `h+n*360 == h`. + /// The hue of the color, in degrees of a circle. /// /// For fully saturated, bright colors /// * 0° corresponds to a kind of magenta-pink (RBG #ff0188), @@ -179,10 +178,8 @@ impl Okhsv { impl FromColorUnclamped> for Okhsv where T: Real - + PartialOrd - + HasBoolMask + MinMax - + Copy + + Clone + Powi + Sqrt + Cbrt @@ -191,17 +188,10 @@ where + Zero + Hypot + One - + FromScalar - + RealAngle, - T::Scalar: Real - + Zero - + One - + Recip - + Hypot + IsValidDivisor - + Arithmetics - + Clone - + FromScalar, + + HasBoolMask + + PartialOrd, + Oklab: GetHue> + IntoColorUnclamped>, { fn from_color_unclamped(lab: Oklab) -> Self { if lab.l == T::zero() { @@ -209,10 +199,10 @@ where return Self::new(T::zero(), T::zero(), T::zero()); } - if let Some(hue) = lab.try_hue() { - let (chroma, normalized_ab) = lab.chroma_and_normalized_ab(); - let (a_, b_) = - normalized_ab.expect("There is a hue, thus there also are normalized a and b"); + let chroma = lab.get_chroma(); + let hue = lab.get_hue(); + if chroma.is_valid_divisor() { + let (a_, b_) = (lab.a / &chroma, lab.b / &chroma); // For each hue the sRGB gamut can be drawn on a 2-dimensional space. // Let L_r, the lightness in relation to the possible luminance of sRGB, be spread @@ -222,23 +212,23 @@ where // To use saturation and brightness values, the gamut must be mapped to a square. // The lower point of the triangle is expanded to the lower side of the square. // The left side remains unchanged and the cusp of the triangle moves to the upper right. - let cusp = LC::find_cusp(a_, b_); + let cusp = LC::find_cusp(a_.clone(), b_.clone()); let st_max = ST::::from(cusp); let s_0 = T::from_f64(0.5); - let k = T::one() - s_0 / st_max.s; + let k = T::one() - s_0.clone() / st_max.s; // first we find L_v, C_v, L_vt and C_vt - let t = st_max.t / (chroma + lab.l * st_max.t); - let l_v = t * lab.l; + let t = st_max.t.clone() / (chroma.clone() + lab.l.clone() * &st_max.t); + let l_v = t.clone() * &lab.l; let c_v = t * chroma; - let l_vt = ok_utils::toe_inv(l_v); - let c_vt = c_v * l_vt / l_v; + let l_vt = ok_utils::toe_inv(l_v.clone()); + let c_vt = c_v.clone() * &l_vt / &l_v; // we can then use these to invert the step that compensates for the toe and the curved top part of the triangle: let rgb_scale: LinSrgb = - Oklab::new(l_vt, a_ * c_vt, b_ * c_vt).into_color_unclamped(); + Oklab::new(l_vt, a_ * &c_vt, b_ * c_vt).into_color_unclamped(); let lightness_scale_factor = T::cbrt( T::one() / T::max( @@ -255,7 +245,8 @@ where // we can now compute v and s: let v = l_r / l_v; - let s = (s_0 + st_max.t) * c_v / ((st_max.t * s_0) + st_max.t * k * c_v); + let s = + (s_0.clone() + &st_max.t) * &c_v / ((st_max.t.clone() * s_0) + st_max.t * k * c_v); Self::new(hue, s, v) } else { @@ -267,38 +258,29 @@ where } impl FromColorUnclamped> for Okhsv where - T: Real - + PartialOrd - + Copy - + Powi - + Sqrt - + Cbrt - + Arithmetics - + Trigonometry - + Zero - + Hypot - + One - + FromScalar - + RealAngle, - T::Scalar: Real - + Zero - + One - + Recip - + Hypot - + IsValidDivisor - + Arithmetics - + Clone - + FromScalar, + T: One + Zero + IsValidDivisor + Arithmetics, + T::Mask: LazySelect, { fn from_color_unclamped(hwb: Okhwb) -> Self { - if hwb.blackness == T::one() { - return Self::new(hwb.hue, T::zero(), T::zero()); + let Okhwb { + hue, + whiteness, + blackness, + } = hwb; + + let value = T::one() - blackness; + + // avoid divide by zero + let saturation = lazy_select! { + if value.is_valid_divisor() => T::one() - (whiteness / &value), + else => T::zero(), + }; + + Self { + hue, + saturation, + value, } - Self::new( - hwb.hue, - T::one() - hwb.whiteness / (T::one() - hwb.blackness), - T::one() - hwb.blackness, - ) } } @@ -311,6 +293,8 @@ mod tests { use crate::visual::VisuallyEqual; use crate::{encoding, Clamp, IsWithinBounds, LinSrgb, Okhsv, Oklab, OklabHue, Srgb}; + test_convert_into_from_xyz!(Okhsv); + #[test] fn test_roundtrip_okhsv_oklab_is_original() { let colors = [ diff --git a/palette/src/okhwb.rs b/palette/src/okhwb.rs index 29e1d0d5a..31e4e1f29 100644 --- a/palette/src/okhwb.rs +++ b/palette/src/okhwb.rs @@ -2,15 +2,13 @@ use core::fmt::Debug; pub use alpha::Okhwba; -use crate::angle::{FromAngle, RealAngle}; -use crate::num::{FromScalar, Hypot, Recip, Sqrt}; -use crate::stimulus::{FromStimulus, Stimulus}; -use crate::white_point::D65; -use crate::HasBoolMask; use crate::{ + angle::FromAngle, convert::FromColorUnclamped, - num::{Arithmetics, Cbrt, IsValidDivisor, One, Real, Trigonometry, Zero}, - Okhsv, OklabHue, + num::{Arithmetics, One}, + stimulus::{FromStimulus, Stimulus}, + white_point::D65, + HasBoolMask, Okhsv, OklabHue, }; mod alpha; @@ -21,9 +19,8 @@ mod random; #[cfg(feature = "approx")] mod visual_eq; -/// A Hue/Whiteness/Blackness representation of [`Oklab`] in the `sRGB` color space. -/// # See -/// https://bottosson.github.io/posts/colorpicker/#okhwb +/// A Hue/Whiteness/Blackness representation of [`Oklab`][crate::Oklab] in the +/// `sRGB` color space, similar to [`Hwb`][crate::Okhwb]. #[derive(Debug, Copy, Clone, ArrayCast, FromColorUnclamped, WithAlpha)] #[cfg_attr(feature = "serializing", derive(Serialize, Deserialize))] #[palette( @@ -34,7 +31,7 @@ mod visual_eq; )] #[repr(C)] pub struct Okhwb { - /// The hue of the color, in degrees of a circle, where for all `h`: `h+n*360 == h`. + /// The hue of the color, in degrees of a circle. /// /// For fully saturated, bright colors /// * 0° corresponds to a kind of magenta-pink (RBG #ff0188), @@ -124,40 +121,16 @@ where impl FromColorUnclamped> for Okhwb where - T: Real - + Copy - + Sqrt - + Cbrt - + Arithmetics - + Trigonometry - + Zero - + Hypot - + One - + FromScalar - + RealAngle, - T::Scalar: Real - + Zero - + One - + Recip - + Hypot - + IsValidDivisor - + Arithmetics - + Clone - + FromScalar, + T: One + Arithmetics, { /// Converts `lab` to `Okhwb` in the bounds of sRGB. - /// - /// # See - /// https://bottosson.github.io/posts/colorpicker/#okhwb - /// See [`srgb_to_okhwb`](https://bottosson.github.io/posts/colorpicker/#okhwb-2). - /// This implementation differs from srgb_to_okhwb in that it starts with the `lab` - /// value and produces hues in degrees, whereas `srgb_to_okhwb` produces degree/360. fn from_color_unclamped(hsv: Okhsv) -> Self { - Self::new( - hsv.hue, - (T::one() - hsv.saturation) * hsv.value, - T::one() - hsv.value, - ) + // See . + Self { + hue: hsv.hue, + whiteness: (T::one() - hsv.saturation) * &hsv.value, + blackness: T::one() - hsv.value, + } } } @@ -195,6 +168,8 @@ mod tests { use crate::visual::VisuallyEqual; use crate::{encoding, LinSrgb, Okhsv, Okhwb, Oklab}; + test_convert_into_from_xyz!(Okhwb); + #[test] fn test_roundtrip_okhwb_oklab_is_original() { let colors = [ diff --git a/palette/src/oklab.rs b/palette/src/oklab.rs index 1ac817ed9..c033edf54 100644 --- a/palette/src/oklab.rs +++ b/palette/src/oklab.rs @@ -1,22 +1,18 @@ -use core::any::TypeId; -use core::fmt::Debug; -use core::ops::Mul; +use core::{any::TypeId, fmt::Debug, ops::Mul}; pub use alpha::Oklaba; -use crate::convert::IntoColorUnclamped; -use crate::encoding::{IntoLinear, Srgb}; -use crate::num::{FromScalar, Hypot, Powi, Recip, Sqrt}; -use crate::ok_utils::{toe_inv, ChromaValues, LC, ST}; -use crate::rgb::{Primaries, Rgb, RgbSpace, RgbStandard}; use crate::{ angle::RealAngle, bool_mask::HasBoolMask, - convert::FromColorUnclamped, + convert::{FromColorUnclamped, IntoColorUnclamped}, + encoding::{IntoLinear, Srgb}, matrix::multiply_xyz, - num::{Arithmetics, Cbrt, IsValidDivisor, MinMax, One, Real, Trigonometry, Zero}, + num::{Arithmetics, Cbrt, Hypot, MinMax, One, Powi, Real, Sqrt, Trigonometry, Zero}, + ok_utils::{toe_inv, ChromaValues, LC, ST}, + rgb::{Rgb, RgbSpace, RgbStandard}, white_point::D65, - LinSrgb, Mat3, Okhsl, Okhsv, OklabHue, Oklch, Xyz, + LinSrgb, Mat3, Okhsl, Okhsv, Oklch, Xyz, }; mod alpha; @@ -77,66 +73,75 @@ pub(crate) fn m2_inv() -> Mat3 { /// The [Oklab color space](https://bottosson.github.io/posts/oklab/). /// /// # Characteristics -/// `Oklab` is a *perceptual* color space. It does not relate to an output device -/// (a monitor or printer) but instead relates to the -/// [CIE standard observer](https://en.wikipedia.org/wiki/CIE_1931_color_space#CIE_standard_observer) +/// `Oklab` is a *perceptual* color space. It does not relate to an output +/// device (a monitor or printer) but instead relates to the [CIE standard +/// observer](https://en.wikipedia.org/wiki/CIE_1931_color_space#CIE_standard_observer) /// -- an averaging of the results of color matching experiments under /// laboratory conditions. /// -/// `Oklab` is a uniform color space -/// ([Compare to the HSV color space](https://bottosson.github.io/posts/oklab/#comparing-oklab-to-hsv)). -/// It is useful for things like: +/// `Oklab` is a uniform color space ([Compare to the HSV color +/// space](https://bottosson.github.io/posts/oklab/#comparing-oklab-to-hsv)). It +/// is useful for things like: /// * Turning an image grayscale, while keeping the perceived lightness the same -/// * Increasing the saturation of colors, while maintaining perceived hue and lightness +/// * Increasing the saturation of colors, while maintaining perceived hue and +/// lightness /// * Creating smooth and uniform looking transitions between colors /// -/// `Oklab`'s structure is similar to [L\*a\*b\*](crate::Lab). It is based on the -/// [opponent color model of human vision](https://en.wikipedia.org/wiki/Opponent_process), -/// where red and green form an opponent pair, and blue and yellow form an opponent pair. +/// `Oklab`'s structure is similar to [L\*a\*b\*](crate::Lab). It is based on +/// the [opponent color model of human +/// vision](https://en.wikipedia.org/wiki/Opponent_process), where red and green +/// form an opponent pair, and blue and yellow form an opponent pair. /// /// `Oklab` uses [D65](https://en.wikipedia.org/wiki/Illuminant_D65)'s -/// whitepoint -- daylight illumination, which is also used by sRGB, rec2020 and Display P3 color -/// spaces -- and assumes normal well-lit viewing conditions, to which the eye is adapted. -/// Thus `Oklab`s lightness `l` technically is a measure of relative brightness -- a subjective -/// measure -- not relative luminance. The lightness is scale/exposure-independend, i.e. -/// independent of the actual luminance of the color, as displayed by some medium, and -/// even for blindingly bright colors or very bright or dark viewing conditions assumes, that the -/// eye is adapted to the color's luminance and the hue and chroma are perceived linearly. +/// whitepoint -- daylight illumination, which is also used by sRGB, rec2020 and +/// Display P3 color spaces -- and assumes normal well-lit viewing conditions, +/// to which the eye is adapted. Thus `Oklab`s lightness `l` technically is a +/// measure of relative brightness -- a subjective measure -- not relative +/// luminance. The lightness is scale/exposure-independend, i.e. independent of +/// the actual luminance of the color, as displayed by some medium, and even for +/// blindingly bright colors or very bright or dark viewing conditions assumes, +/// that the eye is adapted to the color's luminance and the hue and chroma are +/// perceived linearly. /// /// -/// `Oklab`'s chroma is unlimited. Thus it can represent colors of any color space (including HDR). -/// `l` is in the range `0.0 .. 1.0` and `a` and `b` are unbounded. +/// `Oklab`'s chroma is unlimited. Thus it can represent colors of any color +/// space (including HDR). `l` is in the range `0.0 .. 1.0` and `a` and `b` are +/// unbounded. /// /// # Conversions /// [`Oklch`] is a cylindrical form of `Oklab`. /// -/// `Oklab` colors converted from valid (i.e. clamped) `sRGB` will be in the `sRGB` gamut. +/// `Oklab` colors converted from valid (i.e. clamped) `sRGB` will be in the +/// `sRGB` gamut. /// -/// [`Okhsv`], [`Okhwb`] and [`Okhsl`] reference the `sRGB` gamut. +/// [`Okhsv`], [`Okhwb`][crate::Okhsv] and [`Okhsl`] reference the `sRGB` gamut. /// The transformation from `Oklab` to one of them is based on the assumption, /// that the transformed `Oklab` value is within `sRGB`. /// /// `Okhsv`, `Okhwb` and `Okhsl` are not applicable to HDR, which also come with -/// color spaces with wider gamuts. They require -/// [additional research](https://bottosson.github.io/posts/colorpicker/#ideas-for-future-work). +/// color spaces with wider gamuts. They require [additional +/// research](https://bottosson.github.io/posts/colorpicker/#ideas-for-future-work). /// -/// When a `Oklab` color is converted from [`Srgb`](crate::rgb::Srgb) or a equivalent color space, -/// e.g. [`Hsv`], [`Okhsv`], [`Hsl`], [`Okhsl`], [`Hwb`], [`Okhwb`], it's lightness will be -/// relative to the (user controlled) maximum contrast and luminance of the display device, to -/// which the eye is assumed to be adapted. +/// When a `Oklab` color is converted from [`Srgb`](crate::rgb::Srgb) or a +/// equivalent color space, e.g. [`Hsv`][crate::Hsv], [`Okhsv`], +/// [`Hsl`][crate::Hsl], [`Okhsl`], [`Hwb`][crate::Hwb], +/// [`Okhwb`][crate::Okhwb], it's lightness will be relative to the (user +/// controlled) maximum contrast and luminance of the display device, to which +/// the eye is assumed to be adapted. /// /// # Clamping -/// [`Clamp`ing](Clamp) will only clamp `l`. Clamping does not guarantee the color to be inside -/// the perceptible or any display-dependent color space (like *sRGB*). +/// [`Clamp`][crate::Clamp]ing will only clamp `l`. Clamping does not guarantee +/// the color to be inside the perceptible or any display-dependent color space +/// (like *sRGB*). /// -/// To ensure a color is within the *sRGB* gamut, first convert it to `Okhsv`, clamp it there -/// and convert it back to `Oklab`. +/// To ensure a color is within the *sRGB* gamut, it can first be converted to +/// `Okhsv`, clamped there and converted it back to `Oklab`. /// /// ``` -/// // display P3 yellow according to https://colorjs.io/apps/convert/?color=color(display-p3%201%201%200)&precision=17 /// # use approx::assert_abs_diff_eq; /// # use palette::{convert::FromColorUnclamped,IsWithinBounds, LinSrgb, Okhsv, Oklab}; /// # use palette::Clamp; +/// // Display P3 yellow according to https://colorjs.io/apps/convert/?color=color(display-p3%201%201%200)&precision=17 /// let oklab = Oklab::from_color_unclamped(LinSrgb::new(1.0, 1.0, -0.098273600140966)); /// let okhsv: Okhsv = Okhsv::from_color_unclamped(oklab); /// assert!(!okhsv.is_within_bounds()); @@ -147,17 +152,18 @@ pub(crate) fn m2_inv() -> Mat3 { /// assert_abs_diff_eq!(expected, linsrgb, epsilon = 0.02); /// ``` /// Since the conversion contains a gamut mapping, it will map the color to one -/// of the perceptually closest locations in the `sRGB` gamut. Gamut mapping -- unlike clamping -- -/// is an expensive operation. To get computationally cheaper (and perceptually much worse) results, -/// convert directly to [`Srgb`] and clamp there. +/// of the perceptually closest locations in the `sRGB` gamut. Gamut mapping -- +/// unlike clamping -- is an expensive operation. To get computationally cheaper +/// (and perceptually much worse) results, convert directly to [`Srgb`] and +/// clamp there. /// /// # Lightening / Darkening -/// [`Lighten`ing](crate::Lighten) and [`Darken`ing](crate::Darken) will change `l`, as expected. However, -/// either operation may leave an implicit color space (the percetible or a display -/// dependent color space like *sRGB*). +/// [`Lighten`](crate::Lighten)ing and [`Darken`](crate::Darken)ing will change +/// `l`, as expected. However, either operation may leave an implicit color +/// space (the percetible or a display dependent color space like *sRGB*). /// -/// To ensure a color is within the *sRGB* gamut, first convert it to `Okhsl`, lighten/darken -/// it there and convert it back to `Oklab`. +/// To ensure a color is within the *sRGB* gamut, first convert it to `Okhsl`, +/// lighten/darken it there and convert it back to `Oklab`. #[derive(Debug, Copy, Clone, ArrayCast, FromColorUnclamped, WithAlpha)] #[cfg_attr(feature = "serializing", derive(Serialize, Deserialize))] @@ -172,13 +178,10 @@ pub struct Oklab { /// `l` is the lightness of the color. `0` gives absolute black and `1` gives the /// full white point luminance of the display medium. /// - /// [D65 (normalized with Y=1, i.e. white according to the adaption of the eye) transforms to + /// [`D65` (normalized with Y=1, i.e. white according to the adaption of the + /// eye) transforms to /// L=1,a=0,b=0](https://bottosson.github.io/posts/oklab/#how-oklab-was-derived). - /// However intermediate values differ from those of CIELab non-linearly. For visually - /// similar values, transform using [`toe`](crate::ok_utils::toe) and - /// [`toe_inv`](crate::ok_utils::toe_inv). These transformations are also use when - /// converting to [`Okhsv`], [`Okhsl`] and [`Okhwb`]. - /// + /// However intermediate values differ from those of CIELab non-linearly. pub l: T, /// `a` changes the hue from reddish to greenish, when moving from positive @@ -230,40 +233,15 @@ where } } -impl Oklab -where - T: RealAngle + Zero + Arithmetics + Trigonometry + Clone + PartialEq, -{ - /// Returns the hue, if at least one of `a` and `b` are non-zero - pub fn try_hue(&self) -> Option> { - OklabHue::from_ab(self.a.clone(), self.b.clone()) - } -} - impl Oklab where T: Hypot + Clone, { - /// Returns the chroma - pub fn chroma(&self) -> T { + /// Returns the chroma. + pub(crate) fn get_chroma(&self) -> T { T::hypot(self.a.clone(), self.b.clone()) } } -impl Oklab -where - T: Arithmetics + Zero + Hypot + Clone + PartialEq, -{ - /// Returns the `chroma` -- the euclidean norm of `a` and `b`. - /// If `chroma > 0.0` returns normalized versions of `a` and `b` - pub fn chroma_and_normalized_ab(&self) -> (T, Option<(T, T)>) { - let chroma = self.chroma(); - ( - chroma.clone(), - (chroma != T::zero()) - .then(|| (self.a.clone() / chroma.clone(), self.b.clone() / chroma)), - ) - } -} impl FromColorUnclamped> for Oklab { fn from_color_unclamped(color: Self) -> Self { @@ -345,19 +323,11 @@ where impl FromColorUnclamped> for Oklab where - T: Real + Cbrt + Arithmetics + FromScalar + Copy, - T::Scalar: Recip - + IsValidDivisor - + Arithmetics - + FromScalar - + Real - + Zero - + One - + Clone, + T: Real + Cbrt + Arithmetics + Copy, S: RgbStandard, S::TransferFn: IntoLinear, S::Space: RgbSpace + 'static, - ::Primaries: Primaries, + Xyz: FromColorUnclamped>, { fn from_color_unclamped(rgb: Rgb) -> Self { if TypeId::of::<::Space>() == TypeId::of::() { @@ -377,7 +347,7 @@ where T: RealAngle + Zero + MinMax + Trigonometry + Mul + Clone, { fn from_color_unclamped(color: Oklch) -> Self { - let (sin_hue, cos_hue) = color.hue.into_raw_radians().sin_cos(); + let (sin_hue, cos_hue) = color.hue.into_cartesian(); let chroma = color.chroma.max(T::zero()); Oklab { @@ -392,28 +362,19 @@ where /// See [`okhsl_to_srgb`](https://bottosson.github.io/posts/colorpicker/#hsl-2) impl FromColorUnclamped> for Oklab where - T: Real + T: RealAngle + One + Zero + Arithmetics + Sqrt + MinMax - + Copy + PartialOrd + HasBoolMask + Powi + Cbrt + Trigonometry - + FromScalar - + RealAngle, - T::Scalar: Real - + Zero - + One - + Recip - + IsValidDivisor - + Arithmetics - + Clone - + FromScalar, + + Clone, + Oklab: IntoColorUnclamped>, { fn from_color_unclamped(hsl: Okhsl) -> Self { let h = hsl.hue; @@ -426,10 +387,10 @@ where return Oklab::new(T::zero(), T::zero(), T::zero()); } - let (a_, b_) = h.ab(T::one()); + let (a_, b_) = h.into_cartesian(); let oklab_lightness = toe_inv(l); - let cs = ChromaValues::from_normalized(oklab_lightness, a_, b_); + let cs = ChromaValues::from_normalized(oklab_lightness.clone(), a_.clone(), b_.clone()); // Interpolate the three values for C so that: // At s=0: dC/ds = cs.zero, C = 0 @@ -443,48 +404,37 @@ where let t = mid_inv * s; let k_1 = mid * cs.zero; - let k_2 = T::one() - k_1 / cs.mid; + let k_2 = T::one() - k_1.clone() / cs.mid; - t * k_1 / (T::one() - k_2 * t) + t.clone() * k_1 / (T::one() - k_2 * t) } else { - let t = (s - mid) / (T::one() - mid); + let t = (s - &mid) / (T::one() - &mid); - let k_0 = cs.mid; - let k_1 = (T::one() - mid) * cs.mid * cs.mid * mid_inv * mid_inv / cs.zero; - let k_2 = T::one() - (k_1) / (cs.max - cs.mid); + let k_0 = cs.mid.clone(); + let k_1 = (T::one() - mid) * &cs.mid * &cs.mid * &mid_inv * mid_inv / cs.zero; + let k_2 = T::one() - k_1.clone() / (cs.max - cs.mid); - k_0 + t * k_1 / (T::one() - k_2 * t) + k_0 + t.clone() * k_1 / (T::one() - k_2 * t) }; - Oklab::new(oklab_lightness, chroma * a_, chroma * b_) + Oklab::new(oklab_lightness, chroma.clone() * a_, chroma * b_) } } impl FromColorUnclamped> for Oklab where - T: Real + T: RealAngle + PartialOrd + HasBoolMask + MinMax + Powi + Arithmetics - + Copy + + Clone + One + Zero - + Sqrt + Cbrt - + Trigonometry - + RealAngle - + FromScalar, - T::Scalar: Real - + PartialOrd - + Zero - + One - + Recip - + IsValidDivisor - + Arithmetics - + Clone - + FromScalar, + + Trigonometry, + Oklab: IntoColorUnclamped>, { fn from_color_unclamped(hsv: Okhsv) -> Self { if hsv.saturation == T::zero() { @@ -506,30 +456,34 @@ where } let h_radians = hsv.hue.into_raw_radians(); - let a_ = T::cos(h_radians); + let a_ = T::cos(h_radians.clone()); let b_ = T::sin(h_radians); - let cusp = LC::find_cusp(a_, b_); + let cusp = LC::find_cusp(a_.clone(), b_.clone()); let cusp: ST = cusp.into(); let s_0 = T::from_f64(0.5); - let k = T::one() - s_0 / cusp.s; + let k = T::one() - s_0.clone() / cusp.s; // first we compute L and V as if the gamut is a perfect triangle // L, C, when v == 1: - let l_v = T::one() - hsv.saturation * s_0 / (s_0 + cusp.t - cusp.t * k * hsv.saturation); - let c_v = hsv.saturation * cusp.t * s_0 / (s_0 + cusp.t - cusp.t * k * hsv.saturation); + let l_v = T::one() + - hsv.saturation.clone() * s_0.clone() + / (s_0.clone() + &cusp.t - cusp.t.clone() * &k * &hsv.saturation); + let c_v = + hsv.saturation.clone() * &cusp.t * &s_0 / (s_0 + &cusp.t - cusp.t * k * hsv.saturation); // then we compensate for both toe and the curved top part of the triangle: - let l_vt = toe_inv(l_v); - let c_vt = c_v * l_vt / l_v; + let l_vt = toe_inv(l_v.clone()); + let c_vt = c_v.clone() * &l_vt / &l_v; - let mut lightness = hsv.value * l_v; + let mut lightness = hsv.value.clone() * l_v; let mut chroma = hsv.value * c_v; - let lightness_new = toe_inv(lightness); - chroma = chroma * lightness_new / lightness; + let lightness_new = toe_inv(lightness.clone()); + chroma = chroma * &lightness_new / lightness; // the values may be outside the normal range - let rgb_scale: LinSrgb = Oklab::new(l_vt, a_ * c_vt, b_ * c_vt).into_color_unclamped(); + let rgb_scale: LinSrgb = + Oklab::new(l_vt, a_.clone() * &c_vt, b_.clone() * c_vt).into_color_unclamped(); let lightness_scale_factor = T::cbrt( T::one() / T::max( @@ -538,10 +492,10 @@ where ), ); - lightness = lightness_new * lightness_scale_factor; + lightness = lightness_new * &lightness_scale_factor; chroma = chroma * lightness_scale_factor; - Oklab::new(lightness, chroma * a_, chroma * b_) + Oklab::new(lightness, chroma.clone() * a_, chroma * b_) } } @@ -589,6 +543,8 @@ mod test { use super::*; + test_convert_into_from_xyz!(Oklab); + /// Asserts that, for any color space, the lightness of pure white is converted to `l == 1.0` #[test] fn lightness_of_white_is_one() { diff --git a/palette/src/oklab/properties.rs b/palette/src/oklab/properties.rs index 0338e67f1..68e7f37ec 100644 --- a/palette/src/oklab/properties.rs +++ b/palette/src/oklab/properties.rs @@ -1,6 +1,6 @@ use core::ops::BitOr; -use core::ops::{Add, AddAssign, BitAnd, Div, DivAssign, Mul, MulAssign, Sub, SubAssign}; +use core::ops::{Add, AddAssign, BitAnd, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign}; #[cfg(feature = "approx")] use approx::{AbsDiffEq, RelativeEq, UlpsEq}; @@ -61,12 +61,12 @@ impl_premultiply!(Oklab { l, a, b }); impl GetHue for Oklab where - T: RealAngle + Zero + Arithmetics + Trigonometry + Clone + Default + PartialEq, + T: RealAngle + Trigonometry + Add + Neg + Clone, { type Hue = OklabHue; fn get_hue(&self) -> OklabHue { - self.try_hue().unwrap_or_default() + OklabHue::from_cartesian(self.a.clone(), self.b.clone()) } } diff --git a/palette/src/oklch.rs b/palette/src/oklch.rs index fee553ef2..b1c700334 100644 --- a/palette/src/oklch.rs +++ b/palette/src/oklch.rs @@ -5,7 +5,7 @@ use crate::{ convert::FromColorUnclamped, num::{Hypot, One, Zero}, white_point::D65, - GetHue, Oklab, OklabHue, Xyz, + GetHue, Oklab, OklabHue, }; mod alpha; @@ -29,7 +29,7 @@ mod random; palette_internal, white_point = "D65", component = "T", - skip_derives(Oklab, Oklch, Xyz) + skip_derives(Oklab, Oklch) )] #[repr(C)] pub struct Oklch { @@ -101,17 +101,6 @@ impl FromColorUnclamped> for Oklch { } } -impl FromColorUnclamped> for Oklch -where - Oklab: FromColorUnclamped>, - Self: FromColorUnclamped>, -{ - fn from_color_unclamped(color: Xyz) -> Self { - let lab = Oklab::::from_color_unclamped(color); - Self::from_color_unclamped(lab) - } -} - impl FromColorUnclamped> for Oklch where T: Hypot + Clone, @@ -119,7 +108,7 @@ where { fn from_color_unclamped(color: Oklab) -> Self { let hue = color.get_hue(); - let chroma = color.chroma(); + let chroma = color.get_chroma(); Oklch::new(color.l, chroma, hue) } } @@ -163,6 +152,8 @@ unsafe impl bytemuck::Pod for Oklch where T: bytemuck::Pod {} mod test { use crate::Oklch; + test_convert_into_from_xyz!(Oklch); + #[test] fn ranges() { // chroma: 0.0 => infinity diff --git a/palette/src/rgb/rgb.rs b/palette/src/rgb/rgb.rs index 776e6c5c2..98a0b5972 100644 --- a/palette/src/rgb/rgb.rs +++ b/palette/src/rgb/rgb.rs @@ -19,9 +19,6 @@ use rand::{ Rng, }; -use crate::num::Powi; -use crate::oklab::oklab_to_linear_srgb; -use crate::white_point::D65; use crate::{ alpha::Alpha, angle::{RealAngle, UnsignedAngle}, @@ -37,9 +34,10 @@ use crate::{ self, Abs, Arithmetics, FromScalar, FromScalarArray, IntoScalarArray, IsValidDivisor, MinMax, One, PartialCmp, Real, Recip, Round, Trigonometry, Zero, }, + oklab::oklab_to_linear_srgb, rgb::{RgbSpace, RgbStandard}, stimulus::{FromStimulus, Stimulus, StimulusColor}, - white_point::{Any, WhitePoint}, + white_point::{Any, WhitePoint, D65}, Clamp, ClampAssign, FromColor, GetHue, Hsl, Hsv, IsWithinBounds, Lighten, LightenAssign, Luma, Mix, MixAssign, Oklab, RelativeContrast, RgbHue, Xyz, Yxy, }; @@ -744,20 +742,12 @@ where impl FromColorUnclamped> for Rgb where - T: Real + Powi + Arithmetics + FromScalar + Copy, - T::Scalar: Recip - + IsValidDivisor - + Arithmetics - + FromScalar - + Real - + Zero - + One - + Clone, - S: RgbStandard + 'static, + T: Real + Arithmetics + Copy, + S: RgbStandard, S::TransferFn: FromLinear, - S::Space: RgbSpace, - ::Primaries: Primaries, - Yxy: IntoColorUnclamped>, + S::Space: RgbSpace + 'static, + Rgb, T>: IntoColorUnclamped, + Xyz: FromColorUnclamped> + IntoColorUnclamped, { fn from_color_unclamped(oklab: Oklab) -> Self { if TypeId::of::<::Space>() == TypeId::of::() { @@ -829,10 +819,9 @@ where fn get_hue(&self) -> RgbHue { let sqrt_3: T = T::from_f64(1.73205081); - RgbHue::from_radians( - (sqrt_3 * (self.green.clone() - self.blue.clone())).atan2( - self.red.clone() * T::from_f64(2.0) - self.green.clone() - self.blue.clone(), - ), + RgbHue::from_cartesian( + self.red.clone() * T::from_f64(2.0) - self.green.clone() - self.blue.clone(), + sqrt_3 * (self.green.clone() - self.blue.clone()), ) } } @@ -1407,6 +1396,8 @@ mod test { use super::{Rgb, Rgba}; + test_convert_into_from_xyz!(Rgb); + #[test] fn ranges() { assert_ranges! { diff --git a/palette/src/xyz.rs b/palette/src/xyz.rs index 45f39c1be..9c029dff3 100644 --- a/palette/src/xyz.rs +++ b/palette/src/xyz.rs @@ -31,7 +31,7 @@ use crate::{ stimulus::{Stimulus, StimulusColor}, white_point::{Any, WhitePoint, D65}, Alpha, Clamp, ClampAssign, IsWithinBounds, Lab, Lighten, LightenAssign, Luma, Luv, Mix, - MixAssign, Okhsl, Okhsv, Okhwb, Oklab, Oklch, RelativeContrast, Yxy, + MixAssign, Oklab, RelativeContrast, Yxy, }; /// CIE 1931 XYZ with an alpha component. See the [`Xyza` implementation in @@ -53,7 +53,7 @@ pub type Xyza = Alpha, T>; palette_internal, white_point = "Wp", component = "T", - skip_derives(Xyz, Yxy, Luv, Rgb, Lab, Oklab, Oklch, Okhsl, Okhsv, Okhwb, Luma) + skip_derives(Xyz, Yxy, Luv, Rgb, Lab, Oklab, Luma) )] #[repr(C)] pub struct Xyz { @@ -330,49 +330,6 @@ where } } -impl FromColorUnclamped> for Xyz -where - Oklch: IntoColorUnclamped>, - Self: FromColorUnclamped>, -{ - fn from_color_unclamped(color: Oklch) -> Self { - let oklab: Oklab = color.into_color_unclamped(); - Self::from_color_unclamped(oklab) - } -} - -impl FromColorUnclamped> for Xyz -where - Okhsv: IntoColorUnclamped>, - Self: FromColorUnclamped>, -{ - fn from_color_unclamped(color: Okhsv) -> Self { - let oklab: Oklab = color.into_color_unclamped(); - Self::from_color_unclamped(oklab) - } -} - -impl FromColorUnclamped> for Xyz -where - Okhsl: IntoColorUnclamped>, - Self: FromColorUnclamped>, -{ - fn from_color_unclamped(color: Okhsl) -> Self { - let oklab: Oklab = color.into_color_unclamped(); - Self::from_color_unclamped(oklab) - } -} - -impl FromColorUnclamped> for Xyz -where - Okhwb: IntoColorUnclamped>, - Self: FromColorUnclamped>, -{ - fn from_color_unclamped(color: Okhwb) -> Self { - let okhsv: Okhsv = color.into_color_unclamped(); - Self::from_color_unclamped(okhsv) - } -} impl FromColorUnclamped> for Xyz where Self: Mul, @@ -608,6 +565,8 @@ mod test { const Y_N: f64 = 1.0; const Z_N: f64 = 1.08883; + test_convert_into_from_xyz!(Xyz); + #[test] fn luma() { let a = Xyz::::from_color(LinLuma::new(0.5)); diff --git a/palette/src/yxy.rs b/palette/src/yxy.rs index d7a0fb30d..6ef3905cc 100644 --- a/palette/src/yxy.rs +++ b/palette/src/yxy.rs @@ -448,6 +448,8 @@ mod test { use crate::white_point::D65; use crate::{FromColor, LinLuma, LinSrgb}; + test_convert_into_from_xyz!(Yxy); + #[test] fn luma() { let a = Yxy::::from_color(LinLuma::new(0.5));