library / backgrounds / kaleidoscope
live - 60fps 1920 x 1080
kaleidoscope.html - self-contained
<!DOCTYPE html>
<html>
<head>
<title>Kaleidoscope</title>
<style>
    :root {
        --bg-color: #060010;
        --hue-1: #f0abfc;
        --hue-2: #22d3ee;
        --hue-3: #fbbf24;
    }
    * { margin: 0; padding: 0; }
    body { height: 100vh; background: var(--bg-color); overflow: hidden; }
    canvas { display: block; width: 100vw; height: 100vh; }
</style>
</head>
<body>
    <canvas id="c"></canvas>
<script>
    const cv = document.getElementById('c');
    const ctx = cv.getContext('2d');
    const off = document.createElement('canvas');
    const octx = off.getContext('2d');
    const css = getComputedStyle(document.documentElement);
    function hex(h) {
        h = (h || '').trim().replace('#', '');
        if (h.length === 3) h = h.split('').map(c => c + c).join('');
        const n = parseInt(h, 16); return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
    }
    const c1 = hex(css.getPropertyValue('--hue-1') || '#f0abfc');
    const c2 = hex(css.getPropertyValue('--hue-2') || '#22d3ee');
    const c3 = hex(css.getPropertyValue('--hue-3') || '#fbbf24');
    const SCALE = 4;
    const SEG = 8;                 // mirror segments
    let lw, lh, img, buf, cx, cy;
    function build() {
        lw = off.width = Math.ceil(innerWidth / SCALE);
        lh = off.height = Math.ceil(innerHeight / SCALE);
        cv.width = innerWidth; cv.height = innerHeight;
        img = octx.createImageData(lw, lh); buf = img.data;
        cx = lw / 2; cy = lh / 2;
        ctx.imageSmoothingEnabled = true;
    }
    addEventListener('resize', build);
    build();
    const seg = Math.PI * 2 / SEG;
    function step(t) {
        for (let y = 0; y < lh; y++) {
            for (let x = 0; x < lw; x++) {
                const dx = x - cx, dy = y - cy;
                const r = Math.sqrt(dx * dx + dy * dy);
                let a = Math.atan2(dy, dx) + t * 0.25;
                // fold angle into a mirrored wedge -> kaleidoscope symmetry
                a = a % seg; if (a < 0) a += seg;
                if (a > seg / 2) a = seg - a;
                // procedural mandala pattern in (a, r)
                const v1 = Math.sin(r * 0.09 - t * 2) ;
                const v2 = Math.sin(a * 9 + t * 1.5);
                const v3 = Math.sin(r * 0.04 + a * 6 - t);
                const m1 = (v1 * v2 * 0.5 + 0.5);
                const m2 = (v3 * 0.5 + 0.5);
                let r0 = c1[0] * m1 + c2[0] * (1 - m1);
                let g0 = c1[1] * m1 + c2[1] * (1 - m1);
                let b0 = c1[2] * m1 + c2[2] * (1 - m1);
                r0 = r0 * (1 - m2) + c3[0] * m2;
                g0 = g0 * (1 - m2) + c3[1] * m2;
                b0 = b0 * (1 - m2) + c3[2] * m2;
                const bright = 0.35 + 0.65 * Math.max(0, v1 * v2);
                const i = (y * lw + x) * 4;
                buf[i] = r0 * bright; buf[i + 1] = g0 * bright; buf[i + 2] = b0 * bright; buf[i + 3] = 255;
            }
        }
        octx.putImageData(img, 0, 0);
        ctx.drawImage(off, 0, 0, cv.width, cv.height);
    }
    if (matchMedia('(prefers-reduced-motion: reduce)').matches) {
        step(1.0);
    } else {
        let t = 0;
        (function loop() { t += 0.02; step(t); requestAnimationFrame(loop); })();
    }
</script>
</body>
</html>

About this animation

A symmetric mandala of mirrored light that endlessly rotates and morphs, radial petals and rings blooming from a bright core.

Source Code
Copied to clipboard