Search code examples
javascriptarrayscolors

javascript strange results getting nearest colours from array


I am using this script to find nearest color from a list/array of colors. nearest.js

const colors = ["#FFFFFF", "#520975", "#5D0C8B", "#777779"];
const hexToRgb = hex => hex.slice(1).replace(/^(.)(.)(.)$/gi, "$1$1$2$2$3$3").match(/.{2}/g).map(c => parseInt(c, 16));
const distance = (a, b) => Math.sqrt(Math.pow(a[0] - b[0], 2) + Math.pow(a[1] - b[1], 2) + Math.pow(a[2] - b[2], 2));
const nearestColor = colorHex => colors.reduce((a,v,i,arr) => a = distance(hexToRgb(colorHex), hexToRgb(v)) < a[0] ? [distance(hexToRgb(colorHex), hexToRgb(v)), v] : a, [Number.POSITIVE_INFINITY, colors[0]])[1];

Something is wrong here..

nearestColor("#854B97"); this should return one of this two #520975, #5D0C8B instead it returns a grey color #777779

here is a demo pen https://codepen.io/templatetuners/pen/RwOeLxz


Solution

  • Here is a CIELAB version that seems to work better

    /**
     * Converts a hexadecimal color value to an RGB array.
     * @param {string} hex - The hexadecimal color string (e.g., "#FFFFFF").
     * @returns {number[]} An array containing the RGB values [red, green, blue].
     */
    const hexToRgb = hex => {
      const r = parseInt(hex.slice(1, 3), 16);
      const g = parseInt(hex.slice(3, 5), 16);
      const b = parseInt(hex.slice(5, 7), 16);
      return [r, g, b];
    };
    
    /**
     * Converts an RGB color array into an XYZ color array using the sRGB color space.
     * @param {number[]} rgb - An array of RGB values [red, green, blue].
     * @returns {number[]} An array containing the XYZ values.
     */
    const rgbToXyz = rgb => {
      let [r, g, b] = rgb.map(v => {
        v /= 255;
        return v > 0.04045 ? Math.pow((v + 0.055) / 1.055, 2.4) : v / 12.92;
      });
    
      [r, g, b] = [r * 100, g * 100, b * 100];
      const x = r * 0.4124 + g * 0.3576 + b * 0.1805;
      const y = r * 0.2126 + g * 0.7152 + b * 0.0722;
      const z = r * 0.0193 + g * 0.1192 + b * 0.9505;
      return [x, y, z];
    };
    
    /**
     * Converts an XYZ color array to a CIELAB color array which more closely aligns with human color perception.
     * @param {number[]} xyz - An array of XYZ values.
     * @returns {number[]} An array containing the CIELAB values [L*, a*, b*].
     */
    const xyzToLab = xyz => {
      let [x, y, z] = xyz;
      [x, y, z] = [x / 95.047, y / 100.000, z / 108.883];
      [x, y, z] = [x, y, z].map(v => v > 0.008856 ? Math.pow(v, 1 / 3) : (7.787 * v) + (16 / 116));
    
      const l = (116 * y) - 16;
      const a = 500 * (x - y);
      const b = 200 * (y - z);
      return [l, a, b];
    };
    
    /**
     * Calculates the Delta E (Euclidean distance in the LAB color space) color difference between two CIELAB colors.
     * @param {number[]} labA - The first CIELAB color array.
     * @param {number[]} labB - The second CIELAB color array.
     * @returns {number} The Delta E color difference.
     */
    const deltaE = (labA, labB) => {
      return Math.sqrt(
        Math.pow(labA[0] - labB[0], 2) +
        Math.pow(labA[1] - labB[1], 2) +
        Math.pow(labA[2] - labB[2], 2)
      );
    };
    
    /**
     * Finds the nearest color from a predefined set to the given hexadecimal color.
     * @param {string} colorHex - The hexadecimal color code.
     * @returns {string} The hexadecimal code of the nearest color from the set.
     */
    const nearestColor = colorHex => {
      const targetLab = xyzToLab(rgbToXyz(hexToRgb(colorHex)));
      return colors.reduce((acc, curr) => {
        const currLab = xyzToLab(rgbToXyz(hexToRgb(curr)));
        const currDeltaE = deltaE(targetLab, currLab);
        return currDeltaE < acc.deltaE ? { color: curr, deltaE: currDeltaE } : acc;
      }, { color: null, deltaE: Infinity }).color;
    };
    
    /** Test the code **/
    var nearest = nearestColor('#854B97');
    document.getElementById("near").innerHTML = "result<br>" + nearest;
    document.getElementById("near").style.backgroundColor = nearest;
    <title>Color Distance in CIELAB</title>
    <div style="background:#520975; width:100px; height:100px; display:block; float:left; margin:0;">#520975<br>array color</div>
    <div style="background:#5D0C8B; width:100px; height:100px; display:block; float:left; margin:0;">#5D0C8B<br>array color</div>
    <div style="clear:both;background:#854B97; width:100px; height:100px; display:block; float:left; margin:0;">#854B97<br>find near for this color</div>
    <div id="near" style="width:100px; height:100px; display:block; float:left; margin:0;"></div>
    
    <script>
      const colors = [
        "#B2CFAD", "#F4C6CE", "#7CADD3", "#A4C7E2", "#1BCFC9", "#F7EA5F", "#ffffff", "#520975", "#004987", "#F5E500",
        "#FF8300", "#D8262E", "#999899", "#016836", "#005CB8", "#008995", "#D08900", "#EF8F7A", "#F0B2C9", "#EA534D",
        "#823550", "#98C11E", "#009ADD", "#FF8188", "#AA7BC9", "#E7E6E5", "#4D9C2D", "#C5093B", "#D7006D", "#970047",
        "#10576C", "#00CAC3", "#F1F0E2", "#8FC6E8", "#004876", "#FF637D", "#2E5BBE", "#B0A198", "#FFBE9E", "#7B2F3E",
        "#EB6BAF", "#F2F0A1", "#B9AAD4", "#86ad3f", "#AFC9B8", "#D1CCBD", "#D25D12", "#ECCCCE", "#F3CD00", "#006547",
        "#789D90", "#80A7BC", "#B8C7D3", "#AF90A7", "#777779", "#5D0C8B", "#01426A", "#F6CE3C", "#00AFA9", "#E11282",
        "#F3D09E", "#00A6CE", "#F8B5CC", "#32393C", "#FFDB00", "#EB0028", "#F0B2C9", "#FF6B0B", "#C4D5EC"
      ];
    </script>