I want to digress a bit from the controls we have been drawing and measuring to talk a bit about color. In some of the examples we have been building, we have been trying to make it possible to set the color just one time rather than explicitly specifying background colors and highlight colors. Consequently, we have been doing our highlights using white, which ensures that they work equally well with any color the consuming application selects. Unless, of course, the consuming application select white, in which case it's not so great.

In the last control we were building, we were explicitly hard coding two colors, and using a radial gradient fill using these two colors. Now, we are putting the impetus on the developer of the consuming application to select two colors that look good together, rather than a single one - and both of these must look good with white as the highlight color. We are making the job more and more complex for the developer consuming our controls.

It would be easier if we could programatically discover related colors, wouldn't it? That way, the developer of the consuming application could input a single color, and we could intuit related colors and use them. One approach is to use the HSB color space - Hue, Saturation, and Brightness. Using this approach, we can start with one color, and then vary these properties, which seem more intuitive. What does it mean to add 20 to the blue value of a given color? We have to think about that one for a bit. What does it mean to add 20 to the brightness of some color? We can picture that much more easily.

Starting with the same Hue, we could vary the brightness - creating a highlight that fits with the starting color. If we have a more advanced understanding of complementary colors, we could discover a complementary hue around the 360 degree Hue value space. The space is more easy to navigate. Plus, if you have used the Color Picker dialog, you may have already noticed that HSB (here referred to as Hue, Saturation, Lumens) is something that is widely used throughout Windows applications.

Color Picker dialog box

We used a special case of this scenario while creating Gel Buttons earlier, converting to grayscale by applying the brightness value to the R, G, and B channels. Now, we just need to finish our work to perform this conversion in the general case.

Of course, as good developers, our first instinct is to let somebody else do that work, so we can focus our energy on solving unique problems that do not yet have a known solution. Searching around a bit, you can find that somebody has a class submitted to The Code Project to do exactly that: Use both RGB and HSB color schemas in your .NET application using HSBColor class. How convenient! Unfortunately, if you use the algorithm provided by this class, you will notice that if you take an existing Color structure, retrive the HSB values using GetHue(), GetSaturation(), and GetBrightness(), the Color that is returned is not the color that you began with. That's certainly not a good thing.

Poking around a bit more, I discovered another site called Color Conversion Algorithms at the Rochester Institute of Technology. However, the algorithm it presents had similar problems - the HSB model it used apparently differs from the HSB model used by the .NET Framework.

Since I think it is important to not overload the terms Hue, Saturation, and Brightness within a platform, it appeared as if I was going to need to create the implementation on my own. While I could have simply provided both the mechanism to convert both to and from HSB from the standard RGB values, ensuring the symmetry of the altorighms uses, I want somebody to be able to pass in the results of a call to GetHue(), GetSaturation(), and GetBrightness() and correctly anticipate the results. So, it's time to figure out how the algorithms differ.

Taking the values one by one, it is immediately apparent that Brightness is computed differently. In the algorithms used by the above implementations, the Brightness is always set to the value of the RGB color with the highest value. However, this is obviously not true of the implementation in the .NET Framework. If you plug in a few examples, you can fairly quickly determine that the Brightness is, instead, computed as (Min(R,G,B) + Max(R,G,B)) / 2. To me, this actually made more sense than simply taking the maximum value. If, for example, you used the Brighness value to convert to grayscale, the above algorithms will make pure red, pure green, and pure blue all indistinguisable from pure white. With the .NET Framework implementation, it would be half as bright, and clearly distinguishable.

Saturation turns out to have a similarly more sophisticated algorithm. Hue, however, turns out to be identical, which is completely logical.

Once we have computed these formula, it is simply a matter of performing some high school-level algebra to compute values going in the other direction. The result is the following algorithm, expressed in a C# implementation:

public static Color ColorFromAhsb(int a, float h, float s, float b) {

  if (0 > a || 255 < a) {
    throw new ArgumentOutOfRangeException("a", a, 
      Properties.Resources.InvalidAlpha);
  }
  if (0f > h || 360f < h) {
    throw new ArgumentOutOfRangeException("h", h, 
      Properties.Resources.InvalidHue);
  }
  if (0f > s || 1f < s) {
    throw new ArgumentOutOfRangeException("s", s, 
      Properties.Resources.InvalidSaturation);
  }
  if (0f > b || 1f < b) {
    throw new ArgumentOutOfRangeException("b", b, 
      Properties.Resources.InvalidBrightness);
  }

  if (0 == s) {
    return Color.FromArgb(a, Convert.ToInt32(b * 255), 
      Convert.ToInt32(b * 255), Convert.ToInt32(b * 255));
  }

  float fMax, fMid, fMin;
  int iSextant, iMax, iMid, iMin;

  if (0.5 < b) {
    fMax = b - (b * s) + s;
    fMin = b + (b * s) - s;
  } else {
    fMax = b + (b * s);
    fMin = b - (b * s);
  }

  iSextant = (int)Math.Floor(h / 60f);
  if (300f <= h) {
    h -= 360f;
  }
  h /= 60f;
  h -= 2f * (float)Math.Floor(((iSextant + 1f) % 6f) / 2f);
  if (0 == iSextant % 2) {
    fMid = h * (fMax - fMin) + fMin;
  } else {
    fMid = fMin - h * (fMax - fMin);
  }
      
  iMax = Convert.ToInt32(fMax * 255);
  iMid = Convert.ToInt32(fMid * 255);
  iMin = Convert.ToInt32(fMin * 255);

  switch (iSextant) {
    case 1:
      return Color.FromArgb(a, iMid, iMax, iMin);
    case 2:
      return Color.FromArgb(a, iMin, iMax, iMid);
    case 3:
      return Color.FromArgb(a, iMin, iMid, iMax);
    case 4:
      return Color.FromArgb(a, iMid, iMin, iMax);
    case 5:
      return Color.FromArgb(a, iMax, iMin, iMid);
    default:
      return Color.FromArgb(a, iMax, iMid, iMin);
  }
}

Of course, we had to do quite a bit of mathematical manipulation to arrive at this implementation. I am not sure if you are willing to rely on my mathematical acumen, but I know that I certainly am not! This calls for unit testing.

This is actually an interesting bit of code to implement unit tests for. The theoretical number of combinations of HSB values you could provide is infinite. (I say theoretical because the implementation of the float data type is not infinitely precise.) So, where do we begin?

One obvious starting point is to begin with each possible permutation of RGB colors. If we pass in the HSB values computed by the .NET Framework to create a new color, do we end up with the same color? We can write this test fairly easily:

public void TestAllArgbValues() {
  for (int a = 0; a <= 255; a++) {
    for (int r = 0; r <= 255; r++) {
      for (int g = 0; g <= 255; g++) {
        for (int b = 0; b <= 255; b++) {
          Color startColor = Color.FromArgb(a, r, g, b);
          Color endColor = ColorConversions.ColorFromAhsb(startColor.A,
            startColor.GetHue(), startColor.GetSaturation(),
            startColor.GetBrightness());
          Assert.AreEqual(startColor.A, endColor.A,
            string.Format("Alpha does not match for {0},{1},{2},{3}",
            a, r, g, b));
          Assert.AreEqual(startColor.R, endColor.R,
            string.Format("Red does not match for {0},{1},{2},{3}",
            a, r, g, b));
          Assert.AreEqual(startColor.G, endColor.G,
            string.Format("Green does not match for {0},{1},{2},{3}",
            a, r, g, b));
          Assert.AreEqual(startColor.B, endColor.B,
            string.Format("Blue does not match for {0},{1},{2},{3}",
            a, r, g, b));
        }
      }
    }
  }
}

However, it turns out that this is not such a great unit test after all. Can you spot the problem? If you said, "For the love of code, that unit test has 4,294,967,296 permutations!" you are correct. I started that unit test at 9:00 at night. The next morning, it was still running. It was only when I got home from work that day that it had completed. Fortunately, it had passed! However, we certainly don't want to spend a day running a unit test every single time, so I marked that test with the IgnoreAttribute and tried again.

What is a good subset of every possible permutation of RGB colors? How could you select from them? The route I took was to select all KnownColor members, which should be fairly representative, not to mention disproportionally representing the colors we are likely to receive from a caller. So, the unit test I came up with for this was:

public void TestKnownColors() {
  Array colorsArray = Enum.GetValues(typeof(KnownColor));
  KnownColor[] allColors = new KnownColor[colorsArray.Length];
  Array.Copy(colorsArray, allColors, colorsArray.Length);
  for (int i = 0; i < allColors.Length; i++) {
    Color startColor = Color.FromName(allColors[i].ToString());
    Color endColor = ColorConversions.ColorFromAhsb(startColor.A, 
      startColor.GetHue(), startColor.GetSaturation(),
      startColor.GetBrightness());
    Assert.AreEqual(startColor.A, endColor.A,
      "Alpha does not match for color " + allColors[i].ToString());
    Assert.AreEqual(startColor.R, endColor.R, 
      "Red does not match for color " + allColors[i].ToString());
    Assert.AreEqual(startColor.G, endColor.G, 
      "Green does not match for color " + allColors[i].ToString());
    Assert.AreEqual(startColor.B, endColor.B, 
      "Blue does not match for color " + allColors[i].ToString());
  }
}

And, not surprisingly, this unit test passed as well. Are we done? Well, it's possible that we could pass in HSB values that do not directly correspond to an RGB color (with is much more finite integer values). How do we know that these would be converted correctly?

In order to test these permutations, we would have to come up with an operant definition for what the best Color approximation of an HSB value would be. We could potentially define this as the Color structure who's HSB values differed from the input HSB values by the smallest amount. However, then we would need to iterate over the same huge number of possible colors for each initial HSB value. Given that we have a theoretically infinite number of HSB values that do not directly correspond to a Color structure, suddently we have infinity times a huge number, with each of the infinite iterations requiring approximately a day to run. In the end, I decided that I was confident enough in my math to forego this exercise.

In the end, we have created some useful new functionality, which is consistent with the computation of HSB values that the platform provides. Later, we will use this ability to start to do some interesting things.