lib/color-utils.js
"use strict";
/**
* Collection of functions that provide basic operations on colors
* represented as RGB/HSL value (given in the form of array of numbers)
* or hex code (given in the form of string)
*/
class ColorUtils {
/**
* Converts a hex color code string to a decimal representation
* @param {string} hexCode - Hex color code such as "#ffff00"
* @returns {Array<number, number, number>} RGB value represented as
* an array of numbers
*/
static hexCodeToRgb(hexCode) {
const h = this.normalizeHexCode(hexCode, false);
return [0, 2, 4].map(s => h.substr(s, 2))
.map(primaryColor => Number.parseInt(primaryColor, 16));
}
/**
* Converts a hex color code to a 6-digit hexadecimal string
* @param {string} hexString - String that represent a hex code
* @param {boolean} [prefix=true] - Append '#' to the head of return value
* if a truthy value is given
* @returns {string} 6-digit hexadecimal string with/without leading '#'
*/
static normalizeHexCode(hexString, prefix = true) {
const hl = hexString.toLowerCase();
const h = hl.startsWith("#") ? hl.replace("#", "") : hl;
let hexPart = h;
if (h.length === 3) {
hexPart = [0, 1, 2].map(s => h.substr(s, 1).repeat(2)).join("");
}
return prefix ? `#${hexPart}` : hexPart;
}
/**
* Converts a decimal representation of color to a hex code string
* @param {Array<number, number, number>} rgb - RGB value represented as
* an array of numbers
* @returns {string} RGB value in hex code
*/
static rgbToHexCode(rgb) {
return "#" + rgb.map(d => {
const h = d.toString(16);
return h.length === 1 ? "0" + h : h;
}).join("");
}
/**
* Converts HSL value to RGB value
* @param {Array<number, number, number>} hsl - An array of numbers that
* represents HSL value
* @returns {Array<number, number, number>} An array of numbers that
* represents RGB value
*/
static hslToRgb(hsl) {
/*
https://www.w3.org/TR/css3-color/#hsl-color
*/
const h = hsl[0] / 360;
const s = hsl[1] / 100;
const l = hsl[2] / 100;
const m2 = l <= 0.5 ? l * (s + 1) : l + s - l * s;
const m1 = l * 2 - m2;
const r = this.hueToRgb(m1, m2, h + 1 / 3) * 255;
const g = this.hueToRgb(m1, m2, h) * 255;
const b = this.hueToRgb(m1, m2, h - 1 / 3) * 255;
return [r, g, b].map(c => Math.round(c));
}
/**
* @private
*/
static hueToRgb(m1, m2, hInit) {
let h = hInit;
if (h < 0) { h = h + 1; }
if (h > 1) { h = h - 1; }
if (h * 6 < 1) { return m1 + (m2 - m1) * h * 6; }
if (h * 2 < 1) { return m2; }
if (h * 3 < 2) { return m1 + (m2 - m1) * (2 / 3 - h) * 6; }
return m1;
}
/**
* Converts HSL value to hex code
* @param {Array<number, number, number>} hsl - An array of numbers that
* represents HSL value
* @returns {string} Hex code
*/
static hslToHexCode(hsl) {
return this.rgbToHexCode(this.hslToRgb(hsl));
}
/**
* Converts RGB value to HSL value
* @param {Array<number, number, number>} rgb - An array of numbers that
* represents RGB value
* @returns {Array<number, number, number>} An array of numbers that
* represents HSL value
*/
static rgbToHsl(rgb) {
const l = this.rgbToLightness(rgb) * 100;
const s = this.rgbToSaturation(rgb) * 100;
const h = this.rgbToHue(rgb);
return [h, s, l];
}
/**
* @private
*/
static rgbToLightness(rgb) {
return (Math.max(...rgb) + Math.min(...rgb)) / 510;
}
/**
* @private
*/
static rgbToHue(rgb) {
/**
References:
Agoston, Max K. (2005).
"Computer Graphics and Geometric Modeling: Implementation and Algorithms".
London: Springer
https://accessibility.kde.org/hsl-adjusted.php#hue
*/
const max = Math.max(...rgb);
const min = Math.min(...rgb);
if (max === min) { return 0; } /* you can return whatever you like */
const d = max - min;
const mi = rgb.reduce((m, v, i) => rgb[m] > v ? m : i, 0); /* maxIndex */
const h = mi * 120 + (rgb[(mi + 1) % 3] - rgb[(mi + 2) % 3]) * 60 / d;
return h < 0 ? h + 360 : h;
}
/**
* @private
*/
static rgbToSaturation(rgb) {
const l = this.rgbToLightness(rgb);
const max = Math.max(...rgb);
const min = Math.min(...rgb);
const d = max - min;
if (max === min) { return 0; }
if (l <= 0.5) {
return d / (max + min);
} else {
return d / (510 - max - min);
}
}
/**
* Converts a hex color code string to an HSL representation
* @param {string} hexCode - Hex color code such as "#ffff00"
* @returns {Array<number, number, number>} HSL value represented as
* an array of numbers
*/
static hexCodeToHsl(hexCode) {
return this.rgbToHsl(this.hexCodeToRgb(hexCode));
}
/**
* Decimal rounding with a given precision
* @param {number} number - Number to be rounded off
* @param {number} precision - Number of digits after the decimal point
* @returns {number} returns the rounded number
*/
static decimalRound(number, precision) {
const factor = Math.pow(10, precision);
return Math.round(number * factor) / factor;
}
/**
* Checks if a given array is a valid representation of RGB color.
* @param {Array<number, number, number>} rgb - RGB value represented as
* an array of numbers
* @returns {boolean} true if the argument is a valid RGB color
*/
static isValidRgb(rgb) {
return rgb.length === 3 &&
rgb.every(c => c >= 0 && c <= 255 &&
Number.isInteger(c));
}
/**
* Checks if a given array is a valid representation of HSL color.
* @param {Array<number, number, number>} hsl - HSL value represented as
* an array of numbers
* @returns {boolean} true if the argument is a valid HSL color
*/
static isValidHsl(hsl) {
const upperLimits = [360, 100, 100];
return hsl.length === 3 &&
hsl.every((c, i) => typeof c === "number" &&
c >= 0 && c <= upperLimits[i]);
}
/**
* Checks if a given string is a valid representation of RGB color.
* @param {string} code - RGB value in hex code
* @returns {boolean} returns true if then argument is a valid RGB color
*/
static isValidHexCode(code) {
return this.HEX_CODE_RE.test(code);
}
/**
* Checks if given two hex color codes represent a same color.
* @param {string} hexCode1 - Color given as a hex code,
* such as "#ffff00", "#FFFF00" or "#ff0"
* @param {string} hexCode2 - Color given as a hex code,
* such as "#ffff00", "#FFFF00" or "#ff0"
* @returns {boolean} True if given two colors are same
*/
static isSameHexColor(hexCode1, hexCode2) {
const h1 = this.normalizeHexCode(hexCode1);
const h2 = this.normalizeHexCode(hexCode2);
return h1 === h2;
}
/**
* Checks if given two RGB values represent a same color.
* @param {Array<number, number, number>} rgb1 - Color given as an array
* of numbers, such as [255, 255, 0]
* @param {Array<number, number, number>} rgb2 - Color given as an array
* of numbers, such as [255, 255, 0]
* @returns {boolean} True if given two colors are same
*/
static isSameRgbColor(rgb1, rgb2) {
if (rgb1.length !== rgb2.length) { return false; }
return rgb1.every((primaryColor, i) => primaryColor === rgb2[i]);
}
/**
* Checks if a given object is a string
* @param {object} str - Object to be checked
* @returns {boolean} returns true if the argument is a string
*/
static isString(str) {
return typeof str === "string" || str instanceof String;
}
/**
* Checks if a given string is consists of uppercase letters
* @param {string} str - string to be checked
* @returns {boolean} returns true if letters in the argument string are
* all uppercase
*/
static isUpperCase(str) {
return this.isString(str) && str.toUpperCase() === str;
}
/**
* @private
*/
static setup() {
/** @private */
this.HEX_CODE_RE = /^#?[0-9a-f]{3}([0-9a-f]{3})?$/i;
}
/**
* @private
*/
static clampToRange(value, lowerBound, upperBound) {
if (value <= lowerBound) {
return lowerBound;
} else if (value > upperBound) {
return upperBound;
}
return value;
}
/**
* @private
*/
static rgbMap(values, func = null) {
if (func) {
return values.map(val => {
return ColorUtils.clampToRange(Math.round(func(val)), 0, 255);
});
} else {
return values.map(val => {
return ColorUtils.clampToRange(Math.round(val), 0, 255);
});
}
}
}
/**
* @deprecated Use .rgbToHexCode instead.
*/
ColorUtils.decimalToHexCode = ColorUtils.rgbToHexCode;
/**
* @deprecated use .hexCodeToRgb instead.
*/
ColorUtils.hexCodeToDecimal = ColorUtils.hexCodeToRgb;
(function() {
class Matrix {
constructor(matrix) {
this.matrix = matrix;
}
add(otherMatrix) {
const newMatrix = this.matrix.map((row, i) => {
const otherRow = otherMatrix.matrix[i];
return row.map((s, j) => s + otherRow[j]);
});
return new Matrix(newMatrix);
}
multiply(n) {
if (typeof n === "number") {
return this.multiplyByScalar(n);
} else {
return this.productByVector(n);
}
}
multiplyByScalar(n) {
const newMatrix = this.matrix.map(row => row.map(c => c * n));
return new Matrix(newMatrix);
}
productByVector(vector) {
return this.matrix.map(row => {
return row.reduce((s, c, i) => s += c * vector[i], 0);
});
}
}
ColorUtils.Matrix = Matrix;
const rgbMap = ColorUtils.rgbMap;
class ContrastCalc {
/*
https://www.w3.org/TR/filter-effects/#funcdef-contrast
https://www.w3.org/TR/SVG/filters.html#TransferFunctionElementAttributes
*/
static calcRgb(rgb, ratio = 100) {
return rgbMap(rgb, c => (c * ratio + 255 * (50 - ratio / 2)) / 100);
}
}
ColorUtils.ContrastCalc = ContrastCalc;
class BrightnessCalc {
/*
https://www.w3.org/TR/filter-effects/#funcdef-brightness
https://www.w3.org/TR/SVG/filters.html#TransferFunctionElementAttributes
*/
static calcRgb(rgb, ratio = 100) {
return rgbMap(rgb, c => c * ratio / 100);
}
}
ColorUtils.BrightnessCalc = BrightnessCalc;
class InvertCalc {
/*
https://www.w3.org/TR/filter-effects/#funcdef-invert
https://www.w3.org/TR/filter-effects-1/#invertEquivalent
https://www.w3.org/TR/SVG/filters.html#TransferFunctionElementAttributes
*/
static calcRgb(rgb, ratio) {
return rgb.map(c => {
return Math.round((100 * c - 2 * c * ratio + 255 * ratio) / 100);
});
}
}
ColorUtils.InvertCalc = InvertCalc;
class HueRotateCalc {
/*
https://www.w3.org/TR/filter-effects/#funcdef-hue-rotate
https://www.w3.org/TR/SVG/filters.html#TransferFunctionElementAttributes
*/
static calcRgb(rgb, deg) {
return rgbMap(this.calcRotation(deg).multiply(rgb));
}
static degToRad(deg) {
return Math.PI * deg / 180;
}
static calcRotation(deg) {
const rad = this.degToRad(deg);
const cosPartResult = this.cosPart.multiply(Math.cos(rad));
const sinPartResult = this.sinPart.multiply(Math.sin(rad));
return this.constPart.add(cosPartResult).add(sinPartResult);
}
}
HueRotateCalc.constPart = new Matrix([[0.213, 0.715, 0.072],
[0.213, 0.715, 0.072],
[0.213, 0.715, 0.072]]);
HueRotateCalc.cosPart = new Matrix([[0.787, -0.715, -0.072],
[-0.213, 0.285, -0.072],
[-0.213, -0.715, 0.928]]);
HueRotateCalc.sinPart = new Matrix([[-0.213, -0.715, 0.928],
[0.143, 0.140, -0.283],
[-0.787, 0.715, 0.072]]);
ColorUtils.HueRotateCalc = HueRotateCalc;
class SaturateCalc {
/*
https://www.w3.org/TR/filter-effects/#funcdef-saturate
https://www.w3.org/TR/SVG/filters.html#feColorMatrixElement
*/
static calcRgb(rgb, s) {
return rgbMap(this.calcSaturation(s).multiply(rgb));
}
static calcSaturation(s) {
return this.constPart.add(this.saturatePart.multiply(s / 100));
}
}
SaturateCalc.constPart = HueRotateCalc.constPart;
SaturateCalc.saturatePart = HueRotateCalc.cosPart;
ColorUtils.SaturateCalc = SaturateCalc;
class GrayscaleCalc {
/*
https://www.w3.org/TR/filter-effects/#funcdef-grayscale
https://www.w3.org/TR/filter-effects/#grayscaleEquivalent
https://www.w3.org/TR/SVG/filters.html#feColorMatrixElement
*/
static calcRgb(rgb, s) {
return rgbMap(this.calcGrayscale(s).multiply(rgb));
}
static calcGrayscale(s) {
const r = 1 - Math.min(100, s) / 100;
return this.constPart.add(this.ratioPart.multiply(r));
}
}
GrayscaleCalc.constPart = new Matrix([[0.2126, 0.7152, 0.0722],
[0.2126, 0.7152, 0.0722],
[0.2126, 0.7152, 0.0722]]);
GrayscaleCalc.ratioPart = new Matrix([[0.7874, -0.7152, -0.0722],
[-0.2126, 0.2848, -0.0722],
[-0.2126, -0.7152, 0.9278]]);
ColorUtils.GrayscaleCalc = GrayscaleCalc;
/**
* The RGB value of some colors.
*/
ColorUtils.RGB = {
BLACK: [0, 0, 0],
WHITE: [255, 255, 255]
};
Object.freeze(ColorUtils.RGB);
})();
ColorUtils.setup();
module.exports.ColorUtils = ColorUtils;