BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Articles Colors in .NET: an In-Depth Guide

Colors in .NET: an In-Depth Guide

Bookmarks

Key Takeaways

  • Understanding how a monitor creates colors and why some look brighter than others
  • Demystifing Hue, Saturation, Brightness, HSB and HSL color spaces
  • .NET Colors class: Bringing order to a wild pallet of colors
  • Dealing with confusing Color Pickers
  • Generating and manipulating your own color

I don't know about you, but I struggled for many years with the limited number of colors available in the .NET Colors class, trying to get matching colors with ColorPickers and understanding the various color models. To simplify my life, I wrote a few small methods which allow me to change any color towards white and black and another one to mix colors. With this, I get nicely matching colors, a bit like gradients as in the GradientBrush.

Then I had the bad idea to write this article to help  others using my methods. To explain how they work, I had no choice but to investigate in detail what is going on. So the biggest part of this article is about colors, color models, hue, brightness and stuff, but in easy terms a software developer can understand without a math or physics degree.

If you already have a solid understanding of colors, you can just jump to the chapter, Generating your own color with precision, where the actual code is.

Color space HSB: Hue, Saturation and Brightness

As we probably all know, color on a computer screen gets created by pixels and each pixel consists of 3 dots which can emit the light Red, Green and Blue, which explains the names R, G and B of these dots. However, here is already the first misunderstanding, because in actual fact, G is not Colors.Green but Colors.Lime. The human eye has three different kinds of receptors for colors and the colors (hue) of R, G and B are chosen to match well with these receptors, which happens to be the lime green and not the "normal" green. With RGB pixels, a monitor can produce most colors a human eye can differentiate. Well, almost. Of course, the "strength" (brightness) of these colors are rather limited when compared to other light sources such the sun.

Each color is defined by how much light each of the 3 dots emits. One dot can have a value between 0 (emitting nothing) and 255 (or 0xFF in hexadecimal) emitting at full strength. To see one of the primary colors, for example red, R gets set to 255 and the G and B to zero, which gives the brightest red possible. If we want a darker red, we just lower the value of R. Once R is 0, the resulting color is black, since no dot is emitting any light. This is an example of changing the brightness of a color, one of the three properties every color has. Interestingly, brightness is not defined from 0 to 255, but from 0 to 1 or 0 to 100%.

Another property of a color is called hue. Hue assigns distinct numbers for colors like yellow, orange, red, etc. We can produce hues at their purest when one of the 3 dots is 255, one is 0 and the "middle" (third) one can have any value. For example, R 225, G 255, B 0 combines red and green and the result is ? Strangely enough, the result is yellow ! The reason is that when 2 lights shine on the same spot, the spot gets brighter not darker. If we mix R, G and B shining at their maximum, we get a white light. This is the opposite of painting colors. If we mix many painting colors together, we get something dark grayish and ugly.

R:FF G:FF B:00 = Yellow

R:00 G:FF B:FF = Cyan

R:FF G:00 B:FF = Magenta

These, by the way, are the brightest colors a monitor can produce, since they use 2 dots fully turned on as opposed to red, green and blue, where only 1 dot is used. For all other hues, the second strongest dot emits at less than 255. It is this "middle" dot, which is not 255 and not 0 which defines the hue. By incrementing this middle dot slowly from 0 to 255, we get all possible hues, for example, between red (FF0000) and yellow (FFFF00). In total, there are 6 such transitions:

  • 255 Red and some Green: Red to Yellow
  • 255 Red and some Blue: Red to Violet
  • 255 Green and some Red: Green to Yellow
  • 255 Green and some Blue: Green to BlueGreen
  • 255 Blue and some Green: Blue to BlueGreen
  • 255 Blue and some Red: Blue to Violet

When we vary R, G or B by small increments, we get something like a rainbow:

The color at the left and right border is red. For this reason, the rainbow is often drawn as a circle. Hue is defined in degrees with red being 0 and 360 degrees.

The third color property is called saturation. So far, we have dealt only with fully saturated colors, i.e., the darkest dot was 0. If we want to make a fully saturated color brighter and bring it finally close to white, we need to lower the saturation by increasing the intensity of all three dots proportionally closer to 255. To decrease the saturation from 100% to 50%, we have to halve the difference between the present value to 255. Example:

Present value: R 255, G 128, B 0

Decrease saturation from 1 to .5

New R value = 255 + 0.5 * (255-255) = 255

New G value = 128 + 0.5 * (255-128) = 192 (or 191 depending on rounding)

New B value = 0 + 0.5 * (255-0) = 128 (or 127 depending on rounding)

Decrease saturation from 1 to 0 (=white)

New R value = 255 + 1 * (255-255) = 255

New G value = 128 + 1 * (255-128) = 255

New B value = 0 + 1 * (255-0) = 255 

The color red drawn from 100% saturation to 0%:

Now we can draw the rainbow again and include some saturation and brightness variation. The x-axis increases the hue from 0 to 360. In the middle is each hue shown with 100% saturation and 100% brightness. In the upper half, the brightness stays 100% and the saturation is decreased to 0%, which results in white. In the lower half, the saturation stays 100% and the brightness is decreased to 0%, which results in black.

Notice how yellow, cyan and magenta can keep their color longer before they turn white or black than the other hues. They are the strongest colors, because 2 dots shine at full intensity.

A hue with close to 0% saturation and 100% brightness looks white. White has the value FFFFFF and the hue and saturation are undefined.

A hue with 100% saturation and close to 0% brightness looks black. Black has the value 000000 and the hue and saturation are undefined.

A color where all 3 dots shine with the same strength looks gray. A possible value could be 808080.

Note: For gray, i.e., R, G and B have the same value, neither hue nor saturation is defined, only brightness has a meaningful value. We could also say, black, gray and white are not colors. Black 000000 has a brightness of 0, White FFFFFF has a brightness of 1. That brightness alone controls how white, gray and black look has a strange consequence as we can see in the next picture.

Have we now covered all colors a monitor can display? Actually, we have shown less than 1% of all possible R, G and B combinations, i.e., only those where one dot is 255 (100% brightness) or one dot is 0 (100% saturation). Let's say we change first the saturation of a color FF8000 (kind of orange red) to 50%, we get FFC080. When we then change the brightness to 50%, we get 806040. The hue is still orange red, but the color is now much closer to a dark gray.

Here is a picture of the color Red with all possible saturation and brightness combinations:

I guess this is the most confusing picture in this article. Basically, I wanted to change the color on the y axis (top to bottom) from red to black, meaning the brightness from 1 to 0, and on the x axis (left to right) from red to white, meaning saturation from 1 to 0. I would have expected that white and black would mix too and that the bottom right corner would be gray. But no such luck. As soon R, G, B have the same value, hue and saturation lose their meaning. Only brightness has an influence on the colors white, gray and black (right border). Even worse, the whole lower border is just black, because once brightness is 0, hue and saturation become meaningless again.

If you feel confused too, welcome to the club. But that's how the HSB color scheme works. When manipulating just RGB values, it is kind of difficult to tell how the result will look like (remember that mixing R and G results in yellow ?). When manipulating colors in the HSB color space, yellow stays yellow, as long you only change saturation and brightness, until the brightness becomes 1 (white) or 0 (black), at which time the hue and saturation is lost.

HSL Color Space

There is another color space called HSL Hue, Saturation and Luminosity. Hue is the same as in HSB, Saturation doesn't go towards white but gray and Luminosity goes from 0=black, 0.5=gray to 1= white. HSL was useful in the transition from black and white to color TVs. A black and white tv just displayed the L value, while a color tv used HSL.

Color Pickers

In the past, I always struggled to understand how the color pickers are supposed to work and why they failed sometimes. Understanding now hue, saturation and brightness and how they are related to RGB colors makes the color picker easier to understand.

PowerPoint 2010 Color Picker

PowerPoint uses the HSL color space. In the color selection area, they display all hues horizontally, vertically they display the saturation. In the HSL color space, a saturation of 0 is gray, therefore the complete lower border is grey. On the right is a scrollbar which changes the luminosity, 0 meaning black, 128 meaning gray and 255 meaning white (it does not use 0-100% but 0-255). At 0 and 255, hue and saturation lose their meaning. The purest color is at luminosity 128.

Choosing for example a blue in HSL, then reducing the luminosity to 0 and switching to RGB display displays properly 0,0,0. Increasing R a bit then setting it again to 0 and switching back to HSV shows now 0 for Hue and Sat, which should be undefined, of course. A hue of 0 means red, but black has no hue. The pointer in the color area still shows the originally chosen blue, instead of red which is hue 0. This was not just confusing for me, but I finally crashed PowerPoint when I kept playing with black, gray and white and switching between RGB and HSL view.

Paint.net 4.2 Color Picker

Working with the color picker from paint.net proved to be easier. They use the HSV color model, which is the same as the HSB color model, they just changed the word brightness to volume. I appreciate that they display the RGB values and the HSV values in the same window, this makes it much easier to understand how changing one HSV value influences the RGB values. It displays all available hues on the border of the color circle and in the middle is white, meaning 0% saturation. To make colors darker, one has to change the slider of the Volume (brightness) parameter. Of course, it also displays a hue of zero for black, gray and white, but at least it didn't crash when I played with these values.

WinUI ColorPicker

The sad fact is that WPF has no color picker. It's like Microsoft gave up on WPF for many years, trying to force us to write UWP applications instead. Many developers thought "no thank you" and stayed with WPF. So now Microsoft is introducing XAML Islands allowing WPF projects using "newer" controls like a color picker, which should have been included in WPF right from the beginning. I haven't used the WinUI ColorPicker in a project, but I run it in the XAML Controls Gallery:

It works a bit like the PowerPoint color picker but using HSV (HSB) instead, meaning the lower part of the color area is white, not gray. The lower scrollbar allows changing the V value (brightness). When set to black, Hue and Saturation stick to their latest values, even when a different color gets chosen in the color area later on. When Value (brightness) gets increased, the circle in the color area jumps back to the old hue. Strange, but at least no crashing.

Web Color Picker

Here is an example of a internet based color picker as defined in HTML 5 input tag:

<input type="color">.

HTML only defines the functionality of the input tag, how the color picker actually looks depends on the browser. Here is a screenshot from Chrome:

It uses a rainbow scrollbar for selecting the hue. The rectangle above displays that hue in all possible saturation and brightness combinations. For a detailed explanation how the values get calculated see the previous explanation about HSB, which has nearly the same picture, except that white and red corners have switched their position. The resulting values can be displayed as RGB (integer or hex) and HSL.

The .NET Colors Class

The Colors class gives some standard colors. They were chosen by committees mixing some different color schemes with sometimes strange results. For example, Colors.Gray is DARKER than Colors.DarkGray. Strange, right?

Or 2 different names represent actually the same color, like Aqua (00FFFF) and Cyan (00FFFF) or Fuchsia (FF00FF) and Magenta (FF00FF). Unfortunately, the Colors help page displays the colors alphabetically, which makes them easy to find if you know the name, but one has great difficulties to tell which colors are close to each other or matching:

So I spent some time and sorted them vertically by their hue, then horizontally by their brightness and their saturation. Here is the result listing exactly the same colors like in the Colors help page:

Generating Your Own Color With Precision

Making Colors Brighter or Darker (Decreasing Saturation and or Brightness)

When I design a new application and decide about the color scheme to use, I usually cannot use the color palette provided by the Colors class. Often, I need the same hue with shades (different saturation and brightness). To do that takes surprisingly few lines of code. Here is the method to decrease the saturation (make it brighter) or decrease the brightness (make it darker) of any color:

/// <summary>
/// Makes the color lighter if factor>0 and darker if factor<0. 1 returns white, -1 returns 
/// black.
/// </summary>
public static Color GetBrighterOrDarker(this Color color, double factor) {
  if (factor<-1) throw new Exception($"Factor {factor} must be greater equal -1.");
  if (factor>1) throw new Exception($"Factor {factor} must be smaller equal 1.");

  if (factor==0) return color;

  if (factor<0) {
    //make color darker, changer brightness
    factor += 1;
    return Color.FromArgb(
      color.A, 
      (byte)(color.R*factor), 
      (byte)(color.G*factor), 
      (byte)(color.B*factor));
  } else {
    //make color lighter, change saturation
    return Color.FromArgb(
      color.A,
      (byte)(color.R + (255-color.R)*factor),
      (byte)(color.G + (255-color.G)*factor),
      (byte)(color.B + (255-color.B)*factor));
  }
}

Amazing with how few lines saturation and brightness can be changed. A bit of a challenge is to get the saturation calculation right, for which this method comes in handy.

Red, Green, Blue with factor -1 to 1:

To get stronger shining colors, do not use green, which is not 100% saturated, but use yellow, magenta and cyan instead:

Note that applying first a factor of 0.5 and then one of -0.5 does not result in the original color. The first call changes the saturation, the second the brightness.

What I like about using this method is:

  1. I can increase, decrease the changes in small, controlled steps and see the results in the GUI.
  2. I can easily create shadows and highlights, which should have the same hue, but different saturation and brightness.

Mixing Hues

Usually, a user interface should not use too many hues, but a few might be fine and some hues mixed from them. These two methods serve this purpose, the first mixing two colors half, half, the second allowing to use more of one color than the other:

/// <summary>
/// Mixes 2 colors equally
/// </summary>
public static Color Mix(this Color color1, Color color2) {
  return Mix(color1, 0.5, color2);
}

/// <summary>
/// Mixes factor*color1 with (1-factor)*color2.
/// </summary>
public static Color Mix(this Color color1, double factor, Color color2) {
  if (factor<0) throw new Exception($"Factor {factor} must be greater equal 0.");
  if (factor>1) throw new Exception($"Factor {factor} must be smaller equal 1.");

  if (factor==0) return color2;
  if (factor==1) return color1;

  var factor1 = 1 - factor;
  return Color.FromArgb(
    (byte)((color1.A * factor + color2.A * factor1)),
    (byte)((color1.R * factor + color2.R * factor1)),
    (byte)((color1.G * factor + color2.G * factor1)),
    (byte)((color1.B * factor + color2.B * factor1)));
}

That is all that is needed to generate nicely matching colors. The first picture shows how each "main" color gets mixed with each other "main" color to various degrees, again for red, green and blue:

However, it might be better if you use yellow, magenta and cyan instead:

Here I really feel the colors are nicer than mixing red, green and blue. Of course, they might be too pure. A GUI often uses grayish hues, which can easily be made after mixing by using GetBrighterOrDarker().

Getting Hue, Saturation and Brightness of a RGB Color

I made a few more methods I needed for writing this article, which might be useful too. The first calculates hue, saturation and brightness of a RGB color:

/// <summary>
/// Returns the hue, saturation and brightness of color
/// </summary>
public static (int Hue, double Saturation, double Brightness)GetHSB(this Color color) {
  int max = Math.Max(color.R, Math.Max(color.G, color.B));
  int min = Math.Min(color.R, Math.Min(color.G, color.B));
  int hue = 0;//for black, gray or white, hue could be actually any number, but usually 0 is 
              //assign, which means red
  if (max-min!=0) {
    //not black, gray or white
    int maxMinDif = max-min;
    if (max==color.R) {
      #pragma warning disable IDE0045 // Convert to conditional expression
      if (color.G>=color.B) {
      #pragma warning restore IDE0045
        hue = 60 * (color.G-color.B)/maxMinDif;
      } else {
        hue = 60 * (color.G-color.B)/maxMinDif + 360;
      }
    } else if (max==color.G) {
      hue = 60 * (color.B-color.R)/maxMinDif + 120;
    } else if(max == color.B) {
      hue = 60 * (color.R-color.G)/maxMinDif + 240;
    }
  }

  double saturation = (max == 0) ? 0.0 : (1.0-((double)min/(double)max));

  return (hue, saturation, (double)max/0xFF);
}

I copied this code from CodeProject: Manipulating colors in .NET Part 1, which actually won "Best C# article of May 2007" and "improved" it slightly. For example, I feel integer 0 to 360 are enough to enumerate all hues. The original code uses floating point numbers and there you can have an unlimited number of hues. This might make the calculation less susceptible to rounding errors, but I guess one can't see the difference.

Increasing Saturation and Brightness to 100% for Any Color

In my approach of choosing matching colors, it helps to start with "pure" colors which have a saturation and brightness of 100%, which then later are used for mixing and making them darker or brighter. The following method takes any RGB color and returns a RGB color with the same hue, but 100% saturation and brightness:

/// <summary>
/// Returns a color with the same hue, but brightness and saturation increased to 100%.
/// </summary>
public static Color ToFullColor(this Color color) {
  //step 1: increase brightness to 100%
  var max = Math.Max(color.R, Math.Max(color.G, color.B));
  var min = Math.Min(color.R, Math.Min(color.G, color.B));
  if (max==min) {
    //for black, gray or white return white
    return Color.FromArgb(color.A, 0xFF, 0xFF, 0xFF);
  }

  double rBright = (double)color.R * 255 / max;
  double gBright = (double)color.G * 255 / max;
  double bBright = (double)color.B * 255 / max;

  //step2: increase saturation to 100%
  //lower smallest R, G, B component to zero and adjust second smallest color accordingly
  //p = (smallest R, G, B component) / 255
  //(255-FullColor.SecondComponent) * p + FullColor.SecondComponent = color.SecondComponent
  //FullColor.SecondComponent = (color.SecondComponent-255p)/(1-p)
  if (color.R==max) {
    if (color.G==min) {
      double p = gBright / 255;
      return Color.FromArgb(color.A, 0xFF, 0, (byte)((bBright-gBright)/(1-p)));
    } else {
      double p = bBright / 255;
      return Color.FromArgb(color.A, 0xFF, (byte)((gBright-bBright)/(1-p)), 0);
    }
  } else if (color.G==max) {
    if (color.R==min) {
      double p = rBright / 255;
      return Color.FromArgb(color.A, 0, 0xFF, (byte)((bBright-rBright)/(1-p)));
    } else {
      double p = bBright / 255;
      return Color.FromArgb(color.A, (byte)((rBright-bBright)/(1-p)), 0xFF, 0);
    }
  } else {
    if (color.R==min) {
      double p = rBright / 255;
      return Color.FromArgb(color.A, 0, (byte)((gBright-rBright)/(1-p)), 0xFF);
    } else {
      double p = bBright / 255;
      return Color.FromArgb(color.A, (byte)((rBright-bBright)/(1-p)), 0, 0xFF);
    }
  }
}

I wrote this method myself. The math here is a little bit more demanding and I hope I got it right. You can easily verify it by using any color picker. Please let me know if you find any discrepancies.


 

About the Author

Rate this Article

Adoption
Style

Hello stranger!

You need to Register an InfoQ account or or login to post comments. But there's so much more behind being registered.

Get the most out of the InfoQ experience.

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Community comments

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

BT