163 lines
40 KiB
Diff
163 lines
40 KiB
Diff
|
|
--- /mnt/data/mandelbrot_integrated/index.html 2026-09-29 09:15:05.708978122 +0000
|
|||
|
|
+++ index.html 2026-09-29 09:26:37.218893530 +0000
|
|||
|
|
@@ -268,9 +268,6 @@
|
|||
|
|
if(p.unknownOnly!=0u){
|
|||
|
|
let prior=fieldMeta[out];let cls=(prior>>28u)&3u;
|
|||
|
|
if(cls!=FIELD_UNKNOWN){return;}
|
|||
|
|
- let reason=(prior>>20u)&0xffu;
|
|||
|
|
- // mode 2 repairs only untouched coverage holes (zero-filled metadata).
|
|||
|
|
- if(p.unknownOnly==2u && reason!=0u){return;}
|
|||
|
|
}
|
|||
|
|
let gx=f32(p.tileX+gid.x)+p.sampleX;
|
|||
|
|
let gy=f32(p.tileY+gid.y)+p.sampleY;
|
|||
|
|
@@ -915,7 +912,7 @@
|
|||
|
|
};
|
|||
|
|
function normalizeQuality(value){return value==='fast'||value==='power'?'fast':value==='high'||value==='fine'||value==='validate'?'high':'standard'}
|
|||
|
|
function applyQuality(value){state.quality=normalizeQuality(value);state.continuationBudget=QUALITY_PRESETS[state.quality].iterations}
|
|||
|
|
-const state={bits:INITIAL_BITS,re:0n,im:0n,span:0n,baseIter:350,adaptive:true,continuationBudget:16384,quality:'standard',palette:0,cycle:.008,shift:.18,colorAuto:false,colorRevision:0,token:0,rendering:false,recoloring:false,recolorPending:false,dirty:true,renderClock:null,drawState:'REPROJECTED',frameView:null,fieldView:null,pointerActive:false,wheelActive:false,unresolved:0,unknownReasons:null,gpuError:'',gpuInitFailed:false,lastInteraction:performance.now(),uiHidden:false,temporalFill:false,deferNumericPublish:false,heuristicInterior:0,cpuFrameRgba:null};
|
|||
|
|
+const state={bits:INITIAL_BITS,re:0n,im:0n,span:0n,baseIter:350,adaptive:true,continuationBudget:16384,quality:'standard',palette:0,cycle:.008,shift:.18,colorAuto:false,colorRevision:0,token:0,rendering:false,recoloring:false,recolorPending:false,dirty:true,renderClock:null,drawState:'REPROJECTED',frameView:null,fieldView:null,pointerActive:false,wheelActive:false,unresolved:0,unknownReasons:null,gpuError:'',gpuInitFailed:false,lastInteraction:performance.now(),uiHidden:false,temporalFill:false,deferNumericPublish:false,heuristicInterior:0};
|
|||
|
|
let renderer=null,rendererInitPromise=null,raf=0,settleTimer=0,lastWrittenHash='',navigationHash='';
|
|||
|
|
|
|||
|
|
// ── exact fixed-point view state ─────────────────────────────────────────
|
|||
|
|
@@ -1171,6 +1168,12 @@
|
|||
|
|
function buf(device,size,usage,label){return device.createBuffer({label,size:Math.max(4,Math.ceil(size/4)*4),usage})}
|
|||
|
|
function destroy(x){if(x&&x.destroy)try{x.destroy()}catch{}}
|
|||
|
|
function writeU32F32(size,writer){const a=new ArrayBuffer(size),d=new DataView(a);writer(d);return a}
|
|||
|
|
+
|
|||
|
|
+function numericHistoryScaleEligible(w,h){
|
|||
|
|
+ const cw=Math.max(1,Math.round(canvas.clientWidth||w)),ch=Math.max(1,Math.round(canvas.clientHeight||h));
|
|||
|
|
+ const gcd=(a,b)=>{a=Math.abs(a);b=Math.abs(b);while(b){const t=a%b;a=b;b=t}return a||1};
|
|||
|
|
+ return cw/gcd(w,cw)<=8&&ch/gcd(h,ch)<=8;
|
|||
|
|
+}
|
|||
|
|
class WebGpuRenderer{
|
|||
|
|
constructor(adapter,device){
|
|||
|
|
this.device=device;
|
|||
|
|
@@ -1210,7 +1213,7 @@
|
|||
|
|
frameDestroy(){this.releaseColorSource();if(!this.frame)return;this.clearBindGroupCache();for(const ring of Object.values(this.frame.readbackRings||{}))for(const b of ring.buffers)destroy(b);for(const slot of this.frame.precisionUploadRing?.slots||[])destroy(slot.buffer);for(const k of ['meta','smooth','historyMeta','historySmooth','unresolved','deepQueueStats','deepBucketState','deepQueue','deepIndirect','deepActiveState','deepActiveQueueA','deepActiveQueueB','deepActiveCountA','deepActiveCountB','deepActiveIndirect','deepActiveRead','deepActiveMark','deepActivePixel','numericParams','statsParams','symmetryParams','colorParams','presentParams'])destroy(this.frame[k]);for(const t of new Set([this.frame.front,this.frame.back,this.frame.history,this.frame.spare]))destroy(t);this.frame=null;this.historyView=null;this.historyColorKey='';this.frontColorKey='';this.historyReady=false;this.numericHistoryView=null;this.numericHistoryIter=0;this.numericHistoryReady=false}
|
|||
|
|
ensureFrame(w,h){
|
|||
|
|
const n=w*h;if(this.frame&&this.frame.w===w&&this.frame.h===h)return this.frame;this.frameDestroy();const d=this.device,B=GPUBufferUsage,T=GPUTextureUsage,colorUsage=T.STORAGE_BINDING|T.TEXTURE_BINDING|T.COPY_SRC,front=d.createTexture({size:[w,h],format:'rgba8unorm',usage:colorUsage,label:'front-color'}),back=d.createTexture({size:[w,h],format:'rgba8unorm',usage:colorUsage,label:'back-color'}),history=d.createTexture({size:[w,h],format:'rgba8unorm',usage:colorUsage,label:'stable-history'});
|
|||
|
|
- this.frame={w,h,n,meta:buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'field-meta'),smooth:buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'field-smooth'),historyMeta:buf(d,n*4,B.COPY_SRC|B.COPY_DST,'stable-history-meta'),historySmooth:buf(d,n*4,B.COPY_SRC|B.COPY_DST,'stable-history-smooth'),unresolved:buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'unresolved-count'),deepQueueStats:null,deepBucketState:null,deepQueue:null,deepIndirect:null,deepActiveCapacity:0,deepActiveState:null,deepActiveQueueA:null,deepActiveQueueB:null,deepActiveCountA:null,deepActiveCountB:null,deepActiveIndirect:null,deepActiveRead:null,deepActiveMark:null,deepActivePixel:null,readbackRings:{},numericParams:buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST,'numeric-params'),statsParams:buf(d,16,B.UNIFORM|B.COPY_DST,'unknown-stats-params'),symmetryParams:buf(d,16,B.UNIFORM|B.COPY_DST,'symmetry-copy-params'),colorParams:buf(d,32,B.UNIFORM|B.COPY_DST,'color-params'),presentParams:buf(d,48,B.UNIFORM|B.COPY_DST,'present-params'),front,back,history,spare:history};return this.frame;
|
|||
|
|
+ const keepNumericHistory=numericHistoryScaleEligible(w,h);this.frame={w,h,n,meta:buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'field-meta'),smooth:buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'field-smooth'),historyMeta:keepNumericHistory?buf(d,n*4,B.COPY_SRC|B.COPY_DST,'stable-history-meta'):null,historySmooth:keepNumericHistory?buf(d,n*4,B.COPY_SRC|B.COPY_DST,'stable-history-smooth'):null,unresolved:buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'unresolved-count'),deepQueueStats:null,deepBucketState:null,deepQueue:null,deepIndirect:null,deepActiveCapacity:0,deepActiveState:null,deepActiveQueueA:null,deepActiveQueueB:null,deepActiveCountA:null,deepActiveCountB:null,deepActiveIndirect:null,deepActiveRead:null,deepActiveMark:null,deepActivePixel:null,readbackRings:{},numericParams:buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST,'numeric-params'),statsParams:buf(d,16,B.UNIFORM|B.COPY_DST,'unknown-stats-params'),symmetryParams:buf(d,16,B.UNIFORM|B.COPY_DST,'symmetry-copy-params'),colorParams:buf(d,32,B.UNIFORM|B.COPY_DST,'color-params'),presentParams:buf(d,48,B.UNIFORM|B.COPY_DST,'present-params'),front,back,history,spare:history};return this.frame;
|
|||
|
|
}
|
|||
|
|
frameReadback(kind,size){const f=this.frame;if(!f)throw new Error('frame readback requested without a frame');let ring=f.readbackRings[kind];if(!ring){const B=GPUBufferUsage;ring=f.readbackRings[kind]={cursor:0,buffers:Array.from({length:3},(_,i)=>buf(this.device,size,B.COPY_DST|B.MAP_READ,kind+'-readback-'+i))}}for(let tries=0;tries<ring.buffers.length;tries++){const i=ring.cursor++%ring.buffers.length,b=ring.buffers[i];if(!b.mapState||b.mapState==='unmapped')return b}throw new Error(kind+' readback ring is saturated')}
|
|||
|
|
ensureDeepBucketState(f=this.frame){if(!f)return null;if(!f.deepBucketState){const d=this.device,B=GPUBufferUsage;f.deepBucketState=buf(d,DEEP_BUCKET_STATE_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'deep-unknown-bucket-state')}return f}
|
|||
|
|
@@ -1221,8 +1224,9 @@
|
|||
|
|
destroyExportWorkspace(ws){if(!ws)return;this.clearBindGroupCache();for(const k of ['meta','smooth','cbuf','tex','read'])destroy(ws[k]);for(const b of ws.unresolveds)destroy(b);for(const b of ws.pbufs)destroy(b);for(const t of ws.samples)destroy(t)}
|
|||
|
|
ensureExportWorkspace(slot=0,S=this.exportTileSize()){
|
|||
|
|
const old=this.exportWs[slot];if(old&&old.size===S)return old;if(old)this.destroyExportWorkspace(old);const d=this.device,B=GPUBufferUsage,T=GPUTextureUsage,n=S*S,bpr=S*4,pixelBytes=bpr*S,label='export-'+slot+'-';
|
|||
|
|
- const ws={slot,size:S,meta:buf(d,n*4,B.STORAGE|B.COPY_DST,label+'meta'),smooth:buf(d,n*4,B.STORAGE|B.COPY_DST,label+'smooth'),unresolveds:Array.from({length:4},(_,i)=>buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,label+'unresolved-'+i)),pbufs:Array.from({length:4},(_,i)=>buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST,label+'numeric-'+i)),cbuf:buf(d,32,B.UNIFORM|B.COPY_DST,label+'color'),samples:Array.from({length:4},(_,i)=>d.createTexture({label:label+'sample-'+i,size:[S,S],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING})),tex:d.createTexture({label:label+'resolve',size:[S,S],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.COPY_SRC}),read:buf(d,pixelBytes+4*UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ,label+'readback')};this.exportWs[slot]=ws;return ws;
|
|||
|
|
+ const ws={slot,size:S,meta:buf(d,n*4,B.STORAGE|B.COPY_DST,label+'meta'),smooth:buf(d,n*4,B.STORAGE|B.COPY_DST,label+'smooth'),unresolveds:Array.from({length:4},(_,i)=>buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,label+'unresolved-'+i)),pbufs:Array.from({length:4},(_,i)=>buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST,label+'numeric-'+i)),cbuf:buf(d,32,B.UNIFORM|B.COPY_DST,label+'color'),samples:[],tex:d.createTexture({label:label+'resolve',size:[S,S],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.COPY_SRC}),read:buf(d,pixelBytes+4*UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ,label+'readback')};this.exportWs[slot]=ws;return ws;
|
|||
|
|
}
|
|||
|
|
+ ensureExportAaSamples(ws){if(ws.samples.length===4)return ws.samples;const d=this.device,T=GPUTextureUsage;for(let i=0;i<4;i++)ws.samples.push(d.createTexture({label:'export-'+ws.slot+'-sample-'+i,size:[ws.size,ws.size],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING}));return ws.samples}
|
|||
|
|
exportWorkspaceDestroy(){for(const ws of this.exportWs)this.destroyExportWorkspace(ws);this.exportWs=[]}
|
|||
|
|
setDeepContext(ctx){
|
|||
|
|
if(this.deepCtx?.key===ctx.key&&this.deepCtx.refLen>=ctx.refLen)return;
|
|||
|
|
@@ -1296,7 +1300,6 @@
|
|||
|
|
if(input)d.queue.writeBuffer(input,0,mask);const e=d.createCommandEncoder({label:'history-seed-and-exact-interior'});if(historySeed)this.encodeNumericHistorySeed(e,historySeed);else if(clear){e.clearBuffer(f.meta);e.clearBuffer(f.smooth)}
|
|||
|
|
if(pixels){const bg=d.createBindGroup({layout:this.interiorMask.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:input}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}}]});
|
|||
|
|
const pass=e.beginComputePass();pass.setPipeline(this.interiorMask);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));pass.end()}
|
|||
|
|
- this.encodeUnknownStats(e,{meta:f.meta,unresolved:f.unresolved,n:f.n});
|
|||
|
|
return await this.submitStage(e,historySeed?'numeric history + exposed interior':'exact interior before primary',token,'correction')===null?0:pixels;
|
|||
|
|
}finally{destroy(input)}
|
|||
|
|
}
|
|||
|
|
@@ -1347,7 +1350,7 @@
|
|||
|
|
const tileWidth=this.numericTileShape(f.w,iter).width;let rows=this.initialNumericRows(tileWidth,iter),burstCount=0,burstStart=performance.now(),lastStage='',predictedStripMs=22,burstLimit=this.numericBurstLimit(predictedStripMs),burstPredictedMs=0,lastCompletedRows=rows;
|
|||
|
|
const flushBurst=async remaining=>{if(!burstCount)return true;const ms=await this.waitStageBurst(lastStage,token,burstStart,burstCount);if(ms===null)return false;rows=this.adaptNumericRows(lastCompletedRows,ms,remaining);predictedStripMs=.65*predictedStripMs+.35*ms;burstLimit=this.numericBurstLimit(predictedStripMs);burstCount=0;burstPredictedMs=0;burstStart=performance.now();return true};
|
|||
|
|
for(let y=0;y<computeH;){if(token!==state.token)return false;const th=Math.min(rows,computeH-y);lastCompletedRows=th;for(let x=0;x<f.w;x+=tileWidth){if(token!==state.token)return false;const tw=Math.min(tileWidth,f.w-x),base=y*f.w+x,params=this.fastParams(tw,th,f.w,f.h,x,y,iter,snap,.5,.5,refX,refY,f.w,base,reuseResolved?1:0);d.queue.writeBuffer(f.numericParams,0,params);const e=d.createCommandEncoder({label:'fast-numeric-strip-'+y});if(x===0&&y===0){if(historySeed)this.encodeNumericHistorySeed(e,historySeed);else if(!reusePriorField&&!preserveInterior){e.clearBuffer(f.meta);e.clearBuffer(f.smooth)}}this.encodeFastPostStatsNumeric(e,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,w:tw,h:th});lastStage='fast primary rows '+y+'..'+(y+th-1);this.submitStageDeferred(e,lastStage,'numeric');burstCount++;burstPredictedMs+=predictedStripMs;const budgetMs=this.numericInFlightBudgetMs();if(burstCount>=burstLimit||burstPredictedMs>=budgetMs||performance.now()-burstStart>=budgetMs){if(!await flushBurst(computeH-(y+th)))return false}}y+=th}
|
|||
|
|
- if(!await flushBurst(0))return false;d.queue.writeBuffer(f.numericParams,0,this.fastParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,refX,refY,f.w,0,2));if(!deferColor)d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h,iter,colorStyle));const e=d.createCommandEncoder({label:deferColor?'fast-coverage-repair':'fast-coverage-repair-and-color'});if(mirror)this.encodeSymmetryCopy(e,{meta:f.meta,smooth:f.smooth,w:f.w,h:f.h});this.encodeFastNumeric(e,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,unresolved:f.unresolved,w:f.w,h:f.h});this.encodeUnknownStats(e,{meta:f.meta,unresolved:f.unresolved,n:f.n});if(!deferColor){const cbg=this.cachedBindGroup(this.color,[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:this.cachedTextureView(this.colorTarget(f))}]),cp=e.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));cp.end()}const ms=await this.submitStage(e,deferColor?'fast coverage only':'fast coverage + color',token,'numeric');if(ms===null)return false;if(!deferColor){[f.front,f.back]=[f.back,f.front];this.frontColorKey=colorKey}return true
|
|||
|
|
+ if(!await flushBurst(0))return false;if(!deferColor)d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h,iter,colorStyle));const e=d.createCommandEncoder({label:deferColor?'fast-post-stats':'fast-post-stats-and-color'});if(mirror)this.encodeSymmetryCopy(e,{meta:f.meta,smooth:f.smooth,w:f.w,h:f.h});this.encodeUnknownStats(e,{meta:f.meta,unresolved:f.unresolved,n:f.n});if(!deferColor){const cbg=this.cachedBindGroup(this.color,[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:this.cachedTextureView(this.colorTarget(f))}]),cp=e.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));cp.end()}const ms=await this.submitStage(e,deferColor?'fast post-stats only':'fast post-stats + color',token,'numeric');if(ms===null)return false;if(!deferColor){[f.front,f.back]=[f.back,f.front];this.frontColorKey=colorKey}return true
|
|||
|
|
}
|
|||
|
|
async computeFrame(snap,iter,token,referencePixel=null,fastExtended=false,fastContext=null,preserveInterior=false,preseededHistory=false,deferColor=false){
|
|||
|
|
await this.ready;if(fastExtended)await this.ensureFastPipelines();if(token!==state.token)return false;const f=this.ensureFrame(canvas.width,canvas.height),d=this.device,colorStyle=colorStyleSnapshot(),colorKey=colorStyleKey(colorStyle);
|
|||
|
|
@@ -1381,7 +1384,7 @@
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
commitNumericHistory(view=state.frameView,iter=state.fieldView?.iter??maxIter()){
|
|||
|
|
- const f=this.frame;if(!f||!view||state.fieldView?.complete===false)return false;const e=this.device.createCommandEncoder({label:'stable-numeric-history'});e.copyBufferToBuffer(f.meta,0,f.historyMeta,0,f.n*4);e.copyBufferToBuffer(f.smooth,0,f.historySmooth,0,f.n*4);this.device.queue.submit([e.finish()]);this.numericHistoryView={...view,w:f.w,h:f.h};this.numericHistoryIter=iter;this.numericHistoryReady=true;return true;
|
|||
|
|
+ const f=this.frame;if(!f||!f.historyMeta||!f.historySmooth||!view||state.fieldView?.complete===false){this.numericHistoryReady=false;return false}const e=this.device.createCommandEncoder({label:'stable-numeric-history'});e.copyBufferToBuffer(f.meta,0,f.historyMeta,0,f.n*4);e.copyBufferToBuffer(f.smooth,0,f.historySmooth,0,f.n*4);this.device.queue.submit([e.finish()]);this.numericHistoryView={...view,w:f.w,h:f.h};this.numericHistoryIter=iter;this.numericHistoryReady=true;return true;
|
|||
|
|
}
|
|||
|
|
captureColorSource(view=state.frameView,needed=state.colorAuto||state.rendering){
|
|||
|
|
this.colorSourceVisible=false;
|
|||
|
|
@@ -1413,7 +1416,7 @@
|
|||
|
|
}
|
|||
|
|
async recolor(token,iter=state.fieldView?.iter??maxIter()){await this.ready;if(token!==state.token||!this.frame)return false;const f=this.frame,d=this.device,colorStyle=colorStyleSnapshot(),colorKey=colorStyleKey(colorStyle);d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h,iter,colorStyle));const e=d.createCommandEncoder(),bg=this.cachedBindGroup(this.color,[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:this.cachedTextureView(this.colorTarget(f))}]),p=e.beginComputePass();p.setPipeline(this.color);p.setBindGroup(0,bg);p.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));p.end();d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;[f.front,f.back]=[f.back,f.front];this.frontColorKey=colorKey;return true}
|
|||
|
|
presentTransform(view=state.frameView){if(!this.frame||!view)return{scaleX:1,scaleY:1,offsetX:0,offsetY:0};const cur=snapshot(),b=Math.max(cur.bits,view.bits),cs=align(cur.span,cur.bits,b),ps=align(view.span,view.bits,b),dr=align(cur.re,cur.bits,b)-align(view.re,view.bits,b),di=align(cur.im,cur.bits,b)-align(view.im,view.bits,b),scale=fixedRatio(cs,ps);return{scaleX:scale,scaleY:scale,offsetX:fixedRatio(dr,ps),offsetY:-fixedRatio(di,ps)*this.frame.w/Math.max(1,this.frame.h)}}
|
|||
|
|
- commitHistory(view=state.frameView){if(!this.frame||!view||state.fieldView?.complete===false)return false;const f=this.frame;if(f.history!==f.front){f.history=f.front;this.clearBindGroupCache()}this.historyView=view;this.historyColorKey=this.frontColorKey;this.historyReady=true;this.commitNumericHistory(view,state.fieldView?.iter??maxIter());return true}
|
|||
|
|
+ commitHistory(view=state.frameView,commitNumeric=true){if(!this.frame||!view||state.fieldView?.complete===false)return false;const f=this.frame;if(f.history!==f.front){f.history=f.front;this.clearBindGroupCache()}this.historyView=view;this.historyColorKey=this.frontColorKey;this.historyReady=true;if(commitNumeric)this.commitNumericHistory(view,state.fieldView?.iter??maxIter());return true}
|
|||
|
|
presentFrame(transform=this.presentTransform()){if(!this.frame)return;const colored=this.colorSourceVisible&&this.colorSource;if(colored)transform=this.presentTransform(colored.view);const f=this.frame,d=this.device,pb=f.presentParams,interactive=state.pointerActive||state.wheelActive,finite=Number.isFinite(transform?.scaleX)&&Number.isFinite(transform?.scaleY)&&Number.isFinite(transform?.offsetX)&&Number.isFinite(transform?.offsetY)&&transform.scaleX>0&&transform.scaleY>0,ft=finite?transform:{scaleX:1,scaleY:1,offsetX:0,offsetY:0},historyCandidate=!!(!colored&&state.temporalFill&&this.historyReady&&this.historyView&&this.historyColorKey===colorStyleKey()),rawHistory=historyCandidate?this.presentTransform(this.historyView):{scaleX:1,scaleY:1,offsetX:0,offsetY:0},useHistory=!!(historyCandidate&&reprojectionSafe(rawHistory,2.25,.45)),ht=useHistory?rawHistory:{scaleX:1,scaleY:1,offsetX:0,offsetY:0};d.queue.writeBuffer(pb,0,new Float32Array([ft.scaleX,ft.scaleY,ft.offsetX,ft.offsetY,ht.scaleX,ht.scaleY,ht.offsetX,ht.offsetY,useHistory?1:0,0,0,0]));const bg=this.cachedBindGroup(this.present,[{binding:0,resource:interactive?this.reprojectSampler:this.sampler},{binding:1,resource:this.cachedTextureView(colored?colored.texture:f.front)},{binding:2,resource:this.cachedTextureView(f.history)},{binding:3,resource:{buffer:pb}}]),e=d.createCommandEncoder(),pass=e.beginRenderPass({colorAttachments:[{view:this.context.getCurrentTexture().createView(),clearValue:{r:0,g:0,b:0,a:1},loadOp:'clear',storeOp:'store'}]});pass.setPipeline(this.present);pass.setBindGroup(0,bg);pass.draw(3);pass.end();d.queue.submit([e.finish()])}
|
|||
|
|
async readMeta(indices){if(!this.frame||!indices.length)return new Uint32Array();const d=this.device,B=GPUBufferUsage,r=buf(d,indices.length*4,B.COPY_DST|B.MAP_READ),e=d.createCommandEncoder();for(let i=0;i<indices.length;i++)e.copyBufferToBuffer(this.frame.meta,indices[i]*4,r,i*4,4);d.queue.submit([e.finish()]);await r.mapAsync(GPUMapMode.READ);const out=new Uint32Array(r.getMappedRange().slice(0));r.unmap();destroy(r);return out}
|
|||
|
|
async readNumericalIndices(token){
|
|||
|
|
@@ -1504,7 +1507,7 @@
|
|||
|
|
}
|
|||
|
|
async renderTileRGBA2x({snap,iter,fastContext=null,fullW,fullH,tileX,tileY,w,h,strict=false,workspaceSlot=0,workspaceSize=this.exportTileSize()}){
|
|||
|
|
if(w*h*Math.max(1,iter)>48000000){const shape=this.numericTileShape(w,iter),out=new Uint8ClampedArray(w*h*4),generation=state.token;let unresolved=0;for(let y=0;y<h;y+=shape.rows)for(let x=0;x<w;x+=shape.width){if(generation!==state.token||exportJob.cancelled&&exportJob.active)throw new Error('cancelled');const tw=Math.min(shape.width,w-x),th=Math.min(shape.rows,h-y),part=await this.renderTileRGBA2x({snap,iter,fastContext,fullW,fullH,strict,workspaceSlot,workspaceSize,tileX:tileX+x,tileY:tileY+y,w:tw,h:th});for(let row=0;row<th;row++)out.set(part.rgba.subarray(row*tw*4,(row+1)*tw*4),((y+row)*w+x)*4);unresolved+=part.unresolved}return{rgba:out,unresolved}}
|
|||
|
|
- await this.ready;await this.ensureAaPipeline();const ws=this.ensureExportWorkspace(workspaceSlot,workspaceSize);if(w>ws.size||h>ws.size)throw new Error('export tile exceeds reusable workspace');const fastExtended=!!fastContext;if(fastExtended){await this.ensureFastPipelines();this.setFastContext(fastContext)}const d=this.device,meta=ws.meta,smooth=ws.smooth,encoder=d.createCommandEncoder({label:'export-aa2x'}),offsets=[[.25,.25],[.75,.25],[.25,.75],[.75,.75]],ca=this.colorParamsData(w,h,iter);new DataView(ca).setUint32(12,0,true);d.queue.writeBuffer(ws.cbuf,0,ca);for(let si=0;si<4;si++){const [sampleX,sampleY]=offsets[si],pbuf=ws.pbufs[si],unresolved=ws.unresolveds[si];d.queue.writeBuffer(unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));if(fastExtended){const ref=referencePixelForSource(fastContext?.source,snap,fullW,fullH);d.queue.writeBuffer(pbuf,0,this.fastParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,ref.x,ref.y,w,0,0,strict?1:0));this.encodeFastNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,strict?1:0));this.encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h})}const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:ws.cbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}},{binding:3,resource:ws.samples[si].createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));cp.end()}const abg=d.createBindGroup({layout:this.aaResolve.getBindGroupLayout(0),entries:[{binding:0,resource:ws.samples[0].createView()},{binding:1,resource:ws.samples[1].createView()},{binding:2,resource:ws.samples[2].createView()},{binding:3,resource:ws.samples[3].createView()},{binding:4,resource:ws.tex.createView()}]}),ap=encoder.beginComputePass();ap.setPipeline(this.aaResolve);ap.setBindGroup(0,abg);ap.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));ap.end();const bpr=Math.ceil(w*4/256)*256,pixelBytes=bpr*h,statsBytes=4*UNRESOLVED_BYTES;encoder.copyTextureToBuffer({texture:ws.tex},{buffer:ws.read,bytesPerRow:bpr,rowsPerImage:h},{width:w,height:h});for(let si=0;si<4;si++)encoder.copyBufferToBuffer(ws.unresolveds[si],0,ws.read,pixelBytes+si*UNRESOLVED_BYTES,UNRESOLVED_BYTES);d.queue.submit([encoder.finish()]);await ws.read.mapAsync(GPUMapMode.READ,0,pixelBytes+statsBytes);const raw=new Uint8Array(ws.read.getMappedRange(0,pixelBytes+statsBytes)),out=new Uint8ClampedArray(w*h*4);for(let y=0;y<h;y++)out.set(raw.subarray(y*bpr,y*bpr+w*4),y*w*4);let unresolved=0;for(let si=0;si<4;si++)unresolved+=new Uint32Array(raw.buffer,raw.byteOffset+pixelBytes+si*UNRESOLVED_BYTES,UNRESOLVED_BYTES/4)[0]||0;ws.read.unmap();return{rgba:out,unresolved}
|
|||
|
|
+ await this.ready;await this.ensureAaPipeline();const ws=this.ensureExportWorkspace(workspaceSlot,workspaceSize);this.ensureExportAaSamples(ws);if(w>ws.size||h>ws.size)throw new Error('export tile exceeds reusable workspace');const fastExtended=!!fastContext;if(fastExtended){await this.ensureFastPipelines();this.setFastContext(fastContext)}const d=this.device,meta=ws.meta,smooth=ws.smooth,encoder=d.createCommandEncoder({label:'export-aa2x'}),offsets=[[.25,.25],[.75,.25],[.25,.75],[.75,.75]],ca=this.colorParamsData(w,h,iter);new DataView(ca).setUint32(12,0,true);d.queue.writeBuffer(ws.cbuf,0,ca);for(let si=0;si<4;si++){const [sampleX,sampleY]=offsets[si],pbuf=ws.pbufs[si],unresolved=ws.unresolveds[si];d.queue.writeBuffer(unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));if(fastExtended){const ref=referencePixelForSource(fastContext?.source,snap,fullW,fullH);d.queue.writeBuffer(pbuf,0,this.fastParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,ref.x,ref.y,w,0,0,strict?1:0));this.encodeFastNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,strict?1:0));this.encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h})}const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:ws.cbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}},{binding:3,resource:ws.samples[si].createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));cp.end()}const abg=d.createBindGroup({layout:this.aaResolve.getBindGroupLayout(0),entries:[{binding:0,resource:ws.samples[0].createView()},{binding:1,resource:ws.samples[1].createView()},{binding:2,resource:ws.samples[2].createView()},{binding:3,resource:ws.samples[3].createView()},{binding:4,resource:ws.tex.createView()}]}),ap=encoder.beginComputePass();ap.setPipeline(this.aaResolve);ap.setBindGroup(0,abg);ap.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));ap.end();const bpr=Math.ceil(w*4/256)*256,pixelBytes=bpr*h,statsBytes=4*UNRESOLVED_BYTES;encoder.copyTextureToBuffer({texture:ws.tex},{buffer:ws.read,bytesPerRow:bpr,rowsPerImage:h},{width:w,height:h});for(let si=0;si<4;si++)encoder.copyBufferToBuffer(ws.unresolveds[si],0,ws.read,pixelBytes+si*UNRESOLVED_BYTES,UNRESOLVED_BYTES);d.queue.submit([encoder.finish()]);await ws.read.mapAsync(GPUMapMode.READ,0,pixelBytes+statsBytes);const raw=new Uint8Array(ws.read.getMappedRange(0,pixelBytes+statsBytes)),out=new Uint8ClampedArray(w*h*4);for(let y=0;y<h;y++)out.set(raw.subarray(y*bpr,y*bpr+w*4),y*w*4);let unresolved=0;for(let si=0;si<4;si++)unresolved+=new Uint32Array(raw.buffer,raw.byteOffset+pixelBytes+si*UNRESOLVED_BYTES,UNRESOLVED_BYTES/4)[0]||0;ws.read.unmap();return{rgba:out,unresolved}
|
|||
|
|
}
|
|||
|
|
destroy(){this.frameDestroy();this.exportWorkspaceDestroy();this.destroyDeepContext();this.destroyFastContext()}
|
|||
|
|
}
|
|||
|
|
@@ -1514,6 +1517,16 @@
|
|||
|
|
// sparse/moderate failures go directly to local references.
|
|||
|
|
// Integer interval tests over exact rational pixel coordinates. No f32
|
|||
|
|
// rounding, periodic-orbit guess, or agreement of two precisions is a proof.
|
|||
|
|
+
|
|||
|
|
+function knownInteriorViewportMayOverlap(snap,width,height){
|
|||
|
|
+ const unit=1n<<BigInt(snap.bits),bw=BigInt(Math.max(1,width)),bh=BigInt(Math.max(1,height));
|
|||
|
|
+ const xLo=2n*snap.re-snap.span,xHi=2n*snap.re+snap.span,xDen=2n*unit;
|
|||
|
|
+ const yLo=2n*bw*snap.im-snap.span*bh,yHi=2n*bw*snap.im+snap.span*bh,yDen=2n*bw*unit;
|
|||
|
|
+ const axis=(lo,hi,den,lp,lq,hp,hq)=>hi*BigInt(lq)>=BigInt(lp)*den&&lo*BigInt(hq)<=BigInt(hp)*den;
|
|||
|
|
+ const cardioid=axis(xLo,xHi,xDen,-3,4,3,8)&&axis(yLo,yHi,yDen,-2,3,2,3);
|
|||
|
|
+ const bulb=axis(xLo,xHi,xDen,-5,4,-3,4)&&axis(yLo,yHi,yDen,-1,4,1,4);
|
|||
|
|
+ return cardioid||bulb;
|
|||
|
|
+}
|
|||
|
|
function certifiedInteriorTiles(snap,width,height,tile=32,historySeed=null){
|
|||
|
|
const columns=Math.ceil(width/tile),rows=Math.ceil(height/tile),mask=new Uint32Array(4+columns*rows);
|
|||
|
|
mask.set([width,height,tile,columns]);let pixels=0;
|
|||
|
|
@@ -1715,7 +1728,7 @@
|
|||
|
|
constructor(){this.workers=[];this.serial=0;this.pending=new Map();this.failed=false;this.ctx=null}
|
|||
|
|
ensure(){if(this.workers.length)return true;if(this.failed||typeof Worker==='undefined'||typeof Blob==='undefined')return false;try{this.ctx=canvas.getContext('2d');if(!this.ctx)throw new Error('2D canvas unavailable');const count=Math.max(1,Math.min(4,(Number(navigator.hardwareConcurrency)||4)-1));for(let i=0;i<count;i++){const url=URL.createObjectURL(new Blob([cpuFallbackWorkerSource()],{type:'text/javascript'})),w=new Worker(url);w.__url=url;w.onmessage=e=>{const d=e.data,p=this.pending.get(d.id);if(!p)return;this.pending.delete(d.id);d.type==='error'?p.reject(new Error(d.error)):p.resolve(d)};w.onerror=e=>{this.failed=true;for(const p of this.pending.values())p.reject(new Error(e.message||'CPU fallback worker error'));this.pending.clear()};this.workers.push(w)}return true}catch(e){this.failed=true;return false}}
|
|||
|
|
request(worker,payload){const id=++this.serial;return new Promise((resolve,reject)=>{this.pending.set(id,{resolve,reject});worker.postMessage({...payload,id})})}
|
|||
|
|
- async render(snap,iter,token){if(!this.ensure())throw new Error('CPU fallback workerを作成できません');const w=canvas.width,h=canvas.height,re=fixedNum(snap.re,snap.bits),im=fixedNum(snap.im,snap.bits),span=fixedNum(snap.span,snap.bits),step=span/Math.max(1,w);if(!Number.isFinite(re)||!Number.isFinite(im)||!Number.isFinite(span)||span<=0||step===0||(re!==0&&re+step===re)||(im!==0&&im+step===im))throw new Error('CPU fallback の倍精度範囲を超えています');const style=colorStyleSnapshot(),jobs=[],rows=Math.ceil(h/this.workers.length);for(let i=0;i<this.workers.length;i++){const y0=i*rows,y1=Math.min(h,y0+rows);if(y0<y1)jobs.push(this.request(this.workers[i],{type:'render',w,h,y0,y1,maxIter:iter,re,im,span,style}))}const parts=await Promise.all(jobs);if(token!==state.token)throw new Error('cancelled');const rgba=new Uint8ClampedArray(w*h*4);let operationLimit=0,heuristicInterior=0,work=0,escaped=0;for(const part of parts){rgba.set(new Uint8ClampedArray(part.rgba),part.y0*w*4);operationLimit+=part.operationLimit||0;heuristicInterior+=part.heuristicInterior||0;work+=part.work||0;escaped+=part.escaped||0}this.ctx.putImageData(new ImageData(rgba,w,h),0,0);state.cpuFrameRgba=rgba;return{operationLimit,heuristicInterior,work,escaped,workers:this.workers.length}}
|
|||
|
|
+ async render(snap,iter,token){if(!this.ensure())throw new Error('CPU fallback workerを作成できません');const w=canvas.width,h=canvas.height,re=fixedNum(snap.re,snap.bits),im=fixedNum(snap.im,snap.bits),span=fixedNum(snap.span,snap.bits),step=span/Math.max(1,w);if(!Number.isFinite(re)||!Number.isFinite(im)||!Number.isFinite(span)||span<=0||step===0||(re!==0&&re+step===re)||(im!==0&&im+step===im))throw new Error('CPU fallback の倍精度範囲を超えています');const style=colorStyleSnapshot(),jobs=[],rows=Math.ceil(h/this.workers.length);for(let i=0;i<this.workers.length;i++){const y0=i*rows,y1=Math.min(h,y0+rows);if(y0<y1)jobs.push(this.request(this.workers[i],{type:'render',w,h,y0,y1,maxIter:iter,re,im,span,style}))}const parts=await Promise.all(jobs);if(token!==state.token)throw new Error('cancelled');const rgba=new Uint8ClampedArray(w*h*4);let operationLimit=0,heuristicInterior=0,work=0,escaped=0;for(const part of parts){rgba.set(new Uint8ClampedArray(part.rgba),part.y0*w*4);operationLimit+=part.operationLimit||0;heuristicInterior+=part.heuristicInterior||0;work+=part.work||0;escaped+=part.escaped||0}this.ctx.putImageData(new ImageData(rgba,w,h),0,0);return{operationLimit,heuristicInterior,work,escaped,workers:this.workers.length}}
|
|||
|
|
cancelPending(reason='cancelled'){for(const p of this.pending.values())p.reject(new Error(reason));this.pending.clear();this.destroyWorkers()}
|
|||
|
|
destroyWorkers(){for(const w of this.workers){try{w.terminate()}catch{}try{URL.revokeObjectURL(w.__url)}catch{}}this.workers.length=0}
|
|||
|
|
destroy(){this.cancelPending('destroyed');this.ctx=null}
|
|||
|
|
@@ -1759,9 +1772,9 @@
|
|||
|
|
}
|
|||
|
|
async function renderFinalFrame(r,snap,iter,token){
|
|||
|
|
let decision=chooseBackend(snap,canvas.width),fastExtended=decision.fastExtended;
|
|||
|
|
- const historySeed=fastExtended?r.numericHistorySeed(snap,iter,false,true):null,proof=certifiedInteriorTiles(snap,canvas.width,canvas.height,32,historySeed);let certified=0,preserveInterior=false;
|
|||
|
|
+ const historySeed=fastExtended?r.numericHistorySeed(snap,iter,false,true):null,proof=knownInteriorViewportMayOverlap(snap,canvas.width,canvas.height)?certifiedInteriorTiles(snap,canvas.width,canvas.height,32,historySeed):{mask:null,pixels:0};let certified=0,preserveInterior=false;
|
|||
|
|
if(proof.pixels||historySeed){certified=await r.certifyInterior(snap,token,true,proof,historySeed);if(token!==state.token)return null;preserveInterior=certified>0||!!historySeed}
|
|||
|
|
- if(certified===canvas.width*canvas.height){decision={backend:'exact-interior',fastExtended:false,reason:'exact-full-interior'};const stats=await r.readUnresolvedStats();if(token!==state.token)return null;state.unresolved=stats.total;state.unknownReasons=stats.reasons;state.heuristicInterior=0;const painted=await r.recolor(token,iter);if(!painted||token!==state.token)return null;const frontier={stats,iter,minimumIter:iter,rounds:0,processed:0,escaped:0,converged:true,finiteBudgetComplete:true,policy:'exact-interior-proof',membershipCertified:true,applied:true};state.frameView=snap;state.fieldView={...snap,iter,frontier,w:canvas.width,h:canvas.height,fastExtended:false,backend:'exact-interior',referenceKey:'',referenceReused:false,complete:true,membershipCertified:true};r.captureColorSource(snap,state.colorAuto||state.recolorPending);r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});return{decision,referenceReused:false,referencePixel:null,certified}}
|
|||
|
|
+ if(certified===canvas.width*canvas.height){decision={backend:'exact-interior',fastExtended:false,reason:'exact-full-interior'};const stats={total:0,heuristicInterior:0,reasons:{errorBound:0,escapeUncertain:0,referenceEnd:0,rebaseGap:0,range:0,operationLimit:0}};state.unresolved=0;state.unknownReasons=stats.reasons;state.heuristicInterior=0;const painted=await r.recolor(token,iter);if(!painted||token!==state.token)return null;const frontier={stats,iter,minimumIter:iter,rounds:0,processed:0,escaped:0,converged:true,finiteBudgetComplete:true,policy:'exact-interior-proof',membershipCertified:true,applied:true};state.frameView=snap;state.fieldView={...snap,iter,frontier,w:canvas.width,h:canvas.height,fastExtended:false,backend:'exact-interior',referenceKey:'',referenceReused:false,complete:true,membershipCertified:true};r.captureColorSource(snap,state.colorAuto||state.recolorPending);r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});return{decision,referenceReused:false,referencePixel:null,certified}}
|
|||
|
|
let fastCtx=null,referencePixel=null,referenceReused=false;if(fastExtended){const reuse=reusableFastReference(snap,iter,canvas.width,canvas.height);if(reuse){fastCtx=reuse.ctx;referencePixel=reuse.pixel;referenceReused=true}else{fastCtx=await fastRefs.request(snap,iter,canvas.width,canvas.height);if(token!==state.token)return null;referencePixel=referencePixelForSource(fastCtx.source,snap,canvas.width,canvas.height)}}
|
|||
|
|
const deferColor=!!(r.historyReady&&canUseStableReprojection());state.deferNumericPublish=deferColor;const ok=await r.computeFrame(snap,iter,token,referencePixel,fastExtended,fastCtx,preserveInterior,!!historySeed,deferColor);if(!ok||token!==state.token){state.deferNumericPublish=false;return null}if(!deferColor&&!await presentProvisional(r,snap,iter,token,true)){state.deferNumericPublish=false;return null}
|
|||
|
|
let stats=await r.readUnresolvedStats();if(token!==state.token)return null;if(fastExtended){const initialFailed=numericalFailureCount(stats);if(initialFailed>0){const ordered=await recoverNumericalCostOrdered(r,snap,iter,token,stats);if(!ordered||token!==state.token){state.deferNumericPublish=false;return null}stats=ordered.stats}state.unresolved=stats.total;state.unknownReasons=stats.reasons;state.heuristicInterior=stats.heuristicInterior||0;if(initialFailed>0&&!deferColor){const painted=await r.recolor(token,iter);if(!painted||token!==state.token){state.deferNumericPublish=false;return null}}}else{state.unresolved=stats.total;state.unknownReasons=stats.reasons}
|
|||
|
|
@@ -1819,7 +1832,7 @@
|
|||
|
|
// Only promote a colour frame to stable history if it still represents
|
|||
|
|
// the newest slider value. Rapid dragging therefore collapses to the
|
|||
|
|
// latest value instead of replaying stale colour states.
|
|||
|
|
- if(revision===state.colorRevision&&numericFrameComplete()){renderer.commitHistory(state.frameView);state.temporalFill=false}
|
|||
|
|
+ if(revision===state.colorRevision&&numericFrameComplete()){renderer.commitHistory(state.frameView,false);state.temporalFill=false}
|
|||
|
|
updateStats();
|
|||
|
|
// Yield ownership after one pass: a stream of hue changes must not keep
|
|||
|
|
// recoloring=true forever and prevent a pending pan from computing.
|
|||
|
|
@@ -1863,7 +1876,7 @@
|
|||
|
|
function syncColorAutoButton(){const b=$('colorAuto');b.textContent=state.colorAuto?'色相変化停止':'色相変化';b.classList.toggle('on',state.colorAuto);b.setAttribute('aria-pressed',state.colorAuto?'true':'false')}
|
|||
|
|
function stopColorAuto(repaint=true){state.colorAuto=false;colorAutoLast=0;if(colorAutoRaf){cancelAnimationFrame(colorAutoRaf);colorAutoRaf=0}syncColorAutoButton();if(repaint)requestRecolor()}
|
|||
|
|
function colorAutoStep(now){if(!state.colorAuto){colorAutoRaf=0;return}if(!colorAutoLast)colorAutoLast=now;const dt=Math.min(.1,Math.max(0,(now-colorAutoLast)/1000));colorAutoLast=now;/* Hue animation advances shift directly. No cycle or phase compensation is applied. */state.shift=fract01(state.shift+.09*dt);$('shift').value=String(state.shift);$('shiftO').textContent=state.shift.toFixed(2);if(!state.pointerActive&&!state.wheelActive&&now-colorAutoPaint>=(state.rendering?100:50)){colorAutoPaint=now;requestRecolor()}colorAutoRaf=requestAnimationFrame(colorAutoStep)}
|
|||
|
|
-$('colorAuto').onclick=()=>{state.colorAuto=!state.colorAuto;syncColorAutoButton();if(state.colorAuto&&!colorAutoRaf){if(!state.rendering)renderer?.captureColorSource();colorAutoRaf=requestAnimationFrame(colorAutoStep);}else if(!state.colorAuto)stopColorAuto()};
|
|||
|
|
+$('colorAuto').onclick=()=>{state.colorAuto=!state.colorAuto;syncColorAutoButton();if(state.colorAuto&&!colorAutoRaf){colorAutoRaf=requestAnimationFrame(colorAutoStep);}else if(!state.colorAuto)stopColorAuto()};
|
|||
|
|
$('palette').addEventListener('change',e=>{const pal=Math.max(0,Math.min(PALETTE_MAX,Number(e.target.value)|0));state.palette=VALID_PALETTE_IDS.has(pal)?pal:0;requestRecolor();saveHash()});
|
|||
|
|
function applyCycleControl(e){state.cycle=sliderToCycle(e.target.value);$('cycleO').textContent=state.cycle.toFixed(4);requestRecolor()}
|
|||
|
|
function applyShiftControl(e){if(state.colorAuto)stopColorAuto();state.shift=Math.max(0,Math.min(1,Number(e.target.value)||0));$('shiftO').textContent=state.shift.toFixed(2);requestRecolor()}
|
|||
|
|
@@ -1940,7 +1953,7 @@
|
|||
|
|
downloadBlob(blob,base+'.png');downloadBlob(new Blob([JSON.stringify(meta,null,2)],{type:'application/json'}),base+'.json');
|
|||
|
|
$('exportStatus').textContent=unresolvedSamples?'保存しました · 未確定sample '+unresolvedSamples+'(sidecar参照)':'PNGと座標メタデータを保存しました。';
|
|||
|
|
}catch(e){await png.abort(e);$('exportStatus').textContent=String(e.message)==='cancelled'?'出力を中止しました。':'出力失敗: '+String(e&&e.message||e)}
|
|||
|
|
- finally{exportJob.active=false;$('exportStart').disabled=false}
|
|||
|
|
+ finally{r.exportWorkspaceDestroy();exportJob.active=false;$('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','')};$('exportScale').onchange=e=>{const s=Number(e.target.value);if(s)$('exportWidth').value=String(exportDimensions().w)};$('exportStart').onclick=runExport;$('exportCancel').onclick=()=>{if(exportJob.active){exportJob.cancelled=true;$('exportStatus').textContent='中止しています…'}else $('exportDialog').close()};$('exportQuick').onclick=()=>canvas.toBlob(blob=>{if(blob)downloadBlob(blob,'mandelbrot-'+Date.now()+(state.fieldView?.complete===false?'-preview':'')+'.png')},'image/png');
|
|||
|
|
|