1 module fluid.types; 2 3 @safe: 4 5 /// Get a hex code from color. 6 string toHex(string prefix = "#")(Color color) { 7 8 import std.format; 9 10 // Full alpha, use a six digit code 11 if (color.a == 0xff) { 12 13 return format!(prefix ~ "%02x%02x%02x")(color.r, color.g, color.b); 14 15 } 16 17 // Include alpha otherwise 18 else return format!(prefix ~ "%02x%02x%02x%02x")(color.tupleof); 19 20 } 21 22 unittest { 23 24 // No relevant alpha 25 assert(color("fff").toHex == "#ffffff"); 26 assert(color("ffff").toHex == "#ffffff"); 27 assert(color("ffffff").toHex == "#ffffff"); 28 assert(color("ffffffff").toHex == "#ffffff"); 29 assert(color("fafbfc").toHex == "#fafbfc"); 30 assert(color("123").toHex == "#112233"); 31 32 // Alpha set 33 assert(color("c0fe").toHex == "#cc00ffee"); 34 assert(color("1234").toHex == "#11223344"); 35 assert(color("0000").toHex == "#00000000"); 36 assert(color("12345678").toHex == "#12345678"); 37 38 } 39 40 /// Create a color from RGBA values. 41 Color color(ubyte r, ubyte g, ubyte b, ubyte a = ubyte.max) pure nothrow { 42 43 Color color; 44 color.r = r; 45 color.g = g; 46 color.b = b; 47 color.a = a; 48 return color; 49 50 } 51 52 /// Create a color from hex code. 53 Color color(string hexCode)() { 54 55 return color(hexCode); 56 57 } 58 59 /// ditto 60 Color color(string hexCode) pure { 61 62 import std.conv: to; 63 import std.string : chompPrefix; 64 65 // Remove the # if there is any 66 const hex = hexCode.chompPrefix("#"); 67 68 Color result; 69 result.a = 0xff; 70 71 switch (hex.length) { 72 73 // 4 digit RGBA 74 case 4: 75 result.a = hex[3..4].to!ubyte(16); 76 result.a *= 17; 77 78 // Parse the rest like RGB 79 goto case; 80 81 // 3 digit RGB 82 case 3: 83 result.r = hex[0..1].to!ubyte(16); 84 result.g = hex[1..2].to!ubyte(16); 85 result.b = hex[2..3].to!ubyte(16); 86 result.r *= 17; 87 result.g *= 17; 88 result.b *= 17; 89 break; 90 91 // 8 digit RGBA 92 case 8: 93 result.a = hex[6..8].to!ubyte(16); 94 goto case; 95 96 // 6 digit RGB 97 case 6: 98 result.r = hex[0..2].to!ubyte(16); 99 result.g = hex[2..4].to!ubyte(16); 100 result.b = hex[4..6].to!ubyte(16); 101 break; 102 103 default: 104 assert(false, "Invalid hex code length"); 105 106 } 107 108 return result; 109 110 } 111 112 unittest { 113 114 import std.exception; 115 116 assert(color!"#123" == Color(0x11, 0x22, 0x33, 0xff)); 117 assert(color!"#1234" == Color(0x11, 0x22, 0x33, 0x44)); 118 assert(color!"1234" == Color(0x11, 0x22, 0x33, 0x44)); 119 assert(color!"123456" == Color(0x12, 0x34, 0x56, 0xff)); 120 assert(color!"2a5592f0" == Color(0x2a, 0x55, 0x92, 0xf0)); 121 122 assertThrown(color!"ag5"); 123 124 } 125 126 /// Set the alpha channel for the given color, as a float. 127 Color setAlpha(Color color, float alpha) pure nothrow { 128 129 import std.algorithm : clamp; 130 131 color.a = cast(ubyte) clamp(ubyte.max * alpha, 0, ubyte.max); 132 return color; 133 134 } 135 136 Color setAlpha()(Color, int) pure nothrow { 137 138 static assert(false, "Overload setAlpha(Color, int). Explicitly choose setAlpha(Color, float) (0...1 range) or " 139 ~ "setAlpha(Color, ubyte) (0...255 range)"); 140 141 } 142 143 /// Set the alpha channel for the given color, as a float. 144 Color setAlpha(Color color, ubyte alpha) pure nothrow { 145 146 color.a = alpha; 147 return color; 148 149 } 150 151 /// Blend two colors together; apply `top` on top of the `bottom` color. If `top` has maximum alpha, returns `top`. If 152 /// alpha is zero, returns `bottom`. 153 /// 154 /// BUG: This function is currently broken and returns incorrect results. 155 deprecated("alphaBlend is bugged and unused, it will be removed in Fluid 0.8.0") 156 Color alphaBlend(Color bottom, Color top) { 157 158 auto topA = cast(float) top.a / ubyte.max; 159 auto bottomA = (1 - topA) * cast(float) bottom.a / ubyte.max; 160 161 return Color( 162 cast(ubyte) (bottom.r * bottomA + top.r * topA), 163 cast(ubyte) (bottom.g * bottomA + top.g * topA), 164 cast(ubyte) (bottom.b * bottomA + top.b * topA), 165 cast(ubyte) (bottom.a * bottomA + top.a * topA), 166 ); 167 168 } 169 170 /// Multiple color values. 171 Color multiply(Color a, Color b) nothrow { 172 173 return Color( 174 cast(ubyte) (a.r * b.r / 255.0), 175 cast(ubyte) (a.g * b.g / 255.0), 176 cast(ubyte) (a.b * b.b / 255.0), 177 cast(ubyte) (a.a * b.a / 255.0), 178 ); 179 180 } 181 182 unittest { 183 184 assert(multiply(color!"#fff", color!"#a00") == color!"#a00"); 185 assert(multiply(color!"#1eff00", color!"#009bdd") == color!"#009b00"); 186 assert(multiply(color!"#aaaa", color!"#1234") == color!"#0b16222d"); 187 188 } 189 190 /// Generate an image filled with a given color. 191 /// 192 /// Note: Image data is GC-allocated. Make sure to keep a reference alive when passing to the backend. Do not use 193 /// `UnloadImage` if using Raylib. 194 static Image generateColorImage(int width, int height, Color color) { 195 196 // Generate each pixel 197 auto data = new Color[width * height]; 198 data[] = color; 199 200 return Image(data, width, height); 201 202 } 203 204 /// Generate a paletted image filled with 0-index pixels of given alpha value. 205 static Image generatePalettedImage(int width, int height, ubyte alpha) { 206 207 auto data = new PalettedColor[width * height]; 208 data[] = PalettedColor(0, alpha); 209 210 return Image(data, width, height); 211 212 } 213 214 /// Generate an alpha mask filled with given value. 215 static Image generateAlphaMask(int width, int height, ubyte value) { 216 217 auto data = new ubyte[width * height]; 218 data[] = value; 219 220 return Image(data, width, height); 221 222 } 223 224 225 /// A paletted pixel, for use in `palettedAlpha` images; Stores images using an index into a palette, along with an 226 /// alpha value. 227 struct PalettedColor { 228 229 ubyte index; 230 ubyte alpha; 231 232 } 233 234 /// Image available to the CPU. 235 struct Image { 236 237 enum Format { 238 239 /// RGBA, 8 bit per channel (32 bits per pixel). 240 rgba, 241 242 /// Paletted image with alpha channel (16 bits per pixel) 243 palettedAlpha, 244 245 /// Alpha-only image/mask (8 bits per pixel). 246 alpha, 247 248 } 249 250 Format format; 251 252 /// Image data. Make sure to access data relevant to the current format. 253 /// 254 /// Each format has associated data storage. `rgba` has `rgbaPixels`, `palettedAlpha` has `palettedAlphaPixels` and 255 /// `alpha` has `alphaPixels`. 256 Color[] rgbaPixels; 257 258 /// ditto 259 PalettedColor[] palettedAlphaPixels; 260 261 /// ditto 262 ubyte[] alphaPixels; 263 264 /// Palette data, if relevant. Access into an invalid palette index is equivalent to full white. 265 /// 266 /// For `palettedAlpha` images (and `PalettedColor` in general), the alpha value of each color in the palette is 267 /// ignored. 268 Color[] palette; 269 270 /// Width and height of the texture, **in dots**. The meaning of a dot is defined by `dpiX` and `dpiY` 271 int width, height; 272 273 /// Dots per inch for the X and Y axis. Defaults to 96, thus making a dot in the texture equivalent to a pixel. 274 /// 275 /// Applies only if used via `CanvasIO`. 276 int dpiX = 96, dpiY = 96; 277 278 /// This number should be incremented after editing the image to signal `CanvasIO` that a change has been made. 279 /// 280 /// Edits made using `Image`'s methods will *not* bump this number. It has to be incremented manually. 281 int revisionNumber; 282 283 /// Create an RGBA image. 284 this(Color[] rgbaPixels, int width, int height) pure nothrow { 285 286 this.format = Format.rgba; 287 this.rgbaPixels = rgbaPixels; 288 this.width = width; 289 this.height = height; 290 291 } 292 293 /// Create a paletted image. 294 this(PalettedColor[] palettedAlphaPixels, int width, int height) pure nothrow { 295 296 this.format = Format.palettedAlpha; 297 this.palettedAlphaPixels = palettedAlphaPixels; 298 this.width = width; 299 this.height = height; 300 301 } 302 303 /// Create an alpha mask. 304 this(ubyte[] alphaPixels, int width, int height) pure nothrow { 305 306 this.format = Format.alpha; 307 this.alphaPixels = alphaPixels; 308 this.width = width; 309 this.height = height; 310 311 } 312 313 Vector2 size() const pure nothrow { 314 return Vector2(width, height); 315 } 316 317 /// Returns: 318 /// Size of the image in dots. This is the factual size of the image. 319 Vector2 canvasSize() const pure nothrow { 320 return Vector2(width, height); 321 } 322 323 /// Returns: 324 /// Size of the image in pixels (not dots). This is the space the image will occupy 325 /// in the viewport. 326 deprecated("`Image.viewportSize()` yields incorrect results. " 327 ~ "Use `Image.viewportSize(Vector2)` instead. " 328 ~ "The original overload will be removed in Fluid 0.8.0.") 329 Vector2 viewportSize() const pure nothrow { 330 return Vector2( 331 width * 96f / dpiX, 332 height * 96f / dpiY 333 ); 334 } 335 336 /// Params: 337 /// dpi = DPI of the canvas. 338 /// Returns: 339 /// Size of the image in pixels (not dots). This is the space the image will occupy 340 /// in the viewport. 341 Vector2 viewportSize(Vector2 dpi) const pure nothrow { 342 return Vector2( 343 width * dpiX / dpi.x, 344 height * dpiY / dpi.y 345 ); 346 } 347 348 int area() const nothrow { 349 350 return width * height; 351 352 } 353 354 /// Get a palette entry at given index. 355 Color paletteColor(PalettedColor pixel) const pure nothrow { 356 357 // Valid index, return the color; Set alpha to match the pixel 358 if (pixel.index < palette.length) 359 return palette[pixel.index].setAlpha(pixel.alpha); 360 361 // Invalid index, return white 362 else 363 return color(0xff, 0xff, 0xff, pixel.alpha); 364 365 } 366 367 /// Get data of the image in raw form. 368 inout(void)[] data() inout pure nothrow { 369 370 final switch (format) { 371 372 case Format.rgba: 373 return rgbaPixels; 374 case Format.palettedAlpha: 375 return palettedAlphaPixels; 376 case Format.alpha: 377 return alphaPixels; 378 379 } 380 381 } 382 383 /// Get color at given position. Position must be in image bounds. 384 Color get(int x, int y) const { 385 386 const index = y * width + x; 387 388 final switch (format) { 389 390 case Format.rgba: 391 return rgbaPixels[index]; 392 case Format.palettedAlpha: 393 return paletteColor(palettedAlphaPixels[index]); 394 case Format.alpha: 395 return Color(0xff, 0xff, 0xff, alphaPixels[index]); 396 397 } 398 399 } 400 401 unittest { 402 403 auto colors = [ 404 PalettedColor(0, ubyte(0)), 405 PalettedColor(1, ubyte(127)), 406 PalettedColor(2, ubyte(127)), 407 PalettedColor(3, ubyte(255)), 408 ]; 409 410 auto image = Image(colors, 2, 2); 411 image.palette = [ 412 Color(0, 0, 0, 255), 413 Color(255, 0, 0, 255), 414 Color(0, 255, 0, 255), 415 Color(0, 0, 255, 255), 416 ]; 417 418 assert(image.get(0, 0) == Color(0, 0, 0, 0)); 419 assert(image.get(1, 0) == Color(255, 0, 0, 127)); 420 assert(image.get(0, 1) == Color(0, 255, 0, 127)); 421 assert(image.get(1, 1) == Color(0, 0, 255, 255)); 422 423 } 424 425 /// Set color at given position. Does nothing if position is out of bounds. 426 /// 427 /// The `set(int, int, Color)` overload only supports true color images. For paletted images, use 428 /// `set(int, int, PalettedColor)`. The latter can also be used for building true color images using a palette, if 429 /// one is supplied in the image at the time. 430 void set(int x, int y, Color color) { 431 432 if (x < 0 || y < 0) return; 433 if (x >= width || y >= height) return; 434 435 const index = y * width + x; 436 437 final switch (format) { 438 439 case Format.rgba: 440 rgbaPixels[index] = color; 441 return; 442 case Format.palettedAlpha: 443 assert(false, "Unsupported image format: Cannot `set` pixels by color in a paletted image."); 444 case Format.alpha: 445 alphaPixels[index] = color.a; 446 return; 447 448 } 449 450 } 451 452 /// ditto 453 void set(int x, int y, PalettedColor entry) { 454 455 if (x < 0 || y < 0) return; 456 if (x >= width || y >= height) return; 457 458 const index = y * width + x; 459 const color = paletteColor(entry); 460 461 final switch (format) { 462 463 case Format.rgba: 464 rgbaPixels[index] = color; 465 return; 466 case Format.palettedAlpha: 467 palettedAlphaPixels[index] = entry; 468 return; 469 case Format.alpha: 470 alphaPixels[index] = color.a; 471 return; 472 473 } 474 475 } 476 477 /// Clear the image, replacing every pixel with given color. 478 /// 479 /// The `clear(Color)` overload only supports true color images. For paletted images, use `clear(PalettedColor)`. 480 /// The latter can also be used for building true color images using a palette, if one is supplied in the image at 481 /// the time. 482 void clear(Color color) { 483 484 final switch (format) { 485 486 case Format.rgba: 487 rgbaPixels[] = color; 488 return; 489 case Format.palettedAlpha: 490 assert(false, "Unsupported image format: Cannot `clear` by color in a paletted image."); 491 case Format.alpha: 492 alphaPixels[] = color.a; 493 return; 494 495 } 496 497 } 498 499 /// ditto 500 void clear(PalettedColor entry) { 501 502 const color = paletteColor(entry); 503 504 final switch (format) { 505 506 case Format.rgba: 507 rgbaPixels[] = color; 508 return; 509 case Format.palettedAlpha: 510 palettedAlphaPixels[] = entry; 511 return; 512 case Format.alpha: 513 alphaPixels[] = color.a; 514 return; 515 516 } 517 518 } 519 520 /// Convert to an RGBA image. 521 /// 522 /// Does nothing if the image is already an RGBA image. If it's a paletted image, decodes the colors 523 /// using currently assigned palette. If it's an alpha mask, fills the image with white. 524 /// 525 /// Returns: 526 /// Self if already in RGBA format, or a newly made image by converting the data. 527 Image toRGBA() pure nothrow { 528 529 final switch (format) { 530 531 case Format.rgba: 532 return this; 533 534 case Format.palettedAlpha: 535 auto colors = new Color[palettedAlphaPixels.length]; 536 foreach (i, pixel; palettedAlphaPixels) { 537 colors[i] = paletteColor(pixel); 538 } 539 return Image(colors, width, height); 540 541 case Format.alpha: 542 auto colors = new Color[alphaPixels.length]; 543 foreach (i, pixel; alphaPixels) { 544 colors[i] = color(0xff, 0xff, 0xff, pixel); 545 } 546 return Image(colors, width, height); 547 548 } 549 550 } 551 552 string toString() const pure { 553 554 import std.array; 555 556 Appender!(char[]) text; 557 toString(text); 558 return text[]; 559 560 } 561 562 void toString(Writer)(Writer writer) const { 563 564 import std.conv; 565 import std.range; 566 567 put(writer, "Image("); 568 put(writer, format.to!string); 569 put(writer, ", 0x"); 570 put(writer, (cast(size_t) data.ptr).toChars!16); 571 put(writer, ", "); 572 if (format == Format.palettedAlpha) { 573 put(writer, "palette: "); 574 put(writer, palette.to!string); 575 put(writer, ", "); 576 } 577 put(writer, width.toChars); 578 put(writer, "x"); 579 put(writer, height.toChars); 580 put(writer, ", rev "); 581 put(writer, revisionNumber.toChars); 582 put(writer, ")"); 583 584 } 585 586 } 587 588 // Structures 589 version (Have_raylib_d) { 590 591 debug (Fluid_BuildMessages) { 592 pragma(msg, "Fluid: Using Raylib core structures"); 593 } 594 595 import raylib; 596 597 alias Rectangle = raylib.Rectangle; 598 alias Vector2 = raylib.Vector2; 599 alias Color = raylib.Color; 600 601 } 602 603 else { 604 605 struct Vector2 { 606 607 float x = 0; 608 float y = 0; 609 610 mixin Linear; 611 612 } 613 614 struct Rectangle { 615 616 float x, y; 617 float width, height; 618 619 alias w = width; 620 alias h = height; 621 622 } 623 624 struct Color { 625 626 ubyte r, g, b, a; 627 628 } 629 630 /// `mixin Linear` taken from [raylib-d](https://github.com/schveiguy/raylib-d), reformatted and without Rotor3 631 /// support. 632 /// 633 /// Licensed under the [z-lib license](https://github.com/schveiguy/raylib-d/blob/master/LICENSE). 634 private mixin template Linear() { 635 636 private static alias T = typeof(this); 637 private import std.traits : FieldNameTuple; 638 639 static T zero() { 640 641 enum fragment = { 642 string result; 643 static foreach(i; 0 .. T.tupleof.length) 644 result ~= "0,"; 645 return result; 646 }(); 647 648 return mixin("T(", fragment, ")"); 649 } 650 651 static T one() { 652 653 enum fragment = { 654 string result; 655 static foreach(i; 0 .. T.tupleof.length) 656 result ~= "1,"; 657 return result; 658 }(); 659 return mixin("T(", fragment, ")"); 660 661 } 662 663 inout T opUnary(string op)() if (op == "+" || op == "-") { 664 665 enum fragment = { 666 string result; 667 static foreach(fn; FieldNameTuple!T) 668 result ~= op ~ fn ~ ","; 669 return result; 670 }(); 671 return mixin("T(", fragment, ")"); 672 673 } 674 675 inout T opBinary(string op)(inout T rhs) if (op == "+" || op == "-") { 676 677 enum fragment = { 678 string result; 679 foreach(fn; FieldNameTuple!T) 680 result ~= fn ~ op ~ "rhs." ~ fn ~ ","; 681 return result; 682 }(); 683 return mixin("T(", fragment, ")"); 684 685 } 686 687 ref T opOpAssign(string op)(inout T rhs) if (op == "+" || op == "-") { 688 689 foreach (field; FieldNameTuple!T) 690 mixin(field, op, "= rhs.", field, ";"); 691 692 return this; 693 694 } 695 696 inout T opBinary(string op)(inout float rhs) if (op == "+" || op == "-" || op == "*" || op == "/") { 697 698 enum fragment = { 699 string result; 700 foreach(fn; FieldNameTuple!T) 701 result ~= fn ~ op ~ "rhs,"; 702 return result; 703 }(); 704 return mixin("T(", fragment, ")"); 705 706 } 707 708 inout T opBinaryRight(string op)(inout float lhs) if (op == "+" || op == "-" || op == "*" || op == "/") { 709 710 enum fragment = { 711 string result; 712 foreach(fn; FieldNameTuple!T) 713 result ~= "lhs" ~ op ~ fn ~ ","; 714 return result; 715 }(); 716 return mixin("T(", fragment, ")"); 717 718 } 719 720 ref T opOpAssign(string op)(inout float rhs) if (op == "+" || op == "-" || op == "*" || op == "/") { 721 722 foreach (field; FieldNameTuple!T) 723 mixin(field, op, "= rhs;"); 724 return this; 725 726 } 727 } 728 729 }