using System; using System.Globalization; using System.Collections.Generic; using Avalonia.Media; using System.Text; namespace Avalonia.Controls.Primitives { /// /// Contains helpers useful when working with colors. /// public static class ColorHelper { private static readonly Dictionary cachedDisplayNames = new Dictionary(); private static readonly object cacheMutex = new object(); /// /// Gets the relative (perceptual) luminance/brightness of the given color. /// 1 is closer to white while 0 is closer to black. /// /// The color to calculate relative luminance for. /// The relative (perceptual) luminance/brightness of the given color. public static double GetRelativeLuminance(Color color) { // The equation for relative luminance is given by // // L = 0.2126 * Rg + 0.7152 * Gg + 0.0722 * Bg // // where Xg = { X/3294 if X <= 10, (R/269 + 0.0513)^2.4 otherwise } // // If L is closer to 1, then the color is closer to white; if it is closer to 0, // then the color is closer to black. This is based on the fact that the human // eye perceives green to be much brighter than red, which in turn is perceived to be // brighter than blue. double rg = color.R <= 10 ? color.R / 3294.0 : Math.Pow(color.R / 269.0 + 0.0513, 2.4); double gg = color.G <= 10 ? color.G / 3294.0 : Math.Pow(color.G / 269.0 + 0.0513, 2.4); double bg = color.B <= 10 ? color.B / 3294.0 : Math.Pow(color.B / 269.0 + 0.0513, 2.4); return (0.2126 * rg + 0.7152 * gg + 0.0722 * bg); } /// /// Determines if color display names are supported based on the current thread culture. /// /// /// Only English names are currently supported following known color names. /// In the future known color names could be localized. /// public static bool ToDisplayNameExists { get => CultureInfo.CurrentUICulture.Name.StartsWith("EN", StringComparison.OrdinalIgnoreCase); } /// /// Determines an approximate display name for the given color. /// /// The color to get the display name for. /// The approximate color display name. public static string ToDisplayName(Color color) { // Without rounding, there are 16,777,216 possible RGB colors (without alpha). // This is too many to cache and search through for performance reasons. // It is also needlessly large as there are only ~140 known/named colors. // Therefore, rounding of the input color's component values is done to // reduce the color space into something more useful. double rounding = 5; var roundedColor = new Color( 0xFF, Convert.ToByte(Math.Round(color.R / rounding) * rounding), Convert.ToByte(Math.Round(color.G / rounding) * rounding), Convert.ToByte(Math.Round(color.B / rounding) * rounding)); // Attempt to use a previously cached display name lock (cacheMutex) { if (cachedDisplayNames.TryGetValue(roundedColor, out var displayName)) { return displayName; } } // Find the closest known color by measuring 3D Euclidean distance (ignore alpha) var closestKnownColor = KnownColor.None; var closestKnownColorDistance = double.PositiveInfinity; var knownColors = (KnownColor[])Enum.GetValues(typeof(KnownColor)); for (int i = 1; i < knownColors.Length; i++) // Skip 'None' { // Transparent is skipped since alpha is ignored making it equivalent to White if (knownColors[i] != KnownColor.Transparent) { Color knownColor = KnownColors.ToColor(knownColors[i]); 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) { closestKnownColor = knownColors[i]; closestKnownColorDistance = distance; } } } // Return the closest known color as the display name // Cache results for next time as well if (closestKnownColor != KnownColor.None) { var sb = StringBuilderCache.Acquire(); string name = closestKnownColor.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]); } string displayName = StringBuilderCache.GetStringAndRelease(sb); lock (cacheMutex) { cachedDisplayNames.Add(roundedColor, displayName); } return displayName; } else { return string.Empty; } } } }