Kaleidoscope Art: How to Create Mesmerizing Symmetrical Patterns With Code
A kaleidoscope takes a small fragment of the world and reflects it into infinite symmetry. Sir David Brewster invented it in 1816, patented it, and watched helplessly as manufacturers copied it across Europe within months. The device was so simple and so beautiful that it became one of the fastest-spreading inventions in history—an estimated 200,000 sold in London and Paris within three months.
The principle is pure geometry: place two or three mirrors at an angle, drop some colored fragments between them, and the reflections multiply into a symmetric rosette. The math behind it maps perfectly to code. A wedge-shaped slice of pixels, reflected and rotated N times around a center point, produces the same hypnotic patterns—except now you control every parameter, every color, every animation.
This guide builds eight kaleidoscope systems from scratch. Every example runs in a single HTML file, no libraries, no dependencies. Each one produces patterns you could stare at for hours.
1. Basic reflection kaleidoscope — the foundation
The simplest kaleidoscope divides a circle into N equal wedges, draws into one wedge, then reflects and rotates it to fill the circle. The key operation is: for each pixel, find which wedge it belongs to, then map it back to the source wedge (flipping for odd-numbered wedges).
const canvas = document.createElement('canvas');
canvas.width = 600; canvas.height = 600;
canvas.style.background = '#000';
document.body.appendChild(canvas);
document.body.style.margin = '0';
document.body.style.background = '#000';
document.body.style.display = 'flex';
document.body.style.justifyContent = 'center';
document.body.style.alignItems = 'center';
document.body.style.height = '100vh';
const ctx = canvas.getContext('2d');
const cx = 300, cy = 300, R = 280;
const N = 8; // number of reflections
const wedgeAngle = (2 * Math.PI) / N;
// Draw source pattern in first wedge
const src = document.createElement('canvas');
src.width = 600; src.height = 600;
const sctx = src.getContext('2d');
function drawSource(t) {
sctx.clearRect(0, 0, 600, 600);
sctx.save();
sctx.beginPath();
sctx.moveTo(cx, cy);
sctx.arc(cx, cy, R, 0, wedgeAngle);
sctx.closePath();
sctx.clip();
// Draw colorful circles
for (let i = 0; i < 12; i++) {
const a = wedgeAngle * (i / 12) * 0.8 + 0.1;
const r = 60 + 40 * Math.sin(t * 0.001 + i);
const x = cx + r * Math.cos(a);
const y = cy + r * Math.sin(a);
const radius = 15 + 10 * Math.sin(t * 0.002 + i * 0.7);
sctx.beginPath();
sctx.arc(x, y, radius, 0, Math.PI * 2);
sctx.fillStyle = 'hsl(' + ((i * 30 + t * 0.05) % 360) + ', 80%, 60%)';
sctx.fill();
}
// Curved lines
for (let i = 0; i < 5; i++) {
sctx.beginPath();
const r1 = 40 + i * 45;
const r2 = r1 + 30 * Math.sin(t * 0.0015 + i);
sctx.arc(cx, cy, r1, 0, wedgeAngle);
sctx.strokeStyle = 'hsla(' + ((i * 60 + t * 0.03) % 360) + ', 70%, 65%, 0.7)';
sctx.lineWidth = 2;
sctx.stroke();
}
sctx.restore();
}
function render(t) {
ctx.clearRect(0, 0, 600, 600);
drawSource(t);
for (let i = 0; i < N; i++) {
ctx.save();
ctx.translate(cx, cy);
ctx.rotate(i * wedgeAngle);
if (i % 2 === 1) {
ctx.scale(1, -1);
ctx.rotate(wedgeAngle);
}
ctx.translate(-cx, -cy);
ctx.drawImage(src, 0, 0);
ctx.restore();
}
// Fade edges with radial gradient
const grad = ctx.createRadialGradient(cx, cy, R * 0.85, cx, cy, R);
grad.addColorStop(0, 'rgba(0,0,0,0)');
grad.addColorStop(1, 'rgba(0,0,0,1)');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, 600, 600);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
The trick is ctx.scale(1, -1) for odd wedges—this mirrors the source across the x-axis, creating true kaleidoscopic reflection rather than simple rotation. With N=8, you get octagonal symmetry. N=6 gives hexagonal. N=12 gives the dense, intricate patterns you see in real kaleidoscopes.
2. Hexagonal kaleidoscope — honeycomb tiling
Real kaleidoscopes with three mirrors at 60° produce hexagonal patterns that tile the plane infinitely. This example creates a hex kaleidoscope that fills the entire canvas with repeating hexagonal cells, each one a reflected copy of the source pattern.
const canvas = document.createElement('canvas');
canvas.width = 700; canvas.height = 700;
document.body.appendChild(canvas);
document.body.style.margin = '0';
document.body.style.background = '#111';
document.body.style.display = 'flex';
document.body.style.justifyContent = 'center';
document.body.style.alignItems = 'center';
document.body.style.height = '100vh';
const ctx = canvas.getContext('2d');
const S = 80; // hex cell size
const h = S * Math.sqrt(3);
function drawTriangle(cx, cy, size, t, seed) {
const a1 = seed * 137.508;
for (let i = 0; i < 6; i++) {
const angle = (i / 6) * Math.PI / 3;
const r = size * 0.3 + size * 0.2 * Math.sin(t * 0.002 + seed + i);
const x = cx + r * Math.cos(angle + t * 0.001);
const y = cy + r * Math.sin(angle + t * 0.001);
const rad = size * 0.08 + size * 0.05 * Math.sin(t * 0.003 + i);
ctx.beginPath();
ctx.arc(x, y, rad, 0, Math.PI * 2);
ctx.fillStyle = 'hsl(' + ((seed * 40 + i * 60 + t * 0.02) % 360) + ', 75%, 55%)';
ctx.fill();
}
}
function drawHexKaleidoscope(cx, cy, size, t) {
ctx.save();
ctx.translate(cx, cy);
for (let i = 0; i < 6; i++) {
ctx.save();
ctx.rotate(i * Math.PI / 3);
// Draw two triangles per sector (one reflected)
ctx.save();
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(size, 0);
ctx.lineTo(size * 0.5, size * Math.sqrt(3) / 2);
ctx.closePath();
ctx.clip();
drawTriangle(size * 0.5, size * 0.3, size, t, i);
ctx.restore();
ctx.save();
ctx.scale(1, -1);
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(size, 0);
ctx.lineTo(size * 0.5, size * Math.sqrt(3) / 2);
ctx.closePath();
ctx.clip();
drawTriangle(size * 0.5, size * 0.3, size, t, i);
ctx.restore();
ctx.restore();
}
// Center circle
ctx.beginPath();
ctx.arc(0, 0, size * 0.12, 0, Math.PI * 2);
ctx.fillStyle = 'hsl(' + ((t * 0.05) % 360) + ', 60%, 70%)';
ctx.fill();
ctx.restore();
}
function render(t) {
ctx.fillStyle = '#111';
ctx.fillRect(0, 0, 700, 700);
for (let row = -2; row < 6; row++) {
for (let col = -2; col < 6; col++) {
const x = col * S * 1.5;
const y = row * h + (col % 2 ? h / 2 : 0);
drawHexKaleidoscope(x + 50, y + 50, S, t);
}
}
requestAnimationFrame(render);
}
requestAnimationFrame(render);
The hexagonal tiling uses the offset-row pattern: every other column shifts vertically by half a hex height. Each hex cell contains 6 sectors, each split into two reflected triangles—giving 12 symmetric copies of the source drawing. This is exactly how three-mirror kaleidoscopes work in the physical world.
3. Image-based kaleidoscope — photos to patterns
The most striking kaleidoscope patterns come from feeding in real images. This example samples a procedurally generated texture (since we cannot load external images in a standalone file) and reflects it through 8-fold symmetry. The result transforms organic noise into crystalline geometry.
const canvas = document.createElement('canvas');
const W = 600, H = 600;
canvas.width = W; canvas.height = H;
document.body.appendChild(canvas);
document.body.style.margin = '0';
document.body.style.background = '#000';
document.body.style.display = 'flex';
document.body.style.justifyContent = 'center';
document.body.style.alignItems = 'center';
document.body.style.height = '100vh';
const ctx = canvas.getContext('2d');
const cx = W / 2, cy = H / 2;
const N = 8;
const wedge = (2 * Math.PI) / N;
// Generate source texture
const tex = document.createElement('canvas');
tex.width = W; tex.height = H;
const tctx = tex.getContext('2d');
function noise(x, y) {
const n = Math.sin(x * 12.9898 + y * 78.233) * 43758.5453;
return n - Math.floor(n);
}
function generateTexture(t) {
const img = tctx.createImageData(W, H);
const d = img.data;
for (let y = 0; y < H; y++) {
for (let x = 0; x < W; x++) {
const i = (y * W + x) * 4;
const dx = x - cx, dy = y - cy;
const dist = Math.sqrt(dx * dx + dy * dy);
const angle = Math.atan2(dy, dx);
const n1 = noise(x * 0.02 + t * 0.0003, y * 0.02);
const n2 = noise(x * 0.05 + 100, y * 0.05 + t * 0.0002);
const v = Math.sin(dist * 0.03 - t * 0.002 + n1 * 6) * 0.5 + 0.5;
const h = (angle / Math.PI * 180 + 180 + t * 0.01 + n2 * 60) % 360;
const s = 0.7 + 0.3 * n1;
const l = 0.3 + 0.4 * v;
// HSL to RGB
const c = (1 - Math.abs(2 * l - 1)) * s;
const hp = h / 60;
const x2 = c * (1 - Math.abs(hp % 2 - 1));
let r = 0, g = 0, b = 0;
if (hp < 1) { r = c; g = x2; }
else if (hp < 2) { r = x2; g = c; }
else if (hp < 3) { g = c; b = x2; }
else if (hp < 4) { g = x2; b = c; }
else if (hp < 5) { r = x2; b = c; }
else { r = c; b = x2; }
const m = l - c / 2;
d[i] = (r + m) * 255;
d[i + 1] = (g + m) * 255;
d[i + 2] = (b + m) * 255;
d[i + 3] = 255;
}
}
tctx.putImageData(img, 0, 0);
}
function render(t) {
generateTexture(t);
ctx.fillStyle = '#000';
ctx.fillRect(0, 0, W, H);
for (let i = 0; i < N; i++) {
ctx.save();
ctx.translate(cx, cy);
ctx.rotate(i * wedge);
if (i % 2 === 1) { ctx.scale(1, -1); }
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.arc(0, 0, 290, 0, wedge);
ctx.closePath();
ctx.clip();
ctx.translate(-cx, -cy);
ctx.drawImage(tex, 0, 0);
ctx.restore();
}
const grad = ctx.createRadialGradient(cx, cy, 240, cx, cy, 300);
grad.addColorStop(0, 'rgba(0,0,0,0)');
grad.addColorStop(1, 'rgba(0,0,0,1)');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, W, H);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
The procedural texture uses layered sine waves with pseudo-noise for organic variation. The kaleidoscope reflection turns this turbulent noise into structured crystal-like patterns. The slow animation parameters (t * 0.0003) make the pattern evolve gradually—like turning a real kaleidoscope very slowly.
4. Animated rotating kaleidoscope — perpetual motion
A classic kaleidoscope effect: the source pattern rotates slowly while the reflections multiply the motion into a hypnotic dance. This version adds color shifting and pulsing shapes that breathe with the rotation.
const canvas = document.createElement('canvas');
canvas.width = 600; canvas.height = 600;
document.body.appendChild(canvas);
document.body.style.margin = '0';
document.body.style.background = '#000';
document.body.style.display = 'flex';
document.body.style.justifyContent = 'center';
document.body.style.alignItems = 'center';
document.body.style.height = '100vh';
const ctx = canvas.getContext('2d');
const cx = 300, cy = 300;
const N = 10;
const wedge = Math.PI * 2 / N;
function render(t) {
ctx.fillStyle = 'rgba(0,0,0,0.15)';
ctx.fillRect(0, 0, 600, 600);
const rot = t * 0.0003;
for (let i = 0; i < N; i++) {
ctx.save();
ctx.translate(cx, cy);
ctx.rotate(i * wedge + rot);
if (i % 2 === 1) { ctx.scale(1, -1); }
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.arc(0, 0, 280, 0, wedge);
ctx.closePath();
ctx.clip();
// Draw shapes in the wedge
for (let j = 0; j < 8; j++) {
const r = 40 + j * 32;
const a = Math.sin(t * 0.001 + j * 0.5) * wedge * 0.3 + wedge * 0.5;
const x = r * Math.cos(a);
const y = r * Math.sin(a);
const size = 8 + 6 * Math.sin(t * 0.002 + j);
const hue = (j * 45 + t * 0.03) % 360;
ctx.beginPath();
// Alternate between circles and diamonds
if (j % 2 === 0) {
ctx.arc(x, y, size, 0, Math.PI * 2);
} else {
ctx.save();
ctx.translate(x, y);
ctx.rotate(t * 0.002 + j);
ctx.moveTo(0, -size);
ctx.lineTo(size, 0);
ctx.lineTo(0, size);
ctx.lineTo(-size, 0);
ctx.closePath();
ctx.restore();
}
ctx.fillStyle = 'hsla(' + hue + ', 80%, 60%, 0.9)';
ctx.fill();
}
// Connecting arcs
ctx.strokeStyle = 'hsla(' + ((t * 0.05) % 360) + ', 60%, 70%, 0.4)';
ctx.lineWidth = 1.5;
for (let r = 60; r < 280; r += 50) {
ctx.beginPath();
ctx.arc(0, 0, r, 0, wedge);
ctx.stroke();
}
ctx.restore();
}
requestAnimationFrame(render);
}
requestAnimationFrame(render);
The trail effect (rgba(0,0,0,0.15) fill instead of full clear) gives the animation a ghostly persistence, like looking through a kaleidoscope in dim light. The 10-fold symmetry (N=10) is uncommon in physical kaleidoscopes (which typically use 3 or 6) but creates strikingly dense patterns in code.
5. Interactive drawing kaleidoscope — paint with symmetry
This turns the canvas into a live kaleidoscope drawing tool. Whatever you draw with the mouse in one wedge appears simultaneously in all reflected copies. It is the digital equivalent of painting between kaleidoscope mirrors.
const canvas = document.createElement('canvas');
canvas.width = 600; canvas.height = 600;
document.body.appendChild(canvas);
document.body.style.margin = '0';
document.body.style.background = '#000';
document.body.style.display = 'flex';
document.body.style.justifyContent = 'center';
document.body.style.alignItems = 'center';
document.body.style.height = '100vh';
document.body.style.cursor = 'crosshair';
const ctx = canvas.getContext('2d');
const cx = 300, cy = 300;
const N = 12;
const wedge = Math.PI * 2 / N;
let drawing = false;
let lastX = 0, lastY = 0;
let hue = 0;
ctx.fillStyle = '#000';
ctx.fillRect(0, 0, 600, 600);
ctx.font = '14px sans-serif';
ctx.fillStyle = '#666';
ctx.textAlign = 'center';
ctx.fillText('Draw anywhere to paint a kaleidoscope', 300, 300);
ctx.fillText('Click to clear', 300, 320);
canvas.addEventListener('mousedown', (e) => {
const r = canvas.getBoundingClientRect();
lastX = e.clientX - r.left;
lastY = e.clientY - r.top;
drawing = true;
});
canvas.addEventListener('mousemove', (e) => {
if (!drawing) return;
const r = canvas.getBoundingClientRect();
const x = e.clientX - r.left;
const y = e.clientY - r.top;
const dx = x - cx, dy = y - cy;
const ldx = lastX - cx, ldy = lastY - cy;
hue = (hue + 0.5) % 360;
for (let i = 0; i < N; i++) {
const angle = i * wedge;
const flip = i % 2 === 1 ? -1 : 1;
const cos = Math.cos(angle), sin = Math.sin(angle);
const x1 = cx + cos * ldx - sin * ldy * flip;
const y1 = cy + sin * ldx + cos * ldy * flip;
const x2 = cx + cos * dx - sin * dy * flip;
const y2 = cy + sin * dx + cos * dy * flip;
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.strokeStyle = 'hsl(' + hue + ', 80%, 60%)';
ctx.lineWidth = 3;
ctx.lineCap = 'round';
ctx.stroke();
}
lastX = x;
lastY = y;
});
canvas.addEventListener('mouseup', () => { drawing = false; });
canvas.addEventListener('mouseleave', () => { drawing = false; });
// Touch support
canvas.addEventListener('touchstart', (e) => {
e.preventDefault();
const r = canvas.getBoundingClientRect();
const touch = e.touches[0];
lastX = touch.clientX - r.left;
lastY = touch.clientY - r.top;
drawing = true;
});
canvas.addEventListener('touchmove', (e) => {
e.preventDefault();
if (!drawing) return;
const r = canvas.getBoundingClientRect();
const touch = e.touches[0];
const me = new MouseEvent('mousemove', {
clientX: touch.clientX,
clientY: touch.clientY
});
canvas.dispatchEvent(me);
});
canvas.addEventListener('touchend', () => { drawing = false; });
// Double-click to clear
canvas.addEventListener('dblclick', () => {
ctx.fillStyle = '#000';
ctx.fillRect(0, 0, 600, 600);
hue = 0;
});
The math for reflecting a point through N mirrors: convert to polar coordinates relative to center, rotate by the wedge angle, and flip the y-component for odd mirrors. The flip = i % 2 === 1 ? -1 : 1 handles reflection. The continuously incrementing hue creates rainbow trails that spiral out from the center.
6. Triangular kaleidoscope tiling — Wythoff construction
Kaleidoscopes are intimately connected to mathematical wallpaper groups. A triangle with angles that divide evenly into 180° (like 60-60-60, 90-60-30, or 90-45-45) tiles the plane through reflections alone. This example implements the *p6m* wallpaper group using equilateral triangles.
const canvas = document.createElement('canvas');
canvas.width = 700; canvas.height = 700;
document.body.appendChild(canvas);
document.body.style.margin = '0';
document.body.style.background = '#0a0a0a';
document.body.style.display = 'flex';
document.body.style.justifyContent = 'center';
document.body.style.alignItems = 'center';
document.body.style.height = '100vh';
const ctx = canvas.getContext('2d');
const S = 60; // triangle side length
const h = S * Math.sqrt(3) / 2;
function drawTriangleContent(ax, ay, bx, by, cx2, cy2, t, seed) {
ctx.save();
ctx.beginPath();
ctx.moveTo(ax, ay);
ctx.lineTo(bx, by);
ctx.lineTo(cx2, cy2);
ctx.closePath();
ctx.clip();
const mx = (ax + bx + cx2) / 3;
const my = (ay + by + cy2) / 3;
// Concentric shapes from centroid
for (let i = 0; i < 4; i++) {
const r = 5 + i * 8;
const phase = t * 0.001 + seed * 0.1 + i * 0.5;
const hue = (seed * 30 + i * 90 + t * 0.02) % 360;
ctx.beginPath();
ctx.arc(mx + Math.sin(phase) * 3, my + Math.cos(phase) * 3, r, 0, Math.PI * 2);
ctx.strokeStyle = 'hsla(' + hue + ', 75%, 55%, 0.8)';
ctx.lineWidth = 2;
ctx.stroke();
}
// Dot at centroid
ctx.beginPath();
ctx.arc(mx, my, 3, 0, Math.PI * 2);
ctx.fillStyle = 'hsl(' + ((seed * 60 + t * 0.03) % 360) + ', 80%, 70%)';
ctx.fill();
ctx.restore();
}
function render(t) {
ctx.fillStyle = '#0a0a0a';
ctx.fillRect(0, 0, 700, 700);
let seed = 0;
for (let row = -2; row < 14; row++) {
for (let col = -2; col < 14; col++) {
const x = col * S;
const y = row * h;
const offset = row % 2 ? S / 2 : 0;
// Upward triangle
const ax = x + offset, ay = y;
const bx = x + S + offset, by = y;
const cx2 = x + S / 2 + offset, cy2 = y + h;
drawTriangleContent(ax, ay, bx, by, cx2, cy2, t, seed++);
// Downward triangle (reflected)
const dx = x + S / 2 + offset, dy = y;
const ex = x + S + offset, ey = y + h;
const fx = x + offset, fy = y + h;
drawTriangleContent(dx, dy, ex, ey, fx, fy, t, seed++);
}
}
requestAnimationFrame(render);
}
requestAnimationFrame(render);
The equilateral triangle tiling alternates between upward and downward triangles. Each triangle gets its own seed for color variation, but the concentric-circle pattern inside each triangle creates visual continuity. The Wythoff construction is the mathematical framework behind this: every uniform tiling and every uniform polyhedron can be generated by reflecting a point inside a fundamental triangle.
7. Fractal kaleidoscope — recursive reflections
What happens when you put a kaleidoscope inside a kaleidoscope? Recursive subdivision. Each wedge contains a smaller kaleidoscope, which contains a smaller one, and so on. The result is a fractal rosette with self-similar structure at every scale.
const canvas = document.createElement('canvas');
canvas.width = 650; canvas.height = 650;
document.body.appendChild(canvas);
document.body.style.margin = '0';
document.body.style.background = '#000';
document.body.style.display = 'flex';
document.body.style.justifyContent = 'center';
document.body.style.alignItems = 'center';
document.body.style.height = '100vh';
const ctx = canvas.getContext('2d');
const cx = 325, cy = 325;
function drawKaleidoscope(x, y, radius, depth, t, maxDepth) {
if (depth > maxDepth || radius < 4) return;
const N = depth === 0 ? 8 : 6;
const wedge = Math.PI * 2 / N;
for (let i = 0; i < N; i++) {
ctx.save();
ctx.translate(x, y);
ctx.rotate(i * wedge + t * 0.0005 * (depth + 1));
if (i % 2 === 1) ctx.scale(1, -1);
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.arc(0, 0, radius, 0, wedge);
ctx.closePath();
ctx.clip();
// Draw content at this level
const hue = (depth * 60 + i * 30 + t * 0.02) % 360;
const r = radius * 0.5;
const a = wedge * 0.5;
const px = r * Math.cos(a);
const py = r * Math.sin(a);
ctx.beginPath();
ctx.arc(px, py, radius * 0.15, 0, Math.PI * 2);
ctx.fillStyle = 'hsla(' + hue + ', 75%, 55%, ' + (0.6 - depth * 0.1) + ')';
ctx.fill();
// Connecting line
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(px, py);
ctx.strokeStyle = 'hsla(' + hue + ', 60%, 65%, 0.3)';
ctx.lineWidth = 1;
ctx.stroke();
// Recurse: smaller kaleidoscope at the wedge midpoint
const childR = radius * 0.38;
const childDist = radius * 0.55;
drawKaleidoscope(
childDist * Math.cos(wedge * 0.5),
childDist * Math.sin(wedge * 0.5),
childR, depth + 1, t, maxDepth
);
ctx.restore();
}
}
function render(t) {
ctx.fillStyle = '#000';
ctx.fillRect(0, 0, 650, 650);
drawKaleidoscope(cx, cy, 300, 0, t, 3);
// Central glow
const grad = ctx.createRadialGradient(cx, cy, 0, cx, cy, 20);
grad.addColorStop(0, 'hsla(' + ((t * 0.04) % 360) + ', 80%, 80%, 0.8)');
grad.addColorStop(1, 'transparent');
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(cx, cy, 20, 0, Math.PI * 2);
ctx.fill();
requestAnimationFrame(render);
}
requestAnimationFrame(render);
Each recursion level rotates at a slightly different speed (t * 0.0005 * (depth + 1)), creating a gear-like effect where inner kaleidoscopes turn faster than outer ones. The depth limit of 3 produces 8 × 6 × 6 × 6 = 1,728 leaf elements—enough for visual complexity without killing frame rate.
8. Generative kaleidoscope art — full composition
This final example combines everything: layered kaleidoscope reflections with varying symmetry orders, additive blending, color harmonies, and slow morphing animation. The result is a piece of generative art that never repeats.
const canvas = document.createElement('canvas');
canvas.width = 700; canvas.height = 700;
document.body.appendChild(canvas);
document.body.style.margin = '0';
document.body.style.background = '#000';
document.body.style.display = 'flex';
document.body.style.justifyContent = 'center';
document.body.style.alignItems = 'center';
document.body.style.height = '100vh';
const ctx = canvas.getContext('2d');
const cx = 350, cy = 350;
const layers = [
{ N: 12, radius: 320, speed: 0.0002, shapes: 'circles', hueBase: 0 },
{ N: 8, radius: 240, speed: -0.0003, shapes: 'lines', hueBase: 120 },
{ N: 6, radius: 180, speed: 0.0005, shapes: 'arcs', hueBase: 240 },
{ N: 16, radius: 140, speed: -0.0004, shapes: 'dots', hueBase: 60 },
];
function drawLayer(layer, t) {
const { N, radius, speed, shapes, hueBase } = layer;
const wedge = Math.PI * 2 / N;
const rot = t * speed;
ctx.globalCompositeOperation = 'screen';
for (let i = 0; i < N; i++) {
ctx.save();
ctx.translate(cx, cy);
ctx.rotate(i * wedge + rot);
if (i % 2 === 1) ctx.scale(1, -1);
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.arc(0, 0, radius, 0, wedge);
ctx.closePath();
ctx.clip();
const hue = (hueBase + t * 0.01) % 360;
if (shapes === 'circles') {
for (let j = 0; j < 5; j++) {
const r = 30 + j * (radius / 6);
const a = wedge * (0.2 + 0.6 * Math.sin(t * 0.001 + j));
const x = r * Math.cos(a);
const y = r * Math.sin(a);
const sz = 8 + 12 * Math.sin(t * 0.0015 + j * 1.2);
ctx.beginPath();
ctx.arc(x, y, sz, 0, Math.PI * 2);
ctx.fillStyle = 'hsla(' + ((hue + j * 30) % 360) + ', 70%, 50%, 0.6)';
ctx.fill();
}
} else if (shapes === 'lines') {
for (let j = 0; j < 6; j++) {
const r1 = j * (radius / 7);
const r2 = r1 + radius / 7;
const a1 = wedge * 0.5 + Math.sin(t * 0.001 + j) * 0.3;
ctx.beginPath();
ctx.moveTo(r1 * Math.cos(a1 - 0.1), r1 * Math.sin(a1 - 0.1));
ctx.lineTo(r2 * Math.cos(a1 + 0.1), r2 * Math.sin(a1 + 0.1));
ctx.strokeStyle = 'hsla(' + ((hue + j * 25) % 360) + ', 65%, 55%, 0.5)';
ctx.lineWidth = 2;
ctx.stroke();
}
} else if (shapes === 'arcs') {
for (let j = 1; j < 5; j++) {
const r = j * (radius / 5);
const startAngle = Math.sin(t * 0.0008 + j) * 0.3;
ctx.beginPath();
ctx.arc(0, 0, r, startAngle, startAngle + wedge * 0.8);
ctx.strokeStyle = 'hsla(' + ((hue + j * 40) % 360) + ', 75%, 60%, 0.5)';
ctx.lineWidth = 3;
ctx.stroke();
}
} else if (shapes === 'dots') {
for (let j = 0; j < 20; j++) {
const r = 10 + Math.random() * (radius - 20);
const a = Math.random() * wedge;
// Use seeded position based on j and t
const seed = Math.sin(j * 127.1 + Math.floor(t * 0.0001) * 311.7) * 43758.5453;
const sr = 15 + (seed - Math.floor(seed)) * (radius - 30);
const sa = (seed * 7.3 - Math.floor(seed * 7.3)) * wedge;
const x = sr * Math.cos(sa);
const y = sr * Math.sin(sa);
ctx.beginPath();
ctx.arc(x, y, 2, 0, Math.PI * 2);
ctx.fillStyle = 'hsla(' + ((hue + j * 18) % 360) + ', 60%, 65%, 0.7)';
ctx.fill();
}
}
ctx.restore();
}
}
function render(t) {
ctx.globalCompositeOperation = 'source-over';
ctx.fillStyle = 'rgba(0,0,0,0.08)';
ctx.fillRect(0, 0, 700, 700);
layers.forEach(layer => drawLayer(layer, t));
// Vignette
ctx.globalCompositeOperation = 'source-over';
const grad = ctx.createRadialGradient(cx, cy, 250, cx, cy, 360);
grad.addColorStop(0, 'rgba(0,0,0,0)');
grad.addColorStop(1, 'rgba(0,0,0,1)');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, 700, 700);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
Four kaleidoscope layers rotate at different speeds and directions, each with different symmetry orders (12, 8, 6, 16). The screen blend mode makes overlapping colors additive—where blue and red overlap, you get magenta. The trail effect (rgba(0,0,0,0.08)) creates luminous persistence. The dot layer uses seeded randomness (updated every ~10 seconds via Math.floor(t * 0.0001)) for controlled sparkle.
The mathematics of kaleidoscopic symmetry
Every kaleidoscope pattern belongs to a wallpaper group—one of exactly 17 distinct symmetry types that tile the plane. A standard three-mirror kaleidoscope with 60° angles produces the p6m group, the most symmetric of all 17. Two mirrors at 90° produce p4m. The classification was proven complete in 1891 by Evgraf Fedorov.
The reflection count determines the dihedral group Dn. A kaleidoscope with N-fold symmetry has 2N symmetry operations: N rotations and N reflections. D6 (hexagonal) appears in snowflakes, honeycombs, and Islamic geometric art. D5 (pentagonal) appears in flowers and starfish but cannot tile the plane—which is why five-fold kaleidoscopes produce rosettes rather than wallpaper patterns.
The connection to group theory runs deep. Felix Klein’s Erlangen programme (1872) redefined geometry itself as the study of properties invariant under a group of transformations. A kaleidoscope is, quite literally, a group theory machine—each mirror is a generator, and the pattern is the orbit of a fundamental domain under the group action.
Kaleidoscopes in art and culture
Brewster’s original 1816 kaleidoscope was not a toy. He designed it as a scientific instrument for studying optical symmetry and as a tool for artists to generate textile and wallpaper designs. The V&A Museum in London holds kaleidoscope-influenced fabric samples from the 1820s that look remarkably like computer-generated art.
Contemporary kaleidoscope artists push the medium further. Judith Paul creates stained-glass kaleidoscopes with hand-selected dichroic glass. Charles Karadimos builds “wheels” with rotating oil-filled cells. In digital art, the kaleidoscope transform is a staple of VJ culture—software like Resolume and TouchDesigner use real-time kaleidoscopic effects for live concert visuals.
The principle extends beyond 2D. A teleidoscope replaces the object cell with a lens, turning the real world into kaleidoscopic patterns. In 3D, mirrored rooms (like Yayoi Kusama’s Infinity Mirror Rooms) create kaleidoscopic spaces where viewers become part of the pattern.
Performance tips
- Clip before draw. The
ctx.clip()call is expensive, but it is far cheaper than checking each pixel manually. Clip to the wedge, draw freely, restore. The GPU handles the masking. - Draw once, copy N times. Render your source pattern to an offscreen canvas once, then use
drawImagewith transforms to stamp it N times. This is 10x faster than recalculating shapes for each wedge. - Lower symmetry = higher FPS. N=6 is 2x faster than N=12. For mobile, stick to N=6 or N=8.
- Use requestAnimationFrame timestamps. Never
setInterval. Thetparameter gives milliseconds since page load—use it for all animation math to keep motion smooth regardless of frame rate. - WebGL for image-based kaleidoscopes. If you need to kaleidoscope-transform every pixel of a texture, a fragment shader that computes the wedge mapping per-pixel runs 100x faster than Canvas 2D’s clip-and-draw approach.
Explore more generative art on Lumitree, where every branch is a unique micro-world built from code. For related symmetry techniques, see the mandala art guide, the geometric art tutorial, or the sacred geometry deep-dive.