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 	/// See also: Gradient
73 	static typeof(this) itpl(P)(typeof(this) c0, typeof(this) c1, P p, P p0, P p1)
74 	{
75 		alias TryExpandNumericType!(ChannelType, P.sizeof*8) U;
76 		alias Signed!U S;
77 		typeof(this) r;
78 		foreach (i, f; r.tupleof)
79 			static if (r.tupleof[i].stringof != "r.x") // skip padding
80 				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);
81 		return r;
82 	}
83 
84 	/// Alpha-blend two colors.
85 	static typeof(this) blend()(typeof(this) c0, typeof(this) c1)
86 		if (is(typeof(a)))
87 	{
88 		alias A = typeof(c0.a);
89 		A a = flipBits(cast(A)(c0.a.flipBits * c1.a.flipBits / A.max));
90 		if (!a)
91 			return typeof(this).init;
92 		A x = cast(A)(c1.a * A.max / a);
93 
94 		typeof(this) r;
95 		foreach (i, f; r.tupleof)
96 			static if (r.tupleof[i].stringof == "r.x")
97 				{} // skip padding
98 			else
99 			static if (r.tupleof[i].stringof == "r.a")
100 				r.a = a;
101 			else
102 			{
103 				auto v0 = c0.tupleof[i];
104 				auto v1 = c1.tupleof[i];
105 				auto vr = .blend(v1, v0, x);
106 				r.tupleof[i] = vr;
107 			}
108 		return r;
109 	}
110 
111 	/// Alpha-blend a color with an alpha channel on top of one without.
112 	static typeof(this) blend(C)(typeof(this) c0, C c1)
113 		if (!is(typeof(a)) && is(typeof(c1.a)))
114 	{
115 		alias A = typeof(c1.a);
116 		if (!c1.a)
117 			return c0;
118 		//A x = cast(A)(c1.a * A.max / a);
119 
120 		typeof(this) r;
121 		foreach (i, ref f; r.tupleof)
122 		{
123 			enum name = __traits(identifier, r.tupleof[i]);
124 			static if (name == "x")
125 				{} // skip padding
126 			else
127 			static if (name == "a")
128 				static assert(false);
129 			else
130 			{
131 				auto v0 = __traits(getMember, c0, name);
132 				auto v1 = __traits(getMember, c1, name);
133 				f = .blend(v1, v0, c1.a);
134 			}
135 		}
136 		return r;
137 	}
138 
139 	/// Construct an RGB color from a typical hex string.
140 	static if (is(typeof(this.r) == ubyte) && is(typeof(this.g) == ubyte) && is(typeof(this.b) == ubyte))
141 	{
142 		static typeof(this) fromHex(in char[] s)
143 		{
144 			import std.conv;
145 			import std.exception;
146 
147 			enforce(s.length == 6, "Invalid color string");
148 			typeof(this) c;
149 			c.r = s[0..2].to!ubyte(16);
150 			c.g = s[2..4].to!ubyte(16);
151 			c.b = s[4..6].to!ubyte(16);
152 			return c;
153 		}
154 
155 		string toHex() const
156 		{
157 			import std.string;
158 			return format("%02X%02X%02X", r, g, b);
159 		}
160 	}
161 
162 	/// Warning: overloaded operators preserve types and may cause overflows
163 	typeof(this) opUnary(string op)()
164 		if (op=="~" || op=="-")
165 	{
166 		typeof(this) r;
167 		foreach (i, f; r.tupleof)
168 			static if(r.tupleof[i].stringof != "r.x") // skip padding
169 				r.tupleof[i] = cast(typeof(r.tupleof[i])) unary!(op[0])(this.tupleof[i]);
170 		return r;
171 	}
172 
173 	/// ditto
174 	typeof(this) opOpAssign(string op)(int o)
175 	{
176 		foreach (i, f; this.tupleof)
177 			static if(this.tupleof[i].stringof != "this.x") // skip padding
178 				this.tupleof[i] = cast(typeof(this.tupleof[i])) mixin(`this.tupleof[i]` ~ op ~ `=o`);
179 		return this;
180 	}
181 
182 	/// ditto
183 	typeof(this) opOpAssign(string op, T)(T o)
184 		if (is(T==struct) && structFields!T == structFields!Fields)
185 	{
186 		foreach (i, f; this.tupleof)
187 			static if(this.tupleof[i].stringof != "this.x") // skip padding
188 				this.tupleof[i] = cast(typeof(this.tupleof[i])) mixin(`this.tupleof[i]` ~ op ~ `=o.tupleof[i]`);
189 		return this;
190 	}
191 
192 	/// ditto
193 	typeof(this) opBinary(string op, T)(T o)
194 		if (op != "~")
195 	{
196 		auto r = this;
197 		mixin("r" ~ op ~ "=o;");
198 		return r;
199 	}
200 
201 	/// Apply a custom operation for each channel. Example:
202 	/// COLOR.op!q{(a + b) / 2}(colorA, colorB);
203 	static typeof(this) op(string expr, T...)(T values)
204 	{
205 		static assert(values.length <= 10);
206 
207 		string genVars(string channel)
208 		{
209 			string result;
210 			foreach (j, Tj; T)
211 			{
212 				static if (is(Tj == struct)) // TODO: tighter constraint (same color channels)?
213 					result ~= "auto " ~ cast(char)('a' + j) ~ " = values[" ~ cast(char)('0' + j) ~ "]." ~  channel ~ ";\n";
214 				else
215 					result ~= "auto " ~ cast(char)('a' + j) ~ " = values[" ~ cast(char)('0' + j) ~ "];\n";
216 			}
217 			return result;
218 		}
219 
220 		typeof(this) r;
221 		foreach (i, f; r.tupleof)
222 			static if(r.tupleof[i].stringof != "r.x") // skip padding
223 			{
224 				mixin(genVars(r.tupleof[i].stringof[2..$]));
225 				r.tupleof[i] = mixin(expr);
226 			}
227 		return r;
228 	}
229 
230 	T opCast(T)()
231 		if (is(T==struct) && structFields!T == structFields!Fields)
232 	{
233 		T t;
234 		foreach (i, f; this.tupleof)
235 			t.tupleof[i] = cast(typeof(t.tupleof[i])) this.tupleof[i];
236 		return t;
237 	}
238 
239 	/// Sum of all channels
240 	ExpandIntegerType!(ChannelType, ilog2(nextPowerOfTwo(channels))) sum()
241 	{
242 		typeof(return) result;
243 		foreach (i, f; this.tupleof)
244 			static if (this.tupleof[i].stringof != "this.x") // skip padding
245 				result += this.tupleof[i];
246 		return result;
247 	}
248 
249 	static @property Color min()
250 	{
251 		Color result;
252 		foreach (ref v; result.tupleof)
253 			static if (is(typeof(typeof(v).min)))
254 				v = typeof(v).min;
255 			else
256 			static if (is(typeof(typeof(v).max)))
257 				v = -typeof(v).max;
258 		return result;
259 	}
260 
261 	static @property Color max()
262 	{
263 		Color result;
264 		foreach (ref v; result.tupleof)
265 			static if (is(typeof(typeof(v).max)))
266 				v = typeof(v).max;
267 		return result;
268 	}
269 }
270 
271 // The "x" has the special meaning of "padding" and is ignored in some circumstances
272 alias Color!(ubyte  , "r", "g", "b"     ) RGB    ;
273 alias Color!(ushort , "r", "g", "b"     ) RGB16  ;
274 alias Color!(ubyte  , "r", "g", "b", "x") RGBX   ;
275 alias Color!(ushort , "r", "g", "b", "x") RGBX16 ;
276 alias Color!(ubyte  , "r", "g", "b", "a") RGBA   ;
277 alias Color!(ushort , "r", "g", "b", "a") RGBA16 ;
278 
279 alias Color!(ubyte  , "b", "g", "r"     ) BGR    ;
280 alias Color!(ubyte  , "b", "g", "r", "x") BGRX   ;
281 alias Color!(ubyte  , "b", "g", "r", "a") BGRA   ;
282 
283 alias Color!(ubyte  , "l"               ) L8     ;
284 alias Color!(ushort , "l"               ) L16    ;
285 alias Color!(ubyte  , "l", "a"          ) LA     ;
286 alias Color!(ushort , "l", "a"          ) LA16   ;
287 
288 alias Color!(byte   , "l"               ) S8     ;
289 alias Color!(short  , "l"               ) S16    ;
290 
291 alias Color!(float  , "r", "g", "b"     ) RGBf   ;
292 alias Color!(double , "r", "g", "b"     ) RGBd   ;
293 
294 unittest
295 {
296 	static assert(RGB.sizeof == 3);
297 	RGB[2] arr;
298 	static assert(arr.sizeof == 6);
299 
300 	RGB hex = RGB.fromHex("123456");
301 	assert(hex.r == 0x12 && hex.g == 0x34 && hex.b == 0x56);
302 
303 	assert(RGB(1, 2, 3) + RGB(4, 5, 6) == RGB(5, 7, 9));
304 
305 	RGB c = RGB(1, 1, 1);
306 	c += 1;
307 	assert(c == RGB(2, 2, 2));
308 	c += c;
309 	assert(c == RGB(4, 4, 4));
310 }
311 
312 static assert(RGB.min == RGB(  0,   0,   0));
313 static assert(RGB.max == RGB(255, 255, 255));
314 
315 unittest
316 {
317 	import std.conv;
318 
319 	L8 r;
320 
321 	r = L8.itpl(L8(100), L8(200), 15, 10, 20);
322 	assert(r ==  L8(150), text(r));
323 }
324 
325 unittest
326 {
327 	import std.conv;
328 
329 	LA r;
330 
331 	r = LA.blend(LA(123,   0),
332 	             LA(111, 222));
333 	assert(r ==  LA(111, 222), text(r));
334 
335 	r = LA.blend(LA(123, 213),
336 	             LA(111, 255));
337 	assert(r ==  LA(111, 255), text(r));
338 
339 	r = LA.blend(LA(  0, 255),
340 	             LA(255, 100));
341 	assert(r ==  LA(100, 255), text(r));
342 }
343 
344 unittest
345 {
346 	import std.conv;
347 
348 	L8 r;
349 
350 	r = L8.blend(L8(123),
351 	             LA(231, 0));
352 	assert(r ==  L8(123), text(r));
353 
354 	r = L8.blend(L8(123),
355 	             LA(231, 255));
356 	assert(r ==  L8(231), text(r));
357 
358 	r = L8.blend(L8(  0),
359 	             LA(255, 100));
360 	assert(r ==  L8(100), text(r));
361 }
362 
363 unittest
364 {
365 	Color!(real, "r", "g", "b") c;
366 }
367 
368 /// Obtains the type of each channel for homogenous colors.
369 template ChannelType(T)
370 {
371 	static if (is(T == struct))
372 		alias ChannelType = T.ChannelType;
373 	else
374 		alias ChannelType = T;
375 }
376 
377 /// Resolves to a Color instance with a different ChannelType.
378 template ChangeChannelType(COLOR, T)
379 	if (isNumeric!COLOR)
380 {
381 	alias ChangeChannelType = T;
382 }
383 
384 /// ditto
385 template ChangeChannelType(COLOR, T)
386 	if (is(COLOR : Color!Spec, Spec...))
387 {
388 	static assert(COLOR.homogenous, "Can't change ChannelType of non-homogenous Color");
389 	alias ChangeChannelType = Color!(T, COLOR.Spec[1..$]);
390 }
391 
392 static assert(is(ChangeChannelType!(RGB, ushort) == RGB16));
393 static assert(is(ChangeChannelType!(int, ushort) == ushort));
394 
395 /// Wrapper around ExpandNumericType to only expand numeric types.
396 template ExpandIntegerType(T, size_t bits)
397 {
398 	static if (is(T:real))
399 		alias ExpandIntegerType = T;
400 	else
401 		alias ExpandIntegerType = ExpandNumericType!(T, bits);
402 }
403 
404 alias ExpandChannelType(COLOR, int BYTES) =
405 	ChangeChannelType!(COLOR,
406 		ExpandNumericType!(ChannelType!COLOR, BYTES * 8));
407 
408 static assert(is(ExpandChannelType!(RGB, 1) == RGB16));
409 
410 unittest
411 {
412 	alias RGBf = ChangeChannelType!(RGB, float);
413 	auto rgb = RGB(1, 2, 3);
414 	import std.conv : to;
415 	auto rgbf = rgb.to!RGBf();
416 	assert(rgbf.r == 1f);
417 	assert(rgbf.g == 2f);
418 	assert(rgbf.b == 3f);
419 }
420 
421 // ***************************************************************************
422 
423 /// Calculate an interpolated color on a gradient with multiple points
424 struct Gradient(Value, Color)
425 {
426 	struct Point
427 	{
428 		Value value;
429 		Color color;
430 	}
431 	Point[] points;
432 
433 	Color get(Value value) const
434 	{
435 		assert(points.length, "Gradient must have at least one point");
436 
437 		if (value <= points[0].value)
438 			return points[0].color;
439 
440 		for (int i = 0; i < points.length - 1; i++)
441 		{
442 			assert(points[i].value <= points[i+1].value,
443 				"Gradient values are not in ascending order");
444 			if (value < points[i+1].value)
445 				return Color.itpl(points[i].color, points[i+1].color, value, points[i].value, points[i+1].value);
446 		}
447 
448 		return points[$-1].color;
449 	}
450 }
451 
452 unittest
453 {
454 	Gradient!(int, L8) grad;
455 	grad.points = [
456 		grad.Point(0, L8(0)),
457 		grad.Point(10, L8(100)),
458 	];
459 
460 	assert(grad.get(-5) == L8(  0));
461 	assert(grad.get( 0) == L8(  0));
462 	assert(grad.get( 5) == L8( 50));
463 	assert(grad.get(10) == L8(100));
464 	assert(grad.get(15) == L8(100));
465 }
466 
467 // ***************************************************************************
468 
469 // TODO: deprecate
470 T blend(T)(T f, T b, T a) if (is(typeof(f*a+~b))) { return cast(T) ( ((f*a) + (b*flipBits(a))) / T.max ); }