flan/examples/shapes-following-eyes.fln

102 lines
4.9 KiB
Plaintext

;;;; raylib [shapes] example - following eyes
;;;;
;;;; examples/shapes/shapes_following_eyes.c. Ported for the thing it needs
;;;; that raylib does not supply: the whole update is vector arithmetic —
;;;; subtract two points, take the angle, walk back out along it — and every
;;;; one of those steps is raymath. raymath is `static inline` in
;;;; raymath.h, so Vector2Subtract and Vector2Angle have no symbol in
;;;; libraylib at all and there is nothing for declare-c to bind. TODO.org,
;;;; "raymath is written in Flan, because static inline has no symbol".
;;;;
;;;; What that costs here is nothing, because the C does not use raymath
;;;; either — it writes the four subtractions out and calls atan2f, cosf and
;;;; sinf from libm, and Flan's prelude already declares all three as
;;;; atan2-f32, cos-f32 and sin-f32. So this file is the *measurement* of the
;;;; gap rather than a workaround for it: an example whose every line is
;;;; vector maths, written without a vector library, in the same shape the C
;;;; has. It reads fine. A Vector2Add would have saved two lines in the whole
;;;; file.
;;;;
;;;; That measurement is what decided the answer, and the answer landed:
;;;; vendor/raylib/vector.fln is raymath written in Flan, since a C shim
;;;; re-exporting the inlines would have bought identical arithmetic at the
;;;; price of a compilation unit and a second place raylib's semantics live.
;;;; The measurement is left standing rather than rewritten away — one
;;;; subtraction below is `rl/v2-sub` and the rest of the file is unchanged,
;;;; including the atan2/cos/sin path, which is deliberate: it needs no guard
;;;; for a zero-length vector where a normalise would. One line saved, in a
;;;; file that is nothing but vector maths, is the honest size of the gap.
;;;;
;;;; The raylib call it does exercise is check-collision-point-circle, which no
;;;; ported example had called, on a frame path, with the point coming
;;;; straight out of get-mouse-position — one struct out of raylib and back
;;;; into it untouched.
;;;;
;;;; The eye geometry, since the C hides it in a subtraction: the iris is
;;;; pinned inside a circle of (sclera - iris) radius about the sclera's
;;;; centre, so the iris disc is tangent to the sclera's edge at the extreme
;;;; rather than hanging over it.
import rl "vendor:raylib"
const screen-width = 800
const screen-height = 450
const sclera-radius: f32 = 80.0
const iris-radius: f32 = 24.0
once sclera-left: rl/Vector2
once sclera-right: rl/Vector2
once iris-left: rl/Vector2
once iris-right: rl/Vector2
;; One eye's pupil, given where the mouse is and where the eye is. Answers the
;; mouse position unchanged while it is inside the eye, and the point on the
;; limit circle in that direction when it is outside.
;;
;; The C repeats this block twice with different variables; it is one function
;; here because the two copies are identical and a difference between them
;; would be invisible.
fn track(mouse: rl/Vector2, centre: rl/Vector2) -> rl/Vector2
let limit = sclera-radius - iris-radius
if rl/check-collision-point-circle(mouse, centre, limit)
mouse
;; Outside: the direction from the eye to the mouse, and then `limit`
;; units back out along it. atan2 then cos/sin rather than a normalise,
;; because that is what the C does and because it needs no guard for a
;; zero-length vector — atan2(0,0) is 0 and the pupil sits at the right
;; of the eye, which is unreachable anyway since (0,0) is inside.
else
let d = rl/v2-sub(mouse, centre)
angle = atan2-f32(d.y, d.x)
rl/Vector2{.x centre.x + limit * cos-f32(angle), .y centre.y + limit * sin-f32(angle)}
fn main() -> ()
rl/init-window(screen-width, screen-height,
"raylib [shapes] example - following eyes")
defer rl/close-window()
;; The C reads these off get-screen-width/get-screen-height after the window
;; is open rather than off the constants, and so does this: on a high-DPI
;; display the two can differ.
sclera-left = rl/Vector2{.x f32(rl/get-screen-width()) / 2.0 - 100.0, .y f32(rl/get-screen-height()) / 2.0}
sclera-right = rl/Vector2{.x f32(rl/get-screen-width()) / 2.0 + 100.0, .y f32(rl/get-screen-height()) / 2.0}
iris-left = sclera-left
iris-right = sclera-right
rl/set-target-fps(60)
until rl/window-should-close()
;; Update
let mouse = rl/get-mouse-position()
iris-left = track(mouse, sclera-left)
iris-right = track(mouse, sclera-right)
;; Draw
rl/with-drawing:
rl/clear-background(rl/raywhite)
;; White of the eye, iris, pupil — in that order, each over the last.
rl/draw-circle-v(sclera-left, sclera-radius, rl/lightgray)
rl/draw-circle-v(iris-left, iris-radius, rl/brown)
rl/draw-circle-v(iris-left, 10.0, rl/black)
rl/draw-circle-v(sclera-right, sclera-radius, rl/lightgray)
rl/draw-circle-v(iris-right, iris-radius, rl/darkgreen)
rl/draw-circle-v(iris-right, 10.0, rl/black)
rl/draw-fps(10, 10)