← Fractal Explorer results

Fractal Explorer

ling 3.0 tiny:free · full

Initial view of Fractal Explorer

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"} -->