You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, numColors = 5, iterations = 8) {
numColors = Number(numColors) || 5;
iterations = Number(iterations) || 8;
// 1. Prepare a downscaled canvas for faster pixel processing
const processCanvas = document.createElement('canvas');
const processCtx = processCanvas.getContext('2d');
// Scale image down to a maximum of 100x100 for fast K-Means clustering
const MAX_DIM = 100;
const scale = Math.min(MAX_DIM / originalImg.width, MAX_DIM / originalImg.height, 1);
const pw = Math.round(originalImg.width * scale);
const ph = Math.round(originalImg.height * scale);
processCanvas.width = pw || 1;
processCanvas.height = ph || 1;
processCtx.drawImage(originalImg, 0, 0, pw, ph);
const imgData = processCtx.getImageData(0, 0, processCanvas.width, processCanvas.height).data;
// 2. Extract opaque pixels
const pixels = [];
for (let i = 0; i < imgData.length; i += 4) {
if (imgData[i + 3] > 128) { // Ignore transparent pixels
pixels.push([imgData[i], imgData[i + 1], imgData[i + 2]]);
}
}
if (pixels.length === 0) {
pixels.push([0, 0, 0]); // Fallback if image is completely transparent
}
// 3. Simple K-Means++ Initialization
let centroids = [];
// Pick first centroid randomly
centroids.push(pixels[Math.floor(Math.random() * pixels.length)].slice());
// Pick the rest of the centroids by trying to find points dispersed from existing ones
for (let i = 1; i < numColors; i++) {
let maxSqDist = -1;
let nextCentroid = pixels[0];
// Sample random pixels to find a dispersed centroid (optimisation over checking all)
for (let step = 0; step < 100; step++) {
let candidate = pixels[Math.floor(Math.random() * pixels.length)];
let minSqDist = Infinity;
for (let c of centroids) {
let dist = (candidate[0] - c[0])**2 + (candidate[1] - c[1])**2 + (candidate[2] - c[2])**2;
if (dist < minSqDist) {
minSqDist = dist;
}
}
if (minSqDist > maxSqDist) {
maxSqDist = minSqDist;
nextCentroid = candidate;
}
}
centroids.push(nextCentroid.slice());
}
// 4. K-Means Clustering Iterations
let clusters = [];
for (let iter = 0; iter < iterations; iter++) {
clusters = Array.from({ length: numColors }, () => []);
// Assign pixels to closest centroid
for (let p of pixels) {
let minSqDist = Infinity;
let bestIdx = 0;
for (let i = 0; i < numColors; i++) {
let c = centroids[i];
let dist = (p[0] - c[0])**2 + (p[1] - c[1])**2 + (p[2] - c[2])**2;
if (dist < minSqDist) {
minSqDist = dist;
bestIdx = i;
}
}
clusters[bestIdx].push(p);
}
// Update centroids to the mean of their clusters
for (let i = 0; i < numColors; i++) {
if (clusters[i].length > 0) {
let sumR = 0, sumG = 0, sumB = 0;
for (let p of clusters[i]) {
sumR += p[0];
sumG += p[1];
sumB += p[2];
}
centroids[i] = [
Math.round(sumR / clusters[i].length),
Math.round(sumG / clusters[i].length),
Math.round(sumB / clusters[i].length)
];
}
}
}
// 5. Aggregate and Sort Clusters
let resultClusters = [];
for (let i = 0; i < numColors; i++) {
if (clusters[i].length > 0) {
resultClusters.push({
centroid: centroids[i],
count: clusters[i].length
});
}
}
// Sort descending by proportion
resultClusters.sort((a, b) => b.count - a.count);
// 6. Build the Visual UI (Kormulator Output)
const container = document.createElement('div');
container.style.fontFamily = 'system-ui, -apple-system, sans-serif';
container.style.padding = '20px';
container.style.backgroundColor = '#f7f9fa';
container.style.borderRadius = '12px';
container.style.display = 'flex';
container.style.flexDirection = 'column';
container.style.alignItems = 'center';
container.style.boxShadow = '0 8px 16px rgba(0,0,0,0.05)';
container.style.maxWidth = '600px';
container.style.margin = '0 auto';
container.style.boxSizing = 'border-box';
// Title
const title = document.createElement('h2');
title.textContent = 'Image Color Formula';
title.style.margin = '0 0 20px 0';
title.style.color = '#333';
container.appendChild(title);
// Render Original Image
const imgCanvas = document.createElement('canvas');
const imgCtx = imgCanvas.getContext('2d');
// Bound display size while maintaining aspect ratio
const DISPLAY_MAX = 400;
const dispScale = Math.min(DISPLAY_MAX / originalImg.width, DISPLAY_MAX / originalImg.height, 1);
imgCanvas.width = originalImg.width * dispScale;
imgCanvas.height = originalImg.height * dispScale;
imgCtx.drawImage(originalImg, 0, 0, imgCanvas.width, imgCanvas.height);
imgCanvas.style.maxWidth = '100%';
imgCanvas.style.borderRadius = '8px';
imgCanvas.style.marginBottom = '24px';
imgCanvas.style.boxShadow = '0 2px 8px rgba(0,0,0,0.15)';
container.appendChild(imgCanvas);
// Distribution Bar
const bar = document.createElement('div');
bar.style.display = 'flex';
bar.style.width = '100%';
bar.style.height = '36px';
bar.style.borderRadius = '18px';
bar.style.overflow = 'hidden';
bar.style.boxShadow = '0 2px 6px rgba(0,0,0,0.1)';
bar.style.marginBottom = '24px';
container.appendChild(bar);
// Swatch Cards Container
const details = document.createElement('div');
details.style.display = 'grid';
details.style.gridTemplateColumns = 'repeat(auto-fit, minmax(100px, 1fr))';
details.style.gap = '16px';
details.style.width = '100%';
container.appendChild(details);
// Utility to Convert RGB to Hex String
const rgbToHex = (r, g, b) => {
return "#" + (1 << 24 | r << 16 | g << 8 | b).toString(16).slice(1).toUpperCase();
};
// Populate UI with Cluster Data
let cumulativePct = 0;
for (let i = 0; i < resultClusters.length; i++) {
let cluster = resultClusters[i];
let hex = rgbToHex(cluster.centroid[0], cluster.centroid[1], cluster.centroid[2]);
// Handle rounding visually so the bar fills perfectly
let rawPct = (cluster.count / pixels.length) * 100;
let pctLabelText = rawPct.toFixed(1) + '%';
if (i === resultClusters.length - 1) { // ensure the last piece fills the gap
rawPct = 100 - cumulativePct;
}
cumulativePct += rawPct;
// Progress Bar Segment
let segment = document.createElement('div');
segment.style.width = rawPct + '%';
segment.style.backgroundColor = hex;
segment.style.transition = 'width 0.3s ease';
segment.title = `${hex} (${pctLabelText})`;
bar.appendChild(segment);
// Feature Card
let card = document.createElement('div');
card.style.backgroundColor = '#fff';
card.style.padding = '12px 8px';
card.style.borderRadius = '10px';
card.style.display = 'flex';
card.style.flexDirection = 'column';
card.style.alignItems = 'center';
card.style.boxShadow = '0 2px 5px rgba(0,0,0,0.04)';
card.style.border = '1px solid #eee';
let swatch = document.createElement('div');
swatch.style.width = '40px';
swatch.style.height = '40px';
swatch.style.backgroundColor = hex;
swatch.style.borderRadius = '50%';
swatch.style.marginBottom = '12px';
swatch.style.boxShadow = 'inset 0 1px 3px rgba(0,0,0,0.2)';
let hexLabel = document.createElement('div');
hexLabel.textContent = hex;
hexLabel.style.fontWeight = '600';
hexLabel.style.fontSize = '14px';
hexLabel.style.color = '#333';
hexLabel.style.marginBottom = '4px';
let pctLabel = document.createElement('div');
pctLabel.textContent = pctLabelText;
pctLabel.style.color = '#777';
pctLabel.style.fontSize = '13px';
card.appendChild(swatch);
card.appendChild(hexLabel);
card.appendChild(pctLabel);
details.appendChild(card);
}
return container;
}
Apply Changes