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