palette/cam16/
ucs_jab.rs

1use core::ops::Mul;
2
3use crate::{
4    angle::RealAngle,
5    bool_mask::HasBoolMask,
6    color_difference::{DeltaE, EuclideanDistance, ImprovedDeltaE},
7    convert::FromColorUnclamped,
8    num::{MinMax, Powf, Real, Sqrt, Trigonometry, Zero},
9    Alpha,
10};
11
12use super::Cam16UcsJmh;
13
14/// Cartesian CAM16-UCS with an alpha component.
15///
16/// See the [`Cam16UcsJaba` implementation in
17/// `Alpha`](crate::Alpha#Cam16UcsJaba).
18pub type Cam16UcsJaba<T> = Alpha<Cam16UcsJab<T>, T>;
19
20/// The Cartesian form of CAM16-UCS, or J' a' b'.
21///
22/// CAM16-UCS is a perceptually uniform color space, based on CAM16 lightness
23/// and colorfulness. Its polar counterpart is [`Cam16UcsJmh`].
24///
25/// # Creating a Value
26///
27/// ```
28/// use palette::{
29///     Srgb, FromColor, IntoColor,
30///     cam16::{Cam16, Parameters, Cam16UcsJab},
31/// };
32///
33/// let ucs = Cam16UcsJab::new(50.0f32, 80.0, -30.0);
34///
35/// // `new` is also `const`:
36/// const UCS: Cam16UcsJab<f32> = Cam16UcsJab::new(50.0, 80.0, -30.0);
37///
38/// // Customize these according to the viewing conditions:
39/// let mut example_parameters = Parameters::default_static_wp(40.0);
40///
41/// // CAM16-UCS from sRGB, or most other color spaces:
42/// let rgb = Srgb::new(0.3f32, 0.8, 0.1);
43/// let cam16 = Cam16::from_xyz(rgb.into_color(), example_parameters);
44/// let ucs_from_rgb = Cam16UcsJab::from_color(cam16);
45///
46/// // It's also possible to convert from (and to) arrays and tuples:
47/// let ucs_from_array = Cam16UcsJab::from([50.0f32, 80.0, -30.0]);
48/// let ucs_from_tuple = Cam16UcsJab::from((50.0f32, 80.0, -30.0));
49/// ```
50#[derive(Clone, Copy, Debug, Default, WithAlpha, ArrayCast, FromColorUnclamped)]
51#[cfg_attr(feature = "serializing", derive(Serialize, Deserialize))]
52#[palette(
53    palette_internal,
54    component = "T",
55    skip_derives(Cam16UcsJmh, Cam16UcsJab)
56)]
57#[repr(C)]
58pub struct Cam16UcsJab<T> {
59    /// The lightness (J') of the color.
60    ///
61    /// It's derived from [`Cam16::lightness`][crate::cam16::Cam16::lightness]
62    /// and ranges from `0.0` to `100.0`.
63    pub lightness: T,
64
65    /// The redness/greenness (a') of the color.
66    ///
67    /// It's derived from [`Cam16::hue`][crate::cam16::Cam16::hue] and
68    /// [`Cam16::colorfulness`][crate::cam16::Cam16::colorfulness].
69    pub a: T,
70
71    /// The yellowness/blueness (b') of the color.
72    ///
73    /// It's derived from [`Cam16::hue`][crate::cam16::Cam16::hue] and
74    /// [`Cam16::colorfulness`][crate::cam16::Cam16::colorfulness].
75    pub b: T,
76}
77
78impl<T> Cam16UcsJab<T> {
79    /// Create a CAM16-UCS J' a' b' color.
80    pub const fn new(lightness: T, a: T, b: T) -> Self {
81        Self { lightness, a, b }
82    }
83
84    /// Convert to a `(J', a', b')` tuple.
85    pub fn into_components(self) -> (T, T, T) {
86        (self.lightness, self.a, self.b)
87    }
88
89    /// Convert from a `(J', a', b')` tuple.
90    pub fn from_components((lightness, a, b): (T, T, T)) -> Self {
91        Self::new(lightness, a, b)
92    }
93}
94
95impl<T> Cam16UcsJab<T>
96where
97    T: Zero + Real,
98{
99    /// Return the `lightness` value minimum.
100    pub fn min_lightness() -> T {
101        T::zero()
102    }
103
104    /// Return the `lightness` value maximum.
105    pub fn max_lightness() -> T {
106        T::from_f64(100.0)
107    }
108
109    /// Return an `a` value minimum that includes the sRGB gamut.
110    ///
111    /// <p class="warning">
112    /// This is entirely arbitrary and only for use in random generation.
113    /// Colorfulness doesn't have a well defined upper bound, which makes
114    /// a' unbounded.
115    /// </p>
116    pub fn min_srgb_a() -> T {
117        // Based on a plot from https://facelessuser.github.io/coloraide/colors/cam16_ucs/
118        T::from_f64(-50.0)
119    }
120
121    /// Return an `a` value maximum that includes the sRGB gamut.
122    ///
123    /// <p class="warning">
124    /// This is entirely arbitrary and only for use in random generation.
125    /// Colorfulness doesn't have a well defined upper bound, which makes
126    /// a' unbounded.
127    /// </p>
128    pub fn max_srgb_a() -> T {
129        // Based on a plot from https://facelessuser.github.io/coloraide/colors/cam16_ucs/
130        T::from_f64(50.0)
131    }
132
133    /// Return a `b` value minimum that includes the sRGB gamut.
134    ///
135    /// <p class="warning">
136    /// This is entirely arbitrary and only for use in random generation.
137    /// Colorfulness doesn't have a well defined upper bound, which makes
138    /// b' unbounded.
139    /// </p>
140    pub fn min_srgb_b() -> T {
141        // Based on a plot from https://facelessuser.github.io/coloraide/colors/cam16_ucs/
142        T::from_f64(-50.0)
143    }
144
145    /// Return a `b` value maximum that includes the sRGB gamut.
146    ///
147    /// <p class="warning">
148    /// This is entirely arbitrary and only for use in random generation.
149    /// Colorfulness doesn't have a well defined upper bound, which makes
150    /// b' unbounded.
151    /// </p>
152    pub fn max_srgb_b() -> T {
153        // Based on a plot from https://facelessuser.github.io/coloraide/colors/cam16_ucs/
154        T::from_f64(50.0)
155    }
156}
157
158///<span id="Cam16UcsJaba"></span>[`Cam16UcsJaba`](crate::cam16::Cam16UcsJaba) implementations.
159impl<T, A> Alpha<Cam16UcsJab<T>, A> {
160    /// Create a CAM16-UCS J' a' b' color with transparency.
161    pub const fn new(lightness: T, a: T, b: T, alpha: A) -> Self {
162        Self {
163            color: Cam16UcsJab::new(lightness, a, b),
164            alpha,
165        }
166    }
167
168    /// Convert to a `(J', a', b', a)` tuple.
169    pub fn into_components(self) -> (T, T, T, A) {
170        (self.color.lightness, self.color.a, self.color.b, self.alpha)
171    }
172
173    /// Convert from a `(J', a', b', a)` tuple.
174    pub fn from_components((lightness, a, b, alpha): (T, T, T, A)) -> Self {
175        Self::new(lightness, a, b, alpha)
176    }
177}
178
179impl<T> FromColorUnclamped<Cam16UcsJab<T>> for Cam16UcsJab<T> {
180    fn from_color_unclamped(val: Cam16UcsJab<T>) -> Self {
181        val
182    }
183}
184
185impl<T> FromColorUnclamped<Cam16UcsJmh<T>> for Cam16UcsJab<T>
186where
187    T: RealAngle + Zero + Mul<Output = T> + Trigonometry + MinMax + Clone,
188{
189    fn from_color_unclamped(val: Cam16UcsJmh<T>) -> Self {
190        let (a, b) = val.hue.into_cartesian();
191        let colorfulness = val.colorfulness.max(T::zero());
192
193        Self {
194            lightness: val.lightness,
195            a: a * colorfulness.clone(),
196            b: b * colorfulness,
197        }
198    }
199}
200
201impl<T> DeltaE for Cam16UcsJab<T>
202where
203    Self: EuclideanDistance<Scalar = T>,
204    T: Sqrt,
205{
206    type Scalar = T;
207
208    #[inline]
209    fn delta_e(self, other: Self) -> Self::Scalar {
210        self.distance(other)
211    }
212}
213
214impl<T> ImprovedDeltaE for Cam16UcsJab<T>
215where
216    Self: DeltaE<Scalar = T> + EuclideanDistance<Scalar = T>,
217    T: Real + Mul<T, Output = T> + Powf,
218{
219    #[inline]
220    fn improved_delta_e(self, other: Self) -> Self::Scalar {
221        // Coefficients from "Power functions improving the performance of
222        // color-difference formulas" by Huang et al.
223        // https://opg.optica.org/oe/fulltext.cfm?uri=oe-23-1-597&id=307643
224        //
225        // The multiplication of 0.5 in the exponent makes it square root the
226        // squared distance.
227        T::from_f64(1.41) * self.distance_squared(other).powf(T::from_f64(0.63 * 0.5))
228    }
229}
230
231impl<T> HasBoolMask for Cam16UcsJab<T>
232where
233    T: HasBoolMask,
234{
235    type Mask = T::Mask;
236}
237
238#[cfg(feature = "bytemuck")]
239unsafe impl<T> bytemuck::Zeroable for Cam16UcsJab<T> where T: bytemuck::Zeroable {}
240
241#[cfg(feature = "bytemuck")]
242unsafe impl<T> bytemuck::Pod for Cam16UcsJab<T> where T: bytemuck::Pod {}
243
244// Macro implementations
245
246impl_reference_component_methods!(Cam16UcsJab, [lightness, a, b]);
247impl_struct_of_arrays_methods!(Cam16UcsJab, [lightness, a, b]);
248
249impl_tuple_conversion!(Cam16UcsJab as (T, T, T));
250
251impl_is_within_bounds! {
252    Cam16UcsJab {
253        lightness => [Self::min_lightness(), Self::max_lightness()]
254    }
255    where T: Real + Zero
256}
257impl_clamp! {
258    Cam16UcsJab {
259        lightness => [Self::min_lightness(), Self::max_lightness()]
260    }
261    other {a, b}
262    where T: Real + Zero
263}
264
265impl_mix!(Cam16UcsJab);
266impl_lighten!(Cam16UcsJab increase {lightness => [Self::min_lightness(), Self::max_lightness()]} other {a, b});
267impl_premultiply!(Cam16UcsJab { lightness, a, b });
268impl_euclidean_distance!(Cam16UcsJab { lightness, a, b });
269impl_hyab!(Cam16UcsJab {
270    lightness: lightness,
271    chroma1: a,
272    chroma2: b
273});
274impl_lab_color_schemes!(Cam16UcsJab[lightness]);
275
276impl_color_add!(Cam16UcsJab, [lightness, a, b]);
277impl_color_sub!(Cam16UcsJab, [lightness, a, b]);
278impl_color_mul!(Cam16UcsJab, [lightness, a, b]);
279impl_color_div!(Cam16UcsJab, [lightness, a, b]);
280
281impl_array_casts!(Cam16UcsJab<T>, [T; 3]);
282impl_simd_array_conversion!(Cam16UcsJab, [lightness, a, b]);
283impl_struct_of_array_traits!(Cam16UcsJab, [lightness, a, b]);
284
285impl_eq!(Cam16UcsJab, [lightness, a, b]);
286
287impl_rand_traits_cartesian!(
288    UniformCam16UcsJab,
289    Cam16UcsJab {
290        lightness => [|x| x * Cam16UcsJab::<T>::max_lightness()],
291        a => [|x| Cam16UcsJab::<T>::min_srgb_a() + x * (Cam16UcsJab::<T>::max_srgb_a() - Cam16UcsJab::<T>::min_srgb_a())],
292        b => [|x| Cam16UcsJab::<T>::min_srgb_b() + x * (Cam16UcsJab::<T>::max_srgb_b() - Cam16UcsJab::<T>::min_srgb_b())]
293    }
294    where T: Real + Zero + core::ops::Add<Output = T> + core::ops::Sub<Output = T> + core::ops::Mul<Output = T>
295);
296
297// Unit test
298
299#[cfg(test)]
300mod test {
301    #[cfg(feature = "approx")]
302    use crate::{cam16::Cam16Jmh, convert::FromColorUnclamped};
303
304    use super::Cam16UcsJab;
305
306    #[test]
307    fn ranges() {
308        assert_ranges! {
309            Cam16UcsJab<f64>;
310            clamped {
311                lightness: 0.0 => 100.0
312            }
313            clamped_min {}
314            unclamped {
315                a: -100.0 => 100.0,
316                b: -100.0 => 100.0
317            }
318        }
319    }
320
321    #[cfg(feature = "approx")]
322    #[test]
323    fn cam16_roundtrip() {
324        let ucs = Cam16UcsJab::new(50.0f64, 80.0, -30.0);
325        let cam16 = Cam16Jmh::from_color_unclamped(ucs);
326        assert_relative_eq!(
327            Cam16UcsJab::from_color_unclamped(cam16),
328            ucs,
329            epsilon = 0.0000000000001
330        );
331    }
332
333    raw_pixel_conversion_tests!(Cam16UcsJab<>: lightness, a, b);
334    raw_pixel_conversion_fail_tests!(Cam16UcsJab<>: lightness, a, b);
335
336    struct_of_arrays_tests!(
337        Cam16UcsJab[lightness, a, b],
338        super::Cam16UcsJaba::new(0.1f32, 0.2, 0.3, 0.4),
339        super::Cam16UcsJaba::new(0.2, 0.3, 0.4, 0.5),
340        super::Cam16UcsJaba::new(0.3, 0.4, 0.5, 0.6)
341    );
342
343    #[cfg(feature = "serializing")]
344    #[test]
345    fn serialize() {
346        let serialized = ::serde_json::to_string(&Cam16UcsJab::<f32>::new(0.3, 0.8, 0.1)).unwrap();
347
348        assert_eq!(serialized, r#"{"lightness":0.3,"a":0.8,"b":0.1}"#);
349    }
350
351    #[cfg(feature = "serializing")]
352    #[test]
353    fn deserialize() {
354        let deserialized: Cam16UcsJab<f32> =
355            ::serde_json::from_str(r#"{"lightness":0.3,"a":0.8,"b":0.1}"#).unwrap();
356
357        assert_eq!(deserialized, Cam16UcsJab::new(0.3, 0.8, 0.1));
358    }
359
360    test_uniform_distribution! {
361        Cam16UcsJab<f32> {
362            lightness: (0.0, 100.0),
363            a: (-50.0, 50.0),
364            b: (-50.0, 50.0)
365        },
366        min: Cam16UcsJab::new(0.0f32, -50.0, -50.0),
367        max: Cam16UcsJab::new(100.0, 50.0, 50.0)
368    }
369}