

Calculating Color Contrast for Legible Text in Ruby - charliepark
http://charliepark.tumblr.com/post/827693445/calculating-color-contrast-for-legible-text-in-ruby

======
nitrogen
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.

~~~
jacobolus
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.

~~~
jacobolus
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.

------
statenjason
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.

~~~
chriseppstein
Bingo! Such logic is supposed to live in the stylesheets with the design.

------
thristian
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.

------
bradleyland
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

~~~
charliepark
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.

------
azymm
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.

~~~
extension
No it doesn't. That effectively ignores the green and blue channels.

~~~
azymm
Ah, thank you very much for pointing that out!

