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 }