Fractal Explorer
Qwen 3.6 27B MTP GGUF UD Q5 K XL · 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 53,406 bytes · SHA-256 4d72a4826a94
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Fractal Explorer</title>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #000; font-family: 'Segoe UI', system-ui, sans-serif; }
#app { position: relative; width: 100%; height: 100%; }
#fractalCanvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; cursor: crosshair; }
#overlayCanvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; }
#overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; }
#hud {
position: absolute; top: 0; left: 0; right: 0; pointer-events: none; z-index: 10;
transition: opacity 0.4s ease;
}
#hud.hidden { opacity: 0; }
#hud-top {
display: flex; justify-content: space-between; padding: 12px 16px;
font-size: 13px; color: #fff; text-shadow: 0 1px 4px rgba(0,0,0,0.8);
pointer-events: none;
}
#hud-bottom {
position: absolute; bottom: 12px; left: 50%; transform: translateX(-50%);
display: flex; align-items: center; gap: 8px; padding: 8px 16px;
background: rgba(20,20,30,0.65); backdrop-filter: blur(12px);
border-radius: 10px; border: 1px solid rgba(255,255,255,0.1);
pointer-events: auto; flex-wrap: wrap; justify-content: center;
}
#hud-bottom button {
background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2);
color: #fff; padding: 4px 10px; border-radius: 6px; cursor: pointer; font-size: 14px;
transition: background 0.15s;
}
#hud-bottom button:hover { background: rgba(255,255,255,0.25); }
#hud-bottom button.active { background: rgba(100,150,255,0.4); border-color: rgba(100,150,255,0.6); }
#palette-bar { display: flex; gap: 4px; }
.palette-chip {
width: 24px; height: 24px; border-radius: 4px; cursor: pointer;
border: 2px solid transparent; transition: border-color 0.15s;
}
.palette-chip:hover, .palette-chip.active { border-color: #fff; }
#slider-row { display: flex; align-items: center; gap: 8px; font-size: 11px; color: #ccc; }
#slider-row label { display: flex; align-items: center; gap: 4px; white-space: nowrap; }
#slider-row input[type="range"] { width: 80px; height: 4px; accent-color: #6af; }
#fps-label { font-size: 11px; color: #aaa; margin-left: 8px; }
#bookmark-list {
position: absolute; bottom: 56px; left: 50%; transform: translateX(-50%);
background: rgba(20,20,30,0.8); backdrop-filter: blur(12px);
border-radius: 8px; border: 1px solid rgba(255,255,255,0.15);
padding: 6px; display: flex; flex-direction: column; gap: 4px;
pointer-events: auto; min-width: 180px;
}
#bookmark-list .bm-item {
display: flex; justify-content: space-between; align-items: center;
padding: 4px 8px; border-radius: 4px; cursor: pointer; font-size: 12px; color: #ddd;
}
#bookmark-list .bm-item:hover { background: rgba(255,255,255,0.1); }
#bookmark-list .bm-item button {
background: none; border: none; color: #f66; cursor: pointer; font-size: 14px; padding: 0 4px;
}
</style>
</head>
<body>
<canvas id="fractalCanvas"></canvas>
<div id="overlay"></div>
<div id="hud">
<div id="hud-top">
<div id="zoom-label">×1.0</div>
<div id="center-label">(0.000, 0.000)</div>
</div>
<div id="hud-bottom">
<button id="btn-reset" title="Reset view (R)">⟲</button>
<button id="btn-back" title="Zoom history back (Esc)">←</button>
<button id="btn-julia" title="Julia mode (J)">J</button>
<button id="btn-bookmark-save" title="Save bookmark (S)">★</button>
<div id="palette-bar"></div>
<div id="slider-row">
<label>Offset <input type="range" id="slider-offset" min="0" max="1" step="0.001" value="0"></label>
<label>Max Iters <input type="range" id="slider-iters" min="50" max="10000" step="50" value="256"></label>
</div>
<div id="bookmark-list"></div>
<div id="fps-label">60 fps</div>
</div>
</div>
<canvas id="overlayCanvas"></canvas>
<div id="julia-preview" style="position:absolute;bottom:80px;right:12px;width:200px;height:200px;background:rgba(0,0,0,0.7);border:1px solid rgba(255,255,255,0.2);border-radius:8px;overflow:hidden;pointer-events:none;display:none;">
<canvas id="julia-preview-canvas" width="200" height="200" style="width:100%;height:100%;"></canvas>
<div id="julia-preview-label" style="position:absolute;bottom:4px;left:0;right:0;text-align:center;font-size:10px;color:#ccc;text-shadow:0 1px 3px #000;">Julia Preview</div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./types":"src/types.ts","./state":"src/state.ts","./renderer":"src/renderer.ts","./input":"src/input.ts","./ui":"src/ui.ts"},"src/palettes.ts":{"./types":"src/types.ts"},"src/state.ts":{"./types":"src/types.ts","./palettes":"src/palettes.ts"},"src/renderer.ts":{"./types":"src/types.ts","./palettes":"src/palettes.ts"},"src/input.ts":{"./types":"src/types.ts","./state":"src/state.ts"},"src/ui.ts":{"./types":"src/types.ts","./palettes":"src/palettes.ts","./state":"src/state.ts"}};
function __require(id) {
if (__cache[id]) return __cache[id].exports;
var module = __cache[id] = { exports: {} };
var factory = __mods[id];
if (!factory) throw new Error("Module not found: " + id);
factory(module.exports, function (spec) {
var target = (__map[id] && __map[id][spec]) || spec;
return __require(target);
}, module);
return module.exports;
}
// ── module: src/main.ts ──
__mods["src/main.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const types_1 = require("./types");
const state_1 = require("./state");
const renderer_1 = require("./renderer");
const input_1 = require("./input");
const ui_1 = require("./ui");
class FractalApp {
constructor() {
this.state = (0, state_1.createInitialState)();
this.lastCursorPos = { x: 0, y: 0 };
this.juliaPreviewTimer = null;
this.lastFrameTime = performance.now();
this.fpsAccum = 0;
this.fpsCount = 0;
this.displayedFps = 60;
this.pendingRender = false;
this.renderTimer = null;
this.canvas = document.getElementById('fractalCanvas');
this.overlayCanvas = document.getElementById('overlayCanvas');
this.renderer = (0, renderer_1.createRenderer)(this.canvas);
// Setup resize handler
window.addEventListener('resize', () => this.resize());
// Setup Julia preview panel
this.juliaPreviewEl = document.getElementById('julia-preview');
this.juliaPreviewCanvas = document.getElementById('julia-preview-canvas');
// Setup input
const callbacks = {
onZoomRect: (x1, y1, x2, y2) => this.handleZoomRect(x1, y1, x2, y2),
onPan: (dRe, dIm) => this.handlePan(dRe, dIm),
onWheelZoom: (x, y, factor) => this.handleWheelZoom(x, y, factor),
onCursorMove: (x, y) => this.handleCursorMove(x, y),
onJuliaClick: (x, y) => this.handleJuliaClick(x, y),
};
this.input = (0, input_1.createInputManager)(this.canvas, this.overlayCanvas, callbacks);
// Now resize after all components are ready
this.resize();
// Setup HUD
this.hud = new ui_1.HUDManager();
this.hud.onPaletteChange = (i) => { this.state.paletteIndex = i; this.scheduleRender(); };
this.hud.onReset = () => this.handleReset();
this.hud.onBack = () => this.handleBack();
this.hud.onToggleJulia = () => this.toggleJulia();
this.hud.onColorOffsetChange = (offset) => {
this.state.colorOffset = offset;
this.renderer.colorizeOnly((0, state_1.getPalette)(this.state), this.state.colorOffset);
};
this.hud.onManualItersChange = (val) => {
this.state.manualMaxIterations = val;
this.state.view.maxIterations = (0, state_1.computeMaxIterations)(this.state.view.scale, val);
this.scheduleRender();
};
this.hud.onSaveBookmark = (name) => {
(0, state_1.addBookmark)(this.state, name);
this.hud.setBookmarks(this.state.bookmarks);
this.hud.showNow();
};
this.hud.onLoadBookmark = (id) => {
if ((0, state_1.loadBookmark)(this.state, id)) {
this.state.view.maxIterations = (0, state_1.computeMaxIterations)(this.state.view.scale, this.state.manualMaxIterations);
this.scheduleRender();
this.hud.showNow();
}
};
this.hud.onDeleteBookmark = (id) => {
(0, state_1.deleteBookmark)(this.state, id);
this.hud.setBookmarks(this.state.bookmarks);
};
// Hide bookmark list when clicking elsewhere
document.addEventListener('click', (e) => {
const bmList = document.getElementById('bookmark-list');
const bmBtn = document.getElementById('btn-bookmark-save');
if (bmList && bmList.style.display !== 'none') {
if (!bmList.contains(e.target) && bmBtn && bmBtn !== e.target) {
bmList.style.display = 'none';
}
}
});
// Keyboard shortcuts
window.addEventListener('keydown', (e) => {
if (e.key === 'r' || e.key === 'R') {
this.handleReset();
}
if (e.key === 'Escape') {
this.handleBack();
}
if (e.key === 's' || e.key === 'S') {
if (!e.ctrlKey) {
this.hud.showSaveBookmarkDialog();
}
}
});
// Check URL hash for shared view
if (location.hash) {
const hashView = (0, state_1.hashToView)(location.hash);
if (hashView) {
this.state.view = hashView;
}
}
// Initial render
this.state.view.maxIterations = (0, state_1.computeMaxIterations)(this.state.view.scale, this.state.manualMaxIterations);
this.input.setView(this.state.view);
this.scheduleRender();
this.hud.resetIdleTimer();
// Start FPS counter
this.startFpsLoop();
// Start animation loop for Julia preview
this.startAnimLoop();
}
resize() {
const dpr = Math.min(window.devicePixelRatio || 1, 2);
const cw = this.canvas.clientWidth || window.innerWidth;
const ch = this.canvas.clientHeight || window.innerHeight;
const w = Math.max(1, Math.floor(cw * dpr));
const h = Math.max(1, Math.floor(ch * dpr));
this.canvas.width = w;
this.canvas.height = h;
this.overlayCanvas.width = w;
this.overlayCanvas.height = h;
this.renderer.resize(w, h);
this.input.resize(w, h);
}
scheduleRender() {
if (this.renderTimer)
clearTimeout(this.renderTimer);
this.pendingRender = true;
this.renderTimer = window.setTimeout(() => {
this.pendingRender = false;
this.startRender();
}, 50); // Debounce rapid interactions
}
async startRender() {
const palette = (0, state_1.getPalette)(this.state);
const juliaC = this.state.julia.enabled ? this.state.julia.c : null;
await this.renderer.render(this.state.view, palette, this.state.colorOffset, juliaC, (progress) => { });
this.hud.resetIdleTimer();
}
handleZoomRect(x1, y1, x2, y2) {
const w = this.renderer.width;
const h = this.renderer.height;
const aspectRatio = w / h;
// Push current view to history
(0, state_1.pushZoomHistory)(this.state, this.state.view);
// Compute the complex plane region for the selected rectangle
const c1 = (0, state_1.screenToComplex)(x1, y1, this.state.view, w, h, aspectRatio);
const c2 = (0, state_1.screenToComplex)(x2, y2, this.state.view, w, h, aspectRatio);
const newRe = (c1.re + c2.re) / 2;
const newIm = (c1.im + c2.im) / 2;
// The old view covers a region of 4/scale in the smaller dimension
const oldRange = 4 / this.state.view.scale;
const newRangeX = Math.abs(c2.re - c1.re);
const newRangeY = Math.abs(c2.im - c1.im);
// Use the smaller range to zoom "into" the rectangle
const newRange = Math.min(newRangeX, newRangeY / aspectRatio);
let newScale = this.state.view.scale * (oldRange / newRange);
// Clamp to max magnification
if (newScale > types_1.TUNABLES.maxMagnification) {
newScale = types_1.TUNABLES.maxMagnification;
console.warn(`Reached max magnification (${types_1.TUNABLES.maxMagnification.toExponential()}). Double precision degrades beyond this point.`);
}
// Clamp to precision warning threshold
if (newScale > types_1.TUNABLES.precisionWarningMagnification) {
console.warn(`Approaching precision limits at ×${newScale.toExponential()}. Features may appear as noise.`);
}
this.state.view.re = newRe;
this.state.view.im = newIm;
this.state.view.scale = newScale;
this.state.view.maxIterations = (0, state_1.computeMaxIterations)(newScale, this.state.manualMaxIterations);
this.input.setView(this.state.view);
this.hud.resetIdleTimer();
this.scheduleRender();
// Update URL hash
history.replaceState(null, '', (0, state_1.viewToHash)(this.state.view));
}
handlePan(dRe, dIm) {
(0, state_1.pushZoomHistory)(this.state, this.state.view);
this.state.view.re += dRe;
this.state.view.im += dIm;
this.input.setView(this.state.view);
this.hud.resetIdleTimer();
this.scheduleRender();
}
handleWheelZoom(x, y, factor) {
const w = this.renderer.width;
const h = this.renderer.height;
const aspectRatio = w / h;
(0, state_1.pushZoomHistory)(this.state, this.state.view);
// Complex coordinate at cursor before zoom
const cursorC = (0, state_1.screenToComplex)(x, y, this.state.view, w, h, aspectRatio);
let newScale = this.state.view.scale * factor;
newScale = Math.max(0.1, Math.min(newScale, types_1.TUNABLES.maxMagnification));
if (newScale >= types_1.TUNABLES.maxMagnification) {
console.warn(`Reached max magnification (${types_1.TUNABLES.maxMagnification.toExponential()}).`);
}
this.state.view.scale = newScale;
// Adjust center so cursor point stays at same screen position
const newCursorC = (0, state_1.screenToComplex)(x, y, this.state.view, w, h, aspectRatio);
this.state.view.re += (cursorC.re - newCursorC.re);
this.state.view.im += (cursorC.im - newCursorC.im);
this.state.view.maxIterations = (0, state_1.computeMaxIterations)(newScale, this.state.manualMaxIterations);
this.input.setView(this.state.view);
this.hud.resetIdleTimer();
this.scheduleRender();
history.replaceState(null, '', (0, state_1.viewToHash)(this.state.view));
}
handleCursorMove(x, y) {
this.lastCursorPos = { x, y };
this.hud.resetIdleTimer();
// If in Julia mode, show crosshair
if (this.state.julia.enabled) {
this.input.drawCrosshair(x, y);
}
// Julia hover preview (when not in Julia mode)
if (!this.state.julia.enabled) {
if (this.juliaPreviewTimer)
clearTimeout(this.juliaPreviewTimer);
this.juliaPreviewTimer = window.setTimeout(() => {
const w = this.renderer.width;
const h = this.renderer.height;
const aspectRatio = w / h;
const c = (0, state_1.screenToComplex)(x, y, this.state.view, w, h, aspectRatio);
// Only show preview for exterior points (not interior)
const iter = (0, renderer_1.mandelbrotIteration)(c.re, c.im, this.state.view.maxIterations, null);
if (iter <= this.state.view.maxIterations) {
this.juliaPreviewEl.style.display = 'block';
const label = document.getElementById('julia-preview-label');
if (label)
label.textContent = `Julia: c = (${c.re.toFixed(6)}, ${c.im.toFixed(6)})`;
this.renderer.renderJuliaPreview(c, (0, state_1.getPalette)(this.state), this.state.colorOffset, this.state.view.maxIterations, this.juliaPreviewCanvas, 200);
}
else {
this.juliaPreviewEl.style.display = 'none';
}
}, 100);
}
else {
this.juliaPreviewEl.style.display = 'none';
}
}
handleJuliaClick(x, y) {
if (this.state.julia.enabled)
return; // Already in Julia mode, toggle instead
const w = this.renderer.width;
const h = this.renderer.height;
const aspectRatio = w / h;
const c = (0, state_1.screenToComplex)(x, y, this.state.view, w, h, aspectRatio);
// Don't toggle Julia if the point is inside the set
const iter = (0, renderer_1.mandelbrotIteration)(c.re, c.im, this.state.view.maxIterations, null);
if (iter > this.state.view.maxIterations)
return; // Interior point
this.state.julia.c = { re: c.re, im: c.im };
this.state.julia.enabled = true;
this.scheduleRender();
this.hud.showNow();
}
toggleJulia() {
this.state.julia.enabled = !this.state.julia.enabled;
this.scheduleRender();
this.hud.showNow();
}
handleReset() {
(0, state_1.pushZoomHistory)(this.state, this.state.view);
this.state.view = { re: -0.5, im: 0, scale: 1, maxIterations: types_1.TUNABLES.maxIterationsBase };
this.state.julia.enabled = false;
this.state.manualMaxIterations = null;
this.state.view.maxIterations = (0, state_1.computeMaxIterations)(1, null);
this.input.setView(this.state.view);
this.scheduleRender();
this.hud.showNow();
}
handleBack() {
const prev = (0, state_1.popZoomHistory)(this.state);
if (prev) {
this.state.view = prev;
this.state.view.maxIterations = (0, state_1.computeMaxIterations)(prev.scale, this.state.manualMaxIterations);
this.input.setView(this.state.view);
this.scheduleRender();
this.hud.showNow();
}
}
startFpsLoop() {
const loop = () => {
const now = performance.now();
const dt = now - this.lastFrameTime;
this.lastFrameTime = now;
this.fpsAccum += dt;
this.fpsCount++;
if (this.fpsAccum >= 1000) {
this.displayedFps = Math.round(this.fpsCount * 1000 / this.fpsAccum);
this.fpsAccum = 0;
this.fpsCount = 0;
}
requestAnimationFrame(loop);
};
requestAnimationFrame(loop);
}
startAnimLoop() {
const loop = () => {
// Update HUD
const autoMaxIters = (0, state_1.computeMaxIterations)(this.state.view.scale, null);
this.hud.update(this.state.view, this.state.paletteIndex, this.state.colorOffset, this.state.julia, this.state.bookmarks, this.displayedFps, autoMaxIters);
requestAnimationFrame(loop);
};
requestAnimationFrame(loop);
}
}
function main() {
const app = new FractalApp();
window.fractal = {
app,
iterationsAt(re, im) {
const state = app['state'];
const maxIter = state.view.maxIterations;
const juliaC = state.julia.enabled ? state.julia.c : null;
return (0, renderer_1.iterationsAt)(re, im, maxIter, juliaC);
},
setView(params) {
const state = app['state'];
const prev = { ...state.view };
state.view.re = params.re;
state.view.im = params.im;
state.view.scale = params.scale;
if (params.maxIterations !== undefined) {
state.manualMaxIterations = params.maxIterations;
}
state.view.maxIterations = (0, state_1.computeMaxIterations)(state.view.scale, state.manualMaxIterations);
app.input.setView(state.view);
app.scheduleRender();
},
getState() {
return JSON.parse(JSON.stringify(app['state']));
},
};
}
main();
};
// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
// Tunable constants and types for the Fractal Explorer
Object.defineProperty(exports, "__esModule", { value: true });
exports.TUNABLES = void 0;
exports.TUNABLES = Object.freeze({
// Iteration control
maxIterationsBase: 256,
maxIterationsScale: 10, // additional iterations per log10(magnification)
maxManualIterations: 20000,
bailoutRadius: 256, // |z|^2 > bailoutRadius → escaped
// Zoom
wheelZoomFactor: 1.15,
dragZoomMinPixels: 5,
maxMagnification: 1e14,
// Progressive rendering
progressiveSteps: 3, // number of refinement passes (1/4, 1/2, 1)
renderTileWidth: 64,
renderTileHeight: 64,
// Precision
precisionWarningMagnification: 1e13,
// HUD
hudIdleTimeoutMs: 3000,
// Worker
workerCount: 0, // 0 = main thread only, >0 = use workers
});
};
// ── module: src/palettes.ts ──
__mods["src/palettes.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PALETTES = void 0;
exports.samplePalette = samplePalette;
exports.PALETTES = [
{
name: 'Fire',
stops: [
{ pos: 0.0, r: 0, g: 0, b: 0 },
{ pos: 0.15, r: 30, g: 0, b: 50 },
{ pos: 0.3, r: 150, g: 0, b: 0 },
{ pos: 0.5, r: 255, g: 80, b: 0 },
{ pos: 0.7, r: 255, g: 200, b: 0 },
{ pos: 0.85, r: 255, g: 255, b: 150 },
{ pos: 1.0, r: 255, g: 255, b: 255 },
],
thumbnail: 'linear-gradient(90deg,#000,#1e0032,#960000,#ff5000,#ffc800,#ffff96,#fff)',
},
{
name: 'Ocean',
stops: [
{ pos: 0.0, r: 0, g: 0, b: 20 },
{ pos: 0.2, r: 0, g: 20, b: 80 },
{ pos: 0.4, r: 0, g: 80, b: 160 },
{ pos: 0.6, r: 0, g: 150, b: 200 },
{ pos: 0.8, r: 100, g: 200, b: 255 },
{ pos: 1.0, r: 200, g: 240, b: 255 },
],
thumbnail: 'linear-gradient(90deg,#000014,#001450,#0050a0,#0096c8,#64c8ff,#c8f0ff)',
},
{
name: 'Mono',
stops: [
{ pos: 0.0, r: 0, g: 0, b: 0 },
{ pos: 0.5, r: 180, g: 180, b: 180 },
{ pos: 1.0, r: 255, g: 255, b: 255 },
],
thumbnail: 'linear-gradient(90deg,#000,#b4b4b4,#fff)',
},
{
name: 'Psychedelic',
stops: [
{ pos: 0.0, r: 128, g: 0, b: 128 },
{ pos: 0.2, r: 0, g: 128, b: 128 },
{ pos: 0.4, r: 0, g: 128, b: 0 },
{ pos: 0.6, r: 128, g: 128, b: 0 },
{ pos: 0.8, r: 128, g: 0, b: 0 },
{ pos: 1.0, r: 128, g: 0, b: 128 },
],
thumbnail: 'linear-gradient(90deg,#800080,#008080,#008000,#808000,#800000,#800080)',
},
{
name: 'Electric',
stops: [
{ pos: 0.0, r: 10, g: 0, b: 30 },
{ pos: 0.25, r: 30, g: 0, b: 120 },
{ pos: 0.5, r: 0, g: 100, b: 200 },
{ pos: 0.75, r: 0, g: 220, b: 100 },
{ pos: 1.0, r: 200, g: 255, b: 200 },
],
thumbnail: 'linear-gradient(90deg,#0a001e,#1e0078,#0064c8,#00dc64,#c8ffc8)',
},
];
// Given a palette and a t in [0,1], return [r, g, b] with smooth interpolation.
function samplePalette(palette, t, offset) {
// Apply cyclic offset
t = ((t + offset) % 1.0 + 1.0) % 1.0;
const stops = palette.stops;
if (stops.length === 1)
return [stops[0].r, stops[0].g, stops[0].b];
// Find bracketing stops
let i = 0;
while (i < stops.length - 1 && stops[i + 1].pos < t)
i++;
if (i >= stops.length - 1) {
const s = stops[stops.length - 1];
return [s.r, s.g, s.b];
}
const a = stops[i];
const b = stops[i + 1];
const range = b.pos - a.pos;
const frac = range > 0 ? (t - a.pos) / range : 0;
// Smoothstep for nicer gradients
const f = frac * frac * (3 - 2 * frac);
return [
Math.round(a.r + (b.r - a.r) * f),
Math.round(a.g + (b.g - a.g) * f),
Math.round(a.b + (b.b - a.b) * f),
];
}
};
// ── module: src/state.ts ──
__mods["src/state.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.computeMaxIterations = computeMaxIterations;
exports.createInitialState = createInitialState;
exports.getPalette = getPalette;
exports.pushZoomHistory = pushZoomHistory;
exports.popZoomHistory = popZoomHistory;
exports.addBookmark = addBookmark;
exports.loadBookmark = loadBookmark;
exports.deleteBookmark = deleteBookmark;
exports.viewToHash = viewToHash;
exports.hashToView = hashToView;
exports.screenToComplex = screenToComplex;
exports.complexToScreen = complexToScreen;
const types_1 = require("./types");
const palettes_1 = require("./palettes");
function defaultView() {
return { re: -0.5, im: 0, scale: 1, maxIterations: types_1.TUNABLES.maxIterationsBase };
}
function computeMaxIterations(scale, manualOverride) {
if (manualOverride !== null)
return manualOverride;
const logScale = Math.log10(Math.max(scale, 1));
return Math.round(types_1.TUNABLES.maxIterationsBase + types_1.TUNABLES.maxIterationsScale * logScale);
}
function createInitialState() {
return {
view: defaultView(),
paletteIndex: 0,
colorOffset: 0,
julia: { enabled: false, c: { re: -0.7269, im: 0.1889 } },
zoomHistory: [],
bookmarks: loadBookmarks(),
manualMaxIterations: null,
};
}
function getPalette(state) {
return palettes_1.PALETTES[state.paletteIndex % palettes_1.PALETTES.length];
}
function pushZoomHistory(state, view) {
state.zoomHistory.push({ ...view });
if (state.zoomHistory.length > 100)
state.zoomHistory.shift();
}
function popZoomHistory(state) {
if (state.zoomHistory.length === 0)
return null;
const prev = state.zoomHistory.pop();
return prev;
}
const BOOKMARKS_KEY = 'fractal_explorer_bookmarks';
function loadBookmarks() {
try {
const raw = localStorage.getItem(BOOKMARKS_KEY);
if (raw)
return JSON.parse(raw);
}
catch { /* ignore */ }
return [];
}
function saveBookmarks(bookmarks) {
try {
localStorage.setItem(BOOKMARKS_KEY, JSON.stringify(bookmarks));
}
catch { /* ignore */ }
}
let _bookmarkIdCounter = 0;
function addBookmark(state, name) {
_bookmarkIdCounter++;
const bm = {
id: _bookmarkIdCounter,
name,
view: { ...state.view },
paletteIndex: state.paletteIndex,
colorOffset: state.colorOffset,
juliaC: state.julia.c ? { ...state.julia.c } : null,
isJulia: state.julia.enabled,
};
state.bookmarks.push(bm);
saveBookmarks(state.bookmarks);
}
function loadBookmark(state, id) {
const bm = state.bookmarks.find(b => b.id === id);
if (!bm)
return false;
state.view = { ...bm.view };
state.paletteIndex = bm.paletteIndex;
state.colorOffset = bm.colorOffset;
state.julia.enabled = bm.isJulia;
if (bm.juliaC)
state.julia.c = { ...bm.juliaC };
return true;
}
function deleteBookmark(state, id) {
state.bookmarks = state.bookmarks.filter(b => b.id !== id);
saveBookmarks(state.bookmarks);
}
function viewToHash(view) {
return '#' + encodeURIComponent(JSON.stringify({
re: view.re, im: view.im, scale: view.scale, maxIterations: view.maxIterations,
}));
}
function hashToView(hash) {
try {
if (!hash.startsWith('#'))
return null;
const decoded = decodeURIComponent(hash.slice(1));
const parsed = JSON.parse(decoded);
return {
re: parsed.re ?? -0.5,
im: parsed.im ?? 0,
scale: parsed.scale ?? 1,
maxIterations: parsed.maxIterations ?? 256,
};
}
catch {
return null;
}
}
// Screen-to-complex coordinate conversion
function screenToComplex(sx, sy, view, canvasWidth, canvasHeight, aspectRatio) {
const zoomFactor = 4 / (view.scale * Math.min(canvasWidth, canvasHeight));
const w = canvasWidth * zoomFactor;
const h = canvasHeight * zoomFactor;
const offsetX = (canvasWidth - canvasHeight * aspectRatio) / 2;
const offsetY = (canvasHeight - canvasWidth / aspectRatio) / 2;
return {
re: view.re + (sx - offsetX - canvasWidth / 2) * zoomFactor,
im: view.im + (sy - offsetY - canvasHeight / 2) * zoomFactor,
};
}
function complexToScreen(c, view, canvasWidth, canvasHeight, aspectRatio) {
const zoomFactor = 4 / (view.scale * Math.min(canvasWidth, canvasHeight));
const offsetX = (canvasWidth - canvasHeight * aspectRatio) / 2;
const offsetY = (canvasHeight - canvasWidth / aspectRatio) / 2;
return {
x: (c.re - view.re) / zoomFactor + offsetX + canvasWidth / 2,
y: (c.im - view.im) / zoomFactor + offsetY + canvasHeight / 2,
};
}
};
// ── module: src/renderer.ts ──
__mods["src/renderer.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.mandelbrotIteration = mandelbrotIteration;
exports.iterationsAt = iterationsAt;
exports.createRenderer = createRenderer;
const types_1 = require("./types");
const palettes_1 = require("./palettes");
function mandelbrotIteration(cr, ci, maxIter, juliaC) {
const bailout = types_1.TUNABLES.bailoutRadius;
const bailoutLog = Math.log(bailout);
let zr, zi;
if (juliaC !== null) {
zr = cr;
zi = ci;
const jcr = juliaC.re;
const jci = juliaC.im;
let iter = 0;
while (zr * zr + zi * zi <= bailout && iter < maxIter) {
const tmp = zr * zr - zi * zi + jcr;
zi = 2 * zr * zi + jci;
zr = tmp;
iter++;
}
if (iter >= maxIter)
return maxIter + 1;
const log_zn = Math.log(zr * zr + zi * zi);
return iter + 1 - Math.log(log_zn / bailoutLog) / Math.log(2);
}
else {
zr = 0;
zi = 0;
let iter = 0;
while (zr * zr + zi * zi <= bailout && iter < maxIter) {
const tmp = zr * zr - zi * zi + cr;
zi = 2 * zr * zi + ci;
zr = tmp;
iter++;
}
if (iter >= maxIter)
return maxIter + 1;
const log_zn = Math.log(zr * zr + zi * zi);
return iter + 1 - Math.log(log_zn / bailoutLog) / Math.log(2);
}
}
function iterationsAt(re, im, maxIter, juliaC) {
const result = mandelbrotIteration(re, im, maxIter, juliaC);
if (result > maxIter)
return maxIter;
return Math.max(0, Math.floor(result));
}
class Renderer {
constructor(canvas) {
this._w = 0;
this._h = 0;
this.imgData = null;
this.pixels = new Uint8ClampedArray(0);
this.iterBuffer = new Float32Array(0);
this._cancelled = false;
this._currentMaxIter = 0;
this._juliaPreviewCanvas = null;
this._juliaPreviewCtx = null;
this._juliaPreviewImageData = null;
this._juliaPreviewPixels = new Uint8ClampedArray(0);
this._juliaPreviewW = 0;
this._juliaPreviewH = 0;
this.ctx = canvas.getContext('2d');
}
resize(w, h) {
this._w = w;
this._h = h;
this.imgData = this.ctx.createImageData(w, h);
this.pixels = this.imgData.data;
this.iterBuffer = new Float32Array(w * h);
this._cancelled = false;
}
get width() { return this._w; }
get height() { return this._h; }
cancel() { this._cancelled = true; }
// Compute a row of pixels at given resolution and upsample
computeRows(view, palette, colorOffset, juliaC, maxIter, yStart, yEnd, pixels, iterBuf, w, h, zoomFactor, offsetX, offsetY, stepDown) {
for (let sy = yStart; sy < yEnd && sy < h; sy += stepDown) {
if (this._cancelled)
return;
const ci = view.im + (sy - offsetY - h / 2) * zoomFactor;
const row = sy * w;
for (let sx = 0; sx < w; sx++) {
const cr = view.re + (sx - offsetX - w / 2) * zoomFactor;
const si = mandelbrotIteration(cr, ci, maxIter, juliaC);
// Fill stepDown x stepDown block
for (let dy = 0; dy < stepDown && sy + dy < h; dy++) {
let r = 0, g = 0, b = 0;
if (si > maxIter) {
r = 0;
g = 0;
b = 0;
}
else {
const t = si / maxIter;
const c = (0, palettes_1.samplePalette)(palette, t, colorOffset);
r = c[0];
g = c[1];
b = c[2];
}
const baseIdx = ((sy + dy) * w) * 4;
for (let dx = 0; dx < stepDown; dx++) {
const px = sx + dx;
if (px < w) {
const idx = baseIdx + px * 4;
pixels[idx] = r;
pixels[idx + 1] = g;
pixels[idx + 2] = b;
pixels[idx + 3] = 255;
}
}
}
// Store iteration in buffer on finest pass
if (iterBuf && stepDown === 1) {
iterBuf[row + sx] = si;
}
}
}
}
async render(view, palette, colorOffset, juliaC, onProgress) {
this._cancelled = false;
this._currentMaxIter = view.maxIterations;
const w = this._w;
const h = this._h;
const maxIter = view.maxIterations;
const aspectRatio = w / h;
const steps = types_1.TUNABLES.progressiveSteps;
const factors = [];
for (let s = 0; s < steps; s++) {
factors.push(Math.pow(2, steps - 1 - s));
}
for (let si = 0; si < factors.length; si++) {
if (this._cancelled)
return;
const factor = factors[si];
const zoomFactor = factor * 4 / (view.scale * Math.min(w, h));
const offsetX = (w - h * aspectRatio) / 2;
const offsetY = (h - w / aspectRatio) / 2;
const iterBuf = (si === factors.length - 1) ? this.iterBuffer : null;
const stepSize = Math.max(1, Math.ceil(factor));
// Process in chunks of rows for responsiveness
const chunkRows = Math.max(4, Math.ceil(h / (32 * stepSize)));
for (let rowStart = 0; rowStart < h; rowStart += chunkRows * stepSize) {
if (this._cancelled)
return;
const rowEnd = Math.min(rowStart + chunkRows * stepSize, h);
this.computeRows(view, palette, colorOffset, juliaC, maxIter, rowStart, rowEnd, this.pixels, iterBuf, w, h, zoomFactor, offsetX, offsetY, stepSize);
this.ctx.putImageData(this.imgData, 0, 0);
// Yield between chunks
await new Promise(resolve => {
if (this._cancelled) {
resolve();
return;
}
setTimeout(resolve, 0);
});
}
if (onProgress)
onProgress((si + 1) / factors.length);
}
}
colorizeOnly(palette, colorOffset) {
const w = this._w;
const h = this._h;
const pixels = this.pixels;
const buf = this.iterBuffer;
const maxIter = this._currentMaxIter;
for (let i = 0; i < w * h; i++) {
const si = buf[i];
const idx = i * 4;
if (si > maxIter) {
pixels[idx] = 0;
pixels[idx + 1] = 0;
pixels[idx + 2] = 0;
pixels[idx + 3] = 255;
}
else {
const t = si / maxIter;
const c = (0, palettes_1.samplePalette)(palette, t, colorOffset);
pixels[idx] = c[0];
pixels[idx + 1] = c[1];
pixels[idx + 2] = c[2];
pixels[idx + 3] = 255;
}
}
this.ctx.putImageData(this.imgData, 0, 0);
}
getIterationAt(sx, sy) {
if (sx < 0 || sx >= this._w || sy < 0 || sy >= this._h)
return -1;
return this.iterBuffer[sy * this._w + sx];
}
// Render a small Julia preview at given c-value
renderJuliaPreview(c, palette, colorOffset, maxIter, canvas, size) {
const ctx = canvas.getContext('2d');
canvas.width = size;
canvas.height = size;
const imgData = ctx.createImageData(size, size);
const px = imgData.data;
// Julia set: z₀ = (x,y), c = fixed parameter
// Render over a region that typically shows the Julia set nicely
const zoomFactor = 4 / (size * 1); // Scale 1 = base view
const offsetX = 0;
const offsetY = 0;
for (let y = 0; y < size; y++) {
const zi = (y - offsetY - size / 2) * zoomFactor;
for (let x = 0; x < size; x++) {
const zr0 = (x - offsetX - size / 2) * zoomFactor;
const si = mandelbrotIteration(zr0, zi, maxIter, c);
const idx = (y * size + x) * 4;
if (si > maxIter) {
px[idx] = 0;
px[idx + 1] = 0;
px[idx + 2] = 0;
px[idx + 3] = 255;
}
else {
const t = si / maxIter;
const col = (0, palettes_1.samplePalette)(palette, t, colorOffset);
px[idx] = col[0];
px[idx + 1] = col[1];
px[idx + 2] = col[2];
px[idx + 3] = 255;
}
}
}
ctx.putImageData(imgData, 0, 0);
}
}
function createRenderer(canvas) {
return new Renderer(canvas);
}
};
// ── module: src/input.ts ──
__mods["src/input.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createInputManager = createInputManager;
const types_1 = require("./types");
class InputManager {
constructor(canvas, overlayCanvas, callbacks) {
this.state = {
isDragging: false,
dragStartX: 0, dragStartY: 0,
isShiftDrag: false,
lastPanX: 0, lastPanY: 0,
};
this._w = 0;
this._h = 0;
this.canvas = canvas;
this.overlayCanvas = overlayCanvas;
this.overlayCtx = overlayCanvas.getContext('2d');
this.callbacks = callbacks;
this.bindEvents();
}
setView(view) { this.view = view; }
resize(w, h) {
this._w = w;
this._h = h;
}
bindEvents() {
this.canvas.addEventListener('mousedown', this.onMouseDown.bind(this));
window.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));
}
getCanvasPos(e) {
const rect = this.canvas.getBoundingClientRect();
return {
x: (e.clientX - rect.left) * (this._w / rect.width),
y: (e.clientY - rect.top) * (this._h / rect.height),
};
}
onMouseDown(e) {
const pos = this.getCanvasPos(e);
this.state.isDragging = true;
this.state.dragStartX = pos.x;
this.state.dragStartY = pos.y;
this.state.isShiftDrag = e.shiftKey || e.button === 1;
this.state.lastPanX = pos.x;
this.state.lastPanY = pos.y;
if (this.state.isShiftDrag) {
this.canvas.style.cursor = 'grab';
}
}
onMouseMove(e) {
const pos = this.getCanvasPos(e);
this.callbacks.onCursorMove(pos.x, pos.y);
if (!this.state.isDragging)
return;
if (this.state.isShiftDrag) {
// Panning
const dx = pos.x - this.state.lastPanX;
const dy = pos.y - this.state.lastPanY;
this.state.lastPanX = pos.x;
this.state.lastPanY = pos.y;
const aspectRatio = this._w / this._h;
const zoomFactor = 4 / (this.view.scale * Math.min(this._w, this._h));
const dRe = -dx * zoomFactor;
const dIm = -dy * zoomFactor;
this.callbacks.onPan(dRe, dIm);
}
else {
// Drawing zoom rectangle on overlay
this.drawZoomRect(this.state.dragStartX, this.state.dragStartY, pos.x, pos.y);
}
}
onMouseUp(e) {
if (!this.state.isDragging)
return;
const pos = this.getCanvasPos(e);
this.state.isDragging = false;
this.canvas.style.cursor = 'crosshair';
this.clearOverlay();
if (this.state.isShiftDrag)
return;
const x1 = Math.min(this.state.dragStartX, pos.x);
const y1 = Math.min(this.state.dragStartY, pos.y);
const x2 = Math.max(this.state.dragStartX, pos.x);
const y2 = Math.max(this.state.dragStartY, pos.y);
const dx = x2 - x1;
const dy = y2 - y1;
if (dx < types_1.TUNABLES.dragZoomMinPixels || dy < types_1.TUNABLES.dragZoomMinPixels) {
// Tiny drag — treat as Julia mode click or single-point zoom
if (e.shiftKey)
return;
this.callbacks.onJuliaClick(pos.x, pos.y);
}
else {
this.callbacks.onZoomRect(x1, y1, x2, y2);
}
}
onWheel(e) {
e.preventDefault();
const pos = this.getCanvasPos(e);
// Calculate zoom factor from wheel delta
const factor = e.deltaY < 0
? Math.pow(types_1.TUNABLES.wheelZoomFactor, Math.min(Math.abs(e.deltaY / 30), 3))
: Math.pow(1 / types_1.TUNABLES.wheelZoomFactor, Math.min(Math.abs(e.deltaY / 30), 3));
this.callbacks.onWheelZoom(pos.x, pos.y, factor);
}
onKeyDown(e) {
const panAmount = 0.02 / this.view.scale;
switch (e.key) {
case 'ArrowLeft':
e.preventDefault();
this.callbacks.onPan(panAmount, 0);
break;
case 'ArrowRight':
e.preventDefault();
this.callbacks.onPan(-panAmount, 0);
break;
case 'ArrowUp':
e.preventDefault();
this.callbacks.onPan(0, panAmount);
break;
case 'ArrowDown':
e.preventDefault();
this.callbacks.onPan(0, -panAmount);
break;
case '+':
case '=':
e.preventDefault();
this.callbacks.onWheelZoom(this._w / 2, this._h / 2, 2);
break;
case '-':
e.preventDefault();
this.callbacks.onWheelZoom(this._w / 2, this._h / 2, 0.5);
break;
case 'j':
case 'J':
e.preventDefault();
this.callbacks.onJuliaClick(this._w / 2, this._h / 2);
break;
}
}
drawZoomRect(x1, y1, x2, y2) {
const ctx = this.overlayCtx;
ctx.clearRect(0, 0, this._w, this._h);
// Semi-transparent fill
ctx.fillStyle = 'rgba(255, 255, 255, 0.05)';
ctx.fillRect(x1, y1, x2 - x1, y2 - y1);
// Dashed border with inverted color
ctx.strokeStyle = 'rgba(255, 255, 100, 0.9)';
ctx.lineWidth = 1.5;
ctx.setLineDash([6, 4]);
ctx.strokeRect(x1, y1, x2 - x1, y2 - y1);
ctx.setLineDash([]);
// Corner brackets
const len = 12;
ctx.strokeStyle = 'rgba(255, 255, 100, 1)';
ctx.lineWidth = 2;
// Top-left
ctx.beginPath();
ctx.moveTo(x1 - len, y1);
ctx.lineTo(x1, y1);
ctx.lineTo(x1, y1 + len);
ctx.stroke();
// Top-right
ctx.beginPath();
ctx.moveTo(x2 + len, y1);
ctx.lineTo(x2, y1);
ctx.lineTo(x2, y1 + len);
ctx.stroke();
// Bottom-left
ctx.beginPath();
ctx.moveTo(x1 - len, y2);
ctx.lineTo(x1, y2);
ctx.lineTo(x1, y2 - len);
ctx.stroke();
// Bottom-right
ctx.beginPath();
ctx.moveTo(x2 + len, y2);
ctx.lineTo(x2, y2);
ctx.lineTo(x2, y2 - len);
ctx.stroke();
}
clearOverlay() {
this.overlayCtx.clearRect(0, 0, this._w, this._h);
}
drawCrosshair(x, y) {
const ctx = this.overlayCtx;
ctx.clearRect(0, 0, this._w, this._h);
ctx.strokeStyle = 'rgba(255, 255, 255, 0.6)';
ctx.lineWidth = 0.5;
// Crosshair lines
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, this._h);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(this._w, y);
ctx.stroke();
// Center dot
ctx.beginPath();
ctx.arc(x, y, 4, 0, Math.PI * 2);
ctx.stroke();
}
}
function createInputManager(canvas, overlayCanvas, callbacks) {
return new InputManager(canvas, overlayCanvas, callbacks);
}
};
// ── module: src/ui.ts ──
__mods["src/ui.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.HUDManager = void 0;
const types_1 = require("./types");
const palettes_1 = require("./palettes");
class HUDManager {
constructor() {
this._idleTimer = null;
this._isJulia = false;
this._autoMaxIters = 256;
this.bookmarks = [];
this.hudEl = document.getElementById('hud');
this.zoomLabel = document.getElementById('zoom-label');
this.centerLabel = document.getElementById('center-label');
this.fpsLabel = document.getElementById('fps-label');
this.paletteBar = document.getElementById('palette-bar');
this.sliderOffset = document.getElementById('slider-offset');
this.sliderIters = document.getElementById('slider-iters');
this.btnReset = document.getElementById('btn-reset');
this.btnBack = document.getElementById('btn-back');
this.btnJulia = document.getElementById('btn-julia');
this.btnBookmarkSave = document.getElementById('btn-bookmark-save');
this.bookmarkList = document.getElementById('bookmark-list');
this.initPaletteChips();
this.initButtons();
this.bookmarkList.style.display = 'none';
}
initPaletteChips() {
for (let i = 0; i < palettes_1.PALETTES.length; i++) {
const chip = document.createElement('div');
chip.className = 'palette-chip';
chip.style.background = palettes_1.PALETTES[i].thumbnail;
chip.title = palettes_1.PALETTES[i].name;
chip.dataset.index = String(i);
chip.addEventListener('click', () => {
this.onPaletteChange?.(i);
});
this.paletteBar.appendChild(chip);
}
}
initButtons() {
this.btnReset.addEventListener('click', () => { this.onReset?.(); });
this.btnBack.addEventListener('click', () => { this.onBack?.(); });
this.btnJulia.addEventListener('click', () => { this.onToggleJulia?.(); });
this.btnBookmarkSave.addEventListener('click', () => {
this.bookmarkList.style.display =
this.bookmarkList.style.display === 'flex' ? 'none' : 'flex';
if (this.bookmarkList.style.display === 'flex')
this.renderBookmarks();
});
this.sliderOffset.addEventListener('input', () => {
this.onColorOffsetChange?.(parseFloat(this.sliderOffset.value));
});
this.sliderIters.addEventListener('input', () => {
this.onManualItersChange?.(parseInt(this.sliderIters.value, 10));
});
// Reset manual iters when slider is released from max
this.sliderIters.addEventListener('change', () => {
// If user sets iters to the auto value, reset manual override
if (parseInt(this.sliderIters.value, 10) === this._autoMaxIters) {
this.onManualItersChange?.(null);
}
});
}
update(view, paletteIndex, colorOffset, julia, bookmarks, fps, autoMaxIters) {
this._autoMaxIters = autoMaxIters;
// Zoom label
let zoomStr;
if (view.scale >= 1e9)
zoomStr = `×${(view.scale / 1e9).toFixed(1)}e9`;
else if (view.scale >= 1e6)
zoomStr = `×${(view.scale / 1e6).toFixed(1)}e6`;
else if (view.scale >= 1e3)
zoomStr = `×${(view.scale / 1e3).toFixed(1)}e3`;
else
zoomStr = `×${view.scale.toFixed(1)}`;
this.zoomLabel.textContent = zoomStr;
// Center label
this.centerLabel.textContent = `(${view.re.toExponential(4)}, ${view.im.toExponential(4)})`;
// FPS
this.fpsLabel.textContent = `${fps} fps`;
// Julia button state
this.btnJulia.classList.toggle('active', julia.enabled);
// Palette chips
const chips = this.paletteBar.querySelectorAll('.palette-chip');
chips.forEach((chip, i) => {
chip.classList.toggle('active', i === paletteIndex);
});
// Slider values
this.sliderOffset.value = String(colorOffset);
this.sliderIters.value = String(Math.min(Math.max(autoMaxIters, 50), 10000));
}
renderBookmarks() {
if (!this.bookmarks)
return;
this.bookmarkList.innerHTML = '';
if (this.bookmarks.length === 0) {
const empty = document.createElement('div');
empty.className = 'bm-item';
empty.textContent = 'No bookmarks saved';
this.bookmarkList.appendChild(empty);
return;
}
for (const bm of this.bookmarks) {
const item = document.createElement('div');
item.className = 'bm-item';
item.innerHTML = `<span title="Load view">${bm.name} ×${bm.view.scale.toExponential(1)}</span>`;
const delBtn = document.createElement('button');
delBtn.textContent = '✕';
delBtn.title = 'Delete bookmark';
delBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.onDeleteBookmark?.(bm.id);
this.renderBookmarks();
});
item.addEventListener('click', () => {
this.onLoadBookmark?.(bm.id);
this.bookmarkList.style.display = 'none';
});
item.appendChild(delBtn);
this.bookmarkList.appendChild(item);
}
}
setBookmarks(bookmarks) {
this.bookmarks = bookmarks;
if (this.bookmarkList.style.display !== 'none') {
this.renderBookmarks();
}
}
promptSaveBookmark(onSave) {
const name = prompt('Bookmark name:');
if (name)
onSave(name);
}
resetIdleTimer() {
if (this._idleTimer)
clearTimeout(this._idleTimer);
this.hudEl.classList.remove('hidden');
this._idleTimer = window.setTimeout(() => {
this.hudEl.classList.add('hidden');
}, types_1.TUNABLES.hudIdleTimeoutMs);
}
showNow() {
this.hudEl.classList.remove('hidden');
this.resetIdleTimer();
}
// Show save bookmark dialog
showSaveBookmarkDialog() {
this.promptSaveBookmark((name) => {
this.onSaveBookmark?.(name);
});
}
}
exports.HUDManager = HUDManager;
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"unsloth/Qwen3.6-27B-MTP-GGUF-UD-Q5_K_XL","provider":"brigid","persona":"typescript","sessionId":"f77b8615-088d-485a-814e-a6d5a2baa5c5","tokensIn":1808528,"tokensOut":44312,"tokensTotal":1852840,"cost":0,"turns":56,"toolCalls":56,"failedToolCalls":4,"timestamp":"2026-08-04T08:13:25.992Z"} -->