gpui_kit_material_color/dynamic/
hct.rs1use super::solver;
8use super::utils::{
9 WHITE_POINT_D65, argb_from_xyz, blue_from_argb, clamp_double, green_from_argb, lerp,
10 linearized, lstar_from_argb, lstar_from_y, red_from_argb, sanitize_degrees_double, signum,
11 y_from_lstar,
12};
13use std::f64::consts::PI;
14use std::sync::LazyLock;
15
16#[derive(Clone, Copy, Debug, PartialEq)]
19pub struct ViewingConditions {
20 pub n: f64,
22 pub aw: f64,
24 pub nbb: f64,
26 pub ncb: f64,
28 pub c: f64,
30 pub nc: f64,
32 pub rgb_d: [f64; 3],
34 pub fl: f64,
36 pub fl_root: f64,
38 pub z: f64,
40}
41
42static DEFAULT_CONDITIONS: LazyLock<ViewingConditions> = LazyLock::new(|| {
43 ViewingConditions::make(
44 WHITE_POINT_D65,
45 (200. / PI) * y_from_lstar(50.) / 100.,
46 50.,
47 2.,
48 false,
49 )
50});
51
52impl ViewingConditions {
53 pub fn standard() -> &'static Self {
55 &DEFAULT_CONDITIONS
56 }
57
58 pub fn make(
62 white_point: [f64; 3],
63 adapting_luminance: f64,
64 background_lstar: f64,
65 surround: f64,
66 discounting_illuminant: bool,
67 ) -> Self {
68 let [x, y, z] = white_point;
69 let r_w = x * 0.401288 + y * 0.650173 + z * -0.051461;
70 let g_w = x * -0.250268 + y * 1.204414 + z * 0.045854;
71 let b_w = x * -0.002079 + y * 0.048952 + z * 0.953127;
72 let f = 0.8 + surround / 10.;
73 let c = if f >= 0.9 {
74 lerp(0.59, 0.69, (f - 0.9) * 10.)
75 } else {
76 lerp(0.525, 0.59, (f - 0.8) * 10.)
77 };
78 let d = if discounting_illuminant {
79 1.
80 } else {
81 f * (1. - (1. / 3.6) * ((-adapting_luminance - 42.) / 92.).exp())
82 };
83 let d = clamp_double(0., 1., d);
84 let rgb_d = [
85 d * (100. / r_w) + 1. - d,
86 d * (100. / g_w) + 1. - d,
87 d * (100. / b_w) + 1. - d,
88 ];
89 let k = 1. / (5. * adapting_luminance + 1.);
90 let k4 = k * k * k * k;
91 let k4f = 1. - k4;
92 let fl = k4 * adapting_luminance + 0.1 * k4f * k4f * (5. * adapting_luminance).cbrt();
93 let n = y_from_lstar(background_lstar) / y;
94 let z_base = 1.48 + n.sqrt();
95 let nbb = 0.725 / n.powf(0.2);
96 let factors = [
97 ((fl * rgb_d[0] * r_w) / 100.).powf(0.42),
98 ((fl * rgb_d[1] * g_w) / 100.).powf(0.42),
99 ((fl * rgb_d[2] * b_w) / 100.).powf(0.42),
100 ];
101 let rgb_a = factors.map(|factor| (400. * factor) / (factor + 27.13));
102 let aw = (2. * rgb_a[0] + rgb_a[1] + 0.05 * rgb_a[2]) * nbb;
103 Self {
104 n,
105 aw,
106 nbb,
107 ncb: nbb,
108 c,
109 nc: f,
110 rgb_d,
111 fl,
112 fl_root: fl.powf(0.25),
113 z: z_base,
114 }
115 }
116}
117
118#[derive(Clone, Copy, Debug, PartialEq)]
120pub struct Cam16 {
121 pub hue: f64,
123 pub chroma: f64,
125 pub j: f64,
127 pub q: f64,
129 pub m: f64,
131 pub s: f64,
133 pub jstar: f64,
135 pub astar: f64,
137 pub bstar: f64,
139}
140
141impl Cam16 {
142 pub fn distance(&self, other: &Self) -> f64 {
144 let d_j = self.jstar - other.jstar;
145 let d_a = self.astar - other.astar;
146 let d_b = self.bstar - other.bstar;
147 1.41 * (d_j * d_j + d_a * d_a + d_b * d_b).sqrt().powf(0.63)
148 }
149
150 pub fn from_argb(argb: u32) -> Self {
152 let red = linearized(red_from_argb(argb));
153 let green = linearized(green_from_argb(argb));
154 let blue = linearized(blue_from_argb(argb));
155 let x = 0.41233895 * red + 0.35762064 * green + 0.18051042 * blue;
156 let y = 0.2126 * red + 0.7152 * green + 0.0722 * blue;
157 let z = 0.01932141 * red + 0.11916382 * green + 0.95034478 * blue;
158 Self::from_xyz_in(x, y, z, ViewingConditions::standard())
159 }
160
161 pub fn from_xyz_in(x: f64, y: f64, z: f64, vc: &ViewingConditions) -> Self {
163 let r_c = 0.401288 * x + 0.650173 * y - 0.051461 * z;
164 let g_c = -0.250268 * x + 1.204414 * y + 0.045854 * z;
165 let b_c = -0.002079 * x + 0.048952 * y + 0.953127 * z;
166 let r_d = vc.rgb_d[0] * r_c;
167 let g_d = vc.rgb_d[1] * g_c;
168 let b_d = vc.rgb_d[2] * b_c;
169 let adapt = |d: f64| {
170 let af = (vc.fl * d.abs() / 100.).powf(0.42);
171 signum(d) * 400. * af / (af + 27.13)
172 };
173 let (r_a, g_a, b_a) = (adapt(r_d), adapt(g_d), adapt(b_d));
174 let a = (11. * r_a + -12. * g_a + b_a) / 11.;
175 let b = (r_a + g_a - 2. * b_a) / 9.;
176 let u = (20. * r_a + 20. * g_a + 21. * b_a) / 20.;
177 let p2 = (40. * r_a + 20. * g_a + b_a) / 20.;
178 let hue = sanitize_degrees_double(b.atan2(a) * 180. / PI);
179 let hue_radians = hue * PI / 180.;
180 let ac = p2 * vc.nbb;
181 let j = 100. * (ac / vc.aw).powf(vc.c * vc.z);
182 let q = (4. / vc.c) * (j / 100.).sqrt() * (vc.aw + 4.) * vc.fl_root;
183 let hue_prime = if hue < 20.14 { hue + 360. } else { hue };
184 let e_hue = 0.25 * ((hue_prime * PI / 180. + 2.).cos() + 3.8);
185 let p1 = (50000. / 13.) * e_hue * vc.nc * vc.ncb;
186 let t = (p1 * (a * a + b * b).sqrt()) / (u + 0.305);
187 let alpha = t.powf(0.9) * (1.64 - 0.29_f64.powf(vc.n)).powf(0.73);
188 let c = alpha * (j / 100.).sqrt();
189 let m = c * vc.fl_root;
190 let s = 50. * ((alpha * vc.c) / (vc.aw + 4.)).sqrt();
191 let jstar = ((1. + 100. * 0.007) * j) / (1. + 0.007 * j);
192 let mstar = (1. / 0.0228) * (1. + 0.0228 * m).ln();
193 Self {
194 hue,
195 chroma: c,
196 j,
197 q,
198 m,
199 s,
200 jstar,
201 astar: mstar * hue_radians.cos(),
202 bstar: mstar * hue_radians.sin(),
203 }
204 }
205
206 pub fn from_jch(j: f64, c: f64, h: f64) -> Self {
208 Self::from_jch_in(j, c, h, ViewingConditions::standard())
209 }
210
211 pub fn from_jch_in(j: f64, c: f64, h: f64, vc: &ViewingConditions) -> Self {
213 let q = (4. / vc.c) * (j / 100.).sqrt() * (vc.aw + 4.) * vc.fl_root;
214 let m = c * vc.fl_root;
215 let alpha = c / (j / 100.).sqrt();
216 let s = 50. * ((alpha * vc.c) / (vc.aw + 4.)).sqrt();
217 let hue_radians = h * PI / 180.;
218 let jstar = ((1. + 100. * 0.007) * j) / (1. + 0.007 * j);
219 let mstar = (1. / 0.0228) * (1. + 0.0228 * m).ln();
220 Self {
221 hue: h,
222 chroma: c,
223 j,
224 q,
225 m,
226 s,
227 jstar,
228 astar: mstar * hue_radians.cos(),
229 bstar: mstar * hue_radians.sin(),
230 }
231 }
232
233 pub fn from_ucs(jstar: f64, astar: f64, bstar: f64) -> Self {
235 let vc = ViewingConditions::standard();
236 let m = (astar * astar + bstar * bstar).sqrt();
237 let big_m = ((m * 0.0228).exp() - 1.) / 0.0228;
238 let c = big_m / vc.fl_root;
239 let mut h = bstar.atan2(astar) * (180. / PI);
240 if h < 0. {
241 h += 360.;
242 }
243 let j = jstar / (1. - (jstar - 100.) * 0.007);
244 Self::from_jch_in(j, c, h, vc)
245 }
246
247 pub fn to_argb(&self) -> u32 {
249 let [x, y, z] = self.xyz_in(ViewingConditions::standard());
250 argb_from_xyz(x, y, z)
251 }
252
253 pub fn xyz_in(&self, vc: &ViewingConditions) -> [f64; 3] {
255 let alpha = if self.chroma == 0. || self.j == 0. {
256 0.
257 } else {
258 self.chroma / (self.j / 100.).sqrt()
259 };
260 let t = (alpha / (1.64 - 0.29_f64.powf(vc.n)).powf(0.73)).powf(1. / 0.9);
261 let h_rad = self.hue * PI / 180.;
262 let e_hue = 0.25 * ((h_rad + 2.).cos() + 3.8);
263 let ac = vc.aw * (self.j / 100.).powf(1. / vc.c / vc.z);
264 let p1 = e_hue * (50000. / 13.) * vc.nc * vc.ncb;
265 let p2 = ac / vc.nbb;
266 let (h_sin, h_cos) = h_rad.sin_cos();
267 let gamma = (23. * (p2 + 0.305) * t) / (23. * p1 + 11. * t * h_cos + 108. * t * h_sin);
268 let a = gamma * h_cos;
269 let b = gamma * h_sin;
270 let r_a = (460. * p2 + 451. * a + 288. * b) / 1403.;
271 let g_a = (460. * p2 - 891. * a - 261. * b) / 1403.;
272 let b_a = (460. * p2 - 220. * a - 6300. * b) / 1403.;
273 let unadapt = |adapted: f64| {
274 let base = ((27.13 * adapted.abs()) / (400. - adapted.abs())).max(0.);
275 signum(adapted) * (100. / vc.fl) * base.powf(1. / 0.42)
276 };
277 let r_f = unadapt(r_a) / vc.rgb_d[0];
278 let g_f = unadapt(g_a) / vc.rgb_d[1];
279 let b_f = unadapt(b_a) / vc.rgb_d[2];
280 [
281 1.86206786 * r_f - 1.01125463 * g_f + 0.14918677 * b_f,
282 0.38752654 * r_f + 0.62144744 * g_f - 0.00897398 * b_f,
283 -0.01584150 * r_f - 0.03412294 * g_f + 1.04996444 * b_f,
284 ]
285 }
286}
287
288#[derive(Clone, Copy, Debug, PartialEq)]
291pub struct Hct {
292 hue: f64,
293 chroma: f64,
294 tone: f64,
295 argb: u32,
296}
297
298impl Hct {
299 pub fn from_hct(hue: f64, chroma: f64, tone: f64) -> Self {
303 let finite = |value: f64| if value.is_finite() { value } else { 0. };
304 Self::from_argb(solver::solve_to_argb(
305 finite(hue),
306 finite(chroma),
307 finite(tone),
308 ))
309 }
310
311 pub fn from_argb(argb: u32) -> Self {
314 let cam = Cam16::from_argb(argb);
315 Self {
316 hue: cam.hue,
317 chroma: cam.chroma,
318 tone: lstar_from_argb(argb),
319 argb,
320 }
321 }
322
323 pub fn hue(&self) -> f64 {
325 self.hue
326 }
327
328 pub fn chroma(&self) -> f64 {
330 self.chroma
331 }
332
333 pub fn tone(&self) -> f64 {
335 self.tone
336 }
337
338 pub fn argb(&self) -> u32 {
340 self.argb
341 }
342
343 pub fn is_blue(hue: f64) -> bool {
345 (250. ..270.).contains(&hue)
346 }
347
348 pub fn is_yellow(hue: f64) -> bool {
350 (105. ..125.).contains(&hue)
351 }
352
353 pub fn is_cyan(hue: f64) -> bool {
355 (170. ..207.).contains(&hue)
356 }
357
358 pub fn in_viewing_conditions(&self, vc: &ViewingConditions) -> Self {
361 let [x, y, z] = Cam16::from_argb(self.argb).xyz_in(vc);
362 let recast = Cam16::from_xyz_in(x, y, z, ViewingConditions::standard());
363 Self::from_hct(recast.hue, recast.chroma, lstar_from_y(y))
364 }
365}