1 /**
2  * Drawing functions.
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.draw;
15 
16 import std.algorithm : sort;
17 import std.traits;
18 
19 import ae.utils.geometry : TAU;
20 import ae.utils.graphics.view;
21 import ae.utils.math;
22 import ae.utils.meta : structFields, SignedBitsType, UnsignedBitsType;
23 
24 version(unittest) import ae.utils.graphics.image;
25 
26 // Constraints could be simpler if this was fixed:
27 // https://d.puremagic.com/issues/show_bug.cgi?id=12386
28 
29 /// Get the pixel color at the specified coordinates,
30 /// or fall back to the specified default value if
31 /// the coordinates are out of bounds.
32 COLOR safeGet(V, COLOR)(auto ref V v, int x, int y, COLOR def)
33 	if (isView!V && is(COLOR : ViewColor!V))
34 {
35 	if (x>=0 && y>=0 && x<v.w && y<v.h)
36 		return v[x, y];
37 	else
38 		return def;
39 }
40 
41 unittest
42 {
43 	auto v = onePixel(7);
44 	assert(v.safeGet(0, 0, 0) == 7);
45 	assert(v.safeGet(0, 1, 0) == 0);
46 }
47 
48 /// Set the pixel color at the specified coordinates
49 /// if the coordinates are not out of bounds.
50 void safePut(V, COLOR)(auto ref V v, int x, int y, COLOR value)
51 	if (isWritableView!V && is(COLOR : ViewColor!V))
52 {
53 	if (x>=0 && y>=0 && x<v.w && y<v.h)
54 		v[x, y] = value;
55 }
56 
57 unittest
58 {
59 	auto v = Image!int(1, 1);
60 	v.safePut(0, 0, 7);
61 	v.safePut(0, 1, 9);
62 	assert(v[0, 0] == 7);
63 }
64 
65 /// Forwards to safePut or opIndex, depending on the
66 /// CHECKED parameter. Allows propagation of a
67 /// CHECKED parameter from other callers.
68 void putPixel(bool CHECKED, V, COLOR)(auto ref V v, int x, int y, COLOR value)
69 	if (isWritableView!V && is(COLOR : ViewColor!V))
70 {
71 	static if (CHECKED)
72 		v.safePut(x, y, value);
73 	else
74 		v[x, y] = value;
75 }
76 
77 unittest
78 {
79 	auto v = Image!int(1, 1);
80 	v.putPixel!false(0, 0, 7);
81 	v.putPixel!true(0, 1, 9);
82 	assert(v[0, 0] == 7);
83 }
84 
85 /// Gets a pixel's address from a direct view.
86 ViewColor!V* pixelPtr(V)(auto ref V v, int x, int y)
87 	if (isDirectView!V)
88 {
89 	return &v.scanline(y)[x];
90 }
91 
92 unittest
93 {
94 	auto v = Image!int(1, 1);
95 	v[0, 0] = 7;
96 	auto p = v.pixelPtr(0, 0);
97 	assert(*p == 7);
98 }
99 
100 /// Fills a writable view with a solid color.
101 void fill(V, COLOR)(auto ref V v, COLOR c)
102 	if (isWritableView!V
103 	 && is(COLOR : ViewColor!V))
104 {
105 	foreach (y; 0..v.h)
106 	{
107 		static if (isDirectView!V)
108 			v.scanline(y)[] = c;
109 		else
110 			foreach (x; 0..v.w)
111 				v[x, y] = c;
112 	}
113 }
114 deprecated alias clear = fill;
115 
116 unittest
117 {
118 	auto i = onePixel(0).copy();
119 	i.fill(1);
120 	assert(i[0, 0] == 1);
121 	auto t = i.tile(10, 10);
122 	t.fill(2);
123 	assert(i[0, 0] == 2);
124 }
125 
126 // ***************************************************************************
127 
128 enum CheckHLine =
129 q{
130 	static if (CHECKED)
131 	{
132 		if (x1 >= v.w || x2 <= 0 || y < 0 || y >= v.h || x1 >= x2) return;
133 		if (x1 <    0) x1 =   0;
134 		if (x2 >= v.w) x2 = v.w;
135 	}
136 	assert(x1 <= x2);
137 };
138 
139 enum CheckVLine =
140 q{
141 	static if (CHECKED)
142 	{
143 		if (x < 0 || x >= v.w || y1 >= v.h || y2 <= 0 || y1 >= y2) return;
144 		if (y1 <    0) y1 =   0;
145 		if (y2 >= v.h) y2 = v.h;
146 	}
147 	assert(y1 <= y2);
148 };
149 
150 void hline(bool CHECKED=true, V, COLOR)(auto ref V v, int x1, int x2, int y, COLOR c)
151 	if (isWritableView!V && is(COLOR : ViewColor!V))
152 {
153 	mixin(CheckHLine);
154 	static if (isDirectView!V)
155 		v.scanline(y)[x1..x2] = c;
156 	else
157 		foreach (x; x1..x2)
158 			v[x, y] = c;
159 }
160 
161 void vline(bool CHECKED=true, V, COLOR)(auto ref V v, int x, int y1, int y2, COLOR c)
162 	if (isWritableView!V && is(COLOR : ViewColor!V))
163 {
164 	mixin(CheckVLine);
165 	foreach (y; y1..y2) // TODO: optimize
166 		v[x, y] = c;
167 }
168 
169 void line(bool CHECKED=true, V, COLOR)(auto ref V v, int x1, int y1, int x2, int y2, COLOR c)
170 	if (isWritableView!V && is(COLOR : ViewColor!V))
171 {
172 	mixin FixMath;
173 	import std.algorithm.mutation : swap;
174 
175 	enum DrawLine = q{
176 		// Axis-independent part. Mixin context:
177 		// a0 .. a1  - longer side
178 		// b0 .. b1  - shorter side
179 		// DrawPixel - mixin to draw a pixel at coordinates (a, b)
180 
181 		if (a0 == a1)
182 			return;
183 
184 		if (a0 > a1)
185 		{
186 			 swap(a0, a1);
187 			 swap(b0, b1);
188 		}
189 
190 		// Use fixed-point for b position and offset per 1 pixel along "a" axis
191 		assert(b0 < (1L<<coordinateBits) && b1 < (1L<<coordinateBits));
192 		SignedBitsType!(coordinateBits*2) bPos = b0 << coordinateBits;
193 		SignedBitsType!(coordinateBits*2) bOff = ((b1-b0) << coordinateBits) / (a1-a0);
194 
195 		foreach (a; a0..a1+1)
196 		{
197 			int b = (bPos += bOff) >> coordinateBits;
198 			mixin(DrawPixel);
199 		}
200 	};
201 
202 	import std.math : abs;
203 
204 	if (abs(x2-x1) > abs(y2-y1))
205 	{
206 		alias x1 a0;
207 		alias x2 a1;
208 		alias y1 b0;
209 		alias y2 b1;
210 		enum DrawPixel = q{ v.putPixel!CHECKED(a, b, c); };
211 		mixin(DrawLine);
212 	}
213 	else
214 	{
215 		alias y1 a0;
216 		alias y2 a1;
217 		alias x1 b0;
218 		alias x2 b1;
219 		enum DrawPixel = q{ v.putPixel!CHECKED(b, a, c); };
220 		mixin(DrawLine);
221 	}
222 }
223 
224 /// Draws a rectangle with a solid line.
225 /// The coordinates represent bounds (open on the right) for the outside of the rectangle.
226 void rect(bool CHECKED=true, V, COLOR)(auto ref V v, int x1, int y1, int x2, int y2, COLOR c)
227 	if (isWritableView!V && is(COLOR : ViewColor!V))
228 {
229 	sort2(x1, x2);
230 	sort2(y1, y2);
231 	v.hline!CHECKED(x1, x2, y1  , c);
232 	v.hline!CHECKED(x1, x2, y2-1, c);
233 	v.vline!CHECKED(x1  , y1, y2, c);
234 	v.vline!CHECKED(x2-1, y1, y2, c);
235 }
236 
237 void fillRect(bool CHECKED=true, V, COLOR)(auto ref V v, int x1, int y1, int x2, int y2, COLOR b) // [)
238 	if (isWritableView!V && is(COLOR : ViewColor!V))
239 {
240 	sort2(x1, x2);
241 	sort2(y1, y2);
242 	static if (CHECKED)
243 	{
244 		if (x1 >= v.w || y1 >= v.h || x2 <= 0 || y2 <= 0 || x1==x2 || y1==y2) return;
245 		if (x1 <    0) x1 =   0;
246 		if (y1 <    0) y1 =   0;
247 		if (x2 >= v.w) x2 = v.w;
248 		if (y2 >= v.h) y2 = v.h;
249 	}
250 	foreach (y; y1..y2)
251 		v.hline!false(x1, x2, y, b);
252 }
253 
254 void fillRect(bool CHECKED=true, V, COLOR)(auto ref V v, int x1, int y1, int x2, int y2, COLOR c, COLOR b) // [)
255 	if (isWritableView!V && is(COLOR : ViewColor!V))
256 {
257 	v.rect!CHECKED(x1, y1, x2, y2, c);
258 	if (x2-x1>2 && y2-y1>2)
259 		v.fillRect!CHECKED(x1+1, y1+1, x2-1, y2-1, b);
260 }
261 
262 /// Unchecked! Make sure area is bounded.
263 void uncheckedFloodFill(V, COLOR)(auto ref V v, int x, int y, COLOR c)
264 	if (isDirectView!V && is(COLOR : ViewColor!V))
265 {
266 	v.floodFillPtr(&v[x, y], c, v[x, y]);
267 }
268 
269 private void floodFillPtr(V, COLOR)(auto ref V v, COLOR* pp, COLOR c, COLOR f)
270 	if (isDirectView!V && is(COLOR : ViewColor!V))
271 {
272 	COLOR* p0 = pp; while (*p0==f) p0--; p0++;
273 	COLOR* p1 = pp; while (*p1==f) p1++; p1--;
274 	auto stride = v.scanline(1).ptr-v.scanline(0).ptr;
275 	for (auto p=p0; p<=p1; p++)
276 		*p = c;
277 	p0 -= stride; p1 -= stride;
278 	for (auto p=p0; p<=p1; p++)
279 		if (*p == f)
280 			v.floodFillPtr(p, c, f);
281 	p0 += stride*2; p1 += stride*2;
282 	for (auto p=p0; p<=p1; p++)
283 		if (*p == f)
284 			v.floodFillPtr(p, c, f);
285 }
286 
287 void fillCircle(V, COLOR)(auto ref V v, int x, int y, int r, COLOR c)
288 	if (isWritableView!V && is(COLOR : ViewColor!V))
289 {
290 	import std.algorithm.comparison : min;
291 
292 	int x0 = x>r?x-r:0;
293 	int y0 = y>r?y-r:0;
294 	int x1 = min(x+r, v.w-1);
295 	int y1 = min(y+r, v.h-1);
296 	int rs = sqr(r);
297 	// TODO: optimize
298 	foreach (py; y0..y1+1)
299 		foreach (px; x0..x1+1)
300 			if (sqr(x-px) + sqr(y-py) < rs)
301 				v[px, py] = c;
302 }
303 
304 void fillSector(V, COLOR)(auto ref V v, int x, int y, int r0, int r1, real a0, real a1, COLOR c)
305 	if (isWritableView!V && is(COLOR : ViewColor!V))
306 {
307 	import std.algorithm.comparison : min;
308 	import std.math : atan2;
309 
310 	int x0 = x>r1?x-r1:0;
311 	int y0 = y>r1?y-r1:0;
312 	int x1 = min(x+r1, v.w-1);
313 	int y1 = min(y+r1, v.h-1);
314 	int r0s = sqr(r0);
315 	int r1s = sqr(r1);
316 	if (a0 > a1)
317 		a1 += TAU;
318 	foreach (py; y0..y1+1)
319 		foreach (px; x0..x1+1)
320 		{
321 			int dx = px-x;
322 			int dy = py-y;
323 			int rs = sqr(dx) + sqr(dy);
324 			if (r0s <= rs && rs < r1s)
325 			{
326 				real a = atan2(cast(real)dy, cast(real)dx);
327 				if ((a0 <= a     && a     <= a1) ||
328 				    (a0 <= a+TAU && a+TAU <= a1))
329 					v[px, py] = c;
330 			}
331 		}
332 }
333 
334 struct Coord { int x, y; string toString() { import std.string; return format("%s", [this.tupleof]); } }
335 
336 void fillPoly(V, COLOR)(auto ref V v, Coord[] coords, COLOR f)
337 	if (isWritableView!V && is(COLOR : ViewColor!V))
338 {
339 	import std.algorithm.comparison : min, max;
340 
341 	int minY, maxY;
342 	minY = maxY = coords[0].y;
343 	foreach (c; coords[1..$])
344 		minY = min(minY, c.y),
345 		maxY = max(maxY, c.y);
346 
347 	foreach (y; minY..maxY+1)
348 	{
349 		int[] intersections;
350 		for (uint i=0; i<coords.length; i++)
351 		{
352 			auto c0=coords[i], c1=coords[i==$-1?0:i+1];
353 			if (y==c0.y)
354 			{
355 				assert(y == coords[i%$].y);
356 				int pi = i-1; int py;
357 				while ((py=coords[(pi+$)%$].y)==y)
358 					pi--;
359 				int ni = i+1; int ny;
360 				while ((ny=coords[ni%$].y)==y)
361 					ni++;
362 				if (ni > coords.length)
363 					continue;
364 				if ((py>y) == (y>ny))
365 					intersections ~= coords[i%$].x;
366 				i = ni-1;
367 			}
368 			else
369 			if (c0.y<y && y<c1.y)
370 				intersections ~= itpl(c0.x, c1.x, y, c0.y, c1.y);
371 			else
372 			if (c1.y<y && y<c0.y)
373 				intersections ~= itpl(c1.x, c0.x, y, c1.y, c0.y);
374 		}
375 
376 		assert(intersections.length % 2==0);
377 		intersections.sort();
378 		for (uint i=0; i<intersections.length; i+=2)
379 			v.hline!true(intersections[i], intersections[i+1], y, f);
380 	}
381 }
382 
383 // No caps
384 void thickLine(V, COLOR)(auto ref V v, int x1, int y1, int x2, int y2, int r, COLOR c)
385 	if (isWritableView!V && is(COLOR : ViewColor!V))
386 {
387 	int dx = x2-x1;
388 	int dy = y2-y1;
389 	int d  = cast(int)sqrt(cast(float)(sqr(dx)+sqr(dy)));
390 	if (d==0) return;
391 
392 	int nx = dx*r/d;
393 	int ny = dy*r/d;
394 
395 	fillPoly([
396 		Coord(x1-ny, y1+nx),
397 		Coord(x1+ny, y1-nx),
398 		Coord(x2+ny, y2-nx),
399 		Coord(x2-ny, y2+nx),
400 	], c);
401 }
402 
403 // No caps
404 void thickLinePoly(V, COLOR)(auto ref V v, Coord[] coords, int r, COLOR c)
405 	if (isWritableView!V && is(COLOR : ViewColor!V))
406 {
407 	foreach (i; 0..coords.length)
408 		thickLine(coords[i].tupleof, coords[(i+1)%$].tupleof, r, c);
409 }
410 
411 // ************************************************************************************************************************************
412 
413 mixin template FixMath(ubyte coordinateBitsParam = 16)
414 {
415 	import ae.utils.meta : SignedBitsType, UnsignedBitsType;
416 
417 	enum coordinateBits = coordinateBitsParam;
418 
419 	static assert(COLOR.homogenous, "Asymmetric color types not supported, fix me!");
420 	/// Fixed-point type, big enough to hold a coordinate, with fractionary precision corresponding to channel precision.
421 	alias fix  = SignedBitsType!(COLOR.channelBits   + coordinateBits);
422 	/// Type to hold temporary values for multiplication and division
423 	alias fix2 = SignedBitsType!(COLOR.channelBits*2 + coordinateBits);
424 
425 	static assert(COLOR.channelBits < 32, "Shift operators are broken for shifts over 32 bits, fix me!");
426 	fix tofix(T:int  )(T x) { return cast(fix) (x<<COLOR.channelBits); }
427 	fix tofix(T:float)(T x) { return cast(fix) (x*(1<<COLOR.channelBits)); }
428 	T fixto(T:int)(fix x) { return cast(T)(x>>COLOR.channelBits); }
429 
430 	fix fixsqr(fix x)        { return cast(fix)((cast(fix2)x*x) >> COLOR.channelBits); }
431 	fix fixmul(fix x, fix y) { return cast(fix)((cast(fix2)x*y) >> COLOR.channelBits); }
432 	fix fixdiv(fix x, fix y) { return cast(fix)((cast(fix2)x << COLOR.channelBits)/y); }
433 
434 	static assert(COLOR.ChannelType.sizeof*8 == COLOR.channelBits, "COLORs with ChannelType not corresponding to native type not currently supported, fix me!");
435 	/// Type only large enough to hold a fractionary part of a "fix" (i.e. color channel precision). Used for alpha values, etc.
436 	alias COLOR.ChannelType frac;
437 	/// Type to hold temporary values for multiplication and division
438 	alias UnsignedBitsType!(COLOR.channelBits*2) frac2;
439 
440 	frac tofrac(T:float)(T x) { return cast(frac) (x*(1<<COLOR.channelBits)); }
441 	frac fixfpart(fix x) { return cast(frac)x; }
442 	frac fracsqr(frac x        ) { return cast(frac)((cast(frac2)x*x) >> COLOR.channelBits); }
443 	frac fracmul(frac x, frac y) { return cast(frac)((cast(frac2)x*y) >> COLOR.channelBits); }
444 
445 	frac tofracBounded(T:float)(T x) { return cast(frac) bound(tofix(x), 0, frac.max); }
446 }
447 
448 // ************************************************************************************************************************************
449 
450 void whiteNoise(V)(V v)
451 	if (isWritableView!V)
452 {
453 	import std.random;
454 	alias COLOR = ViewColor!V;
455 
456 	for (int y=0;y<v.h/2;y++)
457 		for (int x=0;x<v.w/2;x++)
458 			v[x*2, y*2] = COLOR.monochrome(uniform!(COLOR.ChannelType)());
459 
460 	// interpolate
461 	enum AVERAGE = q{(a+b)/2};
462 
463 	for (int y=0;y<v.h/2;y++)
464 		for (int x=0;x<v.w/2-1;x++)
465 			v[x*2+1, y*2  ] = COLOR.op!AVERAGE(v[x*2  , y*2], v[x*2+2, y*2  ]);
466 	for (int y=0;y<v.h/2-1;y++)
467 		for (int x=0;x<v.w/2;x++)
468 			v[x*2  , y*2+1] = COLOR.op!AVERAGE(v[x*2  , y*2], v[x*2  , y*2+2]);
469 	for (int y=0;y<v.h/2-1;y++)
470 		for (int x=0;x<v.w/2-1;x++)
471 			v[x*2+1, y*2+1] = COLOR.op!AVERAGE(v[x*2+1, y*2], v[x*2+2, y*2+2]);
472 }
473 
474 private template softRoundShape(bool RING)
475 {
476 	void softRoundShape(T, V, COLOR)(auto ref V v, T x, T y, T r0, T r1, T r2, COLOR color)
477 		if (isWritableView!V && isNumeric!T && is(COLOR : ViewColor!V))
478 	{
479 		mixin FixMath;
480 
481 		assert(r0 <= r1);
482 		assert(r1 <= r2);
483 		assert(r2 < 256); // precision constraint - see SqrType
484 		//int ix = cast(int)x;
485 		//int iy = cast(int)y;
486 		//int ir1 = cast(int)sqr(r1-1);
487 		//int ir2 = cast(int)sqr(r2+1);
488 		int x1 = cast(int)(x-r2-1); if (x1<0) x1=0;
489 		int y1 = cast(int)(y-r2-1); if (y1<0) y1=0;
490 		int x2 = cast(int)(x+r2+1); if (x2>v.w) x2 = v.w;
491 		int y2 = cast(int)(y+r2+1); if (y2>v.h) y2 = v.h;
492 
493 		static if (RING)
494 		auto r0s = r0*r0;
495 		auto r1s = r1*r1;
496 		auto r2s = r2*r2;
497 		//float rds = r2s - r1s;
498 
499 		fix fx = tofix(x);
500 		fix fy = tofix(y);
501 
502 		static if (RING)
503 		fix fr0s = tofix(r0s);
504 		fix fr1s = tofix(r1s);
505 		fix fr2s = tofix(r2s);
506 
507 		static if (RING)
508 		fix fr10 = fr1s - fr0s;
509 		fix fr21 = fr2s - fr1s;
510 
511 		for (int cy=y1;cy<y2;cy++)
512 		{
513 			auto row = v.scanline(cy);
514 			for (int cx=x1;cx<x2;cx++)
515 			{
516 				alias SignedBitsType!(2*(8 + COLOR.channelBits)) SqrType; // fit the square of radius expressed as fixed-point
517 				fix frs = cast(fix)((sqr(cast(SqrType)fx-tofix(cx)) + sqr(cast(SqrType)fy-tofix(cy))) >> COLOR.channelBits); // shift-right only once instead of once-per-sqr
518 
519 				//static frac alphafunc(frac x) { return fracsqr(x); }
520 				static frac alphafunc(frac x) { return x; }
521 
522 				static if (RING)
523 				{
524 					if (frs<fr0s)
525 						{}
526 					else
527 					if (frs<fr2s)
528 					{
529 						frac alpha;
530 						if (frs<fr1s)
531 							alpha = alphafunc(cast(frac)fixdiv(frs-fr0s, fr10));
532 						else
533 							alpha = alphafunc(cast(frac)fixdiv(frs-fr1s, fr21)).flipBits;
534 						row[cx] = COLOR.op!q{.blend(a, b, c)}(color, row[cx], alpha);
535 					}
536 				}
537 				else
538 				{
539 					if (frs<fr1s)
540 						row[cx] = color;
541 					else
542 					if (frs<fr2s)
543 					{
544 						frac alpha = alphafunc(cast(frac)fixdiv(frs-fr1s, fr21)).flipBits;
545 						row[cx] = COLOR.op!q{.blend(a, b, c)}(color, row[cx], alpha);
546 					}
547 				}
548 			}
549 		}
550 	}
551 }
552 
553 void softRing(T, V, COLOR)(auto ref V v, T x, T y, T r0, T r1, T r2, COLOR color)
554 	if (isWritableView!V && isNumeric!T && is(COLOR : ViewColor!V))
555 {
556 	v.softRoundShape!true(x, y, r0, r1, r2, color);
557 }
558 
559 void softCircle(T, V, COLOR)(auto ref V v, T x, T y, T r1, T r2, COLOR color)
560 	if (isWritableView!V && isNumeric!T && is(COLOR : ViewColor!V))
561 {
562 	v.softRoundShape!false(x, y, cast(T)0, r1, r2, color);
563 }
564 
565 template aaPutPixel(bool CHECKED=true, bool USE_ALPHA=true)
566 {
567 	void aaPutPixel(F:float, V, COLOR, frac)(auto ref V v, F x, F y, COLOR color, frac alpha)
568 		if (isWritableView!V && is(COLOR : ViewColor!V))
569 	{
570 		mixin FixMath;
571 
572 		void plot(bool CHECKED2)(int x, int y, frac f)
573 		{
574 			static if (CHECKED2)
575 				if (x<0 || x>=v.w || y<0 || y>=v.h)
576 					return;
577 
578 			COLOR* p = v.pixelPtr(x, y);
579 			static if (USE_ALPHA) f = fracmul(f, cast(frac)alpha);
580 			*p = COLOR.op!q{.blend(a, b, c)}(color, *p, f);
581 		}
582 
583 		fix fx = tofix(x);
584 		fix fy = tofix(y);
585 		int ix = fixto!int(fx);
586 		int iy = fixto!int(fy);
587 		static if (CHECKED)
588 			if (ix>=0 && iy>=0 && ix+1<v.w && iy+1<v.h)
589 			{
590 				plot!false(ix  , iy  , fracmul(fixfpart(fx).flipBits, fixfpart(fy).flipBits));
591 				plot!false(ix  , iy+1, fracmul(fixfpart(fx).flipBits, fixfpart(fy)         ));
592 				plot!false(ix+1, iy  , fracmul(fixfpart(fx)         , fixfpart(fy).flipBits));
593 				plot!false(ix+1, iy+1, fracmul(fixfpart(fx)         , fixfpart(fy)         ));
594 				return;
595 			}
596 		plot!CHECKED(ix  , iy  , fracmul(fixfpart(fx).flipBits, fixfpart(fy).flipBits));
597 		plot!CHECKED(ix  , iy+1, fracmul(fixfpart(fx).flipBits, fixfpart(fy)         ));
598 		plot!CHECKED(ix+1, iy  , fracmul(fixfpart(fx)         , fixfpart(fy).flipBits));
599 		plot!CHECKED(ix+1, iy+1, fracmul(fixfpart(fx)         , fixfpart(fy)         ));
600 	}
601 }
602 
603 void aaPutPixel(bool CHECKED=true, F:float, V, COLOR)(auto ref V v, F x, F y, COLOR color)
604 	if (isWritableView!V && is(COLOR : ViewColor!V))
605 {
606 	//aaPutPixel!(false, F)(x, y, color, 0); // doesn't work, wtf
607 	alias aaPutPixel!(CHECKED, false) f;
608 	f(v, x, y, color, 0);
609 }
610 
611 void hline(bool CHECKED=true, V, COLOR, frac)(auto ref V v, int x1, int x2, int y, COLOR color, frac alpha)
612 	if (isWritableView!V && is(COLOR : ViewColor!V))
613 {
614 	mixin(CheckHLine);
615 
616 	if (alpha==0)
617 		return;
618 	else
619 	if (alpha==frac.max)
620 		v.scanline(y)[x1..x2] = color;
621 	else
622 		foreach (ref p; v.scanline(y)[x1..x2])
623 			p = COLOR.op!q{.blend(a, b, c)}(color, p, alpha);
624 }
625 
626 void vline(bool CHECKED=true, V, COLOR, frac)(auto ref V v, int x, int y1, int y2, COLOR color, frac alpha)
627 	if (isWritableView!V && is(COLOR : ViewColor!V))
628 {
629 	mixin(CheckVLine);
630 
631 	if (alpha==0)
632 		return;
633 	else
634 	if (alpha==frac.max)
635 		foreach (y; y1..y2)
636 			v[x, y] = color;
637 	else
638 		foreach (y; y1..y2)
639 		{
640 			auto p = v.pixelPtr(x, y);
641 			*p = COLOR.op!q{.blend(a, b, c)}(color, *p, alpha);
642 		}
643 }
644 
645 void aaFillRect(bool CHECKED=true, F:float, V, COLOR)(auto ref V v, F x1, F y1, F x2, F y2, COLOR color)
646 	if (isWritableView!V && is(COLOR : ViewColor!V))
647 {
648 	mixin FixMath;
649 
650 	sort2(x1, x2);
651 	sort2(y1, y2);
652 	fix x1f = tofix(x1); int x1i = fixto!int(x1f);
653 	fix y1f = tofix(y1); int y1i = fixto!int(y1f);
654 	fix x2f = tofix(x2); int x2i = fixto!int(x2f);
655 	fix y2f = tofix(y2); int y2i = fixto!int(y2f);
656 
657 	v.vline!CHECKED(x1i, y1i+1, y2i, color, fixfpart(x1f).flipBits);
658 	v.vline!CHECKED(x2i, y1i+1, y2i, color, fixfpart(x2f)         );
659 	v.hline!CHECKED(x1i+1, x2i, y1i, color, fixfpart(y1f).flipBits);
660 	v.hline!CHECKED(x1i+1, x2i, y2i, color, fixfpart(y2f)         );
661 	v.aaPutPixel!CHECKED(x1i, y1i, color, fracmul(fixfpart(x1f).flipBits, fixfpart(y1f).flipBits));
662 	v.aaPutPixel!CHECKED(x1i, y2i, color, fracmul(fixfpart(x1f).flipBits, fixfpart(y2f)         ));
663 	v.aaPutPixel!CHECKED(x2i, y1i, color, fracmul(fixfpart(x2f)         , fixfpart(y1f).flipBits));
664 	v.aaPutPixel!CHECKED(x2i, y2i, color, fracmul(fixfpart(x2f)         , fixfpart(y2f)         ));
665 
666 	v.fillRect!CHECKED(x1i+1, y1i+1, x2i, y2i, color);
667 }
668 
669 void aaLine(bool CHECKED=true, V, COLOR)(auto ref V v, float x1, float y1, float x2, float y2, COLOR color)
670 	if (isWritableView!V && is(COLOR : ViewColor!V))
671 {
672 	import std.math : abs;
673 
674 	// Simplistic straight-forward implementation. TODO: optimize
675 	if (abs(x1-x2) > abs(y1-y2))
676 		for (auto x=x1; sign(x1-x2)!=sign(x2-x); x += sign(x2-x1))
677 			v.aaPutPixel!CHECKED(x, itpl(y1, y2, x, x1, x2), color);
678 	else
679 		for (auto y=y1; sign(y1-y2)!=sign(y2-y); y += sign(y2-y1))
680 			v.aaPutPixel!CHECKED(itpl(x1, x2, y, y1, y2), y, color);
681 }
682 
683 void aaLine(bool CHECKED=true, V, COLOR, frac)(auto ref V v, float x1, float y1, float x2, float y2, COLOR color, frac alpha)
684 	if (isWritableView!V && is(COLOR : ViewColor!V))
685 {
686 	import std.math : abs;
687 
688 	// ditto
689 	if (abs(x1-x2) > abs(y1-y2))
690 		for (auto x=x1; sign(x1-x2)!=sign(x2-x); x += sign(x2-x1))
691 			v.aaPutPixel!CHECKED(x, itpl(y1, y2, x, x1, x2), color, alpha);
692 	else
693 		for (auto y=y1; sign(y1-y2)!=sign(y2-y); y += sign(y2-y1))
694 			v.aaPutPixel!CHECKED(itpl(x1, x2, y, y1, y2), y, color, alpha);
695 }
696 
697 unittest
698 {
699 	// Test instantiation
700 	import ae.utils.graphics.color;
701 	auto i = Image!RGB(100, 100);
702 	auto c = RGB(1, 2, 3);
703 	i.whiteNoise();
704 	i.aaLine(10, 10, 20, 20, c);
705 	i.aaLine(10f, 10f, 20f, 20f, c, 100);
706 	i.rect(10, 10, 20, 20, c);
707 	i.fillRect(10, 10, 20, 20, c);
708 	i.aaFillRect(10, 10, 20, 20, c);
709 	i.vline(10, 10, 20, c);
710 	i.vline(10, 10, 20, c);
711 	i.line(10, 10, 20, 20, c);
712 	i.fillCircle(10, 10, 10, c);
713 	i.fillSector(10, 10, 10, 10, 0.0, TAU, c);
714 	i.softRing(50, 50, 10, 15, 20, c);
715 	i.softCircle(50, 50, 10, 15, c);
716 	i.fillPoly([Coord(10, 10), Coord(10, 20), Coord(20, 20)], c);
717 	i.uncheckedFloodFill(15, 15, RGB(4, 5, 6));
718 }
719 
720 // ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
721 
722 // Outdated code from before the overhaul.
723 // TODO: fix and update
724 
725 version(none):
726 
727 /// Draws an image. Returns this.
728 typeof(this) draw(bool CHECKED=true, SRCCANVAS)(int x, int y, SRCCANVAS v)
729 	if (IsCanvas!SRCCANVAS && is(COLOR == SRCCANVAS.COLOR))
730 {
731 	static if (CHECKED)
732 	{
733 		if (v.w == 0 || v.h == 0 ||
734 			x+v.w <= 0 || y+v.h <= 0 || x >= w || y >= h)
735 			return this;
736 
737 		auto r = v.window(0, 0, v.w, v.h);
738 		if (x < 0)
739 			r = r.window(-x, 0, r.w, r.h),
740 			x = 0;
741 		if (y < 0)
742 			r = r.window(0, -y, r.w, r.h),
743 			y = 0;
744 		if (x+r.w > w)
745 			r = r.window(0, 0, w-x, r.h);
746 		if (y+r.h > h)
747 			r = r.window(0, 0, r.w, h-y);
748 
749 		draw!false(x, y, r);
750 	}
751 	else
752 	{
753 		assert(v.w > 0 && v.h > 0);
754 		assert(x >= 0 && x+v.w <= w && y >= 0 && y+v.h <= h);
755 
756 		// TODO: alpha blending
757 		size_t dstStart = y*stride+x, srcStart = 0;
758 		foreach (j; 0..v.h)
759 			pixels[dstStart..dstStart+v.w] = v.pixels[srcStart..srcStart+v.w],
760 			dstStart += stride,
761 			srcStart += v.stride;
762 	}
763 
764 	return this;
765 }
766 
767 void subpixelDownscale()()
768 	if (structFields!COLOR == ["r","g","b"] || structFields!COLOR == ["b","g","r"])
769 {
770 	Image!COLOR i;
771 	i.size(HRX + hr.w*3 + HRX, hr.h);
772 	i.draw(0, 0, hr.window(0, 0, HRX, hr.h));
773 	i.window(HRX, 0, HRX+hr.w*3, hr.h).upscaleDraw!(3, 1)(hr);
774 	i.draw(HRX + hr.w*3, 0, hr.window(hr.w-HRX, 0, hr.w, hr.h));
775 	alias Color!(COLOR.BaseType, "g") BASE;
776 	Image!BASE[3] channels;
777 	Image!BASE scratch;
778 	scratch.size(hr.w*3, hr.h);
779 
780 	foreach (int cx, char c; ValueTuple!('r', 'g', 'b'))
781 	{
782 		auto w = i.window(cx*HRX, 0, cx*HRX+hr.w*3, hr.h);
783 		scratch.transformDraw!(`COLOR(c.`~c~`)`)(0, 0, w);
784 		channels[cx].size(lr.w, lr.h);
785 		channels[cx].downscaleDraw!(3*HRX, HRY)(scratch);
786 	}
787 
788 	foreach (y; 0..lr.h)
789 		foreach (x; 0..lr.w)
790 		{
791 			COLOR c;
792 			c.r = channels[0][x, y].g;
793 			c.g = channels[1][x, y].g;
794 			c.b = channels[2][x, y].g;
795 			lr[x, y] = c;
796 		}
797 }