← Fractal Explorer results

Fractal Explorer

thinkingcap qwen3.6 27b · 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 58,315 bytes · SHA-256 201dac06f686
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Fractal Explorer — Mandelbrot &amp; Julia Sets</title>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #000; font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; color: #fff; }

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

canvas { display: block; position: absolute; top: 0; left: 0; }
#fractal-canvas { z-index: 1; }
#zoom-overlay { z-index: 2; pointer-events: none; }

/* HUD */
#hud {
  position: absolute;
  top: 12px;
  left: 12px;
  z-index: 10;
  background: rgba(10, 10, 30, 0.7);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 12px;
  padding: 14px 18px;
  min-width: 260px;
  transition: opacity 0.4s ease, transform 0.4s ease;
  font-size: 13px;
  line-height: 1.5;
}

#hud.hidden {
  opacity: 0;
  pointer-events: none;
  transform: translateY(-8px);
}

#hud-top, #hud-center, #hud-bottom {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
}

#hud-center { margin: 4px 0; }

#mode-display { font-weight: 600; color: #8cf; text-transform: uppercase; letter-spacing: 0.5px; font-size: 11px; }
#mag-display { font-variant-numeric: tabular-nums; color: #adf; }
#center-display { font-family: 'SF Mono', 'Fira Code', monospace; font-size: 12px; color: #9ab; word-break: break-all; }
#iter-display, #fps-display { font-variant-numeric: tabular-nums; color: #8a9; font-size: 12px; }

/* Palette chips */
#palette-container {
  display: flex;
  gap: 6px;
  margin-top: 10px;
}

.palette-chip {
  width: 32px;
  height: 18px;
  border-radius: 4px;
  cursor: pointer;
  border: 2px solid transparent;
  transition: border-color 0.2s, transform 0.15s;
}

.palette-chip:hover { transform: scale(1.1); }
.palette-chip.active { border-color: #fff; box-shadow: 0 0 6px rgba(255,255,255,0.4); }

/* Controls */
#controls-row {
  display: flex;
  gap: 6px;
  margin-top: 10px;
}

#controls-row button {
  background: rgba(255, 255, 255, 0.1);
  border: 1px solid rgba(255, 255, 255, 0.2);
  color: #fff;
  padding: 4px 10px;
  border-radius: 6px;
  cursor: pointer;
  font-size: 12px;
  transition: background 0.2s;
}

#controls-row button:hover { background: rgba(255, 255, 255, 0.2); }

/* Bookmarks */
#bookmark-section { margin-top: 10px; border-top: 1px solid rgba(255,255,255,0.1); padding-top: 8px; }
#bookmark-name {
  background: rgba(255,255,255,0.08);
  border: 1px solid rgba(255,255,255,0.2);
  color: #fff;
  padding: 4px 8px;
  border-radius: 4px;
  font-size: 12px;
  width: 130px;
}
#btn-save-bookmark {
  background: rgba(100, 150, 255, 0.3);
  border: 1px solid rgba(100, 150, 255, 0.4);
  color: #fff;
  padding: 4px 8px;
  border-radius: 4px;
  cursor: pointer;
  font-size: 12px;
}
#bookmark-list { list-style: none; margin-top: 6px; }
.bookmark-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 3px 0;
  font-size: 12px;
}
.bookmark-name { cursor: pointer; color: #9cf; }
.bookmark-name:hover { text-decoration: underline; }
.bookmark-delete {
  background: none;
  border: none;
  color: #f66;
  cursor: pointer;
  font-size: 14px;
  padding: 0 4px;
}

/* Help overlay */
#help-overlay {
  position: absolute;
  bottom: 20px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 10;
  background: rgba(10, 10, 30, 0.75);
  backdrop-filter: blur(8px);
  border-radius: 10px;
  padding: 12px 24px;
  text-align: center;
  font-size: 13px;
  color: #abc;
  transition: opacity 1.5s ease;
}

#help-overlay p { margin: 2px 0; }
/* ── css: settings ── */
#settings-row {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-top: 8px;
  font-size: 12px;
}

#settings-row label {
  display: flex;
  align-items: center;
  gap: 4px;
  color: #9ab;
  cursor: pointer;
}

#toggle-cycling {
  accent-color: #6af;
  cursor: pointer;
}

#iter-slider {
  accent-color: #6af;
  cursor: pointer;
}

#iter-value {
  font-variant-numeric: tabular-nums;
  color: #8a9;
  min-width: 30px;
}

/* Progress bar */
#render-progress {
  margin-top: 6px;
  width: 100%;
  height: 3px;
  background: rgba(255,255,255,0.1);
  border-radius: 2px;
  overflow: hidden;
}

.progress-bar {
  width: 100%;
  height: 100%;
}

.progress-fill {
  height: 100%;
  background: linear-gradient(90deg, #4af, #6cf);
  border-radius: 2px;
  transition: width 0.1s ease;
  width: 0%;
}
</style>
</head>
<body>
<div id="app">
  <canvas id="fractal-canvas"></canvas>
  <canvas id="zoom-overlay"></canvas>
  
  <!-- HUD -->
  <div id="hud" class="hidden">
    <div id="hud-top">
      <span id="mode-display">Mandelbrot</span>
      <span id="mag-display">×1.0</span>
    </div>
    <div id="hud-center">
      <span id="center-display">-0.5, 0i</span>
    </div>
    <div id="hud-bottom">
      <span id="iter-display">iter: 100</span>
      <span id="fps-display">60 fps</span>
    </div>
    
    <!-- Palette selector -->
    <div id="palette-container"></div>
    
    <!-- Controls -->
    <div id="controls-row">
      <button id="btn-back" title="Back (Z)">← Back</button>
      <button id="btn-reset" title="Reset (R)">⟲ Reset</button>
      <button id="btn-julia" title="Toggle Julia (J)">Julia</button>
    </div><div id="settings-row">
  <label title="Toggle palette cycling animation">
    <input type="checkbox" id="toggle-cycling"> Cycle colors
  </label>
  <label title="Override iteration count">
    Iter: <input type="range" id="iter-slider" min="50" max="5000" step="10" value="auto" style="width:80px">
    <span id="iter-value"></span>
  </label>
</div>
<div id="render-progress" style="display:none">
  <div class="progress-bar"><div class="progress-fill" id="progress-fill"></div></div>
</div>
    
    <!-- Bookmarks -->
    <div id="bookmark-section">
      <input type="text" id="bookmark-name" placeholder="Bookmark name...">
      <button id="btn-save-bookmark">Save</button>
      <ul id="bookmark-list"></ul>
    </div>
  </div>
  
  <!-- Help overlay -->
  <div id="help-overlay">
    <p><strong>Fractal Explorer</strong></p>
    <p>Scroll to zoom · Drag to select region · Right-click drag to pan</p>
    <p>Arrow keys to pan · +/- to zoom · J for Julia mode · Z back · R reset</p>
  </div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./config":"src/config.ts","./renderer":"src/renderer.ts","./navigation":"src/navigation.ts","./ui":"src/ui.ts","./palette":"src/palette.ts","./bookmarks":"src/bookmarks.ts"},"src/math.ts":{"./config":"src/config.ts"},"src/renderer.ts":{"./config":"src/config.ts","./math":"src/math.ts","./palette":"src/palette.ts"},"src/navigation.ts":{"./config":"src/config.ts","./renderer":"src/renderer.ts"},"src/bookmarks.ts":{"./renderer":"src/renderer.ts","./palette":"src/palette.ts"},"src/ui.ts":{"./config":"src/config.ts","./renderer":"src/renderer.ts","./palette":"src/palette.ts","./bookmarks":"src/bookmarks.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";
// Entry module — wires up renderer, navigation, UI, and main loop
Object.defineProperty(exports, "__esModule", { value: true });
const config_1 = require("./config");
const renderer_1 = require("./renderer");
const navigation_1 = require("./navigation");
const ui_1 = require("./ui");
const palette_1 = require("./palette");
const bookmarks_1 = require("./bookmarks");
// ─── Canvas setup ──────────────────────────────────────────────────
const canvas = document.getElementById('fractal-canvas');
const overlay = document.getElementById('zoom-overlay');
function resizeCanvases() {
    const w = window.innerWidth;
    const h = window.innerHeight;
    canvas.width = w;
    canvas.height = h;
    overlay.width = w;
    overlay.height = h;
}
resizeCanvases();
window.addEventListener('resize', () => {
    resizeCanvases();
    scheduleRender();
});
// ─── Core objects ──────────────────────────────────────────────────
const renderer = new renderer_1.FractalRenderer(canvas);
const navigation = new navigation_1.Navigation(canvas, renderer.getView.bind(renderer), renderer.setView.bind(renderer));
const ui = new ui_1.FractalUI();
let currentPaletteName = 'fire';
renderer.setPaletteStops((0, palette_1.getPalette)(currentPaletteName));
// ─── Render scheduling ─────────────────────────────────────────────
let renderPending = false;
let renderInFlight = false;
function scheduleRender() {
    if (renderInFlight) {
        renderer.cancelRender();
    }
    renderPending = true;
}
async function executeRender() {
    renderPending = false;
    renderInFlight = true;
    await renderer.progressiveRender();
    renderInFlight = false;
    // If another render was scheduled during this one, do it now
    if (renderPending) {
        await executeRender();
    }
}
// ─── Palette cycling ──────────────────────────────────────────────
let paletteCyclingEnabled = false;
let lastCycleTime = performance.now();
let cycleAccumulator = 0;
function updatePaletteCycling() {
    if (!paletteCyclingEnabled)
        return;
    const now = performance.now();
    const dt = (now - lastCycleTime) / 1000;
    lastCycleTime = now;
    cycleAccumulator += config_1.CONFIG.paletteCycleSpeed * dt;
    // Re-render when enough offset has accumulated for visible change
    if (cycleAccumulator > 0.02) {
        const state = renderer.getRenderState();
        state.paletteOffset += cycleAccumulator;
        renderer.setPaletteOffset(state.paletteOffset);
        cycleAccumulator = 0;
        scheduleRender();
    }
}
// ─── Main loop ─────────────────────────────────────────────────────
function mainLoop() {
    updatePaletteCycling();
    // Draw zoom rectangle overlay
    const dragRect = navigation.getDragRect();
    if (dragRect) {
        ui.drawZoomRect(dragRect.x1, dragRect.y1, dragRect.x2, dragRect.y2);
    }
    else {
        ui.clearZoomRect();
    }
    // Update HUD
    const view = renderer.getView();
    const state = renderer.getRenderState();
    ui.updateHud(view, state.maxIter);
    if (renderPending && !renderInFlight) {
        executeRender();
    }
    requestAnimationFrame(mainLoop);
}
// ─── Navigation events ────────────────────────────────────────────
navigation.setCallbacks((event) => {
    switch (event.type) {
        case 'zoom':
        case 'pan':
            scheduleRender();
            ui.showHud();
            break;
        case 'juliaSelect':
            const v = renderer.getView();
            if (v.setMode === 'mandelbrot') {
                renderer.setView({
                    setMode: 'julia',
                    juliaCre: event.data.re,
                    juliaCim: event.data.im,
                });
                scheduleRender();
                ui.showHud();
            }
            break;
        case 'back':
        case 'reset':
            scheduleRender();
            ui.updateBookmarks();
            break;
    }
});
// ─── UI callbacks ─────────────────────────────────────────────────
ui.setCallbacks({
    onPaletteChange: (name) => {
        currentPaletteName = name;
        renderer.setPaletteStops((0, palette_1.getPalette)(name));
        scheduleRender();
    },
    onBookmarkLoad: (entry) => {
        renderer.setView(entry.view);
        if (entry.paletteName && palette_1.PALETTE_NAMES.includes(entry.paletteName)) {
            currentPaletteName = entry.paletteName;
            renderer.setPaletteStops((0, palette_1.getPalette)(entry.paletteName));
        }
        scheduleRender();
        ui.updateBookmarks();
    },
});
// ─── Button handlers ──────────────────────────────────────────────
document.getElementById('btn-back')?.addEventListener('click', () => {
    navigation.goBack();
    scheduleRender();
});
document.getElementById('btn-reset')?.addEventListener('click', () => {
    navigation.resetView();
    scheduleRender();
});
document.getElementById('btn-julia')?.addEventListener('click', () => {
    const v = renderer.getView();
    if (v.setMode === 'mandelbrot') {
        renderer.setView({ setMode: 'julia', juliaCre: v.centerRe, juliaCim: v.centerIm });
    }
    else {
        renderer.setView({ setMode: 'mandelbrot' });
    }
    scheduleRender();
});
// Bookmark save
const bookmarkNameInput = document.getElementById('bookmark-name');
document.getElementById('btn-save-bookmark')?.addEventListener('click', () => {
    const name = bookmarkNameInput.value.trim();
    if (!name)
        return;
    (0, bookmarks_1.saveBookmark)(name, renderer.getView(), currentPaletteName);
    bookmarkNameInput.value = '';
    ui.updateBookmarks();
});
// Palette cycling toggle
const cyclingToggle = document.getElementById('toggle-cycling');
if (cyclingToggle) {
    cyclingToggle.addEventListener('change', () => {
        paletteCyclingEnabled = cyclingToggle.checked;
        renderer.setCycling(paletteCyclingEnabled);
    });
}
// Iteration override slider
const iterSlider = document.getElementById('iter-slider');
const iterValueDisplay = document.getElementById('iter-value');
if (iterSlider) {
    // Initialize display with current value
    const updateIterDisplay = () => {
        const state = renderer.getRenderState();
        if (iterValueDisplay) {
            iterValueDisplay.textContent = state.maxIter.toString();
        }
    };
    updateIterDisplay();
    iterSlider.addEventListener('input', () => {
        const val = parseInt(iterSlider.value);
        if (!isNaN(val)) {
            renderer.getRenderState().maxIter = val;
            updateIterDisplay();
        }
    });
}
// Render progress bar
const renderProgressEl = document.getElementById('render-progress');
const progressFill = document.getElementById('progress-fill');
renderer.setCallbacks((fraction) => {
    if (renderProgressEl && progressFill) {
        renderProgressEl.style.display = 'block';
        progressFill.style.width = `${Math.round(fraction * 100)}%`;
    }
}, () => {
    if (renderProgressEl) {
        setTimeout(() => { renderProgressEl.style.display = 'none'; }, 300);
    }
});
// ─── URL hash state loading ───────────────────────────────────────
const hashState = (0, bookmarks_1.decodeStateFromHash)();
if (hashState) {
    renderer.setView(hashState.view);
    if (hashState.paletteName && palette_1.PALETTE_NAMES.includes(hashState.paletteName)) {
        currentPaletteName = hashState.paletteName;
        renderer.setPaletteStops((0, palette_1.getPalette)(currentPaletteName));
    }
}
// ─── Encode state to URL on view change ──────────────────────────
let urlUpdateTimer = null;
function scheduleUrlUpdate() {
    if (urlUpdateTimer)
        clearTimeout(urlUpdateTimer);
    urlUpdateTimer = setTimeout(() => {
        const v = renderer.getView();
        window.location.hash = (0, bookmarks_1.encodeStateToHash)(v, currentPaletteName);
    }, 500);
}
// ─── Debug interface on window ────────────────────────────────────
const fractalDebug = {
    iterationsAt: (re, im) => renderer.iterationsAt(re, im),
    setView: (params) => {
        renderer.setView(params);
        scheduleRender();
    },
    getView: () => renderer.getView(),
    getRenderState: () => renderer.getRenderState(),
};
window.fractal = fractalDebug;
// ─── Help overlay auto-hide ──────────────────────────────────────
const helpOverlay = document.getElementById('help-overlay');
setTimeout(() => {
    if (helpOverlay) {
        helpOverlay.style.opacity = '0';
        setTimeout(() => { if (helpOverlay)
            helpOverlay.remove(); }, 1500);
    }
}, 6000);
// ─── Start ────────────────────────────────────────────────────────
scheduleRender();
ui.updateBookmarks();
requestAnimationFrame(mainLoop);
};

// ── 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 for the fractal explorer
exports.CONFIG = {
    // Rendering
    maxIterationsBase: 100,
    maxIterationsScale: 50, // added per log2(magnification)
    maxIterationsCap: 10000,
    progressivePasses: 4, // number of refinement passes (1/4 → full)
    tileSize: 64, // worker tile size in pixels
    // Zoom & Navigation
    zoomSpeed: 1.5, // wheel zoom factor per tick
    dragZoomMinPixels: 10, // minimum drag distance to trigger zoom
    maxMagnification: 1e13, // precision limit for double-precision floats
    panStepFraction: 0.1, // keyboard pan as fraction of view
    // Julia preview
    juliaPreviewEnabled: true,
    juliaPreviewSize: 200, // pixels for the Julia preview pane
    // UI
    hudHideDelayMs: 3000,
    paletteCycleSpeed: 0.5, // offset per second when cycling enabled
    // Debug
    debugEnabled: false,
};
};

// ── module: src/math.ts ──
__mods["src/math.ts"] = function (exports, require, module) {
"use strict";
// Fractal iteration math — allocation-free core loop
Object.defineProperty(exports, "__esModule", { value: true });
exports.mandelbrotIteration = mandelbrotIteration;
exports.juliaIteration = juliaIteration;
exports.computeMaxIterations = computeMaxIterations;
/**
 * Compute the smooth (continuous) iteration count for a point.
 * Returns maxIter if the point is in the set, otherwise the fractional iteration count.
 * Smooth coloring formula: n + 1 - log2(log|z| / log(2))
 */
function mandelbrotIteration(cre, cim, maxIter) {
    let zre = 0;
    let zim = 0;
    let zre2 = 0;
    let zim2 = 0;
    let i = 0;
    while (zre2 + zim2 <= 4 && i < maxIter) {
        zim = 2 * zre * zim + cim;
        zre = zre2 - zim2 + cre;
        zre2 = zre * zre;
        zim2 = zim * zim;
        i++;
    }
    if (i === maxIter)
        return maxIter; // interior point
    // Smooth iteration count: n + 1 - log2(log|z| / log(2))
    const log_zn = Math.log(zre2 + zim2);
    const nu = Math.log(log_zn / Math.LN2) / Math.LN2;
    return i + 1 - nu;
}
/**
 * Julia set iteration for a given c-value.
 */
function juliaIteration(x0, y0, cre, cim, maxIter) {
    let zre = x0;
    let zim = y0;
    let zre2 = zre * zre;
    let zim2 = zim * zim;
    let i = 0;
    while (zre2 + zim2 <= 4 && i < maxIter) {
        zim = 2 * zre * zim + cim;
        zre = zre2 - zim2 + cre;
        zre2 = zre * zre;
        zim2 = zim * zim;
        i++;
    }
    if (i === maxIter)
        return maxIter;
    const log_zn = Math.log(zre2 + zim2);
    const nu = Math.log(log_zn / Math.LN2) / Math.LN2;
    return i + 1 - nu;
}
/**
 * Compute the effective iteration count based on magnification.
 */
function computeMaxIterations(magnification) {
    const logMag = Math.log2(Math.max(1, magnification));
    const iterations = config_1.CONFIG.maxIterationsBase + config_1.CONFIG.maxIterationsScale * logMag;
    return Math.floor(Math.min(iterations, config_1.CONFIG.maxIterationsCap));
}
// Re-export config reference for iteration computation
const config_1 = require("./config");
};

// ── module: src/palette.ts ──
__mods["src/palette.ts"] = function (exports, require, module) {
"use strict";
// Color palettes with smooth interpolation between stops
Object.defineProperty(exports, "__esModule", { value: true });
exports.PALETTE_NAMES = void 0;
exports.getPalette = getPalette;
exports.sampleColor = sampleColor;
exports.colorIteration = colorIteration;
/** Named palette definitions — each is an array of [t, r, g, b] stops */
const PALETTE_DATA = {
    fire: [
        [0.0, 0, 0, 0],
        [0.15, 128, 0, 0],
        [0.3, 255, 80, 0],
        [0.45, 255, 200, 0],
        [0.6, 255, 255, 100],
        [0.75, 255, 255, 255],
        [1.0, 200, 200, 255],
    ],
    ocean: [
        [0.0, 0, 0, 30],
        [0.2, 0, 20, 80],
        [0.4, 0, 60, 160],
        [0.55, 0, 120, 200],
        [0.7, 30, 180, 220],
        [0.85, 100, 220, 240],
        [1.0, 200, 255, 255],
    ],
    monochrome: [
        [0.0, 0, 0, 0],
        [0.3, 60, 60, 60],
        [0.6, 140, 140, 140],
        [0.85, 200, 200, 200],
        [1.0, 255, 255, 255],
    ],
    psychedelic: [
        [0.0, 128, 0, 128],
        [0.167, 128, 128, 0],
        [0.333, 0, 128, 128],
        [0.5, 128, 0, 0],
        [0.667, 0, 128, 0],
        [0.833, 0, 0, 128],
        [1.0, 128, 0, 128],
    ],
    electric: [
        [0.0, 5, 0, 20],
        [0.2, 30, 0, 100],
        [0.4, 60, 0, 200],
        [0.55, 100, 50, 255],
        [0.7, 180, 100, 255],
        [0.85, 230, 180, 255],
        [1.0, 255, 255, 255],
    ],
};
const PALETTES = {};
for (const [name, data] of Object.entries(PALETTE_DATA)) {
    PALETTES[name] = data.map(([t, r, g, b]) => ({ t, r, g, b }));
}
exports.PALETTE_NAMES = Object.keys(PALETTES);
/** Get palette stops by name */
function getPalette(name) {
    return PALETTES[name] || PALETTES.fire;
}
/**
 * Sample a color from the palette at position t ∈ [0, 1].
 * Smoothly interpolates between stops.
 */
function sampleColor(stops, t) {
    // Clamp and wrap for cyclic palettes
    t = ((t % 1) + 1) % 1;
    if (t <= stops[0].t)
        return [stops[0].r, stops[0].g, stops[0].b];
    if (t >= stops[stops.length - 1].t) {
        const last = stops[stops.length - 1];
        return [last.r, last.g, last.b];
    }
    // Find surrounding stops
    let lo = 0;
    for (let i = 1; i < stops.length; i++) {
        if (stops[i].t > t)
            break;
        lo = i;
    }
    const hi = lo + 1;
    const s0 = stops[lo];
    const s1 = stops[hi];
    // Linear interpolation between stops
    const frac = (t - s0.t) / (s1.t - s0.t);
    return [
        Math.round(s0.r + frac * (s1.r - s0.r)),
        Math.round(s0.g + frac * (s1.g - s0.g)),
        Math.round(s0.b + frac * (s1.b - s0.b)),
    ];
}
/**
 * Colorize a single iteration value into RGB.
 * maxIter is the threshold for interior points (rendered black).
 */
function colorIteration(iter, maxIter, stops, offset = 0) {
    if (iter >= maxIter)
        return [0, 0, 0]; // interior → black
    const t = ((iter / maxIter) + offset) % 1;
    return sampleColor(stops, t);
}
};

// ── module: src/renderer.ts ──
__mods["src/renderer.ts"] = function (exports, require, module) {
"use strict";
// Rendering engine — two-phase pipeline: workers compute iterations, main thread colorizes
Object.defineProperty(exports, "__esModule", { value: true });
exports.FractalRenderer = void 0;
const config_1 = require("./config");
const math_1 = require("./math");
const palette_1 = require("./palette");
// ─── Worker code (inline via Blob URL) — computes iteration counts only ──
const WORKER_CODE = `
self.onmessage = function(e) {
  const d = e.data;
  const { tileX, tileY, tileW, tileH, width, height, centerRe, centerIm, scale, maxIter, setMode, juliaCre, juliaCim } = d;
  
  const halfW = width / (2 * scale);
  const halfH = height / (2 * scale);
  const reMin = centerRe - halfW;
  const imMax = centerIm + halfH;
  
  // Output: Float32Array of iteration counts
  const buf = new Float32Array(tileW * tileH);
  
  for (let ty = 0; ty < tileH; ty++) {
    for (let tx = 0; tx < tileW; tx++) {
      const px = tileX + tx;
      const py = tileY + ty;
      
      const cre = reMin + (px / width) * (2 * halfW);
      const cim = imMax - (py / height) * (2 * halfH);
      
      let iter;
      if (setMode === 'julia') {
        iter = juliaIteration(cre, cim, juliaCre, juliaCim, maxIter);
      } else {
        iter = mandelbrotIteration(cre, cim, maxIter);
      }
      
      buf[ty * tileW + tx] = iter;
    }
  }
  
  self.postMessage({ tileX, tileY, tileW, tileH, buf }, [buf.buffer]);
};

function mandelbrotIteration(cre, cim, maxIter) {
  let zre = 0, zim = 0, zre2 = 0, zim2 = 0, i = 0;
  while (zre2 + zim2 <= 4 && i < maxIter) {
    zim = 2 * zre * zim + cim;
    zre = zre2 - zim2 + cre;
    zre2 = zre * zre;
    zim2 = zim * zim;
    i++;
  }
  if (i === maxIter) return maxIter;
  const log_zn = Math.log(zre2 + zim2);
  const nu = Math.log(log_zn / Math.LN2) / Math.LN2;
  return i + 1 - nu;
}

function juliaIteration(x0, y0, cre, cim, maxIter) {
  let zre = x0, zim = y0, zre2 = x0*x0, zim2 = y0*y0, i = 0;
  while (zre2 + zim2 <= 4 && i < maxIter) {
    zim = 2 * zre * zim + cim;
    zre = zre2 - zim2 + cre;
    zre2 = zre * zre;
    zim2 = zim * zim;
    i++;
  }
  if (i === maxIter) return maxIter;
  const log_zn = Math.log(zre2 + zim2);
  const nu = Math.log(log_zn / Math.LN2) / Math.LN2;
  return i + 1 - nu;
}
`;
let workerBlobUrl = null;
function createWorker() {
    if (!workerBlobUrl) {
        const blob = new Blob([WORKER_CODE], { type: 'application/javascript' });
        workerBlobUrl = URL.createObjectURL(blob);
    }
    return new Worker(workerBlobUrl);
}
// ─── Main renderer ──────────────────────────────────────────────────
class FractalRenderer {
    constructor(canvas) {
        this.imageData = null;
        this.pixelBuffer32 = null;
        // Iteration buffer — stores smooth iteration counts per pixel
        this.iterBuffer = null;
        // Cancellation
        this.cancelled = false;
        this.canvas = canvas;
        this.ctx = canvas.getContext('2d', { willReadFrequently: false });
        this.view = {
            centerRe: -0.5,
            centerIm: 0,
            scale: 150,
            setMode: 'mandelbrot',
            juliaCre: -0.7269,
            juliaCim: 0.1889,
        };
        this.renderState = {
            paletteStops: [],
            paletteOffset: 0,
            maxIter: config_1.CONFIG.maxIterationsBase,
            cyclingEnabled: false,
        };
    }
    setView(view) {
        if (view.re !== undefined)
            this.view.centerRe = view.re;
        if (view.im !== undefined)
            this.view.centerIm = view.im;
        if (view.centerRe !== undefined)
            this.view.centerRe = view.centerRe;
        if (view.centerIm !== undefined)
            this.view.centerIm = view.centerIm;
        if (view.scale !== undefined)
            this.view.scale = view.scale;
        if (view.setMode !== undefined)
            this.view.setMode = view.setMode;
        if (view.juliaCre !== undefined)
            this.view.juliaCre = view.juliaCre;
        if (view.juliaCim !== undefined)
            this.view.juliaCim = view.juliaCim;
        this.updateMaxIterations();
    }
    getView() { return { ...this.view }; }
    getRenderState() { return { ...this.renderState }; }
    setPaletteStops(stops) { this.renderState.paletteStops = stops; }
    setPaletteOffset(offset) { this.renderState.paletteOffset = offset; }
    setCycling(enabled) { this.renderState.cyclingEnabled = enabled; }
    updateMaxIterations() {
        const mag = this.view.scale / 150;
        this.renderState.maxIter = (0, math_1.computeMaxIterations)(mag);
    }
    cancelRender() { this.cancelled = true; }
    /** Progressive render: coarse → fine passes */
    progressiveRender() {
        return new Promise(async (resolve) => {
            this.cancelled = false;
            const passes = config_1.CONFIG.progressivePasses; // e.g. 4
            for (let pass = 0; pass < passes; pass++) {
                if (this.cancelled) {
                    resolve();
                    return;
                }
                const resFactor = Math.pow(2, passes - 1 - pass);
                await this.renderPass(resFactor);
            }
            // Final colorize at full resolution
            this.colorize();
            this.onComplete?.();
            resolve();
        });
    }
    /** Colorize-only pass — applies current palette to iteration buffer */
    colorize() {
        if (!this.iterBuffer || !this.pixelBuffer32)
            return;
        const W = this.canvas.width;
        const H = this.canvas.height;
        const stops = this.renderState.paletteStops;
        const maxIter = this.renderState.maxIter;
        const offset = this.renderState.paletteOffset;
        for (let i = 0; i < W * H; i++) {
            const iter = this.iterBuffer[i];
            if (iter >= maxIter) {
                // Interior point → black
                this.pixelBuffer32[i] = 0xFF000000;
            }
            else {
                const t = ((iter / maxIter) + offset) % 1;
                const [r, g, b] = (0, palette_1.sampleColor)(stops, t);
                // ABGR little-endian
                this.pixelBuffer32[i] = (255 << 24) | (b << 16) | (g << 8) | r;
            }
        }
        if (this.imageData) {
            this.ctx.putImageData(this.imageData, 0, 0);
        }
    }
    renderPass(resolutionFactor) {
        return new Promise((resolve) => {
            const W = this.canvas.width;
            const H = this.canvas.height;
            // Ensure buffers exist at full resolution
            if (!this.imageData || this.imageData.width !== W) {
                this.imageData = this.ctx.createImageData(W, H);
                this.pixelBuffer32 = new Uint32Array(this.imageData.data.buffer);
                this.iterBuffer = new Float32Array(W * H);
            }
            // Tile grid at full resolution
            const tileSize = config_1.CONFIG.tileSize;
            const cols = Math.ceil(W / tileSize);
            const rows = Math.ceil(H / tileSize);
            const totalTiles = cols * rows;
            let completed = 0;
            let activeWorkers = 0;
            const maxConcurrent = Math.min(4, navigator.hardwareConcurrency || 2);
            const queue = [];
            for (let r = 0; r < rows; r++) {
                for (let c = 0; c < cols; c++) {
                    queue.push({ col: c, row: r });
                }
            }
            let queueIdx = 0;
            const dispatchNext = () => {
                while (activeWorkers < maxConcurrent && queueIdx < queue.length) {
                    if (this.cancelled)
                        break;
                    const task = queue[queueIdx++];
                    activeWorkers++;
                    this.renderTile(task.col * tileSize, task.row * tileSize, resolutionFactor, () => {
                        if (this.cancelled) {
                            resolve();
                            return;
                        }
                        completed++;
                        activeWorkers--;
                        // Colorize and blit for progressive visual feedback
                        this.colorize();
                        this.onProgress?.(completed / totalTiles);
                        dispatchNext();
                        if (completed >= queueIdx && activeWorkers === 0) {
                            resolve();
                        }
                    });
                }
                if (queueIdx >= queue.length && activeWorkers === 0 && !this.cancelled) {
                    resolve();
                }
            };
            dispatchNext();
        });
    }
    renderTile(tx, ty, resolutionFactor, onComplete) {
        const W = this.canvas.width;
        const H = this.canvas.height;
        // Tile dimensions at full resolution
        const tw = Math.min(config_1.CONFIG.tileSize, W - tx);
        const th = Math.min(config_1.CONFIG.tileSize, H - ty);
        // Effective tile dimensions for worker (coarse pass)
        const effTw = Math.ceil(tw / resolutionFactor);
        const effTh = Math.ceil(th / resolutionFactor);
        const effTx = Math.floor(tx / resolutionFactor);
        const effTy = Math.floor(ty / resolutionFactor);
        // Effective canvas dimensions for worker
        const effW = Math.ceil(W / resolutionFactor);
        const effH = Math.ceil(H / resolutionFactor);
        const worker = createWorker();
        worker.onmessage = (e) => {
            if (!this.iterBuffer) {
                onComplete();
                return;
            }
            const resultData = new Float32Array(e.data.buf);
            // Write iteration data into buffer, scaling up if needed
            for (let sy = 0; sy < effTh; sy++) {
                for (let sx = 0; sx < effTw; sx++) {
                    const srcVal = resultData[sy * effTw + sx];
                    // Map source pixel to destination region
                    const dstXStart = tx + sx * resolutionFactor;
                    const dstYStart = ty + sy * resolutionFactor;
                    for (let dy = 0; dy < resolutionFactor && (dstYStart + dy) < H; dy++) {
                        for (let dx = 0; dx < resolutionFactor && (dstXStart + dx) < W; dx++) {
                            const dstIdx = (dstYStart + dy) * W + (dstXStart + dx);
                            if (dstIdx < this.iterBuffer.length) {
                                this.iterBuffer[dstIdx] = srcVal;
                            }
                        }
                    }
                }
            }
            onComplete();
        };
        worker.onerror = () => { onComplete(); };
        if (this.cancelled)
            return;
        worker.postMessage({
            tileX: effTx, tileY: effTy, tileW: effTw, tileH: effTh,
            width: effW, height: effH,
            centerRe: this.view.centerRe,
            centerIm: this.view.centerIm,
            scale: resolutionFactor > 1 ? this.view.scale / resolutionFactor : this.view.scale,
            maxIter: this.renderState.maxIter,
            setMode: this.view.setMode,
            juliaCre: this.view.juliaCre,
            juliaCim: this.view.juliaCim,
        });
    }
    /** Compute iteration count at a specific point (for debug) */
    iterationsAt(re, im) {
        const maxIter = this.renderState.maxIter;
        if (this.view.setMode === 'julia') {
            return (0, math_1.juliaIteration)(re, im, this.view.juliaCre, this.view.juliaCim, maxIter);
        }
        return (0, math_1.mandelbrotIteration)(re, im, maxIter);
    }
    setCallbacks(onProgress, onComplete) {
        this.onProgress = onProgress;
        this.onComplete = onComplete;
    }
    destroy() {
        this.cancelRender();
        if (workerBlobUrl) {
            URL.revokeObjectURL(workerBlobUrl);
            workerBlobUrl = null;
        }
    }
}
exports.FractalRenderer = FractalRenderer;
};

// ── module: src/navigation.ts ──
__mods["src/navigation.ts"] = function (exports, require, module) {
"use strict";
// Navigation — mouse/touch input for zoom, pan, drag-select rectangle
Object.defineProperty(exports, "__esModule", { value: true });
exports.Navigation = void 0;
const config_1 = require("./config");
class Navigation {
    constructor(canvas, getView, setView) {
        // Mouse state
        this.isDragging = false;
        this.dragStartX = 0;
        this.dragStartY = 0;
        this.dragCurrentX = 0;
        this.dragCurrentY = 0;
        this.isPanning = false;
        this.panStartX = 0;
        this.panStartY = 0;
        // Zoom history
        this.history = [];
        this.maxHistory = 50;
        this.canvas = canvas;
        this.getView = getView;
        this.setView = setView;
        this.setupMouseEvents();
        this.setupKeyboardEvents();
        this.pushHistory();
    }
    pushHistory() {
        const view = this.getView();
        // Avoid duplicate consecutive entries
        if (this.history.length > 0) {
            const last = this.history[this.history.length - 1];
            if (Math.abs(last.centerRe - view.centerRe) < 1e-15 &&
                Math.abs(last.centerIm - view.centerIm) < 1e-15 &&
                Math.abs(last.scale - view.scale) < 0.001) {
                return;
            }
        }
        this.history.push({ ...view });
        if (this.history.length > this.maxHistory)
            this.history.shift();
    }
    goBack() {
        if (this.history.length <= 1)
            return;
        this.history.pop(); // remove current state
        const prev = this.history[this.history.length - 1];
        this.setView(prev);
        this.onNavEvent?.({ type: 'back' });
    }
    resetView() {
        this.setView({
            centerRe: -0.5,
            centerIm: 0,
            scale: 150,
            setMode: 'mandelbrot',
        });
        this.history = [];
        this.pushHistory();
        this.onNavEvent?.({ type: 'reset' });
    }
    screenToComplex(sx, sy) {
        const view = this.getView();
        const halfW = this.canvas.width / (2 * view.scale);
        const halfH = this.canvas.height / (2 * view.scale);
        return {
            re: view.centerRe + (sx / this.canvas.width - 0.5) * 2 * halfW,
            im: view.centerIm - (sy / this.canvas.height - 0.5) * 2 * halfH,
        };
    }
    setupMouseEvents() {
        // Wheel zoom toward cursor
        this.canvas.addEventListener('wheel', (e) => {
            e.preventDefault();
            const view = this.getView();
            const mouseComplex = this.screenToComplex(e.offsetX, e.offsetY);
            // Zoom factor: scroll up = zoom in, down = zoom out
            const direction = e.deltaY > 0 ? -1 : 1;
            const zoomFactor = Math.pow(config_1.CONFIG.zoomSpeed, direction * 0.3);
            const newScale = view.scale * zoomFactor;
            // Enforce max magnification
            const mag = newScale / 150;
            if (mag > config_1.CONFIG.maxMagnification)
                return;
            // Adjust center so the mouse point stays fixed
            const halfW = this.canvas.width / (2 * newScale);
            const halfH = this.canvas.height / (2 * newScale);
            this.setView({
                scale: newScale,
                centerRe: mouseComplex.re - (e.offsetX / this.canvas.width - 0.5) * 2 * halfW,
                centerIm: mouseComplex.im + (e.offsetY / this.canvas.height - 0.5) * 2 * halfH,
            });
            this.pushHistory();
            this.onNavEvent?.({ type: 'zoom' });
        }, { passive: false });
        // Click-drag for rectangle zoom or pan
        let mouseDownTime = 0;
        this.canvas.addEventListener('mousedown', (e) => {
            if (e.button !== 0)
                return; // only left click
            mouseDownTime = Date.now();
            this.isDragging = true;
            this.dragStartX = e.offsetX;
            this.dragStartY = e.offsetY;
            this.dragCurrentX = e.offsetX;
            this.dragCurrentY = e.offsetY;
        });
        window.addEventListener('mousemove', (e) => {
            if (!this.isDragging)
                return;
            const rect = this.canvas.getBoundingClientRect();
            this.dragCurrentX = Math.max(0, Math.min(this.canvas.width, e.clientX - rect.left));
            this.dragCurrentY = Math.max(0, Math.min(this.canvas.height, e.clientY - rect.top));
        });
        window.addEventListener('mouseup', (e) => {
            if (!this.isDragging)
                return;
            const elapsed = Date.now() - mouseDownTime;
            const dx = this.dragCurrentX - this.dragStartX;
            const dy = this.dragCurrentY - this.dragStartY;
            const dist = Math.sqrt(dx * dx + dy * dy);
            if (dist >= config_1.CONFIG.dragZoomMinPixels) {
                // Rectangle zoom
                const x1 = Math.min(this.dragStartX, this.dragCurrentX);
                const y1 = Math.min(this.dragStartY, this.dragCurrentY);
                const x2 = Math.max(this.dragStartX, this.dragCurrentX);
                const y2 = Math.max(this.dragStartY, this.dragCurrentY);
                // Map rectangle corners to complex plane
                const topLeft = this.screenToComplex(x1, y1);
                const bottomRight = this.screenToComplex(x2, y2);
                const newCenterRe = (topLeft.re + bottomRight.re) / 2;
                const newCenterIm = (topLeft.im + bottomRight.im) / 2;
                // New scale: fit the rectangle to full canvas
                const rectW = x2 - x1;
                const rectH = y2 - y1;
                const complexW = Math.abs(topLeft.re - bottomRight.re);
                const complexH = Math.abs(topLeft.im - bottomRight.im);
                // Use the dimension that requires more zoom (to fill the canvas)
                const scaleX = this.canvas.width / complexW;
                const scaleY = this.canvas.height / complexH;
                let newScale = Math.min(scaleX, scaleY);
                // Enforce max magnification
                if (newScale / 150 > config_1.CONFIG.maxMagnification) {
                    newScale = config_1.CONFIG.maxMagnification * 150;
                }
                this.setView({ centerRe: newCenterRe, centerIm: newCenterIm, scale: newScale });
                this.pushHistory();
                this.onNavEvent?.({ type: 'zoom' });
            }
            else if (elapsed < 300) {
                // Short click = Julia select point
                const clicked = this.screenToComplex(e.offsetX, e.offsetY);
                this.onNavEvent?.({
                    type: 'juliaSelect',
                    data: { re: clicked.re, im: clicked.im }
                });
            }
            this.isDragging = false;
        });
        // Right-click or middle-click for panning
        this.canvas.addEventListener('mousedown', (e) => {
            if (e.button === 1 || e.button === 2) {
                e.preventDefault();
                this.isPanning = true;
                this.panStartX = e.offsetX;
                this.panStartY = e.offsetY;
            }
        });
        window.addEventListener('mousemove', (e) => {
            if (!this.isPanning)
                return;
            const dx = e.offsetX - this.panStartX;
            const dy = e.offsetY - this.panStartY;
            const view = this.getView();
            const halfW = this.canvas.width / (2 * view.scale);
            const halfH = this.canvas.height / (2 * view.scale);
            this.setView({
                centerRe: view.centerRe - (dx / this.canvas.width) * 2 * halfW,
                centerIm: view.centerIm + (dy / this.canvas.height) * 2 * halfH,
            });
            this.panStartX = e.offsetX;
            this.panStartY = e.offsetY;
        });
        window.addEventListener('mouseup', (e) => {
            if (this.isPanning && (e.button === 1 || e.button === 2)) {
                this.isPanning = false;
                this.pushHistory();
                this.onNavEvent?.({ type: 'pan' });
            }
        });
        // Prevent context menu on canvas
        this.canvas.addEventListener('contextmenu', (e) => e.preventDefault());
    }
    setupKeyboardEvents() {
        window.addEventListener('keydown', (e) => {
            const view = this.getView();
            const halfW = this.canvas.width / (2 * view.scale);
            const halfH = this.canvas.height / (2 * view.scale);
            const panStep = config_1.CONFIG.panStepFraction;
            switch (e.key) {
                case 'ArrowLeft':
                    e.preventDefault();
                    this.setView({ centerRe: view.centerRe - 2 * halfW * panStep });
                    break;
                case 'ArrowRight':
                    e.preventDefault();
                    this.setView({ centerRe: view.centerRe + 2 * halfW * panStep });
                    break;
                case 'ArrowUp':
                    e.preventDefault();
                    this.setView({ centerIm: view.centerIm + 2 * halfH * panStep });
                    break;
                case 'ArrowDown':
                    e.preventDefault();
                    this.setView({ centerIm: view.centerIm - 2 * halfH * panStep });
                    break;
                case '+':
                case '=':
                    e.preventDefault();
                    {
                        const newScale = Math.min(view.scale * config_1.CONFIG.zoomSpeed, config_1.CONFIG.maxMagnification * 150);
                        this.setView({ scale: newScale });
                    }
                    break;
                case '-':
                case '_':
                    e.preventDefault();
                    this.setView({ scale: view.scale / config_1.CONFIG.zoomSpeed });
                    break;
                case 'z':
                case 'Z':
                    e.preventDefault();
                    this.goBack();
                    return;
                case 'r':
                case 'R':
                    e.preventDefault();
                    this.resetView();
                    return;
                case 'j':
                case 'J':
                    e.preventDefault();
                    // Toggle Julia mode with current center as c-value
                    if (view.setMode === 'mandelbrot') {
                        this.setView({ setMode: 'julia', juliaCre: view.centerRe, juliaCim: view.centerIm });
                    }
                    else {
                        this.setView({ setMode: 'mandelbrot' });
                    }
                    break;
            }
            this.pushHistory();
            this.onNavEvent?.({ type: e.key.includes('+') || e.key === '=' || e.key === '-' ? 'zoom' : 'pan' });
        });
    }
    getDragRect() {
        if (!this.isDragging)
            return null;
        const x1 = Math.min(this.dragStartX, this.dragCurrentX);
        const y1 = Math.min(this.dragStartY, this.dragCurrentY);
        const x2 = Math.max(this.dragStartX, this.dragCurrentX);
        const y2 = Math.max(this.dragStartY, this.dragCurrentY);
        return { x1, y1, x2, y2 };
    }
    setCallbacks(onNavEvent) {
        this.onNavEvent = onNavEvent;
    }
    getHistory() {
        return [...this.history];
    }
}
exports.Navigation = Navigation;
};

// ── module: src/bookmarks.ts ──
__mods["src/bookmarks.ts"] = function (exports, require, module) {
"use strict";
// Bookmarks — save/load views to localStorage
Object.defineProperty(exports, "__esModule", { value: true });
exports.saveBookmark = saveBookmark;
exports.loadBookmark = loadBookmark;
exports.deleteBookmark = deleteBookmark;
exports.listBookmarks = listBookmarks;
exports.encodeStateToHash = encodeStateToHash;
exports.decodeStateFromHash = decodeStateFromHash;
const STORAGE_KEY = 'fractal-explorer-bookmarks';
function saveBookmark(name, view, paletteName) {
    const bookmarks = loadAllBookmarks();
    // Update existing or add new
    const idx = bookmarks.findIndex(b => b.name === name);
    const entry = {
        name,
        timestamp: Date.now(),
        view: { ...view },
        paletteName,
    };
    if (idx >= 0) {
        bookmarks[idx] = entry;
    }
    else {
        bookmarks.push(entry);
    }
    try {
        localStorage.setItem(STORAGE_KEY, JSON.stringify(bookmarks));
    }
    catch (e) {
        console.warn('Failed to save bookmark:', e);
    }
}
function loadBookmark(name) {
    const bookmarks = loadAllBookmarks();
    return bookmarks.find(b => b.name === name) || null;
}
function deleteBookmark(name) {
    const bookmarks = loadAllBookmarks().filter(b => b.name !== name);
    try {
        localStorage.setItem(STORAGE_KEY, JSON.stringify(bookmarks));
    }
    catch (e) {
        console.warn('Failed to delete bookmark:', e);
    }
}
function listBookmarks() {
    return loadAllBookmarks();
}
function loadAllBookmarks() {
    try {
        const data = localStorage.getItem(STORAGE_KEY);
        if (!data)
            return [];
        return JSON.parse(data);
    }
    catch (e) {
        console.warn('Failed to load bookmarks:', e);
        return [];
    }
}
// ─── URL hash state ────────────────────────────────────────────────
function encodeStateToHash(view, paletteName) {
    const parts = [
        `cr=${view.centerRe.toPrecision(15)}`,
        `ci=${view.centerIm.toPrecision(15)}`,
        `sc=${view.scale.toPrecision(15)}`,
        `md=${view.setMode}`,
        `jcr=${view.juliaCre.toPrecision(15)}`,
        `jci=${view.juliaCim.toPrecision(15)}`,
        `pl=${paletteName}`,
    ];
    return '#' + parts.join('&');
}
function decodeStateFromHash() {
    const hash = window.location.hash.slice(1); // remove #
    if (!hash)
        return null;
    const params = new URLSearchParams(hash);
    return {
        view: {
            centerRe: parseFloat(params.get('cr') || ''),
            centerIm: parseFloat(params.get('ci') || ''),
            scale: parseFloat(params.get('sc') || ''),
            setMode: params.get('md') || 'mandelbrot',
            juliaCre: parseFloat(params.get('jcr') || ''),
            juliaCim: parseFloat(params.get('jci') || ''),
        },
        paletteName: params.get('pl') || 'fire',
    };
}
};

// ── module: src/ui.ts ──
__mods["src/ui.ts"] = function (exports, require, module) {
"use strict";
// UI — glassy HUD, palette selector, zoom overlay, Julia preview
Object.defineProperty(exports, "__esModule", { value: true });
exports.FractalUI = void 0;
const config_1 = require("./config");
const palette_1 = require("./palette");
const bookmarks_1 = require("./bookmarks");
class FractalUI {
    constructor() {
        this.juliaPreviewCanvas = null;
        this.hideTimeout = null;
        this.isHudVisible = true;
        // State tracking for HUD updates
        this.frameCount = 0;
        this.lastFpsTime = performance.now();
        this.currentFps = 60;
        this.hudEl = document.getElementById('hud');
        this.magEl = document.getElementById('mag-display');
        this.centerEl = document.getElementById('center-display');
        this.iterEl = document.getElementById('iter-display');
        this.fpsEl = document.getElementById('fps-display');
        this.paletteContainer = document.getElementById('palette-container');
        this.zoomOverlay = document.getElementById('zoom-overlay');
        this.bookmarkList = document.getElementById('bookmark-list');
        // Setup palette swatches
        this.setupPaletteSwatches();
        // Auto-hide HUD on idle
        this.resetHideTimer();
    }
    setupPaletteSwatches() {
        for (const name of palette_1.PALETTE_NAMES) {
            const chip = document.createElement('div');
            chip.className = 'palette-chip';
            chip.dataset.palette = name;
            // Create gradient preview from palette stops
            const stops = (0, palette_1.getPalette)(name);
            const gradientParts = [];
            for (const stop of stops) {
                const pct = Math.round(stop.t * 100);
                gradientParts.push(`rgb(${stop.r},${stop.g},${stop.b}) ${pct}%`);
            }
            chip.style.background = `linear-gradient(90deg, ${gradientParts.join(', ')})`;
            chip.addEventListener('click', () => {
                this.onPaletteChange?.(name);
                // Mark active
                document.querySelectorAll('.palette-chip').forEach(c => c.classList.remove('active'));
                chip.classList.add('active');
            });
            this.paletteContainer.appendChild(chip);
        }
        // Activate first palette
        const firstChip = this.paletteContainer.querySelector('.palette-chip');
        if (firstChip)
            firstChip.classList.add('active');
    }
    updateHud(view, maxIter) {
        const mag = view.scale / 150;
        // Magnification display
        if (mag >= 1e6) {
            this.magEl.textContent = `×${mag.toExponential(2).replace('e+', 'e')}`;
        }
        else if (mag >= 1000) {
            this.magEl.textContent = `×${Math.round(mag).toLocaleString()}`;
        }
        else {
            this.magEl.textContent = `×${mag.toFixed(1)}`;
        }
        // Center coordinates
        this.centerEl.textContent = `${view.centerRe.toPrecision(8)}, ${view.centerIm.toPrecision(8)}i`;
        // Iterations
        this.iterEl.textContent = `iter: ${maxIter}`;
        // FPS (computed every second)
        this.frameCount++;
        const now = performance.now();
        if (now - this.lastFpsTime >= 1000) {
            this.currentFps = Math.round(this.frameCount * 1000 / (now - this.lastFpsTime));
            this.fpsEl.textContent = `${this.currentFps} fps`;
            this.frameCount = 0;
            this.lastFpsTime = now;
        }
        // Set mode indicator
        const modeEl = document.getElementById('mode-display');
        if (modeEl) {
            modeEl.textContent = view.setMode === 'julia'
                ? `Julia (${view.juliaCre.toFixed(4)}, ${view.juliaCim.toFixed(4)}i)`
                : 'Mandelbrot';
        }
        this.resetHideTimer();
    }
    resetHideTimer() {
        if (this.hideTimeout)
            clearTimeout(this.hideTimeout);
        this.hudEl.classList.remove('hidden');
        this.isHudVisible = true;
        this.hideTimeout = setTimeout(() => {
            this.hudEl.classList.add('hidden');
            this.isHudVisible = false;
        }, config_1.CONFIG.hudHideDelayMs);
    }
    showHud() {
        if (this.hideTimeout)
            clearTimeout(this.hideTimeout);
        this.hudEl.classList.remove('hidden');
        this.isHudVisible = true;
        this.resetHideTimer();
    }
    drawZoomRect(x1, y1, x2, y2) {
        const ctx = this.zoomOverlay.getContext('2d');
        ctx.clearRect(0, 0, this.zoomOverlay.width, this.zoomOverlay.height);
        if (x1 === x2 || y1 === y2)
            return;
        // Inverted-color border effect
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.9)';
        ctx.lineWidth = 2;
        ctx.setLineDash([6, 4]);
        ctx.strokeRect(x1, y1, x2 - x1, y2 - y1);
        ctx.setLineDash([]);
        // Semi-transparent fill with crosshair
        ctx.fillStyle = 'rgba(0, 0, 0, 0.15)';
        ctx.fillRect(x1, y1, x2 - x1, y2 - y1);
        const cx = (x1 + x2) / 2;
        const cy = (y1 + y2) / 2;
        const size = 8;
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.7)';
        ctx.lineWidth = 1;
        ctx.beginPath();
        ctx.moveTo(cx - size, cy);
        ctx.lineTo(cx + size, cy);
        ctx.moveTo(cx, cy - size);
        ctx.lineTo(cx, cy + size);
        ctx.stroke();
        // Corner markers
        const cornerSize = 6;
        ctx.strokeStyle = 'rgba(255, 100, 100, 0.9)';
        ctx.lineWidth = 2;
        this.drawCorner(ctx, x1, y1, cornerSize);
        this.drawCorner(ctx, x2, y1, -cornerSize);
        this.drawCorner(ctx, x1, y2, cornerSize);
        this.drawCorner(ctx, x2, y2, -cornerSize);
    }
    drawCorner(ctx, x, y, dir) {
        ctx.beginPath();
        ctx.moveTo(x + dir * 2, y);
        ctx.lineTo(x, y);
        ctx.lineTo(x, y + dir * 2);
        ctx.stroke();
    }
    clearZoomRect() {
        const ctx = this.zoomOverlay.getContext('2d');
        ctx.clearRect(0, 0, this.zoomOverlay.width, this.zoomOverlay.height);
    }
    updateBookmarks() {
        const bookmarks = (0, bookmarks_1.listBookmarks)();
        this.bookmarkList.innerHTML = '';
        for (const bm of bookmarks) {
            const li = document.createElement('li');
            li.className = 'bookmark-item';
            const nameSpan = document.createElement('span');
            nameSpan.className = 'bookmark-name';
            nameSpan.textContent = bm.name;
            nameSpan.addEventListener('click', () => this.onBookmarkLoad?.(bm));
            const delBtn = document.createElement('button');
            delBtn.className = 'bookmark-delete';
            delBtn.textContent = '×';
            delBtn.addEventListener('click', (e) => {
                e.stopPropagation();
                (0, bookmarks_1.deleteBookmark)(bm.name);
                this.updateBookmarks();
            });
            li.appendChild(nameSpan);
            li.appendChild(delBtn);
            this.bookmarkList.appendChild(li);
        }
    }
    setCallbacks(opts) {
        Object.assign(this, opts);
    }
}
exports.FractalUI = FractalUI;
};

// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingcap-qwen3.6-27b","provider":"lmstudio","persona":"typescript","sessionId":"eebb5a0e-e298-41cb-8fbf-4fda25dc3687","tokensIn":2356497,"tokensOut":47037,"tokensTotal":2403534,"turns":60,"toolCalls":59,"failedToolCalls":1,"timestamp":"2026-07-31T18:59:14.406Z"} -->