Click here to Skip to main content
15,897,315 members
Articles / Desktop Programming / Windows Forms

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

Rate me:
Please Sign up or sign in to vote.
4.82/5 (32 votes)
21 Mar 2009CPOL4 min read 75K   1.1K   25  
Enables side-by-side comparison of close matching color swatches.
using System;
using System.Collections.Generic;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Diagnostics;
using System.Drawing.Imaging;
using System.Reflection;

namespace ColorChart
{
    public partial class ColorChartForm : Form
    {
        #region Member Data
        private static readonly Size SwatchSize = new Size(30, 20);
        private static readonly int SideCount = 25;
        private static readonly double Gamma = 1.45;
        private static readonly int CellHeightAdjust = 5;
        private int m_nSet = 0;
        private List<Color> m_ColorList = new List<Color>();
        private List<Color> m_GrayList = new List<Color>();
        private CellInfo[,] m_Grid = new CellInfo[SideCount, SideCount];
        #endregion

        #region Constructor
        public ColorChartForm()
        {
            InitializeComponent();
            this.DoubleBuffered = true;

            InitializeGrid();

            GenerateColorAndGrayLists(); // make two lists : one for the colour wheel and one for the gray-wedge

            MapColorsToCells(); // map all colours (not grays) to the grid in a standard hue/brightness colour wheel.

            GenerateDiagnostics();
        }
        #endregion

        #region Private Implementation

        // InitializeGrid
        //
        // Initialize the grid that will show all System.Drawing.Color values 
        private void InitializeGrid()
        {

            for (int j = 0; j < SideCount; j++)
            {
                for (int i = 0; i < SideCount; i++)
                {
                    m_Grid[j, i] = null;
                }
            }
        }
        // GenerateColorAndGrayLists
        //
        // m_ColorList stores all colours that will be rendered on the m_Grid as an approximated wheel.
        // m_GrayList stores all gray scale values that will be rendered on the m_Grid as a gray-wedge.

        private void GenerateColorAndGrayLists()
        {
            // Pre-Add Black so we can exclude duplicate black values with different names.

            m_ColorList.Add(Color.Black);

            foreach (string s in Enum.GetNames(typeof(System.Drawing.KnownColor)))
            {
                Color c = System.Drawing.Color.FromName(s);
                if (c.A != 255)
                {
                    // This is "Transparent"
                    // It is not very interesting to draw, so skip it.
                    continue;
                }

                if (c.Name.Contains("Control") ||
                    c.Name.Contains("Active") ||
                    c.Name.Contains("Inactive") ||
                    c.Name.Contains("Button") ||
                    c.Name.Contains("Text") ||
                    c.Name.Contains("Menu") ||
                    c.Name.Contains("Workspace") ||
                    c.Name.Contains("Scroll")
                    )
                {
                    // For this implementation, we skip the Windows theme colors, 
                    // because these are mostly duplicates of other ones we already show.
                    continue;
                }

                // check of the colour is a "gray scale" entity - low saturation
                // so we can draw it in a separate strip away from the main colour wheel.

                if (c.GetSaturation() < 0.0001)
                {
                    // a grey colour...
                    if (c.ToArgb() == Color.White.ToArgb())
                    {
                        // exclude any colours that are white by a different name.
                        if (!m_GrayList.Contains(Color.White))
                        {
                            m_GrayList.Add(Color.White);
                        }
                    }
                    else if (c.ToArgb() == Color.Black.ToArgb())
                    {
                        // exclude any colours that are black by a different name.
                        if (!m_GrayList.Contains(Color.Black))
                        {
                            m_GrayList.Add(Color.Black);
                        }
                    }
                    else
                    {
                        // gray scale value
                        m_GrayList.Add(c);
                    }
                }
                else
                {
                    // this is a colorw we want to plot on the "wheel"
                    m_ColorList.Add(c);
                }
            }

            // Sort the gray scale list so they make a gray wedge
            m_GrayList.Sort(new BWSorter());
        }

        // MapColorsToCells
        //
        // Map all m_ColorList values on to a "wheel" that is centred on the middle of the grid.
        // Depending on SideCount, several m_ColorList values may collide on single cell. 
        // So each cell maintains a list of all color collisions.
        private void MapColorsToCells()
        {
            foreach (Color ci in m_ColorList)
            {
                MapColorCell(ci);
            }

            // Sort the ColorCollisionListin each cell in order of how close they are to the cell's ideal color point.

            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(new ColorPointSorter());
                    }
                }
            }

            // Now try to resolve colliding colors (in each cell's ColorCollisionList list) 
            // by moving them to the nearest free m_Grid cell (a free cell contains null)

            for (int j = 0; j < SideCount; j++)
            {
                for (int i = 0; i < SideCount; i++)
                {
                    if (m_Grid[j, i] != null)
                    {
                        int nTries = 0;
                        while (m_Grid[j, i].ColorCollisionList.Count > 1 && nTries < 30)
                        {

                            for (int k = 1; k < m_Grid[j, i].ColorCollisionList.Count; k++)
                            {
                                ColorPoint cp = m_Grid[j, i].ColorCollisionList[k];
                                // calculate where this will land on adjacent cell
                                bool bMoved = false;

                                double throwDist = 1.99;// this is how far out from centre of current cell we will throw the colliding colour
                                {
                                    // we try throwing the colliding colour in a circle around current cell,
                                    // by looking for empty candidates....
                                    for (double theta = cp.Theta; theta < cp.Theta + 360; theta += 22.5)
                                    {
                                        int xOffset = (int)(throwDist * Math.Cos(2 * Math.PI * theta / 360.0));
                                        int yOffset = (int)(throwDist * Math.Sin(2 * Math.PI * theta / 360.0));

                                        int I = i + xOffset;
                                        int J = j + yOffset;

                                        if (I < 0)
                                            I = 0;
                                        else if (I > SideCount - 1)
                                            I = SideCount - 1;

                                        if (J < 0)
                                            J = 0;
                                        else if (J > SideCount - 1)
                                            J = SideCount - 1;

                                        if (m_Grid[J, I] == null)
                                        {
                                            m_Grid[J, I] = new CellInfo(I, J);
                                            m_Grid[J, I].ColorCollisionList.Add(new ColorPoint(cp.Color, I, J));
                                            m_Grid[J, I].ThrownX = xOffset;
                                            m_Grid[J, I].ThrownY = yOffset;
                                            bMoved = true;
                                            break;
                                        }
                                    }
                                }
                                if (bMoved)
                                {
                                    m_Grid[j, i].ColorCollisionList.RemoveAt(k);
                                    break;
                                }
                            }
                            nTries++;
                        }
                    }
                }
            }
        }

        // GenerateDiagnostics
        //
        // Depending on how congested certain regions of the grid are, some colliding 
        // colors may have been left unmapped.
        // The following m_nSet diagnostic checks how many colour we actually managed to set in the grid
        private void GenerateDiagnostics()
        {
            m_nSet = 0;
            for (int j = 0; j < SideCount; j++)
            {
                for (int i = 0; i < SideCount; i++)
                {
                    if (m_Grid[j, i] != null)
                    {
                        m_nSet++;
                    }
                }
            }
        }

        // MapColorCell
        // 
        // Maps the specified color into the m_Grid cell that most 
        // ideally fits its colour value (in hue and brightness)

        private void MapColorCell(Color color)
        {
            // The colour wheel is organized with black at the centre, and white all around the rim.
            // The hue rotates from  0..360 degrees about the centre.
            // The radius of the wheel is SideCount / 2.
            // To aid compaction of the colour swatches toward the centre of the chart, 
            // the brightness value of each colour is first modified by a gamma factor (1.45)
            //
            // Note that Saturation values are not specifically mapped or taken into account.

            float hue = color.GetHue(); // this is the angle around the wheel (0..359)

            // calculate a brightness gamma factor to force colours to bunch closer to the centre of the chart...
            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)); // add colour value to end of collision list..
        }
        #endregion

        #region Form Events
        // Paint event handler
        private void ColorChartForm_Paint(object sender, PaintEventArgs e)
        {
            Graphics g = e.Graphics;
            g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;

            GraphicsContainer gc = g.BeginContainer();

            RenderColorWheel(g);

            RenderGrayWedge(g);

            g.EndContainer(gc);

            // Draw some diagnostics
            using (Font font = new Font("Arial", 8.0f, GraphicsUnit.Point))
            {
                g.DrawString(this.ClientRectangle.Width.ToString() + " / " + this.ClientRectangle.Height.ToString(),
                                font, Brushes.Black,
                                new PointF(0,12));

                g.DrawString(m_nSet + " / " + m_ColorList.Count,
                                font, Brushes.Black,
                                new PointF(0, 24));
            }
        }

        // RenderColorWheel
        // 
        // Render all color wheel swatches from m_Grid to screen
        private void RenderColorWheel(Graphics g)
        {
            int nWidth = ClientRectangle.Width / SideCount;
            int nHeight = ClientRectangle.Height / SideCount + CellHeightAdjust;
            int yOffset = 2 * nHeight;

            for (int y = 0; y < SideCount; y++)
            {
                for (int x = 0; x < SideCount; x++)
                {
                    Rectangle rc = new Rectangle(x * nWidth, y * nHeight - yOffset, nWidth, nHeight);
                    using (Pen pen = new Pen(Color.DimGray))
                    {
                        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
        //
        // Render the gray scale swatches to top-right of chart...
        private void RenderGrayWedge(Graphics g)
        {
            int nWidth = ClientRectangle.Width / SideCount;
            int nHeight = ClientRectangle.Height / SideCount + CellHeightAdjust;

            for (int x = 0; x < m_GrayList.Count; x++)
            {
                Rectangle rc = new Rectangle((x + 15) * nWidth, 0, nWidth, nHeight);
                using (Pen pen = new Pen(Color.Gray))
                {
                    using (SolidBrush br = new SolidBrush(m_GrayList[x]))
                    {
                        Rectangle rcClip = rc;
                        rcClip.Height += 1;
                        rcClip.Width += 1;

                        g.Clip = new Region(rcClip);

                        g.FillRectangle(br, rc);
                        g.DrawRectangle(pen, rc);

                        g.DrawString(m_GrayList[x].Name,
                           this.Font, Brushes.Black, new PointF((float)rc.Left, (float)rc.Top + 2));
                        g.DrawString(m_GrayList[x].R.ToString() + ", " + m_GrayList[x].G.ToString() + ", " + m_GrayList[x].B.ToString(),
                            this.Font, Brushes.Black, new PointF((float)rc.Left + 1, (float)rc.Top + 12));

                    }
                }
            }
        }

        private void ColorChartForm_Resize(object sender, EventArgs e)
        {
            Invalidate();
        }
        #endregion
    }

    public class CellInfo
    {
        #region Properties
        public int X { get; set; }
        public int Y { get; set; }
        public List<ColorPoint> ColorCollisionList { get; set; }
        public int ThrownX { get; set; }
        public int ThrownY { get; set; }
        #endregion

        #region Constructor
        public CellInfo(int x, int y)
        {
            ThrownX = 0;
            ThrownY = 0;
            X = x;
            Y = y;
            ColorCollisionList = new List<ColorPoint>();
        }
        #endregion
    }

    public class ColorPoint
    {
        #region Properties
        public Color Color { get; set; }
        public double X { get; set; }
        public double Y { get; set; }
        public double NX { get; set; }
        public double NY { get; set; }
        public double R { get; set; }
        public double Theta { get; set; }
        #endregion

        #region Constructor
        public ColorPoint(Color ci, double x, double y)
        {
            Color = ci;
            X = x;
            Y = y;
            double dx = X - (double)(int)Math.Round(X);
            double dy = Y - (double)(int)Math.Round(Y);

            R = Math.Sqrt(dx * dx + dy * dy);

            if (dy == 0 && dx == 0)
            {
                Theta = 0;
            }
            else
            {
                Theta = Math.Atan2(dy, dx) * 360 / (2 * Math.PI);
            }

            if (R > 0)
            {
                NX = dx / R;
                NY = dy / R;
            }
            else
            {
                NX = 0;
                NY = 0;
            }
        }
        #endregion
    }

    public class ColorPointSorter : IComparer<ColorPoint>
    {
        // compare how close two ColorPoints are to their ideal color cell centre point
        public int Compare(ColorPoint cp1, ColorPoint cp2)
        {
            // compare how close 
            double dist1 = DistanceToCentre(cp1);
            double dist2 = DistanceToCentre(cp2);

            if (dist1 > dist2)
            {
                return 1;
            }
            else if (dist1 < dist2)
            {
                return -1;
            }
            else
            {
                return 0;
            }
        }

        private double DistanceToCentre(ColorPoint cp)
        {
            double dx = cp.X - (double)(int)Math.Round(cp.X);
            double dy = cp.Y - (double)(int)Math.Round(cp.Y);

            double dist = Math.Sqrt(dx * dx + dy * dy);

            return dist;
        }
    }

    public class BWSorter : IComparer<Color>
    {
        // compare colours only by their brightness 
        public int Compare(Color x, Color y)
        {
            if (x.GetBrightness() > y.GetBrightness())
            {
                return 1;
            }
            else if (x.GetBrightness() < y.GetBrightness())
            {
                return -1;
            }
            else
            {
                return 0;
            }
        }
    }

    // Not currently used.
    //     public class ColorSorter : IComparer<Color>
    //     {
    //         public int Compare(Color x, Color y)
    //         {
    //             if (x.GetHue() > y.GetHue())
    //             {
    //                 return 1;
    //             }
    //             else if (x.GetHue() < y.GetHue())
    //             {
    //                 return -1;
    //             }
    //             else
    //             {
    //                 if (x.GetBrightness() > y.GetBrightness())
    //                 {
    //                     return 1;
    //                 }
    //                 else if (x.GetBrightness() < y.GetBrightness())
    //                 {
    //                     return -1;
    //                 }
    //                 else
    //                 {
    //                     if (x.GetSaturation() > y.GetSaturation())
    //                     {
    //                         return 1;
    //                     }
    //                     else if (x.GetSaturation() < y.GetSaturation())
    //                     {
    //                         return -1;
    //                     }
    //                     else
    //                     {
    //                         return 0;
    //                     }
    //                 }
    //             }
    //         }
    //     }

}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Sonardyne International
United Kingdom United Kingdom
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions