The OpenD Programming Language

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