1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
5pub struct Lab {
6 pub l: f32,
7 pub a: f32,
8 pub b: f32,
9}
10
11#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
12pub struct Lch {
13 pub l: f32,
14 pub c: f32,
15 pub h: f32,
16}
17
18#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
19pub struct DirectionalDelta {
20 pub from: String,
21 pub to: String,
22 pub delta_l: f32,
23 pub delta_a: f32,
24 pub delta_b: f32,
25 pub delta_e00: f32,
26 pub michelson_lightness_contrast: f32,
27}
28
29pub fn srgb_to_lab(rgb: [u8; 3]) -> Lab {
30 let [r, g, b] = rgb.map(|channel| srgb_channel_to_linear(f32::from(channel) / 255.0));
31 let x = (0.412_456_4 * r + 0.357_576_1 * g + 0.180_437_5 * b) / 0.95047;
32 let y = 0.212_672_9 * r + 0.715_152_2 * g + 0.072_175 * b;
33 let z = (0.019_333_9 * r + 0.119_192 * g + 0.950_304_1 * b) / 1.08883;
34 let fx = xyz_pivot(x);
35 let fy = xyz_pivot(y);
36 let fz = xyz_pivot(z);
37 Lab {
38 l: 116.0 * fy - 16.0,
39 a: 500.0 * (fx - fy),
40 b: 200.0 * (fy - fz),
41 }
42}
43
44#[must_use]
45pub fn lab_to_lch(lab: Lab) -> Lch {
46 let c = (lab.a.mul_add(lab.a, lab.b * lab.b)).sqrt();
47 let mut h = lab.b.atan2(lab.a).to_degrees();
48 if h < 0.0 {
49 h += 360.0;
50 }
51 Lch { l: lab.l, c, h }
52}
53
54#[must_use]
55pub fn ita_degrees(lab: Lab) -> f32 {
56 ((lab.l - 50.0) / lab.b.max(0.001)).atan().to_degrees()
57}
58
59#[must_use]
60pub fn depth_proxy(lab: Lab) -> f32 {
61 crate::score::clamp01(1.0 - ((ita_degrees(lab) + 55.0) / 110.0))
62}
63
64#[must_use]
65pub fn michelson_lightness_contrast(first: Lab, second: Lab) -> f32 {
66 ((first.l - second.l).abs() / (first.l + second.l).max(0.001)).clamp(0.0, 1.0)
67}
68
69#[must_use]
70pub fn directional_delta(
71 from_name: impl Into<String>,
72 from: Lab,
73 to_name: impl Into<String>,
74 to: Lab,
75) -> DirectionalDelta {
76 DirectionalDelta {
77 from: from_name.into(),
78 to: to_name.into(),
79 delta_l: to.l - from.l,
80 delta_a: to.a - from.a,
81 delta_b: to.b - from.b,
82 delta_e00: delta_e00(from, to),
83 michelson_lightness_contrast: michelson_lightness_contrast(from, to),
84 }
85}
86
87#[must_use]
88pub fn delta_e00(first: Lab, second: Lab) -> f32 {
89 let c1 = (first.a.mul_add(first.a, first.b * first.b)).sqrt();
90 let c2 = (second.a.mul_add(second.a, second.b * second.b)).sqrt();
91 let c_bar = (c1 + c2) / 2.0;
92 let c_bar7 = c_bar.powi(7);
93 let g = 0.5 * (1.0 - (c_bar7 / (c_bar7 + 25_f32.powi(7))).sqrt());
94 let a1p = (1.0 + g) * first.a;
95 let a2p = (1.0 + g) * second.a;
96 let c1p = (a1p.mul_add(a1p, first.b * first.b)).sqrt();
97 let c2p = (a2p.mul_add(a2p, second.b * second.b)).sqrt();
98 let h1p = hue_degrees(first.b, a1p);
99 let h2p = hue_degrees(second.b, a2p);
100 let dlp = second.l - first.l;
101 let dcp = c2p - c1p;
102 let dhp = hue_delta(h1p, h2p, c1p, c2p);
103 let dh_big = 2.0 * (c1p * c2p).sqrt() * (dhp.to_radians() / 2.0).sin();
104 let lp_bar = (first.l + second.l) / 2.0;
105 let cp_bar = (c1p + c2p) / 2.0;
106 let hp_bar = hue_average(h1p, h2p, c1p, c2p);
107 let t = 1.0 - 0.17 * (hp_bar - 30.0).to_radians().cos()
108 + 0.24 * (2.0 * hp_bar).to_radians().cos()
109 + 0.32 * (3.0 * hp_bar + 6.0).to_radians().cos()
110 - 0.20 * (4.0 * hp_bar - 63.0).to_radians().cos();
111 let sl = 1.0 + (0.015 * (lp_bar - 50.0).powi(2)) / (20.0 + (lp_bar - 50.0).powi(2)).sqrt();
112 let sc = 1.0 + 0.045 * cp_bar;
113 let sh = 1.0 + 0.015 * cp_bar * t;
114 let delta_theta = 30.0 * (-((hp_bar - 275.0) / 25.0).powi(2)).exp();
115 let rc = 2.0 * (cp_bar.powi(7) / (cp_bar.powi(7) + 25_f32.powi(7))).sqrt();
116 let rt = -rc * (2.0 * delta_theta).to_radians().sin();
117 let l_term = dlp / sl;
118 let c_term = dcp / sc;
119 let h_term = dh_big / sh;
120 (l_term.powi(2) + c_term.powi(2) + h_term.powi(2) + rt * c_term * h_term)
121 .max(0.0)
122 .sqrt()
123}
124
125#[must_use]
126pub fn srgb_channel_to_linear(value: f32) -> f32 {
127 if value <= 0.04045 {
128 value / 12.92
129 } else {
130 ((value + 0.055) / 1.055).powf(2.4)
131 }
132}
133
134fn xyz_pivot(value: f32) -> f32 {
135 if value > 216.0 / 24389.0 {
136 value.cbrt()
137 } else {
138 (841.0 / 108.0) * value + 4.0 / 29.0
139 }
140}
141
142fn hue_degrees(b: f32, a: f32) -> f32 {
143 if b == 0.0 && a == 0.0 {
144 0.0
145 } else {
146 b.atan2(a).to_degrees().rem_euclid(360.0)
147 }
148}
149fn hue_delta(h1: f32, h2: f32, c1: f32, c2: f32) -> f32 {
150 if c1 * c2 == 0.0 {
151 0.0
152 } else if (h2 - h1).abs() <= 180.0 {
153 h2 - h1
154 } else if h2 <= h1 {
155 h2 - h1 + 360.0
156 } else {
157 h2 - h1 - 360.0
158 }
159}
160fn hue_average(h1: f32, h2: f32, c1: f32, c2: f32) -> f32 {
161 if c1 * c2 == 0.0 {
162 h1 + h2
163 } else if (h1 - h2).abs() <= 180.0 {
164 (h1 + h2) / 2.0
165 } else if h1 + h2 < 360.0 {
166 (h1 + h2 + 360.0) / 2.0
167 } else {
168 (h1 + h2 - 360.0) / 2.0
169 }
170}