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