1 /// 2 module arsd.color; 3 4 @safe: 5 6 // importing phobos explodes the size of this code 10x, so not doing it. 7 8 private { 9 real toInternal(T)(string s) { 10 real accumulator = 0.0; 11 size_t i = s.length; 12 foreach(idx, c; s) { 13 if(c >= '0' && c <= '9') { 14 accumulator *= 10; 15 accumulator += c - '0'; 16 } else if(c == '.') { 17 i = idx + 1; 18 break; 19 } else 20 throw new Exception("bad char to make real from " ~ s); 21 } 22 23 real accumulator2 = 0.0; 24 real count = 1; 25 foreach(c; s[i .. $]) { 26 if(c >= '0' && c <= '9') { 27 accumulator2 *= 10; 28 accumulator2 += c - '0'; 29 count *= 10; 30 } else 31 throw new Exception("bad char to make real from " ~ s); 32 } 33 34 return accumulator + accumulator2 / count; 35 } 36 37 @trusted 38 string toInternal(T)(int a) { 39 if(a == 0) 40 return "0"; 41 char[] ret; 42 while(a) { 43 ret ~= (a % 10) + '0'; 44 a /= 10; 45 } 46 for(int i = 0; i < ret.length / 2; i++) { 47 char c = ret[i]; 48 ret[i] = ret[$ - i - 1]; 49 ret[$ - i - 1] = c; 50 } 51 return cast(string) ret; 52 } 53 string toInternal(T)(real a) { 54 // a simplifying assumption here is the fact that we only use this in one place: toInternal!string(cast(real) a / 255) 55 // thus we know this will always be between 0.0 and 1.0, inclusive. 56 if(a <= 0.0) 57 return "0.0"; 58 if(a >= 1.0) 59 return "1.0"; 60 string ret = "0."; 61 // I wonder if I can handle round off error any better. Phobos does, but that isn't worth 100 KB of code. 62 int amt = cast(int)(a * 1000); 63 return ret ~ toInternal!string(amt); 64 } 65 66 nothrow @safe @nogc pure 67 real absInternal(real a) { return a < 0 ? -a : a; } 68 nothrow @safe @nogc pure 69 real minInternal(real a, real b, real c) { 70 auto m = a; 71 if(b < m) m = b; 72 if(c < m) m = c; 73 return m; 74 } 75 nothrow @safe @nogc pure 76 real maxInternal(real a, real b, real c) { 77 auto m = a; 78 if(b > m) m = b; 79 if(c > m) m = c; 80 return m; 81 } 82 nothrow @safe @nogc pure 83 bool startsWithInternal(string a, string b) { 84 return (a.length >= b.length && a[0 .. b.length] == b); 85 } 86 string[] splitInternal(string a, char c) { 87 string[] ret; 88 size_t previous = 0; 89 foreach(i, char ch; a) { 90 if(ch == c) { 91 ret ~= a[previous .. i]; 92 previous = i + 1; 93 } 94 } 95 if(previous != a.length) 96 ret ~= a[previous .. $]; 97 return ret; 98 } 99 nothrow @safe @nogc pure 100 string stripInternal(string s) { 101 foreach(i, char c; s) 102 if(c != ' ' && c != '\t' && c != '\n') { 103 s = s[i .. $]; 104 break; 105 } 106 for(int a = cast(int)(s.length - 1); a > 0; a--) { 107 char c = s[a]; 108 if(c != ' ' && c != '\t' && c != '\n') { 109 s = s[0 .. a + 1]; 110 break; 111 } 112 } 113 114 return s; 115 } 116 } 117 118 // done with mini-phobos 119 120 /// Represents an RGBA color 121 struct Color { 122 @safe: 123 /++ 124 The color components are available as a static array, individual bytes, and a uint inside this union. 125 126 Since it is anonymous, you can use the inner members' names directly. 127 +/ 128 union { 129 ubyte[4] components; /// [r, g, b, a] 130 131 /// Holder for rgba individual components. 132 struct { 133 ubyte r; /// red 134 ubyte g; /// green 135 ubyte b; /// blue 136 ubyte a; /// alpha. 255 == opaque 137 } 138 139 uint asUint; /// The components as a single 32 bit value (beware of endian issues!) 140 } 141 142 /++ 143 Like the constructor, but this makes sure they are in range before casting. If they are out of range, it saturates: anything less than zero becomes zero and anything greater than 255 becomes 255. 144 +/ 145 nothrow pure 146 static Color fromIntegers(int red, int green, int blue, int alpha = 255) { 147 return Color(clampToByte(red), clampToByte(green), clampToByte(blue), clampToByte(alpha)); 148 } 149 150 /// Construct a color with the given values. They should be in range 0 <= x <= 255, where 255 is maximum intensity and 0 is minimum intensity. 151 nothrow pure @nogc 152 this(int red, int green, int blue, int alpha = 255) { 153 this.r = cast(ubyte) red; 154 this.g = cast(ubyte) green; 155 this.b = cast(ubyte) blue; 156 this.a = cast(ubyte) alpha; 157 } 158 159 /// Static convenience functions for common color names 160 nothrow pure @nogc 161 static Color transparent() { return Color(0, 0, 0, 0); } 162 /// Ditto 163 nothrow pure @nogc 164 static Color white() { return Color(255, 255, 255); } 165 /// Ditto 166 nothrow pure @nogc 167 static Color gray() { return Color(128, 128, 128); } 168 /// Ditto 169 nothrow pure @nogc 170 static Color black() { return Color(0, 0, 0); } 171 /// Ditto 172 nothrow pure @nogc 173 static Color red() { return Color(255, 0, 0); } 174 /// Ditto 175 nothrow pure @nogc 176 static Color green() { return Color(0, 255, 0); } 177 /// Ditto 178 nothrow pure @nogc 179 static Color blue() { return Color(0, 0, 255); } 180 /// Ditto 181 nothrow pure @nogc 182 static Color yellow() { return Color(255, 255, 0); } 183 /// Ditto 184 nothrow pure @nogc 185 static Color teal() { return Color(0, 255, 255); } 186 /// Ditto 187 nothrow pure @nogc 188 static Color purple() { return Color(255, 0, 255); } 189 /// Ditto 190 nothrow pure @nogc 191 static Color brown() { return Color(128, 64, 0); } 192 193 /* 194 ubyte[4] toRgbaArray() { 195 return [r,g,b,a]; 196 } 197 */ 198 199 /// Return black-and-white color 200 Color toBW() () { 201 int intens = clampToByte(cast(int)(0.2126*r+0.7152*g+0.0722*b)); 202 return Color(intens, intens, intens, a); 203 } 204 205 /// Makes a string that matches CSS syntax for websites 206 string toCssString() const { 207 if(a == 255) 208 return "#" ~ toHexInternal(r) ~ toHexInternal(g) ~ toHexInternal(b); 209 else { 210 return "rgba("~toInternal!string(r)~", "~toInternal!string(g)~", "~toInternal!string(b)~", "~toInternal!string(cast(real)a / 255.0)~")"; 211 } 212 } 213 214 /// Makes a hex string RRGGBBAA (aa only present if it is not 255) 215 string toString() const { 216 if(a == 255) 217 return toCssString()[1 .. $]; 218 else 219 return toRgbaHexString(); 220 } 221 222 /// returns RRGGBBAA, even if a== 255 223 string toRgbaHexString() const { 224 return toHexInternal(r) ~ toHexInternal(g) ~ toHexInternal(b) ~ toHexInternal(a); 225 } 226 227 /// Gets a color by name, iff the name is one of the static members listed above 228 static Color fromNameString(string s) { 229 Color c; 230 foreach(member; __traits(allMembers, Color)) { 231 static if(__traits(compiles, c = __traits(getMember, Color, member))) { 232 if(s == member) 233 return __traits(getMember, Color, member); 234 } 235 } 236 throw new Exception("Unknown color " ~ s); 237 } 238 239 /// Reads a CSS style string to get the color. Understands #rrggbb, rgba(), hsl(), and rrggbbaa 240 static Color fromString(string s) { 241 s = s.stripInternal(); 242 243 Color c; 244 c.a = 255; 245 246 // trying named colors via the static no-arg methods here 247 foreach(member; __traits(allMembers, Color)) { 248 static if(__traits(compiles, c = __traits(getMember, Color, member))) { 249 if(s == member) 250 return __traits(getMember, Color, member); 251 } 252 } 253 254 // try various notations borrowed from CSS (though a little extended) 255 256 // hsl(h,s,l,a) where h is degrees and s,l,a are 0 >= x <= 1.0 257 if(s.startsWithInternal("hsl(") || s.startsWithInternal("hsla(")) { 258 assert(s[$-1] == ')'); 259 s = s[s.startsWithInternal("hsl(") ? 4 : 5 .. $ - 1]; // the closing paren 260 261 real[3] hsl; 262 ubyte a = 255; 263 264 auto parts = s.splitInternal(','); 265 foreach(i, part; parts) { 266 if(i < 3) 267 hsl[i] = toInternal!real(part.stripInternal); 268 else 269 a = clampToByte(cast(int) (toInternal!real(part.stripInternal) * 255)); 270 } 271 272 c = .fromHsl(hsl); 273 c.a = a; 274 275 return c; 276 } 277 278 // rgb(r,g,b,a) where r,g,b are 0-255 and a is 0-1.0 279 if(s.startsWithInternal("rgb(") || s.startsWithInternal("rgba(")) { 280 assert(s[$-1] == ')'); 281 s = s[s.startsWithInternal("rgb(") ? 4 : 5 .. $ - 1]; // the closing paren 282 283 auto parts = s.splitInternal(','); 284 foreach(i, part; parts) { 285 // lol the loop-switch pattern 286 auto v = toInternal!real(part.stripInternal); 287 switch(i) { 288 case 0: // red 289 c.r = clampToByte(cast(int) v); 290 break; 291 case 1: 292 c.g = clampToByte(cast(int) v); 293 break; 294 case 2: 295 c.b = clampToByte(cast(int) v); 296 break; 297 case 3: 298 c.a = clampToByte(cast(int) (v * 255)); 299 break; 300 default: // ignore 301 } 302 } 303 304 return c; 305 } 306 307 308 309 310 // otherwise let's try it as a hex string, really loosely 311 312 if(s.length && s[0] == '#') 313 s = s[1 .. $]; 314 315 // not a built in... do it as a hex string 316 if(s.length >= 2) { 317 c.r = fromHexInternal(s[0 .. 2]); 318 s = s[2 .. $]; 319 } 320 if(s.length >= 2) { 321 c.g = fromHexInternal(s[0 .. 2]); 322 s = s[2 .. $]; 323 } 324 if(s.length >= 2) { 325 c.b = fromHexInternal(s[0 .. 2]); 326 s = s[2 .. $]; 327 } 328 if(s.length >= 2) { 329 c.a = fromHexInternal(s[0 .. 2]); 330 s = s[2 .. $]; 331 } 332 333 return c; 334 } 335 336 /// from hsl 337 static Color fromHsl(real h, real s, real l) { 338 return .fromHsl(h, s, l); 339 } 340 341 // this is actually branch-less for ints on x86, and even for longs on x86_64 342 static ubyte clampToByte(T) (T n) pure nothrow @safe @nogc if (__traits(isIntegral, T)) { 343 static if (__VERSION__ > 2067) pragma(inline, true); 344 static if (T.sizeof == 2 || T.sizeof == 4) { 345 static if (__traits(isUnsigned, T)) { 346 return cast(ubyte)(n&0xff|(255-((-cast(int)(n < 256))>>24))); 347 } else { 348 n &= -cast(int)(n >= 0); 349 return cast(ubyte)(n|((255-cast(int)n)>>31)); 350 } 351 } else static if (T.sizeof == 1) { 352 static assert(__traits(isUnsigned, T), "clampToByte: signed byte? no, really?"); 353 return cast(ubyte)n; 354 } else static if (T.sizeof == 8) { 355 static if (__traits(isUnsigned, T)) { 356 return cast(ubyte)(n&0xff|(255-((-cast(long)(n < 256))>>56))); 357 } else { 358 n &= -cast(long)(n >= 0); 359 return cast(ubyte)(n|((255-cast(long)n)>>63)); 360 } 361 } else { 362 static assert(false, "clampToByte: integer too big"); 363 } 364 } 365 366 /** this mixin can be used to alphablend two `uint` colors; 367 * `colu32name` is variable that holds color to blend, 368 * `destu32name` is variable that holds "current" color (from surface, for example). 369 * alpha value of `destu32name` doesn't matter. 370 * alpha value of `colu32name` means: 255 for replace color, 0 for keep `destu32name`. 371 * 372 * WARNING! This function does blending in RGB space, and RGB space is not linear! 373 */ 374 public enum ColorBlendMixinStr(string colu32name, string destu32name) = "{ 375 immutable uint a_tmp_ = (256-(255-(("~colu32name~")>>24)))&(-(1-(((255-(("~colu32name~")>>24))+1)>>8))); // to not loose bits, but 255 should become 0 376 immutable uint dc_tmp_ = ("~destu32name~")&0xffffff; 377 immutable uint srb_tmp_ = (("~colu32name~")&0xff00ff); 378 immutable uint sg_tmp_ = (("~colu32name~")&0x00ff00); 379 immutable uint drb_tmp_ = (dc_tmp_&0xff00ff); 380 immutable uint dg_tmp_ = (dc_tmp_&0x00ff00); 381 immutable uint orb_tmp_ = (drb_tmp_+(((srb_tmp_-drb_tmp_)*a_tmp_+0x800080)>>8))&0xff00ff; 382 immutable uint og_tmp_ = (dg_tmp_+(((sg_tmp_-dg_tmp_)*a_tmp_+0x008000)>>8))&0x00ff00; 383 ("~destu32name~") = (orb_tmp_|og_tmp_)|0xff000000; /*&0xffffff;*/ 384 }"; 385 386 387 /// Perform alpha-blending of `fore` to this color, return new color. 388 /// WARNING! This function does blending in RGB space, and RGB space is not linear! 389 Color alphaBlend (Color fore) const pure nothrow @trusted @nogc { 390 static if (__VERSION__ > 2067) pragma(inline, true); 391 Color res; 392 res.asUint = asUint; 393 mixin(ColorBlendMixinStr!("fore.asUint", "res.asUint")); 394 return res; 395 } 396 } 397 398 nothrow @safe 399 private string toHexInternal(ubyte b) { 400 string s; 401 if(b < 16) 402 s ~= '0'; 403 else { 404 ubyte t = (b & 0xf0) >> 4; 405 if(t >= 10) 406 s ~= 'A' + t - 10; 407 else 408 s ~= '0' + t; 409 b &= 0x0f; 410 } 411 if(b >= 10) 412 s ~= 'A' + b - 10; 413 else 414 s ~= '0' + b; 415 416 return s; 417 } 418 419 nothrow @safe @nogc pure 420 private ubyte fromHexInternal(string s) { 421 int result = 0; 422 423 int exp = 1; 424 //foreach(c; retro(s)) { // FIXME: retro doesn't work right in dtojs 425 foreach_reverse(c; s) { 426 if(c >= 'A' && c <= 'F') 427 result += exp * (c - 'A' + 10); 428 else if(c >= 'a' && c <= 'f') 429 result += exp * (c - 'a' + 10); 430 else if(c >= '0' && c <= '9') 431 result += exp * (c - '0'); 432 else 433 // throw new Exception("invalid hex character: " ~ cast(char) c); 434 return 0; 435 436 exp *= 16; 437 } 438 439 return cast(ubyte) result; 440 } 441 442 /// Converts hsl to rgb 443 Color fromHsl(real[3] hsl) { 444 return fromHsl(hsl[0], hsl[1], hsl[2]); 445 } 446 447 /// Converts hsl to rgb 448 Color fromHsl(real h, real s, real l, real a = 255) { 449 h = h % 360; 450 451 real C = (1 - absInternal(2 * l - 1)) * s; 452 453 real hPrime = h / 60; 454 455 real X = C * (1 - absInternal(hPrime % 2 - 1)); 456 457 real r, g, b; 458 459 if(h is real.nan) 460 r = g = b = 0; 461 else if (hPrime >= 0 && hPrime < 1) { 462 r = C; 463 g = X; 464 b = 0; 465 } else if (hPrime >= 1 && hPrime < 2) { 466 r = X; 467 g = C; 468 b = 0; 469 } else if (hPrime >= 2 && hPrime < 3) { 470 r = 0; 471 g = C; 472 b = X; 473 } else if (hPrime >= 3 && hPrime < 4) { 474 r = 0; 475 g = X; 476 b = C; 477 } else if (hPrime >= 4 && hPrime < 5) { 478 r = X; 479 g = 0; 480 b = C; 481 } else if (hPrime >= 5 && hPrime < 6) { 482 r = C; 483 g = 0; 484 b = X; 485 } 486 487 real m = l - C / 2; 488 489 r += m; 490 g += m; 491 b += m; 492 493 return Color( 494 cast(int)(r * 255), 495 cast(int)(g * 255), 496 cast(int)(b * 255), 497 cast(int)(a)); 498 } 499 500 /// Converts an RGB color into an HSL triplet. useWeightedLightness will try to get a better value for luminosity for the human eye, which is more sensitive to green than red and more to red than blue. If it is false, it just does average of the rgb. 501 real[3] toHsl(Color c, bool useWeightedLightness = false) { 502 real r1 = cast(real) c.r / 255; 503 real g1 = cast(real) c.g / 255; 504 real b1 = cast(real) c.b / 255; 505 506 real maxColor = maxInternal(r1, g1, b1); 507 real minColor = minInternal(r1, g1, b1); 508 509 real L = (maxColor + minColor) / 2 ; 510 if(useWeightedLightness) { 511 // the colors don't affect the eye equally 512 // this is a little more accurate than plain HSL numbers 513 L = 0.2126*r1 + 0.7152*g1 + 0.0722*b1; 514 } 515 real S = 0; 516 real H = 0; 517 if(maxColor != minColor) { 518 if(L < 0.5) { 519 S = (maxColor - minColor) / (maxColor + minColor); 520 } else { 521 S = (maxColor - minColor) / (2.0 - maxColor - minColor); 522 } 523 if(r1 == maxColor) { 524 H = (g1-b1) / (maxColor - minColor); 525 } else if(g1 == maxColor) { 526 H = 2.0 + (b1 - r1) / (maxColor - minColor); 527 } else { 528 H = 4.0 + (r1 - g1) / (maxColor - minColor); 529 } 530 } 531 532 H = H * 60; 533 if(H < 0){ 534 H += 360; 535 } 536 537 return [H, S, L]; 538 } 539 540 /// . 541 Color lighten(Color c, real percentage) { 542 auto hsl = toHsl(c); 543 hsl[2] *= (1 + percentage); 544 if(hsl[2] > 1) 545 hsl[2] = 1; 546 return fromHsl(hsl); 547 } 548 549 /// . 550 Color darken(Color c, real percentage) { 551 auto hsl = toHsl(c); 552 hsl[2] *= (1 - percentage); 553 return fromHsl(hsl); 554 } 555 556 /// for light colors, call darken. for dark colors, call lighten. 557 /// The goal: get toward center grey. 558 Color moderate(Color c, real percentage) { 559 auto hsl = toHsl(c); 560 if(hsl[2] > 0.5) 561 hsl[2] *= (1 - percentage); 562 else { 563 if(hsl[2] <= 0.01) // if we are given black, moderating it means getting *something* out 564 hsl[2] = percentage; 565 else 566 hsl[2] *= (1 + percentage); 567 } 568 if(hsl[2] > 1) 569 hsl[2] = 1; 570 return fromHsl(hsl); 571 } 572 573 /// the opposite of moderate. Make darks darker and lights lighter 574 Color extremify(Color c, real percentage) { 575 auto hsl = toHsl(c, true); 576 if(hsl[2] < 0.5) 577 hsl[2] *= (1 - percentage); 578 else 579 hsl[2] *= (1 + percentage); 580 if(hsl[2] > 1) 581 hsl[2] = 1; 582 return fromHsl(hsl); 583 } 584 585 /// Move around the lightness wheel, trying not to break on moderate things 586 Color oppositeLightness(Color c) { 587 auto hsl = toHsl(c); 588 589 auto original = hsl[2]; 590 591 if(original > 0.4 && original < 0.6) 592 hsl[2] = 0.8 - original; // so it isn't quite the same 593 else 594 hsl[2] = 1 - original; 595 596 return fromHsl(hsl); 597 } 598 599 /// Try to determine a text color - either white or black - based on the input 600 Color makeTextColor(Color c) { 601 auto hsl = toHsl(c, true); // give green a bonus for contrast 602 if(hsl[2] > 0.71) 603 return Color(0, 0, 0); 604 else 605 return Color(255, 255, 255); 606 } 607 608 // These provide functional access to hsl manipulation; useful if you need a delegate 609 610 Color setLightness(Color c, real lightness) { 611 auto hsl = toHsl(c); 612 hsl[2] = lightness; 613 return fromHsl(hsl); 614 } 615 616 617 /// 618 Color rotateHue(Color c, real degrees) { 619 auto hsl = toHsl(c); 620 hsl[0] += degrees; 621 return fromHsl(hsl); 622 } 623 624 /// 625 Color setHue(Color c, real hue) { 626 auto hsl = toHsl(c); 627 hsl[0] = hue; 628 return fromHsl(hsl); 629 } 630 631 /// 632 Color desaturate(Color c, real percentage) { 633 auto hsl = toHsl(c); 634 hsl[1] *= (1 - percentage); 635 return fromHsl(hsl); 636 } 637 638 /// 639 Color saturate(Color c, real percentage) { 640 auto hsl = toHsl(c); 641 hsl[1] *= (1 + percentage); 642 if(hsl[1] > 1) 643 hsl[1] = 1; 644 return fromHsl(hsl); 645 } 646 647 /// 648 Color setSaturation(Color c, real saturation) { 649 auto hsl = toHsl(c); 650 hsl[1] = saturation; 651 return fromHsl(hsl); 652 } 653 654 655 /* 656 void main(string[] args) { 657 auto color1 = toHsl(Color(255, 0, 0)); 658 auto color = fromHsl(color1[0] + 60, color1[1], color1[2]); 659 660 writefln("#%02x%02x%02x", color.r, color.g, color.b); 661 } 662 */ 663 664 /* Color algebra functions */ 665 666 /* Alpha putpixel looks like this: 667 668 void putPixel(Image i, Color c) { 669 Color b; 670 b.r = i.data[(y * i.width + x) * bpp + 0]; 671 b.g = i.data[(y * i.width + x) * bpp + 1]; 672 b.b = i.data[(y * i.width + x) * bpp + 2]; 673 b.a = i.data[(y * i.width + x) * bpp + 3]; 674 675 float ca = cast(float) c.a / 255; 676 677 i.data[(y * i.width + x) * bpp + 0] = alpha(c.r, ca, b.r); 678 i.data[(y * i.width + x) * bpp + 1] = alpha(c.g, ca, b.g); 679 i.data[(y * i.width + x) * bpp + 2] = alpha(c.b, ca, b.b); 680 i.data[(y * i.width + x) * bpp + 3] = alpha(c.a, ca, b.a); 681 } 682 683 ubyte alpha(ubyte c1, float alpha, ubyte onto) { 684 auto got = (1 - alpha) * onto + alpha * c1; 685 686 if(got > 255) 687 return 255; 688 return cast(ubyte) got; 689 } 690 691 So, given the background color and the resultant color, what was 692 composited on to it? 693 */ 694 695 /// 696 ubyte unalpha(ubyte colorYouHave, float alpha, ubyte backgroundColor) { 697 // resultingColor = (1-alpha) * backgroundColor + alpha * answer 698 auto resultingColorf = cast(float) colorYouHave; 699 auto backgroundColorf = cast(float) backgroundColor; 700 701 auto answer = (resultingColorf - backgroundColorf + alpha * backgroundColorf) / alpha; 702 return Color.clampToByte(cast(int) answer); 703 } 704 705 /// 706 ubyte makeAlpha(ubyte colorYouHave, ubyte backgroundColor/*, ubyte foreground = 0x00*/) { 707 //auto foregroundf = cast(float) foreground; 708 auto foregroundf = 0.00f; 709 auto colorYouHavef = cast(float) colorYouHave; 710 auto backgroundColorf = cast(float) backgroundColor; 711 712 // colorYouHave = backgroundColorf - alpha * backgroundColorf + alpha * foregroundf 713 auto alphaf = 1 - colorYouHave / backgroundColorf; 714 alphaf *= 255; 715 716 return Color.clampToByte(cast(int) alphaf); 717 } 718 719 720 int fromHex(string s) { 721 int result = 0; 722 723 int exp = 1; 724 // foreach(c; retro(s)) { 725 foreach_reverse(c; s) { 726 if(c >= 'A' && c <= 'F') 727 result += exp * (c - 'A' + 10); 728 else if(c >= 'a' && c <= 'f') 729 result += exp * (c - 'a' + 10); 730 else if(c >= '0' && c <= '9') 731 result += exp * (c - '0'); 732 else 733 throw new Exception("invalid hex character: " ~ cast(char) c); 734 735 exp *= 16; 736 } 737 738 return result; 739 } 740 741 /// 742 Color colorFromString(string s) { 743 if(s.length == 0) 744 return Color(0,0,0,255); 745 if(s[0] == '#') 746 s = s[1..$]; 747 assert(s.length == 6 || s.length == 8); 748 749 Color c; 750 751 c.r = cast(ubyte) fromHex(s[0..2]); 752 c.g = cast(ubyte) fromHex(s[2..4]); 753 c.b = cast(ubyte) fromHex(s[4..6]); 754 if(s.length == 8) 755 c.a = cast(ubyte) fromHex(s[6..8]); 756 else 757 c.a = 255; 758 759 return c; 760 } 761 762 /* 763 import browser.window; 764 import std.conv; 765 void main() { 766 import browser.document; 767 foreach(ele; document.querySelectorAll("input")) { 768 ele.addEventListener("change", { 769 auto h = toInternal!real(document.querySelector("input[name=h]").value); 770 auto s = toInternal!real(document.querySelector("input[name=s]").value); 771 auto l = toInternal!real(document.querySelector("input[name=l]").value); 772 773 Color c = Color.fromHsl(h, s, l); 774 775 auto e = document.getElementById("example"); 776 e.style.backgroundColor = c.toCssString(); 777 778 // JSElement __js_this; 779 // __js_this.style.backgroundColor = c.toCssString(); 780 }, false); 781 } 782 } 783 */ 784 785 786 787 /** 788 This provides two image classes and a bunch of functions that work on them. 789 790 Why are they separate classes? I think the operations on the two of them 791 are necessarily different. There's a whole bunch of operations that only 792 really work on truecolor (blurs, gradients), and a few that only work 793 on indexed images (palette swaps). 794 795 Even putpixel is pretty different. On indexed, it is a palette entry's 796 index number. On truecolor, it is the actual color. 797 798 A greyscale image is the weird thing in the middle. It is truecolor, but 799 fits in the same size as indexed. Still, I'd say it is a specialization 800 of truecolor. 801 802 There is a subset that works on both 803 804 */ 805 806 /// An image in memory 807 interface MemoryImage { 808 //IndexedImage convertToIndexedImage() const; 809 //TrueColorImage convertToTrueColor() const; 810 811 /// gets it as a TrueColorImage. May return this or may do a conversion and return a new image 812 TrueColorImage getAsTrueColorImage(); 813 814 /// Image width, in pixels 815 int width() const; 816 817 /// Image height, in pixels 818 int height() const; 819 820 /// Get image pixel. Slow, but returns valid RGBA color (completely transparent for off-image pixels). 821 Color getPixel(int x, int y) const; 822 823 /// Set image pixel. 824 void setPixel(int x, int y, in Color clr); 825 826 /// Load image from file. This will import arsd.image to do the actual work, and cost nothing if you don't use it. 827 static MemoryImage fromImage(T : const(char)[]) (T filename) @trusted { 828 static if (__traits(compiles, (){import arsd.image;})) { 829 // yay, we have image loader here, try it! 830 import arsd.image; 831 return loadImageFromFile(filename); 832 } else { 833 static assert(0, "please provide 'arsd.image' to load images!"); 834 } 835 } 836 837 /// Convenient alias for `fromImage` 838 alias fromImageFile = fromImage; 839 } 840 841 /// An image that consists of indexes into a color palette. Use [getAsTrueColorImage]() if you don't care about palettes 842 class IndexedImage : MemoryImage { 843 bool hasAlpha; 844 845 /// . 846 Color[] palette; 847 /// the data as indexes into the palette. Stored left to right, top to bottom, no padding. 848 ubyte[] data; 849 850 /// . 851 override int width() const { 852 return _width; 853 } 854 855 /// . 856 override int height() const { 857 return _height; 858 } 859 860 override Color getPixel(int x, int y) const @trusted { 861 if (x >= 0 && y >= 0 && x < _width && y < _height) { 862 uint pos = y*_width+x; 863 if (pos >= data.length) return Color(0, 0, 0, 0); 864 ubyte b = data.ptr[pos]; 865 if (b >= palette.length) return Color(0, 0, 0, 0); 866 return palette.ptr[b]; 867 } else { 868 return Color(0, 0, 0, 0); 869 } 870 } 871 872 override void setPixel(int x, int y, in Color clr) @trusted { 873 if (x >= 0 && y >= 0 && x < _width && y < _height) { 874 uint pos = y*_width+x; 875 if (pos >= data.length) return; 876 ubyte pidx = findNearestColor(palette, clr); 877 if (palette.length < 255 && 878 (palette.ptr[pidx].r != clr.r || palette.ptr[pidx].g != clr.g || palette.ptr[pidx].b != clr.b || palette.ptr[pidx].a != clr.a)) { 879 // add new color 880 pidx = addColor(clr); 881 } 882 data.ptr[pos] = pidx; 883 } 884 } 885 886 private int _width; 887 private int _height; 888 889 /// . 890 this(int w, int h) { 891 _width = w; 892 _height = h; 893 data = new ubyte[w*h]; 894 } 895 896 /* 897 void resize(int w, int h, bool scale) { 898 899 } 900 */ 901 902 /// returns a new image 903 override TrueColorImage getAsTrueColorImage() { 904 return convertToTrueColor(); 905 } 906 907 /// Creates a new TrueColorImage based on this data 908 TrueColorImage convertToTrueColor() const { 909 auto tci = new TrueColorImage(width, height); 910 foreach(i, b; data) { 911 /* 912 if(b >= palette.length) { 913 string fuckyou; 914 fuckyou ~= b + '0'; 915 fuckyou ~= " "; 916 fuckyou ~= palette.length + '0'; 917 assert(0, fuckyou); 918 } 919 */ 920 tci.imageData.colors[i] = palette[b]; 921 } 922 return tci; 923 } 924 925 /// Gets an exact match, if possible, adds if not. See also: the findNearestColor free function. 926 ubyte getOrAddColor(Color c) { 927 foreach(i, co; palette) { 928 if(c == co) 929 return cast(ubyte) i; 930 } 931 932 return addColor(c); 933 } 934 935 /// Number of colors currently in the palette (note: palette entries are not necessarily used in the image data) 936 int numColors() const { 937 return cast(int) palette.length; 938 } 939 940 /// Adds an entry to the palette, returning its inded 941 ubyte addColor(Color c) { 942 assert(palette.length < 256); 943 if(c.a != 255) 944 hasAlpha = true; 945 palette ~= c; 946 947 return cast(ubyte) (palette.length - 1); 948 } 949 } 950 951 /// An RGBA array of image data. Use the free function quantize() to convert to an IndexedImage 952 class TrueColorImage : MemoryImage { 953 // bool hasAlpha; 954 // bool isGreyscale; 955 956 //ubyte[] data; // stored as rgba quads, upper left to right to bottom 957 /// . 958 struct Data { 959 ubyte[] bytes; /// the data as rgba bytes. Stored left to right, top to bottom, no padding. 960 // the union is no good because the length of the struct is wrong! 961 962 /// the same data as Color structs 963 @trusted // the cast here is typically unsafe, but it is ok 964 // here because I guarantee the layout, note the static assert below 965 @property inout(Color)[] colors() inout { 966 return cast(inout(Color)[]) bytes; 967 } 968 969 static assert(Color.sizeof == 4); 970 } 971 972 /// . 973 Data imageData; 974 alias imageData.bytes data; 975 976 int _width; 977 int _height; 978 979 /// . 980 override int width() const { return _width; } 981 ///. 982 override int height() const { return _height; } 983 984 override Color getPixel(int x, int y) const @trusted { 985 if (x >= 0 && y >= 0 && x < _width && y < _height) { 986 uint pos = y*_width+x; 987 return imageData.colors.ptr[pos]; 988 } else { 989 return Color(0, 0, 0, 0); 990 } 991 } 992 993 override void setPixel(int x, int y, in Color clr) @trusted { 994 if (x >= 0 && y >= 0 && x < _width && y < _height) { 995 uint pos = y*_width+x; 996 if (pos < imageData.bytes.length/4) imageData.colors.ptr[pos] = clr; 997 } 998 } 999 1000 /// . 1001 this(int w, int h) { 1002 _width = w; 1003 _height = h; 1004 imageData.bytes = new ubyte[w*h*4]; 1005 } 1006 1007 /// Creates with existing data. The data pointer is stored here. 1008 this(int w, int h, ubyte[] data) { 1009 _width = w; 1010 _height = h; 1011 assert(data.length == w * h * 4); 1012 imageData.bytes = data; 1013 } 1014 1015 /// Returns this 1016 override TrueColorImage getAsTrueColorImage() { 1017 return this; 1018 } 1019 } 1020 1021 /// Converts true color to an indexed image. It uses palette as the starting point, adding entries 1022 /// until maxColors as needed. If palette is null, it creates a whole new palette. 1023 /// 1024 /// After quantizing the image, it applies a dithering algorithm. 1025 /// 1026 /// This is not written for speed. 1027 IndexedImage quantize(in TrueColorImage img, Color[] palette = null, in int maxColors = 256) 1028 // this is just because IndexedImage assumes ubyte palette values 1029 in { assert(maxColors <= 256); } 1030 body { 1031 int[Color] uses; 1032 foreach(pixel; img.imageData.colors) { 1033 if(auto i = pixel in uses) { 1034 (*i)++; 1035 } else { 1036 uses[pixel] = 1; 1037 } 1038 } 1039 1040 struct ColorUse { 1041 Color c; 1042 int uses; 1043 //string toString() { import std.conv; return c.toCssString() ~ " x " ~ to!string(uses); } 1044 int opCmp(ref const ColorUse co) const { 1045 return co.uses - uses; 1046 } 1047 } 1048 1049 ColorUse[] sorted; 1050 1051 foreach(color, count; uses) 1052 sorted ~= ColorUse(color, count); 1053 1054 uses = null; 1055 version(no_phobos) 1056 sorted = sorted.sort; 1057 else { 1058 import std.algorithm : sort; 1059 sort(sorted); 1060 } 1061 1062 ubyte[Color] paletteAssignments; 1063 foreach(idx, entry; palette) 1064 paletteAssignments[entry] = cast(ubyte) idx; 1065 1066 // For the color assignments from the image, I do multiple passes, decreasing the acceptable 1067 // distance each time until we're full. 1068 1069 // This is probably really slow.... but meh it gives pretty good results. 1070 1071 auto ddiff = 32; 1072 outer: for(int d1 = 128; d1 >= 0; d1 -= ddiff) { 1073 auto minDist = d1*d1; 1074 if(d1 <= 64) 1075 ddiff = 16; 1076 if(d1 <= 32) 1077 ddiff = 8; 1078 foreach(possibility; sorted) { 1079 if(palette.length == maxColors) 1080 break; 1081 if(palette.length) { 1082 auto co = palette[findNearestColor(palette, possibility.c)]; 1083 auto pixel = possibility.c; 1084 1085 auto dr = cast(int) co.r - pixel.r; 1086 auto dg = cast(int) co.g - pixel.g; 1087 auto db = cast(int) co.b - pixel.b; 1088 1089 auto dist = dr*dr + dg*dg + db*db; 1090 // not good enough variety to justify an allocation yet 1091 if(dist < minDist) 1092 continue; 1093 } 1094 paletteAssignments[possibility.c] = cast(ubyte) palette.length; 1095 palette ~= possibility.c; 1096 } 1097 } 1098 1099 // Final pass: just fill in any remaining space with the leftover common colors 1100 while(palette.length < maxColors && sorted.length) { 1101 if(sorted[0].c !in paletteAssignments) { 1102 paletteAssignments[sorted[0].c] = cast(ubyte) palette.length; 1103 palette ~= sorted[0].c; 1104 } 1105 sorted = sorted[1 .. $]; 1106 } 1107 1108 1109 bool wasPerfect = true; 1110 auto newImage = new IndexedImage(img.width, img.height); 1111 newImage.palette = palette; 1112 foreach(idx, pixel; img.imageData.colors) { 1113 if(auto p = pixel in paletteAssignments) 1114 newImage.data[idx] = *p; 1115 else { 1116 // gotta find the closest one... 1117 newImage.data[idx] = findNearestColor(palette, pixel); 1118 wasPerfect = false; 1119 } 1120 } 1121 1122 if(!wasPerfect) 1123 floydSteinbergDither(newImage, img); 1124 1125 return newImage; 1126 } 1127 1128 /// Finds the best match for pixel in palette (currently by checking for minimum euclidean distance in rgb colorspace) 1129 ubyte findNearestColor(in Color[] palette, in Color pixel) { 1130 int best = 0; 1131 int bestDistance = int.max; 1132 foreach(pe, co; palette) { 1133 auto dr = cast(int) co.r - pixel.r; 1134 auto dg = cast(int) co.g - pixel.g; 1135 auto db = cast(int) co.b - pixel.b; 1136 int dist = dr*dr + dg*dg + db*db; 1137 1138 if(dist < bestDistance) { 1139 best = cast(int) pe; 1140 bestDistance = dist; 1141 } 1142 } 1143 1144 return cast(ubyte) best; 1145 } 1146 1147 /+ 1148 1149 // Quantizing and dithering test program 1150 1151 void main( ){ 1152 /* 1153 auto img = new TrueColorImage(256, 32); 1154 foreach(y; 0 .. img.height) { 1155 foreach(x; 0 .. img.width) { 1156 img.imageData.colors[x + y * img.width] = Color(x, y * (255 / img.height), 0); 1157 } 1158 } 1159 */ 1160 1161 TrueColorImage img; 1162 1163 { 1164 1165 import arsd.png; 1166 1167 struct P { 1168 ubyte[] range; 1169 void put(ubyte[] a) { range ~= a; } 1170 } 1171 1172 P range; 1173 import std.algorithm; 1174 1175 import std.stdio; 1176 writePngLazy(range, pngFromBytes(File("/home/me/nyesha.png").byChunk(4096)).byRgbaScanline.map!((line) { 1177 foreach(ref pixel; line.pixels) { 1178 continue; 1179 auto sum = cast(int) pixel.r + pixel.g + pixel.b; 1180 ubyte a = cast(ubyte)(sum / 3); 1181 pixel.r = a; 1182 pixel.g = a; 1183 pixel.b = a; 1184 } 1185 return line; 1186 })); 1187 1188 img = imageFromPng(readPng(range.range)).getAsTrueColorImage; 1189 1190 1191 } 1192 1193 1194 1195 auto qimg = quantize(img, null, 2); 1196 1197 import arsd.simpledisplay; 1198 auto win = new SimpleWindow(img.width, img.height * 3); 1199 auto painter = win.draw(); 1200 painter.drawImage(Point(0, 0), Image.fromMemoryImage(img)); 1201 painter.drawImage(Point(0, img.height), Image.fromMemoryImage(qimg)); 1202 floydSteinbergDither(qimg, img); 1203 painter.drawImage(Point(0, img.height * 2), Image.fromMemoryImage(qimg)); 1204 win.eventLoop(0); 1205 } 1206 +/ 1207 1208 /+ 1209 /// If the background is transparent, it simply erases the alpha channel. 1210 void removeTransparency(IndexedImage img, Color background) 1211 +/ 1212 1213 /// Perform alpha-blending of `fore` to this color, return new color. 1214 /// WARNING! This function does blending in RGB space, and RGB space is not linear! 1215 Color alphaBlend(Color foreground, Color background) pure nothrow @safe @nogc { 1216 static if (__VERSION__ > 2067) pragma(inline, true); 1217 return background.alphaBlend(foreground); 1218 } 1219 1220 /* 1221 /// Reduces the number of colors in a palette. 1222 void reducePaletteSize(IndexedImage img, int maxColors = 16) { 1223 1224 } 1225 */ 1226 1227 // I think I did this wrong... but the results aren't too bad so the bug can't be awful. 1228 /// Dithers img in place to look more like original. 1229 void floydSteinbergDither(IndexedImage img, in TrueColorImage original) { 1230 assert(img.width == original.width); 1231 assert(img.height == original.height); 1232 1233 auto buffer = new Color[](original.imageData.colors.length); 1234 1235 int x, y; 1236 1237 foreach(idx, c; original.imageData.colors) { 1238 auto n = img.palette[img.data[idx]]; 1239 int errorR = cast(int) c.r - n.r; 1240 int errorG = cast(int) c.g - n.g; 1241 int errorB = cast(int) c.b - n.b; 1242 1243 void doit(int idxOffset, int multiplier) { 1244 // if(idx + idxOffset < buffer.length) 1245 buffer[idx + idxOffset] = Color.fromIntegers( 1246 c.r + multiplier * errorR / 16, 1247 c.g + multiplier * errorG / 16, 1248 c.b + multiplier * errorB / 16, 1249 c.a 1250 ); 1251 } 1252 1253 if((x+1) != original.width) 1254 doit(1, 7); 1255 if((y+1) != original.height) { 1256 if(x != 0) 1257 doit(-1 + img.width, 3); 1258 doit(img.width, 5); 1259 if(x+1 != original.width) 1260 doit(1 + img.width, 1); 1261 } 1262 1263 img.data[idx] = findNearestColor(img.palette, buffer[idx]); 1264 1265 x++; 1266 if(x == original.width) { 1267 x = 0; 1268 y++; 1269 } 1270 } 1271 } 1272 1273 // these are just really useful in a lot of places where the color/image functions are used, 1274 // so I want them available with Color 1275 /// 1276 struct Point { 1277 int x; /// 1278 int y; /// 1279 1280 pure const nothrow @safe: 1281 1282 Point opBinary(string op)(in Point rhs) { 1283 return Point(mixin("x" ~ op ~ "rhs.x"), mixin("y" ~ op ~ "rhs.y")); 1284 } 1285 1286 Point opBinary(string op)(int rhs) { 1287 return Point(mixin("x" ~ op ~ "rhs"), mixin("y" ~ op ~ "rhs")); 1288 } 1289 } 1290 1291 /// 1292 struct Size { 1293 int width; /// 1294 int height; /// 1295 } 1296 1297 /// 1298 struct Rectangle { 1299 int left; /// 1300 int top; /// 1301 int right; /// 1302 int bottom; /// 1303 1304 pure const nothrow @safe: 1305 1306 /// 1307 this(int left, int top, int right, int bottom) { 1308 this.left = left; 1309 this.top = top; 1310 this.right = right; 1311 this.bottom = bottom; 1312 } 1313 1314 /// 1315 this(in Point upperLeft, in Point lowerRight) { 1316 this(upperLeft.x, upperLeft.y, lowerRight.x, lowerRight.y); 1317 } 1318 1319 /// 1320 this(in Point upperLeft, in Size size) { 1321 this(upperLeft.x, upperLeft.y, upperLeft.x + size.width, upperLeft.y + size.height); 1322 } 1323 1324 /// 1325 @property Point upperLeft() { 1326 return Point(left, top); 1327 } 1328 1329 /// 1330 @property Point lowerRight() { 1331 return Point(right, bottom); 1332 } 1333 1334 /// 1335 @property Size size() { 1336 return Size(width, height); 1337 } 1338 1339 /// 1340 @property int width() { 1341 return right - left; 1342 } 1343 1344 /// 1345 @property int height() { 1346 return bottom - top; 1347 } 1348 1349 /// Returns true if this rectangle entirely contains the other 1350 bool contains(in Rectangle r) { 1351 return contains(r.upperLeft) && contains(r.lowerRight); 1352 } 1353 1354 /// ditto 1355 bool contains(in Point p) { 1356 return (p.x >= left && p.y < right && p.y >= top && p.y < bottom); 1357 } 1358 1359 /// Returns true of the two rectangles at any point overlap 1360 bool overlaps(in Rectangle r) { 1361 // the -1 in here are because right and top are exclusive 1362 return !((right-1) < r.left || (r.right-1) < left || (bottom-1) < r.top || (r.bottom-1) < top); 1363 } 1364 } 1365 1366 /++ 1367 Implements a flood fill algorithm, like the bucket tool in 1368 MS Paint. 1369 1370 Params: 1371 what = the canvas to work with, arranged as top to bottom, left to right elements 1372 width = the width of the canvas 1373 height = the height of the canvas 1374 target = the type to replace. You may pass the existing value if you want to do what Paint does 1375 replacement = the replacement value 1376 x = the x-coordinate to start the fill (think of where the user clicked in Paint) 1377 y = the y-coordinate to start the fill 1378 additionalCheck = A custom additional check to perform on each square before continuing. Returning true means keep flooding, returning false means stop. 1379 +/ 1380 void floodFill(T)( 1381 T[] what, int width, int height, // the canvas to inspect 1382 T target, T replacement, // fill params 1383 int x, int y, bool delegate(int x, int y) @safe additionalCheck) // the node 1384 { 1385 T node = what[y * width + x]; 1386 1387 if(target == replacement) return; 1388 1389 if(node != target) return; 1390 1391 if(additionalCheck is null) 1392 additionalCheck = (int, int) => true; 1393 1394 if(!additionalCheck(x, y)) 1395 return; 1396 1397 what[y * width + x] = replacement; 1398 1399 if(x) 1400 floodFill(what, width, height, target, replacement, 1401 x - 1, y, additionalCheck); 1402 1403 if(x != width - 1) 1404 floodFill(what, width, height, target, replacement, 1405 x + 1, y, additionalCheck); 1406 1407 if(y) 1408 floodFill(what, width, height, target, replacement, 1409 x, y - 1, additionalCheck); 1410 1411 if(y != height - 1) 1412 floodFill(what, width, height, target, replacement, 1413 x, y + 1, additionalCheck); 1414 } 1415 1416 // for scripting, so you can tag it without strictly needing to import arsd.jsvar 1417 enum arsd_jsvar_compatible = "arsd_jsvar_compatible";