diff --git a/README.md b/README.md index a5ec1db..73f740c 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Future release, likely May 2024 or later. Nothing is planned for it at this time. -arsd.pixmappresenter was added. +arsd.pixmappresenter and arsd.pixmappaint were added. ## 11.0 diff --git a/core.d b/core.d index 4f5c4e8..384e6eb 100644 --- a/core.d +++ b/core.d @@ -189,6 +189,45 @@ version(Posix) { ========================= +/ +/++ + Casts value `v` to type `T`. + + $(TIP + This is a helper function for readability purposes. + The idea is to make type-casting as accessible as `to()` from `std.conv`. + ) + + --- + int i = cast(int)(foo * bar); + int i = castTo!int(foo * bar); + + int j = cast(int) round(floatValue); + int j = round(floatValue).castTo!int; + + int k = cast(int) floatValue + foobar; + int k = floatValue.castTo!int + foobar; + + auto m = Point( + cast(int) calc(a.x, b.x), + cast(int) calc(a.y, b.y), + ); + auto m = Point( + calc(a.x, b.x).castTo!int, + calc(a.y, b.y).castTo!int, + ); + --- + + History: + Added on April 24, 2024. + Renamed from `typeCast` to `castTo` on May 24, 2024. + +/ +auto ref T castTo(T, S)(auto ref S v) { + return cast(T) v; +} + +/// +alias typeCast = castTo; + // enum stringz : const(char)* { init = null } /++ diff --git a/dub.json b/dub.json index ca5268c..b8d057a 100644 --- a/dub.json +++ b/dub.json @@ -681,13 +681,27 @@ "arsd-official:color_base":"*" } }, + { + "name": "pixmappaint", + "description": "2D drawing companion library of Pixmap Presenter.", + "targetType": "library", + "sourceFiles": ["pixmappaint.d"], + "dependencies": { + "arsd-official:color_base":"*", + "arsd-official:core":"*" + }, + "dflags-dmd": ["-mv=arsd.pixmappaint=$PACKAGE_DIR/pixmappaint.d"], + "dflags-ldc": ["--mv=arsd.pixmappaint=$PACKAGE_DIR/pixmappaint.d"], + "dflags-gdc": ["-fmodule-file=arsd.pixmappaint=$PACKAGE_DIR/pixmappaint.d"] + }, { "name": "pixmappresenter", "description": "High-level display library. Designed to blit fully-rendered frames to the screen.", "targetType": "library", "sourceFiles": ["pixmappresenter.d"], "dependencies": { - "arsd-official:color_base":"*", + "arsd-official:core":"*", + "arsd-official:pixmappaint":"*", "arsd-official:simpledisplay":"*" }, "dflags-dmd": ["-mv=arsd.pixmappresenter=$PACKAGE_DIR/pixmappresenter.d"], diff --git a/pixmappaint.d b/pixmappaint.d new file mode 100644 index 0000000..48ad2db --- /dev/null +++ b/pixmappaint.d @@ -0,0 +1,1464 @@ +/+ + == pixmappaint == + Copyright Elias Batek (0xEAB) 2024. + Distributed under the Boost Software License, Version 1.0. + + $(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, + ); + } +} diff --git a/pixmappresenter.d b/pixmappresenter.d index 18b935d..21311ef 100644 --- a/pixmappresenter.d +++ b/pixmappresenter.d @@ -87,7 +87,6 @@ --- import arsd.pixmappresenter; - import arsd.simpledisplay : MouseEvent; int main() { // Internal resolution of the images (“frames”) we will render. @@ -157,8 +156,22 @@ +/ module arsd.pixmappresenter; -import arsd.color; -import arsd.simpledisplay; +import arsd.core; + +/++ + While publicly importing `arsd.simpledisplay` is not actually necessary, + most real-world code would eventually import said module as well anyway. + + More importantly, this public import prevents users from facing certain + symbol clashes in their code that would occur in modules importing both + `pixmappresenter` and `simpledisplay`. + For instance both of these modules happen to define different types + as `Pixmap`. + +/ +public import arsd.simpledisplay; + +/// +public import arsd.pixmappaint; /* ## TODO @@ -166,9 +179,6 @@ import arsd.simpledisplay; - More comprehensive documentation - Additional renderer implementations: - a `ScreenPainter`-based renderer - - a legacy OpenGL renderer (maybe) - - Is there something in arsd that serves a similar purpose to `Pixmap`? - - Can we convert to/from it? - Minimum window size - to ensure `Scaling.integer` doesn’t break “unexpectedly” - More control over timing @@ -176,129 +186,14 @@ import arsd.simpledisplay; */ /// -alias Pixel = Color; - -/// -alias ColorF = arsd.color.ColorF; - -/// -alias Size = arsd.color.Size; - -/// -alias Point = arsd.color.Point; +alias Pixmap = arsd.pixmappaint.Pixmap; /// alias WindowResizedCallback = void delegate(Size); -// verify assumption(s) -static assert(Pixel.sizeof == uint.sizeof); - // is the Timer class available on this platform? private enum hasTimer = is(Timer == class); -/// casts value `v` to type `T` -auto ref T typeCast(T, S)(auto ref S v) { - return cast(T) v; -} - -@safe pure nothrow @nogc { - /// - Pixel rgba(ubyte r, ubyte g, ubyte b, ubyte a = 0xFF) { - return Pixel(r, g, b, a); - } - - /// - 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(Pixel[] data, int width) @nogc - in (data.length % width == 0) { - this.data = data; - this.width = width; - } - - // 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; - } - -@safe pure nothrow @nogc: - - /// Height of the buffer, i.e. the number of lines - int height() inout { - if (width == 0) { - return 0; - } - - return typeCast!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 typeCast!int(data.length); - } - - /++ - Number of bytes per line - - Returns: - width × Pixel.sizeof - +/ - int pitch() inout { - return (width * int(Pixel.sizeof)); - } - - /// Clears the buffer’s contents (by setting each pixel to the same color) - void clear(Pixel value) { - data[] = value; - } -} - // viewport math private @safe pure nothrow @nogc { @@ -341,7 +236,7 @@ private @safe pure nothrow @nogc { Point offsetCenter(const Size drawing, const Size canvas) { auto delta = canvas.deltaPerimeter(drawing); - return (typeCast!Point(delta) >> 1); + return (castTo!Point(delta) >> 1); } } @@ -381,8 +276,8 @@ Viewport calculateViewport(const ref PresenterConfig config) @safe pure nothrow case Scaling.contain: const float scaleF = karContainScalingFactorF(config.renderer.resolution, config.window.size); size = Size( - typeCast!int(scaleF * config.renderer.resolution.width), - typeCast!int(scaleF * config.renderer.resolution.height), + castTo!int(scaleF * config.renderer.resolution.width), + castTo!int(scaleF * config.renderer.resolution.height), ); break; @@ -400,8 +295,8 @@ Viewport calculateViewport(const ref PresenterConfig config) @safe pure nothrow case Scaling.cover: const float fillF = karCoverScalingFactorF(config.renderer.resolution, config.window.size); size = Size( - typeCast!int(fillF * config.renderer.resolution.width), - typeCast!int(fillF * config.renderer.resolution.height), + castTo!int(fillF * config.renderer.resolution.width), + castTo!int(fillF * config.renderer.resolution.height), ); break; } @@ -652,7 +547,7 @@ final class OpenGl3PixmapRenderer : PixmapRenderer { 0, 0, _poc.config.renderer.resolution.width, _poc.config.renderer.resolution.height, GL_RGBA, GL_UNSIGNED_BYTE, - typeCast!(void*)(_poc.framebuffer.data.ptr) + castTo!(void*)(_poc.framebuffer.data.ptr) ); glUseProgram(_shader.shaderProgram); @@ -707,7 +602,7 @@ final class OpenGl3PixmapRenderer : PixmapRenderer { glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * GLfloat.sizeof, null); glEnableVertexAttribArray(0); - glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * GLfloat.sizeof, typeCast!(void*)(2 * GLfloat.sizeof)); + glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * GLfloat.sizeof, castTo!(void*)(2 * GLfloat.sizeof)); glEnableVertexAttribArray(1); } @@ -888,7 +783,7 @@ final class OpenGl1PixmapRenderer : PixmapRenderer { 0, 0, _poc.config.renderer.resolution.width, _poc.config.renderer.resolution.height, GL_RGBA, GL_UNSIGNED_BYTE, - typeCast!(void*)(_poc.framebuffer.data.ptr) + castTo!(void*)(_poc.framebuffer.data.ptr) ); glBegin(GL_QUADS);