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 = ChannelType.sizeof*8;
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 	/// Interpolate between two colors.
66 	static typeof(this) itpl(P)(typeof(this) c0, typeof(this) c1, P p, P p0, P p1)
67 	{
68 		alias UnsignedBitsType!(channelBits + P.sizeof*8) U;
69 		alias Signed!U S;
70 		typeof(this) r;
71 		foreach (i, f; r.tupleof)
72 			static if (r.tupleof[i].stringof != "r.x") // skip padding
73 				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);
74 		return r;
75 	}
76 
77 	/// Alpha-blend two colors.
78 	static typeof(this) blend()(typeof(this) c0, typeof(this) c1)
79 		if (is(typeof(a)))
80 	{
81 		alias A = typeof(c0.a);
82 		A a = ~cast(A)(~c0.a * ~c1.a / A.max);
83 		if (!a)
84 			return typeof(this).init;
85 		A x = cast(A)(c1.a * A.max / a);
86 
87 		typeof(this) r;
88 		foreach (i, f; r.tupleof)
89 			static if (r.tupleof[i].stringof == "r.x")
90 				{} // skip padding
91 			else
92 			static if (r.tupleof[i].stringof == "r.a")
93 				r.a = a;
94 			else
95 			{
96 				auto v0 = c0.tupleof[i];
97 				auto v1 = c1.tupleof[i];
98 				auto vr = .blend(v1, v0, x);
99 				r.tupleof[i] = vr;
100 			}
101 		return r;
102 	}
103 
104 	/// Construct an RGB color from a typical hex string.
105 	static if (is(typeof(this.r) == ubyte) && is(typeof(this.g) == ubyte) && is(typeof(this.b) == ubyte))
106 	static typeof(this) fromHex(in char[] s)
107 	{
108 		import std.conv;
109 		import std.exception;
110 
111 		enforce(s.length == 6, "Invalid color string");
112 		typeof(this) c;
113 		c.r = s[0..2].to!ubyte(16);
114 		c.g = s[2..4].to!ubyte(16);
115 		c.b = s[4..6].to!ubyte(16);
116 		return c;
117 	}
118 
119 	/// Warning: overloaded operators preserve types and may cause overflows
120 	typeof(this) opUnary(string op)()
121 		if (op=="~" || op=="-")
122 	{
123 		typeof(this) r;
124 		foreach (i, f; r.tupleof)
125 			static if(r.tupleof[i].stringof != "r.x") // skip padding
126 				r.tupleof[i] = cast(typeof(r.tupleof[i])) mixin(op ~ `this.tupleof[i]`);
127 		return r;
128 	}
129 
130 	/// ditto
131 	typeof(this) opOpAssign(string op)(int o)
132 	{
133 		foreach (i, f; this.tupleof)
134 			static if(this.tupleof[i].stringof != "this.x") // skip padding
135 				this.tupleof[i] = cast(typeof(this.tupleof[i])) mixin(`this.tupleof[i]` ~ op ~ `=o`);
136 		return this;
137 	}
138 
139 	/// ditto
140 	typeof(this) opOpAssign(string op, T)(T o)
141 		if (is(T==struct) && structFields!T == structFields!Fields)
142 	{
143 		foreach (i, f; this.tupleof)
144 			static if(this.tupleof[i].stringof != "this.x") // skip padding
145 				this.tupleof[i] = cast(typeof(this.tupleof[i])) mixin(`this.tupleof[i]` ~ op ~ `=o.tupleof[i]`);
146 		return this;
147 	}
148 
149 	/// ditto
150 	typeof(this) opBinary(string op, T)(T o)
151 		if (op != "~")
152 	{
153 		auto r = this;
154 		mixin("r" ~ op ~ "=o;");
155 		return r;
156 	}
157 
158 	/// Apply a custom operation for each channel. Example:
159 	/// COLOR.op!q{(a + b) / 2}(colorA, colorB);
160 	static typeof(this) op(string expr, T...)(T values)
161 	{
162 		static assert(values.length <= 10);
163 
164 		string genVars(string channel)
165 		{
166 			string result;
167 			foreach (j, Tj; T)
168 			{
169 				static if (is(Tj == struct)) // TODO: tighter constraint (same color channels)?
170 					result ~= "auto " ~ cast(char)('a' + j) ~ " = values[" ~ cast(char)('0' + j) ~ "]." ~  channel ~ ";\n";
171 				else
172 					result ~= "auto " ~ cast(char)('a' + j) ~ " = values[" ~ cast(char)('0' + j) ~ "];\n";
173 			}
174 			return result;
175 		}
176 
177 		typeof(this) r;
178 		foreach (i, f; r.tupleof)
179 			static if(r.tupleof[i].stringof != "r.x") // skip padding
180 			{
181 				mixin(genVars(r.tupleof[i].stringof[2..$]));
182 				r.tupleof[i] = mixin(expr);
183 			}
184 		return r;
185 	}
186 
187 	T opCast(T)()
188 		if (is(T==struct) && structFields!T == structFields!Fields)
189 	{
190 		T t;
191 		foreach (i, f; this.tupleof)
192 			t.tupleof[i] = cast(typeof(t.tupleof[i])) this.tupleof[i];
193 		return t;
194 	}
195 
196 	/// Sum of all channels
197 	UnsignedBitsType!(channelBits + ilog2(nextPowerOfTwo(channels))) sum()
198 	{
199 		typeof(return) result;
200 		foreach (i, f; this.tupleof)
201 			static if (this.tupleof[i].stringof != "this.x") // skip padding
202 				result += this.tupleof[i];
203 		return result;
204 	}
205 }
206 
207 // The "x" has the special meaning of "padding" and is ignored in some circumstances
208 alias Color!(ubyte  , "r", "g", "b"     ) RGB    ;
209 alias Color!(ushort , "r", "g", "b"     ) RGB16  ;
210 alias Color!(ubyte  , "r", "g", "b", "x") RGBX   ;
211 alias Color!(ushort , "r", "g", "b", "x") RGBX16 ;
212 alias Color!(ubyte  , "r", "g", "b", "a") RGBA   ;
213 alias Color!(ushort , "r", "g", "b", "a") RGBA16 ;
214 
215 alias Color!(ubyte  , "b", "g", "r"     ) BGR    ;
216 alias Color!(ubyte  , "b", "g", "r", "x") BGRX   ;
217 alias Color!(ubyte  , "b", "g", "r", "a") BGRA   ;
218 
219 alias Color!(ubyte  , "l"               ) L8     ;
220 alias Color!(ushort , "l"               ) L16    ;
221 alias Color!(ubyte  , "l", "a"          ) LA     ;
222 alias Color!(ushort , "l", "a"          ) LA16   ;
223 
224 alias Color!(byte   , "l"               ) S8     ;
225 alias Color!(short  , "l"               ) S16    ;
226 
227 unittest
228 {
229 	static assert(RGB.sizeof == 3);
230 	RGB[2] arr;
231 	static assert(arr.sizeof == 6);
232 
233 	RGB hex = RGB.fromHex("123456");
234 	assert(hex.r == 0x12 && hex.g == 0x34 && hex.b == 0x56);
235 
236 	assert(RGB(1, 2, 3) + RGB(4, 5, 6) == RGB(5, 7, 9));
237 
238 	RGB c = RGB(1, 1, 1);
239 	c += 1;
240 	assert(c == RGB(2, 2, 2));
241 	c += c;
242 	assert(c == RGB(4, 4, 4));
243 }
244 
245 unittest
246 {
247 	import std.conv;
248 
249 	LA r;
250 
251 	r = LA.blend(LA(123,   0),
252 	             LA(111, 222));
253 	assert(r ==  LA(111, 222), text(r));
254 
255 	r = LA.blend(LA(123, 213),
256 	             LA(111, 255));
257 	assert(r ==  LA(111, 255), text(r));
258 
259 	r = LA.blend(LA(  0, 255),
260 	             LA(255, 100));
261 	assert(r ==  LA(100, 255), text(r));
262 }
263 
264 /// Obtains the type of each channel for homogenous colors.
265 template ChannelType(T)
266 {
267 	static if (is(T == struct))
268 		alias ChannelType = T.ChannelType;
269 	else
270 		alias ChannelType = T;
271 }
272 
273 /// Resolves to a Color instance with a different ChannelType.
274 template ChangeChannelType(COLOR, T)
275 	if (isNumeric!COLOR)
276 {
277 	alias ChangeChannelType = T;
278 }
279 
280 /// ditto
281 template ChangeChannelType(COLOR, T)
282 	if (is(COLOR : Color!Spec, Spec...))
283 {
284 	static assert(COLOR.homogenous, "Can't change ChannelType of non-homogenous Color");
285 	alias ChangeChannelType = Color!(T, COLOR.Spec[1..$]);
286 }
287 
288 static assert(is(ChangeChannelType!(RGB, ushort) == RGB16));
289 static assert(is(ChangeChannelType!(int, ushort) == ushort));
290 
291 alias ExpandChannelType(COLOR, int BYTES) =
292 	ChangeChannelType!(COLOR,
293 		UnsignedBitsType!((ChannelType!COLOR.sizeof + BYTES) * 8));
294 
295 static assert(is(ExpandChannelType!(RGB, 1) == RGB16));
296 
297 // ***************************************************************************
298 
299 // TODO: deprecate
300 T blend(T)(T f, T b, T a) if (is(typeof(f*a+~b))) { return cast(T) ( ((f*a) + (b*~a)) / T.max ); }