0x062a…3dec

All memos sent from and to 0x062a…3dec.

/* KYS engine — generated by build_bundle.js. Do not edit. */ (function(root){ 'use strict'; /* keccak256 — minimal, dependency-free, browser + node. * Replaces js-sha3 (20.8KB) for the one function we need. * Must agree byte-for-byte with Solidity's keccak256. */ (function (root) { 'use strict'; var RC = [ 0x00000001,0x00000000, 0x00008082,0x00000000, 0x0000808a,0x80000000, 0x80008000,0x80000000, 0x0000808b,0x00000000, 0x80000001,0x00000000, 0x80008081,0x80000000, 0x00008009,0x80000000, 0x0000008a,0x00000000, 0x00000088,0x00000000, 0x80008009,0x00000000, 0x8000000a,0x00000000, 0x8000808b,0x00000000, 0x0000008b,0x80000000, 0x00008089,0x80000000, 0x00008003,0x80000000, 0x00008002,0x80000000, 0x00000080,0x80000000, 0x0000800a,0x00000000, 0x8000000a,0x80000000, 0x80008081,0x80000000, 0x00008080,0x80000000, 0x80000001,0x00000000, 0x80008008,0x80000000 ]; function keccakf(s) { var n, i, j, c0,c1,c2,c3,c4,c5,c6,c7,c8,c9, h, l; var b = new Array(50); for (n = 0; n < 24; n++) { c0 = s[0]^s[10]^s[20]^s[30]^s[40]; c1 = s[1]^s[11]^s[21]^s[31]^s[41]; c2 = s[2]^s[12]^s[22]^s[32]^s[42]; c3 = s[3]^s[13]^s[23]^s[33]^s[43]; c4 = s[4]^s[14]^s[24]^s[34]^s[44]; c5 = s[5]^s[15]^s[25]^s[35]^s[45]; c6 = s[6]^s[16]^s[26]^s[36]^s[46]; c7 = s[7]^s[17]^s[27]^s[37]^s[47]; c8 = s[8]^s[18]^s[28]^s[38]^s[48]; c9 = s[9]^s[19]^s[29]^s[39]^s[49]; h = c8 ^ ((c2 << 1) | (c3 >>> 31)); l = c9 ^ ((c3 << 1) | (c2 >>> 31)); for (i = 0; i < 50; i += 10) { s[i] ^= h; s[i+1] ^= l; } h = c0 ^ ((c4 << 1) | (c5 >>> 31)); l = c1 ^ ((c5 << 1) | (c4 >>> 31)); for (i = 0; i < 50; i += 10) { s[i+2] ^= h; s[i+3] ^= l; } h = c2 ^ ((c6 << 1) | (c7 >>> 31)); l = c3 ^ ((c7 << 1) | (c6 >>> 31)); for (i = 0; i < 50; i += 10) { s[i+4] ^= h; s[i+5] ^= l; } h = c4 ^ ((c8 << 1) | (c9 >>> 31)); l = c5 ^ ((c9 << 1) | (c8 >>> 31)); for (i = 0; i < 50; i += 10) { s[i+6] ^= h; s[i+7] ^= l; } h = c6 ^ ((c0 << 1) | (c1 >>> 31)); l = c7 ^ ((c1 << 1) | (c0 >>> 31)); for (i = 0; i < 50; i += 10) { s[i+8] ^= h; s[i+9] ^= l; } b[0]=s[0]; b[1]=s[1]; b[32]=(s[11]<<4)|(s[10]>>>28); b[33]=(s[10]<<4)|(s[11]>>>28); b[14]=(s[20]<<3)|(s[21]>>>29); b[15]=(s[21]<<3)|(s[20]>>>29); b[46]=(s[31]<<9)|(s[30]>>>23); b[47]=(s[30]<<9)|(s[31]>>>23); b[28]=(s[40]<<18)|(s[41]>>>14); b[29]=(s[41]<<18)|(s[40]>>>14); b[20]=(s[2]<<1)|(s[3]>>>31); b[21]=(s[3]<<1)|(s[2]>>>31); b[2]=(s[13]<<12)|(s[12]>>>20); b[3]=(s[12]<<12)|(s[13]>>>20); b[34]=(s[22]<<10)|(s[23]>>>22); b[35]=(s[23]<<10)|(s[22]>>>22); b[16]=(s[33]<<13)|(s[32]>>>19); b[17]=(s[32]<<13)|(s[33]>>>19); b[48]=(s[42]<<2)|(s[43]>>>30); b[49]=(s[43]<<2)|(s[42]>>>30); b[40]=(s[5]<<30)|(s[4]>>>2); b[41]=(s[4]<<30)|(s[5]>>>2); b[22]=(s[14]<<6)|(s[15]>>>26); b[23]=(s[15]<<6)|(s[14]>>>26); b[4]=(s[25]<<11)|(s[24]>>>21); b[5]=(s[24]<<11)|(s[25]>>>21); b[36]=(s[34]<<15)|(s[35]>>>17); b[37]=(s[35]<<15)|(s[34]>>>17); b[18]=(s[45]<<29)|(s[44]>>>3); b[19]=(s[44]<<29)|(s[45]>>>3); b[10]=(s[6]<<28)|(s[7]>>>4); b[11]=(s[7]<<28)|(s[6]>>>4); b[42]=(s[17]<<23)|(s[16]>>>9); b[43]=(s[16]<<23)|(s[17]>>>9); b[24]=(s[26]<<25)|(s[27]>>>7); b[25]=(s[27]<<25)|(s[26]>>>7); b[6]=(s[36]<<21)|(s[37]>>>11); b[7]=(s[37]<<21)|(s[36]>>>11); b[38]=(s[47]<<24)|(s[46]>>>8); b[39]=(s[46]<<24)|(s[47]>>>8); b[30]=(s[8]<<27)|(s[9]>>>5); b[31]=(s[9]<<27)|(s[8]>>>5); b[12]=(s[18]<<20)|(s[19]>>>12); b[13]=(s[19]<<20)|(s[18]>>>12); b[44]=(s[29]<<7)|(s[28]>>>25); b[45]=(s[28]<<7)|(s[29]>>>25); b[26]=(s[38]<<8)|(s[39]>>>24); b[27]=(s[39]<<8)|(s[38]>>>24); b[8]=(s[48]<<14)|(s[49]>>>18); b[9]=(s[49]<<14)|(s[48]>>>18); for (i = 0; i < 50; i += 10) { for (j = 0; j < 10; j++) c0 = 0; s[i] = b[i] ^ (~b[i+2] & b[i+4]); s[i+1] = b[i+1] ^ (~b[i+3] & b[i+5]); s[i+2] = b[i+2] ^ (~b[i+4] & b[i+6]); s[i+3] = b[i+3] ^ (~b[i+5] & b[i+7]); s[i+4] = b[i+4] ^ (~b[i+6] & b[i+8]); s[i+5] = b[i+5] ^ (~b[i+7] & b[i+9]); s[i+6] = b[i+6] ^ (~b[i+8] & b[i]); s[i+7] = b[i+7] ^ (~b[i+9] & b[i+1]); s[i+8] = b[i+8] ^ (~b[i] & b[i+2]); s[i+9] = b[i+9] ^ (~b[i+1] & b[i+3]); } s[0] ^= RC[n*2]; s[1] ^= RC[n*2+1]; } } /* bytes (Uint8Array) -> 32-byte Uint8Array */ function keccak256(msg) { var s = new Int32Array(50), R = 136, i, n = msg.length, off = 0; while (n - off >= R) { absorb(s, msg, off, R); keccakf(s); off += R; } var tail = new Uint8Array(R); tail.set(msg.subarray(off)); tail[n - off] = 0x01; tail[R - 1] |= 0x80; absorb(s, tail, 0, R); keccakf(s); var out = new Uint8Array(32); for (i = 0; i < 32; i++) out[i] = (s[i >> 2] >>> (8 * (i & 3))) & 0xff; return out; } function absorb(s, buf, off, R) { for (var i = 0; i < R; i++) s[i >> 2] ^= buf[off + i] << (8 * (i & 3)); } function toHex(u8) { var h = ''; for (var i = 0; i < u8.length; i++) h += (u8[i] < 16 ? '0' : '') + u8[i].toString(16); return h; } function hexToBytes(h) { /* Accept a number or bigint as well as a hex string: the studio and the * curation tools identify variants by small integer seeds, and derive() * must treat those the same way it treats a 32-byte token seed. */ if (typeof h === 'number' || typeof h === 'bigint') { h = h.toString(16); if (h.length % 2) h = '0' + h; } h = String(h).replace(/^0x/, ''); if (h.length % 2) h = '0' + h; var u = new Uint8Array(h.length / 2); for (var i = 0; i < u.length; i++) u[i] = parseInt(h.substr(i * 2, 2), 16); return u; } var api = { keccak256: keccak256, toHex: toHex, hexToBytes: hexToBytes }; root.KECCAK = api; })(root); /* --- kys.js --- */ /* KYS on-chain renderer — model + substrate + algorithms. * Must produce bit-identical output to substrate_chain.py + roster.py. * No dependencies. Deterministic: no Math.random, no Date, no float drift * beyond IEEE-754 double which both JS and numpy float64 share. */ 'use strict'; /* ---------------------------------------------------------------- model */ function loadModel(buf) { const dv = new DataView(buf); if (dv.getUint8(0) !== 0x4b || dv.getUint8(1) !== 0x59 || dv.getUint8(2) !== 0x53 || dv.getUint8(3) !== 0x4d) throw new Error('bad magic'); const K = dv.getUint8(5); const nvert = dv.getUint32(6, true), ntri = dv.getUint32(10, true); const vscale = dv.getFloat32(14, true); const vmin = [dv.getFloat32(18, true), dv.getFloat32(22, true), dv.getFloat32(26, true)]; let o = 30; const vq = new Int16Array(buf, o, nvert * 3); o += nvert * 6; const tris = new Uint16Array(buf, o, ntri * 3); o += ntri * 6; const basis = f16Array(buf, o, K * nvert * 3); const V0 = new Float64Array(nvert * 3); for (let i = 0; i < nvert; i++) for (let j = 0; j < 3; j++) V0[i * 3 + j] = vq[i * 3 + j] * vscale + vmin[j]; return { K, nvert, ntri, V0, tris, basis }; } /* float16 decode — JS has no Float16Array in older engines */ function f16Array(buf, off, n) { const u = new Uint16Array(buf, off, n), out = new Float32Array(n); for (let i = 0; i < n; i++) out[i] = f16(u[i]); return out; } function f16(h) { const s = (h & 0x8000) >> 15, e = (h & 0x7c00) >> 10, f = h & 0x03ff; if (e === 0) return (s ? -1 : 1) * Math.pow(2, -14) * (f / 1024); if (e === 0x1f) return f ? NaN : (s ? -Infinity : Infinity); return (s ? -1 : 1) * Math.pow(2, e - 15) * (1 + f / 1024); } /* ------------------------------------------------------------ identity */ /* PCA-tapered identity draw — mirrors params.identity() */ function identity(seed, faceShape, dims) { const rng = gauss(seed); const sigma = 2.2 * Math.pow(clamp(faceShape, 0, 1), 1.3); const out = new Float64Array(dims); for (let k = 0; k < dims; k++) out[k] = rng() * (1 / (1 + k / 40)) * sigma; return out; } /* deterministic PRNG: splitmix64 -> uniform -> Box-Muller. * NOTE: this does NOT match numpy's PCG64. See determinism note in README. */ function splitmix64(seed) { let s = BigInt.asUintN(64, BigInt(seed)); return function () { s = BigInt.asUintN(64, s + 0x9e3779b97f4a7c15n); let z = s; z = BigInt.asUintN(64, (z ^ (z >> 30n)) * 0xbf58476d1ce4e5b9n); z = BigInt.asUintN(64, (z ^ (z >> 27n)) * 0x94d049bb133111ebn); z = z ^ (z >> 31n); return Number(z >> 11n) / 9007199254740992; // 53-bit uniform }; } function gauss(seed) { const u = splitmix64(seed); let spare = null; return function () { if (spare !== null) { const v = spare; spare = null; return v; } let a = u(), b = u(); if (a < 1e-300) a = 1e-300; const r = Math.sqrt(-2 * Math.log(a)), th = 2 * Math.PI * b; spare = r * Math.sin(th); return r * Math.cos(th); }; } const clamp = (v, a, b) => v < a ? a : v > b ? b : v; /* ---------------------------------------------------------------- verts */ function verts(M, ident) { /* Accumulate in float32 to match numpy's einsum on a float32 basis. * Math.fround() forces each step through single precision, which is what * makes this bit-identical to the Python reference. */ const n = M.nvert, N = n * 3; const acc = new Float32Array(N); for (let k = 0; k < M.K; k++) { const c = Math.fround(ident[k]); if (c === 0) continue; const off = k * N; for (let i = 0; i < N; i++) acc[i] = Math.fround(acc[i] + Math.fround(c * M.basis[off + i])); } const out = new Float64Array(N); for (let i = 0; i < N; i++) out[i] = M.V0[i] + acc[i]; return out; } function lookAt(V, yawDeg, pitchDeg) { const n = V.length / 3; let cx = 0, cy = 0, cz = 0; for (let i = 0; i < n; i++) { cx += V[i*3]; cy += V[i*3+1]; cz += V[i*3+2]; } cx /= n; cy /= n; cz /= n; const ry = yawDeg * Math.PI / 180, rx = pitchDeg * Math.PI / 180; const cy_ = Math.cos(ry), sy = Math.sin(ry), cp = Math.cos(rx), sp = Math.sin(rx); const out = new Float64Array(n * 3); for (let i = 0; i < n; i++) { const x = V[i*3] - cx, y = V[i*3+1] - cy, z = V[i*3+2] - cz; // Ry then Rx, matching numpy P @ Ry.T @ Rx.T const x1 = cy_*x + sy*z, y1 = y, z1 = -sy*x + cy_*z; out[i*3] = x1; out[i*3+1] = cp*y1 - sp*z1; out[i*3+2] = sp*y1 + cp*z1; } return out; } /* ----------------------------------------------------------- rasterize */ function rasterize(P, tris, size, persp, margin) { margin = margin === undefined ? 0.90 : margin; const nt = tris.length / 3, n = P.length / 3; // face normals const fn = new Float64Array(nt * 3); for (let t = 0; t < nt; t++) { const a = tris[t*3]*3, b = tris[t*3+1]*3, c = tris[t*3+2]*3; const ux = P[b]-P[a], uy = P[b+1]-P[a+1], uz = P[b+2]-P[a+2]; const vx = P[c]-P[a], vy = P[c+1]-P[a+1], vz = P[c+2]-P[a+2]; let nx = uy*vz-uz*vy, ny = uz*vx-ux*vz, nz = ux*vy-uy*vx; const L = Math.hypot(nx, ny, nz) || 1; fn[t*3] = nx/L; fn[t*3+1] = ny/L; fn[t*3+2] = nz/L; } // projection const Q = new Float64Array(P.length); Q.set(P); if (persp > 0) { let zmin = Infinity, zmax = -Infinity; for (let i = 0; i < n; i++) { const z = P[i*3+2]; if (z<zmin) zmin=z; if (z>zmax) zmax=z; } const span = zmax - zmin, d = span / Math.max(persp, 1e-6) * 3.0; for (let i = 0; i < n; i++) { const s = d / (d - P[i*3+2] + span); Q[i*3] = P[i*3]*s; Q[i*3+1] = P[i*3+1]*s; } } let lox=Infinity, loy=Infinity, hix=-Infinity, hiy=-Infinity; for (let i=0;i<n;i++){const x=Q[i*3],y=Q[i*3+1]; if(x<lox)lox=x; if(x>hix)hix=x; if(y<loy)loy=y; if(y>hiy)hiy=y;} const scale = margin * size / Math.max(hix-lox, hiy-loy); const ctrx = (lox+hix)/2, ctry = (loy+hiy)/2; const X = new Float64Array(n), Y = new Float64Array(n), Z = new Float64Array(n); for (let i=0;i<n;i++){ X[i] = (Q[i*3]-ctrx)*scale + size/2; Y[i] = size - ((Q[i*3+1]-ctry)*scale + size/2); Z[i] = Q[i*3+2]; } const zbuf = new Float64Array(size*size).fill(-Infinity); const nbuf = new Float64Array(size*size*3); const mask = new Uint8Array(size*size); // painter order: far -> near, matching the numpy reference const idx = [], area = new Float64Array(nt); for (let t=0;t<nt;t++){ const a=tris[t*3],b=tris[t*3+1],c=tris[t*3+2]; const ar=(X[b]-X[a])*(Y[c]-Y[a])-(X[c]-X[a])*(Y[b]-Y[a]); area[t]=ar; if(ar<0) idx.push(t); } idx.sort((p,q)=>{ const zp=Z[tris[p*3]]+Z[tris[p*3+1]]+Z[tris[p*3+2]]; const zq=Z[tris[q*3]]+Z[tris[q*3+1]]+Z[tris[q*3+2]]; return zq-zp; // descending, == numpy argsort(-z) }); for (const t of idx) { const a=tris[t*3],b=tris[t*3+1],c=tris[t*3+2]; const x0=X[a],x1=X[b],x2=X[c], y0=Y[a],y1=Y[b],y2=Y[c]; let xmin=Math.max(Math.floor(Math.min(x0,x1,x2)),0); let xmax=Math.min(Math.ceil(Math.max(x0,x1,x2)),size-1); let ymin=Math.max(Math.floor(Math.min(y0,y1,y2)),0); let ymax=Math.min(Math.ceil(Math.max(y0,y1,y2)),size-1); if(xmax<xmin||ymax<ymin) continue; const d=area[t]; for(let py=ymin;py<=ymax;py++){ const gy=py+0.5; for(let px=xmin;px<=xmax;px++){ const gx=px+0.5; const w0=((x1-x0)*(gy-y0)-(gx-x0)*(y1-y0))/d; const w1=((x2-x1)*(gy-y1)-(gx-x1)*(y2-y1))/d; const w2=1-w0-w1; if(w0<0||w1<0||w2<0) continue; const zz=w2*Z[a]+w0*Z[b]+w1*Z[c]; const o=py*size+px; if(zz>zbuf[o]){ zbuf[o]=zz; mask[o]=1; nbuf[o*3]=fn[t*3]; nbuf[o*3+1]=fn[t*3+1]; nbuf[o*3+2]=fn[t*3+2]; } } } } // normalise depth over the mask let zlo=Infinity, zhi=-Infinity; for(let i=0;i<size*size;i++) if(mask[i]){const z=zbuf[i]; if(z<zlo)zlo=z; if(z>zhi)zhi=z;} const depth=new Float64Array(size*size); const rng2=Math.max(zhi-zlo,1e-9); for(let i=0;i<size*size;i++) if(mask[i]) depth[i]=(zbuf[i]-zlo)/rng2; return {mask, depth, normal:nbuf, size}; } var KYS={loadModel:loadModel,identity:identity,verts:verts,lookAt:lookAt,rasterize:rasterize,splitmix64:splitmix64,gauss:gauss,clamp:clamp,f16:f16}; /* --- marks.js --- */ /* Canonical mark-scale mapping. Shared by node tools AND the browser studio. * * Rules learned the hard way: * - cell/grid parameters MUST be integers (cellMean's integral image collapses * on fractional box sizes -> 0% ink) * - counts scale by 1/sc^2 (area), spacings by sc (length) * - every value is clamped so no algorithm degenerates at the extremes */ /* Each method's NATIVE pitch, as originally designed. markParams scales these * rather than handing every method the same number — a shared `spacing` blew * out methods designed tight (crosshatch wants 6, not 14). */ const BASE = { crosshatch:6, engraving:6, dashes:5, vertical:7, moire:7, woodcut:9, domainwarp:10, chevron:11, warpbands:11, diagonal:12, sinebands:12, hachure:9, quiver:7, rain:3, bars:9, seismo:13, ridgeline:13, }; function pitchFor(name, sc){ const b = BASE[name]; return b === undefined ? undefined : Math.max(b*sc, 1.5); } function markParams(sc){ const I=(v,lo,hi)=>Math.max(lo,Math.min(hi,Math.round(v))); // integer, clamped const Fl=(v,lo,hi)=>Math.max(lo,Math.min(hi,v)); // float, clamped return { // --- integer cell / grid sizes (MUST be whole numbers) --- cell: I(7*sc, 2, 40), cw: I(14*sc, 3, 60), chh: I(7*sc, 2, 30), mod: I(8*sc, 2, 40), step: I(7*sc, 2, 30), minc: I(4*sc, 2, 24), dot: I(2*sc, 1, 10), band: I(13*sc, 3, 50), // --- float spacings (fractional is fine) --- spacing: Fl(14*sc, 2, 60), sp: Fl(7*sc, 1.5, 30), dash: Fl(9*sc, 2, 40), pitch: Fl(3.1*sc, 0.8, 14), // --- inverse: fewer/larger as scale rises --- rings: Fl(30/sc, 4, 90), turns: Fl(34/sc, 4, 100), bands: Fl(17/sc, 3, 50), rows: I(60/sc, 6, 140), levels: I(26/sc, 4, 60), count: I(150/sc, 12, 400), petals: I(20/sc, 3, 60), sides: I(6, 3, 12), // --- counts scale by area --- sites: I(1500/(sc*sc), 40, 4000), lines: I(2600/(sc*sc), 60, 6000), blots: I(1500/(sc*sc), 40, 4000), walkers: I(2600/(sc*sc), 80, 6000), curves: I(150/(sc*sc), 10, 400), // --- density-style (need explicit handling) --- density: Fl(0.42*sc*sc*0.55, 0.06, 1.0), grain: Fl(0.55, 0.2, 1.6), cols: I(3*sc, 2, 20), }; } /* --- render.js --- */ /* Canonical render entry point. * * RESOLUTION AND MARK SCALE ARE COUPLED. A saved composition is defined at a * reference size (REF). Rendering at any other size multiplies the mark scale * by size/REF, so marks keep the same PROPORTION of the head — the image looks * the same at thumbnail size, but has more pixels describing each mark when * you zoom in. * * Without this, doubling the canvas halves the apparent mark size and you get * a different picture, not a sharper one. */ 'use strict'; const REF=300; // the size mark scales were tuned against function paramsFor(v, size, name){ const k = size/REF; // resolution multiplier /* `mark` and `scale` are the same axis under two names: derive() emits * `mark`, the studio and canonical.json store `scale`. Accept BOTH. * * They were previously not reconciled here, so every seed-derived token — * batches, simulate, render_onchain and the on-chain entry point — silently * rendered at mark 1.0 no matter what its seed said, while the studio (which * stores `scale`) honoured it. That is why studio output and token output * diverged, and why sweeping `mark` appeared to do nothing. */ const ms = (v.scale !== undefined ? v.scale : v.mark !== undefined ? v.mark : 1) * k; // marks grow with the canvas const p = markParams(ms); /* markParams divides counts by ms^2 so that BIGGER MARKS mean FEWER of them. * But when ms grew only because the canvas grew, the count must be restored: * the canvas has k^2 more pixels, so multiply the area-based counts back by * k^2. Otherwise a 1080px render has 13x fewer voronoi sites than a 300px * one and turns into a different (much emptier) picture. */ const a = k*k; for (const key of ['sites','lines','blots','walkers','curves']) { if (p[key] !== undefined) p[key] = Math.round(p[key]*a); } /* stipple/phyllotaxis/spiraldots use ms directly for dot radius; their * density must likewise not thin out with resolution. */ if (p.density !== undefined) p.density = Math.min(p.density*a, 1); /* isoline-family: level count must not fall with resolution, or contours * spread apart and the image empties out. */ for (const key of ['levels','rows','count','petals']) { if (p[key] !== undefined) p[key] = Math.max(Math.round(p[key]*k), 3); } /* per-method native pitch overrides the shared spacing */ const pf = name ? pitchFor(name, ms) : undefined; if (pf !== undefined) { p.spacing = pf; p.sp = pf; } return Object.assign(p, { ms, light_dir:v.light_dir, light_elev:v.light_elev, soft:v.soft, white:v.white, gamma:v.gamma, seed:v.seed, flowseeds: Math.round(4200*a), }); } /* Line-drawing methods hardcode ~1px stroke widths. When the canvas grows, * those strokes stay 1px and the image thins out. Rather than thread a line * width through 80+ algorithms, dilate the result by the resolution factor — * a 1px line at 300px becomes a 2px line at 600px, which is what "same picture, * more pixels" actually means. Only applies to methods that draw thin strokes; * area-filling methods (halftone, dither) are already invariant. */ /* Derived EMPIRICALLY: these are the methods whose coverage tracks the * reference more closely WITH dilation than without. Do not hand-edit — * re-measure if algorithms change (see _thinaudit.js). * * RE-MEASURED after the canvas gained padding. The old 17-method set was * calibrated when artwork == canvas (full bleed). Once the artwork is inset * — 556px inside a 1000px canvas for a 50% head — strokeRadius() jumps from * 0 to 1, so dilation switches on for the first time and FATTENS these * methods by 1.5x-2.7x. Re-running the same coverage test at 556px artwork * against the 420px full-bleed reference, 15 of the 17 land CLOSER to the * reference with dilation OFF. Confirmed by eye, not just by coverage: * ridgeline's 1px scanlines merged into blobs, seismo lost its structure, * bubbles' circles closed up, scales went nearly solid. * * Only isolines and splatter still earn it — both draw sparse strokes with * real whitespace between them, which is the case dilation was meant for. * * Note strokeRadius() is a STEP function (round(size/REF)-1): it flips 0->1 * at 450px artwork and 1->2 at 750px, so quality changes discontinuously * with the inset. Re-measure if the default inset moves. */ const THIN = new Set(["isolines", "splatter"]); function dilate(ink, S, r){ if(r<1) return ink; const out=new Float64Array(S*S); for(let y=0;y<S;y++)for(let x=0;x<S;x++){ if(ink[y*S+x]<=0.5) continue; for(let dy=-r;dy<=r;dy++)for(let dx=-r;dx<=r;dx++){ if(dx*dx+dy*dy>r*r+0.5) continue; const yy=y+dy,xx=x+dx; if(yy<0||yy>=S||xx<0||xx>=S) continue; out[yy*S+xx]=1; } } return out; } function strokeRadius(name, size){ if(!THIN.has(name)) return 0; return Math.max(Math.round(size/REF)-1, 0); } var KYSR={paramsFor:paramsFor,REF:REF,dilate:dilate,strokeRadius:strokeRadius,THIN:THIN}; /* --- derive.js --- */ /* Seed -> full parameter set. Pure function, deterministic, matches on-chain. * * seed = keccak256(tokenId, burner, blockhash) -- 32 bytes * Every parameter is drawn from successive slices of an expanded keystream, * so the whole artwork is a deterministic function of one 32-byte value. */ 'use strict'; /* keccak256 — the SAME function Solidity uses. In node this resolves the * bundled implementation; in the browser build the bundler inlines it and this * require line is stripped, leaving the global KECCAK. */ const KECCAK=root.KECCAK; /* Keystream: keccak256(seed ++ uint32be(counter)), consumed 4 bytes at a time. * * This MUST match the contract. It previously used a sha256 stand-in, which * agreed with Solidity's keccak256 on only 1.3% of seeds -- i.e. chance -- so * the Method trait named a different method than the artwork drew. */ function keystream(seedHex, n) { const seed = KECCAK.hexToBytes(seedHex); const out = []; let ctr = 0; while (out.length < n) { const msg = new Uint8Array(seed.length + 4); msg.set(seed, 0); msg[seed.length] = (ctr >>> 24) & 255; msg[seed.length + 1] = (ctr >>> 16) & 255; msg[seed.length + 2] = (ctr >>> 8) & 255; msg[seed.length + 3] = ctr & 255; const h = KECCAK.keccak256(msg); for (let i = 0; i + 4 <= h.length && out.length < n; i += 4) { const v = ((h[i] << 24) | (h[i+1] << 16) | (h[i+2] << 8) | h[i+3]) >>> 0; out.push(v / 4294967296); } ctr++; } return out; } /* Parameter ranges — must match the contract exactly. */ const RANGES = { /* RESTORED to the full [0.50, 2.25]. It was briefly narrowed to * [0.70, 1.40] because the top end looked "sparse and washed out" — but * that was measured while render.js's THIN set was dilating 17 methods by * 1.5x-2.7x at the inset artwork size. Coarse marks were being fattened * into blobs and then read as degenerate. With THIN corrected to just * {isolines, splatter}, the full range is clean: swept across all 80 * methods at the token's 556px artwork, NOTHING clogs solid (>97% ink) at * 0.50 and nothing washes out (<3%) at 2.25 except `maze`, which already * carries a per-method CLAMPS entry. * * The range is the expressive axis of the whole roster — fine texture at * the low end, coarse abstraction at the high end. Narrowing it to +/-40% * around 1.0 collapses most of that. Re-validate with a full sweep before * changing it again, and check what dilation is doing first. */ mark : [0.50, 2.25], // mark scale — fine texture <-> coarse abstraction face_shape : [0.15, 0.95], rotate : [-70, 85 ], pitch : [ -8, 8 ], perspective: [0.0, 0.90], light_dir : [-100, 100], light_elev : [-15, 75 ], soft : [0.05, 0.70], /* FLOORS RAISED 0.02 -> 0.12 (white) and 0.60 -> 0.95 (gamma). * * The floor governs how DARK a token may get: `tone()` computes * (t-white)/(1-white) then pow(.,gamma), so a low floor lets the head fill * in as a featureless white mass. At the old floor, pinning all 80 methods * to it gave a median of 48.6% ink with 18 methods over 70% — solid * silhouettes with texture only at the edges. * * Raising the FLOOR is a much stronger lever than widening the ceiling: * ceiling work moved over-filled tokens 7 -> 4, this moves 18 -> 3 at the * floor itself. Measured over all 80 methods pinned to the floor: * * floor median max >70% * w0.02 g0.60 48.6% 93.0% 18 (old) * w0.12 g0.95 34.4% 83.3% 3 (this) * w0.22 g1.30 22.1% 75.0% 2 (breaks up shadow detail) * * Chosen for keeping full tonal range — tokens can still be genuinely dark, * just not featureless. Per-method CLAMPS below may still floor LOWER where * a method needs it (tally, morse, crossgrid, maze, diamond). */ white : [0.12, 0.30], gamma : [0.95, 1.60], }; const ORDER = ['mark','face_shape','rotate','pitch','perspective', 'light_dir','light_elev','soft','white','gamma']; /* Per-algorithm clamps for the few whose legibility is fragile. */ /* Per-method mark ceilings, as ABSOLUTE values against RANGES.mark. * These were remapped when the range was briefly narrowed to [0.70,1.40] and * are now restored alongside it. `maze` is the one method that genuinely * degenerates at the top of the global range (2.2% ink at 2.25), which is * what its 1.40 ceiling is for. */ const CLAMPS = { tally:{white:[0.02,0.14],soft:[0.05,0.40]}, morse:{white:[0.02,0.20]}, crossgrid:{white:[0.02,0.20], mark:[0.50,1.90]}, randomwalk:{mark:[0.50,1.90]}, splatter:{mark:[0.50,1.90]}, /* these six lose legibility above the midpoint of the mark range */ maze:{white:[0.02,0.18], mark:[0.50,1.40]}, truchet:{mark:[0.50,1.40]}, ascii:{mark:[0.50,1.40]}, lowpoly:{mark:[0.50,1.40]}, quadtree:{mark:[0.50,1.40]}, wolfram:{mark:[0.50,1.40]}, /* ---- coverage clamps, measured at the token's own 500px inner canvas * (1000px @ inset 0.50) over 8 seeds x the axis quartiles. * These shift the white/gamma WINDOW toward the corrective end while * keeping it a window — a fixed pair would make every token of the * method identical. Only methods a clamp actually rescues are listed; * maze, rain and scales resist white/gamma and need algorithm work. */ diamond:{white:[0.02,0.15], gamma:[0.60,1.05]}, /* 13.2% -> 19.0% */ offsets:{white:[0.06,0.30], gamma:[0.75,1.60]}, /* 55.1% -> 53.2% */ ridgeline:{white:[0.06,0.30], gamma:[0.75,1.60]}, /* 56.8% -> 54.0% */ /* high mark thins these out */ scribble:{mark:[0.50,1.80]}, lissajous:{mark:[0.50,1.80]}, rain:{mark:[0.50,1.90]}, }; /* keccak256(seed ++ uint32(0)) % n -- the contract's method selector. */ function methodIndex(seedHex, n) { const seed = KECCAK.hexToBytes(seedHex); const msg = new Uint8Array(seed.length + 4); msg.set(seed, 0); const h = KECCAK.keccak256(msg); let r = 0n; for (let i = 0; i < 32; i++) r = (r << 8n) | BigInt(h[i]); return Number(r % BigInt(n)); } const ASCII_RAMPS = ['classic','code','letters','blocks','dense','numeric','minimal']; /* ---- ink floor ----------------------------------------------------------- * white and gamma both push the same way: LOW white + LOW gamma = far more ink * = a mostly-white image (measured 58-80% coverage when both bottom out). * * Leave both alone unless their COMBINED position is too low, then lift the * pair. Most tokens are untouched; only the ones that would wash out get * nudged. * * EXPORTED and shared. This logic was previously inline here AND duplicated in * studio.html, with nothing testing that the two agreed — see REVIEW.md 4b. * Mutates `p` in place and returns it. */ const INK_FLOOR = 0.70; // min combined position (of 2.0) -> ~44% ink ceiling function applyInkFloor(p, rr) { const [wlo, whi] = rr.white, [glo, ghi] = rr.gamma; const wPos = (p.white - wlo) / (whi - wlo); const gPos = (p.gamma - glo) / (ghi - glo); const sum = wPos + gPos; if (sum >= INK_FLOOR) return p; /* Lift GAMMA first. Some methods have a deliberately narrow white range (tally, maze, morse, crossgrid need low white space to stay legible), so white often has little room and gamma must carry the shortfall. */ let w = wPos, g = gPos, need = INK_FLOOR - sum; const gTake = Math.min(need, 1 - g); g += gTake; need -= gTake; if (need > 0) w = Math.min(w + need, 1); p.white = wlo + w * (whi - wlo); p.gamma = glo + g * (ghi - glo); return p; } /* Ranges for one method, with its clamps applied. */ function rangesFor(algo) { return Object.assign({}, RANGES, CLAMPS[algo] || {}); } function derive(seedHex, algoNames) { const k = keystream(seedHex, 20); /* Method index is an INTEGER reduction of the first keccak word, matching * KYSRenderer's `keccak256(seed, uint32(0)) % N` exactly. Do not reintroduce * float arithmetic here: floor(k[0]*N) does NOT agree with a modulo. */ const algo = algoNames[methodIndex(seedHex, algoNames.length)]; const rr = rangesFor(algo); const p = { algo, identity_seed: Math.floor(k[1] * 2147483647) }; ORDER.forEach((key, i) => { const [lo, hi] = rr[key]; p[key] = lo + k[2 + i] * (hi - lo); }); applyInkFloor(p, rr); /* per-method sub-parameters */ if (algo === 'ascii') { p.ramp = ASCII_RAMPS[Math.floor(k[15] * ASCII_RAMPS.length) % ASCII_RAMPS.length]; } return p; } /* How many distinct visually-meaningful outcomes? Quantise each axis to the * granularity at which a change is actually perceptible. */ const QUANT = { face_shape:0.08, rotate:6, pitch:4, perspective:0.15, light_dir:10, light_elev:8, soft:0.08, white:0.04, gamma:0.20, }; function bucket(p) { return p.algo + '|' + ORDER.map(k => Math.round(p[k] / QUANT[k])).join(','); } var KYSD={derive:derive,keystream:keystream,methodIndex:methodIndex,RANGES:RANGES,ORDER:ORDER,CLAMPS:CLAMPS,ASCII_RAMPS:ASCII_RAMPS}; root.KYS=KYS; root.KYSR=KYSR; root.KYSD=KYSD; })(typeof self!=="undefined"?self:this); /* --- entry.js --- */ /* KYS token page entry point. * * The page arrives with three globals set by KYSRenderer._page(): * window.KYS_SEED 0x-prefixed 32-byte seed * window.KYS_STORE EthFS FileStore address * window.KYS_MODEL model filename, e.g. "gnm-k32.bin" * * It fetches the model straight from chain -- getFile() for the pointer list, * then one eth_getCode per chunk. eth_getCode is UNMETERED, so this costs the * viewer nothing and no node has to serve a large eth_call. (EthFS.read() on * the whole 2.4MB file measures 31.8 BILLION gas and is unservable.) * * Requires kys-engine.js and kys-roster.js to have loaded first. */ (function (root) { 'use strict'; var SEL_GETFILE = '0xe0876aa8'; // getFile(string) function el(id) { return root.document.getElementById(id); } function status(msg) { var s = el('s'); if (s) s.textContent = msg; } /* ---------------------------------------------------------------- rpc */ /* Provider order: an injected wallet first (no rate limits, no third party), * then public fallbacks. The artwork must not depend on any single host. */ function providers() { var out = []; if (root.ethereum) out.push({ kind: 'injected', p: root.ethereum }); var urls = root.KYS_RPC ? [].concat(root.KYS_RPC) : []; urls.push('https://ethereum-rpc.publicnode.com'); urls.push('https://eth.llamarpc.com'); urls.push('https://rpc.ankr.com/eth'); for (var i = 0; i < urls.length; i++) out.push({ kind: 'http', url: urls[i] }); return out; } function call(prov, method, params) { if (prov.kind === 'injected') { return prov.p.request({ method: method, params: params }); } return root.fetch(prov.url, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: method, params: params }) }).then(function (r) { return r.json(); }) .then(function (j) { if (j.error) throw new Error(j.error.message); return j.result; }); } /* --------------------------------------------------------------- abi */ function encGetFile(name) { var hex = '', i; for (i = 0; i < name.length; i++) { var c = name.charCodeAt(i); hex += (c < 16 ? '0' : '') + c.toString(16); } var byteLen = name.length; var pad = (32 - (byteLen % 32)) % 32; return SEL_GETFILE + pad64(32) + pad64(byteLen) + hex + repeat('00', pad); } function pad64(n) { var h = n.toString(16); return repeat('0', 64 - h.length) + h; } function repeat(s, n) { var o = ''; while (n-- > 0) o += s; return o; } /* File = (uint256 size, Content[] contents); Content = (address,uint32,uint32) */ function decFile(hex) { var h = hex.slice(2); function w(i) { return h.slice(i * 64, (i + 1) * 64); } function n(i) { return parseInt(w(i), 16); } var structOff = n(0) / 32; var size = n(structOff); var arrOff = structOff + n(structOff + 1) / 32; var len = n(arrOff); var contents = []; for (var i = 0; i < len; i++) { var b = arrOff + 1 + i * 3; contents.push({ pointer: '0x' + w(b).slice(24), start: n(b + 1), end: n(b + 2) }); } return { size: size, contents: contents }; } function hexToBytes(hex, start, end) { var s = start * 2 + 2, e = end * 2 + 2; var out = new Uint8Array(end - start); for (var i = 0; i < out.length; i++) { out[i] = parseInt(hex.substr(s + i * 2, 2), 16); } return out; } /* -------------------------------------------------------------- fetch */ function fetchModel(prov, name, onProgress) { return call(prov, 'eth_call', [{ to: root.KYS_STORE, data: encGetFile(name) }, 'latest']) .then(function (raw) { var file = decFile(raw); var parts = new Array(file.contents.length); var total = file.contents.length; /* Sequential: 101 parallel requests get rate-limited by every public * node. This is slower but it actually completes. */ var chain = Promise.resolve(); file.contents.forEach(function (c, i) { chain = chain.then(function () { return call(prov, 'eth_getCode', [c.pointer, 'latest']); }).then(function (code) { parts[i] = hexToBytes(code, c.start, c.end); if (onProgress) onProgress(i + 1, total); }); }); return chain.then(function () { var out = new Uint8Array(file.size), off = 0; for (var i = 0; i < parts.length; i++) { out.set(parts[i], off); off += parts[i].length; } if (off !== file.size) throw new Error('size mismatch ' + off + '/' + file.size); return out; }); }); } /* Try each provider in turn. A dead RPC must not mean a dead artwork. */ function fetchWithFallback(name, onProgress) { var provs = providers(), i = 0; function attempt() { if (i >= provs.length) throw new Error('all RPC providers failed'); var p = provs[i++]; return fetchModel(p, name, onProgress).catch(function (e) { if (i < provs.length) { status('retrying via another node...'); return attempt(); } throw e; }); } return attempt(); } /* ------------------------------------------------------------- render */ /* The payload is "KYSL" + uint32 headerLen + licence text + the KYSM model. * Skip the header; the licence rides along on chain but is not model data. */ function modelBytes(payload) { if (payload[0] === 0x4b && payload[1] === 0x59 && payload[2] === 0x53 && payload[3] === 0x4c) { var len = payload[4] | (payload[5] << 8) | (payload[6] << 16) | (payload[7] << 24); return payload.subarray(8 + len); } return payload; // raw KYSM, no licence header } function draw(payload) { var bytes = modelBytes(payload); var ab = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength); var M = root.KYS.loadModel(ab); var names = Object.keys(root.KYS_A); var p = root.KYSD.derive(root.KYS_SEED, names); var canvas = el('c'); var S = canvas.width; var ident = root.KYS.identity(p.identity_seed, p.face_shape, M.K); var V = root.KYS.verts(M, ident); var F = root.KYS.rasterize(root.KYS.lookAt(V, p.rotate, p.pitch), M.tris, S, p.perspective); var ink = root.KYS_A[p.algo](F, root.KYSR.paramsFor(p, S, p.algo)); var ctx = canvas.getContext('2d'); var img = ctx.createImageData(S, S); var d = img.data; for (var i = 0; i < S * S; i++) { var v = ink[i] > 0.5 ? 0 : 255; d[i * 4] = d[i * 4 + 1] = d[i * 4 + 2] = v; d[i * 4 + 3] = 255; } ctx.putImageData(img, 0, 0); root.KYS_METHOD = p.algo; root.KYS_PARAMS = p; root.KYS_DONE = true; // render_onchain / headless tests poll this status(''); } function main() { if (!root.KYS_SEED) { status('no seed'); return; } status('fetching model from chain...'); fetchWithFallback(root.KYS_MODEL || 'gnm-k32.bin', function (i, n) { if (i % 10 === 0 || i === n) status('chunk ' + i + ' / ' + n); }).then(function (payload) { status('rendering...'); /* yield once so the status paints before the synchronous render blocks */ root.setTimeout(function () { try { draw(payload); } catch (e) { status('render failed: ' + e.message); root.KYS_ERROR = String(e); } }, 0); }).catch(function (e) { status('failed: ' + e.message); root.KYS_ERROR = String(e); }); } if (root.document && root.document.readyState === 'loading') { root.document.addEventListener('DOMContentLoaded', main); } else if (root.document) { main(); } root.KYS_MAIN = main; })(typeof self !== 'undefined' ? self : this);
(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);
/* 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
! # ' ( ) * + ( - . / 0 1 - . 2 3 & ) 3 5 6 7 9 4 7 ; < = < 6 5 > ? > ; : A B C E F G H I F B A K J K M O 9 8 Q O N S Q P D G S U V W Y Z [ \ ] Z W " # _ _ ^ ] a L M c a ` e f g i + f j k j i h b e d m n o l o q v *!+!,!)!,!.!0!*!)!/!)!-!3!0!/!2!/!1!9!6!5!8!5!7!<!9!8!;!8!:!?!@!A!>!A!C!E!?!>!D!>!B!TH!I!J!G!J!L!N!H!G!M!G!K!Q!N!M!S!Q!P!P!M!O!R!P!T!W!OZ![!\!Y!\!^!`!Z!Y!_!Y!]!c!d!e!b!e!g!i!c!b!h!b!f!l!i!h!k!h!j!5 r!o!n!q!n!p!u!r!q!t!q!s!x!u!t!w!t!v!?"@"A"C"?">"@"D"E"D"F"J"M"N"O"L"O"H"L"G"Q"R"S"P"S"U"H"Q"P"I"P"T"W"X"Y"V"Y"R"N"W"V"O"V"Q"["\"]"Z"]"_"R"["Z"S"Z"^"a"b"c"`"c"\"X"a"`"Y"`"["e"f"g"d"g"i"\"e"d"]"d"h"k"l"m"j"m"f"b"k"j"c"j"e"o"p"q"p"r"s"n"q"g"q"s"i"u"v"w"v"o"n"t"w"m"w"n"f"y"z"{"|"}"z"z"p"o"}"r"p"##t"l"%###"#&#u"t"'#&###(#)#k"*#+#)#)#"#l"+#$#"#,#-#"@#A#?#C#@#>#B#>#K#J#J#L#N"K#M#L#L#N#W"M#O#N#N#P#X"O#Q#P#P#R#a"Q#S#R#R#(#b"S#*#(#X#Y#Z#[#\#Z#^#[#Y#]#Y#X#a#^#]#`#]#_#d#e#f#h#d#c#j#h#g#k#l#e#k#m#n#p#m#k#o#k#d#r#p#o#q#o#h#m#s#t#s#u#v#w#x#u#p#w#s#y#z#x#y#w#p#|#}#~#}#$$$-$/$0$1$.$1$3$4$5$0$6$7$5$9$:$;$:$<$=$8$;$?$;$=$@$B$C$D$C$E$F$A$D$:$D$F$<$<$G$H$G$I$J$=$H$K$H$J$L$E$M$N$M$O$P$F$N$G$N$P$I$4#5#R$7#4#Q$:#;#U$5#:#T$`$a$b$a$c$d$f$_$b$e$b$d$j$h$i$a$i$j$c$l$m$n$p$k$n$l$k$r$k$p$s$v$t$n$m$u$o$n$t$d$c$x$w$x$m$d$w$y$w$l$q$u$j$z$x$x$z$u$|$}$~$}$`$_${$~$V%S%R%U%R%T%X%Y%W%T%Y%]%^%_%`%a%^%^%b%c%a%d%b%#g%e%e%h%`%g%i%h%h%j%a%i%k%j%j%l%d%k%m%l%n%v%x%y%x%z%{%z%|%}%y%{%*&)&*&&&,&-&.&+&.&5&1&5&6&3&0&7&8&-&8&9&.&9&:&*&:&;&&&;&<&#&<&=&Q&R&S&l%Q&P&T&U&R&m%T&Q&c%b%W&d%P&W&V&W&Y&P&S&Y&$B%`&N&a&N%M%C%a&`&+"h&f&h&F& " "k&j&k&B& " "j&i&j&D& "#g k y&k h A&{&|&}&&)'`!*'+','d!c! 'j!f!-'f!g!0'a!]!2'0'/'OA'?'<';'C'E'B'C'!<'B'G' '-'-'.'H'#N'V'W'V'U'T'O'W'$_'X'_'a'Z'R'd'e'd'f'g'S'e'g%e'g'i%]'h'i'\'i'%j'^'f'l'm'l'n'o'g'm'k%m'o'm%q'r's'%q'p'p's'u't'u'w'n'z'{'z'|'}'o'{'T&{'}'U&q(b("(#(q(q(r(c(#($(r(r(s(d($(%(s(s(t(](%(&(t(t(u(Z(&('(u(u(v([('(((v(v(w(\((()(w(w(x(a()(*(x(x(y(^(*(+(y(y(z(_(+(,(z(z({(`(,(-({(|(}(i(.(/(}(}(~(j(/(0(~(~( c#f#\#g#c#[#i#g#^#(I$n&m&J$m&o&O$q&p&P$p&n&9%8%s& 9%r&L$o&t&u&?%>%v&>%Wu&w&A%w&x&C%z&}&K%~&z&J%5'6'$'+')'6'*''')'1'^!X![!5'3'6' C';'Z ;'>'[ D'C'] 'I' 'G'A'@'?'F'A'G'F'K'2'1' ()<!;!');!=!d!`!_!e!_!a!+)&"9"+"-";"%"+":"-"/"<"/)"6).#,#8)6$4$7)4$/$:);).$;)9)/$&X U H)I)0&I)J)7&0)X)Y)Z)T)Z)Y)[)[)\))*'`!d!g!a!0'0'2'H'`(f){(g)f)-(c)g)h)(6$8)V [ >'H'2'K'!*),)1 8)7)- 7)9)& 2 ;). 9);)<)' & =)* ' u!x!L)r!u!K)Q)P)~!P)O){!5(I)H)o)p)q)A!()')q)q)r)C!')))r)r)s)%-)>)t)t)u)A#>)6)u)u)v)?#6)5)v)v)w))'*)***+*-*)*(*/*0*1*.*1*3*5***8*9*8*5*4*;*7*6*;*:*0*=*>*?*2*3*?*@*B*C*B*D*E*D*-*,*F*=*<*I*H*I*K*J*K*M*L*M*O*N*O*Q*P*Q*S*R*S*U*T*U*W*Y*Z*[*]*Y*X*X*[*_*\*X*^*b*c*d*f*b*a*a*d*h*e*a*g*(*+*k*,*(*j*j*k*n*l*j*m*p*q***r*p*)*t*u*v*x*t*s*s*v*q*w*s*p*z*{*|*~*z*y*y*|*u*}*y*t*1*0*{*3*1*z**!+#+"+c*m*n*"+$+#+b*o*m*#+%+$+f*A+D+A+@+B+A+D+D+C+G+E+D+F+F+G+J+H+F+I+I+J+M+K+I+L+L+M+P+N+L+O+O+P+S+Q+O+R+R+S+V+T+R+U+|/+X+/+.+Y+.+-+Z+-+,+[+,+++\+++*+]+*+)+_+^+)+a+ ++b+ + +c+`* +d+\*`*e+]*\*f+l+i+h+k+h+j+l+k+o+k+m+p+r+n+o+q+o+p+u+r+q+t+q+s+u+t+x+t+v+y+w+x+{+x+y+|+z+{+~+{+|+*J*L*h*L*N*g*N*P*i*P*R*1+C+@+2+G+C+3+J+G+4+M+J+5+P+M+6+S+P+7+V+S+9+,7+6+^,9+8+r,=+<+s,>+=+t,?+>+v,$[,\,`,;+:+a,<+;+4
f)`({(g){(-(e(e)h)!7)- 1 9). - ;):)& ;)2 . & :)<)' <)=){!O)L)K)u!K)M)r!~!(f)m(f)g)|(g)c).(c)(n)o)n)I)A!@!p)q)p)()C!A!q)r)q)')%t)u)t)>)?#A#u)v)u)6))+*(*)*(*,*-*1*.*/*3*2*.*6*7*9*+***4*9*8*6*:*;*0*/*;*?*<*=*?*>*2*C*A*@*E*C*B*,*E*D*<*G*F*I*H*K*J*H*M*L*J*O*N*L*Q*P*N*S*R*P*U*T*R*W*V*T*[*X*Y*X*\*]*_*^*X*^*`*\*d*a*b*a*e*f*h*g*a*g*i*e*k*j*(*j*l*,*n*m*j*m*o*l***)*p*)*-*r*v*s*t*s*w*x*q*p*s*p*r*w*|*y*z*y*}*~*u*t*y*t*x*}*{*z*1*z*~*3**c*"+!+"+n*c*b*#+"+#+m*b*f*$+#+$+o*f*@+C+D+D+E+B+G+F+D+F+H+E+J+I+F+I+K+H+M+L+I+L+N+K+P+O+L+O+Q+N+S+R+O+R+T+Q+V+U+R+U+W+T+/+X+|.+Y+X+-+Z+Y+,+[+Z+++\+[+*+]+\+)+^+]+)+(+_++`+a+ +a+b+ +b+c+`*c+d+\*d+e+]*e+f+j+h+h+k+l+j+m+k+o+n+l+p+o+k+o+q+r+p+s+q+q+t+u+s+v+t+x+w+u+y+x+t+{+z+w+|+{+x+~+}+z+0+@+0+1+C+1+2+G+2+3+J+3+4+M+4+5+P+5+6+S+6+7+,8+],^,<+a,r,=+r,s,>+s,t,?+t,v,7,h,t r ~, !!!z,"! !,$ U#G,V#B,G,i,m, " # $ " & % % ' $ ) ( + ( , - / . ( . 0 , 2 1 . 1 3 0 5 4 1 4 6 3 8 7 : 7 ; < > = 7 = ? ; A @ = @ B ? D C @ F E H E I J L K E K M I O N Q N R S U T N T V R X W T W Y V [ Z W Z \ Y ^ ] Z ] _ \ a ` c ` d e g f ` f h d j i f i k h ! k m l " l n # p o l o q n s r o r t q v u r u w t y x u x z w | { x { } z " # ' & ) * ' ( - , / 0 , - . / 3 & 2 3 5 4 7 9 8 7 ; : = < = 5 ? > ? : A @ C E D G H E F B J K J L M O N 8 Q P N S R P D R S U T W Y X [ \ Y Z W T # _ ^ _ \ ] a ` M c b ` e d g i h * + f g k j k h b c d m l o l p q v u S R ( X W Z [ Y Z ^ ] Y ] _ X a ` ] ` b _ d c f h g c j i g k d e k l n p o k o h d r q o q j h m n t s t v w s u p m s y w x y r p | { ~ } ~ * + % + - ' P Q K Q S M c [ \ g ^ [ i a ^ b c ] c e _ X U V
) ( ' ( + * / . - - 1 0 3 2 . 3 ) & 7 6 5 7 4 9 = < ; 5 6 < ? > : ; > C B A G F E F I H K A B M K J 8 9 O N O Q P Q S S G D W V U [ Z Y Z ] \ # " W _ ] ^ _ M L a ` a c g f e k j f h i j d e b o n m q o l u u v y w u x x y "- , - / . - 1 0 1 ? > ? A @ > @ C ( R ( * S Z Y X Z \ [ Y [ ^ X Y ] ] ^ a _ ] ` f e d c d h g h j e l k n m k k m p d k o o p r h o q t s m v u s u x w s w p x z y p w y ~ } | % & * ' % + K L P M K Q \ f c [ c g ^ g i 0 p ] ^ b _ ] c V [ X " ) & ' ( ' * / , - - , 0 3 / . 3 2 & 7 4 5 7 8 9 = : ; 5 = < ? : ? > C @ A G D E F E H K J B M L J 8 N O N P Q P R S S R D W T U [ X Y Z Y \ # T W ^ _ ] \ _ M ` a ` b c g d e k g f h k j d c b o l m q p l w u u x y w z x x %%!#!#!&!'!%!(!&!&!,!)!*!.!-!)!)!/!0!-!1!/!/!2!3!1!4!2!2!7!5!5!8!9!7!:!8!8!;!<!:!=!;!A!>!?!C!B!>!>!D!E!B!F!D!D!R# ! J!G!H!L!K!G!G!M!N!K!O!M!M!P!Q!P!R!S!O!T!P!T!U!R!UX!V!\!Y!Z!^!]!Y!Y!_!`!]!a!_!e!b!c!g!f!b!b!h!i!f!j!h!h!k!l!j!m!k!k!2 p!n!n!q!r!p!s!q!q!t!u!s!v!t!t!w!x!v!y!w!w!;"A">"?">"B"C"E"A"@"E"D"I"F"G"K"J"F"F"S"P"Q"U"T"P"P"I"H"T"K"I"Y"V"W"R"Q"V"V"O"N"Q"H"O"]"Z"["_"^"Z"Z"S"R"^"U"S"c"`"a"\"["`"`"Y"X"["R"Y"g"d"e"i"h"d"d"]"\"h"_"]"m"j"k"f"e"j"j"c"b"e"\"c"q"n"o"s"q"p"g"f"n"i"g"q"w"t"u"n"w"v"m"l"t"f"m"w"{"x"y"z"y"|"o"{"z"p"z"}""l""###"#$#%#t"##&###%#'#k"b"(#)#(#*#l"k")#"#)#+#",#-#,#.#-#/#0#1#0#G#D#E#F#E#~"'#G#F#&#F#N"M"J#L#J#K#W"N"L#N#L#M#X"W"N#P#N#O#a"X"P#R#P#Q#b"a"R#(#R#S#Z#W#X#Z#Y#[#Y#]#^#X#_#]#]#`#a#_#b#`#f#c#d#c#g#h#g#i#j#e#d#k#n#l#k#k#o#p#d#h#o#o#q#r#h#j#q#t#n#m#v#t#s#u#s#w#s#m#p#x#w#y#p#r#y#~#{#|#$1$.$/$3$2$.$0$/$4$5$4$6$;$8$9$=$;$:$?$>$8$@$?$;$D$A$B$F$D$C$:$9$A$<$:$D$H$=$<$J$H$G$K$@$=$L$K$H$N$F$E$P$N$M$G$<$F$I$G$N$R$Q$4#Q$S$7#U$T$:#T$R$5#b$_$`$d$b$a$b$e$f$d$g$e$i$h$a$`$h$c$a$i$n$k$l$n$o$p$r$q$l$s$r$k$u$u$t$n$t$v$o$x$w$d$m$l$w$y$g$d$q$y$w$z$j$x$c$j$u$m$x$~${$|$_$~$}$2%0%0%.%-%2%/%.%:T%R%R%U%V%T%W%U%Y%T%Y%[%W%_%\%]%^%]%`%c%_%^%b%^%a%f%#`%f%e%h%e%g%a%`%h%j%h%i%d%a%j%l%j%k%r%D%/%s%r%2%w%t%u%v%u%y%w%v%{%y%x%}%{%z%& &? C B & &G F &&#&$&#&'&(&*&)&3&1&5&8&-&0&9&.&-&;&&&*&<&#&&&=&'&#&%S&P&Q&P&d%l%R&Q&T&Q&l%m%W&V&c%W&b%d%Y&X&V&Y&W&P&%X&Z&[&Z&\&]&V&[&"F&G&f&k&j& "B&D&j&j&i& "D&E&i&l&k& "C&B&k&&g A&y&k }&z&{&z&~&I!H!N!K!L! 'O!K!','f!-' 'g!.'-']!/'0'/'1'2'3'P!7':'!'"'>';'<'='<'C'B'<'C'D'E'B'E'!-'F'G'H'F'-'G' '$Z'X'_'e'S'R'g'e'd'g%%m'g'f'o'm'l'k%i%g'm%k%m's'p'q'p'{'o'n'}'{'z'T&m%o'U&T&{'M'~'"(c(b(q(r(q(#(d(c(r(s(r($(](d(s(t(s(%(Z(](t(u(t(&([(Z(u(v(u('(\([(v(w(v(((a(\(w(x(w()(^(a(x(y(x(*(_(^(y(z(y(+(`(_(z({(z(,(i(m(|(}(|(.(j(i(}(~(}(/(k(j(~( J!I!" L!J!V!\![!X!^!\!!\#[#c#[#^#g#^#a#i#0 m&J$I$o&L$J$p&P$O$n&I$P$s&r&9%r&v&A%?%u&C%A%w&K%J%z&J%N%~&N%a&3'$'3'5'6'5'+')'+'*'X!4'1'3'W![!%'$'6'B$8'7''Z ] C'[ Z ;'] ^ D'D'^ A'I' 'F'H'@'F'?'A'1'4'K' &)$);!')()=!))')_!e!d!a!g!e!;":"+":"7"%"<";"-""2)5),#5)6)4$7)8)/$9)7).$2$:)/$.$;):)2$)`)_)d!,'*'0'.'g!H'.'0'
HCV{.8H VY.fH\VN.#%Z-Mp$(ZBM+% Z+M[<NW$[[VNb#W[ANp#Z,NT%P[NNG&2[4N=&H\V1#{\sN:"d\:NP!=\ ! & $ % + * ) - + ( ( ) / , ( . . / 2 0 . 1 1 2 5 3 1 4 : 9 8 < : 7 7 8 > ; 7 = = > A ? = @ @ A D H G F J H E E F L I E K Q P O S Q N N O U R N T T U X V T W W X [ Y W Z Z [ ^ \ Z ] c b a e c ` ` a g d ` f f g j h f i i j ! k i " $ m # " l l m p n l o o p s q o r r s v t r u u v y w u x x y | z x { { |
===== LICENSE (Apache-2.0, Google GNM) ===== Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================================================ Third-Party Component Licenses ================================================================================ A portion of this repository, specifically the tongue animation meshes for the articulation of the tongue inside of the mouth was adapted from work by Salvador Medina, which is licensed under the MIT License: Copyright (c) 2022 Salvador Medina Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ===== THIRD-PARTY NOTICES ===== # Third-Party Notices This project incorporates material from the third-party works listed below. --- ## GNM (Generative aNthropometric Model) — GNM Head - **Source:** https://github.com/google/gnm - **Copyright:** Copyright 2026 Google LLC - **License:** Apache License, Version 2.0 - **License text:** http://www.apache.org/licenses/LICENSE-2.0 - **Technical report:** https://arxiv.org/abs/2607.23687 ### What is used This project uses the GNM Head statistical model data file `gnm/shape/data/versions/v3_0/gnm_head.npz`. No GNM source code is incorporated; the model data is read directly with NumPy. ### Modifications As required by Section 4(b) of the Apache License 2.0, the following modifications to the GNM model data have been made in this project. The distributed binary `proto/chain/gnm_k32.bin` is a **modified subset** of the original GNM Head model data, NOT the original file: 1. **Vertex subset.** Only vertices belonging to the `skin_exterior` vertex group are retained (17,821 vertices reduced to 11,460); triangles are filtered to those fully inside that group and re-indexed. 2. **Basis truncation.** Only the first K = 32 principal identity components of the 253-component `vertex_identity_basis` are retained. The expression basis, joint/skinning data, UVs, and all other arrays are omitted. 3. **Quantization.** Template vertex positions are quantized from float64 to int16 with a scale/offset; the identity basis is reduced from float32 to float16. These modifications are performed by `proto/chain/pack_model.py`. ### Citation ```bibtex @article{ploumpis2026gnmhead, title={GNM Head: A Generative aNthropometric Model of the human head}, author={Ploumpis, S. and Bednarik, J. and Zoss, G. and Guseinov, R. and Prasso, L. and Chandran, P. and Boyne, O. and Choutas, V. and Bolkart, T. and Wang, D. and Chai, M. and Qiu, D. and Winberg, S. and Rainer, G. and Bridgeman, L. and Vicini, D. and Riviere, J. and Boetzel, Y. and Koumis, A. and Busch, J. and Herrera, C. and Still, J. and Ysebert, S. and Lincoln, P. and Escolano, S. O. and Rhemann, C. and Wood, E. and Beeler, T. and Zafeiriou, S.}, year={2026}, eprint={2607.23687}, archivePrefix={arXiv}, url={https://arxiv.org/abs/2607.23687}, } ``` ### On-chain redistribution — UNRESOLVED `gnm_k32.bin` is intended to be written to Ethereum mainnet via EthFS, where it becomes **permanent and unmodifiable**. Two consequences that a lawyer, not an engineer, should sign off on before deploy: 1. Apache 2.0 §4 requires that recipients receive a copy of the licence and this NOTICE. A file on EthFS carries no such metadata by default — the licence and notice must be written on-chain alongside the model, or the distribution arguably does not comply. 2. Compliance defects are **not fixable after the fact**. Nothing on-chain can be amended or withdrawn. Neither point has been actioned. See `REVIEW.md`. ### Note on trademarks Apache License 2.0 Section 6 does not grant trademark rights. "Google" and "GNM" are used here for attribution and accurate description of origin only. This project is not endorsed by or affiliated with Google LLC. ## On-chain distribution (resolved) `gnm_k32.bin` is published to Ethereum via EthFS with the **full Apache-2.0 licence and these notices embedded in the file's own bytes**, ahead of the model payload. Any reader of the on-chain file receives the licence before the model. EthFS's own licence field (`extraData` on `createFileFromPointers`) is also set to `{"license":"Apache-2.0"}`, but it is **not** relied on for compliance: that value is emitted in an event and is not returned by `getFile()`, so it is indexer metadata rather than content. The licence bytes are read from `GNM-main/LICENSE` verbatim at build time. Verified on a mainnet fork: the licence text and these notices are readable back out of the registered file. See `contracts/DEPLOY.md`. ===== END NOTICES — MODEL PAYLOAD FOLLOWS ===== KYSMZ:Nq;U[TNf<P[ANW<4rPzQ451Q\P 7SOuO/b7L@Q1D:#?U,%5"?Lr>I?wMM?d<8L><{C+O