#[allow(unused_imports)]
use num_traits::Float;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct OKLab {
pub l: f32,
pub a: f32,
pub b: f32,
}
impl OKLab {
pub const fn new(l: f32, a: f32, b: f32) -> Self {
Self { l, a, b }
}
pub fn distance_sq(self, other: Self) -> f32 {
let dl = self.l - other.l;
let da = self.a - other.a;
let db = self.b - other.b;
dl * dl + da * da + db * db
}
pub const fn with_alpha(self, alpha: f32) -> OKLabA {
OKLabA { lab: self, alpha }
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct OKLabA {
pub lab: OKLab,
pub alpha: f32,
}
impl OKLabA {
pub const fn new(l: f32, a: f32, b: f32, alpha: f32) -> Self {
Self {
lab: OKLab::new(l, a, b),
alpha,
}
}
pub fn distance_sq(self, other: Self) -> f32 {
let avg_alpha = (self.alpha + other.alpha) * 0.5;
let color_dist = self.lab.distance_sq(other.lab) * avg_alpha;
let alpha_diff = self.alpha - other.alpha;
color_dist + alpha_diff * alpha_diff
}
}
#[inline(always)]
fn srgb_to_linear(c: u8) -> f32 {
linear_srgb::default::srgb_u8_to_linear(c)
}
#[inline(always)]
fn linear_to_srgb(c: f32) -> u8 {
linear_srgb::default::linear_to_srgb_u8(c.clamp(0.0, 1.0))
}
#[inline(always)]
pub(crate) fn fast_cbrt(x: f32) -> f32 {
if x == 0.0 {
return 0.0;
}
let bits = x.to_bits();
let mut y = f32::from_bits((bits / 3) + 0x2a51_7d48);
y = (2.0 * y + x / (y * y)) / 3.0;
y = (2.0 * y + x / (y * y)) / 3.0;
y
}
#[inline(always)]
#[allow(clippy::excessive_precision)]
pub(crate) fn srgb_to_oklab_l_fast(r: u8, g: u8, b: u8) -> f32 {
let r = srgb_to_linear(r);
let g = srgb_to_linear(g);
let b = srgb_to_linear(b);
let l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b;
let m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b;
let s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b;
0.2104542553 * fast_cbrt(l) + 0.7936177850 * fast_cbrt(m) - 0.0040720468 * fast_cbrt(s)
}
#[inline(always)]
#[allow(clippy::excessive_precision, dead_code)]
pub(crate) fn srgb_to_oklab_fast(r: u8, g: u8, b: u8) -> OKLab {
let r = srgb_to_linear(r);
let g = srgb_to_linear(g);
let b = srgb_to_linear(b);
let l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b;
let m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b;
let s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b;
let l_ = fast_cbrt(l);
let m_ = fast_cbrt(m);
let s_ = fast_cbrt(s);
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_,
}
}
#[allow(clippy::excessive_precision)]
pub fn srgb_to_oklab(r: u8, g: u8, b: u8) -> OKLab {
let r = srgb_to_linear(r);
let g = srgb_to_linear(g);
let b = srgb_to_linear(b);
let l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b;
let m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b;
let s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * 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_,
}
}
#[allow(clippy::excessive_precision)]
pub fn oklab_to_srgb(lab: OKLab) -> (u8, u8, u8) {
let l_ = lab.l + 0.3963377774 * lab.a + 0.2158037573 * lab.b;
let m_ = lab.l - 0.1055613458 * lab.a - 0.0638541728 * lab.b;
let s_ = lab.l - 0.0894841775 * lab.a - 1.2914855480 * lab.b;
let l = l_ * l_ * l_;
let m = m_ * m_ * m_;
let s = s_ * s_ * s_;
let r = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s;
let g = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s;
let b = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s;
(linear_to_srgb(r), linear_to_srgb(g), linear_to_srgb(b))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn black_roundtrip() {
let lab = srgb_to_oklab(0, 0, 0);
assert!(lab.l.abs() < 0.001);
assert!(lab.a.abs() < 0.001);
assert!(lab.b.abs() < 0.001);
let (r, g, b) = oklab_to_srgb(lab);
assert_eq!((r, g, b), (0, 0, 0));
}
#[test]
fn white_roundtrip() {
let lab = srgb_to_oklab(255, 255, 255);
assert!((lab.l - 1.0).abs() < 0.001);
assert!(lab.a.abs() < 0.001);
assert!(lab.b.abs() < 0.001);
let (r, g, b) = oklab_to_srgb(lab);
assert_eq!((r, g, b), (255, 255, 255));
}
#[test]
fn red_roundtrip() {
let lab = srgb_to_oklab(255, 0, 0);
let (r, g, b) = oklab_to_srgb(lab);
assert_eq!(r, 255);
assert!(g <= 1);
assert!(b <= 1);
}
#[test]
fn green_roundtrip() {
let lab = srgb_to_oklab(0, 255, 0);
let (r, g, b) = oklab_to_srgb(lab);
assert!(r <= 1);
assert_eq!(g, 255);
assert!(b <= 1);
}
#[test]
fn blue_roundtrip() {
let lab = srgb_to_oklab(0, 0, 255);
let (r, g, b) = oklab_to_srgb(lab);
assert!(r <= 1);
assert!(g <= 1);
assert_eq!(b, 255);
}
#[test]
fn midtone_roundtrip() {
let lab = srgb_to_oklab(128, 128, 128);
let (r, g, b) = oklab_to_srgb(lab);
assert!((r as i16 - 128).unsigned_abs() <= 1);
assert!((g as i16 - 128).unsigned_abs() <= 1);
assert!((b as i16 - 128).unsigned_abs() <= 1);
}
#[test]
fn distance_symmetric() {
let a = srgb_to_oklab(255, 0, 0);
let b = srgb_to_oklab(0, 0, 255);
assert!((a.distance_sq(b) - b.distance_sq(a)).abs() < 1e-10);
}
#[test]
fn distance_identity() {
let a = srgb_to_oklab(100, 150, 200);
assert!(a.distance_sq(a) < 1e-10);
}
#[test]
fn similar_colors_small_distance() {
let a = srgb_to_oklab(100, 100, 100);
let b = srgb_to_oklab(101, 100, 100);
let far = srgb_to_oklab(200, 50, 50);
assert!(a.distance_sq(b) < a.distance_sq(far));
}
#[test]
fn fast_cbrt_precision() {
for &x in &[0.0f32, 0.001, 0.01, 0.1, 0.5, 1.0, 2.0, 100.0] {
let expected = x.cbrt();
let got = super::fast_cbrt(x);
let rel_err = if expected == 0.0 {
got.abs()
} else {
(got - expected).abs() / expected
};
assert!(
rel_err < 1e-5,
"fast_cbrt({x}): expected {expected}, got {got}, rel_err {rel_err}"
);
}
}
#[test]
fn srgb_to_oklab_fast_matches_exact() {
let test_colors: &[(u8, u8, u8)] = &[
(0, 0, 0),
(255, 255, 255),
(255, 0, 0),
(0, 255, 0),
(0, 0, 255),
(128, 128, 128),
(200, 100, 50),
(10, 200, 150),
];
for &(r, g, b) in test_colors {
let exact = srgb_to_oklab(r, g, b);
let fast = super::srgb_to_oklab_fast(r, g, b);
let dist = exact.distance_sq(fast);
assert!(
dist < 1e-8,
"srgb_to_oklab_fast({r},{g},{b}): dist_sq={dist}, exact={exact:?}, fast={fast:?}"
);
}
}
#[test]
fn srgb_to_oklab_l_fast_matches_exact() {
let test_colors: &[(u8, u8, u8)] = &[
(0, 0, 0),
(255, 255, 255),
(128, 128, 128),
(255, 0, 0),
(0, 255, 0),
(0, 0, 255),
];
for &(r, g, b) in test_colors {
let exact = srgb_to_oklab(r, g, b).l;
let fast = super::srgb_to_oklab_l_fast(r, g, b);
let err = (exact - fast).abs();
assert!(
err < 1e-4,
"srgb_to_oklab_l_fast({r},{g},{b}): exact={exact}, fast={fast}, err={err}"
);
}
}
}