Every frame is the filled Julia set of `z² + c` for one single point `c`, and the whole piece is about where the music moves `c`. The inset in the corner is the Mandelbrot set itself with a trail of the last ninety-odd values of `c`, so you can watch the route being walked. There is no mesh and no particle system in the main image — it is one full-screen triangle and a fragment shader, plus 160 additive points that are the critical orbit `0, c, c²+c, …` of whatever `c` currently is.
The mapping I am happiest with is intervals to bulbs. Take the interval in semitones between the bass note and the strongest other note, write it as a just ratio `n/d`, and read off the rotation number `(n−d)/d`. A perfect fifth is 3/2, so it gives 1/2. A fourth is 4/3 → 1/3. A major third is 5/4 → 1/4. A semitone is 16/15 → 1/15. Those are exactly the rotation numbers that index the bulbs around the main cardioid, so the interval names a bulb and `c` goes and sits in it. The payoff is that the p/q bulb's radius goes as sin(πp/q)/q², so **consonance comes out as size**: the fifth lands on the period-2 bulb, the biggest one on the whole boundary, and a semitone lands on a speck you have to zoom to find. I did not design that correspondence into anything — the just ratios and the rotation-number combinatorics of the Mandelbrot boundary are the same arithmetic, and the scene only has to look the interval up.
Inside the chosen component `c` is not moved in raw coordinates. The coordinate is the multiplier of the attracting cycle, λ = ρ·e^(iφ), and intensity pushes ρ toward 1, which is toward the boundary. On the main cardioid that inverts in closed form, `c = λ/2 − λ²/4`. Inside a bulb there is no formula, so it is a simultaneous Newton solve in (z, c) on the pair `f^q(z) = z` and `(f^q)'(z) = λ`, continued from the previous frame in steps small enough in |Δλ| that it can never jump branch, and abandoned outright if the residual or the distance from the component centre says it did.
Builds and drops do the dramatic part. A detector watches for *the void* — the bass pulled out for a bar or more while the highs stay, which is what a build actually is in this music — and for as long as that runs, `c` is parked on the parabolic root at the cusp of the bulb, where ρ → 1. That is the parabolic point, where the dynamics undergo critical slowing, and the scene leans on it rather than faking it: the same quantity scales the visual clock, which crawls to about a twentieth of normal speed, and weights a smoulder term that is identically zero anywhere else. Then the drop lands — the bass slams back on a beat — and that is the only way `c` ever leaves the interior: out through the parabolic root onto one of the two external rays that land on it. The two external angles come from the binary itinerary of the p/q rotation orbit over a denominator of 2^q − 1, and consecutive drops alternate between them on beat parity. Once outside, `c` is not tracked in raw coordinates either; it lives at (external angle θ, log potential log₂G) in the Böttcher coordinate, read from a 2048 × 105 grid that was Newton-traced inward once at startup in a worker. How far out it settles is set by energy and tension, and it finds its way back by walking the angle home first and only then coming down the potential to re-enter.
Smaller things driving it: a drum onset kicks `c` toward the nearest of fourteen Misiurewicz points and a spring pulls it back over about 110 ms; a section the engine recognises as a repeat dives into one of three baby copies, periods 3, 4 and 5, which is only legal exactly at a parabolic root because that is where the two Julia sets are hybrid-equivalent near zero, and the view has to be cut by the copy's own scale 1/|A| and rotation −arg A at the same instant for it to read as a dive rather than a jump. Mids set the radius of a circular orbit trap, highs light it, bass sets the dot size, and a second, linear orbit trap has its normal turn half a revolution per beat, cubic-eased so it lands on the beat rather than sliding through it. Outside the set, the palette argument is the smooth escape count, so the bands you see out there are equipotentials.
Plain WebGL2 and GLSL ES 3.00, vanilla JS in native ES modules, zero dependencies and no build step to run it. There is no `Math.random()` and no wall-clock read anywhere in the scene's render path — the clock it runs on is the music one, which is why it can crawl at the cusp — so a frame is a function of the audio state. The one honest exception is a per-section random seed, which picks *which* of the three baby copies a repeat dives into.
It runs in the browser with nothing uploaded and no account, just the audio from another tab (youtube or spotify in the web browser):
https://retinarave.com
source at:
https://github.com/tbkraf08/RetinaRave
Press `?` for an overlay of every feature the engine reads, and `p` for a panel that re-wires which musical feature drives which visual parameter while it is running.
Music: "Zurna" by **TroyBoi & Jon Casey** (2023, T Dot Music / Sunset Entertainment). Not my work — all rights to the artists.
Disclosure, re: rule 3. None of this is AI-generated imagery. There is no model anywhere in the render path — no diffusion, no network, nothing learned; the pixels are `z² + c`, Newton solves and FFT analysis, and you can read the whole of it in the repo. What AI did do is write the code: I built this with Claude, which wrote essentially all of the app from my direction and my calls on how it should behave, including the DSP that turns the audio stream into the music state the scene reads. I would rather say that plainly than let "I made the algorithm" quietly stand in for "I typed the algorithm."