103 lines
2.6 KiB
TypeScript
103 lines
2.6 KiB
TypeScript
/** 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%)';
|