import { browser } from '$app/environment'; function srgbToLinear(channel: number): number { const c = channel / 255; return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4); } function rgbToOklch(r: number, g: number, b: number): [number, number, number] { const lr = srgbToLinear(r); const lg = srgbToLinear(g); const lb = srgbToLinear(b); const l = 0.4122214708 * lr + 0.5363325363 * lg + 0.0514459929 * lb; const m = 0.2119034982 * lr + 0.6806995451 * lg + 0.1073969566 * lb; const s = 0.0883024619 * lr + 0.2817188376 * lg + 0.6299787005 * lb; const l_ = Math.cbrt(l); const m_ = Math.cbrt(m); const s_ = Math.cbrt(s); const L = 0.2104542553 * l_ + 0.793617785 * m_ - 0.0040720468 * s_; const a = 1.9779984951 * l_ - 2.428592205 * m_ + 0.4505937099 * s_; const bb = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.808675766 * s_; const C = Math.sqrt(a * a + bb * bb); const H = (Math.atan2(bb, a) * 180) / Math.PI; return [L, C, (H + 360) % 360]; } function loadImage(url: string): Promise { return new Promise((resolve, reject) => { const img = new Image(); img.crossOrigin = 'anonymous'; img.onload = () => resolve(img); img.onerror = reject; img.src = url; }); } const BUCKETS = 36; export async function extractAccentHue(url: string): Promise { if (!browser) return null; let img: HTMLImageElement; try { img = await loadImage(url); } catch { return null; } const size = 40; const canvas = document.createElement('canvas'); canvas.width = size; canvas.height = size; const ctx = canvas.getContext('2d', { willReadFrequently: true }); if (!ctx) return null; ctx.drawImage(img, 0, 0, size, size); let pixels: Uint8ClampedArray; try { pixels = ctx.getImageData(0, 0, size, size).data; } catch { return null; } const weight = new Array(BUCKETS).fill(0); const hueSin = new Array(BUCKETS).fill(0); const hueCos = new Array(BUCKETS).fill(0); for (let i = 0; i < pixels.length; i += 4) { if (pixels[i + 3] < 128) continue; const [L, C, H] = rgbToOklch(pixels[i], pixels[i + 1], pixels[i + 2]); if (L < 0.2 || L > 0.92 || C < 0.03) continue; const bucket = Math.floor((H / 360) * BUCKETS) % BUCKETS; const w = C * C; const rad = (H * Math.PI) / 180; weight[bucket] += w; hueSin[bucket] += Math.sin(rad) * w; hueCos[bucket] += Math.cos(rad) * w; } let best = -1; let bestWeight = 0; for (let b = 0; b < BUCKETS; b++) { if (weight[b] > bestWeight) { bestWeight = weight[b]; best = b; } } if (best < 0) return null; return ((Math.atan2(hueSin[best], hueCos[best]) * 180) / Math.PI + 360) % 360; }