/** HEX · RGB · HSV 변환 (색상 팔레트 UI용) */ export interface Rgb { r: number; g: number; b: number; } export interface Hsv { /** 0–360 */ h: number; /** 0–1 */ s: number; /** 0–1 */ v: number; } function clamp(n: number, min: number, max: number): number { return Math.max(min, Math.min(max, n)); } export function normalizeHex6(hex: string): string { const h = hex.trim(); if (/^#[0-9A-Fa-f]{6}$/.test(h)) return h.toUpperCase(); if (/^#[0-9A-Fa-f]{3}$/.test(h)) { const c = h.slice(1); return ('#' + c[0] + c[0] + c[1] + c[1] + c[2] + c[2]).toUpperCase(); } return '#2962FF'; } export function hexToRgb(hex: string): Rgb { const h = normalizeHex6(hex).slice(1); return { r: parseInt(h.slice(0, 2), 16), g: parseInt(h.slice(2, 4), 16), b: parseInt(h.slice(4, 6), 16), }; } export function rgbToHex(r: number, g: number, b: number): string { const to = (n: number) => clamp(Math.round(n), 0, 255).toString(16).padStart(2, '0'); return `#${to(r)}${to(g)}${to(b)}`.toUpperCase(); } export function rgbToHsv({ r, g, b }: Rgb): Hsv { const rn = r / 255; const gn = g / 255; const bn = b / 255; const max = Math.max(rn, gn, bn); const min = Math.min(rn, gn, bn); const d = max - min; let h = 0; if (d !== 0) { if (max === rn) h = ((gn - bn) / d) % 6; else if (max === gn) h = (bn - rn) / d + 2; else h = (rn - gn) / d + 4; h *= 60; if (h < 0) h += 360; } const s = max === 0 ? 0 : d / max; return { h, s, v: max }; } export function hsvToRgb({ h, s, v }: Hsv): Rgb { const c = v * s; const x = c * (1 - Math.abs(((h / 60) % 2) - 1)); const m = v - c; let rp = 0; let gp = 0; let bp = 0; if (h < 60) { rp = c; gp = x; } else if (h < 120) { rp = x; gp = c; } else if (h < 180) { gp = c; bp = x; } else if (h < 240) { gp = x; bp = c; } else if (h < 300) { rp = x; bp = c; } else { rp = c; bp = x; } return { r: Math.round((rp + m) * 255), g: Math.round((gp + m) * 255), b: Math.round((bp + m) * 255), }; } export function hexToHsv(hex: string): Hsv { return rgbToHsv(hexToRgb(hex)); } export function hsvToHex(hsv: Hsv): string { const { r, g, b } = hsvToRgb(hsv); return rgbToHex(r, g, b); } /** 채도·명도 영역 배경 (hue 고정) */ export function svPanelBackground(hue: number): string { const pure = hsvToHex({ h: hue, s: 1, v: 1 }); return `linear-gradient(to top, #000, transparent), linear-gradient(to right, #fff, ${pure})`; } /** 색상(Hue) 슬라이더 배경 */ export const HUE_SLIDER_BG = 'linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%)';