flan/examples/shapes-following-eyes.flan
Joseph Ferano d7ceec448e Ten more raylib examples, and the three small structs 3D needed
Three shapes, two text, three textures, one models and one core, picked for
binding surface rather than for how they look.

shapes-basic-shapes brings in six draw families nothing had called — the
circle and rectangle gradients, the triangles, all three poly draws — and is
the first call in the corpus to pass two Colors or three Vector2s at once.
shapes-collision-area is get-collision-rec, the only binding that takes two
Rectangles and answers a third, on a frame path. shapes-following-eyes is the
raymath gap measured rather than worked around: every line of it is vector
arithmetic written without a vector library, the way the C writes it.

text-input-box drains get-char-pressed's queue, which no example had read,
and needed a MouseCursor defenum for set-mouse-cursor. text-writing-anim
replaces TextSubtext — unbindable, it answers a pointer into a rotating
static buffer — with (string (slice b 0 n)), which is the same operation
without the shared state.

textures-image-generation runs nine Gen* calls and the
gen/upload/unload-image path, all procedural, no file on disk.
textures-fog-of-war needed a TextureFilter defenum: the smooth fog edge is
entirely :bilinear on a 25x15 render texture, and it is also the first
draw-texture-pro with a negative source height. textures-mouse-painting is
the same render texture used as a document rather than as scratch, plus the
round trip back off the GPU — load-image-from-texture, image-flip-vertical,
export-image — which nothing had run.

models-box-collisions is the counterexample to "a models example is a binding
exercise": nothing in it is a Model, and one BoundingBox defstruct un-refuses
four functions. core-3d-picking is the only caller anywhere for Ray and
RayCollision, and picking is the inverse of the get-world-to-screen the
corpus already had.

Added to vendor/raylib: defstructs BoundingBox, Ray and RayCollision;
defenums MouseCursor and TextureFilter with their mapping lines in bindings;
hand-written declare-c for SetMouseCursor, SetTextureFilter, DrawCubeV,
DrawSphere, DrawSphereWires, DrawRay and GetScreenToWorldRay, each excluded
from the generated half on the rule bindings already states. generated.flan
regenerated against raylib 5.5: 272 declarations, 117 refused, every
defstruct, hand-written declare-c and mapped constant agreeing with the
header.
2026-09-13 14:42:49 +07:00

104 lines
4.5 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. NEXT.md
;;;; names this as one of the three open gaps.
;;;;
;;;; 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.
;;;;
;;;; The raylib call it does exercise is 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")
(defconst screen-width 800)
(defconst screen-height 450)
(defconst sclera-radius f32 80.0)
(defconst iris-radius f32 24.0)
(defvar sclera-left rl/Vector2)
(defvar sclera-right rl/Vector2)
(defvar iris-left rl/Vector2)
(defvar 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.
(defn track [mouse rl/Vector2 centre rl/Vector2] rl/Vector2
(let [limit (- sclera-radius iris-radius)]
(if (rl/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.
(let [dx (- (.x mouse) (.x centre))
dy (- (.y mouse) (.y centre))
angle (atan2-f32 dy dx)]
(rl/Vector2 {.x (+ (.x centre) (* limit (cos-f32 angle)))
.y (+ (.y centre) (* limit (sin-f32 angle)))})))))
(defn 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.
(set sclera-left (rl/Vector2 {.x (- (/ (f32 (rl/get-screen-width)) 2.0) 100.0)
.y (/ (f32 (rl/get-screen-height)) 2.0)}))
(set sclera-right (rl/Vector2 {.x (+ (/ (f32 (rl/get-screen-width)) 2.0) 100.0)
.y (/ (f32 (rl/get-screen-height)) 2.0)}))
(set iris-left sclera-left)
(set iris-right sclera-right)
(rl/set-target-fps 60)
(until (rl/window-should-close?)
;; Update
(let [mouse (rl/get-mouse-position)]
(set iris-left (track mouse sclera-left))
(set iris-right (track mouse sclera-right)))
;; Draw
(rl/begin-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)
(rl/end-drawing)))