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.meta : AliasSeq;
17 import std.traits;
18 
19 import ae.utils.math;
20 import ae.utils.meta;
21 
22 /// Represents a tuple of samples, usually used to represent
23 /// a single color in some color space.
24 /// This helper type allows manipulating such tuples more easily,
25 /// and has special behavior for common color representations
26 /// (e.g. special treatment of the "a" field as an alpha channel,
27 /// and construction from hex strings for R/G/B colors).
28 /// `FieldTuple` is a field spec, as parsed by `ae.utils.meta.FieldList`.
29 /// By convention, each field's name indicates its purpose:
30 /// - `x`: padding
31 /// - `a`: alpha
32 /// - `l`: lightness (or grey, for monochrome images)
33 /// - others (`r`, `g`, `b`, etc.): color information
34 
35 // TODO: figure out if we need alll these methods in the color type itself
36 // - code such as gamma conversion needs to create color types
37 //   - ReplaceType can't copy methods
38 //   - even if we move out all conventional methods, that still leaves operator overloading
39 
40 struct Color(FieldTuple...)
41 {
42 	alias Spec = FieldTuple; ///
43 	mixin FieldList!FieldTuple;
44 
45 	// A "dumb" type to avoid cyclic references.
46 	private struct Fields { mixin FieldList!FieldTuple; }
47 
48 	/// Whether or not all channel fields have the same base type.
49 	// Only "true" supported for now, may change in the future (e.g. for 5:6:5)
50 	enum homogeneous = isHomogeneous!Fields();
51 	deprecated alias homogenous = homogeneous;
52 
53 	/// The number of fields in this color type.
54 	enum channels = Fields.init.tupleof.length;
55 
56 	/// Additional properties for homogeneous colors.
57 	static if (homogeneous)
58 	{
59 		alias ChannelType = typeof(Fields.init.tupleof[0]);
60 		static if (is(typeof(valueBits!ChannelType)))
61 			enum channelBits = valueBits!ChannelType;
62 	}
63 
64 	/// Return a Color instance with all fields set to "value".
65 	static typeof(this) monochrome(ChannelType value)
66 	{
67 		typeof(this) r;
68 		foreach (i, f; r.tupleof)
69 			static if (__traits(identifier, r.tupleof[i]) == "a")
70 				r.tupleof[i] = typeof(r.tupleof[i]).max;
71 			else
72 				r.tupleof[i] = value;
73 		return r;
74 	}
75 
76 	/// Additional properties for integer colors.
77 	static if (is(ChannelType:uint))
78 	{
79 		enum typeof(this) black = monochrome(0);
80 		enum typeof(this) white = monochrome(ChannelType.max);
81 	}
82 
83 	/// Interpolate between two colors.
84 	/// See also: Gradient
85 	static typeof(this) itpl(P)(typeof(this) c0, typeof(this) c1, P p, P p0, P p1)
86 	{
87 		alias TryExpandNumericType!(ChannelType, P.sizeof*8) U;
88 		typeof(this) r;
89 		foreach (i, f; r.tupleof)
90 			static if (r.tupleof[i].stringof != "r.x") // skip padding
91 				r.tupleof[i] = cast(ChannelType).itpl(cast(U)c0.tupleof[i], cast(U)c1.tupleof[i], p, p0, p1);
92 		return r;
93 	}
94 
95 	/// Alpha-blend two colors.
96 	static typeof(this) blend()(typeof(this) c0, typeof(this) c1)
97 		if (is(typeof(a)))
98 	{
99 		alias A = typeof(c0.a);
100 		A a = flipBits(cast(A)(c0.a.flipBits * c1.a.flipBits / A.max));
101 		if (!a)
102 			return typeof(this).init;
103 		A x = cast(A)(c1.a * A.max / a);
104 
105 		typeof(this) r;
106 		foreach (i, f; r.tupleof)
107 			static if (r.tupleof[i].stringof == "r.x")
108 				{} // skip padding
109 			else
110 			static if (r.tupleof[i].stringof == "r.a")
111 				r.a = a;
112 			else
113 			{
114 				auto v0 = c0.tupleof[i];
115 				auto v1 = c1.tupleof[i];
116 				auto vr = ._blend(v1, v0, x);
117 				r.tupleof[i] = vr;
118 			}
119 		return r;
120 	}
121 
122 	/// Alpha-blend a color with an alpha channel on top of one without.
123 	static typeof(this) blend(C)(typeof(this) c0, C c1)
124 		if (!is(typeof(a)) && is(typeof(c1.a)))
125 	{
126 		alias A = typeof(c1.a);
127 		if (!c1.a)
128 			return c0;
129 		//A x = cast(A)(c1.a * A.max / a);
130 
131 		typeof(this) r;
132 		foreach (i, ref f; r.tupleof)
133 		{
134 			enum name = __traits(identifier, r.tupleof[i]);
135 			static if (name == "x")
136 				{} // skip padding
137 			else
138 			static if (name == "a")
139 				static assert(false);
140 			else
141 			{
142 				auto v0 = __traits(getMember, c0, name);
143 				auto v1 = __traits(getMember, c1, name);
144 				f = ._blend(v1, v0, c1.a);
145 			}
146 		}
147 		return r;
148 	}
149 
150 	/// Construct an RGB color from a typical hex string.
151 	static if (is(typeof(this.r) == ubyte) && is(typeof(this.g) == ubyte) && is(typeof(this.b) == ubyte))
152 	{
153 		static typeof(this) fromHex(in char[] s)
154 		{
155 			import std.conv;
156 			import std.exception;
157 
158 			enforce(s.length == 6 || (is(typeof(this.a) == ubyte) && s.length == 8), "Invalid color string");
159 			typeof(this) c;
160 			c.r = s[0..2].to!ubyte(16);
161 			c.g = s[2..4].to!ubyte(16);
162 			c.b = s[4..6].to!ubyte(16);
163 			static if (is(typeof(this.a) == ubyte))
164 			{
165 				if (s.length == 8)
166 					c.a = s[6..8].to!ubyte(16);
167 				else
168 					c.a = ubyte.max;
169 			}
170 			return c;
171 		}
172 
173 		string toHex() const
174 		{
175 			import std.string;
176 			return format("%02X%02X%02X", r, g, b);
177 		}
178 	}
179 
180 	/// Warning: overloaded operators preserve types and may cause overflows
181 	typeof(this) opUnary(string op)()
182 		if (op=="~" || op=="-")
183 	{
184 		typeof(this) r;
185 		foreach (i, f; r.tupleof)
186 			static if(r.tupleof[i].stringof != "r.x") // skip padding
187 				r.tupleof[i] = cast(typeof(r.tupleof[i])) unary!(op[0])(this.tupleof[i]);
188 		return r;
189 	}
190 
191 	/// ditto
192 	typeof(this) opOpAssign(string op)(int o)
193 	{
194 		foreach (i, f; this.tupleof)
195 			static if(this.tupleof[i].stringof != "this.x") // skip padding
196 				this.tupleof[i] = cast(typeof(this.tupleof[i])) mixin(`this.tupleof[i]` ~ op ~ `=o`);
197 		return this;
198 	}
199 
200 	/// ditto
201 	typeof(this) opOpAssign(string op, T)(T o)
202 		if (is(T==struct) && structFields!T == structFields!Fields)
203 	{
204 		foreach (i, f; this.tupleof)
205 			static if(this.tupleof[i].stringof != "this.x") // skip padding
206 				this.tupleof[i] = cast(typeof(this.tupleof[i])) mixin(`this.tupleof[i]` ~ op ~ `=o.tupleof[i]`);
207 		return this;
208 	}
209 
210 	/// ditto
211 	typeof(this) opBinary(string op, T)(T o)
212 		if (op != "~" && op != "in")
213 	{
214 		auto r = this;
215 		mixin("r" ~ op ~ "=o;");
216 		return r;
217 	}
218 
219 	/// Apply a custom operation for each channel. Example:
220 	/// COLOR.op!q{(a + b) / 2}(colorA, colorB);
221 	static typeof(this) op(string expr, T...)(T values)
222 	{
223 		static assert(values.length <= 10);
224 
225 		string genVars(string channel)
226 		{
227 			string result;
228 			foreach (j, Tj; T)
229 			{
230 				static if (is(Tj == struct)) // TODO: tighter constraint (same color channels)?
231 					result ~= "auto " ~ cast(char)('a' + j) ~ " = values[" ~ cast(char)('0' + j) ~ "]." ~  channel ~ ";\n";
232 				else
233 					result ~= "auto " ~ cast(char)('a' + j) ~ " = values[" ~ cast(char)('0' + j) ~ "];\n";
234 			}
235 			return result;
236 		}
237 
238 		typeof(this) r;
239 		foreach (i, f; r.tupleof)
240 			static if(r.tupleof[i].stringof != "r.x") // skip padding
241 			{
242 				mixin(genVars(r.tupleof[i].stringof[2..$]));
243 				r.tupleof[i] = mixin(expr);
244 			}
245 		return r;
246 	}
247 
248 	/// Perform a per-channel hard-cast.
249 	/// Provides continuity with casting of D basic types.
250 	/// If this is not needed, consider using `channelMap` instead.
251 	T opCast(T)() const
252 	if (is(T==struct) && structFields!T == structFields!Fields)
253 	{
254 		static if (is(T == typeof(this)))
255 			return this;
256 		else
257 		{
258 			T t;
259 			foreach (i, f; this.tupleof)
260 				t.tupleof[i] = cast(typeof(t.tupleof[i])) this.tupleof[i];
261 			return t;
262 		}
263 	}
264 
265 	/// Sum of all channels
266 	static if (is(ExpandIntegerType!(ChannelType, ilog2(nextPowerOfTwo(channels)))))
267 	ExpandIntegerType!(ChannelType, ilog2(nextPowerOfTwo(channels))) sum()
268 	{
269 		typeof(return) result;
270 		foreach (i, f; this.tupleof)
271 			static if (this.tupleof[i].stringof != "this.x") // skip padding
272 				result += this.tupleof[i];
273 		return result;
274 	}
275 
276 	/// Returns an instance of this color type
277 	/// with all fields set at their minimum values.
278 	static @property Color min()
279 	{
280 		Color result;
281 		foreach (ref v; result.tupleof)
282 			static if (is(typeof(typeof(v).min)))
283 				v = typeof(v).min;
284 			else
285 			static if (is(typeof(typeof(v).max)))
286 				v = -typeof(v).max;
287 		return result;
288 	}
289 
290 	/// Returns an instance of this color type
291 	/// with all fields set at their maximum values.
292 	static @property Color max()
293 	{
294 		Color result;
295 		foreach (ref v; result.tupleof)
296 			static if (is(typeof(typeof(v).max)))
297 				v = typeof(v).max;
298 		return result;
299 	}
300 }
301 
302 // The "x" has the special meaning of "padding" and is ignored in some circumstances
303 
304 /// Definitions for common color types.
305 version(all)
306 {
307 	alias Color!(ubyte  , "r", "g", "b"     ) RGB    ;
308 	alias Color!(ushort , "r", "g", "b"     ) RGB16  ;
309 	alias Color!(ubyte  , "r", "g", "b", "x") RGBX   ;
310 	alias Color!(ushort , "r", "g", "b", "x") RGBX16 ;
311 	alias Color!(ubyte  , "r", "g", "b", "a") RGBA   ;
312 	alias Color!(ushort , "r", "g", "b", "a") RGBA16 ;
313 
314 	alias Color!(ubyte  , "b", "g", "r"     ) BGR    ;
315 	alias Color!(ubyte  , "b", "g", "r", "x") BGRX   ;
316 	alias Color!(ubyte  , "b", "g", "r", "a") BGRA   ;
317 
318 	alias Color!(ubyte  , "l"               ) L8     ;
319 	alias Color!(ushort , "l"               ) L16    ;
320 	alias Color!(ubyte  , "l", "a"          ) LA     ;
321 	alias Color!(ushort , "l", "a"          ) LA16   ;
322 
323 	alias Color!(byte   , "l"               ) S8     ;
324 	alias Color!(short  , "l"               ) S16    ;
325 
326 	alias Color!(float  , "r", "g", "b"     ) RGBf   ;
327 	alias Color!(double , "r", "g", "b"     ) RGBd   ;
328 }
329 
330 unittest
331 {
332 	static assert(RGB.sizeof == 3);
333 	RGB[2] arr;
334 	static assert(arr.sizeof == 6);
335 
336 	RGB hex = RGB.fromHex("123456");
337 	assert(hex.r == 0x12 && hex.g == 0x34 && hex.b == 0x56);
338 
339 	BGRA hex2 = BGRA.fromHex("12345678");
340 	assert(hex2.r == 0x12 && hex2.g == 0x34 && hex2.b == 0x56 && hex2.a == 0x78);
341 
342 	assert(RGB(1, 2, 3) + RGB(4, 5, 6) == RGB(5, 7, 9));
343 
344 	RGB c = RGB(1, 1, 1);
345 	c += 1;
346 	assert(c == RGB(2, 2, 2));
347 	c += c;
348 	assert(c == RGB(4, 4, 4));
349 }
350 
351 static assert(RGB.min == RGB(  0,   0,   0));
352 static assert(RGB.max == RGB(255, 255, 255));
353 
354 unittest
355 {
356 	import std.conv;
357 
358 	L8 r;
359 
360 	r = L8.itpl(L8(100), L8(200), 15, 10, 20);
361 	assert(r ==  L8(150), text(r));
362 }
363 
364 unittest
365 {
366 	import std.conv;
367 
368 	LA r;
369 
370 	r = LA.blend(LA(123,   0),
371 	             LA(111, 222));
372 	assert(r ==  LA(111, 222), text(r));
373 
374 	r = LA.blend(LA(123, 213),
375 	             LA(111, 255));
376 	assert(r ==  LA(111, 255), text(r));
377 
378 	r = LA.blend(LA(  0, 255),
379 	             LA(255, 100));
380 	assert(r ==  LA(100, 255), text(r));
381 }
382 
383 unittest
384 {
385 	import std.conv;
386 
387 	L8 r;
388 
389 	r = L8.blend(L8(123),
390 	             LA(231, 0));
391 	assert(r ==  L8(123), text(r));
392 
393 	r = L8.blend(L8(123),
394 	             LA(231, 255));
395 	assert(r ==  L8(231), text(r));
396 
397 	r = L8.blend(L8(  0),
398 	             LA(255, 100));
399 	assert(r ==  L8(100), text(r));
400 }
401 
402 unittest
403 {
404 	Color!(real, "r", "g", "b") c;
405 }
406 
407 deprecated unittest
408 {
409 	const RGB c;
410 	RGB x = cast(RGB)c;
411 }
412 
413 /// Obtains the type of each channel for homogeneous colors.
414 template ChannelType(T)
415 {
416 	///
417 	static if (is(T == struct))
418 		alias ChannelType = T.ChannelType;
419 	else
420 		alias ChannelType = T;
421 }
422 
423 private template TransformSpec(alias Transformer, Spec...)
424 {
425 	static if (Spec.length == 0)
426 		alias TransformSpec = Spec;
427 	else
428 	static if (is(typeof(Spec[0]) == string))
429 		alias TransformSpec = AliasSeq!(Spec[0], TransformSpec!(Transformer, Spec[1 .. $]));
430 	else
431 		alias TransformSpec = AliasSeq!(Transformer!(Spec[0]), TransformSpec!(Transformer, Spec[1 .. $]));
432 }
433 
434 /// Resolves to a Color instance with a different ChannelType.
435 template TransformChannelType(COLOR, alias Transformer)
436 	if (isNumeric!COLOR)
437 {
438 	alias TransformChannelType = Transformer!COLOR;
439 }
440 
441 /// ditto
442 template TransformChannelType(COLOR, alias Transformer)
443 	if (is(COLOR : Color!Spec, Spec...))
444 {
445 	static if (is(COLOR : Color!Spec, Spec...))
446 		alias TransformChannelType = Color!(TransformSpec!(Transformer, Spec));
447 }
448 
449 /// ditto
450 template ChangeChannelType(COLOR, T)
451 {
452 	alias Transformer(_) = T;
453 	alias ChangeChannelType = TransformChannelType!(COLOR, Transformer);
454 }
455 
456 static assert(is(ChangeChannelType!(RGB, ushort) == RGB16));
457 static assert(is(ChangeChannelType!(int, ushort) == ushort));
458 
459 /// Wrapper around ExpandNumericType to only expand integer types.
460 template ExpandIntegerType(T, size_t bits)
461 {
462 	///
463 	static if (is(T:real))
464 		alias ExpandIntegerType = T;
465 	else
466 		alias ExpandIntegerType = ExpandNumericType!(T, bits);
467 }
468 
469 /// Resolves to a Color instance with its ChannelType expanded by BYTES bytes.
470 alias ExpandChannelType(COLOR, int BYTES) =
471 	ChangeChannelType!(COLOR,
472 		ExpandNumericType!(ChannelType!COLOR, BYTES * 8));
473 
474 /// Resolves to a Color instance with its ChannelType expanded by BYTES bytes and made signed.
475 alias ExpandChannelTypeSigned(COLOR, int BYTES) =
476 	ChangeChannelType!(COLOR,
477 		Signed!(ExpandNumericType!(ChannelType!COLOR, BYTES * 8)));
478 
479 static assert(is(ExpandChannelType!(RGB, 1) == RGB16));
480 
481 unittest
482 {
483 	alias RGBf = ChangeChannelType!(RGB, float);
484 	auto rgb = RGB(1, 2, 3);
485 	import std.conv : to;
486 	auto rgbf = rgb.channelMap!RGBf();
487 	assert(rgbf.r == 1f);
488 	assert(rgbf.g == 2f);
489 	assert(rgbf.b == 3f);
490 }
491 
492 /// Applies a transformation per-channel.
493 /// `expr` may be in the form of `a => ...`, or `(a, ref b) { b = ...; }` (only if `T` is explicitly specified).
494 template channelMap(T, alias expr = a => a)
495 if (is(T == struct))
496 {
497 	auto channelMap(COLOR)(COLOR color)
498 	if (is(COLOR == struct))
499 	{
500 		T result;
501 		foreach (i, f; color.tupleof)
502 		{
503 			enum name = __traits(identifier, color.tupleof[i]);
504 			static assert(__traits(hasMember, result, name),
505 				"No matching field `" ~ name ~ "` in `" ~ T.stringof ~ "` when mapping from `" ~ COLOR.stringof ~ "`");
506 
507 			static if (is(typeof(__traits(getMember, result, name) = expr(__traits(getMember, color, name)))))
508 				__traits(getMember, result, name) = expr(__traits(getMember, color, name));
509 			else
510 			static if (is(typeof(expr(__traits(getMember, color, name), __traits(getMember, result, name)))))
511 				expr(__traits(getMember, color, name), __traits(getMember, result, name));
512 			else
513 			{
514 				pragma(msg, "channelMap expression does not compile with either 1 or 2 arguments:");
515 				expr(__traits(getMember, color, name), __traits(getMember, result, name));
516 				__traits(getMember, result, name) = expr(__traits(getMember, color, name));
517 			}
518 		}
519 		foreach (i, f; result.tupleof)
520 		{
521 			enum name = __traits(identifier, result.tupleof[i]);
522 			static assert(__traits(hasMember, color, name),
523 				"No matching field `" ~ name ~ "` in `" ~ COLOR.stringof ~ "` when mapping to `" ~ T.stringof ~ "`");
524 		}
525 		return result;
526 	}
527 }
528 
529 /// ditto
530 template channelMap(alias expr)
531 if (!is(expr == struct))
532 {
533 	auto channelMap(COLOR)(COLOR c)
534 	if (is(COLOR == struct) && !is(expr == struct))
535 	{
536 		alias Transformer(C) = typeof({ C v = void; return expr(v); }());
537 		alias T = TransformChannelType!(typeof(c), Transformer);
538 		return c.channelMap!(T, expr);
539 	}
540 }
541 
542 ///
543 unittest
544 {
545 	// Effortlessly reordering channels with no modification.
546 	assert(RGB(1, 2, 3).channelMap!BGR == BGR(3, 2, 1));
547 
548 	// Perform per-channel transformations.
549 	assert(RGB(1, 2, 3).channelMap!(v => cast(ubyte)(v + 1)) == RGB(2, 3, 4));
550 
551 	// Perform per-channel transformations with a different resulting type, implicitly.
552 	assert(RGB(1, 2, 3).channelMap!(v => cast(ushort)(v + 1)) == RGB16(2, 3, 4));
553 
554 	// Perform per-channel transformations with a different resulting type, explicitly.
555 	assert(RGB(1, 2, 3).channelMap!(RGB16, v => cast(ubyte)(v + 1)) == RGB16(2, 3, 4));
556 
557 	// Ditto, with a ref parameter instead of return.
558 	assert(RGB(1, 2, 3).channelMap!(RGB16, (a, ref b) { b = a; b++; }) == RGB16(2, 3, 4));
559 
560 	// Effortlessly convert an image
561 	import ae.utils.graphics.image : Image, colorMap, copy;
562 	Image!BGR i = Image!RGB().colorMap!(channelMap!BGR).copy();
563 }
564 
565 // ***************************************************************************
566 
567 /// Color storage unit for as-is storage.
568 alias PlainStorageUnit(Color) = Color[1];
569 
570 /// Color storage unit description for packed bit colors
571 /// (1-bit, 2-bit, 4-bit etc.)
572 struct BitStorageUnit(ValueType, size_t valueBits, StorageType, bool bigEndian)
573 {
574 	StorageType storageValue; /// Raw value.
575 
576 	/// Array operations.
577 	enum length = StorageType.sizeof * 8 / valueBits;
578 	static assert(length * valueBits == StorageType.sizeof * 8, "Slack bits?");
579 
580 	ValueType opIndex(size_t index) const
581 	{
582 		static if (bigEndian)
583 			index = length - 1 - index;
584 		auto shift = index * valueBits;
585 		return cast(ValueType)((storageValue >> shift) & valueMask);
586 	} /// ditto
587 
588 	ValueType opIndexAssign(ValueType value, size_t index)
589 	{
590 		static if (bigEndian)
591 			index = length - 1 - index;
592 		auto shift = index * valueBits;
593 		StorageType mask = flipBits(cast(StorageType)(valueMask << shift));
594 		storageValue = (storageValue & mask) | cast(StorageType)(cast(StorageType)value << shift);
595 		return value;
596 	} /// ditto
597 private:
598 	enum StorageType valueMask = ((cast(StorageType)1) << valueBits) - 1;
599 }
600 
601 /// 8 monochrome bits packed into a byte, in the usual big-endian order.
602 alias OneBitStorageBE = BitStorageUnit!(bool, 1, ubyte, true);
603 /// As above, but in little-endian order.
604 alias OneBitStorageLE = BitStorageUnit!(bool, 1, ubyte, false);
605 
606 /// Get the color value of a storage unit type.
607 alias StorageColor(StorageType) = typeof(StorageType.init[0]);
608 
609 /// The number of bits that one individual color takes up.
610 enum size_t storageColorBits(StorageType) = StorageType.sizeof * 8 / StorageType.length;
611 
612 /// True when we can take the address of an individual color within a storage unit.
613 enum bool isStorageColorLValue(StorageType) = is(typeof({ StorageType s = void; return &s[0]; }()));
614 
615 /// Construct a `StorageType` with all colors set to the indicated value.
616 StorageType solidStorageUnit(StorageType)(StorageColor!StorageType color)
617 {
618 	StorageType s;
619 	foreach (i; 0 .. StorageType.length)
620 		s[i] = color;
621 	return s;
622 }
623 
624 // ***************************************************************************
625 
626 /// Calculate an interpolated color on a gradient with multiple points
627 struct Gradient(Value, Color)
628 {
629 	/// Gradient points.
630 	struct Point
631 	{
632 		Value value; /// Distance along the gradient.
633 		Color color; /// Color at this point.
634 	}
635 	Point[] points; /// ditto
636 
637 	/// Obtain the value at the given position.
638 	/// If `value` is before the first point, the first point's color is returned.
639 	/// If `value` is after the last point, the last point's color is returned.
640 	Color get(Value value) const
641 	{
642 		assert(points.length, "Gradient must have at least one point");
643 
644 		if (value <= points[0].value)
645 			return points[0].color;
646 
647 		for (size_t i = 1; i < points.length; i++)
648 		{
649 			assert(points[i-1].value <= points[i].value,
650 				"Gradient values are not in ascending order");
651 			if (value < points[i].value)
652 				return Color.itpl(
653 					points[i-1].color, points[i].color, value,
654 					points[i-1].value, points[i].value);
655 		}
656 
657 		return points[$-1].color;
658 	}
659 }
660 
661 unittest
662 {
663 	Gradient!(int, L8) grad;
664 	grad.points = [
665 		grad.Point(0, L8(0)),
666 		grad.Point(10, L8(100)),
667 	];
668 
669 	assert(grad.get(-5) == L8(  0));
670 	assert(grad.get( 0) == L8(  0));
671 	assert(grad.get( 5) == L8( 50));
672 	assert(grad.get(10) == L8(100));
673 	assert(grad.get(15) == L8(100));
674 }
675 
676 unittest
677 {
678 	Gradient!(float, L8) grad;
679 	grad.points = [
680 		grad.Point(0.0f, L8( 0)),
681 		grad.Point(0.5f, L8(10)),
682 		grad.Point(1.0f, L8(30)),
683 	];
684 
685 	assert(grad.get(0.00f) == L8(  0));
686 	assert(grad.get(0.25f) == L8(  5));
687 	assert(grad.get(0.50f) == L8( 10));
688 	assert(grad.get(0.75f) == L8( 20));
689 	assert(grad.get(1.00f) == L8( 30));
690 }
691 
692 // ***************************************************************************
693 
694 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 ); }
695 deprecated alias blend = _blend;