jlt-commons/raylib-jlt has 125 examples. 48 of them need no input at all, and those port almost mechanically. This is what "almost" means, worked through with the five that are done.
Same maths as the original, a screen 1206x2622 instead of 800x450, and a loop it no longer owns. (No screenshot: every capture in this guide was raylib-ios's, the sibling build this project was ported from, not this one's, and they're dropped until there's a real Android capture to show.)
A raylib-jlt example owns its loop:
(defn -main [& _]
(rl/window! :width 800 :height 450 :title "spirograph")
(rl/set-target-fps 60)
(loop [frame 0 st (new-params)]
(when (rl/keep-running? deadline)
...
(rl/begin-drawing) ... (rl/end-drawing)
(recur (inc frame) st))))
(rl/close-window))and, in the ones that take input, reads the keyboard inline from inside its model:
(defn- step [s]
(let [dx (cond (rl/key-pressed? rl/KEY-LEFT) -1 ...)]
...))Neither survives on a phone. The host owns the loop here, and there is no keyboard. The six namespaces this project carries from the Android experiment came across byte-identical precisely because they were written the other way round, pure and touch-first, with input arriving as a data snapshot.
1. Become a reducer over frames. The scene contract in
poc.raylib.gallery is {:id :title :init :update :draw :dispose}, where
update takes state and an input snapshot and returns the next state. So the
body of the original's loop becomes advance, and -main disappears.
2. Derive geometry from the live screen. The originals draw at a fixed
800x450 with constants to match: a ring at radius 170 about (400, 225). A phone
is 1206x2622. Give the namespace a dimensions function taking the metrics, the
way poc.raylib.flappy-bird does, and scale everything off the smaller
dimension so a tall phone and a wide desktop both get something that fits.
3. Replace GetRandomValue with a seeded LCG. Not for purity as an
aesthetic, but because it makes the scene runnable and testable on a build host
with no raylib, no NDK and no device, and reproducible from a seed. The
constants are poc.raylib.flappy-bird's, so a seed means the same thing
everywhere:
(defn- next-random [seed]
(mod (+ (* 1103515245 (long seed)) 12345) 2147483648))4. Leave drawing to the host. The pure namespace computes; a
draw-scene! method in raylib.gallery draws. Colours come back as
[r g b a] and the host packs them, so no raylib type reaches the scene.
Almost nothing, which was the surprise. Across five ports:
| example | new bindings needed |
|---|---|
| spirograph | none |
| kaleidoscope | none |
| fireworks | none |
| boids | none |
| penrose | four: rlBegin, rlEnd, rlVertex2f, rlColor4ub |
| double-pendulum | none |
| fourier-epicycles | none |
DrawLine, DrawCircle, DrawText, DrawRectangle and MeasureText cover
most of the collection. Of eight further candidates surveyed, seven need
nothing new at all; only analog_clock does, wanting DrawLineEx, DrawRing
and a local-time call. Penrose needed rlgl immediate mode only because it fills
polygons and raylib's shapes API has no call for that.
Two of the five did not hold 60 fps on first run, and neither for the reason
anyone would guess. Read
performance-on-a-phone.md before tuning anything:
the short version is that an indexed loop over a vector beats every sequence
function, allocation costs more than the FFI call it decorates, and the fix is
usually in the drawing loop rather than the model.
Give anything that scales with frame cost a plain def rather than a literal,
so it can be tuned live over the nREPL: trail-length, max-points,
default-deflations, default-count all exist for that reason.
fourier_epicycles is the one that needed a real design decision rather than a
mechanical change. The original is landscape: the epicycle chain sits on the
left and the wave it traces scrolls rightward across the remaining width. A
phone is 1206 wide and 2622 tall, so there is no horizontal room for a
scrolling wave and a great deal of vertical.
So the chain hangs near the top and the wave scrolls DOWN. It is the same picture through ninety degrees, and the maths is untouched: what changed is which axis carries time, and therefore which coordinate of the pen the trace records. Worth expecting one of these per handful of ports.
Three edits, all in raylib.gallery:
(:require ... [raylib.scenes.spirograph :as spiro])
(def scenes [... (spiro/scene)])
{:id :generative :title "Generative" :scenes [:spirograph ...]}plus a draw-scene! method. Then a test namespace beside the others, since the
scene is pure and there is no excuse not to.