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 /++ 1339 1340 This interface is likely going to grow, so if you implement it, expect to have to add more methods as you update the library. 1341 1342 History: 1343 Added to arsd.game August 26, 2024, moved to arsd.color April 20, 2025 1344 +/ 1345 @system 1346 interface BasicDrawing { 1347 void fillRectangle(Rectangle r, Color c); 1348 void outlinePolygon(Point[] vertexes, Color c); 1349 void drawText(Rectangle boundingBox, string text, Color c); 1350 } 1351 1352 1353 /// An image that consists of indexes into a color palette. Use [getAsTrueColorImage]() if you don't care about palettes 1354 class IndexedImage : MemoryImage { 1355 bool hasAlpha; 1356 1357 /// . 1358 Color[] palette; 1359 /// the data as indexes into the palette. Stored left to right, top to bottom, no padding. 1360 ubyte[] data; 1361 1362 override void clearInternal () nothrow @system {// @nogc { 1363 import core.memory : GC; 1364 // it is safe to call [GC.free] with `null` pointer. 1365 GC.free(GC.addrOf(palette.ptr)); palette = null; 1366 GC.free(GC.addrOf(data.ptr)); data = null; 1367 _width = _height = 0; 1368 } 1369 1370 /// . 1371 override int width() const pure nothrow @safe @nogc { 1372 return _width; 1373 } 1374 1375 /// . 1376 override int height() const pure nothrow @safe @nogc { 1377 return _height; 1378 } 1379 1380 /// . 1381 override IndexedImage clone() const pure nothrow @trusted { 1382 auto n = new IndexedImage(width, height); 1383 n.data[] = this.data[]; // the data member is already there, so array copy 1384 n.palette = this.palette.dup; // and here we need to allocate too, so dup 1385 n.hasAlpha = this.hasAlpha; 1386 return n; 1387 } 1388 1389 override Color getPixel(int x, int y) const pure nothrow @trusted @nogc { 1390 if (x >= 0 && y >= 0 && x < _width && y < _height) { 1391 size_t pos = cast(size_t)y*_width+x; 1392 if (pos >= data.length) return Color(0, 0, 0, 0); 1393 ubyte b = data.ptr[pos]; 1394 if (b >= palette.length) return Color(0, 0, 0, 0); 1395 return palette.ptr[b]; 1396 } else { 1397 return Color(0, 0, 0, 0); 1398 } 1399 } 1400 1401 override void setPixel(int x, int y, in Color clr) nothrow @trusted { 1402 if (x >= 0 && y >= 0 && x < _width && y < _height) { 1403 size_t pos = cast(size_t)y*_width+x; 1404 if (pos >= data.length) return; 1405 ubyte pidx = findNearestColor(palette, clr); 1406 if (palette.length < 255 && 1407 (palette.ptr[pidx].r != clr.r || palette.ptr[pidx].g != clr.g || palette.ptr[pidx].b != clr.b || palette.ptr[pidx].a != clr.a)) { 1408 // add new color 1409 pidx = addColor(clr); 1410 } 1411 data.ptr[pos] = pidx; 1412 } 1413 } 1414 1415 private int _width; 1416 private int _height; 1417 1418 /// . 1419 this(int w, int h) pure nothrow @safe { 1420 _width = w; 1421 _height = h; 1422 1423 // ensure that the computed size does not exceed basic address space limits 1424 assert(cast(ulong)w * h <= size_t.max); 1425 // upcast to avoid overflow for images larger than 536 Mpix 1426 data = new ubyte[cast(size_t)w*h]; 1427 } 1428 1429 /* 1430 void resize(int w, int h, bool scale) { 1431 1432 } 1433 */ 1434 1435 /// returns a new image 1436 override TrueColorImage getAsTrueColorImage() pure nothrow @safe { 1437 return convertToTrueColor(); 1438 } 1439 1440 /// Creates a new TrueColorImage based on this data 1441 TrueColorImage convertToTrueColor() const pure nothrow @trusted { 1442 auto tci = new TrueColorImage(width, height); 1443 foreach(i, b; data) { 1444 tci.imageData.colors[i] = palette[b]; 1445 } 1446 return tci; 1447 } 1448 1449 /// Gets an exact match, if possible, adds if not. See also: the findNearestColor free function. 1450 ubyte getOrAddColor(Color c) nothrow @trusted { 1451 foreach(i, co; palette) { 1452 if(c == co) 1453 return cast(ubyte) i; 1454 } 1455 1456 return addColor(c); 1457 } 1458 1459 /// Number of colors currently in the palette (note: palette entries are not necessarily used in the image data) 1460 int numColors() const pure nothrow @trusted @nogc { 1461 return cast(int) palette.length; 1462 } 1463 1464 /// Adds an entry to the palette, returning its index 1465 ubyte addColor(Color c) nothrow @trusted { 1466 assert(palette.length < 256); 1467 if(c.a != 255) 1468 hasAlpha = true; 1469 palette ~= c; 1470 1471 return cast(ubyte) (palette.length - 1); 1472 } 1473 } 1474 1475 /// An RGBA array of image data. Use the free function quantize() to convert to an IndexedImage 1476 class TrueColorImage : MemoryImage { 1477 // bool hasAlpha; 1478 // bool isGreyscale; 1479 1480 //ubyte[] data; // stored as rgba quads, upper left to right to bottom 1481 /// . 1482 struct Data { 1483 ubyte[] bytes; /// the data as rgba bytes. Stored left to right, top to bottom, no padding. 1484 // the union is no good because the length of the struct is wrong! 1485 1486 /// the same data as Color structs 1487 @trusted // the cast here is typically unsafe, but it is ok 1488 // here because I guarantee the layout, note the static assert below 1489 @property inout(Color)[] colors() inout pure nothrow @nogc { 1490 return cast(inout(Color)[]) bytes; 1491 } 1492 1493 static assert(Color.sizeof == 4); 1494 } 1495 1496 /// . 1497 Data imageData; 1498 alias imageData.bytes data; 1499 1500 int _width; 1501 int _height; 1502 1503 override void clearInternal () nothrow @system {// @nogc { 1504 import core.memory : GC; 1505 // it is safe to call [GC.free] with `null` pointer. 1506 GC.free(GC.addrOf(imageData.bytes.ptr)); imageData.bytes = null; 1507 _width = _height = 0; 1508 } 1509 1510 /// . 1511 override TrueColorImage clone() const pure nothrow @trusted { 1512 auto n = new TrueColorImage(width, height); 1513 n.imageData.bytes[] = this.imageData.bytes[]; // copy into existing array ctor allocated 1514 return n; 1515 } 1516 1517 /// . 1518 override int width() const pure nothrow @trusted @nogc { return _width; } 1519 ///. 1520 override int height() const pure nothrow @trusted @nogc { return _height; } 1521 1522 override Color getPixel(int x, int y) const pure nothrow @trusted @nogc { 1523 if (x >= 0 && y >= 0 && x < _width && y < _height) { 1524 size_t pos = cast(size_t)y*_width+x; 1525 return imageData.colors.ptr[pos]; 1526 } else { 1527 return Color(0, 0, 0, 0); 1528 } 1529 } 1530 1531 override void setPixel(int x, int y, in Color clr) nothrow @trusted { 1532 if (x >= 0 && y >= 0 && x < _width && y < _height) { 1533 size_t pos = cast(size_t)y*_width+x; 1534 if (pos < imageData.bytes.length/4) imageData.colors.ptr[pos] = clr; 1535 } 1536 } 1537 1538 /// . 1539 this(int w, int h) pure nothrow @safe { 1540 _width = w; 1541 _height = h; 1542 1543 // ensure that the computed size does not exceed basic address space limits 1544 assert(cast(ulong)w * h * 4 <= size_t.max); 1545 // upcast to avoid overflow for images larger than 536 Mpix 1546 imageData.bytes = new ubyte[cast(size_t)w * h * 4]; 1547 } 1548 1549 /// Creates with existing data. The data pointer is stored here. 1550 this(int w, int h, ubyte[] data) pure nothrow @safe { 1551 _width = w; 1552 _height = h; 1553 assert(cast(ulong)w * h * 4 <= size_t.max); 1554 assert(data.length == cast(size_t)w * h * 4); 1555 imageData.bytes = data; 1556 } 1557 1558 /// Returns this 1559 override TrueColorImage getAsTrueColorImage() pure nothrow @safe { 1560 return this; 1561 } 1562 } 1563 1564 /+ 1565 /// An RGB array of image data. 1566 class TrueColorImageWithoutAlpha : MemoryImage { 1567 struct Data { 1568 ubyte[] bytes; // the data as rgba bytes. Stored left to right, top to bottom, no padding. 1569 } 1570 1571 /// . 1572 Data imageData; 1573 1574 int _width; 1575 int _height; 1576 1577 override void clearInternal () nothrow @system {// @nogc { 1578 import core.memory : GC; 1579 // it is safe to call [GC.free] with `null` pointer. 1580 GC.free(imageData.bytes.ptr); imageData.bytes = null; 1581 _width = _height = 0; 1582 } 1583 1584 /// . 1585 override TrueColorImageWithoutAlpha clone() const pure nothrow @trusted { 1586 auto n = new TrueColorImageWithoutAlpha(width, height); 1587 n.imageData.bytes[] = this.imageData.bytes[]; // copy into existing array ctor allocated 1588 return n; 1589 } 1590 1591 /// . 1592 override int width() const pure nothrow @trusted @nogc { return _width; } 1593 ///. 1594 override int height() const pure nothrow @trusted @nogc { return _height; } 1595 1596 override Color getPixel(int x, int y) const pure nothrow @trusted @nogc { 1597 if (x >= 0 && y >= 0 && x < _width && y < _height) { 1598 uint pos = (y*_width+x) * 3; 1599 return Color(imageData.bytes[0], imageData.bytes[1], imageData.bytes[2], 255); 1600 } else { 1601 return Color(0, 0, 0, 0); 1602 } 1603 } 1604 1605 override void setPixel(int x, int y, in Color clr) nothrow @trusted { 1606 if (x >= 0 && y >= 0 && x < _width && y < _height) { 1607 uint pos = y*_width+x; 1608 //if (pos < imageData.bytes.length/4) imageData.colors.ptr[pos] = clr; 1609 // FIXME 1610 } 1611 } 1612 1613 /// . 1614 this(int w, int h) pure nothrow @safe { 1615 _width = w; 1616 _height = h; 1617 imageData.bytes = new ubyte[w*h*3]; 1618 } 1619 1620 /// Creates with existing data. The data pointer is stored here. 1621 this(int w, int h, ubyte[] data) pure nothrow @safe { 1622 _width = w; 1623 _height = h; 1624 assert(data.length == w * h * 3); 1625 imageData.bytes = data; 1626 } 1627 1628 /// 1629 override TrueColorImage getAsTrueColorImage() pure nothrow @safe { 1630 // FIXME 1631 //return this; 1632 } 1633 } 1634 +/ 1635 1636 1637 alias extern(C) int function(scope const void*, scope const void*) @system Comparator; 1638 @trusted void nonPhobosSort(T)(T[] obj, Comparator comparator) { 1639 import core.stdc.stdlib; 1640 qsort(obj.ptr, obj.length, typeof(obj[0]).sizeof, comparator); 1641 } 1642 1643 /// Converts true color to an indexed image. It uses palette as the starting point, adding entries 1644 /// until maxColors as needed. If palette is null, it creates a whole new palette. 1645 /// 1646 /// After quantizing the image, it applies a dithering algorithm. 1647 /// 1648 /// This is not written for speed. 1649 IndexedImage quantize(in TrueColorImage img, Color[] palette = null, in int maxColors = 256) 1650 // this is just because IndexedImage assumes ubyte palette values 1651 in { assert(maxColors <= 256); } 1652 do { 1653 int[Color] uses; 1654 foreach(pixel; img.imageData.colors) { 1655 if(auto i = pixel in uses) { 1656 (*i)++; 1657 } else { 1658 uses[pixel] = 1; 1659 } 1660 } 1661 1662 struct ColorUse { 1663 Color c; 1664 int uses; 1665 //string toString() { return c.toCssString() ~ " x " ~ to!string(uses); } 1666 int opCmp(ref const ColorUse co) const { 1667 return co.uses - uses; 1668 } 1669 extern(C) static int comparator(scope const void* lhs, scope const void* rhs) { 1670 return (cast(ColorUse*)rhs).uses - (cast(ColorUse*)lhs).uses; 1671 } 1672 } 1673 1674 ColorUse[] sorted; 1675 1676 foreach(color, count; uses) 1677 sorted ~= ColorUse(color, count); 1678 1679 uses = null; 1680 1681 nonPhobosSort(sorted, &ColorUse.comparator); 1682 // or, with phobos, but that adds 70ms to compile time 1683 //import std.algorithm.sorting : sort; 1684 //sort(sorted); 1685 1686 ubyte[Color] paletteAssignments; 1687 foreach(idx, entry; palette) 1688 paletteAssignments[entry] = cast(ubyte) idx; 1689 1690 // For the color assignments from the image, I do multiple passes, decreasing the acceptable 1691 // distance each time until we're full. 1692 1693 // This is probably really slow.... but meh it gives pretty good results. 1694 1695 auto ddiff = 32; 1696 outer: for(int d1 = 128; d1 >= 0; d1 -= ddiff) { 1697 auto minDist = d1*d1; 1698 if(d1 <= 64) 1699 ddiff = 16; 1700 if(d1 <= 32) 1701 ddiff = 8; 1702 foreach(possibility; sorted) { 1703 if(palette.length == maxColors) 1704 break; 1705 if(palette.length) { 1706 auto co = palette[findNearestColor(palette, possibility.c)]; 1707 auto pixel = possibility.c; 1708 1709 auto dr = cast(int) co.r - pixel.r; 1710 auto dg = cast(int) co.g - pixel.g; 1711 auto db = cast(int) co.b - pixel.b; 1712 1713 auto dist = dr*dr + dg*dg + db*db; 1714 // not good enough variety to justify an allocation yet 1715 if(dist < minDist) 1716 continue; 1717 } 1718 paletteAssignments[possibility.c] = cast(ubyte) palette.length; 1719 palette ~= possibility.c; 1720 } 1721 } 1722 1723 // Final pass: just fill in any remaining space with the leftover common colors 1724 while(palette.length < maxColors && sorted.length) { 1725 if(sorted[0].c !in paletteAssignments) { 1726 paletteAssignments[sorted[0].c] = cast(ubyte) palette.length; 1727 palette ~= sorted[0].c; 1728 } 1729 sorted = sorted[1 .. $]; 1730 } 1731 1732 1733 bool wasPerfect = true; 1734 auto newImage = new IndexedImage(img.width, img.height); 1735 newImage.palette = palette; 1736 foreach(idx, pixel; img.imageData.colors) { 1737 if(auto p = pixel in paletteAssignments) 1738 newImage.data[idx] = *p; 1739 else { 1740 // gotta find the closest one... 1741 newImage.data[idx] = findNearestColor(palette, pixel); 1742 wasPerfect = false; 1743 } 1744 } 1745 1746 if(!wasPerfect) 1747 floydSteinbergDither(newImage, img); 1748 1749 return newImage; 1750 } 1751 1752 /// Finds the best match for pixel in palette (currently by checking for minimum euclidean distance in rgb colorspace) 1753 ubyte findNearestColor(in Color[] palette, in Color pixel) nothrow pure @trusted @nogc { 1754 int best = 0; 1755 int bestDistance = int.max; 1756 foreach(pe, co; palette) { 1757 auto dr = cast(int) co.r - pixel.r; 1758 auto dg = cast(int) co.g - pixel.g; 1759 auto db = cast(int) co.b - pixel.b; 1760 int dist = dr*dr + dg*dg + db*db; 1761 1762 if(dist < bestDistance) { 1763 best = cast(int) pe; 1764 bestDistance = dist; 1765 } 1766 } 1767 1768 return cast(ubyte) best; 1769 } 1770 1771 /+ 1772 1773 // Quantizing and dithering test program 1774 1775 void main( ){ 1776 /* 1777 auto img = new TrueColorImage(256, 32); 1778 foreach(y; 0 .. img.height) { 1779 foreach(x; 0 .. img.width) { 1780 img.imageData.colors[x + y * img.width] = Color(x, y * (255 / img.height), 0); 1781 } 1782 } 1783 */ 1784 1785 TrueColorImage img; 1786 1787 { 1788 1789 import arsd.png; 1790 1791 struct P { 1792 ubyte[] range; 1793 void put(ubyte[] a) { range ~= a; } 1794 } 1795 1796 P range; 1797 import std.algorithm; // commented out 1798 1799 import std.stdio; // commented out 1800 writePngLazy(range, pngFromBytes(File("/home/me/nyesha.png").byChunk(4096)).byRgbaScanline.map!((line) { 1801 foreach(ref pixel; line.pixels) { 1802 continue; 1803 auto sum = cast(int) pixel.r + pixel.g + pixel.b; 1804 ubyte a = cast(ubyte)(sum / 3); 1805 pixel.r = a; 1806 pixel.g = a; 1807 pixel.b = a; 1808 } 1809 return line; 1810 })); 1811 1812 img = imageFromPng(readPng(range.range)).getAsTrueColorImage; 1813 1814 1815 } 1816 1817 1818 1819 auto qimg = quantize(img, null, 2); 1820 1821 import arsd.simpledisplay; 1822 auto win = new SimpleWindow(img.width, img.height * 3); 1823 auto painter = win.draw(); 1824 painter.drawImage(Point(0, 0), Image.fromMemoryImage(img)); 1825 painter.drawImage(Point(0, img.height), Image.fromMemoryImage(qimg)); 1826 floydSteinbergDither(qimg, img); 1827 painter.drawImage(Point(0, img.height * 2), Image.fromMemoryImage(qimg)); 1828 win.eventLoop(0); 1829 } 1830 +/ 1831 1832 /+ 1833 /// If the background is transparent, it simply erases the alpha channel. 1834 void removeTransparency(IndexedImage img, Color background) 1835 +/ 1836 1837 /// Perform alpha-blending of `fore` to this color, return new color. 1838 /// WARNING! This function does blending in RGB space, and RGB space is not linear! 1839 Color alphaBlend(Color foreground, Color background) pure nothrow @safe @nogc { 1840 //if(foreground.a == 255) 1841 //return foreground; 1842 if(foreground.a == 0) 1843 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) 1844 1845 static if (__VERSION__ > 2067) pragma(inline, true); 1846 return background.alphaBlend(foreground); 1847 } 1848 1849 /* 1850 /// Reduces the number of colors in a palette. 1851 void reducePaletteSize(IndexedImage img, int maxColors = 16) { 1852 1853 } 1854 */ 1855 1856 // I think I did this wrong... but the results aren't too bad so the bug can't be awful. 1857 /// Dithers img in place to look more like original. 1858 void floydSteinbergDither(IndexedImage img, in TrueColorImage original) nothrow @trusted { 1859 assert(img.width == original.width); 1860 assert(img.height == original.height); 1861 1862 auto buffer = new Color[](original.imageData.colors.length); 1863 1864 int x, y; 1865 1866 foreach(idx, c; original.imageData.colors) { 1867 auto n = img.palette[img.data[idx]]; 1868 int errorR = cast(int) c.r - n.r; 1869 int errorG = cast(int) c.g - n.g; 1870 int errorB = cast(int) c.b - n.b; 1871 1872 void doit(int idxOffset, int multiplier) { 1873 // if(idx + idxOffset < buffer.length) 1874 buffer[idx + idxOffset] = Color.fromIntegers( 1875 c.r + multiplier * errorR / 16, 1876 c.g + multiplier * errorG / 16, 1877 c.b + multiplier * errorB / 16, 1878 c.a 1879 ); 1880 } 1881 1882 if((x+1) != original.width) 1883 doit(1, 7); 1884 if((y+1) != original.height) { 1885 if(x != 0) 1886 doit(-1 + img.width, 3); 1887 doit(img.width, 5); 1888 if(x+1 != original.width) 1889 doit(1 + img.width, 1); 1890 } 1891 1892 img.data[idx] = findNearestColor(img.palette, buffer[idx]); 1893 1894 x++; 1895 if(x == original.width) { 1896 x = 0; 1897 y++; 1898 } 1899 } 1900 } 1901 1902 // these are just really useful in a lot of places where the color/image functions are used, 1903 // so I want them available with Color 1904 /++ 1905 2D location point 1906 +/ 1907 struct Point { 1908 int x; /// x-coordinate (aka abscissa) 1909 int y; /// y-coordinate (aka ordinate) 1910 1911 pure const nothrow @safe: 1912 1913 Point opBinary(string op)(in Point rhs) @nogc { 1914 return Point(mixin("x" ~ op ~ "rhs.x"), mixin("y" ~ op ~ "rhs.y")); 1915 } 1916 1917 Point opBinary(string op)(int rhs) @nogc { 1918 return Point(mixin("x" ~ op ~ "rhs"), mixin("y" ~ op ~ "rhs")); 1919 } 1920 1921 Size opCast(T : Size)() inout @nogc { 1922 return Size(x, y); 1923 } 1924 1925 /++ 1926 Calculates the point of linear offset in a rectangle. 1927 1928 `Offset = 0` is assumed to be equivalent to `Point(0,0)`. 1929 1930 See_also: 1931 [linearOffset] is the inverse function. 1932 1933 History: 1934 Added October 05, 2024. 1935 +/ 1936 static Point fromLinearOffset(int linearOffset, int width) @nogc { 1937 const y = (linearOffset / width); 1938 const x = (linearOffset % width); 1939 return Point(x, y); 1940 } 1941 } 1942 1943 /// 1944 struct Size { 1945 int width; /// 1946 int height; /// 1947 1948 pure nothrow @safe: 1949 1950 /++ 1951 Rectangular surface area 1952 1953 Calculates the surface area of a rectangle with dimensions equivalent to the width and height of the size. 1954 +/ 1955 int area() const @nogc { return width * height; } 1956 1957 Point opCast(T : Point)() inout @nogc { 1958 return Point(width, height); 1959 } 1960 1961 // gonna leave this undocumented for now since it might be removed later 1962 /+ + 1963 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). 1964 +/ 1965 Size opBinary(string op)(in Size rhs) const @nogc { 1966 return Size( 1967 mixin("width" ~ op ~ "rhs.width"), 1968 mixin("height" ~ op ~ "rhs.height"), 1969 ); 1970 } 1971 1972 Size opBinary(string op)(int rhs) const @nogc { 1973 return Size( 1974 mixin("width" ~ op ~ "rhs"), 1975 mixin("height" ~ op ~ "rhs"), 1976 ); 1977 } 1978 } 1979 1980 /++ 1981 Calculates the linear offset of a point 1982 from the start (0/0) of a rectangle. 1983 1984 This assumes that (0/0) is equivalent to offset `0`. 1985 Each step on the x-coordinate advances the resulting offset by `1`. 1986 Each step on the y-coordinate advances the resulting offset by `width`. 1987 1988 This function is only defined for the 1st quadrant, 1989 i.e. both coordinates (x and y) of `pos` are positive. 1990 1991 Returns: 1992 `y * width + x` 1993 1994 See_also: 1995 [Point.fromLinearOffset] is the inverse function. 1996 1997 History: 1998 Added December 19, 2023 (dub v11.4) 1999 +/ 2000 int linearOffset(const Point pos, const int width) @safe pure nothrow @nogc { 2001 return ((width * pos.y) + pos.x); 2002 } 2003 2004 /// ditto 2005 int linearOffset(const int width, const Point pos) @safe pure nothrow @nogc { 2006 return ((width * pos.y) + pos.x); 2007 } 2008 2009 /// 2010 struct Rectangle { 2011 int left; /// 2012 int top; /// 2013 int right; /// 2014 int bottom; /// 2015 2016 pure const nothrow @safe @nogc: 2017 2018 /// 2019 this(int left, int top, int right, int bottom) { 2020 this.left = left; 2021 this.top = top; 2022 this.right = right; 2023 this.bottom = bottom; 2024 } 2025 2026 /// 2027 this(in Point upperLeft, in Point lowerRight) { 2028 this(upperLeft.x, upperLeft.y, lowerRight.x, lowerRight.y); 2029 } 2030 2031 /// 2032 this(in Point upperLeft, in Size size) { 2033 this(upperLeft.x, upperLeft.y, upperLeft.x + size.width, upperLeft.y + size.height); 2034 } 2035 2036 /// 2037 @property Point upperLeft() { 2038 return Point(left, top); 2039 } 2040 2041 /// 2042 @property Point upperRight() { 2043 return Point(right, top); 2044 } 2045 2046 /// 2047 @property Point lowerLeft() { 2048 return Point(left, bottom); 2049 } 2050 2051 /// 2052 @property Point lowerRight() { 2053 return Point(right, bottom); 2054 } 2055 2056 /// 2057 @property Point center() { 2058 return Point((right + left) / 2, (bottom + top) / 2); 2059 } 2060 2061 /// 2062 @property Size size() { 2063 return Size(width, height); 2064 } 2065 2066 /// 2067 @property int width() { 2068 return right - left; 2069 } 2070 2071 /// 2072 @property int height() { 2073 return bottom - top; 2074 } 2075 2076 /// Returns true if this rectangle entirely contains the other 2077 bool contains(in Rectangle r) { 2078 return contains(r.upperLeft) && contains(r.lowerRight); 2079 } 2080 2081 /// ditto 2082 bool contains(in Point p) { 2083 return (p.x >= left && p.x < right && p.y >= top && p.y < bottom); 2084 } 2085 2086 /// Returns true of the two rectangles at any point overlap 2087 bool overlaps(in Rectangle r) { 2088 // the -1 in here are because right and top are exclusive 2089 return !((right-1) < r.left || (r.right-1) < left || (bottom-1) < r.top || (r.bottom-1) < top); 2090 } 2091 2092 /++ 2093 Returns a Rectangle representing the intersection of this and the other given one. 2094 2095 History: 2096 Added July 1, 2021 2097 +/ 2098 Rectangle intersectionOf(in Rectangle r) { 2099 auto tmp = Rectangle(max(left, r.left), max(top, r.top), min(right, r.right), min(bottom, r.bottom)); 2100 if(tmp.left >= tmp.right || tmp.top >= tmp.bottom) 2101 tmp = Rectangle.init; 2102 2103 return tmp; 2104 } 2105 } 2106 2107 /++ 2108 A type to represent an angle, taking away ambiguity of if it wants degrees or radians. 2109 2110 --- 2111 Angle a = Angle.degrees(180); 2112 Angle b = Angle.radians(3.14159); 2113 2114 // note there might be slight changes in precision due to internal conversions 2115 --- 2116 2117 History: 2118 Added August 29, 2023 (dub v11.1) 2119 +/ 2120 struct Angle { 2121 private enum PI = 3.14159265358979; 2122 private float angle; 2123 2124 pure @nogc nothrow @safe: 2125 2126 private this(float angle) { 2127 this.angle = angle; 2128 } 2129 2130 /++ 2131 2132 +/ 2133 float degrees() const { 2134 return angle * 180.0 / PI; 2135 } 2136 2137 /// ditto 2138 static Angle degrees(float deg) { 2139 return Angle(deg * PI / 180.0); 2140 } 2141 2142 /// ditto 2143 float radians() const { 2144 return angle; 2145 } 2146 2147 /// ditto 2148 static Angle radians(float rad) { 2149 return Angle(rad); 2150 } 2151 2152 /++ 2153 The +, -, +=, and -= operators all work on the angles too. 2154 +/ 2155 Angle opBinary(string op : "+")(const Angle rhs) const { 2156 return Angle(this.angle + rhs.angle); 2157 } 2158 /// ditto 2159 Angle opBinary(string op : "-")(const Angle rhs) const { 2160 return Angle(this.angle + rhs.angle); 2161 } 2162 /// ditto 2163 Angle opOpAssign(string op : "+")(const Angle rhs) { 2164 return this.angle += rhs.angle; 2165 } 2166 /// ditto 2167 Angle opOpAssign(string op : "-")(const Angle rhs) { 2168 return this.angle -= rhs.angle; 2169 } 2170 2171 // maybe sin, cos, tan but meh you can .radians on them too. 2172 } 2173 2174 private int max(int a, int b) @nogc nothrow pure @safe { 2175 return a >= b ? a : b; 2176 } 2177 private int min(int a, int b) @nogc nothrow pure @safe { 2178 return a <= b ? a : b; 2179 } 2180 2181 /++ 2182 Implements a flood fill algorithm, like the bucket tool in 2183 MS Paint. 2184 2185 Note it assumes `what.length == width*height`. 2186 2187 Params: 2188 what = the canvas to work with, arranged as top to bottom, left to right elements 2189 width = the width of the canvas 2190 height = the height of the canvas 2191 target = the type to replace. You may pass the existing value if you want to do what Paint does 2192 replacement = the replacement value 2193 x = the x-coordinate to start the fill (think of where the user clicked in Paint) 2194 y = the y-coordinate to start the fill 2195 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. 2196 +/ 2197 void floodFill(T)( 2198 T[] what, int width, int height, // the canvas to inspect 2199 T target, T replacement, // fill params 2200 int x, int y, bool delegate(int x, int y) @safe additionalCheck) // the node 2201 2202 // in(what.length == width * height) // gdc doesn't support this syntax yet so not gonna use it until that comes out. 2203 { 2204 assert(what.length == width * height); // will use the contract above when gdc supports it 2205 2206 T node = what[y * width + x]; 2207 2208 if(target == replacement) return; 2209 2210 if(node != target) return; 2211 2212 if(additionalCheck is null) 2213 additionalCheck = (int, int) => true; 2214 2215 if(!additionalCheck(x, y)) 2216 return; 2217 2218 Point[] queue; 2219 2220 queue ~= Point(x, y); 2221 2222 while(queue.length) { 2223 auto n = queue[0]; 2224 queue = queue[1 .. $]; 2225 //queue.assumeSafeAppend(); // lol @safe breakage 2226 2227 auto w = n; 2228 int offset = cast(int) (n.y * width + n.x); 2229 auto e = n; 2230 auto eoffset = offset; 2231 w.x--; 2232 offset--; 2233 while(w.x >= 0 && what[offset] == target && additionalCheck(w.x, w.y)) { 2234 w.x--; 2235 offset--; 2236 } 2237 while(e.x < width && what[eoffset] == target && additionalCheck(e.x, e.y)) { 2238 e.x++; 2239 eoffset++; 2240 } 2241 2242 // to make it inclusive again 2243 w.x++; 2244 offset++; 2245 foreach(o ; offset .. eoffset) { 2246 what[o] = replacement; 2247 if(w.y && what[o - width] == target && additionalCheck(w.x, w.y)) 2248 queue ~= Point(w.x, w.y - 1); 2249 if(w.y + 1 < height && what[o + width] == target && additionalCheck(w.x, w.y)) 2250 queue ~= Point(w.x, w.y + 1); 2251 w.x++; 2252 } 2253 } 2254 2255 /+ 2256 what[y * width + x] = replacement; 2257 2258 if(x) 2259 floodFill(what, width, height, target, replacement, 2260 x - 1, y, additionalCheck); 2261 2262 if(x != width - 1) 2263 floodFill(what, width, height, target, replacement, 2264 x + 1, y, additionalCheck); 2265 2266 if(y) 2267 floodFill(what, width, height, target, replacement, 2268 x, y - 1, additionalCheck); 2269 2270 if(y != height - 1) 2271 floodFill(what, width, height, target, replacement, 2272 x, y + 1, additionalCheck); 2273 +/ 2274 } 2275 2276 // for scripting, so you can tag it without strictly needing to import arsd.jsvar 2277 enum arsd_jsvar_compatible = "arsd_jsvar_compatible";