1 // Written in the D programming language. 2 3 /** 4 This module implements $(LINK2 https://en.wikipedia.org/wiki/HSL_and_HSV, HSV), 5 $(LINK2 https://en.wikipedia.org/wiki/HSL_and_HSV, HSL), 6 $(LINK2 https://en.wikipedia.org/wiki/HSL_and_HSV, HSI), 7 $(LINK2 https://en.wikipedia.org/wiki/HSL_and_HSV, HCY), 8 $(LINK2 https://en.wikipedia.org/wiki/HWB_color_model, HWB), 9 $(LINK2 https://www.npmjs.com/package/hcg-color, HCG) _color types. 10 11 This family of _color spaces represent various cylindrical mappings of the RGB color space. 12 13 Authors: Manu Evans 14 Copyright: Copyright (c) 2015, Manu Evans. 15 License: $(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0) 16 Source: $(PHOBOSSRC std/experimental/color/_hsx.d) 17 */ 18 module std.experimental.color.hsx; 19 20 import std.experimental.color; 21 import std.experimental.color.rgb; 22 import std.experimental.color.colorspace : RGBColorSpace, RGBColorSpaceDesc, rgbColorSpaceDef; 23 import std.experimental.normint; 24 25 import std.traits : isInstanceOf, isFloatingPoint, isUnsigned, Unqual; 26 import std.typetuple : TypeTuple; 27 import std.math : PI; 28 29 @safe pure nothrow @nogc: 30 31 /** 32 Detect whether $(D_INLINECODE T) is a member of the HSx color family. 33 */ 34 enum isHSx(T) = isInstanceOf!(HSx, T); 35 36 /// 37 unittest 38 { 39 static assert(isHSx!(HSV!ushort) == true); 40 static assert(isHSx!RGB8 == false); 41 static assert(isHSx!string == false); 42 } 43 44 /** 45 Alias for a HSV (HSB) color. 46 */ 47 alias HSV(CT = float, RGBColorSpace cs = RGBColorSpace.sRGB) = HSx!(HSxType.HSV, CT, cs); 48 49 /** 50 Alias for a HSL color. 51 */ 52 alias HSL(CT = float, RGBColorSpace cs = RGBColorSpace.sRGB) = HSx!(HSxType.HSL, CT, cs); 53 54 /** 55 Alias for a HSI color. 56 */ 57 alias HSI(CT = float, RGBColorSpace cs = RGBColorSpace.sRGB) = HSx!(HSxType.HSI, CT, cs); 58 59 /** 60 Alias for a HCY' color. 61 */ 62 alias HCY(CT = float, RGBColorSpace cs = RGBColorSpace.sRGB) = HSx!(HSxType.HCY, CT, cs); 63 64 /** 65 Alias for a HWB color. 66 */ 67 alias HWB(CT = float, RGBColorSpace cs = RGBColorSpace.sRGB) = HSx!(HSxType.HWB, CT, cs); 68 69 /** 70 Alias for a HCG color. 71 */ 72 alias HCG(CT = float, RGBColorSpace cs = RGBColorSpace.sRGB) = HSx!(HSxType.HCG, CT, cs); 73 74 /** 75 Define a HSx family color type. 76 */ 77 enum HSxType 78 { 79 /** Hue-saturation-value (aka HSB: Hue-saturation-brightness) */ 80 HSV, 81 /** Hue-saturation-lightness */ 82 HSL, 83 /** Hue-saturation-intensity */ 84 HSI, 85 /** Hue-chroma-luma */ 86 HCY, 87 /** Hue-white-black */ 88 HWB, 89 /** Hue-chroma-grey */ 90 HCG 91 } 92 93 /** 94 HSx color space is used to describe a suite of angular color spaces including HSL, HSV, HSI, HCY. 95 96 Params: type_ = A type from the HSxType enum. 97 ComponentType_ = Type for the color channels. May be unsigned integer or floating point type. 98 colorSpace_ = Color will be within the specified RGB color space. 99 */ 100 struct HSx(HSxType type_, ComponentType_ = float, RGBColorSpace colorSpace_ = RGBColorSpace.sRGB) if (isFloatingPoint!ComponentType_ || isUnsigned!ComponentType_) 101 { 102 @safe pure nothrow @nogc: 103 104 static if (isFloatingPoint!ComponentType_) 105 { 106 /** Type of the hue components. */ 107 alias HueType = ComponentType_; 108 /** Type of the s and x components. */ 109 alias ComponentType = ComponentType_; 110 } 111 else 112 { 113 /** Type of the hue components. */ 114 alias HueType = ComponentType_; 115 /** Type of the s and x components. */ 116 alias ComponentType = NormalizedInt!ComponentType_; 117 } 118 119 /** The parent RGB color space. */ 120 enum colorSpace = colorSpace_; 121 /** The parent RGB color space descriptor. */ 122 enum RGBColorSpaceDesc!F colorSpaceDesc(F = double) = rgbColorSpaceDef!F(colorSpace_); 123 /** The color type from the HSx family. */ 124 enum HSxType type = type_; 125 126 // mixin the color channels according to the type 127 mixin("HueType " ~ Components!type[0] ~ " = 0;"); 128 mixin("ComponentType " ~ Components!type[1] ~ " = 0;"); 129 mixin("ComponentType " ~ Components!type[2] ~ " = 0;"); 130 131 /** Get hue angle in degrees. */ 132 @property double degrees() const 133 { 134 static if (!isFloatingPoint!ComponentType_) 135 return h * (360/(ComponentType_.max + 1.0)); 136 else 137 return (h < 0 ? 1 + h%1 : h%1) * 360; 138 } 139 /** Set hue angle in degrees. */ 140 @property void degrees(double angle) 141 { 142 static if (!isFloatingPoint!ComponentType_) 143 h = cast(ComponentType_)(angle * ((ComponentType_.max + 1.0)/360)); 144 else 145 h = angle * 1.0/360; 146 } 147 148 /** Get hue angle in radians. */ 149 @property double radians() const 150 { 151 static if (!isFloatingPoint!ComponentType_) 152 return h * ((PI*2)/(ComponentType_.max + 1.0)); 153 else 154 return (h < 0 ? 1 + h%1 : h%1) * (PI*2); 155 } 156 /** Set hue angle in radians. */ 157 @property void radians(double angle) 158 { 159 static if (!isFloatingPoint!ComponentType_) 160 h = cast(ComponentType_)(angle * ((ComponentType_.max + 1.0)/(PI*2))); 161 else 162 h = angle * 1.0/(PI*2); 163 } 164 165 /** Construct a color from hsx components. */ 166 this(HueType h, ComponentType s, ComponentType x) 167 { 168 mixin("this." ~ Components!type[0] ~ " = h;"); 169 mixin("this." ~ Components!type[1] ~ " = s;"); 170 mixin("this." ~ Components!type[2] ~ " = x;"); 171 } 172 173 static if (!isFloatingPoint!ComponentType_) 174 { 175 /** Construct a color from hsx components. */ 176 this(HueType h, ComponentType.IntType s, ComponentType.IntType x) 177 { 178 mixin("this." ~ Components!type[0] ~ " = h;"); 179 mixin("this." ~ Components!type[1] ~ " = ComponentType(s);"); 180 mixin("this." ~ Components!type[2] ~ " = ComponentType(x);"); 181 } 182 } 183 184 /** 185 Cast to other color types. 186 187 This cast is a convenience which simply forwards the call to convertColor. 188 */ 189 Color opCast(Color)() const if (isColor!Color) 190 { 191 return convertColor!Color(this); 192 } 193 194 195 package: 196 197 alias ParentColor = RGB!("rgb", ComponentType_, false, colorSpace_); 198 199 static To convertColorImpl(To, From)(From color) if (isHSx!From && isHSx!To) 200 { 201 // HACK: cast through RGB (this works fine, but could be faster) 202 return convertColorImpl!(To)(convertColorImpl!(From.ParentColor)(color)); 203 } 204 unittest 205 { 206 static assert(convertColorImpl!(HSL!float)(HSV!float(1.0/6, 1, 1)) == HSL!float(1.0/6, 1, 0.5)); 207 208 static assert(convertColorImpl!(HSV!float)(HSL!float(1.0/6, 1, 0.5)) == HSV!float(1.0/6, 1, 1)); 209 210 static assert(convertColorImpl!(HSI!float)(HSV!float(0, 1, 1)) == HSI!float(0, 1, 1.0/3)); 211 static assert(convertColorImpl!(HSI!float)(HSV!float(1.0/6, 1, 1)) == HSI!float(1.0/6, 1, 2.0/3)); 212 213 // TODO: HCY (needs approx ==) 214 } 215 216 static To convertColorImpl(To, From)(From color) if (isHSx!From && isRGB!To) 217 { 218 import std.math : abs; 219 220 alias ToType = To.ComponentType; 221 alias WT = FloatTypeFor!ToType; 222 223 auto c = color.tupleof; 224 WT h = cast(WT)color.degrees; 225 WT s = cast(WT)c[1]; 226 WT x = cast(WT)c[2]; 227 228 static if (isFloatingPoint!ComponentType_) 229 { 230 // clamp s and x 231 import std.algorithm.comparison : clamp; 232 s = clamp(s, 0, 1); 233 x = clamp(x, 0, 1); 234 } 235 236 WT C, m; 237 static if (From.type == HSxType.HSV) 238 { 239 C = x*s; 240 m = x - C; 241 } 242 else static if (From.type == HSxType.HSL) 243 { 244 C = (1 - abs(2*x - 1))*s; 245 m = x - C/2; 246 } 247 else static if (From.type == HSxType.HSI) 248 { 249 C = s; 250 } 251 else static if (From.type == HSxType.HCY) 252 { 253 C = s; 254 } 255 else static if (From.type == HSxType.HWB) 256 { 257 WT t = s + x; 258 if (t > 1) 259 { 260 // normalise W/B 261 s /= t; 262 x /= t; 263 } 264 s = x == 1 ? 0 : 1 - (s / (1 - x)); // saturation 265 x = 1 - x; // 'value' 266 267 C = x*s; 268 m = x - C; 269 } 270 else static if (From.type == HSxType.HCG) 271 { 272 C = s; 273 m = x * (1 - C); 274 } 275 276 WT H = h/60; 277 WT X = C*(1 - abs(H%2.0 - 1)); 278 279 WT r, g, b; 280 if (H < 1) 281 r = C, g = X, b = 0; 282 else if (H < 2) 283 r = X, g = C, b = 0; 284 else if (H < 3) 285 r = 0, g = C, b = X; 286 else if (H < 4) 287 r = 0, g = X, b = C; 288 else if (H < 5) 289 r = X, g = 0, b = C; 290 else if (H < 6) 291 r = C, g = 0, b = X; 292 293 static if (From.type == HSxType.HSI) 294 { 295 m = x - (r+g+b)*WT(1.0/3.0); 296 } 297 else static if (From.type == HSxType.HCY) 298 { 299 m = x - toGrayscale!(false, colorSpace_, WT)(r, g, b); // Derive from Luma' 300 } 301 302 return To(cast(ToType)(r+m), cast(ToType)(g+m), cast(ToType)(b+m)); 303 } 304 unittest 305 { 306 static assert(convertColorImpl!(RGB8)(HSV!float(0, 1, 1)) == RGB8(255, 0, 0)); 307 static assert(convertColorImpl!(RGB8)(HSV!float(1.0/6, 0.5, 0.5)) == RGB8(128, 128, 64)); 308 309 static assert(convertColorImpl!(RGB8)(HSL!float(0, 1, 0.5)) == RGB8(255, 0, 0)); 310 static assert(convertColorImpl!(RGB8)(HSL!float(1.0/6, 0.5, 0.5)) == RGB8(191, 191, 64)); 311 } 312 313 static To convertColorImpl(To, From)(From color) if (isRGB!From && isHSx!To) 314 { 315 import std.algorithm : min, max, clamp; 316 import std.math : abs; 317 318 alias ToType = To.ComponentType; 319 alias WT = FloatTypeFor!ToType; 320 321 auto c = color.tristimulus; 322 WT r = cast(WT)c[0]; 323 WT g = cast(WT)c[1]; 324 WT b = cast(WT)c[2]; 325 326 static if (isFloatingPoint!ComponentType_) 327 { 328 // clamp r, g, b 329 r = clamp(r, 0, 1); 330 g = clamp(g, 0, 1); 331 b = clamp(b, 0, 1); 332 } 333 334 WT M = max(r, g, b); 335 WT m = min(r, g, b); 336 WT C = M-m; 337 338 // Calculate Hue 339 WT h; 340 if (C == 0) 341 h = 0; 342 else if (M == r) 343 h = WT(1.0/6) * ((g-b)/C % WT(6)); 344 else if (M == g) 345 h = WT(1.0/6) * ((b-r)/C + WT(2)); 346 else if (M == b) 347 h = WT(1.0/6) * ((r-g)/C + WT(4)); 348 349 WT s, x; 350 static if (To.type == HSxType.HSV) 351 { 352 x = M; // 'Value' 353 s = x == 0 ? WT(0) : C/x; // Saturation 354 } 355 else static if (To.type == HSxType.HSL) 356 { 357 x = (M + m)/WT(2); // Lightness 358 s = (x == 0 || x == 1) ? WT(0) : C/(1 - abs(2*x - 1)); // Saturation 359 } 360 else static if (To.type == HSxType.HSI) 361 { 362 x = (r + g + b)/WT(3); // Intensity 363 s = x == 0 ? WT(0) : 1 - m/x; // Saturation 364 } 365 else static if (To.type == HSxType.HCY) 366 { 367 x = toGrayscale!(false, colorSpace_, WT)(r, g, b); // Calculate Luma' using the proper coefficients 368 s = C; // Chroma 369 } 370 else static if (To.type == HSxType.HWB) 371 { 372 s = M == 0 ? WT(0) : C/M; // Saturation 373 s = (1 - s)*M; // White 374 x = 1 - M; // Black 375 } 376 else static if (To.type == HSxType.HCG) 377 { 378 s = C; 379 x = m / (1 - C); 380 } 381 382 static if (!isFloatingPoint!ToType) 383 h = h * WT(ToType.max + 1.0); 384 385 return To(cast(ToType)h, cast(ToType)s, cast(ToType)x); 386 } 387 unittest 388 { 389 static assert(convertColorImpl!(HSV!float)(RGB8(255, 0, 0)) == HSV!float(0, 1, 1)); 390 static assert(convertColorImpl!(HSL!float)(RGB8(255, 0, 0)) == HSL!float(0, 1, 0.5)); 391 static assert(convertColorImpl!(HSI!float)(RGB8(255, 0, 0)) == HSI!float(0, 1, 1.0/3)); 392 static assert(convertColorImpl!(HSI!float)(RGB8(255, 255, 0)) == HSI!float(1.0/6, 1, 2.0/3)); 393 // static assert(convertColorImpl!(HCY!float)(RGB8(255, 0, 0)) == HCY!float(0, 1, 1)); 394 } 395 396 private: 397 template Components(HSxType type) 398 { 399 static if (type == HSxType.HSV) 400 alias Components = TypeTuple!("h","s","v"); 401 else static if (type == HSxType.HSL) 402 alias Components = TypeTuple!("h","s","l"); 403 else static if (type == HSxType.HSI) 404 alias Components = TypeTuple!("h","s","i"); 405 else static if (type == HSxType.HCY) 406 alias Components = TypeTuple!("h","c","y"); 407 else static if (type == HSxType.HWB) 408 alias Components = TypeTuple!("h","w","b"); 409 else static if (type == HSxType.HCG) 410 alias Components = TypeTuple!("h","c","g"); 411 } 412 alias AllComponents = Components!type_; 413 } 414 415 /// 416 unittest 417 { 418 // HSV color with float components 419 alias HSVf = HSV!float; 420 421 HSVf c = HSVf(3.1415, 1, 0.5); 422 423 // test HSV operators and functions 424 } 425 /// 426 unittest 427 { 428 // HSL color with float components 429 alias HSLf = HSL!float; 430 431 HSLf c = HSLf(3.1415, 1, 0.5); 432 433 // test HSL operators and functions 434 } 435 /// 436 unittest 437 { 438 // HSI color with float components 439 alias HSIf = HSI!float; 440 441 HSIf c = HSIf(3.1415, 1, 0.5); 442 443 // test HSI operators and functions 444 } 445 /// 446 unittest 447 { 448 // HCY color with float components 449 alias HCYf = HCY!float; 450 451 HCYf c = HCYf(3.1415, 1, 0.5); 452 453 // test HCY operators and functions 454 }