← Fractal Explorer results

Fractal Explorer

ProCreations grug 27b GGUF IQ4 · 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 35,382 bytes · SHA-256 791586ecc7fd
<!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: style ── */
html, body { margin: 0; padding: 0; overflow: hidden; width: 100%; height: 100%; background: #0a0a0a; font-family: 'Segoe UI', sans-serif; }
canvas { display: block; width: 100%; height: 100%; }
.glass-panel { background: rgba(0,0,0,0.7); backdrop-filter: blur(8px); border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; color: #fff; font-size: 13px; padding: 12px; }
.hidden { display: none; }
#hud { position: absolute; top: 16px; left: 50%; transform: translateX(-50%); text-align: center; pointer-events: none; }
.controls-panel { position: absolute; top: 16px; left: 16px; width: 220px; z-index: 10; }
.control-row { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; flex-wrap: wrap; }
.control-row label { width: 100px; font-size: 12px; }
.glass-button { background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2); border-radius: 4px; padding: 6px 12px; color: #fff; cursor: pointer; font-size: 12px; }
.glass-button:hover { background: rgba(255,255,255,0.2); }
input[type="range"] { flex: 1; }
.switch { position: relative; display: inline-block; width: 40px; height: 20px; }
.switch input { opacity: 0; width: 0; height: 0; }
.slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background: rgba(255,255,255,0.2); transition: 0.3s; border-radius: 20px; }
.slider:before { position: absolute; content: ""; height: 16px; width: 16px; left: 2px; bottom: 2px; background: #fff; transition: 0.3s; border-radius: 50%; }
input:checked + .slider { background: #4CAF50; }
input:checked + .slider:before { transform: translateX(20px); }
.modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 100; display: none; align-items: center; justify-content: center; }
.modal.show { display: flex; }
.modal-content { background: #1a1a1a; padding: 20px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.2); }
.bookmarks-panel { position: absolute; top: 16px; right: 16px; width: 200px; z-index: 10; max-height: 400px; overflow-y: auto; }
.bookmark-item { padding: 8px; margin-bottom: 4px; background: rgba(255,255,255,0.1); border-radius: 4px; cursor: pointer; }
.bookmark-item:hover { background: rgba(255,255,255,0.2); }
#toast { position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); background: rgba(0,0,0,0.8); padding: 12px 24px; border-radius: 8px; color: #fff; opacity: 0; transition: opacity 0.3s; z-index: 1000; pointer-events: none; }
#toast.show { opacity: 1; }
.bookmark-name-input { width: 150px; padding: 6px; margin-bottom: 8px; background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2); border-radius: 4px; color: #fff; }
.delete-bookmark-btn { float: right; background: rgba(255,0,0,0.3); border: 1px solid rgba(255,0,0,0.5); border-radius: 4px; padding: 2px 6px; color: #fff; cursor: pointer; font-size: 10px; }
.delete-bookmark-btn:hover { background: rgba(255,0,0,0.5); }
</style>
</head>
<body>
<div id="app"><canvas id="fractal-canvas"></canvas>
<div id="hud" class="glass-panel hidden"><span id="hud-text"></span></div>
<div id="controls" class="controls-panel glass-panel"><div class="control-row"><label for="zoom-in">Zoom</label><button id="zoom-in" class="glass-button">+</button><button id="zoom-out" class="glass-button">−</button></div><div class="control-row"><label for="reset-view">Reset</label><button id="reset-view" class="glass-button">Reset view</button></div><div class="control-row"><label for="back-zoom">Back</label><button id="back-zoom" class="glass-button">Back</button></div><div class="control-row"><label for="palette-select">Palette</label><button id="palette-select" class="glass-button">Fire</button></div><div class="control-row"><label for="iter-slider">Max iterations</label><input type="range" id="iter-slider" min="100" max="10000" step="10" value="1000"><span id="iter-display">1000</span></div><div class="control-row"><label for="color-offset">Color drift</label><input type="range" id="color-offset" min="0" max="360" step="1" value="0"><span id="offset-display">0°</span></div><div class="control-row"><label for="smooth-toggle">Smooth coloring</label><label class="switch"><input type="checkbox" id="smooth-toggle" checked=""><span class="slider"></span></label></div><div class="control-row"><label for="julia-toggle">Julia mode</label><label class="switch"><input type="checkbox" id="julia-toggle"><span class="slider"></span></label></div></div>
<div id="save-bookmark-dialog" class="modal"><div class="modal-content"><input type="text" id="bookmark-name" placeholder="Bookmark name"><button id="save-bookmark-btn" class="glass-button">Save</button><button id="cancel-bookmark-btn" class="glass-button">Cancel</button></div></div>
<div id="bookmarks-list" class="bookmarks-panel glass-panel"></div>
<div id="toast"></div></div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./engine":"src/engine.ts","./types":"src/types.ts","./constants":"src/constants.ts","./ui":"src/ui.ts"},"src/constants.ts":{"./types":"src/types.ts"},"src/engine.ts":{"./types":"src/types.ts","./constants":"src/constants.ts"},"src/ui.ts":{"./engine":"src/engine.ts","./types":"src/types.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 engine_1 = require("./engine");
const types_1 = require("./types");
const constants_1 = require("./constants");
const ui_1 = require("./ui");
function start() {
    const canvas = document.getElementById('fractal-canvas');
    const hud = document.getElementById('hud');
    const hudText = document.getElementById('hud-text');
    const view = types_1.FractalView.fromSavedOrDefault();
    const engine = new engine_1.FractalEngine(canvas, view, constants_1.PALETTES);
    const ui = new ui_1.FractalUI(canvas, engine, hud, hudText, constants_1.PALETTES);
    // @ts-ignore: debug interface declared in debug.d.ts, but global Window needs runtime assignment
    window.fractal = {
        iterationsAt: engine.iterationsAt.bind(engine),
        setView: (v) => {
            const newView = new types_1.FractalView({
                re: v.re,
                im: v.im,
                scale: Math.min(v.scale, constants_1.MAX_ZOOM_MAGNIFICATION),
                set: view.set,
                juliaC: view.juliaC,
                palette: view.palette,
                maxIterations: view.maxIterations,
                smoothColoring: view.smoothColoring,
            });
            engine.setView(newView);
            engine.render();
        },
        engine,
    };
    ui.init();
    engine.render();
}
};

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

// ── module: src/constants.ts ──
__mods["src/constants.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PALETTES = exports.DEFAULT_VIEW = exports.JULIA_PREVIEW_SCALE = exports.ZOOM_SPEED = exports.ITERATIONS_ZOOM_SCALE = exports.BASE_ITERATIONS = exports.MAX_ZOOM_MAGNIFICATION = void 0;
const types_1 = require("./types");
exports.MAX_ZOOM_MAGNIFICATION = 1e14;
exports.BASE_ITERATIONS = 1000;
exports.ITERATIONS_ZOOM_SCALE = 10;
exports.ZOOM_SPEED = 1.5;
exports.JULIA_PREVIEW_SCALE = 0.3;
exports.DEFAULT_VIEW = new types_1.FractalView({
    re: -0.5,
    im: 0,
    scale: 3.5,
    set: types_1.FractalSet.MANDELBROT,
    juliaC: { re: -0.7269, im: 0.1889 },
    palette: 'fire',
    maxIterations: exports.BASE_ITERATIONS,
    smoothColoring: true,
});
const fireColors = [
    { offset: 0, r: 0, g: 0, b: 0 },
    { offset: 0.2, r: 80, g: 0, b: 0 },
    { offset: 0.4, r: 255, g: 69, b: 0 },
    { offset: 0.6, r: 255, g: 165, b: 0 },
    { offset: 0.8, r: 255, g: 255, b: 0 },
    { offset: 1, r: 255, g: 255, b: 255 },
];
const oceanColors = [
    { offset: 0, r: 0, g: 0, b: 0 },
    { offset: 0.2, r: 0, g: 0, b: 100 },
    { offset: 0.4, r: 0, g: 60, b: 160 },
    { offset: 0.6, r: 0, g: 130, b: 220 },
    { offset: 0.8, r: 60, g: 190, b: 255 },
    { offset: 1, r: 200, g: 255, b: 255 },
];
const monochromeColors = [
    { offset: 0, r: 0, g: 0, b: 0 },
    { offset: 0.5, r: 180, g: 180, b: 180 },
    { offset: 1, r: 255, g: 255, b: 255 },
];
const psychedelicColors = [
    { offset: 0, r: 128, g: 0, b: 128 },
    { offset: 0.166, r: 0, g: 0, b: 255 },
    { offset: 0.333, r: 0, g: 128, b: 128 },
    { offset: 0.5, r: 0, g: 255, b: 0 },
    { offset: 0.666, r: 255, g: 128, b: 0 },
    { offset: 0.833, r: 255, g: 0, b: 0 },
    { offset: 1, r: 128, g: 0, b: 128 },
];
const grayscaleColors = [
    { offset: 0, r: 0, g: 0, b: 0 },
    { offset: 0.3, r: 40, g: 40, b: 40 },
    { offset: 0.5, r: 100, g: 100, b: 100 },
    { offset: 0.7, r: 180, g: 180, b: 180 },
    { offset: 1, r: 255, g: 255, b: 255 },
];
exports.PALETTES = {
    fire: { name: 'Fire', stops: fireColors },
    ocean: { name: 'Ocean', stops: oceanColors },
    monochrome: { name: 'Monochrome', stops: monochromeColors },
    psychedelic: { name: 'Psychedelic', stops: psychedelicColors },
    grayscale: { name: 'Grayscale', stops: grayscaleColors },
};
};

// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.FractalView = exports.FractalSet = void 0;
var FractalSet;
(function (FractalSet) {
    FractalSet["MANDELBROT"] = "mandelbrot";
    FractalSet["JULIA"] = "julia";
})(FractalSet || (exports.FractalSet = FractalSet = {}));
class FractalView {
    constructor(params) {
        this.re = params.re;
        this.im = params.im;
        this.scale = params.scale;
        this.set = params.set;
        this.juliaC = params.juliaC;
        this.palette = params.palette;
        this.maxIterations = params.maxIterations;
        this.smoothColoring = params.smoothColoring;
    }
    static fromSavedOrDefault() {
        try {
            const saved = localStorage.getItem('fractalView');
            if (saved) {
                const parsed = JSON.parse(saved);
                return new FractalView({
                    re: parsed.re || -0.5,
                    im: parsed.im || 0,
                    scale: parsed.scale || 3.5,
                    set: parsed.set || FractalSet.MANDELBROT,
                    juliaC: parsed.juliaC || { re: -0.7269, im: 0.1889 },
                    palette: parsed.palette || 'fire',
                    maxIterations: parsed.maxIterations || 1000,
                    smoothColoring: parsed.smoothColoring !== false,
                });
            }
        }
        catch (e) {
            console.warn('Failed to load saved view:', e);
        }
        return new FractalView({
            re: -0.5,
            im: 0,
            scale: 3.5,
            set: FractalSet.MANDELBROT,
            juliaC: { re: -0.7269, im: 0.1889 },
            palette: 'fire',
            maxIterations: 1000,
            smoothColoring: true,
        });
    }
    static fromBookmark(bookmark) {
        return new FractalView({
            re: bookmark.re,
            im: bookmark.im,
            scale: bookmark.scale,
            set: bookmark.set || FractalSet.MANDELBROT,
            juliaC: bookmark.juliaC || { re: -0.7269, im: 0.1889 },
            palette: bookmark.palette || 'fire',
            maxIterations: bookmark.maxIterations || 1000,
            smoothColoring: bookmark.smoothColoring !== false,
        });
    }
    clone() {
        return new FractalView({
            re: this.re,
            im: this.im,
            scale: this.scale,
            set: this.set,
            juliaC: { re: this.juliaC.re, im: this.juliaC.im },
            palette: this.palette,
            maxIterations: this.maxIterations,
            smoothColoring: this.smoothColoring,
        });
    }
}
exports.FractalView = FractalView;
};

// ── module: src/engine.ts ──
__mods["src/engine.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.FractalEngine = void 0;
const types_1 = require("./types");
const constants_1 = require("./constants");
const ZOOM_HIST_MAX = 50;
class FractalEngine {
    constructor(canvas, view, palettes) {
        this.currentPass = 0;
        this.cancelRender = false;
        this.zoomHistory = [];
        this.renderStart = 0;
        this.fps = 0;
        this.frameCount = 0;
        this.fpsAccum = 0;
        this.lastFPSTime = 0;
        this.canvas = canvas;
        this.ctx = canvas.getContext('2d');
        this.view = view;
        this.resize();
        this.imageData = this.ctx.createImageData(this.canvas.width, this.canvas.height);
        this.pixels = this.imageData.data;
        this.palettes = palettes;
        this.lastFPSTime = performance.now();
        this.zoomHistory.push(this.view.clone());
        window.addEventListener('resize', () => this.resize());
    }
    resize() {
        const width = window.innerWidth;
        const height = window.innerHeight;
        this.canvas.width = width;
        this.canvas.height = height;
        this.imageData = this.ctx.createImageData(width, height);
        this.pixels = this.imageData.data;
        window.addEventListener('resize', () => this.resize());
    }
    forceResize() {
        this.canvas.width = window.innerWidth;
        this.canvas.height = window.innerHeight;
        this.imageData = this.ctx.createImageData(this.canvas.width, this.canvas.height);
        this.pixels = this.imageData.data;
    }
    getView() {
        return this.view;
    }
    setView(view) {
        this.cancelRender = true;
        const newView = view.clone();
        newView.scale = Math.min(newView.scale, constants_1.MAX_ZOOM_MAGNIFICATION);
        this.view = newView;
        this.zoomHistory.push(newView);
        if (this.zoomHistory.length > ZOOM_HIST_MAX) {
            this.zoomHistory.shift();
        }
        this.render();
    }
    setPalette(name) {
        this.cancelRender = true;
        this.view.palette = name;
        this.render();
    }
    setColorOffset(offset) {
        this.view = this.view.clone();
        this.render();
    }
    toggleJulia(c) {
        this.cancelRender = true;
        const newView = this.view.clone();
        newView.set = this.view.set === types_1.FractalSet.MANDELBROT ? types_1.FractalSet.JULIA : types_1.FractalSet.MANDELBROT;
        newView.juliaC = c;
        this.setView(newView);
    }
    back() {
        if (this.zoomHistory.length > 1) {
            this.cancelRender = true;
            this.zoomHistory.pop();
            this.view = this.zoomHistory[this.zoomHistory.length - 1].clone();
            this.render();
        }
    }
    resetView() {
        this.cancelRender = true;
        this.view = new types_1.FractalView({
            re: -0.5,
            im: 0,
            scale: 3.5,
            set: types_1.FractalSet.MANDELBROT,
            juliaC: { re: -0.7269, im: 0.1889 },
            palette: this.view.palette,
            maxIterations: this.view.maxIterations,
            smoothColoring: this.view.smoothColoring,
        });
        this.zoomHistory = [this.view.clone()];
        this.render();
    }
    iterationsAt(re, im) {
        const maxIter = this.view.maxIterations;
        let zr = re;
        let zi = im;
        const cr = this.view.set === types_1.FractalSet.JULIA ? this.view.juliaC.re : re;
        const ci = this.view.set === types_1.FractalSet.JULIA ? this.view.juliaC.im : im;
        let i = 0;
        while (zr * zr + zi * zi < 4 && i < maxIter) {
            const tmp = zr * zr - zi * zi + cr;
            zi = 2 * zr * zi + ci;
            zr = tmp;
            i++;
        }
        return i;
    }
    saveView() {
        const data = {
            re: this.view.re,
            im: this.view.im,
            scale: this.view.scale,
            set: this.view.set,
            juliaC: this.view.juliaC,
            palette: this.view.palette,
            maxIterations: this.view.maxIterations,
            smoothColoring: this.view.smoothColoring,
        };
        localStorage.setItem('fractalView', JSON.stringify(data));
    }
    getFPS() {
        return this.fps;
    }
    zoomAtRect(x1, y1, x2, y2) {
        this.cancelRender = true;
        const view = this.view.clone();
        const left = Math.min(x1, x2);
        const right = Math.max(x1, x2);
        const top = Math.min(y1, y2);
        const bottom = Math.max(y1, y2);
        const w = right - left;
        const h = bottom - top;
        if (w < 4 || h < 4)
            return;
        const rectCenterRe = view.re + (left / this.canvas.width - 0.5) * view.scale;
        const rectCenterIm = view.im + (top / this.canvas.height - 0.5) * view.scale;
        const oldScaleRe = view.scale / this.canvas.width;
        const newScaleRe = w * oldScaleRe;
        const oldScaleIm = view.scale / this.canvas.height;
        const newScaleIm = h * oldScaleIm;
        view.re = rectCenterRe;
        view.im = rectCenterIm;
        view.scale = newScaleRe;
        this.setView(view);
    }
    zoomAtCursor(px, py, factor) {
        this.cancelRender = true;
        const view = this.view.clone();
        const oldCenterRe = view.re + (px / this.canvas.width - 0.5) * view.scale;
        const oldCenterIm = view.im + (py / this.canvas.height - 0.5) * view.scale;
        view.scale /= factor;
        view.re = oldCenterRe - (px / this.canvas.width - 0.5) * view.scale;
        view.im = oldCenterIm - (py / this.canvas.height - 0.5) * view.scale;
        this.setView(view);
    }
    panByPixels(dx, dy) {
        this.cancelRender = true;
        const view = this.view.clone();
        view.re += (dx / this.canvas.width) * view.scale;
        view.im += (dy / this.canvas.height) * view.scale;
        this.setView(view);
    }
    render() {
        this.cancelRender = false;
        this.renderStart = performance.now();
        this.currentPass = 0;
        this.renderNextPass();
    }
    renderNextPass() {
        if (this.cancelRender)
            return;
        const view = this.view;
        const paletteName = view.palette;
        const palette = this.palettes[paletteName] || this.palettes.fire;
        const smooth = view.smoothColoring;
        const maxIter = view.maxIterations;
        const scale = view.scale / this.canvas.width;
        const pass = this.currentPass;
        const step = 1 << pass;
        const offsetX = 0;
        const offsetY = 0;
        const left = view.re - view.scale / 2 + offsetX;
        const top = view.im - view.scale / 2 + offsetY;
        const juliaSet = view.set === types_1.FractalSet.JULIA;
        const cr = view.juliaC.re;
        const ci = view.juliaC.im;
        let i = 0;
        const dataLen = this.pixels.length;
        for (let y = offsetY; y < this.canvas.height; y += step) {
            for (let x = offsetX; x < this.canvas.width; x += step) {
                const re = left + x * scale;
                const im = top + y * (view.scale / this.canvas.height);
                let zr = juliaSet ? 0 : re;
                let zi = juliaSet ? 0 : im;
                let iter = 0;
                let smoothIter = iter;
                while (zr * zr + zi * zi < 4 && iter < maxIter) {
                    const tmp = zr * zr - zi * zi + (juliaSet ? cr : re);
                    zi = 2 * zr * zi + (juliaSet ? ci : im);
                    zr = tmp;
                    iter++;
                }
                if (smooth && iter < maxIter) {
                    const logZn = Math.log(zr * zr + zi * zi) * 0.5;
                    smoothIter = iter + 1 - Math.log(logZn) / Math.log(2);
                }
                if (iter < maxIter) {
                    const normalized = smoothIter / maxIter;
                    const color = this.sampleColor(palette.stops, normalized);
                    const r = color.r;
                    const g = color.g;
                    const b = color.b;
                    for (let dy = 0; dy < step && y + dy < this.canvas.height; dy++) {
                        for (let dx = 0; dx < step && x + dx < this.canvas.width; dx++) {
                            const pos = ((y + dy) * this.canvas.width + x + dx) * 4;
                            if (pos + 3 < dataLen) {
                                this.pixels[pos] = r;
                                this.pixels[pos + 1] = g;
                                this.pixels[pos + 2] = b;
                                this.pixels[pos + 3] = 255;
                            }
                        }
                    }
                }
                else {
                    for (let dy = 0; dy < step && y + dy < this.canvas.height; dy++) {
                        for (let dx = 0; dx < step && x + dx < this.canvas.width; dx++) {
                            const pos = ((y + dy) * this.canvas.width + x + dx) * 4;
                            if (pos + 3 < dataLen) {
                                this.pixels[pos] = 0;
                                this.pixels[pos + 1] = 0;
                                this.pixels[pos + 2] = 0;
                                this.pixels[pos + 3] = 255;
                            }
                        }
                    }
                }
                i++;
            }
        }
        this.ctx.putImageData(this.imageData, 0, 0);
        this.frameCount++;
        const now = performance.now();
        const delta = now - this.lastFPSTime;
        if (delta >= 1000) {
            this.fps = Math.round((this.frameCount * 1000) / delta);
            this.frameCount = 0;
            this.lastFPSTime = now;
        }
        if (pass < 2) {
            this.currentPass++;
            this.renderNextPass();
        }
    }
    sampleColor(stops, t) {
        if (stops.length === 0)
            return { r: 0, g: 0, b: 0 };
        if (t <= 0)
            return { r: stops[0].r, g: stops[0].g, b: stops[0].b };
        if (t >= 1)
            return { r: stops[stops.length - 1].r, g: stops[stops.length - 1].g, b: stops[stops.length - 1].b };
        for (let i = 0; i < stops.length - 1; i++) {
            const stop1 = stops[i];
            const stop2 = stops[i + 1];
            if (t >= stop1.offset && t <= stop2.offset) {
                const localT = (t - stop1.offset) / (stop2.offset - stop1.offset);
                return {
                    r: Math.round(stop1.r + (stop2.r - stop1.r) * localT),
                    g: Math.round(stop1.g + (stop2.g - stop1.g) * localT),
                    b: Math.round(stop1.b + (stop2.b - stop1.b) * localT),
                };
            }
        }
        return { r: 0, g: 0, b: 0 };
    }
}
exports.FractalEngine = FractalEngine;
};

// ── module: src/ui.ts ──
__mods["src/ui.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.FractalUI = void 0;
const types_1 = require("./types");
const BOOKMARKS_KEY = 'fractalBookmarks';
class FractalUI {
    constructor(canvas, engine, hud, hudText, palettes) {
        this.currentPaletteIndex = 0;
        this.isDragging = false;
        this.dragStartX = 0;
        this.dragStartY = 0;
        this.viewChanged = false;
        this.renderRequest = null;
        this.canvas = canvas;
        this.engine = engine;
        this.hud = hud;
        this.hudText = hudText;
        this.controlsPanel = document.getElementById('controls');
        this.zoomInBtn = document.getElementById('zoom-in');
        this.zoomOutBtn = document.getElementById('zoom-out');
        this.resetBtn = document.getElementById('reset-view');
        this.backBtn = document.getElementById('back-zoom');
        this.paletteBtn = document.getElementById('palette-select');
        this.iterSlider = document.getElementById('iter-slider');
        this.iterDisplay = document.getElementById('iter-display');
        this.colorOffsetSlider = document.getElementById('color-offset');
        this.offsetDisplay = document.getElementById('offset-display');
        this.smoothToggle = document.getElementById('smooth-toggle');
        this.juliaToggle = document.getElementById('julia-toggle');
        this.saveBookmarkDialog = document.getElementById('save-bookmark-dialog');
        this.bookmarkNameInput = document.getElementById('bookmark-name');
        this.saveBookmarkBtn = document.getElementById('save-bookmark-btn');
        this.cancelBookmarkBtn = document.getElementById('cancel-bookmark-btn');
        this.bookmarksPanel = document.getElementById('bookmarks-list');
        this.toast = document.getElementById('toast');
        this.palettes = palettes;
        this.init();
    }
    init() {
        const view = this.engine.getView();
        this.iterSlider.value = view.maxIterations.toString();
        this.iterDisplay.textContent = view.maxIterations.toString();
        this.smoothToggle.checked = view.smoothColoring;
        if (view.set === types_1.FractalSet.JULIA) {
            this.juliaToggle.checked = true;
        }
        const paletteNames = Object.keys(this.palettes);
        this.currentPaletteIndex = paletteNames.indexOf(view.palette);
        this.paletteBtn.textContent = this.palettes[view.palette].name;
        this.iterSlider.addEventListener('input', () => {
            const value = parseInt(this.iterSlider.value);
            this.iterDisplay.textContent = value.toString();
            const newView = view.clone();
            newView.maxIterations = value;
            this.engine.setView(newView);
        });
        this.colorOffsetSlider.addEventListener('input', () => {
            const value = parseInt(this.colorOffsetSlider.value);
            this.offsetDisplay.textContent = value + '°';
            this.engine.setColorOffset(value);
        });
        this.smoothToggle.addEventListener('change', () => {
            const newView = view.clone();
            newView.smoothColoring = this.smoothToggle.checked;
            this.engine.setView(newView);
        });
        this.juliaToggle.addEventListener('change', () => {
            if (this.juliaToggle.checked && view.set === types_1.FractalSet.MANDELBROT) {
                const c = { re: view.re, im: view.im };
                this.engine.toggleJulia(c);
            }
            else if (!this.juliaToggle.checked && view.set === types_1.FractalSet.JULIA) {
                this.engine.setView(view.clone());
            }
        });
        this.zoomInBtn.addEventListener('click', () => {
            const centerX = this.canvas.width / 2;
            const centerY = this.canvas.height / 2;
            this.engine.zoomAtCursor(centerX, centerY, 1.5);
        });
        this.zoomOutBtn.addEventListener('click', () => {
            const centerX = this.canvas.width / 2;
            const centerY = this.canvas.height / 2;
            this.engine.zoomAtCursor(centerX, centerY, 1 / 1.5);
        });
        this.resetBtn.addEventListener('click', () => {
            this.engine.resetView();
        });
        this.backBtn.addEventListener('click', () => {
            this.engine.back();
        });
        this.paletteBtn.addEventListener('click', () => {
            const paletteNames = Object.keys(this.palettes);
            this.currentPaletteIndex = (this.currentPaletteIndex + 1) % paletteNames.length;
            const name = paletteNames[this.currentPaletteIndex];
            this.paletteBtn.textContent = this.palettes[name].name;
            this.engine.setPalette(name);
        });
        this.saveBookmarkBtn.addEventListener('click', () => {
            const name = this.bookmarkNameInput.value;
            if (name) {
                this.saveBookmark(name);
                this.saveBookmarkDialog.classList.remove('show');
                this.showToast('Bookmark saved!');
            }
        });
        this.cancelBookmarkBtn.addEventListener('click', () => {
            this.saveBookmarkDialog.classList.remove('show');
        });
        this.canvas.addEventListener('mousedown', (e) => {
            this.isDragging = true;
            this.dragStartX = e.clientX;
            this.dragStartY = e.clientY;
            this.viewChanged = false;
        });
        this.canvas.addEventListener('mousemove', (e) => {
            if (this.isDragging) {
                this.viewChanged = true;
            }
        });
        this.canvas.addEventListener('mouseup', (e) => {
            this.isDragging = false;
            if (!this.viewChanged && e.button === 0) {
                const rect = this.canvas.getBoundingClientRect();
                const x = e.clientX - rect.left;
                const y = e.clientY - rect.top;
                const view = this.engine.getView();
                const re = view.re + (x / this.canvas.width - 0.5) * view.scale;
                const im = view.im + (y / this.canvas.height - 0.5) * view.scale;
                this.engine.toggleJulia({ re, im });
            }
        });
        this.canvas.addEventListener('wheel', (e) => {
            e.preventDefault();
            const rect = this.canvas.getBoundingClientRect();
            const x = e.clientX - rect.left;
            const y = e.clientY - rect.top;
            const factor = e.deltaY > 0 ? 1.2 : 1 / 1.2;
            this.engine.zoomAtCursor(x, y, factor);
        }, { passive: false });
        window.addEventListener('keydown', (e) => {
            switch (e.key) {
                case 'ArrowLeft':
                    this.engine.panByPixels(20, 0);
                    break;
                case 'ArrowRight':
                    this.engine.panByPixels(-20, 0);
                    break;
                case 'ArrowUp':
                    this.engine.panByPixels(0, 20);
                    break;
                case 'ArrowDown':
                    this.engine.panByPixels(0, -20);
                    break;
                case '+':
                case '=':
                    const centerX = this.canvas.width / 2;
                    const centerY = this.canvas.height / 2;
                    this.engine.zoomAtCursor(centerX, centerY, 1.5);
                    break;
                case '-':
                    const cx = this.canvas.width / 2;
                    const cy = this.canvas.height / 2;
                    this.engine.zoomAtCursor(cx, cy, 1 / 1.5);
                    break;
                case 'r':
                    this.engine.resetView();
                    break;
                case 'b':
                    this.engine.back();
                    break;
                case 'j':
                    if (view.set === types_1.FractalSet.MANDELBROT) {
                        const c = { re: view.re, im: view.im };
                        this.engine.toggleJulia(c);
                    }
                    else {
                        this.engine.setView(view.clone());
                    }
                    break;
                case 's':
                    this.saveBookmarkDialog.classList.add('show');
                    break;
            }
        });
        this.updateHUD();
        this.updateBookmarks();
        setInterval(() => this.updateHUD(), 2000);
    }
    updateHUD() {
        const view = this.engine.getView();
        const fps = this.engine.getFPS();
        this.hudText.innerHTML =
            `Scale: ${view.scale.toFixed(2)} | Center: (${view.re.toFixed(6)}, ${view.im.toFixed(6)}) | Iterations: ${view.maxIterations} | FPS: ${fps}`;
        this.hud.classList.remove('hidden');
    }
    saveBookmark(name) {
        const view = this.engine.getView();
        const data = {
            name,
            re: view.re,
            im: view.im,
            scale: view.scale,
            set: view.set,
            juliaC: view.juliaC,
            palette: view.palette,
            maxIterations: view.maxIterations,
            smoothColoring: view.smoothColoring,
        };
        let bookmarks = [];
        try {
            const saved = localStorage.getItem(BOOKMARKS_KEY);
            if (saved) {
                bookmarks = JSON.parse(saved);
            }
        }
        catch (e) {
            console.warn('Failed to load bookmarks:', e);
        }
        bookmarks.push(data);
        localStorage.setItem(BOOKMARKS_KEY, JSON.stringify(bookmarks));
        this.updateBookmarks();
    }
    updateBookmarks() {
        this.bookmarksPanel.innerHTML = '';
        let bookmarks = [];
        try {
            const saved = localStorage.getItem(BOOKMARKS_KEY);
            if (saved) {
                bookmarks = JSON.parse(saved);
            }
        }
        catch (e) {
            console.warn('Failed to load bookmarks:', e);
        }
        bookmarks.forEach((bookmark, index) => {
            const div = document.createElement('div');
            div.className = 'bookmark-item';
            div.innerHTML = `<span>${bookmark.name}</span><button class="delete-bookmark-btn" data-index="${index}">Delete</button>`;
            div.addEventListener('click', (e) => {
                if (e.target.classList.contains('delete-bookmark-btn')) {
                    e.stopPropagation();
                    const deleteIndex = parseInt(e.target.getAttribute('data-index') || '0');
                    bookmarks.splice(deleteIndex, 1);
                    localStorage.setItem(BOOKMARKS_KEY, JSON.stringify(bookmarks));
                    this.updateBookmarks();
                    this.showToast('Bookmark deleted');
                }
                else {
                    const view = types_1.FractalView.fromBookmark(bookmark);
                    this.engine.setView(view);
                    this.showToast('Bookmark loaded');
                }
            });
            this.bookmarksPanel.appendChild(div);
        });
    }
    showToast(message) {
        this.toast.textContent = message;
        this.toast.classList.add('show');
        setTimeout(() => {
            this.toast.classList.remove('show');
        }, 2000);
    }
}
exports.FractalUI = FractalUI;
};

// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"bartowski/ProCreations_grug-27b-GGUF-IQ4","provider":"brigid","persona":"typescript","sessionId":"4d90a789-9d02-4afa-b95e-82390045846b","tokensIn":434510,"tokensOut":13353,"tokensTotal":447863,"cost":0,"turns":26,"toolCalls":27,"failedToolCalls":1,"timestamp":"2026-08-05T17:32:15.996Z"} -->