flan/examples/textures-mouse-painting.fln

214 lines
9.4 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.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)