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