ColorNameHelper.cs
1 // Copyright (c) Microsoft Corporation 2 // The Microsoft Corporation licenses this file to you under the MIT license. 3 // See the LICENSE file in the project root for more information. 4 5 using System; 6 using System.Drawing; 7 8 namespace ManagedCommon 9 { 10 public static class ColorNameHelper 11 { 12 public const float DBLEPSILON = 2.2204460492503131e-16f; 13 14 // For the purposes of naming colors, there are four steps that we go through. 15 // 16 // 1. For numerical ease, we convert the HSL values from the range [0, 1] 17 // to the range [0, 255]. 18 // 19 // 2. If luminosity is sufficiently high or low (> 240 or < 20), or if 20 // saturation is sufficiently low (<= 20), then we declare that we're in the 21 // achromatic range. In this case, we return either white, black, or three 22 // different shades of gray (depending on luminosity). 23 // 24 // 3. If we do have a chromatic color, the first thing we need to determine 25 // about it is what the hue limits are for its saturation value - at different 26 // levels of saturation, we have different hue values that we'll consider the 27 // boundaries for different classes of named colors. The hue limits for various 28 // saturation values are as below. 29 // 30 // The numbers correspond to the following color buckets, with 0 meaning that 31 // that bucket does not apply to the given saturation value: 32 // 33 // 1 - coral, 2 - red, 3 - orange, 4 - brown, 5 - tan, 6 - gold, 7 - yellow, 8 - olive green (with brown), 34 // 9 - olive green (with green) 10 - lime green, 11 - green - 12 - bright green 13 - teal, 14 - aqua, 35 // 15 - turquoise, 16 - pale blue, 17 - blue, 18 - blue-gray, 19 - indigo, 20 - purple, 21 - pink, 22 - brown, 23 - red 36 private static int[] hueLimitsForSatLevel1 = // Sat: 20-75 37 { 38 // 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23 39 8, 0, 0, 44, 0, 0, 0, 63, 0, 0, 122, 0, 134, 0, 0, 0, 0, 166, 176, 241, 0, 256, 0, 40 }; 41 42 private static int[] hueLimitsForSatLevel2 = // Sat: 75-115 43 { 44 // 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23 45 0, 10, 0, 32, 46, 0, 0, 0, 61, 0, 106, 0, 136, 144, 0, 0, 0, 158, 166, 241, 0, 0, 256, 46 }; 47 48 private static int[] hueLimitsForSatLevel3 = // Sat: 115-150 49 { 50 // 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23 51 0, 8, 0, 0, 39, 46, 0, 0, 0, 71, 120, 0, 131, 144, 0, 0, 163, 0, 177, 211, 249, 0, 256, 52 }; 53 54 private static int[] hueLimitsForSatLevel4 = // Sat: 150-240 55 { 56 // 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23 57 0, 11, 26, 0, 0, 38, 45, 0, 0, 56, 100, 121, 129, 0, 140, 0, 180, 0, 0, 224, 241, 0, 256, 58 }; 59 60 private static int[] hueLimitsForSatLevel5 = // Sat: 240-255 61 { 62 // 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23 63 0, 13, 27, 0, 0, 36, 45, 0, 0, 59, 118, 0, 127, 136, 142, 0, 185, 0, 0, 216, 239, 0, 256, 64 }; 65 66 // 4. Once we have the color bucket, next we have three sub-buckets that we need to worry about, 67 // corresponding to three different levels of luminosity. For example, if we're in the "blue" bucket, 68 // that might correspond to light blue, blue, or dark blue, depending on luminosity. 69 // For each bucket, the luminosity cutoffs for the purposes of discerning between light, mid, and dark colors 70 // are different, so we define luminosity limits for low and high luminosity for each bucket, as follows: 71 private static int[] lumLimitsForHueIndexLow = 72 { 73 // 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23 74 130, 100, 115, 100, 100, 100, 110, 75, 100, 90, 100, 100, 100, 100, 80, 100, 100, 100, 100, 100, 100, 100, 100, 75 }; 76 77 private static int[] lumLimitsForHueIndexHigh = 78 { 79 // 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23 80 170, 170, 170, 155, 170, 170, 170, 170, 170, 115, 170, 170, 170, 170, 170, 170, 170, 170, 150, 150, 170, 140, 165, 81 }; 82 83 // 5. Finally, once we have a luminosity sub-bucket in the saturation color bucket, we have everything we need 84 // to retrieve a name. For each of the 23 buckets, we have names associated with light, mid, and dark variations 85 // of that color, which are defined as follows: 86 private static string[] colorNamesLight = 87 { 88 "TEXT_COLOR_CORAL", 89 "TEXT_COLOR_ROSE", 90 "TEXT_COLOR_LIGHTORANGE", 91 "TEXT_COLOR_TAN", 92 "TEXT_COLOR_TAN", 93 "TEXT_COLOR_LIGHTYELLOW", 94 "TEXT_COLOR_LIGHTYELLOW", 95 "TEXT_COLOR_TAN", 96 "TEXT_COLOR_LIGHTGREEN", 97 "TEXT_COLOR_LIME", 98 "TEXT_COLOR_LIGHTGREEN", 99 "TEXT_COLOR_LIGHTGREEN", 100 "TEXT_COLOR_AQUA", 101 "TEXT_COLOR_SKYBLUE", 102 "TEXT_COLOR_LIGHTTURQUOISE", 103 "TEXT_COLOR_PALEBLUE", 104 "TEXT_COLOR_LIGHTBLUE", 105 "TEXT_COLOR_ICEBLUE", 106 "TEXT_COLOR_PERIWINKLE", 107 "TEXT_COLOR_LAVENDER", 108 "TEXT_COLOR_PINK", 109 "TEXT_COLOR_TAN", 110 "TEXT_COLOR_ROSE", 111 }; 112 113 private static string[] colorNamesMid = 114 { 115 "TEXT_COLOR_CORAL", 116 "TEXT_COLOR_RED", 117 "TEXT_COLOR_ORANGE", 118 "TEXT_COLOR_BROWN", 119 "TEXT_COLOR_TAN", 120 "TEXT_COLOR_GOLD", 121 "TEXT_COLOR_YELLOW", 122 "TEXT_COLOR_OLIVEGREEN", 123 "TEXT_COLOR_OLIVEGREEN", 124 "TEXT_COLOR_GREEN", 125 "TEXT_COLOR_GREEN", 126 "TEXT_COLOR_BRIGHTGREEN", 127 "TEXT_COLOR_TEAL", 128 "TEXT_COLOR_AQUA", 129 "TEXT_COLOR_TURQUOISE", 130 "TEXT_COLOR_PALEBLUE", 131 "TEXT_COLOR_BLUE", 132 "TEXT_COLOR_BLUEGRAY", 133 "TEXT_COLOR_INDIGO", 134 "TEXT_COLOR_PURPLE", 135 "TEXT_COLOR_PINK", 136 "TEXT_COLOR_BROWN", 137 "TEXT_COLOR_RED", 138 }; 139 140 private static string[] colorNamesDark = 141 { 142 "TEXT_COLOR_BROWN", 143 "TEXT_COLOR_DARKRED", 144 "TEXT_COLOR_BROWN", 145 "TEXT_COLOR_BROWN", 146 "TEXT_COLOR_BROWN", 147 "TEXT_COLOR_DARKYELLOW", 148 "TEXT_COLOR_DARKYELLOW", 149 "TEXT_COLOR_BROWN", 150 "TEXT_COLOR_DARKGREEN", 151 "TEXT_COLOR_DARKGREEN", 152 "TEXT_COLOR_DARKGREEN", 153 "TEXT_COLOR_DARKGREEN", 154 "TEXT_COLOR_DARKTEAL", 155 "TEXT_COLOR_DARKTEAL", 156 "TEXT_COLOR_DARKTEAL", 157 "TEXT_COLOR_DARKBLUE", 158 "TEXT_COLOR_DARKBLUE", 159 "TEXT_COLOR_BLUEGRAY", 160 "TEXT_COLOR_INDIGO", 161 "TEXT_COLOR_DARKPURPLE", 162 "TEXT_COLOR_PLUM", 163 "TEXT_COLOR_BROWN", 164 "TEXT_COLOR_DARKRED", 165 }; 166 167 public static string GetColorNameIdentifier(Color color) 168 { 169 var (hue, sat, lum) = ColorFormatHelper.ConvertToHSLColor(color); 170 171 hue = (hue == 0 ? 0 : hue / 360) * 255; // this implementation is using normalization to 0-255 instead of 0-360° 172 sat = sat * 255; 173 lum = lum * 255; 174 175 // First, if we're in the achromatic state, return the appropriate achromatic color name. 176 if (lum > 240) 177 { 178 return "TEXT_COLOR_WHITE"; 179 } 180 else if (lum < 20) 181 { 182 return "TEXT_COLOR_BLACK"; 183 } 184 185 if (sat <= 20) 186 { 187 if (lum > 170) 188 { 189 return "TEXT_COLOR_LIGHTGRAY"; 190 } 191 else if (lum > 100) 192 { 193 return "TEXT_COLOR_GRAY"; 194 } 195 else 196 { 197 return "TEXT_COLOR_DARKGRAY"; 198 } 199 } 200 201 // If we have a chromatic color, we need to first get the hue limits for the saturation value. 202 int[] pHueLimits; 203 if (sat > 20 && sat <= 75) 204 { 205 pHueLimits = hueLimitsForSatLevel1; 206 } 207 else if (sat > 75 && sat <= 115) 208 { 209 pHueLimits = hueLimitsForSatLevel2; 210 } 211 else if (sat > 115 && sat <= 150) 212 { 213 pHueLimits = hueLimitsForSatLevel3; 214 } 215 else if (sat > 150 && sat <= 240) 216 { 217 pHueLimits = hueLimitsForSatLevel4; 218 } 219 else 220 { 221 pHueLimits = hueLimitsForSatLevel5; 222 } 223 224 // Now that we have that, we can get the color index, which represents which 225 // of the 23 buckets we're located in. 226 int colorIndex = -1; 227 for (int i = 0; i < colorNamesMid.Length; ++i) 228 { 229 if (hue < pHueLimits[i]) 230 { 231 colorIndex = i; 232 break; 233 } 234 } 235 236 // Assuming we got a color index (and we always should get one), then next we need to 237 // figure out which luminosity sub-bucket we're located in. 238 // Once we have that, we'll return the color name from the appropriate array. 239 if (colorIndex != -1) 240 { 241 if (lum > lumLimitsForHueIndexHigh[colorIndex]) 242 { 243 return colorNamesLight[colorIndex]; 244 } 245 else if (lum < lumLimitsForHueIndexLow[colorIndex]) 246 { 247 return colorNamesDark[colorIndex]; 248 } 249 else 250 { 251 return colorNamesMid[colorIndex]; 252 } 253 } 254 255 return string.Empty; 256 } 257 258 public static bool AreClose(double a, double b) 259 { 260 return (float)Math.Abs(a - b) <= DBLEPSILON * (float)Math.Abs(a); 261 } 262 } 263 }