156

I'm trying to evaluate the darkness of a color chosen by a color picker to see if it's "too black", and if so, set it to white. I thought I could use the first characters of the hex value to pull this off. It's working, but it's switching some legitimately "light" colors too.

I have the following code:

if (lightcolor.substring(0, 3) == "#00" || lightcolor.substring(0, 3) == "#010") {
  lightcolor = "#FFFFFF";
  color = lightcolor;
}

There must be a more efficient way with hex math to know that a color has gone beyond a certain level of darkness? Like if lightcolor + "some hex value" <= "some hex value" then set it to white.

I have tinyColor added, which might be of use for this, but I don't know for sure.

Mr. Polywhirl
  • 42,981
  • 12
  • 84
  • 132
Dshiz
  • 3,099
  • 3
  • 26
  • 53
  • 1
    Have you tried getting up a color picker and checking the values? I noticed that when R, G and B are all under ~70 it gets dark. This might not be the proper way, but it's one. – Rick Kuipers Aug 20 '12 at 18:43
  • 1
    As you're already using tinyColor, transform the color to [HSL](http://en.wikipedia.org/wiki/HSL_and_HSV) and have a look at the **L** component. 1 = white, 0 = black – Andreas Aug 20 '12 at 18:47
  • 4
    @Andreas HSL lightness doesn't take human perception into account. An L value of 0.5 will have a different perceived brightness for different hues. – Alnitak Aug 20 '12 at 18:55
  • 1
    @Alnitak You're right but the description of the TO isn't that precisely. So any value below 3/8 could have been dark enough for his purpose. – Andreas Aug 20 '12 at 19:06
  • 1
    @Andreas that depends - if you look at the ITU luminance values in my answer you'll see that blue is perceived as only 1/10th as bright as green. – Alnitak Aug 20 '12 at 20:11

9 Answers9

296

You have to extract the three RGB components individually, and then use a standard formula to convert the resulting RGB values into their perceived brightness.

Assuming a six character colour:

var c = c.substring(1);      // strip #
var rgb = parseInt(c, 16);   // convert rrggbb to decimal
var r = (rgb >> 16) & 0xff;  // extract red
var g = (rgb >>  8) & 0xff;  // extract green
var b = (rgb >>  0) & 0xff;  // extract blue

var luma = 0.2126 * r + 0.7152 * g + 0.0722 * b; // per ITU-R BT.709

if (luma < 40) {
    // pick a different colour
}

EDIT

Since May 2014 tinycolor now has a getBrightness() function, albeit using the CCIR601 weighting factors instead of the ITU-R ones above.

EDIT

The resulting luma value range is 0..255, where 0 is the darkest and 255 is the lightest. Values greater than 128 are considered light by tinycolor. (shamelessly copied from the comments by @pau.moreno and @Alnitak)

John Smith
  • 2,282
  • 1
  • 14
  • 22
Alnitak
  • 334,560
  • 70
  • 407
  • 495
  • 20
    haven't seen some good bit manipulation in javascript in a while. cool stuff. http://en.wikipedia.org/wiki/Rec._709#Luma_coefficients – jbabey Aug 20 '12 at 18:54
  • 2
    Good code, but after testing I suggest var luma = (r + g + b)/3; if (luma < 128) { // will be more useful. } – Terry Lin Jul 10 '15 at 17:11
  • 3
    @TerryLin Why? The coefficients given are the _standard_ ITU values that allow for the fact that green is _perceived_ more brightly than red (and then blue). – Alnitak Jul 11 '15 at 12:17
  • @Alnitak I use your code to detect dark/light background and then display white/black font color, the result is not good. But average r,g,b and luma < 128 = dark background, luma > 128 = light background, the result is what I need. Thank you very much for your code. – Terry Lin Jul 12 '15 at 07:16
  • @TerryLin well, whatever works for you. This worked for the OP :) – Alnitak Jul 12 '15 at 21:02
  • 3
    The resulting `luma` value range is `0..255`, where `0` is the darkest and `255` is the lightest (the three coefficients sum to one). – pau.moreno Aug 26 '15 at 10:13
  • luma < 40 ? what does it mean? is it more dark? also why 40? not 125? – MeetMahPuppy Mar 26 '16 at 14:05
  • @MeetMahPuppy the number 40 in this case is just an example – Alnitak Mar 26 '16 at 15:51
  • 1
    @gabssnake only since May 2014, and the `isDark()` threshold is hard-coded at 128 – Alnitak Apr 06 '16 at 16:31
  • @Alnitak fair enough – gabssnake Apr 09 '16 at 09:17
  • let [r, g, b] = elem.style.backgroundColor.match(/\d+/g); – okliv Oct 05 '22 at 15:44
59

I found this WooCommerce Wordpress PHP function (wc_hex_is_light) and I converted to JavaScript. Works fine!

function wc_hex_is_light(color) {
    const hex = color.replace('#', '');
    const c_r = parseInt(hex.substr(0, 2), 16);
    const c_g = parseInt(hex.substr(2, 2), 16);
    const c_b = parseInt(hex.substr(4, 2), 16);
    const brightness = ((c_r * 299) + (c_g * 587) + (c_b * 114)) / 1000;
    return brightness > 155;
}

@Sliffcak, thanks for comment... To use substring, because substr was deprecated:

function wc_hex_is_light(color) {
    const hex = color.replace('#', '');
    const c_r = parseInt(hex.substring(0, 0 + 2), 16);
    const c_g = parseInt(hex.substring(2, 2 + 2), 16);
    const c_b = parseInt(hex.substring(4, 4 + 2), 16);
    const brightness = ((c_r * 299) + (c_g * 587) + (c_b * 114)) / 1000;
    return brightness > 155;
}
Sergio Cabral
  • 6,490
  • 2
  • 35
  • 37
  • 4
    Super cool, thx! I tested it with several colors, detection was correct with all of them :) – David Dal Busco Feb 12 '19 at 12:36
  • colorIsDarkOrLight(color) { var hex = color.replace("#", ""); var c_r, c_g, c_b, brightness = ""; if (hex.length == 3) { c_r = parseInt(hex.substr(0, 2), 16); c_g = parseInt(hex.substr(1, 2), 16); c_b = parseInt(hex.substr(2, 2), 16); brightness = (c_r * 299 + c_g * 587 + c_b * 114) / 1000; } else { c_r = parseInt(hex.substr(0, 2), 16); c_g = parseInt(hex.substr(2, 2), 16); c_b = parseInt(hex.substr(4, 2), 16); } return brightness > 155; }, – Pedro Henrique Nov 04 '19 at 00:43
  • Use with hex 3 characteres and 6 – Pedro Henrique Nov 04 '19 at 00:43
  • This code is GPL licensed. Please don't steal without following the license rules. (I am not affiliated with this project nor am I a lawyer) https://github.com/woocommerce/woocommerce/blob/5d7f6acbcb387f1d51d51305bf949d07fa3c4b08/license.txt – jayphelps Aug 22 '21 at 04:00
  • 2
    The author has always been quoted. The original source code was always linked. What did you (@jayphelps) mean by 'steal'? – Sergio Cabral Aug 23 '21 at 06:39
  • 1
    @SergioCabral thanks! however substr has been deprecated. when I switched it to substring it is not working – Sliffcak Aug 25 '22 at 00:00
37

The TinyColor library (you've already mentioned it) provides several functions for inspecting and manipulating colors, among them:

Community
  • 1
  • 1
skalee
  • 12,331
  • 6
  • 55
  • 57
7

This work with hex e.g #fefefe

function isTooDark(hexcolor){
    var r = parseInt(hexcolor.substr(1,2),16);
    var g = parseInt(hexcolor.substr(3,2),16);
    var b = parseInt(hexcolor.substr(4,2),16);
    var yiq = ((r*299)+(g*587)+(b*114))/1000;
    // Return new color if to dark, else return the original
    return (yiq < 40) ? '#2980b9' : hexcolor;
}

You can change it to return true or false by change

return (yiq < 40) ? '#2980b9' : hexcolor;

to

return (yiq < 40);
TheCrazyProfessor
  • 919
  • 1
  • 15
  • 31
6

You can compute the luminance:

Luminance is thus an indicator of how bright the surface will appear.

So it's great to choose if the text should be white or black.

var getRGB = function(b){
    var a;
    if(b&&b.constructor==Array&&b.length==3)return b;
    if(a=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(b))return[parseInt(a[1]),parseInt(a[2]),parseInt(a[3])];
    if(a=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(b))return[parseFloat(a[1])*2.55,parseFloat(a[2])*2.55,parseFloat(a[3])*2.55];
    if(a=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(b))return[parseInt(a[1],16),parseInt(a[2],16),parseInt(a[3],
16)];
    if(a=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(b))return[parseInt(a[1]+a[1],16),parseInt(a[2]+a[2],16),parseInt(a[3]+a[3],16)];
    return (typeof (colors) != "undefined")?colors[jQuery.trim(b).toLowerCase()]:null
};

var luminance_get = function(color) {
    var rgb = getRGB(color);
    if (!rgb) return null;
        return 0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2];
}

The method above allows you to pass the color in different formats, but the algorithm is basically just in luminance_get.

When I used it, I was setting the color to black if the luminance was greater than 180, white otherwise.

approxiblue
  • 6,982
  • 16
  • 51
  • 59
Robin
  • 21,667
  • 10
  • 62
  • 85
6

There's an important distinction here between luminance and brightness. Luminance, at the end of the day, is a measure of how much energy travels through a certain area and completely ignores how our perceptual systems perceive that energy. Brightness, on the other hand, is a measure of how we perceive that energy and takes into the account the relationship between luminance and our perceptual system. (As a point of confusion, there is a term called relative luminance, which seems to be used synonymously with brightness terms. It tripped me up good).

To be precise, you are looking for "brightness" or "value" or "relatively luminance" as others have suggested. You can calculate this in several different way (such is to be human!) http://en.wikipedia.org/wiki/HSL_and_HSV#Lightness

  1. Take the max of R, G, and B.
  2. Take the average of the max and the min from R, G, and B.
  3. Take the average of all three.
  4. Use some weighted average as others have suggested here.
David Nguyen
  • 331
  • 2
  • 5
  • AFAIK only the _luma_ calculation described on the Wikipedia page is a perception-based model. – Alnitak Aug 20 '12 at 21:57
  • 2
    It's nice to point out the distinction between physical light energy and perceived brightness, but I think you've got things rather mixed up. The section of the Wikipedia article you linked to has a fourth bullet point, which states "A *more perceptually relevant* alternative is to use luma, Y′, as a lightness dimension" (emphasis mine) and then proceeds to give the formula presented in Alnitak's and Robin's answers. In other words, the method you've left out and recommended against is the one which best matches human perception. – John Y Aug 20 '12 at 21:58
  • @JohnY yes, that's what I was trying to say - he's left out the only one which actually matches the rest of his answer. – Alnitak Aug 21 '12 at 06:45
  • Yes, turns out I was the only one confused here. I'm okay with that :) I just wanted to get the major point across that there is a different between energy and perception. I will update my answer accordingly. – David Nguyen Aug 21 '12 at 18:42
3

I realize this conversation is a few years old, but it is still relevant. I wanted to add that my team was having the same issue in Java (SWT) and found this to be a bit more accurate:

private Color getFontColor(RGB bgColor) {
    Color COLOR_BLACK = new Color(Display.getDefault(), 0, 0, 0);
    Color COLOR_WHITE = new Color(Display.getDefault(), 255, 255, 255);

    double luminance = Math.sqrt(0.241 
       * Math.pow(bgColor.red, 2) + 0.691 * Math.pow(bgColor.green, 2) +  0.068 
       * Math.pow(bgColor.blue, 2));
    if (luminance >= 130) {
        return COLOR_BLACK;
    } else {
        return COLOR_WHITE;
    }
}
Chris Clark
  • 340
  • 2
  • 13
2

A possible solution would be to convert your color from RGB to HSB. HSB stands for hue, saturation, and brightness (also known as HSV, where V is for value). Then you have just one parameter to check: brightness.

approxiblue
  • 6,982
  • 16
  • 51
  • 59
Ohad
  • 1,719
  • 1
  • 16
  • 20
1

I combined the answers of @Alnitak and @SergioCabral below. I also implemented a hex value parser that works for standard and short forms.

const hexToRgb = (hex) =>
  (value =>
    value.length === 3
      ? value.split('').map(c => parseInt(c.repeat(2), 16))
      : value.match(/.{1,2}/g).map(v => parseInt(v, 16)))
  (hex.replace('#', ''));

// Luma - https://stackoverflow.com/a/12043228/1762224
const isHexTooDark = (hexColor) =>
  (([r, g, b]) =>
    (0.2126 * r + 0.7152 * g + 0.0722 * b) < 40)
  (hexToRgb(hexColor));

// Brightness - https://stackoverflow.com/a/51567564/1762224
const isHexTooLight = (hexColor) =>
  (([r, g, b]) =>
    (((r * 299) + (g * 587) + (b * 114)) / 1000) > 155)
  (hexToRgb(hexColor));

console.log(isHexTooDark('#222'));  // true
console.log(isHexTooLight('#DDD')); // true
Mr. Polywhirl
  • 42,981
  • 12
  • 84
  • 132