0xee514bd0…b86dsent to0x062a6f33…3dec·#25,857,080·view on Etherscan
(x1<=x0)continue;
const colt=new Float64Array(S); let any=false;
for(let y=0;y<S;y++){let a=0,n=0;
for(let x=x0;x<x1;x++) if(F.mask[y*S+x]){a+=t[y*S+x];n++;}
colt[y]=n?a/n:0; if(n)any=true;}
if(!any)continue;
let y0=-1,y1=-1; for(let y=0;y<S;y++){let has=false;
for(let x=x0;x<x1;x++) if(F.mask[y*S+x]){has=true;break;}
if(has){if(y0<0)y0=y;y1=y;}}
let y=y0;
while(y<y1){const v=colt[y],L=Math.round(2+14*v),gap=Math.round(1+7*(1-v));
for(let q=y;q<Math.min(y+L,S);q++)for(let x=x0;x<x1;x++) if(F.mask[q*S+x])o[q*S+x]=1;
y+=L+Math.max(gap,1);}}
return o;};
/* ---------- tiling / cellular ---------- */
A.truchet=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:0.9},p));
const c=p.cell||9,{tb,den}=cellMean(F,t,c),o=new Float64Array(S*S);
for(let gy=0;gy+c<=S;gy+=c)for(let gx=0;gx+c<=S;gx+=c){
const ci=(gy+(c>>1))*S+gx+(c>>1); if(den[ci]<0.5)continue;
const v=tb[ci]; if(v<0.10)continue;
const flip=(((gx/c)*7919+(gy/c)*104729)%2)===0, w=v>0.55?1.9:1.0;
for(let y=0;y<c;y++)for(let x=0;x<c;x++){
const a=flip? (Math.abs(Math.hypot(x,y)-c/2)<w)||(Math.abs(Math.hypot(x-c,y-c)-c/2)<w)
: (Math.abs(Math.hypot(x-c,y)-c/2)<w)||(Math.abs(Math.hypot(x,y-c)-c/2)<w);
if(a&&F.mask[(gy+y)*S+gx+x])o[(gy+y)*S+gx+x]=1;}}
return o;};
A.maze=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:0.85},p));
const c=p.cell||7,{tb,den}=cellMean(F,t,c),o=new Float64Array(S*S),r=rng32(p.seed||1);
for(let gy=0;gy+c<S;gy+=c)for(let gx=0;gx+c<S;gx+=c){
const ci=(gy+(c>>1))*S+gx+(c>>1); if(den[ci]<0.5)continue;
const v=tb[ci]; if(v<0.06)continue; const pr=0.25+0.75*v;
if(r()<pr) for(let y=0;y<c;y++) if(F.mask[(gy+y)*S+gx])o[(gy+y)*S+gx]=1;
if(r()<pr) for(let x=0;x<c;x++) if(F.mask[gy*S+gx+x])o[gy*S+gx+x]=1;
if(v>0.62&&r()<v) for(let y=0;y<c;y++){const xx=gx+c-1; if(xx<S&&F.mask[(gy+y)*S+xx])o[(gy+y)*S+xx]=1;}}
return o;};
A.brick=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:0.9},p));
const cw=p.cw||14,ch=p.chh||7,{tb,den}=cellMean(F,t,cw),o=new Float64Array(S*S);
for(let y=0;y<S;y++)for(let x=0;x<S;x++){const i=y*S+x;if(!F.mask[i]||den[i]<=0.5)continue;
const row=Math.floor(y/ch),xo=(row%2===0)?0:cw/2;
const u=wrap((x+xo)/cw),v=wrap(y/ch),ins=0.5*(1-cl(tb[i],0,1));
if(u>ins&&u<1-ins&&v>0.14&&v<0.86)o[i]=1;}
return o;};
A.checker=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:0.75,white:0.04},p));
const c=p.cell||7,{tb,den}=cellMean(F,t,c),o=new Float64Array(S*S);
for(let y=0;y<S;y++)for(let x=0;x<S;x++){const i=y*S+x;if(!F.mask[i]||den[i]<=0.5)continue;
const par=((Math.floor(x/c)+Math.floor(y/c))%2)===0;
const s=cl(tb[i]*1.5,0,1),u=Math.abs(wrap(x/c)-0.5)*2,v=Math.abs(wrap(y/c)-0.5)*2,r=Math.max(u,v);
if((par&&r<s)||(!par&&r<cl((tb[i]-0.5)*3,0,1)))o[i]=1;}
return o;};
A.crossgrid=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:0.70,white:0.04},p));
const c=p.cell||8,{tb,den}=cellMean(F,t,c),o=new Float64Array(S*S);
for(let y=0;y<S;y++)for(let x=0;x<S;x++){const i=y*S+x;if(!F.mask[i]||den[i]<=0.5)continue;
const u=Math.abs((x%c)-c/2),v=Math.abs((y%c)-c/2);
const L=0.5*c*cl(tb[i]*1.5,0,1),arm=0.5+1.4*cl(tb[i],0,1);
if((u<arm&&v<L)||(v<arm&&u<L))o[i]=1;}
return o;};
A.trigrid=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:0.9},p));
const c=p.cell||11,{tb,den}=cellMean(F,t,c|0),o=new Float64Array(S*S);
for(let y=0;y<S;y++)for(let x=0;x<S;x++){const i=y*S+x;if(!F.mask[i]||den[i]<=0.5)continue;
const row=Math.floor(y/c),u=wrap((x+(row%2===0?0:c/2))/c),v=wrap(y/c);
if(Math.min(Math.min(u,1-u),v)<0.42*cl(tb[i],0,1))o[i]=1;}
return o;};
A.weave=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:0.85},p));
const c=p.cell||11,{tb,den}=cellMean(F,t,c),o=new Float64Array(S*S);
for(let y=0;y<S;y++)for(let x=0;x<S;x++){const i=y*S+x;if(!F.mask[i]||den[i]<=0.5)continue;
const h=((Math.floor(x/c)+Math.floor(y/c))%2)===0;
/* no 1.3 gain: it pushed mid-tones straight to the clamp, so strand width
saturated and the lit side of the head went flat white. */
const u=wrap(x/c),v=wrap(y/c),w=0.5*cl(tb[i],0,1);
if(h?Math.abs(v-0.5)<w:Math.abs(u-0.5)<w)o[i]=1;}
return o;};
A.herringbone=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:0.85},p));
const c=p.cell||13,{tb,den}=cellMean(F,t,c),o=new Float64Array(S*S);
const a1=Math.PI/4,a2=-Math.PI/4;
for(let y=0;y<S;y++)for(let x=0;x<S;x++){const i=y*S+x;if(!F.mask[i]||den[i]<=0.5)continue;
const blk=((Math.floor(x/c)+Math.floor(y/c))%2)===0;
const q=blk?(x*Math.cos(a1)+y*Math.sin(a1))/3.2:(x*Math.cos(a2)+y*Math.sin(a2))/3.2;
if(Math.abs(wrap(q)-0.5)<cl(tb[i]*0.62,0,0.5))o[i]=1;}
return o;};
A.scales=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:0.85},p));
const c=p.cell||13,{tb,den}=cellMean(F,t,c|0),o=new Float64Array(S*S);
for(let y=0;y<S;y++)for(let x=0;x<S;x++){const i=y*S+x;if(!F.mask[i]||den[i]<=0.5)continue;
const row=Math.floor(y/(c*0.5)),u=((x+(row%2===0?0:c/2))%c)-c/2,v=y%(c*0.5);
const w=0.9+1.9*cl(tb[i],0,1);
if(Math.abs(Math.hypot(u,v)-c*0.5)<w)o[i]=1;}
return o;};
A.nested=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:0.72,white:0.04},p));
const c=p.cell||11,{tb,den}=cellMean(F,t,c),o=new Float64Array(S*S);
for(let y=0;y<S;y++)for(let x=0;x<S;x++){const i=y*S+x;if(!F.mask[i]||den[i]<=0.5)continue;
const u=Math.abs(wrap(x/c)-0.5)*2,v=Math.abs(wrap(y/c)-0.5)*2,r=Math.max(u,v);
/* lv may reach 0 so dark cells draw NOTHING. `floor(g*4)+1` with a min of
1 meant every cell in the mask drew at least one ring regardless of
tone, which is why tone could never empty the shadow side. */
const g=cl(tb[i]*1.45,0,1),lv=cl(Math.round(g*4),0,4);
for(let k=1;k<=4;k++) if(lv>=k&&Math.abs(r-k/4)<0.13){o[i]=1;break;}}
return o;};
A.argyle=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:0.85},p));
const c=p.cell||15,{tb,den}=cellMean(F,t,c),o=new Float64Array(S*S);
for(let y=0;y<S;y++)for(let x=0;x<S;x++){const i=y*S+x;if(!F.mask[i]||den[i]<=0.5)continue;
const a=wrap((x+y)/c),b=wrap((x-y)/c),w=cl(tb[i]*0.55,0.02,0.5);
if(Math.abs(a-0.5)<w||Math.abs(b-0.5)<w)o[i]=1;}
return o;};
A.tally=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:0.70,white:0.04},p));
const c=p.cell||9,{tb,den}=cellMean(F,t,c),o=new Float64Array(S*S);
for(let y=0;y<S;y++)for(let x=0;x<S;x++){const i=y*S+x;if(!F.mask[i]||den[i]<=0.5)continue;
const u=wrap(x/c),v=wrap(y/c),g=cl(tb[i]*1.5,0,1),cnt=cl(Math.floor(g*5),0,5);
let hit=false;
for(let k=0;k<4;k++){const pos=(k+0.6)/4.6;
if(Math.abs(u-pos)<0.10&&v>0.12&&v<0.88&&cnt>k){hit=true;break;}}
if(!hit&&cnt>=5&&Math.abs((u-0.5)*1.2+(v-0.5))<0.10&&v>0.12&&v<0.88)hit=true;
if(hit)o[i]=1;}
return o;};
A.quadtree=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:0.9},p)),o=new Float64Array(S*S);
const minc=p.minc||4,maxd=p.maxd||6,thr=p.thresh||0.055;
function rec(y,x,s,d){
if(y>=S||x>=S)return;
let n=0,sum=0,sq=0;
for(let yy=y;yy<Math.min(y+s,S);yy++)for(let xx=x;xx<Math.min(x+s,S);xx++){
const i=yy*S+xx; if(F.mask[i]){n++;sum+=t[i];sq+=t[i]*t[i];}}
if(n===0)return;
const mean=sum/n,sd=Math.sqrt(Math.max(sq/n-mean*mean,0));
if(s<minc||d>=maxd||sd<thr||s<=minc*2){
for(let yy=y;yy<Math.min(y+s,S);yy++) if(F.mask[yy*S+x])o[yy*S+x]=1;
for(let xx=x;xx<Math.min(x+s,S);xx++) if(F.mask[y*S+xx])o[y*S+xx]=1;
return;}
const h=s>>1;
rec(y,x,h,d+1);rec(y,x+h,h,d+1);rec(y+h,x,h,d+1);rec(y+h,x+h,h,d+1);}
let P2=1; while(P2<S)P2*=2;
rec(0,0,P2>>1,0);rec(0,P2>>1,P2>>1,0);rec(P2>>1,0,P2>>1,0);rec(P2>>1,P2>>1,P2>>1,0);
return o;};
A.wolfram=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:0.9},p));
const c=p.cell||3,rule=p.rule||110,gw=Math.floor(S/c),gh=Math.floor(S/c);
const r=rng32(p.seed||1); let row=new Uint8Array(gw);
for(let i=0;i<gw;i++) row[i]=r()<0.5?1:0;
const R=[]; for(let i=0;i<8;i++) R.push((rule>>i)&1);
const grid=[];
for(let y=0;y<gh;y++){ grid.push(row.slice());
const nr=new Uint8Array(gw);
for(let i=0;i<gw;i++){const l=row[(i-1+gw)%gw],m=row[i],rr=row[(i+1)%gw];
nr[i]=R[(l<<2)|(m<<1)|rr];}
row=nr;}
const {tb,den}=cellMean(F,t,c*3),o=new Float64Array(S*S);
for(let y=0;y<S;y++)for(let x=0;x<S;x++){const i=y*S+x;if(!F.mask[i]||tb[i]<=0.20)continue;
const gy=Math.floor(y/c),gx=Math.floor(x/c);
if(gy<gh&&gx<gw&&grid[gy][gx])o[i]=1;}
return o;};
A.lowpoly=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:0.9},p));
/* Delaunay-free: connect each site to its nearest neighbours (relative
neighbourhood graph approximation) — visually equivalent wireframe. */
const pts=pickWeighted(F.mask,t,S,p.sites||520,p.seed||1);
const er=[]; for(let y=1;y<S-1;y++)for(let x=1;x<S-1;x++){const i=y*S+x;
if(F.mask[i]&&(!F.mask[i-1]||!F.mask[i+1]||!F.mask[i-S]||!F.mask[i+S]))er.push(i);}
const step=Math.max((er.length/220)|0,1);
for(let k=0;k<er.length;k+=step) pts.push(er[k]);
const P=pts.map(i=>[i%S,(i/S)|0]);
const o=new Float64Array(S*S);
for(let a=0;a<P.length;a++){
const d=[]; for(let b=0;b<P.length;b++){ if(a===b)continue;
d.push([Math.hypot(P[a][0]-P[b][0],P[a][1]-P[b][1]),b]);}
d.sort((u,v)=>u[0]-v[0]);
for(let k=0;k<Math.min(4,d.length);k++){const b=d[k][1]; if(b<a)continue;
line(o,F.mask,S,P[a][0],P[a][1],P[b][0],P[b][1]);}}
return o;};
A.lissajous=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:0.9},p)),o=new Float64Array(S*S);
const [cx,cy]=centroid(F.mask,S),r=rng32(p.seed||1),curves=p.curves||150;
for(let k=0;k<curves;k++){
const A1=(0.18+r()*0.32)*S,B1=(0.18+r()*0.32)*S;
const aa=1+((r()*6)|0),bb=1+((r()*6)|0),ph=r()*2*Math.PI,thr=0.12+r()*0.43;
for(let q=0;q<1400;q++){const T=q/1400*2*Math.PI;
const x=Math.round(cx+A1*Math.sin(aa*T+ph)),y=Math.round(cy+B1*Math.sin(bb*T));
if(x<0||x>=S||y<0||y>=S)continue; const i=y*S+x;
if(F.mask[i]&&t[i]>thr)o[i]=1;}}
return o;};
A.ascii=(F,p)=>{
const S=F.size,t=tone(F,Object.assign({gamma:0.95},p));
/* real bitmap typography: a 5x7 font, a density-sorted character ramp, and
per-cell glyph choice. mark scale sets the type size. */
const ms=p.ms||1;
const cw=Math.max(Math.round(5*ms+1),4), ch=Math.max(Math.round(7*ms+1),6);
const sx=(cw-1)/5, sy=(ch-1)/7; // glyph -> cell scale
const ramp=GLYPH_RAMP(p.ramp||'classic');
const n=ramp.length;
const {tb,den}=cellMean(F,t,Math.max(cw,ch));
const o=new Float64Array(S*S);
for(let gy=0;gy+ch<=S;gy+=ch)for(let gx=0;gx+cw<=S;gx+=cw){
const ci=(gy+(ch>>1))*S+gx+(cw>>1);
if(den[ci]<0.45)continue;
const v=cl(tb[ci],0,1);
const gi=cl(Math.round(v*(n-1)),0,n-1);
const bits=GLYPH_FONT[ramp[gi]];
if(!bits)continue;
for(let r=0;r<7;r++){
const row=bits[r]; if(!row)continue;
for(let c=0;c<5;c++){
if(!(row & (1<<(4-c))))continue;
/* paint the glyph pixel scaled into the cell */
const y0=gy+Math.round(r*sy), x0=gx+Math.round(c*sx);
const y1=gy+Math.round((r+1)*sy), x1=gx+Math.round((c+1)*sx);
for(let y=y0;y<Math.max(y1,y0+1);y++)for(let x=x0;x<Math.max(x1,x0+1);x++){
if(y<0||y>=S||x<0||x>=S)continue;
const i=y*S+x; if(F.mask[i])o[i]=1;
}
}
}
}
return o;};
A.voiceprint=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:0.9},p)),o=new Float64Array(S*S);
const rows=p.rows||60,rh=Math.max(S/rows,2);
const {tb,den}=cellMean(F,t,Math.max(Math.round(rh),2));
for(let y=0;y<S;y++)for(let x=0;x<S;x++){const i=y*S+x;if(!F.mask[i])continue;
const frac=(y%rh)/rh,half=cl(tb[i],0,1)*0.5;
if(Math.abs(frac-0.5)<half)o[i]=1;}
return o;};
A.guilloche=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:0.9},p)),o=new Float64Array(S*S);
const [cx,cy]=centroid(F.mask,S); let cnt=0; for(let i=0;i<S*S;i++) if(F.mask[i])cnt++;
const R=Math.max(Math.sqrt(cnt),1),rings=p.rings||30,pet=p.petals||20,amp=p.amp||0.045;
for(let y=0;y<S;y++)for(let x=0;x<S;x++){const i=y*S+x;if(!F.mask[i])continue;
const X=(x-cx)/R,Y=(y-cy)/R,th=Math.atan2(Y,X),rr=Math.hypot(X,Y);
const band=(rr+amp*Math.sin(pet*th))*rings;
if(Math.abs(wrap(band)-0.5)<0.06+0.30*t[i])o[i]=1;}
return o;};
/* helpers */
function pickWeighted(mask,t,S,n,seed){
const idx=[]; for(let i=0;i<S*S;i++) if(mask[i])idx.push(i);
const w=idx.map(i=>t[i]+0.06); let tot=0; for(const v of w)tot+=v;
const r=rng32(seed),out=[],used=new Set();
const take=Math.min(n,idx.length); let guard=0;
while(out.length<take&&guard++<take*40){
let q=r()*tot,k=0; while(k<w.length-1&&q>w[k]){q-=w[k];k++;}
if(used.has(k))continue; used.add(k); out.push(idx[k]);}
return out;
}
function line(o,mask,S,x0,y0,x1,y1){
const dx=Math.abs(x1-x0),dy=Math.abs(y1-y0);
const sx=x0<x1?1:-1,sy=y0<y1?1:-1; let err=dx-dy,x=x0,y=y0;
for(let g=0;g<4000;g++){
if(x>=0&&x<S&&y>=0&&y<S&&mask[y*S+x])o[y*S+x]=1;
if(x===x1&&y===y1)break;
const e2=2*err;
if(e2>-dy){err-=dy;x+=sx;}
if(e2<dx){err+=dx;y+=sy;}}
}
})();
/* --- algos4.js --- */
(function(){
/* KYS algorithms part 4 — flow field, circle packing, recursive subdivision. */
'use strict';
const {A,shade,tone,blur,blueNoise,whiteNoise,rng32,centroid,edt}=KYSU;
const {cellMean,pickN,smoothDepth,wrap,cl}=KYSU;
/* ---- 81. FLOW FIELD -------------------------------------------------
* Long advected streamlines. The field is a stable global angle rotated by
* smoothed depth — NOT raw gradient direction, which is undefined on flat
* regions and produced whorls in an earlier attempt. */
A.flowfield=(F,p)=>{
const S=F.size, t=tone(F,Object.assign({gamma:0.9},p)), o=new Float64Array(S*S);
const ms=p.ms||1;
const dd=blur((()=>{const d=new Float64Array(S*S);
for(let i=0;i<S*S;i++) d[i]=F.mask[i]?F.depth[i]:0; return d;})(), S, 6);
const base=(p.base===undefined?-28:p.base)*Math.PI/180;
const swirl=(p.swirl===undefined?2.6:p.swirl);
/* sub-pixel step + continuous stamping: the previous version wrote one
pixel per unit step, which staircased. Now we walk in small increments
and stamp a soft round nib along the path, so strokes read as drawn
lines rather than aliased runs. */
const step=0.45;
const maxLen=Math.round((p.flowlen||70)*Math.sqrt(ms));
const n=Math.max(Math.round((p.flowseeds||4200)/(ms*ms*1.2)),150);
const seeds=pickN(F.mask,S,n,(p.seed||1));
const rad=Math.max(0.55*ms,0.55); // nib radius in px (float)
const R=Math.ceil(rad);
const stamp=(fx,fy)=>{
const x0=Math.floor(fx-R), x1=Math.ceil(fx+R);
const y0=Math.floor(fy-R), y1=Math.ceil(fy+R);
for(let y=y0;y<=y1;y++){
if(y<0||y>=S)continue;
for(let x=x0;x<=x1;x++){
if(x<0||x>=S)continue;
const i=y*S+x; if(!F.mask[i])continue;
/* coverage of this pixel by the nib, smoothed at the rim */
const d=Math.hypot(x+0.5-fx, y+0.5-fy);
const cov=cl(rad+0.5-d,0,1);
if(cov>o[i]) o[i]=cov;
}
}
};
const angAt=(fx,fy)=>{
/* bilinear sample of the depth field so the field itself is smooth */
const x=cl(fx,0,S-1.001), y=cl(fy,0,S-1.001);
const xi=x|0, yi=y|0, tx=x-xi, ty=y-yi;
const a=dd[yi*S+xi], b=dd[yi*S+xi+1];
const c=dd[(yi+1)*S+xi], e=dd[(yi+1)*S+xi+1];
const v=(a*(1-tx)+b*tx)*(1-ty)+(c*(1-tx)+e*tx)*ty;
return base + swirl*(v-0.5)*Math.PI;
};
/* separation grid: a streamline dies when it comes too close to one already
drawn. Without this, strokes merge into solid slabs at high seed counts. */
const sep=Math.max(rad*2.0+0.9, 1.6);
const gs=Math.max(Math.round(sep),1), gw=Math.ceil(S/gs);
const occ=new Int32Array(gw*gw).fill(-1);
let sid=0;
const tooClose=(fx,fy,id)=>{
const gx=(fx/gs)|0, gy=(fy/gs)|0;
for(let j=gy-1;j<=gy+1;j++){
if(j<0||j>=gw)continue;
for(let i2=gx-1;i2<=gx+1;i2++){
if(i2<0||i2>=gw)continue;
const v=occ[j*gw+i2];
if(v>=0 && v!==id) return true;
}
}
return false;
};
for(const s of seeds){
let x=(s%S)+0.5, y=((s/S)|0)+0.5;
const id=sid++;
const px=[],py=[];
const life=Math.round(maxLen*(0.35+0.9*t[s])/step);
for(let k=0;k<life;k++){
if(x<0||x>=S-1||y<0||y>=S-1) break;
const i=(y|0)*S+(x|0);
if(!F.mask[i]) break;
if(k>2 && tooClose(x,y,id)) break;
px.push(x); py.push(y);
const a=angAt(x,y);
x+=Math.cos(a)*step; y+=Math.sin(a)*step;
}
/* discard stubs: a stroke shorter than minLen is an artefact, not a mark */
const minLen=Math.max(Math.round(6/step),8);
if(px.length<minLen) continue;
for(let q=0;q<px.length;q++){
occ[(((py[q]/gs)|0)*gw)+((px[q]/gs)|0)]=id;
stamp(px[q],py[q]);
}
}
/* threshold the coverage field: keeps rim pixels that are >half covered,
which is what removes the staircase without needing greyscale output */
for(let i=0;i<S*S;i++) o[i]= o[i]>0.5?1:0;
return o;
};
/* ---- 82. CIRCLE PACKING --------------------------------------------
* True greedy packing: candidate centres tested against placed circles,
* radius limited by tone (small in shadow, large in light). */
A.packing=(F,p)=>{
const S=F.size, t=tone(F,Object.assign({gamma:0.9},p)), o=new Float64Array(S*S);
const ms=p.ms||1, r=rng32(p.seed||1);
const lw=Math.max(ms*0.9,0.9);
/* the STROKE has width, so a circle must fit its outer edge inside the
silhouette: R + lw <= dist. Reserving only 0.5px clipped 80% of circles. */
const pad=lw+0.5;
const rmin=Math.max(1.2*ms,1), rmax=Math.max(14*ms,rmin+2);
const tries=Math.round((p.packtries||9000));
const cx=[],cy=[],cr=[];
const dist=edt(F.mask,S);
for(let k=0;k<tries;k++){
const x=(r()*S)|0, y=(r()*S)|0, i=y*S+x;
if(!F.mask[i]) continue;
let R=rmin+(rmax-rmin)*Math.pow(1-cl(t[i],0,1),1.4);
/* shrink to fit rather than clip: the circle packs as close to the edge as
it can while staying wholly inside */
R=Math.min(R, dist[i]-pad);
if(R<rmin) continue;
let ok=true;
for(let j=cx.length-1;j>=0;j--){
const dx=cx[j]-x, dy=cy[j]-y;
if(dx*dx+dy*dy < (cr[j]+R+1)*(cr[j]+R+1)){ ok=false; break; }
}
if(!ok) continue;
cx.push(x); cy.push(y); cr.push(R);
}
for(let j=0;j<cx.length;j++){
const R=cr[j], x0=cx[j], y0=cy[j];
const lo=Math.max(0,Math.floor(y0-R-lw)), hi=Math.min(S-1,Math.ceil(y0+R+lw));
for(let y=lo;y<=hi;y++){
const xl=Math.max(0,Math.floor(x0-R-lw)), xr=Math.min(S-1,Math.ceil(x0+R+lw));
for(let x=xl;x<=xr;x++){
const d=Math.hypot(x-x0,y-y0);
if(Math.abs(d-R)<lw && F.mask[y*S+x]) o[y*S+x]=1;
}
}
}
return o;
};
/* ---- 83. RECURSIVE SUBDIVISION (Mondrian) ---------------------------
* Split rectangles at random ratios, deeper where tone varies. Cells above a
* tone threshold get filled with a dither so the face reads. */
A.subdivide=(F,p)=>{
const S=F.size, t=tone(F,Object.assign({gamma:0.9},p)), o=new Float64Array(S*S);
const ms=p.ms||1, r=rng32(p.seed||1);
const minSide=Math.max(Math.round(7*ms),4);
const maxDepth=p.maxdepth||9;
const bn=blueNoise(S,(p.seed||1)+7,1.6);
const stats=(x,y,w,h)=>{
let n=0,s=0,s2=0;
const sx=Math.max(1,Math.floor(w/12)), sy=Math.max(1,Math.floor(h/12));
for(let j=y;j<y+h;j+=sy)for(let i=x;i<x+w;i+=sx){
if(i>=S||j>=S)continue; const k=j*S+i;
if(!F.mask[k])continue; n++; s+=t[k]; s2+=t[k]*t[k];
}
if(!n) return null;
const m=s/n;
return {n,mean:m,sd:Math.sqrt(Math.max(s2/n-m*m,0))};
};
const edge=(x,y,w,h)=>{
for(let i=x;i<x+w;i++){
if(y<S&&F.mask[y*S+i]) o[y*S+i]=1;
const yb=y+h-1; if(yb<S&&F.mask[yb*S+i]) o[yb*S+i]=1;
}
for(let j=y;j<y+h;j++){
if(x<S&&F.mask[j*S+x]) o[j*S+x]=1;
const xr=x+w-1; if(xr<S&&F.mask[j*S+xr]) o[j*S+xr]=1;
}
};
const fill=(x,y,w,h,lvl)=>{
for(let j=y;j<y+h;j++)for(let i=x;i<x+w;i++){
if(i>=S||j>=S)continue; const k=j*S+i;
if(F.mask[k] && bn[k]<lvl) o[k]=1;
}
};
(function rec(x,y,w,h,d){
if(x>=S||y>=S||w<2||h<2) return;
const st=stats(x,y,w,h);
if(!st) return;
const split = d<maxDepth && w>minSide*2 && h>minSide*2 &&
(st.sd>0.045 || d<2);
if(!split){
edge(x,y,w,h);
/* tone 0 (lit) -> nearly empty, tone 1 (shadow) -> dense.
Cap well below saturation: a fully-inked cell reads as a blown-out
white slab and loses the dither texture that makes the method work. */
/* gamma the cell tone so mid/high tones stay textured rather than
crowding toward solid; cap keeps the brightest cells readable */
fill(x+1,y+1,Math.max(w-2,0),Math.max(h-2,0),
Math.min(Math.pow(cl(st.mean,0,1),1.45)*0.85,0.62));
return;
}
const ratio=0.32+r()*0.36; // random, not always half
if(w>=h){ const cut=Math.max(minSide,Math.round(w*ratio));
rec(x,y,cut,h,d+1); rec(x+cut,y,w-cut,h,d+1); }
else { const cut=Math.max(minSide,Math.round(h*ratio));
rec(x,y,w,cut,d+1); rec(x,y+cut,w,h-cut,d+1); }
})(0,0,S,S,0);
return o;
};
})();
/* --- algos5.js --- */
(function(){
/* KYS algorithms part 5 — restored methods.
* oil / attractor / stringart, rebuilt with the lessons since. */
'use strict';
const {A,shade,tone,blur,blueNoise,whiteNoise,rng32,centroid,edt}=KYSU;
const {cellMean,pickN,smoothDepth,wrap,cl}=KYSU;
/* ---- FINGERPRINT ----------------------------------------------------
* The v2/v3 look: ridge flow following a smoothed orientation field, so the
* ridges whorl around features like a real print. The reason it was cut was
* that raw-gradient orientation is undefined on flat regions and produced
* noise; the fix is a COHERENCE-WEIGHTED blend toward a stable global angle,
* plus enough smoothing that the field varies slowly. */
A.oil=(F,p)=>{
const S=F.size, ms=p.ms||1;
const t=tone(F,Object.assign({gamma:0.9},p));
const lam=shade(F,p);
/* structure tensor of the SHADING, heavily smoothed */
const g=blur(lam,S,3.0);
const gx=new Float64Array(S*S), gy=new Float64Array(S*S);
for(let y=1;y<S-1;y++)for(let x=1;x<S-1;x++){
const i=y*S+x; gx[i]=(g[i+1]-g[i-1])*0.5; gy[i]=(g[i+S]-g[i-S])*0.5;
}
const sm=(p.orient===undefined?11:p.orient);
const Jxx=blur((()=>{const a=new Float64Array(S*S);for(let i=0;i<S*S;i++)a[i]=gx[i]*gx[i];return a;})(),S,sm);
const Jyy=blur((()=>{const a=new Float64Array(S*S);for(let i=0;i<S*S;i++)a[i]=gy[i]*gy[i];return a;})(),S,sm);
const Jxy=blur((()=>{const a=new Float64Array(S*S);for(let i=0;i<S*S;i++)a[i]=gx[i]*gy[i];return a;})(),S,sm);
const base=(p.base===undefined?78:p.base)*Math.PI/180;
const th=new Float64Array(S*S);
for(let i=0;i<S*S;i++){
const tr=Jxx[i]+Jyy[i], det=Jxx[i]*Jyy[i]-Jxy[i]*Jxy[i];
const disc=Math.sqrt(Math.max(tr*tr-4*det,0));
const coh=tr>1e-12 ? cl(disc/(tr+1e-12),0,1) : 0;
const w=cl(coh*2.2,0,1); // trust the field only where coherent
const a=0.5*Math.atan2(2*Jxy[i], Jxx[i]-Jyy[i]);
const cx=w*Math.cos(2*a)+(1-w)*Math.cos(2*base);
const sy=w*Math.sin(2*a)+(1-w)*Math.sin(2*base);
th[i]=0.5*Math.atan2(sy,cx);
}
/* integrate a phase along the ridge direction, then threshold by tone.
Sub-pixel phase (not a per-pixel wave) keeps the ridges smooth. */
const freq=(p.freq===undefined?0.30:p.freq)/Math.max(ms,0.3);
const o=new Float64Array(S*S);
const dd=blur((()=>{const a=new Float64Array(S*S);
for(let i=0;i<S*S;i++)a[i]=F.mask[i]?F.depth[i]:0; return a;})(),S,4);
for(let y=0;y<S;y++)for(let x=0;x<S;x++){
const i=y*S+x; if(!F.mask[i])continue;
const a=th[i]+Math.PI/2; // ridges run across the gradient
const ph=(x*Math.cos(a)+y*Math.sin(a))*freq + 7.0*dd[i];
const v=Math.cos(ph);
const thr=0.52-1.05*cl(t[i],0,1); // duty cycle carries tone
if(v>thr)o[i]=1;
}
return o;
};
/* ---- STRING ART -----------------------------------------------------
* The original saturated to ~92% ink because each chord subtracted too little
* residual and chords were accepted on mean tone alone. Fixes: anti-aliased
* accumulation, a hard per-pixel ink budget, and rejecting chords that do not
* beat a rising bar. */
})();
root.KYSU=KYSU; root.KYS_A=KYSU.A;
})(typeof self!=="undefined"?self:this);