I like generative art. Especially ones that render real time. I built a kaleidoscope that runs entirely in a browser tab. It is called Refracted Descent, and you can open it right now: norikdavtian.github.io/kaleidoscope. No install, no account, no upload. Drop in one of your own photos — or hand it your camera — and the machine folds it into something you will not recognize, or maybe you do. 
This was combination of a few side generative art experiments I had done before that came together nicely thanks to WebGL, P5js, and Threejs.
The Kaleidoscope
David Brewster patented the kaleidoscope in 1817 and wrote the treatise on it two years later. Buried in that book is a fact that constrains everything: only four mirror arrangements tile the plane without gaps. The radial fan, the square, the equilateral triangle, and two other triangles. Every other angle you pick leaves a seam somewhere, and the eye finds it immediately.
So the studio offers exactly those, plus an orb: Radial, Triangle, Square, Orb. Radial then splits again into how the wedge is treated — mirror, pinwheel, or spiral. That is the whole vocabulary. Everything else — cell size, angle, drift — is a dial on top of a shape that was decided in 1819.
The orb, which is a twelve-year-old repo in disguise
Orb is the one mode that is not a mirror arrangement at all. It takes the colored field, treats it as an equirectangular panorama, and reads it from inside a sphere. Drag no longer twists the image — it looks around. You are standing in the middle of the kaleidoscope.
That idea is not new here. It is ThreeJS-360-Panorama, an experiment I pushed to GitHub in September 2014: wrap a panoramic photo onto a sphere, park the camera at the centre, let the mouse look around. Sphere geometry, texture, orbit controls, a whole Three.js scene graph.
Here the same effect is one fragment shader. No mesh, no scene, no library — the panorama projection is just a different way of choosing which source to read, so it costs a pass over pixels I was already coloring. Twelve years of the platform catching up, and the trick collapses from a repo into a function.
Two details it earned along the way. Yaw rides on the existing spin angle, so Spin and drag momentum pan the view for free rather than needing their own camera code. And when breathing is on, the field of view swells with the count — so the inhale reads as the orb expanding around you.
How does it texture, color, move, breath?
It is all simple light geometry, multiplied in a mirror. The background gets rendered then on the canvas based on the position and rotation of the source. The controls make it fun generating endless possibilities. 
Radial mode folds the plane down into a single wedge. The sampler only ever reads a sector of your source image — π / folds wide, anchored at the pan point. At the default of 10 folds, that is an 18° slice. Most of your picture is not merely distorted; it is never read at all. Which is why an upload tends to come out as gorgeous abstract texture rather than as its subject. The mirrors are doing their job. There is just no room in the wedge for a face.
The tessellated modes work differently. They sample a rectangular patch and mirror it across cell edges, which is what a real polycentral scope does — and it is why whole faces show up in the antique references. If you want your subject to survive:
| Symmetry | Triangle (or Square) |
| Zoom | 0.5 – 0.7 |
| Cell Size | 280 – 340 |
| Field Mix | 0 |
| Image Warp | 0 |
| Pan X / Y | centre on the subject |
One trap: Zoom runs the opposite way to the word. A higher value fits more of the image into each cell, so features come out smaller. Lower magnifies. I have not renamed it, because every time I try, the alternative is worse.
The performance problem, and the split that solved it
The first version solved the whole field on the CPU. At full resolution the field solve alone took roughly 1.8 seconds. Not 1.8 seconds to start — 1.8 seconds every time you touched a slider. Unusable for something whose entire point is that you turn a dial and watch it answer.
Moving to the GPU was obvious. The useful part was how I split it — not by stage, but by what actually changes:
- Field pass. Solves the whole field in a fragment shader and writes it to a texture: the palette phase packed across two bytes, plus the shade. This runs only when a parameter that shapes the field moves. ~1.3 ms.
- Color pass. Reads that texture and applies the palette ramp, seam glow and image blend. This is the only thing that has to keep up with the display.
Radial composites the buffer straight to the canvas. The tessellated modes reflect it across cell edges on a 2D canvas, working from a once-per-frame downscaled copy — so a hundred-plus cell blitz stay same-size copies rather than resamples. That one detail is worth more than it sounds.
Measured on an integrated GPU, 1024² field, 817×854 viewport:
| per frame | |
| Radial | 4.9 ms |
| Square | 4.2 ms |
| Triangle | 3.7 ms |
| Field re-solve (parameter change) | 1.3 ms |
From 1.8 s to 1.3 ms for the same work. The CPU path is still in there as a fallback and switches on automatically when WebGL is unavailable — slow, but never broken.
Breathing
Somewhere in the middle of building this, it stopped being a toy and became a place to sit.
Breathing mode runs the color ramp forward on the inhale and back on the exhale — rocking rather than scrolling, so it has somewhere to come to rest — and the whole field swells and settles with the count. Four patterns:
| Box | 4 in · 4 hold · 4 out · 4 hold |
| Calm | 4 in · 7 hold · 8 out |
| Deep | 6 in · 7 out, no holds |
| Longer | starts at 4·2·6·1 and stretches to 1.8× over ~5 minutes |
The guide is a shell of particles — directions laid out on a Fibonacci sphere, thinned by noise so the field breaks into filaments and voids rather than an even fog, with the far side dimmed so a flat scatter of points reads as a sphere. It sits in the artwork rather than on top of it.
The engineering note: brightness and radius are fixed in object space and computed once, since only the rotation changes per frame. The backing canvas is capped at 300 px and CSS scales it up — clearing and uploading that buffer dominates the cost, and it grows with the square of the side. A meditation aid that drops frames is not a meditation aid.
Disco, or: the easter egg gets a second life
There has been a hidden page on this site since October 2024. If you have ever found the unlabelled Sound link on /about or /manifesto, you have seen it: VFX No. 18 — a sphere of points laid out along a spiral, driven by the level coming off your microphone, with a camera that flies from far outside the sphere to somewhere inside it. Two sliders, no explanation. It sat there for two years doing nothing but existing.
Disco is that easter egg, wired into the scope. Sound now drives warp, color and motion by frequency band, and there are drive dials for how hard each one is pushed.
Porting it turned up the one thing I had gotten wrong the first time: take the raw microphone feed. The browser's default capture chain is tuned for voice calls — it hears sustained music and classifies it as background noise, then suppresses it. Which is precisely backwards when the music is the signal. Turning all of that off is the difference between a scope that twitches and one that dances.
The other thing the easter egg left behind is its sphere. That shell of points is the direct ancestor of the breathing guide above — same idea, reworked onto a Fibonacci distribution and thinned by noise. One abandoned experiment ended up supplying both the loudest mode in the app and the quietest one.
Sources: a plate, a photo, a clip, or your face
Nine painted plates ship with it. Past those, a source can be an image, a video file, or — the one worth trying — your camera, live.
A clip gets a trim and a mute, because you usually want four seconds of a thirty-second video and you rarely want to hear it. A live feed gets neither: as the code puts it, it has no duration to trim and nothing to hear. It just arrives, every frame, straight into the same path a still image takes — uploaded to the GPU and folded through the mirrors.
Point it at yourself in Triangle mode and you become the tessellation. It is the fastest way to understand what the thing actually does, because for once you control the source and can watch the fold respond.
Nothing leaves your machine
All of which would be an uncomfortable thing to offer if any of it travelled. It doesn't. Every pixel is processed client-side — the photo, the clip, and the camera feed alike. Live, never recorded. There is no upload endpoint in the standalone build for a frame to go to, and no request that carries one.
Which raises a question: how do you share a result? The answer is that every parameter that shapes the output is packed into the URL hash. The field is a pure function of those values and nothing else, so a link reopens exactly the same image, computed fresh on your machine. The panel also shows a short id — something like 0XOR9SV — for quoting a result without pasting a whole URL.
Built-in plates travel by id. A photo of your own does not travel with the link, and that is the trade I wanted.
Small decisions that mattered more than they should
- The panel opens down the left, not across the middle. The centre of the canvas is the thing being adjusted — a sheet parked over it hides the work.
- Every section has a lock, a dice and an auto. Lock means Randomize leaves it alone. Dice rolls only that section. Auto lets it drift on its own. Lock and Auto are mutually exclusive, because pinning something and letting it wander are the same request twice.
- Continuous sections ease; stepped sections cross-dissolve. Motion and Parameters glide toward a fresh target. Symmetry, Seed, Source and Color can only jump — so the frame is snapshotted before the switch and faded out over the new one. A hard cut still reads as a transition.
- The edge mask is always blurred. A hard cut reads as a crop; a soft one reads as the end of the mirrors.
- Palettes stay narrow on purpose. A kaleidoscope repeats a color dozens of times per frame, so a full-spectrum ramp reads as noise rather than as pattern.
- The welcome disc waits. There is no timer. It sits over the live, already-moving scope until you dismiss it.
Go break it
Press R until something surprises you, H for cinema, S to save a PNG. Then switch to Triangle and give it your camera, and watch yourself come apart.
The optics are Brewster's, from 1819. The tessellation geometry is adapted from kazuhikoarase/kaleidoscope. Leif Gehrmann's write-up is where I first understood why only four arrangements tile, and the palette format is Gregg Gunn's — a palette shown as a composition rather than a strip, which previews how the colors actually sit against each other.