← Fractal Explorer results

Fractal Explorer

Qwen 3.6 27b MTP · typescript

Initial view of Fractal Explorer

Browser check: Artifact runtime error. This is a technical check, not a quality review.

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 58,546 bytes · SHA-256 db1c2a8d00ca
<!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 Sets</title>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #000; font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; }

#app { position: relative; width: 100vw; height: 100vh; }

#fractal-canvas { display: block; width: 100%; height: 100%; image-rendering: auto; }
.overlay-canvas { position: absolute; top: 0; left: 0; pointer-events: none; }

/* ─── HUD ───────────────────────────────────────────── */
#hud {
  position: absolute; top: 0; left: 0; right: 0; z-index: 10;
  pointer-events: none; transition: opacity 0.4s ease;
}

#hud-top, #hud-bottom {
  display: flex; justify-content: space-between; align-items: center;
  padding: 8px 16px; color: rgba(255,255,255,0.85); font-size: 13px;
  text-shadow: 0 1px 4px rgba(0,0,0,0.9), 0 0 12px rgba(0,0,0,0.6);
}

#hud-top { background: linear-gradient(to bottom, rgba(0,0,0,0.5) 0%, transparent 100%); }
#hud-bottom { position: absolute; bottom: 0; top: auto; background: linear-gradient(to top, rgba(0,0,0,0.5) 0%, transparent 100%); }

/* ─── Controls Bar ───────────────────────────────────── */
#controls-bar {
  position: absolute; bottom: 48px; left: 50%; transform: translateX(-50%);
  display: flex; align-items: center; gap: 12px; padding: 8px 16px;
  background: rgba(15, 15, 30, 0.7); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
  border-radius: 12px; border: 1px solid rgba(255,255,255,0.1);
  pointer-events: auto;
}

/* ─── Palette Swatches ──────────────────────────────── */
#palette-container { display: flex; gap: 6px; }

.palette-chip {
  width: 28px; height: 28px; border-radius: 50%; cursor: pointer;
  border: 2px solid transparent; transition: all 0.2s ease;
}
.palette-chip:hover { transform: scale(1.15); }
.palette-chip.active { border-color: #fff; box-shadow: 0 0 8px rgba(255,255,255,0.4); }

/* ─── Sliders ────────────────────────────────────────── */
.slider-group { display: flex; align-items: center; gap: 4px; font-size: 13px; color: rgba(255,255,255,0.7); }
.slider-group input[type="range"] { width: 80px; height: 4px; cursor: pointer; accent-color: #6af; }

/* ─── Buttons ────────────────────────────────────────── */
#controls-bar button {
  background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.15);
  color: #fff; padding: 4px 10px; border-radius: 6px; cursor: pointer;
  font-size: 14px; transition: background 0.2s;
}
#controls-bar button:hover { background: rgba(255,255,255,0.2); }

/* ─── Bookmarks Panel ────────────────────────────────── */
#bookmarks-panel {
  display: none; position: absolute; bottom: 100px; left: 50%; transform: translateX(-50%);
  background: rgba(15, 15, 30, 0.85); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
  border-radius: 10px; border: 1px solid rgba(255,255,255,0.1); padding: 12px; min-width: 240px;
  pointer-events: auto; max-height: 300px; overflow-y: auto;
}

.bookmark-item {
  padding: 6px 10px; color: rgba(255,255,255,0.8); font-size: 12px; cursor: pointer;
  border-radius: 4px; transition: background 0.15s;
}
.bookmark-item:hover { background: rgba(255,255,255,0.1); }
.bookmark-empty { color: rgba(255,255,255,0.4); font-size: 12px; text-align: center; padding: 8px; }

/* ─── Julia Preview ──────────────────────────────────── */
#julia-preview-container {
  display: none; position: absolute; top: 60px; right: 16px; z-index: 5;
}
#julia-preview {
  border-radius: 8px; border: 2px solid rgba(255,255,255,0.3);
  box-shadow: 0 4px 20px rgba(0,0,0,0.6); cursor: pointer;
}

/* ─── Help Overlay ───────────────────────────────────── */
#help-overlay {
  display: none; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  background: rgba(10, 10, 25, 0.92); backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
  border-radius: 16px; border: 1px solid rgba(255,255,255,0.15); padding: 32px; color: #fff;
  z-index: 100; text-align: center; max-width: 400px;
}
#help-overlay h2 { margin-bottom: 16px; font-weight: 300; letter-spacing: 2px; }
#help-overlay p { margin: 6px 0; font-size: 13px; color: rgba(255,255,255,0.7); line-height: 1.6; }

/* ─── Scrollbar ──────────────────────────────────────── */
::-webkit-scrollbar { width: 4px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); border-radius: 2px; }
</style>
</head>
<body>
<div id="app">
  <canvas id="fractal-canvas"></canvas>
  <canvas id="zoom-box-canvas" class="overlay-canvas"></canvas>

  <!-- HUD -->
  <div id="hud">
    <div id="hud-top">
      <span id="mode-display">Mandelbrot</span>
      <span id="mag-display">×3.00</span>
    </div>
    <div id="hud-bottom">
      <span id="center-display">-0.5, 0i</span>
      <span id="iter-display">iterations: 200</span>
      <span id="fps-display">60 fps</span>
    </div>

    <!-- Controls -->
    <div id="controls-bar">
      <div id="palette-container"></div>

      <label class="slider-group" title="Color offset (palette cycling)">
        🎨<input type="range" id="color-slider" min="0" max="1" step="0.001" value="0">
      </label>

      <label class="slider-group" title="Iteration override">
        🔢<input type="range" id="iter-slider" min="50" max="15000" step="10" value="200">
      </label>

      <button id="back-btn" title="Zoom history back (B)">⬅</button>
      <button id="reset-btn" title="Reset view (R)">⟲</button>
      <button id="bookmark-btn" title="Save/Load bookmarks (S)">★</button>
    </div>

    <!-- Bookmarks panel -->
    <div id="bookmarks-panel"></div>
  </div>

  <!-- Julia preview container -->
  <div id="julia-preview-container"></div>

  <!-- Help overlay -->
  <div id="help-overlay">
    <h2>Fractal Explorer</h2>
    <p><b>Scroll</b> — zoom toward cursor &nbsp;|&nbsp; <b>Drag</b> — pan</p>
    <p><b>Shift+Drag</b> — rectangle zoom &nbsp;|&nbsp; <b>J</b> — Julia mode at center</p>
    <p><b>Arrows/+/-</b> — pan/zoom &nbsp;|&nbsp; <b>B</b> — back &nbsp;|&nbsp; <b>R</b> — reset</p>
    <p><b>S</b> — save bookmark &nbsp;|&nbsp; <b>P</b> — cycle palette</p>
  </div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./types":"src/types.ts","./palettes":"src/palettes.ts","./math":"src/math.ts","./renderer":"src/renderer.ts","./navigation":"src/navigation.ts","./ui":"src/ui.ts"},"src/palettes.ts":{"./types":"src/types.ts"},"src/math.ts":{"./types":"src/types.ts"},"src/renderer.ts":{"./types":"src/types.ts","./palettes":"src/palettes.ts"},"src/navigation.ts":{"./types":"src/types.ts"},"src/ui.ts":{"./types":"src/types.ts","./palettes":"src/palettes.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";
Object.defineProperty(exports, "__esModule", { value: true });
const types_1 = require("./types");
const palettes_1 = require("./palettes");
const math_1 = require("./math");
const renderer_1 = require("./renderer");
const navigation_1 = require("./navigation");
const ui_1 = require("./ui");
// ─── Globals ──────────────────────────────────────────────
let canvas;
let renderer;
let nav;
let ui;
let renderTimeout = null;
let iterOverride = 0; // 0 means auto-compute
// Interaction state
let isPanning = false;
let isRectZooming = false;
let panStartX = 0, panStartY = 0;
let rectStartX = 0, rectStartY = 0;
let lastMouseX = 0, lastMouseY = 0;
// Bookmarks
const bookmarks = loadBookmarks();
// ─── Initialization ──────────────────────────────────────
function init() {
    canvas = document.getElementById('fractal-canvas');
    renderer = new renderer_1.FractalRenderer(canvas);
    nav = new navigation_1.Navigation();
    ui = new ui_1.UIManager();
    // Load from URL hash or use defaults
    if (!nav.loadFromHash()) {
        // Check for saved bookmark in localStorage
        const lastView = localStorage.getItem('fractal-last-view');
        if (lastView) {
            try {
                nav.restore(JSON.parse(lastView));
            }
            catch { }
        }
    }
    resize();
    window.addEventListener('resize', resize);
    setupInputHandlers();
    setupKeyboardHandlers();
    setupUICallbacks();
    // Initial render
    scheduleRender();
    // Show help briefly
    const help = document.getElementById('help-overlay');
    help.style.display = 'block';
    setTimeout(() => { help.style.display = 'none'; }, 4000);
    // Expose debug interface
    window.fractal = {
        iterationsAt: (re, im) => (0, math_1.iterationsAt)(re, im, getEffectiveMaxIter()),
        setView: (view) => { nav.restore({ ...nav.state, ...view }); scheduleRender(); },
        getState: () => nav.serialize(),
        getBookmarks: () => bookmarks.map(b => ({ name: b.name, viewState: b.viewState })),
    };
    // FPS counter
    requestAnimationFrame(tick);
}
function resize() {
    const dpr = Math.min(window.devicePixelRatio || 1, 2);
    canvas.width = window.innerWidth * dpr;
    canvas.height = window.innerHeight * dpr;
    ui.resize(canvas.width, canvas.height);
    scheduleRender();
}
// ─── Render Pipeline ──────────────────────────────────────
function getEffectiveMaxIter() {
    if (iterOverride > 0)
        return iterOverride;
    return (0, math_1.computeMaxIterations)(nav.state.magnification);
}
function scheduleRender() {
    renderer.cancel();
    if (renderTimeout !== null)
        clearTimeout(renderTimeout);
    renderTimeout = window.setTimeout(() => doRender(), 50); // debounce rapid interactions
}
async function doRender() {
    const state = nav.state;
    const maxIter = getEffectiveMaxIter();
    const params = {
        width: canvas.width,
        height: canvas.height,
        centerRe: state.centerRe,
        centerIm: state.centerIm,
        magnification: state.magnification,
        maxIterations: maxIter,
        paletteIndex: state.paletteIndex,
        colorOffset: state.colorOffset,
        setMode: state.setMode,
        juliaCRe: state.juliaCRe,
        juliaCIm: state.juliaCIm,
    };
    await renderer.render(params);
    // Update HUD after render completes
    updateHUD(maxIter);
}
function recolorOnly() {
    // For palette/offset changes without recomputing iterations,
    // we need to re-render since we don't store iteration buffer.
    // This is a trade-off for simplicity; the color pass is fast.
    scheduleRender();
}
function updateHUD(maxIter) {
    nav.pushHistory();
    ui.updateMag(nav.formatMagnification());
    ui.updateCenter(nav.formatCenter());
    ui.updateIterations(maxIter, iterOverride > 0);
    ui.updateMode(nav.state.setMode, nav.state.setMode === 'julia' ? { re: nav.state.juliaCRe, im: nav.state.juliaCIm } : undefined);
    ui.updateIterSlider(maxIter);
    ui.setActivePalette(nav.state.paletteIndex);
    // Save to localStorage
    try {
        localStorage.setItem('fractal-last-view', JSON.stringify(nav.serialize()));
    }
    catch { }
}
// ─── Input Handling ──────────────────────────────────────
function setupInputHandlers() {
    canvas.addEventListener('mousedown', onMouseDown);
    window.addEventListener('mousemove', onMouseMove);
    window.addEventListener('mouseup', onMouseUp);
    canvas.addEventListener('wheel', onWheel, { passive: false });
    canvas.addEventListener('contextmenu', e => e.preventDefault());
    // Touch support
    let lastTouchDist = 0;
    canvas.addEventListener('touchstart', (e) => {
        if (e.touches.length === 1) {
            isPanning = true;
            panStartX = e.touches[0].clientX;
            panStartY = e.touches[0].clientY;
        }
        else if (e.touches.length === 2) {
            lastTouchDist = Math.hypot(e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY);
        }
        e.preventDefault();
    }, { passive: false });
    canvas.addEventListener('touchmove', (e) => {
        if (e.touches.length === 1 && isPanning) {
            const dx = e.touches[0].clientX - panStartX;
            const dy = e.touches[0].clientY - panStartY;
            nav.pan(dx, dy, canvas.width, canvas.height);
            panStartX = e.touches[0].clientX;
            panStartY = e.touches[0].clientY;
            scheduleRender();
        }
        else if (e.touches.length === 2) {
            const dist = Math.hypot(e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY);
            const factor = dist / lastTouchDist;
            const cx = (e.touches[0].clientX + e.touches[1].clientX) * 0.5;
            const cy = (e.touches[0].clientY + e.touches[1].clientY) * 0.5;
            nav.zoomToward(cx, cy, canvas.width, canvas.height, factor);
            lastTouchDist = dist;
            scheduleRender();
        }
        e.preventDefault();
    }, { passive: false });
    canvas.addEventListener('touchend', () => { isPanning = false; });
}
function onMouseDown(e) {
    const rect = canvas.getBoundingClientRect();
    const x = (e.clientX - rect.left) * (canvas.width / rect.width);
    const y = (e.clientY - rect.top) * (canvas.height / rect.height);
    if (e.shiftKey || e.button === 2) {
        // Rectangle zoom
        isRectZooming = true;
        rectStartX = x;
        rectStartY = y;
    }
    else {
        // Pan
        isPanning = true;
        panStartX = x;
        panStartY = y;
    }
}
function onMouseMove(e) {
    const rect = canvas.getBoundingClientRect();
    const x = (e.clientX - rect.left) * (canvas.width / rect.width);
    const y = (e.clientY - rect.top) * (canvas.height / rect.height);
    if (isRectZooming) {
        ui.drawZoomBox(Math.min(rectStartX, x), Math.min(rectStartY, y), Math.max(rectStartX, x), Math.max(rectStartY, y));
    }
    else if (isPanning) {
        const dx = x - panStartX;
        const dy = y - panStartY;
        nav.pan(dx, dy, canvas.width, canvas.height);
        panStartX = x;
        panStartY = y;
        scheduleRender();
    }
    lastMouseX = e.clientX;
    lastMouseY = e.clientY;
}
function onMouseUp(e) {
    if (isRectZooming) {
        const rect = canvas.getBoundingClientRect();
        const x = (e.clientX - rect.left) * (canvas.width / rect.width);
        const y = (e.clientY - rect.top) * (canvas.height / rect.height);
        ui.clearZoomBox();
        // Only zoom if the rectangle is large enough
        if (Math.abs(x - rectStartX) > 5 && Math.abs(y - rectStartY) > 5) {
            nav.zoomToRect(Math.min(rectStartX, x), Math.min(rectStartY, y), Math.max(rectStartX, x), Math.max(rectStartY, y), canvas.width, canvas.height);
        }
        isRectZooming = false;
        scheduleRender();
    }
    else if (isPanning) {
        isPanning = false;
        scheduleRender(); // final render after pan
    }
}
function onWheel(e) {
    e.preventDefault();
    const rect = canvas.getBoundingClientRect();
    const x = (e.clientX - rect.left) * (canvas.width / rect.width);
    const y = (e.clientY - rect.top) * (canvas.height / rect.height);
    // Zoom factor based on wheel delta
    const factor = e.deltaY < 0 ? (1 + types_1.CONFIG.zoomSpeed) : (1 / (1 + types_1.CONFIG.zoomSpeed));
    nav.zoomToward(x, y, canvas.width, canvas.height, factor);
    scheduleRender();
}
// ─── Keyboard Handling ──────────────────────────────────
function setupKeyboardHandlers() {
    window.addEventListener('keydown', (e) => {
        const state = nav.state;
        const panAmount = Math.max(20, state.magnification * 0.1);
        switch (e.key.toLowerCase()) {
            case 'arrowleft':
                nav.pan(panAmount, 0, canvas.width, canvas.height);
                scheduleRender();
                e.preventDefault();
                break;
            case 'arrowright':
                nav.pan(-panAmount, 0, canvas.width, canvas.height);
                scheduleRender();
                e.preventDefault();
                break;
            case 'arrowup':
                nav.pan(0, panAmount, canvas.width, canvas.height);
                scheduleRender();
                e.preventDefault();
                break;
            case 'arrowdown':
                nav.pan(0, -panAmount, canvas.width, canvas.height);
                scheduleRender();
                e.preventDefault();
                break;
            case '=':
            case '+':
                nav.zoomToward(canvas.width / 2, canvas.height / 2, canvas.width, canvas.height, 1.5);
                scheduleRender();
                e.preventDefault();
                break;
            case '-':
                nav.zoomToward(canvas.width / 2, canvas.height / 2, canvas.width, canvas.height, 1 / 1.5);
                scheduleRender();
                e.preventDefault();
                break;
            case 'j': {
                // Julia mode at center of current view
                const cRe = state.setMode === 'mandelbrot' ? state.centerRe : state.juliaCRe;
                const cIm = state.setMode === 'mandelbrot' ? state.centerIm : state.juliaCIm;
                if (state.setMode === 'julia') {
                    nav.setMandelbrotMode();
                }
                else {
                    nav.setJuliaMode(cRe, cIm);
                }
                scheduleRender();
                break;
            }
            case 'b':
                if (nav.goBack())
                    scheduleRender();
                e.preventDefault();
                break;
            case 'r':
                nav.reset();
                iterOverride = 0;
                document.getElementById('iter-slider').value = String(types_1.CONFIG.maxIterationsBase);
                scheduleRender();
                e.preventDefault();
                break;
            case 's': {
                // Toggle bookmarks panel
                const panel = document.getElementById('bookmarks-panel');
                if (panel.style.display === 'block') {
                    ui.hideBookmarksPanel();
                }
                else {
                    saveBookmark();
                    e.preventDefault();
                }
                break;
            }
            case 'p': {
                // Cycle palette
                const next = (state.paletteIndex + 1) % palettes_1.PALETTES.length;
                nav.setPalette(next);
                recolorOnly();
                e.preventDefault();
                break;
            }
            case '?':
            case '/': {
                const help = document.getElementById('help-overlay');
                help.style.display = help.style.display === 'block' ? 'none' : 'block';
                e.preventDefault();
                break;
            }
        }
    });
}
// ─── UI Callbacks ────────────────────────────────────────
function setupUICallbacks() {
    ui.onPaletteSelect = (index) => {
        nav.setPalette(index);
        recolorOnly();
    };
    ui.onColorOffsetChange = (offset) => {
        nav.setColorOffset(offset);
        recolorOnly();
    };
    ui.onIterOverrideChange = (iter) => {
        iterOverride = iter;
        scheduleRender();
    };
    ui.onReset = () => {
        nav.reset();
        iterOverride = 0;
        document.getElementById('iter-slider').value = String(types_1.CONFIG.maxIterationsBase);
        scheduleRender();
    };
    ui.onBack = () => {
        if (nav.goBack())
            scheduleRender();
    };
    ui.onBookmark = () => saveBookmark();
    ui.onBookmarkRestore = (index) => {
        const bm = bookmarks[index];
        if (!bm)
            return;
        nav.restore(bm.viewState);
        iterOverride = 0;
        ui.hideBookmarksPanel();
        scheduleRender();
    };
}
// ─── Bookmarks ──────────────────────────────────────────
function saveBookmark() {
    const name = prompt('Bookmark name:', `View ${bookmarks.length + 1}`);
    if (!name)
        return;
    bookmarks.push({
        name,
        viewState: nav.serialize(),
        timestamp: Date.now(),
    });
    try {
        localStorage.setItem('fractal-bookmarks', JSON.stringify(bookmarks));
    }
    catch { }
    ui.showBookmarksPanel(bookmarks.map(b => ({ name: b.name, timestamp: b.timestamp })));
}
function loadBookmarks() {
    try {
        const data = localStorage.getItem('fractal-bookmarks');
        return data ? JSON.parse(data) : [];
    }
    catch {
        return [];
    }
}
// ─── Animation Loop ──────────────────────────────────────
function tick() {
    ui.updateFps();
    requestAnimationFrame(tick);
}
// ─── Start ──────────────────────────────────────────────
init();
};

// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CONFIG = void 0;
// ─── Tunable Constants ────────────────────────────────────────────────
exports.CONFIG = Object.freeze({
    // Iteration limits
    maxIterationsBase: 200,
    maxIterationsScale: 80, // added per log10(magnification)
    maxIterationsCap: 15000,
    // Zoom behavior
    zoomSpeed: 0.3, // wheel zoom factor
    minMagnification: 0.1,
    maxMagnification: 1e14, // double-precision limit
    // Progressive rendering - just a quick initial pass then full resolution
    progressivePasses: [1], // single full-resolution pass; add [4, 1] for coarse-then-fine
    workerCount: navigator.hardwareConcurrency || 4,
    // Julia preview
    juliaPreviewSize: 150, // px for the mini Julia preview pane
    // UI
    hudHideDelayMs: 3000,
});
};

// ── module: src/palettes.ts ──
__mods["src/palettes.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PALETTES = void 0;
exports.buildColorLUT = buildColorLUT;
exports.getPaletteSwatch = getPaletteSwatch;
// ─── Palette Definitions ──────────────────────────────────────────────
exports.PALETTES = [
    {
        name: 'Fire',
        stops: [
            [0.00, 0.00, 0.00, 0.05],
            [0.15, 0.40, 0.00, 0.00],
            [0.30, 0.80, 0.20, 0.00],
            [0.50, 1.00, 0.60, 0.00],
            [0.70, 1.00, 1.00, 0.30],
            [0.85, 1.00, 1.00, 0.90],
            [1.00, 1.00, 1.00, 1.00],
        ],
    },
    {
        name: 'Ocean',
        stops: [
            [0.00, 0.00, 0.02, 0.15],
            [0.15, 0.00, 0.10, 0.40],
            [0.30, 0.00, 0.30, 0.70],
            [0.50, 0.00, 0.60, 0.90],
            [0.70, 0.20, 0.85, 1.00],
            [0.85, 0.60, 0.95, 1.00],
            [1.00, 1.00, 1.00, 1.00],
        ],
    },
    {
        name: 'Monochrome',
        stops: [
            [0.00, 0.00, 0.00, 0.00],
            [0.25, 0.30, 0.30, 0.30],
            [0.50, 0.60, 0.60, 0.60],
            [0.75, 0.85, 0.85, 0.85],
            [1.00, 1.00, 1.00, 1.00],
        ],
    },
    {
        name: 'Psychedelic',
        stops: [
            [0.00, 0.50, 0.00, 0.80],
            [0.17, 0.90, 0.10, 0.30],
            [0.33, 1.00, 0.60, 0.00],
            [0.50, 0.20, 0.80, 0.40],
            [0.67, 0.00, 0.30, 0.90],
            [0.83, 0.80, 0.10, 0.50],
            [1.00, 0.50, 0.00, 0.80],
        ],
    },
    {
        name: 'Electric',
        stops: [
            [0.00, 0.00, 0.00, 0.20],
            [0.15, 0.00, 0.40, 0.80],
            [0.30, 0.20, 0.70, 1.00],
            [0.50, 0.60, 0.90, 1.00],
            [0.70, 1.00, 0.80, 0.40],
            [0.85, 1.00, 0.50, 0.20],
            [1.00, 1.00, 0.30, 0.60],
        ],
    },
];
// ─── Color Interpolation ──────────────────────────────────────────────
/**
 * Pre-compute a color LUT (256 entries) for a palette + offset.
 * Returns Uint8Array of length 1024 (RGBA × 256).
 */
function buildColorLUT(palette, offset) {
    const lut = new Uint8Array(256 * 4);
    const stops = palette.stops;
    for (let i = 0; i < 256; i++) {
        let t = ((i / 255) + offset) % 1.0;
        if (t < 0)
            t += 1.0;
        // Find surrounding stops
        let lo = 0, hi = stops.length - 1;
        for (let s = 0; s < stops.length - 1; s++) {
            if (stops[s][0] <= t && stops[s + 1][0] >= t) {
                lo = s;
                hi = s + 1;
                break;
            }
        }
        const stopLo = stops[lo];
        const stopHi = stops[hi];
        const range = stopHi[0] - stopLo[0];
        const frac = range > 0 ? (t - stopLo[0]) / range : 0;
        // Smoothstep interpolation for smoother gradients
        const s = frac * frac * (3 - 2 * frac);
        lut[i * 4 + 0] = Math.round(lerp(stopLo[1], stopHi[1], s) * 255);
        lut[i * 4 + 1] = Math.round(lerp(stopLo[2], stopHi[2], s) * 255);
        lut[i * 4 + 2] = Math.round(lerp(stopLo[3], stopHi[3], s) * 255);
        lut[i * 4 + 3] = 255; // opaque
    }
    return lut;
}
function lerp(a, b, t) {
    return a + (b - a) * t;
}
// ─── Palette Swatch Colors for UI ──────────────────────────────────────
function getPaletteSwatch(paletteIndex) {
    const p = exports.PALETTES[paletteIndex];
    if (!p)
        return '#888';
    // Use middle stop as representative color
    const mid = p.stops[Math.floor(p.stops.length / 2)];
    return `rgb(${Math.round(mid[1] * 255)}, ${Math.round(mid[2] * 255)}, ${Math.round(mid[3] * 255)})`;
}
};

// ── module: src/math.ts ──
__mods["src/math.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.mandelbrotIteration = mandelbrotIteration;
exports.juliaIteration = juliaIteration;
exports.computePixel = computePixel;
exports.iterationsAt = iterationsAt;
exports.computeMaxIterations = computeMaxIterations;
exports.iterationToColorIndex = iterationToColorIndex;
const types_1 = require("./types");
// ─── Fractal Math ──────────────────────────────────────────────────────
/**
 * Compute the smooth (continuous) iteration count for a point.
 * Returns maxIterations if the point is in the set (does not escape).
 * Uses the normalized iteration count: n + 1 - log2(log|z|)
 */
function mandelbrotIteration(cRe, cIm, maxIter) {
    let zRe = 0;
    let zIm = 0;
    let zRe2 = 0;
    let zIm2 = 0;
    for (let n = 0; n < maxIter; n++) {
        // z = z² + c
        zIm = 2 * zRe * zIm + cIm;
        zRe = zRe2 - zIm2 + cRe;
        zRe2 = zRe * zRe;
        zIm2 = zIm * zIm;
        if (zRe2 + zIm2 > 256) {
            // Smooth iteration count: n + 1 - log2(log|z|)
            const logZn = Math.log(zRe2 + zIm2) * 0.5;
            return n + 1 - Math.log(logZn) / Math.LN2;
        }
    }
    return maxIter; // point is in the set
}
/**
 * Julia set iteration for a given c-value.
 */
function juliaIteration(zRe0, zIm0, cRe, cIm, maxIter) {
    let zRe = zRe0;
    let zIm = zIm0;
    let zRe2 = zRe * zRe;
    let zIm2 = zIm * zIm;
    for (let n = 0; n < maxIter; n++) {
        // z = z² + c
        zIm = 2 * zRe * zIm + cIm;
        zRe = zRe2 - zIm2 + cRe;
        zRe2 = zRe * zRe;
        zIm2 = zIm * zIm;
        if (zRe2 + zIm2 > 256) {
            const logZn = Math.log(zRe2 + zIm2) * 0.5;
            return n + 1 - Math.log(logZn) / Math.LN2;
        }
    }
    return maxIter; // point is in the set
}
/**
 * Compute iterations for a single pixel (dispatches to correct algorithm).
 */
function computePixel(x, y, width, height, centerRe, centerIm, magnification, setMode, juliaCRe, juliaCIm, maxIter) {
    // Convert pixel to complex plane coordinates
    const aspect = width / height;
    const scale = magnification / height;
    let cRe, cIm;
    if (setMode === 'mandelbrot') {
        cRe = centerRe + (x - width * 0.5) * scale;
        cIm = centerIm + (y - height * 0.5) * scale / aspect;
        return mandelbrotIteration(cRe, cIm, maxIter);
    }
    else {
        const zRe = centerRe + (x - width * 0.5) * scale;
        const zIm = centerIm + (y - height * 0.5) * scale / aspect;
        return juliaIteration(zRe, zIm, juliaCRe, juliaCIm, maxIter);
    }
}
/**
 * Compute iterations at a given complex coordinate for Mandelbrot set.
 * Used by the debug interface.
 */
function iterationsAt(re, im, maxIter) {
    return mandelbrotIteration(re, im, maxIter ?? types_1.CONFIG.maxIterationsBase);
}
/**
 * Calculate appropriate iteration count based on magnification.
 * Deeper zooms need more iterations to resolve fine structure.
 */
function computeMaxIterations(magnification) {
    const logMag = Math.log10(Math.max(1, magnification));
    return Math.min(types_1.CONFIG.maxIterationsBase + types_1.CONFIG.maxIterationsScale * logMag, types_1.CONFIG.maxIterationsCap);
}
/**
 * Convert smooth iteration count to a color index [0, 255].
 */
function iterationToColorIndex(smoothIter, maxIter) {
    if (smoothIter >= maxIter)
        return -1; // interior point (black)
    const t = Math.max(0, smoothIter / maxIter);
    return Math.min(255, Math.floor(t * 256));
}
};

// ── module: src/renderer.ts ──
__mods["src/renderer.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.FractalRenderer = void 0;
const types_1 = require("./types");
const palettes_1 = require("./palettes");
// ─── Worker Code (inline via Blob URL) ────────────────────────────────
const WORKER_CODE = `
self.onmessage = function(e) {
  const job = e.data.job;
  const params = e.data.params;
  const lut = e.data.lut;
  const x0 = job.x0, x1 = job.x1, y0 = job.y0, y1 = job.y1;
  const p = params;

  const aspect = p.width / p.height;
  const scale = p.magnification / p.height;
  const tileW = x1 - x0;
  const buf = new Uint8Array(tileW * (y1 - y0) * 4);

  for (let y = y0; y < y1; y++) {
    const cIm_base = p.centerIm + (y - p.height * 0.5) * scale / aspect;

    for (let x = x0; x < x1; x++) {
      let zRe, zIm, cRe, cIm;

      if (p.setMode === 'mandelbrot') {
        cRe = p.centerRe + (x - p.width * 0.5) * scale;
        cIm = cIm_base;
        zRe = 0; zIm = 0;
      } else {
        zRe = p.centerRe + (x - p.width * 0.5) * scale;
        zIm = cIm_base;
        cRe = p.juliaCRe; cIm = p.juliaCIm;
      }

      let iter = p.maxIterations;
      let zr2 = zRe * zRe, zi2 = zIm * zIm;
      for (let n = 0; n < p.maxIterations; n++) {
        const newZi = 2 * zRe * zIm + cIm;
        zRe = zr2 - zi2 + cRe;
        zIm = newZi;
        zr2 = zRe * zRe;
        zi2 = zIm * zIm;
        if (zr2 + zi2 > 256) {
          const logZn = Math.log(zr2 + zi2) * 0.5;
          iter = n + 1 - Math.log(logZn) / Math.LN2;
          break;
        }
      }

      const col = x - x0;
      const row = y - y0;
      const idx = (row * tileW + col) * 4;

      if (iter >= p.maxIterations) {
        buf[idx] = 0; buf[idx+1] = 0; buf[idx+2] = 0; buf[idx+3] = 255;
      } else {
        const t = Math.max(0, iter / p.maxIterations);
        const ci = Math.min(255, Math.floor(t * 256));
        const li = ci * 4;
        buf[idx] = lut[li]; buf[idx+1] = lut[li+1];
        buf[idx+2] = lut[li+2]; buf[idx+3] = 255;
      }
    }
  }

  self.postMessage({ job, buffer: buf }, [buf.buffer]);
};
`;
// ─── Renderer Class ────────────────────────────────────────────────────
class FractalRenderer {
    constructor(canvas) {
        this.imageData = null;
        this.workers = [];
        this.currentParams = null;
        this.renderId = 0;
        this.isRendering = false;
        this.canvas = canvas;
        this.ctx = canvas.getContext('2d', { willReadFrequently: false });
        this.workerBlobUrl = URL.createObjectURL(new Blob([WORKER_CODE], { type: 'application/javascript' }));
        this.initWorkers();
    }
    initWorkers() {
        const count = Math.min(types_1.CONFIG.workerCount || 4, 8);
        for (let i = 0; i < count; i++) {
            try {
                const w = new Worker(this.workerBlobUrl);
                this.workers.push(w);
            }
            catch {
                break;
            }
        }
    }
    /** Start a progressive render. */
    async render(params) {
        const newRenderId = ++this.renderId;
        this.currentParams = params;
        this.isRendering = true;
        if (!this.imageData || this.imageData.width !== params.width || this.imageData.height !== params.height) {
            this.imageData = new ImageData(params.width, params.height);
        }
        const lut = (0, palettes_1.buildColorLUT)(palettes_1.PALETTES[params.paletteIndex], params.colorOffset);
        // Progressive passes: coarse to fine (main thread for responsiveness)
        for (const downscale of types_1.CONFIG.progressivePasses) {
            if (this.renderId !== newRenderId)
                return;
            await this.renderMainThread(params, lut, downscale, newRenderId);
        }
        this.isRendering = false;
        this.onComplete?.();
    }
    async renderMainThread(params, lut, downscale, renderId) {
        const { width, height } = params;
        const aspect = width / height;
        const scale = params.magnification / height;
        // Process in row chunks to keep UI responsive
        const chunkRows = Math.max(1, Math.floor(height / downscale) / 30);
        for (let yBase = 0; yBase < height; yBase += downscale * chunkRows) {
            if (this.renderId !== renderId)
                return;
            const yEnd = Math.min(yBase + downscale * chunkRows, height);
            for (let y = yBase; y < yEnd; y += downscale) {
                const cIm_base = params.centerIm + (y - height * 0.5) * scale / aspect;
                for (let x = 0; x < width; x += downscale) {
                    let zRe, zIm, cRe, cIm;
                    if (params.setMode === 'mandelbrot') {
                        cRe = params.centerRe + (x - width * 0.5) * scale;
                        cIm = cIm_base;
                        zRe = 0;
                        zIm = 0;
                    }
                    else {
                        zRe = params.centerRe + (x - width * 0.5) * scale;
                        zIm = cIm_base;
                        cRe = params.juliaCRe;
                        cIm = params.juliaCIm;
                    }
                    let iter = params.maxIterations;
                    let zr2 = zRe * zRe, zi2 = zIm * zIm;
                    for (let n = 0; n < params.maxIterations; n++) {
                        const newZi = 2 * zRe * zIm + cIm;
                        zRe = zr2 - zi2 + cRe;
                        zIm = newZi;
                        zr2 = zRe * zRe;
                        zi2 = zIm * zIm;
                        if (zr2 + zi2 > 256) {
                            const logZn = Math.log(zr2 + zi2) * 0.5;
                            iter = n + 1 - Math.log(logZn) / Math.LN2;
                            break;
                        }
                    }
                    // Fill the downscale×downscale block with this pixel's color
                    const isInterior = iter >= params.maxIterations;
                    let r, g, b;
                    if (isInterior) {
                        r = 0;
                        g = 0;
                        b = 0;
                    }
                    else {
                        const t = Math.max(0, iter / params.maxIterations);
                        const ci = Math.min(255, Math.floor(t * 256));
                        const li = ci * 4;
                        r = lut[li];
                        g = lut[li + 1];
                        b = lut[li + 2];
                    }
                    for (let dy = 0; dy < downscale && y + dy < height; dy++) {
                        const rowOffset = (y + dy) * width;
                        for (let dx = 0; dx < downscale && x + dx < width; dx++) {
                            const idx = (rowOffset + x + dx) * 4;
                            this.imageData.data[idx] = r;
                            this.imageData.data[idx + 1] = g;
                            this.imageData.data[idx + 2] = b;
                            this.imageData.data[idx + 3] = 255;
                        }
                    }
                }
            }
            // Blit to canvas after each chunk
            if (this.imageData) {
                this.ctx.putImageData(this.imageData, 0, 0);
            }
            // Yield to browser for UI responsiveness
            await new Promise(r => setTimeout(r, 0));
        }
    }
    /** Cancel any in-flight render. */
    cancel() {
        ++this.renderId;
    }
    setOnComplete(cb) {
        this.onComplete = cb;
    }
    getParams() {
        return this.currentParams;
    }
    isRenderingActive() {
        return this.isRendering;
    }
    destroy() {
        this.cancel();
        for (const w of this.workers) {
            try {
                w.terminate();
            }
            catch { }
        }
        URL.revokeObjectURL(this.workerBlobUrl);
    }
}
exports.FractalRenderer = FractalRenderer;
};

// ── module: src/navigation.ts ──
__mods["src/navigation.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Navigation = void 0;
const types_1 = require("./types");
// ─── View State & Navigation ──────────────────────────────────────────
const DEFAULT_VIEW = Object.freeze({
    centerRe: -0.5,
    centerIm: 0,
    magnification: 3,
    paletteIndex: 0,
    colorOffset: 0,
    setMode: 'mandelbrot',
    juliaCRe: -0.7269,
    juliaCIm: 0.1889,
});
class Navigation {
    constructor() {
        this.history = [];
        this.historyIndex = -1;
        this.viewState = { ...DEFAULT_VIEW };
    }
    get state() {
        return this.viewState;
    }
    /** Reset to default view. */
    reset() {
        this.pushHistory();
        this.viewState = { ...DEFAULT_VIEW };
    }
    /** Push current state onto history stack (for back button). */
    pushHistory(maxLen = 50) {
        // Don't save if same as last entry
        const last = this.history[this.historyIndex];
        if (last && viewsEqual(last, this.viewState))
            return;
        // Trim forward history if we're not at the end
        this.history = this.history.slice(0, this.historyIndex + 1);
        this.history.push({ ...this.viewState });
        if (this.history.length > maxLen) {
            this.history.shift();
        }
        this.historyIndex = this.history.length - 1;
    }
    /** Go back in history. */
    goBack() {
        if (this.historyIndex <= 0)
            return false;
        this.historyIndex--;
        this.viewState = { ...this.history[this.historyIndex] };
        return true;
    }
    /** Zoom toward a point by a factor. Returns the new magnification. */
    zoomToward(screenX, screenY, canvasWidth, canvasHeight, factor) {
        const aspect = canvasWidth / canvasHeight;
        const scale = this.viewState.magnification / canvasHeight;
        // Complex coordinate at cursor
        const targetRe = this.viewState.centerRe + (screenX - canvasWidth * 0.5) * scale;
        const targetIm = this.viewState.centerIm + (screenY - canvasHeight * 0.5) * scale / aspect;
        let newMag = this.viewState.magnification * factor;
        newMag = Math.max(types_1.CONFIG.minMagnification, Math.min(types_1.CONFIG.maxMagnification, newMag));
        // Move center toward cursor
        const newScale = newMag / canvasHeight;
        this.viewState.centerRe = targetRe - (screenX - canvasWidth * 0.5) * newScale;
        this.viewState.centerIm = targetIm - (screenY - canvasHeight * 0.5) * newScale / aspect;
        this.viewState.magnification = newMag;
    }
    /** Pan by a delta in screen pixels. */
    pan(dx, dy, canvasWidth, canvasHeight) {
        const aspect = canvasWidth / canvasHeight;
        const scale = this.viewState.magnification / canvasHeight;
        this.viewState.centerRe -= dx * scale;
        this.viewState.centerIm -= dy * scale / aspect;
    }
    /** Zoom into a rectangular region (in screen pixel coordinates). */
    zoomToRect(x0, y0, x1, y1, canvasWidth, canvasHeight) {
        const aspect = canvasWidth / canvasHeight;
        const scale = this.viewState.magnification / canvasHeight;
        // Complex coordinates of rect corners
        const re0 = this.viewState.centerRe + (x0 - canvasWidth * 0.5) * scale;
        const im0 = this.viewState.centerIm + (y0 - canvasHeight * 0.5) * scale / aspect;
        const re1 = this.viewState.centerRe + (x1 - canvasWidth * 0.5) * scale;
        const im1 = this.viewState.centerIm + (y1 - canvasHeight * 0.5) * scale / aspect;
        // New center is midpoint of rect
        const newCenterRe = (re0 + re1) * 0.5;
        const newCenterIm = (im0 + im1) * 0.5;
        // New magnification: fit the rect to the canvas
        const rectWidth = Math.abs(re1 - re0);
        const rectHeight = Math.abs(im1 - im0) * aspect;
        const zoomFactor = Math.max(canvasWidth / (rectWidth * canvasHeight), 1);
        let newMag = this.viewState.magnification * zoomFactor;
        newMag = Math.min(types_1.CONFIG.maxMagnification, newMag);
        this.viewState.centerRe = newCenterRe;
        this.viewState.centerIm = newCenterIm;
        this.viewState.magnification = newMag;
    }
    /** Set Julia mode with a given c-value. */
    setJuliaMode(cRe, cIm) {
        this.pushHistory();
        this.viewState.setMode = 'julia';
        this.viewState.juliaCRe = cRe;
        this.viewState.juliaCIm = cIm;
    }
    /** Switch back to Mandelbrot mode. */
    setMandelbrotMode() {
        this.pushHistory();
        this.viewState.setMode = 'mandelbrot';
    }
    /** Set palette index. */
    setPalette(index) {
        this.viewState.paletteIndex = index;
    }
    /** Set color offset for palette cycling. */
    setColorOffset(offset) {
        this.viewState.colorOffset = ((offset % 1) + 1) % 1;
    }
    /** Get a serializable copy of the current view state. */
    serialize() {
        return { ...this.viewState };
    }
    /** Restore from a serialized view state. */
    restore(state) {
        this.pushHistory();
        this.viewState = { ...state };
    }
    /** Load view state from URL hash if present. */
    loadFromHash() {
        const hash = window.location.hash.slice(1);
        if (!hash)
            return false;
        try {
            const params = new URLSearchParams(hash);
            this.viewState.centerRe = parseFloat(params.get('cr') ?? '0');
            this.viewState.centerIm = parseFloat(params.get('ci') ?? '0');
            this.viewState.magnification = Math.max(types_1.CONFIG.minMagnification, parseFloat(params.get('mag') ?? '3'));
            this.viewState.paletteIndex = parseInt(params.get('pal') ?? '0', 10);
            this.viewState.colorOffset = parseFloat(params.get('coff') ?? '0');
            if (params.get('mode') === 'julia') {
                this.viewState.setMode = 'julia';
                this.viewState.juliaCRe = parseFloat(params.get('jcr') ?? '-0.7269');
                this.viewState.juliaCIm = parseFloat(params.get('jci') ?? '0.1889');
            }
            else {
                this.viewState.setMode = 'mandelbrot';
            }
            return true;
        }
        catch {
            return false;
        }
    }
    /** Save current view state to URL hash. */
    saveToHash() {
        const p = new URLSearchParams();
        p.set('cr', this.viewState.centerRe.toPrecision(15));
        p.set('ci', this.viewState.centerIm.toPrecision(15));
        p.set('mag', this.viewState.magnification.toExponential(6));
        p.set('pal', String(this.viewState.paletteIndex));
        p.set('coff', this.viewState.colorOffset.toFixed(4));
        if (this.viewState.setMode === 'julia') {
            p.set('mode', 'julia');
            p.set('jcr', this.viewState.juliaCRe.toPrecision(15));
            p.set('jci', this.viewState.juliaCIm.toPrecision(15));
        }
        else {
            p.set('mode', 'mandelbrot');
        }
        window.location.hash = p.toString();
    }
    /** Format magnification for display. */
    formatMagnification() {
        const m = this.viewState.magnification;
        if (m >= 1e6)
            return `×${m.toExponential(2).replace('e+', 'e')}`;
        if (m >= 1000)
            return `×${Math.round(m).toLocaleString()}`;
        return `×${m.toFixed(2)}`;
    }
    /** Format center coordinates for display. */
    formatCenter() {
        const prec = Math.min(15, Math.max(6, Math.floor(Math.log10(this.viewState.magnification)) + 4));
        return `${this.viewState.centerRe.toPrecision(prec)}, ${this.viewState.centerIm.toPrecision(prec)}i`;
    }
}
exports.Navigation = Navigation;
function viewsEqual(a, b) {
    return (a.centerRe === b.centerRe &&
        a.centerIm === b.centerIm &&
        a.magnification === b.magnification &&
        a.paletteIndex === b.paletteIndex &&
        Math.abs(a.colorOffset - b.colorOffset) < 0.001 &&
        a.setMode === b.setMode &&
        a.juliaCRe === b.juliaCRe &&
        a.juliaCIm === b.juliaCIm);
}
};

// ── module: src/ui.ts ──
__mods["src/ui.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.UIManager = void 0;
const types_1 = require("./types");
const palettes_1 = require("./palettes");
// ─── UI Manager ────────────────────────────────────────────────────────
class UIManager {
    constructor() {
        this.juliaPreviewCanvas = null;
        this.hudVisible = true;
        this.hideTimeout = null;
        this.lastFpsTime = performance.now();
        this.frameCount = 0;
        this.currentFps = 60;
        this.hudEl = document.getElementById('hud');
        this.magEl = document.getElementById('mag-display');
        this.centerEl = document.getElementById('center-display');
        this.iterEl = document.getElementById('iter-display');
        this.fpsEl = document.getElementById('fps-display');
        this.modeEl = document.getElementById('mode-display');
        this.paletteContainer = document.getElementById('palette-container');
        this.zoomBoxCanvas = document.getElementById('zoom-box-canvas');
        this.colorSlider = document.getElementById('color-slider');
        this.iterSlider = document.getElementById('iter-slider');
        this.resetBtn = document.getElementById('reset-btn');
        this.backBtn = document.getElementById('back-btn');
        this.bookmarkBtn = document.getElementById('bookmark-btn');
        this.bookmarksPanel = document.getElementById('bookmarks-panel');
        // Build palette swatches
        this.buildPaletteSwatches();
        // Setup event listeners
        this.setupEventListeners();
    }
    buildPaletteSwatches() {
        palettes_1.PALETTES.forEach((palette, i) => {
            const chip = document.createElement('div');
            chip.className = 'palette-chip';
            chip.dataset.index = String(i);
            chip.title = palette.name;
            // Create gradient swatch
            let gradient = 'linear-gradient(to right, ';
            const stops = palette.stops.map(s => {
                return `rgb(${Math.round(s[1] * 255)},${Math.round(s[2] * 255)},${Math.round(s[3] * 255)}) ${s[0] * 100}%`;
            });
            gradient += stops.join(',') + ')';
            chip.style.background = gradient;
            this.paletteContainer.appendChild(chip);
        });
    }
    setupEventListeners() {
        // Palette selection
        this.paletteContainer.addEventListener('click', (e) => {
            const chip = e.target.closest('.palette-chip');
            if (!chip)
                return;
            const index = parseInt(chip.dataset.index, 10);
            this.onPaletteSelect?.(index);
        });
        // Color offset slider
        this.colorSlider.addEventListener('input', () => {
            const val = parseFloat(this.colorSlider.value);
            this.onColorOffsetChange?.(val);
        });
        // Iteration override slider
        this.iterSlider.addEventListener('input', () => {
            const val = parseInt(this.iterSlider.value, 10);
            this.onIterOverrideChange?.(val);
        });
        // Reset button
        this.resetBtn.addEventListener('click', () => {
            this.onReset?.();
        });
        // Back button
        this.backBtn.addEventListener('click', () => {
            this.onBack?.();
        });
        // Bookmark button
        this.bookmarkBtn.addEventListener('click', () => {
            this.onBookmark?.();
        });
        // Show HUD on mouse move, hide after delay
        document.addEventListener('mousemove', () => this.showHud());
        document.addEventListener('keydown', () => this.showHud());
    }
    showHud() {
        if (!this.hudVisible) {
            this.hudEl.style.opacity = '1';
            this.hudVisible = true;
        }
        if (this.hideTimeout !== null) {
            clearTimeout(this.hideTimeout);
        }
        this.hideTimeout = window.setTimeout(() => {
            this.hudEl.style.opacity = '0.3';
            this.hudVisible = false;
        }, types_1.CONFIG.hudHideDelayMs);
    }
    // ─── Update Methods ──────────────────────────────────────────────
    updateMag(mag) {
        this.magEl.textContent = mag;
    }
    updateCenter(center) {
        this.centerEl.textContent = center;
    }
    updateIterations(iter, override) {
        const text = override ? `${iter} (override)` : `iterations: ${iter}`;
        this.iterEl.textContent = text;
    }
    updateFps() {
        this.frameCount++;
        const now = performance.now();
        if (now - this.lastFpsTime >= 500) {
            this.currentFps = Math.round(this.frameCount / ((now - this.lastFpsTime) / 1000));
            this.fpsEl.textContent = `${this.currentFps} fps`;
            this.frameCount = 0;
            this.lastFpsTime = now;
        }
    }
    updateMode(mode, juliaC) {
        if (mode === 'julia') {
            this.modeEl.textContent = `Julia (${juliaC?.re.toFixed(4)}, ${juliaC?.im.toFixed(4)}i)`;
        }
        else {
            this.modeEl.textContent = 'Mandelbrot';
        }
    }
    updateIterSlider(maxAuto) {
        // Update slider max to reflect auto-computed iterations
        const currentVal = parseInt(this.iterSlider.value, 10);
        if (currentVal <= 0 || currentVal > types_1.CONFIG.maxIterationsCap) {
            this.iterSlider.value = String(Math.min(maxAuto, types_1.CONFIG.maxIterationsCap));
        }
        this.iterSlider.max = String(types_1.CONFIG.maxIterationsCap);
    }
    setActivePalette(index) {
        const chips = this.paletteContainer.querySelectorAll('.palette-chip');
        chips.forEach((chip, i) => {
            chip.classList.toggle('active', i === index);
        });
    }
    // ─── Zoom Box Overlay ──────────────────────────────────────────────
    drawZoomBox(x0, y0, x1, y1) {
        const ctx = this.zoomBoxCanvas.getContext('2d');
        ctx.clearRect(0, 0, this.zoomBoxCanvas.width, this.zoomBoxCanvas.height);
        if (x1 <= x0 || y1 <= y0)
            return;
        // Inverted-color border effect
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.9)';
        ctx.lineWidth = 2;
        ctx.setLineDash([6, 4]);
        ctx.strokeRect(x0, y0, x1 - x0, y1 - y0);
        ctx.setLineDash([]);
        // Semi-transparent fill
        ctx.fillStyle = 'rgba(255, 255, 255, 0.08)';
        ctx.fillRect(x0, y0, x1 - x0, y1 - y0);
        // Corner markers
        const cornerLen = 12;
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.9)';
        ctx.lineWidth = 2;
        this.drawCorner(ctx, x0, y0, 1, 1);
        this.drawCorner(ctx, x1, y0, -1, 1);
        this.drawCorner(ctx, x0, y1, 1, -1);
        this.drawCorner(ctx, x1, y1, -1, -1);
        // Label with dimensions
        const label = `${Math.round(x1 - x0)}×${Math.round(y1 - y0)}`;
        ctx.font = '11px monospace';
        ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
        const metrics = ctx.measureText(label);
        const lx = x0 + 4;
        const ly = y0 - 6;
        ctx.fillRect(lx - 2, ly - 12, metrics.width + 4, 16);
        ctx.fillStyle = 'rgba(0, 0, 0, 0.8)';
        ctx.fillText(label, lx, ly);
    }
    drawCorner(ctx, x, y, dx, dy) {
        const len = 12;
        ctx.beginPath();
        ctx.moveTo(x + dx * len, y);
        ctx.lineTo(x, y);
        ctx.lineTo(x, y + dy * len);
        ctx.stroke();
    }
    clearZoomBox() {
        const ctx = this.zoomBoxCanvas.getContext('2d');
        ctx.clearRect(0, 0, this.zoomBoxCanvas.width, this.zoomBoxCanvas.height);
    }
    // ─── Julia Preview ────────────────────────────────────────────────
    showJuliaPreview() {
        if (!this.juliaPreviewCanvas) {
            const container = document.getElementById('julia-preview-container');
            this.juliaPreviewCanvas = document.createElement('canvas');
            this.juliaPreviewCanvas.id = 'julia-preview';
            this.juliaPreviewCanvas.width = types_1.CONFIG.juliaPreviewSize;
            this.juliaPreviewCanvas.height = types_1.CONFIG.juliaPreviewSize;
            container.appendChild(this.juliaPreviewCanvas);
        }
        this.juliaPreviewCanvas.parentElement.style.display = 'block';
    }
    hideJuliaPreview() {
        const container = document.getElementById('julia-preview-container');
        if (container)
            container.style.display = 'none';
    }
    // ─── Bookmarks Panel ──────────────────────────────────────────────
    showBookmarksPanel(bookmarks) {
        this.bookmarksPanel.innerHTML = '';
        if (bookmarks.length === 0) {
            this.bookmarksPanel.innerHTML = '<div class="bookmark-empty">No bookmarks saved</div>';
        }
        else {
            bookmarks.forEach((bm, i) => {
                const item = document.createElement('div');
                item.className = 'bookmark-item';
                item.textContent = `${bm.name} (${new Date(bm.timestamp).toLocaleTimeString()})`;
                item.addEventListener('click', () => this.onBookmarkRestore?.(i));
                this.bookmarksPanel.appendChild(item);
            });
        }
        this.bookmarksPanel.style.display = 'block';
    }
    hideBookmarksPanel() {
        this.bookmarksPanel.style.display = 'none';
    }
    // ─── Resize ──────────────────────────────────────────────────────
    resize(w, h) {
        this.zoomBoxCanvas.width = w;
        this.zoomBoxCanvas.height = h;
    }
}
exports.UIManager = UIManager;
};

// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen3.6-27b-mtp","provider":"lmstudio","persona":"typescript","sessionId":"d79c8b2f-2221-495d-b031-08e14386a8a3","tokensIn":5106575,"tokensOut":76300,"tokensTotal":5182875,"turns":90,"toolCalls":89,"failedToolCalls":2,"timestamp":"2026-07-30T21:28:24.777Z"} -->