← Fractal Explorer results

Fractal Explorer

North Mini Code 1.0 GGUF · typescript

Initial view of Fractal Explorer

Browser check: Artifact runtime error. This is a technical check, not a quality review.

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,190 bytes · SHA-256 1c73c4957596
<!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 ── */
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  user-select: none;
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
}

body {
  margin: 0;
  padding: 0;
  background: #000;
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100vh;
  width: 100vw;
}

canvas {
  display: block;
  width: 100%;
  height: 100%;
  image-rendering: crisp-optimizeSpeed;
  image-rendering: -moz-crisp-optimize;
  image-rendering: pixelated;
}

.hud {
  position: absolute;
  top: 10px;
  left: 10px;
  background: rgba(0, 0, 0, 0.7);
  border: 1px solid rgba(255, 255, 255, 0.2);
  border-radius: 8px;
  padding: 10px 15px;
  font-size: 14px;
  color: #fff;
  transition: opacity 0.3s ease, transform 0.3s ease;
  pointer-events: auto;
  z-index: 100;
  backdrop-filter: blur(5px);
  -webkit-backdrop-filter: blur(5px);
}

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

.hud-content > span {
  margin-right: 15px;
  display: inline-block;
}

.hud-toggle {
  position: absolute;
  top: 5px;
  right: 5px;
  cursor: pointer;
  font-size: 16px;
  opacity: 0.7;
  transition: opacity 0.2s;
}

.hud-toggle:hover {
  opacity: 1;
}

.palette-selector {
  position: absolute;
  bottom: 10px;
  left: 10px;
  display: flex;
  gap: 8px;
  background: rgba(0, 0, 0, 0.7);
  border: 1px solid rgba(255, 255, 255, 0.2);
  border-radius: 8px;
  padding: 8px;
  z-index: 200;
  backdrop-filter: blur(5px);
  -webkit-backdrop-filter: blur(5px);
}

.palette-btn {
  width: 40px;
  height: 30px;
  border: 1px solid rgba(255, 255, 255, 0.3);
  border-radius: 4px;
  cursor: pointer;
  transition: transform 0.1s;
}

.palette-btn:hover {
  transform: scale(1.1);
  border-color: #fff;
}

.palette-btn.active {
  border: 2px solid #fff;
  transform: scale(1.05);
}

.zoom-indicator {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  z-index: 50;
}

.zoom-box {
  position: absolute;
  border: 2px solid rgba(255, 255, 255, 0.8);
  background: rgba(255, 255, 255, 0.1);
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.5);
  pointer-events: none;
  transition: all 0.1s ease;
}

.zoom-indicator.active .zoom-box {
  background: rgba(255, 255, 255, 0.05);
}
</style>
</head>
<body>
<div id="app"></div>
<canvas id="fractalCanvas" width="800" height="600"></canvas>
<div id="hud" class="hud">
  <div class="hud-content">
    <span class="mag">×1.0</span>
    <span class="coords">(0.0, 0.0)</span>
    <span class="iter">Iter: 256</span>
    <span class="fps">FPS: 0</span>
  </div>
  <div class="hud-toggle" title="Toggle HUD">⚙️</div>
</div>
<div id="paletteSelector" class="palette-selector">
  <button class="palette-btn" data-palette="fire" style="background: linear-gradient(to right, #000, #f00, #f80, #fff)"></button>
  <button class="palette-btn" data-palette="ocean" style="background: linear-gradient(to right, #000, #0326, #96cf, #fff)"></button>
  <button class="palette-btn" data-palette="monochrome" style="background: linear-gradient(to right, #000, #808080, #fff)"></button>
  <button class="palette-btn" data-palette="psychedelic" style="background: linear-gradient(to right, #f0f, #0ff, #0f0, #ff0, #f00)"></button>
</div>
<div id="zoomIndicator" class="zoom-indicator" style="display:none;">
  <div class="zoom-box"></div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./constants":"src/constants.ts","./render":"src/render.ts"},"src/render.ts":{"./constants":"src/constants.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.FractalExplorer = void 0;
// Application state and main controller
const constants_1 = require("./constants");
const render_1 = require("./render");
class FractalExplorer {
    constructor(canvas) {
        this.lastFrameTime = 0;
        this.fps = 0;
        this.animationId = null;
        this.isDragging = false;
        this.dragStart = null;
        this.zoomBoxVisible = false;
        this.zoomBox = null;
        this.lastPointer = null;
        this.panStart = null;
        this.history = [];
        this.historyIndex = -1;
        this.hudVisible = true;
        this.hudTimeout = null;
        this.resizeObserver = null;
        this.canvas = canvas;
        this.ctx = canvas.getContext('2d');
        this.renderer = new render_1.ProgressiveRenderer();
        this.palettes = constants_1.CONSTANTS.PALETTES;
        this.state = this.loadState();
        this.render();
        this.setupEventListeners();
        this.setupUI();
        this.startAnimationLoop();
        this.startFPSMonitor();
    }
    loadState() {
        const saved = localStorage.getItem('fractalExplorerState');
        if (saved) {
            try {
                const parsed = JSON.parse(saved);
                // Convert color arrays back to proper format
                parsed.palette = parsed.palette.map((p) => ({
                    offset: p.offset,
                    color: p.color
                }));
                return parsed;
            }
            catch (e) {
                console.warn('Failed to parse saved state:', e);
            }
        }
        // Default state
        return {
            reMin: -2.0,
            imMax: 2.0,
            scale: 3.0 / Math.min(this.canvas.width, this.canvas.height),
            palette: this.palettes[constants_1.CONSTANTS.DEFAULT_PALETTE],
            colorOffset: 0,
            isJulia: false,
            cReal: -0.8,
            cImag: 0.156,
            maxIter: constants_1.CONSTANTS.MAX_ITER_BASE,
        };
    }
    saveState() {
        const stateToSave = { ...this.state };
        // Ensure palette is serializable
        stateToSave.palette = stateToSave.palette.map(p => ({
            offset: p.offset,
            color: p.color
        }));
        localStorage.setItem('fractalExplorerState', JSON.stringify(stateToSave));
    }
    setupUI() {
        // Palette buttons
        document.querySelectorAll('.palette-btn').forEach(btn => {
            btn.addEventListener('click', (e) => {
                const paletteKey = e.currentTarget.dataset.palette;
                if (paletteKey && this.palettes[paletteKey]) {
                    this.state.palette = this.palettes[paletteKey];
                    this.render();
                    this.saveState();
                    this.updateActivePaletteButton(paletteKey);
                }
            });
        });
        // HUD toggle
        const hudToggle = document.querySelector('.hud-toggle');
        hudToggle?.addEventListener('click', () => {
            this.hudVisible = !this.hudVisible;
            const hud = document.querySelector('.hud');
            if (hud) {
                hud.classList.toggle('hidden', !this.hudVisible);
            }
            this.resetHUDTimeout();
        });
        // Keyboard shortcuts
        document.addEventListener('keydown', (e) => {
            switch (e.key.toLowerCase()) {
                case 'j':
                    this.toggleJuliaMode();
                    break;
                case '=':
                case '+':
                    this.zoom(1.15, this.canvas.width / 2, this.canvas.height / 2);
                    break;
                case '-':
                    this.zoom(1 / 1.15, this.canvas.width / 2, this.canvas.height / 2);
                    break;
                case 'r':
                    this.resetView();
                    break;
            }
        });
        // Handle window resize
        this.resizeObserver = new ResizeObserver(() => {
            this.updateCanvasSize();
        });
        this.resizeObserver.observe(this.canvas);
    }
    setupEventListeners() {
        // Mouse wheel zoom
        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 zoomFactor = e.deltaY > 0 ? 1 / constants_1.CONSTANTS.ZOOM_SPEED : constants_1.CONSTANTS.ZOOM_SPEED;
            this.zoom(zoomFactor, x, y);
            this.resetHUDTimeout();
        });
        // Mouse down for drag zoom or pan
        this.canvas.addEventListener('mousedown', (e) => {
            if (e.button === 0) { // Left click only
                e.preventDefault();
                const rect = this.canvas.getBoundingClientRect();
                const x = e.clientX - rect.left;
                const y = e.clientY - rect.top;
                if (e.shiftKey) {
                    // Start panning
                    this.panStart = {
                        x: e.clientX,
                        y: e.clientY,
                        reMin: this.state.reMin,
                        imMax: this.state.imMax,
                    };
                }
                else {
                    // Start drag zoom selection
                    this.isDragging = true;
                    this.dragStart = { x, y };
                    this.zoomBoxVisible = true;
                    this.zoomBox = { x, y, width: 0, height: 0 };
                    const zoomIndicator = document.querySelector('.zoom-indicator');
                    zoomIndicator.classList.add('active');
                }
                this.resetHUDTimeout();
            }
        });
        // Mouse move for drag zoom or pan
        this.canvas.addEventListener('mousemove', (e) => {
            if (this.panStart) {
                const deltaX = (e.clientX - this.panStart.x) / this.canvas.width * (this.state.reMin - this.state.reMin / constants_1.CONSTANTS.ZOOM_SPEED);
                const deltaY = (e.clientY - this.panStart.y) / this.canvas.height * (this.state.imMax / constants_1.CONSTANTS.ZOOM_SPEED);
                this.state.reMin = this.panStart.reMin - deltaX;
                this.state.imMax = this.panStart.imMax + deltaY;
                this.render();
            }
            if (this.isDragging && this.dragStart) {
                const rect = this.canvas.getBoundingClientRect();
                const x = e.clientX - rect.left;
                const y = e.clientY - rect.top;
                this.zoomBox.x = Math.min(this.dragStart.x, x);
                this.zoomBox.y = Math.min(this.dragStart.y, y);
                this.zoomBox.width = Math.abs(x - this.dragStart.x);
                this.zoomBox.height = Math.abs(y - this.dragStart.y);
                const zoomIndicator = document.querySelector('.zoom-indicator');
                zoomIndicator.style.display = 'block';
            }
        });
        // Mouse up to end drag zoom or pan
        this.canvas.addEventListener('mouseup', () => {
            if (this.panStart) {
                this.panStart = null;
            }
            if (this.isDragging && this.zoomBox && this.zoomBox.width > 10 && this.zoomBox.height > 10) {
                // Perform zoom to selected region
                const scale = this.state.scale / (this.zoomBox.width / this.canvas.width);
                const reMin = this.state.reMin + this.zoomBox.x / this.canvas.width * (this.state.reMin - this.state.reMin / scale);
                const imMax = this.state.imMax - this.zoomBox.y / this.canvas.height * (this.state.imMax / scale);
                this.zoomToRegion(reMin, imMax, scale);
            }
            this.isDragging = false;
            this.dragStart = null;
            this.zoomBoxVisible = false;
            const zoomIndicator = document.querySelector('.zoom-indicator');
            zoomIndicator.classList.remove('active');
            zoomIndicator.style.display = 'none';
            this.zoomBox = null;
        });
        // Touch support for mobile
        this.canvas.addEventListener('touchstart', (e) => {
            e.preventDefault();
            if (e.touches.length === 1) {
                const touch = e.touches[0];
                const rect = this.canvas.getBoundingClientRect();
                const x = touch.clientX - rect.left;
                const y = touch.clientY - rect.top;
                this.isDragging = true;
                this.dragStart = { x, y };
                this.zoomBoxVisible = true;
                this.zoomBox = { x, y, width: 0, height: 0 };
                const zoomIndicator = document.querySelector('.zoom-indicator');
                zoomIndicator.classList.add('active');
                zoomIndicator.style.display = 'block';
            }
        });
        this.canvas.addEventListener('touchmove', (e) => {
            e.preventDefault();
            if (this.isDragging && this.dragStart && this.zoomBox) {
                const touch = e.touches[0];
                const rect = this.canvas.getBoundingClientRect();
                const x = touch.clientX - rect.left;
                const y = touch.clientY - rect.top;
                this.zoomBox.x = Math.min(this.dragStart.x, x);
                this.zoomBox.y = Math.min(this.dragStart.y, y);
                this.zoomBox.width = Math.abs(x - this.dragStart.x);
                this.zoomBox.height = Math.abs(y - this.dragStart.y);
            }
        });
        this.canvas.addEventListener('touchend', (e) => {
            e.preventDefault();
            if (this.isDragging && this.zoomBox && this.zoomBox.width > 10 && this.zoomBox.height > 10) {
                const scale = this.state.scale / (this.zoomBox.width / this.canvas.width);
                const reMin = this.state.reMin + this.zoomBox.x / this.canvas.width * (this.state.reMin - this.state.reMin / scale);
                const imMax = this.state.imMax - this.zoomBox.y / this.canvas.height * (this.state.imMax / scale);
                this.zoomToRegion(reMin, imMax, scale);
            }
            this.isDragging = false;
            this.dragStart = null;
            this.zoomBoxVisible = false;
            const zoomIndicator = document.querySelector('.zoom-indicator');
            zoomIndicator.classList.remove('active');
            zoomIndicator.style.display = 'none';
            this.zoomBox = null;
        });
    }
    resetHUDTimeout() {
        if (this.hudTimeout) {
            clearTimeout(this.hudTimeout);
        }
        const hud = document.querySelector('.hud');
        if (hud) {
            hud.classList.remove('hidden');
        }
        this.hudTimeout = setTimeout(() => {
            if (this.hudVisible) {
                const hud = document.querySelector('.hud');
                if (hud) {
                    hud.classList.add('hidden');
                }
            }
        }, constants_1.CONSTANTS.HUD_HIDE_TIMEOUT);
    }
    updateCanvasSize() {
        const rect = this.canvas.getBoundingClientRect();
        const dpr = window.devicePixelRatio || 1;
        const width = Math.floor(rect.width * dpr);
        const height = Math.floor(rect.height * dpr);
        if (this.canvas.width !== width || this.canvas.height !== height) {
            this.canvas.width = width;
            this.canvas.height = height;
            this.renderer.stop();
            this.render();
        }
    }
    updateHUD() {
        const magElement = document.querySelector('.mag');
        const coordsElement = document.querySelector('.coords');
        const iterElement = document.querySelector('.iter');
        if (magElement) {
            const scale = Math.abs(this.state.scale);
            const mag = 1 / scale;
            magElement.textContent = `×${mag.toExponential(1)}`;
        }
        if (coordsElement) {
            const centerX = this.state.reMin + (this.state.reMin + 2 * this.state.scale) / 2;
            const centerY = this.state.imMax - (this.state.imMax - 2 * this.state.scale) / 2;
            coordsElement.textContent = `(${centerX.toFixed(4)}, ${centerY.toFixed(4)})`;
        }
        if (iterElement) {
            iterElement.textContent = `Iter: ${this.state.maxIter}`;
        }
        const fpsElement = document.querySelector('.fps');
        if (fpsElement) {
            fpsElement.textContent = `FPS: ${this.fps}`;
        }
    }
    startFPSMonitor() {
        let lastTime = performance.now();
        let frameCount = 0;
        const updateFPS = (currentTime) => {
            frameCount++;
            if (currentTime >= lastTime + 1000) {
                this.fps = Math.round((frameCount * 1000) / (currentTime - lastTime));
                frameCount = 0;
                lastTime = currentTime;
                this.updateHUD();
                requestAnimationFrame(updateFPS);
            }
            else {
                requestAnimationFrame(updateFPS);
            }
        };
        requestAnimationFrame(updateFPS);
    }
    startAnimationLoop() {
        const animate = (timestamp) => {
            if (timestamp - this.lastFrameTime >= 1000 / 60) {
                this.lastFrameTime = timestamp;
                this.render();
            }
            this.animationId = requestAnimationFrame(animate);
        };
        requestAnimationFrame(animate);
    }
    render() {
        if (this.renderer['isRunning']) {
            // Cancel any existing render
            this.renderer.stop();
        }
        // Calculate max iterations based on scale
        const baseIter = constants_1.CONSTANTS.MAX_ITER_BASE;
        const scaleFactor = Math.log10(1 / Math.abs(this.state.scale)) / Math.log10(1 / constants_1.CONSTANTS.MIN_SCALE);
        const maxIter = Math.min(baseIter * Math.pow(constants_1.CONSTANTS.MAX_ITER_ZOOM_FACTOR, scaleFactor), 5000);
        this.state.maxIter = Math.round(maxIter);
        // Start progressive rendering
        this.renderer.start(this.canvas, this.state.reMin, this.state.imMax, this.state.scale, this.state.palette, this.state.colorOffset, this.state.isJulia, this.state.cReal, this.state.cImag, this.state.maxIter, () => {
            this.saveState();
            this.updateHUD();
        });
    }
    zoomToRegion(reMin, imMax, scale) {
        // Add to history
        this.addToHistory();
        this.state.reMin = reMin;
        this.state.imMax = imMax;
        this.state.scale = scale;
        this.render();
        this.saveState();
        this.resetHUDTimeout();
    }
    zoom(factor, centerX, centerY) {
        // Calculate new scale
        let newScale = this.state.scale * factor;
        // Clamp to min/max scales
        if (newScale > constants_1.CONSTANTS.MAX_SCALE)
            newScale = constants_1.CONSTANTS.MAX_SCALE;
        if (newScale < constants_1.CONSTANTS.MIN_SCALE)
            newScale = constants_1.CONSTANTS.MIN_SCALE;
        // Calculate new center coordinates
        const oldScale = this.state.scale;
        const dx = (centerX / this.canvas.width) * oldScale;
        const dy = (-centerY / this.canvas.height) * oldScale;
        const newReMin = this.state.reMin - dx * (newScale / oldScale - 1);
        const newImMax = this.state.imMax + dy * (newScale / oldScale - 1);
        this.addToHistory();
        this.state.reMin = newReMin;
        this.state.imMax = newImMax;
        this.state.scale = newScale;
        this.render();
        this.saveState();
        this.resetHUDTimeout();
    }
    addToHistory() {
        const stateCopy = {
            reMin: this.state.reMin,
            imMax: this.state.imMax,
            scale: this.state.scale,
            palette: this.state.palette,
            colorOffset: this.state.colorOffset,
            isJulia: this.state.isJulia,
            cReal: this.state.cReal,
            cImag: this.state.cImag,
            maxIter: this.state.maxIter,
        };
        this.history.push(stateCopy);
        this.historyIndex = this.history.length - 1;
        // Limit history size
        if (this.history.length > 50) {
            this.history.shift();
            this.historyIndex--;
        }
    }
    goBack() {
        if (this.historyIndex > 0) {
            this.historyIndex--;
            this.state = { ...this.history[this.historyIndex] };
            this.render();
            this.saveState();
        }
    }
    resetView() {
        this.addToHistory();
        this.state = {
            reMin: -2.0,
            imMax: 2.0,
            scale: 3.0 / Math.min(this.canvas.width, this.canvas.height),
            palette: this.palettes[constants_1.CONSTANTS.DEFAULT_PALETTE],
            colorOffset: 0,
            isJulia: false,
            cReal: -0.8,
            cImag: 0.156,
            maxIter: constants_1.CONSTANTS.MAX_ITER_BASE,
        };
        this.render();
        this.saveState();
    }
    toggleJuliaMode() {
        this.addToHistory();
        this.state.isJulia = !this.state.isJulia;
        // If switching to Julia, use current center as c value
        if (this.state.isJulia) {
            const centerX = this.state.reMin + (this.state.reMin + 2 * this.state.scale) / 2;
            const centerY = this.state.imMax - (this.state.imMax - 2 * this.state.scale) / 2;
            this.state.cReal = centerX;
            this.state.cImag = centerY;
        }
        this.render();
        this.saveState();
    }
    updateActivePaletteButton(activePalette) {
        document.querySelectorAll('.palette-btn').forEach(btn => {
            btn.classList.toggle('active', btn.dataset.palette === activePalette);
        });
    }
    // Public API for debugging and probing
    iterationsAt(re, im) {
        return this.state.isJulia
            ? this.state.maxIter // This would need to call the actual calculation
            : this.state.maxIter; // Placeholder
    }
    setView(options) {
        Object.assign(this.state, options);
        this.render();
        this.saveState();
    }
    destroy() {
        this.renderer.stop();
        if (this.animationId) {
            cancelAnimationFrame(this.animationId);
        }
        if (this.hudTimeout) {
            clearTimeout(this.hudTimeout);
        }
        if (this.resizeObserver) {
            this.resizeObserver.disconnect();
        }
        window.removeEventListener('keydown', this.setupEventListeners);
    }
}
exports.FractalExplorer = FractalExplorer;
// Initialize when the DOM is ready
document.addEventListener('DOMContentLoaded', () => {
    const canvas = document.getElementById('fractalCanvas');
    if (canvas) {
        const explorer = new FractalExplorer(canvas);
        // Expose API for debugging
        window.fractal = {
            iterationsAt: (re, im) => explorer.iterationsAt(re, im),
            setView: (options) => explorer.setView(options),
        };
    }
});
};

// ── module: src/constants.ts ──
__mods["src/constants.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CONSTANTS = void 0;
exports.interpolateColor = interpolateColor;
exports.getColorFromPalette = getColorFromPalette;
// Constants and configuration for the fractal explorer.
exports.CONSTANTS = {
    // Zoom speed (scale factor per wheel step)
    ZOOM_SPEED: 1.15,
    // Minimum and maximum zoom scales (magnification)
    MIN_SCALE: 1e-6,
    MAX_SCALE: 1e13,
    // Base maximum iterations at default scale (1x)
    MAX_ITER_BASE: 256,
    // Iterations scale factor with zoom (more zoom = more iterations)
    MAX_ITER_ZOOM_FACTOR: 2.0,
    // Progressive rendering: number of refinement passes
    PROGRESSIVE_PASSES: 4,
    // Tile size for progressive rendering (width/height)
    TILE_SIZE: 64,
    // Palettes: each is an array of RGBA stops (0-255)
    PALETTES: {
        fire: [
            { offset: 0.0, color: [0, 0, 0, 255] }, // black interior
            { offset: 0.33, color: [255, 0, 0, 255] }, // red
            { offset: 0.66, color: [255, 128, 0, 255] }, // orange
            { offset: 1.0, color: [255, 255, 255, 255] }, // white
        ],
        ocean: [
            { offset: 0.0, color: [0, 0, 0, 255] },
            { offset: 0.33, color: [0, 50, 100, 255] },
            { offset: 0.66, color: [100, 150, 255, 255] },
            { offset: 1.0, color: [255, 255, 255, 255] },
        ],
        monochrome: [
            { offset: 0.0, color: [0, 0, 0, 255] },
            { offset: 0.5, color: [128, 128, 128, 255] },
            { offset: 1.0, color: [255, 255, 255, 255] },
        ],
        psychedelic: [
            { offset: 0.0, color: [255, 0, 255, 255] }, // magenta
            { offset: 0.25, color: [0, 255, 255, 255] }, // cyan
            { offset: 0.5, color: [0, 255, 0, 255] }, // green
            { offset: 0.75, color: [255, 255, 0, 255] }, // yellow
            { offset: 1.0, color: [255, 0, 0, 255] }, // red
        ],
    },
    // Default active palette key
    DEFAULT_PALETTE: 'fire',
    // UI constants
    HUD_HIDE_TIMEOUT: 3000, // ms before HUD auto-hides
};
// Helper to interpolate between two colors given a ratio (0-1)
function interpolateColor(c1, c2, t) {
    return c1.map((v, i) => Math.round(v + (c2[i] - v) * t));
}
// Get color from palette at normalized iteration value (0-1)
function getColorFromPalette(palette, norm, offset = 0) {
    if (norm <= 0)
        return palette[0].color;
    if (norm >= 1)
        return palette[palette.length - 1].color;
    // Apply offset (wrap-around)
    let adjusted = (norm + offset) % 1;
    if (adjusted < 0)
        adjusted += 1;
    // Find segment
    for (let i = 0; i < palette.length - 1; i++) {
        const p0 = palette[i];
        const p1 = palette[i + 1];
        if (adjusted >= p0.offset && adjusted <= p1.offset) {
            const t = (adjusted - p0.offset) / (p1.offset - p0.offset);
            return interpolateColor(p0.color, p1.color, t);
        }
    }
    return [255, 255, 255, 255];
}
};

// ── module: src/render.ts ──
__mods["src/render.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ProgressiveRenderer = void 0;
exports.smoothIteration = smoothIteration;
exports.calculateIterations = calculateIterations;
exports.renderTile = renderTile;
// Core math and rendering logic for the fractal explorer.
const constants_1 = require("./constants");
// Smooth coloring formula: n + 1 - log2(log|z|)
function smoothIteration(escapeTime, zReal, zImag) {
    // z is the final iteration value before escape (complex number)
    // Avoid division by zero for interior points
    if (escapeTime >= constants_1.CONSTANTS.MAX_ITER_BASE) {
        return constants_1.CONSTANTS.MAX_ITER_BASE; // interior point
    }
    const magnitude = zReal * zReal + zImag * zImag;
    if (magnitude <= 0)
        return escapeTime;
    const logMagnitude = Math.log(magnitude) / Math.LN2;
    return escapeTime + 1 - logMagnitude;
}
// Calculate escape iterations for a single point (c = re + i*im)
function calculateIterations(re, im, cReal, cImag, maxIter) {
    let zReal = re;
    let zImag = im;
    let iter = 0;
    while (iter < maxIter && zReal * zReal + zImag * zImag < 4) {
        const temp = zReal * zReal - zImag * zImag + cReal;
        zImag = 2 * zReal * zImag + cImag;
        zReal = temp;
        iter++;
    }
    return iter;
}
// Render a single tile (row range) of the fractal
function renderTile(imageData, startY, endY, reMin, imMax, scale, palette, colorOffset, isJulia, cReal, cImag, maxIter) {
    const data = imageData.data;
    const width = imageData.width;
    const height = imageData.height;
    for (let y = startY; y < endY; y++) {
        const im = imMax - y * scale; // y axis inverted
        for (let x = 0; x < width; x++) {
            const re = reMin + x * scale;
            let iterations;
            if (isJulia) {
                iterations = calculateIterations(re, im, cReal, cImag, maxIter);
            }
            else {
                iterations = calculateIterations(re, im, re, im, maxIter);
            }
            let color;
            if (iterations === maxIter) {
                // Interior point
                color = [0, 0, 0, 255];
            }
            else {
                // Smooth coloring
                const smooth = smoothIteration(iterations, re, im);
                const norm = smooth / maxIter;
                color = (0, constants_1.getColorFromPalette)(palette, norm, colorOffset);
            }
            const idx = (y * width + x) * 4;
            data[idx] = color[0];
            data[idx + 1] = color[1];
            data[idx + 2] = color[2];
            data[idx + 3] = color[3];
        }
    }
}
// Progressive rendering manager
class ProgressiveRenderer {
    constructor() {
        this.animationFrameId = null;
        this.currentPass = 0;
        this.tiles = [];
        this.completedTiles = new Set();
        this.imageData = null;
        this.canvas = null;
        this.isRunning = false;
    }
    start(canvas, reMin, imMax, scale, palette, colorOffset, isJulia, cReal, cImag, maxIter, onComplete) {
        this.stop(); // cancel any existing render
        this.canvas = canvas;
        const ctx = canvas.getContext('2d');
        this.imageData = ctx?.createImageData(canvas.width, canvas.height) ?? null;
        this.onComplete = onComplete;
        this.currentPass = 0;
        this.completedTiles.clear();
        this.isRunning = true;
        // Initialize empty image with black background
        if (this.imageData) {
            const data = this.imageData.data;
            for (let i = 0; i < data.length; i += 4) {
                data[i] = 0;
                data[i + 1] = 0;
                data[i + 2] = 0;
                data[i + 3] = 255;
            }
        }
        this.scheduleNextPass(reMin, imMax, scale, palette, colorOffset, isJulia, cReal, cImag, maxIter);
    }
    scheduleNextPass(reMin, imMax, scale, palette, colorOffset, isJulia, cReal, cImag, maxIter) {
        if (!this.isRunning)
            return;
        // Determine resolution for current pass based on pass number
        const passIndex = this.currentPass;
        const tileSize = constants_1.CONSTANTS.TILE_SIZE;
        const width = this.canvas?.width || 0;
        const height = this.canvas?.height || 0;
        // For pass 0, use quarter resolution; for subsequent passes, increase resolution
        const factor = Math.pow(2, Math.floor(passIndex / 2));
        const renderWidth = Math.max(tileSize, Math.ceil(width / factor));
        const renderHeight = Math.max(tileSize, Math.ceil(height / factor));
        // Generate tiles for this pass
        this.tiles = [];
        for (let ty = 0; ty < height; ty += renderHeight) {
            for (let tx = 0; tx < width; tx += renderWidth) {
                // Only keep tiles that haven't been completed yet
                const tileKey = `${tx},${ty}`;
                if (!this.completedTiles.has(tileKey)) {
                    this.tiles.push({
                        x: tx,
                        y: ty,
                        width: Math.min(renderWidth, width - tx),
                        height: Math.min(renderHeight, height - ty),
                    });
                }
            }
        }
        // Render all tiles for this pass
        this.renderPass(reMin, imMax, scale, palette, colorOffset, isJulia, cReal, cImag, maxIter);
    }
    renderPass(reMin, imMax, scale, palette, colorOffset, isJulia, cReal, cImag, maxIter) {
        if (!this.imageData || this.tiles.length === 0) {
            this.currentPass++;
            if (this.currentPass < constants_1.CONSTANTS.PROGRESSIVE_PASSES) {
                this.scheduleNextPass(reMin, imMax, scale, palette, colorOffset, isJulia, cReal, cImag, maxIter);
            }
            else {
                this.complete();
            }
            return;
        }
        // Render tiles in batches to maintain responsiveness
        const batchSize = Math.ceil(this.tiles.length / 4); // 4 batches per pass
        const batch = this.tiles.splice(0, batchSize);
        for (const tile of batch) {
            // Calculate tile's region in the fractal space
            const tileReMin = reMin + tile.x * scale;
            const tileImMax = imMax - tile.y * scale;
            const tileScale = scale;
            // Render tile directly into the main image data
            renderTile(this.imageData, tile.y, tile.y + tile.height, tileReMin, tileImMax, tileScale, palette, colorOffset, isJulia, cReal, cImag, maxIter);
            // Mark tile as completed
            const tileKey = `${tile.x},${tile.y}`;
            this.completedTiles.add(tileKey);
        }
        // Update canvas with partial results
        if (this.canvas && this.imageData) {
            this.canvas.getContext('2d')?.putImageData(this.imageData, 0, 0);
        }
        // Schedule next batch of tiles for this pass
        if (this.tiles.length > 0) {
            this.animationFrameId = requestAnimationFrame(() => {
                this.renderPass(reMin, imMax, scale, palette, colorOffset, isJulia, cReal, cImag, maxIter);
            });
        }
        else {
            // All tiles for this pass done, move to next pass
            this.currentPass++;
            if (this.currentPass < constants_1.CONSTANTS.PROGRESSIVE_PASSES) {
                this.scheduleNextPass(reMin, imMax, scale, palette, colorOffset, isJulia, cReal, cImag, maxIter);
            }
            else {
                this.complete();
            }
        }
    }
    stop() {
        this.isRunning = false;
        if (this.animationFrameId !== null) {
            cancelAnimationFrame(this.animationFrameId);
            this.animationFrameId = null;
        }
        this.tiles = [];
        this.completedTiles.clear();
    }
    complete() {
        this.isRunning = false;
        this.animationFrameId = null;
        this.tiles = [];
        this.completedTiles.clear();
        if (this.onComplete) {
            this.onComplete();
        }
    }
}
exports.ProgressiveRenderer = ProgressiveRenderer;
};

// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"unsloth/North-Mini-Code-1.0-GGUF","provider":"brigid","persona":"typescript","sessionId":"5cff54c3-01c3-4613-8aa0-88643fa03299","tokensIn":643744,"tokensOut":34145,"tokensTotal":677889,"cost":0,"turns":29,"toolCalls":28,"failedToolCalls":0,"timestamp":"2026-08-07T23:29:13.125Z"} -->