← Fractal Explorer results

Fractal Explorer

Qwen 3.6 27B Fable Fusion 711 Uncensored Heretic NM DAU NEO MAX MTP 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 63,214 bytes · SHA-256 f69f5a15d931
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Fractal Explorer — Mandelbrot &amp; Julia Sets</title>
<style>
/* ── css: base ── */
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

html, body, #app {
  width: 100%;
  height: 100%;
  overflow: hidden;
  background: #000;
  font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
  color: #fff;
}

canvas#fractalCanvas {
  display: block;
  width: 100%;
  height: 100%;
  cursor: grab;
  image-rendering: auto;
}

.hidden {
  display: none !important;
}
/* ── css: hud ── */
#hud {
  position: fixed;
  inset: 0;
  pointer-events: none;
  transition: opacity 0.3s ease;
  z-index: 10;
}

#hud.hidden {
  opacity: 0;
}

#hud * {
  pointer-events: auto;
}

#hudTop {
  position: absolute;
  top: 12px;
  left: 12px;
  right: 12px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
}

#hudLeft, #hudCenter, #hudRight {
  display: flex;
  align-items: center;
  gap: 8px;
}

#hudLeft button, #hudRight button {
  width: 32px;
  height: 32px;
  border: none;
  border-radius: 8px;
  background: rgba(15, 23, 42, 0.75);
  backdrop-filter: blur(12px);
  color: #e2e8f0;
  font-size: 16px;
  cursor: pointer;
  transition: all 0.15s ease;
  display: flex;
  align-items: center;
  justify-content: center;
}

#hudLeft button:hover, #hudRight button:hover {
  background: rgba(30, 41, 59, 0.9);
  transform: scale(1.05);
}

#hudMagnify {
  font-size: 14px;
  font-weight: 600;
  padding: 6px 12px;
  border-radius: 8px;
  background: rgba(15, 23, 42, 0.75);
  backdrop-filter: blur(12px);
  font-variant-numeric: tabular-nums;
}

#hudMode {
  font-size: 16px;
  font-weight: 600;
  padding: 6px 14px;
  border-radius: 8px;
  background: rgba(15, 23, 42, 0.75);
  backdrop-filter: blur(12px);
  letter-spacing: 0.03em;
  text-transform: uppercase;
  font-size: 11px;
}

#hudFPS {
  font-size: 12px;
  padding: 6px 10px;
  border-radius: 8px;
  background: rgba(15, 23, 42, 0.75);
  backdrop-filter: blur(12px);
  color: #6ee7b7;
  font-variant-numeric: tabular-nums;
}

#hudBottom {
  position: absolute;
  bottom: 60px;
  left: 12px;
  right: 12px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

#hudCoords {
  font-size: 12px;
  padding: 6px 10px;
  border-radius: 8px;
  background: rgba(15, 23, 42, 0.75);
  backdrop-filter: blur(12px);
  color: #94a3b8;
  font-family: 'SF Mono', 'Fira Code', monospace;
  font-size: 11px;
}

#hudControls {
  display: flex;
  align-items: center;
  gap: 16px;
  flex-wrap: wrap;
  padding: 8px 12px;
  border-radius: 10px;
  background: rgba(15, 23, 42, 0.75);
  backdrop-filter: blur(12px);
}

#hudControls label {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 11px;
  color: #94a3b8;
  white-space: nowrap;
}

#hudControls input[type="range"] {
  width: 100px;
  accent-color: #6366f1;
}

#hudControls input[type="checkbox"] {
  accent-color: #6366f1;
  width: 14px;
  height: 14px;
}

#iterValue {
  font-family: 'SF Mono', monospace;
  min-width: 32px;
  color: #e2e8f0;
}
/* ── css: paletteBar ── */
#paletteBar {
  position: absolute;
  bottom: 16px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  gap: 8px;
  padding: 8px 12px;
  border-radius: 12px;
  background: rgba(15, 23, 42, 0.8);
  backdrop-filter: blur(12px);
  border: 1px solid rgba(75, 85, 99, 0.4);
}

.palette-chip {
  width: 36px;
  height: 16px;
  border-radius: 8px;
  cursor: pointer;
  transition: all 0.15s ease;
  border: 2px solid transparent;
  position: relative;
}

.palette-chip:hover {
  transform: scale(1.15);
}

.palette-chip.active {
  border-color: #fff;
  box-shadow: 0 0 8px rgba(99, 102, 241, 0.6);
}

#colorOffsetBar {
  position: absolute;
  bottom: 50px;
  left: 50%;
  transform: translateX(-50%);
  padding: 4px 10px;
  border-radius: 10px;
  background: rgba(15, 23, 42, 0.75);
  backdrop-filter: blur(12px);
}

#colorOffsetBar input[type="range"] {
  width: 140px;
  accent-color: #6366f1;
}
/* ── css: zoomRect ── */
#zoomRectOverlay {
  position: fixed;
  border: 2px dashed rgba(255, 255, 255, 0.9);
  background: rgba(255, 255, 255, 0.08);
  pointer-events: none;
  z-index: 20;
  display: none;
  box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.25);
}
/* ── css: juliaPreview ── */
#juliaPreview {
  position: fixed;
  bottom: 16px;
  right: 16px;
  z-index: 15;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.2s ease;
}

#juliaPreview.visible {
  opacity: 1;
}

#juliaCanvas {
  width: 160px;
  height: 160px;
  border-radius: 12px;
  border: 2px solid rgba(75, 85, 99, 0.5);
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
  image-rendering: auto;
}

#juliaLabel {
  text-align: center;
  font-size: 10px;
  color: #94a3b8;
  margin-top: 4px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}
/* ── css: bookmarks ── */
#bookmarkPanel {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  min-width: 320px;
  max-width: 400px;
  max-height: 60vh;
  padding: 0;
  border-radius: 16px;
  background: rgba(15, 23, 42, 0.95);
  backdrop-filter: blur(20px);
  border: 1px solid rgba(75, 85, 99, 0.5);
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6);
  z-index: 100;
  overflow: hidden;
}

.panelHeader {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 14px 16px;
  border-bottom: 1px solid rgba(75, 85, 99, 0.4);
  font-weight: 600;
  font-size: 14px;
}

.panelHeader button {
  background: none;
  border: none;
  color: #94a3b8;
  font-size: 18px;
  cursor: pointer;
  padding: 0 4px;
}

.panelHeader button:hover {
  color: #e2e8f0;
}

#bookmarkPanel > input {
  width: 100%;
  padding: 10px 16px;
  border: none;
  border-bottom: 1px solid rgba(75, 85, 99, 0.4);
  background: transparent;
  color: #e2e8f0;
  font-size: 13px;
  outline: none;
}

#bookmarkPanel > input::placeholder {
  color: #64748b;
}

#bookmarkPanel > input:focus {
  border-bottom-color: #6366f1;
}

#btnSaveBookmark {
  width: calc(100% - 32px);
  margin: 10px 16px;
  padding: 8px;
  border: none;
  border-radius: 8px;
  background: #6366f1;
  color: #fff;
  font-size: 13px;
  cursor: pointer;
  font-weight: 500;
}

#btnSaveBookmark:hover {
  background: #4f46e5;
}

#bookmarkList {
  padding: 8px;
  max-height: 300px;
  overflow-y: auto;
}

.bookmarkItem {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px;
  border-radius: 8px;
  margin-bottom: 4px;
  transition: background 0.15s ease;
}

.bookmarkItem:hover {
  background: rgba(75, 85, 99, 0.3);
}

.bookmarkName {
  flex: 1;
  font-size: 13px;
  color: #e2e8f0;
}

.bookmarkItem button {
  padding: 4px 10px;
  border: none;
  border-radius: 6px;
  font-size: 11px;
  cursor: pointer;
  transition: all 0.15s ease;
}

.bookmarkItem button:first-child {
  background: rgba(99, 102, 241, 0.3);
  color: #a5b4fc;
}

.bookmarkItem button:first-child:hover {
  background: rgba(99, 102, 241, 0.5);
}

.bookmarkItem button:last-child {
  background: rgba(239, 68, 68, 0.2);
  color: #fca5a5;
  padding: 4px 6px;
}

.bookmarkItem button:last-child:hover {
  background: rgba(239, 68, 68, 0.4);
}

.emptyBookmarks {
  padding: 20px;
  text-align: center;
  color: #64748b;
  font-size: 13px;
}

#bookmarkPanel.hidden {
  display: none !important;
}
</style>
</head>
<body>
<div id="app">
  <canvas id="fractalCanvas"></canvas>
  <div id="hud">
    <div id="hudTop">
      <div id="hudLeft">
        <button id="btnReset" title="Reset view">⟲</button>
        <button id="btnBack" title="Zoom out / history back">◁</button>
        <span id="hudMagnify" title="Magnification">×1</span>
      </div>
      <div id="hudCenter">
        <span id="hudMode">Mandelbrot</span>
      </div>
      <div id="hudRight">
        <span id="hudFPS" title="Frames per second">60 FPS</span>
        <button id="btnBookmarks" title="Bookmarks">★</button>
      </div>
    </div>
    <div id="hudBottom">
      <div id="hudCoords">
        <span id="hudCenterCoords">Re: 0, Im: 0</span>
      </div>
      <div id="hudControls">
        <label title="Iteration limit">
          Max Iter:
          <input type="range" id="sliderIter" min="50" max="2000" step="10" value="300">
          <span id="iterValue">300</span>
        </label>
        <label title="Smooth coloring">
          Smooth
          <input type="checkbox" id="chkSmooth" checked="">
        </label>
        <label title="Auto iterations at zoom">
          Auto
          <input type="checkbox" id="chkAutoIter" checked="">
        </label>
      </div>
    </div>
    <div id="paletteBar"></div>
    <div id="colorOffsetBar">
      <label title="Palette offset">
        <input type="range" id="sliderColorOffset" min="0" max="1" step="0.005" value="0">
      </label>
    </div>
  </div>
  <div id="zoomRectOverlay"></div>
  <div id="juliaPreview">
    <canvas id="juliaCanvas" width="240" height="240"></canvas>
    <div id="juliaLabel">Julia Preview</div>
  </div>
  <div id="bookmarkPanel" class="hidden">
    <div class="panelHeader">
      <span>Bookmarks</span>
      <button id="btnCloseBookmarks">✕</button>
    </div>
    <input type="text" id="bookmarkName" placeholder="Bookmark name...">
    <button id="btnSaveBookmark">Save</button>
    <div id="bookmarkList"></div>
  </div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./config":"src/config.ts","./state":"src/state.ts","./renderer":"src/renderer.ts","./input":"src/input.ts","./juliaPreview":"src/juliaPreview.ts","./ui":"src/ui.ts"},"src/state.ts":{"./config":"src/config.ts"},"src/renderer.ts":{"./config":"src/config.ts","./state":"src/state.ts"},"src/input.ts":{"./config":"src/config.ts","./state":"src/state.ts"},"src/juliaPreview.ts":{"./config":"src/config.ts","./state":"src/state.ts"},"src/ui.ts":{"./config":"src/config.ts","./state":"src/state.ts"}};
function __require(id) {
  if (__cache[id]) return __cache[id].exports;
  var module = __cache[id] = { exports: {} };
  var factory = __mods[id];
  if (!factory) throw new Error("Module not found: " + id);
  factory(module.exports, function (spec) {
    var target = (__map[id] && __map[id][spec]) || spec;
    return __require(target);
  }, module);
  return module.exports;
}

// ── module: src/main.ts ──
__mods["src/main.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const state_1 = require("./state");
const renderer_1 = require("./renderer");
const input_1 = require("./input");
const juliaPreview_1 = require("./juliaPreview");
const ui_1 = require("./ui");
// ===== Main Application =====
function main() {
    const canvas = document.getElementById('fractalCanvas');
    const juliaCanvas = document.getElementById('juliaCanvas');
    const rectOverlay = document.getElementById('zoomRectOverlay');
    // Initialize managers
    const stateMgr = new state_1.ViewStateManager();
    const colorizer = new state_1.PaletteColorizer();
    // Renderer
    const renderer = new renderer_1.FractalRenderer();
    const renderReq = { canvas, stateMgr, colorizer };
    renderer.setRequest(renderReq);
    // Julia preview
    const juliaPreview = new juliaPreview_1.JuliaPreviewRenderer(juliaCanvas, stateMgr);
    // UI controller
    const ui = new ui_1.UIController(stateMgr, (idx) => { stateMgr.setPaletteIndex(idx); renderer.scheduleRender(); }, (name) => { ui.saveBookmark(name); }, (bookmark) => { ui.loadBookmark(bookmark); renderer.scheduleRenderImmediate(); }, (id) => { ui.deleteBookmark(id); });
    // Input handler
    const inputHandler = new input_1.InputHandler(canvas, stateMgr, () => renderer.scheduleRender(), (re, im) => {
        const state = stateMgr.get();
        if (state.setType === 'mandelbrot') {
            juliaPreview.updatePreview(re, im);
        }
    }, rectOverlay);
    // Resize canvas to fill viewport
    function resizeCanvas() {
        const dpr = Math.min(window.devicePixelRatio || 1, 2);
        canvas.width = window.innerWidth * dpr;
        canvas.height = window.innerHeight * dpr;
        canvas.style.width = window.innerWidth + 'px';
        canvas.style.height = window.innerHeight + 'px';
        renderer.scheduleRender();
    }
    window.addEventListener('resize', () => {
        resizeCanvas();
    });
    // Initial render
    resizeCanvas();
    ui.updateUI();
    // FPS counter loop
    setInterval(() => ui.updateFPS(), 100);
    // Debug API on window
    const fractalAPI = {
        iterationsAt: (re, im) => stateMgr.iterationsAt(re, im),
        setView: ({ re, im, scale }) => {
            stateMgr.setView(re, im, scale, false);
            renderer.scheduleRenderImmediate();
            ui.updateUI();
        },
    };
    window.fractal = fractalAPI;
    // Show Julia preview when hovering canvas in Mandelbrot mode
    const juliaPreviewEl = document.getElementById('juliaPreview');
    canvas.addEventListener('mouseenter', () => {
        if (juliaPreviewEl && stateMgr.get().setType === 'mandelbrot') {
            juliaPreviewEl.classList.add('visible');
        }
    });
    canvas.addEventListener('mouseleave', () => {
        if (juliaPreviewEl) {
            juliaPreviewEl.classList.remove('visible');
            juliaPreview.hide();
        }
    });
    // Override setSetType to manage Julia preview visibility
    const origSetType = stateMgr.setSetType.bind(stateMgr);
    stateMgr.setSetType = (type) => {
        origSetType(type);
        if (juliaPreviewEl) {
            if (type === 'mandelbrot') {
                juliaPreviewEl.classList.add('visible');
                const c = stateMgr.get().juliaC || { re: -0.7269, im: 0.1889 };
                juliaPreview.updatePreview(c.re, c.im);
            }
            else {
                juliaPreviewEl.classList.remove('visible');
                juliaPreview.hide();
            }
        }
    };
    // Also expose helpers for testing
    window.fractalDragZoom = (x1, y1, x2, y2) => {
        const rect = canvas.getBoundingClientRect();
        const aspect = rect.width / rect.height;
        const state = stateMgr.get();
        const re1 = state.re + (x1 / rect.width - 0.5) * state.scale * aspect;
        const im1 = state.im + (y1 / rect.height - 0.5) * state.scale;
        const re2 = state.re + (x2 / rect.width - 0.5) * state.scale * aspect;
        const im2 = state.im + (y2 / rect.height - 0.5) * state.scale;
        const newRe = (re1 + re2) / 2;
        const newIm = (im1 + im2) / 2;
        const reRange = Math.abs(re2 - re1);
        const imRange = Math.abs(im2 - im1);
        let newScale = Math.max(reRange / aspect, imRange);
        newScale = Math.max(newScale, 1e-14);
        stateMgr.setView(newRe, newIm, newScale, true);
        renderer.scheduleRenderImmediate();
        ui.updateUI();
    };
    console.log('Fractal Explorer loaded');
    console.log('Debug API: window.fractal.iterationsAt(re, im), window.fractal.setView({re, im, scale})');
    console.log('Controls: Scroll to zoom, drag to pan, Shift+drag for rect zoom, J=Julia mode, arrows/+/- to navigate');
}
// Run on load (DOM already parsed when script executes)
main();
};

// ── module: src/config.ts ──
__mods["src/config.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PALETTES = exports.CONFIG = void 0;
// ===== Tunable Constants =====
exports.CONFIG = {
    // Iteration scaling: base iterations + scaling with zoom depth
    iterationsBase: 80,
    iterationsScale: 60, // added per log10(magnification)
    iterationsMaxCap: 2000,
    // Zoom behavior
    wheelZoomFactor: 0.9, // per wheel delta unit (inverted; <1 = zoom in on scroll down)
    rectZoomMinPixels: 15, // minimum drag rectangle size in pixels
    maxMagnification: 1e13, // precision limit warning threshold
    maxMagnificationHard: 1e14, // hard stop
    // Progressive rendering passes
    progressivePasses: [16, 8, 4, 2, 1], // downscale factors to render progressively
    progressiveMinTime: 60, // min ms per progressive pass before refining
    // Initial view (classic Mandelbrot)
    initialRe: -0.5,
    initialIm: 0,
    initialScale: 3, // viewport width in complex plane units
    // Julia preview
    juliaPreviewSize: 240,
    juliaRenderScale: 2.5, // Julia set viewport width
    // Performance
    renderTimeout: 100, // ms before starting render (debounce)
};
exports.PALETTES = [
    {
        name: "Fire",
        swatch: "linear-gradient(90deg, #000, #f00, #ff0, #fff)",
        stops: [
            { t: 0, r: 0, g: 0, b: 0 },
            { t: 0.15, r: 30, g: 0, b: 50 },
            { t: 0.3, r: 180, g: 20, b: 0 },
            { t: 0.5, r: 255, g: 160, b: 0 },
            { t: 0.7, r: 255, g: 255, b: 100 },
            { t: 1, r: 255, g: 255, b: 255 },
        ],
    },
    {
        name: "Ocean",
        swatch: "linear-gradient(90deg, #000, #00f, #0af, #aff)",
        stops: [
            { t: 0, r: 0, g: 0, b: 0 },
            { t: 0.2, r: 0, g: 10, b: 60 },
            { t: 0.4, r: 0, g: 40, b: 140 },
            { t: 0.6, r: 0, g: 120, b: 200 },
            { t: 0.8, r: 50, g: 200, b: 240 },
            { t: 1, r: 180, g: 240, b: 255 },
        ],
    },
    {
        name: "Monochrome",
        swatch: "linear-gradient(90deg, #000, #333, #888, #ccc, #fff)",
        stops: [
            { t: 0, r: 0, g: 0, b: 0 },
            { t: 0.25, r: 40, g: 40, b: 45 },
            { t: 0.5, r: 100, g: 100, b: 110 },
            { t: 0.75, r: 170, g: 170, b: 180 },
            { t: 1, r: 240, g: 240, b: 250 },
        ],
    },
    {
        name: "Psychedelic",
        swatch: "linear-gradient(90deg, #f00, #0f0, #00f, #f00)",
        stops: [
            { t: 0, r: 128, g: 0, b: 128 },
            { t: 0.17, r: 0, g: 0, b: 255 },
            { t: 0.33, r: 0, g: 255, b: 255 },
            { t: 0.5, r: 0, g: 255, b: 0 },
            { t: 0.67, r: 255, g: 255, b: 0 },
            { t: 0.83, r: 255, g: 0, b: 0 },
            { t: 1, r: 128, g: 0, b: 128 },
        ],
    },
    {
        name: "Electric",
        swatch: "linear-gradient(90deg, #200, #90a, #a2f, #6ff)",
        stops: [
            { t: 0, r: 10, g: 0, b: 20 },
            { t: 0.2, r: 60, g: 0, b: 100 },
            { t: 0.4, r: 140, g: 20, b: 180 },
            { t: 0.6, r: 200, g: 60, b: 255 },
            { t: 0.8, r: 100, g: 200, b: 255 },
            { t: 1, r: 50, g: 255, b: 220 },
        ],
    },
];
};

// ── module: src/state.ts ──
__mods["src/state.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PaletteColorizer = exports.ViewStateManager = void 0;
const config_1 = require("./config");
// ===== View State Management =====
class ViewStateManager {
    constructor() {
        this.history = [];
        this.historyIndex = -1;
        this.state = this.createInitialState();
    }
    createInitialState() {
        return {
            setType: 'mandelbrot',
            re: config_1.CONFIG.initialRe,
            im: config_1.CONFIG.initialIm,
            scale: config_1.CONFIG.initialScale,
            maxIterations: config_1.CONFIG.iterationsBase,
            smoothColoring: true,
            autoIterations: true,
            paletteIndex: 0,
            colorOffset: 0,
            juliaC: { re: -0.7269, im: 0.1889 },
        };
    }
    get() {
        return this.state;
    }
    getMagnification() {
        return config_1.CONFIG.initialScale / this.state.scale;
    }
    // Update view and manage history
    setView(re, im, scale, pushHistory = true) {
        if (scale < 1e-14)
            scale = 1e-14;
        const oldScale = this.state.scale;
        this.state.re = re;
        this.state.im = im;
        this.state.scale = scale;
        this.updateIterationsForZoom();
        if (pushHistory) {
            // Don't duplicate entries when just adjusting iterations
            if (oldScale === this.state.scale && this.historyIndex >= 0) {
                return;
            }
            // If we went forward in history then panned, cut the future
            if (this.historyIndex < this.history.length - 1) {
                this.history = this.history.slice(0, this.historyIndex + 1);
            }
            this.history.push({ re, im, scale });
            this.historyIndex = this.history.length - 1;
            // Limit history size
            if (this.history.length > 100) {
                this.history.shift();
                this.historyIndex--;
            }
        }
    }
    setSetType(type) {
        this.state.setType = type;
    }
    setJuliaC(re, im) {
        this.state.juliaC = { re, im };
    }
    setMaxIterations(max) {
        this.state.maxIterations = max;
        this.state.autoIterations = false;
    }
    toggleAutoIterations() {
        this.state.autoIterations = !this.state.autoIterations;
        if (this.state.autoIterations) {
            this.updateIterationsForZoom();
        }
    }
    setPaletteIndex(idx) {
        this.state.paletteIndex = idx;
    }
    setColorOffset(offset) {
        this.state.colorOffset = offset;
    }
    setSmoothColoring(smooth) {
        this.state.smoothColoring = smooth;
    }
    updateIterationsForZoom() {
        if (!this.state.autoIterations)
            return;
        const mag = this.getMagnification();
        const logMag = Math.log10(Math.max(1, mag));
        let iter = config_1.CONFIG.iterationsBase + Math.floor(config_1.CONFIG.iterationsScale * logMag);
        this.state.maxIterations = Math.min(iter, config_1.CONFIG.iterationsMaxCap);
    }
    // History navigation
    canGoBack() {
        return this.historyIndex > 0;
    }
    goBack() {
        if (!this.canGoBack())
            return false;
        this.historyIndex--;
        const entry = this.history[this.historyIndex];
        this.state.re = entry.re;
        this.state.im = entry.im;
        this.state.scale = entry.scale;
        this.updateIterationsForZoom();
        return true;
    }
    reset() {
        const init = this.createInitialState();
        this.state.re = init.re;
        this.state.im = init.im;
        this.state.scale = init.scale;
        this.state.setType = init.setType;
        this.updateIterationsForZoom();
        // Reset history
        this.history = [{ re: init.re, im: init.im, scale: init.scale }];
        this.historyIndex = 0;
    }
    // ===== Fractal Math =====
    // Compute Mandelbrot iteration count at point c = re + im*i
    mandelbrotIter(re, im, maxIter) {
        let zr = 0, zi = 0;
        let zr2 = 0, zi2 = 0;
        for (let n = 0; n < maxIter; n++) {
            // z = z^2 + c
            zi = 2 * zr * zi + im;
            zr = zr2 - zi2 + re;
            zr2 = zr * zr;
            zi2 = zi * zi;
            if (zr2 + zi2 > 4)
                return n;
        }
        return maxIter; // interior point
    }
    // Compute Julia iteration count for z = (re, im) with parameter c
    juliaIter(re, im, cRe, cIm, maxIter) {
        let zr = re, zi = im;
        let zr2 = zr * zr, zi2 = zi * zi;
        for (let n = 0; n < maxIter; n++) {
            // z = z^2 + c
            zi = 2 * zr * zi + cIm;
            zr = zr2 - zi2 + cRe;
            zr2 = zr * zr;
            zi2 = zi * zi;
            if (zr2 + zi2 > 4)
                return n;
        }
        return maxIter; // interior point
    }
    // Smooth iteration count (continuous coloring)
    smoothIter(n, zr2, zi2, maxIter) {
        if (n >= maxIter)
            return maxIter; // interior
        const modulus = Math.sqrt(zr2 + zi2);
        return n + 1 - Math.log2(Math.log(modulus));
    }
    // Public debug: get iteration count at a point in the current view
    iterationsAt(re, im) {
        const state = this.state;
        if (state.setType === 'mandelbrot') {
            return this.mandelbrotIter(re, im, state.maxIterations);
        }
        else if (state.juliaC) {
            return this.juliaIter(re, im, state.juliaC.re, state.juliaC.im, state.maxIterations);
        }
        return 0;
    }
}
exports.ViewStateManager = ViewStateManager;
// ===== Palette Colorizer =====
class PaletteColorizer {
    constructor() {
        // Precompute color LUT for speed: maps smooth iteration value [0, maxIter] -> RGBA32
        this.lut = new Uint32Array(0);
        this.initialized = false;
    }
    init(maxIter, palette, offset) {
        this.lut = new Uint32Array(maxIter + 1);
        this.fillLUT(maxIter, palette, offset);
        this.initialized = true;
    }
    fillLUT(maxIter, palette, offset) {
        const stops = palette.stops;
        const lut = this.lut;
        for (let i = 0; i <= maxIter; i++) {
            // Normalize iteration to [0, 1] with offset wrapping
            let t = (i / maxIter + offset) % 1;
            // Find the two surrounding color stops
            let a = stops[0], b = stops[stops.length - 1];
            for (let j = 0; j < stops.length - 1; j++) {
                if (t >= stops[j].t && t <= stops[j + 1].t) {
                    a = stops[j];
                    b = stops[j + 1];
                    break;
                }
            }
            // Interpolate between stops
            const span = b.t - a.t;
            const frac = span === 0 ? 0 : (t - a.t) / span;
            const r = a.r + (b.r - a.r) * frac | 0;
            const g = a.g + (b.g - a.g) * frac | 0;
            const bl = a.b + (b.b - a.b) * frac | 0;
            // Store as ABGR (little-endian Uint32)
            lut[i] = (255 << 24) | (bl << 16) | (g << 8) | r;
        }
    }
    getColor(smoothN, maxIter) {
        if (!this.initialized)
            return 0xff000000;
        if (smoothN >= maxIter)
            return 0xff000000; // interior: black
        const idx = Math.min(Math.max(smoothN | 0, 0), maxIter);
        return this.lut[idx];
    }
    getColorSmooth(smoothN, maxIter, palette) {
        if (!this.initialized)
            return 0xff000000;
        if (smoothN >= maxIter)
            return 0xff000000;
        // For true smooth coloring, interpolate between LUT entries
        const lo = Math.max(0, smoothN | 0);
        const hi = Math.min(maxIter, lo + 1);
        const frac = smoothN - lo;
        if (lo === hi)
            return this.lut[lo];
        const cLo = this.lut[lo];
        const cHi = this.lut[hi];
        const rLo = cLo & 0xff;
        const gLo = (cLo >> 8) & 0xff;
        const bLo = (cLo >> 16) & 0xff;
        const rHi = cHi & 0xff;
        const gHi = (cHi >> 8) & 0xff;
        const bHi = (cHi >> 16) & 0xff;
        const r = rLo + (rHi - rLo) * frac | 0;
        const g = gLo + (gHi - gLo) * frac | 0;
        const b = bLo + (bHi - bLo) * frac | 0;
        return (255 << 24) | (b << 16) | (g << 8) | r;
    }
}
exports.PaletteColorizer = PaletteColorizer;
};

// ── module: src/renderer.ts ──
__mods["src/renderer.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.FractalRenderer = void 0;
const config_1 = require("./config");
class FractalRenderer {
    constructor() {
        this.request = null;
        this.currentTask = null;
        this.renderTimer = null;
    }
    setRequest(req) {
        this.request = req;
    }
    // Schedule a render with debounce; cancels any in-progress render
    scheduleRender() {
        // Cancel in-progress render
        if (this.currentTask) {
            this.currentTask.cancelled = true;
            this.currentTask = null;
        }
        // Cancel pending timer
        if (this.renderTimer) {
            clearTimeout(this.renderTimer);
            this.renderTimer = null;
        }
        // Debounce: wait a bit for user input to settle
        this.renderTimer = window.setTimeout(() => {
            this.renderTimer = null;
            this.startRender();
        }, config_1.CONFIG.renderTimeout);
    }
    // Immediate render (no debounce), e.g. for progressive refinement
    scheduleRenderImmediate() {
        if (this.currentTask) {
            this.currentTask.cancelled = true;
            this.currentTask = null;
        }
        if (this.renderTimer) {
            clearTimeout(this.renderTimer);
            this.renderTimer = null;
        }
        this.startRender();
    }
    startRender() {
        const req = this.request;
        if (!req)
            return;
        const canvas = req.canvas;
        const ctx = canvas.getContext('2d', { willReadFrequently: false });
        if (!ctx)
            return;
        const width = canvas.width;
        const height = canvas.height;
        if (width === 0 || height === 0)
            return;
        // Create image data buffer
        const imageData = ctx.createImageData(width, height);
        const data = imageData.data;
        // Create cancel token
        const cancelToken = { cancelled: false };
        this.currentTask = cancelToken;
        const state = req.stateMgr.get();
        const maxIter = state.maxIterations;
        const smooth = state.smoothColoring;
        const palette = config_1.PALETTES[state.paletteIndex];
        // Update colorizer if needed
        req.colorizer.init(maxIter, palette, state.colorOffset);
        // Progressive rendering through passes
        this.renderProgressive(ctx, imageData, data, width, height, state, maxIter, smooth, palette, cancelToken, 0);
    }
    renderProgressive(ctx, imageData, data, width, height, state, maxIter, smooth, palette, cancelToken, passIndex) {
        if (cancelToken.cancelled)
            return;
        const passes = config_1.CONFIG.progressivePasses;
        if (passIndex >= passes.length) {
            // Render complete
            this.currentTask = null;
            return;
        }
        const downscale = passes[passIndex];
        const pw = Math.ceil(width / downscale);
        const ph = Math.ceil(height / downscale);
        const startTime = performance.now();
        // Render at current resolution
        this.renderFractal(data, width, height, pw, ph, state, maxIter, smooth, cancelToken, palette);
        // Blit to canvas
        ctx.putImageData(imageData, 0, 0);
        if (cancelToken.cancelled)
            return;
        // Schedule next pass
        const elapsed = performance.now() - startTime;
        if (elapsed < config_1.CONFIG.progressiveMinTime && passIndex < passes.length - 1) {
            // Render fast enough, go to next resolution immediately
            this.renderProgressive(ctx, imageData, data, width, height, state, maxIter, smooth, palette, cancelToken, passIndex + 1);
        }
        else {
            // Yield to let UI respond
            requestAnimationFrame(() => {
                if (!cancelToken.cancelled) {
                    this.renderProgressive(ctx, imageData, data, width, height, state, maxIter, smooth, palette, cancelToken, passIndex + 1);
                }
            });
        }
    }
    renderFractal(data, fullWidth, fullHeight, renderWidth, renderHeight, state, maxIter, smooth, cancelToken, palette) {
        const reCenter = state.re;
        const imCenter = state.im;
        const scale = state.scale;
        const setType = state.setType;
        const juliaC = state.juliaC;
        // Aspect ratio correction
        const aspect = fullWidth / fullHeight;
        const reScale = scale * aspect / 2; // half-width in re
        const imScale = scale / 2; // half-height in im
        const colorizer = this.request?.colorizer;
        if (!colorizer)
            return;
        const getColorSmoothFn = colorizer.getColorSmooth.bind(colorizer);
        const getColorFn = colorizer.getColor.bind(colorizer);
        // Render row by row at reduced resolution, then scale up
        const rowsPerCheck = Math.max(1, Math.floor(renderHeight / 20));
        for (let ry = 0; ry < renderHeight; ry++) {
            if (cancelToken.cancelled)
                return;
            // Check cancellation periodically
            if ((ry + 1) % rowsPerCheck === 0) {
                if (cancelToken.cancelled)
                    return;
            }
            // Map screen row y to complex plane imaginary coordinate
            const im = imCenter + (ry / renderHeight - 0.5) * scale;
            for (let rx = 0; rx < renderWidth; rx++) {
                // Map screen col x to complex plane real coordinate
                const re = reCenter + (rx / renderWidth - 0.5) * scale * aspect;
                let n;
                let finalZr2 = 0, finalZi2 = 0;
                if (setType === 'mandelbrot') {
                    // Mandelbrot: z0=0, c=(re,im)
                    let zr = 0, zi = 0;
                    let zr2 = 0, zi2 = 0;
                    for (n = 0; n < maxIter; n++) {
                        zi = 2 * zr * zi + im;
                        zr = zr2 - zi2 + re;
                        zr2 = zr * zr;
                        zi2 = zi * zi;
                        if (zr2 + zi2 > 4)
                            break;
                    }
                    finalZr2 = zr2;
                    finalZi2 = zi2;
                }
                else if (juliaC) {
                    // Julia: c=juliaC, z0=(re,im)
                    let zr = re, zi = im;
                    let zr2 = zr * zr, zi2 = zi * zi;
                    const cRe = juliaC.re, cIm = juliaC.im;
                    for (n = 0; n < maxIter; n++) {
                        zi = 2 * zr * zi + cIm;
                        zr = zr2 - zi2 + cRe;
                        zr2 = zr * zr;
                        zi2 = zi * zi;
                        if (zr2 + zi2 > 4)
                            break;
                    }
                    finalZr2 = zr2;
                    finalZi2 = zi2;
                }
                else {
                    n = maxIter;
                }
                // Compute color
                let color;
                if (smooth && n < maxIter && finalZr2 + finalZi2 > 0) {
                    const smoothN = n + 1 - Math.log2(Math.log(Math.sqrt(finalZr2 + finalZi2)));
                    color = getColorSmoothFn(smoothN, maxIter, palette);
                }
                else {
                    color = getColorFn(Math.min(n, maxIter), maxIter);
                }
                // Write pixel to buffer (ABGR little-endian in data[] as RGBA)
                const r = color & 0xff;
                const g = (color >> 8) & 0xff;
                const b = (color >> 16) & 0xff;
                // Upscale this pixel to the full-resolution block
                const xs = rx * (fullWidth / renderWidth) | 0;
                const ys = ry * (fullHeight / renderHeight) | 0;
                const xe = (rx + 1) * (fullWidth / renderWidth) | 0;
                const ye = (ry + 1) * (fullHeight / renderHeight) | 0;
                for (let y = ys; y < ye && y < fullHeight; y++) {
                    const rowOffset = y * fullWidth * 4;
                    for (let x = xs; x < xe && x < fullWidth; x++) {
                        const idx = rowOffset + x * 4;
                        data[idx] = r;
                        data[idx + 1] = g;
                        data[idx + 2] = b;
                        data[idx + 3] = 255;
                    }
                }
            }
        }
    }
}
exports.FractalRenderer = FractalRenderer;
};

// ── module: src/input.ts ──
__mods["src/input.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.InputHandler = void 0;
const config_1 = require("./config");
class InputHandler {
    constructor(canvas, stateMgr, onRenderRequested, onJuliaPreview, rectOverlay) {
        this.isDragging = false;
        this.isPanning = false;
        this.dragStartX = 0;
        this.dragStartY = 0;
        this.dragStartRe = 0;
        this.dragStartIm = 0;
        this.dragStartScale = 0;
        // Rect zoom state
        this.rectZoomStartX = 0;
        this.rectZoomStartY = 0;
        this.rectZoomEndX = 0;
        this.rectZoomEndY = 0;
        this.isRectZooming = false;
        // Touch events (basic support)
        this.lastTouchDist = 0;
        this.lastTouchCenter = { x: 0, y: 0 };
        this.canvas = canvas;
        this.stateMgr = stateMgr;
        this.onRenderRequested = onRenderRequested;
        this.onJuliaPreview = onJuliaPreview;
        this.rectOverlay = rectOverlay;
        this.setupEvents();
    }
    setupEvents() {
        // Mouse events - shift+click for rect zoom, regular click for pan
        this.canvas.addEventListener('mousedown', this.onMouseDown.bind(this));
        window.addEventListener('mousemove', this.onMouseMove.bind(this));
        window.addEventListener('mouseup', this.onMouseUp.bind(this));
        this.canvas.addEventListener('contextmenu', (e) => e.preventDefault());
        // Wheel zoom
        this.canvas.addEventListener('wheel', this.onWheel.bind(this), { passive: false });
        // Keyboard
        window.addEventListener('keydown', this.onKeyDown.bind(this));
        // Touch support
        this.canvas.addEventListener('touchstart', this.onTouchStart.bind(this), { passive: false });
        window.addEventListener('touchmove', this.onTouchMove.bind(this), { passive: false });
        window.addEventListener('touchend', this.onTouchEnd.bind(this));
    }
    screenToComplex(sx, sy) {
        const rect = this.canvas.getBoundingClientRect();
        const x = (sx - rect.left) / rect.width;
        const y = (sy - rect.top) / rect.height;
        const state = this.stateMgr.get();
        const aspect = rect.width / rect.height;
        const re = state.re + (x - 0.5) * state.scale * aspect;
        const im = state.im + (y - 0.5) * state.scale;
        return { re, im };
    }
    onMouseDown(e) {
        if (e.button === 0 && e.shiftKey) {
            // Rect zoom mode (Shift + left click)
            this.isRectZooming = true;
            this.rectZoomStartX = e.clientX;
            this.rectZoomStartY = e.clientY;
            this.rectZoomEndX = e.clientX;
            this.rectZoomEndY = e.clientY;
            this.rectOverlay.style.display = 'block';
            this.updateRectOverlay();
            this.canvas.style.cursor = 'crosshair';
        }
        else if (e.button === 0 || e.button === 2) {
            // Pan mode
            this.isPanning = true;
            this.dragStartX = e.clientX;
            this.dragStartY = e.clientY;
            this.dragStartRe = this.stateMgr.get().re;
            this.dragStartIm = this.stateMgr.get().im;
            this.dragStartScale = this.stateMgr.get().scale;
            this.canvas.style.cursor = 'grabbing';
        }
    }
    updateRectOverlay() {
        const rect = this.canvas.getBoundingClientRect();
        const x = Math.min(this.rectZoomStartX, this.rectZoomEndX) - rect.left;
        const y = Math.min(this.rectZoomStartY, this.rectZoomEndY) - rect.top;
        const w = Math.abs(this.rectZoomEndX - this.rectZoomStartX);
        const h = Math.abs(this.rectZoomEndY - this.rectZoomStartY);
        this.rectOverlay.style.left = x + 'px';
        this.rectOverlay.style.top = y + 'px';
        this.rectOverlay.style.width = w + 'px';
        this.rectOverlay.style.height = h + 'px';
    }
    onMouseMove(e) {
        const state = this.stateMgr.get();
        if (this.isRectZooming) {
            this.rectZoomEndX = e.clientX;
            this.rectZoomEndY = e.clientY;
            this.updateRectOverlay();
            return;
        }
        if (this.isPanning) {
            const rect = this.canvas.getBoundingClientRect();
            const dx = e.clientX - this.dragStartX;
            const dy = e.clientY - this.dragStartY;
            const aspect = rect.width / rect.height;
            const re = this.dragStartRe - (dx / rect.width) * this.dragStartScale * aspect;
            const im = this.dragStartIm - (dy / rect.height) * this.dragStartScale;
            this.stateMgr.setView(re, im, this.dragStartScale, false);
            this.onRenderRequested();
        }
        // Julia preview on hover (only in Mandelbrot mode)
        if (state.setType === 'mandelbrot' && !this.isPanning) {
            const c = this.screenToComplex(e.clientX, e.clientY);
            this.onJuliaPreview(c.re, c.im);
        }
    }
    onMouseUp(e) {
        this.isPanning = false;
        if (this.isRectZooming) {
            this.isRectZooming = false;
            this.rectOverlay.style.display = 'none';
            // Only zoom if rectangle is large enough
            const dx = Math.abs(e.clientX - this.rectZoomStartX);
            const dy = Math.abs(e.clientY - this.rectZoomStartY);
            if (dx >= config_1.CONFIG.rectZoomMinPixels && dy >= config_1.CONFIG.rectZoomMinPixels) {
                this.applyRectZoom();
            }
        }
        this.canvas.style.cursor = '';
    }
    applyRectZoom() {
        const rect = this.canvas.getBoundingClientRect();
        const x1 = Math.min(this.rectZoomStartX, this.rectZoomEndX) - rect.left;
        const y1 = Math.min(this.rectZoomStartY, this.rectZoomEndY) - rect.top;
        const x2 = Math.max(this.rectZoomStartX, this.rectZoomEndX) - rect.left;
        const y2 = Math.max(this.rectZoomStartY, this.rectZoomEndY) - rect.top;
        // Convert rectangle corners to complex plane coordinates
        const aspect = rect.width / rect.height;
        const state = this.stateMgr.get();
        const re1 = state.re + (x1 / rect.width - 0.5) * state.scale * aspect;
        const im1 = state.im + (y1 / rect.height - 0.5) * state.scale;
        const re2 = state.re + (x2 / rect.width - 0.5) * state.scale * aspect;
        const im2 = state.im + (y2 / rect.height - 0.5) * state.scale;
        // New center is midpoint of rectangle
        const newRe = (re1 + re2) / 2;
        const newIm = (im1 + im2) / 2;
        // New scale is determined by the rectangle's larger dimension (to fully contain it)
        const reRange = Math.abs(re2 - re1);
        const imRange = Math.abs(im2 - im1);
        // Aspect ratio: scale is viewport width. To fit rectangle, we need scale * aspect >= reRange and scale >= imRange
        let newScale = Math.max(reRange / aspect, imRange);
        newScale = Math.max(newScale, 1e-14);
        // Check hard max magnification
        if (config_1.CONFIG.initialScale / newScale > config_1.CONFIG.maxMagnificationHard) {
            const maxScale = config_1.CONFIG.initialScale / config_1.CONFIG.maxMagnificationHard;
            newScale = Math.max(newScale, maxScale);
        }
        this.stateMgr.setView(newRe, newIm, newScale, true);
        this.onRenderRequested();
    }
    onWheel(e) {
        e.preventDefault();
        const state = this.stateMgr.get();
        const rect = this.canvas.getBoundingClientRect();
        // Cursor position in complex plane
        const c = this.screenToComplex(e.clientX, e.clientY);
        // Zoom factor based on wheel delta
        const zoomFactor = e.deltaY > 0 ? config_1.CONFIG.wheelZoomFactor : 1 / config_1.CONFIG.wheelZoomFactor;
        const newScale = state.scale * zoomFactor;
        // Check max magnification
        const magnification = config_1.CONFIG.initialScale / newScale;
        if (magnification > config_1.CONFIG.maxMagnificationHard) {
            return;
        }
        // Compute new center so cursor point stays fixed
        const aspect = rect.width / rect.height;
        const newRe = c.re - (e.clientX / rect.width - 0.5) * newScale * aspect;
        const newIm = c.im - (e.clientY / rect.height - 0.5) * newScale;
        this.stateMgr.setView(newRe, newIm, newScale, true);
        this.onRenderRequested();
    }
    onKeyDown(e) {
        const state = this.stateMgr.get();
        const moveAmount = state.scale * 0.1;
        switch (e.key) {
            case 'ArrowLeft':
                this.stateMgr.setView(state.re - moveAmount, state.im, state.scale, true);
                this.onRenderRequested();
                break;
            case 'ArrowRight':
                this.stateMgr.setView(state.re + moveAmount, state.im, state.scale, true);
                this.onRenderRequested();
                break;
            case 'ArrowUp':
                this.stateMgr.setView(state.re, state.im - moveAmount, state.scale, true);
                this.onRenderRequested();
                break;
            case 'ArrowDown':
                this.stateMgr.setView(state.re, state.im + moveAmount, state.scale, true);
                this.onRenderRequested();
                break;
            case '+':
            case '=':
                this.zoomAtCenter(0.6);
                break;
            case '-':
                this.zoomAtCenter(1.5);
                break;
            case 'j':
            case 'J':
                if (state.setType === 'mandelbrot' && state.juliaC) {
                    this.stateMgr.setSetType('julia');
                    this.stateMgr.setView(state.juliaC.re, state.juliaC.im, config_1.CONFIG.juliaRenderScale, true);
                    this.onRenderRequested();
                }
                break;
            case 'm':
            case 'M':
                if (state.setType === 'julia') {
                    this.stateMgr.setSetType('mandelbrot');
                    this.stateMgr.setView(config_1.CONFIG.initialRe, config_1.CONFIG.initialIm, config_1.CONFIG.initialScale, true);
                    this.onRenderRequested();
                }
                break;
        }
    }
    zoomAtCenter(factor) {
        const state = this.stateMgr.get();
        const newScale = state.scale * factor;
        const magnification = config_1.CONFIG.initialScale / newScale;
        if (magnification > config_1.CONFIG.maxMagnificationHard)
            return;
        this.stateMgr.setView(state.re, state.im, newScale, true);
        this.onRenderRequested();
    }
    onTouchStart(e) {
        e.preventDefault();
        if (e.touches.length === 1) {
            this.isPanning = true;
            this.dragStartX = e.touches[0].clientX;
            this.dragStartY = e.touches[0].clientY;
            this.dragStartRe = this.stateMgr.get().re;
            this.dragStartIm = this.stateMgr.get().im;
            this.dragStartScale = this.stateMgr.get().scale;
        }
        else if (e.touches.length === 2) {
            this.isPanning = false;
            const dx = e.touches[1].clientX - e.touches[0].clientX;
            const dy = e.touches[1].clientY - e.touches[0].clientY;
            this.lastTouchDist = Math.sqrt(dx * dx + dy * dy);
            this.lastTouchCenter = {
                x: (e.touches[0].clientX + e.touches[1].clientX) / 2,
                y: (e.touches[0].clientY + e.touches[1].clientY) / 2,
            };
        }
    }
    onTouchMove(e) {
        e.preventDefault();
        if (e.touches.length === 1 && this.isPanning) {
            const rect = this.canvas.getBoundingClientRect();
            const dx = e.touches[0].clientX - this.dragStartX;
            const dy = e.touches[0].clientY - this.dragStartY;
            const aspect = rect.width / rect.height;
            const re = this.dragStartRe - (dx / rect.width) * this.dragStartScale * aspect;
            const im = this.dragStartIm - (dy / rect.height) * this.dragStartScale;
            this.stateMgr.setView(re, im, this.dragStartScale, false);
            this.onRenderRequested();
        }
        else if (e.touches.length === 2) {
            const dx = e.touches[1].clientX - e.touches[0].clientX;
            const dy = e.touches[1].clientY - e.touches[0].clientY;
            const dist = Math.sqrt(dx * dx + dy * dy);
            const zoomFactor = this.lastTouchDist / dist;
            const rect = this.canvas.getBoundingClientRect();
            const cx = (e.touches[0].clientX + e.touches[1].clientX) / 2;
            const cy = (e.touches[0].clientY + e.touches[1].clientY) / 2;
            const c = this.screenToComplex(cx, cy);
            const state = this.stateMgr.get();
            const newScale = Math.max(1e-14, state.scale * zoomFactor);
            const aspect = rect.width / rect.height;
            const newRe = c.re - (cx / rect.width - 0.5) * newScale * aspect;
            const newIm = c.im - (cy / rect.height - 0.5) * newScale;
            this.stateMgr.setView(newRe, newIm, newScale, true);
            this.lastTouchDist = dist;
            this.onRenderRequested();
        }
    }
    onTouchEnd(e) {
        this.isPanning = false;
    }
}
exports.InputHandler = InputHandler;
};

// ── module: src/juliaPreview.ts ──
__mods["src/juliaPreview.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.JuliaPreviewRenderer = void 0;
const config_1 = require("./config");
const state_1 = require("./state");
class JuliaPreviewRenderer {
    constructor(canvas, stateMgr) {
        this.imageData = null;
        this.lastPreviewC = { re: 0, im: 0 };
        this.rAFId = null;
        this.canvas = canvas;
        this.stateMgr = stateMgr;
        this.colorizer = new state_1.PaletteColorizer();
        const size = config_1.CONFIG.juliaPreviewSize;
        canvas.width = size;
        canvas.height = size;
        this.imageData = canvas.getContext('2d')?.createImageData(size, size) || null;
    }
    updatePreview(re, im) {
        // Don't recompute if c hasn't changed significantly
        if (Math.abs(re - this.lastPreviewC.re) < 1e-6 && Math.abs(im - this.lastPreviewC.im) < 1e-6) {
            return;
        }
        this.lastPreviewC = { re, im };
        // Schedule render
        if (this.rAFId === null) {
            this.rAFId = requestAnimationFrame(() => this.render(re, im));
        }
    }
    render(cRe, cIm) {
        this.rAFId = null;
        if (!this.imageData)
            return;
        const ctx = this.canvas.getContext('2d');
        if (!ctx)
            return;
        const size = config_1.CONFIG.juliaPreviewSize;
        const data = this.imageData.data;
        const state = this.stateMgr.get();
        const maxIter = Math.min(state.maxIterations, 200); // Lower iter count for preview
        const smooth = state.smoothColoring;
        const palette = config_1.PALETTES[state.paletteIndex];
        this.colorizer.init(maxIter, palette, state.colorOffset);
        const scale = config_1.CONFIG.juliaRenderScale;
        for (let y = 0; y < size; y++) {
            const im = (y / size - 0.5) * scale;
            for (let x = 0; x < size; x++) {
                const re = (x / size - 0.5) * scale;
                // Julia iteration
                let zr = re, zi = im;
                let zr2 = zr * zr, zi2 = zi * zi;
                let n = 0;
                for (; n < maxIter; n++) {
                    zi = 2 * zr * zi + cIm;
                    zr = zr2 - zi2 + cRe;
                    zr2 = zr * zr;
                    zi2 = zi * zi;
                    if (zr2 + zi2 > 4)
                        break;
                }
                const idx = (y * size + x) * 4;
                if (n >= maxIter) {
                    // Interior
                    data[idx] = 0;
                    data[idx + 1] = 0;
                    data[idx + 2] = 0;
                    data[idx + 3] = 255;
                }
                else if (smooth && zr2 + zi2 > 0) {
                    const smoothN = n + 1 - Math.log2(Math.log(Math.sqrt(zr2 + zi2)));
                    const color = this.colorizer.getColorSmooth(smoothN, maxIter, palette);
                    data[idx] = color & 0xff;
                    data[idx + 1] = (color >> 8) & 0xff;
                    data[idx + 2] = (color >> 16) & 0xff;
                    data[idx + 3] = 255;
                }
                else {
                    const color = this.colorizer.getColor(Math.min(n, maxIter), maxIter);
                    data[idx] = color & 0xff;
                    data[idx + 1] = (color >> 8) & 0xff;
                    data[idx + 2] = (color >> 16) & 0xff;
                    data[idx + 3] = 255;
                }
            }
        }
        ctx.putImageData(this.imageData, 0, 0);
    }
    hide() {
        this.lastPreviewC = { re: Infinity, im: Infinity };
        const ctx = this.canvas.getContext('2d');
        if (ctx) {
            ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
        }
    }
}
exports.JuliaPreviewRenderer = JuliaPreviewRenderer;
};

// ── module: src/ui.ts ──
__mods["src/ui.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.UIController = void 0;
const config_1 = require("./config");
class UIController {
    constructor(stateMgr, onSetPalette, onSaveBookmark, onLoadBookmark, onDeleteBookmark) {
        this.hudTimeout = null;
        this.fpsFrames = 0;
        this.fpsLastTime = performance.now();
        this.fpsValue = 60;
        this.stateMgr = stateMgr;
        this.onSetPalette = onSetPalette;
        this.onSaveBookmark = onSaveBookmark;
        this.onLoadBookmark = onLoadBookmark;
        this.onDeleteBookmark = onDeleteBookmark;
        this.cacheElements();
        this.setupPaletteBar();
        this.setupEventListeners();
    }
    cacheElements() {
        this.elMagnify = document.getElementById('hudMagnify');
        this.elFPS = document.getElementById('hudFPS');
        this.elMode = document.getElementById('hudMode');
        this.elCenterCoords = document.getElementById('hudCenterCoords');
        this.elSliderIter = document.getElementById('sliderIter');
        this.elIterValue = document.getElementById('iterValue');
        this.elChkSmooth = document.getElementById('chkSmooth');
        this.elChkAutoIter = document.getElementById('chkAutoIter');
        this.elSliderColorOffset = document.getElementById('sliderColorOffset');
        this.elPaletteBar = document.getElementById('paletteBar');
        this.elBookmarkPanel = document.getElementById('bookmarkPanel');
        this.elBookmarkName = document.getElementById('bookmarkName');
        this.elBookmarkList = document.getElementById('bookmarkList');
    }
    setupPaletteBar() {
        this.elPaletteBar.innerHTML = '';
        config_1.PALETTES.forEach((palette, idx) => {
            const chip = document.createElement('div');
            chip.className = 'palette-chip';
            chip.style.background = palette.swatch;
            chip.title = palette.name;
            chip.addEventListener('click', () => {
                this.onSetPalette(idx);
                this.updateUI();
            });
            this.elPaletteBar.appendChild(chip);
        });
    }
    setupEventListeners() {
        // Reset button
        document.getElementById('btnReset')?.addEventListener('click', () => {
            this.stateMgr.reset();
            this.updateUI();
        });
        // Back button
        document.getElementById('btnBack')?.addEventListener('click', () => {
            if (this.stateMgr.goBack()) {
                this.updateUI();
            }
        });
        // Iteration slider
        this.elSliderIter.addEventListener('input', () => {
            const val = parseInt(this.elSliderIter.value, 10);
            this.elIterValue.textContent = val.toString();
            this.stateMgr.setMaxIterations(val);
            this.updateUI();
        });
        // Smooth coloring checkbox
        this.elChkSmooth.addEventListener('change', () => {
            this.stateMgr.setSmoothColoring(this.elChkSmooth.checked);
            this.updateUI();
        });
        // Auto iterations checkbox
        this.elChkAutoIter.addEventListener('change', () => {
            this.stateMgr.toggleAutoIterations();
            this.updateUI();
        });
        // Color offset slider
        this.elSliderColorOffset.addEventListener('input', () => {
            const val = parseFloat(this.elSliderColorOffset.value);
            this.stateMgr.setColorOffset(val);
        });
        // Bookmarks
        document.getElementById('btnBookmarks')?.addEventListener('click', () => {
            this.elBookmarkPanel.classList.toggle('hidden');
            this.renderBookmarks();
        });
        document.getElementById('btnCloseBookmarks')?.addEventListener('click', () => {
            this.elBookmarkPanel.classList.add('hidden');
        });
        document.getElementById('btnSaveBookmark')?.addEventListener('click', () => {
            const name = this.elBookmarkName.value.trim() || 'Untitled';
            this.onSaveBookmark(name);
            this.elBookmarkName.value = '';
            this.renderBookmarks();
        });
        // Mousemove to show HUD
        document.addEventListener('mousemove', () => this.showHUD());
        document.addEventListener('mousedown', () => this.showHUD());
        document.addEventListener('touchstart', () => this.showHUD());
    }
    updateUI() {
        const state = this.stateMgr.get();
        const mag = this.stateMgr.getMagnification();
        // Magnification
        if (mag >= 1e6) {
            this.elMagnify.textContent = `×${(mag).toExponential(1)}`;
        }
        else if (mag >= 100) {
            this.elMagnify.textContent = `×${Math.round(mag)}`;
        }
        else {
            this.elMagnify.textContent = `×${mag.toFixed(1)}`;
        }
        // Mode
        this.elMode.textContent = state.setType === 'mandelbrot' ? 'Mandelbrot' : 'Julia Set';
        // Coordinates
        this.elCenterCoords.textContent = `Re: ${state.re.toExponential(6)}  Im: ${state.im.toExponential(6)}`;
        // Iterations
        this.elSliderIter.value = state.maxIterations.toString();
        this.elIterValue.textContent = state.maxIterations.toString();
        // Smooth checkbox
        this.elChkSmooth.checked = state.smoothColoring;
        // Auto iterations
        this.elChkAutoIter.checked = state.autoIterations;
        // Color offset
        this.elSliderColorOffset.value = state.colorOffset.toString();
        // Palette chips active state
        const chips = this.elPaletteBar.querySelectorAll('.palette-chip');
        chips.forEach((chip, idx) => {
            chip.classList.toggle('active', idx === state.paletteIndex);
        });
        // Check max mag warning
        if (mag > config_1.CONFIG.maxMagnification) {
            this.elMagnify.style.color = '#ff6b6b';
        }
        else {
            this.elMagnify.style.color = '';
        }
    }
    updateFPS() {
        const now = performance.now();
        this.fpsFrames++;
        if (now - this.fpsLastTime >= 500) {
            this.fpsValue = Math.round(this.fpsFrames / ((now - this.fpsLastTime) / 1000));
            this.elFPS.textContent = `${this.fpsValue.toString()} FPS`;
            this.fpsFrames = 0;
            this.fpsLastTime = now;
        }
    }
    showHUD() {
        const hud = document.getElementById('hud');
        if (hud) {
            hud.classList.remove('hidden');
            if (this.hudTimeout) {
                clearTimeout(this.hudTimeout);
            }
            this.hudTimeout = window.setTimeout(() => {
                hud.classList.add('hidden');
            }, 4000);
        }
    }
    renderBookmarks() {
        const bookmarks = this.getBookmarks();
        this.elBookmarkList.innerHTML = '';
        if (bookmarks.length === 0) {
            this.elBookmarkList.innerHTML = '<div class="emptyBookmarks">No bookmarks yet</div>';
            return;
        }
        bookmarks.forEach(bm => {
            const item = document.createElement('div');
            item.className = 'bookmarkItem';
            const nameSpan = document.createElement('span');
            nameSpan.className = 'bookmarkName';
            nameSpan.textContent = bm.name;
            item.appendChild(nameSpan);
            const loadBtn = document.createElement('button');
            loadBtn.textContent = 'Load';
            loadBtn.addEventListener('click', () => {
                this.onLoadBookmark(bm);
                this.elBookmarkPanel.classList.add('hidden');
            });
            item.appendChild(loadBtn);
            const delBtn = document.createElement('button');
            delBtn.textContent = '✕';
            delBtn.addEventListener('click', () => {
                this.onDeleteBookmark(bm.id);
                this.renderBookmarks();
            });
            item.appendChild(delBtn);
            this.elBookmarkList.appendChild(item);
        });
    }
    getBookmarks() {
        try {
            const data = localStorage.getItem('fractalBookmarks');
            return data ? JSON.parse(data) : [];
        }
        catch {
            return [];
        }
    }
    saveBookmarks(bookmarks) {
        try {
            localStorage.setItem('fractalBookmarks', JSON.stringify(bookmarks));
        }
        catch (e) {
            console.warn('Failed to save bookmarks:', e);
        }
    }
    saveBookmark(name) {
        const bookmarks = this.getBookmarks();
        const bookmark = {
            id: Date.now().toString(36) + Math.random().toString(36).substr(2, 5),
            name,
            state: JSON.parse(JSON.stringify(this.stateMgr.get())),
            createdAt: Date.now(),
        };
        bookmarks.push(bookmark);
        this.saveBookmarks(bookmarks);
    }
    loadBookmark(bookmark) {
        // Restore view state
        const s = bookmark.state;
        this.stateMgr.setView(s.re, s.im, s.scale, true);
        this.stateMgr.setSetType(s.setType);
        if (s.juliaC) {
            this.stateMgr.setJuliaC(s.juliaC.re, s.juliaC.im);
        }
        if (s.maxIterations !== undefined) {
            this.stateMgr.setMaxIterations(s.maxIterations);
        }
        this.stateMgr.setPaletteIndex(s.paletteIndex || 0);
        this.stateMgr.setColorOffset(s.colorOffset || 0);
        this.stateMgr.setSmoothColoring(s.smoothColoring ?? true);
        this.stateMgr.toggleAutoIterations(); // Reset auto to false when loading bookmark
        if (s.autoIterations) {
            this.stateMgr.toggleAutoIterations();
        }
        this.updateUI();
    }
    deleteBookmark(id) {
        const bookmarks = this.getBookmarks().filter(b => b.id !== id);
        this.saveBookmarks(bookmarks);
    }
}
exports.UIController = UIController;
};

// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"Qwen3.6-27B-Fable-Fusion-711-Uncensored-Heretic-NM-DAU-NEO-MAX-MTP-GGUF","provider":"brigid","persona":"typescript","sessionId":"0be93740-4d01-4e07-ba6e-c082e68c9a7d","tokensIn":1440501,"tokensOut":30906,"tokensTotal":1471407,"cost":0,"turns":47,"toolCalls":56,"failedToolCalls":2,"timestamp":"2026-08-06T23:56:38.343Z"} -->