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.
352 lines (315 loc) • 16.6 kB
JavaScript
import { calibratePerformance } from './performanceUtils.js';
import { formatTimeEstimate } from './timeUtils.js';
export function showSpinner(message = 'Processing...', showCancel = false, onCancel = null) {
hideSpinner();
const spinner = document.createElement('div');
spinner.id = 'processingSpinner';
spinner.className = 'processing';
spinner.innerHTML = `
<div class="spinner"></div>
<div>${message}</div>
${showCancel ? '<button class="cancel-button">Cancel This Algorithm</button>' : ''}
`;
document.body.appendChild(spinner);
const cancelButton = spinner.querySelector('.cancel-button');
if (cancelButton && onCancel) {
cancelButton.style.display = 'block';
cancelButton.onclick = onCancel;
}
}
export function hideSpinner() {
const spinner = document.getElementById('processingSpinner');
if (spinner) {
spinner.remove();
}
}
export function toggleSettings(button) {
button.classList.toggle('active');
const container = button.closest('.input-group, .import-panel');
if (container) {
container.querySelector('.settings-container').classList.toggle('show');
}
button.textContent = button.classList.contains('active') ? 'Hide Settings' : 'Show Settings';
}
export function toggleImportPanel(button) {
toggleSettings(button);
}
export async function initializePage() {
const controlHtml = `
<div class="control-panel">
<div class="input-group">
<label for="totalColors">Number of random colors to generate:</label>
<input type="number" id="totalColors" value="30" min="0" max="1000">
</div>
<div class="input-group">
<label for="selectColors">Number of colors to select:</label>
<input type="number" id="selectColors" value="10" min="2">
</div>
<div class="import-panel">
<button class="settings-toggle" onclick="toggleImportPanel(this)">Import Custom Colors</button>
<div class="settings-container">
<div class="algorithm-settings">
<p>Paste colors in any of these formats:</p>
<ul style="font-family: monospace; font-size: 0.9em;">
<li>#RRGGBB (one per line)</li>
<li>rgb(r,g,b) (one per line)</li>
<li>r,g,b (one per line)</li>
<li>JavaScript array: [[r,g,b], ...]</li>
</ul>
<textarea id="importText" placeholder="Paste colors here..."></textarea>
<div>
<input type="radio" id="replace" name="importMode" value="replace" checked>
<label for="replace">Replace random colors</label>
<input type="radio" id="append" name="importMode" value="append">
<label for="append">Add to random colors</label>
</div>
<div class="import-error" id="importError"></div>
<button onclick="importColors()">Import Colors</button>
</div>
</div>
</div>
<div class="input-group" style="border-top: 1px solid #ddd; padding-top: 10px; margin-top: 10px;">
<h3 style="margin: 0 0 10px 0;">Algorithm Selection</h3>
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px;">
<div>
<input type="checkbox" id="algo-maximum-sum-global" checked>
<label for="algo-maximum-sum-global">Maximum Sum Global</label>
</div>
<div>
<input type="checkbox" id="algo-maximum-sum-sequential" checked>
<label for="algo-maximum-sum-sequential">Maximum Sum Sequential</label>
</div>
<div>
<input type="checkbox" id="algo-greedy" checked>
<label for="algo-greedy">Greedy</label>
</div>
<div>
<input type="checkbox" id="algo-simulated-annealing" checked>
<label for="algo-simulated-annealing">Simulated Annealing</label>
</div>
<div>
<input type="checkbox" id="algo-k-means" checked>
<label for="algo-k-means">K-Means++</label>
</div>
<div>
<input type="checkbox" id="algo-genetic-algorithm">
<label for="algo-genetic-algorithm">Genetic Algorithm</label>
</div>
<div>
<input type="checkbox" id="algo-particle-swarm">
<label for="algo-particle-swarm">Particle Swarm</label>
</div>
<div>
<input type="checkbox" id="algo-ant-colony">
<label for="algo-ant-colony">Ant Colony</label>
</div>
<div>
<input type="checkbox" id="algo-tabu-search">
<label for="algo-tabu-search">Tabu Search</label>
</div>
<div>
<input type="checkbox" id="algo-exact-minimum" checked>
<label for="algo-exact-minimum">Exact Minimum</label>
</div>
</div>
</div>
<div class="input-group" style="border-top: 1px solid #ddd; padding-top: 10px; margin-top: 10px;">
<h3 style="margin: 0 0 10px 0;">Algorithm Settings</h3>
<button class="settings-toggle" onclick="toggleSettings(this)">Show Settings</button>
<div class="settings-container">
<!-- Simulated Annealing Settings -->
<div class="algorithm-settings" id="settings-annealing">
<h4>Simulated Annealing</h4>
<div>
<label for="initialTemp">Initial Temperature:</label>
<input type="number" id="initialTemp" value="1000" min="1">
</div>
<div>
<label for="coolingRate">Cooling Rate (0-1):</label>
<input type="number" id="coolingRate" value="0.995" min="0" max="1" step="0.001">
</div>
<div>
<label for="minTemp">Minimum Temperature:</label>
<input type="number" id="minTemp" value="0.1" min="0.0001" step="0.1">
</div>
</div>
<!-- Genetic Algorithm Settings -->
<div class="algorithm-settings" id="settings-genetic">
<h4>Genetic Algorithm</h4>
<div>
<label for="populationSize">Population Size:</label>
<input type="number" id="populationSize" value="100" min="10">
</div>
<div>
<label for="generations">Generations:</label>
<input type="number" id="generations" value="100" min="10">
</div>
<div>
<label for="mutationRate">Mutation Rate:</label>
<input type="number" id="mutationRate" value="0.1" min="0" max="1" step="0.01">
</div>
</div>
<!-- PSO Settings -->
<div class="algorithm-settings" id="settings-pso">
<h4>Particle Swarm Optimization</h4>
<div>
<label for="numParticles">Number of Particles:</label>
<input type="number" id="numParticles" value="30" min="5">
</div>
<div>
<label for="psoIterations">Iterations:</label>
<input type="number" id="psoIterations" value="100" min="10">
</div>
</div>
<!-- ACO Settings -->
<div class="algorithm-settings" id="settings-aco">
<h4>Ant Colony Optimization</h4>
<div>
<label for="numAnts">Number of Ants:</label>
<input type="number" id="numAnts" value="20" min="5">
</div>
<div>
<label for="acoIterations">Iterations:</label>
<input type="number" id="acoIterations" value="50" min="10">
</div>
</div>
<!-- Tabu Search Settings -->
<div class="algorithm-settings" id="settings-tabu">
<h4>Tabu Search</h4>
<div>
<label for="tabuIterations">Maximum Iterations:</label>
<input type="number" id="tabuIterations" value="1000" min="100">
</div>
<div>
<label for="tabuTenure">Tabu Tenure:</label>
<input type="number" id="tabuTenure" value="5" min="1">
</div>
</div>
</div>
</div>
<div style="display: flex; gap: 10px; margin-top: 20px;">
<button onclick="generateInitialColors()" id="generateButton">Generate Colors</button>
<button onclick="startCalculation()" id="calculateButton" disabled>Calculate Most Distinct Colors</button>
</div>
<div id="colorSummary" style="margin-top: 10px; font-style: italic;"></div>
<div id="complexityEstimate" style="margin-top: 10px; font-style: italic;"></div>
</div>
`;
// First, add the control panel HTML
document.getElementById('control-container').innerHTML = controlHtml;
// Then, calibrate performance
showSpinner('Calibrating performance...');
const combinationsPerSecond = await calibratePerformance();
hideSpinner();
// Store the calibrated speed as a global variable
window.calibratedSpeed = combinationsPerSecond;
// Now add event listeners after the elements exist
document.getElementById('totalColors').addEventListener('input', updateComplexityEstimate);
document.getElementById('selectColors').addEventListener('input', updateComplexityEstimate);
document.getElementById('totalColors').addEventListener('input', updateGenerateButtonText);
// Initial estimate
updateComplexityEstimate();
}
export function updateGenerateButtonText() {
const randomCount = parseInt(document.getElementById('totalColors').value);
const generateButton = document.getElementById('generateButton');
if (window.importedColors) {
const importedCount = window.importedColors.colors.length;
const totalCount = window.importedColors.mode === 'replace' ?
importedCount :
randomCount + importedCount;
generateButton.textContent = `Generate ${totalCount} Colors (${
window.importedColors.mode === 'replace' ?
'imported only' :
`${randomCount} random + ${importedCount} imported`
})`;
} else {
generateButton.textContent = `Generate ${randomCount} Colors`;
}
}
export function updateComplexityEstimate() {
const selectCount = parseInt(document.getElementById('selectColors').value);
let totalColors;
// Calculate total colors based on both imported and random colors
if (window.importedColors) {
const randomCount = parseInt(document.getElementById('totalColors').value);
if (window.importedColors.mode === 'replace') {
totalColors = window.importedColors.colors.length;
} else {
totalColors = randomCount + window.importedColors.colors.length;
}
} else {
totalColors = parseInt(document.getElementById('totalColors').value);
}
// Calculate number of combinations (n choose r)
let combinations = 1;
for (let i = 0; i < selectCount; i++) {
combinations *= (totalColors - i) / (i + 1);
}
// Use calibrated speed if available, otherwise use default estimate
const combinationsPerSecond = window.calibratedSpeed || 100000;
const estimatedSeconds = combinations / combinationsPerSecond;
const timeString = formatTimeEstimate(estimatedSeconds);
const complexityDiv = document.getElementById('complexityEstimate');
complexityDiv.innerHTML = `
<p>Exact Minimum calculation will check ${combinations.toLocaleString()} combinations</p>
<p>Estimated time: ${timeString}</p>
${estimatedSeconds > 120 ? '<p style="color: orange;">Warning: This might take a while!</p>' : ''}
<p style="font-size: 0.8em;">Based on measured performance: ~${Math.round(combinationsPerSecond).toLocaleString()} combinations/second</p>
`;
}
export function generateInitialColors() {
const totalColors = parseInt(document.getElementById('totalColors').value);
let summary = '';
// Clear previous results but keep initial colors if they exist
document.getElementById('results').innerHTML = '';
if (window.importedColors) {
const importMode = window.importedColors.mode;
const importedCount = window.importedColors.colors.length;
if (importMode === 'replace') {
window.initialColors = window.importedColors.colors;
summary = `Using ${importedCount} imported colors`;
} else { // append
const randomCount = totalColors;
window.initialColors = Array.from({length: randomCount}, () => window.randomColor())
.concat(window.importedColors.colors);
summary = `Generated ${randomCount} random colors and added ${importedCount} imported colors`;
}
} else {
window.initialColors = Array.from({length: totalColors}, () => window.randomColor());
summary = `Generated ${totalColors} random colors`;
}
// Update the display
const sortedInitialColors = window.sortColors(window.initialColors);
const metrics = window.calculateMetrics(sortedInitialColors);
const distribution = window.analyzeColorDistribution(sortedInitialColors);
document.getElementById('colorSummary').textContent = summary;
document.getElementById('calculateButton').disabled = false;
const initialHtml = `
<h2>Initial Colors (${window.initialColors.length} total)</h2>
<div>
${sortedInitialColors.map(rgb =>
`<div class="swatch" style="background-color: ${window.rgbToHex(rgb)}"></div>`
).join('')}
</div>
<div class="metrics">
<p>Total Colors: ${window.initialColors.length}</p>
<p>Minimum deltaE: ${metrics.min.toFixed(2)}</p>
<div style="display: flex; align-items: center;">
<span>Closest pair: </span>
${(() => {
const closest = window.findClosestPair(sortedInitialColors);
return closest.colors.map(rgb =>
`<div class="swatch" style="background-color: ${window.rgbToHex(rgb)}; margin: 0 5px;"></div>`
).join('');
})()}
<span>(deltaE: ${window.findClosestPair(sortedInitialColors).distance.toFixed(2)})</span>
</div>
<p>Average deltaE: ${metrics.avg.toFixed(2)}</p>
<p>Distribution Analysis:<br/> ${distribution}</p>
</div>
<div id="initial-plot" style="width:600px;height:400px;"></div>
`;
document.getElementById('initial-colors').innerHTML = initialHtml;
window.createSpectrumPlot(sortedInitialColors, 'initial-plot');
}