Skip to main content

style/color/
convert.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5//! Color conversion algorithms.
6//!
7//! Algorithms, matrices and constants are from the [color-4] specification,
8//! unless otherwise specified:
9//!
10//! https://drafts.csswg.org/css-color-4/#color-conversion-code
11//!
12//! NOTE: Matrices has to be transposed from the examples in the spec for use
13//! with the `euclid` library.
14
15use crate::color::ColorComponents;
16use crate::values::normalize;
17
18type Transform = euclid::default::Transform3D<f32>;
19type Vector = euclid::default::Vector3D<f32>;
20
21/// Normalize hue into [0, 360).
22#[inline]
23pub fn normalize_hue(hue: f32) -> f32 {
24    hue - 360. * (hue / 360.).floor()
25}
26
27/// Calculate the hue from RGB components and return it along with the min and
28/// max RGB values.
29#[inline]
30fn rgb_to_hue_min_max(red: f32, green: f32, blue: f32) -> (f32, f32, f32) {
31    let max = red.max(green).max(blue);
32    let min = red.min(green).min(blue);
33
34    let delta = max - min;
35
36    let hue = if delta != 0.0 {
37        60.0 * if max == red {
38            (green - blue) / delta + if green < blue { 6.0 } else { 0.0 }
39        } else if max == green {
40            (blue - red) / delta + 2.0
41        } else {
42            (red - green) / delta + 4.0
43        }
44    } else {
45        f32::NAN
46    };
47
48    (hue, min, max)
49}
50
51/// Convert from HSL notation to RGB notation.
52/// https://drafts.csswg.org/css-color-4/#hsl-to-rgb
53#[inline]
54pub fn hsl_to_rgb(from: &ColorComponents) -> ColorComponents {
55    fn hue_to_rgb(t1: f32, t2: f32, hue: f32) -> f32 {
56        let hue = normalize_hue(hue);
57
58        if hue * 6.0 < 360.0 {
59            t1 + (t2 - t1) * hue / 60.0
60        } else if hue * 2.0 < 360.0 {
61            t2
62        } else if hue * 3.0 < 720.0 {
63            t1 + (t2 - t1) * (240.0 - hue) / 60.0
64        } else {
65            t1
66        }
67    }
68
69    // Convert missing components to 0.0.
70    let ColorComponents(hue, saturation, lightness) = from.map(normalize);
71    let saturation = saturation / 100.0;
72    let lightness = lightness / 100.0;
73
74    let t2 = if lightness <= 0.5 {
75        lightness * (saturation + 1.0)
76    } else {
77        lightness + saturation - lightness * saturation
78    };
79    let t1 = lightness * 2.0 - t2;
80
81    ColorComponents(
82        hue_to_rgb(t1, t2, hue + 120.0),
83        hue_to_rgb(t1, t2, hue),
84        hue_to_rgb(t1, t2, hue - 120.0),
85    )
86}
87
88/// Convert from RGB notation to HSL notation.
89/// https://drafts.csswg.org/css-color-4/#rgb-to-hsl
90pub fn rgb_to_hsl(from: &ColorComponents) -> ColorComponents {
91    let ColorComponents(red, green, blue) = *from;
92
93    let (hue, min, max) = rgb_to_hue_min_max(red, green, blue);
94
95    let lightness = (min + max) / 2.0;
96    let delta = max - min;
97
98    let saturation = if delta != 0.0 {
99        if lightness == 0.0 || lightness == 1.0 {
100            0.0
101        } else {
102            (max - lightness) / lightness.min(1.0 - lightness)
103        }
104    } else {
105        0.0
106    };
107
108    ColorComponents(hue, saturation * 100.0, lightness * 100.0)
109}
110
111/// Convert from HWB notation to RGB notation.
112/// https://drafts.csswg.org/css-color-4/#hwb-to-rgb
113#[inline]
114pub fn hwb_to_rgb(from: &ColorComponents) -> ColorComponents {
115    // Convert missing components to 0.0.
116    let ColorComponents(hue, whiteness, blackness) = from.map(normalize);
117
118    let whiteness = whiteness / 100.0;
119    let blackness = blackness / 100.0;
120
121    if whiteness + blackness >= 1.0 {
122        let gray = whiteness / (whiteness + blackness);
123        return ColorComponents(gray, gray, gray);
124    }
125
126    let x = 1.0 - whiteness - blackness;
127    hsl_to_rgb(&ColorComponents(hue, 100.0, 50.0)).map(|v| v * x + whiteness)
128}
129
130/// Convert from RGB notation to HWB notation.
131/// https://drafts.csswg.org/css-color-4/#rgb-to-hwb
132#[inline]
133pub fn rgb_to_hwb(from: &ColorComponents) -> ColorComponents {
134    let ColorComponents(red, green, blue) = *from;
135
136    let (hue, min, max) = rgb_to_hue_min_max(red, green, blue);
137
138    let whiteness = min;
139    let blackness = 1.0 - max;
140
141    ColorComponents(hue, whiteness * 100.0, blackness * 100.0)
142}
143
144/// Calculate an epsilon for a specified range.
145#[inline]
146pub fn epsilon_for_range(min: f32, max: f32) -> f32 {
147    (max - min) / 1.0e5
148}
149
150/// Convert from the rectangular orthogonal to the cylindrical polar coordinate
151/// system. This is used to convert (ok)lab to (ok)lch.
152/// <https://drafts.csswg.org/css-color-4/#lab-to-lch>
153#[inline]
154pub fn orthogonal_to_polar(from: &ColorComponents, e: f32) -> ColorComponents {
155    let ColorComponents(lightness, a, b) = *from;
156
157    let chroma = (a * a + b * b).sqrt();
158
159    let hue = if a.abs() < e && b.abs() < e {
160        // For extremely small values of a and b ... the reported hue angle
161        // swinging about wildly and being essentially random ... this means
162        // the hue is powerless, and treated as missing when converted into LCH
163        // or Oklch.
164        f32::NAN
165    } else if chroma.abs() < e {
166        // Very small chroma values make the hue component powerless.
167        f32::NAN
168    } else {
169        normalize_hue(b.atan2(a).to_degrees())
170    };
171
172    ColorComponents(lightness, chroma, hue)
173}
174
175/// Convert from the cylindrical polar to the rectangular orthogonal coordinate
176/// system. This is used to convert (ok)lch to (ok)lab.
177/// <https://drafts.csswg.org/css-color-4/#lch-to-lab>
178#[inline]
179pub fn polar_to_orthogonal(from: &ColorComponents) -> ColorComponents {
180    let ColorComponents(lightness, chroma, hue) = *from;
181
182    // A missing hue component results in an achromatic color.
183    if hue.is_nan() {
184        return ColorComponents(lightness, 0.0, 0.0);
185    }
186
187    let hue = hue.to_radians();
188    let a = chroma * hue.cos();
189    let b = chroma * hue.sin();
190
191    ColorComponents(lightness, a, b)
192}
193
194#[inline]
195fn transform(from: &ColorComponents, mat: &Transform) -> ColorComponents {
196    let result = mat.transform_vector3d(Vector::new(from.0, from.1, from.2));
197    ColorComponents(result.x, result.y, result.z)
198}
199
200fn xyz_d65_to_xyz_d50(from: &ColorComponents) -> ColorComponents {
201    #[rustfmt::skip]
202    #[allow(clippy::excessive_precision)]
203    const MAT: Transform = Transform::new(
204         1.0479298208405488,    0.029627815688159344, -0.009243058152591178, 0.0,
205         0.022946793341019088,  0.990434484573249,     0.015055144896577895, 0.0,
206        -0.05019222954313557,  -0.01707382502938514,   0.7518742899580008,   0.0,
207         0.0,                   0.0,                   0.0,                  1.0,
208    );
209
210    transform(from, &MAT)
211}
212
213fn xyz_d50_to_xyz_d65(from: &ColorComponents) -> ColorComponents {
214    #[rustfmt::skip]
215    #[allow(clippy::excessive_precision)]
216    const MAT: Transform = Transform::new(
217         0.9554734527042182,   -0.028369706963208136,  0.012314001688319899, 0.0,
218        -0.023098536874261423,  1.0099954580058226,   -0.020507696433477912, 0.0,
219         0.0632593086610217,    0.021041398966943008,  1.3303659366080753,   0.0,
220         0.0,                   0.0,                   0.0,                  1.0,
221    );
222
223    transform(from, &MAT)
224}
225
226/// A reference white that is used during color conversion.
227pub enum WhitePoint {
228    /// D50 white reference.
229    D50,
230    /// D65 white reference.
231    D65,
232}
233
234impl WhitePoint {
235    #[allow(clippy::excessive_precision)]
236    const fn values(&self) -> ColorComponents {
237        // <https://drafts.csswg.org/css-color-4/#color-conversion-code>
238        match self {
239            // [0.3457 / 0.3585, 1.00000, (1.0 - 0.3457 - 0.3585) / 0.3585]
240            WhitePoint::D50 => ColorComponents(0.9642956764295677, 1.0, 0.8251046025104602),
241            // [0.3127 / 0.3290, 1.00000, (1.0 - 0.3127 - 0.3290) / 0.3290]
242            WhitePoint::D65 => ColorComponents(0.9504559270516716, 1.0, 1.0890577507598784),
243        }
244    }
245}
246
247fn convert_white_point(from: WhitePoint, to: WhitePoint, components: &mut ColorComponents) {
248    match (from, to) {
249        (WhitePoint::D50, WhitePoint::D65) => *components = xyz_d50_to_xyz_d65(components),
250        (WhitePoint::D65, WhitePoint::D50) => *components = xyz_d65_to_xyz_d50(components),
251        _ => {},
252    }
253}
254
255/// A trait that allows conversion of color spaces to and from XYZ coordinate
256/// space with a specified white point.
257///
258/// Allows following the specified method of converting between color spaces:
259/// - Convert to values to sRGB linear light.
260/// - Convert to XYZ coordinate space.
261/// - Adjust white point to target white point.
262/// - Convert to sRGB linear light in target color space.
263/// - Convert to sRGB gamma encoded in target color space.
264///
265/// https://drafts.csswg.org/css-color-4/#color-conversion
266pub trait ColorSpaceConversion {
267    /// The white point that the implementer is represented in.
268    const WHITE_POINT: WhitePoint;
269
270    /// Convert the components from sRGB gamma encoded values to sRGB linear
271    /// light values.
272    fn to_linear_light(from: &ColorComponents) -> ColorComponents;
273
274    /// Convert the components from sRGB linear light values to XYZ coordinate
275    /// space.
276    fn to_xyz(from: &ColorComponents) -> ColorComponents;
277
278    /// Convert the components from XYZ coordinate space to sRGB linear light
279    /// values.
280    fn from_xyz(from: &ColorComponents) -> ColorComponents;
281
282    /// Convert the components from sRGB linear light values to sRGB gamma
283    /// encoded values.
284    fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents;
285}
286
287/// Convert the color components from the specified color space to XYZ and
288/// return the components and the white point they are in.
289pub fn to_xyz<From: ColorSpaceConversion>(from: &ColorComponents) -> (ColorComponents, WhitePoint) {
290    // Convert the color components where in-gamut values are in the range
291    // [0 - 1] to linear light (un-companded) form.
292    let result = From::to_linear_light(from);
293
294    // Convert the color components from the source color space to XYZ.
295    (From::to_xyz(&result), From::WHITE_POINT)
296}
297
298/// Convert the color components from XYZ at the given white point to the
299/// specified color space.
300pub fn from_xyz<To: ColorSpaceConversion>(
301    from: &ColorComponents,
302    white_point: WhitePoint,
303) -> ColorComponents {
304    let mut xyz = *from;
305
306    // Convert the white point if needed.
307    convert_white_point(white_point, To::WHITE_POINT, &mut xyz);
308
309    // Convert the color from XYZ to the target color space.
310    let result = To::from_xyz(&xyz);
311
312    // Convert the color components of linear-light values in the range
313    // [0 - 1] to a gamma corrected form.
314    To::to_gamma_encoded(&result)
315}
316
317/// The sRGB color space.
318/// https://drafts.csswg.org/css-color-4/#predefined-sRGB
319pub struct Srgb;
320
321impl Srgb {
322    #[rustfmt::skip]
323    #[allow(clippy::excessive_precision)]
324    const TO_XYZ: Transform = Transform::new(
325        0.4123907992659595,  0.21263900587151036, 0.01933081871559185, 0.0,
326        0.35758433938387796, 0.7151686787677559,  0.11919477979462599, 0.0,
327        0.1804807884018343,  0.07219231536073371, 0.9505321522496606,  0.0,
328        0.0,                 0.0,                 0.0,                 1.0,
329    );
330
331    #[rustfmt::skip]
332    #[allow(clippy::excessive_precision)]
333    const FROM_XYZ: Transform = Transform::new(
334         3.2409699419045213, -0.9692436362808798,  0.05563007969699361, 0.0,
335        -1.5373831775700935,  1.8759675015077206, -0.20397695888897657, 0.0,
336        -0.4986107602930033,  0.04155505740717561, 1.0569715142428786,  0.0,
337         0.0,                 0.0,                 0.0,                 1.0,
338    );
339}
340
341impl ColorSpaceConversion for Srgb {
342    const WHITE_POINT: WhitePoint = WhitePoint::D65;
343
344    fn to_linear_light(from: &ColorComponents) -> ColorComponents {
345        (*from).map(|value| {
346            let abs = value.abs();
347
348            if abs < 0.04045 {
349                value / 12.92
350            } else {
351                value.signum() * ((abs + 0.055) / 1.055).powf(2.4)
352            }
353        })
354    }
355
356    fn to_xyz(from: &ColorComponents) -> ColorComponents {
357        transform(from, &Self::TO_XYZ)
358    }
359
360    fn from_xyz(from: &ColorComponents) -> ColorComponents {
361        transform(from, &Self::FROM_XYZ)
362    }
363
364    fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
365        (*from).map(|value| {
366            let abs = value.abs();
367
368            if abs > 0.0031308 {
369                value.signum() * (1.055 * abs.powf(1.0 / 2.4) - 0.055)
370            } else {
371                12.92 * value
372            }
373        })
374    }
375}
376
377/// Color specified with hue, saturation and lightness components.
378pub struct Hsl;
379
380impl ColorSpaceConversion for Hsl {
381    const WHITE_POINT: WhitePoint = Srgb::WHITE_POINT;
382
383    fn to_linear_light(from: &ColorComponents) -> ColorComponents {
384        Srgb::to_linear_light(&hsl_to_rgb(from))
385    }
386
387    #[inline]
388    fn to_xyz(from: &ColorComponents) -> ColorComponents {
389        Srgb::to_xyz(from)
390    }
391
392    #[inline]
393    fn from_xyz(from: &ColorComponents) -> ColorComponents {
394        Srgb::from_xyz(from)
395    }
396
397    fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
398        rgb_to_hsl(&Srgb::to_gamma_encoded(from))
399    }
400}
401
402/// Color specified with hue, whiteness and blackness components.
403pub struct Hwb;
404
405impl ColorSpaceConversion for Hwb {
406    const WHITE_POINT: WhitePoint = Srgb::WHITE_POINT;
407
408    fn to_linear_light(from: &ColorComponents) -> ColorComponents {
409        Srgb::to_linear_light(&hwb_to_rgb(from))
410    }
411
412    #[inline]
413    fn to_xyz(from: &ColorComponents) -> ColorComponents {
414        Srgb::to_xyz(from)
415    }
416
417    #[inline]
418    fn from_xyz(from: &ColorComponents) -> ColorComponents {
419        Srgb::from_xyz(from)
420    }
421
422    fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
423        rgb_to_hwb(&Srgb::to_gamma_encoded(from))
424    }
425}
426
427/// The same as sRGB color space, except the transfer function is linear light.
428/// https://drafts.csswg.org/css-color-4/#predefined-sRGB-linear
429pub struct SrgbLinear;
430
431impl ColorSpaceConversion for SrgbLinear {
432    const WHITE_POINT: WhitePoint = Srgb::WHITE_POINT;
433
434    fn to_linear_light(from: &ColorComponents) -> ColorComponents {
435        // Already in linear light form.
436        *from
437    }
438
439    fn to_xyz(from: &ColorComponents) -> ColorComponents {
440        Srgb::to_xyz(from)
441    }
442
443    fn from_xyz(from: &ColorComponents) -> ColorComponents {
444        Srgb::from_xyz(from)
445    }
446
447    fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
448        // Stay in linear light form.
449        *from
450    }
451}
452
453/// The Display-P3 color space.
454/// https://drafts.csswg.org/css-color-4/#predefined-display-p3
455pub struct DisplayP3;
456
457impl DisplayP3 {
458    #[rustfmt::skip]
459    #[allow(clippy::excessive_precision)]
460    const TO_XYZ: Transform = Transform::new(
461        0.48657094864821626, 0.22897456406974884, 0.0,                  0.0,
462        0.26566769316909294, 0.6917385218365062,  0.045113381858902575, 0.0,
463        0.1982172852343625,  0.079286914093745,   1.0439443689009757,   0.0,
464        0.0,                 0.0,                 0.0,                  1.0,
465    );
466
467    #[rustfmt::skip]
468    #[allow(clippy::excessive_precision)]
469    const FROM_XYZ: Transform = Transform::new(
470         2.4934969119414245,  -0.829488969561575,    0.035845830243784335, 0.0,
471        -0.9313836179191236,   1.7626640603183468,  -0.07617238926804171,  0.0,
472        -0.40271078445071684,  0.02362468584194359,  0.9568845240076873,   0.0,
473         0.0,                  0.0,                  0.0,                  1.0,
474    );
475}
476
477impl ColorSpaceConversion for DisplayP3 {
478    const WHITE_POINT: WhitePoint = WhitePoint::D65;
479
480    fn to_linear_light(from: &ColorComponents) -> ColorComponents {
481        Srgb::to_linear_light(from)
482    }
483
484    fn to_xyz(from: &ColorComponents) -> ColorComponents {
485        transform(from, &Self::TO_XYZ)
486    }
487
488    fn from_xyz(from: &ColorComponents) -> ColorComponents {
489        transform(from, &Self::FROM_XYZ)
490    }
491
492    fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
493        Srgb::to_gamma_encoded(from)
494    }
495}
496
497/// The Display-P3-linear color space. This is basically display-p3 without gamma encoding.
498pub struct DisplayP3Linear;
499impl ColorSpaceConversion for DisplayP3Linear {
500    const WHITE_POINT: WhitePoint = DisplayP3::WHITE_POINT;
501
502    fn to_linear_light(from: &ColorComponents) -> ColorComponents {
503        *from
504    }
505
506    fn to_xyz(from: &ColorComponents) -> ColorComponents {
507        DisplayP3::to_xyz(from)
508    }
509
510    fn from_xyz(from: &ColorComponents) -> ColorComponents {
511        DisplayP3::from_xyz(from)
512    }
513
514    fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
515        *from
516    }
517}
518
519/// The a98-rgb color space.
520/// https://drafts.csswg.org/css-color-4/#predefined-a98-rgb
521pub struct A98Rgb;
522
523impl A98Rgb {
524    #[rustfmt::skip]
525    #[allow(clippy::excessive_precision)]
526    const TO_XYZ: Transform = Transform::new(
527        0.5766690429101308,  0.29734497525053616, 0.027031361386412378, 0.0,
528        0.18555823790654627, 0.627363566255466,   0.07068885253582714,  0.0,
529        0.18822864623499472, 0.07529145849399789, 0.9913375368376389,   0.0,
530        0.0,                 0.0,                 0.0,                  1.0,
531    );
532
533    #[rustfmt::skip]
534    #[allow(clippy::excessive_precision)]
535    const FROM_XYZ: Transform = Transform::new(
536         2.041587903810746,  -0.9692436362808798,   0.013444280632031024, 0.0,
537        -0.5650069742788596,  1.8759675015077206,  -0.11836239223101824,  0.0,
538        -0.3447313507783295,  0.04155505740717561,  1.0151749943912054,   0.0,
539         0.0,                 0.0,                  0.0,                  1.0,
540    );
541}
542
543impl ColorSpaceConversion for A98Rgb {
544    const WHITE_POINT: WhitePoint = WhitePoint::D65;
545
546    #[allow(clippy::excessive_precision)]
547    fn to_linear_light(from: &ColorComponents) -> ColorComponents {
548        (*from).map(|v| v.signum() * v.abs().powf(2.19921875))
549    }
550
551    fn to_xyz(from: &ColorComponents) -> ColorComponents {
552        transform(from, &Self::TO_XYZ)
553    }
554
555    fn from_xyz(from: &ColorComponents) -> ColorComponents {
556        transform(from, &Self::FROM_XYZ)
557    }
558
559    #[allow(clippy::excessive_precision)]
560    fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
561        (*from).map(|v| v.signum() * v.abs().powf(0.4547069271758437))
562    }
563}
564
565/// The ProPhoto RGB color space.
566/// https://drafts.csswg.org/css-color-4/#predefined-prophoto-rgb
567pub struct ProphotoRgb;
568
569impl ProphotoRgb {
570    #[rustfmt::skip]
571    #[allow(clippy::excessive_precision)]
572    const TO_XYZ: Transform = Transform::new(
573        0.7977604896723027,  0.2880711282292934,     0.0,                0.0,
574        0.13518583717574031, 0.7118432178101014,     0.0,                0.0,
575        0.0313493495815248,  0.00008565396060525902, 0.8251046025104601, 0.0,
576        0.0,                 0.0,                    0.0,                1.0,
577    );
578
579    #[rustfmt::skip]
580    #[allow(clippy::excessive_precision)]
581    const FROM_XYZ: Transform = Transform::new(
582         1.3457989731028281,  -0.5446224939028347,  0.0,                0.0,
583        -0.25558010007997534,  1.5082327413132781,  0.0,                0.0,
584        -0.05110628506753401,  0.02053603239147973, 1.2119675456389454, 0.0,
585         0.0,                  0.0,                 0.0,                1.0,
586    );
587}
588
589impl ColorSpaceConversion for ProphotoRgb {
590    const WHITE_POINT: WhitePoint = WhitePoint::D50;
591
592    fn to_linear_light(from: &ColorComponents) -> ColorComponents {
593        (*from).map(|value| {
594            const ET2: f32 = 16.0 / 512.0;
595
596            let abs = value.abs();
597
598            if abs <= ET2 {
599                value / 16.0
600            } else {
601                value.signum() * abs.powf(1.8)
602            }
603        })
604    }
605
606    fn to_xyz(from: &ColorComponents) -> ColorComponents {
607        transform(from, &Self::TO_XYZ)
608    }
609
610    fn from_xyz(from: &ColorComponents) -> ColorComponents {
611        transform(from, &Self::FROM_XYZ)
612    }
613
614    fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
615        const ET: f32 = 1.0 / 512.0;
616
617        (*from).map(|v| {
618            let abs = v.abs();
619            if abs >= ET {
620                v.signum() * abs.powf(1.0 / 1.8)
621            } else {
622                16.0 * v
623            }
624        })
625    }
626}
627
628/// The Rec.2020 color space.
629/// https://drafts.csswg.org/css-color-4/#predefined-rec2020
630pub struct Rec2020;
631
632impl Rec2020 {
633    #[allow(clippy::excessive_precision)]
634    const ALPHA: f32 = 1.09929682680944;
635    #[allow(clippy::excessive_precision)]
636    const BETA: f32 = 0.018053968510807;
637
638    #[rustfmt::skip]
639    #[allow(clippy::excessive_precision)]
640    const TO_XYZ: Transform = Transform::new(
641        0.6369580483012913,  0.26270021201126703,  0.0,                  0.0,
642        0.14461690358620838, 0.677998071518871,    0.028072693049087508, 0.0,
643        0.16888097516417205, 0.059301716469861945, 1.0609850577107909,   0.0,
644        0.0,                 0.0,                  0.0,                  1.0,
645    );
646
647    #[rustfmt::skip]
648    #[allow(clippy::excessive_precision)]
649    const FROM_XYZ: Transform = Transform::new(
650         1.7166511879712676, -0.666684351832489,    0.017639857445310915, 0.0,
651        -0.3556707837763924,  1.616481236634939,   -0.042770613257808655, 0.0,
652        -0.2533662813736598,  0.01576854581391113,  0.942103121235474,    0.0,
653         0.0,                 0.0,                  0.0,                  1.0,
654    );
655}
656
657impl ColorSpaceConversion for Rec2020 {
658    const WHITE_POINT: WhitePoint = WhitePoint::D65;
659
660    fn to_linear_light(from: &ColorComponents) -> ColorComponents {
661        (*from).map(|value| {
662            let abs = value.abs();
663
664            if abs < Self::BETA * 4.5 {
665                value / 4.5
666            } else {
667                value.signum() * ((abs + Self::ALPHA - 1.0) / Self::ALPHA).powf(1.0 / 0.45)
668            }
669        })
670    }
671
672    fn to_xyz(from: &ColorComponents) -> ColorComponents {
673        transform(from, &Self::TO_XYZ)
674    }
675
676    fn from_xyz(from: &ColorComponents) -> ColorComponents {
677        transform(from, &Self::FROM_XYZ)
678    }
679
680    fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
681        (*from).map(|v| {
682            let abs = v.abs();
683
684            if abs > Self::BETA {
685                v.signum() * (Self::ALPHA * abs.powf(0.45) - (Self::ALPHA - 1.0))
686            } else {
687                4.5 * v
688            }
689        })
690    }
691}
692
693/// A color in the XYZ coordinate space with a D50 white reference.
694/// https://drafts.csswg.org/css-color-4/#predefined-xyz
695pub struct XyzD50;
696
697impl ColorSpaceConversion for XyzD50 {
698    const WHITE_POINT: WhitePoint = WhitePoint::D50;
699
700    fn to_linear_light(from: &ColorComponents) -> ColorComponents {
701        *from
702    }
703
704    fn to_xyz(from: &ColorComponents) -> ColorComponents {
705        *from
706    }
707
708    fn from_xyz(from: &ColorComponents) -> ColorComponents {
709        *from
710    }
711
712    fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
713        *from
714    }
715}
716
717/// A color in the XYZ coordinate space with a D65 white reference.
718/// https://drafts.csswg.org/css-color-4/#predefined-xyz
719pub struct XyzD65;
720
721impl ColorSpaceConversion for XyzD65 {
722    const WHITE_POINT: WhitePoint = WhitePoint::D65;
723
724    fn to_linear_light(from: &ColorComponents) -> ColorComponents {
725        *from
726    }
727
728    fn to_xyz(from: &ColorComponents) -> ColorComponents {
729        *from
730    }
731
732    fn from_xyz(from: &ColorComponents) -> ColorComponents {
733        *from
734    }
735
736    fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
737        *from
738    }
739}
740
741/// The Lab color space.
742/// https://drafts.csswg.org/css-color-4/#specifying-lab-lch
743pub struct Lab;
744
745impl Lab {
746    const KAPPA: f32 = 24389.0 / 27.0;
747    const EPSILON: f32 = 216.0 / 24389.0;
748}
749
750impl ColorSpaceConversion for Lab {
751    const WHITE_POINT: WhitePoint = WhitePoint::D50;
752
753    fn to_linear_light(from: &ColorComponents) -> ColorComponents {
754        // No need for conversion.
755        *from
756    }
757
758    /// Convert a CIELAB color to XYZ as specified in [1] and [2].
759    ///
760    /// [1]: https://drafts.csswg.org/css-color/#lab-to-predefined
761    /// [2]: https://drafts.csswg.org/css-color/#color-conversion-code
762    fn to_xyz(from: &ColorComponents) -> ColorComponents {
763        let ColorComponents(lightness, a, b) = *from;
764
765        let f1 = (lightness + 16.0) / 116.0;
766        let f0 = f1 + a / 500.0;
767        let f2 = f1 - b / 200.0;
768
769        let f0_cubed = f0 * f0 * f0;
770        let x = if f0_cubed > Self::EPSILON {
771            f0_cubed
772        } else {
773            (116.0 * f0 - 16.0) / Self::KAPPA
774        };
775
776        let y = if lightness > Self::KAPPA * Self::EPSILON {
777            let v = (lightness + 16.0) / 116.0;
778            v * v * v
779        } else {
780            lightness / Self::KAPPA
781        };
782
783        let f2_cubed = f2 * f2 * f2;
784        let z = if f2_cubed > Self::EPSILON {
785            f2_cubed
786        } else {
787            (116.0 * f2 - 16.0) / Self::KAPPA
788        };
789
790        ColorComponents(x, y, z) * Self::WHITE_POINT.values()
791    }
792
793    /// Convert an XYZ color to LAB as specified in [1] and [2].
794    ///
795    /// [1]: https://drafts.csswg.org/css-color/#rgb-to-lab
796    /// [2]: https://drafts.csswg.org/css-color/#color-conversion-code
797    fn from_xyz(from: &ColorComponents) -> ColorComponents {
798        let adapted = *from / Self::WHITE_POINT.values();
799
800        // 4. Convert D50-adapted XYZ to Lab.
801        let ColorComponents(f0, f1, f2) = adapted.map(|v| {
802            if v > Self::EPSILON {
803                v.cbrt()
804            } else {
805                (Self::KAPPA * v + 16.0) / 116.0
806            }
807        });
808
809        let lightness = 116.0 * f1 - 16.0;
810        let a = 500.0 * (f0 - f1);
811        let b = 200.0 * (f1 - f2);
812
813        ColorComponents(lightness, a, b)
814    }
815
816    fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
817        // No need for conversion.
818        *from
819    }
820}
821
822/// The Lch color space.
823/// https://drafts.csswg.org/css-color-4/#specifying-lab-lch
824pub struct Lch;
825
826impl ColorSpaceConversion for Lch {
827    const WHITE_POINT: WhitePoint = Lab::WHITE_POINT;
828
829    fn to_linear_light(from: &ColorComponents) -> ColorComponents {
830        // No need for conversion.
831        *from
832    }
833
834    fn to_xyz(from: &ColorComponents) -> ColorComponents {
835        // Convert LCH to Lab first.
836        let lab = polar_to_orthogonal(from);
837
838        // Then convert the Lab to XYZ.
839        Lab::to_xyz(&lab)
840    }
841
842    fn from_xyz(from: &ColorComponents) -> ColorComponents {
843        // First convert the XYZ to LAB.
844        let lab = Lab::from_xyz(from);
845
846        // Then convert the Lab to LCH.
847        orthogonal_to_polar(&lab, epsilon_for_range(0.0, 100.0))
848    }
849
850    fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
851        // No need for conversion.
852        *from
853    }
854}
855
856/// The Oklab color space.
857/// https://drafts.csswg.org/css-color-4/#specifying-oklab-oklch
858pub struct Oklab;
859
860impl Oklab {
861    #[rustfmt::skip]
862    #[allow(clippy::excessive_precision)]
863    const XYZ_TO_LMS: Transform = Transform::new(
864         0.8190224432164319,  0.0329836671980271,  0.048177199566046255, 0.0,
865         0.3619062562801221,  0.9292868468965546,  0.26423952494422764,  0.0,
866        -0.12887378261216414, 0.03614466816999844, 0.6335478258136937,   0.0,
867         0.0,                 0.0,                 0.0,                  1.0,
868    );
869
870    #[rustfmt::skip]
871    #[allow(clippy::excessive_precision)]
872    const LMS_TO_OKLAB: Transform = Transform::new(
873         0.2104542553,  1.9779984951,  0.0259040371, 0.0,
874         0.7936177850, -2.4285922050,  0.7827717662, 0.0,
875        -0.0040720468,  0.4505937099, -0.8086757660, 0.0,
876         0.0,           0.0,           0.0,          1.0,
877    );
878
879    #[rustfmt::skip]
880    #[allow(clippy::excessive_precision)]
881    const LMS_TO_XYZ: Transform = Transform::new(
882         1.2268798733741557,  -0.04057576262431372, -0.07637294974672142, 0.0,
883        -0.5578149965554813,   1.1122868293970594,  -0.4214933239627914,  0.0,
884         0.28139105017721583, -0.07171106666151701,  1.5869240244272418,  0.0,
885         0.0,                  0.0,                  0.0,                 1.0,
886    );
887
888    #[rustfmt::skip]
889    #[allow(clippy::excessive_precision)]
890    const OKLAB_TO_LMS: Transform = Transform::new(
891        0.99999999845051981432,  1.0000000088817607767,    1.0000000546724109177,   0.0,
892        0.39633779217376785678, -0.1055613423236563494,   -0.089484182094965759684, 0.0,
893        0.21580375806075880339, -0.063854174771705903402, -1.2914855378640917399,   0.0,
894        0.0,                     0.0,                      0.0,                     1.0,
895    );
896}
897
898impl ColorSpaceConversion for Oklab {
899    const WHITE_POINT: WhitePoint = WhitePoint::D65;
900
901    fn to_linear_light(from: &ColorComponents) -> ColorComponents {
902        // No need for conversion.
903        *from
904    }
905
906    fn to_xyz(from: &ColorComponents) -> ColorComponents {
907        let lms = transform(from, &Self::OKLAB_TO_LMS);
908        let lms = lms.map(|v| v * v * v);
909        transform(&lms, &Self::LMS_TO_XYZ)
910    }
911
912    fn from_xyz(from: &ColorComponents) -> ColorComponents {
913        let lms = transform(from, &Self::XYZ_TO_LMS);
914        let lms = lms.map(|v| v.cbrt());
915        transform(&lms, &Self::LMS_TO_OKLAB)
916    }
917
918    fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
919        // No need for conversion.
920        *from
921    }
922}
923
924/// The Oklch color space.
925/// https://drafts.csswg.org/css-color-4/#specifying-oklab-oklch
926pub struct Oklch;
927
928impl ColorSpaceConversion for Oklch {
929    const WHITE_POINT: WhitePoint = Oklab::WHITE_POINT;
930
931    fn to_linear_light(from: &ColorComponents) -> ColorComponents {
932        // No need for conversion.
933        *from
934    }
935
936    fn to_xyz(from: &ColorComponents) -> ColorComponents {
937        // First convert OkLCH to Oklab.
938        let oklab = polar_to_orthogonal(from);
939
940        // Then convert Oklab to XYZ.
941        Oklab::to_xyz(&oklab)
942    }
943
944    fn from_xyz(from: &ColorComponents) -> ColorComponents {
945        // First convert XYZ to Oklab.
946        let lab = Oklab::from_xyz(from);
947
948        // Then convert Oklab to OkLCH.
949        orthogonal_to_polar(&lab, epsilon_for_range(0.0, 1.0))
950    }
951
952    fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
953        // No need for conversion.
954        *from
955    }
956}