Fractal Explorer
ThinkingCap Qwen3.6 27B GGUF · typescript

Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 46,499 bytes · SHA-256 630ad661e2e1
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Fractal Explorer</title>
<style>
/* ── css: layout ── */
*, *::before, *::after {
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;
color: #e0e0e0;
}
#app {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
}
#fractal-canvas {
display: block;
width: 100%;
height: 100%;
cursor: crosshair;
}
/* HUD panel - glassy overlay */
.hud-panel {
position: absolute;
top: 12px;
left: 12px;
z-index: 10;
background: rgba(10, 10, 20, 0.75);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 10px;
padding: 14px 18px;
min-width: 260px;
transition: opacity 0.4s ease, transform 0.4s ease;
max-height: calc(100vh - 24px);
overflow-y: auto;
}
.hud-panel.hud-hidden {
opacity: 0;
transform: translateY(-10px);
pointer-events: none;
}
.hud-panel:hover {
opacity: 1 !important;
transform: translateY(0) !important;
}
.hud-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.hud-title {
font-size: 14px;
font-weight: 600;
letter-spacing: 0.5px;
color: #ccc;
}
.hud-controls {
display: flex;
gap: 4px;
}
.hud-controls button {
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.15);
color: #ccc;
border-radius: 6px;
width: 30px;
height: 30px;
cursor: pointer;
font-size: 14px;
transition: background 0.15s;
}
.hud-controls button:hover {
background: rgba(255, 255, 255, 0.2);
color: #fff;
}
.hud-stats {
margin-bottom: 10px;
}
.stat-row {
display: flex;
justify-content: space-between;
font-size: 11px;
padding: 2px 0;
}
.stat-label {
color: #888;
}
.stat-value {
color: #ddd;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 11px;
}
.hud-section-label {
display: block;
font-size: 11px;
color: #888;
margin-bottom: 6px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* Palette chips */
.palette-chips {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-bottom: 12px;
}
.palette-chip {
font-size: 10px;
padding: 3px 8px;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.15);
background: rgba(255, 255, 255, 0.06);
color: #aaa;
cursor: pointer;
transition: all 0.15s;
}
.palette-chip:hover {
background: rgba(255, 255, 255, 0.15);
color: #fff;
}
.palette-chip.active {
background: rgba(100, 150, 255, 0.3);
border-color: rgba(100, 150, 255, 0.6);
color: #fff;
}
/* Sliders */
.hud-sliders {
margin-bottom: 12px;
}
.slider-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 6px;
font-size: 11px;
}
.slider-row label {
color: #888;
min-width: 90px;
}
.slider-row input[type="range"] {
flex: 1;
height: 4px;
-webkit-appearance: none;
appearance: none;
background: rgba(255, 255, 255, 0.15);
border-radius: 2px;
outline: none;
}
.slider-row input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 12px;
height: 12px;
border-radius: 50%;
background: #6a9fff;
cursor: pointer;
}
.slider-row input[type="range"]::-moz-range-thumb {
width: 12px;
height: 12px;
border-radius: 50%;
background: #6a9fff;
cursor: pointer;
border: none;
}
#iter-val {
font-family: 'Consolas', monospace;
color: #aaa;
min-width: 35px;
text-align: right;
font-size: 11px;
}
/* Help text */
.hud-help {
border-top: 1px solid rgba(255, 255, 255, 0.08);
padding-top: 8px;
margin-top: 4px;
}
.help-text {
font-size: 10px;
color: #666;
}
/* Bookmarks */
.bookmarks-panel {
border-top: 1px solid rgba(255, 255, 255, 0.08);
padding-top: 8px;
margin-top: 8px;
}
.bookmark-list {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 6px;
}
.bm-btn {
font-size: 10px;
padding: 3px 8px;
border-radius: 4px;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(255, 255, 255, 0.06);
color: #aaa;
cursor: pointer;
position: relative;
padding-right: 22px;
}
.bm-btn:hover {
background: rgba(255, 255, 255, 0.15);
}
.bm-del {
position: absolute;
right: 2px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: #888;
cursor: pointer;
font-size: 12px;
line-height: 1;
padding: 0;
width: auto;
height: auto;
}
.bm-del:hover {
color: #f66;
}
.bm-empty {
font-size: 10px;
color: #555;
font-style: italic;
}
</style>
</head>
<body>
<div id="app"></div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./constants":"src/constants.ts","./state":"src/state.ts","./compute":"src/compute.ts","./renderer":"src/renderer.ts","./input":"src/input.ts","./hud":"src/hud.ts"},"src/state.ts":{"./constants":"src/constants.ts"},"src/renderer.ts":{"./constants":"src/constants.ts","./state":"src/state.ts","./compute":"src/compute.ts","./palette":"src/palette.ts"},"src/input.ts":{"./constants":"src/constants.ts","./state":"src/state.ts"},"src/hud.ts":{"./state":"src/state.ts","./palette":"src/palette.ts"}};
function __require(id) {
if (__cache[id]) return __cache[id].exports;
var module = __cache[id] = { exports: {} };
var factory = __mods[id];
if (!factory) throw new Error("Module not found: " + id);
factory(module.exports, function (spec) {
var target = (__map[id] && __map[id][spec]) || spec;
return __require(target);
}, module);
return module.exports;
}
// ── module: src/main.ts ──
__mods["src/main.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.start = start;
const constants_1 = require("./constants");
const state_1 = require("./state");
const compute_1 = require("./compute");
const renderer_1 = require("./renderer");
const input_1 = require("./input");
const hud_1 = require("./hud");
function main() {
const app = document.getElementById('app');
// Create canvas
const canvas = document.createElement('canvas');
canvas.id = 'fractal-canvas';
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
app.appendChild(canvas);
// State
const state = (0, state_1.createInitialState)();
// Try to restore from URL hash
if (location.hash) {
const hashView = (0, state_1.hashToView)(location.hash);
if (hashView) {
Object.assign(state.view, hashView);
}
}
// Renderer
const renderer = new renderer_1.Renderer(canvas, { width: canvas.width, height: canvas.height });
// HUD
const hud = new hud_1.HUD(app, state);
// Input handler
const input = new input_1.InputHandler(canvas, state);
// Pending render flag to debounce
let pendingRender = false;
let pendingRecolorize = false;
let juliaPreviewTimer = null;
function doRender() {
pendingRender = false;
renderer.render(state.view);
}
function doRecolorize() {
pendingRecolorize = false;
renderer.recolorize(state.view);
}
// Wire callbacks
input.setRenderRequested(() => {
renderer.cancel();
if (!pendingRender) {
pendingRender = true;
requestAnimationFrame(doRender);
}
});
hud.setRenderCallback(() => {
renderer.cancel();
if (!pendingRender) {
pendingRender = true;
requestAnimationFrame(doRender);
}
});
hud.setRecolorizeCallback(() => {
if (!pendingRecolorize) {
pendingRecolorize = true;
requestAnimationFrame(doRecolorize);
}
});
// Julia hover preview
input.setJuliaHover((re, im) => {
if (juliaPreviewTimer)
clearTimeout(juliaPreviewTimer);
juliaPreviewTimer = window.setTimeout(() => {
state.view.juliaC = { re, im };
}, constants_1.CONSTANTS.juliaPreviewDelay);
});
// Progress callback
renderer.setProgressCb((p) => {
state.renderProgress = p;
hud.updateProgress(p);
});
// FPS tracking
let frameCount = 0;
let lastFpsTime = performance.now();
function updateFps() {
frameCount++;
const now = performance.now();
if (now - lastFpsTime >= 1000) {
const fps = (frameCount / (now - lastFpsTime)) * 1000;
hud.updateFps(fps);
frameCount = 0;
lastFpsTime = now;
}
requestAnimationFrame(updateFps);
}
requestAnimationFrame(updateFps);
// Initial render
hud.update();
renderer.render(state.view);
// Resize handler
window.addEventListener('resize', () => {
const w = window.innerWidth;
const h = window.innerHeight;
canvas.width = w;
canvas.height = h;
// Recreate renderer with new dimensions
const newRenderer = new renderer_1.Renderer(canvas, { width: w, height: h });
Object.assign(newRenderer, renderer);
newRenderer.setProgressCb((p) => {
state.renderProgress = p;
hud.updateProgress(p);
});
renderer.render(state.view);
});
// Expose debug interface
window.fractal = {
iterationsAt: (re, im, maxIter) => {
return (0, compute_1.iterationsAt)(re, im, maxIter ?? 200);
},
setView: (params) => {
state.view.re = params.re;
state.view.im = params.im;
state.view.scale = params.scale;
state.view.maxIter = (0, state_1.computeMaxIter)(params.scale);
hud.update();
renderer.render(state.view);
},
getState: () => state,
cancelRender: () => renderer.cancel(),
saveBookmark: (name, view) => (0, state_1.saveBookmark)(name, view),
loadBookmark: (name) => (0, state_1.loadBookmark)(name),
deleteBookmark: (name) => (0, state_1.deleteBookmark)(name),
};
}
function start() {
main();
}
start();
};
// ── module: src/constants.ts ──
__mods["src/constants.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CONSTANTS = void 0;
// Tunable constants
exports.CONSTANTS = {
// Iteration settings
maxIterBase: 200,
maxIterScale: 80, // additional iterations per log10 of magnification
maxIterCap: 10000,
// Zoom settings
zoomSpeed: 1.3, // multiplier per wheel tick
dragZoomMin: 5, // min pixels to distinguish drag-zoom from click
maxMagnification: 1e14,
// Progressive rendering
progressivePasses: 4, // 1/4 → 1/2 → 1/1 steps (2^passes = max factor)
tilePixelSize: 64, // worker tile size in pixels
// Julia preview
juliaPreviewEnabled: true,
juliaPreviewDelay: 150, // ms debounce for live preview
// Navigation history
zoomHistoryMax: 50,
// Canvas
canvasWidth: 1280,
canvasHeight: 720,
// HUD auto-hide
hudHideDelay: 4000,
};
};
// ── module: src/state.ts ──
__mods["src/state.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.defaultView = defaultView;
exports.createInitialState = createInitialState;
exports.computeMaxIter = computeMaxIter;
exports.pushZoomHistory = pushZoomHistory;
exports.popZoomHistory = popZoomHistory;
exports.saveBookmark = saveBookmark;
exports.loadBookmark = loadBookmark;
exports.listBookmarks = listBookmarks;
exports.deleteBookmark = deleteBookmark;
exports.viewToHash = viewToHash;
exports.hashToView = hashToView;
const constants_1 = require("./constants");
function defaultView() {
return {
re: -0.5,
im: 0,
scale: 250, // fits the classic Mandelbrot view
maxIter: 200,
palette: 0,
paletteOffset: 0,
setMode: 'mandelbrot',
juliaC: { re: -0.75, im: 0.1 },
};
}
function createInitialState() {
return {
view: defaultView(),
zoomHistory: [],
fps: 0,
renderProgress: 0,
};
}
// Compute max iterations based on zoom depth
function computeMaxIter(scale, base = constants_1.CONSTANTS.maxIterBase, scaleFactor = constants_1.CONSTANTS.maxIterScale) {
const logMag = Math.log10(Math.max(scale, 1));
const extra = Math.floor(logMag * scaleFactor);
return Math.min(base + extra, constants_1.CONSTANTS.maxIterCap);
}
// Save/restore zoom history
function pushZoomHistory(state, max = constants_1.CONSTANTS.zoomHistoryMax) {
state.zoomHistory.push(structuredClone(state.view));
if (state.zoomHistory.length > max) {
state.zoomHistory.shift();
}
}
function popZoomHistory(state) {
if (state.zoomHistory.length === 0)
return null;
return state.zoomHistory.pop() ?? null;
}
// Save view as bookmark
function saveBookmark(name, view) {
try {
const bookmarks = JSON.parse(localStorage.getItem('fractal-bookmarks') ?? '{}');
bookmarks[name] = view;
localStorage.setItem('fractal-bookmarks', JSON.stringify(bookmarks));
}
catch { /* ignore */ }
}
function loadBookmark(name) {
try {
const bookmarks = JSON.parse(localStorage.getItem('fractal-bookmarks') ?? '{}');
return bookmarks[name] ?? null;
}
catch {
return null;
}
}
function listBookmarks() {
try {
const bookmarks = JSON.parse(localStorage.getItem('fractal-bookmarks') ?? '{}');
return Object.keys(bookmarks);
}
catch {
return [];
}
}
function deleteBookmark(name) {
try {
const bookmarks = JSON.parse(localStorage.getItem('fractal-bookmarks') ?? '{}');
delete bookmarks[name];
localStorage.setItem('fractal-bookmarks', JSON.stringify(bookmarks));
}
catch { /* ignore */ }
}
// URL hash state
function viewToHash(view) {
return `#v=${view.re.toFixed(15)},${view.im.toFixed(15)},${view.scale},${view.maxIter},${view.palette},${view.paletteOffset.toFixed(2)},${view.setMode},${view.juliaC.re},${view.juliaC.im}`;
}
function hashToView(hash) {
try {
const m = hash.match(/^#v=([^,]+),([^,]+),([^,]+),([^,]+),([^,]+),([^,]+),([^,]+),([^,]+),([^,]+)$/);
if (!m)
return null;
return {
re: parseFloat(m[1]),
im: parseFloat(m[2]),
scale: parseFloat(m[3]),
maxIter: parseInt(m[4], 10),
palette: parseInt(m[5], 10),
paletteOffset: parseFloat(m[6]),
setMode: m[7],
juliaC: { re: parseFloat(m[8]), im: parseFloat(m[9]) },
};
}
catch {
return null;
}
}
};
// ── module: src/palette.ts ──
__mods["src/palette.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PALETTE_NAMES = exports.PALETTES = void 0;
exports.buildColorLUT = buildColorLUT;
exports.colorize = colorize;
exports.recolorize = recolorize;
exports.PALETTES = [
// 0: Fire — black → red → orange → yellow → white
[
[0.00, 0, 0, 0],
[0.15, 128, 0, 0],
[0.30, 255, 60, 0],
[0.50, 255, 180, 20],
[0.75, 255, 255, 150],
[1.00, 255, 255, 255],
],
// 1: Ocean — deep blue → cyan → teal → light blue → white
[
[0.00, 0, 0, 30],
[0.20, 0, 30, 120],
[0.40, 0, 120, 200],
[0.60, 0, 200, 255],
[0.80, 150, 220, 255],
[1.00, 220, 240, 255],
],
// 2: Monochrome — black → gray → white (smooth luminance)
[
[0.00, 0, 0, 0],
[0.25, 40, 40, 40],
[0.50, 90, 90, 90],
[0.75, 170, 170, 170],
[1.00, 255, 255, 255],
],
// 3: Psychedelic cyclic — full RGB cycle
[
[0.00, 0, 0, 255],
[0.17, 0, 255, 255],
[0.33, 0, 255, 0],
[0.50, 255, 255, 0],
[0.67, 255, 0, 0],
[0.83, 255, 0, 255],
[1.00, 0, 0, 255],
],
// 4: Aurora — violet → purple → pink → magenta
[
[0.00, 10, 0, 40],
[0.25, 60, 0, 120],
[0.50, 180, 20, 160],
[0.75, 240, 100, 200],
[1.00, 180, 20, 80],
],
];
exports.PALETTE_NAMES = ['Fire', 'Ocean', 'Monochrome', 'Psychedelic', 'Aurora'];
// Build a color LUT of 2048 entries for the given palette + offset
// Returns a Uint8ClampedArray of length 2048 * 3 (R, G, B per entry)
function buildColorLUT(paletteIdx, offset, size = 2048) {
const palette = exports.PALETTES[paletteIdx % exports.PALETTES.length];
const lut = new Uint8ClampedArray(size * 3);
for (let i = 0; i < size; i++) {
let t = (i / size + offset) % 1.0;
if (t < 0)
t += 1.0;
// Find surrounding stops
let lo = 0, hi = palette.length - 1;
for (let s = 0; s < palette.length - 1; s++) {
if (palette[s][0] <= t && palette[s + 1][0] >= t) {
lo = s;
hi = s + 1;
break;
}
}
const stopA = palette[lo];
const stopB = palette[hi];
const range = stopB[0] - stopA[0];
const frac = range > 0 ? (t - stopA[0]) / range : 0;
const r = Math.round(stopA[1] + frac * (stopB[1] - stopA[1]));
const g = Math.round(stopA[2] + frac * (stopB[2] - stopA[2]));
const b = Math.round(stopA[3] + frac * (stopB[3] - stopA[3]));
lut[i * 3] = r;
lut[i * 3 + 1] = g;
lut[i * 3 + 2] = b;
}
return lut;
}
// Colorize an iteration buffer using the LUT
// buffer: Float32Array of iteration counts (smooth)
// Returns void — writes directly to imageData.data
function colorize(buffer, maxIter, lut, lutSize, imageData) {
const data = imageData.data;
const len = buffer.length;
for (let i = 0; i < len; i++) {
const idx = i * 4;
const iter = buffer[i];
if (iter >= maxIter) {
// Interior point — black
data[idx] = 0;
data[idx + 1] = 0;
data[idx + 2] = 0;
data[idx + 3] = 255;
}
else {
// Smooth coloring: map fractional iteration to LUT
const norm = (iter % lutSize) / lutSize;
const lutIdx = Math.floor(norm * (lutSize - 1));
data[idx] = lut[lutIdx * 3];
data[idx + 1] = lut[lutIdx * 3 + 1];
data[idx + 2] = lut[lutIdx * 3 + 2];
data[idx + 3] = 255;
}
}
}
// Re-colorize only (no iteration recomputation) — for palette offset changes
function recolorize(buffer, maxIter, lut, lutSize, imageData) {
colorize(buffer, maxIter, lut, lutSize, imageData);
}
};
// ── module: src/compute.ts ──
__mods["src/compute.ts"] = function (exports, require, module) {
"use strict";
// Core fractal computation — allocation-free pixel iteration
// Returns smooth (continuous) iteration counts in a Float32Array buffer
Object.defineProperty(exports, "__esModule", { value: true });
exports.mandelbrotIter = mandelbrotIter;
exports.juliaIter = juliaIter;
exports.computeRegion = computeRegion;
exports.iterationsAt = iterationsAt;
// Compute a single pixel iteration count (Mandelbrot)
function mandelbrotIter(re, im, maxIter) {
let zr = 0, zi = 0;
let zr2 = 0, zi2 = 0;
let iter = 0;
while (zr2 + zi2 <= 4 && iter < maxIter) {
zi = 2 * zr * zi + im;
zr = zr2 - zi2 + re;
zr2 = zr * zr;
zi2 = zi * zi;
iter++;
}
if (iter >= maxIter)
return maxIter;
// Smooth coloring: n + 1 - log2(log|z|)
const logZn = Math.log(zr2 + zi2) * 0.5;
const nu = Math.log(logZn / Math.LN2) / Math.LN2;
return iter + 1 - nu;
}
// Compute a single pixel iteration count (Julia)
function juliaIter(zre, zim, cre, cim, maxIter) {
let zr = zre, zi = zim;
let zr2 = zr * zr, zi2 = zi * zi;
let iter = 0;
while (zr2 + zi2 <= 4 && iter < maxIter) {
zi = 2 * zr * zi + cim;
zr = zr2 - zi2 + cre;
zr2 = zr * zr;
zi2 = zi * zi;
iter++;
}
if (iter >= maxIter)
return maxIter;
const logZn = Math.log(zr2 + zi2) * 0.5;
const nu = Math.log(logZn / Math.LN2) / Math.LN2;
return iter + 1 - nu;
}
// Compute a rectangular region into a Float32Array buffer
// x0,y0: pixel coordinates, w,h: pixel dimensions
// re0,im0: complex center, dx,dy: complex increment per pixel
// setMode: 'mandelbrot' | 'julia', juliaC for Julia mode
// Buffer is written at buf[offset + y * w + x]
function computeRegion(buf, offset, x0, y0, w, h, re0, im0, dx, dy, maxIter, setMode, juliaCre, juliaCim) {
for (let py = 0; py < h; py++) {
const ci = im0 + py * dy;
for (let px = 0; px < w; px++) {
const cr = re0 + px * dx;
let val;
if (setMode === 'mandelbrot') {
val = mandelbrotIter(cr, ci, maxIter);
}
else {
val = juliaIter(cr, ci, juliaCre, juliaCim, maxIter);
}
buf[offset + py * w + px] = val;
}
}
}
// Query: iterations at a specific complex point (for debug interface)
function iterationsAt(cre, cim, maxIter = 200) {
return mandelbrotIter(cre, cim, maxIter);
}
};
// ── module: src/renderer.ts ──
__mods["src/renderer.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Renderer = void 0;
const compute_1 = require("./compute");
const palette_1 = require("./palette");
class Renderer {
constructor(canvas, config = { width: 1280, height: 720 }) {
this.lutSize = 2048;
this.renderId = 0;
this.onProgress = null;
this.onComplete = null;
this.chunkSize = 4; // rows per chunk
this.canvas = canvas;
this.width = config.width;
this.height = config.height;
canvas.width = this.width;
canvas.height = this.height;
this.ctx = canvas.getContext('2d', { willReadFrequently: false });
this.imageData = this.ctx.createImageData(this.width, this.height);
this.iterBuffer = new Float32Array(this.width * this.height);
this.colorLUT = (0, palette_1.buildColorLUT)(0, 0, this.lutSize);
}
setProgressCb(cb) { this.onProgress = cb; }
setCompleteCb(cb) { this.onComplete = cb; }
cancel() {
this.renderId++;
}
recolorize(view) {
this.colorLUT = (0, palette_1.buildColorLUT)(view.palette, view.paletteOffset, this.lutSize);
(0, palette_1.colorize)(this.iterBuffer, view.maxIter, this.colorLUT, this.lutSize, this.imageData);
this.ctx.putImageData(this.imageData, 0, 0);
}
async render(view) {
const renderId = ++this.renderId;
this.colorLUT = (0, palette_1.buildColorLUT)(view.palette, view.paletteOffset, this.lutSize);
const halfW = 0.5 / view.scale;
const halfH = halfW * (this.width / this.height);
const re0 = view.re - halfW;
const im0 = view.im - halfH;
// Progressive passes: start coarse, refine
const passes = [16, 8, 4, 2, 1]; // stride values
let totalPasses = passes.length;
let passIdx = 0;
while (passIdx < totalPasses) {
if (renderId !== this.renderId)
return;
const stride = passes[passIdx];
const pw = Math.ceil(this.width / stride);
const ph = Math.ceil(this.height / stride);
const dx = (2 * halfW) / pw;
const dy = (2 * halfH) / ph;
// Compute at reduced resolution
const tileBuf = new Float32Array(pw * ph);
(0, compute_1.computeRegion)(tileBuf, 0, 0, 0, pw, ph, re0, im0, dx, dy, view.maxIter, view.setMode, view.juliaC.re, view.juliaC.im);
// Upsample to full buffer
for (let sy = 0; sy < ph; sy++) {
for (let sx = 0; sx < pw; sx++) {
for (let my = 0; my < stride && (sy * stride + my) < this.height; my++) {
for (let mx = 0; mx < stride && (sx * stride + mx) < this.width; mx++) {
const destY = sy * stride + my;
const destX = sx * stride + mx;
this.iterBuffer[destY * this.width + destX] = tileBuf[sy * pw + sx];
}
}
}
}
// Colorize and blit immediately
(0, palette_1.colorize)(this.iterBuffer, view.maxIter, this.colorLUT, this.lutSize, this.imageData);
this.ctx.putImageData(this.imageData, 0, 0);
const progress = (passIdx + 1) / totalPasses;
this.onProgress?.(progress);
passIdx++;
// Yield to keep UI responsive
await new Promise(r => setTimeout(r, 0));
}
if (renderId === this.renderId) {
this.onProgress?.(1);
this.onComplete?.();
}
}
// Render synchronously (for small regions or final pass)
renderSync(view) {
this.colorLUT = (0, palette_1.buildColorLUT)(view.palette, view.paletteOffset, this.lutSize);
const halfW = 0.5 / view.scale;
const halfH = halfW * (this.width / this.height);
const re0 = view.re - halfW;
const im0 = view.im - halfH;
const dx = (2 * halfW) / this.width;
const dy = (2 * halfH) / this.height;
(0, compute_1.computeRegion)(this.iterBuffer, 0, 0, 0, this.width, this.height, re0, im0, dx, dy, view.maxIter, view.setMode, view.juliaC.re, view.juliaC.im);
(0, palette_1.colorize)(this.iterBuffer, view.maxIter, this.colorLUT, this.lutSize, this.imageData);
this.ctx.putImageData(this.imageData, 0, 0);
this.onProgress?.(1);
this.onComplete?.();
}
dispose() { }
}
exports.Renderer = Renderer;
};
// ── module: src/input.ts ──
__mods["src/input.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.InputHandler = void 0;
const constants_1 = require("./constants");
const state_1 = require("./state");
class InputHandler {
constructor(canvas, state) {
this.isDragging = false;
this.isPanning = false;
this.dragStart = { x: 0, y: 0 };
this.panStart = { re: 0, im: 0, x: 0, y: 0 };
this.zoomBox = null;
this.zoomBoxCtx = null;
this.onRenderRequested = null;
this.onJuliaHover = null;
this.lastMouse = { x: 0, y: 0 };
this.lastClickTime = 0;
this.canvas = canvas;
this.state = state;
// Create overlay canvas for zoom box
this.zoomBox = document.createElement('canvas');
this.zoomBox.style.cssText = `
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
pointer-events: none; z-index: 5;
`;
canvas.parentElement?.appendChild(this.zoomBox);
this.resizeZoomBox();
this.zoomBoxCtx = this.zoomBox.getContext('2d');
this.setupEvents();
}
setRenderRequested(cb) { this.onRenderRequested = cb; }
setJuliaHover(cb) { this.onJuliaHover = cb; }
resizeZoomBox() {
if (!this.zoomBox)
return;
this.zoomBox.width = this.canvas.width;
this.zoomBox.height = this.canvas.height;
}
pixelToComplex(px, py) {
const v = this.state.view;
const halfW = 0.5 / v.scale;
const halfH = halfW * (this.canvas.width / this.canvas.height);
const re = v.re + (px / this.canvas.width - 0.5) * 2 * halfW;
const im = v.im + (py / this.canvas.height - 0.5) * 2 * halfH;
return { re, im };
}
complexToPixel(re, im) {
const v = this.state.view;
const halfW = 0.5 / v.scale;
const halfH = halfW * (this.canvas.width / this.canvas.height);
const x = ((re - v.re) / (2 * halfW) + 0.5) * this.canvas.width;
const y = ((im - v.im) / (2 * halfH) + 0.5) * this.canvas.height;
return { x, y };
}
setupEvents() {
// Mouse down: start drag or pan
this.canvas.addEventListener('mousedown', (e) => {
if (e.button !== 0)
return;
this.isDragging = true;
this.dragStart = { x: e.offsetX, y: e.offsetY };
this.lastClickTime = Date.now();
});
// Mouse move: draw zoom box or pan
this.canvas.addEventListener('mousemove', (e) => {
this.lastMouse = { x: e.offsetX, y: e.offsetY };
if (!this.isDragging) {
// Hover preview for Julia
if (this.state.view.setMode === 'mandelbrot' && this.onJuliaHover) {
const pt = this.pixelToComplex(e.offsetX, e.offsetY);
this.onJuliaHover(pt.re, pt.im);
}
return;
}
const dx = Math.abs(e.offsetX - this.dragStart.x);
const dy = Math.abs(e.offsetY - this.dragStart.y);
if (dx > constants_1.CONSTANTS.dragZoomMin || dy > constants_1.CONSTANTS.dragZoomMin) {
// Drawing zoom box
this.isPanning = false;
this.drawZoomBox(Math.min(this.dragStart.x, e.offsetX), Math.min(this.dragStart.y, e.offsetY), Math.abs(e.offsetX - this.dragStart.x), Math.abs(e.offsetY - this.dragStart.y));
}
});
// Mouse up: zoom or pan
this.canvas.addEventListener('mouseup', (e) => {
if (!this.isDragging)
return;
this.isDragging = false;
const dx = Math.abs(e.offsetX - this.dragStart.x);
const dy = Math.abs(e.offsetX - this.dragStart.y);
if (dx > constants_1.CONSTANTS.dragZoomMin || dy > constants_1.CONSTANTS.dragZoomMin) {
// Zoom into rectangle
const x1 = Math.min(this.dragStart.x, e.offsetX);
const y1 = Math.min(this.dragStart.y, e.offsetY);
const x2 = Math.max(this.dragStart.x, e.offsetX);
const y2 = Math.max(this.dragStart.y, e.offsetY);
const c1 = this.pixelToComplex(x1, y1);
const c2 = this.pixelToComplex(x2, y2);
this.clearZoomBox();
(0, state_1.pushZoomHistory)(this.state);
const v = this.state.view;
v.re = (c1.re + c2.re) / 2;
v.im = (c1.im + c2.im) / 2;
const oldW = 1 / v.scale;
const newW = Math.abs(c2.re - c1.re);
v.scale = 1 / newW;
if (v.scale > constants_1.CONSTANTS.maxMagnification) {
v.scale = constants_1.CONSTANTS.maxMagnification;
}
v.maxIter = (0, state_1.computeMaxIter)(v.scale);
this.onRenderRequested?.();
}
else {
// Click: could be Julia mode switch (handled by parent)
}
});
// Wheel zoom
this.canvas.addEventListener('wheel', (e) => {
e.preventDefault();
const v = this.state.view;
const oldScale = v.scale;
const factor = e.deltaY < 0 ? constants_1.CONSTANTS.zoomSpeed : 1 / constants_1.CONSTANTS.zoomSpeed;
const newScale = oldScale * factor;
if (newScale > constants_1.CONSTANTS.maxMagnification) {
v.scale = constants_1.CONSTANTS.maxMagnification;
}
else if (newScale < 1) {
v.scale = 1;
}
else {
v.scale = newScale;
}
// Zoom toward cursor
const pt = this.pixelToComplex(e.offsetX, e.offsetY);
const oldPt = this.pixelToComplexAtScale(e.offsetX, e.offsetY, oldScale);
v.re += oldPt.re - pt.re;
v.im += oldPt.im - pt.im;
v.maxIter = (0, state_1.computeMaxIter)(v.scale);
(0, state_1.pushZoomHistory)(this.state);
this.onRenderRequested?.();
}, { passive: false });
// Keyboard
window.addEventListener('keydown', (e) => {
const v = this.state.view;
const panAmt = 0.2 / v.scale;
switch (e.key) {
case 'ArrowLeft':
v.re -= panAmt;
e.preventDefault();
this.onRenderRequested?.();
break;
case 'ArrowRight':
v.re += panAmt;
e.preventDefault();
this.onRenderRequested?.();
break;
case 'ArrowUp':
v.im -= panAmt;
e.preventDefault();
this.onRenderRequested?.();
break;
case 'ArrowDown':
v.im += panAmt;
e.preventDefault();
this.onRenderRequested?.();
break;
case '+':
case '=':
v.scale *= constants_1.CONSTANTS.zoomSpeed;
if (v.scale > constants_1.CONSTANTS.maxMagnification)
v.scale = constants_1.CONSTANTS.maxMagnification;
v.maxIter = (0, state_1.computeMaxIter)(v.scale);
(0, state_1.pushZoomHistory)(this.state);
this.onRenderRequested?.();
break;
case '-':
case '_':
v.scale /= constants_1.CONSTANTS.zoomSpeed;
if (v.scale < 1)
v.scale = 1;
v.maxIter = (0, state_1.computeMaxIter)(v.scale);
(0, state_1.pushZoomHistory)(this.state);
this.onRenderRequested?.();
break;
case 'j':
case 'J':
// Toggle Julia mode at current cursor position
const pt = this.pixelToComplex(this.lastMouse.x, this.lastMouse.y);
v.juliaC = { re: pt.re, im: pt.im };
v.setMode = v.setMode === 'mandelbrot' ? 'julia' : 'mandelbrot';
v.maxIter = (0, state_1.computeMaxIter)(v.scale);
this.onRenderRequested?.();
break;
case 'z':
// Undo
this.onRenderRequested?.();
break;
}
});
}
pixelToComplexAtScale(px, py, scale) {
const v = this.state.view;
const halfW = 0.5 / scale;
const halfH = halfW * (this.canvas.width / this.canvas.height);
const re = v.re + (px / this.canvas.width - 0.5) * 2 * halfW;
const im = v.im + (py / this.canvas.height - 0.5) * 2 * halfH;
return { re, im };
}
drawZoomBox(x, y, w, h) {
if (!this.zoomBoxCtx || !this.zoomBox)
return;
this.zoomBoxCtx.clearRect(0, 0, this.zoomBox.width, this.zoomBox.height);
this.zoomBoxCtx.strokeStyle = 'rgba(255, 255, 255, 0.8)';
this.zoomBoxCtx.lineWidth = 2;
this.zoomBoxCtx.setLineDash([6, 3]);
this.zoomBoxCtx.strokeRect(x, y, w, h);
// Inverted overlay inside box
this.zoomBoxCtx.fillStyle = 'rgba(0, 0, 0, 0.15)';
this.zoomBoxCtx.fillRect(x, y, w, h);
this.zoomBoxCtx.setLineDash([]);
}
clearZoomBox() {
if (this.zoomBoxCtx && this.zoomBox) {
this.zoomBoxCtx.clearRect(0, 0, this.zoomBox.width, this.zoomBox.height);
}
}
dispose() {
if (this.zoomBox && this.zoomBox.parentElement) {
this.zoomBox.parentElement.removeChild(this.zoomBox);
}
}
}
exports.InputHandler = InputHandler;
};
// ── module: src/hud.ts ──
__mods["src/hud.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.HUD = void 0;
const state_1 = require("./state");
const palette_1 = require("./palette");
class HUD {
constructor(container, state) {
this.hideTimer = null;
this.paletteSlider = null;
this.iterSlider = null;
// Callbacks from renderer
this.requestRender = (() => { });
this.requestRecolorize = (() => { });
this.container = container;
this.state = state;
this.build();
}
build() {
const hud = document.createElement('div');
hud.id = 'hud';
hud.className = 'hud-panel';
hud.innerHTML = `
<div class="hud-header">
<span class="hud-title">Fractal Explorer</span>
<div class="hud-controls">
<button id="btn-back" title="Back (Zoom History)" aria-label="Back">◀</button>
<button id="btn-reset" title="Reset View" aria-label="Reset">⟲</button>
<button id="btn-bookmark" title="Bookmark View" aria-label="Bookmark">★</button>
</div>
</div>
<div class="hud-stats">
<div class="stat-row">
<span class="stat-label">Magnification:</span>
<span id="stat-mag" class="stat-value">×250</span>
</div>
<div class="stat-row">
<span class="stat-label">Center:</span>
<span id="stat-center" class="stat-value">-0.5 + 0i</span>
</div>
<div class="stat-row">
<span class="stat-label">Mode:</span>
<span id="stat-mode" class="stat-value">Mandelbrot</span>
</div>
<div class="stat-row">
<span class="stat-label">Iterations:</span>
<span id="stat-iter" class="stat-value">200</span>
</div>
<div class="stat-row">
<span class="stat-label">FPS:</span>
<span id="stat-fps" class="stat-value">—</span>
</div>
</div>
<div class="hud-palettes" id="palette-selector">
<span class="hud-section-label">Palette:</span>
<div class="palette-chips" id="palette-chips"></div>
</div>
<div class="hud-sliders">
<div class="slider-row">
<label for="slider-offset">Color Offset</label>
<input type="range" id="slider-offset" min="0" max="1" step="0.001" value="0">
</div>
<div class="slider-row">
<label for="slider-maxiter">Max Iterations</label>
<input type="range" id="slider-maxiter" min="50" max="10000" step="10" value="200">
<span id="iter-val">200</span>
</div>
</div>
<div id="bookmarks-panel" class="bookmarks-panel" style="display:none;">
<span class="hud-section-label">Bookmarks:</span>
<div id="bookmark-list" class="bookmark-list"></div>
</div>
<div class="hud-help">
<span class="help-text">Drag: zoom | Scroll: zoom to cursor | ←→↑↓: pan | +/-: zoom | J: toggle Julia</span>
</div>
`;
this.container.appendChild(hud);
this.setupPaletteChips();
this.setupControls();
}
setupPaletteChips() {
const chipsContainer = document.getElementById('palette-chips');
if (!chipsContainer)
return;
palette_1.PALETTE_NAMES.forEach((name, idx) => {
const chip = document.createElement('button');
chip.className = `palette-chip ${idx === this.state.view.palette ? 'active' : ''}`;
chip.textContent = name;
chip.title = `${name} (click to select)`;
chip.setAttribute('aria-label', `Select ${name} palette`);
chip.dataset.palette = String(idx);
chip.addEventListener('click', () => {
this.state.view.palette = idx;
this.updatePaletteChips();
this.requestRecolorize();
});
chipsContainer.appendChild(chip);
});
}
updatePaletteChips() {
const chips = document.querySelectorAll('.palette-chip');
chips.forEach((chip, idx) => {
chip.classList.toggle('active', idx === this.state.view.palette);
});
}
setupControls() {
// Back button
const backBtn = document.getElementById('btn-back');
backBtn?.addEventListener('click', () => {
const prev = (0, state_1.popZoomHistory)(this.state);
if (prev) {
Object.assign(this.state.view, prev);
this.update();
this.requestRender();
}
});
// Reset button
const resetBtn = document.getElementById('btn-reset');
resetBtn?.addEventListener('click', () => {
this.state.view.re = -0.5;
this.state.view.im = 0;
this.state.view.scale = 250;
this.state.view.setMode = 'mandelbrot';
this.state.view.maxIter = (0, state_1.computeMaxIter)(250);
this.state.view.paletteOffset = 0;
this.state.view.palette = 0;
this.update();
this.requestRender();
});
// Bookmark button
const bookmarkBtn = document.getElementById('btn-bookmark');
const bmPanel = document.getElementById('bookmarks-panel');
bookmarkBtn?.addEventListener('click', () => {
if (bmPanel.style.display === 'none') {
bmPanel.style.display = 'block';
this.renderBookmarks();
}
else {
bmPanel.style.display = 'none';
}
});
// Color offset slider
const offsetSlider = document.getElementById('slider-offset');
offsetSlider?.addEventListener('input', () => {
this.state.view.paletteOffset = parseFloat(offsetSlider.value);
this.requestRecolorize();
});
// Max iterations slider
const iterSlider = document.getElementById('slider-maxiter');
iterSlider?.addEventListener('input', () => {
this.state.view.maxIter = parseInt(iterSlider.value, 10);
this.requestRender();
});
}
renderBookmarks() {
const list = document.getElementById('bookmark-list');
if (!list)
return;
list.innerHTML = '';
const names = (0, state_1.listBookmarks)();
if (names.length === 0) {
list.innerHTML = '<div class="bm-empty">No bookmarks saved</div>';
return;
}
// Save current view
const doSave = () => {
const name = prompt('Bookmark name:');
if (name) {
(0, state_1.saveBookmark)(name, structuredClone(this.state.view));
this.renderBookmarks();
}
};
const saveBtn = document.createElement('button');
saveBtn.className = 'bm-btn';
saveBtn.textContent = 'Save Current';
saveBtn.addEventListener('click', doSave);
list.appendChild(saveBtn);
names.forEach(name => {
const btn = document.createElement('button');
btn.className = 'bm-btn';
btn.textContent = name;
btn.addEventListener('click', () => {
const bm = (0, state_1.loadBookmark)(name);
if (bm) {
Object.assign(this.state.view, bm);
this.update();
this.requestRender();
}
});
const del = document.createElement('button');
del.className = 'bm-del';
del.textContent = '×';
del.addEventListener('click', () => {
(0, state_1.deleteBookmark)(name);
this.renderBookmarks();
});
btn.appendChild(del);
list.appendChild(btn);
});
}
update() {
const v = this.state.view;
const mag = document.getElementById('stat-mag');
if (mag)
mag.textContent = `×${v.scale.toExponential(2)}`;
const center = document.getElementById('stat-center');
if (center)
center.textContent = `${v.re.toExponential(4)} ${v.im >= 0 ? '+' : ''}${v.im.toExponential(4)}i`;
const mode = document.getElementById('stat-mode');
if (mode)
mode.textContent = v.setMode === 'mandelbrot' ? 'Mandelbrot' : `Julia (${v.juliaC.re.toFixed(4)}, ${v.juliaC.im.toFixed(4)})`;
const iter = document.getElementById('stat-iter');
if (iter)
iter.textContent = String(v.maxIter);
const iterSlider = document.getElementById('slider-maxiter');
if (iterSlider)
iterSlider.value = String(v.maxIter);
const iterVal = document.getElementById('iter-val');
if (iterVal)
iterVal.textContent = String(v.maxIter);
const offsetSlider = document.getElementById('slider-offset');
if (offsetSlider)
offsetSlider.value = String(v.paletteOffset);
this.updatePaletteChips();
this.resetHideTimer();
}
updateFps(fps) {
const el = document.getElementById('stat-fps');
if (el)
el.textContent = String(Math.round(fps));
}
updateProgress(progress) {
this.resetHideTimer();
}
// Auto-hide
resetHideTimer() {
if (this.hideTimer)
clearTimeout(this.hideTimer);
const hud = document.getElementById('hud');
if (hud)
hud.classList.remove('hud-hidden');
this.hideTimer = window.setTimeout(() => {
if (hud)
hud.classList.add('hud-hidden');
}, 4000);
}
setRenderCallback(cb) { this.requestRender = cb; }
setRecolorizeCallback(cb) { this.requestRecolorize = cb; }
}
exports.HUD = HUD;
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"ThinkingCap-Qwen3.6-27B-GGUF","provider":"brigid","persona":"typescript","sessionId":"095f9ee8-e1e4-489e-a47b-c1c8bfb82007","tokensIn":1169758,"tokensOut":26680,"tokensTotal":1196438,"cost":0,"turns":46,"toolCalls":46,"failedToolCalls":0,"timestamp":"2026-08-03T19:42:35.634Z"} -->