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.

1 lines 96.6 kB
{"version":3,"file":"index.cjs","sources":["../js/utils/colorUtils.js","../js/utils/securityLimits.js","../js/algorithms/maxSumDistances.js","../js/algorithms/greedy.js","../js/algorithms/kmeans.js","../js/algorithms/simulatedAnnealing.js","../js/algorithms/genetic.js","../js/algorithms/particleSwarm.js","../js/algorithms/antColony.js","../js/algorithms/tabu.js","../js/algorithms/exactMaximum.js","../js/algorithms/exactMinimum.js","../js/algorithms/random.js","../js/utils/pickDistinctColors.js"],"sourcesContent":["export function rgb2lab(rgb) {\n let r = rgb[0] / 255,\n g = rgb[1] / 255,\n b = rgb[2] / 255;\n\n r = r > 0.04045 ? Math.pow((r + 0.055) / 1.055, 2.4) : r / 12.92;\n g = g > 0.04045 ? Math.pow((g + 0.055) / 1.055, 2.4) : g / 12.92;\n b = b > 0.04045 ? Math.pow((b + 0.055) / 1.055, 2.4) : b / 12.92;\n\n let x = (r * 0.4124 + g * 0.3576 + b * 0.1805) * 100;\n let y = (r * 0.2126 + g * 0.7152 + b * 0.0722) * 100;\n let z = (r * 0.0193 + g * 0.1192 + b * 0.9505) * 100;\n\n x /= 95.047;\n y /= 100;\n z /= 108.883;\n\n x = x > 0.008856 ? Math.pow(x, 1/3) : (7.787 * x) + 16/116;\n y = y > 0.008856 ? Math.pow(y, 1/3) : (7.787 * y) + 16/116;\n z = z > 0.008856 ? Math.pow(z, 1/3) : (7.787 * z) + 16/116;\n\n return [(116 * y) - 16, 500 * (x - y), 200 * (y - z)];\n}\n\nexport function deltaE(labA, labB) {\n let deltaL = labA[0] - labB[0];\n let deltaA = labA[1] - labB[1];\n let deltaB = labA[2] - labB[2];\n return Math.sqrt(deltaL * deltaL + deltaA * deltaA + deltaB * deltaB);\n}\n\n// Seedable PRNG (mulberry32)\nexport function mulberry32(seed) {\n let t = seed >>> 0;\n return function() {\n t += 0x6D2B79F5;\n let r = Math.imul(t ^ t >>> 15, 1 | t);\n r ^= r + Math.imul(r ^ r >>> 7, 61 | r);\n return ((r ^ r >>> 14) >>> 0) / 4294967296;\n };\n}\n\n/**\n * Generate a random RGB color using the provided PRNG.\n * @param {function} [prng=Math.random] - Optional PRNG function.\n * @returns {number[]} RGB color [r, g, b]\n */\nexport function randomColor(prng = Math.random) {\n return [\n Math.floor(prng() * 256),\n Math.floor(prng() * 256),\n Math.floor(prng() * 256)\n ];\n}\n\nexport function sortColors(colors) {\n const labColors = colors.map(rgb2lab);\n const indices = Array.from({length: colors.length}, (_, i) => i);\n \n // Sort by L, then a, then b\n indices.sort((i, j) => {\n const [L1, a1, b1] = labColors[i];\n const [L2, a2, b2] = labColors[j];\n if (L1 !== L2) return L2 - L1;\n if (a1 !== a2) return a2 - a1;\n return b2 - b1;\n });\n \n return indices.map(i => colors[i]);\n}\n\nexport function calculateMetrics(colors) {\n const labColors = colors.map(rgb2lab);\n let minDist = Infinity;\n let maxDist = -Infinity;\n let sumDist = 0;\n let count = 0;\n \n for (let i = 0; i < colors.length - 1; i++) {\n for (let j = i + 1; j < colors.length; j++) {\n const dist = deltaE(labColors[i], labColors[j]);\n minDist = Math.min(minDist, dist);\n maxDist = Math.max(maxDist, dist);\n sumDist += dist;\n count++;\n }\n }\n \n return {\n min: minDist,\n max: maxDist,\n avg: sumDist / count,\n sum: sumDist\n };\n}\n\nexport function analyzeColorDistribution(colors) {\n if (!colors || colors.length === 0) return 'No colors to analyze';\n \n try {\n const labColors = colors.map(rgb2lab);\n \n // Initialize stats with first color\n const stats = {\n L: { min: labColors[0][0], max: labColors[0][0], range: [0, 100] },\n a: { min: labColors[0][1], max: labColors[0][1], range: [-128, 127] },\n b: { min: labColors[0][2], max: labColors[0][2], range: [-128, 127] }\n };\n \n // Process colors in chunks\n const chunkSize = 500;\n for (let i = 1; i < labColors.length; i += chunkSize) {\n const chunk = labColors.slice(i, i + chunkSize);\n for (const lab of chunk) {\n stats.L.min = Math.min(stats.L.min, lab[0]);\n stats.L.max = Math.max(stats.L.max, lab[0]);\n stats.a.min = Math.min(stats.a.min, lab[1]);\n stats.a.max = Math.max(stats.a.max, lab[1]);\n stats.b.min = Math.min(stats.b.min, lab[2]);\n stats.b.max = Math.max(stats.b.max, lab[2]);\n }\n }\n \n // Calculate coverage percentages\n const coverage = {\n L: ((stats.L.max - stats.L.min) / (stats.L.range[1] - stats.L.range[0]) * 100).toFixed(1),\n a: ((stats.a.max - stats.a.min) / (stats.a.range[1] - stats.a.range[0]) * 100).toFixed(1),\n b: ((stats.b.max - stats.b.min) / (stats.b.range[1] - stats.b.range[0]) * 100).toFixed(1)\n };\n \n return `\n <strong>Color Space Coverage:</strong><br>\n Lightness (L*): ${coverage.L}%<br>\n Green-Red (a*): ${coverage.a}%<br>\n Blue-Yellow (b*): ${coverage.b}%\n `;\n } catch (error) {\n console.error('Error in analyzeColorDistribution:', error);\n return 'Error analyzing color distribution';\n }\n}\n\nexport function rgbToHex(rgb) {\n return '#' + rgb.map(x => {\n const hex = x.toString(16);\n return hex.length === 1 ? '0' + hex : hex;\n }).join('');\n}\n\nexport function calculateDistanceMatrix(colors) {\n const labColors = colors.map(rgb2lab);\n const matrix = [];\n for (let i = 0; i < colors.length; i++) {\n matrix[i] = [];\n for (let j = 0; j < colors.length; j++) {\n matrix[i][j] = deltaE(labColors[i], labColors[j]);\n }\n }\n return matrix;\n}\n\nexport function findClosestPair(colors) {\n const labColors = colors.map(rgb2lab);\n let minDist = Infinity;\n let closestPair = [0, 1];\n \n for (let i = 0; i < colors.length; i++) {\n for (let j = i + 1; j < colors.length; j++) {\n const dist = deltaE(labColors[i], labColors[j]);\n if (dist < minDist) {\n minDist = dist;\n closestPair = [i, j];\n }\n }\n }\n \n return {\n colors: [colors[closestPair[0]], colors[closestPair[1]]],\n distance: minDist\n };\n} ","export const SECURITY_LIMITS = {\n MAX_GENERATED_POOL_SIZE: 1024,\n MAX_COLOR_POOL_SIZE: 4096,\n MAX_SELECT_COUNT: 256,\n MAX_EXACT_COMBINATIONS: 1000000,\n MAX_PAIRWISE_COLORS: 1024,\n MAX_POPULATION_SIZE: 500,\n MAX_GENERATIONS: 1000,\n MAX_PARTICLES: 200,\n MAX_ANTS: 200,\n MAX_ITERATIONS: 2000,\n MAX_WORK_UNITS: 20000000\n};\n\nexport function normalizePositiveInteger(value, name, max = Number.MAX_SAFE_INTEGER) {\n if (!Number.isSafeInteger(value) || value < 1) {\n throw new RangeError(`${name} must be a positive safe integer`);\n }\n if (value > max) {\n throw new RangeError(`${name} must be less than or equal to ${max}`);\n }\n return value;\n}\n\nexport function validateColorPool(colors, maxColors = SECURITY_LIMITS.MAX_COLOR_POOL_SIZE) {\n if (!Array.isArray(colors) || colors.length === 0) {\n throw new TypeError('colors must be a non-empty array of RGB triples');\n }\n if (colors.length > maxColors) {\n throw new RangeError(`colors must contain no more than ${maxColors} entries`);\n }\n for (const color of colors) {\n if (!Array.isArray(color) || color.length !== 3 ||\n !color.every(component => Number.isInteger(component) && component >= 0 && component <= 255)) {\n throw new TypeError('each color must be an RGB triple with integer components between 0 and 255');\n }\n }\n}\n\nexport function validateSelectionArgs(colors, selectCount, options = {}) {\n const maxColors = options.maxColors ?? SECURITY_LIMITS.MAX_COLOR_POOL_SIZE;\n const maxSelectCount = options.maxSelectCount ?? SECURITY_LIMITS.MAX_SELECT_COUNT;\n validateColorPool(colors, maxColors);\n normalizePositiveInteger(selectCount, 'selectCount', maxSelectCount);\n if (selectCount > colors.length) {\n throw new RangeError('selectCount cannot be greater than the number of colors');\n }\n}\n\nexport function countCombinationsOverLimit(n, k, limit) {\n if (k < 0 || k > n) {\n return false;\n }\n const r = Math.min(k, n - k);\n let combinations = 1;\n for (let i = 1; i <= r; i++) {\n combinations = (combinations * (n - r + i)) / i;\n if (combinations > limit) {\n return true;\n }\n }\n return false;\n}\n\nexport function validateExactSelectionArgs(colors, selectCount) {\n validateSelectionArgs(colors, selectCount);\n if (countCombinationsOverLimit(colors.length, selectCount, SECURITY_LIMITS.MAX_EXACT_COMBINATIONS)) {\n throw new RangeError(\n `exact algorithms are limited to ${SECURITY_LIMITS.MAX_EXACT_COMBINATIONS} combinations`\n );\n }\n}\n\nexport function validatePairwiseSelectionArgs(colors, selectCount) {\n validateSelectionArgs(colors, selectCount, { maxColors: SECURITY_LIMITS.MAX_PAIRWISE_COLORS });\n}\n\nexport function validateWorkUnits(name, terms, maxWorkUnits = SECURITY_LIMITS.MAX_WORK_UNITS) {\n const workUnits = terms.reduce((product, term) => product * term, 1);\n if (!Number.isFinite(workUnits) || workUnits > maxWorkUnits) {\n throw new RangeError(`${name} estimated work must be less than or equal to ${maxWorkUnits}`);\n }\n}\n","import { rgb2lab, deltaE, sortColors, mulberry32 } from '../utils/colorUtils.js';\nimport { validatePairwiseSelectionArgs, validateSelectionArgs } from '../utils/securityLimits.js';\n\nexport function maxSumDistancesGlobal(colors, selectCount) {\n validatePairwiseSelectionArgs(colors, selectCount);\n return new Promise((resolve, reject) => {\n // Create worker code with utility functions in scope\n const workerCode = `\n // Import utility functions from parent\n const rgb2lab = ${rgb2lab.toString()};\n const deltaE = ${deltaE.toString()};\n const sortColors = ${sortColors.toString()};\n\n // Main worker function\n function maxSumDistancesGlobal(colors, selectCount) {\n const start = performance.now();\n const labColors = colors.map(rgb2lab);\n\n // Calculate total distances from each color to all other colors\n const totalDistances = colors.map((_, i) => {\n let sum = 0;\n for (let j = 0; j < colors.length; j++) {\n if (i !== j) {\n sum += deltaE(labColors[i], labColors[j]);\n }\n }\n return { index: i, sum };\n });\n\n // Sort colors by their total distance to all other colors\n totalDistances.sort((a, b) => b.sum - a.sum);\n\n // Take the top selectCount colors that have the highest total distances\n const selectedIndices = totalDistances.slice(0, selectCount).map(item => item.index);\n const selectedColors = selectedIndices.map(i => colors[i]);\n\n return {\n colors: sortColors(selectedColors),\n time: performance.now() - start\n };\n }\n\n // Worker message handler\n self.onmessage = function(e) {\n const { colors, selectCount } = e.data;\n try {\n const result = maxSumDistancesGlobal(colors, selectCount);\n self.postMessage({ type: 'complete', result });\n } catch (error) {\n self.postMessage({ type: 'error', error: error.message });\n }\n };\n `;\n\n // Create blob and worker\n const blob = new Blob([workerCode], { type: 'application/javascript' });\n const workerUrl = URL.createObjectURL(blob);\n const worker = new Worker(workerUrl);\n\n function cleanup() {\n worker.terminate();\n URL.revokeObjectURL(workerUrl);\n }\n\n // Set up worker message handlers\n worker.onmessage = function(e) {\n if (e.data.type === 'complete') {\n resolve(e.data.result);\n } else if (e.data.type === 'error') {\n reject(new Error(e.data.error));\n }\n cleanup();\n };\n\n worker.onerror = function(error) {\n reject(error);\n cleanup();\n };\n\n // Start the worker\n try {\n worker.postMessage({ colors, selectCount });\n } catch (error) {\n cleanup();\n reject(error);\n }\n });\n}\n\nexport function maxSumDistancesSequential(colors, selectCount, seed) {\n console.log('Starting Maximum Sum (Sequential) calculation...');\n const start = performance.now();\n validateSelectionArgs(colors, selectCount);\n\n const labColors = colors.map(rgb2lab);\n const selected = [];\n const available = Array.from({length: colors.length}, (_, i) => i);\n\n // Use seeded PRNG if seed is provided\n const prng = typeof seed === 'number' ? mulberry32(seed) : Math.random;\n\n // Helper function to calculate total distance from a point to selected points\n function calculateTotalDistance(index) {\n return selected.reduce((sum, selectedIndex) =>\n sum + deltaE(labColors[index], labColors[selectedIndex]), 0);\n }\n\n // Select first point randomly\n const firstIndex = Math.floor(prng() * available.length);\n selected.push(available[firstIndex]);\n available.splice(firstIndex, 1);\n\n // Select remaining points\n while (selected.length < selectCount) {\n let bestIndex = 0;\n let bestDistance = -Infinity;\n\n // Find point with maximum sum of distances to selected points\n for (let i = 0; i < available.length; i++) {\n const totalDistance = calculateTotalDistance(available[i]);\n if (totalDistance > bestDistance) {\n bestDistance = totalDistance;\n bestIndex = i;\n }\n }\n\n selected.push(available[bestIndex]);\n available.splice(bestIndex, 1);\n }\n\n return {\n colors: sortColors(selected.map(i => colors[i])),\n time: performance.now() - start\n };\n}\n","import { rgb2lab, deltaE, sortColors, mulberry32 } from '../utils/colorUtils.js';\nimport { validateSelectionArgs } from '../utils/securityLimits.js';\n\nexport function greedySelection(colors, selectCount, seed) {\n console.log('Starting Greedy calculation...');\n const start = performance.now();\n validateSelectionArgs(colors, selectCount);\n\n const labColors = colors.map(rgb2lab);\n const selected = [];\n const available = Array.from({length: colors.length}, (_, i) => i);\n\n // Use seeded PRNG if seed is provided\n const prng = typeof seed === 'number' ? mulberry32(seed) : Math.random;\n\n // Helper function to calculate minimum distance from a point to selected points\n function calculateMinDistance(index) {\n if (selected.length === 0) return Infinity;\n return Math.min(...selected.map(selectedIndex =>\n deltaE(labColors[index], labColors[selectedIndex])\n ));\n }\n\n // Select first point randomly\n const firstIndex = Math.floor(prng() * available.length);\n selected.push(available[firstIndex]);\n available.splice(firstIndex, 1);\n\n // Select remaining points\n while (selected.length < selectCount) {\n let bestIndex = 0;\n let bestMinDistance = -Infinity;\n\n // Find point with maximum minimum distance to selected points\n for (let i = 0; i < available.length; i++) {\n const minDistance = calculateMinDistance(available[i]);\n if (minDistance > bestMinDistance) {\n bestMinDistance = minDistance;\n bestIndex = i;\n }\n }\n\n selected.push(available[bestIndex]);\n available.splice(bestIndex, 1);\n }\n\n return {\n colors: sortColors(selected.map(i => colors[i])),\n time: performance.now() - start\n };\n}\n","import { rgb2lab, deltaE, sortColors, mulberry32 } from '../utils/colorUtils.js';\nimport { validateSelectionArgs } from '../utils/securityLimits.js';\n\nexport function kmeansppSelection(colors, selectCount, seed) {\n console.log('Starting K-means++ calculation...');\n const start = performance.now();\n validateSelectionArgs(colors, selectCount);\n\n const labColors = colors.map(rgb2lab);\n\n // Use seeded PRNG if seed is provided\n const prng = typeof seed === 'number' ? mulberry32(seed) : Math.random;\n\n // Helper function to find minimum distance to existing centers\n function minDistanceToCenters(point, centers) {\n if (centers.length === 0) return Infinity;\n return Math.min(...centers.map(center =>\n deltaE(labColors[point], labColors[center])\n ));\n }\n\n // Select initial center randomly\n const selected = [Math.floor(prng() * colors.length)];\n\n // Select remaining centers using k-means++ initialization\n while (selected.length < selectCount) {\n const distances = Array.from({length: colors.length}, (_, i) => {\n if (selected.includes(i)) return 0;\n const dist = minDistanceToCenters(i, selected);\n return dist * dist; // Square distances for k-means++\n });\n\n const sum = distances.reduce((a, b) => a + b, 0);\n let random = prng() * sum;\n let selectedIndex = 0;\n\n while (random > 0 && selectedIndex < distances.length) {\n if (!selected.includes(selectedIndex)) {\n random -= distances[selectedIndex];\n }\n if (random > 0) selectedIndex++;\n }\n\n selected.push(selectedIndex);\n }\n\n return {\n colors: sortColors(selected.map(i => colors[i])),\n time: performance.now() - start\n };\n}\n","import { rgb2lab, deltaE, sortColors, mulberry32 } from '../utils/colorUtils.js';\nimport {\n SECURITY_LIMITS,\n normalizePositiveInteger,\n validateSelectionArgs,\n validateWorkUnits\n} from '../utils/securityLimits.js';\n\nexport function simulatedAnnealing(colors, selectCount, settings = {}) {\n console.log('Starting Simulated Annealing calculation...');\n const start = performance.now();\n validateSelectionArgs(colors, selectCount);\n\n const labColors = colors.map(rgb2lab);\n const maxIterations = normalizePositiveInteger(settings.maxIterations ?? 1000, 'maxIterations', SECURITY_LIMITS.MAX_ITERATIONS);\n validateWorkUnits('simulatedAnnealing', [maxIterations, selectCount, selectCount]);\n const initialTemp = settings.initialTemp ?? 1000;\n const coolingRate = settings.coolingRate ?? 0.995;\n const minTemp = settings.minTemp ?? 0.1;\n\n // Use seeded PRNG if settings.seed is provided\n const prng = typeof settings.seed === 'number' ? mulberry32(settings.seed) : Math.random;\n\n // Helper function to calculate minimum distance between selected colors\n function calculateFitness(selection) {\n let minDist = Infinity;\n for (let i = 0; i < selection.length - 1; i++) {\n for (let j = i + 1; j < selection.length; j++) {\n const dist = deltaE(labColors[selection[i]], labColors[selection[j]]);\n minDist = Math.min(minDist, dist);\n }\n }\n return minDist;\n }\n\n // Generate initial solution\n let currentSolution = Array.from({length: colors.length}, (_, i) => i)\n .sort(() => prng() - 0.5)\n .slice(0, selectCount);\n let currentFitness = calculateFitness(currentSolution);\n\n let bestSolution = [...currentSolution];\n let bestFitness = currentFitness;\n\n let temperature = initialTemp;\n\n // Main loop\n for (let i = 0; i < maxIterations && temperature > minTemp; i++) {\n // Generate neighbor by swapping one selected color with an unselected one\n const neighborSolution = [...currentSolution];\n const swapIndex = Math.floor(prng() * selectCount);\n const availableIndices = Array.from({length: colors.length}, (_, i) => i)\n .filter(i => !currentSolution.includes(i));\n const newIndex = availableIndices[Math.floor(prng() * availableIndices.length)];\n neighborSolution[swapIndex] = newIndex;\n\n const neighborFitness = calculateFitness(neighborSolution);\n\n // Decide if we should accept the neighbor\n const delta = neighborFitness - currentFitness;\n if (delta > 0 || prng() < Math.exp(delta / temperature)) {\n currentSolution = neighborSolution;\n currentFitness = neighborFitness;\n\n if (currentFitness > bestFitness) {\n bestSolution = [...currentSolution];\n bestFitness = currentFitness;\n }\n }\n\n temperature *= coolingRate;\n }\n\n return {\n colors: sortColors(bestSolution.map(i => colors[i])),\n time: performance.now() - start\n };\n}\n","import { rgb2lab, deltaE, sortColors, mulberry32 } from '../utils/colorUtils.js';\nimport {\n SECURITY_LIMITS,\n normalizePositiveInteger,\n validateSelectionArgs,\n validateWorkUnits\n} from '../utils/securityLimits.js';\n\nexport function geneticAlgorithm(colors, selectCount, settings = {}) {\n console.log('Starting Genetic Algorithm calculation...');\n const start = performance.now();\n validateSelectionArgs(colors, selectCount);\n\n const labColors = colors.map(rgb2lab);\n const populationSize = normalizePositiveInteger(settings.populationSize ?? 100, 'populationSize', SECURITY_LIMITS.MAX_POPULATION_SIZE);\n const generations = normalizePositiveInteger(settings.generations ?? 100, 'generations', SECURITY_LIMITS.MAX_GENERATIONS);\n validateWorkUnits('geneticAlgorithm', [populationSize, generations, selectCount, selectCount]);\n const mutationRate = settings.mutationRate ?? 0.1;\n\n // Use seeded PRNG if settings.seed is provided\n const prng = typeof settings.seed === 'number' ? mulberry32(settings.seed) : Math.random;\n\n // Helper function to calculate minimum distance between selected colors\n function calculateFitness(selection) {\n let minDist = Infinity;\n for (let i = 0; i < selection.length - 1; i++) {\n for (let j = i + 1; j < selection.length; j++) {\n const dist = deltaE(labColors[selection[i]], labColors[selection[j]]);\n minDist = Math.min(minDist, dist);\n }\n }\n return minDist;\n }\n\n // Generate initial population\n let population = Array(populationSize).fill().map(() =>\n Array.from({length: colors.length}, (_, i) => i)\n .sort(() => prng() - 0.5)\n .slice(0, selectCount)\n );\n\n let bestSolution = population[0];\n let bestFitness = calculateFitness(bestSolution);\n\n // Main loop\n for (let generation = 0; generation < generations; generation++) {\n // Calculate fitness for each solution\n const fitnesses = population.map(calculateFitness);\n\n // Update best solution\n const maxFitnessIndex = fitnesses.indexOf(Math.max(...fitnesses));\n if (fitnesses[maxFitnessIndex] > bestFitness) {\n bestSolution = [...population[maxFitnessIndex]];\n bestFitness = fitnesses[maxFitnessIndex];\n }\n\n // Create new population through selection and crossover\n const newPopulation = [];\n\n while (newPopulation.length < populationSize) {\n // Tournament selection\n const tournament1 = Array(3).fill().map(() => Math.floor(prng() * populationSize));\n const tournament2 = Array(3).fill().map(() => Math.floor(prng() * populationSize));\n\n const parent1 = population[tournament1.reduce((a, b) =>\n fitnesses[a] > fitnesses[b] ? a : b)];\n const parent2 = population[tournament2.reduce((a, b) =>\n fitnesses[a] > fitnesses[b] ? a : b)];\n\n // Crossover\n const crossoverPoint = Math.floor(prng() * selectCount);\n const child = [...new Set([\n ...parent1.slice(0, crossoverPoint),\n ...parent2.slice(crossoverPoint)\n ])];\n\n // Fill up with random colors if needed\n while (child.length < selectCount) {\n const available = Array.from({length: colors.length}, (_, i) => i)\n .filter(i => !child.includes(i));\n child.push(available[Math.floor(prng() * available.length)]);\n }\n\n // Mutation\n if (prng() < mutationRate) {\n const mutationIndex = Math.floor(prng() * selectCount);\n const available = Array.from({length: colors.length}, (_, i) => i)\n .filter(i => !child.includes(i));\n child[mutationIndex] = available[Math.floor(prng() * available.length)];\n }\n\n newPopulation.push(child);\n }\n\n population = newPopulation;\n }\n\n return {\n colors: sortColors(bestSolution.map(i => colors[i])),\n time: performance.now() - start\n };\n}\n","import { rgb2lab, deltaE, sortColors, mulberry32 } from '../utils/colorUtils.js';\nimport {\n SECURITY_LIMITS,\n normalizePositiveInteger,\n validateSelectionArgs,\n validateWorkUnits\n} from '../utils/securityLimits.js';\n\nexport function particleSwarmOptimization(colors, selectCount, settings = {}) {\n console.log('Starting Particle Swarm Optimization...');\n const start = performance.now();\n validateSelectionArgs(colors, selectCount);\n\n const labColors = colors.map(rgb2lab);\n const numParticles = normalizePositiveInteger(settings.numParticles ?? 30, 'numParticles', SECURITY_LIMITS.MAX_PARTICLES);\n const maxIterations = normalizePositiveInteger(settings.psoIterations ?? settings.iterations ?? 100, 'psoIterations', SECURITY_LIMITS.MAX_ITERATIONS);\n validateWorkUnits('particleSwarmOptimization', [numParticles, maxIterations, selectCount]);\n const w = settings.inertiaWeight ?? 0.7; // inertia weight\n const c1 = settings.cognitiveWeight ?? 1.5; // cognitive weight\n const c2 = settings.socialWeight ?? 1.5; // social weight\n\n // Use seeded PRNG if settings.seed is provided\n const prng = typeof settings.seed === 'number' ? mulberry32(settings.seed) : Math.random;\n\n // Helper function to calculate minimum distance between selected colors\n function calculateFitness(selection) {\n let minDist = Infinity;\n for (let i = 0; i < selection.length - 1; i++) {\n for (let j = i + 1; j < selection.length; j++) {\n const dist = deltaE(labColors[selection[i]], labColors[selection[j]]);\n minDist = Math.min(minDist, dist);\n }\n }\n return minDist;\n }\n\n // Initialize particles\n const particles = Array(numParticles).fill().map(() => ({\n position: Array.from({length: colors.length}, (_, i) => i)\n .sort(() => prng() - 0.5)\n .slice(0, selectCount),\n velocity: Array(selectCount).fill(0),\n bestPosition: null,\n bestFitness: -Infinity\n }));\n\n let globalBestPosition = null;\n let globalBestFitness = -Infinity;\n\n // Main loop\n for (let iteration = 0; iteration < maxIterations; iteration++) {\n for (const particle of particles) {\n // Calculate fitness\n const fitness = calculateFitness(particle.position);\n\n // Update particle's best\n if (fitness > particle.bestFitness) {\n particle.bestPosition = [...particle.position];\n particle.bestFitness = fitness;\n\n // Update global best\n if (fitness > globalBestFitness) {\n globalBestPosition = [...particle.position];\n globalBestFitness = fitness;\n }\n }\n\n // Update velocity and position\n for (let i = 0; i < selectCount; i++) {\n const r1 = prng();\n const r2 = prng();\n\n particle.velocity[i] = Math.floor(\n w * particle.velocity[i] +\n c1 * r1 * (particle.bestPosition[i] - particle.position[i]) +\n c2 * r2 * (globalBestPosition[i] - particle.position[i])\n );\n\n // Apply velocity (swap with another color)\n if (particle.velocity[i] !== 0) {\n const available = Array.from({length: colors.length}, (_, i) => i)\n .filter(j => !particle.position.includes(j));\n if (available.length > 0) {\n const swapIndex = Math.floor(prng() * available.length);\n particle.position[i] = available[swapIndex];\n }\n }\n }\n }\n }\n\n return {\n colors: sortColors(globalBestPosition.map(i => colors[i])),\n time: performance.now() - start\n };\n}\n","import { rgb2lab, deltaE, sortColors, mulberry32 } from '../utils/colorUtils.js';\nimport {\n SECURITY_LIMITS,\n normalizePositiveInteger,\n validatePairwiseSelectionArgs,\n validateWorkUnits\n} from '../utils/securityLimits.js';\n\nexport function antColonyOptimization(colors, selectCount, settings = {}) {\n console.log('Starting Ant Colony Optimization...');\n const start = performance.now();\n validatePairwiseSelectionArgs(colors, selectCount);\n\n const labColors = colors.map(rgb2lab);\n const numAnts = normalizePositiveInteger(settings.numAnts ?? 20, 'numAnts', SECURITY_LIMITS.MAX_ANTS);\n const maxIterations = normalizePositiveInteger(settings.acoIterations ?? settings.iterations ?? 100, 'acoIterations', SECURITY_LIMITS.MAX_ITERATIONS);\n validateWorkUnits('antColonyOptimization', [maxIterations, numAnts, selectCount, colors.length]);\n const evaporationRate = settings.evaporationRate ?? 0.1;\n const alpha = settings.pheromoneImportance ?? 1; // pheromone importance\n const beta = settings.heuristicImportance ?? 2; // heuristic importance\n\n // Use seeded PRNG if settings.seed is provided\n const prng = typeof settings.seed === 'number' ? mulberry32(settings.seed) : Math.random;\n\n // Initialize pheromone trails\n const pheromones = Array(colors.length).fill(1);\n\n // Calculate heuristic information (distances between colors)\n const distances = Array(colors.length).fill().map(() => Array(colors.length));\n for (let i = 0; i < colors.length; i++) {\n for (let j = i + 1; j < colors.length; j++) {\n const distance = deltaE(labColors[i], labColors[j]);\n distances[i][j] = distance;\n distances[j][i] = distance;\n }\n }\n\n let bestSolution = null;\n let bestFitness = -Infinity;\n\n // Main ACO loop\n for (let iteration = 0; iteration < maxIterations; iteration++) {\n // Solutions found by ants in this iteration\n const solutions = [];\n\n // Each ant constructs a solution\n for (let ant = 0; ant < numAnts; ant++) {\n const available = Array.from({length: colors.length}, (_, i) => i);\n const solution = [];\n\n // Randomly select first color\n const firstIndex = Math.floor(prng() * available.length);\n solution.push(available[firstIndex]);\n available.splice(firstIndex, 1);\n\n // Select remaining colors\n while (solution.length < selectCount) {\n // Calculate probabilities for each available color\n const probabilities = available.map(i => {\n const pheromone = Math.pow(pheromones[i], alpha);\n const minDist = Math.min(...solution.map(j => distances[i][j]));\n const heuristic = Math.pow(minDist, beta);\n return pheromone * heuristic;\n });\n\n // Select next color using roulette wheel selection\n const total = probabilities.reduce((a, b) => a + b, 0);\n let random = prng() * total;\n let selectedIndex = 0;\n\n while (random > 0 && selectedIndex < probabilities.length) {\n random -= probabilities[selectedIndex];\n if (random > 0) selectedIndex++;\n }\n\n solution.push(available[selectedIndex]);\n available.splice(selectedIndex, 1);\n }\n\n solutions.push(solution);\n }\n\n // Evaluate solutions and update best\n for (const solution of solutions) {\n const fitness = Math.min(...solution.map((i, idx) =>\n solution.slice(idx + 1).map(j =>\n deltaE(labColors[i], labColors[j])\n )\n ).flat());\n\n if (fitness > bestFitness) {\n bestFitness = fitness;\n bestSolution = solution;\n }\n }\n\n // Update pheromones\n for (let i = 0; i < pheromones.length; i++) {\n pheromones[i] *= (1 - evaporationRate);\n }\n\n // Add new pheromones from solutions\n for (const solution of solutions) {\n const deposit = 1 / solution.length;\n for (const i of solution) {\n pheromones[i] += deposit;\n }\n }\n }\n\n return {\n colors: sortColors(bestSolution.map(i => colors[i])),\n time: performance.now() - start\n };\n}\n","import { rgb2lab, deltaE, sortColors } from '../utils/colorUtils.js';\nimport {\n SECURITY_LIMITS,\n normalizePositiveInteger,\n validateSelectionArgs,\n validateWorkUnits\n} from '../utils/securityLimits.js';\n\nexport function tabuSearch(colors, selectCount, settings = {}) {\n console.log('Starting Tabu Search...');\n const start = performance.now();\n validateSelectionArgs(colors, selectCount);\n\n const labColors = colors.map(rgb2lab);\n const maxIterations = normalizePositiveInteger(settings.tabuIterations ?? settings.maxIterations ?? 1000, 'tabuIterations', SECURITY_LIMITS.MAX_ITERATIONS);\n const tabuTenure = normalizePositiveInteger(settings.tabuTenure ?? 5, 'tabuTenure', SECURITY_LIMITS.MAX_ITERATIONS);\n validateWorkUnits('tabuSearch', [maxIterations, selectCount, colors.length]);\n\n // Helper function to calculate minimum distance between selected colors\n function calculateFitness(selection) {\n let minDist = Infinity;\n for (let i = 0; i < selection.length - 1; i++) {\n for (let j = i + 1; j < selection.length; j++) {\n const dist = deltaE(labColors[selection[i]], labColors[selection[j]]);\n minDist = Math.min(minDist, dist);\n }\n }\n return minDist;\n }\n\n // Initialize solution\n let current = Array.from({length: selectCount}, (_, i) => i);\n let best = [...current];\n let bestFitness = calculateFitness(best);\n\n // Tabu list implementation using a Map to store move expiration\n const tabuList = new Map();\n\n // Generate move key for tabu list\n function getMoveKey(oldColor, newColor) {\n return `${oldColor}-${newColor}`;\n }\n\n for (let iteration = 0; iteration < maxIterations; iteration++) {\n let bestNeighborSolution = null;\n let bestNeighborFitness = -Infinity;\n\n // Examine all possible moves\n for (let i = 0; i < selectCount; i++) {\n for (let j = 0; j < colors.length; j++) {\n if (!current.includes(j)) {\n const moveKey = getMoveKey(current[i], j);\n const neighbor = [...current];\n neighbor[i] = j;\n\n const fitness = calculateFitness(neighbor);\n\n // Accept if better than current best neighbor and not tabu\n // or if satisfies aspiration criterion (better than global best)\n if ((fitness > bestNeighborFitness &&\n (!tabuList.has(moveKey) || tabuList.get(moveKey) <= iteration)) ||\n fitness > bestFitness) {\n bestNeighborSolution = neighbor;\n bestNeighborFitness = fitness;\n }\n }\n }\n }\n\n if (!bestNeighborSolution) break;\n\n // Update current solution\n current = bestNeighborSolution;\n\n // Update best solution if improved\n if (bestNeighborFitness > bestFitness) {\n best = [...current];\n bestFitness = bestNeighborFitness;\n }\n\n // Update tabu list\n for (let i = 0; i < selectCount; i++) {\n const moveKey = getMoveKey(current[i], best[i]);\n tabuList.set(moveKey, iteration + tabuTenure);\n }\n\n // Clean expired tabu moves\n for (const [move, expiration] of tabuList.entries()) {\n if (expiration <= iteration) {\n tabuList.delete(move);\n }\n }\n }\n\n return {\n colors: sortColors(best.map(i => colors[i])),\n time: performance.now() - start\n };\n}\n","import { rgb2lab, deltaE, sortColors } from '../utils/colorUtils.js';\nimport { validateExactSelectionArgs } from '../utils/securityLimits.js';\n\nexport function exactMaximum(colors, selectCount) {\n console.log('Starting Exact Maximum calculation...');\n const start = performance.now();\n validateExactSelectionArgs(colors, selectCount);\n\n const labColors = colors.map(rgb2lab);\n let bestSelection = null;\n let bestMaxDistance = -Infinity;\n\n // Helper function to calculate maximum distance between selected colors\n function calculateMaxDistance(selection) {\n let maxDist = -Infinity;\n for (let i = 0; i < selection.length - 1; i++) {\n for (let j = i + 1; j < selection.length; j++) {\n const dist = deltaE(labColors[selection[i]], labColors[selection[j]]);\n maxDist = Math.max(maxDist, dist);\n }\n }\n return maxDist;\n }\n\n // Generate all possible combinations\n function* combinations(arr, r) {\n if (r === 1) {\n for (let i = 0; i < arr.length; i++) {\n yield [arr[i]];\n }\n return;\n }\n\n for (let i = 0; i < arr.length - r + 1; i++) {\n const head = arr[i];\n const remaining = arr.slice(i + 1);\n for (const tail of combinations(remaining, r - 1)) {\n yield [head, ...tail];\n }\n }\n }\n\n // Try all combinations\n const indices = Array.from({length: colors.length}, (_, i) => i);\n for (const selection of combinations(indices, selectCount)) {\n const maxDistance = calculateMaxDistance(selection);\n if (maxDistance > bestMaxDistance) {\n bestMaxDistance = maxDistance;\n bestSelection = selection;\n }\n }\n\n return {\n colors: sortColors(bestSelection.map(i => colors[i])),\n time: performance.now() - start\n };\n}\n","import { rgb2lab, deltaE, sortColors } from '../utils/colorUtils.js';\nimport { validateExactSelectionArgs } from '../utils/securityLimits.js';\n\nexport function exactMinimum(colors, selectCount) {\n console.log('Starting Exact Minimum calculation...');\n const start = performance.now();\n validateExactSelectionArgs(colors, selectCount);\n\n const labColors = colors.map(rgb2lab);\n let bestSelection = null;\n let bestMinDistance = -Infinity;\n\n // Helper function to calculate minimum distance between selected colors\n function calculateMinDistance(selection) {\n let minDist = Infinity;\n for (let i = 0; i < selection.length - 1; i++) {\n for (let j = i + 1; j < selection.length; j++) {\n const dist = deltaE(labColors[selection[i]], labColors[selection[j]]);\n minDist = Math.min(minDist, dist);\n }\n }\n return minDist;\n }\n\n // Generate all possible combinations\n function* combinations(arr, r) {\n if (r === 1) {\n for (let i = 0; i < arr.length; i++) {\n yield [arr[i]];\n }\n return;\n }\n\n for (let i = 0; i < arr.length - r + 1; i++) {\n const head = arr[i];\n const remaining = arr.slice(i + 1);\n for (const tail of combinations(remaining, r - 1)) {\n yield [head, ...tail];\n }\n }\n }\n\n // Try all combinations\n const indices = Array.from({length: colors.length}, (_, i) => i);\n for (const selection of combinations(indices, selectCount)) {\n const minDistance = calculateMinDistance(selection);\n if (minDistance > bestMinDistance) {\n bestMinDistance = minDistance;\n bestSelection = selection;\n }\n }\n\n return {\n colors: sortColors(bestSelection.map(i => colors[i])),\n time: performance.now() - start\n };\n}\n","import { sortColors, mulberry32 } from '../utils/colorUtils.js';\nimport { validateSelectionArgs } from '../utils/securityLimits.js';\n\nexport function randomSelection(colors, selectCount, seed) {\n console.log('Starting Random Selection...');\n const start = performance.now();\n validateSelectionArgs(colors, selectCount);\n\n // Use seeded PRNG if seed is provided\n const prng = typeof seed === 'number' ? mulberry32(seed) : Math.random;\n\n // Randomly select indices without replacement\n const indices = Array.from({length: colors.length}, (_, i) => i);\n const selected = [];\n\n for (let i = 0; i < selectCount; i++) {\n const randomIndex = Math.floor(prng() * indices.length);\n selected.push(indices[randomIndex]);\n indices.splice(randomIndex, 1);\n }\n\n return {\n colors: sortColors(selected.map(i => colors[i])),\n time: performance.now() - start\n };\n}\n","import { randomColor, mulberry32 } from './colorUtils.js';\nimport { maxSumDistancesGlobal, maxSumDistancesSequential } from '../algorithms/maxSumDistances.js';\nimport { greedySelection } from '../algorithms/greedy.js';\nimport { kmeansppSelection } from '../algorithms/kmeans.js';\nimport { simulatedAnnealing } from '../algorithms/simulatedAnnealing.js';\nimport { geneticAlgorithm } from '../algorithms/genetic.js';\nimport { particleSwarmOptimization } from '../algorithms/particleSwarm.js';\nimport { antColonyOptimization } from '../algorithms/antColony.js';\nimport { tabuSearch } from '../algorithms/tabu.js';\nimport { exactMaximum } from '../algorithms/exactMaximum.js';\nimport { exactMinimum } from '../algorithms/exactMinimum.js';\nimport { randomSelection } from '../algorithms/random.js';\nimport {\n SECURITY_LIMITS,\n normalizePositiveInteger,\n validateColorPool,\n} from './securityLimits.js';\n\nconst ALGORITHMS = {\n greedy: greedySelection,\n maxSumDistancesGlobal,\n maxSumDistancesSequential,\n kmeansppSelection,\n simulatedAnnealing,\n geneticAlgorithm,\n particleSwarmOptimization,\n antColonyOptimization,\n tabuSearch,\n exactMaximum,\n exactMinimum,\n randomSelection,\n};\n\n/**\n * Pick a set of maximally distinct colors using the specified algorithm.\n *\n * Recommended usage (named arguments):\n * pickDistinctColors({ count, algorithm, poolSize, colors, options, seed })\n *\n * @param {object|number} args - Either an options object or the count (legacy positional signature).\n * @param {number} args.count - Number of colors to select.\n * @param {string} [args.algorithm='greedy'] - Algorithm name (see docs for options).\n * @param {number} [args.poolSize] - Number of random colors to generate if no pool is provided (default: Math.max(count * 16, 128)).\n * @param {number[][]} [args.colors] - Optional array of RGB colors to select from.\n * @param {object} [args.options] - Optional algorithm-specific options.\n * @param {number} [args.seed=42] - Seed for deterministic random color generation.\n * @returns {Promise<{colors: number[][], time: number}>} Selected colors and execution time.\n */\nexport async function pickDistinctColors(args, algorithm, poolSize, colors, options, seed) {\n // Support both: pickDistinctColors({ ... }) and pickDistinctColors(count, ...)\n let count, _algorithm, _poolSize, _colors, _options, _seed;\n if (typeof args === 'object' && args !== null && !Array.isArray(args)) {\n count = args.count;\n _algorithm = args.algorithm ?? 'greedy';\n _poolSize = args.poolSize;\n _colors = args.colors;\n _options = args.options;\n _seed = args.seed ?? 42;\n } else {\n // Legacy positional signature\n count = args;\n _algorithm = algorithm ?? 'greedy';\n _poolSize = poolSize;\n _colors = colors;\n _options = options;\n _seed = seed ?? 42;\n }\n if (!ALGORITHMS[_algorithm]) {\n throw new Error(`Unknown algorithm: ${_algorithm}`);\n }\n count = normalizePositiveInteger(count, 'count', SECURITY_LIMITS.MAX_SELECT_COUNT);\n let pool = _colors;\n if (!Array.isArray(pool) || pool.length === 0) {\n const defaultSize = _algorithm === 'exactMaximum' || _algorithm === 'exactMinimum'\n ? Math.max(count * 4, 20)\n : Math.min(Math.max(count * 16, 128), SECURITY_LIMITS.MAX_GENERATED_POOL_SIZE);\n const size = _poolSize === undefined\n ? defaultSize\n : normalizePositiveInteger(_poolSize, 'poolSize', SECURITY_LIMITS.MAX_GENERATED_POOL_SIZE);\n const prng = mulberry32(_seed);\n pool = Array.from({ length: size }, () => randomColor(prng));\n }\n validateColorPool(pool);\n if (count > pool.length) {\n throw new RangeError('count cannot be greater than the number of colors in the pool');\n }\n if (_algorithm === 'maxSumDistancesGlobal') {\n return await maxSumDistancesGlobal(pool, count);\n }\n if (_algorithm === 'maxSumDistancesSequential') {\n return maxSumDistancesSequential(pool, count, _seed);\n }\n if (_algorithm === 'greedy') {\n return greedySelection(pool, count, _seed);\n }\n if (_algorithm === 'kmeansppSelection') {\n return kmeansppSelection(pool, count, _seed);\n }\n if (_algorithm === 'simulatedAnnealing') {\n const opts = { ...(_options || {}), seed: _seed };\n return simulatedAnnealing(pool, count, opts);\n }\n if (_algorithm === 'geneticAlgorithm') {\n const opts = { ...(_options || {}), seed: _seed };\n return geneticAlgorithm(pool, count, opts);\n }\n if (_algorithm === 'particleSwarmOptimization') {\n const opts = { ...(_options || {}), seed: _seed };\n return particleSwarmOptimization(pool, count, opts);\n }\n if (_algorithm === 'antColonyOptimization') {\n const opts = { ...(_options || {}), seed: _seed };\n return antColonyOptimization(pool, count, opts);\n }\n if (_algorithm === 'tabuSearch') {\n const opts = { ...(_options || {}), seed: _seed };\n return tabuSearch(pool, count, opts);\n }\n if (_algorithm === 'exactMaximum') {\n return exactMaximum(pool, count);\n }\n if (_algorithm === 'exactMinimum') {\n return exactMinimum(pool, count);\n }\n if (_algorithm === 'randomSelection') {\n return randomSelection(pool, count, _seed);\n }\n throw new Error(`Algorithm not implemented: ${_algorithm}`);\n}\n"],"names":["rgb2lab","rgb","r","g","b","Math","pow","x","y","z","deltaE","labA","labB","deltaL","deltaA","deltaB","sqrt","mulberry32","seed","t","imul","randomColor","prng","random","floor","sortColors","colors","labColors","map","indices","Array","from","length","_","i","sort","j","L1","a1","b1","L2","a2","b2","calculateMetrics","minDist","Infinity","maxDist","sumDist","count","dist","min","max","avg","sum","analyzeColorDistribution","stats","L","range","a","chunkSize","chunk","slice","lab","coverage","toFixed","error","console","findClosestPair","closestPair","distance","SECURITY_LIMITS","MAX_GENERATED_POOL_SIZE","MAX_COLOR_POOL_SIZE","MAX_SELECT_COUNT","MAX_EXACT_COMBINATIONS","MAX_PAIRWISE_COLORS","MAX_POPULATION_SIZE","MAX_GENERATIONS","MAX_PARTICLES","MAX_ANTS","MAX_ITERATIONS","MAX_WORK_UNITS","normalizePositiveInteger","value","name","Number","MAX_SAFE_INTEGER","isSafeInteger","RangeError","validateColorPool","maxColors","isArray","TypeError","color","every","component","isInteger","validateSelectionArgs","selectCount","options","maxSelectCount","countCombinationsOverLimit","n","k","limit","combinations","validateExactSelectionArgs","validatePairwiseSelectionArgs","validateWorkUnits","terms","maxWorkUnits","workUnits","reduce","product","term","isFinite","maxSumDistancesGlobal","Promise","resolve","reject","workerCode","toString","blob","Blob","type","workerUrl","URL","createObjectURL","worker","Worker","cleanup","terminate","revokeObjectURL","onmessage","e","data","result","Error","onerror","postMessage","maxSumDistancesSequential","log","start","performance","now","selected","available","calculateTo