diff --git a/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs b/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs index c1a03b1b77..0c47a8ed3f 100644 --- a/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs +++ b/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 /// public static class ColorHelper { - private static readonly Dictionary cachedDisplayNames = new Dictionary(); - private static readonly object cacheMutex = new object(); + private static readonly Dictionary _cachedDisplayNames = new Dictionary(); + private static readonly Dictionary _cachedKnownColorHues = new Dictionary(); + private static readonly Dictionary _cachedKnownColorNames = new Dictionary(); + private static readonly object _displayNameCacheMutex = new object(); + private static readonly object _knownColorCacheMutex = new object(); /// /// 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; } } + + /// + /// Gets the human-readable display name for the given . + /// + /// + /// This currently uses the enum value's C# name directly + /// which limits it to the EN language only. In the future this should be localized + /// to other cultures. + /// + /// The to get the display name for. + /// The human-readable display name for the given . + 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); + } } }