JS function to calculate complementary colour?
Asked Answered
F

8

71

Does anybody know, off the top of your heads, a Javascript solution for calculating the complementary colour of a hex value?

There are a number of colour picking suites and palette generators on the web but I haven't seen any that actually calculate the colour dynamically using JS.

A detailed hint or a snippet would be very much appreciated.

Frontpage answered 2/11, 2009 at 22:26 Comment(0)
S
68

Parsed through http://design.geckotribe.com/colorwheel/

    // Complement
    temprgb={ r: 0, g: 0xff, b: 0xff }; // Cyan
    temphsv=RGB2HSV(temprgb);
    temphsv.hue=HueShift(temphsv.hue,180.0);
    temprgb=HSV2RGB(temphsv);
    console.log(temprgb); // Complement is red (0xff, 0, 0)
    
    function RGB2HSV(rgb) {
    	hsv = new Object();
    	max=max3(rgb.r,rgb.g,rgb.b);
    	dif=max-min3(rgb.r,rgb.g,rgb.b);
    	hsv.saturation=(max==0.0)?0:(100*dif/max);
    	if (hsv.saturation==0) hsv.hue=0;
     	else if (rgb.r==max) hsv.hue=60.0*(rgb.g-rgb.b)/dif;
    	else if (rgb.g==max) hsv.hue=120.0+60.0*(rgb.b-rgb.r)/dif;
    	else if (rgb.b==max) hsv.hue=240.0+60.0*(rgb.r-rgb.g)/dif;
    	if (hsv.hue<0.0) hsv.hue+=360.0;
    	hsv.value=Math.round(max*100/255);
    	hsv.hue=Math.round(hsv.hue);
    	hsv.saturation=Math.round(hsv.saturation);
    	return hsv;
    }
    
    // RGB2HSV and HSV2RGB are based on Color Match Remix [http://color.twysted.net/]
    // which is based on or copied from ColorMatch 5K [http://colormatch.dk/]
    function HSV2RGB(hsv) {
    	var rgb=new Object();
    	if (hsv.saturation==0) {
    		rgb.r=rgb.g=rgb.b=Math.round(hsv.value*2.55);
    	} else {
    		hsv.hue/=60;
    		hsv.saturation/=100;
    		hsv.value/=100;
    		i=Math.floor(hsv.hue);
    		f=hsv.hue-i;
    		p=hsv.value*(1-hsv.saturation);
    		q=hsv.value*(1-hsv.saturation*f);
    		t=hsv.value*(1-hsv.saturation*(1-f));
    		switch(i) {
    		case 0: rgb.r=hsv.value; rgb.g=t; rgb.b=p; break;
    		case 1: rgb.r=q; rgb.g=hsv.value; rgb.b=p; break;
    		case 2: rgb.r=p; rgb.g=hsv.value; rgb.b=t; break;
    		case 3: rgb.r=p; rgb.g=q; rgb.b=hsv.value; break;
    		case 4: rgb.r=t; rgb.g=p; rgb.b=hsv.value; break;
    		default: rgb.r=hsv.value; rgb.g=p; rgb.b=q;
    		}
    		rgb.r=Math.round(rgb.r*255);
    		rgb.g=Math.round(rgb.g*255);
    		rgb.b=Math.round(rgb.b*255);
    	}
    	return rgb;
    }

    //Adding HueShift via Jacob (see comments)
    function HueShift(h,s) { 
        h+=s; while (h>=360.0) h-=360.0; while (h<0.0) h+=360.0; return h; 
    }
    
    //min max via Hairgami_Master (see comments)
    function min3(a,b,c) { 
        return (a<b)?((a<c)?a:c):((b<c)?b:c); 
    } 
    function max3(a,b,c) { 
        return (a>b)?((a>c)?a:c):((b>c)?b:c); 
    }
Showery answered 2/11, 2009 at 22:39 Comment(10)
The magical HueShift(...) function is this: function HueShift(h,s) { h+=s; while (h>=360.0) h-=360.0; while (h<0.0) h+=360.0; return h; }Tarantella
You'll also need the max3 and min3 functions:Lawlor
function min3(a,b,c) { return (a<b)?((a<c)?a:c):((b<c)?b:c); } function max3(a,b,c) { return (a>b)?((a>c)?a:c):((b>c)?b:c); }Lawlor
What should be the thisrgb value ?Nonentity
Although this does do some awesome stuff, there is a massive amount of code missing. "HueShift"?. "See comments" you say? What comments? The lack of entirety has made this answer useless.Frostwork
h+=s;h=(((h % 360.0) + 360.0) % 360.0);return h;Vanhorn
function min3(a,b,c) { return Math.min(Math.min(a,b),c);}Vanhorn
function max3(a,b,c) { return Math.max(Math.max(a,b),c);}Vanhorn
conditional branches are more expensive than modulus or Math.min/max.Vanhorn
Hi, I tried your algorithm, with the following color #00BCD4 which should look something like {r: 0, g: 0xbc, b: 0xd4} it should give me as complementary color this # d41900 instead it gives me this { b: 0, g: 25, r: 212 } this #0019d4. Could you give me a hand?Pelorus
P
39

I find that taking the bit-wise complement works well, and quickly.

var color = 0x320ae3;
var complement = 0xffffff ^ color;

I'm not sure if it's a perfect complement in the sense of "mixes together to form a 70% grey", however a 70% grey is "pure white" in terms of color timing in film. It occurred to me that XORing the RGB hex out of pure white might be a good first approximation. You could also try a darker grey to see how that works for you.

Again, this is a fast approximation and I make no guarantees that it'll be perfectly accurate.

See https://github.com/alfl/textful/blob/master/app.js#L38 for my implementation.

Pectinate answered 21/2, 2014 at 2:34 Comment(4)
This works nicely, except if you want the result to always be 6 characters long. I suggest ('000000' + (('0xffffff' ^ '0x320ae3').toString(16))).slice(-6);Condole
Buzzzz: (wrong) What would the opposite color of 0x7F7F7F be? 0x808080! That's not very "opposite". XOR with 0x808080 would be better (color distance then is always half) but still not "best". The HSL methods give best results.Vanhorn
Fair, if you xor grey out of grey you get a different grey. Call it a limit of the approach :DPectinate
The solution is flipping only the most significant bit of each component, not all: color ^ 0x808080. Grey becomes either almost white or almost black, sufficiently contrasting with grey.Pyroconductivity
F
36

None of the other functions here worked out the box, so I made this one.

It takes a hex value, converts it to HSL, shifts the hue 180 degrees and converts back to Hex

/* hexToComplimentary : Converts hex value to HSL, shifts
 * hue by 180 degrees and then converts hex, giving complimentary color
 * as a hex value
 * @param  [String] hex : hex value  
 * @return [String] : complimentary color as hex value
 */
function hexToComplimentary(hex){

    // Convert hex to rgb
    // Credit to Denis https://mcmap.net/q/9259/-rgb-to-hex-and-hex-to-rgb
    var rgb = 'rgb(' + (hex = hex.replace('#', '')).match(new RegExp('(.{' + hex.length/3 + '})', 'g')).map(function(l) { return parseInt(hex.length%2 ? l+l : l, 16); }).join(',') + ')';

    // Get array of RGB values
    rgb = rgb.replace(/[^\d,]/g, '').split(',');

    var r = rgb[0], g = rgb[1], b = rgb[2];

    // Convert RGB to HSL
    // Adapted from answer by 0x000f https://mcmap.net/q/21312/-hsl-to-rgb-color-conversion
    r /= 255.0;
    g /= 255.0;
    b /= 255.0;
    var max = Math.max(r, g, b);
    var min = Math.min(r, g, b);
    var h, s, l = (max + min) / 2.0;

    if(max == min) {
        h = s = 0;  //achromatic
    } else {
        var d = max - min;
        s = (l > 0.5 ? d / (2.0 - max - min) : d / (max + min));

        if(max == r && g >= b) {
            h = 1.0472 * (g - b) / d ;
        } else if(max == r && g < b) {
            h = 1.0472 * (g - b) / d + 6.2832;
        } else if(max == g) {
            h = 1.0472 * (b - r) / d + 2.0944;
        } else if(max == b) {
            h = 1.0472 * (r - g) / d + 4.1888;
        }
    }

    h = h / 6.2832 * 360.0 + 0;

    // Shift hue to opposite side of wheel and convert to [0-1] value
    h+= 180;
    if (h > 360) { h -= 360; }
    h /= 360;

    // Convert h s and l values into r g and b values
    // Adapted from answer by Mohsen https://mcmap.net/q/21312/-hsl-to-rgb-color-conversion
    if(s === 0){
        r = g = b = l; // achromatic
    } else {
        var hue2rgb = function hue2rgb(p, q, t){
            if(t < 0) t += 1;
            if(t > 1) t -= 1;
            if(t < 1/6) return p + (q - p) * 6 * t;
            if(t < 1/2) return q;
            if(t < 2/3) return p + (q - p) * (2/3 - t) * 6;
            return p;
        };

        var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
        var p = 2 * l - q;

        r = hue2rgb(p, q, h + 1/3);
        g = hue2rgb(p, q, h);
        b = hue2rgb(p, q, h - 1/3);
    }

    r = Math.round(r * 255);
    g = Math.round(g * 255); 
    b = Math.round(b * 255);

    // Convert r b and g values to hex
    rgb = b | (g << 8) | (r << 16); 
    return "#" + (0x1000000 | rgb).toString(16).substring(1);
}  
Fertile answered 6/6, 2016 at 12:49 Comment(5)
This works great. How long did it take you to write this? I've been searching for something that does this for awhile and also encountered several 'out of the box' solutions that did not work.Deed
It took an hour or so - I just patched together some stackoverflow answers (credited in the code).Fertile
@Edd: Hi, I tried your algorithm, with the following color #00BCD4 it should give me as complementary color this #d41900 instead it gives me this #d41800. Could you give me a hand?Pelorus
Works great! For anyone wanting a smaller difference, change h+= 180; to h+= 40;Shall
Produces values that align with this web tool. canva.com/colors/color-wheelManny
S
9

Rather than reinventing the wheel, I found a library to work with colors.

Tiny Color

This is how you would implement some of the other answers using it.

color1 = tinycolor2('#f00').spin(180).toHexString(); // Hue Shift
color2 = tinycolor2("#f00").complement().toHexString(); // bitwise
Sorayasorb answered 21/6, 2018 at 9:51 Comment(0)
W
4

Hex and RGB complementry This is most correct and efficient way to get complementary hex color value

function complementryHexColor(hex){
    let r = hex.length == 4 ? parseInt(hex[1] + hex[1], 16) : parseInt(hex.slice(1, 3), 16);
    let g = hex.length == 4 ? parseInt(hex[2] + hex[2], 16) : parseInt(hex.slice(3, 5), 16);
    let b = hex.length == 4 ? parseInt(hex[3] + hex[3], 16) : parseInt(hex.slice(5), 16);
  
    [r, g, b] = complementryRGBColor(r, g, b);
    return '#' + (r < 16 ? '0' + r.toString(16) : r.toString(16)) + (g < 16 ? '0' + g.toString(16) : g.toString(16)) + (b < 16 ? '0' + b.toString(16) : b.toString(16));
}

function complementryRGBColor(r, g, b) {
    if (Math.max(r, g, b) == Math.min(r, g, b)) {
        return [255 - r, 255 - g, 255 - b];

    } else {
        r /= 255, g /= 255, b /= 255;
        var max = Math.max(r, g, b), min = Math.min(r, g, b);
        var h, s, l = (max + min) / 2;
        var d = max - min;
        s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
    
        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 = Math.round((h*60) + 180) % 360;
        h /= 360;
        
        function hue2rgb(p, q, t) {
            if (t < 0) t += 1;
            if (t > 1) t -= 1;
            if (t < 1/6) return p + (q - p) * 6 * t;
            if (t < 1/2) return q;
            if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
            return p;
        }
    
        var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
        var p = 2 * l - q;
    
        r = hue2rgb(p, q, h + 1/3);
        g = hue2rgb(p, q, h);
        b = hue2rgb(p, q, h - 1/3);

        return [Math.round(r*255), Math.round(g*255), Math.round(b*255)];
    }
}
Walczak answered 6/5, 2021 at 19:47 Comment(0)
P
3

RGB Complimentary

function componentToHex(c) {
    var hex = c.toString(16);
    return hex.length == 1 ? "0" + hex : hex;
}


function hexToRgb(hex) {
  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;
}

function rgbComplimentary(r,g,b){

    var hex = "#" + componentToHex(r) + componentToHex(g) + componentToHex(b);
    var rgb = 'rgb(' + (hex = hex.replace('#', '')).match(new RegExp('(.{' + hex.length/3 + '})', 'g')).map(function(l) { return parseInt(hex.length%2 ? l+l : l, 16); }).join(',') + ')';

    // Get array of RGB values
    rgb = rgb.replace(/[^\d,]/g, '').split(',');

    var r = rgb[0]/255.0, g = rgb[1]/255.0, b = rgb[2]/255.0;

    var max = Math.max(r, g, b);
    var min = Math.min(r, g, b);
    var h, s, l = (max + min) / 2.0;

    if(max == min) {
        h = s = 0;  //achromatic
    } else {
        var d = max - min;
        s = (l > 0.5 ? d / (2.0 - max - min) : d / (max + min));

        if(max == r && g >= b) {
            h = 1.0472 * (g - b) / d ;
        } else if(max == r && g < b) {
            h = 1.0472 * (g - b) / d + 6.2832;
        } else if(max == g) {
            h = 1.0472 * (b - r) / d + 2.0944;
        } else if(max == b) {
            h = 1.0472 * (r - g) / d + 4.1888;
        }
    }

    h = h / 6.2832 * 360.0 + 0;

    // Shift hue to opposite side of wheel and convert to [0-1] value
    h+= 180;
    if (h > 360) { h -= 360; }
    h /= 360;

    // Convert h s and l values into r g and b values
    // Adapted from answer by Mohsen https://mcmap.net/q/21312/-hsl-to-rgb-color-conversion
    if(s === 0){
        r = g = b = l; // achromatic
    } else {
        var hue2rgb = function hue2rgb(p, q, t){
            if(t < 0) t += 1;
            if(t > 1) t -= 1;
            if(t < 1/6) return p + (q - p) * 6 * t;
            if(t < 1/2) return q;
            if(t < 2/3) return p + (q - p) * (2/3 - t) * 6;
            return p;
        };

        var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
        var p = 2 * l - q;

        r = hue2rgb(p, q, h + 1/3);
        g = hue2rgb(p, q, h);
        b = hue2rgb(p, q, h - 1/3);
    }

    r = Math.round(r * 255);
    g = Math.round(g * 255); 
    b = Math.round(b * 255);

    // Convert r b and g values to hex
    rgb = b | (g << 8) | (r << 16); 
    return hexToRgb("#" + (0x1000000 | rgb).toString(16).substring(1));

}


console.log(rgbComplimentary(242, 211, 215));

HEX Complimentary

function hexComplimentary(hex){

    var rgb = 'rgb(' + (hex = hex.replace('#', '')).match(new RegExp('(.{' + hex.length/3 + '})', 'g')).map(function(l) { return parseInt(hex.length%2 ? l+l : l, 16); }).join(',') + ')';

    // Get array of RGB values
    rgb = rgb.replace(/[^\d,]/g, '').split(',');

    var r = rgb[0]/255.0, g = rgb[1]/255.0, b = rgb[2]/255.0;

    var max = Math.max(r, g, b);
    var min = Math.min(r, g, b);
    var h, s, l = (max + min) / 2.0;

    if(max == min) {
        h = s = 0;  //achromatic
    } else {
        var d = max - min;
        s = (l > 0.5 ? d / (2.0 - max - min) : d / (max + min));

        if(max == r && g >= b) {
            h = 1.0472 * (g - b) / d ;
        } else if(max == r && g < b) {
            h = 1.0472 * (g - b) / d + 6.2832;
        } else if(max == g) {
            h = 1.0472 * (b - r) / d + 2.0944;
        } else if(max == b) {
            h = 1.0472 * (r - g) / d + 4.1888;
        }
    }

    h = h / 6.2832 * 360.0 + 0;

    // Shift hue to opposite side of wheel and convert to [0-1] value
    h+= 180;
    if (h > 360) { h -= 360; }
    h /= 360;

    if(s === 0){
        r = g = b = l; // achromatic
    } else {
        var hue2rgb = function hue2rgb(p, q, t){
            if(t < 0) t += 1;
            if(t > 1) t -= 1;
            if(t < 1/6) return p + (q - p) * 6 * t;
            if(t < 1/2) return q;
            if(t < 2/3) return p + (q - p) * (2/3 - t) * 6;
            return p;
        };

        var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
        var p = 2 * l - q;

        r = hue2rgb(p, q, h + 1/3);
        g = hue2rgb(p, q, h);
        b = hue2rgb(p, q, h - 1/3);
    }

    r = Math.round(r * 255);
    g = Math.round(g * 255); 
    b = Math.round(b * 255);

    // Convert r b and g values to hex
    rgb = b | (g << 8) | (r << 16); 
    return "#" + (0x1000000 | rgb).toString(16).substring(1);
}


console.log(hexComplimentary("#ff5a5a"));

Source: Updated https://mcmap.net/q/100655/-js-function-to-calculate-complementary-colour answer

Pernod answered 13/10, 2019 at 14:58 Comment(1)
Hi, I tried your algorithm, with the following color #00BCD4 it should give me as complementary color this #d41900 instead it gives me this #d41800. Could you give me a hand?Pelorus
S
2

There's a much simpler way to get the complementary color than converting back and forth between RGB and HSV/HSL (same results though):

function complement(r, g, b) {
    const m = Math.min(r, g, b) + Math.max(r, g, b);
    return [m - r, m - g, m - b];
}

All the way from hex:

function hexComplement(rgb) {
    rgb = Number.parseInt(rgb.substring(1), 16);
    const [r, g, b] = complement(rgb >> 16, (rgb >> 8) & 0xff, rgb & 0xff);
    return '#' + ((r << 16) | (g << 8) | b).toString(16).padStart(6, '0');
}
Sundowner answered 29/9, 2023 at 8:43 Comment(0)
P
0

Usefuls simple and elegant solution, but unfortunately rgb complement does not work well for black, i.e., rgb(0,0,0). Needed this for Bootstrap cards created with JS loop for all colors in provided in "json-color-names.json", so instead of the given solution, just calculated rgb components as: 255-r, 255-g, 255-b, or simply by using template literals:

return `rgb(${255-r},${255-g},${255-b})`;

But, this one doesn't give good result for gray rgb(128,128,128). So, I'd say that satisfactory results are achievable with other more complicated solutions or libraries, of which some are mentioned above, but when seeking for short and simple solution just to achieve text color different from the background, this might be the one, with just some formula tweaking (maybe by using mod or some other function).

Prehensible answered 29/1 at 19:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.