The first thing you see on this site is a screen full of characters that won’t sit still. That’s the ASCII vortex — a canvas of roughly ten thousand glyphs that swirl, breathe, ripple when you click them, and bend around your cursor. It started because I wanted a homepage that felt like a machine instead of a brochure. It turned into a small rendering engine with patterns, modifiers, a sprite atlas, and opinions.
“Nothing on that screen is a video. Every character decides, sixty times a second, which of its neighbors to become.”
The demo is the homepage. Click the field. Move your mouse through it. Open the control panel and break things.
The Whole Engine Is One Trick
The vortex is a grid of characters, double-buffered. Every frame, every cell asks a single question: where should I look? A pattern function answers with a coordinate, the cell copies whatever character lives there, and the buffers swap.
for each cell (r, c): (r', c') = pattern(r, c, time) // "where should I look?" next[r][c] = grid[round(r')][round(c')] swap(grid, next)
That’s it. There is no velocity, no particles, no physics objects. Motion is an emergent property of sampling: if every cell samples from one cell to its left, the whole field flows right. If cells sample from a slightly rotated position, the field spins. The pattern isn’t animating anything — it’s a lens, and the content advects through it frame after frame.
A field that only copies itself eventually converges into mush, so 2% of cells per frame get re-rolled with a fresh random glyph. That tiny injection of noise keeps the whole system alive indefinitely — a little entropy budget so coherence never wins completely.
Patterns Are Just Pure Functions
Every pattern — mandalas, spirals, glyph rain, moiré interference, a strange attractor with two wandering lobes — is a pure function from (row, col, time) → (row’, col’). There are around thirty-five of them, grouped into families, and the engine auto-rotates through the pool with a short crossfade where both patterns run and their outputs blend.
Because patterns are pure math, writing a new one is a ten-minute exercise: pick a coordinate transform that looks interesting when iterated. The classic vortex is polar — compute each cell’s distance and angle from center, nudge the angle, convert back. A waterfall is Cartesian — sample from the row above. A kaleidoscope folds the angle into a wedge and mirrors it.
The subtle trap: because the map re-applies to its own output every frame, an angular offset isn’t a rotation speed — it’s a rotation per frame. Multiply that offset by elapsed time (which feels natural when you’re writing it) and the pattern accelerates forever. I shipped six patterns with exactly that bug and didn’t notice for months, because each one looked great for the first thirty seconds and I never sat still longer than that. Iterated systems compose; they don’t forgive.
Cursor Wake and Spacetime Ripples
After the pattern picks its sampling coordinate, a stack of modifiers gets a chance to bend it. The kaleidoscope modifier folds any pattern into radial symmetry. The gravity well drags the field toward your pointer. My favorite is the cursor wake: a gaussian bubble around the mouse where the pattern’s own displacement is suppressed and replaced with a gentle tangential swirl — a calm eye of the storm that follows you around, coherent motion punching through the ambient chaos.
Clicks launch ripples. Each click spawns an expanding ring, and a sine wavelet inside a gaussian envelope rides that ring, radially displacing where cells sample from — compressing space ahead of the wavefront and stretching it behind, the way a wave actually moves through a medium. Up to six ripples can overlap and interfere. The same pass brightens glyphs near the wavefront, so the wave is both brighter and geometrically distorted.
Making 10,000 fillText Calls Disappear
The naive version drew every cell with ctx.fillText() — ten thousand text draws per frame, each potentially changing the font size, each filled with a radial gradient. Profiling said the math I was proud of was a rounding error; 70%+ of every frame was text rasterization.
The fix is a glyph sprite atlas. Every (character, size) pair gets rasterized exactly once — in white, at device resolution — onto an offscreen atlas. Per frame, cells become drawImage blits. The glyphs land on an intermediate layer that gets tinted with a single source-in fill — which is mathematically identical to filling each glyph with the gradient directly, because tinting white pixels after the fact commutes with the alpha blend. Same pixels, fraction of the cost.
Two smaller wins: every cell’s distance and angle from center only change on resize, so those live in precomputed Float32Arrays instead of costing a sqrt and atan2 per cell per frame. And blank cells still flow through the simulation but skip their draw call entirely, because rendering an invisible space at 60fps is a strange hill to die on.
The Theater Around It
The engine is only half the point. Text fragments — I call them marginalia — materialize in the field every few seconds, scrambling from random glyphs into legible lines before dissolving back. The trick is inverted priority: for those cells, the utterance overrides the pattern entirely, which is why the words read clearly while everything around them churns.
Everything is theme-aware. The palette system feeds one accent color through CSS variables to the vortex, the HUD, and every page — and in Chromatic Drift mode that color slowly walks the hue wheel, so the whole site breathes. The vortex also hides things: a Konami code, achievement triggers, a pattern you can’t select from the menu. I’m not going to list them. Half of this site’s design philosophy is that undocumented surface area is a feature.
Development Notes
The deepest lesson was learning to think in iterated maps instead of animations. My instincts kept reaching for “move this thing over time,” and the system kept reminding me that there are no things — only a field and a lens. Every bug I shipped came from forgetting that the output of frame N is the input of frame N+1.
Verifying the renderer rewrite was its own adventure. Headless screenshots kept returning a black canvas — on the old code too, which is how I learned my screenshot tool was lying to me, not my renderer. The eventual answer was driving a real browser, reading pixels straight off the canvas, and counting how many were lit. When 16% of the screen glows, the vortex is alive.
Is a hand-rolled sprite-atlas text renderer overkill for a personal website? Completely. But the vortex is the first thing every visitor sees, it runs on whatever device they bring, and it’s the one part of the site that has to feel effortless. The things that look the most like play usually have the most engineering hiding under them.