Fractal Explorer
Deepseek v4 Flash 0731 · 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 60,269 bytes · SHA-256 83c429b906ce
<!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: base ── */
html,body{margin:0;padding:0;height:100%;background:#000;overflow:hidden;font-family:ui-monospace,Menlo,Consolas,monospace;}
#app{position:fixed;inset:0;}
#fractal{position:absolute;inset:0;width:100%;height:100%;display:block;cursor:crosshair;touch-action:none;}
/* ── css: hud ── */
#hud{position:absolute;left:14px;top:14px;z-index:5;color:#dfe7ff;
background:rgba(10,14,30,.55);backdrop-filter:blur(10px) saturate(1.2);
-webkit-backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,.12);
border-radius:12px;padding:10px 12px;font-size:12px;line-height:1.5;
box-shadow:0 8px 30px rgba(0,0,0,.4);max-width:300px;transition:opacity .5s,transform .5s;}
#hud.idle{opacity:0;transform:translateY(-6px);pointer-events:none;}
#hud-top{display:flex;gap:10px;flex-wrap:wrap;align-items:baseline;}
.chip{background:linear-gradient(135deg,#5b8cff,#9a5bff);padding:1px 9px;border-radius:999px;
font-weight:600;letter-spacing:.03em;color:#fff;}
.stat{opacity:.85;}
.stat.warn{color:#ffb84d;background:rgba(255,150,0,.15);padding:0 6px;border-radius:6px;opacity:1;}
#hud-controls{margin-top:10px;border-top:1px solid rgba(255,255,255,.1);padding-top:8px;}
#palettes{display:flex;gap:6px;margin-bottom:8px;flex-wrap:wrap;}
.swatch{width:26px;height:18px;border-radius:5px;border:1px solid rgba(255,255,255,.25);
cursor:pointer;opacity:.55;transition:opacity .15s,transform .15s;}
.swatch.on{opacity:1;transform:scale(1.12);border-color:#fff;box-shadow:0 0 8px rgba(255,255,255,.5);}
.sliders{display:flex;gap:14px;flex-wrap:wrap;align-items:center;}
.sliders label{display:flex;align-items:center;gap:5px;opacity:.85;}
input[type=range]{accent-color:#6c8cff;height:4px;width:110px;}
.btns{display:flex;gap:6px;margin-top:8px;flex-wrap:wrap;}
.btns button,.bm-row button{background:rgba(255,255,255,.08);color:#dfe7ff;border:1px solid rgba(255,255,255,.14);
border-radius:7px;padding:4px 9px;cursor:pointer;font-size:12px;font-family:inherit;}
.btns button:hover,.bm-row button:hover{background:rgba(255,255,255,.18);}
#bmarks{margin-top:10px;border-top:1px solid rgba(255,255,255,.1);padding-top:8px;}
.bm-row{display:flex;gap:6px;margin-bottom:6px;}
#bm-name{flex:1;background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.15);
color:#fff;border-radius:7px;padding:4px 8px;font-family:inherit;}
#bm-list{list-style:none;margin:0;padding:0;max-height:140px;overflow:auto;}
#bm-list li{display:flex;align-items:center;gap:8px;padding:3px 0;justify-content:space-between;border-bottom:1px solid rgba(255,255,255,.06);}
#bm-list .nm{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:150px;}
#bm-list button{background:none;border:none;color:#8fa4ff;cursor:pointer;font-size:12px;}
#help{margin-top:10px;border-top:1px solid rgba(255,255,255,.1);padding-top:8px;font-size:11px;opacity:.85;line-height:1.6;}
#preview-wrap{position:absolute;top:14px;right:14px;z-index:4;text-align:center;
background:rgba(10,14,30,.5);backdrop-filter:blur(8px);border:1px solid rgba(255,255,255,.12);
border-radius:10px;padding:6px;}
#preview-label{font-size:10px;color:#aab8e8;margin-bottom:4px;letter-spacing:.04em;}
/* ── css: overlays ── */
#zoombox{position:absolute;z-index:3;display:none;pointer-events:none;
outline:1.5px dashed #fff;outline-offset:-1.5px;
box-shadow:0 0 0 1px rgba(0,0,0,.6), inset 0 0 0 1px rgba(0,0,0,.6);
background:rgba(255,255,255,.06);mix-blend-mode:difference;}
#reticle{position:absolute;z-index:2;display:none;pointer-events:none;}
#reticle .hr,#reticle .vr{position:absolute;background:rgba(255,255,255,.7);
box-shadow:0 0 0 1px rgba(0,0,0,.5);}
#reticle .hr{height:1px;width:100%;}
#reticle .vr{width:1px;height:100%;}
</style>
</head>
<body>
<div id="app"><canvas id="fractal"></canvas>
<div id="preview-wrap" hidden="">
<div id="preview-label">Julia preview</div>
<canvas id="preview" width="112" height="112"></canvas>
</div>
<div id="reticle"><i class="hr"></i><i class="vr"></i></div>
<div id="zoombox"></div>
<div id="hud">
<div id="hud-top">
<span class="chip" id="mode-chip">Mandelbrot</span>
<span class="stat" id="mag">×1.0e0</span>
<span class="stat" id="center">c = 0 + 0i</span>
<span class="stat" id="iters">iter 220</span>
<span class="stat" id="fps">0 fps</span>
<span class="stat warn" id="warn" hidden="">precision limit</span>
</div>
<div id="hud-controls">
<div id="palettes"></div>
<div class="sliders">
<label>Cycle <input id="offset" type="range" min="0" max="99" value="0"></label>
<label>Iter <input id="iter" type="range" min="30" max="200000" value="300"><button id="iter-auto" title="auto iter limit">AUTO</button></label>
</div>
<div class="btns">
<button id="btn-back" title="Back (B)">◀ Back</button>
<button id="btn-reset" title="Reset view (0)">Reset</button>
<button id="btn-julia" title="Julia (J)">Julia</button>
<button id="btn-bmarks" title="Bookmarks (K)">★</button>
<button id="btn-help" title="Help (H)">?</button>
</div>
</div>
<div id="bmarks" hidden="">
<div class="bm-row"><input id="bm-name" placeholder="Bookmark name"><button id="bm-save">Save</button></div>
<ul id="bm-list"></ul>
</div>
<div id="help" hidden="">
<b>Drag</b> = zoom box <b>Shift-drag / right-drag</b> = pan <b>Wheel</b> = zoom to cursor<br>
<b>Click</b> (Mandelbrot) = open Julia there <b>J</b> = toggle Julia<br>
<b>Arrows</b> = pan <b>+/-</b> = zoom <b>0</b> = reset <b>B</b> = back <b>1-4</b> = palette
</div>
</div></div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./config":"src/config.ts","./state":"src/state.ts","./actions":"src/actions.ts","./render":"src/render.ts","./preview":"src/preview.ts","./input":"src/input.ts","./hud":"src/hud.ts","./bookmarks":"src/bookmarks.ts","./fractal":"src/fractal.ts"},"src/fractal.ts":{"./config":"src/config.ts"},"src/palettes.ts":{"./config":"src/config.ts"},"src/state.ts":{"./fractal":"src/fractal.ts"},"src/actions.ts":{"./state":"src/state.ts","./config":"src/config.ts","./fractal":"src/fractal.ts"},"src/render.ts":{"./config":"src/config.ts","./state":"src/state.ts","./fractal":"src/fractal.ts","./palettes":"src/palettes.ts"},"src/preview.ts":{"./config":"src/config.ts","./state":"src/state.ts","./fractal":"src/fractal.ts","./palettes":"src/palettes.ts"},"src/input.ts":{"./config":"src/config.ts","./state":"src/state.ts","./actions":"src/actions.ts","./preview":"src/preview.ts"},"src/hud.ts":{"./config":"src/config.ts","./state":"src/state.ts","./actions":"src/actions.ts","./palettes":"src/palettes.ts","./render":"src/render.ts"},"src/bookmarks.ts":{"./config":"src/config.ts","./state":"src/state.ts","./actions":"src/actions.ts","./fractal":"src/fractal.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";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
// ============================================================
// ENTRY — wire renderer, preview, input, HUD, bookmarks, resize
// and expose a headless debug interface on `window.fractal`.
// ============================================================
const config_1 = require("./config");
const state_1 = require("./state");
const act = __importStar(require("./actions"));
const render_1 = require("./render");
const preview_1 = require("./preview");
const input_1 = require("./input");
const hud_1 = require("./hud");
const bookmarks_1 = require("./bookmarks");
const fractal_1 = require("./fractal");
const canvas = document.getElementById('fractal');
const renderer = new render_1.Renderer(canvas);
const preview = new preview_1.Preview(document.getElementById('preview'));
const hud = (0, hud_1.setupHud)(renderer);
// ------- central callbacks -----------------------------------
state_1.state.onViewChange = () => { renderer.renderView(); hud.update(); (0, bookmarks_1.syncHash)(); };
state_1.state.onActivity = () => { hud.activity(); };
function resize() {
const dpr = Math.min(window.devicePixelRatio || 1, config_1.CONFIG.MAX_DPR);
const W = Math.max(2, Math.round(window.innerWidth * dpr));
const H = Math.max(2, Math.round(window.innerHeight * dpr));
renderer.setSize(W, H);
act.setSize(W, H);
renderer.renderView();
}
window.addEventListener('resize', resize);
// ------- debug interface -------------------------------------
const win = window;
win.fractal = {
/** Integer escape-iteration count for the current set (interior -> limit). */
iterationsAt(re, im) {
return (0, fractal_1.iterationsAt)(re, im, state_1.state.mode, state_1.state.cRe, state_1.state.cIm, state_1.state.limit);
},
/** Set the view. `scale` = complex units per pixel (same in x & y). */
setView(v) {
if (v.re !== undefined)
state_1.state.re = v.re;
if (v.im !== undefined)
state_1.state.im = v.im;
if (v.scale !== undefined)
state_1.state.step = v.scale;
if (v.cRe !== undefined)
state_1.state.cRe = v.cRe;
if (v.cIm !== undefined)
state_1.state.cIm = v.cIm;
if (v.limit !== undefined) {
state_1.state.limit = v.limit;
state_1.state.limitAuto = false;
}
if (v.mode !== undefined) {
state_1.state.mode = v.mode;
}
act.apply();
},
getView() {
return { re: state_1.state.re, im: state_1.state.im, step: state_1.state.step, mode: state_1.state.mode,
mag: config_1.CONFIG.INIT_STEP / state_1.state.step, limit: state_1.state.limit, cRe: state_1.state.cRe, cIm: state_1.state.cIm };
},
computeZoomRect: (x0, y0, x1, y1) => act.computeZoomRect(x0, y0, x1, y1),
zoomToRect: (x0, y0, x1, y1) => act.zoomToRect(x0, y0, x1, y1),
zoomAt: (f, x, y) => act.zoomAt(f, x, y),
resetView: act.resetView,
magnification: act.magnification,
restore(s) { (0, bookmarks_1.restoreSnapshot)(s); },
save() { return (0, bookmarks_1.currentSnapshot)(); },
state: state_1.state,
CONFIG: config_1.CONFIG,
};
// ------- boot ------------------------------------------------
(0, bookmarks_1.setupBookmarks)();
(0, input_1.setupInput)(preview);
resize(); // size + first render
(0, bookmarks_1.applyHash)(); // if a shareable link is present, adopt it
};
// ── module: src/config.ts ──
__mods["src/config.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CONFIG = void 0;
// ============================================================
// TUNABLE CONSTANTS — edit these to tune behavior/performance
// ============================================================
exports.CONFIG = {
// --- iteration limit (auto mode) ---------------------------
BASE_ITER: 220, // base iterations at magnification ~1
ITER_PER_DECADE: 85, // extra iterations per factor-of-10 of zoom
MIN_ITER: 60, // floor for auto limit
MAX_ITER: 200000, // ceiling for auto limit (also slider max)
// --- escape / coloring -------------------------------------
BAIL2: 256, // escape radius squared (|z|^2 > BAIL2)
COLOR_SCALE: 36, // iterations per color cycle (density)
COLORS_PER_CYCLE_SLIDER_STEP: 1,
// --- view / navigation -------------------------------------
INIT_WIDTH: 3.4, // real span of the default Mandelbrot view
JULIA_WIDTH: 4.0, // real span of a fresh Julia view
INIT_STEP: 0.0034, // = INIT_WIDTH / 1000 (per-pixel complex step at mag 1)
ZOOM_FACTOR: 1.35, // wheel zoom multiplier per notch (in/out)
PAN_SPEED: 1.0,
MOMENTUM: 0.92, // pan inertia decay (0 = none)
// --- precision guard ---------------------------------------
MAX_MAG: 1e15, // hard stop: block further zooming past this
WARN_MAG: 1e13, // show a warning past this magnification
// --- progressive rendering ---------------------------------
COARSE_DIV: 4, // coarse pass samples every 4th pixel
DW_CHUNK: 60, // coarse: rows computed per scheduler tick
REFINE_CHUNK: 22, // refine: full-res rows per scheduler tick
TICK_MS: 0, // setTimeout delay between chunks (yield)
// --- misc --------------------------------------------------
IDLE_MS: 2600, // hide HUD after this much inactivity
PREVIEW: 112, // mini preview pane size (px)
PREVIEW_LIMIT: 600, // cap on iterations used for the live preview
HISTORY_MAX: 80, // zoom-history stack size
MIN_DRAG: 6, // min drag distance (px) before it counts as zoom
MAX_DPR: 2, // cap devicePixelRatio for perf on hi-dpi
};
};
// ── module: src/fractal.ts ──
__mods["src/fractal.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.computeBuffer = computeBuffer;
exports.iterationsAt = iterationsAt;
// ============================================================
// FRACTAL CORE MATH — escape-time iteration, allocation-free
// ============================================================
const config_1 = require("./config");
const BAIL2 = config_1.CONFIG.BAIL2;
/**
* Compute smooth escape values for rows [y0, y1) of a w*h region centered at
* (cx, cy) with the given per-pixel complex step.
* - mode 'm' (Mandelbrot): z0 = 0, c = pixel
* - mode 'j' (Julia): z0 = pixel, c = (jcRe, jcIm)
* Writes into buf (Float32Array of length w*h). Interior points (those that
* never escape before `limit` iterations) are written as exactly `limit`.
*
* Allocation-free inside the per-pixel loop: no objects created per pixel.
*/
function computeBuffer(buf, w, h, cx, cy, step, mode, jcRe, jcIm, limit, y0, y1) {
const hw = w * 0.5, hh = h * 0.5;
for (let y = y0; y < y1; y++) {
const imc = cy + (hh - (y + 0.5)) * step;
const row = y * w;
for (let x = 0; x < w; x++) {
const rec = cx + (x + 0.5 - hw) * step;
const idx = row + x;
// seed the iteration
let zr = 0, zi = 0;
let cr = rec, ci = imc;
if (mode === 'j') {
zr = rec;
zi = imc;
cr = jcRe;
ci = jcIm;
}
let n = 0;
let r2 = 0, i2 = 0;
while (n < limit) {
r2 = zr * zr;
i2 = zi * zi;
if (r2 + i2 > BAIL2)
break;
zi = 2 * zr * zi + ci;
zr = r2 - i2 + cr;
n++;
}
if (n >= limit) {
buf[idx] = limit; // interior -> black
}
else {
// smooth (continuous) escape: n + 1 - log2(log|z|)
const mag = r2 + i2; // |z|^2 at escape, > BAIL2
const mu = n + 1 - Math.log(Math.log(Math.sqrt(mag))) / Math.LN2;
buf[idx] = mu < limit ? mu : limit;
}
}
}
}
/** Integer escape-iteration count at (re, im). Interior returns `limit`. */
function iterationsAt(re, im, mode, jcRe, jcIm, limit) {
let zr = 0, zi = 0;
let cr = re, ci = im;
if (mode === 'j') {
zr = re;
zi = im;
cr = jcRe;
ci = jcIm;
}
let n = 0;
while (n < limit) {
const r2 = zr * zr, i2 = zi * zi;
if (r2 + i2 > BAIL2)
break;
zi = 2 * zr * zi + ci;
zr = r2 - i2 + cr;
n++;
}
return n;
}
};
// ── 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.colorizeInto = colorizeInto;
exports.paletteCSS = paletteCSS;
// ============================================================
// PALETTES — selectable color maps with smooth (lerped) stops
// ============================================================
const config_1 = require("./config");
exports.PALETTES = [
{
name: 'Fire',
stops: [
{ p: 0, c: [6, 0, 0] }, { p: 0.18, c: [38, 0, 0] },
{ p: 0.42, c: [160, 20, 0] }, { p: 0.62, c: [255, 110, 20] },
{ p: 0.82, c: [255, 210, 70] }, { p: 1, c: [255, 252, 220] },
],
},
{
name: 'Ocean',
stops: [
{ p: 0, c: [1, 1, 8] }, { p: 0.25, c: [4, 18, 64] },
{ p: 0.5, c: [6, 84, 150] }, { p: 0.75, c: [50, 210, 245] },
{ p: 1, c: [220, 255, 255] },
],
},
{
name: 'Mono',
stops: [
{ p: 0, c: [0, 0, 0] }, { p: 1, c: [255, 255, 255] },
],
},
{
name: 'Psyche',
stops: [
{ p: 0, c: [255, 0, 90] }, { p: 0.33, c: [255, 255, 0] },
{ p: 0.66, c: [0, 255, 230] }, { p: 1, c: [255, 0, 210] },
],
},
];
/**
* Colorize RGBA buffer `px` for rows [y0, y1) from escape buffer `buf`.
* Interior (mu >= limit) -> black. Otherwise map smooth mu to a [0,1) t,
* offset by the cycle slider, and interpolate through the palette stops.
* Allocation-free per pixel.
*/
function colorizeInto(px, buf, w, h, limit, palette, offset, y0, y1) {
const stops = palette.stops;
const l = stops.length;
const CS = config_1.CONFIG.COLOR_SCALE;
for (let y = y0; y < y1; y++) {
const row = y * w;
for (let x = 0; x < w; x++) {
const idx = row + x;
const mu = buf[idx];
const o = idx * 4;
if (mu >= limit) {
px[o] = 0;
px[o + 1] = 0;
px[o + 2] = 0;
px[o + 3] = 255;
continue;
}
let t = (mu / CS + offset) % 1;
if (t < 0)
t += 1;
let r = 0, g = 0, b = 0;
if (t <= stops[0].p) {
r = stops[0].c[0];
g = stops[0].c[1];
b = stops[0].c[2];
}
else if (t >= stops[l - 1].p) {
const c = stops[l - 1].c;
r = c[0];
g = c[1];
b = c[2];
}
else {
for (let k = 0; k < l - 1; k++) {
const a = stops[k], d = stops[k + 1];
if (t >= a.p && t <= d.p) {
const span = d.p - a.p || 1;
const f = (t - a.p) / span;
r = a.c[0] + (d.c[0] - a.c[0]) * f;
g = a.c[1] + (d.c[1] - a.c[1]) * f;
b = a.c[2] + (d.c[2] - a.c[2]) * f;
break;
}
}
}
px[o] = r;
px[o + 1] = g;
px[o + 2] = b;
px[o + 3] = 255;
}
}
}
/** Build a CSS linear-gradient string from a palette (for swatch chips). */
function paletteCSS(p) {
const parts = p.stops.map(s => {
const [r, g, b] = s.c;
return `rgb(${r | 0},${g | 0},${b | 0}) ${(s.p * 100).toFixed(1)}%`;
});
return `linear-gradient(90deg, ${parts.join(', ')})`;
}
};
// ── module: src/state.ts ──
__mods["src/state.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.state = void 0;
exports.state = {
re: -0.6, im: 0,
step: 0.0034,
mode: 'm',
cRe: -0.745428, cIm: 0.113009,
paletteIndex: 0,
paletteOffset: 0,
limit: 220,
limitAuto: true,
W: 100, H: 100,
hoverRe: -0.8, hoverIm: 0.156,
onViewChange: null,
onActivity: null,
};
};
// ── module: src/actions.ts ──
__mods["src/actions.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.clampLimit = clampLimit;
exports.autoLimit = autoLimit;
exports.apply = apply;
exports.goTo = goTo;
exports.panBy = panBy;
exports.zoomAt = zoomAt;
exports.zoomBy = zoomBy;
exports.computeZoomRect = computeZoomRect;
exports.zoomToRect = zoomToRect;
exports.resetView = resetView;
exports.backView = backView;
exports.openJulia = openJulia;
exports.backToMandelbrot = backToMandelbrot;
exports.toggleJulia = toggleJulia;
exports.selectPalette = selectPalette;
exports.cyclePalette = cyclePalette;
exports.setOffset = setOffset;
exports.setLimitAuto = setLimitAuto;
exports.setManualLimit = setManualLimit;
exports.setSize = setSize;
exports.magnification = magnification;
exports.peekHistory = peekHistory;
exports.currentSaved = currentSaved;
exports.setMode = setMode;
// ============================================================
// NAVIGATION ACTIONS — mutate state, manage zoom history,
// recompute the auto iteration limit, notify the renderer.
// ============================================================
const state_1 = require("./state");
const config_1 = require("./config");
const history = [];
let mSaved = null; // Mandelbrot view saved while in Julia
function mag() { return config_1.CONFIG.INIT_STEP / state_1.state.step; }
function clampLimit(v) {
return Math.max(config_1.CONFIG.MIN_ITER, Math.min(config_1.CONFIG.MAX_ITER, Math.round(v)));
}
function autoLimit(m) {
return clampLimit(config_1.CONFIG.BASE_ITER + config_1.CONFIG.ITER_PER_DECADE * Math.log10(Math.max(m, 1)));
}
function refreshLimit() {
if (state_1.state.limitAuto)
state_1.state.limit = autoLimit(mag());
}
function pushHistory() {
history.push({ re: state_1.state.re, im: state_1.state.im, step: state_1.state.step, mode: state_1.state.mode, cRe: state_1.state.cRe, cIm: state_1.state.cIm });
if (history.length > config_1.CONFIG.HISTORY_MAX)
history.shift();
}
/** Clamp magnification to the precision cap. Returns true if clamped. */
function clampMag() {
const m = mag();
if (m > config_1.CONFIG.MAX_MAG) {
state_1.state.step = config_1.CONFIG.INIT_STEP / config_1.CONFIG.MAX_MAG;
return true;
}
return false;
}
/** Idempotently apply current state: refresh auto limit + notify. */
function apply() {
refreshLimit();
if (state_1.state.onViewChange)
state_1.state.onViewChange();
if (state_1.state.onActivity)
state_1.state.onActivity();
}
// ---------- direct view --------------------------------------------------
function goTo(re, im, step, push = true) {
if (push)
pushHistory();
state_1.state.re = re;
state_1.state.im = im;
state_1.state.step = Math.max(step, config_1.CONFIG.INIT_STEP / config_1.CONFIG.MAX_MAG);
clampMag();
apply();
}
// ---------- pan ----------------------------------------------------------
function panBy(dxPx, dyPx) {
state_1.state.re -= dxPx * state_1.state.step;
state_1.state.im += dyPx * state_1.state.step;
apply();
}
// ---------- zoom toward a screen point -----------------------------------
function zoomAt(factor, sx, sy) {
const beforeRe = state_1.state.re + (sx - state_1.state.W * 0.5) * state_1.state.step;
const beforeIm = state_1.state.im + (state_1.state.H * 0.5 - sy) * state_1.state.step;
const old = state_1.state.step;
let ns = old * factor;
state_1.state.step = ns;
const clamped = clampMag();
ns = state_1.state.step;
if (!clamped || old === ns) {
state_1.state.re = beforeRe - (sx - state_1.state.W * 0.5) * ns;
state_1.state.im = beforeIm + (state_1.state.H * 0.5 - sy) * ns;
}
apply();
}
/** Zoom in/out around the view center. */
function zoomBy(factor, push = true) {
if (push)
pushHistory();
const old = state_1.state.step;
state_1.state.step = old * factor;
clampMag();
apply();
}
// ---------- rectangle zoom (drag box) ------------------------------------
/**
* Given a screen rectangle [x0,y0]-[x1,y1] in device px, return the view that
* shows exactly that framed region: same per-pixel step on both axes (so no
* distortion), centered on the rectangle center, with the rectangle fully
* contained (the larger dimension fills the screen). This is deterministic so
* it can be asserted headlessly.
*/
function computeZoomRect(x0, y0, x1, y1) {
const rw = Math.abs(x1 - x0), rh = Math.abs(y1 - y0);
const cx = (x0 + x1) * 0.5, cy = (y0 + y1) * 0.5;
const newCRe = state_1.state.re + (cx - state_1.state.W * 0.5) * state_1.state.step;
const newCIm = state_1.state.im + (state_1.state.H * 0.5 - cy) * state_1.state.step;
const k = Math.max(rw / state_1.state.W, rh / state_1.state.H); // contains the rect, no crop
const step = Math.max(state_1.state.step * k, config_1.CONFIG.INIT_STEP / config_1.CONFIG.MAX_MAG);
return { re: newCRe, im: newCIm, step, mode: state_1.state.mode, cRe: state_1.state.cRe, cIm: state_1.state.cIm };
}
function zoomToRect(x0, y0, x1, y1) {
const rw = Math.abs(x1 - x0), rh = Math.abs(y1 - y0);
if (rw < config_1.CONFIG.MIN_DRAG || rh < config_1.CONFIG.MIN_DRAG)
return false;
pushHistory();
const v = computeZoomRect(x0, y0, x1, y1);
state_1.state.re = v.re;
state_1.state.im = v.im;
state_1.state.step = v.step;
apply();
return true;
}
// ---------- reset / back ------------------------------------------------
function resetView() {
pushHistory();
state_1.state.mode = 'm';
state_1.state.re = -0.6;
state_1.state.im = 0;
state_1.state.step = config_1.CONFIG.INIT_STEP;
state_1.state.cRe = -0.745428;
state_1.state.cIm = 0.113009;
mSaved = null;
apply();
}
function backView() {
const e = history.pop();
if (!e)
return false;
state_1.state.re = e.re;
state_1.state.im = e.im;
state_1.state.step = e.step;
state_1.state.mode = e.mode;
state_1.state.cRe = e.cRe;
state_1.state.cIm = e.cIm;
apply();
return true;
}
// ---------- Julia mode ---------------------------------------------------
function openJulia(re, im) {
pushHistory();
if (state_1.state.mode === 'm') {
mSaved = { re: state_1.state.re, im: state_1.state.im, step: state_1.state.step, mode: 'm', cRe: state_1.state.cRe, cIm: state_1.state.cIm };
}
state_1.state.cRe = re;
state_1.state.cIm = im;
state_1.state.hoverRe = re;
state_1.state.hoverIm = im;
state_1.state.mode = 'j';
state_1.state.re = 0;
state_1.state.im = 0;
state_1.state.step = config_1.CONFIG.JULIA_WIDTH / state_1.state.H; // fresh Julia window around origin
apply();
}
function backToMandelbrot() {
pushHistory();
state_1.state.mode = 'm';
if (mSaved) {
state_1.state.re = mSaved.re;
state_1.state.im = mSaved.im;
state_1.state.step = mSaved.step;
mSaved = null;
}
else {
state_1.state.re = -0.6;
state_1.state.im = 0;
state_1.state.step = config_1.CONFIG.INIT_STEP;
}
apply();
}
function toggleJulia() {
if (state_1.state.mode === 'm')
openJulia(state_1.state.hoverRe, state_1.state.hoverIm);
else
backToMandelbrot();
}
// ---------- palette / color ---------------------------------------------
function selectPalette(i) {
state_1.state.paletteIndex = ((i % 4) + 4) % 4;
apply();
}
function cyclePalette() { selectPalette(state_1.state.paletteIndex + 1); }
function setOffset(off) { state_1.state.paletteOffset = off; apply(); }
function setLimitAuto(auto) { state_1.state.limitAuto = auto; apply(); }
function setManualLimit(v) {
state_1.state.limitAuto = false;
state_1.state.limit = clampLimit(v);
apply();
}
// ---------- set W/H (device) --------------------------------------------
function setSize(W, H) { state_1.state.W = W; state_1.state.H = H; }
// ---------- debug helpers ------------------------------------------------
function magnification() { return mag(); }
function peekHistory() { return history; }
function currentSaved() {
return { re: state_1.state.re, im: state_1.state.im, step: state_1.state.step, mode: state_1.state.mode, cRe: state_1.state.cRe, cIm: state_1.state.cIm };
}
function setMode(m) { if (m === state_1.state.mode)
return; if (m === 'j')
openJulia(state_1.state.hoverRe, state_1.state.hoverIm);
else
backToMandelbrot(); }
};
// ── module: src/render.ts ──
__mods["src/render.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Renderer = void 0;
// ============================================================
// RENDERER — Canvas2D ImageData pipeline with progressive
// rendering (fast coarse pass, then full-resolution refine).
// View changes bump a version token; in-flight passes abort on
// the next scheduler tick so interaction never freezes and no
// stale/ghost tiles survive.
// ============================================================
const config_1 = require("./config");
const state_1 = require("./state");
const fractal_1 = require("./fractal");
const palettes_1 = require("./palettes");
const tick = () => new Promise(r => setTimeout(r, config_1.CONFIG.TICK_MS));
class Renderer {
constructor(canvas) {
this.W = 2;
this.H = 2;
this.lW = 1;
this.lH = 1;
this.version = 0; // bump to cancel in-flight work
this.fullReady = false; // true when refine completed for current view
this.lastDone = 0; // timestamp of last refine completion
this.fps = 0;
this.canvas = canvas;
const ctx = canvas.getContext('2d', { willReadFrequently: true });
if (!ctx)
throw new Error('no 2d context');
this.ctx = ctx;
}
/** (Re)size buffers. Call after the canvas element changes size. */
setSize(W, H) {
this.W = W;
this.H = H;
this.canvas.width = W;
this.canvas.height = H;
this.it = new Float32Array(W * H);
this.px = new Uint8ClampedArray(W * H * 4);
this.img = new ImageData(this.px, W, H);
// coarse backing
this.lW = Math.max(1, Math.ceil(W / config_1.CONFIG.COARSE_DIV));
this.lH = Math.max(1, Math.ceil(H / config_1.CONFIG.COARSE_DIV));
this.lowIt = new Float32Array(this.lW * this.lH);
this.lowCanvas = document.createElement('canvas');
this.lowCanvas.width = this.lW;
this.lowCanvas.height = this.lH;
this.lowCtx = this.lowCanvas.getContext('2d');
this.lowImg = this.lowCtx.createImageData(this.lW, this.lH);
}
get w() { return this.W; }
get h() { return this.H; }
get framesPerSecond() { return this.fps; }
/** Start (or restart) a full render of the current view. Cancels prior work. */
renderView() {
const v = ++this.version;
this.fullReady = false;
this.run(v).catch(() => { });
}
/** Re-colorize the existing full-res buffer (palette/offset change only). */
recolor() {
if (!this.fullReady) {
this.renderView();
return;
}
const pal = palettes_1.PALETTES[state_1.state.paletteIndex];
const t0 = performance.now();
(0, palettes_1.colorizeInto)(this.px, this.it, this.W, this.H, state_1.state.limit, pal, state_1.state.paletteOffset, 0, this.H);
this.ctx.putImageData(this.img, 0, 0);
this.lastDone = t0;
}
async run(v) {
if (v !== this.version)
return;
await this.coarse(v);
if (v !== this.version)
return;
await this.refine(v);
if (v !== this.version)
return;
this.fullReady = true;
this.recomputeFps();
}
// ---- coarse pass: sample every 4th pixel, blit upscaled ----
async coarse(v) {
const pal = palettes_1.PALETTES[state_1.state.paletteIndex];
const l = state_1.state.limit;
const step4 = state_1.state.step * config_1.CONFIG.COARSE_DIV;
this.ctx.imageSmoothingEnabled = false;
for (let y = 0; y < this.lH;) {
if (v !== this.version)
return;
const y1 = Math.min(y + config_1.CONFIG.DW_CHUNK, this.lH);
(0, fractal_1.computeBuffer)(this.lowIt, this.lW, this.lH, state_1.state.re, state_1.state.im, step4, state_1.state.mode, state_1.state.cRe, state_1.state.cIm, l, y, y1);
(0, palettes_1.colorizeInto)(this.lowImg.data, this.lowIt, this.lW, this.lH, l, pal, state_1.state.paletteOffset, y, y1);
this.lowCtx.putImageData(this.lowImg, 0, 0);
this.ctx.clearRect(0, 0, this.W, this.H);
this.ctx.drawImage(this.lowCanvas, 0, 0, this.W, this.H);
y = y1;
await tick();
}
}
// ---- refine pass: full resolution, drawn band by band ----
async refine(v) {
const pal = palettes_1.PALETTES[state_1.state.paletteIndex];
const l = state_1.state.limit;
for (let y = 0; y < this.H;) {
if (v !== this.version)
return;
const y1 = Math.min(y + config_1.CONFIG.REFINE_CHUNK, this.H);
(0, fractal_1.computeBuffer)(this.it, this.W, this.H, state_1.state.re, state_1.state.im, state_1.state.step, state_1.state.mode, state_1.state.cRe, state_1.state.cIm, l, y, y1);
(0, palettes_1.colorizeInto)(this.px, this.it, this.W, this.H, l, pal, state_1.state.paletteOffset, y, y1);
this.ctx.putImageData(this.img, 0, 0, 0, y, this.W, y1 - y);
y = y1;
await tick();
}
}
recomputeFps() {
const now = performance.now();
if (this.lastDone > 0) {
const dt = (now - this.lastDone) / 1000;
if (dt > 0)
this.fps = Math.round(1 / dt);
}
this.lastDone = now;
}
}
exports.Renderer = Renderer;
};
// ── module: src/preview.ts ──
__mods["src/preview.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Preview = void 0;
// ============================================================
// LIVE PREVIEW PANE — shows the *other* set for the hovered
// point. In Mandelbrot mode it renders the Julia set for the
// current cursor c; in Julia mode it renders the Mandelbrot set.
// ============================================================
const config_1 = require("./config");
const state_1 = require("./state");
const fractal_1 = require("./fractal");
const palettes_1 = require("./palettes");
class Preview {
constructor(canvas) {
this.d = config_1.CONFIG.PREVIEW;
this.lastT = 0;
this.canvas = canvas;
const ctx = canvas.getContext('2d');
this.ctx = ctx;
this.canvas.width = this.d;
this.canvas.height = this.d;
this.it = new Float32Array(this.d * this.d);
this.img = ctx.createImageData(this.d, this.d);
this.wrap = document.getElementById('preview-wrap');
this.label = document.getElementById('preview-label');
}
show() { this.wrap.hidden = false; }
hide() { this.wrap.hidden = true; }
/** Update preview for the point (re, im) in the *other* set. */
update(re, im) {
const now = performance.now();
if (now - this.lastT < 34)
return; // ~30fps throttle
this.lastT = now;
const limit = Math.min(state_1.state.limit, config_1.CONFIG.PREVIEW_LIMIT);
const pal = palettes_1.PALETTES[state_1.state.paletteIndex];
if (state_1.state.mode === 'm') {
// main is Mandelbrot -> preview the Julia set for hovered c
this.label.textContent = `Julia c = ${re.toFixed(4)}${im >= 0 ? '+' : ''}${im.toFixed(4)}i`;
const step = config_1.CONFIG.JULIA_WIDTH / this.d;
(0, fractal_1.computeBuffer)(this.it, this.d, this.d, 0, 0, step, 'j', re, im, limit, 0, this.d);
}
else {
// main is Julia -> preview the Mandelbrot set
this.label.textContent = 'Mandelbrot';
const step = config_1.CONFIG.INIT_WIDTH / this.d;
(0, fractal_1.computeBuffer)(this.it, this.d, this.d, -0.6, 0, step, 'm', re, im, limit, 0, this.d);
}
(0, palettes_1.colorizeInto)(this.img.data, this.it, this.d, this.d, limit, pal, state_1.state.paletteOffset, 0, this.d);
this.ctx.putImageData(this.img, 0, 0);
}
}
exports.Preview = Preview;
};
// ── module: src/input.ts ──
__mods["src/input.ts"] = function (exports, require, module) {
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.setupInput = setupInput;
// ============================================================
// INPUT — pointer (drag-zoom, pan, wheel, click->Julia),
// keyboard, live hover preview + reticle + zoom box overlay.
// ============================================================
const config_1 = require("./config");
const state_1 = require("./state");
const act = __importStar(require("./actions"));
function setupInput(preview) {
const canvas = document.getElementById('fractal');
const box = document.getElementById('zoombox');
const reticle = document.getElementById('reticle');
const hr = reticle.querySelector('.hr');
const vr = reticle.querySelector('.vr');
// ---- pointer helpers --------------------------------------
function toDev(e) {
const r = canvas.getBoundingClientRect();
const s = canvas.width / r.width;
return { x: (e.clientX - r.left) * s, y: (e.clientY - r.top) * s };
}
function activity() { if (state_1.state.onActivity)
state_1.state.onActivity(); }
// ---- interaction state ------------------------------------
let down = false;
let panning = false;
let startX = 0, startY = 0; // pointer-down device coords
let moved = false;
let boxActive = false;
let lastEngaged = false;
// pan inertia
let vx = 0, vy = 0, panTimer = null;
function endPan() {
panning = false;
down = false;
if (panTimer) {
clearInterval(panTimer);
panTimer = null;
}
}
canvas.addEventListener('pointerdown', (e) => {
activity();
if (e.button !== 0 && e.button !== 2)
return;
canvas.setPointerCapture(e.pointerId);
const d = toDev(e);
down = true;
moved = false;
startX = d.x;
startY = d.y;
vx = 0;
vy = 0;
panning = e.button === 2 || e.shiftKey;
if (panning && panTimer) {
clearInterval(panTimer);
panTimer = null;
}
e.preventDefault();
});
canvas.addEventListener('pointermove', (e) => {
const d = toDev(e);
// always update hover preview / reticle when not dragging a box
const hoverRe = state_1.state.re + (d.x - state_1.state.W * 0.5) * state_1.state.step;
const hoverIm = state_1.state.im + (state_1.state.H * 0.5 - d.y) * state_1.state.step;
state_1.state.hoverRe = hoverRe;
state_1.state.hoverIm = hoverIm;
if (!down) {
// hover mode
reticle.style.display = 'block';
const rect = canvas.getBoundingClientRect();
hr.style.height = rect.height + 'px';
vr.style.width = rect.width + 'px';
reticle.style.left = (e.clientX - rect.left) + 'px';
reticle.style.top = (e.clientY - rect.top) + 'px';
reticle.style.width = '30px';
reticle.style.height = '30px';
reticle.style.transform = `translate(-50%,-50%)`;
preview.show();
preview.update(hoverRe, hoverIm);
return;
}
// dragging
activity();
if (!moved && Math.hypot(d.x - startX, d.y - startY) > config_1.CONFIG.MIN_DRAG)
moved = true;
if (panning) {
const dx = d.x - startX, dy = d.y - startY;
act.panBy(dx, dy);
vx = (vx + dx) * 0.5;
vy = (vy + dy) * 0.5;
startX = d.x;
startY = d.y;
if (!panTimer) {
panTimer = window.setInterval(() => {
if (!panning) {
endPan();
return;
}
vx *= config_1.CONFIG.MOMENTUM;
vy *= config_1.CONFIG.MOMENTUM;
if (Math.abs(vx) < 0.05 && Math.abs(vy) < 0.05)
return;
act.panBy(vx, vy);
}, 16);
}
boxActive = false;
box.style.display = 'none';
lastEngaged = false;
}
else if (moved) {
// draw zoom box
boxActive = true;
lastEngaged = true;
const rect = canvas.getBoundingClientRect();
const s = rect.width / canvas.width;
const x0 = Math.min(startX, d.x) * s, y0 = Math.min(startY, d.y) * s;
const w = Math.abs(d.x - startX) * s, h = Math.abs(d.y - startY) * s;
box.style.display = 'block';
box.style.left = (x0 + rect.left) + 'px';
box.style.top = (y0 + rect.top) + 'px';
box.style.width = w + 'px';
box.style.height = h + 'px';
reticle.style.display = 'none';
preview.hide();
}
e.preventDefault();
});
function release(e) {
if (!down)
return;
const d = toDev(e);
down = false;
box.style.display = 'none';
reticle.style.display = 'none';
preview.show();
if (panning)
return; // keep inertia running
if (boxActive) {
if (act.zoomToRect(startX, startY, d.x, d.y)) {
boxActive = false;
return;
}
boxActive = false;
// too small to be a zoom -> treat as a click
}
// click (no meaningful drag): open Julia at the point in Mandelbrot mode
if (state_1.state.mode === 'm') {
const re = state_1.state.re + (d.x - state_1.state.W * 0.5) * state_1.state.step;
const im = state_1.state.im + (state_1.state.H * 0.5 - d.y) * state_1.state.step;
act.openJulia(re, im);
}
else {
// in Julia mode clicking with no drag zooms slightly toward point
act.zoomAt(config_1.CONFIG.ZOOM_FACTOR / 2, d.x, d.y);
}
}
canvas.addEventListener('pointerup', release);
canvas.addEventListener('pointercancel', () => {
box.style.display = 'none';
reticle.style.display = 'none';
preview.show();
down = false;
boxActive = false;
endPan();
});
canvas.addEventListener('lostpointercapture', () => { endPan(); });
// ---- wheel zoom toward cursor -----------------------------
canvas.addEventListener('wheel', (e) => {
e.preventDefault();
activity();
const d = toDev(e);
const dir = e.deltaY < 0 ? 1 / config_1.CONFIG.ZOOM_FACTOR : config_1.CONFIG.ZOOM_FACTOR;
act.zoomAt(dir, d.x, d.y);
}, { passive: false });
// ---- keyboard ---------------------------------------------
window.addEventListener('keydown', (e) => {
const step = state_1.state.step;
const k = e.key;
let handled = true;
if (k === 'ArrowLeft')
act.panBy(-60 * step, 0);
else if (k === 'ArrowRight')
act.panBy(60 * step, 0);
else if (k === 'ArrowUp')
act.panBy(0, -60 * step);
else if (k === 'ArrowDown')
act.panBy(0, 60 * step);
else if (k === '+' || k === '=')
act.zoomAt(1 / config_1.CONFIG.ZOOM_FACTOR, state_1.state.W / 2, state_1.state.H / 2);
else if (k === '-' || k === '_')
act.zoomAt(config_1.CONFIG.ZOOM_FACTOR, state_1.state.W / 2, state_1.state.H / 2);
else if (k === '0' || k === 'Home')
act.resetView();
else if (k === 'b' || k === 'B')
act.backView();
else if (k === 'j' || k === 'J')
act.toggleJulia();
else if (k === 'p' || k === 'P')
act.cyclePalette();
else if (k >= '1' && k <= '4')
act.selectPalette(+k - 1);
else
handled = false;
if (handled) {
e.preventDefault();
activity();
}
});
}
};
// ── module: src/hud.ts ──
__mods["src/hud.ts"] = function (exports, require, module) {
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.setupHud = setupHud;
// ============================================================
// HUD — glassy overlay: palette chips, sliders, buttons,
// live readouts (magnification, center, iters, fps), idle-hide.
// ============================================================
const config_1 = require("./config");
const state_1 = require("./state");
const act = __importStar(require("./actions"));
const palettes_1 = require("./palettes");
function setupHud(renderer) {
const hud = document.getElementById('hud');
const magEl = document.getElementById('mag');
const centerEl = document.getElementById('center');
const itersEl = document.getElementById('iters');
const fpsEl = document.getElementById('fps');
const modeEl = document.getElementById('mode-chip');
const warnEl = document.getElementById('warn');
// --- palette chips -----------------------------------------
const palWrap = document.getElementById('palettes');
const chips = [];
palettes_1.PALETTES.forEach((p, i) => {
const s = document.createElement('button');
s.className = 'swatch';
s.title = p.name;
s.dataset.i = String(i);
s.style.background = (0, palettes_1.paletteCSS)(p);
s.addEventListener('click', () => act.selectPalette(i));
palWrap.appendChild(s);
chips.push(s);
});
// --- offset slider -----------------------------------------
const offset = document.getElementById('offset');
offset.addEventListener('input', () => act.setOffset(+offset.value / 99));
// --- iteration slider + auto -------------------------------
const iter = document.getElementById('iter');
const iterAuto = document.getElementById('iter-auto');
function refreshIterUI() {
iter.disabled = state_1.state.limitAuto;
iter.value = String(state_1.state.limit);
iterAuto.textContent = state_1.state.limitAuto ? 'AUTO' : 'MANUAL';
iterAuto.style.opacity = state_1.state.limitAuto ? '1' : '.5';
}
iter.addEventListener('input', () => act.setManualLimit(+iter.value));
iterAuto.addEventListener('click', () => act.setLimitAuto(!state_1.state.limitAuto));
// --- buttons -----------------------------------------------
document.getElementById('btn-back').addEventListener('click', () => act.backView());
document.getElementById('btn-reset').addEventListener('click', () => act.resetView());
document.getElementById('btn-julia').addEventListener('click', () => act.toggleJulia());
document.getElementById('btn-help').addEventListener('click', () => {
const h = document.getElementById('help');
h.hidden = !h.hidden;
activity();
});
// --- idle hide ---------------------------------------------
let idleTimer = null;
function activity() {
hud.classList.remove('idle');
if (idleTimer)
clearTimeout(idleTimer);
idleTimer = window.setTimeout(() => hud.classList.add('idle'), config_1.CONFIG.IDLE_MS);
}
// --- readouts ----------------------------------------------
function fmtMag() {
const m = config_1.CONFIG.INIT_STEP / state_1.state.step;
if (m >= 1e4)
return '×' + m.toExponential(2);
if (m >= 100)
return '×' + Math.round(m).toLocaleString();
return '×' + m.toFixed(2);
}
function fmtC(re, im) {
const sgn = im < 0 ? '−' : '+';
return `${re.toPrecision(4)} ${sgn} ${Math.abs(im).toPrecision(3)}i`;
}
function update() {
magEl.textContent = fmtMag();
if (state_1.state.mode === 'm') {
modeEl.textContent = 'Mandelbrot';
centerEl.textContent = 'center ' + fmtC(state_1.state.re, state_1.state.im);
}
else {
modeEl.textContent = 'Julia';
centerEl.textContent = 'c = ' + fmtC(state_1.state.cRe, state_1.state.cIm);
}
itersEl.textContent = 'iter ' + state_1.state.limit + (state_1.state.limitAuto ? ' (auto)' : '');
fpsEl.textContent = renderer.framesPerSecond + ' fps';
const m = config_1.CONFIG.INIT_STEP / state_1.state.step;
warnEl.hidden = m <= config_1.CONFIG.WARN_MAG;
refreshIterUI();
chips.forEach((c, i) => c.classList.toggle('on', i === state_1.state.paletteIndex));
}
return { update, activity };
}
};
// ── module: src/bookmarks.ts ──
__mods["src/bookmarks.ts"] = function (exports, require, module) {
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.currentSnapshot = currentSnapshot;
exports.restoreSnapshot = restoreSnapshot;
exports.setupBookmarks = setupBookmarks;
exports.syncHash = syncHash;
exports.applyHash = applyHash;
// ============================================================
// BOOKMARKS (localStorage) + URL-hash state sharing
// ============================================================
const config_1 = require("./config");
const state_1 = require("./state");
const act = __importStar(require("./actions"));
const KEY = 'fractal.bm.v1';
function currentSnapshot() {
return {
re: state_1.state.re, im: state_1.state.im, step: state_1.state.step, mode: state_1.state.mode,
cRe: state_1.state.cRe, cIm: state_1.state.cIm,
paletteIndex: state_1.state.paletteIndex, paletteOffset: state_1.state.paletteOffset,
limit: state_1.state.limit, limitAuto: state_1.state.limitAuto,
};
}
function restoreSnapshot(s) {
state_1.state.re = s.re;
state_1.state.im = s.im;
state_1.state.step = s.step;
state_1.state.mode = s.mode;
state_1.state.cRe = s.cRe;
state_1.state.cIm = s.cIm;
state_1.state.paletteIndex = s.paletteIndex | 0;
state_1.state.paletteOffset = s.paletteOffset | 0;
state_1.state.limit = s.limit | 0;
state_1.state.limitAuto = !!s.limitAuto;
act.apply();
}
function load() {
try {
const v = JSON.parse(localStorage.getItem(KEY) || '[]');
return Array.isArray(v) ? v : [];
}
catch {
return [];
}
}
function persist(list) {
try {
localStorage.setItem(KEY, JSON.stringify(list));
}
catch { /* storage full */ }
}
function setupBookmarks() {
const panel = document.getElementById('bmarks');
const nameInput = document.getElementById('bm-name');
const listEl = document.getElementById('bm-list');
document.getElementById('btn-bmarks').addEventListener('click', () => { panel.hidden = !panel.hidden; if (!panel.hidden)
render(); });
document.getElementById('bm-save').addEventListener('click', () => {
const name = (nameInput.value || 'View ' + new Date().toLocaleTimeString()).trim();
const items = load();
items.unshift({ name, s: currentSnapshot() });
persist(items.slice(0, 60));
render();
});
nameInput.addEventListener('keydown', (e) => { if (e.key === 'Enter')
document.getElementById('bm-save').click(); });
function render() {
listEl.innerHTML = '';
const items = load();
if (!items.length) {
listEl.innerHTML = '<li style="opacity:.5">No bookmarks yet.</li>';
return;
}
items.forEach((b, i) => {
const li = document.createElement('li');
const nm = document.createElement('span');
nm.className = 'nm';
nm.textContent = b.name;
const loadB = document.createElement('button');
loadB.textContent = 'load';
loadB.addEventListener('click', () => restoreSnapshot(b.s));
const del = document.createElement('button');
del.textContent = '✕';
del.addEventListener('click', () => { const l = load(); l.splice(i, 1); persist(l); render(); });
li.appendChild(nm);
li.appendChild(loadB);
li.appendChild(del);
listEl.appendChild(li);
});
}
}
// ---------------- URL hash state ----------------------------
let hashTimer = null;
function syncHash() {
if (hashTimer)
return;
hashTimer = window.setTimeout(() => {
hashTimer = null;
const s = currentSnapshot();
const h = { m: s.mode, r: +s.re.toPrecision(12), i: +s.im.toPrecision(12),
s: +s.step.toPrecision(12), cR: +s.cRe.toPrecision(8), cI: +s.cIm.toPrecision(8),
p: s.paletteIndex, o: +s.paletteOffset.toFixed(3), l: s.limit, a: s.limitAuto ? 1 : 0 };
try {
history.replaceState(null, '', '#' + encodeURIComponent(JSON.stringify(h)));
}
catch { /* noop */ }
}, 120);
}
function applyHash() {
const raw = location.hash.replace(/^#/, '');
if (!raw)
return false;
try {
const h = JSON.parse(decodeURIComponent(raw));
if (typeof h !== 'object' || h.r === undefined || h.s === undefined)
return false;
const snap = {
re: +h.r, im: +h.i || 0, step: +h.s,
mode: h.m === 'j' ? 'j' : 'm',
cRe: +h.cR || 0, cIm: +h.cI || 0,
paletteIndex: +h.p || 0, paletteOffset: +h.o || 0,
limit: +h.l || config_1.CONFIG.MIN_ITER, limitAuto: h.a !== 0,
};
restoreSnapshot(snap);
return true;
}
catch {
return false;
}
}
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>