// sRGB → Linear function srgbToLinear(c) { return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4); } // RGB [0-255] → XYZ function rgbToXyz(r, g, b) { const rl = srgbToLinear(r / 255); const gl = srgbToLinear(g / 255); const bl = srgbToLinear(b / 255); return [ 0.4124564 * rl + 0.3575761 * gl + 0.1804375 * bl, 0.2126729 * rl + 0.7151522 * gl + 0.0721750 * bl, 0.0193339 * rl + 0.1191920 * gl + 0.9503041 * bl, ]; } // XYZ → CIELAB function xyzToLab(x, y, z) { const Xn = 0.95047, Yn = 1.0, Zn = 1.08883; const f = (v) => (v > 0.008856 ? Math.cbrt(v) : 7.787 * v + 16 / 116); const fx = f(x / Xn), fy = f(y / Yn), fz = f(z / Zn); return [116 * fy - 16, 500 * (fx - fy), 200 * (fy - fz)]; } export function rgbToLab(r, g, b) { const [x, y, z] = rgbToXyz(r, g, b); return xyzToLab(x, y, z); } export function labDist(a, b) { const dL = a[0] - b[0], da = a[1] - b[1], db = a[2] - b[2]; return Math.sqrt(dL * dL + da * da + db * db); } // Parse any CSS color string → { r, g, b, hex } const parseCtx = typeof document !== 'undefined' ? document.createElement('canvas').getContext('2d') : null; export function parseColor(str) { if (!str || str === 'none' || str === 'transparent' || str === 'inherit' || str === 'currentColor') return null; if (str.startsWith('url(')) return null; if (!parseCtx) return null; parseCtx.fillStyle = '#000000'; parseCtx.fillStyle = str; const hex = parseCtx.fillStyle; if (hex.startsWith('#')) { const n = parseInt(hex.slice(1), 16); return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255, hex }; } const m = hex.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); if (m) { const r = +m[1], g = +m[2], b = +m[3]; const h = '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); return { r, g, b, hex: h }; } return null; } // Extract unique colors from SVG element (all elements including ) export function extractColorsFromSVG(svgEl) { const colorMap = new Map(); const elements = svgEl.querySelectorAll('*'); for (const el of elements) { const fill = el.getAttribute('fill'); const stroke = el.getAttribute('stroke'); const style = el.getAttribute('style') || ''; const styleFill = style.match(/fill\s*:\s*([^;]+)/); const styleStroke = style.match(/stroke\s*:\s*([^;]+)/); for (const raw of [fill, stroke, styleFill?.[1], styleStroke?.[1]]) { if (!raw) continue; const c = parseColor(raw.trim()); if (c) colorMap.set(c.hex, c); } } return Array.from(colorMap.values()); } // Extract structural color groups from SVG: // - Fill anchor colors from elements // - Stroke colors from individual paths // Returns { anchors: [{hex, r, g, b}], strokes: [{hex, r, g, b}] } or null if no structure export function extractStructuralGroups(svgEl) { const anchors = new Map(); // fill colors on elements const strokes = new Map(); // stroke colors on leaf elements const gElements = svgEl.querySelectorAll('g'); for (const g of gElements) { const fill = g.getAttribute('fill'); if (fill) { const c = parseColor(fill.trim()); if (c) anchors.set(c.hex, c); } } // If no structure found, return null if (anchors.size < 2) return null; const allElements = svgEl.querySelectorAll('*'); for (const el of allElements) { if (el.tagName === 'g' || el.tagName === 'svg' || el.tagName === 'defs') continue; const stroke = el.getAttribute('stroke'); if (stroke) { const c = parseColor(stroke.trim()); if (c && !anchors.has(c.hex)) strokes.set(c.hex, c); } const fill = el.getAttribute('fill'); if (fill) { const c = parseColor(fill.trim()); if (c && !anchors.has(c.hex)) strokes.set(c.hex, c); } } return { anchors: Array.from(anchors.values()), strokes: Array.from(strokes.values()), }; } // Get the color directly set on this element (no inheritance) export function getOwnColor(el, attr) { const style = el.getAttribute('style') || ''; const styleMatch = style.match(new RegExp(attr + '\\s*:\\s*([^;]+)')); if (styleMatch) { const c = parseColor(styleMatch[1].trim()); return c ? c.hex : null; } const val = el.getAttribute(attr); if (val) { const c = parseColor(val.trim()); return c ? c.hex : null; } return null; } // Get effective color including inheritance from parent elements export function getEffectiveColor(el, attr) { let node = el; while (node && node.nodeType === 1) { const color = getOwnColor(node, attr); if (color) return color; const val = node.getAttribute(attr); if (val === 'none') return null; node = node.parentNode; } return null; }