Fractal Explorer
ProCreations grug 27b GGUF IQ4 · 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 35,382 bytes · SHA-256 791586ecc7fd
<!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: style ── */
html, body { margin: 0; padding: 0; overflow: hidden; width: 100%; height: 100%; background: #0a0a0a; font-family: 'Segoe UI', sans-serif; }
canvas { display: block; width: 100%; height: 100%; }
.glass-panel { background: rgba(0,0,0,0.7); backdrop-filter: blur(8px); border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; color: #fff; font-size: 13px; padding: 12px; }
.hidden { display: none; }
#hud { position: absolute; top: 16px; left: 50%; transform: translateX(-50%); text-align: center; pointer-events: none; }
.controls-panel { position: absolute; top: 16px; left: 16px; width: 220px; z-index: 10; }
.control-row { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; flex-wrap: wrap; }
.control-row label { width: 100px; font-size: 12px; }
.glass-button { background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2); border-radius: 4px; padding: 6px 12px; color: #fff; cursor: pointer; font-size: 12px; }
.glass-button:hover { background: rgba(255,255,255,0.2); }
input[type="range"] { flex: 1; }
.switch { position: relative; display: inline-block; width: 40px; height: 20px; }
.switch input { opacity: 0; width: 0; height: 0; }
.slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background: rgba(255,255,255,0.2); transition: 0.3s; border-radius: 20px; }
.slider:before { position: absolute; content: ""; height: 16px; width: 16px; left: 2px; bottom: 2px; background: #fff; transition: 0.3s; border-radius: 50%; }
input:checked + .slider { background: #4CAF50; }
input:checked + .slider:before { transform: translateX(20px); }
.modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 100; display: none; align-items: center; justify-content: center; }
.modal.show { display: flex; }
.modal-content { background: #1a1a1a; padding: 20px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.2); }
.bookmarks-panel { position: absolute; top: 16px; right: 16px; width: 200px; z-index: 10; max-height: 400px; overflow-y: auto; }
.bookmark-item { padding: 8px; margin-bottom: 4px; background: rgba(255,255,255,0.1); border-radius: 4px; cursor: pointer; }
.bookmark-item:hover { background: rgba(255,255,255,0.2); }
#toast { position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); background: rgba(0,0,0,0.8); padding: 12px 24px; border-radius: 8px; color: #fff; opacity: 0; transition: opacity 0.3s; z-index: 1000; pointer-events: none; }
#toast.show { opacity: 1; }
.bookmark-name-input { width: 150px; padding: 6px; margin-bottom: 8px; background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2); border-radius: 4px; color: #fff; }
.delete-bookmark-btn { float: right; background: rgba(255,0,0,0.3); border: 1px solid rgba(255,0,0,0.5); border-radius: 4px; padding: 2px 6px; color: #fff; cursor: pointer; font-size: 10px; }
.delete-bookmark-btn:hover { background: rgba(255,0,0,0.5); }
</style>
</head>
<body>
<div id="app"><canvas id="fractal-canvas"></canvas>
<div id="hud" class="glass-panel hidden"><span id="hud-text"></span></div>
<div id="controls" class="controls-panel glass-panel"><div class="control-row"><label for="zoom-in">Zoom</label><button id="zoom-in" class="glass-button">+</button><button id="zoom-out" class="glass-button">−</button></div><div class="control-row"><label for="reset-view">Reset</label><button id="reset-view" class="glass-button">Reset view</button></div><div class="control-row"><label for="back-zoom">Back</label><button id="back-zoom" class="glass-button">Back</button></div><div class="control-row"><label for="palette-select">Palette</label><button id="palette-select" class="glass-button">Fire</button></div><div class="control-row"><label for="iter-slider">Max iterations</label><input type="range" id="iter-slider" min="100" max="10000" step="10" value="1000"><span id="iter-display">1000</span></div><div class="control-row"><label for="color-offset">Color drift</label><input type="range" id="color-offset" min="0" max="360" step="1" value="0"><span id="offset-display">0°</span></div><div class="control-row"><label for="smooth-toggle">Smooth coloring</label><label class="switch"><input type="checkbox" id="smooth-toggle" checked=""><span class="slider"></span></label></div><div class="control-row"><label for="julia-toggle">Julia mode</label><label class="switch"><input type="checkbox" id="julia-toggle"><span class="slider"></span></label></div></div>
<div id="save-bookmark-dialog" class="modal"><div class="modal-content"><input type="text" id="bookmark-name" placeholder="Bookmark name"><button id="save-bookmark-btn" class="glass-button">Save</button><button id="cancel-bookmark-btn" class="glass-button">Cancel</button></div></div>
<div id="bookmarks-list" class="bookmarks-panel glass-panel"></div>
<div id="toast"></div></div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./engine":"src/engine.ts","./types":"src/types.ts","./constants":"src/constants.ts","./ui":"src/ui.ts"},"src/constants.ts":{"./types":"src/types.ts"},"src/engine.ts":{"./types":"src/types.ts","./constants":"src/constants.ts"},"src/ui.ts":{"./engine":"src/engine.ts","./types":"src/types.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 engine_1 = require("./engine");
const types_1 = require("./types");
const constants_1 = require("./constants");
const ui_1 = require("./ui");
function start() {
const canvas = document.getElementById('fractal-canvas');
const hud = document.getElementById('hud');
const hudText = document.getElementById('hud-text');
const view = types_1.FractalView.fromSavedOrDefault();
const engine = new engine_1.FractalEngine(canvas, view, constants_1.PALETTES);
const ui = new ui_1.FractalUI(canvas, engine, hud, hudText, constants_1.PALETTES);
// @ts-ignore: debug interface declared in debug.d.ts, but global Window needs runtime assignment
window.fractal = {
iterationsAt: engine.iterationsAt.bind(engine),
setView: (v) => {
const newView = new types_1.FractalView({
re: v.re,
im: v.im,
scale: Math.min(v.scale, constants_1.MAX_ZOOM_MAGNIFICATION),
set: view.set,
juliaC: view.juliaC,
palette: view.palette,
maxIterations: view.maxIterations,
smoothColoring: view.smoothColoring,
});
engine.setView(newView);
engine.render();
},
engine,
};
ui.init();
engine.render();
}
};
// ── types: src/debug.d.ts ──
__mods["src/debug.d.ts"] = function (exports) {};
// ── module: src/constants.ts ──
__mods["src/constants.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PALETTES = exports.DEFAULT_VIEW = exports.JULIA_PREVIEW_SCALE = exports.ZOOM_SPEED = exports.ITERATIONS_ZOOM_SCALE = exports.BASE_ITERATIONS = exports.MAX_ZOOM_MAGNIFICATION = void 0;
const types_1 = require("./types");
exports.MAX_ZOOM_MAGNIFICATION = 1e14;
exports.BASE_ITERATIONS = 1000;
exports.ITERATIONS_ZOOM_SCALE = 10;
exports.ZOOM_SPEED = 1.5;
exports.JULIA_PREVIEW_SCALE = 0.3;
exports.DEFAULT_VIEW = new types_1.FractalView({
re: -0.5,
im: 0,
scale: 3.5,
set: types_1.FractalSet.MANDELBROT,
juliaC: { re: -0.7269, im: 0.1889 },
palette: 'fire',
maxIterations: exports.BASE_ITERATIONS,
smoothColoring: true,
});
const fireColors = [
{ offset: 0, r: 0, g: 0, b: 0 },
{ offset: 0.2, r: 80, g: 0, b: 0 },
{ offset: 0.4, r: 255, g: 69, b: 0 },
{ offset: 0.6, r: 255, g: 165, b: 0 },
{ offset: 0.8, r: 255, g: 255, b: 0 },
{ offset: 1, r: 255, g: 255, b: 255 },
];
const oceanColors = [
{ offset: 0, r: 0, g: 0, b: 0 },
{ offset: 0.2, r: 0, g: 0, b: 100 },
{ offset: 0.4, r: 0, g: 60, b: 160 },
{ offset: 0.6, r: 0, g: 130, b: 220 },
{ offset: 0.8, r: 60, g: 190, b: 255 },
{ offset: 1, r: 200, g: 255, b: 255 },
];
const monochromeColors = [
{ offset: 0, r: 0, g: 0, b: 0 },
{ offset: 0.5, r: 180, g: 180, b: 180 },
{ offset: 1, r: 255, g: 255, b: 255 },
];
const psychedelicColors = [
{ offset: 0, r: 128, g: 0, b: 128 },
{ offset: 0.166, r: 0, g: 0, b: 255 },
{ offset: 0.333, r: 0, g: 128, b: 128 },
{ offset: 0.5, r: 0, g: 255, b: 0 },
{ offset: 0.666, r: 255, g: 128, b: 0 },
{ offset: 0.833, r: 255, g: 0, b: 0 },
{ offset: 1, r: 128, g: 0, b: 128 },
];
const grayscaleColors = [
{ offset: 0, r: 0, g: 0, b: 0 },
{ offset: 0.3, r: 40, g: 40, b: 40 },
{ offset: 0.5, r: 100, g: 100, b: 100 },
{ offset: 0.7, r: 180, g: 180, b: 180 },
{ offset: 1, r: 255, g: 255, b: 255 },
];
exports.PALETTES = {
fire: { name: 'Fire', stops: fireColors },
ocean: { name: 'Ocean', stops: oceanColors },
monochrome: { name: 'Monochrome', stops: monochromeColors },
psychedelic: { name: 'Psychedelic', stops: psychedelicColors },
grayscale: { name: 'Grayscale', stops: grayscaleColors },
};
};
// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.FractalView = exports.FractalSet = void 0;
var FractalSet;
(function (FractalSet) {
FractalSet["MANDELBROT"] = "mandelbrot";
FractalSet["JULIA"] = "julia";
})(FractalSet || (exports.FractalSet = FractalSet = {}));
class FractalView {
constructor(params) {
this.re = params.re;
this.im = params.im;
this.scale = params.scale;
this.set = params.set;
this.juliaC = params.juliaC;
this.palette = params.palette;
this.maxIterations = params.maxIterations;
this.smoothColoring = params.smoothColoring;
}
static fromSavedOrDefault() {
try {
const saved = localStorage.getItem('fractalView');
if (saved) {
const parsed = JSON.parse(saved);
return new FractalView({
re: parsed.re || -0.5,
im: parsed.im || 0,
scale: parsed.scale || 3.5,
set: parsed.set || FractalSet.MANDELBROT,
juliaC: parsed.juliaC || { re: -0.7269, im: 0.1889 },
palette: parsed.palette || 'fire',
maxIterations: parsed.maxIterations || 1000,
smoothColoring: parsed.smoothColoring !== false,
});
}
}
catch (e) {
console.warn('Failed to load saved view:', e);
}
return new FractalView({
re: -0.5,
im: 0,
scale: 3.5,
set: FractalSet.MANDELBROT,
juliaC: { re: -0.7269, im: 0.1889 },
palette: 'fire',
maxIterations: 1000,
smoothColoring: true,
});
}
static fromBookmark(bookmark) {
return new FractalView({
re: bookmark.re,
im: bookmark.im,
scale: bookmark.scale,
set: bookmark.set || FractalSet.MANDELBROT,
juliaC: bookmark.juliaC || { re: -0.7269, im: 0.1889 },
palette: bookmark.palette || 'fire',
maxIterations: bookmark.maxIterations || 1000,
smoothColoring: bookmark.smoothColoring !== false,
});
}
clone() {
return new FractalView({
re: this.re,
im: this.im,
scale: this.scale,
set: this.set,
juliaC: { re: this.juliaC.re, im: this.juliaC.im },
palette: this.palette,
maxIterations: this.maxIterations,
smoothColoring: this.smoothColoring,
});
}
}
exports.FractalView = FractalView;
};
// ── module: src/engine.ts ──
__mods["src/engine.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.FractalEngine = void 0;
const types_1 = require("./types");
const constants_1 = require("./constants");
const ZOOM_HIST_MAX = 50;
class FractalEngine {
constructor(canvas, view, palettes) {
this.currentPass = 0;
this.cancelRender = false;
this.zoomHistory = [];
this.renderStart = 0;
this.fps = 0;
this.frameCount = 0;
this.fpsAccum = 0;
this.lastFPSTime = 0;
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.view = view;
this.resize();
this.imageData = this.ctx.createImageData(this.canvas.width, this.canvas.height);
this.pixels = this.imageData.data;
this.palettes = palettes;
this.lastFPSTime = performance.now();
this.zoomHistory.push(this.view.clone());
window.addEventListener('resize', () => this.resize());
}
resize() {
const width = window.innerWidth;
const height = window.innerHeight;
this.canvas.width = width;
this.canvas.height = height;
this.imageData = this.ctx.createImageData(width, height);
this.pixels = this.imageData.data;
window.addEventListener('resize', () => this.resize());
}
forceResize() {
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
this.imageData = this.ctx.createImageData(this.canvas.width, this.canvas.height);
this.pixels = this.imageData.data;
}
getView() {
return this.view;
}
setView(view) {
this.cancelRender = true;
const newView = view.clone();
newView.scale = Math.min(newView.scale, constants_1.MAX_ZOOM_MAGNIFICATION);
this.view = newView;
this.zoomHistory.push(newView);
if (this.zoomHistory.length > ZOOM_HIST_MAX) {
this.zoomHistory.shift();
}
this.render();
}
setPalette(name) {
this.cancelRender = true;
this.view.palette = name;
this.render();
}
setColorOffset(offset) {
this.view = this.view.clone();
this.render();
}
toggleJulia(c) {
this.cancelRender = true;
const newView = this.view.clone();
newView.set = this.view.set === types_1.FractalSet.MANDELBROT ? types_1.FractalSet.JULIA : types_1.FractalSet.MANDELBROT;
newView.juliaC = c;
this.setView(newView);
}
back() {
if (this.zoomHistory.length > 1) {
this.cancelRender = true;
this.zoomHistory.pop();
this.view = this.zoomHistory[this.zoomHistory.length - 1].clone();
this.render();
}
}
resetView() {
this.cancelRender = true;
this.view = new types_1.FractalView({
re: -0.5,
im: 0,
scale: 3.5,
set: types_1.FractalSet.MANDELBROT,
juliaC: { re: -0.7269, im: 0.1889 },
palette: this.view.palette,
maxIterations: this.view.maxIterations,
smoothColoring: this.view.smoothColoring,
});
this.zoomHistory = [this.view.clone()];
this.render();
}
iterationsAt(re, im) {
const maxIter = this.view.maxIterations;
let zr = re;
let zi = im;
const cr = this.view.set === types_1.FractalSet.JULIA ? this.view.juliaC.re : re;
const ci = this.view.set === types_1.FractalSet.JULIA ? this.view.juliaC.im : im;
let i = 0;
while (zr * zr + zi * zi < 4 && i < maxIter) {
const tmp = zr * zr - zi * zi + cr;
zi = 2 * zr * zi + ci;
zr = tmp;
i++;
}
return i;
}
saveView() {
const data = {
re: this.view.re,
im: this.view.im,
scale: this.view.scale,
set: this.view.set,
juliaC: this.view.juliaC,
palette: this.view.palette,
maxIterations: this.view.maxIterations,
smoothColoring: this.view.smoothColoring,
};
localStorage.setItem('fractalView', JSON.stringify(data));
}
getFPS() {
return this.fps;
}
zoomAtRect(x1, y1, x2, y2) {
this.cancelRender = true;
const view = this.view.clone();
const left = Math.min(x1, x2);
const right = Math.max(x1, x2);
const top = Math.min(y1, y2);
const bottom = Math.max(y1, y2);
const w = right - left;
const h = bottom - top;
if (w < 4 || h < 4)
return;
const rectCenterRe = view.re + (left / this.canvas.width - 0.5) * view.scale;
const rectCenterIm = view.im + (top / this.canvas.height - 0.5) * view.scale;
const oldScaleRe = view.scale / this.canvas.width;
const newScaleRe = w * oldScaleRe;
const oldScaleIm = view.scale / this.canvas.height;
const newScaleIm = h * oldScaleIm;
view.re = rectCenterRe;
view.im = rectCenterIm;
view.scale = newScaleRe;
this.setView(view);
}
zoomAtCursor(px, py, factor) {
this.cancelRender = true;
const view = this.view.clone();
const oldCenterRe = view.re + (px / this.canvas.width - 0.5) * view.scale;
const oldCenterIm = view.im + (py / this.canvas.height - 0.5) * view.scale;
view.scale /= factor;
view.re = oldCenterRe - (px / this.canvas.width - 0.5) * view.scale;
view.im = oldCenterIm - (py / this.canvas.height - 0.5) * view.scale;
this.setView(view);
}
panByPixels(dx, dy) {
this.cancelRender = true;
const view = this.view.clone();
view.re += (dx / this.canvas.width) * view.scale;
view.im += (dy / this.canvas.height) * view.scale;
this.setView(view);
}
render() {
this.cancelRender = false;
this.renderStart = performance.now();
this.currentPass = 0;
this.renderNextPass();
}
renderNextPass() {
if (this.cancelRender)
return;
const view = this.view;
const paletteName = view.palette;
const palette = this.palettes[paletteName] || this.palettes.fire;
const smooth = view.smoothColoring;
const maxIter = view.maxIterations;
const scale = view.scale / this.canvas.width;
const pass = this.currentPass;
const step = 1 << pass;
const offsetX = 0;
const offsetY = 0;
const left = view.re - view.scale / 2 + offsetX;
const top = view.im - view.scale / 2 + offsetY;
const juliaSet = view.set === types_1.FractalSet.JULIA;
const cr = view.juliaC.re;
const ci = view.juliaC.im;
let i = 0;
const dataLen = this.pixels.length;
for (let y = offsetY; y < this.canvas.height; y += step) {
for (let x = offsetX; x < this.canvas.width; x += step) {
const re = left + x * scale;
const im = top + y * (view.scale / this.canvas.height);
let zr = juliaSet ? 0 : re;
let zi = juliaSet ? 0 : im;
let iter = 0;
let smoothIter = iter;
while (zr * zr + zi * zi < 4 && iter < maxIter) {
const tmp = zr * zr - zi * zi + (juliaSet ? cr : re);
zi = 2 * zr * zi + (juliaSet ? ci : im);
zr = tmp;
iter++;
}
if (smooth && iter < maxIter) {
const logZn = Math.log(zr * zr + zi * zi) * 0.5;
smoothIter = iter + 1 - Math.log(logZn) / Math.log(2);
}
if (iter < maxIter) {
const normalized = smoothIter / maxIter;
const color = this.sampleColor(palette.stops, normalized);
const r = color.r;
const g = color.g;
const b = color.b;
for (let dy = 0; dy < step && y + dy < this.canvas.height; dy++) {
for (let dx = 0; dx < step && x + dx < this.canvas.width; dx++) {
const pos = ((y + dy) * this.canvas.width + x + dx) * 4;
if (pos + 3 < dataLen) {
this.pixels[pos] = r;
this.pixels[pos + 1] = g;
this.pixels[pos + 2] = b;
this.pixels[pos + 3] = 255;
}
}
}
}
else {
for (let dy = 0; dy < step && y + dy < this.canvas.height; dy++) {
for (let dx = 0; dx < step && x + dx < this.canvas.width; dx++) {
const pos = ((y + dy) * this.canvas.width + x + dx) * 4;
if (pos + 3 < dataLen) {
this.pixels[pos] = 0;
this.pixels[pos + 1] = 0;
this.pixels[pos + 2] = 0;
this.pixels[pos + 3] = 255;
}
}
}
}
i++;
}
}
this.ctx.putImageData(this.imageData, 0, 0);
this.frameCount++;
const now = performance.now();
const delta = now - this.lastFPSTime;
if (delta >= 1000) {
this.fps = Math.round((this.frameCount * 1000) / delta);
this.frameCount = 0;
this.lastFPSTime = now;
}
if (pass < 2) {
this.currentPass++;
this.renderNextPass();
}
}
sampleColor(stops, t) {
if (stops.length === 0)
return { r: 0, g: 0, b: 0 };
if (t <= 0)
return { r: stops[0].r, g: stops[0].g, b: stops[0].b };
if (t >= 1)
return { r: stops[stops.length - 1].r, g: stops[stops.length - 1].g, b: stops[stops.length - 1].b };
for (let i = 0; i < stops.length - 1; i++) {
const stop1 = stops[i];
const stop2 = stops[i + 1];
if (t >= stop1.offset && t <= stop2.offset) {
const localT = (t - stop1.offset) / (stop2.offset - stop1.offset);
return {
r: Math.round(stop1.r + (stop2.r - stop1.r) * localT),
g: Math.round(stop1.g + (stop2.g - stop1.g) * localT),
b: Math.round(stop1.b + (stop2.b - stop1.b) * localT),
};
}
}
return { r: 0, g: 0, b: 0 };
}
}
exports.FractalEngine = FractalEngine;
};
// ── module: src/ui.ts ──
__mods["src/ui.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.FractalUI = void 0;
const types_1 = require("./types");
const BOOKMARKS_KEY = 'fractalBookmarks';
class FractalUI {
constructor(canvas, engine, hud, hudText, palettes) {
this.currentPaletteIndex = 0;
this.isDragging = false;
this.dragStartX = 0;
this.dragStartY = 0;
this.viewChanged = false;
this.renderRequest = null;
this.canvas = canvas;
this.engine = engine;
this.hud = hud;
this.hudText = hudText;
this.controlsPanel = document.getElementById('controls');
this.zoomInBtn = document.getElementById('zoom-in');
this.zoomOutBtn = document.getElementById('zoom-out');
this.resetBtn = document.getElementById('reset-view');
this.backBtn = document.getElementById('back-zoom');
this.paletteBtn = document.getElementById('palette-select');
this.iterSlider = document.getElementById('iter-slider');
this.iterDisplay = document.getElementById('iter-display');
this.colorOffsetSlider = document.getElementById('color-offset');
this.offsetDisplay = document.getElementById('offset-display');
this.smoothToggle = document.getElementById('smooth-toggle');
this.juliaToggle = document.getElementById('julia-toggle');
this.saveBookmarkDialog = document.getElementById('save-bookmark-dialog');
this.bookmarkNameInput = document.getElementById('bookmark-name');
this.saveBookmarkBtn = document.getElementById('save-bookmark-btn');
this.cancelBookmarkBtn = document.getElementById('cancel-bookmark-btn');
this.bookmarksPanel = document.getElementById('bookmarks-list');
this.toast = document.getElementById('toast');
this.palettes = palettes;
this.init();
}
init() {
const view = this.engine.getView();
this.iterSlider.value = view.maxIterations.toString();
this.iterDisplay.textContent = view.maxIterations.toString();
this.smoothToggle.checked = view.smoothColoring;
if (view.set === types_1.FractalSet.JULIA) {
this.juliaToggle.checked = true;
}
const paletteNames = Object.keys(this.palettes);
this.currentPaletteIndex = paletteNames.indexOf(view.palette);
this.paletteBtn.textContent = this.palettes[view.palette].name;
this.iterSlider.addEventListener('input', () => {
const value = parseInt(this.iterSlider.value);
this.iterDisplay.textContent = value.toString();
const newView = view.clone();
newView.maxIterations = value;
this.engine.setView(newView);
});
this.colorOffsetSlider.addEventListener('input', () => {
const value = parseInt(this.colorOffsetSlider.value);
this.offsetDisplay.textContent = value + '°';
this.engine.setColorOffset(value);
});
this.smoothToggle.addEventListener('change', () => {
const newView = view.clone();
newView.smoothColoring = this.smoothToggle.checked;
this.engine.setView(newView);
});
this.juliaToggle.addEventListener('change', () => {
if (this.juliaToggle.checked && view.set === types_1.FractalSet.MANDELBROT) {
const c = { re: view.re, im: view.im };
this.engine.toggleJulia(c);
}
else if (!this.juliaToggle.checked && view.set === types_1.FractalSet.JULIA) {
this.engine.setView(view.clone());
}
});
this.zoomInBtn.addEventListener('click', () => {
const centerX = this.canvas.width / 2;
const centerY = this.canvas.height / 2;
this.engine.zoomAtCursor(centerX, centerY, 1.5);
});
this.zoomOutBtn.addEventListener('click', () => {
const centerX = this.canvas.width / 2;
const centerY = this.canvas.height / 2;
this.engine.zoomAtCursor(centerX, centerY, 1 / 1.5);
});
this.resetBtn.addEventListener('click', () => {
this.engine.resetView();
});
this.backBtn.addEventListener('click', () => {
this.engine.back();
});
this.paletteBtn.addEventListener('click', () => {
const paletteNames = Object.keys(this.palettes);
this.currentPaletteIndex = (this.currentPaletteIndex + 1) % paletteNames.length;
const name = paletteNames[this.currentPaletteIndex];
this.paletteBtn.textContent = this.palettes[name].name;
this.engine.setPalette(name);
});
this.saveBookmarkBtn.addEventListener('click', () => {
const name = this.bookmarkNameInput.value;
if (name) {
this.saveBookmark(name);
this.saveBookmarkDialog.classList.remove('show');
this.showToast('Bookmark saved!');
}
});
this.cancelBookmarkBtn.addEventListener('click', () => {
this.saveBookmarkDialog.classList.remove('show');
});
this.canvas.addEventListener('mousedown', (e) => {
this.isDragging = true;
this.dragStartX = e.clientX;
this.dragStartY = e.clientY;
this.viewChanged = false;
});
this.canvas.addEventListener('mousemove', (e) => {
if (this.isDragging) {
this.viewChanged = true;
}
});
this.canvas.addEventListener('mouseup', (e) => {
this.isDragging = false;
if (!this.viewChanged && e.button === 0) {
const rect = this.canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const view = this.engine.getView();
const re = view.re + (x / this.canvas.width - 0.5) * view.scale;
const im = view.im + (y / this.canvas.height - 0.5) * view.scale;
this.engine.toggleJulia({ re, im });
}
});
this.canvas.addEventListener('wheel', (e) => {
e.preventDefault();
const rect = this.canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const factor = e.deltaY > 0 ? 1.2 : 1 / 1.2;
this.engine.zoomAtCursor(x, y, factor);
}, { passive: false });
window.addEventListener('keydown', (e) => {
switch (e.key) {
case 'ArrowLeft':
this.engine.panByPixels(20, 0);
break;
case 'ArrowRight':
this.engine.panByPixels(-20, 0);
break;
case 'ArrowUp':
this.engine.panByPixels(0, 20);
break;
case 'ArrowDown':
this.engine.panByPixels(0, -20);
break;
case '+':
case '=':
const centerX = this.canvas.width / 2;
const centerY = this.canvas.height / 2;
this.engine.zoomAtCursor(centerX, centerY, 1.5);
break;
case '-':
const cx = this.canvas.width / 2;
const cy = this.canvas.height / 2;
this.engine.zoomAtCursor(cx, cy, 1 / 1.5);
break;
case 'r':
this.engine.resetView();
break;
case 'b':
this.engine.back();
break;
case 'j':
if (view.set === types_1.FractalSet.MANDELBROT) {
const c = { re: view.re, im: view.im };
this.engine.toggleJulia(c);
}
else {
this.engine.setView(view.clone());
}
break;
case 's':
this.saveBookmarkDialog.classList.add('show');
break;
}
});
this.updateHUD();
this.updateBookmarks();
setInterval(() => this.updateHUD(), 2000);
}
updateHUD() {
const view = this.engine.getView();
const fps = this.engine.getFPS();
this.hudText.innerHTML =
`Scale: ${view.scale.toFixed(2)} | Center: (${view.re.toFixed(6)}, ${view.im.toFixed(6)}) | Iterations: ${view.maxIterations} | FPS: ${fps}`;
this.hud.classList.remove('hidden');
}
saveBookmark(name) {
const view = this.engine.getView();
const data = {
name,
re: view.re,
im: view.im,
scale: view.scale,
set: view.set,
juliaC: view.juliaC,
palette: view.palette,
maxIterations: view.maxIterations,
smoothColoring: view.smoothColoring,
};
let bookmarks = [];
try {
const saved = localStorage.getItem(BOOKMARKS_KEY);
if (saved) {
bookmarks = JSON.parse(saved);
}
}
catch (e) {
console.warn('Failed to load bookmarks:', e);
}
bookmarks.push(data);
localStorage.setItem(BOOKMARKS_KEY, JSON.stringify(bookmarks));
this.updateBookmarks();
}
updateBookmarks() {
this.bookmarksPanel.innerHTML = '';
let bookmarks = [];
try {
const saved = localStorage.getItem(BOOKMARKS_KEY);
if (saved) {
bookmarks = JSON.parse(saved);
}
}
catch (e) {
console.warn('Failed to load bookmarks:', e);
}
bookmarks.forEach((bookmark, index) => {
const div = document.createElement('div');
div.className = 'bookmark-item';
div.innerHTML = `<span>${bookmark.name}</span><button class="delete-bookmark-btn" data-index="${index}">Delete</button>`;
div.addEventListener('click', (e) => {
if (e.target.classList.contains('delete-bookmark-btn')) {
e.stopPropagation();
const deleteIndex = parseInt(e.target.getAttribute('data-index') || '0');
bookmarks.splice(deleteIndex, 1);
localStorage.setItem(BOOKMARKS_KEY, JSON.stringify(bookmarks));
this.updateBookmarks();
this.showToast('Bookmark deleted');
}
else {
const view = types_1.FractalView.fromBookmark(bookmark);
this.engine.setView(view);
this.showToast('Bookmark loaded');
}
});
this.bookmarksPanel.appendChild(div);
});
}
showToast(message) {
this.toast.textContent = message;
this.toast.classList.add('show');
setTimeout(() => {
this.toast.classList.remove('show');
}, 2000);
}
}
exports.FractalUI = FractalUI;
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"bartowski/ProCreations_grug-27b-GGUF-IQ4","provider":"brigid","persona":"typescript","sessionId":"4d90a789-9d02-4afa-b95e-82390045846b","tokensIn":434510,"tokensOut":13353,"tokensTotal":447863,"cost":0,"turns":26,"toolCalls":27,"failedToolCalls":1,"timestamp":"2026-08-05T17:32:15.996Z"} -->