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 }