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