Home Reference Source

lib/color-contrast-calc.js

"use strict";

/** @private */
const ColorUtils = require("./color-utils").ColorUtils;
/** @private */
const Utils = ColorUtils;
/** @private */
const Color = require("./color").Color;
/** @private */
const Checker = require("./contrast-checker").ContrastChecker;

/**
 * Provides the top-level name space of this library.
 */
class ColorContrastCalc {
  /**
   * Returns an instance of Color.
   *
   * As colorValue, you can pass a predefined color name, or an RGB
   * value represented as an array of Integers or a hex code such as
   * [255, 255, 255] or "#ffff00". name is assigned to the returned
   * instance if it does not have a name already assigned.
   * @param {string|Array<number, number, number>} colorValue - name
   *     of a predefined color or RGB value
   * @param {string} name - Unless the instance has predefined name,
   *     the name passed to the method is set to self.name
   * @returns {Color} Instance of Color
   */
  static colorFrom(colorValue, name = null) {
    const errMessage = "A color should be given as an array or string.";

    if (! (Utils.isString(colorValue)) && ! (colorValue instanceof Array)) {
      throw new Error(errMessage);
    }

    if (colorValue instanceof Array) {
      return this.colorFromRgb(colorValue, name);
    }

    return this.colorFromStr(colorValue, name);
  }

  /**
   * @private
   */
  static colorFromRgb(colorValue, name = null) {
    const errMessage = "An RGB value should be given in form of [r, g, b].";

    if (! Utils.isValidRgb(colorValue)) {
      throw new Error(errMessage);
    }

    const hexCode = Utils.rgbToHexCode(colorValue);
    return Color.List.HEX_TO_COLOR.get(hexCode) || new Color(hexCode, name);
  }

  /**
   * @private
   */
  static colorFromStr(colorValue, name = null) {
    const errMessage = "A hex code is in form of '#xxxxxx' where 0 <= x <= f.";

    const namedColor = Color.getByName(colorValue);

    if (namedColor) {
      return namedColor;
    }

    if (! Utils.isValidHexCode(colorValue)) {
      throw new Error(errMessage);
    }

    const hexCode = Utils.normalizeHexCode(colorValue);
    return Color.List.HEX_TO_COLOR.get(hexCode) || new Color(hexCode, name);
  }

  /**
   * Returns an array of named colors that satisfy a given level of
   * contrast ratio
   * @param {Color} color - base color to which other colors are compared
   * @param {string} [level="AA"] - A, AA or AAA
   * @returns {Color[]}
   */
  static colorsWithSufficientContrast(color, level = "AA") {
    const ratio = Checker.levelToRatio(level);

    return this.NAMED_COLORS.filter(combinedColor => {
      return color.contrastRatioAgainst(combinedColor) >= ratio;
    });
  }

  /**
   * Returns an array of colors which share the same saturation and lightness.
   * By default, so-called pure colors are returned.
   * @param {number} [s=100] - Ratio of saturation in percentage.
   * @param {number} [l=50] - Ratio of lightness in percentage.
   * @param {number} [h_interval=1] - Interval of hues given in degrees.
   *     By default, it returns 360 hues beginning from red.
   *     (Red is included twice, because it corresponds to 0 and 360 degrees.)
   * @returns {Color[]}
   */
  static hslColors(s = 100, l = 50, h_interval = 1) {
    return Color.List.hslColors(s, l, h_interval);
  }

  /**
   * Returns a function to be used as a parameter of Array.prototype.sort()
   * @param {string} [colorOrder="rgb"] - A left side primary color has a higher
   *     sorting precedence
   * @param {string} [keyType="color"] - Type of keys used for sorting:
   *     "color", "hex" or "rgb"
   * @param {function} [keyMapper=null] - A function used to retrive key values
   *     from elements to be sorted
   * @returns {function} Function that compares given two colors
   */
  static compareFunction(colorOrder = "rgb", keyType = "color", keyMapper = null) {
    return this.Sorter.compareFunction(colorOrder, keyType, keyMapper);
  }

  /**
   * Sorts colors in an array and returns the result as a new array
   * @param {Color[]|String[]} colors - List of colors
   * @param {string} [colorOrder="rgb"] - A left side primary color has a higher
   *     sorting precedence, and an uppercase letter means descending order
   * @param {function} [keyMapper=null] - A function used to retrive key values
   *     from elements to be sorted
   * @param {string} [mode="auto"] - If set to "hex", key values are handled as
   *     hex code strings
   * @returns {Color[]} An array of sorted colors
   */
  static sort(colors, colorOrder = "rgb", keyMapper = null, mode = "auto") {
    return this.Sorter.sort(colors, colorOrder, keyMapper, mode);
  }

  /**
   * @private
   */
  static setup() {
    /**
     * Array of named colors defined at
     * https://www.w3.org/TR/SVG/types.html#ColorKeywords
     * @property {Color[]} NAMED_COLORS
     */
    this.NAMED_COLORS = Color.List.NAMED_COLORS;
    /** @private */
    this.NAME_TO_COLOR = Color.List.NAME_TO_COLOR;
    /** @private */
    this.HEX_TO_COLOR = Color.List.HEX_TO_COLOR;
    /**
     * Array of web safe colors
     * @property {Color[]} WEB_SAFE_COLORS
     */
    this.WEB_SAFE_COLORS = Color.List.WEB_SAFE_COLORS;
    Object.freeze(this);
  }
}

Color.calc = ColorContrastCalc;

(function() {
  class Sorter {
    static sort(colors, colorOrder = "rgb", keyMapper = null, mode = "auto") {
      const keyType = this.guessKeyType(mode, colors[0], keyMapper);
      const compare = this.compareFunction(colorOrder, keyType, keyMapper);

      return colors.slice().sort(compare);
    }

    static compareFunction(colorOrder = "rgb",
                           keyType = this.KEY_TYPE.COLOR,
                           keyMapper = null) {
      let compare = null;

      if (keyType === this.KEY_TYPE.HEX) {
        compare = this.compareHexFunction(colorOrder);
      } else if (this.isComponentType(keyType)) {
        compare = this.compareComponentsFunction(colorOrder);
      } else {
        compare = this.compareColorFunction(colorOrder);
      }

      return this.composeFunction(compare, keyMapper);
    }

    static composeFunction(compareFunc, keyMapper = null) {
      if (! keyMapper) {
        return compareFunc;
      }

      return function(color1, color2) {
        return compareFunc(keyMapper(color1), keyMapper(color2));
      };
    }

    static guessKeyType(mode, color, keyMapper) {
      if (mode === this.KEY_TYPE.HEX ||
          mode === "auto" && this.isStringKey(color, keyMapper)) {
        return this.KEY_TYPE.HEX;
      } else if (this.isComponentType(mode) || Array.isArray(color)) {
        return this.KEY_TYPE.COMPONENTS;
      } else {
        return this.KEY_TYPE.COLOR;
      }
    }

    static isComponentType(keyType) {
      return [
        this.KEY_TYPE.RGB,
        this.KEY_TYPE.HSL,
        this.KEY_TYPE.COMPONENTS
      ].includes(keyType);
    }

    static isStringKey(color, keyMapper) {
      const keyType = keyMapper ? keyMapper(color) : color;
      return Utils.isString(keyType);
    }

    static compareColorFunction(colorOrder = "rgb") {
      const order = this.parseColorOrder(colorOrder);
      const type = order.type;

      return function(color1, color2) {
        return Sorter.compareColorComponents(color1[type], color2[type], order);
      };
    }

    static compareComponentsFunction(colorOrder = "rgb") {
      const order = this.parseColorOrder(colorOrder);

      return function(rgb1, rgb2) {
        return Sorter.compareColorComponents(rgb1, rgb2, order);
      };
    }

    static compareHexFunction(colorOrder = "rgb") {
      const order = this.parseColorOrder(colorOrder);
      const componentsCache = new Map();

      return function(hex1, hex2) {
        const color1 = Sorter.hexToComponents(hex1, order, componentsCache);
        const color2 = Sorter.hexToComponents(hex2, order, componentsCache);

        return Sorter.compareColorComponents(color1, color2, order);
      };
    }

    static compareColorComponents(color1, color2,
                                  order = this.parseColorOrder("rgb")) {
      for (let i of order.pos) {
        const result = order.funcs[i](color1[i], color2[i]);
        if (result !== 0) { return result; }
      }

      return 0;
    }

    static hexToComponents(hex, order, cache) {
      const cachedComponents = cache.get(hex);
      if (cachedComponents) { return cachedComponents; }

      const components = order.toComponents(hex);
      cache.set(hex, components);

      return components;
    }

    static rgbComponentPos(colorOrder) {
      return colorOrder.toLowerCase().split("").map((primary) => {
        return this.RGB_IDENTIFIERS.indexOf(primary);
      });
    }

    static hslComponentPos(hslOrder) {
      return hslOrder.toLowerCase().split("").map(component => {
        return this.HSL_IDENTIFIERS.indexOf(component);
      });
    }

    static ascendComp(component1, component2) {
      return component1 - component2;
    }

    static descendComp(component1, component2) {
      return component2 - component1;
    }

    static chooseRgbCompFunc(colorOrder) {
      const primaryColors = colorOrder.split("")
            .sort(this.caseInsensitiveComp).reverse();

      return primaryColors.map(primary => {
        if (Utils.isUpperCase(primary)) {
          return this.descendComp;
        }

        return this.ascendComp;
      });
    }

    static chooseHslCompFunc(hslOrder) {
      return this.HSL_RES.map(re => {
        const pos = hslOrder.search(re);
        if (Utils.isUpperCase(hslOrder[pos])) {
          return this.descendComp;
        }

        return this.ascendComp;
      });
    }

    static parseColorOrder(colorOrder) {
      if (/[rgb]{3}/i.test(colorOrder)) {
        return {
          pos: this.rgbComponentPos(colorOrder),
          funcs: this.chooseRgbCompFunc(colorOrder),
          toComponents: hexCode => Utils.hexCodeToDecimal(hexCode),
          type: "rgb"
        };
      } else {
        return {
          pos: this.hslComponentPos(colorOrder),
          funcs: this.chooseHslCompFunc(colorOrder),
          toComponents: hexCode => Utils.hexCodeToHsl(hexCode),
          type: "hsl"
        };
      }
    }

    static caseInsensitiveComp(str1, str2) {
      const lStr1 = str1.toLowerCase();
      const lStr2 = str2.toLowerCase();

      if (lStr1 < lStr2) { return -1; }
      if (lStr1 > lStr2) { return 1; }
      return 0;
    }

    static setup() {
      this.RGB_IDENTIFIERS = ["r", "g", "b"];
      this.HSL_IDENTIFIERS = ["h", "s", "l"];
      this.HSL_RES = [/h/i, /s/i, /l/i];
      this.defaultCompFuncs = [
        Sorter.ascendComp,
        Sorter.ascendComp,
        Sorter.ascendComp
      ];
      this.KEY_TYPE = {
        COMPONENTS: "components",
        RGB: "rgb",
        HSL: "hsl",
        HEX: "hex",
        COLOR: "color"
      };
    }
  }

  Sorter.setup();

  ColorContrastCalc.Sorter = Sorter;
})();

ColorContrastCalc.setup();

module.exports.ColorUtils = ColorUtils;
module.exports.ContrastChecker = Checker;
module.exports.ColorContrastCalc = ColorContrastCalc;
module.exports.Color = Color;