Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

A Hue/Brightness Color Wheel Style Chart for System.Drawing.Color Values

0.00/5 (No votes)
21 Mar 2009 1  
Enables side-by-side comparison of close matching color swatches.

ReducedChart600Wide.png

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:

SampleOfChart.png

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(); // set all m_Grid cells to null

GenerateColorAndGrayLists(); // generate 2 separate lists of colors: colors and grays 

MapColorsToCells(); // map all colours (not grays) as a color wheel on the grid

GenerateDiagnostics(); // to check if everything got mapped ok

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(); // angle around the wheel (0..360) 

//calculate a brightness gamma factor to force colors to bunch to the centre 
float brightness = (float)Math.Pow(color.GetBrightness(), Gamma);

int halfSideCount = SideCount / 2; // radius 

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); // assign cell here 
}
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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here