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}