1 /+
2 	== pixmappaint ==
3 	Copyright Elias Batek (0xEAB) 2024.
4 	Distributed under the Boost Software License, Version 1.0.
5 +/
6 /++
7 	Pixmap image manipulation
8 
9 	$(WARNING
10 		$(B Early Technology Preview.)
11 	)
12 
13 	$(PITFALL
14 		This module is $(B work in progress).
15 		API is subject to changes until further notice.
16 	)
17  +/
18 module arsd.pixmappaint;
19 
20 import arsd.color;
21 import arsd.core;
22 
23 private float hackyRound(float f) {
24 	import std.math : round;
25 	return round(f);
26 }
27 
28 float round(float f) pure @nogc nothrow @trusted {
29 	return (cast(float function(float) pure @nogc nothrow) &hackyRound)(f);
30 }
31 
32 /*
33 	## TODO:
34 
35 	- Refactoring the template-mess of blendPixel() & co.
36 	- Scaling
37 	- Cropping
38 	- Rotating
39 	- Skewing
40 	- HSL
41 	- Advanced blend modes (maybe)
42  */
43 
44 ///
45 alias Color = arsd.color.Color;
46 
47 ///
48 alias ColorF = arsd.color.ColorF;
49 
50 ///
51 alias Pixel = Color;
52 
53 ///
54 alias Point = arsd.color.Point;
55 
56 ///
57 alias Rectangle = arsd.color.Rectangle;
58 
59 ///
60 alias Size = arsd.color.Size;
61 
62 // verify assumption(s)
63 static assert(Pixel.sizeof == uint.sizeof);
64 
65 @safe pure nothrow @nogc {
66 	///
67 	Pixel rgba(ubyte r, ubyte g, ubyte b, ubyte a = 0xFF) {
68 		return Pixel(r, g, b, a);
69 	}
70 
71 	///
72 	Pixel rgba(ubyte r, ubyte g, ubyte b, float aPct)
73 	in (aPct >= 0 && aPct <= 1) {
74 		return Pixel(r, g, b, castTo!ubyte(aPct * 255));
75 	}
76 
77 	///
78 	Pixel rgb(ubyte r, ubyte g, ubyte b) {
79 		return rgba(r, g, b, 0xFF);
80 	}
81 }
82 
83 /++
84 	Pixel data container
85  +/
86 struct Pixmap {
87 
88 	/// Pixel data
89 	Pixel[] data;
90 
91 	/// Pixel per row
92 	int width;
93 
94 @safe pure nothrow:
95 
96 	///
97 	this(Size size) {
98 		this.size = size;
99 	}
100 
101 	///
102 	this(int width, int height)
103 	in (width > 0)
104 	in (height > 0) {
105 		this(Size(width, height));
106 	}
107 
108 	///
109 	this(Pixel[] data, int width) @nogc
110 	in (data.length % width == 0) {
111 		this.data = data;
112 		this.width = width;
113 	}
114 
115 	/++
116 		Creates a $(I deep clone) of the Pixmap
117 	 +/
118 	Pixmap clone() const {
119 		auto c = Pixmap();
120 		c.width = this.width;
121 		c.data = this.data.dup;
122 		return c;
123 	}
124 
125 	// undocumented: really shouldn’t be used.
126 	// carries the risks of `length` and `width` getting out of sync accidentally.
127 	deprecated("Use `size` instead.")
128 	void length(int value) {
129 		data.length = value;
130 	}
131 
132 	/++
133 		Changes the size of the buffer
134 
135 		Reallocates the underlying pixel array.
136 	 +/
137 	void size(Size value) {
138 		data.length = value.area;
139 		width = value.width;
140 	}
141 
142 	/// ditto
143 	void size(int totalPixels, int width)
144 	in (totalPixels % width == 0) {
145 		data.length = totalPixels;
146 		this.width = width;
147 	}
148 
149 	static {
150 		/++
151 			Creates a Pixmap wrapping the pixel data from the provided `TrueColorImage`.
152 
153 			Interoperability function: `arsd.color`
154 		 +/
155 		Pixmap fromTrueColorImage(TrueColorImage source) @nogc {
156 			return Pixmap(source.imageData.colors, source.width);
157 		}
158 
159 		/++
160 			Creates a Pixmap wrapping the pixel data from the provided `MemoryImage`.
161 
162 			Interoperability function: `arsd.color`
163 		 +/
164 		Pixmap fromMemoryImage(MemoryImage source) {
165 			return fromTrueColorImage(source.getAsTrueColorImage());
166 		}
167 	}
168 
169 @safe pure nothrow @nogc:
170 
171 	/// Height of the buffer, i.e. the number of lines
172 	int height() inout {
173 		if (width == 0) {
174 			return 0;
175 		}
176 
177 		return castTo!int(data.length / width);
178 	}
179 
180 	/// Rectangular size of the buffer
181 	Size size() inout {
182 		return Size(width, height);
183 	}
184 
185 	/// Length of the buffer, i.e. the number of pixels
186 	int length() inout {
187 		return castTo!int(data.length);
188 	}
189 
190 	/++
191 		Number of bytes per line
192 
193 		Returns:
194 			width × Pixel.sizeof
195 	 +/
196 	int pitch() inout {
197 		return (width * int(Pixel.sizeof));
198 	}
199 
200 	/++
201 		Retrieves a linear slice of the pixmap.
202 
203 		Returns:
204 			`n` pixels starting at the top-left position `pos`.
205 	 +/
206 	inout(Pixel)[] sliceAt(Point pos, int n) inout {
207 		immutable size_t offset = linearOffset(width, pos);
208 		immutable size_t end = (offset + n);
209 		return data[offset .. end];
210 	}
211 
212 	/// Clears the buffer’s contents (by setting each pixel to the same color)
213 	void clear(Pixel value) {
214 		data[] = value;
215 	}
216 }
217 
218 ///
219 struct SpriteSheet {
220 	private {
221 		Pixmap _pixmap;
222 		Size _spriteDimensions;
223 		Size _layout; // pre-computed upon construction
224 	}
225 
226 @safe pure nothrow @nogc:
227 
228 	///
229 	public this(Pixmap pixmap, Size spriteSize) {
230 		_pixmap = pixmap;
231 		_spriteDimensions = spriteSize;
232 
233 		_layout = Size(
234 			_pixmap.width / _spriteDimensions.width,
235 			_pixmap.height / _spriteDimensions.height,
236 		);
237 	}
238 
239 	///
240 	inout(Pixmap) pixmap() inout {
241 		return _pixmap;
242 	}
243 
244 	///
245 	Size spriteSize() inout {
246 		return _spriteDimensions;
247 	}
248 
249 	///
250 	Size layout() inout {
251 		return _layout;
252 	}
253 
254 	///
255 	Point getSpriteColumn(int index) inout {
256 		immutable x = index % layout.width;
257 		immutable y = (index - x) / layout.height;
258 		return Point(x, y);
259 	}
260 
261 	///
262 	Point getSpritePixelOffset2D(int index) inout {
263 		immutable col = this.getSpriteColumn(index);
264 		return Point(
265 			col.x * _spriteDimensions.width,
266 			col.y * _spriteDimensions.height,
267 		);
268 	}
269 }
270 
271 // Silly micro-optimization
272 private struct OriginRectangle {
273 	Size size;
274 
275 @safe pure nothrow @nogc:
276 
277 	int left() const => 0;
278 	int top() const => 0;
279 	int right() const => size.width;
280 	int bottom() const => size.height;
281 
282 	bool intersect(const Rectangle b) const {
283 		// dfmt off
284 		return (
285 			(b.right    > 0          ) &&
286 			(b.left     < this.right ) &&
287 			(b.bottom   > 0          ) &&
288 			(b.top      < this.bottom)
289 		);
290 		// dfmt on
291 	}
292 }
293 
294 @safe pure nothrow @nogc:
295 
296 // misc
297 private {
298 	Point pos(Rectangle r) => r.upperLeft;
299 
300 	T max(T)(T a, T b) => (a >= b) ? a : b;
301 	T min(T)(T a, T b) => (a <= b) ? a : b;
302 }
303 
304 /++
305 	Calculates the square root
306 	of an integer number
307 	as an integer number.
308  +/
309 ubyte intSqrt(const ubyte value) @safe pure nothrow @nogc {
310 	switch (value) {
311 	default:
312 		// unreachable
313 		assert(false, "ubyte != uint8");
314 	case 0:
315 		return 0;
316 	case 1: .. case 2:
317 		return 1;
318 	case 3: .. case 6:
319 		return 2;
320 	case 7: .. case 12:
321 		return 3;
322 	case 13: .. case 20:
323 		return 4;
324 	case 21: .. case 30:
325 		return 5;
326 	case 31: .. case 42:
327 		return 6;
328 	case 43: .. case 56:
329 		return 7;
330 	case 57: .. case 72:
331 		return 8;
332 	case 73: .. case 90:
333 		return 9;
334 	case 91: .. case 110:
335 		return 10;
336 	case 111: .. case 132:
337 		return 11;
338 	case 133: .. case 156:
339 		return 12;
340 	case 157: .. case 182:
341 		return 13;
342 	case 183: .. case 210:
343 		return 14;
344 	case 211: .. case 240:
345 		return 15;
346 	case 241: .. case 255:
347 		return 16;
348 	}
349 }
350 
351 ///
352 unittest {
353 	assert(intSqrt(4) == 2);
354 	assert(intSqrt(9) == 3);
355 	assert(intSqrt(10) == 3);
356 }
357 
358 unittest {
359 	import std.math : round, sqrt;
360 
361 	foreach (n; ubyte.min .. ubyte.max + 1) {
362 		ubyte fp = sqrt(float(n)).round().castTo!ubyte;
363 		ubyte i8 = intSqrt(n.castTo!ubyte);
364 		assert(fp == i8);
365 	}
366 }
367 
368 /++
369 	Calculates the square root
370 	of the normalized value
371 	representated by the input integer number.
372 
373 	Normalization:
374 		`[0x00 .. 0xFF]` → `[0.0 .. 1.0]`
375 
376 	Returns:
377 		sqrt(value / 255f) * 255
378  +/
379 ubyte intNormalizedSqrt(const ubyte value) {
380 	switch (value) {
381 	default:
382 		// unreachable
383 		assert(false, "ubyte != uint8");
384 	case 0x00:
385 		return 0x00;
386 	case 0x01:
387 		return 0x10;
388 	case 0x02:
389 		return 0x17;
390 	case 0x03:
391 		return 0x1C;
392 	case 0x04:
393 		return 0x20;
394 	case 0x05:
395 		return 0x24;
396 	case 0x06:
397 		return 0x27;
398 	case 0x07:
399 		return 0x2A;
400 	case 0x08:
401 		return 0x2D;
402 	case 0x09:
403 		return 0x30;
404 	case 0x0A:
405 		return 0x32;
406 	case 0x0B:
407 		return 0x35;
408 	case 0x0C:
409 		return 0x37;
410 	case 0x0D:
411 		return 0x3A;
412 	case 0x0E:
413 		return 0x3C;
414 	case 0x0F:
415 		return 0x3E;
416 	case 0x10:
417 		return 0x40;
418 	case 0x11:
419 		return 0x42;
420 	case 0x12:
421 		return 0x44;
422 	case 0x13:
423 		return 0x46;
424 	case 0x14:
425 		return 0x47;
426 	case 0x15:
427 		return 0x49;
428 	case 0x16:
429 		return 0x4B;
430 	case 0x17:
431 		return 0x4D;
432 	case 0x18:
433 		return 0x4E;
434 	case 0x19:
435 		return 0x50;
436 	case 0x1A:
437 		return 0x51;
438 	case 0x1B:
439 		return 0x53;
440 	case 0x1C:
441 		return 0x54;
442 	case 0x1D:
443 		return 0x56;
444 	case 0x1E:
445 		return 0x57;
446 	case 0x1F:
447 		return 0x59;
448 	case 0x20:
449 		return 0x5A;
450 	case 0x21:
451 		return 0x5C;
452 	case 0x22:
453 		return 0x5D;
454 	case 0x23:
455 		return 0x5E;
456 	case 0x24:
457 		return 0x60;
458 	case 0x25:
459 		return 0x61;
460 	case 0x26:
461 		return 0x62;
462 	case 0x27:
463 		return 0x64;
464 	case 0x28:
465 		return 0x65;
466 	case 0x29:
467 		return 0x66;
468 	case 0x2A:
469 		return 0x67;
470 	case 0x2B:
471 		return 0x69;
472 	case 0x2C:
473 		return 0x6A;
474 	case 0x2D:
475 		return 0x6B;
476 	case 0x2E:
477 		return 0x6C;
478 	case 0x2F:
479 		return 0x6D;
480 	case 0x30:
481 		return 0x6F;
482 	case 0x31:
483 		return 0x70;
484 	case 0x32:
485 		return 0x71;
486 	case 0x33:
487 		return 0x72;
488 	case 0x34:
489 		return 0x73;
490 	case 0x35:
491 		return 0x74;
492 	case 0x36:
493 		return 0x75;
494 	case 0x37:
495 		return 0x76;
496 	case 0x38:
497 		return 0x77;
498 	case 0x39:
499 		return 0x79;
500 	case 0x3A:
501 		return 0x7A;
502 	case 0x3B:
503 		return 0x7B;
504 	case 0x3C:
505 		return 0x7C;
506 	case 0x3D:
507 		return 0x7D;
508 	case 0x3E:
509 		return 0x7E;
510 	case 0x3F:
511 		return 0x7F;
512 	case 0x40:
513 		return 0x80;
514 	case 0x41:
515 		return 0x81;
516 	case 0x42:
517 		return 0x82;
518 	case 0x43:
519 		return 0x83;
520 	case 0x44:
521 		return 0x84;
522 	case 0x45:
523 		return 0x85;
524 	case 0x46:
525 		return 0x86;
526 	case 0x47: .. case 0x48:
527 		return 0x87;
528 	case 0x49:
529 		return 0x88;
530 	case 0x4A:
531 		return 0x89;
532 	case 0x4B:
533 		return 0x8A;
534 	case 0x4C:
535 		return 0x8B;
536 	case 0x4D:
537 		return 0x8C;
538 	case 0x4E:
539 		return 0x8D;
540 	case 0x4F:
541 		return 0x8E;
542 	case 0x50:
543 		return 0x8F;
544 	case 0x51:
545 		return 0x90;
546 	case 0x52: .. case 0x53:
547 		return 0x91;
548 	case 0x54:
549 		return 0x92;
550 	case 0x55:
551 		return 0x93;
552 	case 0x56:
553 		return 0x94;
554 	case 0x57:
555 		return 0x95;
556 	case 0x58:
557 		return 0x96;
558 	case 0x59: .. case 0x5A:
559 		return 0x97;
560 	case 0x5B:
561 		return 0x98;
562 	case 0x5C:
563 		return 0x99;
564 	case 0x5D:
565 		return 0x9A;
566 	case 0x5E:
567 		return 0x9B;
568 	case 0x5F: .. case 0x60:
569 		return 0x9C;
570 	case 0x61:
571 		return 0x9D;
572 	case 0x62:
573 		return 0x9E;
574 	case 0x63:
575 		return 0x9F;
576 	case 0x64: .. case 0x65:
577 		return 0xA0;
578 	case 0x66:
579 		return 0xA1;
580 	case 0x67:
581 		return 0xA2;
582 	case 0x68:
583 		return 0xA3;
584 	case 0x69: .. case 0x6A:
585 		return 0xA4;
586 	case 0x6B:
587 		return 0xA5;
588 	case 0x6C:
589 		return 0xA6;
590 	case 0x6D: .. case 0x6E:
591 		return 0xA7;
592 	case 0x6F:
593 		return 0xA8;
594 	case 0x70:
595 		return 0xA9;
596 	case 0x71: .. case 0x72:
597 		return 0xAA;
598 	case 0x73:
599 		return 0xAB;
600 	case 0x74:
601 		return 0xAC;
602 	case 0x75: .. case 0x76:
603 		return 0xAD;
604 	case 0x77:
605 		return 0xAE;
606 	case 0x78:
607 		return 0xAF;
608 	case 0x79: .. case 0x7A:
609 		return 0xB0;
610 	case 0x7B:
611 		return 0xB1;
612 	case 0x7C:
613 		return 0xB2;
614 	case 0x7D: .. case 0x7E:
615 		return 0xB3;
616 	case 0x7F:
617 		return 0xB4;
618 	case 0x80: .. case 0x81:
619 		return 0xB5;
620 	case 0x82:
621 		return 0xB6;
622 	case 0x83: .. case 0x84:
623 		return 0xB7;
624 	case 0x85:
625 		return 0xB8;
626 	case 0x86:
627 		return 0xB9;
628 	case 0x87: .. case 0x88:
629 		return 0xBA;
630 	case 0x89:
631 		return 0xBB;
632 	case 0x8A: .. case 0x8B:
633 		return 0xBC;
634 	case 0x8C:
635 		return 0xBD;
636 	case 0x8D: .. case 0x8E:
637 		return 0xBE;
638 	case 0x8F:
639 		return 0xBF;
640 	case 0x90: .. case 0x91:
641 		return 0xC0;
642 	case 0x92:
643 		return 0xC1;
644 	case 0x93: .. case 0x94:
645 		return 0xC2;
646 	case 0x95:
647 		return 0xC3;
648 	case 0x96: .. case 0x97:
649 		return 0xC4;
650 	case 0x98:
651 		return 0xC5;
652 	case 0x99: .. case 0x9A:
653 		return 0xC6;
654 	case 0x9B: .. case 0x9C:
655 		return 0xC7;
656 	case 0x9D:
657 		return 0xC8;
658 	case 0x9E: .. case 0x9F:
659 		return 0xC9;
660 	case 0xA0:
661 		return 0xCA;
662 	case 0xA1: .. case 0xA2:
663 		return 0xCB;
664 	case 0xA3: .. case 0xA4:
665 		return 0xCC;
666 	case 0xA5:
667 		return 0xCD;
668 	case 0xA6: .. case 0xA7:
669 		return 0xCE;
670 	case 0xA8:
671 		return 0xCF;
672 	case 0xA9: .. case 0xAA:
673 		return 0xD0;
674 	case 0xAB: .. case 0xAC:
675 		return 0xD1;
676 	case 0xAD:
677 		return 0xD2;
678 	case 0xAE: .. case 0xAF:
679 		return 0xD3;
680 	case 0xB0: .. case 0xB1:
681 		return 0xD4;
682 	case 0xB2:
683 		return 0xD5;
684 	case 0xB3: .. case 0xB4:
685 		return 0xD6;
686 	case 0xB5: .. case 0xB6:
687 		return 0xD7;
688 	case 0xB7:
689 		return 0xD8;
690 	case 0xB8: .. case 0xB9:
691 		return 0xD9;
692 	case 0xBA: .. case 0xBB:
693 		return 0xDA;
694 	case 0xBC:
695 		return 0xDB;
696 	case 0xBD: .. case 0xBE:
697 		return 0xDC;
698 	case 0xBF: .. case 0xC0:
699 		return 0xDD;
700 	case 0xC1: .. case 0xC2:
701 		return 0xDE;
702 	case 0xC3:
703 		return 0xDF;
704 	case 0xC4: .. case 0xC5:
705 		return 0xE0;
706 	case 0xC6: .. case 0xC7:
707 		return 0xE1;
708 	case 0xC8: .. case 0xC9:
709 		return 0xE2;
710 	case 0xCA:
711 		return 0xE3;
712 	case 0xCB: .. case 0xCC:
713 		return 0xE4;
714 	case 0xCD: .. case 0xCE:
715 		return 0xE5;
716 	case 0xCF: .. case 0xD0:
717 		return 0xE6;
718 	case 0xD1: .. case 0xD2:
719 		return 0xE7;
720 	case 0xD3:
721 		return 0xE8;
722 	case 0xD4: .. case 0xD5:
723 		return 0xE9;
724 	case 0xD6: .. case 0xD7:
725 		return 0xEA;
726 	case 0xD8: .. case 0xD9:
727 		return 0xEB;
728 	case 0xDA: .. case 0xDB:
729 		return 0xEC;
730 	case 0xDC: .. case 0xDD:
731 		return 0xED;
732 	case 0xDE: .. case 0xDF:
733 		return 0xEE;
734 	case 0xE0:
735 		return 0xEF;
736 	case 0xE1: .. case 0xE2:
737 		return 0xF0;
738 	case 0xE3: .. case 0xE4:
739 		return 0xF1;
740 	case 0xE5: .. case 0xE6:
741 		return 0xF2;
742 	case 0xE7: .. case 0xE8:
743 		return 0xF3;
744 	case 0xE9: .. case 0xEA:
745 		return 0xF4;
746 	case 0xEB: .. case 0xEC:
747 		return 0xF5;
748 	case 0xED: .. case 0xEE:
749 		return 0xF6;
750 	case 0xEF: .. case 0xF0:
751 		return 0xF7;
752 	case 0xF1: .. case 0xF2:
753 		return 0xF8;
754 	case 0xF3: .. case 0xF4:
755 		return 0xF9;
756 	case 0xF5: .. case 0xF6:
757 		return 0xFA;
758 	case 0xF7: .. case 0xF8:
759 		return 0xFB;
760 	case 0xF9: .. case 0xFA:
761 		return 0xFC;
762 	case 0xFB: .. case 0xFC:
763 		return 0xFD;
764 	case 0xFD: .. case 0xFE:
765 		return 0xFE;
766 	case 0xFF:
767 		return 0xFF;
768 	}
769 }
770 
771 unittest {
772 	import std.math : round, sqrt;
773 
774 	foreach (n; ubyte.min .. ubyte.max + 1) {
775 		ubyte fp = (sqrt(n / 255.0f) * 255).round().castTo!ubyte;
776 		ubyte i8 = intNormalizedSqrt(n.castTo!ubyte);
777 		assert(fp == i8);
778 	}
779 }
780 
781 /++
782 	Limits a value to a maximum of 0xFF (= 255).
783  +/
784 ubyte clamp255(Tint)(const Tint value) {
785 	pragma(inline, true);
786 	return (value < 0xFF) ? value.castTo!ubyte : 0xFF;
787 }
788 
789 /++
790 	Fast 8-bit “percentage” function
791 
792 	This function optimizes its runtime performance by substituting
793 	the division by 255 with an approximation using bitshifts.
794 
795 	Nonetheless, its result are as accurate as a floating point
796 	division with 64-bit precision.
797 
798 	Params:
799 		nPercentage = percentage as the number of 255ths (“two hundred fifty-fifths”)
800 		value = base value (“total”)
801 
802 	Returns:
803 		`round(value * nPercentage / 255.0)`
804  +/
805 ubyte n255thsOf(const ubyte nPercentage, const ubyte value) {
806 	immutable factor = (nPercentage | (nPercentage << 8));
807 	return (((value * factor) + 0x8080) >> 16);
808 }
809 
810 @safe unittest {
811 	// Accuracy verification
812 
813 	static ubyte n255thsOfFP64(const ubyte nPercentage, const ubyte value) {
814 		return (double(value) * double(nPercentage) / 255.0).round().castTo!ubyte();
815 	}
816 
817 	for (int value = ubyte.min; value <= ubyte.max; ++value) {
818 		for (int percent = ubyte.min; percent <= ubyte.max; ++percent) {
819 			immutable v = cast(ubyte) value;
820 			immutable p = cast(ubyte) percent;
821 
822 			immutable approximated = n255thsOf(p, v);
823 			immutable precise = n255thsOfFP64(p, v);
824 			assert(approximated == precise);
825 		}
826 	}
827 }
828 
829 /++
830 	Sets the opacity of a [Pixmap].
831 
832 	This lossy operation updates the alpha-channel value of each pixel.
833 	→ `alpha *= opacity`
834 
835 	See_Also:
836 		Use [opacityF] with opacity values in percent (%).
837  +/
838 void opacity(Pixmap pixmap, const ubyte opacity) {
839 	foreach (ref px; pixmap.data) {
840 		px.a = opacity.n255thsOf(px.a);
841 	}
842 }
843 
844 /++
845 	Sets the opacity of a [Pixmap].
846 
847 	This lossy operation updates the alpha-channel value of each pixel.
848 	→ `alpha *= opacity`
849 
850 	See_Also:
851 		Use [opacity] with 8-bit integer opacity values (in 255ths).
852  +/
853 void opacityF(Pixmap pixmap, const float opacity)
854 in (opacity >= 0)
855 in (opacity <= 1.0) {
856 	immutable opacity255 = round(opacity * 255).castTo!ubyte;
857 	pixmap.opacity = opacity255;
858 }
859 
860 /++
861 	Inverts a color (to its negative color).
862  +/
863 Pixel invert(const Pixel color) {
864 	return Pixel(
865 		0xFF - color.r,
866 		0xFF - color.g,
867 		0xFF - color.b,
868 		color.a,
869 	);
870 }
871 
872 /++
873 	Inverts all colors to produce a $(B negative image).
874 
875 	$(TIP
876 		Develops a positive image when applied to a negative one.
877 	)
878  +/
879 void invert(Pixmap pixmap) {
880 	foreach (ref px; pixmap.data) {
881 		px = invert(px);
882 	}
883 }
884 
885 // ==== Blending functions ====
886 
887 /++
888 	Alpha-blending accuracy level
889 
890 	$(TIP
891 		This primarily exists for performance reasons.
892 		In my tests LLVM manages to auto-vectorize the RGB-only codepath significantly better,
893 		while the codegen for the accurate RGBA path is pretty conservative.
894 
895 		This provides an optimization opportunity for use-cases
896 		that don’t require an alpha-channel on the result.
897 	)
898  +/
899 enum BlendAccuracy {
900 	/++
901 		Only RGB channels will have the correct result.
902 
903 		A(lpha) channel can contain any value.
904 
905 		Suitable for blending into non-transparent targets (e.g. framebuffer, canvas)
906 		where the resulting alpha-channel (opacity) value does not matter.
907 	 +/
908 	rgb = false,
909 
910 	/++
911 		All RGBA channels will have the correct result.
912 
913 		Suitable for blending into transparent targets (e.g. images)
914 		where the resulting alpha-channel (opacity) value matters.
915 
916 		Use this mode for image manipulation.
917 	 +/
918 	rgba = true,
919 }
920 
921 /++
922 	Blend modes
923 
924 	$(NOTE
925 		As blending operations are implemented as integer calculations,
926 		results may be slightly less precise than those from image manipulation
927 		programs using floating-point math.
928 	)
929 
930 	See_Also:
931 		<https://www.w3.org/TR/compositing/#blending>
932  +/
933 enum BlendMode {
934 	///
935 	none = 0,
936 	///
937 	replace = none,
938 	///
939 	normal = 1,
940 	///
941 	alpha = normal,
942 
943 	///
944 	multiply,
945 	///
946 	screen,
947 
948 	///
949 	overlay,
950 	///
951 	hardLight,
952 	///
953 	softLight,
954 
955 	///
956 	darken,
957 	///
958 	lighten,
959 
960 	///
961 	colorDodge,
962 	///
963 	colorBurn,
964 
965 	///
966 	difference,
967 	///
968 	exclusion,
969 	///
970 	subtract,
971 	///
972 	divide,
973 }
974 
975 ///
976 alias Blend = BlendMode;
977 
978 // undocumented
979 enum blendNormal = BlendMode.normal;
980 
981 ///
982 alias BlendFn = ubyte function(const ubyte background, const ubyte foreground) pure nothrow @nogc;
983 
984 /++
985 	Blends `source` into `target`
986 	with respect to the opacity of the source image (as stored in the alpha channel).
987 
988 	See_Also:
989 		[alphaBlendRGBA] and [alphaBlendRGB] are shorthand functions
990 		in cases where no special blending algorithm is needed.
991  +/
992 template alphaBlend(BlendFn blend = null, BlendAccuracy accuracy = BlendAccuracy.rgba) {
993 	/// ditto
994 	public void alphaBlend(scope Pixel[] target, scope const Pixel[] source) @trusted
995 	in (source.length == target.length) {
996 		foreach (immutable idx, ref pxTarget; target) {
997 			alphaBlend(pxTarget, source.ptr[idx]);
998 		}
999 	}
1000 
1001 	/// ditto
1002 	public void alphaBlend(ref Pixel pxTarget, const Pixel pxSource) @trusted {
1003 		pragma(inline, true);
1004 
1005 		static if (accuracy == BlendAccuracy.rgba) {
1006 			immutable alphaResult = clamp255(pxSource.a + n255thsOf(pxTarget.a, (0xFF - pxSource.a)));
1007 			//immutable alphaResult = clamp255(pxTarget.a + n255thsOf(pxSource.a, (0xFF - pxTarget.a)));
1008 		}
1009 
1010 		immutable alphaSource = (pxSource.a | (pxSource.a << 8));
1011 		immutable alphaTarget = (0xFFFF - alphaSource);
1012 
1013 		foreach (immutable ib, ref px; pxTarget.components) {
1014 			static if (blend !is null) {
1015 				immutable bx = blend(px, pxSource.components.ptr[ib]);
1016 			} else {
1017 				immutable bx = pxSource.components.ptr[ib];
1018 			}
1019 			immutable d = cast(ubyte)(((px * alphaTarget) + 0x8080) >> 16);
1020 			immutable s = cast(ubyte)(((bx * alphaSource) + 0x8080) >> 16);
1021 			px = cast(ubyte)(d + s);
1022 		}
1023 
1024 		static if (accuracy == BlendAccuracy.rgba) {
1025 			pxTarget.a = alphaResult;
1026 		}
1027 	}
1028 }
1029 
1030 /// ditto
1031 template alphaBlend(BlendAccuracy accuracy, BlendFn blend = null) {
1032 	alias alphaBlend = alphaBlend!(blend, accuracy);
1033 }
1034 
1035 /++
1036 	Blends `source` into `target`
1037 	with respect to the opacity of the source image (as stored in the alpha channel).
1038 
1039 	This variant is $(slower than) [alphaBlendRGB],
1040 	but calculates the correct alpha-channel value of the target.
1041 	See [BlendAccuracy] for further explanation.
1042  +/
1043 public void alphaBlendRGBA(scope Pixel[] target, scope const Pixel[] source) @safe {
1044 	return alphaBlend!(null, BlendAccuracy.rgba)(target, source);
1045 }
1046 
1047 /// ditto
1048 public void alphaBlendRGBA(ref Pixel pxTarget, const Pixel pxSource) @safe {
1049 	return alphaBlend!(null, BlendAccuracy.rgba)(pxTarget, pxSource);
1050 }
1051 
1052 /++
1053 	Blends `source` into `target`
1054 	with respect to the opacity of the source image (as stored in the alpha channel).
1055 
1056 	This variant is $(B faster than) [alphaBlendRGBA],
1057 	but leads to a wrong alpha-channel value in the target.
1058 	Useful because of the performance advantage in cases where the resulting
1059 	alpha does not matter.
1060 	See [BlendAccuracy] for further explanation.
1061  +/
1062 public void alphaBlendRGB(scope Pixel[] target, scope const Pixel[] source) @safe {
1063 	return alphaBlend!(null, BlendAccuracy.rgb)(target, source);
1064 }
1065 
1066 /// ditto
1067 public void alphaBlendRGB(ref Pixel pxTarget, const Pixel pxSource) @safe {
1068 	return alphaBlend!(null, BlendAccuracy.rgb)(pxTarget, pxSource);
1069 }
1070 
1071 /++
1072 	Blends pixel `source` into pixel `target`
1073 	using the requested $(B blending mode).
1074  +/
1075 template blendPixel(BlendMode mode, BlendAccuracy accuracy = BlendAccuracy.rgba) {
1076 
1077 	static if (mode == BlendMode.replace) {
1078 		/// ditto
1079 		void blendPixel(ref Pixel target, const Pixel source) {
1080 			target = source;
1081 		}
1082 	}
1083 
1084 	static if (mode == BlendMode.alpha) {
1085 		/// ditto
1086 		void blendPixel(ref Pixel target, const Pixel source) {
1087 			return alphaBlend!accuracy(target, source);
1088 		}
1089 	}
1090 
1091 	static if (mode == BlendMode.multiply) {
1092 		/// ditto
1093 		void blendPixel(ref Pixel target, const Pixel source) {
1094 			return alphaBlend!(accuracy,
1095 				(a, b) => n255thsOf(a, b)
1096 			)(target, source);
1097 		}
1098 	}
1099 
1100 	static if (mode == BlendMode.screen) {
1101 		/// ditto
1102 		void blendPixel()(ref Pixel target, const Pixel source) {
1103 			return alphaBlend!(accuracy,
1104 				(a, b) => castTo!ubyte(0xFF - n255thsOf((0xFF - a), (0xFF - b)))
1105 			)(target, source);
1106 		}
1107 	}
1108 
1109 	static if (mode == BlendMode.darken) {
1110 		/// ditto
1111 		void blendPixel()(ref Pixel target, const Pixel source) {
1112 			return alphaBlend!(accuracy,
1113 				(a, b) => min(a, b)
1114 			)(target, source);
1115 		}
1116 	}
1117 	static if (mode == BlendMode.lighten) {
1118 		/// ditto
1119 		void blendPixel()(ref Pixel target, const Pixel source) {
1120 			return alphaBlend!(accuracy,
1121 				(a, b) => max(a, b)
1122 			)(target, source);
1123 		}
1124 	}
1125 
1126 	static if (mode == BlendMode.overlay) {
1127 		/// ditto
1128 		void blendPixel()(ref Pixel target, const Pixel source) {
1129 			return alphaBlend!(accuracy, function(const ubyte b, const ubyte f) {
1130 				if (b < 0x80) {
1131 					return n255thsOf((2 * b).castTo!ubyte, f);
1132 				}
1133 				return castTo!ubyte(
1134 					0xFF - n255thsOf(castTo!ubyte(2 * (0xFF - b)), (0xFF - f))
1135 				);
1136 			})(target, source);
1137 		}
1138 	}
1139 
1140 	static if (mode == BlendMode.hardLight) {
1141 		/// ditto
1142 		void blendPixel()(ref Pixel target, const Pixel source) {
1143 			return alphaBlend!(accuracy, function(const ubyte b, const ubyte f) {
1144 				if (f < 0x80) {
1145 					return n255thsOf(castTo!ubyte(2 * f), b);
1146 				}
1147 				return castTo!ubyte(
1148 					0xFF - n255thsOf(castTo!ubyte(2 * (0xFF - f)), (0xFF - b))
1149 				);
1150 			})(target, source);
1151 		}
1152 	}
1153 
1154 	static if (mode == BlendMode.softLight) {
1155 		/// ditto
1156 		void blendPixel()(ref Pixel target, const Pixel source) {
1157 			return alphaBlend!(accuracy, function(const ubyte b, const ubyte f) {
1158 				if (f < 0x80) {
1159 					// dfmt off
1160 					return castTo!ubyte(
1161 						b - n255thsOf(
1162 								n255thsOf((0xFF - 2 * f).castTo!ubyte, b),
1163 								(0xFF - b),
1164 							)
1165 					);
1166 					// dfmt on
1167 				}
1168 
1169 				// TODO: optimize if possible
1170 				// dfmt off
1171 				immutable ubyte d = (b < 0x40)
1172 					? castTo!ubyte((b * (0x3FC + (((16 * b - 0xBF4) * b) / 255))) / 255)
1173 					: intNormalizedSqrt(b);
1174 				//dfmt on
1175 
1176 				return castTo!ubyte(
1177 					b + n255thsOf((2 * f - 0xFF).castTo!ubyte, (d - b).castTo!ubyte)
1178 				);
1179 			})(target, source);
1180 		}
1181 	}
1182 
1183 	static if (mode == BlendMode.colorDodge) {
1184 		/// ditto
1185 		void blendPixel()(ref Pixel target, const Pixel source) {
1186 			return alphaBlend!(accuracy, function(const ubyte b, const ubyte f) {
1187 				if (b == 0x00) {
1188 					return ubyte(0x00);
1189 				}
1190 				if (f == 0xFF) {
1191 					return ubyte(0xFF);
1192 				}
1193 				return min(
1194 					ubyte(0xFF),
1195 					clamp255((255 * b) / (0xFF - f))
1196 				);
1197 			})(target, source);
1198 		}
1199 	}
1200 
1201 	static if (mode == BlendMode.colorBurn) {
1202 		/// ditto
1203 		void blendPixel()(ref Pixel target, const Pixel source) {
1204 			return alphaBlend!(accuracy, function(const ubyte b, const ubyte f) {
1205 				if (b == 0xFF) {
1206 					return ubyte(0xFF);
1207 				}
1208 				if (f == 0x00) {
1209 					return ubyte(0x00);
1210 				}
1211 
1212 				immutable m = min(
1213 					ubyte(0xFF),
1214 					clamp255(((0xFF - b) * 255) / f)
1215 				);
1216 				return castTo!ubyte(0xFF - m);
1217 			})(target, source);
1218 		}
1219 	}
1220 
1221 	static if (mode == BlendMode.difference) {
1222 		/// ditto
1223 		void blendPixel()(ref Pixel target, const Pixel source) {
1224 			return alphaBlend!(accuracy,
1225 				(b, f) => (b > f) ? castTo!ubyte(b - f) : castTo!ubyte(f - b)
1226 			)(target, source);
1227 		}
1228 	}
1229 
1230 	static if (mode == BlendMode.exclusion) {
1231 		/// ditto
1232 		void blendPixel()(ref Pixel target, const Pixel source) {
1233 			return alphaBlend!(accuracy,
1234 				(b, f) => castTo!ubyte(b + f - (2 * n255thsOf(f, b)))
1235 			)(target, source);
1236 		}
1237 	}
1238 
1239 	static if (mode == BlendMode.subtract) {
1240 		/// ditto
1241 		void blendPixel()(ref Pixel target, const Pixel source) {
1242 			return alphaBlend!(accuracy,
1243 				(b, f) => (b > f) ? castTo!ubyte(b - f) : ubyte(0)
1244 			)(target, source);
1245 		}
1246 	}
1247 
1248 	static if (mode == BlendMode.divide) {
1249 		/// ditto
1250 		void blendPixel()(ref Pixel target, const Pixel source) {
1251 			return alphaBlend!(accuracy,
1252 				(b, f) => (f == 0) ? ubyte(0xFF) : clamp255(0xFF * b / f)
1253 			)(target, source);
1254 		}
1255 	}
1256 
1257 	//else {
1258 	//	static assert(false, "Missing `blendPixel()` implementation for `BlendMode`.`" ~ mode ~ "`.");
1259 	//}
1260 }
1261 
1262 /++
1263 	Blends the pixel data of `source` into `target`
1264 	using the requested $(B blending mode).
1265 
1266 	`source` and `target` MUST have the same length.
1267  +/
1268 void blendPixels(
1269 	BlendMode mode,
1270 	BlendAccuracy accuracy,
1271 )(scope Pixel[] target, scope const Pixel[] source) @trusted
1272 in (source.length == target.length) {
1273 	static if (mode == BlendMode.replace) {
1274 		// explicit optimization
1275 		target.ptr[0 .. target.length] = source.ptr[0 .. target.length];
1276 	} else {
1277 
1278 		// better error message in case it’s not implemented
1279 		static if (!is(typeof(blendPixel!(mode, accuracy)))) {
1280 			pragma(msg, "Hint: Missing or bad `blendPixel!(" ~ mode.stringof ~ ")`.");
1281 		}
1282 
1283 		foreach (immutable idx, ref pxTarget; target) {
1284 			blendPixel!(mode, accuracy)(pxTarget, source.ptr[idx]);
1285 		}
1286 	}
1287 }
1288 
1289 /// ditto
1290 void blendPixels(BlendAccuracy accuracy)(scope Pixel[] target, scope const Pixel[] source, BlendMode mode) {
1291 	import std.meta : NoDuplicates;
1292 	import std.traits : EnumMembers;
1293 
1294 	final switch (mode) with (BlendMode) {
1295 		static foreach (m; NoDuplicates!(EnumMembers!BlendMode)) {
1296 	case m:
1297 			return blendPixels!(m, accuracy)(target, source);
1298 		}
1299 	}
1300 }
1301 
1302 /// ditto
1303 void blendPixels(
1304 	scope Pixel[] target,
1305 	scope const Pixel[] source,
1306 	BlendMode mode,
1307 	BlendAccuracy accuracy = BlendAccuracy.rgba,
1308 ) {
1309 	if (accuracy == BlendAccuracy.rgb) {
1310 		return blendPixels!(BlendAccuracy.rgb)(target, source, mode);
1311 	} else {
1312 		return blendPixels!(BlendAccuracy.rgba)(target, source, mode);
1313 	}
1314 }
1315 
1316 // ==== Drawing functions ====
1317 
1318 /++
1319 	Draws a single pixel
1320  +/
1321 void drawPixel(Pixmap target, Point pos, Pixel color) {
1322 	immutable size_t offset = linearOffset(target.width, pos);
1323 	target.data[offset] = color;
1324 }
1325 
1326 /++
1327 	Draws a rectangle
1328  +/
1329 void drawRectangle(Pixmap target, Rectangle rectangle, Pixel color) {
1330 	alias r = rectangle;
1331 
1332 	immutable tRect = OriginRectangle(
1333 		Size(target.width, target.height),
1334 	);
1335 
1336 	// out of bounds?
1337 	if (!tRect.intersect(r)) {
1338 		return;
1339 	}
1340 
1341 	immutable drawingTarget = Point(
1342 		(r.pos.x >= 0) ? r.pos.x : 0,
1343 		(r.pos.y >= 0) ? r.pos.y : 0,
1344 	);
1345 
1346 	immutable drawingEnd = Point(
1347 		(r.right < tRect.right) ? r.right : tRect.right,
1348 		(r.bottom < tRect.bottom) ? r.bottom : tRect.bottom,
1349 	);
1350 
1351 	immutable int drawingWidth = drawingEnd.x - drawingTarget.x;
1352 
1353 	foreach (y; drawingTarget.y .. drawingEnd.y) {
1354 		target.sliceAt(Point(drawingTarget.x, y), drawingWidth)[] = color;
1355 	}
1356 }
1357 
1358 /++
1359 	Draws a line
1360  +/
1361 void drawLine(Pixmap target, Point a, Point b, Pixel color) {
1362 	import std.math : sqrt;
1363 
1364 	// TODO: line width
1365 	// TODO: anti-aliasing (looks awful without it!)
1366 
1367 	float deltaX = b.x - a.x;
1368 	float deltaY = b.y - a.y;
1369 	int steps = sqrt(deltaX * deltaX + deltaY * deltaY).castTo!int;
1370 
1371 	float[2] step = [
1372 		(deltaX / steps),
1373 		(deltaY / steps),
1374 	];
1375 
1376 	foreach (i; 0 .. steps) {
1377 		// dfmt off
1378 		immutable Point p = a + Point(
1379 			round(step[0] * i).castTo!int,
1380 			round(step[1] * i).castTo!int,
1381 		);
1382 		// dfmt on
1383 
1384 		immutable offset = linearOffset(p, target.width);
1385 		target.data[offset] = color;
1386 	}
1387 
1388 	immutable offsetEnd = linearOffset(b, target.width);
1389 	target.data[offsetEnd] = color;
1390 }
1391 
1392 /++
1393 	Draws an image (a source pixmap) on a target pixmap
1394 
1395 	Params:
1396 		target = target pixmap to draw on
1397 		image = source pixmap
1398 		pos = top-left destination position (on the target pixmap)
1399  +/
1400 void drawPixmap(Pixmap target, Pixmap image, Point pos, Blend blend = blendNormal) {
1401 	alias source = image;
1402 
1403 	immutable tRect = OriginRectangle(
1404 		Size(target.width, target.height),
1405 	);
1406 
1407 	immutable sRect = Rectangle(pos, source.size);
1408 
1409 	// out of bounds?
1410 	if (!tRect.intersect(sRect)) {
1411 		return;
1412 	}
1413 
1414 	immutable drawingTarget = Point(
1415 		(pos.x >= 0) ? pos.x : 0,
1416 		(pos.y >= 0) ? pos.y : 0,
1417 	);
1418 
1419 	immutable drawingEnd = Point(
1420 		(sRect.right < tRect.right) ? sRect.right : tRect.right,
1421 		(sRect.bottom < tRect.bottom) ? sRect.bottom : tRect.bottom,
1422 	);
1423 
1424 	immutable drawingSource = Point(drawingTarget.x, 0) - Point(sRect.pos.x, sRect.pos.y);
1425 	immutable int drawingWidth = drawingEnd.x - drawingTarget.x;
1426 
1427 	foreach (y; drawingTarget.y .. drawingEnd.y) {
1428 		blendPixels(
1429 			target.sliceAt(Point(drawingTarget.x, y), drawingWidth),
1430 			source.sliceAt(Point(drawingSource.x, y + drawingSource.y), drawingWidth),
1431 			blend,
1432 		);
1433 	}
1434 }
1435 
1436 /++
1437 	Draws a sprite from a spritesheet
1438  +/
1439 void drawSprite(Pixmap target, const SpriteSheet sheet, int spriteIndex, Point pos, Blend blend = blendNormal) {
1440 	immutable tRect = OriginRectangle(
1441 		Size(target.width, target.height),
1442 	);
1443 
1444 	immutable spriteOffset = sheet.getSpritePixelOffset2D(spriteIndex);
1445 	immutable sRect = Rectangle(pos, sheet.spriteSize);
1446 
1447 	// out of bounds?
1448 	if (!tRect.intersect(sRect)) {
1449 		return;
1450 	}
1451 
1452 	immutable drawingTarget = Point(
1453 		(pos.x >= 0) ? pos.x : 0,
1454 		(pos.y >= 0) ? pos.y : 0,
1455 	);
1456 
1457 	immutable drawingEnd = Point(
1458 		(sRect.right < tRect.right) ? sRect.right : tRect.right,
1459 		(sRect.bottom < tRect.bottom) ? sRect.bottom : tRect.bottom,
1460 	);
1461 
1462 	immutable drawingSource =
1463 		spriteOffset
1464 		+ Point(drawingTarget.x, 0)
1465 		- Point(sRect.pos.x, sRect.pos.y);
1466 	immutable int drawingWidth = drawingEnd.x - drawingTarget.x;
1467 
1468 	foreach (y; drawingTarget.y .. drawingEnd.y) {
1469 		blendPixels(
1470 			target.sliceAt(Point(drawingTarget.x, y), drawingWidth),
1471 			sheet.pixmap.sliceAt(Point(drawingSource.x, y + drawingSource.y), drawingWidth),
1472 			blend,
1473 		);
1474 	}
1475 }