1 /// This module contains interfaces for drawing geometry on a canvas.
2 module fluid.io.canvas;
3 
4 import optional;
5 
6 import fluid.types;
7 import fluid.utils;
8 import fluid.backend;
9 import fluid.future.context;
10 
11 @safe:
12 
13 /// I/O interface for canvas drawing functionality.
14 ///
15 /// The canvas should use a coordinate system where (0,0) is the top-left corner. Every increment of 1 is equivalent
16 /// to the distance of 1/96th of an inch. Consequentially, (96, 96) is 1 inch down and 1 inch right from the top-left
17 /// corner of the canvas.
18 ///
19 /// The canvas should allow all inputs and never throw. If there's a defined boundary, the canvas should crop all
20 /// geometry to fit.
21 interface CanvasIO : IO {
22 
23     /// Determines the screen's pixel density. A higher value will effectively scale up the interface, but keeping all
24     /// detail. The I/O system should trigger a resize when this changes.
25     ///
26     /// Note that this value refers to pixels in the physical sense, as in the dots on the screen, as opposed to pixels
27     /// as a unit. In other places, Fluid uses pixels (or "px") to refer to 1/96th of an inch.
28     ///
29     /// For primitive systems, a value of `(96, 96)` may be a good guess. If possible, please fetch this value from
30     /// the operating system.
31     ///
32     /// Returns:
33     ///     Current [dots-per-inch value](https://en.wikipedia.org/wiki/Dots_per_inch) per axis.
34     Vector2 dpi() const nothrow;
35 
36     /// Convert pixels to screen-dependent dots.
37     ///
38     /// In Fluid, pixel is a unit equal to 1/96th of an inch.
39     ///
40     /// Params:
41     ///     pixels = Value in pixels.
42     /// Returns:
43     ///     Corresponding value in dots.
44     final Vector2 toDots(Vector2 pixels) const nothrow {
45 
46         const dpi = this.dpi;
47 
48         return Vector2(
49             pixels.x * dpi.x / 96,
50             pixels.y * dpi.y / 96,
51         );
52 
53     }
54 
55     /// Measure distance in pixels taken by a number of dots.
56     ///
57     /// In Fluid, pixel is a unit equal to 1/96th of an inch.
58     ///
59     /// Params:
60     ///     dots = Value in dots.
61     /// Returns:
62     ///     Corresponding value in pixels.
63     final Vector2 fromDots(Vector2 dots) const nothrow {
64 
65         const dpi = this.dpi;
66 
67         return Vector2(
68             dots.x / dpi.x * 96,
69             dots.y / dpi.y * 96,
70         );
71 
72     }
73 
74     @("toDots/fromDots performs correct conversion")
75     unittest {
76 
77         import std.typecons;
78 
79         auto canvasIO = new class BlackHole!CanvasIO {
80 
81             override Vector2 dpi() const nothrow {
82                 return Vector2(96, 120);
83             }
84 
85         };
86 
87         assert(canvasIO.toDots(Vector2(10, 10)) == Vector2(10, 12.5));
88         assert(canvasIO.fromDots(Vector2(10, 10)) == Vector2(10, 8));
89 
90     }
91 
92     /// Getter for the current crop area, if one is set. Any shape drawn is cropped to fit this area on the canvas.
93     ///
94     /// This may be used by nodes to skip objects that are outside of the area. For this reason, a canvas system may
95     /// (and should) provide a value corresponding to the entire canvas, even if no crop area has been explicitly set.
96     ///
97     /// Returning an empty value may be desirable if the canvas is some form of persistent storage,
98     /// like a printable document or vector image, where the entire content may be displayed all at once.
99     ///
100     /// Crop area should be reset by Canvas I/O to its initial value before every frame.
101     ///
102     /// Returns:
103     ///     An area on the canvas that shapes can be drawn in.
104     Optional!Rectangle cropArea() const nothrow;
105 
106     /// Set an area the shapes can be drawn in. Any shape drawn after this call will be cropped to fit the specified
107     /// rectangle on the canvas.
108     ///
109     /// Calling this again will replace the old area. `resetCropArea` can be called to remove this area.
110     ///
111     /// Params:
112     ///     area = Area on the canvas to restrict all subsequently drawn shapes to.
113     ///         If passed an empty `Optional`, calls `resetCropArea`.
114     void cropArea(Rectangle area) nothrow;
115 
116     /// ditto
117     final void cropArea(Optional!Rectangle area) nothrow {
118 
119         // Reset the area if passed None()
120         if (area.empty) {
121             resetCropArea();
122         }
123 
124         // Crop otherwise
125         else {
126             cropArea(area.front);
127         }
128 
129     }
130 
131     /// Crop the area to fit. Unlike setting `cropArea`, this will not replace the old area, but create an intersection
132     /// between the new and old area.
133     /// Params:
134     ///     rectangle = New crop region.
135     /// Returns:
136     ///     The old region for later restoration.
137     final Optional!Rectangle intersectCrop(Rectangle rectangle) nothrow {
138 
139         import fluid.utils : intersect;
140 
141         const oldArea = cropArea;
142 
143         if (oldArea.empty) {
144             cropArea = rectangle;
145         }
146         else {
147             cropArea = intersect(oldArea.front, rectangle);
148         }
149 
150         return oldArea;
151 
152     }
153 
154     /// If `cropArea` was called before, this will reset set area, disabling the effect.
155     void resetCropArea() nothrow;
156 
157     /// Draw a triangle, consisting of 3 vertices with counter-clockwise winding.
158     /// Params:
159     ///     a     = First of the three points to connect.
160     ///     b     = Second of the three points to connect.
161     ///     c     = Third of the three points to connect.
162     ///     color = Color to fill the triangle with.
163     protected void drawTriangleImpl(Vector2 a, Vector2 b, Vector2 c, Color color) nothrow;
164 
165     /// ditto
166     final void drawTriangle(Vector2 a, Vector2 b, Vector2 c, Color color) nothrow {
167 
168         drawTriangleImpl(a, b, c,
169             multiply(treeContext.tint, color));
170 
171     }
172 
173     /// Draw an outline of a triangle.
174     /// Params:
175     ///     a     = First of the three points to connect.
176     ///     b     = Second of the three points to connect.
177     ///     c     = Third of the three points to connect.
178     ///     width = Width of each line.
179     ///     color = Color of the outline.
180     final void drawTriangleOutline(Vector2 a, Vector2 b, Vector2 c, float width, Color color) nothrow {
181 
182         drawLine(a, b, width, color);
183         drawLine(b, c, width, color);
184         drawLine(c, a, width, color);
185 
186     }
187 
188     /// Draw a circle.
189     /// Params:
190     ///     center = Position of the circle's center.
191     ///     radius = Radius of the circle.
192     ///     color  = Color to fill the circle with.
193     protected void drawCircleImpl(Vector2 center, float radius, Color color) nothrow;
194 
195     /// ditto
196     final void drawCircle(Vector2 center, float radius, Color color) nothrow {
197 
198         drawCircleImpl(center, radius,
199             multiply(treeContext.tint, color));
200 
201     }
202 
203     /// Draw the outline of a circle.
204     /// Params:
205     ///     center = Position of the circle's center.
206     ///     radius = Radius of the circle.
207     ///     width  = Width of the outline.
208     ///     color  = Color for the outline.
209     protected void drawCircleOutlineImpl(Vector2 center, float radius, float width, Color color) nothrow;
210 
211     /// ditto
212     final void drawCircleOutline(Vector2 center, float radius, float width, Color color) nothrow {
213 
214         drawCircleOutlineImpl(center, radius, width,
215             multiply(treeContext.tint, color));
216 
217     }
218 
219     /// Draw a rectangle.
220     /// Params:
221     ///     rectangle = Rectangle to draw.
222     ///     color     = Color to fill the rectangle with.
223     protected void drawRectangleImpl(Rectangle rectangle, Color color) nothrow;
224 
225     /// ditto
226     final void drawRectangle(Rectangle rectangle, Color color) nothrow {
227 
228         drawRectangleImpl(rectangle,
229             multiply(treeContext.tint, color));
230 
231     }
232 
233     /// Draw an outline of a rectangle.
234     /// Params:
235     ///     rectangle = Rectangle to draw an outline of.
236     ///     width = Width of each line.
237     ///     color = Color of the outline.
238     final void drawRectangleOutline(Rectangle rectangle, float width, Color color) nothrow {
239 
240         const a = Vector2(rectangle.start.x, rectangle.start.y);
241         const b = Vector2(rectangle.end.x,   rectangle.start.y);
242         const c = Vector2(rectangle.end.x,   rectangle.end.y);
243         const d = Vector2(rectangle.start.x, rectangle.end.y);
244 
245         drawLine(a, b, width, color);
246         drawLine(b, c, width, color);
247         drawLine(c, d, width, color);
248         drawLine(d, a, width, color);
249 
250     }
251 
252     /// Draw a line between two points.
253     /// Params:
254     ///     start = Start point of the line.
255     ///     end   = End point of the line.
256     ///     width = Width of the line.
257     ///     color = Color of the line.
258     protected void drawLineImpl(Vector2 start, Vector2 end, float width, Color color) nothrow;
259 
260     /// ditto
261     final void drawLine(Vector2 start, Vector2 end, float width, Color color) nothrow {
262 
263         drawLineImpl(start, end, width,
264             multiply(treeContext.tint, color));
265 
266     }
267 
268     /// Prepare an image for drawing. For hardware accelerated backends, this may involve uploading the texture
269     /// to the GPU.
270     ///
271     /// An image may be passed to this function even if it was already loaded. The field `image.data.ptr` can be used
272     /// to uniquely identify an image, so the canvas can use it to reuse previously prepared images. Additionally,
273     /// the `image.revisionNumber` field will increase if the image was updated, so the change should be reflected
274     /// in the canvas.
275     ///
276     /// There is no corresponding `unload` call. The canvas can instead unload images based on whether they
277     /// were loaded during a resize. This may look similar to this:
278     ///
279     /// ---
280     /// int resizeNumber;
281     /// void load(Image image) {
282     ///     // ... load the resource ...
283     ///     resource.lastResize = resizeNumber;
284     /// }
285     /// void resizeImpl(Vector2 space) {
286     ///     auto frame = this.implementIO();
287     ///     resizeNumber++;
288     ///     super.resizeImpl();
289     ///     foreach_reverse (ref resource; resources) {
290     ///         if (resource.lastResize < resizeNumber) {
291     ///             unload(resource);
292     ///             resource.isInvalid = true;
293     ///         }
294     ///     }
295     ///     return size;
296     /// }
297     /// ---
298     ///
299     /// Important:
300     ///     To make [partial resizing](https://git.samerion.com/Samerion/Fluid/issues/118) possible,
301     ///     `load` can also be called outside of `resizeImpl`.
302     ///
303     ///     Unloading resources may change resource indices, but `load` calls must then set the new indices.
304     /// Params:
305     ///     image = Image to prepare.
306     ///         The image may be uninitialized, in which case the image should still be valid, but simply empty.
307     ///         Attention should be paid to the `revisionNumber` field.
308     /// Returns:
309     ///     A number to be associated with the image. Interpretation of this number is up to the backend, but usually
310     ///     it will be an index in an array, since it is faster to look up than an associative array.
311     int load(Image image) nothrow;
312 
313     /// Draw an image on the canvas.
314     ///
315     /// `drawImage` is the usual method, which enables scaling and filtering, likely making it preferable
316     /// for most images. However, this may harm images that have been generated (like text) or adjusted to display
317     /// on the user's screen (like icons), so `drawHintedImage` may be preferrable. For similar reasons,
318     /// `drawHintedImage` may also be better for pixel art images.
319     ///
320     /// While specifics of `drawImage` are left to the implementation, `drawHintedImage` should directly blit
321     /// the image or use nearest-neighbor to scale, if needed. Image boundaries should be adjusted to align exactly
322     /// with the screen's pixel grid.
323     ///
324     /// Params:
325     ///     image       = Image to draw. The image must be prepared with `Node.load` before.
326     ///     destination = Position to place the image's top-left corner at or rectangle to fit the image in.
327     ///         The image should be stretched to fit this box.
328     ///     tint        = Color to modulate the image against. Every pixel in the image should be multiplied
329     ///         channel-wise by this color; values `0...255` should be mapped to `0...1`.
330     protected void drawImageImpl(DrawableImage image, Rectangle destination, Color tint) nothrow;
331 
332     /// ditto
333     final void drawImage(DrawableImage image, Rectangle destination, Color tint) nothrow {
334 
335         drawImageImpl(image, destination,
336             multiply(treeContext.tint, tint));
337 
338     }
339 
340     /// ditto
341     final void drawImage(DrawableImage image, Vector2 destination, Color tint) nothrow {
342 
343         const rect = Rectangle(destination.tupleof, image.width, image.height);
344         drawImageImpl(image, rect,
345             multiply(treeContext.tint, tint));
346 
347     }
348 
349     /// ditto
350     protected void drawHintedImageImpl(DrawableImage image, Rectangle destination, Color tint) nothrow;
351 
352     final void drawHintedImage(DrawableImage image, Rectangle destination, Color tint) nothrow {
353 
354         drawHintedImageImpl(image, destination,
355             multiply(treeContext.tint, tint));
356 
357     }
358 
359     /// ditto
360     final void drawHintedImage(DrawableImage image, Vector2 destination, Color tint) nothrow {
361 
362         const rect = Rectangle(destination.tupleof, image.width, image.height);
363         drawHintedImageImpl(image, rect,
364             multiply(treeContext.tint, tint));
365 
366     }
367 
368 }
369 
370 /// A `DrawableImage` is a variant of `Image` that can be associated with a `CanvasIO` in order to be drawn.
371 ///
372 /// Prepare images for drawing using `load()` in `resizeImpl`:
373 ///
374 /// ---
375 /// CanvasIO canvasIO;
376 /// DrawableImage image;
377 /// void resizeImpl(Vector2 space) {
378 ///     require(canvasIO);
379 ///     load(canvasIO, image);
380 /// }
381 /// ---
382 ///
383 /// Draw images in `drawImpl`:
384 ///
385 /// ---
386 /// void drawImpl(Rectangle outer, Rectangle inner) {
387 ///     image.draw(inner.start);
388 /// }
389 /// ---
390 struct DrawableImage {
391 
392     /// Image to be drawn.
393     Image image;
394 
395     /// Canvas IO responsible for drawing the image.
396     private CanvasIO _canvasIO;
397 
398     /// ID for the image assigned by the canvas.
399     private int _id;
400 
401     alias image this;
402 
403     /// Compare to another image image.
404     /// Params:
405     ///     other = Image to compare to.
406     /// Returns:
407     ///     True if it's the same image,
408     bool opEquals(const DrawableImage other) const {
409 
410         return opEquals(other.image);
411 
412     }
413 
414     /// ditto
415     bool opEquals(const Image other) const {
416 
417         // Do not compare I/O metadata
418         return image == other;
419 
420     }
421 
422     /// Assign an image to draw.
423     Image opAssign(Image other) {
424         this.image = other;
425         this._canvasIO = null;
426         this._id = 0;
427         return other;
428     }
429 
430     /// Returns: The ID/index assigned by `CanvasIO` when this image was loaded.
431     int id() const nothrow {
432         return this._id;
433     }
434 
435     void load(CanvasIO canvasIO, int id) nothrow {
436 
437         this._canvasIO = canvasIO;
438         this._id = id;
439 
440     }
441 
442     /// Draw the image.
443     ///
444     /// `draw` is the usual method, which enables scaling and filtering, likely making it preferable
445     /// for most images. However, for images that have been generated (like text) or adjusted to display
446     /// on the user's screen (like icons), `drawHinted` may be preferrable.
447     ///
448     /// See_Also:
449     ///     `CanvasIO.drawImage`,
450     ///     `CanvasIO.drawHintedImage`
451     /// Params:
452     ///     destination = Place in the canvas to draw the texture to.
453     ///         If a rectangle is given, the image will stretch to fix this box.
454     ///     tint        = Color to multiply the image by. Can be used to reduce opacity, darken or change color.
455     ///         Defaults to white (no change).
456     void draw(Rectangle destination, Color tint = Color(0xff, 0xff, 0xff, 0xff)) nothrow {
457         _canvasIO.drawImage(this, destination, tint);
458     }
459 
460     /// ditto
461     void draw(Vector2 destination, Color tint = Color(0xff, 0xff, 0xff, 0xff)) nothrow {
462         _canvasIO.drawImage(this, destination, tint);
463     }
464 
465     /// ditto
466     void drawHinted(Rectangle destination, Color tint = Color(0xff, 0xff, 0xff, 0xff)) nothrow {
467         _canvasIO.drawHintedImage(this, destination, tint);
468     }
469 
470     /// ditto
471     void drawHinted(Vector2 destination, Color tint = Color(0xff, 0xff, 0xff, 0xff)) nothrow {
472         _canvasIO.drawHintedImage(this, destination, tint);
473     }
474 
475 }
476 
477 /// Get the global program scale from the `FLUID_SCALE` environment variable.
478 ///
479 /// The variable is used by Fluid's built-in systems like `RaylibView` and is particularly useful
480 /// for testing Fluid's HiDPI display.
481 ///
482 /// The value is fetched anew every time this is called, and it is not cached.
483 ///
484 /// Returns:
485 ///     Globally applied scale as a vector for each axis.
486 Vector2 getGlobalScale() {
487 
488     import std.process : environment;
489 
490     return environment
491         .get("FLUID_SCALE", "1")
492         .toSizeVector2();
493 
494 }