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 package com.google.ux.material.libmonet.hct; 18 19 import static java.lang.Math.max; 20 21 import com.google.ux.material.libmonet.utils.ColorUtils; 22 23 /** 24 * CAM16, a color appearance model. Colors are not just defined by their hex code, but rather, a hex 25 * code and viewing conditions. 26 * 27 * <p>CAM16 instances also have coordinates in the CAM16-UCS space, called J*, a*, b*, or jstar, 28 * astar, bstar in code. CAM16-UCS is included in the CAM16 specification, and should be used when 29 * measuring distances between colors. 30 * 31 * <p>In traditional color spaces, a color can be identified solely by the observer's measurement of 32 * the color. Color appearance models such as CAM16 also use information about the environment where 33 * the color was observed, known as the viewing conditions. 34 * 35 * <p>For example, white under the traditional assumption of a midday sun white point is accurately 36 * measured as a slightly chromatic blue by CAM16. (roughly, hue 203, chroma 3, lightness 100) 37 */ 38 public final class Cam16 { 39 // Transforms XYZ color space coordinates to 'cone'/'RGB' responses in CAM16. 40 static final double[][] XYZ_TO_CAM16RGB = { 41 {0.401288, 0.650173, -0.051461}, 42 {-0.250268, 1.204414, 0.045854}, 43 {-0.002079, 0.048952, 0.953127} 44 }; 45 46 // Transforms 'cone'/'RGB' responses in CAM16 to XYZ color space coordinates. 47 static final double[][] CAM16RGB_TO_XYZ = { 48 {1.8620678, -1.0112547, 0.14918678}, 49 {0.38752654, 0.62144744, -0.00897398}, 50 {-0.01584150, -0.03412294, 1.0499644} 51 }; 52 53 // CAM16 color dimensions, see getters for documentation. 54 private final double hue; 55 private final double chroma; 56 private final double j; 57 private final double q; 58 private final double m; 59 private final double s; 60 61 // Coordinates in UCS space. Used to determine color distance, like delta E equations in L*a*b*. 62 private final double jstar; 63 private final double astar; 64 private final double bstar; 65 66 // Avoid allocations during conversion by pre-allocating an array. 67 private final double[] tempArray = new double[] {0.0, 0.0, 0.0}; 68 69 /** 70 * CAM16 instances also have coordinates in the CAM16-UCS space, called J*, a*, b*, or jstar, 71 * astar, bstar in code. CAM16-UCS is included in the CAM16 specification, and is used to measure 72 * distances between colors. 73 */ distance(Cam16 other)74 public double distance(Cam16 other) { 75 double dJ = getJstar() - other.getJstar(); 76 double dA = getAstar() - other.getAstar(); 77 double dB = getBstar() - other.getBstar(); 78 double dEPrime = Math.sqrt(dJ * dJ + dA * dA + dB * dB); 79 double dE = 1.41 * Math.pow(dEPrime, 0.63); 80 return dE; 81 } 82 83 /** Hue in CAM16 */ getHue()84 public double getHue() { 85 return hue; 86 } 87 88 /** Chroma in CAM16 */ getChroma()89 public double getChroma() { 90 return chroma; 91 } 92 93 /** Lightness in CAM16 */ getJ()94 public double getJ() { 95 return j; 96 } 97 98 /** 99 * Brightness in CAM16. 100 * 101 * <p>Prefer lightness, brightness is an absolute quantity. For example, a sheet of white paper is 102 * much brighter viewed in sunlight than in indoor light, but it is the lightest object under any 103 * lighting. 104 */ getQ()105 public double getQ() { 106 return q; 107 } 108 109 /** 110 * Colorfulness in CAM16. 111 * 112 * <p>Prefer chroma, colorfulness is an absolute quantity. For example, a yellow toy car is much 113 * more colorful outside than inside, but it has the same chroma in both environments. 114 */ getM()115 public double getM() { 116 return m; 117 } 118 119 /** 120 * Saturation in CAM16. 121 * 122 * <p>Colorfulness in proportion to brightness. Prefer chroma, saturation measures colorfulness 123 * relative to the color's own brightness, where chroma is colorfulness relative to white. 124 */ getS()125 public double getS() { 126 return s; 127 } 128 129 /** Lightness coordinate in CAM16-UCS */ getJstar()130 public double getJstar() { 131 return jstar; 132 } 133 134 /** a* coordinate in CAM16-UCS */ getAstar()135 public double getAstar() { 136 return astar; 137 } 138 139 /** b* coordinate in CAM16-UCS */ getBstar()140 public double getBstar() { 141 return bstar; 142 } 143 144 /** 145 * All of the CAM16 dimensions can be calculated from 3 of the dimensions, in the following 146 * combinations: - {j or q} and {c, m, or s} and hue - jstar, astar, bstar Prefer using a static 147 * method that constructs from 3 of those dimensions. This constructor is intended for those 148 * methods to use to return all possible dimensions. 149 * 150 * @param hue for example, red, orange, yellow, green, etc. 151 * @param chroma informally, colorfulness / color intensity. like saturation in HSL, except 152 * perceptually accurate. 153 * @param j lightness 154 * @param q brightness; ratio of lightness to white point's lightness 155 * @param m colorfulness 156 * @param s saturation; ratio of chroma to white point's chroma 157 * @param jstar CAM16-UCS J coordinate 158 * @param astar CAM16-UCS a coordinate 159 * @param bstar CAM16-UCS b coordinate 160 */ Cam16( double hue, double chroma, double j, double q, double m, double s, double jstar, double astar, double bstar)161 private Cam16( 162 double hue, 163 double chroma, 164 double j, 165 double q, 166 double m, 167 double s, 168 double jstar, 169 double astar, 170 double bstar) { 171 this.hue = hue; 172 this.chroma = chroma; 173 this.j = j; 174 this.q = q; 175 this.m = m; 176 this.s = s; 177 this.jstar = jstar; 178 this.astar = astar; 179 this.bstar = bstar; 180 } 181 182 /** 183 * Create a CAM16 color from a color, assuming the color was viewed in default viewing conditions. 184 * 185 * @param argb ARGB representation of a color. 186 */ fromInt(int argb)187 public static Cam16 fromInt(int argb) { 188 return fromIntInViewingConditions(argb, ViewingConditions.DEFAULT); 189 } 190 191 /** 192 * Create a CAM16 color from a color in defined viewing conditions. 193 * 194 * @param argb ARGB representation of a color. 195 * @param viewingConditions Information about the environment where the color was observed. 196 */ 197 // The RGB => XYZ conversion matrix elements are derived scientific constants. While the values 198 // may differ at runtime due to floating point imprecision, keeping the values the same, and 199 // accurate, across implementations takes precedence. 200 @SuppressWarnings("FloatingPointLiteralPrecision") fromIntInViewingConditions(int argb, ViewingConditions viewingConditions)201 static Cam16 fromIntInViewingConditions(int argb, ViewingConditions viewingConditions) { 202 // Transform ARGB int to XYZ 203 int red = (argb & 0x00ff0000) >> 16; 204 int green = (argb & 0x0000ff00) >> 8; 205 int blue = (argb & 0x000000ff); 206 double redL = ColorUtils.linearized(red); 207 double greenL = ColorUtils.linearized(green); 208 double blueL = ColorUtils.linearized(blue); 209 double x = 0.41233895 * redL + 0.35762064 * greenL + 0.18051042 * blueL; 210 double y = 0.2126 * redL + 0.7152 * greenL + 0.0722 * blueL; 211 double z = 0.01932141 * redL + 0.11916382 * greenL + 0.95034478 * blueL; 212 213 return fromXyzInViewingConditions(x, y, z, viewingConditions); 214 } 215 fromXyzInViewingConditions( double x, double y, double z, ViewingConditions viewingConditions)216 static Cam16 fromXyzInViewingConditions( 217 double x, double y, double z, ViewingConditions viewingConditions) { 218 // Transform XYZ to 'cone'/'rgb' responses 219 double[][] matrix = XYZ_TO_CAM16RGB; 220 double rT = (x * matrix[0][0]) + (y * matrix[0][1]) + (z * matrix[0][2]); 221 double gT = (x * matrix[1][0]) + (y * matrix[1][1]) + (z * matrix[1][2]); 222 double bT = (x * matrix[2][0]) + (y * matrix[2][1]) + (z * matrix[2][2]); 223 224 // Discount illuminant 225 double rD = viewingConditions.getRgbD()[0] * rT; 226 double gD = viewingConditions.getRgbD()[1] * gT; 227 double bD = viewingConditions.getRgbD()[2] * bT; 228 229 // Chromatic adaptation 230 double rAF = Math.pow(viewingConditions.getFl() * Math.abs(rD) / 100.0, 0.42); 231 double gAF = Math.pow(viewingConditions.getFl() * Math.abs(gD) / 100.0, 0.42); 232 double bAF = Math.pow(viewingConditions.getFl() * Math.abs(bD) / 100.0, 0.42); 233 double rA = Math.signum(rD) * 400.0 * rAF / (rAF + 27.13); 234 double gA = Math.signum(gD) * 400.0 * gAF / (gAF + 27.13); 235 double bA = Math.signum(bD) * 400.0 * bAF / (bAF + 27.13); 236 237 // redness-greenness 238 double a = (11.0 * rA + -12.0 * gA + bA) / 11.0; 239 // yellowness-blueness 240 double b = (rA + gA - 2.0 * bA) / 9.0; 241 242 // auxiliary components 243 double u = (20.0 * rA + 20.0 * gA + 21.0 * bA) / 20.0; 244 double p2 = (40.0 * rA + 20.0 * gA + bA) / 20.0; 245 246 // hue 247 double atan2 = Math.atan2(b, a); 248 double atanDegrees = Math.toDegrees(atan2); 249 double hue = 250 atanDegrees < 0 251 ? atanDegrees + 360.0 252 : atanDegrees >= 360 ? atanDegrees - 360.0 : atanDegrees; 253 double hueRadians = Math.toRadians(hue); 254 255 // achromatic response to color 256 double ac = p2 * viewingConditions.getNbb(); 257 258 // CAM16 lightness and brightness 259 double j = 260 100.0 261 * Math.pow( 262 ac / viewingConditions.getAw(), 263 viewingConditions.getC() * viewingConditions.getZ()); 264 double q = 265 4.0 266 / viewingConditions.getC() 267 * Math.sqrt(j / 100.0) 268 * (viewingConditions.getAw() + 4.0) 269 * viewingConditions.getFlRoot(); 270 271 // CAM16 chroma, colorfulness, and saturation. 272 double huePrime = (hue < 20.14) ? hue + 360 : hue; 273 double eHue = 0.25 * (Math.cos(Math.toRadians(huePrime) + 2.0) + 3.8); 274 double p1 = 50000.0 / 13.0 * eHue * viewingConditions.getNc() * viewingConditions.getNcb(); 275 double t = p1 * Math.hypot(a, b) / (u + 0.305); 276 double alpha = 277 Math.pow(1.64 - Math.pow(0.29, viewingConditions.getN()), 0.73) * Math.pow(t, 0.9); 278 // CAM16 chroma, colorfulness, saturation 279 double c = alpha * Math.sqrt(j / 100.0); 280 double m = c * viewingConditions.getFlRoot(); 281 double s = 282 50.0 * Math.sqrt((alpha * viewingConditions.getC()) / (viewingConditions.getAw() + 4.0)); 283 284 // CAM16-UCS components 285 double jstar = (1.0 + 100.0 * 0.007) * j / (1.0 + 0.007 * j); 286 double mstar = 1.0 / 0.0228 * Math.log1p(0.0228 * m); 287 double astar = mstar * Math.cos(hueRadians); 288 double bstar = mstar * Math.sin(hueRadians); 289 290 return new Cam16(hue, c, j, q, m, s, jstar, astar, bstar); 291 } 292 293 /** 294 * @param j CAM16 lightness 295 * @param c CAM16 chroma 296 * @param h CAM16 hue 297 */ fromJch(double j, double c, double h)298 static Cam16 fromJch(double j, double c, double h) { 299 return fromJchInViewingConditions(j, c, h, ViewingConditions.DEFAULT); 300 } 301 302 /** 303 * @param j CAM16 lightness 304 * @param c CAM16 chroma 305 * @param h CAM16 hue 306 * @param viewingConditions Information about the environment where the color was observed. 307 */ fromJchInViewingConditions( double j, double c, double h, ViewingConditions viewingConditions)308 private static Cam16 fromJchInViewingConditions( 309 double j, double c, double h, ViewingConditions viewingConditions) { 310 double q = 311 4.0 312 / viewingConditions.getC() 313 * Math.sqrt(j / 100.0) 314 * (viewingConditions.getAw() + 4.0) 315 * viewingConditions.getFlRoot(); 316 double m = c * viewingConditions.getFlRoot(); 317 double alpha = c / Math.sqrt(j / 100.0); 318 double s = 319 50.0 * Math.sqrt((alpha * viewingConditions.getC()) / (viewingConditions.getAw() + 4.0)); 320 321 double hueRadians = Math.toRadians(h); 322 double jstar = (1.0 + 100.0 * 0.007) * j / (1.0 + 0.007 * j); 323 double mstar = 1.0 / 0.0228 * Math.log1p(0.0228 * m); 324 double astar = mstar * Math.cos(hueRadians); 325 double bstar = mstar * Math.sin(hueRadians); 326 return new Cam16(h, c, j, q, m, s, jstar, astar, bstar); 327 } 328 329 /** 330 * Create a CAM16 color from CAM16-UCS coordinates. 331 * 332 * @param jstar CAM16-UCS lightness. 333 * @param astar CAM16-UCS a dimension. Like a* in L*a*b*, it is a Cartesian coordinate on the Y 334 * axis. 335 * @param bstar CAM16-UCS b dimension. Like a* in L*a*b*, it is a Cartesian coordinate on the X 336 * axis. 337 */ fromUcs(double jstar, double astar, double bstar)338 public static Cam16 fromUcs(double jstar, double astar, double bstar) { 339 340 return fromUcsInViewingConditions(jstar, astar, bstar, ViewingConditions.DEFAULT); 341 } 342 343 /** 344 * Create a CAM16 color from CAM16-UCS coordinates in defined viewing conditions. 345 * 346 * @param jstar CAM16-UCS lightness. 347 * @param astar CAM16-UCS a dimension. Like a* in L*a*b*, it is a Cartesian coordinate on the Y 348 * axis. 349 * @param bstar CAM16-UCS b dimension. Like a* in L*a*b*, it is a Cartesian coordinate on the X 350 * axis. 351 * @param viewingConditions Information about the environment where the color was observed. 352 */ fromUcsInViewingConditions( double jstar, double astar, double bstar, ViewingConditions viewingConditions)353 public static Cam16 fromUcsInViewingConditions( 354 double jstar, double astar, double bstar, ViewingConditions viewingConditions) { 355 356 double m = Math.hypot(astar, bstar); 357 double m2 = Math.expm1(m * 0.0228) / 0.0228; 358 double c = m2 / viewingConditions.getFlRoot(); 359 double h = Math.atan2(bstar, astar) * (180.0 / Math.PI); 360 if (h < 0.0) { 361 h += 360.0; 362 } 363 double j = jstar / (1. - (jstar - 100.) * 0.007); 364 return fromJchInViewingConditions(j, c, h, viewingConditions); 365 } 366 367 /** 368 * ARGB representation of the color. Assumes the color was viewed in default viewing conditions, 369 * which are near-identical to the default viewing conditions for sRGB. 370 */ toInt()371 public int toInt() { 372 return viewed(ViewingConditions.DEFAULT); 373 } 374 375 /** 376 * ARGB representation of the color, in defined viewing conditions. 377 * 378 * @param viewingConditions Information about the environment where the color will be viewed. 379 * @return ARGB representation of color 380 */ viewed(ViewingConditions viewingConditions)381 int viewed(ViewingConditions viewingConditions) { 382 double[] xyz = xyzInViewingConditions(viewingConditions, tempArray); 383 return ColorUtils.argbFromXyz(xyz[0], xyz[1], xyz[2]); 384 } 385 xyzInViewingConditions(ViewingConditions viewingConditions, double[] returnArray)386 double[] xyzInViewingConditions(ViewingConditions viewingConditions, double[] returnArray) { 387 double alpha = 388 (getChroma() == 0.0 || getJ() == 0.0) ? 0.0 : getChroma() / Math.sqrt(getJ() / 100.0); 389 390 double t = 391 Math.pow( 392 alpha / Math.pow(1.64 - Math.pow(0.29, viewingConditions.getN()), 0.73), 1.0 / 0.9); 393 double hRad = Math.toRadians(getHue()); 394 395 double eHue = 0.25 * (Math.cos(hRad + 2.0) + 3.8); 396 double ac = 397 viewingConditions.getAw() 398 * Math.pow(getJ() / 100.0, 1.0 / viewingConditions.getC() / viewingConditions.getZ()); 399 double p1 = eHue * (50000.0 / 13.0) * viewingConditions.getNc() * viewingConditions.getNcb(); 400 double p2 = (ac / viewingConditions.getNbb()); 401 402 double hSin = Math.sin(hRad); 403 double hCos = Math.cos(hRad); 404 405 double gamma = 23.0 * (p2 + 0.305) * t / (23.0 * p1 + 11.0 * t * hCos + 108.0 * t * hSin); 406 double a = gamma * hCos; 407 double b = gamma * hSin; 408 double rA = (460.0 * p2 + 451.0 * a + 288.0 * b) / 1403.0; 409 double gA = (460.0 * p2 - 891.0 * a - 261.0 * b) / 1403.0; 410 double bA = (460.0 * p2 - 220.0 * a - 6300.0 * b) / 1403.0; 411 412 double rCBase = max(0, (27.13 * Math.abs(rA)) / (400.0 - Math.abs(rA))); 413 double rC = 414 Math.signum(rA) * (100.0 / viewingConditions.getFl()) * Math.pow(rCBase, 1.0 / 0.42); 415 double gCBase = max(0, (27.13 * Math.abs(gA)) / (400.0 - Math.abs(gA))); 416 double gC = 417 Math.signum(gA) * (100.0 / viewingConditions.getFl()) * Math.pow(gCBase, 1.0 / 0.42); 418 double bCBase = max(0, (27.13 * Math.abs(bA)) / (400.0 - Math.abs(bA))); 419 double bC = 420 Math.signum(bA) * (100.0 / viewingConditions.getFl()) * Math.pow(bCBase, 1.0 / 0.42); 421 double rF = rC / viewingConditions.getRgbD()[0]; 422 double gF = gC / viewingConditions.getRgbD()[1]; 423 double bF = bC / viewingConditions.getRgbD()[2]; 424 425 double[][] matrix = CAM16RGB_TO_XYZ; 426 double x = (rF * matrix[0][0]) + (gF * matrix[0][1]) + (bF * matrix[0][2]); 427 double y = (rF * matrix[1][0]) + (gF * matrix[1][1]) + (bF * matrix[1][2]); 428 double z = (rF * matrix[2][0]) + (gF * matrix[2][1]) + (bF * matrix[2][2]); 429 430 if (returnArray != null) { 431 returnArray[0] = x; 432 returnArray[1] = y; 433 returnArray[2] = z; 434 return returnArray; 435 } else { 436 return new double[] {x, y, z}; 437 } 438 } 439 } 440