1 /* 2 * Copyright (C) 2017 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; 18 19 import android.annotation.ColorInt; 20 import android.annotation.FloatRange; 21 import android.annotation.IntRange; 22 import android.annotation.NonNull; 23 import android.graphics.Color; 24 25 /** 26 * Copied from: frameworks/support/core-utils/java/android/support/v4/graphics/ColorUtils.java 27 * 28 * A set of color-related utility methods, building upon those available in {@code Color}. 29 */ 30 public final class ColorUtils { 31 32 private static final double XYZ_WHITE_REFERENCE_X = 95.047; 33 private static final double XYZ_WHITE_REFERENCE_Y = 100; 34 private static final double XYZ_WHITE_REFERENCE_Z = 108.883; 35 private static final double XYZ_EPSILON = 0.008856; 36 private static final double XYZ_KAPPA = 903.3; 37 38 private static final int MIN_ALPHA_SEARCH_MAX_ITERATIONS = 10; 39 private static final int MIN_ALPHA_SEARCH_PRECISION = 1; 40 41 private static final ThreadLocal<double[]> TEMP_ARRAY = new ThreadLocal<>(); 42 ColorUtils()43 private ColorUtils() {} 44 45 /** 46 * Composite two potentially translucent colors over each other and returns the result. 47 */ compositeColors(@olorInt int foreground, @ColorInt int background)48 public static int compositeColors(@ColorInt int foreground, @ColorInt int background) { 49 int bgAlpha = Color.alpha(background); 50 int fgAlpha = Color.alpha(foreground); 51 int a = compositeAlpha(fgAlpha, bgAlpha); 52 53 int r = compositeComponent(Color.red(foreground), fgAlpha, 54 Color.red(background), bgAlpha, a); 55 int g = compositeComponent(Color.green(foreground), fgAlpha, 56 Color.green(background), bgAlpha, a); 57 int b = compositeComponent(Color.blue(foreground), fgAlpha, 58 Color.blue(background), bgAlpha, a); 59 60 return Color.argb(a, r, g, b); 61 } 62 compositeAlpha(int foregroundAlpha, int backgroundAlpha)63 private static int compositeAlpha(int foregroundAlpha, int backgroundAlpha) { 64 return 0xFF - (((0xFF - backgroundAlpha) * (0xFF - foregroundAlpha)) / 0xFF); 65 } 66 compositeComponent(int fgC, int fgA, int bgC, int bgA, int a)67 private static int compositeComponent(int fgC, int fgA, int bgC, int bgA, int a) { 68 if (a == 0) return 0; 69 return ((0xFF * fgC * fgA) + (bgC * bgA * (0xFF - fgA))) / (a * 0xFF); 70 } 71 72 /** 73 * Returns the luminance of a color as a float between {@code 0.0} and {@code 1.0}. 74 * <p>Defined as the Y component in the XYZ representation of {@code color}.</p> 75 */ 76 @FloatRange(from = 0.0, to = 1.0) calculateLuminance(@olorInt int color)77 public static double calculateLuminance(@ColorInt int color) { 78 final double[] result = getTempDouble3Array(); 79 colorToXYZ(color, result); 80 // Luminance is the Y component 81 return result[1] / 100; 82 } 83 84 /** 85 * Returns the contrast ratio between {@code foreground} and {@code background}. 86 * {@code background} must be opaque. 87 * <p> 88 * Formula defined 89 * <a href="http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef">here</a>. 90 */ calculateContrast(@olorInt int foreground, @ColorInt int background)91 public static double calculateContrast(@ColorInt int foreground, @ColorInt int background) { 92 if (Color.alpha(background) != 255) { 93 throw new IllegalArgumentException("background can not be translucent: #" 94 + Integer.toHexString(background)); 95 } 96 if (Color.alpha(foreground) < 255) { 97 // If the foreground is translucent, composite the foreground over the background 98 foreground = compositeColors(foreground, background); 99 } 100 101 final double luminance1 = calculateLuminance(foreground) + 0.05; 102 final double luminance2 = calculateLuminance(background) + 0.05; 103 104 // Now return the lighter luminance divided by the darker luminance 105 return Math.max(luminance1, luminance2) / Math.min(luminance1, luminance2); 106 } 107 108 /** 109 * Calculates the minimum alpha value which can be applied to {@code background} so that would 110 * have a contrast value of at least {@code minContrastRatio} when alpha blended to 111 * {@code foreground}. 112 * 113 * @param foreground the foreground color 114 * @param background the background color, opacity will be ignored 115 * @param minContrastRatio the minimum contrast ratio 116 * @return the alpha value in the range 0-255, or -1 if no value could be calculated 117 */ calculateMinimumBackgroundAlpha(@olorInt int foreground, @ColorInt int background, float minContrastRatio)118 public static int calculateMinimumBackgroundAlpha(@ColorInt int foreground, 119 @ColorInt int background, float minContrastRatio) { 120 // Ignore initial alpha that the background might have since this is 121 // what we're trying to calculate. 122 background = setAlphaComponent(background, 255); 123 final int leastContrastyColor = setAlphaComponent(foreground, 255); 124 return binaryAlphaSearch(foreground, background, minContrastRatio, (fg, bg, alpha) -> { 125 int testBackground = blendARGB(leastContrastyColor, bg, alpha/255f); 126 // Float rounding might set this alpha to something other that 255, 127 // raising an exception in calculateContrast. 128 testBackground = setAlphaComponent(testBackground, 255); 129 return calculateContrast(fg, testBackground); 130 }); 131 } 132 133 /** 134 * Calculates the minimum alpha value which can be applied to {@code foreground} so that would 135 * have a contrast value of at least {@code minContrastRatio} when compared to 136 * {@code background}. 137 * 138 * @param foreground the foreground color 139 * @param background the opaque background color 140 * @param minContrastRatio the minimum contrast ratio 141 * @return the alpha value in the range 0-255, or -1 if no value could be calculated 142 */ calculateMinimumAlpha(@olorInt int foreground, @ColorInt int background, float minContrastRatio)143 public static int calculateMinimumAlpha(@ColorInt int foreground, @ColorInt int background, 144 float minContrastRatio) { 145 if (Color.alpha(background) != 255) { 146 throw new IllegalArgumentException("background can not be translucent: #" 147 + Integer.toHexString(background)); 148 } 149 150 ContrastCalculator contrastCalculator = (fg, bg, alpha) -> { 151 int testForeground = setAlphaComponent(fg, alpha); 152 return calculateContrast(testForeground, bg); 153 }; 154 155 // First lets check that a fully opaque foreground has sufficient contrast 156 double testRatio = contrastCalculator.calculateContrast(foreground, background, 255); 157 if (testRatio < minContrastRatio) { 158 // Fully opaque foreground does not have sufficient contrast, return error 159 return -1; 160 } 161 foreground = setAlphaComponent(foreground, 255); 162 return binaryAlphaSearch(foreground, background, minContrastRatio, contrastCalculator); 163 } 164 165 /** 166 * Calculates the alpha value using binary search based on a given contrast evaluation function 167 * and target contrast that needs to be satisfied. 168 * 169 * @param foreground the foreground color 170 * @param background the opaque background color 171 * @param minContrastRatio the minimum contrast ratio 172 * @param calculator function that calculates contrast 173 * @return the alpha value in the range 0-255, or -1 if no value could be calculated 174 */ binaryAlphaSearch(@olorInt int foreground, @ColorInt int background, float minContrastRatio, ContrastCalculator calculator)175 private static int binaryAlphaSearch(@ColorInt int foreground, @ColorInt int background, 176 float minContrastRatio, ContrastCalculator calculator) { 177 // Binary search to find a value with the minimum value which provides sufficient contrast 178 int numIterations = 0; 179 int minAlpha = 0; 180 int maxAlpha = 255; 181 182 while (numIterations <= MIN_ALPHA_SEARCH_MAX_ITERATIONS && 183 (maxAlpha - minAlpha) > MIN_ALPHA_SEARCH_PRECISION) { 184 final int testAlpha = (minAlpha + maxAlpha) / 2; 185 186 final double testRatio = calculator.calculateContrast(foreground, background, 187 testAlpha); 188 if (testRatio < minContrastRatio) { 189 minAlpha = testAlpha; 190 } else { 191 maxAlpha = testAlpha; 192 } 193 194 numIterations++; 195 } 196 197 // Conservatively return the max of the range of possible alphas, which is known to pass. 198 return maxAlpha; 199 } 200 201 /** 202 * Convert RGB components to HSL (hue-saturation-lightness). 203 * <ul> 204 * <li>outHsl[0] is Hue [0 .. 360)</li> 205 * <li>outHsl[1] is Saturation [0...1]</li> 206 * <li>outHsl[2] is Lightness [0...1]</li> 207 * </ul> 208 * 209 * @param r red component value [0..255] 210 * @param g green component value [0..255] 211 * @param b blue component value [0..255] 212 * @param outHsl 3-element array which holds the resulting HSL components 213 */ RGBToHSL(@ntRangefrom = 0x0, to = 0xFF) int r, @IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b, @NonNull float[] outHsl)214 public static void RGBToHSL(@IntRange(from = 0x0, to = 0xFF) int r, 215 @IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b, 216 @NonNull float[] outHsl) { 217 final float rf = r / 255f; 218 final float gf = g / 255f; 219 final float bf = b / 255f; 220 221 final float max = Math.max(rf, Math.max(gf, bf)); 222 final float min = Math.min(rf, Math.min(gf, bf)); 223 final float deltaMaxMin = max - min; 224 225 float h, s; 226 float l = (max + min) / 2f; 227 228 if (max == min) { 229 // Monochromatic 230 h = s = 0f; 231 } else { 232 if (max == rf) { 233 h = ((gf - bf) / deltaMaxMin) % 6f; 234 } else if (max == gf) { 235 h = ((bf - rf) / deltaMaxMin) + 2f; 236 } else { 237 h = ((rf - gf) / deltaMaxMin) + 4f; 238 } 239 240 s = deltaMaxMin / (1f - Math.abs(2f * l - 1f)); 241 } 242 243 h = (h * 60f) % 360f; 244 if (h < 0) { 245 h += 360f; 246 } 247 248 outHsl[0] = constrain(h, 0f, 360f); 249 outHsl[1] = constrain(s, 0f, 1f); 250 outHsl[2] = constrain(l, 0f, 1f); 251 } 252 253 /** 254 * Convert the ARGB color to its HSL (hue-saturation-lightness) components. 255 * <ul> 256 * <li>outHsl[0] is Hue [0 .. 360)</li> 257 * <li>outHsl[1] is Saturation [0...1]</li> 258 * <li>outHsl[2] is Lightness [0...1]</li> 259 * </ul> 260 * 261 * @param color the ARGB color to convert. The alpha component is ignored 262 * @param outHsl 3-element array which holds the resulting HSL components 263 */ colorToHSL(@olorInt int color, @NonNull float[] outHsl)264 public static void colorToHSL(@ColorInt int color, @NonNull float[] outHsl) { 265 RGBToHSL(Color.red(color), Color.green(color), Color.blue(color), outHsl); 266 } 267 268 /** 269 * Convert HSL (hue-saturation-lightness) components to a RGB color. 270 * <ul> 271 * <li>hsl[0] is Hue [0 .. 360)</li> 272 * <li>hsl[1] is Saturation [0...1]</li> 273 * <li>hsl[2] is Lightness [0...1]</li> 274 * </ul> 275 * If hsv values are out of range, they are pinned. 276 * 277 * @param hsl 3-element array which holds the input HSL components 278 * @return the resulting RGB color 279 */ 280 @ColorInt HSLToColor(@onNull float[] hsl)281 public static int HSLToColor(@NonNull float[] hsl) { 282 final float h = hsl[0]; 283 final float s = hsl[1]; 284 final float l = hsl[2]; 285 286 final float c = (1f - Math.abs(2 * l - 1f)) * s; 287 final float m = l - 0.5f * c; 288 final float x = c * (1f - Math.abs((h / 60f % 2f) - 1f)); 289 290 final int hueSegment = (int) h / 60; 291 292 int r = 0, g = 0, b = 0; 293 294 switch (hueSegment) { 295 case 0: 296 r = Math.round(255 * (c + m)); 297 g = Math.round(255 * (x + m)); 298 b = Math.round(255 * m); 299 break; 300 case 1: 301 r = Math.round(255 * (x + m)); 302 g = Math.round(255 * (c + m)); 303 b = Math.round(255 * m); 304 break; 305 case 2: 306 r = Math.round(255 * m); 307 g = Math.round(255 * (c + m)); 308 b = Math.round(255 * (x + m)); 309 break; 310 case 3: 311 r = Math.round(255 * m); 312 g = Math.round(255 * (x + m)); 313 b = Math.round(255 * (c + m)); 314 break; 315 case 4: 316 r = Math.round(255 * (x + m)); 317 g = Math.round(255 * m); 318 b = Math.round(255 * (c + m)); 319 break; 320 case 5: 321 case 6: 322 r = Math.round(255 * (c + m)); 323 g = Math.round(255 * m); 324 b = Math.round(255 * (x + m)); 325 break; 326 } 327 328 r = constrain(r, 0, 255); 329 g = constrain(g, 0, 255); 330 b = constrain(b, 0, 255); 331 332 return Color.rgb(r, g, b); 333 } 334 335 /** 336 * Set the alpha component of {@code color} to be {@code alpha}. 337 */ 338 @ColorInt setAlphaComponent(@olorInt int color, @IntRange(from = 0x0, to = 0xFF) int alpha)339 public static int setAlphaComponent(@ColorInt int color, 340 @IntRange(from = 0x0, to = 0xFF) int alpha) { 341 if (alpha < 0 || alpha > 255) { 342 throw new IllegalArgumentException("alpha must be between 0 and 255."); 343 } 344 return (color & 0x00ffffff) | (alpha << 24); 345 } 346 347 /** 348 * Convert the ARGB color to its CIE Lab representative components. 349 * 350 * @param color the ARGB color to convert. The alpha component is ignored 351 * @param outLab 3-element array which holds the resulting LAB components 352 */ colorToLAB(@olorInt int color, @NonNull double[] outLab)353 public static void colorToLAB(@ColorInt int color, @NonNull double[] outLab) { 354 RGBToLAB(Color.red(color), Color.green(color), Color.blue(color), outLab); 355 } 356 357 /** 358 * Convert RGB components to its CIE Lab representative components. 359 * 360 * <ul> 361 * <li>outLab[0] is L [0 ...1)</li> 362 * <li>outLab[1] is a [-128...127)</li> 363 * <li>outLab[2] is b [-128...127)</li> 364 * </ul> 365 * 366 * @param r red component value [0..255] 367 * @param g green component value [0..255] 368 * @param b blue component value [0..255] 369 * @param outLab 3-element array which holds the resulting LAB components 370 */ RGBToLAB(@ntRangefrom = 0x0, to = 0xFF) int r, @IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b, @NonNull double[] outLab)371 public static void RGBToLAB(@IntRange(from = 0x0, to = 0xFF) int r, 372 @IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b, 373 @NonNull double[] outLab) { 374 // First we convert RGB to XYZ 375 RGBToXYZ(r, g, b, outLab); 376 // outLab now contains XYZ 377 XYZToLAB(outLab[0], outLab[1], outLab[2], outLab); 378 // outLab now contains LAB representation 379 } 380 381 /** 382 * Convert the ARGB color to its CIE XYZ representative components. 383 * 384 * <p>The resulting XYZ representation will use the D65 illuminant and the CIE 385 * 2° Standard Observer (1931).</p> 386 * 387 * <ul> 388 * <li>outXyz[0] is X [0 ...95.047)</li> 389 * <li>outXyz[1] is Y [0...100)</li> 390 * <li>outXyz[2] is Z [0...108.883)</li> 391 * </ul> 392 * 393 * @param color the ARGB color to convert. The alpha component is ignored 394 * @param outXyz 3-element array which holds the resulting LAB components 395 */ colorToXYZ(@olorInt int color, @NonNull double[] outXyz)396 public static void colorToXYZ(@ColorInt int color, @NonNull double[] outXyz) { 397 RGBToXYZ(Color.red(color), Color.green(color), Color.blue(color), outXyz); 398 } 399 400 /** 401 * Convert RGB components to its CIE XYZ representative components. 402 * 403 * <p>The resulting XYZ representation will use the D65 illuminant and the CIE 404 * 2° Standard Observer (1931).</p> 405 * 406 * <ul> 407 * <li>outXyz[0] is X [0 ...95.047)</li> 408 * <li>outXyz[1] is Y [0...100)</li> 409 * <li>outXyz[2] is Z [0...108.883)</li> 410 * </ul> 411 * 412 * @param r red component value [0..255] 413 * @param g green component value [0..255] 414 * @param b blue component value [0..255] 415 * @param outXyz 3-element array which holds the resulting XYZ components 416 */ RGBToXYZ(@ntRangefrom = 0x0, to = 0xFF) int r, @IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b, @NonNull double[] outXyz)417 public static void RGBToXYZ(@IntRange(from = 0x0, to = 0xFF) int r, 418 @IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b, 419 @NonNull double[] outXyz) { 420 if (outXyz.length != 3) { 421 throw new IllegalArgumentException("outXyz must have a length of 3."); 422 } 423 424 double sr = r / 255.0; 425 sr = sr < 0.04045 ? sr / 12.92 : Math.pow((sr + 0.055) / 1.055, 2.4); 426 double sg = g / 255.0; 427 sg = sg < 0.04045 ? sg / 12.92 : Math.pow((sg + 0.055) / 1.055, 2.4); 428 double sb = b / 255.0; 429 sb = sb < 0.04045 ? sb / 12.92 : Math.pow((sb + 0.055) / 1.055, 2.4); 430 431 outXyz[0] = 100 * (sr * 0.4124 + sg * 0.3576 + sb * 0.1805); 432 outXyz[1] = 100 * (sr * 0.2126 + sg * 0.7152 + sb * 0.0722); 433 outXyz[2] = 100 * (sr * 0.0193 + sg * 0.1192 + sb * 0.9505); 434 } 435 436 /** 437 * Converts a color from CIE XYZ to CIE Lab representation. 438 * 439 * <p>This method expects the XYZ representation to use the D65 illuminant and the CIE 440 * 2° Standard Observer (1931).</p> 441 * 442 * <ul> 443 * <li>outLab[0] is L [0 ...1)</li> 444 * <li>outLab[1] is a [-128...127)</li> 445 * <li>outLab[2] is b [-128...127)</li> 446 * </ul> 447 * 448 * @param x X component value [0...95.047) 449 * @param y Y component value [0...100) 450 * @param z Z component value [0...108.883) 451 * @param outLab 3-element array which holds the resulting Lab components 452 */ 453 public static void XYZToLAB(@FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_X) double x, 454 @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Y) double y, 455 @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Z) double z, 456 @NonNull double[] outLab) { 457 if (outLab.length != 3) { 458 throw new IllegalArgumentException("outLab must have a length of 3."); 459 } 460 x = pivotXyzComponent(x / XYZ_WHITE_REFERENCE_X); 461 y = pivotXyzComponent(y / XYZ_WHITE_REFERENCE_Y); 462 z = pivotXyzComponent(z / XYZ_WHITE_REFERENCE_Z); 463 outLab[0] = Math.max(0, 116 * y - 16); 464 outLab[1] = 500 * (x - y); 465 outLab[2] = 200 * (y - z); 466 } 467 468 /** 469 * Converts a color from CIE Lab to CIE XYZ representation. 470 * 471 * <p>The resulting XYZ representation will use the D65 illuminant and the CIE 472 * 2° Standard Observer (1931).</p> 473 * 474 * <ul> 475 * <li>outXyz[0] is X [0 ...95.047)</li> 476 * <li>outXyz[1] is Y [0...100)</li> 477 * <li>outXyz[2] is Z [0...108.883)</li> 478 * </ul> 479 * 480 * @param l L component value [0...100) 481 * @param a A component value [-128...127) 482 * @param b B component value [-128...127) 483 * @param outXyz 3-element array which holds the resulting XYZ components 484 */ 485 public static void LABToXYZ(@FloatRange(from = 0f, to = 100) final double l, 486 @FloatRange(from = -128, to = 127) final double a, 487 @FloatRange(from = -128, to = 127) final double b, 488 @NonNull double[] outXyz) { 489 final double fy = (l + 16) / 116; 490 final double fx = a / 500 + fy; 491 final double fz = fy - b / 200; 492 493 double tmp = Math.pow(fx, 3); 494 final double xr = tmp > XYZ_EPSILON ? tmp : (116 * fx - 16) / XYZ_KAPPA; 495 final double yr = l > XYZ_KAPPA * XYZ_EPSILON ? Math.pow(fy, 3) : l / XYZ_KAPPA; 496 497 tmp = Math.pow(fz, 3); 498 final double zr = tmp > XYZ_EPSILON ? tmp : (116 * fz - 16) / XYZ_KAPPA; 499 500 outXyz[0] = xr * XYZ_WHITE_REFERENCE_X; 501 outXyz[1] = yr * XYZ_WHITE_REFERENCE_Y; 502 outXyz[2] = zr * XYZ_WHITE_REFERENCE_Z; 503 } 504 505 /** 506 * Converts a color from CIE XYZ to its RGB representation. 507 * 508 * <p>This method expects the XYZ representation to use the D65 illuminant and the CIE 509 * 2° Standard Observer (1931).</p> 510 * 511 * @param x X component value [0...95.047) 512 * @param y Y component value [0...100) 513 * @param z Z component value [0...108.883) 514 * @return int containing the RGB representation 515 */ 516 @ColorInt XYZToColor(@loatRangefrom = 0f, to = XYZ_WHITE_REFERENCE_X) double x, @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Y) double y, @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Z) double z)517 public static int XYZToColor(@FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_X) double x, 518 @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Y) double y, 519 @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Z) double z) { 520 double r = (x * 3.2406 + y * -1.5372 + z * -0.4986) / 100; 521 double g = (x * -0.9689 + y * 1.8758 + z * 0.0415) / 100; 522 double b = (x * 0.0557 + y * -0.2040 + z * 1.0570) / 100; 523 524 r = r > 0.0031308 ? 1.055 * Math.pow(r, 1 / 2.4) - 0.055 : 12.92 * r; 525 g = g > 0.0031308 ? 1.055 * Math.pow(g, 1 / 2.4) - 0.055 : 12.92 * g; 526 b = b > 0.0031308 ? 1.055 * Math.pow(b, 1 / 2.4) - 0.055 : 12.92 * b; 527 528 return Color.rgb( 529 constrain((int) Math.round(r * 255), 0, 255), 530 constrain((int) Math.round(g * 255), 0, 255), 531 constrain((int) Math.round(b * 255), 0, 255)); 532 } 533 534 /** 535 * Converts a color from CIE Lab to its RGB representation. 536 * 537 * @param l L component value [0...100] 538 * @param a A component value [-128...127] 539 * @param b B component value [-128...127] 540 * @return int containing the RGB representation 541 */ 542 @ColorInt LABToColor(@loatRangefrom = 0f, to = 100) final double l, @FloatRange(from = -128, to = 127) final double a, @FloatRange(from = -128, to = 127) final double b)543 public static int LABToColor(@FloatRange(from = 0f, to = 100) final double l, 544 @FloatRange(from = -128, to = 127) final double a, 545 @FloatRange(from = -128, to = 127) final double b) { 546 final double[] result = getTempDouble3Array(); 547 LABToXYZ(l, a, b, result); 548 return XYZToColor(result[0], result[1], result[2]); 549 } 550 551 /** 552 * Returns the euclidean distance between two LAB colors. 553 */ distanceEuclidean(@onNull double[] labX, @NonNull double[] labY)554 public static double distanceEuclidean(@NonNull double[] labX, @NonNull double[] labY) { 555 return Math.sqrt(Math.pow(labX[0] - labY[0], 2) 556 + Math.pow(labX[1] - labY[1], 2) 557 + Math.pow(labX[2] - labY[2], 2)); 558 } 559 constrain(float amount, float low, float high)560 private static float constrain(float amount, float low, float high) { 561 return amount < low ? low : (amount > high ? high : amount); 562 } 563 constrain(int amount, int low, int high)564 private static int constrain(int amount, int low, int high) { 565 return amount < low ? low : (amount > high ? high : amount); 566 } 567 pivotXyzComponent(double component)568 private static double pivotXyzComponent(double component) { 569 return component > XYZ_EPSILON 570 ? Math.pow(component, 1 / 3.0) 571 : (XYZ_KAPPA * component + 16) / 116; 572 } 573 574 /** 575 * Blend between two ARGB colors using the given ratio. 576 * 577 * <p>A blend ratio of 0.0 will result in {@code color1}, 0.5 will give an even blend, 578 * 1.0 will result in {@code color2}.</p> 579 * 580 * @param color1 the first ARGB color 581 * @param color2 the second ARGB color 582 * @param ratio the blend ratio of {@code color1} to {@code color2} 583 */ 584 @ColorInt blendARGB(@olorInt int color1, @ColorInt int color2, @FloatRange(from = 0.0, to = 1.0) float ratio)585 public static int blendARGB(@ColorInt int color1, @ColorInt int color2, 586 @FloatRange(from = 0.0, to = 1.0) float ratio) { 587 final float inverseRatio = 1 - ratio; 588 float a = Color.alpha(color1) * inverseRatio + Color.alpha(color2) * ratio; 589 float r = Color.red(color1) * inverseRatio + Color.red(color2) * ratio; 590 float g = Color.green(color1) * inverseRatio + Color.green(color2) * ratio; 591 float b = Color.blue(color1) * inverseRatio + Color.blue(color2) * ratio; 592 return Color.argb((int) a, (int) r, (int) g, (int) b); 593 } 594 595 /** 596 * Blend between {@code hsl1} and {@code hsl2} using the given ratio. This will interpolate 597 * the hue using the shortest angle. 598 * 599 * <p>A blend ratio of 0.0 will result in {@code hsl1}, 0.5 will give an even blend, 600 * 1.0 will result in {@code hsl2}.</p> 601 * 602 * @param hsl1 3-element array which holds the first HSL color 603 * @param hsl2 3-element array which holds the second HSL color 604 * @param ratio the blend ratio of {@code hsl1} to {@code hsl2} 605 * @param outResult 3-element array which holds the resulting HSL components 606 */ blendHSL(@onNull float[] hsl1, @NonNull float[] hsl2, @FloatRange(from = 0.0, to = 1.0) float ratio, @NonNull float[] outResult)607 public static void blendHSL(@NonNull float[] hsl1, @NonNull float[] hsl2, 608 @FloatRange(from = 0.0, to = 1.0) float ratio, @NonNull float[] outResult) { 609 if (outResult.length != 3) { 610 throw new IllegalArgumentException("result must have a length of 3."); 611 } 612 final float inverseRatio = 1 - ratio; 613 // Since hue is circular we will need to interpolate carefully 614 outResult[0] = circularInterpolate(hsl1[0], hsl2[0], ratio); 615 outResult[1] = hsl1[1] * inverseRatio + hsl2[1] * ratio; 616 outResult[2] = hsl1[2] * inverseRatio + hsl2[2] * ratio; 617 } 618 619 /** 620 * Blend between two CIE-LAB colors using the given ratio. 621 * 622 * <p>A blend ratio of 0.0 will result in {@code lab1}, 0.5 will give an even blend, 623 * 1.0 will result in {@code lab2}.</p> 624 * 625 * @param lab1 3-element array which holds the first LAB color 626 * @param lab2 3-element array which holds the second LAB color 627 * @param ratio the blend ratio of {@code lab1} to {@code lab2} 628 * @param outResult 3-element array which holds the resulting LAB components 629 */ blendLAB(@onNull double[] lab1, @NonNull double[] lab2, @FloatRange(from = 0.0, to = 1.0) double ratio, @NonNull double[] outResult)630 public static void blendLAB(@NonNull double[] lab1, @NonNull double[] lab2, 631 @FloatRange(from = 0.0, to = 1.0) double ratio, @NonNull double[] outResult) { 632 if (outResult.length != 3) { 633 throw new IllegalArgumentException("outResult must have a length of 3."); 634 } 635 final double inverseRatio = 1 - ratio; 636 outResult[0] = lab1[0] * inverseRatio + lab2[0] * ratio; 637 outResult[1] = lab1[1] * inverseRatio + lab2[1] * ratio; 638 outResult[2] = lab1[2] * inverseRatio + lab2[2] * ratio; 639 } 640 circularInterpolate(float a, float b, float f)641 static float circularInterpolate(float a, float b, float f) { 642 if (Math.abs(b - a) > 180) { 643 if (b > a) { 644 a += 360; 645 } else { 646 b += 360; 647 } 648 } 649 return (a + ((b - a) * f)) % 360; 650 } 651 getTempDouble3Array()652 private static double[] getTempDouble3Array() { 653 double[] result = TEMP_ARRAY.get(); 654 if (result == null) { 655 result = new double[3]; 656 TEMP_ARRAY.set(result); 657 } 658 return result; 659 } 660 661 private interface ContrastCalculator { calculateContrast(int foreground, int background, int alpha)662 double calculateContrast(int foreground, int background, int alpha); 663 } 664 665 }