The OpenD Programming Language

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 }