Fractal Explorer
thinkingcap qwen3.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 58,315 bytes · SHA-256 201dac06f686
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Fractal Explorer — Mandelbrot & Julia Sets</title>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #000; font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; color: #fff; }
#app { position: relative; width: 100vw; height: 100vh; }
canvas { display: block; position: absolute; top: 0; left: 0; }
#fractal-canvas { z-index: 1; }
#zoom-overlay { z-index: 2; pointer-events: none; }
/* HUD */
#hud {
position: absolute;
top: 12px;
left: 12px;
z-index: 10;
background: rgba(10, 10, 30, 0.7);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 12px;
padding: 14px 18px;
min-width: 260px;
transition: opacity 0.4s ease, transform 0.4s ease;
font-size: 13px;
line-height: 1.5;
}
#hud.hidden {
opacity: 0;
pointer-events: none;
transform: translateY(-8px);
}
#hud-top, #hud-center, #hud-bottom {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
}
#hud-center { margin: 4px 0; }
#mode-display { font-weight: 600; color: #8cf; text-transform: uppercase; letter-spacing: 0.5px; font-size: 11px; }
#mag-display { font-variant-numeric: tabular-nums; color: #adf; }
#center-display { font-family: 'SF Mono', 'Fira Code', monospace; font-size: 12px; color: #9ab; word-break: break-all; }
#iter-display, #fps-display { font-variant-numeric: tabular-nums; color: #8a9; font-size: 12px; }
/* Palette chips */
#palette-container {
display: flex;
gap: 6px;
margin-top: 10px;
}
.palette-chip {
width: 32px;
height: 18px;
border-radius: 4px;
cursor: pointer;
border: 2px solid transparent;
transition: border-color 0.2s, transform 0.15s;
}
.palette-chip:hover { transform: scale(1.1); }
.palette-chip.active { border-color: #fff; box-shadow: 0 0 6px rgba(255,255,255,0.4); }
/* Controls */
#controls-row {
display: flex;
gap: 6px;
margin-top: 10px;
}
#controls-row button {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
color: #fff;
padding: 4px 10px;
border-radius: 6px;
cursor: pointer;
font-size: 12px;
transition: background 0.2s;
}
#controls-row button:hover { background: rgba(255, 255, 255, 0.2); }
/* Bookmarks */
#bookmark-section { margin-top: 10px; border-top: 1px solid rgba(255,255,255,0.1); padding-top: 8px; }
#bookmark-name {
background: rgba(255,255,255,0.08);
border: 1px solid rgba(255,255,255,0.2);
color: #fff;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
width: 130px;
}
#btn-save-bookmark {
background: rgba(100, 150, 255, 0.3);
border: 1px solid rgba(100, 150, 255, 0.4);
color: #fff;
padding: 4px 8px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
#bookmark-list { list-style: none; margin-top: 6px; }
.bookmark-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 3px 0;
font-size: 12px;
}
.bookmark-name { cursor: pointer; color: #9cf; }
.bookmark-name:hover { text-decoration: underline; }
.bookmark-delete {
background: none;
border: none;
color: #f66;
cursor: pointer;
font-size: 14px;
padding: 0 4px;
}
/* Help overlay */
#help-overlay {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 10;
background: rgba(10, 10, 30, 0.75);
backdrop-filter: blur(8px);
border-radius: 10px;
padding: 12px 24px;
text-align: center;
font-size: 13px;
color: #abc;
transition: opacity 1.5s ease;
}
#help-overlay p { margin: 2px 0; }
/* ── css: settings ── */
#settings-row {
display: flex;
align-items: center;
gap: 12px;
margin-top: 8px;
font-size: 12px;
}
#settings-row label {
display: flex;
align-items: center;
gap: 4px;
color: #9ab;
cursor: pointer;
}
#toggle-cycling {
accent-color: #6af;
cursor: pointer;
}
#iter-slider {
accent-color: #6af;
cursor: pointer;
}
#iter-value {
font-variant-numeric: tabular-nums;
color: #8a9;
min-width: 30px;
}
/* Progress bar */
#render-progress {
margin-top: 6px;
width: 100%;
height: 3px;
background: rgba(255,255,255,0.1);
border-radius: 2px;
overflow: hidden;
}
.progress-bar {
width: 100%;
height: 100%;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #4af, #6cf);
border-radius: 2px;
transition: width 0.1s ease;
width: 0%;
}
</style>
</head>
<body>
<div id="app">
<canvas id="fractal-canvas"></canvas>
<canvas id="zoom-overlay"></canvas>
<!-- HUD -->
<div id="hud" class="hidden">
<div id="hud-top">
<span id="mode-display">Mandelbrot</span>
<span id="mag-display">×1.0</span>
</div>
<div id="hud-center">
<span id="center-display">-0.5, 0i</span>
</div>
<div id="hud-bottom">
<span id="iter-display">iter: 100</span>
<span id="fps-display">60 fps</span>
</div>
<!-- Palette selector -->
<div id="palette-container"></div>
<!-- Controls -->
<div id="controls-row">
<button id="btn-back" title="Back (Z)">← Back</button>
<button id="btn-reset" title="Reset (R)">⟲ Reset</button>
<button id="btn-julia" title="Toggle Julia (J)">Julia</button>
</div><div id="settings-row">
<label title="Toggle palette cycling animation">
<input type="checkbox" id="toggle-cycling"> Cycle colors
</label>
<label title="Override iteration count">
Iter: <input type="range" id="iter-slider" min="50" max="5000" step="10" value="auto" style="width:80px">
<span id="iter-value"></span>
</label>
</div>
<div id="render-progress" style="display:none">
<div class="progress-bar"><div class="progress-fill" id="progress-fill"></div></div>
</div>
<!-- Bookmarks -->
<div id="bookmark-section">
<input type="text" id="bookmark-name" placeholder="Bookmark name...">
<button id="btn-save-bookmark">Save</button>
<ul id="bookmark-list"></ul>
</div>
</div>
<!-- Help overlay -->
<div id="help-overlay">
<p><strong>Fractal Explorer</strong></p>
<p>Scroll to zoom · Drag to select region · Right-click drag to pan</p>
<p>Arrow keys to pan · +/- to zoom · J for Julia mode · Z back · R reset</p>
</div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./config":"src/config.ts","./renderer":"src/renderer.ts","./navigation":"src/navigation.ts","./ui":"src/ui.ts","./palette":"src/palette.ts","./bookmarks":"src/bookmarks.ts"},"src/math.ts":{"./config":"src/config.ts"},"src/renderer.ts":{"./config":"src/config.ts","./math":"src/math.ts","./palette":"src/palette.ts"},"src/navigation.ts":{"./config":"src/config.ts","./renderer":"src/renderer.ts"},"src/bookmarks.ts":{"./renderer":"src/renderer.ts","./palette":"src/palette.ts"},"src/ui.ts":{"./config":"src/config.ts","./renderer":"src/renderer.ts","./palette":"src/palette.ts","./bookmarks":"src/bookmarks.ts"}};
function __require(id) {
if (__cache[id]) return __cache[id].exports;
var module = __cache[id] = { exports: {} };
var factory = __mods[id];
if (!factory) throw new Error("Module not found: " + id);
factory(module.exports, function (spec) {
var target = (__map[id] && __map[id][spec]) || spec;
return __require(target);
}, module);
return module.exports;
}
// ── module: src/main.ts ──
__mods["src/main.ts"] = function (exports, require, module) {
"use strict";
// Entry module — wires up renderer, navigation, UI, and main loop
Object.defineProperty(exports, "__esModule", { value: true });
const config_1 = require("./config");
const renderer_1 = require("./renderer");
const navigation_1 = require("./navigation");
const ui_1 = require("./ui");
const palette_1 = require("./palette");
const bookmarks_1 = require("./bookmarks");
// ─── Canvas setup ──────────────────────────────────────────────────
const canvas = document.getElementById('fractal-canvas');
const overlay = document.getElementById('zoom-overlay');
function resizeCanvases() {
const w = window.innerWidth;
const h = window.innerHeight;
canvas.width = w;
canvas.height = h;
overlay.width = w;
overlay.height = h;
}
resizeCanvases();
window.addEventListener('resize', () => {
resizeCanvases();
scheduleRender();
});
// ─── Core objects ──────────────────────────────────────────────────
const renderer = new renderer_1.FractalRenderer(canvas);
const navigation = new navigation_1.Navigation(canvas, renderer.getView.bind(renderer), renderer.setView.bind(renderer));
const ui = new ui_1.FractalUI();
let currentPaletteName = 'fire';
renderer.setPaletteStops((0, palette_1.getPalette)(currentPaletteName));
// ─── Render scheduling ─────────────────────────────────────────────
let renderPending = false;
let renderInFlight = false;
function scheduleRender() {
if (renderInFlight) {
renderer.cancelRender();
}
renderPending = true;
}
async function executeRender() {
renderPending = false;
renderInFlight = true;
await renderer.progressiveRender();
renderInFlight = false;
// If another render was scheduled during this one, do it now
if (renderPending) {
await executeRender();
}
}
// ─── Palette cycling ──────────────────────────────────────────────
let paletteCyclingEnabled = false;
let lastCycleTime = performance.now();
let cycleAccumulator = 0;
function updatePaletteCycling() {
if (!paletteCyclingEnabled)
return;
const now = performance.now();
const dt = (now - lastCycleTime) / 1000;
lastCycleTime = now;
cycleAccumulator += config_1.CONFIG.paletteCycleSpeed * dt;
// Re-render when enough offset has accumulated for visible change
if (cycleAccumulator > 0.02) {
const state = renderer.getRenderState();
state.paletteOffset += cycleAccumulator;
renderer.setPaletteOffset(state.paletteOffset);
cycleAccumulator = 0;
scheduleRender();
}
}
// ─── Main loop ─────────────────────────────────────────────────────
function mainLoop() {
updatePaletteCycling();
// Draw zoom rectangle overlay
const dragRect = navigation.getDragRect();
if (dragRect) {
ui.drawZoomRect(dragRect.x1, dragRect.y1, dragRect.x2, dragRect.y2);
}
else {
ui.clearZoomRect();
}
// Update HUD
const view = renderer.getView();
const state = renderer.getRenderState();
ui.updateHud(view, state.maxIter);
if (renderPending && !renderInFlight) {
executeRender();
}
requestAnimationFrame(mainLoop);
}
// ─── Navigation events ────────────────────────────────────────────
navigation.setCallbacks((event) => {
switch (event.type) {
case 'zoom':
case 'pan':
scheduleRender();
ui.showHud();
break;
case 'juliaSelect':
const v = renderer.getView();
if (v.setMode === 'mandelbrot') {
renderer.setView({
setMode: 'julia',
juliaCre: event.data.re,
juliaCim: event.data.im,
});
scheduleRender();
ui.showHud();
}
break;
case 'back':
case 'reset':
scheduleRender();
ui.updateBookmarks();
break;
}
});
// ─── UI callbacks ─────────────────────────────────────────────────
ui.setCallbacks({
onPaletteChange: (name) => {
currentPaletteName = name;
renderer.setPaletteStops((0, palette_1.getPalette)(name));
scheduleRender();
},
onBookmarkLoad: (entry) => {
renderer.setView(entry.view);
if (entry.paletteName && palette_1.PALETTE_NAMES.includes(entry.paletteName)) {
currentPaletteName = entry.paletteName;
renderer.setPaletteStops((0, palette_1.getPalette)(entry.paletteName));
}
scheduleRender();
ui.updateBookmarks();
},
});
// ─── Button handlers ──────────────────────────────────────────────
document.getElementById('btn-back')?.addEventListener('click', () => {
navigation.goBack();
scheduleRender();
});
document.getElementById('btn-reset')?.addEventListener('click', () => {
navigation.resetView();
scheduleRender();
});
document.getElementById('btn-julia')?.addEventListener('click', () => {
const v = renderer.getView();
if (v.setMode === 'mandelbrot') {
renderer.setView({ setMode: 'julia', juliaCre: v.centerRe, juliaCim: v.centerIm });
}
else {
renderer.setView({ setMode: 'mandelbrot' });
}
scheduleRender();
});
// Bookmark save
const bookmarkNameInput = document.getElementById('bookmark-name');
document.getElementById('btn-save-bookmark')?.addEventListener('click', () => {
const name = bookmarkNameInput.value.trim();
if (!name)
return;
(0, bookmarks_1.saveBookmark)(name, renderer.getView(), currentPaletteName);
bookmarkNameInput.value = '';
ui.updateBookmarks();
});
// Palette cycling toggle
const cyclingToggle = document.getElementById('toggle-cycling');
if (cyclingToggle) {
cyclingToggle.addEventListener('change', () => {
paletteCyclingEnabled = cyclingToggle.checked;
renderer.setCycling(paletteCyclingEnabled);
});
}
// Iteration override slider
const iterSlider = document.getElementById('iter-slider');
const iterValueDisplay = document.getElementById('iter-value');
if (iterSlider) {
// Initialize display with current value
const updateIterDisplay = () => {
const state = renderer.getRenderState();
if (iterValueDisplay) {
iterValueDisplay.textContent = state.maxIter.toString();
}
};
updateIterDisplay();
iterSlider.addEventListener('input', () => {
const val = parseInt(iterSlider.value);
if (!isNaN(val)) {
renderer.getRenderState().maxIter = val;
updateIterDisplay();
}
});
}
// Render progress bar
const renderProgressEl = document.getElementById('render-progress');
const progressFill = document.getElementById('progress-fill');
renderer.setCallbacks((fraction) => {
if (renderProgressEl && progressFill) {
renderProgressEl.style.display = 'block';
progressFill.style.width = `${Math.round(fraction * 100)}%`;
}
}, () => {
if (renderProgressEl) {
setTimeout(() => { renderProgressEl.style.display = 'none'; }, 300);
}
});
// ─── URL hash state loading ───────────────────────────────────────
const hashState = (0, bookmarks_1.decodeStateFromHash)();
if (hashState) {
renderer.setView(hashState.view);
if (hashState.paletteName && palette_1.PALETTE_NAMES.includes(hashState.paletteName)) {
currentPaletteName = hashState.paletteName;
renderer.setPaletteStops((0, palette_1.getPalette)(currentPaletteName));
}
}
// ─── Encode state to URL on view change ──────────────────────────
let urlUpdateTimer = null;
function scheduleUrlUpdate() {
if (urlUpdateTimer)
clearTimeout(urlUpdateTimer);
urlUpdateTimer = setTimeout(() => {
const v = renderer.getView();
window.location.hash = (0, bookmarks_1.encodeStateToHash)(v, currentPaletteName);
}, 500);
}
// ─── Debug interface on window ────────────────────────────────────
const fractalDebug = {
iterationsAt: (re, im) => renderer.iterationsAt(re, im),
setView: (params) => {
renderer.setView(params);
scheduleRender();
},
getView: () => renderer.getView(),
getRenderState: () => renderer.getRenderState(),
};
window.fractal = fractalDebug;
// ─── Help overlay auto-hide ──────────────────────────────────────
const helpOverlay = document.getElementById('help-overlay');
setTimeout(() => {
if (helpOverlay) {
helpOverlay.style.opacity = '0';
setTimeout(() => { if (helpOverlay)
helpOverlay.remove(); }, 1500);
}
}, 6000);
// ─── Start ────────────────────────────────────────────────────────
scheduleRender();
ui.updateBookmarks();
requestAnimationFrame(mainLoop);
};
// ── module: src/config.ts ──
__mods["src/config.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CONFIG = void 0;
// Tunable constants for the fractal explorer
exports.CONFIG = {
// Rendering
maxIterationsBase: 100,
maxIterationsScale: 50, // added per log2(magnification)
maxIterationsCap: 10000,
progressivePasses: 4, // number of refinement passes (1/4 → full)
tileSize: 64, // worker tile size in pixels
// Zoom & Navigation
zoomSpeed: 1.5, // wheel zoom factor per tick
dragZoomMinPixels: 10, // minimum drag distance to trigger zoom
maxMagnification: 1e13, // precision limit for double-precision floats
panStepFraction: 0.1, // keyboard pan as fraction of view
// Julia preview
juliaPreviewEnabled: true,
juliaPreviewSize: 200, // pixels for the Julia preview pane
// UI
hudHideDelayMs: 3000,
paletteCycleSpeed: 0.5, // offset per second when cycling enabled
// Debug
debugEnabled: false,
};
};
// ── module: src/math.ts ──
__mods["src/math.ts"] = function (exports, require, module) {
"use strict";
// Fractal iteration math — allocation-free core loop
Object.defineProperty(exports, "__esModule", { value: true });
exports.mandelbrotIteration = mandelbrotIteration;
exports.juliaIteration = juliaIteration;
exports.computeMaxIterations = computeMaxIterations;
/**
* Compute the smooth (continuous) iteration count for a point.
* Returns maxIter if the point is in the set, otherwise the fractional iteration count.
* Smooth coloring formula: n + 1 - log2(log|z| / log(2))
*/
function mandelbrotIteration(cre, cim, maxIter) {
let zre = 0;
let zim = 0;
let zre2 = 0;
let zim2 = 0;
let i = 0;
while (zre2 + zim2 <= 4 && i < maxIter) {
zim = 2 * zre * zim + cim;
zre = zre2 - zim2 + cre;
zre2 = zre * zre;
zim2 = zim * zim;
i++;
}
if (i === maxIter)
return maxIter; // interior point
// Smooth iteration count: n + 1 - log2(log|z| / log(2))
const log_zn = Math.log(zre2 + zim2);
const nu = Math.log(log_zn / Math.LN2) / Math.LN2;
return i + 1 - nu;
}
/**
* Julia set iteration for a given c-value.
*/
function juliaIteration(x0, y0, cre, cim, maxIter) {
let zre = x0;
let zim = y0;
let zre2 = zre * zre;
let zim2 = zim * zim;
let i = 0;
while (zre2 + zim2 <= 4 && i < maxIter) {
zim = 2 * zre * zim + cim;
zre = zre2 - zim2 + cre;
zre2 = zre * zre;
zim2 = zim * zim;
i++;
}
if (i === maxIter)
return maxIter;
const log_zn = Math.log(zre2 + zim2);
const nu = Math.log(log_zn / Math.LN2) / Math.LN2;
return i + 1 - nu;
}
/**
* Compute the effective iteration count based on magnification.
*/
function computeMaxIterations(magnification) {
const logMag = Math.log2(Math.max(1, magnification));
const iterations = config_1.CONFIG.maxIterationsBase + config_1.CONFIG.maxIterationsScale * logMag;
return Math.floor(Math.min(iterations, config_1.CONFIG.maxIterationsCap));
}
// Re-export config reference for iteration computation
const config_1 = require("./config");
};
// ── module: src/palette.ts ──
__mods["src/palette.ts"] = function (exports, require, module) {
"use strict";
// Color palettes with smooth interpolation between stops
Object.defineProperty(exports, "__esModule", { value: true });
exports.PALETTE_NAMES = void 0;
exports.getPalette = getPalette;
exports.sampleColor = sampleColor;
exports.colorIteration = colorIteration;
/** Named palette definitions — each is an array of [t, r, g, b] stops */
const PALETTE_DATA = {
fire: [
[0.0, 0, 0, 0],
[0.15, 128, 0, 0],
[0.3, 255, 80, 0],
[0.45, 255, 200, 0],
[0.6, 255, 255, 100],
[0.75, 255, 255, 255],
[1.0, 200, 200, 255],
],
ocean: [
[0.0, 0, 0, 30],
[0.2, 0, 20, 80],
[0.4, 0, 60, 160],
[0.55, 0, 120, 200],
[0.7, 30, 180, 220],
[0.85, 100, 220, 240],
[1.0, 200, 255, 255],
],
monochrome: [
[0.0, 0, 0, 0],
[0.3, 60, 60, 60],
[0.6, 140, 140, 140],
[0.85, 200, 200, 200],
[1.0, 255, 255, 255],
],
psychedelic: [
[0.0, 128, 0, 128],
[0.167, 128, 128, 0],
[0.333, 0, 128, 128],
[0.5, 128, 0, 0],
[0.667, 0, 128, 0],
[0.833, 0, 0, 128],
[1.0, 128, 0, 128],
],
electric: [
[0.0, 5, 0, 20],
[0.2, 30, 0, 100],
[0.4, 60, 0, 200],
[0.55, 100, 50, 255],
[0.7, 180, 100, 255],
[0.85, 230, 180, 255],
[1.0, 255, 255, 255],
],
};
const PALETTES = {};
for (const [name, data] of Object.entries(PALETTE_DATA)) {
PALETTES[name] = data.map(([t, r, g, b]) => ({ t, r, g, b }));
}
exports.PALETTE_NAMES = Object.keys(PALETTES);
/** Get palette stops by name */
function getPalette(name) {
return PALETTES[name] || PALETTES.fire;
}
/**
* Sample a color from the palette at position t ∈ [0, 1].
* Smoothly interpolates between stops.
*/
function sampleColor(stops, t) {
// Clamp and wrap for cyclic palettes
t = ((t % 1) + 1) % 1;
if (t <= stops[0].t)
return [stops[0].r, stops[0].g, stops[0].b];
if (t >= stops[stops.length - 1].t) {
const last = stops[stops.length - 1];
return [last.r, last.g, last.b];
}
// Find surrounding stops
let lo = 0;
for (let i = 1; i < stops.length; i++) {
if (stops[i].t > t)
break;
lo = i;
}
const hi = lo + 1;
const s0 = stops[lo];
const s1 = stops[hi];
// Linear interpolation between stops
const frac = (t - s0.t) / (s1.t - s0.t);
return [
Math.round(s0.r + frac * (s1.r - s0.r)),
Math.round(s0.g + frac * (s1.g - s0.g)),
Math.round(s0.b + frac * (s1.b - s0.b)),
];
}
/**
* Colorize a single iteration value into RGB.
* maxIter is the threshold for interior points (rendered black).
*/
function colorIteration(iter, maxIter, stops, offset = 0) {
if (iter >= maxIter)
return [0, 0, 0]; // interior → black
const t = ((iter / maxIter) + offset) % 1;
return sampleColor(stops, t);
}
};
// ── module: src/renderer.ts ──
__mods["src/renderer.ts"] = function (exports, require, module) {
"use strict";
// Rendering engine — two-phase pipeline: workers compute iterations, main thread colorizes
Object.defineProperty(exports, "__esModule", { value: true });
exports.FractalRenderer = void 0;
const config_1 = require("./config");
const math_1 = require("./math");
const palette_1 = require("./palette");
// ─── Worker code (inline via Blob URL) — computes iteration counts only ──
const WORKER_CODE = `
self.onmessage = function(e) {
const d = e.data;
const { tileX, tileY, tileW, tileH, width, height, centerRe, centerIm, scale, maxIter, setMode, juliaCre, juliaCim } = d;
const halfW = width / (2 * scale);
const halfH = height / (2 * scale);
const reMin = centerRe - halfW;
const imMax = centerIm + halfH;
// Output: Float32Array of iteration counts
const buf = new Float32Array(tileW * tileH);
for (let ty = 0; ty < tileH; ty++) {
for (let tx = 0; tx < tileW; tx++) {
const px = tileX + tx;
const py = tileY + ty;
const cre = reMin + (px / width) * (2 * halfW);
const cim = imMax - (py / height) * (2 * halfH);
let iter;
if (setMode === 'julia') {
iter = juliaIteration(cre, cim, juliaCre, juliaCim, maxIter);
} else {
iter = mandelbrotIteration(cre, cim, maxIter);
}
buf[ty * tileW + tx] = iter;
}
}
self.postMessage({ tileX, tileY, tileW, tileH, buf }, [buf.buffer]);
};
function mandelbrotIteration(cre, cim, maxIter) {
let zre = 0, zim = 0, zre2 = 0, zim2 = 0, i = 0;
while (zre2 + zim2 <= 4 && i < maxIter) {
zim = 2 * zre * zim + cim;
zre = zre2 - zim2 + cre;
zre2 = zre * zre;
zim2 = zim * zim;
i++;
}
if (i === maxIter) return maxIter;
const log_zn = Math.log(zre2 + zim2);
const nu = Math.log(log_zn / Math.LN2) / Math.LN2;
return i + 1 - nu;
}
function juliaIteration(x0, y0, cre, cim, maxIter) {
let zre = x0, zim = y0, zre2 = x0*x0, zim2 = y0*y0, i = 0;
while (zre2 + zim2 <= 4 && i < maxIter) {
zim = 2 * zre * zim + cim;
zre = zre2 - zim2 + cre;
zre2 = zre * zre;
zim2 = zim * zim;
i++;
}
if (i === maxIter) return maxIter;
const log_zn = Math.log(zre2 + zim2);
const nu = Math.log(log_zn / Math.LN2) / Math.LN2;
return i + 1 - nu;
}
`;
let workerBlobUrl = null;
function createWorker() {
if (!workerBlobUrl) {
const blob = new Blob([WORKER_CODE], { type: 'application/javascript' });
workerBlobUrl = URL.createObjectURL(blob);
}
return new Worker(workerBlobUrl);
}
// ─── Main renderer ──────────────────────────────────────────────────
class FractalRenderer {
constructor(canvas) {
this.imageData = null;
this.pixelBuffer32 = null;
// Iteration buffer — stores smooth iteration counts per pixel
this.iterBuffer = null;
// Cancellation
this.cancelled = false;
this.canvas = canvas;
this.ctx = canvas.getContext('2d', { willReadFrequently: false });
this.view = {
centerRe: -0.5,
centerIm: 0,
scale: 150,
setMode: 'mandelbrot',
juliaCre: -0.7269,
juliaCim: 0.1889,
};
this.renderState = {
paletteStops: [],
paletteOffset: 0,
maxIter: config_1.CONFIG.maxIterationsBase,
cyclingEnabled: false,
};
}
setView(view) {
if (view.re !== undefined)
this.view.centerRe = view.re;
if (view.im !== undefined)
this.view.centerIm = view.im;
if (view.centerRe !== undefined)
this.view.centerRe = view.centerRe;
if (view.centerIm !== undefined)
this.view.centerIm = view.centerIm;
if (view.scale !== undefined)
this.view.scale = view.scale;
if (view.setMode !== undefined)
this.view.setMode = view.setMode;
if (view.juliaCre !== undefined)
this.view.juliaCre = view.juliaCre;
if (view.juliaCim !== undefined)
this.view.juliaCim = view.juliaCim;
this.updateMaxIterations();
}
getView() { return { ...this.view }; }
getRenderState() { return { ...this.renderState }; }
setPaletteStops(stops) { this.renderState.paletteStops = stops; }
setPaletteOffset(offset) { this.renderState.paletteOffset = offset; }
setCycling(enabled) { this.renderState.cyclingEnabled = enabled; }
updateMaxIterations() {
const mag = this.view.scale / 150;
this.renderState.maxIter = (0, math_1.computeMaxIterations)(mag);
}
cancelRender() { this.cancelled = true; }
/** Progressive render: coarse → fine passes */
progressiveRender() {
return new Promise(async (resolve) => {
this.cancelled = false;
const passes = config_1.CONFIG.progressivePasses; // e.g. 4
for (let pass = 0; pass < passes; pass++) {
if (this.cancelled) {
resolve();
return;
}
const resFactor = Math.pow(2, passes - 1 - pass);
await this.renderPass(resFactor);
}
// Final colorize at full resolution
this.colorize();
this.onComplete?.();
resolve();
});
}
/** Colorize-only pass — applies current palette to iteration buffer */
colorize() {
if (!this.iterBuffer || !this.pixelBuffer32)
return;
const W = this.canvas.width;
const H = this.canvas.height;
const stops = this.renderState.paletteStops;
const maxIter = this.renderState.maxIter;
const offset = this.renderState.paletteOffset;
for (let i = 0; i < W * H; i++) {
const iter = this.iterBuffer[i];
if (iter >= maxIter) {
// Interior point → black
this.pixelBuffer32[i] = 0xFF000000;
}
else {
const t = ((iter / maxIter) + offset) % 1;
const [r, g, b] = (0, palette_1.sampleColor)(stops, t);
// ABGR little-endian
this.pixelBuffer32[i] = (255 << 24) | (b << 16) | (g << 8) | r;
}
}
if (this.imageData) {
this.ctx.putImageData(this.imageData, 0, 0);
}
}
renderPass(resolutionFactor) {
return new Promise((resolve) => {
const W = this.canvas.width;
const H = this.canvas.height;
// Ensure buffers exist at full resolution
if (!this.imageData || this.imageData.width !== W) {
this.imageData = this.ctx.createImageData(W, H);
this.pixelBuffer32 = new Uint32Array(this.imageData.data.buffer);
this.iterBuffer = new Float32Array(W * H);
}
// Tile grid at full resolution
const tileSize = config_1.CONFIG.tileSize;
const cols = Math.ceil(W / tileSize);
const rows = Math.ceil(H / tileSize);
const totalTiles = cols * rows;
let completed = 0;
let activeWorkers = 0;
const maxConcurrent = Math.min(4, navigator.hardwareConcurrency || 2);
const queue = [];
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
queue.push({ col: c, row: r });
}
}
let queueIdx = 0;
const dispatchNext = () => {
while (activeWorkers < maxConcurrent && queueIdx < queue.length) {
if (this.cancelled)
break;
const task = queue[queueIdx++];
activeWorkers++;
this.renderTile(task.col * tileSize, task.row * tileSize, resolutionFactor, () => {
if (this.cancelled) {
resolve();
return;
}
completed++;
activeWorkers--;
// Colorize and blit for progressive visual feedback
this.colorize();
this.onProgress?.(completed / totalTiles);
dispatchNext();
if (completed >= queueIdx && activeWorkers === 0) {
resolve();
}
});
}
if (queueIdx >= queue.length && activeWorkers === 0 && !this.cancelled) {
resolve();
}
};
dispatchNext();
});
}
renderTile(tx, ty, resolutionFactor, onComplete) {
const W = this.canvas.width;
const H = this.canvas.height;
// Tile dimensions at full resolution
const tw = Math.min(config_1.CONFIG.tileSize, W - tx);
const th = Math.min(config_1.CONFIG.tileSize, H - ty);
// Effective tile dimensions for worker (coarse pass)
const effTw = Math.ceil(tw / resolutionFactor);
const effTh = Math.ceil(th / resolutionFactor);
const effTx = Math.floor(tx / resolutionFactor);
const effTy = Math.floor(ty / resolutionFactor);
// Effective canvas dimensions for worker
const effW = Math.ceil(W / resolutionFactor);
const effH = Math.ceil(H / resolutionFactor);
const worker = createWorker();
worker.onmessage = (e) => {
if (!this.iterBuffer) {
onComplete();
return;
}
const resultData = new Float32Array(e.data.buf);
// Write iteration data into buffer, scaling up if needed
for (let sy = 0; sy < effTh; sy++) {
for (let sx = 0; sx < effTw; sx++) {
const srcVal = resultData[sy * effTw + sx];
// Map source pixel to destination region
const dstXStart = tx + sx * resolutionFactor;
const dstYStart = ty + sy * resolutionFactor;
for (let dy = 0; dy < resolutionFactor && (dstYStart + dy) < H; dy++) {
for (let dx = 0; dx < resolutionFactor && (dstXStart + dx) < W; dx++) {
const dstIdx = (dstYStart + dy) * W + (dstXStart + dx);
if (dstIdx < this.iterBuffer.length) {
this.iterBuffer[dstIdx] = srcVal;
}
}
}
}
}
onComplete();
};
worker.onerror = () => { onComplete(); };
if (this.cancelled)
return;
worker.postMessage({
tileX: effTx, tileY: effTy, tileW: effTw, tileH: effTh,
width: effW, height: effH,
centerRe: this.view.centerRe,
centerIm: this.view.centerIm,
scale: resolutionFactor > 1 ? this.view.scale / resolutionFactor : this.view.scale,
maxIter: this.renderState.maxIter,
setMode: this.view.setMode,
juliaCre: this.view.juliaCre,
juliaCim: this.view.juliaCim,
});
}
/** Compute iteration count at a specific point (for debug) */
iterationsAt(re, im) {
const maxIter = this.renderState.maxIter;
if (this.view.setMode === 'julia') {
return (0, math_1.juliaIteration)(re, im, this.view.juliaCre, this.view.juliaCim, maxIter);
}
return (0, math_1.mandelbrotIteration)(re, im, maxIter);
}
setCallbacks(onProgress, onComplete) {
this.onProgress = onProgress;
this.onComplete = onComplete;
}
destroy() {
this.cancelRender();
if (workerBlobUrl) {
URL.revokeObjectURL(workerBlobUrl);
workerBlobUrl = null;
}
}
}
exports.FractalRenderer = FractalRenderer;
};
// ── module: src/navigation.ts ──
__mods["src/navigation.ts"] = function (exports, require, module) {
"use strict";
// Navigation — mouse/touch input for zoom, pan, drag-select rectangle
Object.defineProperty(exports, "__esModule", { value: true });
exports.Navigation = void 0;
const config_1 = require("./config");
class Navigation {
constructor(canvas, getView, setView) {
// Mouse state
this.isDragging = false;
this.dragStartX = 0;
this.dragStartY = 0;
this.dragCurrentX = 0;
this.dragCurrentY = 0;
this.isPanning = false;
this.panStartX = 0;
this.panStartY = 0;
// Zoom history
this.history = [];
this.maxHistory = 50;
this.canvas = canvas;
this.getView = getView;
this.setView = setView;
this.setupMouseEvents();
this.setupKeyboardEvents();
this.pushHistory();
}
pushHistory() {
const view = this.getView();
// Avoid duplicate consecutive entries
if (this.history.length > 0) {
const last = this.history[this.history.length - 1];
if (Math.abs(last.centerRe - view.centerRe) < 1e-15 &&
Math.abs(last.centerIm - view.centerIm) < 1e-15 &&
Math.abs(last.scale - view.scale) < 0.001) {
return;
}
}
this.history.push({ ...view });
if (this.history.length > this.maxHistory)
this.history.shift();
}
goBack() {
if (this.history.length <= 1)
return;
this.history.pop(); // remove current state
const prev = this.history[this.history.length - 1];
this.setView(prev);
this.onNavEvent?.({ type: 'back' });
}
resetView() {
this.setView({
centerRe: -0.5,
centerIm: 0,
scale: 150,
setMode: 'mandelbrot',
});
this.history = [];
this.pushHistory();
this.onNavEvent?.({ type: 'reset' });
}
screenToComplex(sx, sy) {
const view = this.getView();
const halfW = this.canvas.width / (2 * view.scale);
const halfH = this.canvas.height / (2 * view.scale);
return {
re: view.centerRe + (sx / this.canvas.width - 0.5) * 2 * halfW,
im: view.centerIm - (sy / this.canvas.height - 0.5) * 2 * halfH,
};
}
setupMouseEvents() {
// Wheel zoom toward cursor
this.canvas.addEventListener('wheel', (e) => {
e.preventDefault();
const view = this.getView();
const mouseComplex = this.screenToComplex(e.offsetX, e.offsetY);
// Zoom factor: scroll up = zoom in, down = zoom out
const direction = e.deltaY > 0 ? -1 : 1;
const zoomFactor = Math.pow(config_1.CONFIG.zoomSpeed, direction * 0.3);
const newScale = view.scale * zoomFactor;
// Enforce max magnification
const mag = newScale / 150;
if (mag > config_1.CONFIG.maxMagnification)
return;
// Adjust center so the mouse point stays fixed
const halfW = this.canvas.width / (2 * newScale);
const halfH = this.canvas.height / (2 * newScale);
this.setView({
scale: newScale,
centerRe: mouseComplex.re - (e.offsetX / this.canvas.width - 0.5) * 2 * halfW,
centerIm: mouseComplex.im + (e.offsetY / this.canvas.height - 0.5) * 2 * halfH,
});
this.pushHistory();
this.onNavEvent?.({ type: 'zoom' });
}, { passive: false });
// Click-drag for rectangle zoom or pan
let mouseDownTime = 0;
this.canvas.addEventListener('mousedown', (e) => {
if (e.button !== 0)
return; // only left click
mouseDownTime = Date.now();
this.isDragging = true;
this.dragStartX = e.offsetX;
this.dragStartY = e.offsetY;
this.dragCurrentX = e.offsetX;
this.dragCurrentY = e.offsetY;
});
window.addEventListener('mousemove', (e) => {
if (!this.isDragging)
return;
const rect = this.canvas.getBoundingClientRect();
this.dragCurrentX = Math.max(0, Math.min(this.canvas.width, e.clientX - rect.left));
this.dragCurrentY = Math.max(0, Math.min(this.canvas.height, e.clientY - rect.top));
});
window.addEventListener('mouseup', (e) => {
if (!this.isDragging)
return;
const elapsed = Date.now() - mouseDownTime;
const dx = this.dragCurrentX - this.dragStartX;
const dy = this.dragCurrentY - this.dragStartY;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist >= config_1.CONFIG.dragZoomMinPixels) {
// Rectangle zoom
const x1 = Math.min(this.dragStartX, this.dragCurrentX);
const y1 = Math.min(this.dragStartY, this.dragCurrentY);
const x2 = Math.max(this.dragStartX, this.dragCurrentX);
const y2 = Math.max(this.dragStartY, this.dragCurrentY);
// Map rectangle corners to complex plane
const topLeft = this.screenToComplex(x1, y1);
const bottomRight = this.screenToComplex(x2, y2);
const newCenterRe = (topLeft.re + bottomRight.re) / 2;
const newCenterIm = (topLeft.im + bottomRight.im) / 2;
// New scale: fit the rectangle to full canvas
const rectW = x2 - x1;
const rectH = y2 - y1;
const complexW = Math.abs(topLeft.re - bottomRight.re);
const complexH = Math.abs(topLeft.im - bottomRight.im);
// Use the dimension that requires more zoom (to fill the canvas)
const scaleX = this.canvas.width / complexW;
const scaleY = this.canvas.height / complexH;
let newScale = Math.min(scaleX, scaleY);
// Enforce max magnification
if (newScale / 150 > config_1.CONFIG.maxMagnification) {
newScale = config_1.CONFIG.maxMagnification * 150;
}
this.setView({ centerRe: newCenterRe, centerIm: newCenterIm, scale: newScale });
this.pushHistory();
this.onNavEvent?.({ type: 'zoom' });
}
else if (elapsed < 300) {
// Short click = Julia select point
const clicked = this.screenToComplex(e.offsetX, e.offsetY);
this.onNavEvent?.({
type: 'juliaSelect',
data: { re: clicked.re, im: clicked.im }
});
}
this.isDragging = false;
});
// Right-click or middle-click for panning
this.canvas.addEventListener('mousedown', (e) => {
if (e.button === 1 || e.button === 2) {
e.preventDefault();
this.isPanning = true;
this.panStartX = e.offsetX;
this.panStartY = e.offsetY;
}
});
window.addEventListener('mousemove', (e) => {
if (!this.isPanning)
return;
const dx = e.offsetX - this.panStartX;
const dy = e.offsetY - this.panStartY;
const view = this.getView();
const halfW = this.canvas.width / (2 * view.scale);
const halfH = this.canvas.height / (2 * view.scale);
this.setView({
centerRe: view.centerRe - (dx / this.canvas.width) * 2 * halfW,
centerIm: view.centerIm + (dy / this.canvas.height) * 2 * halfH,
});
this.panStartX = e.offsetX;
this.panStartY = e.offsetY;
});
window.addEventListener('mouseup', (e) => {
if (this.isPanning && (e.button === 1 || e.button === 2)) {
this.isPanning = false;
this.pushHistory();
this.onNavEvent?.({ type: 'pan' });
}
});
// Prevent context menu on canvas
this.canvas.addEventListener('contextmenu', (e) => e.preventDefault());
}
setupKeyboardEvents() {
window.addEventListener('keydown', (e) => {
const view = this.getView();
const halfW = this.canvas.width / (2 * view.scale);
const halfH = this.canvas.height / (2 * view.scale);
const panStep = config_1.CONFIG.panStepFraction;
switch (e.key) {
case 'ArrowLeft':
e.preventDefault();
this.setView({ centerRe: view.centerRe - 2 * halfW * panStep });
break;
case 'ArrowRight':
e.preventDefault();
this.setView({ centerRe: view.centerRe + 2 * halfW * panStep });
break;
case 'ArrowUp':
e.preventDefault();
this.setView({ centerIm: view.centerIm + 2 * halfH * panStep });
break;
case 'ArrowDown':
e.preventDefault();
this.setView({ centerIm: view.centerIm - 2 * halfH * panStep });
break;
case '+':
case '=':
e.preventDefault();
{
const newScale = Math.min(view.scale * config_1.CONFIG.zoomSpeed, config_1.CONFIG.maxMagnification * 150);
this.setView({ scale: newScale });
}
break;
case '-':
case '_':
e.preventDefault();
this.setView({ scale: view.scale / config_1.CONFIG.zoomSpeed });
break;
case 'z':
case 'Z':
e.preventDefault();
this.goBack();
return;
case 'r':
case 'R':
e.preventDefault();
this.resetView();
return;
case 'j':
case 'J':
e.preventDefault();
// Toggle Julia mode with current center as c-value
if (view.setMode === 'mandelbrot') {
this.setView({ setMode: 'julia', juliaCre: view.centerRe, juliaCim: view.centerIm });
}
else {
this.setView({ setMode: 'mandelbrot' });
}
break;
}
this.pushHistory();
this.onNavEvent?.({ type: e.key.includes('+') || e.key === '=' || e.key === '-' ? 'zoom' : 'pan' });
});
}
getDragRect() {
if (!this.isDragging)
return null;
const x1 = Math.min(this.dragStartX, this.dragCurrentX);
const y1 = Math.min(this.dragStartY, this.dragCurrentY);
const x2 = Math.max(this.dragStartX, this.dragCurrentX);
const y2 = Math.max(this.dragStartY, this.dragCurrentY);
return { x1, y1, x2, y2 };
}
setCallbacks(onNavEvent) {
this.onNavEvent = onNavEvent;
}
getHistory() {
return [...this.history];
}
}
exports.Navigation = Navigation;
};
// ── module: src/bookmarks.ts ──
__mods["src/bookmarks.ts"] = function (exports, require, module) {
"use strict";
// Bookmarks — save/load views to localStorage
Object.defineProperty(exports, "__esModule", { value: true });
exports.saveBookmark = saveBookmark;
exports.loadBookmark = loadBookmark;
exports.deleteBookmark = deleteBookmark;
exports.listBookmarks = listBookmarks;
exports.encodeStateToHash = encodeStateToHash;
exports.decodeStateFromHash = decodeStateFromHash;
const STORAGE_KEY = 'fractal-explorer-bookmarks';
function saveBookmark(name, view, paletteName) {
const bookmarks = loadAllBookmarks();
// Update existing or add new
const idx = bookmarks.findIndex(b => b.name === name);
const entry = {
name,
timestamp: Date.now(),
view: { ...view },
paletteName,
};
if (idx >= 0) {
bookmarks[idx] = entry;
}
else {
bookmarks.push(entry);
}
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(bookmarks));
}
catch (e) {
console.warn('Failed to save bookmark:', e);
}
}
function loadBookmark(name) {
const bookmarks = loadAllBookmarks();
return bookmarks.find(b => b.name === name) || null;
}
function deleteBookmark(name) {
const bookmarks = loadAllBookmarks().filter(b => b.name !== name);
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(bookmarks));
}
catch (e) {
console.warn('Failed to delete bookmark:', e);
}
}
function listBookmarks() {
return loadAllBookmarks();
}
function loadAllBookmarks() {
try {
const data = localStorage.getItem(STORAGE_KEY);
if (!data)
return [];
return JSON.parse(data);
}
catch (e) {
console.warn('Failed to load bookmarks:', e);
return [];
}
}
// ─── URL hash state ────────────────────────────────────────────────
function encodeStateToHash(view, paletteName) {
const parts = [
`cr=${view.centerRe.toPrecision(15)}`,
`ci=${view.centerIm.toPrecision(15)}`,
`sc=${view.scale.toPrecision(15)}`,
`md=${view.setMode}`,
`jcr=${view.juliaCre.toPrecision(15)}`,
`jci=${view.juliaCim.toPrecision(15)}`,
`pl=${paletteName}`,
];
return '#' + parts.join('&');
}
function decodeStateFromHash() {
const hash = window.location.hash.slice(1); // remove #
if (!hash)
return null;
const params = new URLSearchParams(hash);
return {
view: {
centerRe: parseFloat(params.get('cr') || ''),
centerIm: parseFloat(params.get('ci') || ''),
scale: parseFloat(params.get('sc') || ''),
setMode: params.get('md') || 'mandelbrot',
juliaCre: parseFloat(params.get('jcr') || ''),
juliaCim: parseFloat(params.get('jci') || ''),
},
paletteName: params.get('pl') || 'fire',
};
}
};
// ── module: src/ui.ts ──
__mods["src/ui.ts"] = function (exports, require, module) {
"use strict";
// UI — glassy HUD, palette selector, zoom overlay, Julia preview
Object.defineProperty(exports, "__esModule", { value: true });
exports.FractalUI = void 0;
const config_1 = require("./config");
const palette_1 = require("./palette");
const bookmarks_1 = require("./bookmarks");
class FractalUI {
constructor() {
this.juliaPreviewCanvas = null;
this.hideTimeout = null;
this.isHudVisible = true;
// State tracking for HUD updates
this.frameCount = 0;
this.lastFpsTime = performance.now();
this.currentFps = 60;
this.hudEl = document.getElementById('hud');
this.magEl = document.getElementById('mag-display');
this.centerEl = document.getElementById('center-display');
this.iterEl = document.getElementById('iter-display');
this.fpsEl = document.getElementById('fps-display');
this.paletteContainer = document.getElementById('palette-container');
this.zoomOverlay = document.getElementById('zoom-overlay');
this.bookmarkList = document.getElementById('bookmark-list');
// Setup palette swatches
this.setupPaletteSwatches();
// Auto-hide HUD on idle
this.resetHideTimer();
}
setupPaletteSwatches() {
for (const name of palette_1.PALETTE_NAMES) {
const chip = document.createElement('div');
chip.className = 'palette-chip';
chip.dataset.palette = name;
// Create gradient preview from palette stops
const stops = (0, palette_1.getPalette)(name);
const gradientParts = [];
for (const stop of stops) {
const pct = Math.round(stop.t * 100);
gradientParts.push(`rgb(${stop.r},${stop.g},${stop.b}) ${pct}%`);
}
chip.style.background = `linear-gradient(90deg, ${gradientParts.join(', ')})`;
chip.addEventListener('click', () => {
this.onPaletteChange?.(name);
// Mark active
document.querySelectorAll('.palette-chip').forEach(c => c.classList.remove('active'));
chip.classList.add('active');
});
this.paletteContainer.appendChild(chip);
}
// Activate first palette
const firstChip = this.paletteContainer.querySelector('.palette-chip');
if (firstChip)
firstChip.classList.add('active');
}
updateHud(view, maxIter) {
const mag = view.scale / 150;
// Magnification display
if (mag >= 1e6) {
this.magEl.textContent = `×${mag.toExponential(2).replace('e+', 'e')}`;
}
else if (mag >= 1000) {
this.magEl.textContent = `×${Math.round(mag).toLocaleString()}`;
}
else {
this.magEl.textContent = `×${mag.toFixed(1)}`;
}
// Center coordinates
this.centerEl.textContent = `${view.centerRe.toPrecision(8)}, ${view.centerIm.toPrecision(8)}i`;
// Iterations
this.iterEl.textContent = `iter: ${maxIter}`;
// FPS (computed every second)
this.frameCount++;
const now = performance.now();
if (now - this.lastFpsTime >= 1000) {
this.currentFps = Math.round(this.frameCount * 1000 / (now - this.lastFpsTime));
this.fpsEl.textContent = `${this.currentFps} fps`;
this.frameCount = 0;
this.lastFpsTime = now;
}
// Set mode indicator
const modeEl = document.getElementById('mode-display');
if (modeEl) {
modeEl.textContent = view.setMode === 'julia'
? `Julia (${view.juliaCre.toFixed(4)}, ${view.juliaCim.toFixed(4)}i)`
: 'Mandelbrot';
}
this.resetHideTimer();
}
resetHideTimer() {
if (this.hideTimeout)
clearTimeout(this.hideTimeout);
this.hudEl.classList.remove('hidden');
this.isHudVisible = true;
this.hideTimeout = setTimeout(() => {
this.hudEl.classList.add('hidden');
this.isHudVisible = false;
}, config_1.CONFIG.hudHideDelayMs);
}
showHud() {
if (this.hideTimeout)
clearTimeout(this.hideTimeout);
this.hudEl.classList.remove('hidden');
this.isHudVisible = true;
this.resetHideTimer();
}
drawZoomRect(x1, y1, x2, y2) {
const ctx = this.zoomOverlay.getContext('2d');
ctx.clearRect(0, 0, this.zoomOverlay.width, this.zoomOverlay.height);
if (x1 === x2 || y1 === y2)
return;
// Inverted-color border effect
ctx.strokeStyle = 'rgba(255, 255, 255, 0.9)';
ctx.lineWidth = 2;
ctx.setLineDash([6, 4]);
ctx.strokeRect(x1, y1, x2 - x1, y2 - y1);
ctx.setLineDash([]);
// Semi-transparent fill with crosshair
ctx.fillStyle = 'rgba(0, 0, 0, 0.15)';
ctx.fillRect(x1, y1, x2 - x1, y2 - y1);
const cx = (x1 + x2) / 2;
const cy = (y1 + y2) / 2;
const size = 8;
ctx.strokeStyle = 'rgba(255, 255, 255, 0.7)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(cx - size, cy);
ctx.lineTo(cx + size, cy);
ctx.moveTo(cx, cy - size);
ctx.lineTo(cx, cy + size);
ctx.stroke();
// Corner markers
const cornerSize = 6;
ctx.strokeStyle = 'rgba(255, 100, 100, 0.9)';
ctx.lineWidth = 2;
this.drawCorner(ctx, x1, y1, cornerSize);
this.drawCorner(ctx, x2, y1, -cornerSize);
this.drawCorner(ctx, x1, y2, cornerSize);
this.drawCorner(ctx, x2, y2, -cornerSize);
}
drawCorner(ctx, x, y, dir) {
ctx.beginPath();
ctx.moveTo(x + dir * 2, y);
ctx.lineTo(x, y);
ctx.lineTo(x, y + dir * 2);
ctx.stroke();
}
clearZoomRect() {
const ctx = this.zoomOverlay.getContext('2d');
ctx.clearRect(0, 0, this.zoomOverlay.width, this.zoomOverlay.height);
}
updateBookmarks() {
const bookmarks = (0, bookmarks_1.listBookmarks)();
this.bookmarkList.innerHTML = '';
for (const bm of bookmarks) {
const li = document.createElement('li');
li.className = 'bookmark-item';
const nameSpan = document.createElement('span');
nameSpan.className = 'bookmark-name';
nameSpan.textContent = bm.name;
nameSpan.addEventListener('click', () => this.onBookmarkLoad?.(bm));
const delBtn = document.createElement('button');
delBtn.className = 'bookmark-delete';
delBtn.textContent = '×';
delBtn.addEventListener('click', (e) => {
e.stopPropagation();
(0, bookmarks_1.deleteBookmark)(bm.name);
this.updateBookmarks();
});
li.appendChild(nameSpan);
li.appendChild(delBtn);
this.bookmarkList.appendChild(li);
}
}
setCallbacks(opts) {
Object.assign(this, opts);
}
}
exports.FractalUI = FractalUI;
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingcap-qwen3.6-27b","provider":"lmstudio","persona":"typescript","sessionId":"eebb5a0e-e298-41cb-8fbf-4fda25dc3687","tokensIn":2356497,"tokensOut":47037,"tokensTotal":2403534,"turns":60,"toolCalls":59,"failedToolCalls":1,"timestamp":"2026-07-31T18:59:14.406Z"} -->