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