← Mike Higgins Home
Custom Code Experiment •

Julia Set

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>Julia Set</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            background: #000;
            color: #eee;
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            overflow: hidden;
            height: 100vh;
        }

        .viewer-container {
            position: relative;
            width: 100%;
            height: 100vh;
            overflow: hidden;
        }

        #fractal {
            display: block;
            width: 100%;
            height: 100%;
        }

        .control-panel {
            position: absolute;
            top: 20px;
            right: 20px;
            background: rgba(20, 20, 30, 0.85);
            backdrop-filter: blur(10px);
            border-radius: 12px;
            padding: 1rem;
            display: flex;
            flex-direction: column;
            gap: 1rem;
            box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
            border: 1px solid rgba(255, 255, 255, 0.1);
            min-width: 200px;
            z-index: 50;
        }

        .control-group {
            display: flex;
            flex-direction: column;
            gap: 0.5rem;
        }

        .control-row {
            display: flex;
            align-items: center;
            gap: 0.5rem;
            justify-content: space-between;
        }

        .control-label {
            font-size: 0.9rem;
            min-width: 100px;
            text-align: left;
        }

        .slider-container {
            display: flex;
            align-items: center;
            gap: 0.8rem;
            flex: 1;
        }

        input[type="range"] {
            flex: 1;
            height: 6px;
            border-radius: 3px;
            background: #333;
            outline: none;
            -webkit-appearance: none;
            margin: 0;
        }

        input[type="range"]::-webkit-slider-thumb {
            -webkit-appearance: none;
            width: 18px;
            height: 18px;
            border-radius: 50%;
            background: #6a11cb;
            cursor: pointer;
        }

        .iteration-value {
            font-family: monospace;
            font-size: 0.9rem;
            min-width: 35px;
            text-align: center;
            background: rgba(255, 255, 255, 0.1);
            padding: 0.2rem 0.4rem;
            border-radius: 4px;
        }

        select {
            background: #1a1a2e;
            color: white;
            border: 1px solid #333;
            border-radius: 6px;
            padding: 0.4rem;
            cursor: pointer;
            flex: 1;
        }

        @media (max-width: 768px) {
            .control-panel {
                top: 10px;
                right: 10px;
                left: 10px;
                flex-direction: column;
                min-width: auto;
                padding: 0.8rem;
            }

            .control-group {
                width: 100%;
            }

            .control-row {
                width: 100%;
                gap: 0.8rem;
            }

            .control-label {
                min-width: 90px;
                font-size: 0.85rem;
            }

            .slider-container {
                gap: 0.6rem;
            }

            input[type="range"], select {
                flex: 1;
            }

            .iteration-value {
                font-size: 0.85rem;
                min-width: 30px;
            }
        }

        @media (max-width: 480px) {
            .control-panel {
                padding: 0.7rem;
                gap: 0.8rem;
            }

            .control-label {
                min-width: 80px;
                font-size: 0.8rem;
            }

            .iteration-value {
                font-size: 0.8rem;
                min-width: 28px;
                padding: 0.15rem 0.3rem;
            }
        }
    </style>
</head>
<body>
    <div class="viewer-container">
        <canvas id="fractal"></canvas>

        <div class="control-panel">
            <div class="control-group">
                <div class="control-row">
                    <div class="control-label">Iterations:</div>
                    <div class="slider-container">
                        <input type="range" id="iterSlider" min="10" max="100" value="10">
                        <span class="iteration-value" id="iterationValue">10</span>
                    </div>
                </div>
            </div>

            <div class="control-group">
                <div class="control-row">
                    <div class="control-label">Colour Palette:</div>
                    <select id="paletteSelect"></select>
                </div>
            </div>
        </div>
    </div>

    <script>
        const canvas = document.getElementById('fractal');
        const ctx = canvas.getContext('2d');

        const state = {
            center_x: 0,
            center_y: 0,
            scale: 1.0,
            max_iter: 10,
            palette: 0,
            c_re: -0.7,
            c_im: 0.27015,
            frame: 0,
            width: 0,
            height: 0,
            animationId: null
        };

        const palettes = [
            t => {
                const r = Math.sin(15 * t + 0) * 127 + 128;
                const g = Math.sin(15 * t + 2) * 127 + 128;
                const b = Math.sin(15 * t + 4) * 127 + 128;
                return [r, g, b];
            },
            t => {
                const r = 255;
                const g = Math.floor(255 * t);
                const b = Math.floor(100 * t * t);
                return [r, g, b];
            },
            t => {
                const r = Math.floor(20 + 50 * t);
                const g = Math.floor(100 + 100 * t);
                const b = Math.floor(200 + 55 * t);
                return [r, g, b];
            },
            t => {
                const r = Math.floor(50 * t);
                const g = Math.floor(150 + 50 * t);
                const b = Math.floor(50 * t);
                return [r, g, b];
            },
            t => {
                const r = Math.floor(150 + 50 * Math.sin(10 * t));
                const g = Math.floor(50 * t);
                const b = Math.floor(150 + 50 * Math.cos(10 * t));
                return [r, g, b];
            },
            t => {
                const r = Math.floor(255 - 50 * t);
                const g = Math.floor(150 - 100 * t);
                const b = Math.floor(100 + 50 * t);
                return [r, g, b];
            }
        ];

        const paletteNames = [
            "Rainbow", "Fire", "Ocean", "Forest", "Purple", "Sunset"
        ];

        function initUI() {
            const paletteSelect = document.getElementById('paletteSelect');
            paletteNames.forEach((name, i) => {
                const option = document.createElement('option');
                option.value = i;
                option.textContent = name;
                paletteSelect.appendChild(option);
            });

            const iterSlider = document.getElementById('iterSlider');
            const iterationValue = document.getElementById('iterationValue');

            iterSlider.addEventListener('input', (e) => {
                state.max_iter = parseInt(e.target.value);
                iterationValue.textContent = state.max_iter;
                renderFractal();
            });

            document.getElementById('paletteSelect').addEventListener('change', (e) => {
                state.palette = parseInt(e.target.value);
                renderFractal();
            });
        }

        function animate() {
            state.frame++;
            const t = state.frame * 0.01;
            state.c_re = 0.7885 * Math.cos(t);
            state.c_im = 0.7885 * Math.sin(t);
            renderFractal();
            state.animationId = requestAnimationFrame(animate);
        }

        function resizeCanvas() {
            state.width = canvas.width = window.innerWidth;
            state.height = canvas.height = window.innerHeight;

            renderFractal();
        }

        function renderFractal() {
            const imageData = ctx.createImageData(state.width, state.height);
            const data = imageData.data;

            const aspect = state.width / state.height;
            const view_w = 3.5 / state.scale;
            const view_h = view_w / aspect;
            const left = state.center_x - view_w / 2;
            const top = state.center_y - view_h / 2;

            for (let y = 0; y < state.height; y++) {
                for (let x = 0; x < state.width; x++) {
                    const cx = left + x * view_w / state.width;
                    const cy = top + y * view_h / state.height;

                    let zx = cx;
                    let zy = cy;
                    const cx0 = state.c_re;
                    const cy0 = state.c_im;

                    let i = 0;
                    while (zx * zx + zy * zy <= 4 && i < state.max_iter) {
                        const tmp = zx * zx - zy * zy + cx0;
                        zy = 2 * zx * zy + cy0;
                        zx = tmp;
                        i++;
                    }

                    const idx = (y * state.width + x) * 4;

                    if (i === state.max_iter) {
                        data[idx] = 0;
                        data[idx+1] = 0;
                        data[idx+2] = 0;
                        data[idx+3] = 255;
                    } else {
                        const t = i / state.max_iter;
                        const [r, g, b] = palettes[state.palette](t);
                        data[idx] = r;
                        data[idx+1] = g;
                        data[idx+2] = b;
                        data[idx+3] = 255;
                    }
                }
            }

            ctx.putImageData(imageData, 0, 0);
        }

        window.addEventListener('load', () => {
            resizeCanvas();
            initUI();
            renderFractal();
            animate();
        });

        window.addEventListener('resize', resizeCanvas);
    </script>
</body>
</html>