diff --git a/color.d b/color.d index 6e5f1bf..d2a0b83 100644 --- a/color.d +++ b/color.d @@ -1906,6 +1906,23 @@ struct Point { Size opCast(T : Size)() inout @nogc { return Size(x, y); } + + /++ + Calculates the point of linear offset in a rectangle. + + `Offset = 0` is assumed to be equivalent to `Point(0,0)`. + + See_also: + [linearOffset] is the inverse function. + + History: + Added October 05, 2024. + +/ + static Point fromLinearOffset(int linearOffset, int width) @nogc { + const y = (linearOffset / width); + const x = (linearOffset % width); + return Point(x, y); + } } /// @@ -1959,6 +1976,9 @@ struct Size { Returns: `y * width + x` + See_also: + [Point.fromLinearOffset] is the inverse function. + History: Added December 19, 2023 (dub v11.4) +/ diff --git a/pixmappaint.d b/pixmappaint.d index 9440603..15acaa0 100644 --- a/pixmappaint.d +++ b/pixmappaint.d @@ -14,19 +14,315 @@ This module is $(B work in progress). API is subject to changes until further notice. ) + + Pixmap refers to raster graphics, a subset of “bitmap” graphics. + A pixmap is an array of pixels and the corresponding meta data to describe + how an image if formed from those pixels. + In the case of this library, a “width” field is used to map a specified + number of pixels to a row of an image. + + ``` + pixels := [ 0, 1, 2, 3 ] + width := 2 + + pixmap(pixels, width) + => [ + [ 0, 1 ] + [ 2, 3 ] + ] + ``` + + ``` + pixels := [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 ] + width := 3 + + pixmap(pixels, width) + => [ + [ 0, 1, 2 ] + [ 3, 4, 5 ] + [ 6, 7, 8 ] + [ 9, 10, 11 ] + ] + ``` + + ``` + pixels := [ 0, 1, 2, 3, 4, 5, 6, 7 ] + width := 4 + + pixmap(pixels, width) + => [ + [ 0, 1, 2, 3 ] + [ 4, 5, 6, 7 ] + ] + ``` + + + + + ### Colors + + Colors are stored in an RGBA format with 8 bit per channel. + See [arsd.color.Color|Pixel] for details. + + + ### The coordinate system + + The top left corner of a pixmap is its $(B origin) `(0,0)`. + + The $(horizontal axis) is called `x`. + Its corresponding length/dimension is known as `width`. + + The letter `y` is used to describe the $(B vertical axis). + Its corresponding length/dimension is known as `height`. + + ``` + 0 → x + ↓ + y + ``` + + Furthermore, $(B length) refers to the areal size of a pixmap. + It represents the total number of pixels in a pixmap. + It follows from the foregoing that the term $(I long) usually refers to + the length (not the width). + + + + + ### Pixmaps + + A [Pixmap] consist of two fields: + $(LIST + * a slice (of an array of [Pixel|Pixels]) + * a width + ) + + This design comes with many advantages. + First and foremost it brings simplicity. + + Pixel data buffers can be reused across pixmaps, + even when those have different sizes. + Simply slice the buffer to fit just enough pixels for the new pixmap. + + Memory management can also happen outside of the pixmap. + It is possible to use a buffer allocated elsewhere. (Such a one shouldn’t + be mixed with the built-in memory management facilities of the pixmap type. + Otherwise one will end up with GC-allocated copies.) + + The most important downside is that it makes pixmaps basically a partial + reference type. + + Copying a pixmap creates a shallow copy still poiting to the same pixel + data that is also used by the source pixmap. + This implies that manipulating the source pixels also manipulates the + pixels of the copy – and vice versa. + + The issues implied by this become an apparent when one of the references + modifies the pixel data in a way that also affects the dimensions of the + image; such as cropping. + + Pixmaps describe how pixel data stored in a 1-dimensional memory space is + meant to be interpreted as a 2-dimensional image. + + A notable implication of this 1D ↔ 2D mapping is, that slicing the 1D data + leads to non-sensical results in the 2D space when the 1D-slice is + reinterpreted as 2D-image. + + Especially slicing across scanlines (→ horizontal rows of an image) is + prone to such errors. + + (Slicing of the 1D array data can actually be utilized to cut off the + bottom part of an image. Any other naiv cropping operations will run into + the aforementioned issues.) + + + + + ### Image manipulation + + The term “image manipulation function” here refers to functions that + manipulate (e.g. transform) an image as a whole. + + Image manipulation functions in this library are provided in up to three + flavors: + + $(LIST + * a “source to target” function + * a “source to newly allocated target” wrapper + * $(I optionally) an “in-place” adaption + ) + + Additionally, a “compute dimensions of target” function is provided. + + + #### Source to Target + + The regular “source to target” function takes (at least) two parameters: + A source [Pixmap] and a target [Pixmap]. + + (Additional operation-specific arguments may be required as well.) + + The target pixmap usually needs to be able to fit at least the same number + of pixels as the source holds. + Use the corresponding “compute size of target function” to calculate the + required size when needed. + (A notable exception would be cropping, where to target pixmap must be only + at least long enough to hold the area of the size to crop to.) + + The data stored in the buffer of the target pixmap is overwritten by the + operation. + + A modified Pixmap structure with adjusted dimensions is returned. + + These functions are named plain and simple after the respective operation + they perform; e.g. [flipHorizontally] or [crop]. + + --- + // Allocate a new target Pixmap. + Pixmap target = Pixmap.makeNew( + flipHorizontallyCalcDims(sourceImage) + ); + + // Flip the image horizontally and store the updated structure. + // (Note: As a horizontal flip does not affect the dimensions of a Pixmap, + // storing the updated structure would not be necessary + // in this specific scenario.) + target = sourceImage.flipHorizontally(target); + --- + + --- + const cropOffset = Point(0, 0); + const cropSize = Size(100, 100); + + // Allocate a new target Pixmap. + Pixmap target = Pixmap.makeNew( + cropCalcDims(sourceImage, cropSize, cropOffset) + ); + + // Crop the Pixmap. + target = sourceImage.crop(target, cropSize, cropOffset); + --- + + $(PITFALL + “Source to target” functions do not work in place. + Do not attempt to pass Pixmaps sharing the same buffer for both source + and target. Such would lead to bad results with heavy artifacts. + + Use the “in-place” variant of the operation instead. + + Moreover: + Do not use the artifacts produced by this as a creative effect. + Those are an implementation detail (and may change at any point). + ) + + + #### Source to New Target + + The “source to newly allocated target” wrapper allocates a new buffer to + hold the manipulated target. + + These wrappers are provided for user convenience. + + They are identified by the suffix `-New` that is appended to the name of + the corresponding “source to target” function; + e.g. [flipHorizontallyNew] or [cropNew]. + + --- + // Create a new flipped Pixmap. + Pixmap target = sourceImage.flipHorizontallyNew(); + --- + + --- + const cropOffset = Point(0, 0); + const cropSize = Size(100, 100); + + // Create a new cropped Pixmap. + Pixmap target = sourceImage.cropNew(cropSize, cropOffset); + --- + + + #### In-Place + + For selected image manipulation functions a special adaption is provided + that stores the result in the source pixel data buffer. + + Depending on the operation, implementing in-place transformations can be + either straightforward or a major undertaking (and topic of research). + This library focuses and the former and leaves out cases where the latter + applies. + In particular, algorithms that require allocating further buffers to store + temporary results or auxiliary data will probably not get implemented. + + Furthermore, operations where to result is longer than the source cannot + be performed in-place. + + Certain in-place manipulation functions return a shallow-copy of the + source structure with dimensions adjusted accordingly. + This is behavior is not streamlined consistently as the lack of an + in-place option for certain operations makes them a special case anyway. + + These function are suffixed with `-InPlace`; + e.g. [flipHorizontallyInPlace] or [cropInPlace]. + + $(TIP + Manipulating the source image directly can lead to unexpected results + when the source image is used in multiple places. + ) + + $(NOTE + Users are usually better off to utilize the regular “source to target” + functions with a reused pixel data buffer. + + These functions do not serve as a performance optimization. + Some of them might perform significantly worse than their regular + variant. Always benchmark and profile. + ) + + --- + image.flipHorizontallyInPlace(); + --- + + --- + const cropOffset = Point(0, 0); + const cropSize = Size(100, 100); + + image = image.cropInPlace(cropSize, cropOffset); + --- + + + #### Compute size of target + + Functions to “compute (the) dimensions of (a) target” are primarily meant + to be utilized to calculate the size for allocating new pixmaps to be used + as a target for manipulation functions. + + They are provided for all manipulation functions even in cases where they + are provide little to no benefit. This is for consistency and to ease + development. + + Such functions are identified by a `-CalcDims` suffix; + e.g. [flipHorizontallyCalcDims] or [cropCalcDims]. + + They usually take the same parameters as their corresponding + “source to new target” function. This does not apply in cases where + certain parameters are irrelevant for the computation of the target size. +/ module arsd.pixmappaint; import arsd.color; import arsd.core; -private float hackyRound(float f) { +private float roundImpl(float f) { import std.math : round; + return round(f); } -float round(float f) pure @nogc nothrow @trusted { - return (cast(float function(float) pure @nogc nothrow) &hackyRound)(f); +// `pure` rounding function. +// std.math.round() isn’t pure on all targets. +// → +private float round(float f) pure @nogc nothrow @trusted { + return (castTo!(float function(float) pure @nogc nothrow)(&roundImpl))(f); } /* @@ -34,7 +330,6 @@ float round(float f) pure @nogc nothrow @trusted { - Refactoring the template-mess of blendPixel() & co. - Scaling - - Cropping - Rotating - Skewing - HSL @@ -69,9 +364,8 @@ static assert(Pixel.sizeof == uint.sizeof); } /// - Pixel rgba(ubyte r, ubyte g, ubyte b, float aPct) - in (aPct >= 0 && aPct <= 1) { - return Pixel(r, g, b, castTo!ubyte(aPct * 255)); + Pixel rgba(ubyte r, ubyte g, ubyte b, float aPct) { + return Pixel(r, g, b, percentageDecimalToUInt8(aPct)); } /// @@ -80,6 +374,63 @@ static assert(Pixel.sizeof == uint.sizeof); } } +/++ + $(I Advanced functionality.) + + Meta data for the construction of a Pixmap. + +/ +struct PixmapBlueprint { + /++ + Total number of pixels stored in a Pixmap. + +/ + size_t length; + + /++ + Width of a Pixmap. + +/ + int width; + +@safe pure nothrow @nogc: + + /// + public static PixmapBlueprint fromSize(const Size size) { + return PixmapBlueprint( + size.area, + size.width, + ); + } + + /// + public static PixmapBlueprint fromPixmap(const Pixmap pixmap) { + return PixmapBlueprint( + pixmap.length, + pixmap.width, + ); + } + + /++ + Determines whether the blueprint is plausible. + +/ + bool isValid() const { + return ((length % width) == 0); + } + + /++ + Height of a Pixmap. + + See_also: + This is the counterpart to the dimension known as [width]. + +/ + int height() const { + return castTo!int(length / width); + } + + /// + Size size() const { + return Size(width, height); + } +} + /++ Pixel data container +/ @@ -94,11 +445,13 @@ struct Pixmap { @safe pure nothrow: /// + deprecated("Use `Pixmap.makeNew(size)` instead.") this(Size size) { this.size = size; } /// + deprecated("Use `Pixmap.makeNew(Size(width, height))` instead.") this(int width, int height) in (width > 0) in (height > 0) { @@ -106,20 +459,71 @@ struct Pixmap { } /// - this(Pixel[] data, int width) @nogc + this(inout(Pixel)[] data, int width) inout @nogc in (data.length % width == 0) { this.data = data; this.width = width; } + /// + static Pixmap makeNew(PixmapBlueprint blueprint) { + auto data = new Pixel[](blueprint.length); + return Pixmap(data, blueprint.width); + } + + /// + static Pixmap makeNew(Size size) { + return Pixmap.makeNew(PixmapBlueprint.fromSize(size)); + } + /++ - Creates a $(I deep clone) of the Pixmap + Creates a $(I deep copy) of the Pixmap +/ Pixmap clone() const { - auto c = Pixmap(); - c.width = this.width; - c.data = this.data.dup; - return c; + return Pixmap( + this.data.dup, + this.width, + ); + } + + /++ + Copies the pixel data to the target Pixmap. + + Returns: + A size-adjusted shallow copy of the input Pixmap overwritten + with the image data of the SubPixmap. + + $(PITFALL + While the returned Pixmap utilizes the buffer provided by the input, + the returned Pixmap might not exactly match the input. + + Always use the returned Pixmap structure. + + --- + // Same buffer, but new structure: + auto pixmap2 = source.copyTo(pixmap); + + // Alternatively, replace the old structure: + pixmap = source.copyTo(pixmap); + --- + ) + +/ + Pixmap copyTo(Pixmap target) @nogc const { + // Length adjustment + const l = this.length; + if (target.data.length < l) { + assert(false, "The target Pixmap is too small."); + } else if (target.data.length > l) { + target.data = target.data[0 .. l]; + } + + copyToImpl(target); + + return target; + } + + private void copyToImpl(Pixmap target) @nogc const { + target.data[] = this.data[]; } // undocumented: really shouldn’t be used. @@ -197,24 +601,848 @@ struct Pixmap { return (width * int(Pixel.sizeof)); } + /++ + Adjusts the Pixmap according to the provided blueprint. + + The blueprint must not be larger than the data buffer of the pixmap. + + This function does not reallocate the pixel data buffer. + + If the blueprint is larger than the data buffer of the pixmap, + this will result in a bounds-check error if applicable. + +/ + void adjustTo(PixmapBlueprint blueprint) { + debug assert(this.data.length >= blueprint.length); + debug assert(blueprint.isValid); + this.data = this.data[0 .. blueprint.length]; + this.width = blueprint.width; + } + + /++ + Calculates the index (linear offset) of the requested position + within the pixmap data. + +/ + int scanTo(Point pos) inout { + return linearOffset(width, pos); + } + + /++ + Accesses the pixel at the requested position within the pixmap data. + +/ + ref inout(Pixel) scan(Point pos) inout { + return data[scanTo(pos)]; + } + /++ Retrieves a linear slice of the pixmap. Returns: `n` pixels starting at the top-left position `pos`. +/ - inout(Pixel)[] sliceAt(Point pos, int n) inout { + inout(Pixel)[] scan(Point pos, int n) inout { immutable size_t offset = linearOffset(width, pos); immutable size_t end = (offset + n); return data[offset .. end]; } + /// ditto + inout(Pixel)[] sliceAt(Point pos, int n) inout { + return scan(pos, n); + } + + /++ + Retrieves a rectangular subimage of the pixmap. + +/ + inout(SubPixmap) scanArea(Point pos, Size size) inout { + return inout(SubPixmap)(this, size, pos); + } + + /// TODO: remove + deprecated alias scanSubPixmap = scanArea; + + /// TODO: remove + deprecated alias scan2D = scanArea; + + /++ + Retrieves the first line of the Pixmap. + + See_also: + Check out [PixmapScanner] for more useful scanning functionality. + +/ + inout(Pixel)[] scanLine() inout { + return data[0 .. width]; + } + + public { + /++ + Provides access to a single pixel at the requested 2D-position. + + See_also: + Accessing pixels through the [data] array will be more useful, + usually. + +/ + ref inout(Pixel) accessPixel(Point pos) inout @system { + const idx = linearOffset(pos, this.width); + return this.data[idx]; + } + + /// ditto + Pixel getPixel(Point pos) const { + const idx = linearOffset(pos, this.width); + return this.data[idx]; + } + + /// ditto + Pixel getPixel(int x, int y) const { + return this.getPixel(Point(x, y)); + } + + /// ditto + void setPixel(Point pos, Pixel value) { + const idx = linearOffset(pos, this.width); + this.data[idx] = value; + } + + /// ditto + void setPixel(int x, int y, Pixel value) { + return this.setPixel(Point(x, y), value); + } + } + /// Clears the buffer’s contents (by setting each pixel to the same color) void clear(Pixel value) { data[] = value; } } +/++ + A subpixmap represents a subimage of a [Pixmap]. + + This wrapper provides convenient access to a rectangular slice of a Pixmap. + + ``` + ╔═════════════╗ + ║ Pixmap ║ + ║ ║ + ║ ┌───┐ ║ + ║ │Sub│ ║ + ║ └───┘ ║ + ╚═════════════╝ + ``` + +/ +struct SubPixmap { + + /++ + Source image referenced by the subimage + +/ + Pixmap source; + + /++ + Size of the subimage + +/ + Size size; + + /++ + 2D offset of the subimage + +/ + Point offset; + + public @safe pure nothrow @nogc { + /// + this(inout Pixmap source, Size size = Size(0, 0), Point offset = Point(0, 0)) inout { + this.source = source; + this.size = size; + this.offset = offset; + } + + /// + this(inout Pixmap source, Point offset, Size size = Size(0, 0)) inout { + this(source, size, offset); + } + } + +@safe pure nothrow: + + public { + /++ + Allocates a new Pixmap cropped to the pixel data of the subimage. + + See_also: + Use [extractToPixmap] for a non-allocating variant with an . + +/ + Pixmap extractToNewPixmap() const { + auto pm = Pixmap.makeNew(size); + this.extractToPixmap(pm); + return pm; + } + + /++ + Copies the pixel data – cropped to the subimage region – + into the target Pixmap. + + $(PITFALL + Do not attempt to extract a subimage back into the source pixmap. + This will fail in cases where source and target regions overlap + and potentially crash the program. + ) + + Returns: + A size-adjusted shallow copy of the input Pixmap overwritten + with the image data of the SubPixmap. + + $(PITFALL + While the returned Pixmap utilizes the buffer provided by the input, + the returned Pixmap might not exactly match the input. + The dimensions (width and height) and the length might have changed. + + Always use the returned Pixmap structure. + + --- + // Same buffer, but new structure: + auto pixmap2 = subPixmap.extractToPixmap(pixmap); + + // Alternatively, replace the old structure: + pixmap = subPixmap.extractToPixmap(pixmap); + --- + ) + +/ + Pixmap extractToPixmap(Pixmap target) @nogc const { + // Length adjustment + const l = this.length; + if (target.data.length < l) { + assert(false, "The target Pixmap is too small."); + } else if (target.data.length > l) { + target.data = target.data[0 .. l]; + } + + target.width = this.width; + + extractToPixmapCopyImpl(target); + return target; + } + + private void extractToPixmapCopyImpl(Pixmap target) @nogc const { + auto src = SubPixmapScanner(this); + auto dst = PixmapScannerRW(target); + + foreach (dstLine; dst) { + dstLine[] = src.front[]; + src.popFront(); + } + } + + private void extractToPixmapCopyPixelByPixelImpl(Pixmap target) @nogc const { + auto src = SubPixmapScanner(this); + auto dst = PixmapScannerRW(target); + + foreach (dstLine; dst) { + const srcLine = src.front; + foreach (idx, ref px; dstLine) { + px = srcLine[idx]; + } + src.popFront(); + } + } + } + +@safe pure nothrow @nogc: + + public { + /++ + Width of the subimage. + +/ + int width() const { + return size.width; + } + + /// ditto + void width(int value) { + size.width = value; + } + + /++ + Height of the subimage. + +/ + int height() const { + return size.height; + } + + /// ditto + void height(int value) { + size.height = value; + } + + /++ + Number of pixels in the subimage. + +/ + int length() const { + return size.area; + } + } + + public { + /++ + Linear offset of the subimage within the source image. + + Calculates the index of the “first pixel of the subimage” + in the “pixel data of the source image”. + +/ + int sourceOffsetLinear() const { + return linearOffset(offset, source.width); + } + + /// ditto + void sourceOffsetLinear(int value) { + this.offset = Point.fromLinearOffset(value, source.width); + } + + /++ + $(I Advanced functionality.) + + Offset of the pixel following the bottom right corner of the subimage. + + (`Point(O, 0)` is the top left corner of the source image.) + +/ + Point sourceOffsetEnd() const { + auto vec = Point(size.width, (size.height - 1)); + return (offset + vec); + } + + /++ + Linear offset of the subimage within the source image. + + Calculates the index of the “first pixel of the subimage” + in the “pixel data of the source image”. + +/ + int sourceOffsetLinearEnd() const { + return linearOffset(sourceOffsetEnd, source.width); + } + } + + /++ + Determines whether the area of the subimage + lies within the source image + and does not overflow its lines. + + $(TIP + If the offset and/or size of a subimage are off, two issues can occur: + + $(LIST + * The resulting subimage will look displaced. + (As if the lines were shifted.) + This indicates that one scanline of the subimage spans over + two ore more lines of the source image. + (Happens when `(subimage.offset.x + subimage.size.width) > source.size.width`.) + * When accessing the pixel data, bounds checks will fail. + This suggests that the area of the subimage extends beyond + the bottom end (and optionally also beyond the right end) of + the source. + ) + + Both defects could indicate an invalid subimage. + Use this function to verify the SubPixmap. + ) + + $(WARNING + Do not use invalid SubPixmaps. + The library assumes that the SubPixmaps it receives are always valid. + + Non-valid SubPixmaps are not meant to be used for creative effects + or similar either. Such uses might lead to unexpected quirks or + crashes eventually. + ) + +/ + bool isValid() const { + return ( + (sourceMarginLeft >= 0) + && (sourceMarginTop >= 0) + && (sourceMarginBottom >= 0) + && (sourceMarginRight >= 0) + ); + } + + public inout { + /++ + Retrieves the pixel at the requested position of the subimage. + +/ + ref inout(Pixel) scan(Point pos) { + return source.scan(offset + pos); + } + + /++ + Retrieves the first line of the subimage. + +/ + inout(Pixel)[] scanLine() { + const lo = linearOffset(offset, size.width); + return source.data[lo .. size.width]; + } + } + + /++ + Copies the pixels of this subimage to a target image. + + The target MUST have the same size. + + See_also: + Usually you’ll want to use [extractToPixmap] or [drawPixmap] instead. + +/ + public void xferTo(SubPixmap target) const { + debug assert(target.size == this.size); + + auto src = SubPixmapScanner(this); + auto dst = SubPixmapScannerRW(target); + + foreach (dstLine; dst) { + dstLine[] = src.front[]; + src.popFront(); + } + } + + /++ + Blends the pixels of this subimage into a target image. + + The target MUST have the same size. + + See_also: + Usually you’ll want to use [extractToPixmap] or [drawPixmap] instead. + +/ + public void xferTo(SubPixmap target, Blend blend) const { + debug assert(target.size == this.size); + + auto src = SubPixmapScanner(this); + auto dst = SubPixmapScannerRW(target); + + foreach (dstLine; dst) { + blendPixels(dstLine, src.front, blend); + src.popFront(); + } + } + + // opposite offset + public const { + /++ + $(I Advanced functionality.) + + Offset of the bottom right corner of the source image + to the bottom right corner of the subimage. + + ``` + ╔═══════════╗ + ║ ║ + ║ ┌───┐ ║ + ║ │ │ ║ + ║ └───┘ ║ + ║ ↘ ║ + ╚═══════════╝ + ``` + +/ + Point oppositeOffset() { + return Point(oppositeOffsetX, oppositeOffsetY); + } + + /++ + $(I Advanced functionality.) + + Offset of the right edge of the source image + to the right edge of the subimage. + + ``` + ╔═══════════╗ + ║ ║ + ║ ┌───┐ ║ + ║ │ S │ → ║ + ║ └───┘ ║ + ║ ║ + ╚═══════════╝ + ``` + +/ + int oppositeOffsetX() { + return (offset.x + size.width); + } + + /++ + $(I Advanced functionality.) + + Offset of the bottom edge of the source image + to the bottom edge of the subimage. + + ``` + ╔═══════════╗ + ║ ║ + ║ ┌───┐ ║ + ║ │ S │ ║ + ║ └───┘ ║ + ║ ↓ ║ + ╚═══════════╝ + ``` + +/ + int oppositeOffsetY() { + return (offset.y + size.height); + } + + } + + // source-image margins + public const { + /++ + $(I Advanced functionality.) + + X-axis margin (left + right) of the subimage within the source image. + + ``` + ╔═══════════╗ + ║ ║ + ║ ┌───┐ ║ + ║ ↔ │ S │ ↔ ║ + ║ └───┘ ║ + ║ ║ + ╚═══════════╝ + ``` + +/ + int sourceMarginX() { + return (source.width - size.width); + } + + /++ + $(I Advanced functionality.) + + Y-axis margin (top + bottom) of the subimage within the source image. + + ``` + ╔═══════════╗ + ║ ↕ ║ + ║ ┌───┐ ║ + ║ │ S │ ║ + ║ └───┘ ║ + ║ ↕ ║ + ╚═══════════╝ + ``` + +/ + int sourceMarginY() { + return (source.height - size.height); + } + + /++ + $(I Advanced functionality.) + + Top margin of the subimage within the source image. + + ``` + ╔═══════════╗ + ║ ↕ ║ + ║ ┌───┐ ║ + ║ │ S │ ║ + ║ └───┘ ║ + ║ ║ + ╚═══════════╝ + ``` + +/ + int sourceMarginTop() { + return offset.y; + } + + /++ + $(I Advanced functionality.) + + Right margin of the subimage within the source image. + + ``` + ╔═══════════╗ + ║ ║ + ║ ┌───┐ ║ + ║ │ S │ ↔ ║ + ║ └───┘ ║ + ║ ║ + ╚═══════════╝ + ``` + +/ + int sourceMarginRight() { + return (sourceMarginX - sourceMarginLeft); + } + + /++ + $(I Advanced functionality.) + + Bottom margin of the subimage within the source image. + + ``` + ╔═══════════╗ + ║ ║ + ║ ┌───┐ ║ + ║ │ S │ ║ + ║ └───┘ ║ + ║ ↕ ║ + ╚═══════════╝ + ``` + +/ + int sourceMarginBottom() { + return (sourceMarginY - sourceMarginTop); + } + + /++ + $(I Advanced functionality.) + + Left margin of the subimage within the source image. + + ``` + ╔═══════════╗ + ║ ║ + ║ ┌───┐ ║ + ║ ↔ │ S │ ║ + ║ └───┘ ║ + ║ ║ + ╚═══════════╝ + ``` + +/ + int sourceMarginLeft() { + return offset.x; + } + } + + public const { + /++ + $(I Advanced functionality.) + + Calculates the linear offset of the provided point in the subimage + relative to the source image. + +/ + int sourceOffsetOf(Point pos) { + pos = (pos + offset); + return linearOffset(pos, source.width); + } + } +} + +/++ + $(I Advanced functionality.) + + Wrapper for scanning a [Pixmap] line by line. + +/ +struct PixmapScanner { + private { + const(Pixel)[] _data; + int _width; + } + +@safe pure nothrow @nogc: + + /// + public this(const(Pixmap) pixmap) { + _data = pixmap.data; + _width = pixmap.width; + } + + /// + typeof(this) save() { + return this; + } + + /// + bool empty() const { + return (_data.length == 0); + } + + /// + const(Pixel)[] front() const { + return _data[0 .. _width]; + } + + /// + void popFront() { + _data = _data[_width .. $]; + } + + /// + const(Pixel)[] back() const { + return _data[($ - _width) .. $]; + } + + /// + void popBack() { + _data = _data[0 .. ($ - _width)]; + } +} + +/++ + $(I Advanced functionality.) + + Wrapper for scanning a [Pixmap] line by line. + + See_also: + Unlike [PixmapScanner], this does not work with `const(Pixmap)`. + +/ +struct PixmapScannerRW { + private { + Pixel[] _data; + int _width; + } + +@safe pure nothrow @nogc: + + /// + public this(Pixmap pixmap) { + _data = pixmap.data; + _width = pixmap.width; + } + + /// + typeof(this) save() { + return this; + } + + /// + bool empty() const { + return (_data.length == 0); + } + + /// + Pixel[] front() { + return _data[0 .. _width]; + } + + /// + void popFront() { + _data = _data[_width .. $]; + } + + /// + Pixel[] back() { + return _data[($ - _width) .. $]; + } + + /// + void popBack() { + _data = _data[0 .. ($ - _width)]; + } +} + +/++ + $(I Advanced functionality.) + + Wrapper for scanning a [Pixmap] line by line. + +/ +struct SubPixmapScanner { + private { + const(Pixel)[] _data; + int _width; + int _feed; + } + +@safe pure nothrow @nogc: + + /// + public this(const(SubPixmap) subPixmap) { + _data = subPixmap.source.data[subPixmap.sourceOffsetLinear .. subPixmap.sourceOffsetLinearEnd]; + _width = subPixmap.size.width; + _feed = subPixmap.source.width; + } + + /// + typeof(this) save() { + return this; + } + + /// + bool empty() const { + return (_data.length == 0); + } + + /// + const(Pixel)[] front() const { + return _data[0 .. _width]; + } + + /// + void popFront() { + if (_data.length < _feed) { + _data.length = 0; + return; + } + + _data = _data[_feed .. $]; + } + + /// + const(Pixel)[] back() const { + return _data[($ - _width) .. $]; + } + + /// + void popBack() { + if (_data.length < _feed) { + _data.length = 0; + return; + } + + _data = _data[0 .. ($ - _feed)]; + } +} + +/++ + $(I Advanced functionality.) + + Wrapper for scanning a [Pixmap] line by line. + + See_also: + Unlike [SubPixmapScanner], this does not work with `const(SubPixmap)`. + +/ +struct SubPixmapScannerRW { + private { + Pixel[] _data; + int _width; + int _feed; + } + +@safe pure nothrow @nogc: + + /// + public this(SubPixmap subPixmap) { + _data = subPixmap.source.data[subPixmap.sourceOffsetLinear .. subPixmap.sourceOffsetLinearEnd]; + _width = subPixmap.size.width; + _feed = subPixmap.source.width; + } + + /// + typeof(this) save() { + return this; + } + + /// + bool empty() const { + return (_data.length == 0); + } + + /// + Pixel[] front() { + return _data[0 .. _width]; + } + + /// + void popFront() { + if (_data.length < _feed) { + _data.length = 0; + return; + } + + _data = _data[_feed .. $]; + } + + /// + Pixel[] back() { + return _data[($ - _width) .. $]; + } + + /// + void popBack() { + if (_data.length < _feed) { + _data.length = 0; + return; + } + + _data = _data[0 .. ($ - _feed)]; + } +} + /// struct SpriteSheet { private { @@ -291,10 +1519,10 @@ private struct OriginRectangle { } } -@safe pure nothrow @nogc: +@safe pure nothrow: // misc -private { +private @nogc { Point pos(Rectangle r) => r.upperLeft; T max(T)(T a, T b) => (a >= b) ? a : b; @@ -376,7 +1604,7 @@ unittest { Returns: sqrt(value / 255f) * 255 +/ -ubyte intNormalizedSqrt(const ubyte value) { +ubyte intNormalizedSqrt(const ubyte value) @nogc { switch (value) { default: // unreachable @@ -781,7 +2009,7 @@ unittest { /++ Limits a value to a maximum of 0xFF (= 255). +/ -ubyte clamp255(Tint)(const Tint value) { +ubyte clamp255(Tint)(const Tint value) @nogc { pragma(inline, true); return (value < 0xFF) ? value.castTo!ubyte : 0xFF; } @@ -802,7 +2030,7 @@ ubyte clamp255(Tint)(const Tint value) { Returns: `round(value * nPercentage / 255.0)` +/ -ubyte n255thsOf(const ubyte nPercentage, const ubyte value) { +ubyte n255thsOf(const ubyte nPercentage, const ubyte value) @nogc { immutable factor = (nPercentage | (nPercentage << 8)); return (((value * factor) + 0x8080) >> 16); } @@ -826,41 +2054,130 @@ ubyte n255thsOf(const ubyte nPercentage, const ubyte value) { } } -/++ - Sets the opacity of a [Pixmap]. +/// +ubyte percentageDecimalToUInt8(const float decimal) @nogc +in (decimal >= 0) +in (decimal <= 1) { + return round(decimal * 255).castTo!ubyte; +} - This lossy operation updates the alpha-channel value of each pixel. - → `alpha *= opacity` +/// +float percentageUInt8ToDecimal(const ubyte n255ths) @nogc { + return (float(n255ths) / 255.0f); +} + +// ==== Image manipulation functions ==== + +/++ + Lowers the opacity of a Pixel. + + This function multiplies the opacity of the input + with the given percentage. See_Also: - Use [opacityF] with opacity values in percent (%). + Use [decreaseOpacityF] with decimal opacity values in percent (%). +/ -void opacity(Pixmap pixmap, const ubyte opacity) { - foreach (ref px; pixmap.data) { - px.a = opacity.n255thsOf(px.a); - } +Pixel decreaseOpacity(const Pixel source, ubyte opacityPercentage) @nogc { + return Pixel( + source.r, + source.g, + source.b, + opacityPercentage.n255thsOf(source.a), + ); } /++ - Sets the opacity of a [Pixmap]. + Lowers the opacity of a Pixel. - This lossy operation updates the alpha-channel value of each pixel. - → `alpha *= opacity` + This function multiplies the opacity of the input + with the given percentage. + + Value Range: + 0.0 = 0% + 1.0 = 100% See_Also: Use [opacity] with 8-bit integer opacity values (in 255ths). +/ -void opacityF(Pixmap pixmap, const float opacity) -in (opacity >= 0) -in (opacity <= 1.0) { - immutable opacity255 = round(opacity * 255).castTo!ubyte; - pixmap.opacity = opacity255; +Pixel decreaseOpacityF(const Pixel source, float opacityPercentage) @nogc { + return decreaseOpacity(source, percentageDecimalToUInt8(opacityPercentage)); +} + +// Don’t get fooled by the name of this function. +// It’s called like that for consistency reasons. +private void decreaseOpacityInto(const Pixmap source, Pixmap target, ubyte opacityPercentage) @trusted @nogc { + debug assert(source.data.length == target.data.length); + foreach (idx, ref px; target.data) { + px = decreaseOpacity(source.data.ptr[idx], opacityPercentage); + } +} + +/++ + Lowers the opacity of a [Pixmap]. + + This operation updates the alpha-channel value of each pixel. + → `alpha *= opacity` + + See_Also: + Use [decreaseOpacityF] with decimal opacity values in percent (%). + +/ +Pixmap decreaseOpacity(const Pixmap source, Pixmap target, ubyte opacityPercentage) @nogc { + target.adjustTo(source.decreaseOpacityCalcDims()); + source.decreaseOpacityInto(target, opacityPercentage); + return target; +} + +/// ditto +Pixmap decreaseOpacityNew(const Pixmap source, ubyte opacityPercentage) { + auto target = Pixmap.makeNew(source.decreaseOpacityCalcDims()); + source.decreaseOpacityInto(target, opacityPercentage); + return target; +} + +/// ditto +void decreaseOpacityInPlace(Pixmap source, ubyte opacityPercentage) @nogc { + foreach (ref px; source.data) { + px.a = opacityPercentage.n255thsOf(px.a); + } +} + +/// ditto +PixmapBlueprint decreaseOpacityCalcDims(const Pixmap source) @nogc { + return PixmapBlueprint.fromPixmap(source); +} + +/++ + Adjusts the opacity of a [Pixmap]. + + This operation updates the alpha-channel value of each pixel. + → `alpha *= opacity` + + See_Also: + Use [decreaseOpacity] with 8-bit integer opacity values (in 255ths). + +/ +Pixmap decreaseOpacityF(const Pixmap source, Pixmap target, float opacityPercentage) @nogc { + return source.decreaseOpacity(target, percentageDecimalToUInt8(opacityPercentage)); +} + +/// ditto +Pixmap decreaseOpacityFNew(const Pixmap source, float opacityPercentage) { + return source.decreaseOpacityNew(percentageDecimalToUInt8(opacityPercentage)); +} + +/// ditto +void decreaseOpacityFInPlace(Pixmap source, float opacityPercentage) @nogc { + return source.decreaseOpacityInPlace(percentageDecimalToUInt8(opacityPercentage)); +} + +/// ditto +PixmapBlueprint decreaseOpacityF(Pixmap source) @nogc { + return PixmapBlueprint.fromPixmap(source); } /++ Inverts a color (to its negative color). +/ -Pixel invert(const Pixel color) { +Pixel invert(const Pixel color) @nogc { return Pixel( 0xFF - color.r, 0xFF - color.g, @@ -869,19 +2186,413 @@ Pixel invert(const Pixel color) { ); } +private void invertInto(const Pixmap source, Pixmap target) @trusted @nogc { + debug assert(source.length == target.length); + foreach (idx, ref px; target.data) { + px = invert(source.data.ptr[idx]); + } +} + /++ - Inverts all colors to produce a $(B negative image). + Inverts all colors to produce a $(I negative image). $(TIP Develops a positive image when applied to a negative one. ) +/ -void invert(Pixmap pixmap) { +Pixmap invert(const Pixmap source, Pixmap target) @nogc { + target.adjustTo(source.invertCalcDims()); + source.invertInto(target); + return target; +} + +/// ditto +Pixmap invertNew(const Pixmap source) { + auto target = Pixmap.makeNew(source.invertCalcDims()); + source.invertInto(target); + return target; +} + +/// ditto +void invertInPlace(Pixmap pixmap) @nogc { foreach (ref px; pixmap.data) { px = invert(px); } } +/// ditto +PixmapBlueprint invertCalcDims(const Pixmap source) @nogc { + return PixmapBlueprint.fromPixmap(source); +} + +/++ + Crops an image and stores the result in the provided target Pixmap. + + The size of the area to crop the image to + is derived from the size of the target. + + --- + // This function can be used to omit a redundant size parameter + // in cases like this: + target = crop(source, target, target.size, offset); + + // → Instead do: + cropTo(source, target, offset); + --- + +/ +void cropTo(const Pixmap source, Pixmap target, Point offset = Point(0, 0)) @nogc { + auto src = const(SubPixmap)(source, target.size, offset); + src.extractToPixmapCopyImpl(target); +} + +// consistency +private alias cropInto = cropTo; + +/++ + Crops an image to the provided size with the requested offset. + + The target Pixmap must be big enough in length to hold the cropped image. + +/ +Pixmap crop(const Pixmap source, Pixmap target, Size cropToSize, Point offset = Point(0, 0)) @nogc { + target.adjustTo(cropCalcDims(cropToSize)); + cropInto(source, target, offset); + return target; +} + +/// ditto +Pixmap cropNew(const Pixmap source, Size cropToSize, Point offset = Point(0, 0)) { + auto target = Pixmap.makeNew(cropToSize); + cropInto(source, target, offset); + return target; +} + +/// ditto +Pixmap cropInPlace(Pixmap source, Size cropToSize, Point offset = Point(0, 0)) @nogc { + Pixmap target = source; + target.width = cropToSize.width; + target.data = target.data[0 .. cropToSize.area]; + + auto src = const(SubPixmap)(source, cropToSize, offset); + src.extractToPixmapCopyPixelByPixelImpl(target); + return target; +} + +/// ditto +PixmapBlueprint cropCalcDims(Size cropToSize) @nogc { + return PixmapBlueprint.fromSize(cropToSize); +} + +private void transposeInto(const Pixmap source, Pixmap target) @nogc { + foreach (y; 0 .. target.width) { + foreach (x; 0 .. source.width) { + const idxSrc = linearOffset(Point(x, y), source.width); + const idxDst = linearOffset(Point(y, x), target.width); + + target.data[idxDst] = source.data[idxSrc]; + } + } +} + +/++ + Transposes an image. + + ``` + ╔══╗ ╔══╗ + ║# ║ ║#+║ + ║+x║ → ║ x║ + ╚══╝ ╚══╝ + ``` + +/ +Pixmap transpose(const Pixmap source, Pixmap target) @nogc { + target.adjustTo(source.transposeCalcDims()); + source.transposeInto(target); + return target; +} + +/// ditto +Pixmap transposeNew(const Pixmap source) { + auto target = Pixmap.makeNew(source.transposeCalcDims()); + source.transposeInto(target); + return target; +} + +/// ditto +PixmapBlueprint transposeCalcDims(const Pixmap source) @nogc { + return PixmapBlueprint(source.length, source.height); +} + +private void rotateClockwiseInto(const Pixmap source, Pixmap target) @nogc { + const area = source.data.length; + const rowLength = source.size.height; + ptrdiff_t cursor = -1; + + foreach (px; source.data) { + cursor += rowLength; + if (cursor > area) { + cursor -= (area + 1); + } + + target.data[cursor] = px; + } +} + +/++ + Rotates an image by 90° clockwise. + + ``` + ╔══╗ ╔══╗ + ║# ║ ║+#║ + ║+x║ → ║x ║ + ╚══╝ ╚══╝ + ``` + +/ +Pixmap rotateClockwise(const Pixmap source, Pixmap target) @nogc { + target.adjustTo(source.rotateClockwiseCalcDims()); + source.rotateClockwiseInto(target); + return target; +} + +/// ditto +Pixmap rotateClockwiseNew(const Pixmap source) { + auto target = Pixmap.makeNew(source.rotateClockwiseCalcDims()); + source.rotateClockwiseInto(target); + return target; +} + +/// ditto +PixmapBlueprint rotateClockwiseCalcDims(const Pixmap source) @nogc { + return PixmapBlueprint(source.length, source.height); +} + +private void rotateCounterClockwiseInto(const Pixmap source, Pixmap target) @nogc { + // TODO: can this be optimized? + target = transpose(source, target); + target.flipVerticallyInPlace(); +} + +/++ + Rotates an image by 90° counter-clockwise. + + ``` + ╔══╗ ╔══╗ + ║# ║ ║ x║ + ║+x║ → ║#+║ + ╚══╝ ╚══╝ + ``` + +/ +Pixmap rotateCounterClockwise(const Pixmap source, Pixmap target) @nogc { + target.adjustTo(source.rotateCounterClockwiseCalcDims()); + source.rotateCounterClockwiseInto(target); + return target; +} + +/// ditto +Pixmap rotateCounterClockwiseNew(const Pixmap source) { + auto target = Pixmap.makeNew(source.rotateCounterClockwiseCalcDims()); + source.rotateCounterClockwiseInto(target); + return target; +} + +/// ditto +PixmapBlueprint rotateCounterClockwiseCalcDims(const Pixmap source) @nogc { + return PixmapBlueprint(source.length, source.height); +} + +private void rotate180degInto(const Pixmap source, Pixmap target) @nogc { + // Technically, this is implemented as flip vertical + flip horizontal. + auto src = PixmapScanner(source); + auto dst = PixmapScannerRW(target); + + foreach (srcLine; src) { + auto dstLine = dst.back; + foreach (idxSrc, px; srcLine) { + const idxDst = (dstLine.length - (idxSrc + 1)); + dstLine[idxDst] = px; + } + dst.popBack(); + } +} + +/++ + Rotates an image by 180°. + + ``` + ╔═══╗ ╔═══╗ + ║#- ║ ║%~~║ + ║~~%║ → ║ -#║ + ╚═══╝ ╚═══╝ + ``` + +/ +Pixmap rotate180deg(const Pixmap source, Pixmap target) @nogc { + target.adjustTo(source.rotate180degCalcDims()); + source.rotate180degInto(target); + return target; +} + +/// ditto +Pixmap rotate180degNew(const Pixmap source) { + auto target = Pixmap.makeNew(source.size); + source.rotate180degInto(target); + return target; +} + +/// ditto +void rotate180degInPlace(Pixmap source) @nogc { + auto scanner = PixmapScannerRW(source); + + // Technically, this is implemented as a flip vertical + flip horizontal + // combo, i.e. the image is flipped vertically line by line, but the lines + // are overwritten in a horizontally flipped way. + while (!scanner.empty) { + auto a = scanner.front; + auto b = scanner.back; + + // middle line? (odd number of lines) + if (a.ptr is b.ptr) { + break; + } + + foreach (idxSrc, ref pxA; a) { + const idxDst = (b.length - (idxSrc + 1)); + const tmp = pxA; + pxA = b[idxDst]; + b[idxDst] = tmp; + } + + scanner.popFront(); + scanner.popBack(); + } +} + +/// +PixmapBlueprint rotate180degCalcDims(const Pixmap source) @nogc { + return PixmapBlueprint.fromPixmap(source); +} + +private void flipHorizontallyInto(const Pixmap source, Pixmap target) @nogc { + auto src = PixmapScanner(source); + auto dst = PixmapScannerRW(target); + + foreach (srcLine; src) { + auto dstLine = dst.front; + foreach (idxSrc, px; srcLine) { + const idxDst = (dstLine.length - (idxSrc + 1)); + dstLine[idxDst] = px; + } + + dst.popFront(); + } +} + +/++ + Flips an image horizontally. + + ``` + ╔═══╗ ╔═══╗ + ║#-.║ → ║.-#║ + ╚═══╝ ╚═══╝ + ``` + +/ +Pixmap flipHorizontally(const Pixmap source, Pixmap target) @nogc { + target.adjustTo(source.flipHorizontallyCalcDims()); + source.flipHorizontallyInto(target); + return target; +} + +/// ditto +Pixmap flipHorizontallyNew(const Pixmap source) { + auto target = Pixmap.makeNew(source.size); + source.flipHorizontallyInto(target); + return target; +} + +/// ditto +void flipHorizontallyInPlace(Pixmap source) @nogc { + auto scanner = PixmapScannerRW(source); + + foreach (line; scanner) { + const idxMiddle = (1 + (line.length >> 1)); + auto halfA = line[0 .. idxMiddle]; + + foreach (idxA, ref px; halfA) { + const idxB = (line.length - (idxA + 1)); + const tmp = line[idxB]; + // swap + line[idxB] = px; + px = tmp; + } + } +} + +/// ditto +PixmapBlueprint flipHorizontallyCalcDims(const Pixmap source) @nogc { + return PixmapBlueprint.fromPixmap(source); +} + +private void flipVerticallyInto(const Pixmap source, Pixmap target) @nogc { + auto src = PixmapScanner(source); + auto dst = PixmapScannerRW(target); + + foreach (srcLine; src) { + dst.back[] = srcLine[]; + dst.popBack(); + } +} + +/++ + Flips an image vertically. + + ``` + ╔═══╗ ╔═══╗ + ║## ║ ║ -║ + ║ -║ → ║## ║ + ╚═══╝ ╚═══╝ + ``` + +/ +Pixmap flipVertically(const Pixmap source, Pixmap target) @nogc { + target.adjustTo(source.flipVerticallyCalcDims()); + flipVerticallyInto(source, target); + return target; +} + +/// ditto +Pixmap flipVerticallyNew(const Pixmap source) { + auto target = Pixmap.makeNew(source.flipVerticallyCalcDims()); + source.flipVerticallyInto(target); + return target; +} + +/// ditto +void flipVerticallyInPlace(Pixmap source) @nogc { + auto scanner = PixmapScannerRW(source); + + while (!scanner.empty) { + auto a = scanner.front; + auto b = scanner.back; + + // middle line? (odd number of lines) + if (a.ptr is b.ptr) { + break; + } + + foreach (idx, ref pxA; a) { + const tmp = pxA; + pxA = b[idx]; + b[idx] = tmp; + } + + scanner.popFront(); + scanner.popBack(); + } +} + +/// ditto +PixmapBlueprint flipVerticallyCalcDims(const Pixmap source) @nogc { + return PixmapBlueprint.fromPixmap(source); +} + +@safe pure nothrow @nogc: + // ==== Blending functions ==== /++ @@ -1070,7 +2781,7 @@ public void alphaBlendRGB(ref Pixel pxTarget, const Pixel pxSource) @safe { /++ Blends pixel `source` into pixel `target` - using the requested $(B blending mode). + using the requested [BlendMode|blending mode]. +/ template blendPixel(BlendMode mode, BlendAccuracy accuracy = BlendAccuracy.rgba) { @@ -1261,7 +2972,7 @@ template blendPixel(BlendMode mode, BlendAccuracy accuracy = BlendAccuracy.rgba) /++ Blends the pixel data of `source` into `target` - using the requested $(B blending mode). + using the requested [BlendMode|blending mode]. `source` and `target` MUST have the same length. +/ @@ -1397,7 +3108,7 @@ void drawLine(Pixmap target, Point a, Point b, Pixel color) { image = source pixmap pos = top-left destination position (on the target pixmap) +/ -void drawPixmap(Pixmap target, Pixmap image, Point pos, Blend blend = blendNormal) { +void drawPixmap(Pixmap target, const Pixmap image, Point pos, Blend blend = blendNormal) { alias source = image; immutable tRect = OriginRectangle( @@ -1433,6 +3144,68 @@ void drawPixmap(Pixmap target, Pixmap image, Point pos, Blend blend = blendNorma } } +/++ + Draws an image (a subimage from a source pixmap) on a target pixmap + + Params: + target = target pixmap to draw on + image = source subpixmap + pos = top-left destination position (on the target pixmap) + +/ +void drawPixmap(Pixmap target, const SubPixmap image, Point pos, Blend blend = blendNormal) { + alias source = image; + + debug assert(source.isValid); + + immutable tRect = OriginRectangle( + Size(target.width, target.height), + ); + + immutable sRect = Rectangle(pos, source.size); + + // out of bounds? + if (!tRect.intersect(sRect)) { + return; + } + + Point sourceOffset = source.offset; + Point drawingTarget; + Size drawingSize = source.size; + + if (pos.x <= 0) { + sourceOffset.x -= pos.x; + drawingTarget.x = 0; + drawingSize.width += pos.x; + } else { + drawingTarget.x = pos.x; + } + + if (pos.y <= 0) { + sourceOffset.y -= pos.y; + drawingTarget.y = 0; + drawingSize.height += pos.y; + } else { + drawingTarget.y = pos.y; + } + + Point drawingEnd = drawingTarget + drawingSize.castTo!Point(); + if (drawingEnd.x >= target.width) { + drawingSize.width -= (drawingEnd.x - target.width); + } + if (drawingEnd.y >= target.height) { + drawingSize.height -= (drawingEnd.y - target.height); + } + + auto dst = SubPixmap(target, drawingTarget, drawingSize); + auto src = const(SubPixmap)( + source.source, + drawingSize, + sourceOffset, + ); + + src.xferTo(dst, blend); +} + /++ Draws a sprite from a spritesheet +/ diff --git a/pixmappresenter.d b/pixmappresenter.d index edc97f4..858d65b 100644 --- a/pixmappresenter.d +++ b/pixmappresenter.d @@ -904,7 +904,7 @@ final class PixmapPresenter { _renderer = renderer; // create software framebuffer - auto framebuffer = Pixmap(config.renderer.resolution); + auto framebuffer = Pixmap.makeNew(config.renderer.resolution); // OpenGL? auto openGlOptions = OpenGlOptions.no;