loading...

Write a function that outputs a pleasant text color hex given a background color hex

ben profile image Ben Halpern ・1 min read

"Pleasant" means a high enough contrast for reading, but also might include colors that don't clash with one another.

Do your best. No harsh judgement. 😝

Any languages/approaches welcome.

Discussion

pic
Editor guide
Collapse
elmuerte profile image
Michiel Hendriks

Basically rotate the HSL components ((value+0.5)%1). The complicated part is converting a rgb() to hsl().

function niceColor(rgb) {    // assumes "rgb(R,G,B)" string
  let hsl = rgb2hsl(rgb);
  hsl[0] = (hsl[0]+0.5) % 1; // Hue
  hsl[1] = (hsl[1]+0.5) % 1; // Saturation
  hsl[2] = (hsl[2]+0.5) % 1; // Luminocity
  return 'hsl('+(hsl[0]*360)+','+(hsl[1]*100)+'%,'+(hsl[2]*100)+'%)';
}

Collapse
elmuerte profile image
Michiel Hendriks

Note that only changing the luminosity part (hsl[2]) is vital. Disabling the other parts (hue and saturation) also creates pleasant results, just less colorful.

Collapse
nektro profile image
Meghan (she/her)
// assumes bgColor is a rgb() string: "rgb(66, 134, 244)"
function getTextColor(c) {
  let rgb = c.substring(4, bgColor.indexOf(')')).split(", ").map(x => parseInt(x));
  let o = Math.round(((parseInt(rgb[0]) * 299) + (parseInt(rgb[1]) * 587) + (parseInt(rgb[2]) * 114)) /1000);
  return ((o > 125) ? ('black') : ('white'));
}
Collapse
ben profile image
Ben Halpern Author

So this returns black or white, but what if the ideal solution returned, perhaps a very dark version of the background color. Like, dark blue on light blue might be more pleasant than black on light blue etc.

Collapse
nektro profile image
Meghan (she/her)

Would this work then? get color, convert color to HSL, return L - 100 % 100

Collapse
cathodion profile image
Dustin King

I decided to use machine learning...

14 hours later 😬, here's a work in progress for a script that randomly generates a background color, then generates the foreground color from a neural net that learns from your selections.

Edit:

Converted to codepen by @rhymes :

The gist is still here but I'll take the embed out since gist embeds load slowly and take up a lot of screen real estate.

Edit 2: I cleaned up the code in both places so hopefully it's more readable now. The bulk of the neural stuff happens in regenerateColors() and trainOnCurrentColors().

Collapse
rhymes profile image
rhymes

I put it in a Codepen for you:

fork it to your account :-)

Collapse
ben profile image
Collapse
booligoosh profile image
Ethan

This would be great if my color choices had any logic to them πŸ˜‚πŸ˜‚πŸ˜‚

No, seriously this is amazing ❀️

Collapse
aralroca profile image
Aral Roca

"Pleasure" is a little bit subjective. Basically I based with the complementary color to be sure that the text is possible to read:

Complementary colors

const complementaryColor = (color) => {
    const hexColor = color.replace('#', '0x');

    return `#${('000000' + (('0xffffff' ^ hexColor).toString(16))).slice(-6)}`;
};

complementaryColor('#ff0000');  // #00ffff
Collapse
aralroca profile image
Aral Roca

React example with random color of background color + complementary for text:

Collapse
itsasine profile image
ItsASine (Kayla)

I mainly was having fun with research for this. Color theory, the parts of RGB color, playing with jQuery...

Known bug: the contrast for Cyan sucks.

Known feature enhancement: provide better suggestions than just white or black πŸ˜…

Collapse
mxl profile image
Maria Boldyreva

I implemented this in python:

import argparse
from colorsys import hls_to_rgb, rgb_to_hls
import sys

# setting up arguments
parser = argparse.ArgumentParser(
    description='Function that outputs a pleasant text color hex '
                'given a background color hex')
parser.add_argument('color', help='Color in #xxxxxx or xxxxxx format')

args = parser.parse_args()
color = args.color

# removing #
if color.startswith('#'):
    color = color[1:]

# splitting, converting to decimal and then to 0-1 range
try:
    r, g, b = [int(color[i:i+2], 16) / 255.0 for i in range(0, len(color), 2)]
except ValueError:
    print 'Wrong color supplied'
    sys.exit()

# shifting
h, l, s = [(i + 0.5) % 1 for i in rgb_to_hls(r, g, b)]

# converting back to rgb
result = [format(int(i*255), 'x') for i in hls_to_rgb(h, l, s)]
print '#{}'.format(''.join(result))
Collapse
jessachandler profile image
Jess Chandler

I wasn't sure what we should do with "pleasant", so I just created little functions to adjust the color

function adjustHex(y) { 
  const x = parseInt(y, 16); 
  if (x<50) {
    return '00';
  } else if(x<=200) {
    return (x-50).toString(16);
  } else {
    return (255).toString(16); 
  }
}

function splitColor(x) { 
  while (x.length <6) { 
    x = '0' + x 
  }; 
  if (x=='000000') {
     return 'ffffff' 
  } else if (x=='ffffff'){
    return '000000'
  } else {
    let y = x.split(/(?=(?:..)*$)/); 
    return adjustHex(y[0])+adjustHex(y[1])+adjustHex(y[2]);
  }
 }

The logic is that if there is an RGB close to FF, then, max that out. If there is one close to 00, then zero that out. If it is in between, lighten it. If the background is black or white, return the opposite.

for example
splitColor('ff00aa') returns "ff0078"
splitColor('ffffff') returns "000000"

There are some edge cases that I did not address - every one that is a combination of extremes. I did not create this in codepen, and now I am kicking myself for just whipping it up in the javascript console while I was debugging something else!

Collapse
andrewlucker profile image
Andrew Lucker

I once attempted this problem for setting up a color scheme for a website. Turns out "pleasant" depends on the size of inner/outer elements. The problem is called "color relativity". People don't physically identify colors based on RGB wavelengths, instead we look at contrast with surrounding context. If there is very little contrast, we can perceive very slight differences in absolute color value. If there is very high contrast, we can only perceive significant differences. Also context can "shift" colors to look the same. For example:

High contrast: At night, with house or street lights. Most people become nearly color blind.
Low contrast: Snow blind. People can visually identify physical shapes of hills, roads, trees etc. despite everything being white.

Collapse
miionu profile image
Rospars Quentin

I made a black and white version. I'll try to work on it a bit more later

(Click on the background to change the color)

Collapse
jrohatiner profile image
Judith

examples.achickandaclick.org/color...

Caveat: this was already done :P and I hope that the rgb(0,0,0) text being only one color (non-color: black is really no color) is considered pleasant :) That being said; the input of a hex generates a color in the box with black text showing the rgb of the hex color. I used js, jquery, html and css :) Namaste!

ps there are two functions

Collapse
bladefidz profile image
Hafidz Jazuli Luthfi

To be honest, I am sorry to not post the code, but just underlying theory. Based what I already learn:

  1. Blue is not good for text color, so try to adding more proportion on Green or Red. Good one proportion is L = 31%R + 59%G + 10%B.
  2. 10% males are color blind and there are more man who code than women, so try to use font with sub-pixel rendering. Sans-serif family may the good one. Another good trick is try to adding more contrast.
  3. Eye can be exhausted, so try to use cooler color as background and sharp color for details such as text and button.

I am also attach an image below to help you interpret:
Color Perception

That's all, hope somebody find this post useful.

Collapse
yechielk profile image
Yechiel Kalmenson

This is when I need the πŸ€” reaction back... πŸ˜‚

Collapse
ben profile image
Ben Halpern Author

I think it'll be back at some point. The underlying logic allows for arbitrary reactions but the proper UI direction is not so obvious.

πŸ€”πŸ€”πŸ€”πŸ€”πŸ€”πŸ€”πŸ€”πŸ€”πŸ€”πŸ€”πŸ€”πŸ€”