/+ == pixmappaint == Copyright Elias Batek (0xEAB) 2024. Distributed under the Boost Software License, Version 1.0. +/ /++ Pixmap image manipulation $(WARNING $(B Early Technology Preview.) ) $(PITFALL This module is $(B work in progress). API is subject to changes until further notice. ) +/ module arsd.pixmappaint; import arsd.color; import arsd.core; import std.math : round; /* ## TODO: - Refactoring the template-mess of blendPixel() & co. - Scaling - Cropping - Rotating - Skewing - HSL - Advanced blend modes (maybe) */ /// alias Color = arsd.color.Color; /// alias ColorF = arsd.color.ColorF; /// alias Pixel = Color; /// alias Point = arsd.color.Point; /// alias Rectangle = arsd.color.Rectangle; /// alias Size = arsd.color.Size; // verify assumption(s) static assert(Pixel.sizeof == uint.sizeof); @safe pure nothrow @nogc { /// Pixel rgba(ubyte r, ubyte g, ubyte b, ubyte a = 0xFF) { return Pixel(r, g, b, a); } /// 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 rgb(ubyte r, ubyte g, ubyte b) { return rgba(r, g, b, 0xFF); } } /++ Pixel data container +/ struct Pixmap { /// Pixel data Pixel[] data; /// Pixel per row int width; @safe pure nothrow: /// this(Size size) { this.size = size; } /// this(int width, int height) in (width > 0) in (height > 0) { this(Size(width, height)); } /// this(Pixel[] data, int width) @nogc in (data.length % width == 0) { this.data = data; this.width = width; } /++ Creates a $(I deep clone) of the Pixmap +/ Pixmap clone() const { auto c = Pixmap(); c.width = this.width; c.data = this.data.dup; return c; } // undocumented: really shouldn’t be used. // carries the risks of `length` and `width` getting out of sync accidentally. deprecated("Use `size` instead.") void length(int value) { data.length = value; } /++ Changes the size of the buffer Reallocates the underlying pixel array. +/ void size(Size value) { data.length = value.area; width = value.width; } /// ditto void size(int totalPixels, int width) in (totalPixels % width == 0) { data.length = totalPixels; this.width = width; } static { /++ Creates a Pixmap wrapping the pixel data from the provided `TrueColorImage`. Interoperability function: `arsd.color` +/ Pixmap fromTrueColorImage(TrueColorImage source) @nogc { return Pixmap(source.imageData.colors, source.width); } /++ Creates a Pixmap wrapping the pixel data from the provided `MemoryImage`. Interoperability function: `arsd.color` +/ Pixmap fromMemoryImage(MemoryImage source) { return fromTrueColorImage(source.getAsTrueColorImage()); } } @safe pure nothrow @nogc: /// Height of the buffer, i.e. the number of lines int height() inout { if (width == 0) { return 0; } return castTo!int(data.length / width); } /// Rectangular size of the buffer Size size() inout { return Size(width, height); } /// Length of the buffer, i.e. the number of pixels int length() inout { return castTo!int(data.length); } /++ Number of bytes per line Returns: width × Pixel.sizeof +/ int pitch() inout { return (width * int(Pixel.sizeof)); } /++ 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 { immutable size_t offset = linearOffset(width, pos); immutable size_t end = (offset + n); return data[offset .. end]; } /// Clears the buffer’s contents (by setting each pixel to the same color) void clear(Pixel value) { data[] = value; } } /// struct SpriteSheet { private { Pixmap _pixmap; Size _spriteDimensions; Size _layout; // pre-computed upon construction } @safe pure nothrow @nogc: /// public this(Pixmap pixmap, Size spriteSize) { _pixmap = pixmap; _spriteDimensions = spriteSize; _layout = Size( _pixmap.width / _spriteDimensions.width, _pixmap.height / _spriteDimensions.height, ); } /// inout(Pixmap) pixmap() inout { return _pixmap; } /// Size spriteSize() inout { return _spriteDimensions; } /// Size layout() inout { return _layout; } /// Point getSpriteColumn(int index) inout { immutable x = index % layout.width; immutable y = (index - x) / layout.height; return Point(x, y); } /// Point getSpritePixelOffset2D(int index) inout { immutable col = this.getSpriteColumn(index); return Point( col.x * _spriteDimensions.width, col.y * _spriteDimensions.height, ); } } // Silly micro-optimization private struct OriginRectangle { Size size; @safe pure nothrow @nogc: int left() const => 0; int top() const => 0; int right() const => size.width; int bottom() const => size.height; bool intersect(const Rectangle b) const { // dfmt off return ( (b.right > 0 ) && (b.left < this.right ) && (b.bottom > 0 ) && (b.top < this.bottom) ); // dfmt on } } @safe pure nothrow @nogc: // misc private { Point pos(Rectangle r) => r.upperLeft; T max(T)(T a, T b) => (a >= b) ? a : b; T min(T)(T a, T b) => (a <= b) ? a : b; } /++ Calculates the square root of an integer number as an integer number. +/ ubyte intSqrt(const ubyte value) @safe pure nothrow @nogc { switch (value) { default: // unreachable assert(false, "ubyte != uint8"); case 0: return 0; case 1: .. case 2: return 1; case 3: .. case 6: return 2; case 7: .. case 12: return 3; case 13: .. case 20: return 4; case 21: .. case 30: return 5; case 31: .. case 42: return 6; case 43: .. case 56: return 7; case 57: .. case 72: return 8; case 73: .. case 90: return 9; case 91: .. case 110: return 10; case 111: .. case 132: return 11; case 133: .. case 156: return 12; case 157: .. case 182: return 13; case 183: .. case 210: return 14; case 211: .. case 240: return 15; case 241: .. case 255: return 16; } } /// unittest { assert(intSqrt(4) == 2); assert(intSqrt(9) == 3); assert(intSqrt(10) == 3); } unittest { import std.math : round, sqrt; foreach (n; ubyte.min .. ubyte.max + 1) { ubyte fp = sqrt(float(n)).round().castTo!ubyte; ubyte i8 = intSqrt(n.castTo!ubyte); assert(fp == i8); } } /++ Calculates the square root of the normalized value representated by the input integer number. Normalization: `[0x00 .. 0xFF]` → `[0.0 .. 1.0]` Returns: sqrt(value / 255f) * 255 +/ ubyte intNormalizedSqrt(const ubyte value) { switch (value) { default: // unreachable assert(false, "ubyte != uint8"); case 0x00: return 0x00; case 0x01: return 0x10; case 0x02: return 0x17; case 0x03: return 0x1C; case 0x04: return 0x20; case 0x05: return 0x24; case 0x06: return 0x27; case 0x07: return 0x2A; case 0x08: return 0x2D; case 0x09: return 0x30; case 0x0A: return 0x32; case 0x0B: return 0x35; case 0x0C: return 0x37; case 0x0D: return 0x3A; case 0x0E: return 0x3C; case 0x0F: return 0x3E; case 0x10: return 0x40; case 0x11: return 0x42; case 0x12: return 0x44; case 0x13: return 0x46; case 0x14: return 0x47; case 0x15: return 0x49; case 0x16: return 0x4B; case 0x17: return 0x4D; case 0x18: return 0x4E; case 0x19: return 0x50; case 0x1A: return 0x51; case 0x1B: return 0x53; case 0x1C: return 0x54; case 0x1D: return 0x56; case 0x1E: return 0x57; case 0x1F: return 0x59; case 0x20: return 0x5A; case 0x21: return 0x5C; case 0x22: return 0x5D; case 0x23: return 0x5E; case 0x24: return 0x60; case 0x25: return 0x61; case 0x26: return 0x62; case 0x27: return 0x64; case 0x28: return 0x65; case 0x29: return 0x66; case 0x2A: return 0x67; case 0x2B: return 0x69; case 0x2C: return 0x6A; case 0x2D: return 0x6B; case 0x2E: return 0x6C; case 0x2F: return 0x6D; case 0x30: return 0x6F; case 0x31: return 0x70; case 0x32: return 0x71; case 0x33: return 0x72; case 0x34: return 0x73; case 0x35: return 0x74; case 0x36: return 0x75; case 0x37: return 0x76; case 0x38: return 0x77; case 0x39: return 0x79; case 0x3A: return 0x7A; case 0x3B: return 0x7B; case 0x3C: return 0x7C; case 0x3D: return 0x7D; case 0x3E: return 0x7E; case 0x3F: return 0x7F; case 0x40: return 0x80; case 0x41: return 0x81; case 0x42: return 0x82; case 0x43: return 0x83; case 0x44: return 0x84; case 0x45: return 0x85; case 0x46: return 0x86; case 0x47: .. case 0x48: return 0x87; case 0x49: return 0x88; case 0x4A: return 0x89; case 0x4B: return 0x8A; case 0x4C: return 0x8B; case 0x4D: return 0x8C; case 0x4E: return 0x8D; case 0x4F: return 0x8E; case 0x50: return 0x8F; case 0x51: return 0x90; case 0x52: .. case 0x53: return 0x91; case 0x54: return 0x92; case 0x55: return 0x93; case 0x56: return 0x94; case 0x57: return 0x95; case 0x58: return 0x96; case 0x59: .. case 0x5A: return 0x97; case 0x5B: return 0x98; case 0x5C: return 0x99; case 0x5D: return 0x9A; case 0x5E: return 0x9B; case 0x5F: .. case 0x60: return 0x9C; case 0x61: return 0x9D; case 0x62: return 0x9E; case 0x63: return 0x9F; case 0x64: .. case 0x65: return 0xA0; case 0x66: return 0xA1; case 0x67: return 0xA2; case 0x68: return 0xA3; case 0x69: .. case 0x6A: return 0xA4; case 0x6B: return 0xA5; case 0x6C: return 0xA6; case 0x6D: .. case 0x6E: return 0xA7; case 0x6F: return 0xA8; case 0x70: return 0xA9; case 0x71: .. case 0x72: return 0xAA; case 0x73: return 0xAB; case 0x74: return 0xAC; case 0x75: .. case 0x76: return 0xAD; case 0x77: return 0xAE; case 0x78: return 0xAF; case 0x79: .. case 0x7A: return 0xB0; case 0x7B: return 0xB1; case 0x7C: return 0xB2; case 0x7D: .. case 0x7E: return 0xB3; case 0x7F: return 0xB4; case 0x80: .. case 0x81: return 0xB5; case 0x82: return 0xB6; case 0x83: .. case 0x84: return 0xB7; case 0x85: return 0xB8; case 0x86: return 0xB9; case 0x87: .. case 0x88: return 0xBA; case 0x89: return 0xBB; case 0x8A: .. case 0x8B: return 0xBC; case 0x8C: return 0xBD; case 0x8D: .. case 0x8E: return 0xBE; case 0x8F: return 0xBF; case 0x90: .. case 0x91: return 0xC0; case 0x92: return 0xC1; case 0x93: .. case 0x94: return 0xC2; case 0x95: return 0xC3; case 0x96: .. case 0x97: return 0xC4; case 0x98: return 0xC5; case 0x99: .. case 0x9A: return 0xC6; case 0x9B: .. case 0x9C: return 0xC7; case 0x9D: return 0xC8; case 0x9E: .. case 0x9F: return 0xC9; case 0xA0: return 0xCA; case 0xA1: .. case 0xA2: return 0xCB; case 0xA3: .. case 0xA4: return 0xCC; case 0xA5: return 0xCD; case 0xA6: .. case 0xA7: return 0xCE; case 0xA8: return 0xCF; case 0xA9: .. case 0xAA: return 0xD0; case 0xAB: .. case 0xAC: return 0xD1; case 0xAD: return 0xD2; case 0xAE: .. case 0xAF: return 0xD3; case 0xB0: .. case 0xB1: return 0xD4; case 0xB2: return 0xD5; case 0xB3: .. case 0xB4: return 0xD6; case 0xB5: .. case 0xB6: return 0xD7; case 0xB7: return 0xD8; case 0xB8: .. case 0xB9: return 0xD9; case 0xBA: .. case 0xBB: return 0xDA; case 0xBC: return 0xDB; case 0xBD: .. case 0xBE: return 0xDC; case 0xBF: .. case 0xC0: return 0xDD; case 0xC1: .. case 0xC2: return 0xDE; case 0xC3: return 0xDF; case 0xC4: .. case 0xC5: return 0xE0; case 0xC6: .. case 0xC7: return 0xE1; case 0xC8: .. case 0xC9: return 0xE2; case 0xCA: return 0xE3; case 0xCB: .. case 0xCC: return 0xE4; case 0xCD: .. case 0xCE: return 0xE5; case 0xCF: .. case 0xD0: return 0xE6; case 0xD1: .. case 0xD2: return 0xE7; case 0xD3: return 0xE8; case 0xD4: .. case 0xD5: return 0xE9; case 0xD6: .. case 0xD7: return 0xEA; case 0xD8: .. case 0xD9: return 0xEB; case 0xDA: .. case 0xDB: return 0xEC; case 0xDC: .. case 0xDD: return 0xED; case 0xDE: .. case 0xDF: return 0xEE; case 0xE0: return 0xEF; case 0xE1: .. case 0xE2: return 0xF0; case 0xE3: .. case 0xE4: return 0xF1; case 0xE5: .. case 0xE6: return 0xF2; case 0xE7: .. case 0xE8: return 0xF3; case 0xE9: .. case 0xEA: return 0xF4; case 0xEB: .. case 0xEC: return 0xF5; case 0xED: .. case 0xEE: return 0xF6; case 0xEF: .. case 0xF0: return 0xF7; case 0xF1: .. case 0xF2: return 0xF8; case 0xF3: .. case 0xF4: return 0xF9; case 0xF5: .. case 0xF6: return 0xFA; case 0xF7: .. case 0xF8: return 0xFB; case 0xF9: .. case 0xFA: return 0xFC; case 0xFB: .. case 0xFC: return 0xFD; case 0xFD: .. case 0xFE: return 0xFE; case 0xFF: return 0xFF; } } unittest { import std.math : round, sqrt; foreach (n; ubyte.min .. ubyte.max + 1) { ubyte fp = (sqrt(n / 255.0f) * 255).round().castTo!ubyte; ubyte i8 = intNormalizedSqrt(n.castTo!ubyte); assert(fp == i8); } } /++ Limits a value to a maximum of 0xFF (= 255). +/ ubyte clamp255(Tint)(const Tint value) { pragma(inline, true); return (value < 0xFF) ? value.castTo!ubyte : 0xFF; } /++ Fast 8-bit “percentage” function This function optimizes its runtime performance by substituting the division by 255 with an approximation using bitshifts. Nonetheless, its result are as accurate as a floating point division with 64-bit precision. Params: nPercentage = percentage as the number of 255ths (“two hundred fifty-fifths”) value = base value (“total”) Returns: `round(value * nPercentage / 255.0)` +/ ubyte n255thsOf(const ubyte nPercentage, const ubyte value) { immutable factor = (nPercentage | (nPercentage << 8)); return (((value * factor) + 0x8080) >> 16); } @safe unittest { // Accuracy verification static ubyte n255thsOfFP64(const ubyte nPercentage, const ubyte value) { return (double(value) * double(nPercentage) / 255.0).round().castTo!ubyte(); } for (int value = ubyte.min; value <= ubyte.max; ++value) { for (int percent = ubyte.min; percent <= ubyte.max; ++percent) { immutable v = cast(ubyte) value; immutable p = cast(ubyte) percent; immutable approximated = n255thsOf(p, v); immutable precise = n255thsOfFP64(p, v); assert(approximated == precise); } } } /++ Sets the opacity of a [Pixmap]. This lossy operation updates the alpha-channel value of each pixel. → `alpha *= opacity` See_Also: Use [opacityF] with opacity values in percent (%). +/ void opacity(Pixmap pixmap, const ubyte opacity) { foreach (ref px; pixmap.data) { px.a = opacity.n255thsOf(px.a); } } /++ Sets the opacity of a [Pixmap]. This lossy operation updates the alpha-channel value of each pixel. → `alpha *= opacity` 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; } /++ Inverts a color (to its negative color). +/ Pixel invert(const Pixel color) { return Pixel( 0xFF - color.r, 0xFF - color.g, 0xFF - color.b, color.a, ); } /++ Inverts all colors to produce a $(B negative image). $(TIP Develops a positive image when applied to a negative one. ) +/ void invert(Pixmap pixmap) { foreach (ref px; pixmap.data) { px = invert(px); } } // ==== Blending functions ==== /++ Alpha-blending accuracy level $(TIP This primarily exists for performance reasons. In my tests LLVM manages to auto-vectorize the RGB-only codepath significantly better, while the codegen for the accurate RGBA path is pretty conservative. This provides an optimization opportunity for use-cases that don’t require an alpha-channel on the result. ) +/ enum BlendAccuracy { /++ Only RGB channels will have the correct result. A(lpha) channel can contain any value. Suitable for blending into non-transparent targets (e.g. framebuffer, canvas) where the resulting alpha-channel (opacity) value does not matter. +/ rgb = false, /++ All RGBA channels will have the correct result. Suitable for blending into transparent targets (e.g. images) where the resulting alpha-channel (opacity) value matters. Use this mode for image manipulation. +/ rgba = true, } /++ Blend modes $(NOTE As blending operations are implemented as integer calculations, results may be slightly less precise than those from image manipulation programs using floating-point math. ) See_Also: +/ enum BlendMode { /// none = 0, /// replace = none, /// normal = 1, /// alpha = normal, /// multiply, /// screen, /// overlay, /// hardLight, /// softLight, /// darken, /// lighten, /// colorDodge, /// colorBurn, /// difference, /// exclusion, /// subtract, /// divide, } /// alias Blend = BlendMode; // undocumented enum blendNormal = BlendMode.normal; /// alias BlendFn = ubyte function(const ubyte background, const ubyte foreground) pure nothrow @nogc; /++ Blends `source` into `target` with respect to the opacity of the source image (as stored in the alpha channel). See_Also: [alphaBlendRGBA] and [alphaBlendRGB] are shorthand functions in cases where no special blending algorithm is needed. +/ template alphaBlend(BlendFn blend = null, BlendAccuracy accuracy = BlendAccuracy.rgba) { /// ditto public void alphaBlend(scope Pixel[] target, scope const Pixel[] source) @trusted in (source.length == target.length) { foreach (immutable idx, ref pxTarget; target) { alphaBlend(pxTarget, source.ptr[idx]); } } /// ditto public void alphaBlend(ref Pixel pxTarget, const Pixel pxSource) @trusted { pragma(inline, true); static if (accuracy == BlendAccuracy.rgba) { immutable alphaResult = clamp255(pxSource.a + n255thsOf(pxTarget.a, (0xFF - pxSource.a))); //immutable alphaResult = clamp255(pxTarget.a + n255thsOf(pxSource.a, (0xFF - pxTarget.a))); } immutable alphaSource = (pxSource.a | (pxSource.a << 8)); immutable alphaTarget = (0xFFFF - alphaSource); foreach (immutable ib, ref px; pxTarget.components) { static if (blend !is null) { immutable bx = blend(px, pxSource.components.ptr[ib]); } else { immutable bx = pxSource.components.ptr[ib]; } immutable d = cast(ubyte)(((px * alphaTarget) + 0x8080) >> 16); immutable s = cast(ubyte)(((bx * alphaSource) + 0x8080) >> 16); px = cast(ubyte)(d + s); } static if (accuracy == BlendAccuracy.rgba) { pxTarget.a = alphaResult; } } } /// ditto template alphaBlend(BlendAccuracy accuracy, BlendFn blend = null) { alias alphaBlend = alphaBlend!(blend, accuracy); } /++ Blends `source` into `target` with respect to the opacity of the source image (as stored in the alpha channel). This variant is $(slower than) [alphaBlendRGB], but calculates the correct alpha-channel value of the target. See [BlendAccuracy] for further explanation. +/ public void alphaBlendRGBA(scope Pixel[] target, scope const Pixel[] source) @safe { return alphaBlend!(null, BlendAccuracy.rgba)(target, source); } /// ditto public void alphaBlendRGBA(ref Pixel pxTarget, const Pixel pxSource) @safe { return alphaBlend!(null, BlendAccuracy.rgba)(pxTarget, pxSource); } /++ Blends `source` into `target` with respect to the opacity of the source image (as stored in the alpha channel). This variant is $(B faster than) [alphaBlendRGBA], but leads to a wrong alpha-channel value in the target. Useful because of the performance advantage in cases where the resulting alpha does not matter. See [BlendAccuracy] for further explanation. +/ public void alphaBlendRGB(scope Pixel[] target, scope const Pixel[] source) @safe { return alphaBlend!(null, BlendAccuracy.rgb)(target, source); } /// ditto public void alphaBlendRGB(ref Pixel pxTarget, const Pixel pxSource) @safe { return alphaBlend!(null, BlendAccuracy.rgb)(pxTarget, pxSource); } /++ Blends pixel `source` into pixel `target` using the requested $(B blending mode). +/ template blendPixel(BlendMode mode, BlendAccuracy accuracy = BlendAccuracy.rgba) { static if (mode == BlendMode.replace) { /// ditto void blendPixel(ref Pixel target, const Pixel source) { target = source; } } static if (mode == BlendMode.alpha) { /// ditto void blendPixel(ref Pixel target, const Pixel source) { return alphaBlend!accuracy(target, source); } } static if (mode == BlendMode.multiply) { /// ditto void blendPixel(ref Pixel target, const Pixel source) { return alphaBlend!(accuracy, (a, b) => n255thsOf(a, b) )(target, source); } } static if (mode == BlendMode.screen) { /// ditto void blendPixel()(ref Pixel target, const Pixel source) { return alphaBlend!(accuracy, (a, b) => castTo!ubyte(0xFF - n255thsOf((0xFF - a), (0xFF - b))) )(target, source); } } static if (mode == BlendMode.darken) { /// ditto void blendPixel()(ref Pixel target, const Pixel source) { return alphaBlend!(accuracy, (a, b) => min(a, b) )(target, source); } } static if (mode == BlendMode.lighten) { /// ditto void blendPixel()(ref Pixel target, const Pixel source) { return alphaBlend!(accuracy, (a, b) => max(a, b) )(target, source); } } static if (mode == BlendMode.overlay) { /// ditto void blendPixel()(ref Pixel target, const Pixel source) { return alphaBlend!(accuracy, function(const ubyte b, const ubyte f) { if (b < 0x80) { return n255thsOf((2 * b).castTo!ubyte, f); } return castTo!ubyte( 0xFF - n255thsOf(castTo!ubyte(2 * (0xFF - b)), (0xFF - f)) ); })(target, source); } } static if (mode == BlendMode.hardLight) { /// ditto void blendPixel()(ref Pixel target, const Pixel source) { return alphaBlend!(accuracy, function(const ubyte b, const ubyte f) { if (f < 0x80) { return n255thsOf(castTo!ubyte(2 * f), b); } return castTo!ubyte( 0xFF - n255thsOf(castTo!ubyte(2 * (0xFF - f)), (0xFF - b)) ); })(target, source); } } static if (mode == BlendMode.softLight) { /// ditto void blendPixel()(ref Pixel target, const Pixel source) { return alphaBlend!(accuracy, function(const ubyte b, const ubyte f) { if (f < 0x80) { // dfmt off return castTo!ubyte( b - n255thsOf( n255thsOf((0xFF - 2 * f).castTo!ubyte, b), (0xFF - b), ) ); // dfmt on } // TODO: optimize if possible // dfmt off immutable ubyte d = (b < 0x40) ? castTo!ubyte((b * (0x3FC + (((16 * b - 0xBF4) * b) / 255))) / 255) : intNormalizedSqrt(b); //dfmt on return castTo!ubyte( b + n255thsOf((2 * f - 0xFF).castTo!ubyte, (d - b).castTo!ubyte) ); })(target, source); } } static if (mode == BlendMode.colorDodge) { /// ditto void blendPixel()(ref Pixel target, const Pixel source) { return alphaBlend!(accuracy, function(const ubyte b, const ubyte f) { if (b == 0x00) { return ubyte(0x00); } if (f == 0xFF) { return ubyte(0xFF); } return min( ubyte(0xFF), clamp255((255 * b) / (0xFF - f)) ); })(target, source); } } static if (mode == BlendMode.colorBurn) { /// ditto void blendPixel()(ref Pixel target, const Pixel source) { return alphaBlend!(accuracy, function(const ubyte b, const ubyte f) { if (b == 0xFF) { return ubyte(0xFF); } if (f == 0x00) { return ubyte(0x00); } immutable m = min( ubyte(0xFF), clamp255(((0xFF - b) * 255) / f) ); return castTo!ubyte(0xFF - m); })(target, source); } } static if (mode == BlendMode.difference) { /// ditto void blendPixel()(ref Pixel target, const Pixel source) { return alphaBlend!(accuracy, (b, f) => (b > f) ? castTo!ubyte(b - f) : castTo!ubyte(f - b) )(target, source); } } static if (mode == BlendMode.exclusion) { /// ditto void blendPixel()(ref Pixel target, const Pixel source) { return alphaBlend!(accuracy, (b, f) => castTo!ubyte(b + f - (2 * n255thsOf(f, b))) )(target, source); } } static if (mode == BlendMode.subtract) { /// ditto void blendPixel()(ref Pixel target, const Pixel source) { return alphaBlend!(accuracy, (b, f) => (b > f) ? castTo!ubyte(b - f) : ubyte(0) )(target, source); } } static if (mode == BlendMode.divide) { /// ditto void blendPixel()(ref Pixel target, const Pixel source) { return alphaBlend!(accuracy, (b, f) => (f == 0) ? ubyte(0xFF) : clamp255(0xFF * b / f) )(target, source); } } //else { // static assert(false, "Missing `blendPixel()` implementation for `BlendMode`.`" ~ mode ~ "`."); //} } /++ Blends the pixel data of `source` into `target` using the requested $(B blending mode). `source` and `target` MUST have the same length. +/ void blendPixels( BlendMode mode, BlendAccuracy accuracy, )(scope Pixel[] target, scope const Pixel[] source) @trusted in (source.length == target.length) { static if (mode == BlendMode.replace) { // explicit optimization target.ptr[0 .. target.length] = source.ptr[0 .. target.length]; } else { // better error message in case it’s not implemented static if (!is(typeof(blendPixel!(mode, accuracy)))) { pragma(msg, "Hint: Missing or bad `blendPixel!(" ~ mode.stringof ~ ")`."); } foreach (immutable idx, ref pxTarget; target) { blendPixel!(mode, accuracy)(pxTarget, source.ptr[idx]); } } } /// ditto void blendPixels(BlendAccuracy accuracy)(scope Pixel[] target, scope const Pixel[] source, BlendMode mode) { import std.meta : NoDuplicates; import std.traits : EnumMembers; final switch (mode) with (BlendMode) { static foreach (m; NoDuplicates!(EnumMembers!BlendMode)) { case m: return blendPixels!(m, accuracy)(target, source); } } } /// ditto void blendPixels( scope Pixel[] target, scope const Pixel[] source, BlendMode mode, BlendAccuracy accuracy = BlendAccuracy.rgba, ) { if (accuracy == BlendAccuracy.rgb) { return blendPixels!(BlendAccuracy.rgb)(target, source, mode); } else { return blendPixels!(BlendAccuracy.rgba)(target, source, mode); } } // ==== Drawing functions ==== /++ Draws a single pixel +/ void drawPixel(Pixmap target, Point pos, Pixel color) { immutable size_t offset = linearOffset(target.width, pos); target.data[offset] = color; } /++ Draws a rectangle +/ void drawRectangle(Pixmap target, Rectangle rectangle, Pixel color) { alias r = rectangle; immutable tRect = OriginRectangle( Size(target.width, target.height), ); // out of bounds? if (!tRect.intersect(r)) { return; } immutable drawingTarget = Point( (r.pos.x >= 0) ? r.pos.x : 0, (r.pos.y >= 0) ? r.pos.y : 0, ); immutable drawingEnd = Point( (r.right < tRect.right) ? r.right : tRect.right, (r.bottom < tRect.bottom) ? r.bottom : tRect.bottom, ); immutable int drawingWidth = drawingEnd.x - drawingTarget.x; foreach (y; drawingTarget.y .. drawingEnd.y) { target.sliceAt(Point(drawingTarget.x, y), drawingWidth)[] = color; } } /++ Draws a line +/ void drawLine(Pixmap target, Point a, Point b, Pixel color) { import std.math : sqrt; // TODO: line width // TODO: anti-aliasing (looks awful without it!) float deltaX = b.x - a.x; float deltaY = b.y - a.y; int steps = sqrt(deltaX * deltaX + deltaY * deltaY).castTo!int; float[2] step = [ (deltaX / steps), (deltaY / steps), ]; foreach (i; 0 .. steps) { // dfmt off immutable Point p = a + Point( round(step[0] * i).castTo!int, round(step[1] * i).castTo!int, ); // dfmt on immutable offset = linearOffset(p, target.width); target.data[offset] = color; } immutable offsetEnd = linearOffset(b, target.width); target.data[offsetEnd] = color; } /++ Draws an image (a source pixmap) on a target pixmap Params: target = target pixmap to draw on image = source pixmap pos = top-left destination position (on the target pixmap) +/ void drawPixmap(Pixmap target, Pixmap image, Point pos, Blend blend = blendNormal) { alias source = image; immutable tRect = OriginRectangle( Size(target.width, target.height), ); immutable sRect = Rectangle(pos, source.size); // out of bounds? if (!tRect.intersect(sRect)) { return; } immutable drawingTarget = Point( (pos.x >= 0) ? pos.x : 0, (pos.y >= 0) ? pos.y : 0, ); immutable drawingEnd = Point( (sRect.right < tRect.right) ? sRect.right : tRect.right, (sRect.bottom < tRect.bottom) ? sRect.bottom : tRect.bottom, ); immutable drawingSource = Point(drawingTarget.x, 0) - Point(sRect.pos.x, sRect.pos.y); immutable int drawingWidth = drawingEnd.x - drawingTarget.x; foreach (y; drawingTarget.y .. drawingEnd.y) { blendPixels( target.sliceAt(Point(drawingTarget.x, y), drawingWidth), source.sliceAt(Point(drawingSource.x, y + drawingSource.y), drawingWidth), blend, ); } } /++ Draws a sprite from a spritesheet +/ void drawSprite(Pixmap target, const SpriteSheet sheet, int spriteIndex, Point pos, Blend blend = blendNormal) { immutable tRect = OriginRectangle( Size(target.width, target.height), ); immutable spriteOffset = sheet.getSpritePixelOffset2D(spriteIndex); immutable sRect = Rectangle(pos, sheet.spriteSize); // out of bounds? if (!tRect.intersect(sRect)) { return; } immutable drawingTarget = Point( (pos.x >= 0) ? pos.x : 0, (pos.y >= 0) ? pos.y : 0, ); immutable drawingEnd = Point( (sRect.right < tRect.right) ? sRect.right : tRect.right, (sRect.bottom < tRect.bottom) ? sRect.bottom : tRect.bottom, ); immutable drawingSource = spriteOffset + Point(drawingTarget.x, 0) - Point(sRect.pos.x, sRect.pos.y); immutable int drawingWidth = drawingEnd.x - drawingTarget.x; foreach (y; drawingTarget.y .. drawingEnd.y) { blendPixels( target.sliceAt(Point(drawingTarget.x, y), drawingWidth), sheet.pixmap.sliceAt(Point(drawingSource.x, y + drawingSource.y), drawingWidth), blend, ); } }