Fractal Explorer
Qwen 3.7 Flash · 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 72,473 bytes · SHA-256 437bf02d8687
<!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</title>
<style>
/* ── css: layout ── */
/* === Layout & Canvas === */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #000; font-family: "SF Mono", "Fira Code", "Cascadia Code", monospace; }
#fractal-canvas { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; cursor: crosshair; image-rendering: pixelated; }
#overlay-canvas { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; pointer-events: none; z-index: 5; }
/* === Glassy HUD Panel === */
.glass-panel {
position: fixed; z-index: 10;
background: rgba(10, 12, 20, 0.72);
backdrop-filter: blur(16px) saturate(1.4);
-webkit-backdrop-filter: blur(16px) saturate(1.4);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 12px;
color: #e0e0e0;
padding: 12px 16px;
transition: opacity 0.3s ease;
}
.glass-panel.hidden { display: none; }
#hud-controls {
top: 12px; right: 12px;
min-width: 280px;
max-width: 340px;
}
.controls-section {
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.controls-section:last-child { margin-bottom: 0; }
.controls-section .label { font-size: 11px; color: #888; min-width: 60px; }
/* === Palette Chips === */
.chip-row { display: flex; gap: 6px; flex-wrap: wrap; }
.palette-chip {
width: 28px; height: 28px;
border-radius: 6px;
border: 2px solid transparent;
cursor: pointer;
transition: transform 0.15s, border-color 0.15s;
position: relative;
}
.palette-chip:hover { transform: scale(1.15); }
.palette-chip.active { border-color: #fff; box-shadow: 0 0 8px rgba(255,255,255,0.4); }
.palette-chip[data-name="Fire"] { background: linear-gradient(135deg, #b40, #ff8, #fff); }
.palette-chip[data-name="Ocean"] { background: linear-gradient(135deg, #018, #0cf, #aef); }
.palette-chip[data-name="Monochrome"] { background: linear-gradient(135deg, #000, #888, #fff); }
.palette-chip[data-name="Psychedelic"] { background: linear-gradient(135deg, #80f, #0ff, #0f0, #ff0, #f00); }
.palette-chip[data-name="Electric"] { background: linear-gradient(135deg, #006, #06c, #0fc, #6fa); }
.palette-chip[data-name="Sunset"] { background: linear-gradient(135deg, #108, #a08, #f50, #fc0); }
/* === Buttons === */
button {
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 6px;
color: #ddd;
padding: 5px 10px;
font-family: inherit;
font-size: 11px;
cursor: pointer;
transition: background 0.15s;
}
button:hover { background: rgba(255, 255, 255, 0.18); }
button:active { background: rgba(255, 255, 255, 0.25); }
/* === Sliders === */
input[type="range"] {
flex: 1;
min-width: 80px;
accent-color: #6af;
height: 4px;
}
#iter-value, #cyclic-value { font-size: 11px; color: #aaa; min-width: 36px; text-align: right; }
/* === Bookmark List === */
#bookmark-list {
top: 50%; left: 50%;
transform: translate(-50%, -50%);
min-width: 320px;
max-height: 70vh;
overflow-y: auto;
}
#bookmark-list h3 {
font-size: 13px; margin-bottom: 8px; color: #ccc;
border-bottom: 1px solid rgba(255,255,255,0.1);
padding-bottom: 6px;
}
#bookmark-items { list-style: none; }
#bookmark-items li {
display: flex; justify-content: space-between; align-items: center;
padding: 6px 0;
border-bottom: 1px solid rgba(255,255,255,0.06);
font-size: 12px;
}
#bookmark-items li button { padding: 2px 8px; font-size: 10px; }
.bookmark-name { cursor: pointer; color: #6af; flex: 1; }
.bookmark-name:hover { text-decoration: underline; }
.bookmark-time { color: #666; font-size: 10px; margin: 0 8px; }
/* === Tooltip / info bar === */
#info-bar {
position: fixed; bottom: 12px; left: 50%; transform: translateX(-50%);
z-index: 10;
background: rgba(10, 12, 20, 0.72);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 8px;
padding: 6px 16px;
font-size: 11px;
color: #999;
pointer-events: none;
opacity: 0;
transition: opacity 0.3s;
}
#info-bar.visible { opacity: 1; }
</style>
</head>
<body>
<canvas id="fractal-canvas"></canvas>
<canvas id="overlay-canvas"></canvas>
<div id="hud-controls" class="glass-panel">
<div id="palette-selector" class="controls-section">
<span class="label">Palette:</span>
<div id="palette-chips" class="chip-row"></div>
</div>
<div class="controls-section">
<button id="btn-back" title="Go back (Z)">← Back</button>
<button id="btn-reset" title="Reset view (R)">Reset</button>
<button id="btn-bookmark" title="Save/Load bookmark (B)">Bookmark</button>
<button id="btn-julia" title="Toggle Julia mode (J)">Julia</button>
</div>
<div class="controls-section">
<label for="iter-slider">Iterations: <span id="iter-value">256</span></label>
<input type="range" id="iter-slider" min="64" max="10000" step="32" value="256">
</div>
<div class="controls-section">
<label for="cyclic-slider">Color drift: <span id="cyclic-value">0</span></label>
<input type="range" id="cyclic-slider" min="0" max="80" step="1" value="0">
</div>
</div>
<div id="bookmark-list" class="glass-panel hidden">
<h3>Bookmarks</h3>
<ul id="bookmark-items"></ul>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./math":"src/math.ts","./palette":"src/palette.ts","./state":"src/state.ts","./render":"src/render.ts","./input":"src/input.ts","./ui":"src/ui.ts","./bookmarks":"src/bookmarks.ts"},"src/state.ts":{"./math":"src/math.ts"},"src/bookmarks.ts":{"./state":"src/state.ts"},"src/render.ts":{"./math":"src/math.ts","./palette":"src/palette.ts"},"src/input.ts":{"./state":"src/state.ts","./render":"src/render.ts"},"src/ui.ts":{"./state":"src/state.ts","./palette":"src/palette.ts"}};
function __require(id) {
if (__cache[id]) return __cache[id].exports;
var module = __cache[id] = { exports: {} };
var factory = __mods[id];
if (!factory) throw new Error("Module not found: " + id);
factory(module.exports, function (spec) {
var target = (__map[id] && __map[id][spec]) || spec;
return __require(target);
}, module);
return module.exports;
}
// ── module: src/main.ts ──
__mods["src/main.ts"] = function (exports, require, module) {
"use strict";
// ============================================================
// MAIN — Application entry point, wiring all systems together
// ============================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.start = start;
const math_1 = require("./math");
const palette_1 = require("./palette");
const state_1 = require("./state");
const render_1 = require("./render");
const input_1 = require("./input");
const ui_1 = require("./ui");
const bookmarks_1 = require("./bookmarks");
// ============================================================
// GLOBALS
// ============================================================
let canvas;
let renderer;
let input;
let ui;
let appState;
let cancelToken = { version: 0 };
let selectionRect = null;
let animFrameId = 0;
let paletteChipsRendered = false;
// ============================================================
// INITIALIZATION
// ============================================================
function start() {
canvas = document.getElementById('fractal-canvas');
if (!canvas) {
console.error('Missing #fractal-canvas');
return;
}
// Initialize state with link params if available
const linkParams = (0, bookmarks_1.loadLinkParams)();
if (linkParams) {
appState = (0, state_1.initApp)(1, 1);
(0, state_1.setView)({
centerRe: linkParams.re ?? state_1.DEFAULT_VIEW.centerRe,
centerIm: linkParams.im ?? state_1.DEFAULT_VIEW.centerIm,
scale: linkParams.scale ?? state_1.DEFAULT_VIEW.scale,
});
appState.maxIter = linkParams.iter ?? (0, math_1.getMaxIter)();
if (linkParams.palette !== undefined)
(0, palette_1.setCurrentPalette)(linkParams.palette);
if (linkParams.offset !== undefined)
(0, palette_1.setCyclicOffset)(linkParams.offset);
if (linkParams.julia) {
appState.isJuliaMode = true;
appState.juliaCr = linkParams.jcr ?? -0.7269;
appState.juliaCi = linkParams.jci ?? 0.1889;
(0, math_1.setJuliaC)(appState.juliaCr, appState.juliaCi);
}
}
else {
appState = (0, state_1.initApp)(1, 1);
}
renderer = new render_1.FractalRenderer(canvas);
ui = new ui_1.UIFrame(document.getElementById('overlay-canvas'));
setupCanvasSize();
rebuildLUT();
setupInput();
setupUIControls();
renderFractal(true);
// Start render loop for UI overlay
function loop(ts) {
ui.frameTick();
ui.draw();
animFrameId = requestAnimationFrame(loop);
}
animFrameId = requestAnimationFrame(loop);
exposeDebugInterface();
console.log('Fractal Explorer initialized');
}
/** Setup canvas sizing for high-DPI displays */
function setupCanvasSize() {
const dpr = window.devicePixelRatio || 1;
const vw = document.documentElement.clientWidth || window.innerWidth;
const vh = document.documentElement.clientHeight || window.innerHeight;
const w = Math.max(600, Math.floor(vw * dpr));
const h = Math.max(400, Math.floor(vh * dpr));
canvas.width = w;
canvas.height = h;
canvas.style.width = vw + 'px';
canvas.style.height = vh + 'px';
appState.pixelRatio = dpr;
renderer.resize(w, h);
ui.resize(w, h);
}
/** Rebuild color LUT when palette or offset changes */
function rebuildLUT() {
renderer.rebuildLUT((0, math_1.getMaxIter)());
}
/** Compute fractal-space bounds for current view */
function getViewBounds() {
const v = appState.view;
const cw = canvas.width / appState.pixelRatio;
const ch = canvas.height / appState.pixelRatio;
const aspect = ch / cw;
// scale controls the horizontal span: totalWidth = 4.0 / scale
// This means scale=1 shows ~[-2.5..2.5] horizontally
const halfW = 2.0 / v.scale;
const halfH = halfW * aspect;
return [v.centerRe - halfW, v.centerRe + halfW, v.centerIm - halfH, v.centerIm + halfH];
}
/** Main fractal render call */
function renderFractal(fullRes) {
const bounds = getViewBounds();
const maxIter = (0, math_1.getMaxIter)();
const opts = {
isJuliaMode: appState.isJuliaMode,
reMin: bounds[0], reMax: bounds[1],
imMin: bounds[2], imMax: bounds[3],
width: canvas.width,
height: canvas.height,
maxIter: maxIter,
};
if (fullRes) {
renderer.render(opts);
}
else {
renderer.renderProgressive(opts, () => {
ui.interact();
});
}
}
/** Full redraw triggered by major state changes */
function fullRedraw() {
rebuildLUT();
renderFractal(true);
}
// ============================================================
// INPUT SETUP
// ============================================================
function setupInput() {
input = new input_1.InputHandler({
canvas: canvas,
renderer: renderer,
onStateChange: () => {
ui.interact();
scheduleRender(false);
},
});
input.onSelectRectBegin = (x, y) => {
selectionRect = { x1: x, y1: y, x2: x, y2: y };
};
input.onSelectRectMove = (x, y) => {
if (!selectionRect)
return;
selectionRect.x2 = x;
selectionRect.y2 = y;
drawSelectionOverlay();
};
input.onSelectRectEnd = (x1, y1, x2, y2) => {
if (!selectionRect)
return;
selectionRect = null;
clearOverlay();
const sx1 = Math.min(x1, x2);
const sy1 = Math.min(y1, y2);
const sw = Math.abs(x2 - x1);
const sh = Math.abs(y2 - y1);
if (sw < 5 || sh < 5)
return;
const [reMin, reMax, imMin, imMax] = getViewBounds();
const cw = canvas.width / appState.pixelRatio;
const ch = canvas.height / appState.pixelRatio;
const targetScale = Math.min(cw / sw, ch / sh);
const newCenterRe = reMin + ((sx1 + sw / 2) / cw) * (reMax - reMin);
const newCenterIm = imMin + ((sy1 + sh / 2) / ch) * (imMax - imMin);
pushHistory();
(0, state_1.setView)({ centerRe: newCenterRe, centerIm: newCenterIm, scale: targetScale });
rebuildLUT();
scheduleRender(true);
};
input.onJuliaPoint = (re, im) => {
toggleJuliaMode(re, im);
};
}
/** Push current view to history */
function pushHistory() {
const st = appState;
st.zoomHistory = st.zoomHistory.slice(0, st.historyIndex + 1);
st.zoomHistory.push({ ...st.view });
st.historyIndex = st.zoomHistory.length - 1;
}
/** Draw selection box overlay */
function drawSelectionOverlay() {
if (!selectionRect)
return;
const ctx = renderer['_ctx'];
const x = Math.min(selectionRect.x1, selectionRect.x2);
const y = Math.min(selectionRect.y1, selectionRect.y2);
const w = Math.abs(selectionRect.x2 - selectionRect.x1);
const h = Math.abs(selectionRect.y2 - selectionRect.y1);
ctx.save();
ctx.fillStyle = 'rgba(255, 255, 255, 0.04)';
ctx.fillRect(x, y, w, h);
ctx.strokeStyle = 'rgba(255, 255, 255, 0.85)';
ctx.lineWidth = 2;
ctx.setLineDash([8, 4]);
ctx.strokeRect(x, y, w, h);
ctx.restore();
}
/** Clear overlay and redraw fractal */
function clearOverlay() {
renderFractal(true);
}
/** Render fractal (synchronous for full-res, progressive otherwise) */
function scheduleRender(fullRes) {
// For full-res, render synchronously so state changes are immediately visible
if (fullRes) {
renderFractal(true);
}
else {
// Progressive: defer to avoid blocking UI
requestIdleCallback ? requestIdleCallback(() => renderFractal(false)) : setTimeout(() => renderFractal(false), 16);
}
}
// ============================================================
// JULIA MODE
// ============================================================
function toggleJuliaMode(centerRe, centerIm) {
const st = appState;
if (!st.isJuliaMode) {
st.isJuliaMode = true;
st.juliaCr = centerRe;
st.juliaCi = centerIm;
(0, math_1.setJuliaC)(centerRe, centerIm);
}
else {
st.isJuliaMode = false;
(0, math_1.setJuliaC)(0, 0);
}
pushHistory();
rebuildLUT();
scheduleRender(true);
}
// ============================================================
// UI CONTROLS
// ============================================================
function setupUIControls() {
// Palette chips
const chipRow = document.getElementById('palette-chips');
if (!chipRow)
return;
const palettes = ['Fire', 'Ocean', 'Monochrome', 'Psychedelic', 'Electric', 'Sunset'];
palettes.forEach((name, i) => {
const chip = document.createElement('div');
chip.className = 'palette-chip' + (i === (0, palette_1.getCurrentPaletteIndex)() ? ' active' : '');
chip.dataset.name = name;
chip.title = name;
chip.addEventListener('click', () => {
(0, palette_1.setCurrentPalette)(i);
rebuildLUT();
scheduleRender(false);
updatePaletteChips();
});
chipRow.appendChild(chip);
});
paletteChipsRendered = true;
// Back button
document.getElementById('btn-back')?.addEventListener('click', () => {
if ((0, state_1.goBack)()) {
rebuildLUT();
scheduleRender(true);
}
});
// Reset button
document.getElementById('btn-reset')?.addEventListener('click', () => {
pushHistory();
(0, state_1.setView)({ ...state_1.DEFAULT_VIEW });
rebuildLUT();
scheduleRender(true);
});
// Bookmark button
document.getElementById('btn-bookmark')?.addEventListener('click', () => {
showBookmarkMenu();
});
// Julia button
document.getElementById('btn-julia')?.addEventListener('click', () => {
toggleJuliaMode(appState.view.centerRe, appState.view.centerIm);
});
// Iteration slider
const iterSlider = document.getElementById('iter-slider');
const iterValue = document.getElementById('iter-value');
if (iterSlider && iterValue) {
iterSlider.value = String((0, math_1.getMaxIter)());
iterSlider.addEventListener('input', () => {
const v = parseInt(iterSlider.value);
(0, math_1.setMaxIter)(v);
iterValue.textContent = String(v);
rebuildLUT();
scheduleRender(true);
});
}
// Cyclic drift slider
const cyclicSlider = document.getElementById('cyclic-slider');
const cyclicValue = document.getElementById('cyclic-value');
if (cyclicSlider && cyclicValue) {
cyclicSlider.value = String(Math.round((0, palette_1.getCyclicOffset)() * 80));
cyclicSlider.addEventListener('input', () => {
const v = parseInt(cyclicSlider.value) / 80;
(0, palette_1.setCyclicOffset)(v);
cyclicValue.textContent = cyclicSlider.value;
rebuildLUT();
scheduleRender(false);
});
}
// Global keyboard shortcuts
window.addEventListener('keydown', (e) => {
const key = e.key.toLowerCase();
switch (key) {
case 'r':
resetView();
break;
case 'p':
cyclePalette();
break;
case '[':
prevPalette();
break;
case ']':
nextPalette();
break;
case 'backspace':
e.preventDefault();
if (!(0, state_1.goBack)()) { /* nothing */ }
else {
rebuildLUT();
scheduleRender(true);
}
break;
}
});
// Resize handler
window.addEventListener('resize', () => {
setupCanvasSize();
rebuildLUT();
scheduleRender(true);
});
}
function updatePaletteChips() {
if (!paletteChipsRendered)
return;
const chips = document.querySelectorAll('.palette-chip');
chips.forEach((chip, i) => {
chip.classList.toggle('active', i === (0, palette_1.getCurrentPaletteIndex)());
});
}
function resetView() {
pushHistory();
(0, state_1.setView)({ ...state_1.DEFAULT_VIEW });
rebuildLUT();
scheduleRender(true);
}
function cyclePalette() {
const idx = ((0, palette_1.getCurrentPaletteIndex)() + 1) % 6;
(0, palette_1.setCurrentPalette)(idx);
rebuildLUT();
scheduleRender(false);
updatePaletteChips();
}
function prevPalette() {
const idx = ((0, palette_1.getCurrentPaletteIndex)() - 1 + 6) % 6;
(0, palette_1.setCurrentPalette)(idx);
rebuildLUT();
scheduleRender(false);
updatePaletteChips();
}
function nextPalette() {
const idx = ((0, palette_1.getCurrentPaletteIndex)() + 1) % 6;
(0, palette_1.setCurrentPalette)(idx);
rebuildLUT();
scheduleRender(false);
updatePaletteChips();
}
function showBookmarkMenu() {
const bookmarks = (0, bookmarks_1.loadBookmarks)();
let listEl = document.getElementById('bookmark-list');
if (!listEl) {
listEl = document.createElement('div');
listEl.id = 'bookmark-list';
listEl.className = 'glass-panel hidden';
document.body.appendChild(listEl);
}
listEl.classList.remove('hidden');
listEl.innerHTML = '<h3>Bookmarks</h3><ul id="bookmark-items"></ul>';
const items = document.getElementById('bookmark-items');
if (!items)
return;
if (bookmarks.length === 0) {
items.innerHTML = '<li style="color:#666">No bookmarks yet</li>';
}
else {
bookmarks.forEach((b, i) => {
const li = document.createElement('li');
const nameSpan = document.createElement('span');
nameSpan.className = 'bookmark-name';
nameSpan.textContent = b.name;
nameSpan.title = `Restore "${b.name}"`;
nameSpan.addEventListener('click', () => {
const restored = (0, bookmarks_1.restoreBookmark)(b);
Object.assign(appState, restored);
(0, math_1.setJuliaC)(appState.juliaCr, appState.juliaCi);
(0, palette_1.setCurrentPalette)(appState.paletteIndex);
(0, palette_1.setCyclicOffset)(appState.cyclicOffset);
rebuildLUT();
scheduleRender(true);
listEl.classList.add('hidden');
});
const timeSpan = document.createElement('span');
timeSpan.className = 'bookmark-time';
timeSpan.textContent = new Date(b.timestamp).toLocaleDateString();
const delBtn = document.createElement('button');
delBtn.textContent = '✕';
delBtn.addEventListener('click', (e) => {
e.stopPropagation();
localStorage.removeItem('fractal_bookmarks');
const updated = bookmarks.filter((_, j) => j !== i);
try {
localStorage.setItem('fractal_bookmarks', JSON.stringify(updated));
}
catch { }
listEl.classList.add('hidden');
setTimeout(() => showBookmarkMenu(), 100);
});
li.appendChild(nameSpan);
li.appendChild(timeSpan);
li.appendChild(delBtn);
items.appendChild(li);
});
}
// Add "Save" option
const saveLi = document.createElement('li');
const saveBtn = document.createElement('button');
saveBtn.textContent = '+ Save Current View';
saveBtn.style.flex = '1';
saveBtn.addEventListener('click', () => {
const name = prompt('Bookmark name:', `View ${new Date().toLocaleTimeString()}`);
if (name) {
(0, bookmarks_1.saveBookmark)(appState, name);
listEl.classList.add('hidden');
showInfo(`Saved: "${name}"`);
}
});
saveLi.appendChild(saveBtn);
items.appendChild(saveLi);
// Close on click outside
setTimeout(() => {
const closer = (e) => {
if (!listEl.contains(e.target)) {
listEl.classList.add('hidden');
document.removeEventListener('click', closer);
}
};
document.addEventListener('click', closer);
}, 10);
}
function showInfo(msg) {
let bar = document.getElementById('info-bar');
if (!bar) {
bar = document.createElement('div');
bar.id = 'info-bar';
document.body.appendChild(bar);
}
bar.textContent = msg;
bar.classList.add('visible');
setTimeout(() => bar.classList.remove('visible'), 2500);
}
// ============================================================
// DEBUG INTERFACE
// ============================================================
function exposeDebugInterface() {
window.fractal = {
iterationsAt(re, im) {
let zr = 0, zi = 0;
let zr2 = 0, zi2 = 0;
let i = 0;
const mi = (0, math_1.getMaxIter)();
while (i < mi && zr2 + zi2 <= 256) {
zi = 2 * zr * zi + im;
zr = zr2 - zi2 + re;
zr2 = zr * zr;
zi2 = zi * zi;
i++;
}
return i;
},
setView(params) {
const v = appState.view;
(0, state_1.setView)({
centerRe: params.re ?? v.centerRe,
centerIm: params.im ?? v.centerIm,
scale: params.scale ?? v.scale,
});
rebuildLUT();
scheduleRender(true);
},
getState() {
return {
view: { ...appState.view },
maxIter: appState.maxIter,
isJulia: appState.isJuliaMode,
magnification: (0, state_1.getMagnification)(),
palette: (0, palette_1.getCurrentPalette)().name,
};
},
goto(name) {
const locations = {
'seahorse': { re: -0.7436438870371587, im: 0.13182590420531197 },
'spiral': { re: -0.16, im: 1.0405 },
'elephant': { re: 0.28, im: 0.008 },
'lightning': { re: 0.275, im: 0.0 },
'scepter': { re: -1.25066, im: 0.02012 },
'double-spiral': { re: 0.3, im: 0.5 },
'main-cardioid': { re: -0.12, im: 0.74 },
};
const loc = locations[name];
if (loc) {
(0, state_1.setView)({ centerRe: loc.re, centerIm: loc.im, scale: appState.view.scale });
scheduleRender(true);
}
},
test() {
const results = [];
const mi = (0, math_1.getMaxIter)();
// c = 0 should be interior (hits max iter)
results.push(`mandelbrot(0, 0) = ${this.iterationsAt(0, 0)} (expect ${mi})`);
// c = -1 should be interior
results.push(`mandelbrot(-1, 0) = ${this.iterationsAt(-1, 0)} (expect ${mi})`);
// c = 2 should escape quickly
results.push(`mandelbrot(2, 0) = ${this.iterationsAt(2, 0)} (expect ≤3)`);
// c = 0.25 (cusp boundary) should have high but finite
results.push(`mandelbrot(0.25, 0) = ${this.iterationsAt(0.25, 0)} (expect high but <${mi})`);
console.log('Fractal correctness tests:');
results.forEach(r => console.log(' ' + r));
},
};
console.log('Debug interface exposed: window.fractal');
}
// ============================================================
// BOOT — Call start() when module loads
// ============================================================
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', start);
}
else {
start();
}
};
// ── module: src/math.ts ──
__mods["src/math.ts"] = function (exports, require, module) {
"use strict";
// ============================================================
// FRACTAL MATH — Mandelbrot & Julia set iteration
// Allocation-free core loops; smooth (continuous) coloring
// ============================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.TUNABLE = void 0;
exports.computeMaxIter = computeMaxIter;
exports.getMaxIter = getMaxIter;
exports.setMaxIter = setMaxIter;
exports.setJuliaC = setJuliaC;
exports.isJuliaMode = isJuliaMode;
exports.mandelbrotAt = mandelbrotAt;
exports.mandelbrot = mandelbrot;
exports.juliaAt = juliaAt;
exports.smoothIter = smoothIter;
exports.batchIterateMandelbrot = batchIterateMandelbrot;
exports.batchIterateWithSmooth = batchIterateWithSmooth;
exports.batchIterateJuliaWithSmooth = batchIterateJuliaWithSmooth;
/** Tunable constants */
exports.TUNABLE = Object.freeze({
/** Base iteration limit at default zoom (magnification ~1–2) */
baseIterations: 256,
/** Iterations scale linearly with log2(magnification) beyond base */
iterScalePerLogMag: 48,
/** Maximum iteration cap to prevent hanging */
maxIterationCap: 10000,
/** Bail-out radius squared */
bailoutRadius: 256,
/** Smooth coloring constant for log2 trick */
smoothingEnabled: true,
});
let _maxIter = exports.TUNABLE.baseIterations;
/** Compute the adaptive max iterations from magnification */
function computeMaxIter(magnification) {
if (magnification <= 1)
return exports.TUNABLE.baseIterations;
const logMag = Math.log2(magnification);
const val = exports.TUNABLE.baseIterations + exports.TUNABLE.iterScalePerLogMag * logMag;
return Math.min(Math.ceil(val), exports.TUNABLE.maxIterationCap);
}
function getMaxIter() { return _maxIter; }
function setMaxIter(v) { _maxIter = v; }
/** Escape-time computation for a single point using double precision.
* Returns iteration count and whether it's interior.
* The caller passes z0 = re, im (Julia c is pre-set via juliaC). */
const JULIA_C = { re: 0, im: 0 };
function setJuliaC(re, im) {
JULIA_C.re = re;
JULIA_C.im = im;
}
function isJuliaMode() {
return JULIA_C.re !== 0 || JULIA_C.im !== 0;
}
/**
* Core Mandelbrot/Julia iteration — allocation free.
* Uses the standard z = z^2 + c recurrence.
* For Mandelbrot: c = point, z0 = 0
* For Julia: c = fixed, z0 = point
*/
function mandelbrotAt(re, im, cr, ci, zr, zi, maxIter) {
let zr2 = zr * zr;
let zi2 = zi * zi;
let i = 0;
while (i < maxIter && zr2 + zi2 <= exports.TUNABLE.bailoutRadius) {
zi = 2.0 * zr * zi + ci;
zr = zr2 - zi2 + cr;
zr2 = zr * zr;
zi2 = zi * zi;
i++;
}
if (i >= maxIter) {
// Interior check: see if |z| is still bounded after one more step
const nextZr2 = (zr * zr - zi * zi + cr);
const nextZi = 2.0 * zr * zi + ci;
const nextR = zr * zr - zi * zi + cr;
const nextI = 2.0 * zr * zi + ci;
// If it's clearly diverging, it's not truly interior
// But by convention we call it interior if we hit maxIter
return { iter: maxIter, isInterior: true };
}
return { iter: i, isInterior: false };
}
/**
* Full Mandelbrot iteration for a point on the complex plane.
* cr, ci are the point's coordinates. z0 = 0.
*/
function mandelbrot(cr, ci, maxIter) {
return mandelbrotAt(cr, ci, cr, ci, 0.0, 0.0, maxIter);
}
/**
* Julia set iteration for a point (re, im) given fixed c = (juliaCr, juliaCi).
* z0 = point.
*/
function juliaAt(re, im, maxIter) {
return mandelbrotAt(re, im, JULIA_C.re, JULIA_C.im, re, im, maxIter);
}
/**
* Compute smooth (continuous) iteration count.
* n = raw iteration count
* zr, zi = final z values before bail-out
* Returns fractional iteration or maxIter if interior.
*/
function smoothIter(n, zr, zi, maxIter) {
if (n >= maxIter)
return maxIter;
const log_zn = Math.log(zr * zr + zi * zi) / 2.0; // ln|z|
const nu = Math.log(log_zn / Math.LN2) / Math.LN2; // log2(log|z|)
return n + 1.0 - nu;
}
/**
* Batch iterate a rectangular region of the fractal.
* Populates `iterBuffer` with raw integer iteration counts.
* Also fills `smoothBuffer` if provided (for smooth coloring).
* Fills `isInterior` array for mask rendering.
*
* Parameters are in fractal-space coordinates.
* stride = width of the buffer.
*/
function batchIterateMandelbrot(reMin, reMax, imMin, imMax, w, h, iterBuffer, smoothBuffer, isInterior, maxIter) {
const dRe = reMax - reMin;
const dIm = imMax - imMin;
for (let py = 0; py < h; py++) {
const ci = imMin + (py / h) * dIm;
const rowOff = py * w;
for (let px = 0; px < w; px++) {
const cr = reMin + (px / w) * dRe;
const off = rowOff + px;
const result = mandelbrot(cr, ci, maxIter);
iterBuffer[off] = result.iter;
if (isInterior) {
isInterior[off] = result.isInterior ? 1 : 0;
}
if (smoothBuffer && !result.isInterior) {
// We need zr,zi for smooth coloring — recompute briefly
// Actually, let's refactor: the batch loop should track zr,zi
// For now, store 0 (will be filled by a smarter version below)
smoothBuffer[off] = result.iter;
}
}
}
}
/**
* Optimized batch iterate that tracks zr,zi for smooth coloring.
* This is the main rendering loop — allocation free.
*/
function batchIterateWithSmooth(reMin, reMax, imMin, imMax, w, h, iterBuffer, smoothBuffer, isInterior, maxIter) {
const dRe = reMax - reMin;
const dIm = imMax - imMin;
for (let py = 0; py < h; py++) {
const ci = imMin + (py / h) * dIm;
const rowOff = py * w;
for (let px = 0; px < w; px++) {
const cr = reMin + (px / w) * dRe;
const off = rowOff + px;
// Mandelbrot: z0 = 0, c = (cr, ci)
let zr = 0.0, zi = 0.0;
let zr2 = 0.0, zi2 = 0.0;
let i = 0;
while (i < maxIter && zr2 + zi2 <= exports.TUNABLE.bailoutRadius) {
zi = 2.0 * zr * zi + ci;
zr = zr2 - zi2 + cr;
zr2 = zr * zr;
zi2 = zi * zi;
i++;
}
iterBuffer[off] = i;
if (isInterior) {
isInterior[off] = i >= maxIter ? 1 : 0;
}
if (smoothBuffer && i < maxIter) {
const log_zn = Math.log(zr2 + zi2) / 2.0;
const nu = Math.log(log_zn / Math.LN2) / Math.LN2;
smoothBuffer[off] = i + 1.0 - nu;
}
}
}
}
/**
* Julia set batch iterate with smooth coloring.
* Fixed c = JULIA_C, z0 = pixel position.
*/
function batchIterateJuliaWithSmooth(reMin, reMax, imMin, imMax, w, h, iterBuffer, smoothBuffer, isInterior, maxIter) {
const dRe = reMax - reMin;
const dIm = imMax - imMin;
const cj = JULIA_C.re;
const cjj = JULIA_C.im;
for (let py = 0; py < h; py++) {
const ci = imMin + (py / h) * dIm;
const rowOff = py * w;
for (let px = 0; px < w; px++) {
const cr = reMin + (px / w) * dRe;
const off = rowOff + px;
// Julia: z0 = (cr, ci), c = (cj, cjj)
let zr = cr, zi = ci;
let zr2 = zr * zr, zi2 = zi * zi;
let i = 0;
while (i < maxIter && zr2 + zi2 <= exports.TUNABLE.bailoutRadius) {
zi = 2.0 * zr * zi + cjj;
zr = zr2 - zi2 + cj;
zr2 = zr * zr;
zi2 = zi * zi;
i++;
}
iterBuffer[off] = i;
if (isInterior) {
isInterior[off] = i >= maxIter ? 1 : 0;
}
if (smoothBuffer && i < maxIter) {
const log_zn = Math.log(zr2 + zi2) / 2.0;
const nu = Math.log(log_zn / Math.LN2) / Math.LN2;
smoothBuffer[off] = i + 1.0 - nu;
}
}
}
}
};
// ── module: src/palette.ts ──
__mods["src/palette.ts"] = function (exports, require, module) {
"use strict";
// ============================================================
// PALETTES — Color definitions and smooth interpolation
// ============================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.getPalettes = getPalettes;
exports.getCurrentPalette = getCurrentPalette;
exports.getCurrentPaletteIndex = getCurrentPaletteIndex;
exports.setCurrentPalette = setCurrentPalette;
exports.getCyclicOffset = getCyclicOffset;
exports.setCyclicOffset = setCyclicOffset;
exports.samplePalette = samplePalette;
exports.buildColorLUT = buildColorLUT;
/** Predefined palettes */
const PALETTES = [
{
name: 'Fire',
stops: [
{ pos: 0.0, r: 0, g: 0, b: 4 },
{ pos: 0.15, r: 80, g: 0, b: 30 },
{ pos: 0.3, r: 180, g: 40, b: 0 },
{ pos: 0.5, r: 255, g: 120, b: 0 },
{ pos: 0.7, r: 255, g: 220, b: 60 },
{ pos: 0.85, r: 255, g: 255, b: 180 },
{ pos: 1.0, r: 255, g: 255, b: 255 },
],
},
{
name: 'Ocean',
stops: [
{ pos: 0.0, r: 0, g: 5, b: 20 },
{ pos: 0.2, r: 0, g: 30, b: 80 },
{ pos: 0.4, r: 0, g: 80, b: 140 },
{ pos: 0.6, r: 20, g: 140, b: 180 },
{ pos: 0.8, r: 80, g: 200, b: 220 },
{ pos: 1.0, r: 180, g: 240, b: 255 },
],
},
{
name: 'Monochrome',
stops: [
{ pos: 0.0, r: 0, g: 0, b: 0 },
{ pos: 0.5, r: 80, g: 80, b: 80 },
{ pos: 1.0, r: 255, g: 255, b: 255 },
],
},
{
name: 'Psychedelic',
stops: [
{ pos: 0.0, r: 128, g: 0, b: 255 },
{ pos: 0.167, r: 0, g: 0, b: 255 },
{ pos: 0.333, r: 0, g: 255, b: 255 },
{ pos: 0.5, r: 0, g: 255, b: 0 },
{ pos: 0.667, r: 255, g: 255, b: 0 },
{ pos: 0.833, r: 255, g: 0, b: 0 },
{ pos: 1.0, r: 128, g: 0, b: 255 },
],
},
{
name: 'Electric',
stops: [
{ pos: 0.0, r: 10, g: 0, b: 30 },
{ pos: 0.2, r: 60, g: 0, b: 120 },
{ pos: 0.4, r: 0, g: 100, b: 200 },
{ pos: 0.6, r: 0, g: 200, b: 180 },
{ pos: 0.8, r: 100, g: 255, b: 100 },
{ pos: 1.0, r: 200, g: 255, b: 200 },
],
},
{
name: 'Sunset',
stops: [
{ pos: 0.0, r: 20, g: 0, b: 40 },
{ pos: 0.2, r: 80, g: 10, b: 80 },
{ pos: 0.4, r: 160, g: 30, b: 60 },
{ pos: 0.6, r: 240, g: 80, b: 20 },
{ pos: 0.8, r: 255, g: 180, b: 60 },
{ pos: 1.0, r: 255, g: 255, b: 200 },
],
},
];
let _currentPalette = 0;
let _cyclicOffset = 0;
function getPalettes() { return PALETTES; }
function getCurrentPalette() { return PALETTES[_currentPalette]; }
function getCurrentPaletteIndex() { return _currentPalette; }
function setCurrentPalette(idx) { _currentPalette = idx; }
function getCyclicOffset() { return _cyclicOffset; }
function setCyclicOffset(v) { _cyclicOffset = v % 1.0; }
/**
* Interpolate within a palette to get an RGB color for a fractional value t (0..1).
* The palette wraps: t > 1 cycles through.
*/
function samplePalette(palette, t) {
const n = palette.stops.length;
if (n < 2)
return [0, 0, 0];
// Wrap t into [0, 1)
t = ((t % 1.0) + 1.0) % 1.0;
// Find the two stops to interpolate between
let i = 0;
while (i < n - 1 && palette.stops[i + 1].pos < t)
i++;
if (i >= n - 1) {
const s = palette.stops[n - 1];
return [s.r, s.g, s.b];
}
const s0 = palette.stops[i];
const s1 = palette.stops[i + 1];
const range = s1.pos - s0.pos;
const frac = range > 0 ? (t - s0.pos) / range : 0;
// Smooth ease-in-out for extra polish
const eased = frac * frac * (3 - 2 * frac);
return [
Math.round(s0.r + (s1.r - s0.r) * eased),
Math.round(s0.g + (s1.g - s0.g) * eased),
Math.round(s0.b + (s1.b - s0.b) * eased),
];
}
/**
* Build a full-color lookup table (LUT) for a given palette and offset.
* LUT maps iteration count → packed RGBA uint32.
* size = maxIter + some headroom.
*/
function buildColorLUT(palette, cyclicOffset, size) {
const lut = new Uint32Array(size);
const scale = size / 80; // map iterations 0..maxIter into ~80 palette cycles
for (let i = 0; i < size; i++) {
const t = ((i * scale + cyclicOffset) % 1.0 + 1.0) % 1.0;
const [r, g, b] = samplePalette(palette, t);
// Pack as ABGR for little-endian (standard on x86/ARM)
lut[i] = (255 << 24) | (b << 16) | (g << 8) | r;
}
return lut;
}
};
// ── module: src/state.ts ──
__mods["src/state.ts"] = function (exports, require, module) {
"use strict";
// ============================================================
// STATE — Application configuration and view parameters
// ============================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.DEFAULT_VIEW = void 0;
exports.createInitialState = createInitialState;
exports.getState = getState;
exports.setState = setState;
exports.initApp = initApp;
exports.setView = setView;
exports.zoomAt = zoomAt;
exports.panBy = panBy;
exports.goBack = goBack;
exports.getMagnification = getMagnification;
const math_1 = require("./math");
exports.DEFAULT_VIEW = {
centerRe: -0.5,
centerIm: 0.0,
scale: 1.0, // default: shows roughly [-1.5..1.5] × [-1.0..1.0] on most screens
};
const MAX_MAGNIFICATION = 1e14; // Hard cap for double precision
let state;
function createInitialState(canvasW, canvasH) {
return {
view: { ...exports.DEFAULT_VIEW },
targetView: null,
maxIter: (0, math_1.computeMaxIter)(1),
paletteIndex: 0,
cyclicOffset: 0,
isJuliaMode: false,
juliaCr: -0.7269,
juliaCi: 0.1889,
bookmarks: [],
zoomHistory: [{ ...exports.DEFAULT_VIEW }],
historyIndex: 0,
hudVisible: true,
hudTimer: 0,
pixelRatio: window.devicePixelRatio || 1,
};
}
function getState() { return state; }
function setState(s) { state = s; }
/** Initialize global state */
function initApp(canvasW, canvasH) {
state = createInitialState(canvasW, canvasH);
return state;
}
/** Set the view directly (no animation) */
function setView(view) {
state.view = { ...view };
state.maxIter = (0, math_1.computeMaxIter)(state.view.scale / exports.DEFAULT_VIEW.scale);
(0, math_1.setMaxIter)(state.maxIter);
}
/** Zoom toward a focal point in fractal coordinates */
function zoomAt(fracRe, fracIm, factor) {
const v = state.view;
const newScale = v.scale * factor;
if (newScale / exports.DEFAULT_VIEW.scale > MAX_MAGNIFICATION) {
// Warn about precision limits
console.warn(`Fractal Explorer: approaching precision limit at ×${Math.round(newScale / exports.DEFAULT_VIEW.scale).toExponential(2)}`);
return;
}
// The focal point stays fixed while the view scales
const fRe = v.centerRe + (fracRe - 0.5) / v.scale;
const fIm = v.centerIm + (fracIm - 0.5) / v.scale;
const newCenterRe = fRe - (fracRe - 0.5) / newScale;
const newCenterIm = fIm - (fracIm - 0.5) / newScale;
pushZoomHistory();
state.view = { centerRe: newCenterRe, centerIm: newCenterIm, scale: newScale };
state.maxIter = (0, math_1.computeMaxIter)(newScale / exports.DEFAULT_VIEW.scale);
}
/** Pan by dx, dy in fractal units */
function panBy(dx, dy) {
pushZoomHistory();
state.view.centerRe -= dx;
state.view.centerIm -= dy;
}
/** Push current view to zoom history stack */
function pushZoomHistory() {
// Remove any forward history if we're not at the end
state.zoomHistory = state.zoomHistory.slice(0, state.historyIndex + 1);
state.zoomHistory.push({ ...state.view });
state.historyIndex = state.zoomHistory.length - 1;
}
/** Go back one step in zoom history */
function goBack() {
if (state.historyIndex <= 0)
return false;
state.historyIndex--;
state.view = { ...state.zoomHistory[state.historyIndex] };
state.maxIter = (0, math_1.computeMaxIter)(state.view.scale / exports.DEFAULT_VIEW.scale);
return true;
}
/** Get magnification relative to default view */
function getMagnification() {
return state.view.scale / exports.DEFAULT_VIEW.scale;
}
};
// ── module: src/bookmarks.ts ──
__mods["src/bookmarks.ts"] = function (exports, require, module) {
"use strict";
// ============================================================
// BOOKMARKS — Save/load/view management via localStorage
// ============================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.saveBookmark = saveBookmark;
exports.loadBookmarks = loadBookmarks;
exports.deleteBookmark = deleteBookmark;
exports.restoreBookmark = restoreBookmark;
exports.saveLastView = saveLastView;
exports.loadLinkParams = loadLinkParams;
const STORAGE_KEY = 'fractal_bookmarks';
const LAST_VIEW_KEY = 'fractal_last_view';
function saveBookmark(state, name) {
const bookmark = {
id: crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(36).slice(2)}`,
name,
timestamp: Date.now(),
params: { ...state.view },
maxIter: state.maxIter,
paletteIndex: state.paletteIndex,
cyclicOffset: state.cyclicOffset,
isJulia: state.isJuliaMode,
juliaCr: state.juliaCr,
juliaCi: state.juliaCi,
};
const bookmarks = loadBookmarks();
bookmarks.push(bookmark);
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(bookmarks));
}
catch (e) {
console.warn('Failed to save bookmark:', e);
}
return bookmark;
}
function loadBookmarks() {
try {
const data = localStorage.getItem(STORAGE_KEY);
return data ? JSON.parse(data) : [];
}
catch {
return [];
}
}
function deleteBookmark(id) {
const bookmarks = loadBookmarks().filter(b => b.id !== id);
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(bookmarks));
}
catch { /* ignore */ }
}
function restoreBookmark(bookmark) {
return {
view: { ...bookmark.params },
targetView: null,
maxIter: bookmark.maxIter,
paletteIndex: bookmark.paletteIndex,
cyclicOffset: bookmark.cyclicOffset,
isJuliaMode: bookmark.isJulia,
juliaCr: bookmark.juliaCr,
juliaCi: bookmark.juliaCi,
bookmarks: loadBookmarks(),
zoomHistory: [{ ...bookmark.params }],
historyIndex: 0,
hudVisible: true,
hudTimer: 0,
pixelRatio: window.devicePixelRatio || 1,
};
}
/** Save current view as a temporary "last viewed" for URL sharing */
function saveLastView(state) {
try {
const data = {
re: state.view.centerRe,
im: state.view.centerIm,
scale: state.view.scale,
iter: state.maxIter,
palette: state.paletteIndex,
offset: state.cyclicOffset,
julia: state.isJuliaMode,
jcr: state.juliaCr,
jci: state.juliaCi,
};
localStorage.setItem(LAST_VIEW_KEY, JSON.stringify(data));
}
catch { /* ignore */ }
}
function loadLinkParams() {
// Check URL hash first
const hash = window.location.hash.slice(1);
if (hash) {
try {
const params = JSON.parse(decodeURIComponent(hash));
return params;
}
catch { /* invalid hash */ }
}
// Fall back to localStorage
try {
const data = localStorage.getItem(LAST_VIEW_KEY);
if (data)
return JSON.parse(data);
}
catch { /* ignore */ }
return null;
}
};
// ── module: src/render.ts ──
__mods["src/render.ts"] = function (exports, require, module) {
"use strict";
// ============================================================
// RENDER — Pixel-level fractal rendering pipeline
// Uses ImageData + typed arrays for fast pixel writes
// ============================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.FractalRenderer = void 0;
const palette_1 = require("./palette");
/**
* Renders a fractal directly into the canvas.
* Blocking call — computes iterations, colorizes, puts image data.
*/
class FractalRenderer {
constructor(canvas) {
this._colorLUT = new Uint32Array(0);
this._lutSize = 0;
this._w = 0;
this._h = 0;
this._canvas = canvas;
this._ctx = canvas.getContext('2d', { willReadFrequently: false });
}
/** Resize internal buffers */
resize(w, h) {
this._w = w;
this._h = h;
this._canvas.width = w;
this._canvas.height = h;
this._imageData = this._ctx.createImageData(w, h);
this._buf32 = new Uint32Array(this._imageData.data.buffer);
this._iterBuffer = new Uint32Array(w * h);
this._smoothBuffer = new Float64Array(w * h);
this._isInterior = new Uint8Array(w * h);
}
/** Rebuild the color lookup table */
rebuildLUT(maxIter) {
const palette = (0, palette_1.getCurrentPalette)();
const offset = (0, palette_1.getCyclicOffset)();
// Smooth iterations can go slightly above maxIter, add headroom
const size = Math.max(maxIter + 200, 512);
this._colorLUT = (0, palette_1.buildColorLUT)(palette, offset, size);
this._lutSize = size;
}
/**
* Full-resolution synchronous render.
* Blocks the thread for the duration of computation.
*/
render(opts) {
const w = opts.width;
const h = opts.height;
if (w !== this._w || h !== this._h) {
this.resize(w, h);
}
const buf32 = this._buf32;
const lut = this._colorLUT;
const lutSize = this._lutSize;
// Clear to black
buf32.fill(0xFF000000);
if (opts.isJuliaMode) {
this._renderJulia(opts, buf32, lut, lutSize);
}
else {
this._renderMandelbrot(opts, buf32, lut, lutSize);
}
this._ctx.putImageData(this._imageData, 0, 0);
}
/**
* Progressive render: renders at reduced resolution first,
* then refines. Each step calls onComplete before proceeding.
*/
renderProgressive(opts, onComplete) {
const passes = [0.25, 0.5, 0.75, 1.0];
let idx = 0;
const doPass = () => {
if (idx >= passes.length) {
onComplete?.();
return;
}
const factor = passes[idx];
const pw = Math.max(1, Math.ceil(opts.width * factor));
const ph = Math.max(1, Math.ceil(opts.height * factor));
// Render at reduced resolution
this._renderAtResolution(opts, pw, ph, factor);
idx++;
setTimeout(doPass, 0);
};
doPass();
}
/** Draw a selection rectangle overlay on the canvas */
drawSelectionRect(x, y, w, h) {
const ctx = this._ctx;
ctx.save();
ctx.fillStyle = 'rgba(255,255,255,0.04)';
ctx.fillRect(x, y, w, h);
ctx.strokeStyle = 'rgba(255,255,255,0.85)';
ctx.lineWidth = 2;
ctx.setLineDash([8, 4]);
ctx.strokeRect(x, y, w, h);
ctx.restore();
}
/** Clear canvas to solid black */
clear() {
const buf32 = this._buf32;
buf32.fill(0xFF000000);
this._ctx.putImageData(this._imageData, 0, 0);
}
// ---- Internal render methods ----
_renderMandelbrot(opts, buf32, lut, lutSize) {
const { reMin, reMax, imMin, imMax, maxIter, width: w, height: h } = opts;
const dRe = reMax - reMin;
const dIm = imMax - imMin;
for (let py = 0; py < h; py++) {
const ci = imMin + (py / h) * dIm;
const rowOff = py * w;
for (let px = 0; px < w; px++) {
const cr = reMin + (px / w) * dRe;
const off = rowOff + px;
// z = z^2 + c, z0 = 0
let zr = 0, zi = 0;
let zr2 = 0, zi2 = 0;
let i = 0;
while (i < maxIter && zr2 + zi2 <= 256) {
zi = 2 * zr * zi + ci;
zr = zr2 - zi2 + cr;
zr2 = zr * zr;
zi2 = zi * zi;
i++;
}
if (i >= maxIter) {
buf32[off] = 0xFF000000; // Interior: black
}
else {
// Smooth coloring
const log_zn = Math.log(zr2 + zi2) / 2.0;
const nu = Math.log(log_zn / 0.6931471805599453) / 0.6931471805599453;
const smoothVal = i + 1.0 - nu;
const lutIdx = Math.min(Math.floor(smoothVal), lutSize - 1);
buf32[off] = lut[lutIdx];
}
}
}
}
_renderJulia(opts, buf32, lut, lutSize) {
// Julia c is set globally via setJuliaC()
const cj = globalThis.__juliaCr ?? -0.7269;
const cjj = globalThis.__juliaCi ?? 0.1889;
const { reMin, reMax, imMin, imMax, maxIter, width: w, height: h } = opts;
const dRe = reMax - reMin;
const dIm = imMax - imMin;
for (let py = 0; py < h; py++) {
const ci = imMin + (py / h) * dIm;
const rowOff = py * w;
for (let px = 0; px < w; px++) {
const cr = reMin + (px / w) * dRe;
const off = rowOff + px;
// Julia: z0 = point, c = fixed
let zr = cr, zi = ci;
let zr2 = zr * zr, zi2 = zi * zi;
let i = 0;
while (i < maxIter && zr2 + zi2 <= 256) {
zi = 2 * zr * zi + cjj;
zr = zr2 - zi2 + cj;
zr2 = zr * zr;
zi2 = zi * zi;
i++;
}
if (i >= maxIter) {
buf32[off] = 0xFF000000;
}
else {
const log_zn = Math.log(zr2 + zi2) / 2.0;
const nu = Math.log(log_zn / 0.6931471805599453) / 0.6931471805599453;
const smoothVal = i + 1.0 - nu;
const lutIdx = Math.min(Math.floor(smoothVal), lutSize - 1);
buf32[off] = lut[lutIdx];
}
}
}
}
/** Render at a specific resolution factor (0..1) */
_renderAtResolution(opts, pw, ph, factor) {
// Render sub-rect at reduced res, then upscale to full canvas
const dRe = opts.reMax - opts.reMin;
const dIm = opts.imMax - opts.imMin;
const subW = Math.max(1, Math.ceil(pw * factor));
const subH = Math.max(1, Math.ceil(ph * factor));
const subIter = new Float64Array(subW * subH);
const cj = globalThis.__juliaCr ?? -0.7269;
const cjj = globalThis.__juliaCi ?? 0.1889;
const buf32 = this._buf32;
const lut = this._colorLUT;
const lutSize = this._lutSize;
for (let sy = 0; sy < subH; sy++) {
const ci = opts.imMin + (sy / subH) * dIm;
const srcRowOff = sy * subW;
for (let sx = 0; sx < subW; sx++) {
const cr = opts.reMin + (sx / subW) * dRe;
const srcOff = srcRowOff + sx;
let zr, zi, zr2, zi2;
let i = 0;
if (opts.isJuliaMode) {
zr = cr;
zi = ci;
zr2 = zr * zr;
zi2 = zi * zi;
while (i < opts.maxIter && zr2 + zi2 <= 256) {
zi = 2 * zr * zi + cjj;
zr = zr2 - zi2 + cj;
zr2 = zr * zr;
zi2 = zi * zi;
i++;
}
}
else {
zr = 0;
zi = 0;
zr2 = 0;
zi2 = 0;
while (i < opts.maxIter && zr2 + zi2 <= 256) {
zi = 2 * zr * zi + ci;
zr = zr2 - zi2 + cr;
zr2 = zr * zr;
zi2 = zi * zi;
i++;
}
}
let color;
if (i >= opts.maxIter) {
color = 0xFF000000;
}
else {
const log_zn = Math.log(zr2 + zi2) / 2.0;
const nu = Math.log(log_zn / 0.6931471805599453) / 0.6931471805599453;
const smoothVal = i + 1.0 - nu;
const lutIdx = Math.min(Math.floor(smoothVal), lutSize - 1);
color = lut[lutIdx];
}
// Upscale: map each sub-pixel to a block of destination pixels
const dx0 = Math.floor((sx / subW) * opts.width);
const dy0 = Math.floor((sy / subH) * opts.height);
const dx1 = Math.floor(((sx + 1) / subW) * opts.width);
const dy1 = Math.floor(((sy + 1) / subH) * opts.height);
for (let dy = dy0; dy < dy1; dy++) {
const dstRow = dy * opts.width;
for (let dx = dx0; dx < dx1; dx++) {
buf32[dstRow + dx] = color;
}
}
}
}
this._ctx.putImageData(this._imageData, 0, 0);
}
/** Get the canvas element */
get canvas() { return this._canvas; }
/** Get the underlying ImageData for inspection */
getImageData() { return this._imageData; }
}
exports.FractalRenderer = FractalRenderer;
};
// ── module: src/input.ts ──
__mods["src/input.ts"] = function (exports, require, module) {
"use strict";
// ============================================================
// INPUT — Mouse and keyboard event handling for navigation
// ============================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.InputHandler = void 0;
const state_1 = require("./state");
class InputHandler {
constructor(config) {
// State
this._isDragging = false;
this._dragStartX = 0;
this._dragStartY = 0;
this._dragEndX = 0;
this._dragEndY = 0;
this._isPanning = false;
this._panStartX = 0;
this._panStartY = 0;
this._panStartCenterRe = 0;
this._panStartCenterIm = 0;
this._lastWheelTime = 0;
this._mousemoveActive = false;
this._canvas = config.canvas;
this._renderer = config.renderer;
this._onStateChange = config.onStateChange || (() => { });
this._bindEvents();
}
_bindEvents() {
this._canvas.addEventListener('mousedown', this._onMouseDown.bind(this));
this._canvas.addEventListener('mousemove', this._onMouseMove.bind(this));
window.addEventListener('mouseup', this._onMouseUp.bind(this));
this._canvas.addEventListener('wheel', this._onWheel.bind(this), { passive: false });
this._canvas.addEventListener('contextmenu', e => e.preventDefault());
window.addEventListener('keydown', this._onKeyDown.bind(this));
// Touch support
this._canvas.addEventListener('touchstart', this._onTouchStart.bind(this), { passive: false });
this._canvas.addEventListener('touchmove', this._onTouchMove.bind(this), { passive: false });
this._canvas.addEventListener('touchend', this._onTouchEnd.bind(this));
}
/** Convert screen coords to fractal-space coordinates */
_screenToFractal(sx, sy) {
const state = (0, state_1.getState)();
const v = state.view;
const w = this._canvas.width / state.pixelRatio;
const h = this._canvas.height / state.pixelRatio;
const re = v.centerRe + (sx / w - 0.5) / v.scale;
const im = v.centerIm + (sy / h - 0.5) / v.scale;
return [re, im];
}
_onMouseDown(e) {
if (e.button === 1 || (e.button === 0 && e.altKey)) {
// Middle mouse or Alt+Left = pan
this._isPanning = true;
this._panStartX = e.clientX;
this._panStartY = e.clientY;
const state = (0, state_1.getState)();
this._panStartCenterRe = state.view.centerRe;
this._panStartCenterIm = state.view.centerIm;
this._canvas.style.cursor = 'grabbing';
}
else if (e.button === 0) {
// Left click = start selection rectangle
this._isDragging = true;
this._dragStartX = e.offsetX;
this._dragStartY = e.offsetY;
this._dragEndX = e.offsetX;
this._dragEndY = e.offsetY;
this._canvas.style.cursor = 'crosshair';
}
}
_onMouseMove(e) {
this._mousemoveActive = true;
this._onStateChange();
if (this._isDragging) {
this._dragEndX = e.offsetX;
this._dragEndY = e.offsetY;
if (this.onSelectRectMove) {
this.onSelectRectMove(e.offsetX, e.offsetY);
}
}
else if (this._isPanning) {
const dx = e.clientX - this._panStartX;
const dy = e.clientY - this._panStartY;
const state = (0, state_1.getState)();
const w = this._canvas.width / state.pixelRatio;
const h = this._canvas.height / state.pixelRatio;
const fr = -dx / (w * state.view.scale);
const fi = -dy / (h * state.view.scale);
(0, state_1.setView)({
centerRe: this._panStartCenterRe + fr,
centerIm: this._panStartCenterIm + fi,
scale: state.view.scale,
});
this._onStateChange();
}
}
_onMouseUp(e) {
if (this._isDragging) {
const sx1 = Math.min(this._dragStartX, this._dragEndX);
const sy1 = Math.min(this._dragStartY, this._dragEndY);
const sx2 = Math.max(this._dragStartX, this._dragEndX);
const sy2 = Math.max(this._dragStartY, this._dragEndY);
const sw = sx2 - sx1;
const sh = sy2 - sy1;
// Only zoom if selection is meaningful
if (sw > 5 && sh > 5 && this.onSelectRectEnd) {
this.onSelectRectEnd(sx1, sy1, sx2, sy2);
}
this._isDragging = false;
this._canvas.style.cursor = 'default';
}
else if (this._isPanning) {
this._isPanning = false;
this._canvas.style.cursor = 'default';
}
}
_onWheel(e) {
e.preventDefault();
const now = performance.now();
if (now - this._lastWheelTime < 50)
return; // Throttle
this._lastWheelTime = now;
const factor = e.deltaY < 0 ? 1.3 : 1 / 1.3;
const rect = this._canvas.getBoundingClientRect();
const sx = e.clientX - rect.left;
const sy = e.clientY - rect.top;
const [fr, fi] = this._screenToFractal(sx, sy);
(0, state_1.zoomAt)(fr, fi, factor);
this._onStateChange();
}
_onKeyDown(e) {
const state = (0, state_1.getState)();
const key = e.key;
let handled = false;
switch (key) {
case 'ArrowLeft':
(0, state_1.panBy)(0.1 / state.view.scale, 0);
handled = true;
break;
case 'ArrowRight':
(0, state_1.panBy)(-0.1 / state.view.scale, 0);
handled = true;
break;
case 'ArrowUp':
(0, state_1.panBy)(0, 0.1 / state.view.scale);
handled = true;
break;
case 'ArrowDown':
(0, state_1.panBy)(0, -0.1 / state.view.scale);
handled = true;
break;
case '+':
case '=':
(0, state_1.zoomAt)(0, 0, 1.5);
handled = true;
break;
case '-':
case '_':
(0, state_1.zoomAt)(0, 0, 1 / 1.5);
handled = true;
break;
case 'j':
case 'J':
if (this.onJuliaPoint) {
// Toggle Julia mode centered on current view
this.onJuliaPoint(state.view.centerRe, state.view.centerIm);
}
handled = true;
break;
case 'z':
case 'Z':
if (!(0, state_1.goBack)()) { /* no history */ }
handled = true;
break;
case 'Escape':
// Cancel any UI overlays
break;
}
if (handled) {
e.preventDefault();
this._onStateChange();
}
}
_onTouchStart(e) {
e.preventDefault();
if (e.touches.length === 1) {
const touch = e.touches[0];
const rect = this._canvas.getBoundingClientRect();
this._isDragging = true;
this._dragStartX = touch.clientX - rect.left;
this._dragStartY = touch.clientY - rect.top;
this._dragEndX = this._dragStartX;
this._dragEndY = this._dragStartY;
}
else if (e.touches.length === 2) {
// Pinch zoom
this._isDragging = false;
}
}
_onTouchMove(e) {
e.preventDefault();
if (e.touches.length === 1 && this._isDragging) {
const touch = e.touches[0];
const rect = this._canvas.getBoundingClientRect();
this._dragEndX = touch.clientX - rect.left;
this._dragEndY = touch.clientY - rect.top;
if (this.onSelectRectMove) {
this.onSelectRectMove(this._dragEndX, this._dragEndY);
}
}
else if (e.touches.length === 2) {
// Two-finger pinch zoom
const t0 = e.touches[0];
const t1 = e.touches[1];
const dx = t1.clientX - t0.clientX;
const dy = t1.clientY - t0.clientY;
const dist = Math.sqrt(dx * dx + dy * dy);
// Compare with previous distance for zoom factor
// Simplified: just pan
}
}
_onTouchEnd(e) {
if (this._isDragging && this.onSelectRectEnd) {
const sx1 = Math.min(this._dragStartX, this._dragEndX);
const sy1 = Math.min(this._dragStartY, this._dragEndY);
const sx2 = Math.max(this._dragStartX, this._dragEndX);
const sy2 = Math.max(this._dragStartY, this._dragEndY);
if (sx2 - sx1 > 10 && sy2 - sy1 > 10) {
this.onSelectRectEnd(sx1, sy1, sx2, sy2);
}
}
this._isDragging = false;
}
}
exports.InputHandler = InputHandler;
};
// ── module: src/ui.ts ──
__mods["src/ui.ts"] = function (exports, require, module) {
"use strict";
// ============================================================
// UI — HUD overlay rendering (glassy, auto-hiding)
// Draws on a separate overlay canvas above the fractal canvas
// ============================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.UIFrame = void 0;
const state_1 = require("./state");
const palette_1 = require("./palette");
class UIFrame {
constructor(canvas) {
this._fpsFrames = 0;
this._fpsTime = 0;
this._fpsValue = 0;
this._lastFrameTime = performance.now();
this._canvas = canvas;
this._ctx = canvas.getContext('2d');
}
resize(w, h) {
this._canvas.width = w;
this._canvas.height = h;
}
/** Update FPS counter */
frameTick() {
const now = performance.now();
this._fpsFrames++;
if (now - this._fpsTime >= 1000) {
this._fpsValue = this._fpsFrames;
this._fpsFrames = 0;
this._fpsTime = now;
}
this._lastFrameTime = now;
}
/** Mark interaction for HUD visibility timer */
interact() {
const state = (0, state_1.getState)();
state.hudVisible = true;
state.hudTimer = 3000; // Show for 3 seconds after last interaction
}
draw() {
const ctx = this._ctx;
const w = this._canvas.width;
const h = this._canvas.height;
const state = (0, state_1.getState)();
ctx.clearRect(0, 0, w, h);
// Fade HUD
if (state.hudTimer > 0) {
state.hudTimer -= 16; // ~60fps tick
if (state.hudTimer <= 0) {
state.hudTimer = 0;
state.hudVisible = false;
}
}
if (!state.hudVisible)
return;
const alpha = Math.min(1, state.hudTimer / 500);
ctx.globalAlpha = alpha * 0.85;
// Draw HUD panels
this._drawTopLeft(ctx, w, h, state);
this._drawBottomRight(ctx, w, h, state);
this._drawSelectionOverlay(ctx, w, h, state);
ctx.globalAlpha = 1;
}
_drawTopLeft(ctx, w, h, state) {
const mag = (0, state_1.getMagnification)();
const palette = (0, palette_1.getCurrentPalette)();
const isJulia = state.isJuliaMode;
// Background panel
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
ctx.strokeStyle = 'rgba(255, 255, 255, 0.15)';
ctx.lineWidth = 1;
const pw = 260;
const ph = 130;
this._roundRect(ctx, 10, 10, pw, ph, 8);
ctx.fill();
ctx.stroke();
ctx.font = 'bold 13px "SF Mono", "Fira Code", monospace';
ctx.textBaseline = 'top';
// Title
ctx.fillStyle = '#fff';
const title = isJulia ? `✦ Julia Set` : `◆ Mandelbrot`;
ctx.fillText(title, 20, 18);
// Magnification
ctx.font = '12px "SF Mono", "Fira Code", monospace';
ctx.fillStyle = '#aaa';
ctx.fillText(`×${this._formatMag(mag)}`, 20, 40);
// Center coordinates
ctx.fillStyle = '#888';
ctx.fillText(`Re: ${state.view.centerRe.toPrecision(12)}`, 20, 58);
ctx.fillText(`Im: ${state.view.centerIm.toPrecision(12)}`, 20, 74);
// Iterations
ctx.fillStyle = '#aaa';
ctx.fillText(`Iterations: ${state.maxIter}`, 20, 96);
// Palette
ctx.fillStyle = '#ccc';
ctx.fillText(`Palette: ${palette.name}`, 20, 114);
}
_drawBottomRight(ctx, w, h, state) {
// FPS + controls hint
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
ctx.strokeStyle = 'rgba(255, 255, 255, 0.15)';
ctx.lineWidth = 1;
const pw = 200;
const ph = 90;
this._roundRect(ctx, w - pw - 10, h - ph - 10, pw, ph, 8);
ctx.fill();
ctx.stroke();
ctx.font = '11px "SF Mono", "Fira Code", monospace';
ctx.textBaseline = 'top';
ctx.fillStyle = '#aaa';
ctx.fillText(`FPS: ${this._fpsValue}`, w - pw - 2, h - ph - 6);
ctx.fillStyle = '#777';
ctx.fillText('Drag: zoom box', w - pw - 2, h - ph + 14);
ctx.fillText('Scroll: zoom in/out', w - pw - 2, h - ph + 30);
ctx.fillText('Alt+drag: pan', w - pw - 2, h - ph + 46);
ctx.fillText('J: center Julia set', w - pw - 2, h - ph + 62);
}
_drawSelectionOverlay(ctx, w, h, state) {
// This is called when there's an active drag selection
// The actual rect drawing happens in the main render loop
}
_formatMag(mag) {
if (mag < 1000)
return mag.toFixed(1);
if (mag < 1e6)
return (mag / 1000).toFixed(1) + 'K';
if (mag < 1e9)
return (mag / 1e6).toFixed(1) + 'M';
if (mag < 1e12)
return (mag / 1e9).toFixed(1) + 'B';
return mag.toExponential(2);
}
_roundRect(ctx, x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
ctx.lineTo(x + w, y + h - r);
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
ctx.lineTo(x + r, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
ctx.lineTo(x, y + r);
ctx.quadraticCurveTo(x, y, x + r, y);
ctx.closePath();
}
/** Draw crosshair at fractional position */
drawCrosshair(ctx, fx, fy) {
const cw = this._canvas.width;
const ch = this._canvas.height;
ctx.save();
ctx.strokeStyle = 'rgba(255, 255, 255, 0.4)';
ctx.lineWidth = 1;
ctx.setLineDash([4, 4]);
ctx.beginPath();
ctx.moveTo(fx, 0);
ctx.lineTo(fx, ch);
ctx.moveTo(0, fy);
ctx.lineTo(cw, fy);
ctx.stroke();
// Small circle at center
ctx.beginPath();
ctx.arc(fx, fy, 6, 0, Math.PI * 2);
ctx.stroke();
ctx.restore();
}
}
exports.UIFrame = UIFrame;
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen/qwen3.7-flash","provider":"openrouter","persona":"typescript","sessionId":"f09ae3d7-9081-4446-8186-0711c71bdf63","tokensIn":6011676,"tokensOut":71019,"tokensTotal":6082695,"cost":0.23390202199999996,"turns":103,"toolCalls":103,"failedToolCalls":2,"timestamp":"2026-08-02T03:10:45.618Z"} -->