1#![allow(non_snake_case)]
5use crate::render::style::color_pro::*;
6use lazy_static::lazy_static;
7use std::f64::consts::PI;
8
9lazy_static! {
10 static ref ENVS: Vec<Environment> = {
11 let v1 = environment(WHITE, 64.0 / PI * 0.2, 20.0, &SURROUND_MAP[2], false);
12 let v2 = environment(
13 WHITE,
14 200.0 / PI * from_lstar(50.0),
15 from_lstar(50.0) * 100.0,
16 &SURROUND_MAP[2],
17 false,
18 );
19 vec![v1, v2]
20 };
25}
26
27const ADAPTED_COEF: f64 = 0.42;
28const ADAPTED_COEF_INV: f64 = 1.0 / ADAPTED_COEF;
29const TAU: f64 = 2.0 * PI;
30
31const CAT16: [[f64; 3]; 3] = [
32 [0.401288, 0.650173, -0.051461],
33 [-0.250268, 1.204414, 0.045854],
34 [-0.002079, 0.048952, 0.953127],
35];
36
37const CAT16_INV: [[f64; 3]; 3] = [
38 [1.8620678550872327, -1.0112546305316843, 0.14918677544445175],
39 [
40 0.38752654323613717,
41 0.6214474419314753,
42 -0.008973985167612518,
43 ],
44 [
45 -0.015841498849333856,
46 -0.03412293802851557,
47 1.0499644368778496,
48 ],
49];
50
51const M1: [[f64; 3]; 3] = [
52 [460.0, 451.0, 288.0],
53 [460.0, -891.0, -261.0],
54 [460.0, -220.0, -6300.0],
55];
56
57const SURROUND_MAP: [[f64; 3]; 3] = [[0.8, 0.525, 0.8], [0.9, 0.59, 0.9], [1.0, 0.69, 1.0]];
58
59const HUE_QUAD_MAP: ([f64; 5], [f64; 5], [f64; 5]) = (
60 [20.14, 90.00, 164.25, 237.53, 380.14],
61 [0.8, 0.7, 1.0, 1.2, 0.8],
62 [0.0, 100.0, 200.0, 300.0, 400.0],
63);
64
65const RAD2DEG: f64 = 180.0 / PI;
66const DEG2RAD: f64 = PI / 180.0;
67
68fn spow(x: f64, y: f64) -> f64 {
69 x.powf(y)
70}
71
72fn copy_sign(x: f64, y: f64) -> f64 {
73 x.copysign(y)
74}
75
76fn multiply_matrices(a: [[f64; 3]; 3], b: [f64; 3]) -> [f64; 3] {
77 let mut result = [0.0; 3];
78 for i in 0..3 {
79 for (j, item) in b.iter().enumerate() {
80 result[i] += a[i][j] * item;
81 }
82 }
83 result
84}
85
86fn interpolate(a: f64, b: f64, t: f64) -> f64 {
87 a + t * (b - a)
88}
89
90fn zdiv(a: f64, b: f64) -> f64 {
91 if b == 0.0 {
92 0.0
93 } else {
94 a / b
95 }
96}
97
98fn constrain(v: f64) -> f64 {
99 if v < 0.0 {
100 v + 360.0
101 } else {
102 v % 360.0
103 }
104}
105
106fn adapt(coords: [f64; 3], fl: f64) -> [f64; 3] {
107 coords.map(|c| {
108 let x = spow(fl * c.abs() * 0.01, ADAPTED_COEF);
109 400.0 * copy_sign(x, c) / (x + 27.13)
110 })
111}
112
113fn unadapt(adapted: [f64; 3], fl: f64) -> [f64; 3] {
114 let constant = 100.0 / fl * (27.13_f64).powf(ADAPTED_COEF_INV);
115 adapted.map(|c| {
116 let cabs = c.abs();
117 copy_sign(constant * spow(cabs / (400.0 - cabs), ADAPTED_COEF_INV), c)
118 })
119}
120
121fn hue_quadrature(h: f64) -> f64 {
122 let mut hp = constrain(h);
123 if hp <= HUE_QUAD_MAP.0[0] {
124 hp += 360.0;
125 }
126
127 let i = HUE_QUAD_MAP.0.iter().position(|&x| x > hp).unwrap() - 1;
128 let hi = HUE_QUAD_MAP.0[i];
129 let hii = HUE_QUAD_MAP.0[i + 1];
130 let ei = HUE_QUAD_MAP.1[i];
131 let eii = HUE_QUAD_MAP.1[i + 1];
132 let Hi = HUE_QUAD_MAP.2[i];
133
134 let t = (hp - hi) / ei;
135 Hi + (100.0 * t) / (t + (hii - hp) / eii)
136}
137
138fn inv_hue_quadrature(H: f64) -> f64 {
139 let Hp = (H % 400.0 + 400.0) % 400.0;
140 let i = (Hp / 100.0).floor() as usize;
141 let Hp = Hp % 100.0;
142 let hi = HUE_QUAD_MAP.0[i];
143 let hii = HUE_QUAD_MAP.0[i + 1];
144 let ei = HUE_QUAD_MAP.1[i];
145 let eii = HUE_QUAD_MAP.1[i + 1];
146
147 constrain((Hp * (eii * hi - ei * hii) - 100.0 * hi * eii) / (Hp * (eii - ei) - 100.0 * eii))
148}
149
150#[derive(Debug)]
151struct Environment {
152 fl: f64,
153 fl_root: f64,
154 n: f64,
155 z: f64,
156 nbb: f64,
157 ncb: f64,
158 c: f64,
159 nc: f64,
160 d_rgb: [f64; 3],
161 d_rgb_inv: [f64; 3],
162 a_w: f64,
163}
164
165fn environment(
166 ref_white: [f64; 3],
167 adapting_luminance: f64,
168 background_luminance: f64,
169 surround: &'static [f64; 3],
170 discounting: bool,
171) -> Environment {
172 let xyz_w = ref_white.map(|c| c * 100.0);
173 let la = adapting_luminance;
174 let yb = background_luminance;
175 let yw = xyz_w[1];
176 let rgb_w = multiply_matrices(CAT16, xyz_w);
177
178 let f = surround[0];
179 let c = surround[1];
180 let nc = surround[2];
181
182 let k = 1.0 / (5.0 * la + 1.0);
183 let k4 = k.powi(4);
184
185 let fl = k4 * la + 0.1 * (1.0 - k4) * (1.0 - k4) * (5.0 * la).cbrt();
186 let fl_root = fl.powf(0.25);
187
188 let n = yb / yw;
189 let z = 1.48 + n.sqrt();
190 let nbb = 0.725 * n.powf(-0.2);
191 let ncb = nbb;
192
193 let d = if discounting {
194 1.0
195 } else {
196 f * (1.0 - 1.0 / 3.6 * ((-la - 42.0) / 92.0).exp()).clamp(0.0, 1.0)
197 };
198 let d_rgb = rgb_w.map(|c| interpolate(1.0, yw / c, d));
199 let d_rgb_inv = d_rgb.map(|c| 1.0 / c);
200
201 let rgb_cw = [
202 rgb_w[0] * d_rgb[0],
203 rgb_w[1] * d_rgb[1],
204 rgb_w[2] * d_rgb[2],
205 ];
206 let rgb_aw = adapt(rgb_cw, fl);
207 let a_w = nbb * (2.0 * rgb_aw[0] + rgb_aw[1] + 0.05 * rgb_aw[2]);
208
209 Environment {
210 fl,
211 fl_root,
212 n,
213 z,
214 nbb,
215 ncb,
216 c,
217 nc,
218 d_rgb,
219 d_rgb_inv,
220 a_w,
221 }
222}
223
224fn from_cam16(cam16: &Cam16Object, env: &Environment) -> [f64; 3] {
225 if !(cam16.J.is_some() ^ cam16.Q.is_some()) {
226 panic!("Conversion requires one and only one: 'J' or 'Q'");
227 }
228
229 if !(cam16.C.is_some() ^ cam16.M.is_some() ^ cam16.s.is_some()) {
230 panic!("Conversion requires one and only one: 'C', 'M' or 's'");
231 }
232
233 if !(cam16.h.is_some() ^ cam16.H.is_some()) {
234 panic!("Conversion requires one and only one: 'h' or 'H'");
235 }
236
237 if cam16.J == Some(0.0) || cam16.Q == Some(0.0) {
238 return [0.0, 0.0, 0.0];
239 }
240
241 let h_rad = if let Some(h) = cam16.h {
242 constrain(h) * DEG2RAD
243 } else {
244 inv_hue_quadrature(cam16.H.unwrap()) * DEG2RAD
245 };
246
247 let cos_h = h_rad.cos();
248 let sin_h = h_rad.sin();
249
250 let j_root = if let Some(J) = cam16.J {
251 spow(J, 0.5) * 0.1
252 } else {
253 0.25 * env.c * cam16.Q.unwrap() / ((env.a_w + 4.0) * env.fl_root)
254 };
255
256 let alpha = if let Some(C) = cam16.C {
257 C / j_root
258 } else if let Some(M) = cam16.M {
259 (M / env.fl_root) / j_root
260 } else {
261 0.0004 * spow(cam16.s.unwrap(), 2.0) * (env.a_w + 4.0) / env.c
262 };
263
264 let t = spow(alpha * spow(1.64 - spow(0.29, env.n), -0.73), 10.0 / 9.0);
265
266 let et = 0.25 * ((h_rad + 2.0).cos() + 3.8);
267
268 let A = env.a_w * spow(j_root, 2.0 / (env.c * env.z));
269
270 let p1 = 50000.0 / 13.0 * env.nc * env.ncb * et;
271 let p2 = A / env.nbb;
272
273 let r = 23.0 * (p2 + 0.305) * zdiv(t, 23.0 * p1 + t * (11.0 * cos_h + 108.0 * sin_h));
274 let a = r * cos_h;
275 let b = r * sin_h;
276
277 let rgb_c = unadapt(
278 multiply_matrices(M1, [p2, a, b]).map(|c| c / 1403.0),
279 env.fl,
280 );
281 let xyz = multiply_matrices(
282 CAT16_INV,
283 [
284 rgb_c[0] * env.d_rgb_inv[0],
285 rgb_c[1] * env.d_rgb_inv[1],
286 rgb_c[2] * env.d_rgb_inv[2],
287 ],
288 );
289
290 [xyz[0] / 100.0, xyz[1] / 100.0, xyz[2] / 100.0]
291}
292
293fn to_cam16(xyzd65: [f64; 3], env: &Environment) -> Cam16Object {
294 let xyz100 = xyzd65.map(|c| c * 100.0);
295 let mut tmp: [f64; 3] = [0.0, 0.0, 0.0];
296 let tmpmm = multiply_matrices(CAT16, xyz100);
297 for i in 0..tmpmm.len() {
298 tmp[i] = tmpmm[i] * env.d_rgb[i];
299 }
300 let rgb_a = adapt(tmp, env.fl);
301
302 let a = rgb_a[0] + (-12.0 * rgb_a[1] + rgb_a[2]) / 11.0;
303 let b = (rgb_a[0] + rgb_a[1] - 2.0 * rgb_a[2]) / 9.0;
304 let h_rad = ((b.atan2(a) % TAU) + TAU) % TAU;
305
306 let et = 0.25 * ((h_rad + 2.0).cos() + 3.8);
307
308 let t = 50000.0 / 13.0
309 * env.nc
310 * env.ncb
311 * zdiv(
312 et * (a * a + b * b).sqrt(),
313 rgb_a[0] + rgb_a[1] + 1.05 * rgb_a[2] + 0.305,
314 );
315 let alpha = spow(t, 0.9) * spow(1.64 - spow(0.29, env.n), 0.73);
316
317 let a = env.nbb * (2.0 * rgb_a[0] + rgb_a[1] + 0.05 * rgb_a[2]);
318
319 let j_root = spow(a / env.a_w, 0.5 * env.c * env.z);
320
321 let j = 100.0 * spow(j_root, 2.0);
322
323 let q = 4.0 / env.c * j_root * (env.a_w + 4.0) * env.fl_root;
324
325 let c = alpha * j_root;
326
327 let m = c * env.fl_root;
328
329 let h = constrain(h_rad * RAD2DEG);
330
331 let H = hue_quadrature(h);
332
333 let s = 50.0 * spow(env.c * alpha / (env.a_w + 4.0), 0.5);
334
335 Cam16Object {
336 J: Some(j),
337 C: Some(c),
338 h: Some(h),
339 s: Some(s),
340 Q: Some(q),
341 M: Some(m),
342 H: Some(H),
343 }
344}
345
346#[derive(Debug)]
347struct Cam16Object {
348 J: Option<f64>,
349 C: Option<f64>,
350 h: Option<f64>,
351 s: Option<f64>,
352 Q: Option<f64>,
353 M: Option<f64>,
354 H: Option<f64>,
355}
356
357fn to_lstar(y: f64) -> f64 {
358 let fy = if y > EPSILON_LSTAR {
359 y.cbrt()
360 } else {
361 (KAPPA * y + 16.0) / 116.0
362 };
363 (116.0 * fy) - 16.0
364}
365
366fn from_lstar(lstar: f64) -> f64 {
367 if lstar > 8.0 {
368 ((lstar + 16.0) / 116.0).powi(3)
369 } else {
370 lstar / KAPPA
371 }
372}
373
374fn from_hct(coords: [f64; 3], env: &Environment) -> [f64; 3] {
375 let [h, c, t] = coords;
376 let mut xyz;
377 let mut j;
378
379 if t == 0.0 {
380 return [0.0, 0.0, 0.0];
381 }
382
383 let y = from_lstar(t);
384
385 if t > 0.0 {
386 j = 0.00379058511492914 * t.powi(2) + 0.608983189401032 * t + 0.9155088574762233;
387 } else {
388 j = 9.514_440_756_550_361e-6 * t.powi(2) + 0.08693057439788597 * t - 21.928975842194614;
389 }
390
391 const THRESHOLD: f64 = 2e-12;
392 const MAX_ATTEMPTS: usize = 15;
393
394 let mut attempt = 0;
395 let mut last = f64::INFINITY;
396 let mut best = j;
397
398 while attempt <= MAX_ATTEMPTS {
399 let cam16 = Cam16Object {
400 J: Some(j),
401 C: Some(c),
402 H: None,
403 s: None,
404 Q: None,
405 M: None,
406 h: Some(h),
407 };
408
409 xyz = from_cam16(&cam16, env);
410
411 let delta = (xyz[1] - y).abs();
412 if delta < last {
413 if delta <= THRESHOLD {
414 return xyz;
415 }
416 best = j;
417 last = delta;
418 }
419
420 j -= (xyz[1] - y) * j / (2.0 * xyz[1]);
421
422 attempt += 1;
423 }
424
425 let cam16 = Cam16Object {
426 J: Some(best),
427 C: Some(c),
428 H: None,
429 s: None,
430 Q: None,
431 M: None,
432 h: Some(h),
433 };
434
435 from_cam16(&cam16, env)
436}
437
438fn to_hct(xyz: [f64; 3], env: &Environment) -> [f64; 3] {
439 let t = to_lstar(xyz[1]);
440 if t == 0.0 {
441 return [0.0, 0.0, 0.0];
442 }
443 let cam16 = to_cam16(xyz, env);
444 [constrain(cam16.h.unwrap()), cam16.C.unwrap(), t]
445}
446
447#[inline(always)]
448pub fn cam16_to_xyz(l: ColorData) -> ColorData {
449 let j = l.v[0];
450 let m = l.v[1];
451 let h = l.v[2];
452 let cam16 = Cam16Object {
453 J: Some(j),
454 C: None,
455 H: None,
456 s: None,
457 Q: None,
458 M: Some(m),
459 h: Some(h),
460 };
461 let xyz = from_cam16(&cam16, &ENVS[0]);
462 ColorData {
463 v: [xyz[0], xyz[1], xyz[2], l.v[3]],
464 }
465}
466
467#[inline(always)]
468pub fn xyz_to_cam16(l: ColorData) -> ColorData {
469 let x = l.v[0];
470 let y = l.v[1];
471 let z = l.v[2];
472 let cam16 = to_cam16([x, y, z], &ENVS[0]);
473 ColorData {
474 v: [cam16.J.unwrap(), cam16.M.unwrap(), cam16.h.unwrap(), l.v[3]],
475 }
476}
477
478#[inline(always)]
479pub fn hct_to_xyz(l: ColorData) -> ColorData {
480 let hct = [l.v[0], l.v[1], l.v[2]];
481 let xyz = from_hct(hct, &ENVS[1]);
482 ColorData {
483 v: [xyz[0], xyz[1], xyz[2], l.v[3]],
484 }
485}
486
487#[inline(always)]
488pub fn xyz_to_hct(l: ColorData) -> ColorData {
489 let xyz = [l.v[0], l.v[1], l.v[2]];
490 let hct = to_hct(xyz, &ENVS[1]);
491 ColorData {
492 v: [hct[0], hct[1], hct[2], l.v[3]],
493 }
494}
495
496