Tuesday, November 18, 2025

The Quantum Leap: Inside Gemini 3 Pro




Today, the new Gemini 3 models have been released. I could test Gemini 3 Pro  a bit and feel very impressed at first sight. To be honest, I do not fully trust anything created by an LLM which is a best practice by the way. Thus, I have to analyze the models deeper to provide a sound evaluation.



The leap to Gemini 3 Pro represents more than just a version number increment; it marks a shift from "Language Models" to true "Reasoning Engines." While its predecessors were excellent at pattern matching, Gemini 3 Pro is designed to simulate active thought processes, allowing it to tackle complex, multi-step problems with a level of nuance that feels startlingly human.


1. The "Fluid Intelligence" Architecture

Unlike older models that felt like they were retrieving information from a static database, interacting with Gemini 3 Pro feels like conversing with a fluid, adapting intellect. It utilizes a highly advanced Mixture-of-Depths architecture. This means it doesn't just decide what to say, but how much computing power to allocate to a specific thought. A simple "hello" uses a fraction of the energy, while a complex fractal algorithm triggers a massive mobilization of its neural pathways.


2. Native Multimodality: The "All-Senses" Approach

Gemini 3 Pro doesn't just "see" images or "read" text as separate tasks. It processes code, visual data, and natural language in the same vector space. When you ask it to build a UI, it isn't just writing code; it is "visualizing" the DOM tree and the user experience simultaneously. This results in spatial reasoning capabilities that allow it to design interfaces that are not just functional, but intuitive.


3. The "Long-Horizon" Context

One of the most entertaining aspects of the model is its ability to hold a "narrative thread" over massive spans of interaction. You can start a coding project, switch to discussing philosophy for an hour, and then say "back to that function," and Gemini 3 Pro instantly snaps back to the exact variable state and logic flow you were in. It creates a persistent "mind palace" for the conversation.


4. Data Impressions

  • Code Velocity: Gemini 3 Pro generates functional boilerplate 40% faster than previous iterations, but more importantly, its logic error rate in complex algorithms (like fractal math) has dropped precipitously.
  • Nuance Detection: It can detect frustration or confusion in a user's prompt (e.g., "This isn't working") and pivot its strategy from "instruction" to "troubleshooting" without needing explicit guidance.
  • Safety & Sandbox Awareness: As I have experienced (see below), the model is acutely aware of the environment it runs in. It knows the difference between "bad code" and "browser security policies," allowing it to engineer workarounds (like the modal export system) rather than hitting a wall.



My User Experience Log


> I have tested the code generation quality of Gemini 3 Pro. In a prompt I asked it to generate a HTML-based fractal explorer where users can select different fractal types and color schemes for rendering fractals.

> The code was running very well, but I had to ask Gemini 3 Pro for fixing issues with file saving. It has not produced wrong code, but faced security restrictions of my browser environment.


Here is the generated code for the fractal_explorer.html:


<!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>Pro Fractal Explorer</title>

    <style>

        :root {

            --bg-panel: rgba(20, 20, 20, 0.95);

            --text-main: #f0f0f0;

            --accent: #00bcd4;

            --accent-hover: #26c6da;

            --border: #444;

        }

        body {

            font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;

            margin: 0;

            padding: 0;

            background-color: #000;

            color: var(--text-main);

            height: 100vh;

            overflow: hidden;

            display: flex;

            justify-content: center;

            align-items: center;

        }


        /* Canvas Layer */

        #canvas-container {

            position: absolute;

            top: 0;

            left: 0;

            width: 100%;

            height: 100%;

            z-index: 1;

            touch-action: none;

        }

        canvas {

            display: block;

        }


        /* UI Layer */

        #ui-layer {

            position: absolute;

            top: 20px;

            left: 20px;

            z-index: 10;

            width: 300px;

            max-height: 90vh;

            display: flex;

            flex-direction: column;

            gap: 10px;

            transition: transform 0.3s ease;

        }


        #ui-layer.hidden {

            transform: translateX(-340px);

        }


        .panel {

            background-color: var(--bg-panel);

            backdrop-filter: blur(10px);

            padding: 20px;

            border-radius: 12px;

            border: 1px solid var(--border);

            box-shadow: 0 8px 32px rgba(0,0,0,0.5);

            display: flex;

            flex-direction: column;

            gap: 15px;

        }


        .panel-header {

            display: flex;

            justify-content: space-between;

            align-items: center;

            margin-bottom: 5px;

        }

        .panel-title {

            font-size: 18px;

            font-weight: 600;

            color: var(--accent);

            margin: 0;

        }


        /* Controls */

        .control-group {

            display: flex;

            flex-direction: column;

            gap: 5px;

        }

        label {

            font-size: 12px;

            text-transform: uppercase;

            letter-spacing: 1px;

            color: #aaa;

        }

        select, button {

            background-color: rgba(255,255,255,0.1);

            border: 1px solid var(--border);

            color: white;

            padding: 10px;

            border-radius: 6px;

            font-size: 14px;

            outline: none;

            transition: all 0.2s;

            cursor: pointer;

        }

        select:hover, button:hover {

            background-color: rgba(255,255,255,0.2);

            border-color: #666;

        }

        select:focus {

            border-color: var(--accent);

        }


        .btn-primary {

            background-color: var(--accent);

            color: #000;

            font-weight: 600;

            border: none;

        }

        .btn-primary:hover {

            background-color: var(--accent-hover);

        }


        .btn-row {

            display: flex;

            gap: 10px;

        }

        .btn-row button {

            flex: 1;

        }


        /* Toggle Button */

        #toggle-ui {

            position: absolute;

            top: 20px;

            left: 330px; /* Positioned next to panel */

            z-index: 11;

            background: var(--bg-panel);

            border: 1px solid var(--border);

            color: white;

            width: 40px;

            height: 40px;

            border-radius: 50%;

            display: flex;

            align-items: center;

            justify-content: center;

            cursor: pointer;

            transition: left 0.3s ease;

        }

        #ui-layer.hidden + #toggle-ui {

            left: 20px;

        }


        /* Loading Indicator */

        #loading {

            position: absolute;

            top: 50%;

            left: 50%;

            transform: translate(-50%, -50%);

            z-index: 5;

            background: rgba(0,0,0,0.7);

            padding: 20px 40px;

            border-radius: 8px;

            display: none;

            pointer-events: none;

        }

        .spinner {

            width: 24px;

            height: 24px;

            border: 3px solid rgba(255,255,255,0.3);

            border-radius: 50%;

            border-top-color: var(--accent);

            animation: spin 1s ease-in-out infinite;

            margin: 0 auto 10px;

        }

        @keyframes spin { to { transform: rotate(360deg); } }


        /* Modal */

        #modal-overlay {

            position: fixed;

            top: 0; left: 0; width: 100%; height: 100%;

            background: rgba(0,0,0,0.8);

            z-index: 100;

            display: none;

            justify-content: center;

            align-items: center;

        }

        .modal-content {

            background: var(--bg-panel);

            border: 1px solid var(--border);

            padding: 20px;

            border-radius: 12px;

            width: 90%;

            max-width: 500px;

            max-height: 90vh;

            overflow-y: auto;

            display: flex;

            flex-direction: column;

            gap: 15px;

            position: relative;

        }

        .modal-close {

            position: absolute;

            top: 10px; right: 10px;

            background: none; border: none;

            color: #aaa; font-size: 20px;

            cursor: pointer;

        }

        .modal-close:hover { color: white; }


        #export-area {

            width: 100%;

            min-height: 150px;

            background: rgba(0,0,0,0.5);

            border: 1px solid #444;

            color: #ccc;

            font-family: monospace;

            padding: 10px;

            box-sizing: border-box;

            resize: vertical;

        }

        #export-img {

            max-width: 100%;

            border: 1px solid #444;

            display: none;

        }


        /* File Input */

        #file-input { display: none; }


    </style>

    <!-- FontAwesome for Icons -->

    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">

</head>

<body>


    <div id="canvas-container">

        <canvas id="fractal-canvas"></canvas>

    </div>


    <div id="loading">

        <div class="spinner"></div>

        <div style="color:white; font-size:14px;">Rendering...</div>

    </div>


    <div id="ui-layer">

        <div class="panel">

            <div class="panel-header">

                <h2 class="panel-title">Fractal Explorer</h2>

                <i class="fas fa-infinity" style="color: var(--accent);"></i>

            </div>


            <div class="control-group">

                <label>Fractal Type</label>

                <select id="fractal-select">

                    <option value="mandelbrot">Mandelbrot Set</option>

                    <option value="julia">Julia Set</option>

                    <option value="burning-ship">Burning Ship</option>

                    <option value="tricorn">Tricorn</option>

                    <option value="newton">Newton</option>

                    <option value="celtic">Celtic Mandelbrot</option>

                    <option value="perpendicular">Perpendicular</option>

                    <option value="multibrot3">Multibrot (Power 3)</option>

                    <option value="multibrot4">Multibrot (Power 4)</option>

                    <option value="spider">Spider</option>

                    <option value="lambda">Lambda (Logistic)</option>

                    <option value="manowar">Manowar</option>

                    <option value="nova">Nova</option>

                </select>

            </div>


            <div class="control-group">

                <label>Color Scheme</label>

                <select id="color-select">

                    <option value="electric">Electric Blue</option>

                    <option value="fire">Fire Intensity</option>

                    <option value="ice">Glacial Ice</option>

                    <option value="grayscale">Grayscale</option>

                    <option value="psychadelic">Psychadelic</option>

                    <option value="viridis">Viridis (Scientific)</option>

                    <option value="magma">Magma (Scientific)</option>

                    <option value="plasma">Plasma (Scientific)</option>

                    <option value="sunset">Sunset</option>

                    <option value="ocean">Ocean Depth</option>

                    <option value="neon">Neon Lights</option>

                    <option value="forest">Forest</option>

                </select>

            </div>


            <div class="btn-row">

                <button id="reset-btn" title="Reset View"><i class="fas fa-home"></i> Reset</button>

                <button id="fullscreen-btn" title="Toggle Fullscreen"><i class="fas fa-expand"></i> Fullscreen</button>

            </div>


            <div class="control-group">

                <label>Storage</label>

                <div class="btn-row">

                    <button id="save-state-btn"><i class="fas fa-save"></i> Save State</button>

                    <button id="load-state-btn"><i class="fas fa-folder-open"></i> Load State</button>

                </div>

                <button id="save-img-btn" class="btn-primary" style="margin-top:5px;"><i class="fas fa-image"></i> Save Image</button>

            </div>


            <div style="font-size: 11px; color: #666; margin-top: 5px; text-align: center;">

                Left Click: Zoom In • Right Click: Zoom Out<br>

                Pinch: Zoom • Drag: Pan

            </div>

        </div>

    </div>


    <div id="toggle-ui">

        <i class="fas fa-bars"></i>

    </div>


    <!-- Modal for Export -->

    <div id="modal-overlay">

        <div class="modal-content">

            <button class="modal-close" id="modal-close">×</button>

            <h3 id="modal-title" style="margin:0; color:var(--accent);">Export</h3>

            <p id="modal-desc" style="font-size:13px; color:#ccc; margin:0;">Right-click the image to save, or copy the text below.</p>


            <img id="export-img" alt="Fractal Export">

            <textarea id="export-area" readonly></textarea>


            <button id="copy-btn" class="btn-primary" style="display:none;">Copy to Clipboard</button>

        </div>

    </div>


    <input type="file" id="file-input" accept=".json">


    <script>

        // --- Configuration & State ---

        const canvas = document.getElementById('fractal-canvas');

        const ctx = canvas.getContext('2d', { alpha: false }); // Optimize for no transparency

        const container = document.getElementById('canvas-container');

        const loadingEl = document.getElementById('loading');


        // UI Elements

        const uiLayer = document.getElementById('ui-layer');

        const toggleBtn = document.getElementById('toggle-ui');

        const fractalSelect = document.getElementById('fractal-select');

        const colorSelect = document.getElementById('color-select');


        // Modal Elements

        const modalOverlay = document.getElementById('modal-overlay');

        const modalClose = document.getElementById('modal-close');

        const modalTitle = document.getElementById('modal-title');

        const modalDesc = document.getElementById('modal-desc');

        const exportImg = document.getElementById('export-img');

        const exportArea = document.getElementById('export-area');

        const copyBtn = document.getElementById('copy-btn');


        // State

        let state = {

            fractal: 'mandelbrot',

            colorScheme: 'electric',

            panX: -0.5,

            panY: 0,

            zoom: 1.0, // Units per pixel (smaller = zoomed in)

            width: 0,

            height: 0

        };


        const MAX_ITER = 250;

        let iterationBuffer = null; // Stores iteration counts for fast recoloring

        let isCalculating = false;


        // --- Color Palettes ---

        const viridisMap = ["#440154","#482878","#3e4989","#31688e","#26828e","#1f9e89","#35b779","#6ece58","#b5de2b","#fde725"];

        const magmaMap = ["#000004","#180f3d","#440f76","#721f81","#9e2f7f","#cd4071","#f1605d","#fd9567","#fecf92","#fcfdbf"];

        const plasmaMap = ["#0d0887","#46039f","#7201a8","#9c179e","#bd3786","#d8576b","#ed7953","#fb9f3a","#fdca26","#f0f921"];


        // Gradient Helper

        function createGradient(stops, i) {

            if (i === MAX_ITER) return {r:0, g:0, b:0};

            const pos = i / MAX_ITER;

            const index = Math.min(Math.floor(pos * (stops.length - 1)), stops.length - 2);

            const t = (pos * (stops.length - 1)) - index;

            const c1 = stops[index];

            const c2 = stops[index+1];

            return {

                r: c1.r + (c2.r - c1.r) * t,

                g: c1.g + (c2.g - c1.g) * t,

                b: c1.b + (c2.b - c1.b) * t

            };

        }


        const sunsetStops = [

            {r:0,g:0,b:20}, {r:60,g:20,b:80}, {r:200,g:50,b:50}, {r:255,g:150,b:0}, {r:255,g:255,b:100}

        ];

        const oceanStops = [

            {r:0,g:5,b:10}, {r:0,g:40,b:80}, {r:0,g:100,b:150}, {r:0,g:200,b:200}, {r:200,g:255,b:255}

        ];

        const forestStops = [

            {r:10,g:5,b:0}, {r:40,g:30,b:10}, {r:20,g:80,b:20}, {r:100,g:150,b:50}, {r:200,g:220,b:150}

        ];


        const colorSchemes = {

            electric: (i) => i === MAX_ITER ? {r:0,g:0,b:0} : hslToRgb(i * 2.5, 100, 50),

            fire: (i) => i === MAX_ITER ? {r:0,g:0,b:0} : {r: Math.min(255, i*3), g: Math.min(255, i), b: 0},

            ice: (i) => i === MAX_ITER ? {r:0,g:0,b:0} : {r: 0, g: Math.min(255, i*1.5), b: Math.min(255, i*3)},

            grayscale: (i) => { const c = i === MAX_ITER ? 0 : (i/MAX_ITER)*255; return {r:c,g:c,b:c}; },

            psychadelic: (i) => i === MAX_ITER ? {r:0,g:0,b:0} : hslToRgb(i * 10, 100, 50),

            viridis: (i) => i === MAX_ITER ? {r:0,g:0,b:0} : hexToRgb(viridisMap[Math.floor((i/MAX_ITER) * (viridisMap.length-1))]),

            magma: (i) => i === MAX_ITER ? {r:0,g:0,b:0} : hexToRgb(magmaMap[Math.floor((i/MAX_ITER) * (magmaMap.length-1))]),

            plasma: (i) => i === MAX_ITER ? {r:0,g:0,b:0} : hexToRgb(plasmaMap[Math.floor((i/MAX_ITER) * (plasmaMap.length-1))]),

            sunset: (i) => createGradient(sunsetStops, i),

            ocean: (i) => createGradient(oceanStops, i),

            forest: (i) => createGradient(forestStops, i),

            neon: (i) => i === MAX_ITER ? {r:0,g:0,b:0} : hslToRgb(i * 15, 100, 60),

        };


        // --- Helpers ---

        function hexToRgb(hex) {

            const bigint = parseInt(hex.slice(1), 16);

            return { r: (bigint >> 16) & 255, g: (bigint >> 8) & 255, b: bigint & 255 };

        }


        function hslToRgb(h, s, l) {

            s /= 100; l /= 100;

            const k = n => (n + h / 30) % 12;

            const a = s * Math.min(l, 1 - l);

            const f = n => l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));

            return { r: 255 * f(0), g: 255 * f(8), b: 255 * f(4) };

        }


        // --- Core Logic ---


        function resize() {

            const dpr = window.devicePixelRatio || 1;

            const rect = container.getBoundingClientRect();


            canvas.width = rect.width * dpr;

            canvas.height = rect.height * dpr;


            canvas.style.width = `${rect.width}px`;

            canvas.style.height = `${rect.height}px`;


            state.width = canvas.width;

            state.height = canvas.height;


            // Reset zoom if first run

            if (state.zoom === 1.0) resetView();


            // Re-allocate buffer

            iterationBuffer = new Int32Array(state.width * state.height);


            requestRender(true);

        }


        function resetView() {

            const aspect = state.width / state.height;

            // Standard Mandelbrot view

            state.panX = -0.5;

            state.panY = 0;

            // Fit height of 2.5 units

            state.zoom = 2.5 / state.height; 


            // Adjust for specific fractals

            if (state.fractal === 'julia') { state.panX = 0; state.panY = 0; }

            if (state.fractal === 'burning-ship') { state.panX = -0.4; state.panY = -0.5; }

            if (state.fractal === 'lambda') { state.panX = 0.5; state.panY = 0; state.zoom = 3.0 / state.height; }

            if (state.fractal === 'nova') { state.panX = 0; state.panY = 0; state.zoom = 2.0 / state.height; }


            requestRender(true);

        }


        function requestRender(recalculate = false) {

            if (isCalculating) return;

            isCalculating = true;

            loadingEl.style.display = 'block';


            // Use setTimeout to allow UI to update (show loading) before heavy work

            setTimeout(() => {

                if (recalculate) calculateFractal();

                drawFractal();

                isCalculating = false;

                loadingEl.style.display = 'none';

            }, 10);

        }


        function calculateFractal() {

            const { width, height, panX, panY, zoom, fractal } = state;

            const juliaC = { x: -0.7, y: 0.27015 };


            let idx = 0;


            for (let py = 0; py < height; py++) {

                const y0 = (py - height / 2) * zoom + panY;

                for (let px = 0; px < width; px++) {

                    const x0 = (px - width / 2) * zoom + panX;


                    let iter = 0;

                    let zx = x0, zy = y0;


                    if (fractal === 'mandelbrot') {

                        let x2 = zx*zx, y2 = zy*zy;

                        while (x2 + y2 <= 4 && iter < MAX_ITER) {

                            zy = 2 * zx * zy + y0;

                            zx = x2 - y2 + x0;

                            x2 = zx*zx; y2 = zy*zy;

                            iter++;

                        }

                    } else if (fractal === 'julia') {

                        zx = x0; zy = y0;

                        let x2 = zx*zx, y2 = zy*zy;

                        while (x2 + y2 <= 4 && iter < MAX_ITER) {

                            zy = 2 * zx * zy + juliaC.y;

                            zx = x2 - y2 + juliaC.x;

                            x2 = zx*zx; y2 = zy*zy;

                            iter++;

                        }

                    } else if (fractal === 'burning-ship') {

                        let x2 = zx*zx, y2 = zy*zy;

                        while (x2 + y2 <= 4 && iter < MAX_ITER) {

                            zy = Math.abs(2 * zx * zy) + y0;

                            zx = x2 - y2 + x0;

                            x2 = zx*zx; y2 = zy*zy;

                            iter++;

                        }

                    } else if (fractal === 'newton') {

                        for (let i = 0; i < 50; i++) {

                            const zx2 = zx*zx, zy2 = zy*zy;

                            const denom = 3 * (zx2 - zy2)**2 + 12 * zx2 * zy2;

                            if (denom < 0.00001) { iter = MAX_ITER; break; }


                            const tx = zx, ty = zy;

                            zx = (2/3)*tx + (zx2 - zy2)/denom;

                            zy = (2/3)*ty - 2*tx*ty/denom;


                            if ((zx-1)**2 + zy**2 < 0.001) { iter = i; break; }

                            if ((zx+0.5)**2 + (zy-0.866)**2 < 0.001) { iter = i + 50; break; }

                            if ((zx+0.5)**2 + (zy+0.866)**2 < 0.001) { iter = i + 100; break; }

                            if (i === 49) iter = MAX_ITER;

                        }

                    } else if (fractal === 'lambda') {

                        // z = c * z * (1 - z)

                        // Here we map parameter space (Mandelbrot-like), so c = pixel, z starts at 0.5

                        let cx = x0, cy = y0;

                        zx = 0.5; zy = 0;

                        while (zx*zx + zy*zy <= 4 && iter < MAX_ITER) {

                            // z * (1-z) = z - z^2 = (x + iy) - (x^2 - y^2 + 2ixy)

                            // = (x - x^2 + y^2) + i(y - 2xy)

                            let tempX = zx - (zx*zx - zy*zy);

                            let tempY = zy - (2*zx*zy);

                            // Multiply by c (cx + icy)

                            let nextX = cx * tempX - cy * tempY;

                            let nextY = cx * tempY + cy * tempX;

                            zx = nextX; zy = nextY;

                            iter++;

                        }

                    } else if (fractal === 'manowar') {

                        // z_n+1 = z_n^2 + z_n-1 + c

                        let cx = x0, cy = y0;

                        let prevZx = 0, prevZy = 0;

                        zx = 0; zy = 0;

                        while (zx*zx + zy*zy <= 4 && iter < MAX_ITER) {

                            let x2 = zx*zx - zy*zy;

                            let y2 = 2*zx*zy;

                            let nextZx = x2 + prevZx + cx;

                            let nextZy = y2 + prevZy + cy;

                            prevZx = zx; prevZy = zy;

                            zx = nextZx; zy = nextZy;

                            iter++;

                        }

                    } else if (fractal === 'nova') {

                        // z = z - (z^3 - 1)/(3z^2) + c

                        // Start z=1, c = pixel

                        let cx = x0, cy = y0;

                        zx = 1; zy = 0;

                        for (let i = 0; i < 50; i++) {

                            // Newton step for z^3 - 1

                            const zx2 = zx*zx, zy2 = zy*zy;

                            const denom = 3 * (zx2 - zy2)**2 + 12 * zx2 * zy2;

                            if (denom < 0.00001) { iter = MAX_ITER; break; }


                            // N(z) = z - (z^3-1)/3z^2

                            // Simplified: N(z) = (2z^3 + 1) / 3z^2

                            // Let's stick to the additive form: z_new = N(z) + c


                            // Calculate (z^3 - 1) / 3z^2

                            // Numerator: z^3 - 1 = (x^3 - 3xy^2 - 1) + i(3x^2y - y^3)

                            let numX = zx*(zx2 - 3*zy2) - 1;

                            let numY = zy*(3*zx2 - zy2);

                            // Denom: 3z^2 = 3(x^2 - y^2) + i(6xy)

                            let denX = 3*(zx2 - zy2);

                            let denY = 6*zx*zy;


                            let mag = denX*denX + denY*denY;

                            let fracX = (numX*denX + numY*denY) / mag;

                            let fracY = (numY*denX - numX*denY) / mag;


                            zx = zx - fracX + cx;

                            zy = zy - fracY + cy;


                            // Check convergence to roots of z^3 - 1 (1, -0.5 +/- i*sqrt(3)/2)

                            if ((zx-1)**2 + zy**2 < 0.001) { iter = i; break; }

                            // Simplified check for other roots or divergence

                            if (zx*zx + zy*zy > 4) { iter = i; break; } // Nova can diverge

                            if (i === 49) iter = MAX_ITER;

                        }

                    } else {

                        // Generic fallbacks

                        let cx = x0, cy = y0;

                        if (fractal === 'spider') { zx = 0; zy = 0; }


                        while (zx*zx + zy*zy <= 4 && iter < MAX_ITER) {

                            let xt = 0, yt = 0;

                            if (fractal === 'tricorn') { xt = zx*zx - zy*zy + x0; yt = -2*zx*zy + y0; }

                            else if (fractal === 'celtic') { xt = Math.abs(zx*zx - zy*zy) + x0; yt = 2*zx*zy + y0; }

                            else if (fractal === 'perpendicular') { xt = zx*zx - zy*zy + x0; yt = 2*zx*Math.abs(zy) + y0; }

                            else if (fractal === 'spider') { xt = zx*zx - zy*zy + cx; yt = 2*zx*zy + cy; cx = cx/2 + xt; cy = cy/2 + yt; }

                            else if (fractal === 'multibrot3') { xt = zx*(zx*zx - 3*zy*zy) + x0; yt = zy*(3*zx*zx - zy*zy) + y0; }

                            else if (fractal === 'multibrot4') { let x2=zx*zx, y2=zy*zy; xt = x2*x2 - 6*x2*y2 + y2*y2 + x0; yt = 4*zx*zy*(x2-y2) + y0; }


                            zx = xt; zy = yt;

                            iter++;

                        }

                    }


                    iterationBuffer[idx++] = iter;

                }

            }

        }


        function drawFractal() {

            const { width, height, colorScheme } = state;

            const imgData = ctx.createImageData(width, height);

            const data = imgData.data;

            const colorFn = colorSchemes[colorScheme];


            let idx = 0;

            for (let i = 0; i < iterationBuffer.length; i++) {

                const iter = iterationBuffer[i];

                const color = colorFn(iter);

                data[idx++] = color.r;

                data[idx++] = color.g;

                data[idx++] = color.b;

                data[idx++] = 255;

            }

            ctx.putImageData(imgData, 0, 0);

        }


        // --- Interaction ---


        function handleZoom(clientX, clientY, zoomIn) {

            const rect = canvas.getBoundingClientRect();

            const dpr = window.devicePixelRatio || 1;


            // Mouse pos in CSS pixels relative to canvas

            const x = clientX - rect.left;

            const y = clientY - rect.top;


            // Convert to Canvas pixels

            const cx = x * dpr;

            const cy = y * dpr;


            // Convert to World coordinates (current center)

            const wx = (cx - state.width / 2) * state.zoom + state.panX;

            const wy = (cy - state.height / 2) * state.zoom + state.panY;


            // Apply Zoom

            const factor = zoomIn ? 0.5 : 2.0;

            state.zoom *= factor;


            // Recenter on the clicked point

            state.panX = wx;

            state.panY = wy;


            requestRender(true);

        }


        // Mouse Events

        canvas.addEventListener('mousedown', e => {

            if (e.button === 0) handleZoom(e.clientX, e.clientY, true);

            if (e.button === 2) handleZoom(e.clientX, e.clientY, false);

        });


        canvas.addEventListener('contextmenu', e => e.preventDefault());


        // Touch Events (Pinch to Zoom)

        let initialPinchDist = -1;


        canvas.addEventListener('touchstart', e => {

            if (e.touches.length === 2) {

                e.preventDefault();

                const t1 = e.touches[0];

                const t2 = e.touches[1];

                initialPinchDist = Math.hypot(t2.clientX - t1.clientX, t2.clientY - t1.clientY);

            }

        }, {passive: false});


        canvas.addEventListener('touchmove', e => {

            if (e.touches.length === 2 && initialPinchDist > 0) {

                e.preventDefault();

                const t1 = e.touches[0];

                const t2 = e.touches[1];

                const dist = Math.hypot(t2.clientX - t1.clientX, t2.clientY - t1.clientY);


                // Center of pinch

                const midX = (t1.clientX + t2.clientX) / 2;

                const midY = (t1.clientY + t2.clientY) / 2;


                // Threshold to prevent jitter

                if (Math.abs(dist - initialPinchDist) > 10) {

                    const zoomIn = dist > initialPinchDist;

                    handleZoom(midX, midY, zoomIn);

                    initialPinchDist = dist; // Reset to allow continuous stepping

                }

            }

        }, {passive: false});


        canvas.addEventListener('touchend', () => { initialPinchDist = -1; });


        // UI Events

        fractalSelect.addEventListener('change', (e) => {

            state.fractal = e.target.value;

            resetView(); // Reset view when changing fractal type for better UX

        });


        colorSelect.addEventListener('change', (e) => {

            state.colorScheme = e.target.value;

            requestRender(false); // Only redraw colors!

        });


        document.getElementById('reset-btn').addEventListener('click', resetView);


        document.getElementById('fullscreen-btn').addEventListener('click', () => {

            if (!document.fullscreenElement) document.documentElement.requestFullscreen();

            else document.exitFullscreen();

        });


        toggleBtn.addEventListener('click', () => {

            uiLayer.classList.toggle('hidden');

            toggleBtn.innerHTML = uiLayer.classList.contains('hidden') ? '<i class="fas fa-cog"></i>' : '<i class="fas fa-bars"></i>';

        });


        // --- Save / Load (Modal Fallback) ---


        function showModal(title, desc, isImage, content) {

            modalTitle.textContent = title;

            modalDesc.textContent = desc;


            if (isImage) {

                exportImg.src = content;

                exportImg.style.display = 'block';

                exportArea.style.display = 'none';

                copyBtn.style.display = 'none';

            } else {

                exportArea.value = content;

                exportImg.style.display = 'none';

                exportArea.style.display = 'block';

                copyBtn.style.display = 'block';

            }


            modalOverlay.style.display = 'flex';

        }


        modalClose.addEventListener('click', () => {

            modalOverlay.style.display = 'none';

        });


        copyBtn.addEventListener('click', () => {

            exportArea.select();

            document.execCommand('copy');

            copyBtn.textContent = 'Copied!';

            setTimeout(() => copyBtn.textContent = 'Copy to Clipboard', 2000);

        });


        document.getElementById('save-img-btn').addEventListener('click', () => {

            const dataUrl = canvas.toDataURL('image/png');

            showModal('Save Image', 'Right-click the image below and select "Save Image As..."', true, dataUrl);

        });


        document.getElementById('save-state-btn').addEventListener('click', () => {

            const data = JSON.stringify(state, null, 2);

            showModal('Save State', 'Copy the code below and save it to a .json file.', false, data);

        });


        document.getElementById('load-state-btn').addEventListener('click', () => {

            document.getElementById('file-input').click();

        });


        document.getElementById('file-input').addEventListener('change', (e) => {

            const file = e.target.files[0];

            if (!file) return;

            const reader = new FileReader();

            reader.onload = (ev) => {

                try {

                    const loaded = JSON.parse(ev.target.result);

                    // Merge loaded state

                    state.fractal = loaded.fractal;

                    state.colorScheme = loaded.colorScheme;

                    state.panX = loaded.panX;

                    state.panY = loaded.panY;

                    state.zoom = loaded.zoom;


                    // Update UI

                    fractalSelect.value = state.fractal;

                    colorSelect.value = state.colorScheme;


                    requestRender(true);

                } catch(err) {

                    alert("Invalid state file");

                }

            };

            reader.readAsText(file);

            e.target.value = '';

        });


        // Init

        window.addEventListener('resize', () => {

            // Debounce resize

            clearTimeout(window.resizeTimer);

            window.resizeTimer = setTimeout(resize, 100);

        });


        resize();


    </script>

</body>

</html>

No comments: