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