flan/examples/textures-mouse-painting.flan
Joseph Ferano 52e9c92b0f The enum prefix goes uniform across all eleven
The author's ruling: "I think the prefix reads better, keep it" — so two
prefixed enums out of eleven was the inconsistency, not the prefix.
TraceLogLevel takes log-, CameraProjection projection-, CameraMode camera-,
GamepadButton button-, GamepadAxis axis-, Gesture gesture-, MouseCursor
cursor-, TextureFilter filter-, PixelFormat pixel-.

Two of those are judgement. CameraProjection and CameraMode share raylib's
CAMERA_ and deliberately do not share a Flan prefix: they are two questions
asked of the same struct, and :projection-perspective beside :camera-orbital
says which is being answered. GamepadButton and GamepadAxis take the short
stems rather than a shared gamepad-, which keeps :button-left-face-up and
:axis-left-trigger readable.

It is a reading choice and not a collision fix, and bindings, raylib.flan and
docs/BUILT.md all say so: a keyword resolves against the expected type and
nothing else, so :point at a TextureFilter site was never ambiguous. What the
prefix buys is the call site read on its own.

The three constant exception lines are keyed on the member's full Flan
spelling and moved with it. flan generate-c vendor/raylib is green against
raylib-5.5.h, and the check was confirmed non-vacuous by breaking it:
filter-trilinearr reported TEXTURE_FILTER_TRILINEARR rather than passing.
test_flan pins one member of each of the eleven to the C name the rule
reaches, read out of the real bindings file.

sand.flan line 121 is (rl/set-trace-log-level :warning) and is the author's
to respell. TraceLogLevel carries a warning alias beside log-warning, mapped
by name in bindings, so the suite stays green until he does; FIX.org has the
three-edit removal recipe.
2026-09-21 07:59:48 +07:00

250 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.
(defonce colors [23 rl/Color])
(defonce colors-recs [23 rl/Rectangle])
(defonce color-selected i32)
(defonce color-selected-prev i32)
(defonce color-mouse-hover i32)
(defonce brush-size f32)
(defonce mouse-was-pressed bool)
(defonce btn-save-rec rl/Rectangle)
(defonce btn-save-mouse-hover bool)
(defonce show-save-message bool)
(defonce save-message-counter i32)
(defonce 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/with-texture-mode target
(rl/clear-background (at 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/key-pressed? :key-right)
(set color-selected (+ color-selected 1))
(when (rl/key-pressed? :key-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? :mouse-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? :key-c)
(rl/with-texture-mode target
(rl/clear-background (at colors 0))))
;; 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? :mouse-left)
(= (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.
(when (> (.y mouse-pos) 50.0)
(rl/draw-circle (i32 (.x mouse-pos)) (i32 (.y mouse-pos))
brush-size (at 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/mouse-button-down? :mouse-right)
(do
(when (not mouse-was-pressed)
(set color-selected-prev color-selected)
(set color-selected 0))
(set mouse-was-pressed true)
(rl/with-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)))))
(when (and (rl/mouse-button-released? :mouse-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? :mouse-left))
(rl/key-pressed? :key-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/with-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? :mouse-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))))))