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