;;;; 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.fln 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 `once` of fixed arrays rather than a Vec: ;;;; a global cannot hold a Vec (docs/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 docs/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" const screen-width = 800 const screen-height = 450 const max-buildings = 100 once buildings: [100 rl/Rectangle] once build-colors: [100 rl/Color] once player: rl/Rectangle once camera: rl/Camera2D fn main() -> () rl/init-window(screen-width, screen-height, "raylib [core] example - 2d camera") defer rl/close-window() player = rl/Rectangle{.x 400.0, .y 280.0, .width 40.0, .height 40.0} buildings = array(max-buildings, rl/Rectangle) 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 for i in range(max-buildings) let w = f32(rl/get-random-value(50, 200)) h = f32(rl/get-random-value(100, 800)) buildings[i] = rl/Rectangle{.x -6000.0 + f32(spacing), .y (f32(screen-height) - 130.0) - h, .width w, .height h} spacing += i32(w) 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.fln's ;; own comment on the struct says so. camera = rl/Camera2D{.offset rl/Vector2{.x f32(screen-width) / 2.0, .y f32(screen-height) / 2.0} .target rl/Vector2{.x player.x + 20.0, .y player.y + 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/is-key-down(:key-right) player.x += 2.0 elif rl/is-key-down(:key-left) player.x -= 2.0 ;; The camera follows the player's centre. camera.target = rl/Vector2{.x player.x + 20.0, .y player.y + 20.0} if rl/is-key-down(:key-a) camera.rotation -= 1.0 elif rl/is-key-down(:key-s) camera.rotation += 1.0 camera.rotation = clamp(camera.rotation, -40.0, 40.0) camera.zoom += rl/get-mouse-wheel-move() * 0.05 camera.zoom = clamp(camera.zoom, 0.1, 3.0) if rl/is-key-pressed(:key-r) camera.zoom = 1.0 camera.rotation = 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) for i in range(max-buildings) rl/draw-rectangle-rec(buildings[i], build-colors[i]) rl/draw-rectangle-rec(player, rl/red) rl/draw-line(i32(camera.target.x), screen-height * -10, i32(camera.target.x), screen-height * 10, rl/green) rl/draw-line(screen-width * -10, i32(camera.target.y), screen-width * 10, i32(camera.target.y), 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)