Spaces:
Running
Running
| // 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 <g>) | |
| 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 <g fill="..."> 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 <g> 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 <g fill> 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 <g> 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; | |
| } | |