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, f32::consts::PI};
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            [3.240_97, -1.537_383_2, -0.498_610_76],
1138            [-0.969_243_65, 1.875_967_5, 0.041_555_06],
1139            [0.055_630_08, -0.203_976_96, 1.056_971_5],
1140        ];
1141        matvecmul(&XYZ_TO_LINEAR_SRGB, src)
1142    }
1143
1144    fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] {
1145        const LINEAR_SRGB_TO_XYZ: [[f32; 3]; 3] = [
1146            [0.412_390_8, 0.357_584_33, 0.180_480_8],
1147            [0.212_639, 0.715_168_65, 0.072_192_32],
1148            [0.019_330_818, 0.119_194_78, 0.950_532_14],
1149        ];
1150        matvecmul(&LINEAR_SRGB_TO_XYZ, src)
1151    }
1152
1153    fn clip([x, y, z]: [f32; 3]) -> [f32; 3] {
1154        [x, y, z]
1155    }
1156}
1157
1158impl From<XyzD65> for ColorSpaceTag {
1159    fn from(_: XyzD65) -> Self {
1160        Self::XyzD65
1161    }
1162}
1163
1164/// 🌌 The Oklab color space, intended to be a perceptually uniform color space.
1165///
1166/// Its components are `[L, a, b]` with
1167/// - `L` - the lightness with a natural bound between 0 and 1, where 0 represents pure black and 1
1168///   represents the lightness of white;
1169/// - `a` - how green/red the color is; and
1170/// - `b` - how blue/yellow the color is.
1171///
1172/// `a` and `b` are unbounded, but are usually between -0.5 and 0.5.
1173///
1174/// This corresponds to the color space in [CSS Color Module Level 4 § 9.2 ][css-sec]. It is
1175/// defined on [Björn Ottosson's blog][bjorn]. It is similar to the [CIELAB] color space but with
1176/// improved hue constancy.
1177///
1178/// Oklab has a cylindrical counterpart: [Oklch](`Oklch`).
1179///
1180/// [css-sec]: https://www.w3.org/TR/css-color-4/#ok-lab
1181/// [bjorn]: https://bottosson.github.io/posts/oklab/
1182/// [CIELAB]: Lab
1183#[derive(Clone, Copy, Debug)]
1184pub struct Oklab;
1185
1186// Matrices taken from [Oklab] blog post, precision reduced to f32
1187//
1188// [Oklab]: https://bottosson.github.io/posts/oklab/
1189const OKLAB_LAB_TO_LMS: [[f32; 3]; 3] = [
1190    [1.0, 0.396_337_78, 0.215_803_76],
1191    [1.0, -0.105_561_346, -0.063_854_17],
1192    [1.0, -0.089_484_18, -1.291_485_5],
1193];
1194
1195const OKLAB_LMS_TO_SRGB: [[f32; 3]; 3] = [
1196    [4.076_741_7, -3.307_711_6, 0.230_969_94],
1197    [-1.268_438, 2.609_757_4, -0.341_319_38],
1198    [-0.004_196_086_3, -0.703_418_6, 1.707_614_7],
1199];
1200
1201const OKLAB_SRGB_TO_LMS: [[f32; 3]; 3] = [
1202    [0.412_221_46, 0.536_332_55, 0.051_445_995],
1203    [0.211_903_5, 0.680_699_5, 0.107_396_96],
1204    [0.088_302_46, 0.281_718_85, 0.629_978_7],
1205];
1206
1207const OKLAB_LMS_TO_LAB: [[f32; 3]; 3] = [
1208    [0.210_454_26, 0.793_617_8, -0.004_072_047],
1209    [1.977_998_5, -2.428_592_2, 0.450_593_7],
1210    [0.025_904_037, 0.782_771_77, -0.808_675_77],
1211];
1212
1213impl ColorSpace for Oklab {
1214    const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::Oklab);
1215
1216    const WHITE_COMPONENTS: [f32; 3] = [1., 0., 0.];
1217
1218    fn to_linear_srgb(src: [f32; 3]) -> [f32; 3] {
1219        let lms = matvecmul(&OKLAB_LAB_TO_LMS, src).map(|x| x * x * x);
1220        matvecmul(&OKLAB_LMS_TO_SRGB, lms)
1221    }
1222
1223    fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] {
1224        let lms = matvecmul(&OKLAB_SRGB_TO_LMS, src).map(f32::cbrt);
1225        matvecmul(&OKLAB_LMS_TO_LAB, lms)
1226    }
1227
1228    fn scale_chroma([l, a, b]: [f32; 3], scale: f32) -> [f32; 3] {
1229        [l, a * scale, b * scale]
1230    }
1231
1232    fn convert<TargetCS: ColorSpace>(src: [f32; 3]) -> [f32; 3] {
1233        if TypeId::of::<Self>() == TypeId::of::<TargetCS>() {
1234            src
1235        } else if TypeId::of::<TargetCS>() == TypeId::of::<Oklch>() {
1236            lab_to_lch(src)
1237        } else {
1238            let lin_rgb = Self::to_linear_srgb(src);
1239            TargetCS::from_linear_srgb(lin_rgb)
1240        }
1241    }
1242
1243    fn clip([l, a, b]: [f32; 3]) -> [f32; 3] {
1244        [l.clamp(0., 1.), a, b]
1245    }
1246}
1247
1248impl From<Oklab> for ColorSpaceTag {
1249    fn from(_: Oklab) -> Self {
1250        Self::Oklab
1251    }
1252}
1253
1254/// Rectangular to cylindrical conversion.
1255fn lab_to_lch([l, a, b]: [f32; 3]) -> [f32; 3] {
1256    let mut h = b.atan2(a) * (180. / PI);
1257    if h < 0.0 {
1258        h += 360.0;
1259    }
1260    let c = b.hypot(a);
1261    [l, c, h]
1262}
1263
1264/// Cylindrical to rectangular conversion.
1265fn lch_to_lab([l, c, h]: [f32; 3]) -> [f32; 3] {
1266    let (sin, cos) = (h * (PI / 180.)).sin_cos();
1267    let a = c * cos;
1268    let b = c * sin;
1269    [l, a, b]
1270}
1271
1272/// 🌌 The cylindrical version of the [Oklab] color space.
1273///
1274/// Its components are `[L, C, h]` with
1275/// - `L` - the lightness as in [`Oklab`];
1276/// - `C` - the chromatic intensity, the natural lower bound of 0 being achromatic, usually not
1277///   exceeding 0.5; and
1278/// - `h` - the hue angle in degrees.
1279#[derive(Clone, Copy, Debug)]
1280pub struct Oklch;
1281
1282impl ColorSpace for Oklch {
1283    const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::Oklch);
1284
1285    const LAYOUT: ColorSpaceLayout = ColorSpaceLayout::HueThird;
1286
1287    const WHITE_COMPONENTS: [f32; 3] = [1., 0., 90.];
1288
1289    fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] {
1290        lab_to_lch(Oklab::from_linear_srgb(src))
1291    }
1292
1293    fn to_linear_srgb(src: [f32; 3]) -> [f32; 3] {
1294        Oklab::to_linear_srgb(lch_to_lab(src))
1295    }
1296
1297    fn scale_chroma([l, c, h]: [f32; 3], scale: f32) -> [f32; 3] {
1298        [l, c * scale, h]
1299    }
1300
1301    fn convert<TargetCS: ColorSpace>(src: [f32; 3]) -> [f32; 3] {
1302        if TypeId::of::<Self>() == TypeId::of::<TargetCS>() {
1303            src
1304        } else if TypeId::of::<TargetCS>() == TypeId::of::<Oklab>() {
1305            lch_to_lab(src)
1306        } else {
1307            let lin_rgb = Self::to_linear_srgb(src);
1308            TargetCS::from_linear_srgb(lin_rgb)
1309        }
1310    }
1311
1312    fn clip([l, c, h]: [f32; 3]) -> [f32; 3] {
1313        [l.clamp(0., 1.), c.max(0.), h]
1314    }
1315}
1316
1317impl From<Oklch> for ColorSpaceTag {
1318    fn from(_: Oklch) -> Self {
1319        Self::Oklch
1320    }
1321}
1322
1323/// 🌌 The CIELAB color space
1324///
1325/// The CIE L\*a\*b\* color space was created in 1976 to be more perceptually
1326/// uniform than RGB color spaces, and is both widely used and the basis of
1327/// other efforts to express colors, including [FreieFarbe].
1328///
1329/// Its components are `[L, a, b]` with
1330/// - `L` - the lightness with a natural bound between 0 and 100, where 0 represents pure black and 100
1331///   represents the lightness of white;
1332/// - `a` - how green/red the color is; and
1333/// - `b` - how blue/yellow the color is.
1334///
1335/// `a` and `b` are unbounded, but are usually between -160 and 160.
1336///
1337/// The color space has poor hue linearity and hue uniformity compared with
1338/// [Oklab], though superior lightness uniformity. Note that the lightness
1339/// range differs from Oklab as well; in Oklab white has a lightness of 1.
1340///
1341/// The CIE L\*a\*b\* color space is defined in terms of a D50 white point. For
1342/// conversion between color spaces with other illuminants (especially D65
1343/// as in sRGB), the standard Bradform linear chromatic adaptation transform
1344/// is used.
1345///
1346/// This corresponds to the color space in [CSS Color Module Level 4 § 9.1 ][css-sec].
1347///
1348/// Lab has a cylindrical counterpart: [Lch].
1349///
1350/// [FreieFarbe]: https://freiefarbe.de/en/
1351/// [css-sec]: https://www.w3.org/TR/css-color-4/#cie-lab
1352#[derive(Clone, Copy, Debug)]
1353pub struct Lab;
1354
1355// Matrices computed from CSS Color 4 spec, then used `cargo clippy --fix`
1356// to reduce precision to f32 and add underscores.
1357
1358// This is D65_to_D50 * lin_sRGB_to_XYZ, then rows scaled by 1 / D50[i].
1359const LAB_SRGB_TO_XYZ: [[f32; 3]; 3] = [
1360    [0.452_211_65, 0.399_412_24, 0.148_376_09],
1361    [0.222_493_17, 0.716_887, 0.060_619_81],
1362    [0.016_875_342, 0.117_659_41, 0.865_465_2],
1363];
1364
1365// This is XYZ_to_lin_sRGB * D50_to_D65, then columns scaled by D50[i].
1366const LAB_XYZ_TO_SRGB: [[f32; 3]; 3] = [
1367    [3.022_233_7, -1.617_386, -0.404_847_65],
1368    [-0.943_848_25, 1.916_254_4, 0.027_593_868],
1369    [0.069_386_27, -0.228_976_76, 1.159_590_5],
1370];
1371
1372const EPSILON: f32 = 216. / 24389.;
1373const KAPPA: f32 = 24389. / 27.;
1374
1375impl ColorSpace for Lab {
1376    const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::Lab);
1377
1378    const WHITE_COMPONENTS: [f32; 3] = [100., 0., 0.];
1379
1380    fn to_linear_srgb([l, a, b]: [f32; 3]) -> [f32; 3] {
1381        let f1 = l * (1. / 116.) + (16. / 116.);
1382        let f0 = a * (1. / 500.) + f1;
1383        let f2 = f1 - b * (1. / 200.);
1384        let xyz = [f0, f1, f2].map(|value| {
1385            // This is EPSILON.cbrt() but that function isn't const (yet)
1386            const EPSILON_CBRT: f32 = 0.206_896_56;
1387            if value > EPSILON_CBRT {
1388                value * value * value
1389            } else {
1390                (116. / KAPPA) * value - (16. / KAPPA)
1391            }
1392        });
1393        matvecmul(&LAB_XYZ_TO_SRGB, xyz)
1394    }
1395
1396    fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] {
1397        let xyz = matvecmul(&LAB_SRGB_TO_XYZ, src);
1398        let f = xyz.map(|value| {
1399            if value > EPSILON {
1400                value.cbrt()
1401            } else {
1402                (KAPPA / 116.) * value + (16. / 116.)
1403            }
1404        });
1405        let l = 116. * f[1] - 16.;
1406        let a = 500. * (f[0] - f[1]);
1407        let b = 200. * (f[1] - f[2]);
1408        [l, a, b]
1409    }
1410
1411    fn scale_chroma([l, a, b]: [f32; 3], scale: f32) -> [f32; 3] {
1412        [l, a * scale, b * scale]
1413    }
1414
1415    fn convert<TargetCS: ColorSpace>(src: [f32; 3]) -> [f32; 3] {
1416        if TypeId::of::<Self>() == TypeId::of::<TargetCS>() {
1417            src
1418        } else if TypeId::of::<TargetCS>() == TypeId::of::<Lch>() {
1419            lab_to_lch(src)
1420        } else {
1421            let lin_rgb = Self::to_linear_srgb(src);
1422            TargetCS::from_linear_srgb(lin_rgb)
1423        }
1424    }
1425
1426    fn clip([l, a, b]: [f32; 3]) -> [f32; 3] {
1427        [l.clamp(0., 100.), a, b]
1428    }
1429}
1430
1431impl From<Lab> for ColorSpaceTag {
1432    fn from(_: Lab) -> Self {
1433        Self::Lab
1434    }
1435}
1436
1437/// 🌌 The cylindrical version of the [Lab] color space.
1438///
1439/// Its components are `[L, C, h]` with
1440/// - `L` - the lightness as in [`Lab`];
1441/// - `C` - the chromatic intensity, the natural lower bound of 0 being achromatic, usually not
1442///   exceeding 160; and
1443/// - `h` - the hue angle in degrees.
1444///
1445/// See [`Oklch`] for a similar color space but with better hue linearity.
1446#[derive(Clone, Copy, Debug)]
1447pub struct Lch;
1448
1449impl ColorSpace for Lch {
1450    const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::Lch);
1451
1452    const LAYOUT: ColorSpaceLayout = ColorSpaceLayout::HueThird;
1453
1454    const WHITE_COMPONENTS: [f32; 3] = [100., 0., 0.];
1455
1456    fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] {
1457        lab_to_lch(Lab::from_linear_srgb(src))
1458    }
1459
1460    fn to_linear_srgb(src: [f32; 3]) -> [f32; 3] {
1461        Lab::to_linear_srgb(lch_to_lab(src))
1462    }
1463
1464    fn scale_chroma([l, c, h]: [f32; 3], scale: f32) -> [f32; 3] {
1465        [l, c * scale, h]
1466    }
1467
1468    fn convert<TargetCS: ColorSpace>(src: [f32; 3]) -> [f32; 3] {
1469        if TypeId::of::<Self>() == TypeId::of::<TargetCS>() {
1470            src
1471        } else if TypeId::of::<TargetCS>() == TypeId::of::<Lab>() {
1472            lch_to_lab(src)
1473        } else {
1474            let lin_rgb = Self::to_linear_srgb(src);
1475            TargetCS::from_linear_srgb(lin_rgb)
1476        }
1477    }
1478
1479    fn clip([l, c, h]: [f32; 3]) -> [f32; 3] {
1480        [l.clamp(0., 100.), c.max(0.), h]
1481    }
1482}
1483
1484impl From<Lch> for ColorSpaceTag {
1485    fn from(_: Lch) -> Self {
1486        Self::Lch
1487    }
1488}
1489
1490/// 🌌 The HSL color space
1491///
1492/// The HSL color space is fairly widely used and convenient, but it is
1493/// not based on sound color science. Among its flaws, colors with the
1494/// same "lightness" value can have wildly varying perceptual lightness.
1495///
1496/// Its components are `[H, S, L]` with
1497/// - `H` - the hue angle in degrees, with red at 0, green at 120, and blue at 240.
1498/// - `S` - the saturation, where 0 is gray and 100 is maximally saturated.
1499/// - `L` - the lightness, where 0 is black and 100 is white.
1500///
1501/// This corresponds to the color space in [CSS Color Module Level 4 § 7][css-sec].
1502///
1503/// [css-sec]: https://www.w3.org/TR/css-color-4/#the-hsl-notation
1504#[derive(Clone, Copy, Debug)]
1505pub struct Hsl;
1506
1507/// Convert HSL to RGB.
1508///
1509/// Reference: § 7.1 of CSS Color 4 spec.
1510fn hsl_to_rgb([h, s, l]: [f32; 3]) -> [f32; 3] {
1511    // Don't need mod 360 for hue, it's subsumed by mod 12 below.
1512    let sat = s * 0.01;
1513    let light = l * 0.01;
1514    let a = sat * light.min(1.0 - light);
1515    [0.0, 8.0, 4.0].map(|n| {
1516        let x = n + h * (1.0 / 30.0);
1517        let k = x - 12.0 * (x * (1.0 / 12.0)).floor();
1518        light - a * (k - 3.0).min(9.0 - k).clamp(-1.0, 1.0)
1519    })
1520}
1521
1522/// Convert RGB to HSL.
1523///
1524/// Reference: § 7.2 of CSS Color 4 spec.
1525///
1526/// See <https://github.com/w3c/csswg-drafts/issues/10695> for an
1527/// explanation of why `hue_hack` is needed.
1528fn rgb_to_hsl([r, g, b]: [f32; 3], hue_hack: bool) -> [f32; 3] {
1529    let max = r.max(g).max(b);
1530    let min = r.min(g).min(b);
1531    let mut hue = 0.0;
1532    let mut sat = 0.0;
1533    let light = 0.5 * (min + max);
1534    let d = max - min;
1535
1536    const EPSILON: f32 = 1e-6;
1537    if d > EPSILON {
1538        let denom = light.min(1.0 - light);
1539        if denom.abs() > EPSILON {
1540            sat = (max - light) / denom;
1541        }
1542        hue = if max == r {
1543            (g - b) / d
1544        } else if max == g {
1545            (b - r) / d + 2.0
1546        } else {
1547            // max == b
1548            (r - g) / d + 4.0
1549        };
1550        hue *= 60.0;
1551        // Deal with negative saturation from out of gamut colors
1552        if hue_hack && sat < 0.0 {
1553            hue += 180.0;
1554            sat = sat.abs();
1555        }
1556        hue -= 360. * (hue * (1.0 / 360.0)).floor();
1557    }
1558    [hue, sat * 100.0, light * 100.0]
1559}
1560
1561impl ColorSpace for Hsl {
1562    const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::Hsl);
1563
1564    const LAYOUT: ColorSpaceLayout = ColorSpaceLayout::HueFirst;
1565
1566    const WHITE_COMPONENTS: [f32; 3] = [0., 0., 100.];
1567
1568    fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] {
1569        let rgb = Srgb::from_linear_srgb(src);
1570        rgb_to_hsl(rgb, true)
1571    }
1572
1573    fn to_linear_srgb(src: [f32; 3]) -> [f32; 3] {
1574        let rgb = hsl_to_rgb(src);
1575        Srgb::to_linear_srgb(rgb)
1576    }
1577
1578    fn scale_chroma([h, s, l]: [f32; 3], scale: f32) -> [f32; 3] {
1579        [h, s * scale, l]
1580    }
1581
1582    fn convert<TargetCS: ColorSpace>(src: [f32; 3]) -> [f32; 3] {
1583        if TypeId::of::<Self>() == TypeId::of::<TargetCS>() {
1584            src
1585        } else if TypeId::of::<TargetCS>() == TypeId::of::<Srgb>() {
1586            hsl_to_rgb(src)
1587        } else if TypeId::of::<TargetCS>() == TypeId::of::<Hwb>() {
1588            rgb_to_hwb(hsl_to_rgb(src))
1589        } else {
1590            let lin_rgb = Self::to_linear_srgb(src);
1591            TargetCS::from_linear_srgb(lin_rgb)
1592        }
1593    }
1594
1595    fn clip([h, s, l]: [f32; 3]) -> [f32; 3] {
1596        [h, s.max(0.), l.clamp(0., 100.)]
1597    }
1598}
1599
1600impl From<Hsl> for ColorSpaceTag {
1601    fn from(_: Hsl) -> Self {
1602        Self::Hsl
1603    }
1604}
1605
1606/// 🌌 The HWB color space
1607///
1608/// The HWB color space is a convenient way to represent colors. It corresponds
1609/// closely to popular color pickers, both a triangle with white, black, and
1610/// fully saturated color at the corner, and also a rectangle with a hue spectrum
1611/// at the top and black at the bottom, with whiteness as a separate slider. It
1612/// was proposed in [HWB–A More Intuitive Hue-Based Color Model].
1613///
1614/// Its components are `[H, W, B]` with
1615/// - `H` - the hue angle in degrees, with red at 0, green at 120, and blue at 240.
1616/// - `W` - an amount of whiteness to mix in, with 100 being white.
1617/// - `B` - an amount of blackness to mix in, with 100 being black.
1618///
1619/// The hue angle is the same as in [Hsl], and thus has the same flaw of poor hue
1620/// uniformity.
1621///
1622/// This corresponds to the color space in [CSS Color Module Level 4 § 8][css-sec].
1623///
1624/// [css-sec]: https://www.w3.org/TR/css-color-4/#the-hwb-notation
1625/// [HWB–A More Intuitive Hue-Based Color Model]: http://alvyray.com/Papers/CG/HWB_JGTv208.pdf
1626#[derive(Clone, Copy, Debug)]
1627pub struct Hwb;
1628
1629/// Convert HWB to RGB.
1630///
1631/// Reference: § 8.1 of CSS Color 4 spec.
1632fn hwb_to_rgb([h, w, b]: [f32; 3]) -> [f32; 3] {
1633    let white = w * 0.01;
1634    let black = b * 0.01;
1635    if white + black >= 1.0 {
1636        let gray = white / (white + black);
1637        [gray, gray, gray]
1638    } else {
1639        let rgb = hsl_to_rgb([h, 100., 50.]);
1640        rgb.map(|x| white + x * (1.0 - white - black))
1641    }
1642}
1643
1644/// Convert RGB to HWB.
1645///
1646/// Reference: § 8.2 of CSS Color 4 spec.
1647fn rgb_to_hwb([r, g, b]: [f32; 3]) -> [f32; 3] {
1648    let hsl = rgb_to_hsl([r, g, b], false);
1649    let white = r.min(g).min(b);
1650    let black = 1.0 - r.max(g).max(b);
1651    [hsl[0], white * 100., black * 100.]
1652}
1653
1654impl ColorSpace for Hwb {
1655    const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::Hwb);
1656
1657    const LAYOUT: ColorSpaceLayout = ColorSpaceLayout::HueFirst;
1658
1659    const WHITE_COMPONENTS: [f32; 3] = [0., 100., 0.];
1660
1661    fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] {
1662        let rgb = Srgb::from_linear_srgb(src);
1663        rgb_to_hwb(rgb)
1664    }
1665
1666    fn to_linear_srgb(src: [f32; 3]) -> [f32; 3] {
1667        let rgb = hwb_to_rgb(src);
1668        Srgb::to_linear_srgb(rgb)
1669    }
1670
1671    fn convert<TargetCS: ColorSpace>(src: [f32; 3]) -> [f32; 3] {
1672        if TypeId::of::<Self>() == TypeId::of::<TargetCS>() {
1673            src
1674        } else if TypeId::of::<TargetCS>() == TypeId::of::<Srgb>() {
1675            hwb_to_rgb(src)
1676        } else if TypeId::of::<TargetCS>() == TypeId::of::<Hsl>() {
1677            rgb_to_hsl(hwb_to_rgb(src), true)
1678        } else {
1679            let lin_rgb = Self::to_linear_srgb(src);
1680            TargetCS::from_linear_srgb(lin_rgb)
1681        }
1682    }
1683
1684    fn clip([h, w, b]: [f32; 3]) -> [f32; 3] {
1685        [h, w.clamp(0., 100.), b.clamp(0., 100.)]
1686    }
1687}
1688
1689impl From<Hwb> for ColorSpaceTag {
1690    fn from(_: Hwb) -> Self {
1691        Self::Hwb
1692    }
1693}
1694
1695#[cfg(test)]
1696mod tests {
1697    extern crate alloc;
1698
1699    use crate::{
1700        A98Rgb, Aces2065_1, AcesCg, Chromaticity, ColorSpace, DisplayP3, Hsl, Hwb, Lab, Lch,
1701        LinearSrgb, Oklab, Oklch, OpaqueColor, ProphotoRgb, Rec2020, Srgb, XyzD50, XyzD65,
1702    };
1703    use alloc::vec::Vec;
1704
1705    #[must_use]
1706    fn almost_equal<CS: ColorSpace>(col1: [f32; 3], col2: [f32; 3], absolute_epsilon: f32) -> bool {
1707        OpaqueColor::<CS>::new(col1).difference(OpaqueColor::new(col2)) <= absolute_epsilon
1708    }
1709
1710    /// The maximal magnitude of the color components. Useful for calculating relative errors.
1711    fn magnitude(col: [f32; 3]) -> f32 {
1712        col[0].abs().max(col[1].abs()).max(col[2].abs())
1713    }
1714
1715    #[test]
1716    fn roundtrip() {
1717        fn test_roundtrips<Source: ColorSpace, Dest: ColorSpace>(colors: &[[f32; 3]]) {
1718            /// A tight bound on relative numerical precision.
1719            const RELATIVE_EPSILON: f32 = f32::EPSILON * 16.;
1720
1721            for color in colors {
1722                let intermediate = Source::convert::<Dest>(*color);
1723                let roundtripped = Dest::convert::<Source>(intermediate);
1724
1725                // The roundtrip error is measured in linear sRGB. This adds more conversions, but
1726                // makes the components analogous.
1727                let linsrgb_color = Source::to_linear_srgb(*color);
1728                let linsrgb_roundtripped = Source::to_linear_srgb(roundtripped);
1729
1730                // The absolute epsilon is based on the maximal magnitude of the source color
1731                // components. The magnitude is at least 1, as that is the natural bound of linear
1732                // sRGB channels and prevents numerical issues around 0.
1733                let absolute_epsilon = magnitude(linsrgb_color).max(1.) * RELATIVE_EPSILON;
1734                assert!(almost_equal::<LinearSrgb>(
1735                    linsrgb_color,
1736                    linsrgb_roundtripped,
1737                    absolute_epsilon,
1738                ));
1739            }
1740        }
1741
1742        // Generate some values to test rectangular color spaces.
1743        let rectangular_values = {
1744            let components = [
1745                0., 1., -1., 0.5, 1234., -1234., 1.000_001, 0.000_001, -0.000_001,
1746            ];
1747            let mut values = Vec::new();
1748            for c0 in components {
1749                for c1 in components {
1750                    for c2 in components {
1751                        values.push([c0, c1, c2]);
1752                    }
1753                }
1754            }
1755            values
1756        };
1757
1758        test_roundtrips::<LinearSrgb, Srgb>(&rectangular_values);
1759        test_roundtrips::<DisplayP3, Srgb>(&rectangular_values);
1760        test_roundtrips::<A98Rgb, Srgb>(&rectangular_values);
1761        test_roundtrips::<ProphotoRgb, Srgb>(&rectangular_values);
1762        test_roundtrips::<Rec2020, Srgb>(&rectangular_values);
1763        test_roundtrips::<Aces2065_1, Srgb>(&rectangular_values);
1764        test_roundtrips::<AcesCg, Srgb>(&rectangular_values);
1765        test_roundtrips::<XyzD50, Srgb>(&rectangular_values);
1766        test_roundtrips::<XyzD65, Srgb>(&rectangular_values);
1767
1768        test_roundtrips::<Oklab, Srgb>(&[
1769            [0., 0., 0.],
1770            [1., 0., 0.],
1771            [0.2, 0.2, -0.1],
1772            [2.0, 0., -0.4],
1773        ]);
1774    }
1775
1776    #[test]
1777    fn white_components() {
1778        fn check_white<CS: ColorSpace>() {
1779            assert!(almost_equal::<Srgb>(
1780                Srgb::WHITE_COMPONENTS,
1781                CS::convert::<Srgb>(CS::WHITE_COMPONENTS),
1782                1e-4,
1783            ));
1784            assert!(almost_equal::<CS>(
1785                CS::WHITE_COMPONENTS,
1786                Srgb::convert::<CS>(Srgb::WHITE_COMPONENTS),
1787                1e-4,
1788            ));
1789        }
1790
1791        check_white::<A98Rgb>();
1792        check_white::<DisplayP3>();
1793        check_white::<Hsl>();
1794        check_white::<Hwb>();
1795        check_white::<Lab>();
1796        check_white::<Lch>();
1797        check_white::<LinearSrgb>();
1798        check_white::<Oklab>();
1799        check_white::<Oklch>();
1800        check_white::<ProphotoRgb>();
1801        check_white::<Rec2020>();
1802        check_white::<Aces2065_1>();
1803        check_white::<AcesCg>();
1804        check_white::<XyzD50>();
1805        check_white::<XyzD65>();
1806    }
1807
1808    #[test]
1809    fn a98rgb_srgb() {
1810        for (srgb, a98) in [
1811            ([0.1, 0.2, 0.3], [0.155_114, 0.212_317, 0.301_498]),
1812            ([0., 1., 0.], [0.564_972, 1., 0.234_424]),
1813        ] {
1814            assert!(almost_equal::<Srgb>(
1815                srgb,
1816                A98Rgb::convert::<Srgb>(a98),
1817                1e-4
1818            ));
1819            assert!(almost_equal::<A98Rgb>(
1820                a98,
1821                Srgb::convert::<A98Rgb>(srgb),
1822                1e-4
1823            ));
1824        }
1825    }
1826
1827    #[test]
1828    fn prophotorgb_srgb() {
1829        for (srgb, prophoto) in [
1830            ([0.1, 0.2, 0.3], [0.133136, 0.147659, 0.223581]),
1831            ([0., 1., 0.], [0.540282, 0.927599, 0.304566]),
1832        ] {
1833            assert!(almost_equal::<Srgb>(
1834                srgb,
1835                ProphotoRgb::convert::<Srgb>(prophoto),
1836                1e-4
1837            ));
1838            assert!(almost_equal::<ProphotoRgb>(
1839                prophoto,
1840                Srgb::convert::<ProphotoRgb>(srgb),
1841                1e-4
1842            ));
1843        }
1844    }
1845
1846    #[test]
1847    fn rec2020_srgb() {
1848        for (srgb, rec2020) in [
1849            ([0.1, 0.2, 0.3], [0.091284, 0.134169, 0.230056]),
1850            ([0.05, 0.1, 0.15], [0.029785, 0.043700, 0.083264]),
1851            ([0., 1., 0.], [0.567542, 0.959279, 0.268969]),
1852        ] {
1853            assert!(almost_equal::<Srgb>(
1854                srgb,
1855                Rec2020::convert::<Srgb>(rec2020),
1856                1e-4
1857            ));
1858            assert!(almost_equal::<Rec2020>(
1859                rec2020,
1860                Srgb::convert::<Rec2020>(srgb),
1861                1e-4
1862            ));
1863        }
1864    }
1865
1866    #[test]
1867    fn aces2065_1_srgb() {
1868        for (srgb, aces2065_1) in [
1869            ([0.6, 0.5, 0.4], [0.245_59, 0.215_57, 0.145_18]),
1870            ([0.0, 0.5, 1.0], [0.259_35, 0.270_89, 0.894_79]),
1871        ] {
1872            assert!(almost_equal::<Srgb>(
1873                srgb,
1874                Aces2065_1::convert::<Srgb>(aces2065_1),
1875                1e-4
1876            ));
1877            assert!(almost_equal::<Aces2065_1>(
1878                aces2065_1,
1879                Srgb::convert::<Aces2065_1>(srgb),
1880                1e-4
1881            ));
1882        }
1883    }
1884
1885    #[test]
1886    fn absolute_conversion() {
1887        assert!(almost_equal::<AcesCg>(
1888            Srgb::convert_absolute::<AcesCg>([0.5, 0.2, 0.4]),
1889            // Calculated using colour-science (https://github.com/colour-science/colour) with
1890            // `chromatic_adaptation_transform=None`
1891            [0.14628284, 0.04714393, 0.13361104],
1892            1e-4,
1893        ));
1894
1895        assert!(almost_equal::<XyzD65>(
1896            Srgb::convert_absolute::<XyzD50>([0.5, 0.2, 0.4]),
1897            Srgb::convert::<XyzD65>([0.5, 0.2, 0.4]),
1898            1e-4,
1899        ));
1900    }
1901
1902    #[test]
1903    fn chromatic_adaptation() {
1904        assert!(almost_equal::<Srgb>(
1905            XyzD50::convert_absolute::<Srgb>(Srgb::convert::<XyzD50>([0.5, 0.2, 0.4])),
1906            Srgb::chromatically_adapt([0.5, 0.2, 0.4], Chromaticity::D65, Chromaticity::D50),
1907            1e-4,
1908        ));
1909    }
1910
1911    /// Test whether `ColorSpace::convert` with implicit chromatic adaptation results in the same
1912    /// color as `ColorSpace::convert_absolute` in combination with explicit chromatic adaptation
1913    /// through `Colorspace::chromatically_adapt`.
1914    #[test]
1915    fn implicit_vs_explicit_chromatic_adaptation() {
1916        fn test<Source: ColorSpace, Dest: ColorSpace>(src: [f32; 3]) {
1917            let convert = Source::convert::<Dest>(src);
1918            let convert_absolute_then_adapt = Dest::chromatically_adapt(
1919                Source::convert_absolute::<Dest>(src),
1920                Source::WHITE_POINT,
1921                Dest::WHITE_POINT,
1922            );
1923            let adapt_then_convert_absolute = Source::convert_absolute::<Dest>(
1924                Source::chromatically_adapt(src, Source::WHITE_POINT, Dest::WHITE_POINT),
1925            );
1926
1927            // The error is measured in linear sRGB. This adds more conversions, but makes it
1928            // easier to reason about the component ranges.
1929            assert!(almost_equal::<LinearSrgb>(
1930                Dest::to_linear_srgb(convert),
1931                Dest::to_linear_srgb(convert_absolute_then_adapt),
1932                1e-4,
1933            ));
1934            assert!(almost_equal::<LinearSrgb>(
1935                Dest::to_linear_srgb(convert),
1936                Dest::to_linear_srgb(adapt_then_convert_absolute),
1937                1e-4,
1938            ));
1939        }
1940
1941        // From a D65 whitepoint to everything
1942        test::<Srgb, LinearSrgb>([0.5, 0.2, 0.4]);
1943        test::<Srgb, Lab>([0.5, 0.2, 0.4]);
1944        test::<Srgb, Lch>([0.5, 0.2, 0.4]);
1945        test::<Srgb, Hsl>([0.5, 0.2, 0.4]);
1946        test::<Srgb, Hwb>([0.5, 0.2, 0.4]);
1947        test::<Srgb, Oklab>([0.5, 0.2, 0.4]);
1948        test::<Srgb, Oklch>([0.5, 0.2, 0.4]);
1949        test::<Srgb, DisplayP3>([0.5, 0.2, 0.4]);
1950        test::<Srgb, A98Rgb>([0.5, 0.2, 0.4]);
1951        test::<Srgb, ProphotoRgb>([0.5, 0.2, 0.4]);
1952        test::<Srgb, Rec2020>([0.5, 0.2, 0.4]);
1953        test::<Srgb, Aces2065_1>([0.5, 0.2, 0.4]);
1954        test::<Srgb, AcesCg>([0.5, 0.2, 0.4]);
1955        test::<Srgb, XyzD50>([0.5, 0.2, 0.4]);
1956        test::<Srgb, XyzD65>([0.5, 0.2, 0.4]);
1957
1958        // From an ACES whitepoint to everything
1959        test::<AcesCg, Srgb>([0.5, 0.2, 0.4]);
1960        test::<AcesCg, LinearSrgb>([0.5, 0.2, 0.4]);
1961        test::<AcesCg, Lab>([0.5, 0.2, 0.4]);
1962        test::<AcesCg, Lch>([0.5, 0.2, 0.4]);
1963        test::<AcesCg, Hsl>([0.5, 0.2, 0.4]);
1964        test::<AcesCg, Hwb>([0.5, 0.2, 0.4]);
1965        test::<AcesCg, Oklab>([0.5, 0.2, 0.4]);
1966        test::<AcesCg, Oklch>([0.5, 0.2, 0.4]);
1967        test::<AcesCg, DisplayP3>([0.5, 0.2, 0.4]);
1968        test::<AcesCg, A98Rgb>([0.5, 0.2, 0.4]);
1969        test::<AcesCg, ProphotoRgb>([0.5, 0.2, 0.4]);
1970        test::<AcesCg, Rec2020>([0.5, 0.2, 0.4]);
1971        test::<AcesCg, Aces2065_1>([0.5, 0.2, 0.4]);
1972        test::<AcesCg, XyzD50>([0.5, 0.2, 0.4]);
1973        test::<AcesCg, XyzD65>([0.5, 0.2, 0.4]);
1974    }
1975}