1use 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
19pub type Lcha<Wp = D65, T = f32> = Alpha<Lch<Wp, T>, T>;
22
23#[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 pub l: T,
42
43 pub chroma: T,
48
49 #[palette(unsafe_same_layout_as = "T")]
52 pub hue: LabHue<T>,
53
54 #[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 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 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 pub fn into_components(self) -> (T, T, LabHue<T>) {
80 (self.l, self.chroma, self.hue)
81 }
82
83 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 pub fn min_l() -> T {
95 T::zero()
96 }
97
98 pub fn max_l() -> T {
100 T::from_f64(100.0)
101 }
102
103 pub fn min_chroma() -> T {
105 T::zero()
106 }
107
108 pub fn max_chroma() -> T {
112 T::from_f64(128.0)
113 }
114
115 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
123impl<Wp, T, A> Alpha<Lch<Wp, T>, A> {
125 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 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 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 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 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 Lab::from_color_unclamped(self).improved_delta_e(other.into_color_unclamped())
220 }
221}
222
223#[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 #[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 #[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}