Browse Source

Switch ColorHelper.ToDisplayName() to a hue-only algorithm

This is not working quite as accurately as expected so will likely be replaced by a hybrid technique next.
pull/10439/head
robloo 3 years ago
parent
commit
1013463262
  1. 146
      src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs

146
src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs

@ -1,6 +1,6 @@
using System;
using System.Globalization;
using System.Collections.Generic;
using System.Globalization;
using Avalonia.Media;
using Avalonia.Utilities;
@ -11,8 +11,11 @@ namespace Avalonia.Controls.Primitives
/// </summary>
public static class ColorHelper
{
private static readonly Dictionary<Color, string> cachedDisplayNames = new Dictionary<Color, string>();
private static readonly object cacheMutex = new object();
private static readonly Dictionary<Color, string> _cachedDisplayNames = new Dictionary<Color, string>();
private static readonly Dictionary<KnownColor, double> _cachedKnownColorHues = new Dictionary<KnownColor, double>();
private static readonly Dictionary<KnownColor, string> _cachedKnownColorNames = new Dictionary<KnownColor, string>();
private static readonly object _displayNameCacheMutex = new object();
private static readonly object _knownColorCacheMutex = new object();
/// <summary>
/// Gets the relative (perceptual) luminance/brightness of the given color.
@ -74,37 +77,91 @@ namespace Avalonia.Controls.Primitives
Convert.ToByte(Math.Round(color.R / rounding) * rounding),
Convert.ToByte(Math.Round(color.G / rounding) * rounding),
Convert.ToByte(Math.Round(color.B / rounding) * rounding));
var hsvColor = color.ToHsv();
// Handle extremes that are outside the below algorithm
if (color.A == 0x00)
{
return GetDisplayName(KnownColor.Transparent);
}
else if (hsvColor.S <= 0.0)
{
return GetDisplayName(KnownColor.White);
}
else if (hsvColor.V <= 0.0)
{
return GetDisplayName(KnownColor.Black);
}
// Attempt to use a previously cached display name
lock (cacheMutex)
lock (_displayNameCacheMutex)
{
if (cachedDisplayNames.TryGetValue(roundedColor, out var displayName))
if (_cachedDisplayNames.TryGetValue(roundedColor, out var displayName))
{
return displayName;
}
}
// Find the closest known color by measuring 3D Euclidean distance (ignore alpha)
// Build KnownColor caches if they don't already exist
lock (_knownColorCacheMutex)
{
if (_cachedKnownColorHues.Count == 0 ||
_cachedKnownColorNames.Count == 0)
{
_cachedKnownColorHues.Clear();
_cachedKnownColorNames.Clear();
var knownColors = (KnownColor[])Enum.GetValues(typeof(KnownColor));
for (int i = 1; i < knownColors.Length; i++) // Skip 'None' so start at 1
{
KnownColor knownColor = knownColors[i];
// Transparent is skipped since alpha is ignored making it equivalent to White
if (knownColor == KnownColor.Transparent)
{
continue;
}
double hue = KnownColors.ToColor(knownColor).ToHsv().H;
// Some known colors have the same numerical value. For example:
// - Aqua = 0xff00ffff
// - Cyan = 0xff00ffff
//
// This is not possible to represent in a dictionary which requires
// unique values. Therefore, only the first value is used.
if (!_cachedKnownColorHues.ContainsKey(knownColor))
{
_cachedKnownColorHues.Add(knownColor, hue);
}
if (!_cachedKnownColorNames.ContainsKey(knownColor))
{
_cachedKnownColorNames.Add(knownColor, GetDisplayName(knownColor));
}
}
}
}
// Find the closest known color by finding nearest Hue
// Since Hue is the best measure of human perception of the color itself
// it is not necessary to check other components (Saturation, Value).
var closestKnownColor = KnownColor.None;
var closestKnownColorDistance = double.PositiveInfinity;
var knownColors = (KnownColor[])Enum.GetValues(typeof(KnownColor));
var closestKnownColorHueDiff = double.PositiveInfinity;
for (int i = 1; i < knownColors.Length; i++) // Skip 'None'
lock (_knownColorCacheMutex)
{
// Transparent is skipped since alpha is ignored making it equivalent to White
if (knownColors[i] != KnownColor.Transparent)
foreach (var hueEntry in _cachedKnownColorHues)
{
Color knownColor = KnownColors.ToColor(knownColors[i]);
// Closest hue before or after is allowed
// Therefore, use an absolute value
double difference = Math.Abs(hsvColor.H - hueEntry.Value);
double distance = Math.Sqrt(
Math.Pow((double)(roundedColor.R - knownColor.R), 2.0) +
Math.Pow((double)(roundedColor.G - knownColor.G), 2.0) +
Math.Pow((double)(roundedColor.B - knownColor.B), 2.0));
if (distance < closestKnownColorDistance)
if (difference < closestKnownColorHueDiff)
{
closestKnownColor = knownColors[i];
closestKnownColorDistance = distance;
closestKnownColor = hueEntry.Key;
closestKnownColorHueDiff = difference;
}
}
}
@ -113,26 +170,19 @@ namespace Avalonia.Controls.Primitives
// Cache results for next time as well
if (closestKnownColor != KnownColor.None)
{
var sb = StringBuilderCache.Acquire();
string name = closestKnownColor.ToString();
string displayName;
// Add spaces converting PascalCase to human-readable names
for (int i = 0; i < name.Length; i++)
lock (_knownColorCacheMutex)
{
if (i != 0 &&
char.IsUpper(name[i]))
if (!_cachedKnownColorNames.TryGetValue(closestKnownColor, out displayName))
{
sb.Append(' ');
displayName = GetDisplayName(closestKnownColor);
}
sb.Append(name[i]);
}
string displayName = StringBuilderCache.GetStringAndRelease(sb);
lock (cacheMutex)
lock (_displayNameCacheMutex)
{
cachedDisplayNames.Add(roundedColor, displayName);
_cachedDisplayNames.Add(roundedColor, displayName);
}
return displayName;
@ -142,5 +192,35 @@ namespace Avalonia.Controls.Primitives
return string.Empty;
}
}
/// <summary>
/// Gets the human-readable display name for the given <see cref="KnownColor"/>.
/// </summary>
/// <remarks>
/// This currently uses the <see cref="KnownColor"/> enum value's C# name directly
/// which limits it to the EN language only. In the future this should be localized
/// to other cultures.
/// </remarks>
/// <param name="knownColor">The <see cref="KnownColor"/> to get the display name for.</param>
/// <returns>The human-readable display name for the given <see cref="KnownColor"/>.</returns>
private static string GetDisplayName(KnownColor knownColor)
{
var sb = StringBuilderCache.Acquire();
string name = knownColor.ToString();
// Add spaces converting PascalCase to human-readable names
for (int i = 0; i < name.Length; i++)
{
if (i != 0 &&
char.IsUpper(name[i]))
{
sb.Append(' ');
}
sb.Append(name[i]);
}
return StringBuilderCache.GetStringAndRelease(sb);
}
}
}

Loading…
Cancel
Save