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