1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
//! [What is Oklab color space?](https://bottosson.github.io/posts/oklab/)

use rgb::ComponentMap;
pub use rgb::RGB;

/// A color in Oklab is represented with three coordinates, similar to how CIELAB works, but with better perceptual properties.
/// Oklab uses a D65 whitepoint, since this is what sRGB and other common color spaces use.
#[derive(Copy, Clone, Debug, PartialOrd, PartialEq)]
#[repr(C)]
pub struct Oklab {
    /// L – perceived lightness
    pub l: f32,
    /// a – how green/red the color is
    pub a: f32,
    /// b – how blue/yellow the color is
    pub b: f32,
}

/// Converts from _linearized_ sRGB (in 0..1 range) to Oklab
///
/// (this is what is usually called a "linear light" or "without gamma" RGB.)
pub fn linear_srgb_to_oklab(c: RGB<f32>) -> Oklab {
    let l = 0.4122214708 * c.r + 0.5363325363 * c.g + 0.0514459929 * c.b;
    let m = 0.2119034982 * c.r + 0.6806995451 * c.g + 0.1073969566 * c.b;
    let s = 0.0883024619 * c.r + 0.2817188376 * c.g + 0.6299787005 * c.b;
    let l_ = l.cbrt();
    let m_ = m.cbrt();
    let s_ = s.cbrt();
    Oklab {
        l: 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_,
        a: 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_,
        b: 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_,
    }
}

/// Converts Oklab to _linear_ sRGB (in 0..1 range)
///
/// (this is what is usually called a "linear light" or "without gamma" RGB.)
///
/// Outputs aren't clamped, and can be negative or slightly over 1.0 due to floating-point rounding errors.
pub fn oklab_to_linear_srgb(c: Oklab) -> RGB<f32> {
    let l_ = c.l + 0.3963377774 * c.a + 0.2158037573 * c.b;
    let m_ = c.l - 0.1055613458 * c.a - 0.0638541728 * c.b;
    let s_ = c.l - 0.0894841775 * c.a - 1.2914855480 * c.b;
    let l = l_ * l_ * l_;
    let m = m_ * m_ * m_;
    let s = s_ * s_ * s_;
    RGB {
        r:  4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
        g: -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
        b: -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s,
    }
}

#[inline(always)]
fn to_gamma8(u: f32) -> u8 {
    (to_gamma(u) * 255.0).round() as u8
}

#[inline(always)]
fn to_linear8(c: u8) -> f32 {
    to_linear(c as f32 / 255.0)
}

#[inline]
fn to_gamma(u: f32) -> f32 {
    if u >= 0.0031308 {
        (1.055) * u.powf(1.0 / 2.4) - 0.055
    } else {
        12.92 * u
    }
}

#[inline]
fn to_linear(u: f32) -> f32 {
    if u >= 0.04045 {
        ((u + 0.055) / (1. + 0.055)).powf(2.4)
    } else {
        u / 12.92
    }
}

/// Converts regular 8-bit sRGB color to Oklab
#[inline]
pub fn srgb_to_oklab(c: RGB<u8>) -> Oklab {
    linear_srgb_to_oklab(c.map(to_linear8))
}

/// Converts Oklab to regular 8-bit sRGB color
#[inline]
pub fn oklab_to_srgb(c: Oklab) -> RGB<u8> {
    oklab_to_linear_srgb(c).map(to_gamma8)
}


impl Oklab {
    /// Converts Oklab to regular 8-bit sRGB color
    #[inline(always)]
    pub fn to_srgb(&self) -> RGB<u8> {
        oklab_to_srgb(*self)
    }

    /// Converts Oklab to _linear_ sRGB (in 0..1 range)
    ///
    /// (this is what is usually called a "linear light" or "without gamma" RGB.)
    ///
    /// Outputs aren't clamped, and can be negative or slightly over 1.0 due to floating-point rounding errors.
    #[inline(always)]
    pub fn to_linear_rgb(&self) -> RGB<f32> {
        oklab_to_linear_srgb(*self)
    }
}

/// Converts regular 8-bit sRGB color to Oklab
impl From<RGB<u8>> for Oklab {
    #[inline(always)]
    fn from(rgb: RGB<u8>) -> Self {
        srgb_to_oklab(rgb)
    }
}


#[test]
fn linear() {
    for i in 0..=255 {
        assert_eq!(i, to_gamma8(to_linear8(i)));
    }
}

#[test]
fn roundtrip_half1() {
    for r in 128..=255 {
        for g in 0..=255 {
            for b in 0..=255 {
                let c = RGB::new(r,g,b);
                assert_eq!(c, oklab_to_srgb(srgb_to_oklab(c)));
            }
        }
    }
}

#[test]
fn roundtrip_half2() {
    for r in 0..128 {
        for g in 0..=255 {
            for b in 0..=255 {
                let c = RGB::new(r,g,b);
                assert_eq!(c, oklab_to_srgb(srgb_to_oklab(c)));
            }
        }
    }
}