Generating unique, contrasting colors in JavaScript

While working on my DOM course for LinkedIn Learning I had to solve a problem I’ve come across several times in my career. It shows up when generating graphs, or in my case when you want to show nested rectangles with contrasting colors.

Like most problems, I started using the “smart friend” ​trick that I learned in one of the very first programming books I ever read. The idea is that as you’re coding, you’re going to come across some part of your project that is tricky, and the answer isn’t immediately obvious. So, instead of stalling there, you encapsulate that tricky bit in a function with a stub return and pretend that your “smart friend” will be back to write it later. Of course, in reality, you are the “smart friend”, but it’s a useful way to keep from getting bogged down in the details when making your first pass at a problem:

function getUniqueColor(n) {
    // !!!TBD!!! write clever algorithm here
    return '#ffffff';
}

The goal is to write a function that given sequential indices (e.g. 1, 2, 3) will return RGB colors that are very distinct from each other (e.g. #000080, #008000, #008080). There are some complex approaches to this problem that require using the HSL (or HSV) color spaces, but I had the gut feeling that there was a simpler way to tackle this without resorting to trigonometry and floating point numbers.

As a quick refresher, a CSS RGB color reference is a six-digit hex number where each color component (red, green, blue) has an intensity value from 0-255. So, for example, #ff0000 is pure red with 100% of the intensity on the red channel and 0% on the green and blue channels.

So, I want to develop a function that accepts an integer index and returns an RGB color and I want adjacent indices to return colors that are not similar in hue. This led me to think about the bits in the index itself.

If I want consecutive values to return dissimilar colors, I need to “distribute” the bits from the index across all three color channels. Since the final RGB color is 24 bits, I would only use the lower 24 bits of the index to calculate my new color. I would then allocate the bits from the index to the color channels in round-robin format, with each bit in the index ending up as one bit in the final color.

So, based on this scheme, the first color generated would be pure blue, then pure green, then cyan, then red, and so on. I knew I was on to something! But there was only one other issue. Just extracting the bits in order and constructing the RGB value would yield a lot of very dark, very similar colors at first:

So, I not only need to distribute the bits among the color channels, I need to make small incremental differences make large differences in the resulting color values. Ideally, the very first few indices would produce the largest possible differences in colors.

So the least significant bits of the index should be stored as the most significant bits of the color channels! I made a simple animation to show how the the algorithm works as a JSFiddle. Click the animate button to see how the bits are distributed:

The final version of my color generator function:

function getUniqueColor(n) {
    const rgb = [0, 0, 0];

    for (let i = 0; i < 24; i++) {
        rgb[i%3] <<= 1;
        rgb[i%3] |= n & 0x01;
        n >>= 1;
    }

    return '#' + rgb.reduce((a, c) => (c > 0x0f ? c.toString(16) : '0' + c.toString(16)) + a, '');
}

It’s not a long function, but with the bit manipulation and reduce() call it might be a little tough to understand at first. On line 2, it allocates an array that will contain the three RGB component integer values and initializes them to 0. The loop on line 4 steps through all 24 significant bits of the input index (n). Unlike many bitwise loop operations, it is important to process all 24 bits of the index due to the way the bits are distributed inside the loop.

The body of the loop shifts the current value of the current channel (line 5) left by 1 bit. This makes room for the new bit to be masked and ored with the channel value (line 6). Then the index is shifted right by 1 bit to prepare a fresh bit for the next iteration (line 7).

Finally, on line 10 the rgb array is converted to a CSS RGB hex color string using the Array.reduce() method and the toString() method with a base of 16. And although the resulting color sequence isn’t perfect, it does produce a lot of high-contrast colors with no fuss. Check out the JSFiddle to see the code in action, and grab it for the next time you need a lot of colors in a hurry!

Leave a Reply

Your email address will not be published. Required fields are marked *