contrastrast
Version:
A lightweight tool that parses color strings and recommends text contrast based on WCAG Standards
423 lines (422 loc) • 17.6 kB
JavaScript
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)
});