← Mike Higgins Home
Custom Code Experiment •

Barnsley Fern

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, maximum-scale=1.0, user-scalable=no">
    <title>Barnsley Fern Fractal</title>
    <style>
        html, body {
            background: black;
            overflow: hidden;
            width: 100%;
            height: 100%;
            margin: 0;
            padding: 0;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
            -webkit-tap-highlight-color: transparent;
            touch-action: manipulation;
        }

        #world {
            display: block;
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            max-width: 95vw;
            max-height: 70vh;
            width: auto;
            height: auto;
        }

        .info {
            position: absolute;
            top: 20px;
            right: 20px;
            background: rgba(0, 0, 0, 0.85);
            color: #ffffff;
            padding: 20px;
            border-radius: 12px;
            max-width: 320px;
            font-size: 14px;
            line-height: 1.6;
            z-index: 10;
            border: 1px solid rgba(255, 255, 255, 0.1);
            box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
            backdrop-filter: blur(10px);
            -webkit-backdrop-filter: blur(10px);
        }

        .info .title {
            font-size: 20px;
            color: #4ade80;
            text-align: center;
            margin: 0 0 15px 0;
            font-weight: bold;
        }

        .info p {
            margin: 10px 0;
            color: #e5e7eb;
        }

        .info a {
            color: #60a5fa;
            text-decoration: none;
            transition: color 0.3s;
        }

        .info a:hover {
            color: #93c5fd;
            text-decoration: underline;
        }

        .counter {
            position: absolute;
            top: 20px;
            left: 20px;
            background: rgba(0, 0, 0, 0.85);
            color: #ffffff;
            padding: 15px 20px;
            border-radius: 12px;
            font-size: 16px;
            z-index: 10;
            border: 1px solid rgba(255, 255, 255, 0.1);
            box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
            backdrop-filter: blur(10px);
            -webkit-backdrop-filter: blur(10px);
            font-weight: 500;
        }

        .instructions {
            position: absolute;
            bottom: 30px;
            left: 0;
            width: 100%;
            text-align: center;
            color: #ffffff;
            font-size: 14px;
            z-index: 10;
            padding: 15px;
            background: rgba(0, 0, 0, 0.7);
            backdrop-filter: blur(10px);
            -webkit-backdrop-filter: blur(10px);
            border-top: 1px solid rgba(255, 255, 255, 0.1);
        }

        .instructions strong {
            color: #4ade80;
            font-weight: 600;
        }

        /* Mobile-specific styles */
        @media (max-width: 768px) {
            #world {
                max-width: 90vw;
                max-height: 60vh;
            }

            .info {
                position: relative;
                top: auto;
                right: auto;
                left: 50%;
                transform: translateX(-50%);
                max-width: 85vw;
                margin: 20px auto;
                font-size: 13px;
                padding: 15px;
            }

            .counter {
                top: 10px;
                left: 50%;
                transform: translateX(-50%);
                width: auto;
                text-align: center;
                padding: 12px 18px;
                font-size: 14px;
            }

            .instructions {
                font-size: 13px;
                padding: 12px;
                bottom: 20px;
            }

            .info .title {
                font-size: 18px;
            }
        }

        @media (max-width: 480px) {
            #world {
                max-width: 85vw;
                max-height: 55vh;
            }

            .info {
                max-width: 90vw;
                padding: 12px;
                font-size: 12px;
            }

            .counter {
                padding: 10px 15px;
                font-size: 13px;
                top: 10px;
            }

            .instructions {
                font-size: 12px;
                padding: 10px;
                bottom: 15px;
            }

            .info .title {
                font-size: 16px;
                margin-bottom: 10px;
            }
        }

        /* Landscape orientation optimization */
        @media (max-height: 500px) and (orientation: landscape) {
            #world {
                max-height: 80vh;
                max-width: 70vw;
            }

            .info {
                max-width: 280px;
                font-size: 12px;
                padding: 12px;
            }

            .counter {
                padding: 8px 12px;
                font-size: 13px;
            }

            .instructions {
                padding: 8px;
                font-size: 12px;
            }
        }

        /* Prevent text selection on mobile */
        .info, .counter, .instructions {
            user-select: none;
            -webkit-user-select: none;
        }
    </style>
</head>
<body>
    <canvas id="world"></canvas>

    <div class="counter">
        Points drawn: <span id="counter">0</span>
    </div>

    <div class="info">
        <p>This fractal is generated using an iterated function system (IFS). Each point is transformed by one of four affine transformations chosen randomly with different probabilities.</p>
        <p><strong>Tap anywhere to reset the animation.</strong> The fern will continue generating points until it reaches maximum detail.</p>
        <p><strong>References:</strong><br>
        <a href="http://en.wikipedia.org/wiki/Iterated_function_system" target="_blank">Iterated Function System</a><br>
        <a href="http://en.wikipedia.org/wiki/Barnsley_fern" target="_blank">Barnsley Fern</a><br>
        <a href="http://mathworld.wolfram.com/BarnsleysFern.html" target="_blank">MathWorld: Barnsley's Fern</a></p>
    </div>

    <div class="instructions">
        <strong>Tap anywhere to reset</strong>
    </div>

    <script>
        // Canvas dimensions
        const W = 350;
        const H = 500;

        // Fixed speed
        const speed = 2;

        // Transformation functions for the Barnsley fern
        const transformations = [
            [
                (x, y) => 0,
                (x, y) => 0.16 * y,
                1
            ],
            [
                (x, y) => 0.2 * x - 0.26 * y,
                (x, y) => 0.23 * x + 0.22 * y + 1.6,
                7
            ],
            [
                (x, y) => -0.15 * x + 0.28 * y,
                (x, y) => 0.26 * x + 0.24 * y + 0.44,
                7
            ],
            [
                (x, y) => 0.85 * x + 0.04 * y,
                (x, y) => -0.04 * x + 0.85 * y + 1.6,
                85
            ]
        ];

        const maxDraws = 200000;

        const xRange = [ -2.182, 2.6558 ];
        const yRange = [ 0, 9.9983 ];

        const canvas = document.getElementById("world");
        const ctx = canvas.getContext("2d");

        // Handle high DPI displays
        const dpr = window.devicePixelRatio || 1;
        canvas.width = W * dpr;
        canvas.height = H * dpr;
        canvas.style.width = W + 'px';
        canvas.style.height = H + 'px';
        ctx.scale(dpr, dpr);

        const colorStops = [
            [ 0, "rgba(255, 255, 255, 1)" ],
            [ 1000, "rgba(253, 253, 52, 0.6)" ],
            [ 5000, "rgba(0, 171, 56, 0.7)" ],
            [ 10000, "rgba(168, 210, 110, 0.7)" ],
            [ 20000, "rgba(246, 243, 27, 0.6)" ],
            [ 40000, "rgba(138, 215, 39, 0.7)" ],
            [ 65000, "rgba(243, 205, 8, 0.5)" ],
            [ 90000, "rgba(150, 204, 104, 0.4)" ],
            [ 125000, "rgba(137, 230, 101, 0.5)" ],
            [ 150000, "rgba(246, 243, 27, 0.5)" ],
            [ 190000, "rgba(255, 255, 255, 0.8)" ]
        ];

        function animationCurve(frame) {
            if (frame < 1000) return 25;
            if (frame < 5000) return 50;
            if (frame < 10000) return 75;
            if (frame < 20000) return 100;
            if (frame < 40000) return 150;
            if (frame < 65000) return 250;
            if (frame < 90000) return 400;
            if (frame < 150000) return 600;
            if (frame < 190000) return 400;
            if (frame < 200000) return 100;
            return 100;
        }

        // Build lookup table for transformations
        let lut = {};
        let pMax = 0;

        for (let i = 0; i < transformations.length; i++) {
            const row = transformations[i];
            for (let j = pMax; j < row[2] + pMax; j++) {
                lut[j] = [row[0], row[1]];
            }
            pMax += row[2];
        }

        let numDraws = 0;
        let colors = [];
        let animationId = null;
        let isAnimating = false;

        function getColors() {
            colors = colorStops.slice();
            return function() {
                if (colors[0] && numDraws >= colors[0][0]) {
                    const colorStop = colors.shift();
                    ctx.fillStyle = colorStop[1];
                }
            };
        }

        // Initialize color
        let updateColor = getColors();

        const xSpan = xRange[1] - xRange[0];
        const ySpan = yRange[1] - yRange[0];

        // Update counter display
        function updateCounter() {
            document.getElementById("counter").textContent = numDraws.toLocaleString();
        }

        // Random number generator
        function rand(a, b) {
            return (b - a) * Math.random() + a;
        }

        // Main render function
        function render() {
            if (!isAnimating) return;

            if (numDraws > maxDraws) {
                isAnimating = false;
                return;
            }

            updateColor();
            const drawsPerFrame = speed * animationCurve(numDraws);

            for (let i = 0; i < drawsPerFrame && numDraws < maxDraws; i++) {
                let iter = 0;
                let x = rand(xRange[0], xRange[1]);
                let y = rand(yRange[0], yRange[1]);

                // Apply multiple iterations to reach attractor
                while (iter++ < 50) {
                    const f = lut[Math.floor(rand(0, pMax))];
                    const newX = f[0](x, y);
                    const newY = f[1](x, y);
                    x = newX;
                    y = newY;
                }

                // Map to canvas coordinates
                const canvasX = W * (x - xRange[0]) / xSpan;
                const canvasY = H * (1 - y / ySpan);

                ctx.fillRect(canvasX, canvasY, 1, 1);
                numDraws++;
            }

            updateCounter();

            // Continue animation
            if (isAnimating) {
                animationId = requestAnimationFrame(render);
            }
        }

        // Start animation
        function startAnimation() {
            if (!isAnimating) {
                isAnimating = true;
                animationId = requestAnimationFrame(render);
            }
        }

        // Stop animation
        function stopAnimation() {
            isAnimating = false;
            if (animationId) {
                cancelAnimationFrame(animationId);
                animationId = null;
            }
        }

        // Reset function - FIXED VERSION
        function resetFern() {
            // Stop current animation
            stopAnimation();

            // Reset counters and colors
            numDraws = 0;
            updateColor = getColors();

            // Clear canvas
            ctx.clearRect(0, 0, W, H);

            // Reset to initial color
            ctx.fillStyle = colorStops[0][1];

            // Update counter
            updateCounter();

            // Restart animation
            setTimeout(() => {
                startAnimation();
            }, 50);
        }

        // Set up event handlers
        canvas.addEventListener('click', resetFern);
        canvas.addEventListener('touchstart', (e) => {
            e.preventDefault();
            resetFern();
        });

        // Initialize and start animation
        updateCounter();
        startAnimation();

        // Handle window resize
        function handleResize() {
            canvas.style.maxWidth = '95vw';
            canvas.style.maxHeight = '70vh';
        }

        window.addEventListener('resize', handleResize);
        handleResize(); // Initial call

        // Clean up on page unload
        window.addEventListener('beforeunload', () => {
            stopAnimation();
        });
    </script>
</body>
</html>