flan/examples/core-2d-camera.flan
Joseph Ferano 25f56c24a8 A begin/end pair that cannot come apart, and what it still cannot promise
vendor/raylib/modes.flan: five macros over the five pairs the package binds
-- with-drawing, with-mode-2d, with-mode-3d, with-texture-mode,
with-scissor-mode. A second file with no declare-c in it, split out on
vector.flan's reasoning: raylib.flan is the package's statement about C and
nothing here names C, so nothing here can be made wrong by raylib changing.

Each expands to (do (begin-... args) body... (end-...)) -- the calls the
author used to type, in the order they typed them. No let, no gensym: nothing
binds a name, so there is nothing for a caller's name to collide with.

What it removes is the End* that is missing, wrong, or no longer beside its
Begin*. What it cannot remove is a body leaving through the unwind path: a
return or an invoke-restart skips the rest of the do and the End* with it.
defer is the obvious fix and is refused inside a loop body, which is where a
pair always lives -- checked, not assumed. So sand.flan's discipline stays:
keep the restart boundary outside the pair.

35 call sites converted across examples/ and sand.flan. The one left is
core-scissor-test.flan, whose Begin and End sit in two separate `when`s with
the drawing between them -- a conditional pair is a shape a bracketing macro
cannot express.

test/programs/rl-with.flan covers with-scissor-mode, which no example can,
with a frame function unreachable from main so it needs no libraylib;
rl-with-reject.flan is the arity half.
2026-09-13 18:00:35 +07:00

138 lines
6.0 KiB
Plaintext

;;;; raylib [core] example - 2d camera
;;;;
;;;; examples/core/core_2d_camera.c. Needed no new binding at all, which is
;;;; the reason to port it: Camera2D, begin-mode-2d and end-mode-2d have been
;;;; in vendor/raylib/raylib.flan since the beginning and nothing in the
;;;; corpus had ever passed a whole Camera2D across the FFI on a frame's path.
;;;; Two Vector2s, a float and a float, by value, sixty times a second — the
;;;; acceptance table pins that layout with get-screen-to-world-2d, and this
;;;; is the same struct going into the call that actually draws with it.
;;;;
;;;; The hundred buildings are a `defvar` of fixed arrays rather than a Vec:
;;;; a global cannot hold a Vec (PORTING.md §3) and does not need to here,
;;;; because the count is a constant in the C too. `(array n T)` is the zeroed
;;;; fixed array, and the C's `= { 0 }` is exactly that.
;;;;
;;;; get-random-value comes from the generated half of the bindings. It is on
;;;; no frame's path — it runs once, before the loop — and its Flan face is
;;;; the C one, so PORTING.md §1's rule does not reach it.
;;;;
;;;; One deliberate difference from the C: `(- camera.rotation 1.0)` and the
;;;; clamp after it are written with the prelude's `clamp` macro rather than
;;;; two ifs. It is the same arithmetic; the C spells it out because C has no
;;;; clamp.
(import rl "vendor:raylib")
(defconst screen-width 800)
(defconst screen-height 450)
(defconst max-buildings 100)
(defvar buildings [100 rl/Rectangle])
(defvar build-colors [100 rl/Color])
(defvar player rl/Rectangle)
(defvar camera rl/Camera2D)
(defn main [] ()
(rl/init-window screen-width screen-height
"raylib [core] example - 2d camera")
(defer (rl/close-window))
(set player (rl/Rectangle {.x 400.0 .y 280.0 .width 40.0 .height 40.0}))
(set buildings (array max-buildings rl/Rectangle))
(set build-colors (array max-buildings rl/Color))
;; The skyline: each building is as wide as the last one left room for, so
;; the spacing accumulates and the row starts 6000 units to the left.
(let [spacing 0]
(dotimes [i max-buildings]
(let [w (f32 (rl/get-random-value 50 200))
h (f32 (rl/get-random-value 100 800))]
(set (at buildings i)
(rl/Rectangle {.x (+ -6000.0 (f32 spacing))
.y (- (- (f32 screen-height) 130.0) h)
.width w
.height h}))
(set spacing (+ spacing (i32 w)))
(set (at build-colors i)
(rl/Color {.r (u8 (rl/get-random-value 200 240))
.g (u8 (rl/get-random-value 200 240))
.b (u8 (rl/get-random-value 200 250))
.a 255})))))
;; A fresh Camera2D is all zeroes and a zoom of 0 makes the transform
;; singular, so the zoom is the one field that must be set — raylib.flan's
;; own comment on the struct says so.
(set camera (rl/Camera2D {.offset (rl/Vector2 {.x (/ (f32 screen-width) 2.0)
.y (/ (f32 screen-height) 2.0)})
.target (rl/Vector2 {.x (+ (.x player) 20.0)
.y (+ (.y player) 20.0)})
.rotation 0.0
.zoom 1.0}))
(rl/set-target-fps 60)
(until (rl/window-should-close?)
;; Update
;; The C's `if (RIGHT) ... else if (LEFT) ...`: both held cancels to the
;; right, which is the else-if and not an accident.
(if (rl/key-down? :right)
(set (.x player) (+ (.x player) 2.0))
(when (rl/key-down? :left) (set (.x player) (- (.x player) 2.0))))
;; The camera follows the player's centre.
(set (.target camera) (rl/Vector2 {.x (+ (.x player) 20.0)
.y (+ (.y player) 20.0)}))
(if (rl/key-down? :a)
(set (.rotation camera) (- (.rotation camera) 1.0))
(when (rl/key-down? :s)
(set (.rotation camera) (+ (.rotation camera) 1.0))))
(set (.rotation camera) (clamp (.rotation camera) -40.0 40.0))
(set (.zoom camera) (+ (.zoom camera) (* (rl/get-mouse-wheel-move) 0.05)))
(set (.zoom camera) (clamp (.zoom camera) 0.1 3.0))
(when (rl/key-pressed? :r)
(set (.zoom camera) 1.0)
(set (.rotation camera) 0.0))
;; Draw
(rl/with-drawing
(rl/clear-background rl/raywhite)
;; Everything inside with-mode-2d is in world space and goes through
;; the camera. The ground and the crosshair lines are drawn far outside
;; the window on purpose: at zoom 0.1 they still have to reach the edges.
(rl/with-mode-2d camera
(rl/draw-rectangle -6000 320 13000 8000 rl/darkgray)
(dotimes [i max-buildings]
(rl/draw-rectangle-rec (at buildings i) (at build-colors i)))
(rl/draw-rectangle-rec player rl/red)
(rl/draw-line (i32 (.x (.target camera))) (* screen-height -10)
(i32 (.x (.target camera))) (* screen-height 10) rl/green)
(rl/draw-line (* screen-width -10) (i32 (.y (.target camera)))
(* screen-width 10) (i32 (.y (.target camera))) rl/green))
;; And everything after it is in screen space again — the frame drawn
;; around the window is how the example shows the difference.
(rl/draw-text "SCREEN AREA" 640 10 20 rl/red)
(rl/draw-rectangle 0 0 screen-width 5 rl/red)
(rl/draw-rectangle 0 5 5 (- screen-height 10) rl/red)
(rl/draw-rectangle (- screen-width 5) 5 5 (- screen-height 10) rl/red)
(rl/draw-rectangle 0 (- screen-height 5) screen-width 5 rl/red)
(rl/draw-rectangle 10 10 250 113 (rl/fade rl/skyblue 0.5))
(rl/draw-rectangle-lines 10 10 250 113 rl/blue)
(rl/draw-text "Free 2d camera controls:" 20 20 10 rl/black)
(rl/draw-text "- Right/Left to move Offset" 40 40 10 rl/darkgray)
(rl/draw-text "- Mouse Wheel to Zoom in-out" 40 60 10 rl/darkgray)
(rl/draw-text "- A / S to Rotate" 40 80 10 rl/darkgray)
(rl/draw-text "- R to reset Zoom and Rotation" 40 100 10 rl/darkgray))))