1use super::{Cam16, ViewingConditions};
2#[cfg(all(not(feature = "std"), feature = "libm"))]
3#[allow(unused_imports)]
4use crate::utils::no_std::FloatExt;
5use crate::{
6 color::{y_from_lstar, Argb, LinearRgb},
7 utils::math::{matrix_multiply, sanitize_degrees_double, signum},
8};
9use core::f64::consts::PI;
10
11const SCALED_DISCOUNT_FROM_LINRGB: [[f64; 3]; 3] = [
13 [
14 0.001200833568784504,
15 0.002389694492170889,
16 0.0002795742885861124,
17 ],
18 [
19 0.0005891086651375999,
20 0.0029785502573438758,
21 0.0003270666104008398,
22 ],
23 [
24 0.00010146692491640572,
25 0.0005364214359186694,
26 0.0032979401770712076,
27 ],
28];
29
30const LINRGB_FROM_SCALED_DISCOUNT: [[f64; 3]; 3] = [
31 [1373.2198709594231, -1100.4251190754821, -7.278681089101213],
32 [-271.815969077903, 559.6580465940733, -32.46047482791194],
33 [1.9622899599665666, -57.173814538844006, 308.7233197812385],
34];
35
36const Y_FROM_LINRGB: [f64; 3] = [0.2126, 0.7152, 0.0722];
37
38const CRITICAL_PLANES: [f64; 255] = [
39 0.015176349177441876,
40 0.045529047532325624,
41 0.07588174588720938,
42 0.10623444424209313,
43 0.13658714259697685,
44 0.16693984095186062,
45 0.19729253930674434,
46 0.2276452376616281,
47 0.2579979360165119,
48 0.28835063437139563,
49 0.3188300904430532,
50 0.350925934958123,
51 0.3848314933096426,
52 0.42057480301049466,
53 0.458183274052838,
54 0.4976837250274023,
55 0.5391024159806381,
56 0.5824650784040898,
57 0.6277969426914107,
58 0.6751227633498623,
59 0.7244668422128921,
60 0.775853049866786,
61 0.829304845476233,
62 0.8848452951698498,
63 0.942497089126609,
64 1.0022825574869039,
65 1.0642236851973577,
66 1.1283421258858297,
67 1.1946592148522128,
68 1.2631959812511864,
69 1.3339731595349034,
70 1.407011200216447,
71 1.4823302800086415,
72 1.5599503113873272,
73 1.6398909516233677,
74 1.7221716113234105,
75 1.8068114625156377,
76 1.8938294463134073,
77 1.9832442801866852,
78 2.075074464868551,
79 2.1693382909216234,
80 2.2660538449872063,
81 2.36523901573795,
82 2.4669114995532007,
83 2.5710888059345764,
84 2.6777882626779785,
85 2.7870270208169257,
86 2.898822059350997,
87 3.0131901897720907,
88 3.1301480604002863,
89 3.2497121605402226,
90 3.3718988244681087,
91 3.4967242352587946,
92 3.624204428461639,
93 3.754355295633311,
94 3.887192587735158,
95 4.022731918402185,
96 4.160988767090289,
97 4.301978482107941,
98 4.445716283538092,
99 4.592217266055746,
100 4.741496401646282,
101 4.893568542229298,
102 5.048448422192488,
103 5.20615066083972,
104 5.3666897647573375,
105 5.5300801301023865,
106 5.696336044816294,
107 5.865471690767354,
108 6.037501145825082,
109 6.212438385869475,
110 6.390297286737924,
111 6.571091626112461,
112 6.7548350853498045,
113 6.941541251256611,
114 7.131223617812143,
115 7.323895587840543,
116 7.5195704746346665,
117 7.7182615035334345,
118 7.919981813454504,
119 8.124744458384042,
120 8.332562408825165,
121 8.543448553206703,
122 8.757415699253682,
123 8.974476575321063,
124 9.194643831691977,
125 9.417930041841839,
126 9.644347703669503,
127 9.873909240696694,
128 10.106627003236781,
129 10.342513269534024,
130 10.58158024687427,
131 10.8238400726681,
132 11.069304815507364,
133 11.317986476196008,
134 11.569896988756009,
135 11.825048221409341,
136 12.083451977536606,
137 12.345119996613247,
138 12.610063955123938,
139 12.878295467455942,
140 13.149826086772048,
141 13.42466730586372,
142 13.702830557985108,
143 13.984327217668513,
144 14.269168601521828,
145 14.55736596900856,
146 14.848930523210871,
147 15.143873411576273,
148 15.44220572664832,
149 15.743938506781891,
150 16.04908273684337,
151 16.35764934889634,
152 16.66964922287304,
153 16.985093187232053,
154 17.30399201960269,
155 17.62635644741625,
156 17.95219714852476,
157 18.281524751807332,
158 18.614349837764564,
159 18.95068293910138,
160 19.290534541298456,
161 19.633915083172692,
162 19.98083495742689,
163 20.331304511189067,
164 20.685334046541502,
165 21.042933821039977,
166 21.404114048223256,
167 21.76888489811322,
168 22.137256497705877,
169 22.50923893145328,
170 22.884842241736916,
171 23.264076429332462,
172 23.6469514538663,
173 24.033477234264016,
174 24.42366364919083,
175 24.817520537484558,
176 25.21505769858089,
177 25.61628489293138,
178 26.021211842414342,
179 26.429848230738664,
180 26.842203703840827,
181 27.258287870275353,
182 27.678110301598522,
183 28.10168053274597,
184 28.529008062403893,
185 28.96010235337422,
186 29.39497283293396,
187 29.83362889318845,
188 30.276079891419332,
189 30.722335150426627,
190 31.172403958865512,
191 31.62629557157785,
192 32.08401920991837,
193 32.54558406207592,
194 33.010999283389665,
195 33.4802739966603,
196 33.953417292456834,
197 34.430438229418264,
198 34.911345834551085,
199 35.39614910352207,
200 35.88485700094671,
201 36.37747846067349,
202 36.87402238606382,
203 37.37449765026789,
204 37.87891309649659,
205 38.38727753828926,
206 38.89959975977785,
207 39.41588851594697,
208 39.93615253289054,
209 40.460400508064545,
210 40.98864111053629,
211 41.520882981230194,
212 42.05713473317016,
213 42.597404951718396,
214 43.141702194811224,
215 43.6900349931913,
216 44.24241185063697,
217 44.798841244188324,
218 45.35933162437017,
219 45.92389141541209,
220 46.49252901546552,
221 47.065252796817916,
222 47.64207110610409,
223 48.22299226451468,
224 48.808024568002054,
225 49.3971762874833,
226 49.9904556690408,
227 50.587870934119984,
228 51.189430279724725,
229 51.79514187861014,
230 52.40501387947288,
231 53.0190544071392,
232 53.637271562750364,
233 54.259673423945976,
234 54.88626804504493,
235 55.517063457223934,
236 56.15206766869424,
237 56.79128866487574,
238 57.43473440856916,
239 58.08241284012621,
240 58.734331877617365,
241 59.39049941699807,
242 60.05092333227251,
243 60.715611475655585,
244 61.38457167773311,
245 62.057811747619894,
246 62.7353394731159,
247 63.417162620860914,
248 64.10328893648692,
249 64.79372614476921,
250 65.48848194977529,
251 66.18756403501224,
252 66.89098006357258,
253 67.59873767827808,
254 68.31084450182222,
255 69.02730813691093,
256 69.74813616640164,
257 70.47333615344107,
258 71.20291564160104,
259 71.93688215501312,
260 72.67524319850172,
261 73.41800625771542,
262 74.16517879925733,
263 74.9167682708136,
264 75.67278210128072,
265 76.43322770089146,
266 77.1981124613393,
267 77.96744375590167,
268 78.74122893956174,
269 79.51947534912904,
270 80.30219030335869,
271 81.08938110306934,
272 81.88105503125999,
273 82.67721935322541,
274 83.4778813166706,
275 84.28304815182372,
276 85.09272707154808,
277 85.90692527145302,
278 86.72564993000343,
279 87.54890820862819,
280 88.3767072518277,
281 89.2090541872801,
282 90.04595612594655,
283 90.88742016217518,
284 91.73345337380438,
285 92.58406282226491,
286 93.43925555268066,
287 94.29903859396902,
288 95.16341895893969,
289 96.03240364439274,
290 96.9059996312159,
291 97.78421388448044,
292 98.6670533535366,
293 99.55452497210776,
294];
295
296pub struct HctSolver;
297
298impl HctSolver {
299 fn sanitize_radians(angle: f64) -> f64 {
305 PI.mul_add(8.0, angle) % (PI * 2.0)
306 }
307
308 fn true_delinearized(rgb_component: f64) -> f64 {
315 let normalized = rgb_component / 100.0;
316 let delinearized = if normalized <= 0.0031308 {
317 normalized * 12.92
318 } else {
319 1.055f64.mul_add(normalized.powf(1.0 / 2.4), -0.055)
320 };
321
322 delinearized * 255.0
323 }
324
325 fn chromatic_adaptation(component: f64) -> f64 {
326 let af = component.abs().powf(0.42);
327 signum(component) * 400.0 * af / (af + 27.13)
328 }
329
330 fn hue_of(linrgb: [f64; 3]) -> f64 {
333 let scaled_discount = matrix_multiply(linrgb, SCALED_DISCOUNT_FROM_LINRGB);
334
335 let r_a = Self::chromatic_adaptation(scaled_discount[0]);
336 let g_a = Self::chromatic_adaptation(scaled_discount[1]);
337 let b_a = Self::chromatic_adaptation(scaled_discount[2]);
338
339 let a = (11.0f64.mul_add(r_a, -12.0 * g_a) + b_a) / 11.0;
341
342 let b = 2.0f64.mul_add(-b_a, r_a + g_a) / 9.0;
344
345 b.atan2(a)
346 }
347
348 fn are_in_cyclic_order(a: f64, b: f64, c: f64) -> bool {
349 let delta_ab = Self::sanitize_radians(b - a);
350 let delta_ac = Self::sanitize_radians(c - a);
351
352 delta_ab < delta_ac
353 }
354
355 fn intercept(source: f64, mid: f64, target: f64) -> f64 {
360 (mid - source) / (target - source)
361 }
362
363 fn lerp_point(source: [f64; 3], t: f64, target: [f64; 3]) -> [f64; 3] {
364 [
365 (target[0] - source[0]).mul_add(t, source[0]),
366 (target[1] - source[1]).mul_add(t, source[1]),
367 (target[2] - source[2]).mul_add(t, source[2]),
368 ]
369 }
370
371 fn set_coordinate(
380 source: [f64; 3],
381 coordinate: f64,
382 target: [f64; 3],
383 axis: usize,
384 ) -> [f64; 3] {
385 let t = Self::intercept(source[axis], coordinate, target[axis]);
386
387 Self::lerp_point(source, t, target)
388 }
389
390 fn is_bounded(x: f64) -> bool {
391 (0.0..=100.0).contains(&x)
392 }
393
394 fn nth_vertex(y: f64, n: i32) -> [f64; 3] {
403 let k_r = Y_FROM_LINRGB[0];
404 let k_g = Y_FROM_LINRGB[1];
405 let k_b = Y_FROM_LINRGB[2];
406
407 let coord_a = if n % 4 <= 1 { 0.0 } else { 100.0 };
408 let coord_b = if n % 2 == 0 { 0.0 } else { 100.0 };
409
410 if n < 4 {
411 let g: f64 = coord_a;
412 let b: f64 = coord_b;
413 let r = b.mul_add(-k_b, g.mul_add(-k_g, y)) / k_r;
414
415 if Self::is_bounded(r) {
416 [r, g, b]
417 } else {
418 [-1.0; 3]
419 }
420 } else if n < 8 {
421 let b = coord_a;
422 let r = coord_b;
423 let g = b.mul_add(-k_b, r.mul_add(-k_r, y)) / k_g;
424
425 if Self::is_bounded(g) {
426 [r, g, b]
427 } else {
428 [-1.0; 3]
429 }
430 } else {
431 let r = coord_a;
432 let g = coord_b;
433 let b = g.mul_add(-k_g, r.mul_add(-k_r, y)) / k_b;
434
435 if Self::is_bounded(b) {
436 [r, g, b]
437 } else {
438 [-1.0; 3]
439 }
440 }
441 }
442
443 fn bisect_to_segment(y: f64, target_hue: f64) -> [[f64; 3]; 2] {
449 let mut left = [-1.0; 3];
450 let mut right = left;
451 let mut left_hue = 0.0;
452 let mut right_hue = 0.0;
453 let mut initialized = false;
454 let mut uncut = true;
455
456 for n in 0..12 {
457 let mid = Self::nth_vertex(y, n);
458
459 if mid[0] < 0.0 {
460 continue;
461 }
462
463 let mid_hue = Self::hue_of(mid);
464
465 if !initialized {
466 left = mid;
467 right = mid;
468 left_hue = mid_hue;
469 right_hue = mid_hue;
470 initialized = true;
471 continue;
472 }
473
474 if uncut || Self::are_in_cyclic_order(left_hue, mid_hue, right_hue) {
475 uncut = false;
476
477 if Self::are_in_cyclic_order(left_hue, target_hue, mid_hue) {
478 right = mid;
479 right_hue = mid_hue;
480 } else {
481 left = mid;
482 left_hue = mid_hue;
483 }
484 }
485 }
486
487 [left, right]
488 }
489
490 fn mid_point(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
491 [
492 (a[0] + b[0]) / 2.0,
493 (a[1] + b[1]) / 2.0,
494 (a[2] + b[2]) / 2.0,
495 ]
496 }
497
498 fn critical_plane_below(x: f64) -> i16 {
499 (x - 0.5).floor() as i16
500 }
501
502 fn critical_plane_above(x: f64) -> i16 {
503 (x - 0.5).ceil() as i16
504 }
505
506 fn bisect_to_limit(y: f64, target_hue: f64) -> [f64; 3] {
512 let segment = Self::bisect_to_segment(y, target_hue);
513 let mut left = segment[0];
514 let mut left_hue = Self::hue_of(left);
515 let mut right = segment[1];
516
517 for axis in 0..3 {
518 if (left[axis] - right[axis]).abs() > f64::EPSILON {
519 let [mut l_plane, mut r_plane] = if left[axis] < right[axis] {
520 [
521 Self::critical_plane_below(Self::true_delinearized(left[axis])),
522 Self::critical_plane_above(Self::true_delinearized(right[axis])),
523 ]
524 } else {
525 [
526 Self::critical_plane_above(Self::true_delinearized(left[axis])),
527 Self::critical_plane_below(Self::true_delinearized(right[axis])),
528 ]
529 };
530
531 for _ in 0..8 {
532 if (r_plane - l_plane).abs() <= 1 {
533 break;
534 }
535
536 let m_plane = ((f64::from(l_plane) + f64::from(r_plane)) / 2.0).floor() as i16;
537 let mid_plane_coordinate = CRITICAL_PLANES[m_plane as usize];
538 let mid = Self::set_coordinate(left, mid_plane_coordinate, right, axis);
539 let mid_hue = Self::hue_of(mid);
540
541 if Self::are_in_cyclic_order(left_hue, target_hue, mid_hue) {
542 right = mid;
543 r_plane = m_plane;
544 } else {
545 left = mid;
546 left_hue = mid_hue;
547 l_plane = m_plane;
548 }
549 }
550 }
551 }
552
553 Self::mid_point(left, right)
554 }
555
556 fn inverse_chromatic_adaptation(adapted: f64) -> f64 {
557 let adapted_abs = adapted.abs();
558 let base = (27.13 * adapted_abs / (400.0 - adapted_abs)).max(0.0);
559
560 signum(adapted) * base.powf(1.0 / 0.42)
561 }
562
563 fn find_result_by_j(hue_radians: f64, chroma: f64, y: f64) -> Argb {
568 let mut j = y.sqrt() * 11.0;
570 let viewing_conditions = ViewingConditions::standard();
574 let t_inner_coeff =
575 1.0 / (1.64 - 0.29f64.powf(viewing_conditions.background_ytowhite_point_y)).powf(0.73);
576 let e_hue = 0.25 * ((hue_radians + 2.0).cos() + 3.8);
577 let p1 = e_hue * (50000.0 / 13.0) * viewing_conditions.n_c * viewing_conditions.ncb;
578 let (h_sin, h_cos) = (hue_radians.sin(), hue_radians.cos());
579
580 for iteration_round in 0..5 {
581 let j_normalized = j / 100.0;
585 let alpha = if chroma == 0.0 || j == 0.0 {
586 0.0
587 } else {
588 chroma / j_normalized.sqrt()
589 };
590
591 let t = (alpha * t_inner_coeff).powf(1.0 / 0.9);
592 let ac = viewing_conditions.aw
593 * j_normalized.powf(1.0 / viewing_conditions.c / viewing_conditions.z);
594 let p2 = ac / viewing_conditions.nbb;
595 let gamma = 23.0 * (p2 + 0.305) * t
596 / (108.0 * t).mul_add(h_sin, 23.0f64.mul_add(p1, 11.0 * t * h_cos));
597 let a = gamma * h_cos;
598 let b = gamma * h_sin;
599 let (r_a, g_a, b_a) = (
600 288.0f64.mul_add(b, 460.0f64.mul_add(p2, 451.0 * a)) / 1403.0,
601 261.0f64.mul_add(-b, 460.0f64.mul_add(p2, -891.0 * a)) / 1403.0,
602 6300.0f64.mul_add(-b, 460.0f64.mul_add(p2, -220.0 * a)) / 1403.0,
603 );
604
605 let r_cscaled = Self::inverse_chromatic_adaptation(r_a);
606 let g_cscaled = Self::inverse_chromatic_adaptation(g_a);
607 let b_cscaled = Self::inverse_chromatic_adaptation(b_a);
608 let [red, green, blue] = matrix_multiply(
609 [r_cscaled, g_cscaled, b_cscaled],
610 LINRGB_FROM_SCALED_DISCOUNT,
611 );
612
613 let linrgb = LinearRgb { red, green, blue };
614 if linrgb.red < 0.0 || linrgb.green < 0.0 || linrgb.blue < 0.0 {
618 return Argb::default();
619 }
620
621 let [k_r, k_g, k_b] = Y_FROM_LINRGB;
622 let fnj = k_b.mul_add(linrgb.blue, k_r.mul_add(linrgb.red, k_g * linrgb.green));
623 if fnj <= 0.0 {
624 return Argb::default();
625 }
626
627 if iteration_round == 4 || (fnj - y).abs() < 0.002 {
628 if linrgb.red > 100.01 || linrgb.green > 100.01 || linrgb.blue > 100.01 {
629 return Argb::default();
630 }
631
632 return linrgb.into();
633 }
634
635 j = j - (fnj - y) * j / (2.0 * fnj);
638 }
639
640 Argb::default()
641 }
642
643 pub fn solve_to_argb(hue_degrees: f64, chroma: f64, lstar: f64) -> Argb {
652 if chroma < 0.0001 || !(0.0001..=99.9999).contains(&lstar) {
653 return Argb::from_lstar(lstar);
654 }
655
656 let hue_degrees = sanitize_degrees_double(hue_degrees);
657 let hue_radians = hue_degrees.to_radians();
658
659 let y = y_from_lstar(lstar);
660
661 let exact_answer = Self::find_result_by_j(hue_radians, chroma, y);
662
663 if exact_answer != Argb::default() {
664 return exact_answer;
665 }
666
667 let [red, green, blue] = Self::bisect_to_limit(y, hue_radians);
668
669 let linrgb = LinearRgb { red, green, blue };
670
671 linrgb.into()
672 }
673
674 pub fn solve_to_cam(hue_degrees: f64, chroma: f64, lstar: f64) -> Cam16 {
683 Cam16::from(Self::solve_to_argb(hue_degrees, chroma, lstar))
684 }
685}