mirror of https://github.com/adamdruppe/arsd.git
1476 lines
30 KiB
D
1476 lines
30 KiB
D
/+
|
||
== 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;
|
||
|
||
private float hackyRound(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);
|
||
}
|
||
|
||
/*
|
||
## 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:
|
||
<https://www.w3.org/TR/compositing/#blending>
|
||
+/
|
||
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,
|
||
);
|
||
}
|
||
}
|