1 /**
2     Monomorphic color type, to represent any tristimulus + alpha.
3 
4     Copyright: Copyright Guillaume Piolat 2023-2024.
5     License:   $(LINK2 http://www.boost.org/LICENSE_1_0.txt, BSL-1.0)
6 */
7 module colors.types;
8 
9 //import std.math;
10 import colors.conversions;
11 import colors.colorspace;
12 import colors.parser;
13 
14 
15 pure nothrow @nogc @safe:
16 
17 
18 /**
19     The Color type is a tagged union that can hold one color in a
20     CSS-defined colorspace. This correspond to both "specified",
21     "computed", and "used" colors in CSS specification.
22     In typical use, you'll want to map to sRGB 32-bit RGBA quadruplet,
23     and the function for this is called: `toRGBA8()`.
24     Reference: https://www.w3.org/TR/css-color-4/
25 */
26 struct Color
27 {
28 pure nothrow @nogc @safe:
29 
30     // Note: accessing these representation is dangerous, only one of
31     // them is meaningful according to `colorspace`.
32     union
33     {
34         // Efficient representation go there:
35         RGBA8 _RGBA8;
36         RGBA16 _RGBA16;
37 
38         // CSS spec recommends:
39         // "(16bit, half-float, or float per component is recommended
40         // for internal storage).
41         // Values must be rounded towards +∞, not truncated."
42         // CSS-compatible types below, with more precision.
43         // Note that the CSS types can contain NaN, unlike the
44         // efficient representations, and it's meaningful.
45 
46         RGBAf _RGBAf;
47         HSLAf _HSLAf;
48     }
49 
50     /**
51         Build from CSS color string.
52     */
53     this(const(char)[] cssColor)
54     {
55         this = color(cssColor);
56     }
57 
58     /**
59         Build from a 8-bit sRGB quadruplet.
60     */
61     this(RGBA8 c)
62     {
63         this._RGBA8 = c;
64         this._colorspace = Colorspace.rgba8;
65     }
66 
67     /**
68         Build from a 16-bit sRGB quadruplet.
69     */
70     this(RGBA16 c)
71     {
72         this._RGBA16 = c;
73         this._colorspace = Colorspace.rgba16;
74     }
75 
76 
77     /**
78         Build from a 32-bit float sRGB quadruplet.
79     */
80     this(RGBAf c)
81     {
82         this._RGBAf = c;
83         this._colorspace = Colorspace.rgbaf32;
84     }
85 
86     /**
87         Get colorspace tag. This is the tag of this tagged union.
88     */
89     Colorspace colorspace() const
90     {
91         return _colorspace;
92     }
93 
94     /**
95         Unsafe cast of colorspace. Normally you never need this.
96     */
97     void assumeColorspace(Colorspace colorspace) @system
98     {
99         _colorspace = colorspace;
100     }
101 
102     /**
103         Returns: A 8-bit tristimulus sRGB color, with alpha.
104     */
105     RGBA8 toRGBA8() const
106     {
107         Color c = this.toColorSpace(Colorspace.rgba8);
108         return c._RGBA8;
109     }
110 
111     /**
112         Returns: A 16-bit tristimulus sRGB color, with alpha.
113     */
114     RGBA16 toRGBA16() const
115     {
116         Color c = this.toColorSpace(Colorspace.rgba16);
117         return c._RGBA16;
118     }
119 
120     /**
121         A 32-bit float normalized tristimulus sRGB color, with alpha.
122     */
123     RGBAf toRGBAf() const
124     {
125         Color c = this.toColorSpace(Colorspace.rgbaf32);
126         return c._RGBAf;
127     }
128 
129 private:
130     /**
131         Colorspace in the type. Which means `Color` is not meant for
132         storage, but for intermediate computation and user experience.
133     */
134     Colorspace _colorspace;
135 }
136 unittest
137 {
138     Color c = Color("cyan");
139 }
140 
141 
142 /**
143     The `rgb` function is the same as in CSS color specifications.
144 
145     Expected values ranges from 0 to 255.
146 
147     Instead of being either numbers or percentages, all values here
148     are assumed to be numbers from 0 to 255. However, internally the
149     color will be stored with higher accuracy, as per CSS spec.
150 
151     Params:
152       red   = Red value in 0 to 255.0f.
153       green = Green value in 0 to 255.0f.
154       blue  = Blue value in 0 to 255.0f.
155       alpha = Alpha value in 0 to 1.0f (opacity).
156 */
157 Color rgb(float red, float green, float blue, float alpha = 1.0f)
158 {
159     clamp_0_255(red);
160     clamp_0_255(green);
161     clamp_0_255(blue);
162     clamp_0_1(alpha);
163     Color c;
164     c._RGBAf.r = red / 255.0f;
165     c._RGBAf.g = green / 255.0f;
166     c._RGBAf.b = blue / 255.0f;
167     c._RGBAf.a = alpha;
168     c._colorspace = Colorspace.rgbaf32;
169     return c;
170 }
171 
172 
173 /**
174     In CSS, the `rgba` function is simply in alias of `rgb`. You can
175     specify an alpha to `rgb`, or omit it with `rgba`.
176 */
177 alias rgba = rgb;
178 
179 
180 /**
181     The `hsl` function is the same as in CSS color specifications.
182 
183     Expected values ranges from 0 to 255.
184 
185     Params:
186       hueDegrees = Hue value (0 = red, 60 = yellow, 240 = blue).
187                  = Can wrap around.
188       sat        = Saturation value in 0 to 1.0f.
189       light      = Light value in 0 to 1.0f.
190       alpha      = Alpha value in 0 to 1.0f (opacity).
191 */
192 Color hsl(float hueDegrees,
193           float sat,
194           float light,
195           float alpha = 1.0f)
196 {
197     // TODO: should clamp or fmod hueDegrees here?
198     clamp_0_1(sat);
199     clamp_0_1(light);
200     clamp_0_1(alpha);
201     Color c;
202     c._HSLAf.h = hueDegrees,
203     c._HSLAf.s = sat;
204     c._HSLAf.l = light;
205     c._HSLAf.a = alpha;
206     c._colorspace = Colorspace.hslaf32;
207     return c;
208 }
209 
210 
211 /**
212     In CSS, the `hsla` function is simply in alias of `rgb`. You can
213     specify an alpha to `rgb`, or omit it with `rgba`.
214 */
215 alias hsla = hsl;
216 
217 
218 /**
219     Convert a `Color` in-place to another colorspace.
220 */
221 Color toColorSpace(const(Color) color, Colorspace target)
222 {
223     // CSS mandates that we preserve at least 16-bit of precision.
224 
225     Colorspace from = color.colorspace;
226 
227     if (target == Colorspace.unknown)
228     {
229         // Not supposed to happen, every Color are convertible to any
230         // other space (though the semantics could change).
231         assert(false);
232     }
233 
234     if (from == target)
235         return color; // already there
236 
237     Colorspace inter = getIntermediateColorspace(from, target);
238 
239     if (from == inter)
240     {
241         return convertFromIntermediate(color, target);
242     }
243     else if (target == inter)
244     {
245         return convertToIntermediate(color, target);
246     }
247     else
248     {
249         Color c = convertToIntermediate(color, inter);
250         return convertFromIntermediate(c, target);
251     }
252 }
253