Continuing from where Part 1 left off, this article describes the algorithms, math, and data involved with storing, manipulating, and converting colors. Unit testing with the NUnit framework is also covered.

Figure:3D color models rendered in WPF.
Table of Contents:
1. Overview
Color processing has a vast array of scientific and practical applications, from hyperspectral analysis to print media optimization. There is a vast wealth of information available online from both educational institutions and serious hobbyists that cover these problems and additional issues within specific domains. Within the scope of this color selector, the technical aspects are centered upon the problems of how to store color information in various formats, convert between formats, and create visualizations.
2. Data
The color selector stores color information in a RawColor class that contains double properties for storing the four main channels of Alpha, Red, Green, and Blue (ARGB) color data. The IFormattable interface is implemented in order to provide a string representation of the data.
public class RawColor : IFormattable
{
public double A { get; set; } = 0;
public double R { get; set; } = 0;
public double G { get; set; } = 0;
public double B { get; set; } = 0;
public RawColor() { }
public RawColor(double a, double r, double g, double b)
{
A = a;
R = r;
G = g;
B = b;
}
public string ToString(string? format, IFormatProvider? formatProvider)
{
return Color.FromArgb((byte)A, (byte)R, (byte)G, (byte)B).ToString();
}
}
Source:ColorSelector.cs
The double floating-point precision of the ARGB properties is extremely important for reducing rounding errors during color model conversion and improving visualization accuracy. C# has several main color APIs, and WPF mainly uses structs and classes within the System.Windows.Media and System.Drawing namespaces. The System.Windows.Media.Color struct stores color information as both bytes (A, R, G, B) and as single floating-point percentages (ScA, ScR, ScG, ScB). While single floating-point provides decent accuracy, double floating-point provides much greater accuracy - up to nine significant digits in the current implementation of the color selector. The computational performance difference between single and double floating-point precision is negligible within the scope of this project, but the accuracy difference is clearly visible. By using higher precision backing data, the color selector still leverages the C# single floating-point and byte APIs by casting to lower-precision data-types, while retaining the original higher-fidelity information.
3. Math
Integer and floating-point arithmetic is used in various functions of the color selector. Care was taken (after at least a couple of mistakes) to explicitly define the correct mathematic operators to avoid unintentional integer math, which can quickly cause havoc and underscores the importance of unit testing. Conversion from RawColor to Bytes is performed using the System.Convert.ToByte() function, which throws exceptions if the input is problematic. Conversions from RGB to Hue Saturation Lightness (HSL) and Hue Saturation Value (HSV) models are performed with custom double floating-point functions. The System.Drawing namespace provides functions for returning HSL values in single floating-point precision, but these are inadequate for handling the double floating-point conversions.
Another caveat is that divide by zero errors are possible, if unchecked, when calculating values at the extremes of color saturation, lightness, and value. This is mainly due to the calculation of chromatic intensity, which involves subtracting the minimum RGB channel value from the maximum RGB channel value. The below code snippet shows the function that returns Hue from given R,G, and B components:
public static double GetHueFromRgbPercent(double r, double g, double b)
{
var min = Math.Min(Math.Min(r, g), b);
var max = Math.Max(Math.Max(r, g), b);
var chroma = max - min;
if (chroma == 0)
return 0;
var hue = 0.0;
if (r == max)
{
var segment = (g - b) / chroma;
var shift = 0 / 60;
if (segment < 0)
{
shift = 360 / 60;
}
hue = segment + shift;
}
else if (g == max)
{
var segment = (b - r) / chroma;
var shift = 120 / 60;
hue = segment + shift;
}
else if (b == max)
{
var segment = (r - g) / chroma;
var shift = 240 / 60;
hue = segment + shift;
}
return hue * 60;
}
Source:ColorSelector.cs
In order to generate the colors for each 3D cube face, a bilinear interpolation algorithm is used to evenly blend colors emanating from the corners of a rectangle. The resulting ImageBrush is then applied to a DiffuseMaterial set as the Material of the GeometryModel3D of a cube face. The colors of each face were carefully coordinated in order to create a seamless RGB spectrum around the cube. Below is the algorithm that generates the ImageBrush based on the bilinear interpolation logic:
public static ImageBrush CreateBilinearGradient(int w, int h, Color upperLeft, Color upperRight, Color lowerLeft, Color lowerRight)
{
BitmapSource? source;
using (System.Drawing.Bitmap bmp = new(w, h))
{
System.Drawing.Graphics flagGraphics = System.Drawing.Graphics.FromImage(bmp);
for (int x = 0; x < w; x++)
{
for (int y = 0; y < h; y++)
{
double fracX = x / (w * 1.0);
double fracY = y / (h * 1.0);
Color color = BilinearInterpolateColor(upperLeft, upperRight, lowerLeft, lowerRight, fracX, fracY);
bmp.SetPixel(x, y, System.Drawing.Color.FromArgb(color.A, color.R, color.G, color.B));
}
}
IntPtr hBitmap = bmp.GetHbitmap();
try
{
source = System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap(hBitmap, IntPtr.Zero, Int32Rect.Empty, System.Windows.Media.Imaging.BitmapSizeOptions.FromEmptyOptions());
}
finally
{
DeleteObject(hBitmap);
}
}
return new ImageBrush() { ImageSource = source };
}
Source:ColorSelector.cs
In order to generate the colors for the 3D HSV cone model, a procedural approach was taken: for each subdivision of the cone, generate a Color that has a hue equal to the position of that subdivision relative to 360°. With sufficiently small subdivisions, the individual colors become increasingly difficult to differentiate, resulting in the appearance of a continuous spectrum gradient along the perimeter of the cone. Below is the snippet that generates the colors for the 3D cone model:
int faces = 128;
for (int i = 0; i < faces; ++i)
{
DiffuseMaterial material = new(new SolidColorBrush(GetRgbColorFromModel((360.0 * i) / faces, 1, 1)));
...
}
Source:ColorSelector.cs
4. Testing
Unit testing is implemented with the NUnit framework in order to improve maintainability, discover logical errors, and reduce regression defects. The two main test fixtures within the color selector's unit test suite are dedicated to testing input validation and color processing outcomes. The validation test fixture passing strings of random characters into the ValidationRule classes defined for the different types of text input, in order to catch unexpected validation results from unique input combinations. Below is the unit test for checking the text input validators of A, R, G, and B channels; note that I use the Random attribute in order to produce a string of random length between 0 and 20 characters, repeated 1000 times:
public const int ValidatorRand = 1000;
...
[Test]
public void ArgbValidator([Random(0, 20, ValidatorRand)] int length)
{
string str = TestContext.CurrentContext.Random.GetString(length, FuzzingChars);
TestContext.WriteLine(str);
ValidationResult result = argbRule.Validate(str, CultureInfo.CurrentCulture);
if (double.TryParse(str, NumberStyles.AllowDecimalPoint, CultureInfo.CurrentCulture, out double parsed))
{
if ((MinRGBA <= parsed && parsed <= MaxRGBA))
Assert.That(result.IsValid, Is.True, str);
}
else
Assert.That(result.IsValid, Is.False, str);
}
Source:ColoSelectorUnitTests/ColorSelectorUnitTests.cs
The color processing test fixture tests the outcomes of color creation and modification based on random numeric values. By using the Range and Random NUnit attributes, test automation works its magic: the test suite executes roughly 13,500 tests within 100 milliseconds! These tests cover the entire range of each color channel and component, and assert a certain degree of precision is maintained when performing conversions. The following example shows the CurrentColorFromHslDoubles test, which creates a random HSL color, converts it to RGB, and checks that the resulting RGB can derive a hue that is equal, at nine significant digits, to the originally specified hue:
public const double MinRGBA = 0.0;
public const double MaxRGBA = 255.0;
public const double MinHue = 0.0;
public const double MaxHue = 360.0;
public const double MinSLV = 0.0;
public const double MaxSLV = 1.0;
public const int RgbRand = 6;
public const int HslvRand = 10;
public const int SignificantDigits = 9;
readonly ColorSelector.ColorSelector selector = new();
...
[Test]
public void CurrentColorFromHslDoubles([Random(MinHue, MaxHue, HslvRand)] double h, [Random(MinSLV, MaxSLV, HslvRand)] double s, [Random(MinSLV, MaxSLV, HslvRand)] double l)
{
selector.ColorModel = ColorModel.HSL;
var rgb = selector.ModelToRgb(h, s, l);
selector.CurrentColor = new RawColor(selector.A, rgb[0], rgb[1], rgb[2]);
selector.ProcessColorChange("");
var r = selector.CurrentColor.R;
var g = selector.CurrentColor.G;
var b = selector.CurrentColor.B;
Assert.Multiple(() =>
{
Assert.That(Math.Round(ColorSelector.ColorSelector.GetHueFromRgbByteRange(r, g, b), SignificantDigits), Is.EqualTo(Math.Round(h, SignificantDigits)));
Assert.That(Math.Round(ColorSelector.ColorSelector.GetHslSaturationFromRgbByteRange(r, g, b), SignificantDigits), Is.EqualTo(Math.Round(s, SignificantDigits)));
Assert.That(Math.Round(ColorSelector.ColorSelector.GetLightnessFromRgbByteRange(r, g, b), SignificantDigits), Is.EqualTo(Math.Round(l, SignificantDigits)));
});
}
Source:ColorSelectorUnitTests/ColorSelectorUnitTests.cs
By implementing even a small number of unit tests, I was able to detect and fix several logical errors that would have otherwise taken a much longer time to discover via manual testing. NUnit is an excellent tool.
5. Conclusion
That covers the Color Selector technical details at a medium to lowish level overview. Part one covers the aspects of the application structure, the UI, and UX. The complete source is available via GitHub. Thanks for reading!