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 Pixmap refers to raster graphics, a subset of “bitmap” graphics. 19 A pixmap is an array of pixels and the corresponding meta data to describe 20 how an image if formed from those pixels. 21 In the case of this library, a “width” field is used to map a specified 22 number of pixels to a row of an image. 23 24 25 26 27 ### Pixel mapping 28 29 ```text 30 pixels := [ 0, 1, 2, 3 ] 31 width := 2 32 33 pixmap(pixels, width) 34 => [ 35 [ 0, 1 ] 36 [ 2, 3 ] 37 ] 38 ``` 39 40 ```text 41 pixels := [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 ] 42 width := 3 43 44 pixmap(pixels, width) 45 => [ 46 [ 0, 1, 2 ] 47 [ 3, 4, 5 ] 48 [ 6, 7, 8 ] 49 [ 9, 10, 11 ] 50 ] 51 ``` 52 53 ```text 54 pixels := [ 0, 1, 2, 3, 4, 5, 6, 7 ] 55 width := 4 56 57 pixmap(pixels, width) 58 => [ 59 [ 0, 1, 2, 3 ] 60 [ 4, 5, 6, 7 ] 61 ] 62 ``` 63 64 65 66 67 ### Colors 68 69 Colors are stored in an RGBA format with 8 bit per channel. 70 See [arsd.color.Color|Pixel] for details. 71 72 73 ### The coordinate system 74 75 The top left corner of a pixmap is its $(B origin) `(0,0)`. 76 77 The $(horizontal axis) is called `x`. 78 Its corresponding length/dimension is known as `width`. 79 80 The letter `y` is used to describe the $(B vertical axis). 81 Its corresponding length/dimension is known as `height`. 82 83 ``` 84 0 → x 85 ↓ 86 y 87 ``` 88 89 Furthermore, $(B length) refers to the areal size of a pixmap. 90 It represents the total number of pixels in a pixmap. 91 It follows from the foregoing that the term $(I long) usually refers to 92 the length (not the width). 93 94 95 96 97 ### Pixmaps 98 99 A [Pixmap] consist of two fields: 100 $(LIST 101 * a slice (of an array of [Pixel|Pixels]) 102 * a width 103 ) 104 105 This design comes with many advantages. 106 First and foremost it brings simplicity. 107 108 Pixel data buffers can be reused across pixmaps, 109 even when those have different sizes. 110 Simply slice the buffer to fit just enough pixels for the new pixmap. 111 112 Memory management can also happen outside of the pixmap. 113 It is possible to use a buffer allocated elsewhere. (Such a one shouldn’t 114 be mixed with the built-in memory management facilities of the pixmap type. 115 Otherwise one will end up with GC-allocated copies.) 116 117 The most important downside is that it makes pixmaps basically a partial 118 reference type. 119 120 Copying a pixmap creates a shallow copy still poiting to the same pixel 121 data that is also used by the source pixmap. 122 This implies that manipulating the source pixels also manipulates the 123 pixels of the copy – and vice versa. 124 125 The issues implied by this become an apparent when one of the references 126 modifies the pixel data in a way that also affects the dimensions of the 127 image; such as cropping. 128 129 Pixmaps describe how pixel data stored in a 1-dimensional memory space is 130 meant to be interpreted as a 2-dimensional image. 131 132 A notable implication of this 1D ↔ 2D mapping is, that slicing the 1D data 133 leads to non-sensical results in the 2D space when the 1D-slice is 134 reinterpreted as 2D-image. 135 136 Especially slicing across scanlines (→ horizontal rows of an image) is 137 prone to such errors. 138 139 (Slicing of the 1D array data can actually be utilized to cut off the 140 top or bottom part of an image. Any other naiv cropping operations will run 141 into the aforementioned issues.) 142 143 144 145 146 ### Image manipulation 147 148 The term “image manipulation function” here refers to functions that 149 manipulate (e.g. transform) an image as a whole. 150 151 Image manipulation functions in this library are provided in up to three 152 flavors: 153 154 $(LIST 155 * a “source to target” function 156 * a “source to newly allocated target” wrapper 157 * $(I optionally) an “in-place” adaption 158 ) 159 160 Additionally, a “compute dimensions of target” function is provided. 161 162 163 #### Source to Target 164 165 The regular “source to target” function takes (at least) two parameters: 166 A source [Pixmap] and a target [Pixmap]. 167 168 (Additional operation-specific arguments may be required as well.) 169 170 The target pixmap usually needs to be able to fit at least the same number 171 of pixels as the source holds. 172 Use the corresponding “compute size of target function” to calculate the 173 required size when needed. 174 (A notable exception would be cropping, where to target pixmap must be only 175 at least long enough to hold the area of the size to crop to.) 176 177 The data stored in the buffer of the target pixmap is overwritten by the 178 operation. 179 180 A modified Pixmap structure with adjusted dimensions is returned. 181 182 These functions are named plain and simple after the respective operation 183 they perform; e.g. [flipHorizontally] or [crop]. 184 185 --- 186 // Allocate a new target Pixmap. 187 Pixmap target = Pixmap.makeNew( 188 flipHorizontallyCalcDims(sourceImage) 189 ); 190 191 // Flip the image horizontally and store the updated structure. 192 // (Note: As a horizontal flip does not affect the dimensions of a Pixmap, 193 // storing the updated structure would not be necessary 194 // in this specific scenario.) 195 target = sourceImage.flipHorizontally(target); 196 --- 197 198 --- 199 const cropOffset = Point(0, 0); 200 const cropSize = Size(100, 100); 201 202 // Allocate a new target Pixmap. 203 Pixmap target = Pixmap.makeNew( 204 cropCalcDims(sourceImage, cropSize, cropOffset) 205 ); 206 207 // Crop the Pixmap. 208 target = sourceImage.crop(target, cropSize, cropOffset); 209 --- 210 211 $(PITFALL 212 “Source to target” functions do not work in place. 213 Do not attempt to pass Pixmaps sharing the same buffer for both source 214 and target. Such would lead to bad results with heavy artifacts. 215 216 Use the “in-place” variant of the operation instead. 217 218 Moreover: 219 Do not use the artifacts produced by this as a creative effect. 220 Those are an implementation detail (and may change at any point). 221 ) 222 223 224 #### Source to New Target 225 226 The “source to newly allocated target” wrapper allocates a new buffer to 227 hold the manipulated target. 228 229 These wrappers are provided for user convenience. 230 231 They are identified by the suffix `-New` that is appended to the name of 232 the corresponding “source to target” function; 233 e.g. [flipHorizontallyNew] or [cropNew]. 234 235 --- 236 // Create a new flipped Pixmap. 237 Pixmap target = sourceImage.flipHorizontallyNew(); 238 --- 239 240 --- 241 const cropOffset = Point(0, 0); 242 const cropSize = Size(100, 100); 243 244 // Create a new cropped Pixmap. 245 Pixmap target = sourceImage.cropNew(cropSize, cropOffset); 246 --- 247 248 249 #### In-Place 250 251 For selected image manipulation functions a special adaption is provided 252 that stores the result in the source pixel data buffer. 253 254 Depending on the operation, implementing in-place transformations can be 255 either straightforward or a major undertaking (and topic of research). 256 This library focuses and the former case and leaves out those where the 257 latter applies. 258 In particular, algorithms that require allocating further buffers to store 259 temporary results or auxiliary data will probably not get implemented. 260 261 Furthermore, operations where to result is larger than the source cannot 262 be performed in-place. 263 264 Certain in-place manipulation functions return a shallow-copy of the 265 source structure with dimensions adjusted accordingly. 266 This is behavior is not streamlined consistently as the lack of an 267 in-place option for certain operations makes them a special case anyway. 268 269 These function are suffixed with `-InPlace`; 270 e.g. [flipHorizontallyInPlace] or [cropInPlace]. 271 272 $(TIP 273 Manipulating the source image directly can lead to unexpected results 274 when the source image is used in multiple places. 275 ) 276 277 $(NOTE 278 Users are usually better off to utilize the regular “source to target” 279 functions with a reused pixel data buffer. 280 281 These functions do not serve as a performance optimization. 282 Some of them might perform significantly worse than their regular 283 variant. Always benchmark and profile. 284 ) 285 286 --- 287 image.flipHorizontallyInPlace(); 288 --- 289 290 --- 291 const cropOffset = Point(0, 0); 292 const cropSize = Size(100, 100); 293 294 image = image.cropInPlace(cropSize, cropOffset); 295 --- 296 297 298 #### Compute size of target 299 300 Functions to “compute (the) dimensions of (a) target” are primarily meant 301 to be utilized to calculate the size for allocating new pixmaps to be used 302 as a target for manipulation functions. 303 304 They are provided for all manipulation functions even in cases where they 305 are provide little to no benefit. This is for consistency and to ease 306 development. 307 308 Such functions are identified by a `-CalcDims` suffix; 309 e.g. [flipHorizontallyCalcDims] or [cropCalcDims]. 310 311 They usually take the same parameters as their corresponding 312 “source to new target” function. This does not apply in cases where 313 certain parameters are irrelevant for the computation of the target size. 314 +/ 315 module arsd.pixmappaint; 316 317 import arsd.color; 318 import arsd.core; 319 320 private float roundImpl(float f) { 321 import std.math : round; 322 323 return round(f); 324 } 325 326 // `pure` rounding function. 327 // std.math.round() isn’t pure on all targets. 328 // → <https://issues.dlang.org/show_bug.cgi?id=11320> 329 private float round(float f) pure @nogc nothrow @trusted { 330 return (castTo!(float function(float) pure @nogc nothrow)(&roundImpl))(f); 331 } 332 333 /* 334 ## TODO: 335 336 - Refactoring the template-mess of blendPixel() & co. 337 - Rotating (by arbitrary angles) 338 - Skewing 339 - HSL 340 - Advanced blend modes (maybe) 341 */ 342 343 /// 344 alias Color = arsd.color.Color; 345 346 /// 347 alias ColorF = arsd.color.ColorF; 348 349 /// 350 alias Pixel = Color; 351 352 /// 353 alias Point = arsd.color.Point; 354 355 /// 356 alias Rectangle = arsd.color.Rectangle; 357 358 /// 359 alias Size = arsd.color.Size; 360 361 // verify assumption(s) 362 static assert(Pixel.sizeof == uint.sizeof); 363 364 @safe pure nothrow @nogc { 365 /// 366 Pixel rgba(ubyte r, ubyte g, ubyte b, ubyte a = 0xFF) { 367 return Pixel(r, g, b, a); 368 } 369 370 /// 371 Pixel rgba(ubyte r, ubyte g, ubyte b, float aPct) { 372 return Pixel(r, g, b, percentageDecimalToUInt8(aPct)); 373 } 374 375 /// 376 Pixel rgb(ubyte r, ubyte g, ubyte b) { 377 return rgba(r, g, b, 0xFF); 378 } 379 } 380 381 /++ 382 Unsigned 64-bit fixed-point decimal type 383 384 Assigns 32 bits to the digits of the pre-decimal point portion 385 and the other 32 bits to fractional digits. 386 +/ 387 struct UDecimal { 388 private { 389 ulong _value = 0; 390 } 391 392 @safe pure nothrow @nogc: 393 394 /// 395 public this(uint initialValue) { 396 _value = (ulong(initialValue) << 32); 397 } 398 399 private static UDecimal make(ulong internal) { 400 auto result = UDecimal(); 401 result._value = internal; 402 return result; 403 } 404 405 /// 406 T opCast(T : uint)() const { 407 return (_value >> 32).castTo!uint; 408 } 409 410 /// 411 T opCast(T : double)() const { 412 return (_value / double(0xFFFF_FFFF)); 413 } 414 415 /// 416 T opCast(T : float)() const { 417 return (_value / float(0xFFFF_FFFF)); 418 } 419 420 /// 421 public UDecimal round() const { 422 const truncated = (_value & 0xFFFF_FFFF_0000_0000); 423 const delta = _value - truncated; 424 425 // dfmt off 426 const rounded = (delta >= 0x8000_0000) 427 ? truncated + 0x1_0000_0000 428 : truncated; 429 // dfmt on 430 431 return UDecimal.make(rounded); 432 } 433 434 /// 435 public UDecimal roundEven() const { 436 const truncated = (_value & 0xFFFF_FFFF_0000_0000); 437 const delta = _value - truncated; 438 439 ulong rounded; 440 441 if (delta == 0x8000_0000) { 442 const bool floorIsOdd = ((truncated & 0x1_0000_0000) != 0); 443 // dfmt off 444 rounded = (floorIsOdd) 445 ? truncated + 0x1_0000_0000 // ceil 446 : truncated; // floor 447 // dfmt on 448 } else if (delta > 0x8000_0000) { 449 rounded = truncated + 0x1_0000_0000; 450 } else { 451 rounded = truncated; 452 } 453 454 return UDecimal.make(rounded); 455 } 456 457 /// 458 public UDecimal floor() const { 459 const truncated = (_value & 0xFFFF_FFFF_0000_0000); 460 return UDecimal.make(truncated); 461 } 462 463 /// 464 public UDecimal ceil() const { 465 const truncated = (_value & 0xFFFF_FFFF_0000_0000); 466 467 // dfmt off 468 const ceiling = (truncated != _value) 469 ? truncated + 0x1_0000_0000 470 : truncated; 471 // dfmt on 472 473 return UDecimal.make(ceiling); 474 } 475 476 /// 477 public uint fractionalDigits() const { 478 return (_value & 0x0000_0000_FFFF_FFFF); 479 } 480 481 public { 482 /// 483 int opCmp(const UDecimal that) const { 484 return ((this._value > that._value) - (this._value < that._value)); 485 } 486 } 487 488 public { 489 /// 490 UDecimal opBinary(string op : "+")(const uint rhs) const { 491 return UDecimal.make(_value + (ulong(rhs) << 32)); 492 } 493 494 /// ditto 495 UDecimal opBinary(string op : "+")(const UDecimal rhs) const { 496 return UDecimal.make(_value + rhs._value); 497 } 498 499 /// ditto 500 UDecimal opBinary(string op : "-")(const uint rhs) const { 501 return UDecimal.make(_value - (ulong(rhs) << 32)); 502 } 503 504 /// ditto 505 UDecimal opBinary(string op : "-")(const UDecimal rhs) const { 506 return UDecimal.make(_value - rhs._value); 507 } 508 509 /// ditto 510 UDecimal opBinary(string op : "*")(const uint rhs) const { 511 return UDecimal.make(_value * rhs); 512 } 513 514 /// ditto 515 UDecimal opBinary(string op : "/")(const uint rhs) const { 516 return UDecimal.make(_value / rhs); 517 } 518 519 /// ditto 520 UDecimal opBinary(string op : "<<")(const uint rhs) const { 521 return UDecimal.make(_value << rhs); 522 } 523 524 /// ditto 525 UDecimal opBinary(string op : ">>")(const uint rhs) const { 526 return UDecimal.make(_value >> rhs); 527 } 528 } 529 530 public { 531 /// 532 UDecimal opBinaryRight(string op : "+")(const uint lhs) const { 533 return UDecimal.make((ulong(lhs) << 32) + _value); 534 } 535 536 /// ditto 537 UDecimal opBinaryRight(string op : "-")(const uint lhs) const { 538 return UDecimal.make((ulong(lhs) << 32) - _value); 539 } 540 541 /// ditto 542 UDecimal opBinaryRight(string op : "*")(const uint lhs) const { 543 return UDecimal.make(lhs * _value); 544 } 545 546 /// ditto 547 UDecimal opBinaryRight(string op : "/")(const uint) const { 548 static assert(false, "Use `uint(…) / cast(uint)(UDecimal(…))` instead."); 549 } 550 } 551 552 public { 553 /// 554 UDecimal opOpAssign(string op : "+")(const uint rhs) { 555 _value += (ulong(rhs) << 32); 556 return this; 557 } 558 559 /// ditto 560 UDecimal opOpAssign(string op : "+")(const UDecimal rhs) { 561 _value += rhs._value; 562 return this; 563 } 564 565 /// ditto 566 UDecimal opOpAssign(string op : "-")(const uint rhs) { 567 _value -= (ulong(rhs) << 32); 568 return this; 569 } 570 571 /// ditto 572 UDecimal opOpAssign(string op : "-")(const UDecimal rhs) { 573 _value -= rhs._value; 574 return this; 575 } 576 577 /// ditto 578 UDecimal opOpAssign(string op : "*")(const uint rhs) { 579 _value *= rhs; 580 return this; 581 } 582 583 /// ditto 584 UDecimal opOpAssign(string op : "/")(const uint rhs) { 585 _value /= rhs; 586 return this; 587 } 588 589 /// ditto 590 UDecimal opOpAssign(string op : "<<")(const uint rhs) const { 591 _value <<= rhs; 592 return this; 593 } 594 595 /// ditto 596 UDecimal opOpAssign(string op : ">>")(const uint rhs) const { 597 _value >>= rhs; 598 return this; 599 } 600 } 601 } 602 603 @safe unittest { 604 assert(UDecimal(uint.max).castTo!uint == uint.max); 605 assert(UDecimal(uint.min).castTo!uint == uint.min); 606 assert(UDecimal(1).castTo!uint == 1); 607 assert(UDecimal(2).castTo!uint == 2); 608 assert(UDecimal(1_991_007).castTo!uint == 1_991_007); 609 610 assert((UDecimal(10) + 9).castTo!uint == 19); 611 assert((UDecimal(10) - 9).castTo!uint == 1); 612 assert((UDecimal(10) * 9).castTo!uint == 90); 613 assert((UDecimal(99) / 9).castTo!uint == 11); 614 615 assert((4 + UDecimal(4)).castTo!uint == 8); 616 assert((4 - UDecimal(4)).castTo!uint == 0); 617 assert((4 * UDecimal(4)).castTo!uint == 16); 618 619 assert((UDecimal(uint.max) / 2).castTo!uint == 2_147_483_647); 620 assert((UDecimal(uint.max) / 2).round().castTo!uint == 2_147_483_648); 621 622 assert((UDecimal(10) / 8).round().castTo!uint == 1); 623 assert((UDecimal(10) / 8).floor().castTo!uint == 1); 624 assert((UDecimal(10) / 8).ceil().castTo!uint == 2); 625 626 assert((UDecimal(10) / 4).round().castTo!uint == 3); 627 assert((UDecimal(10) / 4).floor().castTo!uint == 2); 628 assert((UDecimal(10) / 4).ceil().castTo!uint == 3); 629 630 assert((UDecimal(10) / 5).round().castTo!uint == 2); 631 assert((UDecimal(10) / 5).floor().castTo!uint == 2); 632 assert((UDecimal(10) / 5).ceil().castTo!uint == 2); 633 } 634 635 @safe unittest { 636 UDecimal val; 637 638 val = (UDecimal(1) / 2); 639 assert(val.roundEven().castTo!uint == 0); 640 assert(val.castTo!double > 0.49); 641 assert(val.castTo!double < 0.51); 642 643 val = (UDecimal(3) / 2); 644 assert(val.roundEven().castTo!uint == 2); 645 assert(val.castTo!double > 1.49); 646 assert(val.castTo!double < 1.51); 647 } 648 649 @safe unittest { 650 UDecimal val; 651 652 val = UDecimal(10); 653 val += 12; 654 assert(val.castTo!uint == 22); 655 656 val = UDecimal(1024); 657 val -= 24; 658 assert(val.castTo!uint == 1000); 659 val -= 100; 660 assert(val.castTo!uint == 900); 661 val += 5; 662 assert(val.castTo!uint == 905); 663 664 val = UDecimal(256); 665 val *= 4; 666 assert(val.castTo!uint == (256 * 4)); 667 668 val = UDecimal(2048); 669 val /= 10; 670 val *= 10; 671 assert(val.castTo!uint == 2047); 672 } 673 674 @safe unittest { 675 UDecimal val; 676 677 val = UDecimal(9_000_000); 678 val /= 13; 679 val *= 4; 680 681 // ≈ 2,769,230.8 682 assert(val.castTo!uint == 2_769_230); 683 assert(val.round().castTo!uint == 2_769_231); 684 // assert(uint(9_000_000) / uint(13) * uint(4) == 2_769_228); 685 686 val = UDecimal(64); 687 val /= 31; 688 val *= 30; 689 val /= 29; 690 val *= 28; 691 692 // ≈ 59.8 693 assert(val.castTo!uint == 59); 694 assert(val.round().castTo!uint == 60); 695 // assert(((((64 / 31) * 30) / 29) * 28) == 56); 696 } 697 698 /++ 699 $(I Advanced functionality.) 700 701 Meta data for the construction of a Pixmap. 702 +/ 703 struct PixmapBlueprint { 704 /++ 705 Total number of pixels stored in a Pixmap. 706 +/ 707 size_t length; 708 709 /++ 710 Width of a Pixmap. 711 +/ 712 int width; 713 714 @safe pure nothrow @nogc: 715 716 /// 717 public static PixmapBlueprint fromSize(const Size size) { 718 return PixmapBlueprint( 719 size.area, 720 size.width, 721 ); 722 } 723 724 /// 725 public static PixmapBlueprint fromPixmap(const Pixmap pixmap) { 726 return PixmapBlueprint( 727 pixmap.length, 728 pixmap.width, 729 ); 730 } 731 732 /++ 733 Determines whether the blueprint is plausible. 734 +/ 735 bool isValid() const { 736 return ((length % width) == 0); 737 } 738 739 /++ 740 Height of a Pixmap. 741 742 See_also: 743 This is the counterpart to the dimension known as [width]. 744 +/ 745 int height() const { 746 return castTo!int(length / width); 747 } 748 749 /// 750 Size size() const { 751 return Size(width, height); 752 } 753 } 754 755 /++ 756 Pixel data container 757 +/ 758 struct Pixmap { 759 760 /// Pixel data 761 Pixel[] data; 762 763 /// Pixel per row 764 int width; 765 766 @safe pure nothrow: 767 768 /// 769 deprecated("Use `Pixmap.makeNew(size)` instead.") 770 this(Size size) { 771 this.size = size; 772 } 773 774 /// 775 deprecated("Use `Pixmap.makeNew(Size(width, height))` instead.") 776 this(int width, int height) 777 in (width > 0) 778 in (height > 0) { 779 this(Size(width, height)); 780 } 781 782 /// 783 this(inout(Pixel)[] data, int width) inout @nogc 784 in (data.length % width == 0) { 785 this.data = data; 786 this.width = width; 787 } 788 789 /// 790 static Pixmap makeNew(PixmapBlueprint blueprint) { 791 auto data = new Pixel[](blueprint.length); 792 return Pixmap(data, blueprint.width); 793 } 794 795 /// 796 static Pixmap makeNew(Size size) { 797 return Pixmap.makeNew(PixmapBlueprint.fromSize(size)); 798 } 799 800 /++ 801 Creates a $(I deep copy) of the Pixmap 802 +/ 803 Pixmap clone() const { 804 return Pixmap( 805 this.data.dup, 806 this.width, 807 ); 808 } 809 810 /++ 811 Copies the pixel data to the target Pixmap. 812 813 Returns: 814 A size-adjusted shallow copy of the input Pixmap overwritten 815 with the image data of the SubPixmap. 816 817 $(PITFALL 818 While the returned Pixmap utilizes the buffer provided by the input, 819 the returned Pixmap might not exactly match the input. 820 821 Always use the returned Pixmap structure. 822 823 --- 824 // Same buffer, but new structure: 825 auto pixmap2 = source.copyTo(pixmap); 826 827 // Alternatively, replace the old structure: 828 pixmap = source.copyTo(pixmap); 829 --- 830 ) 831 +/ 832 Pixmap copyTo(Pixmap target) @nogc const { 833 // Length adjustment 834 const l = this.length; 835 if (target.data.length < l) { 836 assert(false, "The target Pixmap is too small."); 837 } else if (target.data.length > l) { 838 target.data = target.data[0 .. l]; 839 } 840 841 copyToImpl(target); 842 843 return target; 844 } 845 846 private void copyToImpl(Pixmap target) @nogc const { 847 target.data[] = this.data[]; 848 } 849 850 // undocumented: really shouldn’t be used. 851 // carries the risks of `length` and `width` getting out of sync accidentally. 852 deprecated("Use `size` instead.") 853 void length(int value) { 854 data.length = value; 855 } 856 857 /++ 858 Changes the size of the buffer 859 860 Reallocates the underlying pixel array. 861 +/ 862 void size(Size value) { 863 data.length = value.area; 864 width = value.width; 865 } 866 867 /// ditto 868 void size(int totalPixels, int width) 869 in (totalPixels % width == 0) { 870 data.length = totalPixels; 871 this.width = width; 872 } 873 874 static { 875 /++ 876 Creates a Pixmap wrapping the pixel data from the provided `TrueColorImage`. 877 878 Interoperability function: `arsd.color` 879 +/ 880 Pixmap fromTrueColorImage(TrueColorImage source) @nogc { 881 return Pixmap(source.imageData.colors, source.width); 882 } 883 884 /++ 885 Creates a Pixmap wrapping the pixel data from the provided `MemoryImage`. 886 887 Interoperability function: `arsd.color` 888 +/ 889 Pixmap fromMemoryImage(MemoryImage source) { 890 return fromTrueColorImage(source.getAsTrueColorImage()); 891 } 892 } 893 894 @safe pure nothrow @nogc: 895 896 /// Height of the buffer, i.e. the number of lines 897 int height() inout { 898 if (width == 0) { 899 return 0; 900 } 901 902 return castTo!int(data.length / width); 903 } 904 905 /// Rectangular size of the buffer 906 Size size() inout { 907 return Size(width, height); 908 } 909 910 /// Length of the buffer, i.e. the number of pixels 911 int length() inout { 912 return castTo!int(data.length); 913 } 914 915 /++ 916 Number of bytes per line 917 918 Returns: 919 width × Pixel.sizeof 920 +/ 921 int pitch() inout { 922 return (width * int(Pixel.sizeof)); 923 } 924 925 /++ 926 Adjusts the Pixmap according to the provided blueprint. 927 928 The blueprint must not be larger than the data buffer of the pixmap. 929 930 This function does not reallocate the pixel data buffer. 931 932 If the blueprint is larger than the data buffer of the pixmap, 933 this will result in a bounds-check error if applicable. 934 +/ 935 void adjustTo(PixmapBlueprint blueprint) { 936 debug assert(this.data.length >= blueprint.length); 937 debug assert(blueprint.isValid); 938 this.data = this.data[0 .. blueprint.length]; 939 this.width = blueprint.width; 940 } 941 942 /++ 943 Calculates the index (linear offset) of the requested position 944 within the pixmap data. 945 +/ 946 int scanTo(Point pos) inout { 947 return linearOffset(width, pos); 948 } 949 950 /++ 951 Accesses the pixel at the requested position within the pixmap data. 952 +/ 953 ref inout(Pixel) scan(Point pos) inout { 954 return data[scanTo(pos)]; 955 } 956 957 /++ 958 Retrieves a linear slice of the pixmap. 959 960 Returns: 961 `n` pixels starting at the top-left position `pos`. 962 +/ 963 inout(Pixel)[] scan(Point pos, int n) inout { 964 immutable size_t offset = linearOffset(width, pos); 965 immutable size_t end = (offset + n); 966 return data[offset .. end]; 967 } 968 969 /// ditto 970 inout(Pixel)[] sliceAt(Point pos, int n) inout { 971 return scan(pos, n); 972 } 973 974 /++ 975 Retrieves a rectangular subimage of the pixmap. 976 +/ 977 inout(SubPixmap) scanArea(Point pos, Size size) inout { 978 return inout(SubPixmap)(this, size, pos); 979 } 980 981 /// TODO: remove 982 deprecated alias scanSubPixmap = scanArea; 983 984 /// TODO: remove 985 deprecated alias scan2D = scanArea; 986 987 /++ 988 Retrieves the first line of the Pixmap. 989 990 See_also: 991 Check out [PixmapScanner] for more useful scanning functionality. 992 +/ 993 inout(Pixel)[] scanLine() inout { 994 return data[0 .. width]; 995 } 996 997 public { 998 /++ 999 Provides access to a single pixel at the requested 2D-position. 1000 1001 See_also: 1002 Accessing pixels through the [data] array will be more useful, 1003 usually. 1004 +/ 1005 ref inout(Pixel) accessPixel(Point pos) inout @system { 1006 const idx = linearOffset(pos, this.width); 1007 return this.data[idx]; 1008 } 1009 1010 /// ditto 1011 Pixel getPixel(Point pos) const { 1012 const idx = linearOffset(pos, this.width); 1013 return this.data[idx]; 1014 } 1015 1016 /// ditto 1017 Pixel getPixel(int x, int y) const { 1018 return this.getPixel(Point(x, y)); 1019 } 1020 1021 /// ditto 1022 void setPixel(Point pos, Pixel value) { 1023 const idx = linearOffset(pos, this.width); 1024 this.data[idx] = value; 1025 } 1026 1027 /// ditto 1028 void setPixel(int x, int y, Pixel value) { 1029 return this.setPixel(Point(x, y), value); 1030 } 1031 } 1032 1033 /// Clears the buffer’s contents (by setting each pixel to the same color) 1034 void clear(Pixel value) { 1035 data[] = value; 1036 } 1037 } 1038 1039 /++ 1040 A subpixmap represents a subimage of a [Pixmap]. 1041 1042 This wrapper provides convenient access to a rectangular slice of a Pixmap. 1043 1044 ``` 1045 ╔═════════════╗ 1046 ║ Pixmap ║ 1047 ║ ║ 1048 ║ ┌───┐ ║ 1049 ║ │Sub│ ║ 1050 ║ └───┘ ║ 1051 ╚═════════════╝ 1052 ``` 1053 +/ 1054 struct SubPixmap { 1055 1056 /++ 1057 Source image referenced by the subimage 1058 +/ 1059 Pixmap source; 1060 1061 /++ 1062 Size of the subimage 1063 +/ 1064 Size size; 1065 1066 /++ 1067 2D offset of the subimage 1068 +/ 1069 Point offset; 1070 1071 public @safe pure nothrow @nogc { 1072 /// 1073 this(inout Pixmap source, Size size = Size(0, 0), Point offset = Point(0, 0)) inout { 1074 this.source = source; 1075 this.size = size; 1076 this.offset = offset; 1077 } 1078 1079 /// 1080 this(inout Pixmap source, Point offset, Size size = Size(0, 0)) inout { 1081 this(source, size, offset); 1082 } 1083 } 1084 1085 @safe pure nothrow: 1086 1087 public { 1088 /++ 1089 Allocates a new Pixmap cropped to the pixel data of the subimage. 1090 1091 See_also: 1092 Use [extractToPixmap] for a non-allocating variant with a 1093 target parameter. 1094 +/ 1095 Pixmap extractToNewPixmap() const { 1096 auto pm = Pixmap.makeNew(size); 1097 this.extractToPixmap(pm); 1098 return pm; 1099 } 1100 1101 /++ 1102 Copies the pixel data – cropped to the subimage region – 1103 into the target Pixmap. 1104 1105 $(PITFALL 1106 Do not attempt to extract a subimage back into the source pixmap. 1107 This will fail in cases where source and target regions overlap 1108 and potentially crash the program. 1109 ) 1110 1111 Returns: 1112 A size-adjusted shallow copy of the input Pixmap overwritten 1113 with the image data of the SubPixmap. 1114 1115 $(PITFALL 1116 While the returned Pixmap utilizes the buffer provided by the input, 1117 the returned Pixmap might not exactly match the input. 1118 The dimensions (width and height) and the length might have changed. 1119 1120 Always use the returned Pixmap structure. 1121 1122 --- 1123 // Same buffer, but new structure: 1124 auto pixmap2 = subPixmap.extractToPixmap(pixmap); 1125 1126 // Alternatively, replace the old structure: 1127 pixmap = subPixmap.extractToPixmap(pixmap); 1128 --- 1129 ) 1130 +/ 1131 Pixmap extractToPixmap(Pixmap target) @nogc const { 1132 // Length adjustment 1133 const l = this.length; 1134 if (target.data.length < l) { 1135 assert(false, "The target Pixmap is too small."); 1136 } else if (target.data.length > l) { 1137 target.data = target.data[0 .. l]; 1138 } 1139 1140 target.width = this.width; 1141 1142 extractToPixmapCopyImpl(target); 1143 return target; 1144 } 1145 1146 private void extractToPixmapCopyImpl(Pixmap target) @nogc const { 1147 auto src = SubPixmapScanner(this); 1148 auto dst = PixmapScannerRW(target); 1149 1150 foreach (dstLine; dst) { 1151 dstLine[] = src.front[]; 1152 src.popFront(); 1153 } 1154 } 1155 1156 private void extractToPixmapCopyPixelByPixelImpl(Pixmap target) @nogc const { 1157 auto src = SubPixmapScanner(this); 1158 auto dst = PixmapScannerRW(target); 1159 1160 foreach (dstLine; dst) { 1161 const srcLine = src.front; 1162 foreach (idx, ref px; dstLine) { 1163 px = srcLine[idx]; 1164 } 1165 src.popFront(); 1166 } 1167 } 1168 } 1169 1170 @safe pure nothrow @nogc: 1171 1172 public { 1173 /++ 1174 Width of the subimage. 1175 +/ 1176 int width() const { 1177 return size.width; 1178 } 1179 1180 /// ditto 1181 void width(int value) { 1182 size.width = value; 1183 } 1184 1185 /++ 1186 Height of the subimage. 1187 +/ 1188 int height() const { 1189 return size.height; 1190 } 1191 1192 /// ditto 1193 void height(int value) { 1194 size.height = value; 1195 } 1196 1197 /++ 1198 Number of pixels in the subimage. 1199 +/ 1200 int length() const { 1201 return size.area; 1202 } 1203 } 1204 1205 public { 1206 /++ 1207 Linear offset of the subimage within the source image. 1208 1209 Calculates the index of the “first pixel of the subimage” 1210 in the “pixel data of the source image”. 1211 +/ 1212 int sourceOffsetLinear() const { 1213 return linearOffset(offset, source.width); 1214 } 1215 1216 /// ditto 1217 void sourceOffsetLinear(int value) { 1218 this.offset = Point.fromLinearOffset(value, source.width); 1219 } 1220 1221 /++ 1222 $(I Advanced functionality.) 1223 1224 Offset of the pixel following the bottom right corner of the subimage. 1225 1226 (`Point(O, 0)` is the top left corner of the source image.) 1227 +/ 1228 Point sourceOffsetEnd() const { 1229 auto vec = Point(size.width, (size.height - 1)); 1230 return (offset + vec); 1231 } 1232 1233 /++ 1234 Linear offset of the subimage within the source image. 1235 1236 Calculates the index of the “first pixel of the subimage” 1237 in the “pixel data of the source image”. 1238 +/ 1239 int sourceOffsetLinearEnd() const { 1240 return linearOffset(sourceOffsetEnd, source.width); 1241 } 1242 } 1243 1244 /++ 1245 Determines whether the area of the subimage 1246 lies within the source image 1247 and does not overflow its lines. 1248 1249 $(TIP 1250 If the offset and/or size of a subimage are off, two issues can occur: 1251 1252 $(LIST 1253 * The resulting subimage will look displaced. 1254 (As if the lines were shifted.) 1255 This indicates that one scanline of the subimage spans over 1256 two ore more lines of the source image. 1257 (Happens when `(subimage.offset.x + subimage.size.width) > source.size.width`.) 1258 * When accessing the pixel data, bounds checks will fail. 1259 This suggests that the area of the subimage extends beyond 1260 the bottom end (and optionally also beyond the right end) of 1261 the source. 1262 ) 1263 1264 Both defects could indicate an invalid subimage. 1265 Use this function to verify the SubPixmap. 1266 ) 1267 1268 $(WARNING 1269 Do not use invalid SubPixmaps. 1270 The library assumes that the SubPixmaps it receives are always valid. 1271 1272 Non-valid SubPixmaps are not meant to be used for creative effects 1273 or similar either. Such uses might lead to unexpected quirks or 1274 crashes eventually. 1275 ) 1276 +/ 1277 bool isValid() const { 1278 return ( 1279 (sourceMarginLeft >= 0) 1280 && (sourceMarginTop >= 0) 1281 && (sourceMarginBottom >= 0) 1282 && (sourceMarginRight >= 0) 1283 ); 1284 } 1285 1286 public inout { 1287 /++ 1288 Retrieves the pixel at the requested position of the subimage. 1289 +/ 1290 ref inout(Pixel) scan(Point pos) { 1291 return source.scan(offset + pos); 1292 } 1293 1294 /++ 1295 Retrieves the first line of the subimage. 1296 +/ 1297 inout(Pixel)[] scanLine() { 1298 const lo = linearOffset(offset, size.width); 1299 return source.data[lo .. size.width]; 1300 } 1301 } 1302 1303 /++ 1304 Copies the pixels of this subimage to a target image. 1305 1306 The target MUST have the same size. 1307 1308 See_also: 1309 Usually you’ll want to use [extractToPixmap] or [drawPixmap] instead. 1310 +/ 1311 public void xferTo(SubPixmap target) const { 1312 debug assert(target.size == this.size); 1313 1314 auto src = SubPixmapScanner(this); 1315 auto dst = SubPixmapScannerRW(target); 1316 1317 foreach (dstLine; dst) { 1318 dstLine[] = src.front[]; 1319 src.popFront(); 1320 } 1321 } 1322 1323 /++ 1324 Blends the pixels of this subimage into a target image. 1325 1326 The target MUST have the same size. 1327 1328 See_also: 1329 Usually you’ll want to use [extractToPixmap] or [drawPixmap] instead. 1330 +/ 1331 public void xferTo(SubPixmap target, Blend blend) const { 1332 debug assert(target.size == this.size); 1333 1334 auto src = SubPixmapScanner(this); 1335 auto dst = SubPixmapScannerRW(target); 1336 1337 foreach (dstLine; dst) { 1338 blendPixels(dstLine, src.front, blend); 1339 src.popFront(); 1340 } 1341 } 1342 1343 // opposite offset 1344 public const { 1345 /++ 1346 $(I Advanced functionality.) 1347 1348 Offset of the bottom right corner of the source image 1349 to the bottom right corner of the subimage. 1350 1351 ``` 1352 ╔═══════════╗ 1353 ║ ║ 1354 ║ ┌───┐ ║ 1355 ║ │ │ ║ 1356 ║ └───┘ ║ 1357 ║ ↘ ║ 1358 ╚═══════════╝ 1359 ``` 1360 +/ 1361 Point oppositeOffset() { 1362 return Point(oppositeOffsetX, oppositeOffsetY); 1363 } 1364 1365 /++ 1366 $(I Advanced functionality.) 1367 1368 Offset of the right edge of the source image 1369 to the right edge of the subimage. 1370 1371 ``` 1372 ╔═══════════╗ 1373 ║ ║ 1374 ║ ┌───┐ ║ 1375 ║ │ S │ → ║ 1376 ║ └───┘ ║ 1377 ║ ║ 1378 ╚═══════════╝ 1379 ``` 1380 +/ 1381 int oppositeOffsetX() { 1382 return (offset.x + size.width); 1383 } 1384 1385 /++ 1386 $(I Advanced functionality.) 1387 1388 Offset of the bottom edge of the source image 1389 to the bottom edge of the subimage. 1390 1391 ``` 1392 ╔═══════════╗ 1393 ║ ║ 1394 ║ ┌───┐ ║ 1395 ║ │ S │ ║ 1396 ║ └───┘ ║ 1397 ║ ↓ ║ 1398 ╚═══════════╝ 1399 ``` 1400 +/ 1401 int oppositeOffsetY() { 1402 return (offset.y + size.height); 1403 } 1404 1405 } 1406 1407 // source-image margins 1408 public const { 1409 /++ 1410 $(I Advanced functionality.) 1411 1412 X-axis margin (left + right) of the subimage within the source image. 1413 1414 ``` 1415 ╔═══════════╗ 1416 ║ ║ 1417 ║ ┌───┐ ║ 1418 ║ ↔ │ S │ ↔ ║ 1419 ║ └───┘ ║ 1420 ║ ║ 1421 ╚═══════════╝ 1422 ``` 1423 +/ 1424 int sourceMarginX() { 1425 return (source.width - size.width); 1426 } 1427 1428 /++ 1429 $(I Advanced functionality.) 1430 1431 Y-axis margin (top + bottom) of the subimage within the source image. 1432 1433 ``` 1434 ╔═══════════╗ 1435 ║ ↕ ║ 1436 ║ ┌───┐ ║ 1437 ║ │ S │ ║ 1438 ║ └───┘ ║ 1439 ║ ↕ ║ 1440 ╚═══════════╝ 1441 ``` 1442 +/ 1443 int sourceMarginY() { 1444 return (source.height - size.height); 1445 } 1446 1447 /++ 1448 $(I Advanced functionality.) 1449 1450 Top margin of the subimage within the source image. 1451 1452 ``` 1453 ╔═══════════╗ 1454 ║ ↕ ║ 1455 ║ ┌───┐ ║ 1456 ║ │ S │ ║ 1457 ║ └───┘ ║ 1458 ║ ║ 1459 ╚═══════════╝ 1460 ``` 1461 +/ 1462 int sourceMarginTop() { 1463 return offset.y; 1464 } 1465 1466 /++ 1467 $(I Advanced functionality.) 1468 1469 Right margin of the subimage within the source image. 1470 1471 ``` 1472 ╔═══════════╗ 1473 ║ ║ 1474 ║ ┌───┐ ║ 1475 ║ │ S │ ↔ ║ 1476 ║ └───┘ ║ 1477 ║ ║ 1478 ╚═══════════╝ 1479 ``` 1480 +/ 1481 int sourceMarginRight() { 1482 return (sourceMarginX - sourceMarginLeft); 1483 } 1484 1485 /++ 1486 $(I Advanced functionality.) 1487 1488 Bottom margin of the subimage within the source image. 1489 1490 ``` 1491 ╔═══════════╗ 1492 ║ ║ 1493 ║ ┌───┐ ║ 1494 ║ │ S │ ║ 1495 ║ └───┘ ║ 1496 ║ ↕ ║ 1497 ╚═══════════╝ 1498 ``` 1499 +/ 1500 int sourceMarginBottom() { 1501 return (sourceMarginY - sourceMarginTop); 1502 } 1503 1504 /++ 1505 $(I Advanced functionality.) 1506 1507 Left margin of the subimage within the source image. 1508 1509 ``` 1510 ╔═══════════╗ 1511 ║ ║ 1512 ║ ┌───┐ ║ 1513 ║ ↔ │ S │ ║ 1514 ║ └───┘ ║ 1515 ║ ║ 1516 ╚═══════════╝ 1517 ``` 1518 +/ 1519 int sourceMarginLeft() { 1520 return offset.x; 1521 } 1522 } 1523 1524 public const { 1525 /++ 1526 $(I Advanced functionality.) 1527 1528 Calculates the linear offset of the provided point in the subimage 1529 relative to the source image. 1530 +/ 1531 int sourceOffsetOf(Point pos) { 1532 pos = (pos + offset); 1533 return linearOffset(pos, source.width); 1534 } 1535 } 1536 } 1537 1538 /++ 1539 $(I Advanced functionality.) 1540 1541 Wrapper for scanning a [Pixmap] line by line. 1542 +/ 1543 struct PixmapScanner { 1544 private { 1545 const(Pixel)[] _data; 1546 int _width; 1547 } 1548 1549 @safe pure nothrow @nogc: 1550 1551 /// 1552 public this(const(Pixmap) pixmap) { 1553 _data = pixmap.data; 1554 _width = pixmap.width; 1555 } 1556 1557 /// 1558 typeof(this) save() { 1559 return this; 1560 } 1561 1562 /// 1563 bool empty() const { 1564 return (_data.length == 0); 1565 } 1566 1567 /// 1568 const(Pixel)[] front() const { 1569 return _data[0 .. _width]; 1570 } 1571 1572 /// 1573 void popFront() { 1574 _data = _data[_width .. $]; 1575 } 1576 1577 /// 1578 const(Pixel)[] back() const { 1579 return _data[($ - _width) .. $]; 1580 } 1581 1582 /// 1583 void popBack() { 1584 _data = _data[0 .. ($ - _width)]; 1585 } 1586 } 1587 1588 /++ 1589 $(I Advanced functionality.) 1590 1591 Wrapper for scanning a [Pixmap] line by line. 1592 1593 See_also: 1594 Unlike [PixmapScanner], this does not work with `const(Pixmap)`. 1595 +/ 1596 struct PixmapScannerRW { 1597 private { 1598 Pixel[] _data; 1599 int _width; 1600 } 1601 1602 @safe pure nothrow @nogc: 1603 1604 /// 1605 public this(Pixmap pixmap) { 1606 _data = pixmap.data; 1607 _width = pixmap.width; 1608 } 1609 1610 /// 1611 typeof(this) save() { 1612 return this; 1613 } 1614 1615 /// 1616 bool empty() const { 1617 return (_data.length == 0); 1618 } 1619 1620 /// 1621 Pixel[] front() { 1622 return _data[0 .. _width]; 1623 } 1624 1625 /// 1626 void popFront() { 1627 _data = _data[_width .. $]; 1628 } 1629 1630 /// 1631 Pixel[] back() { 1632 return _data[($ - _width) .. $]; 1633 } 1634 1635 /// 1636 void popBack() { 1637 _data = _data[0 .. ($ - _width)]; 1638 } 1639 } 1640 1641 /++ 1642 $(I Advanced functionality.) 1643 1644 Wrapper for scanning a [Pixmap] line by line. 1645 +/ 1646 struct SubPixmapScanner { 1647 private { 1648 const(Pixel)[] _data; 1649 int _width; 1650 int _feed; 1651 } 1652 1653 @safe pure nothrow @nogc: 1654 1655 /// 1656 public this(const(SubPixmap) subPixmap) { 1657 _data = subPixmap.source.data[subPixmap.sourceOffsetLinear .. subPixmap.sourceOffsetLinearEnd]; 1658 _width = subPixmap.size.width; 1659 _feed = subPixmap.source.width; 1660 } 1661 1662 /// 1663 typeof(this) save() { 1664 return this; 1665 } 1666 1667 /// 1668 bool empty() const { 1669 return (_data.length == 0); 1670 } 1671 1672 /// 1673 const(Pixel)[] front() const { 1674 return _data[0 .. _width]; 1675 } 1676 1677 /// 1678 void popFront() { 1679 if (_data.length < _feed) { 1680 _data.length = 0; 1681 return; 1682 } 1683 1684 _data = _data[_feed .. $]; 1685 } 1686 1687 /// 1688 const(Pixel)[] back() const { 1689 return _data[($ - _width) .. $]; 1690 } 1691 1692 /// 1693 void popBack() { 1694 if (_data.length < _feed) { 1695 _data.length = 0; 1696 return; 1697 } 1698 1699 _data = _data[0 .. ($ - _feed)]; 1700 } 1701 } 1702 1703 /++ 1704 $(I Advanced functionality.) 1705 1706 Wrapper for scanning a [Pixmap] line by line. 1707 1708 See_also: 1709 Unlike [SubPixmapScanner], this does not work with `const(SubPixmap)`. 1710 +/ 1711 struct SubPixmapScannerRW { 1712 private { 1713 Pixel[] _data; 1714 int _width; 1715 int _feed; 1716 } 1717 1718 @safe pure nothrow @nogc: 1719 1720 /// 1721 public this(SubPixmap subPixmap) { 1722 _data = subPixmap.source.data[subPixmap.sourceOffsetLinear .. subPixmap.sourceOffsetLinearEnd]; 1723 _width = subPixmap.size.width; 1724 _feed = subPixmap.source.width; 1725 } 1726 1727 /// 1728 typeof(this) save() { 1729 return this; 1730 } 1731 1732 /// 1733 bool empty() const { 1734 return (_data.length == 0); 1735 } 1736 1737 /// 1738 Pixel[] front() { 1739 return _data[0 .. _width]; 1740 } 1741 1742 /// 1743 void popFront() { 1744 if (_data.length < _feed) { 1745 _data.length = 0; 1746 return; 1747 } 1748 1749 _data = _data[_feed .. $]; 1750 } 1751 1752 /// 1753 Pixel[] back() { 1754 return _data[($ - _width) .. $]; 1755 } 1756 1757 /// 1758 void popBack() { 1759 if (_data.length < _feed) { 1760 _data.length = 0; 1761 return; 1762 } 1763 1764 _data = _data[0 .. ($ - _feed)]; 1765 } 1766 } 1767 1768 /// 1769 struct SpriteSheet { 1770 private { 1771 Pixmap _pixmap; 1772 Size _spriteDimensions; 1773 Size _layout; // pre-computed upon construction 1774 } 1775 1776 @safe pure nothrow @nogc: 1777 1778 /// 1779 public this(Pixmap pixmap, Size spriteSize) { 1780 _pixmap = pixmap; 1781 _spriteDimensions = spriteSize; 1782 1783 _layout = Size( 1784 _pixmap.width / _spriteDimensions.width, 1785 _pixmap.height / _spriteDimensions.height, 1786 ); 1787 } 1788 1789 /// 1790 inout(Pixmap) pixmap() inout { 1791 return _pixmap; 1792 } 1793 1794 /// 1795 Size spriteSize() inout { 1796 return _spriteDimensions; 1797 } 1798 1799 /// 1800 Size layout() inout { 1801 return _layout; 1802 } 1803 1804 /// 1805 Point getSpriteColumn(int index) inout { 1806 immutable x = index % layout.width; 1807 immutable y = (index - x) / layout.height; 1808 return Point(x, y); 1809 } 1810 1811 /// 1812 Point getSpritePixelOffset2D(int index) inout { 1813 immutable col = this.getSpriteColumn(index); 1814 return Point( 1815 col.x * _spriteDimensions.width, 1816 col.y * _spriteDimensions.height, 1817 ); 1818 } 1819 } 1820 1821 // Silly micro-optimization 1822 private struct OriginRectangle { 1823 Size size; 1824 1825 @safe pure nothrow @nogc: 1826 1827 int left() const => 0; 1828 int top() const => 0; 1829 int right() const => size.width; 1830 int bottom() const => size.height; 1831 1832 bool intersect(const Rectangle b) const { 1833 // dfmt off 1834 return ( 1835 (b.right > 0 ) && 1836 (b.left < this.right ) && 1837 (b.bottom > 0 ) && 1838 (b.top < this.bottom) 1839 ); 1840 // dfmt on 1841 } 1842 } 1843 1844 @safe pure nothrow: 1845 1846 // misc 1847 private @nogc { 1848 Point pos(Rectangle r) => r.upperLeft; 1849 1850 T max(T)(T a, T b) => (a >= b) ? a : b; 1851 T min(T)(T a, T b) => (a <= b) ? a : b; 1852 } 1853 1854 /++ 1855 Calculates the square root 1856 of an integer number 1857 as an integer number. 1858 +/ 1859 ubyte intSqrt(const ubyte value) @safe pure nothrow @nogc { 1860 switch (value) { 1861 default: 1862 // unreachable 1863 assert(false, "ubyte != uint8"); 1864 case 0: 1865 return 0; 1866 case 1: .. case 2: 1867 return 1; 1868 case 3: .. case 6: 1869 return 2; 1870 case 7: .. case 12: 1871 return 3; 1872 case 13: .. case 20: 1873 return 4; 1874 case 21: .. case 30: 1875 return 5; 1876 case 31: .. case 42: 1877 return 6; 1878 case 43: .. case 56: 1879 return 7; 1880 case 57: .. case 72: 1881 return 8; 1882 case 73: .. case 90: 1883 return 9; 1884 case 91: .. case 110: 1885 return 10; 1886 case 111: .. case 132: 1887 return 11; 1888 case 133: .. case 156: 1889 return 12; 1890 case 157: .. case 182: 1891 return 13; 1892 case 183: .. case 210: 1893 return 14; 1894 case 211: .. case 240: 1895 return 15; 1896 case 241: .. case 255: 1897 return 16; 1898 } 1899 } 1900 1901 /// 1902 unittest { 1903 assert(intSqrt(4) == 2); 1904 assert(intSqrt(9) == 3); 1905 assert(intSqrt(10) == 3); 1906 } 1907 1908 unittest { 1909 import std.math : round, sqrt; 1910 1911 foreach (n; ubyte.min .. ubyte.max + 1) { 1912 ubyte fp = sqrt(float(n)).round().castTo!ubyte; 1913 ubyte i8 = intSqrt(n.castTo!ubyte); 1914 assert(fp == i8); 1915 } 1916 } 1917 1918 /++ 1919 Calculates the square root 1920 of the normalized value 1921 representated by the input integer number. 1922 1923 Normalization: 1924 `[0x00 .. 0xFF]` → `[0.0 .. 1.0]` 1925 1926 Returns: 1927 sqrt(value / 255f) * 255 1928 +/ 1929 ubyte intNormalizedSqrt(const ubyte value) @nogc { 1930 switch (value) { 1931 default: 1932 // unreachable 1933 assert(false, "ubyte != uint8"); 1934 case 0x00: 1935 return 0x00; 1936 case 0x01: 1937 return 0x10; 1938 case 0x02: 1939 return 0x17; 1940 case 0x03: 1941 return 0x1C; 1942 case 0x04: 1943 return 0x20; 1944 case 0x05: 1945 return 0x24; 1946 case 0x06: 1947 return 0x27; 1948 case 0x07: 1949 return 0x2A; 1950 case 0x08: 1951 return 0x2D; 1952 case 0x09: 1953 return 0x30; 1954 case 0x0A: 1955 return 0x32; 1956 case 0x0B: 1957 return 0x35; 1958 case 0x0C: 1959 return 0x37; 1960 case 0x0D: 1961 return 0x3A; 1962 case 0x0E: 1963 return 0x3C; 1964 case 0x0F: 1965 return 0x3E; 1966 case 0x10: 1967 return 0x40; 1968 case 0x11: 1969 return 0x42; 1970 case 0x12: 1971 return 0x44; 1972 case 0x13: 1973 return 0x46; 1974 case 0x14: 1975 return 0x47; 1976 case 0x15: 1977 return 0x49; 1978 case 0x16: 1979 return 0x4B; 1980 case 0x17: 1981 return 0x4D; 1982 case 0x18: 1983 return 0x4E; 1984 case 0x19: 1985 return 0x50; 1986 case 0x1A: 1987 return 0x51; 1988 case 0x1B: 1989 return 0x53; 1990 case 0x1C: 1991 return 0x54; 1992 case 0x1D: 1993 return 0x56; 1994 case 0x1E: 1995 return 0x57; 1996 case 0x1F: 1997 return 0x59; 1998 case 0x20: 1999 return 0x5A; 2000 case 0x21: 2001 return 0x5C; 2002 case 0x22: 2003 return 0x5D; 2004 case 0x23: 2005 return 0x5E; 2006 case 0x24: 2007 return 0x60; 2008 case 0x25: 2009 return 0x61; 2010 case 0x26: 2011 return 0x62; 2012 case 0x27: 2013 return 0x64; 2014 case 0x28: 2015 return 0x65; 2016 case 0x29: 2017 return 0x66; 2018 case 0x2A: 2019 return 0x67; 2020 case 0x2B: 2021 return 0x69; 2022 case 0x2C: 2023 return 0x6A; 2024 case 0x2D: 2025 return 0x6B; 2026 case 0x2E: 2027 return 0x6C; 2028 case 0x2F: 2029 return 0x6D; 2030 case 0x30: 2031 return 0x6F; 2032 case 0x31: 2033 return 0x70; 2034 case 0x32: 2035 return 0x71; 2036 case 0x33: 2037 return 0x72; 2038 case 0x34: 2039 return 0x73; 2040 case 0x35: 2041 return 0x74; 2042 case 0x36: 2043 return 0x75; 2044 case 0x37: 2045 return 0x76; 2046 case 0x38: 2047 return 0x77; 2048 case 0x39: 2049 return 0x79; 2050 case 0x3A: 2051 return 0x7A; 2052 case 0x3B: 2053 return 0x7B; 2054 case 0x3C: 2055 return 0x7C; 2056 case 0x3D: 2057 return 0x7D; 2058 case 0x3E: 2059 return 0x7E; 2060 case 0x3F: 2061 return 0x7F; 2062 case 0x40: 2063 return 0x80; 2064 case 0x41: 2065 return 0x81; 2066 case 0x42: 2067 return 0x82; 2068 case 0x43: 2069 return 0x83; 2070 case 0x44: 2071 return 0x84; 2072 case 0x45: 2073 return 0x85; 2074 case 0x46: 2075 return 0x86; 2076 case 0x47: .. case 0x48: 2077 return 0x87; 2078 case 0x49: 2079 return 0x88; 2080 case 0x4A: 2081 return 0x89; 2082 case 0x4B: 2083 return 0x8A; 2084 case 0x4C: 2085 return 0x8B; 2086 case 0x4D: 2087 return 0x8C; 2088 case 0x4E: 2089 return 0x8D; 2090 case 0x4F: 2091 return 0x8E; 2092 case 0x50: 2093 return 0x8F; 2094 case 0x51: 2095 return 0x90; 2096 case 0x52: .. case 0x53: 2097 return 0x91; 2098 case 0x54: 2099 return 0x92; 2100 case 0x55: 2101 return 0x93; 2102 case 0x56: 2103 return 0x94; 2104 case 0x57: 2105 return 0x95; 2106 case 0x58: 2107 return 0x96; 2108 case 0x59: .. case 0x5A: 2109 return 0x97; 2110 case 0x5B: 2111 return 0x98; 2112 case 0x5C: 2113 return 0x99; 2114 case 0x5D: 2115 return 0x9A; 2116 case 0x5E: 2117 return 0x9B; 2118 case 0x5F: .. case 0x60: 2119 return 0x9C; 2120 case 0x61: 2121 return 0x9D; 2122 case 0x62: 2123 return 0x9E; 2124 case 0x63: 2125 return 0x9F; 2126 case 0x64: .. case 0x65: 2127 return 0xA0; 2128 case 0x66: 2129 return 0xA1; 2130 case 0x67: 2131 return 0xA2; 2132 case 0x68: 2133 return 0xA3; 2134 case 0x69: .. case 0x6A: 2135 return 0xA4; 2136 case 0x6B: 2137 return 0xA5; 2138 case 0x6C: 2139 return 0xA6; 2140 case 0x6D: .. case 0x6E: 2141 return 0xA7; 2142 case 0x6F: 2143 return 0xA8; 2144 case 0x70: 2145 return 0xA9; 2146 case 0x71: .. case 0x72: 2147 return 0xAA; 2148 case 0x73: 2149 return 0xAB; 2150 case 0x74: 2151 return 0xAC; 2152 case 0x75: .. case 0x76: 2153 return 0xAD; 2154 case 0x77: 2155 return 0xAE; 2156 case 0x78: 2157 return 0xAF; 2158 case 0x79: .. case 0x7A: 2159 return 0xB0; 2160 case 0x7B: 2161 return 0xB1; 2162 case 0x7C: 2163 return 0xB2; 2164 case 0x7D: .. case 0x7E: 2165 return 0xB3; 2166 case 0x7F: 2167 return 0xB4; 2168 case 0x80: .. case 0x81: 2169 return 0xB5; 2170 case 0x82: 2171 return 0xB6; 2172 case 0x83: .. case 0x84: 2173 return 0xB7; 2174 case 0x85: 2175 return 0xB8; 2176 case 0x86: 2177 return 0xB9; 2178 case 0x87: .. case 0x88: 2179 return 0xBA; 2180 case 0x89: 2181 return 0xBB; 2182 case 0x8A: .. case 0x8B: 2183 return 0xBC; 2184 case 0x8C: 2185 return 0xBD; 2186 case 0x8D: .. case 0x8E: 2187 return 0xBE; 2188 case 0x8F: 2189 return 0xBF; 2190 case 0x90: .. case 0x91: 2191 return 0xC0; 2192 case 0x92: 2193 return 0xC1; 2194 case 0x93: .. case 0x94: 2195 return 0xC2; 2196 case 0x95: 2197 return 0xC3; 2198 case 0x96: .. case 0x97: 2199 return 0xC4; 2200 case 0x98: 2201 return 0xC5; 2202 case 0x99: .. case 0x9A: 2203 return 0xC6; 2204 case 0x9B: .. case 0x9C: 2205 return 0xC7; 2206 case 0x9D: 2207 return 0xC8; 2208 case 0x9E: .. case 0x9F: 2209 return 0xC9; 2210 case 0xA0: 2211 return 0xCA; 2212 case 0xA1: .. case 0xA2: 2213 return 0xCB; 2214 case 0xA3: .. case 0xA4: 2215 return 0xCC; 2216 case 0xA5: 2217 return 0xCD; 2218 case 0xA6: .. case 0xA7: 2219 return 0xCE; 2220 case 0xA8: 2221 return 0xCF; 2222 case 0xA9: .. case 0xAA: 2223 return 0xD0; 2224 case 0xAB: .. case 0xAC: 2225 return 0xD1; 2226 case 0xAD: 2227 return 0xD2; 2228 case 0xAE: .. case 0xAF: 2229 return 0xD3; 2230 case 0xB0: .. case 0xB1: 2231 return 0xD4; 2232 case 0xB2: 2233 return 0xD5; 2234 case 0xB3: .. case 0xB4: 2235 return 0xD6; 2236 case 0xB5: .. case 0xB6: 2237 return 0xD7; 2238 case 0xB7: 2239 return 0xD8; 2240 case 0xB8: .. case 0xB9: 2241 return 0xD9; 2242 case 0xBA: .. case 0xBB: 2243 return 0xDA; 2244 case 0xBC: 2245 return 0xDB; 2246 case 0xBD: .. case 0xBE: 2247 return 0xDC; 2248 case 0xBF: .. case 0xC0: 2249 return 0xDD; 2250 case 0xC1: .. case 0xC2: 2251 return 0xDE; 2252 case 0xC3: 2253 return 0xDF; 2254 case 0xC4: .. case 0xC5: 2255 return 0xE0; 2256 case 0xC6: .. case 0xC7: 2257 return 0xE1; 2258 case 0xC8: .. case 0xC9: 2259 return 0xE2; 2260 case 0xCA: 2261 return 0xE3; 2262 case 0xCB: .. case 0xCC: 2263 return 0xE4; 2264 case 0xCD: .. case 0xCE: 2265 return 0xE5; 2266 case 0xCF: .. case 0xD0: 2267 return 0xE6; 2268 case 0xD1: .. case 0xD2: 2269 return 0xE7; 2270 case 0xD3: 2271 return 0xE8; 2272 case 0xD4: .. case 0xD5: 2273 return 0xE9; 2274 case 0xD6: .. case 0xD7: 2275 return 0xEA; 2276 case 0xD8: .. case 0xD9: 2277 return 0xEB; 2278 case 0xDA: .. case 0xDB: 2279 return 0xEC; 2280 case 0xDC: .. case 0xDD: 2281 return 0xED; 2282 case 0xDE: .. case 0xDF: 2283 return 0xEE; 2284 case 0xE0: 2285 return 0xEF; 2286 case 0xE1: .. case 0xE2: 2287 return 0xF0; 2288 case 0xE3: .. case 0xE4: 2289 return 0xF1; 2290 case 0xE5: .. case 0xE6: 2291 return 0xF2; 2292 case 0xE7: .. case 0xE8: 2293 return 0xF3; 2294 case 0xE9: .. case 0xEA: 2295 return 0xF4; 2296 case 0xEB: .. case 0xEC: 2297 return 0xF5; 2298 case 0xED: .. case 0xEE: 2299 return 0xF6; 2300 case 0xEF: .. case 0xF0: 2301 return 0xF7; 2302 case 0xF1: .. case 0xF2: 2303 return 0xF8; 2304 case 0xF3: .. case 0xF4: 2305 return 0xF9; 2306 case 0xF5: .. case 0xF6: 2307 return 0xFA; 2308 case 0xF7: .. case 0xF8: 2309 return 0xFB; 2310 case 0xF9: .. case 0xFA: 2311 return 0xFC; 2312 case 0xFB: .. case 0xFC: 2313 return 0xFD; 2314 case 0xFD: .. case 0xFE: 2315 return 0xFE; 2316 case 0xFF: 2317 return 0xFF; 2318 } 2319 } 2320 2321 unittest { 2322 import std.math : round, sqrt; 2323 2324 foreach (n; ubyte.min .. ubyte.max + 1) { 2325 ubyte fp = (sqrt(n / 255.0f) * 255).round().castTo!ubyte; 2326 ubyte i8 = intNormalizedSqrt(n.castTo!ubyte); 2327 assert(fp == i8); 2328 } 2329 } 2330 2331 /++ 2332 Limits a value to a maximum of 0xFF (= 255). 2333 +/ 2334 ubyte clamp255(Tint)(const Tint value) @nogc { 2335 pragma(inline, true); 2336 return (value < 0xFF) ? value.castTo!ubyte : 0xFF; 2337 } 2338 2339 /++ 2340 Fast 8-bit “percentage” function 2341 2342 This function optimizes its runtime performance by substituting 2343 the division by 255 with an approximation using bitshifts. 2344 2345 Nonetheless, its result are as accurate as a floating point 2346 division with 64-bit precision. 2347 2348 Params: 2349 nPercentage = percentage as the number of 255ths (“two hundred fifty-fifths”) 2350 value = base value (“total”) 2351 2352 Returns: 2353 `round(value * nPercentage / 255.0)` 2354 +/ 2355 ubyte n255thsOf(const ubyte nPercentage, const ubyte value) @nogc { 2356 immutable factor = (nPercentage | (nPercentage << 8)); 2357 return (((value * factor) + 0x8080) >> 16); 2358 } 2359 2360 @safe unittest { 2361 // Accuracy verification 2362 2363 static ubyte n255thsOfFP64(const ubyte nPercentage, const ubyte value) { 2364 return (double(value) * double(nPercentage) / 255.0).round().castTo!ubyte(); 2365 } 2366 2367 for (int value = ubyte.min; value <= ubyte.max; ++value) { 2368 for (int percent = ubyte.min; percent <= ubyte.max; ++percent) { 2369 immutable v = cast(ubyte) value; 2370 immutable p = cast(ubyte) percent; 2371 2372 immutable approximated = n255thsOf(p, v); 2373 immutable precise = n255thsOfFP64(p, v); 2374 assert(approximated == precise); 2375 } 2376 } 2377 } 2378 2379 /// 2380 ubyte percentageDecimalToUInt8(const float decimal) @nogc 2381 in (decimal >= 0) 2382 in (decimal <= 1) { 2383 return round(decimal * 255).castTo!ubyte; 2384 } 2385 2386 /// 2387 float percentageUInt8ToDecimal(const ubyte n255ths) @nogc { 2388 return (float(n255ths) / 255.0f); 2389 } 2390 2391 // ==== Image manipulation functions ==== 2392 2393 /++ 2394 Lowers the opacity of a Pixel. 2395 2396 This function multiplies the opacity of the input 2397 with the given percentage. 2398 2399 See_Also: 2400 Use [decreaseOpacityF] with decimal opacity values in percent (%). 2401 +/ 2402 Pixel decreaseOpacity(const Pixel source, ubyte opacityPercentage) @nogc { 2403 return Pixel( 2404 source.r, 2405 source.g, 2406 source.b, 2407 opacityPercentage.n255thsOf(source.a), 2408 ); 2409 } 2410 2411 /++ 2412 Lowers the opacity of a Pixel. 2413 2414 This function multiplies the opacity of the input 2415 with the given percentage. 2416 2417 Value Range: 2418 0.0 = 0% 2419 1.0 = 100% 2420 2421 See_Also: 2422 Use [opacity] with 8-bit integer opacity values (in 255ths). 2423 +/ 2424 Pixel decreaseOpacityF(const Pixel source, float opacityPercentage) @nogc { 2425 return decreaseOpacity(source, percentageDecimalToUInt8(opacityPercentage)); 2426 } 2427 2428 // Don’t get fooled by the name of this function. 2429 // It’s called like that for consistency reasons. 2430 private void decreaseOpacityInto(const Pixmap source, Pixmap target, ubyte opacityPercentage) @trusted @nogc { 2431 debug assert(source.data.length == target.data.length); 2432 foreach (idx, ref px; target.data) { 2433 px = decreaseOpacity(source.data.ptr[idx], opacityPercentage); 2434 } 2435 } 2436 2437 /++ 2438 Lowers the opacity of a [Pixmap]. 2439 2440 This operation updates the alpha-channel value of each pixel. 2441 → `alpha *= opacity` 2442 2443 See_Also: 2444 Use [decreaseOpacityF] with decimal opacity values in percent (%). 2445 +/ 2446 Pixmap decreaseOpacity(const Pixmap source, Pixmap target, ubyte opacityPercentage) @nogc { 2447 target.adjustTo(source.decreaseOpacityCalcDims()); 2448 source.decreaseOpacityInto(target, opacityPercentage); 2449 return target; 2450 } 2451 2452 /// ditto 2453 Pixmap decreaseOpacityNew(const Pixmap source, ubyte opacityPercentage) { 2454 auto target = Pixmap.makeNew(source.decreaseOpacityCalcDims()); 2455 source.decreaseOpacityInto(target, opacityPercentage); 2456 return target; 2457 } 2458 2459 /// ditto 2460 void decreaseOpacityInPlace(Pixmap source, ubyte opacityPercentage) @nogc { 2461 foreach (ref px; source.data) { 2462 px.a = opacityPercentage.n255thsOf(px.a); 2463 } 2464 } 2465 2466 /// ditto 2467 PixmapBlueprint decreaseOpacityCalcDims(const Pixmap source) @nogc { 2468 return PixmapBlueprint.fromPixmap(source); 2469 } 2470 2471 /++ 2472 Adjusts the opacity of a [Pixmap]. 2473 2474 This operation updates the alpha-channel value of each pixel. 2475 → `alpha *= opacity` 2476 2477 See_Also: 2478 Use [decreaseOpacity] with 8-bit integer opacity values (in 255ths). 2479 +/ 2480 Pixmap decreaseOpacityF(const Pixmap source, Pixmap target, float opacityPercentage) @nogc { 2481 return source.decreaseOpacity(target, percentageDecimalToUInt8(opacityPercentage)); 2482 } 2483 2484 /// ditto 2485 Pixmap decreaseOpacityFNew(const Pixmap source, float opacityPercentage) { 2486 return source.decreaseOpacityNew(percentageDecimalToUInt8(opacityPercentage)); 2487 } 2488 2489 /// ditto 2490 void decreaseOpacityFInPlace(Pixmap source, float opacityPercentage) @nogc { 2491 return source.decreaseOpacityInPlace(percentageDecimalToUInt8(opacityPercentage)); 2492 } 2493 2494 /// ditto 2495 PixmapBlueprint decreaseOpacityF(Pixmap source) @nogc { 2496 return PixmapBlueprint.fromPixmap(source); 2497 } 2498 2499 /++ 2500 Inverts a color (to its negative color). 2501 +/ 2502 Pixel invert(const Pixel color) @nogc { 2503 return Pixel( 2504 0xFF - color.r, 2505 0xFF - color.g, 2506 0xFF - color.b, 2507 color.a, 2508 ); 2509 } 2510 2511 private void invertInto(const Pixmap source, Pixmap target) @trusted @nogc { 2512 debug assert(source.length == target.length); 2513 foreach (idx, ref px; target.data) { 2514 px = invert(source.data.ptr[idx]); 2515 } 2516 } 2517 2518 /++ 2519 Inverts all colors to produce a $(I negative image). 2520 2521 $(TIP 2522 Develops a positive image when applied to a negative one. 2523 ) 2524 +/ 2525 Pixmap invert(const Pixmap source, Pixmap target) @nogc { 2526 target.adjustTo(source.invertCalcDims()); 2527 source.invertInto(target); 2528 return target; 2529 } 2530 2531 /// ditto 2532 Pixmap invertNew(const Pixmap source) { 2533 auto target = Pixmap.makeNew(source.invertCalcDims()); 2534 source.invertInto(target); 2535 return target; 2536 } 2537 2538 /// ditto 2539 void invertInPlace(Pixmap pixmap) @nogc { 2540 foreach (ref px; pixmap.data) { 2541 px = invert(px); 2542 } 2543 } 2544 2545 /// ditto 2546 PixmapBlueprint invertCalcDims(const Pixmap source) @nogc { 2547 return PixmapBlueprint.fromPixmap(source); 2548 } 2549 2550 /++ 2551 Crops an image and stores the result in the provided target Pixmap. 2552 2553 The size of the area to crop the image to 2554 is derived from the size of the target. 2555 2556 --- 2557 // This function can be used to omit a redundant size parameter 2558 // in cases like this: 2559 target = crop(source, target, target.size, offset); 2560 2561 // → Instead do: 2562 cropTo(source, target, offset); 2563 --- 2564 +/ 2565 void cropTo(const Pixmap source, Pixmap target, Point offset = Point(0, 0)) @nogc { 2566 auto src = const(SubPixmap)(source, target.size, offset); 2567 src.extractToPixmapCopyImpl(target); 2568 } 2569 2570 // consistency 2571 private alias cropInto = cropTo; 2572 2573 /++ 2574 Crops an image to the provided size with the requested offset. 2575 2576 The target Pixmap must be big enough in length to hold the cropped image. 2577 +/ 2578 Pixmap crop(const Pixmap source, Pixmap target, Size cropToSize, Point offset = Point(0, 0)) @nogc { 2579 target.adjustTo(cropCalcDims(cropToSize)); 2580 cropInto(source, target, offset); 2581 return target; 2582 } 2583 2584 /// ditto 2585 Pixmap cropNew(const Pixmap source, Size cropToSize, Point offset = Point(0, 0)) { 2586 auto target = Pixmap.makeNew(cropToSize); 2587 cropInto(source, target, offset); 2588 return target; 2589 } 2590 2591 /// ditto 2592 Pixmap cropInPlace(Pixmap source, Size cropToSize, Point offset = Point(0, 0)) @nogc { 2593 Pixmap target = source; 2594 target.width = cropToSize.width; 2595 target.data = target.data[0 .. cropToSize.area]; 2596 2597 auto src = const(SubPixmap)(source, cropToSize, offset); 2598 src.extractToPixmapCopyPixelByPixelImpl(target); 2599 return target; 2600 } 2601 2602 /// ditto 2603 PixmapBlueprint cropCalcDims(Size cropToSize) @nogc { 2604 return PixmapBlueprint.fromSize(cropToSize); 2605 } 2606 2607 private void transposeInto(const Pixmap source, Pixmap target) @nogc { 2608 foreach (y; 0 .. target.width) { 2609 foreach (x; 0 .. source.width) { 2610 const idxSrc = linearOffset(Point(x, y), source.width); 2611 const idxDst = linearOffset(Point(y, x), target.width); 2612 2613 target.data[idxDst] = source.data[idxSrc]; 2614 } 2615 } 2616 } 2617 2618 /++ 2619 Transposes an image. 2620 2621 ``` 2622 ╔══╗ ╔══╗ 2623 ║# ║ ║#+║ 2624 ║+x║ → ║ x║ 2625 ╚══╝ ╚══╝ 2626 ``` 2627 +/ 2628 Pixmap transpose(const Pixmap source, Pixmap target) @nogc { 2629 target.adjustTo(source.transposeCalcDims()); 2630 source.transposeInto(target); 2631 return target; 2632 } 2633 2634 /// ditto 2635 Pixmap transposeNew(const Pixmap source) { 2636 auto target = Pixmap.makeNew(source.transposeCalcDims()); 2637 source.transposeInto(target); 2638 return target; 2639 } 2640 2641 /// ditto 2642 PixmapBlueprint transposeCalcDims(const Pixmap source) @nogc { 2643 return PixmapBlueprint(source.length, source.height); 2644 } 2645 2646 private void rotateClockwiseInto(const Pixmap source, Pixmap target) @nogc { 2647 const area = source.data.length; 2648 const rowLength = source.size.height; 2649 ptrdiff_t cursor = -1; 2650 2651 foreach (px; source.data) { 2652 cursor += rowLength; 2653 if (cursor > area) { 2654 cursor -= (area + 1); 2655 } 2656 2657 target.data[cursor] = px; 2658 } 2659 } 2660 2661 /++ 2662 Rotates an image by 90° clockwise. 2663 2664 ``` 2665 ╔══╗ ╔══╗ 2666 ║# ║ ║+#║ 2667 ║+x║ → ║x ║ 2668 ╚══╝ ╚══╝ 2669 ``` 2670 +/ 2671 Pixmap rotateClockwise(const Pixmap source, Pixmap target) @nogc { 2672 target.adjustTo(source.rotateClockwiseCalcDims()); 2673 source.rotateClockwiseInto(target); 2674 return target; 2675 } 2676 2677 /// ditto 2678 Pixmap rotateClockwiseNew(const Pixmap source) { 2679 auto target = Pixmap.makeNew(source.rotateClockwiseCalcDims()); 2680 source.rotateClockwiseInto(target); 2681 return target; 2682 } 2683 2684 /// ditto 2685 PixmapBlueprint rotateClockwiseCalcDims(const Pixmap source) @nogc { 2686 return PixmapBlueprint(source.length, source.height); 2687 } 2688 2689 private void rotateCounterClockwiseInto(const Pixmap source, Pixmap target) @nogc { 2690 // TODO: can this be optimized? 2691 target = transpose(source, target); 2692 target.flipVerticallyInPlace(); 2693 } 2694 2695 /++ 2696 Rotates an image by 90° counter-clockwise. 2697 2698 ``` 2699 ╔══╗ ╔══╗ 2700 ║# ║ ║ x║ 2701 ║+x║ → ║#+║ 2702 ╚══╝ ╚══╝ 2703 ``` 2704 +/ 2705 Pixmap rotateCounterClockwise(const Pixmap source, Pixmap target) @nogc { 2706 target.adjustTo(source.rotateCounterClockwiseCalcDims()); 2707 source.rotateCounterClockwiseInto(target); 2708 return target; 2709 } 2710 2711 /// ditto 2712 Pixmap rotateCounterClockwiseNew(const Pixmap source) { 2713 auto target = Pixmap.makeNew(source.rotateCounterClockwiseCalcDims()); 2714 source.rotateCounterClockwiseInto(target); 2715 return target; 2716 } 2717 2718 /// ditto 2719 PixmapBlueprint rotateCounterClockwiseCalcDims(const Pixmap source) @nogc { 2720 return PixmapBlueprint(source.length, source.height); 2721 } 2722 2723 private void rotate180degInto(const Pixmap source, Pixmap target) @nogc { 2724 // Technically, this is implemented as flip vertical + flip horizontal. 2725 auto src = PixmapScanner(source); 2726 auto dst = PixmapScannerRW(target); 2727 2728 foreach (srcLine; src) { 2729 auto dstLine = dst.back; 2730 foreach (idxSrc, px; srcLine) { 2731 const idxDst = (dstLine.length - (idxSrc + 1)); 2732 dstLine[idxDst] = px; 2733 } 2734 dst.popBack(); 2735 } 2736 } 2737 2738 /++ 2739 Rotates an image by 180°. 2740 2741 ``` 2742 ╔═══╗ ╔═══╗ 2743 ║#- ║ ║%~~║ 2744 ║~~%║ → ║ -#║ 2745 ╚═══╝ ╚═══╝ 2746 ``` 2747 +/ 2748 Pixmap rotate180deg(const Pixmap source, Pixmap target) @nogc { 2749 target.adjustTo(source.rotate180degCalcDims()); 2750 source.rotate180degInto(target); 2751 return target; 2752 } 2753 2754 /// ditto 2755 Pixmap rotate180degNew(const Pixmap source) { 2756 auto target = Pixmap.makeNew(source.size); 2757 source.rotate180degInto(target); 2758 return target; 2759 } 2760 2761 /// ditto 2762 void rotate180degInPlace(Pixmap source) @nogc { 2763 auto scanner = PixmapScannerRW(source); 2764 2765 // Technically, this is implemented as a flip vertical + flip horizontal 2766 // combo, i.e. the image is flipped vertically line by line, but the lines 2767 // are overwritten in a horizontally flipped way. 2768 while (!scanner.empty) { 2769 auto a = scanner.front; 2770 auto b = scanner.back; 2771 2772 // middle line? (odd number of lines) 2773 if (a.ptr is b.ptr) { 2774 break; 2775 } 2776 2777 foreach (idxSrc, ref pxA; a) { 2778 const idxDst = (b.length - (idxSrc + 1)); 2779 const tmp = pxA; 2780 pxA = b[idxDst]; 2781 b[idxDst] = tmp; 2782 } 2783 2784 scanner.popFront(); 2785 scanner.popBack(); 2786 } 2787 } 2788 2789 /// 2790 PixmapBlueprint rotate180degCalcDims(const Pixmap source) @nogc { 2791 return PixmapBlueprint.fromPixmap(source); 2792 } 2793 2794 private void flipHorizontallyInto(const Pixmap source, Pixmap target) @nogc { 2795 auto src = PixmapScanner(source); 2796 auto dst = PixmapScannerRW(target); 2797 2798 foreach (srcLine; src) { 2799 auto dstLine = dst.front; 2800 foreach (idxSrc, px; srcLine) { 2801 const idxDst = (dstLine.length - (idxSrc + 1)); 2802 dstLine[idxDst] = px; 2803 } 2804 2805 dst.popFront(); 2806 } 2807 } 2808 2809 /++ 2810 Flips an image horizontally. 2811 2812 ``` 2813 ╔═══╗ ╔═══╗ 2814 ║#-.║ → ║.-#║ 2815 ╚═══╝ ╚═══╝ 2816 ``` 2817 +/ 2818 Pixmap flipHorizontally(const Pixmap source, Pixmap target) @nogc { 2819 target.adjustTo(source.flipHorizontallyCalcDims()); 2820 source.flipHorizontallyInto(target); 2821 return target; 2822 } 2823 2824 /// ditto 2825 Pixmap flipHorizontallyNew(const Pixmap source) { 2826 auto target = Pixmap.makeNew(source.size); 2827 source.flipHorizontallyInto(target); 2828 return target; 2829 } 2830 2831 /// ditto 2832 void flipHorizontallyInPlace(Pixmap source) @nogc { 2833 auto scanner = PixmapScannerRW(source); 2834 2835 foreach (line; scanner) { 2836 const idxMiddle = (1 + (line.length >> 1)); 2837 auto halfA = line[0 .. idxMiddle]; 2838 2839 foreach (idxA, ref px; halfA) { 2840 const idxB = (line.length - (idxA + 1)); 2841 const tmp = line[idxB]; 2842 // swap 2843 line[idxB] = px; 2844 px = tmp; 2845 } 2846 } 2847 } 2848 2849 /// ditto 2850 PixmapBlueprint flipHorizontallyCalcDims(const Pixmap source) @nogc { 2851 return PixmapBlueprint.fromPixmap(source); 2852 } 2853 2854 private void flipVerticallyInto(const Pixmap source, Pixmap target) @nogc { 2855 auto src = PixmapScanner(source); 2856 auto dst = PixmapScannerRW(target); 2857 2858 foreach (srcLine; src) { 2859 dst.back[] = srcLine[]; 2860 dst.popBack(); 2861 } 2862 } 2863 2864 /++ 2865 Flips an image vertically. 2866 2867 ``` 2868 ╔═══╗ ╔═══╗ 2869 ║## ║ ║ -║ 2870 ║ -║ → ║## ║ 2871 ╚═══╝ ╚═══╝ 2872 ``` 2873 +/ 2874 Pixmap flipVertically(const Pixmap source, Pixmap target) @nogc { 2875 target.adjustTo(source.flipVerticallyCalcDims()); 2876 source.flipVerticallyInto(target); 2877 return target; 2878 } 2879 2880 /// ditto 2881 Pixmap flipVerticallyNew(const Pixmap source) { 2882 auto target = Pixmap.makeNew(source.flipVerticallyCalcDims()); 2883 source.flipVerticallyInto(target); 2884 return target; 2885 } 2886 2887 /// ditto 2888 void flipVerticallyInPlace(Pixmap source) @nogc { 2889 auto scanner = PixmapScannerRW(source); 2890 2891 while (!scanner.empty) { 2892 auto a = scanner.front; 2893 auto b = scanner.back; 2894 2895 // middle line? (odd number of lines) 2896 if (a.ptr is b.ptr) { 2897 break; 2898 } 2899 2900 foreach (idx, ref pxA; a) { 2901 const tmp = pxA; 2902 pxA = b[idx]; 2903 b[idx] = tmp; 2904 } 2905 2906 scanner.popFront(); 2907 scanner.popBack(); 2908 } 2909 } 2910 2911 /// ditto 2912 PixmapBlueprint flipVerticallyCalcDims(const Pixmap source) @nogc { 2913 return PixmapBlueprint.fromPixmap(source); 2914 } 2915 2916 /++ 2917 Interpolation methods to apply when scaling images 2918 2919 Each filter has its own distinctive properties. 2920 2921 $(TIP 2922 Bilinear filtering (`linear`) is general-purpose. 2923 Works well with photos. 2924 2925 For pixel graphics the retro look of `nearest` (as 2926 in $(I nearest neighbor)) is usually the option of choice. 2927 ) 2928 2929 $(NOTE 2930 When used as a parameter, it shall be understood as a hint. 2931 2932 Implementations are not required to support all enumerated options 2933 and may pick a different filter as a substitute at their own discretion. 2934 ) 2935 +/ 2936 enum ScalingFilter { 2937 /++ 2938 Nearest neighbor interpolation 2939 2940 Also known as $(B proximal interpolation) 2941 and $(B point sampling). 2942 2943 $(TIP 2944 Visual impression: “blocky”, “pixelated”, “slightly displaced” 2945 ) 2946 +/ 2947 nearest, 2948 2949 /++ 2950 Bilinear interpolation 2951 2952 (Uses arithmetic mean for downscaling.) 2953 2954 $(TIP 2955 Visual impression: “smooth”, “blurred” 2956 ) 2957 +/ 2958 bilinear, 2959 2960 /// 2961 linear = bilinear, 2962 } 2963 2964 private enum ScalingDirection { 2965 none, 2966 up, 2967 down, 2968 } 2969 2970 private static ScalingDirection scalingDirectionFromDelta(const int delta) @nogc { 2971 if (delta == 0) { 2972 return ScalingDirection.none; 2973 } else if (delta > 0) { 2974 return ScalingDirection.up; 2975 } else { 2976 return ScalingDirection.down; 2977 } 2978 } 2979 2980 private void scaleToImpl(ScalingFilter filter)(const Pixmap source, Pixmap target) @nogc { 2981 enum none = ScalingDirection.none; 2982 enum up = ScalingDirection.up; 2983 enum down = ScalingDirection.down; 2984 2985 enum udecimalHalf = UDecimal.make(0x8000_0000); 2986 enum uint udecimalHalfFD = udecimalHalf.fractionalDigits; 2987 2988 enum idxX = 0, idxY = 1; 2989 enum idxL = 0, idxR = 1; 2990 enum idxT = 0, idxB = 1; 2991 2992 const int[2] sourceMax = [ 2993 (source.width - 1), 2994 (source.height - 1), 2995 ]; 2996 2997 const UDecimal[2] ratios = [ 2998 (UDecimal(source.width) / target.width), 2999 (UDecimal(source.height) / target.height), 3000 ]; 3001 3002 const UDecimal[2] ratiosHalf = [ 3003 (ratios[idxX] >> 1), 3004 (ratios[idxY] >> 1), 3005 ]; 3006 3007 // ==== Nearest Neighbor ==== 3008 static if (filter == ScalingFilter.nearest) { 3009 3010 Point translate(const Point dstPos) { 3011 pragma(inline, true); 3012 const x = (dstPos.x * ratios[idxX]).castTo!int; 3013 const y = (dstPos.y * ratios[idxY]).castTo!int; 3014 return Point(x, y); 3015 } 3016 3017 auto dst = PixmapScannerRW(target); 3018 3019 size_t y = 0; 3020 foreach (dstLine; dst) { 3021 foreach (x, ref pxDst; dstLine) { 3022 const posDst = Point(x.castTo!int, y.castTo!int); 3023 const posSrc = translate(posDst); 3024 const pxInt = source.getPixel(posSrc); 3025 pxDst = pxInt; 3026 } 3027 ++y; 3028 } 3029 } 3030 3031 // ==== Bilinear ==== 3032 static if (filter == ScalingFilter.bilinear) { 3033 void scaleToLinearImpl(ScalingDirection directionX, ScalingDirection directionY)() { 3034 3035 alias InterPixel = ulong[4]; 3036 3037 static Pixel toPixel(const InterPixel ipx) @safe pure nothrow @nogc { 3038 pragma(inline, true); 3039 return Pixel( 3040 clamp255(ipx[0]), 3041 clamp255(ipx[1]), 3042 clamp255(ipx[2]), 3043 clamp255(ipx[3]), 3044 ); 3045 } 3046 3047 static InterPixel toInterPixel(const Pixel ipx) @safe pure nothrow @nogc { 3048 pragma(inline, true); 3049 InterPixel result = [ 3050 ipx.r, 3051 ipx.g, 3052 ipx.b, 3053 ipx.a, 3054 ]; 3055 return result; 3056 } 3057 3058 int[2] posSrcCenterToInterpolationTargets( 3059 ScalingDirection direction, 3060 )( 3061 UDecimal posSrcCenter, 3062 int sourceMax, 3063 ) { 3064 pragma(inline, true); 3065 3066 int[2] result; 3067 static if (direction == none) { 3068 const value = posSrcCenter.castTo!int; 3069 result = [ 3070 value, 3071 value, 3072 ]; 3073 } 3074 3075 static if (direction == up || direction == down) { 3076 if (posSrcCenter < udecimalHalf) { 3077 result = [ 3078 0, 3079 0, 3080 ]; 3081 } else { 3082 const floor = posSrcCenter.castTo!uint; 3083 if (posSrcCenter.fractionalDigits == udecimalHalfFD) { 3084 result = [ 3085 floor, 3086 floor, 3087 ]; 3088 } else if (posSrcCenter.fractionalDigits > udecimalHalfFD) { 3089 const upper = min((floor + 1), sourceMax); 3090 result = [ 3091 floor, 3092 upper, 3093 ]; 3094 } else { 3095 result = [ 3096 floor - 1, 3097 floor, 3098 ]; 3099 } 3100 } 3101 } 3102 3103 return result; 3104 } 3105 3106 auto dst = PixmapScannerRW(target); 3107 3108 size_t y = 0; 3109 foreach (dstLine; dst) { 3110 const posDstY = y.castTo!uint; 3111 const UDecimal posSrcCenterY = posDstY * ratios[idxY] + ratiosHalf[idxY]; 3112 3113 const int[2] posSrcY = posSrcCenterToInterpolationTargets!(directionY)( 3114 posSrcCenterY, 3115 sourceMax[idxY], 3116 ); 3117 3118 static if (directionY == down) { 3119 const nLines = 1 + posSrcY[idxB] - posSrcY[idxT]; 3120 } 3121 3122 static if (directionY == up) { 3123 const ulong[2] weightsY = () { 3124 ulong[2] result; 3125 result[0] = (udecimalHalf + posSrcY[1] - posSrcCenterY).fractionalDigits; 3126 result[1] = ulong(uint.max) + 1 - result[0]; 3127 return result; 3128 }(); 3129 } 3130 3131 foreach (const x, ref pxDst; dstLine) { 3132 const posDstX = x.castTo!uint; 3133 const int[2] posDst = [ 3134 posDstX, 3135 posDstY, 3136 ]; 3137 3138 const posSrcCenterX = posDst[idxX] * ratios[idxX] + ratiosHalf[idxX]; 3139 3140 const int[2] posSrcX = posSrcCenterToInterpolationTargets!(directionX)( 3141 posSrcCenterX, 3142 sourceMax[idxX], 3143 ); 3144 3145 static if (directionX == down) { 3146 const nSamples = 1 + posSrcX[idxR] - posSrcX[idxL]; 3147 } 3148 3149 const Point[4] posNeighs = [ 3150 Point(posSrcX[idxL], posSrcY[idxT]), 3151 Point(posSrcX[idxR], posSrcY[idxT]), 3152 Point(posSrcX[idxL], posSrcY[idxB]), 3153 Point(posSrcX[idxR], posSrcY[idxB]), 3154 ]; 3155 3156 const Color[4] pxNeighs = [ 3157 source.getPixel(posNeighs[0]), 3158 source.getPixel(posNeighs[1]), 3159 source.getPixel(posNeighs[2]), 3160 source.getPixel(posNeighs[3]), 3161 ]; 3162 3163 enum idxTL = 0, idxTR = 1, idxBL = 2, idxBR = 3; 3164 3165 // ====== Proper bilinear (up) + Avg (down) ====== 3166 static if (filter == ScalingFilter.bilinear) { 3167 auto pxInt = Pixel(0, 0, 0, 0); 3168 3169 // ======== Interpolate X ======== 3170 auto sampleX() { 3171 pragma(inline, true); 3172 3173 static if (directionY == down) { 3174 alias ForeachLineCallback = 3175 InterPixel delegate(const Point posLine) @safe pure nothrow @nogc; 3176 3177 InterPixel foreachLine(scope ForeachLineCallback apply) { 3178 pragma(inline, true); 3179 InterPixel linesSum = 0; 3180 foreach (const lineY; posSrcY[idxT] .. (1 + posSrcY[idxB])) { 3181 const posLine = Point(posSrcX[idxL], lineY); 3182 const lineValues = apply(posLine); 3183 linesSum[] += lineValues[]; 3184 } 3185 return linesSum; 3186 } 3187 } 3188 3189 // ========== None ========== 3190 static if (directionX == none) { 3191 static if (directionY == none) { 3192 return pxNeighs[idxTL]; 3193 } 3194 3195 static if (directionY == up) { 3196 return () @trusted { 3197 InterPixel[2] result = [ 3198 toInterPixel(pxNeighs[idxTL]), 3199 toInterPixel(pxNeighs[idxBL]), 3200 ]; 3201 return result; 3202 }(); 3203 } 3204 3205 static if (directionY == down) { 3206 auto ySum = foreachLine(delegate(const Point posLine) { 3207 const pxSrc = source.getPixel(posLine); 3208 return toInterPixel(pxSrc); 3209 }); 3210 ySum[] /= nLines; 3211 return ySum; 3212 } 3213 } 3214 3215 // ========== Down ========== 3216 static if (directionX == down) { 3217 static if (directionY == none) { 3218 const posSampling = posNeighs[idxTL]; 3219 const samplingOffset = source.scanTo(posSampling); 3220 const srcSamples = () @trusted { 3221 return source.data.ptr[samplingOffset .. (samplingOffset + nSamples)]; 3222 }(); 3223 3224 InterPixel xSum = [0, 0, 0, 0]; 3225 3226 foreach (const srcSample; srcSamples) { 3227 foreach (immutable ib, const c; srcSample.components) { 3228 () @trusted { xSum.ptr[ib] += c; }(); 3229 } 3230 } 3231 3232 xSum[] /= nSamples; 3233 return toPixel(xSum); 3234 } 3235 3236 static if (directionY == up) { 3237 const Point[2] posSampling = [ 3238 posNeighs[idxTL], 3239 posNeighs[idxBL], 3240 ]; 3241 3242 const int[2] samplingOffsets = [ 3243 source.scanTo(posSampling[idxT]), 3244 source.scanTo(posSampling[idxB]), 3245 ]; 3246 3247 const srcSamples2 = () @trusted { 3248 const(const(Pixel)[])[2] result = [ 3249 source.data.ptr[samplingOffsets[idxT] .. (samplingOffsets[idxT] + nSamples)], 3250 source.data.ptr[samplingOffsets[idxB] .. (samplingOffsets[idxB] + nSamples)], 3251 ]; 3252 return result; 3253 }(); 3254 3255 InterPixel[2] xSums = [[0, 0, 0, 0], [0, 0, 0, 0]]; 3256 3257 foreach (immutable idx, const srcSamples; srcSamples2) { 3258 foreach (const srcSample; srcSamples) { 3259 foreach (immutable ib, const c; srcSample.components) 3260 () @trusted { xSums.ptr[idx].ptr[ib] += c; }(); 3261 } 3262 } 3263 3264 foreach (ref xSum; xSums) { 3265 xSum[] /= nSamples; 3266 } 3267 3268 return xSums; 3269 } 3270 3271 static if (directionY == down) { 3272 auto ySum = foreachLine(delegate(const Point posLine) { 3273 const samplingOffset = source.scanTo(posLine); 3274 const srcSamples = () @trusted { 3275 return source.data.ptr[samplingOffset .. (samplingOffset + nSamples)]; 3276 }(); 3277 3278 InterPixel xSum = 0; 3279 3280 foreach (srcSample; srcSamples) { 3281 foreach (immutable ib, const c; srcSample.components) { 3282 () @trusted { xSum.ptr[ib] += c; }(); 3283 } 3284 } 3285 3286 return xSum; 3287 }); 3288 3289 ySum[] /= nSamples; 3290 ySum[] /= nLines; 3291 return ySum; 3292 } 3293 } 3294 3295 // ========== Up ========== 3296 static if (directionX == up) { 3297 3298 if (posSrcX[0] == posSrcX[1]) { 3299 static if (directionY == none) { 3300 return pxNeighs[idxTL]; 3301 } 3302 static if (directionY == up) { 3303 return () @trusted { 3304 InterPixel[2] result = [ 3305 toInterPixel(pxNeighs[idxTL]), 3306 toInterPixel(pxNeighs[idxBL]), 3307 ]; 3308 return result; 3309 }(); 3310 } 3311 static if (directionY == down) { 3312 auto ySum = foreachLine(delegate(const Point posLine) { 3313 const samplingOffset = source.scanTo(posLine); 3314 return toInterPixel( 3315 (() @trusted => source.data.ptr[samplingOffset])() 3316 ); 3317 }); 3318 ySum[] /= nLines; 3319 return ySum; 3320 } 3321 } 3322 3323 const ulong[2] weightsX = () { 3324 ulong[2] result; 3325 result[0] = (udecimalHalf + posSrcX[1] - posSrcCenterX).fractionalDigits; 3326 result[1] = ulong(uint.max) + 1 - result[0]; 3327 return result; 3328 }(); 3329 3330 static if (directionY == none) { 3331 InterPixel xSum = [0, 0, 0, 0]; 3332 3333 foreach (immutable ib, ref c; xSum) { 3334 c += ((() @trusted => pxNeighs[idxTL].components.ptr[ib])() * weightsX[0]); 3335 c += ((() @trusted => pxNeighs[idxTR].components.ptr[ib])() * weightsX[1]); 3336 } 3337 3338 foreach (ref c; xSum) { 3339 c >>= 32; 3340 } 3341 return toPixel(xSum); 3342 } 3343 3344 static if (directionY == up) { 3345 InterPixel[2] xSums = [[0, 0, 0, 0], [0, 0, 0, 0]]; 3346 3347 () @trusted { 3348 foreach (immutable ib, ref c; xSums[0]) { 3349 c += (pxNeighs[idxTL].components.ptr[ib] * weightsX[idxL]); 3350 c += (pxNeighs[idxTR].components.ptr[ib] * weightsX[idxR]); 3351 } 3352 3353 foreach (immutable ib, ref c; xSums[1]) { 3354 c += (pxNeighs[idxBL].components.ptr[ib] * weightsX[idxL]); 3355 c += (pxNeighs[idxBR].components.ptr[ib] * weightsX[idxR]); 3356 } 3357 }(); 3358 3359 foreach (ref sum; xSums) { 3360 foreach (ref c; sum) { 3361 c >>= 32; 3362 } 3363 } 3364 3365 return xSums; 3366 } 3367 3368 static if (directionY == down) { 3369 auto ySum = foreachLine(delegate(const Point posLine) { 3370 InterPixel xSum = [0, 0, 0, 0]; 3371 3372 const samplingOffset = source.scanTo(posLine); 3373 Pixel[2] pxcLR = () @trusted { 3374 Pixel[2] result = [ 3375 source.data.ptr[samplingOffset], 3376 source.data.ptr[samplingOffset + 1], 3377 ]; 3378 return result; 3379 }(); 3380 3381 foreach (immutable ib, ref c; xSum) { 3382 c += ((() @trusted => pxcLR[idxL].components.ptr[ib])() * weightsX[idxL]); 3383 c += ((() @trusted => pxcLR[idxR].components.ptr[ib])() * weightsX[idxR]); 3384 } 3385 3386 foreach (ref c; xSum) { 3387 c >>= 32; 3388 } 3389 return xSum; 3390 }); 3391 3392 ySum[] /= nLines; 3393 return ySum; 3394 } 3395 } 3396 } 3397 3398 // ======== Interpolate Y ======== 3399 static if (directionY == none) { 3400 const Pixel tmp = sampleX(); 3401 pxInt = tmp; 3402 } 3403 static if (directionY == down) { 3404 const InterPixel tmp = sampleX(); 3405 pxInt = toPixel(tmp); 3406 } 3407 static if (directionY == up) { 3408 const InterPixel[2] xSums = sampleX(); 3409 foreach (immutable ib, ref c; pxInt.components) { 3410 ulong ySum = 0; 3411 ySum += ((() @trusted => xSums[idxT].ptr[ib])() * weightsY[idxT]); 3412 ySum += ((() @trusted => xSums[idxB].ptr[ib])() * weightsY[idxB]); 3413 3414 const xySum = (ySum >> 32); 3415 c = clamp255(xySum); 3416 } 3417 } 3418 } 3419 3420 pxDst = pxInt; 3421 } 3422 3423 ++y; 3424 } 3425 } 3426 3427 const Size delta = (target.size - source.size); 3428 3429 const ScalingDirection[2] directions = [ 3430 scalingDirectionFromDelta(delta.width), 3431 scalingDirectionFromDelta(delta.height), 3432 ]; 3433 3434 if (directions[0] == none) { 3435 if (directions[1] == none) { 3436 version (none) { 3437 scaleToLinearImpl!(none, none)(); 3438 } else { 3439 target.data[] = source.data[]; 3440 } 3441 } else if (directions[1] == up) { 3442 scaleToLinearImpl!(none, up)(); 3443 } else /* if (directions[1] == down) */ { 3444 scaleToLinearImpl!(none, down)(); 3445 } 3446 } else if (directions[0] == up) { 3447 if (directions[1] == none) { 3448 scaleToLinearImpl!(up, none)(); 3449 } else if (directions[1] == up) { 3450 scaleToLinearImpl!(up, up)(); 3451 } else /* if (directions[1] == down) */ { 3452 scaleToLinearImpl!(up, down)(); 3453 } 3454 } else /* if (directions[0] == down) */ { 3455 if (directions[1] == none) { 3456 scaleToLinearImpl!(down, none)(); 3457 } else if (directions[1] == up) { 3458 scaleToLinearImpl!(down, up)(); 3459 } else /* if (directions[1] == down) */ { 3460 scaleToLinearImpl!(down, down)(); 3461 } 3462 } 3463 } 3464 } 3465 3466 /++ 3467 Scales a pixmap and stores the result in the provided target Pixmap. 3468 3469 The size to scale the image to 3470 is derived from the size of the target. 3471 3472 --- 3473 // This function can be used to omit a redundant size parameter 3474 // in cases like this: 3475 target = scale(source, target, target.size, ScalingFilter.bilinear); 3476 3477 // → Instead do: 3478 scaleTo(source, target, ScalingFilter.bilinear); 3479 --- 3480 +/ 3481 void scaleTo(const Pixmap source, Pixmap target, ScalingFilter filter) @nogc { 3482 import std.meta : NoDuplicates; 3483 import std.traits : EnumMembers; 3484 3485 // dfmt off 3486 final switch (filter) { 3487 static foreach (scalingFilter; NoDuplicates!(EnumMembers!ScalingFilter)) 3488 case scalingFilter: { 3489 scaleToImpl!scalingFilter(source, target); 3490 return; 3491 } 3492 } 3493 // dfmt on 3494 } 3495 3496 // consistency 3497 private alias scaleInto = scaleTo; 3498 3499 /++ 3500 Scales an image to a new size. 3501 3502 ``` 3503 ╔═══╗ ╔═╗ 3504 ║———║ → ║—║ 3505 ╚═══╝ ╚═╝ 3506 ``` 3507 +/ 3508 Pixmap scale(const Pixmap source, Pixmap target, Size scaleToSize, ScalingFilter filter) @nogc { 3509 target.adjustTo(scaleCalcDims(scaleToSize)); 3510 source.scaleInto(target, filter); 3511 return target; 3512 } 3513 3514 /// ditto 3515 Pixmap scaleNew(const Pixmap source, Size scaleToSize, ScalingFilter filter) { 3516 auto target = Pixmap.makeNew(scaleToSize); 3517 source.scaleInto(target, filter); 3518 return target; 3519 } 3520 3521 /// ditto 3522 PixmapBlueprint scaleCalcDims(Size scaleToSize) @nogc { 3523 return PixmapBlueprint.fromSize(scaleToSize); 3524 } 3525 3526 @safe pure nothrow @nogc: 3527 3528 // ==== Blending functions ==== 3529 3530 /++ 3531 Alpha-blending accuracy level 3532 3533 $(TIP 3534 This primarily exists for performance reasons. 3535 In my tests LLVM manages to auto-vectorize the RGB-only codepath significantly better, 3536 while the codegen for the accurate RGBA path is pretty conservative. 3537 3538 This provides an optimization opportunity for use-cases 3539 that don’t require an alpha-channel on the result. 3540 ) 3541 +/ 3542 enum BlendAccuracy { 3543 /++ 3544 Only RGB channels will have the correct result. 3545 3546 A(lpha) channel can contain any value. 3547 3548 Suitable for blending into non-transparent targets (e.g. framebuffer, canvas) 3549 where the resulting alpha-channel (opacity) value does not matter. 3550 +/ 3551 rgb = false, 3552 3553 /++ 3554 All RGBA channels will have the correct result. 3555 3556 Suitable for blending into transparent targets (e.g. images) 3557 where the resulting alpha-channel (opacity) value matters. 3558 3559 Use this mode for image manipulation. 3560 +/ 3561 rgba = true, 3562 } 3563 3564 /++ 3565 Blend modes 3566 3567 $(NOTE 3568 As blending operations are implemented as integer calculations, 3569 results may be slightly less precise than those from image manipulation 3570 programs using floating-point math. 3571 ) 3572 3573 See_Also: 3574 <https://www.w3.org/TR/compositing/#blending> 3575 +/ 3576 enum BlendMode { 3577 /// 3578 none = 0, 3579 /// 3580 replace = none, 3581 /// 3582 normal = 1, 3583 /// 3584 alpha = normal, 3585 3586 /// 3587 multiply, 3588 /// 3589 screen, 3590 3591 /// 3592 overlay, 3593 /// 3594 hardLight, 3595 /// 3596 softLight, 3597 3598 /// 3599 darken, 3600 /// 3601 lighten, 3602 3603 /// 3604 colorDodge, 3605 /// 3606 colorBurn, 3607 3608 /// 3609 difference, 3610 /// 3611 exclusion, 3612 /// 3613 subtract, 3614 /// 3615 divide, 3616 } 3617 3618 /// 3619 alias Blend = BlendMode; 3620 3621 // undocumented 3622 enum blendNormal = BlendMode.normal; 3623 3624 /// 3625 alias BlendFn = ubyte function(const ubyte background, const ubyte foreground) pure nothrow @nogc; 3626 3627 /++ 3628 Blends `source` into `target` 3629 with respect to the opacity of the source image (as stored in the alpha channel). 3630 3631 See_Also: 3632 [alphaBlendRGBA] and [alphaBlendRGB] are shorthand functions 3633 in cases where no special blending algorithm is needed. 3634 +/ 3635 template alphaBlend(BlendFn blend = null, BlendAccuracy accuracy = BlendAccuracy.rgba) { 3636 /// ditto 3637 public void alphaBlend(scope Pixel[] target, scope const Pixel[] source) @trusted 3638 in (source.length == target.length) { 3639 foreach (immutable idx, ref pxTarget; target) { 3640 alphaBlend(pxTarget, source.ptr[idx]); 3641 } 3642 } 3643 3644 /// ditto 3645 public void alphaBlend(ref Pixel pxTarget, const Pixel pxSource) @trusted { 3646 pragma(inline, true); 3647 3648 static if (accuracy == BlendAccuracy.rgba) { 3649 immutable alphaResult = clamp255(pxSource.a + n255thsOf(pxTarget.a, (0xFF - pxSource.a))); 3650 //immutable alphaResult = clamp255(pxTarget.a + n255thsOf(pxSource.a, (0xFF - pxTarget.a))); 3651 } 3652 3653 immutable alphaSource = (pxSource.a | (pxSource.a << 8)); 3654 immutable alphaTarget = (0xFFFF - alphaSource); 3655 3656 foreach (immutable ib, ref px; pxTarget.components) { 3657 static if (blend !is null) { 3658 immutable bx = blend(px, pxSource.components.ptr[ib]); 3659 } else { 3660 immutable bx = pxSource.components.ptr[ib]; 3661 } 3662 immutable d = cast(ubyte)(((px * alphaTarget) + 0x8080) >> 16); 3663 immutable s = cast(ubyte)(((bx * alphaSource) + 0x8080) >> 16); 3664 px = cast(ubyte)(d + s); 3665 } 3666 3667 static if (accuracy == BlendAccuracy.rgba) { 3668 pxTarget.a = alphaResult; 3669 } 3670 } 3671 } 3672 3673 /// ditto 3674 template alphaBlend(BlendAccuracy accuracy, BlendFn blend = null) { 3675 alias alphaBlend = alphaBlend!(blend, accuracy); 3676 } 3677 3678 /++ 3679 Blends `source` into `target` 3680 with respect to the opacity of the source image (as stored in the alpha channel). 3681 3682 This variant is $(slower than) [alphaBlendRGB], 3683 but calculates the correct alpha-channel value of the target. 3684 See [BlendAccuracy] for further explanation. 3685 +/ 3686 public void alphaBlendRGBA(scope Pixel[] target, scope const Pixel[] source) @safe { 3687 return alphaBlend!(null, BlendAccuracy.rgba)(target, source); 3688 } 3689 3690 /// ditto 3691 public void alphaBlendRGBA(ref Pixel pxTarget, const Pixel pxSource) @safe { 3692 return alphaBlend!(null, BlendAccuracy.rgba)(pxTarget, pxSource); 3693 } 3694 3695 /++ 3696 Blends `source` into `target` 3697 with respect to the opacity of the source image (as stored in the alpha channel). 3698 3699 This variant is $(B faster than) [alphaBlendRGBA], 3700 but leads to a wrong alpha-channel value in the target. 3701 Useful because of the performance advantage in cases where the resulting 3702 alpha does not matter. 3703 See [BlendAccuracy] for further explanation. 3704 +/ 3705 public void alphaBlendRGB(scope Pixel[] target, scope const Pixel[] source) @safe { 3706 return alphaBlend!(null, BlendAccuracy.rgb)(target, source); 3707 } 3708 3709 /// ditto 3710 public void alphaBlendRGB(ref Pixel pxTarget, const Pixel pxSource) @safe { 3711 return alphaBlend!(null, BlendAccuracy.rgb)(pxTarget, pxSource); 3712 } 3713 3714 /++ 3715 Blends pixel `source` into pixel `target` 3716 using the requested [BlendMode|blending mode]. 3717 +/ 3718 template blendPixel(BlendMode mode, BlendAccuracy accuracy = BlendAccuracy.rgba) { 3719 3720 static if (mode == BlendMode.replace) { 3721 /// ditto 3722 void blendPixel(ref Pixel target, const Pixel source) { 3723 target = source; 3724 } 3725 } 3726 3727 static if (mode == BlendMode.alpha) { 3728 /// ditto 3729 void blendPixel(ref Pixel target, const Pixel source) { 3730 return alphaBlend!accuracy(target, source); 3731 } 3732 } 3733 3734 static if (mode == BlendMode.multiply) { 3735 /// ditto 3736 void blendPixel(ref Pixel target, const Pixel source) { 3737 return alphaBlend!(accuracy, 3738 (a, b) => n255thsOf(a, b) 3739 )(target, source); 3740 } 3741 } 3742 3743 static if (mode == BlendMode.screen) { 3744 /// ditto 3745 void blendPixel()(ref Pixel target, const Pixel source) { 3746 return alphaBlend!(accuracy, 3747 (a, b) => castTo!ubyte(0xFF - n255thsOf((0xFF - a), (0xFF - b))) 3748 )(target, source); 3749 } 3750 } 3751 3752 static if (mode == BlendMode.darken) { 3753 /// ditto 3754 void blendPixel()(ref Pixel target, const Pixel source) { 3755 return alphaBlend!(accuracy, 3756 (a, b) => min(a, b) 3757 )(target, source); 3758 } 3759 } 3760 static if (mode == BlendMode.lighten) { 3761 /// ditto 3762 void blendPixel()(ref Pixel target, const Pixel source) { 3763 return alphaBlend!(accuracy, 3764 (a, b) => max(a, b) 3765 )(target, source); 3766 } 3767 } 3768 3769 static if (mode == BlendMode.overlay) { 3770 /// ditto 3771 void blendPixel()(ref Pixel target, const Pixel source) { 3772 return alphaBlend!(accuracy, function(const ubyte b, const ubyte f) { 3773 if (b < 0x80) { 3774 return n255thsOf((2 * b).castTo!ubyte, f); 3775 } 3776 return castTo!ubyte( 3777 0xFF - n255thsOf(castTo!ubyte(2 * (0xFF - b)), (0xFF - f)) 3778 ); 3779 })(target, source); 3780 } 3781 } 3782 3783 static if (mode == BlendMode.hardLight) { 3784 /// ditto 3785 void blendPixel()(ref Pixel target, const Pixel source) { 3786 return alphaBlend!(accuracy, function(const ubyte b, const ubyte f) { 3787 if (f < 0x80) { 3788 return n255thsOf(castTo!ubyte(2 * f), b); 3789 } 3790 return castTo!ubyte( 3791 0xFF - n255thsOf(castTo!ubyte(2 * (0xFF - f)), (0xFF - b)) 3792 ); 3793 })(target, source); 3794 } 3795 } 3796 3797 static if (mode == BlendMode.softLight) { 3798 /// ditto 3799 void blendPixel()(ref Pixel target, const Pixel source) { 3800 return alphaBlend!(accuracy, function(const ubyte b, const ubyte f) { 3801 if (f < 0x80) { 3802 // dfmt off 3803 return castTo!ubyte( 3804 b - n255thsOf( 3805 n255thsOf((0xFF - 2 * f).castTo!ubyte, b), 3806 (0xFF - b), 3807 ) 3808 ); 3809 // dfmt on 3810 } 3811 3812 // TODO: optimize if possible 3813 // dfmt off 3814 immutable ubyte d = (b < 0x40) 3815 ? castTo!ubyte((b * (0x3FC + (((16 * b - 0xBF4) * b) / 255))) / 255) 3816 : intNormalizedSqrt(b); 3817 //dfmt on 3818 3819 return castTo!ubyte( 3820 b + n255thsOf((2 * f - 0xFF).castTo!ubyte, (d - b).castTo!ubyte) 3821 ); 3822 })(target, source); 3823 } 3824 } 3825 3826 static if (mode == BlendMode.colorDodge) { 3827 /// ditto 3828 void blendPixel()(ref Pixel target, const Pixel source) { 3829 return alphaBlend!(accuracy, function(const ubyte b, const ubyte f) { 3830 if (b == 0x00) { 3831 return ubyte(0x00); 3832 } 3833 if (f == 0xFF) { 3834 return ubyte(0xFF); 3835 } 3836 return min( 3837 ubyte(0xFF), 3838 clamp255((255 * b) / (0xFF - f)) 3839 ); 3840 })(target, source); 3841 } 3842 } 3843 3844 static if (mode == BlendMode.colorBurn) { 3845 /// ditto 3846 void blendPixel()(ref Pixel target, const Pixel source) { 3847 return alphaBlend!(accuracy, function(const ubyte b, const ubyte f) { 3848 if (b == 0xFF) { 3849 return ubyte(0xFF); 3850 } 3851 if (f == 0x00) { 3852 return ubyte(0x00); 3853 } 3854 3855 immutable m = min( 3856 ubyte(0xFF), 3857 clamp255(((0xFF - b) * 255) / f) 3858 ); 3859 return castTo!ubyte(0xFF - m); 3860 })(target, source); 3861 } 3862 } 3863 3864 static if (mode == BlendMode.difference) { 3865 /// ditto 3866 void blendPixel()(ref Pixel target, const Pixel source) { 3867 return alphaBlend!(accuracy, 3868 (b, f) => (b > f) ? castTo!ubyte(b - f) : castTo!ubyte(f - b) 3869 )(target, source); 3870 } 3871 } 3872 3873 static if (mode == BlendMode.exclusion) { 3874 /// ditto 3875 void blendPixel()(ref Pixel target, const Pixel source) { 3876 return alphaBlend!(accuracy, 3877 (b, f) => castTo!ubyte(b + f - (2 * n255thsOf(f, b))) 3878 )(target, source); 3879 } 3880 } 3881 3882 static if (mode == BlendMode.subtract) { 3883 /// ditto 3884 void blendPixel()(ref Pixel target, const Pixel source) { 3885 return alphaBlend!(accuracy, 3886 (b, f) => (b > f) ? castTo!ubyte(b - f) : ubyte(0) 3887 )(target, source); 3888 } 3889 } 3890 3891 static if (mode == BlendMode.divide) { 3892 /// ditto 3893 void blendPixel()(ref Pixel target, const Pixel source) { 3894 return alphaBlend!(accuracy, 3895 (b, f) => (f == 0) ? ubyte(0xFF) : clamp255(0xFF * b / f) 3896 )(target, source); 3897 } 3898 } 3899 3900 //else { 3901 // static assert(false, "Missing `blendPixel()` implementation for `BlendMode`.`" ~ mode ~ "`."); 3902 //} 3903 } 3904 3905 /++ 3906 Blends the pixel data of `source` into `target` 3907 using the requested [BlendMode|blending mode]. 3908 3909 `source` and `target` MUST have the same length. 3910 +/ 3911 void blendPixels( 3912 BlendMode mode, 3913 BlendAccuracy accuracy, 3914 )(scope Pixel[] target, scope const Pixel[] source) @trusted 3915 in (source.length == target.length) { 3916 static if (mode == BlendMode.replace) { 3917 // explicit optimization 3918 target.ptr[0 .. target.length] = source.ptr[0 .. target.length]; 3919 } else { 3920 3921 // better error message in case it’s not implemented 3922 static if (!is(typeof(blendPixel!(mode, accuracy)))) { 3923 pragma(msg, "Hint: Missing or bad `blendPixel!(" ~ mode.stringof ~ ")`."); 3924 } 3925 3926 foreach (immutable idx, ref pxTarget; target) { 3927 blendPixel!(mode, accuracy)(pxTarget, source.ptr[idx]); 3928 } 3929 } 3930 } 3931 3932 /// ditto 3933 void blendPixels(BlendAccuracy accuracy)(scope Pixel[] target, scope const Pixel[] source, BlendMode mode) { 3934 import std.meta : NoDuplicates; 3935 import std.traits : EnumMembers; 3936 3937 final switch (mode) with (BlendMode) { 3938 static foreach (m; NoDuplicates!(EnumMembers!BlendMode)) { 3939 case m: 3940 return blendPixels!(m, accuracy)(target, source); 3941 } 3942 } 3943 } 3944 3945 /// ditto 3946 void blendPixels( 3947 scope Pixel[] target, 3948 scope const Pixel[] source, 3949 BlendMode mode, 3950 BlendAccuracy accuracy = BlendAccuracy.rgba, 3951 ) { 3952 if (accuracy == BlendAccuracy.rgb) { 3953 return blendPixels!(BlendAccuracy.rgb)(target, source, mode); 3954 } else { 3955 return blendPixels!(BlendAccuracy.rgba)(target, source, mode); 3956 } 3957 } 3958 3959 // ==== Drawing functions ==== 3960 3961 /++ 3962 Draws a single pixel 3963 +/ 3964 void drawPixel(Pixmap target, Point pos, Pixel color) { 3965 immutable size_t offset = linearOffset(target.width, pos); 3966 target.data[offset] = color; 3967 } 3968 3969 /++ 3970 Draws a rectangle 3971 +/ 3972 void drawRectangle(Pixmap target, Rectangle rectangle, Pixel color) { 3973 alias r = rectangle; 3974 3975 immutable tRect = OriginRectangle( 3976 Size(target.width, target.height), 3977 ); 3978 3979 // out of bounds? 3980 if (!tRect.intersect(r)) { 3981 return; 3982 } 3983 3984 immutable drawingTarget = Point( 3985 (r.pos.x >= 0) ? r.pos.x : 0, 3986 (r.pos.y >= 0) ? r.pos.y : 0, 3987 ); 3988 3989 immutable drawingEnd = Point( 3990 (r.right < tRect.right) ? r.right : tRect.right, 3991 (r.bottom < tRect.bottom) ? r.bottom : tRect.bottom, 3992 ); 3993 3994 immutable int drawingWidth = drawingEnd.x - drawingTarget.x; 3995 3996 foreach (y; drawingTarget.y .. drawingEnd.y) { 3997 target.sliceAt(Point(drawingTarget.x, y), drawingWidth)[] = color; 3998 } 3999 } 4000 4001 /++ 4002 Draws a line 4003 +/ 4004 void drawLine(Pixmap target, Point a, Point b, Pixel color) { 4005 import std.math : sqrt; 4006 4007 // TODO: line width 4008 // TODO: anti-aliasing (looks awful without it!) 4009 4010 float deltaX = b.x - a.x; 4011 float deltaY = b.y - a.y; 4012 int steps = sqrt(deltaX * deltaX + deltaY * deltaY).castTo!int; 4013 4014 float[2] step = [ 4015 (deltaX / steps), 4016 (deltaY / steps), 4017 ]; 4018 4019 foreach (i; 0 .. steps) { 4020 // dfmt off 4021 immutable Point p = a + Point( 4022 round(step[0] * i).castTo!int, 4023 round(step[1] * i).castTo!int, 4024 ); 4025 // dfmt on 4026 4027 immutable offset = linearOffset(p, target.width); 4028 target.data[offset] = color; 4029 } 4030 4031 immutable offsetEnd = linearOffset(b, target.width); 4032 target.data[offsetEnd] = color; 4033 } 4034 4035 /++ 4036 Draws an image (a source pixmap) on a target pixmap 4037 4038 Params: 4039 target = target pixmap to draw on 4040 image = source pixmap 4041 pos = top-left destination position (on the target pixmap) 4042 +/ 4043 void drawPixmap(Pixmap target, const Pixmap image, Point pos, Blend blend = blendNormal) { 4044 alias source = image; 4045 4046 immutable tRect = OriginRectangle( 4047 Size(target.width, target.height), 4048 ); 4049 4050 immutable sRect = Rectangle(pos, source.size); 4051 4052 // out of bounds? 4053 if (!tRect.intersect(sRect)) { 4054 return; 4055 } 4056 4057 immutable drawingTarget = Point( 4058 (pos.x >= 0) ? pos.x : 0, 4059 (pos.y >= 0) ? pos.y : 0, 4060 ); 4061 4062 immutable drawingEnd = Point( 4063 (sRect.right < tRect.right) ? sRect.right : tRect.right, 4064 (sRect.bottom < tRect.bottom) ? sRect.bottom : tRect.bottom, 4065 ); 4066 4067 immutable drawingSource = Point(drawingTarget.x, 0) - Point(sRect.pos.x, sRect.pos.y); 4068 immutable int drawingWidth = drawingEnd.x - drawingTarget.x; 4069 4070 foreach (y; drawingTarget.y .. drawingEnd.y) { 4071 blendPixels( 4072 target.sliceAt(Point(drawingTarget.x, y), drawingWidth), 4073 source.sliceAt(Point(drawingSource.x, y + drawingSource.y), drawingWidth), 4074 blend, 4075 ); 4076 } 4077 } 4078 4079 /++ 4080 Draws an image (a subimage from a source pixmap) on a target pixmap 4081 4082 Params: 4083 target = target pixmap to draw on 4084 image = source subpixmap 4085 pos = top-left destination position (on the target pixmap) 4086 +/ 4087 void drawPixmap(Pixmap target, const SubPixmap image, Point pos, Blend blend = blendNormal) { 4088 alias source = image; 4089 4090 debug assert(source.isValid); 4091 4092 immutable tRect = OriginRectangle( 4093 Size(target.width, target.height), 4094 ); 4095 4096 immutable sRect = Rectangle(pos, source.size); 4097 4098 // out of bounds? 4099 if (!tRect.intersect(sRect)) { 4100 return; 4101 } 4102 4103 Point sourceOffset = source.offset; 4104 Point drawingTarget; 4105 Size drawingSize = source.size; 4106 4107 if (pos.x <= 0) { 4108 sourceOffset.x -= pos.x; 4109 drawingTarget.x = 0; 4110 drawingSize.width += pos.x; 4111 } else { 4112 drawingTarget.x = pos.x; 4113 } 4114 4115 if (pos.y <= 0) { 4116 sourceOffset.y -= pos.y; 4117 drawingTarget.y = 0; 4118 drawingSize.height += pos.y; 4119 } else { 4120 drawingTarget.y = pos.y; 4121 } 4122 4123 Point drawingEnd = drawingTarget + drawingSize.castTo!Point(); 4124 if (drawingEnd.x >= target.width) { 4125 drawingSize.width -= (drawingEnd.x - target.width); 4126 } 4127 if (drawingEnd.y >= target.height) { 4128 drawingSize.height -= (drawingEnd.y - target.height); 4129 } 4130 4131 auto dst = SubPixmap(target, drawingTarget, drawingSize); 4132 auto src = const(SubPixmap)( 4133 source.source, 4134 drawingSize, 4135 sourceOffset, 4136 ); 4137 4138 src.xferTo(dst, blend); 4139 } 4140 4141 /++ 4142 Draws a sprite from a spritesheet 4143 +/ 4144 void drawSprite(Pixmap target, const SpriteSheet sheet, int spriteIndex, Point pos, Blend blend = blendNormal) { 4145 immutable tRect = OriginRectangle( 4146 Size(target.width, target.height), 4147 ); 4148 4149 immutable spriteOffset = sheet.getSpritePixelOffset2D(spriteIndex); 4150 immutable sRect = Rectangle(pos, sheet.spriteSize); 4151 4152 // out of bounds? 4153 if (!tRect.intersect(sRect)) { 4154 return; 4155 } 4156 4157 immutable drawingTarget = Point( 4158 (pos.x >= 0) ? pos.x : 0, 4159 (pos.y >= 0) ? pos.y : 0, 4160 ); 4161 4162 immutable drawingEnd = Point( 4163 (sRect.right < tRect.right) ? sRect.right : tRect.right, 4164 (sRect.bottom < tRect.bottom) ? sRect.bottom : tRect.bottom, 4165 ); 4166 4167 immutable drawingSource = 4168 spriteOffset 4169 + Point(drawingTarget.x, 0) 4170 - Point(sRect.pos.x, sRect.pos.y); 4171 immutable int drawingWidth = drawingEnd.x - drawingTarget.x; 4172 4173 foreach (y; drawingTarget.y .. drawingEnd.y) { 4174 blendPixels( 4175 target.sliceAt(Point(drawingTarget.x, y), drawingWidth), 4176 sheet.pixmap.sliceAt(Point(drawingSource.x, y + drawingSource.y), drawingWidth), 4177 blend, 4178 ); 4179 } 4180 }