From 014290441412f02e2ac62c88848121966d73e88d Mon Sep 17 00:00:00 2001 From: ztimson Date: Sun, 1 Mar 2026 21:53:29 -0500 Subject: [PATCH] Added color utilities --- package.json | 2 +- src/color.ts | 67 +++++++++++++++++++++++++++++++++++++++++++--- src/math.ts | 6 ++++- tests/math.spec.ts | 22 +++++++-------- 4 files changed, 81 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index 381017b..247dc48 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ztimson/utils", - "version": "0.28.13", + "version": "0.28.14", "description": "Utility library", "author": "Zak Timson", "license": "MIT", diff --git a/src/color.ts b/src/color.ts index d0996e9..96670d4 100644 --- a/src/color.ts +++ b/src/color.ts @@ -1,12 +1,73 @@ +import {dec2Hex} from './math.ts'; + /** * Determine if either black or white provides more contrast to the provided color - * @param {string} background Color to compare against + * @param {string} color Color to compare against * @return {"white" | "black"} Color with the most contrast */ -export function contrast(background: string): 'white' | 'black' { - const exploded = background?.match(background.length >= 6 ? /[0-9a-fA-F]{2}/g : /[0-9a-fA-F]/g); +export function contrast(color: string): 'white' | 'black' { + const exploded = color?.match(color.length >= 6 ? /[0-9a-fA-F]{2}/g : /[0-9a-fA-F]/g); if(!exploded || exploded?.length < 3) return 'black'; const [r, g, b] = exploded.map(hex => parseInt(hex.length == 1 ? `${hex}${hex}` : hex, 16)); const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; return luminance > 0.5 ? 'black' : 'white'; } + +export function hex2Int(hex: string): {r: number, g: number, b: number} { + let r = 0, g = 0, b = 0; + if (hex.length === 4) { + r = parseInt(hex[1] + hex[1], 16); + g = parseInt(hex[2] + hex[2], 16); + b = parseInt(hex[3] + hex[3], 16); + } else { + r = parseInt(hex.slice(1, 3), 16); + g = parseInt(hex.slice(3, 5), 16); + b = parseInt(hex.slice(5, 7), 16); + } + return {r, g, b}; +} + +export function hue2rgb(p: number, q: number, t: number): number { + if(t < 0) t += 1; + if(t > 1) t -= 1; + if(t < 1 / 6) return p + (q - p) * 6 * t; + if(t < 1 / 2) return q; + if(t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; + return p; +} + +export function int2Hex(r: number, g: number, b: number) { + return '#' + dec2Hex(r) + dec2Hex(g) + dec2Hex(b); +} + +/** + * Adjusts the darkness of a hex color. + * @param {string} hex - The hex color (e.g., '#ff0000'). + * @param {number} amount - A value between -1 (black) and 1 (white) + */ +export function shadeColor(hex: string, amount: number) { + let {r, g, b} = hex2Int(hex); + r /= 255; g /= 255; b /= 255; + const max = Math.max(r, g, b), min = Math.min(r, g, b); + let h, s, l = (max + min) / 2; + + if (max === min) { + h = s = 0; + } else { + const d = max - min; + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + switch (max) { + case r: h = (g - b) / d + (g < b ? 6 : 0); break; + case g: h = (b - r) / d + 2; break; + case b: h = (r - g) / d + 4; break; + default: h = 0; break; + } + h /= 6; + } + + // Adjust Lightness + l = Math.max(0, Math.min(1, l + amount)); + const q = l < 0.5 ? l * (1 + s) : l + s - l * s; + const p = 2 * l - q; + return int2Hex(hue2rgb(p, q, h + 1/3), hue2rgb(p, q, h), hue2rgb(p, q, h - 1/3)); +}; diff --git a/src/math.ts b/src/math.ts index 8d88724..85fc048 100644 --- a/src/math.ts +++ b/src/math.ts @@ -30,6 +30,10 @@ export function dec2Frac(num: number, maxDen=1000): string { (numerator ? numerator + '/' + closest.d : ''); } +export function dec2Hex(num: number): string { + const hex = Math.round(num * 255).toString(16); + return hex.length === 1 ? '0' + hex : hex; +} /** * Convert fraction to decimal number @@ -42,7 +46,7 @@ export function dec2Frac(num: number, maxDen=1000): string { * @param {string} frac Fraction to convert * @return {number} Faction as a decimal */ -export function fracToDec(frac: string) { +export function frac2Dec(frac: string) { let split = frac.split(' '); const whole = split.length == 2 ? Number(split[0]) : 0; split = (split.pop()).split('/'); diff --git a/tests/math.spec.ts b/tests/math.spec.ts index afe6090..a2d0564 100644 --- a/tests/math.spec.ts +++ b/tests/math.spec.ts @@ -1,4 +1,4 @@ -import { dec2Frac, fracToDec } from '../src'; +import { dec2Frac, frac2Dec } from '../src'; describe('Math Utilities', () => { describe('dec2Frac', () => { @@ -27,25 +27,25 @@ describe('Math Utilities', () => { describe('fracToDec', () => { it('should convert mixed fraction to decimal', () => { - expect(fracToDec('1 1/4')).toBeCloseTo(1.25); - expect(fracToDec('2 1/2')).toBeCloseTo(2.5); - expect(fracToDec('3 3/4')).toBeCloseTo(3.75); + expect(frac2Dec('1 1/4')).toBeCloseTo(1.25); + expect(frac2Dec('2 1/2')).toBeCloseTo(2.5); + expect(frac2Dec('3 3/4')).toBeCloseTo(3.75); }); it('should convert fraction without whole part to decimal', () => { - expect(fracToDec('3/4')).toBeCloseTo(0.75); - expect(fracToDec('1/2')).toBeCloseTo(0.5); - expect(fracToDec('1/10')).toBeCloseTo(0.1); + expect(frac2Dec('3/4')).toBeCloseTo(0.75); + expect(frac2Dec('1/2')).toBeCloseTo(0.5); + expect(frac2Dec('1/10')).toBeCloseTo(0.1); }); it('should convert whole number fraction', () => { - expect(fracToDec('4 0/1')).toBeCloseTo(4); - expect(fracToDec('0/1')).toBeCloseTo(0); + expect(frac2Dec('4 0/1')).toBeCloseTo(4); + expect(frac2Dec('0/1')).toBeCloseTo(0); }); it('should handle zero correctly', () => { - expect(fracToDec('0/1')).toBeCloseTo(0); - expect(fracToDec('0 0/1')).toBeCloseTo(0); + expect(frac2Dec('0/1')).toBeCloseTo(0); + expect(frac2Dec('0 0/1')).toBeCloseTo(0); }); }); });