tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

color.js (19539B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 "use strict";
      6 
      7 const COLOR_UNIT_PREF = "devtools.defaultColorUnit";
      8 const SPECIALVALUES = new Set([
      9  "currentcolor",
     10  "initial",
     11  "inherit",
     12  "transparent",
     13  "unset",
     14 ]);
     15 
     16 /**
     17 * This module is used to convert between various color types.
     18 *
     19 * Usage:
     20 *   let {colorUtils} = require("devtools/shared/css/color");
     21 *   let color = new colorUtils.CssColor("red");
     22 *   // In order to support css-color-4 color function, pass true to the
     23 *   // second argument.
     24 *   // e.g.
     25 *   //   let color = new colorUtils.CssColor("red", true);
     26 *
     27 *   color.authored === "red"
     28 *   color.hasAlpha === false
     29 *   color.valid === true
     30 *   color.transparent === false // transparent has a special status.
     31 *   color.name === "red"        // returns hex when no name available.
     32 *   color.hex === "#f00"        // returns shortHex when available else returns
     33 *                                  longHex. If alpha channel is present then we
     34 *                                  return this.alphaHex if available,
     35 *                                  or this.longAlphaHex if not.
     36 *   color.alphaHex === "#f00f"  // returns short alpha hex when available
     37 *                                  else returns longAlphaHex.
     38 *   color.longHex === "#ff0000" // If alpha channel is present then we return
     39 *                                  this.longAlphaHex.
     40 *   color.longAlphaHex === "#ff0000ff"
     41 *   color.rgb === "rgb(255, 0, 0)" // If alpha channel is present
     42 *                                  // then we return this.rgba.
     43 *   color.rgba === "rgba(255, 0, 0, 1)"
     44 *   color.hsl === "hsl(0, 100%, 50%)"
     45 *   color.hsla === "hsla(0, 100%, 50%, 1)" // If alpha channel is present
     46 *                                             then we return this.rgba.
     47 *   color.hwb === "hwb(0, 0%, 0%)"
     48 *
     49 *   color.toString() === "#f00"; // Outputs the color type determined in the
     50 *                                   COLOR_UNIT_PREF constant (above).
     51 *
     52 *   Valid values for COLOR_UNIT_PREF are contained in CssColor.COLORUNIT.
     53 */
     54 class CssColor {
     55  /**
     56   * @param  {string} colorValue: Any valid color string
     57   */
     58  constructor(colorValue) {
     59    // Store a lower-cased version of the color to help with format
     60    // testing.  The original text is kept as well so it can be
     61    // returned when needed.
     62    this.#lowerCased = colorValue.toLowerCase();
     63    this.#authored = colorValue;
     64    this.specialValue = SPECIALVALUES.has(this.#lowerCased)
     65      ? this.#authored
     66      : null;
     67  }
     68 
     69  /**
     70   * Values used in COLOR_UNIT_PREF
     71   */
     72  static COLORUNIT = {
     73    authored: "authored",
     74    hex: "hex",
     75    name: "name",
     76    rgb: "rgb",
     77    hsl: "hsl",
     78    hwb: "hwb",
     79  };
     80 
     81  // The value as-authored.
     82  #authored = null;
     83  #currentFormat;
     84  // A lower-cased copy of |authored|.
     85  #lowerCased = null;
     86 
     87  get hasAlpha() {
     88    if (!this.valid) {
     89      return false;
     90    }
     91    return this.getRGBATuple().a !== 1;
     92  }
     93 
     94  /**
     95   * Return true if the color is a valid color and we can get rgba tuples from it.
     96   */
     97  get valid() {
     98    // We can't use InspectorUtils.isValidCSSColor as colors can be valid but we can't have
     99    // their rgba tuples (e.g. currentColor, accentColor, … whose actual values depends on
    100    // additional context we don't have here).
    101    return InspectorUtils.colorToRGBA(this.#authored) !== null;
    102  }
    103 
    104  /**
    105   * Not a real color type but used to preserve accuracy when converting between
    106   * e.g. 8 character hex -> rgba -> 8 character hex (hex alpha values are
    107   * 0 - 255 but rgba alpha values are only 0.0 to 1.0).
    108   */
    109  get highResTuple() {
    110    const type = classifyColor(this.#authored);
    111 
    112    if (type === CssColor.COLORUNIT.hex) {
    113      return hexToRGBA(this.#authored.substring(1), true);
    114    }
    115 
    116    // If we reach this point then the alpha value must be in the range
    117    // 0.0 - 1.0 so we need to multiply it by 255.
    118    const tuple = InspectorUtils.colorToRGBA(this.#authored);
    119    tuple.a *= 255;
    120    return tuple;
    121  }
    122 
    123  /**
    124   * Return true for all transparent values e.g. rgba(0, 0, 0, 0).
    125   */
    126  get transparent() {
    127    try {
    128      const tuple = this.getRGBATuple();
    129      return !(tuple.r || tuple.g || tuple.b || tuple.a);
    130    } catch (e) {
    131      return false;
    132    }
    133  }
    134 
    135  get name() {
    136    const invalidOrSpecialValue = this.#getInvalidOrSpecialValue();
    137    if (invalidOrSpecialValue !== false) {
    138      return invalidOrSpecialValue;
    139    }
    140 
    141    const tuple = this.getRGBATuple();
    142 
    143    if (tuple.a !== 1) {
    144      return this.hex;
    145    }
    146    const { r, g, b } = tuple;
    147    return InspectorUtils.rgbToColorName(r, g, b) || this.hex;
    148  }
    149 
    150  get hex() {
    151    const invalidOrSpecialValue = this.#getInvalidOrSpecialValue();
    152    if (invalidOrSpecialValue !== false) {
    153      return invalidOrSpecialValue;
    154    }
    155    if (this.hasAlpha) {
    156      return this.alphaHex;
    157    }
    158 
    159    let hex = this.longHex;
    160    if (
    161      hex.charAt(1) == hex.charAt(2) &&
    162      hex.charAt(3) == hex.charAt(4) &&
    163      hex.charAt(5) == hex.charAt(6)
    164    ) {
    165      hex = "#" + hex.charAt(1) + hex.charAt(3) + hex.charAt(5);
    166    }
    167    return hex;
    168  }
    169 
    170  get alphaHex() {
    171    const invalidOrSpecialValue = this.#getInvalidOrSpecialValue();
    172    if (invalidOrSpecialValue !== false) {
    173      return invalidOrSpecialValue;
    174    }
    175 
    176    let alphaHex = this.longAlphaHex;
    177    if (
    178      alphaHex.charAt(1) == alphaHex.charAt(2) &&
    179      alphaHex.charAt(3) == alphaHex.charAt(4) &&
    180      alphaHex.charAt(5) == alphaHex.charAt(6) &&
    181      alphaHex.charAt(7) == alphaHex.charAt(8)
    182    ) {
    183      alphaHex =
    184        "#" +
    185        alphaHex.charAt(1) +
    186        alphaHex.charAt(3) +
    187        alphaHex.charAt(5) +
    188        alphaHex.charAt(7);
    189    }
    190    return alphaHex;
    191  }
    192 
    193  get longHex() {
    194    const invalidOrSpecialValue = this.#getInvalidOrSpecialValue();
    195    if (invalidOrSpecialValue !== false) {
    196      return invalidOrSpecialValue;
    197    }
    198    if (this.hasAlpha) {
    199      return this.longAlphaHex;
    200    }
    201 
    202    const tuple = this.getRGBATuple();
    203    return (
    204      "#" +
    205      ((1 << 24) + (tuple.r << 16) + (tuple.g << 8) + (tuple.b << 0))
    206        .toString(16)
    207        .substr(-6)
    208    );
    209  }
    210 
    211  get longAlphaHex() {
    212    const invalidOrSpecialValue = this.#getInvalidOrSpecialValue();
    213    if (invalidOrSpecialValue !== false) {
    214      return invalidOrSpecialValue;
    215    }
    216 
    217    const tuple = this.highResTuple;
    218 
    219    return (
    220      "#" +
    221      ((1 << 24) + (tuple.r << 16) + (tuple.g << 8) + (tuple.b << 0))
    222        .toString(16)
    223        .substr(-6) +
    224      Math.round(tuple.a).toString(16).padStart(2, "0")
    225    );
    226  }
    227 
    228  get rgb() {
    229    const invalidOrSpecialValue = this.#getInvalidOrSpecialValue();
    230    if (invalidOrSpecialValue !== false) {
    231      return invalidOrSpecialValue;
    232    }
    233    if (!this.hasAlpha) {
    234      if (this.#lowerCased.startsWith("rgb(")) {
    235        // The color is valid and begins with rgb(.
    236        return this.#authored;
    237      }
    238      const tuple = this.getRGBATuple();
    239      return "rgb(" + tuple.r + ", " + tuple.g + ", " + tuple.b + ")";
    240    }
    241    return this.rgba;
    242  }
    243 
    244  get rgba() {
    245    const invalidOrSpecialValue = this.#getInvalidOrSpecialValue();
    246    if (invalidOrSpecialValue !== false) {
    247      return invalidOrSpecialValue;
    248    }
    249    if (this.#lowerCased.startsWith("rgba(")) {
    250      // The color is valid and begins with rgba(.
    251      return this.#authored;
    252    }
    253    const components = this.getRGBATuple();
    254    return (
    255      "rgba(" +
    256      components.r +
    257      ", " +
    258      components.g +
    259      ", " +
    260      components.b +
    261      ", " +
    262      components.a +
    263      ")"
    264    );
    265  }
    266 
    267  get hsl() {
    268    const invalidOrSpecialValue = this.#getInvalidOrSpecialValue();
    269    if (invalidOrSpecialValue !== false) {
    270      return invalidOrSpecialValue;
    271    }
    272    if (this.#lowerCased.startsWith("hsl(")) {
    273      // The color is valid and begins with hsl(.
    274      return this.#authored;
    275    }
    276    if (this.hasAlpha) {
    277      return this.hsla;
    278    }
    279    return this.#hsl();
    280  }
    281 
    282  get hsla() {
    283    const invalidOrSpecialValue = this.#getInvalidOrSpecialValue();
    284    if (invalidOrSpecialValue !== false) {
    285      return invalidOrSpecialValue;
    286    }
    287    if (this.#lowerCased.startsWith("hsla(")) {
    288      // The color is valid and begins with hsla(.
    289      return this.#authored;
    290    }
    291    if (this.hasAlpha) {
    292      const a = this.getRGBATuple().a;
    293      return this.#hsl(a);
    294    }
    295    return this.#hsl(1);
    296  }
    297 
    298  get hwb() {
    299    const invalidOrSpecialValue = this.#getInvalidOrSpecialValue();
    300    if (invalidOrSpecialValue !== false) {
    301      return invalidOrSpecialValue;
    302    }
    303    if (this.#lowerCased.startsWith("hwb(")) {
    304      // The color is valid and begins with hwb(.
    305      return this.#authored;
    306    }
    307    if (this.hasAlpha) {
    308      const a = this.getRGBATuple().a;
    309      return this.#hwb(a);
    310    }
    311    return this.#hwb();
    312  }
    313 
    314  /**
    315   * Check whether the current color value is in the special list e.g.
    316   * transparent or invalid.
    317   *
    318   * @return {string | boolean}
    319   *         - If the current color is a special value e.g. "transparent" then
    320   *           return the color.
    321   *         - If the current color is a system value e.g. "accentcolor" then
    322   *           return the color.
    323   *         - If the color is invalid or that we can't get rgba components from it
    324   *           (e.g. "accentcolor"), return an empty string.
    325   *         - If the color is a regular color e.g. #F06 so we return false
    326   *           to indicate that the color is neither invalid or special.
    327   */
    328  #getInvalidOrSpecialValue() {
    329    if (this.specialValue) {
    330      return this.specialValue;
    331    }
    332    if (!this.valid) {
    333      return "";
    334    }
    335    return false;
    336  }
    337 
    338  nextColorUnit() {
    339    // Reorder the formats array to have the current format at the
    340    // front so we can cycle through.
    341    // Put "name" at the end as that provides a hex value if there's
    342    // no name for the color.
    343    let formats = ["hex", "hsl", "rgb", "hwb", "name"];
    344 
    345    let currentFormat = this.#currentFormat;
    346    // If we don't have determined the current format yet
    347    if (!currentFormat) {
    348      // If the pref value is COLORUNIT.authored, get the actual unit from the authored color,
    349      // otherwise use the pref value.
    350      const defaultFormat = Services.prefs.getCharPref(COLOR_UNIT_PREF);
    351      currentFormat =
    352        defaultFormat === CssColor.COLORUNIT.authored
    353          ? classifyColor(this.#authored)
    354          : defaultFormat;
    355    }
    356    const putOnEnd = formats.splice(0, formats.indexOf(currentFormat));
    357    formats = [...formats, ...putOnEnd];
    358 
    359    const currentDisplayedColor = this[formats[0]];
    360 
    361    let colorUnit;
    362    for (const format of formats) {
    363      if (this[format].toLowerCase() !== currentDisplayedColor.toLowerCase()) {
    364        colorUnit = CssColor.COLORUNIT[format];
    365        break;
    366      }
    367    }
    368 
    369    this.#currentFormat = colorUnit;
    370    return this.toString(colorUnit);
    371  }
    372 
    373  /**
    374   * Return a string representing a color of type defined in COLOR_UNIT_PREF.
    375   */
    376  toString(colorUnit, forceUppercase) {
    377    let color;
    378 
    379    switch (colorUnit) {
    380      case CssColor.COLORUNIT.authored:
    381        color = this.#authored;
    382        break;
    383      case CssColor.COLORUNIT.hex:
    384        color = this.hex;
    385        break;
    386      case CssColor.COLORUNIT.hsl:
    387        color = this.hsl;
    388        break;
    389      case CssColor.COLORUNIT.name:
    390        color = this.name;
    391        break;
    392      case CssColor.COLORUNIT.rgb:
    393        color = this.rgb;
    394        break;
    395      case CssColor.COLORUNIT.hwb:
    396        color = this.hwb;
    397        break;
    398      default:
    399        color = this.rgb;
    400    }
    401 
    402    if (
    403      forceUppercase ||
    404      (colorUnit != CssColor.COLORUNIT.authored &&
    405        colorIsUppercase(this.#authored))
    406    ) {
    407      color = color.toUpperCase();
    408    }
    409 
    410    return color;
    411  }
    412 
    413  /**
    414   * Returns a RGBA 4-Tuple representation of a color or transparent as
    415   * appropriate.
    416   */
    417  getRGBATuple() {
    418    const tuple = InspectorUtils.colorToRGBA(this.#authored);
    419 
    420    tuple.a = parseFloat(tuple.a.toFixed(2));
    421 
    422    return tuple;
    423  }
    424 
    425  #hsl(maybeAlpha) {
    426    if (this.#lowerCased.startsWith("hsl(") && maybeAlpha === undefined) {
    427      // We can use it as-is.
    428      return this.#authored;
    429    }
    430 
    431    const { r, g, b } = this.getRGBATuple();
    432    const [h, s, l] = rgbToHsl([r, g, b]);
    433    if (maybeAlpha !== undefined) {
    434      return "hsla(" + h + ", " + s + "%, " + l + "%, " + maybeAlpha + ")";
    435    }
    436    return "hsl(" + h + ", " + s + "%, " + l + "%)";
    437  }
    438 
    439  #hwb(maybeAlpha) {
    440    if (this.#lowerCased.startsWith("hwb(") && maybeAlpha === undefined) {
    441      // We can use it as-is.
    442      return this.#authored;
    443    }
    444 
    445    const { r, g, b } = this.getRGBATuple();
    446    const [hue, white, black] = rgbToHwb([r, g, b]);
    447    return `hwb(${hue} ${white}% ${black}%${
    448      maybeAlpha !== undefined ? " / " + maybeAlpha : ""
    449    })`;
    450  }
    451 
    452  /**
    453   * This method allows comparison of CssColor objects using ===.
    454   */
    455  valueOf() {
    456    return this.rgba;
    457  }
    458 
    459  /**
    460   * Check whether the color is fully transparent (alpha === 0).
    461   *
    462   * @return {boolean} True if the color is transparent and valid.
    463   */
    464  isTransparent() {
    465    return this.getRGBATuple().a === 0;
    466  }
    467 }
    468 
    469 /**
    470 * Convert rgb value to hsl
    471 *
    472 * @param {Array} rgb
    473 *         Array of rgb values
    474 * @return {Array}
    475 *         Array of hsl values.
    476 */
    477 function rgbToHsl([r, g, b]) {
    478  r = r / 255;
    479  g = g / 255;
    480  b = b / 255;
    481 
    482  const max = Math.max(r, g, b);
    483  const min = Math.min(r, g, b);
    484  let h;
    485  let s;
    486  const l = (max + min) / 2;
    487 
    488  if (max == min) {
    489    h = s = 0;
    490  } else {
    491    const d = max - min;
    492    s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
    493 
    494    switch (max) {
    495      case r:
    496        h = ((g - b) / d) % 6;
    497        break;
    498      case g:
    499        h = (b - r) / d + 2;
    500        break;
    501      case b:
    502        h = (r - g) / d + 4;
    503        break;
    504    }
    505    h *= 60;
    506    if (h < 0) {
    507      h += 360;
    508    }
    509  }
    510 
    511  return [roundTo(h, 1), roundTo(s * 100, 1), roundTo(l * 100, 1)];
    512 }
    513 
    514 /**
    515 * Convert RGB value to HWB
    516 *
    517 * @param {Array} rgb
    518 *         Array of RGB values
    519 * @return {Array}
    520 *         Array of HWB values.
    521 */
    522 function rgbToHwb([r, g, b]) {
    523  const hsl = rgbToHsl([r, g, b]);
    524 
    525  r = r / 255;
    526  g = g / 255;
    527  b = b / 255;
    528 
    529  const white = Math.min(r, g, b);
    530  const black = 1 - Math.max(r, g, b);
    531  return [roundTo(hsl[0], 1), roundTo(white * 100, 1), roundTo(black * 100, 1)];
    532 }
    533 
    534 function roundTo(number, digits) {
    535  const multiplier = Math.pow(10, digits);
    536  return Math.round(number * multiplier) / multiplier;
    537 }
    538 
    539 /**
    540 * Given a color, classify its type as one of the possible color
    541 * units, as known by |CssColor.COLORUNIT|.
    542 *
    543 * @param  {string} value
    544 *         The color, in any form accepted by CSS.
    545 * @return {string}
    546 *         The color classification, one of "rgb", "hsl", "hwb",
    547 *         "hex", "name", or if no format is recognized, "authored".
    548 */
    549 function classifyColor(value) {
    550  value = value.toLowerCase();
    551  if (value.startsWith("rgb(") || value.startsWith("rgba(")) {
    552    return CssColor.COLORUNIT.rgb;
    553  } else if (value.startsWith("hsl(") || value.startsWith("hsla(")) {
    554    return CssColor.COLORUNIT.hsl;
    555  } else if (value.startsWith("hwb(")) {
    556    return CssColor.COLORUNIT.hwb;
    557  } else if (/^#[0-9a-f]+$/.exec(value)) {
    558    return CssColor.COLORUNIT.hex;
    559  } else if (/^[a-z\-]+$/.exec(value)) {
    560    return CssColor.COLORUNIT.name;
    561  }
    562  return CssColor.COLORUNIT.authored;
    563 }
    564 
    565 /**
    566 * A helper function to convert a hex string like "F0C" or "F0C8" to a color.
    567 *
    568 * @param {string} name the color string
    569 * @param {boolean} highResolution Forces returned alpha value to be in the
    570 *                  range 0 - 255 as opposed to 0.0 - 1.0.
    571 * @return {object} an object of the form {r, g, b, a}; or null if the
    572 *         name was not a valid color
    573 */
    574 function hexToRGBA(name, highResolution) {
    575  let r,
    576    g,
    577    b,
    578    a = 1;
    579 
    580  if (name.length === 3) {
    581    // short hex string (e.g. F0C)
    582    r = parseInt(name.charAt(0) + name.charAt(0), 16);
    583    g = parseInt(name.charAt(1) + name.charAt(1), 16);
    584    b = parseInt(name.charAt(2) + name.charAt(2), 16);
    585  } else if (name.length === 4) {
    586    // short alpha hex string (e.g. F0CA)
    587    r = parseInt(name.charAt(0) + name.charAt(0), 16);
    588    g = parseInt(name.charAt(1) + name.charAt(1), 16);
    589    b = parseInt(name.charAt(2) + name.charAt(2), 16);
    590    a = parseInt(name.charAt(3) + name.charAt(3), 16);
    591 
    592    if (!highResolution) {
    593      a /= 255;
    594    }
    595  } else if (name.length === 6) {
    596    // hex string (e.g. FD01CD)
    597    r = parseInt(name.charAt(0) + name.charAt(1), 16);
    598    g = parseInt(name.charAt(2) + name.charAt(3), 16);
    599    b = parseInt(name.charAt(4) + name.charAt(5), 16);
    600  } else if (name.length === 8) {
    601    // alpha hex string (e.g. FD01CDAB)
    602    r = parseInt(name.charAt(0) + name.charAt(1), 16);
    603    g = parseInt(name.charAt(2) + name.charAt(3), 16);
    604    b = parseInt(name.charAt(4) + name.charAt(5), 16);
    605    a = parseInt(name.charAt(6) + name.charAt(7), 16);
    606 
    607    if (!highResolution) {
    608      a /= 255;
    609    }
    610  } else {
    611    return null;
    612  }
    613  if (!highResolution) {
    614    a = Math.round(a * 10) / 10;
    615  }
    616  return { r, g, b, a };
    617 }
    618 
    619 /**
    620 * Blend background and foreground colors takign alpha into account.
    621 *
    622 * @param  {Array} foregroundColor
    623 *         An array with [r,g,b,a] values containing the foreground color.
    624 * @param  {Array} backgroundColor
    625 *         An array with [r,g,b,a] values containing the background color. Defaults to
    626 *         [ 255, 255, 255, 1 ].
    627 * @return {Array}
    628 *         An array with combined [r,g,b,a] colors.
    629 */
    630 function blendColors(foregroundColor, backgroundColor = [255, 255, 255, 1]) {
    631  const [fgR, fgG, fgB, fgA] = foregroundColor;
    632  const [bgR, bgG, bgB, bgA] = backgroundColor;
    633  if (fgA === 1) {
    634    return foregroundColor;
    635  }
    636 
    637  return [
    638    (1 - fgA) * bgR + fgA * fgR,
    639    (1 - fgA) * bgG + fgA * fgG,
    640    (1 - fgA) * bgB + fgA * fgB,
    641    fgA + bgA * (1 - fgA),
    642  ];
    643 }
    644 
    645 /**
    646 * TODO: Replace with RelativeLuminanceUtils::ContrastRatio, see bug 1984999.
    647 *
    648 * Calculates the contrast ratio of 2 rgba tuples based on the formula in
    649 * https://www.w3.org/TR/2008/REC-WCAG20-20081211/#visual-audio-contrast7
    650 *
    651 * @param {Array} backgroundColor An array with [r,g,b,a] values containing
    652 * the background color.
    653 * @param {Array} textColor An array with [r,g,b,a] values containing
    654 * the text color.
    655 * @return {number} The calculated luminance.
    656 */
    657 function calculateContrastRatio(backgroundColor, textColor) {
    658  // Do not modify given colors.
    659  backgroundColor = Array.from(backgroundColor);
    660  textColor = Array.from(textColor);
    661 
    662  backgroundColor = blendColors(backgroundColor);
    663  textColor = blendColors(textColor, backgroundColor);
    664 
    665  const backgroundLuminance = InspectorUtils.relativeLuminance(
    666    ...backgroundColor.map(c => c / 255)
    667  );
    668  const textLuminance = InspectorUtils.relativeLuminance(
    669    ...textColor.map(c => c / 255)
    670  );
    671  const ratio = (textLuminance + 0.05) / (backgroundLuminance + 0.05);
    672 
    673  return ratio > 1.0 ? ratio : 1 / ratio;
    674 }
    675 
    676 function colorIsUppercase(color) {
    677  // Specifically exclude the case where the color is
    678  // case-insensitive.  This makes it so that "#000" isn't
    679  // considered "upper case" for the purposes of color cycling.
    680  return color === color.toUpperCase() && color !== color.toLowerCase();
    681 }
    682 
    683 module.exports.colorUtils = {
    684  CssColor,
    685  rgbToHsl,
    686  rgbToHwb,
    687  classifyColor,
    688  calculateContrastRatio,
    689  blendColors,
    690  colorIsUppercase,
    691 };