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