import { useRef, useState, useCallback } from "react"; import { OKLCH_L, OKLCH_C } from "#shared/theme"; const COLOR_NAMES = [ { name: "Candy Apple Red", hue: 0 }, { name: "Boiling Magma", hue: 15 }, { name: "Aerospace Orange", hue: 25 }, { name: "Burtuqali Orange", hue: 33 }, { name: "American Orange", hue: 40 }, { name: "Cheese", hue: 48 }, { name: "Amber", hue: 55 }, { name: "Demonic Yellow", hue: 65 }, { name: "Bat-Signal", hue: 72 }, { name: "Bright Yellow Green", hue: 85 }, { name: "Lasting Lime", hue: 95 }, { name: "Bright Green", hue: 110 }, { name: "Chlorophyll Green", hue: 120 }, { name: "Green Screen", hue: 130 }, { name: "Cathode Green", hue: 148 }, { name: "Booger Buster", hue: 160 }, { name: "Enthusiasm", hue: 170 }, { name: "Ice Ice Baby", hue: 180 }, { name: "Vivid Sky Blue", hue: 195 }, { name: "Azure", hue: 214 }, { name: "Blue Ribbon", hue: 225 }, { name: "Icelandic Water", hue: 238 }, { name: "Blue Pencil", hue: 250 }, { name: "Electric Ultramarine", hue: 260 }, { name: "Purple Climax", hue: 275 }, { name: "Amethyst Ganzstar", hue: 285 }, { name: "Electric Purple", hue: 295 }, { name: "Phlox", hue: 305 }, { name: "Brusque Pink", hue: 315 }, { name: "Bright Magenta", hue: 325 }, { name: "Pink", hue: 335 }, { name: "Hot Flamingoes", hue: 345 }, { name: "Carmine Red", hue: 355 }, ]; function getColorName(hue: number): string { let best = COLOR_NAMES[0].name; let bestDist = 361; for (const c of COLOR_NAMES) { const d = Math.abs(c.hue - hue); const dist = Math.min(d, 360 - d); if (dist < bestDist) { bestDist = dist; best = c.name; } } return best; } function oklchToHex(l: number, c: number, h: number): string { const hRad = (h * Math.PI) / 180; const a = c * Math.cos(hRad); const b = c * Math.sin(hRad); const l_ = l + 0.3963377774 * a + 0.2158037573 * b; const m_ = l - 0.1055613458 * a - 0.0638541728 * b; const s_ = l - 0.0894841775 * a - 1.291485548 * b; const ll = l_ * l_ * l_; const mm = m_ * m_ * m_; const ss = s_ * s_ * s_; const toSrgb = (u: number) => u <= 0.0031308 ? 12.92 * u : 1.055 * Math.pow(Math.max(0, u), 1 / 2.4) - 0.055; const r = toSrgb(+4.0767416621 * ll - 3.3077115913 * mm + 0.2309699292 * ss); const g = toSrgb(-1.2684380046 * ll + 2.6097574011 * mm - 0.3413193965 * ss); const bVal = toSrgb(-0.0041960863 * ll - 0.7034186147 * mm + 1.707614701 * ss); const clamp = (v: number) => Math.max(0, Math.min(255, Math.round(v * 255))); const hex = (v: number) => clamp(v).toString(16).padStart(2, "0"); return `#${hex(r)}${hex(g)}${hex(bVal)}`; } interface HueSliderProps { hue: number; onChange: (hue: number) => void; } export function HueSlider({ hue, onChange }: HueSliderProps) { const sliderRef = useRef(null); const [dragging, setDragging] = useState(false); const hexColor = oklchToHex(OKLCH_L, OKLCH_C, hue); const colorName = getColorName(hue); const hueFromClientX = useCallback((clientX: number) => { const rect = sliderRef.current?.getBoundingClientRect(); if (!rect) return hue; const x = Math.max(0, Math.min(clientX - rect.left, rect.width)); return (x / rect.width) * 360; }, [hue]); const handlePointerDown = useCallback((e: React.PointerEvent) => { e.preventDefault(); (e.target as HTMLElement).setPointerCapture(e.pointerId); setDragging(true); onChange(hueFromClientX(e.clientX)); }, [hueFromClientX, onChange]); const handlePointerMove = useCallback((e: React.PointerEvent) => { if (!dragging) return; onChange(hueFromClientX(e.clientX)); }, [dragging, hueFromClientX, onChange]); const handlePointerUp = useCallback(() => { setDragging(false); }, []); const handleKeyDown = useCallback((e: React.KeyboardEvent) => { const step = e.shiftKey ? 10 : 2; if (e.key === "ArrowLeft") { e.preventDefault(); onChange(((hue - step) % 360 + 360) % 360); } if (e.key === "ArrowRight") { e.preventDefault(); onChange((hue + step) % 360); } }, [hue, onChange]); return (
{colorName}
{hexColor.toUpperCase()} - {Math.round(hue)}°
); } export { oklchToHex };