color/
colorspace.rs

1// Copyright 2024 the Color Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4#[cfg(feature = "std")]
5extern crate std;
6
7use core::any::TypeId;
8
9use crate::{matvecmul, tag::ColorSpaceTag, Chromaticity};
10
11#[cfg(all(not(feature = "std"), not(test)))]
12use crate::floatfuncs::FloatFuncs;
13
14/// The main trait for color spaces.
15///
16/// This can be implemented by clients for conversions in and out of
17/// new color spaces. It is expected to be a zero-sized type.
18///
19/// The [linear sRGB](`LinearSrgb`) color space is central, and other
20/// color spaces are defined as conversions in and out of that. A color
21/// space does not explicitly define a gamut, so generally conversions
22/// will succeed and round-trip, subject to numerical precision.
23///
24/// White point is handled implicitly in the general conversion methods. For color spaces with a
25/// white point other than D65 (the native white point for sRGB), use a linear Bradford chromatic
26/// adaptation, following CSS Color 4. The conversion methods suffixed with `_absolute` do not
27/// perform chromatic adaptation.
28///
29/// See the [XYZ-D65 color space](`XyzD65`) documentation for some
30/// background information on color spaces.
31///
32/// # Implementing `ColorSpace`
33///
34/// When implementing a custom color space, take care to set the associated constants correctly.
35/// The following is an example implementation of the
36/// [Rec. 709](https://www.color.org/chardata/rgb/BT2020.xalter) color space.
37///
38/// **Note:**
39/// - [`ColorSpace::convert`] can be implemented to specialize specific conversions;
40/// - implement [`ColorSpace::scale_chroma`] if your color space has a natural representation of
41///   chroma.
42///
43/// ```rust
44/// use color::{ColorSpace, ColorSpaceLayout};
45///
46/// /// The Rec. 709 color space, using the electro-optical transfer function
47/// /// defined in ITU-R BT.1886.
48/// ///
49/// /// Rec. 709 is very similar to sRGB, having the same natural gamut, but
50/// /// does have a different transfer function.
51/// ///
52/// /// See https://www.color.org/chardata/rgb/BT709.xalter.
53/// #[derive(Clone, Copy, Debug)]
54/// pub struct Rec709;
55///
56/// impl ColorSpace for Rec709 {
57///     const IS_LINEAR: bool = false;
58///
59///     const LAYOUT: ColorSpaceLayout = ColorSpaceLayout::Rectangular;
60///
61///     const WHITE_COMPONENTS: [f32; 3] = [1., 1., 1.];
62///
63///     fn to_linear_srgb(src: [f32; 3]) -> [f32; 3] {
64///         src.map(|x| x.powf(2.4))
65///     }
66///
67///     fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] {
68///         src.map(|x| x.powf(1. / 2.4))
69///     }
70///
71///     fn clip([r, g, b]: [f32; 3]) -> [f32; 3] {
72///         [r.clamp(0., 1.), g.clamp(0., 1.), b.clamp(0., 1.)]
73///     }
74/// }
75/// ```
76pub trait ColorSpace: Clone + Copy + 'static {
77    /// Whether the color space is linear.
78    ///
79    /// Calculations in linear color spaces can sometimes be simplified,
80    /// for example it is not necessary to undo premultiplication when
81    /// converting.
82    const IS_LINEAR: bool = false;
83
84    /// The layout of the color space.
85    ///
86    /// The layout primarily identifies the hue channel for cylindrical
87    /// color spaces, which is important because hue is not premultiplied.
88    const LAYOUT: ColorSpaceLayout = ColorSpaceLayout::Rectangular;
89
90    /// The tag corresponding to this color space, if a matching tag exists.
91    const TAG: Option<ColorSpaceTag> = None;
92
93    /// The white point of the color space.
94    ///
95    /// See the [XYZ-D65 color space](`XyzD65`) documentation for some background information on
96    /// the meaning of "white point."
97    const WHITE_POINT: Chromaticity = Chromaticity::D65;
98
99    /// The component values for the color white within this color space.
100    const WHITE_COMPONENTS: [f32; 3];
101
102    /// Convert an opaque color to linear sRGB.
103    ///
104    /// Values are likely to exceed [0, 1] for wide-gamut and HDR colors.
105    ///
106    /// This performs chromatic adaptation from the source color space's reference white to the
107    /// target color space's reference white; see the [XYZ-D65 color space](`XyzD65`) documentation
108    /// for some background information on the meaning of "reference white." Use
109    /// [`ColorSpace::to_linear_srgb_absolute`] to convert the absolute color instead.
110    fn to_linear_srgb(src: [f32; 3]) -> [f32; 3];
111
112    /// Convert an opaque color from linear sRGB.
113    ///
114    /// In general, this method should not do any gamut clipping.
115    fn from_linear_srgb(src: [f32; 3]) -> [f32; 3];
116
117    /// Convert to a different color space.
118    ///
119    /// The default implementation is a no-op if the color spaces
120    /// are the same, otherwise converts from the source to linear
121    /// sRGB, then from that to the target. Implementations are
122    /// encouraged to specialize further (using the [`TypeId`] of
123    /// the color spaces), effectively finding a shortest path in
124    /// the conversion graph.
125    fn convert<TargetCS: ColorSpace>(src: [f32; 3]) -> [f32; 3] {
126        if TypeId::of::<Self>() == TypeId::of::<TargetCS>() {
127            src
128        } else {
129            let lin_rgb = Self::to_linear_srgb(src);
130            TargetCS::from_linear_srgb(lin_rgb)
131        }
132    }
133
134    /// Convert an opaque color to linear sRGB, without chromatic adaptation.
135    ///
136    /// For most use-cases you should consider using the chromatically-adapting
137    /// [`ColorSpace::to_linear_srgb`] instead.
138    ///
139    /// Values are likely to exceed [0, 1] for wide-gamut and HDR colors.
140    ///
141    /// This does not perform chromatic adaptation from the source color space's reference white to
142    /// sRGB's standard reference white; thereby representing the same absolute color in sRGB. See
143    /// the [XYZ-D65 color space](`XyzD65`) documentation for some background information on the
144    /// meaning of "reference white."
145    ///
146    /// # Note to implementers
147    ///
148    /// The default implementation undoes the chromatic adaptation performed by
149    /// [`ColorSpace::to_linear_srgb`]. This can be overridden for better performance and greater
150    /// calculation accuracy.
151    fn to_linear_srgb_absolute(src: [f32; 3]) -> [f32; 3] {
152        let lin_srgb = Self::to_linear_srgb(src);
153        if Self::WHITE_POINT == Chromaticity::D65 {
154            lin_srgb
155        } else {
156            let lin_srgb_adaptation_matrix = const {
157                Chromaticity::D65.linear_srgb_chromatic_adaptation_matrix(Self::WHITE_POINT)
158            };
159            matvecmul(&lin_srgb_adaptation_matrix, lin_srgb)
160        }
161    }
162
163    /// Convert an opaque color from linear sRGB, without chromatic adaptation.
164    ///
165    /// For most use-cases you should consider using the chromatically-adapting
166    /// [`ColorSpace::from_linear_srgb`] instead.
167    ///
168    /// In general, this method should not do any gamut clipping.
169    ///
170    /// This does not perform chromatic adaptation to the destination color space's reference white
171    /// from sRGB's standard reference white; thereby representing the same absolute color in the
172    /// target color space. See the [XYZ-D65 color space](`XyzD65`) documentation for some
173    /// background information on the meaning of "reference white."
174    ///
175    /// # Note to implementers
176    ///
177    /// The default implementation undoes the chromatic adaptation performed by
178    /// [`ColorSpace::from_linear_srgb`]. This can be overridden for better performance and greater
179    /// calculation accuracy.
180    fn from_linear_srgb_absolute(src: [f32; 3]) -> [f32; 3] {
181        let lin_srgb_adapted = if Self::WHITE_POINT == Chromaticity::D65 {
182            src
183        } else {
184            let lin_srgb_adaptation_matrix = const {
185                Self::WHITE_POINT.linear_srgb_chromatic_adaptation_matrix(Chromaticity::D65)
186            };
187            matvecmul(&lin_srgb_adaptation_matrix, src)
188        };
189        Self::from_linear_srgb(lin_srgb_adapted)
190    }
191
192    /// Convert to a different color space, without chromatic adaptation.
193    ///
194    /// For most use-cases you should consider using the chromatically-adapting
195    /// [`ColorSpace::convert`] instead.
196    ///
197    /// This does not perform chromatic adaptation from the source color space's reference white to
198    /// the destination color space's reference white; thereby representing the same absolute color
199    /// in the destination color space. See the [XYZ-D65 color space](`XyzD65`) documentation for
200    /// some background information on the meaning of "reference white."
201    ///
202    /// The default implementation is a no-op if the color spaces are the same, otherwise converts
203    /// from the source to linear sRGB, then from that to the target, without chromatic adaptation.
204    /// Implementations are encouraged to specialize further (using the [`TypeId`] of the color
205    /// spaces), effectively finding a shortest path in the conversion graph.
206    fn convert_absolute<TargetCS: ColorSpace>(src: [f32; 3]) -> [f32; 3] {
207        if TypeId::of::<Self>() == TypeId::of::<TargetCS>() {
208            src
209        } else {
210            let lin_rgb = Self::to_linear_srgb_absolute(src);
211            TargetCS::from_linear_srgb_absolute(lin_rgb)
212        }
213    }
214
215    /// Chromatically adapt the color between the given white point chromaticities.
216    ///
217    /// The color is assumed to be under a reference white point of `from` and is chromatically
218    /// adapted to the given white point `to`. The linear Bradford transform is used to perform the
219    /// chromatic adaptation.
220    fn chromatically_adapt(src: [f32; 3], from: Chromaticity, to: Chromaticity) -> [f32; 3] {
221        if from == to {
222            return src;
223        }
224
225        let lin_srgb_adaptation_matrix = if from == Chromaticity::D65 && to == Chromaticity::D50 {
226            Chromaticity::D65.linear_srgb_chromatic_adaptation_matrix(Chromaticity::D50)
227        } else if from == Chromaticity::D50 && to == Chromaticity::D65 {
228            Chromaticity::D50.linear_srgb_chromatic_adaptation_matrix(Chromaticity::D65)
229        } else {
230            from.linear_srgb_chromatic_adaptation_matrix(to)
231        };
232
233        let lin_srgb_adapted = matvecmul(
234            &lin_srgb_adaptation_matrix,
235            Self::to_linear_srgb_absolute(src),
236        );
237        Self::from_linear_srgb_absolute(lin_srgb_adapted)
238    }
239
240    /// Scale the chroma by the given amount.
241    ///
242    /// In color spaces with a natural representation of chroma, scale
243    /// directly. In other color spaces, equivalent results as scaling
244    /// chroma in Oklab.
245    fn scale_chroma(src: [f32; 3], scale: f32) -> [f32; 3] {
246        let rgb = Self::to_linear_srgb(src);
247        let scaled = LinearSrgb::scale_chroma(rgb, scale);
248        Self::from_linear_srgb(scaled)
249    }
250
251    /// Clip the color's components to fit within the natural gamut of the color space.
252    ///
253    /// There are many possible ways to map colors outside of a color space's gamut to colors
254    /// inside the gamut. Some methods are perceptually better than others (for example, preserving
255    /// the mapped color's hue is usually preferred over preserving saturation). This method will
256    /// generally do the mathematically simplest thing, namely clamping the individual color
257    /// components' values to the color space's natural limits of those components, bringing
258    /// out-of-gamut colors just onto the gamut boundary. The resultant color may be perceptually
259    /// quite distinct from the original color.
260    ///
261    /// # Examples
262    ///
263    /// ```rust
264    /// use color::{ColorSpace, Srgb, XyzD65};
265    ///
266    /// assert_eq!(Srgb::clip([0.4, -0.2, 1.2]), [0.4, 0., 1.]);
267    /// assert_eq!(XyzD65::clip([0.4, -0.2, 1.2]), [0.4, -0.2, 1.2]);
268    /// ```
269    fn clip(src: [f32; 3]) -> [f32; 3];
270}
271
272/// The layout of a color space, particularly the hue component.
273#[derive(Clone, Copy, PartialEq, Eq, Debug)]
274#[non_exhaustive]
275pub enum ColorSpaceLayout {
276    /// Rectangular, no hue component.
277    Rectangular,
278    /// Cylindrical, hue is first component.
279    HueFirst,
280    /// Cylindrical, hue is third component.
281    HueThird,
282}
283
284impl ColorSpaceLayout {
285    /// Multiply all components except for hue by scale.
286    ///
287    /// This function is used for both premultiplying and un-premultiplying. See
288    /// §12.3 of Color 4 spec for context.
289    pub(crate) const fn scale(self, components: [f32; 3], scale: f32) -> [f32; 3] {
290        match self {
291            Self::Rectangular => [
292                components[0] * scale,
293                components[1] * scale,
294                components[2] * scale,
295            ],
296            Self::HueFirst => [components[0], components[1] * scale, components[2] * scale],
297            Self::HueThird => [components[0] * scale, components[1] * scale, components[2]],
298        }
299    }
300
301    pub(crate) const fn hue_channel(self) -> Option<usize> {
302        match self {
303            Self::Rectangular => None,
304            Self::HueFirst => Some(0),
305            Self::HueThird => Some(2),
306        }
307    }
308}
309
310/// 🌌 The linear-light RGB color space with [sRGB](`Srgb`) primaries.
311///
312/// This color space is identical to sRGB, having the same components and natural gamut, except
313/// that the transfer function is linear.
314///
315/// Its components are `[r, g, b]` (red, green, and blue channels respectively), with `[0, 0, 0]`
316/// pure black and `[1, 1, 1]` white. The natural bounds of the channels are `[0, 1]`.
317///
318/// This corresponds to the color space in [CSS Color Module Level 4 § 10.3][css-sec].
319///
320/// [css-sec]: https://www.w3.org/TR/css-color-4/#predefined-sRGB-linear
321#[derive(Clone, Copy, Debug)]
322pub struct LinearSrgb;
323
324impl ColorSpace for LinearSrgb {
325    const IS_LINEAR: bool = true;
326
327    const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::LinearSrgb);
328
329    const WHITE_COMPONENTS: [f32; 3] = [1., 1., 1.];
330
331    fn to_linear_srgb(src: [f32; 3]) -> [f32; 3] {
332        src
333    }
334
335    fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] {
336        src
337    }
338
339    fn scale_chroma(src: [f32; 3], scale: f32) -> [f32; 3] {
340        let lms = matvecmul(&OKLAB_SRGB_TO_LMS, src).map(f32::cbrt);
341        let l = OKLAB_LMS_TO_LAB[0];
342        let lightness = l[0] * lms[0] + l[1] * lms[1] + l[2] * lms[2];
343        let lms_scaled = [
344            lightness + scale * (lms[0] - lightness),
345            lightness + scale * (lms[1] - lightness),
346            lightness + scale * (lms[2] - lightness),
347        ];
348        matvecmul(&OKLAB_LMS_TO_SRGB, lms_scaled.map(|x| x * x * x))
349    }
350
351    fn clip([r, g, b]: [f32; 3]) -> [f32; 3] {
352        [r.clamp(0., 1.), g.clamp(0., 1.), b.clamp(0., 1.)]
353    }
354}
355
356impl From<LinearSrgb> for ColorSpaceTag {
357    fn from(_: LinearSrgb) -> Self {
358        Self::LinearSrgb
359    }
360}
361
362/// 🌌 The standard RGB color space.
363///
364/// Its components are `[r, g, b]` (red, green, and blue channels respectively), with `[0, 0, 0]`
365/// pure black and `[1, 1, 1]` white. The natural bounds of the components are `[0, 1]`.
366///
367/// This corresponds to the color space in [CSS Color Module Level 4 § 10.2][css-sec]. It is
368/// defined in IEC 61966-2-1.
369///
370/// [css-sec]: https://www.w3.org/TR/css-color-4/#predefined-sRGB
371#[derive(Clone, Copy, Debug)]
372pub struct Srgb;
373
374fn srgb_to_lin(x: f32) -> f32 {
375    if x.abs() <= 0.04045 {
376        x * (1.0 / 12.92)
377    } else {
378        ((x.abs() + 0.055) * (1.0 / 1.055)).powf(2.4).copysign(x)
379    }
380}
381
382fn lin_to_srgb(x: f32) -> f32 {
383    if x.abs() <= 0.0031308 {
384        x * 12.92
385    } else {
386        (1.055 * x.abs().powf(1.0 / 2.4) - 0.055).copysign(x)
387    }
388}
389
390impl ColorSpace for Srgb {
391    const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::Srgb);
392
393    const WHITE_COMPONENTS: [f32; 3] = [1., 1., 1.];
394
395    fn to_linear_srgb(src: [f32; 3]) -> [f32; 3] {
396        src.map(srgb_to_lin)
397    }
398
399    fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] {
400        src.map(lin_to_srgb)
401    }
402
403    fn convert<TargetCS: ColorSpace>(src: [f32; 3]) -> [f32; 3] {
404        if TypeId::of::<Self>() == TypeId::of::<TargetCS>() {
405            src
406        } else if TypeId::of::<TargetCS>() == TypeId::of::<Hsl>() {
407            rgb_to_hsl(src, true)
408        } else if TypeId::of::<TargetCS>() == TypeId::of::<Hwb>() {
409            rgb_to_hwb(src)
410        } else {
411            let lin_rgb = Self::to_linear_srgb(src);
412            TargetCS::from_linear_srgb(lin_rgb)
413        }
414    }
415
416    fn clip([r, g, b]: [f32; 3]) -> [f32; 3] {
417        [r.clamp(0., 1.), g.clamp(0., 1.), b.clamp(0., 1.)]
418    }
419}
420
421impl From<Srgb> for ColorSpaceTag {
422    fn from(_: Srgb) -> Self {
423        Self::Srgb
424    }
425}
426
427/// 🌌 The Display P3 color space, often used for wide-gamut displays.
428///
429/// Display P3 is similar to [sRGB](`Srgb`) but has higher red and, especially, green
430/// chromaticities, thereby extending its gamut over sRGB on those components.
431///
432/// Its components are `[r, g, b]` (red, green, and blue channels respectively), with `[0, 0, 0]`
433/// pure black and `[1, 1, 1]` white. The natural bounds of the channels are `[0, 1]`.
434///
435/// This corresponds to the color space in [CSS Color Module Level 4 § 10.4][css-sec] and is
436/// [characterized by the ICC][icc]. Display P3 is a variant of the DCI-P3 color space
437/// described in [SMPTE EG 432-1:2010][smpte].
438///
439/// [css-sec]: https://www.w3.org/TR/css-color-4/#predefined-display-p3
440/// [icc]: https://www.color.org/chardata/rgb/DisplayP3.xalter
441/// [smpte]: https://pub.smpte.org/doc/eg432-1/20101110-pub/eg0432-1-2010.pdf
442#[derive(Clone, Copy, Debug)]
443pub struct DisplayP3;
444
445impl ColorSpace for DisplayP3 {
446    const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::DisplayP3);
447
448    const WHITE_COMPONENTS: [f32; 3] = [1., 1., 1.];
449
450    fn to_linear_srgb(src: [f32; 3]) -> [f32; 3] {
451        const LINEAR_DISPLAYP3_TO_SRGB: [[f32; 3]; 3] = [
452            [1.224_940_2, -0.224_940_18, 0.0],
453            [-0.042_056_955, 1.042_056_9, 0.0],
454            [-0.019_637_555, -0.078_636_04, 1.098_273_6],
455        ];
456        matvecmul(&LINEAR_DISPLAYP3_TO_SRGB, src.map(srgb_to_lin))
457    }
458
459    fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] {
460        const LINEAR_SRGB_TO_DISPLAYP3: [[f32; 3]; 3] = [
461            [0.822_461_96, 0.177_538_04, 0.0],
462            [0.033_194_2, 0.966_805_8, 0.0],
463            [0.017_082_632, 0.072_397_44, 0.910_519_96],
464        ];
465        matvecmul(&LINEAR_SRGB_TO_DISPLAYP3, src).map(lin_to_srgb)
466    }
467
468    fn clip([r, g, b]: [f32; 3]) -> [f32; 3] {
469        [r.clamp(0., 1.), g.clamp(0., 1.), b.clamp(0., 1.)]
470    }
471}
472
473impl From<DisplayP3> for ColorSpaceTag {
474    fn from(_: DisplayP3) -> Self {
475        Self::DisplayP3
476    }
477}
478
479/// 🌌 The Adobe RGB (1998) color space.
480///
481/// Adobe RGB is similar to [sRGB](`Srgb`) but has higher green chromaticity, thereby extending its
482/// gamut over sRGB on that component. It was developed to encompass typical color print gamuts.
483///
484/// Its components are `[r, g, b]` (red, green, and blue channels respectively), with `[0, 0, 0]`
485/// pure black and `[1, 1, 1]` white. The natural bounds of the channels are `[0, 1]`.
486///
487/// This corresponds to the color space in [CSS Color Module Level 4 § 10.5][css-sec] and is
488/// [characterized by the ICC][icc]. Adobe RGB is described [here][adobe] by Adobe.
489///
490/// [css-sec]: https://www.w3.org/TR/css-color-4/#predefined-a98-rgb
491/// [icc]: https://www.color.org/chardata/rgb/adobergb.xalter
492/// [adobe]: https://www.adobe.com/digitalimag/adobergb.html
493#[derive(Clone, Copy, Debug)]
494pub struct A98Rgb;
495
496impl ColorSpace for A98Rgb {
497    const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::A98Rgb);
498
499    const WHITE_COMPONENTS: [f32; 3] = [1., 1., 1.];
500
501    fn to_linear_srgb([r, g, b]: [f32; 3]) -> [f32; 3] {
502        // XYZ_to_lin_sRGB * lin_A98_to_XYZ
503        #[expect(
504            clippy::cast_possible_truncation,
505            reason = "exact rational, truncate at compile-time"
506        )]
507        const LINEAR_A98RGB_TO_SRGB: [[f32; 3]; 3] = [
508            [
509                (66_942_405. / 47_872_228.) as f32,
510                (-19_070_177. / 47_872_228.) as f32,
511                0.,
512            ],
513            [0., 1., 0.],
514            [
515                0.,
516                (-11_512_411. / 268_173_353.) as f32,
517                (279_685_764. / 268_173_353.) as f32,
518            ],
519        ];
520        matvecmul(
521            &LINEAR_A98RGB_TO_SRGB,
522            [r, g, b].map(|x| x.abs().powf(563. / 256.).copysign(x)),
523        )
524    }
525
526    fn from_linear_srgb([r, g, b]: [f32; 3]) -> [f32; 3] {
527        // XYZ_to_lin_A98RGB * lin_sRGB_to_XYZ
528        #[expect(
529            clippy::cast_possible_truncation,
530            reason = "exact rational, truncate at compile-time"
531        )]
532        const LINEAR_SRGB_TO_A98RGB: [[f32; 3]; 3] = [
533            [
534                (47_872_228. / 66_942_405.) as f32,
535                (19_070_177. / 66_942_405.) as f32,
536                0.0,
537            ],
538            [0., 1., 0.],
539            [
540                0.,
541                (11_512_411. / 279_685_764.) as f32,
542                (268_173_353. / 279_685_764.) as f32,
543            ],
544        ];
545        matvecmul(&LINEAR_SRGB_TO_A98RGB, [r, g, b]).map(|x| x.abs().powf(256. / 563.).copysign(x))
546    }
547
548    fn clip([r, g, b]: [f32; 3]) -> [f32; 3] {
549        [r.clamp(0., 1.), g.clamp(0., 1.), b.clamp(0., 1.)]
550    }
551}
552
553impl From<A98Rgb> for ColorSpaceTag {
554    fn from(_: A98Rgb) -> Self {
555        Self::A98Rgb
556    }
557}
558
559/// 🌌 The ProPhoto RGB color space.
560///
561/// ProPhoto RGB is similar to [sRGB](`Srgb`) but has higher red, green and blue chromaticities,
562/// thereby extending its gamut over sRGB on all components. ProPhoto RGB has a reference white of
563/// D50; see the [XYZ-D65 color space](`XyzD65`) documentation for some background information on
564/// the meaning of "reference white."
565///
566/// Its components are `[r, g, b]` (red, green, and blue channels respectively), with `[0, 0, 0]`
567/// pure black and `[1, 1, 1]` white. The natural bounds of the channels are `[0, 1]`.
568///
569/// This corresponds to the color space in [CSS Color Module Level 4 § 10.6][css-sec] and is
570/// [characterized by the ICC][icc].
571///
572/// ProPhoto RGB is also known as ROMM RGB.
573///
574/// [css-sec]: https://www.w3.org/TR/css-color-4/#predefined-prophoto-rgb
575/// [icc]: https://www.color.org/chardata/rgb/rommrgb.xalter
576#[derive(Clone, Copy, Debug)]
577pub struct ProphotoRgb;
578
579impl ProphotoRgb {
580    fn transfer_to_linear(x: f32) -> f32 {
581        if x.abs() <= 16. / 512. {
582            x / 16.
583        } else {
584            x.abs().powf(1.8).copysign(x)
585        }
586    }
587
588    fn transfer_from_linear(x: f32) -> f32 {
589        if x.abs() <= 1. / 512. {
590            x * 16.
591        } else {
592            x.abs().powf(1. / 1.8).copysign(x)
593        }
594    }
595}
596
597impl ColorSpace for ProphotoRgb {
598    const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::ProphotoRgb);
599
600    const WHITE_POINT: Chromaticity = Chromaticity::D50;
601    const WHITE_COMPONENTS: [f32; 3] = [1., 1., 1.];
602
603    fn to_linear_srgb([r, g, b]: [f32; 3]) -> [f32; 3] {
604        // XYZ_to_lin_sRGB * D50_to_D65 * lin_prophoto_to_XYZ
605        const LINEAR_PROPHOTORGB_TO_SRGB: [[f32; 3]; 3] = [
606            [2.034_367_6, -0.727_634_5, -0.306_733_07],
607            [-0.228_826_79, 1.231_753_3, -0.002_926_598],
608            [-0.008_558_424, -0.153_268_2, 1.161_826_6],
609        ];
610
611        matvecmul(
612            &LINEAR_PROPHOTORGB_TO_SRGB,
613            [r, g, b].map(Self::transfer_to_linear),
614        )
615    }
616
617    fn from_linear_srgb([r, g, b]: [f32; 3]) -> [f32; 3] {
618        // XYZ_to_lin_prophoto * D65_to_D50 * lin_sRGB_to_XYZ
619        const LINEAR_SRGB_TO_PROPHOTORGB: [[f32; 3]; 3] = [
620            [0.529_280_4, 0.330_153, 0.140_566_6],
621            [0.098_366_22, 0.873_463_9, 0.028_169_824],
622            [0.016_875_342, 0.117_659_41, 0.865_465_2],
623        ];
624
625        matvecmul(&LINEAR_SRGB_TO_PROPHOTORGB, [r, g, b]).map(Self::transfer_from_linear)
626    }
627
628    fn to_linear_srgb_absolute([r, g, b]: [f32; 3]) -> [f32; 3] {
629        // XYZ_to_lin_sRGB * lin_prophoto_to_XYZ
630        const LINEAR_PROPHOTORGB_TO_SRGB: [[f32; 3]; 3] = [
631            [
632                11_822_636_894_621. / 5_517_784_378_314.,
633                -2_646_118_971_832. / 4_032_227_045_691.,
634                -2_824_985_149. / 9_114_754_233.,
635            ],
636            [
637                -270_896_603_412_176. / 1_163_584_209_404_097.,
638                107_798_623_831_136. / 89_506_477_646_469.,
639                822_014_396. / 202_327_283_847.,
640            ],
641            [
642                -2412976100974. / 167_796_255_001_401.,
643                -1_777_081_293_536. / 12_907_404_230_877.,
644                879_168_464. / 1_006_099_419.,
645            ],
646        ];
647
648        matvecmul(
649            &LINEAR_PROPHOTORGB_TO_SRGB,
650            [r, g, b].map(Self::transfer_to_linear),
651        )
652    }
653
654    fn from_linear_srgb_absolute([r, g, b]: [f32; 3]) -> [f32; 3] {
655        // XYZ_to_lin_prophoto * lin_sRGB_to_XYZ
656        const LINEAR_SRGB_TO_PROPHOTORGB: [[f32; 3]; 3] = [
657            [
658                7_356_071_250_722. / 14_722_127_359_275.,
659                25_825_157_007_599. / 88_332_764_155_650.,
660                1_109_596_896_521. / 6_309_483_153_975.,
661            ],
662            [
663                170_513_936_009. / 1_766_822_975_400.,
664                18_792_073_269_331. / 21_201_875_704_800.,
665                91_195_554_323. / 3_028_839_386_400.,
666            ],
667            [
668                946_201. / 40_387_053.,
669                105_017_795. / 726_966_954.,
670                8_250_997. / 7_162_236.,
671            ],
672        ];
673
674        matvecmul(&LINEAR_SRGB_TO_PROPHOTORGB, [r, g, b]).map(Self::transfer_from_linear)
675    }
676
677    fn clip([r, g, b]: [f32; 3]) -> [f32; 3] {
678        [r.clamp(0., 1.), g.clamp(0., 1.), b.clamp(0., 1.)]
679    }
680}
681
682impl From<ProphotoRgb> for ColorSpaceTag {
683    fn from(_: ProphotoRgb) -> Self {
684        Self::ProphotoRgb
685    }
686}
687
688/// 🌌 The Rec. 2020 color space.
689///
690/// Rec. 2020 is similar to [sRGB](`Srgb`) but has higher red, green and blue chromaticities,
691/// thereby extending its gamut over sRGB on all components.
692///
693/// Its components are `[r, g, b]` (red, green, and blue channels respectively), with `[0, 0, 0]`
694/// pure black and `[1, 1, 1]` white. The natural bounds of the channels are `[0, 1]`.
695///
696/// This corresponds to the color space in [CSS Color Module Level 4 § 10.7][css-sec] and is
697/// [characterized by the ICC][icc]. The color space is defined by the International
698/// Telecommunication Union [here][itu].
699///
700/// [css-sec]: https://www.w3.org/TR/css-color-4/#predefined-rec2020
701/// [icc]: https://www.color.org/chardata/rgb/BT2020.xalter
702/// [itu]: https://www.itu.int/rec/R-REC-BT.2020/en
703#[derive(Clone, Copy, Debug)]
704pub struct Rec2020;
705
706impl Rec2020 {
707    // These are the parameters of the transfer function defined in the Rec. 2020 specification.
708    // They are truncated here to f32 precision.
709    const A: f32 = 1.099_296_8;
710    const B: f32 = 0.018_053_97;
711}
712
713impl ColorSpace for Rec2020 {
714    const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::Rec2020);
715
716    const WHITE_COMPONENTS: [f32; 3] = [1., 1., 1.];
717
718    fn to_linear_srgb([r, g, b]: [f32; 3]) -> [f32; 3] {
719        // XYZ_to_lin_sRGB * lin_Rec2020_to_XYZ
720        #[expect(
721            clippy::cast_possible_truncation,
722            reason = "exact rational, truncate at compile-time"
723        )]
724        const LINEAR_REC2020_TO_SRGB: [[f32; 3]; 3] = [
725            [
726                (2_785_571_537. / 1_677_558_947.) as f32,
727                (-985_802_650. / 1_677_558_947.) as f32,
728                (-122_209_940. / 1_677_558_947.) as f32,
729            ],
730            [
731                (-4_638_020_506. / 37_238_079_773.) as f32,
732                (42_187_016_744. / 37_238_079_773.) as f32,
733                (-310_916_465. / 37_238_079_773.) as f32,
734            ],
735            [
736                (-97_469_024. / 5_369_968_309.) as f32,
737                (-3_780_738_464. / 37_589_778_163.) as f32,
738                (42_052_799_795. / 37_589_778_163.) as f32,
739            ],
740        ];
741
742        fn transfer(x: f32) -> f32 {
743            if x.abs() < Rec2020::B * 4.5 {
744                x * (1. / 4.5)
745            } else {
746                ((x.abs() + (Rec2020::A - 1.)) / Rec2020::A)
747                    .powf(1. / 0.45)
748                    .copysign(x)
749            }
750        }
751
752        matvecmul(&LINEAR_REC2020_TO_SRGB, [r, g, b].map(transfer))
753    }
754
755    fn from_linear_srgb([r, g, b]: [f32; 3]) -> [f32; 3] {
756        // XYZ_to_lin_Rec2020 * lin_sRGB_to_XYZ
757        #[expect(
758            clippy::cast_possible_truncation,
759            reason = "exact rational, truncate at compile-time"
760        )]
761        const LINEAR_SRGB_TO_REC2020: [[f32; 3]; 3] = [
762            [
763                (2_939_026_994. / 4_684_425_795.) as f32,
764                (9_255_011_753. / 28_106_554_770.) as f32,
765                (173_911_579. / 4_015_222_110.) as f32,
766            ],
767            [
768                (76_515_593. / 1_107_360_270.) as f32,
769                (6_109_575_001. / 6_644_161_620.) as f32,
770                (75_493_061. / 6_644_161_620.) as f32,
771            ],
772            [
773                (12_225_392. / 745_840_075.) as f32,
774                (1_772_384_008. / 20_137_682_025.) as f32,
775                (18_035_212_433. / 20_137_682_025.) as f32,
776            ],
777        ];
778
779        fn transfer(x: f32) -> f32 {
780            if x.abs() < Rec2020::B {
781                x * 4.5
782            } else {
783                (Rec2020::A * x.abs().powf(0.45) - (Rec2020::A - 1.)).copysign(x)
784            }
785        }
786        matvecmul(&LINEAR_SRGB_TO_REC2020, [r, g, b]).map(transfer)
787    }
788
789    fn clip([r, g, b]: [f32; 3]) -> [f32; 3] {
790        [r.clamp(0., 1.), g.clamp(0., 1.), b.clamp(0., 1.)]
791    }
792}
793
794impl From<Rec2020> for ColorSpaceTag {
795    fn from(_: Rec2020) -> Self {
796        Self::Rec2020
797    }
798}
799
800/// 🌌 The ACES2065-1 color space.
801///
802/// This is a linear color space with a very wide gamut. It is is often used for archival and
803/// interchange.
804///
805/// Its components are `[r, g, b]` (red, green, and blue channels respectively), with `[0, 0, 0]`
806/// pure black and `[1, 1, 1]` white. The natural bounds of the components are
807/// `[-65504.0, 65504.0]`.
808///
809/// This color space is [characterized by the Academy Color Encoding System][aces20651] and is
810/// specified in [SMPTE ST 2065-1:2021][smpte].
811///
812/// ACES2065-1 has a reference white [near D60][aceswp]; see the [XYZ-D65 color space](`XyzD65`)
813/// documentation for some background information on the meaning of "reference white."
814///
815/// See also [`AcesCg`].
816///
817/// [aces20651]: https://draftdocs.acescentral.com/specifications/encodings/aces2065-1/
818/// [smpte]: https://pub.smpte.org/doc/st2065-1/20200909-pub/st2065-1-2021.pdf
819/// [aceswp]: https://docs.acescentral.com/tb/white-point
820#[derive(Clone, Copy, Debug)]
821pub struct Aces2065_1;
822
823impl ColorSpace for Aces2065_1 {
824    const IS_LINEAR: bool = true;
825
826    const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::Aces2065_1);
827
828    const WHITE_POINT: Chromaticity = Chromaticity::ACES;
829    const WHITE_COMPONENTS: [f32; 3] = [1.0, 1.0, 1.0];
830
831    fn to_linear_srgb(src: [f32; 3]) -> [f32; 3] {
832        // XYZ_to_lin_sRGB * ACESwp_to_D65 * ACES2065_1_to_XYZ
833        const ACES2065_1_TO_LINEAR_SRGB: [[f32; 3]; 3] = [
834            [2.521_686, -1.134_131, -0.387_555_2],
835            [-0.276_479_9, 1.372_719, -0.096_239_17],
836            [-0.015_378_065, -0.152_975_34, 1.168_353_4],
837        ];
838        matvecmul(&ACES2065_1_TO_LINEAR_SRGB, src)
839    }
840
841    fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] {
842        // XYZ_to_ACES2065_1 * D65_to_ACESwp * lin_sRGB_to_XYZ
843        const LINEAR_SRGB_TO_ACES2065_1: [[f32; 3]; 3] = [
844            [0.439_632_98, 0.382_988_7, 0.177_378_33],
845            [0.089_776_44, 0.813_439_4, 0.096_784_13],
846            [0.017_541_17, 0.111_546_55, 0.870_912_25],
847        ];
848        matvecmul(&LINEAR_SRGB_TO_ACES2065_1, src)
849    }
850
851    fn to_linear_srgb_absolute(src: [f32; 3]) -> [f32; 3] {
852        // XYZ_to_lin_sRGB * ACES2065_1_to_XYZ
853        const ACES2065_1_TO_LINEAR_SRGB: [[f32; 3]; 3] = [
854            [
855                54_120_196_967_290_615. / 21_154_043_450_084_358.,
856                -320_017_885_460_000. / 285_865_452_028_167.,
857                -564_067_687_050. / 1_439_638_182_257.,
858            ],
859            [
860                -65_267_199_138_999_760. / 234_786_371_866_236_861.,
861                320_721_924_808_012_000. / 234_786_371_866_236_861.,
862                -2_987_552_619_450. / 31_956_767_642_063.,
863            ],
864            [
865                -581_359_048_862_990. / 33_857_690_407_037_013.,
866                -457_168_407_800_000. / 3_077_971_855_185_183.,
867                4_981_730_664_150. / 4_608_369_457_879.,
868            ],
869        ];
870        matvecmul(&ACES2065_1_TO_LINEAR_SRGB, src)
871    }
872
873    fn from_linear_srgb_absolute(src: [f32; 3]) -> [f32; 3] {
874        // XYZ_to_ACES2065_1 * lin_sRGB_to_XYZ
875        const LINEAR_SRGB_TO_ACES2065_1: [[f32; 3]; 3] = [
876            [
877                26_324_697_889_654. / 60_805_826_029_215.,
878                95_867_335_448_462. / 255_384_469_322_703.,
879                34_545_867_731_048. / 182_417_478_087_645.,
880            ],
881            [
882                1_068_725_544_495_979. / 11_952_668_021_931_000.,
883                9_008_998_273_654_297. / 11_033_232_020_244_000.,
884                2_110_950_307_239_113. / 20_490_288_037_596_000.,
885            ],
886            [
887                267_367_106. / 13_953_194_325.,
888                2_967_477_727. / 25_115_749_785.,
889                33_806_406_089. / 35_879_642_550.,
890            ],
891        ];
892        matvecmul(&LINEAR_SRGB_TO_ACES2065_1, src)
893    }
894
895    fn clip([r, g, b]: [f32; 3]) -> [f32; 3] {
896        [
897            r.clamp(-65504., 65504.),
898            g.clamp(-65504., 65504.),
899            b.clamp(-65504., 65504.),
900        ]
901    }
902}
903
904impl From<Aces2065_1> for ColorSpaceTag {
905    fn from(_: Aces2065_1) -> Self {
906        Self::Aces2065_1
907    }
908}
909
910/// 🌌 The ACEScg color space.
911///
912/// The ACEScg color space is a linear color space. The wide gamut makes this color space useful as
913/// a working space for computer graphics.
914///
915/// Its components are `[r, g, b]` (red, green, and blue channels respectively), with `[0, 0, 0]`
916/// pure black and `[1, 1, 1]` white. The natural bounds of the components are
917/// `[-65504.0, 65504.0]`, though it is unusual to clip in this color space.
918///
919/// This color space is defined by the Academy Color Encoding System [specification][acescg].
920///
921/// ACEScg has a reference white [near D60][aceswp]; see the [XYZ-D65 color space](`XyzD65`)
922/// documentation for some background information on the meaning of "reference white."
923///
924/// See also [`Aces2065_1`].
925///
926/// [acescg]: https://docs.acescentral.com/specifications/acescg/
927/// [aceswp]: https://docs.acescentral.com/tb/white-point
928#[derive(Clone, Copy, Debug)]
929pub struct AcesCg;
930
931impl ColorSpace for AcesCg {
932    const IS_LINEAR: bool = true;
933
934    const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::AcesCg);
935
936    const WHITE_POINT: Chromaticity = Chromaticity::ACES;
937    const WHITE_COMPONENTS: [f32; 3] = [1.0, 1.0, 1.0];
938
939    fn to_linear_srgb(src: [f32; 3]) -> [f32; 3] {
940        // XYZ_to_lin_sRGB * ACESwp_to_D65 * ACEScg_to_XYZ
941        const ACESCG_TO_LINEAR_SRGB: [[f32; 3]; 3] = [
942            [1.705_051, -0.621_792_14, -0.083_258_875],
943            [-0.130_256_41, 1.140_804_8, -0.010_548_319],
944            [-0.024_003_357, -0.128_968_97, 1.152_972_3],
945        ];
946        matvecmul(&ACESCG_TO_LINEAR_SRGB, src)
947    }
948
949    fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] {
950        // XYZ_to_ACEScg * D65_to_ACESwp * lin_sRGB_to_XYZ
951        const LINEAR_SRGB_TO_ACESCG: [[f32; 3]; 3] = [
952            [0.613_097_4, 0.339_523_14, 0.047_379_453],
953            [0.070_193_72, 0.916_353_9, 0.013_452_399],
954            [0.020_615_593, 0.109_569_77, 0.869_814_63],
955        ];
956        matvecmul(&LINEAR_SRGB_TO_ACESCG, src)
957    }
958
959    fn to_linear_srgb_absolute(src: [f32; 3]) -> [f32; 3] {
960        // XYZ_to_lin_sRGB * ACEScg_to_XYZ
961        const ACESCG_TO_LINEAR_SRGB: [[f32; 3]; 3] = [
962            [
963                9_932_023_100_445. / 5_736_895_993_442.,
964                -1_732_666_183_650. / 2_868_447_996_721.,
965                -229_784_797_280. / 2_868_447_996_721.,
966            ],
967            [
968                -194_897_543_280. / 1_480_771_385_773.,
969                72_258_955_647_750. / 63_673_169_588_239.,
970                -552_646_980_800. / 63_673_169_588_239.,
971            ],
972            [
973                -68_657_089_110. / 2_794_545_067_783.,
974                -8082548957250. / 64_274_536_559_009.,
975                14_669_805_440. / 13_766_231_861.,
976            ],
977        ];
978        matvecmul(&ACESCG_TO_LINEAR_SRGB, src)
979    }
980
981    fn from_linear_srgb_absolute(src: [f32; 3]) -> [f32; 3] {
982        // XYZ_to_ACEScg * lin_sRGB_to_XYZ
983        const LINEAR_SRGB_TO_ACESCG: [[f32; 3]; 3] = [
984            [
985                2_095_356_009_722. / 3_474_270_183_447.,
986                17_006_614_853_437. / 52_114_052_751_705.,
987                71_464_174_897. / 1_488_972_935_763.,
988            ],
989            [
990                1_774_515_482_522. / 25_307_573_950_575.,
991                69_842_555_782_672. / 75_922_721_851_725.,
992                276_870_186_577. / 21_692_206_243_350.,
993            ],
994            [
995                101_198_449_621. / 4_562_827_993_584.,
996                31_778_718_978_443. / 273_769_679_615_040.,
997                1_600_138_878_851. / 1_700_432_792_640.,
998            ],
999        ];
1000        matvecmul(&LINEAR_SRGB_TO_ACESCG, src)
1001    }
1002
1003    fn clip([r, g, b]: [f32; 3]) -> [f32; 3] {
1004        [
1005            r.clamp(-65504., 65504.),
1006            g.clamp(-65504., 65504.),
1007            b.clamp(-65504., 65504.),
1008        ]
1009    }
1010}
1011
1012impl From<AcesCg> for ColorSpaceTag {
1013    fn from(_: AcesCg) -> Self {
1014        Self::AcesCg
1015    }
1016}
1017
1018/// 🌌 The CIE XYZ color space with a 2° observer and a reference white of D50.
1019///
1020/// Its components are `[X, Y, Z]`. The components are unbounded, but are usually positive.
1021/// Reference white has a luminance `Y` of 1.
1022///
1023/// This corresponds to the color space in [CSS Color Module Level 4 § 10.8][css-sec]. It is
1024/// defined in CIE 015:2018. Following [CSS Color Module Level 4 § 11][css-chromatic-adaptation],
1025/// the conversion between D50 and D65 white points is done with the standard Bradford linear
1026/// chromatic adaptation transform.
1027///
1028/// See the [XYZ-D65 color space](`XyzD65`) documentation for some background information on color
1029/// spaces.
1030///
1031/// [css-sec]: https://www.w3.org/TR/css-color-4/#predefined-xyz
1032/// [css-chromatic-adaptation]: https://www.w3.org/TR/css-color-4/#color-conversion
1033#[derive(Clone, Copy, Debug)]
1034pub struct XyzD50;
1035
1036impl ColorSpace for XyzD50 {
1037    const IS_LINEAR: bool = true;
1038
1039    const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::XyzD50);
1040
1041    const WHITE_POINT: Chromaticity = Chromaticity::D50;
1042    const WHITE_COMPONENTS: [f32; 3] = [3457. / 3585., 1., 986. / 1195.];
1043
1044    fn to_linear_srgb(src: [f32; 3]) -> [f32; 3] {
1045        // XYZ_to_lin_sRGB * D50_to_D65
1046        const XYZ_TO_LINEAR_SRGB: [[f32; 3]; 3] = [
1047            [3.134_136, -1.617_386, -0.490_662_22],
1048            [-0.978_795_47, 1.916_254_4, 0.033_442_874],
1049            [0.071_955_39, -0.228_976_76, 1.405_386_1],
1050        ];
1051        matvecmul(&XYZ_TO_LINEAR_SRGB, src)
1052    }
1053
1054    fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] {
1055        // D65_to_D50 * lin_sRGB_to_XYZ
1056        const LINEAR_SRGB_TO_XYZ: [[f32; 3]; 3] = [
1057            [0.436_065_73, 0.385_151_5, 0.143_078_42],
1058            [0.222_493_17, 0.716_887, 0.060_619_81],
1059            [0.013_923_922, 0.097_081_326, 0.714_099_35],
1060        ];
1061        matvecmul(&LINEAR_SRGB_TO_XYZ, src)
1062    }
1063
1064    fn clip([x, y, z]: [f32; 3]) -> [f32; 3] {
1065        [x, y, z]
1066    }
1067}
1068
1069impl From<XyzD50> for ColorSpaceTag {
1070    fn from(_: XyzD50) -> Self {
1071        Self::XyzD50
1072    }
1073}
1074
1075/// 🌌 The CIE XYZ color space with a 2° observer and a reference white of D65.
1076///
1077/// Its components are `[X, Y, Z]`. The components are unbounded, but are usually positive.
1078/// Reference white has a luminance `Y` of 1.
1079///
1080/// This corresponds to the color space in [CSS Color Module Level 4 § 10.8][css-sec]. It is
1081/// defined in CIE 015:2018. Following [CSS Color Module Level 4 § 11][css-chromatic-adaptation],
1082/// the conversion between D50 and D65 white points is done with the standard Bradford linear
1083/// chromatic adaptation transform.
1084///
1085/// # Human color vision and color spaces
1086///
1087/// Human color vision uses three types of photoreceptive cell in the eye that are sensitive to
1088/// light. These cells have their peak sensitivity at different wavelengths of light: roughly 570
1089/// nm, 535 nm and 430 nm, usually named Long, Medium and Short (LMS) respectively. The cells'
1090/// sensitivities to light taper off as the wavelength moves away from their peaks, but all three
1091/// cells overlap in wavelength sensitivity.
1092///
1093/// Visible light with a combination of wavelengths at specific intensities (the light's *spectral
1094/// density*), causes excitation of these three cell types in varying amounts. The human brain
1095/// interprets this as a specific color at a certain luminosity. Importantly, humans do not
1096/// directly perceive the light's wavelength: for example, monochromatic light with a wavelength of
1097/// 580 nm is perceived as "yellow," and light made up of two wavelengths at roughly 550nm
1098/// ("green") and 610 nm ("red") is also perceived as "yellow."
1099///
1100/// The CIE XYZ color space is an experimentally-obtained mapping of monochromatic light at a
1101/// specific wavelength to the response of human L, M and S photoreceptive cells (with some
1102/// additional mathematically desirable properties). Light of a specific spectral density maps onto
1103/// a specific coordinate in the XYZ color space. Light of a different spectral density that maps
1104/// onto the same XYZ coordinate is predicted by the color space to be perceived as the same
1105/// color and luminosity.
1106///
1107/// The XYZ color space is often used in the characterization of other color spaces.
1108///
1109/// ## White point
1110///
1111/// An important concept in color spaces is the *white point*. Whereas pure black is the absence of
1112/// illumination and has a natural representation in additive color spaces, white is more difficult
1113/// to define. CIE D65 defines white as the perceived color of diffuse standard noon daylight
1114/// perfectly reflected off a surface observed under some foveal angle; here 2°.
1115///
1116/// In many color spaces, their white point is the brightest illumination they can naturally
1117/// represent.
1118///
1119/// For further reading, the [Wikipedia article on the CIE XYZ color space][wikipedia-cie] provides
1120/// a good introduction to color theory as relevant to color spaces.
1121///
1122/// [css-sec]: https://www.w3.org/TR/css-color-4/#predefined-xyz
1123/// [css-chromatic-adaptation]: https://www.w3.org/TR/css-color-4/#color-conversion
1124/// [wikipedia-cie]: https://en.wikipedia.org/wiki/CIE_1931_color_space
1125#[derive(Clone, Copy, Debug)]
1126pub struct XyzD65;
1127
1128impl ColorSpace for XyzD65 {
1129    const IS_LINEAR: bool = true;
1130
1131    const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::XyzD65);
1132
1133    const WHITE_COMPONENTS: [f32; 3] = [3127. / 3290., 1., 3583. / 3290.];
1134
1135    fn to_linear_srgb(src: [f32; 3]) -> [f32; 3] {
1136        const XYZ_TO_LINEAR_SRGB: [[f32; 3]; 3] = [
1137            [12_831. / 3_959., -329. / 214., -1_974. / 3_959.],
1138            [
1139                -851_781. / 878_810.,
1140                1_648_619. / 878_810.,
1141                36_519. / 878_810.,
1142            ],
1143            [705. / 12_673., -2_585. / 12_673., 705. / 667.],
1144        ];
1145        matvecmul(&XYZ_TO_LINEAR_SRGB, src)
1146    }
1147
1148    fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] {
1149        const LINEAR_SRGB_TO_XYZ: [[f32; 3]; 3] = [
1150            [506_752. / 1_228_815., 87_881. / 245_763., 12_673. / 70_218.],
1151            [87_098. / 409_605., 175_762. / 245_763., 12_673. / 175_545.],
1152            [
1153                7_918. / 409_605.,
1154                87_881. / 737_289.,
1155                100_1167. / 1_053_270.,
1156            ],
1157        ];
1158        matvecmul(&LINEAR_SRGB_TO_XYZ, src)
1159    }
1160
1161    fn clip([x, y, z]: [f32; 3]) -> [f32; 3] {
1162        [x, y, z]
1163    }
1164}
1165
1166impl From<XyzD65> for ColorSpaceTag {
1167    fn from(_: XyzD65) -> Self {
1168        Self::XyzD65
1169    }
1170}
1171
1172/// 🌌 The Oklab color space, intended to be a perceptually uniform color space.
1173///
1174/// Its components are `[L, a, b]` with
1175/// - `L` - the lightness with a natural bound between 0 and 1, where 0 represents pure black and 1
1176///   represents the lightness of white;
1177/// - `a` - how green/red the color is; and
1178/// - `b` - how blue/yellow the color is.
1179///
1180/// `a` and `b` are unbounded, but are usually between -0.5 and 0.5.
1181///
1182/// This corresponds to the color space in [CSS Color Module Level 4 § 9.2 ][css-sec]. It is
1183/// defined on [Björn Ottosson's blog][bjorn]. It is similar to the [CIELAB] color space but with
1184/// improved hue constancy.
1185///
1186/// Oklab has a cylindrical counterpart: [Oklch](`Oklch`).
1187///
1188/// [css-sec]: https://www.w3.org/TR/css-color-4/#ok-lab
1189/// [bjorn]: https://bottosson.github.io/posts/oklab/
1190/// [CIELAB]: Lab
1191#[derive(Clone, Copy, Debug)]
1192pub struct Oklab;
1193
1194// Matrices taken from [Oklab] blog post, precision reduced to f32
1195//
1196// [Oklab]: https://bottosson.github.io/posts/oklab/
1197const OKLAB_LAB_TO_LMS: [[f32; 3]; 3] = [
1198    [1.0, 0.396_337_78, 0.215_803_76],
1199    [1.0, -0.105_561_346, -0.063_854_17],
1200    [1.0, -0.089_484_18, -1.291_485_5],
1201];
1202
1203const OKLAB_LMS_TO_SRGB: [[f32; 3]; 3] = [
1204    [4.076_741_7, -3.307_711_6, 0.230_969_94],
1205    [-1.268_438, 2.609_757_4, -0.341_319_38],
1206    [-0.004_196_086_3, -0.703_418_6, 1.707_614_7],
1207];
1208
1209const OKLAB_SRGB_TO_LMS: [[f32; 3]; 3] = [
1210    [0.412_221_46, 0.536_332_55, 0.051_445_995],
1211    [0.211_903_5, 0.680_699_5, 0.107_396_96],
1212    [0.088_302_46, 0.281_718_85, 0.629_978_7],
1213];
1214
1215const OKLAB_LMS_TO_LAB: [[f32; 3]; 3] = [
1216    [0.210_454_26, 0.793_617_8, -0.004_072_047],
1217    [1.977_998_5, -2.428_592_2, 0.450_593_7],
1218    [0.025_904_037, 0.782_771_77, -0.808_675_77],
1219];
1220
1221impl ColorSpace for Oklab {
1222    const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::Oklab);
1223
1224    const WHITE_COMPONENTS: [f32; 3] = [1., 0., 0.];
1225
1226    fn to_linear_srgb(src: [f32; 3]) -> [f32; 3] {
1227        let lms = matvecmul(&OKLAB_LAB_TO_LMS, src).map(|x| x * x * x);
1228        matvecmul(&OKLAB_LMS_TO_SRGB, lms)
1229    }
1230
1231    fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] {
1232        let lms = matvecmul(&OKLAB_SRGB_TO_LMS, src).map(f32::cbrt);
1233        matvecmul(&OKLAB_LMS_TO_LAB, lms)
1234    }
1235
1236    fn scale_chroma([l, a, b]: [f32; 3], scale: f32) -> [f32; 3] {
1237        [l, a * scale, b * scale]
1238    }
1239
1240    fn convert<TargetCS: ColorSpace>(src: [f32; 3]) -> [f32; 3] {
1241        if TypeId::of::<Self>() == TypeId::of::<TargetCS>() {
1242            src
1243        } else if TypeId::of::<TargetCS>() == TypeId::of::<Oklch>() {
1244            lab_to_lch(src)
1245        } else {
1246            let lin_rgb = Self::to_linear_srgb(src);
1247            TargetCS::from_linear_srgb(lin_rgb)
1248        }
1249    }
1250
1251    fn clip([l, a, b]: [f32; 3]) -> [f32; 3] {
1252        [l.clamp(0., 1.), a, b]
1253    }
1254}
1255
1256impl From<Oklab> for ColorSpaceTag {
1257    fn from(_: Oklab) -> Self {
1258        Self::Oklab
1259    }
1260}
1261
1262/// Rectangular to cylindrical conversion.
1263fn lab_to_lch([l, a, b]: [f32; 3]) -> [f32; 3] {
1264    let mut h = b.atan2(a).to_degrees();
1265    if h < 0.0 {
1266        h += 360.0;
1267    }
1268    let c = b.hypot(a);
1269    [l, c, h]
1270}
1271
1272/// Cylindrical to rectangular conversion.
1273fn lch_to_lab([l, c, h]: [f32; 3]) -> [f32; 3] {
1274    let (sin, cos) = h.to_radians().sin_cos();
1275    let a = c * cos;
1276    let b = c * sin;
1277    [l, a, b]
1278}
1279
1280/// 🌌 The cylindrical version of the [Oklab] color space.
1281///
1282/// Its components are `[L, C, h]` with
1283/// - `L` - the lightness as in [`Oklab`];
1284/// - `C` - the chromatic intensity, the natural lower bound of 0 being achromatic, usually not
1285///   exceeding 0.5; and
1286/// - `h` - the hue angle in degrees.
1287#[derive(Clone, Copy, Debug)]
1288pub struct Oklch;
1289
1290impl ColorSpace for Oklch {
1291    const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::Oklch);
1292
1293    const LAYOUT: ColorSpaceLayout = ColorSpaceLayout::HueThird;
1294
1295    const WHITE_COMPONENTS: [f32; 3] = [1., 0., 90.];
1296
1297    fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] {
1298        lab_to_lch(Oklab::from_linear_srgb(src))
1299    }
1300
1301    fn to_linear_srgb(src: [f32; 3]) -> [f32; 3] {
1302        Oklab::to_linear_srgb(lch_to_lab(src))
1303    }
1304
1305    fn scale_chroma([l, c, h]: [f32; 3], scale: f32) -> [f32; 3] {
1306        [l, c * scale, h]
1307    }
1308
1309    fn convert<TargetCS: ColorSpace>(src: [f32; 3]) -> [f32; 3] {
1310        if TypeId::of::<Self>() == TypeId::of::<TargetCS>() {
1311            src
1312        } else if TypeId::of::<TargetCS>() == TypeId::of::<Oklab>() {
1313            lch_to_lab(src)
1314        } else {
1315            let lin_rgb = Self::to_linear_srgb(src);
1316            TargetCS::from_linear_srgb(lin_rgb)
1317        }
1318    }
1319
1320    fn clip([l, c, h]: [f32; 3]) -> [f32; 3] {
1321        [l.clamp(0., 1.), c.max(0.), h]
1322    }
1323}
1324
1325impl From<Oklch> for ColorSpaceTag {
1326    fn from(_: Oklch) -> Self {
1327        Self::Oklch
1328    }
1329}
1330
1331/// 🌌 The CIELAB color space
1332///
1333/// The CIE L\*a\*b\* color space was created in 1976 to be more perceptually
1334/// uniform than RGB color spaces, and is both widely used and the basis of
1335/// other efforts to express colors, including [FreieFarbe].
1336///
1337/// Its components are `[L, a, b]` with
1338/// - `L` - the lightness with a natural bound between 0 and 100, where 0 represents pure black and 100
1339///   represents the lightness of white;
1340/// - `a` - how green/red the color is; and
1341/// - `b` - how blue/yellow the color is.
1342///
1343/// `a` and `b` are unbounded, but are usually between -160 and 160.
1344///
1345/// The color space has poor hue linearity and hue uniformity compared with
1346/// [Oklab], though superior lightness uniformity. Note that the lightness
1347/// range differs from Oklab as well; in Oklab white has a lightness of 1.
1348///
1349/// The CIE L\*a\*b\* color space is defined in terms of a D50 white point. For
1350/// conversion between color spaces with other illuminants (especially D65
1351/// as in sRGB), the standard Bradform linear chromatic adaptation transform
1352/// is used.
1353///
1354/// This corresponds to the color space in [CSS Color Module Level 4 § 9.1 ][css-sec].
1355///
1356/// Lab has a cylindrical counterpart: [Lch].
1357///
1358/// [FreieFarbe]: https://freiefarbe.de/en/
1359/// [css-sec]: https://www.w3.org/TR/css-color-4/#cie-lab
1360#[derive(Clone, Copy, Debug)]
1361pub struct Lab;
1362
1363// Matrices computed from CSS Color 4 spec, then used `cargo clippy --fix`
1364// to reduce precision to f32 and add underscores.
1365
1366// This is D65_to_D50 * lin_sRGB_to_XYZ, then rows scaled by 1 / D50[i].
1367const LAB_SRGB_TO_XYZ: [[f32; 3]; 3] = [
1368    [0.452_211_65, 0.399_412_24, 0.148_376_09],
1369    [0.222_493_17, 0.716_887, 0.060_619_81],
1370    [0.016_875_342, 0.117_659_41, 0.865_465_2],
1371];
1372
1373// This is XYZ_to_lin_sRGB * D50_to_D65, then columns scaled by D50[i].
1374const LAB_XYZ_TO_SRGB: [[f32; 3]; 3] = [
1375    [3.022_233_7, -1.617_386, -0.404_847_65],
1376    [-0.943_848_25, 1.916_254_4, 0.027_593_868],
1377    [0.069_386_27, -0.228_976_76, 1.159_590_5],
1378];
1379
1380const EPSILON: f32 = 216. / 24389.;
1381const KAPPA: f32 = 24389. / 27.;
1382
1383impl ColorSpace for Lab {
1384    const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::Lab);
1385
1386    const WHITE_COMPONENTS: [f32; 3] = [100., 0., 0.];
1387
1388    fn to_linear_srgb([l, a, b]: [f32; 3]) -> [f32; 3] {
1389        let f1 = l * (1. / 116.) + (16. / 116.);
1390        let f0 = a * (1. / 500.) + f1;
1391        let f2 = f1 - b * (1. / 200.);
1392        let xyz = [f0, f1, f2].map(|value| {
1393            // This is EPSILON.cbrt() but that function isn't const (yet)
1394            const EPSILON_CBRT: f32 = 0.206_896_56;
1395            if value > EPSILON_CBRT {
1396                value * value * value
1397            } else {
1398                (116. / KAPPA) * value - (16. / KAPPA)
1399            }
1400        });
1401        matvecmul(&LAB_XYZ_TO_SRGB, xyz)
1402    }
1403
1404    fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] {
1405        let xyz = matvecmul(&LAB_SRGB_TO_XYZ, src);
1406        let f = xyz.map(|value| {
1407            if value > EPSILON {
1408                value.cbrt()
1409            } else {
1410                (KAPPA / 116.) * value + (16. / 116.)
1411            }
1412        });
1413        let l = 116. * f[1] - 16.;
1414        let a = 500. * (f[0] - f[1]);
1415        let b = 200. * (f[1] - f[2]);
1416        [l, a, b]
1417    }
1418
1419    fn scale_chroma([l, a, b]: [f32; 3], scale: f32) -> [f32; 3] {
1420        [l, a * scale, b * scale]
1421    }
1422
1423    fn convert<TargetCS: ColorSpace>(src: [f32; 3]) -> [f32; 3] {
1424        if TypeId::of::<Self>() == TypeId::of::<TargetCS>() {
1425            src
1426        } else if TypeId::of::<TargetCS>() == TypeId::of::<Lch>() {
1427            lab_to_lch(src)
1428        } else {
1429            let lin_rgb = Self::to_linear_srgb(src);
1430            TargetCS::from_linear_srgb(lin_rgb)
1431        }
1432    }
1433
1434    fn clip([l, a, b]: [f32; 3]) -> [f32; 3] {
1435        [l.clamp(0., 100.), a, b]
1436    }
1437}
1438
1439impl From<Lab> for ColorSpaceTag {
1440    fn from(_: Lab) -> Self {
1441        Self::Lab
1442    }
1443}
1444
1445/// 🌌 The cylindrical version of the [Lab] color space.
1446///
1447/// Its components are `[L, C, h]` with
1448/// - `L` - the lightness as in [`Lab`];
1449/// - `C` - the chromatic intensity, the natural lower bound of 0 being achromatic, usually not
1450///   exceeding 160; and
1451/// - `h` - the hue angle in degrees.
1452///
1453/// See [`Oklch`] for a similar color space but with better hue linearity.
1454#[derive(Clone, Copy, Debug)]
1455pub struct Lch;
1456
1457impl ColorSpace for Lch {
1458    const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::Lch);
1459
1460    const LAYOUT: ColorSpaceLayout = ColorSpaceLayout::HueThird;
1461
1462    const WHITE_COMPONENTS: [f32; 3] = [100., 0., 0.];
1463
1464    fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] {
1465        lab_to_lch(Lab::from_linear_srgb(src))
1466    }
1467
1468    fn to_linear_srgb(src: [f32; 3]) -> [f32; 3] {
1469        Lab::to_linear_srgb(lch_to_lab(src))
1470    }
1471
1472    fn scale_chroma([l, c, h]: [f32; 3], scale: f32) -> [f32; 3] {
1473        [l, c * scale, h]
1474    }
1475
1476    fn convert<TargetCS: ColorSpace>(src: [f32; 3]) -> [f32; 3] {
1477        if TypeId::of::<Self>() == TypeId::of::<TargetCS>() {
1478            src
1479        } else if TypeId::of::<TargetCS>() == TypeId::of::<Lab>() {
1480            lch_to_lab(src)
1481        } else {
1482            let lin_rgb = Self::to_linear_srgb(src);
1483            TargetCS::from_linear_srgb(lin_rgb)
1484        }
1485    }
1486
1487    fn clip([l, c, h]: [f32; 3]) -> [f32; 3] {
1488        [l.clamp(0., 100.), c.max(0.), h]
1489    }
1490}
1491
1492impl From<Lch> for ColorSpaceTag {
1493    fn from(_: Lch) -> Self {
1494        Self::Lch
1495    }
1496}
1497
1498/// 🌌 The HSL color space
1499///
1500/// The HSL color space is fairly widely used and convenient, but it is
1501/// not based on sound color science. Among its flaws, colors with the
1502/// same "lightness" value can have wildly varying perceptual lightness.
1503///
1504/// Its components are `[H, S, L]` with
1505/// - `H` - the hue angle in degrees, with red at 0, green at 120, and blue at 240.
1506/// - `S` - the saturation, where 0 is gray and 100 is maximally saturated.
1507/// - `L` - the lightness, where 0 is black and 100 is white.
1508///
1509/// This corresponds to the color space in [CSS Color Module Level 4 § 7][css-sec].
1510///
1511/// [css-sec]: https://www.w3.org/TR/css-color-4/#the-hsl-notation
1512#[derive(Clone, Copy, Debug)]
1513pub struct Hsl;
1514
1515/// Convert HSL to RGB.
1516///
1517/// Reference: § 7.1 of CSS Color 4 spec.
1518fn hsl_to_rgb([h, s, l]: [f32; 3]) -> [f32; 3] {
1519    // Don't need mod 360 for hue, it's subsumed by mod 12 below.
1520    let sat = s * 0.01;
1521    let light = l * 0.01;
1522    let a = sat * light.min(1.0 - light);
1523    [0.0, 8.0, 4.0].map(|n| {
1524        let x = n + h * (1.0 / 30.0);
1525        let k = x - 12.0 * (x * (1.0 / 12.0)).floor();
1526        light - a * (k - 3.0).min(9.0 - k).clamp(-1.0, 1.0)
1527    })
1528}
1529
1530/// Convert RGB to HSL.
1531///
1532/// Reference: § 7.2 of CSS Color 4 spec.
1533///
1534/// See <https://github.com/w3c/csswg-drafts/issues/10695> for an
1535/// explanation of why `hue_hack` is needed.
1536fn rgb_to_hsl([r, g, b]: [f32; 3], hue_hack: bool) -> [f32; 3] {
1537    let max = r.max(g).max(b);
1538    let min = r.min(g).min(b);
1539    let mut hue = 0.0;
1540    let mut sat = 0.0;
1541    let light = 0.5 * (min + max);
1542    let d = max - min;
1543
1544    const EPSILON: f32 = 1e-6;
1545    if d > EPSILON {
1546        let denom = light.min(1.0 - light);
1547        if denom.abs() > EPSILON {
1548            sat = (max - light) / denom;
1549        }
1550        hue = if max == r {
1551            (g - b) / d
1552        } else if max == g {
1553            (b - r) / d + 2.0
1554        } else {
1555            // max == b
1556            (r - g) / d + 4.0
1557        };
1558        hue *= 60.0;
1559        // Deal with negative saturation from out of gamut colors
1560        if hue_hack && sat < 0.0 {
1561            hue += 180.0;
1562            sat = sat.abs();
1563        }
1564        hue -= 360. * (hue * (1.0 / 360.0)).floor();
1565    }
1566    [hue, sat * 100.0, light * 100.0]
1567}
1568
1569impl ColorSpace for Hsl {
1570    const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::Hsl);
1571
1572    const LAYOUT: ColorSpaceLayout = ColorSpaceLayout::HueFirst;
1573
1574    const WHITE_COMPONENTS: [f32; 3] = [0., 0., 100.];
1575
1576    fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] {
1577        let rgb = Srgb::from_linear_srgb(src);
1578        rgb_to_hsl(rgb, true)
1579    }
1580
1581    fn to_linear_srgb(src: [f32; 3]) -> [f32; 3] {
1582        let rgb = hsl_to_rgb(src);
1583        Srgb::to_linear_srgb(rgb)
1584    }
1585
1586    fn scale_chroma([h, s, l]: [f32; 3], scale: f32) -> [f32; 3] {
1587        [h, s * scale, l]
1588    }
1589
1590    fn convert<TargetCS: ColorSpace>(src: [f32; 3]) -> [f32; 3] {
1591        if TypeId::of::<Self>() == TypeId::of::<TargetCS>() {
1592            src
1593        } else if TypeId::of::<TargetCS>() == TypeId::of::<Srgb>() {
1594            hsl_to_rgb(src)
1595        } else if TypeId::of::<TargetCS>() == TypeId::of::<Hwb>() {
1596            rgb_to_hwb(hsl_to_rgb(src))
1597        } else {
1598            let lin_rgb = Self::to_linear_srgb(src);
1599            TargetCS::from_linear_srgb(lin_rgb)
1600        }
1601    }
1602
1603    fn clip([h, s, l]: [f32; 3]) -> [f32; 3] {
1604        [h, s.max(0.), l.clamp(0., 100.)]
1605    }
1606}
1607
1608impl From<Hsl> for ColorSpaceTag {
1609    fn from(_: Hsl) -> Self {
1610        Self::Hsl
1611    }
1612}
1613
1614/// 🌌 The HWB color space
1615///
1616/// The HWB color space is a convenient way to represent colors. It corresponds
1617/// closely to popular color pickers, both a triangle with white, black, and
1618/// fully saturated color at the corner, and also a rectangle with a hue spectrum
1619/// at the top and black at the bottom, with whiteness as a separate slider. It
1620/// was proposed in [HWB–A More Intuitive Hue-Based Color Model].
1621///
1622/// Its components are `[H, W, B]` with
1623/// - `H` - the hue angle in degrees, with red at 0, green at 120, and blue at 240.
1624/// - `W` - an amount of whiteness to mix in, with 100 being white.
1625/// - `B` - an amount of blackness to mix in, with 100 being black.
1626///
1627/// The hue angle is the same as in [Hsl], and thus has the same flaw of poor hue
1628/// uniformity.
1629///
1630/// This corresponds to the color space in [CSS Color Module Level 4 § 8][css-sec].
1631///
1632/// [css-sec]: https://www.w3.org/TR/css-color-4/#the-hwb-notation
1633/// [HWB–A More Intuitive Hue-Based Color Model]: http://alvyray.com/Papers/CG/HWB_JGTv208.pdf
1634#[derive(Clone, Copy, Debug)]
1635pub struct Hwb;
1636
1637/// Convert HWB to RGB.
1638///
1639/// Reference: § 8.1 of CSS Color 4 spec.
1640fn hwb_to_rgb([h, w, b]: [f32; 3]) -> [f32; 3] {
1641    let white = w * 0.01;
1642    let black = b * 0.01;
1643    if white + black >= 1.0 {
1644        let gray = white / (white + black);
1645        [gray, gray, gray]
1646    } else {
1647        let rgb = hsl_to_rgb([h, 100., 50.]);
1648        rgb.map(|x| white + x * (1.0 - white - black))
1649    }
1650}
1651
1652/// Convert RGB to HWB.
1653///
1654/// Reference: § 8.2 of CSS Color 4 spec.
1655fn rgb_to_hwb([r, g, b]: [f32; 3]) -> [f32; 3] {
1656    let hsl = rgb_to_hsl([r, g, b], false);
1657    let white = r.min(g).min(b);
1658    let black = 1.0 - r.max(g).max(b);
1659    [hsl[0], white * 100., black * 100.]
1660}
1661
1662impl ColorSpace for Hwb {
1663    const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::Hwb);
1664
1665    const LAYOUT: ColorSpaceLayout = ColorSpaceLayout::HueFirst;
1666
1667    const WHITE_COMPONENTS: [f32; 3] = [0., 100., 0.];
1668
1669    fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] {
1670        let rgb = Srgb::from_linear_srgb(src);
1671        rgb_to_hwb(rgb)
1672    }
1673
1674    fn to_linear_srgb(src: [f32; 3]) -> [f32; 3] {
1675        let rgb = hwb_to_rgb(src);
1676        Srgb::to_linear_srgb(rgb)
1677    }
1678
1679    fn convert<TargetCS: ColorSpace>(src: [f32; 3]) -> [f32; 3] {
1680        if TypeId::of::<Self>() == TypeId::of::<TargetCS>() {
1681            src
1682        } else if TypeId::of::<TargetCS>() == TypeId::of::<Srgb>() {
1683            hwb_to_rgb(src)
1684        } else if TypeId::of::<TargetCS>() == TypeId::of::<Hsl>() {
1685            rgb_to_hsl(hwb_to_rgb(src), true)
1686        } else {
1687            let lin_rgb = Self::to_linear_srgb(src);
1688            TargetCS::from_linear_srgb(lin_rgb)
1689        }
1690    }
1691
1692    fn clip([h, w, b]: [f32; 3]) -> [f32; 3] {
1693        [h, w.clamp(0., 100.), b.clamp(0., 100.)]
1694    }
1695}
1696
1697impl From<Hwb> for ColorSpaceTag {
1698    fn from(_: Hwb) -> Self {
1699        Self::Hwb
1700    }
1701}
1702
1703#[cfg(test)]
1704mod tests {
1705    extern crate alloc;
1706
1707    use crate::{
1708        A98Rgb, Aces2065_1, AcesCg, Chromaticity, ColorSpace, DisplayP3, Hsl, Hwb, Lab, Lch,
1709        LinearSrgb, Oklab, Oklch, OpaqueColor, ProphotoRgb, Rec2020, Srgb, XyzD50, XyzD65,
1710    };
1711    use alloc::vec::Vec;
1712
1713    #[must_use]
1714    fn almost_equal<CS: ColorSpace>(col1: [f32; 3], col2: [f32; 3], absolute_epsilon: f32) -> bool {
1715        OpaqueColor::<CS>::new(col1).difference(OpaqueColor::new(col2)) <= absolute_epsilon
1716    }
1717
1718    /// The maximal magnitude of the color components. Useful for calculating relative errors.
1719    fn magnitude(col: [f32; 3]) -> f32 {
1720        col[0].abs().max(col[1].abs()).max(col[2].abs())
1721    }
1722
1723    #[test]
1724    fn roundtrip() {
1725        fn test_roundtrips<Source: ColorSpace, Dest: ColorSpace>(colors: &[[f32; 3]]) {
1726            /// A tight bound on relative numerical precision.
1727            ///
1728            /// Some floating point operations we use from the standard library do not technically
1729            /// have a specified precision. Testing under Miri may cause failures, as Miri
1730            /// randomizes precision of floating point operations that do not have a guaranteed
1731            /// precision. This tight bound is likely to fail under such randomization. On our
1732            /// target platforms, we'd still like to notice it we don't reach a tight precision
1733            /// bound anymore.
1734            const RELATIVE_EPSILON: f32 = f32::EPSILON * 16.;
1735
1736            for color in colors {
1737                let intermediate = Source::convert::<Dest>(*color);
1738                let roundtripped = Dest::convert::<Source>(intermediate);
1739
1740                // The roundtrip error is measured in linear sRGB. This adds more conversions, but
1741                // makes the components analogous.
1742                let linsrgb_color = Source::to_linear_srgb(*color);
1743                let linsrgb_roundtripped = Source::to_linear_srgb(roundtripped);
1744
1745                // The absolute epsilon is based on the maximal magnitude of the source color
1746                // components. The magnitude is at least 1, as that is the natural bound of linear
1747                // sRGB channels and prevents numerical issues around 0.
1748                let absolute_epsilon = magnitude(linsrgb_color).max(1.) * RELATIVE_EPSILON;
1749                assert!(almost_equal::<LinearSrgb>(
1750                    linsrgb_color,
1751                    linsrgb_roundtripped,
1752                    absolute_epsilon,
1753                ));
1754            }
1755        }
1756
1757        // Generate some values to test rectangular color spaces.
1758        let rectangular_values = {
1759            let components = [
1760                0., 1., -1., 0.5, 1234., -1234., 1.000_001, 0.000_001, -0.000_001,
1761            ];
1762            let mut values = Vec::new();
1763            for c0 in components {
1764                for c1 in components {
1765                    for c2 in components {
1766                        values.push([c0, c1, c2]);
1767                    }
1768                }
1769            }
1770            values
1771        };
1772
1773        test_roundtrips::<LinearSrgb, Srgb>(&rectangular_values);
1774        test_roundtrips::<DisplayP3, Srgb>(&rectangular_values);
1775        test_roundtrips::<A98Rgb, Srgb>(&rectangular_values);
1776        test_roundtrips::<ProphotoRgb, Srgb>(&rectangular_values);
1777        test_roundtrips::<Rec2020, Srgb>(&rectangular_values);
1778        test_roundtrips::<Aces2065_1, Srgb>(&rectangular_values);
1779        test_roundtrips::<AcesCg, Srgb>(&rectangular_values);
1780        test_roundtrips::<XyzD50, Srgb>(&rectangular_values);
1781        test_roundtrips::<XyzD65, Srgb>(&rectangular_values);
1782
1783        test_roundtrips::<Oklab, Srgb>(&[
1784            [0., 0., 0.],
1785            [1., 0., 0.],
1786            [0.2, 0.2, -0.1],
1787            [2.0, 0., -0.4],
1788        ]);
1789    }
1790
1791    #[test]
1792    fn white_components() {
1793        fn check_white<CS: ColorSpace>() {
1794            assert!(almost_equal::<Srgb>(
1795                Srgb::WHITE_COMPONENTS,
1796                CS::convert::<Srgb>(CS::WHITE_COMPONENTS),
1797                1e-4,
1798            ));
1799            assert!(almost_equal::<CS>(
1800                CS::WHITE_COMPONENTS,
1801                Srgb::convert::<CS>(Srgb::WHITE_COMPONENTS),
1802                1e-4,
1803            ));
1804        }
1805
1806        check_white::<A98Rgb>();
1807        check_white::<DisplayP3>();
1808        check_white::<Hsl>();
1809        check_white::<Hwb>();
1810        check_white::<Lab>();
1811        check_white::<Lch>();
1812        check_white::<LinearSrgb>();
1813        check_white::<Oklab>();
1814        check_white::<Oklch>();
1815        check_white::<ProphotoRgb>();
1816        check_white::<Rec2020>();
1817        check_white::<Aces2065_1>();
1818        check_white::<AcesCg>();
1819        check_white::<XyzD50>();
1820        check_white::<XyzD65>();
1821    }
1822
1823    #[test]
1824    fn a98rgb_srgb() {
1825        for (srgb, a98) in [
1826            ([0.1, 0.2, 0.3], [0.155_114, 0.212_317, 0.301_498]),
1827            ([0., 1., 0.], [0.564_972, 1., 0.234_424]),
1828        ] {
1829            assert!(almost_equal::<Srgb>(
1830                srgb,
1831                A98Rgb::convert::<Srgb>(a98),
1832                1e-4
1833            ));
1834            assert!(almost_equal::<A98Rgb>(
1835                a98,
1836                Srgb::convert::<A98Rgb>(srgb),
1837                1e-4
1838            ));
1839        }
1840    }
1841
1842    #[test]
1843    fn prophotorgb_srgb() {
1844        for (srgb, prophoto) in [
1845            ([0.1, 0.2, 0.3], [0.133136, 0.147659, 0.223581]),
1846            ([0., 1., 0.], [0.540282, 0.927599, 0.304566]),
1847        ] {
1848            assert!(almost_equal::<Srgb>(
1849                srgb,
1850                ProphotoRgb::convert::<Srgb>(prophoto),
1851                1e-4
1852            ));
1853            assert!(almost_equal::<ProphotoRgb>(
1854                prophoto,
1855                Srgb::convert::<ProphotoRgb>(srgb),
1856                1e-4
1857            ));
1858        }
1859    }
1860
1861    #[test]
1862    fn rec2020_srgb() {
1863        for (srgb, rec2020) in [
1864            ([0.1, 0.2, 0.3], [0.091284, 0.134169, 0.230056]),
1865            ([0.05, 0.1, 0.15], [0.029785, 0.043700, 0.083264]),
1866            ([0., 1., 0.], [0.567542, 0.959279, 0.268969]),
1867        ] {
1868            assert!(almost_equal::<Srgb>(
1869                srgb,
1870                Rec2020::convert::<Srgb>(rec2020),
1871                1e-4
1872            ));
1873            assert!(almost_equal::<Rec2020>(
1874                rec2020,
1875                Srgb::convert::<Rec2020>(srgb),
1876                1e-4
1877            ));
1878        }
1879    }
1880
1881    #[test]
1882    fn aces2065_1_srgb() {
1883        for (srgb, aces2065_1) in [
1884            ([0.6, 0.5, 0.4], [0.245_59, 0.215_57, 0.145_18]),
1885            ([0.0, 0.5, 1.0], [0.259_35, 0.270_89, 0.894_79]),
1886        ] {
1887            assert!(almost_equal::<Srgb>(
1888                srgb,
1889                Aces2065_1::convert::<Srgb>(aces2065_1),
1890                1e-4
1891            ));
1892            assert!(almost_equal::<Aces2065_1>(
1893                aces2065_1,
1894                Srgb::convert::<Aces2065_1>(srgb),
1895                1e-4
1896            ));
1897        }
1898    }
1899
1900    #[test]
1901    fn absolute_conversion() {
1902        assert!(almost_equal::<AcesCg>(
1903            Srgb::convert_absolute::<AcesCg>([0.5, 0.2, 0.4]),
1904            // Calculated using colour-science (https://github.com/colour-science/colour) with
1905            // `chromatic_adaptation_transform=None`
1906            [0.14628284, 0.04714393, 0.13361104],
1907            1e-4,
1908        ));
1909
1910        assert!(almost_equal::<XyzD65>(
1911            Srgb::convert_absolute::<XyzD50>([0.5, 0.2, 0.4]),
1912            Srgb::convert::<XyzD65>([0.5, 0.2, 0.4]),
1913            1e-4,
1914        ));
1915    }
1916
1917    #[test]
1918    fn chromatic_adaptation() {
1919        assert!(almost_equal::<Srgb>(
1920            XyzD50::convert_absolute::<Srgb>(Srgb::convert::<XyzD50>([0.5, 0.2, 0.4])),
1921            Srgb::chromatically_adapt([0.5, 0.2, 0.4], Chromaticity::D65, Chromaticity::D50),
1922            1e-4,
1923        ));
1924    }
1925
1926    /// Test whether `ColorSpace::convert` with implicit chromatic adaptation results in the same
1927    /// color as `ColorSpace::convert_absolute` in combination with explicit chromatic adaptation
1928    /// through `Colorspace::chromatically_adapt`.
1929    #[test]
1930    fn implicit_vs_explicit_chromatic_adaptation() {
1931        fn test<Source: ColorSpace, Dest: ColorSpace>(src: [f32; 3]) {
1932            let convert = Source::convert::<Dest>(src);
1933            let convert_absolute_then_adapt = Dest::chromatically_adapt(
1934                Source::convert_absolute::<Dest>(src),
1935                Source::WHITE_POINT,
1936                Dest::WHITE_POINT,
1937            );
1938            let adapt_then_convert_absolute = Source::convert_absolute::<Dest>(
1939                Source::chromatically_adapt(src, Source::WHITE_POINT, Dest::WHITE_POINT),
1940            );
1941
1942            // The error is measured in linear sRGB. This adds more conversions, but makes it
1943            // easier to reason about the component ranges.
1944            assert!(almost_equal::<LinearSrgb>(
1945                Dest::to_linear_srgb(convert),
1946                Dest::to_linear_srgb(convert_absolute_then_adapt),
1947                1e-4,
1948            ));
1949            assert!(almost_equal::<LinearSrgb>(
1950                Dest::to_linear_srgb(convert),
1951                Dest::to_linear_srgb(adapt_then_convert_absolute),
1952                1e-4,
1953            ));
1954        }
1955
1956        // From a D65 whitepoint to everything
1957        test::<Srgb, LinearSrgb>([0.5, 0.2, 0.4]);
1958        test::<Srgb, Lab>([0.5, 0.2, 0.4]);
1959        test::<Srgb, Lch>([0.5, 0.2, 0.4]);
1960        test::<Srgb, Hsl>([0.5, 0.2, 0.4]);
1961        test::<Srgb, Hwb>([0.5, 0.2, 0.4]);
1962        test::<Srgb, Oklab>([0.5, 0.2, 0.4]);
1963        test::<Srgb, Oklch>([0.5, 0.2, 0.4]);
1964        test::<Srgb, DisplayP3>([0.5, 0.2, 0.4]);
1965        test::<Srgb, A98Rgb>([0.5, 0.2, 0.4]);
1966        test::<Srgb, ProphotoRgb>([0.5, 0.2, 0.4]);
1967        test::<Srgb, Rec2020>([0.5, 0.2, 0.4]);
1968        test::<Srgb, Aces2065_1>([0.5, 0.2, 0.4]);
1969        test::<Srgb, AcesCg>([0.5, 0.2, 0.4]);
1970        test::<Srgb, XyzD50>([0.5, 0.2, 0.4]);
1971        test::<Srgb, XyzD65>([0.5, 0.2, 0.4]);
1972
1973        // From an ACES whitepoint to everything
1974        test::<AcesCg, Srgb>([0.5, 0.2, 0.4]);
1975        test::<AcesCg, LinearSrgb>([0.5, 0.2, 0.4]);
1976        test::<AcesCg, Lab>([0.5, 0.2, 0.4]);
1977        test::<AcesCg, Lch>([0.5, 0.2, 0.4]);
1978        test::<AcesCg, Hsl>([0.5, 0.2, 0.4]);
1979        test::<AcesCg, Hwb>([0.5, 0.2, 0.4]);
1980        test::<AcesCg, Oklab>([0.5, 0.2, 0.4]);
1981        test::<AcesCg, Oklch>([0.5, 0.2, 0.4]);
1982        test::<AcesCg, DisplayP3>([0.5, 0.2, 0.4]);
1983        test::<AcesCg, A98Rgb>([0.5, 0.2, 0.4]);
1984        test::<AcesCg, ProphotoRgb>([0.5, 0.2, 0.4]);
1985        test::<AcesCg, Rec2020>([0.5, 0.2, 0.4]);
1986        test::<AcesCg, Aces2065_1>([0.5, 0.2, 0.4]);
1987        test::<AcesCg, XyzD50>([0.5, 0.2, 0.4]);
1988        test::<AcesCg, XyzD65>([0.5, 0.2, 0.4]);
1989    }
1990}