Extract colors

by Marc Malignan

HTML

<div id="main">
  <form id="form">
    <div class="form-row withLabel">
      <label for="detail">Detail</label>
      <input id="detail" type="number" value="1000" />
      <span class="helper">Number of pixels to analyze</span>
    </div>
    <div class="form-row withLabel">
      <label for="nbColors">Nb colors</label>
      <input id="nbColors" type="number" value="5" />
      <span class="helper">Number of results</span>
    </div>
    <div class="form-row">
      <input id="upload" type='file'>
      <img id="image" src="" />
    </div>
    <button type="submit" disabled>Analyze</button>
  </form>
  <div id="results"></div>
</div>

CSS

* {
  box-sizing: border-box;
}

body,
html {
  margin: 0;
  height: 100%;
}

body {
  background: linear-gradient(#333, #222);
}

#main {
  position: absolute;
  left: 50%;
  top: 50%;
  width: calc((70px * 5) + (5px * 10));
  transform: translate(-50%, -50%);
  color: #eee;
  text-align: center;
  font-family: Arial, sans-serif;
}

#form,
.form-row {
  margin-bottom: 10px;
}

.form-row.withLabel {
  display: flex;
  align-items: center;
}

.form-row.withLabel>label {
  flex-shrink: 0;
  width: calc(100% / 3);
  padding-right: 10px;
  text-align: right;
}

.form-row input[type=number] {
  flex-shrink: 0;
  width: 70px;
}

.form-row .helper {
  padding-left: 10px;
  font-size: 0.8rem;
  opacity: 0.5;
  text-align: left;
}

#image {
  display: none;
  width: 100%;
  max-width: 200px;
  margin: 10px auto 0;
  box-shadow: 0 5px 10px rgba(0, 0, 0, 0.3);
}

#results {
  display: flex;
  justify-content: center;
  flex-wrap: wrap;
  margin: 0 auto;
}

.result {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 70px;
  height: 70px;
  margin: 5px;
  border-radius: 50%;
  font-family: monospace;
  font-size: 0.9rem;
  box-shadow: 0 5px 10px rgba(0, 0, 0, 0.3);
}

JavaScript

const imgEl = document.getElementById('image');
const uploadEl = document.getElementById('upload');
const formEl = document.getElementById('form');
const resultsEl = document.getElementById('results');
const detailEl = document.getElementById('detail');
const nbColorsEl = document.getElementById('nbColors');

const createCanvas = img => {
  const canvas = document.createElement('canvas');
  canvas.width = img.naturalWidth;
  canvas.height = img.naturalHeight;
  canvas.getContext('2d').drawImage(img, 0, 0, img.naturalWidth, img.naturalHeight);
  return canvas;
}

const getColors = (canvas, detail) => {
  const colors = [];
  let total = 0;

  const nbPixels = canvas.width * canvas.height;
  const analyzeSpacing = detail < nbPixels ? Math.floor(nbPixels / detail) : 1;

  let index = 0;
  for (let y = 0; y < canvas.height; y++) {
    for (let x = 0; x < canvas.width; x++) {
      index++;
      if (index % analyzeSpacing === 0) {
        const pixel = canvas.getContext('2d').getImageData(x, y, 1, 1).data;
        const rgb = pixel.slice(0, 3).join();

        const colorIndex = colors.findIndex(color => color.rgb === rgb);

        if (colorIndex > -1) {
          colors[colorIndex].nb++;
        } else {
          colors.push({
            rgb,
            nb: 1
          });
        }

        total++;
      }
    }
  }

  return colors.sort((colorA, colorB) => colorB.nb - colorA.nb);
}

const findClosestColor = (color, colors) => {
  const colorRgb = color.rgb.split(',');
  return [...colors].reduce((prev, curr) => {
    const prevRgb = prev.rgb.split(',');
    const currRgb = curr.rgb.split(',');
    return (
      Math.abs(currRgb[0] - colorRgb[0]) < Math.abs(prevRgb[0] - colorRgb[0]) &&
      Math.abs(currRgb[1] - colorRgb[1]) < Math.abs(prevRgb[1] - colorRgb[1]) &&
      Math.abs(currRgb[2] - colorRgb[2]) < Math.abs(prevRgb[2] - colorRgb[2])
    ) ? curr : prev;
  });
}

const approximateColors = (colors, nbColors) => {
  if (colors.length <= nbColors) return...