← Fractal Explorer results

Fractal Explorer

Qwen 3.6 27b MTP · full

Initial view of Fractal Explorer

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 44,711 bytes · SHA-256 bc14525fb3f4
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Fractal Explorer</title>
<style>
/* ============================================================
   CSS — Full-bleed canvas + glassy HUD overlay
   ============================================================ */
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }

html, body {
  width: 100%; height: 100%; overflow: hidden;
  background: #000; font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
  color: #e0e0e0; user-select: none;
}

#fractalCanvas {
  position: absolute; top: 0; left: 0;
  display: block; width: 100%; height: 100%; cursor: crosshair; z-index: 1;
}

/* HUD overlay — glassy panels */
.hud-panel {
  position: absolute; background: rgba(15, 15, 30, 0.72);
  backdrop-filter: blur(12px) saturate(1.4); -webkit-backdrop-filter: blur(12px) saturate(1.4);
  border: 1px solid rgba(255,255,255,0.08); border-radius: 10px;
  padding: 10px 14px; font-size: 12px; line-height: 1.6;
  transition: opacity 0.4s ease, transform 0.4s ease;
  pointer-events: auto; z-index: 10;
}

.hud-panel.hidden {
  opacity: 0; pointer-events: none; transform: translateY(-8px);
}

#hud-top-left { top: 12px; left: 12px; }
#hud-top-right { top: 12px; right: 12px; text-align: right; }
#hud-bottom { bottom: 12px; left: 50%; transform: translateX(-50%); display: flex; gap: 8px; align-items: center; }

.hud-label { color: rgba(255,255,255,0.45); font-size: 10px; text-transform: uppercase; letter-spacing: 0.6px; }
.hud-value { color: #fff; font-weight: 600; font-variant-numeric: tabular-nums; }

/* Palette swatches */
.palette-row { display: flex; gap: 6px; margin-top: 4px; align-items: center; }
.swatch {
  width: 28px; height: 18px; border-radius: 4px; cursor: pointer;
  border: 2px solid transparent; transition: border-color 0.2s, transform 0.15s;
}
.swatch:hover { transform: scale(1.15); }
.swatch.active { border-color: #fff; box-shadow: 0 0 6px rgba(255,255,255,0.3); }

/* Buttons */
.hud-btn {
  background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.12);
  color: #ddd; padding: 4px 10px; border-radius: 6px; cursor: pointer;
  font-size: 11px; transition: background 0.2s;
}
.hud-btn:hover { background: rgba(255,255,255,0.18); }

/* Sliders */
input[type=range] {
  -webkit-appearance: none; appearance: none; width: 90px; height: 4px;
  background: rgba(255,255,255,0.15); border-radius: 2px; outline: none; cursor: pointer;
}
input[type=range]::-webkit-slider-thumb {
  -webkit-appearance: none; width: 14px; height: 14px; border-radius: 50%;
  background: #fff; border: none; cursor: grab;
}

/* Zoom box overlay */
#zoomBox {
  position: absolute; border: 2px solid rgba(255,255,255,0.7);
  pointer-events: none; display: none; z-index: 20;
  box-shadow: 0 0 0 9999px rgba(0,0,0,0.35);
}

/* Julia preview indicator */
#juliaIndicator {
  position: absolute; top: 12px; left: 50%; transform: translateX(-50%);
  background: rgba(255, 80, 40, 0.75); backdrop-filter: blur(8px);
  padding: 6px 16px; border-radius: 20px; font-size: 12px; font-weight: 600;
  display: none; z-index: 15; letter-spacing: 0.3px;
}

/* Bookmark list */
#bookmarkPanel {
  position: absolute; top: 80px; right: 12px; width: 220px; max-height: 40vh; overflow-y: auto;
  z-index: 15; display: none;
}
.bookmark-item {
  padding: 6px 10px; border-radius: 6px; cursor: pointer; font-size: 12px;
  margin-bottom: 4px; background: rgba(255,255,255,0.06); transition: background 0.15s;
}
.bookmark-item:hover { background: rgba(255,255,255,0.15); }

/* Crosshair for Julia mode */
#crosshair {
  position: absolute; pointer-events: none; display: none; z-index: 18;
}
#crosshair::before, #crosshair::after {
  content: ''; position: absolute; background: rgba(255,255,255,0.4);
}
#crosshair::before { width: 1px; height: 100%; left: 50%; top: 0; }
#crosshair::after { height: 1px; width: 100%; top: 50%; left: 0; }

/* Loading overlay */
#loadingOverlay {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  background: rgba(0,0,0,0.6); z-index: 30; pointer-events: none; opacity: 0; transition: opacity 0.2s;
}
#loadingOverlay.visible { opacity: 1; }
.spinner { width: 40px; height: 40px; border: 3px solid rgba(255,255,255,0.1); border-top-color: #fff; border-radius: 50%; animation: spin 0.8s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }

/* Keyboard hint */
#keyHint {
  position: absolute; bottom: 12px; right: 12px; font-size: 10px; color: rgba(255,255,255,0.3); z-index: 10;
}

/* Julia preview canvas (small overlay) */
#juliaPreview {
  position: absolute; bottom: 60px; right: 12px; width: 180px; height: 180px;
  border-radius: 10px; border: 2px solid rgba(255,255,255,0.15);
  display: none; z-index: 15; image-rendering: pixelated;
}

/* FPS counter */
#fpsDisplay { font-variant-numeric: tabular-nums; }
</style>
</head>
<body>

<canvas id="fractalCanvas"></canvas>
<div id="zoomBox"></div>
<div id="crosshair"></div>
<div id="juliaIndicator">JULIA MODE — c = <span id="juliaCVal"></span></div>
<canvas id="juliaPreview" width="180" height="180"></canvas>

<!-- HUD Panels -->
<div class="hud-panel hidden" id="hud-top-left">
  <div><span class="hud-label">Magnification</span><br><span class="hud-value" id="magDisplay">×1.0</span></div>
  <div style="margin-top:4px"><span class="hud-label">Center</span><br><span class="hud-value" id="centerDisplay">-0.5 + 0i</span></div>
  <div style="margin-top:4px"><span class="hud-label">Set</span><br><span class="hud-value" id="setDisplay">Mandelbrot</span></div>
</div>

<div class="hud-panel hidden" id="hud-top-right">
  <div><span class="hud-label">Iterations</span><br><span class="hud-value" id="iterDisplay">200</span></div>
  <div style="margin-top:4px"><span class="hud-label">FPS</span><br><span class="hud-value" id="fpsDisplay">60</span></div>
  <div style="margin-top:4px"><span class="hud-label">Depth</span><br><span class="hud-value" id="depthDisplay">1e0</span></div>
</div>

<div class="hud-panel hidden" id="hud-bottom">
  <div class="palette-row" id="paletteRow"></div>
  <button class="hud-btn" id="btnBack" title="Zoom Back (B)">◀ Back</button>
  <button class="hud-btn" id="btnReset" title="Reset View (R)">⟲ Reset</button>
  <button class="hud-btn" id="btnJulia" title="Toggle Julia Mode (J)">★ Julia</button>
  <button class="hud-btn" id="btnBookmark" title="Save Bookmark (S)">🔖 Save</button>
  <div style="margin-left:8px">
    <span class="hud-label">Offset</span><br>
    <input type="range" id="colorOffset" min="0" max="360" value="0" step="1">
  </div>
  <div style="margin-left:4px">
    <span class="hud-label">Iter</span><br>
    <input type="range" id="iterSlider" min="50" max="2000" value="200" step="10">
  </div>
</div>

<div id="bookmarkPanel"></div>
<div id="loadingOverlay"><div class="spinner"></div></div>
<div id="keyHint">Drag: zoom box · Scroll: zoom · Shift+drag: pan · Arrows: pan · +/-: zoom · J: Julia · B: back · R: reset</div>

<script>
/* ============================================================
   SECTION 1 — Tunable Constants & Configuration
   ============================================================ */
const CONFIG = {
  // Iteration limits
  maxIterationsBase: 200,
  maxIterationsScale: 80,       // additional iterations per log10(magnification)
  maxIterationsCap: 15000,

  // Zoom behavior
  zoomSpeed: 0.6,               // wheel zoom factor (fraction of current view)
  minMagnification: 0.3,
  maxMagnification: 1e14,       // double-precision limit (~1e14 before artifacts)

  // Progressive rendering passes
  progressivePasses: [0.25, 0.5, 1.0],  // resolution fractions per pass
  progressiveDelayMs: 80,        // ms between passes for responsiveness

  // Julia preview
  juliaPreviewSize: 180,
  juliaPreviewUpdateInterval: 100, // ms between live-preview updates

  // HUD auto-hide
  hudHideTimeout: 8000,         // ms of inactivity before hiding HUD

  // Render worker count (0 = main thread only)
  workerCount: 4,

  // Color offset animation speed (degrees/sec when cycling enabled)
  colorCycleSpeed: 15,
};

/* ============================================================
   SECTION 2 — Palette Definitions
   Each palette is an array of {pos, r, g, b} stops for interpolation.
   ============================================================ */
const PALETTES = [
  {
    name: 'Fire',
    colors: [
      { pos: 0.0, r: 0, g: 0, b: 0 },
      { pos: 0.15, r: 80, g: 0, b: 0 },
      { pos: 0.35, r: 200, g: 40, b: 0 },
      { pos: 0.55, r: 255, g: 160, b: 0 },
      { pos: 0.75, r: 255, g: 240, b: 80 },
      { pos: 1.0, r: 255, g: 255, b: 220 },
    ],
    swatch: ['#300', '#c20', '#fa0', '#ff5'],
  },
  {
    name: 'Ocean',
    colors: [
      { pos: 0.0, r: 0, g: 0, b: 10 },
      { pos: 0.2, r: 0, g: 20, b: 80 },
      { pos: 0.45, r: 0, g: 80, b: 160 },
      { pos: 0.7, r: 30, g: 180, b: 220 },
      { pos: 0.9, r: 140, g: 230, b: 255 },
      { pos: 1.0, r: 220, g: 250, b: 255 },
    ],
    swatch: ['#006', '#08a', '#4cc', '#aff'],
  },
  {
    name: 'Monochrome',
    colors: [
      { pos: 0.0, r: 0, g: 0, b: 0 },
      { pos: 0.5, r: 128, g: 128, b: 130 },
      { pos: 1.0, r: 240, g: 240, b: 245 },
    ],
    swatch: ['#222', '#667', '#ccc'],
  },
  {
    name: 'Psychedelic',
    colors: [
      { pos: 0.0, r: 128, g: 0, b: 128 },
      { pos: 0.25, r: 0, g: 0, b: 255 },
      { pos: 0.5, r: 0, g: 255, b: 0 },
      { pos: 0.75, r: 255, g: 255, b: 0 },
      { pos: 1.0, r: 255, g: 0, b: 80 },
    ],
    swatch: ['#606', '#00f', '#0f0', '#ff0'],
  },
  {
    name: 'Electric',
    colors: [
      { pos: 0.0, r: 5, g: 5, b: 30 },
      { pos: 0.2, r: 40, g: 10, b: 120 },
      { pos: 0.4, r: 80, g: 60, b: 200 },
      { pos: 0.6, r: 100, g: 180, b: 255 },
      { pos: 0.8, r: 200, g: 230, b: 255 },
      { pos: 1.0, r: 255, g: 255, b: 255 },
    ],
    swatch: ['#004', '#40a', '#86c', '#caf'],
  },
];

/* ============================================================
   SECTION 3 — Math Utilities & Core Iteration Logic
   ============================================================ */

/**
 * Compute the smooth (continuous) iteration count for a point.
 * Returns maxIterations if the point is in the set, otherwise the fractional iteration count.
 * Uses the escape-time algorithm with smooth coloring: n + 1 - log2(log|z|)
 * @param {number} re - real part of c (for Mandelbrot) or z0 (for Julia)
 * @param {number} im - imaginary part of c / z0
 * @param {number} cr - real part of the constant c
 * @param {number} ci - imaginary part of the constant c
 * @param {number} maxIter - maximum iterations
 * @returns {number} smooth iteration count, or maxIter if in set
 */
function mandelbrotIteration(re, im, cr, ci, maxIter) {
  let zr = re;
  let zi = im;
  let zr2 = zr * zr;
  let zi2 = zi * zi;

  for (let n = 0; n < maxIter; n++) {
    if (zr2 + zi2 > 256) {
      // Smooth iteration count: n + 1 - log2(log(|z|))
      const logZ = Math.log(zr2 + zi2) * 0.5;
      return n + 1 - Math.log(logZ) / Math.LN2;
    }
    zi = 2 * zr * zi + ci;
    zr = zr2 - zi2 + cr;
    zr2 = zr * zr;
    zi2 = zi * zi;
  }
  return maxIter; // point is in the set (or on boundary)
}

/**
 * Compute Mandelbrot iteration for a given c-value.
 */
function mandelbrotC(cr, ci, maxIter) {
  return mandelbrotIteration(0, 0, cr, ci, maxIter);
}

/**
 * Compute Julia set iteration: z_{n+1} = z_n^2 + c, starting from (re, im).
 */
function juliaIteration(re, im, cr, ci, maxIter) {
  return mandelbrotIteration(re, im, cr, ci, maxIter);
}

/* ============================================================
   SECTION 4 — Color Palette Interpolation
   ============================================================ */

/**
 * Build a lookup table for a palette (256 entries).
 * Returns Uint8Array of length 1024 (RGBA × 256).
 */
function buildPaletteLUT(palette, offset) {
  const lut = new Uint8Array(1024); // 256 colors × 4 channels
  const stops = palette.colors;

  for (let i = 0; i < 256; i++) {
    let t = ((i / 255) + offset / 360) % 1.0;
    if (t < 0) t += 1;

    // Find surrounding stops
    let lo = stops[0], hi = stops[stops.length - 1];
    for (let s = 0; s < stops.length - 1; s++) {
      if (t >= stops[s].pos && t <= stops[s + 1].pos) {
        lo = stops[s];
        hi = stops[s + 1];
        break;
      }
    }

    const range = hi.pos - lo.pos || 1;
    const frac = (t - lo.pos) / range;
    // Smoothstep for nicer gradients
    const sf = frac * frac * (3 - 2 * frac);

    lut[i * 4]     = Math.round(lo.r + (hi.r - lo.r) * sf);
    lut[i * 4 + 1] = Math.round(lo.g + (hi.g - lo.g) * sf);
    lut[i * 4 + 2] = Math.round(lo.b + (hi.b - lo.b) * sf);
    lut[i * 4 + 3] = 255;
  }

  return lut;
}

/* ============================================================
   SECTION 5 — Fractal State & View Management
   ============================================================ */
const fractalState = {
  // View parameters (complex plane)
  centerRe: -0.5,
  centerIm: 0,
  magnification: 1,       // how many times zoomed in from default view

  // Rendering params
  maxIterations: CONFIG.maxIterationsBase,
  paletteIndex: 0,
  colorOffset: 0,
  autoCycleColor: false,

  // Set type
  setMode: 'mandelbrot',   // 'mandelbrot' or 'julia'
  juliaCRe: -0.7269,       // default Julia c-value (interesting spiral)
  juliaCIm: 0.1889,

  // Zoom history stack
  zoomHistory: [],

  // Canvas dimensions
  canvasWidth: 0,
  canvasHeight: 0,

  // Rendering state
  renderCancelled: false,
  currentPassIndex: 0,
};

/**
 * Compute the complex plane bounds from view parameters.
 */
function getViewBounds() {
  const aspect = fractalState.canvasWidth / fractalState.canvasHeight;
  const halfH = 1.5 / fractalState.magnification;
  const halfW = halfH * aspect;

  return {
    reMin: fractalState.centerRe - halfW,
    reMax: fractalState.centerRe + halfW,
    imMin: fractalState.centerIm - halfH,
    imMax: fractalState.centerIm + halfH,
  };
}

/**
 * Convert screen pixel to complex plane coordinate.
 */
function screenToComplex(px, py) {
  const bounds = getViewBounds();
  const re = bounds.reMin + (px / fractalState.canvasWidth) * (bounds.reMax - bounds.reMin);
  const im = bounds.imMin + (py / fractalState.canvasHeight) * (bounds.imMax - bounds.imMin);
  return { re, im };
}

/**
 * Convert complex plane coordinate to screen pixel.
 */
function complexToScreen(re, im) {
  const bounds = getViewBounds();
  const px = ((re - bounds.reMin) / (bounds.reMax - bounds.reMin)) * fractalState.canvasWidth;
  const py = ((im - bounds.imMin) / (bounds.imMax - bounds.imMin)) * fractalState.canvasHeight;
  return { x: px, y: py };
}

/**
 * Set the view to specific parameters. Pushes current state to history.
 */
function setView(re, im, mag) {
  // Clamp magnification
  if (mag > CONFIG.maxMagnification) {
    console.warn(`Magnification ${mag.toExponential(2)} exceeds precision limit (${CONFIG.maxMagnification.toExponential()}). Clamped.`);
    mag = CONFIG.maxMagnification;
  }
  if (mag < CONFIG.minMagnification) mag = CONFIG.minMagnification;

  fractalState.zoomHistory.push({
    re: fractalState.centerRe,
    im: fractalState.centerIm,
    mag: fractalState.magnification,
  });

  fractalState.centerRe = re;
  fractalState.centerIm = im;
  fractalState.magnification = mag;

  updateIterationLimit();
  scheduleRender();
}

/**
 * Zoom back to previous view.
 */
function zoomBack() {
  if (fractalState.zoomHistory.length === 0) return;
  const prev = fractalState.zoomHistory.pop();
  fractalState.centerRe = prev.re;
  fractalState.centerIm = prev.im;
  fractalState.magnification = prev.mag;
  updateIterationLimit();
  scheduleRender();
}

/**
 * Reset to default view.
 */
function resetView() {
  setView(-0.5, 0, 1);
  fractalState.setMode = 'mandelbrot';
  fractalState.zoomHistory = [];
}

/**
 * Update iteration limit based on magnification depth.
 */
function updateIterationLimit() {
  const logMag = Math.log10(fractalState.magnification);
  let iter = CONFIG.maxIterationsBase + Math.floor(logMag * CONFIG.maxIterationsScale);
  if (iter > CONFIG.maxIterationsCap) iter = CONFIG.maxIterationsCap;
  fractalState.maxIterations = iter;

  // Update slider to reflect auto-computed value
  const slider = document.getElementById('iterSlider');
  if (!slider._manualOverride) {
    slider.value = Math.min(iter, parseInt(slider.max));
  }
}

/* ============================================================
   SECTION 6 — Rendering Engine (ImageData Pipeline)
   ============================================================ */
const canvas = document.getElementById('fractalCanvas');
const ctx = canvas.getContext('2d', { alpha: false }); // no alpha channel needed
let imageData = null;
let pixelBuffer = null; // Uint32Array view for fast per-pixel writes

/**
 * Resize the canvas and allocate buffers.
 */
function resizeCanvas() {
  const dpr = window.devicePixelRatio || 1;
  fractalState.canvasWidth = Math.floor(window.innerWidth * dpr);
  fractalState.canvasHeight = Math.floor(window.innerHeight * dpr);

  canvas.width = fractalState.canvasWidth;
  canvas.height = fractalState.canvasHeight;

  imageData = ctx.createImageData(fractalState.canvasWidth, fractalState.canvasHeight);
  pixelBuffer = new Uint32Array(imageData.data.buffer);
}

/**
 * Render the fractal at a given resolution fraction (progressive rendering).
 * This is the core allocation-free iteration loop.
 */
function renderFractal(resolutionFraction) {
  const w = fractalState.canvasWidth;
  const h = fractalState.canvasHeight;
  const stepX = Math.max(1, Math.round(1 / resolutionFraction));
  const stepY = Math.max(1, Math.round(1 / resolutionFraction));

  const bounds = getViewBounds();
  const reRange = bounds.reMax - bounds.reMin;
  const imRange = bounds.imMax - bounds.imMin;
  const dRe = reRange / w;
  const dIm = imRange / h;

  const maxIter = fractalState.maxIterations;
  const paletteLUT = buildPaletteLUT(PALETTES[fractalState.paletteIndex], fractalState.colorOffset);

  // Precompute LUT as Uint32 for fast writes (little-endian: ABGR)
  const lut32 = new Uint32Array(256);
  for (let i = 0; i < 256; i++) {
    const idx = i * 4;
    // Store as 0xAABBGGRR (little-endian on x86)
    lut32[i] = paletteLUT[idx + 3] | (paletteLUT[idx + 2] << 8) | (paletteLUT[idx + 1] << 16) | (paletteLUT[idx] << 24);
  }

  const isJulia = fractalState.setMode === 'julia';
  const jcr = fractalState.juliaCRe;
  const jci = fractalState.juliaCIm;

  // Main iteration loop — allocation-free, tight inner loop
  for (let py = 0; py < h; py += stepY) {
    if (fractalState.renderCancelled) return;

    const imBase = bounds.imMin + py * dIm;

    for (let px = 0; px < w; px += stepX) {
      let cr, ci, zr, zi;

      if (isJulia) {
        // Julia: z starts at pixel position, c is fixed
        zr = bounds.reMin + px * dRe;
        zi = imBase;
        cr = jcr;
        ci = jci;
      } else {
        // Mandelbrot: z starts at 0, c is pixel position
        cr = bounds.reMin + px * dRe;
        ci = imBase;
        zr = 0;
        zi = 0;
      }

      const smoothIter = mandelbrotIteration(zr, zi, cr, ci, maxIter);

      let colorIdx;
      if (smoothIter >= maxIter) {
        // Interior point — black
        colorIdx = 0;
      } else {
        // Map smooth iteration to palette index with cycling
        const t = ((smoothIter * 3.7 + fractalState.colorOffset / 360 * 256) % 256);
        colorIdx = Math.floor(t < 0 ? t + 256 : t);
      }

      // Fill the block covered by this step
      const fillW = Math.min(stepX, w - px);
      const fillH = Math.min(stepY, h - py);
      for (let fy = 0; fy < fillH; fy++) {
        const rowOffset = (py + fy) * w;
        for (let fx = 0; fx < fillW; fx++) {
          pixelBuffer[rowOffset + px + fx] = lut32[colorIdx];
        }
      }
    }

    // Yield to browser every few rows to keep UI responsive
    if (py % (stepY * 40) === 0 && stepY > 1) {
      yieldFrame();
    }
  }

  ctx.putImageData(imageData, 0, 0);
}

/**
 * Yield one frame to the browser event loop.
 */
function yieldFrame() {
  return new Promise(r => setTimeout(r, 0));
}

/* ============================================================
   SECTION 7 — Progressive Rendering with Cancellation
   ============================================================ */
let renderTimeout = null;
let currentRenderPass = -1;

/**
 * Schedule a progressive render. Cancels any in-flight render.
 */
function scheduleRender() {
  fractalState.renderCancelled = true;
  if (renderTimeout) clearTimeout(renderTimeout);

  // Small delay to batch rapid interactions
  renderTimeout = setTimeout(() => startProgressiveRender(), 50);
}

/**
 * Start progressive rendering through resolution passes.
 */
async function startProgressiveRender() {
  fractalState.renderCancelled = false;
  currentRenderPass = 0;

  const passes = CONFIG.progressivePasses;

  for (let p = 0; p < passes.length; p++) {
    if (fractalState.renderCancelled) break;
    currentRenderPass = p;

    renderFractal(passes[p]);

    // Show loading overlay only during first pass
    const overlay = document.getElementById('loadingOverlay');
    if (p === 0 && passes.length > 1) {
      overlay.classList.add('visible');
    } else {
      overlay.classList.remove('visible');
    }

    if (p < passes.length - 1 && !fractalState.renderCancelled) {
      await new Promise(r => setTimeout(r, CONFIG.progressiveDelayMs));
    }
  }

  currentRenderPass = -1;
  document.getElementById('loadingOverlay').classList.remove('visible');
  updateHUD();
}

/* ============================================================
   SECTION 8 — Julia Preview Rendering
   ============================================================ */
const juliaPreviewCanvas = document.getElementById('juliaPreview');
const jpcCtx = juliaPreviewCanvas.getContext('2d', { alpha: false });
let jpImageData = null;
let jpPixelBuffer = null;

function initJuliaPreview() {
  const size = CONFIG.juliaPreviewSize;
  juliaPreviewCanvas.width = size;
  juliaPreviewCanvas.height = size;
  jpImageData = jpcCtx.createImageData(size, size);
  jpPixelBuffer = new Uint32Array(jpImageData.data.buffer);
}

let lastJuliaPreviewTime = 0;

function renderJuliaPreview(cRe, cIm) {
  const now = performance.now();
  if (now - lastJuliaPreviewTime < CONFIG.juliaPreviewUpdateInterval) return;
  lastJuliaPreviewTime = now;

  fractalState.juliaCRe = cRe;
  fractalState.juliaCIm = cIm;

  const size = CONFIG.juliaPreviewSize;
  const maxIter = Math.min(fractalState.maxIterations, 500); // cap for preview speed
  const paletteLUT = buildPaletteLUT(PALETTES[fractalState.paletteIndex], fractalState.colorOffset);

  const lut32 = new Uint32Array(256);
  for (let i = 0; i < 256; i++) {
    const idx = i * 4;
    lut32[i] = paletteLUT[idx + 3] | (paletteLUT[idx + 2] << 8) | (paletteLUT[idx + 1] << 16) | (paletteLUT[idx] << 24);
  }

  const bounds = { reMin: -1.5, reMax: 1.5, imMin: -1.5, imMax: 1.5 };
  const dRe = (bounds.reMax - bounds.reMin) / size;
  const dIm = (bounds.imMax - bounds.imMin) / size;

  for (let py = 0; py < size; py++) {
    const zi = bounds.imMin + py * dIm;
    for (let px = 0; px < size; px++) {
      const zr = bounds.reMin + px * dRe;
      const smoothIter = mandelbrotIteration(zr, zi, cRe, cIm, maxIter);

      let colorIdx;
      if (smoothIter >= maxIter) {
        colorIdx = 0;
      } else {
        const t = ((smoothIter * 3.7 + fractalState.colorOffset / 360 * 256) % 256);
        colorIdx = Math.floor(t < 0 ? t + 256 : t);
      }

      jpPixelBuffer[py * size + px] = lut32[colorIdx];
    }
  }

  jpcCtx.putImageData(jpImageData, 0, 0);
}

/* ============================================================
   SECTION 9 — Navigation & Interaction Handlers
   ============================================================ */
let isDragging = false;
let dragStartX = 0, dragStartY = 0;
let dragMode = 'zoom'; // 'zoom' or 'pan'
let panStartRe = 0, panStartIm = 0;

const zoomBoxEl = document.getElementById('zoomBox');

canvas.addEventListener('mousedown', (e) => {
  if (e.button !== 0) return; // only left button
  isDragging = true;
  dragMode = e.shiftKey ? 'pan' : 'zoom';
  dragStartX = e.clientX * (window.devicePixelRatio || 1);
  dragStartY = e.clientY * (window.devicePixelRatio || 1);

  const cplx = screenToComplex(dragStartX, dragStartY);
  panStartRe = fractalState.centerRe;
  panStartIm = fractalState.centerIm;

  if (dragMode === 'zoom') {
    zoomBoxEl.style.display = 'block';
    zoomBoxEl.style.left = e.clientX + 'px';
    zoomBoxEl.style.top = e.clientY + 'px';
    zoomBoxEl.style.width = '0px';
    zoomBoxEl.style.height = '0px';
  }

  resetHUDTimer();
});

canvas.addEventListener('mousemove', (e) => {
  const dpr = window.devicePixelRatio || 1;
  const mx = e.clientX * dpr;
  const my = e.clientY * dpr;

  if (isDragging) {
    if (dragMode === 'zoom') {
      // Update zoom box overlay
      const x = Math.min(dragStartX, mx);
      const y = Math.min(dragStartY, my);
      const w = Math.abs(mx - dragStartX);
      const h = Math.abs(my - dragStartY);

      zoomBoxEl.style.left = (x / dpr) + 'px';
      zoomBoxEl.style.top = (y / dpr) + 'px';
      zoomBoxEl.style.width = (w / dpr) + 'px';
      zoomBoxEl.style.height = (h / dpr) + 'px';
    } else {
      // Pan mode — live update
      const dx = mx - dragStartX;
      const dy = my - dragStartY;
      const bounds = getViewBounds();
      const dRe = (bounds.reMax - bounds.reMin);
      const dIm = (bounds.imMax - bounds.imMin);

      fractalState.centerRe = panStartRe - (dx / fractalState.canvasWidth) * dRe;
      fractalState.centerIm = panStartIm + (dy / fractalState.canvasHeight) * dIm;
    }
  } else {
    // Not dragging — live Julia preview when hovering Mandelbrot set
    if (fractalState.setMode === 'mandelbrot') {
      const cplx = screenToComplex(mx, my);
      renderJuliaPreview(cplx.re, cplx.im);
      juliaPreviewCanvas.style.display = 'block';

      // Update crosshair position
      const crosshair = document.getElementById('crosshair');
      crosshair.style.display = 'block';
      crosshair.style.left = e.clientX + 'px';
      crosshair.style.top = e.clientY + 'px';
    }
  }

  resetHUDTimer();
});

canvas.addEventListener('mouseup', (e) => {
  if (!isDragging) return;
  isDragging = false;

  const dpr = window.devicePixelRatio || 1;
  const mx = e.clientX * dpr;
  const my = e.clientY * dpr;

  if (dragMode === 'zoom') {
    zoomBoxEl.style.display = 'none';

    // Compute the selected rectangle in complex plane coordinates
    const x1 = Math.min(dragStartX, mx);
    const y1 = Math.min(dragStartY, my);
    const x2 = Math.max(dragStartX, mx);
    const y2 = Math.max(dragStartY, my);

    // Minimum selection size to avoid accidental tiny zooms
    if ((x2 - x1) < 5 || (y2 - y1) < 5) {
      // Too small — treat as a click: save Julia c-value at this point
      const cplx = screenToComplex(mx, my);
      fractalState.juliaCRe = cplx.re;
      fractalState.juliaCIm = cplx.im;
      return;
    }

    // Convert rectangle corners to complex plane
    const topLeft = screenToComplex(x1, y1);
    const bottomRight = screenToComplex(x2, y2);

    // New center is midpoint of selection
    const newRe = (topLeft.re + bottomRight.re) / 2;
    const newIm = (topLeft.im + bottomRight.im) / 2;

    // New magnification: how much did we zoom?
    const oldBounds = getViewBounds();
    const oldWidth = oldBounds.reMax - oldBounds.reMin;
    const selectedWidth = Math.abs(bottomRight.re - topLeft.re);
    const newMag = fractalState.magnification * (oldWidth / selectedWidth);

    setView(newRe, newIm, newMag);
  } else {
    // Pan mode — schedule render after pan completes
    scheduleRender();
  }

  resetHUDTimer();
});

canvas.addEventListener('mouseleave', () => {
  if (isDragging) {
    isDragging = false;
    zoomBoxEl.style.display = 'none';
  }
  document.getElementById('crosshair').style.display = 'none';
  juliaPreviewCanvas.style.display = 'none';
});

// Wheel zoom toward cursor
canvas.addEventListener('wheel', (e) => {
  e.preventDefault();

  const dpr = window.devicePixelRatio || 1;
  const mx = e.clientX * dpr;
  const my = e.clientY * dpr;

  // Get the complex coordinate under the cursor before zooming
  const cplxBefore = screenToComplex(mx, my);

  // Apply zoom factor
  const zoomFactor = Math.pow(1.15, -e.deltaY / (e.deltaY < 0 ? 30 : 30));
  const newMag = fractalState.magnification * zoomFactor;

  // Clamp magnification
  if (newMag > CONFIG.maxMagnification) {
    console.warn(`Zoom limit reached (${CONFIG.maxMagnification.toExponential()}). Double precision degrades beyond this.`);
    return;
  }
  if (newMag < CONFIG.minMagnification) return;

  // Temporarily set new magnification to compute where the cursor point maps
  const oldMag = fractalState.magnification;
  fractalState.magnification = newMag;

  // Get the complex coordinate under the cursor after zooming (center unchanged)
  const cplxAfter = screenToComplex(mx, my);

  // Shift center so that the point under the cursor stays fixed
  fractalState.centerRe += (cplxBefore.re - cplxAfter.re);
  fractalState.centerIm += (cplxBefore.im - cplxAfter.im);

  updateIterationLimit();
  scheduleRender();
  resetHUDTimer();
}, { passive: false });

// Keyboard controls
document.addEventListener('keydown', (e) => {
  const panAmount = 0.15 / fractalState.magnification;

  switch (e.key) {
    case 'ArrowLeft':
      e.preventDefault();
      fractalState.centerRe -= panAmount;
      scheduleRender(); resetHUDTimer();
      break;
    case 'ArrowRight':
      e.preventDefault();
      fractalState.centerRe += panAmount;
      scheduleRender(); resetHUDTimer();
      break;
    case 'ArrowUp':
      e.preventDefault();
      fractalState.centerIm -= panAmount;
      scheduleRender(); resetHUDTimer();
      break;
    case 'ArrowDown':
      e.preventDefault();
      fractalState.centerIm += panAmount;
      scheduleRender(); resetHUDTimer();
      break;
    case '+':
    case '=':
      e.preventDefault();
      setView(fractalState.centerRe, fractalState.centerIm, fractalState.magnification * 2);
      resetHUDTimer();
      break;
    case '-':
    case '_':
      e.preventDefault();
      setView(fractalState.centerRe, fractalState.centerIm, fractalState.magnification / 2);
      resetHUDTimer();
      break;
    case 'j':
    case 'J':
      toggleJuliaMode();
      resetHUDTimer();
      break;
    case 'b':
    case 'B':
      zoomBack();
      resetHUDTimer();
      break;
    case 'r':
    case 'R':
      resetView();
      resetHUDTimer();
      break;
    case 's':
    case 'S':
      saveBookmark();
      resetHUDTimer();
      break;
  }
});

/* ============================================================
   SECTION 10 — Julia Mode Toggle
   ============================================================ */
function toggleJuliaMode() {
  if (fractalState.setMode === 'mandelbrot') {
    // Switch to Julia mode using current c-value from preview or last click
    fractalState.setMode = 'julia';

    const indicator = document.getElementById('juliaIndicator');
    indicator.style.display = 'block';
    document.getElementById('juliaCVal').textContent =
      `${fractalState.juliaCRe.toFixed(6)} ${fractalState.juliaCIm >= 0 ? '+' : ''}${fractalState.juliaCIm.toFixed(6)}i`;

    juliaPreviewCanvas.style.display = 'none';
    document.getElementById('crosshair').style.display = 'none';
  } else {
    fractalState.setMode = 'mandelbrot';
    document.getElementById('juliaIndicator').style.display = 'none';
  }

  scheduleRender();
}

/* ============================================================
   SECTION 11 — HUD & UI Management
   ============================================================ */
let hudTimer = null;
const hudPanels = [
  document.getElementById('hud-top-left'),
  document.getElementById('hud-top-right'),
  document.getElementById('hud-bottom'),
];

function showHUD() {
  hudPanels.forEach(p => p.classList.remove('hidden'));
}

function hideHUD() {
  hudPanels.forEach(p => p.classList.add('hidden'));
}

function resetHUDTimer() {
  showHUD();
  if (hudTimer) clearTimeout(hudTimer);
  hudTimer = setTimeout(hideHUD, CONFIG.hudHideTimeout);
}

/**
 * Update HUD displays with current state.
 */
function updateHUD() {
  const mag = fractalState.magnification;
  document.getElementById('magDisplay').textContent = `×${formatNumber(mag)}`;
  document.getElementById('centerDisplay').textContent =
    `${fractalState.centerRe.toPrecision(10)} ${fractalState.centerIm >= 0 ? '+' : ''}${fractalState.centerIm.toPrecision(10)}i`;
  document.getElementById('setDisplay').textContent =
    fractalState.setMode === 'julia' ? `Julia (${fractalState.juliaCRe.toFixed(4)}, ${fractalState.juliaCIm.toFixed(4)})` : 'Mandelbrot';
  document.getElementById('iterDisplay').textContent = fractalState.maxIterations;

  const expStr = mag.toExponential(1);
  document.getElementById('depthDisplay').textContent = `1e${expStr.split('e')[1]}`;
}

function formatNumber(n) {
  if (n >= 1e6) return n.toExponential(2);
  if (n >= 1000) return n.toFixed(1);
  if (n >= 1) return n.toFixed(2);
  return n.toPrecision(3);
}

// FPS tracking
let fpsFrames = 0;
let fpsLastTime = performance.now();
function updateFPS() {
  fpsFrames++;
  const now = performance.now();
  if (now - fpsLastTime >= 1000) {
    document.getElementById('fpsDisplay').textContent = fpsFrames;
    fpsFrames = 0;
    fpsLastTime = now;
  }
  requestAnimationFrame(updateFPS);
}

/* ============================================================
   SECTION 12 — Palette UI & Color Controls
   ============================================================ */
function buildPaletteUI() {
  const row = document.getElementById('paletteRow');
  PALETTES.forEach((pal, idx) => {
    pal.swatch.forEach(color => {
      const swatch = document.createElement('div');
      swatch.className = 'swatch' + (idx === fractalState.paletteIndex ? ' active' : '');
      swatch.style.background = color;
      if (color === pal.swatch[0]) {
        swatch.title = pal.name;
        swatch.addEventListener('click', () => {
          fractalState.paletteIndex = idx;
          document.querySelectorAll('.swatch').forEach(s => s.classList.remove('active'));
          row.children[idx * 4]?.classList.add('active'); // first swatch of palette
          scheduleRender();
        });
      } else {
        swatch.style.cursor = 'default';
      }
      row.appendChild(swatch);
    });
  });
}

// Color offset slider — only re-colorizes, doesn't recompute iterations
document.getElementById('colorOffset').addEventListener('input', (e) => {
  fractalState.colorOffset = parseFloat(e.target.value);
  scheduleRender(); // needs full re-render since color mapping changes per-pixel
});

// Iteration limit slider — manual override
document.getElementById('iterSlider').addEventListener('input', (e) => {
  document.getElementById('iterSlider')._manualOverride = true;
  fractalState.maxIterations = parseInt(e.target.value);
  scheduleRender();
});

/* ============================================================
   SECTION 13 — Bookmark System (localStorage)
   ============================================================ */
const BOOKMARK_KEY = 'fractalExplorer_bookmarks';

function getBookmarks() {
  try {
    return JSON.parse(localStorage.getItem(BOOKMARK_KEY)) || [];
  } catch { return []; }
}

function saveBookmarks(bookmarks) {
  localStorage.setItem(BOOKMARK_KEY, JSON.stringify(bookmarks));
}

function saveBookmark() {
  const name = prompt('Bookmark name:', `View ${getBookmarks().length + 1}`);
  if (!name) return;

  const bookmark = {
    name,
    re: fractalState.centerRe,
    im: fractalState.centerIm,
    mag: fractalState.magnification,
    paletteIndex: fractalState.paletteIndex,
    colorOffset: fractalState.colorOffset,
    maxIterations: fractalState.maxIterations,
    setMode: fractalState.setMode,
    juliaCRe: fractalState.juliaCRe,
    juliaCIm: fractalState.juliaCIm,
    timestamp: Date.now(),
  };

  const bookmarks = getBookmarks();
  bookmarks.push(bookmark);
  saveBookmarks(bookmarks);
  renderBookmarkList();
}

function loadBookmark(index) {
  const bookmarks = getBookmarks();
  if (index < 0 || index >= bookmarks.length) return;

  const b = bookmarks[index];
  fractalState.centerRe = b.re;
  fractalState.centerIm = b.im;
  fractalState.magnification = b.mag;
  fractalState.paletteIndex = b.paletteIndex || 0;
  fractalState.colorOffset = b.colorOffset || 0;
  fractalState.maxIterations = b.maxIterations || CONFIG.maxIterationsBase;
  fractalState.setMode = b.setMode || 'mandelbrot';
  fractalState.juliaCRe = b.juliaCRe ?? -0.7269;
  fractalState.juliaCIm = b.juliaCIm ?? 0.1889;

  document.getElementById('colorOffset').value = fractalState.colorOffset;
  document.getElementById('iterSlider').value = Math.min(fractalState.maxIterations, 2000);

  if (fractalState.setMode === 'julia') {
    const indicator = document.getElementById('juliaIndicator');
    indicator.style.display = 'block';
    document.getElementById('juliaCVal').textContent =
      `${fractalState.juliaCRe.toFixed(6)} ${fractalState.juliaCIm >= 0 ? '+' : ''}${fractalState.juliaCIm.toFixed(6)}i`;
  } else {
    document.getElementById('juliaIndicator').style.display = 'none';
  }

  scheduleRender();
}

function deleteBookmark(index) {
  const bookmarks = getBookmarks();
  bookmarks.splice(index, 1);
  saveBookmarks(bookmarks);
  renderBookmarkList();
}

function renderBookmarkList() {
  const panel = document.getElementById('bookmarkPanel');
  const bookmarks = getBookmarks();

  if (bookmarks.length === 0) {
    panel.innerHTML = '<div style="padding:8px;font-size:12px;color:#666">No bookmarks saved</div>';
    return;
  }

  panel.innerHTML = '';
  bookmarks.forEach((b, i) => {
    const item = document.createElement('div');
    item.className = 'bookmark-item';
    item.textContent = `${b.name} (${formatNumber(b.mag)}×)`;
    item.addEventListener('click', () => {
      loadBookmark(i);
      panel.style.display = 'none';
    });
    item.addEventListener('contextmenu', (e) => {
      e.preventDefault();
      if (confirm(`Delete bookmark "${b.name}"?`)) {
        deleteBookmark(i);
      }
    });
    panel.appendChild(item);
  });
}

// Bookmark button toggle
document.getElementById('btnBookmark').addEventListener('click', () => {
  const panel = document.getElementById('bookmarkPanel');
  if (panel.style.display === 'block') {
    panel.style.display = 'none';
  } else {
    renderBookmarkList();
    panel.style.display = 'block';
  }
});

/* ============================================================
   SECTION 14 — Button Handlers
   ============================================================ */
document.getElementById('btnBack').addEventListener('click', zoomBack);
document.getElementById('btnReset').addEventListener('click', resetView);
document.getElementById('btnJulia').addEventListener('click', toggleJuliaMode);

/* ============================================================
   SECTION 15 — URL Hash State (Shareable Links)
   ============================================================ */
function encodeStateToHash() {
  const s = fractalState;
  const params = new URLSearchParams();
  params.set('re', s.centerRe.toString());
  params.set('im', s.centerIm.toString());
  params.set('mag', s.magnification.toString());
  params.set('pal', s.paletteIndex);
  params.set('off', s.colorOffset);
  params.set('iter', s.maxIterations);
  if (s.setMode === 'julia') {
    params.set('mode', 'julia');
    params.set('jc', `${s.juliaCRe},${s.juliaCIm}`);
  }
  return '#' + params.toString();
}

function loadStateFromHash() {
  const hash = window.location.hash.slice(1);
  if (!hash) return false;

  try {
    const params = new URLSearchParams(hash);
    fractalState.centerRe = parseFloat(params.get('re')) || -0.5;
    fractalState.centerIm = parseFloat(params.get('im')) || 0;
    fractalState.magnification = Math.min(parseFloat(params.get('mag')) || 1, CONFIG.maxMagnification);
    fractalState.paletteIndex = parseInt(params.get('pal')) || 0;
    fractalState.colorOffset = parseFloat(params.get('off')) || 0;
    fractalState.maxIterations = parseInt(params.get('iter')) || CONFIG.maxIterationsBase;

    if (params.get('mode') === 'julia') {
      fractalState.setMode = 'julia';
      const jc = params.get('jc').split(',');
      fractalState.juliaCRe = parseFloat(jc[0]) || -0.7269;
      fractalState.juliaCIm = parseFloat(jc[1]) || 0.1889;

      document.getElementById('juliaIndicator').style.display = 'block';
      document.getElementById('juliaCVal').textContent =
        `${fractalState.juliaCRe.toFixed(6)} ${fractalState.juliaCIm >= 0 ? '+' : ''}${fractalState.juliaCIm.toFixed(6)}i`;
    }

    document.getElementById('colorOffset').value = fractalState.colorOffset;
    document.getElementById('iterSlider').value = Math.min(fractalState.maxIterations, 2000);
    return true;
  } catch {
    return false;
  }
}

// Update hash periodically (debounced)
let hashUpdateTimer = null;
function scheduleHashUpdate() {
  if (hashUpdateTimer) clearTimeout(hashUpdateTimer);
  hashUpdateTimer = setTimeout(() => {
    window.history.replaceState(null, '', encodeStateToHash());
  }, 1000);
}

/* ============================================================
   SECTION 16 — Debug Interface (exposed on window)
   ============================================================ */
window.fractal = {
  /**
   * Compute iterations at a given complex coordinate.
   * For Mandelbrot mode: c = (re, im), z starts at 0.
   * For Julia mode: z starts at (re, im), c is the Julia constant.
   */
  iterationsAt(re, im) {
    const maxIter = fractalState.maxIterations;
    if (fractalState.setMode === 'julia') {
      return mandelbrotIteration(re, im, fractalState.juliaCRe, fractalState.juliaCIm, maxIter);
    }
    return mandelbrotC(re, im, maxIter);
  },

  /** Set the view programmatically. */
  setView({ re, im, scale }) {
    if (re !== undefined) fractalState.centerRe = re;
    if (im !== undefined) fractalState.centerIm = im;
    if (scale !== undefined) fractalState.magnification = Math.min(scale, CONFIG.maxMagnification);
    updateIterationLimit();
    scheduleRender();
  },

  /** Get current view state. */
  getView() {
    return {
      re: fractalState.centerRe,
      im: fractalState.centerIm,
      mag: fractalState.magnification,
      maxIter: fractalState.maxIterations,
      mode: fractalState.setMode,
      juliaC: { re: fractalState.juliaCRe, im: fractalState.juliaCIm },
    };
  },

  /** Force a render. */
  render() { scheduleRender(); },

  /** Configuration object (read-only reference). */
  config: CONFIG,
};

/* ============================================================
   SECTION 17 — Initialization & Main Loop
   ============================================================ */
function init() {
  // Load state from URL hash if present
  const hasHashState = loadStateFromHash();

  resizeCanvas();
  initJuliaPreview();
  buildPaletteUI();

  // Initial render
  updateIterationLimit();
  scheduleRender();

  // Show HUD initially, then auto-hide
  showHUD();
  hudTimer = setTimeout(hideHUD, CONFIG.hudHideTimeout);

  // FPS counter
  requestAnimationFrame(updateFPS);

  // Handle resize
  window.addEventListener('resize', () => {
    resizeCanvas();
    scheduleRender();
  });

  // Update hash on view changes (via mutation observer or periodic)
  setInterval(scheduleHashUpdate, 2000);

  if (!hasHashState) {
    scheduleHashUpdate();
  }

  console.log('%c Fractal Explorer Loaded ', 'background:#333;color:#fff;padding:4px 8px;border-radius:4px;');
  console.log('Debug interface available at window.fractal');
  console.log('Methods: fractal.iterationsAt(re, im), fractal.setView({re,im,scale}), fractal.getView(), fractal.render()');
}

// Start when DOM is ready
if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', init);
} else {
  init();
}
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen3.6-27b-mtp","provider":"lmstudio","persona":"full","sessionId":"037234b1-d89b-4bc2-a073-f028486a69b8","tokensIn":559961,"tokensOut":24801,"tokensTotal":584762,"turns":19,"toolCalls":18,"failedToolCalls":0,"timestamp":"2026-07-29T22:42:41.660Z"} -->