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 }