flan/examples/textures-mouse-painting.flan
Joseph Ferano d7ceec448e Ten more raylib examples, and the three small structs 3D needed
Three shapes, two text, three textures, one models and one core, picked for
binding surface rather than for how they look.

shapes-basic-shapes brings in six draw families nothing had called — the
circle and rectangle gradients, the triangles, all three poly draws — and is
the first call in the corpus to pass two Colors or three Vector2s at once.
shapes-collision-area is get-collision-rec, the only binding that takes two
Rectangles and answers a third, on a frame path. shapes-following-eyes is the
raymath gap measured rather than worked around: every line of it is vector
arithmetic written without a vector library, the way the C writes it.

text-input-box drains get-char-pressed's queue, which no example had read,
and needed a MouseCursor defenum for set-mouse-cursor. text-writing-anim
replaces TextSubtext — unbindable, it answers a pointer into a rotating
static buffer — with (string (slice b 0 n)), which is the same operation
without the shared state.

textures-image-generation runs nine Gen* calls and the
gen/upload/unload-image path, all procedural, no file on disk.
textures-fog-of-war needed a TextureFilter defenum: the smooth fog edge is
entirely :bilinear on a 25x15 render texture, and it is also the first
draw-texture-pro with a negative source height. textures-mouse-painting is
the same render texture used as a document rather than as scratch, plus the
round trip back off the GPU — load-image-from-texture, image-flip-vertical,
export-image — which nothing had run.

models-box-collisions is the counterexample to "a models example is a binding
exercise": nothing in it is a Model, and one BoundingBox defstruct un-refuses
four functions. core-3d-picking is the only caller anywhere for Ray and
RayCollision, and picking is the inverse of the get-world-to-screen the
corpus already had.

Added to vendor/raylib: defstructs BoundingBox, Ray and RayCollision;
defenums MouseCursor and TextureFilter with their mapping lines in bindings;
hand-written declare-c for SetMouseCursor, SetTextureFilter, DrawCubeV,
DrawSphere, DrawSphereWires, DrawRay and GetScreenToWorldRay, each excluded
from the generated half on the rule bindings already states. generated.flan
regenerated against raylib 5.5: 272 declarations, 117 refused, every
defstruct, hand-written declare-c and mapped constant agreeing with the
header.
2026-09-13 14:42:49 +07:00

256 lines
10 KiB
Plaintext

;;;; 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.flan 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.flan, 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")
(defconst screen-width 800)
(defconst screen-height 450)
(defconst 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.
(defvar colors [23 rl/Color])
(defvar colors-recs [23 rl/Rectangle])
(defvar color-selected i32)
(defvar color-selected-prev i32)
(defvar color-mouse-hover i32)
(defvar brush-size f32)
(defvar mouse-was-pressed bool)
(defvar btn-save-rec rl/Rectangle)
(defvar btn-save-mouse-hover bool)
(defvar show-save-message bool)
(defvar save-message-counter i32)
(defvar target rl/RenderTexture2D)
(defn main [] ()
(rl/init-window screen-width screen-height
"raylib [textures] example - mouse painting")
(defer (rl/close-window))
(set colors (array max-colors-count rl/Color))
(set (at colors 0) rl/raywhite)
(set (at colors 1) rl/yellow)
(set (at colors 2) rl/gold)
(set (at colors 3) rl/orange)
(set (at colors 4) rl/pink)
(set (at colors 5) rl/red)
(set (at colors 6) rl/maroon)
(set (at colors 7) rl/green)
(set (at colors 8) rl/lime)
(set (at colors 9) rl/darkgreen)
(set (at colors 10) rl/skyblue)
(set (at colors 11) rl/blue)
(set (at colors 12) rl/darkblue)
(set (at colors 13) rl/purple)
(set (at colors 14) rl/violet)
(set (at colors 15) rl/darkpurple)
(set (at colors 16) rl/beige)
(set (at colors 17) rl/brown)
(set (at colors 18) rl/darkbrown)
(set (at colors 19) rl/lightgray)
(set (at colors 20) rl/gray)
(set (at colors 21) rl/darkgray)
(set (at colors 22) rl/black)
;; 30 wide with a 2-pixel gap, starting 10 from the left.
(set colors-recs (array max-colors-count rl/Rectangle))
(dotimes [i max-colors-count]
(set (at colors-recs i)
(rl/Rectangle {.x (+ 10.0 (* 32.0 (f32 i)))
.y 10.0 .width 30.0 .height 30.0})))
(set color-selected 0)
(set color-selected-prev 0)
(set color-mouse-hover 0)
(set brush-size 20.0)
(set mouse-was-pressed false)
(set btn-save-rec (rl/Rectangle {.x 750.0 .y 10.0 .width 40.0 .height 30.0}))
(set btn-save-mouse-hover false)
(set show-save-message false)
(set save-message-counter 0)
;; The canvas. It is the document, not a scratch buffer: nothing clears it
;; again until the user presses C.
(set target (rl/load-render-texture screen-width screen-height))
(defer (rl/unload-render-texture target))
(rl/begin-texture-mode target)
(rl/clear-background (at colors 0))
(rl/end-texture-mode)
;; 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/key-pressed? :right)
(set color-selected (+ color-selected 1))
(when (rl/key-pressed? :left)
(set color-selected (- color-selected 1))))
(set 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.
(set color-mouse-hover -1)
(dotimes [i max-colors-count]
(when (rl/collision-point-rec? mouse-pos (at colors-recs i))
(set color-mouse-hover i)
(break)))
(when (and (>= color-mouse-hover 0) (rl/mouse-button-pressed? :left))
(set color-selected color-mouse-hover)
(set color-selected-prev color-selected))
(set brush-size (clamp (+ brush-size (* (rl/get-mouse-wheel-move) 5.0))
2.0 50.0))
(when (rl/key-pressed? :c)
(rl/begin-texture-mode target)
(rl/clear-background (at colors 0))
(rl/end-texture-mode))
;; Paint. The gesture test is what makes the example work on a
;; touchscreen, where there is no mouse button to hold.
(when (or (rl/mouse-button-down? :left)
(= (rl/get-gesture-detected) :drag))
(rl/begin-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.
(when (> (.y mouse-pos) 50.0)
(rl/draw-circle (i32 (.x mouse-pos)) (i32 (.y mouse-pos))
brush-size (at colors color-selected)))
(rl/end-texture-mode))
;; 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/mouse-button-down? :right)
(do
(when (not mouse-was-pressed)
(set color-selected-prev color-selected)
(set color-selected 0))
(set mouse-was-pressed true)
(rl/begin-texture-mode target)
(when (> (.y mouse-pos) 50.0)
(rl/draw-circle (i32 (.x mouse-pos)) (i32 (.y mouse-pos))
brush-size (at colors 0)))
(rl/end-texture-mode))
(when (and (rl/mouse-button-released? :right) mouse-was-pressed)
(set color-selected color-selected-prev)
(set mouse-was-pressed false)))
(set btn-save-mouse-hover (rl/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.
(when (or (and btn-save-mouse-hover (rl/mouse-button-released? :left))
(rl/key-pressed? :s))
(let [image (rl/load-image-from-texture (.texture target))]
(rl/image-flip-vertical (addr image))
(rl/export-image image "my_amazing_texture_painting.png")
(rl/unload-image image))
(set show-save-message true))
(when show-save-message
(set save-message-counter (+ save-message-counter 1))
(when (> save-message-counter 240)
(set show-save-message false)
(set save-message-counter 0)))
;; Draw
(rl/begin-drawing)
(rl/clear-background rl/raywhite)
;; The canvas, flipped by the negative source height.
(rl/draw-texture-rec (.texture target)
(rl/Rectangle {.x 0.0 .y 0.0
.width (f32 (.width (.texture target)))
.height (- 0.0 (f32 (.height (.texture target))))})
(rl/Vector2 {.x 0.0 .y 0.0})
rl/white)
;; The brush preview, drawn on the screen and not into the canvas.
(when (> (.y mouse-pos) 50.0)
(if (rl/mouse-button-down? :right)
(rl/draw-circle-lines (i32 (.x mouse-pos)) (i32 (.y mouse-pos))
brush-size rl/gray)
(rl/draw-circle (rl/get-mouse-x) (rl/get-mouse-y)
brush-size (at 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)
(dotimes [i max-colors-count]
(rl/draw-rectangle-rec (at colors-recs i) (at 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)
(when (>= color-mouse-hover 0)
(rl/draw-rectangle-rec (at colors-recs color-mouse-hover)
(rl/fade rl/white 0.6)))
(let [r (at colors-recs color-selected)]
(rl/draw-rectangle-lines-ex
(rl/Rectangle {.x (- (.x r) 2.0) .y (- (.y r) 2.0)
.width (+ (.width r) 4.0) .height (+ (.height r) 4.0)})
2.0 rl/black))
(rl/draw-rectangle-lines-ex btn-save-rec 2.0
(if btn-save-mouse-hover rl/red rl/black))
(rl/draw-text "SAVE!" 755 20 10
(if btn-save-mouse-hover rl/red rl/black))
(when 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))
(rl/end-drawing))))