This should be a simple question, but I haven't been able to find a way to make it work.
Essentially, I have a silly localhost page that I use in my webdevelopment. When I am surfing between our development server and my local version of the C# code (redirected from the dev url via host file) I have been known to sometimes forget what 'dev.foo.com' points at - local or server.
So I created a page which will run locally as my default web page's default page, so I can easily identify my localhost from the server.
This page does a lot of things randomly (including generating a character's starting stats for D&D), including setting a random background color. I do this by generating 3 random numbers between 0 and 255, and setting them as the RGB value for the body background color in CSS.
Given the 3 ints R, G, and B, how do I determine R2, G2, and B2 such that the second color will have high contrast with the first? I like having the page have random background colors (it keeps me from getting used to the look of the landing page) but I also like to be able to read the text.
You need a difference in brightness for text to be readable, as color vision itself has too low resolution.
So as an algorithm I'd suggest the following:
Pick a random background color.
Then decide whether it is a light or a dark color. For example you could check whether the average of the three primary colors is greater or equal 128.
For a light color use black text, for a dark one white text.
Update: Here is an example image I made while playing with the split_evenly example of the Rust crate plotters. It shows the colors in Palette99:
"Contrast" is a loaded word. If you just care about being able to read the text, then one easy way is to work in a luminance-based color space like HSL, and pick foreground and background colors with big differences in luminance.
The conversion between HSL and RGB is well-known--see Wikipedia for the details.
If you're talking about actual color contrast, it's not nearly as cut-and-dried (there are a lot of perceptual factors that, as far as I know, haven't been reduced to a single colors space), but I suspect you don't need that level of sophistication.
Check out this PHP solution: Calculating Color Contrast with PHP by Andreas Gohr. It can be ported to any language of course.
He also has a very nice demonstration of his contrast analyzer where you can find some minimal contrast levels to work with.
You can use method GetBrightness() on Color class. It returns a float value from 0.0 (brightness of black) to 1.0 (white).
A simple solution would be:
var color1 = new Color.FromArgb(r1, g1, b1);
var brightness = color1.GetBrightness();
var color2 = brightness > 0.5 ? Color.Black : Color.White;
I did something like this in a Palm OS application. This is what I came up with. It doesn't give you "high contrast" colors but it gives you a background color that's different enough from the text color to be quite readable:
// Black background is a special case. It's fairly likely to occur and
// the default color shift we do isn't very noticeable with very dark colors.
if (backColor.r < 0x20 && backColor.g < 0x20 && backColor.b < 0x20)
{
textColor.r = backColor.r + 0x20;
textColor.g = backColor.g + 0x20;
textColor.b = backColor.b + 0x20;
}
else
{
textColor.r = backColor.r + ((backColor.r < 128) ? 0x10 : -0x10);
textColor.g = backColor.g + ((backColor.g < 128) ? 0x10 : -0x10);
textColor.b = backColor.b + ((backColor.b < 128) ? 0x10 : -0x10);
}
You might not need to do black as a special case for your purposes - Palm's color handling is a bit funky (16-bit color).
These answers are more or less suggesting to use one of the two or three color choices based on whether the color is bright or dark.
I use a bit different approach and it worked elegantly in my case. Here is the implementation.
int color = your_color;
contrastColor = Color.rgb(255-(color >> 16)&0xFF, 255-(color >> 8)&0xFF, 255- color&0xFF);
It's simple and wonderful.
If you flip all the bits, you will get the "opposite" color which would be pretty good contrast.
I believe it's the ~ operator in C#:
R2 = ~R1;
G2 = ~G1;
B2 = ~B1;
Thanks to #starblue !
Here is C# code that I use
public static string GetContrastBlackOrWhiteColorAsHtmlColorCode(Color c)
{
System.Drawing.Color color = System.Drawing.ColorTranslator.FromHtml("transparent");
try
{
if (c.R >= 128 && c.G >= 128 && c.B >= 128)
{
return System.Drawing.ColorTranslator.ToHtml(Color.Black);
}
else
{
return System.Drawing.ColorTranslator.ToHtml(Color.White);
}
}
catch (Exception)
{
}
return System.Drawing.ColorTranslator.ToHtml(color);
}
For best contrast use this code
function lumdiff($R1,$G1,$B1,$R2,$G2,$B2){
$L1 = 0.2126 * pow($R1/255, 2.2) +
0.7152 * pow($G1/255, 2.2) +
0.0722 * pow($B1/255, 2.2);
$L2 = 0.2126 * pow($R2/255, 2.2) +
0.7152 * pow($G2/255, 2.2) +
0.0722 * pow($B2/255, 2.2);
if($L1 > $L2){
return ($L1+0.05) / ($L2+0.05);
}else{
return ($L2+0.05) / ($L1+0.05);
}
}
function get_the_contrast($c1, $c2) {
return (lumdiff(hexdec(substr($c1,0,2)),
hexdec(substr($c1,2,2)),hexdec(substr($c1,4,2)),
hexdec(substr($c2,0,2)),hexdec(substr($c2,2,2)),
hexdec(substr($c2,4,2))));
}
The method above ( AVG(red,green,blue) > 128 ) is not realy good.
private Color GetContrastingColor(Color color)
{
int r = color.R > 0 ? 256 - color.R : 255;
int g = color.G > 0 ? 256 - color.G : 255;
int b = color.B > 0 ? 256 - color.B : 255;
return System.Drawing.Color.FromArgb(r, g, b);
}
Related
Mapbox provides Global elevation data with height data encoded in PNG image. Height is decoded by height = -10000 + ((R * 256 * 256 + G * 256 + B) * 0.1). Details are in https://www.mapbox.com/blog/terrain-rgb/.
I want to import the height data to generate terrains in Unity3D.
Texture2D dem = (Texture2D)AssetDatabase.LoadAssetAtPath("Assets/dem/12/12_3417_1536.png", typeof(Texture2D));
for (int i = 0; i < width; i++)
for (int j = 0; j < height; j++)
{
Color c = dem.GetPixel(i, j);
float R = c.r*255;
float G = c.g*255;
float B = c.b*255;
array[i, j] = -10000 + ((R * 256 * 256 + G * 256 + B) * 0.1f);
}
Here I set a break point and the rgba value of the first pixel is RGBA(0.000, 0.592, 0.718, 1.000). c.r is 0. The height is incorrect as this point represent the height of somewhere on a mountain.
Then I open the image in Photoshop and get RGB of the first pixel: R=1,G=152,B=179.
I write a test program in C#.
System.Drawing.Bitmap bitmap = new System.Drawing.Bitmap("12_3417_1536.png");
Color a = bitmap.GetPixel(0, 0);
It shows Color a is (R,G,B,A)=(1,147,249,255)
Here is the image I test:
https://api.mapbox.com/v4/mapbox.terrain-rgb/12/3417/1536.pngraw?access_token=pk.eyJ1Ijoib2xlb3RpZ2VyIiwiYSI6ImZ2cllZQ3cifQ.2yDE9wUcfO_BLiinccfOKg
Why I got different RGBA value with different method? Which one is correct?
According to the comments below, different read order and compressed data in unity may result in different value of the rgba of pixel at (0,0).
Now I want to focus on----How to convert the rgba(0~1) to RGBA(0~255)?
r_ps=r_unity*255? But how can I explain r=0 in unity and r=1 in PS of pixel at (0,0)
?
Try disabling compression from the texture's import settings in Unity (No compression). Alternatively, if you fetch the data at runtime, you can use Texture.LoadBytes() to avoid compression artifacts.
I will assume you are using the same picture and that there aren't two 12_3417_1536.png files in separate folders.
Each of these functions has a different concept of which pixel is at (0,0). Not sure what you mean by "first" pixel when you tested with photoshop, but Texture coordinates in unity start at lower left corner.
When I tested the lower left corner pixel using paint, I got the same value as you did with photoshop. However, if you test the upper left corner, you get (1,147,249,255) which is the result bitmap.GetPixel returns.
The unity values that you're getting seem to be way off. Try calling dem.GetPixel(0,0) so that you're sure you're analyzing the simplest case.
How do you use DwmGetColorizationColor?
The documentation says it returns two values:
a 32-bit 0xAARRGGBB containing the color used for glass composition
a boolean parameter that is true "if the color is an opaque blend" (whatever that means)
Here's a color that i like, a nice puke green:
You can notice the color is greeny, and the translucent title bar (against a white background) shows the snot color very clearly:
i try to get the color from Windows:
DwmGetColorizationColor(dwCcolorization, bIsOpaqueBlend);
And i get
dwColorization: 0x0D0A0F04
bIsOpaqueBlend: false
According to the documentation this value is of the format AARRGGBB, and so contains:
AA: 0x0D (13)
RR: 0x0A (10)
GG: 0x0F (15)
BB: 0x04 (4)
This supposedly means that the color is (10, 15, 4), with an opacity of ~5.1%.
But if you actually look at this RGB value, it's nowhere near my desired snot green. Here is
(10, 15, 4) with zero opacity (the original color), and
(10,15,4) with 5% opacity against a white/checkerboard background:
Rather than being Lime green, DwmGetColorizationColor returns an almost fully transparent black.
So the question is: How to get glass color in Windows Vista/7?
i tried using DwmGetColorizationColor, but that doesn't work very well.
A person with same problem, but a nicer shiny picture to attract you squirrels:
So, it boils down to –
DwmGetColorizationColor is completely
unusable for applications attempting
to apply the current color onto an
opaque surface.
i love this guy's screenshots much better than mine. Using his screenshots as a template, i made up a few more sparklies:
For the last two screenshots, the alpha blended chip is a true partially transparent PNG, blending to your browser's background. Cool! (i'm such a geek)
Edit 2: Had to arrange them in rainbow color. (i'm such a geek)
Edit 3: Well now i of course have to add Yellow.
Undocumented/Unsupported/Fragile Workarounds
There is an undocumented export from DwmApi.dll at entry point 137, which we'll call DwmGetColorizationParameters:
HRESULT GetColorizationParameters_Undocumented(out DWMCOLORIZATIONPARAMS params);
struct DWMCOLORIZATIONPARAMS
{
public UInt32 ColorizationColor;
public UInt32 ColorizationAfterglow;
public UInt32 ColorizationColorBalance;
public UInt32 ColorizationAfterglowBalance;
public UInt32 ColorizationBlurBalance;
public UInt32 ColorizationGlassReflectionIntensity;
public UInt32 ColorizationOpaqueBlend;
}
We're interested in the first parameter: ColorizationColor.
We can also read the value out of the registry:
HKEY_CURRENT_USER\Software\Microsoft\Windows\DWM
ColorizationColor: REG_DWORD = 0x6614A600
So you pick your poison of creating appcompat issues. You can
rely on an undocumented API (which is bad, bad, bad, and can go away at any time)
use an undocumented registry key (which is also bad, and can go away at any time)
See also
Is there a list of valid parameter combinations for GetThemeColor / Visual Styles API
How does Windows change Aero Glass color?
DWM - Colorization Color Handling Using DWMGetColorizationColor
Retrieving Aero Glass base color for opaque surface rendering
i've been wanting to ask this question for over a year now. i always knew that it's impossible to answer, and that the only way to get anyone to actually pay attention is to have colorful screenshots; developers are attracted to shiny things. But on the downside it means i had to put all kinds of work into making the lures.
Colorization color != the base color chosen. It's misleading, I know.
But I'm confused. The image you borrowed was from my post entitled "Retrieving Aero Glass base color for opaque surface rendering". Is this not what you want to do? I also indicated in the post the registry location in which all the color information is stored (HKEY_CURRENT_USER\Software\Microsoft\Windows\DWM) for retrieval purposes.
Edited 8/26
DwmGetColorizationColor (dwmapi.dll) returns the "colorization color", which is a blend of various colors (incl. your selected base color) and shader logic to achieve the overall glass effect.
All the color information you need/want can be found in the registry key noted above. The base color, the colors used in blending, and the resulting colorization color are all there.
(The key above is present on Windows Vista and above.)
I believe I have solved the Aero Color. The color given by ColorizationColor is in fact AARRGGBB but it is not being used in the way that you think at all. And in order to solve the final color, you also need to get the Color Intensity as shown here: http://www.codeproject.com/Articles/610909/Changing-Windows-Aero-Color
First step is to parse AARRGGBB. Then take the resulting RGB and convert to HSV. The pure Hue plus Saturation at full brightness is the base color. Now overlay Value as a grayscale at Alpha over top of pure Hue and Saturation to get the Aero color. Then overlay that color over the frame color: rgb(235, 235, 235) at Intensity to get the final Composite Aero color result.
Lastly, I've also provided an example of how to extract a useable toolbar color that matches the Aero frame color, but will always work with black text and other basic Aero features. This is accomplished by limiting Intensity to 35%.
Here is the math:
function dwmToRgb() {
// Input Values
var colorizationColor = "a84f1b1b"; // dwmcolor.clrColor = ColorizationColor
var colorizationColorBalance = 60; // dwmcolor.nIntensity = ColorizationColorBalance
var F = 235; // Frame base grayscale color when Transparency is disabled
// Parse the input values
var A = Math.round(parseInt(colorizationColor.substr(0,2),16)/2.55)/100;
var R1 = parseInt(colorizationColor.substr(2,2), 16);
var G1 = parseInt(colorizationColor.substr(4,2), 16);
var B1 = parseInt(colorizationColor.substr(6,2), 16);
var I = colorizationColorBalance/100;
// Solve for HSV Value and pure Hue+Sat
var V = Math.max(R1, G1, B1);
var R2 = R1*255/V;
var G2 = G1*255/V;
var B2 = B1*255/V;
// Aero Frame Pure Hue: Overlay Value # Alpha over pure Hue+Sat
var R3 = Math.round(V+(R2-V)-((R2-V)*A));
var G3 = Math.round(V+(G2-V)-((G2-V)*A));
var B3 = Math.round(V+(B2-V)-((B2-V)*A));
var hexRGB3 = "#" + ((1 << 24) + (R3 << 16) + (G3 << 8) + B3).toString(16).slice(1);
// Aero Frame Composite Color: Overlay RGB3 # Intensity over Frame base color
var R4 = Math.round(R3+(F-R3)-((F-R3)*I));
var G4 = Math.round(G3+(F-G3)-((F-G3)*I));
var B4 = Math.round(B3+(F-B3)-((F-B3)*I));
var hexRGB4 = "#" + ((1 << 24) + (R4 << 16) + (G4 << 8) + B4).toString(16).slice(1);
// Aero Toolbar Color: Overlay RGB3 # max 35% Intensity over Frame base color
if (I > 0.35) { I5 = 0.35;} else { I5 = I;}
var R5 = Math.round(R3+(F-R3)-((F-R3)*I5));
var G5 = Math.round(G3+(F-G3)-((F-G3)*I5));
var B5 = Math.round(B3+(F-B3)-((F-B3)*I5));
var hexRGB5 = "#" + ((1 << 24) + (R5 << 16) + (G5 << 8) + B5).toString(16).slice(1);
How does A0F040 look to you?
OP Edit: This is how 0xA0F040 looks to me:
I'm trying to generate a color that could highlight an item as "selected" based on the color of the current object. I've tried increasing some of the HSB values, but I can't come up with a generalized formula. Particularly, I have problems when working with white (a brighter white doesn't look much different than a regular white). There's no requirement that says I need to make it brighter, so some sort of "inverse" color would work well too. Are there any standard algorithms or techniques for doing something like this (I'm guessing yes, but I couldn't find any -- I'm not sure if there's a name for this)?
thanks,
Jeff
Maybe the negatif effect:
pseudo:
int red = originalColor.red
int green = originalColor.green
int blue = originalColor.blue
int newRed = 255 - red
int newGreen = 255 - green
int newBlue = 255 - blue
Color negativeColor = new Color(newRed, newGreen, newBlue)
Or adding a blue color-effect:
int red = originalColor.red
int green = originalColor.green
int blue = originalColor.blue
int newRed = 255 - red
int newGreen = 255 - green
int newBlue = 255 - blue + 100
if newBlue > 255 {
newBlue = 255
newRed = newRed - 50
newGreen = newGreen - 50
if newRed < 0 {newRed = 0}
if newGreen < 0 {newGreen = 0}
}
Color negativeColor = new Color(newRed, newGreen, newBlue)
If you're using HSB, try shifting the hue by half the maximum value either up or down, that should give you the "opposite" color (also called the complementary color). However, this doesn't do you any good for the grey spectrum, which has no hue and will thus look identical.
If you do this with both hue and brightness, you will get a kind of "negative", which works in all cases. A true negative would have you "flip" the brightness value around the mid-point, but that doesn't work for medium-gray, which would still be medium-gray.
It not always possible to make a color brighter (what do you do with white?), so shifting both hue and brightness by half is the most reliable if you're looking for contrast.
One technique you can use is to swap the item's foreground (text) color and its background color. If the text and background colors of your item already have a pleasing contrast, the selected item should continue to look good.
(source: wordpress.com)
That is the technique used on this site (Stack Overflow) when you mouse-over the tags in your post. They turn from DarkBlue-on-LightBlue to LightBlue-on-DarkBlue. Try it to see the effect.
You might find the tools at http://www.easyrgb.com/ give you some ideas.
Given a system (a website for instance) that lets a user customize the background color for some section but not the font color (to keep number of options to a minimum), is there a way to programmatically determine if a "light" or "dark" font color is necessary?
I'm sure there is some algorithm, but I don't know enough about colors, luminosity, etc to figure it out on my own.
I encountered similar problem. I had to find a good method of selecting contrastive font color to display text labels on colorscales/heatmaps. It had to be universal method and generated color had to be "good looking", which means that simple generating complementary color was not good solution - sometimes it generated strange, very intensive colors that were hard to watch and read.
After long hours of testing and trying to solve this problem, I found out that the best solution is to select white font for "dark" colors, and black font for "bright" colors.
Here's an example of function I am using in C#:
Color ContrastColor(Color color)
{
int d = 0;
// Counting the perceptive luminance - human eye favors green color...
double luminance = (0.299 * color.R + 0.587 * color.G + 0.114 * color.B)/255;
if (luminance > 0.5)
d = 0; // bright colors - black font
else
d = 255; // dark colors - white font
return Color.FromArgb(d, d, d);
}
This was tested for many various colorscales (rainbow, grayscale, heat, ice, and many others) and is the only "universal" method I found out.
Edit
Changed the formula of counting a to "perceptive luminance" - it really looks better! Already implemented it in my software, looks great.
Edit 2
#WebSeed provided a great working example of this algorithm: http://codepen.io/WebSeed/full/pvgqEq/
Based on Gacek's answer but directly returning color constants (additional modifications see below):
public Color ContrastColor(Color iColor)
{
// Calculate the perceptive luminance (aka luma) - human eye favors green color...
double luma = ((0.299 * iColor.R) + (0.587 * iColor.G) + (0.114 * iColor.B)) / 255;
// Return black for bright colors, white for dark colors
return luma > 0.5 ? Color.Black : Color.White;
}
Note: I removed the inversion of the luma value to make bright colors have a higher value, what seems more natural to me and is also the 'default' calculation method.
(Edit: This has since been adopted in the original answer, too)
I used the same constants as Gacek from here since they worked great for me.
You can also implement this as an Extension Method using the following signature:
public static Color ContrastColor(this Color iColor)
You can then easily call it via
foregroundColor = backgroundColor.ContrastColor().
Thank you #Gacek. Here's a version for Android:
#ColorInt
public static int getContrastColor(#ColorInt int color) {
// Counting the perceptive luminance - human eye favors green color...
double a = 1 - (0.299 * Color.red(color) + 0.587 * Color.green(color) + 0.114 * Color.blue(color)) / 255;
int d;
if (a < 0.5) {
d = 0; // bright colors - black font
} else {
d = 255; // dark colors - white font
}
return Color.rgb(d, d, d);
}
And an improved (shorter) version:
#ColorInt
public static int getContrastColor(#ColorInt int color) {
// Counting the perceptive luminance - human eye favors green color...
double a = 1 - (0.299 * Color.red(color) + 0.587 * Color.green(color) + 0.114 * Color.blue(color)) / 255;
return a < 0.5 ? Color.BLACK : Color.WHITE;
}
My Swift implementation of Gacek's answer:
func contrastColor(color: UIColor) -> UIColor {
var d = CGFloat(0)
var r = CGFloat(0)
var g = CGFloat(0)
var b = CGFloat(0)
var a = CGFloat(0)
color.getRed(&r, green: &g, blue: &b, alpha: &a)
// Counting the perceptive luminance - human eye favors green color...
let luminance = 1 - ((0.299 * r) + (0.587 * g) + (0.114 * b))
if luminance < 0.5 {
d = CGFloat(0) // bright colors - black font
} else {
d = CGFloat(1) // dark colors - white font
}
return UIColor( red: d, green: d, blue: d, alpha: a)
}
Javascript [ES2015]
const hexToLuma = (colour) => {
const hex = colour.replace(/#/, '');
const r = parseInt(hex.substr(0, 2), 16);
const g = parseInt(hex.substr(2, 2), 16);
const b = parseInt(hex.substr(4, 2), 16);
return [
0.299 * r,
0.587 * g,
0.114 * b
].reduce((a, b) => a + b) / 255;
};
Ugly Python if you don't feel like writing it :)
'''
Input a string without hash sign of RGB hex digits to compute
complementary contrasting color such as for fonts
'''
def contrasting_text_color(hex_str):
(r, g, b) = (hex_str[:2], hex_str[2:4], hex_str[4:])
return '000' if 1 - (int(r, 16) * 0.299 + int(g, 16) * 0.587 + int(b, 16) * 0.114) / 255 < 0.5 else 'fff'
Thanks for this post.
For whoever might be interested, here's an example of that function in Delphi:
function GetContrastColor(ABGColor: TColor): TColor;
var
ADouble: Double;
R, G, B: Byte;
begin
if ABGColor <= 0 then
begin
Result := clWhite;
Exit; // *** EXIT RIGHT HERE ***
end;
if ABGColor = clWhite then
begin
Result := clBlack;
Exit; // *** EXIT RIGHT HERE ***
end;
// Get RGB from Color
R := GetRValue(ABGColor);
G := GetGValue(ABGColor);
B := GetBValue(ABGColor);
// Counting the perceptive luminance - human eye favors green color...
ADouble := 1 - (0.299 * R + 0.587 * G + 0.114 * B) / 255;
if (ADouble < 0.5) then
Result := clBlack // bright colors - black font
else
Result := clWhite; // dark colors - white font
end;
This is such a helpful answer. Thanks for it!
I'd like to share an SCSS version:
#function is-color-light( $color ) {
// Get the components of the specified color
$red: red( $color );
$green: green( $color );
$blue: blue( $color );
// Compute the perceptive luminance, keeping
// in mind that the human eye favors green.
$l: 1 - ( 0.299 * $red + 0.587 * $green + 0.114 * $blue ) / 255;
#return ( $l < 0.5 );
}
Now figuring out how to use the algorithm to auto-create hover colors for menu links. Light headers get a darker hover, and vice-versa.
Short Answer:
Calculate the luminance (Y) of the given color, and flip the text either black or white based on a pre-determined middle contrast figure. For a typical sRGB display, flip to white when Y < 0.4 (i.e. 40%)
Longer Answer
Not surprisingly, nearly every answer here presents some misunderstanding, and/or is quoting incorrect coefficients. The only answer that is actually close is that of Seirios, though it relies on WCAG 2 contrast which is known to be incorrect itself.
If I say "not surprisingly", it is due in part to the massive amount of misinformation on the internet on this particular subject. The fact this field is still a subject of active research and unsettled science adds to the fun. I come to this conclusion as the result of the last few years of research into a new contrast prediction method for readability.
The field of visual perception is dense and abstract, as well as developing, so it is common for misunderstandings to exist. For instance, HSV and HSL are not even close to perceptually accurate. For that you need a perceptually uniform model such as CIELAB or CIELUV or CIECAM02 etc.
Some misunderstandings have even made their way into standards, such as the contrast part of WCAG 2 (1.4.3), which has been demonstrated as incorrect over much of its range.
First Fix:
The coefficients shown in many answers here are (.299, .587, .114) and are wrong, as they pertain to a long obsolete system known as NTSC YIQ, the analog broadcast system in North America some decades ago. While they may still be used in some YCC encoding specs for backwards compatibility, they should not be used in an sRGB context.
The coefficients for sRGB and Rec.709 (HDTV) are:
Red: 0.2126
Green: 0.7152
Blue: 0.0722
Other color spaces like Rec2020 or AdobeRGB use different coefficients, and it is important to use the correct coefficients for a given color space.
The coefficients can not be applied directly to 8 bit sRGB encoded image or color data. The encoded data must first be linearized, then the coefficients applied to find the luminance (light value) of the given pixel or color.
For sRGB there is a piecewise transform, but as we are only interested in the perceived lightness contrast to find the point to "flip" the text from black to white, we can take a shortcut via the simple gamma method.
Andy's Shortcut to Luminance & Lightness
Divide each sRGB color by 255.0, then raise to the power of 2.2, then multiply by the coefficients and sum them to find estimated luminance.
let Ys = Math.pow(sR/255.0,2.2) * 0.2126 +
Math.pow(sG/255.0,2.2) * 0.7152 +
Math.pow(sB/255.0,2.2) * 0.0722; // Andy's Easy Luminance for sRGB. For Rec709 HDTV change the 2.2 to 2.4
Here, Y is the relative luminance from an sRGB monitor, on a 0.0 to 1.0 scale. This is not relative to perception though, and we need further transforms to fit our human visual perception of the relative lightness, and also of the perceived contrast.
The 40% Flip
But before we get there, if you are only looking for a basic point to flip the text from black to white or vice versa, the cheat is to use the Y we just derived, and make the flip point about Y = 0.40;. so for colors higher than 0.4 Y, make the text black #000 and for colors darker than 0.4 Y, make the text white #fff.
let textColor = (Ys < 0.4) ? "#fff" : "#000"; // Low budget down and dirty text flipper.
Why 40% and not 50%? Our human perception of lightness/darkness and of contrast is not linear. For a self illuminated display, it so happens that 0.4 Y is about middle contrast under most typical conditions.
Yes it varies, and yes this is an over simplification. But if you are flipping text black or white, the simple answer is a useful one.
Perceptual Bonus Round
Predicting the perception of a given color and lightness is still a subject of active research, and not entirely settled science. The L* (Lstar) of CIELAB or LUV has been used to predict perceptual lightness, and even to predict perceived contrast. However, L* works well for surface colors in a very defined/controlled environment, and does not work as well for self illuminated displays.
While this varies depending on not only the display type and calibration, but also your environment and the overall page content, if you take the Y from above, and raise it by around ^0.685 to ^0.75, you'll find that 0.5 is typically the middle point to flip the text from white to black.
let textColor = (Math.pow(Ys,0.75) < 0.5) ? "#fff" : "#000"; // perceptually based text flipper.
Using the exponent 0.685 will make the text color swap on a darker color, and using 0.8 will make the text swap on a lighter color.
Spatial Frequency Double Bonus Round
It is useful to note that contrast is NOT just the distance between two colors. Spatial frequency, in other words font weight and size, are also CRITICAL factors that cannot be ignored.
That said, you may find that when colors are in the midrange, that you'd want to increase the size and or weight of the font.
let textSize = "16px";
let textWeight = "normal";
let Ls = Math.pow(Ys,0.7);
if (Ls > 0.33 && Ls < 0.66) {
textSize = "18px";
textWeight = "bold";
} // scale up fonts for the lower contrast mid luminances.
Hue R U
It's outside the scope of this post to delve deeply, but above we are ignoring hue and chroma. Hue and chroma do have an effect, such as Helmholtz Kohlrausch, and the simpler luminance calculations above do not always predict intensity due to saturated hues.
To predict these more subtle aspects of perception, a complete appearance model is needed. R. Hunt, M. Fairshild, E. Burns are a few authors worth looking into if you want to plummet down the rabbit hole of human visual perception...
For this narrow purpose, we could re-weight the coefficients slightly, knowing that green makes up the majority of of luminance, and pure blue and pure red should always be the darkest of two colors. What tends to happen using the standard coefficients, is middle colors with a lot of blue or red may flip to black at a lower than ideal luminance, and colors with a high green component may do the opposite.
That said, I find this is best addressed by increasing font size and weight in the middle colors.
Putting it all together
So we'll assume you'll send this function a hex string, and it will return a style string that can be sent to a particular HTML element.
Check out the CODEPEN, inspired by the one Seirios did:
CodePen: Fancy Font Flipping
One of the things the Codepen code does is increase the text size for the lower contrast midrange. Here's a sample:
And if you want to play around with some of these concepts, see the SAPC development site at https://www.myndex.com/SAPC/ clicking on "research mode" provides interactive experiments to demonstrate these concepts.
Terms of enlightenment
Luminance: Y (relative) or L (absolute cd/m2) a spectrally weighted but otherwise linear measure of light. Not to be confused with "Luminosity".
Luminosity: light over time, useful in astronomy.
Lightness: L* (Lstar) perceptual lightness as defined by the CIE. Some models have a related lightness J*.
I had the same problem but i had to develop it in PHP. I used #Garek's solution and i also used this answer:
Convert hex color to RGB values in PHP to convert HEX color code to RGB.
So i'm sharing it.
I wanted to use this function with given Background HEX color, but not always starting from '#'.
//So it can be used like this way:
$color = calculateColor('#804040');
echo $color;
//or even this way:
$color = calculateColor('D79C44');
echo '<br/>'.$color;
function calculateColor($bgColor){
//ensure that the color code will not have # in the beginning
$bgColor = str_replace('#','',$bgColor);
//now just add it
$hex = '#'.$bgColor;
list($r, $g, $b) = sscanf($hex, "#%02x%02x%02x");
$color = 1 - ( 0.299 * $r + 0.587 * $g + 0.114 * $b)/255;
if ($color < 0.5)
$color = '#000000'; // bright colors - black font
else
$color = '#ffffff'; // dark colors - white font
return $color;
}
Flutter implementation
Color contrastColor(Color color) {
if (color == Colors.transparent || color.alpha < 50) {
return Colors.black;
}
double luminance = (0.299 * color.red + 0.587 * color.green + 0.114 * color.blue) / 255;
return luminance > 0.5 ? Colors.black : Colors.white;
}
Based on Gacek's answer, and after analyzing #WebSeed's example with the WAVE browser extension, I've come up with the following version that chooses black or white text based on contrast ratio (as defined in W3C's Web Content Accessibility Guidelines (WCAG) 2.1), instead of luminance.
This is the code (in javascript):
// As defined in WCAG 2.1
var relativeLuminance = function (R8bit, G8bit, B8bit) {
var RsRGB = R8bit / 255.0;
var GsRGB = G8bit / 255.0;
var BsRGB = B8bit / 255.0;
var R = (RsRGB <= 0.03928) ? RsRGB / 12.92 : Math.pow((RsRGB + 0.055) / 1.055, 2.4);
var G = (GsRGB <= 0.03928) ? GsRGB / 12.92 : Math.pow((GsRGB + 0.055) / 1.055, 2.4);
var B = (BsRGB <= 0.03928) ? BsRGB / 12.92 : Math.pow((BsRGB + 0.055) / 1.055, 2.4);
return 0.2126 * R + 0.7152 * G + 0.0722 * B;
};
var blackContrast = function(r, g, b) {
var L = relativeLuminance(r, g, b);
return (L + 0.05) / 0.05;
};
var whiteContrast = function(r, g, b) {
var L = relativeLuminance(r, g, b);
return 1.05 / (L + 0.05);
};
// If both options satisfy AAA criterion (at least 7:1 contrast), use preference
// else, use higher contrast (white breaks tie)
var chooseFGcolor = function(r, g, b, prefer = 'white') {
var Cb = blackContrast(r, g, b);
var Cw = whiteContrast(r, g, b);
if(Cb >= 7.0 && Cw >= 7.0) return prefer;
else return (Cb > Cw) ? 'black' : 'white';
};
A working example may be found in my fork of #WebSeed's codepen, which produces zero low contrast errors in WAVE.
As Kotlin / Android extension:
fun Int.getContrastColor(): Int {
// Counting the perceptive luminance - human eye favors green color...
val a = 1 - (0.299 * Color.red(this) + 0.587 * Color.green(this) + 0.114 * Color.blue(this)) / 255
return if (a < 0.5) Color.BLACK else Color.WHITE
}
An implementation for objective-c
+ (UIColor*) getContrastColor:(UIColor*) color {
CGFloat red, green, blue, alpha;
[color getRed:&red green:&green blue:&blue alpha:&alpha];
double a = ( 0.299 * red + 0.587 * green + 0.114 * blue);
return (a > 0.5) ? [[UIColor alloc]initWithRed:0 green:0 blue:0 alpha:1] : [[UIColor alloc]initWithRed:255 green:255 blue:255 alpha:1];
}
iOS Swift 3.0 (UIColor extension):
func isLight() -> Bool
{
if let components = self.cgColor.components, let firstComponentValue = components[0], let secondComponentValue = components[1], let thirdComponentValue = components[2] {
let firstComponent = (firstComponentValue * 299)
let secondComponent = (secondComponentValue * 587)
let thirdComponent = (thirdComponentValue * 114)
let brightness = (firstComponent + secondComponent + thirdComponent) / 1000
if brightness < 0.5
{
return false
}else{
return true
}
}
print("Unable to grab components and determine brightness")
return nil
}
Swift 4 Example:
extension UIColor {
var isLight: Bool {
let components = cgColor.components
let firstComponent = ((components?[0]) ?? 0) * 299
let secondComponent = ((components?[1]) ?? 0) * 587
let thirdComponent = ((components?[2]) ?? 0) * 114
let brightness = (firstComponent + secondComponent + thirdComponent) / 1000
return !(brightness < 0.6)
}
}
UPDATE - Found that 0.6 was a better test bed for the query
Note there is an algorithm for this in the google closure library that references a w3c recommendation: http://www.w3.org/TR/AERT#color-contrast. However, in this API you provide a list of suggested colors as a starting point.
/**
* Find the "best" (highest-contrast) of the suggested colors for the prime
* color. Uses W3C formula for judging readability and visual accessibility:
* http://www.w3.org/TR/AERT#color-contrast
* #param {goog.color.Rgb} prime Color represented as a rgb array.
* #param {Array<goog.color.Rgb>} suggestions Array of colors,
* each representing a rgb array.
* #return {!goog.color.Rgb} Highest-contrast color represented by an array.
*/
goog.color.highContrast = function(prime, suggestions) {
var suggestionsWithDiff = [];
for (var i = 0; i < suggestions.length; i++) {
suggestionsWithDiff.push({
color: suggestions[i],
diff: goog.color.yiqBrightnessDiff_(suggestions[i], prime) +
goog.color.colorDiff_(suggestions[i], prime)
});
}
suggestionsWithDiff.sort(function(a, b) { return b.diff - a.diff; });
return suggestionsWithDiff[0].color;
};
/**
* Calculate brightness of a color according to YIQ formula (brightness is Y).
* More info on YIQ here: http://en.wikipedia.org/wiki/YIQ. Helper method for
* goog.color.highContrast()
* #param {goog.color.Rgb} rgb Color represented by a rgb array.
* #return {number} brightness (Y).
* #private
*/
goog.color.yiqBrightness_ = function(rgb) {
return Math.round((rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 1000);
};
/**
* Calculate difference in brightness of two colors. Helper method for
* goog.color.highContrast()
* #param {goog.color.Rgb} rgb1 Color represented by a rgb array.
* #param {goog.color.Rgb} rgb2 Color represented by a rgb array.
* #return {number} Brightness difference.
* #private
*/
goog.color.yiqBrightnessDiff_ = function(rgb1, rgb2) {
return Math.abs(
goog.color.yiqBrightness_(rgb1) - goog.color.yiqBrightness_(rgb2));
};
/**
* Calculate color difference between two colors. Helper method for
* goog.color.highContrast()
* #param {goog.color.Rgb} rgb1 Color represented by a rgb array.
* #param {goog.color.Rgb} rgb2 Color represented by a rgb array.
* #return {number} Color difference.
* #private
*/
goog.color.colorDiff_ = function(rgb1, rgb2) {
return Math.abs(rgb1[0] - rgb2[0]) + Math.abs(rgb1[1] - rgb2[1]) +
Math.abs(rgb1[2] - rgb2[2]);
};
base R version of #Gacek's answer to get luminance (you can apply your own threshold easily)
# vectorized
luminance = function(col) c(c(.299, .587, .114) %*% col2rgb(col)/255)
Usage:
luminance(c('black', 'white', '#236FAB', 'darkred', '#01F11F'))
# [1] 0.0000000 1.0000000 0.3730039 0.1629843 0.5698039
If you're manipulating color spaces for visual effect it's generally easier to work in HSL (Hue, Saturation and Lightness) than RGB. Moving colours in RGB to give naturally pleasing effects tends to be quite conceptually difficult, whereas converting into HSL, manipulating there, then converting back out again is more intuitive in concept and invariably gives better looking results.
Wikipedia has a good introduction to HSL and the closely related HSV. And there's free code around the net to do the conversion (for example here is a javascript implementation)
What precise transformation you use is a matter of taste, but personally I'd have thought reversing the Hue and Lightness components would be certain to generate a good high contrast colour as a first approximation, but you can easily go for more subtle effects.
You can have any hue text on any hue background and ensure that it is legible. I do it all the time. There's a formula for this in Javascript on Readable Text in Colour – STW*
As it says on that link, the formula is a variation on the inverse-gamma adjustment calculation, though a bit more manageable IMHO.
The menus on the right-hand side of that link and its associated pages use randomly-generated colours for text and background, always legible. So yes, clearly it can be done, no problem.
An Android variation that captures the alpha as well.
(thanks #thomas-vos)
/**
* Returns a colour best suited to contrast with the input colour.
*
* #param colour
* #return
*/
#ColorInt
public static int contrastingColour(#ColorInt int colour) {
// XXX https://stackoverflow.com/questions/1855884/determine-font-color-based-on-background-color
// Counting the perceptive luminance - human eye favors green color...
double a = 1 - (0.299 * Color.red(colour) + 0.587 * Color.green(colour) + 0.114 * Color.blue(colour)) / 255;
int alpha = Color.alpha(colour);
int d = 0; // bright colours - black font;
if (a >= 0.5) {
d = 255; // dark colours - white font
}
return Color.argb(alpha, d, d, d);
}
I would have commented on the answer by #MichaelChirico but I don't have enough reputation. So, here's an example in R with returning the colours:
get_text_colour <- function(
background_colour,
light_text_colour = 'white',
dark_text_colour = 'black',
threshold = 0.5
) {
background_luminance <- c(
c( .299, .587, .114 ) %*% col2rgb( background_colour ) / 255
)
return(
ifelse(
background_luminance < threshold,
light_text_colour,
dark_text_colour
)
)
}
> get_text_colour( background_colour = 'blue' )
[1] "white"
> get_text_colour( background_colour = c( 'blue', 'yellow', 'pink' ) )
[1] "white" "black" "black"
> get_text_colour( background_colour = c('black', 'white', '#236FAB', 'darkred', '#01F11F') )
[1] "white" "black" "white" "white" "black"
Motivation
I'd like to find a way to take an arbitrary color and lighten it a few shades, so that I can programatically create a nice gradient from the one color to a lighter version. The gradient will be used as a background in a UI.
Possibility 1
Obviously I can just split out the RGB values and increase them individually by a certain amount. Is this actually what I want?
Possibility 2
My second thought was to convert the RGB to HSV/HSB/HSL (Hue, Saturation, Value/Brightness/Lightness), increase the brightness a bit, decrease the saturation a bit, and then convert it back to RGB. Will this have the desired effect in general?
As Wedge said, you want to multiply to make things brighter, but that only works until one of the colors becomes saturated (i.e. hits 255 or greater). At that point, you can just clamp the values to 255, but you'll be subtly changing the hue as you get lighter. To keep the hue, you want to maintain the ratio of (middle-lowest)/(highest-lowest).
Here are two functions in Python. The first implements the naive approach which just clamps the RGB values to 255 if they go over. The second redistributes the excess values to keep the hue intact.
def clamp_rgb(r, g, b):
return min(255, int(r)), min(255, int(g)), min(255, int(b))
def redistribute_rgb(r, g, b):
threshold = 255.999
m = max(r, g, b)
if m <= threshold:
return int(r), int(g), int(b)
total = r + g + b
if total >= 3 * threshold:
return int(threshold), int(threshold), int(threshold)
x = (3 * threshold - total) / (3 * m - total)
gray = threshold - x * m
return int(gray + x * r), int(gray + x * g), int(gray + x * b)
I created a gradient starting with the RGB value (224,128,0) and multiplying it by 1.0, 1.1, 1.2, etc. up to 2.0. The upper half is the result using clamp_rgb and the bottom half is the result with redistribute_rgb. I think it's easy to see that redistributing the overflows gives a much better result, without having to leave the RGB color space.
For comparison, here's the same gradient in the HLS and HSV color spaces, as implemented by Python's colorsys module. Only the L component was modified, and clamping was performed on the resulting RGB values. The results are similar, but require color space conversions for every pixel.
I would go for the second option. Generally speaking the RGB space is not really good for doing color manipulation (creating transition from one color to an other, lightening / darkening a color, etc). Below are two sites I've found with a quick search to convert from/to RGB to/from HSL:
from the "Fundamentals of Computer Graphics"
some sourcecode in C# - should be easy to adapt to other programming languages.
In C#:
public static Color Lighten(Color inColor, double inAmount)
{
return Color.FromArgb(
inColor.A,
(int) Math.Min(255, inColor.R + 255 * inAmount),
(int) Math.Min(255, inColor.G + 255 * inAmount),
(int) Math.Min(255, inColor.B + 255 * inAmount) );
}
I've used this all over the place.
ControlPaint class in System.Windows.Forms namespace has static methods Light and Dark:
public static Color Dark(Color baseColor, float percOfDarkDark);
These methods use private implementation of HLSColor. I wish this struct was public and in System.Drawing.
Alternatively, you can use GetHue, GetSaturation, GetBrightness on Color struct to get HSB components. Unfortunately, I didn't find the reverse conversion.
Convert it to RGB and linearly interpolate between the original color and the target color (often white). So, if you want 16 shades between two colors, you do:
for(i = 0; i < 16; i++)
{
colors[i].R = start.R + (i * (end.R - start.R)) / 15;
colors[i].G = start.G + (i * (end.G - start.G)) / 15;
colors[i].B = start.B + (i * (end.B - start.B)) / 15;
}
In order to get a lighter or a darker version of a given color you should modify its brightness. You can do this easily even without converting your color to HSL or HSB color. For example to make a color lighter you can use the following code:
float correctionFactor = 0.5f;
float red = (255 - color.R) * correctionFactor + color.R;
float green = (255 - color.G) * correctionFactor + color.G;
float blue = (255 - color.B) * correctionFactor + color.B;
Color lighterColor = Color.FromArgb(color.A, (int)red, (int)green, (int)blue);
If you need more details, read the full story on my blog.
Converting to HS(LVB), increasing the brightness and then converting back to RGB is the only way to reliably lighten the colour without effecting the hue and saturation values (ie to only lighten the colour without changing it in any other way).
A very similar question, with useful answers, was asked previously:
How do I determine darker or lighter color variant of a given color?
Short answer: multiply the RGB values by a constant if you just need "good enough", translate to HSV if you require accuracy.
I used Andrew's answer and Mark's answer to make this (as of 1/2013 no range input for ff).
function calcLightness(l, r, g, b) {
var tmp_r = r;
var tmp_g = g;
var tmp_b = b;
tmp_r = (255 - r) * l + r;
tmp_g = (255 - g) * l + g;
tmp_b = (255 - b) * l + b;
if (tmp_r > 255 || tmp_g > 255 || tmp_b > 255)
return { r: r, g: g, b: b };
else
return { r:parseInt(tmp_r), g:parseInt(tmp_g), b:parseInt(tmp_b) }
}
I've done this both ways -- you get much better results with Possibility 2.
Any simple algorithm you construct for Possibility 1 will probably work well only for a limited range of starting saturations.
You would want to look into Poss 1 if (1) you can restrict the colors and brightnesses used, and (2) you are performing the calculation a lot in a rendering.
Generating the background for a UI won't need very many shading calculations, so I suggest Poss 2.
-Al.
IF you want to produce a gradient fade-out, I would suggest the following optimization: Rather than doing RGB->HSB->RGB for each individual color you should only calculate the target color. Once you know the target RGB, you can simply calculate the intermediate values in RGB space without having to convert back and forth. Whether you calculate a linear transition of use some sort of curve is up to you.
Method 1: Convert RGB to HSL, adjust HSL, convert back to RGB.
Method 2: Lerp the RGB colour values - http://en.wikipedia.org/wiki/Lerp_(computing)
See my answer to this similar question for a C# implementation of method 2.
Pretend that you alpha blended to white:
oneMinus = 1.0 - amount
r = amount + oneMinus * r
g = amount + oneMinus * g
b = amount + oneMinus * b
where amount is from 0 to 1, with 0 returning the original color and 1 returning white.
You might want to blend with whatever the background color is if you are lightening to display something disabled:
oneMinus = 1.0 - amount
r = amount * dest_r + oneMinus * r
g = amount * dest_g + oneMinus * g
b = amount * dest_b + oneMinus * b
where (dest_r, dest_g, dest_b) is the color being blended to and amount is from 0 to 1, with zero returning (r, g, b) and 1 returning (dest.r, dest.g, dest.b)
I didn't find this question until after it became a related question to my original question.
However, using insight from these great answers. I pieced together a nice two-liner function for this:
Programmatically Lighten or Darken a hex color (or rgb, and blend colors)
Its a version of method 1. But with over saturation taken into account. Like Keith said in his answer above; use Lerp to seemly solve the same problem Mark mentioned, but without redistribution. The results of shadeColor2 should be much closer to doing it the right way with HSL, but without the overhead.
A bit late to the party, but if you use javascript or nodejs, you can use tinycolor library, and manipulate the color the way you want:
tinycolor("red").lighten().desaturate().toHexString() // "#f53d3d"
I would have tried number #1 first, but #2 sounds pretty good. Try doing it yourself and see if you're satisfied with the results, it sounds like it'll take you maybe 10 minutes to whip up a test.
Technically, I don't think either is correct, but I believe you want a variant of option #2. The problem being that taken RGB 990000 and "lightening" it would really just add onto the Red channel (Value, Brightness, Lightness) until you got to FF. After that (solid red), it would be taking down the saturation to go all the way to solid white.
The conversions get annoying, especially since you can't go direct to and from RGB and Lab, but I think you really want to separate the chrominance and luminence values, and just modify the luminence to really achieve what you want.
Here's an example of lightening an RGB colour in Python:
def lighten(hex, amount):
""" Lighten an RGB color by an amount (between 0 and 1),
e.g. lighten('#4290e5', .5) = #C1FFFF
"""
hex = hex.replace('#','')
red = min(255, int(hex[0:2], 16) + 255 * amount)
green = min(255, int(hex[2:4], 16) + 255 * amount)
blue = min(255, int(hex[4:6], 16) + 255 * amount)
return "#%X%X%X" % (int(red), int(green), int(blue))
This is based on Mark Ransom's answer.
Where the clampRGB function tries to maintain the hue, it however miscalculates the scaling to keep the same luminance. This is because the calculation directly uses sRGB values which are not linear.
Here's a Java version that does the same as clampRGB (although with values ranging from 0 to 1) that maintains luminance as well:
private static Color convertToDesiredLuminance(Color input, double desiredLuminance) {
if(desiredLuminance > 1.0) {
return Color.WHITE;
}
if(desiredLuminance < 0.0) {
return Color.BLACK;
}
double ratio = desiredLuminance / luminance(input);
double r = Double.isInfinite(ratio) ? desiredLuminance : toLinear(input.getRed()) * ratio;
double g = Double.isInfinite(ratio) ? desiredLuminance : toLinear(input.getGreen()) * ratio;
double b = Double.isInfinite(ratio) ? desiredLuminance : toLinear(input.getBlue()) * ratio;
if(r > 1.0 || g > 1.0 || b > 1.0) { // anything outside range?
double br = Math.min(r, 1.0); // base values
double bg = Math.min(g, 1.0);
double bb = Math.min(b, 1.0);
double rr = 1.0 - br; // ratios between RGB components to maintain
double rg = 1.0 - bg;
double rb = 1.0 - bb;
double x = (desiredLuminance - luminance(br, bg, bb)) / luminance(rr, rg, rb);
r = 0.0001 * Math.round(10000.0 * (br + rr * x));
g = 0.0001 * Math.round(10000.0 * (bg + rg * x));
b = 0.0001 * Math.round(10000.0 * (bb + rb * x));
}
return Color.color(toGamma(r), toGamma(g), toGamma(b));
}
And supporting functions:
private static double toLinear(double v) { // inverse is #toGamma
return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
}
private static double toGamma(double v) { // inverse is #toLinear
return v <= 0.0031308 ? v * 12.92 : 1.055 * Math.pow(v, 1.0 / 2.4) - 0.055;
}
private static double luminance(Color c) {
return luminance(toLinear(c.getRed()), toLinear(c.getGreen()), toLinear(c.getBlue()));
}
private static double luminance(double r, double g, double b) {
return r * 0.2126 + g * 0.7152 + b * 0.0722;
}