A while back I rebuilt a Spirograph in the lab: two gear sizes, a pen, one closed curve no hand could draw. The kaleidoscope is the other toy in that pencil case, and it runs the opposite way. The Spirograph makes a single line look complicated. The kaleidoscope takes whatever’s in front of it and multiplies it into something composed.
Here, whatever’s in front of it is your pointer. Move across the demo and one stroke blooms into a symmetric flower, because the browser is stamping every flick of your hand into a ring of mirrors. Nothing you draw is designed. The symmetry does the designing, because symmetry is the thing your eye reads as on purpose.
Move your pointer across the canvas to paint
const SEG = 8; // radial wedges (N of them), each also mirrored
function frame(now) {
const t = (now - t0) / 1000;
ctx.fillStyle = 'rgba(8,7,12,0.045)'; // let the last frame fade, don't clear it
ctx.fillRect(0, 0, W, H);
ctx.globalCompositeOperation = 'lighter'; // overlapping strokes add up
ctx.strokeStyle = `hsl(${(t * 50) % 360} 92% 62%)`; // hue drifts with the clock
ctx.lineWidth = 4;
const { px, py, x, y } = pointer; // where it was, where it is
for (let s = 0; s < SEG; s++) {
for (const flip of [1, -1]) {
ctx.save();
ctx.translate(cx, cy);
ctx.rotate((s / SEG) * Math.PI * 2);
ctx.scale(1, flip); // flip: 1 keeps it, -1 mirrors
ctx.translate(-cx, -cy);
ctx.beginPath();
ctx.moveTo(px, py);
ctx.lineTo(x, y);
ctx.stroke();
ctx.restore();
}
}
requestAnimationFrame(frame);
}
That’s the whole illusion, and every part of it is one small idea stacked on the last: copy the stroke, mirror the copies, make them glow, let them fade. Take them one at a time.
One stroke, stamped around a ring
Between two animation frames your pointer travels a short distance, a single segment from where it was to where it is. Draw that segment once and it’s a scribble. Draw it N times instead, each copy rotated by an equal share of the full turn, and you get rotational symmetry: a pinwheel. Flip every copy as well and you get the mirror version too, which is what turns a pinwheel into a kaleidoscope.
one stroke → 16 copies
Hit Folded to drop back to a single copy and the demo paints your raw gesture, one lonely ribbon wandering the canvas. Fold it again and the same gesture lands in every wedge at once. The reflection is the cheap part: ctx.scale(1, -1) negates the y-axis, so everything you draw afterward comes out mirror-flipped with no second copy of the artwork to manage. You draw one line. The matrix does the reflecting.
Closing the circle
To fill the disk cleanly the copies have to meet. Rotate by a full turn split into N steps and mirror each step, and the seams line up on themselves: N rotations, N mirror lines, 2N copies in total. Each rotation is 360 / N degrees, and because every copy is also flipped, the smallest slice that repeats is half that, 180 / N. That’s dihedral symmetry, the same rule behind a snowflake or a rose window.
The spokes are the wedge boundaries and the little motif is stamped once per wedge, alternately rotated and mirrored, exactly what the pointer stroke does in motion. Nothing here knows what you’re going to draw, which is the useful part. Build the ring of mirrors once and you can pour any gesture through it.
const step = (Math.PI * 2) / SEG; // 45° when SEG is 8
// every other copy is mirrored, so the ring closes on 2·SEG wedges
Glow and decay are the whole mood
The symmetry gives you the shape. Two rendering choices give you the feeling, and without them the same math looks like a coloring book. The first is additive light: draw with globalCompositeOperation = 'lighter' and overlapping strokes add toward white instead of painting over each other, so every crossing blooms. The second is decay. Instead of clearing the canvas each frame, wash it with a nearly transparent fill, so old strokes fade a little every frame and a quick gesture leaves a comet trail behind it.
Turn both off and it’s flat, opaque line art that never moves on once it’s drawn. Turn on glow alone and the crossings brighten but the whole thing eventually clips to a white smear, because additive light with nothing to pull it back only ever climbs. I found that out the slow way, wondering why mine kept bleaching itself white. The decay is what balances it: it keeps clearing room so the brightness has somewhere to fall. Add a hue that drifts over time, hsl() walked by the clock, the same unit-circle timing behind any looping value, and a plain white scribble becomes a moving stained-glass window.
Why this isn’t CSS
I wanted a pure-CSS version, because no-JavaScript is usually the better answer on this site. You can fake a still one: clip-path cuts an element into a wedge and a flipped transform places the copies over a shared background. Where it falls apart is everything that makes this feel alive. There’s no gesture to mirror, no buffer of light building up frame over frame, no decay. A stylesheet composites boxes that already exist. It can’t keep re-drawing the last thing you did, sixteen ways, and let it fade. The moment the picture is made of your motion instead of your markup, you’re drawing, which means canvas.
So the honest version is canvas, and it stays small: one stamp loop, one translucent wash, a requestAnimationFrame gated to when the demo’s on screen and stilled for anyone who’s asked for reduced motion. Paint slowly for tight knots near the center, fast for long arcs that reach the rim. Swap the pointer for a touch, a scroll position, a microphone level, and the ring doesn’t care. It mirrors whatever you feed it.