use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct Lab {
pub l: f32,
pub a: f32,
pub b: f32,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct Lch {
pub l: f32,
pub c: f32,
pub h: f32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct DirectionalDelta {
pub from: String,
pub to: String,
pub delta_l: f32,
pub delta_a: f32,
pub delta_b: f32,
pub delta_e00: f32,
pub michelson_lightness_contrast: f32,
}
pub fn srgb_to_lab(rgb: [u8; 3]) -> Lab {
let [r, g, b] = rgb.map(|channel| srgb_channel_to_linear(f32::from(channel) / 255.0));
let x = (0.412_456_4 * r + 0.357_576_1 * g + 0.180_437_5 * b) / 0.95047;
let y = 0.212_672_9 * r + 0.715_152_2 * g + 0.072_175 * b;
let z = (0.019_333_9 * r + 0.119_192 * g + 0.950_304_1 * b) / 1.08883;
let fx = xyz_pivot(x);
let fy = xyz_pivot(y);
let fz = xyz_pivot(z);
Lab {
l: 116.0 * fy - 16.0,
a: 500.0 * (fx - fy),
b: 200.0 * (fy - fz),
}
}
#[must_use]
pub fn lab_to_lch(lab: Lab) -> Lch {
let c = (lab.a.mul_add(lab.a, lab.b * lab.b)).sqrt();
let mut h = lab.b.atan2(lab.a).to_degrees();
if h < 0.0 {
h += 360.0;
}
Lch { l: lab.l, c, h }
}
#[must_use]
pub fn ita_degrees(lab: Lab) -> f32 {
((lab.l - 50.0) / lab.b.max(0.001)).atan().to_degrees()
}
#[must_use]
pub fn depth_proxy(lab: Lab) -> f32 {
crate::score::clamp01(1.0 - ((ita_degrees(lab) + 55.0) / 110.0))
}
#[must_use]
pub fn michelson_lightness_contrast(first: Lab, second: Lab) -> f32 {
((first.l - second.l).abs() / (first.l + second.l).max(0.001)).clamp(0.0, 1.0)
}
#[must_use]
pub fn directional_delta(
from_name: impl Into<String>,
from: Lab,
to_name: impl Into<String>,
to: Lab,
) -> DirectionalDelta {
DirectionalDelta {
from: from_name.into(),
to: to_name.into(),
delta_l: to.l - from.l,
delta_a: to.a - from.a,
delta_b: to.b - from.b,
delta_e00: delta_e00(from, to),
michelson_lightness_contrast: michelson_lightness_contrast(from, to),
}
}
#[must_use]
pub fn delta_e00(first: Lab, second: Lab) -> f32 {
let c1 = (first.a.mul_add(first.a, first.b * first.b)).sqrt();
let c2 = (second.a.mul_add(second.a, second.b * second.b)).sqrt();
let c_bar = (c1 + c2) / 2.0;
let c_bar7 = c_bar.powi(7);
let g = 0.5 * (1.0 - (c_bar7 / (c_bar7 + 25_f32.powi(7))).sqrt());
let a1p = (1.0 + g) * first.a;
let a2p = (1.0 + g) * second.a;
let c1p = (a1p.mul_add(a1p, first.b * first.b)).sqrt();
let c2p = (a2p.mul_add(a2p, second.b * second.b)).sqrt();
let h1p = hue_degrees(first.b, a1p);
let h2p = hue_degrees(second.b, a2p);
let dlp = second.l - first.l;
let dcp = c2p - c1p;
let dhp = hue_delta(h1p, h2p, c1p, c2p);
let dh_big = 2.0 * (c1p * c2p).sqrt() * (dhp.to_radians() / 2.0).sin();
let lp_bar = (first.l + second.l) / 2.0;
let cp_bar = (c1p + c2p) / 2.0;
let hp_bar = hue_average(h1p, h2p, c1p, c2p);
let t = 1.0 - 0.17 * (hp_bar - 30.0).to_radians().cos()
+ 0.24 * (2.0 * hp_bar).to_radians().cos()
+ 0.32 * (3.0 * hp_bar + 6.0).to_radians().cos()
- 0.20 * (4.0 * hp_bar - 63.0).to_radians().cos();
let sl = 1.0 + (0.015 * (lp_bar - 50.0).powi(2)) / (20.0 + (lp_bar - 50.0).powi(2)).sqrt();
let sc = 1.0 + 0.045 * cp_bar;
let sh = 1.0 + 0.015 * cp_bar * t;
let delta_theta = 30.0 * (-((hp_bar - 275.0) / 25.0).powi(2)).exp();
let rc = 2.0 * (cp_bar.powi(7) / (cp_bar.powi(7) + 25_f32.powi(7))).sqrt();
let rt = -rc * (2.0 * delta_theta).to_radians().sin();
let l_term = dlp / sl;
let c_term = dcp / sc;
let h_term = dh_big / sh;
(l_term.powi(2) + c_term.powi(2) + h_term.powi(2) + rt * c_term * h_term)
.max(0.0)
.sqrt()
}
#[must_use]
pub fn srgb_channel_to_linear(value: f32) -> f32 {
if value <= 0.04045 {
value / 12.92
} else {
((value + 0.055) / 1.055).powf(2.4)
}
}
fn xyz_pivot(value: f32) -> f32 {
if value > 216.0 / 24389.0 {
value.cbrt()
} else {
(841.0 / 108.0) * value + 4.0 / 29.0
}
}
fn hue_degrees(b: f32, a: f32) -> f32 {
if b == 0.0 && a == 0.0 {
0.0
} else {
b.atan2(a).to_degrees().rem_euclid(360.0)
}
}
fn hue_delta(h1: f32, h2: f32, c1: f32, c2: f32) -> f32 {
if c1 * c2 == 0.0 {
0.0
} else if (h2 - h1).abs() <= 180.0 {
h2 - h1
} else if h2 <= h1 {
h2 - h1 + 360.0
} else {
h2 - h1 - 360.0
}
}
fn hue_average(h1: f32, h2: f32, c1: f32, c2: f32) -> f32 {
if c1 * c2 == 0.0 {
h1 + h2
} else if (h1 - h2).abs() <= 180.0 {
(h1 + h2) / 2.0
} else if h1 + h2 < 360.0 {
(h1 + h2 + 360.0) / 2.0
} else {
(h1 + h2 - 360.0) / 2.0
}
}