Fractal Explorer
ling 3.0 tiny:free · full

Browser check: Artifact runtime error. This is a technical check, not a quality review.
Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 37,932 bytes · SHA-256 4b52f6f8a561
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Fractal Explorer — Mandelbrot & Julia</title>
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
body{overflow:hidden;background:#0a0a0f;font-family:'Courier New',monospace;color:#e0e0e0;user-select:none;-webkit-user-select:none}
canvas{display:block;position:fixed;top:0;left:0;z-index:0}
/* HUD */
#hud{position:fixed;top:0;left:0;right:0;z-index:10;pointer-events:none;display:flex;justify-content:space-between;align-items:center;padding:8px 14px;background:rgba(8,8,16,0.85);backdrop-filter:blur(6px);border-bottom:1px solid rgba(0,255,136,0.15)}
#hud.hidden{display:none}
.hud-item{display:flex;flex-direction:column;gap:1px;font-size:11px;letter-spacing:0.5px;white-space:nowrap}
.hud-label{font-size:9px;color:#6a6a7a;text-transform:uppercase}
.hud-val{font-size:15px;font-weight:bold;color:#00ff88;font-family:'Courier New',monospace;text-shadow:0 0 8px rgba(0,255,136,0.4)}
.hud-val.magn{font-size:16px}
.hud-val.fps{color:#ff66aa}
.hud-val.iter{color:#ffcc00}
/* Zoom box */
#zoomBox{position:fixed;top:0;left:0;width:0;height:0;border:2px solid #00ff88;pointer-events:none;z-index:20;transition:all 0.1s;opacity:0}
#zoomBox.visible{opacity:1}
/* Controls bar */
#controls{position:fixed;bottom:0;left:0;right:0;z-index:10;display:flex;align-items:center;gap:6px;padding:6px 12px;background:rgba(8,8,16,0.85);backdrop-filter:blur(6px);border-top:1px solid rgba(0,255,136,0.15);flex-wrap:wrap}
#controls.hidden{display:none}
.ctrl-group{display:flex;align-items:center;gap:3px}
.ctrl-btn{background:rgba(0,255,136,0.08);border:1px solid rgba(0,255,136,0.25);color:#00ff88;border-radius:4px;padding:4px 10px;font-size:11px;cursor:pointer;transition:all 0.15s;font-family:'Courier New',monospace;white-space:nowrap}
.ctrl-btn:hover{background:rgba(0,255,136,0.2);border-color:#00ff88}
.ctrl-btn.active{background:rgba(0,255,136,0.3);border-color:#00ff88}
.ctrl-btn:focus-visible{outline:2px solid #00ff88;outline-offset:1px}
.ctrl-btn.julia-btn{color:#ff66aa;border-color:rgba(255,100,100,0.3);background:rgba(255,100,100,0.08)}
.ctrl-btn.julia-btn:hover{background:rgba(255,100,100,0.2);border-color:#ff66aa}
.ctrl-sep{width:1px;height:24px;background:rgba(255,255,255,0.1);margin:0 4px}
.ctrl-label{font-size:10px;color:#6a6a7a;margin-right:2px}
.ctrl-slider{-webkit-appearance:none;appearance:none;width:80px;height:4px;background:rgba(255,255,255,0.1);border-radius:2px;outline:none}
.ctrl-slider::-webkit-slider-thumb{-webkit-appearance:none;width:12px;height:12px;border-radius:50%;background:#00ff88;cursor:pointer}
.ctrl-slider::-moz-range-thumb{width:12px;height:12px;border-radius:50%;background:#00ff88;cursor:pointer;border:none}
.ctrl-val{font-size:10px;color:#00ff88;min-width:50px;text-align:right;font-family:'Courier New',monospace}
/* Palette selector */
#paletteSelect{position:fixed;bottom:44px;left:12px;z-index:10;display:flex;flex-direction:column;gap:3px}
.palette-chip{width:28px;height:28px;border-radius:4px;cursor:pointer;border:2px solid transparent;transition:all 0.15s}
.palette-chip:hover{transform:scale(1.2);border-color:#fff}
.palette-chip.active{border-color:#fff;box-shadow:0 0 8px rgba(255,255,255,0.5)}
.palette-chip:focus-visible{outline:2px solid #fff;outline-offset:1px}
/* Debug panel */
#debug{position:fixed;bottom:44px;right:12px;z-index:10;background:rgba(8,8,16,0.9);border:1px solid rgba(0,255,136,0.2);border-radius:6px;padding:8px 10px;font-size:10px;max-width:240px;line-height:1.6}
#debug.hidden{display:none}
#debug h4{color:#00ff88;margin-bottom:4px;font-size:10px;text-transform:uppercase}
#debug .row{display:flex;justify-content:space-between;padding:1px 0}
#debug .key{color:#888}
#debug .val{color:#00ff88;font-family:'Courier New',monospace}
/* Julia preview */
#juliaPreview{position:fixed;top:0;right:0;bottom:0;width:380px;z-index:15;background:rgba(8,8,16,0.9);border-left:1px solid rgba(0,255,136,0.15);display:flex;flex-direction:column}
#juliaPreview.hidden{display:none}
.julia-header{display:flex;align-items:center;justify-content:space-between;padding:8px 10px;background:rgba(0,255,136,0.05);border-bottom:1px solid rgba(0,255,136,0.15)}
.julia-header h3{font-size:11px;color:#ff66aa;text-transform:uppercase;letter-spacing:1px}
.julia-close{background:none;border:1px solid rgba(255,100,100,0.3);color:#ff66aa;border-radius:3px;padding:2px 8px;cursor:pointer;font-size:10px;transition:all 0.15s}
.julia-close:hover{background:rgba(255,100,100,0.2)}
.julia-canvas-wrap{flex:1;display:flex;align-items:center;justify-content:center;position:relative;overflow:hidden}
.julia-canvas{display:block;cursor:crosshair}
#juliaInfo{font-size:10px;color:#8a8a9a;padding:6px 10px;border-top:1px solid rgba(0,255,136,0.15)}
/* Toast */
#toast{position:fixed;top:50px;left:50%;transform:translateX(-50%);z-index:100;background:rgba(0,255,136,0.15);border:1px solid rgba(0,255,136,0.4);color:#00ff88;padding:6px 16px;border-radius:4px;font-size:11px;pointer-events:none;opacity:0;transition:opacity 0.2s}
#toast.show{opacity:1}
/* Scrollbar */
::-webkit-scrollbar{width:6px}
::-webkit-scrollbar-track{background:transparent}
::-webkit-scrollbar-thumb{background:rgba(255,255,255,0.15);border-radius:3px}
</style>
</head>
<body>
<canvas id="mainCanvas"></canvas>
<div id="hud" class="hidden">
<div class="hud-item"><span class="hud-label">Magnification</span><span class="hud-val magn" id="hudMag">×1</span></div>
<div class="hud-item"><span class="hud-label">Center</span><span class="hud-val" id="hudCenter">(-0.0000, 0.0000)</span></div>
<div class="hud-item"><span class="hud-label">Iterations</span><span class="hud-val iter" id="hudIter">—</span></div>
<div class="hud-item"><span class="hud-label">FPS</span><span class="hud-val fps" id="hudFPS">—</span></div>
</div>
<div id="zoomBox"></div>
<div id="controls">
<button class="ctrl-btn" id="btnReset" aria-label="Reset view">Reset</button>
<button class="ctrl-btn" id="btnJulia" aria-label="Toggle Julia mode">Julia</button>
<button class="ctrl-btn" id="btnPause" aria-label="Pause rendering">Pause</button>
<div class="ctrl-sep"></div>
<div class="ctrl-group">
<span class="ctrl-label">Zoom</span>
<input type="range" class="ctrl-slider" id="zoomSlider" min="-4" max="0" step="0.01" value="0" aria-label="Zoom level">
<span class="ctrl-val" id="zoomVal">×1</span>
</div>
<div class="ctrl-group">
<span class="ctrl-label">Max Iter</span>
<input type="range" class="ctrl-slider" id="iterSlider" min="200" max="10000" step="100" value="5000" aria-label="Max iterations">
<span class="ctrl-val" id="iterVal">5000</span>
</div>
<div class="ctrl-group">
<span class="ctrl-label">Color</span>
<input type="range" class="ctrl-slider" id="colorSlider" min="0" max="3" step="1" value="0" aria-label="Color palette">
<span class="ctrl-val" id="colorVal">0</span>
</div>
</div>
<div id="paletteSelect" role="radiogroup" aria-label="Color palette selector">
<button class="palette-chip active" data-palette="0" role="radio" aria-checked="true" aria-label="Fire palette" tabindex="0"></button>
<button class="palette-chip" data-palette="1" role="radio" aria-checked="false" aria-label="Ocean palette" tabindex="0"></button>
<button class="palette-chip" data-palette="2" role="radio" aria-checked="false" aria-label="Monochrome palette" tabindex="0"></button>
<button class="palette-chip" data-palette="3" role="radio" aria-checked="false" aria-label="Spectral palette" tabindex="0"></button>
<button class="palette-chip" data-palette="4" role="radio" aria-checked="false" aria-label="Cyan palette" tabindex="0"></button>
</div>
<div id="juliaPreview" class="hidden">
<div class="julia-header">
<h3>Julia Set — c = <span id="juliaCVal">—</span></h3>
<button class="julia-close" id="juliaClose" aria-label="Close Julia preview">Close</button>
</div>
<div class="julia-canvas-wrap">
<canvas id="juliaCanvas" class="julia-canvas"></canvas>
</div>
<div id="juliaInfo">Click a point in Mandelbrot view to preview Julia set for that c. Press J to toggle.</div>
</div>
<div id="debug">
<h4>Debug Interface</h4>
<div class="row"><span class="key">iterationsAt(0,0)</span><span class="val" id="debugIter00">—</span></div>
<div class="row"><span class="key">iterationsAt(-1,0)</span><span class="val" id="debugIter-10">—</span></div>
<div class="row"><span class="key">iterationsAt(2,0)</span><span class="val" id="debugIter20">—</span></div>
<div class="row"><span class="key">iterationsAt(0.25,0)</span><span class="val" id="debugIter0250">—</span></div>
<div class="row"><span class="key">setView({re,im,scale})</span><span class="val" id="debugSetView">—</span></div>
<div class="row"><span class="key">fractal.c</span><span class="val" id="debugC">—</span></div>
</div>
<div id="toast" role="status" aria-live="polite"></div>
<script>
// ========================================================================
// Fractal Explorer — Mandelbrot & Julia Set
// All code: vanilla JS, no external dependencies, no CDN
// ========================================================================
// ---- Configuration ----
const CONFIG = {
MAX_ITER_BASE: 5000,
ZOOM_SPEED: 0.8,
MIN_MAGNIFICATION: 1e-14,
MAX_MAGNIFICATION: 1e14,
MAX_ITER_CAP: 10000,
PROGRESSIVE_PASSES: [0.25, 0.5, 1.0],
FRAME_TIMEOUT: 5000,
JULIA_PRECISION: 15,
PALETTE_STOPS: [
{pos: 0.0, r: 128, g: 0, b: 255},
{pos: 0.3, r: 255, g: 128, b: 0},
{pos: 0.6, r: 255, g: 255, b: 0},
{pos: 0.8, r: 0, g: 255, b: 128},
{pos: 1.0, r: 0, g: 0, b: 255},
],
PALETTE_OCEAN: [
{pos: 0.0, r: 0, g: 255, b: 255},
{pos: 0.25, r: 0, g: 200, b: 255},
{pos: 0.5, r: 0, g: 100, b: 255},
{pos: 0.75, r: 20, g: 0, b: 255},
{pos: 1.0, r: 40, g: 0, b: 255},
],
PALETTE_MONOCHROME: [
{pos: 0.0, r: 0, g: 0, b: 0},
{pos: 0.5, r: 128, g: 128, b: 128},
{pos: 1.0, r: 255, g: 255, b: 255},
],
PALETTE_SPECTRAL: [
{pos: 0.0, r: 100, g: 0, b: 150},
{pos: 0.25, r: 0, g: 200, b: 200},
{pos: 0.5, r: 200, g: 0, b: 100},
{pos: 0.75, r: 0, g: 150, b: 200},
{pos: 1.0, r: 150, g: 150, b: 0},
],
PALETTE_CYAN: [
{pos: 0.0, r: 0, g: 255, b: 255},
{pos: 0.33, r: 0, g: 200, b: 255},
{pos: 0.66, r: 100, g: 0, b: 255},
{pos: 1.0, r: 0, g: 0, b: 255},
],
};
// ---- Debug console ----
const debug = {
log: function(msg) {
const el = document.getElementById('debug');
const div = document.createElement('div');
div.textContent = msg;
el.innerHTML = '<h4>Debug Interface</h4>' + div.textContent;
},
print: function(a, b, c, d, e, f, g, h) {
const vals = [a, b, c, d, e, f, g, h].filter(v => typeof v === 'number');
this.log('iterationsAt(' + vals.join(',') + ') = ' + vals.map(v => Math.round(v)).join(', '));
},
setView: function(v) {
this.log('setView(' + JSON.stringify(v) + ')');
}
};
// ---- State ----
let state = {
zoom: 1,
center: { re: -0.5, im: 0 },
c: { re: -0.7, im: 0.27015 },
juliaMode: false,
palette: 0,
maxIter: 5000,
};
// ---- Render state ----
let renderState = {
active: false,
cancelled: false,
phase: 0,
};
// ---- FPS ----
let fps = 0;
let fpsFrames = 0;
let fpsLastTime = performance.now();
// ---- Mouse / Touch ----
let mouseDown = false;
let mouseButton = 0;
let mouseX = 0, mouseY = 0;
let dragMoved = false;
let zoomBox = null;
let boxStart = null;
let boxEnd = null;
// ---- Progressive rendering ----
let progressiveCancelled = false;
// ---- Toast ----
function showToast(msg) {
const toast = document.getElementById('toast');
toast.textContent = msg;
toast.classList.add('show');
setTimeout(() => toast.classList.remove('show'), 2000);
}
// ========================================================================
// Math Utilities
// ========================================================================
function computeIterations(x, y, cRe, cIm, maxIter) {
let zx = 0, zy = 0;
let iter = 0;
while (iter < maxIter) {
const x2 = zx * zx;
const y2 = zy * zy;
if (x2 + y2 > 4) break;
const newX = x2 - y2 + cRe;
const newY = 2 * zx * zy + cIm;
zx = newX;
zy = newY;
iter++;
}
return { iter, zx, zy };
}
function smoothColor(iter, zx, zy, maxIter) {
if (iter >= maxIter) return 0;
const log2_abs2 = Math.log2(zx * zx + zy * zy);
const clamped = Math.max(log2_abs2, 1e-10);
const smooth = iter + 1 - Math.log2(Math.log2(clamped));
return Math.max(0, Math.min(maxIter, smooth));
}
// ========================================================================
// Color Palettes
// ========================================================================
function interpolatePalette(stops, t) {
t = Math.max(0, Math.min(1, t));
let i = 0;
for (let j = 0; j < stops.length - 1; j++) {
if (t >= stops[j].pos && t <= stops[j + 1].pos) { i = j; break; }
}
const s0 = stops[i], s1 = stops[i + 1];
const f = (t - s0.pos) / Math.max(1e-10, s1.pos - s0.pos);
return {
r: Math.round(s0.r + (s1.r - s0.r) * f),
g: Math.round(s0.g + (s1.g - s0.g) * f),
b: Math.round(s0.b + (s1.b - s0.b) * f),
};
}
function getPaletteColor(smoothIter, maxIter) {
if (smoothIter >= maxIter) return 'rgb(0,0,0)';
const t = Math.min(1, smoothIter / maxIter);
const stops = CONFIG.PALETTE_STOPS;
let color = [128, 128, 128];
for (let j = 0; j < stops.length - 1; j++) {
if (t >= stops[j].pos && t <= stops[j + 1].pos) {
const f = (t - stops[j].pos) / Math.max(1e-10, stops[j + 1].pos - stops[j].pos);
color[0] = Math.round(stops[j].r + (stops[j + 1].r - stops[j].r) * f);
color[1] = Math.round(stops[j].g + (stops[j + 1].g - stops[j].g) * f);
color[2] = Math.round(stops[j].b + (stops[j + 1].b - stops[j].b) * f);
break;
}
}
return 'rgb(' + color[0] + ',' + color[1] + ',' + color[2] + ')';
}
function getJuliaColor(smoothIter, maxIter) {
if (smoothIter >= maxIter) return 'rgb(0,0,0)';
const t = smoothIter / maxIter;
// Spectral-like coloring for Julia set
const r = Math.round(128 + (t * 127 - 0.5) * 127);
const g = Math.round(128 + (t * 127 - 0.5) * 127);
const b = Math.round(128 + (t * 127 - 0.5) * 127);
return 'rgb(' + Math.max(0, Math.min(255, r)) + ',' + Math.max(0, Math.min(255, g)) + ',' + Math.max(0, Math.min(255, b)) + ')';
}
// ========================================================================
// Rendering
// ========================================================================
function getMaxIterForZoom(scale) {
const mag = Math.log10(Math.max(scale * 0.01, 1));
return Math.min(CONFIG.MAX_ITER_CAP, Math.max(200, CONFIG.MAX_ITER_BASE + Math.round(mag * 1500)));
}
function renderFrame() {
if (renderState.cancelled) { renderState.active = false; return; }
if (renderState.active) return;
renderState.active = true;
renderState.cancelled = false;
const w = canvas.width;
const h = canvas.height;
if (w === 0 || h === 0) { renderState.active = false; return; }
const maxIter = state.maxIter;
const cRe = state.c.re;
const cIm = state.c.im;
// Render at proportional resolution based on zoom
const resolutionFactor = 0.25;
const rw = Math.ceil(w * resolutionFactor);
const rh = Math.ceil(h * resolutionFactor);
const imageData = new ImageData(rw, rh);
const pixels = new Uint32Array(rw * rh);
for (let y = 0; y < rh; y++) {
for (let x = 0; x < rw; x++) {
const px = (x / rw) * resolutionFactor - 0.5;
const py = (y / rh) * resolutionFactor - 0.5;
const result = computeIterations(px, py, cRe, cIm, maxIter);
const smooth = result.iter < maxIter
? smoothColor(result.iter, result.zx, result.zy, maxIter)
: 0;
pixels[y * rw + x] = smooth;
}
}
// Colorize
for (let i = 0; i < pixels.length; i++) {
const smooth = pixels[i];
let col;
if (smooth < 0) {
col = [0, 0, 0];
} else {
const t = Math.min(1, smooth / maxIter);
const stops = CONFIG.PALETTE_STOPS;
let color = [128, 128, 128];
for (let j = 0; j < stops.length - 1; j++) {
if (t >= stops[j].pos && t <= stops[j + 1].pos) {
const f = (t - stops[j].pos) / Math.max(1e-10, stops[j + 1].pos - stops[j].pos);
color[0] = Math.round(stops[j].r + (stops[j + 1].r - stops[j].r) * f);
color[1] = Math.round(stops[j].g + (stops[j + 1].g - stops[j].g) * f);
color[2] = Math.round(stops[j].b + (stops[j + 1].b - stops[j].b) * f);
break;
}
}
col = color;
}
pixels[i] = (col[0] << 16) | (col[1] << 8) | col[2];
}
imageData.data.set(pixels);
ctx.putImageData(imageData, 0, 0);
// Scale the coarse pass up to fill the canvas
ctx.drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, canvas.width, canvas.height);
renderState.active = false;
updateHUD();
}
function updateHUD() {
const mag = state.zoom * 1e-6;
const zoomStr = mag.toExponential(2);
document.getElementById('hudMag').textContent = '×' + zoomStr;
document.getElementById('hudCenter').textContent =
'(' + state.center.re.toFixed(6) + ', ' + state.center.im.toFixed(6) + ')';
document.getElementById('hudIter').textContent =
state.maxIter.toLocaleString() + ' max';
document.getElementById('hudFPS').textContent = fps;
}
// ========================================================================
// Progressive Rendering
// ========================================================================
function cancelRender() {
renderState.cancelled = true;
}
function startProgressiveRender() {
// Cancel any in-flight render
renderState.cancelled = true;
renderState.cancelled = false;
renderState.phase = 0;
renderFramePass(0);
}
function renderFramePass(phase) {
if (renderState.cancelled) { return; }
if (phase >= CONFIG.PROGRESSIVE_PASSES.length) {
renderState.active = false;
return;
}
const resolution = CONFIG.PROGRESSIVE_PASSES[phase];
const w = canvas.width;
const h = canvas.height;
if (w === 0 || h === 0) { return; }
const rw = Math.ceil(w * resolution);
const rh = Math.ceil(h * resolution);
const imageData = new ImageData(rw, rh);
const pixels = new Uint32Array(rw * rh);
const maxIter = state.maxIter;
const cRe = state.c.re;
const cIm = state.c.im;
for (let y = 0; y < rh; y++) {
for (let x = 0; x < rw; x++) {
const px = (x / rw) * resolution - 0.5;
const py = (y / rh) * resolution - 0.5;
const result = computeIterations(px, py, cRe, cIm, maxIter);
const smooth = result.iter < maxIter
? smoothColor(result.iter, result.zx, result.zy, maxIter)
: 0;
pixels[y * rw + x] = smooth;
}
}
// Colorize
for (let i = 0; i < pixels.length; i++) {
const smooth = pixels[i];
let col;
if (smooth < 0) {
col = [0, 0, 0];
} else {
const t = Math.min(1, smooth / maxIter);
const stops = CONFIG.PALETTE_STOPS;
let color = [128, 128, 128];
for (let j = 0; j < stops.length - 1; j++) {
if (t >= stops[j].pos && t <= stops[j + 1].pos) {
const f = (t - stops[j].pos) / Math.max(1e-10, stops[j + 1].pos - stops[j].pos);
color[0] = Math.round(stops[j].r + (stops[j + 1].r - stops[j].r) * f);
color[1] = Math.round(stops[j].g + (stops[j + 1].g - stops[j].g) * f);
color[2] = Math.round(stops[j].b + (stops[j + 1].b - stops[j].b) * f);
break;
}
}
col = color;
}
pixels[i] = (col[0] << 16) | (col[1] << 8) | col[2];
}
imageData.data.set(pixels);
ctx.putImageData(imageData, 0, 0);
// Scale the coarse pass up to fill the canvas
ctx.drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, canvas.width, canvas.height);
// Continue to next phase
renderState.phase = phase + 1;
setTimeout(() => renderFramePass(phase + 1), 50);
}
// ========================================================================
// Interaction
// ========================================================================
function updateMousePosition(clientX, clientY) {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
mouseX = (clientX - rect.left) * scaleX;
mouseY = (clientY - rect.top) * scaleY;
}
function screenToComplex(sx, sy) {
const w = canvas.width;
const h = canvas.height;
const x = (sx / w) * 2 - 1;
const y = 1 - (sy / h) * 2;
return { re: x * state.zoom + state.center.re, im: y * state.zoom + state.center.im };
}
function complexToScreen(re, im) {
const w = canvas.width;
const h = canvas.height;
const x = (re - state.center.re) / state.zoom + w / 2;
const y = (im - state.center.im) / state.zoom + h / 2;
return { sx: x, sy: y };
}
function drawZoomBox(clientX, clientY, isDrag) {
if (!isDrag) return;
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const sx1 = (mouseX / rect.width) * canvas.width;
const sy1 = (mouseY / rect.height) * canvas.height;
const sx2 = (clientX / rect.width) * canvas.width;
const sy2 = (clientY / rect.height) * canvas.height;
const x1 = Math.min(sx1, sx2);
const y1 = Math.min(sy1, sy2);
const x2 = Math.max(sx1, sx2);
const y2 = Math.max(sy1, sy2);
const w = x2 - x1;
const h = y2 - y1;
const z1 = screenToComplex(x1, y1);
const z2 = screenToComplex(x2, y2);
zoomBox = document.getElementById('zoomBox');
zoomBox.style.left = x1 + 'px';
zoomBox.style.top = y1 + 'px';
zoomBox.style.width = w + 'px';
zoomBox.style.height = h + 'px';
zoomBox.classList.add('visible');
boxStart = z1;
boxEnd = z2;
}
function applyZoomBox() {
if (!boxStart) return;
const z1 = boxStart;
const z2 = boxEnd;
const newCenterRe = (z1.re + z2.re) / 2;
const newCenterIm = (z1.im + z2.im) / 2;
const newScale = (z2.re - z1.re) * (z2.im - z1.im);
if (newScale > 0) {
state.center = { re: newCenterRe, im: newCenterIm };
state.zoom = Math.max(1e-12, Math.min(state.zoom / (1 + newScale), CONFIG.MAX_MAGNIFICATION));
state.zoom = Math.max(1e-12, Math.min(state.zoom, CONFIG.MAX_MAGNIFICATION));
}
// Clamp zoom to prevent infinite zoom
state.zoom = Math.max(1e-12, Math.min(state.zoom, CONFIG.MAX_MAGNIFICATION));
cancelRender();
startProgressiveRender();
}
function onMouseDown(e) {
e.preventDefault();
mouseDown = true;
dragMoved = false;
const clientX = e.clientX || (e.touches && e.touches[0].clientX);
const clientY = e.clientY || (e.touches && e.touches[0].clientY);
updateMousePosition(clientX, clientY);
drawZoomBox(clientX, clientY, false);
}
function onMouseMove(e) {
const clientX = e.clientX || (e.touches && e.touches[0].clientX);
const clientY = e.clientY || (e.touches && e.touches[0].clientY);
updateMousePosition(clientX, clientY);
if (mouseDown) {
dragMoved = true;
drawZoomBox(clientX, clientY, true);
}
}
function onMouseUp(e) {
mouseDown = false;
if (dragMoved) {
applyZoomBox();
}
zoomBox.classList.remove('visible');
}
function onWheel(e) {
e.preventDefault();
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const mouseX = (e.clientX - rect.left) * scaleX;
const mouseY = (e.clientY - rect.top) * scaleY;
const centerBefore = { re: state.center.re, im: state.center.im };
const complexBefore = screenToComplex(mouseX, mouseY);
const zoomFactor = e.deltaY > 0 ? 1 + CONFIG.ZOOM_SPEED : 1 / (1 + CONFIG.ZOOM_SPEED);
state.zoom *= zoomFactor;
const complexAfter = screenToComplex(mouseX, mouseY);
const reDiff = complexBefore.re - complexAfter.re;
const imDiff = complexBefore.im - complexAfter.im;
state.center.re -= reDiff * state.zoom * 0.1;
state.center.im -= imDiff * state.zoom * 0.1;
state.zoom = Math.max(1e-12, Math.min(state.zoom, CONFIG.MAX_MAGNIFICATION));
cancelRender();
startProgressiveRender();
updateHUD();
}
function onKeyDown(e) {
const step = 0.1;
switch (e.key) {
case 'ArrowLeft':
state.center.re -= step / state.zoom;
cancelRender();
startProgressiveRender();
break;
case 'ArrowRight':
state.center.re += step / state.zoom;
cancelRender();
startProgressiveRender();
break;
case 'ArrowUp':
state.center.im -= step / state.zoom;
cancelRender();
startProgressiveRender();
break;
case 'ArrowDown':
state.center.im += step / state.zoom;
cancelRender();
startProgressiveRender();
break;
case '+':
case '=':
state.zoom = Math.min(state.zoom * (1 + CONFIG.ZOOM_SPEED * 0.1), CONFIG.MAX_MAGNIFICATION);
cancelRender();
startProgressiveRender();
updateHUD();
break;
case '-':
state.zoom = Math.max(state.zoom * (1 - CONFIG.ZOOM_SPEED * 0.1), 1e-12);
cancelRender();
startProgressiveRender();
updateHUD();
break;
case 'j':
case 'J':
toggleJuliaMode();
break;
case 'r':
case 'R':
resetView();
break;
case 'z':
case 'Z':
undoZoom();
break;
}
}
// ========================================================================
// Julia Set
// ========================================================================
function renderJulia() {
if (!state.juliaMode) return;
const juliaCanvasEl = juliaCanvas;
const juliaW = juliaCanvasEl.width;
const juliaH = juliaCanvasEl.height;
if (juliaW === 0 || juliaH === 0) return;
const maxIter = state.maxIter;
const cRe = state.c.re;
const cIm = state.c.im;
const pixels = new Uint32Array(juliaW * juliaH);
for (let y = 0; y < juliaH; y++) {
for (let x = 0; x < juliaW; x++) {
const px = (x / juliaW) * 2 - 1;
const py = 1 - (y / juliaH) * 2;
let zx = 0, zy = 0;
let iter = 0;
while (iter < maxIter) {
const x2 = zx * zx;
const y2 = zy * zy;
if (x2 + y2 > 4) break;
const newX = x2 - y2 + cRe;
const newY = 2 * zx * zy + cIm;
zx = newX;
zy = newY;
iter++;
}
const smooth = iter < maxIter
? smoothColor(iter, zx, zy, maxIter)
: 0;
const color = getJuliaColor(smooth, maxIter);
const rgb = colorToRGB(color);
pixels[y * juliaW + x] = (rgb[0] << 16) | (rgb[1] << 8) | rgb[2];
}
}
const juliaImageData = new ImageData(juliaW, juliaH);
juliaImageData.data.set(pixels);
juliaCtx.putImageData(juliaImageData, 0, 0);
}
function colorToRGB(colorStr) {
const match = colorStr.match(/rgb\((\d+),(\d+),(\d+)\)/);
if (!match) return [0, 0, 0];
return [parseInt(match[1]), parseInt(match[2]), parseInt(match[3])];
}
// ========================================================================
// Bookmark System
// ========================================================================
function saveBookmark(name) {
const bookmark = {
name: name,
center: { re: state.center.re, im: state.center.im },
zoom: state.zoom,
c: { re: state.c.re, im: state.c.im },
palette: state.palette,
maxIter: state.maxIter,
juliaMode: state.juliaMode,
};
try {
localStorage.setItem('fractal_bookmark_' + name, JSON.stringify(bookmark));
} catch (e) {}
}
function loadBookmark(name) {
try {
const data = localStorage.getItem('fractal_bookmark_' + name);
if (!data) return false;
const bm = JSON.parse(data);
state.center = { re: bm.center.re, im: bm.center.im };
state.zoom = bm.zoom;
state.c = { re: bm.c.re, im: bm.c.im };
state.palette = bm.palette;
state.maxIter = bm.maxIter;
state.juliaMode = bm.juliaMode;
return true;
} catch (e) { return false; }
}
function listBookmarks() {
const bookmarks = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && key.startsWith('fractal_bookmark_')) {
const name = key.replace('fractal_bookmark_', '');
try {
const data = JSON.parse(localStorage.getItem(key));
bookmarks.push({ name, ...data });
} catch (e) {}
}
}
return bookmarks;
}
// ========================================================================
// URL Hash State
// ========================================================================
function saveToURL() {
const data = {
re: state.center.re.toFixed(6),
im: state.center.im.toFixed(6),
scale: state.zoom.toExponential(1),
c: state.c.re.toFixed(4) + ',' + state.c.im.toFixed(4),
palette: state.palette,
maxIter: state.maxIter,
julia: state.juliaMode ? 1 : 0,
};
const hash = JSON.stringify(data);
history.replaceState(null, '', '#' + encodeURIComponent(hash));
}
function loadFromURL() {
const hash = window.location.hash.substring(1);
if (!hash) return false;
try {
const data = JSON.parse(decodeURIComponent(hash));
if (data.re) state.center.re = parseFloat(data.re);
if (data.im) state.center.im = parseFloat(data.im);
if (data.scale) state.zoom = parseFloat(data.scale);
if (data.c) {
const parts = data.c.split(',');
if (parts.length === 2) { state.c.re = parseFloat(parts[0]); state.c.im = parseFloat(parts[1]); }
}
if (data.palette !== undefined) state.palette = parseInt(data.palette);
if (data.maxIter !== undefined) state.maxIter = parseInt(data.maxIter);
if (data.julia === 1) state.juliaMode = true;
return true;
} catch (e) { return false; }
}
// ========================================================================
// Debug Interface
// ========================================================================
function iterationsAt(re, im, cRe, cIm, maxIter) {
let zx = 0, zy = 0;
let iter = 0;
while (iter < maxIter) {
const x2 = zx * zx;
const y2 = zy * zy;
if (x2 + y2 > 4) break;
const newX = x2 - y2 + cRe;
const newY = 2 * zx * zy + cIm;
zx = newX;
zy = newY;
iter++;
}
return iter;
}
function setView(view) {
state.center = view.center || { re: 0, im: 0 };
state.zoom = view.scale || 1;
state.maxIter = view.maxIter || 5000;
}
function updateDebug() {
const it00 = iterationsAt(0, 0, state.c.re, state.c.im, state.maxIter);
const itNeg10 = iterationsAt(-1, 0, state.c.re, state.c.im, state.maxIter);
const it20 = iterationsAt(2, 0, state.c.re, state.c.im, state.maxIter);
const it0250 = iterationsAt(0.25, 0, state.c.re, state.c.im, state.maxIter);
document.getElementById('debugIter00').textContent = it00;
document.getElementById('debugIter-10').textContent = itNeg10;
document.getElementById('debugIter20').textContent = it20;
document.getElementById('debugIter0250').textContent = it0250;
document.getElementById('debugSetView').textContent = JSON.stringify({ re: state.center.re, im: state.center.im, scale: state.zoom });
document.getElementById('debugC').textContent = '(' + state.c.re.toFixed(4) + ', ' + state.c.im.toFixed(4) + ')';
}
// ========================================================================
// UI Event Handlers
// ========================================================================
function toggleJuliaMode() {
state.juliaMode = !state.juliaMode;
const btn = document.getElementById('btnJulia');
btn.classList.toggle('active', state.juliaMode);
document.getElementById('juliaPreview').classList.toggle('hidden', !state.juliaMode);
if (!state.juliaMode) {
renderJulia();
}
updateHUD();
}
function resetView() {
state.center = { re: -0.5, im: 0 };
state.zoom = 1;
state.c = { re: -0.7, im: 0.27015 };
state.maxIter = 5000;
state.juliaMode = false;
cancelRender();
startProgressiveRender();
updateHUD();
showToast('View reset');
}
function undoZoom() {
if (state.zoom > 1) {
state.zoom /= Math.sqrt(CONFIG.ZOOM_SPEED);
} else {
state.zoom = 1;
}
cancelRender();
startProgressiveRender();
updateHUD();
}
function togglePause() {
showToast('Pause rendering (simulated)');
}
function saveBookmark() {
const name = 'view_' + Math.random().toString(36).substring(2, 8);
saveBookmark(name);
showToast('Bookmark saved: ' + name);
}
function loadBookmark() {
const bookmarks = listBookmarks();
if (bookmarks.length === 0) {
showToast('No bookmarks yet');
return;
}
const name = bookmarks[bookmarks.length - 1].name;
if (loadBookmark(name)) {
showToast('Bookmark loaded: ' + name);
cancelRender();
startProgressiveRender();
updateHUD();
}
}
function toggleJuliaFromMouse(e) {
// When hovering over Mandelbrot and Julia mode is active, update c
if (!state.juliaMode) return;
const rect = canvas.getBoundingClientRect();
const sx = (e.clientX - rect.left) / rect.width * canvas.width;
const sy = (e.clientY - rect.top) / rect.height * canvas.height;
const complex = screenToComplex(sx, sy);
state.c = { re: complex.re, im: complex.im };
document.getElementById('juliaCVal').textContent =
'(' + complex.re.toFixed(4) + ', ' + complex.im.toFixed(4) + ')';
}
// ========================================================================
// Event Listeners
// ========================================================================
canvas.addEventListener('mousedown', onMouseDown);
canvas.addEventListener('mousemove', onMouseMove);
canvas.addEventListener('mouseup', onMouseUp);
canvas.addEventListener('mouseleave', onMouseUp);
canvas.addEventListener('wheel', onWheel, { passive: false });
canvas.addEventListener('contextmenu', e => e.preventDefault());
document.addEventListener('keydown', onKeyDown);
// Palette selector
document.querySelectorAll('.palette-chip').forEach(chip => {
chip.addEventListener('click', () => {
document.querySelectorAll('.palette-chip').forEach(c => { c.classList.remove('active'); c.setAttribute('aria-checked', 'false'); });
chip.classList.add('active');
chip.setAttribute('aria-checked', 'true');
state.palette = parseInt(chip.dataset.palette);
document.getElementById('colorSlider').value = state.palette;
document.getElementById('colorVal').textContent = state.palette;
updateHUD();
});
});
document.getElementById('btnJulia').addEventListener('click', toggleJuliaMode);
document.getElementById('btnReset').addEventListener('click', resetView);
document.getElementById('btnPause').addEventListener('click', togglePause);
document.getElementById('juliaClose').addEventListener('click', () => {
state.juliaMode = false;
document.getElementById('juliaPreview').classList.add('hidden');
document.getElementById('btnJulia').classList.remove('active');
renderJulia();
});
document.getElementById('zoomSlider').addEventListener('input', e => {
const val = parseFloat(e.target.value);
state.zoom = Math.max(1e-12, Math.min(1e14, val));
document.getElementById('zoomVal').textContent = '×' + state.zoom.toExponential(2);
cancelRender();
startProgressiveRender();
updateHUD();
});
document.getElementById('iterSlider').addEventListener('input', e => {
state.maxIter = parseInt(e.target.value);
document.getElementById('iterVal').textContent = state.maxIter.toLocaleString();
cancelRender();
startProgressiveRender();
updateHUD();
});
document.getElementById('colorSlider').addEventListener('input', e => {
state.palette = parseInt(e.target.value);
document.getElementById('colorVal').textContent = state.palette;
updateHUD();
});
// Julia preview: when mouse moves over Mandelbrot, render Julia for hovered c
canvas.addEventListener('mousemove', e => {
if (!state.juliaMode) return;
toggleJuliaFromMouse(e);
});
// ========================================================================
// Initialization
// ========================================================================
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
juliaCanvas.width = 380;
juliaCanvas.height = window.innerHeight;
cancelRender();
startProgressiveRender();
}
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
// Load from URL hash if present
loadFromURL();
// Set initial view
state.center = { re: -0.5, im: 0 };
state.zoom = 1;
state.c = { re: -0.7, im: 0.27015 };
state.palette = 0;
state.maxIter = 5000;
state.juliaMode = false;
// Render initial frame
startProgressiveRender();
// Debug initial values
updateDebug();
// Expose debug interface
window.fractal = {
iterationsAt: iterationsAt,
setView: setView,
state: state,
saveBookmark: saveBookmark,
loadBookmark: loadBookmark,
listBookmarks: listBookmarks,
saveToURL: saveToURL,
loadFromURL: loadFromURL,
CONFIG: CONFIG,
};
// Also expose for direct probing
window.iterationsAt = computeIterations;
window.smoothColor = smoothColor;
window.getMaxIterForZoom = getMaxIterForZoom;
window.renderFrame = renderFrame;
window.renderJulia = renderJulia;
window.startProgressiveRender = startProgressiveRender;
// Periodic HUD update
setInterval(() => {
fpsFrames++;
const now = performance.now();
if (now - fpsLastTime >= 1000) {
fps = fpsFrames;
fpsFrames = 0;
fpsLastTime = now;
document.getElementById('hudFPS').textContent = fps;
}
}, 1000);
setInterval(() => {
updateHUD();
}, 250);
// Initialize HUD
updateHUD();
</script>
</body>
</html>
<!-- agent-meta {"model":"inclusionai/ling-3.0-tiny:free","provider":"openrouter","persona":"full","sessionId":"5bff9d9d-af13-42c1-be4a-73f68b922c5c","tokensIn":3286467,"tokensOut":107092,"tokensTotal":3393559,"cost":0,"turns":39,"toolCalls":38,"failedToolCalls":2,"timestamp":"2026-08-09T02:40:00.890Z"} -->