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,038 lines (942 loc) 35 kB
'use strict'; 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)]; } 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) 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] */ function randomColor(prng = Math.random) { return [Math.floor(prng() * 256), Math.floor(prng() * 256), Math.floor(prng() * 256)]; } 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]); } 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 }; } 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'; } } 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 }; } function maxSumDistancesGlobal(colors, selectCount) { return new Promise((resolve, reject) => { // Create worker code with utility functions in scope const workerCode = ` // Import utility functions from parent const rgb2lab = ${rgb2lab.toString()}; const deltaE = ${deltaE.toString()}; const sortColors = ${sortColors.toString()}; // Main worker function function maxSumDistancesGlobal(colors, selectCount) { const start = performance.now(); const labColors = colors.map(rgb2lab); // Calculate total distances from each color to all other colors const totalDistances = colors.map((_, i) => { let sum = 0; for (let j = 0; j < colors.length; j++) { if (i !== j) { sum += deltaE(labColors[i], labColors[j]); } } return { index: i, sum }; }); // Sort colors by their total distance to all other colors totalDistances.sort((a, b) => b.sum - a.sum); // Take the top selectCount colors that have the highest total distances const selectedIndices = totalDistances.slice(0, selectCount).map(item => item.index); const selectedColors = selectedIndices.map(i => colors[i]); return { colors: sortColors(selectedColors), time: performance.now() - start }; } // Worker message handler self.onmessage = function(e) { const { colors, selectCount } = e.data; try { const result = maxSumDistancesGlobal(colors, selectCount); self.postMessage({ type: 'complete', result }); } catch (error) { self.postMessage({ type: 'error', error: error.message }); } }; `; // Create blob and worker const blob = new Blob([workerCode], { type: 'application/javascript' }); const worker = new Worker(URL.createObjectURL(blob)); // Set up worker message handlers worker.onmessage = function (e) { if (e.data.type === 'complete') { resolve(e.data.result); } else if (e.data.type === 'error') { reject(new Error(e.data.error)); } worker.terminate(); }; worker.onerror = function (error) { reject(error); worker.terminate(); }; // Start the worker worker.postMessage({ colors, selectCount }); }); } function maxSumDistancesSequential(colors, selectCount, seed) { console.log('Starting Maximum Sum (Sequential) calculation...'); const start = performance.now(); const labColors = colors.map(rgb2lab); const selected = []; const available = Array.from({ length: colors.length }, (_, i) => i); // Use seeded PRNG if seed is provided const prng = typeof seed === 'number' ? mulberry32(seed) : Math.random; // Helper function to calculate total distance from a point to selected points function calculateTotalDistance(index) { return selected.reduce((sum, selectedIndex) => sum + deltaE(labColors[index], labColors[selectedIndex]), 0); } // Select first point randomly const firstIndex = Math.floor(prng() * available.length); selected.push(available[firstIndex]); available.splice(firstIndex, 1); // Select remaining points while (selected.length < selectCount) { let bestIndex = 0; let bestDistance = -Infinity; // Find point with maximum sum of distances to selected points for (let i = 0; i < available.length; i++) { const totalDistance = calculateTotalDistance(available[i]); if (totalDistance > bestDistance) { bestDistance = totalDistance; bestIndex = i; } } selected.push(available[bestIndex]); available.splice(bestIndex, 1); } return { colors: sortColors(selected.map(i => colors[i])), time: performance.now() - start }; } function greedySelection(colors, selectCount, seed) { console.log('Starting Greedy calculation...'); const start = performance.now(); const labColors = colors.map(rgb2lab); const selected = []; const available = Array.from({ length: colors.length }, (_, i) => i); // Use seeded PRNG if seed is provided const prng = typeof seed === 'number' ? mulberry32(seed) : Math.random; // Helper function to calculate minimum distance from a point to selected points function calculateMinDistance(index) { if (selected.length === 0) return Infinity; return Math.min(...selected.map(selectedIndex => deltaE(labColors[index], labColors[selectedIndex]))); } // Select first point randomly const firstIndex = Math.floor(prng() * available.length); selected.push(available[firstIndex]); available.splice(firstIndex, 1); // Select remaining points while (selected.length < selectCount) { let bestIndex = 0; let bestMinDistance = -Infinity; // Find point with maximum minimum distance to selected points for (let i = 0; i < available.length; i++) { const minDistance = calculateMinDistance(available[i]); if (minDistance > bestMinDistance) { bestMinDistance = minDistance; bestIndex = i; } } selected.push(available[bestIndex]); available.splice(bestIndex, 1); } return { colors: sortColors(selected.map(i => colors[i])), time: performance.now() - start }; } function kmeansppSelection(colors, selectCount, seed) { console.log('Starting K-means++ calculation...'); const start = performance.now(); const labColors = colors.map(rgb2lab); // Use seeded PRNG if seed is provided const prng = typeof seed === 'number' ? mulberry32(seed) : Math.random; // Helper function to find minimum distance to existing centers function minDistanceToCenters(point, centers) { if (centers.length === 0) return Infinity; return Math.min(...centers.map(center => deltaE(labColors[point], labColors[center]))); } // Select initial center randomly const selected = [Math.floor(prng() * colors.length)]; // Select remaining centers using k-means++ initialization while (selected.length < selectCount) { const distances = Array.from({ length: colors.length }, (_, i) => { if (selected.includes(i)) return 0; const dist = minDistanceToCenters(i, selected); return dist * dist; // Square distances for k-means++ }); const sum = distances.reduce((a, b) => a + b, 0); let random = prng() * sum; let selectedIndex = 0; while (random > 0 && selectedIndex < distances.length) { if (!selected.includes(selectedIndex)) { random -= distances[selectedIndex]; } if (random > 0) selectedIndex++; } selected.push(selectedIndex); } return { colors: sortColors(selected.map(i => colors[i])), time: performance.now() - start }; } function simulatedAnnealing(colors, selectCount, settings = {}) { console.log('Starting Simulated Annealing calculation...'); const start = performance.now(); const labColors = colors.map(rgb2lab); const maxIterations = 10000; const initialTemp = settings.initialTemp ?? 1000; const coolingRate = settings.coolingRate ?? 0.995; const minTemp = settings.minTemp ?? 0.1; // Use seeded PRNG if settings.seed is provided const prng = typeof settings.seed === 'number' ? mulberry32(settings.seed) : Math.random; // Helper function to calculate minimum distance between selected colors function calculateFitness(selection) { let minDist = Infinity; for (let i = 0; i < selection.length - 1; i++) { for (let j = i + 1; j < selection.length; j++) { const dist = deltaE(labColors[selection[i]], labColors[selection[j]]); minDist = Math.min(minDist, dist); } } return minDist; } // Generate initial solution let currentSolution = Array.from({ length: colors.length }, (_, i) => i).sort(() => prng() - 0.5).slice(0, selectCount); let currentFitness = calculateFitness(currentSolution); let bestSolution = [...currentSolution]; let bestFitness = currentFitness; let temperature = initialTemp; // Main loop for (let i = 0; i < maxIterations && temperature > minTemp; i++) { // Generate neighbor by swapping one selected color with an unselected one const neighborSolution = [...currentSolution]; const swapIndex = Math.floor(prng() * selectCount); const availableIndices = Array.from({ length: colors.length }, (_, i) => i).filter(i => !currentSolution.includes(i)); const newIndex = availableIndices[Math.floor(prng() * availableIndices.length)]; neighborSolution[swapIndex] = newIndex; const neighborFitness = calculateFitness(neighborSolution); // Decide if we should accept the neighbor const delta = neighborFitness - currentFitness; if (delta > 0 || prng() < Math.exp(delta / temperature)) { currentSolution = neighborSolution; currentFitness = neighborFitness; if (currentFitness > bestFitness) { bestSolution = [...currentSolution]; bestFitness = currentFitness; } } temperature *= coolingRate; } return { colors: sortColors(bestSolution.map(i => colors[i])), time: performance.now() - start }; } function geneticAlgorithm(colors, selectCount, settings = {}) { console.log('Starting Genetic Algorithm calculation...'); const start = performance.now(); const labColors = colors.map(rgb2lab); const populationSize = settings.populationSize ?? 100; const generations = settings.generations ?? 100; const mutationRate = settings.mutationRate ?? 0.1; // Use seeded PRNG if settings.seed is provided const prng = typeof settings.seed === 'number' ? mulberry32(settings.seed) : Math.random; // Helper function to calculate minimum distance between selected colors function calculateFitness(selection) { let minDist = Infinity; for (let i = 0; i < selection.length - 1; i++) { for (let j = i + 1; j < selection.length; j++) { const dist = deltaE(labColors[selection[i]], labColors[selection[j]]); minDist = Math.min(minDist, dist); } } return minDist; } // Generate initial population let population = Array(populationSize).fill().map(() => Array.from({ length: colors.length }, (_, i) => i).sort(() => prng() - 0.5).slice(0, selectCount)); let bestSolution = population[0]; let bestFitness = calculateFitness(bestSolution); // Main loop for (let generation = 0; generation < generations; generation++) { // Calculate fitness for each solution const fitnesses = population.map(calculateFitness); // Update best solution const maxFitnessIndex = fitnesses.indexOf(Math.max(...fitnesses)); if (fitnesses[maxFitnessIndex] > bestFitness) { bestSolution = [...population[maxFitnessIndex]]; bestFitness = fitnesses[maxFitnessIndex]; } // Create new population through selection and crossover const newPopulation = []; while (newPopulation.length < populationSize) { // Tournament selection const tournament1 = Array(3).fill().map(() => Math.floor(prng() * populationSize)); const tournament2 = Array(3).fill().map(() => Math.floor(prng() * populationSize)); const parent1 = population[tournament1.reduce((a, b) => fitnesses[a] > fitnesses[b] ? a : b)]; const parent2 = population[tournament2.reduce((a, b) => fitnesses[a] > fitnesses[b] ? a : b)]; // Crossover const crossoverPoint = Math.floor(prng() * selectCount); const child = [...new Set([...parent1.slice(0, crossoverPoint), ...parent2.slice(crossoverPoint)])]; // Fill up with random colors if needed while (child.length < selectCount) { const available = Array.from({ length: colors.length }, (_, i) => i).filter(i => !child.includes(i)); child.push(available[Math.floor(prng() * available.length)]); } // Mutation if (prng() < mutationRate) { const mutationIndex = Math.floor(prng() * selectCount); const available = Array.from({ length: colors.length }, (_, i) => i).filter(i => !child.includes(i)); child[mutationIndex] = available[Math.floor(prng() * available.length)]; } newPopulation.push(child); } population = newPopulation; } return { colors: sortColors(bestSolution.map(i => colors[i])), time: performance.now() - start }; } function particleSwarmOptimization(colors, selectCount, settings = {}) { console.log('Starting Particle Swarm Optimization...'); const start = performance.now(); const labColors = colors.map(rgb2lab); const numParticles = settings.numParticles ?? 30; const maxIterations = settings.psoIterations ?? 100; const w = settings.inertiaWeight ?? 0.7; // inertia weight const c1 = settings.cognitiveWeight ?? 1.5; // cognitive weight const c2 = settings.socialWeight ?? 1.5; // social weight // Use seeded PRNG if settings.seed is provided const prng = typeof settings.seed === 'number' ? mulberry32(settings.seed) : Math.random; // Helper function to calculate minimum distance between selected colors function calculateFitness(selection) { let minDist = Infinity; for (let i = 0; i < selection.length - 1; i++) { for (let j = i + 1; j < selection.length; j++) { const dist = deltaE(labColors[selection[i]], labColors[selection[j]]); minDist = Math.min(minDist, dist); } } return minDist; } // Initialize particles const particles = Array(numParticles).fill().map(() => ({ position: Array.from({ length: colors.length }, (_, i) => i).sort(() => prng() - 0.5).slice(0, selectCount), velocity: Array(selectCount).fill(0), bestPosition: null, bestFitness: -Infinity })); let globalBestPosition = null; let globalBestFitness = -Infinity; // Main loop for (let iteration = 0; iteration < maxIterations; iteration++) { for (const particle of particles) { // Calculate fitness const fitness = calculateFitness(particle.position); // Update particle's best if (fitness > particle.bestFitness) { particle.bestPosition = [...particle.position]; particle.bestFitness = fitness; // Update global best if (fitness > globalBestFitness) { globalBestPosition = [...particle.position]; globalBestFitness = fitness; } } // Update velocity and position for (let i = 0; i < selectCount; i++) { const r1 = prng(); const r2 = prng(); particle.velocity[i] = Math.floor(w * particle.velocity[i] + c1 * r1 * (particle.bestPosition[i] - particle.position[i]) + c2 * r2 * (globalBestPosition[i] - particle.position[i])); // Apply velocity (swap with another color) if (particle.velocity[i] !== 0) { const available = Array.from({ length: colors.length }, (_, i) => i).filter(j => !particle.position.includes(j)); if (available.length > 0) { const swapIndex = Math.floor(prng() * available.length); particle.position[i] = available[swapIndex]; } } } } } return { colors: sortColors(globalBestPosition.map(i => colors[i])), time: performance.now() - start }; } function antColonyOptimization(colors, selectCount, settings = {}) { console.log('Starting Ant Colony Optimization...'); const start = performance.now(); const labColors = colors.map(rgb2lab); const numAnts = settings.numAnts ?? 20; const maxIterations = settings.acoIterations ?? 100; const evaporationRate = settings.evaporationRate ?? 0.1; const alpha = settings.pheromoneImportance ?? 1; // pheromone importance const beta = settings.heuristicImportance ?? 2; // heuristic importance // Use seeded PRNG if settings.seed is provided const prng = typeof settings.seed === 'number' ? mulberry32(settings.seed) : Math.random; // Initialize pheromone trails const pheromones = Array(colors.length).fill(1); // Calculate heuristic information (distances between colors) const distances = Array(colors.length).fill().map(() => Array(colors.length)); for (let i = 0; i < colors.length; i++) { for (let j = i + 1; j < colors.length; j++) { const distance = deltaE(labColors[i], labColors[j]); distances[i][j] = distance; distances[j][i] = distance; } } let bestSolution = null; let bestFitness = -Infinity; // Main ACO loop for (let iteration = 0; iteration < maxIterations; iteration++) { // Solutions found by ants in this iteration const solutions = []; // Each ant constructs a solution for (let ant = 0; ant < numAnts; ant++) { const available = Array.from({ length: colors.length }, (_, i) => i); const solution = []; // Randomly select first color const firstIndex = Math.floor(prng() * available.length); solution.push(available[firstIndex]); available.splice(firstIndex, 1); // Select remaining colors while (solution.length < selectCount) { // Calculate probabilities for each available color const probabilities = available.map(i => { const pheromone = Math.pow(pheromones[i], alpha); const minDist = Math.min(...solution.map(j => distances[i][j])); const heuristic = Math.pow(minDist, beta); return pheromone * heuristic; }); // Select next color using roulette wheel selection const total = probabilities.reduce((a, b) => a + b, 0); let random = prng() * total; let selectedIndex = 0; while (random > 0 && selectedIndex < probabilities.length) { random -= probabilities[selectedIndex]; if (random > 0) selectedIndex++; } solution.push(available[selectedIndex]); available.splice(selectedIndex, 1); } solutions.push(solution); } // Evaluate solutions and update best for (const solution of solutions) { const fitness = Math.min(...solution.map((i, idx) => solution.slice(idx + 1).map(j => deltaE(labColors[i], labColors[j]))).flat()); if (fitness > bestFitness) { bestFitness = fitness; bestSolution = solution; } } // Update pheromones for (let i = 0; i < pheromones.length; i++) { pheromones[i] *= 1 - evaporationRate; } // Add new pheromones from solutions for (const solution of solutions) { const deposit = 1 / solution.length; for (const i of solution) { pheromones[i] += deposit; } } } return { colors: sortColors(bestSolution.map(i => colors[i])), time: performance.now() - start }; } function tabuSearch(colors, selectCount, settings = {}) { console.log('Starting Tabu Search...'); const start = performance.now(); const labColors = colors.map(rgb2lab); const maxIterations = settings.tabuIterations ?? 1000; const tabuTenure = settings.tabuTenure ?? 5; // Helper function to calculate minimum distance between selected colors function calculateFitness(selection) { let minDist = Infinity; for (let i = 0; i < selection.length - 1; i++) { for (let j = i + 1; j < selection.length; j++) { const dist = deltaE(labColors[selection[i]], labColors[selection[j]]); minDist = Math.min(minDist, dist); } } return minDist; } // Initialize solution let current = Array.from({ length: selectCount }, (_, i) => i); let best = [...current]; let bestFitness = calculateFitness(best); // Tabu list implementation using a Map to store move expiration const tabuList = new Map(); // Generate move key for tabu list function getMoveKey(oldColor, newColor) { return `${oldColor}-${newColor}`; } for (let iteration = 0; iteration < maxIterations; iteration++) { let bestNeighborSolution = null; let bestNeighborFitness = -Infinity; // Examine all possible moves for (let i = 0; i < selectCount; i++) { for (let j = 0; j < colors.length; j++) { if (!current.includes(j)) { const moveKey = getMoveKey(current[i], j); const neighbor = [...current]; neighbor[i] = j; const fitness = calculateFitness(neighbor); // Accept if better than current best neighbor and not tabu // or if satisfies aspiration criterion (better than global best) if (fitness > bestNeighborFitness && (!tabuList.has(moveKey) || tabuList.get(moveKey) <= iteration) || fitness > bestFitness) { bestNeighborSolution = neighbor; bestNeighborFitness = fitness; } } } } if (!bestNeighborSolution) break; // Update current solution current = bestNeighborSolution; // Update best solution if improved if (bestNeighborFitness > bestFitness) { best = [...current]; bestFitness = bestNeighborFitness; } // Update tabu list for (let i = 0; i < selectCount; i++) { const moveKey = getMoveKey(current[i], best[i]); tabuList.set(moveKey, iteration + tabuTenure); } // Clean expired tabu moves for (const [move, expiration] of tabuList.entries()) { if (expiration <= iteration) { tabuList.delete(move); } } } return { colors: sortColors(best.map(i => colors[i])), time: performance.now() - start }; } function exactMaximum(colors, selectCount) { console.log('Starting Exact Maximum calculation...'); const start = performance.now(); const labColors = colors.map(rgb2lab); let bestSelection = null; let bestMaxDistance = -Infinity; // Helper function to calculate maximum distance between selected colors function calculateMaxDistance(selection) { let maxDist = -Infinity; for (let i = 0; i < selection.length - 1; i++) { for (let j = i + 1; j < selection.length; j++) { const dist = deltaE(labColors[selection[i]], labColors[selection[j]]); maxDist = Math.max(maxDist, dist); } } return maxDist; } // Generate all possible combinations function* combinations(arr, r) { if (r === 1) { for (let i = 0; i < arr.length; i++) { yield [arr[i]]; } return; } for (let i = 0; i < arr.length - r + 1; i++) { const head = arr[i]; const remaining = arr.slice(i + 1); for (const tail of combinations(remaining, r - 1)) { yield [head, ...tail]; } } } // Try all combinations const indices = Array.from({ length: colors.length }, (_, i) => i); for (const selection of combinations(indices, selectCount)) { const maxDistance = calculateMaxDistance(selection); if (maxDistance > bestMaxDistance) { bestMaxDistance = maxDistance; bestSelection = selection; } } return { colors: sortColors(bestSelection.map(i => colors[i])), time: performance.now() - start }; } function exactMinimum(colors, selectCount) { console.log('Starting Exact Minimum calculation...'); const start = performance.now(); const labColors = colors.map(rgb2lab); let bestSelection = null; let bestMinDistance = -Infinity; // Helper function to calculate minimum distance between selected colors function calculateMinDistance(selection) { let minDist = Infinity; for (let i = 0; i < selection.length - 1; i++) { for (let j = i + 1; j < selection.length; j++) { const dist = deltaE(labColors[selection[i]], labColors[selection[j]]); minDist = Math.min(minDist, dist); } } return minDist; } // Generate all possible combinations function* combinations(arr, r) { if (r === 1) { for (let i = 0; i < arr.length; i++) { yield [arr[i]]; } return; } for (let i = 0; i < arr.length - r + 1; i++) { const head = arr[i]; const remaining = arr.slice(i + 1); for (const tail of combinations(remaining, r - 1)) { yield [head, ...tail]; } } } // Try all combinations const indices = Array.from({ length: colors.length }, (_, i) => i); for (const selection of combinations(indices, selectCount)) { const minDistance = calculateMinDistance(selection); if (minDistance > bestMinDistance) { bestMinDistance = minDistance; bestSelection = selection; } } return { colors: sortColors(bestSelection.map(i => colors[i])), time: performance.now() - start }; } function randomSelection(colors, selectCount, seed) { console.log('Starting Random Selection...'); const start = performance.now(); // Use seeded PRNG if seed is provided const prng = typeof seed === 'number' ? mulberry32(seed) : Math.random; // Randomly select indices without replacement const indices = Array.from({ length: colors.length }, (_, i) => i); const selected = []; for (let i = 0; i < selectCount; i++) { const randomIndex = Math.floor(prng() * indices.length); selected.push(indices[randomIndex]); indices.splice(randomIndex, 1); } return { colors: sortColors(selected.map(i => colors[i])), time: performance.now() - start }; } const ALGORITHMS = { greedy: greedySelection, maxSumDistancesGlobal, maxSumDistancesSequential, kmeansppSelection, simulatedAnnealing, geneticAlgorithm, particleSwarmOptimization, antColonyOptimization, tabuSearch, exactMaximum, exactMinimum, randomSelection }; /** * Pick a set of maximally distinct colors using the specified algorithm. * * Recommended usage (named arguments): * pickDistinctColors({ count, algorithm, poolSize, colors, options, seed }) * * @param {object|number} args - Either an options object or the count (legacy positional signature). * @param {number} args.count - Number of colors to select. * @param {string} [args.algorithm='greedy'] - Algorithm name (see docs for options). * @param {number} [args.poolSize] - Number of random colors to generate if no pool is provided (default: Math.max(count * 16, 128)). * @param {number[][]} [args.colors] - Optional array of RGB colors to select from. * @param {object} [args.options] - Optional algorithm-specific options. * @param {number} [args.seed=42] - Seed for deterministic random color generation. * @returns {Promise<{colors: number[][], time: number}>} Selected colors and execution time. */ async function pickDistinctColors(args, algorithm, poolSize, colors, options, seed) { // Support both: pickDistinctColors({ ... }) and pickDistinctColors(count, ...) let count, _algorithm, _poolSize, _colors, _options, _seed; if (typeof args === 'object' && args !== null && !Array.isArray(args)) { count = args.count; _algorithm = args.algorithm ?? 'greedy'; _poolSize = args.poolSize; _colors = args.colors; _options = args.options; _seed = args.seed ?? 42; } else { // Legacy positional signature count = args; _algorithm = algorithm ?? 'greedy'; _poolSize = poolSize; _colors = colors; _options = options; _seed = seed ?? 42; } if (!ALGORITHMS[_algorithm]) { throw new Error(`Unknown algorithm: ${_algorithm}`); } let pool = _colors; if (!Array.isArray(pool) || pool.length === 0) { const size = _poolSize || Math.max(count * 16, 128); const prng = mulberry32(_seed); pool = Array.from({ length: size }, () => randomColor(prng)); } if (_algorithm === 'maxSumDistancesGlobal') { return await maxSumDistancesGlobal(pool, count); } if (_algorithm === 'maxSumDistancesSequential') { return maxSumDistancesSequential(pool, count, _seed); } if (_algorithm === 'greedy') { return greedySelection(pool, count, _seed); } if (_algorithm === 'kmeansppSelection') { return kmeansppSelection(pool, count, _seed); } if (_algorithm === 'simulatedAnnealing') { const opts = { ...(_options || {}), seed: _seed }; return simulatedAnnealing(pool, count, opts); } if (_algorithm === 'geneticAlgorithm') { const opts = { ...(_options || {}), seed: _seed }; return geneticAlgorithm(pool, count, opts); } if (_algorithm === 'particleSwarmOptimization') { const opts = { ...(_options || {}), seed: _seed }; return particleSwarmOptimization(pool, count, opts); } if (_algorithm === 'antColonyOptimization') { const opts = { ...(_options || {}), seed: _seed }; return antColonyOptimization(pool, count, opts); } if (_algorithm === 'tabuSearch') { const opts = { ...(_options || {}), seed: _seed }; return tabuSearch(pool, count, opts); } if (_algorithm === 'exactMaximum') { return exactMaximum(pool, count); } if (_algorithm === 'exactMinimum') { return exactMinimum(pool, count); } if (_algorithm === 'randomSelection') { return randomSelection(pool, count, _seed); } throw new Error(`Algorithm not implemented: ${_algorithm}`); } exports.analyzeColorDistribution = analyzeColorDistribution; exports.antColonyOptimization = antColonyOptimization; exports.calculateMetrics = calculateMetrics; exports.deltaE = deltaE; exports.findClosestPair = findClosestPair; exports.geneticAlgorithm = geneticAlgorithm; exports.greedySelection = greedySelection; exports.kmeansppSelection = kmeansppSelection; exports.maxSumDistancesGlobal = maxSumDistancesGlobal; exports.maxSumDistancesSequential = maxSumDistancesSequential; exports.particleSwarmOptimization = particleSwarmOptimization; exports.pickDistinctColors = pickDistinctColors; exports.rgb2lab = rgb2lab; exports.simulatedAnnealing = simulatedAnnealing; exports.tabuSearch = tabuSearch; //# sourceMappingURL=index.cjs.map