Fractal Explorer
Qwen 3.8 27B GGUF · 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 81,183 bytes · SHA-256 aba0d71d55e7
<!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 ── */
:root {
--glass: rgba(12, 16, 26, 0.55);
--glass-brd: rgba(255, 255, 255, 0.14);
--ink: rgba(240, 244, 252, 0.92);
--ink-dim: rgba(200, 210, 228, 0.55);
--accent: #7dd3fc;
--warn: #fbbf24;
--mono: "SF Mono", ui-monospace, "Cascadia Code", Menlo, Consolas, monospace;
--ui: "Inter", -apple-system, "Segoe UI", system-ui, sans-serif;
}
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
background: #05070c;
font-family: var(--ui);
color: var(--ink);
}
#app, .stage {
position: fixed;
inset: 0;
}
canvas#fractal-canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
display: block;
cursor: crosshair;
}
canvas#overlay-canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
display: block;
pointer-events: none;
z-index: 2;
}
/* ── css: hud ── */
/* ---- glassy HUD (top-left) ---- */
.hud {
position: absolute;
top: 14px;
left: 14px;
z-index: 5;
background: var(--glass);
border: 1px solid var(--glass-brd);
border-radius: 12px;
padding: 10px 14px 9px;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.35);
pointer-events: none;
user-select: none;
transition: opacity 0.35s ease, transform 0.35s ease;
min-width: 220px;
}
.hud.idle { opacity: 0; transform: translateY(-4px); }
.hud-actions { pointer-events: auto; }
.hud-mag {
font-family: var(--mono);
font-size: 21px;
font-weight: 600;
letter-spacing: 0.02em;
line-height: 1.1;
}
.hud-mag.warn { color: var(--warn); }
.hud-sub {
font-family: var(--mono);
font-size: 10.5px;
color: var(--ink-dim);
margin-top: 3px;
white-space: nowrap;
}
.hud-iter {
font-family: var(--mono);
font-size: 10.5px;
color: var(--ink-dim);
margin-top: 5px;
white-space: nowrap;
}
.hud-actions {
display: flex;
gap: 6px;
margin-top: 8px;
}
.hud-btn {
font-family: var(--ui);
font-size: 11px;
color: var(--ink);
background: rgba(255, 255, 255, 0.07);
border: 1px solid var(--glass-brd);
border-radius: 7px;
padding: 4px 9px;
cursor: pointer;
transition: background 0.15s ease;
}
.hud-btn:hover { background: rgba(255, 255, 255, 0.16); }
.hud-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* ---- toolbar (bottom center) ---- */
.toolbar {
position: absolute;
bottom: 14px;
left: 50%;
transform: translateX(-50%);
z-index: 5;
display: flex;
align-items: center;
gap: 8px;
background: var(--glass);
border: 1px solid var(--glass-brd);
border-radius: 12px;
padding: 8px 12px;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.35);
max-width: calc(100vw - 24px);
flex-wrap: wrap;
justify-content: center;
}
.chip {
width: 34px;
height: 18px;
border-radius: 5px;
border: 1px solid rgba(255, 255, 255, 0.25);
cursor: pointer;
padding: 0;
transition: transform 0.12s ease, box-shadow 0.12s ease;
}
.chip:hover { transform: translateY(-1px); }
.chip.on {
box-shadow: 0 0 0 2px var(--accent), 0 0 12px rgba(125, 211, 252, 0.5);
border-color: #fff;
}
.chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.ctl {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 10.5px;
color: var(--ink-dim);
letter-spacing: 0.04em;
}
.ctl-lbl { text-transform: uppercase; font-size: 9px; letter-spacing: 0.08em; }
.ctl-val {
font-family: var(--mono);
font-size: 10px;
color: var(--ink);
min-width: 58px;
text-align: left;
}
.ctl input[type="range"] {
width: 86px;
accent-color: var(--accent);
height: 3px;
cursor: pointer;
}
.ctl.chk { cursor: pointer; user-select: none; }
.ctl.chk input { accent-color: var(--accent); cursor: pointer; }
.sep { width: 1px; height: 22px; background: var(--glass-brd); margin: 0 2px; }
.btn {
font-family: var(--ui);
font-size: 11px;
color: var(--ink);
background: rgba(255, 255, 255, 0.07);
border: 1px solid var(--glass-brd);
border-radius: 7px;
padding: 4px 9px;
cursor: pointer;
}
.btn:hover { background: rgba(255, 255, 255, 0.16); }
.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.btn:disabled { opacity: 0.4; cursor: default; }
.btn.sel { max-width: 120px; }
/* ---- toast ---- */
.toast {
position: absolute;
top: 16px;
left: 50%;
transform: translateX(-50%) translateY(-8px);
z-index: 9;
background: var(--glass);
border: 1px solid var(--glass-brd);
border-radius: 9px;
padding: 7px 14px;
font-size: 12px;
color: var(--ink);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease, transform 0.25s ease;
white-space: nowrap;
}
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
/* ---- help bar ---- */
.help {
position: absolute;
bottom: 58px;
left: 50%;
transform: translateX(-50%);
z-index: 4;
font-size: 10.5px;
color: var(--ink-dim);
background: rgba(5, 8, 14, 0.5);
border-radius: 8px;
padding: 4px 10px;
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
pointer-events: none;
user-select: none;
white-space: nowrap;
letter-spacing: 0.02em;
}
.help b { color: var(--ink); font-weight: 600; }
/* ── css: julia ── */
/* ---- Julia pane (right side) ---- */
.julia-pane {
position: absolute;
top: 14px;
right: 14px;
bottom: 14px;
width: clamp(220px, 26vw, 380px);
z-index: 6;
display: none;
flex-direction: column;
gap: 6px;
background: var(--glass);
border: 1px solid var(--glass-brd);
border-radius: 12px;
padding: 10px;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.35);
}
.jp-head {
display: flex;
align-items: center;
gap: 8px;
min-height: 22px;
}
.jp-title {
font-size: 10px;
font-weight: 700;
letter-spacing: 0.22em;
color: var(--accent);
}
.jp-c {
font-family: var(--mono);
font-size: 10px;
color: var(--ink-dim);
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.jp-close {
width: 22px;
height: 22px;
border-radius: 6px;
border: 1px solid var(--glass-brd);
background: rgba(255, 255, 255, 0.07);
color: var(--ink);
font-size: 13px;
line-height: 1;
cursor: pointer;
}
.jp-close:hover { background: rgba(255, 255, 255, 0.16); }
.jp-close:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.jp-canvas {
flex: 1;
width: 100%;
height: 100%;
display: block;
border-radius: 8px;
background: #05070c;
cursor: crosshair;
}
.jp-hint {
font-size: 9.5px;
color: var(--ink-dim);
letter-spacing: 0.03em;
text-align: center;
user-select: none;
}
/* narrow screens: shrink panes */
@media (max-width: 760px) {
.julia-pane { width: 46vw; min-width: 180px; }
.help { display: none; }
.toolbar { gap: 6px; padding: 6px 8px; }
.ctl input[type="range"] { width: 64px; }
}
</style>
</head>
<body>
<div id="app">
<div class="stage">
<canvas id="fractal-canvas"></canvas>
<canvas id="overlay-canvas"></canvas>
<div class="hud"></div>
<div class="toolbar"></div>
<aside class="julia-pane" id="julia-pane">
<div class="jp-head">
<span class="jp-title">JULIA</span>
<span class="jp-c" id="julia-c-label">c = (—, —)</span>
<button class="jp-close" title="Close Julia pane">×</button>
</div>
<canvas class="jp-canvas" id="julia-canvas"></canvas>
<span class="jp-hint">hover Mandelbrot to move c · click fixes c</span>
</aside>
<div class="toast"></div>
<div class="help"></div>
</div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./consts":"src/consts.ts","./math":"src/math.ts","./state":"src/state.ts","./renderer":"src/renderer.ts","./ui":"src/ui.ts","./input":"src/input.ts","./overlay":"src/overlay.ts"},"src/math.ts":{"./consts":"src/consts.ts"},"src/state.ts":{"./consts":"src/consts.ts","./math":"src/math.ts"},"src/renderer.ts":{"./consts":"src/consts.ts","./math":"src/math.ts"},"src/ui.ts":{"./consts":"src/consts.ts","./state":"src/state.ts","./math":"src/math.ts"},"src/input.ts":{"./consts":"src/consts.ts","./math":"src/math.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 — app shell: DOM wiring, render loop, event plumbing, debug API
//
// window.fractal = {
// iterationsAt(re, im) -> smooth iter count at a complex point
// setView({re, im, scale}) -> jump to a view (scale = magnification)
// setJulia / setMandelbrot -> switch sets
// setPalette / setPaletteOffset / setDrift
// save / load -> URL hash state (shareable links)
// bookmarks() -> names of saved bookmarks
// reset / finish / version
// }
// ============================================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.start = start;
const consts_1 = require("./consts");
const math_1 = require("./math");
const state_1 = require("./state");
const renderer_1 = require("./renderer");
const ui_1 = require("./ui");
const input_1 = require("./input");
const overlay_1 = require("./overlay");
// ---------------------------------------------------------------------------
// module-level refs
// ---------------------------------------------------------------------------
let mainCanvas;
let overlayCanvas;
let juliaCanvas;
let mainRenderer;
let juliaRenderer = null;
let overlay;
let juliaPaneEl;
let juliaCLabel;
let hud = null;
let toolbar = null;
let toastEl = null;
let toast = null;
let mainNav = null;
let juliaNav = null;
let mainView = { re: 0, im: 0, w: 3 };
let viewChanged = true;
let lastC = { re: -0.743, im: 0.131 };
let lastHashTick = 0;
let lastWarn = 0;
let fps = 60;
let lastFrameT = 0;
let lastHudT = 0;
let lastDrift = 0;
let lastInteraction = performance.now();
function markActive() {
lastInteraction = performance.now();
}
/** Baked 256-entry LUT per palette (built once at startup). */
const luts = consts_1.PALETTES.map((p) => ({ table: (0, consts_1.bakePalette)(p), cyclic: p.cyclic }));
function toastMsg(msg) {
if (toast)
toast.show(msg);
}
// ---------------------------------------------------------------------------
// render orchestration
// ---------------------------------------------------------------------------
function applyMainView(anchor) {
mainView = { re: state_1.state.view.re, im: state_1.state.view.im, w: state_1.state.view.w };
mainRenderer.setView(mainView, (0, state_1.effIters)(state_1.state), luts[state_1.state.palette] ?? luts[0], state_1.state.paletteOffset, anchor ?? null);
(0, ui_1.syncIterLabel)(toolbar, state_1.state);
viewChanged = true;
scheduleHash();
}
function applyJuliaParams() {
if (!juliaRenderer)
return;
juliaRenderer.setView(state_1.state.view, (0, state_1.effIters)(state_1.state), luts[state_1.state.palette] ?? luts[0], state_1.state.paletteOffset);
juliaRenderer.setJuliaC(state_1.state.cRe, state_1.state.cIm);
updateJuliaLabel();
}
function updateJuliaLabel() {
if (juliaCLabel) {
juliaCLabel.textContent = `c = (${(0, math_1.fmtCoord)(state_1.state.cRe)}, ${(0, math_1.fmtCoord)(state_1.state.cIm)})`;
}
}
function ensureJuliaRenderer() {
if (juliaRenderer)
return;
juliaRenderer = new renderer_1.FractalRenderer(juliaCanvas, 'julia');
juliaNav = new input_1.NavInput({
canvas: juliaCanvas,
w0: state_1.state.w0,
getView: () => state_1.state.view,
pushHistory: () => (0, state_1.pushHistory)(state_1.state),
commit: (v) => {
(0, state_1.clampView)(state_1.state, v);
state_1.state.view = v;
applyJuliaParams();
},
});
}
function setMode(mode, pick) {
(0, state_1.pushHistory)(state_1.state);
state_1.state.mode = mode;
if (mode === 'julia') {
if (pick) {
state_1.state.cRe = pick.re;
state_1.state.cIm = pick.im;
}
ensureJuliaRenderer();
sizeJuliaPane();
applyJuliaParams();
juliaPaneEl.style.display = 'flex';
}
else {
juliaPaneEl.style.display = 'none';
}
updateJuliaLabel();
scheduleHash();
}
function sizeJuliaPane() {
if (!juliaRenderer)
return;
const pw = Math.max(140, Math.round((mainCanvas.clientWidth || 800) * 0.24));
const ph = Math.round(pw * 0.66);
const dpr = Math.min(2, window.devicePixelRatio || 1);
juliaCanvas.style.width = pw + 'px';
juliaCanvas.style.height = ph + 'px';
juliaRenderer.resize(pw, ph, dpr);
}
/** Full palette/offset re-render from existing iteration buffers (no recompute). */
function recolorAll() {
const lut = luts[state_1.state.palette] ?? luts[0];
mainRenderer.recolorize(lut, state_1.state.paletteOffset);
if (juliaRenderer)
juliaRenderer.recolorize(lut, state_1.state.paletteOffset);
if (toolbar)
(0, ui_1.syncPaletteChips)(toolbar, state_1.state.palette);
}
function refreshBookmarkList() {
if (!toolbar)
return;
const names = state_1.state.bookmarks.map((b) => b.name);
(0, ui_1.fillBookmarkList)(toolbar.bmList, names, -1);
toolbar.bmDel.disabled = state_1.state.bookmarks.length === 0;
}
function scheduleHash() {
const now = performance.now();
if (now - lastHashTick > 400) {
lastHashTick = now;
(0, state_1.writeHash)(state_1.state);
}
}
function syncModeUI() {
if (state_1.state.mode === 'julia') {
ensureJuliaRenderer();
sizeJuliaPane();
applyJuliaParams();
juliaPaneEl.style.display = 'flex';
}
else {
juliaPaneEl.style.display = 'none';
}
}
// ---------------------------------------------------------------------------
// setup
// ---------------------------------------------------------------------------
function start() {
mainCanvas = document.getElementById('fractal-canvas');
overlayCanvas = document.getElementById('overlay-canvas');
juliaCanvas = document.getElementById('julia-canvas');
if (!mainCanvas || !overlayCanvas || !juliaCanvas) {
throw new Error('missing canvas elements');
}
juliaPaneEl = document.getElementById('julia-pane');
juliaCLabel = document.getElementById('julia-c-label');
mainRenderer = new renderer_1.FractalRenderer(mainCanvas, 'mandelbrot');
overlay = new overlay_1.Overlay(overlayCanvas);
hud = (0, ui_1.buildHUD)();
toolbar = (0, ui_1.buildToolbar)();
toastEl = (0, ui_1.buildToast)();
toast = new overlay_1.Toast(toastEl);
const help = (0, ui_1.buildHelp)();
const app = document.getElementById('app');
const wrap = app.firstElementChild;
wrap.querySelector('.hud').replaceWith(hud.root);
wrap.querySelector('.toolbar').replaceWith(toolbar.root);
wrap.querySelector('.julia-pane').replaceWith(juliaPaneEl);
wrap.querySelector('.toast').replaceWith(toastEl);
wrap.querySelector('.help').replaceWith(help);
// ---- state init: URL hash > defaults; bookmarks always loaded ----
(0, state_1.loadBookmarks)(state_1.state);
(0, state_1.readHash)(state_1.state);
(0, state_1.clampView)(state_1.state, state_1.state.view);
refreshBookmarkList();
// ---- initial sizes ----
const sz = () => {
const w = mainCanvas.clientWidth || 800;
const h = mainCanvas.clientHeight || 600;
return { w, h, dpr: Math.min(2, window.devicePixelRatio || 1) };
};
const s0 = sz();
mainRenderer.resize(s0.w, s0.h, s0.dpr);
overlay.resize(s0.w, s0.h, s0.dpr);
const ro = new ResizeObserver(() => {
const { w, h, dpr } = sz();
const ow = mainCanvas.clientWidth;
const oh = mainCanvas.clientHeight;
if (Math.abs(ow - w) < 1 && Math.abs(oh - h) < 1)
return;
mainRenderer.resize(w, h, dpr);
overlay.resize(w, h, dpr);
viewChanged = true;
});
ro.observe(mainCanvas);
// ---- main canvas navigation ----
mainNav = new input_1.NavInput({
canvas: mainCanvas,
w0: state_1.state.w0,
getView: () => state_1.state.view,
pushHistory: () => (0, state_1.pushHistory)(state_1.state),
commit: (v, anchor) => {
(0, state_1.clampView)(state_1.state, v);
state_1.state.view = v;
markActive();
applyMainView(anchor);
},
onRect: (x0, y0, x1, y1, active) => {
overlay.rect = { x0, y0, x1, y1, active };
markActive();
},
onPick: (re, im) => {
// clicking a point in Mandelbrot view opens Julia for that c
setMode('julia', { re, im });
toastMsg(`Julia set · c = (${(0, math_1.fmtCoord)(re)}, ${(0, math_1.fmtCoord)(im)})`);
},
onHover: (re, im, cssX, cssY) => {
lastC = { re, im };
markActive();
if (state_1.state.juliaLive && juliaRenderer) {
juliaRenderer.setJuliaC(re, im);
if (juliaCLabel) {
juliaCLabel.textContent = `c = (${(0, math_1.fmtCoord)(re)}, ${(0, math_1.fmtCoord)(im)})`;
}
}
overlay.reticle = { x: cssX, y: cssY, show: state_1.state.juliaLive };
},
});
// ---- toolbar wiring ----
const t = toolbar;
t.chips.forEach((chip, i) => {
chip.addEventListener('click', () => {
if (state_1.state.palette === i)
return;
state_1.state.palette = i;
recolorAll();
scheduleHash();
});
});
t.offset.addEventListener('input', () => {
state_1.state.paletteOffset = Number(t.offset.value) | 0;
recolorAll();
});
t.drift.addEventListener('change', () => {
state_1.state.drift = t.drift.checked;
lastDrift = performance.now();
});
t.iters.addEventListener('input', () => {
const v = Number(t.iters.value) | 0;
state_1.state.itersOverride = v < 10 ? 0 : v; // low values snap back to auto
t.iters.value = String(state_1.state.itersOverride);
if (state_1.state.mode === 'mandelbrot')
applyMainView();
if (state_1.state.mode === 'julia')
applyJuliaParams();
(0, ui_1.syncIterLabel)(t, state_1.state);
});
t.live.addEventListener('change', () => {
state_1.state.juliaLive = t.live.checked;
if (!state_1.state.juliaLive && juliaRenderer) {
juliaRenderer.setJuliaC(state_1.state.cRe, state_1.state.cIm);
overlay.reticle.show = false;
}
});
toolbar.root.addEventListener('pointerdown', markActive);
t.bmSave.addEventListener('click', () => {
const base = 'view ' + (state_1.state.bookmarks.length + 1);
const name = (window.prompt('Bookmark name', base) || base).trim();
(0, state_1.addBookmark)(state_1.state, name);
refreshBookmarkList();
t.bmList.value = String(state_1.state.bookmarks.length - 1);
toastMsg(`saved bookmark “${name}”`);
});
t.bmList.addEventListener('change', () => {
const i = Number(t.bmList.value);
const bm = state_1.state.bookmarks[i];
if (!bm)
return;
(0, state_1.restoreBookmark)(state_1.state, bm);
syncModeUI();
applyMainView();
recolorAll();
toastMsg(`restored “${bm.name}”`);
});
t.bmDel.addEventListener('click', () => {
const i = Number(t.bmList.value);
if (i >= 0 && i < state_1.state.bookmarks.length) {
(0, state_1.deleteBookmark)(state_1.state, i);
refreshBookmarkList();
toastMsg('bookmark deleted');
}
});
t.copyLink.addEventListener('click', () => {
(0, state_1.writeHash)(state_1.state);
const url = location.href;
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(url).then(() => toastMsg('link copied to clipboard'), () => toastMsg('link in address bar'));
}
else {
toastMsg('link in address bar');
}
});
// ---- HUD actions ----
hud.back.addEventListener('click', () => {
(0, state_1.goBack)(state_1.state);
syncModeUI();
applyMainView();
recolorAll();
});
const doReset = () => {
(0, state_1.resetView)(state_1.state);
state_1.state.mode = 'mandelbrot';
state_1.state.itersOverride = 0;
syncModeUI();
applyMainView();
recolorAll();
};
hud.reset.addEventListener('click', doReset);
hud.juliaBtn.addEventListener('click', () => {
setMode(state_1.state.mode === 'julia' ? 'mandelbrot' : 'julia', state_1.state.mode === 'mandelbrot' ? { re: lastC.re, im: lastC.im } : undefined);
});
const closeBtn = juliaPaneEl.querySelector('.jp-close');
closeBtn.addEventListener('click', () => setMode('mandelbrot'));
// ---- keyboard ----
window.addEventListener('keydown', (e) => {
const tag = e.target?.tagName;
if (tag === 'INPUT' || tag === 'SELECT' || tag === 'TEXTAREA')
return;
const k = e.key;
const v = state_1.state.view;
const act = (fn) => {
fn();
e.preventDefault();
markActive();
};
if (k === 'j' || k === 'J') {
act(() => setMode(state_1.state.mode === 'julia' ? 'mandelbrot' : 'julia', state_1.state.mode === 'mandelbrot' ? { re: lastC.re, im: lastC.im } : undefined));
}
else if (k === 'r' || k === 'R') {
act(doReset);
}
else if (k === 'Backspace' && e.ctrlKey) {
act(() => {
(0, state_1.goBack)(state_1.state);
syncModeUI();
applyMainView();
recolorAll();
});
}
else if (k === '+' || k === '=') {
act(() => {
(0, state_1.pushHistory)(state_1.state);
(0, state_1.setView)(state_1.state, v.re, v.im, v.w / consts_1.KEY_ZOOM_FACTOR);
applyMainView();
});
}
else if (k === '-' || k === '_') {
act(() => {
(0, state_1.pushHistory)(state_1.state);
(0, state_1.setView)(state_1.state, v.re, v.im, v.w * consts_1.KEY_ZOOM_FACTOR);
applyMainView();
});
}
else if (k.startsWith('Arrow')) {
act(() => {
const d = v.w * consts_1.KEY_PAN_FRAC;
if (k === 'ArrowLeft')
(0, state_1.setView)(state_1.state, v.re - d, v.im, v.w);
if (k === 'ArrowRight')
(0, state_1.setView)(state_1.state, v.re + d, v.im, v.w);
if (k === 'ArrowUp')
(0, state_1.setView)(state_1.state, v.re, v.im - d, v.w);
if (k === 'ArrowDown')
(0, state_1.setView)(state_1.state, v.re, v.im + d, v.w);
applyMainView();
});
}
});
// ---- debug / probe interface ----
window.fractal = {
iterationsAt: (re, im) => state_1.state.mode === 'julia'
? (0, math_1.juliaIter)(re, im, state_1.state.cRe, state_1.state.cIm, (0, state_1.effIters)(state_1.state))
: (0, math_1.mandelIter)(re, im, (0, state_1.effIters)(state_1.state)),
setView: (o) => {
const w = o.w !== undefined ? o.w : o.scale !== undefined ? state_1.state.w0 / o.scale : state_1.state.view.w;
(0, state_1.setView)(state_1.state, o.re, o.im, w);
applyMainView();
},
setJulia: (cRe, cIm) => {
setMode('julia', { re: cRe, im: cIm });
},
setMandelbrot: () => {
setMode('mandelbrot');
},
setPalette: (i) => {
state_1.state.palette = Math.max(0, Math.min(consts_1.PALETTES.length - 1, i | 0));
recolorAll();
},
setPaletteOffset: (o) => {
state_1.state.paletteOffset = o | 0;
recolorAll();
},
setDrift: (on) => {
state_1.state.drift = !!on;
if (toolbar)
toolbar.drift.checked = on;
lastDrift = performance.now();
},
state: state_1.state,
palettes: consts_1.PALETTES.map((p) => p.name),
save: () => {
(0, state_1.writeHash)(state_1.state);
return location.hash;
},
load: () => {
const ok = (0, state_1.readHash)(state_1.state);
syncModeUI();
applyMainView();
recolorAll();
return ok;
},
bookmarks: () => state_1.state.bookmarks.map((b) => b.name),
bookmark: (name) => {
(0, state_1.addBookmark)(state_1.state, name);
refreshBookmarkList();
return state_1.state.bookmarks.length - 1;
},
restoreBookmark: (i) => {
const bm = state_1.state.bookmarks[i];
if (!bm)
return false;
(0, state_1.restoreBookmark)(state_1.state, bm);
syncModeUI();
applyMainView();
recolorAll();
return true;
},
reset: doReset,
finish: () => {
(0, renderer_1.finishSync)(mainRenderer);
if (juliaRenderer)
(0, renderer_1.finishSync)(juliaRenderer);
return true;
},
version: () => mainRenderer.version,
};
// ---- initial render request ----
applyMainView();
syncModeUI();
(0, ui_1.syncPaletteChips)(toolbar, state_1.state.palette);
(0, ui_1.syncIterLabel)(toolbar, state_1.state);
// ---- main loop ----
const loop = (t) => {
requestAnimationFrame(loop);
if (lastFrameT === 0)
lastFrameT = t;
const dt = Math.max(0.001, (t - lastFrameT) / 1000);
lastFrameT = t;
fps += (1 / dt - fps) * 0.06;
// animated palette drift (colorize-only, no iteration recompute)
if (state_1.state.drift) {
const now = performance.now();
const step = (now - lastDrift) * (consts_1.DRIFT_SPEED / 1000);
if (step >= 1) {
lastDrift = now;
state_1.state.paletteOffset = (state_1.state.paletteOffset + Math.round(step)) & 255;
if (toolbar)
toolbar.offset.value = String(state_1.state.paletteOffset);
recolorAll();
}
}
mainRenderer.process(9);
if (juliaRenderer)
juliaRenderer.process(5);
// HUD + hash at ~4 Hz
const now = performance.now();
if (now - lastHudT > 250) {
lastHudT = now;
if (hud) {
(0, ui_1.updateHUD)(hud, state_1.state, fps, (0, state_1.effIters)(state_1.state), (0, math_1.autoIterations)((0, math_1.magOf)(state_1.state.view, state_1.state.w0)));
hud.root.classList.toggle('idle', now - lastInteraction > 5000);
}
const mag = (0, math_1.magOf)(state_1.state.view, state_1.state.w0);
if (mag > consts_1.WARN_MAG && now - lastWarn > 6000) {
lastWarn = now;
toastMsg('near double-precision limit — structure may degrade past ×2e13');
}
if (viewChanged) {
viewChanged = false;
scheduleHash();
}
}
overlay.draw();
};
requestAnimationFrame(loop);
}
start();
};
// ── module: src/consts.ts ──
__mods["src/consts.ts"] = function (exports, require, module) {
"use strict";
// ============================================================================
// TUNABLE CONSTANTS — adjust these to taste
// ============================================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.DRIFT_SPEED = exports.PALETTES = exports.SMOOTH_LOG = exports.ESCAPE_R2 = exports.ZOOM_PREVIEW_MAX = exports.ZOOM_PREVIEW_MIN = exports.REFIN_CHUNK_ROWS = exports.COARSE_MIN_F = exports.COARSE_MAX_F = exports.COARSE_TARGET_ITERS = exports.WARN_MAG = exports.MAX_MAG = exports.KEY_PAN_FRAC = exports.KEY_ZOOM_FACTOR = exports.WHEEL_ZOOM_FACTOR = exports.ASPECT_CORRECTION = exports.INITIAL_VIEW = exports.DEFAULT_ITERS = exports.MAX_ITERS = exports.ITERS_PER_DECADE = exports.BASE_ITERS = void 0;
exports.bakePalette = bakePalette;
/** Base iteration cap at the default view width (w0). */
exports.BASE_ITERS = 600;
/** Iterations added per decade of magnification. */
exports.ITERS_PER_DECADE = 38;
/** Max iteration cap (also the "auto" slider's upper bound). */
exports.MAX_ITERS = 4000;
/** Default iteration slider value (0 = auto, derived from zoom). */
exports.DEFAULT_ITERS = 0;
/** Initial view: world width in complex-plane units, centered here. */
exports.INITIAL_VIEW = { re: -0.58, im: 0.0, w0: 3.2 };
/** Aspect correction applied to w0 (canvas is wider than 3:2). */
exports.ASPECT_CORRECTION = 1.15;
/** Wheel zoom: scale factor per wheel "step" (deltaY 100). */
exports.WHEEL_ZOOM_FACTOR = 1.15;
/** Keyboard zoom step per press. */
exports.KEY_ZOOM_FACTOR = 1.4;
/** Arrow-key pan speed (fraction of view width per press). */
exports.KEY_PAN_FRAC = 0.05;
/** Maximum magnification (width w0/currentW) — double precision degrades beyond ~1e13. */
exports.MAX_MAG = 2e13;
/** Warn (amber HUD + toast) when this magnitude is exceeded (approaching the cap). */
exports.WARN_MAG = 1e13;
/**
* Progressive refinement: an adaptive coarse pass (bounded below by
* COARSE_TARGET_ITERS total escape iterations, capped at COARSE_MAX_F of full
* resolution) paints the frame in a few ms, then full resolution is refined
* in REFIN_CHUNK_ROWS-row chunks.
*/
exports.COARSE_TARGET_ITERS = 5e6; // coarse pass budget (escape iters)
exports.COARSE_MAX_F = 0.25; // coarse is at most 1/4 resolution
exports.COARSE_MIN_F = 0.04; // ... but never coarser than this
exports.REFIN_CHUNK_ROWS = 8; // full-res refinement chunk height (rows)
/** Zoom ratios for which the previous frame makes a usable instant preview. */
exports.ZOOM_PREVIEW_MIN = 0.45;
exports.ZOOM_PREVIEW_MAX = 2.2;
/**
* Escape radius squared: |z|^2 > 4 (i.e. |z| > 2) is the standard textbook
* bound — once |z| > 2 the orbit is guaranteed to escape to infinity. Using
* 4 (not a larger radius) keeps escape detection tight so fast exterior points
* (e.g. c = 2) escape within a couple of iterations, and makes the smooth
* fractional count well-conditioned.
*/
exports.ESCAPE_R2 = 4;
exports.SMOOTH_LOG = Math.log(2);
exports.PALETTES = [
{
name: 'Aurora',
cyclic: true,
stops: [
9, 6, 36, 32, 10, 84, 15, 52, 118, 16, 108, 168, 102, 154, 160,
252, 246, 200, 152, 154, 120, 14, 120, 136,
],
},
{
name: 'Fire',
cyclic: false,
stops: [
0, 0, 0, 40, 8, 2, 104, 22, 6, 196, 62, 8, 255, 140, 20,
255, 214, 88, 255, 252, 224,
],
},
{
name: 'Ocean',
cyclic: false,
stops: [
2, 8, 22, 6, 32, 72, 8, 74, 128, 10, 120, 178, 44, 178, 210,
150, 232, 236, 240, 255, 255,
],
},
{
name: 'Mono',
cyclic: false,
stops: [0, 0, 0, 64, 64, 64, 140, 140, 140, 255, 255, 255],
},
{
name: 'Psyche',
cyclic: true,
stops: [
255, 0, 110, 255, 170, 0, 255, 255, 0, 0, 255, 160, 0, 80, 255,
120, 0, 255, 255, 0, 110,
],
},
{
name: 'Glow',
cyclic: false,
stops: [
0, 0, 0, 28, 6, 60, 90, 24, 140, 150, 120, 230, 200, 190, 255,
255, 255, 255,
],
},
];
/** Pre-bake a lookup table (256 entries per channel) for one palette. */
function bakePalette(pal) {
const n = pal.stops.length / 3;
const table = new Uint8Array(256 * 3);
const stops = pal.stops;
for (let i = 0; i < 256; i++) {
const t = i / 255;
const scaled = t * (n - 1);
let s = Math.floor(scaled);
if (s > n - 2)
s = n - 2;
const f = scaled - s;
const o = i * 3;
const s0 = s * 3;
const s1 = s0 + 3;
table[o] = Math.round(stops[s0] + (stops[s1] - stops[s0]) * f);
table[o + 1] = Math.round(stops[s0 + 1] + (stops[s1 + 1] - stops[s0 + 1]) * f);
table[o + 2] = Math.round(stops[s0 + 2] + (stops[s1 + 2] - stops[s0 + 2]) * f);
}
return table;
}
/** Color drift cycle speed (table steps per second) for the animated offset. */
exports.DRIFT_SPEED = 6; // table steps / s
};
// ── module: src/math.ts ──
__mods["src/math.ts"] = function (exports, require, module) {
"use strict";
// ============================================================================
// ESCAPE-TIME MATH
//
// Allocation-free per pixel: no objects, no arrays, no closures created
// inside the iteration loops. Smooth (continuous) coloring uses
// n + 1 - log2( log(|z|) / log(2) ) ≈ n + 1 - log2(log|z|)
// at the moment of escape, which removes the banding of integer coloring.
// ============================================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.mandelIter = mandelIter;
exports.juliaIter = juliaIter;
exports.autoIterations = autoIterations;
exports.setupScanline = setupScanline;
exports.magOf = magOf;
exports.fmtMag = fmtMag;
exports.fmtCoord = fmtCoord;
const consts_1 = require("./consts");
/**
* Mandelbrot: iterate z = z^2 + c starting from z = 0.
* Returns 0 when the orbit stays bounded (interior / set member) after
* `maxIter` steps; otherwise the smooth (fractional) iteration count.
*/
const INV_L2 = 1 / Math.log(2);
function mandelIter(x0, y0, maxIter) {
let x = 0, y = 0;
for (let n = 0; n < maxIter; n++) {
const x2 = x * x;
const y2 = y * y;
if (x2 + y2 > consts_1.ESCAPE_R2) {
// smooth fractional escape value: n + 1 - log2(log|z|)
// (clamped to > 0 so far-field Julia points never read as "interior")
const l2 = Math.log(x2 + y2) * 0.5; // log|z|
const sm = n + 1 - Math.log(l2) * INV_L2;
return sm > 0 ? sm : 0.5;
}
y = 2 * x * y + y0;
x = x2 - y2 + x0;
}
return 0;
}
/**
* Julia: iterate z = z^2 + c starting from z = z0.
* Same return convention as mandelIter.
*/
function juliaIter(x0, y0, cRe, cIm, maxIter) {
let x = x0, y = y0;
for (let n = 0; n < maxIter; n++) {
const x2 = x * x;
const y2 = y * y;
if (x2 + y2 > consts_1.ESCAPE_R2) {
const l2 = Math.log(x2 + y2) * 0.5;
const sm = n + 1 - Math.log(l2) * INV_L2;
return sm > 0 ? sm : 0.5;
}
y = 2 * x * y + cIm;
x = x2 - y2 + cRe;
}
return 0;
}
/**
* Iteration limit scaled by zoom depth: more magnification -> more detail
* near the boundary. `mag` = world-width0 / current-world-width.
*/
function autoIterations(mag) {
if (mag <= 1)
return consts_1.BASE_ITERS;
const iters = consts_1.BASE_ITERS + consts_1.ITERS_PER_DECADE * Math.log10(mag);
return Math.min(consts_1.MAX_ITERS, Math.floor(iters));
}
/**
* Precompute the complex-plane coordinate of the first pixel of row 0 and the
* per-pixel / per-row steps, so the scan loop only does adds — no per-pixel
* multiply-by-view. x0 is the re of pixel (0,0), y0 the im of pixel (0,0).
*/
function setupScanline(v, pxw, pxh) {
const s = v.w / pxw;
const h = v.w * (pxh / pxw);
return {
x0: v.re - v.w * 0.5 + s * 0.5,
y0: v.im + h * 0.5 - (h / pxh) * 0.5,
stepX: s,
rowStepY: -(h / pxh),
};
}
function magOf(v, w0) {
return w0 / v.w;
}
/** Format a number for HUD display: 1.234e+09 style for magnitudes. */
function fmtMag(mag) {
if (mag < 1000)
return '×' + mag.toFixed(mag < 10 ? 2 : 1);
return '×' + mag.toExponential(3);
}
function fmtCoord(v) {
if (v === 0)
return '0.000000000';
const a = Math.abs(v);
if (a >= 1e4 || a < 1e-4)
return v.toExponential(5);
return v.toFixed(9);
}
};
// ── module: src/state.ts ──
__mods["src/state.ts"] = function (exports, require, module) {
"use strict";
// ============================================================================
// STATE — view, mode, history stack, bookmarks (localStorage), URL hash
// ============================================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.MAX_ITERS = exports.state = void 0;
exports.effIters = effIters;
exports.clampView = clampView;
exports.pushHistory = pushHistory;
exports.goBack = goBack;
exports.resetView = resetView;
exports.saveBookmarks = saveBookmarks;
exports.loadBookmarks = loadBookmarks;
exports.addBookmark = addBookmark;
exports.restoreBookmark = restoreBookmark;
exports.deleteBookmark = deleteBookmark;
exports.writeHash = writeHash;
exports.readHash = readHash;
exports.setView = setView;
const consts_1 = require("./consts");
Object.defineProperty(exports, "MAX_ITERS", { enumerable: true, get: function () { return consts_1.MAX_ITERS; } });
const math_1 = require("./math");
exports.state = {
view: { re: consts_1.INITIAL_VIEW.re, im: consts_1.INITIAL_VIEW.im, w: consts_1.INITIAL_VIEW.w0 * consts_1.ASPECT_CORRECTION },
w0: consts_1.INITIAL_VIEW.w0 * consts_1.ASPECT_CORRECTION,
mode: 'mandelbrot',
cRe: -0.743643887037151,
cIm: 0.13182590420531197,
itersOverride: consts_1.DEFAULT_ITERS,
palette: 0,
paletteOffset: 0,
drift: false,
juliaLive: true,
history: [],
bookmarks: [],
toast: null,
};
// ---------------------------------------------------------------------------
// derived helpers
// ---------------------------------------------------------------------------
function effIters(s) {
return s.itersOverride > 0 ? s.itersOverride : (0, math_1.autoIterations)((0, math_1.magOf)(s.view, s.w0));
}
function clampView(s, v) {
// enforce max magnification (double-precision cap) — center preserved,
// the view simply stops shrinking
const minW = s.w0 / consts_1.MAX_MAG;
if (v.w < minW)
v.w = minW;
if (v.w > s.w0 * 100)
v.w = s.w0 * 100;
}
// ---------------------------------------------------------------------------
// history (back button)
// ---------------------------------------------------------------------------
function pushHistory(s) {
s.history.push({
re: s.view.re, im: s.view.im, w: s.view.w,
mode: s.mode, cRe: s.cRe, cIm: s.cIm,
});
if (s.history.length > 64)
s.history.shift();
}
function goBack(s) {
const prev = s.history.pop();
if (prev) {
s.view = { re: prev.re, im: prev.im, w: prev.w };
s.mode = prev.mode;
s.cRe = prev.cRe;
s.cIm = prev.cIm;
}
}
function resetView(s) {
pushHistory(s);
s.view = { re: consts_1.INITIAL_VIEW.re, im: consts_1.INITIAL_VIEW.im, w: s.w0 };
}
// ---------------------------------------------------------------------------
// bookmarks — localStorage
// ---------------------------------------------------------------------------
const LS_KEY = 'fractal.explorer.bookmarks.v1';
function saveBookmarks(s) {
try {
localStorage.setItem(LS_KEY, JSON.stringify(s.bookmarks));
}
catch {
/* storage unavailable (file:// privacy mode) — ignore */
}
}
function loadBookmarks(s) {
try {
const raw = localStorage.getItem(LS_KEY);
if (!raw)
return;
const arr = JSON.parse(raw);
if (Array.isArray(arr)) {
s.bookmarks = arr.filter((b) => b && typeof b.name === 'string' && typeof b.w === 'number');
}
}
catch {
/* corrupt storage — ignore */
}
}
function addBookmark(s, name) {
s.bookmarks.push({
name,
time: Date.now(),
re: s.view.re,
im: s.view.im,
w: s.view.w,
iters: s.itersOverride,
palette: s.palette,
paletteOffset: s.paletteOffset,
mode: s.mode,
cRe: s.cRe,
cIm: s.cIm,
});
saveBookmarks(s);
}
function restoreBookmark(s, bm) {
pushHistory(s);
s.view = { re: bm.re, im: bm.im, w: bm.w };
s.itersOverride = typeof bm.iters === 'number' ? bm.iters : 0;
s.palette = typeof bm.palette === 'number' ? bm.palette : 0;
s.paletteOffset = typeof bm.paletteOffset === 'number' ? bm.paletteOffset : 0;
s.mode = bm.mode === 'julia' ? 'julia' : 'mandelbrot';
s.cRe = typeof bm.cRe === 'number' ? bm.cRe : s.cRe;
s.cIm = typeof bm.cIm === 'number' ? bm.cIm : s.cIm;
}
function deleteBookmark(s, index) {
s.bookmarks.splice(index, 1);
saveBookmarks(s);
}
// ---------------------------------------------------------------------------
// URL hash state (shareable links)
// #m=<mode>&re=&im=&w=&it=&pal=&off=&cre=&cim=
// ---------------------------------------------------------------------------
function writeHash(s) {
// String(n) gives the exact round-trippable representation of a double,
// so a shared link reproduces the view bit-for-bit.
const h = (s.mode === 'julia' ? 'j' : 'm') +
`|${s.view.re}|${s.view.im}|${s.view.w}` +
`|${s.itersOverride}|${s.palette}|${Math.round(s.paletteOffset)}` +
`|${s.cRe}|${s.cIm}`;
try {
history.replaceState(null, '', '#' + h);
}
catch {
// file:// and other contexts block replaceState — direct assignment works
try {
location.hash = h;
}
catch {
/* no history access at all — ignore */
}
}
}
function readHash(s) {
let h = '';
try {
h = location.hash.slice(1);
}
catch {
return false;
}
if (!h)
return false;
const parts = h.split('|');
if (parts.length < 5)
return false;
const mode = parts[0] === 'j' ? 'julia' : 'mandelbrot';
const nums = parts.slice(1).map(Number);
if (nums.length < 4 || nums.some((n) => !isFinite(n)))
return false;
if (mode === 'julia' && parts.length < 9)
return false;
s.view = { re: nums[0], im: nums[1], w: nums[2] };
s.itersOverride = nums[3] || 0;
s.palette = nums[4] | 0;
s.paletteOffset = nums[5] | 0;
if (mode === 'julia') {
s.cRe = nums[6];
s.cIm = nums[7];
}
clampView(s, s.view);
return true;
}
function setView(s, re, im, w) {
s.view.re = re;
s.view.im = im;
s.view.w = Math.max(w, 1e-16);
clampView(s, s.view);
}
};
// ── module: src/renderer.ts ──
__mods["src/renderer.ts"] = function (exports, require, module) {
"use strict";
// ============================================================================
// RENDERER — Canvas2D + ImageData/typed-array pipeline
//
// compute escape iterations -> Float32 buffer (value <= 0 == interior)
// colorize (palette LUT) -> RGBA Uint8ClampedArray (ImageData)
// blit -> putImageData / scaled drawImage
//
// Responsiveness strategy (frame-by-frame, never a multi-second freeze):
//
// * PAN -> instant: the previous full-res frame is blitted shifted
// (drawImage of the kept copy canvas). Refinement bands
// then replace it row-chunk by row-chunk.
// * SMALL ZOOM -> instant: the previous frame is blitted scaled about the
// cursor (slightly soft; refinement sharpens it).
// * BIG ZOOM / FIRST -> adaptive coarse pass: coarse resolution is chosen
// so the coarse pass itself costs a bounded number of
// escape iterations (a few ms), then refined.
//
// Any view/c change bumps the render version; all in-flight work is dropped
// and the new base is painted completely before refinement, so no ghost
// tiles can survive. Row-chunk rendering resumes across frames within a time
// budget, so each rAF callback stays short.
//
// The per-pixel iteration loop (math.ts) is allocation-free; the renderer
// pre-allocates every buffer on resize and allocates nothing per frame.
// ============================================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.FractalRenderer = void 0;
exports.colorizeBuffer = colorizeBuffer;
exports.colorizeRow = colorizeRow;
exports.finishSync = finishSync;
const consts_1 = require("./consts");
const math_1 = require("./math");
/**
* Colorize a Float32 iteration buffer into an RGBA Uint8ClampedArray.
* idx = smooth * scale + offset; wrapped for cyclic palettes, clamped for
* linear ones. Interior (v <= 0) is rendered black.
*/
function colorizeBuffer(iters, out, lut, offset, maxIter) {
const scaleCyc = 255 / Math.max(1, Math.log2(maxIter));
const scaleLin = (255 / Math.max(1, maxIter)) * 1.5;
const table = lut.table;
const cyclic = lut.cyclic;
const n = iters.length;
for (let i = 0, p = 0; i < n; i++, p += 4) {
const v = iters[i];
if (v <= 0) {
out[p] = 0;
out[p + 1] = 0;
out[p + 2] = 0;
out[p + 3] = 255;
continue;
}
let idx = v * (cyclic ? scaleCyc : scaleLin) + offset;
if (cyclic) {
idx = ((idx % 256) + 256) % 256;
}
else if (idx >= 255) {
idx = 255;
}
else if (idx < 0) {
idx = 0;
}
const t = (idx | 0) * 3;
out[p] = table[t];
out[p + 1] = table[t + 1];
out[p + 2] = table[t + 2];
out[p + 3] = 255;
}
}
/** Colorize one row of `iters` (starting at i0, n values) into out at o4. */
function colorizeRow(iters, i0, n, out, o4, lut, offset, maxIter) {
const scaleCyc = 255 / Math.max(1, Math.log2(maxIter));
const scaleLin = (255 / Math.max(1, maxIter)) * 1.5;
const table = lut.table;
const cyclic = lut.cyclic;
for (let i = 0, p = o4; i < n; i++, p += 4) {
const v = iters[i0 + i];
if (v <= 0) {
out[p] = 0;
out[p + 1] = 0;
out[p + 2] = 0;
out[p + 3] = 255;
continue;
}
let idx = v * (cyclic ? scaleCyc : scaleLin) + offset;
if (cyclic) {
idx = ((idx % 256) + 256) % 256;
}
else if (idx >= 255) {
idx = 255;
}
else if (idx < 0) {
idx = 0;
}
const t = (idx | 0) * 3;
out[p] = table[t];
out[p + 1] = table[t + 1];
out[p + 2] = table[t + 2];
out[p + 3] = 255;
}
}
/** Rows per refinement chunk (a chunk is the atomic render+blit unit). */
const CHUNK = consts_1.REFIN_CHUNK_ROWS;
class FractalRenderer {
constructor(canvas, kind) {
// full-res buffers
this.W = 0;
this.H = 0;
this.dpr = 1;
this.iter = new Float32Array(0); // master iteration field
this.scratchFull = null;
this.chunkIter = new Float32Array(0);
this.chunkImg = null;
// coarse buffers (max 1/4 res; used region may be smaller)
this.Lo = 0;
this.Ho = 0;
this.iterLo = new Float32Array(0);
this.imageLo = null;
this.loW = 0;
this.loH = 0;
// render parameters
this.view = { re: 0, im: 0, w: 3 };
this.cRe = 0;
this.cIm = 0;
this.maxIter = 600;
this.lut = { table: new Uint8Array(768), cyclic: false };
this.offset = 0;
// render progress
this.version = 0;
this.baseDirty = true; // a base (coarse or preview) must be painted
this.baseType = 'lo';
this.row = 0; // full-res rows completed
this.fullDone = false;
this.hasFull = false; // fullCopy holds a complete current frame
this.canvas = canvas;
this.kind = kind;
const ctx = canvas.getContext('2d');
if (!ctx)
throw new Error('2d context unavailable');
this.ctx = ctx;
this.loCanvas = document.createElement('canvas');
const lctx = this.loCanvas.getContext('2d');
if (!lctx)
throw new Error('2d context unavailable (lo)');
this.loCtx = lctx;
this.fullCopy = document.createElement('canvas');
const fctx = this.fullCopy.getContext('2d');
if (!fctx)
throw new Error('2d context unavailable (copy)');
this.fullCopyCtx = fctx;
if (kind === 'julia') {
this.cRe = -0.743643887037151;
this.cIm = 0.13182590420531197;
}
}
/**
* Change what to draw. Cancels in-flight work and, when the previous
* complete frame is reusable, blits a cheap preview (pan shift / zoom
* scale) so the screen updates immediately.
* anchorCss: optional css-pixel anchor for the zoom preview (cursor).
*/
setView(v, maxIter, lut, offset, anchorCss) {
const paramsSame = maxIter === this.maxIter && lut === this.lut && offset === this.offset;
const reusable = this.hasFull && this.W > 0 && v.w > 0 && paramsSame;
let baseType = 'lo';
if (reusable) {
const ratio = this.view.w / v.w; // >1 = zooming in
if (Math.abs(ratio - 1) < 1e-9) {
// pure pan: shift the previous frame
const s = this.W / v.w; // canvas px per complex unit
const dx = (this.view.re - v.re) * s;
const dy = (v.im - this.view.im) * s;
this.ctx.imageSmoothingEnabled = false;
this.ctx.clearRect(0, 0, this.W, this.H);
this.ctx.drawImage(this.fullCopy, Math.round(dx), Math.round(dy));
baseType = 'pan';
}
else if (ratio >= consts_1.ZOOM_PREVIEW_MIN && ratio <= consts_1.ZOOM_PREVIEW_MAX) {
// small zoom: scale the previous frame about the anchor
const cssW = this.W / this.dpr;
const cssH = this.H / this.dpr;
const ax = (anchorCss ? anchorCss.x : cssW * 0.5) * this.dpr;
const ay = (anchorCss ? anchorCss.y : cssH * 0.5) * this.dpr;
const ctx = this.ctx;
ctx.save();
ctx.imageSmoothingEnabled = true;
ctx.setTransform(ratio, 0, 0, ratio, ax * (1 - ratio), ay * (1 - ratio));
ctx.drawImage(this.fullCopy, 0, 0);
ctx.restore();
baseType = 'zoom';
}
}
this.view = { re: v.re, im: v.im, w: v.w };
this.maxIter = maxIter;
this.lut = lut;
this.offset = offset;
this.baseType = baseType;
this.baseDirty = baseType === 'lo'; // pan/zoom previews are already painted
this.row = 0;
this.fullDone = false;
this.hasFull = false;
this.version++;
}
setKind(k) {
if (k === this.kind)
return;
this.kind = k;
this.hasFull = false;
this.baseType = 'lo';
this.baseDirty = true;
this.row = 0;
this.fullDone = false;
this.version++;
}
setJuliaC(cRe, cIm) {
if (this.kind !== 'julia')
return;
if (cRe === this.cRe && cIm === this.cIm)
return;
this.cRe = cRe;
this.cIm = cIm;
this.hasFull = false;
this.baseType = 'lo';
this.baseDirty = true;
this.row = 0;
this.fullDone = false;
this.version++;
}
/** Recolorize from existing iteration buffers — no escape-time recompute. */
recolorize(lut, offset) {
this.lut = lut;
this.offset = offset;
if (this.W === 0 || this.baseDirty)
return;
if (!this.fullDone && this.baseType === 'lo' && this.imageLo) {
// coarse base on screen: recolorize it
for (let r = 0; r < this.loH; r++) {
colorizeRow(this.iterLo, r * this.loW, this.loW, this.imageLo.data, r * this.loW * 4, this.lut, this.offset, this.maxIter);
}
this.loCtx.putImageData(this.imageLo, 0, 0);
this.blitLo();
}
else if (this.fullDone && this.scratchFull) {
colorizeBuffer(this.iter, this.scratchFull.data, this.lut, this.offset, this.maxIter);
this.ctx.putImageData(this.scratchFull, 0, 0);
this.fullCopyCtx.putImageData(this.scratchFull, 0, 0);
this.hasFull = true;
}
else if (this.row > 0 && this.scratchFull) {
// partial: refresh the already-computed rows (dirty-rect blit)
for (let r = 0; r < this.row; r++) {
colorizeRow(this.iter, r * this.W, this.W, this.scratchFull.data, r * this.W * 4, this.lut, this.offset, this.maxIter);
}
this.ctx.putImageData(this.scratchFull, 0, 0, 0, 0, this.W, this.row);
}
}
blitLo() {
this.ctx.imageSmoothingEnabled = true;
this.ctx.drawImage(this.loCanvas, 0, 0, this.loW, this.loH, 0, 0, this.W, this.H);
}
resize(cssW, cssH, dpr) {
const W = Math.max(2, Math.round(cssW * dpr));
const H = Math.max(2, Math.round(cssH * dpr));
if (W === this.W && H === this.H && dpr === this.dpr)
return;
this.W = W;
this.H = H;
this.dpr = dpr;
this.canvas.width = W;
this.canvas.height = H;
this.iter = new Float32Array(W * H);
this.scratchFull = new ImageData(W, H);
this.chunkIter = new Float32Array(CHUNK * W);
this.chunkImg = new ImageData(W, CHUNK);
this.Lo = Math.max(8, Math.round(W * consts_1.COARSE_MAX_F));
this.Ho = Math.max(8, Math.round(H * consts_1.COARSE_MAX_F));
this.loCanvas.width = this.Lo;
this.loCanvas.height = this.Ho;
this.fullCopy.width = W;
this.fullCopy.height = H;
this.iterLo = new Float32Array(this.Lo * this.Ho);
this.imageLo = new ImageData(this.Lo, this.Ho);
this.hasFull = false;
this.baseType = 'lo';
this.baseDirty = true;
this.row = 0;
this.fullDone = false;
this.version++;
}
/** Advance the render within a time budget. Returns true when done. */
process(budgetMs) {
if (this.W === 0)
return true;
if (this.baseDirty) {
this.renderLoPass();
this.blitLo();
this.baseDirty = false;
// (coarse pass is bounded by COARSE_TARGET_ITERS; fall through to refine)
}
const start = performance.now();
while (!this.fullDone) {
const y = this.row;
const end = Math.min(this.H, y + CHUNK);
this.renderChunk(y, end);
this.row = end;
if (this.row >= this.H) {
this.fullDone = true;
if (this.scratchFull) {
this.fullCopyCtx.putImageData(this.scratchFull, 0, 0);
this.hasFull = true;
}
}
if (performance.now() - start > budgetMs)
return false;
}
return true;
}
// ---- adaptive coarse pass -----------------------------------------------
renderLoPass() {
const v = this.view;
const work = this.W * this.H * this.maxIter;
let f = Math.sqrt(consts_1.COARSE_TARGET_ITERS / Math.max(1, work));
if (f > consts_1.COARSE_MAX_F)
f = consts_1.COARSE_MAX_F;
if (f < consts_1.COARSE_MIN_F)
f = consts_1.COARSE_MIN_F;
const Wf = Math.max(8, Math.min(this.Lo, Math.round(this.W * f)));
const Hf = Math.max(8, Math.min(this.Ho, Math.round(this.H * f)));
this.loW = Wf;
this.loH = Hf;
const s = v.w / Wf;
const h = v.w * (Hf / Wf);
let x0 = v.re - v.w * 0.5 + s * 0.5;
let y0 = v.im + h * 0.5 - (h / Hf) * 0.5;
const rowStepY = -(h / Hf);
const it = this.iterLo;
const mi = this.maxIter;
const kr = this.kind;
const cre = this.cRe, cim = this.cIm;
for (let py = 0; py < Hf; py++) {
const y = y0;
let x = x0;
let o = py * Wf;
if (kr === 'mandelbrot') {
for (let px = 0; px < Wf; px++) {
it[o + px] = (0, math_1.mandelIter)(x, y, mi);
x += s;
}
}
else {
for (let px = 0; px < Wf; px++) {
it[o + px] = (0, math_1.juliaIter)(x, y, cre, cim, mi);
x += s;
}
}
y0 += rowStepY;
}
const img = this.imageLo;
for (let r = 0; r < Hf; r++) {
colorizeRow(it, r * Wf, Wf, img.data, r * Wf * 4, this.lut, this.offset, mi);
}
// putImageData of the full buffer is fine: blitLo only draws the used
// region (loW x loH), so stale pixels outside are never displayed
this.loCtx.putImageData(img, 0, 0);
}
// ---- full-res chunk: rows y..end (atomic render + blit unit) -------------
renderChunk(y, end) {
const v = this.view;
const W = this.W, H = this.H;
const s = v.w / W;
const h = v.w * (H / W);
const it = this.chunkIter;
const mi = this.maxIter;
const kr = this.kind;
const cre = this.cRe, cim = this.cIm;
const rows = end - y;
for (let py = y; py < end; py++) {
const yy = v.im + h * 0.5 - (py + 0.5) * (h / H);
let x = v.re - v.w * 0.5 + s * 0.5;
let o = (py - y) * W;
if (kr === 'mandelbrot') {
for (let px = 0; px < W; px++) {
it[o + px] = (0, math_1.mandelIter)(x, yy, mi);
x += s;
}
}
else {
for (let px = 0; px < W; px++) {
it[o + px] = (0, math_1.juliaIter)(x, yy, cre, cim, mi);
x += s;
}
}
}
const img = this.chunkImg;
for (let r = 0; r < rows; r++) {
colorizeRow(it, r * W, W, img.data, r * W * 4, this.lut, this.offset, mi);
}
this.ctx.putImageData(img, 0, y, 0, 0, W, rows);
// stash the chunk's iteration values in the master buffer
const master = this.iter;
for (let r = 0; r < rows; r++) {
master.set(it.subarray(r * W, (r + 1) * W), (y + r) * W);
}
// keep the scratch master in sync for recolorize
if (this.scratchFull) {
for (let r = 0; r < rows; r++) {
this.scratchFull.data.set(img.data.subarray(r * W * 4, (r + 1) * W * 4), (y + r) * W * 4);
}
}
}
}
exports.FractalRenderer = FractalRenderer;
/** Synchronously finish all passes (tests / debug interface). */
function finishSync(r) {
let n = 0;
while (!r.process(1e9) && n++ < 512) {
/* keep going */
}
}
};
// ── module: src/ui.ts ──
__mods["src/ui.ts"] = function (exports, require, module) {
"use strict";
// ============================================================================
// UI CONSTRUCTION — DOM building (elements created here, wired in main.ts)
// Glassy HUD, toolbar with palette swatch chips, Julia pane, toast, help bar.
// ============================================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.paletteGradient = paletteGradient;
exports.buildHUD = buildHUD;
exports.buildToolbar = buildToolbar;
exports.buildJuliaPane = buildJuliaPane;
exports.buildToast = buildToast;
exports.buildHelp = buildHelp;
exports.updateHUD = updateHUD;
exports.syncIterLabel = syncIterLabel;
exports.syncPaletteChips = syncPaletteChips;
exports.fillBookmarkList = fillBookmarkList;
const consts_1 = require("./consts");
const math_1 = require("./math");
function el(tag, cls, text) {
const e = document.createElement(tag);
if (cls)
e.className = cls;
if (text !== undefined)
e.textContent = text;
return e;
}
/** Gradient CSS for a palette's swatch chip. */
function paletteGradient(i) {
const p = consts_1.PALETTES[i];
const n = p.stops.length / 3;
const parts = [];
for (let s = 0; s < n; s++) {
const pct = Math.round((s / (n - 1)) * 100);
parts.push(`rgb(${p.stops[s * 3]},${p.stops[s * 3 + 1]},${p.stops[s * 3 + 2]}) ${pct}%`);
}
return `linear-gradient(90deg, ${parts.join(', ')})`;
}
function buildHUD() {
const root = el('div', 'hud');
const mag = el('div', 'hud-mag');
const coords = el('div', 'hud-sub');
const iter = el('div', 'hud-iter');
const fps = el('div', 'hud-sub');
const actions = el('div', 'hud-actions');
const back = el('button', 'hud-btn', '⟲ back');
const reset = el('button', 'hud-btn', '⌂ reset');
const juliaBtn = el('button', 'hud-btn', 'J · julia');
back.title = 'Zoom back (history)';
reset.title = 'Reset to default view';
juliaBtn.title = 'Toggle Julia view (J)';
actions.append(back, reset, juliaBtn);
root.append(mag, coords, iter, fps, actions);
return { root, mag, coords, iter, fps, back, reset, juliaBtn };
}
function buildToolbar() {
const root = el('div', 'toolbar');
const chips = consts_1.PALETTES.map((p, i) => {
const b = el('button', 'chip');
b.style.background = paletteGradient(i);
b.title = p.name;
b.setAttribute('aria-label', 'palette ' + p.name);
return b;
});
const offWrap = el('label', 'ctl');
const offLbl = el('span', 'ctl-lbl', 'color');
const offset = el('input');
offset.type = 'range';
offset.min = '0';
offset.max = '255';
offset.value = '0';
offset.step = '1';
offWrap.append(offLbl, offset);
const drift = el('input');
drift.type = 'checkbox';
drift.id = 'drift-chk';
const driftWrap = el('label', 'ctl chk', 'drift');
driftWrap.append(drift);
const itWrap = el('label', 'ctl');
const itLbl = el('span', 'ctl-lbl', 'iterations');
const iters = el('input');
iters.type = 'range';
iters.min = '0';
iters.max = String(consts_1.MAX_ITERS);
iters.value = '0';
iters.step = '10';
const itersVal = el('span', 'ctl-val', 'auto');
itWrap.append(itLbl, iters, itersVal);
const live = el('input');
live.type = 'checkbox';
live.checked = true;
live.id = 'live-chk';
const liveWrap = el('label', 'ctl chk', 'live julia');
liveWrap.append(live);
const sep = el('span', 'sep');
const bmSave = el('button', 'btn', '+ bookmark');
const bmList = el('select');
bmList.className = 'btn sel';
const ph = document.createElement('option');
ph.value = '';
ph.textContent = 'bookmarks…';
bmList.append(ph);
const bmDel = el('button', 'btn', 'del');
bmDel.disabled = true;
const copyLink = el('button', 'btn', 'copy link');
root.append(...chips, sep, offWrap, driftWrap, itWrap, liveWrap, sep, bmSave, bmList, bmDel, copyLink);
return {
root, chips, offset, drift, iters, itersVal, live,
bmSave, bmList, bmDel, copyLink,
};
}
function buildJuliaPane() {
const root = el('aside', 'julia-pane');
const head = el('div', 'jp-head');
const title = el('span', 'jp-title', 'JULIA');
const cLabel = el('span', 'jp-c');
const close = el('button', 'jp-close', '×');
close.title = 'Close Julia pane';
head.append(title, cLabel, close);
const canvas = el('canvas', 'jp-canvas');
const hint = el('span', 'jp-hint', 'hover the Mandelbrot to move c · click sets c');
root.append(head, canvas, hint);
return { root, canvas, close, cLabel, hint };
}
// ---------------------------------------------------------------------------
function buildToast() {
return el('div', 'toast');
}
function buildHelp() {
const h = el('div', 'help');
h.innerHTML =
'<b>drag</b> zoom-rect · <b>drag</b>(pan mode) / arrows pan · ' +
'<b>wheel</b> zoom · <b>dbl-click</b> / <b>J</b> julia · ' +
'<b>+</b>/<b>−</b> zoom · <b>R</b> reset';
return h;
}
// ---------------------------------------------------------------------------
// HUD refresh (called a few times per second, not every frame)
// ---------------------------------------------------------------------------
function updateHUD(h, s, fps, effIter, autoIter) {
const mag = (0, math_1.magOf)(s.view, s.w0);
h.mag.textContent = (0, math_1.fmtMag)(mag);
h.mag.classList.toggle('warn', mag > consts_1.WARN_MAG);
h.coords.textContent = `re ${(0, math_1.fmtCoord)(s.view.re)} im ${(0, math_1.fmtCoord)(s.view.im)}`;
h.iter.textContent = `iter ${effIter}` + (s.itersOverride === 0 ? ` (auto ${autoIter})` : '');
h.fps.textContent = `${Math.round(fps)} fps · ${s.mode === 'julia' ? 'julia' : 'mandelbrot'}` +
(s.mode === 'julia' ? ` c=(${(0, math_1.fmtCoord)(s.cRe)}, ${(0, math_1.fmtCoord)(s.cIm)})` : '');
}
/** Keep the iterations slider label in sync. */
function syncIterLabel(t, s) {
const mag = (0, math_1.magOf)(s.view, s.w0);
const auto = s.itersOverride === 0;
t.itersVal.textContent = auto ? `auto ${(0, math_1.autoIterations)(mag)}` : String(s.itersOverride);
t.iters.value = String(s.itersOverride);
}
function syncPaletteChips(t, selected) {
t.chips.forEach((c, i) => c.classList.toggle('on', i === selected));
}
function fillBookmarkList(sel, names, current) {
sel.innerHTML = '';
const ph = document.createElement('option');
ph.value = '';
ph.textContent = 'bookmarks…';
sel.append(ph);
names.forEach((n, i) => {
const o = document.createElement('option');
o.value = String(i);
o.textContent = n;
sel.append(o);
});
if (current >= 0 && current < names.length)
sel.value = String(current);
}
};
// ── module: src/input.ts ──
__mods["src/input.ts"] = function (exports, require, module) {
"use strict";
// ============================================================================
// INPUT — pointer & wheel navigation for a fractal canvas
//
// plain drag : rectangle zoom (live overlay reported via onRect)
// shift+drag : pan
// wheel : smooth zoom toward cursor (pinned at the precision cap)
// dbl-click : 3× zoom toward cursor
// click : onPick (complex point under cursor)
// hover : onHover (complex point + css coords)
// ============================================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.NavInput = void 0;
const consts_1 = require("./consts");
const math_1 = require("./math");
class NavInput {
constructor(t) {
this.mode = 'none';
this.sx = 0;
this.sy = 0;
this.cx = 0;
this.cy = 0;
this.startView = { re: 0, im: 0, w: 1 };
this.pushed = false;
this.lastHist = 0;
this.onDown = (e) => {
if (e.button !== 0)
return;
const [x, y] = this.px(e);
this.mode = e.shiftKey ? 'pan' : 'rect';
this.sx = x;
this.sy = y;
this.cx = x;
this.cy = y;
this.startView = { ...this.t.getView() };
this.pushed = false;
this.rect(false);
};
this.onMove = (e) => {
if (e.target !== this.t.canvas && this.mode !== 'none') {
// pointer left the canvas while dragging — keep going (window listener)
}
const [x, y] = this.px(e);
if (this.mode === 'none') {
// hover only while actually over the canvas
const r = this.t.canvas.getBoundingClientRect();
if (e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom) {
return;
}
const p = this.pointFromCss(x, y);
this.t.onHover?.(p.re, p.im, x, y);
return;
}
this.cx = x;
this.cy = y;
if (!this.pushed && Math.hypot(this.cx - this.sx, this.cy - this.sy) > 3) {
this.pushed = true;
const now = performance.now();
if (now - this.lastHist > 350) {
this.t.pushHistory();
this.lastHist = now;
}
}
if (this.mode === 'pan') {
const W = this.t.canvas.clientWidth || 1;
const s = this.startView.w / W;
this.t.commit({
re: this.startView.re - (this.cx - this.sx) * s,
im: this.startView.im + (this.cy - this.sy) * s,
w: this.startView.w,
});
}
else {
this.rect(true);
}
};
this.onUp = (e) => {
if (this.mode === 'none')
return;
const m = this.mode;
this.mode = 'none';
this.rect(false);
void e;
const dx = this.cx - this.sx;
const dy = this.cy - this.sy;
const dist = Math.hypot(dx, dy);
const W = this.t.canvas.clientWidth || 1;
const H = this.t.canvas.clientHeight || 1;
if (m === 'rect') {
if (dist <= 4) {
// plain click -> pick the point
const p = this.pointFromCss(this.cx, this.cy);
this.t.onPick?.(p.re, p.im);
return;
}
// ---- rectangle zoom: frame exactly the selected region ----
const x0 = Math.min(this.sx, this.cx);
const x1 = Math.max(this.sx, this.cx);
const y0 = Math.min(this.sy, this.cy);
const y1 = Math.max(this.sy, this.cy);
const v = this.startView;
const s = v.w / W;
const h = v.w * (H / W);
const reL = v.re - v.w * 0.5 + x0 * s;
const reR = v.re - v.w * 0.5 + x1 * s;
const imT = v.im + h * 0.5 - y0 * (h / H);
const imB = v.im + h * 0.5 - y1 * (h / H);
const rw = reR - reL;
const rh = imT - imB;
// fit the whole rectangle: width >= rw and height (w*H/W) >= rh
const w = Math.max(rw, rh * (W / H));
const re = (reL + reR) * 0.5;
const im = (imT + imB) * 0.5;
if (!this.pushed) {
const now = performance.now();
if (now - this.lastHist > 350) {
this.t.pushHistory();
this.lastHist = now;
}
}
this.t.commit({ re, im, w });
}
};
this.onCancel = () => {
this.mode = 'none';
this.rect(false);
};
this.onWheel = (e) => {
e.preventDefault();
const [x, y] = this.px(e);
const W = this.t.canvas.clientWidth || 1;
const H = this.t.canvas.clientHeight || 1;
const v = this.t.getView();
const k = Math.pow(consts_1.WHEEL_ZOOM_FACTOR, -e.deltaY / 100);
let nw = v.w / k;
const minW = this.t.w0 / consts_1.MAX_MAG;
if (nw < minW)
nw = minW; // precision cap — zoom pins, doesn't blow past
const p = this.pointFromCss(x, y);
const s = nw / W;
this.t.commit({
re: p.re - (x - W * 0.5) * s,
im: p.im + (y - H * 0.5) * s,
w: nw,
}, { x, y });
const now = performance.now();
if (now - this.lastHist > 500) {
this.t.pushHistory();
this.lastHist = now;
}
};
this.onDbl = (e) => {
const [x, y] = this.px(e);
const W = this.t.canvas.clientWidth || 1;
const H = this.t.canvas.clientHeight || 1;
const v = this.t.getView();
const p = this.pointFromCss(x, y);
const nw = Math.max(v.w / 3, this.t.w0 / consts_1.MAX_MAG);
const s = nw / W;
this.t.commit({
re: p.re - (x - W * 0.5) * s,
im: p.im + (y - H * 0.5) * s,
w: nw,
});
this.t.pushHistory();
};
this.t = t;
const c = t.canvas;
c.style.touchAction = 'none';
c.addEventListener('pointerdown', this.onDown);
window.addEventListener('pointermove', this.onMove);
window.addEventListener('pointerup', this.onUp);
c.addEventListener('pointercancel', this.onCancel);
c.addEventListener('wheel', this.onWheel, { passive: false });
c.addEventListener('dblclick', this.onDbl);
}
dispose() {
const c = this.t.canvas;
c.removeEventListener('pointerdown', this.onDown);
window.removeEventListener('pointermove', this.onMove);
window.removeEventListener('pointerup', this.onUp);
c.removeEventListener('pointercancel', this.onCancel);
c.removeEventListener('wheel', this.onWheel);
c.removeEventListener('dblclick', this.onDbl);
}
/** css pixel coordinates of a pointer event relative to the canvas. */
px(e) {
const r = this.t.canvas.getBoundingClientRect();
return [e.clientX - r.left, e.clientY - r.top];
}
rect(active) {
this.t.onRect?.(this.sx, this.sy, this.cx, this.cy, active);
}
/** Complex-plane point under a css pixel. */
pointFromCss(cssX, cssY) {
const W = this.t.canvas.clientWidth || 1;
const H = this.t.canvas.clientHeight || 1;
const v = this.t.getView();
const s = v.w / W;
return {
re: v.re + (cssX - W * 0.5) * s,
im: v.im - (cssY - H * 0.5) * s,
};
}
/** Magnification of the current view. */
currentMag() {
return (0, math_1.magOf)(this.t.getView(), this.t.w0);
}
}
exports.NavInput = NavInput;
};
// ── module: src/overlay.ts ──
__mods["src/overlay.ts"] = function (exports, require, module) {
"use strict";
// ============================================================================
// OVERLAY — zoom rectangle (inverted-color border), reticle crosshair, toast
// Drawn over the main canvas; never touches the iteration buffers.
// ============================================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.Toast = exports.Overlay = void 0;
class Overlay {
constructor(canvas) {
this.W = 0;
this.H = 0;
this.rect = {
x0: 0, y0: 0, x1: 0, y1: 0, active: false,
};
this.reticle = { x: 0, y: 0, show: false };
const ctx = canvas.getContext('2d');
if (!ctx)
throw new Error('2d context unavailable (overlay)');
this.ctx = ctx;
}
resize(cssW, cssH, dpr) {
const W = Math.max(1, Math.round(cssW * dpr));
const H = Math.max(1, Math.round(cssH * dpr));
if (W === this.W && H === this.H)
return;
this.W = W;
this.H = H;
this.ctx.canvas.width = W;
this.ctx.canvas.height = H;
this.ctx.clearRect(0, 0, W, H);
}
/** Clear everything. */
clear() {
this.rect.active = false;
this.reticle.show = false;
this.ctx.clearRect(0, 0, this.W, this.H);
}
/** Draw the current overlay state (call once per frame). */
draw() {
const ctx = this.ctx;
const dpr = this.W / Math.max(1, (this.ctx.canvas.clientWidth || 1));
ctx.clearRect(0, 0, this.W, this.H);
if (this.rect.active)
this.drawRect(dpr);
if (this.reticle.show)
this.drawReticle(dpr);
}
drawRect(dpr) {
const ctx = this.ctx;
const x0 = Math.min(this.rect.x0, this.rect.x1) * dpr;
const y0 = Math.min(this.rect.y0, this.rect.y1) * dpr;
const w = Math.abs(this.rect.x1 - this.rect.x0) * dpr;
const h = Math.abs(this.rect.y1 - this.rect.y0) * dpr;
if (w < 2 || h < 2)
return;
// inverted-color border: 1px white outline, 3px black halo just outside
ctx.save();
ctx.lineJoin = 'miter';
ctx.strokeStyle = 'rgba(0,0,0,0.9)';
ctx.lineWidth = 6 * dpr * 0.5;
ctx.strokeRect(x0 - 3 * dpr * 0.5, y0 - 3 * dpr * 0.5, w + 3 * dpr, h + 3 * dpr);
ctx.strokeStyle = 'rgba(255,255,255,0.95)';
ctx.lineWidth = 1.2 * dpr;
ctx.strokeRect(x0, y0, w, h);
// corner ticks
const t = 8 * dpr;
ctx.lineWidth = 1.6 * dpr;
ctx.strokeStyle = '#fff';
ctx.beginPath();
ctx.moveTo(x0, y0 + t);
ctx.lineTo(x0, y0);
ctx.lineTo(x0 + t, y0);
ctx.moveTo(x0 + w - t, y0);
ctx.lineTo(x0 + w, y0);
ctx.lineTo(x0 + w, y0 + t);
ctx.moveTo(x0 + w, y0 + h - t);
ctx.lineTo(x0 + w, y0 + h);
ctx.lineTo(x0 + w - t, y0 + h);
ctx.moveTo(x0 + t, y0 + h);
ctx.lineTo(x0, y0 + h);
ctx.lineTo(x0, y0 + h - t);
ctx.stroke();
ctx.restore();
}
drawReticle(dpr) {
const ctx = this.ctx;
const x = this.reticle.x * dpr;
const y = this.reticle.y * dpr;
const r = 14 * dpr;
ctx.save();
// soft halo
ctx.strokeStyle = 'rgba(255,255,255,0.35)';
ctx.lineWidth = 4 * dpr * 0.5;
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2);
ctx.stroke();
// crisp ring
ctx.strokeStyle = 'rgba(255,255,255,0.9)';
ctx.lineWidth = 1.2 * dpr;
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2);
ctx.stroke();
// crosshair arms
const a = r * 0.45;
ctx.beginPath();
ctx.moveTo(x - r - a, y);
ctx.lineTo(x - r + 2 * dpr, y);
ctx.moveTo(x + r - 2 * dpr, y);
ctx.lineTo(x + r + a, y);
ctx.moveTo(x, y - r - a);
ctx.lineTo(x, y - r + 2 * dpr);
ctx.moveTo(x, y + r - 2 * dpr);
ctx.lineTo(x, y + r + a);
ctx.stroke();
// center dot
ctx.fillStyle = 'rgba(255,255,255,0.95)';
ctx.beginPath();
ctx.arc(x, y, 1.6 * dpr, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
}
exports.Overlay = Overlay;
class Toast {
constructor(el) {
this.timer = null;
this.el = el;
}
show(msg, ms = 2600) {
this.el.textContent = msg;
this.el.classList.add('show');
if (this.timer !== null)
clearTimeout(this.timer);
this.timer = window.setTimeout(() => {
this.el.classList.remove('show');
this.timer = null;
}, ms);
}
}
exports.Toast = Toast;
};
// ── types: globals.d.ts ──
__mods["globals.d.ts"] = function (exports) {};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"unsloth/Qwen3.8-27B-GGUF","provider":"brigid","persona":"typescript","sessionId":"df21a283-c64a-4987-bf01-0e097472e70f","tokensIn":8355980,"tokensOut":279614,"tokensTotal":8635594,"cost":0,"turns":120,"toolCalls":140,"failedToolCalls":3,"timestamp":"2026-08-15T09:33:27.776Z"} -->