1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.internal.graphics.cam;
18 
19 /**
20  * An efficient algorithm for determining the closest sRGB color to a set of HCT coordinates,
21  * based on geometrical insights for finding intersections in linear RGB, CAM16, and L*a*b*.
22  *
23  * Algorithm identified and implemented by Tianguang Zhang.
24  * Copied from //java/com/google/ux/material/libmonet/hct on May 22 2022.
25  * ColorUtils/MathUtils functions that were required were added to CamUtils.
26  */
27 public class HctSolver {
HctSolver()28     private HctSolver() {}
29 
30     // Matrix used when converting from linear RGB to CAM16.
31     static final double[][] SCALED_DISCOUNT_FROM_LINRGB =
32             new double[][] {
33                     new double[] {
34                             0.001200833568784504, 0.002389694492170889, 0.0002795742885861124,
35                     },
36                     new double[] {
37                             0.0005891086651375999, 0.0029785502573438758, 0.0003270666104008398,
38                     },
39                     new double[] {
40                             0.00010146692491640572, 0.0005364214359186694, 0.0032979401770712076,
41                     },
42             };
43 
44     // Matrix used when converting from CAM16 to linear RGB.
45     static final double[][] LINRGB_FROM_SCALED_DISCOUNT =
46             new double[][] {
47                     new double[] {
48                             1373.2198709594231, -1100.4251190754821, -7.278681089101213,
49                     },
50                     new double[] {
51                             -271.815969077903, 559.6580465940733, -32.46047482791194,
52                     },
53                     new double[] {
54                             1.9622899599665666, -57.173814538844006, 308.7233197812385,
55                     },
56             };
57 
58     // Weights for transforming a set of linear RGB coordinates to Y in XYZ.
59     static final double[] Y_FROM_LINRGB = new double[] {0.2126, 0.7152, 0.0722};
60 
61     // Lookup table for plane in XYZ's Y axis (relative luminance) that corresponds to a given
62     // L* in L*a*b*. HCT's T is L*, and XYZ's Y is directly correlated to linear RGB, this table
63     // allows us to thus find the intersection between HCT and RGB, giving a solution to the
64     // RGB coordinates that correspond to a given set of HCT coordinates.
65     static final double[] CRITICAL_PLANES =
66             new double[] {
67                     0.015176349177441876,
68                     0.045529047532325624,
69                     0.07588174588720938,
70                     0.10623444424209313,
71                     0.13658714259697685,
72                     0.16693984095186062,
73                     0.19729253930674434,
74                     0.2276452376616281,
75                     0.2579979360165119,
76                     0.28835063437139563,
77                     0.3188300904430532,
78                     0.350925934958123,
79                     0.3848314933096426,
80                     0.42057480301049466,
81                     0.458183274052838,
82                     0.4976837250274023,
83                     0.5391024159806381,
84                     0.5824650784040898,
85                     0.6277969426914107,
86                     0.6751227633498623,
87                     0.7244668422128921,
88                     0.775853049866786,
89                     0.829304845476233,
90                     0.8848452951698498,
91                     0.942497089126609,
92                     1.0022825574869039,
93                     1.0642236851973577,
94                     1.1283421258858297,
95                     1.1946592148522128,
96                     1.2631959812511864,
97                     1.3339731595349034,
98                     1.407011200216447,
99                     1.4823302800086415,
100                     1.5599503113873272,
101                     1.6398909516233677,
102                     1.7221716113234105,
103                     1.8068114625156377,
104                     1.8938294463134073,
105                     1.9832442801866852,
106                     2.075074464868551,
107                     2.1693382909216234,
108                     2.2660538449872063,
109                     2.36523901573795,
110                     2.4669114995532007,
111                     2.5710888059345764,
112                     2.6777882626779785,
113                     2.7870270208169257,
114                     2.898822059350997,
115                     3.0131901897720907,
116                     3.1301480604002863,
117                     3.2497121605402226,
118                     3.3718988244681087,
119                     3.4967242352587946,
120                     3.624204428461639,
121                     3.754355295633311,
122                     3.887192587735158,
123                     4.022731918402185,
124                     4.160988767090289,
125                     4.301978482107941,
126                     4.445716283538092,
127                     4.592217266055746,
128                     4.741496401646282,
129                     4.893568542229298,
130                     5.048448422192488,
131                     5.20615066083972,
132                     5.3666897647573375,
133                     5.5300801301023865,
134                     5.696336044816294,
135                     5.865471690767354,
136                     6.037501145825082,
137                     6.212438385869475,
138                     6.390297286737924,
139                     6.571091626112461,
140                     6.7548350853498045,
141                     6.941541251256611,
142                     7.131223617812143,
143                     7.323895587840543,
144                     7.5195704746346665,
145                     7.7182615035334345,
146                     7.919981813454504,
147                     8.124744458384042,
148                     8.332562408825165,
149                     8.543448553206703,
150                     8.757415699253682,
151                     8.974476575321063,
152                     9.194643831691977,
153                     9.417930041841839,
154                     9.644347703669503,
155                     9.873909240696694,
156                     10.106627003236781,
157                     10.342513269534024,
158                     10.58158024687427,
159                     10.8238400726681,
160                     11.069304815507364,
161                     11.317986476196008,
162                     11.569896988756009,
163                     11.825048221409341,
164                     12.083451977536606,
165                     12.345119996613247,
166                     12.610063955123938,
167                     12.878295467455942,
168                     13.149826086772048,
169                     13.42466730586372,
170                     13.702830557985108,
171                     13.984327217668513,
172                     14.269168601521828,
173                     14.55736596900856,
174                     14.848930523210871,
175                     15.143873411576273,
176                     15.44220572664832,
177                     15.743938506781891,
178                     16.04908273684337,
179                     16.35764934889634,
180                     16.66964922287304,
181                     16.985093187232053,
182                     17.30399201960269,
183                     17.62635644741625,
184                     17.95219714852476,
185                     18.281524751807332,
186                     18.614349837764564,
187                     18.95068293910138,
188                     19.290534541298456,
189                     19.633915083172692,
190                     19.98083495742689,
191                     20.331304511189067,
192                     20.685334046541502,
193                     21.042933821039977,
194                     21.404114048223256,
195                     21.76888489811322,
196                     22.137256497705877,
197                     22.50923893145328,
198                     22.884842241736916,
199                     23.264076429332462,
200                     23.6469514538663,
201                     24.033477234264016,
202                     24.42366364919083,
203                     24.817520537484558,
204                     25.21505769858089,
205                     25.61628489293138,
206                     26.021211842414342,
207                     26.429848230738664,
208                     26.842203703840827,
209                     27.258287870275353,
210                     27.678110301598522,
211                     28.10168053274597,
212                     28.529008062403893,
213                     28.96010235337422,
214                     29.39497283293396,
215                     29.83362889318845,
216                     30.276079891419332,
217                     30.722335150426627,
218                     31.172403958865512,
219                     31.62629557157785,
220                     32.08401920991837,
221                     32.54558406207592,
222                     33.010999283389665,
223                     33.4802739966603,
224                     33.953417292456834,
225                     34.430438229418264,
226                     34.911345834551085,
227                     35.39614910352207,
228                     35.88485700094671,
229                     36.37747846067349,
230                     36.87402238606382,
231                     37.37449765026789,
232                     37.87891309649659,
233                     38.38727753828926,
234                     38.89959975977785,
235                     39.41588851594697,
236                     39.93615253289054,
237                     40.460400508064545,
238                     40.98864111053629,
239                     41.520882981230194,
240                     42.05713473317016,
241                     42.597404951718396,
242                     43.141702194811224,
243                     43.6900349931913,
244                     44.24241185063697,
245                     44.798841244188324,
246                     45.35933162437017,
247                     45.92389141541209,
248                     46.49252901546552,
249                     47.065252796817916,
250                     47.64207110610409,
251                     48.22299226451468,
252                     48.808024568002054,
253                     49.3971762874833,
254                     49.9904556690408,
255                     50.587870934119984,
256                     51.189430279724725,
257                     51.79514187861014,
258                     52.40501387947288,
259                     53.0190544071392,
260                     53.637271562750364,
261                     54.259673423945976,
262                     54.88626804504493,
263                     55.517063457223934,
264                     56.15206766869424,
265                     56.79128866487574,
266                     57.43473440856916,
267                     58.08241284012621,
268                     58.734331877617365,
269                     59.39049941699807,
270                     60.05092333227251,
271                     60.715611475655585,
272                     61.38457167773311,
273                     62.057811747619894,
274                     62.7353394731159,
275                     63.417162620860914,
276                     64.10328893648692,
277                     64.79372614476921,
278                     65.48848194977529,
279                     66.18756403501224,
280                     66.89098006357258,
281                     67.59873767827808,
282                     68.31084450182222,
283                     69.02730813691093,
284                     69.74813616640164,
285                     70.47333615344107,
286                     71.20291564160104,
287                     71.93688215501312,
288                     72.67524319850172,
289                     73.41800625771542,
290                     74.16517879925733,
291                     74.9167682708136,
292                     75.67278210128072,
293                     76.43322770089146,
294                     77.1981124613393,
295                     77.96744375590167,
296                     78.74122893956174,
297                     79.51947534912904,
298                     80.30219030335869,
299                     81.08938110306934,
300                     81.88105503125999,
301                     82.67721935322541,
302                     83.4778813166706,
303                     84.28304815182372,
304                     85.09272707154808,
305                     85.90692527145302,
306                     86.72564993000343,
307                     87.54890820862819,
308                     88.3767072518277,
309                     89.2090541872801,
310                     90.04595612594655,
311                     90.88742016217518,
312                     91.73345337380438,
313                     92.58406282226491,
314                     93.43925555268066,
315                     94.29903859396902,
316                     95.16341895893969,
317                     96.03240364439274,
318                     96.9059996312159,
319                     97.78421388448044,
320                     98.6670533535366,
321                     99.55452497210776,
322             };
323 
324     /**
325      * Sanitizes a small enough angle in radians.
326      *
327      * @param angle An angle in radians; must not deviate too much from 0.
328      * @return A coterminal angle between 0 and 2pi.
329      */
sanitizeRadians(double angle)330     static double sanitizeRadians(double angle) {
331         return (angle + Math.PI * 8) % (Math.PI * 2);
332     }
333 
334     /**
335      * Delinearizes an RGB component, returning a floating-point number.
336      *
337      * @param rgbComponent 0.0 <= rgb_component <= 100.0, represents linear R/G/B channel
338      * @return 0.0 <= output <= 255.0, color channel converted to regular RGB space
339      */
trueDelinearized(double rgbComponent)340     static double trueDelinearized(double rgbComponent) {
341         double normalized = rgbComponent / 100.0;
342         double delinearized;
343         if (normalized <= 0.0031308) {
344             delinearized = normalized * 12.92;
345         } else {
346             delinearized = 1.055 * Math.pow(normalized, 1.0 / 2.4) - 0.055;
347         }
348         return delinearized * 255.0;
349     }
350 
chromaticAdaptation(double component)351     static double chromaticAdaptation(double component) {
352         double af = Math.pow(Math.abs(component), 0.42);
353         return CamUtils.signum(component) * 400.0 * af / (af + 27.13);
354     }
355 
356     /**
357      * Returns the hue of a linear RGB color in CAM16.
358      *
359      * @param linrgb The linear RGB coordinates of a color.
360      * @return The hue of the color in CAM16, in radians.
361      */
hueOf(double[] linrgb)362     static double hueOf(double[] linrgb) {
363         // Calculate scaled discount components using in-lined matrix multiplication to avoid
364         // an array allocation.
365         double[][] matrix = SCALED_DISCOUNT_FROM_LINRGB;
366         double[] row = linrgb;
367         double rD = linrgb[0] * matrix[0][0] + row[1] * matrix[0][1] + row[2] * matrix[0][2];
368         double gD = linrgb[0] * matrix[1][0] + row[1] * matrix[1][1] + row[2] * matrix[1][2];
369         double bD = linrgb[0] * matrix[2][0] + row[1] * matrix[2][1] + row[2] * matrix[2][2];
370 
371         double rA = chromaticAdaptation(rD);
372         double gA = chromaticAdaptation(gD);
373         double bA = chromaticAdaptation(bD);
374         // redness-greenness
375         double a = (11.0 * rA + -12.0 * gA + bA) / 11.0;
376         // yellowness-blueness
377         double b = (rA + gA - 2.0 * bA) / 9.0;
378         return Math.atan2(b, a);
379     }
380 
381     /**
382      * Cyclic order is the idea that 330° → 5° → 200° is in order, but, 180° → 270° → 210° is not.
383      * Visually, A B and C are angles, and they are in cyclic order if travelling from A to C
384      * in a way that increases angle (ex. counter-clockwise if +x axis = 0 degrees and +y = 90)
385      * means you must cross B.
386      * @param a first angle in possibly cyclic triplet
387      * @param b second angle in possibly cyclic triplet
388      * @param c third angle in possibly cyclic triplet
389      * @return true if B is between A and C
390      */
areInCyclicOrder(double a, double b, double c)391     static boolean areInCyclicOrder(double a, double b, double c) {
392         double deltaAB = sanitizeRadians(b - a);
393         double deltaAC = sanitizeRadians(c - a);
394         return deltaAB < deltaAC;
395     }
396 
397     /**
398      * Find an intercept using linear interpolation.
399      *
400      * @param source The starting number.
401      * @param mid The number in the middle.
402      * @param target The ending number.
403      * @return A number t such that lerp(source, target, t) = mid.
404      */
intercept(double source, double mid, double target)405     static double intercept(double source, double mid, double target) {
406         if (target == source) {
407             return target;
408         }
409         return (mid - source) / (target - source);
410     }
411 
412     /**
413      * Linearly interpolate between two points in three dimensions.
414      *
415      * @param source three dimensions representing the starting point
416      * @param t the percentage to travel between source and target, from 0 to 1
417      * @param target three dimensions representing the end point
418      * @return three dimensions representing the point t percent from source to target.
419      */
lerpPoint(double[] source, double t, double[] target)420     static double[] lerpPoint(double[] source, double t, double[] target) {
421         return new double[] {
422                 source[0] + (target[0] - source[0]) * t,
423                 source[1] + (target[1] - source[1]) * t,
424                 source[2] + (target[2] - source[2]) * t,
425         };
426     }
427 
428     /**
429      * Intersects a segment with a plane.
430      *
431      * @param source The coordinates of point A.
432      * @param coordinate The R-, G-, or B-coordinate of the plane.
433      * @param target The coordinates of point B.
434      * @param axis The axis the plane is perpendicular with. (0: R, 1: G, 2: B)
435      * @return The intersection point of the segment AB with the plane R=coordinate, G=coordinate,
436      *     or B=coordinate
437      */
setCoordinate(double[] source, double coordinate, double[] target, int axis)438     static double[] setCoordinate(double[] source, double coordinate, double[] target, int axis) {
439         double t = intercept(source[axis], coordinate, target[axis]);
440         return lerpPoint(source, t, target);
441     }
442 
443     /** Ensure X is between 0 and 100. */
isBounded(double x)444     static boolean isBounded(double x) {
445         return 0.0 <= x && x <= 100.0;
446     }
447 
448     /**
449      * Returns the nth possible vertex of the polygonal intersection.
450      *
451      * @param y The Y value of the plane.
452      * @param n The zero-based index of the point. 0 <= n <= 11.
453      * @return The nth possible vertex of the polygonal intersection of the y plane and the RGB cube
454      *     in linear RGB coordinates, if it exists. If the possible vertex lies outside of the cube,
455      *     [-1.0, -1.0, -1.0] is returned.
456      */
nthVertex(double y, int n)457     static double[] nthVertex(double y, int n) {
458         double kR = Y_FROM_LINRGB[0];
459         double kG = Y_FROM_LINRGB[1];
460         double kB = Y_FROM_LINRGB[2];
461         double coordA = n % 4 <= 1 ? 0.0 : 100.0;
462         double coordB = n % 2 == 0 ? 0.0 : 100.0;
463         if (n < 4) {
464             double g = coordA;
465             double b = coordB;
466             double r = (y - g * kG - b * kB) / kR;
467             if (isBounded(r)) {
468                 return new double[] {r, g, b};
469             } else {
470                 return new double[] {-1.0, -1.0, -1.0};
471             }
472         } else if (n < 8) {
473             double b = coordA;
474             double r = coordB;
475             double g = (y - r * kR - b * kB) / kG;
476             if (isBounded(g)) {
477                 return new double[] {r, g, b};
478             } else {
479                 return new double[] {-1.0, -1.0, -1.0};
480             }
481         } else {
482             double r = coordA;
483             double g = coordB;
484             double b = (y - r * kR - g * kG) / kB;
485             if (isBounded(b)) {
486                 return new double[] {r, g, b};
487             } else {
488                 return new double[] {-1.0, -1.0, -1.0};
489             }
490         }
491     }
492 
493     /**
494      * Finds the segment containing the desired color.
495      *
496      * @param y The Y value of the color.
497      * @param targetHue The hue of the color.
498      * @return A list of two sets of linear RGB coordinates, each corresponding to an endpoint of
499      *     the segment containing the desired color.
500      */
bisectToSegment(double y, double targetHue)501     static double[][] bisectToSegment(double y, double targetHue) {
502         double[] left = new double[] {-1.0, -1.0, -1.0};
503         double[] right = left;
504         double leftHue = 0.0;
505         double rightHue = 0.0;
506         boolean initialized = false;
507         boolean uncut = true;
508         for (int n = 0; n < 12; n++) {
509             double[] mid = nthVertex(y, n);
510             if (mid[0] < 0) {
511                 continue;
512             }
513             double midHue = hueOf(mid);
514             if (!initialized) {
515                 left = mid;
516                 right = mid;
517                 leftHue = midHue;
518                 rightHue = midHue;
519                 initialized = true;
520                 continue;
521             }
522             if (uncut || areInCyclicOrder(leftHue, midHue, rightHue)) {
523                 uncut = false;
524                 if (areInCyclicOrder(leftHue, targetHue, midHue)) {
525                     right = mid;
526                     rightHue = midHue;
527                 } else {
528                     left = mid;
529                     leftHue = midHue;
530                 }
531             }
532         }
533         return new double[][] {left, right};
534     }
535 
criticalPlaneBelow(double x)536     static int criticalPlaneBelow(double x) {
537         return (int) Math.floor(x - 0.5);
538     }
539 
criticalPlaneAbove(double x)540     static int criticalPlaneAbove(double x) {
541         return (int) Math.ceil(x - 0.5);
542     }
543 
544     /**
545      * Finds a color with the given Y and hue on the boundary of the cube.
546      *
547      * @param y The Y value of the color.
548      * @param targetHue The hue of the color.
549      * @return The desired color, in linear RGB coordinates.
550      */
bisectToLimit(double y, double targetHue)551     static int bisectToLimit(double y, double targetHue) {
552         double[][] segment = bisectToSegment(y, targetHue);
553         double[] left = segment[0];
554         double leftHue = hueOf(left);
555         double[] right = segment[1];
556         for (int axis = 0; axis < 3; axis++) {
557             if (left[axis] != right[axis]) {
558                 int lPlane = -1;
559                 int rPlane = 255;
560                 if (left[axis] < right[axis]) {
561                     lPlane = criticalPlaneBelow(trueDelinearized(left[axis]));
562                     rPlane = criticalPlaneAbove(trueDelinearized(right[axis]));
563                 } else {
564                     lPlane = criticalPlaneAbove(trueDelinearized(left[axis]));
565                     rPlane = criticalPlaneBelow(trueDelinearized(right[axis]));
566                 }
567                 for (int i = 0; i < 8; i++) {
568                     if (Math.abs(rPlane - lPlane) <= 1) {
569                         break;
570                     } else {
571                         int mPlane = (int) Math.floor((lPlane + rPlane) / 2.0);
572                         double midPlaneCoordinate = CRITICAL_PLANES[mPlane];
573                         double[] mid = setCoordinate(left, midPlaneCoordinate, right, axis);
574                         double midHue = hueOf(mid);
575                         if (areInCyclicOrder(leftHue, targetHue, midHue)) {
576                             right = mid;
577                             rPlane = mPlane;
578                         } else {
579                             left = mid;
580                             leftHue = midHue;
581                             lPlane = mPlane;
582                         }
583                     }
584                 }
585             }
586         }
587         return CamUtils.argbFromLinrgbComponents((left[0] + right[0]) / 2,
588                 (left[1] + right[1]) / 2, (left[2] + right[2]) / 2);
589     }
590 
591     /** Equation used in CAM16 conversion that removes the effect of chromatic adaptation. */
inverseChromaticAdaptation(double adapted)592     static double inverseChromaticAdaptation(double adapted) {
593         double adaptedAbs = Math.abs(adapted);
594         double base = Math.max(0, 27.13 * adaptedAbs / (400.0 - adaptedAbs));
595         return CamUtils.signum(adapted) * Math.pow(base, 1.0 / 0.42);
596     }
597 
598     /**
599      * Finds a color with the given hue, chroma, and Y.
600      *
601      * @param hueRadians The desired hue in radians.
602      * @param chroma The desired chroma.
603      * @param y The desired Y.
604      * @return The desired color as a hexadecimal integer, if found; 0 otherwise.
605      */
findResultByJ(double hueRadians, double chroma, double y)606     static int findResultByJ(double hueRadians, double chroma, double y) {
607         // Initial estimate of j.
608         double j = Math.sqrt(y) * 11.0;
609         // ===========================================================
610         // Operations inlined from Cam16 to avoid repeated calculation
611         // ===========================================================
612         Frame viewingConditions = Frame.DEFAULT;
613         double tInnerCoeff = 1 / Math.pow(1.64 - Math.pow(0.29, viewingConditions.getN()), 0.73);
614         double eHue = 0.25 * (Math.cos(hueRadians + 2.0) + 3.8);
615         double p1 = eHue * (50000.0 / 13.0) * viewingConditions.getNc()
616                 * viewingConditions.getNcb();
617         double hSin = Math.sin(hueRadians);
618         double hCos = Math.cos(hueRadians);
619         for (int iterationRound = 0; iterationRound < 5; iterationRound++) {
620             // ===========================================================
621             // Operations inlined from Cam16 to avoid repeated calculation
622             // ===========================================================
623             double jNormalized = j / 100.0;
624             double alpha = chroma == 0.0 || j == 0.0 ? 0.0 : chroma / Math.sqrt(jNormalized);
625             double t = Math.pow(alpha * tInnerCoeff, 1.0 / 0.9);
626             double acExponent = 1.0 / viewingConditions.getC() / viewingConditions.getZ();
627             double ac = viewingConditions.getAw() * Math.pow(jNormalized, acExponent);
628             double p2 = ac / viewingConditions.getNbb();
629             double gamma = 23.0 * (p2 + 0.305) * t / (23.0 * p1 + 11 * t * hCos + 108.0 * t * hSin);
630             double a = gamma * hCos;
631             double b = gamma * hSin;
632             double rA = (460.0 * p2 + 451.0 * a + 288.0 * b) / 1403.0;
633             double gA = (460.0 * p2 - 891.0 * a - 261.0 * b) / 1403.0;
634             double bA = (460.0 * p2 - 220.0 * a - 6300.0 * b) / 1403.0;
635             double rCScaled = inverseChromaticAdaptation(rA);
636             double gCScaled = inverseChromaticAdaptation(gA);
637             double bCScaled = inverseChromaticAdaptation(bA);
638             double[][] matrix = LINRGB_FROM_SCALED_DISCOUNT;
639             double linrgbR = rCScaled * matrix[0][0] + gCScaled * matrix[0][1]
640                     + bCScaled * matrix[0][2];
641             double linrgbG = rCScaled * matrix[1][0] + gCScaled * matrix[1][1]
642                     + bCScaled * matrix[1][2];
643             double linrgbB = rCScaled * matrix[2][0] + gCScaled * matrix[2][1]
644                     + bCScaled * matrix[2][2];
645             // ===========================================================
646             // Operations inlined from Cam16 to avoid repeated calculation
647             // ===========================================================
648             if (linrgbR < 0 || linrgbG < 0 || linrgbB < 0) {
649                 return 0;
650             }
651             double kR = Y_FROM_LINRGB[0];
652             double kG = Y_FROM_LINRGB[1];
653             double kB = Y_FROM_LINRGB[2];
654             double fnj = kR * linrgbR + kG * linrgbG + kB * linrgbB;
655             if (fnj <= 0) {
656                 return 0;
657             }
658             if (iterationRound == 4 || Math.abs(fnj - y) < 0.002) {
659                 if (linrgbR > 100.01 || linrgbG > 100.01 || linrgbB > 100.01) {
660                     return 0;
661                 }
662                 return CamUtils.argbFromLinrgbComponents(linrgbR, linrgbG, linrgbB);
663             }
664             // Iterates with Newton method,
665             // Using 2 * fn(j) / j as the approximation of fn'(j)
666             j = j - (fnj - y) * j / (2 * fnj);
667         }
668         return 0;
669     }
670 
671     /**
672      * Finds an sRGB color with the given hue, chroma, and L*, if possible.
673      *
674      * @param hueDegrees The desired hue, in degrees.
675      * @param chroma The desired chroma.
676      * @param lstar The desired L*.
677      * @return A hexadecimal representing the sRGB color. The color has sufficiently close hue,
678      *     chroma, and L* to the desired values, if possible; otherwise, the hue and L* will be
679      *     sufficiently close, and chroma will be maximized.
680      */
solveToInt(double hueDegrees, double chroma, double lstar)681     public static int solveToInt(double hueDegrees, double chroma, double lstar) {
682         if (chroma < 0.0001 || lstar < 0.0001 || lstar > 99.9999) {
683             return CamUtils.argbFromLstar(lstar);
684         }
685         hueDegrees = sanitizeDegreesDouble(hueDegrees);
686         double hueRadians = Math.toRadians(hueDegrees);
687         double y = CamUtils.yFromLstar(lstar);
688         int exactAnswer = findResultByJ(hueRadians, chroma, y);
689         if (exactAnswer != 0) {
690             return exactAnswer;
691         }
692         return bisectToLimit(y, hueRadians);
693     }
694 
695     /**
696      * Sanitizes a degree measure as a floating-point number.
697      *
698      * @return a degree measure between 0.0 (inclusive) and 360.0 (exclusive).
699      */
sanitizeDegreesDouble(double degrees)700     public static double sanitizeDegreesDouble(double degrees) {
701         degrees = degrees % 360.0;
702         if (degrees < 0) {
703             degrees = degrees + 360.0;
704         }
705         return degrees;
706     }
707 
708     /**
709      * Finds an sRGB color with the given hue, chroma, and L*, if possible.
710      *
711      * @param hueDegrees The desired hue, in degrees.
712      * @param chroma The desired chroma.
713      * @param lstar The desired L*.
714      * @return An CAM16 object representing the sRGB color. The color has sufficiently close hue,
715      *     chroma, and L* to the desired values, if possible; otherwise, the hue and L* will be
716      *     sufficiently close, and chroma will be maximized.
717      */
solveToCam(double hueDegrees, double chroma, double lstar)718     public static Cam solveToCam(double hueDegrees, double chroma, double lstar) {
719         return Cam.fromInt(solveToInt(hueDegrees, chroma, lstar));
720     }
721 }
722