This commit is contained in:
33333-33333 2026-08-22 20:55:27 +09:00
commit caa90775f9
89 changed files with 7126 additions and 0 deletions

11
dist/hosted/_headers vendored Normal file
View file

@ -0,0 +1,11 @@
/*
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
X-Content-Type-Options: nosniff
/wasm/*
Content-Type: application/wasm
Cache-Control: public, max-age=31536000, immutable
/hosted/*
Cache-Control: no-cache

36
dist/hosted/hosted-loader.js vendored Normal file
View file

@ -0,0 +1,36 @@
const wasmRoot = new URL('../wasm/', import.meta.url);
async function compileAsset(name) {
const url = new URL(name, wasmRoot);
const response = await fetch(url, { cache: 'force-cache' });
if (!response.ok) throw new Error(`WASM fetch failed: ${name} (${response.status})`);
if (WebAssembly.compileStreaming) {
try { return await WebAssembly.compileStreaming(Promise.resolve(response.clone())); }
catch { /* A proxy may have supplied the wrong MIME type; use bytes below. */ }
}
return WebAssembly.compile(await response.arrayBuffer());
}
let shallowModule, shallowSimd = true;
try { shallowModule = await compileAsset('wasm-simd.d19b26c04e1b2f59.wasm'); }
catch { shallowModule = await compileAsset('wasm-scalar.d19b26c04e1b2f59.wasm'); shallowSimd = false; }
const asset = name => new URL(name, wasmRoot).href;
globalThis.MANDEL_KERNELS = Object.freeze({
// The compiled module is structured-cloned to the shallow Worker. No Base64
// conversion or second compile is needed in the hosted build.
WASM_SIMD_B64: shallowModule,
WASM_SCALAR_B64: shallowModule,
DEEP_SIMD_B64: asset('deep-simd.d49385f06e4a8f55.wasm'),
DEEP_SCALAR_B64: asset('deep-scalar.d133bbecc8f6dbdf.wasm'),
BLA_SIMD_B64: asset('bla-simd.f80f136e9fb676ce.wasm'),
BLA_SCALAR_B64: asset('bla-scalar.4f06913487044823.wasm'),
COLOR_SIMD_B64: asset('color-simd.8cf83374ed0c680b.wasm'),
COLOR_SCALAR_B64: asset('color-scalar.aa2914ec1acacaa2.wasm')
});
globalThis.MANDEL_KERNEL_META = Object.freeze({"WASM_SIMD_B64":"d19b26c04e1b2f59ee1e9c8f6df0ceb9d6b2a56d08b906c0f3b8d9e10903460b","WASM_SCALAR_B64":"d19b26c04e1b2f59ee1e9c8f6df0ceb9d6b2a56d08b906c0f3b8d9e10903460b","DEEP_SIMD_B64":"d49385f06e4a8f55c82fc8b4ecf7beb26313624dbcc038301a33a3eaa3465214","DEEP_SCALAR_B64":"d133bbecc8f6dbdf4fdce2b400044ae50e34589c3cb1d612f4c8d644cbddffc2","BLA_SIMD_B64":"f80f136e9fb676ce65b2ae53be4ccfdc65712e623a5a06312645a025428e0c1e","BLA_SCALAR_B64":"4f0691348704482331e48cb0739500fc2e716bc1f6a5c3b8f10ac5f2d54e985f","COLOR_SIMD_B64":"8cf83374ed0c680b9f0cd3619c736689680fbc8c3b5475c0aa20b45073b01d03","COLOR_SCALAR_B64":"aa2914ec1acacaa29ac2408118c320235657a7b807b9a60fd7abb49ba3424d90"});
globalThis.MANDEL_HOSTED_SHALLOW_SIMD = shallowSimd;
const app = document.createElement('script');
app.src = './script.js';
document.body.appendChild(app);

69
dist/hosted/index.html vendored Normal file
View file

@ -0,0 +1,69 @@
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
<meta name="theme-color" content="#050813">
<title>Mandelbrot Deep Zoom v23</title>
<style>
:root{color-scheme:dark;--panel:rgba(7,12,25,.88);--line:rgba(255,255,255,.12);--text:#f7f8ff;--muted:#a9b3ca;--accent:#61dbe9}
*{box-sizing:border-box}html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#050813;font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}body{-webkit-user-select:none;user-select:none}
#view{position:fixed;inset:0;width:100%;height:100%;display:block;background:#050813;image-rendering:auto;touch-action:none}
.top{position:fixed;z-index:5;top:max(10px,env(safe-area-inset-top));left:10px;right:10px;display:flex;gap:8px;pointer-events:none}.brand,.stats,.panel,.toast{backdrop-filter:blur(18px) saturate(130%);-webkit-backdrop-filter:blur(18px) saturate(130%)}
.brand{pointer-events:auto;background:var(--panel);border:1px solid var(--line);border-radius:16px;padding:10px 14px;font-weight:850;letter-spacing:.04em;font-size:13px;box-shadow:0 12px 40px rgba(0,0,0,.32)}.brand small{display:block;margin-top:2px;color:var(--muted);font-size:10px;font-weight:600;letter-spacing:0}
.stats{margin-left:auto;max-width:min(560px,65vw);padding:9px 12px;border:1px solid var(--line);border-radius:14px;background:var(--panel);font:11px/1.45 ui-monospace,SFMono-Regular,Menlo,monospace;overflow:hidden}.row{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.muted{color:var(--muted)}
.panel{position:fixed;z-index:6;right:10px;bottom:max(10px,env(safe-area-inset-bottom));width:min(380px,calc(100vw - 20px));padding:11px;border:1px solid var(--line);border-radius:19px;background:var(--panel);box-shadow:0 18px 58px rgba(0,0,0,.44)}
.toolbar{display:grid;grid-template-columns:repeat(4,1fr);gap:7px}select{appearance:auto;border:1px solid rgba(255,255,255,.14);background:rgba(255,255,255,.07);color:var(--text);min-height:44px;padding:4px 8px;border-radius:10px;font-size:12px}#palette{color:#111;background:#f4f5f8}#palette option{color:#111;background:#fff}button{appearance:none;border:1px solid rgba(255,255,255,.14);background:rgba(255,255,255,.07);color:var(--text);min-height:44px;padding:7px 5px;border-radius:12px;font-size:12px;font-weight:760;cursor:pointer}button:active{transform:translateY(1px)}button.primary{background:linear-gradient(135deg,rgba(64,215,236,.25),rgba(139,78,255,.22));border-color:rgba(97,219,233,.48)}button.on{outline:1px solid rgba(97,219,233,.8)}button:focus-visible,select:focus-visible,input:focus-visible,#view:focus-visible{outline:3px solid #fff;outline-offset:2px}
.group{margin-top:10px;padding-top:9px;border-top:1px solid rgba(255,255,255,.08)}.line{display:grid;grid-template-columns:98px 1fr 48px;align-items:center;gap:8px;margin:7px 0}.line label{font-size:12px;color:#dce1ef}.line output{text-align:right;color:var(--muted);font:11px ui-monospace,monospace}input[type=range]{width:100%;min-height:44px;accent-color:var(--accent)}.checks{display:flex;gap:12px;flex-wrap:wrap;margin-top:8px;color:#dce1ef;font-size:12px}.checks label{display:flex;align-items:center;min-height:44px;gap:6px}
details{margin-top:9px;border-top:1px solid rgba(255,255,255,.08);padding-top:8px}summary{display:flex;align-items:center;min-height:44px;cursor:pointer;color:var(--muted);font-size:12px}.exact-grid{display:grid;grid-template-columns:54px 1fr;gap:6px;margin-top:8px}.exact-grid input{min-width:0;width:100%;min-height:44px;border:1px solid var(--line);border-radius:8px;background:#070c19;color:var(--text);padding:6px;font:11px ui-monospace,monospace}.mini-actions{display:flex;gap:6px;margin-top:7px}.mini-actions button{flex:1}
.diagnostics{margin-top:8px;color:var(--muted);font:10.5px/1.5 ui-monospace,monospace;white-space:pre-wrap;overflow-wrap:anywhere}.exact-grid input{user-select:text;-webkit-user-select:text}
.bottom{display:flex;align-items:center;justify-content:space-between;gap:8px}.badge{display:inline-flex;align-items:center;gap:6px;padding:4px 8px;border-radius:999px;background:rgba(255,255,255,.07);font-size:10px;color:#d9dfed}.dot{width:7px;height:7px;border-radius:50%;background:#61dbe9;box-shadow:0 0 12px #61dbe9}.hint{margin-top:8px;color:var(--muted);font-size:10.5px;line-height:1.45}
.toast{position:fixed;z-index:10;left:50%;bottom:24px;transform:translate(-50%,16px);opacity:0;transition:.18s;pointer-events:none;padding:9px 12px;border:1px solid var(--line);border-radius:12px;background:rgba(7,12,25,.95);font-size:12px}.toast.show{opacity:1;transform:translate(-50%,0)}
dialog{width:min(430px,calc(100vw - 24px));border:1px solid var(--line);border-radius:18px;background:#0b1120;color:var(--text);padding:16px;box-shadow:0 24px 80px #000}dialog::backdrop{background:rgba(0,0,0,.65)}dialog h2{font-size:16px;margin:0 0 12px}.export-grid{display:grid;grid-template-columns:130px 1fr;gap:10px;align-items:center}.export-grid label{font-size:12px}.export-grid input,.export-grid select{width:100%}.export-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:14px}progress{width:100%;margin-top:12px}
#uiToggle{position:fixed;z-index:20;left:max(10px,env(safe-area-inset-left));bottom:max(10px,env(safe-area-inset-bottom));min-width:52px;min-height:44px;padding:8px 12px;border-radius:999px;background:rgba(7,12,25,.78);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);box-shadow:0 8px 30px rgba(0,0,0,.3)}body.ui-hidden .top,body.ui-hidden .panel{display:none}body.ui-hidden #uiToggle{background:rgba(7,12,25,.7)}
.compact-status{display:none;position:fixed;z-index:4;right:8px;top:max(8px,env(safe-area-inset-top));max-width:58vw;padding:7px 10px;border:1px solid var(--line);border-radius:999px;background:var(--panel);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
@media(max-width:700px){.stats{display:none}.brand small{display:none}.panel{left:8px;right:8px;bottom:max(8px,env(safe-area-inset-bottom));width:auto;padding:10px;touch-action:pan-x pan-y pinch-zoom}.line{grid-template-columns:82px 1fr 42px}.hint{display:none}.compact-status{display:block}button,select{min-height:44px}}
@media(prefers-reduced-motion:reduce){.toast{transition:none}button:active{transform:none}}
@media(prefers-reduced-transparency:reduce){.brand,.stats,.panel,.toast,#uiToggle,.compact-status{backdrop-filter:none;-webkit-backdrop-filter:none;background:#0b1120}}
</style>
</head>
<body>
<canvas id="view" tabindex="0" role="img" aria-label="マンデルブロ集合。矢印キーで移動、Enterで拡大、Shift+Enterで縮小できます"></canvas>
<div class="top"><div class="brand">MANDELBROT DEEP ZOOM</div><div class="stats" role="status" aria-live="polite" aria-atomic="true"><div class="row"><span class="muted">中心</span> <span id="coord">…</span></div><div class="row"><span class="muted">倍率</span> <span id="zoom">…</span> <span class="muted">表示幅</span> <span id="span">…</span></div><div class="row"><span class="muted">計算</span> <span id="engine">起動中…</span> <span class="muted">描画</span> <span id="render">…</span></div></div></div>
<div id="compactStatus" class="compact-status" role="status" aria-live="polite">起動中</div>
<div id="controls" class="panel">
<div class="toolbar"><button id="zin" aria-label="中心を拡大">+</button><button id="zout" aria-label="中心を縮小">−</button><button id="reset">リセット</button><button id="png">出力</button></div>
<div class="group">
<div class="line"><label for="processMode">処理モード</label><select id="processMode"><option value="power">省電力</option><option value="standard" selected>標準</option><option value="fine">精細</option><option value="validate">精度優先</option></select><output></output></div>
<div class="line"><label for="palette">彩色</label><select id="palette"><option value="0">昼夜</option><option value="1">虹色</option><option value="2">白黒</option></select><output></output></div>
<div class="line"><label for="cycle">色周期</label><input id="cycle" type="range" min="0.001" max="0.05" step="0.0005" value="0.008"><output id="cycleO" for="cycle">0.0080</output></div>
<div class="line"><label for="shift">色相位置</label><input id="shift" type="range" min="0" max="1" step="0.005" value="0.18"><output id="shiftO" for="shift">.18</output></div>
<details><summary>詳細設定・正確な座標</summary>
<div class="line"><label for="iters">基準反復</label><input id="iters" type="range" min="100" max="2500" step="25" value="350"><output id="itersO" for="iters">350</output></div>
<div class="checks"><label><input id="adaptive" type="checkbox" checked> 反復回数を自動調整</label><label><input id="hq" type="checkbox"> 境界AAを追加</label></div>
<div class="exact-grid"><label for="coordReInput">実部</label><input id="coordReInput"><label for="coordImInput">虚部</label><input id="coordImInput"><label for="coordSpanInput">表示幅</label><input id="coordSpanInput"></div>
<div class="mini-actions"><button id="coordApply">座標を適用</button><button id="coordCopy">正確値をコピー</button><button id="undoView">戻す</button><button id="redoView">進む</button></div>
</details>
<details><summary>診断情報</summary><div class="diagnostics"><div id="diagEngine">engine: …</div><div id="diagNumeric">numeric: …</div><div id="diagMemory">memory: …</div></div></details>
</div>
<div class="group bottom"><span class="badge"><span class="dot"></span><span id="badge">起動中</span></span><div style="display:flex;gap:6px"><button id="share">URL共有</button></div></div>
<div class="hint">ホイール / ピンチでズーム、ドラッグで移動。HキーでUI表示を切り替え。</div>
</div>
<button id="uiToggle" title="UIを隠す / 表示" aria-controls="controls" aria-expanded="true">UI−</button>
<div id="toast" class="toast" role="status" aria-live="polite"></div>
<dialog id="exportDialog" aria-labelledby="exportTitle">
<h2 id="exportTitle">高解像度 PNG 出力</h2>
<div class="export-grid">
<label for="exportScale">出力倍率</label><select id="exportScale"><option value="1">1×</option><option value="2">2×</option><option value="4">4×</option><option value="0">カスタム幅</option></select>
<label for="exportWidth">幅(px)</label><input id="exportWidth" type="number" min="64" max="16384" step="1">
<label for="exportAA">サブサンプル</label><select id="exportAA"><option value="1">1×(高速)</option><option value="2">2×2 AA</option></select>
<label for="exportPrecision">精度方針</label><select id="exportPrecision"><option value="balanced">Balanced</option><option value="validated">Validated direct</option></select>
</div>
<progress id="exportProgress" max="1" value="0" hidden></progress>
<div id="exportStatus" role="status" aria-live="polite"></div>
<div class="export-actions"><button id="exportCancel" type="button">閉じる</button><button id="exportQuick" type="button">表示を即時保存</button><button id="exportStart" class="primary" type="button">PNGを生成</button></div>
</dialog>
<script type="module" src="./hosted-loader.js"></script>
</body>
</html>

912
dist/hosted/script.js vendored Normal file
View file

@ -0,0 +1,912 @@
(()=>{'use strict';
const K=globalThis.MANDEL_KERNELS;
if(!K)throw new Error('kernels.js が読み込まれていません');
const {WASM_SIMD_B64,WASM_SCALAR_B64,DEEP_SIMD_B64,DEEP_SCALAR_B64,BLA_SIMD_B64,BLA_SCALAR_B64,COLOR_SIMD_B64,COLOR_SCALAR_B64}=K;
const $=s=>document.querySelector(s);
const canvas=$('#view');
const ctx=canvas.getContext('2d',{alpha:false,desynchronized:true})||canvas.getContext('2d',{alpha:false});
if(!ctx){document.body.innerHTML='<div style="padding:30px;color:white">Canvas 2Dを利用できません。</div>';return;}
const INITIAL_BITS=256;
const MIN_SPAN_BITS=224;
const TARGET_SPAN_BITS=240;
const RATIO_DEN=4503599627370496n; // 2^52
const POW256=1.157920892373162e77;
const INV256=8.636168555094445e-78;
const HI128=3.402823669209385e38;
const LO128=2.938735877055719e-39;
const NEG_BUCKET=-1000000000;
const LEGACY_DEEP_ZOOM_THRESHOLD=11.5;
const RENDER_PASS=Object.freeze({PREVIEW:'preview',COVERED:'covered'});
const MODE_TARGET=Object.freeze({power:'PREVIEW',standard:'COVERED',fine:'REFINED',validate:'VALIDATED'});
function modeTarget(mode=state.processMode){return MODE_TARGET[mode]||MODE_TARGET.standard}
const RENDER_PROFILE=Object.freeze({
[RENDER_PASS.PREVIEW]:Object.freeze({id:RENDER_PASS.PREVIEW,covered:false,budgetMs:110,nominalScale:.42,minWidth:360,maxWidth:900,densityLimit:1.55,blaSteps:1700,ptbSteps:2500}),
[RENDER_PASS.COVERED]:Object.freeze({id:RENDER_PASS.COVERED,covered:true,budgetMs:1050,nominalScale:1,minWidth:0,maxWidth:Infinity,densityLimit:1,blaSteps:0,ptbSteps:0})
});
function renderProfile(pass){return RENDER_PROFILE[pass]||RENDER_PROFILE[RENDER_PASS.PREVIEW]}
let deepMode=false;
const state={
bits:INITIAL_BITS,re:0n,im:0n,span:0n,
baseIter:350,adaptive:true,hq:false,processMode:'standard',palette:0,cycle:.008,shift:.18,
token:0,rendering:false,lastRender:0,lastEngine:'起動中',lastPass:null,lastInteraction:performance.now(),dirty:true,
uiHidden:false,frameView:null,fieldView:null,lastFrameDone:0,
detailGeneration:0,detailActive:false,detailQueued:0,detailDone:0,
drawState:'REPROJECTED',coverage:0,unresolved:0,
precisionPending:false,
pointerActive:false,wheelActive:false,panReuse:null,effectiveDpr:1,screenPixelBudget:0,adaptivePixelBudget:0,continuationProgress:0,focusX:.5,focusY:.5
};
// Deep telemetry is collected from the actual BLA/perturbation
// kernel rather than from a synthetic JavaScript loop.
const deepTelemetry={
frames:0,kernelMPP:0,meanIter:0,blackRatio:0,badRatio:0,blaBuildEMA:0,
refDistance:0,interiorRatio:0,repairRatio:0,unresolvedRatio:0,
blaStepsPerPixel:0,ptbStepsPerPixel:0,rebasePerPixel:0,lastPilotMs:0,lastPilotMPP:0,lastKernelMs:0,lastVerifyMs:0,lastPixels:0
};
const refControl={buildEMA:18,lastBuildMs:0,lastRecenterAt:0,refId:0,baseMPP:0,lastMPP:0,lastPixels:0,cooldownMs:900};
// Adaptive quality controller: instead of tying a quality level to a fixed
// pixel width, learn the recent cost per pixel and spend a bounded amount of time.
const renderPerf={deepMPP:0,shallowMPP:0};
const runtimeMetrics={canvasWrites:0,domWrites:0,renderStarts:0,renderStartsDuringGesture:0,longTasks:0,maxLongTaskMs:0,pendingBackgroundJobs:0};
try{if('PerformanceObserver'in globalThis){const observer=new PerformanceObserver(list=>{for(const entry of list.getEntries()){runtimeMetrics.longTasks++;runtimeMetrics.maxLongTaskMs=Math.max(runtimeMetrics.maxLongTaskMs,entry.duration||0)}});observer.observe({type:'longtask',buffered:true})}}catch{}
function scheduleBackground(fn,delay=0){runtimeMetrics.pendingBackgroundJobs++;return setTimeout(()=>{runtimeMetrics.pendingBackgroundJobs=Math.max(0,runtimeMetrics.pendingBackgroundJobs-1);fn()},delay)}
function scheduleIdle(fn,timeout=350){runtimeMetrics.pendingBackgroundJobs++;const run=()=>{runtimeMetrics.pendingBackgroundJobs=Math.max(0,runtimeMetrics.pendingBackgroundJobs-1);fn()};return'requestIdleCallback'in window?requestIdleCallback(run,{timeout}):setTimeout(run,Math.min(80,timeout))}
const DETAIL_TILE_CACHE=new Map();
let detailCacheBytes=0;
const activeDetailTiles=[];
let detailPlan=null;
let schedulerRAF=0,schedulerTimer=0,schedulerDue=0,needsPaint=true,needsStats=true,wheelSettleTimer=0,pointerSettleTimer=0;
function requestScheduler(delay=0){
if(document.hidden)return;
if(delay>0){
const due=performance.now()+delay;
if(schedulerTimer&&schedulerDue<=due)return;
if(schedulerTimer)clearTimeout(schedulerTimer);
schedulerDue=due;
schedulerTimer=setTimeout(()=>{schedulerTimer=0;schedulerDue=0;requestScheduler()},Math.max(0,Math.ceil(delay)));
return
}
if(schedulerTimer){clearTimeout(schedulerTimer);schedulerTimer=0;schedulerDue=0}
if(!schedulerRAF)schedulerRAF=requestAnimationFrame(loop)
}
function invalidateView(withStats=true){needsPaint=true;if(withStats)needsStats=true;requestScheduler()}
function invalidateStats(){needsStats=true;requestScheduler()}
// Device-specific runtime wisdom. It is populated by a small background CPU
// concurrency benchmark and then refined by real deep-render strip timings.
const deepWisdom={
ready:false,running:false,maxWorkers:1,workerCount:1,
targetStripMs:12,stripRows:24,rowMsEMA:0,
realBench:false
};
function deepWisdomStorageKey(){
const meta=globalThis.MANDEL_KERNEL_META||{},kernel=String(meta.BLA_SIMD_B64||meta.BLA_SCALAR_B64||'embedded').slice(0,16),hc=Math.max(1,navigator.hardwareConcurrency||1),dm=Number(navigator.deviceMemory||0);return'mandelbrot.wisdom.v23.'+[kernel,hc,dm].join('.')
}
function restoreDeepWisdom(){
try{const saved=JSON.parse(localStorage.getItem(deepWisdomStorageKey())||'null');if(!saved||saved.version!==23)return;const row=Number(saved.rowMsEMA),rows=Number(saved.stripRows),workers=Number(saved.workerCount);if(Number.isFinite(row)&&row>0&&row<1000)deepWisdom.rowMsEMA=row;if(Number.isInteger(rows)&&rows>=2&&rows<=128)deepWisdom.stripRows=rows;if(Number.isInteger(workers)&&workers>=1&&workers<=deepWorkerLimit())deepWisdom.workerCount=workers;deepWisdom.ready=deepWisdom.rowMsEMA>0}catch{}
}
function persistDeepWisdom(){
if(!deepWisdom.rowMsEMA)return;try{localStorage.setItem(deepWisdomStorageKey(),JSON.stringify({version:23,rowMsEMA:deepWisdom.rowMsEMA,stripRows:deepWisdom.stripRows,workerCount:deepWisdom.workerCount}))}catch{}
}
function one(bits=state.bits){return 1n<<BigInt(bits)}
function fromFrac(n,d=1n){return n*one()/d}
function fromDec(s){
s=String(s).trim();let neg=s[0]==='-';if(neg)s=s.slice(1);
const p=s.toLowerCase().split('e'),mant=p[0],exp=p[1]?parseInt(p[1],10):0;
const a=mant.split('.'),i=a[0]||'0',f=a[1]||'';
let digits=(i+f).replace(/^0+(?=\d)/,'')||'0',decPlaces=f.length-exp;
if(decPlaces<0){digits+='0'.repeat(-decPlaces);decPlaces=0}
const den=10n**BigInt(decPlaces),v=(BigInt(digits)*one()+den/2n)/den;return neg?-v:v;
}
function decimalRequiredBits(s){s=String(s).trim().replace(/^[+-]/,'');const p=s.toLowerCase().split('e'),f=(p[0].split('.')[1]||'').length,e=p[1]?parseInt(p[1],10):0,places=Math.max(0,f-e);return Math.max(64,Math.ceil(places*Math.log2(10))+32)}
function bitLen(n){n=n<0n?-n:n;return n===0n?0:n.toString(2).length}
function fixedNum(v,bits=state.bits){
if(v===0n)return 0;let neg=v<0n;if(neg)v=-v;
const bl=bitLen(v),keep=52;let top,exp;
if(bl>keep){const sh=BigInt(bl-keep);top=Number(v>>sh);exp=(bl-keep)-bits}else{top=Number(v);exp=-bits}
const x=top*Math.pow(2,exp);return neg?-x:x;
}
// Hot-path conversion for orbit values known to stay O(1).
function fixedOrbitNum(v,bits){
if(v===0n)return 0;
const sh=bits-54;
if(sh>0)return Number(v>>BigInt(sh))*Math.pow(2,-54);
return Number(v)*Math.pow(2,-bits);
}
function log2FixedAt(v,bits){v=v<0n?-v:v;if(v===0n)return-Infinity;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh));return Math.log2(top)+sh-bits}
function log2Fixed(v){return log2FixedAt(v,state.bits)}
function zoomExp(){return Math.max(0,Math.log10(3.4)-log2Fixed(state.span)/Math.log2(10))}
function fixedRatio(a,b){
if(b===0n)return 0;if(a===0n)return 0;const neg=a<0n;if(neg)a=-a;
const q=(a<<52n)/b;const v=Number(q)/4503599627370496;return neg?-v:v;
}
function align(v,fromBits,toBits){const d=toBits-fromBits;return d===0?v:d>0?v<<BigInt(d):v>>BigInt(-d)}
function promoteState(shift){
// Precision promotion used to invalidate the reference orbit. Keep the same
// mathematical values by shifting their fixed-point representation instead.
// If a render is in flight, logically cancel it before mutating shared cache state.
if(state.rendering)cancelRender();
const oldBits=state.bits,s=BigInt(shift);state.re<<=s;state.im<<=s;state.span<<=s;
if(state.frameView){state.frameView.re<<=s;state.frameView.im<<=s;state.frameView.span<<=s;state.frameView.bits+=shift}
state.bits+=shift;
promoteReferenceCache(shift,oldBits);
}
function ensurePrecision(){
const bl=bitLen(state.span);
if(bl>=MIN_SPAN_BITS)return false;
// Defer representation-only precision promotion until the current frame finishes.
if(state.rendering){state.precisionPending=true;return false}
state.precisionPending=false;promoteState(TARGET_SPAN_BITS-bl);return true
}
function flushPendingPrecision(){
if(state.rendering||!state.precisionPending)return false;state.precisionPending=false;
const bl=bitLen(state.span);if(bl<MIN_SPAN_BITS){promoteState(TARGET_SPAN_BITS-bl);return true}return false
}
function mulRatio(v,factor){
const n=BigInt(Math.max(1,Math.round(factor*Number(RATIO_DEN))));return v*n/RATIO_DEN;
}
function fmtFixed(v,d=17){let neg=v<0n;if(neg)v=-v;const scale=10n**BigInt(d),q=v*scale>>BigInt(state.bits);let s=q.toString().padStart(d+1,'0');s=s.slice(0,-d)+'.'+s.slice(-d);s=s.replace(/(\.\d*?)0+$/,'$1').replace(/\.$/,'');return(neg?'-':'')+s}
function fmtFixedExact(v){let neg=v<0n;if(neg)v=-v;const d=state.bits,q=v*(10n**BigInt(d))>>BigInt(state.bits);let s=q.toString().padStart(d+1,'0');s=s.slice(0,-d)+'.'+s.slice(-d);s=s.replace(/(\.\d*?)0+$/,'$1').replace(/\.$/,'');return(neg?'-':'')+s}
function fmtSpan(){const l=log2Fixed(state.span);if(!Number.isFinite(l))return'0';const e=Math.floor(l/Math.log2(10)),m=Math.pow(2,l-e*Math.log2(10));return m.toFixed(4)+'e'+e}
function maxIter(){if(!state.adaptive)return state.baseIter;const z=zoomExp();if(z<12)return Math.min(12000,Math.round(state.baseIter+22*Math.sqrt(z)*Math.log2(2+z)));return Math.min(140000,Math.round(state.baseIter+150*z+260*Math.log2(2+z)))}
function ensureOrbitPrecision(iter,width=Math.max(1,canvas.width)){const conditionBits=referenceCache.rr?Math.max(0,Math.min(256,Math.ceil(referenceCache.conditionLog2||0))):0,required=160+conditionBits,available=bitLen(state.span)-Math.ceil(Math.log2(width))-Math.ceil(Math.log2(Math.max(2,iter)));if(available>=required)return false;promoteState(required+32-available);invalidateReferenceOrbit();return true}
function f64Ulp(x){x=Math.abs(x);if(!Number.isFinite(x))return Infinity;if(x===0)return Number.MIN_VALUE;return Math.pow(2,Math.floor(Math.log2(x))-52)}
function deepResolutionRatio(snap=snapshot(),width=Math.max(1,canvas.width)){const step=Math.abs(fixedNum(snap.span,snap.bits))/Math.max(1,width),ulp=Math.max(f64Ulp(fixedNum(snap.re,snap.bits)),f64Ulp(fixedNum(snap.im,snap.bits)));return step===0||!Number.isFinite(step)?0:step/Math.max(Number.MIN_VALUE,ulp)}
function deepEngineNeeded(snap=snapshot(),width=Math.max(1,canvas.width)){if(exportForcePrecision)return true;const ratio=deepResolutionRatio(snap,width),orbitRisk=ratio<=128&&(deepTelemetry.badRatio>1e-4||deepTelemetry.unresolvedRatio>2e-3||deepTelemetry.repairRatio>.08);deepMode=orbitRisk||(deepMode?ratio<64:ratio<=32);return deepMode}
function iterationPlan(profile,deep){
const colorIter=maxIter();return{colorIter,computeIter:colorIter};
}
function workProfile(profile){return{bla:profile.blaSteps,ptb:profile.ptbSteps}}
function cancelRender(){state.token++;state.rendering=false;cancelDeepPoolJob();flushPendingPrecision()}
function cancelDetailRefinement(clearCurrent=true){
state.detailGeneration++;state.detailActive=false;state.detailQueued=0;state.detailDone=0;detailPlan=null;
if(clearCurrent)activeDetailTiles.length=0;
}
function clearDetailCache(){DETAIL_TILE_CACHE.clear();detailCacheBytes=0;activeDetailTiles.length=0;cancelDetailRefinement(false)}
function setDirty(cancel=true){cancelUnknownContinuation();state.lastInteraction=performance.now();state.dirty=true;state.lastPass=null;state.drawState=state.frameView?'REPROJECTED':'PREVIEW';state.coverage=0;cancelDetailRefinement(true);if(cancel)cancelRender();invalidateView()}
function reset(){
clearDetailCache();clearPanReuse();state.bits=INITIAL_BITS;state.re=-fromFrac(1n,2n);state.im=0n;state.span=fromFrac(34n,10n);setDirty();saveHash(false)
}
function viewRect(){return canvas.getBoundingClientRect()}
function updateFocus(x,y){const r=viewRect();state.focusX=Math.max(0,Math.min(1,(x-r.left)/Math.max(1,r.width)));state.focusY=Math.max(0,Math.min(1,(y-r.top)/Math.max(1,r.height)))}
function zoomAt(x,y,factor){
if(!state.panReuse)capturePanSource();
const r=viewRect(),fx=(x-r.left)/Math.max(1,r.width)-.5,fy=(y-r.top)/Math.max(1,r.height)-.5;
factor=Math.max(.01,Math.min(100,factor));const old=state.span,neu=mulRatio(old,factor);
const dx=BigInt(Math.round(fx*1e9)),dy=BigInt(Math.round(fy*1e9));
state.re+=(old-neu)*dx/1000000000n;
const oldY=old*BigInt(canvas.height)/BigInt(Math.max(1,canvas.width)),newY=neu*BigInt(canvas.height)/BigInt(Math.max(1,canvas.width));
state.im-=(oldY-newY)*dy/1000000000n;state.span=neu;ensurePrecision();setDirty(!(state.pointerActive||state.wheelActive));
}
function pan(dx,dy){
const w=Math.max(1,canvas.clientWidth),h=Math.max(1,canvas.clientHeight);
state.re-=state.span*BigInt(Math.round(dx*100000))/BigInt(Math.round(w*100000));
const ys=state.span*BigInt(canvas.height)/BigInt(Math.max(1,canvas.width));
state.im+=ys*BigInt(Math.round(dy*100000))/BigInt(Math.round(h*100000));setDirty(!(state.pointerActive||state.wheelActive));
}
function clearPanReuse(){state.panReuse=null}
function capturePanSource(){
if(!state.frameView||!frameCanvas.width||!frameCanvas.height)return false;
const fv=state.frameView;state.panReuse={canvas:frameCanvas,snap:{bits:fv.bits,re:fv.re,im:fv.im,span:fv.span},style:styleSignature(),pass:state.lastPass,iter:maxIter(),created:performance.now()};return true
}
function buildPanReuse(snap,w,h,iter,profile){
const src=state.panReuse;if(!src||!src.canvas||src.style!==styleSignature())return null;
// Final HQ must recompute after a real scale change. Interactive/base passes may
// reproject a much larger range so wheel/pinch zoom can reuse existing pixels.
const re=align(src.snap.re,src.snap.bits,snap.bits),im=align(src.snap.im,src.snap.bits,snap.bits),sp=align(src.snap.span,src.snap.bits,snap.bits),scale=fixedRatio(sp,snap.span);
if(!Number.isFinite(scale)||scale<.22||scale>4.5)return null;
const scaleChange=Math.abs(Math.log2(Math.max(1e-12,scale)));
// Covered is a sampling contract, not a presentation shortcut. Reprojection
// is allowed only for Preview; the target grid is always recomputed in full.
if(profile.covered)return null;
const dx=fixedRatio(re-snap.re,snap.span)*w,dy=-fixedRatio(im-snap.im,snap.span)*w,move=Math.hypot(dx/Math.max(1,w),dy/Math.max(1,w));if(!Number.isFinite(move)||move>1.35)return null;
const dw=w*scale,dh=w*scale*(src.canvas.height/Math.max(1,src.canvas.width)),density=dw/Math.max(1,src.canvas.width);
// A zoomed preview may stretch old pixels temporarily, but the idle HQ pass will
// recompute it. Keep the stretch bounded so interaction never becomes misleading.
if(density>profile.densityLimit)return null;
const x0=w*.5+dx-dw*.5,y0=h*.5+dy-dh*.5,x1=x0+dw,y1=y0+dh;
let ix0=Math.max(0,Math.ceil(x0)+2),iy0=Math.max(0,Math.ceil(y0)+2),ix1=Math.min(w,Math.floor(x1)-2),iy1=Math.min(h,Math.floor(y1)-2);
if(ix1<=ix0||iy1<=iy0)return null;
const overlap=(ix1-ix0)*(iy1-iy0),frac=overlap/Math.max(1,w*h);if(frac<.20)return null;
const c=document.createElement('canvas');c.width=w;c.height=h;const cc=c.getContext('2d',{alpha:false});if(!cc)return null;cc.fillStyle='#050813';cc.fillRect(0,0,w,h);cc.imageSmoothingEnabled=true;try{cc.imageSmoothingQuality='high'}catch{};cc.drawImage(src.canvas,x0,y0,dw,dh);
const out=new Uint8ClampedArray(cc.getImageData(0,0,w,h).data),rects=[];
const add=(x,y,rw,rh)=>{x=Math.max(0,x|0);y=Math.max(0,y|0);rw=Math.min(w-x,rw|0);rh=Math.min(h-y,rh|0);if(rw>0&&rh>0)rects.push({x0:x,y0:y,w:rw,h:rh})};
add(0,0,w,iy0);add(0,iy1,w,h-iy1);add(0,iy0,ix0,iy1-iy0);add(ix1,iy0,w-ix1,iy1-iy0);
const exposed=rects.reduce((a,r)=>a+r.w*r.h,0);if(exposed>w*h*.80)return null;
return{out,rects,reusedPixels:w*h-exposed,exposedPixels:exposed,scale};
}
// ── Palette / smooth colouring ───────────────────────────────────────────
const PALETTE_CURRENT=0,PALETTE_RAINBOW=1,PALETTE_MONO=2;
const FIELD_ESCAPED=1,FIELD_INTERIOR_PROVEN=2,FIELD_INTERIOR_LIKELY=3,FIELD_UNKNOWN=4;
const stops=[[0,[4,10,27]],[.11,[12,53,79]],[.25,[31,156,184]],[.38,[91,226,234]],[.50,[66,53,151]],[.62,[139,49,170]],[.73,[232,72,145]],[.84,[255,137,64]],[.93,[255,211,99]],[1,[255,250,223]]];
const COLOR_PHASES=2048,COLOR_MIXES=64;
function hsvRgb(h,sat,val){h=((h%1)+1)%1;const x=h*6,i=Math.floor(x),f=x-i,p=val*(1-sat),q=val*(1-sat*f),s=val*(1-sat*(1-f));let r,g,b;switch(i%6){case 0:r=val;g=s;b=p;break;case 1:r=q;g=val;b=p;break;case 2:r=p;g=val;b=s;break;case 3:r=p;g=q;b=val;break;case 4:r=s;g=p;b=val;break;default:r=val;g=p;b=q}return[r*255,g*255,b*255]}
function basePalette(kind,t){
t=((t%1)+1)%1;
if(kind===PALETTE_RAINBOW)return hsvRgb(t,.92,1);
if(kind===PALETTE_MONO){const g=22+233*(.5-.5*Math.cos(Math.PI*2*t));return[g,g,g]}
let a=stops[0],b=stops[stops.length-1];for(let j=1;j<stops.length;j++){if(t<=stops[j][0]){a=stops[j-1];b=stops[j];break}}
let f=(t-a[0])/(b[0]-a[0]||1);f=f*f*(3-2*f);return[a[1][0]+(b[1][0]-a[1][0])*f,a[1][1]+(b[1][1]-a[1][1])*f,a[1][2]+(b[1][2]-a[1][2])*f]
}
function buildColorLut(kind){const lut=new Uint8Array(COLOR_PHASES*3);for(let pi=0;pi<COLOR_PHASES;pi++){const u=pi/(COLOR_PHASES-1),t=kind===PALETTE_CURRENT?(u<=.5?u*2:2-u*2):u,c=basePalette(kind,t),k=pi*3;lut[k]=c[0]|0;lut[k+1]=c[1]|0;lut[k+2]=c[2]|0}return lut}
const COLOR_LUTS=[buildColorLut(PALETTE_CURRENT),buildColorLut(PALETTE_RAINBOW),buildColorLut(PALETTE_MONO)];
const SMOOTH_U_MIN=2,SMOOTH_U_STEP=1/128,SMOOTH_U_N=4097,SMOOTH_CORR=new Float32Array(SMOOTH_U_N);
for(let i=0;i<SMOOTH_U_N;i++){const u=SMOOTH_U_MIN+i*SMOOTH_U_STEP;SMOOTH_CORR[i]=1-Math.log2(.5*u)}
const COLOR_CTX_CACHE=new Map();
function makeColorCtx(iter){let hit=COLOR_CTX_CACHE.get(iter);if(hit)return hit;const mixBin=new Uint8Array(iter+1),den=Math.log1p(Math.max(8,iter));for(let n=0;n<=iter;n++){const edge=Math.max(0,Math.min(1,Math.log1p(n)/den));mixBin[n]=Math.min(COLOR_MIXES-1,Math.round((COLOR_MIXES-1)*Math.pow(edge,.38)))}hit={mixBin};COLOR_CTX_CACHE.set(iter,hit);if(COLOR_CTX_CACHE.size>8)COLOR_CTX_CACHE.delete(COLOR_CTX_CACHE.keys().next().value);return hit}
function smoothEscape(n,m){const u=Math.log2(Math.max(4.0000001,m));let corr;const fi=(u-SMOOTH_U_MIN)/SMOOTH_U_STEP;if(fi>=0&&fi<SMOOTH_U_N-1){const i=fi|0,f=fi-i;corr=SMOOTH_CORR[i]+(SMOOTH_CORR[i+1]-SMOOTH_CORR[i])*f}else corr=1-Math.log2(.5*u);return n+corr}
function putPaletteColor(out,oi,sm,n,iter,colorCtx){let phase=state.shift+sm*state.cycle;phase-=Math.floor(phase);const pi=Math.min(COLOR_PHASES-1,(phase*COLOR_PHASES)|0),mi=colorCtx.mixBin[Math.min(iter,n)],mix=.34+.66*(mi/(COLOR_MIXES-1)),kind=state.palette,lut=COLOR_LUTS[kind]||COLOR_LUTS[0],k=pi*3,f0=kind===PALETTE_MONO?8:2,f1=kind===PALETTE_MONO?8:5,f2=kind===PALETTE_MONO?8:15;out[oi]=(f0+(lut[k]-f0)*mix)|0;out[oi+1]=(f1+(lut[k+1]-f1)*mix)|0;out[oi+2]=(f2+(lut[k+2]-f2)*mix)|0;out[oi+3]=255}
function putFastColor(out,oi,n,m,iter,colorCtx){putPaletteColor(out,oi,smoothEscape(n,m),n,iter,colorCtx)}
function makeField(size,iter){return{smooth:new Float32Array(size),iterations:new Uint32Array(size),classes:new Uint8Array(size),confidence:new Uint8Array(size),iter}}
function fieldConfidence(kind){return kind===FIELD_ESCAPED||kind===FIELD_INTERIOR_PROVEN?255:kind===FIELD_INTERIOR_LIKELY?192:0}
function putField(field,index,n,m,kind){field.classes[index]=kind;if(field.iterations)field.iterations[index]=Math.max(0,n)>>>0;if(field.confidence)field.confidence[index]=fieldConfidence(kind);if(kind===FIELD_ESCAPED)field.smooth[index]=smoothEscape(n,m);else field.smooth[index]=NaN}
function fillFieldConfidence(field){if(!field.confidence)field.confidence=new Uint8Array(field.classes.length);for(let i=0;i<field.classes.length;i++)field.confidence[i]=fieldConfidence(field.classes[i])}
function colorizeField(field){const out=new Uint8ClampedArray(field.classes.length*4),colorCtx=makeColorCtx(field.iter);for(let i=0,oi=0;i<field.classes.length;i++,oi+=4){const kind=field.classes[i];if(kind!==FIELD_ESCAPED){const neutral=kind===FIELD_UNKNOWN;out[oi]=neutral?20:0;out[oi+1]=neutral?22:0;out[oi+2]=neutral?30:0;out[oi+3]=255;continue}const sm=field.smooth[i],n=Math.max(0,Math.min(field.iter,Math.floor(sm)));putPaletteColor(out,oi,sm,n,field.iter,colorCtx)}return out}
// Embedded WebAssembly: SIMD-optimized kernel with a scalar fallback.
let wasm=null;
function instantiateWasm(b64){
const module=b64 instanceof WebAssembly.Module?b64:(()=>{const raw=atob(b64),bytes=new Uint8Array(raw.length);for(let i=0;i<raw.length;i++)bytes[i]=raw.charCodeAt(i);return new WebAssembly.Module(bytes)})(),inst=new WebAssembly.Instance(module,{}),ex=inst.exports;
return {module,ex,counts:()=>new Uint32Array(ex.memory.buffer,ex.counts_ptr(),65536),mags:()=>new Float64Array(ex.memory.buffer,ex.mags_ptr(),65536),refsR:()=>new Float64Array(ex.memory.buffer,ex.refs_r_ptr(),150001),refsI:()=>new Float64Array(ex.memory.buffer,ex.refs_i_ptr(),150001)};
}
try{wasm=instantiateWasm(WASM_SIMD_B64);wasm.simd=globalThis.MANDEL_HOSTED_SHALLOW_SIMD!==false}catch(e){try{wasm=instantiateWasm(WASM_SCALAR_B64);wasm.simd=false}catch(_e){wasm=null}}
let shallowWorker=null,shallowWorkerUrl=null,shallowWorkerBusy=false,shallowRecycle=null;
function shallowWorkerSource(){return `'use strict';let ex=null;function smooth(n,m){const u=Math.log2(Math.max(4.0000001,m));return n+1-Math.log2(.5*u)}function likely(cr,ci){const y2=ci*ci,x=cr-.25,q=x*x+y2;if(q*(q+x)<=.25*y2)return true;const x2=cr+1;return x2*x2+y2<=.0625}self.onmessage=e=>{const d=e.data;if(d.type==='init'){ex=new WebAssembly.Instance(d.module,{}).exports;postMessage({type:'ready'});return}if(d.type!=='render'||!ex)return;try{const scale=d.sp/d.w,npx=ex.render_rows(d.cre+scale*.5,d.cim-scale*.5,d.sp,d.w,d.h,d.y,d.rows,d.iter),counts=new Uint32Array(ex.memory.buffer,ex.counts_ptr(),65536),mags=new Float64Array(ex.memory.buffer,ex.mags_ptr(),65536),field=d.fieldBuffer&&d.fieldBuffer.byteLength>=npx*4?new Float32Array(d.fieldBuffer,0,npx):new Float32Array(npx),iterations=d.iterationBuffer&&d.iterationBuffer.byteLength>=npx*4?new Uint32Array(d.iterationBuffer,0,npx):new Uint32Array(npx),classes=d.classBuffer&&d.classBuffer.byteLength>=npx?new Uint8Array(d.classBuffer,0,npx):new Uint8Array(npx);for(let i=0;i<npx;i++){const n=counts[i],x=i%d.w,y=d.y+((i/d.w)|0),cr=d.cre+(x+.5-d.w*.5)*scale,ci=d.cim+(d.h*.5-y-.5)*scale;iterations[i]=n;if(n<d.iter){classes[i]=1;field[i]=smooth(n,mags[i])}else{classes[i]=likely(cr,ci)?3:4;field[i]=NaN}}postMessage({type:'render',jobId:d.jobId,y:d.y,rows:d.rows,field:field.buffer,iterations:iterations.buffer,classes:classes.buffer},[field.buffer,iterations.buffer,classes.buffer])}catch(error){postMessage({type:'render',jobId:d.jobId,error:String(error&&error.message||error)})}}`}
function ensureShallowWorker(){if(shallowWorker||!wasm||typeof Worker==='undefined'||typeof Blob==='undefined')return!!shallowWorker;try{shallowWorkerUrl=URL.createObjectURL(new Blob([shallowWorkerSource()],{type:'text/javascript'}));shallowWorker=new Worker(shallowWorkerUrl);shallowWorker.postMessage({type:'init',module:wasm.module});return true}catch{shallowWorker=null;return false}}
function destroyShallowWorker(){if(shallowWorker){try{shallowWorker.terminate()}catch{}shallowWorker=null}if(shallowWorkerUrl){try{URL.revokeObjectURL(shallowWorkerUrl)}catch{}shallowWorkerUrl=null}shallowWorkerBusy=false;shallowRecycle=null}
function instantiateDeepWasm(b64){
const module=b64 instanceof WebAssembly.Module?b64:(()=>{const raw=atob(b64),bytes=new Uint8Array(raw.length);for(let i=0;i<raw.length;i++)bytes[i]=raw.charCodeAt(i);return new WebAssembly.Module(bytes)})();
const ex=new WebAssembly.Instance(module,{}).exports;
return {ex,counts:()=>new Uint32Array(ex.memory.buffer,ex.counts_ptr(),65536),mags:()=>new Float64Array(ex.memory.buffer,ex.mags_ptr(),65536),refsR:()=>new Float64Array(ex.memory.buffer,ex.refs_r_ptr(),150001),refsI:()=>new Float64Array(ex.memory.buffer,ex.refs_i_ptr(),150001)};
}
let deepWasm=null,deepWasmTried=false,deepModuleBundle=null,deepModulePromise=null;
async function compileKernelSource(source){if(source instanceof WebAssembly.Module)return source;if(/^https?:/i.test(source)){const response=await fetch(source,{cache:'force-cache'});if(!response.ok)throw new Error('WASM request failed '+response.status);if(WebAssembly.compileStreaming){try{return await WebAssembly.compileStreaming(Promise.resolve(response.clone()))}catch{}}return WebAssembly.compile(await response.arrayBuffer())}const raw=atob(source),bytes=new Uint8Array(raw.length);for(let i=0;i<raw.length;i++)bytes[i]=raw.charCodeAt(i);return WebAssembly.compile(bytes)}
async function compileKernelPair(simd,scalar){try{return{module:await compileKernelSource(simd),simd:true}}catch{return{module:await compileKernelSource(scalar),simd:false}}}
function prepareDeepModules(){if(deepModuleBundle)return Promise.resolve(deepModuleBundle);if(!deepModulePromise)deepModulePromise=Promise.all([compileKernelPair(DEEP_SIMD_B64,DEEP_SCALAR_B64),compileKernelPair(BLA_SIMD_B64,BLA_SCALAR_B64),compileKernelPair(COLOR_SIMD_B64,COLOR_SCALAR_B64)]).then(([deep,bla,color])=>deepModuleBundle={deep,bla,color}).catch(error=>{deepModulePromise=null;throw error});return deepModulePromise}
function ensureDeepWasm(){
if(deepWasm)return true;if(deepWasmTried)return false;deepWasmTried=true;if(deepModuleBundle){try{deepWasm=instantiateDeepWasm(deepModuleBundle.deep.module);deepWasm.simd=deepModuleBundle.deep.simd}catch{deepWasm=null}return!!deepWasm}if(/^https?:/i.test(DEEP_SIMD_B64)||/^https?:/i.test(DEEP_SCALAR_B64))return false;
try{deepWasm=instantiateDeepWasm(DEEP_SIMD_B64);deepWasm.simd=true}catch(e){try{deepWasm=instantiateDeepWasm(DEEP_SCALAR_B64);deepWasm.simd=false}catch(_e){deepWasm=null}}
return!!deepWasm
}
// Deep renderer worker pool. Each worker owns a WASM instance and performs both the
// perturbation kernel and colour mapping, so colourful frames no longer funnel all
// post-processing through the UI thread. Jobs are striped dynamically for load balance.
const deepPool={workers:[],url:null,activeToken:0,serial:0,failed:false,current:null,maxWorkers:0,benchTimer:0};
// ── Persistent deep-render workers / BLA kernel ──────────────────────────
function deepWorkerSource(){
return `'use strict';
let core=null,blaCore=null,colorCore=null,refKey='',refLoaded=0,RR=new Float64Array(150001),RI=new Float64Array(150001),blaRefKey='',blaRefLoaded=0,blaBuiltKey='',lastBlaBuildMs=0;
function ensure(){if(!core)throw new Error('deep module not initialized');return core}
function ensureBla(){if(!blaCore)throw new Error('BLA module not initialized');return blaCore}
function ensureColor(){if(!colorCore)throw new Error('color module not initialized');return colorCore}
function applyRef(d,ex){if(d.refKey!==refKey){refKey=d.refKey;refLoaded=0;blaRefKey='';blaRefLoaded=0;blaBuiltKey=''}if(d.rr){const ar=new Float64Array(d.rr),ai=new Float64Array(d.ri),st=d.rrStart|0;RR.set(ar,st);RI.set(ai,st);new Float64Array(ex.memory.buffer,ex.refs_r_ptr(),150001).set(ar,st);new Float64Array(ex.memory.buffer,ex.refs_i_ptr(),150001).set(ai,st);refLoaded=Math.max(refLoaded,st+ar.length);if(blaCore&&blaRefKey===refKey){const bx=blaCore.ex;new Float64Array(bx.memory.buffer,bx.refs_r_ptr(),150001).set(ar,st);new Float64Array(bx.memory.buffer,bx.refs_i_ptr(),150001).set(ai,st);blaRefLoaded=Math.max(blaRefLoaded,st+ar.length);blaBuiltKey=''}}if(refLoaded<d.refLen+1)throw new Error('reference cache miss')}
function syncRefsToBla(d,key=d.blaKey,eps=d.blaEps){if(!d.useBla)return null;const b=ensureBla(),ex=b.ex;if(blaRefKey!==refKey){new Float64Array(ex.memory.buffer,ex.refs_r_ptr(),150001).set(RR.subarray(0,refLoaded));new Float64Array(ex.memory.buffer,ex.refs_i_ptr(),150001).set(RI.subarray(0,refLoaded));blaRefKey=refKey;blaRefLoaded=refLoaded;blaBuiltKey=''}else if(refLoaded>blaRefLoaded){new Float64Array(ex.memory.buffer,ex.refs_r_ptr(),150001).set(RR.subarray(blaRefLoaded,refLoaded),blaRefLoaded);blaRefLoaded=refLoaded;blaBuiltKey=''}lastBlaBuildMs=0;if(blaBuiltKey!==key){const t=performance.now(),levels=ex.build_bla(d.refLen,d.cMax,eps);lastBlaBuildMs=performance.now()-t;if(!levels)return null;blaBuiltKey=key}return b}
function smoothBatch(srcMags,npx){const c=ensureColor(),ex=c.ex,mi=new Float64Array(ex.memory.buffer,ex.mags_ptr(),65536),co=new Float32Array(ex.memory.buffer,ex.corr_ptr(),65536);mi.set(srcMags.subarray(0,npx),0);ex.smooth_batch(npx);return co}
function strictOne(ex,x,y,d){const counts=new Uint32Array(ex.memory.buffer,ex.counts_ptr(),65536),mags=new Float64Array(ex.memory.buffer,ex.mags_ptr(),65536);ex.render_perturb_rebase_rect(d.spanMant,d.spanBucket,d.offR,d.offI,d.offBucket,d.refLen,d.w,d.h,d.cx,d.cy,x,y,1,1,d.iter,0,0,0,0,0,0,0);return[counts[0],mags[0]]}
function stats(bx){return{blaSteps:bx.stat_bla_steps?bx.stat_bla_steps():0,ptbSteps:bx.stat_ptb_steps?bx.stat_ptb_steps():0,rebases:bx.stat_rebases?bx.stat_rebases():0,interior:bx.stat_interior?bx.stat_interior():0,unresolved:bx.stat_unresolved?bx.stat_unresolved():0,fail:bx.stat_fail?bx.stat_fail():0}}
function renderBla(d,b,key,eps,bcap,pcap){const bx=b.ex;if(key!==d.blaKey||eps!==d.blaEps)b=syncRefsToBla(d,key,eps)||b;const t=performance.now(),npx=bx.render_bla_rect_v2?bx.render_bla_rect_v2(d.spanNormal,d.offRn,d.offIn,d.baseCr,d.baseCi,d.refLen,d.w,d.h,d.x0||0,d.y0,d.rectW||d.w,d.rows,d.iter,bcap||0,pcap||0,1):bx.render_bla_rect(d.spanNormal,d.offRn,d.offIn,d.refLen,d.w,d.h,d.x0||0,d.y0,d.rectW||d.w,d.rows,d.iter);return{npx,kernelMs:performance.now()-t,counts:new Uint32Array(bx.memory.buffer,bx.counts_ptr(),65536),mags:new Float64Array(bx.memory.buffer,bx.mags_ptr(),65536),stats:stats(bx)}}
function verifyAgainstSafe(d,result,rw,npx,b){const want=d.verifySamples||0;if(!want||!b)return{samples:0,mismatch:0};const counts=Uint32Array.from(result.counts.subarray(0,npx)),mags=Float64Array.from(result.mags.subarray(0,npx));result.counts=counts;result.mags=mags;const picks=[],seen=new Set(),add=i=>{if(picks.length>=want)return;i=Math.max(0,Math.min(npx-1,i|0));if(!seen.has(i)){seen.add(i);picks.push(i)}};for(let k=0;k<Math.max(2,want>>1);k++)add(((k+.37)*npx/Math.max(2,want>>1))|0);const stride=Math.max(1,Math.floor(npx/Math.max(16,want*10))),top=[];for(let i=stride;i<npx;i+=stride){const n=counts[i],p=counts[i-stride];if(n<0xfffffffe&&p<0xfffffffe&&((n>=d.iter)!==(p>=d.iter))){add(i);add(i-stride)}if(n<d.iter&&n<0xfffffffe){top.push([n,i]);top.sort((a,b)=>b[0]-a[0]);if(top.length>want)top.length=want}}for(const x of top)add(x[1]);for(let k=0;picks.length<want&&k<want*2;k++)add(((k+.73)*npx/want)|0);let mismatch=0;const bad=[];for(const i of picks){const bn=counts[i];if(bn>=0xfffffffe)continue;const x=(d.x0||0)+(i%rw),y=d.y0+((i/rw)|0),sr=renderBla({...d,x0:x,y0:y,rectW:1,rows:1},b,d.safeBlaKey||d.blaKey,d.safeBlaEps||d.blaEps,0,0),sn=sr.counts[0];if(sn>=0xfffffffe)continue;if((bn>=d.iter)!==(sn>=d.iter)||Math.abs((bn|0)-(sn|0))>(d.verifyDelta||64)){mismatch++;bad.push(i)}}return{samples:picks.length,mismatch,bad}}
self.onmessage=async e=>{const d=e.data;if(!d)return;if(d.type==='init'){try{core={ex:new WebAssembly.Instance(d.modules.deep.module,{}).exports,simd:!!d.modules.deep.simd};blaCore={ex:new WebAssembly.Instance(d.modules.bla.module,{}).exports,simd:!!d.modules.bla.simd};colorCore={ex:new WebAssembly.Instance(d.modules.color.module,{}).exports,simd:!!d.modules.color.simd};postMessage({type:'ready'})}catch(error){postMessage({type:'ready',error:String(error&&error.message||error)})}return}if(d.type!=='render'&&d.type!=='pilot'&&d.type!=='realBench')return;try{const c=ensure(),ex=c.ex;applyRef(d,ex);const b=syncRefsToBla(d);if((d.type==='pilot'||d.type==='realBench')&&!b)throw new Error('BLA unavailable');if(d.type==='pilot'||d.type==='realBench'){const reps=d.type==='realBench'?Math.max(1,d.repeats|0):1;let rr=null,total=0;for(let k=0;k<reps;k++){rr=renderBla({...d,x0:0,y0:0,rectW:d.w,rows:d.h},b,d.blaKey,d.blaEps,0,0);total+=rr.kernelMs}if(d.type==='realBench'){postMessage({type:'realBench',benchId:d.benchId,kernelMs:total,pixels:rr.npx*reps,blaBuildMs:lastBlaBuildMs,simd:b.simd});return}const co=rr.counts;let black=0,bad=0,sum=0;for(let i=0;i<rr.npx;i++){const n=co[i];if(n>=0xfffffffe){bad++;continue}sum+=Math.min(d.iter,n);if(n>=d.iter)black++}postMessage({type:'pilot',pilotId:d.pilotId,kernelMs:total,pixels:rr.npx,blaBuildMs:lastBlaBuildMs,blackRatio:black/Math.max(1,rr.npx),badRatio:bad/Math.max(1,rr.npx),meanIter:sum/Math.max(1,rr.npx-bad),stats:rr.stats,simd:b.simd});return}
const rx=d.x0||0,rw=d.rectW||d.w,field=d.fieldBuffer&&d.fieldBuffer.byteLength>=rw*d.rows*4?new Float32Array(d.fieldBuffer,0,rw*d.rows):new Float32Array(rw*d.rows),iterations=d.iterationBuffer&&d.iterationBuffer.byteLength>=rw*d.rows*4?new Uint32Array(d.iterationBuffer,0,rw*d.rows):new Uint32Array(rw*d.rows),classes=d.classBuffer&&d.classBuffer.byteLength>=rw*d.rows?new Uint8Array(d.classBuffer,0,rw*d.rows):new Uint8Array(rw*d.rows),bad=[];let result,repaired=0,verifyMs=0;if(b){result=renderBla(d,b,d.blaKey,d.blaEps,d.maxBlaSteps||0,d.maxPtbSteps||0);let unresolved=result.stats.unresolved||0;if(unresolved&&d.covered){const co=result.counts,ma=result.mags;for(let i=0;i<result.npx;i++)if(co[i]===0xfffffffe){const x=rx+(i%rw),y=d.y0+((i/rw)|0),r=strictOne(ex,x,y,d);co[i]=r[0];ma[i]=r[1]}}
const tv=performance.now();let v=verifyAgainstSafe(d,result,rw,result.npx,b);verifyMs=performance.now()-tv;if(v.mismatch){const safeKey=d.safeBlaKey||d.blaKey,safeEps=d.safeBlaEps||d.blaEps;result=renderBla(d,b,safeKey,safeEps,0,0);repaired=1}
}else{const t=performance.now(),npx=ex.render_perturb_rebase_rect(d.spanMant,d.spanBucket,d.offR,d.offI,d.offBucket,d.refLen,d.w,d.h,d.cx,d.cy,rx,d.y0,rw,d.rows,d.iter,d.skip||0,d.Ar||0,d.Ai||0,d.Ab||0,d.Br||0,d.Bi||0,d.Bb||0);result={npx,kernelMs:performance.now()-t,counts:new Uint32Array(ex.memory.buffer,ex.counts_ptr(),65536),mags:new Float64Array(ex.memory.buffer,ex.mags_ptr(),65536),stats:{blaSteps:0,ptbSteps:0,rebases:0,interior:0,unresolved:0,fail:0}}}
const corr=smoothBatch(result.mags,result.npx);let blackCount=0,sumIter=0;for(let i=0;i<result.npx;i++){let n=result.counts[i],m=result.mags[i];const x=rx+(i%rw),y=d.y0+((i/rw)|0);if(n>=0xfffffffe){if(n===0xffffffff){const r=strictOne(ex,x,y,d);n=r[0];m=r[1]}else{n=d.iter;m=0}}iterations[i]=n;if(n===0xffffffff){classes[i]=4;field[i]=NaN;bad.push(i);continue}sumIter+=Math.min(d.iter,n);if(n>=d.iter){classes[i]=4;field[i]=NaN;blackCount++;continue}classes[i]=1;field[i]=n+corr[i]}
postMessage({type:'render',jobId:d.jobId,x0:rx,rectW:rw,y0:d.y0,rows:d.rows,field:field.buffer,iterations:iterations.buffer,classes:classes.buffer,bad,simd:c.simd,bla:!!b,blaSimd:b?b.simd:false,colorSimd:colorCore?colorCore.simd:false,kernelMs:result.kernelMs,verifyMs,blaBuildMs:lastBlaBuildMs,pixels:result.npx,blackCount,sumIter,stats:result.stats,verifySamples:d.verifySamples||0,repaired},[field.buffer,iterations.buffer,classes.buffer])}catch(err){postMessage({type:'render',jobId:d.jobId,error:String(err&&err.message||err)})}};
`;
}
function destroyDeepPool(){
if(deepPool.current)deepPool.current.cancelled=true;deepPool.current=null;
for(const w of deepPool.workers){try{w.terminate()}catch{}}
deepPool.workers.length=0;if(deepPool.url){try{URL.revokeObjectURL(deepPool.url)}catch{}deepPool.url=null}
deepPool.activeToken=0;deepPool.maxWorkers=0
}
function retireDeepAssets(){destroyDeepPool();deepPool.failed=false;deepWasm=null;deepWasmTried=false;deepModuleBundle=null;deepModulePromise=null;referenceCache.rr=null;referenceCache.ri=null;referenceCache.series=null;referenceCache.n=0;referenceCache.escape=0;referenceCache.loadedLen=0;referenceCache.conditionLog2=0;referenceCache.derivative=[0,0,NEG_BUCKET]}
function cancelDeepPoolJob(){
// Logical cancellation: keep Worker, WASM, reference and BLA caches alive.
deepPool.activeToken=0;
if(deepPool.current){deepPool.current.cancelled=true;deepPool.current=null}
}
function handleDeepWorkerMessage(worker,d){
const job=worker._job;worker._job=null;worker._busy=false;
if(d&&d.type==='ready'){if(d.error){worker._ready=false;deepPool.failed=true;destroyDeepPool();return}worker._ready=true;kickDeepWorker(worker);return}
if(!job){kickDeepWorker(worker);return}
if(job.type==='bench'){if(d&&d.type==='bench'&&d.benchId===job.benchId)job.resolve(d);else job.reject(new Error('benchmark reply mismatch'));kickDeepWorker(worker);return}
if(job.type==='pilot'||job.type==='realBench'){if(d&&((job.type==='pilot'&&d.type==='pilot'&&d.pilotId===job.id)||(job.type==='realBench'&&d.type==='realBench'&&d.benchId===job.id)))job.resolve(d);else job.reject(new Error(job.type+' reply mismatch'));kickDeepWorker(worker);return}
if(job.type==='render')job.runner.onResult(worker,d,job);
kickDeepWorker(worker)
}
function handleDeepWorkerError(worker,e){
const job=worker._job;worker._job=null;worker._busy=false;
if(job&&(job.type==='bench'||job.type==='pilot'||job.type==='realBench'))job.reject(e instanceof Error?e:new Error('worker job failed'));
else if(job&&job.type==='render')job.runner.fail(e);
else{deepPool.failed=true;destroyDeepPool()}
}
function deepWorkerLimit(){const hc=Math.max(1,navigator.hardwareConcurrency||4),memoryLimited=Number(navigator.deviceMemory||8)<=4?1:2;return Math.max(1,Math.min(memoryLimited,hc>2?hc-1:1))}
function addDeepWorker(){if(!deepModuleBundle)throw new Error('deep modules are not ready');const i=deepPool.workers.length,w=new Worker(deepPool.url);w._index=i;w._refKey='';w._refLoaded=0;w._busy=false;w._ready=false;w._job=null;w._recycle=null;w.onmessage=e=>handleDeepWorkerMessage(w,e.data);w.onerror=e=>handleDeepWorkerError(w,e);deepPool.workers.push(w);w.postMessage({type:'init',modules:deepModuleBundle});return w}
function ensureDeepPool(){
if(deepPool.failed||!deepModuleBundle||typeof Worker==='undefined'||typeof Blob==='undefined')return false;
if(deepPool.workers.length)return true;
try{
const count=deepWorkerLimit();
deepPool.url=URL.createObjectURL(new Blob([deepWorkerSource()],{type:'text/javascript'}));
deepPool.maxWorkers=count;deepWisdom.maxWorkers=count;deepWisdom.workerCount=1;addDeepWorker();
return true
}catch(e){deepPool.failed=true;destroyDeepPool();return false}
}
function prewarmDeepAssets(){prepareDeepModules().then(()=>{if(deepResolutionRatio()<=128)ensureDeepPool()}).catch(()=>{})}
function kickDeepWorker(worker){
if(worker._busy||!worker._ready)return;
const r=deepPool.current;if(!r||r.cancelled||r.token!==state.token)return;
const active=Math.max(1,Math.min(deepWisdom.ready?deepWisdom.workerCount:Math.min(2,deepPool.workers.length),deepPool.workers.length));
if(worker._index>=active)return;
r.dispatch(worker)
}
function attachReference(worker,msg,ref,refLen,refKey,transfer){
let start=worker._refKey===refKey?worker._refLoaded:0;start=Math.max(0,Math.min(start,refLen+1));
if(start<refLen+1){const rr=ref.rr.slice(start,refLen+1),ri=ref.ri.slice(start,refLen+1);msg.rr=rr.buffer;msg.ri=ri.buffer;msg.rrStart=start;transfer.push(rr.buffer,ri.buffer);worker._refKey=refKey;worker._refLoaded=refLen+1}
}
function blaProfile(profile){
const z=zoomExp(),exp=profile.covered?32:(z<16?28:23);
// Fast pass + local verification/repair is cheaper than making the entire frame
// conservative. Safe strips are rebuilt at e-48 only when a probe disagrees.
const safeExp=z<16?48:Math.min(48,Math.max(32,exp+8));
return{exp,eps:Math.pow(2,-exp),safeExp}
}
function runDeepPool(profile,snap,w,h,iter,colorIter,token,t0,out,sb,refC,off,ref,refLen,series,onFallback,rects=null){
if(!ensureDeepPool())return false;if(deepPool.current)deepPool.current.cancelled=true;
const centered=pixelCenteredOffset(snap,refC,w),workers=deepPool.workers,refKey=String(ref.id),bad=[],field=makeField(w*h,colorIter),spanNormal=fixedNum(snap.span,snap.bits),offRn=fixedNum(centered.r,snap.bits),offIn=fixedNum(centered.i,snap.bits),baseCr=fixedNum(refC.re,snap.bits),baseCi=fixedNum(refC.im,snap.bits);
const cMaxRaw=Math.hypot(offRn,offIn)+Math.abs(spanNormal)*Math.hypot(.5,h/(2*Math.max(1,w))),cBucket=cMaxRaw>0&&Number.isFinite(cMaxRaw)?Math.ceil(Math.log2(cMaxRaw)*8):0,cMaxSafe=cMaxRaw>0?Math.pow(2,cBucket/8):0,bp=blaProfile(profile),useBla=Number.isFinite(spanNormal)&&Math.abs(spanNormal)>=1e-280&&refLen>8,wp=workProfile(profile),colorCtx=makeColorCtx(colorIter);
const regions=(rects?rects:[{x0:0,y0:0,w,h}]).map(r=>({x0:Math.max(0,r.x0|0),y0:Math.max(0,r.y0|0),w:Math.max(0,Math.min(w-(r.x0|0),r.w|0)),h:Math.max(0,Math.min(h-(r.y0|0),r.h|0))})).filter(r=>r.w>0&&r.h>0),chunks=[];let preferredRows=deepWisdom.stripRows||16;if(deepWisdom.rowMsEMA>0)preferredRows=Math.round(deepWisdom.targetStripMs/deepWisdom.rowMsEMA);for(const r of regions){const rows=Math.max(2,Math.min(Math.max(1,Math.floor(65536/Math.max(1,r.w))),profile.covered?Math.max(4,preferredRows):Math.min(36,preferredRows)));for(let y=r.y0;y<r.y0+r.h;y+=rows)chunks.push({x0:r.x0,y0:y,w:r.w,rows:Math.min(rows,r.y0+r.h-y)})}chunks.sort((a,b)=>{const ad=Math.hypot((a.x0+a.w*.5)/w-state.focusX,(a.y0+a.rows*.5)/h-state.focusY),bd=Math.hypot((b.x0+b.w*.5)/w-state.focusX,(b.y0+b.rows*.5)/h-state.focusY);return ad-bd});
const totalPixels=regions.reduce((a,r)=>a+r.w*r.h,0),verifyJobs=profile.covered?8:3;
const runner={token,cancelled:false,chunkIndex:0,donePixels:0,totalPixels,failed:false,finishing:false,simd:true,blaUsed:false,kernelMs:0,verifyMs:0,blaBuildMs:0,sumIter:0,blackCount:0,badCount:0,blaSteps:0,ptbSteps:0,rebases:0,interior:0,unresolved:0,repaired:0,verifiedBuckets:new Set(),
fail(err){if(this.failed||this.cancelled)return;this.failed=true;deepPool.failed=true;destroyDeepPool();if(token===state.token)onFallback()},
nextChunk(){return this.chunkIndex<chunks.length?chunks[this.chunkIndex++]:null},
dispatch(worker){
if(this.cancelled||this.failed||token!==state.token)return false;const ch=this.nextChunk();if(!ch){this.maybeFinish();return false}
const jobId=token+':'+(++deepPool.serial),vb=Math.min(verifyJobs-1,Math.max(0,Math.floor((ch.y0+ch.rows*.5)*verifyJobs/Math.max(1,h)))),verifySamples=useBla&&!this.verifiedBuckets.has(vb)?(this.verifiedBuckets.add(vb),profile.covered?4:3):0,blaKey=refKey+':'+ref.version+':'+refLen+':'+cBucket+':e'+bp.exp,safeBlaKey=refKey+':'+ref.version+':'+refLen+':'+cBucket+':e'+bp.safeExp;
const msg={type:'render',jobId,refKey,refLen,w,h,x0:ch.x0,rectW:ch.w,y0:ch.y0,rows:ch.rows,iter,colorIter,covered:profile.covered,spanMant:sb.mant,spanBucket:sb.bucket,offR:off[0],offI:off[1],offBucket:off[2],cx:w*.5,cy:h*.5,skip:series.skip,Ar:series.Ar,Ai:series.Ai,Ab:series.Ab,Br:series.Br,Bi:series.Bi,Bb:series.Bb,shift:state.shift,cycle:state.cycle,palette:state.palette,useBla,blaEps:bp.eps,blaKey,safeBlaEps:Math.pow(2,-bp.safeExp),safeBlaKey,cMax:cMaxSafe,spanNormal,offRn,offIn,baseCr,baseCi,maxBlaSteps:wp.bla,maxPtbSteps:wp.ptb,verifySamples,verifyDelta:profile.covered?8:64},transfer=[];
attachReference(worker,msg,ref,refLen,refKey,transfer);if(worker._recycle){msg.fieldBuffer=worker._recycle.field;msg.iterationBuffer=worker._recycle.iterations;msg.classBuffer=worker._recycle.classes;transfer.push(msg.fieldBuffer,msg.iterationBuffer,msg.classBuffer);worker._recycle=null}worker._busy=true;worker._job={type:'render',runner:this,jobId,x0:ch.x0,y0:ch.y0,rectW:ch.w,rows:ch.rows,pixels:ch.w*ch.rows,started:performance.now()};try{worker.postMessage(msg,transfer)}catch(e){worker._busy=false;worker._job=null;this.fail(e);return false}return true
},
onResult(worker,d,job){
const elapsed=Math.max(.05,performance.now()-job.started),mpr=elapsed/Math.max(1,job.rows);deepWisdom.rowMsEMA=deepWisdom.rowMsEMA?deepWisdom.rowMsEMA*.86+mpr*.14:mpr;deepWisdom.stripRows=Math.round((deepWisdom.stripRows||12)*.75+Math.max(3,Math.min(128,Math.round(deepWisdom.targetStripMs/Math.max(.001,deepWisdom.rowMsEMA))))*.25);
if(this.cancelled||this.failed||token!==state.token)return;if(!d||d.jobId!==job.jobId)return;if(d.error){this.fail(new Error(d.error));return}
this.simd=this.simd&&!!d.simd;this.blaUsed=this.blaUsed||!!d.bla;this.kernelMs+=d.kernelMs||0;this.verifyMs+=d.verifyMs||0;this.blaBuildMs+=d.blaBuildMs||0;this.sumIter+=d.sumIter||0;this.blackCount+=d.blackCount||0;this.badCount+=(d.bad||[]).length;const st=d.stats||{};this.blaSteps+=st.blaSteps||0;this.ptbSteps+=st.ptbSteps||0;this.rebases+=st.rebases||0;this.interior+=st.interior||0;this.unresolved+=st.unresolved||0;this.repaired+=d.repaired||0;
const rw=d.rectW||job.rectW,rx=d.x0==null?job.x0:d.x0,sf=d.field?new Float32Array(d.field):null,it=d.iterations?new Uint32Array(d.iterations):null,cl=d.classes?new Uint8Array(d.classes):null,ro=sf&&cl?colorizeField({smooth:sf,classes:cl,iter:colorIter}):new Uint8ClampedArray(d.out);for(let yy=0;yy<d.rows;yy++){const dst=(d.y0+yy)*w+rx;out.set(ro.subarray(yy*rw*4,(yy+1)*rw*4),dst*4);if(sf)field.smooth.set(sf.subarray(yy*rw,(yy+1)*rw),dst);if(it)field.iterations.set(it.subarray(yy*rw,(yy+1)*rw),dst);if(cl)field.classes.set(cl.subarray(yy*rw,(yy+1)*rw),dst)}presentPartialStripe(ro,rx,d.y0,rw,d.rows,w,h,snap);if(d.field&&d.iterations&&d.classes)worker._recycle={field:d.field,iterations:d.iterations,classes:d.classes};if(d.bad)for(const local of d.bad){const gx=rx+(local%rw),gy=d.y0+((local/rw)|0);bad.push(gy*w+gx)}this.donePixels+=job.pixels;if(profile.covered&&workers.length<deepPool.maxWorkers&&memoryLedger().managedBytes+24*1048576<rendererMemoryBudget()&&this.totalPixels-this.donePixels>job.pixels*4&&elapsed>deepWisdom.targetStripMs){const added=addDeepWorker();deepWisdom.workerCount=workers.length;deepWisdom.ready=true;kickDeepWorker(added)}this.maybeFinish()
},
maybeFinish(){
if(this.cancelled||this.failed||token!==state.token||this.finishing||this.donePixels<this.totalPixels)return;this.finishing=true;if(deepPool.current===this)deepPool.current=null;deepPool.activeToken=0;const px=Math.max(1,this.totalPixels),kmpp=this.kernelMs/px;deepTelemetry.frames++;deepTelemetry.kernelMPP=deepTelemetry.kernelMPP?deepTelemetry.kernelMPP*.78+kmpp*.22:kmpp;deepTelemetry.meanIter=this.sumIter/px;deepTelemetry.blackRatio=this.blackCount/px;deepTelemetry.badRatio=this.badCount/px;deepTelemetry.blaBuildEMA=deepTelemetry.blaBuildEMA?deepTelemetry.blaBuildEMA*.85+this.blaBuildMs*.15:this.blaBuildMs;deepTelemetry.interiorRatio=this.interior/px;deepTelemetry.repairRatio=this.repaired/Math.max(1,regions.length);deepTelemetry.unresolvedRatio=this.unresolved/px;deepTelemetry.blaStepsPerPixel=this.blaSteps/px;deepTelemetry.ptbStepsPerPixel=this.ptbSteps/px;deepTelemetry.rebasePerPixel=this.rebases/px;deepTelemetry.lastKernelMs=this.kernelMs;deepTelemetry.lastVerifyMs=this.verifyMs;deepTelemetry.lastPixels=px;deepTelemetry.refDistance=refC.dist||0;refControl.lastMPP=kmpp;refControl.lastPixels=px;if(refControl.refId!==ref.id){refControl.refId=ref.id;refControl.baseMPP=kmpp}else if((refC.dist||0)<.2)refControl.baseMPP=refControl.baseMPP?refControl.baseMPP*.8+kmpp*.2:kmpp;persistDeepWisdom();
const label='WASM '+(this.simd?'SIMD':'scalar')+' ×'+Math.max(1,Math.min(deepWisdom.workerCount,workers.length))+' · '+(this.blaUsed?'BLA e-'+bp.exp+' + ':'')+'rebase'+(this.interior?' · 内部早期終了 '+Math.round(100*this.interior/px)+'%':'')+(this.repaired?' · 局所補修 '+this.repaired:'')+(this.totalPixels<w*h?' · 既存画像再利用 '+Math.round(100*(1-this.totalPixels/(w*h)))+'%':'');
const completeField=this.totalPixels===w*h?field:null;if(!bad.length){finishRender(token,t0,profile,snap,w,h,out,label,this.totalPixels,completeField);return}let k=0;const residual=async()=>{if(token!==state.token||this.cancelled)return;const deadline=performance.now()+5;while(k<bad.length&&performance.now()<deadline){const idx=bad[k++],x=idx%w,py=(idx/w)|0,result=await highPrecisionDirectPixelAsync(snap,w,h,iter,x,py,false,()=>token!==state.token||this.cancelled);if(!result)return;const[n,m]=result;putField(field,idx,n,m,n<iter?FIELD_ESCAPED:FIELD_UNKNOWN);putDeepPixel(out,w,iter,colorIter,x,py,n,m,colorCtx)}if(k<bad.length)requestAnimationFrame(residual);else finishRender(token,t0,profile,snap,w,h,out,label+' · 残差 '+bad.length,this.totalPixels,completeField)};requestAnimationFrame(residual)
}
};
deepPool.current=runner;deepPool.activeToken=token;if(totalPixels===0){queueMicrotask(()=>runner.maybeFinish());return true}const active=Math.max(1,Math.min(deepWisdom.ready?deepWisdom.workerCount:Math.min(2,workers.length),workers.length));for(let i=0;i<active;i++)kickDeepWorker(workers[i]);return true
}
// ── Frame reprojection / world-space detail cache ───────────────────────
const frameCanvas=document.createElement('canvas'),frameCtx=frameCanvas.getContext('2d',{alpha:false});
function snapshot(){return{bits:state.bits,re:state.re,im:state.im,span:state.span}}
function snapshotToCurrent(s){if(s.bits===state.bits)return s;return{bits:state.bits,re:align(s.re,s.bits,state.bits),im:align(s.im,s.bits,state.bits),span:align(s.span,s.bits,state.bits)}}
function styleSignature(){return state.palette+':'+state.cycle.toFixed(6)+':'+state.shift.toFixed(5)}
function commitImage(data,w,h,snap,field=null){
frameCanvas.width=w;frameCanvas.height=h;const id=frameCtx.createImageData(w,h);id.data.set(data);frameCtx.putImageData(id,0,0);runtimeMetrics.canvasWrites++;
state.frameView=snapshotToCurrent(snap);state.fieldView=field?{field,w,h,snap:state.frameView}:null;invalidateView();
}
function presentPartialStripe(data,x,y,w,h,frameW,frameH,snap){if(frameCanvas.width!==frameW||frameCanvas.height!==frameH||!sameSnapshot(state.frameView,snap)){frameCanvas.width=frameW;frameCanvas.height=frameH;frameCtx.fillStyle='#050813';frameCtx.fillRect(0,0,frameW,frameH);state.frameView=snapshotToCurrent(snap);state.fieldView=null}const id=frameCtx.createImageData(w,h);id.data.set(data);frameCtx.putImageData(id,x,y);runtimeMetrics.canvasWrites++;invalidateView(false)}
function sameSnapshot(a,b){return!!a&&!!b&&a.bits===b.bits&&a.re===b.re&&a.im===b.im&&a.span===b.span}
function recolorCurrentField(){const fv=state.fieldView;if(!fv||state.dirty||state.rendering||!sameSnapshot(fv.snap,state.frameView))return false;const out=colorizeField(fv.field);commitImage(out,fv.w,fv.h,fv.snap,fv.field);state.lastEngine='フィールド再彩色';state.lastRender=0;recolorCachedDetails();invalidateView();return true}
const validationJob={active:false,key:''};
const unknownContinuationJob={active:false,key:''};let unknownContinuationTimer=0;
function cancelUnknownContinuation(){if(unknownContinuationTimer){clearTimeout(unknownContinuationTimer);unknownContinuationTimer=0}unknownContinuationJob.active=false;state.continuationProgress=0}
function scheduleUnknownContinuation(delay=120){
const fv=state.fieldView;if(state.processMode!=='fine'||state.dirty||state.rendering||!fv||!state.unresolved)return;
const key=viewSpecKey(currentViewSpec())+':'+fv.w+'x'+fv.h+':'+fv.field.iter;
if(unknownContinuationJob.key===key||unknownContinuationTimer)return;
unknownContinuationTimer=setTimeout(()=>{unknownContinuationTimer=0;startUnknownContinuation(key)},delay)
}
function startUnknownContinuation(key){
const fv=state.fieldView;if(unknownContinuationJob.active||state.dirty||state.rendering||!fv||!sameSnapshot(fv.snap,state.frameView))return;
const field=fv.field,ranked=[];for(let i=0;i<field.classes.length;i++){if(field.classes[i]!==FIELD_UNKNOWN)continue;const x=i%fv.w,y=(i/fv.w)|0;let boundary=false;for(let yy=Math.max(0,y-1);yy<=Math.min(fv.h-1,y+1)&&!boundary;yy++)for(let xx=Math.max(0,x-1);xx<=Math.min(fv.w-1,x+1);xx++)if(field.classes[yy*fv.w+xx]===FIELD_ESCAPED){boundary=true;break}if(boundary)ranked.push({i,d:Math.hypot(x/fv.w-state.focusX,y/fv.h-state.focusY)})}if(!ranked.length)return;
const deep=deepEngineNeeded(fv.snap,fv.w),cap=Math.min(ranked.length,deep?384:4096);ranked.sort((a,b)=>a.d-b.d);const indices=ranked.slice(0,cap).map(v=>v.i);
unknownContinuationJob.active=true;unknownContinuationJob.key=key;const token=state.token,snap=fv.snap,baseIter=field.iter,extendedIter=Math.min(280000,Math.max(baseIter+256,baseIter*2)),cre=fixedNum(snap.re,snap.bits),cim=fixedNum(snap.im,snap.bits),scale=fixedNum(snap.span,snap.bits)/fv.w;let p=0;
state.drawState='RESOLVING';state.continuationProgress=0;invalidateStats();
async function slice(){
if(token!==state.token||state.dirty){unknownContinuationJob.active=false;return}
const deadline=performance.now()+7;while(p<indices.length&&performance.now()<deadline){const i=indices[p++],x=i%fv.w,y=(i/fv.w)|0,result=deep?await highPrecisionDirectPixelAsync(snap,fv.w,fv.h,extendedIter,x,y,false,()=>token!==state.token||state.dirty):exportSampleShallow(cre,cim,scale,fv.w,fv.h,extendedIter,x,y);if(!result){unknownContinuationJob.active=false;return}const[n,m]=result;putField(field,i,n,m,n<extendedIter?FIELD_ESCAPED:FIELD_UNKNOWN)}
state.continuationProgress=p/indices.length;if(p<indices.length){invalidateStats();requestAnimationFrame(slice);return}
state.unresolved=field.classes.reduce((n,c)=>n+(c===FIELD_UNKNOWN),0);unknownContinuationJob.active=false;state.continuationProgress=1;state.drawState='COVERED';commitImage(colorizeField(field),fv.w,fv.h,snap,field);state.lastEngine+=' · 境界 '+indices.length+'点を追加反復';invalidateStats()
}
requestAnimationFrame(slice)
}
function scheduleValidation(delay=350){if(state.processMode!=='validate'||state.dirty||state.rendering)return;scheduleBackground(()=>{if(state.processMode==='validate'&&!state.detailActive&&!unknownContinuationJob.active)startValidation();else if(state.processMode==='validate')scheduleValidation(300)},delay)}
function startValidation(){
const fv=state.fieldView;if(validationJob.active||!fv||state.dirty||state.rendering||!sameSnapshot(fv.snap,state.frameView))return;
const key=viewSpecKey(currentViewSpec())+':'+fv.w+'x'+fv.h+':'+fv.field.iter;if(validationJob.key===key)return;
validationJob.active=true;validationJob.key=key;
const token=state.token,snap=fv.snap,field=fv.field,baseIter=field.iter,extendedIter=Math.min(280000,Math.max(baseIter+256,baseIter*2));let i=0,lastStatus=0;
state.drawState='VALIDATING';state.coverage=0;invalidateStats();
async function slice(now){
if(token!==state.token||state.processMode!=='validate'){validationJob.active=false;return}
const deadline=performance.now()+9;
while(i<field.classes.length&&performance.now()<deadline){
const old=field.classes[i],x=i%fv.w,y=(i/fv.w)|0,limit=old===FIELD_ESCAPED?baseIter:extendedIter;
if(fixedAnalyticPixelProven(snap,fv.w,fv.h,x,y)){putField(field,i,limit,0,FIELD_INTERIOR_PROVEN);i++;continue}
const result=await highPrecisionDirectPixelAsync(snap,fv.w,fv.h,limit,x,y,true,()=>token!==state.token||state.processMode!=='validate');
if(!result){validationJob.active=false;return}
const[n,m]=result;if(n<limit)putField(field,i,n,m,FIELD_ESCAPED);else putField(field,i,n,0,FIELD_UNKNOWN);i++
}
if(now-lastStatus>180){lastStatus=now;state.coverage=i/field.classes.length;invalidateStats()}
if(i<field.classes.length){requestAnimationFrame(slice);return}
field.iter=extendedIter;state.unresolved=field.classes.reduce((n,c)=>n+(c!==FIELD_ESCAPED&&c!==FIELD_INTERIOR_PROVEN),0);state.drawState=state.unresolved?'VALIDATION_INCOMPLETE':'VALIDATED';state.coverage=1;validationJob.active=false;const out=colorizeField(field);commitImage(out,fv.w,fv.h,snap,field);state.lastEngine='高精度direct照合';invalidateStats()
}
requestAnimationFrame(slice)
}
function drawWorldDetail(entry){
if(!entry||!entry.complete||entry.style!==styleSignature()||!entry.canvas)return;if(entry.iter){const rr=entry.iter/Math.max(1,maxIter());if(rr<.82||rr>1.22)return}
const re=align(entry.re,entry.bits,state.bits),im=align(entry.im,entry.bits,state.bits),sxv=align(entry.spanX,entry.bits,state.bits),syv=align(entry.spanY,entry.bits,state.bits);
const x=canvas.width*.5+fixedRatio(re-state.re,state.span)*canvas.width,y=canvas.height*.5-fixedRatio(im-state.im,state.span)*canvas.width;
const dw=Math.abs(fixedRatio(sxv,state.span)*canvas.width),dh=Math.abs(fixedRatio(syv,state.span)*canvas.width);
if(!Number.isFinite(x+y+dw+dh)||dw<5||dh<5||x+dw*.5<0||x-dw*.5>canvas.width||y+dh*.5<0||y-dh*.5>canvas.height)return;
// Do not magnify a cached tile beyond ~1.7 display pixels per source pixel.
if(dw/Math.max(1,entry.canvas.width)>1.7)return;
entry.lastUsed=performance.now();
ctx.imageSmoothingEnabled=true;try{ctx.imageSmoothingQuality='high'}catch{}
ctx.drawImage(entry.canvas,x-dw*.5,y-dh*.5,dw,dh);
}
function paintFrame(){
if(!state.frameView||!frameCanvas.width)return;
runtimeMetrics.canvasWrites++;
const fv=state.frameView;
const a=fixedRatio(fv.span,state.span);if(!Number.isFinite(a)||a<=0)return;
const dx=fixedRatio(fv.re-state.re,state.span)*canvas.width;
const dy=-fixedRatio(fv.im-state.im,state.span)*canvas.width;
ctx.save();ctx.setTransform(1,0,0,1,0,0);ctx.fillStyle='#050813';ctx.fillRect(0,0,canvas.width,canvas.height);
ctx.translate(canvas.width*.5+dx,canvas.height*.5+dy);ctx.scale(a,a);ctx.imageSmoothingEnabled=true;try{ctx.imageSmoothingQuality=state.lastPass===RENDER_PASS.COVERED?'high':'medium'}catch{}
const fw=canvas.width,fh=fw*(frameCanvas.height/Math.max(1,frameCanvas.width));ctx.drawImage(frameCanvas,-fw*.5,-fh*.5,fw,fh);ctx.restore();
// Only completed, world-validated tiles are composited. Partial tiles remain offscreen.
ctx.save();ctx.setTransform(1,0,0,1,0,0);for(const e of DETAIL_TILE_CACHE.values())drawWorldDetail(e);ctx.restore();
}
function renderBudget(profile,deep){return profile.budgetMs}
function targetSize(profile,deep){
// Resolution is intentionally independent of zoom depth. 10^20, 10^100 and
// beyond use the same quality targets as low zoom; only the numerical engine changes.
const cssW=Math.max(1,canvas.clientWidth),cssH=Math.max(1,canvas.clientHeight),aspect=cssH/cssW,dpr=Math.max(.25,state.effectiveDpr||1);
if(profile.covered)return[canvas.width,canvas.height];
let nominal=Math.max(profile.minWidth,cssW*dpr*profile.nominalScale),minW=profile.minWidth,maxW=profile.maxWidth;
const measured=renderPerf[deep?'deepMPP':'shallowMPP'];
if(deep&&!profile.covered&&measured<=0){nominal=Math.min(nominal,48);minW=32}
if(measured>0){
const timedW=Math.sqrt(renderBudget(profile,deep)/Math.max(1e-7,measured*aspect));
nominal=Math.min(nominal,timedW);minW=deep?32:240
}
let w=Math.max(minW,Math.min(maxW,Math.round(nominal))),h=Math.max(deep&&!profile.covered?32:96,Math.round(w*aspect));
if(h>2200){const sc=2200/h;h=2200;w=Math.round(w*sc)}return[w,h]
}
function analyzeDetailTiles(data,w,h,baseMs,deep){
const field=state.fieldView&&state.fieldView.w===w&&state.fieldView.h===h?state.fieldView.field:null;if(!field)return[];
const leaves=[],minTile=32,rootTile=256;
function scoreRect(x0,y0,tw,th){
const step=Math.max(1,Math.floor(Math.min(tw,th)/14));let grad=0,edges=0,uncertain=0,samples=0;
for(let y=y0;y<y0+th;y+=step)for(let x=x0;x<x0+tw;x+=step){const i=y*w+x,c=field.classes[i],s=field.smooth[i];samples++;uncertain+=(255-(field.confidence?field.confidence[i]:fieldConfidence(c)))/255;if(x+step<x0+tw){const j=i+step,c2=field.classes[j];if((c===FIELD_ESCAPED)!==(c2===FIELD_ESCAPED))edges++;else if(c===FIELD_ESCAPED&&c2===FIELD_ESCAPED)grad+=Math.min(12,Math.abs(s-field.smooth[j]))}if(y+step<y0+th){const j=i+step*w,c2=field.classes[j];if((c===FIELD_ESCAPED)!==(c2===FIELD_ESCAPED))edges++;else if(c===FIELD_ESCAPED&&c2===FIELD_ESCAPED)grad+=Math.min(12,Math.abs(s-field.smooth[j]))}}
return edges/Math.max(1,samples)*4.4+grad/Math.max(1,samples*12)*1.5+uncertain/Math.max(1,samples)*.9
}
function visit(x,y,tw,th){const score=scoreRect(x,y,tw,th);if(score<.055)return;if((tw>minTile||th>minTile)&&score>.11){const aw=Math.max(1,tw>>1),bw=tw-aw,ah=Math.max(1,th>>1),bh=th-ah;visit(x,y,aw,ah);if(bw)visit(x+aw,y,bw,ah);if(bh)visit(x,y+ah,aw,bh);if(bw&&bh)visit(x+aw,y+ah,bw,bh);return}leaves.push({x,y,w:tw,h:th,score,area:tw*th})}
for(let y=0;y<h;y+=rootTile)for(let x=0;x<w;x+=rootTile)visit(x,y,Math.min(rootTile,w-x),Math.min(rootTile,h-y));
leaves.sort((a,b)=>{const ad=Math.hypot((a.x+a.w*.5)/w-state.focusX,(a.y+a.h*.5)/h-state.focusY),bd=Math.hypot((b.x+b.w*.5)/w-state.focusX,(b.y+b.h*.5)/h-state.focusY);return(b.score+.08/(.08+bd))-(a.score+.08/(.08+ad))});const byteCap=Math.max(0,detailCacheBudget()-detailCacheBytes),baseBudget=renderBudget(RENDER_PROFILE[RENDER_PASS.COVERED],deep),spare=Math.max(0,baseBudget*1.7-baseMs),sampleCap=Math.floor(w*h*Math.min(2.6,spare/Math.max(1,baseMs)));let bytes=0,sampleArea=0,n=0;while(n<leaves.length){const tile=leaves[n],sampleScale=tile.score>=1.15?4:2,costBytes=tile.area*(sampleScale*sampleScale*10+4),costSamples=tile.area*sampleScale*sampleScale;if(bytes+costBytes>byteCap||sampleArea+costSamples>sampleCap)break;bytes+=costBytes;sampleArea+=costSamples;n++}return leaves.slice(0,n)
}
function detailGeometry(tile,plan){
const s=plan.snap,den=BigInt(2*plan.w),re=s.re+s.span*BigInt(2*tile.x+tile.w-plan.w)/den,im=s.im+s.span*BigInt(plan.h-2*tile.y-tile.h)/den;
const spanX=s.span*BigInt(tile.w)/BigInt(plan.w),spanY=s.span*BigInt(tile.h)/BigInt(plan.w);
return{bits:s.bits,re,im,spanX,spanY}
}
function detailKey(tile,plan,sampleScale){const g=detailGeometry(tile,plan);return [g.bits,g.re,g.im,g.spanX,g.spanY,plan.iter,'aa'+sampleScale,'centered-v23'].join(':')}
function rendererMemoryBudget(){return(Number(navigator.deviceMemory||8)<=4||matchMedia('(max-width:700px)').matches?96:192)*1048576}
function wasmBytes(core){try{return core&&core.ex&&core.ex.memory?core.ex.memory.buffer.byteLength:0}catch{return 0}}
function memoryLedger(includeDetail=true){
const screenCanvasBytes=canvas.width*canvas.height*4,frameCanvasBytes=frameCanvas.width*frameCanvas.height*4;
const fieldBytes=state.fieldView?state.fieldView.w*state.fieldView.h*10:0;
const referenceBytes=(referenceCache.rr?.byteLength||0)+(referenceCache.ri?.byteLength||0);
const mainWasmBytes=wasmBytes(wasm)+wasmBytes(deepWasm);
// Worker memories are isolated, so browsers do not expose their byteLength.
// Account them conservatively: reference copies + kernel scratch/linear memory.
const workerEstimateBytes=(shallowWorker?2*1048576:0)+deepPool.workers.length*24*1048576;
const renderBytes=state.rendering?(()=>{const size=targetSize(renderProfile(state.lastPass),deepMode),pixels=size[0]*size[1];return pixels*18})():0;
const exportBytes=exportJob&&exportJob.active?exportJob.bytes||0:0,activeDetailBytes=activeDetailTiles.reduce((sum,e)=>sum+(e.transientBytes||0),0);
const cacheBytes=includeDetail?detailCacheBytes:0;
const managedBytes=fieldBytes+referenceBytes+mainWasmBytes+workerEstimateBytes+renderBytes+exportBytes+activeDetailBytes+cacheBytes;
const canvasBytes=screenCanvasBytes+frameCanvasBytes;
return{managedBytes,logicalBytes:managedBytes+canvasBytes,canvasBytes,screenCanvasBytes,frameCanvasBytes,fieldBytes,referenceBytes,mainWasmBytes,workerEstimateBytes,renderBytes,exportBytes,activeDetailBytes,detailCacheBytes:cacheBytes,budget:rendererMemoryBudget()}
}
function detailCacheBudget(){const base=memoryLedger(false).managedBytes,reserve=16*1048576;return Math.max(0,Math.floor(Math.min(rendererMemoryBudget()*.18,rendererMemoryBudget()-base-reserve)))}
function detailEntryBytes(entry){return entry&&entry.canvas?entry.canvas.width*entry.canvas.height*4+(entry.field?entry.field.classes.length*10:0):0}
function detailDistance(entry){try{const re=align(entry.re,entry.bits,state.bits),im=align(entry.im,entry.bits,state.bits);return Math.hypot(fixedRatio(re-state.re,state.span),fixedRatio(im-state.im,state.span))}catch{return Infinity}}
function trimDetailCache(){const budget=detailCacheBudget();while(detailCacheBytes>budget&&DETAIL_TILE_CACHE.size){let victim=null,rank=-Infinity;const now=performance.now();for(const [key,entry]of DETAIL_TILE_CACHE){const age=Math.max(0,now-(entry.lastUsed||0))/60000,distance=detailDistance(entry),score=(Number.isFinite(distance)?distance:1000)*4+age;if(score>rank){rank=score;victim=[key,entry]}}if(!victim)break;DETAIL_TILE_CACHE.delete(victim[0]);detailCacheBytes=Math.max(0,detailCacheBytes-detailEntryBytes(victim[1]))}}
function linearChannel(v){v/=255;return v<=.04045?v/12.92:Math.pow((v+.055)/1.055,2.4)}
function srgbChannel(v){v=Math.max(0,Math.min(1,v));return Math.round(255*(v<=.0031308?12.92*v:1.055*Math.pow(v,1/2.4)-.055))}
function resolveSubsampleField(field,w,h,scale=2){const hi=colorizeField(field),c=document.createElement('canvas');c.width=w;c.height=h;const cc=c.getContext('2d',{alpha:false}),id=cc.createImageData(w,h),out=id.data,samples=scale*scale;for(let y=0;y<h;y++)for(let x=0;x<w;x++){let r=0,g=0,b=0;for(let sy=0;sy<scale;sy++)for(let sx=0;sx<scale;sx++){const i=(((y*scale+sy)*w*scale)+(x*scale+sx))*4;r+=linearChannel(hi[i]);g+=linearChannel(hi[i+1]);b+=linearChannel(hi[i+2])}const oi=(y*w+x)*4;out[oi]=srgbChannel(r/samples);out[oi+1]=srgbChannel(g/samples);out[oi+2]=srgbChannel(b/samples);out[oi+3]=255}cc.putImageData(id,0,0);return c}
function recolorDetailEntry(entry){if(!entry||!entry.field)return;entry.canvas=resolveSubsampleField(entry.field,entry.baseW,entry.baseH,entry.sampleScale||2);entry.style=styleSignature()}
function recolorCachedDetails(){cancelDetailRefinement(true);for(const entry of DETAIL_TILE_CACHE.values())recolorDetailEntry(entry)}
function makeDetailTask(tile,plan){
const sampleScale=tile.score>=1.15?4:2,key=detailKey(tile,plan,sampleScale),cached=DETAIL_TILE_CACHE.get(key);if(cached){DETAIL_TILE_CACHE.delete(key);DETAIL_TILE_CACHE.set(key,cached);cached.lastUsed=performance.now();if(cached.style!==styleSignature())recolorDetailEntry(cached);return{cached:true,entry:cached}}
const c=document.createElement('canvas');c.width=Math.max(2,tile.w*sampleScale);c.height=Math.max(2,tile.h*sampleScale);const dc=c.getContext('2d',{alpha:false});
dc.imageSmoothingEnabled=true;try{dc.imageSmoothingQuality='high'}catch{};dc.drawImage(frameCanvas,tile.x,tile.y,tile.w,tile.h,0,0,c.width,c.height);
const g=detailGeometry(tile,plan),entry={...g,canvas:c,field:null,baseW:tile.w,baseH:tile.h,sampleScale,style:styleSignature(),key,score:tile.score,iter:plan.iter,complete:false,lastUsed:performance.now(),transientBytes:c.width*c.height*18};
const task={tile,canvas:c,ctx:dc,field:makeField(c.width*c.height,plan.iter),entry,phases:0,plan};activeDetailTiles.push(entry);return task
}
function validateDetailTask(task){
const plan=task&&task.plan,t=task&&task.tile,base=plan&&plan.baseData;if(!plan||!t||!base)return false;
let samples=0,blackMismatch=0,rgbDiff=0,colorSamples=0;const sampleScale=task.entry.sampleScale||2,step=Math.max(3,Math.floor(Math.min(t.w,t.h)/7)),pix=task.ctx.getImageData(0,0,task.canvas.width,task.canvas.height).data;
for(let by=t.y+1;by<t.y+t.h-1;by+=step)for(let bx=t.x+1;bx<t.x+t.w-1;bx+=step){
const bi=(by*plan.w+bx)*4,lx=Math.min(task.canvas.width-1,Math.max(0,sampleScale*(bx-t.x)+(sampleScale>>1))),ly=Math.min(task.canvas.height-1,Math.max(0,sampleScale*(by-t.y)+(sampleScale>>1))),hi=(ly*task.canvas.width+lx)*4;
const bb=(base[bi]|base[bi+1]|base[bi+2])===0,hb=(pix[hi]|pix[hi+1]|pix[hi+2])===0;samples++;if(bb!==hb)blackMismatch++;else if(!bb){rgbDiff+=Math.abs(base[bi]-pix[hi])+Math.abs(base[bi+1]-pix[hi+1])+Math.abs(base[bi+2]-pix[hi+2]);colorSamples++}
}
if(samples<4)return false;const classRate=blackMismatch/samples,meanDiff=colorSamples?rgbDiff/(3*colorSamples):0;return classRate<=.035&&meanDiff<=48
}
function cacheDetailTask(task){
if(!task||task.cached)return;const i=activeDetailTiles.indexOf(task.entry);if(i>=0)activeDetailTiles.splice(i,1);
if(!validateDetailTask(task))return;fillFieldConfidence(task.field);task.entry.field=task.field;task.entry.canvas=resolveSubsampleField(task.field,task.tile.w,task.tile.h,task.entry.sampleScale);task.entry.style=styleSignature();task.entry.complete=true;task.entry.lastUsed=performance.now();task.entry.transientBytes=0;const old=DETAIL_TILE_CACHE.get(task.entry.key);if(old)detailCacheBytes-=detailEntryBytes(old);DETAIL_TILE_CACHE.delete(task.entry.key);DETAIL_TILE_CACHE.set(task.entry.key,task.entry);detailCacheBytes+=detailEntryBytes(task.entry);
trimDetailCache()
}
function phaseRect(task,phase){
const t=task.tile,scale=task.entry.sampleScale,left=(phase%scale)*t.w,top=((phase/scale)|0)*t.h;
return{x0:t.x*scale+left,y0:t.y*scale+top,rw:t.w,rows:t.h,dx:left,dy:top}
}
function prepareDeepTileContext(plan,iter,sampleScale,done){
const cacheKey='i'+iter+':s'+sampleScale;if(plan.deepCtx&&plan.deepCtx[cacheKey]){done(plan.deepCtx[cacheKey]);return}if(!plan.deepCtx)plan.deepCtx={};
const snap=plan.snap,refC=chooseReference(snap),W=plan.w*sampleScale,H=plan.h*sampleScale,refX=W*.5-.5+fixedRatio(refC.re-snap.re,snap.span)*W,refY=H*.5-.5-fixedRatio(refC.im-snap.im,snap.span)*W;
const cornerR=Math.max(Math.hypot(refX,refY),Math.hypot(W-refX,refY),Math.hypot(refX,H-refY),Math.hypot(W-refX,H-refY))/Math.max(1,W),logMaxDc=log2FixedAt(snap.span,snap.bits)+Math.log2(Math.max(1e-300,cornerR));
buildReferenceAt(snap.bits,refC.re,refC.im,iter,state.token,(ref,refLen)=>{
if(plan.gen!==state.detailGeneration)return;const series=computeSeries(ref,refLen,logMaxDc),centered=pixelCenteredOffset(snap,refC,W),sb=spanMantBucket(snap.span,snap.bits),off=fixedComplexScaled(centered.r,centered.i,snap.bits),spanNormal=fixedNum(snap.span,snap.bits),offRn=fixedNum(centered.r,snap.bits),offIn=fixedNum(centered.i,snap.bits);
const cMaxRaw=Math.hypot(offRn,offIn)+Math.abs(spanNormal)*Math.hypot(.5,H/(2*Math.max(1,W))),cBucket=cMaxRaw>0&&Number.isFinite(cMaxRaw)?Math.ceil(Math.log2(cMaxRaw)*8):0,cMaxSafe=cMaxRaw>0?Math.pow(2,cBucket/8):0;
const ctx={snap,refC,ref,refLen,series,sb,off,spanNormal,offRn,offIn,cBucket,cMaxSafe,W,H,iter};plan.deepCtx[cacheKey]=ctx;done(ctx)
})
}
function renderGuardedDeepDetailRect(task,phase,iter,done){
const plan=task.plan,r=phaseRect(task,phase),W=plan.w*task.entry.sampleScale,H=plan.h*task.entry.sampleScale,snap=plan.snap;let yy=0;
async function slice(){if(plan.gen!==state.detailGeneration){done(false);return}const deadline=performance.now()+6;while(yy<r.rows&&performance.now()<deadline){const gy=r.y0+yy;for(let xx=0;xx<r.rw;xx++){const gx=r.x0+xx,result=await highPrecisionDirectPixelAsync(snap,W,H,iter,gx,gy,true,()=>plan.gen!==state.detailGeneration);if(!result){done(false);return}const[n,m]=result,fi=(r.dy+yy)*task.canvas.width+r.dx+xx;putField(task.field,fi,n,m,n<iter?FIELD_ESCAPED:FIELD_UNKNOWN)}yy++}if(yy<r.rows){requestAnimationFrame(slice);return}const stripe=makeField(r.rw*r.rows,iter);for(let y=0;y<r.rows;y++){const src=(r.dy+y)*task.canvas.width+r.dx,dst=y*r.rw;stripe.smooth.set(task.field.smooth.subarray(src,src+r.rw),dst);stripe.iterations.set(task.field.iterations.subarray(src,src+r.rw),dst);stripe.classes.set(task.field.classes.subarray(src,src+r.rw),dst)}const id=task.ctx.createImageData(r.rw,r.rows);id.data.set(colorizeField(stripe));task.ctx.putImageData(id,r.dx,r.dy);task.phases|=(1<<phase);invalidateView(false);done(true)}
requestAnimationFrame(slice)
}
function sendDeepDetailRect(task,phase,iter,colorIter,done){
const plan=task.plan,gen=plan.gen;if(gen!==state.detailGeneration){done(false);return}if(state.processMode==='validate'||task.tile.score>1.35){renderGuardedDeepDetailRect(task,phase,iter,done);return}if(!ensureDeepPool()){done(false);return}
prepareDeepTileContext(plan,iter,task.entry.sampleScale,dc=>{
if(gen!==state.detailGeneration){done(false);return}const worker=deepPool.workers.find(w=>!w._busy);if(!worker){scheduleBackground(()=>sendDeepDetailRect(task,phase,iter,colorIter,done),18);return}
const r=phaseRect(task,phase),ref=dc.ref,refKey=String(ref.id),bpExp=32,useBla=Number.isFinite(dc.spanNormal)&&Math.abs(dc.spanNormal)>=1e-280&&dc.refLen>8,blaEps=Math.pow(2,-bpExp),blaKey=refKey+':'+ref.version+':'+dc.refLen+':'+dc.cBucket+':e'+bpExp;
const jobId='detail:'+gen+':'+Date.now()+':'+Math.random(),msg={type:'render',jobId,refKey,refLen:dc.refLen,w:dc.W,h:dc.H,x0:r.x0,y0:r.y0,rectW:r.rw,rows:r.rows,iter,colorIter,spanMant:dc.sb.mant,spanBucket:dc.sb.bucket,offR:dc.off[0],offI:dc.off[1],offBucket:dc.off[2],cx:dc.W*.5,cy:dc.H*.5,skip:dc.series.skip,Ar:dc.series.Ar,Ai:dc.series.Ai,Ab:dc.series.Ab,Br:dc.series.Br,Bi:dc.series.Bi,Bb:dc.series.Bb,shift:state.shift,cycle:state.cycle,palette:state.palette,useBla,blaEps,blaKey,cMax:dc.cMaxSafe,spanNormal:dc.spanNormal,offRn:dc.offRn,offIn:dc.offIn,baseCr:fixedNum(dc.refC.re,dc.snap.bits),baseCi:fixedNum(dc.refC.im,dc.snap.bits),maxBlaSteps:0,maxPtbSteps:0,verifySamples:3,verifyDelta:6,safeBlaEps:Math.pow(2,-48),safeBlaKey:blaKey+':safe48'},transfer=[];
let start=worker._refKey===refKey?worker._refLoaded:0;start=Math.max(0,Math.min(start,dc.refLen+1));if(start<dc.refLen+1){const rr=ref.rr.slice(start,dc.refLen+1),ri=ref.ri.slice(start,dc.refLen+1);msg.rr=rr.buffer;msg.ri=ri.buffer;msg.rrStart=start;transfer.push(rr.buffer,ri.buffer);worker._refKey=refKey;worker._refLoaded=dc.refLen+1}
const runner={token:state.token,cancelled:false,fail(){done(false)},onResult(w,d){if(gen!==state.detailGeneration||!d||d.error){done(false);return}const sf=d.field?new Float32Array(d.field):null,it=d.iterations?new Uint32Array(d.iterations):null,cl=d.classes?new Uint8Array(d.classes):null,arr=sf&&cl?colorizeField({smooth:sf,classes:cl,iter:colorIter}):new Uint8ClampedArray(d.out),id=task.ctx.createImageData(r.rw,r.rows);id.data.set(arr);task.ctx.putImageData(id,r.dx,r.dy);if(sf&&it&&cl)for(let yy=0;yy<r.rows;yy++){const dst=(r.dy+yy)*task.canvas.width+r.dx;task.field.smooth.set(sf.subarray(yy*r.rw,(yy+1)*r.rw),dst);task.field.iterations.set(it.subarray(yy*r.rw,(yy+1)*r.rw),dst);task.field.classes.set(cl.subarray(yy*r.rw,(yy+1)*r.rw),dst)}if(d.field&&d.iterations&&d.classes)w._recycle={field:d.field,iterations:d.iterations,classes:d.classes};task.phases|=(1<<phase);task.entry.canvas=task.canvas;invalidateView(false);done(true)}};
if(worker._recycle){msg.fieldBuffer=worker._recycle.field;msg.iterationBuffer=worker._recycle.iterations;msg.classBuffer=worker._recycle.classes;transfer.push(msg.fieldBuffer,msg.iterationBuffer,msg.classBuffer);worker._recycle=null}worker._busy=true;worker._job={type:'render',runner,jobId,y0:r.y0,rows:r.rows,started:performance.now()};try{worker.postMessage(msg,transfer)}catch(e){worker._busy=false;worker._job=null;done(false)}
})
}
function renderShallowDetailRect(task,phase,done){
const plan=task.plan,r=phaseRect(task,phase),sampleScale=task.entry.sampleScale,W=plan.w*sampleScale,H=plan.h*sampleScale,iter=plan.iter,snap=plan.snap,sp=fixedNum(snap.span,snap.bits),cre=fixedNum(snap.re,snap.bits),cim=fixedNum(snap.im,snap.bits),scale=sp/W,colorCtx=makeColorCtx(iter),out=new Uint8ClampedArray(r.rw*r.rows*4);let yy=0;
function inBulbs(cr,ci){const y2=ci*ci,x=cr-.25,q=x*x+y2;if(q*(q+x)<=.25*y2)return true;const x2=cr+1;return x2*x2+y2<=.0625}
function slice(){if(plan.gen!==state.detailGeneration){done(false);return}const deadline=performance.now()+5;while(yy<r.rows&&performance.now()<deadline){const gy=r.y0+yy,ci=cim+(H*.5-gy-.5)*scale;let oi=yy*r.rw*4;for(let xx=0;xx<r.rw;xx++){const gx=r.x0+xx,cr=cre+(gx+.5-W*.5)*scale;let zr=0,zi=0,zr2=0,zi2=0,n=0,inside=inBulbs(cr,ci);if(inside)n=iter;else while(n<iter&&zr2+zi2<=4){zi=(zr+zr)*zi+ci;zr=zr2-zi2+cr;zr2=zr*zr;zi2=zi*zi;n++}const m=n<iter?Math.max(4.0000001,zr2+zi2):0,fi=(r.dy+yy)*task.canvas.width+r.dx+xx;putField(task.field,fi,n,m,n<iter?FIELD_ESCAPED:(inside?FIELD_INTERIOR_LIKELY:FIELD_UNKNOWN));if(n>=iter){out[oi]=out[oi+1]=out[oi+2]=0;out[oi+3]=255}else putFastColor(out,oi,n,m,iter,colorCtx);oi+=4}yy++}if(yy<r.rows)requestAnimationFrame(slice);else{const id=task.ctx.createImageData(r.rw,r.rows),data=id.data;data.set(out);task.ctx.putImageData(id,r.dx,r.dy);task.phases|=(1<<phase);invalidateView(false);done(true)}}requestAnimationFrame(slice)
}
function pumpDetailRefinement(){
const plan=detailPlan;if(!plan||plan.gen!==state.detailGeneration||!state.hq){state.detailActive=false;return}if(state.rendering||unknownContinuationJob.active||performance.now()-state.lastInteraction<720){scheduleBackground(pumpDetailRefinement,90);return}
while(plan.index<plan.tasks.length&&plan.tasks[plan.index].cached)plan.index++;
if(plan.index>=plan.tasks.length){state.detailActive=false;state.detailDone=plan.tasks.length;state.drawState='REFINED';updateStats();scheduleValidation();return}
const task=plan.tasks[plan.index],phase=task.nextPhase||0,cb=ok=>{if(!ok){const i=activeDetailTiles.indexOf(task.entry);if(i>=0)activeDetailTiles.splice(i,1);plan.index++;scheduleBackground(pumpDetailRefinement,10);return}task.nextPhase=phase+1;if(task.nextPhase>=task.entry.sampleScale*task.entry.sampleScale){cacheDetailTask(task);plan.index++}state.detailDone=plan.tasks.reduce((n,t)=>n+(t.cached||t.entry.complete?1:0),0);updateStats();scheduleBackground(pumpDetailRefinement,0)};
if(plan.deep)sendDeepDetailRect(task,phase,plan.iter,plan.iter,cb);else renderShallowDetailRect(task,phase,cb)
}
// ── Idle-only adaptive detail refinement ────────────────────────────────
function scheduleDetailRefinement(data,w,h,snap,iter,baseMs,deep){
if(!state.hq)return;cancelDetailRefinement(true);const gen=state.detailGeneration,tiles=analyzeDetailTiles(data,w,h,baseMs,deep);if(!tiles.length){state.drawState='REFINED';invalidateStats();scheduleValidation();return}
const plan={gen,snap,w,h,iter,deep,tiles,tasks:[],index:0,deepCtx:null,baseData:data};for(const t of tiles)plan.tasks.push(makeDetailTask(t,plan));detailPlan=plan;state.detailActive=true;state.drawState='REFINING';state.detailQueued=plan.tasks.length;state.detailDone=plan.tasks.filter(t=>t.cached).length;scheduleBackground(pumpDetailRefinement,160)
}
function finishRender(token,t0,profile,snap,w,h,out,engine,computedPixels=w*h,field=null){
if(token!==state.token)return;const elapsed=Math.max(.1,performance.now()-t0),deep=deepEngineNeeded(snap,Math.max(1,canvas.width));
// Reproject-only frames must not poison performance estimates with near-zero work.
if(computedPixels>Math.max(512,w*h*.01)){const mpp=elapsed/computedPixels,key=deep?'deepMPP':'shallowMPP';renderPerf[key]=renderPerf[key]?renderPerf[key]*.72+mpp*.28:mpp}
if(field){fillFieldConfidence(field);out=colorizeField(field)}commitImage(out,w,h,snap,field);state.rendering=false;state.lastRender=elapsed;state.lastPass=profile.id;state.dirty=false;state.lastEngine=engine;state.lastFrameDone=performance.now();state.coverage=Math.min(1,w*h/Math.max(1,canvas.width*canvas.height));state.drawState=profile.covered&&state.coverage>=.999?'COVERED':'PREVIEW';state.unresolved=field?field.classes.reduce((n,c)=>n+(c===FIELD_UNKNOWN),0):0;flushPendingPrecision();
if(!profile.covered&&adaptStandardDeepBudget(deep)){updateStats();return}
if(profile.covered&&state.unresolved)scheduleUnknownContinuation();
if(profile.covered&&state.hq){const gen=state.detailGeneration;const schedule=()=>{if(gen!==state.detailGeneration||state.rendering)return;if(unknownContinuationJob.active||unknownContinuationTimer){scheduleBackground(schedule,90);return}const fv=state.fieldView,base=fv&&sameSnapshot(fv.snap,snap)?colorizeField(fv.field):out;scheduleDetailRefinement(base,w,h,snap,fv?fv.field.iter:iterationPlan(profile,deep).colorIter,elapsed,deep)};scheduleIdle(schedule,350);clearPanReuse()}
if(profile.covered&&state.processMode==='validate')scheduleValidation();
updateStats();
}
// ── Shallow renderers ───────────────────────────────────────────────────
function renderShallowReuse(profile,snap,w,h,iter,token,t0,reuse){
const out=reuse.out,colorCtx=makeColorCtx(iter),sp=fixedNum(snap.span,snap.bits),cre=fixedNum(snap.re,snap.bits),cim=fixedNum(snap.im,snap.bits),scale=sp/w,rects=reuse.rects;let ri=0,yy=0;
function inBulbs(cr,ci){const y2=ci*ci,x=cr-.25,qq=x*x+y2;if(qq*(qq+x)<=.25*y2)return true;const x2=cr+1;return x2*x2+y2<=.0625}
function slice(){if(token!==state.token)return;const deadline=performance.now()+7;while(ri<rects.length&&performance.now()<deadline){const r=rects[ri];while(yy<r.h&&performance.now()<deadline){const gy=r.y0+yy,ci=cim+(h*.5-gy-.5)*scale;for(let xx=0;xx<r.w;xx++){const gx=r.x0+xx,cr=cre+(gx+.5-w*.5)*scale;let zr=0,zi=0,zr2=0,zi2=0,n=0;if(inBulbs(cr,ci))n=iter;else while(n<iter&&zr2+zi2<=4){zi=(zr+zr)*zi+ci;zr=zr2-zi2+cr;zr2=zr*zr;zi2=zi*zi;n++}const oi=(gy*w+gx)*4;if(n>=iter){out[oi]=out[oi+1]=out[oi+2]=0;out[oi+3]=255}else putFastColor(out,oi,n,Math.max(4.0000001,zr2+zi2),iter,colorCtx)}yy++}if(yy>=r.h){ri++;yy=0}}
if(ri<rects.length)requestAnimationFrame(slice);else finishRender(token,t0,profile,snap,w,h,out,'JavaScript f64 · 既存画像再利用 '+Math.round(100*reuse.reusedPixels/(w*h))+'%',reuse.exposedPixels)
}requestAnimationFrame(slice)
}
function renderWasmMain(profile,snap,w,h,iter,token,t0){
const out=new Uint8ClampedArray(w*h*4),field=makeField(w*h,iter),colorCtx=makeColorCtx(iter);
const cre=fixedNum(snap.re,snap.bits),cim=fixedNum(snap.im,snap.bits),sp=fixedNum(snap.span,snap.bits),scale=sp/w;
function proven(cr,ci){const y2=ci*ci,x=cr-.25,q0=x*x+y2;if(q0*(q0+x)<=.25*y2)return true;const x2=cr+1;return x2*x2+y2<=.0625}
let y=0;const counts=wasm.counts(),mags=wasm.mags();
function slice(){
if(token!==state.token)return;const deadline=performance.now()+(profile.covered?11:8);
while(y<h&&performance.now()<deadline){
const rows=Math.min(24,h-y),y0=y,npx=wasm.ex.render_rows(cre+scale*.5,cim-scale*.5,sp,w,h,y,rows,iter);let oi=y*w*4;
for(let i=0;i<npx;i++){
const n=counts[i],m=mags[i],px=i%w,py=y0+((i/w)|0);
const cr=cre+(px+.5-w*.5)*scale,ci=cim+(h*.5-py-.5)*scale,kind=n<iter?FIELD_ESCAPED:(proven(cr,ci)?FIELD_INTERIOR_LIKELY:FIELD_UNKNOWN);putField(field,py*w+px,n,m,kind);
if(n>=iter){out[oi++]=0;out[oi++]=0;out[oi++]=0;out[oi++]=255}
else{
putFastColor(out,oi,n,Math.max(4.0000001,m),iter,colorCtx);oi+=4;
}
}y+=rows;
}
if(y<h)requestAnimationFrame(slice);else finishRender(token,t0,profile,snap,w,h,out,'WebAssembly f64',w*h,field);
}requestAnimationFrame(slice);
}
function renderWasm(profile,snap,w,h,iter,token,t0){
if(!ensureShallowWorker()||shallowWorkerBusy){renderWasmMain(profile,snap,w,h,iter,token,t0);return}
const worker=shallowWorker,out=new Uint8ClampedArray(w*h*4),field=makeField(w*h,iter),cre=fixedNum(snap.re,snap.bits),cim=fixedNum(snap.im,snap.bits),sp=fixedNum(snap.span,snap.bits),jobId='shallow:'+token+':'+Date.now(),rowsPerChunk=Math.min(h,Math.max(4,Math.floor(32768/Math.max(1,w)))),chunks=[];for(let y=0;y<h;y+=rowsPerChunk)chunks.push({y,rows:Math.min(rowsPerChunk,h-y)});chunks.sort((a,b)=>Math.abs((a.y+a.rows*.5)/h-state.focusY)-Math.abs((b.y+b.rows*.5)/h-state.focusY));let next=0;shallowWorkerBusy=true;
const dispatch=()=>{if(token!==state.token){shallowWorkerBusy=false;return}if(next>=chunks.length){shallowWorkerBusy=false;finishRender(token,t0,profile,snap,w,h,out,'WebAssembly '+(wasm.simd?'SIMD':'scalar')+' Worker',w*h,field);return}const chunk=chunks[next++],msg={type:'render',jobId,cre,cim,sp,w,h,y:chunk.y,rows:chunk.rows,iter},transfer=[];if(shallowRecycle){msg.fieldBuffer=shallowRecycle.field;msg.iterationBuffer=shallowRecycle.iterations;msg.classBuffer=shallowRecycle.classes;transfer.push(msg.fieldBuffer,msg.iterationBuffer,msg.classBuffer);shallowRecycle=null}worker.postMessage(msg,transfer)};
worker.onmessage=e=>{const d=e.data;if(!d||d.type==='ready')return;if(d.jobId!==jobId)return;if(d.error){shallowWorkerBusy=false;renderWasmMain(profile,snap,w,h,iter,token,t0);return}const sf=new Float32Array(d.field),it=new Uint32Array(d.iterations),cl=new Uint8Array(d.classes);if(token!==state.token){shallowRecycle={field:d.field,iterations:d.iterations,classes:d.classes};shallowWorkerBusy=false;return}const stripe=colorizeField({smooth:sf,classes:cl,iter}),dst=d.y*w;field.smooth.set(sf,dst);field.iterations.set(it,dst);field.classes.set(cl,dst);out.set(stripe,dst*4);presentPartialStripe(stripe,0,d.y,w,d.rows,w,h,snap);shallowRecycle={field:d.field,iterations:d.iterations,classes:d.classes};dispatch()};worker.onerror=()=>{shallowWorkerBusy=false;destroyShallowWorker();if(token===state.token)renderWasmMain(profile,snap,w,h,iter,token,t0)};dispatch()
}
function renderJsDouble(profile,snap,w,h,iter,token,t0){
const out=new Uint8ClampedArray(w*h*4),field=makeField(w*h,iter),colorCtx=makeColorCtx(iter),sp=fixedNum(snap.span,snap.bits),cre=fixedNum(snap.re,snap.bits),cim=fixedNum(snap.im,snap.bits),scale=sp/w;let y=0;
function inBulbs(cr,ci){const y2=ci*ci,x=cr-.25,qq=x*x+y2;if(qq*(qq+x)<=.25*y2)return true;const x2=cr+1;return x2*x2+y2<=.0625}
function slice(){if(token!==state.token)return;const deadline=performance.now()+(profile.covered?10:7);while(y<h&&performance.now()<deadline){let oi=y*w*4,ci=cim+(h*.5-y-.5)*scale,cr=cre+(.5-w*.5)*scale;for(let x=0;x<w;x++,cr+=scale){let zr=0,zi=0,zr2=0,zi2=0,n=0,inside=inBulbs(cr,ci);if(inside)n=iter;else{let oldr=0,oldi=0;while(n<iter&&zr2+zi2<=4){zi=(zr+zr)*zi+ci;zr=zr2-zi2+cr;zr2=zr*zr;zi2=zi*zi;n++;if((n&63)===0){if(Math.abs(zr-oldr)+Math.abs(zi-oldi)<1e-15){n=iter;break}oldr=zr;oldi=zi}}}const mm=n>=iter?0:Math.max(4.0000001,zr2+zi2),kind=n<iter?FIELD_ESCAPED:(inside?FIELD_INTERIOR_LIKELY:FIELD_UNKNOWN);putField(field,y*w+x,n,mm,kind);if(n>=iter){out[oi++]=0;out[oi++]=0;out[oi++]=0;out[oi++]=255}else{putFastColor(out,oi,n,mm,iter,colorCtx);oi+=4}}y++}if(y<h)requestAnimationFrame(slice);else finishRender(token,t0,profile,snap,w,h,out,'JavaScript f64',w*h,field);}requestAnimationFrame(slice);
}
function spanMantBucket(span,bits){
const l=log2FixedAt(span,bits);let bucket=Math.round(l/256),mant=Math.pow(2,l-bucket*256);return{mant,bucket};
}
function normalizeBucket(r,i,b){
let m=Math.max(Math.abs(r),Math.abs(i));if(!m)return[0,0,NEG_BUCKET];
while(m>HI128){r*=INV256;i*=INV256;b++;m*=INV256}
while(m<LO128){r*=POW256;i*=POW256;b--;m*=POW256}
return[r,i,b];
}
function scAdd(a,b){
if(a[2]===NEG_BUCKET)return b;if(b[2]===NEG_BUCKET)return a;
const eb=Math.max(a[2],b[2]);let r=0,i=0;
if(a[2]===eb){r+=a[0];i+=a[1]}else if(a[2]===eb-1){r+=a[0]*INV256;i+=a[1]*INV256}
if(b[2]===eb){r+=b[0];i+=b[1]}else if(b[2]===eb-1){r+=b[0]*INV256;i+=b[1]*INV256}
return normalizeBucket(r,i,eb);
}
function scLog2(a){return a[2]===NEG_BUCKET?-Infinity:Math.log2(Math.hypot(a[0],a[1]))+256*a[2]}
function fixedMantAtBucket(v,bits,bucket){
if(v===0n)return 0;const neg=v<0n;let a=neg?-v:v,bl=bitLen(a),take=Math.min(53,bl),sh=bl-take,top=Number(a>>BigInt(sh));
const exp=sh-bits-256*bucket;const n=top*Math.pow(2,exp);return neg?-n:n;
}
function fixedComplexScaled(r,i,bits){
if(r===0n&&i===0n)return[0,0,NEG_BUCKET];
const lr=r===0n?-Infinity:log2FixedAt(r,bits),li=i===0n?-Infinity:log2FixedAt(i,bits),bucket=Math.round(Math.max(lr,li)/256);
return normalizeBucket(fixedMantAtBucket(r,bits,bucket),fixedMantAtBucket(i,bits,bucket),bucket);
}
function pixelCenteredOffset(snap,refC,w){const half=roundDiv(snap.span,BigInt(2*Math.max(1,w)));return{r:snap.re-refC.re+half,i:snap.im-refC.im-half}}
function roundDiv(v,d){const neg=v<0n,a=neg?-v:v,q=(a+d/2n)/d;return neg?-q:q}
function fixedPixelPoint(snap,w,h,x,y,bits=snap.bits){const den=BigInt(2*w),re=align(snap.re,snap.bits,bits),im=align(snap.im,snap.bits,bits),span=align(snap.span,snap.bits,bits);return[re+roundDiv(span*BigInt(2*x+1-w),den),im+roundDiv(span*BigInt(h-2*y-1),den)]}
function fixedAnalyticInterior(cr,ci,bits){const S=1n<<BigInt(bits),X=cr-(S>>2n),Y=ci,Q=X*X+Y*Y;if(4n*Q*(Q+X*S)<=Y*Y*S*S)return true;const D=cr+S;return 16n*(D*D+Y*Y)<=S*S}
function fixedAnalyticPixelProven(snap,w,h,x,y){const p=fixedPixelPoint(snap,w,h,x,y),g=fixedPixelPoint(snap,w,h,x,y,snap.bits+64);return fixedAnalyticInterior(p[0],p[1],snap.bits)&&fixedAnalyticInterior(g[0],g[1],snap.bits+64)}
function roundShift(v,bits){const neg=v<0n,a=neg?-v:v,half=1n<<(BigInt(bits)-1n),q=(a+half)>>BigInt(bits);return neg?-q:q}
// ── Arbitrary-precision reference orbit / perturbation setup ────────────
const referenceCache={id:0,bits:0,re:0n,im:0n,n:0,escape:0,zr:0n,zi:0n,rr:null,ri:null,series:null,version:0,loadedLen:0,derivative:[0,0,NEG_BUCKET],conditionLog2:0,checkpointVersion:0,checkpointBits:0,checkpointCount:0,checkpointMismatch:false};
function promoteReferenceCache(shift,oldBits){
const c=referenceCache;if(!c.rr||c.bits!==oldBits)return;
const sh=BigInt(shift);c.re<<=sh;c.im<<=sh;c.zr<<=sh;c.zi<<=sh;c.bits+=shift;
// rr/ri are normalized Float64 values, so neither they nor Worker-side copies
// need to change when only the fixed-point radix moves.
}
function sameReference(bits,re,im){
return referenceCache.rr&&referenceCache.bits===bits&&referenceCache.re===re&&referenceCache.im===im;
}
function resetReference(bits,re,im){
const c=referenceCache;c.id++;c.bits=bits;c.re=re;c.im=im;c.n=0;c.escape=0;c.zr=0n;c.zi=0n;c.derivative=[0,0,NEG_BUCKET];c.conditionLog2=0;
if(!c.rr){c.rr=new Float64Array(150001);c.ri=new Float64Array(150001)}
c.series=null;c.version=1;c.loadedLen=0;c.checkpointVersion=0;c.checkpointBits=0;c.checkpointCount=0;c.checkpointMismatch=false;
}
function invalidateReferenceOrbit(){const c=referenceCache;c.id++;c.bits=0;c.re=0n;c.im=0n;c.n=0;c.escape=0;c.zr=0n;c.zi=0n;c.rr=null;c.ri=null;c.series=null;c.version=0;c.loadedLen=0;c.derivative=[0,0,NEG_BUCKET];c.conditionLog2=0;c.checkpointVersion=0;c.checkpointBits=0;c.checkpointCount=0;c.checkpointMismatch=false;for(const worker of deepPool.workers){worker._refKey='';worker._refLoaded=0}}
function buildReferenceAt(bits,cRe,cIm,iter,token,done){
if(!sameReference(bits,cRe,cIm))resetReference(bits,cRe,cIm);
const c=referenceCache,B=BigInt(bits),ONE=1n<<B,BAIL=16n*ONE,startN=c.n,refT0=performance.now();
if((c.escape&&c.escape<=iter)||c.n>=iter){
const refLen=c.escape&&c.escape<=iter?c.escape:iter;done(c,refLen);return
}
function slice(){
if(token!==state.token)return;const deadline=performance.now()+6.5;
while(c.n<iter&&!c.escape&&performance.now()<deadline){
c.rr[c.n]=fixedOrbitNum(c.zr,bits);c.ri[c.n]=fixedOrbitNum(c.zi,bits);const Rr=c.rr[c.n],Ri=c.ri[c.n],d=c.derivative,term=d[2]===NEG_BUCKET?[0,0,NEG_BUCKET]:normalizeBucket(2*(Rr*d[0]-Ri*d[1]),2*(Rr*d[1]+Ri*d[0]),d[2]);c.derivative=scAdd(term,[1,0,0]);c.conditionLog2=Math.max(c.conditionLog2,scLog2(c.derivative));
const zr2=roundShift(c.zr*c.zr,bits),zi2=roundShift(c.zi*c.zi,bits);c.zi=roundShift(2n*c.zr*c.zi,bits)+cIm;c.zr=zr2-zi2+cRe;c.n++;
const mag=roundShift(c.zr*c.zr,bits)+roundShift(c.zi*c.zi,bits);if(mag>BAIL)c.escape=c.n;
}
if(c.escape||c.n>=iter){
c.rr[c.n]=fixedOrbitNum(c.zr,bits);c.ri[c.n]=fixedOrbitNum(c.zi,bits);c.version++;if(c.n>startN){const ms=performance.now()-refT0;refControl.lastBuildMs=ms;refControl.buildEMA=refControl.buildEMA?refControl.buildEMA*.82+ms*.18:ms}
const refLen=c.escape&&c.escape<=iter?c.escape:iter;done(c,refLen)
}else requestAnimationFrame(slice);
}
requestAnimationFrame(slice);
}
function verifyReferenceCheckpoints(ref,refLen,token,done){
const guardBits=ref.bits+64;
if(ref.checkpointVersion===ref.version&&ref.checkpointBits===guardBits&&!ref.checkpointMismatch){done(true);return}
const cRe=align(ref.re,ref.bits,guardBits),cIm=align(ref.im,ref.bits,guardBits),ONE=1n<<BigInt(guardBits),BAIL=16n*ONE;
const targets=new Set([0,refLen]);for(let n=1;n<refLen;n*=2)targets.add(n);const stride=Math.max(1,Math.floor(refLen/32));for(let n=stride;n<refLen;n+=stride)targets.add(n);
let zr=0n,zi=0n,n=0,escape=0,checked=0,mismatch=false;
function compare(){if(!targets.has(n)||n>refLen)return;checked++;const rr=fixedOrbitNum(zr,guardBits),ri=fixedOrbitNum(zi,guardBits);if(!Object.is(rr,ref.rr[n])||!Object.is(ri,ref.ri[n]))mismatch=true}
function slice(){
if(token!==state.token)return;const deadline=performance.now()+6;compare();
while(n<refLen&&!escape&&!mismatch&&performance.now()<deadline){const zr2=roundShift(zr*zr,guardBits),zi2=roundShift(zi*zi,guardBits);zi=roundShift(2n*zr*zi,guardBits)+cIm;zr=zr2-zi2+cRe;n++;const mag=roundShift(zr*zr,guardBits)+roundShift(zi*zi,guardBits);if(mag>BAIL)escape=n;compare()}
if(mismatch||escape||n>=refLen){if((ref.escape||0)!==escape&&((ref.escape||0)<=refLen||escape<=refLen))mismatch=true;ref.checkpointVersion=ref.version;ref.checkpointBits=guardBits;ref.checkpointCount=checked;ref.checkpointMismatch=mismatch;done(!mismatch)}else requestAnimationFrame(slice)
}
requestAnimationFrame(slice)
}
function computeSeries(ref,refLen,logMaxDc){
// A series valid for a wider image remains valid after zooming further in.
if(ref.series&&logMaxDc<=ref.series.logMaxDc+.02&&ref.series.skip<refLen)return ref.series;
let A=[0,0,NEG_BUCKET],Bc=[0,0,NEG_BUCKET],bestSkip=0,bestA=[0,0,NEG_BUCKET],bestB=[0,0,NEG_BUCKET];
const LOG_LIMIT=Math.log2(2.2e-4),LOG_RATIO=Math.log2(.10);
for(let n=0;n<refLen;n++){
const R=ref.rr[n],I=ref.ri[n],oldA=A;
const at=oldA[2]===NEG_BUCKET?[0,0,NEG_BUCKET]:normalizeBucket(2*(R*oldA[0]-I*oldA[1]),2*(R*oldA[1]+I*oldA[0]),oldA[2]);
A=scAdd(at,[1,0,0]);
const bt=Bc[2]===NEG_BUCKET?[0,0,NEG_BUCKET]:normalizeBucket(2*(R*Bc[0]-I*Bc[1]),2*(R*Bc[1]+I*Bc[0]),Bc[2]);
const a2=oldA[2]===NEG_BUCKET?[0,0,NEG_BUCKET]:normalizeBucket(oldA[0]*oldA[0]-oldA[1]*oldA[1],2*oldA[0]*oldA[1],2*oldA[2]);
Bc=scAdd(bt,a2);
const l1=scLog2(A)+logMaxDc,l2=scLog2(Bc)+2*logMaxDc;
if(Number.isFinite(l1)&&l1<LOG_LIMIT&&(!Number.isFinite(l2)||(l2<LOG_LIMIT&&l2<l1+LOG_RATIO))){bestSkip=n+1;bestA=A.slice();bestB=Bc.slice()}
}
if(bestSkip>=refLen)bestSkip=Math.max(0,refLen-1);
ref.series={logMaxDc,skip:bestSkip,Ar:bestA[0],Ai:bestA[1],Ab:bestA[2],Br:bestB[0],Bi:bestB[1],Bb:bestB[2]};
return ref.series;
}
function chooseReference(snap){
const c=referenceCache;if(c.rr&&c.n>8){const rr=align(c.re,c.bits,snap.bits),ri=align(c.im,c.bits,snap.bits),dx=fixedRatio(rr-snap.re,snap.span),dy=fixedRatio(ri-snap.im,snap.span),dist=Math.hypot(dx,dy);if(Number.isFinite(dist)){const hard=2.25;if(dist<=hard){let keep=true;if(dist>.45&&refControl.refId===c.id&&refControl.baseMPP>0&&refControl.lastMPP>refControl.baseMPP*1.34&&performance.now()-refControl.lastRecenterAt>refControl.cooldownMs){const extra=(refControl.lastMPP-refControl.baseMPP)*Math.max(1,refControl.lastPixels),build=Math.max(5,refControl.buildEMA||18);if(extra>build*1.65)keep=false}if(keep)return{re:rr,im:ri,reused:true,dist};refControl.lastRecenterAt=performance.now()}}}return{re:snap.re,im:snap.im,reused:false,dist:0}
}
function putDeepPixel(out,w,computeIter,colorIter,x,y,n,m,colorCtx){
const oi=(y*w+x)*4,mm=n>=computeIter?0:Math.max(4.0000001,m);
if(n>=computeIter){out[oi]=0;out[oi+1]=0;out[oi+2]=0;out[oi+3]=255;return}
putFastColor(out,oi,n,mm,colorIter,colorCtx);
}
function directOrbitState(cr,ci,bits,iter){return{cr,ci,bits,iter,zr:0n,zi:0n,mag:0n,n:0,done:false,four:4n*(1n<<BigInt(bits))}}
function stepDirectOrbit(orbit,deadline){let batch=0;while(!orbit.done){const zr2=roundShift(orbit.zr*orbit.zr,orbit.bits),zi2=roundShift(orbit.zi*orbit.zi,orbit.bits);orbit.mag=zr2+zi2;if(orbit.mag>orbit.four||orbit.n>=orbit.iter){orbit.done=true;break}orbit.zi=roundShift(2n*orbit.zr*orbit.zi,orbit.bits)+orbit.ci;orbit.zr=zr2-zi2+orbit.cr;orbit.n++;if((++batch&31)===0&&performance.now()>=deadline)break}if(orbit.n>=orbit.iter)orbit.done=true}
async function highPrecisionDirectPixelAsync(snap,w,h,iter,x,y,guarded=true,cancelled=()=>false){const p=fixedPixelPoint(snap,w,h,x,y),base=directOrbitState(p[0],p[1],snap.bits,iter);let guard=null;if(guarded){const bits=snap.bits+64,g=fixedPixelPoint(snap,w,h,x,y,bits);guard=directOrbitState(g[0],g[1],bits,iter)}while(!base.done||(guard&&!guard.done)){if(cancelled())return null;const deadline=performance.now()+6;stepDirectOrbit(base,deadline);if(guard)stepDirectOrbit(guard,deadline);if(!base.done||(guard&&!guard.done))await new Promise(requestAnimationFrame)}if(guard&&(guard.n!==base.n||((guard.n<iter)!==(base.n<iter))))return[iter,0];const chosen=guard||base;return[chosen.n,chosen.n<iter?Math.max(4.0000001,fixedOrbitNum(chosen.mag,chosen.bits)):0]}
function renderDeepDirect(profile,snap,w,h,iter,colorIter,token,t0){
const cap=profile.covered?w:390,scale=Math.min(1,cap/w);if(scale<1){h=Math.max(100,Math.round(h*scale));w=Math.max(160,Math.round(w*scale))}
const out=new Uint8ClampedArray(w*h*4),field=makeField(w*h,colorIter),colorCtx=makeColorCtx(colorIter);let y=0;
state.lastEngine='BigInt direct fallback';
async function slice(){
if(token!==state.token)return;const deadline=performance.now()+7;
while(y<h&&performance.now()<deadline){const y0=y;for(let x=0;x<w;x++){const result=await highPrecisionDirectPixelAsync(snap,w,h,iter,x,y,false,()=>token!==state.token);if(!result)return;const[n,m]=result;putField(field,y*w+x,n,m,n<iter?FIELD_ESCAPED:FIELD_UNKNOWN);putDeepPixel(out,w,iter,colorIter,x,y,n,m,colorCtx)}presentPartialStripe(out.subarray(y0*w*4,(y0+1)*w*4),0,y0,w,1,w,h,snap);y++}
if(y<h)requestAnimationFrame(slice);else finishRender(token,t0,profile,snap,w,h,out,'BigInt direct fallback',w*h,field);
}requestAnimationFrame(slice);
}
function prepareDeepContext(snap,refC,iter,token,done){
const probeW=64,probeH=Math.max(28,Math.round(probeW*canvas.clientHeight/Math.max(1,canvas.clientWidth))),refX=probeW*.5-.5+fixedRatio(refC.re-snap.re,snap.span)*probeW,refY=probeH*.5-.5-fixedRatio(refC.im-snap.im,snap.span)*probeW,cornerR=Math.max(Math.hypot(refX,refY),Math.hypot(probeW-refX,refY),Math.hypot(refX,probeH-refY),Math.hypot(probeW-refX,probeH-refY))/Math.max(1,probeW),logMaxDc=log2FixedAt(snap.span,snap.bits)+Math.log2(Math.max(1e-300,cornerR));
buildReferenceAt(snap.bits,refC.re,refC.im,iter,token,(ref,refLen)=>{if(token!==state.token)return;if(ensureOrbitPrecision(iter,Math.max(1,canvas.width))){state.dirty=true;invalidateView();return}const finish=()=>done({ref,refLen,series:computeSeries(ref,refLen,logMaxDc)});if(state.processMode!=='validate'){finish();return}verifyReferenceCheckpoints(ref,refLen,token,ok=>{if(token!==state.token)return;if(ok){finish();return}promoteState(32);invalidateReferenceOrbit();state.dirty=true;invalidateView()})})
}
function renderPerturbPrepared(profile,snap,w,h,iter,colorIter,token,t0,refC,ref,refLen,series,reuse=null){
const workerReady=ensureDeepPool();if(!workerReady&&(!ensureDeepWasm()||!deepWasm.ex.render_perturb_rebase_rect)){renderDeepDirect(profile,snap,w,h,iter,colorIter,token,t0);return}
const centered=pixelCenteredOffset(snap,refC,w),out=reuse?reuse.out:new Uint8ClampedArray(w*h*4),field=makeField(w*h,colorIter),colorCtx=makeColorCtx(colorIter),sb=spanMantBucket(snap.span,snap.bits),off=fixedComplexScaled(centered.r,centered.i,snap.bits);
const fallback=(strict=false)=>{if(token!==state.token)return;if(!ensureDeepWasm()||!deepWasm.ex.render_perturb_rebase_rect){renderDeepDirect(profile,snap,w,h,iter,colorIter,token,t0);return}if(ref.loadedLen<refLen+1){deepWasm.refsR().set(ref.rr.subarray(ref.loadedLen,refLen+1),ref.loadedLen);deepWasm.refsI().set(ref.ri.subarray(ref.loadedLen,refLen+1),ref.loadedLen);ref.loadedLen=refLen+1}const counts=deepWasm.counts(),mags=deepWasm.mags(),bad=[];let y=0;function rows(){if(token!==state.token)return;const deadline=performance.now()+(profile.covered?11:8);while(y<h&&performance.now()<deadline){const rows=Math.min(Math.max(1,Math.floor(65536/Math.max(1,w))),h-y),y0=y,npx=deepWasm.ex.render_perturb_rebase_rect(sb.mant,sb.bucket,off[0],off[1],off[2],refLen,w,h,w*.5,h*.5,0,y,w,rows,iter,strict?0:series.skip,strict?0:series.Ar,strict?0:series.Ai,strict?0:series.Ab,strict?0:series.Br,strict?0:series.Bi,strict?0:series.Bb);for(let i=0;i<npx;i++){const x=i%w,py=y0+((i/w)|0),n=counts[i],m=mags[i];if(n===0xffffffff){bad.push(py*w+x);continue}putField(field,py*w+x,n,m,n<iter?FIELD_ESCAPED:FIELD_UNKNOWN);putDeepPixel(out,w,iter,colorIter,x,py,n,m,colorCtx)}y+=rows}if(y<h)requestAnimationFrame(rows);else if(bad.length)runResidual();else finish()}function runResidual(){let k=0;async function slice(){if(token!==state.token)return;const deadline=performance.now()+6;while(k<bad.length&&performance.now()<deadline){const idx=bad[k++],x=idx%w,py=(idx/w)|0,result=await highPrecisionDirectPixelAsync(snap,w,h,iter,x,py,false,()=>token!==state.token);if(!result)return;const[n,m]=result;putField(field,idx,n,m,n<iter?FIELD_ESCAPED:FIELD_UNKNOWN);putDeepPixel(out,w,iter,colorIter,x,py,n,m,colorCtx)}if(k<bad.length)requestAnimationFrame(slice);else finish()}requestAnimationFrame(slice)}function finish(){state.lastEngine='WASM '+(deepWasm.simd?'SIMD':'scalar')+' · '+(strict?'strict ':'')+'main-thread rebase · skip '+(strict?0:series.skip);finishRender(token,t0,profile,snap,w,h,out,state.lastEngine,w*h,field)}requestAnimationFrame(rows)};
// Never abort and restart an in-flight frame because a budget estimate was wrong.
// Finish it once and feed the measured cost into the next frame.
if(!workerReady||!runDeepPool(profile,snap,w,h,iter,colorIter,token,t0,out,sb,refC,off,ref,refLen,series,fallback,reuse?reuse.rects:null))fallback();
}
async function renderDeepAdaptive(profile,snap,plan,token,t0){
try{await prepareDeepModules()}catch{if(token===state.token){const [fallbackW,fallbackH]=targetSize(profile,true);renderDeepDirect(profile,snap,fallbackW,fallbackH,plan.colorIter,plan.colorIter,token,t0)}return}if(token!==state.token)return;
const iter=plan.colorIter,colorIter=plan.colorIter,[w,h]=targetSize(profile,true),reuse=buildPanReuse(snap,w,h,iter,profile);
// A zoom-in can be represented entirely by the previous frame. Commit that preview
// immediately; the normal idle HQ pass performs the exact full-resolution render.
if(reuse&&reuse.rects.length===0&&!profile.covered){finishRender(token,t0,profile,snap,w,h,reuse.out,'既存画像再利用 100%',0);return}
const refC=chooseReference(snap);state.lastEngine='参照軌道を準備中…';prepareDeepContext(snap,refC,iter,token,ctx=>{if(token!==state.token)return;renderPerturbPrepared(profile,snap,w,h,iter,colorIter,token,t0,refC,ctx.ref,ctx.refLen,ctx.series,reuse)})
}
function render(pass=RENDER_PASS.PREVIEW){
const profile=renderProfile(pass);
if(pass===RENDER_PASS.COVERED){state.fieldView=null;trimDetailCache()}
runtimeMetrics.renderStarts++;if(state.pointerActive||state.wheelActive)runtimeMetrics.renderStartsDuringGesture++;
let snap=snapshot(),deep=deepEngineNeeded(snap);if(!deep&&state.adaptivePixelBudget){state.adaptivePixelBudget=0;resize()}if(deep&&ensureOrbitPrecision(maxIter()))snap=snapshot();const plan=iterationPlan(profile,deep),token=++state.token,t0=performance.now();state.rendering=true;state.drawState=profile.covered?'COVERING':'PREVIEW';state.lastPass=profile.id;state.lastEngine=deep?'参照軌道を準備中…':(wasm?('WebAssembly '+(wasm.simd?'SIMD':'scalar')):'JavaScript f64');
if(!deep){const ratio=deepResolutionRatio(snap);if(ratio<=128)prewarmDeepAssets();else if(deepPool.workers.length||deepWasm||deepModuleBundle||referenceCache.rr)retireDeepAssets()}
invalidateStats();
if(deep)renderDeepAdaptive(profile,snap,plan,token,t0);else{const [w,h]=targetSize(profile,false),reuse=buildPanReuse(snap,w,h,plan.computeIter,profile);if(reuse)renderShallowReuse(profile,snap,w,h,plan.computeIter,token,t0,reuse);else if(wasm)renderWasm(profile,snap,w,h,plan.computeIter,token,t0);else renderJsDouble(profile,snap,w,h,plan.computeIter,token,t0)}
}
function screenPixelBudget(){
const lowMemory=Number(navigator.deviceMemory||8)<=4,small=matchMedia('(max-width:700px)').matches;if(state.processMode==='power')return 1*1048576;if(state.processMode==='fine'||state.processMode==='validate')return(lowMemory||small?4:8)*1048576;
const base=(lowMemory||small?2:4)*1048576;return state.adaptivePixelBudget?Math.min(base,state.adaptivePixelBudget):base
}
function adaptStandardDeepBudget(deep){if(state.processMode!=='standard'||!deep)return false;const measuredMPP=renderPerf.deepMPP||.03,target=Math.max(32768,Math.min(4*1048576,Math.round(1400/measuredMPP))),oldBudget=state.adaptivePixelBudget||screenPixelBudget();if(target/oldBudget>=.8&&target/oldBudget<=1.25)return false;const oldPixels=canvas.width*canvas.height;state.adaptivePixelBudget=target;resize();return canvas.width*canvas.height!==oldPixels}
function resize(){
const cssW=Math.max(1,innerWidth),cssH=Math.max(1,innerHeight),budget=screenPixelBudget(),nativeDpr=Math.max(1,window.devicePixelRatio||1),budgetDpr=Math.sqrt(budget/Math.max(1,cssW*cssH)),minDpr=Math.min(1,64/Math.max(cssW,cssH)),dpr=Math.max(minDpr,Math.min(nativeDpr,budgetDpr)),w=Math.max(2,Math.round(cssW*dpr)),h=Math.max(2,Math.round(cssH*dpr));
state.effectiveDpr=dpr;state.screenPixelBudget=budget;
if(canvas.width!==w||canvas.height!==h){clearPanReuse();canvas.width=w;canvas.height=h;trimDetailCache();state.dirty=true;invalidateView()}
}
function jaEngine(s){return String(s||'').replace('preparing cached rebase reference…','参照軌道を準備中…').replace('persistent workers','常駐Worker').replace('main-thread','メインスレッド').replace('strict','厳密').replace('fallback','フォールバック').replace('rebase','リベース').replace('skip','スキップ').replace('residual','残差').replace('starting','起動中')}
function updateStats(){
runtimeMetrics.domWrites++;
const z=zoomExp(),digits=Math.max(8,Math.min(80,Math.ceil(z)+8));$('#coord').textContent=fmtFixed(state.re,digits)+' '+(state.im<0n?'−':'+')+' '+fmtFixed(state.im<0n?-state.im:state.im,digits)+'i';
$('#zoom').textContent=z<4?Math.pow(10,z).toFixed(1)+'×':'≈ 10^'+z.toFixed(2);$('#span').textContent=fmtSpan();const deep=deepEngineNeeded(),diagnosticProfile=RENDER_PROFILE[RENDER_PASS.COVERED],ip=iterationPlan(diagnosticProfile,deep);$('#engine').textContent=deep?'高精度深部':'標準精度';$('#render').textContent=state.rendering?'描画中…':(state.lastRender?state.lastRender.toFixed(0)+' ms':'準備完了');let status;if(state.drawState==='REPROJECTED')status='再投影';else if(state.drawState==='PREVIEW')status=state.rendering?'プレビュー描画中':'プレビュー';else if(state.drawState==='COVERING')status='全域描画中';else if(state.drawState==='RESOLVING')status='未確定を追加計算 '+Math.round(state.continuationProgress*100)+'%';else if(state.drawState==='REFINING')status='境界AA '+state.detailDone+'/'+state.detailQueued;else if(state.drawState==='REFINED')status='境界AA 完了';else if(state.drawState==='VALIDATING')status='精度照合 '+Math.round(state.coverage*100)+'%';else if(state.drawState==='VALIDATION_INCOMPLETE')status='検証未完了';else if(state.drawState==='VALIDATED')status='検証完了';else status='全域描画 完了';if(state.unresolved)status+=' · 未確定 '+state.unresolved;if(state.effectiveDpr<(devicePixelRatio||1)*.99)status+=' · '+canvas.width+'×'+canvas.height;$('#badge').textContent=status;$('#compactStatus').textContent=status;const ledger=memoryLedger(),bp=blaProfile(diagnosticProfile);$('#diagEngine').textContent='engine: '+jaEngine(state.lastEngine)+' | workers '+deepPool.workers.length+' | '+(wasm&&wasm.simd?'SIMD':'scalar/JS');$('#diagNumeric').textContent='numeric: '+state.bits+' bit | iter '+ip.colorIter+' | BLA ε 2^-'+bp.exp+' | condition '+Math.round(referenceCache.conditionLog2||0)+' bit';$('#diagMemory').textContent='memory: managed '+(ledger.managedBytes/1048576).toFixed(1)+' / '+(ledger.budget/1048576).toFixed(0)+' MiB | canvas est. '+(ledger.canvasBytes/1048576).toFixed(1)+' MiB';
}
// ── Scheduler / interaction loop ────────────────────────────────────────
function nextQualityDue(deep){
if(state.processMode==='power'||state.dirty||state.rendering||state.pointerActive||state.wheelActive)return Infinity;
if(deep&&state.lastPass!==RENDER_PASS.COVERED)return Math.max(state.lastInteraction+680,state.lastFrameDone+260);
if(!deep&&state.lastPass!==RENDER_PASS.COVERED)return Math.max(state.lastInteraction+520,state.lastFrameDone+280);
return Infinity
}
function loop(now){
schedulerRAF=0;if(document.hidden)return;
const deep=deepEngineNeeded();
if(!state.pointerActive&&!state.wheelActive&&!state.rendering){
if(state.dirty)render(RENDER_PASS.PREVIEW);
else{
const due=nextQualityDue(deep);
if(due<=now)render(RENDER_PASS.COVERED)
}
}
if(needsPaint){needsPaint=false;paintFrame()}
if(needsStats){needsStats=false;updateStats()}
if(!state.rendering&&!state.pointerActive&&!state.wheelActive&&!state.dirty){
const due=nextQualityDue(deep);if(Number.isFinite(due))requestScheduler(Math.max(1,due-performance.now()))
}
}
const pts=new Map();let lx=0,ly=0,pinch=0;
canvas.addEventListener('wheel',e=>{e.preventDefault();updateFocus(e.clientX,e.clientY);if(!state.wheelActive){state.wheelActive=true;cancelDetailRefinement(true);cancelRender();capturePanSource()}zoomAt(e.clientX,e.clientY,Math.exp(e.deltaY*.00125));clearTimeout(wheelSettleTimer);wheelSettleTimer=setTimeout(()=>{wheelSettleTimer=0;state.wheelActive=false;state.lastInteraction=performance.now();state.dirty=true;recordView();saveHash(false);invalidateView()},110)},{passive:false});
canvas.addEventListener('pointerdown',e=>{updateFocus(e.clientX,e.clientY);try{canvas.setPointerCapture(e.pointerId)}catch{};if(pts.size===0){clearTimeout(pointerSettleTimer);pointerSettleTimer=0;clearTimeout(wheelSettleTimer);wheelSettleTimer=0;state.wheelActive=false;state.pointerActive=true;cancelDetailRefinement(true);cancelRender();capturePanSource()}pts.set(e.pointerId,[e.clientX,e.clientY]);if(pts.size===1){lx=e.clientX;ly=e.clientY}else if(pts.size===2){const a=[...pts.values()];pinch=Math.hypot(a[0][0]-a[1][0],a[0][1]-a[1][1])}});
canvas.addEventListener('pointermove',e=>{if(!pts.has(e.pointerId))return;updateFocus(e.clientX,e.clientY);pts.set(e.pointerId,[e.clientX,e.clientY]);if(pts.size===1){const dx=e.clientX-lx,dy=e.clientY-ly;pan(dx,dy);lx=e.clientX;ly=e.clientY}else if(pts.size===2){const a=[...pts.values()],d=Math.hypot(a[0][0]-a[1][0],a[0][1]-a[1][1]);if(pinch>0&&d>0)zoomAt((a[0][0]+a[1][0])/2,(a[0][1]+a[1][1])/2,pinch/d);pinch=d}});
function end(e){pts.delete(e.pointerId);pinch=0;if(pts.size)return;clearTimeout(pointerSettleTimer);pointerSettleTimer=setTimeout(()=>{pointerSettleTimer=0;state.pointerActive=false;state.lastInteraction=performance.now();state.dirty=true;recordView();saveHash(false);invalidateView()},90)}canvas.addEventListener('pointerup',end);canvas.addEventListener('pointercancel',end);
function applyUiVisibility(){document.body.classList.toggle('ui-hidden',state.uiHidden);$('#uiToggle').textContent=state.uiHidden?'UI+':'UI−';$('#uiToggle').setAttribute('aria-expanded',String(!state.uiHidden))}
$('#uiToggle').onclick=()=>{state.uiHidden=!state.uiHidden;applyUiVisibility();try{localStorage.setItem('mandelbrot.uiHidden',state.uiHidden?'1':'0')}catch{}};
$('#zin').onclick=()=>{capturePanSource();zoomAt(innerWidth/2,innerHeight/2,.5);recordView();saveHash(false)};$('#zout').onclick=()=>{capturePanSource();zoomAt(innerWidth/2,innerHeight/2,2);recordView();saveHash(false)};$('#reset').onclick=()=>{reset();recordView()};
const exportJob={active:false,cancelled:false,bytes:0};let exportForcePrecision=false;
function downloadBlob(blob,name){const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=name;a.click();setTimeout(()=>URL.revokeObjectURL(a.href),1500)}
function exportLedgerBytes(){return memoryLedger().logicalBytes}
function exportSampleShallow(cre,cim,scale,W,H,iter,x,y){const cr=cre+(x+.5-W*.5)*scale,ci=cim+(H*.5-y-.5)*scale;let zr=0,zi=0,zr2=0,zi2=0,n=0;while(n<iter&&zr2+zi2<=4){zi=2*zr*zi+ci;zr=zr2-zi2+cr;zr2=zr*zr;zi2=zi*zi;n++}return[n,n<iter?Math.max(4.0000001,zr2+zi2):0]}
async function runExport(){if(exportJob.active)return;const scaleChoice=Number($('#exportScale').value),w=Math.max(64,Math.min(16384,Math.round(scaleChoice?canvas.width*scaleChoice:Number($('#exportWidth').value)||canvas.width))),h=Math.max(1,Math.round(w*canvas.height/Math.max(1,canvas.width))),ss=Math.max(1,Math.min(2,Number($('#exportAA').value)||1)),needed=w*h*8,budget=rendererMemoryBudget(),available=Math.max(0,budget-exportLedgerBytes());if(w>16384||h>16384){$('#exportStatus').textContent='辺の長さは 16384px 以下にしてください。';return}if(needed>available){$('#exportStatus').textContent='メモリ予算を超えます。幅または倍率を下げてください(必要 '+Math.ceil(needed/1048576)+' MiB / 空き '+Math.floor(available/1048576)+' MiB)。';return}const outCanvas=document.createElement('canvas');outCanvas.width=w;outCanvas.height=h;const oc=outCanvas.getContext('2d',{alpha:false});if(!oc){$('#exportStatus').textContent='出力 Canvas を作成できません。';return}const snap=snapshot(),iter=maxIter(),deep=deepEngineNeeded(snap,w),cre=fixedNum(snap.re,snap.bits),cim=fixedNum(snap.im,snap.bits),sp=fixedNum(snap.span,snap.bits),W=w*ss,H=h*ss,pixelScale=sp/W,colorCtx=makeColorCtx(iter),tmp=new Uint8ClampedArray(4),tiles=[],tileW=exportForcePrecision?4:(deep?16:128),tileH=exportForcePrecision?1:(deep?4:24);for(let y=0;y<h;y+=tileH)for(let x=0;x<w;x+=tileW)tiles.push({x,y,w:Math.min(tileW,w-x),h:Math.min(tileH,h-y)});exportJob.active=true;exportJob.cancelled=false;exportJob.bytes=needed;$('#exportProgress').hidden=false;$('#exportProgress').value=0;$('#exportStart').disabled=true;$('#exportStatus').textContent='タイル描画中…';let done=0,unresolvedSamples=0;try{for(const tile of tiles){if(exportJob.cancelled)throw new Error('cancelled');const id=oc.createImageData(tile.w,tile.h),data=id.data;for(let yy=0;yy<tile.h;yy++)for(let xx=0;xx<tile.w;xx++){let ar=0,ag=0,ab=0;for(let sy=0;sy<ss;sy++)for(let sx=0;sx<ss;sx++){const gx=(tile.x+xx)*ss+sx,gy=(tile.y+yy)*ss+sy,sample=deep?await highPrecisionDirectPixelAsync(snap,W,H,iter,gx,gy,exportForcePrecision,()=>exportJob.cancelled):exportSampleShallow(cre,cim,pixelScale,W,H,iter,gx,gy);if(!sample)throw new Error('cancelled');const[n,m]=sample;if(n<iter){putFastColor(tmp,0,n,m,iter,colorCtx);ar+=linearChannel(tmp[0]);ag+=linearChannel(tmp[1]);ab+=linearChannel(tmp[2])}else{ar+=linearChannel(20);ag+=linearChannel(22);ab+=linearChannel(30);unresolvedSamples++}}const samples=ss*ss,oi=(yy*tile.w+xx)*4;data[oi]=srgbChannel(ar/samples);data[oi+1]=srgbChannel(ag/samples);data[oi+2]=srgbChannel(ab/samples);data[oi+3]=255}oc.putImageData(id,tile.x,tile.y);done++;$('#exportProgress').value=done/tiles.length;$('#exportStatus').textContent='生成中 '+Math.round(done/tiles.length*100)+'%';await new Promise(requestAnimationFrame)}if(exportJob.cancelled)throw new Error('cancelled');$('#exportStatus').textContent='PNGを圧縮中…';const blob=await new Promise(resolve=>outCanvas.toBlob(resolve,'image/png'));if(!blob)throw new Error('PNG encode failed');const stamp=Date.now(),base='mandelbrot-'+stamp,meta={format:'mandelbrot-view-v23',rendererVersion:23,pixelContract:'centered',width:w,height:h,supersampling:ss,sampleCount:w*h*ss*ss,unresolvedSamples,membershipCertified:false,precisionPolicy:{mode:exportForcePrecision?'validated-direct':'balanced',baseBits:snap.bits,agreementGuardBits:exportForcePrecision?64:0},iterationPolicy:{adaptive:state.adaptive,base:state.baseIter,effective:iter},numericEngine:deep?'bigint-fixed-direct':'javascript-f64-direct',kernelSha256:globalThis.MANDEL_KERNEL_META||null,colorSpace:'sRGB with linear-light sample resolve',encoder:{mime:'image/png',api:'HTMLCanvasElement.toBlob'},view:{bits:snap.bits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString()},palette:{id:state.palette,cycle:state.cycle,shift:state.shift},determinism:{randomSeed:null,tiled:true,allTilesCompleted:true}};downloadBlob(blob,base+'.png');downloadBlob(new Blob([JSON.stringify(meta,null,2)],{type:'application/json'}),base+'.json');$('#exportStatus').textContent='PNG と座標メタデータを保存しました。'}catch(e){$('#exportStatus').textContent=String(e&&e.message)==='cancelled'?'出力を中止しました。':'出力に失敗しました: '+String(e&&e.message||e)}finally{exportJob.active=false;exportJob.bytes=0;$('#exportStart').disabled=false}}
$('#png').onclick=()=>{const d=$('#exportDialog');$('#exportWidth').value=String(canvas.width);$('#exportScale').value='1';$('#exportProgress').hidden=true;$('#exportStatus').textContent='';d.showModal?d.showModal():d.setAttribute('open','')};
$('#exportQuick').onclick=()=>canvas.toBlob(blob=>{if(!blob)return;downloadBlob(blob,'mandelbrot-'+state.drawState.toLowerCase()+'-'+Date.now()+'.png');$('#exportStatus').textContent='現在の表示('+state.drawState+')を保存しました。'},'image/png');
$('#exportScale').onchange=e=>{const s=Number(e.target.value);if(s)$('#exportWidth').value=String(Math.min(16384,canvas.width*s))};$('#exportStart').onclick=async()=>{exportForcePrecision=$('#exportPrecision').value==='validated';try{await runExport()}finally{exportForcePrecision=false}};$('#exportCancel').onclick=()=>{if(exportJob.active){exportJob.cancelled=true;$('#exportStatus').textContent='中止しています…'}else $('#exportDialog').close()};
function toast(s){const t=$('#toast');t.textContent=s;t.classList.add('show');clearTimeout(toast._t);toast._t=setTimeout(()=>t.classList.remove('show'),1500)}
let lastWrittenHash='',navigationHash='';
const viewHistory=[];let viewHistoryIndex=-1;
function currentViewSpec(){return{bits:state.bits,re:state.re,im:state.im,span:state.span,palette:state.palette,cycle:state.cycle,shift:state.shift,baseIter:state.baseIter,adaptive:state.adaptive}}
function viewSpecKey(v){return[v.bits,v.re,v.im,v.span,v.palette,v.cycle,v.shift,v.baseIter,v.adaptive].join(':')}
function recordView(){const v=currentViewSpec(),key=viewSpecKey(v);if(viewHistoryIndex>=0&&viewSpecKey(viewHistory[viewHistoryIndex])===key)return;viewHistory.splice(viewHistoryIndex+1);viewHistory.push(v);if(viewHistory.length>80)viewHistory.shift();viewHistoryIndex=viewHistory.length-1;syncHistoryButtons()}
function restoreView(v){if(!v)return;state.bits=v.bits;state.re=v.re;state.im=v.im;state.span=v.span;state.palette=v.palette;state.cycle=v.cycle;state.shift=v.shift;state.baseIter=v.baseIter;state.adaptive=v.adaptive;clearDetailCache();clearPanReuse();ensurePrecision();syncControls();saveHash(false);setDirty()}
function syncHistoryButtons(){$('#undoView').disabled=viewHistoryIndex<=0;$('#redoView').disabled=viewHistoryIndex<0||viewHistoryIndex>=viewHistory.length-1}
function syncCoordinateInputs(){$('#coordReInput').value=fmtFixedExact(state.re);$('#coordImInput').value=fmtFixedExact(state.im);$('#coordSpanInput').value=fmtFixedExact(state.span)}
function saveHash(push){const p=new URLSearchParams();p.set('v','23');p.set('b',String(state.bits));p.set('re',state.re.toString());p.set('im',state.im.toString());p.set('sp',state.span.toString());p.set('pal',String(state.palette));p.set('cy',String(state.cycle));p.set('sh',String(state.shift));p.set('it',String(state.baseIter));p.set('ad',state.adaptive?'1':'0');const h='#'+p.toString();lastWrittenHash=h;try{if(location.protocol==='file:'||location.origin==='null'){if(location.hash!==h)location.hash=h}else{push?history.pushState(null,'',h):history.replaceState(null,'',h)}}catch{}}
function loadHash(){const p=new URLSearchParams(location.hash.slice(1));if(!p.has('b'))return false;try{const b=Number(p.get('b')),re=BigInt(p.get('re')),im=BigInt(p.get('im')),sp=BigInt(p.get('sp'));if(!Number.isInteger(b)||b<64||sp<=0n)return false;state.bits=b;state.re=re;state.im=im;state.span=sp;if(p.has('pal'))state.palette=Math.max(0,Math.min(2,Number(p.get('pal'))|0));if(p.has('cy'))state.cycle=Math.max(.001,Math.min(.05,Number(p.get('cy'))||.008));if(p.has('sh'))state.shift=Math.max(0,Math.min(1,Number(p.get('sh'))||0));if(p.has('it'))state.baseIter=Math.max(100,Math.min(2500,Number(p.get('it'))||350));if(p.has('ad'))state.adaptive=p.get('ad')!=='0';ensurePrecision();return true}catch{return false}}
function syncControls(){$('#processMode').value=state.processMode;$('#palette').value=String(state.palette);$('#cycle').value=String(state.cycle);$('#cycleO').textContent=state.cycle.toFixed(4);$('#shift').value=String(state.shift);$('#shiftO').textContent=state.shift.toFixed(2);$('#iters').value=String(state.baseIter);$('#itersO').textContent=String(Math.round(state.baseIter));$('#adaptive').checked=state.adaptive;$('#hq').checked=state.hq;syncCoordinateInputs();syncHistoryButtons()}
function applyHashNavigation(){const h=location.hash;if(h===lastWrittenHash){lastWrittenHash='';return}if(h===navigationHash)return;navigationHash=h;setTimeout(()=>{navigationHash=''},0);if(loadHash()){clearDetailCache();clearPanReuse();recordView();syncControls();setDirty()}}
$('#share').onclick=async()=>{saveHash(true);try{await navigator.clipboard.writeText(location.href);toast('共有URLをコピーしました')}catch{toast('URLを更新しました')}};
$('#coordApply').onclick=()=>{try{const values=[$('#coordReInput').value,$('#coordImInput').value,$('#coordSpanInput').value],required=Math.max(...values.map(decimalRequiredBits));if(required>state.bits)promoteState(Math.ceil((required-state.bits)/64)*64);const re=fromDec(values[0]),im=fromDec(values[1]),span=fromDec(values[2]);if(span<=0n)throw new Error('表示幅は正数にしてください');state.re=re;state.im=im;state.span=span;ensurePrecision();recordView();saveHash(false);setDirty()}catch(e){toast('座標を適用できません: '+String(e&&e.message||e))}};
$('#coordCopy').onclick=async()=>{const value=JSON.stringify({rendererVersion:23,bits:state.bits,re:state.re.toString(),im:state.im.toString(),span:state.span.toString(),decimal:{re:fmtFixedExact(state.re),im:fmtFixedExact(state.im),span:fmtFixedExact(state.span)}});try{await navigator.clipboard.writeText(value);toast('正確な座標をコピーしました')}catch{toast('コピーできませんでした')}};
$('#undoView').onclick=()=>{if(viewHistoryIndex>0){viewHistoryIndex--;restoreView(viewHistory[viewHistoryIndex]);syncHistoryButtons()}};$('#redoView').onclick=()=>{if(viewHistoryIndex<viewHistory.length-1){viewHistoryIndex++;restoreView(viewHistory[viewHistoryIndex]);syncHistoryButtons()}};
function bind(id,key,out,fmt,flush=false){const el=$(id),o=$(out),apply=()=>{state[key]=Number(el.value);o.textContent=fmt(Number(el.value));if(flush)clearDetailCache();setDirty()};el.addEventListener('input',apply);apply()}
function bindColor(id,key,out,fmt){const el=$(id),o=$(out),apply=()=>{state[key]=Number(el.value);o.textContent=fmt(Number(el.value));if(!recolorCurrentField())setDirty()};el.addEventListener('input',apply);apply()}
bind('#iters','baseIter','#itersO',x=>String(Math.round(x)),true);bindColor('#cycle','cycle','#cycleO',x=>x.toFixed(4));bindColor('#shift','shift','#shiftO',x=>x.toFixed(2));
$('#palette').onchange=e=>{state.palette=Math.max(0,Math.min(2,Number(e.target.value)|0));if(!recolorCurrentField())setDirty()};
$('#processMode').onchange=e=>{state.processMode=/^(power|standard|fine|validate)$/.test(e.target.value)?e.target.value:'standard';state.hq=state.processMode==='fine'||state.processMode==='validate';state.adaptivePixelBudget=0;$('#hq').checked=state.hq;try{localStorage.setItem('mandelbrot.processMode',state.processMode)}catch{}resize();setDirty()};
$('#adaptive').onchange=e=>{state.adaptive=e.target.checked;clearDetailCache();setDirty()};$('#hq').onchange=e=>{state.hq=e.target.checked;if(!state.hq)cancelDetailRefinement(true);setDirty()};
addEventListener('resize',()=>{resize();setDirty()});addEventListener('keydown',e=>{if(/^(INPUT|SELECT|TEXTAREA|BUTTON)$/.test(e.target.tagName))return;let handled=true;if(e.key==='h'||e.key==='H')$('#uiToggle').click();else if(e.key==='r'||e.key==='R')$('#reset').click();else if(e.key==='+'||e.key==='='||e.key==='Enter'&&!e.shiftKey)$('#zin').click();else if(e.key==='-'||e.key==='Enter'&&e.shiftKey)$('#zout').click();else if(e.key==='ArrowLeft'){capturePanSource();pan(innerWidth*.08,0)}else if(e.key==='ArrowRight'){capturePanSource();pan(-innerWidth*.08,0)}else if(e.key==='ArrowUp'){capturePanSource();pan(0,innerHeight*.08)}else if(e.key==='ArrowDown'){capturePanSource();pan(0,-innerHeight*.08)}else handled=false;if(handled){e.preventDefault();recordView();saveHash(false)}});addEventListener('popstate',applyHashNavigation);addEventListener('hashchange',applyHashNavigation);
function stopSchedulers(){if(schedulerRAF){cancelAnimationFrame(schedulerRAF);schedulerRAF=0}if(schedulerTimer){clearTimeout(schedulerTimer);schedulerTimer=0;schedulerDue=0}if(wheelSettleTimer){clearTimeout(wheelSettleTimer);wheelSettleTimer=0}if(pointerSettleTimer){clearTimeout(pointerSettleTimer);pointerSettleTimer=0}cancelUnknownContinuation()}
addEventListener('visibilitychange',()=>{if(document.hidden){exportJob.cancelled=true;stopSchedulers();state.wheelActive=false;cancelRender();cancelDetailRefinement(true)}else{state.lastInteraction=performance.now();state.dirty=true;invalidateView()}});
globalThis.__MANDEL_DIAG__={snapshot:()=>{const deep=deepEngineNeeded(),ledger=memoryLedger(),coveredProfile=RENDER_PROFILE[RENDER_PASS.COVERED];return{rendererVersion:23,pixelContract:'centered',processMode:state.processMode,automaticTarget:modeTarget(),drawState:state.drawState,coverage:state.coverage,unresolved:state.unresolved,zoom:zoomExp(),deep,legacyDeepThreshold:LEGACY_DEEP_ZOOM_THRESHOLD,bits:state.bits,palette:state.palette,rendering:state.rendering,validating:validationJob.active,exporting:exportJob.active,lastRender:state.lastRender,lastPass:state.lastPass,transformReuse:!!state.panReuse,detailCache:DETAIL_TILE_CACHE.size,detailCacheBytes,detailCacheBudget:detailCacheBudget(),detailActive:state.detailActive,frame:frameCanvas.width+'x'+frameCanvas.height,frameCoverage:frameCanvas.width/Math.max(1,canvas.width),hqTarget:targetSize(coveredProfile,deep).join('x'),scheduler:{raf:!!schedulerRAF,timer:!!schedulerTimer,needsPaint,needsStats,wheelActive:state.wheelActive,pointerSettle:!!pointerSettleTimer,unknownTimer:!!unknownContinuationTimer,backgroundJobs:runtimeMetrics.pendingBackgroundJobs},screen:{effectiveDpr:state.effectiveDpr,deviceDpr:window.devicePixelRatio||1,pixelBudget:state.screenPixelBudget,pixels:canvas.width*canvas.height},memory:ledger,shallowAssets:{wasmReady:!!wasm,worker:!!shallowWorker,workerBusy:shallowWorkerBusy},deepAssets:{wasmReady:!!deepWasm,workers:deepPool.workers.length},runtimeMetrics:{...runtimeMetrics},telemetry:{...deepTelemetry},renderPerf:{...renderPerf},wisdom:{workerCount:deepWisdom.workerCount,rowMsEMA:deepWisdom.rowMsEMA,stripRows:deepWisdom.stripRows,realBench:deepWisdom.realBench},reference:{id:referenceCache.id,bits:referenceCache.bits,n:referenceCache.n,escape:referenceCache.escape,buildMs:refControl.lastBuildMs,buildEMA:refControl.buildEMA,baseMPP:refControl.baseMPP,lastMPP:refControl.lastMPP,conditionLog2:referenceCache.conditionLog2,checkpointBits:referenceCache.checkpointBits,checkpointCount:referenceCache.checkpointCount,checkpointMismatch:referenceCache.checkpointMismatch}}},memoryLedger:()=>memoryLedger()};
addEventListener('pagehide',()=>{stopSchedulers();destroyShallowWorker();destroyDeepPool()},{once:true});
try{state.uiHidden=localStorage.getItem('mandelbrot.uiHidden')==='1';const savedMode=localStorage.getItem('mandelbrot.processMode');if(/^(power|standard|fine|validate)$/.test(savedMode)){state.processMode=savedMode;state.hq=savedMode==='fine'||savedMode==='validate'}}catch{}restoreDeepWisdom();applyUiVisibility();resize();if(!loadHash())reset();else setDirty();recordView();syncControls();ctx.fillStyle='#050813';ctx.fillRect(0,0,canvas.width,canvas.height);render(RENDER_PASS.PREVIEW);requestScheduler();
})();