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 import arsd.core; 10 11 @safe: 12 13 // importing phobos explodes the size of this code 10x, so not doing it. 14 15 private { 16 double toInternal(T)(scope const(char)[] s) { 17 double accumulator = 0.0; 18 size_t i = s.length; 19 foreach(idx, c; s) { 20 if(c >= '0' && c <= '9') { 21 accumulator *= 10; 22 accumulator += c - '0'; 23 } else if(c == '.') { 24 i = idx + 1; 25 break; 26 } else { 27 string wtfIsWrongWithThisStupidLanguageWithItsBrokenSafeAttribute = "bad char to make double from "; 28 wtfIsWrongWithThisStupidLanguageWithItsBrokenSafeAttribute ~= s; 29 throw new Exception(wtfIsWrongWithThisStupidLanguageWithItsBrokenSafeAttribute); 30 } 31 } 32 33 double accumulator2 = 0.0; 34 double count = 1; 35 foreach(c; s[i .. $]) { 36 if(c >= '0' && c <= '9') { 37 accumulator2 *= 10; 38 accumulator2 += c - '0'; 39 count *= 10; 40 } else { 41 string wtfIsWrongWithThisStupidLanguageWithItsBrokenSafeAttribute = "bad char to make double from "; 42 wtfIsWrongWithThisStupidLanguageWithItsBrokenSafeAttribute ~= s; 43 throw new Exception(wtfIsWrongWithThisStupidLanguageWithItsBrokenSafeAttribute); 44 } 45 } 46 47 return accumulator + accumulator2 / count; 48 } 49 50 package(arsd) @trusted 51 string toInternal(T)(long a) { 52 if(a == 0) 53 return "0"; 54 char[] ret; 55 bool neg; 56 if(a < 0) { 57 neg = true; 58 a = -a; 59 } 60 while(a) { 61 ret ~= (a % 10) + '0'; 62 a /= 10; 63 } 64 for(int i = 0; i < ret.length / 2; i++) { 65 char c = ret[i]; 66 ret[i] = ret[$ - i - 1]; 67 ret[$ - i - 1] = c; 68 } 69 if(neg) 70 ret = "-" ~ ret; 71 72 return cast(string) ret; 73 } 74 string toInternal(T)(double a) { 75 // a simplifying assumption here is the fact that we only use this in one place: toInternal!string(cast(double) a / 255) 76 // thus we know this will always be between 0.0 and 1.0, inclusive. 77 if(a <= 0.0) 78 return "0.0"; 79 if(a >= 1.0) 80 return "1.0"; 81 string ret = "0."; 82 // I wonder if I can handle round off error any better. Phobos does, but that isn't worth 100 KB of code. 83 int amt = cast(int)(a * 1000); 84 return ret ~ toInternal!string(amt); 85 } 86 87 nothrow @safe @nogc pure 88 double absInternal(double a) { return a < 0 ? -a : a; } 89 nothrow @safe @nogc pure 90 double minInternal(double a, double b, double c) { 91 auto m = a; 92 if(b < m) m = b; 93 if(c < m) m = c; 94 return m; 95 } 96 nothrow @safe @nogc pure 97 double maxInternal(double a, double b, double c) { 98 auto m = a; 99 if(b > m) m = b; 100 if(c > m) m = c; 101 return m; 102 } 103 nothrow @safe @nogc pure 104 bool startsWithInternal(in char[] a, in char[] b) { 105 return (a.length >= b.length && a[0 .. b.length] == b); 106 } 107 void splitInternal(scope inout(char)[] a, char c, scope void delegate(int, scope inout(char)[]) @safe dg) { 108 int count; 109 size_t previous = 0; 110 foreach(i, char ch; a) { 111 if(ch == c) { 112 dg(count++, a[previous .. i]); 113 previous = i + 1; 114 } 115 } 116 if(previous != a.length) 117 dg(count++, a[previous .. $]); 118 } 119 } 120 121 // done with mini-phobos 122 123 /// Represents an RGBA color 124 struct Color { 125 126 @system static Color fromJsVar(T)(T v) { // it is a template so i don't have to actually import arsd.jsvar... 127 return Color.fromString(v.get!string); 128 } 129 130 @safe: 131 /++ 132 The color components are available as a static array, individual bytes, and a uint inside this union. 133 134 Since it is anonymous, you can use the inner members' names directly. 135 +/ 136 union { 137 ubyte[4] components; /// [r, g, b, a] 138 139 /// Holder for rgba individual components. 140 struct { 141 ubyte r; /// red 142 ubyte g; /// green 143 ubyte b; /// blue 144 ubyte a; /// alpha. 255 == opaque 145 } 146 147 uint asUint; /// The components as a single 32 bit value (beware of endian issues!) 148 } 149 150 /++ 151 Returns a value compatible with [https://docs.microsoft.com/en-us/windows/win32/gdi/colorref|a Win32 COLORREF]. 152 153 Please note that the alpha value is lost in translation. 154 155 History: 156 Added November 27, 2021 (dub v10.4) 157 See_Also: 158 [fromWindowsColorRef] 159 +/ 160 nothrow pure @nogc 161 uint asWindowsColorRef() { 162 uint cr; 163 cr |= b << 16; 164 cr |= g << 8; 165 cr |= r; 166 return cr; 167 } 168 169 /++ 170 Constructs a Color from [https://docs.microsoft.com/en-us/windows/win32/gdi/colorref|a Win32 COLORREF]. 171 172 History: 173 Added November 27, 2021 (dub v10.4) 174 See_Also: 175 [asWindowsColorRef] 176 +/ 177 nothrow pure @nogc 178 static Color fromWindowsColorRef(uint cr) { 179 return Color(cr & 0xff, (cr >> 8) & 0xff, (cr >> 16) & 0xff); 180 } 181 182 /++ 183 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. 184 +/ 185 nothrow pure @nogc 186 static Color fromIntegers(int red, int green, int blue, int alpha = 255) { 187 return Color(clampToByte(red), clampToByte(green), clampToByte(blue), clampToByte(alpha)); 188 } 189 190 /// 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. 191 nothrow pure @nogc 192 this(int red, int green, int blue, int alpha = 255) { 193 this.r = cast(ubyte) red; 194 this.g = cast(ubyte) green; 195 this.b = cast(ubyte) blue; 196 this.a = cast(ubyte) alpha; 197 } 198 199 /++ 200 Construct a color from components[0 .. 4]. It must have length of at least 4 and be in r, g, b, a order. 201 202 History: 203 Added July 18, 2022 (dub v10.9) 204 +/ 205 nothrow pure @nogc 206 this(scope ubyte[] components) { 207 this.components[] = components[0 .. 4]; 208 } 209 210 /++ 211 Constructs a color from floating-point rgba components, each between 0 and 1.0. 212 213 History: 214 Added December 1, 2022 (dub v10.10) 215 +/ 216 this(float r, float g, float b, float a = 1.0) { 217 if(r < 0) r = 0; 218 if(g < 0) g = 0; 219 if(b < 0) b = 0; 220 if(r > 1) r = 1; 221 if(g > 1) g = 1; 222 if(b > 1) b = 1; 223 /* 224 assert(r >= 0.0 && r <= 1.0, to!string(r)); 225 assert(g >= 0.0 && g <= 1.0, to!string(g)); 226 assert(b >= 0.0 && b <= 1.0, to!string(b)); 227 assert(a >= 0.0 && a <= 1.0, to!string(a)); 228 */ 229 this.r = cast(ubyte) (r * 255); 230 this.g = cast(ubyte) (g * 255); 231 this.b = cast(ubyte) (b * 255); 232 this.a = cast(ubyte) (a * 255); 233 } 234 235 /++ 236 Constructs a color from a [ColorF] (floating-point) 237 238 History: 239 Added December 20, 2023 240 +/ 241 nothrow pure @nogc 242 this(const ColorF colorF) { 243 this.r = cast(ubyte) (colorF.r * 255); 244 this.g = cast(ubyte) (colorF.g * 255); 245 this.b = cast(ubyte) (colorF.b * 255); 246 this.a = cast(ubyte) (colorF.a * 255); 247 } 248 249 /// Static convenience functions for common color names 250 nothrow pure @nogc 251 static Color transparent() { return Color(0, 0, 0, 0); } 252 /// Ditto 253 nothrow pure @nogc 254 static Color white() { return Color(255, 255, 255); } 255 /// Ditto 256 nothrow pure @nogc 257 static Color gray() { return Color(128, 128, 128); } 258 /// Ditto 259 nothrow pure @nogc 260 static Color black() { return Color(0, 0, 0); } 261 /// Ditto 262 nothrow pure @nogc 263 static Color red() { return Color(255, 0, 0); } 264 /// Ditto 265 nothrow pure @nogc 266 static Color green() { return Color(0, 255, 0); } 267 /// Ditto 268 nothrow pure @nogc 269 static Color blue() { return Color(0, 0, 255); } 270 /// Ditto 271 nothrow pure @nogc 272 static Color yellow() { return Color(255, 255, 0); } 273 /// Ditto 274 nothrow pure @nogc 275 static Color teal() { return Color(0, 255, 255); } 276 /// Ditto 277 nothrow pure @nogc 278 static Color purple() { return Color(128, 0, 128); } 279 /// Ditto 280 nothrow pure @nogc 281 static Color magenta() { return Color(255, 0, 255); } 282 /// Ditto 283 nothrow pure @nogc 284 static Color brown() { return Color(128, 64, 0); } 285 286 nothrow pure @nogc 287 void premultiply() { 288 r = (r * a) / 255; 289 g = (g * a) / 255; 290 b = (b * a) / 255; 291 } 292 293 nothrow pure @nogc 294 void unPremultiply() { 295 r = cast(ubyte)(r * 255 / a); 296 g = cast(ubyte)(g * 255 / a); 297 b = cast(ubyte)(b * 255 / a); 298 } 299 300 301 /* 302 ubyte[4] toRgbaArray() { 303 return [r,g,b,a]; 304 } 305 */ 306 307 /// Return black-and-white color 308 Color toBW() () nothrow pure @safe @nogc { 309 // FIXME: gamma? 310 int intens = clampToByte(cast(int)(0.2126*r+0.7152*g+0.0722*b)); 311 return Color(intens, intens, intens, a); 312 } 313 314 /// Makes a string that matches CSS syntax for websites 315 string toCssString() const { 316 if(a == 255) 317 return "#" ~ toHexInternal(r) ~ toHexInternal(g) ~ toHexInternal(b); 318 else { 319 return "rgba("~toInternal!string(r)~", "~toInternal!string(g)~", "~toInternal!string(b)~", "~toInternal!string(cast(double)a / 255.0)~")"; 320 } 321 } 322 323 /// Makes a hex string RRGGBBAA (aa only present if it is not 255) 324 string toString() const { 325 if(a == 255) 326 return toCssString()[1 .. $]; 327 else 328 return toRgbaHexString(); 329 } 330 331 /// returns RRGGBBAA, even if a== 255 332 string toRgbaHexString() const { 333 return toHexInternal(r) ~ toHexInternal(g) ~ toHexInternal(b) ~ toHexInternal(a); 334 } 335 336 /// Gets a color by name, iff the name is one of the static members listed above 337 static Color fromNameString(string s) { 338 Color c; 339 foreach(member; __traits(allMembers, Color)) { 340 static if(__traits(compiles, c = __traits(getMember, Color, member))) { 341 if(s == member) 342 return __traits(getMember, Color, member); 343 } 344 } 345 throw new Exception("Unknown color " ~ s); 346 } 347 348 /++ 349 Reads a CSS style string to get the color. Understands #rrggbb, rgba(), hsl(), and rrggbbaa 350 351 History: 352 The short-form hex string parsing (`#fff`) was added on April 10, 2020. (v7.2.0) 353 +/ 354 static Color fromString(scope const(char)[] s) { 355 s = s.stripInternal(); 356 357 Color c; 358 c.a = 255; 359 360 // trying named colors via the static no-arg methods here 361 foreach(member; __traits(allMembers, Color)) { 362 static if(__traits(compiles, c = __traits(getMember, Color, member))) { 363 if(s == member) 364 return __traits(getMember, Color, member); 365 } 366 } 367 368 // try various notations borrowed from CSS (though a little extended) 369 370 // hsl(h,s,l,a) where h is degrees and s,l,a are 0 >= x <= 1.0 371 if(s.startsWithInternal("hsl(") || s.startsWithInternal("hsla(")) { 372 assert(s[$-1] == ')'); 373 s = s[s.startsWithInternal("hsl(") ? 4 : 5 .. $ - 1]; // the closing paren 374 375 double[3] hsl; 376 ubyte a = 255; 377 378 s.splitInternal(',', (int i, scope const(char)[] part) { 379 if(i < 3) 380 hsl[i] = toInternal!double(part.stripInternal); 381 else 382 a = clampToByte(cast(int) (toInternal!double(part.stripInternal) * 255)); 383 }); 384 385 c = .fromHsl(hsl); 386 c.a = a; 387 388 return c; 389 } 390 391 // rgb(r,g,b,a) where r,g,b are 0-255 and a is 0-1.0 392 if(s.startsWithInternal("rgb(") || s.startsWithInternal("rgba(")) { 393 assert(s[$-1] == ')'); 394 s = s[s.startsWithInternal("rgb(") ? 4 : 5 .. $ - 1]; // the closing paren 395 396 s.splitInternal(',', (int i, scope const(char)[] part) { 397 // lol the loop-switch pattern 398 auto v = toInternal!double(part.stripInternal); 399 switch(i) { 400 case 0: // red 401 c.r = clampToByte(cast(int) v); 402 break; 403 case 1: 404 c.g = clampToByte(cast(int) v); 405 break; 406 case 2: 407 c.b = clampToByte(cast(int) v); 408 break; 409 case 3: 410 c.a = clampToByte(cast(int) (v * 255)); 411 break; 412 default: // ignore 413 } 414 }); 415 416 return c; 417 } 418 419 420 421 422 // otherwise let's try it as a hex string, really loosely 423 424 if(s.length && s[0] == '#') 425 s = s[1 .. $]; 426 427 // support short form #fff for example 428 if(s.length == 3 || s.length == 4) { 429 string n; 430 n.reserve(8); 431 foreach(ch; s) { 432 n ~= ch; 433 n ~= ch; 434 } 435 s = n; 436 } 437 438 // not a built in... do it as a hex string 439 if(s.length >= 2) { 440 c.r = fromHexInternal(s[0 .. 2]); 441 s = s[2 .. $]; 442 } 443 if(s.length >= 2) { 444 c.g = fromHexInternal(s[0 .. 2]); 445 s = s[2 .. $]; 446 } 447 if(s.length >= 2) { 448 c.b = fromHexInternal(s[0 .. 2]); 449 s = s[2 .. $]; 450 } 451 if(s.length >= 2) { 452 c.a = fromHexInternal(s[0 .. 2]); 453 s = s[2 .. $]; 454 } 455 456 return c; 457 } 458 459 /// from hsl 460 static Color fromHsl(double h, double s, double l) { 461 return .fromHsl(h, s, l); 462 } 463 464 // this is actually branch-less for ints on x86, and even for longs on x86_64 465 static ubyte clampToByte(T) (T n) pure nothrow @safe @nogc if (__traits(isIntegral, T)) { 466 static if (__VERSION__ > 2067) pragma(inline, true); 467 static if (T.sizeof == 2 || T.sizeof == 4) { 468 static if (__traits(isUnsigned, T)) { 469 return cast(ubyte)(n&0xff|(255-((-cast(int)(n < 256))>>24))); 470 } else { 471 n &= -cast(int)(n >= 0); 472 return cast(ubyte)(n|((255-cast(int)n)>>31)); 473 } 474 } else static if (T.sizeof == 1) { 475 static assert(__traits(isUnsigned, T), "clampToByte: signed byte? no, really?"); 476 return cast(ubyte)n; 477 } else static if (T.sizeof == 8) { 478 static if (__traits(isUnsigned, T)) { 479 return cast(ubyte)(n&0xff|(255-((-cast(long)(n < 256))>>56))); 480 } else { 481 n &= -cast(long)(n >= 0); 482 return cast(ubyte)(n|((255-cast(long)n)>>63)); 483 } 484 } else { 485 static assert(false, "clampToByte: integer too big"); 486 } 487 } 488 489 /** this mixin can be used to alphablend two `uint` colors; 490 * `colu32name` is variable that holds color to blend, 491 * `destu32name` is variable that holds "current" color (from surface, for example). 492 * alpha value of `destu32name` doesn't matter. 493 * alpha value of `colu32name` means: 255 for replace color, 0 for keep `destu32name`. 494 * 495 * WARNING! This function does blending in RGB space, and RGB space is not linear! 496 */ 497 public enum ColorBlendMixinStr(string colu32name, string destu32name) = "{ 498 immutable uint a_tmp_ = (256-(255-(("~colu32name~")>>24)))&(-(1-(((255-(("~colu32name~")>>24))+1)>>8))); // to not lose bits, but 255 should become 0 499 immutable uint dc_tmp_ = ("~destu32name~")&0xffffff; 500 immutable uint srb_tmp_ = (("~colu32name~")&0xff00ff); 501 immutable uint sg_tmp_ = (("~colu32name~")&0x00ff00); 502 immutable uint drb_tmp_ = (dc_tmp_&0xff00ff); 503 immutable uint dg_tmp_ = (dc_tmp_&0x00ff00); 504 immutable uint orb_tmp_ = (drb_tmp_+(((srb_tmp_-drb_tmp_)*a_tmp_+0x800080)>>8))&0xff00ff; 505 immutable uint og_tmp_ = (dg_tmp_+(((sg_tmp_-dg_tmp_)*a_tmp_+0x008000)>>8))&0x00ff00; 506 ("~destu32name~") = (orb_tmp_|og_tmp_)|0xff000000; /*&0xffffff;*/ 507 }"; 508 509 510 /// Perform alpha-blending of `fore` to this color, return new color. 511 /// WARNING! This function does blending in RGB space, and RGB space is not linear! 512 Color alphaBlend (Color fore) const pure nothrow @trusted @nogc { 513 version(LittleEndian) { 514 static if (__VERSION__ > 2067) pragma(inline, true); 515 Color res; 516 res.asUint = asUint; 517 mixin(ColorBlendMixinStr!("fore.asUint", "res.asUint")); 518 return res; 519 } else { 520 alias foreground = fore; 521 alias background = this; 522 foreach(idx, ref part; foreground.components) 523 part = cast(ubyte) (part * foreground.a / 255 + background.components[idx] * (255 - foreground.a) / 255); 524 return foreground; 525 } 526 } 527 } 528 529 /++ 530 Represents an RGBA color in floating-point (from 0 to 1.0). 531 532 $(NOTE 533 Most of the time, you’ll probably want to use [Color] instead. 534 535 This primarily exists to provide a tested out-of-the-box solution 536 when utilizing APIs that work with FP colors. 537 538 Constructors and setters come with $(B `in`-contracts) 539 to assert one won’t run into out-of-range color values. 540 ) 541 542 History: 543 Added December 20, 2023 544 +/ 545 struct ColorF { 546 547 private float[4] _components; 548 549 @safe pure nothrow @nogc: 550 551 /// 552 public this(const float[4] components) 553 in(isValidComponent(components[0])) 554 in(isValidComponent(components[1])) 555 in(isValidComponent(components[2])) 556 in(isValidComponent(components[3])) { 557 _components = components; 558 } 559 560 /// ditto 561 public this(float r, float g, float b, float a = 1.0f) 562 in(isValidComponent(r)) 563 in(isValidComponent(g)) 564 in(isValidComponent(b)) 565 in(isValidComponent(a)) { 566 _components = [r, g, b, a]; 567 } 568 569 /++ 570 Constructs a FP color from an integer one 571 +/ 572 public this(const Color integer) { 573 _components[] = integer.components[] / 255.0f; 574 } 575 576 /// 577 float[4] components() inout { return _components; } 578 579 // component getters 580 float r() inout { return _components[0]; } /// red 581 float g() inout { return _components[1]; } /// green 582 float b() inout { return _components[2]; } /// blue 583 float a() inout { return _components[3]; } /// alpha 584 585 // component setters 586 void r(float v) in(isValidComponent(v)) { _components[0] = v; } /// red 587 void g(float v) in(isValidComponent(v)) { _components[1] = v; } /// green 588 void b(float v) in(isValidComponent(v)) { _components[2] = v; } /// blue 589 void a(float v) in(isValidComponent(v)) { _components[3] = v; } /// alpha 590 591 /// 592 static bool isValidComponent(const float v) { 593 return (v >= 0.0f && v <= 1.0f); 594 } 595 } 596 597 /++ 598 OKLab colorspace conversions to/from [Color]. See: [https://bottosson.github.io/posts/oklab/] 599 600 L = perceived lightness. From 0 to 1.0. 601 602 a = how green/red the color is. Apparently supposed to be from -.233887 to .276216 603 604 b = how blue/yellow the color is. Apparently supposed to be from -.311528 to 0.198570. 605 606 History: 607 Added December 1, 2022 (dub v10.10) 608 609 Bugs: 610 Seems to be some but i might just not understand what the result is supposed to be. 611 +/ 612 struct Lab { 613 float L = 0.0; 614 float a = 0.0; 615 float b = 0.0; 616 float alpha = 1.0; 617 618 float C() const { 619 import core.stdc.math; 620 return sqrtf(a * a + b * b); 621 } 622 623 float h() const { 624 import core.stdc.math; 625 return atan2f(b, a); 626 } 627 628 /++ 629 L's useful range is between 0 and 1.0 630 631 C's useful range is between 0 and 0.4 632 633 H can be 0 to 360 for degrees, or 0 to 2pi for radians. 634 +/ 635 static Lab fromLChDegrees(float L, float C, float h, float alpha = 1.0) { 636 return fromLChRadians(L, C, h * 3.14159265358979323f / 180.0f, alpha); 637 } 638 639 /// ditto 640 static Lab fromLChRadians(float L, float C, float h, float alpha = 1.0) { 641 import core.stdc.math; 642 // if(C > 0.4) C = 0.4; 643 return Lab(L, C * cosf(h), C * sinf(h), alpha); 644 } 645 } 646 647 /// ditto 648 Lab toOklab(Color c) { 649 import core.stdc.math; 650 651 // this algorithm requires linear sRGB 652 653 float f(float w) { 654 w = srbgToLinear(w); 655 if(w < 0) 656 w = 0; 657 if(w > 1) 658 w = 1; 659 return w; 660 } 661 662 float r = f(cast(float) c.r / 255); 663 float g = f(cast(float) c.g / 255); 664 float b = f(cast(float) c.b / 255); 665 666 float l = 0.4122214708f * r + 0.5363325363f * g + 0.0514459929f * b; 667 float m = 0.2119034982f * r + 0.6806995451f * g + 0.1073969566f * b; 668 float s = 0.0883024619f * r + 0.2817188376f * g + 0.6299787005f * b; 669 670 float l_ = cbrtf(l); 671 float m_ = cbrtf(m); 672 float s_ = cbrtf(s); 673 674 return Lab( 675 0.2104542553f*l_ + 0.7936177850f*m_ - 0.0040720468f*s_, 676 1.9779984951f*l_ - 2.4285922050f*m_ + 0.4505937099f*s_, 677 0.0259040371f*l_ + 0.7827717662f*m_ - 0.8086757660f*s_, 678 cast(float) c.a / 255 679 ); 680 } 681 682 /// ditto 683 Color fromOklab(Lab c) { 684 float l_ = c.L + 0.3963377774f * c.a + 0.2158037573f * c.b; 685 float m_ = c.L - 0.1055613458f * c.a - 0.0638541728f * c.b; 686 float s_ = c.L - 0.0894841775f * c.a - 1.2914855480f * c.b; 687 688 float l = l_*l_*l_; 689 float m = m_*m_*m_; 690 float s = s_*s_*s_; 691 692 float f(float w) { 693 w = linearToSrbg(w); 694 if(w < 0) 695 w = 0; 696 if(w > 1) 697 w = 1; 698 return w; 699 } 700 701 return Color( 702 f(+4.0767416621f * l - 3.3077115913f * m + 0.2309699292f * s), 703 f(-1.2684380046f * l + 2.6097574011f * m - 0.3413193965f * s), 704 f(-0.0041960863f * l - 0.7034186147f * m + 1.7076147010f * s), 705 c.alpha 706 ); 707 } 708 709 // from https://bottosson.github.io/posts/colorwrong/#what-can-we-do%3F 710 float linearToSrbg(float x) { // aka f 711 import core.stdc.math; 712 if (x >= 0.0031308) 713 return (1.055) * powf(x, (1.0/2.4)) - 0.055; 714 else 715 return 12.92 * x; 716 } 717 718 float srbgToLinear(float x) { // aka f_inv 719 import core.stdc.math; 720 if (x >= 0.04045) 721 return powf((x + 0.055)/(1 + 0.055), 2.4); 722 else 723 return x / 12.92; 724 } 725 726 /+ 727 float[3] colorToYCbCr(Color c) { 728 return matrixMultiply( 729 [ 730 +0.2126, +0.7152, +0.0722, 731 -0.1146, -0.3854, +0.5000, 732 +0.5000, -0.4542, -0.0458 733 ], 734 [float(c.r) / 255, float(c.g) / 255, float(c.b) / 255] 735 ); 736 } 737 738 Color YCbCrToColor(float Y, float Cb, float Cr) { 739 740 /* 741 Y = Y * 255; 742 Cb = Cb * 255; 743 Cr = Cr * 255; 744 745 int r = cast(int) (Y + 1.40200 * (Cr - 0x80)); 746 int g = cast(int) (Y - 0.34414 * (Cb - 0x80) - 0.71414 * (Cr - 0x80)); 747 int b = cast(int) (Y + 1.77200 * (Cb - 0x80)); 748 749 void clamp(ref int item, int min, int max) { 750 if(item < min) item = min; 751 if(item > max) item = max; 752 } 753 754 clamp(r, 0, 255); 755 clamp(g, 0, 255); 756 clamp(b, 0, 255); 757 return Color(r, g, b); 758 */ 759 760 float f(float w) { 761 if(w < 0 || w > 1) 762 return 0; 763 assert(w >= 0.0); 764 assert(w <= 1.0); 765 //w = linearToSrbg(w); 766 if(w < 0) 767 w = 0; 768 if(w > 1) 769 w = 1; 770 return w; 771 } 772 773 auto rgb = matrixMultiply( 774 [ 775 1, +0.0000, +1.5748, 776 1, -0.1873, -0.4681, 777 1, +1.8556, +0.0000 778 ], 779 [Y, Cb, Cr] 780 ); 781 782 return Color(f(rgb[0]), f(rgb[1]), f(rgb[2])); 783 } 784 785 private float[3] matrixMultiply(float[9] matrix, float[3] vector) { 786 return [ 787 matrix[0] * vector[0] + matrix[1] * vector[1] + matrix[2] * vector[2], 788 matrix[3] * vector[0] + matrix[4] * vector[1] + matrix[5] * vector[2], 789 matrix[6] * vector[0] + matrix[7] * vector[1] + matrix[8] * vector[2], 790 ]; 791 } 792 793 +/ 794 795 void premultiplyBgra(ubyte[] bgra) pure @nogc @safe nothrow in { assert(bgra.length == 4); } do { 796 auto a = bgra[3]; 797 798 bgra[2] = (bgra[2] * a) / 255; 799 bgra[1] = (bgra[1] * a) / 255; 800 bgra[0] = (bgra[0] * a) / 255; 801 } 802 803 void unPremultiplyRgba(ubyte[] rgba) pure @nogc @safe nothrow in { assert(rgba.length == 4); } do { 804 auto a = rgba[3]; 805 if(a == 0) 806 return; 807 808 rgba[0] = cast(ubyte)(rgba[0] * 255 / a); 809 rgba[1] = cast(ubyte)(rgba[1] * 255 / a); 810 rgba[2] = cast(ubyte)(rgba[2] * 255 / a); 811 } 812 813 unittest { 814 Color c = Color.fromString("#fff"); 815 assert(c == Color.white); 816 assert(c == Color.fromString("#ffffff")); 817 818 c = Color.fromString("#f0f"); 819 assert(c == Color.fromString("rgb(255, 0, 255)")); 820 } 821 822 nothrow @safe 823 private string toHexInternal(ubyte b) { 824 string s; 825 if(b < 16) 826 s ~= '0'; 827 else { 828 ubyte t = (b & 0xf0) >> 4; 829 if(t >= 10) 830 s ~= 'A' + t - 10; 831 else 832 s ~= '0' + t; 833 b &= 0x0f; 834 } 835 if(b >= 10) 836 s ~= 'A' + b - 10; 837 else 838 s ~= '0' + b; 839 840 return s; 841 } 842 843 nothrow @safe @nogc pure 844 private ubyte fromHexInternal(in char[] s) { 845 int result = 0; 846 847 int exp = 1; 848 //foreach(c; retro(s)) { // FIXME: retro doesn't work right in dtojs 849 foreach_reverse(c; s) { 850 if(c >= 'A' && c <= 'F') 851 result += exp * (c - 'A' + 10); 852 else if(c >= 'a' && c <= 'f') 853 result += exp * (c - 'a' + 10); 854 else if(c >= '0' && c <= '9') 855 result += exp * (c - '0'); 856 else 857 // throw new Exception("invalid hex character: " ~ cast(char) c); 858 return 0; 859 860 exp *= 16; 861 } 862 863 return cast(ubyte) result; 864 } 865 866 /// Converts hsl to rgb 867 Color fromHsl(real[3] hsl) nothrow pure @safe @nogc { 868 return fromHsl(cast(double) hsl[0], cast(double) hsl[1], cast(double) hsl[2]); 869 } 870 871 Color fromHsl(double[3] hsl) nothrow pure @safe @nogc { 872 return fromHsl(hsl[0], hsl[1], hsl[2]); 873 } 874 875 /// Converts hsl to rgb 876 Color fromHsl(double h, double s, double l, double a = 255) nothrow pure @safe @nogc { 877 h = h % 360; 878 879 double C = (1 - absInternal(2 * l - 1)) * s; 880 881 double hPrime = h / 60; 882 883 double X = C * (1 - absInternal(hPrime % 2 - 1)); 884 885 double r, g, b; 886 887 if(h is double.nan) 888 r = g = b = 0; 889 else if (hPrime >= 0 && hPrime < 1) { 890 r = C; 891 g = X; 892 b = 0; 893 } else if (hPrime >= 1 && hPrime < 2) { 894 r = X; 895 g = C; 896 b = 0; 897 } else if (hPrime >= 2 && hPrime < 3) { 898 r = 0; 899 g = C; 900 b = X; 901 } else if (hPrime >= 3 && hPrime < 4) { 902 r = 0; 903 g = X; 904 b = C; 905 } else if (hPrime >= 4 && hPrime < 5) { 906 r = X; 907 g = 0; 908 b = C; 909 } else if (hPrime >= 5 && hPrime < 6) { 910 r = C; 911 g = 0; 912 b = X; 913 } 914 915 double m = l - C / 2; 916 917 r += m; 918 g += m; 919 b += m; 920 921 return Color( 922 cast(int)(r * 255), 923 cast(int)(g * 255), 924 cast(int)(b * 255), 925 cast(int)(a)); 926 } 927 928 /// Assumes the input `u` is already between 0 and 1 fyi. 929 nothrow pure @safe @nogc 930 double srgbToLinearRgb(double u) { 931 if(u < 0.4045) 932 return u / 12.92; 933 else 934 return pow((u + 0.055) / 1.055, 2.4); 935 // return ((u + 0.055) / 1.055) ^^ 2.4; 936 } 937 938 // could use the ^^ operator but that drags in some phobos. In this case, the import is 939 // actually insignificant, it doesn't really impact compile time, but it does complicate 940 // the dmd -v | grep std check to confirm i didn't miss any. 941 private extern(C) nothrow pure @safe @nogc double pow(double, double); 942 943 /// 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. 944 double[3] toHsl(Color c, bool useWeightedLightness = false) nothrow pure @trusted @nogc { 945 double r1 = cast(double) c.r / 255; 946 double g1 = cast(double) c.g / 255; 947 double b1 = cast(double) c.b / 255; 948 949 double maxColor = maxInternal(r1, g1, b1); 950 double minColor = minInternal(r1, g1, b1); 951 952 double L = (maxColor + minColor) / 2 ; 953 if(useWeightedLightness) { 954 // the colors don't affect the eye equally 955 // this is a little more accurate than plain HSL numbers 956 L = 0.2126*srgbToLinearRgb(r1) + 0.7152*srgbToLinearRgb(g1) + 0.0722*srgbToLinearRgb(b1); 957 // maybe a better number is 299, 587, 114 958 } 959 double S = 0; 960 double H = 0; 961 if(maxColor != minColor) { 962 if(L < 0.5) { 963 S = (maxColor - minColor) / (maxColor + minColor); 964 } else { 965 S = (maxColor - minColor) / (2.0 - maxColor - minColor); 966 } 967 if(r1 == maxColor) { 968 H = (g1-b1) / (maxColor - minColor); 969 } else if(g1 == maxColor) { 970 H = 2.0 + (b1 - r1) / (maxColor - minColor); 971 } else { 972 H = 4.0 + (r1 - g1) / (maxColor - minColor); 973 } 974 } 975 976 H = H * 60; 977 if(H < 0){ 978 H += 360; 979 } 980 981 return [H, S, L]; 982 } 983 984 /// . 985 Color lighten(Color c, double percentage) nothrow pure @safe @nogc { 986 auto hsl = toHsl(c); 987 hsl[2] *= (1 + percentage); 988 if(hsl[2] > 1) 989 hsl[2] = 1; 990 return fromHsl(hsl); 991 } 992 993 /// . 994 Color darken(Color c, double percentage) nothrow pure @safe @nogc { 995 auto hsl = toHsl(c); 996 hsl[2] *= (1 - percentage); 997 return fromHsl(hsl); 998 } 999 1000 /// for light colors, call darken. for dark colors, call lighten. 1001 /// The goal: get toward center grey. 1002 Color moderate(Color c, double percentage) nothrow pure @safe @nogc { 1003 auto hsl = toHsl(c); 1004 if(hsl[2] > 0.5) 1005 hsl[2] *= (1 - percentage); 1006 else { 1007 if(hsl[2] <= 0.01) // if we are given black, moderating it means getting *something* out 1008 hsl[2] = percentage; 1009 else 1010 hsl[2] *= (1 + percentage); 1011 } 1012 if(hsl[2] > 1) 1013 hsl[2] = 1; 1014 return fromHsl(hsl); 1015 } 1016 1017 /// the opposite of moderate. Make darks darker and lights lighter 1018 Color extremify(Color c, double percentage) nothrow pure @safe @nogc { 1019 auto hsl = toHsl(c, true); 1020 if(hsl[2] < 0.5) 1021 hsl[2] *= (1 - percentage); 1022 else 1023 hsl[2] *= (1 + percentage); 1024 if(hsl[2] > 1) 1025 hsl[2] = 1; 1026 return fromHsl(hsl); 1027 } 1028 1029 /// Move around the lightness wheel, trying not to break on moderate things 1030 Color oppositeLightness(Color c) nothrow pure @safe @nogc { 1031 auto hsl = toHsl(c); 1032 1033 auto original = hsl[2]; 1034 1035 if(original > 0.4 && original < 0.6) 1036 hsl[2] = 0.8 - original; // so it isn't quite the same 1037 else 1038 hsl[2] = 1 - original; 1039 1040 return fromHsl(hsl); 1041 } 1042 1043 /// Try to determine a text color - either white or black - based on the input 1044 Color makeTextColor(Color c) nothrow pure @safe @nogc { 1045 auto hsl = toHsl(c, true); // give green a bonus for contrast 1046 if(hsl[2] > 0.71) 1047 return Color(0, 0, 0); 1048 else 1049 return Color(255, 255, 255); 1050 } 1051 1052 // These provide functional access to hsl manipulation; useful if you need a delegate 1053 1054 Color setLightness(Color c, double lightness) nothrow pure @safe @nogc { 1055 auto hsl = toHsl(c); 1056 hsl[2] = lightness; 1057 return fromHsl(hsl); 1058 } 1059 1060 1061 /// 1062 Color rotateHue(Color c, double degrees) nothrow pure @safe @nogc { 1063 auto hsl = toHsl(c); 1064 hsl[0] += degrees; 1065 return fromHsl(hsl); 1066 } 1067 1068 /// 1069 Color setHue(Color c, double hue) nothrow pure @safe @nogc { 1070 auto hsl = toHsl(c); 1071 hsl[0] = hue; 1072 return fromHsl(hsl); 1073 } 1074 1075 /// 1076 Color desaturate(Color c, double percentage) nothrow pure @safe @nogc { 1077 auto hsl = toHsl(c); 1078 hsl[1] *= (1 - percentage); 1079 return fromHsl(hsl); 1080 } 1081 1082 /// 1083 Color saturate(Color c, double percentage) nothrow pure @safe @nogc { 1084 auto hsl = toHsl(c); 1085 hsl[1] *= (1 + percentage); 1086 if(hsl[1] > 1) 1087 hsl[1] = 1; 1088 return fromHsl(hsl); 1089 } 1090 1091 /// 1092 Color setSaturation(Color c, double saturation) nothrow pure @safe @nogc { 1093 auto hsl = toHsl(c); 1094 hsl[1] = saturation; 1095 return fromHsl(hsl); 1096 } 1097 1098 1099 /* 1100 void main(string[] args) { 1101 auto color1 = toHsl(Color(255, 0, 0)); 1102 auto color = fromHsl(color1[0] + 60, color1[1], color1[2]); 1103 1104 writefln("#%02x%02x%02x", color.r, color.g, color.b); 1105 } 1106 */ 1107 1108 /* Color algebra functions */ 1109 1110 /* Alpha putpixel looks like this: 1111 1112 void putPixel(Image i, Color c) { 1113 Color b; 1114 b.r = i.data[(y * i.width + x) * bpp + 0]; 1115 b.g = i.data[(y * i.width + x) * bpp + 1]; 1116 b.b = i.data[(y * i.width + x) * bpp + 2]; 1117 b.a = i.data[(y * i.width + x) * bpp + 3]; 1118 1119 float ca = cast(float) c.a / 255; 1120 1121 i.data[(y * i.width + x) * bpp + 0] = alpha(c.r, ca, b.r); 1122 i.data[(y * i.width + x) * bpp + 1] = alpha(c.g, ca, b.g); 1123 i.data[(y * i.width + x) * bpp + 2] = alpha(c.b, ca, b.b); 1124 i.data[(y * i.width + x) * bpp + 3] = alpha(c.a, ca, b.a); 1125 } 1126 1127 ubyte alpha(ubyte c1, float alpha, ubyte onto) { 1128 auto got = (1 - alpha) * onto + alpha * c1; 1129 1130 if(got > 255) 1131 return 255; 1132 return cast(ubyte) got; 1133 } 1134 1135 So, given the background color and the resultant color, what was 1136 composited on to it? 1137 */ 1138 1139 /// 1140 ubyte unalpha(ubyte colorYouHave, float alpha, ubyte backgroundColor) nothrow pure @safe @nogc { 1141 // resultingColor = (1-alpha) * backgroundColor + alpha * answer 1142 auto resultingColorf = cast(float) colorYouHave; 1143 auto backgroundColorf = cast(float) backgroundColor; 1144 1145 auto answer = (resultingColorf - backgroundColorf + alpha * backgroundColorf) / alpha; 1146 return Color.clampToByte(cast(int) answer); 1147 } 1148 1149 /// 1150 ubyte makeAlpha(ubyte colorYouHave, ubyte backgroundColor/*, ubyte foreground = 0x00*/) nothrow pure @safe @nogc { 1151 //auto foregroundf = cast(float) foreground; 1152 auto foregroundf = 0.00f; 1153 auto colorYouHavef = cast(float) colorYouHave; 1154 auto backgroundColorf = cast(float) backgroundColor; 1155 1156 // colorYouHave = backgroundColorf - alpha * backgroundColorf + alpha * foregroundf 1157 auto alphaf = 1 - colorYouHave / backgroundColorf; 1158 alphaf *= 255; 1159 1160 return Color.clampToByte(cast(int) alphaf); 1161 } 1162 1163 1164 int fromHex(string s) { 1165 int result = 0; 1166 1167 int exp = 1; 1168 // foreach(c; retro(s)) { 1169 foreach_reverse(c; s) { 1170 if(c >= 'A' && c <= 'F') 1171 result += exp * (c - 'A' + 10); 1172 else if(c >= 'a' && c <= 'f') 1173 result += exp * (c - 'a' + 10); 1174 else if(c >= '0' && c <= '9') 1175 result += exp * (c - '0'); 1176 else 1177 throw new Exception("invalid hex character: " ~ cast(char) c); 1178 1179 exp *= 16; 1180 } 1181 1182 return result; 1183 } 1184 1185 /// 1186 Color colorFromString(string s) { 1187 if(s.length == 0) 1188 return Color(0,0,0,255); 1189 if(s[0] == '#') 1190 s = s[1..$]; 1191 assert(s.length == 6 || s.length == 8); 1192 1193 Color c; 1194 1195 c.r = cast(ubyte) fromHex(s[0..2]); 1196 c.g = cast(ubyte) fromHex(s[2..4]); 1197 c.b = cast(ubyte) fromHex(s[4..6]); 1198 if(s.length == 8) 1199 c.a = cast(ubyte) fromHex(s[6..8]); 1200 else 1201 c.a = 255; 1202 1203 return c; 1204 } 1205 1206 /* 1207 import browser.window; 1208 void main() { 1209 import browser.document; 1210 foreach(ele; document.querySelectorAll("input")) { 1211 ele.addEventListener("change", { 1212 auto h = toInternal!double(document.querySelector("input[name=h]").value); 1213 auto s = toInternal!double(document.querySelector("input[name=s]").value); 1214 auto l = toInternal!double(document.querySelector("input[name=l]").value); 1215 1216 Color c = Color.fromHsl(h, s, l); 1217 1218 auto e = document.getElementById("example"); 1219 e.style.backgroundColor = c.toCssString(); 1220 1221 // JSElement __js_this; 1222 // __js_this.style.backgroundColor = c.toCssString(); 1223 }, false); 1224 } 1225 } 1226 */ 1227 1228 1229 1230 /** 1231 This provides two image classes and a bunch of functions that work on them. 1232 1233 Why are they separate classes? I think the operations on the two of them 1234 are necessarily different. There's a whole bunch of operations that only 1235 really work on truecolor (blurs, gradients), and a few that only work 1236 on indexed images (palette swaps). 1237 1238 Even putpixel is pretty different. On indexed, it is a palette entry's 1239 index number. On truecolor, it is the actual color. 1240 1241 A greyscale image is the weird thing in the middle. It is truecolor, but 1242 fits in the same size as indexed. Still, I'd say it is a specialization 1243 of truecolor. 1244 1245 There is a subset that works on both 1246 1247 */ 1248 1249 /// An image in memory 1250 interface MemoryImage { 1251 //IndexedImage convertToIndexedImage() const; 1252 //TrueColorImage convertToTrueColor() const; 1253 1254 /// gets it as a TrueColorImage. May return this or may do a conversion and return a new image 1255 TrueColorImage getAsTrueColorImage() pure nothrow @safe; 1256 1257 /// Image width, in pixels 1258 int width() const pure nothrow @safe @nogc; 1259 1260 /// Image height, in pixels 1261 int height() const pure nothrow @safe @nogc; 1262 1263 /// Get image pixel. Slow, but returns valid RGBA color (completely transparent for off-image pixels). 1264 Color getPixel(int x, int y) const pure nothrow @safe @nogc; 1265 1266 /// Set image pixel. 1267 void setPixel(int x, int y, in Color clr) nothrow @safe; 1268 1269 /// Returns a copy of the image 1270 MemoryImage clone() const pure nothrow @safe; 1271 1272 /// Load image from file. This will import arsd.image to do the actual work, and cost nothing if you don't use it. 1273 static MemoryImage fromImage(T : const(char)[]) (T filename) @trusted { 1274 static if (__traits(compiles, (){import arsd.image;})) { 1275 // yay, we have image loader here, try it! 1276 import arsd.image; 1277 return loadImageFromFile(filename); 1278 } else { 1279 static assert(0, "please provide 'arsd.image' to load images!"); 1280 } 1281 } 1282 1283 // ***This method is deliberately not publicly documented.*** 1284 // What it does is unconditionally frees internal image storage, without any sanity checks. 1285 // If you will do this, make sure that you have no references to image data left (like 1286 // slices of [data] array, for example). Those references will become invalid, and WILL 1287 // lead to Undefined Behavior. 1288 // tl;dr: IF YOU HAVE *ANY* QUESTIONS REGARDING THIS COMMENT, DON'T USE THIS! 1289 // Note to implementors: it is safe to simply do nothing in this method. 1290 // Also, it should be safe to call this method twice or more. 1291 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 1292 1293 /// Convenient alias for `fromImage` 1294 alias fromImageFile = fromImage; 1295 } 1296 1297 /// An image that consists of indexes into a color palette. Use [getAsTrueColorImage]() if you don't care about palettes 1298 class IndexedImage : MemoryImage { 1299 bool hasAlpha; 1300 1301 /// . 1302 Color[] palette; 1303 /// the data as indexes into the palette. Stored left to right, top to bottom, no padding. 1304 ubyte[] data; 1305 1306 override void clearInternal () nothrow @system {// @nogc { 1307 import core.memory : GC; 1308 // it is safe to call [GC.free] with `null` pointer. 1309 GC.free(GC.addrOf(palette.ptr)); palette = null; 1310 GC.free(GC.addrOf(data.ptr)); data = null; 1311 _width = _height = 0; 1312 } 1313 1314 /// . 1315 override int width() const pure nothrow @safe @nogc { 1316 return _width; 1317 } 1318 1319 /// . 1320 override int height() const pure nothrow @safe @nogc { 1321 return _height; 1322 } 1323 1324 /// . 1325 override IndexedImage clone() const pure nothrow @trusted { 1326 auto n = new IndexedImage(width, height); 1327 n.data[] = this.data[]; // the data member is already there, so array copy 1328 n.palette = this.palette.dup; // and here we need to allocate too, so dup 1329 n.hasAlpha = this.hasAlpha; 1330 return n; 1331 } 1332 1333 override Color getPixel(int x, int y) const pure nothrow @trusted @nogc { 1334 if (x >= 0 && y >= 0 && x < _width && y < _height) { 1335 size_t pos = cast(size_t)y*_width+x; 1336 if (pos >= data.length) return Color(0, 0, 0, 0); 1337 ubyte b = data.ptr[pos]; 1338 if (b >= palette.length) return Color(0, 0, 0, 0); 1339 return palette.ptr[b]; 1340 } else { 1341 return Color(0, 0, 0, 0); 1342 } 1343 } 1344 1345 override void setPixel(int x, int y, in Color clr) nothrow @trusted { 1346 if (x >= 0 && y >= 0 && x < _width && y < _height) { 1347 size_t pos = cast(size_t)y*_width+x; 1348 if (pos >= data.length) return; 1349 ubyte pidx = findNearestColor(palette, clr); 1350 if (palette.length < 255 && 1351 (palette.ptr[pidx].r != clr.r || palette.ptr[pidx].g != clr.g || palette.ptr[pidx].b != clr.b || palette.ptr[pidx].a != clr.a)) { 1352 // add new color 1353 pidx = addColor(clr); 1354 } 1355 data.ptr[pos] = pidx; 1356 } 1357 } 1358 1359 private int _width; 1360 private int _height; 1361 1362 /// . 1363 this(int w, int h) pure nothrow @safe { 1364 _width = w; 1365 _height = h; 1366 1367 // ensure that the computed size does not exceed basic address space limits 1368 assert(cast(ulong)w * h <= size_t.max); 1369 // upcast to avoid overflow for images larger than 536 Mpix 1370 data = new ubyte[cast(size_t)w*h]; 1371 } 1372 1373 /* 1374 void resize(int w, int h, bool scale) { 1375 1376 } 1377 */ 1378 1379 /// returns a new image 1380 override TrueColorImage getAsTrueColorImage() pure nothrow @safe { 1381 return convertToTrueColor(); 1382 } 1383 1384 /// Creates a new TrueColorImage based on this data 1385 TrueColorImage convertToTrueColor() const pure nothrow @trusted { 1386 auto tci = new TrueColorImage(width, height); 1387 foreach(i, b; data) { 1388 tci.imageData.colors[i] = palette[b]; 1389 } 1390 return tci; 1391 } 1392 1393 /// Gets an exact match, if possible, adds if not. See also: the findNearestColor free function. 1394 ubyte getOrAddColor(Color c) nothrow @trusted { 1395 foreach(i, co; palette) { 1396 if(c == co) 1397 return cast(ubyte) i; 1398 } 1399 1400 return addColor(c); 1401 } 1402 1403 /// Number of colors currently in the palette (note: palette entries are not necessarily used in the image data) 1404 int numColors() const pure nothrow @trusted @nogc { 1405 return cast(int) palette.length; 1406 } 1407 1408 /// Adds an entry to the palette, returning its index 1409 ubyte addColor(Color c) nothrow @trusted { 1410 assert(palette.length < 256); 1411 if(c.a != 255) 1412 hasAlpha = true; 1413 palette ~= c; 1414 1415 return cast(ubyte) (palette.length - 1); 1416 } 1417 } 1418 1419 /// An RGBA array of image data. Use the free function quantize() to convert to an IndexedImage 1420 class TrueColorImage : MemoryImage { 1421 // bool hasAlpha; 1422 // bool isGreyscale; 1423 1424 //ubyte[] data; // stored as rgba quads, upper left to right to bottom 1425 /// . 1426 struct Data { 1427 ubyte[] bytes; /// the data as rgba bytes. Stored left to right, top to bottom, no padding. 1428 // the union is no good because the length of the struct is wrong! 1429 1430 /// the same data as Color structs 1431 @trusted // the cast here is typically unsafe, but it is ok 1432 // here because I guarantee the layout, note the static assert below 1433 @property inout(Color)[] colors() inout pure nothrow @nogc { 1434 return cast(inout(Color)[]) bytes; 1435 } 1436 1437 static assert(Color.sizeof == 4); 1438 } 1439 1440 /// . 1441 Data imageData; 1442 alias imageData.bytes data; 1443 1444 int _width; 1445 int _height; 1446 1447 override void clearInternal () nothrow @system {// @nogc { 1448 import core.memory : GC; 1449 // it is safe to call [GC.free] with `null` pointer. 1450 GC.free(GC.addrOf(imageData.bytes.ptr)); imageData.bytes = null; 1451 _width = _height = 0; 1452 } 1453 1454 /// . 1455 override TrueColorImage clone() const pure nothrow @trusted { 1456 auto n = new TrueColorImage(width, height); 1457 n.imageData.bytes[] = this.imageData.bytes[]; // copy into existing array ctor allocated 1458 return n; 1459 } 1460 1461 /// . 1462 override int width() const pure nothrow @trusted @nogc { return _width; } 1463 ///. 1464 override int height() const pure nothrow @trusted @nogc { return _height; } 1465 1466 override Color getPixel(int x, int y) const pure nothrow @trusted @nogc { 1467 if (x >= 0 && y >= 0 && x < _width && y < _height) { 1468 size_t pos = cast(size_t)y*_width+x; 1469 return imageData.colors.ptr[pos]; 1470 } else { 1471 return Color(0, 0, 0, 0); 1472 } 1473 } 1474 1475 override void setPixel(int x, int y, in Color clr) nothrow @trusted { 1476 if (x >= 0 && y >= 0 && x < _width && y < _height) { 1477 size_t pos = cast(size_t)y*_width+x; 1478 if (pos < imageData.bytes.length/4) imageData.colors.ptr[pos] = clr; 1479 } 1480 } 1481 1482 /// . 1483 this(int w, int h) pure nothrow @safe { 1484 _width = w; 1485 _height = h; 1486 1487 // ensure that the computed size does not exceed basic address space limits 1488 assert(cast(ulong)w * h * 4 <= size_t.max); 1489 // upcast to avoid overflow for images larger than 536 Mpix 1490 imageData.bytes = new ubyte[cast(size_t)w * h * 4]; 1491 } 1492 1493 /// Creates with existing data. The data pointer is stored here. 1494 this(int w, int h, ubyte[] data) pure nothrow @safe { 1495 _width = w; 1496 _height = h; 1497 assert(cast(ulong)w * h * 4 <= size_t.max); 1498 assert(data.length == cast(size_t)w * h * 4); 1499 imageData.bytes = data; 1500 } 1501 1502 /// Returns this 1503 override TrueColorImage getAsTrueColorImage() pure nothrow @safe { 1504 return this; 1505 } 1506 } 1507 1508 /+ 1509 /// An RGB array of image data. 1510 class TrueColorImageWithoutAlpha : MemoryImage { 1511 struct Data { 1512 ubyte[] bytes; // the data as rgba bytes. Stored left to right, top to bottom, no padding. 1513 } 1514 1515 /// . 1516 Data imageData; 1517 1518 int _width; 1519 int _height; 1520 1521 override void clearInternal () nothrow @system {// @nogc { 1522 import core.memory : GC; 1523 // it is safe to call [GC.free] with `null` pointer. 1524 GC.free(imageData.bytes.ptr); imageData.bytes = null; 1525 _width = _height = 0; 1526 } 1527 1528 /// . 1529 override TrueColorImageWithoutAlpha clone() const pure nothrow @trusted { 1530 auto n = new TrueColorImageWithoutAlpha(width, height); 1531 n.imageData.bytes[] = this.imageData.bytes[]; // copy into existing array ctor allocated 1532 return n; 1533 } 1534 1535 /// . 1536 override int width() const pure nothrow @trusted @nogc { return _width; } 1537 ///. 1538 override int height() const pure nothrow @trusted @nogc { return _height; } 1539 1540 override Color getPixel(int x, int y) const pure nothrow @trusted @nogc { 1541 if (x >= 0 && y >= 0 && x < _width && y < _height) { 1542 uint pos = (y*_width+x) * 3; 1543 return Color(imageData.bytes[0], imageData.bytes[1], imageData.bytes[2], 255); 1544 } else { 1545 return Color(0, 0, 0, 0); 1546 } 1547 } 1548 1549 override void setPixel(int x, int y, in Color clr) nothrow @trusted { 1550 if (x >= 0 && y >= 0 && x < _width && y < _height) { 1551 uint pos = y*_width+x; 1552 //if (pos < imageData.bytes.length/4) imageData.colors.ptr[pos] = clr; 1553 // FIXME 1554 } 1555 } 1556 1557 /// . 1558 this(int w, int h) pure nothrow @safe { 1559 _width = w; 1560 _height = h; 1561 imageData.bytes = new ubyte[w*h*3]; 1562 } 1563 1564 /// Creates with existing data. The data pointer is stored here. 1565 this(int w, int h, ubyte[] data) pure nothrow @safe { 1566 _width = w; 1567 _height = h; 1568 assert(data.length == w * h * 3); 1569 imageData.bytes = data; 1570 } 1571 1572 /// 1573 override TrueColorImage getAsTrueColorImage() pure nothrow @safe { 1574 // FIXME 1575 //return this; 1576 } 1577 } 1578 +/ 1579 1580 1581 alias extern(C) int function(scope const void*, scope const void*) @system Comparator; 1582 @trusted void nonPhobosSort(T)(T[] obj, Comparator comparator) { 1583 import core.stdc.stdlib; 1584 qsort(obj.ptr, obj.length, typeof(obj[0]).sizeof, comparator); 1585 } 1586 1587 /// Converts true color to an indexed image. It uses palette as the starting point, adding entries 1588 /// until maxColors as needed. If palette is null, it creates a whole new palette. 1589 /// 1590 /// After quantizing the image, it applies a dithering algorithm. 1591 /// 1592 /// This is not written for speed. 1593 IndexedImage quantize(in TrueColorImage img, Color[] palette = null, in int maxColors = 256) 1594 // this is just because IndexedImage assumes ubyte palette values 1595 in { assert(maxColors <= 256); } 1596 do { 1597 int[Color] uses; 1598 foreach(pixel; img.imageData.colors) { 1599 if(auto i = pixel in uses) { 1600 (*i)++; 1601 } else { 1602 uses[pixel] = 1; 1603 } 1604 } 1605 1606 struct ColorUse { 1607 Color c; 1608 int uses; 1609 //string toString() { return c.toCssString() ~ " x " ~ to!string(uses); } 1610 int opCmp(ref const ColorUse co) const { 1611 return co.uses - uses; 1612 } 1613 extern(C) static int comparator(scope const void* lhs, scope const void* rhs) { 1614 return (cast(ColorUse*)rhs).uses - (cast(ColorUse*)lhs).uses; 1615 } 1616 } 1617 1618 ColorUse[] sorted; 1619 1620 foreach(color, count; uses) 1621 sorted ~= ColorUse(color, count); 1622 1623 uses = null; 1624 1625 nonPhobosSort(sorted, &ColorUse.comparator); 1626 // or, with phobos, but that adds 70ms to compile time 1627 //import std.algorithm.sorting : sort; 1628 //sort(sorted); 1629 1630 ubyte[Color] paletteAssignments; 1631 foreach(idx, entry; palette) 1632 paletteAssignments[entry] = cast(ubyte) idx; 1633 1634 // For the color assignments from the image, I do multiple passes, decreasing the acceptable 1635 // distance each time until we're full. 1636 1637 // This is probably really slow.... but meh it gives pretty good results. 1638 1639 auto ddiff = 32; 1640 outer: for(int d1 = 128; d1 >= 0; d1 -= ddiff) { 1641 auto minDist = d1*d1; 1642 if(d1 <= 64) 1643 ddiff = 16; 1644 if(d1 <= 32) 1645 ddiff = 8; 1646 foreach(possibility; sorted) { 1647 if(palette.length == maxColors) 1648 break; 1649 if(palette.length) { 1650 auto co = palette[findNearestColor(palette, possibility.c)]; 1651 auto pixel = possibility.c; 1652 1653 auto dr = cast(int) co.r - pixel.r; 1654 auto dg = cast(int) co.g - pixel.g; 1655 auto db = cast(int) co.b - pixel.b; 1656 1657 auto dist = dr*dr + dg*dg + db*db; 1658 // not good enough variety to justify an allocation yet 1659 if(dist < minDist) 1660 continue; 1661 } 1662 paletteAssignments[possibility.c] = cast(ubyte) palette.length; 1663 palette ~= possibility.c; 1664 } 1665 } 1666 1667 // Final pass: just fill in any remaining space with the leftover common colors 1668 while(palette.length < maxColors && sorted.length) { 1669 if(sorted[0].c !in paletteAssignments) { 1670 paletteAssignments[sorted[0].c] = cast(ubyte) palette.length; 1671 palette ~= sorted[0].c; 1672 } 1673 sorted = sorted[1 .. $]; 1674 } 1675 1676 1677 bool wasPerfect = true; 1678 auto newImage = new IndexedImage(img.width, img.height); 1679 newImage.palette = palette; 1680 foreach(idx, pixel; img.imageData.colors) { 1681 if(auto p = pixel in paletteAssignments) 1682 newImage.data[idx] = *p; 1683 else { 1684 // gotta find the closest one... 1685 newImage.data[idx] = findNearestColor(palette, pixel); 1686 wasPerfect = false; 1687 } 1688 } 1689 1690 if(!wasPerfect) 1691 floydSteinbergDither(newImage, img); 1692 1693 return newImage; 1694 } 1695 1696 /// Finds the best match for pixel in palette (currently by checking for minimum euclidean distance in rgb colorspace) 1697 ubyte findNearestColor(in Color[] palette, in Color pixel) nothrow pure @trusted @nogc { 1698 int best = 0; 1699 int bestDistance = int.max; 1700 foreach(pe, co; palette) { 1701 auto dr = cast(int) co.r - pixel.r; 1702 auto dg = cast(int) co.g - pixel.g; 1703 auto db = cast(int) co.b - pixel.b; 1704 int dist = dr*dr + dg*dg + db*db; 1705 1706 if(dist < bestDistance) { 1707 best = cast(int) pe; 1708 bestDistance = dist; 1709 } 1710 } 1711 1712 return cast(ubyte) best; 1713 } 1714 1715 /+ 1716 1717 // Quantizing and dithering test program 1718 1719 void main( ){ 1720 /* 1721 auto img = new TrueColorImage(256, 32); 1722 foreach(y; 0 .. img.height) { 1723 foreach(x; 0 .. img.width) { 1724 img.imageData.colors[x + y * img.width] = Color(x, y * (255 / img.height), 0); 1725 } 1726 } 1727 */ 1728 1729 TrueColorImage img; 1730 1731 { 1732 1733 import arsd.png; 1734 1735 struct P { 1736 ubyte[] range; 1737 void put(ubyte[] a) { range ~= a; } 1738 } 1739 1740 P range; 1741 import std.algorithm; // commented out 1742 1743 import std.stdio; // commented out 1744 writePngLazy(range, pngFromBytes(File("/home/me/nyesha.png").byChunk(4096)).byRgbaScanline.map!((line) { 1745 foreach(ref pixel; line.pixels) { 1746 continue; 1747 auto sum = cast(int) pixel.r + pixel.g + pixel.b; 1748 ubyte a = cast(ubyte)(sum / 3); 1749 pixel.r = a; 1750 pixel.g = a; 1751 pixel.b = a; 1752 } 1753 return line; 1754 })); 1755 1756 img = imageFromPng(readPng(range.range)).getAsTrueColorImage; 1757 1758 1759 } 1760 1761 1762 1763 auto qimg = quantize(img, null, 2); 1764 1765 import arsd.simpledisplay; 1766 auto win = new SimpleWindow(img.width, img.height * 3); 1767 auto painter = win.draw(); 1768 painter.drawImage(Point(0, 0), Image.fromMemoryImage(img)); 1769 painter.drawImage(Point(0, img.height), Image.fromMemoryImage(qimg)); 1770 floydSteinbergDither(qimg, img); 1771 painter.drawImage(Point(0, img.height * 2), Image.fromMemoryImage(qimg)); 1772 win.eventLoop(0); 1773 } 1774 +/ 1775 1776 /+ 1777 /// If the background is transparent, it simply erases the alpha channel. 1778 void removeTransparency(IndexedImage img, Color background) 1779 +/ 1780 1781 /// Perform alpha-blending of `fore` to this color, return new color. 1782 /// WARNING! This function does blending in RGB space, and RGB space is not linear! 1783 Color alphaBlend(Color foreground, Color background) pure nothrow @safe @nogc { 1784 //if(foreground.a == 255) 1785 //return foreground; 1786 if(foreground.a == 0) 1787 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) 1788 1789 static if (__VERSION__ > 2067) pragma(inline, true); 1790 return background.alphaBlend(foreground); 1791 } 1792 1793 /* 1794 /// Reduces the number of colors in a palette. 1795 void reducePaletteSize(IndexedImage img, int maxColors = 16) { 1796 1797 } 1798 */ 1799 1800 // I think I did this wrong... but the results aren't too bad so the bug can't be awful. 1801 /// Dithers img in place to look more like original. 1802 void floydSteinbergDither(IndexedImage img, in TrueColorImage original) nothrow @trusted { 1803 assert(img.width == original.width); 1804 assert(img.height == original.height); 1805 1806 auto buffer = new Color[](original.imageData.colors.length); 1807 1808 int x, y; 1809 1810 foreach(idx, c; original.imageData.colors) { 1811 auto n = img.palette[img.data[idx]]; 1812 int errorR = cast(int) c.r - n.r; 1813 int errorG = cast(int) c.g - n.g; 1814 int errorB = cast(int) c.b - n.b; 1815 1816 void doit(int idxOffset, int multiplier) { 1817 // if(idx + idxOffset < buffer.length) 1818 buffer[idx + idxOffset] = Color.fromIntegers( 1819 c.r + multiplier * errorR / 16, 1820 c.g + multiplier * errorG / 16, 1821 c.b + multiplier * errorB / 16, 1822 c.a 1823 ); 1824 } 1825 1826 if((x+1) != original.width) 1827 doit(1, 7); 1828 if((y+1) != original.height) { 1829 if(x != 0) 1830 doit(-1 + img.width, 3); 1831 doit(img.width, 5); 1832 if(x+1 != original.width) 1833 doit(1 + img.width, 1); 1834 } 1835 1836 img.data[idx] = findNearestColor(img.palette, buffer[idx]); 1837 1838 x++; 1839 if(x == original.width) { 1840 x = 0; 1841 y++; 1842 } 1843 } 1844 } 1845 1846 // these are just really useful in a lot of places where the color/image functions are used, 1847 // so I want them available with Color 1848 /++ 1849 2D location point 1850 +/ 1851 struct Point { 1852 int x; /// x-coordinate (aka abscissa) 1853 int y; /// y-coordinate (aka ordinate) 1854 1855 pure const nothrow @safe: 1856 1857 Point opBinary(string op)(in Point rhs) @nogc { 1858 return Point(mixin("x" ~ op ~ "rhs.x"), mixin("y" ~ op ~ "rhs.y")); 1859 } 1860 1861 Point opBinary(string op)(int rhs) @nogc { 1862 return Point(mixin("x" ~ op ~ "rhs"), mixin("y" ~ op ~ "rhs")); 1863 } 1864 1865 Size opCast(T : Size)() inout @nogc { 1866 return Size(x, y); 1867 } 1868 } 1869 1870 /// 1871 struct Size { 1872 int width; /// 1873 int height; /// 1874 1875 pure nothrow @safe: 1876 1877 /++ 1878 Rectangular surface area 1879 1880 Calculates the surface area of a rectangle with dimensions equivalent to the width and height of the size. 1881 +/ 1882 int area() const @nogc { return width * height; } 1883 1884 Point opCast(T : Point)() inout @nogc { 1885 return Point(width, height); 1886 } 1887 1888 // gonna leave this undocumented for now since it might be removed later 1889 /+ + 1890 Adding (and other arithmetic operations) two sizes together will operate on the width and height independently. So Size(2, 3) + Size(4, 5) will give you Size(6, 8). 1891 +/ 1892 Size opBinary(string op)(in Size rhs) const @nogc { 1893 return Size( 1894 mixin("width" ~ op ~ "rhs.width"), 1895 mixin("height" ~ op ~ "rhs.height"), 1896 ); 1897 } 1898 1899 Size opBinary(string op)(int rhs) const @nogc { 1900 return Size( 1901 mixin("width" ~ op ~ "rhs"), 1902 mixin("height" ~ op ~ "rhs"), 1903 ); 1904 } 1905 } 1906 1907 /++ 1908 Calculates the linear offset of a point 1909 from the start (0/0) of a rectangle. 1910 1911 This assumes that (0/0) is equivalent to offset `0`. 1912 Each step on the x-coordinate advances the resulting offset by `1`. 1913 Each step on the y-coordinate advances the resulting offset by `width`. 1914 1915 This function is only defined for the 1st quadrant, 1916 i.e. both coordinates (x and y) of `pos` are positive. 1917 1918 Returns: 1919 `y * width + x` 1920 1921 History: 1922 Added December 19, 2023 (dub v11.4) 1923 +/ 1924 int linearOffset(const Point pos, const int width) @safe pure nothrow @nogc { 1925 return ((width * pos.y) + pos.x); 1926 } 1927 1928 /// ditto 1929 int linearOffset(const int width, const Point pos) @safe pure nothrow @nogc { 1930 return ((width * pos.y) + pos.x); 1931 } 1932 1933 /// 1934 struct Rectangle { 1935 int left; /// 1936 int top; /// 1937 int right; /// 1938 int bottom; /// 1939 1940 pure const nothrow @safe @nogc: 1941 1942 /// 1943 this(int left, int top, int right, int bottom) { 1944 this.left = left; 1945 this.top = top; 1946 this.right = right; 1947 this.bottom = bottom; 1948 } 1949 1950 /// 1951 this(in Point upperLeft, in Point lowerRight) { 1952 this(upperLeft.x, upperLeft.y, lowerRight.x, lowerRight.y); 1953 } 1954 1955 /// 1956 this(in Point upperLeft, in Size size) { 1957 this(upperLeft.x, upperLeft.y, upperLeft.x + size.width, upperLeft.y + size.height); 1958 } 1959 1960 /// 1961 @property Point upperLeft() { 1962 return Point(left, top); 1963 } 1964 1965 /// 1966 @property Point upperRight() { 1967 return Point(right, top); 1968 } 1969 1970 /// 1971 @property Point lowerLeft() { 1972 return Point(left, bottom); 1973 } 1974 1975 /// 1976 @property Point lowerRight() { 1977 return Point(right, bottom); 1978 } 1979 1980 /// 1981 @property Point center() { 1982 return Point((right + left) / 2, (bottom + top) / 2); 1983 } 1984 1985 /// 1986 @property Size size() { 1987 return Size(width, height); 1988 } 1989 1990 /// 1991 @property int width() { 1992 return right - left; 1993 } 1994 1995 /// 1996 @property int height() { 1997 return bottom - top; 1998 } 1999 2000 /// Returns true if this rectangle entirely contains the other 2001 bool contains(in Rectangle r) { 2002 return contains(r.upperLeft) && contains(r.lowerRight); 2003 } 2004 2005 /// ditto 2006 bool contains(in Point p) { 2007 return (p.x >= left && p.x < right && p.y >= top && p.y < bottom); 2008 } 2009 2010 /// Returns true of the two rectangles at any point overlap 2011 bool overlaps(in Rectangle r) { 2012 // the -1 in here are because right and top are exclusive 2013 return !((right-1) < r.left || (r.right-1) < left || (bottom-1) < r.top || (r.bottom-1) < top); 2014 } 2015 2016 /++ 2017 Returns a Rectangle representing the intersection of this and the other given one. 2018 2019 History: 2020 Added July 1, 2021 2021 +/ 2022 Rectangle intersectionOf(in Rectangle r) { 2023 auto tmp = Rectangle(max(left, r.left), max(top, r.top), min(right, r.right), min(bottom, r.bottom)); 2024 if(tmp.left >= tmp.right || tmp.top >= tmp.bottom) 2025 tmp = Rectangle.init; 2026 2027 return tmp; 2028 } 2029 } 2030 2031 /++ 2032 A type to represent an angle, taking away ambiguity of if it wants degrees or radians. 2033 2034 --- 2035 Angle a = Angle.degrees(180); 2036 Angle b = Angle.radians(3.14159); 2037 2038 // note there might be slight changes in precision due to internal conversions 2039 --- 2040 2041 History: 2042 Added August 29, 2023 (dub v11.1) 2043 +/ 2044 struct Angle { 2045 private enum PI = 3.14159265358979; 2046 private float angle; 2047 2048 pure @nogc nothrow @safe: 2049 2050 private this(float angle) { 2051 this.angle = angle; 2052 } 2053 2054 /++ 2055 2056 +/ 2057 float degrees() const { 2058 return angle * 180.0 / PI; 2059 } 2060 2061 /// ditto 2062 static Angle degrees(float deg) { 2063 return Angle(deg * PI / 180.0); 2064 } 2065 2066 /// ditto 2067 float radians() const { 2068 return angle; 2069 } 2070 2071 /// ditto 2072 static Angle radians(float rad) { 2073 return Angle(rad); 2074 } 2075 2076 /++ 2077 The +, -, +=, and -= operators all work on the angles too. 2078 +/ 2079 Angle opBinary(string op : "+")(const Angle rhs) const { 2080 return Angle(this.angle + rhs.angle); 2081 } 2082 /// ditto 2083 Angle opBinary(string op : "-")(const Angle rhs) const { 2084 return Angle(this.angle + rhs.angle); 2085 } 2086 /// ditto 2087 Angle opOpAssign(string op : "+")(const Angle rhs) { 2088 return this.angle += rhs.angle; 2089 } 2090 /// ditto 2091 Angle opOpAssign(string op : "-")(const Angle rhs) { 2092 return this.angle -= rhs.angle; 2093 } 2094 2095 // maybe sin, cos, tan but meh you can .radians on them too. 2096 } 2097 2098 private int max(int a, int b) @nogc nothrow pure @safe { 2099 return a >= b ? a : b; 2100 } 2101 private int min(int a, int b) @nogc nothrow pure @safe { 2102 return a <= b ? a : b; 2103 } 2104 2105 /++ 2106 Implements a flood fill algorithm, like the bucket tool in 2107 MS Paint. 2108 2109 Note it assumes `what.length == width*height`. 2110 2111 Params: 2112 what = the canvas to work with, arranged as top to bottom, left to right elements 2113 width = the width of the canvas 2114 height = the height of the canvas 2115 target = the type to replace. You may pass the existing value if you want to do what Paint does 2116 replacement = the replacement value 2117 x = the x-coordinate to start the fill (think of where the user clicked in Paint) 2118 y = the y-coordinate to start the fill 2119 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. 2120 +/ 2121 void floodFill(T)( 2122 T[] what, int width, int height, // the canvas to inspect 2123 T target, T replacement, // fill params 2124 int x, int y, bool delegate(int x, int y) @safe additionalCheck) // the node 2125 2126 // in(what.length == width * height) // gdc doesn't support this syntax yet so not gonna use it until that comes out. 2127 { 2128 assert(what.length == width * height); // will use the contract above when gdc supports it 2129 2130 T node = what[y * width + x]; 2131 2132 if(target == replacement) return; 2133 2134 if(node != target) return; 2135 2136 if(additionalCheck is null) 2137 additionalCheck = (int, int) => true; 2138 2139 if(!additionalCheck(x, y)) 2140 return; 2141 2142 Point[] queue; 2143 2144 queue ~= Point(x, y); 2145 2146 while(queue.length) { 2147 auto n = queue[0]; 2148 queue = queue[1 .. $]; 2149 //queue.assumeSafeAppend(); // lol @safe breakage 2150 2151 auto w = n; 2152 int offset = cast(int) (n.y * width + n.x); 2153 auto e = n; 2154 auto eoffset = offset; 2155 w.x--; 2156 offset--; 2157 while(w.x >= 0 && what[offset] == target && additionalCheck(w.x, w.y)) { 2158 w.x--; 2159 offset--; 2160 } 2161 while(e.x < width && what[eoffset] == target && additionalCheck(e.x, e.y)) { 2162 e.x++; 2163 eoffset++; 2164 } 2165 2166 // to make it inclusive again 2167 w.x++; 2168 offset++; 2169 foreach(o ; offset .. eoffset) { 2170 what[o] = replacement; 2171 if(w.y && what[o - width] == target && additionalCheck(w.x, w.y)) 2172 queue ~= Point(w.x, w.y - 1); 2173 if(w.y + 1 < height && what[o + width] == target && additionalCheck(w.x, w.y)) 2174 queue ~= Point(w.x, w.y + 1); 2175 w.x++; 2176 } 2177 } 2178 2179 /+ 2180 what[y * width + x] = replacement; 2181 2182 if(x) 2183 floodFill(what, width, height, target, replacement, 2184 x - 1, y, additionalCheck); 2185 2186 if(x != width - 1) 2187 floodFill(what, width, height, target, replacement, 2188 x + 1, y, additionalCheck); 2189 2190 if(y) 2191 floodFill(what, width, height, target, replacement, 2192 x, y - 1, additionalCheck); 2193 2194 if(y != height - 1) 2195 floodFill(what, width, height, target, replacement, 2196 x, y + 1, additionalCheck); 2197 +/ 2198 } 2199 2200 // for scripting, so you can tag it without strictly needing to import arsd.jsvar 2201 enum arsd_jsvar_compatible = "arsd_jsvar_compatible";