mirror of https://github.com/adamdruppe/arsd.git
764 lines
16 KiB
D
764 lines
16 KiB
D
module arsd.color;
|
|
|
|
// importing phobos explodes the size of this code 10x, so not doing it.
|
|
|
|
private {
|
|
real toInternal(T)(string s) {
|
|
real accumulator = 0.0;
|
|
size_t i = s.length;
|
|
foreach(idx, c; s) {
|
|
if(c >= '0' && c <= '9') {
|
|
accumulator *= 10;
|
|
accumulator += c - '0';
|
|
} else if(c == '.') {
|
|
i = idx + 1;
|
|
break;
|
|
} else
|
|
throw new Exception("bad char to make real from " ~ s);
|
|
}
|
|
|
|
real accumulator2 = 0.0;
|
|
real count = 1;
|
|
foreach(c; s[i .. $]) {
|
|
if(c >= '0' && c <= '9') {
|
|
accumulator2 *= 10;
|
|
accumulator2 += c - '0';
|
|
count *= 10;
|
|
} else
|
|
throw new Exception("bad char to make real from " ~ s);
|
|
}
|
|
|
|
return accumulator + accumulator2 / count;
|
|
}
|
|
|
|
string toInternal(T)(int a) {
|
|
if(a == 0)
|
|
return "0";
|
|
char[] ret;
|
|
while(a) {
|
|
ret ~= (a % 10) + '0';
|
|
a /= 10;
|
|
}
|
|
for(int i = 0; i < ret.length / 2; i++) {
|
|
char c = ret[i];
|
|
ret[i] = ret[$ - i - 1];
|
|
ret[$ - i - 1] = c;
|
|
}
|
|
return cast(string) ret;
|
|
}
|
|
string toInternal(T)(real a) {
|
|
// a simplifying assumption here is the fact that we only use this in one place: toInternal!string(cast(real) a / 255)
|
|
// thus we know this will always be between 0.0 and 1.0, inclusive.
|
|
if(a <= 0.0)
|
|
return "0.0";
|
|
if(a >= 1.0)
|
|
return "1.0";
|
|
string ret = "0.";
|
|
// I wonder if I can handle round off error any better. Phobos does, but that isn't worth 100 KB of code.
|
|
int amt = cast(int)(a * 1000);
|
|
return ret ~ toInternal!string(amt);
|
|
}
|
|
|
|
real absInternal(real a) { return a < 0 ? -a : a; }
|
|
real minInternal(real a, real b, real c) {
|
|
auto m = a;
|
|
if(b < m) m = b;
|
|
if(c < m) m = c;
|
|
return m;
|
|
}
|
|
real maxInternal(real a, real b, real c) {
|
|
auto m = a;
|
|
if(b > m) m = b;
|
|
if(c > m) m = c;
|
|
return m;
|
|
}
|
|
bool startsWithInternal(string a, string b) {
|
|
return (a.length >= b.length && a[0 .. b.length] == b);
|
|
}
|
|
string[] splitInternal(string a, char c) {
|
|
string[] ret;
|
|
size_t previous = 0;
|
|
foreach(i, char ch; a) {
|
|
if(ch == c) {
|
|
ret ~= a[previous .. i];
|
|
previous = i + 1;
|
|
}
|
|
}
|
|
if(previous != a.length)
|
|
ret ~= a[previous .. $];
|
|
return ret;
|
|
}
|
|
string stripInternal(string s) {
|
|
foreach(i, char c; s)
|
|
if(c != ' ' && c != '\t' && c != '\n') {
|
|
s = s[i .. $];
|
|
break;
|
|
}
|
|
for(int a = cast(int)(s.length - 1); a > 0; a--) {
|
|
char c = s[a];
|
|
if(c != ' ' && c != '\t' && c != '\n') {
|
|
s = s[0 .. a + 1];
|
|
break;
|
|
}
|
|
}
|
|
|
|
return s;
|
|
}
|
|
}
|
|
|
|
// done with mini-phobos
|
|
|
|
struct Color {
|
|
ubyte r;
|
|
ubyte g;
|
|
ubyte b;
|
|
ubyte a;
|
|
|
|
this(int red, int green, int blue, int alpha = 255) {
|
|
this.r = cast(ubyte) red;
|
|
this.g = cast(ubyte) green;
|
|
this.b = cast(ubyte) blue;
|
|
this.a = cast(ubyte) alpha;
|
|
}
|
|
|
|
static Color transparent() { return Color(0, 0, 0, 0); }
|
|
static Color white() { return Color(255, 255, 255); }
|
|
static Color black() { return Color(0, 0, 0); }
|
|
static Color red() { return Color(255, 0, 0); }
|
|
static Color green() { return Color(0, 255, 0); }
|
|
static Color blue() { return Color(0, 0, 255); }
|
|
static Color yellow() { return Color(255, 255, 0); }
|
|
static Color teal() { return Color(0, 255, 255); }
|
|
static Color purple() { return Color(255, 0, 255); }
|
|
|
|
/*
|
|
ubyte[4] toRgbaArray() {
|
|
return [r,g,b,a];
|
|
}
|
|
*/
|
|
|
|
string toCssString() {
|
|
if(a == 255)
|
|
return "#" ~ toHexInternal(r) ~ toHexInternal(g) ~ toHexInternal(b);
|
|
else {
|
|
return "rgba("~toInternal!string(r)~", "~toInternal!string(g)~", "~toInternal!string(b)~", "~toInternal!string(cast(real)a / 255.0)~")";
|
|
}
|
|
}
|
|
|
|
string toString() {
|
|
if(a == 255)
|
|
return toCssString()[1 .. $];
|
|
else
|
|
return toHexInternal(r) ~ toHexInternal(g) ~ toHexInternal(b) ~ toHexInternal(a);
|
|
}
|
|
|
|
static Color fromNameString(string s) {
|
|
Color c;
|
|
foreach(member; __traits(allMembers, Color)) {
|
|
static if(__traits(compiles, c = __traits(getMember, Color, member))) {
|
|
if(s == member)
|
|
return __traits(getMember, Color, member);
|
|
}
|
|
}
|
|
throw new Exception("Unknown color " ~ s);
|
|
}
|
|
|
|
static Color fromString(string s) {
|
|
s = s.stripInternal();
|
|
|
|
Color c;
|
|
c.a = 255;
|
|
|
|
// trying named colors via the static no-arg methods here
|
|
foreach(member; __traits(allMembers, Color)) {
|
|
static if(__traits(compiles, c = __traits(getMember, Color, member))) {
|
|
if(s == member)
|
|
return __traits(getMember, Color, member);
|
|
}
|
|
}
|
|
|
|
// try various notations borrowed from CSS (though a little extended)
|
|
|
|
// hsl(h,s,l,a) where h is degrees and s,l,a are 0 >= x <= 1.0
|
|
if(s.startsWithInternal("hsl(") || s.startsWithInternal("hsla(")) {
|
|
assert(s[$-1] == ')');
|
|
s = s[s.startsWithInternal("hsl(") ? 4 : 5 .. $ - 1]; // the closing paren
|
|
|
|
real[3] hsl;
|
|
ubyte a = 255;
|
|
|
|
auto parts = s.splitInternal(',');
|
|
foreach(i, part; parts) {
|
|
if(i < 3)
|
|
hsl[i] = toInternal!real(part.stripInternal);
|
|
else
|
|
a = cast(ubyte) (toInternal!real(part.stripInternal) * 255);
|
|
}
|
|
|
|
c = .fromHsl(hsl);
|
|
c.a = a;
|
|
|
|
return c;
|
|
}
|
|
|
|
// rgb(r,g,b,a) where r,g,b are 0-255 and a is 0-1.0
|
|
if(s.startsWithInternal("rgb(") || s.startsWithInternal("rgba(")) {
|
|
assert(s[$-1] == ')');
|
|
s = s[s.startsWithInternal("rgb(") ? 4 : 5 .. $ - 1]; // the closing paren
|
|
|
|
auto parts = s.splitInternal(',');
|
|
foreach(i, part; parts) {
|
|
// lol the loop-switch pattern
|
|
auto v = toInternal!real(part.stripInternal);
|
|
switch(i) {
|
|
case 0: // red
|
|
c.r = cast(ubyte) v;
|
|
break;
|
|
case 1:
|
|
c.g = cast(ubyte) v;
|
|
break;
|
|
case 2:
|
|
c.b = cast(ubyte) v;
|
|
break;
|
|
case 3:
|
|
c.a = cast(ubyte) (v * 255);
|
|
break;
|
|
default: // ignore
|
|
}
|
|
}
|
|
|
|
return c;
|
|
}
|
|
|
|
|
|
|
|
|
|
// otherwise let's try it as a hex string, really loosely
|
|
|
|
if(s.length && s[0] == '#')
|
|
s = s[1 .. $];
|
|
|
|
// not a built in... do it as a hex string
|
|
if(s.length >= 2) {
|
|
c.r = fromHexInternal(s[0 .. 2]);
|
|
s = s[2 .. $];
|
|
}
|
|
if(s.length >= 2) {
|
|
c.g = fromHexInternal(s[0 .. 2]);
|
|
s = s[2 .. $];
|
|
}
|
|
if(s.length >= 2) {
|
|
c.b = fromHexInternal(s[0 .. 2]);
|
|
s = s[2 .. $];
|
|
}
|
|
if(s.length >= 2) {
|
|
c.a = fromHexInternal(s[0 .. 2]);
|
|
s = s[2 .. $];
|
|
}
|
|
|
|
return c;
|
|
}
|
|
|
|
static Color fromHsl(real h, real s, real l) {
|
|
return .fromHsl(h, s, l);
|
|
}
|
|
}
|
|
|
|
private string toHexInternal(ubyte b) {
|
|
string s;
|
|
if(b < 16)
|
|
s ~= '0';
|
|
else {
|
|
ubyte t = (b & 0xf0) >> 4;
|
|
if(t >= 10)
|
|
s ~= 'A' + t - 10;
|
|
else
|
|
s ~= '0' + t;
|
|
b &= 0x0f;
|
|
}
|
|
if(b >= 10)
|
|
s ~= 'A' + b - 10;
|
|
else
|
|
s ~= '0' + b;
|
|
|
|
return s;
|
|
}
|
|
|
|
private ubyte fromHexInternal(string s) {
|
|
int result = 0;
|
|
|
|
int exp = 1;
|
|
//foreach(c; retro(s)) { // FIXME: retro doesn't work right in dtojs
|
|
foreach_reverse(c; s) {
|
|
if(c >= 'A' && c <= 'F')
|
|
result += exp * (c - 'A' + 10);
|
|
else if(c >= 'a' && c <= 'f')
|
|
result += exp * (c - 'a' + 10);
|
|
else if(c >= '0' && c <= '9')
|
|
result += exp * (c - '0');
|
|
else
|
|
// throw new Exception("invalid hex character: " ~ cast(char) c);
|
|
return 0;
|
|
|
|
exp *= 16;
|
|
}
|
|
|
|
return cast(ubyte) result;
|
|
}
|
|
|
|
|
|
Color fromHsl(real[3] hsl) {
|
|
return fromHsl(hsl[0], hsl[1], hsl[2]);
|
|
}
|
|
|
|
Color fromHsl(real h, real s, real l, real a = 255) {
|
|
h = h % 360;
|
|
|
|
real C = (1 - absInternal(2 * l - 1)) * s;
|
|
|
|
real hPrime = h / 60;
|
|
|
|
real X = C * (1 - absInternal(hPrime % 2 - 1));
|
|
|
|
real r, g, b;
|
|
|
|
if(h is real.nan)
|
|
r = g = b = 0;
|
|
else if (hPrime >= 0 && hPrime < 1) {
|
|
r = C;
|
|
g = X;
|
|
b = 0;
|
|
} else if (hPrime >= 1 && hPrime < 2) {
|
|
r = X;
|
|
g = C;
|
|
b = 0;
|
|
} else if (hPrime >= 2 && hPrime < 3) {
|
|
r = 0;
|
|
g = C;
|
|
b = X;
|
|
} else if (hPrime >= 3 && hPrime < 4) {
|
|
r = 0;
|
|
g = X;
|
|
b = C;
|
|
} else if (hPrime >= 4 && hPrime < 5) {
|
|
r = X;
|
|
g = 0;
|
|
b = C;
|
|
} else if (hPrime >= 5 && hPrime < 6) {
|
|
r = C;
|
|
g = 0;
|
|
b = X;
|
|
}
|
|
|
|
real m = l - C / 2;
|
|
|
|
r += m;
|
|
g += m;
|
|
b += m;
|
|
|
|
return Color(
|
|
cast(ubyte)(r * 255),
|
|
cast(ubyte)(g * 255),
|
|
cast(ubyte)(b * 255),
|
|
cast(ubyte)(a));
|
|
}
|
|
|
|
real[3] toHsl(Color c, bool useWeightedLightness = false) {
|
|
real r1 = cast(real) c.r / 255;
|
|
real g1 = cast(real) c.g / 255;
|
|
real b1 = cast(real) c.b / 255;
|
|
|
|
real maxColor = maxInternal(r1, g1, b1);
|
|
real minColor = minInternal(r1, g1, b1);
|
|
|
|
real L = (maxColor + minColor) / 2 ;
|
|
if(useWeightedLightness) {
|
|
// the colors don't affect the eye equally
|
|
// this is a little more accurate than plain HSL numbers
|
|
L = 0.2126*r1 + 0.7152*g1 + 0.0722*b1;
|
|
}
|
|
real S = 0;
|
|
real H = 0;
|
|
if(maxColor != minColor) {
|
|
if(L < 0.5) {
|
|
S = (maxColor - minColor) / (maxColor + minColor);
|
|
} else {
|
|
S = (maxColor - minColor) / (2.0 - maxColor - minColor);
|
|
}
|
|
if(r1 == maxColor) {
|
|
H = (g1-b1) / (maxColor - minColor);
|
|
} else if(g1 == maxColor) {
|
|
H = 2.0 + (b1 - r1) / (maxColor - minColor);
|
|
} else {
|
|
H = 4.0 + (r1 - g1) / (maxColor - minColor);
|
|
}
|
|
}
|
|
|
|
H = H * 60;
|
|
if(H < 0){
|
|
H += 360;
|
|
}
|
|
|
|
return [H, S, L];
|
|
}
|
|
|
|
|
|
Color lighten(Color c, real percentage) {
|
|
auto hsl = toHsl(c);
|
|
hsl[2] *= (1 + percentage);
|
|
if(hsl[2] > 1)
|
|
hsl[2] = 1;
|
|
return fromHsl(hsl);
|
|
}
|
|
|
|
Color darken(Color c, real percentage) {
|
|
auto hsl = toHsl(c);
|
|
hsl[2] *= (1 - percentage);
|
|
return fromHsl(hsl);
|
|
}
|
|
|
|
/// for light colors, call darken. for dark colors, call lighten.
|
|
/// The goal: get toward center grey.
|
|
Color moderate(Color c, real percentage) {
|
|
auto hsl = toHsl(c);
|
|
if(hsl[2] > 0.5)
|
|
hsl[2] *= (1 - percentage);
|
|
else {
|
|
if(hsl[2] <= 0.01) // if we are given black, moderating it means getting *something* out
|
|
hsl[2] = percentage;
|
|
else
|
|
hsl[2] *= (1 + percentage);
|
|
}
|
|
if(hsl[2] > 1)
|
|
hsl[2] = 1;
|
|
return fromHsl(hsl);
|
|
}
|
|
|
|
/// the opposite of moderate. Make darks darker and lights lighter
|
|
Color extremify(Color c, real percentage) {
|
|
auto hsl = toHsl(c, true);
|
|
if(hsl[2] < 0.5)
|
|
hsl[2] *= (1 - percentage);
|
|
else
|
|
hsl[2] *= (1 + percentage);
|
|
if(hsl[2] > 1)
|
|
hsl[2] = 1;
|
|
return fromHsl(hsl);
|
|
}
|
|
|
|
/// Move around the lightness wheel, trying not to break on moderate things
|
|
Color oppositeLightness(Color c) {
|
|
auto hsl = toHsl(c);
|
|
|
|
auto original = hsl[2];
|
|
|
|
if(original > 0.4 && original < 0.6)
|
|
hsl[2] = 0.8 - original; // so it isn't quite the same
|
|
else
|
|
hsl[2] = 1 - original;
|
|
|
|
return fromHsl(hsl);
|
|
}
|
|
|
|
/// Try to determine a text color - either white or black - based on the input
|
|
Color makeTextColor(Color c) {
|
|
auto hsl = toHsl(c, true); // give green a bonus for contrast
|
|
if(hsl[2] > 0.71)
|
|
return Color(0, 0, 0);
|
|
else
|
|
return Color(255, 255, 255);
|
|
}
|
|
|
|
Color setLightness(Color c, real lightness) {
|
|
auto hsl = toHsl(c);
|
|
hsl[2] = lightness;
|
|
return fromHsl(hsl);
|
|
}
|
|
|
|
|
|
|
|
Color rotateHue(Color c, real degrees) {
|
|
auto hsl = toHsl(c);
|
|
hsl[0] += degrees;
|
|
return fromHsl(hsl);
|
|
}
|
|
|
|
Color setHue(Color c, real hue) {
|
|
auto hsl = toHsl(c);
|
|
hsl[0] = hue;
|
|
return fromHsl(hsl);
|
|
}
|
|
|
|
Color desaturate(Color c, real percentage) {
|
|
auto hsl = toHsl(c);
|
|
hsl[1] *= (1 - percentage);
|
|
return fromHsl(hsl);
|
|
}
|
|
|
|
Color saturate(Color c, real percentage) {
|
|
auto hsl = toHsl(c);
|
|
hsl[1] *= (1 + percentage);
|
|
if(hsl[1] > 1)
|
|
hsl[1] = 1;
|
|
return fromHsl(hsl);
|
|
}
|
|
|
|
Color setSaturation(Color c, real saturation) {
|
|
auto hsl = toHsl(c);
|
|
hsl[1] = saturation;
|
|
return fromHsl(hsl);
|
|
}
|
|
|
|
|
|
/*
|
|
void main(string[] args) {
|
|
auto color1 = toHsl(Color(255, 0, 0));
|
|
auto color = fromHsl(color1[0] + 60, color1[1], color1[2]);
|
|
|
|
writefln("#%02x%02x%02x", color.r, color.g, color.b);
|
|
}
|
|
*/
|
|
|
|
/* Color algebra functions */
|
|
|
|
/* Alpha putpixel looks like this:
|
|
|
|
void putPixel(Image i, Color c) {
|
|
Color b;
|
|
b.r = i.data[(y * i.width + x) * bpp + 0];
|
|
b.g = i.data[(y * i.width + x) * bpp + 1];
|
|
b.b = i.data[(y * i.width + x) * bpp + 2];
|
|
b.a = i.data[(y * i.width + x) * bpp + 3];
|
|
|
|
float ca = cast(float) c.a / 255;
|
|
|
|
i.data[(y * i.width + x) * bpp + 0] = alpha(c.r, ca, b.r);
|
|
i.data[(y * i.width + x) * bpp + 1] = alpha(c.g, ca, b.g);
|
|
i.data[(y * i.width + x) * bpp + 2] = alpha(c.b, ca, b.b);
|
|
i.data[(y * i.width + x) * bpp + 3] = alpha(c.a, ca, b.a);
|
|
}
|
|
|
|
ubyte alpha(ubyte c1, float alpha, ubyte onto) {
|
|
auto got = (1 - alpha) * onto + alpha * c1;
|
|
|
|
if(got > 255)
|
|
return 255;
|
|
return cast(ubyte) got;
|
|
}
|
|
|
|
So, given the background color and the resultant color, what was
|
|
composited on to it?
|
|
*/
|
|
|
|
ubyte unalpha(ubyte colorYouHave, float alpha, ubyte backgroundColor) {
|
|
// resultingColor = (1-alpha) * backgroundColor + alpha * answer
|
|
auto resultingColorf = cast(float) colorYouHave;
|
|
auto backgroundColorf = cast(float) backgroundColor;
|
|
|
|
auto answer = (resultingColorf - backgroundColorf + alpha * backgroundColorf) / alpha;
|
|
if(answer > 255)
|
|
return 255;
|
|
if(answer < 0)
|
|
return 0;
|
|
return cast(ubyte) answer;
|
|
}
|
|
|
|
ubyte makeAlpha(ubyte colorYouHave, ubyte backgroundColor/*, ubyte foreground = 0x00*/) {
|
|
//auto foregroundf = cast(float) foreground;
|
|
auto foregroundf = 0.00f;
|
|
auto colorYouHavef = cast(float) colorYouHave;
|
|
auto backgroundColorf = cast(float) backgroundColor;
|
|
|
|
// colorYouHave = backgroundColorf - alpha * backgroundColorf + alpha * foregroundf
|
|
auto alphaf = 1 - colorYouHave / backgroundColorf;
|
|
alphaf *= 255;
|
|
|
|
if(alphaf < 0)
|
|
return 0;
|
|
if(alphaf > 255)
|
|
return 255;
|
|
return cast(ubyte) alphaf;
|
|
}
|
|
|
|
|
|
int fromHex(string s) {
|
|
int result = 0;
|
|
|
|
int exp = 1;
|
|
// foreach(c; retro(s)) {
|
|
foreach_reverse(c; s) {
|
|
if(c >= 'A' && c <= 'F')
|
|
result += exp * (c - 'A' + 10);
|
|
else if(c >= 'a' && c <= 'f')
|
|
result += exp * (c - 'a' + 10);
|
|
else if(c >= '0' && c <= '9')
|
|
result += exp * (c - '0');
|
|
else
|
|
throw new Exception("invalid hex character: " ~ cast(char) c);
|
|
|
|
exp *= 16;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
Color colorFromString(string s) {
|
|
if(s.length == 0)
|
|
return Color(0,0,0,255);
|
|
if(s[0] == '#')
|
|
s = s[1..$];
|
|
assert(s.length == 6 || s.length == 8);
|
|
|
|
Color c;
|
|
|
|
c.r = cast(ubyte) fromHex(s[0..2]);
|
|
c.g = cast(ubyte) fromHex(s[2..4]);
|
|
c.b = cast(ubyte) fromHex(s[4..6]);
|
|
if(s.length == 8)
|
|
c.a = cast(ubyte) fromHex(s[6..8]);
|
|
else
|
|
c.a = 255;
|
|
|
|
return c;
|
|
}
|
|
|
|
/*
|
|
import browser.window;
|
|
import std.conv;
|
|
void main() {
|
|
import browser.document;
|
|
foreach(ele; document.querySelectorAll("input")) {
|
|
ele.addEventListener("change", {
|
|
auto h = toInternal!real(document.querySelector("input[name=h]").value);
|
|
auto s = toInternal!real(document.querySelector("input[name=s]").value);
|
|
auto l = toInternal!real(document.querySelector("input[name=l]").value);
|
|
|
|
Color c = Color.fromHsl(h, s, l);
|
|
|
|
auto e = document.getElementById("example");
|
|
e.style.backgroundColor = c.toCssString();
|
|
|
|
// JSElement __js_this;
|
|
// __js_this.style.backgroundColor = c.toCssString();
|
|
}, false);
|
|
}
|
|
}
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
This provides two image classes and a bunch of functions that work on them.
|
|
|
|
Why are they separate classes? I think the operations on the two of them
|
|
are necessarily different. There's a whole bunch of operations that only
|
|
really work on truecolor (blurs, gradients), and a few that only work
|
|
on indexed images (palette swaps).
|
|
|
|
Even putpixel is pretty different. On indexed, it is a palette entry's
|
|
index number. On truecolor, it is the actual color.
|
|
|
|
A greyscale image is the weird thing in the middle. It is truecolor, but
|
|
fits in the same size as indexed. Still, I'd say it is a specialization
|
|
of truecolor.
|
|
|
|
There is a subset that works on both
|
|
|
|
*/
|
|
|
|
|
|
interface MemoryImage {
|
|
//IndexedImage convertToIndexedImage() const;
|
|
//TrueColorImage convertToTrueColor() const;
|
|
TrueColorImage getAsTrueColorImage();
|
|
}
|
|
|
|
class IndexedImage : MemoryImage {
|
|
bool hasAlpha;
|
|
Color[] palette;
|
|
ubyte[] data;
|
|
|
|
int width;
|
|
int height;
|
|
|
|
this(int w, int h) {
|
|
width = w;
|
|
height = h;
|
|
data = new ubyte[w*h];
|
|
}
|
|
|
|
/*
|
|
void resize(int w, int h, bool scale) {
|
|
|
|
}
|
|
*/
|
|
|
|
override TrueColorImage getAsTrueColorImage() {
|
|
return convertToTrueColor();
|
|
}
|
|
|
|
TrueColorImage convertToTrueColor() const {
|
|
auto tci = new TrueColorImage(width, height);
|
|
foreach(i, b; data) {
|
|
tci.imageData.colors[i] = palette[b];
|
|
}
|
|
return tci;
|
|
}
|
|
|
|
ubyte getOrAddColor(Color c) {
|
|
foreach(i, co; palette) {
|
|
if(c == co)
|
|
return cast(ubyte) i;
|
|
}
|
|
|
|
return addColor(c);
|
|
}
|
|
|
|
int numColors() const {
|
|
return palette.length;
|
|
}
|
|
|
|
ubyte addColor(Color c) {
|
|
assert(palette.length < 256);
|
|
if(c.a != 255)
|
|
hasAlpha = true;
|
|
palette ~= c;
|
|
|
|
return cast(ubyte) (palette.length - 1);
|
|
}
|
|
}
|
|
|
|
class TrueColorImage : MemoryImage {
|
|
// bool hasAlpha;
|
|
// bool isGreyscale;
|
|
//ubyte[] data; // stored as rgba quads, upper left to right to bottom
|
|
union Data {
|
|
ubyte[] bytes;
|
|
Color[] colors;
|
|
|
|
static assert(Color.sizeof == 4);
|
|
}
|
|
|
|
Data imageData;
|
|
alias imageData.bytes data;
|
|
|
|
int width;
|
|
int height;
|
|
|
|
this(int w, int h) {
|
|
width = w;
|
|
height = h;
|
|
imageData.bytes = new ubyte[w*h*4];
|
|
}
|
|
|
|
override TrueColorImage getAsTrueColorImage() {
|
|
return this;
|
|
}
|
|
|
|
/+
|
|
IndexedImage convertToIndexedImage(int maxColors = 256) {
|
|
|
|
}
|
|
+/
|
|
}
|