← Fractal Explorer results

Fractal Explorer

Deepseek v4 Flash 0731 · 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 60,269 bytes · SHA-256 83c429b906ce
<!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: base ── */
html,body{margin:0;padding:0;height:100%;background:#000;overflow:hidden;font-family:ui-monospace,Menlo,Consolas,monospace;}
#app{position:fixed;inset:0;}
#fractal{position:absolute;inset:0;width:100%;height:100%;display:block;cursor:crosshair;touch-action:none;}
/* ── css: hud ── */
#hud{position:absolute;left:14px;top:14px;z-index:5;color:#dfe7ff;
  background:rgba(10,14,30,.55);backdrop-filter:blur(10px) saturate(1.2);
  -webkit-backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,.12);
  border-radius:12px;padding:10px 12px;font-size:12px;line-height:1.5;
  box-shadow:0 8px 30px rgba(0,0,0,.4);max-width:300px;transition:opacity .5s,transform .5s;}
#hud.idle{opacity:0;transform:translateY(-6px);pointer-events:none;}
#hud-top{display:flex;gap:10px;flex-wrap:wrap;align-items:baseline;}
.chip{background:linear-gradient(135deg,#5b8cff,#9a5bff);padding:1px 9px;border-radius:999px;
  font-weight:600;letter-spacing:.03em;color:#fff;}
.stat{opacity:.85;}
.stat.warn{color:#ffb84d;background:rgba(255,150,0,.15);padding:0 6px;border-radius:6px;opacity:1;}
#hud-controls{margin-top:10px;border-top:1px solid rgba(255,255,255,.1);padding-top:8px;}
#palettes{display:flex;gap:6px;margin-bottom:8px;flex-wrap:wrap;}
.swatch{width:26px;height:18px;border-radius:5px;border:1px solid rgba(255,255,255,.25);
  cursor:pointer;opacity:.55;transition:opacity .15s,transform .15s;}
.swatch.on{opacity:1;transform:scale(1.12);border-color:#fff;box-shadow:0 0 8px rgba(255,255,255,.5);}
.sliders{display:flex;gap:14px;flex-wrap:wrap;align-items:center;}
.sliders label{display:flex;align-items:center;gap:5px;opacity:.85;}
input[type=range]{accent-color:#6c8cff;height:4px;width:110px;}
.btns{display:flex;gap:6px;margin-top:8px;flex-wrap:wrap;}
.btns button,.bm-row button{background:rgba(255,255,255,.08);color:#dfe7ff;border:1px solid rgba(255,255,255,.14);
  border-radius:7px;padding:4px 9px;cursor:pointer;font-size:12px;font-family:inherit;}
.btns button:hover,.bm-row button:hover{background:rgba(255,255,255,.18);}
#bmarks{margin-top:10px;border-top:1px solid rgba(255,255,255,.1);padding-top:8px;}
.bm-row{display:flex;gap:6px;margin-bottom:6px;}
#bm-name{flex:1;background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.15);
  color:#fff;border-radius:7px;padding:4px 8px;font-family:inherit;}
#bm-list{list-style:none;margin:0;padding:0;max-height:140px;overflow:auto;}
#bm-list li{display:flex;align-items:center;gap:8px;padding:3px 0;justify-content:space-between;border-bottom:1px solid rgba(255,255,255,.06);}
#bm-list .nm{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:150px;}
#bm-list button{background:none;border:none;color:#8fa4ff;cursor:pointer;font-size:12px;}
#help{margin-top:10px;border-top:1px solid rgba(255,255,255,.1);padding-top:8px;font-size:11px;opacity:.85;line-height:1.6;}
#preview-wrap{position:absolute;top:14px;right:14px;z-index:4;text-align:center;
  background:rgba(10,14,30,.5);backdrop-filter:blur(8px);border:1px solid rgba(255,255,255,.12);
  border-radius:10px;padding:6px;}
#preview-label{font-size:10px;color:#aab8e8;margin-bottom:4px;letter-spacing:.04em;}
/* ── css: overlays ── */
#zoombox{position:absolute;z-index:3;display:none;pointer-events:none;
  outline:1.5px dashed #fff;outline-offset:-1.5px;
  box-shadow:0 0 0 1px rgba(0,0,0,.6), inset 0 0 0 1px rgba(0,0,0,.6);
  background:rgba(255,255,255,.06);mix-blend-mode:difference;}
#reticle{position:absolute;z-index:2;display:none;pointer-events:none;}
#reticle .hr,#reticle .vr{position:absolute;background:rgba(255,255,255,.7);
  box-shadow:0 0 0 1px rgba(0,0,0,.5);}
#reticle .hr{height:1px;width:100%;}
#reticle .vr{width:1px;height:100%;}
</style>
</head>
<body>
<div id="app"><canvas id="fractal"></canvas>
  <div id="preview-wrap" hidden="">
    <div id="preview-label">Julia preview</div>
    <canvas id="preview" width="112" height="112"></canvas>
  </div>
  <div id="reticle"><i class="hr"></i><i class="vr"></i></div>
  <div id="zoombox"></div>
  <div id="hud">
    <div id="hud-top">
      <span class="chip" id="mode-chip">Mandelbrot</span>
      <span class="stat" id="mag">×1.0e0</span>
      <span class="stat" id="center">c = 0 + 0i</span>
      <span class="stat" id="iters">iter 220</span>
      <span class="stat" id="fps">0 fps</span>
      <span class="stat warn" id="warn" hidden="">precision limit</span>
    </div>
    <div id="hud-controls">
      <div id="palettes"></div>
      <div class="sliders">
        <label>Cycle <input id="offset" type="range" min="0" max="99" value="0"></label>
        <label>Iter <input id="iter" type="range" min="30" max="200000" value="300"><button id="iter-auto" title="auto iter limit">AUTO</button></label>
      </div>
      <div class="btns">
        <button id="btn-back" title="Back (B)">◀ Back</button>
        <button id="btn-reset" title="Reset view (0)">Reset</button>
        <button id="btn-julia" title="Julia (J)">Julia</button>
        <button id="btn-bmarks" title="Bookmarks (K)">★</button>
        <button id="btn-help" title="Help (H)">?</button>
      </div>
    </div>
    <div id="bmarks" hidden="">
      <div class="bm-row"><input id="bm-name" placeholder="Bookmark name"><button id="bm-save">Save</button></div>
      <ul id="bm-list"></ul>
    </div>
    <div id="help" hidden="">
      <b>Drag</b> = zoom box &nbsp; <b>Shift-drag / right-drag</b> = pan &nbsp; <b>Wheel</b> = zoom to cursor<br>
      <b>Click</b> (Mandelbrot) = open Julia there &nbsp; <b>J</b> = toggle Julia<br>
      <b>Arrows</b> = pan &nbsp; <b>+/-</b> = zoom &nbsp; <b>0</b> = reset &nbsp; <b>B</b> = back &nbsp; <b>1-4</b> = palette
    </div>
  </div></div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./config":"src/config.ts","./state":"src/state.ts","./actions":"src/actions.ts","./render":"src/render.ts","./preview":"src/preview.ts","./input":"src/input.ts","./hud":"src/hud.ts","./bookmarks":"src/bookmarks.ts","./fractal":"src/fractal.ts"},"src/fractal.ts":{"./config":"src/config.ts"},"src/palettes.ts":{"./config":"src/config.ts"},"src/state.ts":{"./fractal":"src/fractal.ts"},"src/actions.ts":{"./state":"src/state.ts","./config":"src/config.ts","./fractal":"src/fractal.ts"},"src/render.ts":{"./config":"src/config.ts","./state":"src/state.ts","./fractal":"src/fractal.ts","./palettes":"src/palettes.ts"},"src/preview.ts":{"./config":"src/config.ts","./state":"src/state.ts","./fractal":"src/fractal.ts","./palettes":"src/palettes.ts"},"src/input.ts":{"./config":"src/config.ts","./state":"src/state.ts","./actions":"src/actions.ts","./preview":"src/preview.ts"},"src/hud.ts":{"./config":"src/config.ts","./state":"src/state.ts","./actions":"src/actions.ts","./palettes":"src/palettes.ts","./render":"src/render.ts"},"src/bookmarks.ts":{"./config":"src/config.ts","./state":"src/state.ts","./actions":"src/actions.ts","./fractal":"src/fractal.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";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
// ============================================================
//  ENTRY — wire renderer, preview, input, HUD, bookmarks, resize
//  and expose a headless debug interface on `window.fractal`.
// ============================================================
const config_1 = require("./config");
const state_1 = require("./state");
const act = __importStar(require("./actions"));
const render_1 = require("./render");
const preview_1 = require("./preview");
const input_1 = require("./input");
const hud_1 = require("./hud");
const bookmarks_1 = require("./bookmarks");
const fractal_1 = require("./fractal");
const canvas = document.getElementById('fractal');
const renderer = new render_1.Renderer(canvas);
const preview = new preview_1.Preview(document.getElementById('preview'));
const hud = (0, hud_1.setupHud)(renderer);
// ------- central callbacks -----------------------------------
state_1.state.onViewChange = () => { renderer.renderView(); hud.update(); (0, bookmarks_1.syncHash)(); };
state_1.state.onActivity = () => { hud.activity(); };
function resize() {
    const dpr = Math.min(window.devicePixelRatio || 1, config_1.CONFIG.MAX_DPR);
    const W = Math.max(2, Math.round(window.innerWidth * dpr));
    const H = Math.max(2, Math.round(window.innerHeight * dpr));
    renderer.setSize(W, H);
    act.setSize(W, H);
    renderer.renderView();
}
window.addEventListener('resize', resize);
// ------- debug interface -------------------------------------
const win = window;
win.fractal = {
    /** Integer escape-iteration count for the current set (interior -> limit). */
    iterationsAt(re, im) {
        return (0, fractal_1.iterationsAt)(re, im, state_1.state.mode, state_1.state.cRe, state_1.state.cIm, state_1.state.limit);
    },
    /** Set the view. `scale` = complex units per pixel (same in x & y). */
    setView(v) {
        if (v.re !== undefined)
            state_1.state.re = v.re;
        if (v.im !== undefined)
            state_1.state.im = v.im;
        if (v.scale !== undefined)
            state_1.state.step = v.scale;
        if (v.cRe !== undefined)
            state_1.state.cRe = v.cRe;
        if (v.cIm !== undefined)
            state_1.state.cIm = v.cIm;
        if (v.limit !== undefined) {
            state_1.state.limit = v.limit;
            state_1.state.limitAuto = false;
        }
        if (v.mode !== undefined) {
            state_1.state.mode = v.mode;
        }
        act.apply();
    },
    getView() {
        return { re: state_1.state.re, im: state_1.state.im, step: state_1.state.step, mode: state_1.state.mode,
            mag: config_1.CONFIG.INIT_STEP / state_1.state.step, limit: state_1.state.limit, cRe: state_1.state.cRe, cIm: state_1.state.cIm };
    },
    computeZoomRect: (x0, y0, x1, y1) => act.computeZoomRect(x0, y0, x1, y1),
    zoomToRect: (x0, y0, x1, y1) => act.zoomToRect(x0, y0, x1, y1),
    zoomAt: (f, x, y) => act.zoomAt(f, x, y),
    resetView: act.resetView,
    magnification: act.magnification,
    restore(s) { (0, bookmarks_1.restoreSnapshot)(s); },
    save() { return (0, bookmarks_1.currentSnapshot)(); },
    state: state_1.state,
    CONFIG: config_1.CONFIG,
};
// ------- boot ------------------------------------------------
(0, bookmarks_1.setupBookmarks)();
(0, input_1.setupInput)(preview);
resize(); // size + first render
(0, bookmarks_1.applyHash)(); // if a shareable link is present, adopt it
};

// ── module: src/config.ts ──
__mods["src/config.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CONFIG = void 0;
// ============================================================
//  TUNABLE CONSTANTS — edit these to tune behavior/performance
// ============================================================
exports.CONFIG = {
    // --- iteration limit (auto mode) ---------------------------
    BASE_ITER: 220, // base iterations at magnification ~1
    ITER_PER_DECADE: 85, // extra iterations per factor-of-10 of zoom
    MIN_ITER: 60, // floor for auto limit
    MAX_ITER: 200000, // ceiling for auto limit (also slider max)
    // --- escape / coloring -------------------------------------
    BAIL2: 256, // escape radius squared (|z|^2 > BAIL2)
    COLOR_SCALE: 36, // iterations per color cycle (density)
    COLORS_PER_CYCLE_SLIDER_STEP: 1,
    // --- view / navigation -------------------------------------
    INIT_WIDTH: 3.4, // real span of the default Mandelbrot view
    JULIA_WIDTH: 4.0, // real span of a fresh Julia view
    INIT_STEP: 0.0034, // = INIT_WIDTH / 1000  (per-pixel complex step at mag 1)
    ZOOM_FACTOR: 1.35, // wheel zoom multiplier per notch (in/out)
    PAN_SPEED: 1.0,
    MOMENTUM: 0.92, // pan inertia decay (0 = none)
    // --- precision guard ---------------------------------------
    MAX_MAG: 1e15, // hard stop: block further zooming past this
    WARN_MAG: 1e13, // show a warning past this magnification
    // --- progressive rendering ---------------------------------
    COARSE_DIV: 4, // coarse pass samples every 4th pixel
    DW_CHUNK: 60, // coarse: rows computed per scheduler tick
    REFINE_CHUNK: 22, // refine: full-res rows per scheduler tick
    TICK_MS: 0, // setTimeout delay between chunks (yield)
    // --- misc --------------------------------------------------
    IDLE_MS: 2600, // hide HUD after this much inactivity
    PREVIEW: 112, // mini preview pane size (px)
    PREVIEW_LIMIT: 600, // cap on iterations used for the live preview
    HISTORY_MAX: 80, // zoom-history stack size
    MIN_DRAG: 6, // min drag distance (px) before it counts as zoom
    MAX_DPR: 2, // cap devicePixelRatio for perf on hi-dpi
};
};

// ── module: src/fractal.ts ──
__mods["src/fractal.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.computeBuffer = computeBuffer;
exports.iterationsAt = iterationsAt;
// ============================================================
//  FRACTAL CORE MATH — escape-time iteration, allocation-free
// ============================================================
const config_1 = require("./config");
const BAIL2 = config_1.CONFIG.BAIL2;
/**
 * Compute smooth escape values for rows [y0, y1) of a w*h region centered at
 * (cx, cy) with the given per-pixel complex step.
 *  - mode 'm' (Mandelbrot): z0 = 0,  c = pixel
 *  - mode 'j' (Julia):      z0 = pixel, c = (jcRe, jcIm)
 * Writes into buf (Float32Array of length w*h). Interior points (those that
 * never escape before `limit` iterations) are written as exactly `limit`.
 *
 * Allocation-free inside the per-pixel loop: no objects created per pixel.
 */
function computeBuffer(buf, w, h, cx, cy, step, mode, jcRe, jcIm, limit, y0, y1) {
    const hw = w * 0.5, hh = h * 0.5;
    for (let y = y0; y < y1; y++) {
        const imc = cy + (hh - (y + 0.5)) * step;
        const row = y * w;
        for (let x = 0; x < w; x++) {
            const rec = cx + (x + 0.5 - hw) * step;
            const idx = row + x;
            // seed the iteration
            let zr = 0, zi = 0;
            let cr = rec, ci = imc;
            if (mode === 'j') {
                zr = rec;
                zi = imc;
                cr = jcRe;
                ci = jcIm;
            }
            let n = 0;
            let r2 = 0, i2 = 0;
            while (n < limit) {
                r2 = zr * zr;
                i2 = zi * zi;
                if (r2 + i2 > BAIL2)
                    break;
                zi = 2 * zr * zi + ci;
                zr = r2 - i2 + cr;
                n++;
            }
            if (n >= limit) {
                buf[idx] = limit; // interior -> black
            }
            else {
                // smooth (continuous) escape: n + 1 - log2(log|z|)
                const mag = r2 + i2; // |z|^2 at escape, > BAIL2
                const mu = n + 1 - Math.log(Math.log(Math.sqrt(mag))) / Math.LN2;
                buf[idx] = mu < limit ? mu : limit;
            }
        }
    }
}
/** Integer escape-iteration count at (re, im). Interior returns `limit`. */
function iterationsAt(re, im, mode, jcRe, jcIm, limit) {
    let zr = 0, zi = 0;
    let cr = re, ci = im;
    if (mode === 'j') {
        zr = re;
        zi = im;
        cr = jcRe;
        ci = jcIm;
    }
    let n = 0;
    while (n < limit) {
        const r2 = zr * zr, i2 = zi * zi;
        if (r2 + i2 > BAIL2)
            break;
        zi = 2 * zr * zi + ci;
        zr = r2 - i2 + cr;
        n++;
    }
    return n;
}
};

// ── 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.colorizeInto = colorizeInto;
exports.paletteCSS = paletteCSS;
// ============================================================
//  PALETTES — selectable color maps with smooth (lerped) stops
// ============================================================
const config_1 = require("./config");
exports.PALETTES = [
    {
        name: 'Fire',
        stops: [
            { p: 0, c: [6, 0, 0] }, { p: 0.18, c: [38, 0, 0] },
            { p: 0.42, c: [160, 20, 0] }, { p: 0.62, c: [255, 110, 20] },
            { p: 0.82, c: [255, 210, 70] }, { p: 1, c: [255, 252, 220] },
        ],
    },
    {
        name: 'Ocean',
        stops: [
            { p: 0, c: [1, 1, 8] }, { p: 0.25, c: [4, 18, 64] },
            { p: 0.5, c: [6, 84, 150] }, { p: 0.75, c: [50, 210, 245] },
            { p: 1, c: [220, 255, 255] },
        ],
    },
    {
        name: 'Mono',
        stops: [
            { p: 0, c: [0, 0, 0] }, { p: 1, c: [255, 255, 255] },
        ],
    },
    {
        name: 'Psyche',
        stops: [
            { p: 0, c: [255, 0, 90] }, { p: 0.33, c: [255, 255, 0] },
            { p: 0.66, c: [0, 255, 230] }, { p: 1, c: [255, 0, 210] },
        ],
    },
];
/**
 * Colorize RGBA buffer `px` for rows [y0, y1) from escape buffer `buf`.
 * Interior (mu >= limit) -> black. Otherwise map smooth mu to a [0,1) t,
 * offset by the cycle slider, and interpolate through the palette stops.
 * Allocation-free per pixel.
 */
function colorizeInto(px, buf, w, h, limit, palette, offset, y0, y1) {
    const stops = palette.stops;
    const l = stops.length;
    const CS = config_1.CONFIG.COLOR_SCALE;
    for (let y = y0; y < y1; y++) {
        const row = y * w;
        for (let x = 0; x < w; x++) {
            const idx = row + x;
            const mu = buf[idx];
            const o = idx * 4;
            if (mu >= limit) {
                px[o] = 0;
                px[o + 1] = 0;
                px[o + 2] = 0;
                px[o + 3] = 255;
                continue;
            }
            let t = (mu / CS + offset) % 1;
            if (t < 0)
                t += 1;
            let r = 0, g = 0, b = 0;
            if (t <= stops[0].p) {
                r = stops[0].c[0];
                g = stops[0].c[1];
                b = stops[0].c[2];
            }
            else if (t >= stops[l - 1].p) {
                const c = stops[l - 1].c;
                r = c[0];
                g = c[1];
                b = c[2];
            }
            else {
                for (let k = 0; k < l - 1; k++) {
                    const a = stops[k], d = stops[k + 1];
                    if (t >= a.p && t <= d.p) {
                        const span = d.p - a.p || 1;
                        const f = (t - a.p) / span;
                        r = a.c[0] + (d.c[0] - a.c[0]) * f;
                        g = a.c[1] + (d.c[1] - a.c[1]) * f;
                        b = a.c[2] + (d.c[2] - a.c[2]) * f;
                        break;
                    }
                }
            }
            px[o] = r;
            px[o + 1] = g;
            px[o + 2] = b;
            px[o + 3] = 255;
        }
    }
}
/** Build a CSS linear-gradient string from a palette (for swatch chips). */
function paletteCSS(p) {
    const parts = p.stops.map(s => {
        const [r, g, b] = s.c;
        return `rgb(${r | 0},${g | 0},${b | 0}) ${(s.p * 100).toFixed(1)}%`;
    });
    return `linear-gradient(90deg, ${parts.join(', ')})`;
}
};

// ── module: src/state.ts ──
__mods["src/state.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.state = void 0;
exports.state = {
    re: -0.6, im: 0,
    step: 0.0034,
    mode: 'm',
    cRe: -0.745428, cIm: 0.113009,
    paletteIndex: 0,
    paletteOffset: 0,
    limit: 220,
    limitAuto: true,
    W: 100, H: 100,
    hoverRe: -0.8, hoverIm: 0.156,
    onViewChange: null,
    onActivity: null,
};
};

// ── module: src/actions.ts ──
__mods["src/actions.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.clampLimit = clampLimit;
exports.autoLimit = autoLimit;
exports.apply = apply;
exports.goTo = goTo;
exports.panBy = panBy;
exports.zoomAt = zoomAt;
exports.zoomBy = zoomBy;
exports.computeZoomRect = computeZoomRect;
exports.zoomToRect = zoomToRect;
exports.resetView = resetView;
exports.backView = backView;
exports.openJulia = openJulia;
exports.backToMandelbrot = backToMandelbrot;
exports.toggleJulia = toggleJulia;
exports.selectPalette = selectPalette;
exports.cyclePalette = cyclePalette;
exports.setOffset = setOffset;
exports.setLimitAuto = setLimitAuto;
exports.setManualLimit = setManualLimit;
exports.setSize = setSize;
exports.magnification = magnification;
exports.peekHistory = peekHistory;
exports.currentSaved = currentSaved;
exports.setMode = setMode;
// ============================================================
//  NAVIGATION ACTIONS — mutate state, manage zoom history,
//  recompute the auto iteration limit, notify the renderer.
// ============================================================
const state_1 = require("./state");
const config_1 = require("./config");
const history = [];
let mSaved = null; // Mandelbrot view saved while in Julia
function mag() { return config_1.CONFIG.INIT_STEP / state_1.state.step; }
function clampLimit(v) {
    return Math.max(config_1.CONFIG.MIN_ITER, Math.min(config_1.CONFIG.MAX_ITER, Math.round(v)));
}
function autoLimit(m) {
    return clampLimit(config_1.CONFIG.BASE_ITER + config_1.CONFIG.ITER_PER_DECADE * Math.log10(Math.max(m, 1)));
}
function refreshLimit() {
    if (state_1.state.limitAuto)
        state_1.state.limit = autoLimit(mag());
}
function pushHistory() {
    history.push({ re: state_1.state.re, im: state_1.state.im, step: state_1.state.step, mode: state_1.state.mode, cRe: state_1.state.cRe, cIm: state_1.state.cIm });
    if (history.length > config_1.CONFIG.HISTORY_MAX)
        history.shift();
}
/** Clamp magnification to the precision cap. Returns true if clamped. */
function clampMag() {
    const m = mag();
    if (m > config_1.CONFIG.MAX_MAG) {
        state_1.state.step = config_1.CONFIG.INIT_STEP / config_1.CONFIG.MAX_MAG;
        return true;
    }
    return false;
}
/** Idempotently apply current state: refresh auto limit + notify. */
function apply() {
    refreshLimit();
    if (state_1.state.onViewChange)
        state_1.state.onViewChange();
    if (state_1.state.onActivity)
        state_1.state.onActivity();
}
// ---------- direct view --------------------------------------------------
function goTo(re, im, step, push = true) {
    if (push)
        pushHistory();
    state_1.state.re = re;
    state_1.state.im = im;
    state_1.state.step = Math.max(step, config_1.CONFIG.INIT_STEP / config_1.CONFIG.MAX_MAG);
    clampMag();
    apply();
}
// ---------- pan ----------------------------------------------------------
function panBy(dxPx, dyPx) {
    state_1.state.re -= dxPx * state_1.state.step;
    state_1.state.im += dyPx * state_1.state.step;
    apply();
}
// ---------- zoom toward a screen point -----------------------------------
function zoomAt(factor, sx, sy) {
    const beforeRe = state_1.state.re + (sx - state_1.state.W * 0.5) * state_1.state.step;
    const beforeIm = state_1.state.im + (state_1.state.H * 0.5 - sy) * state_1.state.step;
    const old = state_1.state.step;
    let ns = old * factor;
    state_1.state.step = ns;
    const clamped = clampMag();
    ns = state_1.state.step;
    if (!clamped || old === ns) {
        state_1.state.re = beforeRe - (sx - state_1.state.W * 0.5) * ns;
        state_1.state.im = beforeIm + (state_1.state.H * 0.5 - sy) * ns;
    }
    apply();
}
/** Zoom in/out around the view center. */
function zoomBy(factor, push = true) {
    if (push)
        pushHistory();
    const old = state_1.state.step;
    state_1.state.step = old * factor;
    clampMag();
    apply();
}
// ---------- rectangle zoom (drag box) ------------------------------------
/**
 * Given a screen rectangle [x0,y0]-[x1,y1] in device px, return the view that
 * shows exactly that framed region: same per-pixel step on both axes (so no
 * distortion), centered on the rectangle center, with the rectangle fully
 * contained (the larger dimension fills the screen). This is deterministic so
 * it can be asserted headlessly.
 */
function computeZoomRect(x0, y0, x1, y1) {
    const rw = Math.abs(x1 - x0), rh = Math.abs(y1 - y0);
    const cx = (x0 + x1) * 0.5, cy = (y0 + y1) * 0.5;
    const newCRe = state_1.state.re + (cx - state_1.state.W * 0.5) * state_1.state.step;
    const newCIm = state_1.state.im + (state_1.state.H * 0.5 - cy) * state_1.state.step;
    const k = Math.max(rw / state_1.state.W, rh / state_1.state.H); // contains the rect, no crop
    const step = Math.max(state_1.state.step * k, config_1.CONFIG.INIT_STEP / config_1.CONFIG.MAX_MAG);
    return { re: newCRe, im: newCIm, step, mode: state_1.state.mode, cRe: state_1.state.cRe, cIm: state_1.state.cIm };
}
function zoomToRect(x0, y0, x1, y1) {
    const rw = Math.abs(x1 - x0), rh = Math.abs(y1 - y0);
    if (rw < config_1.CONFIG.MIN_DRAG || rh < config_1.CONFIG.MIN_DRAG)
        return false;
    pushHistory();
    const v = computeZoomRect(x0, y0, x1, y1);
    state_1.state.re = v.re;
    state_1.state.im = v.im;
    state_1.state.step = v.step;
    apply();
    return true;
}
// ---------- reset / back ------------------------------------------------
function resetView() {
    pushHistory();
    state_1.state.mode = 'm';
    state_1.state.re = -0.6;
    state_1.state.im = 0;
    state_1.state.step = config_1.CONFIG.INIT_STEP;
    state_1.state.cRe = -0.745428;
    state_1.state.cIm = 0.113009;
    mSaved = null;
    apply();
}
function backView() {
    const e = history.pop();
    if (!e)
        return false;
    state_1.state.re = e.re;
    state_1.state.im = e.im;
    state_1.state.step = e.step;
    state_1.state.mode = e.mode;
    state_1.state.cRe = e.cRe;
    state_1.state.cIm = e.cIm;
    apply();
    return true;
}
// ---------- Julia mode ---------------------------------------------------
function openJulia(re, im) {
    pushHistory();
    if (state_1.state.mode === 'm') {
        mSaved = { re: state_1.state.re, im: state_1.state.im, step: state_1.state.step, mode: 'm', cRe: state_1.state.cRe, cIm: state_1.state.cIm };
    }
    state_1.state.cRe = re;
    state_1.state.cIm = im;
    state_1.state.hoverRe = re;
    state_1.state.hoverIm = im;
    state_1.state.mode = 'j';
    state_1.state.re = 0;
    state_1.state.im = 0;
    state_1.state.step = config_1.CONFIG.JULIA_WIDTH / state_1.state.H; // fresh Julia window around origin
    apply();
}
function backToMandelbrot() {
    pushHistory();
    state_1.state.mode = 'm';
    if (mSaved) {
        state_1.state.re = mSaved.re;
        state_1.state.im = mSaved.im;
        state_1.state.step = mSaved.step;
        mSaved = null;
    }
    else {
        state_1.state.re = -0.6;
        state_1.state.im = 0;
        state_1.state.step = config_1.CONFIG.INIT_STEP;
    }
    apply();
}
function toggleJulia() {
    if (state_1.state.mode === 'm')
        openJulia(state_1.state.hoverRe, state_1.state.hoverIm);
    else
        backToMandelbrot();
}
// ---------- palette / color ---------------------------------------------
function selectPalette(i) {
    state_1.state.paletteIndex = ((i % 4) + 4) % 4;
    apply();
}
function cyclePalette() { selectPalette(state_1.state.paletteIndex + 1); }
function setOffset(off) { state_1.state.paletteOffset = off; apply(); }
function setLimitAuto(auto) { state_1.state.limitAuto = auto; apply(); }
function setManualLimit(v) {
    state_1.state.limitAuto = false;
    state_1.state.limit = clampLimit(v);
    apply();
}
// ---------- set W/H (device) --------------------------------------------
function setSize(W, H) { state_1.state.W = W; state_1.state.H = H; }
// ---------- debug helpers ------------------------------------------------
function magnification() { return mag(); }
function peekHistory() { return history; }
function currentSaved() {
    return { re: state_1.state.re, im: state_1.state.im, step: state_1.state.step, mode: state_1.state.mode, cRe: state_1.state.cRe, cIm: state_1.state.cIm };
}
function setMode(m) { if (m === state_1.state.mode)
    return; if (m === 'j')
    openJulia(state_1.state.hoverRe, state_1.state.hoverIm);
else
    backToMandelbrot(); }
};

// ── module: src/render.ts ──
__mods["src/render.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Renderer = void 0;
// ============================================================
//  RENDERER — Canvas2D ImageData pipeline with progressive
//  rendering (fast coarse pass, then full-resolution refine).
//  View changes bump a version token; in-flight passes abort on
//  the next scheduler tick so interaction never freezes and no
//  stale/ghost tiles survive.
// ============================================================
const config_1 = require("./config");
const state_1 = require("./state");
const fractal_1 = require("./fractal");
const palettes_1 = require("./palettes");
const tick = () => new Promise(r => setTimeout(r, config_1.CONFIG.TICK_MS));
class Renderer {
    constructor(canvas) {
        this.W = 2;
        this.H = 2;
        this.lW = 1;
        this.lH = 1;
        this.version = 0; // bump to cancel in-flight work
        this.fullReady = false; // true when refine completed for current view
        this.lastDone = 0; // timestamp of last refine completion
        this.fps = 0;
        this.canvas = canvas;
        const ctx = canvas.getContext('2d', { willReadFrequently: true });
        if (!ctx)
            throw new Error('no 2d context');
        this.ctx = ctx;
    }
    /** (Re)size buffers. Call after the canvas element changes size. */
    setSize(W, H) {
        this.W = W;
        this.H = H;
        this.canvas.width = W;
        this.canvas.height = H;
        this.it = new Float32Array(W * H);
        this.px = new Uint8ClampedArray(W * H * 4);
        this.img = new ImageData(this.px, W, H);
        // coarse backing
        this.lW = Math.max(1, Math.ceil(W / config_1.CONFIG.COARSE_DIV));
        this.lH = Math.max(1, Math.ceil(H / config_1.CONFIG.COARSE_DIV));
        this.lowIt = new Float32Array(this.lW * this.lH);
        this.lowCanvas = document.createElement('canvas');
        this.lowCanvas.width = this.lW;
        this.lowCanvas.height = this.lH;
        this.lowCtx = this.lowCanvas.getContext('2d');
        this.lowImg = this.lowCtx.createImageData(this.lW, this.lH);
    }
    get w() { return this.W; }
    get h() { return this.H; }
    get framesPerSecond() { return this.fps; }
    /** Start (or restart) a full render of the current view. Cancels prior work. */
    renderView() {
        const v = ++this.version;
        this.fullReady = false;
        this.run(v).catch(() => { });
    }
    /** Re-colorize the existing full-res buffer (palette/offset change only). */
    recolor() {
        if (!this.fullReady) {
            this.renderView();
            return;
        }
        const pal = palettes_1.PALETTES[state_1.state.paletteIndex];
        const t0 = performance.now();
        (0, palettes_1.colorizeInto)(this.px, this.it, this.W, this.H, state_1.state.limit, pal, state_1.state.paletteOffset, 0, this.H);
        this.ctx.putImageData(this.img, 0, 0);
        this.lastDone = t0;
    }
    async run(v) {
        if (v !== this.version)
            return;
        await this.coarse(v);
        if (v !== this.version)
            return;
        await this.refine(v);
        if (v !== this.version)
            return;
        this.fullReady = true;
        this.recomputeFps();
    }
    // ---- coarse pass: sample every 4th pixel, blit upscaled ----
    async coarse(v) {
        const pal = palettes_1.PALETTES[state_1.state.paletteIndex];
        const l = state_1.state.limit;
        const step4 = state_1.state.step * config_1.CONFIG.COARSE_DIV;
        this.ctx.imageSmoothingEnabled = false;
        for (let y = 0; y < this.lH;) {
            if (v !== this.version)
                return;
            const y1 = Math.min(y + config_1.CONFIG.DW_CHUNK, this.lH);
            (0, fractal_1.computeBuffer)(this.lowIt, this.lW, this.lH, state_1.state.re, state_1.state.im, step4, state_1.state.mode, state_1.state.cRe, state_1.state.cIm, l, y, y1);
            (0, palettes_1.colorizeInto)(this.lowImg.data, this.lowIt, this.lW, this.lH, l, pal, state_1.state.paletteOffset, y, y1);
            this.lowCtx.putImageData(this.lowImg, 0, 0);
            this.ctx.clearRect(0, 0, this.W, this.H);
            this.ctx.drawImage(this.lowCanvas, 0, 0, this.W, this.H);
            y = y1;
            await tick();
        }
    }
    // ---- refine pass: full resolution, drawn band by band ----
    async refine(v) {
        const pal = palettes_1.PALETTES[state_1.state.paletteIndex];
        const l = state_1.state.limit;
        for (let y = 0; y < this.H;) {
            if (v !== this.version)
                return;
            const y1 = Math.min(y + config_1.CONFIG.REFINE_CHUNK, this.H);
            (0, fractal_1.computeBuffer)(this.it, this.W, this.H, state_1.state.re, state_1.state.im, state_1.state.step, state_1.state.mode, state_1.state.cRe, state_1.state.cIm, l, y, y1);
            (0, palettes_1.colorizeInto)(this.px, this.it, this.W, this.H, l, pal, state_1.state.paletteOffset, y, y1);
            this.ctx.putImageData(this.img, 0, 0, 0, y, this.W, y1 - y);
            y = y1;
            await tick();
        }
    }
    recomputeFps() {
        const now = performance.now();
        if (this.lastDone > 0) {
            const dt = (now - this.lastDone) / 1000;
            if (dt > 0)
                this.fps = Math.round(1 / dt);
        }
        this.lastDone = now;
    }
}
exports.Renderer = Renderer;
};

// ── module: src/preview.ts ──
__mods["src/preview.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Preview = void 0;
// ============================================================
//  LIVE PREVIEW PANE — shows the *other* set for the hovered
//  point. In Mandelbrot mode it renders the Julia set for the
//  current cursor c; in Julia mode it renders the Mandelbrot set.
// ============================================================
const config_1 = require("./config");
const state_1 = require("./state");
const fractal_1 = require("./fractal");
const palettes_1 = require("./palettes");
class Preview {
    constructor(canvas) {
        this.d = config_1.CONFIG.PREVIEW;
        this.lastT = 0;
        this.canvas = canvas;
        const ctx = canvas.getContext('2d');
        this.ctx = ctx;
        this.canvas.width = this.d;
        this.canvas.height = this.d;
        this.it = new Float32Array(this.d * this.d);
        this.img = ctx.createImageData(this.d, this.d);
        this.wrap = document.getElementById('preview-wrap');
        this.label = document.getElementById('preview-label');
    }
    show() { this.wrap.hidden = false; }
    hide() { this.wrap.hidden = true; }
    /** Update preview for the point (re, im) in the *other* set. */
    update(re, im) {
        const now = performance.now();
        if (now - this.lastT < 34)
            return; // ~30fps throttle
        this.lastT = now;
        const limit = Math.min(state_1.state.limit, config_1.CONFIG.PREVIEW_LIMIT);
        const pal = palettes_1.PALETTES[state_1.state.paletteIndex];
        if (state_1.state.mode === 'm') {
            // main is Mandelbrot -> preview the Julia set for hovered c
            this.label.textContent = `Julia  c = ${re.toFixed(4)}${im >= 0 ? '+' : ''}${im.toFixed(4)}i`;
            const step = config_1.CONFIG.JULIA_WIDTH / this.d;
            (0, fractal_1.computeBuffer)(this.it, this.d, this.d, 0, 0, step, 'j', re, im, limit, 0, this.d);
        }
        else {
            // main is Julia -> preview the Mandelbrot set
            this.label.textContent = 'Mandelbrot';
            const step = config_1.CONFIG.INIT_WIDTH / this.d;
            (0, fractal_1.computeBuffer)(this.it, this.d, this.d, -0.6, 0, step, 'm', re, im, limit, 0, this.d);
        }
        (0, palettes_1.colorizeInto)(this.img.data, this.it, this.d, this.d, limit, pal, state_1.state.paletteOffset, 0, this.d);
        this.ctx.putImageData(this.img, 0, 0);
    }
}
exports.Preview = Preview;
};

// ── module: src/input.ts ──
__mods["src/input.ts"] = function (exports, require, module) {
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.setupInput = setupInput;
// ============================================================
//  INPUT — pointer (drag-zoom, pan, wheel, click->Julia),
//  keyboard, live hover preview + reticle + zoom box overlay.
// ============================================================
const config_1 = require("./config");
const state_1 = require("./state");
const act = __importStar(require("./actions"));
function setupInput(preview) {
    const canvas = document.getElementById('fractal');
    const box = document.getElementById('zoombox');
    const reticle = document.getElementById('reticle');
    const hr = reticle.querySelector('.hr');
    const vr = reticle.querySelector('.vr');
    // ---- pointer helpers --------------------------------------
    function toDev(e) {
        const r = canvas.getBoundingClientRect();
        const s = canvas.width / r.width;
        return { x: (e.clientX - r.left) * s, y: (e.clientY - r.top) * s };
    }
    function activity() { if (state_1.state.onActivity)
        state_1.state.onActivity(); }
    // ---- interaction state ------------------------------------
    let down = false;
    let panning = false;
    let startX = 0, startY = 0; // pointer-down device coords
    let moved = false;
    let boxActive = false;
    let lastEngaged = false;
    // pan inertia
    let vx = 0, vy = 0, panTimer = null;
    function endPan() {
        panning = false;
        down = false;
        if (panTimer) {
            clearInterval(panTimer);
            panTimer = null;
        }
    }
    canvas.addEventListener('pointerdown', (e) => {
        activity();
        if (e.button !== 0 && e.button !== 2)
            return;
        canvas.setPointerCapture(e.pointerId);
        const d = toDev(e);
        down = true;
        moved = false;
        startX = d.x;
        startY = d.y;
        vx = 0;
        vy = 0;
        panning = e.button === 2 || e.shiftKey;
        if (panning && panTimer) {
            clearInterval(panTimer);
            panTimer = null;
        }
        e.preventDefault();
    });
    canvas.addEventListener('pointermove', (e) => {
        const d = toDev(e);
        // always update hover preview / reticle when not dragging a box
        const hoverRe = state_1.state.re + (d.x - state_1.state.W * 0.5) * state_1.state.step;
        const hoverIm = state_1.state.im + (state_1.state.H * 0.5 - d.y) * state_1.state.step;
        state_1.state.hoverRe = hoverRe;
        state_1.state.hoverIm = hoverIm;
        if (!down) {
            // hover mode
            reticle.style.display = 'block';
            const rect = canvas.getBoundingClientRect();
            hr.style.height = rect.height + 'px';
            vr.style.width = rect.width + 'px';
            reticle.style.left = (e.clientX - rect.left) + 'px';
            reticle.style.top = (e.clientY - rect.top) + 'px';
            reticle.style.width = '30px';
            reticle.style.height = '30px';
            reticle.style.transform = `translate(-50%,-50%)`;
            preview.show();
            preview.update(hoverRe, hoverIm);
            return;
        }
        // dragging
        activity();
        if (!moved && Math.hypot(d.x - startX, d.y - startY) > config_1.CONFIG.MIN_DRAG)
            moved = true;
        if (panning) {
            const dx = d.x - startX, dy = d.y - startY;
            act.panBy(dx, dy);
            vx = (vx + dx) * 0.5;
            vy = (vy + dy) * 0.5;
            startX = d.x;
            startY = d.y;
            if (!panTimer) {
                panTimer = window.setInterval(() => {
                    if (!panning) {
                        endPan();
                        return;
                    }
                    vx *= config_1.CONFIG.MOMENTUM;
                    vy *= config_1.CONFIG.MOMENTUM;
                    if (Math.abs(vx) < 0.05 && Math.abs(vy) < 0.05)
                        return;
                    act.panBy(vx, vy);
                }, 16);
            }
            boxActive = false;
            box.style.display = 'none';
            lastEngaged = false;
        }
        else if (moved) {
            // draw zoom box
            boxActive = true;
            lastEngaged = true;
            const rect = canvas.getBoundingClientRect();
            const s = rect.width / canvas.width;
            const x0 = Math.min(startX, d.x) * s, y0 = Math.min(startY, d.y) * s;
            const w = Math.abs(d.x - startX) * s, h = Math.abs(d.y - startY) * s;
            box.style.display = 'block';
            box.style.left = (x0 + rect.left) + 'px';
            box.style.top = (y0 + rect.top) + 'px';
            box.style.width = w + 'px';
            box.style.height = h + 'px';
            reticle.style.display = 'none';
            preview.hide();
        }
        e.preventDefault();
    });
    function release(e) {
        if (!down)
            return;
        const d = toDev(e);
        down = false;
        box.style.display = 'none';
        reticle.style.display = 'none';
        preview.show();
        if (panning)
            return; // keep inertia running
        if (boxActive) {
            if (act.zoomToRect(startX, startY, d.x, d.y)) {
                boxActive = false;
                return;
            }
            boxActive = false;
            // too small to be a zoom -> treat as a click
        }
        // click (no meaningful drag): open Julia at the point in Mandelbrot mode
        if (state_1.state.mode === 'm') {
            const re = state_1.state.re + (d.x - state_1.state.W * 0.5) * state_1.state.step;
            const im = state_1.state.im + (state_1.state.H * 0.5 - d.y) * state_1.state.step;
            act.openJulia(re, im);
        }
        else {
            // in Julia mode clicking with no drag zooms slightly toward point
            act.zoomAt(config_1.CONFIG.ZOOM_FACTOR / 2, d.x, d.y);
        }
    }
    canvas.addEventListener('pointerup', release);
    canvas.addEventListener('pointercancel', () => {
        box.style.display = 'none';
        reticle.style.display = 'none';
        preview.show();
        down = false;
        boxActive = false;
        endPan();
    });
    canvas.addEventListener('lostpointercapture', () => { endPan(); });
    // ---- wheel zoom toward cursor -----------------------------
    canvas.addEventListener('wheel', (e) => {
        e.preventDefault();
        activity();
        const d = toDev(e);
        const dir = e.deltaY < 0 ? 1 / config_1.CONFIG.ZOOM_FACTOR : config_1.CONFIG.ZOOM_FACTOR;
        act.zoomAt(dir, d.x, d.y);
    }, { passive: false });
    // ---- keyboard ---------------------------------------------
    window.addEventListener('keydown', (e) => {
        const step = state_1.state.step;
        const k = e.key;
        let handled = true;
        if (k === 'ArrowLeft')
            act.panBy(-60 * step, 0);
        else if (k === 'ArrowRight')
            act.panBy(60 * step, 0);
        else if (k === 'ArrowUp')
            act.panBy(0, -60 * step);
        else if (k === 'ArrowDown')
            act.panBy(0, 60 * step);
        else if (k === '+' || k === '=')
            act.zoomAt(1 / config_1.CONFIG.ZOOM_FACTOR, state_1.state.W / 2, state_1.state.H / 2);
        else if (k === '-' || k === '_')
            act.zoomAt(config_1.CONFIG.ZOOM_FACTOR, state_1.state.W / 2, state_1.state.H / 2);
        else if (k === '0' || k === 'Home')
            act.resetView();
        else if (k === 'b' || k === 'B')
            act.backView();
        else if (k === 'j' || k === 'J')
            act.toggleJulia();
        else if (k === 'p' || k === 'P')
            act.cyclePalette();
        else if (k >= '1' && k <= '4')
            act.selectPalette(+k - 1);
        else
            handled = false;
        if (handled) {
            e.preventDefault();
            activity();
        }
    });
}
};

// ── module: src/hud.ts ──
__mods["src/hud.ts"] = function (exports, require, module) {
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.setupHud = setupHud;
// ============================================================
//  HUD — glassy overlay: palette chips, sliders, buttons,
//  live readouts (magnification, center, iters, fps), idle-hide.
// ============================================================
const config_1 = require("./config");
const state_1 = require("./state");
const act = __importStar(require("./actions"));
const palettes_1 = require("./palettes");
function setupHud(renderer) {
    const hud = document.getElementById('hud');
    const magEl = document.getElementById('mag');
    const centerEl = document.getElementById('center');
    const itersEl = document.getElementById('iters');
    const fpsEl = document.getElementById('fps');
    const modeEl = document.getElementById('mode-chip');
    const warnEl = document.getElementById('warn');
    // --- palette chips -----------------------------------------
    const palWrap = document.getElementById('palettes');
    const chips = [];
    palettes_1.PALETTES.forEach((p, i) => {
        const s = document.createElement('button');
        s.className = 'swatch';
        s.title = p.name;
        s.dataset.i = String(i);
        s.style.background = (0, palettes_1.paletteCSS)(p);
        s.addEventListener('click', () => act.selectPalette(i));
        palWrap.appendChild(s);
        chips.push(s);
    });
    // --- offset slider -----------------------------------------
    const offset = document.getElementById('offset');
    offset.addEventListener('input', () => act.setOffset(+offset.value / 99));
    // --- iteration slider + auto -------------------------------
    const iter = document.getElementById('iter');
    const iterAuto = document.getElementById('iter-auto');
    function refreshIterUI() {
        iter.disabled = state_1.state.limitAuto;
        iter.value = String(state_1.state.limit);
        iterAuto.textContent = state_1.state.limitAuto ? 'AUTO' : 'MANUAL';
        iterAuto.style.opacity = state_1.state.limitAuto ? '1' : '.5';
    }
    iter.addEventListener('input', () => act.setManualLimit(+iter.value));
    iterAuto.addEventListener('click', () => act.setLimitAuto(!state_1.state.limitAuto));
    // --- buttons -----------------------------------------------
    document.getElementById('btn-back').addEventListener('click', () => act.backView());
    document.getElementById('btn-reset').addEventListener('click', () => act.resetView());
    document.getElementById('btn-julia').addEventListener('click', () => act.toggleJulia());
    document.getElementById('btn-help').addEventListener('click', () => {
        const h = document.getElementById('help');
        h.hidden = !h.hidden;
        activity();
    });
    // --- idle hide ---------------------------------------------
    let idleTimer = null;
    function activity() {
        hud.classList.remove('idle');
        if (idleTimer)
            clearTimeout(idleTimer);
        idleTimer = window.setTimeout(() => hud.classList.add('idle'), config_1.CONFIG.IDLE_MS);
    }
    // --- readouts ----------------------------------------------
    function fmtMag() {
        const m = config_1.CONFIG.INIT_STEP / state_1.state.step;
        if (m >= 1e4)
            return '×' + m.toExponential(2);
        if (m >= 100)
            return '×' + Math.round(m).toLocaleString();
        return '×' + m.toFixed(2);
    }
    function fmtC(re, im) {
        const sgn = im < 0 ? '−' : '+';
        return `${re.toPrecision(4)} ${sgn} ${Math.abs(im).toPrecision(3)}i`;
    }
    function update() {
        magEl.textContent = fmtMag();
        if (state_1.state.mode === 'm') {
            modeEl.textContent = 'Mandelbrot';
            centerEl.textContent = 'center ' + fmtC(state_1.state.re, state_1.state.im);
        }
        else {
            modeEl.textContent = 'Julia';
            centerEl.textContent = 'c = ' + fmtC(state_1.state.cRe, state_1.state.cIm);
        }
        itersEl.textContent = 'iter ' + state_1.state.limit + (state_1.state.limitAuto ? ' (auto)' : '');
        fpsEl.textContent = renderer.framesPerSecond + ' fps';
        const m = config_1.CONFIG.INIT_STEP / state_1.state.step;
        warnEl.hidden = m <= config_1.CONFIG.WARN_MAG;
        refreshIterUI();
        chips.forEach((c, i) => c.classList.toggle('on', i === state_1.state.paletteIndex));
    }
    return { update, activity };
}
};

// ── module: src/bookmarks.ts ──
__mods["src/bookmarks.ts"] = function (exports, require, module) {
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.currentSnapshot = currentSnapshot;
exports.restoreSnapshot = restoreSnapshot;
exports.setupBookmarks = setupBookmarks;
exports.syncHash = syncHash;
exports.applyHash = applyHash;
// ============================================================
//  BOOKMARKS (localStorage) + URL-hash state sharing
// ============================================================
const config_1 = require("./config");
const state_1 = require("./state");
const act = __importStar(require("./actions"));
const KEY = 'fractal.bm.v1';
function currentSnapshot() {
    return {
        re: state_1.state.re, im: state_1.state.im, step: state_1.state.step, mode: state_1.state.mode,
        cRe: state_1.state.cRe, cIm: state_1.state.cIm,
        paletteIndex: state_1.state.paletteIndex, paletteOffset: state_1.state.paletteOffset,
        limit: state_1.state.limit, limitAuto: state_1.state.limitAuto,
    };
}
function restoreSnapshot(s) {
    state_1.state.re = s.re;
    state_1.state.im = s.im;
    state_1.state.step = s.step;
    state_1.state.mode = s.mode;
    state_1.state.cRe = s.cRe;
    state_1.state.cIm = s.cIm;
    state_1.state.paletteIndex = s.paletteIndex | 0;
    state_1.state.paletteOffset = s.paletteOffset | 0;
    state_1.state.limit = s.limit | 0;
    state_1.state.limitAuto = !!s.limitAuto;
    act.apply();
}
function load() {
    try {
        const v = JSON.parse(localStorage.getItem(KEY) || '[]');
        return Array.isArray(v) ? v : [];
    }
    catch {
        return [];
    }
}
function persist(list) {
    try {
        localStorage.setItem(KEY, JSON.stringify(list));
    }
    catch { /* storage full */ }
}
function setupBookmarks() {
    const panel = document.getElementById('bmarks');
    const nameInput = document.getElementById('bm-name');
    const listEl = document.getElementById('bm-list');
    document.getElementById('btn-bmarks').addEventListener('click', () => { panel.hidden = !panel.hidden; if (!panel.hidden)
        render(); });
    document.getElementById('bm-save').addEventListener('click', () => {
        const name = (nameInput.value || 'View ' + new Date().toLocaleTimeString()).trim();
        const items = load();
        items.unshift({ name, s: currentSnapshot() });
        persist(items.slice(0, 60));
        render();
    });
    nameInput.addEventListener('keydown', (e) => { if (e.key === 'Enter')
        document.getElementById('bm-save').click(); });
    function render() {
        listEl.innerHTML = '';
        const items = load();
        if (!items.length) {
            listEl.innerHTML = '<li style="opacity:.5">No bookmarks yet.</li>';
            return;
        }
        items.forEach((b, i) => {
            const li = document.createElement('li');
            const nm = document.createElement('span');
            nm.className = 'nm';
            nm.textContent = b.name;
            const loadB = document.createElement('button');
            loadB.textContent = 'load';
            loadB.addEventListener('click', () => restoreSnapshot(b.s));
            const del = document.createElement('button');
            del.textContent = '✕';
            del.addEventListener('click', () => { const l = load(); l.splice(i, 1); persist(l); render(); });
            li.appendChild(nm);
            li.appendChild(loadB);
            li.appendChild(del);
            listEl.appendChild(li);
        });
    }
}
// ---------------- URL hash state ----------------------------
let hashTimer = null;
function syncHash() {
    if (hashTimer)
        return;
    hashTimer = window.setTimeout(() => {
        hashTimer = null;
        const s = currentSnapshot();
        const h = { m: s.mode, r: +s.re.toPrecision(12), i: +s.im.toPrecision(12),
            s: +s.step.toPrecision(12), cR: +s.cRe.toPrecision(8), cI: +s.cIm.toPrecision(8),
            p: s.paletteIndex, o: +s.paletteOffset.toFixed(3), l: s.limit, a: s.limitAuto ? 1 : 0 };
        try {
            history.replaceState(null, '', '#' + encodeURIComponent(JSON.stringify(h)));
        }
        catch { /* noop */ }
    }, 120);
}
function applyHash() {
    const raw = location.hash.replace(/^#/, '');
    if (!raw)
        return false;
    try {
        const h = JSON.parse(decodeURIComponent(raw));
        if (typeof h !== 'object' || h.r === undefined || h.s === undefined)
            return false;
        const snap = {
            re: +h.r, im: +h.i || 0, step: +h.s,
            mode: h.m === 'j' ? 'j' : 'm',
            cRe: +h.cR || 0, cIm: +h.cI || 0,
            paletteIndex: +h.p || 0, paletteOffset: +h.o || 0,
            limit: +h.l || config_1.CONFIG.MIN_ITER, limitAuto: h.a !== 0,
        };
        restoreSnapshot(snap);
        return true;
    }
    catch {
        return false;
    }
}
};

// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>