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