Fractal Explorer
Qwen 3.6 27B Fable Fusion 711 Uncensored Heretic NM DAU NEO MAX MTP 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 63,214 bytes · SHA-256 f69f5a15d931
<!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: base ── */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body, #app {
width: 100%;
height: 100%;
overflow: hidden;
background: #000;
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
color: #fff;
}
canvas#fractalCanvas {
display: block;
width: 100%;
height: 100%;
cursor: grab;
image-rendering: auto;
}
.hidden {
display: none !important;
}
/* ── css: hud ── */
#hud {
position: fixed;
inset: 0;
pointer-events: none;
transition: opacity 0.3s ease;
z-index: 10;
}
#hud.hidden {
opacity: 0;
}
#hud * {
pointer-events: auto;
}
#hudTop {
position: absolute;
top: 12px;
left: 12px;
right: 12px;
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
}
#hudLeft, #hudCenter, #hudRight {
display: flex;
align-items: center;
gap: 8px;
}
#hudLeft button, #hudRight button {
width: 32px;
height: 32px;
border: none;
border-radius: 8px;
background: rgba(15, 23, 42, 0.75);
backdrop-filter: blur(12px);
color: #e2e8f0;
font-size: 16px;
cursor: pointer;
transition: all 0.15s ease;
display: flex;
align-items: center;
justify-content: center;
}
#hudLeft button:hover, #hudRight button:hover {
background: rgba(30, 41, 59, 0.9);
transform: scale(1.05);
}
#hudMagnify {
font-size: 14px;
font-weight: 600;
padding: 6px 12px;
border-radius: 8px;
background: rgba(15, 23, 42, 0.75);
backdrop-filter: blur(12px);
font-variant-numeric: tabular-nums;
}
#hudMode {
font-size: 16px;
font-weight: 600;
padding: 6px 14px;
border-radius: 8px;
background: rgba(15, 23, 42, 0.75);
backdrop-filter: blur(12px);
letter-spacing: 0.03em;
text-transform: uppercase;
font-size: 11px;
}
#hudFPS {
font-size: 12px;
padding: 6px 10px;
border-radius: 8px;
background: rgba(15, 23, 42, 0.75);
backdrop-filter: blur(12px);
color: #6ee7b7;
font-variant-numeric: tabular-nums;
}
#hudBottom {
position: absolute;
bottom: 60px;
left: 12px;
right: 12px;
display: flex;
flex-direction: column;
gap: 8px;
}
#hudCoords {
font-size: 12px;
padding: 6px 10px;
border-radius: 8px;
background: rgba(15, 23, 42, 0.75);
backdrop-filter: blur(12px);
color: #94a3b8;
font-family: 'SF Mono', 'Fira Code', monospace;
font-size: 11px;
}
#hudControls {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
padding: 8px 12px;
border-radius: 10px;
background: rgba(15, 23, 42, 0.75);
backdrop-filter: blur(12px);
}
#hudControls label {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
color: #94a3b8;
white-space: nowrap;
}
#hudControls input[type="range"] {
width: 100px;
accent-color: #6366f1;
}
#hudControls input[type="checkbox"] {
accent-color: #6366f1;
width: 14px;
height: 14px;
}
#iterValue {
font-family: 'SF Mono', monospace;
min-width: 32px;
color: #e2e8f0;
}
/* ── css: paletteBar ── */
#paletteBar {
position: absolute;
bottom: 16px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 8px;
padding: 8px 12px;
border-radius: 12px;
background: rgba(15, 23, 42, 0.8);
backdrop-filter: blur(12px);
border: 1px solid rgba(75, 85, 99, 0.4);
}
.palette-chip {
width: 36px;
height: 16px;
border-radius: 8px;
cursor: pointer;
transition: all 0.15s ease;
border: 2px solid transparent;
position: relative;
}
.palette-chip:hover {
transform: scale(1.15);
}
.palette-chip.active {
border-color: #fff;
box-shadow: 0 0 8px rgba(99, 102, 241, 0.6);
}
#colorOffsetBar {
position: absolute;
bottom: 50px;
left: 50%;
transform: translateX(-50%);
padding: 4px 10px;
border-radius: 10px;
background: rgba(15, 23, 42, 0.75);
backdrop-filter: blur(12px);
}
#colorOffsetBar input[type="range"] {
width: 140px;
accent-color: #6366f1;
}
/* ── css: zoomRect ── */
#zoomRectOverlay {
position: fixed;
border: 2px dashed rgba(255, 255, 255, 0.9);
background: rgba(255, 255, 255, 0.08);
pointer-events: none;
z-index: 20;
display: none;
box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.25);
}
/* ── css: juliaPreview ── */
#juliaPreview {
position: fixed;
bottom: 16px;
right: 16px;
z-index: 15;
pointer-events: none;
opacity: 0;
transition: opacity 0.2s ease;
}
#juliaPreview.visible {
opacity: 1;
}
#juliaCanvas {
width: 160px;
height: 160px;
border-radius: 12px;
border: 2px solid rgba(75, 85, 99, 0.5);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
image-rendering: auto;
}
#juliaLabel {
text-align: center;
font-size: 10px;
color: #94a3b8;
margin-top: 4px;
text-transform: uppercase;
letter-spacing: 0.08em;
}
/* ── css: bookmarks ── */
#bookmarkPanel {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
min-width: 320px;
max-width: 400px;
max-height: 60vh;
padding: 0;
border-radius: 16px;
background: rgba(15, 23, 42, 0.95);
backdrop-filter: blur(20px);
border: 1px solid rgba(75, 85, 99, 0.5);
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6);
z-index: 100;
overflow: hidden;
}
.panelHeader {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14px 16px;
border-bottom: 1px solid rgba(75, 85, 99, 0.4);
font-weight: 600;
font-size: 14px;
}
.panelHeader button {
background: none;
border: none;
color: #94a3b8;
font-size: 18px;
cursor: pointer;
padding: 0 4px;
}
.panelHeader button:hover {
color: #e2e8f0;
}
#bookmarkPanel > input {
width: 100%;
padding: 10px 16px;
border: none;
border-bottom: 1px solid rgba(75, 85, 99, 0.4);
background: transparent;
color: #e2e8f0;
font-size: 13px;
outline: none;
}
#bookmarkPanel > input::placeholder {
color: #64748b;
}
#bookmarkPanel > input:focus {
border-bottom-color: #6366f1;
}
#btnSaveBookmark {
width: calc(100% - 32px);
margin: 10px 16px;
padding: 8px;
border: none;
border-radius: 8px;
background: #6366f1;
color: #fff;
font-size: 13px;
cursor: pointer;
font-weight: 500;
}
#btnSaveBookmark:hover {
background: #4f46e5;
}
#bookmarkList {
padding: 8px;
max-height: 300px;
overflow-y: auto;
}
.bookmarkItem {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 10px;
border-radius: 8px;
margin-bottom: 4px;
transition: background 0.15s ease;
}
.bookmarkItem:hover {
background: rgba(75, 85, 99, 0.3);
}
.bookmarkName {
flex: 1;
font-size: 13px;
color: #e2e8f0;
}
.bookmarkItem button {
padding: 4px 10px;
border: none;
border-radius: 6px;
font-size: 11px;
cursor: pointer;
transition: all 0.15s ease;
}
.bookmarkItem button:first-child {
background: rgba(99, 102, 241, 0.3);
color: #a5b4fc;
}
.bookmarkItem button:first-child:hover {
background: rgba(99, 102, 241, 0.5);
}
.bookmarkItem button:last-child {
background: rgba(239, 68, 68, 0.2);
color: #fca5a5;
padding: 4px 6px;
}
.bookmarkItem button:last-child:hover {
background: rgba(239, 68, 68, 0.4);
}
.emptyBookmarks {
padding: 20px;
text-align: center;
color: #64748b;
font-size: 13px;
}
#bookmarkPanel.hidden {
display: none !important;
}
</style>
</head>
<body>
<div id="app">
<canvas id="fractalCanvas"></canvas>
<div id="hud">
<div id="hudTop">
<div id="hudLeft">
<button id="btnReset" title="Reset view">⟲</button>
<button id="btnBack" title="Zoom out / history back">◁</button>
<span id="hudMagnify" title="Magnification">×1</span>
</div>
<div id="hudCenter">
<span id="hudMode">Mandelbrot</span>
</div>
<div id="hudRight">
<span id="hudFPS" title="Frames per second">60 FPS</span>
<button id="btnBookmarks" title="Bookmarks">★</button>
</div>
</div>
<div id="hudBottom">
<div id="hudCoords">
<span id="hudCenterCoords">Re: 0, Im: 0</span>
</div>
<div id="hudControls">
<label title="Iteration limit">
Max Iter:
<input type="range" id="sliderIter" min="50" max="2000" step="10" value="300">
<span id="iterValue">300</span>
</label>
<label title="Smooth coloring">
Smooth
<input type="checkbox" id="chkSmooth" checked="">
</label>
<label title="Auto iterations at zoom">
Auto
<input type="checkbox" id="chkAutoIter" checked="">
</label>
</div>
</div>
<div id="paletteBar"></div>
<div id="colorOffsetBar">
<label title="Palette offset">
<input type="range" id="sliderColorOffset" min="0" max="1" step="0.005" value="0">
</label>
</div>
</div>
<div id="zoomRectOverlay"></div>
<div id="juliaPreview">
<canvas id="juliaCanvas" width="240" height="240"></canvas>
<div id="juliaLabel">Julia Preview</div>
</div>
<div id="bookmarkPanel" class="hidden">
<div class="panelHeader">
<span>Bookmarks</span>
<button id="btnCloseBookmarks">✕</button>
</div>
<input type="text" id="bookmarkName" placeholder="Bookmark name...">
<button id="btnSaveBookmark">Save</button>
<div id="bookmarkList"></div>
</div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./config":"src/config.ts","./state":"src/state.ts","./renderer":"src/renderer.ts","./input":"src/input.ts","./juliaPreview":"src/juliaPreview.ts","./ui":"src/ui.ts"},"src/state.ts":{"./config":"src/config.ts"},"src/renderer.ts":{"./config":"src/config.ts","./state":"src/state.ts"},"src/input.ts":{"./config":"src/config.ts","./state":"src/state.ts"},"src/juliaPreview.ts":{"./config":"src/config.ts","./state":"src/state.ts"},"src/ui.ts":{"./config":"src/config.ts","./state":"src/state.ts"}};
function __require(id) {
if (__cache[id]) return __cache[id].exports;
var module = __cache[id] = { exports: {} };
var factory = __mods[id];
if (!factory) throw new Error("Module not found: " + id);
factory(module.exports, function (spec) {
var target = (__map[id] && __map[id][spec]) || spec;
return __require(target);
}, module);
return module.exports;
}
// ── module: src/main.ts ──
__mods["src/main.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const state_1 = require("./state");
const renderer_1 = require("./renderer");
const input_1 = require("./input");
const juliaPreview_1 = require("./juliaPreview");
const ui_1 = require("./ui");
// ===== Main Application =====
function main() {
const canvas = document.getElementById('fractalCanvas');
const juliaCanvas = document.getElementById('juliaCanvas');
const rectOverlay = document.getElementById('zoomRectOverlay');
// Initialize managers
const stateMgr = new state_1.ViewStateManager();
const colorizer = new state_1.PaletteColorizer();
// Renderer
const renderer = new renderer_1.FractalRenderer();
const renderReq = { canvas, stateMgr, colorizer };
renderer.setRequest(renderReq);
// Julia preview
const juliaPreview = new juliaPreview_1.JuliaPreviewRenderer(juliaCanvas, stateMgr);
// UI controller
const ui = new ui_1.UIController(stateMgr, (idx) => { stateMgr.setPaletteIndex(idx); renderer.scheduleRender(); }, (name) => { ui.saveBookmark(name); }, (bookmark) => { ui.loadBookmark(bookmark); renderer.scheduleRenderImmediate(); }, (id) => { ui.deleteBookmark(id); });
// Input handler
const inputHandler = new input_1.InputHandler(canvas, stateMgr, () => renderer.scheduleRender(), (re, im) => {
const state = stateMgr.get();
if (state.setType === 'mandelbrot') {
juliaPreview.updatePreview(re, im);
}
}, rectOverlay);
// Resize canvas to fill viewport
function resizeCanvas() {
const dpr = Math.min(window.devicePixelRatio || 1, 2);
canvas.width = window.innerWidth * dpr;
canvas.height = window.innerHeight * dpr;
canvas.style.width = window.innerWidth + 'px';
canvas.style.height = window.innerHeight + 'px';
renderer.scheduleRender();
}
window.addEventListener('resize', () => {
resizeCanvas();
});
// Initial render
resizeCanvas();
ui.updateUI();
// FPS counter loop
setInterval(() => ui.updateFPS(), 100);
// Debug API on window
const fractalAPI = {
iterationsAt: (re, im) => stateMgr.iterationsAt(re, im),
setView: ({ re, im, scale }) => {
stateMgr.setView(re, im, scale, false);
renderer.scheduleRenderImmediate();
ui.updateUI();
},
};
window.fractal = fractalAPI;
// Show Julia preview when hovering canvas in Mandelbrot mode
const juliaPreviewEl = document.getElementById('juliaPreview');
canvas.addEventListener('mouseenter', () => {
if (juliaPreviewEl && stateMgr.get().setType === 'mandelbrot') {
juliaPreviewEl.classList.add('visible');
}
});
canvas.addEventListener('mouseleave', () => {
if (juliaPreviewEl) {
juliaPreviewEl.classList.remove('visible');
juliaPreview.hide();
}
});
// Override setSetType to manage Julia preview visibility
const origSetType = stateMgr.setSetType.bind(stateMgr);
stateMgr.setSetType = (type) => {
origSetType(type);
if (juliaPreviewEl) {
if (type === 'mandelbrot') {
juliaPreviewEl.classList.add('visible');
const c = stateMgr.get().juliaC || { re: -0.7269, im: 0.1889 };
juliaPreview.updatePreview(c.re, c.im);
}
else {
juliaPreviewEl.classList.remove('visible');
juliaPreview.hide();
}
}
};
// Also expose helpers for testing
window.fractalDragZoom = (x1, y1, x2, y2) => {
const rect = canvas.getBoundingClientRect();
const aspect = rect.width / rect.height;
const state = stateMgr.get();
const re1 = state.re + (x1 / rect.width - 0.5) * state.scale * aspect;
const im1 = state.im + (y1 / rect.height - 0.5) * state.scale;
const re2 = state.re + (x2 / rect.width - 0.5) * state.scale * aspect;
const im2 = state.im + (y2 / rect.height - 0.5) * state.scale;
const newRe = (re1 + re2) / 2;
const newIm = (im1 + im2) / 2;
const reRange = Math.abs(re2 - re1);
const imRange = Math.abs(im2 - im1);
let newScale = Math.max(reRange / aspect, imRange);
newScale = Math.max(newScale, 1e-14);
stateMgr.setView(newRe, newIm, newScale, true);
renderer.scheduleRenderImmediate();
ui.updateUI();
};
console.log('Fractal Explorer loaded');
console.log('Debug API: window.fractal.iterationsAt(re, im), window.fractal.setView({re, im, scale})');
console.log('Controls: Scroll to zoom, drag to pan, Shift+drag for rect zoom, J=Julia mode, arrows/+/- to navigate');
}
// Run on load (DOM already parsed when script executes)
main();
};
// ── module: src/config.ts ──
__mods["src/config.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PALETTES = exports.CONFIG = void 0;
// ===== Tunable Constants =====
exports.CONFIG = {
// Iteration scaling: base iterations + scaling with zoom depth
iterationsBase: 80,
iterationsScale: 60, // added per log10(magnification)
iterationsMaxCap: 2000,
// Zoom behavior
wheelZoomFactor: 0.9, // per wheel delta unit (inverted; <1 = zoom in on scroll down)
rectZoomMinPixels: 15, // minimum drag rectangle size in pixels
maxMagnification: 1e13, // precision limit warning threshold
maxMagnificationHard: 1e14, // hard stop
// Progressive rendering passes
progressivePasses: [16, 8, 4, 2, 1], // downscale factors to render progressively
progressiveMinTime: 60, // min ms per progressive pass before refining
// Initial view (classic Mandelbrot)
initialRe: -0.5,
initialIm: 0,
initialScale: 3, // viewport width in complex plane units
// Julia preview
juliaPreviewSize: 240,
juliaRenderScale: 2.5, // Julia set viewport width
// Performance
renderTimeout: 100, // ms before starting render (debounce)
};
exports.PALETTES = [
{
name: "Fire",
swatch: "linear-gradient(90deg, #000, #f00, #ff0, #fff)",
stops: [
{ t: 0, r: 0, g: 0, b: 0 },
{ t: 0.15, r: 30, g: 0, b: 50 },
{ t: 0.3, r: 180, g: 20, b: 0 },
{ t: 0.5, r: 255, g: 160, b: 0 },
{ t: 0.7, r: 255, g: 255, b: 100 },
{ t: 1, r: 255, g: 255, b: 255 },
],
},
{
name: "Ocean",
swatch: "linear-gradient(90deg, #000, #00f, #0af, #aff)",
stops: [
{ t: 0, r: 0, g: 0, b: 0 },
{ t: 0.2, r: 0, g: 10, b: 60 },
{ t: 0.4, r: 0, g: 40, b: 140 },
{ t: 0.6, r: 0, g: 120, b: 200 },
{ t: 0.8, r: 50, g: 200, b: 240 },
{ t: 1, r: 180, g: 240, b: 255 },
],
},
{
name: "Monochrome",
swatch: "linear-gradient(90deg, #000, #333, #888, #ccc, #fff)",
stops: [
{ t: 0, r: 0, g: 0, b: 0 },
{ t: 0.25, r: 40, g: 40, b: 45 },
{ t: 0.5, r: 100, g: 100, b: 110 },
{ t: 0.75, r: 170, g: 170, b: 180 },
{ t: 1, r: 240, g: 240, b: 250 },
],
},
{
name: "Psychedelic",
swatch: "linear-gradient(90deg, #f00, #0f0, #00f, #f00)",
stops: [
{ t: 0, r: 128, g: 0, b: 128 },
{ t: 0.17, r: 0, g: 0, b: 255 },
{ t: 0.33, r: 0, g: 255, b: 255 },
{ t: 0.5, r: 0, g: 255, b: 0 },
{ t: 0.67, r: 255, g: 255, b: 0 },
{ t: 0.83, r: 255, g: 0, b: 0 },
{ t: 1, r: 128, g: 0, b: 128 },
],
},
{
name: "Electric",
swatch: "linear-gradient(90deg, #200, #90a, #a2f, #6ff)",
stops: [
{ t: 0, r: 10, g: 0, b: 20 },
{ t: 0.2, r: 60, g: 0, b: 100 },
{ t: 0.4, r: 140, g: 20, b: 180 },
{ t: 0.6, r: 200, g: 60, b: 255 },
{ t: 0.8, r: 100, g: 200, b: 255 },
{ t: 1, r: 50, g: 255, b: 220 },
],
},
];
};
// ── module: src/state.ts ──
__mods["src/state.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PaletteColorizer = exports.ViewStateManager = void 0;
const config_1 = require("./config");
// ===== View State Management =====
class ViewStateManager {
constructor() {
this.history = [];
this.historyIndex = -1;
this.state = this.createInitialState();
}
createInitialState() {
return {
setType: 'mandelbrot',
re: config_1.CONFIG.initialRe,
im: config_1.CONFIG.initialIm,
scale: config_1.CONFIG.initialScale,
maxIterations: config_1.CONFIG.iterationsBase,
smoothColoring: true,
autoIterations: true,
paletteIndex: 0,
colorOffset: 0,
juliaC: { re: -0.7269, im: 0.1889 },
};
}
get() {
return this.state;
}
getMagnification() {
return config_1.CONFIG.initialScale / this.state.scale;
}
// Update view and manage history
setView(re, im, scale, pushHistory = true) {
if (scale < 1e-14)
scale = 1e-14;
const oldScale = this.state.scale;
this.state.re = re;
this.state.im = im;
this.state.scale = scale;
this.updateIterationsForZoom();
if (pushHistory) {
// Don't duplicate entries when just adjusting iterations
if (oldScale === this.state.scale && this.historyIndex >= 0) {
return;
}
// If we went forward in history then panned, cut the future
if (this.historyIndex < this.history.length - 1) {
this.history = this.history.slice(0, this.historyIndex + 1);
}
this.history.push({ re, im, scale });
this.historyIndex = this.history.length - 1;
// Limit history size
if (this.history.length > 100) {
this.history.shift();
this.historyIndex--;
}
}
}
setSetType(type) {
this.state.setType = type;
}
setJuliaC(re, im) {
this.state.juliaC = { re, im };
}
setMaxIterations(max) {
this.state.maxIterations = max;
this.state.autoIterations = false;
}
toggleAutoIterations() {
this.state.autoIterations = !this.state.autoIterations;
if (this.state.autoIterations) {
this.updateIterationsForZoom();
}
}
setPaletteIndex(idx) {
this.state.paletteIndex = idx;
}
setColorOffset(offset) {
this.state.colorOffset = offset;
}
setSmoothColoring(smooth) {
this.state.smoothColoring = smooth;
}
updateIterationsForZoom() {
if (!this.state.autoIterations)
return;
const mag = this.getMagnification();
const logMag = Math.log10(Math.max(1, mag));
let iter = config_1.CONFIG.iterationsBase + Math.floor(config_1.CONFIG.iterationsScale * logMag);
this.state.maxIterations = Math.min(iter, config_1.CONFIG.iterationsMaxCap);
}
// History navigation
canGoBack() {
return this.historyIndex > 0;
}
goBack() {
if (!this.canGoBack())
return false;
this.historyIndex--;
const entry = this.history[this.historyIndex];
this.state.re = entry.re;
this.state.im = entry.im;
this.state.scale = entry.scale;
this.updateIterationsForZoom();
return true;
}
reset() {
const init = this.createInitialState();
this.state.re = init.re;
this.state.im = init.im;
this.state.scale = init.scale;
this.state.setType = init.setType;
this.updateIterationsForZoom();
// Reset history
this.history = [{ re: init.re, im: init.im, scale: init.scale }];
this.historyIndex = 0;
}
// ===== Fractal Math =====
// Compute Mandelbrot iteration count at point c = re + im*i
mandelbrotIter(re, im, maxIter) {
let zr = 0, zi = 0;
let zr2 = 0, zi2 = 0;
for (let n = 0; n < maxIter; n++) {
// z = z^2 + c
zi = 2 * zr * zi + im;
zr = zr2 - zi2 + re;
zr2 = zr * zr;
zi2 = zi * zi;
if (zr2 + zi2 > 4)
return n;
}
return maxIter; // interior point
}
// Compute Julia iteration count for z = (re, im) with parameter c
juliaIter(re, im, cRe, cIm, maxIter) {
let zr = re, zi = im;
let zr2 = zr * zr, zi2 = zi * zi;
for (let n = 0; n < maxIter; n++) {
// z = z^2 + c
zi = 2 * zr * zi + cIm;
zr = zr2 - zi2 + cRe;
zr2 = zr * zr;
zi2 = zi * zi;
if (zr2 + zi2 > 4)
return n;
}
return maxIter; // interior point
}
// Smooth iteration count (continuous coloring)
smoothIter(n, zr2, zi2, maxIter) {
if (n >= maxIter)
return maxIter; // interior
const modulus = Math.sqrt(zr2 + zi2);
return n + 1 - Math.log2(Math.log(modulus));
}
// Public debug: get iteration count at a point in the current view
iterationsAt(re, im) {
const state = this.state;
if (state.setType === 'mandelbrot') {
return this.mandelbrotIter(re, im, state.maxIterations);
}
else if (state.juliaC) {
return this.juliaIter(re, im, state.juliaC.re, state.juliaC.im, state.maxIterations);
}
return 0;
}
}
exports.ViewStateManager = ViewStateManager;
// ===== Palette Colorizer =====
class PaletteColorizer {
constructor() {
// Precompute color LUT for speed: maps smooth iteration value [0, maxIter] -> RGBA32
this.lut = new Uint32Array(0);
this.initialized = false;
}
init(maxIter, palette, offset) {
this.lut = new Uint32Array(maxIter + 1);
this.fillLUT(maxIter, palette, offset);
this.initialized = true;
}
fillLUT(maxIter, palette, offset) {
const stops = palette.stops;
const lut = this.lut;
for (let i = 0; i <= maxIter; i++) {
// Normalize iteration to [0, 1] with offset wrapping
let t = (i / maxIter + offset) % 1;
// Find the two surrounding color stops
let a = stops[0], b = stops[stops.length - 1];
for (let j = 0; j < stops.length - 1; j++) {
if (t >= stops[j].t && t <= stops[j + 1].t) {
a = stops[j];
b = stops[j + 1];
break;
}
}
// Interpolate between stops
const span = b.t - a.t;
const frac = span === 0 ? 0 : (t - a.t) / span;
const r = a.r + (b.r - a.r) * frac | 0;
const g = a.g + (b.g - a.g) * frac | 0;
const bl = a.b + (b.b - a.b) * frac | 0;
// Store as ABGR (little-endian Uint32)
lut[i] = (255 << 24) | (bl << 16) | (g << 8) | r;
}
}
getColor(smoothN, maxIter) {
if (!this.initialized)
return 0xff000000;
if (smoothN >= maxIter)
return 0xff000000; // interior: black
const idx = Math.min(Math.max(smoothN | 0, 0), maxIter);
return this.lut[idx];
}
getColorSmooth(smoothN, maxIter, palette) {
if (!this.initialized)
return 0xff000000;
if (smoothN >= maxIter)
return 0xff000000;
// For true smooth coloring, interpolate between LUT entries
const lo = Math.max(0, smoothN | 0);
const hi = Math.min(maxIter, lo + 1);
const frac = smoothN - lo;
if (lo === hi)
return this.lut[lo];
const cLo = this.lut[lo];
const cHi = this.lut[hi];
const rLo = cLo & 0xff;
const gLo = (cLo >> 8) & 0xff;
const bLo = (cLo >> 16) & 0xff;
const rHi = cHi & 0xff;
const gHi = (cHi >> 8) & 0xff;
const bHi = (cHi >> 16) & 0xff;
const r = rLo + (rHi - rLo) * frac | 0;
const g = gLo + (gHi - gLo) * frac | 0;
const b = bLo + (bHi - bLo) * frac | 0;
return (255 << 24) | (b << 16) | (g << 8) | r;
}
}
exports.PaletteColorizer = PaletteColorizer;
};
// ── 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 config_1 = require("./config");
class FractalRenderer {
constructor() {
this.request = null;
this.currentTask = null;
this.renderTimer = null;
}
setRequest(req) {
this.request = req;
}
// Schedule a render with debounce; cancels any in-progress render
scheduleRender() {
// Cancel in-progress render
if (this.currentTask) {
this.currentTask.cancelled = true;
this.currentTask = null;
}
// Cancel pending timer
if (this.renderTimer) {
clearTimeout(this.renderTimer);
this.renderTimer = null;
}
// Debounce: wait a bit for user input to settle
this.renderTimer = window.setTimeout(() => {
this.renderTimer = null;
this.startRender();
}, config_1.CONFIG.renderTimeout);
}
// Immediate render (no debounce), e.g. for progressive refinement
scheduleRenderImmediate() {
if (this.currentTask) {
this.currentTask.cancelled = true;
this.currentTask = null;
}
if (this.renderTimer) {
clearTimeout(this.renderTimer);
this.renderTimer = null;
}
this.startRender();
}
startRender() {
const req = this.request;
if (!req)
return;
const canvas = req.canvas;
const ctx = canvas.getContext('2d', { willReadFrequently: false });
if (!ctx)
return;
const width = canvas.width;
const height = canvas.height;
if (width === 0 || height === 0)
return;
// Create image data buffer
const imageData = ctx.createImageData(width, height);
const data = imageData.data;
// Create cancel token
const cancelToken = { cancelled: false };
this.currentTask = cancelToken;
const state = req.stateMgr.get();
const maxIter = state.maxIterations;
const smooth = state.smoothColoring;
const palette = config_1.PALETTES[state.paletteIndex];
// Update colorizer if needed
req.colorizer.init(maxIter, palette, state.colorOffset);
// Progressive rendering through passes
this.renderProgressive(ctx, imageData, data, width, height, state, maxIter, smooth, palette, cancelToken, 0);
}
renderProgressive(ctx, imageData, data, width, height, state, maxIter, smooth, palette, cancelToken, passIndex) {
if (cancelToken.cancelled)
return;
const passes = config_1.CONFIG.progressivePasses;
if (passIndex >= passes.length) {
// Render complete
this.currentTask = null;
return;
}
const downscale = passes[passIndex];
const pw = Math.ceil(width / downscale);
const ph = Math.ceil(height / downscale);
const startTime = performance.now();
// Render at current resolution
this.renderFractal(data, width, height, pw, ph, state, maxIter, smooth, cancelToken, palette);
// Blit to canvas
ctx.putImageData(imageData, 0, 0);
if (cancelToken.cancelled)
return;
// Schedule next pass
const elapsed = performance.now() - startTime;
if (elapsed < config_1.CONFIG.progressiveMinTime && passIndex < passes.length - 1) {
// Render fast enough, go to next resolution immediately
this.renderProgressive(ctx, imageData, data, width, height, state, maxIter, smooth, palette, cancelToken, passIndex + 1);
}
else {
// Yield to let UI respond
requestAnimationFrame(() => {
if (!cancelToken.cancelled) {
this.renderProgressive(ctx, imageData, data, width, height, state, maxIter, smooth, palette, cancelToken, passIndex + 1);
}
});
}
}
renderFractal(data, fullWidth, fullHeight, renderWidth, renderHeight, state, maxIter, smooth, cancelToken, palette) {
const reCenter = state.re;
const imCenter = state.im;
const scale = state.scale;
const setType = state.setType;
const juliaC = state.juliaC;
// Aspect ratio correction
const aspect = fullWidth / fullHeight;
const reScale = scale * aspect / 2; // half-width in re
const imScale = scale / 2; // half-height in im
const colorizer = this.request?.colorizer;
if (!colorizer)
return;
const getColorSmoothFn = colorizer.getColorSmooth.bind(colorizer);
const getColorFn = colorizer.getColor.bind(colorizer);
// Render row by row at reduced resolution, then scale up
const rowsPerCheck = Math.max(1, Math.floor(renderHeight / 20));
for (let ry = 0; ry < renderHeight; ry++) {
if (cancelToken.cancelled)
return;
// Check cancellation periodically
if ((ry + 1) % rowsPerCheck === 0) {
if (cancelToken.cancelled)
return;
}
// Map screen row y to complex plane imaginary coordinate
const im = imCenter + (ry / renderHeight - 0.5) * scale;
for (let rx = 0; rx < renderWidth; rx++) {
// Map screen col x to complex plane real coordinate
const re = reCenter + (rx / renderWidth - 0.5) * scale * aspect;
let n;
let finalZr2 = 0, finalZi2 = 0;
if (setType === 'mandelbrot') {
// Mandelbrot: z0=0, c=(re,im)
let zr = 0, zi = 0;
let zr2 = 0, zi2 = 0;
for (n = 0; n < maxIter; n++) {
zi = 2 * zr * zi + im;
zr = zr2 - zi2 + re;
zr2 = zr * zr;
zi2 = zi * zi;
if (zr2 + zi2 > 4)
break;
}
finalZr2 = zr2;
finalZi2 = zi2;
}
else if (juliaC) {
// Julia: c=juliaC, z0=(re,im)
let zr = re, zi = im;
let zr2 = zr * zr, zi2 = zi * zi;
const cRe = juliaC.re, cIm = juliaC.im;
for (n = 0; n < maxIter; n++) {
zi = 2 * zr * zi + cIm;
zr = zr2 - zi2 + cRe;
zr2 = zr * zr;
zi2 = zi * zi;
if (zr2 + zi2 > 4)
break;
}
finalZr2 = zr2;
finalZi2 = zi2;
}
else {
n = maxIter;
}
// Compute color
let color;
if (smooth && n < maxIter && finalZr2 + finalZi2 > 0) {
const smoothN = n + 1 - Math.log2(Math.log(Math.sqrt(finalZr2 + finalZi2)));
color = getColorSmoothFn(smoothN, maxIter, palette);
}
else {
color = getColorFn(Math.min(n, maxIter), maxIter);
}
// Write pixel to buffer (ABGR little-endian in data[] as RGBA)
const r = color & 0xff;
const g = (color >> 8) & 0xff;
const b = (color >> 16) & 0xff;
// Upscale this pixel to the full-resolution block
const xs = rx * (fullWidth / renderWidth) | 0;
const ys = ry * (fullHeight / renderHeight) | 0;
const xe = (rx + 1) * (fullWidth / renderWidth) | 0;
const ye = (ry + 1) * (fullHeight / renderHeight) | 0;
for (let y = ys; y < ye && y < fullHeight; y++) {
const rowOffset = y * fullWidth * 4;
for (let x = xs; x < xe && x < fullWidth; x++) {
const idx = rowOffset + x * 4;
data[idx] = r;
data[idx + 1] = g;
data[idx + 2] = b;
data[idx + 3] = 255;
}
}
}
}
}
}
exports.FractalRenderer = FractalRenderer;
};
// ── 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 config_1 = require("./config");
class InputHandler {
constructor(canvas, stateMgr, onRenderRequested, onJuliaPreview, rectOverlay) {
this.isDragging = false;
this.isPanning = false;
this.dragStartX = 0;
this.dragStartY = 0;
this.dragStartRe = 0;
this.dragStartIm = 0;
this.dragStartScale = 0;
// Rect zoom state
this.rectZoomStartX = 0;
this.rectZoomStartY = 0;
this.rectZoomEndX = 0;
this.rectZoomEndY = 0;
this.isRectZooming = false;
// Touch events (basic support)
this.lastTouchDist = 0;
this.lastTouchCenter = { x: 0, y: 0 };
this.canvas = canvas;
this.stateMgr = stateMgr;
this.onRenderRequested = onRenderRequested;
this.onJuliaPreview = onJuliaPreview;
this.rectOverlay = rectOverlay;
this.setupEvents();
}
setupEvents() {
// Mouse events - shift+click for rect zoom, regular click for pan
this.canvas.addEventListener('mousedown', this.onMouseDown.bind(this));
window.addEventListener('mousemove', this.onMouseMove.bind(this));
window.addEventListener('mouseup', this.onMouseUp.bind(this));
this.canvas.addEventListener('contextmenu', (e) => e.preventDefault());
// Wheel zoom
this.canvas.addEventListener('wheel', this.onWheel.bind(this), { passive: false });
// Keyboard
window.addEventListener('keydown', this.onKeyDown.bind(this));
// Touch support
this.canvas.addEventListener('touchstart', this.onTouchStart.bind(this), { passive: false });
window.addEventListener('touchmove', this.onTouchMove.bind(this), { passive: false });
window.addEventListener('touchend', this.onTouchEnd.bind(this));
}
screenToComplex(sx, sy) {
const rect = this.canvas.getBoundingClientRect();
const x = (sx - rect.left) / rect.width;
const y = (sy - rect.top) / rect.height;
const state = this.stateMgr.get();
const aspect = rect.width / rect.height;
const re = state.re + (x - 0.5) * state.scale * aspect;
const im = state.im + (y - 0.5) * state.scale;
return { re, im };
}
onMouseDown(e) {
if (e.button === 0 && e.shiftKey) {
// Rect zoom mode (Shift + left click)
this.isRectZooming = true;
this.rectZoomStartX = e.clientX;
this.rectZoomStartY = e.clientY;
this.rectZoomEndX = e.clientX;
this.rectZoomEndY = e.clientY;
this.rectOverlay.style.display = 'block';
this.updateRectOverlay();
this.canvas.style.cursor = 'crosshair';
}
else if (e.button === 0 || e.button === 2) {
// Pan mode
this.isPanning = true;
this.dragStartX = e.clientX;
this.dragStartY = e.clientY;
this.dragStartRe = this.stateMgr.get().re;
this.dragStartIm = this.stateMgr.get().im;
this.dragStartScale = this.stateMgr.get().scale;
this.canvas.style.cursor = 'grabbing';
}
}
updateRectOverlay() {
const rect = this.canvas.getBoundingClientRect();
const x = Math.min(this.rectZoomStartX, this.rectZoomEndX) - rect.left;
const y = Math.min(this.rectZoomStartY, this.rectZoomEndY) - rect.top;
const w = Math.abs(this.rectZoomEndX - this.rectZoomStartX);
const h = Math.abs(this.rectZoomEndY - this.rectZoomStartY);
this.rectOverlay.style.left = x + 'px';
this.rectOverlay.style.top = y + 'px';
this.rectOverlay.style.width = w + 'px';
this.rectOverlay.style.height = h + 'px';
}
onMouseMove(e) {
const state = this.stateMgr.get();
if (this.isRectZooming) {
this.rectZoomEndX = e.clientX;
this.rectZoomEndY = e.clientY;
this.updateRectOverlay();
return;
}
if (this.isPanning) {
const rect = this.canvas.getBoundingClientRect();
const dx = e.clientX - this.dragStartX;
const dy = e.clientY - this.dragStartY;
const aspect = rect.width / rect.height;
const re = this.dragStartRe - (dx / rect.width) * this.dragStartScale * aspect;
const im = this.dragStartIm - (dy / rect.height) * this.dragStartScale;
this.stateMgr.setView(re, im, this.dragStartScale, false);
this.onRenderRequested();
}
// Julia preview on hover (only in Mandelbrot mode)
if (state.setType === 'mandelbrot' && !this.isPanning) {
const c = this.screenToComplex(e.clientX, e.clientY);
this.onJuliaPreview(c.re, c.im);
}
}
onMouseUp(e) {
this.isPanning = false;
if (this.isRectZooming) {
this.isRectZooming = false;
this.rectOverlay.style.display = 'none';
// Only zoom if rectangle is large enough
const dx = Math.abs(e.clientX - this.rectZoomStartX);
const dy = Math.abs(e.clientY - this.rectZoomStartY);
if (dx >= config_1.CONFIG.rectZoomMinPixels && dy >= config_1.CONFIG.rectZoomMinPixels) {
this.applyRectZoom();
}
}
this.canvas.style.cursor = '';
}
applyRectZoom() {
const rect = this.canvas.getBoundingClientRect();
const x1 = Math.min(this.rectZoomStartX, this.rectZoomEndX) - rect.left;
const y1 = Math.min(this.rectZoomStartY, this.rectZoomEndY) - rect.top;
const x2 = Math.max(this.rectZoomStartX, this.rectZoomEndX) - rect.left;
const y2 = Math.max(this.rectZoomStartY, this.rectZoomEndY) - rect.top;
// Convert rectangle corners to complex plane coordinates
const aspect = rect.width / rect.height;
const state = this.stateMgr.get();
const re1 = state.re + (x1 / rect.width - 0.5) * state.scale * aspect;
const im1 = state.im + (y1 / rect.height - 0.5) * state.scale;
const re2 = state.re + (x2 / rect.width - 0.5) * state.scale * aspect;
const im2 = state.im + (y2 / rect.height - 0.5) * state.scale;
// New center is midpoint of rectangle
const newRe = (re1 + re2) / 2;
const newIm = (im1 + im2) / 2;
// New scale is determined by the rectangle's larger dimension (to fully contain it)
const reRange = Math.abs(re2 - re1);
const imRange = Math.abs(im2 - im1);
// Aspect ratio: scale is viewport width. To fit rectangle, we need scale * aspect >= reRange and scale >= imRange
let newScale = Math.max(reRange / aspect, imRange);
newScale = Math.max(newScale, 1e-14);
// Check hard max magnification
if (config_1.CONFIG.initialScale / newScale > config_1.CONFIG.maxMagnificationHard) {
const maxScale = config_1.CONFIG.initialScale / config_1.CONFIG.maxMagnificationHard;
newScale = Math.max(newScale, maxScale);
}
this.stateMgr.setView(newRe, newIm, newScale, true);
this.onRenderRequested();
}
onWheel(e) {
e.preventDefault();
const state = this.stateMgr.get();
const rect = this.canvas.getBoundingClientRect();
// Cursor position in complex plane
const c = this.screenToComplex(e.clientX, e.clientY);
// Zoom factor based on wheel delta
const zoomFactor = e.deltaY > 0 ? config_1.CONFIG.wheelZoomFactor : 1 / config_1.CONFIG.wheelZoomFactor;
const newScale = state.scale * zoomFactor;
// Check max magnification
const magnification = config_1.CONFIG.initialScale / newScale;
if (magnification > config_1.CONFIG.maxMagnificationHard) {
return;
}
// Compute new center so cursor point stays fixed
const aspect = rect.width / rect.height;
const newRe = c.re - (e.clientX / rect.width - 0.5) * newScale * aspect;
const newIm = c.im - (e.clientY / rect.height - 0.5) * newScale;
this.stateMgr.setView(newRe, newIm, newScale, true);
this.onRenderRequested();
}
onKeyDown(e) {
const state = this.stateMgr.get();
const moveAmount = state.scale * 0.1;
switch (e.key) {
case 'ArrowLeft':
this.stateMgr.setView(state.re - moveAmount, state.im, state.scale, true);
this.onRenderRequested();
break;
case 'ArrowRight':
this.stateMgr.setView(state.re + moveAmount, state.im, state.scale, true);
this.onRenderRequested();
break;
case 'ArrowUp':
this.stateMgr.setView(state.re, state.im - moveAmount, state.scale, true);
this.onRenderRequested();
break;
case 'ArrowDown':
this.stateMgr.setView(state.re, state.im + moveAmount, state.scale, true);
this.onRenderRequested();
break;
case '+':
case '=':
this.zoomAtCenter(0.6);
break;
case '-':
this.zoomAtCenter(1.5);
break;
case 'j':
case 'J':
if (state.setType === 'mandelbrot' && state.juliaC) {
this.stateMgr.setSetType('julia');
this.stateMgr.setView(state.juliaC.re, state.juliaC.im, config_1.CONFIG.juliaRenderScale, true);
this.onRenderRequested();
}
break;
case 'm':
case 'M':
if (state.setType === 'julia') {
this.stateMgr.setSetType('mandelbrot');
this.stateMgr.setView(config_1.CONFIG.initialRe, config_1.CONFIG.initialIm, config_1.CONFIG.initialScale, true);
this.onRenderRequested();
}
break;
}
}
zoomAtCenter(factor) {
const state = this.stateMgr.get();
const newScale = state.scale * factor;
const magnification = config_1.CONFIG.initialScale / newScale;
if (magnification > config_1.CONFIG.maxMagnificationHard)
return;
this.stateMgr.setView(state.re, state.im, newScale, true);
this.onRenderRequested();
}
onTouchStart(e) {
e.preventDefault();
if (e.touches.length === 1) {
this.isPanning = true;
this.dragStartX = e.touches[0].clientX;
this.dragStartY = e.touches[0].clientY;
this.dragStartRe = this.stateMgr.get().re;
this.dragStartIm = this.stateMgr.get().im;
this.dragStartScale = this.stateMgr.get().scale;
}
else if (e.touches.length === 2) {
this.isPanning = false;
const dx = e.touches[1].clientX - e.touches[0].clientX;
const dy = e.touches[1].clientY - e.touches[0].clientY;
this.lastTouchDist = Math.sqrt(dx * dx + dy * dy);
this.lastTouchCenter = {
x: (e.touches[0].clientX + e.touches[1].clientX) / 2,
y: (e.touches[0].clientY + e.touches[1].clientY) / 2,
};
}
}
onTouchMove(e) {
e.preventDefault();
if (e.touches.length === 1 && this.isPanning) {
const rect = this.canvas.getBoundingClientRect();
const dx = e.touches[0].clientX - this.dragStartX;
const dy = e.touches[0].clientY - this.dragStartY;
const aspect = rect.width / rect.height;
const re = this.dragStartRe - (dx / rect.width) * this.dragStartScale * aspect;
const im = this.dragStartIm - (dy / rect.height) * this.dragStartScale;
this.stateMgr.setView(re, im, this.dragStartScale, false);
this.onRenderRequested();
}
else if (e.touches.length === 2) {
const dx = e.touches[1].clientX - e.touches[0].clientX;
const dy = e.touches[1].clientY - e.touches[0].clientY;
const dist = Math.sqrt(dx * dx + dy * dy);
const zoomFactor = this.lastTouchDist / dist;
const rect = this.canvas.getBoundingClientRect();
const cx = (e.touches[0].clientX + e.touches[1].clientX) / 2;
const cy = (e.touches[0].clientY + e.touches[1].clientY) / 2;
const c = this.screenToComplex(cx, cy);
const state = this.stateMgr.get();
const newScale = Math.max(1e-14, state.scale * zoomFactor);
const aspect = rect.width / rect.height;
const newRe = c.re - (cx / rect.width - 0.5) * newScale * aspect;
const newIm = c.im - (cy / rect.height - 0.5) * newScale;
this.stateMgr.setView(newRe, newIm, newScale, true);
this.lastTouchDist = dist;
this.onRenderRequested();
}
}
onTouchEnd(e) {
this.isPanning = false;
}
}
exports.InputHandler = InputHandler;
};
// ── module: src/juliaPreview.ts ──
__mods["src/juliaPreview.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.JuliaPreviewRenderer = void 0;
const config_1 = require("./config");
const state_1 = require("./state");
class JuliaPreviewRenderer {
constructor(canvas, stateMgr) {
this.imageData = null;
this.lastPreviewC = { re: 0, im: 0 };
this.rAFId = null;
this.canvas = canvas;
this.stateMgr = stateMgr;
this.colorizer = new state_1.PaletteColorizer();
const size = config_1.CONFIG.juliaPreviewSize;
canvas.width = size;
canvas.height = size;
this.imageData = canvas.getContext('2d')?.createImageData(size, size) || null;
}
updatePreview(re, im) {
// Don't recompute if c hasn't changed significantly
if (Math.abs(re - this.lastPreviewC.re) < 1e-6 && Math.abs(im - this.lastPreviewC.im) < 1e-6) {
return;
}
this.lastPreviewC = { re, im };
// Schedule render
if (this.rAFId === null) {
this.rAFId = requestAnimationFrame(() => this.render(re, im));
}
}
render(cRe, cIm) {
this.rAFId = null;
if (!this.imageData)
return;
const ctx = this.canvas.getContext('2d');
if (!ctx)
return;
const size = config_1.CONFIG.juliaPreviewSize;
const data = this.imageData.data;
const state = this.stateMgr.get();
const maxIter = Math.min(state.maxIterations, 200); // Lower iter count for preview
const smooth = state.smoothColoring;
const palette = config_1.PALETTES[state.paletteIndex];
this.colorizer.init(maxIter, palette, state.colorOffset);
const scale = config_1.CONFIG.juliaRenderScale;
for (let y = 0; y < size; y++) {
const im = (y / size - 0.5) * scale;
for (let x = 0; x < size; x++) {
const re = (x / size - 0.5) * scale;
// Julia iteration
let zr = re, zi = im;
let zr2 = zr * zr, zi2 = zi * zi;
let n = 0;
for (; n < maxIter; n++) {
zi = 2 * zr * zi + cIm;
zr = zr2 - zi2 + cRe;
zr2 = zr * zr;
zi2 = zi * zi;
if (zr2 + zi2 > 4)
break;
}
const idx = (y * size + x) * 4;
if (n >= maxIter) {
// Interior
data[idx] = 0;
data[idx + 1] = 0;
data[idx + 2] = 0;
data[idx + 3] = 255;
}
else if (smooth && zr2 + zi2 > 0) {
const smoothN = n + 1 - Math.log2(Math.log(Math.sqrt(zr2 + zi2)));
const color = this.colorizer.getColorSmooth(smoothN, maxIter, palette);
data[idx] = color & 0xff;
data[idx + 1] = (color >> 8) & 0xff;
data[idx + 2] = (color >> 16) & 0xff;
data[idx + 3] = 255;
}
else {
const color = this.colorizer.getColor(Math.min(n, maxIter), maxIter);
data[idx] = color & 0xff;
data[idx + 1] = (color >> 8) & 0xff;
data[idx + 2] = (color >> 16) & 0xff;
data[idx + 3] = 255;
}
}
}
ctx.putImageData(this.imageData, 0, 0);
}
hide() {
this.lastPreviewC = { re: Infinity, im: Infinity };
const ctx = this.canvas.getContext('2d');
if (ctx) {
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
}
}
exports.JuliaPreviewRenderer = JuliaPreviewRenderer;
};
// ── module: src/ui.ts ──
__mods["src/ui.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.UIController = void 0;
const config_1 = require("./config");
class UIController {
constructor(stateMgr, onSetPalette, onSaveBookmark, onLoadBookmark, onDeleteBookmark) {
this.hudTimeout = null;
this.fpsFrames = 0;
this.fpsLastTime = performance.now();
this.fpsValue = 60;
this.stateMgr = stateMgr;
this.onSetPalette = onSetPalette;
this.onSaveBookmark = onSaveBookmark;
this.onLoadBookmark = onLoadBookmark;
this.onDeleteBookmark = onDeleteBookmark;
this.cacheElements();
this.setupPaletteBar();
this.setupEventListeners();
}
cacheElements() {
this.elMagnify = document.getElementById('hudMagnify');
this.elFPS = document.getElementById('hudFPS');
this.elMode = document.getElementById('hudMode');
this.elCenterCoords = document.getElementById('hudCenterCoords');
this.elSliderIter = document.getElementById('sliderIter');
this.elIterValue = document.getElementById('iterValue');
this.elChkSmooth = document.getElementById('chkSmooth');
this.elChkAutoIter = document.getElementById('chkAutoIter');
this.elSliderColorOffset = document.getElementById('sliderColorOffset');
this.elPaletteBar = document.getElementById('paletteBar');
this.elBookmarkPanel = document.getElementById('bookmarkPanel');
this.elBookmarkName = document.getElementById('bookmarkName');
this.elBookmarkList = document.getElementById('bookmarkList');
}
setupPaletteBar() {
this.elPaletteBar.innerHTML = '';
config_1.PALETTES.forEach((palette, idx) => {
const chip = document.createElement('div');
chip.className = 'palette-chip';
chip.style.background = palette.swatch;
chip.title = palette.name;
chip.addEventListener('click', () => {
this.onSetPalette(idx);
this.updateUI();
});
this.elPaletteBar.appendChild(chip);
});
}
setupEventListeners() {
// Reset button
document.getElementById('btnReset')?.addEventListener('click', () => {
this.stateMgr.reset();
this.updateUI();
});
// Back button
document.getElementById('btnBack')?.addEventListener('click', () => {
if (this.stateMgr.goBack()) {
this.updateUI();
}
});
// Iteration slider
this.elSliderIter.addEventListener('input', () => {
const val = parseInt(this.elSliderIter.value, 10);
this.elIterValue.textContent = val.toString();
this.stateMgr.setMaxIterations(val);
this.updateUI();
});
// Smooth coloring checkbox
this.elChkSmooth.addEventListener('change', () => {
this.stateMgr.setSmoothColoring(this.elChkSmooth.checked);
this.updateUI();
});
// Auto iterations checkbox
this.elChkAutoIter.addEventListener('change', () => {
this.stateMgr.toggleAutoIterations();
this.updateUI();
});
// Color offset slider
this.elSliderColorOffset.addEventListener('input', () => {
const val = parseFloat(this.elSliderColorOffset.value);
this.stateMgr.setColorOffset(val);
});
// Bookmarks
document.getElementById('btnBookmarks')?.addEventListener('click', () => {
this.elBookmarkPanel.classList.toggle('hidden');
this.renderBookmarks();
});
document.getElementById('btnCloseBookmarks')?.addEventListener('click', () => {
this.elBookmarkPanel.classList.add('hidden');
});
document.getElementById('btnSaveBookmark')?.addEventListener('click', () => {
const name = this.elBookmarkName.value.trim() || 'Untitled';
this.onSaveBookmark(name);
this.elBookmarkName.value = '';
this.renderBookmarks();
});
// Mousemove to show HUD
document.addEventListener('mousemove', () => this.showHUD());
document.addEventListener('mousedown', () => this.showHUD());
document.addEventListener('touchstart', () => this.showHUD());
}
updateUI() {
const state = this.stateMgr.get();
const mag = this.stateMgr.getMagnification();
// Magnification
if (mag >= 1e6) {
this.elMagnify.textContent = `×${(mag).toExponential(1)}`;
}
else if (mag >= 100) {
this.elMagnify.textContent = `×${Math.round(mag)}`;
}
else {
this.elMagnify.textContent = `×${mag.toFixed(1)}`;
}
// Mode
this.elMode.textContent = state.setType === 'mandelbrot' ? 'Mandelbrot' : 'Julia Set';
// Coordinates
this.elCenterCoords.textContent = `Re: ${state.re.toExponential(6)} Im: ${state.im.toExponential(6)}`;
// Iterations
this.elSliderIter.value = state.maxIterations.toString();
this.elIterValue.textContent = state.maxIterations.toString();
// Smooth checkbox
this.elChkSmooth.checked = state.smoothColoring;
// Auto iterations
this.elChkAutoIter.checked = state.autoIterations;
// Color offset
this.elSliderColorOffset.value = state.colorOffset.toString();
// Palette chips active state
const chips = this.elPaletteBar.querySelectorAll('.palette-chip');
chips.forEach((chip, idx) => {
chip.classList.toggle('active', idx === state.paletteIndex);
});
// Check max mag warning
if (mag > config_1.CONFIG.maxMagnification) {
this.elMagnify.style.color = '#ff6b6b';
}
else {
this.elMagnify.style.color = '';
}
}
updateFPS() {
const now = performance.now();
this.fpsFrames++;
if (now - this.fpsLastTime >= 500) {
this.fpsValue = Math.round(this.fpsFrames / ((now - this.fpsLastTime) / 1000));
this.elFPS.textContent = `${this.fpsValue.toString()} FPS`;
this.fpsFrames = 0;
this.fpsLastTime = now;
}
}
showHUD() {
const hud = document.getElementById('hud');
if (hud) {
hud.classList.remove('hidden');
if (this.hudTimeout) {
clearTimeout(this.hudTimeout);
}
this.hudTimeout = window.setTimeout(() => {
hud.classList.add('hidden');
}, 4000);
}
}
renderBookmarks() {
const bookmarks = this.getBookmarks();
this.elBookmarkList.innerHTML = '';
if (bookmarks.length === 0) {
this.elBookmarkList.innerHTML = '<div class="emptyBookmarks">No bookmarks yet</div>';
return;
}
bookmarks.forEach(bm => {
const item = document.createElement('div');
item.className = 'bookmarkItem';
const nameSpan = document.createElement('span');
nameSpan.className = 'bookmarkName';
nameSpan.textContent = bm.name;
item.appendChild(nameSpan);
const loadBtn = document.createElement('button');
loadBtn.textContent = 'Load';
loadBtn.addEventListener('click', () => {
this.onLoadBookmark(bm);
this.elBookmarkPanel.classList.add('hidden');
});
item.appendChild(loadBtn);
const delBtn = document.createElement('button');
delBtn.textContent = '✕';
delBtn.addEventListener('click', () => {
this.onDeleteBookmark(bm.id);
this.renderBookmarks();
});
item.appendChild(delBtn);
this.elBookmarkList.appendChild(item);
});
}
getBookmarks() {
try {
const data = localStorage.getItem('fractalBookmarks');
return data ? JSON.parse(data) : [];
}
catch {
return [];
}
}
saveBookmarks(bookmarks) {
try {
localStorage.setItem('fractalBookmarks', JSON.stringify(bookmarks));
}
catch (e) {
console.warn('Failed to save bookmarks:', e);
}
}
saveBookmark(name) {
const bookmarks = this.getBookmarks();
const bookmark = {
id: Date.now().toString(36) + Math.random().toString(36).substr(2, 5),
name,
state: JSON.parse(JSON.stringify(this.stateMgr.get())),
createdAt: Date.now(),
};
bookmarks.push(bookmark);
this.saveBookmarks(bookmarks);
}
loadBookmark(bookmark) {
// Restore view state
const s = bookmark.state;
this.stateMgr.setView(s.re, s.im, s.scale, true);
this.stateMgr.setSetType(s.setType);
if (s.juliaC) {
this.stateMgr.setJuliaC(s.juliaC.re, s.juliaC.im);
}
if (s.maxIterations !== undefined) {
this.stateMgr.setMaxIterations(s.maxIterations);
}
this.stateMgr.setPaletteIndex(s.paletteIndex || 0);
this.stateMgr.setColorOffset(s.colorOffset || 0);
this.stateMgr.setSmoothColoring(s.smoothColoring ?? true);
this.stateMgr.toggleAutoIterations(); // Reset auto to false when loading bookmark
if (s.autoIterations) {
this.stateMgr.toggleAutoIterations();
}
this.updateUI();
}
deleteBookmark(id) {
const bookmarks = this.getBookmarks().filter(b => b.id !== id);
this.saveBookmarks(bookmarks);
}
}
exports.UIController = UIController;
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"Qwen3.6-27B-Fable-Fusion-711-Uncensored-Heretic-NM-DAU-NEO-MAX-MTP-GGUF","provider":"brigid","persona":"typescript","sessionId":"0be93740-4d01-4e07-ba6e-c082e68c9a7d","tokensIn":1440501,"tokensOut":30906,"tokensTotal":1471407,"cost":0,"turns":47,"toolCalls":56,"failedToolCalls":2,"timestamp":"2026-08-06T23:56:38.343Z"} -->