ColorHelper.cs 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. using System;
  2. using System.Globalization;
  3. using System.Collections.Generic;
  4. using Avalonia.Media;
  5. using System.Text;
  6. namespace Avalonia.Controls.Primitives
  7. {
  8. /// <summary>
  9. /// Contains helpers useful when working with colors.
  10. /// </summary>
  11. public static class ColorHelper
  12. {
  13. private static readonly Dictionary<Color, string> cachedDisplayNames = new Dictionary<Color, string>();
  14. private static readonly object cacheMutex = new object();
  15. /// <summary>
  16. /// Gets the relative (perceptual) luminance/brightness of the given color.
  17. /// 1 is closer to white while 0 is closer to black.
  18. /// </summary>
  19. /// <param name="color">The color to calculate relative luminance for.</param>
  20. /// <returns>The relative (perceptual) luminance/brightness of the given color.</returns>
  21. public static double GetRelativeLuminance(Color color)
  22. {
  23. // The equation for relative luminance is given by
  24. //
  25. // L = 0.2126 * Rg + 0.7152 * Gg + 0.0722 * Bg
  26. //
  27. // where Xg = { X/3294 if X <= 10, (R/269 + 0.0513)^2.4 otherwise }
  28. //
  29. // If L is closer to 1, then the color is closer to white; if it is closer to 0,
  30. // then the color is closer to black. This is based on the fact that the human
  31. // eye perceives green to be much brighter than red, which in turn is perceived to be
  32. // brighter than blue.
  33. double rg = color.R <= 10 ? color.R / 3294.0 : Math.Pow(color.R / 269.0 + 0.0513, 2.4);
  34. double gg = color.G <= 10 ? color.G / 3294.0 : Math.Pow(color.G / 269.0 + 0.0513, 2.4);
  35. double bg = color.B <= 10 ? color.B / 3294.0 : Math.Pow(color.B / 269.0 + 0.0513, 2.4);
  36. return (0.2126 * rg + 0.7152 * gg + 0.0722 * bg);
  37. }
  38. /// <summary>
  39. /// Determines if color display names are supported based on the current thread culture.
  40. /// </summary>
  41. /// <remarks>
  42. /// Only English names are currently supported following known color names.
  43. /// In the future known color names could be localized.
  44. /// </remarks>
  45. public static bool ToDisplayNameExists
  46. {
  47. get => CultureInfo.CurrentUICulture.Name.StartsWith("EN", StringComparison.OrdinalIgnoreCase);
  48. }
  49. /// <summary>
  50. /// Determines an approximate display name for the given color.
  51. /// </summary>
  52. /// <param name="color">The color to get the display name for.</param>
  53. /// <returns>The approximate color display name.</returns>
  54. public static string ToDisplayName(Color color)
  55. {
  56. // Without rounding, there are 16,777,216 possible RGB colors (without alpha).
  57. // This is too many to cache and search through for performance reasons.
  58. // It is also needlessly large as there are only ~140 known/named colors.
  59. // Therefore, rounding of the input color's component values is done to
  60. // reduce the color space into something more useful.
  61. double rounding = 5;
  62. var roundedColor = new Color(
  63. 0xFF,
  64. Convert.ToByte(Math.Round(color.R / rounding) * rounding),
  65. Convert.ToByte(Math.Round(color.G / rounding) * rounding),
  66. Convert.ToByte(Math.Round(color.B / rounding) * rounding));
  67. // Attempt to use a previously cached display name
  68. lock (cacheMutex)
  69. {
  70. if (cachedDisplayNames.TryGetValue(roundedColor, out var displayName))
  71. {
  72. return displayName;
  73. }
  74. }
  75. // Find the closest known color by measuring 3D Euclidean distance (ignore alpha)
  76. var closestKnownColor = KnownColor.None;
  77. var closestKnownColorDistance = double.PositiveInfinity;
  78. var knownColors = (KnownColor[])Enum.GetValues(typeof(KnownColor));
  79. for (int i = 1; i < knownColors.Length; i++) // Skip 'None'
  80. {
  81. // Transparent is skipped since alpha is ignored making it equivalent to White
  82. if (knownColors[i] != KnownColor.Transparent)
  83. {
  84. Color knownColor = KnownColors.ToColor(knownColors[i]);
  85. double distance = Math.Sqrt(
  86. Math.Pow((double)(roundedColor.R - knownColor.R), 2.0) +
  87. Math.Pow((double)(roundedColor.G - knownColor.G), 2.0) +
  88. Math.Pow((double)(roundedColor.B - knownColor.B), 2.0));
  89. if (distance < closestKnownColorDistance)
  90. {
  91. closestKnownColor = knownColors[i];
  92. closestKnownColorDistance = distance;
  93. }
  94. }
  95. }
  96. // Return the closest known color as the display name
  97. // Cache results for next time as well
  98. if (closestKnownColor != KnownColor.None)
  99. {
  100. var sb = StringBuilderCache.Acquire();
  101. string name = closestKnownColor.ToString();
  102. // Add spaces converting PascalCase to human-readable names
  103. for (int i = 0; i < name.Length; i++)
  104. {
  105. if (i != 0 &&
  106. char.IsUpper(name[i]))
  107. {
  108. sb.Append(' ');
  109. }
  110. sb.Append(name[i]);
  111. }
  112. string displayName = StringBuilderCache.GetStringAndRelease(sb);
  113. lock (cacheMutex)
  114. {
  115. cachedDisplayNames.Add(roundedColor, displayName);
  116. }
  117. return displayName;
  118. }
  119. else
  120. {
  121. return string.Empty;
  122. }
  123. }
  124. }
  125. }