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