UNPKG

nhb-toolbox

Version:

A versatile collection of smart, efficient, and reusable utility functions and classes for everyday development needs.

449 lines (448 loc) 19.4 kB
import { convertColorCode } from './convert.js'; import { CSS_COLORS } from './css-colors.js'; import { _convertOpacityToHex, _isHSL, _isHSLA, _isRGB, _isRGBA, } from './helpers.js'; import { generateRandomHSLColor } from './random.js'; import { extractAlphaColorValues, extractSolidColorValues } from './utils.js'; /** * * Class representing a color and its conversions among `Hex`, `Hex8` `RGB`, `RGBA`, `HSL` and `HSLA` formats. * * It has 13 instance methods to manipulate and play with the color values. * * It has 7 static methods that can be used to check if a color is in `Hex`, `Hex8` `RGB`, `RGBA`, `HSL` or `HSLA` format. * * @property hex - The color in `Hex` format. * @property hex8 - The color in `Hex8` format. * @property rgb - The color in `RGB` format. * @property rgba - The color in `RGBA` format. * @property hsl - The color in `HSL` format. * @property hsla - The color in `HSLA` format. */ export class Color { hex; hex8; rgb; rgba; hsl; hsla; /** * * Creates a new `Color` instance and automatically converts the input color to all other supported formats: `Hex`, `Hex8`, `RGB`, `RGBA`, `HSL`, and `HSLA`. * * @description * The `Color` class allows seamless transformation between six common color representations: * - `Hex` (e.g., `#ff5733`) * - `Hex8` (Hex with opacity, e.g., `#ff573380`) * - `RGB` (e.g., `rgb(255, 87, 51)`) * - `RGBA` (e.g., `rgba(255, 87, 51, 1)`) * - `HSL` (e.g., `hsl(14, 100%, 60%)`) * - `HSLA` (e.g., `hsla(14, 100%, 60%, 1)`) * * You can create a color from any of these formats, and the class will populate the rest. * If no color is passed, a random color will be generated. * * Additionally: * - It has 13 instance methods to manipulate and play with the color values. * - Use static methods like `Color.isHex6(color)` to validate color strings. * * @param color - An optional input color string in any supported format (`Hex`, `Hex8`, `RGB`, `RGBA`, `HSL`, or `HSLA`) to convert in all other (includes the current format) formats. * * @example * // Convert an existing Hex color to all other formats * const color = new Color("#ff5733"); * console.log(color.rgb); // 'rgb(255, 87, 51)' * console.log(color.hsl); // 'hsl(14, 100%, 60%)' * console.log(color.rgba); // 'rgba(255, 87, 51, 1)' * console.log(color.hsla); // 'hsla(14, 100%, 60%, 1)' * console.log(color.hex8); // '#FF5733FF' * * @example * // Handle a color with alpha * const alphaColor = new Color("rgba(255, 0, 0, 0.5)"); * console.log(alphaColor.hex8); // '#FF000080' * console.log(alphaColor.hsla); // 'hsla(0, 100%, 50%, 0.5)' * * @example * // Generate a random color * const randomColor = new Color(); * console.log(randomColor.hex, randomColor.rgb, randomColor.hsl); * * @returns Instance of `Color`. */ constructor(color) { if (color) { if (Color.isCSSColor(color)) { const newColor = new Color(CSS_COLORS[color?.trim()]); this.hex = newColor.hex; this.hex8 = newColor.hex8; this.rgb = newColor.rgb; this.rgba = newColor.rgba; this.hsl = newColor.hsl; this.hsla = newColor.hsla; } else { const colors = this.#convertColorToOthers(color?.trim()); if ('hex8' in colors) { // Extract alpha color values (Hex8, RGBA, HSLA) const rgbaValues = extractAlphaColorValues(colors.rgba); const hslaValues = extractAlphaColorValues(colors.hsla); this.hex = colors.hex8.toUpperCase().slice(0, 7); this.hex8 = colors.hex8.toUpperCase(); this.rgb = `rgb(${rgbaValues[0]}, ${rgbaValues[1]}, ${rgbaValues[2]})`; this.rgba = colors.rgba; this.hsl = `hsl(${hslaValues[0]}, ${hslaValues[1]}%, ${hslaValues[2]}%)`; this.hsla = colors.hsla; } else { // Extract solid color values (Hex, RGB, HSL) const rgbValues = extractSolidColorValues(colors.rgb); const hslValues = extractSolidColorValues(colors.hsl); this.hex = colors.hex.toUpperCase(); this.hex8 = `${colors.hex.toUpperCase()}${_convertOpacityToHex(100)}`; this.rgb = colors.rgb; this.rgba = `rgba(${rgbValues[0]}, ${rgbValues[1]}, ${rgbValues[2]}, 1)`; this.hsl = colors.hsl; this.hsla = `hsla(${hslValues[0]}, ${hslValues[1]}%, ${hslValues[2]}%, 1)`; } } } else { const hsl = generateRandomHSLColor(); const { hex, rgb } = convertColorCode(hsl); const rgbValues = extractSolidColorValues(rgb); const hslValues = extractSolidColorValues(hsl); // Generate random colors this.hex = hex.toUpperCase(); this.hex8 = `${hex.toUpperCase()}${_convertOpacityToHex(100)}`; this.rgb = rgb; this.rgba = `rgba(${rgbValues[0]}, ${rgbValues[1]}, ${rgbValues[2]}, 1)`; this.hsl = hsl; this.hsla = `hsla(${hslValues[0]}, ${hslValues[1]}%, ${hslValues[2]}%, 1)`; } } /** - Iterates over the color representations (Hex, RGB, HSL). */ *[Symbol.iterator]() { yield this.hex; yield this.hex8; yield this.rgb; yield this.rgba; yield this.hsl; yield this.hsla; } /** * @instance Applies or modifies the opacity of a color. Mutate the original instance. * - For solid colors (Hex6/RGB/HSL): Adds an alpha channel with the specified opacity. * - For alpha colors (Hex8/RGBA/HSLA): Updates the existing alpha channel. * * @param opacity - A number between 0-100 representing the opacity percentage. * @returns A new instance of `Color` containing all color formats with the applied opacity. * * @example * const color = new Color("#ff0000"); * const alpha50 = color.applyOpacity(50); // 50% opacity * console.log(alpha50.rgba); // rgba(255, 0, 0, 0.5) * * @example * const alphaColor = new Color("#ff000080"); // Color with 50% opacity * const alpha75 = alphaColor.applyOpacity(75); // Change to 75% opacity * console.log(alpha75.hex8); // #FF0000BF */ applyOpacity(opacity) { const validOpacity = Math.min(100, Math.max(0, opacity)); const alphaHex = _convertOpacityToHex(opacity); const alphaDecimal = validOpacity / 100; const rgbValues = extractSolidColorValues(this.rgb); const hslValues = extractSolidColorValues(this.hsl); return Color.#fromParts({ hex: this.hex.slice(0, 7).toUpperCase(), hex8: `${this.hex.slice(0, 7)}${alphaHex}`.toUpperCase(), rgb: `rgb(${rgbValues[0]}, ${rgbValues[1]}, ${rgbValues[2]})`, rgba: `rgba(${rgbValues[0]}, ${rgbValues[1]}, ${rgbValues[2]}, ${alphaDecimal})`, hsl: `hsl(${hslValues[0]}, ${hslValues[1]}%, ${hslValues[2]}%)`, hsla: `hsla(${hslValues[0]}, ${hslValues[1]}%, ${hslValues[2]}%, ${alphaDecimal})`, }); } /** * @instance Darkens the color by reducing the lightness by the given percentage. * @param percent - The percentage to darken (0–100). * @returns A new `Color` instance with the modified darkness. */ applyDarkness(percent) { const [h, s, l, a] = extractAlphaColorValues(this.hsla); const newL = Math.max(0, l - percent); const newHSL = `hsl(${h}, ${s}%, ${newL}%)`; return new Color(newHSL).applyOpacity((a * 100)); } /** * @instance Lightens the color by increasing the lightness by the given percentage. * @param percent - The percentage to brighten (0–100). * @returns A new `Color` instance with the modified lightness. */ applyBrightness(percent) { const [h, s, l, a] = extractAlphaColorValues(this.hsla); const newL = Math.min(100, l + percent); const newHSL = `hsl(${h}, ${s}%, ${newL}%)`; return new Color(newHSL).applyOpacity((a * 100)); } /** * @instance Reduces the saturation of the color to make it appear duller. * @param percent - The percentage to reduce saturation (0–100). * @returns A new `Color` instance with the modified saturation. */ applyDullness(percent) { const [h, s, l, a] = extractAlphaColorValues(this.hsla); const newS = Math.max(0, s - percent); const newHSL = `hsl(${h}, ${newS}%, ${l}%)`; return new Color(newHSL).applyOpacity((a * 100)); } /** * @instance Softens the color toward white by reducing saturation and increasing lightness based on a percentage. * - *This creates a soft UI-like white shade effect (similar to some UI libraries' light color scale).* * @param percent - Value from 0 to 100 representing how far to push the color toward white. * @returns A new `Color` instance shifted toward white. */ applyWhiteShade(percent) { const [h, s, l, a] = extractAlphaColorValues(this.hsla); // Cap values to avoid overshooting const newS = Math.max(0, s - (s * percent) / 100); const newL = Math.min(100, l + ((100 - l) * percent) / 100); const newHSL = `hsl(${h}, ${newS}%, ${newL}%)`; return new Color(newHSL).applyOpacity((a * 100)); } /** * @instance Blends the current color with another color based on the given weight. * * - **NOTE:** *If any of the input colors has opacity (alpha channel), it might be lost or distorted from the generated alpha variants of the respective color formats.* * * @param other - The color in any 6 `(Hex, Hex8 RGB, RGBA, HSL or HSLA)` format to blend with. * @param weight - A number from 0 to 1 indicating the weight of the other color. Defaults to `0.5`. * - `weight = 0` → only the original color. * - `weight = 1` → only the other color. * - `weight = 0.5` → equal blend between the two. * @returns A new `Color` instance representing the blended result, with proper alpha blending. */ blendWith(other, weight = 0.5) { const w = Math.max(0, Math.min(1, weight)); const converted = Color.isCSSColor(other) ? new Color(other) : new Color(other); const [r1, b1, g1, a1] = extractAlphaColorValues(this.rgba); const [r2, b2, g2, a2] = extractAlphaColorValues(converted.rgba); const alpha = Math.round((a1 * (1 - w) + a2 * w) * 100) / 100; const blendChannel = (c1, c2) => { return Math.round((c1 * a1 * (1 - w) + c2 * a2 * w) / alpha); }; const r = blendChannel(r1, r2); const g = blendChannel(g1, g2); const b = blendChannel(b1, b2); const blended = `rgba(${r}, ${g}, ${b}, ${alpha})`; return new Color(blended); } /** * @instance Calculates the contrast ratio between this color and another color (WCAG). * @param other - The other color to compare against. * @returns A number representing the contrast ratio (rounded to 2 decimal places). */ contrastRatio(other) { const newColor = Color.isCSSColor(other) ? new Color(other) : new Color(other); const luminance = (rgb) => { const [r, g, b] = extractSolidColorValues(rgb).map((v) => { const c = v / 255; return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4); }); return 0.2126 * r + 0.7152 * g + 0.0722 * b; }; const lum1 = luminance(this.rgb); const lum2 = luminance(newColor.rgb); const brighter = Math.max(lum1, lum2); const darker = Math.min(lum1, lum2); const ratio = (brighter + 0.05) / (darker + 0.05); return Math.round(ratio * 100) / 100; } /** * @instance Returns the complementary color by rotating the hue 180 degrees. * @returns A new Color that is the complement of the current color. */ getComplementaryColor() { const [h, s, l, a] = extractAlphaColorValues(this.hsla); const newHue = (h + 180) % 360; const newHSL = `hsl(${newHue}, ${s}%, ${l}%)`; return new Color(newHSL).applyOpacity((a * 100)); } /** * @instance Generates a color scheme of analogous colors, including the base color. * Analogous colors are next to each other on the color wheel (±30°). * @returns An array of three Color instances: [base, left, right]. */ getAnalogousColors() { const [h, s, l, a] = extractAlphaColorValues(this.hsla); const left = `hsl(${(h + 330) % 360}, ${s}%, ${l}%)`; const right = `hsl(${(h + 30) % 360}, ${s}%, ${l}%)`; const analogous = [this, new Color(left), new Color(right)]; return analogous.map((c) => c.applyOpacity((a * 100))); } /** * @instance Generates a color triad scheme including the base color. * Triadic colors are evenly spaced (120° apart) on the color wheel. * @returns An array of three Color instances: [base, triad1, triad2]. */ getTriadColors() { const [h, s, l, a] = extractAlphaColorValues(this.hsla); const c1 = `hsl(${(h + 120) % 360}, ${s}%, ${l}%)`; const c2 = `hsl(${(h + 240) % 360}, ${s}%, ${l}%)`; const triad = [this, new Color(c1), new Color(c2)]; return triad.map((c) => c.applyOpacity((a * 100))); } /** * @instance Generates a tetradic color scheme including the base color. * Tetradic colors form a rectangle on the color wheel (90° apart). * @returns An array of four Color instances: [base, tetrad1, tetrad2, tetrad3]. */ getTetradColors() { const [h, s, l, a] = extractAlphaColorValues(this.hsla); const c1 = `hsl(${(h + 90) % 360}, ${s}%, ${l}%)`; const c2 = `hsl(${(h + 180) % 360}, ${s}%, ${l}%)`; const c3 = `hsl(${(h + 270) % 360}, ${s}%, ${l}%)`; const tetrad = [this, new Color(c1), new Color(c2), new Color(c3)]; return tetrad.map((c) => c.applyOpacity((a * 100))); } /** * @instance Gets the `WCAG` accessibility rating between this and another color. * @param other - The other color to test contrast against. * @returns 'Fail', 'AA', or 'AAA' based on `WCAG 2.1` contrast standards. */ getWCAGRating(other) { const ratio = this.contrastRatio(other); if (ratio >= 7) return 'AAA'; if (ratio >= 4.5) return 'AA'; return 'Fail'; } /** * @instance Determines if the color is light based on its perceived brightness. * @returns `true` if light, `false` if dark. */ isLightColor() { const [r, g, b] = extractSolidColorValues(this.rgb); const brightness = (r * 299 + g * 587 + b * 114) / 1000; return brightness > 127.5; } /** * @static Checks if a color is in `Hex6` format. * * @param color Color to check. * @returns Boolean: `true` if it's a `Hex6` color, `false` if not. */ static isHex6(color) { return /^#[0-9A-Fa-f]{6}$/.test(color?.trim()); } /** * @static Checks if a color is in `Hex8` format. * * @param color Color to check. * @returns Boolean: `true` if it's a `Hex8` color, `false` if not. */ static isHex8(color) { return /^#[0-9A-Fa-f]{8}$/.test(color?.trim()); } /** * @static Checks if a color is in `RGB` format and within valid ranges. * * @param color Color to check. * @returns `true` if it's a `RGB` color, `false` if not. */ static isRGB(color) { return _isRGB(color); } /** * @static Checks if a color is in `RGBA` format and within valid ranges. * * @param color Color to check. * @returns `true` if it's a `RGBA` color, `false` if not. */ static isRGBA(color) { return _isRGBA(color); } /** * @static Checks if a color is in `HSL` format and within valid ranges. * * @param color Color to check. * @returns `true` if it's a `HSL` color, `false` if not. */ static isHSL(color) { return _isHSL(color); } /** * @static Checks if a color is in `HSLA` format and within valid ranges. * * @param color Color to check. * @returns `true` if it's a `HSLA` color, `false` if not. */ static isHSLA(color) { return _isHSLA(color); } /** * @static Checks if a color is a valid CSS color name. * - This method checks against a predefined list of CSS color names. * - It does not validate format types like Hex, RGB, or HSL or their alpha channels. * * @param color - The color to check. * @returns `true` if the color is a valid CSS color name, `false` otherwise. */ static isCSSColor(color) { return (!Color.isHex6(color) && !Color.isHex8(color) && !_isRGB(color) && !_isRGBA(color) && !_isHSL(color) && !_isHSLA(color) && color in CSS_COLORS); } /** * @private Converts the given color to all other formats while preserving the original. * * @param color - The color to convert. * @returns An object containing Hex, RGB, and HSL representations. */ #convertColorToOthers(color) { if (Color.isHex6(color)) { const { rgb, hsl } = convertColorCode(color); return { hex: color, rgb, hsl }; } else if (Color.isRGB(color)) { const { hex, hsl } = convertColorCode(color); return { hex, rgb: color, hsl }; } else if (Color.isHSL(color)) { const { hex, rgb } = convertColorCode(color); return { hex, rgb, hsl: color }; } else if (Color.isHex8(color)) { const { rgba, hsla } = convertColorCode(color); return { hex8: color, rgba, hsla }; } else if (Color.isRGBA(color)) { const { hex8, hsla } = convertColorCode(color); return { hex8, rgba: color, hsla }; } else if (Color.isHSLA(color)) { const { hex8, rgba } = convertColorCode(color); return { hex8, rgba, hsla: color }; } throw new Error(`Unrecognized color format: ${color}`); } /** * @private @static Internal factory to create a Color instance from parsed parts. * @param parts All the color parts as object. * @returns An instance of `Color`. */ static #fromParts(parts) { const color = Object.create(Color.prototype); color.hex = parts.hex; color.hex8 = parts.hex8; color.rgb = parts.rgb; color.rgba = parts.rgba; color.hsl = parts.hsl; color.hsla = parts.hsla; return color; } }