;;;; raylib [textures] example - mouse painting ;;;; ;;;; examples/textures/textures_mouse_painting.c. The one example in the ;;;; category that takes a picture back OFF the GPU and writes it to disk, and ;;;; that round trip is why it is here: ;;;; ;;;; load-image-from-texture → image-flip-vertical → export-image ;;;; ;;;; Nothing in the corpus had run it. image-from-image has a headless ;;;; acceptance case and sand.fln loads images the other way, but the path ;;;; that reads a render target's pixels back into CPU memory, corrects for ;;;; GL's bottom-up rows and encodes a PNG had never been exercised at all. ;;;; All three came out of the generated half of the bindings; nothing needed ;;;; adding for this file. ;;;; ;;;; The flip is the same fact as the negative source height in ;;;; examples/textures-fog-of-war.fln, met from the other side: on screen the ;;;; canvas is drawn with a negative-height source rectangle so GL's bottom-up ;;;; rows come out the right way up, and on save there is no source rectangle ;;;; to negate, so the pixels are turned over in memory instead. A program ;;;; that did one and not the other would show a correct picture and save an ;;;; upside-down one — which is exactly the bug the C's comment warns about. ;;;; ;;;; The other thing this puts under load is the render texture as *persistent ;;;; state*. The fog-of-war target is rebuilt from nothing every frame; this ;;;; one is the document — it is cleared once before the loop and every stroke ;;;; after that accumulates in it. Between the two the corpus has both ways a ;;;; render texture gets used. ;;;; ;;;; Two deviations from the C, both in the same place and both deliberate. ;;;; The C computes colorMouseHover with a `for` whose `else` clause resets it ;;;; on every miss and whose `break` leaves it set on a hit, so the value that ;;;; survives depends on the loop exiting early — it works, but only because ;;;; of the break. Here the reset is before the loop and the loop only ever ;;;; sets it, which is the same result said plainly. And "no colour hovered" ;;;; is -1 in both, tested with `>= 0` before it is used as an index. import rl "vendor:raylib" const screen-width = 800 const screen-height = 450 const max-colors-count = 23 ;; The palette strip along the top. colors[0] is also the canvas's clear ;; colour and the "eraser", which is why raywhite is first and not simply ;; another entry. once colors: [23 rl/Color] once colors-recs: [23 rl/Rectangle] once color-selected: i32 once color-selected-prev: i32 once color-mouse-hover: i32 once brush-size: f32 once mouse-was-pressed: bool once btn-save-rec: rl/Rectangle once btn-save-mouse-hover: bool once show-save-message: bool once save-message-counter: i32 once target: rl/RenderTexture2D fn main() -> () rl/init-window(screen-width, screen-height, "raylib [textures] example - mouse painting") defer rl/close-window() colors = array(max-colors-count, rl/Color) colors[0] = rl/raywhite colors[1] = rl/yellow colors[2] = rl/gold colors[3] = rl/orange colors[4] = rl/pink colors[5] = rl/red colors[6] = rl/maroon colors[7] = rl/green colors[8] = rl/lime colors[9] = rl/darkgreen colors[10] = rl/skyblue colors[11] = rl/blue colors[12] = rl/darkblue colors[13] = rl/purple colors[14] = rl/violet colors[15] = rl/darkpurple colors[16] = rl/beige colors[17] = rl/brown colors[18] = rl/darkbrown colors[19] = rl/lightgray colors[20] = rl/gray colors[21] = rl/darkgray colors[22] = rl/black ;; 30 wide with a 2-pixel gap, starting 10 from the left. colors-recs = array(max-colors-count, rl/Rectangle) for i in range(max-colors-count) colors-recs[i] = rl/Rectangle{.x 10.0 + 32.0 * f32(i), .y 10.0, .width 30.0, .height 30.0} color-selected = 0 color-selected-prev = 0 color-mouse-hover = 0 brush-size = 20.0 mouse-was-pressed = false btn-save-rec = rl/Rectangle{.x 750.0 .y 10.0 .width 40.0 .height 30.0} btn-save-mouse-hover = false show-save-message = false save-message-counter = 0 ;; The canvas. It is the document, not a scratch buffer: nothing clears it ;; again until the user presses C. target = rl/load-render-texture(screen-width, screen-height) defer rl/unload-render-texture(target) rl/with-texture-mode(target): rl/clear-background(colors[0]) ;; 120 and not 60: the stroke is a circle stamped at the mouse position once ;; per frame, so the gaps between stamps during a fast drag are a function ;; of the frame rate. The C's comment says as much. rl/set-target-fps(120) until rl/window-should-close() ;; Update let mouse-pos = rl/get-mouse-position() if rl/is-key-pressed(:key-right) color-selected += 1 elif rl/is-key-pressed(:key-left) color-selected -= 1 color-selected = clamp(color-selected, 0, max-colors-count - 1) ;; Which swatch the pointer is over, or -1. See the header comment: the ;; reset is here rather than in an else branch inside the loop. color-mouse-hover = -1 for i in range(max-colors-count) if rl/check-collision-point-rec(mouse-pos, colors-recs[i]) color-mouse-hover = i break if color-mouse-hover >= 0 and rl/is-mouse-button-pressed(:mouse-left) color-selected = color-mouse-hover color-selected-prev = color-selected brush-size = clamp(brush-size + rl/get-mouse-wheel-move() * 5.0, 2.0, 50.0) if rl/is-key-pressed(:key-c) rl/with-texture-mode(target): rl/clear-background(colors[0]) ;; Paint. The gesture test is what makes the example work on a ;; touchscreen, where there is no mouse button to hold. if rl/is-mouse-button-down(:mouse-left) or rl/get-gesture-detected() == :gesture-drag rl/with-texture-mode(target): ;; Above y=50 is the palette strip, and a stroke there would paint ;; under the toolbar where it could never be seen. if mouse-pos.y > 50.0 rl/draw-circle(i32(mouse-pos.x), i32(mouse-pos.y), brush-size, colors[color-selected]) ;; Right button erases, which is painting in the clear colour. The ;; selected swatch is parked while the button is held so the toolbar ;; shows what is being drawn, and restored on release. if rl/is-mouse-button-down(:mouse-right) if not mouse-was-pressed color-selected-prev = color-selected color-selected = 0 mouse-was-pressed = true rl/with-texture-mode(target): if mouse-pos.y > 50.0 rl/draw-circle(i32(mouse-pos.x), i32(mouse-pos.y), brush-size, colors[0]) elif rl/is-mouse-button-released(:mouse-right) and mouse-was-pressed color-selected = color-selected-prev mouse-was-pressed = false btn-save-mouse-hover = rl/check-collision-point-rec(mouse-pos, btn-save-rec) ;; The round trip. See the header comment for why the flip is here and ;; not on the draw. if (btn-save-mouse-hover and rl/is-mouse-button-released(:mouse-left)) or rl/is-key-pressed(:key-s) let image = rl/load-image-from-texture(target.texture) rl/image-flip-vertical(addr(image)) rl/export-image(image, "my_amazing_texture_painting.png") rl/unload-image(image) show-save-message = true if show-save-message save-message-counter += 1 if save-message-counter > 240 show-save-message = false save-message-counter = 0 ;; Draw rl/with-drawing: rl/clear-background(rl/raywhite) ;; The canvas, flipped by the negative source height. rl/draw-texture-rec(target.texture, rl/Rectangle{.x 0.0, .y 0.0, .width f32(target.texture.width), .height 0.0 - f32(target.texture.height)}, rl/Vector2{.x 0.0 .y 0.0}, rl/white) ;; The brush preview, drawn on the screen and not into the canvas. if mouse-pos.y > 50.0 if rl/is-mouse-button-down(:mouse-right) rl/draw-circle-lines(i32(mouse-pos.x), i32(mouse-pos.y), brush-size, rl/gray) else rl/draw-circle(rl/get-mouse-x(), rl/get-mouse-y(), brush-size, colors[color-selected]) ;; The toolbar, over the canvas. rl/draw-rectangle(0, 0, rl/get-screen-width(), 50, rl/raywhite) rl/draw-line(0, 50, rl/get-screen-width(), 50, rl/lightgray) for i in range(max-colors-count) rl/draw-rectangle-rec(colors-recs[i], colors[i]) ;; The first swatch is raywhite on raywhite, so it needs an outline to ;; be visible at all. rl/draw-rectangle-lines(10, 10, 30, 30, rl/lightgray) if color-mouse-hover >= 0 rl/draw-rectangle-rec(colors-recs[color-mouse-hover], rl/fade(rl/white, 0.6)) let r = colors-recs[color-selected] rl/draw-rectangle-lines-ex(rl/Rectangle{.x r.x - 2.0, .y r.y - 2.0, .width r.width + 4.0, .height r.height + 4.0}, 2.0, rl/black) rl/draw-rectangle-lines-ex(btn-save-rec, 2.0, if btn-save-mouse-hover then rl/red else rl/black) rl/draw-text("SAVE!", 755, 20, 10, if btn-save-mouse-hover then rl/red else rl/black) if show-save-message rl/draw-rectangle(0, 0, rl/get-screen-width(), rl/get-screen-height(), rl/fade(rl/raywhite, 0.8)) rl/draw-rectangle(0, 150, rl/get-screen-width(), 80, rl/black) rl/draw-text("IMAGE SAVED: my_amazing_texture_painting.png", 150, 180, 20, rl/raywhite)