arsd/color.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) {
}
+/
}