Origins
Where it started: old-school Psychodeli.
Psychodeli was on the desktop of millions in the early 90s via AfterDark: a spiral twisted on itself. That phrase is the engine — a spiral index, twisted by a periodic function, folded into color.
These are classic field signatures with the default wave function ($\sin$). Though enhanced from the original — smoother interpolation, HDR color, higher resolution — they illustrate the core engine: nodes radiating force fields, interference patterns where fields overlap, and a single periodic function turning distance into color.
In early 2025, the project to source the psychodeli DNA for a modern web instantiation began. That effort eventually delivered the current engine: WebGL 2.0, an HDR color pipeline, and a steady 60fps with a multitude of user control mechanisms, eqs. An early detour mixing CSS transforms and WebGL was eventually abandoned for pure GL.
My quip is it was "coding the vibe" not vibe coding. Whatever you call it, the throughput over a year has been staggering — and the output is miles from what I could have done solo.
§ 1 — The Force Field
Every pixel feels every node.
In 1993, the original C code calculated the distance from every pixel to every moving node, summing them up to determine that pixel's color. It did this on the CPU, one pixel at a time, for ~300,000 pixels. It was a gravitational force field — the same inverse-distance mathematics behind electrostatic potentials and Shepard's spatial interpolation.
Today, Psychodeli+ runs the same math on the GPU via WebGL — millions of pixels at 60fps. The pipeline has three stages: node space (N-body superposition), polar decomposition, and color formula.
Stage 1 — Node Space
Every pixel sums weighted force vectors from all nodes into a single combined vector. Think of 30 pulsing electric charges — every point in the field feels every charge simultaneously. The result is an N-body superposition:
$\vec{k}_i$ is node $i$'s position, $w_i$ its base weight, and $z_i$ is the Lorenz attractor's $z$-variable for that node — a continuously changing charge modulator (not a spatial coordinate). When $z_i$ flips sign, the node switches between attractor and repulsor. More on this in §2.
Stage 2 — Polar Decomposition
The combined force vector $\vec{F}$ is decomposed into polar coordinates — magnitude and direction:
The log compresses infinite dynamic range (forces near singularities blow up) into palette-sized values. This is the modern form — the original 1993 code used raw Euclidean distance from each pixel to a single node: $r = \sqrt{(x - x_n)^2 + (y - y_n)^2}$.
Stage 3 — Color Formula
Then $r$ and $\theta$ feed the color formula — unchanged since 1993:
$C$ is a color index (mod 256) mapped to a palette. Three additive terms: spiral pitch (doubled), an angular sine with amplitude and frequency knobs, and a radial sine shifted positive by +1. The formula builder below demonstrates this with a single node at center — $r$ is raw distance (no log needed without singularities).
The moving points are nodes — point singularities in the force field that reach into every pixel and pull. Influence is infinite (inverse distance law) but decays rapidly with distance. The visual pattern emerges from the interference of these competing forces.
Interactive formula builder — each step adds one term from the 1993 C code. Drag the pitch slider to feel how it tightens or loosens the spiral.
On top of this base layer optionally sits the kaleidoscope — a second shader pass that slices the rendered frame into symmetric wedges and mirrors them. We originally added it to hide seam artifacts at the edges of the cylindrical parameterization. It worked so well it became a first-class feature, with modes that respond to audio, breath at phrase boundaries, and shift segment count on the beat.
Two nodes. The force field flows between an attractor (positive charge) and a repulsor (negative charge).
Inverse Distance Weighting (Shepard's Method)
This is the formal version of the "gravity" metaphor. Newtonian gravity falls off as $1/r^2$. Psychodeli+'s force field is closer kin: inverse-distance weighting (Shepard's method), where influence falls as $1/r$. For every pixel $\vec{P}$, it calculates a Force Vector $\vec{F}_{total}$ — the weighted sum of vectors pointing to each node:
Where $\vec{K}_i$ is the position of node $i$, $w_i$ is its charge (positive = attractor, negative = repulsor), and $p$ is the metric power (usually 2 for the squared-distance denominator — giving inverse-distance falloff). The trick is that forces add as vectors. Between two identical attractors, their pulls cancel — creating a saddle point.
In practice, the denominator uses a soft-minimum $\sqrt{|\vec{d}|^4 + r_{min}^4}$ to prevent singularities — a smoothing radius that controls how "hot" the center of each node appears.
Deep dive: Color mapping & Lorenz weight modulation
The raw force vector is invisible. We convert it to color using polar coordinates. Field Magnitude ($m$) uses a log scale because forces near singularities approach infinity:
Field Angle ($\theta$) represents flow direction: $\theta = \operatorname{atan2}(F_y, F_x)$. The modern color index adds a second wave term in the radial dimension:
Each node's effective weight is modulated in real-time by the Lorenz Attractor's centered $z$-variable (range ≈ −25 to +25):
When $z_i$ is positive, the node attracts; when negative, it repels — forces flip polarity dynamically, turning the force landscape inside out. More on this in §2.
The missing step: Color Index → actual color. $C$ is just a number. To get RGB, the shader maps it to palette coordinate $u = \text{fract}(C / 256)$ and samples a 1D palette texture — a 256-color gradient built by interpolating through each theme's 8 anchor colors via Catmull-Rom splines in CIELAB space. Think of it as a color wheel: $C$ tells you where on the wheel to look. The palette wraps, so the visual texture is determined by how $C$ varies across the surface — smooth gradients where the field changes slowly, sharp bands where it changes fast.
§ 2 — The Lorenz Attractor
Chaos drives the motion.
The original Psychodeli moved its nodes with simple periodic functions — smooth, predictable, looping. The first thing we changed was the motion itself. Each node began to ride a Lorenz attractor — the system of differential equations that Edward Lorenz discovered in 1963 while modeling weather convection.
The Lorenz system is deterministic but sensitive to initial conditions — two trajectories starting 0.0001 apart diverge exponentially. Same inputs, same outputs, but you can't predict where it'll be in 30 seconds. The visual result: patterns that evolve continuously without ever settling into a visible loop.
Three nodes riding Lorenz attractors in slow motion. The "breathing" is the z-variable modulating each node's gravitational weight.
The equations
Three coupled differential equations, three variables — and the butterfly-shaped attractor they trace out:
Classic parameters: $\sigma = 10$, $\rho = 28$, $\beta = 8/3$. The trajectory orbits two lobes — the butterfly — switching between them aperiodically. $x$ and $y$ drive node position. $z$ modulates gravitational weight: how strongly each node pulls its neighbors.
The $z$-variable is centered around zero (range ≈ −25 to +25), so the effective weight swings from roughly $-2.5 w_i$ to $+2.5 w_i$. When $z_i$ goes negative, a node's force flips sign entirely — an attractor becomes a repulsor and vice versa. Since each node rides its own Lorenz trajectory with different initial conditions, their fields swell, shrink, and flip polarity out of phase — the force landscape is continuously turning itself inside out. Deterministic, bounded, never repeating.
Deep dive: Why chaos looks organic
Random noise looks like static — high-frequency, spatially uncorrelated. Chaotic systems have structure: smooth trajectories, bounded orbits, fractal cross-sections. The Lorenz attractor is bounded to a finite region of phase space but fills it densely.
Turbulent water, smoke curling off a candle, a flag snapping in gusty wind — all deterministic but never repeating. Lorenz orbits have that same visual quality: smooth enough to follow, unpredictable enough that you never catch the loop point.
§ 3 — The AI Collaboration
Who taught it to hear?
The original Psychodeli was written by Benjamin McCurtain — in the late 1990s. It shipped on millions of computers as part of Berkeley Systems' AfterDark screensaver suite. Then AfterDark withered, the hardware disappeared, and the code went extinct.
Twenty-five years later, we brought it back as an exploration of what AI could do with the core ideas.
The numbers
| Commits | 1,703 in 11 months — peak hours 6–8 AM & 5–7 PM, weekends dominate (Sunday 349, Saturday 283) |
| Step change | 768 commits since November 2025 with Gemini in AGY & Claude Code |
| JavaScript | 145,500 lines of core application code |
| Shaders | 8,600 lines of GLSL |
| Documentation | 159 technical documents |
| Audio analyzers | 20+ custom — none existed in the original |
| Input | MIDI (learn mode — wiggle a knob, it binds), gyroscope, three-zone touch gestures, tap corners, mouse wheel effects, 40+ keyboard shortcuts |
| Distribution | PWA, macOS desktop app (Electron, beta), deep-linkable — every parameter is URL-encodable |
| Palettes | 151 curated + HSV generation for custom. Display P3 wide gamut on capable hardware. |
Gotchas: Meta Quest's browser can't keep up with the per-pixel shader load. On Windows, check that your browser is actually using the discrete GPU (Settings → Display → Graphics) or you'll get 15fps for no reason.
The experimentation loop
88 commits (~5% of all commits) are pure refactoring — but they touched 142,000 lines (19% of total code churn), deleting twice as much as they added. That ratio held roughly constant across the whole year. Every month, roughly 1 in 6 commits was consolidating what the previous month's experiments had taught us.
Early on, the refactoring was partly driven by switching models — different AI collaborators leave different fingerprints in the code, and reconciling those styles is real work. Later, with Claude as primary collaborator, the refactoring shifted to architectural: breaking 6,000-line monoliths into composable modules, unifying parameter systems, killing dead code paths that accumulated from rapid feature iteration.
The git log tells the real story: 39 reverts, 58 abandoned features, 116 regression fixes. I would get excited about adding an understanding layer that could pick up nuance from say Sly Stone, and early on I'd abandon maybe a third of what the AI generated. But those abandoned branches weren't wasted — they were how the architecture found itself.
Reverts + abandoned features — % of commits by month
The rhythm was: push hard, let things get messy, then consolidate before pushing again. By September it was time to formalize: the AudioReactivityBus, a pub/sub layer that decoupled analyzers from visuals. Deferring that structure to the last responsible moment also benefitted from better coding models as 2025 progressed. My pattern of asking the model to "write a note to your future self so you don't mess up the next attempt so badly" has vanished with the 11/25 generation of models.
In my way of working, the generation is cognitively fast — you can try an idea without the overhead of researching and implementing it yourself. Knowing what to keep, and having the appetite to throw the rest away, is hard work.
The original code could draw spirals. It couldn't hear.
The entire audio system was built in collaboration with AI. Not generated by AI. Built with AI, the way you build with a colleague who happens to have read every paper on psychoacoustics.
It doesn't just hear volume.
Psychodeli+ runs 20+ custom analyzers including:
Harmonic tension — chord complexity, key detection via Krumhansl-Schmuckler profiles. Melodic contour — pitch velocity, phrase arcs, vibrato. Instrument classification — speech, brass, strings, percussion. (Honest caveat: the specific labels are still pretty unreliable. But it doesn't matter much — a false positive on "brass" vs. "strings" still tells the system something changed in the spectral character, which is the signal that actually drives visuals.)
Beat structure — BPM, syncopation, section boundaries (verse/chorus/drop). Phrase momentum — not just where beats are, but where the music is going.
Open-source starting points included meyda.js (audio feature extraction) and BeatDetektor (beat detection).
Perceptual scaling uses Weber-Fechner law so visual changes match how humans actually perceive loudness — logarithmically, not linearly.
What AI actually contributed
The design goal was to build something that hears music, not just audio. I'm a cognitive psychologist by training, so the target was always perceptual — not spectrum analysis but something closer to how a listener actually processes sound. Loudness follows Weber-Fechner. Key detection uses Krumhansl-Schmuckler profiles. Phrase momentum tracks where the music is going, not just where it is.
What AI contributed was the mathematical knowledge to wire that up — modular forms, non-Euclidean metrics, Lorenz attractors, perceptual scaling laws. Was that creative? I don't know. But the capability became a kind of art material — and the result is extinct code that grew leopard spots.
§ 4 — Transcendental Waves
We trawled Wikipedia for fancy math to throw at Claude.
The original Psychodeli used $\sin()$ — the simplest periodic function. At the cusp of '26, the latest generation of coding models had gotten good enough that we could try something reckless: browsing Wikipedia pages for exotic mathematics and asking Claude, what would this look like as a wave function?
The Riemann zeta function turned out to produce sharp crystalline edges — something about the way its partial sums spike at prime-related intervals. Möbius transformations fold space inside out. And the modular forms (Dedekind eta, Jacobi theta, Eisenstein series) create stained glass motifs from their lattice symmetries.
Combined modular form (mode 11) — Dedekind eta + Jacobi theta + Eisenstein series. Cathedral windows from number theory.
The progression
Every wave mode replaces $\text{wave}(\cdot)$ in the core color equation — $\text{wave}$ can be any of the transcendental modes (sin, zeta, tanh, Möbius, modular forms). The force field geometry stays the same — only the "lens" changes.
| Mode | Source | Visual character |
|---|---|---|
| sin | Trigonometry 101 | Smooth, predictable, the original |
| zeta | Riemann zeta function | Sharp edges, crystalline fractures |
| tanh | Hyperbolic tangent | Saturated plateaus, neon bands |
| Möbius | Complex analysis | Folding, twisting, inside-out |
| eta | Dedekind eta · number theory | Rhythmic cascades, partition patterns |
| theta | Jacobi theta · lattice theory | Frosted glass, crystalline grids |
| Eisenstein | Eisenstein series · modular forms | Starburst symmetry, snowflakes |
| modular | All three combined | Stained glass cathedral windows |
In the app, press q to cycle through wave modes.
sin()
The original. Smooth and predictable.
Riemann zeta
Crystalline edges from the most famous unsolved problem in math.
Möbius
Space folds inside out. Conformal maps from complex analysis.
Modular (combined)
Eta + theta + Eisenstein. Cathedral windows from number theory.
Deep dive: What are modular forms?
Modular forms are functions on the upper half of the complex plane that satisfy specific symmetry properties under the modular group — the set of all Möbius transformations with integer coefficients. They appear in:
Number theory — counting lattice points, proving Fermat's Last Theorem. String theory — partition functions of vibrating strings. Moonshine — the mysterious connection between the Monster group (the largest sporadic simple group, with ~8×10⁵³ elements) and modular forms.
The Dedekind eta function creates rhythmic cascades from its infinite product. The Jacobi theta function produces crystalline lattice grids from Gaussian-weighted sums. The Eisenstein series creates star-shaped symmetry from summing over all lattice points. Combined, they produce patterns that look like stained glass — because both stained glass and modular forms arise from the mathematics of symmetric tiling.
What the shader actually computes: A GPU can't evaluate infinite products at 60fps. The shader computes truncated approximations — 5 product terms for eta, 3 Gaussian sum terms for theta, a 5×5 lattice sum (24 terms) for Eisenstein. These are mathematically faithful truncations of the real functions, not hand-wavy analogs. Convergence is fast enough that the first few terms capture the visual signature; adding more wouldn't change what you see. The zeta mode sums 8 terms of the Dirichlet series ($\sum 1/n^s$ for $n=1$ to $8$). Being honest about truncation makes the story more interesting — these are the opening measures of each function, and they already produce the characteristic texture.
Wave Shapers
Reshape the wave, keep the function.
The 12 wave functions produce raw output — a value between −1 and 1. Wave shapers transform that output before it enters the color formula. They're non-linear post-processors, applied independently to the theta and radial components. A fold doubles the apparent frequency by mirroring negative values upward. A diamond sharpens peaks into points. XOR carves hard-edged alternating bands.
Because shapers are orthogonal to wave functions, any of the 12 functions can be paired with any of the 6 shapers on theta and radial independently: 12 × 6 × 6 = 432 unique wave behaviors from 24 building blocks.
Sin wave with shaper applied to both theta and radial channels. Toggle above to compare transforms.
| Shaper | Transform | Visual character |
|---|---|---|
| Bypass | s → s | Pass-through — the raw wave function |
| Fold | s → |s| | Absolute value — doubles apparent frequency, removes negative half |
| Inv Fold | s → −|s| | Inverted fold — valleys become the dominant feature |
| Diamond | s → max(s, 1−s) | Pointy peaks, angular ridgelines |
| Valley | s → min(s, 1−s) | Pointy troughs, sharp creases between smooth plateaus |
| XOR | s → max(1−s, 1+min(1−s, s)) | Sharp alternating bands, hard-edged digital texture |
In the app, press w to cycle through wave shapers.
XOR
Hard-edged alternating bands. The visual equivalent of a bitwise operation.
Fold
Absolute value doubles apparent frequency. Twice the bands, half the wavelength.
Diamond
Pointy peaks sharpened from smooth curves. Angular ridgelines emerge.
Deep dive: Why shapers multiply creative range
Without shapers, the color formula's periodic term is whatever the wave function outputs. Fold (absolute value) is the simplest non-trivial transform — it mirrors the negative half of any wave upward, visually doubling the frequency. Applied to sin, you get a rectified sine. Applied to zeta, you get a rectified zeta with twice as many crystalline edges per revolution.
Diamond and Valley are complementary: both compare the signal against its complement (1−s), but Diamond keeps the larger value (sharp peaks) while Valley keeps the smaller (sharp troughs). XOR combines both comparisons into a pattern that alternates between high and low bands with no smooth transition — the visual equivalent of a bitwise operation.
The key design choice: theta and radial shapers are independent. Setting theta to Fold and radial to Diamond produces a different texture than the reverse, because angular frequency doubling interacts differently with radial peak-sharpening than the other way around. This independence is what turns 6 shapers into 36 shaper combinations per wave function.
§ 5 — Metric Geometry
Change the ruler, change reality.
Standard physics uses Euclidean distance — $d = \sqrt{x^2 + y^2}$ — which produces perfectly circular field shapes. But distance is a choice, not a law. Swap the metric and the "gravity wells" around every node transform from circles into diamonds, squares, and concave stars.
The resulting tessellations share structural properties with biological cell patterns, fish scales, and crystal lattices.
Euclidean metric — standard circular force fields. Use the buttons to swap distance functions live.
| Mode | Formula | Shape | Visual character |
|---|---|---|---|
| Standard | $\sqrt{x^2+y^2}$ | Circles | Smooth, organic, classical physics |
| Manhattan | $|x|+|y|$ | Diamonds | Crystalline, tiled, Islamic geometry |
| Chebyshev | $\max(|x|,|y|)$ | Squares | Grid-like, architectural, pixel art |
| Astroid | $(|x|^{0.5}+|y|^{0.5})^2$ | Stars | Concave, biological, cell-like |
| Hyperbolic | $\ln\!\tfrac{1+r}{1-r}$, $\;0 \le r < 1$ | Poincaré disk | Void-like, infinite regression, Escher |
In the app, press i to cycle through distance metrics.
The astroid metric is particularly striking. Its concave star shapes create interference patterns that look like biological cell boundaries — the same geometry that emerges from surface tension minimization in soap films and cell membranes. The resemblance isn't coincidence; it's the same concave constraint.
§ 6 — Turing Patterns
How math grows leopard spots.
In 1952, Alan Turing published his only paper on biology: The Chemical Basis of Morphogenesis. He proved that two chemicals diffusing at different rates and reacting with each other could spontaneously self-organize into stable patterns — spots, stripes, and spirals.
Psychodeli+ doesn't use Turing's reaction-diffusion equations, but it produces visually convergent results through a different mechanism: competing frequencies of trigonometric interference.
Each button changes both the wave function and the frequency ratio — two knobs that jointly control which biological pattern emerges.
The Math of Morphogenesis
In the shader, the final color index is a sum of two competing periodic functions — one in the angular dimension ($\theta$), one in the radial dimension ($r$):
Two controls determine which biological pattern emerges. The frequency ratio sets the spatial competition: freq₁ controls angular bands ("petals"), freq₂ controls concentric rings — where rings cross petals, you get spots. The wave function shapes the interference character: sine gives clean geometric stripes, but zeta (Riemann zeta) and Chebyshev polynomials produce organic, irregular edges — the kind of imperfect symmetry-breaking you see in actual animal markings.
When both frequencies are high (e.g., 18 and 22), the angular and radial waves beat against each other and against the underlying force field. On a finite pixel grid, this beating is amplified by sampling aliasing, which makes macro-scale structures snap into view: Moiré patterns. Swap to zeta or Chebyshev and those clean Moiré lines roughen into something that looks genuinely biological.
Leopard Spots
Elliptic wave, dense frequencies (35:45) — discrete dots from high-frequency interference on a tight spiral.
Cell Membranes
Zeta wave, angular-dominant (30:10) — rounded irregular boundaries, like cells under a microscope.
Rosettes
Zeta wave + kaleidoscope (25:10) — mirrored symmetry creates rosette rings with dark boundaries.
Snakeskin
Zeta wave + HEAD_CHEESE (15:2) — rounded scales in concentric rows with dark outlines.
The mechanism here is fundamentally different from Turing's reaction-diffusion PDEs (which need short-range activation and long-range inhibition to break symmetry). But when local forces compete on different spatial scales — regardless of whether that competition comes from differential diffusion rates or from trigonometric interference — the same visual patterns show up. That's a universality claim, not a mathematical equivalence — similar visual output from completely different dynamics.
Playground
Explore...
Algo exploration will take you on a journey, but there are lots of ways to steer your own destiny.
- h / Space all controls
- f full screen
- x node motion juggle / tap upper-left corner
- Enter freeze (stare mode)
- t slooow it down
- 0–9 jukebox signatures
- [ ] signature nav / double-tap
- o toggle neon mode
In memoriam
Benjamin McCurtain
A spiral twisted on itself. Everything else grew from here.
With Eric Pitcher and Jefferson Provost
Web application (PWA)
Install to your home screen for a full-screen, app-like experience.
Apple TV
Native Metal app for Apple TV 4K. 4K HDR, OLED mode, Siri Remote.
Fire TV
Native app for Amazon Fire TV. D-pad controls, OLED mode, 60 color themes.