Fractal Explorer
Qwen 3.6 27b MTP · typescript

Browser check: Artifact runtime error. This is a technical check, not a quality review.
Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 58,546 bytes · SHA-256 db1c2a8d00ca
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Fractal Explorer — Mandelbrot & Julia Sets</title>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #000; font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; }
#app { position: relative; width: 100vw; height: 100vh; }
#fractal-canvas { display: block; width: 100%; height: 100%; image-rendering: auto; }
.overlay-canvas { position: absolute; top: 0; left: 0; pointer-events: none; }
/* ─── HUD ───────────────────────────────────────────── */
#hud {
position: absolute; top: 0; left: 0; right: 0; z-index: 10;
pointer-events: none; transition: opacity 0.4s ease;
}
#hud-top, #hud-bottom {
display: flex; justify-content: space-between; align-items: center;
padding: 8px 16px; color: rgba(255,255,255,0.85); font-size: 13px;
text-shadow: 0 1px 4px rgba(0,0,0,0.9), 0 0 12px rgba(0,0,0,0.6);
}
#hud-top { background: linear-gradient(to bottom, rgba(0,0,0,0.5) 0%, transparent 100%); }
#hud-bottom { position: absolute; bottom: 0; top: auto; background: linear-gradient(to top, rgba(0,0,0,0.5) 0%, transparent 100%); }
/* ─── Controls Bar ───────────────────────────────────── */
#controls-bar {
position: absolute; bottom: 48px; left: 50%; transform: translateX(-50%);
display: flex; align-items: center; gap: 12px; padding: 8px 16px;
background: rgba(15, 15, 30, 0.7); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
border-radius: 12px; border: 1px solid rgba(255,255,255,0.1);
pointer-events: auto;
}
/* ─── Palette Swatches ──────────────────────────────── */
#palette-container { display: flex; gap: 6px; }
.palette-chip {
width: 28px; height: 28px; border-radius: 50%; cursor: pointer;
border: 2px solid transparent; transition: all 0.2s ease;
}
.palette-chip:hover { transform: scale(1.15); }
.palette-chip.active { border-color: #fff; box-shadow: 0 0 8px rgba(255,255,255,0.4); }
/* ─── Sliders ────────────────────────────────────────── */
.slider-group { display: flex; align-items: center; gap: 4px; font-size: 13px; color: rgba(255,255,255,0.7); }
.slider-group input[type="range"] { width: 80px; height: 4px; cursor: pointer; accent-color: #6af; }
/* ─── Buttons ────────────────────────────────────────── */
#controls-bar button {
background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.15);
color: #fff; padding: 4px 10px; border-radius: 6px; cursor: pointer;
font-size: 14px; transition: background 0.2s;
}
#controls-bar button:hover { background: rgba(255,255,255,0.2); }
/* ─── Bookmarks Panel ────────────────────────────────── */
#bookmarks-panel {
display: none; position: absolute; bottom: 100px; left: 50%; transform: translateX(-50%);
background: rgba(15, 15, 30, 0.85); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
border-radius: 10px; border: 1px solid rgba(255,255,255,0.1); padding: 12px; min-width: 240px;
pointer-events: auto; max-height: 300px; overflow-y: auto;
}
.bookmark-item {
padding: 6px 10px; color: rgba(255,255,255,0.8); font-size: 12px; cursor: pointer;
border-radius: 4px; transition: background 0.15s;
}
.bookmark-item:hover { background: rgba(255,255,255,0.1); }
.bookmark-empty { color: rgba(255,255,255,0.4); font-size: 12px; text-align: center; padding: 8px; }
/* ─── Julia Preview ──────────────────────────────────── */
#julia-preview-container {
display: none; position: absolute; top: 60px; right: 16px; z-index: 5;
}
#julia-preview {
border-radius: 8px; border: 2px solid rgba(255,255,255,0.3);
box-shadow: 0 4px 20px rgba(0,0,0,0.6); cursor: pointer;
}
/* ─── Help Overlay ───────────────────────────────────── */
#help-overlay {
display: none; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
background: rgba(10, 10, 25, 0.92); backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
border-radius: 16px; border: 1px solid rgba(255,255,255,0.15); padding: 32px; color: #fff;
z-index: 100; text-align: center; max-width: 400px;
}
#help-overlay h2 { margin-bottom: 16px; font-weight: 300; letter-spacing: 2px; }
#help-overlay p { margin: 6px 0; font-size: 13px; color: rgba(255,255,255,0.7); line-height: 1.6; }
/* ─── Scrollbar ──────────────────────────────────────── */
::-webkit-scrollbar { width: 4px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); border-radius: 2px; }
</style>
</head>
<body>
<div id="app">
<canvas id="fractal-canvas"></canvas>
<canvas id="zoom-box-canvas" class="overlay-canvas"></canvas>
<!-- HUD -->
<div id="hud">
<div id="hud-top">
<span id="mode-display">Mandelbrot</span>
<span id="mag-display">×3.00</span>
</div>
<div id="hud-bottom">
<span id="center-display">-0.5, 0i</span>
<span id="iter-display">iterations: 200</span>
<span id="fps-display">60 fps</span>
</div>
<!-- Controls -->
<div id="controls-bar">
<div id="palette-container"></div>
<label class="slider-group" title="Color offset (palette cycling)">
🎨<input type="range" id="color-slider" min="0" max="1" step="0.001" value="0">
</label>
<label class="slider-group" title="Iteration override">
🔢<input type="range" id="iter-slider" min="50" max="15000" step="10" value="200">
</label>
<button id="back-btn" title="Zoom history back (B)">⬅</button>
<button id="reset-btn" title="Reset view (R)">⟲</button>
<button id="bookmark-btn" title="Save/Load bookmarks (S)">★</button>
</div>
<!-- Bookmarks panel -->
<div id="bookmarks-panel"></div>
</div>
<!-- Julia preview container -->
<div id="julia-preview-container"></div>
<!-- Help overlay -->
<div id="help-overlay">
<h2>Fractal Explorer</h2>
<p><b>Scroll</b> — zoom toward cursor | <b>Drag</b> — pan</p>
<p><b>Shift+Drag</b> — rectangle zoom | <b>J</b> — Julia mode at center</p>
<p><b>Arrows/+/-</b> — pan/zoom | <b>B</b> — back | <b>R</b> — reset</p>
<p><b>S</b> — save bookmark | <b>P</b> — cycle palette</p>
</div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./types":"src/types.ts","./palettes":"src/palettes.ts","./math":"src/math.ts","./renderer":"src/renderer.ts","./navigation":"src/navigation.ts","./ui":"src/ui.ts"},"src/palettes.ts":{"./types":"src/types.ts"},"src/math.ts":{"./types":"src/types.ts"},"src/renderer.ts":{"./types":"src/types.ts","./palettes":"src/palettes.ts"},"src/navigation.ts":{"./types":"src/types.ts"},"src/ui.ts":{"./types":"src/types.ts","./palettes":"src/palettes.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 palettes_1 = require("./palettes");
const math_1 = require("./math");
const renderer_1 = require("./renderer");
const navigation_1 = require("./navigation");
const ui_1 = require("./ui");
// ─── Globals ──────────────────────────────────────────────
let canvas;
let renderer;
let nav;
let ui;
let renderTimeout = null;
let iterOverride = 0; // 0 means auto-compute
// Interaction state
let isPanning = false;
let isRectZooming = false;
let panStartX = 0, panStartY = 0;
let rectStartX = 0, rectStartY = 0;
let lastMouseX = 0, lastMouseY = 0;
// Bookmarks
const bookmarks = loadBookmarks();
// ─── Initialization ──────────────────────────────────────
function init() {
canvas = document.getElementById('fractal-canvas');
renderer = new renderer_1.FractalRenderer(canvas);
nav = new navigation_1.Navigation();
ui = new ui_1.UIManager();
// Load from URL hash or use defaults
if (!nav.loadFromHash()) {
// Check for saved bookmark in localStorage
const lastView = localStorage.getItem('fractal-last-view');
if (lastView) {
try {
nav.restore(JSON.parse(lastView));
}
catch { }
}
}
resize();
window.addEventListener('resize', resize);
setupInputHandlers();
setupKeyboardHandlers();
setupUICallbacks();
// Initial render
scheduleRender();
// Show help briefly
const help = document.getElementById('help-overlay');
help.style.display = 'block';
setTimeout(() => { help.style.display = 'none'; }, 4000);
// Expose debug interface
window.fractal = {
iterationsAt: (re, im) => (0, math_1.iterationsAt)(re, im, getEffectiveMaxIter()),
setView: (view) => { nav.restore({ ...nav.state, ...view }); scheduleRender(); },
getState: () => nav.serialize(),
getBookmarks: () => bookmarks.map(b => ({ name: b.name, viewState: b.viewState })),
};
// FPS counter
requestAnimationFrame(tick);
}
function resize() {
const dpr = Math.min(window.devicePixelRatio || 1, 2);
canvas.width = window.innerWidth * dpr;
canvas.height = window.innerHeight * dpr;
ui.resize(canvas.width, canvas.height);
scheduleRender();
}
// ─── Render Pipeline ──────────────────────────────────────
function getEffectiveMaxIter() {
if (iterOverride > 0)
return iterOverride;
return (0, math_1.computeMaxIterations)(nav.state.magnification);
}
function scheduleRender() {
renderer.cancel();
if (renderTimeout !== null)
clearTimeout(renderTimeout);
renderTimeout = window.setTimeout(() => doRender(), 50); // debounce rapid interactions
}
async function doRender() {
const state = nav.state;
const maxIter = getEffectiveMaxIter();
const params = {
width: canvas.width,
height: canvas.height,
centerRe: state.centerRe,
centerIm: state.centerIm,
magnification: state.magnification,
maxIterations: maxIter,
paletteIndex: state.paletteIndex,
colorOffset: state.colorOffset,
setMode: state.setMode,
juliaCRe: state.juliaCRe,
juliaCIm: state.juliaCIm,
};
await renderer.render(params);
// Update HUD after render completes
updateHUD(maxIter);
}
function recolorOnly() {
// For palette/offset changes without recomputing iterations,
// we need to re-render since we don't store iteration buffer.
// This is a trade-off for simplicity; the color pass is fast.
scheduleRender();
}
function updateHUD(maxIter) {
nav.pushHistory();
ui.updateMag(nav.formatMagnification());
ui.updateCenter(nav.formatCenter());
ui.updateIterations(maxIter, iterOverride > 0);
ui.updateMode(nav.state.setMode, nav.state.setMode === 'julia' ? { re: nav.state.juliaCRe, im: nav.state.juliaCIm } : undefined);
ui.updateIterSlider(maxIter);
ui.setActivePalette(nav.state.paletteIndex);
// Save to localStorage
try {
localStorage.setItem('fractal-last-view', JSON.stringify(nav.serialize()));
}
catch { }
}
// ─── Input Handling ──────────────────────────────────────
function setupInputHandlers() {
canvas.addEventListener('mousedown', onMouseDown);
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('mouseup', onMouseUp);
canvas.addEventListener('wheel', onWheel, { passive: false });
canvas.addEventListener('contextmenu', e => e.preventDefault());
// Touch support
let lastTouchDist = 0;
canvas.addEventListener('touchstart', (e) => {
if (e.touches.length === 1) {
isPanning = true;
panStartX = e.touches[0].clientX;
panStartY = e.touches[0].clientY;
}
else if (e.touches.length === 2) {
lastTouchDist = Math.hypot(e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY);
}
e.preventDefault();
}, { passive: false });
canvas.addEventListener('touchmove', (e) => {
if (e.touches.length === 1 && isPanning) {
const dx = e.touches[0].clientX - panStartX;
const dy = e.touches[0].clientY - panStartY;
nav.pan(dx, dy, canvas.width, canvas.height);
panStartX = e.touches[0].clientX;
panStartY = e.touches[0].clientY;
scheduleRender();
}
else if (e.touches.length === 2) {
const dist = Math.hypot(e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY);
const factor = dist / lastTouchDist;
const cx = (e.touches[0].clientX + e.touches[1].clientX) * 0.5;
const cy = (e.touches[0].clientY + e.touches[1].clientY) * 0.5;
nav.zoomToward(cx, cy, canvas.width, canvas.height, factor);
lastTouchDist = dist;
scheduleRender();
}
e.preventDefault();
}, { passive: false });
canvas.addEventListener('touchend', () => { isPanning = false; });
}
function onMouseDown(e) {
const rect = canvas.getBoundingClientRect();
const x = (e.clientX - rect.left) * (canvas.width / rect.width);
const y = (e.clientY - rect.top) * (canvas.height / rect.height);
if (e.shiftKey || e.button === 2) {
// Rectangle zoom
isRectZooming = true;
rectStartX = x;
rectStartY = y;
}
else {
// Pan
isPanning = true;
panStartX = x;
panStartY = y;
}
}
function onMouseMove(e) {
const rect = canvas.getBoundingClientRect();
const x = (e.clientX - rect.left) * (canvas.width / rect.width);
const y = (e.clientY - rect.top) * (canvas.height / rect.height);
if (isRectZooming) {
ui.drawZoomBox(Math.min(rectStartX, x), Math.min(rectStartY, y), Math.max(rectStartX, x), Math.max(rectStartY, y));
}
else if (isPanning) {
const dx = x - panStartX;
const dy = y - panStartY;
nav.pan(dx, dy, canvas.width, canvas.height);
panStartX = x;
panStartY = y;
scheduleRender();
}
lastMouseX = e.clientX;
lastMouseY = e.clientY;
}
function onMouseUp(e) {
if (isRectZooming) {
const rect = canvas.getBoundingClientRect();
const x = (e.clientX - rect.left) * (canvas.width / rect.width);
const y = (e.clientY - rect.top) * (canvas.height / rect.height);
ui.clearZoomBox();
// Only zoom if the rectangle is large enough
if (Math.abs(x - rectStartX) > 5 && Math.abs(y - rectStartY) > 5) {
nav.zoomToRect(Math.min(rectStartX, x), Math.min(rectStartY, y), Math.max(rectStartX, x), Math.max(rectStartY, y), canvas.width, canvas.height);
}
isRectZooming = false;
scheduleRender();
}
else if (isPanning) {
isPanning = false;
scheduleRender(); // final render after pan
}
}
function onWheel(e) {
e.preventDefault();
const rect = canvas.getBoundingClientRect();
const x = (e.clientX - rect.left) * (canvas.width / rect.width);
const y = (e.clientY - rect.top) * (canvas.height / rect.height);
// Zoom factor based on wheel delta
const factor = e.deltaY < 0 ? (1 + types_1.CONFIG.zoomSpeed) : (1 / (1 + types_1.CONFIG.zoomSpeed));
nav.zoomToward(x, y, canvas.width, canvas.height, factor);
scheduleRender();
}
// ─── Keyboard Handling ──────────────────────────────────
function setupKeyboardHandlers() {
window.addEventListener('keydown', (e) => {
const state = nav.state;
const panAmount = Math.max(20, state.magnification * 0.1);
switch (e.key.toLowerCase()) {
case 'arrowleft':
nav.pan(panAmount, 0, canvas.width, canvas.height);
scheduleRender();
e.preventDefault();
break;
case 'arrowright':
nav.pan(-panAmount, 0, canvas.width, canvas.height);
scheduleRender();
e.preventDefault();
break;
case 'arrowup':
nav.pan(0, panAmount, canvas.width, canvas.height);
scheduleRender();
e.preventDefault();
break;
case 'arrowdown':
nav.pan(0, -panAmount, canvas.width, canvas.height);
scheduleRender();
e.preventDefault();
break;
case '=':
case '+':
nav.zoomToward(canvas.width / 2, canvas.height / 2, canvas.width, canvas.height, 1.5);
scheduleRender();
e.preventDefault();
break;
case '-':
nav.zoomToward(canvas.width / 2, canvas.height / 2, canvas.width, canvas.height, 1 / 1.5);
scheduleRender();
e.preventDefault();
break;
case 'j': {
// Julia mode at center of current view
const cRe = state.setMode === 'mandelbrot' ? state.centerRe : state.juliaCRe;
const cIm = state.setMode === 'mandelbrot' ? state.centerIm : state.juliaCIm;
if (state.setMode === 'julia') {
nav.setMandelbrotMode();
}
else {
nav.setJuliaMode(cRe, cIm);
}
scheduleRender();
break;
}
case 'b':
if (nav.goBack())
scheduleRender();
e.preventDefault();
break;
case 'r':
nav.reset();
iterOverride = 0;
document.getElementById('iter-slider').value = String(types_1.CONFIG.maxIterationsBase);
scheduleRender();
e.preventDefault();
break;
case 's': {
// Toggle bookmarks panel
const panel = document.getElementById('bookmarks-panel');
if (panel.style.display === 'block') {
ui.hideBookmarksPanel();
}
else {
saveBookmark();
e.preventDefault();
}
break;
}
case 'p': {
// Cycle palette
const next = (state.paletteIndex + 1) % palettes_1.PALETTES.length;
nav.setPalette(next);
recolorOnly();
e.preventDefault();
break;
}
case '?':
case '/': {
const help = document.getElementById('help-overlay');
help.style.display = help.style.display === 'block' ? 'none' : 'block';
e.preventDefault();
break;
}
}
});
}
// ─── UI Callbacks ────────────────────────────────────────
function setupUICallbacks() {
ui.onPaletteSelect = (index) => {
nav.setPalette(index);
recolorOnly();
};
ui.onColorOffsetChange = (offset) => {
nav.setColorOffset(offset);
recolorOnly();
};
ui.onIterOverrideChange = (iter) => {
iterOverride = iter;
scheduleRender();
};
ui.onReset = () => {
nav.reset();
iterOverride = 0;
document.getElementById('iter-slider').value = String(types_1.CONFIG.maxIterationsBase);
scheduleRender();
};
ui.onBack = () => {
if (nav.goBack())
scheduleRender();
};
ui.onBookmark = () => saveBookmark();
ui.onBookmarkRestore = (index) => {
const bm = bookmarks[index];
if (!bm)
return;
nav.restore(bm.viewState);
iterOverride = 0;
ui.hideBookmarksPanel();
scheduleRender();
};
}
// ─── Bookmarks ──────────────────────────────────────────
function saveBookmark() {
const name = prompt('Bookmark name:', `View ${bookmarks.length + 1}`);
if (!name)
return;
bookmarks.push({
name,
viewState: nav.serialize(),
timestamp: Date.now(),
});
try {
localStorage.setItem('fractal-bookmarks', JSON.stringify(bookmarks));
}
catch { }
ui.showBookmarksPanel(bookmarks.map(b => ({ name: b.name, timestamp: b.timestamp })));
}
function loadBookmarks() {
try {
const data = localStorage.getItem('fractal-bookmarks');
return data ? JSON.parse(data) : [];
}
catch {
return [];
}
}
// ─── Animation Loop ──────────────────────────────────────
function tick() {
ui.updateFps();
requestAnimationFrame(tick);
}
// ─── Start ──────────────────────────────────────────────
init();
};
// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CONFIG = void 0;
// ─── Tunable Constants ────────────────────────────────────────────────
exports.CONFIG = Object.freeze({
// Iteration limits
maxIterationsBase: 200,
maxIterationsScale: 80, // added per log10(magnification)
maxIterationsCap: 15000,
// Zoom behavior
zoomSpeed: 0.3, // wheel zoom factor
minMagnification: 0.1,
maxMagnification: 1e14, // double-precision limit
// Progressive rendering - just a quick initial pass then full resolution
progressivePasses: [1], // single full-resolution pass; add [4, 1] for coarse-then-fine
workerCount: navigator.hardwareConcurrency || 4,
// Julia preview
juliaPreviewSize: 150, // px for the mini Julia preview pane
// UI
hudHideDelayMs: 3000,
});
};
// ── 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.buildColorLUT = buildColorLUT;
exports.getPaletteSwatch = getPaletteSwatch;
// ─── Palette Definitions ──────────────────────────────────────────────
exports.PALETTES = [
{
name: 'Fire',
stops: [
[0.00, 0.00, 0.00, 0.05],
[0.15, 0.40, 0.00, 0.00],
[0.30, 0.80, 0.20, 0.00],
[0.50, 1.00, 0.60, 0.00],
[0.70, 1.00, 1.00, 0.30],
[0.85, 1.00, 1.00, 0.90],
[1.00, 1.00, 1.00, 1.00],
],
},
{
name: 'Ocean',
stops: [
[0.00, 0.00, 0.02, 0.15],
[0.15, 0.00, 0.10, 0.40],
[0.30, 0.00, 0.30, 0.70],
[0.50, 0.00, 0.60, 0.90],
[0.70, 0.20, 0.85, 1.00],
[0.85, 0.60, 0.95, 1.00],
[1.00, 1.00, 1.00, 1.00],
],
},
{
name: 'Monochrome',
stops: [
[0.00, 0.00, 0.00, 0.00],
[0.25, 0.30, 0.30, 0.30],
[0.50, 0.60, 0.60, 0.60],
[0.75, 0.85, 0.85, 0.85],
[1.00, 1.00, 1.00, 1.00],
],
},
{
name: 'Psychedelic',
stops: [
[0.00, 0.50, 0.00, 0.80],
[0.17, 0.90, 0.10, 0.30],
[0.33, 1.00, 0.60, 0.00],
[0.50, 0.20, 0.80, 0.40],
[0.67, 0.00, 0.30, 0.90],
[0.83, 0.80, 0.10, 0.50],
[1.00, 0.50, 0.00, 0.80],
],
},
{
name: 'Electric',
stops: [
[0.00, 0.00, 0.00, 0.20],
[0.15, 0.00, 0.40, 0.80],
[0.30, 0.20, 0.70, 1.00],
[0.50, 0.60, 0.90, 1.00],
[0.70, 1.00, 0.80, 0.40],
[0.85, 1.00, 0.50, 0.20],
[1.00, 1.00, 0.30, 0.60],
],
},
];
// ─── Color Interpolation ──────────────────────────────────────────────
/**
* Pre-compute a color LUT (256 entries) for a palette + offset.
* Returns Uint8Array of length 1024 (RGBA × 256).
*/
function buildColorLUT(palette, offset) {
const lut = new Uint8Array(256 * 4);
const stops = palette.stops;
for (let i = 0; i < 256; i++) {
let t = ((i / 255) + offset) % 1.0;
if (t < 0)
t += 1.0;
// Find surrounding stops
let lo = 0, hi = stops.length - 1;
for (let s = 0; s < stops.length - 1; s++) {
if (stops[s][0] <= t && stops[s + 1][0] >= t) {
lo = s;
hi = s + 1;
break;
}
}
const stopLo = stops[lo];
const stopHi = stops[hi];
const range = stopHi[0] - stopLo[0];
const frac = range > 0 ? (t - stopLo[0]) / range : 0;
// Smoothstep interpolation for smoother gradients
const s = frac * frac * (3 - 2 * frac);
lut[i * 4 + 0] = Math.round(lerp(stopLo[1], stopHi[1], s) * 255);
lut[i * 4 + 1] = Math.round(lerp(stopLo[2], stopHi[2], s) * 255);
lut[i * 4 + 2] = Math.round(lerp(stopLo[3], stopHi[3], s) * 255);
lut[i * 4 + 3] = 255; // opaque
}
return lut;
}
function lerp(a, b, t) {
return a + (b - a) * t;
}
// ─── Palette Swatch Colors for UI ──────────────────────────────────────
function getPaletteSwatch(paletteIndex) {
const p = exports.PALETTES[paletteIndex];
if (!p)
return '#888';
// Use middle stop as representative color
const mid = p.stops[Math.floor(p.stops.length / 2)];
return `rgb(${Math.round(mid[1] * 255)}, ${Math.round(mid[2] * 255)}, ${Math.round(mid[3] * 255)})`;
}
};
// ── module: src/math.ts ──
__mods["src/math.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.mandelbrotIteration = mandelbrotIteration;
exports.juliaIteration = juliaIteration;
exports.computePixel = computePixel;
exports.iterationsAt = iterationsAt;
exports.computeMaxIterations = computeMaxIterations;
exports.iterationToColorIndex = iterationToColorIndex;
const types_1 = require("./types");
// ─── Fractal Math ──────────────────────────────────────────────────────
/**
* Compute the smooth (continuous) iteration count for a point.
* Returns maxIterations if the point is in the set (does not escape).
* Uses the normalized iteration count: n + 1 - log2(log|z|)
*/
function mandelbrotIteration(cRe, cIm, maxIter) {
let zRe = 0;
let zIm = 0;
let zRe2 = 0;
let zIm2 = 0;
for (let n = 0; n < maxIter; n++) {
// z = z² + c
zIm = 2 * zRe * zIm + cIm;
zRe = zRe2 - zIm2 + cRe;
zRe2 = zRe * zRe;
zIm2 = zIm * zIm;
if (zRe2 + zIm2 > 256) {
// Smooth iteration count: n + 1 - log2(log|z|)
const logZn = Math.log(zRe2 + zIm2) * 0.5;
return n + 1 - Math.log(logZn) / Math.LN2;
}
}
return maxIter; // point is in the set
}
/**
* Julia set iteration for a given c-value.
*/
function juliaIteration(zRe0, zIm0, cRe, cIm, maxIter) {
let zRe = zRe0;
let zIm = zIm0;
let zRe2 = zRe * zRe;
let zIm2 = zIm * zIm;
for (let n = 0; n < maxIter; n++) {
// z = z² + c
zIm = 2 * zRe * zIm + cIm;
zRe = zRe2 - zIm2 + cRe;
zRe2 = zRe * zRe;
zIm2 = zIm * zIm;
if (zRe2 + zIm2 > 256) {
const logZn = Math.log(zRe2 + zIm2) * 0.5;
return n + 1 - Math.log(logZn) / Math.LN2;
}
}
return maxIter; // point is in the set
}
/**
* Compute iterations for a single pixel (dispatches to correct algorithm).
*/
function computePixel(x, y, width, height, centerRe, centerIm, magnification, setMode, juliaCRe, juliaCIm, maxIter) {
// Convert pixel to complex plane coordinates
const aspect = width / height;
const scale = magnification / height;
let cRe, cIm;
if (setMode === 'mandelbrot') {
cRe = centerRe + (x - width * 0.5) * scale;
cIm = centerIm + (y - height * 0.5) * scale / aspect;
return mandelbrotIteration(cRe, cIm, maxIter);
}
else {
const zRe = centerRe + (x - width * 0.5) * scale;
const zIm = centerIm + (y - height * 0.5) * scale / aspect;
return juliaIteration(zRe, zIm, juliaCRe, juliaCIm, maxIter);
}
}
/**
* Compute iterations at a given complex coordinate for Mandelbrot set.
* Used by the debug interface.
*/
function iterationsAt(re, im, maxIter) {
return mandelbrotIteration(re, im, maxIter ?? types_1.CONFIG.maxIterationsBase);
}
/**
* Calculate appropriate iteration count based on magnification.
* Deeper zooms need more iterations to resolve fine structure.
*/
function computeMaxIterations(magnification) {
const logMag = Math.log10(Math.max(1, magnification));
return Math.min(types_1.CONFIG.maxIterationsBase + types_1.CONFIG.maxIterationsScale * logMag, types_1.CONFIG.maxIterationsCap);
}
/**
* Convert smooth iteration count to a color index [0, 255].
*/
function iterationToColorIndex(smoothIter, maxIter) {
if (smoothIter >= maxIter)
return -1; // interior point (black)
const t = Math.max(0, smoothIter / maxIter);
return Math.min(255, Math.floor(t * 256));
}
};
// ── module: src/renderer.ts ──
__mods["src/renderer.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.FractalRenderer = void 0;
const types_1 = require("./types");
const palettes_1 = require("./palettes");
// ─── Worker Code (inline via Blob URL) ────────────────────────────────
const WORKER_CODE = `
self.onmessage = function(e) {
const job = e.data.job;
const params = e.data.params;
const lut = e.data.lut;
const x0 = job.x0, x1 = job.x1, y0 = job.y0, y1 = job.y1;
const p = params;
const aspect = p.width / p.height;
const scale = p.magnification / p.height;
const tileW = x1 - x0;
const buf = new Uint8Array(tileW * (y1 - y0) * 4);
for (let y = y0; y < y1; y++) {
const cIm_base = p.centerIm + (y - p.height * 0.5) * scale / aspect;
for (let x = x0; x < x1; x++) {
let zRe, zIm, cRe, cIm;
if (p.setMode === 'mandelbrot') {
cRe = p.centerRe + (x - p.width * 0.5) * scale;
cIm = cIm_base;
zRe = 0; zIm = 0;
} else {
zRe = p.centerRe + (x - p.width * 0.5) * scale;
zIm = cIm_base;
cRe = p.juliaCRe; cIm = p.juliaCIm;
}
let iter = p.maxIterations;
let zr2 = zRe * zRe, zi2 = zIm * zIm;
for (let n = 0; n < p.maxIterations; n++) {
const newZi = 2 * zRe * zIm + cIm;
zRe = zr2 - zi2 + cRe;
zIm = newZi;
zr2 = zRe * zRe;
zi2 = zIm * zIm;
if (zr2 + zi2 > 256) {
const logZn = Math.log(zr2 + zi2) * 0.5;
iter = n + 1 - Math.log(logZn) / Math.LN2;
break;
}
}
const col = x - x0;
const row = y - y0;
const idx = (row * tileW + col) * 4;
if (iter >= p.maxIterations) {
buf[idx] = 0; buf[idx+1] = 0; buf[idx+2] = 0; buf[idx+3] = 255;
} else {
const t = Math.max(0, iter / p.maxIterations);
const ci = Math.min(255, Math.floor(t * 256));
const li = ci * 4;
buf[idx] = lut[li]; buf[idx+1] = lut[li+1];
buf[idx+2] = lut[li+2]; buf[idx+3] = 255;
}
}
}
self.postMessage({ job, buffer: buf }, [buf.buffer]);
};
`;
// ─── Renderer Class ────────────────────────────────────────────────────
class FractalRenderer {
constructor(canvas) {
this.imageData = null;
this.workers = [];
this.currentParams = null;
this.renderId = 0;
this.isRendering = false;
this.canvas = canvas;
this.ctx = canvas.getContext('2d', { willReadFrequently: false });
this.workerBlobUrl = URL.createObjectURL(new Blob([WORKER_CODE], { type: 'application/javascript' }));
this.initWorkers();
}
initWorkers() {
const count = Math.min(types_1.CONFIG.workerCount || 4, 8);
for (let i = 0; i < count; i++) {
try {
const w = new Worker(this.workerBlobUrl);
this.workers.push(w);
}
catch {
break;
}
}
}
/** Start a progressive render. */
async render(params) {
const newRenderId = ++this.renderId;
this.currentParams = params;
this.isRendering = true;
if (!this.imageData || this.imageData.width !== params.width || this.imageData.height !== params.height) {
this.imageData = new ImageData(params.width, params.height);
}
const lut = (0, palettes_1.buildColorLUT)(palettes_1.PALETTES[params.paletteIndex], params.colorOffset);
// Progressive passes: coarse to fine (main thread for responsiveness)
for (const downscale of types_1.CONFIG.progressivePasses) {
if (this.renderId !== newRenderId)
return;
await this.renderMainThread(params, lut, downscale, newRenderId);
}
this.isRendering = false;
this.onComplete?.();
}
async renderMainThread(params, lut, downscale, renderId) {
const { width, height } = params;
const aspect = width / height;
const scale = params.magnification / height;
// Process in row chunks to keep UI responsive
const chunkRows = Math.max(1, Math.floor(height / downscale) / 30);
for (let yBase = 0; yBase < height; yBase += downscale * chunkRows) {
if (this.renderId !== renderId)
return;
const yEnd = Math.min(yBase + downscale * chunkRows, height);
for (let y = yBase; y < yEnd; y += downscale) {
const cIm_base = params.centerIm + (y - height * 0.5) * scale / aspect;
for (let x = 0; x < width; x += downscale) {
let zRe, zIm, cRe, cIm;
if (params.setMode === 'mandelbrot') {
cRe = params.centerRe + (x - width * 0.5) * scale;
cIm = cIm_base;
zRe = 0;
zIm = 0;
}
else {
zRe = params.centerRe + (x - width * 0.5) * scale;
zIm = cIm_base;
cRe = params.juliaCRe;
cIm = params.juliaCIm;
}
let iter = params.maxIterations;
let zr2 = zRe * zRe, zi2 = zIm * zIm;
for (let n = 0; n < params.maxIterations; n++) {
const newZi = 2 * zRe * zIm + cIm;
zRe = zr2 - zi2 + cRe;
zIm = newZi;
zr2 = zRe * zRe;
zi2 = zIm * zIm;
if (zr2 + zi2 > 256) {
const logZn = Math.log(zr2 + zi2) * 0.5;
iter = n + 1 - Math.log(logZn) / Math.LN2;
break;
}
}
// Fill the downscale×downscale block with this pixel's color
const isInterior = iter >= params.maxIterations;
let r, g, b;
if (isInterior) {
r = 0;
g = 0;
b = 0;
}
else {
const t = Math.max(0, iter / params.maxIterations);
const ci = Math.min(255, Math.floor(t * 256));
const li = ci * 4;
r = lut[li];
g = lut[li + 1];
b = lut[li + 2];
}
for (let dy = 0; dy < downscale && y + dy < height; dy++) {
const rowOffset = (y + dy) * width;
for (let dx = 0; dx < downscale && x + dx < width; dx++) {
const idx = (rowOffset + x + dx) * 4;
this.imageData.data[idx] = r;
this.imageData.data[idx + 1] = g;
this.imageData.data[idx + 2] = b;
this.imageData.data[idx + 3] = 255;
}
}
}
}
// Blit to canvas after each chunk
if (this.imageData) {
this.ctx.putImageData(this.imageData, 0, 0);
}
// Yield to browser for UI responsiveness
await new Promise(r => setTimeout(r, 0));
}
}
/** Cancel any in-flight render. */
cancel() {
++this.renderId;
}
setOnComplete(cb) {
this.onComplete = cb;
}
getParams() {
return this.currentParams;
}
isRenderingActive() {
return this.isRendering;
}
destroy() {
this.cancel();
for (const w of this.workers) {
try {
w.terminate();
}
catch { }
}
URL.revokeObjectURL(this.workerBlobUrl);
}
}
exports.FractalRenderer = FractalRenderer;
};
// ── module: src/navigation.ts ──
__mods["src/navigation.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Navigation = void 0;
const types_1 = require("./types");
// ─── View State & Navigation ──────────────────────────────────────────
const DEFAULT_VIEW = Object.freeze({
centerRe: -0.5,
centerIm: 0,
magnification: 3,
paletteIndex: 0,
colorOffset: 0,
setMode: 'mandelbrot',
juliaCRe: -0.7269,
juliaCIm: 0.1889,
});
class Navigation {
constructor() {
this.history = [];
this.historyIndex = -1;
this.viewState = { ...DEFAULT_VIEW };
}
get state() {
return this.viewState;
}
/** Reset to default view. */
reset() {
this.pushHistory();
this.viewState = { ...DEFAULT_VIEW };
}
/** Push current state onto history stack (for back button). */
pushHistory(maxLen = 50) {
// Don't save if same as last entry
const last = this.history[this.historyIndex];
if (last && viewsEqual(last, this.viewState))
return;
// Trim forward history if we're not at the end
this.history = this.history.slice(0, this.historyIndex + 1);
this.history.push({ ...this.viewState });
if (this.history.length > maxLen) {
this.history.shift();
}
this.historyIndex = this.history.length - 1;
}
/** Go back in history. */
goBack() {
if (this.historyIndex <= 0)
return false;
this.historyIndex--;
this.viewState = { ...this.history[this.historyIndex] };
return true;
}
/** Zoom toward a point by a factor. Returns the new magnification. */
zoomToward(screenX, screenY, canvasWidth, canvasHeight, factor) {
const aspect = canvasWidth / canvasHeight;
const scale = this.viewState.magnification / canvasHeight;
// Complex coordinate at cursor
const targetRe = this.viewState.centerRe + (screenX - canvasWidth * 0.5) * scale;
const targetIm = this.viewState.centerIm + (screenY - canvasHeight * 0.5) * scale / aspect;
let newMag = this.viewState.magnification * factor;
newMag = Math.max(types_1.CONFIG.minMagnification, Math.min(types_1.CONFIG.maxMagnification, newMag));
// Move center toward cursor
const newScale = newMag / canvasHeight;
this.viewState.centerRe = targetRe - (screenX - canvasWidth * 0.5) * newScale;
this.viewState.centerIm = targetIm - (screenY - canvasHeight * 0.5) * newScale / aspect;
this.viewState.magnification = newMag;
}
/** Pan by a delta in screen pixels. */
pan(dx, dy, canvasWidth, canvasHeight) {
const aspect = canvasWidth / canvasHeight;
const scale = this.viewState.magnification / canvasHeight;
this.viewState.centerRe -= dx * scale;
this.viewState.centerIm -= dy * scale / aspect;
}
/** Zoom into a rectangular region (in screen pixel coordinates). */
zoomToRect(x0, y0, x1, y1, canvasWidth, canvasHeight) {
const aspect = canvasWidth / canvasHeight;
const scale = this.viewState.magnification / canvasHeight;
// Complex coordinates of rect corners
const re0 = this.viewState.centerRe + (x0 - canvasWidth * 0.5) * scale;
const im0 = this.viewState.centerIm + (y0 - canvasHeight * 0.5) * scale / aspect;
const re1 = this.viewState.centerRe + (x1 - canvasWidth * 0.5) * scale;
const im1 = this.viewState.centerIm + (y1 - canvasHeight * 0.5) * scale / aspect;
// New center is midpoint of rect
const newCenterRe = (re0 + re1) * 0.5;
const newCenterIm = (im0 + im1) * 0.5;
// New magnification: fit the rect to the canvas
const rectWidth = Math.abs(re1 - re0);
const rectHeight = Math.abs(im1 - im0) * aspect;
const zoomFactor = Math.max(canvasWidth / (rectWidth * canvasHeight), 1);
let newMag = this.viewState.magnification * zoomFactor;
newMag = Math.min(types_1.CONFIG.maxMagnification, newMag);
this.viewState.centerRe = newCenterRe;
this.viewState.centerIm = newCenterIm;
this.viewState.magnification = newMag;
}
/** Set Julia mode with a given c-value. */
setJuliaMode(cRe, cIm) {
this.pushHistory();
this.viewState.setMode = 'julia';
this.viewState.juliaCRe = cRe;
this.viewState.juliaCIm = cIm;
}
/** Switch back to Mandelbrot mode. */
setMandelbrotMode() {
this.pushHistory();
this.viewState.setMode = 'mandelbrot';
}
/** Set palette index. */
setPalette(index) {
this.viewState.paletteIndex = index;
}
/** Set color offset for palette cycling. */
setColorOffset(offset) {
this.viewState.colorOffset = ((offset % 1) + 1) % 1;
}
/** Get a serializable copy of the current view state. */
serialize() {
return { ...this.viewState };
}
/** Restore from a serialized view state. */
restore(state) {
this.pushHistory();
this.viewState = { ...state };
}
/** Load view state from URL hash if present. */
loadFromHash() {
const hash = window.location.hash.slice(1);
if (!hash)
return false;
try {
const params = new URLSearchParams(hash);
this.viewState.centerRe = parseFloat(params.get('cr') ?? '0');
this.viewState.centerIm = parseFloat(params.get('ci') ?? '0');
this.viewState.magnification = Math.max(types_1.CONFIG.minMagnification, parseFloat(params.get('mag') ?? '3'));
this.viewState.paletteIndex = parseInt(params.get('pal') ?? '0', 10);
this.viewState.colorOffset = parseFloat(params.get('coff') ?? '0');
if (params.get('mode') === 'julia') {
this.viewState.setMode = 'julia';
this.viewState.juliaCRe = parseFloat(params.get('jcr') ?? '-0.7269');
this.viewState.juliaCIm = parseFloat(params.get('jci') ?? '0.1889');
}
else {
this.viewState.setMode = 'mandelbrot';
}
return true;
}
catch {
return false;
}
}
/** Save current view state to URL hash. */
saveToHash() {
const p = new URLSearchParams();
p.set('cr', this.viewState.centerRe.toPrecision(15));
p.set('ci', this.viewState.centerIm.toPrecision(15));
p.set('mag', this.viewState.magnification.toExponential(6));
p.set('pal', String(this.viewState.paletteIndex));
p.set('coff', this.viewState.colorOffset.toFixed(4));
if (this.viewState.setMode === 'julia') {
p.set('mode', 'julia');
p.set('jcr', this.viewState.juliaCRe.toPrecision(15));
p.set('jci', this.viewState.juliaCIm.toPrecision(15));
}
else {
p.set('mode', 'mandelbrot');
}
window.location.hash = p.toString();
}
/** Format magnification for display. */
formatMagnification() {
const m = this.viewState.magnification;
if (m >= 1e6)
return `×${m.toExponential(2).replace('e+', 'e')}`;
if (m >= 1000)
return `×${Math.round(m).toLocaleString()}`;
return `×${m.toFixed(2)}`;
}
/** Format center coordinates for display. */
formatCenter() {
const prec = Math.min(15, Math.max(6, Math.floor(Math.log10(this.viewState.magnification)) + 4));
return `${this.viewState.centerRe.toPrecision(prec)}, ${this.viewState.centerIm.toPrecision(prec)}i`;
}
}
exports.Navigation = Navigation;
function viewsEqual(a, b) {
return (a.centerRe === b.centerRe &&
a.centerIm === b.centerIm &&
a.magnification === b.magnification &&
a.paletteIndex === b.paletteIndex &&
Math.abs(a.colorOffset - b.colorOffset) < 0.001 &&
a.setMode === b.setMode &&
a.juliaCRe === b.juliaCRe &&
a.juliaCIm === b.juliaCIm);
}
};
// ── module: src/ui.ts ──
__mods["src/ui.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.UIManager = void 0;
const types_1 = require("./types");
const palettes_1 = require("./palettes");
// ─── UI Manager ────────────────────────────────────────────────────────
class UIManager {
constructor() {
this.juliaPreviewCanvas = null;
this.hudVisible = true;
this.hideTimeout = null;
this.lastFpsTime = performance.now();
this.frameCount = 0;
this.currentFps = 60;
this.hudEl = document.getElementById('hud');
this.magEl = document.getElementById('mag-display');
this.centerEl = document.getElementById('center-display');
this.iterEl = document.getElementById('iter-display');
this.fpsEl = document.getElementById('fps-display');
this.modeEl = document.getElementById('mode-display');
this.paletteContainer = document.getElementById('palette-container');
this.zoomBoxCanvas = document.getElementById('zoom-box-canvas');
this.colorSlider = document.getElementById('color-slider');
this.iterSlider = document.getElementById('iter-slider');
this.resetBtn = document.getElementById('reset-btn');
this.backBtn = document.getElementById('back-btn');
this.bookmarkBtn = document.getElementById('bookmark-btn');
this.bookmarksPanel = document.getElementById('bookmarks-panel');
// Build palette swatches
this.buildPaletteSwatches();
// Setup event listeners
this.setupEventListeners();
}
buildPaletteSwatches() {
palettes_1.PALETTES.forEach((palette, i) => {
const chip = document.createElement('div');
chip.className = 'palette-chip';
chip.dataset.index = String(i);
chip.title = palette.name;
// Create gradient swatch
let gradient = 'linear-gradient(to right, ';
const stops = palette.stops.map(s => {
return `rgb(${Math.round(s[1] * 255)},${Math.round(s[2] * 255)},${Math.round(s[3] * 255)}) ${s[0] * 100}%`;
});
gradient += stops.join(',') + ')';
chip.style.background = gradient;
this.paletteContainer.appendChild(chip);
});
}
setupEventListeners() {
// Palette selection
this.paletteContainer.addEventListener('click', (e) => {
const chip = e.target.closest('.palette-chip');
if (!chip)
return;
const index = parseInt(chip.dataset.index, 10);
this.onPaletteSelect?.(index);
});
// Color offset slider
this.colorSlider.addEventListener('input', () => {
const val = parseFloat(this.colorSlider.value);
this.onColorOffsetChange?.(val);
});
// Iteration override slider
this.iterSlider.addEventListener('input', () => {
const val = parseInt(this.iterSlider.value, 10);
this.onIterOverrideChange?.(val);
});
// Reset button
this.resetBtn.addEventListener('click', () => {
this.onReset?.();
});
// Back button
this.backBtn.addEventListener('click', () => {
this.onBack?.();
});
// Bookmark button
this.bookmarkBtn.addEventListener('click', () => {
this.onBookmark?.();
});
// Show HUD on mouse move, hide after delay
document.addEventListener('mousemove', () => this.showHud());
document.addEventListener('keydown', () => this.showHud());
}
showHud() {
if (!this.hudVisible) {
this.hudEl.style.opacity = '1';
this.hudVisible = true;
}
if (this.hideTimeout !== null) {
clearTimeout(this.hideTimeout);
}
this.hideTimeout = window.setTimeout(() => {
this.hudEl.style.opacity = '0.3';
this.hudVisible = false;
}, types_1.CONFIG.hudHideDelayMs);
}
// ─── Update Methods ──────────────────────────────────────────────
updateMag(mag) {
this.magEl.textContent = mag;
}
updateCenter(center) {
this.centerEl.textContent = center;
}
updateIterations(iter, override) {
const text = override ? `${iter} (override)` : `iterations: ${iter}`;
this.iterEl.textContent = text;
}
updateFps() {
this.frameCount++;
const now = performance.now();
if (now - this.lastFpsTime >= 500) {
this.currentFps = Math.round(this.frameCount / ((now - this.lastFpsTime) / 1000));
this.fpsEl.textContent = `${this.currentFps} fps`;
this.frameCount = 0;
this.lastFpsTime = now;
}
}
updateMode(mode, juliaC) {
if (mode === 'julia') {
this.modeEl.textContent = `Julia (${juliaC?.re.toFixed(4)}, ${juliaC?.im.toFixed(4)}i)`;
}
else {
this.modeEl.textContent = 'Mandelbrot';
}
}
updateIterSlider(maxAuto) {
// Update slider max to reflect auto-computed iterations
const currentVal = parseInt(this.iterSlider.value, 10);
if (currentVal <= 0 || currentVal > types_1.CONFIG.maxIterationsCap) {
this.iterSlider.value = String(Math.min(maxAuto, types_1.CONFIG.maxIterationsCap));
}
this.iterSlider.max = String(types_1.CONFIG.maxIterationsCap);
}
setActivePalette(index) {
const chips = this.paletteContainer.querySelectorAll('.palette-chip');
chips.forEach((chip, i) => {
chip.classList.toggle('active', i === index);
});
}
// ─── Zoom Box Overlay ──────────────────────────────────────────────
drawZoomBox(x0, y0, x1, y1) {
const ctx = this.zoomBoxCanvas.getContext('2d');
ctx.clearRect(0, 0, this.zoomBoxCanvas.width, this.zoomBoxCanvas.height);
if (x1 <= x0 || y1 <= y0)
return;
// Inverted-color border effect
ctx.strokeStyle = 'rgba(255, 255, 255, 0.9)';
ctx.lineWidth = 2;
ctx.setLineDash([6, 4]);
ctx.strokeRect(x0, y0, x1 - x0, y1 - y0);
ctx.setLineDash([]);
// Semi-transparent fill
ctx.fillStyle = 'rgba(255, 255, 255, 0.08)';
ctx.fillRect(x0, y0, x1 - x0, y1 - y0);
// Corner markers
const cornerLen = 12;
ctx.strokeStyle = 'rgba(255, 255, 255, 0.9)';
ctx.lineWidth = 2;
this.drawCorner(ctx, x0, y0, 1, 1);
this.drawCorner(ctx, x1, y0, -1, 1);
this.drawCorner(ctx, x0, y1, 1, -1);
this.drawCorner(ctx, x1, y1, -1, -1);
// Label with dimensions
const label = `${Math.round(x1 - x0)}×${Math.round(y1 - y0)}`;
ctx.font = '11px monospace';
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
const metrics = ctx.measureText(label);
const lx = x0 + 4;
const ly = y0 - 6;
ctx.fillRect(lx - 2, ly - 12, metrics.width + 4, 16);
ctx.fillStyle = 'rgba(0, 0, 0, 0.8)';
ctx.fillText(label, lx, ly);
}
drawCorner(ctx, x, y, dx, dy) {
const len = 12;
ctx.beginPath();
ctx.moveTo(x + dx * len, y);
ctx.lineTo(x, y);
ctx.lineTo(x, y + dy * len);
ctx.stroke();
}
clearZoomBox() {
const ctx = this.zoomBoxCanvas.getContext('2d');
ctx.clearRect(0, 0, this.zoomBoxCanvas.width, this.zoomBoxCanvas.height);
}
// ─── Julia Preview ────────────────────────────────────────────────
showJuliaPreview() {
if (!this.juliaPreviewCanvas) {
const container = document.getElementById('julia-preview-container');
this.juliaPreviewCanvas = document.createElement('canvas');
this.juliaPreviewCanvas.id = 'julia-preview';
this.juliaPreviewCanvas.width = types_1.CONFIG.juliaPreviewSize;
this.juliaPreviewCanvas.height = types_1.CONFIG.juliaPreviewSize;
container.appendChild(this.juliaPreviewCanvas);
}
this.juliaPreviewCanvas.parentElement.style.display = 'block';
}
hideJuliaPreview() {
const container = document.getElementById('julia-preview-container');
if (container)
container.style.display = 'none';
}
// ─── Bookmarks Panel ──────────────────────────────────────────────
showBookmarksPanel(bookmarks) {
this.bookmarksPanel.innerHTML = '';
if (bookmarks.length === 0) {
this.bookmarksPanel.innerHTML = '<div class="bookmark-empty">No bookmarks saved</div>';
}
else {
bookmarks.forEach((bm, i) => {
const item = document.createElement('div');
item.className = 'bookmark-item';
item.textContent = `${bm.name} (${new Date(bm.timestamp).toLocaleTimeString()})`;
item.addEventListener('click', () => this.onBookmarkRestore?.(i));
this.bookmarksPanel.appendChild(item);
});
}
this.bookmarksPanel.style.display = 'block';
}
hideBookmarksPanel() {
this.bookmarksPanel.style.display = 'none';
}
// ─── Resize ──────────────────────────────────────────────────────
resize(w, h) {
this.zoomBoxCanvas.width = w;
this.zoomBoxCanvas.height = h;
}
}
exports.UIManager = UIManager;
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen3.6-27b-mtp","provider":"lmstudio","persona":"typescript","sessionId":"d79c8b2f-2221-495d-b031-08e14386a8a3","tokensIn":5106575,"tokensOut":76300,"tokensTotal":5182875,"turns":90,"toolCalls":89,"failedToolCalls":2,"timestamp":"2026-07-30T21:28:24.777Z"} -->