← Fractal Explorer results

Fractal Explorer

Qwen 3.8 27B GGUF · 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 81,183 bytes · SHA-256 aba0d71d55e7
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Fractal Explorer — Mandelbrot &amp; Julia</title>
<style>
/* ── css: layout ── */
:root {
  --glass: rgba(12, 16, 26, 0.55);
  --glass-brd: rgba(255, 255, 255, 0.14);
  --ink: rgba(240, 244, 252, 0.92);
  --ink-dim: rgba(200, 210, 228, 0.55);
  --accent: #7dd3fc;
  --warn: #fbbf24;
  --mono: "SF Mono", ui-monospace, "Cascadia Code", Menlo, Consolas, monospace;
  --ui: "Inter", -apple-system, "Segoe UI", system-ui, sans-serif;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  width: 100%;
  height: 100%;
  overflow: hidden;
  background: #05070c;
  font-family: var(--ui);
  color: var(--ink);
}

#app, .stage {
  position: fixed;
  inset: 0;
}

canvas#fractal-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  cursor: crosshair;
}

canvas#overlay-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  pointer-events: none;
  z-index: 2;
}
/* ── css: hud ── */
/* ---- glassy HUD (top-left) ---- */
.hud {
  position: absolute;
  top: 14px;
  left: 14px;
  z-index: 5;
  background: var(--glass);
  border: 1px solid var(--glass-brd);
  border-radius: 12px;
  padding: 10px 14px 9px;
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  box-shadow: 0 8px 28px rgba(0, 0, 0, 0.35);
  pointer-events: none;
  user-select: none;
  transition: opacity 0.35s ease, transform 0.35s ease;
  min-width: 220px;
}
.hud.idle { opacity: 0; transform: translateY(-4px); }
.hud-actions { pointer-events: auto; }

.hud-mag {
  font-family: var(--mono);
  font-size: 21px;
  font-weight: 600;
  letter-spacing: 0.02em;
  line-height: 1.1;
}
.hud-mag.warn { color: var(--warn); }
.hud-sub {
  font-family: var(--mono);
  font-size: 10.5px;
  color: var(--ink-dim);
  margin-top: 3px;
  white-space: nowrap;
}
.hud-iter {
  font-family: var(--mono);
  font-size: 10.5px;
  color: var(--ink-dim);
  margin-top: 5px;
  white-space: nowrap;
}
.hud-actions {
  display: flex;
  gap: 6px;
  margin-top: 8px;
}
.hud-btn {
  font-family: var(--ui);
  font-size: 11px;
  color: var(--ink);
  background: rgba(255, 255, 255, 0.07);
  border: 1px solid var(--glass-brd);
  border-radius: 7px;
  padding: 4px 9px;
  cursor: pointer;
  transition: background 0.15s ease;
}
.hud-btn:hover { background: rgba(255, 255, 255, 0.16); }
.hud-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* ---- toolbar (bottom center) ---- */
.toolbar {
  position: absolute;
  bottom: 14px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 5;
  display: flex;
  align-items: center;
  gap: 8px;
  background: var(--glass);
  border: 1px solid var(--glass-brd);
  border-radius: 12px;
  padding: 8px 12px;
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  box-shadow: 0 8px 28px rgba(0, 0, 0, 0.35);
  max-width: calc(100vw - 24px);
  flex-wrap: wrap;
  justify-content: center;
}

.chip {
  width: 34px;
  height: 18px;
  border-radius: 5px;
  border: 1px solid rgba(255, 255, 255, 0.25);
  cursor: pointer;
  padding: 0;
  transition: transform 0.12s ease, box-shadow 0.12s ease;
}
.chip:hover { transform: translateY(-1px); }
.chip.on {
  box-shadow: 0 0 0 2px var(--accent), 0 0 12px rgba(125, 211, 252, 0.5);
  border-color: #fff;
}
.chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.ctl {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 10.5px;
  color: var(--ink-dim);
  letter-spacing: 0.04em;
}
.ctl-lbl { text-transform: uppercase; font-size: 9px; letter-spacing: 0.08em; }
.ctl-val {
  font-family: var(--mono);
  font-size: 10px;
  color: var(--ink);
  min-width: 58px;
  text-align: left;
}
.ctl input[type="range"] {
  width: 86px;
  accent-color: var(--accent);
  height: 3px;
  cursor: pointer;
}
.ctl.chk { cursor: pointer; user-select: none; }
.ctl.chk input { accent-color: var(--accent); cursor: pointer; }

.sep { width: 1px; height: 22px; background: var(--glass-brd); margin: 0 2px; }

.btn {
  font-family: var(--ui);
  font-size: 11px;
  color: var(--ink);
  background: rgba(255, 255, 255, 0.07);
  border: 1px solid var(--glass-brd);
  border-radius: 7px;
  padding: 4px 9px;
  cursor: pointer;
}
.btn:hover { background: rgba(255, 255, 255, 0.16); }
.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.btn:disabled { opacity: 0.4; cursor: default; }
.btn.sel { max-width: 120px; }

/* ---- toast ---- */
.toast {
  position: absolute;
  top: 16px;
  left: 50%;
  transform: translateX(-50%) translateY(-8px);
  z-index: 9;
  background: var(--glass);
  border: 1px solid var(--glass-brd);
  border-radius: 9px;
  padding: 7px 14px;
  font-size: 12px;
  color: var(--ink);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.25s ease, transform 0.25s ease;
  white-space: nowrap;
}
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }

/* ---- help bar ---- */
.help {
  position: absolute;
  bottom: 58px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 4;
  font-size: 10.5px;
  color: var(--ink-dim);
  background: rgba(5, 8, 14, 0.5);
  border-radius: 8px;
  padding: 4px 10px;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  pointer-events: none;
  user-select: none;
  white-space: nowrap;
  letter-spacing: 0.02em;
}
.help b { color: var(--ink); font-weight: 600; }
/* ── css: julia ── */
/* ---- Julia pane (right side) ---- */
.julia-pane {
  position: absolute;
  top: 14px;
  right: 14px;
  bottom: 14px;
  width: clamp(220px, 26vw, 380px);
  z-index: 6;
  display: none;
  flex-direction: column;
  gap: 6px;
  background: var(--glass);
  border: 1px solid var(--glass-brd);
  border-radius: 12px;
  padding: 10px;
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  box-shadow: 0 8px 28px rgba(0, 0, 0, 0.35);
}

.jp-head {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 22px;
}
.jp-title {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.22em;
  color: var(--accent);
}
.jp-c {
  font-family: var(--mono);
  font-size: 10px;
  color: var(--ink-dim);
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.jp-close {
  width: 22px;
  height: 22px;
  border-radius: 6px;
  border: 1px solid var(--glass-brd);
  background: rgba(255, 255, 255, 0.07);
  color: var(--ink);
  font-size: 13px;
  line-height: 1;
  cursor: pointer;
}
.jp-close:hover { background: rgba(255, 255, 255, 0.16); }
.jp-close:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.jp-canvas {
  flex: 1;
  width: 100%;
  height: 100%;
  display: block;
  border-radius: 8px;
  background: #05070c;
  cursor: crosshair;
}

.jp-hint {
  font-size: 9.5px;
  color: var(--ink-dim);
  letter-spacing: 0.03em;
  text-align: center;
  user-select: none;
}

/* narrow screens: shrink panes */
@media (max-width: 760px) {
  .julia-pane { width: 46vw; min-width: 180px; }
  .help { display: none; }
  .toolbar { gap: 6px; padding: 6px 8px; }
  .ctl input[type="range"] { width: 64px; }
}
</style>
</head>
<body>
<div id="app">
  <div class="stage">
    <canvas id="fractal-canvas"></canvas>
    <canvas id="overlay-canvas"></canvas>
    <div class="hud"></div>
    <div class="toolbar"></div>
    <aside class="julia-pane" id="julia-pane">
      <div class="jp-head">
        <span class="jp-title">JULIA</span>
        <span class="jp-c" id="julia-c-label">c = (—, —)</span>
        <button class="jp-close" title="Close Julia pane">×</button>
      </div>
      <canvas class="jp-canvas" id="julia-canvas"></canvas>
      <span class="jp-hint">hover Mandelbrot to move c · click fixes c</span>
    </aside>
    <div class="toast"></div>
    <div class="help"></div>
  </div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./consts":"src/consts.ts","./math":"src/math.ts","./state":"src/state.ts","./renderer":"src/renderer.ts","./ui":"src/ui.ts","./input":"src/input.ts","./overlay":"src/overlay.ts"},"src/math.ts":{"./consts":"src/consts.ts"},"src/state.ts":{"./consts":"src/consts.ts","./math":"src/math.ts"},"src/renderer.ts":{"./consts":"src/consts.ts","./math":"src/math.ts"},"src/ui.ts":{"./consts":"src/consts.ts","./state":"src/state.ts","./math":"src/math.ts"},"src/input.ts":{"./consts":"src/consts.ts","./math":"src/math.ts"}};
function __require(id) {
  if (__cache[id]) return __cache[id].exports;
  var module = __cache[id] = { exports: {} };
  var factory = __mods[id];
  if (!factory) throw new Error("Module not found: " + id);
  factory(module.exports, function (spec) {
    var target = (__map[id] && __map[id][spec]) || spec;
    return __require(target);
  }, module);
  return module.exports;
}

// ── module: src/main.ts ──
__mods["src/main.ts"] = function (exports, require, module) {
"use strict";
// ============================================================================
// MAIN — app shell: DOM wiring, render loop, event plumbing, debug API
//
//   window.fractal = {
//     iterationsAt(re, im)          -> smooth iter count at a complex point
//     setView({re, im, scale})      -> jump to a view (scale = magnification)
//     setJulia / setMandelbrot      -> switch sets
//     setPalette / setPaletteOffset / setDrift
//     save / load                   -> URL hash state (shareable links)
//     bookmarks()                   -> names of saved bookmarks
//     reset / finish / version
//   }
// ============================================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.start = start;
const consts_1 = require("./consts");
const math_1 = require("./math");
const state_1 = require("./state");
const renderer_1 = require("./renderer");
const ui_1 = require("./ui");
const input_1 = require("./input");
const overlay_1 = require("./overlay");
// ---------------------------------------------------------------------------
// module-level refs
// ---------------------------------------------------------------------------
let mainCanvas;
let overlayCanvas;
let juliaCanvas;
let mainRenderer;
let juliaRenderer = null;
let overlay;
let juliaPaneEl;
let juliaCLabel;
let hud = null;
let toolbar = null;
let toastEl = null;
let toast = null;
let mainNav = null;
let juliaNav = null;
let mainView = { re: 0, im: 0, w: 3 };
let viewChanged = true;
let lastC = { re: -0.743, im: 0.131 };
let lastHashTick = 0;
let lastWarn = 0;
let fps = 60;
let lastFrameT = 0;
let lastHudT = 0;
let lastDrift = 0;
let lastInteraction = performance.now();
function markActive() {
    lastInteraction = performance.now();
}
/** Baked 256-entry LUT per palette (built once at startup). */
const luts = consts_1.PALETTES.map((p) => ({ table: (0, consts_1.bakePalette)(p), cyclic: p.cyclic }));
function toastMsg(msg) {
    if (toast)
        toast.show(msg);
}
// ---------------------------------------------------------------------------
// render orchestration
// ---------------------------------------------------------------------------
function applyMainView(anchor) {
    mainView = { re: state_1.state.view.re, im: state_1.state.view.im, w: state_1.state.view.w };
    mainRenderer.setView(mainView, (0, state_1.effIters)(state_1.state), luts[state_1.state.palette] ?? luts[0], state_1.state.paletteOffset, anchor ?? null);
    (0, ui_1.syncIterLabel)(toolbar, state_1.state);
    viewChanged = true;
    scheduleHash();
}
function applyJuliaParams() {
    if (!juliaRenderer)
        return;
    juliaRenderer.setView(state_1.state.view, (0, state_1.effIters)(state_1.state), luts[state_1.state.palette] ?? luts[0], state_1.state.paletteOffset);
    juliaRenderer.setJuliaC(state_1.state.cRe, state_1.state.cIm);
    updateJuliaLabel();
}
function updateJuliaLabel() {
    if (juliaCLabel) {
        juliaCLabel.textContent = `c = (${(0, math_1.fmtCoord)(state_1.state.cRe)}, ${(0, math_1.fmtCoord)(state_1.state.cIm)})`;
    }
}
function ensureJuliaRenderer() {
    if (juliaRenderer)
        return;
    juliaRenderer = new renderer_1.FractalRenderer(juliaCanvas, 'julia');
    juliaNav = new input_1.NavInput({
        canvas: juliaCanvas,
        w0: state_1.state.w0,
        getView: () => state_1.state.view,
        pushHistory: () => (0, state_1.pushHistory)(state_1.state),
        commit: (v) => {
            (0, state_1.clampView)(state_1.state, v);
            state_1.state.view = v;
            applyJuliaParams();
        },
    });
}
function setMode(mode, pick) {
    (0, state_1.pushHistory)(state_1.state);
    state_1.state.mode = mode;
    if (mode === 'julia') {
        if (pick) {
            state_1.state.cRe = pick.re;
            state_1.state.cIm = pick.im;
        }
        ensureJuliaRenderer();
        sizeJuliaPane();
        applyJuliaParams();
        juliaPaneEl.style.display = 'flex';
    }
    else {
        juliaPaneEl.style.display = 'none';
    }
    updateJuliaLabel();
    scheduleHash();
}
function sizeJuliaPane() {
    if (!juliaRenderer)
        return;
    const pw = Math.max(140, Math.round((mainCanvas.clientWidth || 800) * 0.24));
    const ph = Math.round(pw * 0.66);
    const dpr = Math.min(2, window.devicePixelRatio || 1);
    juliaCanvas.style.width = pw + 'px';
    juliaCanvas.style.height = ph + 'px';
    juliaRenderer.resize(pw, ph, dpr);
}
/** Full palette/offset re-render from existing iteration buffers (no recompute). */
function recolorAll() {
    const lut = luts[state_1.state.palette] ?? luts[0];
    mainRenderer.recolorize(lut, state_1.state.paletteOffset);
    if (juliaRenderer)
        juliaRenderer.recolorize(lut, state_1.state.paletteOffset);
    if (toolbar)
        (0, ui_1.syncPaletteChips)(toolbar, state_1.state.palette);
}
function refreshBookmarkList() {
    if (!toolbar)
        return;
    const names = state_1.state.bookmarks.map((b) => b.name);
    (0, ui_1.fillBookmarkList)(toolbar.bmList, names, -1);
    toolbar.bmDel.disabled = state_1.state.bookmarks.length === 0;
}
function scheduleHash() {
    const now = performance.now();
    if (now - lastHashTick > 400) {
        lastHashTick = now;
        (0, state_1.writeHash)(state_1.state);
    }
}
function syncModeUI() {
    if (state_1.state.mode === 'julia') {
        ensureJuliaRenderer();
        sizeJuliaPane();
        applyJuliaParams();
        juliaPaneEl.style.display = 'flex';
    }
    else {
        juliaPaneEl.style.display = 'none';
    }
}
// ---------------------------------------------------------------------------
// setup
// ---------------------------------------------------------------------------
function start() {
    mainCanvas = document.getElementById('fractal-canvas');
    overlayCanvas = document.getElementById('overlay-canvas');
    juliaCanvas = document.getElementById('julia-canvas');
    if (!mainCanvas || !overlayCanvas || !juliaCanvas) {
        throw new Error('missing canvas elements');
    }
    juliaPaneEl = document.getElementById('julia-pane');
    juliaCLabel = document.getElementById('julia-c-label');
    mainRenderer = new renderer_1.FractalRenderer(mainCanvas, 'mandelbrot');
    overlay = new overlay_1.Overlay(overlayCanvas);
    hud = (0, ui_1.buildHUD)();
    toolbar = (0, ui_1.buildToolbar)();
    toastEl = (0, ui_1.buildToast)();
    toast = new overlay_1.Toast(toastEl);
    const help = (0, ui_1.buildHelp)();
    const app = document.getElementById('app');
    const wrap = app.firstElementChild;
    wrap.querySelector('.hud').replaceWith(hud.root);
    wrap.querySelector('.toolbar').replaceWith(toolbar.root);
    wrap.querySelector('.julia-pane').replaceWith(juliaPaneEl);
    wrap.querySelector('.toast').replaceWith(toastEl);
    wrap.querySelector('.help').replaceWith(help);
    // ---- state init: URL hash > defaults; bookmarks always loaded ----
    (0, state_1.loadBookmarks)(state_1.state);
    (0, state_1.readHash)(state_1.state);
    (0, state_1.clampView)(state_1.state, state_1.state.view);
    refreshBookmarkList();
    // ---- initial sizes ----
    const sz = () => {
        const w = mainCanvas.clientWidth || 800;
        const h = mainCanvas.clientHeight || 600;
        return { w, h, dpr: Math.min(2, window.devicePixelRatio || 1) };
    };
    const s0 = sz();
    mainRenderer.resize(s0.w, s0.h, s0.dpr);
    overlay.resize(s0.w, s0.h, s0.dpr);
    const ro = new ResizeObserver(() => {
        const { w, h, dpr } = sz();
        const ow = mainCanvas.clientWidth;
        const oh = mainCanvas.clientHeight;
        if (Math.abs(ow - w) < 1 && Math.abs(oh - h) < 1)
            return;
        mainRenderer.resize(w, h, dpr);
        overlay.resize(w, h, dpr);
        viewChanged = true;
    });
    ro.observe(mainCanvas);
    // ---- main canvas navigation ----
    mainNav = new input_1.NavInput({
        canvas: mainCanvas,
        w0: state_1.state.w0,
        getView: () => state_1.state.view,
        pushHistory: () => (0, state_1.pushHistory)(state_1.state),
        commit: (v, anchor) => {
            (0, state_1.clampView)(state_1.state, v);
            state_1.state.view = v;
            markActive();
            applyMainView(anchor);
        },
        onRect: (x0, y0, x1, y1, active) => {
            overlay.rect = { x0, y0, x1, y1, active };
            markActive();
        },
        onPick: (re, im) => {
            // clicking a point in Mandelbrot view opens Julia for that c
            setMode('julia', { re, im });
            toastMsg(`Julia set · c = (${(0, math_1.fmtCoord)(re)}, ${(0, math_1.fmtCoord)(im)})`);
        },
        onHover: (re, im, cssX, cssY) => {
            lastC = { re, im };
            markActive();
            if (state_1.state.juliaLive && juliaRenderer) {
                juliaRenderer.setJuliaC(re, im);
                if (juliaCLabel) {
                    juliaCLabel.textContent = `c = (${(0, math_1.fmtCoord)(re)}, ${(0, math_1.fmtCoord)(im)})`;
                }
            }
            overlay.reticle = { x: cssX, y: cssY, show: state_1.state.juliaLive };
        },
    });
    // ---- toolbar wiring ----
    const t = toolbar;
    t.chips.forEach((chip, i) => {
        chip.addEventListener('click', () => {
            if (state_1.state.palette === i)
                return;
            state_1.state.palette = i;
            recolorAll();
            scheduleHash();
        });
    });
    t.offset.addEventListener('input', () => {
        state_1.state.paletteOffset = Number(t.offset.value) | 0;
        recolorAll();
    });
    t.drift.addEventListener('change', () => {
        state_1.state.drift = t.drift.checked;
        lastDrift = performance.now();
    });
    t.iters.addEventListener('input', () => {
        const v = Number(t.iters.value) | 0;
        state_1.state.itersOverride = v < 10 ? 0 : v; // low values snap back to auto
        t.iters.value = String(state_1.state.itersOverride);
        if (state_1.state.mode === 'mandelbrot')
            applyMainView();
        if (state_1.state.mode === 'julia')
            applyJuliaParams();
        (0, ui_1.syncIterLabel)(t, state_1.state);
    });
    t.live.addEventListener('change', () => {
        state_1.state.juliaLive = t.live.checked;
        if (!state_1.state.juliaLive && juliaRenderer) {
            juliaRenderer.setJuliaC(state_1.state.cRe, state_1.state.cIm);
            overlay.reticle.show = false;
        }
    });
    toolbar.root.addEventListener('pointerdown', markActive);
    t.bmSave.addEventListener('click', () => {
        const base = 'view ' + (state_1.state.bookmarks.length + 1);
        const name = (window.prompt('Bookmark name', base) || base).trim();
        (0, state_1.addBookmark)(state_1.state, name);
        refreshBookmarkList();
        t.bmList.value = String(state_1.state.bookmarks.length - 1);
        toastMsg(`saved bookmark “${name}”`);
    });
    t.bmList.addEventListener('change', () => {
        const i = Number(t.bmList.value);
        const bm = state_1.state.bookmarks[i];
        if (!bm)
            return;
        (0, state_1.restoreBookmark)(state_1.state, bm);
        syncModeUI();
        applyMainView();
        recolorAll();
        toastMsg(`restored “${bm.name}”`);
    });
    t.bmDel.addEventListener('click', () => {
        const i = Number(t.bmList.value);
        if (i >= 0 && i < state_1.state.bookmarks.length) {
            (0, state_1.deleteBookmark)(state_1.state, i);
            refreshBookmarkList();
            toastMsg('bookmark deleted');
        }
    });
    t.copyLink.addEventListener('click', () => {
        (0, state_1.writeHash)(state_1.state);
        const url = location.href;
        if (navigator.clipboard && navigator.clipboard.writeText) {
            navigator.clipboard.writeText(url).then(() => toastMsg('link copied to clipboard'), () => toastMsg('link in address bar'));
        }
        else {
            toastMsg('link in address bar');
        }
    });
    // ---- HUD actions ----
    hud.back.addEventListener('click', () => {
        (0, state_1.goBack)(state_1.state);
        syncModeUI();
        applyMainView();
        recolorAll();
    });
    const doReset = () => {
        (0, state_1.resetView)(state_1.state);
        state_1.state.mode = 'mandelbrot';
        state_1.state.itersOverride = 0;
        syncModeUI();
        applyMainView();
        recolorAll();
    };
    hud.reset.addEventListener('click', doReset);
    hud.juliaBtn.addEventListener('click', () => {
        setMode(state_1.state.mode === 'julia' ? 'mandelbrot' : 'julia', state_1.state.mode === 'mandelbrot' ? { re: lastC.re, im: lastC.im } : undefined);
    });
    const closeBtn = juliaPaneEl.querySelector('.jp-close');
    closeBtn.addEventListener('click', () => setMode('mandelbrot'));
    // ---- keyboard ----
    window.addEventListener('keydown', (e) => {
        const tag = e.target?.tagName;
        if (tag === 'INPUT' || tag === 'SELECT' || tag === 'TEXTAREA')
            return;
        const k = e.key;
        const v = state_1.state.view;
        const act = (fn) => {
            fn();
            e.preventDefault();
            markActive();
        };
        if (k === 'j' || k === 'J') {
            act(() => setMode(state_1.state.mode === 'julia' ? 'mandelbrot' : 'julia', state_1.state.mode === 'mandelbrot' ? { re: lastC.re, im: lastC.im } : undefined));
        }
        else if (k === 'r' || k === 'R') {
            act(doReset);
        }
        else if (k === 'Backspace' && e.ctrlKey) {
            act(() => {
                (0, state_1.goBack)(state_1.state);
                syncModeUI();
                applyMainView();
                recolorAll();
            });
        }
        else if (k === '+' || k === '=') {
            act(() => {
                (0, state_1.pushHistory)(state_1.state);
                (0, state_1.setView)(state_1.state, v.re, v.im, v.w / consts_1.KEY_ZOOM_FACTOR);
                applyMainView();
            });
        }
        else if (k === '-' || k === '_') {
            act(() => {
                (0, state_1.pushHistory)(state_1.state);
                (0, state_1.setView)(state_1.state, v.re, v.im, v.w * consts_1.KEY_ZOOM_FACTOR);
                applyMainView();
            });
        }
        else if (k.startsWith('Arrow')) {
            act(() => {
                const d = v.w * consts_1.KEY_PAN_FRAC;
                if (k === 'ArrowLeft')
                    (0, state_1.setView)(state_1.state, v.re - d, v.im, v.w);
                if (k === 'ArrowRight')
                    (0, state_1.setView)(state_1.state, v.re + d, v.im, v.w);
                if (k === 'ArrowUp')
                    (0, state_1.setView)(state_1.state, v.re, v.im - d, v.w);
                if (k === 'ArrowDown')
                    (0, state_1.setView)(state_1.state, v.re, v.im + d, v.w);
                applyMainView();
            });
        }
    });
    // ---- debug / probe interface ----
    window.fractal = {
        iterationsAt: (re, im) => state_1.state.mode === 'julia'
            ? (0, math_1.juliaIter)(re, im, state_1.state.cRe, state_1.state.cIm, (0, state_1.effIters)(state_1.state))
            : (0, math_1.mandelIter)(re, im, (0, state_1.effIters)(state_1.state)),
        setView: (o) => {
            const w = o.w !== undefined ? o.w : o.scale !== undefined ? state_1.state.w0 / o.scale : state_1.state.view.w;
            (0, state_1.setView)(state_1.state, o.re, o.im, w);
            applyMainView();
        },
        setJulia: (cRe, cIm) => {
            setMode('julia', { re: cRe, im: cIm });
        },
        setMandelbrot: () => {
            setMode('mandelbrot');
        },
        setPalette: (i) => {
            state_1.state.palette = Math.max(0, Math.min(consts_1.PALETTES.length - 1, i | 0));
            recolorAll();
        },
        setPaletteOffset: (o) => {
            state_1.state.paletteOffset = o | 0;
            recolorAll();
        },
        setDrift: (on) => {
            state_1.state.drift = !!on;
            if (toolbar)
                toolbar.drift.checked = on;
            lastDrift = performance.now();
        },
        state: state_1.state,
        palettes: consts_1.PALETTES.map((p) => p.name),
        save: () => {
            (0, state_1.writeHash)(state_1.state);
            return location.hash;
        },
        load: () => {
            const ok = (0, state_1.readHash)(state_1.state);
            syncModeUI();
            applyMainView();
            recolorAll();
            return ok;
        },
        bookmarks: () => state_1.state.bookmarks.map((b) => b.name),
        bookmark: (name) => {
            (0, state_1.addBookmark)(state_1.state, name);
            refreshBookmarkList();
            return state_1.state.bookmarks.length - 1;
        },
        restoreBookmark: (i) => {
            const bm = state_1.state.bookmarks[i];
            if (!bm)
                return false;
            (0, state_1.restoreBookmark)(state_1.state, bm);
            syncModeUI();
            applyMainView();
            recolorAll();
            return true;
        },
        reset: doReset,
        finish: () => {
            (0, renderer_1.finishSync)(mainRenderer);
            if (juliaRenderer)
                (0, renderer_1.finishSync)(juliaRenderer);
            return true;
        },
        version: () => mainRenderer.version,
    };
    // ---- initial render request ----
    applyMainView();
    syncModeUI();
    (0, ui_1.syncPaletteChips)(toolbar, state_1.state.palette);
    (0, ui_1.syncIterLabel)(toolbar, state_1.state);
    // ---- main loop ----
    const loop = (t) => {
        requestAnimationFrame(loop);
        if (lastFrameT === 0)
            lastFrameT = t;
        const dt = Math.max(0.001, (t - lastFrameT) / 1000);
        lastFrameT = t;
        fps += (1 / dt - fps) * 0.06;
        // animated palette drift (colorize-only, no iteration recompute)
        if (state_1.state.drift) {
            const now = performance.now();
            const step = (now - lastDrift) * (consts_1.DRIFT_SPEED / 1000);
            if (step >= 1) {
                lastDrift = now;
                state_1.state.paletteOffset = (state_1.state.paletteOffset + Math.round(step)) & 255;
                if (toolbar)
                    toolbar.offset.value = String(state_1.state.paletteOffset);
                recolorAll();
            }
        }
        mainRenderer.process(9);
        if (juliaRenderer)
            juliaRenderer.process(5);
        // HUD + hash at ~4 Hz
        const now = performance.now();
        if (now - lastHudT > 250) {
            lastHudT = now;
            if (hud) {
                (0, ui_1.updateHUD)(hud, state_1.state, fps, (0, state_1.effIters)(state_1.state), (0, math_1.autoIterations)((0, math_1.magOf)(state_1.state.view, state_1.state.w0)));
                hud.root.classList.toggle('idle', now - lastInteraction > 5000);
            }
            const mag = (0, math_1.magOf)(state_1.state.view, state_1.state.w0);
            if (mag > consts_1.WARN_MAG && now - lastWarn > 6000) {
                lastWarn = now;
                toastMsg('near double-precision limit — structure may degrade past ×2e13');
            }
            if (viewChanged) {
                viewChanged = false;
                scheduleHash();
            }
        }
        overlay.draw();
    };
    requestAnimationFrame(loop);
}
start();
};

// ── module: src/consts.ts ──
__mods["src/consts.ts"] = function (exports, require, module) {
"use strict";
// ============================================================================
// TUNABLE CONSTANTS — adjust these to taste
// ============================================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.DRIFT_SPEED = exports.PALETTES = exports.SMOOTH_LOG = exports.ESCAPE_R2 = exports.ZOOM_PREVIEW_MAX = exports.ZOOM_PREVIEW_MIN = exports.REFIN_CHUNK_ROWS = exports.COARSE_MIN_F = exports.COARSE_MAX_F = exports.COARSE_TARGET_ITERS = exports.WARN_MAG = exports.MAX_MAG = exports.KEY_PAN_FRAC = exports.KEY_ZOOM_FACTOR = exports.WHEEL_ZOOM_FACTOR = exports.ASPECT_CORRECTION = exports.INITIAL_VIEW = exports.DEFAULT_ITERS = exports.MAX_ITERS = exports.ITERS_PER_DECADE = exports.BASE_ITERS = void 0;
exports.bakePalette = bakePalette;
/** Base iteration cap at the default view width (w0). */
exports.BASE_ITERS = 600;
/** Iterations added per decade of magnification. */
exports.ITERS_PER_DECADE = 38;
/** Max iteration cap (also the "auto" slider's upper bound). */
exports.MAX_ITERS = 4000;
/** Default iteration slider value (0 = auto, derived from zoom). */
exports.DEFAULT_ITERS = 0;
/** Initial view: world width in complex-plane units, centered here. */
exports.INITIAL_VIEW = { re: -0.58, im: 0.0, w0: 3.2 };
/** Aspect correction applied to w0 (canvas is wider than 3:2). */
exports.ASPECT_CORRECTION = 1.15;
/** Wheel zoom: scale factor per wheel "step" (deltaY 100). */
exports.WHEEL_ZOOM_FACTOR = 1.15;
/** Keyboard zoom step per press. */
exports.KEY_ZOOM_FACTOR = 1.4;
/** Arrow-key pan speed (fraction of view width per press). */
exports.KEY_PAN_FRAC = 0.05;
/** Maximum magnification (width w0/currentW) — double precision degrades beyond ~1e13. */
exports.MAX_MAG = 2e13;
/** Warn (amber HUD + toast) when this magnitude is exceeded (approaching the cap). */
exports.WARN_MAG = 1e13;
/**
 * Progressive refinement: an adaptive coarse pass (bounded below by
 * COARSE_TARGET_ITERS total escape iterations, capped at COARSE_MAX_F of full
 * resolution) paints the frame in a few ms, then full resolution is refined
 * in REFIN_CHUNK_ROWS-row chunks.
 */
exports.COARSE_TARGET_ITERS = 5e6; // coarse pass budget (escape iters)
exports.COARSE_MAX_F = 0.25; // coarse is at most 1/4 resolution
exports.COARSE_MIN_F = 0.04; // ... but never coarser than this
exports.REFIN_CHUNK_ROWS = 8; // full-res refinement chunk height (rows)
/** Zoom ratios for which the previous frame makes a usable instant preview. */
exports.ZOOM_PREVIEW_MIN = 0.45;
exports.ZOOM_PREVIEW_MAX = 2.2;
/**
 * Escape radius squared: |z|^2 > 4 (i.e. |z| > 2) is the standard textbook
 * bound — once |z| > 2 the orbit is guaranteed to escape to infinity. Using
 * 4 (not a larger radius) keeps escape detection tight so fast exterior points
 * (e.g. c = 2) escape within a couple of iterations, and makes the smooth
 * fractional count well-conditioned.
 */
exports.ESCAPE_R2 = 4;
exports.SMOOTH_LOG = Math.log(2);
exports.PALETTES = [
    {
        name: 'Aurora',
        cyclic: true,
        stops: [
            9, 6, 36, 32, 10, 84, 15, 52, 118, 16, 108, 168, 102, 154, 160,
            252, 246, 200, 152, 154, 120, 14, 120, 136,
        ],
    },
    {
        name: 'Fire',
        cyclic: false,
        stops: [
            0, 0, 0, 40, 8, 2, 104, 22, 6, 196, 62, 8, 255, 140, 20,
            255, 214, 88, 255, 252, 224,
        ],
    },
    {
        name: 'Ocean',
        cyclic: false,
        stops: [
            2, 8, 22, 6, 32, 72, 8, 74, 128, 10, 120, 178, 44, 178, 210,
            150, 232, 236, 240, 255, 255,
        ],
    },
    {
        name: 'Mono',
        cyclic: false,
        stops: [0, 0, 0, 64, 64, 64, 140, 140, 140, 255, 255, 255],
    },
    {
        name: 'Psyche',
        cyclic: true,
        stops: [
            255, 0, 110, 255, 170, 0, 255, 255, 0, 0, 255, 160, 0, 80, 255,
            120, 0, 255, 255, 0, 110,
        ],
    },
    {
        name: 'Glow',
        cyclic: false,
        stops: [
            0, 0, 0, 28, 6, 60, 90, 24, 140, 150, 120, 230, 200, 190, 255,
            255, 255, 255,
        ],
    },
];
/** Pre-bake a lookup table (256 entries per channel) for one palette. */
function bakePalette(pal) {
    const n = pal.stops.length / 3;
    const table = new Uint8Array(256 * 3);
    const stops = pal.stops;
    for (let i = 0; i < 256; i++) {
        const t = i / 255;
        const scaled = t * (n - 1);
        let s = Math.floor(scaled);
        if (s > n - 2)
            s = n - 2;
        const f = scaled - s;
        const o = i * 3;
        const s0 = s * 3;
        const s1 = s0 + 3;
        table[o] = Math.round(stops[s0] + (stops[s1] - stops[s0]) * f);
        table[o + 1] = Math.round(stops[s0 + 1] + (stops[s1 + 1] - stops[s0 + 1]) * f);
        table[o + 2] = Math.round(stops[s0 + 2] + (stops[s1 + 2] - stops[s0 + 2]) * f);
    }
    return table;
}
/** Color drift cycle speed (table steps per second) for the animated offset. */
exports.DRIFT_SPEED = 6; // table steps / s
};

// ── module: src/math.ts ──
__mods["src/math.ts"] = function (exports, require, module) {
"use strict";
// ============================================================================
// ESCAPE-TIME MATH
//
// Allocation-free per pixel: no objects, no arrays, no closures created
// inside the iteration loops. Smooth (continuous) coloring uses
//     n + 1 - log2( log(|z|) / log(2) )  ≈  n + 1 - log2(log|z|)
// at the moment of escape, which removes the banding of integer coloring.
// ============================================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.mandelIter = mandelIter;
exports.juliaIter = juliaIter;
exports.autoIterations = autoIterations;
exports.setupScanline = setupScanline;
exports.magOf = magOf;
exports.fmtMag = fmtMag;
exports.fmtCoord = fmtCoord;
const consts_1 = require("./consts");
/**
 * Mandelbrot: iterate z = z^2 + c starting from z = 0.
 * Returns 0 when the orbit stays bounded (interior / set member) after
 * `maxIter` steps; otherwise the smooth (fractional) iteration count.
 */
const INV_L2 = 1 / Math.log(2);
function mandelIter(x0, y0, maxIter) {
    let x = 0, y = 0;
    for (let n = 0; n < maxIter; n++) {
        const x2 = x * x;
        const y2 = y * y;
        if (x2 + y2 > consts_1.ESCAPE_R2) {
            // smooth fractional escape value:  n + 1 - log2(log|z|)
            // (clamped to > 0 so far-field Julia points never read as "interior")
            const l2 = Math.log(x2 + y2) * 0.5; // log|z|
            const sm = n + 1 - Math.log(l2) * INV_L2;
            return sm > 0 ? sm : 0.5;
        }
        y = 2 * x * y + y0;
        x = x2 - y2 + x0;
    }
    return 0;
}
/**
 * Julia: iterate z = z^2 + c starting from z = z0.
 * Same return convention as mandelIter.
 */
function juliaIter(x0, y0, cRe, cIm, maxIter) {
    let x = x0, y = y0;
    for (let n = 0; n < maxIter; n++) {
        const x2 = x * x;
        const y2 = y * y;
        if (x2 + y2 > consts_1.ESCAPE_R2) {
            const l2 = Math.log(x2 + y2) * 0.5;
            const sm = n + 1 - Math.log(l2) * INV_L2;
            return sm > 0 ? sm : 0.5;
        }
        y = 2 * x * y + cIm;
        x = x2 - y2 + cRe;
    }
    return 0;
}
/**
 * Iteration limit scaled by zoom depth: more magnification -> more detail
 * near the boundary. `mag` = world-width0 / current-world-width.
 */
function autoIterations(mag) {
    if (mag <= 1)
        return consts_1.BASE_ITERS;
    const iters = consts_1.BASE_ITERS + consts_1.ITERS_PER_DECADE * Math.log10(mag);
    return Math.min(consts_1.MAX_ITERS, Math.floor(iters));
}
/**
 * Precompute the complex-plane coordinate of the first pixel of row 0 and the
 * per-pixel / per-row steps, so the scan loop only does adds — no per-pixel
 * multiply-by-view. x0 is the re of pixel (0,0), y0 the im of pixel (0,0).
 */
function setupScanline(v, pxw, pxh) {
    const s = v.w / pxw;
    const h = v.w * (pxh / pxw);
    return {
        x0: v.re - v.w * 0.5 + s * 0.5,
        y0: v.im + h * 0.5 - (h / pxh) * 0.5,
        stepX: s,
        rowStepY: -(h / pxh),
    };
}
function magOf(v, w0) {
    return w0 / v.w;
}
/** Format a number for HUD display: 1.234e+09 style for magnitudes. */
function fmtMag(mag) {
    if (mag < 1000)
        return '×' + mag.toFixed(mag < 10 ? 2 : 1);
    return '×' + mag.toExponential(3);
}
function fmtCoord(v) {
    if (v === 0)
        return '0.000000000';
    const a = Math.abs(v);
    if (a >= 1e4 || a < 1e-4)
        return v.toExponential(5);
    return v.toFixed(9);
}
};

// ── module: src/state.ts ──
__mods["src/state.ts"] = function (exports, require, module) {
"use strict";
// ============================================================================
// STATE — view, mode, history stack, bookmarks (localStorage), URL hash
// ============================================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.MAX_ITERS = exports.state = void 0;
exports.effIters = effIters;
exports.clampView = clampView;
exports.pushHistory = pushHistory;
exports.goBack = goBack;
exports.resetView = resetView;
exports.saveBookmarks = saveBookmarks;
exports.loadBookmarks = loadBookmarks;
exports.addBookmark = addBookmark;
exports.restoreBookmark = restoreBookmark;
exports.deleteBookmark = deleteBookmark;
exports.writeHash = writeHash;
exports.readHash = readHash;
exports.setView = setView;
const consts_1 = require("./consts");
Object.defineProperty(exports, "MAX_ITERS", { enumerable: true, get: function () { return consts_1.MAX_ITERS; } });
const math_1 = require("./math");
exports.state = {
    view: { re: consts_1.INITIAL_VIEW.re, im: consts_1.INITIAL_VIEW.im, w: consts_1.INITIAL_VIEW.w0 * consts_1.ASPECT_CORRECTION },
    w0: consts_1.INITIAL_VIEW.w0 * consts_1.ASPECT_CORRECTION,
    mode: 'mandelbrot',
    cRe: -0.743643887037151,
    cIm: 0.13182590420531197,
    itersOverride: consts_1.DEFAULT_ITERS,
    palette: 0,
    paletteOffset: 0,
    drift: false,
    juliaLive: true,
    history: [],
    bookmarks: [],
    toast: null,
};
// ---------------------------------------------------------------------------
// derived helpers
// ---------------------------------------------------------------------------
function effIters(s) {
    return s.itersOverride > 0 ? s.itersOverride : (0, math_1.autoIterations)((0, math_1.magOf)(s.view, s.w0));
}
function clampView(s, v) {
    // enforce max magnification (double-precision cap) — center preserved,
    // the view simply stops shrinking
    const minW = s.w0 / consts_1.MAX_MAG;
    if (v.w < minW)
        v.w = minW;
    if (v.w > s.w0 * 100)
        v.w = s.w0 * 100;
}
// ---------------------------------------------------------------------------
// history (back button)
// ---------------------------------------------------------------------------
function pushHistory(s) {
    s.history.push({
        re: s.view.re, im: s.view.im, w: s.view.w,
        mode: s.mode, cRe: s.cRe, cIm: s.cIm,
    });
    if (s.history.length > 64)
        s.history.shift();
}
function goBack(s) {
    const prev = s.history.pop();
    if (prev) {
        s.view = { re: prev.re, im: prev.im, w: prev.w };
        s.mode = prev.mode;
        s.cRe = prev.cRe;
        s.cIm = prev.cIm;
    }
}
function resetView(s) {
    pushHistory(s);
    s.view = { re: consts_1.INITIAL_VIEW.re, im: consts_1.INITIAL_VIEW.im, w: s.w0 };
}
// ---------------------------------------------------------------------------
// bookmarks — localStorage
// ---------------------------------------------------------------------------
const LS_KEY = 'fractal.explorer.bookmarks.v1';
function saveBookmarks(s) {
    try {
        localStorage.setItem(LS_KEY, JSON.stringify(s.bookmarks));
    }
    catch {
        /* storage unavailable (file:// privacy mode) — ignore */
    }
}
function loadBookmarks(s) {
    try {
        const raw = localStorage.getItem(LS_KEY);
        if (!raw)
            return;
        const arr = JSON.parse(raw);
        if (Array.isArray(arr)) {
            s.bookmarks = arr.filter((b) => b && typeof b.name === 'string' && typeof b.w === 'number');
        }
    }
    catch {
        /* corrupt storage — ignore */
    }
}
function addBookmark(s, name) {
    s.bookmarks.push({
        name,
        time: Date.now(),
        re: s.view.re,
        im: s.view.im,
        w: s.view.w,
        iters: s.itersOverride,
        palette: s.palette,
        paletteOffset: s.paletteOffset,
        mode: s.mode,
        cRe: s.cRe,
        cIm: s.cIm,
    });
    saveBookmarks(s);
}
function restoreBookmark(s, bm) {
    pushHistory(s);
    s.view = { re: bm.re, im: bm.im, w: bm.w };
    s.itersOverride = typeof bm.iters === 'number' ? bm.iters : 0;
    s.palette = typeof bm.palette === 'number' ? bm.palette : 0;
    s.paletteOffset = typeof bm.paletteOffset === 'number' ? bm.paletteOffset : 0;
    s.mode = bm.mode === 'julia' ? 'julia' : 'mandelbrot';
    s.cRe = typeof bm.cRe === 'number' ? bm.cRe : s.cRe;
    s.cIm = typeof bm.cIm === 'number' ? bm.cIm : s.cIm;
}
function deleteBookmark(s, index) {
    s.bookmarks.splice(index, 1);
    saveBookmarks(s);
}
// ---------------------------------------------------------------------------
// URL hash state (shareable links)
//   #m=<mode>&re=&im=&w=&it=&pal=&off=&cre=&cim=
// ---------------------------------------------------------------------------
function writeHash(s) {
    // String(n) gives the exact round-trippable representation of a double,
    // so a shared link reproduces the view bit-for-bit.
    const h = (s.mode === 'julia' ? 'j' : 'm') +
        `|${s.view.re}|${s.view.im}|${s.view.w}` +
        `|${s.itersOverride}|${s.palette}|${Math.round(s.paletteOffset)}` +
        `|${s.cRe}|${s.cIm}`;
    try {
        history.replaceState(null, '', '#' + h);
    }
    catch {
        // file:// and other contexts block replaceState — direct assignment works
        try {
            location.hash = h;
        }
        catch {
            /* no history access at all — ignore */
        }
    }
}
function readHash(s) {
    let h = '';
    try {
        h = location.hash.slice(1);
    }
    catch {
        return false;
    }
    if (!h)
        return false;
    const parts = h.split('|');
    if (parts.length < 5)
        return false;
    const mode = parts[0] === 'j' ? 'julia' : 'mandelbrot';
    const nums = parts.slice(1).map(Number);
    if (nums.length < 4 || nums.some((n) => !isFinite(n)))
        return false;
    if (mode === 'julia' && parts.length < 9)
        return false;
    s.view = { re: nums[0], im: nums[1], w: nums[2] };
    s.itersOverride = nums[3] || 0;
    s.palette = nums[4] | 0;
    s.paletteOffset = nums[5] | 0;
    if (mode === 'julia') {
        s.cRe = nums[6];
        s.cIm = nums[7];
    }
    clampView(s, s.view);
    return true;
}
function setView(s, re, im, w) {
    s.view.re = re;
    s.view.im = im;
    s.view.w = Math.max(w, 1e-16);
    clampView(s, s.view);
}
};

// ── module: src/renderer.ts ──
__mods["src/renderer.ts"] = function (exports, require, module) {
"use strict";
// ============================================================================
// RENDERER — Canvas2D + ImageData/typed-array pipeline
//
//   compute escape iterations  ->  Float32 buffer (value <= 0 == interior)
//   colorize (palette LUT)     ->  RGBA Uint8ClampedArray (ImageData)
//   blit                       ->  putImageData / scaled drawImage
//
// Responsiveness strategy (frame-by-frame, never a multi-second freeze):
//
//   * PAN          -> instant: the previous full-res frame is blitted shifted
//                     (drawImage of the kept copy canvas). Refinement bands
//                     then replace it row-chunk by row-chunk.
//   * SMALL ZOOM   -> instant: the previous frame is blitted scaled about the
//                     cursor (slightly soft; refinement sharpens it).
//   * BIG ZOOM / FIRST -> adaptive coarse pass: coarse resolution is chosen
//                     so the coarse pass itself costs a bounded number of
//                     escape iterations (a few ms), then refined.
//
// Any view/c change bumps the render version; all in-flight work is dropped
// and the new base is painted completely before refinement, so no ghost
// tiles can survive. Row-chunk rendering resumes across frames within a time
// budget, so each rAF callback stays short.
//
// The per-pixel iteration loop (math.ts) is allocation-free; the renderer
// pre-allocates every buffer on resize and allocates nothing per frame.
// ============================================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.FractalRenderer = void 0;
exports.colorizeBuffer = colorizeBuffer;
exports.colorizeRow = colorizeRow;
exports.finishSync = finishSync;
const consts_1 = require("./consts");
const math_1 = require("./math");
/**
 * Colorize a Float32 iteration buffer into an RGBA Uint8ClampedArray.
 * idx = smooth * scale + offset; wrapped for cyclic palettes, clamped for
 * linear ones. Interior (v <= 0) is rendered black.
 */
function colorizeBuffer(iters, out, lut, offset, maxIter) {
    const scaleCyc = 255 / Math.max(1, Math.log2(maxIter));
    const scaleLin = (255 / Math.max(1, maxIter)) * 1.5;
    const table = lut.table;
    const cyclic = lut.cyclic;
    const n = iters.length;
    for (let i = 0, p = 0; i < n; i++, p += 4) {
        const v = iters[i];
        if (v <= 0) {
            out[p] = 0;
            out[p + 1] = 0;
            out[p + 2] = 0;
            out[p + 3] = 255;
            continue;
        }
        let idx = v * (cyclic ? scaleCyc : scaleLin) + offset;
        if (cyclic) {
            idx = ((idx % 256) + 256) % 256;
        }
        else if (idx >= 255) {
            idx = 255;
        }
        else if (idx < 0) {
            idx = 0;
        }
        const t = (idx | 0) * 3;
        out[p] = table[t];
        out[p + 1] = table[t + 1];
        out[p + 2] = table[t + 2];
        out[p + 3] = 255;
    }
}
/** Colorize one row of `iters` (starting at i0, n values) into out at o4. */
function colorizeRow(iters, i0, n, out, o4, lut, offset, maxIter) {
    const scaleCyc = 255 / Math.max(1, Math.log2(maxIter));
    const scaleLin = (255 / Math.max(1, maxIter)) * 1.5;
    const table = lut.table;
    const cyclic = lut.cyclic;
    for (let i = 0, p = o4; i < n; i++, p += 4) {
        const v = iters[i0 + i];
        if (v <= 0) {
            out[p] = 0;
            out[p + 1] = 0;
            out[p + 2] = 0;
            out[p + 3] = 255;
            continue;
        }
        let idx = v * (cyclic ? scaleCyc : scaleLin) + offset;
        if (cyclic) {
            idx = ((idx % 256) + 256) % 256;
        }
        else if (idx >= 255) {
            idx = 255;
        }
        else if (idx < 0) {
            idx = 0;
        }
        const t = (idx | 0) * 3;
        out[p] = table[t];
        out[p + 1] = table[t + 1];
        out[p + 2] = table[t + 2];
        out[p + 3] = 255;
    }
}
/** Rows per refinement chunk (a chunk is the atomic render+blit unit). */
const CHUNK = consts_1.REFIN_CHUNK_ROWS;
class FractalRenderer {
    constructor(canvas, kind) {
        // full-res buffers
        this.W = 0;
        this.H = 0;
        this.dpr = 1;
        this.iter = new Float32Array(0); // master iteration field
        this.scratchFull = null;
        this.chunkIter = new Float32Array(0);
        this.chunkImg = null;
        // coarse buffers (max 1/4 res; used region may be smaller)
        this.Lo = 0;
        this.Ho = 0;
        this.iterLo = new Float32Array(0);
        this.imageLo = null;
        this.loW = 0;
        this.loH = 0;
        // render parameters
        this.view = { re: 0, im: 0, w: 3 };
        this.cRe = 0;
        this.cIm = 0;
        this.maxIter = 600;
        this.lut = { table: new Uint8Array(768), cyclic: false };
        this.offset = 0;
        // render progress
        this.version = 0;
        this.baseDirty = true; // a base (coarse or preview) must be painted
        this.baseType = 'lo';
        this.row = 0; // full-res rows completed
        this.fullDone = false;
        this.hasFull = false; // fullCopy holds a complete current frame
        this.canvas = canvas;
        this.kind = kind;
        const ctx = canvas.getContext('2d');
        if (!ctx)
            throw new Error('2d context unavailable');
        this.ctx = ctx;
        this.loCanvas = document.createElement('canvas');
        const lctx = this.loCanvas.getContext('2d');
        if (!lctx)
            throw new Error('2d context unavailable (lo)');
        this.loCtx = lctx;
        this.fullCopy = document.createElement('canvas');
        const fctx = this.fullCopy.getContext('2d');
        if (!fctx)
            throw new Error('2d context unavailable (copy)');
        this.fullCopyCtx = fctx;
        if (kind === 'julia') {
            this.cRe = -0.743643887037151;
            this.cIm = 0.13182590420531197;
        }
    }
    /**
     * Change what to draw. Cancels in-flight work and, when the previous
     * complete frame is reusable, blits a cheap preview (pan shift / zoom
     * scale) so the screen updates immediately.
     * anchorCss: optional css-pixel anchor for the zoom preview (cursor).
     */
    setView(v, maxIter, lut, offset, anchorCss) {
        const paramsSame = maxIter === this.maxIter && lut === this.lut && offset === this.offset;
        const reusable = this.hasFull && this.W > 0 && v.w > 0 && paramsSame;
        let baseType = 'lo';
        if (reusable) {
            const ratio = this.view.w / v.w; // >1 = zooming in
            if (Math.abs(ratio - 1) < 1e-9) {
                // pure pan: shift the previous frame
                const s = this.W / v.w; // canvas px per complex unit
                const dx = (this.view.re - v.re) * s;
                const dy = (v.im - this.view.im) * s;
                this.ctx.imageSmoothingEnabled = false;
                this.ctx.clearRect(0, 0, this.W, this.H);
                this.ctx.drawImage(this.fullCopy, Math.round(dx), Math.round(dy));
                baseType = 'pan';
            }
            else if (ratio >= consts_1.ZOOM_PREVIEW_MIN && ratio <= consts_1.ZOOM_PREVIEW_MAX) {
                // small zoom: scale the previous frame about the anchor
                const cssW = this.W / this.dpr;
                const cssH = this.H / this.dpr;
                const ax = (anchorCss ? anchorCss.x : cssW * 0.5) * this.dpr;
                const ay = (anchorCss ? anchorCss.y : cssH * 0.5) * this.dpr;
                const ctx = this.ctx;
                ctx.save();
                ctx.imageSmoothingEnabled = true;
                ctx.setTransform(ratio, 0, 0, ratio, ax * (1 - ratio), ay * (1 - ratio));
                ctx.drawImage(this.fullCopy, 0, 0);
                ctx.restore();
                baseType = 'zoom';
            }
        }
        this.view = { re: v.re, im: v.im, w: v.w };
        this.maxIter = maxIter;
        this.lut = lut;
        this.offset = offset;
        this.baseType = baseType;
        this.baseDirty = baseType === 'lo'; // pan/zoom previews are already painted
        this.row = 0;
        this.fullDone = false;
        this.hasFull = false;
        this.version++;
    }
    setKind(k) {
        if (k === this.kind)
            return;
        this.kind = k;
        this.hasFull = false;
        this.baseType = 'lo';
        this.baseDirty = true;
        this.row = 0;
        this.fullDone = false;
        this.version++;
    }
    setJuliaC(cRe, cIm) {
        if (this.kind !== 'julia')
            return;
        if (cRe === this.cRe && cIm === this.cIm)
            return;
        this.cRe = cRe;
        this.cIm = cIm;
        this.hasFull = false;
        this.baseType = 'lo';
        this.baseDirty = true;
        this.row = 0;
        this.fullDone = false;
        this.version++;
    }
    /** Recolorize from existing iteration buffers — no escape-time recompute. */
    recolorize(lut, offset) {
        this.lut = lut;
        this.offset = offset;
        if (this.W === 0 || this.baseDirty)
            return;
        if (!this.fullDone && this.baseType === 'lo' && this.imageLo) {
            // coarse base on screen: recolorize it
            for (let r = 0; r < this.loH; r++) {
                colorizeRow(this.iterLo, r * this.loW, this.loW, this.imageLo.data, r * this.loW * 4, this.lut, this.offset, this.maxIter);
            }
            this.loCtx.putImageData(this.imageLo, 0, 0);
            this.blitLo();
        }
        else if (this.fullDone && this.scratchFull) {
            colorizeBuffer(this.iter, this.scratchFull.data, this.lut, this.offset, this.maxIter);
            this.ctx.putImageData(this.scratchFull, 0, 0);
            this.fullCopyCtx.putImageData(this.scratchFull, 0, 0);
            this.hasFull = true;
        }
        else if (this.row > 0 && this.scratchFull) {
            // partial: refresh the already-computed rows (dirty-rect blit)
            for (let r = 0; r < this.row; r++) {
                colorizeRow(this.iter, r * this.W, this.W, this.scratchFull.data, r * this.W * 4, this.lut, this.offset, this.maxIter);
            }
            this.ctx.putImageData(this.scratchFull, 0, 0, 0, 0, this.W, this.row);
        }
    }
    blitLo() {
        this.ctx.imageSmoothingEnabled = true;
        this.ctx.drawImage(this.loCanvas, 0, 0, this.loW, this.loH, 0, 0, this.W, this.H);
    }
    resize(cssW, cssH, dpr) {
        const W = Math.max(2, Math.round(cssW * dpr));
        const H = Math.max(2, Math.round(cssH * dpr));
        if (W === this.W && H === this.H && dpr === this.dpr)
            return;
        this.W = W;
        this.H = H;
        this.dpr = dpr;
        this.canvas.width = W;
        this.canvas.height = H;
        this.iter = new Float32Array(W * H);
        this.scratchFull = new ImageData(W, H);
        this.chunkIter = new Float32Array(CHUNK * W);
        this.chunkImg = new ImageData(W, CHUNK);
        this.Lo = Math.max(8, Math.round(W * consts_1.COARSE_MAX_F));
        this.Ho = Math.max(8, Math.round(H * consts_1.COARSE_MAX_F));
        this.loCanvas.width = this.Lo;
        this.loCanvas.height = this.Ho;
        this.fullCopy.width = W;
        this.fullCopy.height = H;
        this.iterLo = new Float32Array(this.Lo * this.Ho);
        this.imageLo = new ImageData(this.Lo, this.Ho);
        this.hasFull = false;
        this.baseType = 'lo';
        this.baseDirty = true;
        this.row = 0;
        this.fullDone = false;
        this.version++;
    }
    /** Advance the render within a time budget. Returns true when done. */
    process(budgetMs) {
        if (this.W === 0)
            return true;
        if (this.baseDirty) {
            this.renderLoPass();
            this.blitLo();
            this.baseDirty = false;
            // (coarse pass is bounded by COARSE_TARGET_ITERS; fall through to refine)
        }
        const start = performance.now();
        while (!this.fullDone) {
            const y = this.row;
            const end = Math.min(this.H, y + CHUNK);
            this.renderChunk(y, end);
            this.row = end;
            if (this.row >= this.H) {
                this.fullDone = true;
                if (this.scratchFull) {
                    this.fullCopyCtx.putImageData(this.scratchFull, 0, 0);
                    this.hasFull = true;
                }
            }
            if (performance.now() - start > budgetMs)
                return false;
        }
        return true;
    }
    // ---- adaptive coarse pass -----------------------------------------------
    renderLoPass() {
        const v = this.view;
        const work = this.W * this.H * this.maxIter;
        let f = Math.sqrt(consts_1.COARSE_TARGET_ITERS / Math.max(1, work));
        if (f > consts_1.COARSE_MAX_F)
            f = consts_1.COARSE_MAX_F;
        if (f < consts_1.COARSE_MIN_F)
            f = consts_1.COARSE_MIN_F;
        const Wf = Math.max(8, Math.min(this.Lo, Math.round(this.W * f)));
        const Hf = Math.max(8, Math.min(this.Ho, Math.round(this.H * f)));
        this.loW = Wf;
        this.loH = Hf;
        const s = v.w / Wf;
        const h = v.w * (Hf / Wf);
        let x0 = v.re - v.w * 0.5 + s * 0.5;
        let y0 = v.im + h * 0.5 - (h / Hf) * 0.5;
        const rowStepY = -(h / Hf);
        const it = this.iterLo;
        const mi = this.maxIter;
        const kr = this.kind;
        const cre = this.cRe, cim = this.cIm;
        for (let py = 0; py < Hf; py++) {
            const y = y0;
            let x = x0;
            let o = py * Wf;
            if (kr === 'mandelbrot') {
                for (let px = 0; px < Wf; px++) {
                    it[o + px] = (0, math_1.mandelIter)(x, y, mi);
                    x += s;
                }
            }
            else {
                for (let px = 0; px < Wf; px++) {
                    it[o + px] = (0, math_1.juliaIter)(x, y, cre, cim, mi);
                    x += s;
                }
            }
            y0 += rowStepY;
        }
        const img = this.imageLo;
        for (let r = 0; r < Hf; r++) {
            colorizeRow(it, r * Wf, Wf, img.data, r * Wf * 4, this.lut, this.offset, mi);
        }
        // putImageData of the full buffer is fine: blitLo only draws the used
        // region (loW x loH), so stale pixels outside are never displayed
        this.loCtx.putImageData(img, 0, 0);
    }
    // ---- full-res chunk: rows y..end (atomic render + blit unit) -------------
    renderChunk(y, end) {
        const v = this.view;
        const W = this.W, H = this.H;
        const s = v.w / W;
        const h = v.w * (H / W);
        const it = this.chunkIter;
        const mi = this.maxIter;
        const kr = this.kind;
        const cre = this.cRe, cim = this.cIm;
        const rows = end - y;
        for (let py = y; py < end; py++) {
            const yy = v.im + h * 0.5 - (py + 0.5) * (h / H);
            let x = v.re - v.w * 0.5 + s * 0.5;
            let o = (py - y) * W;
            if (kr === 'mandelbrot') {
                for (let px = 0; px < W; px++) {
                    it[o + px] = (0, math_1.mandelIter)(x, yy, mi);
                    x += s;
                }
            }
            else {
                for (let px = 0; px < W; px++) {
                    it[o + px] = (0, math_1.juliaIter)(x, yy, cre, cim, mi);
                    x += s;
                }
            }
        }
        const img = this.chunkImg;
        for (let r = 0; r < rows; r++) {
            colorizeRow(it, r * W, W, img.data, r * W * 4, this.lut, this.offset, mi);
        }
        this.ctx.putImageData(img, 0, y, 0, 0, W, rows);
        // stash the chunk's iteration values in the master buffer
        const master = this.iter;
        for (let r = 0; r < rows; r++) {
            master.set(it.subarray(r * W, (r + 1) * W), (y + r) * W);
        }
        // keep the scratch master in sync for recolorize
        if (this.scratchFull) {
            for (let r = 0; r < rows; r++) {
                this.scratchFull.data.set(img.data.subarray(r * W * 4, (r + 1) * W * 4), (y + r) * W * 4);
            }
        }
    }
}
exports.FractalRenderer = FractalRenderer;
/** Synchronously finish all passes (tests / debug interface). */
function finishSync(r) {
    let n = 0;
    while (!r.process(1e9) && n++ < 512) {
        /* keep going */
    }
}
};

// ── module: src/ui.ts ──
__mods["src/ui.ts"] = function (exports, require, module) {
"use strict";
// ============================================================================
// UI CONSTRUCTION — DOM building (elements created here, wired in main.ts)
// Glassy HUD, toolbar with palette swatch chips, Julia pane, toast, help bar.
// ============================================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.paletteGradient = paletteGradient;
exports.buildHUD = buildHUD;
exports.buildToolbar = buildToolbar;
exports.buildJuliaPane = buildJuliaPane;
exports.buildToast = buildToast;
exports.buildHelp = buildHelp;
exports.updateHUD = updateHUD;
exports.syncIterLabel = syncIterLabel;
exports.syncPaletteChips = syncPaletteChips;
exports.fillBookmarkList = fillBookmarkList;
const consts_1 = require("./consts");
const math_1 = require("./math");
function el(tag, cls, text) {
    const e = document.createElement(tag);
    if (cls)
        e.className = cls;
    if (text !== undefined)
        e.textContent = text;
    return e;
}
/** Gradient CSS for a palette's swatch chip. */
function paletteGradient(i) {
    const p = consts_1.PALETTES[i];
    const n = p.stops.length / 3;
    const parts = [];
    for (let s = 0; s < n; s++) {
        const pct = Math.round((s / (n - 1)) * 100);
        parts.push(`rgb(${p.stops[s * 3]},${p.stops[s * 3 + 1]},${p.stops[s * 3 + 2]}) ${pct}%`);
    }
    return `linear-gradient(90deg, ${parts.join(', ')})`;
}
function buildHUD() {
    const root = el('div', 'hud');
    const mag = el('div', 'hud-mag');
    const coords = el('div', 'hud-sub');
    const iter = el('div', 'hud-iter');
    const fps = el('div', 'hud-sub');
    const actions = el('div', 'hud-actions');
    const back = el('button', 'hud-btn', '⟲ back');
    const reset = el('button', 'hud-btn', '⌂ reset');
    const juliaBtn = el('button', 'hud-btn', 'J · julia');
    back.title = 'Zoom back (history)';
    reset.title = 'Reset to default view';
    juliaBtn.title = 'Toggle Julia view (J)';
    actions.append(back, reset, juliaBtn);
    root.append(mag, coords, iter, fps, actions);
    return { root, mag, coords, iter, fps, back, reset, juliaBtn };
}
function buildToolbar() {
    const root = el('div', 'toolbar');
    const chips = consts_1.PALETTES.map((p, i) => {
        const b = el('button', 'chip');
        b.style.background = paletteGradient(i);
        b.title = p.name;
        b.setAttribute('aria-label', 'palette ' + p.name);
        return b;
    });
    const offWrap = el('label', 'ctl');
    const offLbl = el('span', 'ctl-lbl', 'color');
    const offset = el('input');
    offset.type = 'range';
    offset.min = '0';
    offset.max = '255';
    offset.value = '0';
    offset.step = '1';
    offWrap.append(offLbl, offset);
    const drift = el('input');
    drift.type = 'checkbox';
    drift.id = 'drift-chk';
    const driftWrap = el('label', 'ctl chk', 'drift');
    driftWrap.append(drift);
    const itWrap = el('label', 'ctl');
    const itLbl = el('span', 'ctl-lbl', 'iterations');
    const iters = el('input');
    iters.type = 'range';
    iters.min = '0';
    iters.max = String(consts_1.MAX_ITERS);
    iters.value = '0';
    iters.step = '10';
    const itersVal = el('span', 'ctl-val', 'auto');
    itWrap.append(itLbl, iters, itersVal);
    const live = el('input');
    live.type = 'checkbox';
    live.checked = true;
    live.id = 'live-chk';
    const liveWrap = el('label', 'ctl chk', 'live julia');
    liveWrap.append(live);
    const sep = el('span', 'sep');
    const bmSave = el('button', 'btn', '+ bookmark');
    const bmList = el('select');
    bmList.className = 'btn sel';
    const ph = document.createElement('option');
    ph.value = '';
    ph.textContent = 'bookmarks…';
    bmList.append(ph);
    const bmDel = el('button', 'btn', 'del');
    bmDel.disabled = true;
    const copyLink = el('button', 'btn', 'copy link');
    root.append(...chips, sep, offWrap, driftWrap, itWrap, liveWrap, sep, bmSave, bmList, bmDel, copyLink);
    return {
        root, chips, offset, drift, iters, itersVal, live,
        bmSave, bmList, bmDel, copyLink,
    };
}
function buildJuliaPane() {
    const root = el('aside', 'julia-pane');
    const head = el('div', 'jp-head');
    const title = el('span', 'jp-title', 'JULIA');
    const cLabel = el('span', 'jp-c');
    const close = el('button', 'jp-close', '×');
    close.title = 'Close Julia pane';
    head.append(title, cLabel, close);
    const canvas = el('canvas', 'jp-canvas');
    const hint = el('span', 'jp-hint', 'hover the Mandelbrot to move c · click sets c');
    root.append(head, canvas, hint);
    return { root, canvas, close, cLabel, hint };
}
// ---------------------------------------------------------------------------
function buildToast() {
    return el('div', 'toast');
}
function buildHelp() {
    const h = el('div', 'help');
    h.innerHTML =
        '<b>drag</b> zoom-rect &nbsp;·&nbsp; <b>drag</b>(pan mode) / arrows pan &nbsp;·&nbsp; ' +
            '<b>wheel</b> zoom &nbsp;·&nbsp; <b>dbl-click</b> / <b>J</b> julia &nbsp;·&nbsp; ' +
            '<b>+</b>/<b>−</b> zoom &nbsp;·&nbsp; <b>R</b> reset';
    return h;
}
// ---------------------------------------------------------------------------
// HUD refresh (called a few times per second, not every frame)
// ---------------------------------------------------------------------------
function updateHUD(h, s, fps, effIter, autoIter) {
    const mag = (0, math_1.magOf)(s.view, s.w0);
    h.mag.textContent = (0, math_1.fmtMag)(mag);
    h.mag.classList.toggle('warn', mag > consts_1.WARN_MAG);
    h.coords.textContent = `re ${(0, math_1.fmtCoord)(s.view.re)}   im ${(0, math_1.fmtCoord)(s.view.im)}`;
    h.iter.textContent = `iter ${effIter}` + (s.itersOverride === 0 ? ` (auto ${autoIter})` : '');
    h.fps.textContent = `${Math.round(fps)} fps · ${s.mode === 'julia' ? 'julia' : 'mandelbrot'}` +
        (s.mode === 'julia' ? `  c=(${(0, math_1.fmtCoord)(s.cRe)}, ${(0, math_1.fmtCoord)(s.cIm)})` : '');
}
/** Keep the iterations slider label in sync. */
function syncIterLabel(t, s) {
    const mag = (0, math_1.magOf)(s.view, s.w0);
    const auto = s.itersOverride === 0;
    t.itersVal.textContent = auto ? `auto ${(0, math_1.autoIterations)(mag)}` : String(s.itersOverride);
    t.iters.value = String(s.itersOverride);
}
function syncPaletteChips(t, selected) {
    t.chips.forEach((c, i) => c.classList.toggle('on', i === selected));
}
function fillBookmarkList(sel, names, current) {
    sel.innerHTML = '';
    const ph = document.createElement('option');
    ph.value = '';
    ph.textContent = 'bookmarks…';
    sel.append(ph);
    names.forEach((n, i) => {
        const o = document.createElement('option');
        o.value = String(i);
        o.textContent = n;
        sel.append(o);
    });
    if (current >= 0 && current < names.length)
        sel.value = String(current);
}
};

// ── module: src/input.ts ──
__mods["src/input.ts"] = function (exports, require, module) {
"use strict";
// ============================================================================
// INPUT — pointer & wheel navigation for a fractal canvas
//
//   plain drag  : rectangle zoom (live overlay reported via onRect)
//   shift+drag  : pan
//   wheel       : smooth zoom toward cursor (pinned at the precision cap)
//   dbl-click   : 3× zoom toward cursor
//   click       : onPick (complex point under cursor)
//   hover       : onHover (complex point + css coords)
// ============================================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.NavInput = void 0;
const consts_1 = require("./consts");
const math_1 = require("./math");
class NavInput {
    constructor(t) {
        this.mode = 'none';
        this.sx = 0;
        this.sy = 0;
        this.cx = 0;
        this.cy = 0;
        this.startView = { re: 0, im: 0, w: 1 };
        this.pushed = false;
        this.lastHist = 0;
        this.onDown = (e) => {
            if (e.button !== 0)
                return;
            const [x, y] = this.px(e);
            this.mode = e.shiftKey ? 'pan' : 'rect';
            this.sx = x;
            this.sy = y;
            this.cx = x;
            this.cy = y;
            this.startView = { ...this.t.getView() };
            this.pushed = false;
            this.rect(false);
        };
        this.onMove = (e) => {
            if (e.target !== this.t.canvas && this.mode !== 'none') {
                // pointer left the canvas while dragging — keep going (window listener)
            }
            const [x, y] = this.px(e);
            if (this.mode === 'none') {
                // hover only while actually over the canvas
                const r = this.t.canvas.getBoundingClientRect();
                if (e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom) {
                    return;
                }
                const p = this.pointFromCss(x, y);
                this.t.onHover?.(p.re, p.im, x, y);
                return;
            }
            this.cx = x;
            this.cy = y;
            if (!this.pushed && Math.hypot(this.cx - this.sx, this.cy - this.sy) > 3) {
                this.pushed = true;
                const now = performance.now();
                if (now - this.lastHist > 350) {
                    this.t.pushHistory();
                    this.lastHist = now;
                }
            }
            if (this.mode === 'pan') {
                const W = this.t.canvas.clientWidth || 1;
                const s = this.startView.w / W;
                this.t.commit({
                    re: this.startView.re - (this.cx - this.sx) * s,
                    im: this.startView.im + (this.cy - this.sy) * s,
                    w: this.startView.w,
                });
            }
            else {
                this.rect(true);
            }
        };
        this.onUp = (e) => {
            if (this.mode === 'none')
                return;
            const m = this.mode;
            this.mode = 'none';
            this.rect(false);
            void e;
            const dx = this.cx - this.sx;
            const dy = this.cy - this.sy;
            const dist = Math.hypot(dx, dy);
            const W = this.t.canvas.clientWidth || 1;
            const H = this.t.canvas.clientHeight || 1;
            if (m === 'rect') {
                if (dist <= 4) {
                    // plain click -> pick the point
                    const p = this.pointFromCss(this.cx, this.cy);
                    this.t.onPick?.(p.re, p.im);
                    return;
                }
                // ---- rectangle zoom: frame exactly the selected region ----
                const x0 = Math.min(this.sx, this.cx);
                const x1 = Math.max(this.sx, this.cx);
                const y0 = Math.min(this.sy, this.cy);
                const y1 = Math.max(this.sy, this.cy);
                const v = this.startView;
                const s = v.w / W;
                const h = v.w * (H / W);
                const reL = v.re - v.w * 0.5 + x0 * s;
                const reR = v.re - v.w * 0.5 + x1 * s;
                const imT = v.im + h * 0.5 - y0 * (h / H);
                const imB = v.im + h * 0.5 - y1 * (h / H);
                const rw = reR - reL;
                const rh = imT - imB;
                // fit the whole rectangle: width >= rw and height (w*H/W) >= rh
                const w = Math.max(rw, rh * (W / H));
                const re = (reL + reR) * 0.5;
                const im = (imT + imB) * 0.5;
                if (!this.pushed) {
                    const now = performance.now();
                    if (now - this.lastHist > 350) {
                        this.t.pushHistory();
                        this.lastHist = now;
                    }
                }
                this.t.commit({ re, im, w });
            }
        };
        this.onCancel = () => {
            this.mode = 'none';
            this.rect(false);
        };
        this.onWheel = (e) => {
            e.preventDefault();
            const [x, y] = this.px(e);
            const W = this.t.canvas.clientWidth || 1;
            const H = this.t.canvas.clientHeight || 1;
            const v = this.t.getView();
            const k = Math.pow(consts_1.WHEEL_ZOOM_FACTOR, -e.deltaY / 100);
            let nw = v.w / k;
            const minW = this.t.w0 / consts_1.MAX_MAG;
            if (nw < minW)
                nw = minW; // precision cap — zoom pins, doesn't blow past
            const p = this.pointFromCss(x, y);
            const s = nw / W;
            this.t.commit({
                re: p.re - (x - W * 0.5) * s,
                im: p.im + (y - H * 0.5) * s,
                w: nw,
            }, { x, y });
            const now = performance.now();
            if (now - this.lastHist > 500) {
                this.t.pushHistory();
                this.lastHist = now;
            }
        };
        this.onDbl = (e) => {
            const [x, y] = this.px(e);
            const W = this.t.canvas.clientWidth || 1;
            const H = this.t.canvas.clientHeight || 1;
            const v = this.t.getView();
            const p = this.pointFromCss(x, y);
            const nw = Math.max(v.w / 3, this.t.w0 / consts_1.MAX_MAG);
            const s = nw / W;
            this.t.commit({
                re: p.re - (x - W * 0.5) * s,
                im: p.im + (y - H * 0.5) * s,
                w: nw,
            });
            this.t.pushHistory();
        };
        this.t = t;
        const c = t.canvas;
        c.style.touchAction = 'none';
        c.addEventListener('pointerdown', this.onDown);
        window.addEventListener('pointermove', this.onMove);
        window.addEventListener('pointerup', this.onUp);
        c.addEventListener('pointercancel', this.onCancel);
        c.addEventListener('wheel', this.onWheel, { passive: false });
        c.addEventListener('dblclick', this.onDbl);
    }
    dispose() {
        const c = this.t.canvas;
        c.removeEventListener('pointerdown', this.onDown);
        window.removeEventListener('pointermove', this.onMove);
        window.removeEventListener('pointerup', this.onUp);
        c.removeEventListener('pointercancel', this.onCancel);
        c.removeEventListener('wheel', this.onWheel);
        c.removeEventListener('dblclick', this.onDbl);
    }
    /** css pixel coordinates of a pointer event relative to the canvas. */
    px(e) {
        const r = this.t.canvas.getBoundingClientRect();
        return [e.clientX - r.left, e.clientY - r.top];
    }
    rect(active) {
        this.t.onRect?.(this.sx, this.sy, this.cx, this.cy, active);
    }
    /** Complex-plane point under a css pixel. */
    pointFromCss(cssX, cssY) {
        const W = this.t.canvas.clientWidth || 1;
        const H = this.t.canvas.clientHeight || 1;
        const v = this.t.getView();
        const s = v.w / W;
        return {
            re: v.re + (cssX - W * 0.5) * s,
            im: v.im - (cssY - H * 0.5) * s,
        };
    }
    /** Magnification of the current view. */
    currentMag() {
        return (0, math_1.magOf)(this.t.getView(), this.t.w0);
    }
}
exports.NavInput = NavInput;
};

// ── module: src/overlay.ts ──
__mods["src/overlay.ts"] = function (exports, require, module) {
"use strict";
// ============================================================================
// OVERLAY — zoom rectangle (inverted-color border), reticle crosshair, toast
// Drawn over the main canvas; never touches the iteration buffers.
// ============================================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.Toast = exports.Overlay = void 0;
class Overlay {
    constructor(canvas) {
        this.W = 0;
        this.H = 0;
        this.rect = {
            x0: 0, y0: 0, x1: 0, y1: 0, active: false,
        };
        this.reticle = { x: 0, y: 0, show: false };
        const ctx = canvas.getContext('2d');
        if (!ctx)
            throw new Error('2d context unavailable (overlay)');
        this.ctx = ctx;
    }
    resize(cssW, cssH, dpr) {
        const W = Math.max(1, Math.round(cssW * dpr));
        const H = Math.max(1, Math.round(cssH * dpr));
        if (W === this.W && H === this.H)
            return;
        this.W = W;
        this.H = H;
        this.ctx.canvas.width = W;
        this.ctx.canvas.height = H;
        this.ctx.clearRect(0, 0, W, H);
    }
    /** Clear everything. */
    clear() {
        this.rect.active = false;
        this.reticle.show = false;
        this.ctx.clearRect(0, 0, this.W, this.H);
    }
    /** Draw the current overlay state (call once per frame). */
    draw() {
        const ctx = this.ctx;
        const dpr = this.W / Math.max(1, (this.ctx.canvas.clientWidth || 1));
        ctx.clearRect(0, 0, this.W, this.H);
        if (this.rect.active)
            this.drawRect(dpr);
        if (this.reticle.show)
            this.drawReticle(dpr);
    }
    drawRect(dpr) {
        const ctx = this.ctx;
        const x0 = Math.min(this.rect.x0, this.rect.x1) * dpr;
        const y0 = Math.min(this.rect.y0, this.rect.y1) * dpr;
        const w = Math.abs(this.rect.x1 - this.rect.x0) * dpr;
        const h = Math.abs(this.rect.y1 - this.rect.y0) * dpr;
        if (w < 2 || h < 2)
            return;
        // inverted-color border: 1px white outline, 3px black halo just outside
        ctx.save();
        ctx.lineJoin = 'miter';
        ctx.strokeStyle = 'rgba(0,0,0,0.9)';
        ctx.lineWidth = 6 * dpr * 0.5;
        ctx.strokeRect(x0 - 3 * dpr * 0.5, y0 - 3 * dpr * 0.5, w + 3 * dpr, h + 3 * dpr);
        ctx.strokeStyle = 'rgba(255,255,255,0.95)';
        ctx.lineWidth = 1.2 * dpr;
        ctx.strokeRect(x0, y0, w, h);
        // corner ticks
        const t = 8 * dpr;
        ctx.lineWidth = 1.6 * dpr;
        ctx.strokeStyle = '#fff';
        ctx.beginPath();
        ctx.moveTo(x0, y0 + t);
        ctx.lineTo(x0, y0);
        ctx.lineTo(x0 + t, y0);
        ctx.moveTo(x0 + w - t, y0);
        ctx.lineTo(x0 + w, y0);
        ctx.lineTo(x0 + w, y0 + t);
        ctx.moveTo(x0 + w, y0 + h - t);
        ctx.lineTo(x0 + w, y0 + h);
        ctx.lineTo(x0 + w - t, y0 + h);
        ctx.moveTo(x0 + t, y0 + h);
        ctx.lineTo(x0, y0 + h);
        ctx.lineTo(x0, y0 + h - t);
        ctx.stroke();
        ctx.restore();
    }
    drawReticle(dpr) {
        const ctx = this.ctx;
        const x = this.reticle.x * dpr;
        const y = this.reticle.y * dpr;
        const r = 14 * dpr;
        ctx.save();
        // soft halo
        ctx.strokeStyle = 'rgba(255,255,255,0.35)';
        ctx.lineWidth = 4 * dpr * 0.5;
        ctx.beginPath();
        ctx.arc(x, y, r, 0, Math.PI * 2);
        ctx.stroke();
        // crisp ring
        ctx.strokeStyle = 'rgba(255,255,255,0.9)';
        ctx.lineWidth = 1.2 * dpr;
        ctx.beginPath();
        ctx.arc(x, y, r, 0, Math.PI * 2);
        ctx.stroke();
        // crosshair arms
        const a = r * 0.45;
        ctx.beginPath();
        ctx.moveTo(x - r - a, y);
        ctx.lineTo(x - r + 2 * dpr, y);
        ctx.moveTo(x + r - 2 * dpr, y);
        ctx.lineTo(x + r + a, y);
        ctx.moveTo(x, y - r - a);
        ctx.lineTo(x, y - r + 2 * dpr);
        ctx.moveTo(x, y + r - 2 * dpr);
        ctx.lineTo(x, y + r + a);
        ctx.stroke();
        // center dot
        ctx.fillStyle = 'rgba(255,255,255,0.95)';
        ctx.beginPath();
        ctx.arc(x, y, 1.6 * dpr, 0, Math.PI * 2);
        ctx.fill();
        ctx.restore();
    }
}
exports.Overlay = Overlay;
class Toast {
    constructor(el) {
        this.timer = null;
        this.el = el;
    }
    show(msg, ms = 2600) {
        this.el.textContent = msg;
        this.el.classList.add('show');
        if (this.timer !== null)
            clearTimeout(this.timer);
        this.timer = window.setTimeout(() => {
            this.el.classList.remove('show');
            this.timer = null;
        }, ms);
    }
}
exports.Toast = Toast;
};

// ── types: globals.d.ts ──
__mods["globals.d.ts"] = function (exports) {};

// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"unsloth/Qwen3.8-27B-GGUF","provider":"brigid","persona":"typescript","sessionId":"df21a283-c64a-4987-bf01-0e097472e70f","tokensIn":8355980,"tokensOut":279614,"tokensTotal":8635594,"cost":0,"turns":120,"toolCalls":140,"failedToolCalls":3,"timestamp":"2026-08-15T09:33:27.776Z"} -->