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 }