← Mike Higgins Home
Custom Code Experiment •

Voronoi 5

Source Code View HTML / CSS / JavaScript
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Hexagonal Gradient Flow Fields</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            background-color: #000;
            overflow: hidden;
        }

        canvas {
            display: block;
            width: 100vw;
            height: 100vh;
        }
    </style>
</head>
<body>
    <canvas id="can"></canvas>

    <script>
        const can = document.getElementById('can');
        const c = can.getContext('2d');

        const CONFIG = {
            speed: 22,
            density: 40,
            hexSize: 45,
            hexSpacing: 1.5,
            colorTransitionDuration: 4000,
            backgroundWaveSpeed: 0.00015
        };

        let isAnimating = true;
        let animationId = null;
        let resolutionScale = 0.2;
        let time = 0;

        let points = [];

        let colorSets = [];
        let currentColorSet = 0;
        let targetColorSet = 1;
        let colorTransitionStartTime = 0;
        let isTransitioning = false;
        let lastTimestamp = 0;

        function initColorSets() {
            colorSets = [];
            const schemes = [
                { h1: 180, s1: 100, l1: 55, h2: 320, s2: 95, l2: 60, h3: 60, s3: 100, l3: 65 },
                { h1: 20, s1: 95, l1: 60, h2: 350, s2: 90, l2: 55, h3: 280, s3: 85, l3: 50 },
                { h1: 200, s1: 90, l1: 50, h2: 170, s2: 85, l2: 55, h3: 240, s3: 80, l3: 45 },
                { h1: 90, s1: 100, l1: 60, h2: 330, s2: 100, l2: 65, h3: 190, s3: 95, l3: 58 },
                { h1: 270, s1: 85, l1: 55, h2: 45, s2: 90, l2: 60, h3: 340, s3: 85, l3: 50 },
                { h1: 310, s1: 100, l1: 60, h2: 200, s2: 100, l2: 58, h3: 280, s3: 95, l3: 62 },
                { h1: 15, s1: 95, l1: 65, h2: 175, s2: 90, l2: 60, h3: 100, s3: 95, l3: 62 },
                { h1: 140, s1: 90, l1: 55, h2: 260, s2: 85, l2: 58, h3: 210, s3: 88, l3: 52 },
                { h1: 285, s1: 90, l1: 50, h2: 30, s2: 95, l2: 62, h3: 340, s3: 90, l3: 60 },
                { h1: 320, s1: 100, l1: 62, h2: 180, s2: 95, l2: 58, h3: 270, s3: 90, l3: 55 },
                { h1: 340, s1: 100, l1: 65, h2: 55, s2: 100, l2: 68, h3: 200, s3: 95, l3: 60 },
                { h1: 230, s1: 85, l1: 48, h2: 315, s2: 100, l2: 60, h3: 120, s3: 95, l3: 58 }
            ];

            for (let i = 0; i < schemes.length; i++) {
                const scheme = schemes[i];
                colorSets.push([
                    hslToRgb(scheme.h1, scheme.s1, scheme.l1),
                    hslToRgb(scheme.h2, scheme.s2, scheme.l2),
                    hslToRgb(scheme.h3, scheme.s3, scheme.l3)
                ]);
            }
        }

        function startColorTransition() {
            targetColorSet = (currentColorSet + 1) % colorSets.length;
            colorTransitionStartTime = time;
            isTransitioning = true;
        }

        function hslToRgb(h, s, l) {
            h /= 360;
            s /= 100;
            l /= 100;

            let r, g, b;

            if (s === 0) {
                r = g = b = l;
            } else {
                const hue2rgb = (p, q, t) => {
                    if (t < 0) t += 1;
                    if (t > 1) t -= 1;
                    if (t < 1/6) return p + (q - p) * 6 * t;
                    if (t < 1/2) return q;
                    if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
                    return p;
                };

                const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
                const p = 2 * l - q;

                r = hue2rgb(p, q, h + 1/3);
                g = hue2rgb(p, q, h);
                b = hue2rgb(p, q, h - 1/3);
            }

            return {
                r: Math.round(r * 255),
                g: Math.round(g * 255),
                b: Math.round(b * 255)
            };
        }

        function interpolateColor(color1, color2, t) {
            const easeT = 0.5 - Math.cos(t * Math.PI) * 0.5;
            return {
                r: color1.r * (1 - easeT) + color2.r * easeT,
                g: color1.g * (1 - easeT) + color2.g * easeT,
                b: color1.b * (1 - easeT) + color2.b * easeT
            };
        }

        function getGradientColor(x, y, width, height, timestamp) {
            const nx = x / width;
            const ny = y / height;
            const rotation = timestamp * 0.00001;
            const rotatedX = (nx - 0.5) * Math.cos(rotation) - (ny - 0.5) * Math.sin(rotation) + 0.5;
            const rotatedY = (nx - 0.5) * Math.sin(rotation) + (ny - 0.5) * Math.cos(rotation) + 0.5;
            const diagonalMix = (rotatedX + rotatedY) * 0.5;
            const radialMix = Math.sqrt((rotatedX - 0.5) ** 2 + (rotatedY - 0.5) ** 2);
            const gradientPos = (diagonalMix * 0.6 + radialMix * 0.4) % 1;

            let color1;
            const currentSet = colorSets[currentColorSet];
            const targetSet = colorSets[targetColorSet];

            if (isTransitioning) {
                const transitionProgress = Math.min(1, (timestamp - colorTransitionStartTime) / CONFIG.colorTransitionDuration);

                if (transitionProgress >= 1) {
                    isTransitioning = false;
                    currentColorSet = targetColorSet;
                    setTimeout(startColorTransition, 1000);
                }

                if (gradientPos < 0.5) {
                    const t = gradientPos * 2;
                    const currentColor = interpolateColor(currentSet[0], currentSet[1], t);
                    const targetColor = interpolateColor(targetSet[0], targetSet[1], t);
                    color1 = interpolateColor(currentColor, targetColor, transitionProgress);
                } else {
                    const t = (gradientPos - 0.5) * 2;
                    const currentColor = interpolateColor(currentSet[1], currentSet[2], t);
                    const targetColor = interpolateColor(targetSet[1], targetSet[2], t);
                    color1 = interpolateColor(currentColor, targetColor, transitionProgress);
                }
            } else {
                if (gradientPos < 0.5) {
                    const t = gradientPos * 2;
                    color1 = interpolateColor(currentSet[0], currentSet[1], t);
                } else {
                    const t = (gradientPos - 0.5) * 2;
                    color1 = interpolateColor(currentSet[1], currentSet[2], t);
                }
            }

            return color1;
        }

        function rand(min, max) {
            return Math.random() * (max - min) + min;
        }

        function initPoints(width, height) {
            points = [];

            const pointCount = Math.floor((width * height) / (35000 / CONFIG.density));

            for (let i = 0; i < pointCount; i++) {
                points.push({
                    x: rand(0, width),
                    y: rand(0, height),
                    vx: rand(-CONFIG.speed/15, CONFIG.speed/15),
                    vy: rand(-CONFIG.speed/15, CONFIG.speed/15)
                });
            }
        }

        function updatePoints(width, height) {
            for (let i = 0; i < points.length; i++) {
                let pt = points[i];

                pt.vx += rand(-0.03, 0.03);
                pt.vy += rand(-0.03, 0.03);

                const maxSpeed = CONFIG.speed / 12;
                const speed = Math.sqrt(pt.vx * pt.vx + pt.vy * pt.vy);
                if (speed > maxSpeed) {
                    pt.vx = (pt.vx / speed) * maxSpeed;
                    pt.vy = (pt.vy / speed) * maxSpeed;
                }

                pt.x += pt.vx;
                pt.y += pt.vy;

                if (pt.x <= 0 || pt.x >= width) {
                    pt.vx = -pt.vx * 0.95;
                    pt.x = pt.x <= 0 ? 1 : width - 1;
                }
                if (pt.y <= 0 || pt.y >= height) {
                    pt.vy = -pt.vy * 0.95;
                    pt.y = pt.y <= 0 ? 1 : height - 1;
                }
            }
        }

        function render(timestamp) {
            if (!lastTimestamp) lastTimestamp = timestamp;
            const deltaTime = timestamp - lastTimestamp;
            lastTimestamp = timestamp;

            time += deltaTime * 0.06;

            const width = window.innerWidth;
            const height = window.innerHeight;

            const renderWidth = Math.floor(width * resolutionScale);
            const renderHeight = Math.floor(height * resolutionScale);

            if (can.width !== renderWidth || can.height !== renderHeight) {
                can.width = renderWidth;
                can.height = renderHeight;
                can.style.width = width + 'px';
                can.style.height = height + 'px';
                initPoints(renderWidth, renderHeight);
            }

            updatePoints(renderWidth, renderHeight);

            const imageData = c.createImageData(renderWidth, renderHeight);
            const data = imageData.data;

            const pixelStep = 2;

            for (let y = 0; y < renderHeight; y += pixelStep) {
                for (let x = 0; x < renderWidth; x += pixelStep) {
                    const i = (x + y * renderWidth) * 4;

                    let closestDist = Infinity;
                    let secondClosestDist = Infinity;
                    let closestPoint = null;

                    for (let h = 0; h < points.length; h++) {
                        const pt = points[h];
                        const dx = x - pt.x;
                        const dy = y - pt.y;

                        const angle = Math.atan2(dy, dx);
                        const starMod = 1 + Math.sin(angle * 5) * 0.25;

                        const dist = (dx * dx + dy * dy) / starMod;

                        if (dist < closestDist) {
                            secondClosestDist = closestDist;
                            closestDist = dist;
                            closestPoint = pt;
                        } else if (dist < secondClosestDist) {
                            secondClosestDist = dist;
                        }
                    }

                    if (closestPoint) {
                        const distDiff = Math.sqrt(secondClosestDist) - Math.sqrt(closestDist);
                        const isEdge = distDiff < 2;

                        if (isEdge) {
                            data[i] = 255;
                            data[i + 1] = 255;
                            data[i + 2] = 255;
                            data[i + 3] = 255;
                        } else {
                            const pointColor = getGradientColor(closestPoint.x, closestPoint.y, renderWidth, renderHeight, time);
                            data[i] = pointColor.r;
                            data[i + 1] = pointColor.g;
                            data[i + 2] = pointColor.b;
                            data[i + 3] = 255;
                        }
                    } else {
                        data[i] = 8;
                        data[i + 1] = 8;
                        data[i + 2] = 10;
                        data[i + 3] = 255;
                    }

                    if (pixelStep > 1) {
                        for (let dy = 0; dy < pixelStep && y + dy < renderHeight; dy++) {
                            for (let dx = 0; dx < pixelStep && x + dx < renderWidth; dx++) {
                                if (dx === 0 && dy === 0) continue;
                                const ni = ((x + dx) + (y + dy) * renderWidth) * 4;
                                data[ni] = data[i];
                                data[ni + 1] = data[i + 1];
                                data[ni + 2] = data[i + 2];
                                data[ni + 3] = data[i + 3];
                            }
                        }
                    }
                }
            }

            c.putImageData(imageData, 0, 0);

            if (isAnimating) {
                animationId = requestAnimationFrame(render);
            }
        }

        function handleResize() {
            initPoints(can.width, can.height);
        }

        function init() {
            initColorSets();
            startColorTransition();
            isAnimating = true;
            animationId = requestAnimationFrame(render);
            window.addEventListener('resize', handleResize);
        }

        window.addEventListener('load', init);

        document.addEventListener('visibilitychange', function() {
            if (document.hidden) {
                isAnimating = false;
                if (animationId) {
                    cancelAnimationFrame(animationId);
                    animationId = null;
                }
            } else {
                isAnimating = true;
                lastTimestamp = 0;
                if (!animationId) {
                    animationId = requestAnimationFrame(render);
                }
            }
        });
    </script>
</body>
</html>