Fractal Explorer
Qwen 3.6 27b · typescript

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 55,732 bytes · SHA-256 295e23fd1ed8
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Fractal Explorer — Mandelbrot & Julia Sets</title>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #000; font-family: 'Segoe UI', system-ui, sans-serif; }
#fractalCanvas { display: block; width: 100%; height: 100%; cursor: crosshair; }
#zoom-box { position: absolute; border: 2px solid rgba(255,255,255,0.8); border-style: dashed; pointer-events: none; display: none; }
#hud { position: absolute; top: 0; left: 0; right: 0; bottom: 0; pointer-events: none; transition: opacity 0.5s; }
#hud.hidden { opacity: 0; }
#hud > * { pointer-events: auto; }
#hud-top { display: flex; justify-content: space-between; padding: 12px; }
#info-panel { background: rgba(10,10,20,0.75); backdrop-filter: blur(8px); border-radius: 10px; padding: 10px 14px; color: #ccc; font-size: 13px; line-height: 1.6; border: 1px solid rgba(255,255,255,0.1); }
#info-panel div { white-space: nowrap; }
#controls-panel { display: flex; gap: 6px; }
#controls-panel button { background: rgba(10,10,20,0.75); backdrop-filter: blur(8px); color: #ddd; border: 1px solid rgba(255,255,255,0.15); border-radius: 8px; padding: 8px 14px; font-size: 16px; cursor: pointer; transition: background 0.2s; }
#controls-panel button:hover { background: rgba(40,40,80,0.85); }
#hud-bottom { position: absolute; bottom: 12px; left: 12px; right: 12px; display: flex; align-items: center; gap: 16px; }
#palette-bar { display: flex; gap: 6px; }
.palette-swatch { width: 28px; height: 28px; border-radius: 50%; cursor: pointer; border: 2px solid transparent; transition: border-color 0.2s, transform 0.15s; }
.palette-swatch.active { border-color: #fff; transform: scale(1.15); }
#slider-group { display: flex; gap: 16px; align-items: center; color: #ccc; font-size: 12px; background: rgba(10,10,20,0.75); backdrop-filter: blur(8px); padding: 6px 12px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.1); }
#slider-group input[type="range"] { width: 100px; accent-color: #6af; }
#julia-preview { position: absolute; top: 12px; right: 12px; width: 260px; height: 260px; border-radius: 10px; overflow: hidden; border: 2px solid rgba(255,255,255,0.2); box-shadow: 0 4px 20px rgba(0,0,0,0.6); }
#juliaCanvas { width: 100%; height: 100%; display: block; }
#bookmark-modal { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; z-index: 100; }
#bookmark-inner { background: rgba(15,15,30,0.95); backdrop-filter: blur(10px); border-radius: 14px; padding: 24px; min-width: 300px; color: #ddd; border: 1px solid rgba(255,255,255,0.15); }
#bookmark-inner h3 { margin-bottom: 12px; }
#bookmark-list { max-height: 300px; overflow-y: auto; margin-bottom: 12px; }
.bookmark-item { padding: 8px 12px; margin: 4px 0; background: rgba(255,255,255,0.05); border-radius: 6px; cursor: pointer; font-size: 13px; display: flex; justify-content: space-between; align-items: center; }
.bookmark-item:hover { background: rgba(255,255,255,0.12); }
.bookmark-item .del { color: #f66; cursor: pointer; font-size: 16px; }
#btn-close-bookmarks { background: rgba(60,60,100,0.8); color: #ddd; border: 1px solid rgba(255,255,255,0.15); border-radius: 8px; padding: 8px 16px; cursor: pointer; }
</style>
</head>
<body>
<canvas id="fractalCanvas"></canvas>
<div id="hud">
<div id="hud-top">
<div id="info-panel">
<div id="zoom-level">×1.0</div>
<div id="center-coords">Re: 0.0, Im: 0.0</div>
<div id="iter-info">Iterations: 100</div>
<div id="fps-display">FPS: --</div>
</div>
<div id="controls-panel">
<button id="btn-reset" title="Reset View">⟲</button>
<button id="btn-back" title="Zoom Back">←</button>
<button id="btn-julia" title="Julia Mode (J)">J</button>
<button id="btn-bookmark" title="Save Bookmark">★</button>
</div>
</div>
<div id="hud-bottom">
<div id="palette-bar"></div>
<div id="slider-group">
<label>Iterations: <input type="range" id="iter-slider" min="50" max="5000" value="200"></label>
<label>Color Offset: <input type="range" id="color-slider" min="0" max="1000" value="0"></label>
</div>
</div>
</div>
<div id="zoom-box"></div>
<div id="bookmark-modal" style="display:none">
<div id="bookmark-inner">
<h3>Bookmarks</h3>
<div id="bookmark-list"></div>
<button id="btn-close-bookmarks">Close</button>
</div>
</div>
<div id="julia-preview" style="display:none">
<canvas id="juliaCanvas"></canvas>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./config":"src/config.ts","./state":"src/state.ts","./math":"src/math.ts","./renderer":"src/renderer.ts","./input":"src/input.ts","./ui":"src/ui.ts"},"src/state.ts":{"./config":"src/config.ts"},"src/math.ts":{"./config":"src/config.ts","./state":"src/state.ts"},"src/color.ts":{"./config":"src/config.ts"},"src/renderer.ts":{"./config":"src/config.ts","./state":"src/state.ts","./math":"src/math.ts","./color":"src/color.ts","./workerCode":"src/workerCode.ts"},"src/input.ts":{"./config":"src/config.ts","./state":"src/state.ts","./math":"src/math.ts"},"src/ui.ts":{"./config":"src/config.ts","./state":"src/state.ts"}};
function __require(id) {
if (__cache[id]) return __cache[id].exports;
var module = __cache[id] = { exports: {} };
var factory = __mods[id];
if (!factory) throw new Error("Module not found: " + id);
factory(module.exports, function (spec) {
var target = (__map[id] && __map[id][spec]) || spec;
return __require(target);
}, module);
return module.exports;
}
// ── module: src/main.ts ──
__mods["src/main.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
// Entry point: wire up all systems
const config_1 = require("./config");
const state_1 = require("./state");
const math_1 = require("./math");
const renderer_1 = require("./renderer");
const input_1 = require("./input");
const ui_1 = require("./ui");
// Julia preview canvas
let juliaCanvas;
let juliaCtx;
let juliaImageData;
let juliaPixelBuffer;
function main() {
const canvas = document.getElementById('fractalCanvas');
const zoomBox = document.getElementById('zoom-box');
// Try to load state from URL hash
if (location.hash) {
(0, state_1.hashToState)(location.hash);
}
// Init systems
(0, renderer_1.initRenderer)(canvas);
(0, ui_1.initUI)();
(0, input_1.initInput)(canvas, zoomBox, () => { });
// Init Julia preview
juliaCanvas = document.getElementById('juliaCanvas');
juliaCtx = juliaCanvas.getContext('2d', { alpha: false });
juliaCanvas.width = config_1.CONFIG.juliaPreviewSize;
juliaCanvas.height = config_1.CONFIG.juliaPreviewSize;
juliaImageData = juliaCtx.createImageData(config_1.CONFIG.juliaPreviewSize, config_1.CONFIG.juliaPreviewSize);
juliaPixelBuffer = juliaImageData.data;
// Wire up callbacks
(0, input_1.setOnRender)(() => {
if ((0, renderer_1.isRendering)())
(0, renderer_1.cancelRender)();
// Auto-update maxIter based on zoom depth
state_1.viewState.maxIter = (0, state_1.calcMaxIter)();
(0, ui_1.updateHUD)();
(0, renderer_1.startRender)();
(0, input_1.resetHudTimer)();
});
(0, ui_1.setOnRecolor)(() => {
(0, renderer_1.recolor)();
(0, input_1.resetHudTimer)();
});
(0, ui_1.setOnReset)(() => {
const ds = (0, state_1.defaultState)();
Object.assign(state_1.viewState, ds);
(0, ui_1.updateHUD)();
(0, renderer_1.startRender)();
});
(0, ui_1.setOnBack)(() => {
if ((0, state_1.popZoomHistory)()) {
(0, ui_1.updateHUD)();
(0, renderer_1.startRender)();
}
});
(0, ui_1.setOnToggleJulia)(() => {
if (state_1.viewState.setMode === 'mandelbrot') {
state_1.viewState.setMode = 'julia';
(0, ui_1.showJuliaPreview)(false);
}
else {
state_1.viewState.setMode = 'mandelbrot';
(0, ui_1.showJuliaPreview)(true);
}
(0, ui_1.updateJuliaButton)();
(0, ui_1.updateHUD)();
(0, renderer_1.startRender)();
});
(0, input_1.setOnJuliaCChanged)(() => {
// Render Julia preview with new c-value
renderJuliaPreview();
});
(0, input_1.setOnHover)((x, y) => {
// Live Julia preview on hover in Mandelbrot mode
if (state_1.viewState.setMode === 'mandelbrot' && !isDragging()) {
const rect = canvas.getBoundingClientRect();
const mx = x - rect.left;
const my = y - rect.top;
const c = screenToComplexFromInput(mx, my, canvas.clientWidth, canvas.clientHeight);
state_1.viewState.juliaCRe = c.re;
state_1.viewState.juliaCIm = c.im;
renderJuliaPreview();
}
});
// Show HUD initially
(0, input_1.showHud)();
// Show Julia preview by default
(0, ui_1.showJuliaPreview)(true);
(0, ui_1.updateJuliaButton)();
// Initial Julia preview render
renderJuliaPreview();
// Initial render
(0, renderer_1.startRender)();
// Expose debug interface
window.fractal = {
iterationsAt: (re, im) => (0, math_1.iterationsAt)(re, im),
setView: (params) => {
if (params.re !== undefined)
state_1.viewState.centerRe = params.re;
if (params.im !== undefined)
state_1.viewState.centerIm = params.im;
if (params.scale !== undefined)
state_1.viewState.scale = params.scale;
state_1.viewState.maxIter = (0, state_1.calcMaxIter)();
(0, ui_1.updateHUD)();
(0, renderer_1.startRender)();
},
getState: () => ({ ...state_1.viewState }),
magnification: () => {
return state_1.viewState.scale / 150;
},
};
// Handle resize
window.addEventListener('resize', () => {
if ((0, renderer_1.isRendering)())
(0, renderer_1.cancelRender)();
const newCanvas = document.getElementById('fractalCanvas');
(0, renderer_1.initRenderer)(newCanvas);
(0, renderer_1.startRender)();
});
// Auto-animate color offset if configured
if (config_1.CONFIG.colorAnimSpeed > 0) {
setInterval(() => {
state_1.viewState.colorOffset = (state_1.viewState.colorOffset + config_1.CONFIG.colorAnimSpeed) % 1000;
(0, renderer_1.recolor)();
}, 50);
}
// FPS update loop
setInterval(() => {
const fps = (0, renderer_1.updateFps)();
(0, ui_1.updateFps)(fps);
}, 1000);
}
// Track dragging state for hover preview
let dragging = false;
let dragStartX = 0, dragStartY = 0;
const origCanvas = document.getElementById('fractalCanvas');
origCanvas.addEventListener('mousedown', (e) => {
dragging = true;
dragStartX = e.clientX;
dragStartY = e.clientY;
});
origCanvas.addEventListener('mouseup', (e) => {
const dx = e.clientX - dragStartX;
const dy = e.clientY - dragStartY;
if (Math.sqrt(dx * dx + dy * dy) < 5) {
dragging = false;
}
else {
dragging = false;
}
});
origCanvas.addEventListener('mouseleave', () => { dragging = false; });
function isDragging() { return dragging; }
// Screen to complex (duplicated here since input.ts uses the one from math.ts)
function screenToComplexFromInput(sx, sy, canvasW, canvasH) {
const halfW = canvasW / (2 * state_1.viewState.scale);
const halfH = canvasH / (2 * state_1.viewState.scale);
const re = state_1.viewState.centerRe + (sx - canvasW / 2) / state_1.viewState.scale;
const im = state_1.viewState.centerIm - (sy - canvasH / 2) / state_1.viewState.scale;
return { re, im };
}
// Julia preview rendering (fast, no workers needed for small canvas)
function renderJuliaPreview() {
const size = config_1.CONFIG.juliaPreviewSize;
const maxIter = config_1.CONFIG.juliaPreviewIter;
const scale = size / 3; // view range of -1.5 to 1.5
const paletteIndex = state_1.viewState.palette;
const colorOffset = state_1.viewState.colorOffset;
// Build small LUT
const lutSize = 512;
const lut = buildSmallLUT(paletteIndex, colorOffset, lutSize);
const halfW = size / (2 * scale);
const halfH = size / (2 * scale);
const dRe = (2 * halfW) / size;
const dIm = (2 * halfH) / size;
for (let row = 0; row < size; row++) {
const im = halfH - row * dIm;
let re = -halfW;
for (let col = 0; col < size; col++) {
const smoothIter = juliaIterFast(re, im, state_1.viewState.juliaCRe, state_1.viewState.juliaCIm, maxIter);
const pidx = (row * size + col) * 4;
if (smoothIter >= maxIter) {
juliaPixelBuffer[pidx] = 0;
juliaPixelBuffer[pidx + 1] = 0;
juliaPixelBuffer[pidx + 2] = 0;
juliaPixelBuffer[pidx + 3] = 255;
}
else {
const normIter = (smoothIter % maxIter) / maxIter;
const lutIdx = Math.floor(normIter * lutSize) % lutSize;
const lutOff = lutIdx * 4;
juliaPixelBuffer[pidx] = lut[lutOff];
juliaPixelBuffer[pidx + 1] = lut[lutOff + 1];
juliaPixelBuffer[pidx + 2] = lut[lutOff + 2];
juliaPixelBuffer[pidx + 3] = 255;
}
re += dRe;
}
}
juliaCtx.putImageData(juliaImageData, 0, 0);
}
function juliaIterFast(zr, zi, cr, ci, maxIter) {
let zr2 = zr * zr;
let zi2 = zi * zi;
for (let i = 0; i < maxIter; i++) {
zi = 2 * zr * zi + ci;
zr = zr2 - zi2 + cr;
zr2 = zr * zr;
zi2 = zi * zi;
if (zr2 + zi2 > 4) {
const logZn = Math.log(zr2 + zi2) * 0.5;
return i + 1 - Math.log(logZn) / Math.log(2);
}
}
return maxIter;
}
function buildSmallLUT(paletteIndex, colorOffset, size) {
const palette = config_1.PALETTES[paletteIndex % config_1.PALETTES.length];
const stops = palette[1];
const lut = new Uint8Array(size * 4);
const offset = colorOffset / 1000;
for (let i = 0; i < size; i++) {
const t = ((i / size) + offset) % 1.0;
const [r, g, b] = interpolateStops(stops, t);
const idx = i * 4;
lut[idx] = r;
lut[idx + 1] = g;
lut[idx + 2] = b;
lut[idx + 3] = 255;
}
return lut;
}
function interpolateStops(stops, t) {
for (let i = 0; i < stops.length - 1; i++) {
if (t >= stops[i][0] && t <= stops[i + 1][0]) {
const t0 = stops[i][0];
const t1 = stops[i + 1][0];
const range = t1 - t0;
const frac = range > 0 ? (t - t0) / range : 0;
const r = Math.round(stops[i][1] + frac * (stops[i + 1][1] - stops[i][1]));
const g = Math.round(stops[i][2] + frac * (stops[i + 1][2] - stops[i][2]));
const b = Math.round(stops[i][3] + frac * (stops[i + 1][3] - stops[i][3]));
return [r, g, b];
}
}
const last = stops[stops.length - 1];
return [last[1], last[2], last[3]];
}
main();
};
// ── module: src/config.ts ──
__mods["src/config.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PALETTES = exports.CONFIG = void 0;
// Tunable constants for the fractal explorer
exports.CONFIG = {
// Iteration settings
maxIterBase: 150,
maxIterScale: 80, // added per log2(magnification)
maxIterCap: 15000,
// Zoom settings
zoomFactor: 0.5, // how much to zoom on wheel (fraction of current view)
wheelZoomSpeed: 1.15, // multiplier per wheel tick
maxMagnification: 1e14, // double-precision limit
zoomBoxMinSize: 5, // minimum drag box size in px
// Progressive rendering
progressivePasses: 4, // start at 1/16 res, end at full
tileSize: 256, // worker tile size (larger = fewer tiles, faster startup)
// Julia preview
juliaPreviewSize: 260,
juliaPreviewIter: 200,
// HUD
hudHideDelay: 0, // ms before HUD auto-hides (0 = never hide)
// Worker count
workerCount: 0, // 0 = auto (navigator.hardwareConcurrency or 4)
// Color offset animation
colorAnimSpeed: 0, // 0 = off, >0 = auto-animate color offset
};
exports.PALETTES = [
["Fire", [
[0.0, 0, 0, 0],
[0.15, 128, 0, 0],
[0.3, 255, 80, 0],
[0.5, 255, 200, 0],
[0.7, 255, 255, 100],
[0.85, 200, 255, 255],
[1.0, 255, 255, 255],
]],
["Ocean", [
[0.0, 0, 0, 0],
[0.1, 0, 10, 60],
[0.25, 0, 40, 120],
[0.4, 0, 100, 180],
[0.55, 0, 180, 220],
[0.7, 80, 220, 255],
[0.85, 180, 240, 255],
[1.0, 255, 255, 255],
]],
["Monochrome", [
[0.0, 0, 0, 0],
[0.2, 30, 30, 30],
[0.4, 80, 80, 80],
[0.6, 150, 150, 150],
[0.8, 210, 210, 210],
[1.0, 255, 255, 255],
]],
["Psychedelic", [
[0.0, 128, 0, 128],
[0.167, 128, 128, 0],
[0.333, 0, 128, 128],
[0.5, 0, 128, 0],
[0.667, 128, 0, 128],
[0.833, 128, 128, 0],
[1.0, 128, 0, 128],
]],
["Electric", [
[0.0, 0, 0, 0],
[0.1, 20, 0, 80],
[0.25, 60, 0, 200],
[0.4, 120, 0, 255],
[0.55, 180, 100, 255],
[0.7, 220, 200, 255],
[0.85, 255, 255, 220],
[1.0, 255, 255, 255],
]],
];
};
// ── module: src/state.ts ──
__mods["src/state.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.zoomHistory = exports.viewState = exports.defaultState = void 0;
exports.pushZoomHistory = pushZoomHistory;
exports.popZoomHistory = popZoomHistory;
exports.magnification = magnification;
exports.calcMaxIter = calcMaxIter;
exports.loadBookmarks = loadBookmarks;
exports.saveBookmarks = saveBookmarks;
exports.addBookmark = addBookmark;
exports.restoreBookmark = restoreBookmark;
exports.deleteBookmark = deleteBookmark;
exports.stateToHash = stateToHash;
exports.hashToState = hashToState;
// View state: center, scale, palette, iterations, set type, Julia c-value
const config_1 = require("./config");
const defaultState = () => ({
centerRe: -0.5,
centerIm: 0,
scale: 150,
palette: 0,
maxIter: config_1.CONFIG.maxIterBase,
setMode: 'mandelbrot',
juliaCRe: -0.7269,
juliaCIm: 0.1889,
colorOffset: 0,
});
exports.defaultState = defaultState;
exports.viewState = (0, exports.defaultState)();
// Zoom history stack for back navigation
exports.zoomHistory = [];
function pushZoomHistory() {
exports.zoomHistory.push({
re: exports.viewState.centerRe,
im: exports.viewState.centerIm,
scale: exports.viewState.scale,
});
if (exports.zoomHistory.length > 100)
exports.zoomHistory.shift();
}
function popZoomHistory() {
if (exports.zoomHistory.length === 0)
return false;
const prev = exports.zoomHistory.pop();
exports.viewState.centerRe = prev.re;
exports.viewState.centerIm = prev.im;
exports.viewState.scale = prev.scale;
return true;
}
function magnification() {
return exports.viewState.scale / 150;
}
// Auto-calculate max iterations based on zoom depth
function calcMaxIter() {
const mag = magnification();
const logMag = Math.log2(Math.max(mag, 1));
return Math.min(config_1.CONFIG.maxIterBase + Math.floor(config_1.CONFIG.maxIterScale * Math.log2(Math.max(logMag, 1))), config_1.CONFIG.maxIterCap);
}
// Bookmarks
function loadBookmarks() {
try {
const raw = localStorage.getItem('fractal-bookmarks');
return raw ? JSON.parse(raw) : [];
}
catch {
return [];
}
}
function saveBookmarks(bookmarks) {
localStorage.setItem('fractal-bookmarks', JSON.stringify(bookmarks));
}
function addBookmark(name) {
const bookmarks = loadBookmarks();
bookmarks.push({ name, time: Date.now(), state: { ...exports.viewState } });
saveBookmarks(bookmarks);
}
function restoreBookmark(index) {
const bookmarks = loadBookmarks();
if (index < 0 || index >= bookmarks.length)
return false;
exports.viewState = { ...bookmarks[index].state };
return true;
}
function deleteBookmark(index) {
const bookmarks = loadBookmarks();
bookmarks.splice(index, 1);
saveBookmarks(bookmarks);
}
// URL hash state
function stateToHash() {
const s = exports.viewState;
return `#${btoa(JSON.stringify({
re: s.centerRe, im: s.centerIm, sc: s.scale,
p: s.palette, mi: s.maxIter, sm: s.setMode,
jcr: s.juliaCRe, jci: s.juliaCIm, co: s.colorOffset
}))}`;
}
function hashToState(hash) {
try {
const decoded = atob(hash.replace('#', ''));
const obj = JSON.parse(decoded);
exports.viewState.centerRe = obj.re ?? exports.viewState.centerRe;
exports.viewState.centerIm = obj.im ?? exports.viewState.centerIm;
exports.viewState.scale = obj.sc ?? exports.viewState.scale;
exports.viewState.palette = obj.p ?? exports.viewState.palette;
exports.viewState.maxIter = obj.mi ?? exports.viewState.maxIter;
exports.viewState.setMode = obj.sm ?? exports.viewState.setMode;
exports.viewState.juliaCRe = obj.jcr ?? exports.viewState.juliaCRe;
exports.viewState.juliaCIm = obj.jci ?? exports.viewState.juliaCIm;
exports.viewState.colorOffset = obj.co ?? exports.viewState.colorOffset;
return true;
}
catch {
return false;
}
}
};
// ── module: src/math.ts ──
__mods["src/math.ts"] = function (exports, require, module) {
"use strict";
// Fractal iteration math — allocation-free core loop
Object.defineProperty(exports, "__esModule", { value: true });
exports.mandelbrotIter = mandelbrotIter;
exports.juliaIter = juliaIter;
exports.computeRegion = computeRegion;
exports.iterationsAt = iterationsAt;
exports.iterationsAtWith = iterationsAtWith;
exports.screenToComplex = screenToComplex;
const state_1 = require("./state");
// Compute the smooth iteration count for a single point
// Returns the iteration count (integer) and smooth value
// For Mandelbrot: z = 0, c = (re, im)
// For Julia: z = (re, im), c = (juliaCRe, juliaCIm)
function mandelbrotIter(re, im, maxIter) {
let zr = 0, zi = 0;
let zr2 = 0, zi2 = 0;
for (let i = 0; i < maxIter; i++) {
// z = z*z + c => (zr + izi)^2 + c = (zr^2 - zi^2 + cr) + i(2*zr*zi + ci)
zi = 2 * zr * zi + im;
zr = zr2 - zi2 + re;
zr2 = zr * zr;
zi2 = zi * zi;
if (zr2 + zi2 > 4) {
// Smooth iteration count: n + 1 - log2(log|z|)
const logZn = Math.log(zr2 + zi2) * 0.5;
return i + 1 - Math.log(logZn) / Math.log(2);
}
}
return maxIter; // interior point
}
// Julia set iteration
function juliaIter(zr, zi, cr, ci, maxIter) {
let zr2 = zr * zr;
let zi2 = zi * zi;
for (let i = 0; i < maxIter; i++) {
zi = 2 * zr * zi + ci;
zr = zr2 - zi2 + cr;
zr2 = zr * zr;
zi2 = zi * zi;
if (zr2 + zi2 > 4) {
const logZn = Math.log(zr2 + zi2) * 0.5;
return i + 1 - Math.log(logZn) / Math.log(2);
}
}
return maxIter;
}
// Compute iterations for a rectangular region
// result is Float64Array of smooth iteration counts
function computeRegion(result, offsetX, offsetY, width, height, centerRe, centerIm, scale, maxIter, setMode, juliaCRe, juliaCIm) {
const pixelsPerUnit = scale;
const halfW = width / (2 * pixelsPerUnit);
const halfH = height / (2 * pixelsPerUnit);
const reStart = centerRe - halfW + (offsetX / pixelsPerUnit);
const imStart = centerIm + halfH - (offsetY / pixelsPerUnit); // y goes up in complex plane
const dRe = 1 / pixelsPerUnit;
const dIm = -1 / pixelsPerUnit;
let idx = 0;
for (let row = 0; row < height; row++) {
const im = imStart + row * dIm;
let re = reStart;
for (let col = 0; col < width; col++) {
if (setMode === 'mandelbrot') {
result[idx] = mandelbrotIter(re, im, maxIter);
}
else {
result[idx] = juliaIter(re, im, juliaCRe, juliaCIm, maxIter);
}
re += dRe;
idx++;
}
}
}
// Debug: compute iterations at a specific point
function iterationsAt(re, im) {
const maxIter = (0, state_1.calcMaxIter)();
return mandelbrotIter(re, im, maxIter);
}
// Debug: compute iterations at a specific point with explicit maxIter
function iterationsAtWith(re, im, maxIter) {
return mandelbrotIter(re, im, maxIter);
}
// Convert screen coords to complex plane coords
function screenToComplex(sx, sy, canvasW, canvasH) {
const halfW = canvasW / (2 * state_1.viewState.scale);
const halfH = canvasH / (2 * state_1.viewState.scale);
const re = state_1.viewState.centerRe + (sx - canvasW / 2) / state_1.viewState.scale;
const im = state_1.viewState.centerIm - (sy - canvasH / 2) / state_1.viewState.scale;
return { re, im };
}
};
// ── module: src/color.ts ──
__mods["src/color.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getPaletteLUT = getPaletteLUT;
exports.colorizeRegion = colorizeRegion;
exports.colorizeFull = colorizeFull;
exports.invalidateLUTCache = invalidateLUTCache;
// Color palette interpolation and pixel coloring
const config_1 = require("./config");
// Cached LUT — only rebuild when palette or offset changes
let cachedLUT = null;
let cachedPaletteIdx = -1;
let cachedOffset = -1;
// Pre-compute a LUT (look-up table) for a palette
// Returns Uint8Array of size 2048*4 (RGBA) for smooth interpolation
function getPaletteLUT(paletteIndex, colorOffset) {
if (cachedLUT && cachedPaletteIdx === paletteIndex && cachedOffset === colorOffset) {
return cachedLUT;
}
return buildPaletteLUT(paletteIndex, colorOffset);
}
function buildPaletteLUT(paletteIndex, colorOffset) {
const idx = ((paletteIndex % config_1.PALETTES.length) + config_1.PALETTES.length) % config_1.PALETTES.length;
const palette = config_1.PALETTES[idx];
const stops = palette[1];
if (!stops || stops.length === 0) {
// Fallback: simple grayscale
const lutSize = 2048;
const lut = new Uint8Array(lutSize * 4);
for (let i = 0; i < lutSize; i++) {
const v = Math.round((i / lutSize) * 255);
const o = i * 4;
lut[o] = v;
lut[o + 1] = v;
lut[o + 2] = v;
lut[o + 3] = 255;
}
return lut;
}
const lutSize = 2048;
const lut = new Uint8Array(lutSize * 4);
const offset = colorOffset / 1000;
for (let i = 0; i < lutSize; i++) {
const t = ((i / lutSize) + offset) % 1.0;
const [r, g, b] = interpolateStops(stops, t);
const o = i * 4;
lut[o] = r;
lut[o + 1] = g;
lut[o + 2] = b;
lut[o + 3] = 255;
}
cachedLUT = lut;
cachedPaletteIdx = paletteIndex;
cachedOffset = colorOffset;
return lut;
}
// Bilinear interpolation between palette stops
function interpolateStops(stops, t) {
if (!stops || stops.length < 2) {
return [128, 128, 128];
}
// Clamp t to [0, 1]
t = Math.max(0, Math.min(1, t));
for (let i = 0; i < stops.length - 1; i++) {
const s0 = stops[i];
const s1 = stops[i + 1];
if (!s0 || !s1)
continue;
if (t >= s0[0] && t <= s1[0]) {
const range = s1[0] - s0[0];
const frac = range > 0 ? (t - s0[0]) / range : 0;
const r = Math.round(s0[1] + frac * (s1[1] - s0[1]));
const g = Math.round(s0[2] + frac * (s1[2] - s0[2]));
const b = Math.round(s0[3] + frac * (s1[3] - s0[3]));
return [r, g, b];
}
}
// Fallback: use last stop
const last = stops[stops.length - 1];
if (last)
return [last[1], last[2], last[3]];
return [128, 128, 128];
}
// Colorize a region of pixel data from iteration counts
function colorizeRegion(pixels, iterData, paletteLUT, width, height, offsetX, offsetY, maxIter) {
const halfLut = paletteLUT.length / 4;
for (let row = 0; row < height; row++) {
for (let col = 0; col < width; col++) {
const iterIdx = row * width + col;
const smoothIter = iterData[iterIdx];
const pixelX = offsetX + col;
const pixelY = offsetY + row;
const pixelIdx = (pixelY * width + pixelX) * 4;
if (smoothIter >= maxIter) {
pixels[pixelIdx] = 0;
pixels[pixelIdx + 1] = 0;
pixels[pixelIdx + 2] = 0;
pixels[pixelIdx + 3] = 255;
}
else {
const normIter = ((smoothIter % maxIter) + maxIter) / maxIter;
const lutIdx = Math.floor(normIter * halfLut) % halfLut;
const lutOff = lutIdx * 4;
pixels[pixelIdx] = paletteLUT[lutOff];
pixels[pixelIdx + 1] = paletteLUT[lutOff + 1];
pixels[pixelIdx + 2] = paletteLUT[lutOff + 2];
pixels[pixelIdx + 3] = 255;
}
}
}
}
function colorizeFull(pixels, iterData, paletteLUT, width, height, maxIter) {
colorizeRegion(pixels, iterData, paletteLUT, width, height, 0, 0, maxIter);
}
// Invalidate LUT cache (call when palette or offset changes)
function invalidateLUTCache() {
cachedLUT = null;
cachedPaletteIdx = -1;
cachedOffset = -1;
}
};
// ── module: src/workerCode.ts ──
__mods["src/workerCode.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.workerSource = void 0;
// Web Worker code for fractal computation (inline via Blob URL)
exports.workerSource = `
function mandelbrotIter(re, im, maxIter) {
var zr = 0, zi = 0, zr2 = 0, zi2 = 0;
for (var i = 0; i < maxIter; i++) {
zi = 2 * zr * zi + im;
zr = zr2 - zi2 + re;
zr2 = zr * zr;
zi2 = zi * zi;
if (zr2 + zi2 > 4) {
var logZn = Math.log(zr2 + zi2) * 0.5;
return i + 1 - Math.log(logZn) / Math.log(2);
}
}
return maxIter;
}
function juliaIter(zr, zi, cr, ci, maxIter) {
var zr2 = zr * zr, zi2 = zi * zi;
for (var i = 0; i < maxIter; i++) {
zi = 2 * zr * zi + ci;
zr = zr2 - zi2 + cr;
zr2 = zr * zr;
zi2 = zi * zi;
if (zr2 + zi2 > 4) {
var logZn = Math.log(zr2 + zi2) * 0.5;
return i + 1 - Math.log(logZn) / Math.log(2);
}
}
return maxIter;
}
self.onmessage = function(e) {
var msg = e.data;
if (msg.type === 'compute') {
var { tileX, tileY, tileW, tileH, canvasW, canvasH, centerRe, centerIm, scale, maxIter, setMode, juliaCRe, juliaCIm } = msg;
var result = new Float64Array(tileW * tileH);
var pixelsPerUnit = scale;
// Compute the complex plane region for this tile
// The canvas maps: left edge = centerRe - canvasW/(2*scale), right edge = centerRe + canvasW/(2*scale)
// top edge = centerIm + canvasH/(2*scale), bottom edge = centerIm - canvasH/(2*scale)
var reStart = centerRe - canvasW / (2 * pixelsPerUnit) + tileX / pixelsPerUnit;
var imStart = centerIm + canvasH / (2 * pixelsPerUnit) - tileY / pixelsPerUnit;
var dRe = 1 / pixelsPerUnit;
var dIm = -1 / pixelsPerUnit;
var idx = 0;
for (var row = 0; row < tileH; row++) {
var im = imStart + row * dIm;
var re = reStart;
for (var col = 0; col < tileW; col++) {
if (setMode === 'mandelbrot') {
result[idx] = mandelbrotIter(re, im, maxIter);
} else {
result[idx] = juliaIter(re, im, juliaCRe, juliaCIm, maxIter);
}
re += dRe;
idx++;
}
}
self.postMessage({ type: 'tileResult', token: msg.token, tileX, tileY, tileW, tileH, result }, [result.buffer]);
}
};
`;
};
// ── module: src/renderer.ts ──
__mods["src/renderer.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.initRenderer = initRenderer;
exports.startRender = startRender;
exports.cancelRender = cancelRender;
exports.recolor = recolor;
exports.fastRender = fastRender;
exports.updateFps = updateFps;
exports.isRendering = isRendering;
exports.destroy = destroy;
// Renderer: manages canvas, workers, tile-based rendering with progressive display
const config_1 = require("./config");
const state_1 = require("./state");
const math_1 = require("./math");
const color_1 = require("./color");
const workerCode_1 = require("./workerCode");
let canvas;
let ctx;
let imageData;
let pixelBuffer;
let iterBuffer;
// Worker management
let workers = [];
let renderToken = 0;
let pendingTiles = 0;
let renderComplete = true;
let renderCallback;
// FPS tracking
let frameCount = 0;
let lastFpsTime = performance.now();
let currentFps = 0;
let tileQueue = [];
let freeWorkers = [];
function initRenderer(canvasEl) {
canvas = canvasEl;
ctx = canvas.getContext('2d', { alpha: false });
resizeCanvas();
setupWorkers();
}
function resizeCanvas() {
const dpr = Math.min(window.devicePixelRatio || 1, 2);
const w = Math.floor(window.innerWidth * dpr);
const h = Math.floor(window.innerHeight * dpr);
canvas.width = w;
canvas.height = h;
canvas.style.width = window.innerWidth + 'px';
canvas.style.height = window.innerHeight + 'px';
imageData = ctx.createImageData(canvas.width, canvas.height);
pixelBuffer = imageData.data;
iterBuffer = new Float64Array(canvas.width * canvas.height);
}
function setupWorkers() {
workers.forEach(w => w.terminate());
workers = [];
freeWorkers = [];
const count = Math.max(2, config_1.CONFIG.workerCount || (navigator.hardwareConcurrency || 4));
const blob = new Blob([workerCode_1.workerSource], { type: 'application/javascript' });
const url = URL.createObjectURL(blob);
for (let i = 0; i < count; i++) {
const w = new Worker(url);
w.onmessage = handleWorkerMessage;
workers.push(w);
freeWorkers.push(i);
}
URL.revokeObjectURL(url);
}
function handleWorkerMessage(e) {
const msg = e.data;
if (msg.type !== 'tileResult')
return;
if (msg.token !== renderToken)
return;
colorizeTile(msg.tileX, msg.tileY, msg.tileW, msg.tileH, msg.result);
blitToCanvas();
pendingTiles--;
dispatchNextTile();
if (pendingTiles === 0) {
renderComplete = true;
renderCallback?.();
renderCallback = undefined;
}
}
function colorizeTile(tx, ty, tw, th, result) {
const paletteLUT = (0, color_1.getPaletteLUT)(state_1.viewState.palette, state_1.viewState.colorOffset);
const maxIter = state_1.viewState.maxIter;
const halfLut = paletteLUT.length / 4;
const cw = canvas.width;
for (let row = 0; row < th; row++) {
const py = ty + row;
for (let col = 0; col < tw; col++) {
const smoothIter = result[row * tw + col];
const px = tx + col;
const pidx = (py * cw + px) * 4;
if (smoothIter >= maxIter) {
pixelBuffer[pidx] = 0;
pixelBuffer[pidx + 1] = 0;
pixelBuffer[pidx + 2] = 0;
pixelBuffer[pidx + 3] = 255;
}
else {
const normIter = ((smoothIter % maxIter) + maxIter) / maxIter;
const lutOff = (Math.floor(normIter * halfLut) % halfLut) * 4;
pixelBuffer[pidx] = paletteLUT[lutOff];
pixelBuffer[pidx + 1] = paletteLUT[lutOff + 1];
pixelBuffer[pidx + 2] = paletteLUT[lutOff + 2];
pixelBuffer[pidx + 3] = 255;
}
}
}
}
function dispatchNextTile() {
if (freeWorkers.length === 0 || tileQueue.length === 0)
return;
const workerIdx = freeWorkers.pop();
const task = tileQueue.shift();
workers[workerIdx].postMessage({
type: 'compute', token: renderToken,
tileX: task.tx, tileY: task.ty, tileW: task.tw, tileH: task.th,
canvasW: canvas.width, canvasH: canvas.height,
centerRe: state_1.viewState.centerRe, centerIm: state_1.viewState.centerIm,
scale: state_1.viewState.scale, maxIter: state_1.viewState.maxIter,
setMode: state_1.viewState.setMode,
juliaCRe: state_1.viewState.juliaCRe, juliaCIm: state_1.viewState.juliaCIm,
});
}
function buildTileQueue() {
const queue = [];
const tileSize = config_1.CONFIG.tileSize;
for (let ty = 0; ty < canvas.height; ty += tileSize) {
for (let tx = 0; tx < canvas.width; tx += tileSize) {
queue.push({
tx, ty,
tw: Math.min(tileSize, canvas.width - tx),
th: Math.min(tileSize, canvas.height - ty),
});
}
}
// Progressive: render center tiles first for faster visual feedback
const cx = canvas.width / 2;
const cy = canvas.height / 2;
queue.sort((a, b) => {
const da = Math.abs(a.tx + a.tw / 2 - cx) + Math.abs(a.ty + a.th / 2 - cy);
const db = Math.abs(b.tx + b.tw / 2 - cx) + Math.abs(b.ty + b.th / 2 - cy);
return da - db;
});
return queue;
}
function blitToCanvas() {
ctx.putImageData(imageData, 0, 0);
}
function startRender(onComplete) {
cancelRender();
renderToken++;
renderComplete = false;
for (let i = 0; i < pixelBuffer.length; i += 4) {
pixelBuffer[i] = 8;
pixelBuffer[i + 1] = 8;
pixelBuffer[i + 2] = 12;
pixelBuffer[i + 3] = 255;
}
blitToCanvas();
renderCallback = onComplete;
tileQueue = buildTileQueue();
pendingTiles = tileQueue.length;
while (freeWorkers.length > 0 && tileQueue.length > 0)
dispatchNextTile();
}
function cancelRender() {
renderToken++;
renderComplete = true;
pendingTiles = 0;
tileQueue.length = 0;
freeWorkers.length = 0;
workers.forEach((_, i) => freeWorkers.push(i));
renderCallback = undefined;
}
function recolor() {
const paletteLUT = (0, color_1.getPaletteLUT)(state_1.viewState.palette, state_1.viewState.colorOffset);
(0, color_1.colorizeFull)(pixelBuffer, iterBuffer, paletteLUT, canvas.width, canvas.height, state_1.viewState.maxIter);
blitToCanvas();
}
function fastRender() {
const maxIter = state_1.viewState.maxIter;
(0, math_1.computeRegion)(iterBuffer, 0, 0, canvas.width, canvas.height, state_1.viewState.centerRe, state_1.viewState.centerIm, state_1.viewState.scale, maxIter, state_1.viewState.setMode, state_1.viewState.juliaCRe, state_1.viewState.juliaCIm);
const paletteLUT = (0, color_1.getPaletteLUT)(state_1.viewState.palette, state_1.viewState.colorOffset);
(0, color_1.colorizeFull)(pixelBuffer, iterBuffer, paletteLUT, canvas.width, canvas.height, maxIter);
blitToCanvas();
}
function updateFps() {
frameCount++;
const now = performance.now();
if (now - lastFpsTime >= 1000) {
currentFps = Math.round(frameCount * 1000 / (now - lastFpsTime));
frameCount = 0;
lastFpsTime = now;
}
return currentFps;
}
function isRendering() { return !renderComplete; }
function destroy() { cancelRender(); workers.forEach(w => w.terminate()); workers = []; }
};
// ── module: src/input.ts ──
__mods["src/input.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.initInput = initInput;
exports.showHud = showHud;
exports.setOnRender = setOnRender;
exports.setOnJuliaCChanged = setOnJuliaCChanged;
exports.setOnHover = setOnHover;
exports.setHudVisible = setHudVisible;
exports.resetHudTimer = resetHudTimer;
// Input handling: mouse, keyboard, wheel, drag-to-zoom
const config_1 = require("./config");
const state_1 = require("./state");
const math_1 = require("./math");
let canvas;
let zoomBox;
// Mouse state
let isDragging = false;
let dragStartX = 0, dragStartY = 0;
let dragMode = 'none';
let hasDragged = false;
// Pan state
let panStartX = 0, panStartY = 0;
let panStartRe = 0, panStartIm = 0;
function initInput(canvasEl, zoomBoxEl, onRender) {
canvas = canvasEl;
zoomBox = zoomBoxEl;
canvas.addEventListener('mousedown', onMouseDown);
canvas.addEventListener('mousemove', onMouseMove);
canvas.addEventListener('mouseup', onMouseUp);
canvas.addEventListener('wheel', onWheel, { passive: false });
canvas.addEventListener('contextmenu', e => e.preventDefault());
window.addEventListener('keydown', onKeyDown);
}
function onMouseDown(e) {
if (e.button !== 0)
return; // only left click
isDragging = true;
dragStartX = e.clientX;
dragStartY = e.clientY;
hasDragged = false;
dragMode = 'zoom';
zoomBox.style.left = e.clientX + 'px';
zoomBox.style.top = e.clientY + 'px';
zoomBox.style.width = '0px';
zoomBox.style.height = '0px';
zoomBox.style.display = 'block';
}
function onMouseMove(e) {
if (!isDragging) {
// Update crosshair / julia preview on hover
onHover?.(e.clientX, e.clientY);
return;
}
const dx = e.clientX - dragStartX;
const dy = e.clientY - dragStartY;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 5) {
hasDragged = true;
}
if (dragMode === 'zoom') {
// Show zoom box
const x = Math.min(dragStartX, e.clientX);
const y = Math.min(dragStartY, e.clientY);
const w = Math.abs(dx);
const h = Math.abs(dy);
zoomBox.style.left = x + 'px';
zoomBox.style.top = y + 'px';
zoomBox.style.width = w + 'px';
zoomBox.style.height = h + 'px';
}
}
function onMouseUp(e) {
if (!isDragging)
return;
isDragging = false;
zoomBox.style.display = 'none';
const dx = e.clientX - dragStartX;
const dy = e.clientY - dragStartY;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dragMode === 'zoom' && dist >= config_1.CONFIG.zoomBoxMinSize) {
// Zoom into the rectangle
(0, state_1.pushZoomHistory)();
const x1 = Math.min(dragStartX, e.clientX);
const y1 = Math.min(dragStartY, e.clientY);
const x2 = Math.max(dragStartX, e.clientX);
const y2 = Math.max(dragStartY, e.clientY);
const c1 = (0, math_1.screenToComplex)(x1, y1, canvas.clientWidth, canvas.clientHeight);
const c2 = (0, math_1.screenToComplex)(x2, y2, canvas.clientWidth, canvas.clientHeight);
state_1.viewState.centerRe = (c1.re + c2.re) / 2;
state_1.viewState.centerIm = (c1.im + c2.im) / 2;
// New scale: fit the rectangle
const pixelW = x2 - x1;
const pixelH = y2 - y1;
const complexW = Math.abs(c2.re - c1.re);
const complexH = Math.abs(c2.im - c1.im);
// Use the smaller dimension to ensure the rectangle fills the view
const newScale = Math.min(pixelW / complexW, pixelH / complexH);
state_1.viewState.scale = newScale;
checkMaxMagnification();
onRender?.();
}
else if (dist < config_1.CONFIG.zoomBoxMinSize) {
// Click without drag — pick Julia c-value in Mandelbrot mode
if (state_1.viewState.setMode === 'mandelbrot') {
const c = (0, math_1.screenToComplex)(e.clientX, e.clientY, canvas.clientWidth, canvas.clientHeight);
state_1.viewState.juliaCRe = c.re;
state_1.viewState.juliaCIm = c.im;
onJuliaCChanged?.();
}
}
}
function onWheel(e) {
e.preventDefault();
(0, state_1.pushZoomHistory)();
const rect = canvas.getBoundingClientRect();
const mx = e.clientX - rect.left;
const my = e.clientY - rect.top;
// Zoom toward cursor
const complexPoint = (0, math_1.screenToComplex)(mx, my, canvas.clientWidth, canvas.clientHeight);
const factor = e.deltaY > 0 ? 1 / config_1.CONFIG.wheelZoomSpeed : config_1.CONFIG.wheelZoomSpeed;
state_1.viewState.scale *= factor;
// Adjust center so the cursor point stays fixed
const newComplexPoint = (0, math_1.screenToComplex)(mx, my, canvas.clientWidth, canvas.clientHeight);
state_1.viewState.centerRe += complexPoint.re - newComplexPoint.re;
state_1.viewState.centerIm += complexPoint.im - newComplexPoint.im;
checkMaxMagnification();
onRender?.();
}
function onKeyDown(e) {
const panAmount = 0.05 / state_1.viewState.scale;
const zoomIn = 1.5;
const zoomOut = 1 / 1.5;
switch (e.key) {
case 'ArrowLeft':
state_1.viewState.centerRe -= panAmount;
onRender?.();
e.preventDefault();
break;
case 'ArrowRight':
state_1.viewState.centerRe += panAmount;
onRender?.();
e.preventDefault();
break;
case 'ArrowUp':
state_1.viewState.centerIm += panAmount;
onRender?.();
e.preventDefault();
break;
case 'ArrowDown':
state_1.viewState.centerIm -= panAmount;
onRender?.();
e.preventDefault();
break;
case '+':
case '=':
(0, state_1.pushZoomHistory)();
state_1.viewState.scale *= zoomIn;
checkMaxMagnification();
onRender?.();
e.preventDefault();
break;
case '-':
case '_':
(0, state_1.pushZoomHistory)();
state_1.viewState.scale *= zoomOut;
onRender?.();
e.preventDefault();
break;
case 'j':
case 'J':
toggleJuliaMode();
e.preventDefault();
break;
case 'Escape':
hideHud = false;
showHud();
e.preventDefault();
break;
}
}
function checkMaxMagnification() {
const mag = (0, state_1.magnification)();
if (mag > config_1.CONFIG.maxMagnification) {
state_1.viewState.scale = 150 * config_1.CONFIG.maxMagnification;
}
}
// Julia mode toggle
function toggleJuliaMode() {
if (state_1.viewState.setMode === 'mandelbrot') {
state_1.viewState.setMode = 'julia';
onRender?.();
}
else {
state_1.viewState.setMode = 'mandelbrot';
onRender?.();
}
}
// HUD visibility
let hideHud = false;
let hudHideTimer = null;
function showHud() {
const hud = document.getElementById('hud');
if (hud)
hud.classList.remove('hidden');
resetHudHideTimer();
}
function resetHudHideTimer() {
if (hudHideTimer)
clearTimeout(hudHideTimer);
if (config_1.CONFIG.hudHideDelay > 0) {
hudHideTimer = window.setTimeout(() => {
const hud = document.getElementById('hud');
if (hud)
hud.classList.add('hidden');
}, config_1.CONFIG.hudHideDelay);
}
else {
const hud = document.getElementById('hud');
if (hud)
hud.classList.remove('hidden');
}
}
// Callbacks
let onRender;
let onJuliaCChanged;
let onHover;
function setOnRender(cb) { onRender = cb; }
function setOnJuliaCChanged(cb) { onJuliaCChanged = cb; }
function setOnHover(cb) { onHover = cb; }
function setHudVisible(visible) {
const hud = document.getElementById('hud');
if (hud) {
if (visible)
hud.classList.remove('hidden');
else
hud.classList.add('hidden');
}
}
function resetHudTimer() { resetHudHideTimer(); }
};
// ── module: src/ui.ts ──
__mods["src/ui.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.initUI = initUI;
exports.updateHUD = updateHUD;
exports.updateFps = updateFps;
exports.showJuliaPreview = showJuliaPreview;
exports.updateJuliaButton = updateJuliaButton;
exports.setOnRender = setOnRender;
exports.setOnRecolor = setOnRecolor;
exports.setOnReset = setOnReset;
exports.setOnBack = setOnBack;
exports.setOnToggleJulia = setOnToggleJulia;
// UI: HUD updates, palette selector, bookmarks
const config_1 = require("./config");
const state_1 = require("./state");
// DOM references
let zoomLevelEl;
let centerCoordsEl;
let iterInfoEl;
let fpsDisplayEl;
let iterSlider;
let colorSlider;
let paletteBar;
let bookmarkModal;
let bookmarkList;
let juliaPreview;
function initUI() {
zoomLevelEl = document.getElementById('zoom-level');
centerCoordsEl = document.getElementById('center-coords');
iterInfoEl = document.getElementById('iter-info');
fpsDisplayEl = document.getElementById('fps-display');
iterSlider = document.getElementById('iter-slider');
colorSlider = document.getElementById('color-slider');
paletteBar = document.getElementById('palette-bar');
bookmarkModal = document.getElementById('bookmark-modal');
bookmarkList = document.getElementById('bookmark-list');
juliaPreview = document.getElementById('julia-preview');
// Build palette swatches
buildPaletteSwatches();
// Slider events
iterSlider.addEventListener('input', () => {
state_1.viewState.maxIter = parseInt(iterSlider.value);
onRender?.();
});
colorSlider.addEventListener('input', () => {
state_1.viewState.colorOffset = parseInt(colorSlider.value);
onRecolor?.();
});
// Button events
document.getElementById('btn-reset').addEventListener('click', () => {
onReset?.();
});
document.getElementById('btn-back').addEventListener('click', () => {
onBack?.();
});
document.getElementById('btn-julia').addEventListener('click', () => {
onToggleJulia?.();
});
document.getElementById('btn-bookmark').addEventListener('click', () => {
showBookmarkDialog();
});
document.getElementById('btn-close-bookmarks').addEventListener('click', () => {
bookmarkModal.style.display = 'none';
});
// Initial UI update
updateHUD();
}
function buildPaletteSwatches() {
paletteBar.innerHTML = '';
config_1.PALETTES.forEach((pal, i) => {
const swatch = document.createElement('div');
swatch.className = 'palette-swatch' + (i === state_1.viewState.palette ? ' active' : '');
swatch.title = pal[0];
// Create a gradient background from the palette stops
const stops = pal[1];
const colors = stops.map(s => `rgb(${s[1]},${s[2]},${s[3]})`).join(',');
swatch.style.background = `linear-gradient(135deg, ${colors})`;
swatch.addEventListener('click', () => {
state_1.viewState.palette = i;
document.querySelectorAll('.palette-swatch').forEach(s => s.classList.remove('active'));
swatch.classList.add('active');
onRender?.();
});
paletteBar.appendChild(swatch);
});
}
function updateHUD() {
const mag = (0, state_1.magnification)();
zoomLevelEl.textContent = '×' + formatNumber(mag);
centerCoordsEl.textContent = `Re: ${state_1.viewState.centerRe.toPrecision(10)}, Im: ${state_1.viewState.centerIm.toPrecision(10)}`;
const autoIter = (0, state_1.calcMaxIter)();
iterInfoEl.textContent = `Iterations: ${state_1.viewState.maxIter} (auto: ${autoIter})`;
iterSlider.max = Math.max(50, autoIter * 2).toString();
if (state_1.viewState.maxIter !== parseInt(iterSlider.value)) {
iterSlider.value = state_1.viewState.maxIter.toString();
}
colorSlider.value = state_1.viewState.colorOffset.toString();
}
function updateFps(fps) {
fpsDisplayEl.textContent = `FPS: ${fps}`;
}
function formatNumber(n) {
if (n >= 1e15)
return n.toExponential(2);
if (n >= 1e10)
return n.toExponential(2);
if (n >= 1e6)
return (n / 1e6).toFixed(1) + 'M';
if (n >= 1e3)
return (n / 1e3).toFixed(1) + 'K';
return n.toFixed(1);
}
// Bookmark dialog
function showBookmarkDialog() {
const bookmarks = (0, state_1.loadBookmarks)();
bookmarkList.innerHTML = '';
if (bookmarks.length === 0) {
bookmarkList.innerHTML = '<p style="color:#888;padding:8px">No bookmarks saved</p>';
}
else {
bookmarks.forEach((bm, i) => {
const item = document.createElement('div');
item.className = 'bookmark-item';
const date = new Date(bm.time);
const timeStr = date.toLocaleTimeString();
item.innerHTML = `
<span>${bm.name} <small style="color:#888">(${timeStr})</small></span>
<span class="del" data-index="${i}">✕</span>
`;
item.addEventListener('click', (e) => {
if (e.target.classList.contains('del')) {
const idx = parseInt(e.target.dataset.index);
(0, state_1.deleteBookmark)(idx);
showBookmarkDialog();
}
else {
(0, state_1.restoreBookmark)(i);
bookmarkModal.style.display = 'none';
onRender?.();
}
});
bookmarkList.appendChild(item);
});
}
// Add "Save current view" button
const saveBtn = document.createElement('button');
saveBtn.textContent = 'Save Current View';
saveBtn.style.cssText = 'background:rgba(40,120,200,0.6);color:#ddd;border:1px solid rgba(255,255,255,0.2);border-radius:8px;padding:8px 16px;cursor:pointer;margin-top:8px;width:100%';
saveBtn.addEventListener('click', () => {
const name = prompt('Bookmark name:', `View ${(0, state_1.loadBookmarks)().length + 1}`);
if (name) {
(0, state_1.addBookmark)(name);
showBookmarkDialog();
}
});
bookmarkList.appendChild(saveBtn);
bookmarkModal.style.display = 'flex';
}
// Julia preview visibility
function showJuliaPreview(show) {
if (juliaPreview) {
juliaPreview.style.display = show ? 'block' : 'none';
}
}
// Update Julia mode button state
function updateJuliaButton() {
const btn = document.getElementById('btn-julia');
if (btn) {
btn.style.background = state_1.viewState.setMode === 'julia'
? 'rgba(100,60,180,0.8)'
: 'rgba(10,10,20,0.75)';
btn.textContent = state_1.viewState.setMode === 'julia' ? 'M' : 'J';
}
}
// Callbacks
let onRender;
let onRecolor;
let onReset;
let onBack;
let onToggleJulia;
function setOnRender(cb) { onRender = cb; }
function setOnRecolor(cb) { onRecolor = cb; }
function setOnReset(cb) { onReset = cb; }
function setOnBack(cb) { onBack = cb; }
function setOnToggleJulia(cb) { onToggleJulia = cb; }
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen/qwen3.6-27b","provider":"lmstudio","persona":"typescript","sessionId":"de523cf0-26c7-440c-a3f2-f2904ed6a0f4","tokensIn":2403260,"tokensOut":42270,"tokensTotal":2445530,"cost":0,"turns":63,"toolCalls":63,"failedToolCalls":1,"timestamp":"2026-08-01T06:53:54.873Z"} -->