← Fractal Explorer results

Fractal Explorer

Qwen 3.7 Flash · typescript

Initial view of Fractal Explorer

Reviews & generation details

Reviews

No human review yet. Unreviewed does not mean unsuccessful.

Generation context

Imported historical artifact. Its exact original instructions and lineage are not known.

Generation prompt · exact text unavailable

See the current prompt for reference. It may differ from the instructions used to generate this artifact.

Inspect original source 72,473 bytes · SHA-256 437bf02d8687
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Fractal Explorer — Mandelbrot &amp; Julia</title>
<style>
/* ── css: layout ── */
/* === Layout & Canvas === */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #000; font-family: "SF Mono", "Fira Code", "Cascadia Code", monospace; }
#fractal-canvas { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; cursor: crosshair; image-rendering: pixelated; }
#overlay-canvas { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; pointer-events: none; z-index: 5; }

/* === Glassy HUD Panel === */
.glass-panel {
  position: fixed; z-index: 10;
  background: rgba(10, 12, 20, 0.72);
  backdrop-filter: blur(16px) saturate(1.4);
  -webkit-backdrop-filter: blur(16px) saturate(1.4);
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 12px;
  color: #e0e0e0;
  padding: 12px 16px;
  transition: opacity 0.3s ease;
}
.glass-panel.hidden { display: none; }

#hud-controls {
  top: 12px; right: 12px;
  min-width: 280px;
  max-width: 340px;
}

.controls-section {
  margin-bottom: 10px;
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}
.controls-section:last-child { margin-bottom: 0; }
.controls-section .label { font-size: 11px; color: #888; min-width: 60px; }

/* === Palette Chips === */
.chip-row { display: flex; gap: 6px; flex-wrap: wrap; }
.palette-chip {
  width: 28px; height: 28px;
  border-radius: 6px;
  border: 2px solid transparent;
  cursor: pointer;
  transition: transform 0.15s, border-color 0.15s;
  position: relative;
}
.palette-chip:hover { transform: scale(1.15); }
.palette-chip.active { border-color: #fff; box-shadow: 0 0 8px rgba(255,255,255,0.4); }
.palette-chip[data-name="Fire"] { background: linear-gradient(135deg, #b40, #ff8, #fff); }
.palette-chip[data-name="Ocean"] { background: linear-gradient(135deg, #018, #0cf, #aef); }
.palette-chip[data-name="Monochrome"] { background: linear-gradient(135deg, #000, #888, #fff); }
.palette-chip[data-name="Psychedelic"] { background: linear-gradient(135deg, #80f, #0ff, #0f0, #ff0, #f00); }
.palette-chip[data-name="Electric"] { background: linear-gradient(135deg, #006, #06c, #0fc, #6fa); }
.palette-chip[data-name="Sunset"] { background: linear-gradient(135deg, #108, #a08, #f50, #fc0); }

/* === Buttons === */
button {
  background: rgba(255, 255, 255, 0.08);
  border: 1px solid rgba(255, 255, 255, 0.15);
  border-radius: 6px;
  color: #ddd;
  padding: 5px 10px;
  font-family: inherit;
  font-size: 11px;
  cursor: pointer;
  transition: background 0.15s;
}
button:hover { background: rgba(255, 255, 255, 0.18); }
button:active { background: rgba(255, 255, 255, 0.25); }

/* === Sliders === */
input[type="range"] {
  flex: 1;
  min-width: 80px;
  accent-color: #6af;
  height: 4px;
}
#iter-value, #cyclic-value { font-size: 11px; color: #aaa; min-width: 36px; text-align: right; }

/* === Bookmark List === */
#bookmark-list {
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  min-width: 320px;
  max-height: 70vh;
  overflow-y: auto;
}
#bookmark-list h3 {
  font-size: 13px; margin-bottom: 8px; color: #ccc;
  border-bottom: 1px solid rgba(255,255,255,0.1);
  padding-bottom: 6px;
}
#bookmark-items { list-style: none; }
#bookmark-items li {
  display: flex; justify-content: space-between; align-items: center;
  padding: 6px 0;
  border-bottom: 1px solid rgba(255,255,255,0.06);
  font-size: 12px;
}
#bookmark-items li button { padding: 2px 8px; font-size: 10px; }
.bookmark-name { cursor: pointer; color: #6af; flex: 1; }
.bookmark-name:hover { text-decoration: underline; }
.bookmark-time { color: #666; font-size: 10px; margin: 0 8px; }

/* === Tooltip / info bar === */
#info-bar {
  position: fixed; bottom: 12px; left: 50%; transform: translateX(-50%);
  z-index: 10;
  background: rgba(10, 12, 20, 0.72);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border: 1px solid rgba(255,255,255,0.1);
  border-radius: 8px;
  padding: 6px 16px;
  font-size: 11px;
  color: #999;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.3s;
}
#info-bar.visible { opacity: 1; }
</style>
</head>
<body>
<canvas id="fractal-canvas"></canvas>
<canvas id="overlay-canvas"></canvas>
<div id="hud-controls" class="glass-panel">
  <div id="palette-selector" class="controls-section">
    <span class="label">Palette:</span>
    <div id="palette-chips" class="chip-row"></div>
  </div>
  <div class="controls-section">
    <button id="btn-back" title="Go back (Z)">← Back</button>
    <button id="btn-reset" title="Reset view (R)">Reset</button>
    <button id="btn-bookmark" title="Save/Load bookmark (B)">Bookmark</button>
    <button id="btn-julia" title="Toggle Julia mode (J)">Julia</button>
  </div>
  <div class="controls-section">
    <label for="iter-slider">Iterations: <span id="iter-value">256</span></label>
    <input type="range" id="iter-slider" min="64" max="10000" step="32" value="256">
  </div>
  <div class="controls-section">
    <label for="cyclic-slider">Color drift: <span id="cyclic-value">0</span></label>
    <input type="range" id="cyclic-slider" min="0" max="80" step="1" value="0">
  </div>
</div>
<div id="bookmark-list" class="glass-panel hidden">
  <h3>Bookmarks</h3>
  <ul id="bookmark-items"></ul>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./math":"src/math.ts","./palette":"src/palette.ts","./state":"src/state.ts","./render":"src/render.ts","./input":"src/input.ts","./ui":"src/ui.ts","./bookmarks":"src/bookmarks.ts"},"src/state.ts":{"./math":"src/math.ts"},"src/bookmarks.ts":{"./state":"src/state.ts"},"src/render.ts":{"./math":"src/math.ts","./palette":"src/palette.ts"},"src/input.ts":{"./state":"src/state.ts","./render":"src/render.ts"},"src/ui.ts":{"./state":"src/state.ts","./palette":"src/palette.ts"}};
function __require(id) {
  if (__cache[id]) return __cache[id].exports;
  var module = __cache[id] = { exports: {} };
  var factory = __mods[id];
  if (!factory) throw new Error("Module not found: " + id);
  factory(module.exports, function (spec) {
    var target = (__map[id] && __map[id][spec]) || spec;
    return __require(target);
  }, module);
  return module.exports;
}

// ── module: src/main.ts ──
__mods["src/main.ts"] = function (exports, require, module) {
"use strict";
// ============================================================
// MAIN — Application entry point, wiring all systems together
// ============================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.start = start;
const math_1 = require("./math");
const palette_1 = require("./palette");
const state_1 = require("./state");
const render_1 = require("./render");
const input_1 = require("./input");
const ui_1 = require("./ui");
const bookmarks_1 = require("./bookmarks");
// ============================================================
// GLOBALS
// ============================================================
let canvas;
let renderer;
let input;
let ui;
let appState;
let cancelToken = { version: 0 };
let selectionRect = null;
let animFrameId = 0;
let paletteChipsRendered = false;
// ============================================================
// INITIALIZATION
// ============================================================
function start() {
    canvas = document.getElementById('fractal-canvas');
    if (!canvas) {
        console.error('Missing #fractal-canvas');
        return;
    }
    // Initialize state with link params if available
    const linkParams = (0, bookmarks_1.loadLinkParams)();
    if (linkParams) {
        appState = (0, state_1.initApp)(1, 1);
        (0, state_1.setView)({
            centerRe: linkParams.re ?? state_1.DEFAULT_VIEW.centerRe,
            centerIm: linkParams.im ?? state_1.DEFAULT_VIEW.centerIm,
            scale: linkParams.scale ?? state_1.DEFAULT_VIEW.scale,
        });
        appState.maxIter = linkParams.iter ?? (0, math_1.getMaxIter)();
        if (linkParams.palette !== undefined)
            (0, palette_1.setCurrentPalette)(linkParams.palette);
        if (linkParams.offset !== undefined)
            (0, palette_1.setCyclicOffset)(linkParams.offset);
        if (linkParams.julia) {
            appState.isJuliaMode = true;
            appState.juliaCr = linkParams.jcr ?? -0.7269;
            appState.juliaCi = linkParams.jci ?? 0.1889;
            (0, math_1.setJuliaC)(appState.juliaCr, appState.juliaCi);
        }
    }
    else {
        appState = (0, state_1.initApp)(1, 1);
    }
    renderer = new render_1.FractalRenderer(canvas);
    ui = new ui_1.UIFrame(document.getElementById('overlay-canvas'));
    setupCanvasSize();
    rebuildLUT();
    setupInput();
    setupUIControls();
    renderFractal(true);
    // Start render loop for UI overlay
    function loop(ts) {
        ui.frameTick();
        ui.draw();
        animFrameId = requestAnimationFrame(loop);
    }
    animFrameId = requestAnimationFrame(loop);
    exposeDebugInterface();
    console.log('Fractal Explorer initialized');
}
/** Setup canvas sizing for high-DPI displays */
function setupCanvasSize() {
    const dpr = window.devicePixelRatio || 1;
    const vw = document.documentElement.clientWidth || window.innerWidth;
    const vh = document.documentElement.clientHeight || window.innerHeight;
    const w = Math.max(600, Math.floor(vw * dpr));
    const h = Math.max(400, Math.floor(vh * dpr));
    canvas.width = w;
    canvas.height = h;
    canvas.style.width = vw + 'px';
    canvas.style.height = vh + 'px';
    appState.pixelRatio = dpr;
    renderer.resize(w, h);
    ui.resize(w, h);
}
/** Rebuild color LUT when palette or offset changes */
function rebuildLUT() {
    renderer.rebuildLUT((0, math_1.getMaxIter)());
}
/** Compute fractal-space bounds for current view */
function getViewBounds() {
    const v = appState.view;
    const cw = canvas.width / appState.pixelRatio;
    const ch = canvas.height / appState.pixelRatio;
    const aspect = ch / cw;
    // scale controls the horizontal span: totalWidth = 4.0 / scale
    // This means scale=1 shows ~[-2.5..2.5] horizontally
    const halfW = 2.0 / v.scale;
    const halfH = halfW * aspect;
    return [v.centerRe - halfW, v.centerRe + halfW, v.centerIm - halfH, v.centerIm + halfH];
}
/** Main fractal render call */
function renderFractal(fullRes) {
    const bounds = getViewBounds();
    const maxIter = (0, math_1.getMaxIter)();
    const opts = {
        isJuliaMode: appState.isJuliaMode,
        reMin: bounds[0], reMax: bounds[1],
        imMin: bounds[2], imMax: bounds[3],
        width: canvas.width,
        height: canvas.height,
        maxIter: maxIter,
    };
    if (fullRes) {
        renderer.render(opts);
    }
    else {
        renderer.renderProgressive(opts, () => {
            ui.interact();
        });
    }
}
/** Full redraw triggered by major state changes */
function fullRedraw() {
    rebuildLUT();
    renderFractal(true);
}
// ============================================================
// INPUT SETUP
// ============================================================
function setupInput() {
    input = new input_1.InputHandler({
        canvas: canvas,
        renderer: renderer,
        onStateChange: () => {
            ui.interact();
            scheduleRender(false);
        },
    });
    input.onSelectRectBegin = (x, y) => {
        selectionRect = { x1: x, y1: y, x2: x, y2: y };
    };
    input.onSelectRectMove = (x, y) => {
        if (!selectionRect)
            return;
        selectionRect.x2 = x;
        selectionRect.y2 = y;
        drawSelectionOverlay();
    };
    input.onSelectRectEnd = (x1, y1, x2, y2) => {
        if (!selectionRect)
            return;
        selectionRect = null;
        clearOverlay();
        const sx1 = Math.min(x1, x2);
        const sy1 = Math.min(y1, y2);
        const sw = Math.abs(x2 - x1);
        const sh = Math.abs(y2 - y1);
        if (sw < 5 || sh < 5)
            return;
        const [reMin, reMax, imMin, imMax] = getViewBounds();
        const cw = canvas.width / appState.pixelRatio;
        const ch = canvas.height / appState.pixelRatio;
        const targetScale = Math.min(cw / sw, ch / sh);
        const newCenterRe = reMin + ((sx1 + sw / 2) / cw) * (reMax - reMin);
        const newCenterIm = imMin + ((sy1 + sh / 2) / ch) * (imMax - imMin);
        pushHistory();
        (0, state_1.setView)({ centerRe: newCenterRe, centerIm: newCenterIm, scale: targetScale });
        rebuildLUT();
        scheduleRender(true);
    };
    input.onJuliaPoint = (re, im) => {
        toggleJuliaMode(re, im);
    };
}
/** Push current view to history */
function pushHistory() {
    const st = appState;
    st.zoomHistory = st.zoomHistory.slice(0, st.historyIndex + 1);
    st.zoomHistory.push({ ...st.view });
    st.historyIndex = st.zoomHistory.length - 1;
}
/** Draw selection box overlay */
function drawSelectionOverlay() {
    if (!selectionRect)
        return;
    const ctx = renderer['_ctx'];
    const x = Math.min(selectionRect.x1, selectionRect.x2);
    const y = Math.min(selectionRect.y1, selectionRect.y2);
    const w = Math.abs(selectionRect.x2 - selectionRect.x1);
    const h = Math.abs(selectionRect.y2 - selectionRect.y1);
    ctx.save();
    ctx.fillStyle = 'rgba(255, 255, 255, 0.04)';
    ctx.fillRect(x, y, w, h);
    ctx.strokeStyle = 'rgba(255, 255, 255, 0.85)';
    ctx.lineWidth = 2;
    ctx.setLineDash([8, 4]);
    ctx.strokeRect(x, y, w, h);
    ctx.restore();
}
/** Clear overlay and redraw fractal */
function clearOverlay() {
    renderFractal(true);
}
/** Render fractal (synchronous for full-res, progressive otherwise) */
function scheduleRender(fullRes) {
    // For full-res, render synchronously so state changes are immediately visible
    if (fullRes) {
        renderFractal(true);
    }
    else {
        // Progressive: defer to avoid blocking UI
        requestIdleCallback ? requestIdleCallback(() => renderFractal(false)) : setTimeout(() => renderFractal(false), 16);
    }
}
// ============================================================
// JULIA MODE
// ============================================================
function toggleJuliaMode(centerRe, centerIm) {
    const st = appState;
    if (!st.isJuliaMode) {
        st.isJuliaMode = true;
        st.juliaCr = centerRe;
        st.juliaCi = centerIm;
        (0, math_1.setJuliaC)(centerRe, centerIm);
    }
    else {
        st.isJuliaMode = false;
        (0, math_1.setJuliaC)(0, 0);
    }
    pushHistory();
    rebuildLUT();
    scheduleRender(true);
}
// ============================================================
// UI CONTROLS
// ============================================================
function setupUIControls() {
    // Palette chips
    const chipRow = document.getElementById('palette-chips');
    if (!chipRow)
        return;
    const palettes = ['Fire', 'Ocean', 'Monochrome', 'Psychedelic', 'Electric', 'Sunset'];
    palettes.forEach((name, i) => {
        const chip = document.createElement('div');
        chip.className = 'palette-chip' + (i === (0, palette_1.getCurrentPaletteIndex)() ? ' active' : '');
        chip.dataset.name = name;
        chip.title = name;
        chip.addEventListener('click', () => {
            (0, palette_1.setCurrentPalette)(i);
            rebuildLUT();
            scheduleRender(false);
            updatePaletteChips();
        });
        chipRow.appendChild(chip);
    });
    paletteChipsRendered = true;
    // Back button
    document.getElementById('btn-back')?.addEventListener('click', () => {
        if ((0, state_1.goBack)()) {
            rebuildLUT();
            scheduleRender(true);
        }
    });
    // Reset button
    document.getElementById('btn-reset')?.addEventListener('click', () => {
        pushHistory();
        (0, state_1.setView)({ ...state_1.DEFAULT_VIEW });
        rebuildLUT();
        scheduleRender(true);
    });
    // Bookmark button
    document.getElementById('btn-bookmark')?.addEventListener('click', () => {
        showBookmarkMenu();
    });
    // Julia button
    document.getElementById('btn-julia')?.addEventListener('click', () => {
        toggleJuliaMode(appState.view.centerRe, appState.view.centerIm);
    });
    // Iteration slider
    const iterSlider = document.getElementById('iter-slider');
    const iterValue = document.getElementById('iter-value');
    if (iterSlider && iterValue) {
        iterSlider.value = String((0, math_1.getMaxIter)());
        iterSlider.addEventListener('input', () => {
            const v = parseInt(iterSlider.value);
            (0, math_1.setMaxIter)(v);
            iterValue.textContent = String(v);
            rebuildLUT();
            scheduleRender(true);
        });
    }
    // Cyclic drift slider
    const cyclicSlider = document.getElementById('cyclic-slider');
    const cyclicValue = document.getElementById('cyclic-value');
    if (cyclicSlider && cyclicValue) {
        cyclicSlider.value = String(Math.round((0, palette_1.getCyclicOffset)() * 80));
        cyclicSlider.addEventListener('input', () => {
            const v = parseInt(cyclicSlider.value) / 80;
            (0, palette_1.setCyclicOffset)(v);
            cyclicValue.textContent = cyclicSlider.value;
            rebuildLUT();
            scheduleRender(false);
        });
    }
    // Global keyboard shortcuts
    window.addEventListener('keydown', (e) => {
        const key = e.key.toLowerCase();
        switch (key) {
            case 'r':
                resetView();
                break;
            case 'p':
                cyclePalette();
                break;
            case '[':
                prevPalette();
                break;
            case ']':
                nextPalette();
                break;
            case 'backspace':
                e.preventDefault();
                if (!(0, state_1.goBack)()) { /* nothing */ }
                else {
                    rebuildLUT();
                    scheduleRender(true);
                }
                break;
        }
    });
    // Resize handler
    window.addEventListener('resize', () => {
        setupCanvasSize();
        rebuildLUT();
        scheduleRender(true);
    });
}
function updatePaletteChips() {
    if (!paletteChipsRendered)
        return;
    const chips = document.querySelectorAll('.palette-chip');
    chips.forEach((chip, i) => {
        chip.classList.toggle('active', i === (0, palette_1.getCurrentPaletteIndex)());
    });
}
function resetView() {
    pushHistory();
    (0, state_1.setView)({ ...state_1.DEFAULT_VIEW });
    rebuildLUT();
    scheduleRender(true);
}
function cyclePalette() {
    const idx = ((0, palette_1.getCurrentPaletteIndex)() + 1) % 6;
    (0, palette_1.setCurrentPalette)(idx);
    rebuildLUT();
    scheduleRender(false);
    updatePaletteChips();
}
function prevPalette() {
    const idx = ((0, palette_1.getCurrentPaletteIndex)() - 1 + 6) % 6;
    (0, palette_1.setCurrentPalette)(idx);
    rebuildLUT();
    scheduleRender(false);
    updatePaletteChips();
}
function nextPalette() {
    const idx = ((0, palette_1.getCurrentPaletteIndex)() + 1) % 6;
    (0, palette_1.setCurrentPalette)(idx);
    rebuildLUT();
    scheduleRender(false);
    updatePaletteChips();
}
function showBookmarkMenu() {
    const bookmarks = (0, bookmarks_1.loadBookmarks)();
    let listEl = document.getElementById('bookmark-list');
    if (!listEl) {
        listEl = document.createElement('div');
        listEl.id = 'bookmark-list';
        listEl.className = 'glass-panel hidden';
        document.body.appendChild(listEl);
    }
    listEl.classList.remove('hidden');
    listEl.innerHTML = '<h3>Bookmarks</h3><ul id="bookmark-items"></ul>';
    const items = document.getElementById('bookmark-items');
    if (!items)
        return;
    if (bookmarks.length === 0) {
        items.innerHTML = '<li style="color:#666">No bookmarks yet</li>';
    }
    else {
        bookmarks.forEach((b, i) => {
            const li = document.createElement('li');
            const nameSpan = document.createElement('span');
            nameSpan.className = 'bookmark-name';
            nameSpan.textContent = b.name;
            nameSpan.title = `Restore "${b.name}"`;
            nameSpan.addEventListener('click', () => {
                const restored = (0, bookmarks_1.restoreBookmark)(b);
                Object.assign(appState, restored);
                (0, math_1.setJuliaC)(appState.juliaCr, appState.juliaCi);
                (0, palette_1.setCurrentPalette)(appState.paletteIndex);
                (0, palette_1.setCyclicOffset)(appState.cyclicOffset);
                rebuildLUT();
                scheduleRender(true);
                listEl.classList.add('hidden');
            });
            const timeSpan = document.createElement('span');
            timeSpan.className = 'bookmark-time';
            timeSpan.textContent = new Date(b.timestamp).toLocaleDateString();
            const delBtn = document.createElement('button');
            delBtn.textContent = '✕';
            delBtn.addEventListener('click', (e) => {
                e.stopPropagation();
                localStorage.removeItem('fractal_bookmarks');
                const updated = bookmarks.filter((_, j) => j !== i);
                try {
                    localStorage.setItem('fractal_bookmarks', JSON.stringify(updated));
                }
                catch { }
                listEl.classList.add('hidden');
                setTimeout(() => showBookmarkMenu(), 100);
            });
            li.appendChild(nameSpan);
            li.appendChild(timeSpan);
            li.appendChild(delBtn);
            items.appendChild(li);
        });
    }
    // Add "Save" option
    const saveLi = document.createElement('li');
    const saveBtn = document.createElement('button');
    saveBtn.textContent = '+ Save Current View';
    saveBtn.style.flex = '1';
    saveBtn.addEventListener('click', () => {
        const name = prompt('Bookmark name:', `View ${new Date().toLocaleTimeString()}`);
        if (name) {
            (0, bookmarks_1.saveBookmark)(appState, name);
            listEl.classList.add('hidden');
            showInfo(`Saved: "${name}"`);
        }
    });
    saveLi.appendChild(saveBtn);
    items.appendChild(saveLi);
    // Close on click outside
    setTimeout(() => {
        const closer = (e) => {
            if (!listEl.contains(e.target)) {
                listEl.classList.add('hidden');
                document.removeEventListener('click', closer);
            }
        };
        document.addEventListener('click', closer);
    }, 10);
}
function showInfo(msg) {
    let bar = document.getElementById('info-bar');
    if (!bar) {
        bar = document.createElement('div');
        bar.id = 'info-bar';
        document.body.appendChild(bar);
    }
    bar.textContent = msg;
    bar.classList.add('visible');
    setTimeout(() => bar.classList.remove('visible'), 2500);
}
// ============================================================
// DEBUG INTERFACE
// ============================================================
function exposeDebugInterface() {
    window.fractal = {
        iterationsAt(re, im) {
            let zr = 0, zi = 0;
            let zr2 = 0, zi2 = 0;
            let i = 0;
            const mi = (0, math_1.getMaxIter)();
            while (i < mi && zr2 + zi2 <= 256) {
                zi = 2 * zr * zi + im;
                zr = zr2 - zi2 + re;
                zr2 = zr * zr;
                zi2 = zi * zi;
                i++;
            }
            return i;
        },
        setView(params) {
            const v = appState.view;
            (0, state_1.setView)({
                centerRe: params.re ?? v.centerRe,
                centerIm: params.im ?? v.centerIm,
                scale: params.scale ?? v.scale,
            });
            rebuildLUT();
            scheduleRender(true);
        },
        getState() {
            return {
                view: { ...appState.view },
                maxIter: appState.maxIter,
                isJulia: appState.isJuliaMode,
                magnification: (0, state_1.getMagnification)(),
                palette: (0, palette_1.getCurrentPalette)().name,
            };
        },
        goto(name) {
            const locations = {
                'seahorse': { re: -0.7436438870371587, im: 0.13182590420531197 },
                'spiral': { re: -0.16, im: 1.0405 },
                'elephant': { re: 0.28, im: 0.008 },
                'lightning': { re: 0.275, im: 0.0 },
                'scepter': { re: -1.25066, im: 0.02012 },
                'double-spiral': { re: 0.3, im: 0.5 },
                'main-cardioid': { re: -0.12, im: 0.74 },
            };
            const loc = locations[name];
            if (loc) {
                (0, state_1.setView)({ centerRe: loc.re, centerIm: loc.im, scale: appState.view.scale });
                scheduleRender(true);
            }
        },
        test() {
            const results = [];
            const mi = (0, math_1.getMaxIter)();
            // c = 0 should be interior (hits max iter)
            results.push(`mandelbrot(0, 0) = ${this.iterationsAt(0, 0)} (expect ${mi})`);
            // c = -1 should be interior
            results.push(`mandelbrot(-1, 0) = ${this.iterationsAt(-1, 0)} (expect ${mi})`);
            // c = 2 should escape quickly
            results.push(`mandelbrot(2, 0) = ${this.iterationsAt(2, 0)} (expect ≤3)`);
            // c = 0.25 (cusp boundary) should have high but finite
            results.push(`mandelbrot(0.25, 0) = ${this.iterationsAt(0.25, 0)} (expect high but <${mi})`);
            console.log('Fractal correctness tests:');
            results.forEach(r => console.log('  ' + r));
        },
    };
    console.log('Debug interface exposed: window.fractal');
}
// ============================================================
// BOOT — Call start() when module loads
// ============================================================
if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', start);
}
else {
    start();
}
};

// ── module: src/math.ts ──
__mods["src/math.ts"] = function (exports, require, module) {
"use strict";
// ============================================================
// FRACTAL MATH — Mandelbrot & Julia set iteration
// Allocation-free core loops; smooth (continuous) coloring
// ============================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.TUNABLE = void 0;
exports.computeMaxIter = computeMaxIter;
exports.getMaxIter = getMaxIter;
exports.setMaxIter = setMaxIter;
exports.setJuliaC = setJuliaC;
exports.isJuliaMode = isJuliaMode;
exports.mandelbrotAt = mandelbrotAt;
exports.mandelbrot = mandelbrot;
exports.juliaAt = juliaAt;
exports.smoothIter = smoothIter;
exports.batchIterateMandelbrot = batchIterateMandelbrot;
exports.batchIterateWithSmooth = batchIterateWithSmooth;
exports.batchIterateJuliaWithSmooth = batchIterateJuliaWithSmooth;
/** Tunable constants */
exports.TUNABLE = Object.freeze({
    /** Base iteration limit at default zoom (magnification ~1–2) */
    baseIterations: 256,
    /** Iterations scale linearly with log2(magnification) beyond base */
    iterScalePerLogMag: 48,
    /** Maximum iteration cap to prevent hanging */
    maxIterationCap: 10000,
    /** Bail-out radius squared */
    bailoutRadius: 256,
    /** Smooth coloring constant for log2 trick */
    smoothingEnabled: true,
});
let _maxIter = exports.TUNABLE.baseIterations;
/** Compute the adaptive max iterations from magnification */
function computeMaxIter(magnification) {
    if (magnification <= 1)
        return exports.TUNABLE.baseIterations;
    const logMag = Math.log2(magnification);
    const val = exports.TUNABLE.baseIterations + exports.TUNABLE.iterScalePerLogMag * logMag;
    return Math.min(Math.ceil(val), exports.TUNABLE.maxIterationCap);
}
function getMaxIter() { return _maxIter; }
function setMaxIter(v) { _maxIter = v; }
/** Escape-time computation for a single point using double precision.
 *  Returns iteration count and whether it's interior.
 *  The caller passes z0 = re, im (Julia c is pre-set via juliaC). */
const JULIA_C = { re: 0, im: 0 };
function setJuliaC(re, im) {
    JULIA_C.re = re;
    JULIA_C.im = im;
}
function isJuliaMode() {
    return JULIA_C.re !== 0 || JULIA_C.im !== 0;
}
/**
 * Core Mandelbrot/Julia iteration — allocation free.
 * Uses the standard z = z^2 + c recurrence.
 * For Mandelbrot: c = point, z0 = 0
 * For Julia: c = fixed, z0 = point
 */
function mandelbrotAt(re, im, cr, ci, zr, zi, maxIter) {
    let zr2 = zr * zr;
    let zi2 = zi * zi;
    let i = 0;
    while (i < maxIter && zr2 + zi2 <= exports.TUNABLE.bailoutRadius) {
        zi = 2.0 * zr * zi + ci;
        zr = zr2 - zi2 + cr;
        zr2 = zr * zr;
        zi2 = zi * zi;
        i++;
    }
    if (i >= maxIter) {
        // Interior check: see if |z| is still bounded after one more step
        const nextZr2 = (zr * zr - zi * zi + cr);
        const nextZi = 2.0 * zr * zi + ci;
        const nextR = zr * zr - zi * zi + cr;
        const nextI = 2.0 * zr * zi + ci;
        // If it's clearly diverging, it's not truly interior
        // But by convention we call it interior if we hit maxIter
        return { iter: maxIter, isInterior: true };
    }
    return { iter: i, isInterior: false };
}
/**
 * Full Mandelbrot iteration for a point on the complex plane.
 * cr, ci are the point's coordinates. z0 = 0.
 */
function mandelbrot(cr, ci, maxIter) {
    return mandelbrotAt(cr, ci, cr, ci, 0.0, 0.0, maxIter);
}
/**
 * Julia set iteration for a point (re, im) given fixed c = (juliaCr, juliaCi).
 * z0 = point.
 */
function juliaAt(re, im, maxIter) {
    return mandelbrotAt(re, im, JULIA_C.re, JULIA_C.im, re, im, maxIter);
}
/**
 * Compute smooth (continuous) iteration count.
 * n = raw iteration count
 * zr, zi = final z values before bail-out
 * Returns fractional iteration or maxIter if interior.
 */
function smoothIter(n, zr, zi, maxIter) {
    if (n >= maxIter)
        return maxIter;
    const log_zn = Math.log(zr * zr + zi * zi) / 2.0; // ln|z|
    const nu = Math.log(log_zn / Math.LN2) / Math.LN2; // log2(log|z|)
    return n + 1.0 - nu;
}
/**
 * Batch iterate a rectangular region of the fractal.
 * Populates `iterBuffer` with raw integer iteration counts.
 * Also fills `smoothBuffer` if provided (for smooth coloring).
 * Fills `isInterior` array for mask rendering.
 *
 * Parameters are in fractal-space coordinates.
 * stride = width of the buffer.
 */
function batchIterateMandelbrot(reMin, reMax, imMin, imMax, w, h, iterBuffer, smoothBuffer, isInterior, maxIter) {
    const dRe = reMax - reMin;
    const dIm = imMax - imMin;
    for (let py = 0; py < h; py++) {
        const ci = imMin + (py / h) * dIm;
        const rowOff = py * w;
        for (let px = 0; px < w; px++) {
            const cr = reMin + (px / w) * dRe;
            const off = rowOff + px;
            const result = mandelbrot(cr, ci, maxIter);
            iterBuffer[off] = result.iter;
            if (isInterior) {
                isInterior[off] = result.isInterior ? 1 : 0;
            }
            if (smoothBuffer && !result.isInterior) {
                // We need zr,zi for smooth coloring — recompute briefly
                // Actually, let's refactor: the batch loop should track zr,zi
                // For now, store 0 (will be filled by a smarter version below)
                smoothBuffer[off] = result.iter;
            }
        }
    }
}
/**
 * Optimized batch iterate that tracks zr,zi for smooth coloring.
 * This is the main rendering loop — allocation free.
 */
function batchIterateWithSmooth(reMin, reMax, imMin, imMax, w, h, iterBuffer, smoothBuffer, isInterior, maxIter) {
    const dRe = reMax - reMin;
    const dIm = imMax - imMin;
    for (let py = 0; py < h; py++) {
        const ci = imMin + (py / h) * dIm;
        const rowOff = py * w;
        for (let px = 0; px < w; px++) {
            const cr = reMin + (px / w) * dRe;
            const off = rowOff + px;
            // Mandelbrot: z0 = 0, c = (cr, ci)
            let zr = 0.0, zi = 0.0;
            let zr2 = 0.0, zi2 = 0.0;
            let i = 0;
            while (i < maxIter && zr2 + zi2 <= exports.TUNABLE.bailoutRadius) {
                zi = 2.0 * zr * zi + ci;
                zr = zr2 - zi2 + cr;
                zr2 = zr * zr;
                zi2 = zi * zi;
                i++;
            }
            iterBuffer[off] = i;
            if (isInterior) {
                isInterior[off] = i >= maxIter ? 1 : 0;
            }
            if (smoothBuffer && i < maxIter) {
                const log_zn = Math.log(zr2 + zi2) / 2.0;
                const nu = Math.log(log_zn / Math.LN2) / Math.LN2;
                smoothBuffer[off] = i + 1.0 - nu;
            }
        }
    }
}
/**
 * Julia set batch iterate with smooth coloring.
 * Fixed c = JULIA_C, z0 = pixel position.
 */
function batchIterateJuliaWithSmooth(reMin, reMax, imMin, imMax, w, h, iterBuffer, smoothBuffer, isInterior, maxIter) {
    const dRe = reMax - reMin;
    const dIm = imMax - imMin;
    const cj = JULIA_C.re;
    const cjj = JULIA_C.im;
    for (let py = 0; py < h; py++) {
        const ci = imMin + (py / h) * dIm;
        const rowOff = py * w;
        for (let px = 0; px < w; px++) {
            const cr = reMin + (px / w) * dRe;
            const off = rowOff + px;
            // Julia: z0 = (cr, ci), c = (cj, cjj)
            let zr = cr, zi = ci;
            let zr2 = zr * zr, zi2 = zi * zi;
            let i = 0;
            while (i < maxIter && zr2 + zi2 <= exports.TUNABLE.bailoutRadius) {
                zi = 2.0 * zr * zi + cjj;
                zr = zr2 - zi2 + cj;
                zr2 = zr * zr;
                zi2 = zi * zi;
                i++;
            }
            iterBuffer[off] = i;
            if (isInterior) {
                isInterior[off] = i >= maxIter ? 1 : 0;
            }
            if (smoothBuffer && i < maxIter) {
                const log_zn = Math.log(zr2 + zi2) / 2.0;
                const nu = Math.log(log_zn / Math.LN2) / Math.LN2;
                smoothBuffer[off] = i + 1.0 - nu;
            }
        }
    }
}
};

// ── module: src/palette.ts ──
__mods["src/palette.ts"] = function (exports, require, module) {
"use strict";
// ============================================================
// PALETTES — Color definitions and smooth interpolation
// ============================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.getPalettes = getPalettes;
exports.getCurrentPalette = getCurrentPalette;
exports.getCurrentPaletteIndex = getCurrentPaletteIndex;
exports.setCurrentPalette = setCurrentPalette;
exports.getCyclicOffset = getCyclicOffset;
exports.setCyclicOffset = setCyclicOffset;
exports.samplePalette = samplePalette;
exports.buildColorLUT = buildColorLUT;
/** Predefined palettes */
const PALETTES = [
    {
        name: 'Fire',
        stops: [
            { pos: 0.0, r: 0, g: 0, b: 4 },
            { pos: 0.15, r: 80, g: 0, b: 30 },
            { pos: 0.3, r: 180, g: 40, b: 0 },
            { pos: 0.5, r: 255, g: 120, b: 0 },
            { pos: 0.7, r: 255, g: 220, b: 60 },
            { pos: 0.85, r: 255, g: 255, b: 180 },
            { pos: 1.0, r: 255, g: 255, b: 255 },
        ],
    },
    {
        name: 'Ocean',
        stops: [
            { pos: 0.0, r: 0, g: 5, b: 20 },
            { pos: 0.2, r: 0, g: 30, b: 80 },
            { pos: 0.4, r: 0, g: 80, b: 140 },
            { pos: 0.6, r: 20, g: 140, b: 180 },
            { pos: 0.8, r: 80, g: 200, b: 220 },
            { pos: 1.0, r: 180, g: 240, b: 255 },
        ],
    },
    {
        name: 'Monochrome',
        stops: [
            { pos: 0.0, r: 0, g: 0, b: 0 },
            { pos: 0.5, r: 80, g: 80, b: 80 },
            { pos: 1.0, r: 255, g: 255, b: 255 },
        ],
    },
    {
        name: 'Psychedelic',
        stops: [
            { pos: 0.0, r: 128, g: 0, b: 255 },
            { pos: 0.167, r: 0, g: 0, b: 255 },
            { pos: 0.333, r: 0, g: 255, b: 255 },
            { pos: 0.5, r: 0, g: 255, b: 0 },
            { pos: 0.667, r: 255, g: 255, b: 0 },
            { pos: 0.833, r: 255, g: 0, b: 0 },
            { pos: 1.0, r: 128, g: 0, b: 255 },
        ],
    },
    {
        name: 'Electric',
        stops: [
            { pos: 0.0, r: 10, g: 0, b: 30 },
            { pos: 0.2, r: 60, g: 0, b: 120 },
            { pos: 0.4, r: 0, g: 100, b: 200 },
            { pos: 0.6, r: 0, g: 200, b: 180 },
            { pos: 0.8, r: 100, g: 255, b: 100 },
            { pos: 1.0, r: 200, g: 255, b: 200 },
        ],
    },
    {
        name: 'Sunset',
        stops: [
            { pos: 0.0, r: 20, g: 0, b: 40 },
            { pos: 0.2, r: 80, g: 10, b: 80 },
            { pos: 0.4, r: 160, g: 30, b: 60 },
            { pos: 0.6, r: 240, g: 80, b: 20 },
            { pos: 0.8, r: 255, g: 180, b: 60 },
            { pos: 1.0, r: 255, g: 255, b: 200 },
        ],
    },
];
let _currentPalette = 0;
let _cyclicOffset = 0;
function getPalettes() { return PALETTES; }
function getCurrentPalette() { return PALETTES[_currentPalette]; }
function getCurrentPaletteIndex() { return _currentPalette; }
function setCurrentPalette(idx) { _currentPalette = idx; }
function getCyclicOffset() { return _cyclicOffset; }
function setCyclicOffset(v) { _cyclicOffset = v % 1.0; }
/**
 * Interpolate within a palette to get an RGB color for a fractional value t (0..1).
 * The palette wraps: t > 1 cycles through.
 */
function samplePalette(palette, t) {
    const n = palette.stops.length;
    if (n < 2)
        return [0, 0, 0];
    // Wrap t into [0, 1)
    t = ((t % 1.0) + 1.0) % 1.0;
    // Find the two stops to interpolate between
    let i = 0;
    while (i < n - 1 && palette.stops[i + 1].pos < t)
        i++;
    if (i >= n - 1) {
        const s = palette.stops[n - 1];
        return [s.r, s.g, s.b];
    }
    const s0 = palette.stops[i];
    const s1 = palette.stops[i + 1];
    const range = s1.pos - s0.pos;
    const frac = range > 0 ? (t - s0.pos) / range : 0;
    // Smooth ease-in-out for extra polish
    const eased = frac * frac * (3 - 2 * frac);
    return [
        Math.round(s0.r + (s1.r - s0.r) * eased),
        Math.round(s0.g + (s1.g - s0.g) * eased),
        Math.round(s0.b + (s1.b - s0.b) * eased),
    ];
}
/**
 * Build a full-color lookup table (LUT) for a given palette and offset.
 * LUT maps iteration count → packed RGBA uint32.
 * size = maxIter + some headroom.
 */
function buildColorLUT(palette, cyclicOffset, size) {
    const lut = new Uint32Array(size);
    const scale = size / 80; // map iterations 0..maxIter into ~80 palette cycles
    for (let i = 0; i < size; i++) {
        const t = ((i * scale + cyclicOffset) % 1.0 + 1.0) % 1.0;
        const [r, g, b] = samplePalette(palette, t);
        // Pack as ABGR for little-endian (standard on x86/ARM)
        lut[i] = (255 << 24) | (b << 16) | (g << 8) | r;
    }
    return lut;
}
};

// ── module: src/state.ts ──
__mods["src/state.ts"] = function (exports, require, module) {
"use strict";
// ============================================================
// STATE — Application configuration and view parameters
// ============================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.DEFAULT_VIEW = void 0;
exports.createInitialState = createInitialState;
exports.getState = getState;
exports.setState = setState;
exports.initApp = initApp;
exports.setView = setView;
exports.zoomAt = zoomAt;
exports.panBy = panBy;
exports.goBack = goBack;
exports.getMagnification = getMagnification;
const math_1 = require("./math");
exports.DEFAULT_VIEW = {
    centerRe: -0.5,
    centerIm: 0.0,
    scale: 1.0, // default: shows roughly [-1.5..1.5] × [-1.0..1.0] on most screens
};
const MAX_MAGNIFICATION = 1e14; // Hard cap for double precision
let state;
function createInitialState(canvasW, canvasH) {
    return {
        view: { ...exports.DEFAULT_VIEW },
        targetView: null,
        maxIter: (0, math_1.computeMaxIter)(1),
        paletteIndex: 0,
        cyclicOffset: 0,
        isJuliaMode: false,
        juliaCr: -0.7269,
        juliaCi: 0.1889,
        bookmarks: [],
        zoomHistory: [{ ...exports.DEFAULT_VIEW }],
        historyIndex: 0,
        hudVisible: true,
        hudTimer: 0,
        pixelRatio: window.devicePixelRatio || 1,
    };
}
function getState() { return state; }
function setState(s) { state = s; }
/** Initialize global state */
function initApp(canvasW, canvasH) {
    state = createInitialState(canvasW, canvasH);
    return state;
}
/** Set the view directly (no animation) */
function setView(view) {
    state.view = { ...view };
    state.maxIter = (0, math_1.computeMaxIter)(state.view.scale / exports.DEFAULT_VIEW.scale);
    (0, math_1.setMaxIter)(state.maxIter);
}
/** Zoom toward a focal point in fractal coordinates */
function zoomAt(fracRe, fracIm, factor) {
    const v = state.view;
    const newScale = v.scale * factor;
    if (newScale / exports.DEFAULT_VIEW.scale > MAX_MAGNIFICATION) {
        // Warn about precision limits
        console.warn(`Fractal Explorer: approaching precision limit at ×${Math.round(newScale / exports.DEFAULT_VIEW.scale).toExponential(2)}`);
        return;
    }
    // The focal point stays fixed while the view scales
    const fRe = v.centerRe + (fracRe - 0.5) / v.scale;
    const fIm = v.centerIm + (fracIm - 0.5) / v.scale;
    const newCenterRe = fRe - (fracRe - 0.5) / newScale;
    const newCenterIm = fIm - (fracIm - 0.5) / newScale;
    pushZoomHistory();
    state.view = { centerRe: newCenterRe, centerIm: newCenterIm, scale: newScale };
    state.maxIter = (0, math_1.computeMaxIter)(newScale / exports.DEFAULT_VIEW.scale);
}
/** Pan by dx, dy in fractal units */
function panBy(dx, dy) {
    pushZoomHistory();
    state.view.centerRe -= dx;
    state.view.centerIm -= dy;
}
/** Push current view to zoom history stack */
function pushZoomHistory() {
    // Remove any forward history if we're not at the end
    state.zoomHistory = state.zoomHistory.slice(0, state.historyIndex + 1);
    state.zoomHistory.push({ ...state.view });
    state.historyIndex = state.zoomHistory.length - 1;
}
/** Go back one step in zoom history */
function goBack() {
    if (state.historyIndex <= 0)
        return false;
    state.historyIndex--;
    state.view = { ...state.zoomHistory[state.historyIndex] };
    state.maxIter = (0, math_1.computeMaxIter)(state.view.scale / exports.DEFAULT_VIEW.scale);
    return true;
}
/** Get magnification relative to default view */
function getMagnification() {
    return state.view.scale / exports.DEFAULT_VIEW.scale;
}
};

// ── module: src/bookmarks.ts ──
__mods["src/bookmarks.ts"] = function (exports, require, module) {
"use strict";
// ============================================================
// BOOKMARKS — Save/load/view management via localStorage
// ============================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.saveBookmark = saveBookmark;
exports.loadBookmarks = loadBookmarks;
exports.deleteBookmark = deleteBookmark;
exports.restoreBookmark = restoreBookmark;
exports.saveLastView = saveLastView;
exports.loadLinkParams = loadLinkParams;
const STORAGE_KEY = 'fractal_bookmarks';
const LAST_VIEW_KEY = 'fractal_last_view';
function saveBookmark(state, name) {
    const bookmark = {
        id: crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(36).slice(2)}`,
        name,
        timestamp: Date.now(),
        params: { ...state.view },
        maxIter: state.maxIter,
        paletteIndex: state.paletteIndex,
        cyclicOffset: state.cyclicOffset,
        isJulia: state.isJuliaMode,
        juliaCr: state.juliaCr,
        juliaCi: state.juliaCi,
    };
    const bookmarks = loadBookmarks();
    bookmarks.push(bookmark);
    try {
        localStorage.setItem(STORAGE_KEY, JSON.stringify(bookmarks));
    }
    catch (e) {
        console.warn('Failed to save bookmark:', e);
    }
    return bookmark;
}
function loadBookmarks() {
    try {
        const data = localStorage.getItem(STORAGE_KEY);
        return data ? JSON.parse(data) : [];
    }
    catch {
        return [];
    }
}
function deleteBookmark(id) {
    const bookmarks = loadBookmarks().filter(b => b.id !== id);
    try {
        localStorage.setItem(STORAGE_KEY, JSON.stringify(bookmarks));
    }
    catch { /* ignore */ }
}
function restoreBookmark(bookmark) {
    return {
        view: { ...bookmark.params },
        targetView: null,
        maxIter: bookmark.maxIter,
        paletteIndex: bookmark.paletteIndex,
        cyclicOffset: bookmark.cyclicOffset,
        isJuliaMode: bookmark.isJulia,
        juliaCr: bookmark.juliaCr,
        juliaCi: bookmark.juliaCi,
        bookmarks: loadBookmarks(),
        zoomHistory: [{ ...bookmark.params }],
        historyIndex: 0,
        hudVisible: true,
        hudTimer: 0,
        pixelRatio: window.devicePixelRatio || 1,
    };
}
/** Save current view as a temporary "last viewed" for URL sharing */
function saveLastView(state) {
    try {
        const data = {
            re: state.view.centerRe,
            im: state.view.centerIm,
            scale: state.view.scale,
            iter: state.maxIter,
            palette: state.paletteIndex,
            offset: state.cyclicOffset,
            julia: state.isJuliaMode,
            jcr: state.juliaCr,
            jci: state.juliaCi,
        };
        localStorage.setItem(LAST_VIEW_KEY, JSON.stringify(data));
    }
    catch { /* ignore */ }
}
function loadLinkParams() {
    // Check URL hash first
    const hash = window.location.hash.slice(1);
    if (hash) {
        try {
            const params = JSON.parse(decodeURIComponent(hash));
            return params;
        }
        catch { /* invalid hash */ }
    }
    // Fall back to localStorage
    try {
        const data = localStorage.getItem(LAST_VIEW_KEY);
        if (data)
            return JSON.parse(data);
    }
    catch { /* ignore */ }
    return null;
}
};

// ── module: src/render.ts ──
__mods["src/render.ts"] = function (exports, require, module) {
"use strict";
// ============================================================
// RENDER — Pixel-level fractal rendering pipeline
// Uses ImageData + typed arrays for fast pixel writes
// ============================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.FractalRenderer = void 0;
const palette_1 = require("./palette");
/**
 * Renders a fractal directly into the canvas.
 * Blocking call — computes iterations, colorizes, puts image data.
 */
class FractalRenderer {
    constructor(canvas) {
        this._colorLUT = new Uint32Array(0);
        this._lutSize = 0;
        this._w = 0;
        this._h = 0;
        this._canvas = canvas;
        this._ctx = canvas.getContext('2d', { willReadFrequently: false });
    }
    /** Resize internal buffers */
    resize(w, h) {
        this._w = w;
        this._h = h;
        this._canvas.width = w;
        this._canvas.height = h;
        this._imageData = this._ctx.createImageData(w, h);
        this._buf32 = new Uint32Array(this._imageData.data.buffer);
        this._iterBuffer = new Uint32Array(w * h);
        this._smoothBuffer = new Float64Array(w * h);
        this._isInterior = new Uint8Array(w * h);
    }
    /** Rebuild the color lookup table */
    rebuildLUT(maxIter) {
        const palette = (0, palette_1.getCurrentPalette)();
        const offset = (0, palette_1.getCyclicOffset)();
        // Smooth iterations can go slightly above maxIter, add headroom
        const size = Math.max(maxIter + 200, 512);
        this._colorLUT = (0, palette_1.buildColorLUT)(palette, offset, size);
        this._lutSize = size;
    }
    /**
     * Full-resolution synchronous render.
     * Blocks the thread for the duration of computation.
     */
    render(opts) {
        const w = opts.width;
        const h = opts.height;
        if (w !== this._w || h !== this._h) {
            this.resize(w, h);
        }
        const buf32 = this._buf32;
        const lut = this._colorLUT;
        const lutSize = this._lutSize;
        // Clear to black
        buf32.fill(0xFF000000);
        if (opts.isJuliaMode) {
            this._renderJulia(opts, buf32, lut, lutSize);
        }
        else {
            this._renderMandelbrot(opts, buf32, lut, lutSize);
        }
        this._ctx.putImageData(this._imageData, 0, 0);
    }
    /**
     * Progressive render: renders at reduced resolution first,
     * then refines. Each step calls onComplete before proceeding.
     */
    renderProgressive(opts, onComplete) {
        const passes = [0.25, 0.5, 0.75, 1.0];
        let idx = 0;
        const doPass = () => {
            if (idx >= passes.length) {
                onComplete?.();
                return;
            }
            const factor = passes[idx];
            const pw = Math.max(1, Math.ceil(opts.width * factor));
            const ph = Math.max(1, Math.ceil(opts.height * factor));
            // Render at reduced resolution
            this._renderAtResolution(opts, pw, ph, factor);
            idx++;
            setTimeout(doPass, 0);
        };
        doPass();
    }
    /** Draw a selection rectangle overlay on the canvas */
    drawSelectionRect(x, y, w, h) {
        const ctx = this._ctx;
        ctx.save();
        ctx.fillStyle = 'rgba(255,255,255,0.04)';
        ctx.fillRect(x, y, w, h);
        ctx.strokeStyle = 'rgba(255,255,255,0.85)';
        ctx.lineWidth = 2;
        ctx.setLineDash([8, 4]);
        ctx.strokeRect(x, y, w, h);
        ctx.restore();
    }
    /** Clear canvas to solid black */
    clear() {
        const buf32 = this._buf32;
        buf32.fill(0xFF000000);
        this._ctx.putImageData(this._imageData, 0, 0);
    }
    // ---- Internal render methods ----
    _renderMandelbrot(opts, buf32, lut, lutSize) {
        const { reMin, reMax, imMin, imMax, maxIter, width: w, height: h } = opts;
        const dRe = reMax - reMin;
        const dIm = imMax - imMin;
        for (let py = 0; py < h; py++) {
            const ci = imMin + (py / h) * dIm;
            const rowOff = py * w;
            for (let px = 0; px < w; px++) {
                const cr = reMin + (px / w) * dRe;
                const off = rowOff + px;
                // z = z^2 + c, z0 = 0
                let zr = 0, zi = 0;
                let zr2 = 0, zi2 = 0;
                let i = 0;
                while (i < maxIter && zr2 + zi2 <= 256) {
                    zi = 2 * zr * zi + ci;
                    zr = zr2 - zi2 + cr;
                    zr2 = zr * zr;
                    zi2 = zi * zi;
                    i++;
                }
                if (i >= maxIter) {
                    buf32[off] = 0xFF000000; // Interior: black
                }
                else {
                    // Smooth coloring
                    const log_zn = Math.log(zr2 + zi2) / 2.0;
                    const nu = Math.log(log_zn / 0.6931471805599453) / 0.6931471805599453;
                    const smoothVal = i + 1.0 - nu;
                    const lutIdx = Math.min(Math.floor(smoothVal), lutSize - 1);
                    buf32[off] = lut[lutIdx];
                }
            }
        }
    }
    _renderJulia(opts, buf32, lut, lutSize) {
        // Julia c is set globally via setJuliaC()
        const cj = globalThis.__juliaCr ?? -0.7269;
        const cjj = globalThis.__juliaCi ?? 0.1889;
        const { reMin, reMax, imMin, imMax, maxIter, width: w, height: h } = opts;
        const dRe = reMax - reMin;
        const dIm = imMax - imMin;
        for (let py = 0; py < h; py++) {
            const ci = imMin + (py / h) * dIm;
            const rowOff = py * w;
            for (let px = 0; px < w; px++) {
                const cr = reMin + (px / w) * dRe;
                const off = rowOff + px;
                // Julia: z0 = point, c = fixed
                let zr = cr, zi = ci;
                let zr2 = zr * zr, zi2 = zi * zi;
                let i = 0;
                while (i < maxIter && zr2 + zi2 <= 256) {
                    zi = 2 * zr * zi + cjj;
                    zr = zr2 - zi2 + cj;
                    zr2 = zr * zr;
                    zi2 = zi * zi;
                    i++;
                }
                if (i >= maxIter) {
                    buf32[off] = 0xFF000000;
                }
                else {
                    const log_zn = Math.log(zr2 + zi2) / 2.0;
                    const nu = Math.log(log_zn / 0.6931471805599453) / 0.6931471805599453;
                    const smoothVal = i + 1.0 - nu;
                    const lutIdx = Math.min(Math.floor(smoothVal), lutSize - 1);
                    buf32[off] = lut[lutIdx];
                }
            }
        }
    }
    /** Render at a specific resolution factor (0..1) */
    _renderAtResolution(opts, pw, ph, factor) {
        // Render sub-rect at reduced res, then upscale to full canvas
        const dRe = opts.reMax - opts.reMin;
        const dIm = opts.imMax - opts.imMin;
        const subW = Math.max(1, Math.ceil(pw * factor));
        const subH = Math.max(1, Math.ceil(ph * factor));
        const subIter = new Float64Array(subW * subH);
        const cj = globalThis.__juliaCr ?? -0.7269;
        const cjj = globalThis.__juliaCi ?? 0.1889;
        const buf32 = this._buf32;
        const lut = this._colorLUT;
        const lutSize = this._lutSize;
        for (let sy = 0; sy < subH; sy++) {
            const ci = opts.imMin + (sy / subH) * dIm;
            const srcRowOff = sy * subW;
            for (let sx = 0; sx < subW; sx++) {
                const cr = opts.reMin + (sx / subW) * dRe;
                const srcOff = srcRowOff + sx;
                let zr, zi, zr2, zi2;
                let i = 0;
                if (opts.isJuliaMode) {
                    zr = cr;
                    zi = ci;
                    zr2 = zr * zr;
                    zi2 = zi * zi;
                    while (i < opts.maxIter && zr2 + zi2 <= 256) {
                        zi = 2 * zr * zi + cjj;
                        zr = zr2 - zi2 + cj;
                        zr2 = zr * zr;
                        zi2 = zi * zi;
                        i++;
                    }
                }
                else {
                    zr = 0;
                    zi = 0;
                    zr2 = 0;
                    zi2 = 0;
                    while (i < opts.maxIter && zr2 + zi2 <= 256) {
                        zi = 2 * zr * zi + ci;
                        zr = zr2 - zi2 + cr;
                        zr2 = zr * zr;
                        zi2 = zi * zi;
                        i++;
                    }
                }
                let color;
                if (i >= opts.maxIter) {
                    color = 0xFF000000;
                }
                else {
                    const log_zn = Math.log(zr2 + zi2) / 2.0;
                    const nu = Math.log(log_zn / 0.6931471805599453) / 0.6931471805599453;
                    const smoothVal = i + 1.0 - nu;
                    const lutIdx = Math.min(Math.floor(smoothVal), lutSize - 1);
                    color = lut[lutIdx];
                }
                // Upscale: map each sub-pixel to a block of destination pixels
                const dx0 = Math.floor((sx / subW) * opts.width);
                const dy0 = Math.floor((sy / subH) * opts.height);
                const dx1 = Math.floor(((sx + 1) / subW) * opts.width);
                const dy1 = Math.floor(((sy + 1) / subH) * opts.height);
                for (let dy = dy0; dy < dy1; dy++) {
                    const dstRow = dy * opts.width;
                    for (let dx = dx0; dx < dx1; dx++) {
                        buf32[dstRow + dx] = color;
                    }
                }
            }
        }
        this._ctx.putImageData(this._imageData, 0, 0);
    }
    /** Get the canvas element */
    get canvas() { return this._canvas; }
    /** Get the underlying ImageData for inspection */
    getImageData() { return this._imageData; }
}
exports.FractalRenderer = FractalRenderer;
};

// ── module: src/input.ts ──
__mods["src/input.ts"] = function (exports, require, module) {
"use strict";
// ============================================================
// INPUT — Mouse and keyboard event handling for navigation
// ============================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.InputHandler = void 0;
const state_1 = require("./state");
class InputHandler {
    constructor(config) {
        // State
        this._isDragging = false;
        this._dragStartX = 0;
        this._dragStartY = 0;
        this._dragEndX = 0;
        this._dragEndY = 0;
        this._isPanning = false;
        this._panStartX = 0;
        this._panStartY = 0;
        this._panStartCenterRe = 0;
        this._panStartCenterIm = 0;
        this._lastWheelTime = 0;
        this._mousemoveActive = false;
        this._canvas = config.canvas;
        this._renderer = config.renderer;
        this._onStateChange = config.onStateChange || (() => { });
        this._bindEvents();
    }
    _bindEvents() {
        this._canvas.addEventListener('mousedown', this._onMouseDown.bind(this));
        this._canvas.addEventListener('mousemove', this._onMouseMove.bind(this));
        window.addEventListener('mouseup', this._onMouseUp.bind(this));
        this._canvas.addEventListener('wheel', this._onWheel.bind(this), { passive: false });
        this._canvas.addEventListener('contextmenu', e => e.preventDefault());
        window.addEventListener('keydown', this._onKeyDown.bind(this));
        // Touch support
        this._canvas.addEventListener('touchstart', this._onTouchStart.bind(this), { passive: false });
        this._canvas.addEventListener('touchmove', this._onTouchMove.bind(this), { passive: false });
        this._canvas.addEventListener('touchend', this._onTouchEnd.bind(this));
    }
    /** Convert screen coords to fractal-space coordinates */
    _screenToFractal(sx, sy) {
        const state = (0, state_1.getState)();
        const v = state.view;
        const w = this._canvas.width / state.pixelRatio;
        const h = this._canvas.height / state.pixelRatio;
        const re = v.centerRe + (sx / w - 0.5) / v.scale;
        const im = v.centerIm + (sy / h - 0.5) / v.scale;
        return [re, im];
    }
    _onMouseDown(e) {
        if (e.button === 1 || (e.button === 0 && e.altKey)) {
            // Middle mouse or Alt+Left = pan
            this._isPanning = true;
            this._panStartX = e.clientX;
            this._panStartY = e.clientY;
            const state = (0, state_1.getState)();
            this._panStartCenterRe = state.view.centerRe;
            this._panStartCenterIm = state.view.centerIm;
            this._canvas.style.cursor = 'grabbing';
        }
        else if (e.button === 0) {
            // Left click = start selection rectangle
            this._isDragging = true;
            this._dragStartX = e.offsetX;
            this._dragStartY = e.offsetY;
            this._dragEndX = e.offsetX;
            this._dragEndY = e.offsetY;
            this._canvas.style.cursor = 'crosshair';
        }
    }
    _onMouseMove(e) {
        this._mousemoveActive = true;
        this._onStateChange();
        if (this._isDragging) {
            this._dragEndX = e.offsetX;
            this._dragEndY = e.offsetY;
            if (this.onSelectRectMove) {
                this.onSelectRectMove(e.offsetX, e.offsetY);
            }
        }
        else if (this._isPanning) {
            const dx = e.clientX - this._panStartX;
            const dy = e.clientY - this._panStartY;
            const state = (0, state_1.getState)();
            const w = this._canvas.width / state.pixelRatio;
            const h = this._canvas.height / state.pixelRatio;
            const fr = -dx / (w * state.view.scale);
            const fi = -dy / (h * state.view.scale);
            (0, state_1.setView)({
                centerRe: this._panStartCenterRe + fr,
                centerIm: this._panStartCenterIm + fi,
                scale: state.view.scale,
            });
            this._onStateChange();
        }
    }
    _onMouseUp(e) {
        if (this._isDragging) {
            const sx1 = Math.min(this._dragStartX, this._dragEndX);
            const sy1 = Math.min(this._dragStartY, this._dragEndY);
            const sx2 = Math.max(this._dragStartX, this._dragEndX);
            const sy2 = Math.max(this._dragStartY, this._dragEndY);
            const sw = sx2 - sx1;
            const sh = sy2 - sy1;
            // Only zoom if selection is meaningful
            if (sw > 5 && sh > 5 && this.onSelectRectEnd) {
                this.onSelectRectEnd(sx1, sy1, sx2, sy2);
            }
            this._isDragging = false;
            this._canvas.style.cursor = 'default';
        }
        else if (this._isPanning) {
            this._isPanning = false;
            this._canvas.style.cursor = 'default';
        }
    }
    _onWheel(e) {
        e.preventDefault();
        const now = performance.now();
        if (now - this._lastWheelTime < 50)
            return; // Throttle
        this._lastWheelTime = now;
        const factor = e.deltaY < 0 ? 1.3 : 1 / 1.3;
        const rect = this._canvas.getBoundingClientRect();
        const sx = e.clientX - rect.left;
        const sy = e.clientY - rect.top;
        const [fr, fi] = this._screenToFractal(sx, sy);
        (0, state_1.zoomAt)(fr, fi, factor);
        this._onStateChange();
    }
    _onKeyDown(e) {
        const state = (0, state_1.getState)();
        const key = e.key;
        let handled = false;
        switch (key) {
            case 'ArrowLeft':
                (0, state_1.panBy)(0.1 / state.view.scale, 0);
                handled = true;
                break;
            case 'ArrowRight':
                (0, state_1.panBy)(-0.1 / state.view.scale, 0);
                handled = true;
                break;
            case 'ArrowUp':
                (0, state_1.panBy)(0, 0.1 / state.view.scale);
                handled = true;
                break;
            case 'ArrowDown':
                (0, state_1.panBy)(0, -0.1 / state.view.scale);
                handled = true;
                break;
            case '+':
            case '=':
                (0, state_1.zoomAt)(0, 0, 1.5);
                handled = true;
                break;
            case '-':
            case '_':
                (0, state_1.zoomAt)(0, 0, 1 / 1.5);
                handled = true;
                break;
            case 'j':
            case 'J':
                if (this.onJuliaPoint) {
                    // Toggle Julia mode centered on current view
                    this.onJuliaPoint(state.view.centerRe, state.view.centerIm);
                }
                handled = true;
                break;
            case 'z':
            case 'Z':
                if (!(0, state_1.goBack)()) { /* no history */ }
                handled = true;
                break;
            case 'Escape':
                // Cancel any UI overlays
                break;
        }
        if (handled) {
            e.preventDefault();
            this._onStateChange();
        }
    }
    _onTouchStart(e) {
        e.preventDefault();
        if (e.touches.length === 1) {
            const touch = e.touches[0];
            const rect = this._canvas.getBoundingClientRect();
            this._isDragging = true;
            this._dragStartX = touch.clientX - rect.left;
            this._dragStartY = touch.clientY - rect.top;
            this._dragEndX = this._dragStartX;
            this._dragEndY = this._dragStartY;
        }
        else if (e.touches.length === 2) {
            // Pinch zoom
            this._isDragging = false;
        }
    }
    _onTouchMove(e) {
        e.preventDefault();
        if (e.touches.length === 1 && this._isDragging) {
            const touch = e.touches[0];
            const rect = this._canvas.getBoundingClientRect();
            this._dragEndX = touch.clientX - rect.left;
            this._dragEndY = touch.clientY - rect.top;
            if (this.onSelectRectMove) {
                this.onSelectRectMove(this._dragEndX, this._dragEndY);
            }
        }
        else if (e.touches.length === 2) {
            // Two-finger pinch zoom
            const t0 = e.touches[0];
            const t1 = e.touches[1];
            const dx = t1.clientX - t0.clientX;
            const dy = t1.clientY - t0.clientY;
            const dist = Math.sqrt(dx * dx + dy * dy);
            // Compare with previous distance for zoom factor
            // Simplified: just pan
        }
    }
    _onTouchEnd(e) {
        if (this._isDragging && this.onSelectRectEnd) {
            const sx1 = Math.min(this._dragStartX, this._dragEndX);
            const sy1 = Math.min(this._dragStartY, this._dragEndY);
            const sx2 = Math.max(this._dragStartX, this._dragEndX);
            const sy2 = Math.max(this._dragStartY, this._dragEndY);
            if (sx2 - sx1 > 10 && sy2 - sy1 > 10) {
                this.onSelectRectEnd(sx1, sy1, sx2, sy2);
            }
        }
        this._isDragging = false;
    }
}
exports.InputHandler = InputHandler;
};

// ── module: src/ui.ts ──
__mods["src/ui.ts"] = function (exports, require, module) {
"use strict";
// ============================================================
// UI — HUD overlay rendering (glassy, auto-hiding)
// Draws on a separate overlay canvas above the fractal canvas
// ============================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.UIFrame = void 0;
const state_1 = require("./state");
const palette_1 = require("./palette");
class UIFrame {
    constructor(canvas) {
        this._fpsFrames = 0;
        this._fpsTime = 0;
        this._fpsValue = 0;
        this._lastFrameTime = performance.now();
        this._canvas = canvas;
        this._ctx = canvas.getContext('2d');
    }
    resize(w, h) {
        this._canvas.width = w;
        this._canvas.height = h;
    }
    /** Update FPS counter */
    frameTick() {
        const now = performance.now();
        this._fpsFrames++;
        if (now - this._fpsTime >= 1000) {
            this._fpsValue = this._fpsFrames;
            this._fpsFrames = 0;
            this._fpsTime = now;
        }
        this._lastFrameTime = now;
    }
    /** Mark interaction for HUD visibility timer */
    interact() {
        const state = (0, state_1.getState)();
        state.hudVisible = true;
        state.hudTimer = 3000; // Show for 3 seconds after last interaction
    }
    draw() {
        const ctx = this._ctx;
        const w = this._canvas.width;
        const h = this._canvas.height;
        const state = (0, state_1.getState)();
        ctx.clearRect(0, 0, w, h);
        // Fade HUD
        if (state.hudTimer > 0) {
            state.hudTimer -= 16; // ~60fps tick
            if (state.hudTimer <= 0) {
                state.hudTimer = 0;
                state.hudVisible = false;
            }
        }
        if (!state.hudVisible)
            return;
        const alpha = Math.min(1, state.hudTimer / 500);
        ctx.globalAlpha = alpha * 0.85;
        // Draw HUD panels
        this._drawTopLeft(ctx, w, h, state);
        this._drawBottomRight(ctx, w, h, state);
        this._drawSelectionOverlay(ctx, w, h, state);
        ctx.globalAlpha = 1;
    }
    _drawTopLeft(ctx, w, h, state) {
        const mag = (0, state_1.getMagnification)();
        const palette = (0, palette_1.getCurrentPalette)();
        const isJulia = state.isJuliaMode;
        // Background panel
        ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.15)';
        ctx.lineWidth = 1;
        const pw = 260;
        const ph = 130;
        this._roundRect(ctx, 10, 10, pw, ph, 8);
        ctx.fill();
        ctx.stroke();
        ctx.font = 'bold 13px "SF Mono", "Fira Code", monospace';
        ctx.textBaseline = 'top';
        // Title
        ctx.fillStyle = '#fff';
        const title = isJulia ? `✦ Julia Set` : `◆ Mandelbrot`;
        ctx.fillText(title, 20, 18);
        // Magnification
        ctx.font = '12px "SF Mono", "Fira Code", monospace';
        ctx.fillStyle = '#aaa';
        ctx.fillText(`×${this._formatMag(mag)}`, 20, 40);
        // Center coordinates
        ctx.fillStyle = '#888';
        ctx.fillText(`Re: ${state.view.centerRe.toPrecision(12)}`, 20, 58);
        ctx.fillText(`Im: ${state.view.centerIm.toPrecision(12)}`, 20, 74);
        // Iterations
        ctx.fillStyle = '#aaa';
        ctx.fillText(`Iterations: ${state.maxIter}`, 20, 96);
        // Palette
        ctx.fillStyle = '#ccc';
        ctx.fillText(`Palette: ${palette.name}`, 20, 114);
    }
    _drawBottomRight(ctx, w, h, state) {
        // FPS + controls hint
        ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.15)';
        ctx.lineWidth = 1;
        const pw = 200;
        const ph = 90;
        this._roundRect(ctx, w - pw - 10, h - ph - 10, pw, ph, 8);
        ctx.fill();
        ctx.stroke();
        ctx.font = '11px "SF Mono", "Fira Code", monospace';
        ctx.textBaseline = 'top';
        ctx.fillStyle = '#aaa';
        ctx.fillText(`FPS: ${this._fpsValue}`, w - pw - 2, h - ph - 6);
        ctx.fillStyle = '#777';
        ctx.fillText('Drag: zoom box', w - pw - 2, h - ph + 14);
        ctx.fillText('Scroll: zoom in/out', w - pw - 2, h - ph + 30);
        ctx.fillText('Alt+drag: pan', w - pw - 2, h - ph + 46);
        ctx.fillText('J: center Julia set', w - pw - 2, h - ph + 62);
    }
    _drawSelectionOverlay(ctx, w, h, state) {
        // This is called when there's an active drag selection
        // The actual rect drawing happens in the main render loop
    }
    _formatMag(mag) {
        if (mag < 1000)
            return mag.toFixed(1);
        if (mag < 1e6)
            return (mag / 1000).toFixed(1) + 'K';
        if (mag < 1e9)
            return (mag / 1e6).toFixed(1) + 'M';
        if (mag < 1e12)
            return (mag / 1e9).toFixed(1) + 'B';
        return mag.toExponential(2);
    }
    _roundRect(ctx, x, y, w, h, r) {
        ctx.beginPath();
        ctx.moveTo(x + r, y);
        ctx.lineTo(x + w - r, y);
        ctx.quadraticCurveTo(x + w, y, x + w, y + r);
        ctx.lineTo(x + w, y + h - r);
        ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
        ctx.lineTo(x + r, y + h);
        ctx.quadraticCurveTo(x, y + h, x, y + h - r);
        ctx.lineTo(x, y + r);
        ctx.quadraticCurveTo(x, y, x + r, y);
        ctx.closePath();
    }
    /** Draw crosshair at fractional position */
    drawCrosshair(ctx, fx, fy) {
        const cw = this._canvas.width;
        const ch = this._canvas.height;
        ctx.save();
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.4)';
        ctx.lineWidth = 1;
        ctx.setLineDash([4, 4]);
        ctx.beginPath();
        ctx.moveTo(fx, 0);
        ctx.lineTo(fx, ch);
        ctx.moveTo(0, fy);
        ctx.lineTo(cw, fy);
        ctx.stroke();
        // Small circle at center
        ctx.beginPath();
        ctx.arc(fx, fy, 6, 0, Math.PI * 2);
        ctx.stroke();
        ctx.restore();
    }
}
exports.UIFrame = UIFrame;
};

// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen/qwen3.7-flash","provider":"openrouter","persona":"typescript","sessionId":"f09ae3d7-9081-4446-8186-0711c71bdf63","tokensIn":6011676,"tokensOut":71019,"tokensTotal":6082695,"cost":0.23390202199999996,"turns":103,"toolCalls":103,"failedToolCalls":2,"timestamp":"2026-08-02T03:10:45.618Z"} -->