Introduction
This article describes a method for mapping the named System.Drawing.Color
values to a 2D rectangular grid in a systematic way, rather than the alphabetic order in which the color names appear. The mapping algorithm is based on the Hue/Brightness color wheel. The result is shown in the (downscaled) image above.
To see a full size pre-rendered version of this chart, go here.
I created this utility because of my need to compare and choose color values that are visually close to one another, like this snippet from the chart:
A definition of close may be described by how near their hue, brightness, and saturation are to each other. Visualizing these parameters generally requires a 3D representation; however, since I want a 2D chart, I have only mapped the Hue and Brightness values (saturation is therefore disregarded). For more information on color space definitions, see HSL and HSV.
Background
In mapping System.Drawing.Color
values, I wanted the color's name and RGB value to appear as text on top of a swatch of the actual colour as background. This constrained the design to a rectangular grid of colored cells, where:
- Hue is mapped in a circular fashion 0-360° around the center cell of the chart.
- Brightness is mapped from Pure Black at the centre to Pure White all the way around the perimeter of the chart.
Since there is no saturation mapping, pure gray-value colors can get seemingly lost within a color wheel, making it hard to compare them side by side. Consequently, I decided to programmatically strip them from the main Hue/Brightness section and present them separately as a "gray-wedge" at the top left of the chart.
Using the code
The Color Chart application is a Windows Forms based application. The entire client area of the window is divided into a fixed number of rows and columns. This is internally represented by the following array:
private CellInfo[,] m_Grid = new CellInfo[SideCount, SideCount];
The code structure is quite simple:
- The
Form
's constructor populates this array from the System.Drawing.Color
set.
- The
Paint
event handler simply renders the array to the entire client area, whenever it needs to.
Constructor - ColorChartForm()
InitializeGrid();
GenerateColorAndGrayLists();
MapColorsToCells();
GenerateDiagnostics();
GenerateColorAndGrayLists()
filters System.Drawing.Colors
into two lists:
m_ColorList
destined for the Hue/Brightness part of the chart
m_GrayList
destined for the gray-wedge.
If a color's GetSaturation()
value is less than 0.0001, then the color is deemed to be a gray. This method also tries to eliminate differently named colors that actually have the same RGB color value.
MapColorsToCells()
applies the Hue/Brightness mapping to each color in the m_ColorList
. This is implemented by making calls to the Color.GetHue()
and Color.GetBrightness()
for each color, and scaling the color point to land onto an ideal cell inside the m_Grid
.
float hue = color.GetHue();
float brightness = (float)Math.Pow(color.GetBrightness(), Gamma);
int halfSideCount = SideCount / 2;
double dx = halfSideCount * (1.0 + brightness *
Math.Cos(hue * 2.0 * Math.PI / 360.0));
double dy = halfSideCount * (1.0 + brightness *
Math.Sin(hue * 2.0 * Math.PI / 360.0));
int x = (int)Math.Round(dx);
int y = (int)Math.Round(dy);
if (m_Grid[y, x] == null)
{
m_Grid[y, x] = new CellInfo(x, y);
}
m_Grid[y, x].ColorCollisionList.Add(new ColorPoint(color, dx, dy));
The Gamma
factor used above in calculating a modified brightness value is set so that colors tend to congregate towards the center of the wheel, rather than being spaced out with unoccupied (white) cells between them. This aids side-by-side comparison of colors.
In mapping idealized Hue/Brightness values onto a grid with a limited number of cells, quite often, there is a "collision" when two named color values want to occupy the same cell.
The last line in the code above gets around this by having each cell keep a list of all colors that land on it. Subsequently, each cell's list is sorted according to how "close" each color is to the ideal color centre point of the cell:
for (int j = 0; j < SideCount; j++)
{
for (int i = 0; i < SideCount; i++)
{
if (m_Grid[j, i] != null)
{
m_Grid[j, i].ColorCollisionList.Sort(m_ColorPointSorter);
}
}
}
The closest color to the center of the cell will be the first in the ColorCollisionList
for that cell. This color is then used to render that cell's appearance via the Paint
handler.
The remaining (colliding) colors in each cell's ColorCollisionList
are dealt with by trying to move them to the nearest empty cell. Ideally, this should be a cell that is adjacent to the target cell, but depending on the value of SideCount
, it is possible that not all colors in the ColorCollisionList
will be moved successfully in this way.
My first implementation of this actually allowed for moving (or throwing) the colliding colors radially out further than one cell distance away from the target cell. However, the end result did not generally put colors at very good locations, so I limited the "throw" distance to one cell and fixed the problem by carefully choosing the SideCount
and Gamma
factor until all colors were successfully mapped to the grid.
The current implementation uses SideCount
=25 and Gamma
= 1.45.
Paint Event Handler - ColorChartForm_Paint(...)
This simply calls RenderColorWheel()
and RenderGrayWedge()
to update the client area. RenderColorWheel()
iterates through all cells in the m_Grid
and performs:
...
...
if (m_Grid[y, x] != null)
{
CellInfo cellInfo = m_Grid[y, x];
List<colorpoint> colorPoints = cellInfo.ColorCollisionList;
using (SolidBrush br = new SolidBrush(colorPoints[0].Color))
{
Rectangle rcClip = rc;
rcClip.Height += 1;
rcClip.Width += 1;
g.Clip = new Region(rcClip);
g.FillRectangle(br, rc);
g.DrawRectangle(pen, rc);
g.DrawString(cellInfo.ColorCollisionList[0].Color.Name,
this.Font,
Brushes.Black,
new PointF((float)rc.Left + 1, (float)rc.Top + 2));
g.DrawString(cellInfo.ColorCollisionList[0].Color.R.ToString() +
", " + cellInfo.ColorCollisionList[0].Color.G.ToString() +
", " + cellInfo.ColorCollisionList[0].Color.B.ToString(),
this.Font,
Brushes.Black,
new PointF((float)rc.Left + 1, (float)rc.Top + 12));
}
}
...
...
RenderGrayWedge()
is similar, but forces the gray-wedge to be rendered at an unused strip of cells at the top left of the chart.
Links
To see a full size pre-rendered version of this chart, go here.