1use crate::Color;
8
9#[derive(Clone, Copy, Debug, PartialEq)]
11pub struct OkLab {
12 pub l: f32,
14 pub a: f32,
16 pub b: f32,
18}
19
20#[derive(Clone, Copy, Debug, PartialEq)]
22pub struct OkLch {
23 pub l: f32,
25 pub c: f32,
27 pub h: f32,
29}
30
31#[inline]
33fn linear_rgb_to_lms(r: f32, g: f32, b: f32) -> (f32, f32, f32) {
34 let l = 0.412_221_5 * r + 0.536_332_55 * g + 0.051_445_94 * b;
35 let m = 0.211_903_5 * r + 0.680_699_5 * g + 0.107_396_96 * b;
36 let s = 0.088_302_46 * r + 0.281_718_85 * g + 0.629_978_7 * b;
37 (l, m, s)
38}
39
40#[inline]
42fn lms_to_oklab(l: f32, m: f32, s: f32) -> (f32, f32, f32) {
43 let l_ = l.cbrt();
44 let m_ = m.cbrt();
45 let s_ = s.cbrt();
46
47 let lab_l = 0.210_454_26 * l_ + 0.793_617_8 * m_ - 0.004_072_047 * s_;
48 let lab_a = 1.977_998_5 * l_ - 2.428_592_2 * m_ + 0.450_593_7 * s_;
49 let lab_b = 0.025_904_037 * l_ + 0.782_771_8 * m_ - 0.808_675_77 * s_;
50 (lab_l, lab_a, lab_b)
51}
52
53#[inline]
55fn oklab_to_lms_cubed(l: f32, a: f32, b: f32) -> (f32, f32, f32) {
56 let l_ = l + 0.396_337_78 * a + 0.215_803_76 * b;
57 let m_ = l - 0.105_561_346 * a - 0.063_854_17 * b;
58 let s_ = l - 0.089_484_18 * a - 1.291_485_5 * b;
59
60 (l_ * l_ * l_, m_ * m_ * m_, s_ * s_ * s_)
61}
62
63#[inline]
65fn lms_to_linear_rgb(l: f32, m: f32, s: f32) -> (f32, f32, f32) {
66 let r = 4.076_741_7 * l - 3.307_711_6 * m + 0.230_969_94 * s;
67 let g = -1.268_438 * l + 2.609_757_4 * m - 0.341_319_38 * s;
68 let b = -0.004_196_086_3 * l - 0.703_418_6 * m + 1.707_614_7 * s;
69 (r, g, b)
70}
71
72impl OkLab {
73 pub fn new(l: f32, a: f32, b: f32) -> Self {
75 Self { l, a, b }
76 }
77
78 pub fn from_linear_rgb(c: Color) -> Self {
80 let (l, m, s) = linear_rgb_to_lms(c.r, c.g, c.b);
81 let (lab_l, lab_a, lab_b) = lms_to_oklab(l, m, s);
82 Self {
83 l: lab_l,
84 a: lab_a,
85 b: lab_b,
86 }
87 }
88
89 pub fn to_linear_rgb(self) -> Color {
91 let (l, m, s) = oklab_to_lms_cubed(self.l, self.a, self.b);
92 let (r, g, b) = lms_to_linear_rgb(l, m, s);
93 Color::new(r, g, b, 1.0)
94 }
95
96 pub fn to_oklch(self) -> OkLch {
98 let c = self.a.hypot(self.b);
99 let h = if c < 1e-8 {
100 0.0
101 } else {
102 self.b.atan2(self.a).to_degrees().rem_euclid(360.0)
103 };
104 OkLch { l: self.l, c, h }
105 }
106
107 pub fn lerp(self, other: Self, t: f32) -> Self {
109 let t = t.clamp(0.0, 1.0);
110 Self {
111 l: self.l + (other.l - self.l) * t,
112 a: self.a + (other.a - self.a) * t,
113 b: self.b + (other.b - self.b) * t,
114 }
115 }
116}
117
118impl OkLch {
119 pub fn new(l: f32, c: f32, h: f32) -> Self {
121 Self { l, c, h }
122 }
123
124 pub fn to_oklab(self) -> OkLab {
126 let h_rad = self.h.to_radians();
127 OkLab {
128 l: self.l,
129 a: self.c * h_rad.cos(),
130 b: self.c * h_rad.sin(),
131 }
132 }
133
134 pub fn to_linear_rgb(self) -> Color {
136 self.to_oklab().to_linear_rgb()
137 }
138
139 pub fn lerp(self, other: Self, t: f32) -> Self {
141 let t = t.clamp(0.0, 1.0);
142
143 let mut dh = other.h - self.h;
145 if dh > 180.0 {
146 dh -= 360.0;
147 } else if dh < -180.0 {
148 dh += 360.0;
149 }
150
151 Self {
152 l: self.l + (other.l - self.l) * t,
153 c: self.c + (other.c - self.c) * t,
154 h: (self.h + dh * t).rem_euclid(360.0),
155 }
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 #[test]
164 fn black_roundtrip() {
165 let lab = OkLab::from_linear_rgb(Color::BLACK);
166 assert!(lab.l.abs() < 1e-4);
167 let back = lab.to_linear_rgb();
168 assert!(back.r.abs() < 1e-4);
169 }
170
171 #[test]
172 fn white_roundtrip() {
173 let lab = OkLab::from_linear_rgb(Color::WHITE);
174 assert!((lab.l - 1.0).abs() < 1e-3);
175 let back = lab.to_linear_rgb();
176 assert!((back.r - 1.0).abs() < 1e-3);
177 assert!((back.g - 1.0).abs() < 1e-3);
178 assert!((back.b - 1.0).abs() < 1e-3);
179 }
180
181 #[test]
182 fn oklch_hue_wrapping() {
183 let a = OkLch::new(0.7, 0.15, 350.0);
184 let b = OkLch::new(0.7, 0.15, 10.0);
185 let mid = a.lerp(b, 0.5);
186 assert!(mid.h < 10.0 || mid.h > 350.0);
188 }
189
190 #[test]
191 fn color_rgb_roundtrip() {
192 let colors = [
193 Color::from_hex("#ff0000").unwrap(),
194 Color::from_hex("#00ff00").unwrap(),
195 Color::from_hex("#0000ff").unwrap(),
196 Color::from_hex("#1f77b4").unwrap(),
197 ];
198 for c in colors {
199 let lab = OkLab::from_linear_rgb(c);
200 let back = lab.to_linear_rgb();
201 assert!(
202 (c.r - back.r).abs() < 1e-3,
203 "r: {} vs {} for {:?}",
204 c.r,
205 back.r,
206 c
207 );
208 assert!(
209 (c.g - back.g).abs() < 1e-3,
210 "g: {} vs {} for {:?}",
211 c.g,
212 back.g,
213 c
214 );
215 assert!(
216 (c.b - back.b).abs() < 1e-3,
217 "b: {} vs {} for {:?}",
218 c.b,
219 back.b,
220 c
221 );
222 }
223 }
224
225 #[test]
226 fn oklch_roundtrip() {
227 let c = Color::from_hex("#1f77b4").unwrap();
228 let lch = c.to_oklch();
229 let lab = lch.to_oklab();
230 let original_lab = c.to_oklab();
231 assert!((lab.l - original_lab.l).abs() < 1e-5);
232 assert!((lab.a - original_lab.a).abs() < 1e-5);
233 assert!((lab.b - original_lab.b).abs() < 1e-5);
234 }
235}