Reputation: 199
I have a custom HsbColor
class instance. It's exactly what it sounds like, a class that describes a color in terms of hue, saturation, brightness. I also have access to the corresponding java.awt.Color
object.
We dynamically compute background color of table cells. That means we have to compute foreground colors as well.
How do we determine whether black or white would be a better match? I would prefer something without complex computations (if it's available through some library, it would be nice). We tried comparing the result of brightness * (saturation + 1)
with 0.5 (assuming brightness
and saturation
are floating-point figures), but it seems it doesn't work great with yellow (I get white which doesn't provide much contrast).
Java 8, Swing.
Upvotes: 0
Views: 152
Reputation: 150
Is there a reason you can't use the WCAG contrast ratio definition?
L1 = lighter colour relative luminance
L2 = darker colour relative luminance
contrast ratio = (L1 + 0.05) / (L2 + 0.05)
Assuming your colours are within sRGB, you can calculate the relative luminance from RGB values.
relative luminance = 0.2126 * Linear(R) + 0.7152 * Linear(G) + 0.0722 * Linear(B)
Linear(value) => value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ^ 2.4
You could check which foreground colour has a greater contrast with the background colour, and use that.
(It's the approach I use for the RGB text at the bottom of my colour picker)
Note that HSB's Brightness and HSL's Lightness don't represent what the eye perceives, they are just a reshaping of RGB.
Upvotes: 1
Reputation: 15936
We dynamically compute background color of table cells.
That means we have to compute foreground colors as well.
How do we determine whether black or white text would be a better match?
I would prefer something without complex computations.
A simple logic to decide "black or white" without complex computations is to just isolate the colour wheel into light and dark areas, and then for contrast simply use the appropriate "opposite" luminosity.
If you cover one eye then relax (or "blur") your vision while looking at the colour wheel below, it will become obvious which hues are in the darks and which are in the lights. No math needed, just natural biology.
The Java code to achieve same result could be something like this:
String color_hint = "";
int hue = 60; //# test degree: Yellow
if( (hue <= 45 || hue >= 205) ) { color_hint = "needs white text "; }
else { color_hint = "needs black text"; }
System.out.println("cell BG hue: " + hue + " degrees");
System.out.println("colour hint: " + color_hint);
You could also try using Luminosity.
Note that the highest "darks" is Magenta @ lum: 105.315
, the lowest "lights" is Green @ lum: 149.65
. There is a distance of 44 units between them, this is orange (high) and teal (low). They might become akward for you since these ones look okay with either black or white text. You will have to test and decide if, for your app's logic, it should be having a black or white foreground (eg: white foreground text).
A starting point in code:
public class Main
{
public static double lum = 0;
public static int hue = 0;
public static int red = 0, grn = 0, blu = 0;
public static String color_hint = "";
public static void main( String[] args )
{
//### Option 1: Checking via degree of Hue (H)...
hue = 60; //# testing hue of 60 degrees (yellow)
if( (hue <= 45 || hue >= 205) ) { color_hint = "Hue needs white text "; }
else { color_hint = "Hue needs black text"; }
//### Option 2: Checking via R+G+B Luminosity (Y)...
red = 0; grn = 255; blu = 0;
lum = get_luma( red , grn , blu );
//lum = ( (lum + 2.2) + 16 ); //# not needed here, but it might help your decision
System.out.println("cell BG hue: " + hue + " degrees");
System.out.println("colour hint: " + color_hint);
System.out.println("cell BG RGB: " + red +","+ grn +","+ blu);
System.out.println("luminosity backgrnd: " + lum);
System.out.println("======================================");
}
public static double get_luma( int in_red, int in_grn , int in_blu )
{
return ((in_red * 0.299) + (in_grn * 0.587) + (in_blu * 0.114));
}
}
Upvotes: 1
Reputation: 44414
This was solved 30+ years ago by X/Motif (constants are in ColorP.h and Xm.h):
/* Contributions of each primary to overall luminosity, sum to 1.0 */
#define XmRED_LUMINOSITY 0.30
#define XmGREEN_LUMINOSITY 0.59
#define XmBLUE_LUMINOSITY 0.11
/* Percent effect of intensity, light, and luminosity & on brightness,
sum to 100 */
#define XmINTENSITY_FACTOR 75
#define XmLIGHT_FACTOR 0
#define XmLUMINOSITY_FACTOR 25
static int
Brightness(
XColor *color )
{
int brightness;
int intensity;
int light;
int luminosity, maxprimary, minprimary;
int red = color->red;
int green = color->green;
int blue = color->blue;
intensity = (red + green + blue) / 3;
/*
* The casting nonsense below is to try to control the point at
* the truncation occurs.
*/
luminosity = (int) ((XmRED_LUMINOSITY * (float) red)
+ (XmGREEN_LUMINOSITY * (float) green)
+ (XmBLUE_LUMINOSITY * (float) blue));
maxprimary = ( (red > green) ?
( (red > blue) ? red : blue ) :
( (green > blue) ? green : blue ) );
minprimary = ( (red < green) ?
( (red < blue) ? red : blue ) :
( (green < blue) ? green : blue ) );
light = (minprimary + maxprimary) / 2;
brightness = ( (intensity * XmINTENSITY_FACTOR) +
(light * XmLIGHT_FACTOR) +
(luminosity * XmLUMINOSITY_FACTOR) ) / 100;
return(brightness);
}
And it gets used thus:
#define XmMAX_SHORT 65535
#define XmCOLOR_PERCENTILE (XmMAX_SHORT / 100)
#define XmDEFAULT_FOREGROUND_THRESHOLD 70
static int XmFOREGROUND_THRESHOLD;
/* ... */
int default_foreground_threshold_spec;
default_foreground_threshold_spec = screen.foregroundThreshold;
if ((default_foreground_threshold_spec <= 0) ||
(default_foreground_threshold_spec > 100))
default_foreground_threshold_spec = XmDEFAULT_FOREGROUND_THRESHOLD;
XmFOREGROUND_THRESHOLD = default_foreground_threshold_spec * XmCOLOR_PERCENTILE;
/* ... */
int brightness = Brightness(bg_color);
if (brightness > XmFOREGROUND_THRESHOLD)
{
fg_color->red = 0;
fg_color->green = 0;
fg_color->blue = 0;
}
else
{
fg_color->red = XmMAX_SHORT;
fg_color->green = XmMAX_SHORT;
fg_color->blue = XmMAX_SHORT;
}
As far as I know, XmDEFAULT_FOREGROUND_THRESHOLD
is always used, so default_foreground_threshold_spec
is always 70.
Translating that into Java, we get:
/* Contributions of each primary to overall luminosity, sum to 1.0 */
private static final double RED_LUMINOSITY = 0.30;
private static final double GREEN_LUMINOSITY = 0.59;
private static final double BLUE_LUMINOSITY = 0.11;
/* Percent effect of intensity, light, and luminosity & on brightness,
sum to 100 */
private static final int INTENSITY_FACTOR = 75;
private static final int LIGHT_FACTOR = 0;
private static final int LUMINOSITY_FACTOR = 25;
private static int brightness(Color color) {
int red = color.getRed();
int green = color.getGreen();
int blue = color.getBlue();
int intensity = (red + green + blue) / 3;
int luminosity = (int) ((RED_LUMINOSITY * red)
+ (GREEN_LUMINOSITY * green)
+ (BLUE_LUMINOSITY * blue));
int maxprimary = IntStream.of(red, green, blue).max().getAsInt();
int minprimary = IntStream.of(red, green, blue).min().getAsInt();
int light = (minprimary + maxprimary) / 2;
int brightness = ((intensity * INTENSITY_FACTOR) +
(light * LIGHT_FACTOR) +
(luminosity * LUMINOSITY_FACTOR)) / 100;
return brightness;
}
private static final int DEFAULT_FOREGROUND_THRESHOLD = 70;
public static Color foregroundColorFor(Color bgColor) {
int foregroundThreshold = DEFAULT_FOREGROUND_THRESHOLD * 255 / 100;
int brightness = brightness(bgColor);
if (brightness > foregroundThreshold) {
return Color.BLACK;
} else {
return Color.WHITE;
}
}
(There are some small differences, because in X, a color component is 0–65535, unlike in Java where a component is either 0–255 or a float.)
Upvotes: 1