Skip to main content

chromaframe_sdk/
color.rs

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}