0xee514bd0…b86dsent to0x062a6f33…3dec·#25,857,079·view on Etherscan
/* 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