UNPKG

contrastrast

Version:

A lightweight tool that parses color strings and recommends text contrast based on WCAG Standards

423 lines (422 loc) 17.6 kB
import { getRGBFromColorString } from "./helpers/colorStringParsers.js"; import { BRIGHTNESS_COEFFICIENTS, CONTRAST_THRESHOLD, GAMMA_CORRECTION, HSL_CONVERSION, LUMINANCE_COEFFICIENTS, RGB_BOUNDS, WCAG_LEVELS, } from "./constants.js"; import { contrastRatio } from "./utils/contrastRatio.js"; import { textContrast, } from "./utils/textContrast.js"; const DEFAULT_PARSE_OPTIONS = { throwOnError: true, fallbackColor: "#000000", }; /** * A comprehensive color manipulation class that supports parsing, conversion, and accessibility analysis * @example * ```typescript * const color = new Contrastrast("#1a73e8"); * const isLight = color.isLight(); // false * const hexValue = color.toHex(); // "#1a73e8" * const ratio = color.contrastRatio("#ffffff"); // 4.5 * ``` */ export class Contrastrast { /** * Create a new Contrastrast instance from a color string * @param colorString Color string in hex (#abc or #abcdef), rgb (rgb(r,g,b)), or hsl (hsl(h,s%,l%)) format * @param parseOpts Optional parsing configuration * @param parseOpts.throwOnError Whether to throw an error on invalid color strings (default: true) * @param parseOpts.fallbackColor Fallback color to use when throwOnError is false (default: "#000000") * @throws {Error} When the color string format is not supported and throwOnError is true * @example * ```typescript * const color1 = new Contrastrast("#ff0000"); * const color2 = new Contrastrast("rgb(255, 0, 0)"); * const color3 = new Contrastrast("hsl(0, 100%, 50%)"); * * // With error handling * const color4 = new Contrastrast("invalid", { throwOnError: false, fallbackColor: "#333333" }); * ``` */ constructor(colorString, parseOpts) { Object.defineProperty(this, "rgb", { enumerable: true, configurable: true, writable: true, value: void 0 }); // Conversion & Output Methods /** * Convert the color to a hex string representation * @param includeHash Whether to include the # prefix (default: true) * @returns Hex color string (e.g., "#ff0000" or "ff0000") * @example * ```typescript * const color = new Contrastrast("rgb(255, 0, 0)"); * const withHash = color.toHex(); // "#ff0000" * const withoutHash = color.toHex(false); // "ff0000" * ``` */ Object.defineProperty(this, "toHex", { enumerable: true, configurable: true, writable: true, value: (includeHash = true) => { const toHex = (n) => { const hex = Math.round(n).toString(16); return hex.length === 1 ? "0" + hex : hex; }; const hexValue = toHex(this.rgb.r) + toHex(this.rgb.g) + toHex(this.rgb.b); return includeHash ? `#${hexValue}` : hexValue; } }); /** * Get the RGB values as an object * @returns RGB values object with r, g, b properties (0-255) * @example * ```typescript * const color = new Contrastrast("#ff0000"); * const rgb = color.toRgb(); // { r: 255, g: 0, b: 0 } * ``` */ Object.defineProperty(this, "toRgb", { enumerable: true, configurable: true, writable: true, value: () => ({ ...this.rgb }) }); /** * Convert the color to an RGB string representation * @returns RGB color string (e.g., "rgb(255, 0, 0)") * @example * ```typescript * const color = new Contrastrast("#ff0000"); * const rgbString = color.toRgbString(); // "rgb(255, 0, 0)" * ``` */ Object.defineProperty(this, "toRgbString", { enumerable: true, configurable: true, writable: true, value: () => `rgb(${this.rgb.r}, ${this.rgb.g}, ${this.rgb.b})` }); /** * Convert the color to HSL values * @returns HSL values object with h (0-360), s (0-100), l (0-100) properties * @example * ```typescript * const color = new Contrastrast("#ff0000"); * const hsl = color.toHsl(); // { h: 0, s: 100, l: 50 } * ``` */ Object.defineProperty(this, "toHsl", { enumerable: true, configurable: true, writable: true, value: () => { const r = this.rgb.r / RGB_BOUNDS.MAX; const g = this.rgb.g / RGB_BOUNDS.MAX; const b = this.rgb.b / RGB_BOUNDS.MAX; const max = Math.max(r, g, b); const min = Math.min(r, g, b); let h, s; const l = (max + min) / 2; if (max === min) { h = s = 0; // achromatic } else { const d = max - min; s = l > HSL_CONVERSION.LIGHTNESS_THRESHOLD ? d / (2 - max - min) : d / (max + min); switch (max) { case r: h = (g - b) / d + (g < b ? HSL_CONVERSION.HUE_SECTORS : 0); break; case g: h = (b - r) / d + 2; break; case b: h = (r - g) / d + 4; break; default: h = 0; } h /= HSL_CONVERSION.HUE_SECTORS; } return { h: Math.round(h * HSL_CONVERSION.FULL_CIRCLE_DEGREES), s: Math.round(s * HSL_CONVERSION.PERCENTAGE_MULTIPLIER), l: Math.round(l * HSL_CONVERSION.PERCENTAGE_MULTIPLIER), }; } }); /** * Convert the color to an HSL string representation * @returns HSL color string (e.g., "hsl(0, 100%, 50%)") * @example * ```typescript * const color = new Contrastrast("#ff0000"); * const hslString = color.toHslString(); // "hsl(0, 100%, 50%)" * ``` */ Object.defineProperty(this, "toHslString", { enumerable: true, configurable: true, writable: true, value: () => { const hsl = this.toHsl(); return `hsl(${hsl.h}, ${hsl.s}%, ${hsl.l}%)`; } }); /** * Calculate the WCAG 2.1 relative luminance of the color * @returns Luminance value between 0 (darkest) and 1 (lightest) * @example * ```typescript * const black = new Contrastrast("#000000"); * const white = new Contrastrast("#ffffff"); * console.log(black.luminance()); // 0 * console.log(white.luminance()); // 1 * ``` */ Object.defineProperty(this, "luminance", { enumerable: true, configurable: true, writable: true, value: () => { const gammaCorrect = (colorValue) => { const c = colorValue / RGB_BOUNDS.MAX; return c <= GAMMA_CORRECTION.THRESHOLD ? c / GAMMA_CORRECTION.LINEAR_DIVISOR : Math.pow((c + GAMMA_CORRECTION.GAMMA_OFFSET) / GAMMA_CORRECTION.GAMMA_DIVISOR, GAMMA_CORRECTION.GAMMA_EXPONENT); }; const rLinear = gammaCorrect(this.rgb.r); const gLinear = gammaCorrect(this.rgb.g); const bLinear = gammaCorrect(this.rgb.b); return (LUMINANCE_COEFFICIENTS.RED * rLinear + LUMINANCE_COEFFICIENTS.GREEN * gLinear + LUMINANCE_COEFFICIENTS.BLUE * bLinear); } }); /** * Calculate the perceived brightness of the color using the WCAG formula * @returns Brightness value between 0 (darkest) and 255 (brightest) * @example * ```typescript * const color = new Contrastrast("#1a73e8"); * const brightness = color.brightness(); // ~102.4 * ``` */ Object.defineProperty(this, "brightness", { enumerable: true, configurable: true, writable: true, value: () => (this.rgb.r * BRIGHTNESS_COEFFICIENTS.RED + this.rgb.g * BRIGHTNESS_COEFFICIENTS.GREEN + this.rgb.b * BRIGHTNESS_COEFFICIENTS.BLUE) / BRIGHTNESS_COEFFICIENTS.DIVISOR }); /* Utility Methods */ /** * Determine if the color is considered "light" based on WCAG brightness threshold * @returns True if the color is light (brightness > 124), false otherwise * @example * ```typescript * const lightColor = new Contrastrast("#ffffff"); * const darkColor = new Contrastrast("#000000"); * console.log(lightColor.isLight()); // true * console.log(darkColor.isLight()); // false * ``` */ Object.defineProperty(this, "isLight", { enumerable: true, configurable: true, writable: true, value: () => this.brightness() > CONTRAST_THRESHOLD }); /** * Determine if the color is considered "dark" based on WCAG brightness threshold * @returns True if the color is dark (brightness <= 124), false otherwise * @example * ```typescript * const lightColor = new Contrastrast("#ffffff"); * const darkColor = new Contrastrast("#000000"); * console.log(lightColor.isDark()); // false * console.log(darkColor.isDark()); // true * ``` */ Object.defineProperty(this, "isDark", { enumerable: true, configurable: true, writable: true, value: () => !this.isLight() }); /** * Calculate the WCAG 2.1 contrast ratio between this color and another color * @param color Color to compare against - accepts hex, rgb, hsl strings or Contrastrast instance * @returns Contrast ratio from 1:1 (no contrast) to 21:1 (maximum contrast) * @example * ```typescript * const bgColor = new Contrastrast("#1a73e8"); * const ratio = bgColor.contrastRatio("#ffffff"); // 4.5 * ``` */ Object.defineProperty(this, "contrastRatio", { enumerable: true, configurable: true, writable: true, value: (color) => contrastRatio(this, color) }); /** * Check if the color combination meets specific WCAG contrast requirements * @param comparisonColor Color to compare against - accepts hex, rgb, hsl strings or Contrastrast instance * @param role Role of this color instance ("foreground" for text color, "background" for background color) * @param targetWcagLevel Target WCAG compliance level ("AA" or "AAA") * @param textSize Text size category ("normal" or "large") - affects required contrast ratio * @returns True if the combination meets the specified WCAG requirements * @example * ```typescript * const bgColor = new Contrastrast("#1a73e8"); * const meetsAA = bgColor.meetsWCAG("#ffffff", "background", "AA"); // true * const meetsAAA = bgColor.meetsWCAG("#ffffff", "background", "AAA"); // false * ``` */ Object.defineProperty(this, "meetsWCAG", { enumerable: true, configurable: true, writable: true, value: (comparisonColor, role, targetWcagLevel, textSize = "normal") => { const ratio = this.textContrast(comparisonColor, role); const required = WCAG_LEVELS[targetWcagLevel][textSize]; return ratio >= required; } }); /** * Check if this color is equal to another color (RGB values comparison) * @param color Color to compare against - accepts hex, rgb, hsl strings or Contrastrast instance * @returns True if both colors have identical RGB values * @example * ```typescript * const color1 = new Contrastrast("#ff0000"); * const color2 = new Contrastrast("rgb(255, 0, 0)"); * const color3 = new Contrastrast("hsl(0, 100%, 50%)"); * console.log(color1.equals(color2)); // true * console.log(color1.equals(color3)); // true * ``` */ Object.defineProperty(this, "equals", { enumerable: true, configurable: true, writable: true, value: (color) => { const other = color instanceof Contrastrast ? color : new Contrastrast(color); return (this.rgb.r === other.rgb.r && this.rgb.g === other.rgb.g && this.rgb.b === other.rgb.b); } }); const options = parseOpts || DEFAULT_PARSE_OPTIONS; try { this.rgb = getRGBFromColorString(colorString); } catch { if (options.throwOnError === false) { console.warn(`Invalid color string "${colorString}"; Using "${options.fallbackColor || DEFAULT_PARSE_OPTIONS.fallbackColor}" as fallback color`); this.rgb = getRGBFromColorString(options.fallbackColor || DEFAULT_PARSE_OPTIONS.fallbackColor); } else { throw Error(`Invalid color string "${colorString}"`); } } } /** * Create a Contrastrast instance from RGB values * @param rOrRgb Either red value (0-255) or RGB values object * @param g Green value (0-255) when first parameter is red value * @param b Blue value (0-255) when first parameter is red value * @returns New Contrastrast instance * @example * ```typescript * const red1 = Contrastrast.fromRgb(255, 0, 0); * const red2 = Contrastrast.fromRgb({ r: 255, g: 0, b: 0 }); * ``` */ static fromRgb(rOrRgb, g, b) { if (typeof rOrRgb === "object") { return new Contrastrast(`rgb(${rOrRgb.r}, ${rOrRgb.g}, ${rOrRgb.b})`); } return new Contrastrast(`rgb(${rOrRgb}, ${g}, ${b})`); } /** * Create a Contrastrast instance from HSL values * @param hOrHsl Either hue value (0-360) or HSL values object * @param s Saturation value (0-100) when first parameter is hue value * @param l Lightness value (0-100) when first parameter is hue value * @returns New Contrastrast instance * @example * ```typescript * const red1 = Contrastrast.fromHsl(0, 100, 50); * const red2 = Contrastrast.fromHsl({ h: 0, s: 100, l: 50 }); * ``` */ static fromHsl(hOrHsl, s, l) { if (typeof hOrHsl === "object") { return new Contrastrast(`hsl(${hOrHsl.h}, ${hOrHsl.s}%, ${hOrHsl.l}%)`); } return new Contrastrast(`hsl(${hOrHsl}, ${s}%, ${l}%)`); } // Implementation textContrast(comparisonColor, role = "background", options = {}) { if (role === "background") { // Current color is background, comparisonColor is foreground (text color) return textContrast(comparisonColor, this, options); } else { // Current color is foreground (text color), comparisonColor is background return textContrast(this, comparisonColor, options); } } } // Parser/Creator Methods /** * Create a Contrastrast instance from a hex color string * @param hex Hex color string with or without # prefix (e.g., "#ff0000" or "ff0000") * @returns New Contrastrast instance * @example * ```typescript * const red1 = Contrastrast.fromHex("#ff0000"); * const red2 = Contrastrast.fromHex("ff0000"); * const shortRed = Contrastrast.fromHex("#f00"); * ``` */ Object.defineProperty(Contrastrast, "fromHex", { enumerable: true, configurable: true, writable: true, value: (hex) => { const normalizedHex = hex.startsWith("#") ? hex : `#${hex}`; return new Contrastrast(normalizedHex); } }); /** * Parse a color string into a Contrastrast instance (alias for constructor) * @param colorString Color string in hex, rgb, or hsl format * @param parseOpts Optional parsing configuration * @param parseOpts.throwOnError Whether to throw an error on invalid color strings (default: true) * @param parseOpts.fallbackColor Fallback color to use when throwOnError is false (default: "#000000") * @returns New Contrastrast instance * @throws {Error} When the color string format is not supported and throwOnError is true * @example * ```typescript * const color = Contrastrast.parse("#1a73e8"); * * // With error handling * const safeColor = Contrastrast.parse("invalid", { throwOnError: false, fallbackColor: "#ffffff" }); * ``` */ Object.defineProperty(Contrastrast, "parse", { enumerable: true, configurable: true, writable: true, value: (colorString, parseOpts) => new Contrastrast(colorString, parseOpts) });