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.
spirograph, the first port. Same maths as the original, a screen 1206x2622 instead of 800x450, and a loop it no longer owns.
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 SDL 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.
Three of the five ports, running on the phone. None of them needed a single new raylib binding.
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.
Most ports lose something to the smaller screen. multitouch is the one that
gains, and it is worth knowing the shape of that case because it is easy to port
the limitation along with the code.
The desktop original says plainly what it cannot do. GetTouchPosition returns
a Vector2 by value, the desktop binding set had no path for that, so it reads
point zero through the scalar GetTouchX and GetTouchY pair. Its own docstring
calls this the honest limit: the ids of every point visible, the coordinates of
only the first. Transcribing it faithfully would have reproduced a workaround for
a problem this project does not have.
So read the original's docstrings for what they concede, not only for what they describe.
The multi-finger path cannot be exercised from a REPL. tap! synthesises exactly
one point by construction, so every synthetic test passes on a code path that
has never seen two fingers. It took a person putting four on the glass.
That found a bug no amount of local testing would have. Colours were keyed off
the raylib touch id, (mod id 8) into an eight-entry palette. iOS derives those
ids from object pointers, so every one is 8-byte aligned. Two separate runs
reported:
809313472 809313920 809314368 809317952 stride 448
809133248 809134144 809136832 809137728 stride 896
163292352 163292800 163294592 after a relaunch
Every value divisible by 8, so all four fingers landed on slot 0 and drew in the same blue. Shifting the alignment away does not help, because the strides are themselves multiples of 8.
The third run is the useful one for deciding what to rely on. It sits in a different address range because the app had restarted, so neither the magnitude nor the spacing survives a relaunch. The alignment is the only invariant, and it is the one that broke things.
The instructive part is what happened next. The obvious fix is a better hash, and a tuned one scored 100% on synthetic strides, which turned out to measure the regularity of the test inputs rather than the quality of the hash. A proper murmur3 finalizer then scored a flat 41% at every stride, and that number is the answer: 8/8 x 7/8 x 6/8 x 5/8 is 41%, the chance four items land in four different buckets out of eight. The hash was already ideal and still collided most of the time, because with four fingers and eight colours collisions are the birthday problem, not a hashing defect.
Assigning the lowest unused slot is exact for up to eight simultaneous touches. No hash needed. When a measurement comes out at exactly the theoretical value, that usually means the approach is finished rather than that the tuning is.
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.




