You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, numColors = 5) {
// Parameter validation
let k = parseInt(numColors, 10);
if (isNaN(k) || k < 2) k = 5;
k = Math.max(1, Math.min(k, 20)); // clamp between 1 and 20
// Step 1: Downscale image for fast processing to extract colors
const processSize = 64;
const tempCanvas = document.createElement('canvas');
tempCanvas.width = processSize;
tempCanvas.height = processSize;
const tempCtx = tempCanvas.getContext('2d');
// Draw original image into small canvas
tempCtx.drawImage(originalImg, 0, 0, processSize, processSize);
const imgData = tempCtx.getImageData(0, 0, processSize, processSize).data;
// Collect valid pixels (ignoring highly transparent ones)
let pixels = [];
for (let i = 0; i < imgData.length; i += 4) {
if (imgData[i + 3] >= 128) { // alpha channel
pixels.push({
r: imgData[i],
g: imgData[i + 1],
b: imgData[i + 2]
});
}
}
// Fallback if image is fully transparent
if (pixels.length === 0) {
pixels.push({ r: 0, g: 0, b: 0 });
}
// Step 2: K-Means++ Initialization
let centroids = [];
centroids.push(pixels[Math.floor(Math.random() * pixels.length)]);
while (centroids.length < k) {
let maxDist = -1;
let bestPixel = pixels[0];
for (let i = 0; i < pixels.length; i++) {
let p = pixels[i];
let minDist = Infinity;
for (let c of centroids) {
let dist = (p.r - c.r) ** 2 + (p.g - c.g) ** 2 + (p.b - c.b) ** 2;
if (dist < minDist) minDist = dist;
}
if (minDist > maxDist) {
maxDist = minDist;
bestPixel = p;
}
}
centroids.push({ ...bestPixel });
}
// Step 3: K-Means Clustering (up to 10 iterations)
for (let iter = 0; iter < 10; iter++) {
let clusters = Array(k).fill(0).map(() => ({ r: 0, g: 0, b: 0, count: 0 }));
for (let i = 0; i < pixels.length; i++) {
let p = pixels[i];
let minDist = Infinity;
let minIdx = -1;
for (let j = 0; j < k; j++) {
let c = centroids[j];
let dist = (p.r - c.r) ** 2 + (p.g - c.g) ** 2 + (p.b - c.b) ** 2;
if (dist < minDist) {
minDist = dist;
minIdx = j;
}
}
clusters[minIdx].r += p.r;
clusters[minIdx].g += p.g;
clusters[minIdx].b += p.b;
clusters[minIdx].count++;
}
let changed = false;
for (let j = 0; j < k; j++) {
if (clusters[j].count > 0) {
let newR = Math.round(clusters[j].r / clusters[j].count);
let newG = Math.round(clusters[j].g / clusters[j].count);
let newB = Math.round(clusters[j].b / clusters[j].count);
if (newR !== centroids[j].r || newG !== centroids[j].g || newB !== centroids[j].b) {
changed = true;
}
centroids[j].r = newR;
centroids[j].g = newG;
centroids[j].b = newB;
} else {
// Handle empty clusters by re-assigning a random pixel
centroids[j] = { ...pixels[Math.floor(Math.random() * pixels.length)] };
changed = true;
}
}
if (!changed) break;
}
// Step 4: Sort colors by perceptual luminance
centroids.sort((a, b) => {
let lumA = 0.299 * a.r + 0.587 * a.g + 0.114 * a.b;
let lumB = 0.299 * b.r + 0.587 * b.g + 0.114 * b.b;
return lumB - lumA; // Lightest to darkest
});
// Step 5: Construction of Visual Output Canvas
const outCanvas = document.createElement('canvas');
const outCtx = outCanvas.getContext('2d');
const PADDING = 24;
const MAX_IMG_WIDTH = 800;
// Scale image to fit within max width while maintaining aspect ratio
const scale = Math.min(1, MAX_IMG_WIDTH / originalImg.width);
const imgDrawWidth = originalImg.width * scale;
const imgDrawHeight = originalImg.height * scale;
const paletteHeight = 100;
const gap = 8; // gap between swatches
outCanvas.width = imgDrawWidth + PADDING * 2;
outCanvas.height = PADDING + imgDrawHeight + PADDING + paletteHeight + PADDING;
// Background
outCtx.fillStyle = '#1e1e1e';
outCtx.fillRect(0, 0, outCanvas.width, outCanvas.height);
// Draw Original Image
outCtx.shadowColor = 'rgba(0,0,0,0.5)';
outCtx.shadowBlur = 10;
outCtx.drawImage(originalImg, PADDING, PADDING, imgDrawWidth, imgDrawHeight);
// Reset shadow for swatches
outCtx.shadowColor = 'transparent';
outCtx.shadowBlur = 0;
// Helper to convert RGB to HEX
const rgbToHex = (r, g, b) => {
return "#" + [r, g, b].map(x => {
const hex = x.toString(16);
return hex.length === 1 ? '0' + hex : hex;
}).join('').toUpperCase();
};
// Draw Color Palette Swatches
const swatchWidth = (imgDrawWidth - gap * (k - 1)) / k;
const paletteY = PADDING + imgDrawHeight + PADDING;
// Determine safe font size based on swatch layout
const fontSize = Math.max(10, Math.min(16, swatchWidth / 5));
for (let i = 0; i < k; i++) {
let c = centroids[i];
let hex = rgbToHex(c.r, c.g, c.b);
let x = PADDING + i * (swatchWidth + gap);
// Draw Swatch
outCtx.fillStyle = hex;
outCtx.fillRect(x, paletteY, swatchWidth, paletteHeight);
// Determine Text Color (Black/White contrast based on luminance)
let lum = 0.299 * c.r + 0.587 * c.g + 0.114 * c.b;
outCtx.fillStyle = lum > 140 ? '#000000' : '#FFFFFF';
// Draw Hex Text
outCtx.font = `bold ${fontSize}px sans-serif`;
outCtx.textAlign = 'center';
outCtx.textBaseline = 'middle';
// Prevent text from overflowing its box drastically
if (swatchWidth > 40) {
outCtx.fillText(hex, x + swatchWidth / 2, paletteY + paletteHeight / 2);
}
}
return outCanvas;
}
Apply Changes