#[derive(Debug, Clone, Copy, PartialEq, Default)]
#[repr(C)]
pub struct Oklab {
pub l: f32,
pub a: f32,
pub b: f32,
}
impl Oklab {
#[must_use]
pub const fn new(l: f32, a: f32, b: f32) -> Self {
Self { l, a, b }
}
#[must_use]
pub fn lerp(self, other: Self, t: f32) -> Self {
use crate::space::math::lerp_f32;
Self {
l: lerp_f32(self.l, other.l, t),
a: lerp_f32(self.a, other.a, t),
b: lerp_f32(self.b, other.b, t),
}
}
#[must_use]
#[allow(clippy::suboptimal_flops)]
pub fn chroma(self) -> f32 {
crate::space::math::sqrt(self.a * self.a + self.b * self.b)
}
#[must_use]
pub const fn clamp_lightness(self) -> Self {
Self {
l: self.l.clamp(0.0, 1.0),
..self
}
}
}
impl From<[f32; 3]> for Oklab {
fn from([l, a, b]: [f32; 3]) -> Self {
Self { l, a, b }
}
}
impl From<Oklab> for [f32; 3] {
fn from(c: Oklab) -> Self {
[c.l, c.a, c.b]
}
}
#[cfg(test)]
#[allow(clippy::float_cmp)]
mod tests {
use super::*;
use crate::space::Srgb;
#[test]
fn black_is_zero_lightness() {
let lab = Oklab::from(Srgb::BLACK);
assert!(lab.l.abs() < 1e-4, "l={}", lab.l);
assert!(lab.a.abs() < 1e-4, "a={}", lab.a);
assert!(lab.b.abs() < 1e-4, "b={}", lab.b);
}
#[test]
fn white_has_max_lightness() {
let lab = Oklab::from(Srgb::WHITE);
assert!((lab.l - 1.0).abs() < 1e-4, "l={}", lab.l);
}
#[test]
fn srgb_roundtrip() {
for &srgb in &[Srgb::RED, Srgb::GREEN, Srgb::BLUE, Srgb::WHITE] {
let lab = Oklab::from(srgb);
let back = Srgb::from(lab).clamp();
assert!(
(back.r - srgb.r).abs() < 0.01,
"r: {} vs {}",
back.r,
srgb.r
);
assert!(
(back.g - srgb.g).abs() < 0.01,
"g: {} vs {}",
back.g,
srgb.g
);
assert!(
(back.b - srgb.b).abs() < 0.01,
"b: {} vs {}",
back.b,
srgb.b
);
}
}
#[test]
fn gray_low_chroma() {
let gray = Oklab::from(Srgb::new(0.5, 0.5, 0.5));
assert!(gray.chroma() < 0.01, "chroma={}", gray.chroma());
}
#[test]
fn lerp_midpoint_lightness() {
let black = Oklab::from(Srgb::BLACK);
let white = Oklab::from(Srgb::WHITE);
let mid = black.lerp(white, 0.5);
assert!((mid.l - 0.5).abs() < 0.05, "l={}", mid.l);
}
#[test]
fn from_array_roundtrip() {
let lab = Oklab::new(0.5, 0.1, -0.1);
let arr: [f32; 3] = lab.into();
assert!((arr[0] - 0.5).abs() < f32::EPSILON);
assert!((arr[1] - 0.1).abs() < f32::EPSILON);
assert!((arr[2] - (-0.1)).abs() < f32::EPSILON);
let back = Oklab::from(arr);
assert!((back.l - 0.5).abs() < f32::EPSILON);
}
}