#![allow(clippy::many_single_char_names)]
pub fn srgb_to_oklab(r: u8, g: u8, b: u8) -> (f32, f32, f32) {
let r = srgb_to_linear(f32::from(r) / 255.0);
let g = srgb_to_linear(f32::from(g) / 255.0);
let b = srgb_to_linear(f32::from(b) / 255.0);
let l = 0.412_221_46 * r + 0.536_332_55 * g + 0.051_445_995 * b;
let m = 0.211_903_5 * r + 0.680_699_5 * g + 0.107_396_96 * b;
let s = 0.088_302_46 * r + 0.281_718_85 * g + 0.629_978_7 * b;
let l_ = l.cbrt();
let m_ = m.cbrt();
let s_ = s.cbrt();
let oklab_l = 0.210_454_26 * l_ + 0.793_617_8 * m_ - 0.004_072_047 * s_;
let oklab_a = 1.977_998_5 * l_ - 2.428_592_2 * m_ + 0.450_593_7 * s_;
let oklab_b = 0.025_904_037 * l_ + 0.782_771_77 * m_ - 0.808_675_77 * s_;
(oklab_l, oklab_a, oklab_b)
}
pub fn oklab_to_srgb(l: f32, a: f32, b: f32) -> (u8, u8, u8) {
let l_ = l + 0.396_337_78 * a + 0.215_803_76 * b;
let m_ = l - 0.105_561_346 * a - 0.063_854_17 * b;
let s_ = l - 0.089_484_18 * a - 1.291_485_5 * b;
let l = l_ * l_ * l_;
let m = m_ * m_ * m_;
let s = s_ * s_ * s_;
let r = 4.076_741_7 * l - 3.307_711_6 * m + 0.230_969_94 * s;
let g = -1.268_438 * l + 2.609_757_4 * m - 0.341_319_38 * s;
let b = -0.004_196_086 * l - 0.703_418_6 * m + 1.707_614_7 * s;
(
to_srgb_byte(linear_to_srgb(r)),
to_srgb_byte(linear_to_srgb(g)),
to_srgb_byte(linear_to_srgb(b)),
)
}
pub fn lerp_oklab(c1: (u8, u8, u8), c2: (u8, u8, u8), t: f32) -> (u8, u8, u8) {
let t = t.clamp(0.0, 1.0);
let (l1, a1, b1) = srgb_to_oklab(c1.0, c1.1, c1.2);
let (l2, a2, b2) = srgb_to_oklab(c2.0, c2.1, c2.2);
let l = l1 + (l2 - l1) * t;
let a = a1 + (a2 - a1) * t;
let b = b1 + (b2 - b1) * t;
oklab_to_srgb(l, a, b)
}
fn srgb_to_linear(c: f32) -> f32 {
if c <= 0.040_45 {
c / 12.92
} else {
((c + 0.055) / 1.055).powf(2.4)
}
}
fn linear_to_srgb(c: f32) -> f32 {
if c <= 0.003_130_8 {
12.92 * c
} else {
1.055 * c.powf(1.0 / 2.4) - 0.055
}
}
fn to_srgb_byte(c: f32) -> u8 {
(c * 255.0).round().clamp(0.0, 255.0) as u8
}
#[cfg(test)]
mod test {
use super::*;
const OKLAB_EPS: f32 = 0.001;
fn close(actual: f32, expected: f32, eps: f32) -> bool {
(actual - expected).abs() <= eps
}
#[test]
fn white_reference_value() {
let (l, a, b) = srgb_to_oklab(255, 255, 255);
assert!(close(l, 1.000, OKLAB_EPS), "L was {l}");
assert!(close(a, 0.000, OKLAB_EPS), "a was {a}");
assert!(close(b, 0.000, OKLAB_EPS), "b was {b}");
}
#[test]
fn black_reference_value() {
let (l, a, b) = srgb_to_oklab(0, 0, 0);
assert!(close(l, 0.000, OKLAB_EPS), "L was {l}");
assert!(close(a, 0.000, OKLAB_EPS), "a was {a}");
assert!(close(b, 0.000, OKLAB_EPS), "b was {b}");
}
#[test]
fn red_reference_value() {
let (l, a, b) = srgb_to_oklab(255, 0, 0);
assert!(close(l, 0.628, OKLAB_EPS), "L was {l}");
assert!(close(a, 0.225, OKLAB_EPS), "a was {a}");
assert!(close(b, 0.126, OKLAB_EPS), "b was {b}");
}
#[test]
fn green_reference_value() {
let (l, a, b) = srgb_to_oklab(0, 255, 0);
assert!(close(l, 0.866, OKLAB_EPS), "L was {l}");
assert!(close(a, -0.234, OKLAB_EPS), "a was {a}");
assert!(close(b, 0.180, OKLAB_EPS), "b was {b}");
}
#[test]
fn blue_reference_value() {
let (l, a, b) = srgb_to_oklab(0, 0, 255);
assert!(close(l, 0.452, OKLAB_EPS), "L was {l}");
assert!(close(a, -0.032, OKLAB_EPS), "a was {a}");
assert!(close(b, -0.312, OKLAB_EPS), "b was {b}");
}
#[test]
fn round_trip_preservation() {
for r in (0..=255).step_by(17) {
for g in (0..=255).step_by(17) {
for b in (0..=255).step_by(17) {
let (l, a_, b_) = srgb_to_oklab(r, g, b);
let (r2, g2, b2) = oklab_to_srgb(l, a_, b_);
let dr = i32::from(r) - i32::from(r2);
let dg = i32::from(g) - i32::from(g2);
let db = i32::from(b) - i32::from(b2);
assert!(
dr.abs() <= 1 && dg.abs() <= 1 && db.abs() <= 1,
"round-trip failed: ({r},{g},{b}) → ({r2},{g2},{b2})",
);
}
}
}
}
#[test]
fn lerp_endpoints_match_inputs() {
let red = (255, 0, 0);
let blue = (0, 0, 255);
let start = lerp_oklab(red, blue, 0.0);
let end = lerp_oklab(red, blue, 1.0);
assert!(
(i32::from(start.0) - 255).abs() <= 1 && start.1 == 0 && start.2 == 0,
"start was {start:?}",
);
assert!(
end.0 == 0 && end.1 == 0 && (i32::from(end.2) - 255).abs() <= 1,
"end was {end:?}",
);
}
#[test]
fn lerp_t_is_clamped() {
let red = (255, 0, 0);
let blue = (0, 0, 255);
assert_eq!(lerp_oklab(red, blue, -5.0), lerp_oklab(red, blue, 0.0));
assert_eq!(lerp_oklab(red, blue, 5.0), lerp_oklab(red, blue, 1.0));
}
#[test]
fn red_green_midpoint_is_not_muddy() {
let midpoint = lerp_oklab((255, 0, 0), (0, 255, 0), 0.5);
let (r, g, b) = midpoint;
assert!(r > 100, "R channel too low at midpoint: {midpoint:?}");
assert!(g > 100, "G channel too low at midpoint: {midpoint:?}");
assert!(
b < 50,
"B channel unexpectedly high at midpoint: {midpoint:?}"
);
}
}