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