Fractal Explorer
Tiel Coder 35B A3B GGUF MTP · 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 38,922 bytes · SHA-256 d41676fdf874
<!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>
/* ============================================================
GLOBAL / LAYOUT
============================================================ */
* { margin:0; padding:0; box-sizing:border-box; }
html,body { width:100%; height:100%; overflow:hidden; background:#000; font-family: ui-monospace, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace; color:#e8f0ff; }
#stage { position:fixed; inset:0; }
canvas#main { display:block; width:100%; height:100%; cursor:crosshair; touch-action:none; }
/* ============================================================
OVERLAYS (reticle + selection box) — live DOM, no canvas cost
============================================================ */
#reticle { position:absolute; pointer-events:none; width:26px; height:26px; margin:-13px 0 0 -13px; opacity:0; transition:opacity .15s; z-index:5; }
#reticle.on { opacity:.9; }
#reticle::before, #reticle::after { content:""; position:absolute; background:#9fe0ff; box-shadow:0 0 4px rgba(0,0,0,.8); }
#reticle::before { left:12px; top:0; width:1px; height:26px; }
#reticle::after { top:12px; left:0; height:1px; width:26px; }
#selbox { position:absolute; pointer-events:none; opacity:0; z-index:6; border:2px solid #fff; box-shadow:0 0 0 9999px rgba(0,0,0,.35); border-radius:2px; transition:opacity .08s; }
#selbox.on { opacity:1; }
#selbox .corner { position:absolute; width:8px; height:8px; border:2px solid #fff; }
#selbox .tl{ top:-5px; left:-5px; border-right:0; border-bottom:0; }
#selbox .tr{ top:-5px; right:-5px; border-left:0; border-bottom:0; }
#selbox .bl{ bottom:-5px; left:-5px; border-right:0; border-top:0; }
#selbox .br{ bottom:-5px; right:-5px; border-left:0; border-top:0; }
#selbox .lbl { position:absolute; bottom:-22px; left:0; background:rgba(10,16,28,.85); border:1px solid rgba(160,200,255,.4); padding:1px 6px; font-size:11px; white-space:nowrap; color:#cfe4ff; }
/* ============================================================
GLASSY HUD
============================================================ */
#hud { position:absolute; top:12px; left:12px; z-index:8; padding:12px 14px; min-width:236px; background:rgba(12,18,30,.55); backdrop-filter:blur(10px) saturate(140%); -webkit-backdrop-filter:blur(10px) saturate(140%); border:1px solid rgba(140,180,255,.18); border-radius:12px; box-shadow:0 8px 30px rgba(0,0,0,.45); transition:opacity .35s ease, transform .35s ease; user-select:none; }
#hud.hidden { opacity:0; transform:translateY(-6px); pointer-events:none; }
#hud h1 { font-size:12px; letter-spacing:.14em; text-transform:uppercase; font-weight:600; color:#9fd0ff; margin-bottom:8px; }
.row { display:flex; justify-content:space-between; font-size:12px; line-height:1.7; gap:14px; }
.row .k { color:#8fa4c0; }
.row .v { color:#eaf3ff; font-variant-numeric:tabular-nums; text-align:right; }
.v.accent { color:#9fe6a0; }
#hud .sep { height:1px; background:rgba(140,180,255,.15); margin:6px 0; }
/* ============================================================
CONTROL PANEL (right)
============================================================ */
#panel { position:absolute; top:12px; right:12px; z-index:8; width:244px; padding:14px; background:rgba(12,18,30,.55); backdrop-filter:blur(10px) saturate(140%); -webkit-backdrop-filter:blur(10px) saturate(140%); border:1px solid rgba(140,180,255,.18); border-radius:12px; box-shadow:0 8px 30px rgba(0,0,0,.45); transition:opacity .35s ease, transform .35s ease; }
#panel.hidden { opacity:0; transform:translateX(6px); pointer-events:none; }
#panel h2 { font-size:11px; letter-spacing:.14em; text-transform:uppercase; color:#9fd0ff; margin:2px 0 8px; }
.seg { display:flex; gap:6px; margin-bottom:10px; }
.seg button { flex:1; padding:6px 0; font:inherit; font-size:11px; color:#cfe0ff; background:rgba(255,255,255,.05); border:1px solid rgba(140,180,255,.22); border-radius:8px; cursor:pointer; transition:background .15s, color .15s; }
.seg button:hover { background:rgba(255,255,255,.12); }
.seg button.active { background:rgba(120,180,255,.28); border-color:rgba(150,200,255,.6); color:#fff; }
.chip-row { display:flex; flex-wrap:wrap; gap:6px; margin-bottom:6px; }
.chip { width:26px; height:26px; border-radius:7px; border:2px solid rgba(255,255,255,.15); cursor:pointer; position:relative; transition:transform .12s, border-color .12s; }
.chip:hover { transform:scale(1.08); }
.chip.active { border-color:#fff; box-shadow:0 0 0 2px rgba(255,255,255,.25); }
.ctrl { margin:10px 0; }
.ctrl label { display:block; font-size:11px; color:#8fa4c0; margin-bottom:4px; display:flex; justify-content:space-between; }
.ctrl label span { color:#cfe4ff; }
input[type=range] { width:100%; accent-color:#6fb3ff; }
.btn-row { display:flex; gap:6px; margin:8px 0; flex-wrap:wrap; }
.btn-row button { flex:1; min-width:64px; padding:7px 4px; font:inherit; font-size:11px; color:#dce9ff; background:rgba(255,255,255,.06); border:1px solid rgba(140,180,255,.24); border-radius:8px; cursor:pointer; }
.btn-row button:hover { background:rgba(255,255,255,.14); }
.check { display:flex; align-items:center; gap:8px; font-size:11px; color:#b8cbe6; margin:8px 0; cursor:pointer; }
.check input { accent-color:#6fb3ff; }
#bookmarks { margin-top:6px; max-height:150px; overflow-y:auto; }
.bk { display:flex; align-items:center; justify-content:space-between; gap:6px; padding:5px 6px; font-size:11px; border-radius:7px; background:rgba(255,255,255,.04); margin-bottom:4px; border:1px solid rgba(140,180,255,.12); }
.bk span { flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; color:#cfe4ff; cursor:pointer; }
.bk span:hover { color:#fff; }
.bk button { background:none; border:none; color:#8fa4c0; cursor:pointer; font-size:12px; padding:0 2px; }
.bk button:hover { color:#ff9b9b; }
.bkadd { display:flex; gap:6px; margin-top:6px; }
.bkadd input { flex:1; background:rgba(0,0,0,.25); border:1px solid rgba(140,180,255,.2); border-radius:7px; color:#eaf3ff; padding:5px 7px; font:inherit; font-size:11px; }
.bkadd button { background:rgba(120,180,255,.22); border:1px solid rgba(150,200,255,.5); color:#fff; border-radius:7px; padding:5px 9px; cursor:pointer; font:inherit; font-size:11px; }
#hint { position:absolute; bottom:12px; left:12px; z-index:8; font-size:11px; color:rgba(180,205,240,.6); background:rgba(12,18,30,.45); padding:6px 10px; border-radius:8px; border:1px solid rgba(140,180,255,.12); transition:opacity .35s; pointer-events:none; max-width:52vw; line-height:1.6; }
#hint.hidden { opacity:0; }
#warn { position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); z-index:9; background:rgba(40,10,10,.9); border:1px solid rgba(255,120,120,.5); color:#ffc9c9; padding:12px 18px; border-radius:10px; font-size:13px; display:none; text-align:center; max-width:80vw; }
.preview-wrap { margin-top:10px; border-top:1px solid rgba(140,180,255,.15); padding-top:10px; }
.preview-wrap h2 { margin-bottom:6px; }
#preview { width:100%; height:120px; border-radius:8px; border:1px solid rgba(140,180,255,.2); background:#000; display:block; }
#previewC { font-size:11px; color:#b8cbe6; margin-top:4px; text-align:center; }
</style>
</head>
<body>
<div id="stage">
<canvas id="main"></canvas>
<div id="reticle"></div>
<div id="selbox"><span class="corner tl"></span><span class="corner tr"></span><span class="corner bl"></span><span class="corner br"></span><span class="lbl"></span></div>
</div>
<div id="hud">
<h1>Fractal Explorer</h1>
<div class="row"><span class="k">Mode</span><span class="v" id="hudMode">Mandelbrot</span></div>
<div class="row"><span class="k">Magnification</span><span class="v accent" id="hudMag">×1</span></div>
<div class="row"><span class="k">Center Re</span><span class="v" id="hudRe">0</span></div>
<div class="row"><span class="k">Center Im</span><span class="v" id="hudIm">0</span></div>
<div class="sep"></div>
<div class="row"><span class="k">Iterations</span><span class="v" id="hudIter">0</span></div>
<div class="row"><span class="k">FPS</span><span class="v" id="hudFps">0</span></div>
<div class="row"><span class="k">Pass</span><span class="v" id="hudPass">—</span></div>
</div>
<div id="panel">
<h2>Set</h2>
<div class="seg">
<button id="btnMandel" class="active">Mandelbrot</button>
<button id="btnJulia">Julia</button>
</div>
<h2>Palette</h2>
<div class="chip-row" id="palettes"></div>
<div class="ctrl">
<label>Iterations <span id="iterVal">+0</span></label>
<input type="range" id="iterSlider" min="0" max="8000" step="1" value="0">
</div>
<div class="ctrl">
<label>Color cycle <span id="cycleVal">0</span></label>
<input type="range" id="cycleSlider" min="0" max="4095" step="1" value="0">
</div>
<label class="check"><input type="checkbox" id="animCheck"> Animate color drift</label>
<div class="btn-row">
<button id="btnBack">◀ Back</button>
<button id="btnReset">Reset</button>
</div>
<h2>Bookmarks</h2>
<div id="bookmarks"></div>
<div class="bkadd"><input id="bkName" placeholder="View name"><button id="bkAdd">Save</button></div>
<div class="preview-wrap">
<h2>Julia Preview</h2>
<canvas id="preview"></canvas>
<div id="previewC">hover the view</div>
</div>
</div>
<div id="hint">
Drag to zoom · <b>Space+drag</b> pan · <b>Wheel</b> zoom · <b>Arrows</b> pan · <b>+ / −</b> zoom · <b>J</b> Julia · <b>Click</b> open Julia · <b>H</b> hide UI
</div>
<div id="warn"></div>
<script>
/* ============================================================
TUNABLE CONSTANTS (edit these to trade quality / speed)
============================================================ */
const T = {
ESCAPE_RADIUS2 : 256, // |z|^2 escape threshold (smooth coloring uses log|z|)
BASE_ITER : 300, // max iterations at magnification ≈ 1×
ITER_PER_OCTAVE: 20, // +iterations per 2× magnification
ITER_SLIDER_MAX: 8000, // manual iteration override max
MAX_ITER_CAP : 20000, // hard cap
MAX_MAGNIFICATION: 1e14, // double-precision ceiling (precision degrades past this)
ZOOM_FACTOR : 0.0022, // wheel sensitivity (exponential)
KB_ZOOM : 1.6, // +/- zoom step multiplier
KB_PAN_PX : 8, // arrow-key pan distance in px
COARSE_DIV : 4, // coarse pass resolution divisor
IDLE_MS : 380, // idle time before full-res refine begins
ROWS_PER_FRAME : 20, // full-res refine rows per animation frame (progressive)
COLOR_LUT_SIZE : 4096, // color lookup table size
COLOR_SPEED : 0.018, // color cycles per iteration unit
MOMENTUM_DAMP : 0.90, // pan momentum decay
};
const INITIAL_VIEW = { re:-0.5, im:0.0, scale:4.0 }; // complex width 4 → full Mandelbrot visible
/* ============================================================
PALETTES (stops over t∈[0,1); 'periodic' ⇒ seamless cyclic wrap)
============================================================ */
const PALETTES = {
fire: { periodic:false, stops:[[0,[4,0,12]],[.42,[150,18,4]],[.7,[238,120,18]],[.88,[255,214,96]],[1,[255,255,238]]] },
ocean: { periodic:false, stops:[[0,[2,6,20]],[.4,[8,44,120]],[.7,[14,150,200]],[.88,[120,230,255]],[1,[236,255,255]]] },
mono: { periodic:false, stops:[[0,[0,0,0]],[.55,[55,55,60]],[.8,[170,170,180]],[1,[255,255,255]]] },
psychedelic:{ periodic:true, stops:null }, // built via cosine formula
supernova: { periodic:false, stops:[[0,[8,0,18]],[.35,[120,20,140]],[.6,[230,70,20]],[.82,[255,190,60]],[1,[255,255,255]]] },
arctic: { periodic:false, stops:[[0,[2,8,18]],[.45,[30,90,140]],[.7,[120,210,215]],[.88,[200,245,240]],[1,[255,255,255]]] },
};
const PALETTE_ORDER = ['fire','ocean','mono','psychedelic','supernova','arctic'];
function cosinePalette(v){
// Inigo Quilez style cyclic palette; v∈[0,1) wraps seamlessly
const a=0.5, b=0.5, p=6.283185307179586;
const r = a + b*Math.cos(p*(1.0*v + 0.00));
const g = a + b*Math.cos(p*(1.0*v + 0.66));
const bl= a + b*Math.cos(p*(1.0*v + 0.33));
return [r,g,bl];
}
function sampleStops(stops, t){
const n = stops.length - 1;
const ft = t * n;
const i = Math.min(Math.floor(ft), n-1);
const f = ft - i;
const s = f*f*(3-2*f); // smoothstep → no visible bands
const a = stops[i], b = stops[i+1];
return [ a[1][0]+(b[1][0]-a[1][0])*s, a[1][1]+(b[1][1]-a[1][1])*s, a[1][2]+(b[1][2]-a[1][2])*s ];
}
function buildLUT(name){
const L = T.COLOR_LUT_SIZE;
const local = new Array(L*3);
const pal = PALETTES[name];
// Escape-count ramp: LUT index 0 (fast escape) → dark, index L-1 → brightest stop.
// The brightest stop is reserved for the very end so the set boundary glows.
for(let i=0;i<L;i++){
const v = i / L; // [0,1)
let c;
if(pal.periodic){
c = cosinePalette(v); // seamless cyclic (period 2π)
} else {
c = sampleStops(pal.stops, v); // ramp: index 0 (dark) → index L-1 (brightest)
}
const bright = Math.min(1, v*0.95); // ramp brightness from dark → near-bright
const r = Math.min(255, Math.round(c[0]*255*bright));
const g = Math.min(255, Math.round(c[1]*255*bright));
const bl= Math.min(255, Math.round(c[2]*255*bright));
local[i*3] = r;
local[i*3+1] = g;
local[i*3+2] = bl;
}
return local;
}
/* ============================================================
VIEW STATE
============================================================ */
const view = Object.assign({}, INITIAL_VIEW);
const juliaC = { re:-0.5, im:0.0 };
let mode = 'mandel'; // 'mandel' | 'julia'
let followCursor = true; // in Julia mode, move cursor ⇒ new c
let paletteName= 'fire';
let iterOffset = 0;
let cycleOffset= 0;
let animating = false;
let curMaxIter = T.BASE_ITER;
let lut = buildLUT(paletteName);
const history = []; // zoom history stack
/* ============================================================
CANVAS / BUFFER SETUP
============================================================ */
const canvas = document.getElementById('main');
const ctx = canvas.getContext('2d', { alpha:false });
let W = 0, H = 0; // display (backing) size
let coarseW = 0, coarseH = 0;
let coarseBuf, fullBuf;
let coarseIA, coarseScr, fullIA, fullScr;
function makeScratch(w,h){
const c = document.createElement('canvas');
c.width = w; c.height = h;
c.getContext('2d').drawImage(canvas,0,0); // warm
return c;
}
function resize(){
const r = canvas.getBoundingClientRect();
W = Math.max(1, Math.round(r.width));
H = Math.max(1, Math.round(r.height));
canvas.width = W; canvas.height = H;
coarseW = Math.max(1, Math.floor(W / T.COARSE_DIV));
coarseH = Math.max(1, Math.floor(H / T.COARSE_DIV));
coarseBuf = new Float32Array(coarseW*coarseH);
fullBuf = new Float32Array(W*H);
coarseScr = makeScratch(coarseW,coarseH); coarseIA = coarseScr.getContext('2d').createImageData(coarseW,coarseH);
fullScr = makeScratch(W,H); fullIA = fullScr.getContext('2d').createImageData(W,H);
viewDirty = true;
}
/* ============================================================
COORDINATE MAPPING
uniform px→complex scale ⇒ shapes undistorted, aspect = W:H
============================================================ */
function complexHeight(){ return view.scale * (H / W); }
function pxToComplex(px, py){
return {
re: view.re + ((px + 0.5) / W - 0.5) * view.scale,
im: view.im + ((py + 0.5) / H - 0.5) * complexHeight()
};
}
/* ============================================================
ITERATION MATH (allocation-free inner loop, smooth coloring)
============================================================ */
function computeMaxIter(){
const mag = (INITIAL_VIEW.scale / view.scale);
const fromZoom = Math.log2(Math.max(1, mag)) * T.ITER_PER_OCTAVE;
let m = T.BASE_ITER + Math.round(fromZoom) + iterOffset;
m = Math.max(T.BASE_ITER, Math.min(m, T.MAX_ITER_CAP));
curMaxIter = m;
return m;
}
// compute iterations for rectangle [x0,x1)×[y0,y1) into buf (row-major, W columns)
function computeRect(buf, W, H, x0, x1, y0, y1, cRe, cIm, reC, imC, scale, maxIter){
const aspect = scale * (H / W);
const invW = 1 / W, invH = 1 / H;
const idx = y0 * W + x0;
let o = idx;
for(let y = y0; y < y1; y++){
const cy = imC + ((y + 0.5) * invH - 0.5) * aspect;
for(let x = x0; x < x1; x++){
const cx = reC + ((x + 0.5) * invW - 0.5) * scale;
let zr = cx, zi = cy; // Mandelbrot: start z=c; Julia handled by caller
let n = 0;
while(n < maxIter){
const zr2 = zr*zr - zi*zi + cx;
zi = 2*zr*zi + cy;
zr = zr2;
const m2 = zr*zr + zi*zi;
if(m2 > 4.0) break;
n++;
}
if(n < maxIter){
const m2 = zr*zr + zi*zi;
const logZ = 0.5 * Math.log(m2);
buf[o] = n + 1 - (Math.log(logZ / Math.LN2) / Math.LN2); // smooth fractional count
} else {
buf[o] = maxIter; // interior → black
}
o++;
}
}
}
// Public debug helper: smooth iteration count at a complex point.
function iterationsAt(re, im){
const maxIter = computeMaxIter();
if(mode === 'mandel'){
computeRect(new Float32Array(1), 1, 1, 0, 1, 0, 1, re, im, re, im, 1, maxIter);
// recompute inline to avoid buffer confusion:
let zr=0,zi=0,n=0,cR=re,cI=im;
while(n < maxIter){
const zr2 = zr*zr - zi*zi + cR;
zi = 2*zr*zi + cI; zr = zr2;
if(zr*zr+zi*zi > T.ESCAPE_RADIUS2) break;
n++;
}
if(n < maxIter){ const lz=0.5*Math.log(zr*zr+zi*zi); return n + 1 - (Math.log(lz/Math.LN2)/Math.LN2); }
return maxIter;
} else {
let zr=re,zi=im,n=0,cR=juliaC.re,cI=juliaC.im;
while(n < maxIter){
const zr2 = zr*zr - zi*zi + cR;
zi = 2*zr*zi + cI; zr = zr2;
if(zr*zr+zi*zi > T.ESCAPE_RADIUS2) break;
n++;
}
if(n < maxIter){ const lz=0.5*Math.log(zr*zr+zi*zi); return n + 1 - (Math.log(lz/Math.LN2)/Math.LN2); }
return maxIter;
}
}
/* ============================================================
COLORIZE (colorize-pass only; cycle offset here ⇒ no recompute)
============================================================ */
function colorize(buf, W, H, IA){
const data = IA.data;
const L = lut.length, co = cycleOffset, mi = curMaxIter;
let i = 0, p = 0;
const lim = W * H;
while(p < lim){
const count = buf[p++];
let o;
if(count < mi){
const v = count / mi; // normalize to [0,1) so full LUT range is used
o = ((v * L) | 0 + co);
if(o >= L) o -= L; // co < L so single subtraction suffices
}
data[i++] = lut[o]; data[i++] = lut[o+1]; data[i++] = lut[o+2]; data[i++] = 255;
}
}
/* ============================================================
RENDER PIPELINE (progressive + token cancellation)
============================================================ */
let token = 0;
let viewDirty = true;
let refining = false;
let refineTk = -1;
let refineRow = 0;
let refiningFull = false;
function blit(offScr, offW, offH){
ctx.drawImage(offScr, 0, 0, W, H);
}
function renderAt(resW, resH, buf, IA, offScr, isFull, tk){
if(tk !== token) return false; // cancelled
computeMaxIter();
const cRe = mode==='mandel' ? 0 : juliaC.re;
const cIm = mode==='mandel' ? 0 : juliaC.im;
computeRect(buf, resW, resH, 0, resW, 0, resH, cRe, cIm, view.re, view.im, view.scale, curMaxIter);
colorize(buf, resW, resH, IA);
offScr.getContext('2d').putImageData(IA, 0, 0);
blit(offScr, resW, resH);
return true;
}
function startRefine(tk){
if(refining && refineTk === tk) return;
stopRefine();
refining = true; refineTk = tk; refineRow = 0; refiningFull = true;
hudPass('full');
}
function stopRefine(){
refining = false; refineTk = -1; refineRow = 0; refiningFull = false;
}
function advanceRefine(){
if(refineTk !== token){ stopRefine(); return; }
const end = Math.min(H, refineRow + T.ROWS_PER_FRAME);
const cRe = mode==='mandel' ? 0 : juliaC.re;
const cIm = mode==='mandel' ? 0 : juliaC.im;
computeRect(fullBuf, W, H, 0, W, refineRow, end, cRe, cIm, view.re, view.im, view.scale, curMaxIter);
refineRow = end;
if(refineRow >= H){
colorize(fullBuf, W, H, fullIA);
if(token === refineTk){
fullScr.getContext('2d').putImageData(fullIA, 0, 0);
blit(fullScr, W, H);
}
stopRefine();
}
}
/* ============================================================
VIEW CHANGES (with history)
============================================================ */
function applyView(re, im, scale, remember){
if(scale > INITIAL_VIEW.scale) scale = INITIAL_VIEW.scale;
if(scale < INITIAL_VIEW.scale / T.MAX_MAGNIFICATION){
scale = INITIAL_VIEW.scale / T.MAX_MAGNIFICATION;
showWarn('Precision limit reached — zoom capped at ~×' +
T.MAX_MAGNIFICATION.toExponential(1) + '<br>Doubles lose precision past this point.');
} else hideWarn();
if(remember){ history.push(Object.assign({}, view)); if(history.length>60) history.shift(); }
view.re = re; view.im = im; view.scale = scale;
viewDirty = true;
lastInteraction = performance.now();
schedule();
}
function zoomToWorld(px, py, factor){
const c = pxToComplex(px, py);
const ns = view.scale / factor;
// keep cursor-fixed: new center so c maps to same px
const nr = c.re + (view.re - c.re) / factor;
const ni = c.im + (view.im - c.im) / factor;
applyView(nr, ni, ns, true);
}
function panByPixels(dxpx, dpy){
view.re -= (dxpx / W) * view.scale;
view.im -= (dpy / H) * complexHeight();
viewDirty = true;
lastInteraction = performance.now();
schedule();
}
function doRectZoom(a, b){ // a,b = px corners
const ca = pxToComplex(a.x, a.y), cb = pxToComplex(b.x, b.y);
const newRe = (ca.re + cb.re) / 2;
const newIm = (ca.im + cb.im) / 2;
const newScale = Math.abs(b.x - a.x) / W * view.scale; // width-mapped (exact framing)
applyView(newRe, newIm, newScale, true);
}
function centerViewOn(re, im){
applyView(re, im, view.scale, false);
}
/* ============================================================
INTERACTION STATE
============================================================ */
let dragging = false, spacePanning = false;
let moved = false, selStart = null, selCur = null;
let lastPan = null, panVel = {x:0,y:0}, momentum = false;
let pointerIdle = true, lastMoveT = 0, mx = 0, my = 0;
let lastInteraction = performance.now();
let previewDirty = true, hoveredC = {re:INITIAL_VIEW.re, im:0};
let hudForced = false, hudHidden = false;
const reticle = document.getElementById('reticle');
const selbox = document.getElementById('selbox');
const selLbl = selbox.querySelector('.lbl');
const warnEl = document.getElementById('warn');
function showWarn(html){ warnEl.innerHTML = html; warnEl.style.display = 'block'; }
function hideWarn(){ warnEl.style.display = 'none'; }
function schedule(){ if(!rafOn){ rafOn = true; requestAnimationFrame(loop); } }
canvas.addEventListener('mousedown', e => {
if(e.button !== 0) return;
const r = canvas.getBoundingClientRect();
const sx = e.clientX - r.left, sy = e.clientY - r.top;
dragging = true; moved = false; spacePanning = (e.code === 'Space') || _space;
selStart = {x:sx, y:sy}; lastPan = {x:sx, y:sy}; panVel = {x:0,y:0}; momentum = false;
lastMoveT = performance.now(); pointerIdle = false;
});
window.addEventListener('mousemove', e => {
const r = canvas.getBoundingClientRect();
mx = e.clientX - r.left; my = e.clientY - r.top;
// reticle + hovered c
reticle.classList.add('on');
reticle.style.left = mx + 'px'; reticle.style.top = my + 'px';
hoveredC = pxToComplex(mx, my);
previewDirty = true;
if(dragging){
const dx = mx - selStart.x, dy = my - selStart.y;
if(Math.abs(dx) + Math.abs(dy) > 3) moved = true;
if(spacePanning){
const now = performance.now(); const dt = Math.max(1, now - lastMoveT);
const ndx = mx - lastPan.x, ndy = my - lastPan.y;
panVel = { x: ndx / dt * 16, y: ndy / dt * 16 };
panByPixels(-ndx, -ndy);
lastPan = {x:mx, y:my}; lastMoveT = now;
selbox.classList.remove('on');
} else {
updateSelBox();
}
}
});
window.addEventListener('mouseup', e => {
if(!dragging) return;
dragging = false;
reticle.classList.remove('on');
if(spacePanning){ momentum = true; schedule(); return; }
selbox.classList.remove('on');
if(moved) doRectZoom(selStart, {x:mx, y:my});
else openJuliaAt(mx, my);
pointerIdle = true;
});
function updateSelBox(){
const x = Math.min(selStart.x, mx), y = Math.min(selStart.y, my);
const w = Math.abs(mx - selStart.x), h = Math.abs(my - selStart.y);
selbox.style.left = x+'px'; selbox.style.top = y+'px';
selbox.style.width = w+'px'; selbox.style.height = h+'px';
selbox.classList.add('on');
const ca = pxToComplex(x, y), cb = pxToComplex(x + w, y + h);
const cw = Math.abs(cb.re - ca.re), ch = Math.abs(cb.im - ca.im);
selLbl.textContent = cw.toExponential(2)+' × '+ch.toExponential(2);
}
// momentum pan loop
function momentumStep(){
if(!momentum) return;
const ex = Math.abs(panVel.x), ey = Math.abs(panVel.y);
if(ex < 0.05 && ey < 0.05){ momentum = false; return; }
panByPixels(panVel.x, panVel.y);
panVel.x *= T.MOMENTUM_DAMP; panVel.y *= T.MOMENTUM_DAMP;
schedule();
}
function openJuliaAt(px, py){
const c = pxToComplex(px, py);
juliaC.re = c.re; juliaC.im = c.im;
mode = 'julia'; followCursor = true;
applyView(c.re, c.im, 3.0, false);
syncModeButtons();
}
/* ============================================================
WHEEL + KEYBOARD
============================================================ */
canvas.addEventListener('wheel', e => {
e.preventDefault();
const r = canvas.getBoundingClientRect();
const px = e.clientX - r.left, py = e.clientY - r.top;
const factor = Math.exp(-e.deltaY * T.ZOOM_FACTOR);
zoomToWorld(px, py, factor);
}, { passive:false });
let _space = false;
window.addEventListener('keydown', e => {
if(e.target.tagName === 'INPUT') return;
const k = e.key;
if(k === ' '){ _space = true; e.preventDefault(); return; }
if(k === 'h' || k === 'H'){ hudForced = !hudForced; updateHudVisibility(); return; }
if(k === 'j' || k === 'J'){ toggleMode(); return; }
let handled = true;
if(k === 'ArrowLeft') panByPixels( T.KB_PAN_PX, 0);
else if(k === 'ArrowRight') panByPixels(-T.KB_PAN_PX, 0);
else if(k === 'ArrowUp') panByPixels(0, T.KB_PAN_PX);
else if(k === 'ArrowDown') panByPixels(0, -T.KB_PAN_PX);
else if(k === '+' || k === '=') zoomToWorld(W/2, H/2, T.KB_ZOOM);
else if(k === '-' || k === '_') zoomToWorld(W/2, H/2, 1/T.KB_ZOOM);
else handled = false;
if(handled) e.preventDefault();
});
window.addEventListener('keyup', e => { if(e.key === ' ') _space = false; });
canvas.addEventListener('dblclick', () => { zoomToWorld(W/2, H/2, 3); });
function toggleMode(){
if(mode === 'mandel'){
mode = 'julia'; followCursor = true;
juliaC.re = view.re; juliaC.im = view.im;
applyView(juliaC.re, juliaC.im, 3.0, false);
} else { mode = 'mandel'; }
syncModeButtons();
}
/* ============================================================
MAIN LOOP (progressive render + cancellation + HUD + fps)
============================================================ */
let rafOn = false, rafId = 0, lastFrame = 0, fps = 0;
const hud = document.getElementById('hud');
const panel = document.getElementById('panel');
const hint = document.getElementById('hint');
function updateHudVisibility(){
const show = hudForced;
hud.classList.toggle('hidden', !show);
panel.classList.toggle('hidden', !show);
hint.classList.toggle('hidden', !show);
}
function hudPass(s){ document.getElementById('hudPass').textContent = s; }
function loop(now){
rafId = requestAnimationFrame(loop);
if(lastFrame > 0) fps = fps*0.9 + (1000 / Math.max(0.01, now - lastFrame))*0.1;
lastFrame = now;
const idle = (now - lastInteraction) > T.IDLE_MS;
if(!hudForced){
const hide = idle && momentum === false;
hud.classList.toggle('hidden', hide);
panel.classList.toggle('hidden', hide);
hint.classList.toggle('hidden', hide);
}
momentumStep();
// live Julia preview (mandel mode, pointer idle) — colorize-only cost on small canvas
if(mode === 'mandel' && pointerIdle && previewDirty){
renderPreview();
previewDirty = false;
}
if(viewDirty){
viewDirty = false;
token++;
const tk = token;
computeMaxIter();
renderAt(coarseW, coarseH, coarseBuf, coarseIA, coarseScr, false, tk); // fast coarse
if(idle) startRefine(tk); else stopRefine();
} else if(refining){
if(refineTk === token) advanceRefine();
else { stopRefine(); if(idle) startRefine(token); }
}
if(animating && idle){
cycleOffset = (cycleOffset + 1) % lut.length;
document.getElementById('cycleSlider').value = cycleOffset;
document.getElementById('cycleVal').textContent = cycleOffset;
// force colorize-only redraw
if(!refining){ computeMaxIter(); colorize(curMaxIter>0?fullBuf:coarseBuf, (refining?W:coarseW),(refining?H:coarseH), refining?fullIA:coarseIA); (refining?fullScr:coarseScr).getContext('2d').putImageData(refining?fullIA:coarseIA,0,0); blit(refining?fullScr:coarseScr, refining?W:coarseW, refining?H:coarseH); }
}
updateHUD();
}
/* ============================================================
JULIA PREVIEW PANEL
============================================================ */
const pvCanvas = document.getElementById('preview');
const pvCtx = pvCanvas.getContext('2d', { alpha:false });
const PV = 200;
pvCanvas.width = PV; pvCanvas.height = PV;
let pvBuf = new Float32Array(PV*PV);
let pvIA = pvCtx.createImageData(PV, PV);
let pvScr = makeScratch(PV, PV);
let pvMax = T.BASE_ITER;
function renderPreview(){
pvMax = computeMaxIter();
computeRect(pvBuf, PV, PV, 0, PV, 0, PV, juliaC.re, juliaC.im, hoveredC.re, hoveredC.im, 3.0, pvMax);
colorize(pvBuf, PV, PV, pvIA);
pvScr.getContext('2d').putImageData(pvIA, 0, 0);
pvCtx.drawImage(pvScr, 0, 0, PV, PV);
document.getElementById('previewC').textContent =
'c = ' + juliaC.re.toFixed(5) + ' ' + (juliaC.im>=0?'+':'−') + ' ' + Math.abs(juliaC.im).toFixed(5) + 'i';
}
/* ============================================================
HUD UPDATE
============================================================ */
function updateHUD(){
document.getElementById('hudMode').textContent = mode==='mandel' ? 'Mandelbrot' : 'Julia';
document.getElementById('hudMag').textContent = '×' + (INITIAL_VIEW.scale / view.scale).toExponential(2);
document.getElementById('hudRe').textContent = view.re.toExponential(5);
document.getElementById('hudIm').textContent = view.im.toExponential(3);
document.getElementById('hudIter').textContent = curMaxIter;
document.getElementById('hudFps').textContent = fps.toFixed(0);
}
/* ============================================================
UI WIRING (palettes, sliders, buttons, bookmarks)
============================================================ */
function syncModeButtons(){
document.getElementById('btnMandel').classList.toggle('active', mode==='mandel');
document.getElementById('btnJulia').classList.toggle('active', mode==='julia');
}
// palette chips
const chipRow = document.getElementById('palettes');
const chips = {};
PALETTE_ORDER.forEach(name => {
const el = document.createElement('div');
el.className = 'chip'; el.dataset.name = name;
el.title = name;
el.style.background = palettePreviewBg(name);
el.onclick = () => setPalette(name);
chipRow.appendChild(el); chips[name] = el;
});
function palettePreviewBg(name){
const p = PALETTES[name];
if(p.periodic){
// derive a representative gradient
const stops=[];
for(let i=0;i<5;i++){ const c=cosinePalette(i/5); stops.push(`rgb(${c[0]*255|0},${c[1]*255|0},${c[2]*255|0})`); }
return `linear-gradient(90deg,${stops.join(',')})`;
}
const s = p.stops;
const grad = s.map(([t,col])=>`${col.map(x=>x|0).join(',')} ${Math.round(t*100)}%`).join(',');
return `linear-gradient(90deg,rgb(${grad}))`;
}
function setPalette(name){
paletteName = name;
lut = buildLUT(name);
PALETTE_ORDER.forEach(n => chips[n].classList.toggle('active', n===name));
viewDirty = true;
saveHash();
}
document.getElementById('btnMandel').onclick = () => { if(mode==='julia'){ mode='mandel'; syncModeButtons(); viewDirty=true; } };
document.getElementById('btnJulia').onclick = () => toggleMode();
document.getElementById('btnReset').onclick = () => applyView(INITIAL_VIEW.re, INITIAL_VIEW.im, INITIAL_VIEW.scale, false);
document.getElementById('btnBack').onclick = () => { if(history.length){ const p = history.pop(); applyView(p.re,p.im,p.scale,false); } };
document.getElementById('iterSlider').addEventListener('input', e => {
iterOffset = +e.target.value;
document.getElementById('iterVal').textContent = (iterOffset>=0?'+':'') + iterOffset;
computeMaxIter(); viewDirty = true;
});
document.getElementById('cycleSlider').addEventListener('input', e => {
cycleOffset = +e.target.value;
document.getElementById('cycleVal').textContent = cycleOffset;
viewDirty = true;
});
document.getElementById('animCheck').addEventListener('change', e => { animating = e.target.checked; });
/* ---- Bookmarks (localStorage) ---- */
const BK_KEY = 'fractal-explorer-bookmarks';
function loadBookmarks(){
try { return JSON.parse(localStorage.getItem(BK_KEY)) || {}; } catch(e){ return {}; }
}
function saveBookmarks(b){
try { localStorage.setItem(BK_KEY, JSON.stringify(b)); } catch(e){}
}
function renderBookmarks(){
const box = document.getElementById('bookmarks');
const b = loadBookmarks();
box.innerHTML = '';
const keys = Object.keys(b);
if(!keys.length){ box.innerHTML = '<div style="font-size:11px;color:#6f82a0;padding:4px">no bookmarks yet</div>'; }
keys.forEach(name => {
const row = document.createElement('div'); row.className='bk';
const s = document.createElement('span'); s.textContent = name;
s.onclick = () => restoreBookmark(b[name]);
const d = document.createElement('button'); d.textContent='✕';
d.onclick = () => { const b2=loadBookmarks(); delete b2[name]; saveBookmarks(b2); renderBookmarks(); };
row.appendChild(s); row.appendChild(d); box.appendChild(row);
});
}
function currentBookmarkData(name){
return { name, mode, re:view.re, im:view.im, scale:view.scale, palette:paletteName,
iterOffset, juliaC:Object.assign({}, juliaC), followCursor };
}
function restoreBookmark(d){
mode = d.mode || 'mandel';
view.re = d.re; view.im = d.im; view.scale = d.scale;
if(d.juliaC){ juliaC.re = d.juliaC.re; juliaC.im = d.juliaC.im; }
if(d.palette) setPalette(d.palette);
iterOffset = d.iterOffset || 0;
document.getElementById('iterSlider').value = iterOffset;
document.getElementById('iterVal').textContent = (iterOffset>=0?'+':'')+iterOffset;
followCursor = d.followCursor !== undefined ? d.followCursor : true;
syncModeButtons();
viewDirty = true;
saveHash();
}
document.getElementById('bkAdd').onclick = () => {
const inp = document.getElementById('bkName');
let name = inp.value.trim();
if(!name) name = 'View ' + (Object.keys(loadBookmarks()).length + 1);
const b = loadBookmarks();
b[name] = currentBookmarkData(name);
saveBookmarks(b);
inp.value = '';
renderBookmarks();
};
/* ---- URL-hash shareable state ---- */
function encodeState(){
const s = JSON.stringify({
m:mode, re:view.re, im:view.im, scale:view.scale,
pal:paletteName, io:iterOffset, jc:juliaC, fc:followCursor
});
return '#' + btoa(unescape(encodeURIComponent(s)));
}
function saveHash(){
historyDebounce = clearTimeout ? historyDebounce : historyDebounce;
if(historyTimer) clearTimeout(historyTimer);
historyTimer = setTimeout(() => { history.replaceState(null, '', encodeState()); }, 250);
}
let historyTimer = null, historyDebounce = null;
function loadHash(){
if(!location.hash) return false;
try {
const s = decodeURIComponent(escape(atob(location.hash.slice(1))));
const d = JSON.parse(s);
mode = d.m || 'mandel';
view.re = d.re; view.im = d.im; view.scale = d.scale;
juliaC.re = d.jc ? d.jc.re : -0.5; juliaC.im = d.jc ? d.jc.im : 0;
followCursor = d.fc !== undefined ? d.fc : true;
if(d.pal) setPalette(d.pal);
iterOffset = d.io || 0;
document.getElementById('iterSlider').value = iterOffset;
document.getElementById('iterVal').textContent = (iterOffset>=0?'+':'')+iterOffset;
syncModeButtons();
return true;
} catch(e){ return false; }
}
/* ============================================================
INIT
============================================================ */
window.addEventListener('resize', resize);
window.addEventListener('blur', () => { dragging=false; momentum=false; });
document.addEventListener('contextmenu', e => { if(e.target===canvas) e.preventDefault(); });
resize();
if(!loadHash()){ computeMaxIter(); }
renderBookmarks();
syncModeButtons();
// initial coarse+refine
token++; viewDirty = true;
requestAnimationFrame(loop);
/* ============================================================
DEBUG / PROBE API (exposed on window)
============================================================ */
window.fractal = {
iterationsAt,
setView: (o) => { applyView(o.re, o.im, o.scale, false); },
getState: () => ({ mode, re:view.re, im:view.im, scale:view.scale, palette:paletteName,
iterOffset, juliaC:Object.assign({}, juliaC), curMaxIter }),
zoomBy: (factor, px, py) => zoomToWorld(px||W/2, py||H/2, factor),
setPalette,
setIterations: (n) => { iterOffset = n; document.getElementById('iterSlider').value=n; document.getElementById('iterVal').textContent=n; computeMaxIter(); viewDirty=true; },
restoreBookmark,
saveBookmark: (name) => { const b = loadBookmarks(); b[name] = currentBookmarkData(name); saveBookmarks(b); renderBookmarks(); return b[name]; },
_T: T,
};
</script>
</body>
</html>
<!-- agent-meta {"model":"peculiar-ragdoll/Tiel-Coder-35B-A3B-GGUF-MTP","provider":"brigid","persona":"full","sessionId":"e0c80179-e1e5-4eb6-880d-e641f50fed1f","tokensIn":20838761,"tokensOut":218690,"tokensTotal":21057451,"cost":0,"turns":193,"toolCalls":192,"failedToolCalls":2,"timestamp":"2026-09-04T19:00:47.082Z"} -->