Files
ui-essentials/projects/hcl-studio/src/lib/core/hcl-converter.ts
skyai_dev 5346d6d0c9 Add comprehensive library expansion with new components and demos
- Add new libraries: ui-accessibility, ui-animations, ui-backgrounds, ui-code-display, ui-data-utils, ui-font-manager, hcl-studio
- Add extensive layout components: gallery-grid, infinite-scroll-container, kanban-board, masonry, split-view, sticky-layout
- Add comprehensive demo components for all new features
- Update project configuration and dependencies
- Expand component exports and routing structure
- Add UI landing pages planning document

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-05 05:37:37 +10:00

324 lines
9.9 KiB
TypeScript

/**
* ==========================================================================
* HCL COLOR CONVERTER
* ==========================================================================
* Accurate color space conversions between RGB, LAB, and HCL
* Based on CIE LAB color space for perceptual uniformity
* ==========================================================================
*/
import { RGB, LAB, HCL } from '../models/hcl.models';
export class HCLConverter {
// ==========================================================================
// RGB ↔ HCL CONVERSIONS
// ==========================================================================
/**
* Convert RGB to HCL color space
* @param r Red component (0-255)
* @param g Green component (0-255)
* @param b Blue component (0-255)
* @returns HCL color
*/
static rgbToHcl(r: number, g: number, b: number): HCL {
const lab = this.rgbToLab(r, g, b);
return this.labToHcl(lab.l, lab.a, lab.b);
}
/**
* Convert HCL to RGB color space
* @param h Hue (0-360)
* @param c Chroma (0-100+)
* @param l Lightness (0-100)
* @returns RGB color
*/
static hclToRgb(h: number, c: number, l: number): RGB {
const lab = this.hclToLab(h, c, l);
return this.labToRgb(lab.l, lab.a, lab.b);
}
// ==========================================================================
// HEX CONVERSIONS
// ==========================================================================
/**
* Convert hex string to HCL
* @param hex Hex color string (#RRGGBB or #RGB)
* @returns HCL color
*/
static hexToHcl(hex: string): HCL {
const rgb = this.hexToRgb(hex);
return this.rgbToHcl(rgb.r, rgb.g, rgb.b);
}
/**
* Convert HCL to hex string
* @param h Hue (0-360)
* @param c Chroma (0-100+)
* @param l Lightness (0-100)
* @returns Hex color string
*/
static hclToHex(h: number, c: number, l: number): string {
const rgb = this.hclToRgb(h, c, l);
return this.rgbToHex(rgb.r, rgb.g, rgb.b);
}
// ==========================================================================
// RGB ↔ LAB CONVERSIONS (via XYZ)
// ==========================================================================
/**
* Convert RGB to LAB color space
* @param r Red component (0-255)
* @param g Green component (0-255)
* @param b Blue component (0-255)
* @returns LAB color
*/
static rgbToLab(r: number, g: number, b: number): LAB {
// Convert RGB to XYZ first
let rNorm = r / 255;
let gNorm = g / 255;
let bNorm = b / 255;
// Apply gamma correction (sRGB)
rNorm = rNorm > 0.04045 ? Math.pow((rNorm + 0.055) / 1.055, 2.4) : rNorm / 12.92;
gNorm = gNorm > 0.04045 ? Math.pow((gNorm + 0.055) / 1.055, 2.4) : gNorm / 12.92;
bNorm = bNorm > 0.04045 ? Math.pow((bNorm + 0.055) / 1.055, 2.4) : bNorm / 12.92;
// Convert to XYZ using sRGB matrix
let x = rNorm * 0.4124564 + gNorm * 0.3575761 + bNorm * 0.1804375;
let y = rNorm * 0.2126729 + gNorm * 0.7151522 + bNorm * 0.0721750;
let z = rNorm * 0.0193339 + gNorm * 0.1191920 + bNorm * 0.9503041;
// Normalize by D65 illuminant
x = x / 0.95047;
y = y / 1.00000;
z = z / 1.08883;
// Convert XYZ to LAB
const fx = x > 0.008856 ? Math.pow(x, 1/3) : (7.787 * x + 16/116);
const fy = y > 0.008856 ? Math.pow(y, 1/3) : (7.787 * y + 16/116);
const fz = z > 0.008856 ? Math.pow(z, 1/3) : (7.787 * z + 16/116);
const l = (116 * fy) - 16;
const a = 500 * (fx - fy);
const bValue = 200 * (fy - fz);
return { l: Math.max(0, Math.min(100, l)), a, b: bValue };
}
/**
* Convert LAB to RGB color space
* @param l Lightness (0-100)
* @param a Green-Red axis
* @param b Blue-Yellow axis
* @returns RGB color
*/
static labToRgb(l: number, a: number, b: number): RGB {
// Convert LAB to XYZ
const fy = (l + 16) / 116;
const fx = a / 500 + fy;
const fz = fy - b / 200;
const x = (fx > 0.206897 ? Math.pow(fx, 3) : (fx - 16/116) / 7.787) * 0.95047;
const y = (fy > 0.206897 ? Math.pow(fy, 3) : (fy - 16/116) / 7.787) * 1.00000;
const z = (fz > 0.206897 ? Math.pow(fz, 3) : (fz - 16/116) / 7.787) * 1.08883;
// Convert XYZ to RGB
let r = x * 3.2404542 + y * -1.5371385 + z * -0.4985314;
let g = x * -0.9692660 + y * 1.8760108 + z * 0.0415560;
let b2 = x * 0.0556434 + y * -0.2040259 + z * 1.0572252;
// Apply inverse gamma correction
r = r > 0.0031308 ? 1.055 * Math.pow(r, 1/2.4) - 0.055 : 12.92 * r;
g = g > 0.0031308 ? 1.055 * Math.pow(g, 1/2.4) - 0.055 : 12.92 * g;
b2 = b2 > 0.0031308 ? 1.055 * Math.pow(b2, 1/2.4) - 0.055 : 12.92 * b2;
// Clamp to valid RGB range
const rFinal = Math.max(0, Math.min(255, Math.round(r * 255)));
const gFinal = Math.max(0, Math.min(255, Math.round(g * 255)));
const bFinal = Math.max(0, Math.min(255, Math.round(b2 * 255)));
return { r: rFinal, g: gFinal, b: bFinal };
}
// ==========================================================================
// LAB ↔ HCL CONVERSIONS
// ==========================================================================
/**
* Convert LAB to HCL (cylindrical representation)
* @param l Lightness (0-100)
* @param a Green-Red axis
* @param b Blue-Yellow axis
* @returns HCL color
*/
static labToHcl(l: number, a: number, b: number): HCL {
const c = Math.sqrt(a * a + b * b);
let h = Math.atan2(b, a) * (180 / Math.PI);
// Normalize hue to 0-360
if (h < 0) h += 360;
return {
h: isNaN(h) ? 0 : h,
c: isNaN(c) ? 0 : c,
l: Math.max(0, Math.min(100, l))
};
}
/**
* Convert HCL to LAB color space
* @param h Hue (0-360)
* @param c Chroma (0-100+)
* @param l Lightness (0-100)
* @returns LAB color
*/
static hclToLab(h: number, c: number, l: number): LAB {
const hRad = (h * Math.PI) / 180;
const a = c * Math.cos(hRad);
const b = c * Math.sin(hRad);
return { l, a, b };
}
// ==========================================================================
// HEX UTILITY METHODS
// ==========================================================================
/**
* Convert hex string to RGB
* @param hex Hex color string
* @returns RGB color
*/
static hexToRgb(hex: string): RGB {
// Remove # if present
hex = hex.replace('#', '');
// Handle 3-digit hex
if (hex.length === 3) {
hex = hex.split('').map(h => h + h).join('');
}
if (hex.length !== 6) {
throw new Error(`Invalid hex color: ${hex}`);
}
const r = parseInt(hex.substr(0, 2), 16);
const g = parseInt(hex.substr(2, 2), 16);
const b = parseInt(hex.substr(4, 2), 16);
return { r, g, b };
}
/**
* Convert RGB to hex string
* @param r Red component (0-255)
* @param g Green component (0-255)
* @param b Blue component (0-255)
* @returns Hex color string
*/
static rgbToHex(r: number, g: number, b: number): string {
const toHex = (n: number) => {
const hex = Math.max(0, Math.min(255, Math.round(n))).toString(16);
return hex.length === 1 ? '0' + hex : hex;
};
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
}
// ==========================================================================
// COLOR MANIPULATION METHODS
// ==========================================================================
/**
* Adjust lightness of a color
* @param hex Original color
* @param amount Lightness adjustment (-100 to 100)
* @returns Adjusted color
*/
static adjustLightness(hex: string, amount: number): string {
const hcl = this.hexToHcl(hex);
const newL = Math.max(0, Math.min(100, hcl.l + amount));
return this.hclToHex(hcl.h, hcl.c, newL);
}
/**
* Adjust chroma (saturation) of a color
* @param hex Original color
* @param amount Chroma adjustment (-100 to 100)
* @returns Adjusted color
*/
static adjustChroma(hex: string, amount: number): string {
const hcl = this.hexToHcl(hex);
const newC = Math.max(0, hcl.c + amount);
return this.hclToHex(hcl.h, newC, hcl.l);
}
/**
* Adjust hue of a color
* @param hex Original color
* @param amount Hue adjustment in degrees (-360 to 360)
* @returns Adjusted color
*/
static adjustHue(hex: string, amount: number): string {
const hcl = this.hexToHcl(hex);
let newH = hcl.h + amount;
// Normalize hue to 0-360
while (newH < 0) newH += 360;
while (newH >= 360) newH -= 360;
return this.hclToHex(newH, hcl.c, hcl.l);
}
// ==========================================================================
// VALIDATION METHODS
// ==========================================================================
/**
* Validate if a string is a valid hex color
* @param hex Color string to validate
* @returns True if valid hex color
*/
static isValidHex(hex: string): boolean {
try {
const normalizedHex = hex.replace('#', '');
return /^[0-9A-Fa-f]{3}$|^[0-9A-Fa-f]{6}$/.test(normalizedHex);
} catch {
return false;
}
}
/**
* Calculate relative luminance of a color (for contrast calculation)
* @param hex Hex color string
* @returns Relative luminance (0-1)
*/
static getRelativeLuminance(hex: string): number {
const rgb = this.hexToRgb(hex);
const [r, g, b] = [rgb.r, rgb.g, rgb.b].map(c => {
c = c / 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;
}
/**
* Calculate contrast ratio between two colors
* @param color1 First color
* @param color2 Second color
* @returns Contrast ratio (1-21)
*/
static getContrastRatio(color1: string, color2: string): number {
const lum1 = this.getRelativeLuminance(color1);
const lum2 = this.getRelativeLuminance(color2);
const lighter = Math.max(lum1, lum2);
const darker = Math.min(lum1, lum2);
return (lighter + 0.05) / (darker + 0.05);
}
}