You can edit the below JavaScript code to customize the image tool.
Apply Changes
/**
* Creates a theme (a color palette) from an image by finding the dominant colors.
* This function uses a k-means clustering algorithm to identify the most representative
* colors in the source image, simulating an "AI" powered theme creator.
* The result is a canvas displaying the original image with the generated color
* palette swatches and their hex codes underneath.
*
* @param {HTMLImageElement} originalImg The source image object.
* @param {number} [paletteSize=5] The number of dominant colors to extract for the theme palette.
* @returns {Promise<HTMLCanvasElement>} A canvas element containing the original image and the generated color theme palette.
*/
async function processImage(originalImg, paletteSize = 5) {
// Ensure paletteSize is a valid positive integer
const k = Math.max(1, parseInt(paletteSize, 10) || 5);
// --- 1. Get Pixel Data ---
// Use a temporary canvas to draw the image and extract its pixel data.
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
tempCanvas.width = originalImg.naturalWidth;
tempCanvas.height = originalImg.naturalHeight;
tempCtx.drawImage(originalImg, 0, 0);
let imageData;
try {
imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
} catch (e) {
console.error("Could not get image data. The image might be cross-origin.", e);
// Create a fallback canvas with an error message
const errorCanvas = document.createElement('canvas');
errorCanvas.width = originalImg.width;
errorCanvas.height = 100;
const errorCtx = errorCanvas.getContext('2d');
errorCtx.fillStyle = '#f0f0f0';
errorCtx.fillRect(0, 0, errorCanvas.width, errorCanvas.height);
errorCtx.fillStyle = '#ff0000';
errorCtx.font = '16px Arial';
errorCtx.textAlign = 'center';
errorCtx.fillText('Error: Could not process image.', errorCanvas.width / 2, 40);
errorCtx.fillText('Image may be from another domain (CORS issue).', errorCanvas.width / 2, 60);
return errorCanvas;
}
const pixels = imageData.data;
const pixelArray = [];
// Create an array of [R, G, B] values, ignoring transparent pixels
for (let i = 0; i < pixels.length; i += 4) {
if (pixels[i + 3] > 128) { // Alpha channel check
pixelArray.push([pixels[i], pixels[i + 1], pixels[i + 2]]);
}
}
// If image is fully transparent or empty, return a default palette
if (pixelArray.length === 0) {
pixelArray.push([255, 255, 255], [221, 221, 221], [170, 170, 170], [85, 85, 85], [0, 0, 0]);
}
/**
* --- 2. K-Means Clustering Algorithm ---
* This algorithm finds 'k' cluster centers (centroids) in the pixel color data.
* These centroids represent the dominant colors in the image.
*/
function getDominantColors(pixels, k) {
// Use a subset of pixels for performance on large images
const sample = pixels.length > 20000 ? pixels.filter((_, i) => i % Math.floor(pixels.length / 20000) === 0) : pixels;
// Helper to calculate squared distance (faster than Euclidean)
const distance = (a, b) => (a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2 + (a[2] - b[2]) ** 2;
// Initialize centroids by picking random pixels from the sample
let centroids = [];
const usedIndices = new Set();
while (centroids.length < k && centroids.length < sample.length) {
const index = Math.floor(Math.random() * sample.length);
if (!usedIndices.has(index)) {
centroids.push([...sample[index]]);
usedIndices.add(index);
}
}
const maxIterations = 20;
for (let iter = 0; iter < maxIterations; iter++) {
const clusters = Array.from({ length: k }, () => []);
// Assign each pixel to the closest centroid
for (const pixel of sample) {
let minDistance = Infinity;
let closestCentroidIndex = 0;
for (let i = 0; i < k; i++) {
const d = distance(pixel, centroids[i]);
if (d < minDistance) {
minDistance = d;
closestCentroidIndex = i;
}
}
clusters[closestCentroidIndex].push(pixel);
}
let hasConverged = true;
const newCentroids = [];
// Recalculate centroids
for (let i = 0; i < k; i++) {
if (clusters[i].length === 0) {
// If a cluster is empty, re-initialize its centroid to a random pixel
newCentroids.push(sample[Math.floor(Math.random() * sample.length)]);
hasConverged = false;
continue;
}
const sum = clusters[i].reduce((acc, p) => [acc[0] + p[0], acc[1] + p[1], acc[2] + p[2]], [0, 0, 0]);
const avg = sum.map(c => Math.round(c / clusters[i].length));
if (distance(avg, centroids[i]) > 1) {
hasConverged = false;
}
newCentroids.push(avg);
}
centroids = newCentroids;
if (hasConverged) break;
}
return centroids;
}
const dominantColors = getDominantColors(pixelArray, k);
// --- 3. Create Output Canvas ---
const swatchHeight = Math.max(60, Math.min(100, originalImg.naturalWidth / k));
const outputCanvas = document.createElement('canvas');
const ctx = outputCanvas.getContext('2d');
outputCanvas.width = originalImg.naturalWidth;
outputCanvas.height = originalImg.naturalHeight + swatchHeight;
// Draw original image
ctx.drawImage(originalImg, 0, 0);
// Draw color palette swatches
const swatchWidth = outputCanvas.width / dominantColors.length;
// Helper functions for drawing
const rgbToHex = (r, g, b) => '#' + [r, g, b].map(x => x.toString(16).padStart(2, '0')).join('');
const getTextColor = (r, g, b) => (0.299 * r + 0.587 * g + 0.114 * b) > 128 ? '#000000' : '#FFFFFF';
dominantColors.forEach((color, index) => {
const [r, g, b] = color;
const hex = rgbToHex(r, g, b);
const x = index * swatchWidth;
const y = originalImg.naturalHeight;
// Draw color swatch
ctx.fillStyle = `rgb(${r}, ${g}, ${b})`;
ctx.fillRect(x, y, swatchWidth, swatchHeight);
// Draw hex code on top
ctx.fillStyle = getTextColor(r, g, b);
const fontSize = Math.max(12, Math.min(18, swatchWidth / 6));
ctx.font = `${fontSize}px Arial`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(hex.toUpperCase(), x + swatchWidth / 2, y + swatchHeight / 2);
});
return outputCanvas;
}
Apply Changes