UNPKG

better-font-color-contrast

Version:
279 lines (249 loc) 11.1 kB
import { cssNamedColors } from './cssNamedColors' import { CssColor } from './CssNamedColorsType' export enum NumberType { COLOR = 0xff, RGB = 0xffffff, THRESHOLD = 1, } export class FontColorContrast { red = 0 green = 0 blue = 0 #hexColorOrRedOrArray: string | number | number[] #greenOrThreshold?: number #blue?: number #threshold?: number /** * Contrast threshold to control the resulting font color, float values from 0 to 1. Default is 0.5 */ threshold = 0.5 /** * Sets the #params in the instance * @param hexColorOrRedOrArray One of the options: hex color number, hex color string, named CSS color, array with red, green and blue or string or the red portion of the color * @param greenOrThreshold The green portion of the color or the contrast threshold to control the resulting font color * @param blue The blue portion of the color * @param threshold Contrast threshold to control the resulting font color */ constructor (hexColorOrRedOrArray: string | number | number[] | CssColor, greenOrThreshold?: number, blue?: number, threshold?: number) { this.#hexColorOrRedOrArray = hexColorOrRedOrArray this.#greenOrThreshold = greenOrThreshold this.#blue = blue this.#threshold = threshold } /** * Analyses the color (normally used in the background) and retrieves what color (black or white) has a better contrast. * @returns The best contrast between black and white */ getColor () { if (this.isRgb()) { this.setColorsFromRgbNumbers() } else if (this.isHexString()) { this.setColorsFromHexString() } else if (this.isNumber()) { this.setColorsFromNumber() } else if (this.isArray()) { this.setColorsFromArray() } else { return '#ffffff' } return this.contrastFromHSP() } /** * Checks if the color is set as RGB on each param * @returns True if color is set as RGB on each param */ isRgb () { return ( FontColorContrast.isValidNumber(this.#hexColorOrRedOrArray, NumberType.COLOR) && FontColorContrast.isValidNumber(this.#greenOrThreshold, NumberType.COLOR) && FontColorContrast.isValidNumber(this.#blue, NumberType.COLOR) && FontColorContrast.isValidNumber(this.#threshold, NumberType.THRESHOLD) ) } /** * Checks if color is set on the first param as a hex string and removes the hash of it * @returns True if color is a hex string */ isHexString () { const [cleanString, hexNum] = this.getCleanStringAndHexNum() if (FontColorContrast.isValidNumber(hexNum, NumberType.RGB) && FontColorContrast.isValidNumber(this.#greenOrThreshold, NumberType.THRESHOLD) && FontColorContrast.isNotSet(this.#blue) && FontColorContrast.isNotSet(this.#threshold) ) { this.#hexColorOrRedOrArray = cleanString return true } return false } /** * Checks if color is set on the first param as a number * @returns True if color is a valid RGB nunbernumber */ isNumber () { return ( FontColorContrast.isValidNumber(this.#hexColorOrRedOrArray, NumberType.RGB) && FontColorContrast.isValidNumber(this.#greenOrThreshold, NumberType.THRESHOLD) && FontColorContrast.isNotSet(this.#blue) && FontColorContrast.isNotSet(this.#threshold) ) } /** * Checks if color is set as an RGB array * @returns True if color is set as an RGB array */ isArray () { return ( Array.isArray(this.#hexColorOrRedOrArray) && this.#hexColorOrRedOrArray.length === 3 && FontColorContrast.isValidNumber(this.#hexColorOrRedOrArray[0], NumberType.COLOR) && FontColorContrast.isValidNumber(this.#hexColorOrRedOrArray[1], NumberType.COLOR) && FontColorContrast.isValidNumber(this.#hexColorOrRedOrArray[2], NumberType.COLOR) && FontColorContrast.isValidNumber(this.#greenOrThreshold, NumberType.THRESHOLD) && FontColorContrast.isNotSet(this.#blue) && FontColorContrast.isNotSet(this.#threshold) ) } /** * Converts a color array or separated in RGB to the respective RGB values * @example All these examples produces the same value * arrayOrRgbToRGB(0, 0xcc, 153) * arrayOrRgbToRGB(0x0, 0xcc, 153) * arrayOrRgbToRGB(0, 204, 0x99) */ setColorsFromRgbNumbers (): void { this.red = this.#hexColorOrRedOrArray as number this.green = this.#greenOrThreshold as number this.blue = this.#blue as number this.setThreshold(this.#threshold) } /** * Converts a color array or separated in RGB to the respective RGB values * @param this.#hexColorOrRedOrArray The RGB array * @param threshold The threshold * @example All these examples produces the same value * arrayOrRgbToRGB([0, 0xcc, 153]) * arrayOrRgbToRGB([0x0, 0xcc, 153]) * arrayOrRgbToRGB([0, 204, 0x99]) */ setColorsFromArray (): void { this.red = (this.#hexColorOrRedOrArray as number[])[0] this.green = (this.#hexColorOrRedOrArray as number[])[1] this.blue = (this.#hexColorOrRedOrArray as number[])[2] this.setThreshold(this.#greenOrThreshold) } /** * Converts a ColorIntensity string or number, with all possibilities (e.g. '#009', '009', '#000099', '000099', 153, 0x00099) to the respective RGB values * @param hexColor The color string or number * @param threshold The threshold * @example All these examples produces the same value * hexColorToRGB('#0C9') * hexColorToRGB('0C9') * hexColorToRGB('#00CC99') * hexColorToRGB('00cc99') * hexColorToRGB(52377) * hexColorToRGB(0x00Cc99) */ setColorsFromHexString (): void { switch ((this.#hexColorOrRedOrArray as string).length) { // Color has one char for each color, so they must be repeated case 3: this.red = parseInt((this.#hexColorOrRedOrArray as string)[0].repeat(2), 16) this.green = parseInt((this.#hexColorOrRedOrArray as string)[1].repeat(2), 16) this.blue = parseInt((this.#hexColorOrRedOrArray as string)[2].repeat(2), 16) break // All chars are filled, so no transformation is needed default: this.red = parseInt((this.#hexColorOrRedOrArray as string).substring(0, 2), 16) this.green = parseInt((this.#hexColorOrRedOrArray as string).substring(2, 4), 16) this.blue = parseInt((this.#hexColorOrRedOrArray as string).substring(4, 6), 16) break } this.setThreshold(this.#greenOrThreshold) } /** * Converts the RGB number and sets the respective RGB values. */ setColorsFromNumber (): void { /* * The RGB color has 24 bits (8 bits per color). * This function uses binary operations for better performance, but can be tricky to understand. A 24 bits color could be represented as RRRRRRRR GGGGGGGG BBBBBBBB (the first 8 bits are red, the middle 8 bits are green and the last 8 bits are blue). * To get each color we perform some RIGHT SHIFT and AND operations. * Gets the first 8 bits of the color by shifting it 16 bits * RIGHT SHIFT operation (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Right_shift) * AND operation (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Bitwise_AND) */ // To get red, we shift the 24 bits number 16 bits to the right, leaving the number only with the leftmost 8 bits (RRRRRRRR) this.red = (this.#hexColorOrRedOrArray as number) >> 16 // To get green, the middle 8 bits, we shift it by 8 bits (removing all blue bits - RRRRRRRR GGGGGGGG) and use an AND operation with "0b0000000011111111 = 0xff" to get only the rightmost bits (GGGGGGGG) this.green = ((this.#hexColorOrRedOrArray as number) >> 8) & 0xff // To get blue we use an AND operation with "0b000000000000000011111111 = 0xff" to get only the rightmost bits (BBBBBBBB) this.blue = (this.#hexColorOrRedOrArray as number) & 0xff this.setThreshold(this.#greenOrThreshold) } /** * Sets the threshold to the passed value (if valid - less than or equal 1) or the dafault (0.5) * @param threshold The passed threshold or undefined if not passed */ setThreshold (threshold: any) { this.threshold = threshold || this.threshold } /** * Verifies if a number is a valid color number (numberType = NumberType.COLOR = 0xff) or a valid RGB (numberType = NumberType.RGB = 0xffffff) or a valid threshold (numberType = NumberType.THRESHOLD = 1) * @param num The number to be checked * @param numberType The type of number to be chacked that defines maximum value of the number (default = NumberType.COLOR = 0xff) * @returns True if the number is valid */ static isValidNumber (num: any, numberType: NumberType): boolean { if (numberType === NumberType.THRESHOLD && (num === undefined || num === null)) return true return ( typeof num === 'number' && ((numberType !== NumberType.THRESHOLD && Number.isInteger(num)) || numberType === NumberType.THRESHOLD) && num !== undefined && num !== null && num >= 0 && num <= numberType ) } /** * Verifies if a string is a valig string to be used as a color and if true, returns the correspondent hex number * @returns Array with an empty string and false if the string is invalid or an array with the clean string and the converted string number] */ getCleanStringAndHexNum (): ['', false]|[string, number] { if (typeof this.#hexColorOrRedOrArray !== 'string') return ['', false] const cleanRegEx = /(#|\s)/ig const namedColor = cssNamedColors.find(color => color.name === this.#hexColorOrRedOrArray) if (namedColor) { this.#hexColorOrRedOrArray = namedColor.hex.replace(cleanRegEx, '') } const cleanString = (this.#hexColorOrRedOrArray).replace(cleanRegEx, '') if (cleanString.length !== 3 && cleanString.length !== 6) return ['', false] const hexNum = Number('0x' + cleanString) return [cleanString, hexNum] } /** * Verifies if a value is not set * @param value The value that should be undefined or null * @returns True if the value is not set */ static isNotSet (value: any): boolean { return (value === undefined || value === null) } /** * Calculates the best color (black or white) to contrast with the passed RGB color using the algorithm from https://alienryderflex.com/hsp.html * @returns Black or White depending on the best possible contrast */ contrastFromHSP (): '#000000'|'#ffffff' { const pRed = 0.299 const pGreen = 0.587 const pBlue = 0.114 const contrast = Math.sqrt( pRed * (this.red / 255) ** 2 + pGreen * (this.green / 255) ** 2 + pBlue * (this.blue / 255) ** 2 ) return contrast > this.threshold ? '#000000' : '#ffffff' } }