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);
}