Home Reference Source

lib/contrast-checker.js

"use strict";

/** @private */
const Utils = require("./color-utils").ColorUtils;

/**
 * Collection of functions that check properties of given colors
 */
class ContrastChecker {
  /**
   * Calculate the relative luminance of a RGB color given as a string or
   * an array of numbers
   * @param {string|Array<number, number, number>} rgb - RGB value represented
   *     as a string (hex code) or an array of numbers
   * @returns {number} Relative luminance
   */
  static relativeLuminance(rgb = [255, 255, 255]) {
    /*
      https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
    */
    if (Utils.isString(rgb)) { rgb = Utils.hexCodeToRgb(rgb); }

    const [r, g, b] = rgb.map(c => this.tristimulusValue(c));
    return r * 0.2126 + g * 0.7152 + b * 0.0722;
  }

  /**
   * Calculate the contrast ratio of given colors
   * @param {string|Array<number, number, number>} foreground - RGB value
   *     represented as a string (hex code) or an array of numbers
   * @param {string|Array<number, number, number>} background - RGB value
   *     represented as a string (hex code) or an array of numbers
   * @returns {number} Contrast ratio
   */
  static contrastRatio(foreground, background) {
    /*
      https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
    */
    const [l1, l2] = [foreground, background]
          .map(c => this.relativeLuminance(c));
    return this.luminanceToContrastRatio(l1, l2);
  }

  /**
   * Rate a given contrast ratio according to the WCAG 2.0 criteria
   * @param {number} ratio - Contrast ratio
   * @returns {string} A, AA or AAA if the contrast ratio meets the criteria of
   *     WCAG 2.0, otherwise "-"
   */
  static ratioToLevel(ratio) {
    if (ratio >= 7) {
      return "AAA";
    } else if (ratio >= 4.5) {
      return "AA";
    } else if (ratio >= 3) {
      return "A";
    }

    return "-";
  }

  /**
   * Check if the contrast ratio of a given color against black is higher
   * than against white.
   * @param {string|Array<number, number, number>} color - RGB value
   *     represented as a string (hex code) or an array of numbers
   * @returns {boolean} true if the contrast ratio against white is qual to or
   *     less than the ratio against black
   */
  static isLightColor(color) {
    const whiteLuminance = this.LUMINANCE.WHITE;
    const blackLuminance = this.LUMINANCE.BLACK;
    const l = this.relativeLuminance(color);
    const ratioWithWhite = this.luminanceToContrastRatio(whiteLuminance, l);
    const ratioWithBlack = this.luminanceToContrastRatio(blackLuminance, l);
    return ratioWithWhite <= ratioWithBlack;
  }

  /**
   * @private
   */
  static tristimulusValue(primaryColor, base = 255) {
    const s = primaryColor / base;
    if (s <= 0.03928) {
      return s / 12.92;
    } else {
      return Math.pow((s + 0.055) / 1.055, 2.4);
    }
  }

  /**
   * @private
   */
  static luminanceToContrastRatio(luminance1, luminance2) {
    const [l1, l2] = [luminance1, luminance2]
          .sort((f, s) => s - f);
    return (l1 + 0.05) / (l2 + 0.05);
  }

  /**
   * @private
   */
  static levelToRatio(level) {
    if (typeof level === "number" && level >= 1.0 && level <= 21.0) {
      return level;
    }

    if (level === "A") {
      return 3.0;
    } else if (level === "AA") {
      return 4.5;
    } else if (level === "AAA") {
      return 7.0;
    }
  }
}

/**
 * The relative luminance of some colors.
 */
ContrastChecker.LUMINANCE = {
  BLACK: 0.0,
  WHITE: 1.0
};

Object.freeze(ContrastChecker.LUMINANCE);

module.exports.ContrastChecker = ContrastChecker;