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