用于 System.Drawing.Color 值的色相/亮度色轮式图表






4.82/5 (30投票s)
实现相似颜色样本的并排比较。
引言
本文描述了一种将命名的 System.Drawing.Color
值系统地映射到二维矩形网格的方法,而不是按照颜色名称出现的字母顺序排列。映射算法基于色相/亮度色轮。结果显示在上图(已缩小)中。
要查看此图表的完整尺寸预渲染版本,请单击此处。
我创建此实用程序是因为我需要比较和选择在视觉上彼此接近的颜色值,例如图表的这一片段
接近的定义可以描述为它们的色相、亮度和饱和度彼此有多近。可视化这些参数通常需要三维表示;然而,由于我想要一个二维图表,我只映射了色相和亮度值(因此忽略了饱和度)。有关颜色空间定义的更多信息,请参阅HSL 和 HSV。
背景
在映射 System.Drawing.Color
值时,我希望颜色名称和 RGB 值显示在实际颜色样本上的文本。这使得设计被限制在一个矩形彩色单元格网格中,其中
- 色相以圆形方式从图表中心单元格的 0-360° 映射。
- 亮度从中心处的纯黑色映射到图表四周的纯白色。
由于没有饱和度映射,纯灰色值颜色在色轮中可能会显得迷失,难以并排比较。因此,我决定以编程方式将它们从主要的色相/亮度部分剥离,并将它们作为“灰色楔形”单独显示在图表的左上方。
使用代码
颜色图表应用程序是一个基于 Windows Forms 的应用程序。窗口的整个客户区域被划分为固定数量的行和列。这在内部由以下数组表示
private CellInfo[,] m_Grid = new CellInfo[SideCount, SideCount];
代码结构非常简单
Form
的构造函数将此数组从System.Drawing.Color
集合中填充。Paint
事件处理程序只需在需要时将数组渲染到整个客户区域。
构造函数 - 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()
将 System.Drawing.Colors
过滤到两个列表中
m_ColorList
用于图表的色相/亮度部分m_GrayList
用于灰色楔形。
如果颜色的 GetSaturation()
值小于 0.0001,则认为该颜色是灰色。此方法还试图消除名称不同但实际上具有相同 RGB 颜色值的颜色。
MapColorsToCells()
将色相/亮度映射应用于 m_ColorList
中的每种颜色。这是通过为每种颜色调用 Color.GetHue()
和 Color.GetBrightness()
来实现的,并将颜色点缩放到落在 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));
上面在计算修改后的亮度值时使用的 Gamma
因子被设置,使得颜色倾向于聚集在轮子的中心,而不是分散开并留有未占用的(白色)单元格。这有助于颜色的并排比较。
在将理想化的色相/亮度值映射到具有有限单元格数量的网格时,经常会出现“冲突”,即两个命名的颜色值想要占据同一个单元格。
上面的代码中的最后一行通过让每个单元格保留一个列表来解决这个问题,该列表包含所有落入其中的颜色。随后,每个单元格的列表根据每种颜色与单元格的理想颜色中心点的“接近”程度进行排序
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);
}
}
}
最接近单元格中心颜色的颜色将是该单元格的 ColorCollisionList
中的第一个。然后,该颜色用于通过 Paint
处理程序渲染该单元格的外观。
单元格 ColorCollisionList
中剩余的(冲突的)颜色通过尝试将它们移动到最近的空单元格来处理。理想情况下,这应该是一个与目标单元格相邻的单元格,但取决于 SideCount
的值,ColorCollisionList
中的所有颜色可能无法以这种方式成功移动。
我的第一个实现实际上允许将冲突的颜色移动(或“抛出”)到比目标单元格远一个单元格的距离。然而,最终结果通常不会将颜色放在非常好的位置,所以我将“抛出”距离限制为一个单元格,并通过仔细选择 SideCount
和 Gamma
因子来解决问题,直到所有颜色都成功映射到网格。
当前实现使用 SideCount
=25 和 Gamma
= 1.45。
Paint 事件处理程序 - ColorChartForm_Paint(...)
这会简单地调用 RenderColorWheel()
和 RenderGrayWedge()
来更新客户区域。RenderColorWheel()
遍历 m_Grid
中的所有单元格并执行
...
...
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()
类似,但会将灰色楔形强制渲染到图表左上方的一个未使用的单元格条带中。
链接
要查看此图表的完整尺寸预渲染版本,请单击此处。