palette/cam16/
ucs_jmh.rs

1use crate::{
2    angle::RealAngle,
3    bool_mask::HasBoolMask,
4    color_difference::{DeltaE, ImprovedDeltaE},
5    convert::{FromColorUnclamped, IntoColorUnclamped},
6    hues::{Cam16Hue, Cam16HueIter},
7    num::{Arithmetics, Hypot, Ln, One, Real, Trigonometry, Zero},
8    Alpha,
9};
10
11use super::{Cam16Jmh, Cam16UcsJab};
12
13/// Polar CAM16-UCS with an alpha component.
14///
15/// See the [`Cam16UcsJmha` implementation in
16/// `Alpha`](crate::Alpha#Cam16UcsJmha).
17pub type Cam16UcsJmha<T> = Alpha<Cam16UcsJmh<T>, T>;
18
19/// The polar form of CAM16-UCS, or J'M'h'.
20///
21/// CAM16-UCS is a perceptually uniform color space, based on CAM16 lightness
22/// and colorfulness. Its cartesian counterpart is [`Cam16UcsJab`].
23///
24/// # Creating a Value
25///
26/// ```
27/// use palette::{
28///     Srgb, FromColor, IntoColor, hues::Cam16Hue,
29///     cam16::{Cam16, Parameters, Cam16UcsJmh},
30/// };
31///
32/// let ucs = Cam16UcsJmh::new(50.0f32, 80.0, 120.0);
33///
34/// // There's also `new_const`:
35/// const UCS: Cam16UcsJmh<f32> = Cam16UcsJmh::new_const(50.0, 80.0, Cam16Hue::new(120.0));
36///
37/// // Customize these according to the viewing conditions:
38/// let mut example_parameters = Parameters::default_static_wp(40.0);
39///
40/// // CAM16-UCS from sRGB, or most other color spaces:
41/// let rgb = Srgb::new(0.3f32, 0.8, 0.1);
42/// let cam16 = Cam16::from_xyz(rgb.into_color(), example_parameters);
43/// let ucs_from_rgb = Cam16UcsJmh::from_color(cam16);
44///
45/// // It's also possible to convert from (and to) arrays and tuples:
46/// let ucs_from_array = Cam16UcsJmh::from([50.0f32, 80.0, 120.0]);
47/// let ucs_from_tuple = Cam16UcsJmh::from((50.0f32, 80.0, 120.0));
48/// ```
49#[derive(Clone, Copy, Debug, Default, WithAlpha, ArrayCast, FromColorUnclamped)]
50#[cfg_attr(feature = "serializing", derive(Serialize, Deserialize))]
51#[palette(
52    palette_internal,
53    component = "T",
54    skip_derives(Cam16Jmh, Cam16UcsJmh, Cam16UcsJab)
55)]
56#[repr(C)]
57pub struct Cam16UcsJmh<T> {
58    /// The lightness (J') of the color.
59    ///
60    /// It's derived from [`Cam16::lightness`][crate::cam16::Cam16::lightness]
61    /// and ranges from `0.0` to `100.0`.
62    pub lightness: T,
63
64    /// The colorfulness (M') of the color.
65    ///
66    /// It's derived from [`Cam16::colorfulness`][crate::cam16::Cam16::colorfulness].
67    pub colorfulness: T,
68
69    /// The hue (h') of the color.
70    ///
71    /// It's the same as [`Cam16::hue`][crate::cam16::Cam16::hue], despite the
72    /// h' notation.
73    #[palette(unsafe_same_layout_as = "T")]
74    pub hue: Cam16Hue<T>,
75}
76
77impl<T> Cam16UcsJmh<T> {
78    /// Create a CAM16-UCS J' M' h' color.
79    pub fn new<H: Into<Cam16Hue<T>>>(lightness: T, colorfulness: T, hue: H) -> Self {
80        Self::new_const(lightness, colorfulness, hue.into())
81    }
82
83    /// Create a CAM16-UCS J' M' h' color. This is the same as
84    /// `Cam16UcsJmh::new` without the generic hue type. It's temporary until
85    /// `const fn` supports traits.
86    pub const fn new_const(lightness: T, colorfulness: T, hue: Cam16Hue<T>) -> Self {
87        Self {
88            lightness,
89            colorfulness,
90            hue,
91        }
92    }
93
94    /// Convert to a `(J', M', h')` tuple.
95    pub fn into_components(self) -> (T, T, Cam16Hue<T>) {
96        (self.lightness, self.colorfulness, self.hue)
97    }
98
99    /// Convert from a `(J', M', h')` tuple.
100    pub fn from_components<H: Into<Cam16Hue<T>>>(
101        (lightness, colorfulness, hue): (T, T, H),
102    ) -> Self {
103        Self::new(lightness, colorfulness, hue)
104    }
105}
106
107impl<T> Cam16UcsJmh<T>
108where
109    T: Zero + Real,
110{
111    /// Return the `lightness` value minimum.
112    pub fn min_lightness() -> T {
113        T::zero()
114    }
115
116    /// Return the `lightness` value maximum.
117    pub fn max_lightness() -> T {
118        T::from_f64(100.0)
119    }
120
121    /// Return the `colorfulness` value minimum.
122    pub fn min_colorfulness() -> T {
123        T::zero()
124    }
125
126    /// Return a `colorfulness` value maximum that includes the sRGB gamut.
127    ///
128    /// <p class="warning">
129    /// This is entirely arbitrary and only for use in `Lighten`, `Darken` and
130    /// random generation. Colorfulness doesn't have a well defined upper
131    /// bound.
132    /// </p>
133    pub fn max_srgb_colorfulness() -> T {
134        // Based on a plot from https://facelessuser.github.io/coloraide/colors/cam16_ucs/
135        T::from_f64(50.0)
136    }
137}
138
139///<span id="Cam16UcsJmha"></span>[`Cam16UcsJmha`](crate::cam16::Cam16UcsJmha) implementations.
140impl<T, A> Alpha<Cam16UcsJmh<T>, A> {
141    /// Create a CAM16-UCS J' M' h' color with transparency.
142    pub fn new<H: Into<Cam16Hue<T>>>(lightness: T, colorfulness: T, hue: H, alpha: A) -> Self {
143        Self::new_const(lightness, colorfulness, hue.into(), alpha)
144    }
145
146    /// Create a CAM16-UCS J' M' h' color with transparency. This is the same as
147    /// `Cam16UcsJmha::new` without the generic hue type. It's temporary until
148    /// `const fn` supports traits.
149    pub const fn new_const(lightness: T, colorfulness: T, hue: Cam16Hue<T>, alpha: A) -> Self {
150        Self {
151            color: Cam16UcsJmh::new_const(lightness, colorfulness, hue),
152            alpha,
153        }
154    }
155
156    /// Convert to a `(J', M', h', a)` tuple.
157    pub fn into_components(self) -> (T, T, Cam16Hue<T>, A) {
158        (
159            self.color.lightness,
160            self.color.colorfulness,
161            self.color.hue,
162            self.alpha,
163        )
164    }
165
166    /// Convert from a `(J', M', h', a)` tuple.
167    pub fn from_components<H: Into<Cam16Hue<T>>>(
168        (lightness, colorfulness, hue, alpha): (T, T, H, A),
169    ) -> Self {
170        Self::new(lightness, colorfulness, hue, alpha)
171    }
172}
173
174impl<T> FromColorUnclamped<Cam16UcsJmh<T>> for Cam16UcsJmh<T> {
175    fn from_color_unclamped(val: Cam16UcsJmh<T>) -> Self {
176        val
177    }
178}
179
180impl<T> FromColorUnclamped<Cam16Jmh<T>> for Cam16UcsJmh<T>
181where
182    T: Real + One + Ln + Arithmetics,
183{
184    fn from_color_unclamped(val: Cam16Jmh<T>) -> Self {
185        let colorfulness =
186            (T::one() + T::from_f64(0.0228) * val.colorfulness).ln() / T::from_f64(0.0228);
187        let lightness =
188            T::from_f64(1.7) * &val.lightness / (T::one() + T::from_f64(0.007) * val.lightness);
189
190        Cam16UcsJmh {
191            lightness,
192            colorfulness,
193            hue: val.hue,
194        }
195    }
196}
197
198impl<T> FromColorUnclamped<Cam16UcsJab<T>> for Cam16UcsJmh<T>
199where
200    T: RealAngle + Hypot + Trigonometry + Arithmetics + Clone,
201{
202    fn from_color_unclamped(val: Cam16UcsJab<T>) -> Self {
203        Self {
204            lightness: val.lightness,
205            colorfulness: val.a.clone().hypot(val.b.clone()),
206            hue: Cam16Hue::from_cartesian(val.a, val.b),
207        }
208    }
209}
210
211impl<T> DeltaE for Cam16UcsJmh<T>
212where
213    Cam16UcsJab<T>: DeltaE<Scalar = T> + FromColorUnclamped<Self>,
214{
215    type Scalar = T;
216
217    #[inline]
218    fn delta_e(self, other: Self) -> Self::Scalar {
219        // Jab and Jmh have the same delta E.
220        Cam16UcsJab::from_color_unclamped(self).delta_e(other.into_color_unclamped())
221    }
222}
223
224impl<T> ImprovedDeltaE for Cam16UcsJmh<T>
225where
226    Cam16UcsJab<T>: ImprovedDeltaE<Scalar = T> + FromColorUnclamped<Self>,
227{
228    #[inline]
229    fn improved_delta_e(self, other: Self) -> Self::Scalar {
230        // Jab and Jmh have the same delta E.
231        Cam16UcsJab::from_color_unclamped(self).improved_delta_e(other.into_color_unclamped())
232    }
233}
234
235impl<T> HasBoolMask for Cam16UcsJmh<T>
236where
237    T: HasBoolMask,
238{
239    type Mask = T::Mask;
240}
241
242#[cfg(feature = "bytemuck")]
243unsafe impl<T> bytemuck::Zeroable for Cam16UcsJmh<T> where T: bytemuck::Zeroable {}
244
245#[cfg(feature = "bytemuck")]
246unsafe impl<T> bytemuck::Pod for Cam16UcsJmh<T> where T: bytemuck::Pod {}
247
248// Macro implementations
249
250impl_reference_component_methods_hue!(Cam16UcsJmh, [lightness, colorfulness]);
251impl_struct_of_arrays_methods_hue!(Cam16UcsJmh, [lightness, colorfulness]);
252impl_tuple_conversion_hue!(Cam16UcsJmh as (T, T, H), Cam16Hue);
253
254impl_is_within_bounds! {
255    Cam16UcsJmh {
256        lightness => [Self::min_lightness(), Self::max_lightness()],
257        colorfulness => [Self::min_colorfulness(), None]
258    }
259    where T: Zero + Real
260}
261impl_clamp! {
262    Cam16UcsJmh {
263        lightness => [Self::min_lightness(), Self::max_lightness()],
264        colorfulness => [Self::min_colorfulness()]
265    }
266    other {hue}
267    where T: Zero + Real
268}
269
270impl_mix_hue!(Cam16UcsJmh {
271    lightness,
272    colorfulness
273});
274impl_lighten!(Cam16UcsJmh increase {lightness => [Self::min_lightness(), Self::max_lightness()]} other {hue, colorfulness});
275impl_saturate!(Cam16UcsJmh increase {colorfulness => [Self::min_colorfulness(), Self::max_srgb_colorfulness()]} other {hue, lightness});
276impl_hue_ops!(Cam16UcsJmh, Cam16Hue);
277
278impl_color_add!(Cam16UcsJmh, [lightness, colorfulness, hue]);
279impl_color_sub!(Cam16UcsJmh, [lightness, colorfulness, hue]);
280
281impl_array_casts!(Cam16UcsJmh<T>, [T; 3]);
282impl_simd_array_conversion_hue!(Cam16UcsJmh, [lightness, colorfulness]);
283impl_struct_of_array_traits_hue!(Cam16UcsJmh, Cam16HueIter, [lightness, colorfulness]);
284
285impl_eq_hue!(Cam16UcsJmh, Cam16Hue, [lightness, colorfulness, hue]);
286
287impl_rand_traits_cylinder!(
288    UniformCam16UcsJmh,
289    Cam16UcsJmh {
290        hue: UniformCam16Hue => Cam16Hue,
291        height: lightness => [|l: T| l * Cam16UcsJmh::<T>::max_lightness()],
292        radius: colorfulness => [|c| c *  Cam16UcsJmh::<T>::max_srgb_colorfulness()]
293    }
294    where T: Real + Zero + core::ops::Mul<Output = T>,
295);
296
297// Unit tests
298
299#[cfg(test)]
300mod test {
301    use crate::{
302        cam16::{Cam16Jmh, Cam16UcsJmh},
303        convert::FromColorUnclamped,
304    };
305
306    #[cfg(feature = "approx")]
307    use crate::color_difference::DeltaE;
308
309    #[cfg(all(feature = "approx", feature = "alloc"))]
310    use crate::{
311        cam16::Cam16UcsJab, color_difference::ImprovedDeltaE, convert::IntoColorUnclamped,
312    };
313
314    #[test]
315    fn ranges() {
316        assert_ranges! {
317            Cam16UcsJmh<f64>;
318            clamped {
319                lightness: 0.0 => 100.0
320            }
321            clamped_min {
322                colorfulness: 0.0 => 200.0
323            }
324            unclamped {
325                hue: -360.0 => 360.0
326            }
327        }
328    }
329
330    #[test]
331    fn cam16_roundtrip() {
332        let ucs = Cam16UcsJmh::new(50.0f64, 80.0, 120.0);
333        let cam16 = Cam16Jmh::from_color_unclamped(ucs);
334        assert_eq!(Cam16UcsJmh::from_color_unclamped(cam16), ucs);
335    }
336
337    raw_pixel_conversion_tests!(Cam16UcsJmh<>: lightness, colorfulness, hue);
338    raw_pixel_conversion_fail_tests!(Cam16UcsJmh<>: lightness, colorfulness, hue);
339
340    #[test]
341    #[cfg(feature = "approx")]
342    fn delta_e_large_hue_diff() {
343        let lhs1 = Cam16UcsJmh::<f64>::new(50.0, 64.0, -730.0);
344        let rhs1 = Cam16UcsJmh::new(50.0, 64.0, 730.0);
345
346        let lhs2 = Cam16UcsJmh::<f64>::new(50.0, 64.0, -10.0);
347        let rhs2 = Cam16UcsJmh::new(50.0, 64.0, 10.0);
348
349        assert_relative_eq!(
350            lhs1.delta_e(rhs1),
351            lhs2.delta_e(rhs2),
352            epsilon = 0.0000000000001
353        );
354    }
355
356    // Jab and Jmh have the same delta E.
357    #[test]
358    #[cfg(all(feature = "approx", feature = "alloc"))]
359    fn jab_delta_e_equality() {
360        let mut jab_colors: Vec<Cam16UcsJab<f64>> = alloc::vec::Vec::new();
361
362        for j_step in 0i8..5 {
363            for a_step in -2i8..3 {
364                for b_step in -2i8..3 {
365                    jab_colors.push(Cam16UcsJab::new(
366                        j_step as f64 * 25.0,
367                        a_step as f64 * 60.0,
368                        b_step as f64 * 60.0,
369                    ))
370                }
371            }
372        }
373
374        let jmh_colors: alloc::vec::Vec<Cam16UcsJmh<_>> = jab_colors.clone().into_color_unclamped();
375
376        for (&lhs_jab, &lhs_jmh) in jab_colors.iter().zip(&jmh_colors) {
377            for (&rhs_jab, &rhs_jmh) in jab_colors.iter().zip(&jmh_colors) {
378                let delta_e_jab = lhs_jab.delta_e(rhs_jab);
379                let delta_e_jmh = lhs_jmh.delta_e(rhs_jmh);
380                assert_relative_eq!(delta_e_jab, delta_e_jmh, epsilon = 0.0000000000001);
381            }
382        }
383    }
384
385    // Jab and Jmh have the same delta E, so should also have the same improved
386    // delta E.
387    #[test]
388    #[cfg(all(feature = "approx", feature = "alloc"))]
389    fn jab_improved_delta_e_equality() {
390        let mut jab_colors: Vec<Cam16UcsJab<f64>> = alloc::vec::Vec::new();
391
392        for j_step in 0i8..5 {
393            for a_step in -2i8..3 {
394                for b_step in -2i8..3 {
395                    jab_colors.push(Cam16UcsJab::new(
396                        j_step as f64 * 25.0,
397                        a_step as f64 * 60.0,
398                        b_step as f64 * 60.0,
399                    ))
400                }
401            }
402        }
403
404        let jmh_colors: alloc::vec::Vec<Cam16UcsJmh<_>> = jab_colors.clone().into_color_unclamped();
405
406        for (&lhs_jab, &lhs_jmh) in jab_colors.iter().zip(&jmh_colors) {
407            for (&rhs_jab, &rhs_jmh) in jab_colors.iter().zip(&jmh_colors) {
408                let delta_e_jab = lhs_jab.improved_delta_e(rhs_jab);
409                let delta_e_jmh = lhs_jmh.improved_delta_e(rhs_jmh);
410                assert_relative_eq!(delta_e_jab, delta_e_jmh, epsilon = 0.0000000000001);
411            }
412        }
413    }
414
415    struct_of_arrays_tests!(
416        Cam16UcsJmh[lightness, colorfulness, hue],
417        super::Cam16UcsJmha::new(0.1f32, 0.2, 0.3, 0.4),
418        super::Cam16UcsJmha::new(0.2, 0.3, 0.4, 0.5),
419        super::Cam16UcsJmha::new(0.3, 0.4, 0.5, 0.6)
420    );
421
422    #[cfg(feature = "serializing")]
423    #[test]
424    fn serialize() {
425        let serialized = ::serde_json::to_string(&Cam16UcsJmh::new(0.3, 0.8, 0.1)).unwrap();
426
427        assert_eq!(
428            serialized,
429            r#"{"lightness":0.3,"colorfulness":0.8,"hue":0.1}"#
430        );
431    }
432
433    #[cfg(feature = "serializing")]
434    #[test]
435    fn deserialize() {
436        let deserialized: Cam16UcsJmh<f32> =
437            ::serde_json::from_str(r#"{"lightness":0.3,"colorfulness":0.8,"hue":0.1}"#).unwrap();
438
439        assert_eq!(deserialized, Cam16UcsJmh::new(0.3, 0.8, 0.1));
440    }
441
442    test_uniform_distribution! {
443        Cam16UcsJmh<f32> as crate::cam16::Cam16UcsJab<f32> {
444            lightness: (0.0, 100.0),
445            a: (-30.0, 30.0),
446            b: (-30.0, 30.0),
447        },
448        min: Cam16UcsJmh::new(0.0f32, 0.0, 0.0),
449        max: Cam16UcsJmh::new(100.0, 50.0, 360.0)
450    }
451}