UNPKG

pick-distinct-colors

Version:

A collection of algorithms and utilities for analyzing and selecting maximally distinct colors. Now includes a unified pickDistinctColors API for easy color selection.

181 lines (156 loc) 5.72 kB
export function rgb2lab(rgb) { let r = rgb[0] / 255, g = rgb[1] / 255, b = rgb[2] / 255; r = r > 0.04045 ? Math.pow((r + 0.055) / 1.055, 2.4) : r / 12.92; g = g > 0.04045 ? Math.pow((g + 0.055) / 1.055, 2.4) : g / 12.92; b = b > 0.04045 ? Math.pow((b + 0.055) / 1.055, 2.4) : b / 12.92; let x = (r * 0.4124 + g * 0.3576 + b * 0.1805) * 100; let y = (r * 0.2126 + g * 0.7152 + b * 0.0722) * 100; let z = (r * 0.0193 + g * 0.1192 + b * 0.9505) * 100; x /= 95.047; y /= 100; z /= 108.883; x = x > 0.008856 ? Math.pow(x, 1/3) : (7.787 * x) + 16/116; y = y > 0.008856 ? Math.pow(y, 1/3) : (7.787 * y) + 16/116; z = z > 0.008856 ? Math.pow(z, 1/3) : (7.787 * z) + 16/116; return [(116 * y) - 16, 500 * (x - y), 200 * (y - z)]; } export function deltaE(labA, labB) { let deltaL = labA[0] - labB[0]; let deltaA = labA[1] - labB[1]; let deltaB = labA[2] - labB[2]; return Math.sqrt(deltaL * deltaL + deltaA * deltaA + deltaB * deltaB); } // Seedable PRNG (mulberry32) export function mulberry32(seed) { let t = seed >>> 0; return function() { t += 0x6D2B79F5; let r = Math.imul(t ^ t >>> 15, 1 | t); r ^= r + Math.imul(r ^ r >>> 7, 61 | r); return ((r ^ r >>> 14) >>> 0) / 4294967296; }; } /** * Generate a random RGB color using the provided PRNG. * @param {function} [prng=Math.random] - Optional PRNG function. * @returns {number[]} RGB color [r, g, b] */ export function randomColor(prng = Math.random) { return [ Math.floor(prng() * 256), Math.floor(prng() * 256), Math.floor(prng() * 256) ]; } export function sortColors(colors) { const labColors = colors.map(rgb2lab); const indices = Array.from({length: colors.length}, (_, i) => i); // Sort by L, then a, then b indices.sort((i, j) => { const [L1, a1, b1] = labColors[i]; const [L2, a2, b2] = labColors[j]; if (L1 !== L2) return L2 - L1; if (a1 !== a2) return a2 - a1; return b2 - b1; }); return indices.map(i => colors[i]); } export function calculateMetrics(colors) { const labColors = colors.map(rgb2lab); let minDist = Infinity; let maxDist = -Infinity; let sumDist = 0; let count = 0; for (let i = 0; i < colors.length - 1; i++) { for (let j = i + 1; j < colors.length; j++) { const dist = deltaE(labColors[i], labColors[j]); minDist = Math.min(minDist, dist); maxDist = Math.max(maxDist, dist); sumDist += dist; count++; } } return { min: minDist, max: maxDist, avg: sumDist / count, sum: sumDist }; } export function analyzeColorDistribution(colors) { if (!colors || colors.length === 0) return 'No colors to analyze'; try { const labColors = colors.map(rgb2lab); // Initialize stats with first color const stats = { L: { min: labColors[0][0], max: labColors[0][0], range: [0, 100] }, a: { min: labColors[0][1], max: labColors[0][1], range: [-128, 127] }, b: { min: labColors[0][2], max: labColors[0][2], range: [-128, 127] } }; // Process colors in chunks const chunkSize = 500; for (let i = 1; i < labColors.length; i += chunkSize) { const chunk = labColors.slice(i, i + chunkSize); for (const lab of chunk) { stats.L.min = Math.min(stats.L.min, lab[0]); stats.L.max = Math.max(stats.L.max, lab[0]); stats.a.min = Math.min(stats.a.min, lab[1]); stats.a.max = Math.max(stats.a.max, lab[1]); stats.b.min = Math.min(stats.b.min, lab[2]); stats.b.max = Math.max(stats.b.max, lab[2]); } } // Calculate coverage percentages const coverage = { L: ((stats.L.max - stats.L.min) / (stats.L.range[1] - stats.L.range[0]) * 100).toFixed(1), a: ((stats.a.max - stats.a.min) / (stats.a.range[1] - stats.a.range[0]) * 100).toFixed(1), b: ((stats.b.max - stats.b.min) / (stats.b.range[1] - stats.b.range[0]) * 100).toFixed(1) }; return ` <strong>Color Space Coverage:</strong><br> Lightness (L*): ${coverage.L}%<br> Green-Red (a*): ${coverage.a}%<br> Blue-Yellow (b*): ${coverage.b}% `; } catch (error) { console.error('Error in analyzeColorDistribution:', error); return 'Error analyzing color distribution'; } } export function rgbToHex(rgb) { return '#' + rgb.map(x => { const hex = x.toString(16); return hex.length === 1 ? '0' + hex : hex; }).join(''); } export function calculateDistanceMatrix(colors) { const labColors = colors.map(rgb2lab); const matrix = []; for (let i = 0; i < colors.length; i++) { matrix[i] = []; for (let j = 0; j < colors.length; j++) { matrix[i][j] = deltaE(labColors[i], labColors[j]); } } return matrix; } export function findClosestPair(colors) { const labColors = colors.map(rgb2lab); let minDist = Infinity; let closestPair = [0, 1]; for (let i = 0; i < colors.length; i++) { for (let j = i + 1; j < colors.length; j++) { const dist = deltaE(labColors[i], labColors[j]); if (dist < minDist) { minDist = dist; closestPair = [i, j]; } } } return { colors: [colors[closestPair[0]], colors[closestPair[1]]], distance: minDist }; }