Calculating contrasting colours in javascript
Asked Answered
E

7

20

On one of pages we're currently working on users can change the background of the text displayed.

We would like to automatically alter the foreground colour to maintain reasonable contrast of the text.

We would also prefer the colour range to be discretionary. For example, if background is changing from white to black in 255 increments, we don't want to see 255 shades of foreground colour. In this scenario, perhaps 2 to 4, just to maintain reasonable contrast.

Any UI/design/colour specialists/painters out there to whip out the formula?

Enamelware answered 11/3, 2009 at 15:23 Comment(0)
C
15

In terms of sheer readability, you want to use black and white text on whatever background it is. So convert RGB to HSV, and just check whether V is < 0.5. If so, white, if not, black.

Try that first and see if you find it attractive.

If you don't, then you probably want the white and black not to be so stark when your background is too bright or too dark. To tone this down, keep the same hue and saturation, and use these values for brightness:

background V  foreground V
0.0-0.25      0.75
0.25-0.5      1.0
0.5-0.75      0.0
0.75-1.0      0.25

On a medium color, you'll still see black or white text which will be nicely readable. On a dark color or light color, you'll see the same color text but at least 3/4 away in terms of brightness and therefore still readable. I hope it looks nice :)

Cummerbund answered 11/3, 2009 at 15:31 Comment(2)
For whatever reason HSV does not give the desired effect. HSL, on the other hand, with the exeption of few saturated colours, even in pure b/w (with a slight bias towards black) does the job quite nicely.Enamelware
OK, maybe HSL is what I really meant :) I'm not totally sure.Cummerbund
M
38

Basing your black-white decision off of luma works pretty well for me. Luma is a weighted sum of the R, G, and B values, adjusted for human perception of relative brightness, apparently common in video applications. The official definition of luma has changed over time, with different weightings; see here: http://en.wikipedia.org/wiki/Luma_(video). I got the best results using the Rec. 709 version, as in the code below. A black-white threshold of maybe 165 seems good.

function contrastingColor(color)
{
    return (luma(color) >= 165) ? '000' : 'fff';
}
function luma(color) // color can be a hx string or an array of RGB values 0-255
{
    var rgb = (typeof color === 'string') ? hexToRGBArray(color) : color;
    return (0.2126 * rgb[0]) + (0.7152 * rgb[1]) + (0.0722 * rgb[2]); // SMPTE C, Rec. 709 weightings
}
function hexToRGBArray(color)
{
    if (color.length === 3)
        color = color.charAt(0) + color.charAt(0) + color.charAt(1) + color.charAt(1) + color.charAt(2) + color.charAt(2);
    else if (color.length !== 6)
        throw('Invalid hex color: ' + color);
    var rgb = [];
    for (var i = 0; i <= 2; i++)
        rgb[i] = parseInt(color.substr(i * 2, 2), 16);
    return rgb;
}
Mccluskey answered 28/6, 2011 at 18:50 Comment(0)
C
15

In terms of sheer readability, you want to use black and white text on whatever background it is. So convert RGB to HSV, and just check whether V is < 0.5. If so, white, if not, black.

Try that first and see if you find it attractive.

If you don't, then you probably want the white and black not to be so stark when your background is too bright or too dark. To tone this down, keep the same hue and saturation, and use these values for brightness:

background V  foreground V
0.0-0.25      0.75
0.25-0.5      1.0
0.5-0.75      0.0
0.75-1.0      0.25

On a medium color, you'll still see black or white text which will be nicely readable. On a dark color or light color, you'll see the same color text but at least 3/4 away in terms of brightness and therefore still readable. I hope it looks nice :)

Cummerbund answered 11/3, 2009 at 15:31 Comment(2)
For whatever reason HSV does not give the desired effect. HSL, on the other hand, with the exeption of few saturated colours, even in pure b/w (with a slight bias towards black) does the job quite nicely.Enamelware
OK, maybe HSL is what I really meant :) I'm not totally sure.Cummerbund
W
7

Try this example, which just sets it to black or white depending on the average of the r,g and b values. Also good for testing! Substitute a better algorithm in there if you find cases that don't work very well.

<html> 
<head> 
<script> 
function rgbstringToTriplet(rgbstring)
{
    var commadelim = rgbstring.substring(4,rgbstring.length-1);
var strings = commadelim.split(",");
var numeric = [];
for(var i=0; i<3; i++) { numeric[i] = parseInt(strings[i]); }
return numeric;
}

function adjustColour(someelement)
{
   var rgbstring = someelement.style.backgroundColor;
   var triplet = rgbstringToTriplet(rgbstring);
   var newtriplet = [];
   // black or white:
   var total = 0; for (var i=0; i<triplet.length; i++) { total += triplet[i]; } 
   if(total > (3*256/2)) {
     newtriplet = [0,0,0];
   } else {
 newtriplet = [255,255,255];
   }
   var newstring = "rgb("+newtriplet.join(",")+")";
   someelement.style.color = newstring;
   return true;
}

function randomiseBackground(someelement)
{
 var vals = [];
 for(var i=0; i<3; i++) { vals[i]=Math.floor(Math.random()*256+1); }
 someelement.style.backgroundColor="rgb("+vals.join(",")+")";
 return true;
}
</script> 
</head> 
<body onload="adjustColour(document.getElementById('mypara'));"> 
<p id="mypara" style="background-color:#2287ea">Some text</p> 
<form> 
    <input type=button onclick="var elem=document.getElementById('mypara'); randomiseBackground(elem); adjustColour(elem);" value="randomise background" /> 
</form> 
</body> 
</html>
Wardmote answered 11/3, 2009 at 16:54 Comment(0)
C
1

You probably want to shift hue, but not saturation or value.

So, convert your RGB value into HSV, add 180 degrees to H (modulo 360 degrees of course), and convert back to RGB.

Conversion forumlas are here.

Crenelate answered 11/3, 2009 at 15:25 Comment(1)
Ugh, that's a terrible idea. Shifting hue gets you a muddy mess at best (wood brown + dark blue) and a blinding headache at worst (red + green). A much safer course is shifting value.Refugiorefulgence
I
1

If you are using d3.js, you can do some judgment with

d3.hsl(d3.color("black")).l > 0.5 ? "#000" : "fff" (simple but may not perfect)

or

getLuma(r, g, b, fmt) > 165 ? "#000" : "#fff" see Example2

Example1

<script src="https://d3js.org/d3.v7.min.js"></script>
<script>
  const bgColorList = ["black", "purple", "#cc1515", "green", "blue", "red", "yellow", "white"]
  window.onload = () => {
    d3.select("body").append("section")
      .selectAll("div")
      .data(bgColorList)
      .enter().append("div")
      .text((bgColor, idx, arr) => {
        const div = arr[idx]
        const {h, l} = d3.hsl(d3.color(bgColor))
        const foreColor = l > 0.5 ? "#000" : "#fff"
        div.outerHTML = `<div style="width:100px;height:50px;
background-color: ${bgColor};
color:${foreColor}
">Hello world</div>`
      })
  }
</script>

But when l is equal to 0.5 (for example: yellow, blue), there will be controversy.

Example2

<script src="https://d3js.org/d3.v7.min.js"></script>

<table>
  <tr>
    <th>601</th>
    <th>709</th>
    <th>240</th>
  </tr>
  <tr id="output"></tr>
</table>

<script>
  /**
   * https://en.wikipedia.org/wiki/Luma_%28video%29
   * @param {"601"|"709"|"240"|"145"} format
   */
  function getLuma(r, g, b, format) {
    switch (format) {
      case "601":
        return 0.2999 * r + 0.587 * g + 0.114 * b
      case "709":
        return 0.2126 * r + 0.7152 * g + 0.0722 * b
      case "145":
      case "240":
        return 0.212 * r + 0.701 * g + 0.087 * b
    }
  }

  const bgColorList = [...Array(100).keys()].map(t => d3.scaleSequential(d3.interpolateTurbo)(t / 100))
    .concat(["black", "purple", "#cc1515", "green", "blue", "red", "white", "yellow"])
  window.onload = () => {
    const tdArr = []
    d3.select("#output")
      .selectAll("td")
      .data(["601", "709", "240"])
      .enter().append("td")
      .text((fmt, _idx, _arr) => {
        const td = _arr[_idx]
        tdArr.push([td, fmt])
      })

    tdArr.forEach(([td, fmt]) => {
      d3.select(td).append("section")
        .selectAll("div")
        .data(bgColorList)
        .enter().append("div")
        .text((bgColor, idx, arr) => {
          const div = arr[idx]
          const {r, g, b} = d3.color(bgColor)
          const foreColor = getLuma(r, g, b, fmt) > 165 ? "#000" : "#fff"
          // const coefficient = [0.2126, 0.7152, 0.0722]
          // const foreColor = [r, g, b].reduce((sum, color, i) => sum + color * coefficient[i], 0) > 165 ? "#000" : "#fff"
          div.outerHTML = `<div style="width:100px;height:26px;
background-color: ${bgColor};
color:${foreColor}
">Hello</div>`
        })
    })
  }
</script>

This is same as the enigment's answer

Iridotomy answered 15/12, 2021 at 8:51 Comment(0)
T
0

It's a bit complicated t implement because of javascript's way of dealing with decimal values. For this purpose I've found it to be much better to let the background change any color but the text just switch between black and white. If you use contrasting colors alot of combinations will be really hard to stand. Also note that some people are colorblind, so it's not a very good idea from accessibility perspective.

Tripersonal answered 11/3, 2009 at 15:44 Comment(0)
F
0

This is a very old question I run into, but I didn’t like any of the answers so I’m providing mine:

To get a favorable text color you can check the background color's luminance and return either black or white color for the text.

See the function GetLumColor() below in this demo code which is the crux of the answer to the question.

<!DOCTYPE html><head></head>

<body onload="Test();">

<script type="text/javascript">

//--------------------------------------------------------------------------------

function RandomNum(Num){return Math.floor(Math.random()*Num);}

//--------------------------------------------------------------------------------

// NB: accepts rgb() or rgba() color values but the alpha-channel is not taken into consideration

function GetLumColor(Rgb){

Rgb=Rgb.replace(/ /g,'').replace('rgba(','').replace('rgb(','').replace(')','').split(',');

let R=Number(Rgb[0]), G=Number(Rgb[1]), B=Number(Rgb[2]), Color='black';

let Lum=((R+G+B)*100)/765; // luminance as a percentage

if(Lum<50){Color='white';} // adjust this number to suit

return Color;}

//--------------------------------------------------------------------------------

// Tests the function above

function Test(){

var S='', Bg='', Fg='';

for(let i=0; i<40; i++){

 Bg='rgb('+RandomNum(256)+','+RandomNum(256)+','+RandomNum(256)+')';

 Fg=GetLumColor(Bg);

 S+='<div style="background:'+Bg+'; color:'+Fg+';"> Sample 123 </div>';

}

document.write(S);

}

//--------------------------------------------------------------------------------

</script></body></html>

Basic usage...

some_ID.style.color = GetLumColor(some_ID.style.background);
Foin answered 4/5 at 2:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.