memoscan
← all memos

Memo 0xbcba9446…4a3186 on Ethereum

/* KYS roster — 80 methods. Generated by build_bundle.js. Do not edit. */ (function(root){ 'use strict'; var KYSU={}; /* --- algos.js --- */ /* KYS ink algorithms — JS port. Deterministic given (seed, params). * Each: (F) -> Float64Array ink 0..1, where F = {mask,depth,normal,size,...} */ 'use strict'; /* ------------------------------------------------------------ utilities */ function shade(F, p) { const {normal:n, mask:m, size:S} = F; const a = (p.light_dir||0)*Math.PI/180, e = (p.light_elev||0)*Math.PI/180; let lx = Math.sin(a)*Math.cos(e), ly = Math.sin(e), lz = Math.cos(a)*Math.cos(e); const L = Math.hypot(lx,ly,lz)||1; lx/=L; ly/=L; lz/=L; const soft = p.soft||0, N = S*S, out = new Float64Array(N); for (let i=0;i<N;i++){ if(!m[i]) continue; let lam = n[i*3]*lx + n[i*3+1]*ly + n[i*3+2]*lz; lam = lam<0?0:lam>1?1:lam; if(soft>0){ let nz=n[i*3+2]; nz=nz<0?0:nz>1?1:nz; lam = lam*(1-soft)+soft*nz; } out[i]=lam; } return out; } function tone(F, p) { const lam = shade(F,p), N = F.size*F.size, out = new Float64Array(N); const w = p.white===undefined?0.10:p.white, g = p.gamma===undefined?1.0:p.gamma; for (let i=0;i<N;i++){ if(!F.mask[i]) continue; let t = 1-lam[i]; t = (t-w)/(1-w+1e-9); t = t<0?0:t>1?1:t; out[i] = g===1 ? t : Math.pow(t,g); } return out; } /* separable gaussian blur (mirrors scipy.ndimage.gaussian_filter closely enough) */ function blur(src, S, sigma) { if (sigma<=0) return src.slice(); const r = Math.max(1,Math.ceil(sigma*3)), k = new Float64Array(2*r+1); let s=0; for(let i=-r;i<=r;i++){ const v=Math.exp(-i*i/(2*sigma*sigma)); k[i+r]=v; s+=v; } for(let i=0;i<k.length;i++) k[i]/=s; const tmp=new Float64Array(S*S), out=new Float64Array(S*S); for(let y=0;y<S;y++) for(let x=0;x<S;x++){ let a=0; for(let i=-r;i<=r;i++){ const xx=Math.min(S-1,Math.max(0,x+i)); a+=src[y*S+xx]*k[i+r]; } tmp[y*S+x]=a; } for(let y=0;y<S;y++) for(let x=0;x<S;x++){ let a=0; for(let i=-r;i<=r;i++){ const yy=Math.min(S-1,Math.max(0,y+i)); a+=tmp[yy*S+x]*k[i+r]; } out[y*S+x]=a; } return out; } function boxMasked(t, m, S, c) { /* mask-aware cell mean: average tone over masked pixels only */ const num=new Float64Array(S*S), den=new Float64Array(S*S); const half=Math.floor(c/2); const it=new Float64Array((S+1)*(S+1)), im=new Float64Array((S+1)*(S+1)); for(let y=0;y<S;y++) for(let x=0;x<S;x++){ const v=m[y*S+x]?t[y*S+x]:0, w=m[y*S+x]?1:0; it[(y+1)*(S+1)+x+1]=v+it[y*(S+1)+x+1]+it[(y+1)*(S+1)+x]-it[y*(S+1)+x]; im[(y+1)*(S+1)+x+1]=w+im[y*(S+1)+x+1]+im[(y+1)*(S+1)+x]-im[y*(S+1)+x]; } const sum=(I,x0,y0,x1,y1)=>I[y1*(S+1)+x1]-I[y0*(S+1)+x1]-I[y1*(S+1)+x0]+I[y0*(S+1)+x0]; for(let y=0;y<S;y++) for(let x=0;x<S;x++){ const x0=Math.max(0,x-half),y0=Math.max(0,y-half); const x1=Math.min(S,x+c-half),y1=Math.min(S,y+c-half); const dv=sum(im,x0,y0,x1,y1); den[y*S+x]=dv/((x1-x0)*(y1-y0)); num[y*S+x]=dv>0.35*((x1-x0)*(y1-y0))?sum(it,x0,y0,x1,y1)/Math.max(dv,1e-6):0; } return {tb:num, den}; } /* deterministic per-tile PRNG */ function rng32(seed){ let s = seed>>>0; return function(){ s^=s<<13; s>>>=0; s^=s>>17; s^=s<<5; s>>>=0; return s/4294967296; }; } function whiteNoise(S, seed){ const r=rng32(seed||1), o=new Float64Array(S*S); for(let i=0;i<S*S;i++) o[i]=r(); return o; } function blueNoise(S, seed, sigma){ const wn=whiteNoise(S,seed), lo=blur(wn,S,sigma===undefined?2.2:sigma); const o=new Float64Array(S*S); let mn=Infinity,mx=-Infinity; for(let i=0;i<S*S;i++){ const v=wn[i]-lo[i]; o[i]=v; if(v<mn)mn=v; if(v>mx)mx=v; } const rg=Math.max(mx-mn,1e-9); for(let i=0;i<S*S;i++) o[i]=(o[i]-mn)/rg; return o; } function centroid(m,S){ let sx=0,sy=0,c=0; for(let y=0;y<S;y++)for(let x=0;x<S;x++) if(m[y*S+x]){sx+=x;sy+=y;c++;} return c? [sx/c,sy/c] : [S/2,S/2]; } const A = {}; /* algorithm registry */ /* 5x7 bitmap font + density ramps for the ascii method (see glyphs.js) */ let GLYPH_FONT={}, GLYPH_RAMP=()=>[' ']; function installGlyphs(font, rampOf){ GLYPH_FONT=font; GLYPH_RAMP=rampOf; } /* ---- mark-scale for dither families ------------------------------------ * Dithering has no intrinsic mark size — its dot is one pixel of whatever * matrix it uses. To make MARK SCALE meaningful (and render size irrelevant), * we quantise the tone field into blocks of `b` pixels and dither the BLOCKS, * then paint each block solid. Bigger scale = chunkier, more abstract, less * photographic — which is the point: the method should obscure the model. */ /* ---- boundary-aware shape placement ------------------------------------ * A grid cell whose shape would be substantially cut by the silhouette reads * as a broken fragment rather than a mark. `cellFits` returns the fraction of * a disc of radius R at (x0,y0) that lies inside the mask, so callers can drop * or shrink shapes that do not fit. */ function discFit(mask,S,x0,y0,R){ if(R<=0.5) return 1; let inside=0,total=0; const r=Math.ceil(R); for(let dy=-r;dy<=r;dy++)for(let dx=-r;dx<=r;dx++){ if(dx*dx+dy*dy>R*R) continue; total++; const y=y0+dy,x=x0+dx; if(y>=0&&y<S&&x>=0&&x<S&&mask[y*S+x]) inside++; } return total? inside/total : 0; } function blockSize(p){ return Math.max(Math.round((p.ms||1)*2), 1); } function blockTone(t, mask, S, b){ if(b<=1) return {tone:t, b:1}; const gw=Math.ceil(S/b), acc=new Float64Array(gw*gw), cnt=new Float64Array(gw*gw); for(let y=0;y<S;y++)for(let x=0;x<S;x++){ const i=y*S+x; if(!mask[i])continue; const g=((y/b)|0)*gw+((x/b)|0); acc[g]+=t[i]; cnt[g]++; } for(let g=0;g<acc.length;g++) if(cnt[g]) acc[g]/=cnt[g]; return {tone:acc, b, gw}; } function paintBlocks(hit, gw, S, b, mask){ const 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(!mask[i])continue; if(hit[((y/b)|0)*gw+((x/b)|0)]) o[i]=1; } return o; } /* ------------------------------------------------------- 01 banding */ /* ------------------------------------------------------- 02 vertical */ A.vertical=(F,p)=>{ const S=F.size,t0=tone(F,Object.assign({gamma:0.85,white:0.08},p)); // horizontal tone smoothing const t=new Float64Array(S*S); for(let y=0;y<S;y++)for(let x=0;x<S;x++){ let a=0,n=0; for(let d=-4;d<=4;d++){const xx=x+d; if(xx<0||xx>=S)continue; if(F.mask[y*S+xx]){a+=t0[y*S+xx];n++;}} t[y*S+x]=n?a/n:0; } const sp=p.spacing||7,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])continue; const ph=(x%sp)/sp, half=Math.min(t[i]*0.70,0.5); if(Math.abs(ph-0.5)<half) o[i]=1;} return o; }; /* ------------------------------------------------------- 03 diagonal */ A.diagonal=(F,p)=>{ const S=F.size,t=tone(F,Object.assign({gamma:0.9},p)),o=new Float64Array(S*S); const sp=p.spacing||12,a=(p.angle||34)*Math.PI/180,ca=Math.cos(a),sa=Math.sin(a); 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 q=(x*ca+y*sa)/sp, ph=((q%1)+1)%1, half=Math.min(t[i]*0.62,0.5); if(Math.abs(ph-0.5)<half)o[i]=1;} return o; }; /* ------------------------------------------------------- 04 radial */ /* ------------------------------------------------------- 05 spiral */ A.spiral=(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), turns=p.turns||34, R=Math.max(S*0.5,1); 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,Y=y-cy, th=Math.atan2(Y,X), rr=Math.hypot(X,Y)/R; const ph=((rr*turns+th/(2*Math.PI))%1+1)%1; if(Math.abs(ph-0.5)<Math.min(t[i]*0.58,0.5))o[i]=1;} return o; }; /* ------------------------------------------------------- 13 edgerings */ A.edgerings=(F,p)=>{ const S=F.size,t=tone(F,Object.assign({gamma:0.9},p)),o=new Float64Array(S*S); const dist=edt(F.mask,S); let mx=0; for(let i=0;i<S*S;i++) if(dist[i]>mx)mx=dist[i]; const bands=p.bands||17; for(let i=0;i<S*S;i++){ if(!F.mask[i])continue; const dn=dist[i]/Math.max(mx,1e-9), ph=((dn*bands)%1+1)%1; if(Math.abs(ph-0.5)<Math.min(t[i]*0.58,0.5))o[i]=1;} return o; }; /* ------------------------------------------------------- 17 moire */ A.moire=(F,p)=>{ const S=F.size,t=tone(F,Object.assign({gamma:0.9},p)),o=new Float64Array(S*S); const sp=p.sp||7, dth=(p.dth||4)*Math.PI/180; const dsrc=new Float64Array(S*S); for(let i=0;i<S*S;i++) dsrc[i]=F.mask[i]?F.depth[i]:0; const dd=blur(dsrc,S,7); const layer=(x,y,ang,warp,i)=>{ const q=(x*Math.cos(ang)+y*Math.sin(ang))/sp + warp*dd[i]*9; return Math.abs(((q%1)+1)%1-0.5)<0.25; }; 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 l1=layer(x,y,0,1.0,i), l2=layer(x,y,dth,1.35,i); if(l1!==l2)o[i]=1;} return o; }; /* ------------------------------------------------------- 19 bayer */ const BAYER8=(()=>{ let B=[[0,2],[3,1]]; while(B.length<8){ const n=B.length*2,N=[]; for(let y=0;y<n;y++){N.push([]); for(let x=0;x<n;x++){ const q=B[y%B.length][x%B.length]; const s=(y<B.length?(x<B.length?0:2):(x<B.length?3:1)); N[y].push(4*q+s);}} B=N; } const f=[]; for(let y=0;y<8;y++)for(let x=0;x<8;x++) f.push(B[y][x]/64); return f; })(); A.bayer=(F,p)=>{ const S=F.size,t=tone(F,p),b=blockSize(p); if(b<=1){ const 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])continue; if(t[i]>BAYER8[(y%8)*8+(x%8)])o[i]=1;} return o; } const {tone:bt,gw}=blockTone(t,F.mask,S,b); const hit=new Uint8Array(bt.length); for(let gy=0;gy<gw;gy++)for(let gx=0;gx<gw;gx++){ const g=gy*gw+gx; if(bt[g]>BAYER8[(gy%8)*8+(gx%8)])hit[g]=1; } return paintBlocks(hit,gw,S,b,F.mask); }; /* ------------------------------------------------------- 20 floyd */ A.floyd=(F,p)=>{ const S=F.size,t0=tone(F,p),b=blockSize(p); if(b<=1){ const t=t0.slice(),o=new Float64Array(S*S); for(let y=0;y<S;y++)for(let x=0;x<S;x++){ const i=y*S+x;const old=t[i],nv=old>0.5?1:0; if(F.mask[i])o[i]=nv;const err=old-nv; if(x+1<S)t[i+1]+=err*7/16; if(y+1<S){if(x>0)t[i+S-1]+=err*3/16;t[i+S]+=err*5/16;if(x+1<S)t[i+S+1]+=err*1/16;} } return o; } const {tone:bt,gw}=blockTone(t0,F.mask,S,b); const g2=bt.slice(),hit=new Uint8Array(bt.length); for(let y=0;y<gw;y++)for(let x=0;x<gw;x++){ const i=y*gw+x,old=g2[i],nv=old>0.5?1:0; hit[i]=nv;const err=old-nv; if(x+1<gw)g2[i+1]+=err*7/16; if(y+1<gw){if(x>0)g2[i+gw-1]+=err*3/16;g2[i+gw]+=err*5/16;if(x+1<gw)g2[i+gw+1]+=err*1/16;} } return paintBlocks(hit,gw,S,b,F.mask); }; /* ------------------------------------------------------- 21 halftone */ A.halftone_am=(F,p)=>{ const S=F.size,t=tone(F,Object.assign({gamma:0.9},p)),o=new Float64Array(S*S); const c=p.cell||7,a=(p.angle||45)*Math.PI/180,ca=Math.cos(a),sa=Math.sin(a); 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 u=(x*ca+y*sa)/c, v=(-x*sa+y*ca)/c; const du=((u%1)+1)%1-0.5, dv=((v%1)+1)%1-0.5; if(Math.hypot(du,dv)<0.72*Math.sqrt(Math.max(t[i],0)))o[i]=1;} return o; }; /* ------------------------------------------------------- 24 stipple */ A.stipple=(F,p)=>{ const S=F.size,t=tone(F,Object.assign({gamma:1.15},p)); /* Rank-based seeding + dilation. Dot radius follows mark scale, and the number of seeds is divided by the dot AREA so coverage stays stable — this is resolution-independent because both terms scale together. */ const ms=p.ms||1, rad=Math.max(Math.round(ms)-1,0); const area=Math.max((2*rad+1)*(2*rad+1)*0.78,1); const bn=blueNoise(S,p.seed||1,Math.max(2.2*ms,1.4)); const cand=[]; for(let i=0;i<S*S;i++) if(F.mask[i]&&t[i]>0.02) cand.push([t[i]-bn[i]*0.9,i]); if(!cand.length) return new Float64Array(S*S); cand.sort((a,c)=>c[0]-a[0]); const frac=Math.min(0.42/area*1.9,0.95); const target=Math.min(cand.length,Math.round(cand.length*frac)); const o=new Float64Array(S*S); for(let k=0;k<target;k++){ const i=cand[k][1], y=(i/S)|0, x=i%S; if(rad===0){ o[i]=1; continue; } for(let dy=-rad;dy<=rad;dy++)for(let dx=-rad;dx<=rad;dx++){ if(dx*dx+dy*dy>rad*rad+0.5)continue; const yy=y+dy,xx=x+dx; if(yy<0||yy>=S||xx<0||xx>=S)continue; if(F.mask[yy*S+xx])o[yy*S+xx]=1;}} return o; }; /* ------------------------------------------------------- 33 mezzotint */ A.mezzotint=(F,p)=>{ /* `lam` comes from tone(), not shade(). shade() is raw lambert lighting and never sees white/gamma — those are applied only inside tone() — so this method used to render IDENTICALLY at every white/gamma value (measured: 77% ink at all four points across the range). tone() returns 1-lam, so invert it back to keep the rest of the algorithm unchanged. */ const S=F.size,ms=p.ms||1; const _t=tone(F,p), lam=new Float64Array(S*S); for(let i=0;i<S*S;i++) lam[i]=F.mask[i]?1-_t[i]:0; /* grain cell grows with mark scale: coarse mezzotint reads as torn ink rather than a photograph */ const b=Math.max(Math.round(ms*1.6),1); const wn=whiteNoise(S,p.seed||1), nz=blur(wn,S,Math.max(0.6*ms,0.6)); let mn=Infinity,mx=-Infinity; for(let i=0;i<S*S;i++){if(nz[i]<mn)mn=nz[i];if(nz[i]>mx)mx=nz[i];} const rg=Math.max(mx-mn,1e-9),grain=p.grain||0.55; const t=new Float64Array(S*S); for(let i=0;i<S*S;i++) t[i]=F.mask[i]?1-Math.pow(lam[i],1/Math.max(grain,1e-3)):0; if(b<=1){ const o=new Float64Array(S*S); for(let i=0;i<S*S;i++){if(!F.mask[i])continue; if((nz[i]-mn)/rg > 1-t[i]) o[i]=1;} return o; } const {tone:bt,gw}=blockTone(t,F.mask,S,b); const bn=blockTone((()=>{const q=new Float64Array(S*S); for(let i=0;i<S*S;i++)q[i]=(nz[i]-mn)/rg; return q;})(),F.mask,S,b); const hit=new Uint8Array(bt.length); for(let g=0;g<bt.length;g++) if(bn.tone[g] < bt[g]) hit[g]=1; return paintBlocks(hit,gw,S,b,F.mask); }; /* ------------------------------------------------------- 44 voronoi */ A.voronoi=(F,p)=>{ const S=F.size,t=tone(F,Object.assign({gamma:0.9},p)); const pts=weightedPick(F.mask,t,S,p.sites||1500,p.seed||1); const lab=nearestLabel(pts,S); const 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])continue; if((x+1<S&&lab[i]!==lab[i+1])||(y+1<S&&lab[i]!==lab[i+S]))o[i]=1;} return o; }; /* ------------------------------------------------------- 71 woodcut */ A.woodcut=(F,p)=>{ const S=F.size,t=tone(F,Object.assign({gamma:0.80},p)),o=new Float64Array(S*S); const sp=p.spacing||9,a=(p.angle||15)*Math.PI/180,ca=Math.cos(a),sa=Math.sin(a); 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 q=(x*ca+y*sa)/sp, ph=Math.abs(((q%1)+1)%1-0.5); if(ph<0.06+0.44*Math.pow(Math.max(t[i],0),1.5))o[i]=1;} return o; }; /* ---------------------------------------------------------- helpers */ function edt(mask,S){ /* two-pass chamfer distance transform (interior distance to boundary) */ const INF=1e9, d=new Float64Array(S*S); for(let i=0;i<S*S;i++) d[i]=mask[i]?INF:0; for(let y=0;y<S;y++)for(let x=0;x<S;x++){const i=y*S+x; if(!mask[i])continue; let v=d[i]; if(y>0)v=Math.min(v,d[i-S]+1); if(x>0)v=Math.min(v,d[i-1]+1); if(y>0&&x>0)v=Math.min(v,d[i-S-1]+1.41421356); if(y>0&&x+1<S)v=Math.min(v,d[i-S+1]+1.41421356); d[i]=v;} for(let y=S-1;y>=0;y--)for(let x=S-1;x>=0;x--){const i=y*S+x; if(!mask[i])continue; let v=d[i]; if(y+1<S)v=Math.min(v,d[i+S]+1); if(x+1<S)v=Math.min(v,d[i+1]+1); if(y+1<S&&x+1<S)v=Math.min(v,d[i+S+1]+1.41421356); if(y+1<S&&x>0)v=Math.min(v,d[i+S-1]+1.41421356); d[i]=v;} return d; } function weightedPick(mask,t,S,n,seed){ const idx=[]; for(let i=0;i<S*S;i++) if(mask[i]) idx.push(i); const r=rng32(seed), out=[]; const w=idx.map(i=>t[i]+0.06); let tot=0; for(const v of w) tot+=v; const take=Math.min(n,idx.length); const used=new Set(); 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 nearestLabel(pts,S){ const lab=new Int32Array(S*S).fill(-1); const dist=new Float64Array(S*S).fill(1e9); const q=[]; pts.forEach((i,k)=>{ lab[i]=k; dist[i]=0; q.push(i); }); for(let h=0;h<q.length;h++){ const i=q[h], y=(i/S)|0, x=i%S; for(let dy=-1;dy<=1;dy++)for(let dx=-1;dx<=1;dx++){ if(!dx&&!dy)continue; const yy=y+dy,xx=x+dx; if(yy<0||yy>=S||xx<0||xx>=S)continue; const j=yy*S+xx, nd=dist[i]+((dx&&dy)?1.41421356:1); if(nd<dist[j]-1e-9){ dist[j]=nd; lab[j]=lab[i]; q.push(j); } } } return lab; } Object.assign(KYSU,{A,shade,tone,blur,blueNoise,whiteNoise,rng32,centroid,edt,discFit,blockTone,paintBlocks,blockSize,installGlyphs}); /* --- glyphs.js --- */ (function(){ /* 5x7 bitmap font for the ASCII method. * Each glyph is 7 rows of 5 bits, MSB = leftmost pixel. * Characters chosen as a density ramp: from sparse punctuation to dense blocks. */ 'use strict'; const FONT = { ' ': [0x00,0x00,0x00,0x00,0x00,0x00,0x00], '.': [0x00,0x00,0x00,0x00,0x00,0x0C,0x0C], ',': [0x00,0x00,0x00,0x00,0x0C,0x0C,0x08], ':': [0x00,0x0C,0x0C,0x00,0x0C,0x0C,0x00], '-': [0x00,0x00,0x00,0x1F,0x00,0x00,0x00], "'": [0x0C,0x0C,0x08,0x00,0x00,0x00,0x00], '"': [0x1B,0x1B,0x12,0x00,0x00,0x00,0x00], '^': [0x04,0x0A,0x11,0x00,0x00,0x00,0x00], '~': [0x00,0x00,0x09,0x16,0x00,0x00,0x00], '=': [0x00,0x00,0x1F,0x00,0x1F,0x00,0x00], '+': [0x00,0x04,0x04,0x1F,0x04,0x04,0x00], '*': [0x00,0x0A,0x04,0x1F,0x04,0x0A,0x00], ';': [0x00,0x0C,0x0C,0x00,0x0C,0x0C,0x08], '!': [0x04,0x04,0x04,0x04,0x04,0x00,0x04], '/': [0x01,0x02,0x02,0x04,0x08,0x08,0x10], '\\':[0x10,0x08,0x08,0x04,0x02,0x02,0x01], '|': [0x04,0x04,0x04,0x04,0x04,0x04,0x04], '(': [0x02,0x04,0x08,0x08,0x08,0x04,0x02], ')': [0x08,0x04,0x02,0x02,0x02,0x04,0x08], '<': [0x02,0x04,0x08,0x10,0x08,0x04,0x02], '>': [0x08,0x04,0x02,0x01,0x02,0x04,0x08], 'i': [0x04,0x00,0x0C,0x04,0x04,0x04,0x0E], 'l': [0x0C,0x04,0x04,0x04,0x04,0x04,0x0E], 'r': [0x00,0x00,0x16,0x19,0x10,0x10,0x10], 'c': [0x00,0x00,0x0E,0x11,0x10,0x11,0x0E], 'v': [0x00,0x00,0x11,0x11,0x11,0x0A,0x04], 'x': [0x00,0x00,0x11,0x0A,0x04,0x0A,0x11], 'z': [0x00,0x00,0x1F,0x02,0x04,0x08,0x1F], 't': [0x08,0x08,0x1C,0x08,0x08,0x09,0x06], 'n': [0x00,0x00,0x16,0x19,0x11,0x11,0x11], 'o': [0x00,0x00,0x0E,0x11,0x11,0x11,0x0E], 's': [0x00,0x00,0x0F,0x10,0x0E,0x01,0x1E], 'u': [0x00,0x00,0x11,0x11,0x11,0x13,0x0D], 'w': [0x00,0x00,0x11,0x11,0x15,0x15,0x0A], 'a': [0x00,0x00,0x0E,0x01,0x0F,0x11,0x0F], 'e': [0x00,0x00,0x0E,0x11,0x1F,0x10,0x0E], 'k': [0x10,0x10,0x12,0x14,0x18,0x14,0x12], 'h': [0x10,0x10,0x16,0x19,0x11,0x11,0x11], 'd': [0x01,0x01,0x0D,0x13,0x11,0x11,0x0F], 'b': [0x10,0x10,0x16,0x19,0x11,0x11,0x1E], 'p': [0x00,0x00,0x1E,0x11,0x1E,0x10,0x10], 'q': [0x00,0x00,0x0F,0x11,0x0F,0x01,0x01], 'm': [0x00,0x00,0x1A,0x15,0x15,0x15,0x15], 'g': [0x00,0x00,0x0F,0x11,0x0F,0x01,0x0E], '0': [0x0E,0x11,0x13,0x15,0x19,0x11,0x0E], '4': [0x02,0x06,0x0A,0x12,0x1F,0x02,0x02], '6': [0x06,0x08,0x10,0x1E,0x11,0x11,0x0E], '8': [0x0E,0x11,0x11,0x0E,0x11,0x11,0x0E], '9': [0x0E,0x11,0x11,0x0F,0x01,0x02,0x0C], '$': [0x04,0x0F,0x14,0x0E,0x05,0x1E,0x04], '&': [0x0C,0x12,0x14,0x08,0x15,0x12,0x0D], '%': [0x18,0x19,0x02,0x04,0x08,0x13,0x03], '#': [0x0A,0x0A,0x1F,0x0A,0x1F,0x0A,0x0A], '@': [0x0E,0x11,0x17,0x15,0x17,0x10,0x0E], 'M': [0x11,0x1B,0x15,0x15,0x11,0x11,0x11], 'W': [0x11,0x11,0x11,0x15,0x15,0x1B,0x11], 'N': [0x11,0x19,0x15,0x13,0x11,0x11,0x11], 'B': [0x1E,0x11,0x11,0x1E,0x11,0x11,0x1E], 'Q': [0x0E,0x11,0x11,0x11,0x15,0x12,0x0D], 'R': [0x1E,0x11,0x11,0x1E,0x14,0x12,0x11], 'D': [0x1C,0x12,0x11,0x11,0x11,0x12,0x1C], 'H': [0x11,0x11,0x11,0x1F,0x11,0x11,0x11], 'K': [0x11,0x12,0x14,0x18,0x14,0x12,0x11], 'X': [0x11,0x11,0x0A,0x04,0x0A,0x11,0x11], 'A': [0x0E,0x11,0x11,0x1F,0x11,0x11,0x11], 'G': [0x0E,0x11,0x10,0x17,0x11,0x11,0x0F], 'O': [0x0E,0x11,0x11,0x11,0x11,0x11,0x0E], '█': [0x1F,0x1F,0x1F,0x1F,0x1F,0x1F,0x1F], // full block '▓': [0x1F,0x15,0x1F,0x0A,0x1F,0x15,0x1F], // dark shade '▒': [0x15,0x0A,0x15,0x0A,0x15,0x0A,0x15], // medium shade '░': [0x11,0x00,0x04,0x00,0x11,0x00,0x04], // light shade }; /* count set bits so ramps can be built by measured density, not by eye */ function density(ch){ const g = FONT[ch]; if(!g) return 0; let n = 0; for (const row of g) for (let b = 0; b < 5; b++) if (row & (1 << b)) n++; return n / 35; } /* Named ramps, each sorted light -> dark by actual pixel density. */ const RAMPS = { classic: ' .:-=+*#%@', blocks: ' ░▒▓█', code: ' .,:;!|/\\<>()+*=#$&%@', letters: ' .:iclrvxznsoauwekhdbpqmMWNBQ@', minimal: ' .:-+*#@', dense: ' ░:*oOB▓█', numeric: ' .:-+049688$&%@', }; function rampOf(name){ const chars = (RAMPS[name] || RAMPS.classic).split(''); return chars.slice().sort((a,b)=>density(a)-density(b)); } root.GLYPH_FONT=FONT; root.GLYPH_RAMPOF=rampOf; })(); /* --- algos2.js --- */ (function(){ /* KYS algorithms part 2 — contour/banding + geometric families. */ 'use strict'; const {A,shade,tone,blur,blueNoise,whiteNoise,rng32,centroid,edt}=KYSU; const wrap=q=>((q%1)+1)%1; const cl=(v,a,b)=>v<a?a:v>b?b:v; function grid(F){return {S:F.size,m:F.mask};} function smoothDepth(F,s){const S=F.size,d=new Float64Array(S*S); for(let i=0;i<S*S;i++)d[i]=F.mask[i]?F.depth[i]:0; return blur(d,S,s);} function cells(F,t,c){return U.blur?cellMean(F,t,c):null;} function cellMean(F,t,c){ const S=F.size,half=(c/2)|0; const it=new Float64Array((S+1)*(S+1)),im=new Float64Array((S+1)*(S+1)); for(let y=0;y<S;y++)for(let x=0;x<S;x++){ const v=F.mask[y*S+x]?t[y*S+x]:0,w=F.mask[y*S+x]?1:0; it[(y+1)*(S+1)+x+1]=v+it[y*(S+1)+x+1]+it[(y+1)*(S+1)+x]-it[y*(S+1)+x]; im[(y+1)*(S+1)+x+1]=w+im[y*(S+1)+x+1]+im[(y+1)*(S+1)+x]-im[y*(S+1)+x];} const S1=S+1,tb=new Float64Array(S*S),den=new Float64Array(S*S); const sm=(I,x0,y0,x1,y1)=>I[y1*S1+x1]-I[y0*S1+x1]-I[y1*S1+x0]+I[y0*S1+x0]; for(let y=0;y<S;y++)for(let x=0;x<S;x++){ const x0=Math.max(0,x-half),y0=Math.max(0,y-half); const x1=Math.min(S,x+c-half),y1=Math.min(S,y+c-half); const area=(x1-x0)*(y1-y0),dv=sm(im,x0,y0,x1,y1); den[y*S+x]=dv/area; tb[y*S+x]=dv>0.35*area?sm(it,x0,y0,x1,y1)/Math.max(dv,1e-6):0;} return {tb,den}; } /* ---- band helper: phase field -> ink by tone-modulated half width ---- */ function bandFill(F,t,phase,gain){ const S=F.size,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])continue; if(Math.abs(wrap(phase(x,y,i))-0.5)<Math.min(t[i]*gain,0.5))o[i]=1;} return o; } A.sqrings=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:0.9},p)),[cx,cy]=centroid(F.mask,S); const R=Math.max(S*0.5,1),r=p.rings||26; return bandFill(F,t,(x,y)=>Math.max(Math.abs(x-cx),Math.abs(y-cy))/R*r,0.60);}; A.ellipses=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:0.9},p)),[cx,cy]=centroid(F.mask,S); const R=Math.max(S*0.5,1),r=p.rings||24,e=p.ecc||1.55; return bandFill(F,t,(x,y)=>Math.hypot((x-cx)*e,y-cy)/R*r,0.58);}; A.hexrings=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:0.9},p)),[cx,cy]=centroid(F.mask,S); const R=Math.max(S*0.5,1),r=p.rings||20; return bandFill(F,t,(x,y)=>{const X=Math.abs(x-cx),Y=Math.abs(y-cy); return Math.max(X*0.8660+Y*0.5,Y)/R*r;},0.58);}; A.polyrings=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:0.9},p)),[cx,cy]=centroid(F.mask,S); const R=Math.max(S*0.5,1),r=p.rings||22,n=p.sides||6,k=Math.PI/n; return bandFill(F,t,(x,y)=>{const th=Math.atan2(y-cy,x-cx),rr=Math.hypot(x-cx,y-cy); return rr*Math.cos(((th+k)%(2*k))-k)/R*r;},0.58);}; A.superformula=(F,p)=>{const S=F.size,t=tone(F,Object.assign{gamma:0.9},p)),[cx,cy]=centroid(F.mask,S); const R=Math.max(S*0.42,1),r=p.rings||20,mm=p.mm||7,n1=p.n1||0.3; return bandFill(F,t,(x,y)=>{const th=Math.atan2(y-cy,x-cx),rr=Math.hypot(x-cx,y-cy); const f=Math.pow(Math.pow(Math.abs(Math.cos(mm*th/4)),n1)+Math.pow(Math.abs(Math.sin(mm*th/4)),n1),-1/Math.max(n1,1e-3)); return rr/Math.max(f,0.35)/R*r;},0.55);}; A.spokes=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:0.9},p)),[cx,cy]=centroid(F.mask,S); const n=p.count||150; return bandFill(F,t,(x,y)=>(Math.atan2(y-cy,x-cx)/(2*Math.PI)+0.5)*n,0.60);}; A.morse=(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=Math.max(S*0.5,1),rings=p.rings||22,sp=p.spokes||90; 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 r=Math.hypot(x-cx,y-cy)/R,th=Math.atan2(y-cy,x-cx)/(2*Math.PI)+0.5; if(Math.abs(wrap(r*rings)-0.5)<0.30 && wrap(th*sp)<cl(t[i]*1.25,0,1))o[i]=1;} return o;}; A.chevron=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:0.9},p)); const sp=p.spacing||11,amp=p.amp||15,per=p.period||26; return bandFill(F,t,(x,y)=>(y+2*Math.abs(wrap(x/per)-0.5)*amp)/sp,0.62);}; A.sinebands=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:0.9},p)); const sp=p.spacing||12,amp=p.amp||9,per=p.period||60; return bandFill(F,t,(x,y)=>(y+amp*Math.sin(x/per*2*Math.PI))/sp,0.62);}; A.warpbands=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:0.9},p)),dd=smoothDepth(F,6); const sp=p.spacing||11,w=p.warp||46; return bandFill(F,t,(x,y,i)=>(y+w*dd[i])/sp,0.62);}; A.strata=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:0.9},p)); const wn=whiteNoise(S,p.seed||1),warp=blur(wn,S,18); let mx=0; for(let i=0;i<S*S;i++) mx=Math.max(mx,Math.abs(warp[i]-0.5)); /* `levels` so mark reaches this method. markParams inverse-scales it (52 at mark 0.50 -> 11 at 2.25) and lands on exactly 26 at mark 1.0, which was the hardcoded default. Without this strata renders IDENTICALLY at every mark — it reads only layers/jitter, neither of which markParams supplies. */ const layers=p.layers||p.levels||26,j=p.jitter||6; return bandFill(F,t,(x,y,i)=>(y+j*((warp[i]-0.5)/Math.max(mx,1e-9))*3)/(S/layers),0.60);}; A.domainwarp=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:0.9},p)); const n1=blur(whiteNoise(S,(p.seed||1)),S,14),n2=blur(whiteNoise(S,(p.seed||1)+77),S,7); let m1=0,m2=0; for(let i=0;i<S*S;i++){m1=Math.max(m1,Math.abs(n1[i]-0.5));m2=Math.max(m2,Math.abs(n2[i]-0.5));} const sp=p.spacing||10,w=p.warp||26; return bandFill(F,t,(x,y,i)=>(y+w*((n1[i]-0.5)/Math.max(m1,1e-9))+w*0.45*((n2[i]-0.5)/Math.max(m2,1e-9)))/sp,0.62);}; A.worley=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:0.9},p)); const pts=pickN(F.mask,S,p.sites||420,p.seed||1); const d=nearestDist(pts,S); let mx=0; for(let i=0;i<S*S;i++) if(F.mask[i]&&d[i]>mx)mx=d[i]; const b=p.bands||9; return bandFill(F,t,(x,y,i)=>d[i]/Math.max(mx,1e-9)*b,0.55);}; A.offsets=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:0.9},p)),o=new Float64Array(S*S); const din=edt(F.mask,S),step=p.step||7; for(let i=0;i<S*S;i++){if(!F.mask[i])continue; if(Math.abs(wrap(din[i]/step)-0.5)<0.16+0.30*t[i])o[i]=1;} return o;}; A.interference=(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=Math.max(S*0.5,1),rings=p.rings||26,off=(p.sep||0.30)*R; 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=Math.abs(wrap(Math.hypot(x-(cx-off),y-cy)/R*rings)-0.5)<Math.min(t[i]*0.55,0.5); const b=Math.abs(wrap(Math.hypot(x-(cx+off),y-cy)/R*rings)-0.5)<Math.min(t[i]*0.55,0.5); if(a!==b)o[i]=1;} return o;}; /* ---- isoline family ---- */ function isoCore(F,p,levels,widthFn){ const S=F.size,f=smoothDepth(F,p.smooth===undefined?2:p.smooth),o=new Float64Array(S*S); const gx=new Float64Array(S*S),gy=new Float64Array(S*S); for(let y=0;y<S;y++)for(let x=0;x<S;x++){const i=y*S+x; gx[i]=(f[i+(x+1<S?1:0)]-f[i-(x>0?1:0)])/2; gy[i]=(f[i+(y+1<S?S:0)]-f[i-(y>0?S:0)])/2;} for(let L=1;L<levels;L++){const lv=L/levels; for(let i=0;i<S*S;i++){ if(!F.mask[i]||o[i])continue; const g=Math.hypot(gx[i],gy[i])+1e-6; if(Math.abs(f[i]-lv)/g<widthFn(L,i))o[i]=1;}} return o; } A.isolines=(F,p)=>{const lw=p.lw||1.1,ie=p.index_every||5; return isoCore(F,p,p.levels||26,(L)=>lw*(L%ie===0?1.9:1.0));}; A.contourhatch=(F,p)=>{const t=tone(F,Object.assign({gamma:0.9},p)); return isoCore(F,Object.assign({smooth:2.5},p),p.levels||44,(L,i)=>0.35+1.5*t[i]);}; A.terraces=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:0.9},p)),dd=smoothDepth(F,3); const b=p.bands||13,bn=blueNoise(S,(p.seed||1)+3,1.6),o=new Float64Array(S*S); const q=new Int32Array(S*S); for(let i=0;i<S*S;i++) q[i]=cl(Math.floor(dd[i]*b),0,b-1); for(let i=0;i<S*S;i++){ if(!F.mask[i])continue; if(q[i]%2===0 && bn[i]<t[i]*1.25)o[i]=1;} for(let y=0;y<S;y++)for(let x=0;x<S;x++){const i=y*S+x;if(!F.mask[i])continue; if((x+1<S&&q[i]!==q[i+1])||(y+1<S&&q[i]!==q[i+S]))o[i]=1;} return o;}; A.ribbons=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:0.9},p)),dd=smoothDepth(F,2.5); const b=p.bands||17,o=new Float64Array(S*S); for(let i=0;i<S*S;i++){ if(!F.mask[i])continue; const q=cl(Math.floor(dd[i]*b),0,b-1),fr=wrap(dd[i]*b),w=cl(t[i]*0.95,0.05,0.95); if((q%2===0&&fr<w)||(q%2===1&&fr>1-w*0.45))o[i]=1;} return o;}; A.layercut=(F,p)=>{const S=F.size,dd=smoothDepth(F,2),o=new Float64Array(S*S); /* Same inert-method fix as strata: layercut read only layers/off, so mark did nothing to it. Its default 13 is half of markParams' `levels` at mark 1.0 (26), so halving preserves the existing look at mark 1.0 while letting mark drive layer count either way. */ const layers=p.layers||(p.levels?Math.max(Math.round(p.levels/2),3):13),off=p.off||3; for(let L=0;L<layers;L++){const lv=L/layers,k=Math.round(L*off); for(let y=0;y<S;y++)for(let x=0;x<S;x++){const i=y*S+x; if(!(F.mask[i]&&dd[i]>lv))continue; // boundary of this slice const b=(x===0||y===0||x===S-1||y===S-1)|| !(F.mask[i-1]&&dd[i-1]>lv)||!(F.mask[i+1]&&dd[i+1]>lv)|| !(F.mask[i-S]&&dd[i-S]>lv)||!(F.mask[i+S]&&dd[i+S]>lv); if(!b)continue; const ty=y+k; if(ty<S && F.mask[ty*S+x]) o[ty*S+x]=1;}} return o;}; A.hachure=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:0.9},p)),f=smoothDepth(F,3); const o=new Float64Array(S*S),levels=p.levels||22,tick=p.tick||5; 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]=(f[i+1]-f[i-1])/2; gy[i]=(f[i+S]-f[i-S])/2;} for(let L=1;L<levels;L++){const lv=L/levels; 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])continue; const g=Math.hypot(gx[i],gy[i])+1e-6; if(Math.abs(f[i]-lv)/g<0.9){ o[i]=1; if(((x+y)%tick)===0 && t[i]>0.3){ for(let s=1;s<4;s++){const ty=cl(Math.round(y+gy[i]/g*s),0,S-1),tx=cl(Math.round(x+gx[i]/g*s),0,S-1); if(F.mask[ty*S+tx])o[ty*S+tx]=1;}}}}} return o;}; A.ridgeline=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:0.9},p)),o=new Float64Array(S*S); const rows=p.rows||44,amp=p.amp||17,prof=blur(t,S,2),step=Math.max((S/rows)|0,3); for(let y0=0;y0<S;y0+=step)for(let x=0;x<S;x++){ if(!F.mask[y0*S+x])continue; const yy=Math.round(y0-amp*prof[y0*S+x]); if(yy>=0&&yy<S)o[yy*S+x]=1; if(x%3===0){const a=cl(Math.min(yy,y0),0,S-1); for(let q=a;q<=y0;q++)o[q*S+x]=1;}} return o;}; A.seismo=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:0.9},p)),o=new Float64Array(S*S); const rows=p.rows||40,amp=p.amp||13,fr=p.freq||0.55,step=Math.max((S/rows)|0,3); for(let y0=0;y0<S;y0+=step)for(let x=0;x<S;x++){ if(!F.mask[y0*S+x])continue; const yy=cl(Math.round(y0+amp*t[y0*S+x]*Math.sin(x*fr+y0*0.4)),0,S-1); o[yy*S+x]=1; if(yy+1<S)o[(yy+1)*S+x]=1;} return o;}; /* ---- helpers shared with part 3 ---- */ function pickN(mask,S,n,seed){ const idx=[]; for(let i=0;i<S*S;i++) if(mask[i])idx.push(i); 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){ const k=(r()*idx.length)|0; if(used.has(k))continue; used.add(k); out.push(idx[k]);} return out; } function nearestDist(pts,S){ const d=new Float64Array(S*S).fill(1e9),q=[]; for(const i of pts){d[i]=0;q.push(i);} for(let h=0;h<q.length;h++){const i=q[h],y=(i/S)|0,x=i%S; for(let dy=-1;dy<=1;dy++)for(let dx=-1;dx<=1;dx++){ if(!dx&&!dy)continue; const yy=y+dy,xx=x+dx; if(yy<0||yy>=S||xx<0||xx>=S)continue; const j=yy*S+xx,nd=d[i]+((dx&&dy)?1.41421356:1); if(nd<d[j]-1e-9){d[j]=nd;q.push(j);}}} return d; } Object.assign(KYSU,{cellMean,bandFill,pickN,nearestDist,smoothDepth,wrap,cl}); })(); /* --- algos3.js --- */ (function(){ /* KYS algorithms part 3 — dither/halftone/tiling/engraving/misc. */ 'use strict'; const {A,shade,tone,blur,blueNoise,whiteNoise,rng32,centroid,edt,discFit}=KYSU; const {cellMean,pickN,nearestDist,smoothDepth,wrap,cl}=KYSU; const GLYPH_FONT=root.GLYPH_FONT, GLYPH_RAMP=root.GLYPH_RAMPOF; /* ---------- halftone / dither ---------- */ A.fm_halftone=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:1.1},p)),o=new Float64Array(S*S); /* FM screening: fixed dot SIZE, variable dot POSITION. The seed count must be divided by the dot AREA or the dilated blocks overlap and fill solid — at dot=3 the old threshold produced 100% ink. */ const dot=Math.max(p.dot||2,1); /* blue noise is spatially separated, so dilated dots overlap only partially. sqrt(area) tracks the observed coverage far better than area. */ const norm=Math.sqrt(dot*dot); const bn=blueNoise(S,p.seed||1,Math.max(3.0*dot*0.6,2.0)); const hit=new Uint8Array(S*S); for(let i=0;i<S*S;i++) if(F.mask[i] && t[i]*1.15/norm > bn[i]) hit[i]=1; for(let y=0;y<S;y++)for(let x=0;x<S;x++){ if(!hit[y*S+x])continue; for(let dy=0;dy<dot;dy++)for(let dx=0;dx<dot;dx++){ const yy=y+dy,xx=x+dx; if(yy<S&&xx<S&&F.mask[yy*S+xx])o[yy*S+xx]=1;}} return o;}; A.rosette=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:0.95},p)),o=new Float64Array(S*S); const c=p.cell||6.5; for(const deg of [15,45,75]){const a=deg*Math.PI/180,ca=Math.cos(a),sa=Math.sin(a); for(let y=0;y<S;y++)for(let x=0;x<S;x++){const i=y*S+x;if(!F.mask[i]||o[i])continue; const u=(x*ca+y*sa)/c,v=(-x*sa+y*ca)/c; /* radius tracks tone LINEARLY. sqrt() compressed the low end so hard that halving the tone shrank the dot only 29% — the rings merged into a solid mass and white/gamma barely moved coverage (48%->19% over the whole range, vs 26%->10% now). */ if(Math.hypot(wrap(u)-0.5,wrap(v)-0.5)<0.52*Math.max(t[i],0))o[i]=1;}} return o;}; A.posterize=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:0.9},p)); const steps=p.steps||6,bn=blueNoise(S,(p.seed||1)+1,1.4); const b=Math.max(Math.round((p.ms||1)*2),1); if(b<=1){const o=new Float64Array(S*S); for(let i=0;i<S*S;i++){if(!F.mask[i])continue; const q=cl(Math.floor(t[i]*steps),0,steps-1); if(bn[i]<q/(steps-1))o[i]=1;} return o;} const gw=Math.ceil(S/b),acc=new Float64Array(gw*gw),cnt=new Float64Array(gw*gw); const nacc=new Float64Array(gw*gw); 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 g=((y/b)|0)*gw+((x/b)|0);acc[g]+=t[i];nacc[g]+=bn[i];cnt[g]++;} const hit=new Uint8Array(gw*gw); for(let g=0;g<hit.length;g++){if(!cnt[g])continue; const tv=acc[g]/cnt[g],nv=nacc[g]/cnt[g]; const q=cl(Math.floor(tv*steps),0,steps-1); if(nv<q/(steps-1))hit[g]=1;} const 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])continue; if(hit[((y/b)|0)*gw+((x/b)|0)])o[i]=1;} return o;}; A.dotmatrix=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:0.72,white:0.04},p)); const c=p.cell||5,{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=(x%c)-c/2,v=(y%c)-c/2,lv=cl(Math.round(tb[i]*5)/5*1.35,0,1); if(Math.hypot(u,v)<0.62*c*lv)o[i]=1;} return o;}; A.diamond=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:0.92},p)); const c=p.cell||8,{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; if(Math.abs(wrap(x/c)-0.5)+Math.abs(wrap(y/c)-0.5)<0.62*cl(tb[i],0,1))o[i]=1;} return o;}; A.circlepack=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:0.85},p)); const c=Math.max(Math.round(p.cell||11),2),{tb,den}=cellMean(F,t,c),o=new Float64Array(S*S); /* one fit test per CELL (not per pixel): drop a dot only if the disc at the cell centre would be substantially outside the silhouette. */ for(let gy=0;gy+c<=S;gy+=c)for(let gx=0;gx+c<=S;gx+=c){ const cxp=gx+(c>>1), cyp=gy+(c>>1), ci=cyp*S+cxp; if(!F.mask[ci]||den[ci]<=0.5) continue; const RR=0.42*c*Math.pow(cl(tb[ci],0,1),0.62)+0.55; // floor keeps lit areas alive if(RR<=0.55) continue; if(discFit(F.mask,S,cxp,cyp,RR)<0.55) continue; for(let y=gy;y<gy+c;y++)for(let x=gx;x<gx+c;x++){ const i=y*S+x; if(!F.mask[i]) continue; if(Math.hypot(x-cxp,y-cyp)<RR) o[i]=1;}} return o;}; A.bubbles=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:0.88},p)); const c=Math.max(Math.round(p.cell||9),2),{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 cxp=gx+(c>>1), cyp=gy+(c>>1), ci=cyp*S+cxp; if(!F.mask[ci]||den[ci]<=0.5) continue; const R=0.40*c*Math.pow(cl(tb[ci]*1.25,0,1),0.62)+0.9; if(R<=0.9) continue; if(discFit(F.mask,S,cxp,cyp,R+1)<0.55) continue; for(let y=gy-1;y<gy+c+1;y++)for(let x=gx-1;x<gx+c+1;x++){ if(y<0||y>=S||x<0||x>=S) continue; const i=y*S+x; if(!F.mask[i]) continue; if(Math.abs(Math.hypot(x-cxp,y-cyp)-R)<0.9) o[i]=1;}} return o;}; A.morphcell=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:0.88},p)); const c=p.cell||9,{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 u=(wrap(x/c)-0.5)*2,v=(wrap(y/c)-0.5)*2,g=cl(tb[i],0,1),pw=1+3*g; const r=Math.pow(Math.pow(Math.abs(u),pw)+Math.pow(Math.abs(v),pw),1/Math.max(pw,1e-3)); if(r<0.95*g)o[i]=1;} return o;}; A.hexgrid=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:0.9},p)); const c=Math.max(Math.round(p.cell||11),2),{tb,den}=cellMean(F,t,c),o=new Float64Array(S*S); const rh=c*0.866; for(let row=0; row*rh<S; row++){ const yc=Math.round(row*rh+rh/2), xo=(row%2===0)?0:c/2; if(yc>=S) break; for(let k=-1; k*c+xo<S+c; k++){ const xc=Math.round(k*c+xo+c/2); if(xc<0||xc>=S) continue; const ci=yc*S+xc; if(!F.mask[ci]||den[ci]<=0.5) continue; const RH=0.45*c*Math.pow(cl(tb[ci],0,1),0.62)+0.55; if(RH<=0.55) continue; if(discFit(F.mask,S,xc,yc,RH)<0.55) continue; const r=Math.ceil(RH); for(let dy=-r;dy<=r;dy++)for(let dx=-r;dx<=r;dx++){ const y=yc+dy,x=xc+dx; if(y<0||y>=S||x<0||x>=S) continue; const i=y*S+x; if(!F.mask[i]) continue; if(dx*dx+dy*dy<RH*RH) o[i]=1;}}} return o;}; A.perforation=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:0.80,white:0.05},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 g=cl(tb[i]*1.35,0,1); if(g<=0.10)continue; if(Math.abs(wrap(x/c)-0.5)<0.34&&Math.abs(wrap(y/c)-0.5)<0.16+0.28*g)o[i]=1;} return o;}; A.slicescreen=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:0.9},p)),dd=smoothDepth(F,4); const c=p.cell||7,sl=p.slices||5,o=new Float64Array(S*S); for(let k=0;k<sl;k++){const lo=k/sl,hi=(k+1)/sl,a=(12+k*31)*Math.PI/180,ca=Math.cos(a),sa=Math.sin(a); for(let y=0;y<S;y++)for(let x=0;x<S;x++){const i=y*S+x; if(!F.mask[i]||dd[i]<lo||dd[i]>=hi)continue; const u=(x*ca+y*sa)/c,v=(-x*sa+y*ca)/c; if(Math.hypot(wrap(u)-0.5,wrap(v)-0.5)<0.60*Math.sqrt(Math.max(t[i],0)))o[i]=1;}} return o;}; A.phyllotaxis=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:0.9},p)),o=new Float64Array(S*S); /* mark scale: bigger dots need TIGHTER spiral pitch so the head stays covered */ const ms=p.ms||1; const [cx,cy]=centroid(F.mask,S),ga=Math.PI*(3-Math.sqrt(5)); const n=Math.round(20000/ms),sc=2.6*Math.sqrt(ms); for(let i=1;i<=n;i++){const r=sc*Math.sqrt(i),th=i*ga; const x=Math.round(cx+r*Math.cos(th)),y=Math.round(cy+r*Math.sin(th)); if(x<0||x>=S||y<0||y>=S)continue; const j=y*S+x; if(!F.mask[j])continue; /* CONTINUOUS radius. The old 4-step staircase (>0.85->3, >0.62->2, >0.30->1, else 0) meant every tone from 0.30 to 0.62 drew an identical dot, so white/gamma did nothing until a value crossed a step. t^1.5*3 tracks that staircase closely but smoothly — a LINEAR t*3.2 is wrong because dot area goes as r^2, so it reaches radius 1 at t=0.16 where the staircase waited for 0.30, and ADDS ink instead of removing it. */ const rad=Math.max(0,Math.round(Math.pow(Math.max(t[j],0),1.5)*3*ms)); for(let dy=-rad;dy<=rad;dy++)for(let dx=-rad;dx<=rad;dx++){ if(dx*dx+dy*dy>rad*rad+0.5)continue; const yy=cl(y+dy,0,S-1),xx=cl(x+dx,0,S-1); if(F.mask[yy*S+xx])o[yy*S+xx]=1;}} return o;}; A.spiraldots=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:0.88},p)),o=new Float64Array(S*S); const ms=p.ms||1; const [cx,cy]=centroid(F.mask,S); /* reach must cover the furthest masked pixel from the centre, not a fixed radius — otherwise the spiral is a small disc in the middle of the frame. */ let maxr=0; for(let y=0;y<S;y++)for(let x=0;x<S;x++) if(F.mask[y*S+x]){ const d=Math.hypot(x-cx,y-cy); if(d>maxr)maxr=d;} maxr+=2; const pitch=Math.max((p.pitch||3.1),0.8); // radial gap between windings const turns=maxr/pitch; // derived, not fixed /* angular step: keep dot spacing along the arc roughly constant */ for(let th=0.0001; ; ){ const r=pitch*th/(2*Math.PI); if(r>maxr)break; const x=Math.round(cx+r*Math.cos(th)),y=Math.round(cy+r*Math.sin(th)); th += Math.min(Math.max(pitch*0.55/Math.max(r,1),0.004),0.35); if(x<0||x>=S||y<0||y>=S)continue; const j=y*S+x; if(!F.mask[j])continue; const tv=t[j],base=tv>0.66?2:tv>0.30?1:0; const rad=Math.max(0,Math.round(base*ms)); if(rad===0){o[j]=1;continue;} for(let dy=-rad;dy<=rad;dy++)for(let dx=-rad;dx<=rad;dx++){ if(dx*dx+dy*dy>rad*rad+0.5)continue; const yy=cl(y+dy,0,S-1),xx=cl(x+dx,0,S-1); if(F.mask[yy*S+xx])o[yy*S+xx]=1;}} return o;}; /* ---------- engraving / hatching ---------- */ A.crosshatch=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:0.95},p)),o=new Float64Array(S*S); const sp=p.spacing||6,layers=p.layers||4,degs=[28,-52,74,-8]; for(let k=0;k<layers;k++){const a=degs[k]*Math.PI/180,ca=Math.cos(a),sa=Math.sin(a); const need=(k+0.55)/(layers+0.25); for(let y=0;y<S;y++)for(let x=0;x<S;x++){const i=y*S+x;if(!F.mask[i]||o[i]||t[i]<=need)continue; if(Math.abs(wrap((x*ca+y*sa)/sp)-0.5)<0.165)o[i]=1;}} return o;}; A.engraving=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:0.9},p)),o=new Float64Array(S*S); const sp=p.spacing||6,a=22*Math.PI/180,ca=Math.cos(a),sa=Math.sin(a); const bn=blueNoise(S,p.seed||1,2.0); 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 tv=t[i]; const line=tv>0.16&&tv<0.72&&Math.abs(wrap((x*ca+y*sa)/sp)-0.5)<0.16*(0.4+1.4*tv); const dots=tv>0.72&&tv*1.4>bn[i]; if(line||dots||tv>0.93)o[i]=1;} return o;}; A.scribble=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:1.0},p)),o=new Float64Array(S*S); const pts=pickWeighted(F.mask,t,S,p.lines||2600,p.seed||1),r=rng32((p.seed||1)+5); const base=28*Math.PI/180; for(const j of pts){const y0=(j/S)|0,x0=j%S,tv=t[j],L=4+12*tv; const a=base+(r()*2-1)*0.28; for(let q=0;q<L;q++){const x=Math.round(x0+q*Math.cos(a)),y=Math.round(y0+q*Math.sin(a)); if(x<0||x>=S||y<0||y>=S)break; if(F.mask[y*S+x])o[y*S+x]=1;}} return o;}; A.dashes=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:0.72,white:0.04},p)),o=new Float64Array(S*S); const ms=p.ms||1, sp=Math.max(Math.round(5*ms),2), dash=9*ms, thick=Math.max(Math.round(2*ms),1); for(let y=0;y<S;y++){ if(y%sp>=thick)continue; for(let x=0;x<S;x++){const i=y*S+x;if(!F.mask[i])continue; if(wrap(x/dash)<cl(t[i]*1.45,0,1))o[i]=1;}} return o;}; A.randomwalk=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:1.0},p)); const walkers=p.walkers||2600,steps=p.steps||110,r=rng32(p.seed||1); const g=blur(t,S,3),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])/2; gy[i]=(g[i+S]-g[i-S])/2;} const start=pickN(F.mask,S,walkers,p.seed||1); const acc=new Float64Array(S*S); const px=[],py=[]; for(const j of start){py.push((j/S)|0);px.push(j%S);} for(let s=0;s<steps;s++){ for(let k=0;k<px.length;k++){ const iy=cl(Math.round(py[k]),0,S-1),ix=cl(Math.round(px[k]),0,S-1),i=iy*S+ix; py[k]=cl(py[k]+1.6*gy[i]*40+(r()*2-1)*1.5,0,S-1); px[k]=cl(px[k]+1.6*gx[i]*40+(r()*2-1)*1.5,0,S-1); const j=cl(Math.round(py[k]),0,S-1)*S+cl(Math.round(px[k]),0,S-1); if(F.mask[j])acc[j]++;}} let mx=0; for(let i=0;i<S*S;i++){acc[i]=Math.log1p(acc[i]); if(acc[i]>mx)mx=acc[i];} const o=new Float64Array(S*S); for(let i=0;i<S*S;i++) if(F.mask[i]&&acc[i]/Math.max(mx,1e-9)>0.30)o[i]=1; return o;}; A.splatter=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:1.2},p)),o=new Float64Array(S*S); const pts=pickWeighted(F.mask,t,S,p.blots||1500,p.seed||1),r=rng32((p.seed||1)+9); for(const j of pts){const cy=(j/S)|0,cx=j%S,rad=Math.round(1+4*t[j]*r()); if(rad<1)continue; for(let dy=-rad;dy<=rad;dy++)for(let dx=-rad;dx<=rad;dx++){ if(dx*dx+dy*dy>rad*rad)continue; const yy=cy+dy,xx=cx+dx; if(yy<0||yy>=S||xx<0||xx>=S)continue; if(F.mask[yy*S+xx])o[yy*S+xx]=1;}} return o;}; A.quiver=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:0.72,white:0.04},p)),o=new Float64Array(S*S); const ms=p.ms||1; const f=smoothDepth(F,5),step=Math.max(Math.round(5*Math.sqrt(ms)),3); 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]=(f[i+1]-f[i-1])/2; gy[i]=(f[i+S]-f[i-S])/2;} for(let y=0;y<S;y+=step)for(let x=0;x<S;x+=step){const i=y*S+x;if(!F.mask[i])continue; const g=Math.hypot(gx[i],gy[i])+1e-9,L=Math.round((2+9*cl(t[i]*1.4,0,1))*ms); for(let k=0;k<L&&k<Math.round(12*ms);k++){ const ty=cl(Math.round(y+gy[i]/g*k),0,S-1),tx=cl(Math.round(x+gx[i]/g*k),0,S-1); if(F.mask[ty*S+tx])o[ty*S+tx]=1; const t2=cl(ty+1,0,S-1); if(F.mask[t2*S+tx])o[t2*S+tx]=1;}} return o;}; A.rain=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:0.70,white:0.04},p)),o=new Float64Array(S*S); const cols=p.cols||3; for(let x=0;x<S;x+=cols){ let y0=-1,y1=-1; for(let y=0;y<S;y++) if(F.mask[y*S+x]){if(y0<0)y0=y;y1=y;} if(y0<0)continue; let y=y0; while(y<y1){const v=cl(t[y*S+x]*1.45,0,1),L=Math.round(3+20*v),gap=Math.max(Math.round(1+5*(1-v)),1); for(let q=y;q<Math.min(y+L,S);q++) if(F.mask[q*S+x])o[q*S+x]=1; y+=L+gap;}} return o;}; A.bars=(F,p)=>{const S=F.size,t=tone(F,Object.assign({gamma:0.9},p)),o=new Float64Array(S*S); const cols=Math.max(p.cols||9,3); for(let x0=0;x0<S;x0+=cols){const x1=Math.min(x0+cols-2,S); if