Have you ever wondered how to tell whether two colors are "close" together in that they would be hard to read if overlapped? For instance, if you have a background color that's white and a foreground color that's light grey -- how hard is it to see the foreground color?
I recently ran into this issue with the REALbasic preferences dialog. There's a listbox which allows you to pick various preferential colors for things like syntax highlighting, etc. The listbox background color defaults to white, except that background color isn't *always* white. For instance, if you change the default "window" color on Windows, then the ListBox also gets that as its default color.
I happened to be testing a bug fix that involved the default window color being something other than white (which is common for people with visual disabilities, I might add) and I noticed that I could no longer read some of my color preferences in that listbox because the background color almost exactly matched the text color.
And thus was born my quest for determining how "close" two colors are to one another. Yay for math! You can find the Cartesian distance between the colors by using their RGB (or better yet, their HSV) parts. Remember, a Cartesian distance is simply a distance in space -- so you can map the distance between two objects. The equation is: f = Sqrt( d1 ^ 2 + d2 ^ 2 + ... + dn ^ 2 ), where d1 = Hue (or Red), d2 = Saturation (or Green), etc.
So, a simple function can determine the "distance" between two colors:[rbcode]
Function CartesianDistance( c1 as Color, c2 as Color ) as Double
Return Sqrt( (c1.hue - c2.hue) ^ 2 + (c1.saturation - c2.saturation) ^ 2 + (c1.value - c2.value) ^ 2 )
End Function[/rbcode]
Now you can call this function and it will tell you the relative distance between the two colors. Small values (closer to 0) are considered close together, and larger values mean they're farther apart.
So now comes the second piece to the puzzle -- how do you pick a color that's farther apart? Easy -- pick the "opposite" color! The values of each part of HSV (and RGB as well) are constrained to a specific range. With HSV, that's 0 to 1 (RGB is 0 to 255). So for any given value, you can compute its inverse, which leads to this function:[rbcode]
Function OppositeColor( c as Color ) as Color
dim ret as Color = HSV( 1 - c.hue, 1 - c.saturation, 1 - c.value )
if CartesianDistance( c, ret ) < .1 then
if ret.Value < .5 then ret = &cFFFFFF else ret = &c000000
end if
return ret
End Function[/rbcode]
(Edit (11am): Big thanks to Steve Garman for pointing out a flaw in the above function and working out a solution with me)
Given these two functions, you now have a way to change around the colors so that you can display easily. For instance, if the background color doesn't matter, but you want to display the foreground color in a readable manner, you can write code like this:
[rbcode]
if CartesianDistance( g.ForeColor, Platform.DefaultWindowBackgroundColor ) < .1 then
dim oldForeColor as Color = g.ForeColor
g.ForeColor = OppositeColor( g.ForeColor )
g.FillRect( 0, 0, g.Width, g.Height )
g.ForeColor = oldForeColor
end if
[/rbcode]
Obviously, your situations may vary -- but in this case, the code was used in a ListBox.CellTextPaint event, and so we use some declares (hidden away in the Platform module) to find the default background color of the ListBox.
Isn't math fun?
Am I misunderstanding your OppositeColor function?
Try this
EditField1.BackColor = &C3f7f7f
EditField2.BackColor = OppositeColor( EditField1.BackColor)
Good point, if the values are all at 50% (or close to it), then you'll get the same color back from the function. The OppositeColor seems to only work for values not near 50%.
So let's see if we can come up with a better algorithm for OppositeColor!
I've struggled and struggled with it.
In the end, I tend to use black or white for the lettering depending on which side of 128 c.Red falls.
Perhaps that should be depending on the cartesian distance from RGB(128,128,128)
Essentially, there's one problem with one fringe-case requiring a little extra work..
Problem: invert a color based on HSV or RGB
Solution: invert the constituent parts.
Fringe case: the inverse can be "too close" to the original value.
Modified solution: check the inversion against the original value using the CartesianDistance. If it fails that test, then the "opposite" color isn't effective, so choose an effective color.
So here's my modified version of OppositeColor, what's your thoughts?
Function OppositeColor(c as Color) As Color
dim ret as Color = HSV( 1 - c.hue, 1 - c.saturation, 1 - c.value )
if CartesianDistance( c, ret ) if ret.Value end if
return ret
End Function
Yes. The alternative colour to be black or white, whichever has the greatest Cartesian distance from the original value
Hmm, that another interesting way of looking at it. I guess my thought is that there's no need to test the Cartesian distance from black or white because the value of that could end up being the same as well. For instance, if HSV = .5, .5, .5, then it's going to be the same distance to HSV = 0, 0, 0 as it is HSV = 1, 1, 1. So I'd just pick black or white based on the Value of the color. Values closer to 0 are can go to black, values closer to 1 can go to white. Though in retrospect, I suppose the opposite would make more sense. Values closer to 0 would be "more opposite" to white than they would be to black.
Looks right to me.
So not only did I get a glaring gramattical error in my reply, but it also managed to arrive after you fixed it correctly :(
Hah, that's fine -- I've updated the post to have the proper OppositeColor function (note that I did swap the black vs white). I also gave you credit for pointing out the issue and working thru the solution with me. :-)
You're overly generous about my minimal contribution, but thanks anyway :)
This topic comes up on the NUG every once in a while, and there are all manners of crazy solutions for this but the simplest way is to calculate the RGB luminance of the colour and pick black or white based on that - inverting the colour just looks odd IMHO, and as you've found it doesn't always work. Here's a relevant thread: http://support.realsoftware.com/listarchives/realbasic-nug/2005-10/msg00485.html
I think that the name of the function "OppositeColor" is misleading. I would expect this function to provide the exact opposite in regards to HSV, in other words a color other than Black or White. A more precise name for your function would be "HighContrastColor".
Where were you when we were discussing this on the NUG a couple of months back? =)
I needed similar functions myself a few months ago and got some help from people on the NUG.
I also have the problem with need for a "high contrast" opposite color that's neither black nor white. I found that simply changing the .Cue by +.5 would help often, but not always. I'd also need to change the brightness if the original color is already too dark. Not found a satisfying solution yet
(I use it like this - this is for a game: I show colored text on a plainly colored background. The user can choose a set of colors (with brightness) for both the background and the text. Now, I can end up combining any chosen color for BG with any color for the text. But sometimes the user made a bad choice and the combination turns out to be giving a badly readable text. I like to detect such cases and change the text color a little so that it stand out more, while trying to keep the color of it. So, basically, I try to vary its brightness so that usually, if the text is dark red, I might make it bright red in case its on a purple background. Oh well, not really the topic that Aaron started, but if someone has a suggestion...)
@Frank -- that's also a perfectly peachy solution. I threw the "Opposite color" idea out there simply as an exercise to see what we can come up with.
@PhilM -- good point, the name is rather misleading in that regard. As for where I was when this was discussed on the NUG; most likely got lost in the fluff. :-P
If you want a _true_ perceptual opposite color or close color, I'd recommend converting colors to CIE Luv color space for the calculations.