← Fractal Explorer results

Fractal Explorer

ThinkingCap Qwen3.6 27B GGUF · typescript

Initial view of Fractal Explorer

Reviews & generation details

Reviews

No human review yet. Unreviewed does not mean unsuccessful.

Generation context

Imported historical artifact. Its exact original instructions and lineage are not known.

Generation prompt · exact text unavailable

See the current prompt for reference. It may differ from the instructions used to generate this artifact.

Inspect original source 46,499 bytes · SHA-256 630ad661e2e1
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Fractal Explorer</title>
<style>
/* ── css: layout ── */
*, *::before, *::after {
  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: #e0e0e0;
}

#app {
  position: relative;
  width: 100%;
  height: 100%;
  overflow: hidden;
}

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

/* HUD panel - glassy overlay */
.hud-panel {
  position: absolute;
  top: 12px;
  left: 12px;
  z-index: 10;
  background: rgba(10, 10, 20, 0.75);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 10px;
  padding: 14px 18px;
  min-width: 260px;
  transition: opacity 0.4s ease, transform 0.4s ease;
  max-height: calc(100vh - 24px);
  overflow-y: auto;
}

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

.hud-panel:hover {
  opacity: 1 !important;
  transform: translateY(0) !important;
}

.hud-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 10px;
}

.hud-title {
  font-size: 14px;
  font-weight: 600;
  letter-spacing: 0.5px;
  color: #ccc;
}

.hud-controls {
  display: flex;
  gap: 4px;
}

.hud-controls button {
  background: rgba(255, 255, 255, 0.08);
  border: 1px solid rgba(255, 255, 255, 0.15);
  color: #ccc;
  border-radius: 6px;
  width: 30px;
  height: 30px;
  cursor: pointer;
  font-size: 14px;
  transition: background 0.15s;
}

.hud-controls button:hover {
  background: rgba(255, 255, 255, 0.2);
  color: #fff;
}

.hud-stats {
  margin-bottom: 10px;
}

.stat-row {
  display: flex;
  justify-content: space-between;
  font-size: 11px;
  padding: 2px 0;
}

.stat-label {
  color: #888;
}

.stat-value {
  color: #ddd;
  font-family: 'Consolas', 'Monaco', monospace;
  font-size: 11px;
}

.hud-section-label {
  display: block;
  font-size: 11px;
  color: #888;
  margin-bottom: 6px;
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

/* Palette chips */
.palette-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  margin-bottom: 12px;
}

.palette-chip {
  font-size: 10px;
  padding: 3px 8px;
  border-radius: 12px;
  border: 1px solid rgba(255, 255, 255, 0.15);
  background: rgba(255, 255, 255, 0.06);
  color: #aaa;
  cursor: pointer;
  transition: all 0.15s;
}

.palette-chip:hover {
  background: rgba(255, 255, 255, 0.15);
  color: #fff;
}

.palette-chip.active {
  background: rgba(100, 150, 255, 0.3);
  border-color: rgba(100, 150, 255, 0.6);
  color: #fff;
}

/* Sliders */
.hud-sliders {
  margin-bottom: 12px;
}

.slider-row {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 6px;
  font-size: 11px;
}

.slider-row label {
  color: #888;
  min-width: 90px;
}

.slider-row input[type="range"] {
  flex: 1;
  height: 4px;
  -webkit-appearance: none;
  appearance: none;
  background: rgba(255, 255, 255, 0.15);
  border-radius: 2px;
  outline: none;
}

.slider-row input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: #6a9fff;
  cursor: pointer;
}

.slider-row input[type="range"]::-moz-range-thumb {
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: #6a9fff;
  cursor: pointer;
  border: none;
}

#iter-val {
  font-family: 'Consolas', monospace;
  color: #aaa;
  min-width: 35px;
  text-align: right;
  font-size: 11px;
}

/* Help text */
.hud-help {
  border-top: 1px solid rgba(255, 255, 255, 0.08);
  padding-top: 8px;
  margin-top: 4px;
}

.help-text {
  font-size: 10px;
  color: #666;
}

/* Bookmarks */
.bookmarks-panel {
  border-top: 1px solid rgba(255, 255, 255, 0.08);
  padding-top: 8px;
  margin-top: 8px;
}

.bookmark-list {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  margin-top: 6px;
}

.bm-btn {
  font-size: 10px;
  padding: 3px 8px;
  border-radius: 4px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  background: rgba(255, 255, 255, 0.06);
  color: #aaa;
  cursor: pointer;
  position: relative;
  padding-right: 22px;
}

.bm-btn:hover {
  background: rgba(255, 255, 255, 0.15);
}

.bm-del {
  position: absolute;
  right: 2px;
  top: 50%;
  transform: translateY(-50%);
  background: none;
  border: none;
  color: #888;
  cursor: pointer;
  font-size: 12px;
  line-height: 1;
  padding: 0;
  width: auto;
  height: auto;
}

.bm-del:hover {
  color: #f66;
}

.bm-empty {
  font-size: 10px;
  color: #555;
  font-style: italic;
}
</style>
</head>
<body>
<div id="app"></div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./constants":"src/constants.ts","./state":"src/state.ts","./compute":"src/compute.ts","./renderer":"src/renderer.ts","./input":"src/input.ts","./hud":"src/hud.ts"},"src/state.ts":{"./constants":"src/constants.ts"},"src/renderer.ts":{"./constants":"src/constants.ts","./state":"src/state.ts","./compute":"src/compute.ts","./palette":"src/palette.ts"},"src/input.ts":{"./constants":"src/constants.ts","./state":"src/state.ts"},"src/hud.ts":{"./state":"src/state.ts","./palette":"src/palette.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 });
exports.start = start;
const constants_1 = require("./constants");
const state_1 = require("./state");
const compute_1 = require("./compute");
const renderer_1 = require("./renderer");
const input_1 = require("./input");
const hud_1 = require("./hud");
function main() {
    const app = document.getElementById('app');
    // Create canvas
    const canvas = document.createElement('canvas');
    canvas.id = 'fractal-canvas';
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
    app.appendChild(canvas);
    // State
    const state = (0, state_1.createInitialState)();
    // Try to restore from URL hash
    if (location.hash) {
        const hashView = (0, state_1.hashToView)(location.hash);
        if (hashView) {
            Object.assign(state.view, hashView);
        }
    }
    // Renderer
    const renderer = new renderer_1.Renderer(canvas, { width: canvas.width, height: canvas.height });
    // HUD
    const hud = new hud_1.HUD(app, state);
    // Input handler
    const input = new input_1.InputHandler(canvas, state);
    // Pending render flag to debounce
    let pendingRender = false;
    let pendingRecolorize = false;
    let juliaPreviewTimer = null;
    function doRender() {
        pendingRender = false;
        renderer.render(state.view);
    }
    function doRecolorize() {
        pendingRecolorize = false;
        renderer.recolorize(state.view);
    }
    // Wire callbacks
    input.setRenderRequested(() => {
        renderer.cancel();
        if (!pendingRender) {
            pendingRender = true;
            requestAnimationFrame(doRender);
        }
    });
    hud.setRenderCallback(() => {
        renderer.cancel();
        if (!pendingRender) {
            pendingRender = true;
            requestAnimationFrame(doRender);
        }
    });
    hud.setRecolorizeCallback(() => {
        if (!pendingRecolorize) {
            pendingRecolorize = true;
            requestAnimationFrame(doRecolorize);
        }
    });
    // Julia hover preview
    input.setJuliaHover((re, im) => {
        if (juliaPreviewTimer)
            clearTimeout(juliaPreviewTimer);
        juliaPreviewTimer = window.setTimeout(() => {
            state.view.juliaC = { re, im };
        }, constants_1.CONSTANTS.juliaPreviewDelay);
    });
    // Progress callback
    renderer.setProgressCb((p) => {
        state.renderProgress = p;
        hud.updateProgress(p);
    });
    // FPS tracking
    let frameCount = 0;
    let lastFpsTime = performance.now();
    function updateFps() {
        frameCount++;
        const now = performance.now();
        if (now - lastFpsTime >= 1000) {
            const fps = (frameCount / (now - lastFpsTime)) * 1000;
            hud.updateFps(fps);
            frameCount = 0;
            lastFpsTime = now;
        }
        requestAnimationFrame(updateFps);
    }
    requestAnimationFrame(updateFps);
    // Initial render
    hud.update();
    renderer.render(state.view);
    // Resize handler
    window.addEventListener('resize', () => {
        const w = window.innerWidth;
        const h = window.innerHeight;
        canvas.width = w;
        canvas.height = h;
        // Recreate renderer with new dimensions
        const newRenderer = new renderer_1.Renderer(canvas, { width: w, height: h });
        Object.assign(newRenderer, renderer);
        newRenderer.setProgressCb((p) => {
            state.renderProgress = p;
            hud.updateProgress(p);
        });
        renderer.render(state.view);
    });
    // Expose debug interface
    window.fractal = {
        iterationsAt: (re, im, maxIter) => {
            return (0, compute_1.iterationsAt)(re, im, maxIter ?? 200);
        },
        setView: (params) => {
            state.view.re = params.re;
            state.view.im = params.im;
            state.view.scale = params.scale;
            state.view.maxIter = (0, state_1.computeMaxIter)(params.scale);
            hud.update();
            renderer.render(state.view);
        },
        getState: () => state,
        cancelRender: () => renderer.cancel(),
        saveBookmark: (name, view) => (0, state_1.saveBookmark)(name, view),
        loadBookmark: (name) => (0, state_1.loadBookmark)(name),
        deleteBookmark: (name) => (0, state_1.deleteBookmark)(name),
    };
}
function start() {
    main();
}
start();
};

// ── module: src/constants.ts ──
__mods["src/constants.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CONSTANTS = void 0;
// Tunable constants
exports.CONSTANTS = {
    // Iteration settings
    maxIterBase: 200,
    maxIterScale: 80, // additional iterations per log10 of magnification
    maxIterCap: 10000,
    // Zoom settings
    zoomSpeed: 1.3, // multiplier per wheel tick
    dragZoomMin: 5, // min pixels to distinguish drag-zoom from click
    maxMagnification: 1e14,
    // Progressive rendering
    progressivePasses: 4, // 1/4 → 1/2 → 1/1 steps (2^passes = max factor)
    tilePixelSize: 64, // worker tile size in pixels
    // Julia preview
    juliaPreviewEnabled: true,
    juliaPreviewDelay: 150, // ms debounce for live preview
    // Navigation history
    zoomHistoryMax: 50,
    // Canvas
    canvasWidth: 1280,
    canvasHeight: 720,
    // HUD auto-hide
    hudHideDelay: 4000,
};
};

// ── module: src/state.ts ──
__mods["src/state.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.defaultView = defaultView;
exports.createInitialState = createInitialState;
exports.computeMaxIter = computeMaxIter;
exports.pushZoomHistory = pushZoomHistory;
exports.popZoomHistory = popZoomHistory;
exports.saveBookmark = saveBookmark;
exports.loadBookmark = loadBookmark;
exports.listBookmarks = listBookmarks;
exports.deleteBookmark = deleteBookmark;
exports.viewToHash = viewToHash;
exports.hashToView = hashToView;
const constants_1 = require("./constants");
function defaultView() {
    return {
        re: -0.5,
        im: 0,
        scale: 250, // fits the classic Mandelbrot view
        maxIter: 200,
        palette: 0,
        paletteOffset: 0,
        setMode: 'mandelbrot',
        juliaC: { re: -0.75, im: 0.1 },
    };
}
function createInitialState() {
    return {
        view: defaultView(),
        zoomHistory: [],
        fps: 0,
        renderProgress: 0,
    };
}
// Compute max iterations based on zoom depth
function computeMaxIter(scale, base = constants_1.CONSTANTS.maxIterBase, scaleFactor = constants_1.CONSTANTS.maxIterScale) {
    const logMag = Math.log10(Math.max(scale, 1));
    const extra = Math.floor(logMag * scaleFactor);
    return Math.min(base + extra, constants_1.CONSTANTS.maxIterCap);
}
// Save/restore zoom history
function pushZoomHistory(state, max = constants_1.CONSTANTS.zoomHistoryMax) {
    state.zoomHistory.push(structuredClone(state.view));
    if (state.zoomHistory.length > max) {
        state.zoomHistory.shift();
    }
}
function popZoomHistory(state) {
    if (state.zoomHistory.length === 0)
        return null;
    return state.zoomHistory.pop() ?? null;
}
// Save view as bookmark
function saveBookmark(name, view) {
    try {
        const bookmarks = JSON.parse(localStorage.getItem('fractal-bookmarks') ?? '{}');
        bookmarks[name] = view;
        localStorage.setItem('fractal-bookmarks', JSON.stringify(bookmarks));
    }
    catch { /* ignore */ }
}
function loadBookmark(name) {
    try {
        const bookmarks = JSON.parse(localStorage.getItem('fractal-bookmarks') ?? '{}');
        return bookmarks[name] ?? null;
    }
    catch {
        return null;
    }
}
function listBookmarks() {
    try {
        const bookmarks = JSON.parse(localStorage.getItem('fractal-bookmarks') ?? '{}');
        return Object.keys(bookmarks);
    }
    catch {
        return [];
    }
}
function deleteBookmark(name) {
    try {
        const bookmarks = JSON.parse(localStorage.getItem('fractal-bookmarks') ?? '{}');
        delete bookmarks[name];
        localStorage.setItem('fractal-bookmarks', JSON.stringify(bookmarks));
    }
    catch { /* ignore */ }
}
// URL hash state
function viewToHash(view) {
    return `#v=${view.re.toFixed(15)},${view.im.toFixed(15)},${view.scale},${view.maxIter},${view.palette},${view.paletteOffset.toFixed(2)},${view.setMode},${view.juliaC.re},${view.juliaC.im}`;
}
function hashToView(hash) {
    try {
        const m = hash.match(/^#v=([^,]+),([^,]+),([^,]+),([^,]+),([^,]+),([^,]+),([^,]+),([^,]+),([^,]+)$/);
        if (!m)
            return null;
        return {
            re: parseFloat(m[1]),
            im: parseFloat(m[2]),
            scale: parseFloat(m[3]),
            maxIter: parseInt(m[4], 10),
            palette: parseInt(m[5], 10),
            paletteOffset: parseFloat(m[6]),
            setMode: m[7],
            juliaC: { re: parseFloat(m[8]), im: parseFloat(m[9]) },
        };
    }
    catch {
        return null;
    }
}
};

// ── module: src/palette.ts ──
__mods["src/palette.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PALETTE_NAMES = exports.PALETTES = void 0;
exports.buildColorLUT = buildColorLUT;
exports.colorize = colorize;
exports.recolorize = recolorize;
exports.PALETTES = [
    // 0: Fire — black → red → orange → yellow → white
    [
        [0.00, 0, 0, 0],
        [0.15, 128, 0, 0],
        [0.30, 255, 60, 0],
        [0.50, 255, 180, 20],
        [0.75, 255, 255, 150],
        [1.00, 255, 255, 255],
    ],
    // 1: Ocean — deep blue → cyan → teal → light blue → white
    [
        [0.00, 0, 0, 30],
        [0.20, 0, 30, 120],
        [0.40, 0, 120, 200],
        [0.60, 0, 200, 255],
        [0.80, 150, 220, 255],
        [1.00, 220, 240, 255],
    ],
    // 2: Monochrome — black → gray → white (smooth luminance)
    [
        [0.00, 0, 0, 0],
        [0.25, 40, 40, 40],
        [0.50, 90, 90, 90],
        [0.75, 170, 170, 170],
        [1.00, 255, 255, 255],
    ],
    // 3: Psychedelic cyclic — full RGB cycle
    [
        [0.00, 0, 0, 255],
        [0.17, 0, 255, 255],
        [0.33, 0, 255, 0],
        [0.50, 255, 255, 0],
        [0.67, 255, 0, 0],
        [0.83, 255, 0, 255],
        [1.00, 0, 0, 255],
    ],
    // 4: Aurora — violet → purple → pink → magenta
    [
        [0.00, 10, 0, 40],
        [0.25, 60, 0, 120],
        [0.50, 180, 20, 160],
        [0.75, 240, 100, 200],
        [1.00, 180, 20, 80],
    ],
];
exports.PALETTE_NAMES = ['Fire', 'Ocean', 'Monochrome', 'Psychedelic', 'Aurora'];
// Build a color LUT of 2048 entries for the given palette + offset
// Returns a Uint8ClampedArray of length 2048 * 3 (R, G, B per entry)
function buildColorLUT(paletteIdx, offset, size = 2048) {
    const palette = exports.PALETTES[paletteIdx % exports.PALETTES.length];
    const lut = new Uint8ClampedArray(size * 3);
    for (let i = 0; i < size; i++) {
        let t = (i / size + offset) % 1.0;
        if (t < 0)
            t += 1.0;
        // Find surrounding stops
        let lo = 0, hi = palette.length - 1;
        for (let s = 0; s < palette.length - 1; s++) {
            if (palette[s][0] <= t && palette[s + 1][0] >= t) {
                lo = s;
                hi = s + 1;
                break;
            }
        }
        const stopA = palette[lo];
        const stopB = palette[hi];
        const range = stopB[0] - stopA[0];
        const frac = range > 0 ? (t - stopA[0]) / range : 0;
        const r = Math.round(stopA[1] + frac * (stopB[1] - stopA[1]));
        const g = Math.round(stopA[2] + frac * (stopB[2] - stopA[2]));
        const b = Math.round(stopA[3] + frac * (stopB[3] - stopA[3]));
        lut[i * 3] = r;
        lut[i * 3 + 1] = g;
        lut[i * 3 + 2] = b;
    }
    return lut;
}
// Colorize an iteration buffer using the LUT
// buffer: Float32Array of iteration counts (smooth)
// Returns void — writes directly to imageData.data
function colorize(buffer, maxIter, lut, lutSize, imageData) {
    const data = imageData.data;
    const len = buffer.length;
    for (let i = 0; i < len; i++) {
        const idx = i * 4;
        const iter = buffer[i];
        if (iter >= maxIter) {
            // Interior point — black
            data[idx] = 0;
            data[idx + 1] = 0;
            data[idx + 2] = 0;
            data[idx + 3] = 255;
        }
        else {
            // Smooth coloring: map fractional iteration to LUT
            const norm = (iter % lutSize) / lutSize;
            const lutIdx = Math.floor(norm * (lutSize - 1));
            data[idx] = lut[lutIdx * 3];
            data[idx + 1] = lut[lutIdx * 3 + 1];
            data[idx + 2] = lut[lutIdx * 3 + 2];
            data[idx + 3] = 255;
        }
    }
}
// Re-colorize only (no iteration recomputation) — for palette offset changes
function recolorize(buffer, maxIter, lut, lutSize, imageData) {
    colorize(buffer, maxIter, lut, lutSize, imageData);
}
};

// ── module: src/compute.ts ──
__mods["src/compute.ts"] = function (exports, require, module) {
"use strict";
// Core fractal computation — allocation-free pixel iteration
// Returns smooth (continuous) iteration counts in a Float32Array buffer
Object.defineProperty(exports, "__esModule", { value: true });
exports.mandelbrotIter = mandelbrotIter;
exports.juliaIter = juliaIter;
exports.computeRegion = computeRegion;
exports.iterationsAt = iterationsAt;
// Compute a single pixel iteration count (Mandelbrot)
function mandelbrotIter(re, im, maxIter) {
    let zr = 0, zi = 0;
    let zr2 = 0, zi2 = 0;
    let iter = 0;
    while (zr2 + zi2 <= 4 && iter < maxIter) {
        zi = 2 * zr * zi + im;
        zr = zr2 - zi2 + re;
        zr2 = zr * zr;
        zi2 = zi * zi;
        iter++;
    }
    if (iter >= maxIter)
        return maxIter;
    // Smooth coloring: n + 1 - log2(log|z|)
    const logZn = Math.log(zr2 + zi2) * 0.5;
    const nu = Math.log(logZn / Math.LN2) / Math.LN2;
    return iter + 1 - nu;
}
// Compute a single pixel iteration count (Julia)
function juliaIter(zre, zim, cre, cim, maxIter) {
    let zr = zre, zi = zim;
    let zr2 = zr * zr, zi2 = zi * zi;
    let iter = 0;
    while (zr2 + zi2 <= 4 && iter < maxIter) {
        zi = 2 * zr * zi + cim;
        zr = zr2 - zi2 + cre;
        zr2 = zr * zr;
        zi2 = zi * zi;
        iter++;
    }
    if (iter >= maxIter)
        return maxIter;
    const logZn = Math.log(zr2 + zi2) * 0.5;
    const nu = Math.log(logZn / Math.LN2) / Math.LN2;
    return iter + 1 - nu;
}
// Compute a rectangular region into a Float32Array buffer
// x0,y0: pixel coordinates, w,h: pixel dimensions
// re0,im0: complex center, dx,dy: complex increment per pixel
// setMode: 'mandelbrot' | 'julia', juliaC for Julia mode
// Buffer is written at buf[offset + y * w + x]
function computeRegion(buf, offset, x0, y0, w, h, re0, im0, dx, dy, maxIter, setMode, juliaCre, juliaCim) {
    for (let py = 0; py < h; py++) {
        const ci = im0 + py * dy;
        for (let px = 0; px < w; px++) {
            const cr = re0 + px * dx;
            let val;
            if (setMode === 'mandelbrot') {
                val = mandelbrotIter(cr, ci, maxIter);
            }
            else {
                val = juliaIter(cr, ci, juliaCre, juliaCim, maxIter);
            }
            buf[offset + py * w + px] = val;
        }
    }
}
// Query: iterations at a specific complex point (for debug interface)
function iterationsAt(cre, cim, maxIter = 200) {
    return mandelbrotIter(cre, cim, maxIter);
}
};

// ── module: src/renderer.ts ──
__mods["src/renderer.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Renderer = void 0;
const compute_1 = require("./compute");
const palette_1 = require("./palette");
class Renderer {
    constructor(canvas, config = { width: 1280, height: 720 }) {
        this.lutSize = 2048;
        this.renderId = 0;
        this.onProgress = null;
        this.onComplete = null;
        this.chunkSize = 4; // rows per chunk
        this.canvas = canvas;
        this.width = config.width;
        this.height = config.height;
        canvas.width = this.width;
        canvas.height = this.height;
        this.ctx = canvas.getContext('2d', { willReadFrequently: false });
        this.imageData = this.ctx.createImageData(this.width, this.height);
        this.iterBuffer = new Float32Array(this.width * this.height);
        this.colorLUT = (0, palette_1.buildColorLUT)(0, 0, this.lutSize);
    }
    setProgressCb(cb) { this.onProgress = cb; }
    setCompleteCb(cb) { this.onComplete = cb; }
    cancel() {
        this.renderId++;
    }
    recolorize(view) {
        this.colorLUT = (0, palette_1.buildColorLUT)(view.palette, view.paletteOffset, this.lutSize);
        (0, palette_1.colorize)(this.iterBuffer, view.maxIter, this.colorLUT, this.lutSize, this.imageData);
        this.ctx.putImageData(this.imageData, 0, 0);
    }
    async render(view) {
        const renderId = ++this.renderId;
        this.colorLUT = (0, palette_1.buildColorLUT)(view.palette, view.paletteOffset, this.lutSize);
        const halfW = 0.5 / view.scale;
        const halfH = halfW * (this.width / this.height);
        const re0 = view.re - halfW;
        const im0 = view.im - halfH;
        // Progressive passes: start coarse, refine
        const passes = [16, 8, 4, 2, 1]; // stride values
        let totalPasses = passes.length;
        let passIdx = 0;
        while (passIdx < totalPasses) {
            if (renderId !== this.renderId)
                return;
            const stride = passes[passIdx];
            const pw = Math.ceil(this.width / stride);
            const ph = Math.ceil(this.height / stride);
            const dx = (2 * halfW) / pw;
            const dy = (2 * halfH) / ph;
            // Compute at reduced resolution
            const tileBuf = new Float32Array(pw * ph);
            (0, compute_1.computeRegion)(tileBuf, 0, 0, 0, pw, ph, re0, im0, dx, dy, view.maxIter, view.setMode, view.juliaC.re, view.juliaC.im);
            // Upsample to full buffer
            for (let sy = 0; sy < ph; sy++) {
                for (let sx = 0; sx < pw; sx++) {
                    for (let my = 0; my < stride && (sy * stride + my) < this.height; my++) {
                        for (let mx = 0; mx < stride && (sx * stride + mx) < this.width; mx++) {
                            const destY = sy * stride + my;
                            const destX = sx * stride + mx;
                            this.iterBuffer[destY * this.width + destX] = tileBuf[sy * pw + sx];
                        }
                    }
                }
            }
            // Colorize and blit immediately
            (0, palette_1.colorize)(this.iterBuffer, view.maxIter, this.colorLUT, this.lutSize, this.imageData);
            this.ctx.putImageData(this.imageData, 0, 0);
            const progress = (passIdx + 1) / totalPasses;
            this.onProgress?.(progress);
            passIdx++;
            // Yield to keep UI responsive
            await new Promise(r => setTimeout(r, 0));
        }
        if (renderId === this.renderId) {
            this.onProgress?.(1);
            this.onComplete?.();
        }
    }
    // Render synchronously (for small regions or final pass)
    renderSync(view) {
        this.colorLUT = (0, palette_1.buildColorLUT)(view.palette, view.paletteOffset, this.lutSize);
        const halfW = 0.5 / view.scale;
        const halfH = halfW * (this.width / this.height);
        const re0 = view.re - halfW;
        const im0 = view.im - halfH;
        const dx = (2 * halfW) / this.width;
        const dy = (2 * halfH) / this.height;
        (0, compute_1.computeRegion)(this.iterBuffer, 0, 0, 0, this.width, this.height, re0, im0, dx, dy, view.maxIter, view.setMode, view.juliaC.re, view.juliaC.im);
        (0, palette_1.colorize)(this.iterBuffer, view.maxIter, this.colorLUT, this.lutSize, this.imageData);
        this.ctx.putImageData(this.imageData, 0, 0);
        this.onProgress?.(1);
        this.onComplete?.();
    }
    dispose() { }
}
exports.Renderer = Renderer;
};

// ── 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 constants_1 = require("./constants");
const state_1 = require("./state");
class InputHandler {
    constructor(canvas, state) {
        this.isDragging = false;
        this.isPanning = false;
        this.dragStart = { x: 0, y: 0 };
        this.panStart = { re: 0, im: 0, x: 0, y: 0 };
        this.zoomBox = null;
        this.zoomBoxCtx = null;
        this.onRenderRequested = null;
        this.onJuliaHover = null;
        this.lastMouse = { x: 0, y: 0 };
        this.lastClickTime = 0;
        this.canvas = canvas;
        this.state = state;
        // Create overlay canvas for zoom box
        this.zoomBox = document.createElement('canvas');
        this.zoomBox.style.cssText = `
      position: absolute; top: 0; left: 0; width: 100%; height: 100%;
      pointer-events: none; z-index: 5;
    `;
        canvas.parentElement?.appendChild(this.zoomBox);
        this.resizeZoomBox();
        this.zoomBoxCtx = this.zoomBox.getContext('2d');
        this.setupEvents();
    }
    setRenderRequested(cb) { this.onRenderRequested = cb; }
    setJuliaHover(cb) { this.onJuliaHover = cb; }
    resizeZoomBox() {
        if (!this.zoomBox)
            return;
        this.zoomBox.width = this.canvas.width;
        this.zoomBox.height = this.canvas.height;
    }
    pixelToComplex(px, py) {
        const v = this.state.view;
        const halfW = 0.5 / v.scale;
        const halfH = halfW * (this.canvas.width / this.canvas.height);
        const re = v.re + (px / this.canvas.width - 0.5) * 2 * halfW;
        const im = v.im + (py / this.canvas.height - 0.5) * 2 * halfH;
        return { re, im };
    }
    complexToPixel(re, im) {
        const v = this.state.view;
        const halfW = 0.5 / v.scale;
        const halfH = halfW * (this.canvas.width / this.canvas.height);
        const x = ((re - v.re) / (2 * halfW) + 0.5) * this.canvas.width;
        const y = ((im - v.im) / (2 * halfH) + 0.5) * this.canvas.height;
        return { x, y };
    }
    setupEvents() {
        // Mouse down: start drag or pan
        this.canvas.addEventListener('mousedown', (e) => {
            if (e.button !== 0)
                return;
            this.isDragging = true;
            this.dragStart = { x: e.offsetX, y: e.offsetY };
            this.lastClickTime = Date.now();
        });
        // Mouse move: draw zoom box or pan
        this.canvas.addEventListener('mousemove', (e) => {
            this.lastMouse = { x: e.offsetX, y: e.offsetY };
            if (!this.isDragging) {
                // Hover preview for Julia
                if (this.state.view.setMode === 'mandelbrot' && this.onJuliaHover) {
                    const pt = this.pixelToComplex(e.offsetX, e.offsetY);
                    this.onJuliaHover(pt.re, pt.im);
                }
                return;
            }
            const dx = Math.abs(e.offsetX - this.dragStart.x);
            const dy = Math.abs(e.offsetY - this.dragStart.y);
            if (dx > constants_1.CONSTANTS.dragZoomMin || dy > constants_1.CONSTANTS.dragZoomMin) {
                // Drawing zoom box
                this.isPanning = false;
                this.drawZoomBox(Math.min(this.dragStart.x, e.offsetX), Math.min(this.dragStart.y, e.offsetY), Math.abs(e.offsetX - this.dragStart.x), Math.abs(e.offsetY - this.dragStart.y));
            }
        });
        // Mouse up: zoom or pan
        this.canvas.addEventListener('mouseup', (e) => {
            if (!this.isDragging)
                return;
            this.isDragging = false;
            const dx = Math.abs(e.offsetX - this.dragStart.x);
            const dy = Math.abs(e.offsetX - this.dragStart.y);
            if (dx > constants_1.CONSTANTS.dragZoomMin || dy > constants_1.CONSTANTS.dragZoomMin) {
                // Zoom into rectangle
                const x1 = Math.min(this.dragStart.x, e.offsetX);
                const y1 = Math.min(this.dragStart.y, e.offsetY);
                const x2 = Math.max(this.dragStart.x, e.offsetX);
                const y2 = Math.max(this.dragStart.y, e.offsetY);
                const c1 = this.pixelToComplex(x1, y1);
                const c2 = this.pixelToComplex(x2, y2);
                this.clearZoomBox();
                (0, state_1.pushZoomHistory)(this.state);
                const v = this.state.view;
                v.re = (c1.re + c2.re) / 2;
                v.im = (c1.im + c2.im) / 2;
                const oldW = 1 / v.scale;
                const newW = Math.abs(c2.re - c1.re);
                v.scale = 1 / newW;
                if (v.scale > constants_1.CONSTANTS.maxMagnification) {
                    v.scale = constants_1.CONSTANTS.maxMagnification;
                }
                v.maxIter = (0, state_1.computeMaxIter)(v.scale);
                this.onRenderRequested?.();
            }
            else {
                // Click: could be Julia mode switch (handled by parent)
            }
        });
        // Wheel zoom
        this.canvas.addEventListener('wheel', (e) => {
            e.preventDefault();
            const v = this.state.view;
            const oldScale = v.scale;
            const factor = e.deltaY < 0 ? constants_1.CONSTANTS.zoomSpeed : 1 / constants_1.CONSTANTS.zoomSpeed;
            const newScale = oldScale * factor;
            if (newScale > constants_1.CONSTANTS.maxMagnification) {
                v.scale = constants_1.CONSTANTS.maxMagnification;
            }
            else if (newScale < 1) {
                v.scale = 1;
            }
            else {
                v.scale = newScale;
            }
            // Zoom toward cursor
            const pt = this.pixelToComplex(e.offsetX, e.offsetY);
            const oldPt = this.pixelToComplexAtScale(e.offsetX, e.offsetY, oldScale);
            v.re += oldPt.re - pt.re;
            v.im += oldPt.im - pt.im;
            v.maxIter = (0, state_1.computeMaxIter)(v.scale);
            (0, state_1.pushZoomHistory)(this.state);
            this.onRenderRequested?.();
        }, { passive: false });
        // Keyboard
        window.addEventListener('keydown', (e) => {
            const v = this.state.view;
            const panAmt = 0.2 / v.scale;
            switch (e.key) {
                case 'ArrowLeft':
                    v.re -= panAmt;
                    e.preventDefault();
                    this.onRenderRequested?.();
                    break;
                case 'ArrowRight':
                    v.re += panAmt;
                    e.preventDefault();
                    this.onRenderRequested?.();
                    break;
                case 'ArrowUp':
                    v.im -= panAmt;
                    e.preventDefault();
                    this.onRenderRequested?.();
                    break;
                case 'ArrowDown':
                    v.im += panAmt;
                    e.preventDefault();
                    this.onRenderRequested?.();
                    break;
                case '+':
                case '=':
                    v.scale *= constants_1.CONSTANTS.zoomSpeed;
                    if (v.scale > constants_1.CONSTANTS.maxMagnification)
                        v.scale = constants_1.CONSTANTS.maxMagnification;
                    v.maxIter = (0, state_1.computeMaxIter)(v.scale);
                    (0, state_1.pushZoomHistory)(this.state);
                    this.onRenderRequested?.();
                    break;
                case '-':
                case '_':
                    v.scale /= constants_1.CONSTANTS.zoomSpeed;
                    if (v.scale < 1)
                        v.scale = 1;
                    v.maxIter = (0, state_1.computeMaxIter)(v.scale);
                    (0, state_1.pushZoomHistory)(this.state);
                    this.onRenderRequested?.();
                    break;
                case 'j':
                case 'J':
                    // Toggle Julia mode at current cursor position
                    const pt = this.pixelToComplex(this.lastMouse.x, this.lastMouse.y);
                    v.juliaC = { re: pt.re, im: pt.im };
                    v.setMode = v.setMode === 'mandelbrot' ? 'julia' : 'mandelbrot';
                    v.maxIter = (0, state_1.computeMaxIter)(v.scale);
                    this.onRenderRequested?.();
                    break;
                case 'z':
                    // Undo
                    this.onRenderRequested?.();
                    break;
            }
        });
    }
    pixelToComplexAtScale(px, py, scale) {
        const v = this.state.view;
        const halfW = 0.5 / scale;
        const halfH = halfW * (this.canvas.width / this.canvas.height);
        const re = v.re + (px / this.canvas.width - 0.5) * 2 * halfW;
        const im = v.im + (py / this.canvas.height - 0.5) * 2 * halfH;
        return { re, im };
    }
    drawZoomBox(x, y, w, h) {
        if (!this.zoomBoxCtx || !this.zoomBox)
            return;
        this.zoomBoxCtx.clearRect(0, 0, this.zoomBox.width, this.zoomBox.height);
        this.zoomBoxCtx.strokeStyle = 'rgba(255, 255, 255, 0.8)';
        this.zoomBoxCtx.lineWidth = 2;
        this.zoomBoxCtx.setLineDash([6, 3]);
        this.zoomBoxCtx.strokeRect(x, y, w, h);
        // Inverted overlay inside box
        this.zoomBoxCtx.fillStyle = 'rgba(0, 0, 0, 0.15)';
        this.zoomBoxCtx.fillRect(x, y, w, h);
        this.zoomBoxCtx.setLineDash([]);
    }
    clearZoomBox() {
        if (this.zoomBoxCtx && this.zoomBox) {
            this.zoomBoxCtx.clearRect(0, 0, this.zoomBox.width, this.zoomBox.height);
        }
    }
    dispose() {
        if (this.zoomBox && this.zoomBox.parentElement) {
            this.zoomBox.parentElement.removeChild(this.zoomBox);
        }
    }
}
exports.InputHandler = InputHandler;
};

// ── module: src/hud.ts ──
__mods["src/hud.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.HUD = void 0;
const state_1 = require("./state");
const palette_1 = require("./palette");
class HUD {
    constructor(container, state) {
        this.hideTimer = null;
        this.paletteSlider = null;
        this.iterSlider = null;
        // Callbacks from renderer
        this.requestRender = (() => { });
        this.requestRecolorize = (() => { });
        this.container = container;
        this.state = state;
        this.build();
    }
    build() {
        const hud = document.createElement('div');
        hud.id = 'hud';
        hud.className = 'hud-panel';
        hud.innerHTML = `
      <div class="hud-header">
        <span class="hud-title">Fractal Explorer</span>
        <div class="hud-controls">
          <button id="btn-back" title="Back (Zoom History)" aria-label="Back">◀</button>
          <button id="btn-reset" title="Reset View" aria-label="Reset">⟲</button>
          <button id="btn-bookmark" title="Bookmark View" aria-label="Bookmark">★</button>
        </div>
      </div>
      
      <div class="hud-stats">
        <div class="stat-row">
          <span class="stat-label">Magnification:</span>
          <span id="stat-mag" class="stat-value">×250</span>
        </div>
        <div class="stat-row">
          <span class="stat-label">Center:</span>
          <span id="stat-center" class="stat-value">-0.5 + 0i</span>
        </div>
        <div class="stat-row">
          <span class="stat-label">Mode:</span>
          <span id="stat-mode" class="stat-value">Mandelbrot</span>
        </div>
        <div class="stat-row">
          <span class="stat-label">Iterations:</span>
          <span id="stat-iter" class="stat-value">200</span>
        </div>
        <div class="stat-row">
          <span class="stat-label">FPS:</span>
          <span id="stat-fps" class="stat-value">—</span>
        </div>
      </div>

      <div class="hud-palettes" id="palette-selector">
        <span class="hud-section-label">Palette:</span>
        <div class="palette-chips" id="palette-chips"></div>
      </div>

      <div class="hud-sliders">
        <div class="slider-row">
          <label for="slider-offset">Color Offset</label>
          <input type="range" id="slider-offset" min="0" max="1" step="0.001" value="0">
        </div>
        <div class="slider-row">
          <label for="slider-maxiter">Max Iterations</label>
          <input type="range" id="slider-maxiter" min="50" max="10000" step="10" value="200">
          <span id="iter-val">200</span>
        </div>
      </div>

      <div id="bookmarks-panel" class="bookmarks-panel" style="display:none;">
        <span class="hud-section-label">Bookmarks:</span>
        <div id="bookmark-list" class="bookmark-list"></div>
      </div>

      <div class="hud-help">
        <span class="help-text">Drag: zoom | Scroll: zoom to cursor | ←→↑↓: pan | +/-: zoom | J: toggle Julia</span>
      </div>
    `;
        this.container.appendChild(hud);
        this.setupPaletteChips();
        this.setupControls();
    }
    setupPaletteChips() {
        const chipsContainer = document.getElementById('palette-chips');
        if (!chipsContainer)
            return;
        palette_1.PALETTE_NAMES.forEach((name, idx) => {
            const chip = document.createElement('button');
            chip.className = `palette-chip ${idx === this.state.view.palette ? 'active' : ''}`;
            chip.textContent = name;
            chip.title = `${name} (click to select)`;
            chip.setAttribute('aria-label', `Select ${name} palette`);
            chip.dataset.palette = String(idx);
            chip.addEventListener('click', () => {
                this.state.view.palette = idx;
                this.updatePaletteChips();
                this.requestRecolorize();
            });
            chipsContainer.appendChild(chip);
        });
    }
    updatePaletteChips() {
        const chips = document.querySelectorAll('.palette-chip');
        chips.forEach((chip, idx) => {
            chip.classList.toggle('active', idx === this.state.view.palette);
        });
    }
    setupControls() {
        // Back button
        const backBtn = document.getElementById('btn-back');
        backBtn?.addEventListener('click', () => {
            const prev = (0, state_1.popZoomHistory)(this.state);
            if (prev) {
                Object.assign(this.state.view, prev);
                this.update();
                this.requestRender();
            }
        });
        // Reset button
        const resetBtn = document.getElementById('btn-reset');
        resetBtn?.addEventListener('click', () => {
            this.state.view.re = -0.5;
            this.state.view.im = 0;
            this.state.view.scale = 250;
            this.state.view.setMode = 'mandelbrot';
            this.state.view.maxIter = (0, state_1.computeMaxIter)(250);
            this.state.view.paletteOffset = 0;
            this.state.view.palette = 0;
            this.update();
            this.requestRender();
        });
        // Bookmark button
        const bookmarkBtn = document.getElementById('btn-bookmark');
        const bmPanel = document.getElementById('bookmarks-panel');
        bookmarkBtn?.addEventListener('click', () => {
            if (bmPanel.style.display === 'none') {
                bmPanel.style.display = 'block';
                this.renderBookmarks();
            }
            else {
                bmPanel.style.display = 'none';
            }
        });
        // Color offset slider
        const offsetSlider = document.getElementById('slider-offset');
        offsetSlider?.addEventListener('input', () => {
            this.state.view.paletteOffset = parseFloat(offsetSlider.value);
            this.requestRecolorize();
        });
        // Max iterations slider
        const iterSlider = document.getElementById('slider-maxiter');
        iterSlider?.addEventListener('input', () => {
            this.state.view.maxIter = parseInt(iterSlider.value, 10);
            this.requestRender();
        });
    }
    renderBookmarks() {
        const list = document.getElementById('bookmark-list');
        if (!list)
            return;
        list.innerHTML = '';
        const names = (0, state_1.listBookmarks)();
        if (names.length === 0) {
            list.innerHTML = '<div class="bm-empty">No bookmarks saved</div>';
            return;
        }
        // Save current view
        const doSave = () => {
            const name = prompt('Bookmark name:');
            if (name) {
                (0, state_1.saveBookmark)(name, structuredClone(this.state.view));
                this.renderBookmarks();
            }
        };
        const saveBtn = document.createElement('button');
        saveBtn.className = 'bm-btn';
        saveBtn.textContent = 'Save Current';
        saveBtn.addEventListener('click', doSave);
        list.appendChild(saveBtn);
        names.forEach(name => {
            const btn = document.createElement('button');
            btn.className = 'bm-btn';
            btn.textContent = name;
            btn.addEventListener('click', () => {
                const bm = (0, state_1.loadBookmark)(name);
                if (bm) {
                    Object.assign(this.state.view, bm);
                    this.update();
                    this.requestRender();
                }
            });
            const del = document.createElement('button');
            del.className = 'bm-del';
            del.textContent = '×';
            del.addEventListener('click', () => {
                (0, state_1.deleteBookmark)(name);
                this.renderBookmarks();
            });
            btn.appendChild(del);
            list.appendChild(btn);
        });
    }
    update() {
        const v = this.state.view;
        const mag = document.getElementById('stat-mag');
        if (mag)
            mag.textContent = `×${v.scale.toExponential(2)}`;
        const center = document.getElementById('stat-center');
        if (center)
            center.textContent = `${v.re.toExponential(4)} ${v.im >= 0 ? '+' : ''}${v.im.toExponential(4)}i`;
        const mode = document.getElementById('stat-mode');
        if (mode)
            mode.textContent = v.setMode === 'mandelbrot' ? 'Mandelbrot' : `Julia (${v.juliaC.re.toFixed(4)}, ${v.juliaC.im.toFixed(4)})`;
        const iter = document.getElementById('stat-iter');
        if (iter)
            iter.textContent = String(v.maxIter);
        const iterSlider = document.getElementById('slider-maxiter');
        if (iterSlider)
            iterSlider.value = String(v.maxIter);
        const iterVal = document.getElementById('iter-val');
        if (iterVal)
            iterVal.textContent = String(v.maxIter);
        const offsetSlider = document.getElementById('slider-offset');
        if (offsetSlider)
            offsetSlider.value = String(v.paletteOffset);
        this.updatePaletteChips();
        this.resetHideTimer();
    }
    updateFps(fps) {
        const el = document.getElementById('stat-fps');
        if (el)
            el.textContent = String(Math.round(fps));
    }
    updateProgress(progress) {
        this.resetHideTimer();
    }
    // Auto-hide
    resetHideTimer() {
        if (this.hideTimer)
            clearTimeout(this.hideTimer);
        const hud = document.getElementById('hud');
        if (hud)
            hud.classList.remove('hud-hidden');
        this.hideTimer = window.setTimeout(() => {
            if (hud)
                hud.classList.add('hud-hidden');
        }, 4000);
    }
    setRenderCallback(cb) { this.requestRender = cb; }
    setRecolorizeCallback(cb) { this.requestRecolorize = cb; }
}
exports.HUD = HUD;
};

// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"ThinkingCap-Qwen3.6-27B-GGUF","provider":"brigid","persona":"typescript","sessionId":"095f9ee8-e1e4-489e-a47b-c1c8bfb82007","tokensIn":1169758,"tokensOut":26680,"tokensTotal":1196438,"cost":0,"turns":46,"toolCalls":46,"failedToolCalls":0,"timestamp":"2026-08-03T19:42:35.634Z"} -->