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
JavaScript
'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