All files / Rindu/utils getMainColorFromImage.ts

98.36% Statements 60/61
93.33% Branches 14/15
100% Functions 2/2
100% Lines 57/57

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101    45x   45x   10x           6x 6x 1x     5x 1x 1x   4x 4x 4x   4x 4x 4x 4x 4x 4x   4x 4x       4x 4x   4x   4x   4x 4x   4x 4x   4x 400x   400x   300x 300x 300x   300x 300x   100x 100x 100x   100x   100x 1x 1x     100x     4x 4x   4x 1x 1x 1x       4x   4x 1x     4x 4x   4x   4x      
import { Dispatch, SetStateAction } from "react";
 
import { snapToAnchorColor } from "utils";
 
const cache: Record<string, string> = {};
 
export function getMainColorFromImage(
  imageId: string,
  callback: Dispatch<SetStateAction<string>> | ((color: string) => void),
  config?: { sampleSize?: number; qualityReduction?: number },
  document: Document = window.document
): void {
  const img = document.querySelector(`#${imageId}`) as HTMLImageElement;
  if (!img) {
    return;
  }
 
  if (cache[img.src]) {
    callback(cache[img.src]);
    return;
  }
  const canvas = document.createElement("canvas");
  const ctx = canvas.getContext("2d");
  Iif (!ctx) return;
 
  const image = new Image();
  image.crossOrigin = "anonymous";
  image.src = img.src;
  image.onload = () => {
    const sampleSize = config?.sampleSize || 100;
    const qualityReduction = config?.qualityReduction || 10;
 
    const scaledWidth = Math.max(1, Math.floor(image.width / qualityReduction));
    const scaledHeight = Math.max(
      1,
      Math.floor(image.height / qualityReduction)
    );
    canvas.width = scaledWidth;
    canvas.height = scaledHeight;
 
    ctx.drawImage(image, 0, 0, scaledWidth, scaledHeight);
 
    const imageData = ctx.getImageData(0, 0, scaledWidth, scaledHeight).data;
 
    const colorCounts: Record<string, number> = {};
    const colorValues: Record<string, { r: number; g: number; b: number }> = {};
 
    const totalPixels = scaledWidth * scaledHeight;
    const step = Math.max(1, Math.floor(totalPixels / sampleSize));
 
    for (let i = 0; i < totalPixels; i += step) {
      const idx = i * 4;
 
      if (imageData[idx + 3] === 0) continue;
 
      const r = imageData[idx];
      const g = imageData[idx + 1];
      const b = imageData[idx + 2];
 
      const luma = 0.2126 * r + 0.7152 * g + 0.0722 * b;
      if (luma > 240 || luma < 15) continue;
 
      const simplifiedR = Math.floor(r / 8) * 8;
      const simplifiedG = Math.floor(g / 8) * 8;
      const simplifiedB = Math.floor(b / 8) * 8;
 
      const colorKey = `${simplifiedR},${simplifiedG},${simplifiedB}`;
 
      if (!colorCounts[colorKey]) {
        colorCounts[colorKey] = 0;
        colorValues[colorKey] = { r, g, b };
      }
 
      colorCounts[colorKey]++;
    }
 
    let dominantColorKey = Object.keys(colorCounts)[0];
    let maxCount = 0;
 
    for (const colorKey in colorCounts) {
      if (colorCounts[colorKey] > maxCount) {
        maxCount = colorCounts[colorKey];
        dominantColorKey = colorKey;
      }
    }
 
    let dominantColor = { r: 122, g: 122, b: 122 };
 
    if (dominantColorKey) {
      dominantColor = colorValues[dominantColorKey];
    }
 
    const { r, g, b } = dominantColor;
    const snappedHex = snapToAnchorColor(r, g, b);
 
    cache[img.src] = snappedHex;
 
    callback(snappedHex);
  };
}