Files
oh-my-pi/packages/utils/src/color.ts
T
can1357 2c85f43709 refactor(utils): renamed packages/pi-utils to packages/utils
- Renamed package directory from packages/pi-utils to packages/utils.
- Updated all path aliases and references in tsconfig.base.json to reflect new package location.
- Updated workspace references in bun.lock and package.json to point to new packages/utils directory.
- Updated documentation and build scripts to reference the renamed utils package.
2026-01-29 11:03:27 +01:00

217 lines
4.2 KiB
TypeScript

/**
* Color manipulation utilities for hex colors.
*
* @example
* ```ts
* import { shiftHue, hexToHsv, hsvToHex } from "@oh-my-pi/pi-utils";
*
* // Shift green toward blue for colorblind accessibility
* const blue = shiftHue("#4ade80", 90); // ~90° shift
*
* // Or work with HSV directly
* const hsv = hexToHsv("#4ade80");
* hsv.h = (hsv.h + 90) % 360;
* const newHex = hsvToHex(hsv);
* ```
*/
export interface HSV {
/** Hue in degrees (0-360) */
h: number;
/** Saturation (0-1) */
s: number;
/** Value/brightness (0-1) */
v: number;
}
export interface RGB {
/** Red (0-255) */
r: number;
/** Green (0-255) */
g: number;
/** Blue (0-255) */
b: number;
}
/**
* Parse a hex color string to RGB.
* Supports #RGB, #RRGGBB formats.
*/
export function hexToRgb(hex: string): RGB {
const h = hex.startsWith("#") ? hex.slice(1) : hex;
if (h.length === 3) {
return {
r: parseInt(h[0] + h[0], 16),
g: parseInt(h[1] + h[1], 16),
b: parseInt(h[2] + h[2], 16),
};
}
return {
r: parseInt(h.slice(0, 2), 16),
g: parseInt(h.slice(2, 4), 16),
b: parseInt(h.slice(4, 6), 16),
};
}
/**
* Convert RGB to hex color string.
*/
export function rgbToHex(rgb: RGB): string {
const toHex = (n: number) =>
Math.max(0, Math.min(255, Math.round(n)))
.toString(16)
.padStart(2, "0");
return `#${toHex(rgb.r)}${toHex(rgb.g)}${toHex(rgb.b)}`;
}
/**
* Convert RGB to HSV.
*/
export function rgbToHsv(rgb: RGB): HSV {
const r = rgb.r / 255;
const g = rgb.g / 255;
const b = rgb.b / 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const d = max - min;
let h = 0;
if (d !== 0) {
if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
else if (max === g) h = ((b - r) / d + 2) / 6;
else h = ((r - g) / d + 4) / 6;
}
return {
h: h * 360,
s: max === 0 ? 0 : d / max,
v: max,
};
}
/**
* Convert HSV to RGB.
*/
export function hsvToRgb(hsv: HSV): RGB {
const { s, v } = hsv;
const h = ((hsv.h % 360) + 360) % 360; // Normalize to 0-360
const i = Math.floor(h / 60);
const f = h / 60 - i;
const p = v * (1 - s);
const q = v * (1 - f * s);
const t = v * (1 - (1 - f) * s);
let r: number, g: number, b: number;
switch (i % 6) {
case 0:
r = v;
g = t;
b = p;
break;
case 1:
r = q;
g = v;
b = p;
break;
case 2:
r = p;
g = v;
b = t;
break;
case 3:
r = p;
g = q;
b = v;
break;
case 4:
r = t;
g = p;
b = v;
break;
default:
r = v;
g = p;
b = q;
break;
}
return {
r: Math.round(r * 255),
g: Math.round(g * 255),
b: Math.round(b * 255),
};
}
/**
* Convert hex color to HSV.
*/
export function hexToHsv(hex: string): HSV {
return rgbToHsv(hexToRgb(hex));
}
/**
* Convert HSV to hex color.
*/
export function hsvToHex(hsv: HSV): string {
return rgbToHex(hsvToRgb(hsv));
}
/**
* Shift the hue of a hex color by a given number of degrees.
*
* @param hex - Hex color string (#RGB or #RRGGBB)
* @param degrees - Degrees to shift (positive = toward blue, negative = toward red)
* @returns New hex color string
*
* @example
* ```ts
* // Shift green 90° toward blue (for colorblind accessibility)
* shiftHue("#4ade80", 90) // Returns a cyan/blue color
* ```
*/
export function shiftHue(hex: string, degrees: number): string {
const hsv = hexToHsv(hex);
hsv.h = (hsv.h + degrees) % 360;
if (hsv.h < 0) hsv.h += 360;
return hsvToHex(hsv);
}
export interface HSVAdjustment {
/** Hue shift in degrees (additive) */
h?: number;
/** Saturation multiplier */
s?: number;
/** Value/brightness multiplier */
v?: number;
}
/**
* Adjust HSV components of a hex color.
*
* @param hex - Hex color string (#RGB or #RRGGBB)
* @param adj - Adjustments: h is additive degrees, s and v are multipliers
* @returns New hex color string
*
* @example
* ```ts
* // Shift hue +60°, reduce saturation to 71%
* adjustHsv("#00ff88", { h: 60, s: 0.71 }) // "#4a9eff"
* ```
*/
export function adjustHsv(hex: string, adj: HSVAdjustment): string {
const hsv = hexToHsv(hex);
if (adj.h !== undefined) {
hsv.h = (hsv.h + adj.h) % 360;
if (hsv.h < 0) hsv.h += 360;
}
if (adj.s !== undefined) {
hsv.s = Math.max(0, Math.min(1, hsv.s * adj.s));
}
if (adj.v !== undefined) {
hsv.v = Math.max(0, Math.min(1, hsv.v * adj.v));
}
return hsvToHex(hsv);
}