13 comments

[ 3.1 ms ] story [ 62.4 ms ] thread
Why the extra method for converting to brightness? Why not just this:

def contrasting_text_color(background_hex_color) (background_hex_color.scan(/../).map {|color| color.hex}).sum > 382.5 ? '#000' : '#fff' end

Could certainly do it that way, if you don't need the brightness value.

We have a color picker that sorts a hash of "available" hexadecimal colors by their brightness value, though, so we needed to have it available as an independent method.

I also had to do something like this for my Python project and ended up with this:

'#000' if int(my_color, 16) > 0xffffff/2 else '#fff'

This approach isn't perfect, but it does the job.

No it doesn't. That effectively ignores the green and blue channels.
Ah, thank you very much for pointing that out!
This is ugly, but it should do the job:

"#000000" if sum([int(i,16) for i in re.findall("..",myColour)]) > 382.5 else "#ffffff"

There may be better ways of calculating brightness values for determining true perceptual contrast. Some starting points for the curious:

http://en.wikipedia.org/wiki/Colorspace http://en.wikipedia.org/wiki/YUV http://en.wikipedia.org/wiki/HSL_and_HSV http://en.wikipedia.org/wiki/YIQ

The sample images at http://en.wikipedia.org/wiki/HSL_and_HSV#Disadvantages show the difference between various brightness calculations.

Yeah. I wrote the long one of those. What you should use for best results is something like CIELAB lightness. Basically the steps are:

1. Take "gamma-compressed" RGB and scale to the range [0, 1] (if it's an 8-bit integer representation, divide by 255), and convert to linear RGB: undo the sRGB nonlinearity or for the lazy, take each component to the 2.2 power.

2. Take a weighted sum of R, G, B components. The weights for an sRGB/HDTV image are .2126, .7152, and .0722 for R, G, and B respectively. This gives you the luminance, Y.

3. Apply some non-linearity to the luminance to get a decent correlate of human-perceived lightness. A gamma curve w/ 2.2 gamma (that is, raise your value of Y to the (1/2.2) power) actually is an okay function to use. If you want a better one, feel free to use the CIELAB definition: L = 116 Y^(1/3) - 16, where L ranges from 0 to 100.

4. Anything with L >= 50, use black, anything with L < 50, use white.

(properly L*, but hacker news will think I'm using the asterisk to indicate emphasis if there is more than one of them)

Since this is likely only being calculated for a few colors (even up to dozens or hundreds), there's really not enough overhead in doing this math to worry about the speed.

I found the HSV/HSL article immensely useful on a semi-recent project, as Google seems to have forgotten about the original references I used when I first learned about those two colorspaces (i.e. students' and professors' personal web sites, found using the Altavista search engine).
Actually, now that I think about it, if the only goal is to decide on black or white text, steps 3–4 aren't really necessary. Just undo the gamma compression (1), take a weighted sum or linear RGB intensities (2), and then pick a cut-off for Y of 18% or 18.4% (or whatever you prefer), about at mid gray.
I wanted to do something similar to figure out which of the 256 colours available in my terminal-emulator would be reasonably visible against a black background. I wrote a bash script to answer the question, at the heart of which was this function:

  get_luminance_from_color() {
      # Return the luminance of a color as a number 0-65535.
      local red=$1
      local green=$2
      local blue=$3
  
      # Colour weights from Wikipedia. Note bash doesn't have floats.
      echo $(( (2126 * $red + 7152 * $green + 722 * $blue) / 10000 ))
  }
It works like this:

  $ get_luminance_from_color 16384 32768 16384
  28101
...which is to say, then input is an RGB tuple where each field is 0-65535, and the output is a single integer between 0-65535 representing the luminance. If the luminance is > 32767, it's a light colour, otherwise it's a dark colour.
Interesting. Made me think to write a simple mixin for sass.

    @mixin fg-contrast($bg) {
      background: $bg;
      $textcolor: #ffffff;
      @if lightness($bg) > 50% {
        $textcolor: #000000;
      }
      color: $textcolor;
    }
Could also be done by using the red(), green(), and blue() functions, but lightness was simpler.
Bingo! Such logic is supposed to live in the stylesheets with the design.