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;
// }
// }
// }
// }
// }
}