2

I'm trying to build a hexadecimal color comparator which compares values in an array and deletes the ones that are similar keeping colors with the most pixels.

Here's an example of what the input looks like, the length of the array can change from 1 to more :

[…]
0: Array [ "ffffff", 12992 ]
1: Array [ "da542f", 3117 ] #similar
2: Array [ "da5630", 60 ]   #similar
length: 3

(index: Array [hexColor, NumberOfPixel])

The output would look like this :

[…]
0: Array [ "ffffff", 12992 ]
1: Array [ "da542f", 3117 ]
length: 2

What I come up with, since the example below is super hardcoded and produce bugs I'd like to avoid having multiple if/else inside each other, hexColorCalc() function comes from this post :

FIDDLE >

// ordered by highest pixel
colors_lst = [
  ["333333", 6421], 
  ["da542f", 3117],
  ["da5630", 60]
]

console.log(colors_lst);

function hexColorDelta(array) {
  function hexColorCalc(hex1, hex2) {
    // get red/green/blue int values of hex1
    var r1 = parseInt(hex1.substring(0, 2), 16);
    var g1 = parseInt(hex1.substring(2, 4), 16);
    var b1 = parseInt(hex1.substring(4, 6), 16);
    // get red/green/blue int values of hex2
    var r2 = parseInt(hex2.substring(0, 2), 16);
    var g2 = parseInt(hex2.substring(2, 4), 16);
    var b2 = parseInt(hex2.substring(4, 6), 16);
    // calculate differences between reds, greens and blues
    var r = 255 - Math.abs(r1 - r2);
    var g = 255 - Math.abs(g1 - g2);
    var b = 255 - Math.abs(b1 - b2);
    // limit differences between 0 and 1
    r /= 255;
    g /= 255;
    b /= 255;
    // 0 means opposit colors, 1 means same colors
    return (r + g + b) / 3;
  }

  // Do nothing since nothing to compare
  if (array.length == 1) {
    console.log('length of array : 1');
    return array
  }

  // Just compare both values and erase the one with least pixel
  if (array.length == 2) {
    console.log('length of array : 2');
    var hex1 = array[0][0];
    var hex2 = array[1][0];


    if (hexColorCalc(hex1, hex2) > 0.9) {
      colors_lst = array.pop(); // Get ride of last item in array
      return colors_lst;
    }
  }

  // Problems 
  if (array.length == 3) {
    var hex1 = array[0][0];
    var hex2 = array[1][0];
    var hex3 = array[2][0];

    // if True, other if/else below won't be working
    if (hexColorCalc(hex1, hex2) > 0.9) {
      array.splice(2, 1);
    }

    if (hexColorCalc(hex1, hex3) > 0.9) {
      array.splice(3, 1);
    }

    if (hexColorCalc(hex2, hex3) > 0.9) {
      array.splice(2, 1);
    }

    return array
  }
}

console.log(hexColorDelta(colors_lst));

I commented out how the code works, I'm having some trouble understanding the correct algorithm for this problem. How can I avoid the hardcoding and return a proper list without similarities?

Horai Nuri
  • 5,358
  • 16
  • 75
  • 127
  • seems like you need the combinations https://stackoverflow.com/questions/43241174/javascript-generating-all-combinations-of-elements-in-a-single-array-in-pairs – Slai Nov 09 '17 at 10:37
  • How exactly do you define "similarity"? `da542f` and `da5630` are pretty close to each other but not identical. Where's the threshold? – Thomas Nov 09 '17 at 11:25
  • there are still some issues as you will get different number of colors depending on the order in which the colors are compared and removed. I suggest internet search for JavaScript [color quantization](https://en.wikipedia.org/wiki/Color_quantization) and possibly use any of the available implementations https://github.com/leeoniya/RgbQuant.js – Slai Nov 09 '17 at 23:19

2 Answers2

0

To avoid excessive usage of if, especially for larger array where combination will explode, you can rely on loops. Below is the solution I proposed after doing a bit of cleanup to your code:

// splits the hex value to its components
function splitHex(hex) {
    return {
        r: parseInt(hex.substring(0, 2), 16),
        g: parseInt(hex.substring(2, 4), 16),
        b: parseInt(hex.substring(4, 6), 16)
    };
}

// return number between 0 (opposit colors) and 1 (same colors)
function hexColorCalc(hex1, hex2) {
    // get red/green/blue int values of hex1
    const hex1Split = splitHex(hex1);
    // get red/green/blue int values of hex2
    const hex2Split = splitHex(hex2);
    // calculate normalized differences between reds, greens and blues
    const r = 1 - Math.abs(hex1Split.r - hex2Split.r) / 255;
    const g = 1 - Math.abs(hex1Split.g - hex2Split.g) / 255;
    const b = 1 - Math.abs(hex1Split.b - hex2Split.b) / 255;

    return (r + g + b) / 3;
}

// clears array from similar colors with threshold
function hexColorDelta(array, threshold) {
    // loop through array twice as a table to get all combinations
    for (let i = 0; i < array.length; i++) {
        for (let j = 0; j < array.length; j++) {
            // only remove the color in the second loop if similar
            if (i !== j && hexColorCalc(array[i][0], array[j][0]) > threshold) {
                array.splice(j, 1);
                j--;
            }   
        }
    }
    return array;
}

// test
const similar_1_2 = [["da542f", 3117], ["da5630", 60], ["333333", 6421]];
const similar_1_3 = [["da542f", 3117], ["333333", 6421], ["da5630", 60]];
const similar_2_3 = [["333333", 6421], ["da542f", 3117], ["da5630", 60]];
const similar_all = [["da5630", 60], ["da542f", 3117], ["da5630", 60]];
console.log(JSON.stringify(hexColorDelta(similar_1_2, 0.9)));
console.log(JSON.stringify(hexColorDelta(similar_1_3, 0.9)));
console.log(JSON.stringify(hexColorDelta(similar_2_3, 0.9)));
console.log(JSON.stringify(hexColorDelta(similar_all, 0.9)));

I hope this helps.

M0nst3R
  • 5,186
  • 1
  • 23
  • 36
  • Awesome answer! I see that you're using `const` instead of `var` to define variables, is there any particular reason for that ? – Horai Nuri Nov 09 '17 at 12:27
  • 1
    Thanks, and yes, `const` is better than `var` especially inside functions, as not only will it declare a scope-local variable but it will also not allow you to reassign the variable, if you want to reassign it, you can use `let`. – M0nst3R Nov 09 '17 at 12:36
-1

Check it out here: https://jsfiddle.net/4ph8wozd/1/

// convert HEX code to RGB
function hexToRgb(hex) {
    var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
    hex = hex.replace(shorthandRegex, function (m, r, g, b) {
        return r + r + g + g + b + b;
    });

    var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);

    return result ? {
        r: parseInt(result[1], 16),
        g: parseInt(result[2], 16),
        b: parseInt(result[3], 16)
    } : null;
}
// compare colors
function luminanace(r, g, b) {
    var colorArray = [r, g, b];
    var colorFactor;
    var i;
    for (i = 0; i < colorArray.length; i++) {
        colorFactor = colorArray[i] / 255;
        if (colorFactor <= 0.03928) {
            colorFactor = colorFactor / 12.92;
        } else {
            colorFactor = Math.pow(((colorFactor + 0.055) / 1.055), 2.4);
        }
        colorArray[i] = colorFactor;
    }
    return (colorArray[0] * 0.2126 + colorArray[1] * 0.7152 + colorArray[2] * 0.0722) + 0.05;
}
function contrast(hex1, hex2) {
    var color1 = hexToRgb(hex1);
    var color2 = hexToRgb(hex2);
    if (color1 != null && color2 != null) {
        var result = (luminanace(color1.r, color1.g, color1.b)) / (luminanace(color2.r, color2.g, color2.b)); if (result < 1) result = 1 / result;

        // show result
        // Google says lowest:4.5 | enough:7 | good:14 | best:21
        var rgood = 7;
        var rbad = 4.5;
        contrastratio = result;
        switch (true) {
            case result >= rgood:
                contrastratiomessage = '<span class="cr-green">Text is well readable</span>';
                break;
            case result > rbad && result < rgood:
                contrastratiomessage = '<span class="cr-orange">Text is enough readable</span>';
                break;
            case result <= rbad:
                contrastratiomessage = '<span class="cr-red">Text is not readable!!!</span>';
                break;
            default:
        }
        document.getElementById('colors').innerHTML = contrastratiomessage;
    }
    console.log(result);
}
contrast('#fff000','#000fff');
<div id="colors"></div>
Radek Mezuláník
  • 351
  • 1
  • 5
  • 15