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 			static if (__traits(identifier, r.tupleof[i]) == "a")
62 				r.tupleof[i] = typeof(r.tupleof[i]).max;
63 			else
64 				r.tupleof[i] = value;
65 		return r;
66 	}
67 
68 	static if (is(ChannelType:uint))
69 	{
70 		enum typeof(this) black = monochrome(0);
71 		enum typeof(this) white = monochrome(ChannelType.max);
72 	}
73 
74 	/// Interpolate between two colors.
75 	/// See also: Gradient
76 	static typeof(this) itpl(P)(typeof(this) c0, typeof(this) c1, P p, P p0, P p1)
77 	{
78 		alias TryExpandNumericType!(ChannelType, P.sizeof*8) U;
79 		typeof(this) r;
80 		foreach (i, f; r.tupleof)
81 			static if (r.tupleof[i].stringof != "r.x") // skip padding
82 				r.tupleof[i] = cast(ChannelType).itpl(cast(U)c0.tupleof[i], cast(U)c1.tupleof[i], p, p0, p1);
83 		return r;
84 	}
85 
86 	/// Alpha-blend two colors.
87 	static typeof(this) blend()(typeof(this) c0, typeof(this) c1)
88 		if (is(typeof(a)))
89 	{
90 		alias A = typeof(c0.a);
91 		A a = flipBits(cast(A)(c0.a.flipBits * c1.a.flipBits / A.max));
92 		if (!a)
93 			return typeof(this).init;
94 		A x = cast(A)(c1.a * A.max / a);
95 
96 		typeof(this) r;
97 		foreach (i, f; r.tupleof)
98 			static if (r.tupleof[i].stringof == "r.x")
99 				{} // skip padding
100 			else
101 			static if (r.tupleof[i].stringof == "r.a")
102 				r.a = a;
103 			else
104 			{
105 				auto v0 = c0.tupleof[i];
106 				auto v1 = c1.tupleof[i];
107 				auto vr = .blend(v1, v0, x);
108 				r.tupleof[i] = vr;
109 			}
110 		return r;
111 	}
112 
113 	/// Alpha-blend a color with an alpha channel on top of one without.
114 	static typeof(this) blend(C)(typeof(this) c0, C c1)
115 		if (!is(typeof(a)) && is(typeof(c1.a)))
116 	{
117 		alias A = typeof(c1.a);
118 		if (!c1.a)
119 			return c0;
120 		//A x = cast(A)(c1.a * A.max / a);
121 
122 		typeof(this) r;
123 		foreach (i, ref f; r.tupleof)
124 		{
125 			enum name = __traits(identifier, r.tupleof[i]);
126 			static if (name == "x")
127 				{} // skip padding
128 			else
129 			static if (name == "a")
130 				static assert(false);
131 			else
132 			{
133 				auto v0 = __traits(getMember, c0, name);
134 				auto v1 = __traits(getMember, c1, name);
135 				f = .blend(v1, v0, c1.a);
136 			}
137 		}
138 		return r;
139 	}
140 
141 	/// Construct an RGB color from a typical hex string.
142 	static if (is(typeof(this.r) == ubyte) && is(typeof(this.g) == ubyte) && is(typeof(this.b) == ubyte))
143 	{
144 		static typeof(this) fromHex(in char[] s)
145 		{
146 			import std.conv;
147 			import std.exception;
148 
149 			enforce(s.length == 6 || (is(typeof(this.a) == ubyte) && s.length == 8), "Invalid color string");
150 			typeof(this) c;
151 			c.r = s[0..2].to!ubyte(16);
152 			c.g = s[2..4].to!ubyte(16);
153 			c.b = s[4..6].to!ubyte(16);
154 			static if (is(typeof(this.a) == ubyte))
155 			{
156 				if (s.length == 8)
157 					c.a = s[6..8].to!ubyte(16);
158 				else
159 					c.a = ubyte.max;
160 			}
161 			return c;
162 		}
163 
164 		string toHex() const
165 		{
166 			import std.string;
167 			return format("%02X%02X%02X", r, g, b);
168 		}
169 	}
170 
171 	/// Warning: overloaded operators preserve types and may cause overflows
172 	typeof(this) opUnary(string op)()
173 		if (op=="~" || op=="-")
174 	{
175 		typeof(this) r;
176 		foreach (i, f; r.tupleof)
177 			static if(r.tupleof[i].stringof != "r.x") // skip padding
178 				r.tupleof[i] = cast(typeof(r.tupleof[i])) unary!(op[0])(this.tupleof[i]);
179 		return r;
180 	}
181 
182 	/// ditto
183 	typeof(this) opOpAssign(string op)(int o)
184 	{
185 		foreach (i, f; this.tupleof)
186 			static if(this.tupleof[i].stringof != "this.x") // skip padding
187 				this.tupleof[i] = cast(typeof(this.tupleof[i])) mixin(`this.tupleof[i]` ~ op ~ `=o`);
188 		return this;
189 	}
190 
191 	/// ditto
192 	typeof(this) opOpAssign(string op, T)(T o)
193 		if (is(T==struct) && structFields!T == structFields!Fields)
194 	{
195 		foreach (i, f; this.tupleof)
196 			static if(this.tupleof[i].stringof != "this.x") // skip padding
197 				this.tupleof[i] = cast(typeof(this.tupleof[i])) mixin(`this.tupleof[i]` ~ op ~ `=o.tupleof[i]`);
198 		return this;
199 	}
200 
201 	/// ditto
202 	typeof(this) opBinary(string op, T)(T o)
203 		if (op != "~")
204 	{
205 		auto r = this;
206 		mixin("r" ~ op ~ "=o;");
207 		return r;
208 	}
209 
210 	/// Apply a custom operation for each channel. Example:
211 	/// COLOR.op!q{(a + b) / 2}(colorA, colorB);
212 	static typeof(this) op(string expr, T...)(T values)
213 	{
214 		static assert(values.length <= 10);
215 
216 		string genVars(string channel)
217 		{
218 			string result;
219 			foreach (j, Tj; T)
220 			{
221 				static if (is(Tj == struct)) // TODO: tighter constraint (same color channels)?
222 					result ~= "auto " ~ cast(char)('a' + j) ~ " = values[" ~ cast(char)('0' + j) ~ "]." ~  channel ~ ";\n";
223 				else
224 					result ~= "auto " ~ cast(char)('a' + j) ~ " = values[" ~ cast(char)('0' + j) ~ "];\n";
225 			}
226 			return result;
227 		}
228 
229 		typeof(this) r;
230 		foreach (i, f; r.tupleof)
231 			static if(r.tupleof[i].stringof != "r.x") // skip padding
232 			{
233 				mixin(genVars(r.tupleof[i].stringof[2..$]));
234 				r.tupleof[i] = mixin(expr);
235 			}
236 		return r;
237 	}
238 
239 	T opCast(T)() const
240 	if (is(T==struct) && structFields!T == structFields!Fields)
241 	{
242 		static if (is(T == typeof(this)))
243 			return this;
244 		else
245 		{
246 			T t;
247 			foreach (i, f; this.tupleof)
248 				t.tupleof[i] = cast(typeof(t.tupleof[i])) this.tupleof[i];
249 			return t;
250 		}
251 	}
252 
253 	/// Sum of all channels
254 	ExpandIntegerType!(ChannelType, ilog2(nextPowerOfTwo(channels))) sum()
255 	{
256 		typeof(return) result;
257 		foreach (i, f; this.tupleof)
258 			static if (this.tupleof[i].stringof != "this.x") // skip padding
259 				result += this.tupleof[i];
260 		return result;
261 	}
262 
263 	static @property Color min()
264 	{
265 		Color result;
266 		foreach (ref v; result.tupleof)
267 			static if (is(typeof(typeof(v).min)))
268 				v = typeof(v).min;
269 			else
270 			static if (is(typeof(typeof(v).max)))
271 				v = -typeof(v).max;
272 		return result;
273 	}
274 
275 	static @property Color max()
276 	{
277 		Color result;
278 		foreach (ref v; result.tupleof)
279 			static if (is(typeof(typeof(v).max)))
280 				v = typeof(v).max;
281 		return result;
282 	}
283 }
284 
285 // The "x" has the special meaning of "padding" and is ignored in some circumstances
286 alias Color!(ubyte  , "r", "g", "b"     ) RGB    ;
287 alias Color!(ushort , "r", "g", "b"     ) RGB16  ;
288 alias Color!(ubyte  , "r", "g", "b", "x") RGBX   ;
289 alias Color!(ushort , "r", "g", "b", "x") RGBX16 ;
290 alias Color!(ubyte  , "r", "g", "b", "a") RGBA   ;
291 alias Color!(ushort , "r", "g", "b", "a") RGBA16 ;
292 
293 alias Color!(ubyte  , "b", "g", "r"     ) BGR    ;
294 alias Color!(ubyte  , "b", "g", "r", "x") BGRX   ;
295 alias Color!(ubyte  , "b", "g", "r", "a") BGRA   ;
296 
297 alias Color!(ubyte  , "l"               ) L8     ;
298 alias Color!(ushort , "l"               ) L16    ;
299 alias Color!(ubyte  , "l", "a"          ) LA     ;
300 alias Color!(ushort , "l", "a"          ) LA16   ;
301 
302 alias Color!(byte   , "l"               ) S8     ;
303 alias Color!(short  , "l"               ) S16    ;
304 
305 alias Color!(float  , "r", "g", "b"     ) RGBf   ;
306 alias Color!(double , "r", "g", "b"     ) RGBd   ;
307 
308 unittest
309 {
310 	static assert(RGB.sizeof == 3);
311 	RGB[2] arr;
312 	static assert(arr.sizeof == 6);
313 
314 	RGB hex = RGB.fromHex("123456");
315 	assert(hex.r == 0x12 && hex.g == 0x34 && hex.b == 0x56);
316 
317 	BGRA hex2 = BGRA.fromHex("12345678");
318 	assert(hex2.r == 0x12 && hex2.g == 0x34 && hex2.b == 0x56 && hex2.a == 0x78);
319 
320 	assert(RGB(1, 2, 3) + RGB(4, 5, 6) == RGB(5, 7, 9));
321 
322 	RGB c = RGB(1, 1, 1);
323 	c += 1;
324 	assert(c == RGB(2, 2, 2));
325 	c += c;
326 	assert(c == RGB(4, 4, 4));
327 }
328 
329 static assert(RGB.min == RGB(  0,   0,   0));
330 static assert(RGB.max == RGB(255, 255, 255));
331 
332 unittest
333 {
334 	import std.conv;
335 
336 	L8 r;
337 
338 	r = L8.itpl(L8(100), L8(200), 15, 10, 20);
339 	assert(r ==  L8(150), text(r));
340 }
341 
342 unittest
343 {
344 	import std.conv;
345 
346 	LA r;
347 
348 	r = LA.blend(LA(123,   0),
349 	             LA(111, 222));
350 	assert(r ==  LA(111, 222), text(r));
351 
352 	r = LA.blend(LA(123, 213),
353 	             LA(111, 255));
354 	assert(r ==  LA(111, 255), text(r));
355 
356 	r = LA.blend(LA(  0, 255),
357 	             LA(255, 100));
358 	assert(r ==  LA(100, 255), text(r));
359 }
360 
361 unittest
362 {
363 	import std.conv;
364 
365 	L8 r;
366 
367 	r = L8.blend(L8(123),
368 	             LA(231, 0));
369 	assert(r ==  L8(123), text(r));
370 
371 	r = L8.blend(L8(123),
372 	             LA(231, 255));
373 	assert(r ==  L8(231), text(r));
374 
375 	r = L8.blend(L8(  0),
376 	             LA(255, 100));
377 	assert(r ==  L8(100), text(r));
378 }
379 
380 unittest
381 {
382 	Color!(real, "r", "g", "b") c;
383 }
384 
385 unittest
386 {
387 	const RGB c;
388 	RGB x = cast(RGB)c;
389 }
390 
391 /// Obtains the type of each channel for homogenous colors.
392 template ChannelType(T)
393 {
394 	static if (is(T == struct))
395 		alias ChannelType = T.ChannelType;
396 	else
397 		alias ChannelType = T;
398 }
399 
400 /// Resolves to a Color instance with a different ChannelType.
401 template ChangeChannelType(COLOR, T)
402 	if (isNumeric!COLOR)
403 {
404 	alias ChangeChannelType = T;
405 }
406 
407 /// ditto
408 template ChangeChannelType(COLOR, T)
409 	if (is(COLOR : Color!Spec, Spec...))
410 {
411 	static assert(COLOR.homogenous, "Can't change ChannelType of non-homogenous Color");
412 	alias ChangeChannelType = Color!(T, COLOR.Spec[1..$]);
413 }
414 
415 static assert(is(ChangeChannelType!(RGB, ushort) == RGB16));
416 static assert(is(ChangeChannelType!(int, ushort) == ushort));
417 
418 /// Wrapper around ExpandNumericType to only expand numeric types.
419 template ExpandIntegerType(T, size_t bits)
420 {
421 	static if (is(T:real))
422 		alias ExpandIntegerType = T;
423 	else
424 		alias ExpandIntegerType = ExpandNumericType!(T, bits);
425 }
426 
427 alias ExpandChannelType(COLOR, int BYTES) =
428 	ChangeChannelType!(COLOR,
429 		ExpandNumericType!(ChannelType!COLOR, BYTES * 8));
430 
431 static assert(is(ExpandChannelType!(RGB, 1) == RGB16));
432 
433 unittest
434 {
435 	alias RGBf = ChangeChannelType!(RGB, float);
436 	auto rgb = RGB(1, 2, 3);
437 	import std.conv : to;
438 	auto rgbf = rgb.to!RGBf();
439 	assert(rgbf.r == 1f);
440 	assert(rgbf.g == 2f);
441 	assert(rgbf.b == 3f);
442 }
443 
444 
445 // ***************************************************************************
446 
447 /// Color storage unit for as-is storage.
448 alias PlainStorageUnit(Color) = Color[1];
449 
450 /// Color storage unit description for packed bit colors
451 /// (1-bit, 2-bit, 4-bit etc.)
452 struct BitStorageUnit(ValueType, size_t valueBits, StorageType, bool bigEndian)
453 {
454 	StorageType storageValue;
455 
456 	enum length = StorageType.sizeof * 8 / valueBits;
457 	static assert(length * valueBits == StorageType.sizeof * 8, "Slack bits?");
458 
459 	ValueType opIndex(size_t index) const
460 	{
461 		static if (bigEndian)
462 			index = length - 1 - index;
463 		auto shift = index * valueBits;
464 		return cast(ValueType)((storageValue >> shift) & valueMask);
465 	}
466 
467 	ValueType opIndexAssign(ValueType value, size_t index)
468 	{
469 		static if (bigEndian)
470 			index = length - 1 - index;
471 		auto shift = index * valueBits;
472 		StorageType mask = flipBits(cast(StorageType)(valueMask << shift));
473 		storageValue = (storageValue & mask) | cast(StorageType)(cast(StorageType)value << shift);
474 		return value;
475 	}
476 private:
477 	enum StorageType valueMask = ((cast(StorageType)1) << valueBits) - 1;
478 }
479 
480 /// 8 monochrome bits packed into a byte, in the usual big-endian order.
481 alias OneBitStorageBE = BitStorageUnit!(bool, 1, ubyte, true);
482 /// As above, but in little-endian order.
483 alias OneBitStorageLE = BitStorageUnit!(bool, 1, ubyte, false);
484 
485 /// Get the color value of a storage unit type.
486 alias StorageColor(StorageType) = typeof(StorageType.init[0]);
487 
488 /// The number of bits that one individual color takes up.
489 enum size_t storageColorBits(StorageType) = StorageType.sizeof * 8 / StorageType.length;
490 
491 /// True when we can take the address of an individual color within a storage unit.
492 enum bool isStorageColorLValue(StorageType) = is(typeof({ StorageType s = void; return &s[0]; }()));
493 
494 StorageType solidStorageUnit(StorageType)(StorageColor!StorageType color)
495 {
496 	StorageType s;
497 	foreach (i; 0 .. StorageType.length)
498 		s[i] = color;
499 	return s;
500 }
501 
502 // ***************************************************************************
503 
504 /// Calculate an interpolated color on a gradient with multiple points
505 struct Gradient(Value, Color)
506 {
507 	struct Point
508 	{
509 		Value value;
510 		Color color;
511 	}
512 	Point[] points;
513 
514 	Color get(Value value) const
515 	{
516 		assert(points.length, "Gradient must have at least one point");
517 
518 		if (value <= points[0].value)
519 			return points[0].color;
520 
521 		for (size_t i = 1; i < points.length; i++)
522 		{
523 			assert(points[i-1].value <= points[i].value,
524 				"Gradient values are not in ascending order");
525 			if (value < points[i].value)
526 				return Color.itpl(
527 					points[i-1].color, points[i].color, value,
528 					points[i-1].value, points[i].value);
529 		}
530 
531 		return points[$-1].color;
532 	}
533 }
534 
535 unittest
536 {
537 	Gradient!(int, L8) grad;
538 	grad.points = [
539 		grad.Point(0, L8(0)),
540 		grad.Point(10, L8(100)),
541 	];
542 
543 	assert(grad.get(-5) == L8(  0));
544 	assert(grad.get( 0) == L8(  0));
545 	assert(grad.get( 5) == L8( 50));
546 	assert(grad.get(10) == L8(100));
547 	assert(grad.get(15) == L8(100));
548 }
549 
550 unittest
551 {
552 	Gradient!(float, L8) grad;
553 	grad.points = [
554 		grad.Point(0.0f, L8( 0)),
555 		grad.Point(0.5f, L8(10)),
556 		grad.Point(1.0f, L8(30)),
557 	];
558 
559 	assert(grad.get(0.00f) == L8(  0));
560 	assert(grad.get(0.25f) == L8(  5));
561 	assert(grad.get(0.50f) == L8( 10));
562 	assert(grad.get(0.75f) == L8( 20));
563 	assert(grad.get(1.00f) == L8( 30));
564 }
565 
566 // ***************************************************************************
567 
568 // TODO: deprecate
569 T blend(T)(T f, T b, T a) if (is(typeof(f*a+flipBits(b)))) { return cast(T) ( ((f*a) + (b*flipBits(a))) / T.max ); }