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.
88 lines (75 loc) • 3.52 kB
JavaScript
import { rgb2lab, deltaE, sortColors, mulberry32 } from '../utils/colorUtils.js';
export 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
};
}