/** * 채도·명도 영역 + Hue 슬라이더 + 스포이드 + RGB 입력 (TradingView 스타일) */ import React, { useCallback, useEffect, useRef, useState } from 'react'; import { hexToHsv, hexToRgb, hsvToHex, hsvToRgb, normalizeHex6, rgbToHex, rgbToHsv, svPanelBackground, HUE_SLIDER_BG, type Hsv, } from '../utils/colorSpaceUtils'; export interface AdvancedColorPickerProps { hex6: string; onChange: (hex6: string) => void; className?: string; } function clamp01(n: number): number { return Math.max(0, Math.min(1, n)); } function useHueDrag(onHue: (h: number) => void) { const dragging = useRef(false); const onPointerDown = useCallback((e: React.PointerEvent) => { dragging.current = true; e.currentTarget.setPointerCapture(e.pointerId); const rect = e.currentTarget.getBoundingClientRect(); const t = rect.width > 0 ? (e.clientX - rect.left) / rect.width : 0; onHue(clamp01(t) * 360); }, [onHue]); const onPointerMove = useCallback((e: React.PointerEvent) => { if (!dragging.current) return; const rect = e.currentTarget.getBoundingClientRect(); const t = rect.width > 0 ? (e.clientX - rect.left) / rect.width : 0; onHue(clamp01(t) * 360); }, [onHue]); const onPointerUp = useCallback((e: React.PointerEvent) => { dragging.current = false; try { e.currentTarget.releasePointerCapture(e.pointerId); } catch { /* noop */ } }, []); return { onPointerDown, onPointerMove, onPointerUp }; } const AdvancedColorPicker: React.FC = ({ hex6, onChange, className = '', }) => { const [hsv, setHsv] = useState(() => hexToHsv(normalizeHex6(hex6))); const [rgb, setRgb] = useState(() => hexToRgb(normalizeHex6(hex6))); const [colorMode, setColorMode] = useState<'rgb' | 'hex'>('rgb'); const skipSync = useRef(false); useEffect(() => { if (skipSync.current) { skipSync.current = false; return; } const h = hexToHsv(normalizeHex6(hex6)); setHsv(h); setRgb(hexToRgb(normalizeHex6(hex6))); }, [hex6]); const emitHsv = useCallback((next: Hsv) => { const h = { h: next.h, s: clamp01(next.s), v: clamp01(next.v) }; setHsv(h); const r = hsvToRgb(h); setRgb(r); skipSync.current = true; onChange(hsvToHex(h)); }, [onChange]); const emitRgb = useCallback((r: number, g: number, b: number) => { const next = { r, g, b }; setRgb(next); setHsv(rgbToHsv(next)); skipSync.current = true; onChange(rgbToHex(r, g, b)); }, [onChange]); const hueDrag = useHueDrag(h => emitHsv({ ...hsv, h })); const handleSvPointerDown = (e: React.PointerEvent) => { const el = e.currentTarget; const update = (cx: number, cy: number) => { const rect = el.getBoundingClientRect(); const s = clamp01(rect.width > 0 ? (cx - rect.left) / rect.width : 0); const v = clamp01(rect.height > 0 ? 1 - (cy - rect.top) / rect.height : 0); emitHsv({ ...hsv, s, v }); }; update(e.clientX, e.clientY); const onMove = (ev: PointerEvent) => update(ev.clientX, ev.clientY); const onUp = () => { window.removeEventListener('pointermove', onMove); window.removeEventListener('pointerup', onUp); }; window.addEventListener('pointermove', onMove); window.addEventListener('pointerup', onUp); }; const pickEyedropper = async () => { const EyeDropperCtor = (window as Window & { EyeDropper?: new () => { open: () => Promise<{ sRGBHex: string }> } }).EyeDropper; if (!EyeDropperCtor) return; try { const result = await new EyeDropperCtor().open(); if (result?.sRGBHex) { const hex = normalizeHex6(result.sRGBHex); emitHsv(hexToHsv(hex)); } } catch { /* 사용자 취소 */ } }; const pointerX = `${hsv.s * 100}%`; const pointerY = `${(1 - hsv.v) * 100}%`; const hueLeft = `${(hsv.h / 360) * 100}%`; return (
{colorMode === 'rgb' ? ( <> {(['r', 'g', 'b'] as const).map(ch => ( ))} ) : ( )}
); }; export default AdvancedColorPicker;