Fractal Explorer
ox alpha · full

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 31,252 bytes · SHA-256 409be53e6e28
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Fractal Explorer — Mandelbrot & Julia</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
/* ============================================================
STYLES
============================================================ */
:root{
--glass: rgba(16,18,24,.72);
--glass-brd: rgba(255,255,255,.14);
--txt: #e8ecf4;
--dim: #9aa3b5;
--accent: #6fb7ff;
}
*{box-sizing:border-box; margin:0; padding:0}
html,body{width:100%;height:100%;overflow:hidden;background:#000;
font:13px/1.45 "Segoe UI",system-ui,-apple-system,sans-serif;color:var(--txt)}
#stage{position:fixed;inset:0}
canvas#view,canvas#overlay{position:absolute;inset:0;width:100%;height:100%;display:block}
canvas#view{cursor:crosshair}
#overlay{pointer-events:none}
/* --- HUD --- */
.hud{
position:fixed; background:var(--glass); border:1px solid var(--glass-brd);
border-radius:10px; padding:8px 12px; backdrop-filter:blur(10px);
-webkit-backdrop-filter:blur(10px); transition:opacity .5s ease;
box-shadow:0 4px 24px rgba(0,0,0,.45); user-select:none;
}
body.idle .hud{opacity:0; pointer-events:none}
#hudInfo{top:12px;left:12px; font-variant-numeric:tabular-nums; min-width:230px}
#hudInfo .row{display:flex;justify-content:space-between;gap:14px}
#hudInfo .k{color:var(--dim)}
#hudInfo .v{color:var(--txt)}
#hudInfo .mag{color:var(--accent);font-weight:600}
#hudCtrl{top:12px;right:12px; width:236px; display:flex;flex-direction:column;gap:8px}
.hud h1{font-size:12px;letter-spacing:.14em;text-transform:uppercase;color:var(--dim);font-weight:600}
.ctl{display:flex;flex-direction:column;gap:3px}
.ctl label{color:var(--dim);font-size:11px;display:flex;justify-content:space-between}
.ctl label output{color:var(--txt)}
input[type=range]{width:100%;accent-color:var(--accent);height:18px}
.chips{display:flex;flex-wrap:wrap;gap:6px}
.chip{width:34px;height:20px;border-radius:5px;border:2px solid transparent;cursor:pointer;padding:0}
.chip.sel{border-color:#fff;box-shadow:0 0 6px rgba(255,255,255,.6)}
.btnrow{display:flex;gap:6px;flex-wrap:wrap}
button{
background:rgba(255,255,255,.08);color:var(--txt);border:1px solid var(--glass-brd);
border-radius:6px;padding:4px 9px;cursor:pointer;font:inherit;font-size:12px
}
button:hover{background:rgba(255,255,255,.18)}
button.primary{background:rgba(111,183,255,.25);border-color:rgba(111,183,255,.5)}
input[type=text]{
background:rgba(0,0,0,.4);border:1px solid var(--glass-brd);color:var(--txt);
border-radius:6px;padding:4px 8px;font:inherit;width:100%
}
#bmList{max-height:110px;overflow-y:auto;display:flex;flex-direction:column;gap:3px}
#bmList .bm{display:flex;align-items:center;gap:6px}
#bmList .bm button{flex:1;text-align:left;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
#bmList .bm .del{flex:0 0 auto;padding:4px 7px;color:#ff9a9a}
#bmList .empty{color:var(--dim);font-size:11px}
/* --- Julia preview pane --- */
#previewPane{
right:12px;bottom:12px;padding:6px;display:flex;flex-direction:column;gap:4px
}
#previewPane .cap{font-size:11px;color:var(--dim);text-align:center;font-variant-numeric:tabular-nums}
#preview{border-radius:6px;display:block;cursor:default}
/* --- toast / help --- */
#toast{
left:50%;bottom:26px;transform:translateX(-50%);padding:8px 18px;
opacity:0;transition:opacity .35s;pointer-events:none;white-space:nowrap
}
#toast.show{opacity:1}
#help{
left:50%;top:50%;transform:translate(-50%,-50%);max-width:430px;line-height:1.7;display:none
}
#help.show{display:block}
#help kbd{
background:rgba(255,255,255,.12);border:1px solid var(--glass-brd);border-radius:4px;
padding:0 5px;font:11px ui-monospace,monospace
}
#setBadge{
position:fixed;left:50%;top:12px;transform:translateX(-50%);
font-size:11px;letter-spacing:.2em;text-transform:uppercase;color:var(--dim)
}
#setBadge b{color:var(--accent)}
</style>
</head>
<body>
<div id="stage">
<canvas id="view"></canvas>
<canvas id="overlay"></canvas>
</div>
<div id="setBadge">Set: <b id="setName">Mandelbrot</b> · <span id="juliaC"></span></div>
<div class="hud" id="hudInfo">
<div class="row"><span class="k">Magnification</span><span class="v mag" id="hudMag">×1</span></div>
<div class="row"><span class="k">Center</span><span class="v" id="hudCenter">—</span></div>
<div class="row"><span class="k">Iterations</span><span class="v" id="hudIter">—</span></div>
<div class="row"><span class="k">Render</span><span class="v" id="hudFps">—</span></div>
</div>
<div class="hud" id="hudCtrl">
<h1>Fractal Explorer</h1>
<div class="ctl">
<label>Palette</label>
<div class="chips" id="paletteChips"></div>
</div>
<div class="ctl">
<label>Max iterations <output id="iterOut">auto</output></label>
<input type="range" id="iterSlider" min="0" max="5000" step="50" value="0">
</div>
<div class="ctl">
<label>Color cycle <output id="cycleOut">0</output></label>
<input type="range" id="cycleSlider" min="0" max="4096" step="1" value="0">
</div>
<div class="ctl">
<label>Color density <output id="densOut">3.0%</output></label>
<input type="range" id="densSlider" min="5" max="120" step="1" value="30">
</div>
<div class="btnrow">
<button id="btnBack" title="Back (B)">◀ Back</button>
<button id="btnReset" title="Reset (R)">Reset</button>
<button id="btnJulia" title="Swap Mandelbrot/Julia (J)">Julia</button>
<button id="btnHelp" title="Help (H)">?</button>
</div>
<h1 style="margin-top:4px">Bookmarks</h1>
<div class="btnrow">
<input type="text" id="bmName" placeholder="view name…">
<button id="bmSave" class="primary">Save</button>
</div>
<div id="bmList"><div class="empty">No bookmarks yet</div></div>
</div>
<div class="hud" id="previewPane">
<canvas id="preview" width="220" height="165"></canvas>
<div class="cap" id="previewCap">Julia preview</div>
</div>
<div class="hud" id="toast"></div>
<div class="hud" id="help">
<h1 style="margin-bottom:6px">Controls</h1>
<kbd>drag</kbd> rectangle zoom <kbd>Shift</kbd>+<kbd>drag</kbd> / middle-drag pan<br>
<kbd>wheel</kbd> zoom to cursor <kbd>+</kbd>/<kbd>−</kbd> zoom <kbd>arrows</kbd> pan<br>
<kbd>J</kbd> swap Mandelbrot ↔ Julia (c = hover point) <kbd>B</kbd> back <kbd>R</kbd> reset<br>
<kbd>H</kbd> toggle this help. Hover the main view to live-preview the Julia set for that c.<br>
Double precision holds to ~×1e13; deeper zooms are blocked (precision limit).
</div>
<script>
"use strict";
/* ============================================================
1. TUNABLE CONSTANTS
============================================================ */
const CFG = {
baseIter: 250, // iteration budget at magnification ×1
iterPerDecade: 130, // extra iterations per decade of zoom
iterCap: 20000, // hard ceiling for auto iterations
zoomSpeed: 1.25, // per wheel notch zoom factor
panKeyFrac: 0.12, // arrow-key pan as fraction of view
maxMag: 1e13, // double-precision limit; block beyond
passes: [4,2,1], // progressive refinement steps (grid stride)
sliceMs: 12, // target time budget per render slice
bailout2: 65536, // escape radius² (R=256 → very smooth mu)
colorLutSize: 4096,
previewW: 220,
previewH: 165,
previewIter: 400,
idleHideMs: 3200, // HUD auto-hide delay
initial: { set:'mandel', re:-0.743643887037151, im:0.131825904205330, scale:3.0 }
};
/* Palettes: cyclic stops [pos, #hex] */
const PALETTES = [
{ name:'Ultra', stops:[[0,'#000764'],[0.16,'#206bcb'],[0.42,'#edffff'],[0.6425,'#ffaa00'],[0.8575,'#310230'],[1,'#000764']] },
{ name:'Fire', stops:[[0,'#000000'],[0.2,'#7f0000'],[0.45,'#ff4400'],[0.7,'#ffcc00'],[0.85,'#ffffff'],[1,'#000000']] },
{ name:'Ocean', stops:[[0,'#000010'],[0.25,'#00337f'],[0.5,'#0099dd'],[0.75,'#aaffff'],[0.9,'#ffffff'],[1,'#000010']] },
{ name:'Mono', stops:[[0,'#000000'],[0.5,'#ffffff'],[1,'#000000']] },
{ name:'Psychedelic', stops:[[0,'#ff0000'],[0.2,'#ffff00'],[0.4,'#00ff00'],[0.6,'#00ffff'],[0.8,'#0000ff'],[1,'#ff00ff']] }
];
/* ============================================================
2. STATE
============================================================ */
const view = {
set: CFG.initial.set, // 'mandel' | 'julia'
re: CFG.initial.re, // center
im: CFG.initial.im,
scale:CFG.initial.scale, // complex units spanning canvas HEIGHT
jre: -0.8, jim: 0.156, // julia c
pal: 0, // palette index
iterOverride: 0, // 0 = auto
cycle: 0, // palette cycle offset (LUT entries)
density: 30 // color density (percent ×10 → 3.0%)
};
const history = []; // zoom/pan history stack for Back
let W=0, H=0, dpr=1;
let renderToken = 0, renderScheduled = false;
let muBuf = null; // full-res Float32 mu buffer (last completed full pass)
let img = null, imgData = null, imgU32 = null;
const LUT_N = CFG.colorLutSize;
const lut = new Uint8Array(LUT_N*3);
const INV_LN2 = 1/Math.LN2;
const B2 = CFG.bailout2;
/* DOM */
const cv = document.getElementById('view');
const ov = document.getElementById('overlay');
const pv = document.getElementById('preview');
const ctx = cv.getContext('2d');
const octx = ov.getContext('2d');
const pctx = pv.getContext('2d');
const $ = id => document.getElementById(id);
/* ============================================================
3. COLOR — palette LUT (cyclic, smooth multi-stop interpolation)
============================================================ */
function hex2rgb(h){ return [parseInt(h.slice(1,3),16), parseInt(h.slice(3,5),16), parseInt(h.slice(5,7),16)]; }
function buildLUT(){
const stops = PALETTES[view.pal].stops;
for(let i=0;i<LUT_N;i++){
const t = i/LUT_N;
let a = stops[stops.length-1], b = stops[0];
for(let s=0;s<stops.length-1;s++){
if(t >= stops[s][0] && t <= stops[s+1][0]){ a=stops[s]; b=stops[s+1]; break; }
}
const f = (b[0]===a[0]) ? 0 : (t-a[0])/(b[0]-a[0]);
const ca = hex2rgb(a[1]), cb = hex2rgb(b[1]);
lut[i*3 ] = ca[0]+(cb[0]-ca[0])*f;
lut[i*3+1] = ca[1]+(cb[1]-ca[1])*f;
lut[i*3+2] = ca[2]+(cb[2]-ca[2])*f;
}
}
/* ============================================================
4. MATH — escape-time kernel (allocation-free)
Returns smooth mu (>0) or -1 for interior.
============================================================ */
function iterate(ax, ay, maxIter, julia, cre, cim){
let zr, zi, cx, cy;
if(julia){ zr = ax; zi = ay; cx = cre; cy = cim; }
else { zr = 0; zi = 0; cx = ax; cy = ay; }
let zr2 = zr*zr, zi2 = zi*zi, n = 0;
if(!julia){
/* cardioid / period-2 bulb shortcut: guaranteed interior */
const y1 = cx-0.25, q = y1*y1 + cy*cy;
if(q*(q+y1) <= 0.25*cy*cy) return -1;
const xp = cx+1;
if(xp*xp + cy*cy <= 0.0625) return -1;
}
while(n < maxIter && zr2+zi2 <= B2){
zi = 2*zr*zi + cy;
zr = zr2 - zi2 + cx;
zr2 = zr*zr; zi2 = zi*zi;
n++;
}
if(n >= maxIter) return -1;
/* smooth coloring: n + 1 - log2(log|z|) */
return n + 1 - Math.log(0.5*Math.log(zr2+zi2))*INV_LN2;
}
function mag(){ return 3.0/view.scale; }
function autoIter(){ return Math.min(CFG.iterCap, Math.round(CFG.baseIter + CFG.iterPerDecade*Math.log10(Math.max(1,mag())))); }
function effIter(){ return view.iterOverride > 0 ? view.iterOverride : autoIter(); }
function pxSize(){ return view.scale/H; } // complex units per pixel
function c2px(re,im){ return [ (re-view.re)/pxSize() + W/2, (im-view.im)/pxSize() + H/2 ]; }
function px2c(x,y){ const p = pxSize(); return [ view.re + (x-W/2)*p, view.im + (y-H/2)*p ]; }
/* ============================================================
5. RENDER ENGINE — progressive passes into ImageData
Each pass computes a strided grid of mu values, then
colorizes full-res rows from the nearest grid point.
Sliced across macrotasks; cancelled by token on view change.
============================================================ */
function resize(){
dpr = Math.min(window.devicePixelRatio||1, 2);
W = Math.floor(innerWidth*dpr); H = Math.floor(innerHeight*dpr);
cv.width = W; cv.height = H;
ov.width = W; ov.height = H;
muBuf = new Float32Array(W*H);
imgData = ctx.createImageData(W,H);
imgU32 = new Uint32Array(imgData.data.buffer);
requestRender();
}
function requestRender(){
renderToken++;
if(!renderScheduled){ renderScheduled = true; setTimeout(renderLoop, 0); }
}
function renderLoop(){
renderScheduled = false;
const token = renderToken;
const maxIter = effIter();
const julia = view.set === 'julia';
const jre = view.jre, jim = view.jim;
const passes = CFG.passes;
let pi = 0, gy = 0, dirty = false;
const t0 = performance.now();
function slice(){
if(token !== renderToken) return; // cancelled — view changed
const budgetEnd = performance.now() + CFG.sliceMs;
while(performance.now() < budgetEnd){
if(pi >= passes.length){ // all passes done
if(dirty) ctx.putImageData(imgData, 0, 0);
lastFullPassDone = true;
hudRender(performance.now()-t0);
return;
}
const s = passes[pi];
const gw = Math.ceil(W/s), gh = Math.ceil(H/s);
if(gy >= gh){ pi++; gy = 0; continue; }
/* compute one grid row of the current pass */
const row = gy*s;
const p = pxSize();
const im0 = view.im + (row - H/2)*p;
const re0 = view.re - (W/2)*p;
const base = gy*gw;
for(let gx=0; gx<gw; gx++){
const re = re0 + gx*s*p;
muBuf[base+gx] = julia ? iterate(re, im0, maxIter, true, jre, jim)
: iterate(re, im0, maxIter, false, 0, 0);
}
/* colorize the full-res rows this grid row covers */
colorizeRows(row, Math.min(row+s, H), s, gw);
dirty = true; gy++;
}
if(dirty) ctx.putImageData(imgData, 0, 0); // partial blit — always something on screen
setTimeout(slice, 0);
}
slice();
}
/* colorize full-res rows [y0,y1) from strided grid */
function colorizeRows(y0, y1, s, gw){
const d = imgData.data, dens = view.density/1000;
const cyc = view.cycle;
for(let y=y0; y<y1; y++){
const grow = (y/s|0)*gw;
const di = y*W*4;
for(let x=0; x<W; x++){
const mu = muBuf[grow + (x/s|0)];
const o = di + x*4;
if(mu < 0){ d[o]=0; d[o+1]=0; d[o+2]=0; }
else{
let idx = ((mu*dens + cyc/LUT_N) * LUT_N) | 0;
idx = ((idx % LUT_N) + LUT_N) % LUT_N;
const li = idx*3;
d[o]=lut[li]; d[o+1]=lut[li+1]; d[o+2]=lut[li+2];
}
d[o+3]=255;
}
}
}
/* ============================================================
6. NAVIGATION — zoom, pan, rect-select, history
============================================================ */
function pushHistory(){
history.push({ set:view.set, re:view.re, im:view.im, scale:view.scale, jre:view.jre, jim:view.jim });
if(history.length > 100) history.shift();
}
function goBack(){
const s = history.pop();
if(!s){ toast('No history'); return; }
Object.assign(view, s);
onViewChanged();
}
function clampDepth(){
if(mag() > CFG.maxMag){
view.scale = 3.0/CFG.maxMag;
toast('Precision limit (×1e13) — zoom blocked');
}
}
/* zoom keeping complex point (re,im) fixed at pixel (x,y) */
function zoomAt(x, y, factor){
pushHistory();
const [cre, cim] = px2c(x, y);
view.scale /= factor;
clampDepth();
const p = pxSize();
view.re = cre - (x - W/2)*p;
view.im = cim - (y - H/2)*p;
onViewChanged();
}
/* zoom so the canvas-px rect fits the view (aspect handled by expansion) */
function zoomToRect(x0, y0, x1, y1){
if(Math.abs(x1-x0) < 8 || Math.abs(y1-y0) < 8) return;
pushHistory();
const p = pxSize();
const rcx = (x0+x1)/2, rcy = (y0+y1)/2;
const cre = view.re + (rcx - W/2)*p, cim = view.im + (rcy - H/2)*p;
const wC = Math.abs(x1-x0)*p, hC = Math.abs(y1-y0)*p;
view.re = cre; view.im = cim;
view.scale = Math.max(wC/W, hC/H); // expand to canvas aspect — rect fully contained & centered
clampDepth();
onViewChanged();
}
function panBy(dxPx, dyPx){
const p = pxSize();
view.re -= dxPx*p; view.im -= dyPx*p;
onViewChanged();
}
function resetView(){
pushHistory();
Object.assign(view, { set:'mandel', re:CFG.initial.re, im:CFG.initial.im, scale:CFG.initial.scale });
onViewChanged();
}
function onViewChanged(){
renderToken++; // cancel in-flight render immediately
requestRender();
updateHUD();
updateHash();
wake();
}
/* ============================================================
7. POINTER INTERACTION
left-drag = rect zoom · shift/middle-drag = pan · wheel = zoom
============================================================ */
let drag = null; // {x0,y0,x1,y1,mode:'rect'|'pan',button,shift}
cv.addEventListener('mousedown', e => {
if(e.target !== cv) return;
drag = { x0:e.clientX, y0:e.clientY, x1:e.clientX, y1:e.clientY,
mode:(e.shiftKey || e.button===1) ? 'pan' : 'rect', moved:false };
if(drag.mode==='pan') cv.style.cursor='grabbing';
e.preventDefault();
});
window.addEventListener('mousemove', e => {
hoverMove(e.clientX, e.clientY);
if(!drag) return;
drag.x1 = e.clientX; drag.y1 = e.clientY;
const dx = drag.x1-drag.x0, dy = drag.y1-drag.y0;
if(Math.abs(dx)>3 || Math.abs(dy)>3) drag.moved = true;
if(drag.mode==='pan'){
panBy(-(e.movementX)*dpr, -(e.movementY)*dpr);
drag.x0 = e.clientX; drag.y0 = e.clientY;
} else {
drawOverlay();
}
});
window.addEventListener('mouseup', e => {
if(!drag) return;
const d = drag; drag = null;
cv.style.cursor='crosshair';
if(d.mode==='rect' && d.moved){
zoomToRect(Math.min(d.x0,d.x1)*dpr, Math.min(d.y0,d.y1)*dpr,
Math.max(d.x0,d.x1)*dpr, Math.max(d.y0,d.y1)*dpr);
} else if(d.mode==='rect' && !d.moved){
/* plain click: pick Julia c from clicked point */
const [re,im] = px2c(e.clientX*dpr, e.clientY*dpr);
view.jre = re; view.jim = im;
renderPreview(re, im);
toast('Julia c = '+fmt(re)+', '+fmt(im)+' — press J to open');
}
drawOverlay();
});
cv.addEventListener('wheel', e => {
e.preventDefault();
const f = Math.pow(CFG.zoomSpeed, -Math.sign(e.deltaY) * Math.min(3, Math.abs(e.deltaY)/100));
zoomAt(e.clientX*dpr, e.clientY*dpr, f);
}, { passive:false });
cv.addEventListener('contextmenu', e => e.preventDefault());
cv.addEventListener('dblclick', e => zoomAt(e.clientX*dpr, e.clientY*dpr, 4));
/* overlay: zoom rect + julia reticle */
let hover = null; // css px
function hoverMove(x, y){
hover = { x, y };
schedulePreview();
drawOverlay();
wake();
}
function drawOverlay(){
octx.clearRect(0,0,W,H);
if(drag && drag.mode==='rect' && drag.moved){
const x = Math.min(drag.x0,drag.x1)*dpr, y = Math.min(drag.y0,drag.y1)*dpr;
const w = Math.abs(drag.x1-drag.x0)*dpr, h = Math.abs(drag.y1-drag.y0)*dpr;
octx.save();
octx.globalCompositeOperation = 'difference';
octx.strokeStyle = '#fff'; octx.lineWidth = 2*dpr;
octx.strokeRect(x, y, w, h);
octx.fillStyle = 'rgba(255,255,255,0.08)';
octx.fillRect(x, y, w, h);
octx.restore();
}
if(hover && !drag){
octx.save();
octx.globalCompositeOperation = 'difference';
octx.strokeStyle = '#fff'; octx.lineWidth = 1;
const x = hover.x*dpr, y = hover.y*dpr, r = 9*dpr;
octx.beginPath();
octx.moveTo(x-r,y); octx.lineTo(x-3*dpr,y);
octx.moveTo(x+3*dpr,y); octx.lineTo(x+r,y);
octx.moveTo(x,y-r); octx.lineTo(x,y-3*dpr);
octx.moveTo(x,y+3*dpr); octx.lineTo(x,y+r);
octx.stroke();
octx.restore();
}
}
/* ============================================================
8. KEYBOARD
============================================================ */
window.addEventListener('keydown', e => {
if(e.target.tagName === 'INPUT' && e.target.type === 'text') return;
wake();
const panPx = CFG.panKeyFrac;
switch(e.key){
case 'ArrowLeft': pushHistory(); view.re -= panPx*W*pxSize(); onViewChanged(); break;
case 'ArrowRight': pushHistory(); view.re += panPx*W*pxSize(); onViewChanged(); break;
case 'ArrowUp': pushHistory(); view.im -= panPx*H*pxSize(); onViewChanged(); break;
case 'ArrowDown': pushHistory(); view.im += panPx*H*pxSize(); onViewChanged(); break;
case '+': case '=': zoomAt(W/2, H/2, CFG.zoomSpeed*2); break;
case '-': case '_': zoomAt(W/2, H/2, 1/(CFG.zoomSpeed*2)); break;
case 'j': case 'J': toggleJulia(); break;
case 'b': case 'B': goBack(); break;
case 'r': case 'R': resetView(); break;
case 'h': case 'H': $('help').classList.toggle('show'); break;
case 'Escape': $('help').classList.remove('show'); break;
default: return;
}
e.preventDefault();
});
function toggleJulia(){
pushHistory();
if(view.set === 'mandel'){
/* use hovered c if available, else current center */
if(hover){ const [re,im] = px2c(hover.x*dpr, hover.y*dpr); view.jre = re; view.jim = im; }
view.set = 'julia';
toast('Julia set for c = '+fmt(view.jre)+', '+fmt(view.jim));
} else {
view.set = 'mandel';
}
onViewChanged();
renderPreview(view.jre, view.jim);
}
/* ============================================================
9. JULIA LIVE PREVIEW (small canvas, other set + marker)
============================================================ */
let previewPending = false;
function schedulePreview(){
if(previewPending) return;
previewPending = true;
requestAnimationFrame(() => {
previewPending = false;
if(!hover) return;
const [re,im] = px2c(hover.x*dpr, hover.y*dpr);
if(view.set === 'mandel'){ renderPreview(re, im); }
else { renderMandelPreview(re, im); }
});
}
function renderPreview(cre, cim){
const w = CFG.previewW, h = CFG.previewH;
const imgp = pctx.createImageData(w, h);
const d = imgp.data;
const p = 3.0/h;
for(let y=0; y<h; y++){
for(let x=0; x<w; x++){
const zx = (x - w/2)*p, zy = (y - h/2)*p;
const mu = iterate(zx, zy, CFG.previewIter, true, cre, cim);
const o = (y*w + x)*4;
if(mu < 0){ d[o]=0; d[o+1]=0; d[o+2]=0; }
else{
let idx = ((mu*0.03 + view.cycle/LUT_N) * LUT_N) | 0;
idx = ((idx % LUT_N) + LUT_N) % LUT_N;
d[o]=lut[idx*3]; d[o+1]=lut[idx*3+1]; d[o+2]=lut[idx*3+2];
}
d[o+3]=255;
}
}
pctx.putImageData(imgp, 0, 0);
$('previewCap').textContent = 'Julia c = '+fmt(cre)+', '+fmt(cim);
/* marker: crosshair at c in preview is implicit (c not in z-plane); show label only */
}
function renderMandelPreview(cre, cim){
const w = CFG.previewW, h = CFG.previewH;
const imgp = pctx.createImageData(w, h);
const d = imgp.data;
const p = 3.5/w;
for(let y=0; y<h; y++){
for(let x=0; x<w; x++){
const cx = (x - w/2)*p - 0.6, cy = (y - h/2)*p;
const mu = iterate(cx, cy, CFG.previewIter, false, 0, 0);
const o = (y*w + x)*4;
if(mu < 0){ d[o]=0; d[o+1]=0; d[o+2]=0; }
else{
let idx = ((mu*0.03 + view.cycle/LUT_N) * LUT_N) | 0;
idx = ((idx % LUT_N) + LUT_N) % LUT_N;
d[o]=lut[idx*3]; d[o+1]=lut[idx*3+1]; d[o+2]=lut[idx*3+2];
}
d[o+3]=255;
}
}
/* marker at hovered c */
const mx = (cre + 0.6)/p, my = cim/p + h/2;
pctx.putImageData(imgp, 0, 0);
pctx.strokeStyle = '#fff'; pctx.lineWidth = 1;
pctx.beginPath(); pctx.arc(mx, my, 4, 0, Math.PI*2); pctx.stroke();
$('previewCap').textContent = 'Mandelbrot — hover c marked';
}
/* ============================================================
10. HUD / UI
============================================================ */
function fmt(v){
if(Math.abs(v) < 1e-12) v = 0;
return v.toFixed(Math.min(15, Math.max(5, 15 - Math.floor(Math.log10(Math.max(1e-15, Math.abs(v)))) )));
}
function fmtMag(m){
if(m < 1e6) return '×'+Math.round(m).toLocaleString();
return '×'+m.toExponential(1);
}
let lastRenderMs = 0, fpsSmooth = 60, lastFrame = performance.now();
function hudRender(ms){
lastRenderMs = ms;
}
function tick(now){
const dt = now - lastFrame; lastFrame = now;
fpsSmooth = fpsSmooth*0.9 + (1000/Math.max(1,dt))*0.1;
$('hudFps').textContent = Math.round(fpsSmooth)+' fps · last render '+Math.round(lastRenderMs)+' ms';
requestAnimationFrame(tick);
}
function updateHUD(){
$('hudMag').textContent = fmtMag(mag());
$('hudCenter').textContent = fmt(view.re)+', '+fmt(view.im);
$('hudIter').textContent = effIter()+(view.iterOverride>0?' (manual)':' (auto)');
$('setName').textContent = view.set==='julia' ? 'Julia' : 'Mandelbrot';
$('juliaC').textContent = 'c = '+fmt(view.jre)+', '+fmt(view.jim);
$('btnJulia').textContent = view.set==='julia' ? 'Mandel' : 'Julia';
}
let idleTimer = 0;
function wake(){
document.body.classList.remove('idle');
clearTimeout(idleTimer);
idleTimer = setTimeout(() => document.body.classList.add('idle'), CFG.idleHideMs);
}
let toastTimer = 0;
function toast(msg){
const t = $('toast');
t.textContent = msg; t.classList.add('show');
clearTimeout(toastTimer);
toastTimer = setTimeout(() => t.classList.remove('show'), 2600);
}
/* palette chips */
function buildChips(){
const box = $('paletteChips'); box.innerHTML = '';
PALETTES.forEach((p, i) => {
const b = document.createElement('button');
b.className = 'chip' + (i===view.pal ? ' sel' : '');
b.title = p.name;
const g = p.stops.map(s => s[1]).join(',');
b.style.background = 'linear-gradient(90deg,'+g+')';
b.onclick = () => { view.pal = i; buildLUT(); buildChips(); recolorOnly(); };
box.appendChild(b);
});
}
/* recolor without recomputing iterations (uses full-res muBuf) */
function recolorOnly(){
colorizeRows(0, H, 1, W);
ctx.putImageData(imgData, 0, 0);
}
/* NOTE: recolorOnly requires the last full pass (s=1) to have completed;
if interrupted, fall back to a re-render. */
function safeRecolor(){
if(muBuf && muBuf.length === W*H && lastFullPassDone) recolorOnly();
else requestRender();
}
let lastFullPassDone = false;
/* sliders */
$('iterSlider').oninput = e => {
view.iterOverride = +e.target.value;
$('iterOut').textContent = view.iterOverride>0 ? view.iterOverride : 'auto';
requestRender(); updateHUD(); updateHash();
};
$('cycleSlider').oninput = e => {
view.cycle = +e.target.value;
$('cycleOut').textContent = view.cycle;
safeRecolor(); updateHash();
};
$('densSlider').oninput = e => {
view.density = +e.target.value;
$('densOut').textContent = (view.density/10).toFixed(1)+'%';
safeRecolor(); updateHash();
};
$('btnBack').onclick = goBack;
$('btnReset').onclick = resetView;
$('btnJulia').onclick = toggleJulia;
$('btnHelp').onclick = () => $('help').classList.toggle('show');
/* ============================================================
11. BOOKMARKS (localStorage) + URL hash
============================================================ */
const BM_KEY = 'fractal.bookmarks';
function snapshotState(){
return { set:view.set, re:view.re, im:view.im, scale:view.scale,
jre:view.jre, jim:view.jim, pal:view.pal,
iter:view.iterOverride, cycle:view.cycle, density:view.density };
}
function applyState(s){
Object.assign(view, {
set:s.set, re:s.re, im:s.im, scale:s.scale, jre:s.jre, jim:s.jim,
pal:s.pal||0, iterOverride:s.iter||0, cycle:s.cycle||0, density:s.density||30
});
$('iterSlider').value = view.iterOverride;
$('iterOut').textContent = view.iterOverride>0 ? view.iterOverride : 'auto';
$('cycleSlider').value = view.cycle; $('cycleOut').textContent = view.cycle;
$('densSlider').value = view.density; $('densOut').textContent = (view.density/10).toFixed(1)+'%';
buildLUT(); buildChips();
onViewChanged();
}
function getBookmarks(){
try{ return JSON.parse(localStorage.getItem(BM_KEY)) || []; }catch(_){ return []; }
}
function renderBookmarks(){
const list = getBookmarks(), box = $('bmList');
box.innerHTML = '';
if(!list.length){ box.innerHTML = '<div class="empty">No bookmarks yet</div>'; return; }
list.forEach((bm, i) => {
const row = document.createElement('div'); row.className = 'bm';
const b = document.createElement('button');
b.textContent = bm.name;
b.onclick = () => { pushHistory(); applyState(JSON.parse(JSON.stringify(bm.state))); toast('Restored "'+bm.name+'"'); };
const del = document.createElement('button');
del.className = 'del'; del.textContent = '×';
del.onclick = () => { const l = getBookmarks(); l.splice(i,1); localStorage.setItem(BM_KEY, JSON.stringify(l)); renderBookmarks(); };
row.appendChild(b); row.appendChild(del); box.appendChild(row);
});
}
$('bmSave').onclick = () => {
const name = $('bmName').value.trim() || ('view '+new Date().toLocaleTimeString());
const list = getBookmarks();
list.push({ name, state: snapshotState() });
localStorage.setItem(BM_KEY, JSON.stringify(list));
$('bmName').value = '';
renderBookmarks();
toast('Bookmark saved');
};
function updateHash(){
try{
const s = snapshotState();
const enc = btoa(JSON.stringify(s)).replace(/\+/g,'-').replace(/\//g,'_').replace(/=+$/,'');
history.replaceState(null, '', '#'+enc);
}catch(_){}
}
function loadHash(){
if(!location.hash || location.hash.length < 2) return false;
try{
let b = location.hash.slice(1).replace(/-/g,'+').replace(/_/g,'/');
while(b.length % 4) b += '=';
applyState(JSON.parse(atob(b)));
return true;
}catch(_){ return false; }
}
/* ============================================================
12. DEBUG INTERFACE (window.fractal)
============================================================ */
window.fractal = {
view,
iterationsAt(re, im, maxIter){
const mi = maxIter || effIter();
const mu = (view.set === 'julia')
? iterate(re, im, mi, true, view.jre, view.jim)
: iterate(re, im, mi, false, 0, 0);
return mu < 0 ? mi : mu; // interior → exactly maxIter
},
setView(o){
if(o.set) view.set = o.set;
if('re' in o) view.re = o.re;
if('im' in o) view.im = o.im;
if('scale' in o) view.scale = o.scale;
if('jre' in o) view.jre = o.jre;
if('jim' in o) view.jim = o.jim;
onViewChanged();
},
getView(){ return snapshotState(); },
zoomToRect, resetView, goBack, toggleJulia, applyState, snapshotState,
px2c, c2px, mag, effIter,
bookmarks: { list:getBookmarks, save:(n)=>{ $('bmName').value=n; $('bmSave').click(); } }
};
/* ============================================================
13. INIT
============================================================ */
window.addEventListener('resize', resize);
buildLUT();
buildChips();
renderBookmarks();
resize();
updateHUD();
if(!loadHash()) renderPreview(view.jre, view.jim);
wake();
requestAnimationFrame(tick);
</script>
</body>
</html>
<!-- agent-meta {"model":"stealth/ox-alpha","provider":"openrouter","persona":"full","sessionId":"3c5f314b-fa9f-4994-aa6c-abd252630978","tokensIn":439563,"tokensOut":18567,"tokensTotal":458130,"cost":0,"turns":14,"toolCalls":15,"failedToolCalls":0,"timestamp":"2026-08-21T21:22:57.130Z"} -->