palette/cam16/
full.rs

1use crate::{
2    angle::RealAngle,
3    bool_mask::{HasBoolMask, LazySelect},
4    hues::Cam16Hue,
5    num::{Abs, Arithmetics, FromScalar, PartialCmp, Powf, Real, Signum, Sqrt, Trigonometry, Zero},
6    Alpha, GetHue, Xyz,
7};
8
9use super::{
10    BakedParameters, Cam16FromUnclamped, Cam16IntoUnclamped, Cam16Jch, Cam16Jmh, Cam16Jsh,
11    Cam16Qch, Cam16Qmh, Cam16Qsh, FromCam16Unclamped, IntoCam16Unclamped, WhitePointParameter,
12};
13
14/// CIE CAM16 with an alpha component.
15///
16/// See the [`Cam16a` implementation in `Alpha`](crate::Alpha#Cam16a).
17pub type Cam16a<T> = Alpha<Cam16<T>, T>;
18
19/// The CIE CAM16 color appearance model.
20///
21/// It's a set of six technically defined attributes that describe the
22/// appearance of a color under certain viewing conditions, and it's a successor
23/// of [CIECAM02](https://en.wikipedia.org/wiki/CIECAM02). The viewing
24/// conditions are defined using [`Parameters`][super::Parameters], and two sets
25/// of parameters can be used to translate the appearance of a color from one
26/// set of viewing conditions to another.
27///
28/// The use of the viewing conditions parameters sets `Cam16` and its derived
29/// types apart from most other color types in this library. It's, for example,
30/// not possible to use [`FromColor`][crate::FromColor] and friends to convert
31/// to and from other types, since that would require default viewing conditions
32/// to exist. Instead, the explicit [`Cam16::from_xyz`] and [`Cam16::into_xyz`]
33/// are there to bridge the gap.
34///
35/// Not all attributes are used when converting _from_ CAM16, since they are
36/// correlated and derived from each other. This library also provides partial
37/// versions of this struct, to make it easier to correctly specify a minimum
38/// attribute set.
39///
40/// The full list of partial CAM16 variants is:
41///
42/// * [`Cam16Jch`](crate::cam16::Cam16Jch): lightness and chroma.
43/// * [`Cam16Jmh`](crate::cam16::Cam16Jmh): lightness and colorfulness.
44/// * [`Cam16Jsh`](crate::cam16::Cam16Jsh): lightness and saturation.
45/// * [`Cam16Qch`](crate::cam16::Cam16Qch): brightness and chroma.
46/// * [`Cam16Qmh`](crate::cam16::Cam16Qmh): brightness and colorfulness.
47/// * [`Cam16Qsh`](crate::cam16::Cam16Qsh): brightness and saturation.
48///
49/// # CAM16-UCS
50///
51/// While CIE CAM16 is a model of color appearance, it's not necessarily
52/// suitable as a color space. Instead, there is the CAM16-UCS (CAM16 uniform
53/// color space), that's based off of the lightness, colorfulness and hue
54/// attributes. This colorspace is represented by the
55/// [`Cam16UcsJmh`][crate::cam16::Cam16UcsJmh] and
56/// [`Cam16UcsJab`][crate::cam16::Cam16UcsJab] types.
57///
58/// # Creating a Value
59///
60/// A `Cam16` value would typically come from another color space, or one of the
61/// partial sets of CAM16 attributes. All of which require known viewing
62/// conditions.
63///
64/// ```
65/// use palette::{
66///     Srgb, FromColor, IntoColor,
67///     cam16::{Cam16, Parameters, Cam16Jmh, Cam16UcsJmh},
68/// };
69///
70/// // Customize these according to the viewing conditions:
71/// let mut example_parameters = Parameters::default_static_wp(40.0);
72///
73/// // CAM16 from sRGB, or most other color spaces:
74/// let rgb = Srgb::new(0.3f32, 0.8, 0.1);
75/// let cam16_from_rgb = Cam16::from_xyz(rgb.into_color(), example_parameters);
76///
77/// // Full CAM16 from a partial set (any partial set can be used):
78/// let partial = Cam16Jmh::new(50.0f32, 80.0, 120.0);
79/// let cam16_from_partial = partial.into_full(example_parameters);
80///
81/// // Full CAM16 from CAM16-UCS J'M'h':
82/// let ucs = Cam16UcsJmh::new(50.0f32, 80.0, 120.0);
83/// let cam16_from_ucs = Cam16Jmh::from_color(ucs).into_full(example_parameters);
84/// ```
85#[derive(Clone, Copy, Debug, WithAlpha, Default)]
86#[palette(palette_internal, component = "T")]
87#[repr(C)]
88pub struct Cam16<T> {
89    /// The [lightness](https://cie.co.at/eilvterm/17-22-063) (J) of the
90    /// color.
91    ///
92    /// It's a perception of the color's luminance, but not linear to it, and is
93    /// relative to the reference white. The lightness of black is `0.0` and the
94    /// lightness of white is `100.0`.
95    ///
96    /// Lightness behaves similarly to L\* in [`Lch`][crate::Lch] or lightness
97    /// in [`Hsl`][crate::Hsl].
98    ///
99    /// See also <https://en.wikipedia.org/wiki/Lightness>.
100    #[doc(alias = "J")]
101    pub lightness: T,
102
103    /// The [chroma](https://cie.co.at/eilvterm/17-22-074) (C) of
104    /// the color.
105    ///
106    /// It's how chromatic the color appears in comparison with a grey color of
107    /// the same lightness. Changing the perceived chroma doesn't change the
108    /// perceived lightness, and vice versa.
109    ///
110    /// Chroma behaves similarly to chroma in [`Lch`][crate::Lch] or saturation
111    /// in [`Hsl`][crate::Hsl].
112    ///
113    /// See also <https://en.wikipedia.org/wiki/Colorfulness#Chroma>.
114    #[doc(alias = "C")]
115    pub chroma: T,
116
117    /// The [hue](https://cie.co.at/eilvterm/17-22-067) (h) of the color.
118    ///
119    /// The color's position around a color circle, in degrees.
120    ///
121    /// See also <https://en.wikipedia.org/wiki/Hue>.
122    #[doc(alias = "h")]
123    pub hue: Cam16Hue<T>,
124
125    /// The [brightness](https://cie.co.at/eilvterm/17-22-059) (Q) of the
126    /// color.
127    ///
128    /// It's the perception of how much light appears to shine from an object.
129    /// As opposed to `lightness`, this is not in comparison to a reference
130    /// white, but in more absolute terms. Lightness and brightness area also
131    /// not linearly correlated in CAM16.
132    ///
133    /// Brightness behaves similarly to value in [`Hsv`][crate::Hsv].
134    ///
135    /// See also <https://en.wikipedia.org/wiki/Brightness>.
136    #[doc(alias = "Q")]
137    pub brightness: T,
138
139    /// The [colorfulness](https://cie.co.at/eilvterm/17-22-072) (M) of the
140    /// color.
141    ///
142    /// It's a perception of how chromatic the color is and usually increases
143    /// with luminance, unless the brightness is very high.
144    ///
145    /// See also <https://en.wikipedia.org/wiki/Colorfulness>.
146    #[doc(alias = "M")]
147    pub colorfulness: T,
148
149    /// The [saturation](https://cie.co.at/eilvterm/17-22-073)
150    /// (s) of the color.
151    ///
152    /// It's the colorfulness of a color in proportion to its own brightness.
153    /// The perceived saturation should stay the same when the perceived
154    /// brightness changes, and vice versa.
155    ///
156    /// Saturation behaves similarly to saturation in [`Hsv`][crate::Hsv].
157    ///
158    /// See also <https://en.wikipedia.org/wiki/Colorfulness#Saturation>.
159    #[doc(alias = "s")]
160    pub saturation: T,
161}
162
163impl<T> Cam16<T> {
164    /// Derive CIE CAM16 attributes for the provided color, under the provided
165    /// viewing conditions.
166    ///
167    /// ```
168    /// use palette::{Srgb, IntoColor, cam16::{Cam16, Parameters}};
169    ///
170    /// // Customize these according to the viewing conditions:
171    /// let mut example_parameters = Parameters::default_static_wp(40.0);
172    ///
173    /// let rgb = Srgb::new(0.3f32, 0.8, 0.1);
174    /// let cam16 = Cam16::from_xyz(rgb.into_color(), example_parameters);
175    /// ```
176    ///
177    /// It's also possible to "pre-bake" the parameters, to avoid recalculate
178    /// some of the derived values when converting multiple color value.
179    ///
180    /// ```
181    /// use palette::{Srgb, IntoColor, cam16::{Cam16, Parameters}};
182    ///
183    /// // Customize these according to the viewing conditions:
184    /// let mut example_parameters = Parameters::default_static_wp(40.0);
185    ///
186    /// let baked_parameters = example_parameters.bake();
187    ///
188    /// let rgb = Srgb::new(0.3f32, 0.8, 0.1);
189    /// let cam16 = Cam16::from_xyz(rgb.into_color(), baked_parameters);
190    /// ```
191    #[inline]
192    pub fn from_xyz<WpParam>(
193        color: Xyz<WpParam::StaticWp, T>,
194        parameters: impl Into<BakedParameters<WpParam, T::Scalar>>,
195    ) -> Self
196    where
197        Xyz<WpParam::StaticWp, T>: IntoCam16Unclamped<WpParam, Self, Scalar = T::Scalar>,
198        T: FromScalar,
199        WpParam: WhitePointParameter<T::Scalar>,
200    {
201        color.into_cam16_unclamped(parameters.into())
202    }
203
204    /// Construct an XYZ color that matches these CIE CAM16 attributes, under
205    /// the provided viewing conditions.
206    ///
207    /// <p class="warning">
208    /// This assumes that all of the correlated attributes are consistent, as
209    /// only some of them are actually used. You may want to use one of the
210    /// partial CAM16 representations for more control over which set of
211    /// attributes that should be.
212    /// </p>
213    ///
214    /// ```
215    /// use palette::{Srgb, FromColor, cam16::{Cam16, Parameters}};
216    /// # fn get_cam16_value() -> Cam16<f32> {Cam16::default()}
217    ///
218    /// // Customize these according to the viewing conditions:
219    /// let mut example_parameters = Parameters::default_static_wp(40.0);
220    ///
221    /// let cam16: Cam16<f32> = get_cam16_value();
222    /// let rgb = Srgb::from_color(cam16.into_xyz(example_parameters));
223    /// ```
224    ///
225    /// It's also possible to "pre-bake" the parameters, to avoid recalculate
226    /// some of the derived values when converting multiple color value.
227    ///
228    /// ```
229    /// use palette::{Srgb, FromColor, cam16::{Cam16, Parameters}};
230    /// # fn get_cam16_value() -> Cam16<f32> {Cam16::default()}
231    ///
232    /// // Customize these according to the viewing conditions:
233    /// let mut example_parameters = Parameters::default_static_wp(40.0);
234    ///
235    /// let baked_parameters = example_parameters.bake();
236    ///
237    /// let cam16: Cam16<f32> = get_cam16_value();
238    /// let rgb = Srgb::from_color(cam16.into_xyz(baked_parameters));
239    /// ```
240    #[inline]
241    pub fn into_xyz<WpParam>(
242        self,
243        parameters: impl Into<BakedParameters<WpParam, T::Scalar>>,
244    ) -> Xyz<WpParam::StaticWp, T>
245    where
246        Self: Cam16IntoUnclamped<WpParam, Xyz<WpParam::StaticWp, T>, Scalar = T::Scalar>,
247        WpParam: WhitePointParameter<T>,
248        T: FromScalar,
249    {
250        self.cam16_into_unclamped(parameters.into())
251    }
252}
253
254///<span id="Cam16a"></span>[`Cam16a`](crate::cam16::Cam16a) implementations.
255impl<T, A> Alpha<Cam16<T>, A> {
256    /// Derive CIE CAM16 attributes with transparency for the provided color,
257    /// under the provided viewing conditions.
258    ///
259    /// ```
260    /// use palette::{Srgba, IntoColor, cam16::{Cam16a, Parameters}};
261    ///
262    /// // Customize these according to the viewing conditions:
263    /// let mut example_parameters = Parameters::default_static_wp(40.0);
264    ///
265    /// let rgba = Srgba::new(0.3f32, 0.8, 0.1, 0.9);
266    /// let cam16a = Cam16a::from_xyz(rgba.into_color(), example_parameters);
267    /// ```
268    ///
269    /// It's also possible to "pre-bake" the parameters, to avoid recalculate
270    /// some of the derived values when converting multiple color value.
271    ///
272    /// ```
273    /// use palette::{Srgba, IntoColor, cam16::{Cam16a, Parameters}};
274    ///
275    /// // Customize these according to the viewing conditions:
276    /// let mut example_parameters = Parameters::default_static_wp(40.0);
277    ///
278    /// let baked_parameters = example_parameters.bake();
279    ///
280    /// let rgba = Srgba::new(0.3f32, 0.8, 0.1, 0.9);
281    /// let cam16a = Cam16a::from_xyz(rgba.into_color(), baked_parameters);
282    /// ```
283    #[inline]
284    pub fn from_xyz<WpParam>(
285        color: Alpha<Xyz<WpParam::StaticWp, T>, A>,
286        parameters: impl Into<BakedParameters<WpParam, T::Scalar>>,
287    ) -> Self
288    where
289        Xyz<WpParam::StaticWp, T>: IntoCam16Unclamped<WpParam, Cam16<T>, Scalar = T::Scalar>,
290        T: FromScalar,
291        WpParam: WhitePointParameter<T::Scalar>,
292    {
293        let Alpha { color, alpha } = color;
294
295        Alpha {
296            color: Cam16::from_xyz(color, parameters),
297            alpha,
298        }
299    }
300
301    /// Construct an XYZ color with transparency, that matches these CIE CAM16
302    /// attributes, under the provided viewing conditions.
303    ///
304    /// <p class="warning">
305    /// This assumes that all of the correlated attributes are consistent, as
306    /// only some of them are actually used. You may want to use one of the
307    /// partial CAM16 representations for more control over which set of
308    /// attributes that should be.
309    /// </p>
310    ///
311    /// ```
312    /// use palette::{Srgba, FromColor, cam16::{Cam16a, Parameters}};
313    /// # fn get_cam16a_value() -> Cam16a<f32> {Cam16a::default()}
314    ///
315    /// // Customize these according to the viewing conditions:
316    /// let mut example_parameters = Parameters::default_static_wp(40.0);
317    ///
318    /// let cam16a = get_cam16a_value();
319    /// let rgba = Srgba::from_color(cam16a.into_xyz(example_parameters));
320    /// ```
321    ///
322    /// It's also possible to "pre-bake" the parameters, to avoid recalculate
323    /// some of the derived values when converting multiple color value.
324    ///
325    /// ```
326    /// use palette::{Srgba, FromColor, cam16::{Cam16a, Parameters}};
327    /// # fn get_cam16a_value() -> Cam16a<f32> {Cam16a::default()}
328    ///
329    /// // Customize these according to the viewing conditions:
330    /// let mut example_parameters = Parameters::default_static_wp(40.0);
331    ///
332    /// let baked_parameters = example_parameters.bake();
333    ///
334    /// let cam16a = get_cam16a_value();
335    /// let rgba = Srgba::from_color(cam16a.into_xyz(baked_parameters));
336    /// ```
337    #[inline]
338    pub fn into_xyz<WpParam>(
339        self,
340        parameters: impl Into<BakedParameters<WpParam, T::Scalar>>,
341    ) -> Alpha<Xyz<WpParam::StaticWp, T>, A>
342    where
343        Cam16<T>: Cam16IntoUnclamped<WpParam, Xyz<WpParam::StaticWp, T>, Scalar = T::Scalar>,
344        WpParam: WhitePointParameter<T>,
345        T: FromScalar,
346    {
347        let Alpha { color, alpha } = self;
348
349        Alpha {
350            color: color.into_xyz(parameters),
351            alpha,
352        }
353    }
354}
355
356impl<WpParam, T> Cam16FromUnclamped<WpParam, Xyz<WpParam::StaticWp, T>> for Cam16<T>
357where
358    WpParam: WhitePointParameter<T::Scalar>,
359    T: Real
360        + FromScalar
361        + Arithmetics
362        + Powf
363        + Sqrt
364        + Abs
365        + Signum
366        + Trigonometry
367        + RealAngle
368        + Clone,
369    T::Scalar: Clone,
370{
371    type Scalar = T::Scalar;
372
373    fn cam16_from_unclamped(
374        color: Xyz<WpParam::StaticWp, T>,
375        parameters: BakedParameters<WpParam, Self::Scalar>,
376    ) -> Self {
377        super::math::xyz_to_cam16(color.with_white_point(), parameters.inner)
378    }
379}
380
381macro_rules! impl_from_cam16_partial {
382    ($($name: ident),+) => {
383        $(
384            impl<WpParam, T> Cam16FromUnclamped<WpParam, $name<T>> for Cam16<T>
385            where
386                WpParam: WhitePointParameter<T>,
387                T: Real + FromScalar + Zero + Arithmetics + Sqrt + PartialCmp + Clone,
388                T::Mask: LazySelect<T> + Clone,
389                T::Scalar: Clone
390            {
391                type Scalar = T::Scalar;
392
393                fn cam16_from_unclamped(
394                    cam16: $name<T>,
395                    parameters: crate::cam16::BakedParameters<WpParam, Self::Scalar>,
396                ) -> Self {
397                    let (
398                        luminance,
399                        chromaticity,
400                        hue,
401                    ) = cam16.into_dynamic();
402
403                    let (lightness, brightness) = luminance.into_cam16(parameters.clone());
404                    let (chroma, colorfulness, saturation) =
405                        chromaticity.into_cam16(lightness.clone(), parameters);
406
407                    Cam16 {
408                        lightness,
409                        chroma,
410                        hue,
411                        brightness,
412                        colorfulness,
413                        saturation,
414                    }
415                }
416            }
417
418            impl<WpParam, T> FromCam16Unclamped<WpParam, $name<T>> for Cam16<T>
419            where
420                Self: Cam16FromUnclamped<WpParam, $name<T>>,
421            {
422                type Scalar = <Self as Cam16FromUnclamped<WpParam, $name<T>>>::Scalar;
423
424                fn from_cam16_unclamped(
425                    cam16: $name<T>,
426                    parameters: crate::cam16::BakedParameters<WpParam, Self::Scalar>,
427                ) -> Self {
428                    Self::cam16_from_unclamped(cam16, parameters)
429                }
430            }
431        )+
432    };
433}
434
435impl_from_cam16_partial!(Cam16Jmh, Cam16Jch, Cam16Jsh, Cam16Qmh, Cam16Qch, Cam16Qsh);
436
437impl<T> GetHue for Cam16<T>
438where
439    T: Clone,
440{
441    type Hue = Cam16Hue<T>;
442
443    fn get_hue(&self) -> Cam16Hue<T> {
444        self.hue.clone()
445    }
446}
447
448impl<T> HasBoolMask for Cam16<T>
449where
450    T: HasBoolMask,
451{
452    type Mask = T::Mask;
453}
454
455// Macro implementations
456
457impl_is_within_bounds! {
458    Cam16 {
459        lightness => [T::zero(), None],
460        chroma => [T::zero(), None],
461        brightness => [T::zero(), None],
462        colorfulness => [T::zero(), None],
463        saturation => [T::zero(), None]
464    }
465    where T: Zero
466}
467impl_clamp! {
468    Cam16 {
469        lightness => [T::zero()],
470        chroma => [T::zero()],
471        brightness => [T::zero()],
472        colorfulness => [T::zero()],
473        saturation => [T::zero()]
474    }
475    other {hue}
476    where T: Zero
477}
478
479impl_eq_hue!(
480    Cam16,
481    Cam16Hue,
482    [lightness, chroma, brightness, colorfulness, saturation]
483);
484impl_simd_array_conversion_hue!(
485    Cam16,
486    [lightness, chroma, brightness, colorfulness, saturation]
487);
488
489// Unit test
490
491#[cfg(test)]
492#[cfg(feature = "approx")]
493mod test {
494    use crate::{
495        cam16::{
496            math::{chromaticity::ChromaticityType, luminance::LuminanceType},
497            BakedParameters, Cam16Jch, Parameters,
498        },
499        convert::{FromColorUnclamped, IntoColorUnclamped},
500        Srgb,
501    };
502
503    use super::Cam16;
504
505    macro_rules! assert_cam16_to_rgb {
506        ($cam16:expr, $rgb:expr, $($params:tt)*) => {
507            let cam16 = $cam16;
508            let parameters = BakedParameters::from(Parameters::TEST_DEFAULTS);
509
510            let rgb: Srgb<f64> = cam16.into_xyz(parameters).into_color_unclamped();
511            assert_relative_eq!(rgb, $rgb, $($params)*);
512
513            let chromaticities = [
514                ChromaticityType::Chroma(cam16.chroma),
515                ChromaticityType::Colorfulness(cam16.colorfulness),
516                ChromaticityType::Saturation(cam16.saturation),
517            ];
518            let luminances = [
519                LuminanceType::Lightness(cam16.lightness),
520                LuminanceType::Brightness(cam16.brightness),
521            ];
522
523            for luminance in luminances {
524                for chromaticity in chromaticities {
525                    let partial = (
526                        luminance,
527                        chromaticity,
528                        cam16.hue,
529                    );
530
531                    let xyz = crate::cam16::math::cam16_to_xyz(partial, parameters.inner).with_white_point();
532
533                    assert_relative_eq!(
534                        Srgb::<f64>::from_color_unclamped(xyz),
535                        $rgb,
536                        $($params)*
537                    );
538                }
539            }
540        };
541    }
542
543    #[test]
544    fn converts_with_jch() {
545        let parameters = Parameters::TEST_DEFAULTS.bake();
546        let xyz = Srgb::from(0x5588cc).into_linear().into_color_unclamped();
547        let mut cam16: Cam16<f64> = Cam16::from_xyz(xyz, parameters);
548        let cam16jch = Cam16Jch::from_full(cam16);
549
550        // Zero the other attributes so they produce errors if they are used.
551        cam16.brightness = 0.0;
552        cam16.colorfulness = 0.0;
553        cam16.saturation = 0.0;
554
555        assert_eq!(cam16.into_xyz(parameters), cam16jch.into_xyz(parameters));
556    }
557
558    #[test]
559    fn example_blue() {
560        // Uses the example color from https://observablehq.com/@jrus/cam16
561        let xyz = Srgb::from(0x5588cc).into_linear().into_color_unclamped();
562        let mut cam16: Cam16<f64> = Cam16::from_xyz(xyz, Parameters::TEST_DEFAULTS);
563        cam16.hue = cam16.hue.into_positive_degrees().into();
564
565        assert_relative_eq!(
566            cam16,
567            Cam16 {
568                lightness: 45.544264720360346,
569                chroma: 45.07001048293764,
570                hue: 259.225345298129.into(),
571                brightness: 132.96974182692045,
572                colorfulness: 39.4130607870103,
573                saturation: 54.4432031413259,
574            },
575            epsilon = 0.01
576        );
577
578        assert_cam16_to_rgb!(
579            cam16,
580            Srgb::from(0x5588cc).into_format(),
581            epsilon = 0.0000001
582        );
583    }
584
585    #[test]
586    fn black() {
587        // Checks against the output from https://observablehq.com/@jrus/cam16
588        let xyz = Srgb::from(0x000000).into_linear().into_color_unclamped();
589        let mut cam16: Cam16<f64> = Cam16::from_xyz(xyz, Parameters::TEST_DEFAULTS);
590        cam16.hue = cam16.hue.into_positive_degrees().into();
591
592        assert_relative_eq!(
593            cam16,
594            Cam16 {
595                lightness: 0.0,
596                chroma: 0.0,
597                hue: 0.0.into(),
598                brightness: 0.0,
599                colorfulness: 0.0,
600                saturation: 0.0,
601            },
602            epsilon = 0.01
603        );
604
605        assert_cam16_to_rgb!(
606            cam16,
607            Srgb::from(0x000000).into_format(),
608            epsilon = 0.0000001
609        );
610    }
611
612    #[test]
613    fn white() {
614        // Checks against the output from https://observablehq.com/@jrus/cam16
615        let xyz = Srgb::from(0xffffff).into_linear().into_color_unclamped();
616        let mut cam16: Cam16<f64> = Cam16::from_xyz(xyz, Parameters::TEST_DEFAULTS);
617        cam16.hue = cam16.hue.into_positive_degrees().into();
618
619        assert_relative_eq!(
620            cam16,
621            Cam16 {
622                lightness: 99.99955537650459,
623                chroma: 2.1815254387079435,
624                hue: 209.49854407518228.into(),
625                brightness: 197.03120459014184,
626                colorfulness: 1.9077118865271965,
627                saturation: 9.839859256901553,
628            },
629            epsilon = 0.1
630        );
631
632        assert_cam16_to_rgb!(
633            cam16,
634            Srgb::from(0xffffff).into_format(),
635            epsilon = 0.0000001
636        );
637    }
638
639    #[test]
640    fn red() {
641        // Checks against the output from https://observablehq.com/@jrus/cam16
642        let xyz = Srgb::from(0xff0000).into_linear().into_color_unclamped();
643        let mut cam16: Cam16<f64> = Cam16::from_xyz(xyz, Parameters::TEST_DEFAULTS);
644        cam16.hue = cam16.hue.into_positive_degrees().into();
645
646        assert_relative_eq!(
647            cam16,
648            Cam16 {
649                lightness: 46.23623443823762,
650                chroma: 113.27879472174797,
651                hue: 27.412485587695937.into(),
652                brightness: 133.9760614641257,
653                colorfulness: 99.06063864657237,
654                saturation: 85.98782392745971,
655            },
656            epsilon = 0.01
657        );
658
659        assert_cam16_to_rgb!(cam16, Srgb::from(0xff0000).into_format(), epsilon = 0.00001);
660    }
661
662    #[test]
663    fn green() {
664        // Checks against the output from https://observablehq.com/@jrus/cam16
665        let xyz = Srgb::from(0x00ff00).into_linear().into_color_unclamped();
666        let mut cam16: Cam16<f64> = Cam16::from_xyz(xyz, Parameters::TEST_DEFAULTS);
667        cam16.hue = cam16.hue.into_positive_degrees().into();
668
669        assert_relative_eq!(
670            cam16,
671            Cam16 {
672                lightness: 79.23121430933533,
673                chroma: 107.77869525794452,
674                hue: 141.93451307926003.into(),
675                brightness: 175.38164288466993,
676                colorfulness: 94.25088262080988,
677                saturation: 73.30787758114869,
678            },
679            epsilon = 0.01
680        );
681
682        assert_cam16_to_rgb!(
683            cam16,
684            Srgb::from(0x00ff00).into_format(),
685            epsilon = 0.000001
686        );
687    }
688
689    #[test]
690    fn blue() {
691        // Checks against the output from https://observablehq.com/@jrus/cam16
692        let xyz = Srgb::from(0x0000ff).into_linear().into_color_unclamped();
693        let mut cam16: Cam16<f64> = Cam16::from_xyz(xyz, Parameters::TEST_DEFAULTS);
694        cam16.hue = cam16.hue.into_positive_degrees().into();
695
696        assert_relative_eq!(
697            cam16,
698            Cam16 {
699                lightness: 25.22701796474445,
700                chroma: 86.59618504567312,
701                hue: 282.81848901862566.into(),
702                brightness: 98.96210767195342,
703                colorfulness: 75.72708922311855,
704                saturation: 87.47645277637828,
705            },
706            epsilon = 0.01
707        );
708
709        assert_cam16_to_rgb!(
710            cam16,
711            Srgb::from(0x0000ff).into_format(),
712            epsilon = 0.000001
713        );
714    }
715
716    #[cfg(feature = "wide")]
717    #[test]
718    fn simd() {
719        let white_srgb = Srgb::from(0xffffff).into_format();
720        let white_cam16 = Cam16 {
721            lightness: 99.99955537650459,
722            chroma: 2.1815254387079435,
723            hue: 209.49854407518228.into(),
724            brightness: 197.03120459014184,
725            colorfulness: 1.9077118865271965,
726            saturation: 9.839859256901553,
727        };
728
729        let red_srgb = Srgb::from(0xff0000).into_format();
730        let red_cam16 = Cam16 {
731            lightness: 46.23623443823762,
732            chroma: 113.27879472174797,
733            hue: 27.412485587695937.into(),
734            brightness: 133.9760614641257,
735            colorfulness: 99.06063864657237,
736            saturation: 85.98782392745971,
737        };
738
739        let green_srgb = Srgb::from(0x00ff00).into_format();
740        let green_cam16 = Cam16 {
741            lightness: 79.23121430933533,
742            chroma: 107.77869525794452,
743            hue: 141.93451307926003.into(),
744            brightness: 175.38164288466993,
745            colorfulness: 94.25088262080988,
746            saturation: 73.30787758114869,
747        };
748
749        let blue_srgb = Srgb::from(0x0000ff).into_format();
750        let blue_cam16 = Cam16 {
751            lightness: 25.22701796474445,
752            chroma: 86.59618504567312,
753            hue: 282.81848901862566.into(),
754            brightness: 98.96210767195342,
755            colorfulness: 75.72708922311855,
756            saturation: 87.47645277637828,
757        };
758
759        let srgb = Srgb::<wide::f64x4>::from([white_srgb, red_srgb, green_srgb, blue_srgb]);
760        let xyz = srgb.into_linear().into_color_unclamped();
761        let mut cam16 = Cam16::from_xyz(xyz, Parameters::TEST_DEFAULTS);
762        cam16.hue = cam16.hue.into_positive_degrees().into();
763
764        assert_relative_eq!(
765            &<[Cam16<_>; 4]>::from(cam16)[..],
766            &[white_cam16, red_cam16, green_cam16, blue_cam16][..],
767            epsilon = 0.1
768        );
769
770        let srgb = Srgb::from_color_unclamped(cam16.into_xyz(Parameters::TEST_DEFAULTS));
771
772        assert_relative_eq!(
773            &<[Srgb<_>; 4]>::from(srgb)[..],
774            &[white_srgb, red_srgb, green_srgb, blue_srgb][..],
775            epsilon = 0.00001
776        );
777    }
778}