Fractal Explorer
GPT 6 Luna · 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 29,542 bytes · SHA-256 a4bfbf5786b3
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
<title>Fractal Field — Mandelbrot & Julia Explorer</title>
<style>
:root{color-scheme:dark;--ink:#f4f2ff;--muted:#a9a8bf;--glass:rgba(17,17,32,.72);--line:rgba(255,255,255,.13);--accent:#b2a4ff}
*{box-sizing:border-box}html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#05050b;color:var(--ink);font-family:Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif}button,input,select{font:inherit}button{color:inherit}#stage{position:fixed;inset:0;overflow:hidden;background:#05050b}#view{display:block;width:100%;height:100%;touch-action:none;cursor:crosshair}#glass{position:absolute;inset:0;pointer-events:none;transition:opacity .45s ease;opacity:1}#glass.idle{opacity:.28}#topbar,#bottomBar,#previewCard{pointer-events:auto;position:absolute;background:var(--glass);border:1px solid var(--line);box-shadow:0 14px 50px #0005;backdrop-filter:blur(18px);-webkit-backdrop-filter:blur(18px)}#topbar{top:18px;left:18px;right:18px;display:flex;align-items:center;justify-content:space-between;gap:18px;padding:12px 15px;border-radius:16px}.brand{display:flex;align-items:center;gap:10px;min-width:190px}.mark{width:28px;height:28px;border-radius:9px;background:linear-gradient(140deg,#ff9c7a,#bc83ff 55%,#54d9d1);display:grid;place-items:center;color:#171322;font-size:16px;font-weight:900}.brand-title{font-size:12px;font-weight:750;letter-spacing:.14em;text-transform:uppercase}.brand-sub{font-size:10px;color:var(--muted);margin-top:2px;letter-spacing:.06em}.readouts{display:flex;gap:22px;align-items:center;justify-content:center;flex:1}.metric{min-width:78px}.metric label{display:block;font-size:9px;text-transform:uppercase;letter-spacing:.13em;color:#aaa8c0;margin-bottom:3px}.metric strong{font:500 12px ui-monospace,SFMono-Regular,monospace;color:#f1efff}.metric.zoom strong{font-size:15px;color:#fff}.actions{display:flex;gap:7px;align-items:center}.btn{border:1px solid var(--line);background:rgba(255,255,255,.065);padding:8px 11px;border-radius:10px;font-size:11px;cursor:pointer;transition:.18s}.btn:hover,.btn:focus-visible{background:rgba(255,255,255,.15);border-color:#ffffff55;outline:none}.btn:disabled{opacity:.35;cursor:default}.btn.primary{background:#b4a7ff;color:#171322;border-color:transparent;font-weight:700}.btn.primary:hover{background:#c4baff}.btn.active{background:#7565c9;border-color:#c2b5ff}.iconbtn{width:34px;padding:8px 0;font-size:15px}.bottom{position:absolute;left:18px;right:18px;bottom:18px;display:flex;align-items:end;justify-content:space-between;gap:14px}.panel{background:var(--glass);border:1px solid var(--line);box-shadow:0 14px 50px #0005;backdrop-filter:blur(18px);-webkit-backdrop-filter:blur(18px);border-radius:15px;padding:12px 14px;pointer-events:auto}.palette-panel{display:flex;align-items:center;gap:11px}.small-label{font-size:9px;color:#aaa8c0;letter-spacing:.12em;text-transform:uppercase;white-space:nowrap}.swatches{display:flex;gap:6px}.swatch{width:28px;height:24px;border:1px solid #ffffff35;border-radius:7px;cursor:pointer;position:relative;padding:0}.swatch[aria-pressed="true"]:after{content:"";position:absolute;inset:-3px;border:1px solid #fff;border-radius:9px}.sliders{display:flex;align-items:center;gap:8px;min-width:170px}.sliders input[type=range]{width:100px;accent-color:#b7aaff}.sliders output{font:10px ui-monospace,monospace;color:#ded9ff;min-width:32px}.right-tools{display:flex;align-items:end;gap:10px}.iter-panel{display:flex;align-items:center;gap:8px}.iter-panel input[type=range]{width:90px;accent-color:#b7aaff}.iter-number{font:10px ui-monospace,monospace;min-width:30px}.book-panel{display:flex;gap:7px;align-items:center}.book-panel select{max-width:135px;color:#eee;background:#242236;border:1px solid var(--line);border-radius:8px;padding:7px;font-size:11px}.book-panel input{width:110px;color:#eee;background:#242236;border:1px solid var(--line);border-radius:8px;padding:7px;font-size:11px}.book-panel input::placeholder{color:#9491a9}#previewCard{right:20px;bottom:98px;width:232px;padding:10px;border-radius:13px;transition:opacity .2s}.preview-head{display:flex;justify-content:space-between;align-items:center;font-size:9px;text-transform:uppercase;letter-spacing:.13em;color:#c9c5df;margin-bottom:7px}.preview-head span:last-child{color:#8e89a5}#preview{display:block;width:100%;height:132px;border-radius:8px;background:#05050b}.preview-c{font:9px ui-monospace,monospace;color:#aba6c4;margin-top:6px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}#selection{position:absolute;display:none;pointer-events:none;border:1px solid #fff;outline:1px solid #171522;box-shadow:0 0 0 9999px #05050b55;z-index:3}#selection:after{content:"";position:absolute;inset:3px;border:1px solid #ffffffa0}#reticle{position:absolute;display:none;width:19px;height:19px;border:1px solid #fff;border-radius:50%;transform:translate(-50%,-50%);pointer-events:none;z-index:2;box-shadow:0 0 0 1px #0008}#reticle:before,#reticle:after{content:"";position:absolute;background:#fff}#reticle:before{width:31px;height:1px;left:-7px;top:8px}#reticle:after{height:31px;width:1px;top:-7px;left:8px}.toast{position:absolute;left:50%;top:78px;transform:translateX(-50%);padding:9px 14px;background:#171625dd;border:1px solid var(--line);border-radius:12px;color:#e8e4ff;font-size:11px;opacity:0;transition:opacity .2s;pointer-events:none;white-space:nowrap}.toast.show{opacity:1}.help{position:absolute;right:20px;top:80px;color:#d4d0e3a8;font-size:10px;text-align:right;line-height:1.7;pointer-events:none}.progress{position:absolute;left:0;bottom:0;height:2px;background:linear-gradient(90deg,#ff9c7a,#be88ff,#55d8d1);width:0;transition:width .12s;pointer-events:none}.hidden{display:none!important}
@media(max-width:850px){#topbar{left:10px;right:10px;top:10px;flex-wrap:wrap;padding:10px}.brand{min-width:0}.readouts{order:3;flex-basis:100%;justify-content:flex-start;gap:15px}.readouts .metric:nth-child(2){display:none}.bottom{left:10px;right:10px;bottom:10px;align-items:end}.panel{padding:9px}.palette-panel{gap:7px}.swatch{width:24px;height:22px}.small-label{font-size:8px}.sliders{min-width:120px}.sliders input[type=range]{width:70px}.book-panel input{display:none}.right-tools{gap:6px}.iter-panel input[type=range]{width:60px}#previewCard{right:12px;bottom:88px;width:190px}#preview{height:105px}.help{display:none}}
@media(max-width:560px){.brand-sub{display:none}.brand-title{font-size:10px}.actions .btn:not(.iconbtn):not(.primary){display:none}.readouts{gap:10px}.metric{min-width:auto}.bottom{flex-wrap:wrap}.palette-panel{flex:1;justify-content:space-between}.right-tools{width:100%;justify-content:space-between}.book-panel select{max-width:110px}.iter-panel{flex:1}.iter-panel input[type=range]{flex:1}#previewCard{bottom:125px}}
</style>
</head>
<body>
<div id="stage">
<canvas id="view" aria-label="Interactive fractal canvas"></canvas>
<div id="selection"></div><div id="reticle"></div>
<div id="glass">
<header id="topbar">
<div class="brand"><div class="mark">∞</div><div><div class="brand-title">Fractal Field</div><div class="brand-sub" id="modeLabel">MANDELBROT EXPLORER</div></div></div>
<div class="readouts">
<div class="metric zoom"><label>Magnification</label><strong id="zoomRead">×1.0</strong></div>
<div class="metric"><label>Center · real</label><strong id="reRead">−0.500000</strong></div>
<div class="metric"><label>Center · imaginary</label><strong id="imRead">0.000000</strong></div>
<div class="metric"><label>Iterations / FPS</label><strong><span id="iterRead">180</span> · <span id="fpsRead">—</span></strong></div>
</div>
<div class="actions">
<button class="btn iconbtn" id="backBtn" title="Back (Alt+Left)" aria-label="Back" disabled>↶</button>
<button class="btn" id="resetBtn" title="Reset view (0)">Reset</button>
<button class="btn" id="modeBtn" title="Switch between Mandelbrot and Julia (J)">Julia mode</button>
<button class="btn primary" id="navBtn" title="Toggle selection and pan mode">Select zoom</button>
</div>
</header>
<div class="help">DRAG · select zoom SHIFT+DRAG · pan<br>WHEEL · zoom to cursor J · Julia</div>
<div id="previewCard"><div class="preview-head"><span>Julia preview</span><span>LIVE c</span></div><canvas id="preview" width="220" height="126"></canvas><div class="preview-c" id="previewC">Move over the Mandelbrot plane</div></div>
<div class="bottom">
<div class="panel palette-panel"><span class="small-label">Palette</span><div class="swatches" id="swatches" role="group" aria-label="Color palette"></div><div class="sliders"><span class="small-label">Drift</span><input id="drift" type="range" min="0" max="1" step="0.001" value="0" aria-label="Palette color offset"><output id="driftOut">0%</output></div></div>
<div class="right-tools">
<div class="panel iter-panel"><label class="small-label" for="autoIter">Auto iter</label><input id="autoIter" type="checkbox" checked aria-label="Automatic iteration limit"><input id="iterSlider" type="range" min="80" max="1600" step="20" value="180" aria-label="Manual iteration limit"><span class="iter-number" id="iterValue">180</span></div>
<div class="panel book-panel"><input id="bookmarkName" maxlength="24" placeholder="Name this view" aria-label="Bookmark name"><button class="btn" id="saveBtn">Save</button><select id="bookmarks" aria-label="Saved views"><option value="">Bookmarks…</option></select><button class="btn" id="loadBtn">Load</button></div>
</div>
</div>
<div class="toast" id="toast"></div><div class="progress" id="progress"></div>
</div>
</div>
<script>
'use strict';
/* ===================== TUNABLE CONSTANTS ===================== */
const CONFIG={BASE_ITER:180,ITER_PER_OCTAVE:42,MAX_ITER:1600,MAX_MAGNIFICATION:1e13,ZOOM_SPEED:.0012,PROGRESSIVE_PASSES:[4,2,1],ROWS_PER_FRAME:7,PREVIEW_DELAY:100,MAX_DPR:1.5};
/* ===================== PALETTES ===================== */
const PALETTES=[
{name:'Ember',stops:[[0,[8,5,18]],[.13,[45,9,35]],[.32,[180,31,35]],[.52,[255,119,44]],[.72,[255,224,119]],[.9,[116,20,80]],[1,[8,5,18]]]},
{name:'Abyss',stops:[[0,[4,8,21]],[.16,[8,31,73]],[.36,[15,112,153]],[.58,[72,229,210]],[.78,[164,217,255]],[.92,[76,62,174]],[1,[4,8,21]]]},
{name:'Nocturne',stops:[[0,[0,0,0]],[.18,[35,35,48]],[.4,[112,116,130]],[.65,[220,218,208]],[.84,[99,100,111]],[1,[0,0,0]]]},
{name:'Prism',stops:[[0,[30,4,75]],[.16,[26,52,192]],[.34,[0,220,212]],[.52,[91,242,76]],[.68,[250,226,41]],[.84,[255,48,120]],[1,[30,4,75]]]},
{name:'Amethyst',stops:[[0,[6,4,17]],[.18,[50,20,107]],[.38,[122,60,200]],[.58,[235,117,189]],[.77,[247,209,255]],[.91,[69,31,135]],[1,[6,4,17]]]}
];
/* ===================== DOM / VIEW STATE ===================== */
const canvas=document.getElementById('view'),ctx=canvas.getContext('2d',{alpha:false,desynchronized:true}),stage=document.getElementById('stage');
const ui={zoom:document.getElementById('zoomRead'),re:document.getElementById('reRead'),im:document.getElementById('imRead'),iter:document.getElementById('iterRead'),fps:document.getElementById('fpsRead'),mode:document.getElementById('modeLabel'),progress:document.getElementById('progress'),selection:document.getElementById('selection'),reticle:document.getElementById('reticle'),toast:document.getElementById('toast'),previewCard:document.getElementById('previewCard'),preview:document.getElementById('preview'),previewC:document.getElementById('previewC')};
const view={re:-.5,im:0,scale:3.5,mode:'mandelbrot',cRe:-.12,cIm:.74,palette:0,drift:0,manualIter:180,auto:true};
let history=[],W=0,H=0,dpr=1,image=null,pixels=null,iterBuffer=null,renderToken=0,renderBusy=false,renderFrame=0,activePass=0,activeRow=0,lastActivity=Date.now(),previewTimer=0,previewPoint={re:0,im:0},lastFpsAt=performance.now(),fpsFrames=0,navMode='select',pointer=null,selectionStart=null,lastPointer={x:0,y:0},bookmarkStore={};
/* ===================== MATH / COLOR CORE ===================== */
function iterationLimit(){if(!view.auto)return view.manualIter;const mag=3.5/view.scale;return Math.max(CONFIG.BASE_ITER,Math.min(CONFIG.MAX_ITER,Math.round(CONFIG.BASE_ITER+CONFIG.ITER_PER_OCTAVE*Math.log2(Math.max(1,mag)))))}
function smoothIterations(re,im,mode=view.mode,cRe=view.cRe,cIm=view.cIm,max=iterationLimit()){
// Mandelbrot main-cardioid and period-2 bulb tests are exact interior shortcuts.
if(mode!=='julia'){const q=(re-.25)*(re-.25)+im*im;if(q*(q+(re-.25))<=.25*im*im||(re+1)*(re+1)+im*im<=.0625)return -1}
let zr=mode==='julia'?re:0,zi=mode==='julia'?im:0,cr=mode==='julia'?cRe:re,ci=mode==='julia'?cIm:im,n=0,zr2=zr*zr,zi2=zi*zi;
while(zr2+zi2<=4&&n<max){const nextZi=2*zr*zi+ci;zr=zr2-zi2+cr;zi=nextZi;zr2=zr*zr;zi2=zi*zi;n++}
if(zr2+zi2<=4&&n>=max)return -1;
const mag2=zr2+zi2;return n+1-Math.log2(Math.log(Math.sqrt(mag2)));
}
function colorAt(smooth,outIndex,data,offset=view.drift,palette=view.palette){
if(smooth<0){data[outIndex]=3;data[outIndex+1]=3;data[outIndex+2]=7;data[outIndex+3]=255;return}
const t=((smooth*.022+offset)%1+1)%1,stops=PALETTES[palette].stops;let k=1;while(k<stops.length&&t>stops[k][0])k++;const a=stops[k-1],b=stops[Math.min(k,stops.length-1)],f=(t-a[0])/(b[0]-a[0]||1);data[outIndex]=a[1][0]+(b[1][0]-a[1][0])*f;data[outIndex+1]=a[1][1]+(b[1][1]-a[1][1])*f;data[outIndex+2]=a[1][2]+(b[1][2]-a[1][2])*f;data[outIndex+3]=255;
}
function paintCell(x,y,step,val){const xEnd=Math.min(W,x+step),yEnd=Math.min(H,y+step);for(let py=y;py<yEnd;py++){let idx=(py*W+x)*4;for(let px=x;px<xEnd;px++,idx+=4){iterBuffer[py*W+px]=val;colorAt(val,idx,pixels)}}}
function paintPoint(x,y,val){const idx=(y*W+x)*4;iterBuffer[y*W+x]=val;colorAt(val,idx,pixels)}
function complexAt(clientX,clientY){const r=canvas.getBoundingClientRect(),x=(clientX-r.left)*W/r.width,y=(clientY-r.top)*H/r.height;return {re:view.re+(x-W/2)*view.scale/W,im:view.im-(y-H/2)*view.scale/W,x,y}}
/* ===================== PROGRESSIVE RENDERER ===================== */
function resize(){const r=canvas.getBoundingClientRect();dpr=Math.min(CONFIG.MAX_DPR,window.devicePixelRatio||1);W=Math.max(1,Math.round(r.width*dpr));H=Math.max(1,Math.round(r.height*dpr));canvas.width=W;canvas.height=H;image=ctx.createImageData(W,H);pixels=image.data;iterBuffer=new Float32Array(W*H);render()}
function cancelRender(){renderToken++;if(renderFrame)cancelAnimationFrame(renderFrame);renderFrame=0;renderBusy=false}
function render(){if(!W||!H)return;cancelRender();const token=renderToken;renderBusy=true;activePass=0;activeRow=0;pixels.fill(0);ctx.fillStyle='#05050b';ctx.fillRect(0,0,W,H);runPass(token)}
function runPass(token){if(token!==renderToken)return;if(activePass>=CONFIG.PROGRESSIVE_PASSES.length){renderBusy=false;renderFrame=0;ui.progress.style.width='100%';setTimeout(()=>{if(!renderBusy)ui.progress.style.width='0'},260);return}
const step=CONFIG.PROGRESSIVE_PASSES[activePass],rows=Math.ceil(H/step),start=activeRow,end=Math.min(rows,start+CONFIG.ROWS_PER_FRAME),max=iterationLimit();
for(let gy=start;gy<end;gy++){const y=gy*step,sy=Math.min(H-1,y+Math.floor(step/2));for(let x=0;x<W;x+=step){const sx=Math.min(W-1,x+Math.floor(step/2)),re=view.re+(sx-W/2)*view.scale/W,im=view.im-(sy-H/2)*view.scale/W,val=smoothIterations(re,im,view.mode,view.cRe,view.cIm,max);paintCell(x,y,step,val)}}
const dirtyY=start*step,dirtyH=Math.min(H-dirtyY,(end-start)*step);if(dirtyH>0)ctx.putImageData(image,0,0,0,dirtyY,W,dirtyH);
activeRow=end;const totalRows=CONFIG.PROGRESSIVE_PASSES.reduce((s,n)=>s+Math.ceil(H/n),0),done=CONFIG.PROGRESSIVE_PASSES.slice(0,activePass).reduce((s,n)=>s+Math.ceil(H/n),0)+activeRow;ui.progress.style.width=(done/totalRows*100)+'%';fpsFrames++;
if(activeRow>=rows){activePass++;activeRow=0}
renderFrame=requestAnimationFrame(()=>runPass(token));
}
function recolor(){if(!iterBuffer||!pixels)return;for(let p=0,i=0;p<iterBuffer.length;p++,i+=4)colorAt(iterBuffer[p],i,pixels);ctx.putImageData(image,0,0)}
function changed(push=true){if(push){history.push(snapshot());if(history.length>60)history.shift();document.getElementById('backBtn').disabled=false}render();updateReadouts();wakeHud();updateHash()}
/* ===================== HUD / STATE SERIALIZATION ===================== */
function snapshot(){return {re:view.re,im:view.im,scale:view.scale,mode:view.mode,cRe:view.cRe,cIm:view.cIm,palette:view.palette,drift:view.drift,manualIter:view.manualIter,auto:view.auto}}
function restore(s,push=false){if(!s)return;if(push)history.push(snapshot());Object.assign(view,s);view.scale=Math.max(3.5/CONFIG.MAX_MAGNIFICATION,Math.min(3.5,view.scale));syncControls();render();updateReadouts();updateHash();wakeHud()}
function fmtCoord(v){if(Math.abs(v)>0&&Math.abs(v)<1e-5)return v.toExponential(5);return v.toFixed(6)}
function updateReadouts(){const mag=3.5/view.scale;ui.zoom.textContent='×'+(mag<1e4?mag.toFixed(mag<10?1:0):mag.toExponential(1));ui.re.textContent=fmtCoord(view.re);ui.im.textContent=fmtCoord(view.im);ui.iter.textContent=iterationLimit();ui.mode.textContent=view.mode==='julia'?'JULIA SET · c='+fmtCoord(view.cRe)+' '+(view.cIm<0?'−':'+')+' '+fmtCoord(Math.abs(view.cIm))+'i':'MANDELBROT EXPLORER';document.title=(view.mode==='julia'?'Julia':'Mandelbrot')+' · Fractal Field'}
function syncControls(){document.getElementById('iterSlider').value=view.manualIter;document.getElementById('iterValue').textContent=view.manualIter;document.getElementById('autoIter').checked=view.auto;document.getElementById('drift').value=view.drift;document.getElementById('driftOut').textContent=Math.round(view.drift*100)+'%';document.querySelectorAll('.swatch').forEach((b,i)=>b.setAttribute('aria-pressed',i===view.palette?'true':'false'));document.getElementById('modeBtn').textContent=view.mode==='julia'?'Mandelbrot mode':'Julia mode';ui.previewCard.classList.toggle('hidden',view.mode==='julia');updateReadouts()}
function updateHash(){try{const s=snapshot();location.hash='v='+btoa(unescape(encodeURIComponent(JSON.stringify(s)))).replace(/=+$/,'')}catch(e){}}
function loadHash(){try{if(!location.hash.startsWith('#v='))return;const s=JSON.parse(decodeURIComponent(escape(atob(location.hash.slice(3)))));restore(s)}catch(e){}}
function wakeHud(){lastActivity=Date.now();document.getElementById('glass').classList.remove('idle')}
setInterval(()=>{if(Date.now()-lastActivity>5500)document.getElementById('glass').classList.add('idle');const now=performance.now();if(now-lastFpsAt>900){ui.fps.textContent=Math.round(fpsFrames*1000/(now-lastFpsAt));fpsFrames=0;lastFpsAt=now}},500);
let toastTimer=0;function toast(msg){ui.toast.textContent=msg;ui.toast.classList.add('show');clearTimeout(toastTimer);toastTimer=setTimeout(()=>ui.toast.classList.remove('show'),2200)}
/* ===================== NAVIGATION ===================== */
function clampScale(s){return Math.max(3.5/CONFIG.MAX_MAGNIFICATION,Math.min(3.5,s))}
function zoomAt(clientX,clientY,factor){const before=complexAt(clientX,clientY),next=clampScale(view.scale*factor),r=canvas.getBoundingClientRect(),px=(clientX-r.left)*W/r.width,py=(clientY-r.top)*H/r.height;view.scale=next;view.re=before.re-(px-W/2)*next/W;view.im=before.im+(py-H/2)*next/W;changed();if(next<=3.5/CONFIG.MAX_MAGNIFICATION+1e-26)toast('Maximum magnification reached · double precision limit')}
canvas.addEventListener('wheel',e=>{e.preventDefault();wakeHud();zoomAt(e.clientX,e.clientY,Math.exp(e.deltaY*CONFIG.ZOOM_SPEED))},{passive:false});
function startPan(e){pointer={id:e.pointerId,type:'pan',x:e.clientX,y:e.clientY};canvas.setPointerCapture(e.pointerId);lastPointer={x:e.clientX,y:e.clientY}}
canvas.addEventListener('pointerdown',e=>{wakeHud();if(e.button===2||e.shiftKey||navMode==='pan'){startPan(e);return}const p=complexAt(e.clientX,e.clientY);pointer={id:e.pointerId,type:'select',x:e.clientX,y:e.clientY};selectionStart={x:e.clientX,y:e.clientY,p};canvas.setPointerCapture(e.pointerId);ui.selection.style.display='block';drawSelection(e.clientX,e.clientY)});
canvas.addEventListener('contextmenu',e=>e.preventDefault());
function drawSelection(x,y){if(!selectionStart)return;const r=canvas.getBoundingClientRect(),ratio=r.width/r.height;let dx=x-selectionStart.x,dy=y-selectionStart.y;if(Math.abs(dx)<2&&Math.abs(dy)<2){ui.selection.style.width='0';ui.selection.style.height='0';return}const signX=dx<0?-1:1,signY=dy<0?-1:1;let w=Math.abs(dx),h=Math.abs(dy);if(w/h>ratio)h=w/ratio;else w=h*ratio;const x2=selectionStart.x+signX*w,y2=selectionStart.y+signY*h;const left=Math.min(selectionStart.x,x2)-r.left,top=Math.min(selectionStart.y,y2)-r.top;Object.assign(ui.selection.style,{left:left+'px',top:top+'px',width:w+'px',height:h+'px'})}
canvas.addEventListener('pointermove',e=>{const p=complexAt(e.clientX,e.clientY);if(view.mode==='mandelbrot'){previewPoint={re:p.re,im:p.im};positionReticle(e.clientX,e.clientY);schedulePreview()}if(!pointer||pointer.id!==e.pointerId)return;if(pointer.type==='pan'){const r=canvas.getBoundingClientRect(),dx=e.clientX-pointer.x,dy=e.clientY-pointer.y;view.re-=dx/r.width*view.scale;view.im+=dy/r.width*view.scale;pointer.x=e.clientX;pointer.y=e.clientY;changed(false)}else drawSelection(e.clientX,e.clientY)});
function positionReticle(x,y){const r=canvas.getBoundingClientRect();ui.reticle.style.left=(x-r.left)+'px';ui.reticle.style.top=(y-r.top)+'px';ui.reticle.style.display='block'}
canvas.addEventListener('pointerleave',()=>{if(!pointer)ui.reticle.style.display='none'});
canvas.addEventListener('pointerup',e=>{if(!pointer||pointer.id!==e.pointerId)return;const type=pointer.type;if(type==='select'&&selectionStart){const r=canvas.getBoundingClientRect(),dx=Math.abs(e.clientX-selectionStart.x),dy=Math.abs(e.clientY-selectionStart.y),w=Math.max(dx,dy*r.width/r.height);if(w>8){const h=w*r.height/r.width,sgnX=e.clientX<selectionStart.x?-1:1,sgnY=e.clientY<selectionStart.y?-1:1,x2=selectionStart.x+sgnX*w,y2=selectionStart.y+sgnY*h;const left=Math.min(selectionStart.x,x2)-r.left,top=Math.min(selectionStart.y,y2)-r.top;const centerPixelX=(left+w/2)*W/r.width,centerPixelY=(top+h/2)*H/r.height;history.push(snapshot());view.re=selectionStart.p.re+(centerPixelX-(selectionStart.p.x))*view.scale/W;view.im=selectionStart.p.im-(centerPixelY-(selectionStart.p.y))*view.scale/W;view.scale=clampScale(view.scale*w/r.width);changed(false)}ui.selection.style.display='none';selectionStart=null}pointer=null});
canvas.addEventListener('pointercancel',()=>{pointer=null;selectionStart=null;ui.selection.style.display='none'});
function schedulePreview(){if(view.mode!=='mandelbrot')return;clearTimeout(previewTimer);previewTimer=setTimeout(renderPreview,CONFIG.PREVIEW_DELAY)}
function renderPreview(){const c=ui.preview,cc=c.getContext('2d'),w=c.width,h=c.height,id=cc.createImageData(w,h),d=id.data,pr=previewPoint.re,pi=previewPoint.im,max=190;ui.previewC.textContent='c = '+fmtCoord(pr)+(pi<0?' − ':' + ')+fmtCoord(Math.abs(pi))+'i';for(let y=0;y<h;y+=2)for(let x=0;x<w;x+=2){const re=(x/w*3.2)-1.6,im=1.2-(y/h*2.4),v=smoothIterations(re,im,'julia',pr,pi,max);for(let yy=y;yy<Math.min(h,y+2);yy++)for(let xx=x;xx<Math.min(w,x+2);xx++)colorAt(v,(yy*w+xx)*4,d,0,3)}cc.putImageData(id,0,0)}
function toggleJulia(){if(view.mode==='mandelbrot'){view.cRe=previewPoint.re;view.cIm=previewPoint.im;view.mode='julia';view.re=0;view.im=0;view.scale=3.2}else{view.mode='mandelbrot';view.re=-.5;view.im=0;view.scale=3.5}changed();syncControls()}
/* ===================== CONTROLS ===================== */
const swatches=document.getElementById('swatches');PALETTES.forEach((p,i)=>{const b=document.createElement('button');b.className='swatch';b.title=p.name;b.setAttribute('aria-label',p.name+' palette');b.setAttribute('aria-pressed',i===view.palette?'true':'false');b.style.background='linear-gradient(110deg,'+p.stops.slice(1,-1).map(s=>'rgb('+s[1].join(',')+') '+Math.round(s[0]*100)+'%').join(',')+')';b.addEventListener('click',()=>{view.palette=i;syncControls();recolor();wakeHud()});swatches.appendChild(b)});
document.getElementById('backBtn').addEventListener('click',()=>{if(history.length){const s=history.pop();restore(s);document.getElementById('backBtn').disabled=!history.length}});
document.getElementById('resetBtn').addEventListener('click',()=>{restore({re:-.5,im:0,scale:3.5,mode:'mandelbrot',cRe:-.12,cIm:.74,palette:view.palette,drift:view.drift,manualIter:view.manualIter,auto:view.auto},true)});
document.getElementById('modeBtn').addEventListener('click',toggleJulia);
document.getElementById('navBtn').addEventListener('click',e=>{navMode=navMode==='select'?'pan':'select';e.currentTarget.textContent=navMode==='select'?'Select zoom':'Pan view';e.currentTarget.classList.toggle('active',navMode==='pan')});
document.getElementById('drift').addEventListener('input',e=>{view.drift=+e.target.value;document.getElementById('driftOut').textContent=Math.round(view.drift*100)+'%';recolor();wakeHud()});
document.getElementById('autoIter').addEventListener('change',e=>{view.auto=e.target.checked;updateReadouts();render()});
document.getElementById('iterSlider').addEventListener('input',e=>{view.manualIter=+e.target.value;document.getElementById('iterValue').textContent=view.manualIter;updateReadouts();if(!view.auto)render()});
document.getElementById('saveBtn').addEventListener('click',()=>{const name=document.getElementById('bookmarkName').value.trim()||'View '+(Object.keys(bookmarkStore).length+1);bookmarkStore[name]=snapshot();try{localStorage.setItem('fractal-field-bookmarks',JSON.stringify(bookmarkStore))}catch(e){}populateBookmarks(name);toast('Saved “'+name+'”')});
document.getElementById('loadBtn').addEventListener('click',()=>{const name=document.getElementById('bookmarks').value;if(name&&bookmarkStore[name])restore(bookmarkStore[name],true)});
function populateBookmarks(selected=''){const sel=document.getElementById('bookmarks');sel.innerHTML='<option value="">Bookmarks…</option>';Object.keys(bookmarkStore).forEach(n=>{const o=document.createElement('option');o.value=n;o.textContent=n;sel.appendChild(o)});if(selected)sel.value=selected}
try{bookmarkStore=JSON.parse(localStorage.getItem('fractal-field-bookmarks')||'{}')||{}}catch(e){bookmarkStore={}}populateBookmarks();
/* ===================== KEYBOARD ACCESS ===================== */
window.addEventListener('keydown',e=>{if(e.target.matches('input,select,button')){if(e.key==='Escape')e.target.blur();else return}wakeHud();const step=view.scale*.12;if(e.key==='ArrowLeft'){view.re-=step;changed()}else if(e.key==='ArrowRight'){view.re+=step;changed()}else if(e.key==='ArrowUp'){view.im+=step;changed()}else if(e.key==='ArrowDown'){view.im-=step;changed()}else if(e.key==='+'||e.key==='='){zoomAt(innerWidth/2,innerHeight/2,.78)}else if(e.key==='-'){zoomAt(innerWidth/2,innerHeight/2,1/.78)}else if(e.key.toLowerCase()==='j'){toggleJulia()}else if(e.key==='0'){restore({re:-.5,im:0,scale:3.5,mode:'mandelbrot',cRe:-.12,cIm:.74,palette:view.palette,drift:view.drift,manualIter:view.manualIter,auto:view.auto},true)}else if(e.key==='Escape'){ui.selection.style.display='none';selectionStart=null;pointer=null}});
window.addEventListener('resize',resize);window.addEventListener('hashchange',()=>{if(location.hash.startsWith('#v='))loadHash()});
/* ===================== DEBUG / PROBE INTERFACE ===================== */
window.fractal={iterationsAt(re,im,options={}){const max=options.max||iterationLimit();const value=smoothIterations(re,im,options.mode||'mandelbrot',options.cRe??view.cRe,options.cIm??view.cIm,max);return value<0?max:value},setView(next){history.push(snapshot());if(Number.isFinite(next.re))view.re=next.re;if(Number.isFinite(next.im))view.im=next.im;if(Number.isFinite(next.scale))view.scale=clampScale(next.scale);if(next.mode)view.mode=next.mode;if(Number.isFinite(next.cRe))view.cRe=next.cRe;if(Number.isFinite(next.cIm))view.cIm=next.cIm;if(Number.isInteger(next.palette))view.palette=Math.max(0,Math.min(PALETTES.length-1,next.palette));if(Number.isFinite(next.maxIterations)){view.auto=false;view.manualIter=Math.max(1,Math.min(CONFIG.MAX_ITER,next.maxIterations))}syncControls();changed(false);return snapshot()},getView(){return snapshot()},get maxIterations(){return iterationLimit()},get canvas(){return {width:W,height:H}},selectRect(x1,y1,x2,y2){const rect=canvas.getBoundingClientRect(),before=snapshot(),start=complexAt(rect.left+x1,rect.top+y1);const dx=Math.abs(x2-x1),dy=Math.abs(y2-y1),w=Math.max(dx,dy*rect.width/rect.height);if(w>8){const h=w*rect.height/rect.width,signX=x2<x1?-1:1,signY=y2<y1?-1:1,left=Math.min(x1,x1+signX*w),top=Math.min(y1,y1+signY*h),centerX=left+w/2,centerY=top+h/2;history.push(before);view.re=start.re+(centerX-x1)*view.scale/W;view.im=start.im-(centerY-y1)*view.scale/W;view.scale=clampScale(view.scale*w/rect.width);changed(false)}return snapshot()},saveBookmark(name){bookmarkStore[name]=snapshot();localStorage.setItem('fractal-field-bookmarks',JSON.stringify(bookmarkStore));populateBookmarks(name);return true},loadBookmark(name){if(!bookmarkStore[name])return false;restore(bookmarkStore[name]);return snapshot()},getRenderState(){return {busy:renderBusy,token:renderToken,pass:activePass,row:activeRow}}};
resize();syncControls();loadHash();schedulePreview();
</script>
</body>
</html>
<!-- agent-meta {"model":"openai/gpt-6-luna","provider":"openrouter","persona":"full","sessionId":"bd13fd77-43f4-4348-94e6-5941d8ff41bd","tokensIn":1323420,"tokensOut":32271,"tokensTotal":1355691,"cost":0.034503219999999994,"turns":44,"toolCalls":43,"failedToolCalls":8,"timestamp":"2026-09-22T23:58:34.600Z"} -->