1 /**
2  * Color type and operations.
3  *
4  * License:
5  *   This Source Code Form is subject to the terms of
6  *   the Mozilla Public License, v. 2.0. If a copy of
7  *   the MPL was not distributed with this file, You
8  *   can obtain one at http://mozilla.org/MPL/2.0/.
9  *
10  * Authors:
11  *   Vladimir Panteleev <vladimir@thecybershadow.net>
12  */
13 
14 module ae.utils.graphics.color;
15 
16 import std.traits;
17 
18 import ae.utils.math;
19 import ae.utils.meta;
20 
21 /// Instantiates to a color type.
22 /// FieldTuple is the color specifier, as parsed by
23 /// the FieldList template from ae.utils.meta.
24 /// By convention, each field's name indicates its purpose:
25 /// - x: padding
26 /// - a: alpha
27 /// - l: lightness (or grey, for monochrome images)
28 /// - others (r, g, b, etc.): color information
29 
30 // TODO: figure out if we need alll these methods in the color type itself
31 // - code such as gamma conversion needs to create color types
32 //   - ReplaceType can't copy methods
33 //   - even if we move out all conventional methods, that still leaves operator overloading
34 
35 struct Color(FieldTuple...)
36 {
37 	alias Spec = FieldTuple;
38 	mixin FieldList!FieldTuple;
39 
40 	// A "dumb" type to avoid cyclic references.
41 	private struct Fields { mixin FieldList!FieldTuple; }
42 
43 	/// Whether or not all channel fields have the same base type.
44 	// Only "true" supported for now, may change in the future (e.g. for 5:6:5)
45 	enum homogenous = isHomogenous!Fields();
46 
47 	/// The number of fields in this color type.
48 	enum channels = Fields.init.tupleof.length;
49 
50 	static if (homogenous)
51 	{
52 		alias ChannelType = typeof(Fields.init.tupleof[0]);
53 		enum channelBits = valueBits!ChannelType;
54 	}
55 
56 	/// Return a Color instance with all fields set to "value".
57 	static typeof(this) monochrome(ChannelType value)
58 	{
59 		typeof(this) r;
60 		foreach (i, f; r.tupleof)
61 			r.tupleof[i] = value;
62 		return r;
63 	}
64 
65 	static if (is(ChannelType:uint))
66 	{
67 		enum typeof(this) black = monochrome(0);
68 		enum typeof(this) white = monochrome(ChannelType.max);
69 	}
70 
71 	/// Interpolate between two colors.
72 	static typeof(this) itpl(P)(typeof(this) c0, typeof(this) c1, P p, P p0, P p1)
73 	{
74 		alias ExpandNumericType!(ChannelType, P.sizeof*8) U;
75 		alias Signed!U S;
76 		typeof(this) r;
77 		foreach (i, f; r.tupleof)
78 			static if (r.tupleof[i].stringof != "r.x") // skip padding
79 				r.tupleof[i] = cast(ChannelType).itpl(cast(U)c0.tupleof[i], cast(U)c1.tupleof[i], cast(S)p, cast(S)p0, cast(S)p1);
80 		return r;
81 	}
82 
83 	/// Alpha-blend two colors.
84 	static typeof(this) blend()(typeof(this) c0, typeof(this) c1)
85 		if (is(typeof(a)))
86 	{
87 		alias A = typeof(c0.a);
88 		A a = ~cast(A)(~c0.a * ~c1.a / A.max);
89 		if (!a)
90 			return typeof(this).init;
91 		A x = cast(A)(c1.a * A.max / a);
92 
93 		typeof(this) r;
94 		foreach (i, f; r.tupleof)
95 			static if (r.tupleof[i].stringof == "r.x")
96 				{} // skip padding
97 			else
98 			static if (r.tupleof[i].stringof == "r.a")
99 				r.a = a;
100 			else
101 			{
102 				auto v0 = c0.tupleof[i];
103 				auto v1 = c1.tupleof[i];
104 				auto vr = .blend(v1, v0, x);
105 				r.tupleof[i] = vr;
106 			}
107 		return r;
108 	}
109 
110 	/// Construct an RGB color from a typical hex string.
111 	static if (is(typeof(this.r) == ubyte) && is(typeof(this.g) == ubyte) && is(typeof(this.b) == ubyte))
112 	{
113 		static typeof(this) fromHex(in char[] s)
114 		{
115 			import std.conv;
116 			import std.exception;
117 
118 			enforce(s.length == 6, "Invalid color string");
119 			typeof(this) c;
120 			c.r = s[0..2].to!ubyte(16);
121 			c.g = s[2..4].to!ubyte(16);
122 			c.b = s[4..6].to!ubyte(16);
123 			return c;
124 		}
125 
126 		string toHex() const
127 		{
128 			import std.string;
129 			return format("%02X%02X%02X", r, g, b);
130 		}
131 	}
132 
133 	/// Warning: overloaded operators preserve types and may cause overflows
134 	typeof(this) opUnary(string op)()
135 		if (op=="~" || op=="-")
136 	{
137 		typeof(this) r;
138 		foreach (i, f; r.tupleof)
139 			static if(r.tupleof[i].stringof != "r.x") // skip padding
140 				r.tupleof[i] = cast(typeof(r.tupleof[i])) mixin(op ~ `this.tupleof[i]`);
141 		return r;
142 	}
143 
144 	/// ditto
145 	typeof(this) opOpAssign(string op)(int o)
146 	{
147 		foreach (i, f; this.tupleof)
148 			static if(this.tupleof[i].stringof != "this.x") // skip padding
149 				this.tupleof[i] = cast(typeof(this.tupleof[i])) mixin(`this.tupleof[i]` ~ op ~ `=o`);
150 		return this;
151 	}
152 
153 	/// ditto
154 	typeof(this) opOpAssign(string op, T)(T o)
155 		if (is(T==struct) && structFields!T == structFields!Fields)
156 	{
157 		foreach (i, f; this.tupleof)
158 			static if(this.tupleof[i].stringof != "this.x") // skip padding
159 				this.tupleof[i] = cast(typeof(this.tupleof[i])) mixin(`this.tupleof[i]` ~ op ~ `=o.tupleof[i]`);
160 		return this;
161 	}
162 
163 	/// ditto
164 	typeof(this) opBinary(string op, T)(T o)
165 		if (op != "~")
166 	{
167 		auto r = this;
168 		mixin("r" ~ op ~ "=o;");
169 		return r;
170 	}
171 
172 	/// Apply a custom operation for each channel. Example:
173 	/// COLOR.op!q{(a + b) / 2}(colorA, colorB);
174 	static typeof(this) op(string expr, T...)(T values)
175 	{
176 		static assert(values.length <= 10);
177 
178 		string genVars(string channel)
179 		{
180 			string result;
181 			foreach (j, Tj; T)
182 			{
183 				static if (is(Tj == struct)) // TODO: tighter constraint (same color channels)?
184 					result ~= "auto " ~ cast(char)('a' + j) ~ " = values[" ~ cast(char)('0' + j) ~ "]." ~  channel ~ ";\n";
185 				else
186 					result ~= "auto " ~ cast(char)('a' + j) ~ " = values[" ~ cast(char)('0' + j) ~ "];\n";
187 			}
188 			return result;
189 		}
190 
191 		typeof(this) r;
192 		foreach (i, f; r.tupleof)
193 			static if(r.tupleof[i].stringof != "r.x") // skip padding
194 			{
195 				mixin(genVars(r.tupleof[i].stringof[2..$]));
196 				r.tupleof[i] = mixin(expr);
197 			}
198 		return r;
199 	}
200 
201 	T opCast(T)()
202 		if (is(T==struct) && structFields!T == structFields!Fields)
203 	{
204 		T t;
205 		foreach (i, f; this.tupleof)
206 			t.tupleof[i] = cast(typeof(t.tupleof[i])) this.tupleof[i];
207 		return t;
208 	}
209 
210 	/// Sum of all channels
211 	ExpandIntegerType!(ChannelType, ilog2(nextPowerOfTwo(channels))) sum()
212 	{
213 		typeof(return) result;
214 		foreach (i, f; this.tupleof)
215 			static if (this.tupleof[i].stringof != "this.x") // skip padding
216 				result += this.tupleof[i];
217 		return result;
218 	}
219 
220 	static @property Color min()
221 	{
222 		Color result;
223 		foreach (ref v; result.tupleof)
224 			static if (is(typeof(typeof(v).min)))
225 				v = typeof(v).min;
226 			else
227 			static if (is(typeof(typeof(v).max)))
228 				v = -typeof(v).max;
229 		return result;
230 	}
231 
232 	static @property Color max()
233 	{
234 		Color result;
235 		foreach (ref v; result.tupleof)
236 			static if (is(typeof(typeof(v).max)))
237 				v = typeof(v).max;
238 		return result;
239 	}
240 }
241 
242 // The "x" has the special meaning of "padding" and is ignored in some circumstances
243 alias Color!(ubyte  , "r", "g", "b"     ) RGB    ;
244 alias Color!(ushort , "r", "g", "b"     ) RGB16  ;
245 alias Color!(ubyte  , "r", "g", "b", "x") RGBX   ;
246 alias Color!(ushort , "r", "g", "b", "x") RGBX16 ;
247 alias Color!(ubyte  , "r", "g", "b", "a") RGBA   ;
248 alias Color!(ushort , "r", "g", "b", "a") RGBA16 ;
249 
250 alias Color!(ubyte  , "b", "g", "r"     ) BGR    ;
251 alias Color!(ubyte  , "b", "g", "r", "x") BGRX   ;
252 alias Color!(ubyte  , "b", "g", "r", "a") BGRA   ;
253 
254 alias Color!(ubyte  , "l"               ) L8     ;
255 alias Color!(ushort , "l"               ) L16    ;
256 alias Color!(ubyte  , "l", "a"          ) LA     ;
257 alias Color!(ushort , "l", "a"          ) LA16   ;
258 
259 alias Color!(byte   , "l"               ) S8     ;
260 alias Color!(short  , "l"               ) S16    ;
261 
262 alias Color!(float  , "r", "g", "b"     ) RGBf   ;
263 alias Color!(double , "r", "g", "b"     ) RGBd   ;
264 
265 unittest
266 {
267 	static assert(RGB.sizeof == 3);
268 	RGB[2] arr;
269 	static assert(arr.sizeof == 6);
270 
271 	RGB hex = RGB.fromHex("123456");
272 	assert(hex.r == 0x12 && hex.g == 0x34 && hex.b == 0x56);
273 
274 	assert(RGB(1, 2, 3) + RGB(4, 5, 6) == RGB(5, 7, 9));
275 
276 	RGB c = RGB(1, 1, 1);
277 	c += 1;
278 	assert(c == RGB(2, 2, 2));
279 	c += c;
280 	assert(c == RGB(4, 4, 4));
281 }
282 
283 static assert(RGB.min == RGB(  0,   0,   0));
284 static assert(RGB.max == RGB(255, 255, 255));
285 
286 unittest
287 {
288 	import std.conv;
289 
290 	L8 r;
291 
292 	r = L8.itpl(L8(100), L8(200), 15, 10, 20);
293 	assert(r ==  L8(150), text(r));
294 }
295 
296 unittest
297 {
298 	import std.conv;
299 
300 	LA r;
301 
302 	r = LA.blend(LA(123,   0),
303 	             LA(111, 222));
304 	assert(r ==  LA(111, 222), text(r));
305 
306 	r = LA.blend(LA(123, 213),
307 	             LA(111, 255));
308 	assert(r ==  LA(111, 255), text(r));
309 
310 	r = LA.blend(LA(  0, 255),
311 	             LA(255, 100));
312 	assert(r ==  LA(100, 255), text(r));
313 }
314 
315 unittest
316 {
317 	Color!(real, "r", "g", "b") c;
318 }
319 
320 /// Obtains the type of each channel for homogenous colors.
321 template ChannelType(T)
322 {
323 	static if (is(T == struct))
324 		alias ChannelType = T.ChannelType;
325 	else
326 		alias ChannelType = T;
327 }
328 
329 /// Resolves to a Color instance with a different ChannelType.
330 template ChangeChannelType(COLOR, T)
331 	if (isNumeric!COLOR)
332 {
333 	alias ChangeChannelType = T;
334 }
335 
336 /// ditto
337 template ChangeChannelType(COLOR, T)
338 	if (is(COLOR : Color!Spec, Spec...))
339 {
340 	static assert(COLOR.homogenous, "Can't change ChannelType of non-homogenous Color");
341 	alias ChangeChannelType = Color!(T, COLOR.Spec[1..$]);
342 }
343 
344 static assert(is(ChangeChannelType!(RGB, ushort) == RGB16));
345 static assert(is(ChangeChannelType!(int, ushort) == ushort));
346 
347 /// Wrapper around ExpandNumericType to only expand numeric types.
348 template ExpandIntegerType(T, size_t bits)
349 {
350 	static if (is(T:real))
351 		alias ExpandIntegerType = T;
352 	else
353 		alias ExpandIntegerType = ExpandNumericType!(T, bits);
354 }
355 
356 alias ExpandChannelType(COLOR, int BYTES) =
357 	ChangeChannelType!(COLOR,
358 		ExpandNumericType!(ChannelType!COLOR, BYTES * 8));
359 
360 static assert(is(ExpandChannelType!(RGB, 1) == RGB16));
361 
362 unittest
363 {
364 	alias RGBf = ChangeChannelType!(RGB, float);
365 	auto rgb = RGB(1, 2, 3);
366 	import std.conv : to;
367 	auto rgbf = rgb.to!RGBf();
368 	assert(rgbf.r == 1f);
369 	assert(rgbf.g == 2f);
370 	assert(rgbf.b == 3f);
371 }
372 
373 // ***************************************************************************
374 
375 // TODO: deprecate
376 T blend(T)(T f, T b, T a) if (is(typeof(f*a+~b))) { return cast(T) ( ((f*a) + (b*~a)) / T.max ); }