← Fractal Explorer results

Fractal Explorer

Qwen 3.6 27B MTP GGUF UD Q5 K XL · 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 53,406 bytes · SHA-256 4d72a4826a94
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Fractal Explorer</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, sans-serif; }
#app { position: relative; width: 100%; height: 100%; }
#fractalCanvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; cursor: crosshair; }
#overlayCanvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; }
#overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; }
#hud {
  position: absolute; top: 0; left: 0; right: 0; pointer-events: none; z-index: 10;
  transition: opacity 0.4s ease;
}
#hud.hidden { opacity: 0; }
#hud-top {
  display: flex; justify-content: space-between; padding: 12px 16px;
  font-size: 13px; color: #fff; text-shadow: 0 1px 4px rgba(0,0,0,0.8);
  pointer-events: none;
}
#hud-bottom {
  position: absolute; bottom: 12px; left: 50%; transform: translateX(-50%);
  display: flex; align-items: center; gap: 8px; padding: 8px 16px;
  background: rgba(20,20,30,0.65); backdrop-filter: blur(12px);
  border-radius: 10px; border: 1px solid rgba(255,255,255,0.1);
  pointer-events: auto; flex-wrap: wrap; justify-content: center;
}
#hud-bottom button {
  background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2);
  color: #fff; padding: 4px 10px; border-radius: 6px; cursor: pointer; font-size: 14px;
  transition: background 0.15s;
}
#hud-bottom button:hover { background: rgba(255,255,255,0.25); }
#hud-bottom button.active { background: rgba(100,150,255,0.4); border-color: rgba(100,150,255,0.6); }
#palette-bar { display: flex; gap: 4px; }
.palette-chip {
  width: 24px; height: 24px; border-radius: 4px; cursor: pointer;
  border: 2px solid transparent; transition: border-color 0.15s;
}
.palette-chip:hover, .palette-chip.active { border-color: #fff; }
#slider-row { display: flex; align-items: center; gap: 8px; font-size: 11px; color: #ccc; }
#slider-row label { display: flex; align-items: center; gap: 4px; white-space: nowrap; }
#slider-row input[type="range"] { width: 80px; height: 4px; accent-color: #6af; }
#fps-label { font-size: 11px; color: #aaa; margin-left: 8px; }
#bookmark-list {
  position: absolute; bottom: 56px; left: 50%; transform: translateX(-50%);
  background: rgba(20,20,30,0.8); backdrop-filter: blur(12px);
  border-radius: 8px; border: 1px solid rgba(255,255,255,0.15);
  padding: 6px; display: flex; flex-direction: column; gap: 4px;
  pointer-events: auto; min-width: 180px;
}
#bookmark-list .bm-item {
  display: flex; justify-content: space-between; align-items: center;
  padding: 4px 8px; border-radius: 4px; cursor: pointer; font-size: 12px; color: #ddd;
}
#bookmark-list .bm-item:hover { background: rgba(255,255,255,0.1); }
#bookmark-list .bm-item button {
  background: none; border: none; color: #f66; cursor: pointer; font-size: 14px; padding: 0 4px;
}
</style>
</head>
<body>
<canvas id="fractalCanvas"></canvas>
<div id="overlay"></div>
<div id="hud">
  <div id="hud-top">
    <div id="zoom-label">×1.0</div>
    <div id="center-label">(0.000, 0.000)</div>
  </div>
  <div id="hud-bottom">
    <button id="btn-reset" title="Reset view (R)">⟲</button>
    <button id="btn-back" title="Zoom history back (Esc)">←</button>
    <button id="btn-julia" title="Julia mode (J)">J</button>
    <button id="btn-bookmark-save" title="Save bookmark (S)">★</button>
    <div id="palette-bar"></div>
    <div id="slider-row">
      <label>Offset <input type="range" id="slider-offset" min="0" max="1" step="0.001" value="0"></label>
      <label>Max Iters <input type="range" id="slider-iters" min="50" max="10000" step="50" value="256"></label>
    </div>
    <div id="bookmark-list"></div>
    <div id="fps-label">60 fps</div>
  </div>
</div>
<canvas id="overlayCanvas"></canvas>
<div id="julia-preview" style="position:absolute;bottom:80px;right:12px;width:200px;height:200px;background:rgba(0,0,0,0.7);border:1px solid rgba(255,255,255,0.2);border-radius:8px;overflow:hidden;pointer-events:none;display:none;">
  <canvas id="julia-preview-canvas" width="200" height="200" style="width:100%;height:100%;"></canvas>
  <div id="julia-preview-label" style="position:absolute;bottom:4px;left:0;right:0;text-align:center;font-size:10px;color:#ccc;text-shadow:0 1px 3px #000;">Julia Preview</div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./types":"src/types.ts","./state":"src/state.ts","./renderer":"src/renderer.ts","./input":"src/input.ts","./ui":"src/ui.ts"},"src/palettes.ts":{"./types":"src/types.ts"},"src/state.ts":{"./types":"src/types.ts","./palettes":"src/palettes.ts"},"src/renderer.ts":{"./types":"src/types.ts","./palettes":"src/palettes.ts"},"src/input.ts":{"./types":"src/types.ts","./state":"src/state.ts"},"src/ui.ts":{"./types":"src/types.ts","./palettes":"src/palettes.ts","./state":"src/state.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 state_1 = require("./state");
const renderer_1 = require("./renderer");
const input_1 = require("./input");
const ui_1 = require("./ui");
class FractalApp {
    constructor() {
        this.state = (0, state_1.createInitialState)();
        this.lastCursorPos = { x: 0, y: 0 };
        this.juliaPreviewTimer = null;
        this.lastFrameTime = performance.now();
        this.fpsAccum = 0;
        this.fpsCount = 0;
        this.displayedFps = 60;
        this.pendingRender = false;
        this.renderTimer = null;
        this.canvas = document.getElementById('fractalCanvas');
        this.overlayCanvas = document.getElementById('overlayCanvas');
        this.renderer = (0, renderer_1.createRenderer)(this.canvas);
        // Setup resize handler
        window.addEventListener('resize', () => this.resize());
        // Setup Julia preview panel
        this.juliaPreviewEl = document.getElementById('julia-preview');
        this.juliaPreviewCanvas = document.getElementById('julia-preview-canvas');
        // Setup input
        const callbacks = {
            onZoomRect: (x1, y1, x2, y2) => this.handleZoomRect(x1, y1, x2, y2),
            onPan: (dRe, dIm) => this.handlePan(dRe, dIm),
            onWheelZoom: (x, y, factor) => this.handleWheelZoom(x, y, factor),
            onCursorMove: (x, y) => this.handleCursorMove(x, y),
            onJuliaClick: (x, y) => this.handleJuliaClick(x, y),
        };
        this.input = (0, input_1.createInputManager)(this.canvas, this.overlayCanvas, callbacks);
        // Now resize after all components are ready
        this.resize();
        // Setup HUD
        this.hud = new ui_1.HUDManager();
        this.hud.onPaletteChange = (i) => { this.state.paletteIndex = i; this.scheduleRender(); };
        this.hud.onReset = () => this.handleReset();
        this.hud.onBack = () => this.handleBack();
        this.hud.onToggleJulia = () => this.toggleJulia();
        this.hud.onColorOffsetChange = (offset) => {
            this.state.colorOffset = offset;
            this.renderer.colorizeOnly((0, state_1.getPalette)(this.state), this.state.colorOffset);
        };
        this.hud.onManualItersChange = (val) => {
            this.state.manualMaxIterations = val;
            this.state.view.maxIterations = (0, state_1.computeMaxIterations)(this.state.view.scale, val);
            this.scheduleRender();
        };
        this.hud.onSaveBookmark = (name) => {
            (0, state_1.addBookmark)(this.state, name);
            this.hud.setBookmarks(this.state.bookmarks);
            this.hud.showNow();
        };
        this.hud.onLoadBookmark = (id) => {
            if ((0, state_1.loadBookmark)(this.state, id)) {
                this.state.view.maxIterations = (0, state_1.computeMaxIterations)(this.state.view.scale, this.state.manualMaxIterations);
                this.scheduleRender();
                this.hud.showNow();
            }
        };
        this.hud.onDeleteBookmark = (id) => {
            (0, state_1.deleteBookmark)(this.state, id);
            this.hud.setBookmarks(this.state.bookmarks);
        };
        // Hide bookmark list when clicking elsewhere
        document.addEventListener('click', (e) => {
            const bmList = document.getElementById('bookmark-list');
            const bmBtn = document.getElementById('btn-bookmark-save');
            if (bmList && bmList.style.display !== 'none') {
                if (!bmList.contains(e.target) && bmBtn && bmBtn !== e.target) {
                    bmList.style.display = 'none';
                }
            }
        });
        // Keyboard shortcuts
        window.addEventListener('keydown', (e) => {
            if (e.key === 'r' || e.key === 'R') {
                this.handleReset();
            }
            if (e.key === 'Escape') {
                this.handleBack();
            }
            if (e.key === 's' || e.key === 'S') {
                if (!e.ctrlKey) {
                    this.hud.showSaveBookmarkDialog();
                }
            }
        });
        // Check URL hash for shared view
        if (location.hash) {
            const hashView = (0, state_1.hashToView)(location.hash);
            if (hashView) {
                this.state.view = hashView;
            }
        }
        // Initial render
        this.state.view.maxIterations = (0, state_1.computeMaxIterations)(this.state.view.scale, this.state.manualMaxIterations);
        this.input.setView(this.state.view);
        this.scheduleRender();
        this.hud.resetIdleTimer();
        // Start FPS counter
        this.startFpsLoop();
        // Start animation loop for Julia preview
        this.startAnimLoop();
    }
    resize() {
        const dpr = Math.min(window.devicePixelRatio || 1, 2);
        const cw = this.canvas.clientWidth || window.innerWidth;
        const ch = this.canvas.clientHeight || window.innerHeight;
        const w = Math.max(1, Math.floor(cw * dpr));
        const h = Math.max(1, Math.floor(ch * dpr));
        this.canvas.width = w;
        this.canvas.height = h;
        this.overlayCanvas.width = w;
        this.overlayCanvas.height = h;
        this.renderer.resize(w, h);
        this.input.resize(w, h);
    }
    scheduleRender() {
        if (this.renderTimer)
            clearTimeout(this.renderTimer);
        this.pendingRender = true;
        this.renderTimer = window.setTimeout(() => {
            this.pendingRender = false;
            this.startRender();
        }, 50); // Debounce rapid interactions
    }
    async startRender() {
        const palette = (0, state_1.getPalette)(this.state);
        const juliaC = this.state.julia.enabled ? this.state.julia.c : null;
        await this.renderer.render(this.state.view, palette, this.state.colorOffset, juliaC, (progress) => { });
        this.hud.resetIdleTimer();
    }
    handleZoomRect(x1, y1, x2, y2) {
        const w = this.renderer.width;
        const h = this.renderer.height;
        const aspectRatio = w / h;
        // Push current view to history
        (0, state_1.pushZoomHistory)(this.state, this.state.view);
        // Compute the complex plane region for the selected rectangle
        const c1 = (0, state_1.screenToComplex)(x1, y1, this.state.view, w, h, aspectRatio);
        const c2 = (0, state_1.screenToComplex)(x2, y2, this.state.view, w, h, aspectRatio);
        const newRe = (c1.re + c2.re) / 2;
        const newIm = (c1.im + c2.im) / 2;
        // The old view covers a region of 4/scale in the smaller dimension
        const oldRange = 4 / this.state.view.scale;
        const newRangeX = Math.abs(c2.re - c1.re);
        const newRangeY = Math.abs(c2.im - c1.im);
        // Use the smaller range to zoom "into" the rectangle
        const newRange = Math.min(newRangeX, newRangeY / aspectRatio);
        let newScale = this.state.view.scale * (oldRange / newRange);
        // Clamp to max magnification
        if (newScale > types_1.TUNABLES.maxMagnification) {
            newScale = types_1.TUNABLES.maxMagnification;
            console.warn(`Reached max magnification (${types_1.TUNABLES.maxMagnification.toExponential()}). Double precision degrades beyond this point.`);
        }
        // Clamp to precision warning threshold
        if (newScale > types_1.TUNABLES.precisionWarningMagnification) {
            console.warn(`Approaching precision limits at ×${newScale.toExponential()}. Features may appear as noise.`);
        }
        this.state.view.re = newRe;
        this.state.view.im = newIm;
        this.state.view.scale = newScale;
        this.state.view.maxIterations = (0, state_1.computeMaxIterations)(newScale, this.state.manualMaxIterations);
        this.input.setView(this.state.view);
        this.hud.resetIdleTimer();
        this.scheduleRender();
        // Update URL hash
        history.replaceState(null, '', (0, state_1.viewToHash)(this.state.view));
    }
    handlePan(dRe, dIm) {
        (0, state_1.pushZoomHistory)(this.state, this.state.view);
        this.state.view.re += dRe;
        this.state.view.im += dIm;
        this.input.setView(this.state.view);
        this.hud.resetIdleTimer();
        this.scheduleRender();
    }
    handleWheelZoom(x, y, factor) {
        const w = this.renderer.width;
        const h = this.renderer.height;
        const aspectRatio = w / h;
        (0, state_1.pushZoomHistory)(this.state, this.state.view);
        // Complex coordinate at cursor before zoom
        const cursorC = (0, state_1.screenToComplex)(x, y, this.state.view, w, h, aspectRatio);
        let newScale = this.state.view.scale * factor;
        newScale = Math.max(0.1, Math.min(newScale, types_1.TUNABLES.maxMagnification));
        if (newScale >= types_1.TUNABLES.maxMagnification) {
            console.warn(`Reached max magnification (${types_1.TUNABLES.maxMagnification.toExponential()}).`);
        }
        this.state.view.scale = newScale;
        // Adjust center so cursor point stays at same screen position
        const newCursorC = (0, state_1.screenToComplex)(x, y, this.state.view, w, h, aspectRatio);
        this.state.view.re += (cursorC.re - newCursorC.re);
        this.state.view.im += (cursorC.im - newCursorC.im);
        this.state.view.maxIterations = (0, state_1.computeMaxIterations)(newScale, this.state.manualMaxIterations);
        this.input.setView(this.state.view);
        this.hud.resetIdleTimer();
        this.scheduleRender();
        history.replaceState(null, '', (0, state_1.viewToHash)(this.state.view));
    }
    handleCursorMove(x, y) {
        this.lastCursorPos = { x, y };
        this.hud.resetIdleTimer();
        // If in Julia mode, show crosshair
        if (this.state.julia.enabled) {
            this.input.drawCrosshair(x, y);
        }
        // Julia hover preview (when not in Julia mode)
        if (!this.state.julia.enabled) {
            if (this.juliaPreviewTimer)
                clearTimeout(this.juliaPreviewTimer);
            this.juliaPreviewTimer = window.setTimeout(() => {
                const w = this.renderer.width;
                const h = this.renderer.height;
                const aspectRatio = w / h;
                const c = (0, state_1.screenToComplex)(x, y, this.state.view, w, h, aspectRatio);
                // Only show preview for exterior points (not interior)
                const iter = (0, renderer_1.mandelbrotIteration)(c.re, c.im, this.state.view.maxIterations, null);
                if (iter <= this.state.view.maxIterations) {
                    this.juliaPreviewEl.style.display = 'block';
                    const label = document.getElementById('julia-preview-label');
                    if (label)
                        label.textContent = `Julia: c = (${c.re.toFixed(6)}, ${c.im.toFixed(6)})`;
                    this.renderer.renderJuliaPreview(c, (0, state_1.getPalette)(this.state), this.state.colorOffset, this.state.view.maxIterations, this.juliaPreviewCanvas, 200);
                }
                else {
                    this.juliaPreviewEl.style.display = 'none';
                }
            }, 100);
        }
        else {
            this.juliaPreviewEl.style.display = 'none';
        }
    }
    handleJuliaClick(x, y) {
        if (this.state.julia.enabled)
            return; // Already in Julia mode, toggle instead
        const w = this.renderer.width;
        const h = this.renderer.height;
        const aspectRatio = w / h;
        const c = (0, state_1.screenToComplex)(x, y, this.state.view, w, h, aspectRatio);
        // Don't toggle Julia if the point is inside the set
        const iter = (0, renderer_1.mandelbrotIteration)(c.re, c.im, this.state.view.maxIterations, null);
        if (iter > this.state.view.maxIterations)
            return; // Interior point
        this.state.julia.c = { re: c.re, im: c.im };
        this.state.julia.enabled = true;
        this.scheduleRender();
        this.hud.showNow();
    }
    toggleJulia() {
        this.state.julia.enabled = !this.state.julia.enabled;
        this.scheduleRender();
        this.hud.showNow();
    }
    handleReset() {
        (0, state_1.pushZoomHistory)(this.state, this.state.view);
        this.state.view = { re: -0.5, im: 0, scale: 1, maxIterations: types_1.TUNABLES.maxIterationsBase };
        this.state.julia.enabled = false;
        this.state.manualMaxIterations = null;
        this.state.view.maxIterations = (0, state_1.computeMaxIterations)(1, null);
        this.input.setView(this.state.view);
        this.scheduleRender();
        this.hud.showNow();
    }
    handleBack() {
        const prev = (0, state_1.popZoomHistory)(this.state);
        if (prev) {
            this.state.view = prev;
            this.state.view.maxIterations = (0, state_1.computeMaxIterations)(prev.scale, this.state.manualMaxIterations);
            this.input.setView(this.state.view);
            this.scheduleRender();
            this.hud.showNow();
        }
    }
    startFpsLoop() {
        const loop = () => {
            const now = performance.now();
            const dt = now - this.lastFrameTime;
            this.lastFrameTime = now;
            this.fpsAccum += dt;
            this.fpsCount++;
            if (this.fpsAccum >= 1000) {
                this.displayedFps = Math.round(this.fpsCount * 1000 / this.fpsAccum);
                this.fpsAccum = 0;
                this.fpsCount = 0;
            }
            requestAnimationFrame(loop);
        };
        requestAnimationFrame(loop);
    }
    startAnimLoop() {
        const loop = () => {
            // Update HUD
            const autoMaxIters = (0, state_1.computeMaxIterations)(this.state.view.scale, null);
            this.hud.update(this.state.view, this.state.paletteIndex, this.state.colorOffset, this.state.julia, this.state.bookmarks, this.displayedFps, autoMaxIters);
            requestAnimationFrame(loop);
        };
        requestAnimationFrame(loop);
    }
}
function main() {
    const app = new FractalApp();
    window.fractal = {
        app,
        iterationsAt(re, im) {
            const state = app['state'];
            const maxIter = state.view.maxIterations;
            const juliaC = state.julia.enabled ? state.julia.c : null;
            return (0, renderer_1.iterationsAt)(re, im, maxIter, juliaC);
        },
        setView(params) {
            const state = app['state'];
            const prev = { ...state.view };
            state.view.re = params.re;
            state.view.im = params.im;
            state.view.scale = params.scale;
            if (params.maxIterations !== undefined) {
                state.manualMaxIterations = params.maxIterations;
            }
            state.view.maxIterations = (0, state_1.computeMaxIterations)(state.view.scale, state.manualMaxIterations);
            app.input.setView(state.view);
            app.scheduleRender();
        },
        getState() {
            return JSON.parse(JSON.stringify(app['state']));
        },
    };
}
main();
};

// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
// Tunable constants and types for the Fractal Explorer
Object.defineProperty(exports, "__esModule", { value: true });
exports.TUNABLES = void 0;
exports.TUNABLES = Object.freeze({
    // Iteration control
    maxIterationsBase: 256,
    maxIterationsScale: 10, // additional iterations per log10(magnification)
    maxManualIterations: 20000,
    bailoutRadius: 256, // |z|^2 > bailoutRadius → escaped
    // Zoom
    wheelZoomFactor: 1.15,
    dragZoomMinPixels: 5,
    maxMagnification: 1e14,
    // Progressive rendering
    progressiveSteps: 3, // number of refinement passes (1/4, 1/2, 1)
    renderTileWidth: 64,
    renderTileHeight: 64,
    // Precision
    precisionWarningMagnification: 1e13,
    // HUD
    hudIdleTimeoutMs: 3000,
    // Worker
    workerCount: 0, // 0 = main thread only, >0 = use workers
});
};

// ── 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.samplePalette = samplePalette;
exports.PALETTES = [
    {
        name: 'Fire',
        stops: [
            { pos: 0.0, r: 0, g: 0, b: 0 },
            { pos: 0.15, r: 30, g: 0, b: 50 },
            { pos: 0.3, r: 150, g: 0, b: 0 },
            { pos: 0.5, r: 255, g: 80, b: 0 },
            { pos: 0.7, r: 255, g: 200, b: 0 },
            { pos: 0.85, r: 255, g: 255, b: 150 },
            { pos: 1.0, r: 255, g: 255, b: 255 },
        ],
        thumbnail: 'linear-gradient(90deg,#000,#1e0032,#960000,#ff5000,#ffc800,#ffff96,#fff)',
    },
    {
        name: 'Ocean',
        stops: [
            { pos: 0.0, r: 0, g: 0, b: 20 },
            { pos: 0.2, r: 0, g: 20, b: 80 },
            { pos: 0.4, r: 0, g: 80, b: 160 },
            { pos: 0.6, r: 0, g: 150, b: 200 },
            { pos: 0.8, r: 100, g: 200, b: 255 },
            { pos: 1.0, r: 200, g: 240, b: 255 },
        ],
        thumbnail: 'linear-gradient(90deg,#000014,#001450,#0050a0,#0096c8,#64c8ff,#c8f0ff)',
    },
    {
        name: 'Mono',
        stops: [
            { pos: 0.0, r: 0, g: 0, b: 0 },
            { pos: 0.5, r: 180, g: 180, b: 180 },
            { pos: 1.0, r: 255, g: 255, b: 255 },
        ],
        thumbnail: 'linear-gradient(90deg,#000,#b4b4b4,#fff)',
    },
    {
        name: 'Psychedelic',
        stops: [
            { pos: 0.0, r: 128, g: 0, b: 128 },
            { pos: 0.2, r: 0, g: 128, b: 128 },
            { pos: 0.4, r: 0, g: 128, b: 0 },
            { pos: 0.6, r: 128, g: 128, b: 0 },
            { pos: 0.8, r: 128, g: 0, b: 0 },
            { pos: 1.0, r: 128, g: 0, b: 128 },
        ],
        thumbnail: 'linear-gradient(90deg,#800080,#008080,#008000,#808000,#800000,#800080)',
    },
    {
        name: 'Electric',
        stops: [
            { pos: 0.0, r: 10, g: 0, b: 30 },
            { pos: 0.25, r: 30, g: 0, b: 120 },
            { pos: 0.5, r: 0, g: 100, b: 200 },
            { pos: 0.75, r: 0, g: 220, b: 100 },
            { pos: 1.0, r: 200, g: 255, b: 200 },
        ],
        thumbnail: 'linear-gradient(90deg,#0a001e,#1e0078,#0064c8,#00dc64,#c8ffc8)',
    },
];
// Given a palette and a t in [0,1], return [r, g, b] with smooth interpolation.
function samplePalette(palette, t, offset) {
    // Apply cyclic offset
    t = ((t + offset) % 1.0 + 1.0) % 1.0;
    const stops = palette.stops;
    if (stops.length === 1)
        return [stops[0].r, stops[0].g, stops[0].b];
    // Find bracketing stops
    let i = 0;
    while (i < stops.length - 1 && stops[i + 1].pos < t)
        i++;
    if (i >= stops.length - 1) {
        const s = stops[stops.length - 1];
        return [s.r, s.g, s.b];
    }
    const a = stops[i];
    const b = stops[i + 1];
    const range = b.pos - a.pos;
    const frac = range > 0 ? (t - a.pos) / range : 0;
    // Smoothstep for nicer gradients
    const f = frac * frac * (3 - 2 * frac);
    return [
        Math.round(a.r + (b.r - a.r) * f),
        Math.round(a.g + (b.g - a.g) * f),
        Math.round(a.b + (b.b - a.b) * f),
    ];
}
};

// ── module: src/state.ts ──
__mods["src/state.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.computeMaxIterations = computeMaxIterations;
exports.createInitialState = createInitialState;
exports.getPalette = getPalette;
exports.pushZoomHistory = pushZoomHistory;
exports.popZoomHistory = popZoomHistory;
exports.addBookmark = addBookmark;
exports.loadBookmark = loadBookmark;
exports.deleteBookmark = deleteBookmark;
exports.viewToHash = viewToHash;
exports.hashToView = hashToView;
exports.screenToComplex = screenToComplex;
exports.complexToScreen = complexToScreen;
const types_1 = require("./types");
const palettes_1 = require("./palettes");
function defaultView() {
    return { re: -0.5, im: 0, scale: 1, maxIterations: types_1.TUNABLES.maxIterationsBase };
}
function computeMaxIterations(scale, manualOverride) {
    if (manualOverride !== null)
        return manualOverride;
    const logScale = Math.log10(Math.max(scale, 1));
    return Math.round(types_1.TUNABLES.maxIterationsBase + types_1.TUNABLES.maxIterationsScale * logScale);
}
function createInitialState() {
    return {
        view: defaultView(),
        paletteIndex: 0,
        colorOffset: 0,
        julia: { enabled: false, c: { re: -0.7269, im: 0.1889 } },
        zoomHistory: [],
        bookmarks: loadBookmarks(),
        manualMaxIterations: null,
    };
}
function getPalette(state) {
    return palettes_1.PALETTES[state.paletteIndex % palettes_1.PALETTES.length];
}
function pushZoomHistory(state, view) {
    state.zoomHistory.push({ ...view });
    if (state.zoomHistory.length > 100)
        state.zoomHistory.shift();
}
function popZoomHistory(state) {
    if (state.zoomHistory.length === 0)
        return null;
    const prev = state.zoomHistory.pop();
    return prev;
}
const BOOKMARKS_KEY = 'fractal_explorer_bookmarks';
function loadBookmarks() {
    try {
        const raw = localStorage.getItem(BOOKMARKS_KEY);
        if (raw)
            return JSON.parse(raw);
    }
    catch { /* ignore */ }
    return [];
}
function saveBookmarks(bookmarks) {
    try {
        localStorage.setItem(BOOKMARKS_KEY, JSON.stringify(bookmarks));
    }
    catch { /* ignore */ }
}
let _bookmarkIdCounter = 0;
function addBookmark(state, name) {
    _bookmarkIdCounter++;
    const bm = {
        id: _bookmarkIdCounter,
        name,
        view: { ...state.view },
        paletteIndex: state.paletteIndex,
        colorOffset: state.colorOffset,
        juliaC: state.julia.c ? { ...state.julia.c } : null,
        isJulia: state.julia.enabled,
    };
    state.bookmarks.push(bm);
    saveBookmarks(state.bookmarks);
}
function loadBookmark(state, id) {
    const bm = state.bookmarks.find(b => b.id === id);
    if (!bm)
        return false;
    state.view = { ...bm.view };
    state.paletteIndex = bm.paletteIndex;
    state.colorOffset = bm.colorOffset;
    state.julia.enabled = bm.isJulia;
    if (bm.juliaC)
        state.julia.c = { ...bm.juliaC };
    return true;
}
function deleteBookmark(state, id) {
    state.bookmarks = state.bookmarks.filter(b => b.id !== id);
    saveBookmarks(state.bookmarks);
}
function viewToHash(view) {
    return '#' + encodeURIComponent(JSON.stringify({
        re: view.re, im: view.im, scale: view.scale, maxIterations: view.maxIterations,
    }));
}
function hashToView(hash) {
    try {
        if (!hash.startsWith('#'))
            return null;
        const decoded = decodeURIComponent(hash.slice(1));
        const parsed = JSON.parse(decoded);
        return {
            re: parsed.re ?? -0.5,
            im: parsed.im ?? 0,
            scale: parsed.scale ?? 1,
            maxIterations: parsed.maxIterations ?? 256,
        };
    }
    catch {
        return null;
    }
}
// Screen-to-complex coordinate conversion
function screenToComplex(sx, sy, view, canvasWidth, canvasHeight, aspectRatio) {
    const zoomFactor = 4 / (view.scale * Math.min(canvasWidth, canvasHeight));
    const w = canvasWidth * zoomFactor;
    const h = canvasHeight * zoomFactor;
    const offsetX = (canvasWidth - canvasHeight * aspectRatio) / 2;
    const offsetY = (canvasHeight - canvasWidth / aspectRatio) / 2;
    return {
        re: view.re + (sx - offsetX - canvasWidth / 2) * zoomFactor,
        im: view.im + (sy - offsetY - canvasHeight / 2) * zoomFactor,
    };
}
function complexToScreen(c, view, canvasWidth, canvasHeight, aspectRatio) {
    const zoomFactor = 4 / (view.scale * Math.min(canvasWidth, canvasHeight));
    const offsetX = (canvasWidth - canvasHeight * aspectRatio) / 2;
    const offsetY = (canvasHeight - canvasWidth / aspectRatio) / 2;
    return {
        x: (c.re - view.re) / zoomFactor + offsetX + canvasWidth / 2,
        y: (c.im - view.im) / zoomFactor + offsetY + canvasHeight / 2,
    };
}
};

// ── module: src/renderer.ts ──
__mods["src/renderer.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.mandelbrotIteration = mandelbrotIteration;
exports.iterationsAt = iterationsAt;
exports.createRenderer = createRenderer;
const types_1 = require("./types");
const palettes_1 = require("./palettes");
function mandelbrotIteration(cr, ci, maxIter, juliaC) {
    const bailout = types_1.TUNABLES.bailoutRadius;
    const bailoutLog = Math.log(bailout);
    let zr, zi;
    if (juliaC !== null) {
        zr = cr;
        zi = ci;
        const jcr = juliaC.re;
        const jci = juliaC.im;
        let iter = 0;
        while (zr * zr + zi * zi <= bailout && iter < maxIter) {
            const tmp = zr * zr - zi * zi + jcr;
            zi = 2 * zr * zi + jci;
            zr = tmp;
            iter++;
        }
        if (iter >= maxIter)
            return maxIter + 1;
        const log_zn = Math.log(zr * zr + zi * zi);
        return iter + 1 - Math.log(log_zn / bailoutLog) / Math.log(2);
    }
    else {
        zr = 0;
        zi = 0;
        let iter = 0;
        while (zr * zr + zi * zi <= bailout && iter < maxIter) {
            const tmp = zr * zr - zi * zi + cr;
            zi = 2 * zr * zi + ci;
            zr = tmp;
            iter++;
        }
        if (iter >= maxIter)
            return maxIter + 1;
        const log_zn = Math.log(zr * zr + zi * zi);
        return iter + 1 - Math.log(log_zn / bailoutLog) / Math.log(2);
    }
}
function iterationsAt(re, im, maxIter, juliaC) {
    const result = mandelbrotIteration(re, im, maxIter, juliaC);
    if (result > maxIter)
        return maxIter;
    return Math.max(0, Math.floor(result));
}
class Renderer {
    constructor(canvas) {
        this._w = 0;
        this._h = 0;
        this.imgData = null;
        this.pixels = new Uint8ClampedArray(0);
        this.iterBuffer = new Float32Array(0);
        this._cancelled = false;
        this._currentMaxIter = 0;
        this._juliaPreviewCanvas = null;
        this._juliaPreviewCtx = null;
        this._juliaPreviewImageData = null;
        this._juliaPreviewPixels = new Uint8ClampedArray(0);
        this._juliaPreviewW = 0;
        this._juliaPreviewH = 0;
        this.ctx = canvas.getContext('2d');
    }
    resize(w, h) {
        this._w = w;
        this._h = h;
        this.imgData = this.ctx.createImageData(w, h);
        this.pixels = this.imgData.data;
        this.iterBuffer = new Float32Array(w * h);
        this._cancelled = false;
    }
    get width() { return this._w; }
    get height() { return this._h; }
    cancel() { this._cancelled = true; }
    // Compute a row of pixels at given resolution and upsample
    computeRows(view, palette, colorOffset, juliaC, maxIter, yStart, yEnd, pixels, iterBuf, w, h, zoomFactor, offsetX, offsetY, stepDown) {
        for (let sy = yStart; sy < yEnd && sy < h; sy += stepDown) {
            if (this._cancelled)
                return;
            const ci = view.im + (sy - offsetY - h / 2) * zoomFactor;
            const row = sy * w;
            for (let sx = 0; sx < w; sx++) {
                const cr = view.re + (sx - offsetX - w / 2) * zoomFactor;
                const si = mandelbrotIteration(cr, ci, maxIter, juliaC);
                // Fill stepDown x stepDown block
                for (let dy = 0; dy < stepDown && sy + dy < h; dy++) {
                    let r = 0, g = 0, b = 0;
                    if (si > maxIter) {
                        r = 0;
                        g = 0;
                        b = 0;
                    }
                    else {
                        const t = si / maxIter;
                        const c = (0, palettes_1.samplePalette)(palette, t, colorOffset);
                        r = c[0];
                        g = c[1];
                        b = c[2];
                    }
                    const baseIdx = ((sy + dy) * w) * 4;
                    for (let dx = 0; dx < stepDown; dx++) {
                        const px = sx + dx;
                        if (px < w) {
                            const idx = baseIdx + px * 4;
                            pixels[idx] = r;
                            pixels[idx + 1] = g;
                            pixels[idx + 2] = b;
                            pixels[idx + 3] = 255;
                        }
                    }
                }
                // Store iteration in buffer on finest pass
                if (iterBuf && stepDown === 1) {
                    iterBuf[row + sx] = si;
                }
            }
        }
    }
    async render(view, palette, colorOffset, juliaC, onProgress) {
        this._cancelled = false;
        this._currentMaxIter = view.maxIterations;
        const w = this._w;
        const h = this._h;
        const maxIter = view.maxIterations;
        const aspectRatio = w / h;
        const steps = types_1.TUNABLES.progressiveSteps;
        const factors = [];
        for (let s = 0; s < steps; s++) {
            factors.push(Math.pow(2, steps - 1 - s));
        }
        for (let si = 0; si < factors.length; si++) {
            if (this._cancelled)
                return;
            const factor = factors[si];
            const zoomFactor = factor * 4 / (view.scale * Math.min(w, h));
            const offsetX = (w - h * aspectRatio) / 2;
            const offsetY = (h - w / aspectRatio) / 2;
            const iterBuf = (si === factors.length - 1) ? this.iterBuffer : null;
            const stepSize = Math.max(1, Math.ceil(factor));
            // Process in chunks of rows for responsiveness
            const chunkRows = Math.max(4, Math.ceil(h / (32 * stepSize)));
            for (let rowStart = 0; rowStart < h; rowStart += chunkRows * stepSize) {
                if (this._cancelled)
                    return;
                const rowEnd = Math.min(rowStart + chunkRows * stepSize, h);
                this.computeRows(view, palette, colorOffset, juliaC, maxIter, rowStart, rowEnd, this.pixels, iterBuf, w, h, zoomFactor, offsetX, offsetY, stepSize);
                this.ctx.putImageData(this.imgData, 0, 0);
                // Yield between chunks
                await new Promise(resolve => {
                    if (this._cancelled) {
                        resolve();
                        return;
                    }
                    setTimeout(resolve, 0);
                });
            }
            if (onProgress)
                onProgress((si + 1) / factors.length);
        }
    }
    colorizeOnly(palette, colorOffset) {
        const w = this._w;
        const h = this._h;
        const pixels = this.pixels;
        const buf = this.iterBuffer;
        const maxIter = this._currentMaxIter;
        for (let i = 0; i < w * h; i++) {
            const si = buf[i];
            const idx = i * 4;
            if (si > maxIter) {
                pixels[idx] = 0;
                pixels[idx + 1] = 0;
                pixels[idx + 2] = 0;
                pixels[idx + 3] = 255;
            }
            else {
                const t = si / maxIter;
                const c = (0, palettes_1.samplePalette)(palette, t, colorOffset);
                pixels[idx] = c[0];
                pixels[idx + 1] = c[1];
                pixels[idx + 2] = c[2];
                pixels[idx + 3] = 255;
            }
        }
        this.ctx.putImageData(this.imgData, 0, 0);
    }
    getIterationAt(sx, sy) {
        if (sx < 0 || sx >= this._w || sy < 0 || sy >= this._h)
            return -1;
        return this.iterBuffer[sy * this._w + sx];
    }
    // Render a small Julia preview at given c-value
    renderJuliaPreview(c, palette, colorOffset, maxIter, canvas, size) {
        const ctx = canvas.getContext('2d');
        canvas.width = size;
        canvas.height = size;
        const imgData = ctx.createImageData(size, size);
        const px = imgData.data;
        // Julia set: z₀ = (x,y), c = fixed parameter
        // Render over a region that typically shows the Julia set nicely
        const zoomFactor = 4 / (size * 1); // Scale 1 = base view
        const offsetX = 0;
        const offsetY = 0;
        for (let y = 0; y < size; y++) {
            const zi = (y - offsetY - size / 2) * zoomFactor;
            for (let x = 0; x < size; x++) {
                const zr0 = (x - offsetX - size / 2) * zoomFactor;
                const si = mandelbrotIteration(zr0, zi, maxIter, c);
                const idx = (y * size + x) * 4;
                if (si > maxIter) {
                    px[idx] = 0;
                    px[idx + 1] = 0;
                    px[idx + 2] = 0;
                    px[idx + 3] = 255;
                }
                else {
                    const t = si / maxIter;
                    const col = (0, palettes_1.samplePalette)(palette, t, colorOffset);
                    px[idx] = col[0];
                    px[idx + 1] = col[1];
                    px[idx + 2] = col[2];
                    px[idx + 3] = 255;
                }
            }
        }
        ctx.putImageData(imgData, 0, 0);
    }
}
function createRenderer(canvas) {
    return new Renderer(canvas);
}
};

// ── module: src/input.ts ──
__mods["src/input.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createInputManager = createInputManager;
const types_1 = require("./types");
class InputManager {
    constructor(canvas, overlayCanvas, callbacks) {
        this.state = {
            isDragging: false,
            dragStartX: 0, dragStartY: 0,
            isShiftDrag: false,
            lastPanX: 0, lastPanY: 0,
        };
        this._w = 0;
        this._h = 0;
        this.canvas = canvas;
        this.overlayCanvas = overlayCanvas;
        this.overlayCtx = overlayCanvas.getContext('2d');
        this.callbacks = callbacks;
        this.bindEvents();
    }
    setView(view) { this.view = view; }
    resize(w, h) {
        this._w = w;
        this._h = h;
    }
    bindEvents() {
        this.canvas.addEventListener('mousedown', this.onMouseDown.bind(this));
        window.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));
    }
    getCanvasPos(e) {
        const rect = this.canvas.getBoundingClientRect();
        return {
            x: (e.clientX - rect.left) * (this._w / rect.width),
            y: (e.clientY - rect.top) * (this._h / rect.height),
        };
    }
    onMouseDown(e) {
        const pos = this.getCanvasPos(e);
        this.state.isDragging = true;
        this.state.dragStartX = pos.x;
        this.state.dragStartY = pos.y;
        this.state.isShiftDrag = e.shiftKey || e.button === 1;
        this.state.lastPanX = pos.x;
        this.state.lastPanY = pos.y;
        if (this.state.isShiftDrag) {
            this.canvas.style.cursor = 'grab';
        }
    }
    onMouseMove(e) {
        const pos = this.getCanvasPos(e);
        this.callbacks.onCursorMove(pos.x, pos.y);
        if (!this.state.isDragging)
            return;
        if (this.state.isShiftDrag) {
            // Panning
            const dx = pos.x - this.state.lastPanX;
            const dy = pos.y - this.state.lastPanY;
            this.state.lastPanX = pos.x;
            this.state.lastPanY = pos.y;
            const aspectRatio = this._w / this._h;
            const zoomFactor = 4 / (this.view.scale * Math.min(this._w, this._h));
            const dRe = -dx * zoomFactor;
            const dIm = -dy * zoomFactor;
            this.callbacks.onPan(dRe, dIm);
        }
        else {
            // Drawing zoom rectangle on overlay
            this.drawZoomRect(this.state.dragStartX, this.state.dragStartY, pos.x, pos.y);
        }
    }
    onMouseUp(e) {
        if (!this.state.isDragging)
            return;
        const pos = this.getCanvasPos(e);
        this.state.isDragging = false;
        this.canvas.style.cursor = 'crosshair';
        this.clearOverlay();
        if (this.state.isShiftDrag)
            return;
        const x1 = Math.min(this.state.dragStartX, pos.x);
        const y1 = Math.min(this.state.dragStartY, pos.y);
        const x2 = Math.max(this.state.dragStartX, pos.x);
        const y2 = Math.max(this.state.dragStartY, pos.y);
        const dx = x2 - x1;
        const dy = y2 - y1;
        if (dx < types_1.TUNABLES.dragZoomMinPixels || dy < types_1.TUNABLES.dragZoomMinPixels) {
            // Tiny drag — treat as Julia mode click or single-point zoom
            if (e.shiftKey)
                return;
            this.callbacks.onJuliaClick(pos.x, pos.y);
        }
        else {
            this.callbacks.onZoomRect(x1, y1, x2, y2);
        }
    }
    onWheel(e) {
        e.preventDefault();
        const pos = this.getCanvasPos(e);
        // Calculate zoom factor from wheel delta
        const factor = e.deltaY < 0
            ? Math.pow(types_1.TUNABLES.wheelZoomFactor, Math.min(Math.abs(e.deltaY / 30), 3))
            : Math.pow(1 / types_1.TUNABLES.wheelZoomFactor, Math.min(Math.abs(e.deltaY / 30), 3));
        this.callbacks.onWheelZoom(pos.x, pos.y, factor);
    }
    onKeyDown(e) {
        const panAmount = 0.02 / this.view.scale;
        switch (e.key) {
            case 'ArrowLeft':
                e.preventDefault();
                this.callbacks.onPan(panAmount, 0);
                break;
            case 'ArrowRight':
                e.preventDefault();
                this.callbacks.onPan(-panAmount, 0);
                break;
            case 'ArrowUp':
                e.preventDefault();
                this.callbacks.onPan(0, panAmount);
                break;
            case 'ArrowDown':
                e.preventDefault();
                this.callbacks.onPan(0, -panAmount);
                break;
            case '+':
            case '=':
                e.preventDefault();
                this.callbacks.onWheelZoom(this._w / 2, this._h / 2, 2);
                break;
            case '-':
                e.preventDefault();
                this.callbacks.onWheelZoom(this._w / 2, this._h / 2, 0.5);
                break;
            case 'j':
            case 'J':
                e.preventDefault();
                this.callbacks.onJuliaClick(this._w / 2, this._h / 2);
                break;
        }
    }
    drawZoomRect(x1, y1, x2, y2) {
        const ctx = this.overlayCtx;
        ctx.clearRect(0, 0, this._w, this._h);
        // Semi-transparent fill
        ctx.fillStyle = 'rgba(255, 255, 255, 0.05)';
        ctx.fillRect(x1, y1, x2 - x1, y2 - y1);
        // Dashed border with inverted color
        ctx.strokeStyle = 'rgba(255, 255, 100, 0.9)';
        ctx.lineWidth = 1.5;
        ctx.setLineDash([6, 4]);
        ctx.strokeRect(x1, y1, x2 - x1, y2 - y1);
        ctx.setLineDash([]);
        // Corner brackets
        const len = 12;
        ctx.strokeStyle = 'rgba(255, 255, 100, 1)';
        ctx.lineWidth = 2;
        // Top-left
        ctx.beginPath();
        ctx.moveTo(x1 - len, y1);
        ctx.lineTo(x1, y1);
        ctx.lineTo(x1, y1 + len);
        ctx.stroke();
        // Top-right
        ctx.beginPath();
        ctx.moveTo(x2 + len, y1);
        ctx.lineTo(x2, y1);
        ctx.lineTo(x2, y1 + len);
        ctx.stroke();
        // Bottom-left
        ctx.beginPath();
        ctx.moveTo(x1 - len, y2);
        ctx.lineTo(x1, y2);
        ctx.lineTo(x1, y2 - len);
        ctx.stroke();
        // Bottom-right
        ctx.beginPath();
        ctx.moveTo(x2 + len, y2);
        ctx.lineTo(x2, y2);
        ctx.lineTo(x2, y2 - len);
        ctx.stroke();
    }
    clearOverlay() {
        this.overlayCtx.clearRect(0, 0, this._w, this._h);
    }
    drawCrosshair(x, y) {
        const ctx = this.overlayCtx;
        ctx.clearRect(0, 0, this._w, this._h);
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.6)';
        ctx.lineWidth = 0.5;
        // Crosshair lines
        ctx.beginPath();
        ctx.moveTo(x, 0);
        ctx.lineTo(x, this._h);
        ctx.stroke();
        ctx.beginPath();
        ctx.moveTo(0, y);
        ctx.lineTo(this._w, y);
        ctx.stroke();
        // Center dot
        ctx.beginPath();
        ctx.arc(x, y, 4, 0, Math.PI * 2);
        ctx.stroke();
    }
}
function createInputManager(canvas, overlayCanvas, callbacks) {
    return new InputManager(canvas, overlayCanvas, callbacks);
}
};

// ── module: src/ui.ts ──
__mods["src/ui.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.HUDManager = void 0;
const types_1 = require("./types");
const palettes_1 = require("./palettes");
class HUDManager {
    constructor() {
        this._idleTimer = null;
        this._isJulia = false;
        this._autoMaxIters = 256;
        this.bookmarks = [];
        this.hudEl = document.getElementById('hud');
        this.zoomLabel = document.getElementById('zoom-label');
        this.centerLabel = document.getElementById('center-label');
        this.fpsLabel = document.getElementById('fps-label');
        this.paletteBar = document.getElementById('palette-bar');
        this.sliderOffset = document.getElementById('slider-offset');
        this.sliderIters = document.getElementById('slider-iters');
        this.btnReset = document.getElementById('btn-reset');
        this.btnBack = document.getElementById('btn-back');
        this.btnJulia = document.getElementById('btn-julia');
        this.btnBookmarkSave = document.getElementById('btn-bookmark-save');
        this.bookmarkList = document.getElementById('bookmark-list');
        this.initPaletteChips();
        this.initButtons();
        this.bookmarkList.style.display = 'none';
    }
    initPaletteChips() {
        for (let i = 0; i < palettes_1.PALETTES.length; i++) {
            const chip = document.createElement('div');
            chip.className = 'palette-chip';
            chip.style.background = palettes_1.PALETTES[i].thumbnail;
            chip.title = palettes_1.PALETTES[i].name;
            chip.dataset.index = String(i);
            chip.addEventListener('click', () => {
                this.onPaletteChange?.(i);
            });
            this.paletteBar.appendChild(chip);
        }
    }
    initButtons() {
        this.btnReset.addEventListener('click', () => { this.onReset?.(); });
        this.btnBack.addEventListener('click', () => { this.onBack?.(); });
        this.btnJulia.addEventListener('click', () => { this.onToggleJulia?.(); });
        this.btnBookmarkSave.addEventListener('click', () => {
            this.bookmarkList.style.display =
                this.bookmarkList.style.display === 'flex' ? 'none' : 'flex';
            if (this.bookmarkList.style.display === 'flex')
                this.renderBookmarks();
        });
        this.sliderOffset.addEventListener('input', () => {
            this.onColorOffsetChange?.(parseFloat(this.sliderOffset.value));
        });
        this.sliderIters.addEventListener('input', () => {
            this.onManualItersChange?.(parseInt(this.sliderIters.value, 10));
        });
        // Reset manual iters when slider is released from max
        this.sliderIters.addEventListener('change', () => {
            // If user sets iters to the auto value, reset manual override
            if (parseInt(this.sliderIters.value, 10) === this._autoMaxIters) {
                this.onManualItersChange?.(null);
            }
        });
    }
    update(view, paletteIndex, colorOffset, julia, bookmarks, fps, autoMaxIters) {
        this._autoMaxIters = autoMaxIters;
        // Zoom label
        let zoomStr;
        if (view.scale >= 1e9)
            zoomStr = `×${(view.scale / 1e9).toFixed(1)}e9`;
        else if (view.scale >= 1e6)
            zoomStr = `×${(view.scale / 1e6).toFixed(1)}e6`;
        else if (view.scale >= 1e3)
            zoomStr = `×${(view.scale / 1e3).toFixed(1)}e3`;
        else
            zoomStr = `×${view.scale.toFixed(1)}`;
        this.zoomLabel.textContent = zoomStr;
        // Center label
        this.centerLabel.textContent = `(${view.re.toExponential(4)}, ${view.im.toExponential(4)})`;
        // FPS
        this.fpsLabel.textContent = `${fps} fps`;
        // Julia button state
        this.btnJulia.classList.toggle('active', julia.enabled);
        // Palette chips
        const chips = this.paletteBar.querySelectorAll('.palette-chip');
        chips.forEach((chip, i) => {
            chip.classList.toggle('active', i === paletteIndex);
        });
        // Slider values
        this.sliderOffset.value = String(colorOffset);
        this.sliderIters.value = String(Math.min(Math.max(autoMaxIters, 50), 10000));
    }
    renderBookmarks() {
        if (!this.bookmarks)
            return;
        this.bookmarkList.innerHTML = '';
        if (this.bookmarks.length === 0) {
            const empty = document.createElement('div');
            empty.className = 'bm-item';
            empty.textContent = 'No bookmarks saved';
            this.bookmarkList.appendChild(empty);
            return;
        }
        for (const bm of this.bookmarks) {
            const item = document.createElement('div');
            item.className = 'bm-item';
            item.innerHTML = `<span title="Load view">${bm.name} ×${bm.view.scale.toExponential(1)}</span>`;
            const delBtn = document.createElement('button');
            delBtn.textContent = '✕';
            delBtn.title = 'Delete bookmark';
            delBtn.addEventListener('click', (e) => {
                e.stopPropagation();
                this.onDeleteBookmark?.(bm.id);
                this.renderBookmarks();
            });
            item.addEventListener('click', () => {
                this.onLoadBookmark?.(bm.id);
                this.bookmarkList.style.display = 'none';
            });
            item.appendChild(delBtn);
            this.bookmarkList.appendChild(item);
        }
    }
    setBookmarks(bookmarks) {
        this.bookmarks = bookmarks;
        if (this.bookmarkList.style.display !== 'none') {
            this.renderBookmarks();
        }
    }
    promptSaveBookmark(onSave) {
        const name = prompt('Bookmark name:');
        if (name)
            onSave(name);
    }
    resetIdleTimer() {
        if (this._idleTimer)
            clearTimeout(this._idleTimer);
        this.hudEl.classList.remove('hidden');
        this._idleTimer = window.setTimeout(() => {
            this.hudEl.classList.add('hidden');
        }, types_1.TUNABLES.hudIdleTimeoutMs);
    }
    showNow() {
        this.hudEl.classList.remove('hidden');
        this.resetIdleTimer();
    }
    // Show save bookmark dialog
    showSaveBookmarkDialog() {
        this.promptSaveBookmark((name) => {
            this.onSaveBookmark?.(name);
        });
    }
}
exports.HUDManager = HUDManager;
};

// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"unsloth/Qwen3.6-27B-MTP-GGUF-UD-Q5_K_XL","provider":"brigid","persona":"typescript","sessionId":"f77b8615-088d-485a-814e-a6d5a2baa5c5","tokensIn":1808528,"tokensOut":44312,"tokensTotal":1852840,"cost":0,"turns":56,"toolCalls":56,"failedToolCalls":4,"timestamp":"2026-08-04T08:13:25.992Z"} -->