palette/
lch.rs

1//! Types for the CIE L\*C\*h° color space.
2
3use core::{
4    marker::PhantomData,
5    ops::{BitAnd, BitOr},
6};
7
8use crate::{
9    angle::RealAngle,
10    bool_mask::{HasBoolMask, LazySelect},
11    color_difference::{get_ciede2000_difference, Ciede2000, DeltaE, ImprovedDeltaE, LabColorDiff},
12    convert::{FromColorUnclamped, IntoColorUnclamped},
13    hues::LabHueIter,
14    num::{Abs, Arithmetics, Exp, Hypot, One, PartialCmp, Powi, Real, Sqrt, Trigonometry, Zero},
15    white_point::D65,
16    Alpha, FromColor, GetHue, Lab, LabHue, Xyz,
17};
18
19/// CIE L\*C\*h° with an alpha component. See the [`Lcha` implementation in
20/// `Alpha`](crate::Alpha#Lcha).
21pub type Lcha<Wp = D65, T = f32> = Alpha<Lch<Wp, T>, T>;
22
23/// CIE L\*C\*h°, a polar version of [CIE L\*a\*b\*](crate::Lab).
24///
25/// L\*C\*h° shares its range and perceptual uniformity with L\*a\*b\*, but
26/// it's a cylindrical color space, like [HSL](crate::Hsl) and
27/// [HSV](crate::Hsv). This gives it the same ability to directly change
28/// the hue and colorfulness of a color, while preserving other visual aspects.
29#[derive(Debug, ArrayCast, FromColorUnclamped, WithAlpha)]
30#[cfg_attr(feature = "serializing", derive(Serialize, Deserialize))]
31#[palette(
32    palette_internal,
33    white_point = "Wp",
34    component = "T",
35    skip_derives(Lab, Lch)
36)]
37#[repr(C)]
38pub struct Lch<Wp = D65, T = f32> {
39    /// L\* is the lightness of the color. 0.0 gives absolute black and 100.0
40    /// gives the brightest white.
41    pub l: T,
42
43    /// C\* is the colorfulness of the color. It's similar to saturation. 0.0
44    /// gives gray scale colors, and numbers around 128-181 gives fully
45    /// saturated colors. The upper limit of 128 should
46    /// include the whole L\*a\*b\* space and some more.
47    pub chroma: T,
48
49    /// The hue of the color, in degrees. Decides if it's red, blue, purple,
50    /// etc.
51    #[palette(unsafe_same_layout_as = "T")]
52    pub hue: LabHue<T>,
53
54    /// The white point associated with the color's illuminant and observer.
55    /// D65 for 2 degree observer is used by default.
56    #[cfg_attr(feature = "serializing", serde(skip))]
57    #[palette(unsafe_zero_sized)]
58    pub white_point: PhantomData<Wp>,
59}
60
61impl<Wp, T> Lch<Wp, T> {
62    /// Create a CIE L\*C\*h° color.
63    pub fn new<H: Into<LabHue<T>>>(l: T, chroma: T, hue: H) -> Self {
64        Self::new_const(l, chroma, hue.into())
65    }
66
67    /// Create a CIE L\*C\*h° color. This is the same as `Lch::new` without the
68    /// generic hue type. It's temporary until `const fn` supports traits.
69    pub const fn new_const(l: T, chroma: T, hue: LabHue<T>) -> Self {
70        Lch {
71            l,
72            chroma,
73            hue,
74            white_point: PhantomData,
75        }
76    }
77
78    /// Convert to a `(L\*, C\*, h°)` tuple.
79    pub fn into_components(self) -> (T, T, LabHue<T>) {
80        (self.l, self.chroma, self.hue)
81    }
82
83    /// Convert from a `(L\*, C\*, h°)` tuple.
84    pub fn from_components<H: Into<LabHue<T>>>((l, chroma, hue): (T, T, H)) -> Self {
85        Self::new(l, chroma, hue)
86    }
87}
88
89impl<Wp, T> Lch<Wp, T>
90where
91    T: Zero + Real,
92{
93    /// Return the `l` value minimum.
94    pub fn min_l() -> T {
95        T::zero()
96    }
97
98    /// Return the `l` value maximum.
99    pub fn max_l() -> T {
100        T::from_f64(100.0)
101    }
102
103    /// Return the `chroma` value minimum.
104    pub fn min_chroma() -> T {
105        T::zero()
106    }
107
108    /// Return the `chroma` value maximum. This value does not cover the entire
109    /// color space, but covers enough to be practical for downsampling to
110    /// smaller color spaces like sRGB.
111    pub fn max_chroma() -> T {
112        T::from_f64(128.0)
113    }
114
115    /// Return the `chroma` extended maximum value. This value covers the entire
116    /// color space and is included for completeness, but the additional range
117    /// should be unnecessary for most use cases.
118    pub fn max_extended_chroma() -> T {
119        T::from_f64(crate::num::Sqrt::sqrt(128.0f64 * 128.0 + 128.0 * 128.0))
120    }
121}
122
123///<span id="Lcha"></span>[`Lcha`](crate::Lcha) implementations.
124impl<Wp, T, A> Alpha<Lch<Wp, T>, A> {
125    /// Create a CIE L\*C\*h° color with transparency.
126    pub fn new<H: Into<LabHue<T>>>(l: T, chroma: T, hue: H, alpha: A) -> Self {
127        Self::new_const(l, chroma, hue.into(), alpha)
128    }
129
130    /// Create a CIE L\*C\*h° color with transparency. This is the same as
131    /// `Lcha::new` without the generic hue type. It's temporary until `const
132    /// fn` supports traits.
133    pub const fn new_const(l: T, chroma: T, hue: LabHue<T>, alpha: A) -> Self {
134        Alpha {
135            color: Lch::new_const(l, chroma, hue),
136            alpha,
137        }
138    }
139
140    /// Convert to a `(L\*, C\*, h°, alpha)` tuple.
141    pub fn into_components(self) -> (T, T, LabHue<T>, A) {
142        (self.color.l, self.color.chroma, self.color.hue, self.alpha)
143    }
144
145    /// Convert from a `(L\*, C\*, h°, alpha)` tuple.
146    pub fn from_components<H: Into<LabHue<T>>>((l, chroma, hue, alpha): (T, T, H, A)) -> Self {
147        Self::new(l, chroma, hue, alpha)
148    }
149}
150
151impl_reference_component_methods_hue!(Lch<Wp>, [l, chroma], white_point);
152impl_struct_of_arrays_methods_hue!(Lch<Wp>, [l, chroma], white_point);
153
154impl<Wp, T> FromColorUnclamped<Lch<Wp, T>> for Lch<Wp, T> {
155    fn from_color_unclamped(color: Lch<Wp, T>) -> Self {
156        color
157    }
158}
159
160impl<Wp, T> FromColorUnclamped<Lab<Wp, T>> for Lch<Wp, T>
161where
162    T: Zero + Hypot,
163    Lab<Wp, T>: GetHue<Hue = LabHue<T>>,
164{
165    fn from_color_unclamped(color: Lab<Wp, T>) -> Self {
166        Lch {
167            hue: color.get_hue(),
168            l: color.l,
169            chroma: color.a.hypot(color.b),
170            white_point: PhantomData,
171        }
172    }
173}
174
175impl_tuple_conversion_hue!(Lch<Wp> as (T, T, H), LabHue);
176
177impl_is_within_bounds! {
178    Lch<Wp> {
179        l => [Self::min_l(), Self::max_l()],
180        chroma => [Self::min_chroma(), None]
181    }
182    where T: Real + Zero
183}
184impl_clamp! {
185    Lch<Wp> {
186        l => [Self::min_l(), Self::max_l()],
187        chroma => [Self::min_chroma()]
188    }
189    other {hue, white_point}
190    where T: Real + Zero
191}
192
193impl_mix_hue!(Lch<Wp> {l, chroma} phantom: white_point);
194impl_lighten!(Lch<Wp> increase {l => [Self::min_l(), Self::max_l()]} other {hue, chroma} phantom: white_point);
195impl_saturate!(Lch<Wp> increase {chroma => [Self::min_chroma(), Self::max_chroma()]} other {hue, l} phantom: white_point);
196impl_hue_ops!(Lch<Wp>, LabHue);
197
198impl<Wp, T> DeltaE for Lch<Wp, T>
199where
200    Lab<Wp, T>: FromColorUnclamped<Self> + DeltaE<Scalar = T>,
201{
202    type Scalar = T;
203
204    #[inline]
205    fn delta_e(self, other: Self) -> Self::Scalar {
206        // The definitions of delta E for Lch and Lab are equivalent. Converting
207        // to Lab is the fastest way, so far.
208        Lab::from_color_unclamped(self).delta_e(other.into_color_unclamped())
209    }
210}
211
212impl<Wp, T> ImprovedDeltaE for Lch<Wp, T>
213where
214    Lab<Wp, T>: FromColorUnclamped<Self> + ImprovedDeltaE<Scalar = T>,
215{
216    #[inline]
217    fn improved_delta_e(self, other: Self) -> Self::Scalar {
218        // The definitions of delta E for Lch and Lab are equivalent.
219        Lab::from_color_unclamped(self).improved_delta_e(other.into_color_unclamped())
220    }
221}
222
223/// CIEDE2000 distance metric for color difference.
224#[allow(deprecated)]
225impl<Wp, T> crate::ColorDifference for Lch<Wp, T>
226where
227    T: Real
228        + RealAngle
229        + One
230        + Zero
231        + Trigonometry
232        + Abs
233        + Sqrt
234        + Powi
235        + Exp
236        + Arithmetics
237        + PartialCmp
238        + Clone,
239    T::Mask: LazySelect<T> + BitAnd<Output = T::Mask> + BitOr<Output = T::Mask>,
240    Self: Into<LabColorDiff<T>>,
241{
242    type Scalar = T;
243
244    #[inline]
245    fn get_color_difference(self, other: Lch<Wp, T>) -> Self::Scalar {
246        get_ciede2000_difference(self.into(), other.into())
247    }
248}
249
250impl<Wp, T> Ciede2000 for Lch<Wp, T>
251where
252    T: Real
253        + RealAngle
254        + One
255        + Zero
256        + Powi
257        + Exp
258        + Trigonometry
259        + Abs
260        + Sqrt
261        + Arithmetics
262        + PartialCmp
263        + Clone,
264    T::Mask: LazySelect<T> + BitAnd<Output = T::Mask> + BitOr<Output = T::Mask>,
265    Self: IntoColorUnclamped<Lab<Wp, T>>,
266{
267    type Scalar = T;
268
269    #[inline]
270    fn difference(self, other: Self) -> Self::Scalar {
271        get_ciede2000_difference(self.into(), other.into())
272    }
273}
274
275impl<Wp, T> HasBoolMask for Lch<Wp, T>
276where
277    T: HasBoolMask,
278{
279    type Mask = T::Mask;
280}
281
282impl<Wp, T> Default for Lch<Wp, T>
283where
284    T: Zero + Real,
285    LabHue<T>: Default,
286{
287    fn default() -> Lch<Wp, T> {
288        Lch::new(Self::min_l(), Self::min_chroma(), LabHue::default())
289    }
290}
291
292impl_color_add!(Lch<Wp>, [l, chroma, hue], white_point);
293impl_color_sub!(Lch<Wp>, [l, chroma, hue], white_point);
294
295impl_array_casts!(Lch<Wp, T>, [T; 3]);
296impl_simd_array_conversion_hue!(Lch<Wp>, [l, chroma], white_point);
297impl_struct_of_array_traits_hue!(Lch<Wp>, LabHueIter, [l, chroma], white_point);
298
299impl_eq_hue!(Lch<Wp>, LabHue, [l, chroma, hue]);
300impl_copy_clone!(Lch<Wp>, [l, chroma, hue], white_point);
301
302#[allow(deprecated)]
303impl<Wp, T> crate::RelativeContrast for Lch<Wp, T>
304where
305    T: Real + Arithmetics + PartialCmp,
306    T::Mask: LazySelect<T>,
307    Xyz<Wp, T>: FromColor<Self>,
308{
309    type Scalar = T;
310
311    #[inline]
312    fn get_contrast_ratio(self, other: Self) -> T {
313        let xyz1 = Xyz::from_color(self);
314        let xyz2 = Xyz::from_color(other);
315
316        crate::contrast_ratio(xyz1.y, xyz2.y)
317    }
318}
319
320impl_rand_traits_cylinder!(
321    UniformLch,
322    Lch<Wp> {
323        hue: UniformLabHue => LabHue,
324        height: l => [|l: T| l * Lch::<Wp, T>::max_l()],
325        radius: chroma => [|chroma| chroma *  Lch::<Wp, T>::max_chroma()]
326    }
327    phantom: white_point: PhantomData<Wp>
328    where T: Real + Zero + core::ops::Mul<Output = T>,
329);
330
331#[cfg(feature = "bytemuck")]
332unsafe impl<Wp, T> bytemuck::Zeroable for Lch<Wp, T> where T: bytemuck::Zeroable {}
333
334#[cfg(feature = "bytemuck")]
335unsafe impl<Wp: 'static, T> bytemuck::Pod for Lch<Wp, T> where T: bytemuck::Pod {}
336
337#[cfg(test)]
338mod test {
339    use crate::{white_point::D65, Lch};
340
341    #[cfg(all(feature = "alloc", feature = "approx"))]
342    use crate::{
343        color_difference::{DeltaE, ImprovedDeltaE},
344        convert::IntoColorUnclamped,
345        Lab,
346    };
347
348    test_convert_into_from_xyz!(Lch);
349
350    #[test]
351    fn ranges() {
352        assert_ranges! {
353            Lch<D65, f64>;
354            clamped {
355                l: 0.0 => 100.0
356            }
357            clamped_min {
358                chroma: 0.0 => 200.0
359            }
360            unclamped {
361                hue: -360.0 => 360.0
362            }
363        }
364    }
365
366    raw_pixel_conversion_tests!(Lch<D65>: l, chroma, hue);
367    raw_pixel_conversion_fail_tests!(Lch<D65>: l, chroma, hue);
368
369    #[test]
370    fn check_min_max_components() {
371        assert_eq!(Lch::<D65, f64>::min_l(), 0.0);
372        assert_eq!(Lch::<D65, f64>::max_l(), 100.0);
373        assert_eq!(Lch::<D65, f64>::min_chroma(), 0.0);
374        assert_eq!(Lch::<D65, f64>::max_chroma(), 128.0);
375
376        #[cfg(feature = "approx")]
377        assert_relative_eq!(Lch::<D65, f64>::max_extended_chroma(), 181.01933598375618);
378    }
379
380    #[cfg(feature = "approx")]
381    #[test]
382    fn delta_e_large_hue_diff() {
383        use crate::color_difference::DeltaE;
384
385        let lhs1 = Lch::<D65, f64>::new(50.0, 64.0, -730.0);
386        let rhs1 = Lch::new(50.0, 64.0, 730.0);
387
388        let lhs2 = Lch::<D65, f64>::new(50.0, 64.0, -10.0);
389        let rhs2 = Lch::new(50.0, 64.0, 10.0);
390
391        assert_relative_eq!(
392            lhs1.delta_e(rhs1),
393            lhs2.delta_e(rhs2),
394            epsilon = 0.0000000000001
395        );
396    }
397
398    // Lab and Lch have the same delta E.
399    #[cfg(all(feature = "alloc", feature = "approx"))]
400    #[test]
401    fn lab_delta_e_equality() {
402        let mut lab_colors: Vec<Lab<D65, f64>> = Vec::new();
403
404        for l_step in 0i8..5 {
405            for a_step in -2i8..3 {
406                for b_step in -2i8..3 {
407                    lab_colors.push(Lab::new(
408                        l_step as f64 * 25.0,
409                        a_step as f64 * 60.0,
410                        b_step as f64 * 60.0,
411                    ))
412                }
413            }
414        }
415
416        let lch_colors: Vec<Lch<_, _>> = lab_colors.clone().into_color_unclamped();
417
418        for (&lhs_lab, &lhs_lch) in lab_colors.iter().zip(&lch_colors) {
419            for (&rhs_lab, &rhs_lch) in lab_colors.iter().zip(&lch_colors) {
420                let delta_e_lab = lhs_lab.delta_e(rhs_lab);
421                let delta_e_lch = lhs_lch.delta_e(rhs_lch);
422                assert_relative_eq!(delta_e_lab, delta_e_lch, epsilon = 0.0000000000001);
423            }
424        }
425    }
426
427    // Lab and Lch have the same delta E, so should also have the same improved
428    // delta E.
429    #[cfg(all(feature = "alloc", feature = "approx"))]
430    #[test]
431    fn lab_improved_delta_e_equality() {
432        let mut lab_colors: Vec<Lab<D65, f64>> = Vec::new();
433
434        for l_step in 0i8..5 {
435            for a_step in -2i8..3 {
436                for b_step in -2i8..3 {
437                    lab_colors.push(Lab::new(
438                        l_step as f64 * 25.0,
439                        a_step as f64 * 60.0,
440                        b_step as f64 * 60.0,
441                    ))
442                }
443            }
444        }
445
446        let lch_colors: Vec<Lch<_, _>> = lab_colors.clone().into_color_unclamped();
447
448        for (&lhs_lab, &lhs_lch) in lab_colors.iter().zip(&lch_colors) {
449            for (&rhs_lab, &rhs_lch) in lab_colors.iter().zip(&lch_colors) {
450                let delta_e_lab = lhs_lab.improved_delta_e(rhs_lab);
451                let delta_e_lch = lhs_lch.improved_delta_e(rhs_lch);
452                assert_relative_eq!(delta_e_lab, delta_e_lch, epsilon = 0.0000000000001);
453            }
454        }
455    }
456
457    struct_of_arrays_tests!(
458        Lch<D65>[l, chroma, hue] phantom: white_point,
459        super::Lcha::new(0.1f32, 0.2, 0.3, 0.4),
460        super::Lcha::new(0.2, 0.3, 0.4, 0.5),
461        super::Lcha::new(0.3, 0.4, 0.5, 0.6)
462    );
463
464    #[cfg(feature = "serializing")]
465    #[test]
466    fn serialize() {
467        let serialized = ::serde_json::to_string(&Lch::<D65>::new(0.3, 0.8, 0.1)).unwrap();
468
469        assert_eq!(serialized, r#"{"l":0.3,"chroma":0.8,"hue":0.1}"#);
470    }
471
472    #[cfg(feature = "serializing")]
473    #[test]
474    fn deserialize() {
475        let deserialized: Lch =
476            ::serde_json::from_str(r#"{"l":0.3,"chroma":0.8,"hue":0.1}"#).unwrap();
477
478        assert_eq!(deserialized, Lch::new(0.3, 0.8, 0.1));
479    }
480
481    test_uniform_distribution! {
482        Lch<D65, f32> as crate::Lab {
483            l: (0.0, 100.0),
484            a: (-89.0, 89.0),
485            b: (-89.0, 89.0),
486        },
487        min: Lch::new(0.0f32, 0.0, 0.0),
488        max: Lch::new(100.0, 128.0, 360.0)
489    }
490}