Design
COLOR PALETTE EXTRACTOR
Upload an image to extract its dominant colors. Export as CSS variables or JSON.
Drop an image here or click to upload
Developer Reference
Core Algorithm & Standalone Script
Standalone, zero-dependency JavaScript implementation powering this tool. Free to inspect, copy, and build upon.
let imageLoaded = false, pixelData = null, imgW = 0, imgH = 0;
let extractedColors = [];
const dropZone = document.getElementById('dropZone');
dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('dragover'); });
dropZone.addEventListener('dragleave', () => dropZone.classList.remove('dragover'));
dropZone.addEventListener('drop', e => { e.preventDefault(); dropZone.classList.remove('dragover'); handleFile(e.dataTransfer.files[0]); });
function handleFile(file) {
if (!file || !file.type.startsWith('image/')) return;
const reader = new FileReader();
reader.onload = e => {
const img = new Image();
img.onload = () => {
document.getElementById('previewImg').src = e.target.result;
document.getElementById('previewImg').style.display = 'block';
document.getElementById('controls').style.display = 'flex';
const canvas = document.getElementById('hiddenCanvas');
const ctx = canvas.getContext('2d');
const scale = Math.min(1, 200 / Math.max(img.width, img.height));
imgW = Math.round(img.width * scale);
imgH = Math.round(img.height * scale);
canvas.width = imgW; canvas.height = imgH;
ctx.drawImage(img, 0, 0, imgW, imgH);
pixelData = ctx.getImageData(0, 0, imgW, imgH).data;
imageLoaded = true;
extract();
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
}
function extract() {
if (!imageLoaded) return;
const n = parseInt(document.getElementById('numColors').value);
document.getElementById('numVal').textContent = n;
// Get all pixels
const pixels = [];
for (let i = 0; i < pixelData.length; i += 4) {
if (pixelData[i+3] < 128) continue;
pixels.push([pixelData[i], pixelData[i+1], pixelData[i+2]]);
}
// Median cut quantization
extractedColors = medianCut(pixels, n);
renderPalette();
}
function medianCut(pixels, n) {
if (pixels.length === 0) return [];
let buckets = [pixels];
while (buckets.length < n) {
// Find bucket with largest range
let maxRange = -1, maxIdx = 0;
buckets.forEach((b, i) => {
const range = bucketRange(b);
if (range.max > maxRange) { maxRange = range.max; maxIdx = i; }
});
const bucket = buckets.splice(maxIdx, 1)[0];
if (bucket.length < 2) { buckets.push(bucket); break; }
const range = bucketRange(bucket);
bucket.sort((a, b) => a[range.channel] - b[range.channel]);
const mid = Math.floor(bucket.length / 2);
buckets.push(bucket.slice(0, mid), bucket.slice(mid));
}
return buckets.map(b => {
const avg = [0, 0, 0];
b.forEach(p => { avg[0] += p[0]; avg[1] += p[1]; avg[2] += p[2]; });
return {
r: Math.round(avg[0] / b.length),
g: Math.round(avg[1] / b.length),
b: Math.round(avg[2] / b.length),
count: b.length
};
}).sort((a, b) => b.count - a.count);
}
function bucketRange(bucket) {
let minR=255,maxR=0,minG=255,maxG=0,minB=255,maxB=0;
bucket.forEach(p => {
if(p[0]<minR)minR=p[0];if(p[0]>maxR)maxR=p[0];
if(p[1]<minG)minG=p[1];if(p[1]>maxG)maxG=p[1];
if(p[2]<minB)minB=p[2];if(p[2]>maxB)maxB=p[2];
});
const rR=maxR-minR,rG=maxG-minG,rB=maxB-minB;
const max = Math.max(rR,rG,rB);
const channel = max===rR?0:max===rG?1:2;
return { max, channel };
}
function rgbToHex(r,g,b) {
return '#'+[r,g,b].map(v=>v.toString(16).padStart(2,'0')).join('');
}
function textColor(r,g,b) {
return (0.299*r+0.587*g+0.114*b) > 128 ? '#000000' : '#ffffff';
}
function renderPalette() {
const total = extractedColors.reduce((s,c) => s+c.count, 0);
const display = document.getElementById('paletteDisplay');
display.innerHTML = extractedColors.map(c => {
const hex = rgbToHex(c.r,c.g,c.b);
const pct = ((c.count/total)*100).toFixed(1);
const tc = textColor(c.r,c.g,c.b);
return `<div class="swatch-col" style="background:${hex};color:${tc}" onclick="copyHex('${hex}')">
<div class="swatch-info"><div class="swatch-hex">${hex}</div><div>rgb(${c.r},${c.g},${c.b})</div><div class="swatch-pct">${pct}%</div></div>
</div>`;
}).join('');
document.getElementById('exportSection').style.display = 'block';
updateCodeOutput();
}
function updateCodeOutput() {
const css = extractedColors.map((c,i) => ` --color-${i+1}: ${rgbToHex(c.r,c.g,c.b)};`).join('\n');
document.getElementById('codeOutput').textContent = `:root {\n${css}\n}`;
}
function copyCSS() {
const css = extractedColors.map((c,i) => ` --color-${i+1}: ${rgbToHex(c.r,c.g,c.b)};`).join('\n');
navigator.clipboard.writeText(`:root {\n${css}\n}`).catch(()=>{});
showToast('CSS copied');
}
function copyJSON() {
const json = JSON.stringify(extractedColors.map(c => rgbToHex(c.r,c.g,c.b)), null, 2);
navigator.clipboard.writeText(json).catch(()=>{});
showToast('JSON copied');
}
function copyHex(hex) {
navigator.clipboard.writeText(hex).catch(()=>{});
showToast('Copied ' + hex);
}
function showToast(msg) {
const t = document.getElementById('toast');
t.textContent = msg; t.classList.add('show');
setTimeout(() => t.classList.remove('show'), 2000);
}