Home | History | Annotate | Download | only in graphics
      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 
     43     private ColorUtils() {}
     44 
     45     /**
     46      * Composite two potentially translucent colors over each other and returns the result.
     47      */
     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 
     63     private static int compositeAlpha(int foregroundAlpha, int backgroundAlpha) {
     64         return 0xFF - (((0xFF - backgroundAlpha) * (0xFF - foregroundAlpha)) / 0xFF);
     65     }
     66 
     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)
     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      */
     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      */
    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      */
    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      */
    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      */
    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      */
    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
    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
    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      */
    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      */
    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      */
    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      */
    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
    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
    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      */
    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 
    560     private static float constrain(float amount, float low, float high) {
    561         return amount < low ? low : (amount > high ? high : amount);
    562     }
    563 
    564     private static int constrain(int amount, int low, int high) {
    565         return amount < low ? low : (amount > high ? high : amount);
    566     }
    567 
    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
    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      */
    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      */
    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 
    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 
    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 {
    662         double calculateContrast(int foreground, int background, int alpha);
    663     }
    664 
    665 }