パレット生成


最初のパレット

こっから始める。

支配的な色をソートで選択する。
基準となる色をなんか適当に作成する。
基準色から中間色を作り、最終パレットを生成する。

createBalancedPalette

この `createBalancedPalette` 関数は、支配的な色の色相を均等に分散させて、バランスの取れた色のパレットを作成する手順を示しています。以下に、手順を説明します。

  1. 空の `hues` 配列を初期化します。この配列には、生成される色相の値が格納されます。

  2. `step` 変数を計算します。これは、色相環を等分する際のステップサイズを表します。`1 / baseSize` という計算式で、`baseSize` は生成するパレットの色数を指定します。

  3. `baseSize` の回数だけループを実行します。

    • ループ変数 `i` を使用して、`dominantHues` 配列から現在の色相を取得します。`i % dominantHues.length` という計算式で、`i` が `dominantHues` の長さを超えた場合にはインデックスを循環させます。

    • 現在の色相に `i * step` を加算し、色相環上で等間隔に分散させます。`% 1` を使用して、色相値を0から1の範囲内に収めます。

    • 現在の色相の彩度と明度をそのまま使用します。

    • 生成された色相、彩度、明度の組み合わせを `hues` 配列に追加します。

  4. `hues` 配列内の色相を昇順にソートします。これにより、色相環上で連続的に並ぶようになります。

  5. ソートされた `hues` 配列を返却します。この配列には、均等に分散された色相とその彩度・明度が含まれています。

この手順により、支配的な色の色相を均等に分散させたバランスの取れた色のパレットが生成されます。色相環上で等間隔に色を配置することで、調和のとれた色の組み合わせが得られます。

ただし、この関数では色相のみを考慮しており、彩度と明度は支配的な色のものをそのまま使用しています。必要に応じて、彩度と明度についても調整を加えることで、さらに洗練されたパレットが得られる可能性があります。

%%html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Color Palette Generator</title>
<style>
  body {
    font-family: Arial, sans-serif;
    text-align: center;
  }
  #palette, #dominant-colors, #base-colors {
    display: flex;
    justify-content: center;
    margin-top: 20px;
  }
  .color-block {
    width: 50px;
    height: 50px;
    margin: 5px;
  }
  #drop-area {
    border: 2px dashed #ccc;
    border-radius: 20px;
    padding: 20px;
    width: 80%;
    margin: 20px auto;
  }
  #thumbnail {
    max-width: 300px;
    max-height: 300px;
    margin: 20px auto;
  }
</style>
</head>
<body>
<h1>Color Palette Generator</h1>
<div id="drop-area">ここに画像をコピペしてください</div>
<canvas id="canvas" style="display: none;"></canvas>
<img id="thumbnail" alt="Image thumbnail">
<h2>支配的な色</h2>
<div id="dominant-colors"></div>
<h2>基準に用いた色</h2>
<div id="base-colors"></div>
<h2>パレット</h2>
<div id="palette"></div>

<script>
  const dropArea = document.getElementById('drop-area');
  const canvas = document.getElementById('canvas');
  const ctx = canvas.getContext('2d');
  const paletteDiv = document.getElementById('palette');
  const dominantColorsDiv = document.getElementById('dominant-colors');
  const baseColorsDiv = document.getElementById('base-colors');
  const thumbnail = document.getElementById('thumbnail');

  const paletteCount = 24;

  dropArea.addEventListener('paste', (event) => {
    const items = (event.clipboardData || event.originalEvent.clipboardData).items;
    for (let i = 0; i < items.length; i++) {
      if (items[i].type.indexOf('image') !== -1) {
        const file = items[i].getAsFile();
        const reader = new FileReader();
        reader.onload = (e) => {
          const img = new Image();
          img.onload = () => {
            thumbnail.src = e.target.result;
            canvas.width = img.width;
            canvas.height = img.height;
            ctx.drawImage(img, 0, 0);
            const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
            const colors = getColors(imageData);
            const { dominantColors, baseColors, palette } = generatePalette(colors, paletteCount);
            displayColors(dominantColors, dominantColorsDiv);
            displayBaseColors(baseColors, baseColorsDiv);
            displayPalette(palette);
          }
          img.src = e.target.result;
        }
        reader.readAsDataURL(file);
      }
    }
  });

  function rgbToHsv(r, g, b) {
    r /= 255, g /= 255, b /= 255;
    const max = Math.max(r, g, b), min = Math.min(r, g, b);
    let h, s, v = max;

    const d = max - min;
    s = max === 0 ? 0 : d / max;

    if (max === min) {
      h = 0;
    } else {
      switch (max) {
        case r: h = (g - b) / d + (g < b ? 6 : 0); break;
        case g: h = (b - r) / d + 2; break;
        case b: h = (r - g) / d + 4; break;
      }
      h /= 6;
    }

    return [h, s, v];
  }

  function getColors(imageData) {
    const data = imageData.data;
    const colors = {};
    for (let i = 0; i < data.length; i += 4) {
      const r = data[i];
      const g = data[i + 1];
      const b = data[i + 2];
      const color = `rgb(${r},${g},${b})`;
      const hsv = rgbToHsv(r, g, b);
      if (colors[color]) {
        colors[color].count++;
      } else {
        colors[color] = { count: 1, hsv, r, g, b };
      }
    }
    return colors;
  }

  function generatePalette(colors, paletteSize) {
    const keys = Object.keys(colors);
    if (keys.length === 0) return { dominantColors: [], baseColors: [], palette: [] };

    // 支配的な色を抽出
    const sortedColors = keys.sort((a, b) => colors[b].count - colors[a].count);
    const dominantColors = sortedColors.slice(0, Math.min(paletteSize, sortedColors.length)).map(color => colors[color]);
    const dominantHues = dominantColors.map(color => color.hsv);

    // 基準色の決定
    const baseColors = createBalancedPalette(dominantHues, Math.min(dominantHues.length, Math.ceil(paletteSize / 2)));
    const palette = generateFinalPalette(baseColors, paletteSize);

    return { dominantColors, baseColors, palette };
  }

  function createBalancedPalette(dominantHues, baseSize) {
    const hues = [];
    const step = 1 / baseSize;

    for (let i = 0; i < baseSize; i++) {
      const currentHue = dominantHues[i % dominantHues.length];
      let hue = (currentHue[0] + i * step) % 1;
      let saturation = currentHue[1];
      let value = currentHue[2];
      hues.push([hue, saturation, value]);
    }

    hues.sort((a, b) => a[0] - b[0]);
    return hues;
  }

  function generateFinalPalette(baseColors, paletteSize) {
    const finalPalette = [];
    const totalColors = baseColors.length;

    // まずは基準となる色をパレットに追加
    baseColors.forEach(hsv => {
      finalPalette.push(hsvToRgb(hsv[0], hsv[1], hsv[2]));
    });

    // 基準となる色の間に中間色を追加
    for (let i = 0; i < totalColors; i++) {
      const currentColor = baseColors[i];
      const nextColor = baseColors[(i + 1) % totalColors];  // 次の基準色(最後の色の次は最初の色)

      // 現在の色と次の色の間に挿入する中間色の数
      const steps = Math.floor((paletteSize - totalColors) / totalColors);

      for (let j = 1; j <= steps; j++) {
        const ratio = j / (steps + 1);
        const intermediateHue = (currentColor[0] * (1 - ratio) + nextColor[0] * ratio) % 1;
        const intermediateSaturation = currentColor[1] * (1 - ratio) + nextColor[1] * ratio;
        const intermediateValue = currentColor[2] * (1 - ratio) + nextColor[2] * ratio;
        finalPalette.push(hsvToRgb(intermediateHue, intermediateSaturation, intermediateValue));
      }
    }

    // パレットサイズを満たすために必要な色を追加
    while (finalPalette.length < paletteSize) {
      finalPalette.push(finalPalette[finalPalette.length % totalColors]);
    }

    // 色相環上の順番に並べる
    finalPalette.sort((a, b) => {
      const hueA = rgbToHsvString(a)[0];
      const hueB = rgbToHsvString(b)[0];
      return hueA - hueB;
    });

    return finalPalette;
  }

  function rgbToHsvString(rgb) {
    const [r, g, b] = rgb.match(/\d+/g).map(Number);
    return rgbToHsv(r, g, b);
  }

  function hsvToRgb(h, s, v) {
    let r, g, b;
    let i = Math.floor(h * 6);
    let f = h * 6 - i;
    let p = v * (1 - s);
    let q = v * (1 - f * s);
    let t = v * (1 - (1 - f) * s);

    switch (i % 6) {
      case 0: r = v, g = t, b = p; break;
      case 1: r = q, g = v, b = p; break;
      case 2: r = p, g = v, b = t; break;
      case 3: r = p, g = q, b = v; break;
      case 4: r = t, g = p, b = v; break;
      case 5: r = v, g = p, b = q; break;
    }

    return `rgb(${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round(b * 255)})`;
  }

  function displayColors(colors, container) {
    container.innerHTML = '';
    colors.sort((a, b) => a.hsv[0] - b.hsv[0]);
    colors.forEach(color => {
      const div = document.createElement('div');
      div.className = 'color-block';
      div.style.backgroundColor = `rgb(${color.r}, ${color.g}, ${color.b})`;
      container.appendChild(div);
    });
  }

  function displayBaseColors(baseColors, container) {
    container.innerHTML = '';
    baseColors.sort((a, b) => a[0] - b[0]);
    baseColors.forEach(hsv => {
      const div = document.createElement('div');
      div.className = 'color-block';
      div.style.backgroundColor = hsvToRgb(hsv[0], hsv[1], hsv[2]);
      container.appendChild(div);
    });
  }

  function displayPalette(palette) {
    paletteDiv.innerHTML = '';
    palette.forEach(color => {
      const div = document.createElement('div');
      div.className = 'color-block';
      div.style.backgroundColor = color;
      paletteDiv.appendChild(div);
    });
  }
</script>
</body>
</html>


kmeansパレット

支配色選択にkmeansを使用する。

%%html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Color Palette Generator</title>
<style>
  body {
    font-family: Arial, sans-serif;
    text-align: center;
  }
  #palette, #dominant-colors, #base-colors {
    display: flex;
    justify-content: center;
    margin-top: 20px;
  }
  .color-block {
    width: 50px;
    height: 50px;
    margin: 5px;
  }
  #drop-area {
    border: 2px dashed #ccc;
    border-radius: 20px;
    padding: 20px;
    width: 80%;
    margin: 20px auto;
  }
  #thumbnail {
    max-width: 300px;
    max-height: 300px;
    margin: 20px auto;
  }
</style>
</head>
<body>
<h1>Color Palette Generator</h1>
<div id="drop-area">ここに画像をコピペしてください</div>
<canvas id="canvas" style="display: none;"></canvas>
<img id="thumbnail" alt="Image thumbnail">
<h2>支配的な色</h2>
<div id="dominant-colors"></div>
<h2>基準に用いた色</h2>
<div id="base-colors"></div>
<h2>パレット</h2>
<div id="palette"></div>

<script>
  const dropArea = document.getElementById('drop-area');
  const canvas = document.getElementById('canvas');
  const ctx = canvas.getContext('2d');
  const paletteDiv = document.getElementById('palette');
  const dominantColorsDiv = document.getElementById('dominant-colors');
  const baseColorsDiv = document.getElementById('base-colors');
  const thumbnail = document.getElementById('thumbnail');

  dropArea.addEventListener('paste', (event) => {
    const items = (event.clipboardData || event.originalEvent.clipboardData).items;
    for (let i = 0; i < items.length; i++) {
      if (items[i].type.indexOf('image') !== -1) {
        const file = items[i].getAsFile();
        const reader = new FileReader();
        reader.onload = (e) => {
          const img = new Image();
          img.onload = () => {
            thumbnail.src = e.target.result;
            canvas.width = img.width;
            canvas.height = img.height;
            ctx.drawImage(img, 0, 0);
            const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
            const colors = getColors(imageData);
            const dominantColors = extractDominantColorsKMeans(colors, 8);
            const baseColors = determineBaseColors(dominantColors, 4);
            const palette = generateFinalPalette(baseColors, 8);
            displayColors(dominantColors, dominantColorsDiv);
            displayBaseColors(baseColors, baseColorsDiv);
            displayPalette(palette);
          }
          img.src = e.target.result;
        }
        reader.readAsDataURL(file);
      }
    }
  });

  function rgbToHsv(r, g, b) {
    r /= 255, g /= 255, b /= 255;
    const max = Math.max(r, g, b), min = Math.min(r, g, b);
    let h, s, v = max;

    const d = max - min;
    s = max === 0 ? 0 : d / max;

    if (max === min) {
      h = 0;
    } else {
      switch (max) {
        case r: h = (g - b) / d + (g < b ? 6 : 0); break;
        case g: h = (b - r) / d + 2; break;
        case b: h = (r - g) / d + 4; break;
      }
      h /= 6;
    }

    return [h, s, v];
  }

  function getColors(imageData) {
    const data = imageData.data;
    const colors = [];
    for (let i = 0; i < data.length; i += 4) {
      const r = data[i];
      const g = data[i + 1];
      const b = data[i + 2];
      colors.push([r, g, b]);
    }
    return colors;
  }

  function extractDominantColorsKMeans(colors, numColors) {
    const result = kMeans(colors, numColors);
    const dominantColors = result.map(cluster => {
      const [r, g, b] = cluster.centroid;
      const hsv = rgbToHsv(r, g, b);
      return { r, g, b, hsv };
    });
    return dominantColors;
  }

  function determineBaseColors(dominantColors, numBaseColors) {
    const dominantHues = dominantColors.map(color => color.hsv);
    return createBalancedPalette(dominantHues, Math.min(dominantHues.length, numBaseColors));
  }

  function createBalancedPalette(dominantHues, baseSize) {
    const hues = [];
    const step = 1 / baseSize;

    for (let i = 0; i < baseSize; i++) {
      const currentHue = dominantHues[i % dominantHues.length];
      let hue = currentHue[0] + i * step;  // 色相を等間隔に増加
      let saturation = currentHue[1];
      let value = currentHue[2];
      hues.push([hue, saturation, value]);
    }

    hues.sort((a, b) => a[0] - b[0]);  // 色相 (Hue) に基づいてソート
    return hues;
  }

  function generateFinalPalette(baseColors, paletteSize) {
    const finalPalette = [];
    const totalColors = baseColors.length;

    // まずは基準となる色をパレットに追加
    baseColors.forEach(hsv => {
      finalPalette.push(hsvToRgb(hsv[0], hsv[1], hsv[2]));
    });

    // 基準となる色の間に中間色を追加
    for (let i = 0; i < totalColors; i++) {
      const currentColor = baseColors[i];
      const nextColor = baseColors[(i + 1) % totalColors];  // 次の基準色(最後の色の次は最初の色)

      // 現在の色と次の色の間に挿入する中間色の数
      const steps = Math.floor((paletteSize - totalColors) / totalColors);

      for (let j = 1; j <= steps; j++) {
        const ratio = j / (steps + 1);
        const intermediateHue = (currentColor[0] * (1 - ratio) + nextColor[0] * ratio) % 1;
        const intermediateSaturation = currentColor[1] * (1 - ratio) + nextColor[1] * ratio;
        const intermediateValue = currentColor[2] * (1 - ratio) + nextColor[2] * ratio;
        finalPalette.push(hsvToRgb(intermediateHue, intermediateSaturation, intermediateValue));
      }
    }

    // パレットサイズを満たすために必要な色を追加
    while (finalPalette.length < paletteSize) {
      finalPalette.push(finalPalette[finalPalette.length % totalColors]);
    }

    // 色相環上の順番に並べる
    finalPalette.sort((a, b) => {
      const hueA = rgbToHsvString(a)[0];
      const hueB = rgbToHsvString(b)[0];
      return hueA - hueB;
    });

    return finalPalette;
  }

  function rgbToHsvString(rgb) {
    const [r, g, b] = rgb.match(/\d+/g).map(Number);
    return rgbToHsv(r, g, b);
  }

  function hsvToRgb(h, s, v) {
    let r, g, b;
    let i = Math.floor(h * 6);
    let f = h * 6 - i;
    let p = v * (1 - s);
    let q = v * (1 - f * s);
    let t = v * (1 - (1 - f) * s);

    switch (i % 6) {
      case 0: r = v, g = t, b = p; break;
      case 1: r = q, g = v, b = p; break;
      case 2: r = p, g = v, b = t; break;
      case 3: r = p, g = q, b = v; break;
      case 4: r = t, g = p, b = v; break;
      case 5: r = v, g = p, b = q; break;
    }

    return `rgb(${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round(b * 255)})`;
  }

  function displayColors(colors, container) {
    container.innerHTML = '';
    colors.sort((a, b) => a.hsv[0] - b.hsv[0]);
    colors.forEach(color => {
      const div = document.createElement('div');
      div.className = 'color-block';
      div.style.backgroundColor = `rgb(${color.r}, ${color.g}, ${color.b})`;
      container.appendChild(div);
    });
  }

  function displayBaseColors(baseColors, container) {
    container.innerHTML = '';
    baseColors.sort((a, b) => a[0] - b[0]);
    baseColors.forEach(hsv => {
      const div = document.createElement('div');
      div.className = 'color-block';
      div.style.backgroundColor = hsvToRgb(hsv[0], hsv[1], hsv[2]);
      container.appendChild(div);
    });
  }

  function displayPalette(palette) {
    paletteDiv.innerHTML = '';
    palette.forEach(color => {
      const div = document.createElement('div');
      div.className = 'color-block';
      div.style.backgroundColor = color;
      paletteDiv.appendChild(div);
    });
  }

  function areCentroidsEqual(centroids1, centroids2, threshold = 1e-5) {
    if (centroids1.length !== centroids2.length) return false;
    return centroids1.every((centroid, i) => {
      return centroid.every((value, j) => Math.abs(value - centroids2[i][j]) < threshold);
    });
  }

  function getRandomCentroids(colors, k) {
    const centroids = [];
    const usedIndexes = new Set();

    while (centroids.length < k) {
      const randomIndex = Math.floor(Math.random() * colors.length);
      if (!usedIndexes.has(randomIndex)) {
        centroids.push([...colors[randomIndex]]);
        usedIndexes.add(randomIndex);
      }
    }

    return centroids;
  }

  function kMeans(colors, k, maxIterations = 100) {
    //let centroids = colors.slice(0, k).map(color => [...color]);//ランダムですらない初期セントロイド
    let centroids = getRandomCentroids(colors, k);
    let prevCentroids = JSON.parse(JSON.stringify(centroids));
    let clusters = Array(k).fill().map(() => []);
    let iterations = 0;

    while (!areCentroidsEqual(centroids, prevCentroids) && iterations < maxIterations) {
      clusters = Array(k).fill().map(() => []);
      prevCentroids = JSON.parse(JSON.stringify(centroids));

      colors.forEach(color => {
        let minDistance = Infinity;
        let clusterIndex = 0;

        centroids.forEach((centroid, index) => {
          const distance = labDistance(rgbToLab(color), rgbToLab(centroid));
          if (distance < minDistance) {
            minDistance = distance;
            clusterIndex = index;
          }
        });

        clusters[clusterIndex].push(color);
      });

      centroids = clusters.map(cluster => {
        const length = cluster.length;
        const mean = cluster.reduce((acc, val) => acc.map((sum, i) => sum + val[i]), [0, 0, 0])
                            .map(sum => sum / length);
        return mean;
      });

      iterations++;
    }

    return centroids.map((centroid, index) => ({
      centroid,
      points: clusters[index]
    }));
  }

  function labDistance(lab1, lab2) {
    const [l1, a1, b1] = lab1;
    const [l2, a2, b2] = lab2;
    return Math.sqrt(Math.pow(l1 - l2, 2) + Math.pow(a1 - a2, 2) + Math.pow(b1 - b2, 2));
  }

  function rgbToLab(rgb) {
    const [r, g, b_rgb] = rgb.map(v => v / 255);
    let x = r > 0.04045 ? Math.pow((r + 0.055) / 1.055, 2.4) : r / 12.92;
    let y = g > 0.04045 ? Math.pow((g + 0.055) / 1.055, 2.4) : g / 12.92;
    let z = b_rgb > 0.04045 ? Math.pow((b_rgb + 0.055) / 1.055, 2.4) : b_rgb / 12.92;

    x *= 100;
    y *= 100;
    z *= 100;

    x = x * 0.4124 + y * 0.3576 + z * 0.1805;
    y = x * 0.2126 + y * 0.7152 + z * 0.0722;
    z = x * 0.0193 + y * 0.1192 + z * 0.9505;

    x /= 95.047;
    y /= 100;
    z /= 108.883;

    x = x > 0.008856 ? Math.pow(x, 1/3) : (7.787 * x) + (16 / 116);
    y = y > 0.008856 ? Math.pow(y, 1/3) : (7.787 * y) + (16 / 116);
    z = z > 0.008856 ? Math.pow(z, 1/3) : (7.787 * z) + (16 / 116);

    const l = (116 * y) - 16;
    const a = 500 * (x - y);
    const b_lab = 200 * (y - z);

    return [l, a, b_lab];
  }

  function hsvDistance(hsv1, hsv2) {
    const [h1, s1, v1] = hsv1;
    const [h2, s2, v2] = hsv2;
    const dh = Math.min(Math.abs(h1 - h2), 1 - Math.abs(h1 - h2));
    const ds = Math.abs(s1 - s2);
    const dv = Math.abs(v1 - v2);
    return Math.sqrt(dh * dh + ds * ds + dv * dv);
  }

  function ycbcrDistance(ycbcr1, ycbcr2) {
    const [y1, cb1, cr1] = ycbcr1;
    const [y2, cb2, cr2] = ycbcr2;
    return Math.sqrt(Math.pow(y1 - y2, 2) + Math.pow(cb1 - cb2, 2) + Math.pow(cr1 - cr2, 2));
  }

  function rgbDistance(rgb1, rgb2) {
    const [r1, g1, b1] = rgb1;
    const [r2, g2, b2] = rgb2;
    return Math.sqrt(Math.pow(r1 - r2, 2) + Math.pow(g1 - g2, 2) + Math.pow(b1 - b2, 2));
  }

  function rgbToHsv(r, g, b) {
    r /= 255, g /= 255, b /= 255;
    const max = Math.max(r, g, b), min = Math.min(r, g, b);
    let h, s, v = max;

    const d = max - min;
    s = max === 0 ? 0 : d / max;

    if (max === min) {
      h = 0;
    } else {
      switch (max) {
        case r: h = (g - b) / d + (g < b ? 6 : 0); break;
        case g: h = (b - r) / d + 2; break;
        case b: h = (r - g) / d + 4; break;
      }
      h /= 6;
    }

    return [h, s, v];
  }

  function rgbToYCbCr(r, g, b) {
    const y = 0.299 * r + 0.587 * g + 0.114 * b;
    const cb = 128 - 0.168736 * r - 0.331264 * g + 0.5 * b;
    const cr = 128 + 0.5 * r - 0.418688 * g - 0.081312 * b;
    return [y, cb, cr];
  }
</script>
</body>
</html>


Google Colabでセル毎に管理する場合。


Google Colab側にファイルをセーブしていく。
ここではマジックコマンド%%writefileを用いる。

マジックコマンドが成立するとファイルが作られる。

セルをまとめて動かしたりなんだり。

セルにタイトルを付け、折りたたんだりできる。
@title Utils

セルの左の方を右クリックしてフォームを追加を実行。


Google Colab側に仮想サーバー立てる


最初に動かしておく。切りたくなったらChatGPTにきいてください。

# @title Server
import http.server
import socketserver

PORT = 8000

Handler = http.server.SimpleHTTPRequestHandler

httpd = socketserver.TCPServer(("", PORT), Handler)
print("serving at port", PORT)
#httpd.serve_forever()

# # サーバーをバックグラウンドで実行
import threading
thread = threading.Thread(target=httpd.serve_forever)
thread.start()

color_palette.js


jsファイルはGoogle Colab上に保存された状態にしておく。
最終的にhtmlに読み込まれる。

# @title ColorPalette
%%writefile color_palette.js
import { handlePasteEvent } from './event_handlers.js';
import { displayColors, displayBaseColors, displayPalette } from './display.js';

const dropArea = document.getElementById('drop-area');
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const dominantColorsDiv = document.getElementById('dominant-colors');
const baseColorsDiv = document.getElementById('base-colors');
const paletteDiv = document.getElementById('palette');
const thumbnail = document.getElementById('thumbnail');

dropArea.addEventListener('paste', (event) => {
  handlePasteEvent(event, ctx, thumbnail, canvas, dominantColorsDiv, baseColorsDiv, paletteDiv);
});

event_handlers.js

# @title Handlers
%%writefile event_handlers.js
import { getColors, extractDominantColorsKMeans, determineBaseColors, generateFinalPalette } from './color_utils.js';
import { displayColors, displayBaseColors, displayPalette } from './display.js';
import { rgbToHex } from './color_convert.js';

export function handlePasteEvent(event, ctx, thumbnail, canvas, dominantColorsDiv, baseColorsDiv, paletteDiv) {
  const items = (event.clipboardData || event.originalEvent.clipboardData).items;
  for (let i = 0; i < items.length; i++) {
    if (items[i].type.indexOf('image') !== -1) {
      const file = items[i].getAsFile();
      const reader = new FileReader();
      reader.onload = (e) => {
        const img = new Image();
        img.onload = () => {
          thumbnail.src = e.target.result;
          canvas.width = img.width;
          canvas.height = img.height;
          ctx.drawImage(img, 0, 0);
          const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
          const colors = getColors(imageData);
          const dominantColors = extractDominantColorsKMeans(colors, 8);
          const baseColors = determineBaseColors(dominantColors, 4);
          const palette = generateFinalPalette(baseColors, 8);
          displayColors(dominantColors, dominantColorsDiv);
          displayBaseColors(baseColors, baseColorsDiv);
          displayPalette(palette, paletteDiv);
        };
        img.src = e.target.result;
      };
      reader.readAsDataURL(file);
    }
  }
}


export function addRightClickHandler(element, r, g, b) {
  element.addEventListener('contextmenu', (event) => {
    event.preventDefault();
    const hexColor = rgbToHex(r, g, b);
    console.log(`Hex Color: ${hexColor}`);
  });
}

color_distance.js

# @title Distance
%%writefile color_distance.js

function labDistance(lab1, lab2) {
  const [l1, a1, b1] = lab1;
  const [l2, a2, b2] = lab2;
  return Math.sqrt(Math.pow(l1 - l2, 2) + Math.pow(a1 - a2, 2) + Math.pow(b1 - b2, 2));
}

function hsvDistance(hsv1, hsv2) {
  const [h1, s1, v1] = hsv1;
  const [h2, s2, v2] = hsv2;
  const dh = Math.min(Math.abs(h1 - h2), 1 - Math.abs(h1 - h2));
  const ds = Math.abs(s1 - s2);
  const dv = Math.abs(v1 - v2);
  return Math.sqrt(dh * dh + ds * ds + dv * dv);
}

function ycbcrDistance(ycbcr1, ycbcr2) {
  const [y1, cb1, cr1] = ycbcr1;
  const [y2, cb2, cr2] = ycbcr2;
  return Math.sqrt(Math.pow(y1 - y2, 2) + Math.pow(cb1 - cb2, 2) + Math.pow(cr1 - cr2, 2));
}

function rgbDistance(rgb1, rgb2) {
  const [r1, g1, b1] = rgb1;
  const [r2, g2, b2] = rgb2;
  return Math.sqrt(Math.pow(r1 - r2, 2) + Math.pow(g1 - g2, 2) + Math.pow(b1 - b2, 2));
}

color_convert.js

# @title Convert
%%writefile color_convert.js

export function rgbToHsv(r, g, b) {
  r /= 255, g /= 255, b /= 255;
  const max = Math.max(r, g, b), min = Math.min(r, g, b);
  let h, s, v = max;

  const d = max - min;
  s = max === 0 ? 0 : d / max;

  if (max === min) {
    h = 0;
  } else {
    switch (max) {
      case r: h = (g - b) / d + (g < b ? 6 : 0); break;
      case g: h = (b - r) / d + 2; break;
      case b: h = (r - g) / d + 4; break;
    }
    h /= 6;
  }

  return [h, s, v];
}

export function hsvToRgb(h, s, v) {
  let r, g, b;
  let i = Math.floor(h * 6);
  let f = h * 6 - i;
  let p = v * (1 - s);
  let q = v * (1 - f * s);
  let t = v * (1 - (1 - f) * s);

  switch (i % 6) {
    case 0: r = v, g = t, b = p; break;
    case 1: r = q, g = v, b = p; break;
    case 2: r = p, g = v, b = t; break;
    case 3: r = p, g = q, b = v; break;
    case 4: r = t, g = p, b = v; break;
    case 5: r = v, g = p, b = q; break;
  }

  return `rgb(${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round(b * 255)})`;
}

export function rgbToHsvString(rgb) {
  const [r, g, b] = rgb.match(/\d+/g).map(Number);
  return rgbToHsv(r, g, b);
}

export function rgbToLab(rgb) {
  const [r, g, b] = rgb.map(v => v / 255);
  let x = r > 0.04045 ? Math.pow((r + 0.055) / 1.055, 2.4) : r / 12.92;
  let y = g > 0.04045 ? Math.pow((g + 0.055) / 1.055, 2.4) : g / 12.92;
  let z = b > 0.04045 ? Math.pow((b + 0.055) / 1.055, 2.4) : b / 12.92;

  x *= 100;
  y *= 100;
  z *= 100;

  x = x * 0.4124 + y * 0.3576 + z * 0.1805;
  y = x * 0.2126 + y * 0.7152 + z * 0.0722;
  z = x * 0.0193 + y * 0.1192 + z * 0.9505;

  x /= 95.047;
  y /= 100;
  z /= 108.883;

  x = x > 0.008856 ? Math.pow(x, 1/3) : (7.787 * x) + (16 / 116);
  y = y > 0.008856 ? Math.pow(y, 1/3) : (7.787 * y) + (16 / 116);
  z = z > 0.008856 ? Math.pow(z, 1/3) : (7.787 * z) + (16 / 116);

  const l = (116 * y) - 16;
  const a = 500 * (x - y);
  const b_lab = 200 * (y - z);

  return [l, a, b_lab];
}

export function labToRgb(l, a, b) {
  let y = (l + 16) / 116;
  let x = a / 500 + y;
  let z = y - b / 200;

  x = 95.047 * ((x ** 3 > 0.008856) ? x ** 3 : (x - 16 / 116) / 7.787);
  y = 100 * ((y ** 3 > 0.008856) ? y ** 3 : (y - 16 / 116) / 7.787);
  z = 108.883 * ((z ** 3 > 0.008856) ? z ** 3 : (z - 16 / 116) / 7.787);

  x = x / 100;
  y = y / 100;
  z = z / 100;

  let r = x * 3.2406 + y * -1.5372 + z * -0.4986;
  let g = x * -0.9689 + y * 1.8758 + z * 0.0415;
  let b_rgb = x * 0.0557 + y * -0.2040 + z * 1.0570;

  r = r > 0.0031308 ? 1.055 * (r ** (1 / 2.4)) - 0.055 : r * 12.92;
  g = g > 0.0031308 ? 1.055 * (g ** (1 / 2.4)) - 0.055 : g * 12.92;
  b_rgb = b_rgb > 0.0031308 ? 1.055 * (b_rgb ** (1 / 2.4)) - 0.055 : b_rgb * 12.92;

  return [Math.max(0, Math.min(255, Math.round(r * 255))),
          Math.max(0, Math.min(255, Math.round(g * 255))),
          Math.max(0, Math.min(255, Math.round(b_rgb * 255)))];
}

export function rgbToYCbCr(r, g, b) {
  const y = 0.299 * r + 0.587 * g + 0.114 * b;
  const cb = 128 - 0.168736 * r - 0.331264 * g + 0.5 * b;
  const cr = 128 + 0.5 * r - 0.418688 * g - 0.081312 * b;
  return [y, cb, cr];
}

export function ycbcrToRgb(y, cb, cr) {
  const r = y + 1.402 * (cr - 128);
  const g = y - 0.344136 * (cb - 128) - 0.714136 * (cr - 128);
  const b = y + 1.772 * (cb - 128);
  return [Math.max(0, Math.min(255, Math.round(r))),
          Math.max(0, Math.min(255, Math.round(g))),
          Math.max(0, Math.min(255, Math.round(b)))];
}

export function rgbToHex(r, g, b) {
  return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase();
}

export function hexToRgb(hex) {
  let bigint = parseInt(hex.slice(1), 16);
  let r = (bigint >> 16) & 255;
  let g = (bigint >> 8) & 255;
  let b = bigint & 255;
  return [r, g, b];
}

color_utils.js

# @title Utils
%%writefile color_utils.js

import { kMeans } from './kmeans.js';
import { rgbToHsv, hsvToRgb, rgbToHsvString, rgbToLab, labToRgb, rgbToYCbCr, ycbcrToRgb, rgbToHex, hexToRgb } from './color_convert.js';

export function getColors(imageData) {
  const data = imageData.data;
  const colors = [];
  for (let i = 0; i < data.length; i += 4) {
    const r = data[i];
    const g = data[i + 1];
    const b = data[i + 2];
    colors.push([r, g, b]);
  }
  return colors;
}

export function extractDominantColorsKMeans(colors, numColors) {
  const result = kMeans(colors, numColors);
  const dominantColors = result.map(cluster => {
    const [r, g, b] = cluster.centroid;
    const hsv = rgbToHsv(r, g, b);
    return { r, g, b, hsv };
  });
  return dominantColors;
}

export function determineBaseColors(dominantColors, numBaseColors) {
  const dominantHues = dominantColors.map(color => color.hsv);
  return createBalancedPalette(dominantHues, Math.min(dominantHues.length, numBaseColors));
}

function createBalancedPalette(dominantHues, baseSize) {
  const hues = [];
  const step = 1 / baseSize;

  for (let i = 0; i < baseSize; i++) {
    const currentHue = dominantHues[i % dominantHues.length];
    let hue = currentHue[0] + i * step;  // 色相を等間隔に増加
    let saturation = currentHue[1];
    let value = currentHue[2];
    hues.push([hue, saturation, value]);
  }
  hues.sort((a, b) => a[0] - b[0]);  // 色相 (Hue) に基づいてソート
  return hues;
}

export function generateFinalPalette(baseColors, paletteSize) {
  const finalPalette = [];
  const totalColors = baseColors.length;

  baseColors.forEach(hsv => {
    finalPalette.push(hsvToRgb(hsv[0], hsv[1], hsv[2]));
  });

  for (let i = 0; i < totalColors; i++) {
    const currentColor = baseColors[i];
    const nextColor = baseColors[(i + 1) % totalColors];

    const steps = Math.floor((paletteSize - totalColors) / totalColors);

    for (let j = 1; j <= steps; j++) {
      const ratio = j / (steps + 1);
      const intermediateHue = (currentColor[0] * (1 - ratio) + nextColor[0] * ratio) % 1;
      const intermediateSaturation = currentColor[1] * (1 - ratio) + nextColor[1] * ratio;
      const intermediateValue = currentColor[2] * (1 - ratio) + nextColor[2] * ratio;
      finalPalette.push(hsvToRgb(intermediateHue, intermediateSaturation, intermediateValue));
    }
  }

  while (finalPalette.length < paletteSize) {
    finalPalette.push(finalPalette[finalPalette.length % totalColors]);
  }

  finalPalette.sort((a, b) => {
    const hueA = rgbToHsvString(a)[0];
    const hueB = rgbToHsvString(b)[0];
    return hueA - hueB;
  });

  return finalPalette;
}

kmeans.js

# @title kmeans
%%writefile kmeans.js


export function kMeans(colors, k, maxIterations = 100) {
  let centroids = getRandomCentroids(colors, k);
  let prevCentroids = JSON.parse(JSON.stringify(centroids));
  let clusters = Array(k).fill().map(() => []);
  let iterations = 0;

  while (!areCentroidsEqual(centroids, prevCentroids) && iterations < maxIterations) {
    clusters = Array(k).fill().map(() => []);
    prevCentroids = JSON.parse(JSON.stringify(centroids));

    colors.forEach(color => {
      let minDistance = Infinity;
      let clusterIndex = 0;

      centroids.forEach((centroid, index) => {
        const distance = labDistance(rgbToLab(color), rgbToLab(centroid));
        if (distance < minDistance) {
          minDistance = distance;
          clusterIndex = index;
        }
      });

      clusters[clusterIndex].push(color);
    });

    centroids = clusters.map(cluster => {
      const length = cluster.length;
      const mean = cluster.reduce((acc, val) => acc.map((sum, i) => sum + val[i]), [0, 0, 0])
                          .map(sum => sum / length);
      return mean;
    });

    iterations++;
  }

  return centroids.map((centroid, index) => ({
    centroid,
    points: clusters[index]
  }));
}

function areCentroidsEqual(centroids1, centroids2, threshold = 1e-5) {
  if (centroids1.length !== centroids2.length) return false;
  return centroids1.every((centroid, i) => {
    return centroid.every((value, j) => Math.abs(value - centroids2[i][j]) < threshold);
  });
}

function getRandomCentroids(colors, k) {
  const centroids = [];
  const usedIndexes = new Set();

  while (centroids.length < k) {
    const randomIndex = Math.floor(Math.random() * colors.length);
    if (!usedIndexes.has(randomIndex)) {
      centroids.push([...colors[randomIndex]]);
      usedIndexes.add(randomIndex);
    }
  }
  return centroids;
}

display.js

# @title Display
%%writefile display.js
import { hsvToRgb } from './color_convert.js';
export function displayColors(colors, container) {
  container.innerHTML = '';
  colors.sort((a, b) => a.hsv[0] - b.hsv[0]);
  colors.forEach(color => {
    const div = document.createElement('div');
    div.className = 'color-block';
    div.style.backgroundColor = `rgb(${color.r}, ${color.g}, ${color.b})`;
    container.appendChild(div);
  });
}

export function displayBaseColors(baseColors, container) {
  container.innerHTML = '';
  baseColors.sort((a, b) => a[0] - b[0]);
  baseColors.forEach(hsv => {
    const div = document.createElement('div');
    div.className = 'color-block';
    div.style.backgroundColor = hsvToRgb(hsv[0], hsv[1], hsv[2]);
    container.appendChild(div);
  });
}

export function displayPalette(palette, container) {
  container.innerHTML = '';
  palette.forEach(color => {
    const div = document.createElement('div');
    div.className = 'color-block';
    div.style.backgroundColor = color;
    container.appendChild(div);
  });
}


html


こいつを実行するとhtmlがレンダリングされる。
このhtmlがjsファイルのロードを試み、そのために仮想サーバとキャッチボールする。

# @title html
%%html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Color Palette Generator</title>
<style>
  body {
    font-family: Arial, sans-serif;
    text-align: center;
  }
  #palette, #dominant-colors, #base-colors {
    display: flex;
    justify-content: center;
    margin-top: 20px;
  }
  .color-block {
    width: 50px;
    height: 50px;
    margin: 5px;
  }
  #drop-area {
    border: 2px dashed #ccc;
    border-radius: 20px;
    padding: 20px;
    width: 80%;
    margin: 20px auto;
  }
  #thumbnail {
    max-width: 300px;
    max-height: 300px;
    margin: 20px auto;
  }
</style>
</head>
<body>
<h1>Color Palette Generator</h1>
<div id="drop-area">ここに画像をコピペしてください</div>
<canvas id="canvas" style="display: none;"></canvas>
<img id="thumbnail" alt="Image thumbnail">
<h2>支配的な色</h2>
<div id="dominant-colors"></div>
<h2>基準に用いた色</h2>
<div id="base-colors"></div>
<h2>パレット</h2>
<div id="palette"></div>
<script type="module" src="http://localhost:8000/color_palette.js"></script>
</body>
</html>


この記事が気に入ったらサポートをしてみませんか?