• 01 - Critter Models 02
  • 02 - VantaJS Shaders
  • 03 - App Bundle 02
  • 04 - Cathedrals
  • 05 - App Bundle 01
  • 06 - Retroscopic
  • 07 - Games
  • 08 - Voronoi
  • 09 - Terrain
  • 10 - Critter Models 01
  • 11 - Chromatophores
  • 12 - Conformal Maps
  • 13 - Echo Logix
  • 14 - Music
  • 15 - Colour Theory
MIKE'S NOTEBOOK
Notes
Number:
Project:
07Games
└ Rockfall└ Speak & Math└ Space Invaders└ Apache└ Asteroids└ Molly Roger└ Carrom└ Chess└ Connect 4└ Anagrams
08Voronoi
└ 001└ 002└ 003└ 004└ 005
09Terrain
└ Simple Polygon└ Simple Flyover└ Infinite Flyover 01└ Infinite Flyover 02└ Hexagonal Plane└ Mountain Ridges
10Critter Models 01
└ Wireframe Spider└ Wireframe Fly└ Wireframe Scorpion└ Wireframe Mosquito└ Wireframe Bee└ Wireframe Butterfly└ Wireframe Locust
11Chromatophores
└ 01└ 02└ 03└ 04
12Conformal Maps
└ 01└ 02└ 03└ 04└ 05
02 / 03

003 - VORONOI FLOW FIELD SOURCE CODE

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Voronoi 3</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            overflow: hidden;
            background: #0a0a0a;
            font-family: sans-serif;
            transition: background-color 0.5s ease;
        }

        body.light-mode {
            background-color: #ffffff;
        }

        .container {
            position: relative;
            width: 100vw;
            height: 100vh;
        }

        canvas {
            display: block;
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            opacity: 0;
            transition: opacity 0.8s ease;
        }

        canvas.active {
            opacity: 1;
        }

        .nav-buttons {
            position: absolute;
            bottom: 40px;
            right: 40px;
            left: auto;
            transform: none;
            display: flex;
            gap: 12px;
            z-index: 100;
        }

        .nav-button {
            width: 28px;
            height: 28px;
            background: transparent;
            border: 1px solid #fff;
            border-radius: 0;
            cursor: pointer;
            display: flex;
            align-items: center;
            justify-content: center;
            transition: all 0.2s ease;
            box-shadow: 2px 2px 0px 0px rgba(255,255,255,0.3);
        }

        body.light-mode .nav-button {
            border: 1px solid #000;
            box-shadow: 2px 2px 0px 0px rgba(0,0,0,0.8);
            background: transparent;
        }

        .nav-button:hover {
            background: #fff;
            transform: translate(2px, 2px);
            box-shadow: none;
        }
        
        body.light-mode .nav-button:hover {
            background: #000;
            transform: translate(2px, 2px);
            box-shadow: none;
        }

        .nav-button:active {
            transform: translate(2px, 2px) scale(0.95);
        }

        .nav-arrow {
            width: 8px;
            height: 8px;
            border: solid #fff;
            border-width: 0 1px 1px 0;
            display: inline-block;
            transition: border-color 0.2s ease;
        }

        .nav-button:hover .nav-arrow {
            border-color: #000;
        }
        
        body.light-mode .nav-arrow {
            border-color: #000;
        }

        body.light-mode .nav-button:hover .nav-arrow {
            border-color: #fff;
        }

        .arrow-left {
            transform: rotate(135deg);
            margin-left: 2px;
        }

        .arrow-right {
            transform: rotate(-45deg);
            margin-right: 2px;
        }
    </style>
</head>
<body>
    <div class="container">
        <canvas id="canvas1"></canvas>
        <canvas id="canvas2"></canvas>
        <canvas id="canvas3"></canvas>

        <div class="nav-buttons">
            <div class="nav-button" id="prevBtn">
                <div class="nav-arrow arrow-left"></div>
            </div>
            <div class="nav-button" id="nextBtn">
                <div class="nav-arrow arrow-right"></div>
            </div>
        </div>
    </div>

    <script>
        // ---------- THEME SYNC ----------
        function syncTheme() {
            try {
                let isDark = true;
                if (window.parent && window.parent.document) {
                    isDark = window.parent.document.documentElement.classList.contains('dark');
                } else {
                    const theme = localStorage.getItem('theme');
                    isDark = theme === 'dark' || (!theme && window.matchMedia('(prefers-color-scheme: dark)').matches);
                }
                
                if (!isDark) {
                    document.body.classList.add('light-mode');
                } else {
                    document.body.classList.remove('light-mode');
                }
            } catch (e) {
                console.warn("Could not sync theme", e);
            }
        }
        
        syncTheme();
        if (window.parent && window.parent.document) {
            try {
                const observer = new MutationObserver(syncTheme);
                observer.observe(window.parent.document.documentElement, { attributes: true, attributeFilter: ['class'] });
            } catch(e) {}
        }
        window.addEventListener('storage', syncTheme);

        // Global state
        let currentAnimation = 0;
        const animations = [
            {
                name: "Electric Dreams",
                canvas: null,
                ctx: null,
                points: [],
                time: 0,
                isActive: false,
                animationId: null
            },
            {
                name: "Neon Pulse",
                canvas: null,
                ctx: null,
                points: [],
                time: 0,
                isActive: false,
                animationId: null
            },
            {
                name: "Cyber Drift",
                canvas: null,
                ctx: null,
                points: [],
                time: 0,
                isActive: false,
                animationId: null
            }
        ];

        // Initialize canvases
        function initCanvases() {
            animations[0].canvas = document.getElementById('canvas1');
            animations[0].ctx = animations[0].canvas.getContext('2d', { alpha: true });
            animations[0].canvas.classList.add('active');
            animations[0].isActive = true;

            animations[1].canvas = document.getElementById('canvas2');
            animations[1].ctx = animations[1].canvas.getContext('2d', { alpha: true });

            animations[2].canvas = document.getElementById('canvas3');
            animations[2].ctx = animations[2].canvas.getContext('2d', { alpha: true });

            resizeCanvases();
        }

        // Resize all canvases
        function resizeCanvases() {
            animations.forEach(anim => {
                if (anim.canvas) {
                    anim.canvas.width = window.innerWidth;
                    anim.canvas.height = window.innerHeight;
                }
            });

            // Reinitialize points if needed
            animations.forEach((anim, i) => {
                if (anim.points.length > 0) initPoints(i);
            });
        }

        // Electric Point class - fast, sharp movements
        class ElectricPoint {
            constructor(x, y) {
                this.x = x;
                this.y = y;
                this.baseX = x;
                this.baseY = y;
                this.vx = (Math.random() - 0.5) * 3;
                this.vy = (Math.random() - 0.5) * 3;
                this.jitter = Math.random() * 5;
            }

            update(t, width, height) {
                // Sharp, erratic movements
                const jitterX = (Math.random() - 0.5) * this.jitter;
                const jitterY = (Math.random() - 0.5) * this.jitter;

                this.baseX += this.vx;
                this.baseY += this.vy;

                if (this.baseX < 0 || this.baseX > width) {
                    this.vx *= -1;
                    this.baseX = Math.max(0, Math.min(width, this.baseX));
                }
                if (this.baseY < 0 || this.baseY > height) {
                    this.vy *= -1;
                    this.baseY = Math.max(0, Math.min(height, this.baseY));
                }

                this.x = this.baseX + jitterX;
                this.y = this.baseY + jitterY;
            }
        }

        // Pulse Point class - rhythmic expansion/contraction
        class PulsePoint {
            constructor(x, y) {
                this.x = x;
                this.y = y;
                this.centerX = x;
                this.centerY = y;
                this.phase = Math.random() * Math.PI * 2;
                this.pulseSpeed = 0.8 + Math.random() * 0.4;
                this.pulseRadius = 40 + Math.random() * 60;
            }

            update(t, width, height) {
                const pulse = Math.sin(t * this.pulseSpeed + this.phase);
                const offsetX = Math.cos(this.phase) * pulse * this.pulseRadius;
                const offsetY = Math.sin(this.phase) * pulse * this.pulseRadius;

                this.x = this.centerX + offsetX;
                this.y = this.centerY + offsetY;

                // Slow drift
                this.centerX += Math.sin(t * 0.1 + this.phase) * 0.3;
                this.centerY += Math.cos(t * 0.1 + this.phase) * 0.3;

                // Keep in bounds
                this.centerX = Math.max(50, Math.min(width - 50, this.centerX));
                this.centerY = Math.max(50, Math.min(height - 50, this.centerY));
            }
        }

        // Drift Point class - smooth, flowing movements
        class DriftPoint {
            constructor(x, y) {
                this.x = x;
                this.y = y;
                this.angle = Math.random() * Math.PI * 2;
                this.speed = 0.3 + Math.random() * 0.7;
                this.spiralFactor = 0.01 + Math.random() * 0.02;
                this.radius = Math.sqrt(x * x + y * y);
            }

            update(t, width, height) {
                this.angle += this.spiralFactor;
                this.radius += Math.sin(t * 0.5) * this.speed;

                const centerX = width / 2;
                const centerY = height / 2;

                this.x = centerX + Math.cos(this.angle) * this.radius;
                this.y = centerY + Math.sin(this.angle) * this.radius;

                // Reset if too far
                const maxDist = Math.max(width, height);
                if (this.radius > maxDist * 0.6 || this.radius < 50) {
                    this.radius = 100 + Math.random() * 200;
                }
            }
        }

        // Initialize points for specific animation
        function initPoints(animIndex) {
            const anim = animations[animIndex];
            anim.points = [];
            const numPoints = animIndex === 0 ? 50 : animIndex === 1 ? 40 : 55;
            const centerX = anim.canvas.width / 2;
            const centerY = anim.canvas.height / 2;

            for (let i = 0; i < numPoints; i++) {
                const angle = (i / numPoints) * Math.PI * 2;
                const radius = Math.random() * Math.min(anim.canvas.width, anim.canvas.height) * 0.35;
                const x = centerX + Math.cos(angle) * radius;
                const y = centerY + Math.sin(angle) * radius;

                if (animIndex === 0) {
                    anim.points.push(new ElectricPoint(x, y));
                } else if (animIndex === 1) {
                    anim.points.push(new PulsePoint(x, y));
                } else {
                    anim.points.push(new DriftPoint(x - centerX, y - centerY));
                }
            }
        }

        // Color function for Animation 1 (Electric Dreams)
        function getColor1(index, total, t, isLightMode) {
            const hue = (index / total * 120 + t * 20) % 120 + 160; // Cyan to electric blue
            const lightness = 50 + Math.sin(t * 2 + index * 0.5) * 20;
            const saturation = 90 + Math.cos(t * 1.5 + index) * 10;
            const alpha = isLightMode ? 0.2 : 1;
            return `hsla(${hue}, ${saturation}%, ${lightness}%, ${alpha})`;
        }

        // Color function for Animation 2 (Neon Pulse)
        function getColor2(index, total, t, isLightMode) {
            const pulse = Math.sin(t * 1.2 + index * 0.3) * 0.5 + 0.5;
            const alpha = isLightMode ? 0.2 : 1;
            // Alternate between hot pink and electric green
            if ((index + Math.floor(t * 2)) % 2 === 0) {
                const hue = 320 + pulse * 20; // Hot pink to magenta
                const sat = 95;
                const light = 55 + pulse * 20;
                return `hsla(${hue}, ${sat}%, ${light}%, ${alpha})`;
            } else {
                const hue = 140 + pulse * 30; // Electric green
                const sat = 90;
                const light = 50 + pulse * 25;
                return `hsla(${hue}, ${sat}%, ${light}%, ${alpha})`;
            }
        }

        // Color function for Animation 3 (Cyber Drift)
        function getColor3(index, total, t, isLightMode) {
            const progress = index / total;
            const shift = Math.sin(t * 0.3) * 0.2;
            const adjustedProgress = (progress + shift) % 1;
            const alpha = isLightMode ? 0.2 : 1;

            // Purple to cyan gradient
            const hue = 280 - adjustedProgress * 100 + Math.sin(t * 0.5 + index) * 15;
            const sat = 85 + Math.cos(t * 0.4 + index * 0.6) * 10;
            const light = 45 + Math.sin(t * 0.6 + index * 0.3) * 15;

            return `hsla(${hue}, ${sat}%, ${light}%, ${alpha})`;
        }

        // Draw Voronoi for specific animation
        function drawVoronoi(animIndex) {
            const anim = animations[animIndex];
            const ctx = anim.ctx;
            const width = anim.canvas.width;
            const height = anim.canvas.height;
            const points = anim.points;
            const time = anim.time;
            const isLightMode = document.body.classList.contains('light-mode');

            // Clear with strong fade for sharper look
            if (isLightMode) {
                ctx.fillStyle = 'rgba(255, 255, 255, 0.4)';
            } else {
                ctx.fillStyle = 'rgba(10, 10, 10, 0.15)';
            }
            ctx.fillRect(0, 0, width, height);

            const step = 8; // Adjust step for performance
            let getColor;
            if (animIndex === 0) getColor = getColor1;
            else if (animIndex === 1) getColor = getColor2;
            else getColor = getColor3;

            // Grid-based rendering with glow effect
            for (let y = 0; y < height; y += step) {
                for (let x = 0; x < width; x += step) {
                    let minDist = Infinity;
                    let closestIndex = 0;

                    // Find closest point
                    for (let i = 0; i < points.length; i++) {
                        const dx = points[i].x - x;
                        const dy = points[i].y - y;
                        const d = dx * dx + dy * dy;
                        if (d < minDist) {
                            minDist = d;
                            closestIndex = i;
                        }
                    }

                    // Color based on closest point
                    const color = getColor(closestIndex, points.length, time, isLightMode);
                    ctx.fillStyle = color;
                    ctx.fillRect(x, y, step, step);

                    // Add subtle glow near boundaries
                    if (minDist > 5000 && Math.random() > 0.7) {
                        ctx.globalAlpha = 0.3;
                        ctx.fillRect(x, y, step, step);
                        ctx.globalAlpha = 1;
                    }
                }
            }
        }

        // Animation loop for specific animation
        function animate(animIndex) {
            const anim = animations[animIndex];

            if (!anim.isActive) return;

            anim.time += 0.016;

            // Update all points
            anim.points.forEach(p => p.update(anim.time, anim.canvas.width, anim.canvas.height));

            // Draw the Voronoi diagram
            drawVoronoi(animIndex);

            anim.animationId = requestAnimationFrame(() => animate(animIndex));
        }

        // Switch to a specific animation
        function switchAnimation(index) {
            // Stop all animations first
            animations.forEach(anim => {
                if (anim.animationId) {
                    cancelAnimationFrame(anim.animationId);
                    anim.animationId = null;
                }
                anim.canvas.classList.remove('active');
                anim.isActive = false;
            });

            // Update current index
            currentAnimation = index;

            // Start the new animation
            const newAnim = animations[index];
            newAnim.isActive = true;
            newAnim.canvas.classList.add('active');

            // Start animation if not already initialized
            if (newAnim.points.length === 0) {
                initPoints(index);
            }

            animate(index);
        }

        // Next animation
        function nextAnimation() {
            const nextIndex = (currentAnimation + 1) % animations.length;
            switchAnimation(nextIndex);
        }

        // Previous animation
        function prevAnimation() {
            const prevIndex = (currentAnimation - 1 + animations.length) % animations.length;
            switchAnimation(prevIndex);
        }

        // Initialize everything
        function init() {
            initCanvases();
            animations.forEach((anim, i) => initPoints(i));
            switchAnimation(0);

            // Button event listeners
            document.getElementById('nextBtn').addEventListener('click', nextAnimation);
            document.getElementById('prevBtn').addEventListener('click', prevAnimation);

            // Keyboard navigation
            document.addEventListener('keydown', (e) => {
                if (e.key === 'ArrowRight') nextAnimation();
                if (e.key === 'ArrowLeft') prevAnimation();
            });

            // Window resize
            window.addEventListener('resize', () => {
                resizeCanvases();
            });
        }

        // Start everything when page loads
        window.addEventListener('load', init);
    </script>
</body>
</html>
MikeHiggins.me.uk LinkedIn GitHub Bluesky Contra Upwork
© 2026 Mike Higgins. All rights reserved.