asciify-engine 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -485,3 +485,28 @@ Scroll scrubbing works in live video mode. Leave `preExtract` off when using `sc
485
485
  MIT © [asciify.org](https://asciify.org)
486
486
 
487
487
  > ☕ [Buy me a coffee](https://www.buymeacoffee.com/asciify) — if this saved you time, I'd appreciate it!
488
+
489
+
490
+ ## Unreleased hero finish
491
+
492
+ Version 1.3.0 adds text-shaped charcoal cutouts (`textMask`), peripheral scanline/fringe control (`edgeEffect`), fixed accent ink, and velocity-driven Trail source flow. These additions ship in **npm 1.3.0**. Upgrade older applications before using them. See the [complete API, layering example, lifecycle, and fallback limits](skills/asciify-engine/references/hero-finish.md).
493
+
494
+ ## Optional Studio tools
495
+
496
+ The `asciify-engine/studio` entry adds a reusable composition pipeline: alternate render styles, palette dithering, crop and masks, source/blurred/gradient backdrops, combined optical effects, saved looks, text fonts, and PNG/JPG/GIF/MP4/WebM export. Existing ASCII defaults and `/core` and `/hover` imports stay unchanged. Fonts and video encoders load on demand.
497
+
498
+ ```ts
499
+ import { mountStudio } from 'asciify-engine/studio';
500
+
501
+ const studio = await mountStudio(canvas, '/your-video.mp4', {
502
+ settings: {
503
+ style: 'ascii',
504
+ backdrop: { mode: 'blurred', opacity: 0.4 },
505
+ },
506
+ });
507
+ studio.update({ hover: { effect: 'water', radius: 0.35 } });
508
+ // On unmount:
509
+ studio.destroy();
510
+ ```
511
+
512
+ See the [Studio API guide](skills/asciify-engine/references/studio-workspace.md) for complete settings, import/export, media ownership, browser requirements, and performance budgets.
package/THIRD_PARTY.md ADDED
@@ -0,0 +1,14 @@
1
+ # Third-party components
2
+
3
+ Asciify's own code is MIT licensed. Dependencies keep their own licenses.
4
+
5
+ | Component | Version used for this release | License | Source |
6
+ | --- | --- | --- | --- |
7
+ | gifuct-js | 2.1.2 | MIT | https://github.com/matt-way/gifuct-js |
8
+ | gifenc | 1.0.3 | MIT | https://github.com/mattdesl/gifenc |
9
+ | FIGlet.js | 1.11.4 | MIT | https://github.com/patorjk/figlet.js |
10
+ | Mediabunny | 1.56.0 | MPL-2.0 | https://github.com/Vanilagy/mediabunny/tree/533c857a313b15f98af24ad916eeda27204c38ce |
11
+
12
+ Mediabunny is loaded on demand for Studio video encoding. Its source is unmodified; the corresponding source release is available at https://registry.npmjs.org/mediabunny/-/mediabunny-1.56.0.tgz. The MPL-2.0 text is included in `licenses/Mediabunny-MPL-2.0.txt`. Encoding uses browser-provided WebCodecs implementations.
13
+
14
+ Font data loads individually from FIGlet.js and retains its embedded font authorship notices. The package includes only code for selecting fonts; it does not relicense the font authors' work.
package/dist/hover.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";var t=[{value:"none",mode:"none",label:"Off",description:"Keep the image still. Hover stays available."},{value:"breathe",mode:"print",label:"Living Print",description:"Soft tonal movement. The character grid stays still."},{value:"wave",mode:"current",label:"Slow Current",description:"A gentle flow through the image, with steady edges."},{value:"rain",mode:"reform",label:"Reveal & Reform",description:"Disperse, gather, then hold the complete image."}];function e(e){return"orbit"===e?"current":t.find(t=>t.value===e||t.mode===e)?.mode??"none"}var i=(t,e,i)=>{const s=(t=>Math.max(0,Math.min(1,t)))((i-t)/(e-t));return s*s*(3-2*s)},s=(t,e)=>{const i=(.06711056*t+.00583715*e)%1*52.9829189;return i-Math.floor(i)},n=class{constructor(){this.last=null,this.mode="none",this.phase=0}update(t,e,i=1){return Number.isFinite(t)?((e!==this.mode||null!==this.last&&t<this.last)&&(this.mode=e,this.phase=0,this.last=t),null!==this.last&&(this.phase+=Math.max(0,t-this.last)*Math.max(.1,Math.min(3,Number.isFinite(i)?i:1))),this.last=t,this.phase):this.phase}};function h(t,e,s,n,h){const a=n*Math.PI/6,o=i(0,.12,Math.min(e,1-e,s,1-s));if(h[0]=h[1]=h[2]=0,h[3]=1,"print"===t)h[2]=Math.sin(9*e+5*s+a)*Math.cos(8*s-3*e-a)*.065*o,h[3]=.9+.1*Math.sin(6*e-4*s+a);else if("current"===t)h[0]=Math.sin(9*s+a)*Math.cos(7*e-a)*6*o,h[1]=Math.cos(8*e+a)*Math.sin(6*s-a)*4*o;else if("reform"===t){const t=(n%12+12)%12,a=.8*e+.2*s+.08*Math.sin(8*e-5*s),o=i(1.8+1.2*a,3.8+1.2*a,t)*(1-i(5+1.7*a,7.2+1.7*a,t));h[2]=.95*-o,h[3]=1-.82*o}return h}function a(t,e,i){const s=Math.min(t*Math.max(1,i),(1-t)*Math.max(1,i),e*Math.max(1,1/i),(1-e)*Math.max(1,1/i)),n=Math.max(0,Math.min(1,(s-.035)/.105));return n*n*n*(n*(6*n-15)+10)}var o=class{constructor(t,e){this.previous=null,this.peak=0,this.mode="dissolve",this.width=t,this.height=e;const i=t*e;this.pixels=new Uint8Array(4*i),this.ink=new Float32Array(i),this.targetX=new Float32Array(i),this.targetY=new Float32Array(i),this.offsetX=new Float32Array(i),this.offsetY=new Float32Array(i),this.edge=new Float32Array(i);for(let i=0;i<e;i++)for(let s=0;s<t;s++)this.edge[i*t+s]=a(s/(t-1),i/(e-1),t/e);this.clear()}get active(){return this.peak>2e-4}setMode(t){t!==this.mode&&(this.mode=t,this.clear())}clear(){for(const t of[this.ink,this.targetX,this.targetY,this.offsetX,this.offsetY,this.pixels])t.fill(0);if(this.previous=null,this.peak=0,"dissolve"!==this.mode)for(let t=0;t<this.pixels.length;t+=4)this.pixels[t]=this.pixels[t+2]=128}leave(){this.previous=null}move(t,e,i){if(!Number.isFinite(t+e+i))return;t=Math.max(0,Math.min(1,t)),e=Math.max(0,Math.min(1,e));const s=this.previous;this.previous={x:t,y:e};const n=this.width,h=this.height,a=Math.max(1,n/h),o=Math.max(1,h/n),r=s?(t-s.x)*a:0,l=s?(e-s.y)*o:0,c=Math.hypot(r,l);if(!s&&"dissolve"!==this.mode)return;if(s&&c<1e-4)return;const m=.035+.11*Math.max(.1,Math.min(1,i)),d=Math.min(128,Math.max(1,Math.ceil(c/(.45*m)))),u=c?r/c:1,f=c?l/c:0,g=Math.min(1,c/(m*d)*2.2);for(let i=0;i<d;i++){const r=(i+.5)/d,l=s?s.x+(t-s.x)*r:t,c=s?s.y+(e-s.y)*r:e,p=Math.max(0,Math.floor((l-2.5*m/a)*(n-1))),x=Math.min(n-1,Math.ceil((l+2.5*m/a)*(n-1))),v=Math.max(0,Math.floor((c-2.5*m/o)*(h-1))),M=Math.min(h-1,Math.ceil((c+2.5*m/o)*(h-1)));for(let t=v;t<=M;t++)for(let e=p;e<=x;e++){const i=(e/(n-1)-l)*a/m,s=(t/(h-1)-c)*o/m,r=Math.exp(1.5*-(i*i+s*s)),d=t*n+e;if("dissolve"===this.mode)this.ink[d]=Math.max(this.ink[d],r);else{const t=(-i*f+s*u)*r*2.8,e="silk"===this.mode?u*t:-s*r*2.5,n="silk"===this.mode?f*t:i*r*2.5;this.targetX[d]=Math.max(-1,Math.min(1,this.targetX[d]+e*g)),this.targetY[d]=Math.max(-1,Math.min(1,this.targetY[d]+n*g))}}}this.peak=1}step(t){if(!this.active||!Number.isFinite(t))return;const e=Math.max(0,Math.min(.05,t)),i=Math.exp(-("dissolve"===this.mode?2.15:3.2)*e),s=1-Math.exp(-22*e);let n=0;for(let t=0;t<this.ink.length;t++)if("dissolve"===this.mode){this.ink[t]*=i,n=Math.max(n,this.ink[t]);const e=Math.round(65535*this.ink[t]);this.pixels[4*t]=e>>>8,this.pixels[4*t+1]=255&e}else{this.targetX[t]*=i,this.targetY[t]*=i,this.offsetX[t]+=(this.targetX[t]-this.offsetX[t])*s,this.offsetY[t]+=(this.targetY[t]-this.offsetY[t])*s,n=Math.max(n,Math.abs(this.offsetX[t]),Math.abs(this.offsetY[t]),Math.abs(this.targetX[t]),Math.abs(this.targetY[t]));const e=Math.round(this.offsetX[t]*this.edge[t]*32767+32768),h=Math.round(this.offsetY[t]*this.edge[t]*32767+32768);this.pixels[4*t]=e>>>8,this.pixels[4*t+1]=255&e,this.pixels[4*t+2]=h>>>8,this.pixels[4*t+3]=255&h}this.peak=n,this.active||this.clear()}sample(t,e,i){const s=Math.max(0,Math.min(this.width-1.001,t*(this.width-1))),n=Math.max(0,Math.min(this.height-1.001,e*(this.height-1))),h=Math.floor(s),o=Math.floor(n),r=s-h,l=n-o,c=o*this.width+h,m=c+this.width,d=t=>(t[c]*(1-r)+t[c+1]*r)*(1-l)+(t[m]*(1-r)+t[m+1]*r)*l;i[0]=d(this.offsetX)*a(t,e,this.width/this.height)*.08,i[1]=d(this.offsetY)*a(t,e,this.width/this.height)*.08,i[2]="dissolve"===this.mode?6*-d(this.ink):0}},r=class{constructor(t,e){this.rings=[],this.previous=null,this.elapsed=1,this.echoPeak=0,this.width=t,this.height=e,this.pixels=new Uint8Array(t*e*4),this.echo=new Float32Array(t*e)}get active(){return this.rings.length>0||this.echoPeak>2e-4}clear(){this.rings=[],this.previous=null,this.elapsed=1,this.pixels.fill(0),this.echo.fill(0),this.echoPeak=0}leave(){this.previous=null}move(t,e,i){const s=Math.max(1,this.width/this.height),n=Math.max(1,this.height/this.width);this.previous&&(Math.hypot((t-this.previous.x)*s,(e-this.previous.y)*n)<.065||this.elapsed<.3)||4!==this.rings.length&&(this.previous={x:t,y:e},this.elapsed=0,this.rings.push({x:t,y:e,age:0,radius:i}))}ringSample(t,e){const i=Math.max(1,this.width/this.height),s=Math.max(1,this.height/this.width);let n=0;for(const h of this.rings){const a=.015+h.age*(.15+.22*h.radius),o=.009+.014*h.radius,r=(Math.hypot((t-h.x)*i,(e-h.y)*s)-a)/o,l=Math.sin(Math.min(1,h.age/.07)*Math.PI/2)*Math.max(0,1-h.age/1.35)**2;n=Math.max(n,Math.exp(-r*r)*l)}return n}sample(t,e){const i=Math.max(0,Math.min(this.width-1,Math.round(t*(this.width-1)))),s=Math.max(0,Math.min(this.height-1,Math.round(e*(this.height-1))));return Math.max(this.ringSample(t,e),this.echo[s*this.width+i])}step(t){if(!Number.isFinite(t))return;const e=Math.max(0,Math.min(.05,t));this.elapsed+=e;for(const t of this.rings)t.age+=e;if(this.rings=this.rings.filter(t=>t.age<1.35),!this.active)return void this.pixels.fill(0);const i=Math.exp(-5.5*e);let s=0;for(let t=0;t<this.height;t++)for(let e=0;e<this.width;e++){const n=t*this.width+e;this.echo[n]=Math.max(this.ringSample(e/(this.width-1),t/(this.height-1)),this.echo[n]*i),s=Math.max(s,this.echo[n]);const h=Math.round(65535*this.echo[n]),a=4*n;this.pixels[a]=h>>>8,this.pixels[a+1]=255&h}this.echoPeak=s,this.active||(this.echo.fill(0),this.pixels.fill(0))}},l=class{constructor(t,e){this.previous=null,this.remainder=0,this.peak=0,this.width=t,this.height=e;const i=t*e;this.pixels=new Uint8Array(4*i),this.u=new Float32Array(i),this.v=new Float32Array(i),this.ink=new Float32Array(i),this.a=new Float32Array(i),this.b=new Float32Array(i),this.c=new Float32Array(i),this.pressure=new Float32Array(i),this.pressureNext=new Float32Array(i),this.divergence=new Float32Array(i),this.curl=new Float32Array(i)}get active(){return this.peak>2e-4}clear(){for(const t of[this.u,this.v,this.ink,this.a,this.b,this.c,this.pressure,this.pressureNext,this.divergence,this.curl,this.pixels])t.fill(0);this.previous=null,this.peak=0,this.remainder=0}leave(){this.previous=null}move(t,e,i){const s=this.previous;if(this.previous={x:t,y:e},!s)return;const n=(t-s.x)*(this.width-1),h=(e-s.y)*(this.height-1),a=Math.hypot(n,h);if(a<.001)return;const o=Math.min(this.width,this.height)*(.038+.12*i),r=Math.min(128,Math.max(1,Math.ceil(a/(.5*o)))),l=a/o*.95/r;for(let i=0;i<r;i++){const a=(i+.5)/r,c=(s.x+(t-s.x)*a)*(this.width-1),m=(s.y+(e-s.y)*a)*(this.height-1),d=Math.max(1,Math.floor(c-2.5*o)),u=Math.min(this.width-2,Math.ceil(c+2.5*o)),f=Math.max(1,Math.floor(m-2.5*o)),g=Math.min(this.height-2,Math.ceil(m+2.5*o));for(let t=f;t<=g;t++)for(let e=d;e<=u;e++){const i=Math.exp(-((e-c)**2+(t-m)**2)/(o*o)),s=t*this.width+e;this.ink[s]=Math.min(1,this.ink[s]+i*l),this.u[s]=Math.max(-50,Math.min(50,this.u[s]+n*i*5/r)),this.v[s]=Math.max(-50,Math.min(50,this.v[s]+h*i*5/r))}}this.peak=1}read(t,e,i){const s=0|(e=Math.max(.5,Math.min(this.width-1.5,e))),n=0|(i=Math.max(.5,Math.min(this.height-1.5,i))),h=e-s,a=i-n,o=n*this.width+s;return(t[o]*(1-h)+t[o+1]*h)*(1-a)+(t[o+this.width]*(1-h)+t[o+this.width+1]*h)*a}sample(t,e){return this.read(this.ink,t*(this.width-1),e*(this.height-1))}step(t){if(!this.active||!Number.isFinite(t))return;this.remainder+=Math.max(0,Math.min(.05,t));const e=1/60,i=this.width,s=this.height;for(;this.remainder+1e-8>=e;){this.remainder-=e;for(let t=1;t<s-1;t++)for(let s=1;s<i-1;s++){const n=t*i+s,h=s-this.u[n]*e,a=t-this.v[n]*e;this.a[n]=.972*this.read(this.u,h,a),this.b[n]=.972*this.read(this.v,h,a),this.c[n]=.964*this.read(this.ink,h,a)}[this.u,this.a]=[this.a,this.u],[this.v,this.b]=[this.b,this.v],[this.ink,this.c]=[this.c,this.ink];for(let t=1;t<s-1;t++)for(let e=1;e<i-1;e++){const s=t*i+e;this.curl[s]=.5*(this.v[s+1]-this.v[s-1]-this.u[s+i]+this.u[s-i])}for(let t=1;t<s-1;t++)for(let s=1;s<i-1;s++){const n=t*i+s,h=Math.abs(this.curl[n+1])-Math.abs(this.curl[n-1]),a=Math.abs(this.curl[n+i])-Math.abs(this.curl[n-i]),o=Math.hypot(h,a)+1e-4,r=this.curl[n]*e*5;this.u[n]+=a/o*r,this.v[n]-=h/o*r,this.divergence[n]=-.5*(this.u[n+1]-this.u[n-1]+this.v[n+i]-this.v[n-i])}this.pressure.fill(0);for(let t=0;t<4;t++){for(let t=1;t<s-1;t++)for(let e=1;e<i-1;e++){const s=t*i+e;this.pressureNext[s]=.25*(this.divergence[s]+this.pressure[s-1]+this.pressure[s+1]+this.pressure[s-i]+this.pressure[s+i])}[this.pressure,this.pressureNext]=[this.pressureNext,this.pressure]}let t=0;for(let e=1;e<s-1;e++)for(let s=1;s<i-1;s++){const n=e*i+s;this.u[n]=Math.max(-50,Math.min(50,this.u[n]-.5*(this.pressure[n+1]-this.pressure[n-1]))),this.v[n]=Math.max(-50,Math.min(50,this.v[n]-.5*(this.pressure[n+i]-this.pressure[n-i]))),t=Math.max(t,this.ink[n])}this.peak=t}if(this.active)for(let t=0;t<this.ink.length;t++){const e=Math.round(65535*Math.min(1,this.ink[t]));this.pixels[4*t]=e>>>8,this.pixels[4*t+1]=255&e}else this.clear()}},c=t=>["trail","contour","dissolve"].includes(t),m=class{constructor(t=1){this.mode="water",this.amount=.55,this.radius=.2,this.lensX=.5,this.lensY=.5,this.lensStrength=0,this.previous=null,this.energy=0,this.pending=!1,this.remainder=0,t=Math.max(.25,Math.min(4,t)),this.columns=Math.round(t>=1?128:128*t),this.rows=Math.round(t>=1?128/t:128),this.xScale=Math.max(1,t),this.yScale=Math.max(1,1/t),this.heights=new Float32Array(this.columns*this.rows),this.velocity=new Float32Array(this.heights.length),this.next=new Float32Array(this.heights.length),this.normals=new Float32Array(2*this.heights.length),this.edgeMask=new Float32Array(this.heights.length);for(let e=0;e<this.rows;e++)for(let i=0;i<this.columns;i++)this.edgeMask[e*this.columns+i]=a(i/(this.columns-1),e/(this.rows-1),t);this.refraction={width:this.columns,height:this.rows,pixels:new Uint8Array(4*this.heights.length),strength:30,mode:"water",focus:[.5,.5,0,.2]},this.waterPixels=this.refraction.pixels,this.trail=new l(this.columns,this.rows),this.contour=new r(this.columns,this.rows),this.afterimage=new o(this.columns,this.rows),this.clear()}get densityTrail(){return["trail","contour","dissolve"].includes(this.mode)}get hasRefraction(){return this.active||this.lensStrength>15e-5}get active(){return["dissolve","silk","vortex"].includes(this.mode)?this.afterimage.active:"contour"===this.mode?this.contour.active:"trail"===this.mode?this.trail.active:this.pending||this.energy>15e-5}setMode(t){t!==this.mode&&(this.clear(),this.mode=t,this.refraction.mode=t,"dissolve"!==t&&"silk"!==t&&"vortex"!==t||this.afterimage.setMode(t),this.refraction.pixels="trail"===t?this.trail.pixels:"contour"===t?this.contour.pixels:["dissolve","silk","vortex"].includes(t)?this.afterimage.pixels:this.waterPixels,this.refraction.strength="water"===t?30*this.amount/.55:"silk"===t?28*this.amount:"vortex"===t?38*this.amount:0)}configure(t,e=.55,i=.2){this.setMode(t),this.amount=Number.isFinite(e)?Math.max(0,Math.min(1,e)):.55,this.radius=Number.isFinite(i)?Math.max(.1,Math.min(1,i)):.2,this.refraction.strength="water"===t?30*this.amount/.55:"silk"===t?28*this.amount:"vortex"===t?38*this.amount:0,this.refraction.focus[2]=this.densityTrail?this.amount:this.lensStrength*this.amount,this.refraction.focus[3]=this.radius}move(t,e){if("none"===this.mode||0===this.amount)return;if(!Number.isFinite(t+e))return;if(t=Math.max(0,Math.min(1,t)),e=Math.max(0,Math.min(1,e)),["dissolve","silk","vortex"].includes(this.mode))return void this.afterimage.move(t,e,this.radius);if("contour"===this.mode)return void this.contour.move(t,e,this.radius);if("trail"===this.mode)return void this.trail.move(t,e,this.radius);if("water"!==this.mode)return!this.previous&&this.lensStrength<.001&&(this.lensX=t,this.lensY=e),this.previous={x:t,y:e},void(this.pending=!0);if(!this.previous)return void(this.previous={x:t,y:e});const i=this.previous;this.previous={x:t,y:e};const s=(t-i.x)*this.xScale,n=(e-i.y)*this.yScale,h=Math.hypot(s,n);if(h<1e-5)return;const a=.035+.1*this.radius,o=3*a,r=Math.min(192,Math.max(1,Math.ceil(h/(.5*a)))),l=95*Math.min(h,1)/r;for(let s=0;s<r;s++){const n=(s+.5)/r,h=i.x+(t-i.x)*n,c=i.y+(e-i.y)*n,m=Math.max(1,Math.floor((h-o/this.xScale)*(this.columns-1))),d=Math.min(this.columns-2,Math.ceil((h+o/this.xScale)*(this.columns-1))),u=Math.max(1,Math.floor((c-o/this.yScale)*(this.rows-1))),f=Math.min(this.rows-2,Math.ceil((c+o/this.yScale)*(this.rows-1)));for(let t=u;t<=f;t++)for(let e=m;e<=d;e++){const i=(e/(this.columns-1)-h)*this.xScale,s=(t/(this.rows-1)-c)*this.yScale,n=Math.exp(-(i*i+s*s)/(a*a))*l,o=t*this.columns+e;this.velocity[o]=Math.max(-12,this.velocity[o]-n)}}this.pending=!0}leave(){this.afterimage.leave(),this.contour.leave(),this.trail.leave(),this.previous=null,"water"!==this.mode&&this.lensStrength&&(this.pending=!0)}clear(){this.trail.clear(),this.contour.clear(),this.afterimage.clear(),this.heights.fill(0),this.velocity.fill(0),this.next.fill(0),this.normals.fill(0),this.refraction.focus[2]=0,this.previous=null,this.lensStrength=0,this.energy=0,this.pending=!1,this.remainder=0;for(let t=0;t<this.waterPixels.length;t+=4)this.waterPixels[t]=this.waterPixels[t+2]=128,this.waterPixels[t+1]=this.waterPixels[t+3]=0}step(t){if(!this.active||!Number.isFinite(t))return;if(["dissolve","silk","vortex"].includes(this.mode))return void this.afterimage.step(t);if("contour"===this.mode)return void this.contour.step(t);if("trail"===this.mode)return void this.trail.step(t);if("water"!==this.mode)return void this.stepLight(Math.max(0,Math.min(.05,t)));this.remainder+=Math.max(0,Math.min(.05,t));const e=1/120,i=Math.exp(-3.8*e),s=.3*Math.min(this.columns,this.rows);for(;this.remainder+1e-8>=e;){this.remainder-=e;let t=0;for(let n=1;n<this.rows-1;n++)for(let h=1;h<this.columns-1;h++){const a=n*this.columns+h,o=this.heights[a],r=this.heights[a-1]+this.heights[a+1]+this.heights[a-this.columns]+this.heights[a+this.columns]-4*o,l=Math.min(h,n,this.columns-1-h,this.rows-1-n),c=l<5?.88+.024*l:1,m=(this.velocity[a]+(r*s*s-8*o)*e)*i*c;this.velocity[a]=m,this.next[a]=(o+m*e)*c,t=Math.max(t,Math.abs(this.next[a]),.1*Math.abs(m))}this.heights.set(this.next),this.energy=t,this.pending=!1}if(!this.active)return void this.clear();const n=.18*Math.min(this.columns,this.rows);for(let t=0;t<this.rows;t++)for(let e=0;e<this.columns;e++){const i=t*this.columns+e,s=(this.heights[t*this.columns+Math.max(0,e-1)]-this.heights[t*this.columns+Math.min(this.columns-1,e+1)])*n,h=(this.heights[Math.max(0,t-1)*this.columns+e]-this.heights[Math.min(this.rows-1,t+1)*this.columns+e])*n,a=Math.tanh(s)*this.edgeMask[i],o=Math.tanh(h)*this.edgeMask[i];this.normals[2*i]=a,this.normals[2*i+1]=o;const r=Math.round(32767*a+32768),l=Math.round(32767*o+32768);this.refraction.pixels[4*i]=r>>>8,this.refraction.pixels[4*i+1]=255&r,this.refraction.pixels[4*i+2]=l>>>8,this.refraction.pixels[4*i+3]=255&l}}stepLight(t){const e=this.previous,i=1-Math.exp(-30*t),s=1-Math.exp(-12*t);e&&(this.lensX+=(e.x-this.lensX)*i,this.lensY+=(e.y-this.lensY)*i),this.lensStrength+=((e?1:0)-this.lensStrength)*s,this.energy=Math.max(e?Math.abs(e.x-this.lensX)+Math.abs(e.y-this.lensY):0,Math.abs((e?1:0)-this.lensStrength)),this.pending=!1,e||this.active?this.refraction.focus=[this.lensX,this.lensY,this.lensStrength*this.amount,this.radius]:this.clear()}sample(t,e,i){if(["dissolve","silk","vortex"].includes(this.mode))return this.afterimage.sample(t,e,i),i[0]*=this.amount,i[1]*=this.amount,void(i[2]*=this.amount);if("contour"===this.mode)return i[0]=i[1]=0,void(i[2]=this.contour.sample(t,e)*this.amount*3);if("trail"===this.mode)return i[0]=i[1]=0,void(i[2]=this.trail.sample(t,e)*this.amount*3);const s=Math.max(0,Math.min(this.columns-1.001,t*(this.columns-1))),n=Math.max(0,Math.min(this.rows-1.001,e*(this.rows-1))),h=Math.floor(s),a=Math.floor(n),o=s-h,r=n-a,l=2*(a*this.columns+h),c=l+2*this.columns;for(let t=0;t<2;t++)i[t]=.08*((this.normals[l+t]*(1-o)+this.normals[l+2+t]*o)*(1-r)+(this.normals[c+t]*(1-o)+this.normals[c+2+t]*o)*r);i[0]*=this.amount/.55,i[1]*=this.amount/.55,i[2]="light"===this.mode||"scan"===this.mode?function(t,e,i,s){const[n,h,a,o]=i.focus,r=(t-n)*Math.max(1,s)/o,l=(e-h)*Math.max(1,1/s)/o;return"light"===i.mode?Math.exp(-3*(r*r+l*l))*a:"scan"===i.mode?(1.3*Math.exp(-r*r*110)-.3*Math.exp(-r*r*12))*Math.exp(-l*l*1.8)*a:0}(t,e,this.refraction,this.xScale/this.yScale):0}};function d(t){const e=Intl.Segmenter;return e?Array.from(new e(void 0,{granularity:"grapheme"}).segment(t),t=>t.segment):Array.from(t)}function u(){const t=document.createElement("canvas"),e=document.createElement("canvas"),i=t.getContext("2d"),s=e.getContext("2d",{willReadFrequently:!0});let n,h=null;return{update(a,o,r,l,c,m,u,f){const g=u?.[0]?.length??Math.max(1,Math.round(o/l)),p=u?.length??Math.max(1,Math.round(r/c)),x=f??{charset:" .:-=+*#%@",customText:"",colorMode:"fullcolor"},v=x.charset+(x.charsetFrames?.join("")??""),M=function(t,e,i,s,n,h,a,o=4096,r=0){if(![i,s,n,h,a,o].every(t=>Number.isFinite(t)&&t>0))throw new Error("Glyph atlas dimensions must be positive and finite.");const l=JSON.stringify([e.charset,e.customText,i,s,n,h,a,o,r]);if(t?.key===l)return t;const c=[...new Set([" ",...d(e.charset),...d(e.customText??"")])];if(c.length>65536)throw new Error("This character set exceeds the glyph texture capacity.");const m=i/h,u=s/a,f=.9*Math.min(m/.55,u),g=Math.ceil(m*n)+4,p=Math.ceil(u*n)+4,x=Math.min(c.length,Math.floor(o/g));if(x<1||Math.ceil(c.length/x)*p>o)throw new Error("The glyph atlas exceeds the available GPU texture size.");return{key:l,glyphs:c,indices:new Map(c.map((t,e)=>[t,e])),cellWidth:m,cellHeight:u,fontSize:f,padding:2,tileWidth:g,tileHeight:p,atlasColumns:x,atlasWidth:x*g,atlasHeight:Math.ceil(c.length/x)*p}}(h,{...x,charset:v},o,r,m,g,p);if(M!==h&&(t.width=M.atlasWidth,t.height=M.atlasHeight,i.font=M.fontSize*m+'px "JetBrains Mono", monospace',i.textAlign="center",i.textBaseline="middle",i.fillStyle="#fff",M.glyphs.forEach((t,e)=>i.fillText(t,e%M.atlasColumns*M.tileWidth+M.padding+M.cellWidth*m*.5,Math.floor(e/M.atlasColumns)*M.tileHeight+M.padding+M.cellHeight*m*.5))),h=M,u){n=function(t,e,i,s,n=!1){const h=t.length,a=t[0]?.length??0,o=s?.cols===a&&s.rows===h?s:{cols:a,rows:h,indices:new Uint8Array(a*h*4),colors:new Uint8ClampedArray(a*h*4)};o.indices.fill(0),o.colors.fill(0);const r="accent"===i.colorMode?[1,3,5].map(t=>parseInt(i.accentColor.slice(t,t+2),16)):null;for(let s=0;s<h;s++){if(t[s].length!==a)throw new Error("Glyph frames must have a rectangular cell grid.");for(let h=0;h<a;h++){const l=t[s][h];if(!l.char||!n&&" "===l.char||l.a<10)continue;const c=e.get(l.char);if(void 0===c)throw new Error(`Glyph is absent from the configured character set: ${l.char}`);const m=4*(s*a+h);let d=l.r,u=l.g,f=l.b;if(r)[d,u,f]=r;else if("fullcolor"!==i.colorMode){const t=.299*d+.587*u+.114*f|0;d="matrix"===i.colorMode?0:t,u=t,f=d}(n||0!==d||0!==u||0!==f)&&(o.indices[m]=255&c,o.indices[m+1]=c>>>8,o.colors[m]=d,o.colors[m+1]=u,o.colors[m+2]=f,o.colors[m+3]=l.a)}}return o}(u,h.indices,x,n,!0);for(let t=0;t<p;t++)for(let e=0;e<g;e++){const i=u[t][e],s=4*(t*g+e);n.indices[s+2]=Math.round(.299*i.r+.587*i.g+.114*i.b)}}else{e.width=g,e.height=p,s.drawImage(a,0,0,g,p);const t=s.getImageData(0,0,g,p).data;n={cols:g,rows:p,indices:new Uint8Array(g*p*4),colors:new Uint8ClampedArray(t)};for(let e=0;e<t.length;e+=4){const i=Math.min(1,(.299*t[e]+.587*t[e+1]+.114*t[e+2])/255*(t[e+3]/255)*3);n.indices[e+2]=Math.round(255*i),n.indices[e]=Math.min(h.glyphs.length-1,Math.floor(i*h.glyphs.length))}}return{plan:h,packed:n,atlas:t,dots:"dots"===x.renderMode,dotSize:x.dotSizeRatio??.72}},destroy(){t.width=t.height=e.width=e.height=1,n=void 0,h=null}}}function f(t,i,a={}){let o={effect:"trail",strength:.55,radius:.2,animated:!1,fps:30,fontSize:7,charAspect:.58,charSpacing:1,paused:!1,charset:" .:-=+*#%@",customText:"",renderMode:"ascii",dotSizeRatio:.72,...a},r=new m(i.clientWidth/Math.max(1,i.clientHeight)),l=!1,d=!1,f=!0,g=0,p=0,x=-1/0,v=0,M=null,w=!1;const y=matchMedia("(prefers-reduced-motion: reduce)"),S=()=>{l||!d||document.hidden||g||(g=requestAnimationFrame(b))},E=function(t,i,a){const o=i.style.visibility;let r=document.createElement("canvas"),l=null,m=null,d=null,f=null;const g=[],p=[];let x=!1,v=!1,M=4096;const w=document.createElement("canvas"),y=document.createElement("canvas"),S=w.getContext("2d"),E=y.getContext("2d");let b=!1;const T=new n,A=u();let k=null,R={};const z=()=>{r.className="ascii-hover-surface",r.style.cssText="position:absolute;inset:0;width:100%;height:100%;pointer-events:none",r.setAttribute("role","img"),r.setAttribute("aria-label","ASCII artwork preview"),r.dataset.renderer=l?"surface-gpu":"surface-canvas",t.prepend(r),i.style.visibility="hidden"},_=()=>{l&&(g.forEach(t=>l.deleteTexture(t)),p.forEach(t=>l.deleteShader(t)),d&&l.deleteProgram(d),f&&l.deleteBuffer(f))},D=()=>{x||m||(r.removeEventListener("webglcontextlost",D),_(),r.remove(),l=null,r=document.createElement("canvas"),m=r.getContext("2d"),z(),a())};try{if(l=r.getContext("webgl",{alpha:!0,antialias:!1,depth:!1,stencil:!1,premultipliedAlpha:!0,preserveDrawingBuffer:!0}),!l)throw new Error("WebGL unavailable");M=l.getParameter(l.MAX_TEXTURE_SIZE);const t=(t,e)=>{const i=l.createShader(t);if(p.push(i),l.shaderSource(i,e),l.compileShader(i),!l.getShaderParameter(i,l.COMPILE_STATUS))throw new Error(l.getShaderInfoLog(i)||"Shader compilation failed");return i};if(d=l.createProgram(),l.attachShader(d,t(l.VERTEX_SHADER,"attribute vec2 position; void main(){gl_Position=vec4(position,0.,1.);}")),l.attachShader(d,t(l.FRAGMENT_SHADER,"\n precision highp float;\n uniform sampler2D artwork; uniform sampler2D surface;\n uniform vec2 resolution; uniform vec2 sceneSize; uniform vec2 cellSize;\n uniform vec2 surfaceSize; uniform float strength;\n \nuniform float hoverMode;\nuniform vec4 hoverFocus;\nfloat surfaceLight(vec2 uv, vec2 size) {\n vec2 delta = (uv - hoverFocus.xy) * size / min(size.x, size.y) / hoverFocus.w;\n float light = exp(-3.0 * dot(delta, delta));\n if (hoverMode > 1.5) light = (exp(-delta.x * delta.x * 110.0) * 1.3 - exp(-delta.x * delta.x * 12.0) * .3) * exp(-delta.y * delta.y * 1.8);\n return hoverMode > .5 ? light * hoverFocus.z : 0.0;\n}\nvec3 illuminate(vec3 ink, float light) {\n return mix(ink, vec3(1.0), max(0.0, light) * .82) * (1.0 + min(0.0, light));\n}\n\n uniform sampler2D codes; uniform sampler2D cellColors; uniform sampler2D glyphAtlas;\n uniform vec2 gridSize; uniform vec2 atlasSize; uniform vec2 tileSize;\n uniform float atlasColumns; uniform float padding; uniform float glyphCount;\n uniform float dots; uniform float dotSize;\n \nuniform float trailAmount;\nuniform float trailKind;\nfloat trailDensity(vec2 address) {\n vec2 encodedInk = texture2D(surface, (address * (surfaceSize - 1.0) + .5) / surfaceSize).rg;\n return (encodedInk.r * 65280.0 + encodedInk.g * 255.0) / 65535.0 * trailAmount;\n}\nvec3 trailInk(vec3 ink, float density) {\n float peak=max(ink.r,max(ink.g,ink.b));\n float raised=trailKind>1.5?peak:max(peak,min(.44,density*.75));\n return peak>.001 ? ink*(raised/peak) : vec3(raised);\n}\nfloat trailIndex(float index, float count, float density, vec2 cell) {\n // A stable cell pattern gives Dissolve a granular edge without random flicker.\n float grain=fract((cell.x*cell.x*17.0+cell.y*cell.y*23.0+cell.x*cell.y*19.0)*.0137);\n if(trailKind>1.5) return max(0.0,index-floor(density*count*(1.35+grain*.9)));\n return min(count - 1.0, index + floor(density * count * .85));\n}\n\n \n uniform float motionMode; uniform float motionTime; uniform float fineDither; uniform float ditherAmount;\n float printGrain(vec2 p) { return fract(52.9829189*fract(dot(p,vec2(.06711056,.00583715)))); }\n vec4 ambientAt(vec2 uv) {\n float p=motionTime*.5235987756;\n float edge=smoothstep(0.,.12,min(min(uv.x,1.-uv.x),min(uv.y,1.-uv.y)));\n if(motionMode<.5) return vec4(0.,0.,0.,1.);\n if(motionMode<1.5) return vec4(0.,0.,sin(uv.x*9.+uv.y*5.+p)*cos(uv.y*8.-uv.x*3.-p)*.065*edge,.9+.1*sin(uv.x*6.-uv.y*4.+p));\n if(motionMode<2.5) return vec4(sin(uv.y*9.+p)*cos(uv.x*7.-p)*6.*edge,cos(uv.x*8.+p)*sin(uv.y*6.-p)*4.*edge,0.,1.);\n float cycle=mod(motionTime,12.);\n float order=uv.x*.8+uv.y*.2+sin(uv.x*8.-uv.y*5.)*.08;\n float gone=smoothstep(1.8+order*1.2,3.8+order*1.2,cycle)*(1.-smoothstep(5.+order*1.7,7.2+order*1.7,cycle));\n return vec4(0.,0.,-gone*.95,1.-gone*.82);\n }\n\n float toneAt(vec2 uv) {\n vec2 p=uv*gridSize-.5, b=floor(p), f=fract(p);\n return mix(mix(texture2D(codes,(b+.5)/gridSize).b,texture2D(codes,(b+vec2(1.5,.5))/gridSize).b,f.x),mix(texture2D(codes,(b+vec2(.5,1.5))/gridSize).b,texture2D(codes,(b+1.5)/gridSize).b,f.x),f.y);\n }\n void main(){\n vec2 uv = vec2(gl_FragCoord.x, resolution.y - gl_FragCoord.y) / resolution;\n vec2 pixel = uv * sceneSize;\n if (strength > 0.) {\n vec4 normal = texture2D(surface, (uv * (surfaceSize - 1.) + .5) / surfaceSize) * 255.;\n vec2 slope = (vec2(normal.r * 256. + normal.g, normal.b * 256. + normal.a) - 32768.) / 32767.;\n vec2 edge = min(pixel, sceneSize - pixel) / cellSize;\n uv -= slope * strength * smoothstep(2.,8.,min(edge.x,edge.y)) / sceneSize;\n }\n vec4 ambient=ambientAt(uv);\n uv -= ambient.xy/sceneSize;\n pixel=uv*sceneSize;\n vec4 ink = texture2D(artwork, uv);\n if (fineDither > .5) {\n vec2 cell=floor(pixel/cellSize),address=(cell+.5)/gridSize;\n vec4 color=texture2D(cellColors,address);\n float density=trailAmount>0.?trailDensity(address):0.;\n float tone=clamp(toneAt(uv)+ambient.z+(trailKind>1.5?-density:density)*.65,0.,1.);\n float pitch=max(1.,min(cellSize.x,cellSize.y)*.32);\n float grain=.5+(printGrain(floor(pixel/pitch))-.5)*ditherAmount;\n float alpha=(tone<.001?0.:tone>.999?1.:smoothstep(grain-.035,grain+.035,tone))*color.a*ambient.w;\n // Preserve hue; tone is carried by mark coverage rather than muddy ink.\n vec3 hue=color.rgb/max(.001,max(color.r,max(color.g,color.b)));\n gl_FragColor=vec4(hue*alpha,alpha); return;\n }\n if (trailAmount > 0. || motionMode > .5) {\n vec2 cell=floor(pixel/cellSize), address=(cell+.5)/gridSize;\n float density=trailAmount>0.?trailDensity(address):0.;\n vec4 encoded=texture2D(codes,address);\n float original=floor(encoded.r*255.+.5)+floor(encoded.g*255.+.5)*256.;\n float index=trailIndex(original,glyphCount,density,cell);\n if(original>.5) index=clamp(floor(index+ambient.z*(glyphCount-1.)+printGrain(cell)),0.,glyphCount-1.);\n vec4 color=texture2D(cellColors,address);\n if((index!=original || motionMode>.5) && color.a>.01) {\n vec2 local=(pixel-cell*cellSize)*resolution/sceneSize;\n vec2 slot=vec2(mod(index,atlasColumns),floor(index/atlasColumns));\n float alpha=texture2D(glyphAtlas,(slot*tileSize+padding+local)/atlasSize).a;\n if(dots>.5) {\n vec2 d=(pixel-(cell+.5)*cellSize)/min(cellSize.x,cellSize.y);\n float r=min(.48,(index/(glyphCount-1.))*.5*dotSize);\n alpha=1.-smoothstep(r-.06,r+.02,length(d));\n }\n // Replace the cell, rather than blending two glyphs into a ghost.\n vec3 rgb=trailInk(color.rgb,density);\n ink=vec4(rgb*alpha*color.a,alpha*color.a);\n }\n }\n vec3 straightInk = ink.a > .0 ? ink.rgb / ink.a : vec3(.0);\n float value = max(straightInk.r, max(straightInk.g, straightInk.b));\n // Lighting belongs to character ink; black/transparent gaps stay empty.\n float light = surfaceLight((floor(pixel / cellSize) + .5) * cellSize / sceneSize, sceneSize);\n vec3 lit = illuminate(straightInk, light * smoothstep(0., .22, value)) * ink.a;\n gl_FragColor = vec4(lit, ink.a)*ambient.w;\n }")),l.linkProgram(d),!l.getProgramParameter(d,l.LINK_STATUS))throw new Error("Surface shader link failed");l.useProgram(d),f=l.createBuffer(),l.bindBuffer(l.ARRAY_BUFFER,f),l.bufferData(l.ARRAY_BUFFER,new Float32Array([-1,-1,1,-1,-1,1,1,1]),l.STATIC_DRAW);const e=l.getAttribLocation(d,"position");l.enableVertexAttribArray(e),l.vertexAttribPointer(e,2,l.FLOAT,!1,0,0),R=Object.fromEntries(["artwork","surface","resolution","sceneSize","cellSize","surfaceSize","strength","hoverMode","hoverFocus","codes","cellColors","glyphAtlas","gridSize","atlasSize","tileSize","atlasColumns","padding","glyphCount","dots","dotSize","trailKind","trailAmount","motionMode","motionTime","fineDither","ditherAmount"].map(t=>[t,l.getUniformLocation(d,t)]));for(let t=0;t<5;t++){const e=l.createTexture();g.push(e),l.activeTexture(l.TEXTURE0+t),l.bindTexture(l.TEXTURE_2D,e),l.texParameteri(l.TEXTURE_2D,l.TEXTURE_MIN_FILTER,l.LINEAR),l.texParameteri(l.TEXTURE_2D,l.TEXTURE_MAG_FILTER,l.LINEAR),l.texImage2D(l.TEXTURE_2D,0,l.RGBA,1,1,0,l.RGBA,l.UNSIGNED_BYTE,new Uint8Array(4)),l.texParameteri(l.TEXTURE_2D,l.TEXTURE_WRAP_S,l.CLAMP_TO_EDGE),l.texParameteri(l.TEXTURE_2D,l.TEXTURE_WRAP_T,l.CLAMP_TO_EDGE)}l.uniform1i(R.codes,2),l.uniform1i(R.cellColors,3),l.uniform1i(R.glyphAtlas,4),l.uniform1i(R.artwork,0),l.uniform1i(R.surface,1),l.pixelStorei(l.UNPACK_PREMULTIPLY_ALPHA_WEBGL,!1),l.disable(l.BLEND),l.disable(l.DEPTH_TEST),r.addEventListener("webglcontextlost",D),z()}catch{D()}return{get canvas(){return r},render(t,n,a,o,d,u,f,p,z=0){const _=e(p?.animationStyle),C=T.update(z,_,p?.animationSpeed),F=(t=>"terminal"===t?.artStyle&&!t.customText&&!t.charsetFrames?.length&&!/[A-Za-z]/.test(t.charset??""))(p);if(x||!a||!o||!i.width||!i.height)return;const I=Math.min(i.width/a,1.5,Math.sqrt(4e6/(a*o))),P=Math.max(1,Math.round(a*I)),U=Math.max(1,Math.round(o*I));if(r.width===P&&r.height===U||(r.width=P,r.height=U,v=!1),l&&(i.width>M||i.height>M)&&D(),(c(n.mode)||"none"!==_||F)&&(t||!v||!k)&&(k=A.update(i,a,o,d,u,I,f,p),l)){const{plan:t,packed:e,atlas:i}=k;for(const[t,i]of[[2,e.indices],[3,e.colors]])l.activeTexture(l.TEXTURE0+t),l.bindTexture(l.TEXTURE_2D,g[t]),l.texParameteri(l.TEXTURE_2D,l.TEXTURE_MIN_FILTER,l.NEAREST),l.texParameteri(l.TEXTURE_2D,l.TEXTURE_MAG_FILTER,l.NEAREST),l.texImage2D(l.TEXTURE_2D,0,l.RGBA,e.cols,e.rows,0,l.RGBA,l.UNSIGNED_BYTE,i);l.activeTexture(l.TEXTURE4),l.bindTexture(l.TEXTURE_2D,g[4]),l.texImage2D(l.TEXTURE_2D,0,l.RGBA,l.RGBA,l.UNSIGNED_BYTE,i),l.uniform2f(R.gridSize,e.cols,e.rows),l.uniform2f(R.atlasSize,t.atlasWidth,t.atlasHeight),l.uniform2f(R.tileSize,t.tileWidth,t.tileHeight),l.uniform1f(R.atlasColumns,t.atlasColumns),l.uniform1f(R.padding,t.padding),l.uniform1f(R.glyphCount,t.glyphs.length),l.uniform1f(R.dots,k.dots?1:0),l.uniform1f(R.dotSize,k.dotSize)}if(l)l.uniform1f(R.motionMode,(t=>["none","print","current","reform"].indexOf(t))(_)),l.uniform1f(R.motionTime,C),l.uniform1f(R.fineDither,F?1:0),l.uniform1f(R.ditherAmount,Math.max(0,Math.min(2,p?.ditherStrength??1))),l.uniform1f(R.trailKind,"dissolve"===n.mode?2:0),l.uniform1f(R.trailAmount,c(n.mode)?n.focus[2]:0),l.viewport(0,0,P,U),!t&&v||(l.activeTexture(l.TEXTURE0),l.bindTexture(l.TEXTURE_2D,g[0]),l.pixelStorei(l.UNPACK_PREMULTIPLY_ALPHA_WEBGL,!0),l.texImage2D(l.TEXTURE_2D,0,l.RGBA,l.RGBA,l.UNSIGNED_BYTE,i),l.pixelStorei(l.UNPACK_PREMULTIPLY_ALPHA_WEBGL,!1),v=!0),l.activeTexture(l.TEXTURE1),l.bindTexture(l.TEXTURE_2D,g[1]),l.texImage2D(l.TEXTURE_2D,0,l.RGBA,n.width,n.height,0,l.RGBA,l.UNSIGNED_BYTE,n.pixels),l.uniform2f(R.resolution,P,U),l.uniform2f(R.sceneSize,a,o),l.uniform2f(R.cellSize,d,u),l.uniform2f(R.surfaceSize,n.width,n.height),l.uniform1f(R.strength,n.strength),l.uniform1f(R.hoverMode,"light"===n.mode?1:"scan"===n.mode?2:0),l.uniform4fv(R.hoverFocus,n.focus),l.drawArrays(l.TRIANGLE_STRIP,0,4);else if(m)if(m.setTransform(I,0,0,I,0,0),m.clearRect(0,0,a,o),m.drawImage(i,0,0,a,o),("none"!==_||F)&&k){m.clearRect(0,0,a,o);const{plan:t,packed:e,atlas:i}=k,r=[0,0,0,1],l=Math.max(1,.32*Math.min(d,u));for(let a=0;a<e.rows;a++)for(let o=0;o<e.cols;o++){const f=(o+.5)/e.cols,g=(a+.5)/e.rows,x=4*(a*e.cols+o);if(e.colors[x+3]<10)continue;h(_,f,g,C,r);const v=4*(Math.min(n.height-1,Math.round(g*(n.height-1)))*n.width+Math.min(n.width-1,Math.round(f*(n.width-1)))),M=c(n.mode)?(256*n.pixels[v]+n.pixels[v+1])/65535*n.focus[2]:0,w=e.indices[x]+256*e.indices[x+1];let y="dissolve"===n.mode?w-Math.floor(M*t.glyphs.length*1.8):w+Math.floor(M*t.glyphs.length*.85);w>0&&(y+=r[2]*(t.glyphs.length-1)),y=Math.max(0,Math.min(t.glyphs.length-1,Math.floor(y+s(o,a))));let S=r[0],E=r[1];n.strength>0&&(S+=(256*n.pixels[v]+n.pixels[v+1]-32768)/32767*n.strength,E+=(256*n.pixels[v+2]+n.pixels[v+3]-32768)/32767*n.strength);const b=o*d+S,T=a*u+E,A=e.colors[x+3]/255*r[3],R=F?Math.max(1,e.colors[x],e.colors[x+1],e.colors[x+2]):255,z=`rgb(${e.colors[x]/R*255},${e.colors[x+1]/R*255},${e.colors[x+2]/R*255})`;if(m.fillStyle=z,F){const t=Math.max(0,Math.min(1,e.indices[x+2]/255+r[2]+.65*("dissolve"===n.mode?-M:M)));m.globalAlpha=A;for(let e=0;e<u;e+=l)for(let i=0;i<d;i+=l)t>.5+(s(Math.floor((b+i)/l),Math.floor((T+e)/l))-.5)*Math.max(0,Math.min(2,p?.ditherStrength??1))&&m.fillRect(b+i,T+e,Math.min(l,d-i),Math.min(l,u-e))}else if(y>0)if(m.globalAlpha=A,k.dots){const e=Math.min(.48,y/(t.glyphs.length-1)*.5*k.dotSize)*Math.min(d,u);m.beginPath(),m.arc(b+.5*d,T+.5*u,e,0,2*Math.PI),m.fill()}else m.drawImage(i,y%t.atlasColumns*t.tileWidth+t.padding,Math.floor(y/t.atlasColumns)*t.tileHeight+t.padding,t.cellWidth*I,t.cellHeight*I,b,T,d,u),m.globalAlpha=1,m.globalCompositeOperation="source-atop",m.fillRect(b,T,d,u),m.globalCompositeOperation="source-over"}m.globalAlpha=1}else if(c(n.mode)&&k){const{plan:t,packed:e,atlas:i}=k;for(let s=0;s<e.rows;s++)for(let h=0;h<e.cols;h++){const a=4*(Math.min(n.height-1,Math.round((s+.5)/e.rows*(n.height-1)))*n.width+Math.min(n.width-1,Math.round((h+.5)/e.cols*(n.width-1)))),o=(256*n.pixels[a]+n.pixels[a+1])/65535*n.focus[2],r=4*(s*e.cols+h),l=.0137*(h*h*17+s*s*23+h*s*19),c=l-Math.floor(l),f=e.indices[r]+256*e.indices[r+1],g="dissolve"===n.mode?Math.max(0,f-Math.floor(o*t.glyphs.length*(1.35+.9*c))):Math.min(t.glyphs.length-1,f+Math.floor(o*t.glyphs.length*.85));if(g===f||e.colors[r+3]<10)continue;const p=h*d,x=s*u;if(m.clearRect(p,x,d,u),m.globalAlpha=e.colors[r+3]/255,k.dots){const e=Math.min(.48,g/(t.glyphs.length-1)*.5*k.dotSize)*Math.min(d,u);m.fillStyle="#fff",m.beginPath(),m.arc(p+.5*d,x+.5*u,e,0,2*Math.PI),m.fill()}else m.drawImage(i,g%t.atlasColumns*t.tileWidth+t.padding,Math.floor(g/t.atlasColumns)*t.tileHeight+t.padding,t.cellWidth*I,t.cellHeight*I,p,x,d,u);m.globalAlpha=1,m.globalCompositeOperation="source-atop";const v=Math.max(e.colors[r],e.colors[r+1],e.colors[r+2]),M="dissolve"===n.mode?v:Math.max(v,Math.min(112,191*o)),w=v?M/v:0;m.fillStyle=`rgb(${v?e.colors[r]*w:M},${v?e.colors[r+1]*w:M},${v?e.colors[r+2]*w:M})`,m.fillRect(p,x,d,u),m.globalCompositeOperation="source-over"}}else if("light"===n.mode||"scan"===n.mode){!t&&b&&w.width===P&&w.height===U||(w.width=P,w.height=U,y.width=P,y.height=U,S.filter="brightness(3)",S.drawImage(i,0,0,P,U),S.filter="none",b=!0);const[e,s,h,r]=n.focus;if(h>.001){E.setTransform(1,0,0,1,0,0),E.clearRect(0,0,P,U),E.drawImage(w,0,0),E.globalCompositeOperation="destination-in",E.save(),E.translate(e*P,s*U),E.scale("scan"===n.mode?.13:1,1);const t=r*Math.min(P,U),i=E.createRadialGradient(0,0,0,0,0,t);i.addColorStop(0,`rgba(255,255,255,${h})`),i.addColorStop(.5,`rgba(255,255,255,${.4*h})`),i.addColorStop(1,"rgba(255,255,255,0)"),E.fillStyle=i,E.fillRect(10*-P,10*-U,20*P,20*U),E.restore(),E.globalCompositeOperation="source-over",m.globalCompositeOperation="source-atop",m.drawImage(y,0,0,a,o),m.globalCompositeOperation="source-over"}}else if(n.strength>0){const t=32,e=18,s=a/t,h=o/e;for(let r=0;r<e;r++)for(let l=0;l<t;l++){const c=(l+.5)/t,f=(r+.5)/e,g=Math.min(n.width-1,Math.round(c*(n.width-1))),p=4*(Math.min(n.height-1,Math.round(f*(n.height-1)))*n.width+g),x=n.pixels,v=Math.min(l*s/d,(a-(l+1)*s)/d,r*h/u,(o-(r+1)*h)/u),M=Math.max(0,Math.min(1,(v-2)/6)),w=M*M*(3-2*M),y=(256*x[p]+x[p+1]-32768)/32767*n.strength*w,S=(256*x[p+2]+x[p+3]-32768)/32767*n.strength*w;Math.abs(y)+Math.abs(S)<.02||(m.clearRect(l*s,r*h,s,h),m.drawImage(i,Math.max(0,Math.min(a-s,l*s-y))*i.width/a,Math.max(0,Math.min(o-h,r*h-S))*i.height/o,s*i.width/a,h*i.height/o,l*s,r*h,s,h))}}},destroy(){x=!0,A.destroy(),r.removeEventListener("webglcontextlost",D),_(),r.remove(),w.width=w.height=y.width=y.height=1,i.style.visibility=o,l?.getExtension("WEBGL_lose_context")?.loseContext()}}}(t,i,()=>queueMicrotask(()=>{f=!0,S()}));function b(t){if(g=0,l||!d||document.hidden)return;const s=i.clientWidth,n=i.clientHeight;if(!s||!n)return;const h=y.matches||o.paused;r.configure(o.effect,o.strength,o.radius);const a=r.active;if(!h){if(v+=p?Math.min(.1,(t-p)/1e3):0,w){const t=i.getBoundingClientRect();M&&t.width&&t.height?r.move((M.x-t.left)/t.width,(M.y-t.top)/t.height):r.leave(),w=!1}r.step(p?(t-p)/1e3:1/60)}const c=f||o.animated&&!h&&t-x>=1e3/Math.max(1,Math.min(60,o.fps))-.5;(c||a||r.active||!h&&"none"!==e(o.motion))&&(E.render(c,r.refraction,s,n,o.fontSize*o.charSpacing,o.fontSize/o.charAspect*o.charSpacing,c?o.getFrame?.():void 0,{charset:o.charset,customText:o.customText,renderMode:o.renderMode,dotSizeRatio:o.dotSizeRatio,colorMode:"fullcolor",animationStyle:o.motion,animationSpeed:o.motionSpeed??1,artStyle:o.fineDither?"terminal":"classic",ditherStrength:o.ditherStrength??1},v),c&&(x=t),f=!1),p=t,h||!o.animated&&!r.active&&"none"===e(o.motion)||S()}const T=()=>{r.clear(),M=null,w=!1,p=0,f=!0,cancelAnimationFrame(g),g=0,S()},A=t=>{if("touch"===t.pointerType||o.paused||y.matches)return;if(t.target instanceof Element&&t.target.closest("button,a,input,summary,select"))return void k();const e=i.getBoundingClientRect();t.clientX<e.left||t.clientX>e.right||t.clientY<e.top||t.clientY>e.bottom?k():(M={x:t.clientX,y:t.clientY},w=!0,S())},k=()=>{M=null,w=!0,S()},R=new IntersectionObserver(([t])=>{d=t.isIntersecting,T()});R.observe(t);const z=new ResizeObserver(()=>{r=new m(i.clientWidth/Math.max(1,i.clientHeight)),T()});return z.observe(i),t.addEventListener("pointermove",A),t.addEventListener("pointerleave",k),t.addEventListener("pointercancel",k),document.addEventListener("visibilitychange",T),y.addEventListener("change",T),{get canvas(){return E.canvas},invalidate(){f=!0,S()},update(t){o={...o,...t},T()},destroy(){l||(l=!0,cancelAnimationFrame(g),R.disconnect(),z.disconnect(),E.destroy(),t.removeEventListener("pointermove",A),t.removeEventListener("pointerleave",k),t.removeEventListener("pointercancel",k),document.removeEventListener("visibilitychange",T),y.removeEventListener("change",T))}}}exports.mountHover=f,exports.mountStudioHover=f;//# sourceMappingURL=hover.cjs.map
1
+ "use strict";function t(t,e,i=()=>{}){const s=Array.isArray(e)?e:[e],n=document.createElement("canvas"),a=n.getContext("2d");let o,r=!0,h=!1,l=0;const c=()=>{h||(r=!0,i())},d=new ResizeObserver(c);d.observe(t),s.forEach(t=>d.observe(t));const m=new MutationObserver(c);return s.forEach(t=>m.observe(t,{subtree:!0,childList:!0,characterData:!0,attributes:!0})),document.fonts?.addEventListener("loadingdone",c),document.fonts?.ready.then(c),window.addEventListener("resize",c),{invalidate:c,read(){if(h)return;if(!r)return o;r=!1;const e=t.getBoundingClientRect(),i=Math.min(devicePixelRatio||1,1.5,Math.sqrt(4e6/Math.max(1,e.width*e.height)));n.width=Math.max(1,Math.round(e.width*i)),n.height=Math.max(1,Math.round(e.height*i)),a.setTransform(i,0,0,i,0,0),a.fillStyle="#fff",a.textBaseline="alphabetic";for(const t of s){const i=document.createTreeWalker(t,NodeFilter.SHOW_TEXT);for(let t=i.nextNode();t;t=i.nextNode()){const i=t.parentElement;if(!i)continue;const s=getComputedStyle(i);if("hidden"===s.visibility||"none"===s.display)continue;a.font=`${s.fontStyle} ${s.fontWeight} ${s.fontSize} ${s.fontFamily}`;const n=a.measureText("Hg"),o=n.fontBoundingBoxAscent??.8*parseFloat(s.fontSize),r=n.fontBoundingBoxDescent??.2*parseFloat(s.fontSize),h=t.textContent??"",l=document.createRange();let c=0;for(const i of h){if(l.setStart(t,c),c+=i.length,l.setEnd(t,c),/\s/.test(i))continue;const s=l.getBoundingClientRect();s.width&&s.height&&a.fillText(i,s.left-e.left,s.top-e.top+(s.height-o-r)/2+o)}}}return o={canvas:n,revision:++l},o},destroy(){h=!0,d.disconnect(),m.disconnect(),document.fonts?.removeEventListener("loadingdone",c),window.removeEventListener("resize",c),n.width=n.height=1,o=void 0}}}var e=[{value:"none",mode:"none",label:"Off",description:"Keep the image still. Hover stays available."},{value:"breathe",mode:"print",label:"Living Print",description:"Soft tonal movement. The character grid stays still."},{value:"wave",mode:"current",label:"Slow Current",description:"A gentle flow through the image, with steady edges."},{value:"rain",mode:"reform",label:"Reveal & Reform",description:"Disperse, gather, then hold the complete image."}];function i(t){return"orbit"===t?"current":e.find(e=>e.value===t||e.mode===t)?.mode??"none"}var s=(t,e,i)=>{const s=(t=>Math.max(0,Math.min(1,t)))((i-t)/(e-t));return s*s*(3-2*s)},n=(t,e)=>{const i=(.06711056*t+.00583715*e)%1*52.9829189;return i-Math.floor(i)},a=class{constructor(){this.last=null,this.mode="none",this.phase=0}update(t,e,i=1){return Number.isFinite(t)?((e!==this.mode||null!==this.last&&t<this.last)&&(this.mode=e,this.phase=0,this.last=t),null!==this.last&&(this.phase+=Math.max(0,t-this.last)*Math.max(.1,Math.min(3,Number.isFinite(i)?i:1))),this.last=t,this.phase):this.phase}};function o(t,e,i,n,a){const o=n*Math.PI/6,r=s(0,.12,Math.min(e,1-e,i,1-i));if(a[0]=a[1]=a[2]=0,a[3]=1,"print"===t)a[2]=Math.sin(9*e+5*i+o)*Math.cos(8*i-3*e-o)*.065*r,a[3]=.9+.1*Math.sin(6*e-4*i+o);else if("current"===t)a[0]=Math.sin(9*i+o)*Math.cos(7*e-o)*6*r,a[1]=Math.cos(8*e+o)*Math.sin(6*i-o)*4*r;else if("reform"===t){const t=(n%12+12)%12,o=.8*e+.2*i+.08*Math.sin(8*e-5*i),r=s(1.8+1.2*o,3.8+1.2*o,t)*(1-s(5+1.7*o,7.2+1.7*o,t));a[2]=.95*-r,a[3]=1-.82*r}return a}function r(t,e,i){const s=Math.min(t*Math.max(1,i),(1-t)*Math.max(1,i),e*Math.max(1,1/i),(1-e)*Math.max(1,1/i)),n=Math.max(0,Math.min(1,(s-.035)/.105));return n*n*n*(n*(6*n-15)+10)}var h=class{constructor(t,e){this.edgeSafe=!1,this.previous=null,this.peak=0,this.mode="dissolve",this.width=t,this.height=e;const i=t*e;this.pixels=new Uint8Array(4*i),this.ink=new Float32Array(i),this.targetX=new Float32Array(i),this.targetY=new Float32Array(i),this.offsetX=new Float32Array(i),this.offsetY=new Float32Array(i),this.edge=new Float32Array(i);for(let i=0;i<e;i++)for(let s=0;s<t;s++)this.edge[i*t+s]=r(s/(t-1),i/(e-1),t/e);this.clear()}get active(){return this.peak>2e-4}setMode(t){t!==this.mode&&(this.mode=t,this.clear())}clear(){for(const t of[this.ink,this.targetX,this.targetY,this.offsetX,this.offsetY,this.pixels])t.fill(0);if(this.previous=null,this.peak=0,"dissolve"!==this.mode)for(let t=0;t<this.pixels.length;t+=4)this.pixels[t]=this.pixels[t+2]=128}leave(){this.previous=null}move(t,e,i){if(!Number.isFinite(t+e+i))return;t=Math.max(0,Math.min(1,t)),e=Math.max(0,Math.min(1,e));const s=this.previous;this.previous={x:t,y:e};const n=this.width,a=this.height,o=Math.max(1,n/a),r=Math.max(1,a/n),h=s?(t-s.x)*o:0,l=s?(e-s.y)*r:0,c=Math.hypot(h,l);if(!s&&"dissolve"!==this.mode)return;if(s&&c<1e-4)return;const d=.035+.11*Math.max(.1,Math.min(1,i)),m=Math.min(128,Math.max(1,Math.ceil(c/(.45*d)))),f=c?h/c:1,u=c?l/c:0,g=Math.min(1,c/(d*m)*2.2);for(let i=0;i<m;i++){const h=(i+.5)/m,l=s?s.x+(t-s.x)*h:t,c=s?s.y+(e-s.y)*h:e,p=Math.max(0,Math.floor((l-2.5*d/o)*(n-1))),x=Math.min(n-1,Math.ceil((l+2.5*d/o)*(n-1))),v=Math.max(0,Math.floor((c-2.5*d/r)*(a-1))),M=Math.min(a-1,Math.ceil((c+2.5*d/r)*(a-1)));for(let t=v;t<=M;t++)for(let e=p;e<=x;e++){const i=(e/(n-1)-l)*o/d,s=(t/(a-1)-c)*r/d,h=Math.exp(1.5*-(i*i+s*s)),m=t*n+e;if("dissolve"===this.mode)this.ink[m]=Math.max(this.ink[m],h);else{const t=(-i*u+s*f)*h*2.8,e="silk"===this.mode?f*t:-s*h*2.5,n="silk"===this.mode?u*t:i*h*2.5;this.targetX[m]=Math.max(-1,Math.min(1,this.targetX[m]+e*g)),this.targetY[m]=Math.max(-1,Math.min(1,this.targetY[m]+n*g))}}}this.peak=1}step(t){if(!this.active||!Number.isFinite(t))return;const e=Math.max(0,Math.min(.05,t)),i=Math.exp(-("dissolve"===this.mode?2.15:3.2)*e),s=1-Math.exp(-22*e);let n=0;for(let t=0;t<this.ink.length;t++)if("dissolve"===this.mode){this.ink[t]*=i,n=Math.max(n,this.ink[t]);const e=Math.round(65535*this.ink[t]);this.pixels[4*t]=e>>>8,this.pixels[4*t+1]=255&e}else{this.targetX[t]*=i,this.targetY[t]*=i,this.offsetX[t]+=(this.targetX[t]-this.offsetX[t])*s,this.offsetY[t]+=(this.targetY[t]-this.offsetY[t])*s,n=Math.max(n,Math.abs(this.offsetX[t]),Math.abs(this.offsetY[t]),Math.abs(this.targetX[t]),Math.abs(this.targetY[t]));const e=Math.round(this.offsetX[t]*(this.edgeSafe?this.edge[t]:1)*32767+32768),a=Math.round(this.offsetY[t]*(this.edgeSafe?this.edge[t]:1)*32767+32768);this.pixels[4*t]=e>>>8,this.pixels[4*t+1]=255&e,this.pixels[4*t+2]=a>>>8,this.pixels[4*t+3]=255&a}this.peak=n,this.active||this.clear()}sample(t,e,i){const s=Math.max(0,Math.min(this.width-1.001,t*(this.width-1))),n=Math.max(0,Math.min(this.height-1.001,e*(this.height-1))),a=Math.floor(s),o=Math.floor(n),h=s-a,l=n-o,c=o*this.width+a,d=c+this.width,m=t=>(t[c]*(1-h)+t[c+1]*h)*(1-l)+(t[d]*(1-h)+t[d+1]*h)*l;i[0]=m(this.offsetX)*(this.edgeSafe?r(t,e,this.width/this.height):1)*.08,i[1]=m(this.offsetY)*(this.edgeSafe?r(t,e,this.width/this.height):1)*.08,i[2]="dissolve"===this.mode?6*-m(this.ink):0}},l=class{constructor(t,e){this.rings=[],this.previous=null,this.elapsed=1,this.echoPeak=0,this.width=t,this.height=e,this.pixels=new Uint8Array(t*e*4),this.echo=new Float32Array(t*e)}get active(){return this.rings.length>0||this.echoPeak>2e-4}clear(){this.rings=[],this.previous=null,this.elapsed=1,this.pixels.fill(0),this.echo.fill(0),this.echoPeak=0}leave(){this.previous=null}move(t,e,i){const s=Math.max(1,this.width/this.height),n=Math.max(1,this.height/this.width);this.previous&&(Math.hypot((t-this.previous.x)*s,(e-this.previous.y)*n)<.065||this.elapsed<.3)||4!==this.rings.length&&(this.previous={x:t,y:e},this.elapsed=0,this.rings.push({x:t,y:e,age:0,radius:i}))}ringSample(t,e){const i=Math.max(1,this.width/this.height),s=Math.max(1,this.height/this.width);let n=0;for(const a of this.rings){const o=.015+a.age*(.15+.22*a.radius),r=.009+.014*a.radius,h=(Math.hypot((t-a.x)*i,(e-a.y)*s)-o)/r,l=Math.sin(Math.min(1,a.age/.07)*Math.PI/2)*Math.max(0,1-a.age/1.35)**2;n=Math.max(n,Math.exp(-h*h)*l)}return n}sample(t,e){const i=Math.max(0,Math.min(this.width-1,Math.round(t*(this.width-1)))),s=Math.max(0,Math.min(this.height-1,Math.round(e*(this.height-1))));return Math.max(this.ringSample(t,e),this.echo[s*this.width+i])}step(t){if(!Number.isFinite(t))return;const e=Math.max(0,Math.min(.05,t));this.elapsed+=e;for(const t of this.rings)t.age+=e;if(this.rings=this.rings.filter(t=>t.age<1.35),!this.active)return void this.pixels.fill(0);const i=Math.exp(-5.5*e);let s=0;for(let t=0;t<this.height;t++)for(let e=0;e<this.width;e++){const n=t*this.width+e;this.echo[n]=Math.max(this.ringSample(e/(this.width-1),t/(this.height-1)),this.echo[n]*i),s=Math.max(s,this.echo[n]);const a=Math.round(65535*this.echo[n]),o=4*n;this.pixels[o]=a>>>8,this.pixels[o+1]=255&a}this.echoPeak=s,this.active||(this.echo.fill(0),this.pixels.fill(0))}},c=class{constructor(t,e){this.previous=null,this.peak=0,this.width=t,this.height=e;const i=t*e;this.pixels=new Uint8Array(4*i),this.signal=new Float32Array(i),this.offsetX=new Float32Array(i),this.offsetY=new Float32Array(i),this.driftX=new Float32Array(i),this.driftY=new Float32Array(i),this.u=new Float32Array(i),this.v=new Float32Array(i),this.ink=new Float32Array(i),this.a=new Float32Array(i),this.b=new Float32Array(i),this.c=new Float32Array(i),this.pressure=new Float32Array(i),this.pressureNext=new Float32Array(i),this.divergence=new Float32Array(i),this.curl=new Float32Array(i)}get active(){return this.peak>2e-4}clear(){for(const t of[this.u,this.v,this.ink,this.a,this.b,this.c,this.pressure,this.pressureNext,this.divergence,this.curl,this.signal,this.pixels,this.offsetX,this.offsetY,this.driftX,this.driftY])t.fill(0);this.previous=null,this.peak=0}leave(){this.previous=null}move(t,e,i,s=performance.now()){if(!Number.isFinite(t+e+i+s))return;t=Math.max(0,Math.min(1,t)),e=Math.max(0,Math.min(1,e)),i=Math.max(.1,Math.min(1,i));const n=this.previous;if(this.previous={x:t,y:e,time:s},!n)return;const a=(t-n.x)*(this.width-1),o=(e-n.y)*(this.height-1),r=Math.hypot(a,o);if(r<.001)return;const h=Math.min(this.width,this.height),l=s>n.time?Math.max(1/240,(s-n.time)/1e3):1/60,c=Math.min(12,r/h/l),d=1-Math.exp(-c/2.2),m=h*(.022+.075*i)*(.65+.95*d),f=Math.min(128,Math.max(1,Math.ceil(r/(.4*m)))),u=r/m*(.32+.22*d)/f,g=a/r,p=o/r,x=-p,v=g,M=r*(2+18*d)/f;for(let i=0;i<f;i++){const s=(i+.5)/f,a=(n.x+(t-n.x)*s)*(this.width-1),o=(n.y+(e-n.y)*s)*(this.height-1),r=Math.max(1,Math.floor(a-3*m)),h=Math.min(this.width-2,Math.ceil(a+3*m)),l=Math.max(1,Math.floor(o-3*m)),c=Math.min(this.height-2,Math.ceil(o+3*m));for(let t=l;t<=c;t++)for(let e=r;e<=h;e++){const i=((e-a)*g+(t-o)*p)/m,s=((e-a)*x+(t-o)*v)/m,n=Math.exp(-i*i*.65-s*s*1.7),r=t*this.width+e;this.ink[r]=1-(1-this.ink[r])*Math.exp(-n*u);const h=1-s*s*.55,l=i*s*.7;this.u[r]=Math.max(-80,Math.min(80,this.u[r]+(g*h+x*l)*n*M)),this.v[r]=Math.max(-80,Math.min(80,this.v[r]+(p*h+v*l)*n*M))}}this.peak=1}read(t,e,i){const s=0|(e=Math.max(.5,Math.min(this.width-1.5,e))),n=0|(i=Math.max(.5,Math.min(this.height-1.5,i))),a=e-s,o=i-n,r=n*this.width+s;return(t[r]*(1-a)+t[r+1]*a)*(1-o)+(t[r+this.width]*(1-a)+t[r+this.width+1]*a)*o}sample(t,e){return this.read(this.signal,t*(this.width-1),e*(this.height-1))}displacement(t,e,i){i[0]=this.read(this.offsetX,t*(this.width-1),e*(this.height-1)),i[1]=this.read(this.offsetY,t*(this.width-1),e*(this.height-1))}step(t){if(!this.active||!Number.isFinite(t))return;let e=Math.max(0,Math.min(.05,t));const i=this.width,s=this.height;for(;e>1e-7;){const t=Math.min(1/60,e);e-=t;const n=Math.exp(-1.9*t),a=Math.exp(-3.2*t);for(let e=1;e<s-1;e++)for(let o=1;o<i-1;o++){const r=e*i+o,h=Math.max(.5,Math.min(i-1.5,o-this.u[r]*t)),l=Math.max(.5,Math.min(s-1.5,e-this.v[r]*t)),c=0|h,d=0|l,m=h-c,f=l-d,u=d*i+c,g=(1-m)*(1-f),p=m*(1-f),x=(1-m)*f,v=m*f;this.a[r]=(this.u[u]*g+this.u[u+1]*p+this.u[u+i]*x+this.u[u+i+1]*v)*n,this.b[r]=(this.v[u]*g+this.v[u+1]*p+this.v[u+i]*x+this.v[u+i+1]*v)*n,this.c[r]=(this.ink[u]*g+this.ink[u+1]*p+this.ink[u+i]*x+this.ink[u+i+1]*v)*a}[this.u,this.a]=[this.a,this.u],[this.v,this.b]=[this.b,this.v],[this.ink,this.c]=[this.c,this.ink];for(let t=1;t<s-1;t++)for(let e=1;e<i-1;e++){const s=t*i+e;this.curl[s]=.5*(this.v[s+1]-this.v[s-1]-this.u[s+i]+this.u[s-i])}for(let e=1;e<s-1;e++)for(let s=1;s<i-1;s++){const n=e*i+s,a=Math.abs(this.curl[n+1])-Math.abs(this.curl[n-1]),o=Math.abs(this.curl[n+i])-Math.abs(this.curl[n-i]),r=Math.hypot(a,o)+1e-4,h=this.curl[n]*t*10,l=Math.hypot(this.u[n],this.v[n]);this.u[n]+=o/r*h,this.v[n]-=a/r*h;const c=Math.min(1,l/(Math.hypot(this.u[n],this.v[n])+1e-6));this.u[n]*=c,this.v[n]*=c,this.divergence[n]=-.5*(this.u[n+1]-this.u[n-1]+this.v[n+i]-this.v[n-i])}this.pressure.fill(0);for(let t=0;t<4;t++){for(let t=1;t<s-1;t++)for(let e=1;e<i-1;e++){const s=t*i+e;this.pressureNext[s]=.25*(this.divergence[s]+this.pressure[s-1]+this.pressure[s+1]+this.pressure[s-i]+this.pressure[s+i])}[this.pressure,this.pressureNext]=[this.pressureNext,this.pressure]}let o=0;for(let e=1;e<s-1;e++)for(let s=1;s<i-1;s++){const n=e*i+s;this.u[n]=Math.max(-80,Math.min(80,this.u[n]-.5*(this.pressure[n+1]-this.pressure[n-1]))),this.v[n]=Math.max(-80,Math.min(80,this.v[n]-.5*(this.pressure[n+i]-this.pressure[n-i]))),this.driftX[n]+=(3.6*this.u[n]-58*this.offsetX[n]-12*this.driftX[n])*t,this.driftY[n]+=(3.6*this.v[n]-58*this.offsetY[n]-12*this.driftY[n])*t,this.offsetX[n]=Math.max(-2,Math.min(2,this.offsetX[n]+this.driftX[n]*t)),this.offsetY[n]=Math.max(-2,Math.min(2,this.offsetY[n]+this.driftY[n]*t)),this.signal[n]=1-Math.exp(.06*-Math.hypot(this.u[n],this.v[n])-.16*this.ink[n]),o=Math.max(o,this.signal[n])}this.peak=o}if(this.active)for(let t=0;t<this.ink.length;t++){const e=Math.round(65535*Math.min(1,this.signal[t]));this.pixels[4*t]=e>>>8,this.pixels[4*t+1]=255&e,this.pixels[4*t+2]=Math.round(128+63.5*this.offsetX[t]),this.pixels[4*t+3]=Math.round(128+63.5*this.offsetY[t])}else this.clear()}},d=t=>["trail","contour","dissolve"].includes(t);function m(t,e,i){const s=Math.max(0,e-1),n=1-Math.exp(3.5*-Math.max(0,i));return Math.max(0,s-Math.abs(s-(t+n*s*2.1)))}function f(t,e,i,s,n,a,o,r,h,l,c,d=!1){const f=Math.min(n,a,i-1-n,s-1-a),u=Math.max(0,Math.min(1,(f-1)/4)),g=d?u*u*(3-2*u):1;o*=g;const p=1-Math.exp(3.5*-Math.max(0,o)),x=Math.max(0,Math.min(i-1,n-r*p*.026*i*g)),v=Math.max(0,Math.min(s-1,a-h*p*.026*s*g)),M=Math.floor(x),y=Math.floor(v),w=x-M,S=v-y;c.fill(0);for(let n=0;n<2;n++)for(let a=0;a<2;a++){const o=4*(Math.min(s-1,y+n)*i+Math.min(i-1,M+a)),r=(a?w:1-w)*(n?S:1-S);c[0]+=(t[o]+256*t[o+1])*r,c[1]+=t[o+2]/255*r;for(let t=0;t<4;t++)c[t+2]+=e[o+t]*r}const b=43758.5453*Math.sin(127.1*n+311.7*a),E=b-Math.floor(b);c[0]=Math.max(0,m(c[0],l,o)-p*(1-p)*E*3)}var u=class{constructor(t=1){this.mode="water",this.amount=.55,this.radius=.2,this.lensX=.5,this.lensY=.5,this.lensStrength=0,this.previous=null,this.energy=0,this.pending=!1,this.remainder=0,t=Math.max(.25,Math.min(4,t)),this.columns=Math.round(t>=1?128:128*t),this.rows=Math.round(t>=1?128/t:128),this.xScale=Math.max(1,t),this.yScale=Math.max(1,1/t),this.heights=new Float32Array(this.columns*this.rows),this.velocity=new Float32Array(this.heights.length),this.next=new Float32Array(this.heights.length),this.normals=new Float32Array(2*this.heights.length),this.edgeMask=new Float32Array(this.heights.length);for(let e=0;e<this.rows;e++)for(let i=0;i<this.columns;i++)this.edgeMask[e*this.columns+i]=r(i/(this.columns-1),e/(this.rows-1),t);this.refraction={width:this.columns,height:this.rows,pixels:new Uint8Array(4*this.heights.length),strength:30,mode:"water",focus:[.5,.5,0,.2]},this.waterPixels=this.refraction.pixels,this.trail=new c(this.columns,this.rows),this.contour=new l(this.columns,this.rows),this.afterimage=new h(this.columns,this.rows),this.clear()}get invertsDensity(){return"trail"===this.mode}get densityTrail(){return["trail","contour","dissolve"].includes(this.mode)}get hasRefraction(){return this.active||this.lensStrength>15e-5}get active(){return["dissolve","silk","vortex"].includes(this.mode)?this.afterimage.active:"contour"===this.mode?this.contour.active:"trail"===this.mode?this.trail.active:this.pending||this.energy>15e-5}setMode(t){t!==this.mode&&(this.clear(),this.mode=t,this.refraction.mode=t,"dissolve"!==t&&"silk"!==t&&"vortex"!==t||this.afterimage.setMode(t),this.refraction.pixels="trail"===t?this.trail.pixels:"contour"===t?this.contour.pixels:["dissolve","silk","vortex"].includes(t)?this.afterimage.pixels:this.waterPixels,this.refraction.strength="water"===t?30*this.amount/.55:"silk"===t?28*this.amount:"vortex"===t?38*this.amount:0)}configure(t,e=.55,i=.2,s=!1){this.setMode(t),this.refraction.edgeSafe=s,this.afterimage.edgeSafe=s,this.amount=Number.isFinite(e)?Math.max(0,Math.min(1,e)):.55,this.radius=Number.isFinite(i)?Math.max(.1,Math.min(1,i)):.2,this.refraction.strength="water"===t?30*this.amount/.55:"silk"===t?28*this.amount:"vortex"===t?38*this.amount:0,this.refraction.focus[2]=this.densityTrail?this.amount:this.lensStrength*this.amount,this.refraction.focus[3]=this.radius}move(t,e,i){if("none"===this.mode||0===this.amount)return;if(!Number.isFinite(t+e))return;if(t=Math.max(0,Math.min(1,t)),e=Math.max(0,Math.min(1,e)),["dissolve","silk","vortex"].includes(this.mode))return void this.afterimage.move(t,e,this.radius);if("contour"===this.mode)return void this.contour.move(t,e,this.radius);if("trail"===this.mode)return void this.trail.move(t,e,this.radius,i);if("water"!==this.mode)return!this.previous&&this.lensStrength<.001&&(this.lensX=t,this.lensY=e),this.previous={x:t,y:e},void(this.pending=!0);if(!this.previous)return void(this.previous={x:t,y:e});const s=this.previous;this.previous={x:t,y:e};const n=(t-s.x)*this.xScale,a=(e-s.y)*this.yScale,o=Math.hypot(n,a);if(o<1e-5)return;const r=.035+.1*this.radius,h=3*r,l=Math.min(192,Math.max(1,Math.ceil(o/(.5*r)))),c=95*Math.min(o,1)/l;for(let i=0;i<l;i++){const n=(i+.5)/l,a=s.x+(t-s.x)*n,o=s.y+(e-s.y)*n,d=Math.max(1,Math.floor((a-h/this.xScale)*(this.columns-1))),m=Math.min(this.columns-2,Math.ceil((a+h/this.xScale)*(this.columns-1))),f=Math.max(1,Math.floor((o-h/this.yScale)*(this.rows-1))),u=Math.min(this.rows-2,Math.ceil((o+h/this.yScale)*(this.rows-1)));for(let t=f;t<=u;t++)for(let e=d;e<=m;e++){const i=(e/(this.columns-1)-a)*this.xScale,s=(t/(this.rows-1)-o)*this.yScale,n=Math.exp(-(i*i+s*s)/(r*r))*c,h=t*this.columns+e;this.velocity[h]=Math.max(-12,this.velocity[h]-n)}}this.pending=!0}leave(){this.afterimage.leave(),this.contour.leave(),this.trail.leave(),this.previous=null,"water"!==this.mode&&this.lensStrength&&(this.pending=!0)}clear(){this.trail.clear(),this.contour.clear(),this.afterimage.clear(),this.heights.fill(0),this.velocity.fill(0),this.next.fill(0),this.normals.fill(0),this.refraction.focus[2]=0,this.previous=null,this.lensStrength=0,this.energy=0,this.pending=!1,this.remainder=0;for(let t=0;t<this.waterPixels.length;t+=4)this.waterPixels[t]=this.waterPixels[t+2]=128,this.waterPixels[t+1]=this.waterPixels[t+3]=0}step(t){if(!this.active||!Number.isFinite(t))return;if(["dissolve","silk","vortex"].includes(this.mode))return void this.afterimage.step(t);if("contour"===this.mode)return void this.contour.step(t);if("trail"===this.mode)return void this.trail.step(t);if("water"!==this.mode)return void this.stepLight(Math.max(0,Math.min(.05,t)));this.remainder+=Math.max(0,Math.min(.05,t));const e=1/120,i=Math.exp(-3.8*e),s=.3*Math.min(this.columns,this.rows);for(;this.remainder+1e-8>=e;){this.remainder-=e;let t=0;for(let n=1;n<this.rows-1;n++)for(let a=1;a<this.columns-1;a++){const o=n*this.columns+a,r=this.heights[o],h=this.heights[o-1]+this.heights[o+1]+this.heights[o-this.columns]+this.heights[o+this.columns]-4*r,l=Math.min(a,n,this.columns-1-a,this.rows-1-n),c=l<5?.88+.024*l:1,d=(this.velocity[o]+(h*s*s-8*r)*e)*i*c;this.velocity[o]=d,this.next[o]=(r+d*e)*c,t=Math.max(t,Math.abs(this.next[o]),.1*Math.abs(d))}this.heights.set(this.next),this.energy=t,this.pending=!1}if(!this.active)return void this.clear();const n=.18*Math.min(this.columns,this.rows);for(let t=0;t<this.rows;t++)for(let e=0;e<this.columns;e++){const i=t*this.columns+e,s=(this.heights[t*this.columns+Math.max(0,e-1)]-this.heights[t*this.columns+Math.min(this.columns-1,e+1)])*n,a=(this.heights[Math.max(0,t-1)*this.columns+e]-this.heights[Math.min(this.rows-1,t+1)*this.columns+e])*n,o=Math.tanh(s)*(this.refraction.edgeSafe?this.edgeMask[i]:1),r=Math.tanh(a)*(this.refraction.edgeSafe?this.edgeMask[i]:1);this.normals[2*i]=o,this.normals[2*i+1]=r;const h=Math.round(32767*o+32768),l=Math.round(32767*r+32768);this.refraction.pixels[4*i]=h>>>8,this.refraction.pixels[4*i+1]=255&h,this.refraction.pixels[4*i+2]=l>>>8,this.refraction.pixels[4*i+3]=255&l}}stepLight(t){const e=this.previous,i=1-Math.exp(-30*t),s=1-Math.exp(-12*t);e&&(this.lensX+=(e.x-this.lensX)*i,this.lensY+=(e.y-this.lensY)*i),this.lensStrength+=((e?1:0)-this.lensStrength)*s,this.energy=Math.max(e?Math.abs(e.x-this.lensX)+Math.abs(e.y-this.lensY):0,Math.abs((e?1:0)-this.lensStrength)),this.pending=!1,e||this.active?this.refraction.focus=[this.lensX,this.lensY,this.lensStrength*this.amount,this.radius]:this.clear()}sample(t,e,i){if(["dissolve","silk","vortex"].includes(this.mode))return this.afterimage.sample(t,e,i),i[0]*=this.amount,i[1]*=this.amount,void(i[2]*=this.amount);if("contour"===this.mode)return i[0]=i[1]=0,void(i[2]=this.contour.sample(t,e)*this.amount*3);if("trail"===this.mode)return i[0]=i[1]=0,void(i[2]=this.trail.sample(t,e)*this.amount*3);const s=Math.max(0,Math.min(this.columns-1.001,t*(this.columns-1))),n=Math.max(0,Math.min(this.rows-1.001,e*(this.rows-1))),a=Math.floor(s),o=Math.floor(n),r=s-a,h=n-o,l=2*(o*this.columns+a),c=l+2*this.columns;for(let t=0;t<2;t++)i[t]=.08*((this.normals[l+t]*(1-r)+this.normals[l+2+t]*r)*(1-h)+(this.normals[c+t]*(1-r)+this.normals[c+2+t]*r)*h);i[0]*=this.amount/.55,i[1]*=this.amount/.55,i[2]="light"===this.mode||"scan"===this.mode?function(t,e,i,s){const[n,a,o,r]=i.focus,h=(t-n)*Math.max(1,s)/r,l=(e-a)*Math.max(1,1/s)/r;return"light"===i.mode?Math.exp(-3*(h*h+l*l))*o:"scan"===i.mode?(1.3*Math.exp(-h*h*110)-.3*Math.exp(-h*h*12))*Math.exp(-l*l*1.8)*o:0}(t,e,this.refraction,this.xScale/this.yScale):0}};var g=[{value:"clean",label:"Clean",description:"Crisp characters with no optical texture."},{value:"signal",label:"Signal",description:"Broken scanlines at the edges, with a clear center."},{value:"prism",label:"Prism",description:"Soft red/cyan separation at the edges, with a crisp center."},{value:"etch",label:"Etch",description:"A fine engraved texture, keeping the ink monochrome."}];function p(t){const e=Intl.Segmenter;return e?Array.from(new e(void 0,{granularity:"grapheme"}).segment(t),t=>t.segment):Array.from(t)}function x(){const t=document.createElement("canvas"),e=document.createElement("canvas"),i=t.getContext("2d"),s=e.getContext("2d",{willReadFrequently:!0}),n=function(){const t=new WeakMap;return(e,i,s="")=>{if(!e)return i;let n=t.get(e);if(!n){const i=new Set;for(const t of e)for(const e of t)e.char&&i.add(e.char);n=[...i].sort(),t.set(e,n)}const a=i+s;return i+n.filter(t=>!a.includes(t)).join("")}}();let a,o=null;return{update(r,h,l,c,d,m,f,u){const g=f?.[0]?.length??Math.max(1,Math.round(h/c)),x=f?.length??Math.max(1,Math.round(l/d)),v=u??{charset:" .:-=+*#%@",customText:"",colorMode:"fullcolor"},M=n(f,v.charset+(v.charsetFrames?.join("")??""),v.customText),y=function(t,e,i,s,n,a,o,r=4096,h=0,l=1){if(![i,s,n,a,o,r].every(t=>Number.isFinite(t)&&t>0))throw new Error("Glyph atlas dimensions must be positive and finite.");const c=JSON.stringify([e.charset,e.customText,i,s,n,a,o,r,h,l]);if(t?.key===c)return t;const d=[...new Set([" ",...p(e.charset),...p(e.customText??"")])];if(d.length>65536)throw new Error("This character set exceeds the glyph texture capacity.");const m=i/a,f=s/o,u=.9*Math.min(m/.55,f)*l,g=Math.ceil(m*n)+6,x=Math.ceil(f*n)+6,v=Math.min(d.length,Math.floor(r/g));if(v<1||Math.ceil(d.length/v)*x>r)throw new Error("The glyph atlas exceeds the available GPU texture size.");return{key:c,glyphs:d,indices:new Map(d.map((t,e)=>[t,e])),cellWidth:m,cellHeight:f,fontSize:u,padding:3,tileWidth:g,tileHeight:x,atlasColumns:v,atlasWidth:v*g,atlasHeight:Math.ceil(d.length/v)*x}}(o,{...v,charset:M},h,l,m,g,x);if(y!==o&&(t.width=y.atlasWidth,t.height=y.atlasHeight,i.font=y.fontSize*m+'px "JetBrains Mono", monospace',i.textAlign="center",i.textBaseline="middle",i.fillStyle="#fff",y.glyphs.forEach((t,e)=>i.fillText(t,e%y.atlasColumns*y.tileWidth+y.padding+y.cellWidth*m*.5,Math.floor(e/y.atlasColumns)*y.tileHeight+y.padding+y.cellHeight*m*.5))),o=y,f)a=function(t,e,i,s,n=!1){const a=t.length,o=t[0]?.length??0,r=s?.cols===o&&s.rows===a?s:{cols:o,rows:a,indices:new Uint8Array(o*a*4),colors:new Uint8ClampedArray(o*a*4)};r.indices.fill(0),r.colors.fill(0);const h="accent"===i.colorMode?[1,3,5].map(t=>parseInt(i.accentColor.slice(t,t+2),16)):null;for(let s=0;s<a;s++){if(t[s].length!==o)throw new Error("Glyph frames must have a rectangular cell grid.");for(let a=0;a<o;a++){const l=t[s][a];if(!l.char||!n&&" "===l.char||l.a<10)continue;const c=e.get(l.char);if(void 0===c)throw new Error(`Glyph is absent from the configured character set: ${l.char}`);const d=4*(s*o+a);let m=l.r,f=l.g,u=l.b;if(h)[m,f,u]=h;else if("fullcolor"!==i.colorMode){const t=.299*m+.587*f+.114*u|0;m="matrix"===i.colorMode?0:t,f=t,u=m}(n||0!==m||0!==f||0!==u)&&(r.indices[d]=255&c,r.indices[d+1]=c>>>8,r.indices[d+2]=Math.round(.299*l.r+.587*l.g+.114*l.b),r.colors[d]=m,r.colors[d+1]=f,r.colors[d+2]=u,r.colors[d+3]=l.a)}}return r}(f,o.indices,v,a,!0);else{e.width=g,e.height=x,s.drawImage(r,0,0,g,x);const t=s.getImageData(0,0,g,x).data;a={cols:g,rows:x,indices:new Uint8Array(g*x*4),colors:new Uint8ClampedArray(t)};for(let e=0;e<t.length;e+=4){const i=Math.min(1,(.299*t[e]+.587*t[e+1]+.114*t[e+2])/255*(t[e+3]/255)*3);a.indices[e+2]=Math.round(255*i),a.indices[e]=Math.min(o.glyphs.length-1,Math.floor(i*o.glyphs.length))}}return{plan:o,packed:a,atlas:t,dots:"dots"===v.renderMode,dotSize:v.dotSizeRatio??.72}},reset(){o=null},destroy(){t.width=t.height=e.width=e.height=1,a=void 0,o=null}}}function v(t){return!!t&&"ascii"===t.renderMode&&"emoji"!==t.artStyle&&!t.charsetFrames?.length&&["fullcolor","grayscale","matrix","accent"].includes(t.colorMode)&&("accent"!==t.colorMode||/^#[\da-f]{6}$/i.test(t.accentColor))}function M(t){for(let e=t;e;e=e.parentElement){const t=getComputedStyle(e).backgroundColor.match(/[\d.]+/g)?.map(Number);if(t&&t.length>=3&&(t[3]??1)>.5)return(.299*t[0]+.587*t[1]+.114*t[2])/255<.4?[10/255,10/255,10/255,1]:[250/255,249/255,247/255,1]}const e=document.documentElement.dataset.theme;if("dark"!==e&&"light"!==e&&!t.isConnected&&document.body)return M(document.body);return"dark"===e||"light"!==e&&(document.documentElement.classList.contains("dark")||matchMedia("(prefers-color-scheme: dark)").matches)?[10/255,10/255,10/255,1]:[250/255,249/255,247/255,1]}function y(t,e,s){const r=e.style.visibility;let h=document.createElement("canvas"),l=null,c=null,u=null,p=null;const y=[],w=[];let S=!1,b=!1,E=4096,T=null,A=0,k=0,z=0,R=0,D=!1;const C=function(){const t=document.createElement("canvas"),e=t.getContext("2d");return{paint(i,s,n,a){s&&(t.width===i.canvas.width&&t.height===i.canvas.height||(t.width=i.canvas.width,t.height=i.canvas.height),e.globalCompositeOperation="copy",e.drawImage(i.canvas,0,0),e.globalCompositeOperation="destination-in",e.drawImage(s.canvas,0,0,t.width,t.height),e.globalCompositeOperation="source-in",e.fillStyle="#090909",e.fillRect(0,0,t.width,t.height),e.globalCompositeOperation="source-over",i.drawImage(t,0,0,n,a))},destroy(){t.width=t.height=1}}}();let F;const _=document.createElement("canvas"),I=document.createElement("canvas"),X=_.getContext("2d"),P=I.getContext("2d");let L=!1;const U=new a,N=x();let G=null,Y={};const B=()=>{h.className="ascii-hover-surface",h.style.cssText="position:absolute;inset:0;width:100%;height:100%;pointer-events:none",h.setAttribute("role","img"),h.setAttribute("aria-label","ASCII artwork preview"),h.dataset.renderer=l?"surface-gpu":"surface-canvas",t.prepend(h),e.style.visibility="hidden"},H=()=>{l&&(y.forEach(t=>l.deleteTexture(t)),w.forEach(t=>l.deleteShader(t)),u&&l.deleteProgram(u),p&&l.deleteBuffer(p))},W=()=>{S||c||(h.removeEventListener("webglcontextlost",W),H(),h.remove(),l=null,h=document.createElement("canvas"),c=h.getContext("2d"),B(),s())};try{if(l=h.getContext("webgl",{alpha:!0,antialias:!1,depth:!1,stencil:!1,premultipliedAlpha:!0,preserveDrawingBuffer:!0}),!l)throw new Error("WebGL unavailable");E=l.getParameter(l.MAX_TEXTURE_SIZE);const t=(t,e)=>{const i=l.createShader(t);if(w.push(i),l.shaderSource(i,e),l.compileShader(i),!l.getShaderParameter(i,l.COMPILE_STATUS))throw new Error(l.getShaderInfoLog(i)||"Shader compilation failed");return i};if(u=l.createProgram(),l.attachShader(u,t(l.VERTEX_SHADER,"attribute vec2 position; void main(){gl_Position=vec4(position,0.,1.);}")),l.attachShader(u,t(l.FRAGMENT_SHADER,"\n precision highp float;\n uniform sampler2D artwork; uniform sampler2D surface;\n uniform vec2 resolution; uniform vec2 sceneSize; uniform vec2 cellSize;\n uniform vec2 surfaceSize; uniform float strength;\n \nuniform float hoverMode;\nuniform vec4 hoverFocus;\nfloat surfaceLight(vec2 uv, vec2 size) {\n vec2 delta = (uv - hoverFocus.xy) * size / min(size.x, size.y) / hoverFocus.w;\n float light = exp(-3.0 * dot(delta, delta));\n if (hoverMode > 1.5) light = (exp(-delta.x * delta.x * 110.0) * 1.3 - exp(-delta.x * delta.x * 12.0) * .3) * exp(-delta.y * delta.y * 1.8);\n return hoverMode > .5 ? light * hoverFocus.z : 0.0;\n}\nvec3 illuminate(vec3 ink, float light) {\n return mix(ink, vec3(1.0), max(0.0, light) * .82) * (1.0 + min(0.0, light));\n}\n\n uniform sampler2D codes; uniform sampler2D cellColors; uniform sampler2D glyphAtlas;\n uniform vec2 gridSize; uniform vec2 atlasSize; uniform vec2 tileSize;\n uniform float atlasColumns; uniform float padding; uniform float glyphCount;\n uniform float dots; uniform float dotSize; uniform float directGlyphs; uniform vec4 backdrop;\n uniform float edgeEffect; uniform sampler2D textMask; uniform float hasTextMask;\n \nuniform float finishMode;\nuniform float finishPixelRatio;\nuniform float finishSoftness;\nvec3 edgeCoverage(sampler2D sheet, vec2 uv, vec2 atlas, vec2 pixel, vec2 viewport, float amount) {\n float a=texture2D(sheet,uv).a;\n if(amount<=0.0 || finishMode<.5) return vec3(a);\n vec2 radial=(pixel/viewport-.5)*2.0;\n float edge=smoothstep(.3,1.05,length(radial));\n float strength=clamp(amount,0.0,1.0)*edge;\n if(strength<=0.0) return vec3(a);\n vec2 cssPixel=pixel/max(1.0,finishPixelRatio);\n // Etch is a single texture sample: linework, not an opacity wash.\n if(finishMode>1.5 && finishMode<2.5) {\n float hatch=step(.68,fract((cssPixel.y+cssPixel.x*.22)*.5));\n return vec3(a*(1.0-hatch*strength*.64));\n }\n // Samples remain inside the current atlas tile's three-pixel padding.\n if(finishMode>2.5) {\n float band=floor(cssPixel.y/3.0);\n float phase=fract(sin(band*12.9898)*43758.5453);\n float tear=(phase-.5)*3.6*strength;\n float displaced=texture2D(sheet,uv+vec2(tear,0.0)/atlas).a;\n float cut=step(.72,fract(cssPixel.y*.5));\n return vec3(mix(a,displaced,strength)*(1.0-cut*.55*strength));\n }\n // A five-tap lens: opposed red/cyan edges with a short soft shoulder.\n // The center remains untouched; all offsets stay within three atlas texels.\n vec2 direction=normalize(vec2(radial.x-radial.y*.3,radial.y+radial.x*.3)+vec2(.00001));\n vec2 shift=direction*(2.35+.45*finishSoftness)*strength/atlas;\n float left=texture2D(sheet,uv-shift).a,right=texture2D(sheet,uv+shift).a;\n float softLeft=texture2D(sheet,uv-shift*.45).a,softRight=texture2D(sheet,uv+shift*.45).a;\n float red=left*.7+softLeft*.22+a*.08;\n float cyan=right*.7+softRight*.22+a*.08;\n // Reuse the same taps for a short optical blur and peripheral fade.\n // No whole-canvas blur, extra pass or softened HTML controls.\n float soft=a*.4+(left+right+softLeft+softRight)*.15;\n vec3 fringe=mix(vec3(red,cyan,cyan),vec3(soft),finishSoftness*.45);\n return mix(vec3(a),fringe,strength)*(1.-finishSoftness*strength*.32);\n\n}\n\n \nuniform float edgeSafe;\nuniform float trailAmount;\nuniform float trailKind;\nvec4 trailField(vec2 address) {\n return texture2D(surface,(address*(surfaceSize-1.0)+.5)/surfaceSize);\n}\nfloat trailFieldDensity(vec4 field) {\n return (field.r*65280.0+field.g*255.0)/65535.0*trailAmount;\n}\nfloat trailDensity(vec2 address) { return trailFieldDensity(trailField(address)); }\nfloat trailPin(vec2 cell, vec2 grid) {\n vec2 edge=min(cell,grid-1.0-cell);\n return mix(1.,smoothstep(1.0,5.0,min(edge.x,edge.y)),edgeSafe);\n}\nvec2 trailSource(vec2 cell, vec2 grid, vec4 field) {\n vec2 home=(cell+.5)/grid;\n if(trailKind>.5 || trailAmount<=0.) return home;\n float pin=trailPin(cell,grid);\n float response=1.-exp(-trailFieldDensity(field)*pin*3.5);\n vec2 drift=(field.ba*255.-128.)/127.;\n return clamp(home-drift*response*.026*pin,.5/grid,1.-.5/grid);\n}\nvec2 decodedGlyph(vec4 code) {\n return vec2(floor(code.r*255.+.5)+floor(code.g*255.+.5)*256.,code.b);\n}\n// Bilinear interpolation happens before glyph selection, never between bitmap\n// letters. Four small metadata reads replace the old four full-glyph gathers.\nvec2 flowGlyph(sampler2D codes,vec2 uv,vec2 grid) {\n vec2 p=uv*grid-.5,b=floor(p),f=fract(p);\n return mix(mix(decodedGlyph(texture2D(codes,(b+.5)/grid)),decodedGlyph(texture2D(codes,(b+vec2(1.5,.5))/grid)),f.x),\n mix(decodedGlyph(texture2D(codes,(b+vec2(.5,1.5))/grid)),decodedGlyph(texture2D(codes,(b+1.5)/grid)),f.x),f.y);\n}\nvec3 trailInk(vec3 ink, float density) {\n if (trailKind < .5) return ink;\n float peak=max(ink.r,max(ink.g,ink.b));\n float raised=trailKind>1.5?peak:max(peak,min(.44,density*.75));\n return peak>.001 ? ink*(raised/peak) : vec3(raised);\n}\nfloat flowIndex(float index,float count,float density) {\n float last=max(0.,count-1.);\n float response=1.-exp(-max(0.,density)*3.5);\n return max(0.,last-abs(last-(index+response*last*2.1)));\n}\nfloat trailTone(float tone, float density) {\n if (trailKind < .5) return flowIndex(tone,2.,density);\n return clamp(tone + (trailKind > 1.5 ? -density : density) * .65, 0.0, 1.0);\n}\nfloat trailIndex(float index, float count, float density, vec2 cell) {\n float grain=fract((cell.x*cell.x*17.0+cell.y*cell.y*23.0+cell.x*cell.y*19.0)*.0137);\n if(trailKind>1.5) return max(0.0,index-floor(density*count*(1.35+grain*.9)));\n if(trailKind<.5) {\n float seed=fract(sin(dot(cell,vec2(127.1,311.7)))*43758.5453);\n float response=1.-exp(-max(0.,density)*3.5);\n return floor(max(0.,flowIndex(index,count,density)-response*(1.-response)*seed*3.)+.5);\n }\n return min(count-1.0,index+floor(density*count*.85));\n}\n\n \n uniform float motionMode; uniform float motionTime; uniform float fineDither; uniform float ditherAmount;\n float printGrain(vec2 p) { return fract(52.9829189*fract(dot(p,vec2(.06711056,.00583715)))); }\n vec4 ambientAt(vec2 uv) {\n float p=motionTime*.5235987756;\n float edge=smoothstep(0.,.12,min(min(uv.x,1.-uv.x),min(uv.y,1.-uv.y)));\n if(motionMode<.5) return vec4(0.,0.,0.,1.);\n if(motionMode<1.5) return vec4(0.,0.,sin(uv.x*9.+uv.y*5.+p)*cos(uv.y*8.-uv.x*3.-p)*.065*edge,.9+.1*sin(uv.x*6.-uv.y*4.+p));\n if(motionMode<2.5) return vec4(sin(uv.y*9.+p)*cos(uv.x*7.-p)*6.*edge,cos(uv.x*8.+p)*sin(uv.y*6.-p)*4.*edge,0.,1.);\n float cycle=mod(motionTime,12.);\n float order=uv.x*.8+uv.y*.2+sin(uv.x*8.-uv.y*5.)*.08;\n float gone=smoothstep(1.8+order*1.2,3.8+order*1.2,cycle)*(1.-smoothstep(5.+order*1.7,7.2+order*1.7,cycle));\n return vec4(0.,0.,-gone*.95,1.-gone*.82);\n }\n\n float toneAt(vec2 uv) {\n vec2 p=uv*gridSize-.5, b=floor(p), f=fract(p);\n return mix(mix(texture2D(codes,(b+.5)/gridSize).b,texture2D(codes,(b+vec2(1.5,.5))/gridSize).b,f.x),mix(texture2D(codes,(b+vec2(.5,1.5))/gridSize).b,texture2D(codes,(b+1.5)/gridSize).b,f.x),f.y);\n }\n vec4 flowingCharacter(vec2 pixel, vec2 cell, vec4 ambient) {\n if(cell.x<0. || cell.y<0. || cell.x>=gridSize.x || cell.y>=gridSize.y) return vec4(0.);\n vec2 address=(cell+.5)/gridSize;\n vec4 field=trailField(address);\n vec2 local=pixel-cell*cellSize;\n if(local.x<0. || local.y<0. || local.x>=cellSize.x || local.y>=cellSize.y) return vec4(0.);\n vec2 source=trailSource(cell,gridSize,field);\n vec2 encoded=flowGlyph(codes,source,gridSize);\n vec4 color=texture2D(cellColors,source);\n float original=encoded.x;\n float density=trailFieldDensity(field)*trailPin(cell,gridSize),index=trailIndex(original,glyphCount,density,cell);\n if(original>.5) index=clamp(floor(index+ambient.z*(glyphCount-1.)+printGrain(cell)),0.,glyphCount-1.);\n if(index<.5 || color.a<.01) return vec4(0.);\n vec2 slot=vec2(mod(index,atlasColumns),floor(index/atlasColumns));\n float alpha=texture2D(glyphAtlas,(slot*tileSize+padding+local*resolution/sceneSize)/atlasSize).a;\n if(dots>.5) {\n vec2 d=(local-cellSize*.5)/min(cellSize.x,cellSize.y);\n float r=min(.48,(index/(glyphCount-1.))*.5*dotSize);\n alpha=1.-smoothstep(r-.06,r+.02,length(d));\n }\n vec3 coverage=dots>.5?vec3(alpha):edgeCoverage(glyphAtlas,(slot*tileSize+padding+local*resolution/sceneSize)/atlasSize,atlasSize,pixel,sceneSize,edgeEffect);\n coverage*=color.a;alpha=max(coverage.r,max(coverage.g,coverage.b));\n return vec4(trailInk(color.rgb,density)*coverage,alpha);\n }\n void main(){\n vec2 uv = vec2(gl_FragCoord.x, resolution.y - gl_FragCoord.y) / resolution;\n vec2 pixel = uv * sceneSize;\n if (strength > 0.) {\n vec4 normal = texture2D(surface, (uv * (surfaceSize - 1.) + .5) / surfaceSize) * 255.;\n vec2 slope = (vec2(normal.r * 256. + normal.g, normal.b * 256. + normal.a) - 32768.) / 32767.;\n vec2 edge = min(pixel, sceneSize - pixel) / cellSize;\n uv -= slope * strength * mix(1.,smoothstep(2.,8.,min(edge.x,edge.y)),edgeSafe) / sceneSize;\n }\n vec4 ambient=ambientAt(uv);\n uv -= ambient.xy/sceneSize;\n pixel=uv*sceneSize;\n vec4 ink = vec4(0.);\n if (directGlyphs > .5) {\n vec2 cell=floor(pixel/cellSize), address=(cell+.5)/gridSize;\n vec4 encoded=texture2D(codes,address), color=texture2D(cellColors,address);\n float index=floor(encoded.r*255.+.5)+floor(encoded.g*255.+.5)*256.;\n if(index>.5 && color.a>.01) {\n vec2 local=(pixel-cell*cellSize)*resolution/sceneSize;\n vec2 slot=vec2(mod(index,atlasColumns),floor(index/atlasColumns));\n float alpha=texture2D(glyphAtlas,(slot*tileSize+padding+local)/atlasSize).a*color.a;\n vec3 coverage=edgeCoverage(glyphAtlas,(slot*tileSize+padding+local)/atlasSize,atlasSize,pixel,sceneSize,edgeEffect)*color.a;\n ink=vec4(color.rgb*coverage,max(coverage.r,max(coverage.g,coverage.b)));\n }\n } else ink = texture2D(artwork, uv);\n if (fineDither > .5) {\n vec2 cell=floor(pixel/cellSize),address=(cell+.5)/gridSize;\n vec4 color=texture2D(cellColors,address);\n float density=trailAmount>0.?trailDensity(address):0.;\n vec2 source=trailAmount>0. && trailKind<.5?trailSource(cell,gridSize,trailField(address)):uv;\n float tone=trailTone(clamp(toneAt(source)+ambient.z,0.,1.),density*(trailKind<.5?trailPin(cell,gridSize):1.));\n float pitch=max(1.,min(cellSize.x,cellSize.y)*.32);\n float grain=.5+(printGrain(floor(pixel/pitch))-.5)*ditherAmount;\n float alpha=(tone<.001?0.:tone>.999?1.:smoothstep(grain-.035,grain+.035,tone))*color.a*ambient.w;\n // Preserve hue; tone is carried by mark coverage rather than muddy ink.\n vec3 hue=color.rgb/max(.001,max(color.r,max(color.g,color.b)));\n if(hasTextMask>.5) hue=mix(hue,vec3(.035),texture2D(textMask,vec2(gl_FragCoord.x,resolution.y-gl_FragCoord.y)/resolution).a);\n gl_FragColor=vec4(hue*alpha,alpha); return;\n }\n if ((trailAmount > 0. && trailKind > .5) || motionMode > .5) {\n vec2 cell=floor(pixel/cellSize), address=(cell+.5)/gridSize;\n float density=trailAmount>0.?trailDensity(address):0.;\n vec4 encoded=texture2D(codes,address);\n float original=floor(encoded.r*255.+.5)+floor(encoded.g*255.+.5)*256.;\n float index=trailIndex(original,glyphCount,density,cell);\n if(original>.5) index=clamp(floor(index+ambient.z*(glyphCount-1.)+printGrain(cell)),0.,glyphCount-1.);\n vec4 color=texture2D(cellColors,address);\n if((index!=original || motionMode>.5) && color.a>.01) {\n vec2 local=(pixel-cell*cellSize)*resolution/sceneSize;\n vec2 slot=vec2(mod(index,atlasColumns),floor(index/atlasColumns));\n float alpha=texture2D(glyphAtlas,(slot*tileSize+padding+local)/atlasSize).a;\n if(dots>.5) {\n vec2 d=(pixel-(cell+.5)*cellSize)/min(cellSize.x,cellSize.y);\n float r=min(.48,(index/(glyphCount-1.))*.5*dotSize);\n alpha=1.-smoothstep(r-.06,r+.02,length(d));\n }\n // Replace the cell, rather than blending two glyphs into a ghost.\n vec3 rgb=trailInk(color.rgb,density);\n ink=vec4(rgb*alpha*color.a,alpha*color.a);\n }\n }\n if(trailAmount>0. && trailKind<.5 && trailDensity((floor(pixel/cellSize)+.5)/gridSize)>.001) {\n ink=flowingCharacter(pixel,floor(pixel/cellSize),ambient);\n }\n vec3 straightInk = ink.a > .0 ? ink.rgb / ink.a : vec3(.0);\n float value = max(straightInk.r, max(straightInk.g, straightInk.b));\n // Lighting belongs to character ink; black/transparent gaps stay empty.\n float light = surfaceLight((floor(pixel / cellSize) + .5) * cellSize / sceneSize, sceneSize);\n vec3 lit = illuminate(straightInk, light * smoothstep(0., .22, value)) * ink.a;\n if(hasTextMask>.5) lit=mix(lit,vec3(.035)*ink.a,texture2D(textMask,vec2(gl_FragCoord.x,resolution.y-gl_FragCoord.y)/resolution).a);\n vec4 result = vec4(lit, ink.a)*ambient.w;\n gl_FragColor = directGlyphs > .5 ? result + backdrop*(1.-result.a) : result;\n }")),l.linkProgram(u),!l.getProgramParameter(u,l.LINK_STATUS))throw new Error("Surface shader link failed");l.useProgram(u),p=l.createBuffer(),l.bindBuffer(l.ARRAY_BUFFER,p),l.bufferData(l.ARRAY_BUFFER,new Float32Array([-1,-1,1,-1,-1,1,1,1]),l.STATIC_DRAW);const e=l.getAttribLocation(u,"position");l.enableVertexAttribArray(e),l.vertexAttribPointer(e,2,l.FLOAT,!1,0,0),Y=Object.fromEntries(["finishSoftness","finishMode","finishPixelRatio","edgeEffect","textMask","hasTextMask","artwork","surface","resolution","sceneSize","cellSize","surfaceSize","strength","hoverMode","hoverFocus","codes","cellColors","glyphAtlas","gridSize","atlasSize","tileSize","atlasColumns","padding","glyphCount","dots","dotSize","directGlyphs","backdrop","trailKind","edgeSafe","trailAmount","motionMode","motionTime","fineDither","ditherAmount"].map(t=>[t,l.getUniformLocation(u,t)]));for(let t=0;t<6;t++){const e=l.createTexture();y.push(e),l.activeTexture(l.TEXTURE0+t),l.bindTexture(l.TEXTURE_2D,e),l.texParameteri(l.TEXTURE_2D,l.TEXTURE_MIN_FILTER,l.LINEAR),l.texParameteri(l.TEXTURE_2D,l.TEXTURE_MAG_FILTER,l.LINEAR),l.texImage2D(l.TEXTURE_2D,0,l.RGBA,1,1,0,l.RGBA,l.UNSIGNED_BYTE,new Uint8Array(4)),l.texParameteri(l.TEXTURE_2D,l.TEXTURE_WRAP_S,l.CLAMP_TO_EDGE),l.texParameteri(l.TEXTURE_2D,l.TEXTURE_WRAP_T,l.CLAMP_TO_EDGE)}l.uniform1i(Y.textMask,5),l.uniform1i(Y.codes,2),l.uniform1i(Y.cellColors,3),l.uniform1i(Y.glyphAtlas,4),l.uniform1i(Y.artwork,0),l.uniform1i(Y.surface,1),l.pixelStorei(l.UNPACK_PREMULTIPLY_ALPHA_WEBGL,!1),l.disable(l.BLEND),l.disable(l.DEPTH_TEST),h.addEventListener("webglcontextlost",W),B()}catch{W()}const O=()=>{S||(N.reset(),G=null,b=!1,s())};return document.fonts?.addEventListener("loadingdone",O),document.fonts?.ready.then(O),{get canvas(){return h},canRenderGlyphs:t=>!!l&&!S&&v(t),render(t,s,a,r,u,p,x,w,B=0,H={}){const O=i(w?.animationStyle),K=U.update(B,O,w?.animationSpeed),$=(t=>"terminal"===t?.artStyle&&!t.customText&&!t.charsetFrames?.length&&!/[A-Za-z]/.test(t.charset??""))(w),q=!!l&&!!x&&v(w);if(q!==D&&(b=!1,D=q),S||!a||!r||!e.width||!e.height)return;const j=Math.min(e.width/a,1.5,Math.sqrt(4e6/(a*r))),J=Math.max(1,Math.round(a*j)),V=Math.max(1,Math.round(r*j));if(h.width===J&&h.height===V||(h.width=J,h.height=V,b=!1),l&&(e.width>E||e.height>E)&&W(),(q||d(s.mode)||"none"!==O||$)&&(t||!b||!G)&&(G=N.update(e,a,r,u,p,j,x,w),l)){const{plan:t,packed:e,atlas:i}=G;for(const[t,i]of[[2,e.indices],[3,e.colors]])l.activeTexture(l.TEXTURE0+t),l.bindTexture(l.TEXTURE_2D,y[t]),l.texParameteri(l.TEXTURE_2D,l.TEXTURE_MIN_FILTER,3===t?l.LINEAR:l.NEAREST),l.texParameteri(l.TEXTURE_2D,l.TEXTURE_MAG_FILTER,3===t?l.LINEAR:l.NEAREST),A!==e.cols||k!==e.rows?l.texImage2D(l.TEXTURE_2D,0,l.RGBA,e.cols,e.rows,0,l.RGBA,l.UNSIGNED_BYTE,i):l.texSubImage2D(l.TEXTURE_2D,0,0,0,e.cols,e.rows,l.RGBA,l.UNSIGNED_BYTE,i);A=e.cols,k=e.rows,T!==t&&(l.activeTexture(l.TEXTURE4),l.bindTexture(l.TEXTURE_2D,y[4]),l.texImage2D(l.TEXTURE_2D,0,l.RGBA,l.RGBA,l.UNSIGNED_BYTE,i),T=t),l.uniform2f(Y.gridSize,e.cols,e.rows),l.uniform2f(Y.atlasSize,t.atlasWidth,t.atlasHeight),l.uniform2f(Y.tileSize,t.tileWidth,t.tileHeight),l.uniform1f(Y.atlasColumns,t.atlasColumns),l.uniform1f(Y.padding,t.padding),l.uniform1f(Y.glyphCount,t.glyphs.length),l.uniform1f(Y.dots,G.dots?1:0),l.uniform1f(Y.dotSize,G.dotSize)}if(l){l.uniform1f(Y.motionMode,(t=>["none","print","current","reform"].indexOf(t))(O)),l.uniform1f(Y.motionTime,K),l.uniform1f(Y.fineDither,$?1:0),l.uniform1f(Y.ditherAmount,Math.max(0,Math.min(2,w?.ditherStrength??1))),l.uniform1f(Y.trailKind,"dissolve"===s.mode?2:"contour"===s.mode?1:0),l.uniform1f(Y.edgeSafe,s.edgeSafe?1:0),l.uniform1f(Y.trailAmount,d(s.mode)?s.focus[2]:0),l.viewport(0,0,J,V),q||!t&&b||(l.activeTexture(l.TEXTURE0),l.bindTexture(l.TEXTURE_2D,y[0]),l.pixelStorei(l.UNPACK_PREMULTIPLY_ALPHA_WEBGL,!0),l.texImage2D(l.TEXTURE_2D,0,l.RGBA,l.RGBA,l.UNSIGNED_BYTE,e),l.pixelStorei(l.UNPACK_PREMULTIPLY_ALPHA_WEBGL,!1)),l.uniform1f(Y.directGlyphs,q?1:0),!q||!t&&b||l.uniform4fv(Y.backdrop,H.textMask||function(t){const e=t.length,i=t[0]?.length??0;for(let s=0;s<e;s+=Math.max(1,e>>2))for(let e=0;e<i;e+=Math.max(1,i>>2))if(t[s][e].a<200)return!0;return!1}(x)?[0,0,0,0]:M(e)),b=!0,l.activeTexture(l.TEXTURE1),l.bindTexture(l.TEXTURE_2D,y[1]),z!==s.width||R!==s.height?(l.texImage2D(l.TEXTURE_2D,0,l.RGBA,s.width,s.height,0,l.RGBA,l.UNSIGNED_BYTE,s.pixels),z=s.width,R=s.height):l.texSubImage2D(l.TEXTURE_2D,0,0,0,s.width,s.height,l.RGBA,l.UNSIGNED_BYTE,s.pixels),l.uniform2f(Y.resolution,J,V),l.uniform2f(Y.sceneSize,a,r),l.uniform2f(Y.cellSize,u,p),l.uniform2f(Y.surfaceSize,s.width,s.height),l.uniform1f(Y.strength,s.strength),l.uniform1f(Y.hoverMode,"light"===s.mode?1:"scan"===s.mode?2:0),l.uniform4fv(Y.hoverFocus,s.focus),H.textMask&&H.textMask!==F&&(l.activeTexture(l.TEXTURE5),l.bindTexture(l.TEXTURE_2D,y[5]),l.texImage2D(l.TEXTURE_2D,0,l.RGBA,l.RGBA,l.UNSIGNED_BYTE,H.textMask.canvas),F=H.textMask),l.uniform1f(Y.hasTextMask,H.textMask?1:0);const i=function(t={}){const e=g.some(e=>e.value===t.filter)?t.filter:"prism",i=Number.isFinite(t.edgeEffect)?Math.max(0,Math.min(1,t.edgeEffect)):t.filter?1:0,s=Number.isFinite(t.edgeSoftness)?Math.max(0,Math.min(1,t.edgeSoftness)):0;return{mode:["clean","prism","etch","signal"].indexOf(e),amount:"clean"===e?0:i,softness:"prism"===e?s:0}}(H);l.uniform1f(Y.edgeEffect,i.amount),l.uniform1f(Y.finishMode,i.mode),l.uniform1f(Y.finishSoftness,i.softness),l.uniform1f(Y.finishPixelRatio,1),l.drawArrays(l.TRIANGLE_STRIP,0,4)}else if(c){if(c.setTransform(j,0,0,j,0,0),c.clearRect(0,0,a,r),c.drawImage(e,0,0,a,r),("none"!==O||$)&&G){c.clearRect(0,0,a,r);const{plan:t,packed:e,atlas:i}=G,h=[0,0,0,1],l=[0,0,0,0,0,0],g=Math.max(1,.32*Math.min(u,p));for(let a=0;a<e.rows;a++)for(let r=0;r<e.cols;r++){const x=(r+.5)/e.cols,v=(a+.5)/e.rows,M=4*(a*e.cols+r);if(e.colors[M+3]<10)continue;o(O,x,v,K,h);const y=4*(Math.min(s.height-1,Math.round(v*(s.height-1)))*s.width+Math.min(s.width-1,Math.round(x*(s.width-1)))),S=d(s.mode)?(256*s.pixels[y]+s.pixels[y+1])/65535*s.focus[2]:0,b=e.indices[M]+256*e.indices[M+1];"trail"===s.mode&&f(e.indices,e.colors,e.cols,e.rows,r,a,S,(s.pixels[y+2]-128)/127,(s.pixels[y+3]-128)/127,t.glyphs.length,l,s.edgeSafe);let E="trail"===s.mode?l[0]:"dissolve"===s.mode?b-Math.floor(S*t.glyphs.length*1.8):b+Math.floor(S*t.glyphs.length*.85);b>0&&(E+=h[2]*(t.glyphs.length-1)),E=Math.max(0,Math.min(t.glyphs.length-1,Math.floor(E+n(r,a))));let T=h[0],A=h[1];s.strength>0&&(T+=(256*s.pixels[y]+s.pixels[y+1]-32768)/32767*s.strength,A+=(256*s.pixels[y+2]+s.pixels[y+3]-32768)/32767*s.strength);const k=r*u+T,z=a*p+A,R=("trail"===s.mode?l[5]:e.colors[M+3])/255*h[3],D="trail"===s.mode?l[2]:e.colors[M],C="trail"===s.mode?l[3]:e.colors[M+1],F="trail"===s.mode?l[4]:e.colors[M+2],_=$?Math.max(1,D,C,F):255,I=`rgb(${D/_*255},${C/_*255},${F/_*255})`;if(c.fillStyle=I,$){const t="trail"===s.mode?m(Math.max(0,Math.min(1,l[1]+h[2])),2,S):Math.max(0,Math.min(1,e.indices[M+2]/255+h[2]+.65*("dissolve"===s.mode?-S:S)));c.globalAlpha=R;for(let e=0;e<p;e+=g)for(let i=0;i<u;i+=g)t>.5+(n(Math.floor((k+i)/g),Math.floor((z+e)/g))-.5)*Math.max(0,Math.min(2,w?.ditherStrength??1))&&c.fillRect(k+i,z+e,Math.min(g,u-i),Math.min(g,p-e))}else if(E>0)if(c.globalAlpha=R,G.dots){const e=Math.min(.48,E/(t.glyphs.length-1)*.5*G.dotSize)*Math.min(u,p);c.beginPath(),c.arc(k+.5*u,z+.5*p,e,0,2*Math.PI),c.fill()}else c.drawImage(i,E%t.atlasColumns*t.tileWidth+t.padding,Math.floor(E/t.atlasColumns)*t.tileHeight+t.padding,t.cellWidth*j,t.cellHeight*j,k,z,u,p),c.globalAlpha=1,c.globalCompositeOperation="source-atop",c.fillRect(k,z,u,p),c.globalCompositeOperation="source-over"}c.globalAlpha=1}else if(d(s.mode)&&G){const{plan:t,packed:e,atlas:i}=G,n=[0,0,0,0,0,0];for(let a=0;a<e.rows;a++)for(let o=0;o<e.cols;o++){const r=4*(Math.min(s.height-1,Math.round((a+.5)/e.rows*(s.height-1)))*s.width+Math.min(s.width-1,Math.round((o+.5)/e.cols*(s.width-1)))),h=(256*s.pixels[r]+s.pixels[r+1])/65535*s.focus[2],l=4*(a*e.cols+o),d=.0137*(o*o*17+a*a*23+o*a*19),m=d-Math.floor(d);"trail"===s.mode&&f(e.indices,e.colors,e.cols,e.rows,o,a,h,(s.pixels[r+2]-128)/127,(s.pixels[r+3]-128)/127,t.glyphs.length,n,s.edgeSafe);const g=e.indices[l]+256*e.indices[l+1],x="trail"===s.mode?Math.round(n[0]):"dissolve"===s.mode?Math.max(0,g-Math.floor(h*t.glyphs.length*(1.35+.9*m))):Math.min(t.glyphs.length-1,g+Math.floor(h*t.glyphs.length*.85));if(x===g&&("trail"!==s.mode||h<1e-4)||e.colors[l+3]<10)continue;const v=o*u,M=a*p;if(c.clearRect(v,M,u,p),c.globalAlpha=("trail"===s.mode?n[5]:e.colors[l+3])/255,G.dots){const e=Math.min(.48,x/(t.glyphs.length-1)*.5*G.dotSize)*Math.min(u,p);c.fillStyle="#fff",c.beginPath(),c.arc(v+.5*u,M+.5*p,e,0,2*Math.PI),c.fill()}else c.drawImage(i,x%t.atlasColumns*t.tileWidth+t.padding,Math.floor(x/t.atlasColumns)*t.tileHeight+t.padding,t.cellWidth*j,t.cellHeight*j,v,M,u,p);c.globalAlpha=1,c.globalCompositeOperation="source-atop";const y="trail"===s.mode?n[2]:e.colors[l],w="trail"===s.mode?n[3]:e.colors[l+1],S="trail"===s.mode?n[4]:e.colors[l+2],b=Math.max(y,w,S),E="dissolve"===s.mode||"trail"===s.mode?b:Math.max(b,Math.min(112,191*h)),T=b?E/b:0;c.fillStyle=`rgb(${b?y*T:E},${b?w*T:E},${b?S*T:E})`,c.fillRect(v,M,u,p),c.globalCompositeOperation="source-over"}}else if("light"===s.mode||"scan"===s.mode){!t&&L&&_.width===J&&_.height===V||(_.width=J,_.height=V,I.width=J,I.height=V,X.filter="brightness(3)",X.drawImage(e,0,0,J,V),X.filter="none",L=!0);const[i,n,o,h]=s.focus;if(o>.001){P.setTransform(1,0,0,1,0,0),P.clearRect(0,0,J,V),P.drawImage(_,0,0),P.globalCompositeOperation="destination-in",P.save(),P.translate(i*J,n*V),P.scale("scan"===s.mode?.13:1,1);const t=h*Math.min(J,V),e=P.createRadialGradient(0,0,0,0,0,t);e.addColorStop(0,`rgba(255,255,255,${o})`),e.addColorStop(.5,`rgba(255,255,255,${.4*o})`),e.addColorStop(1,"rgba(255,255,255,0)"),P.fillStyle=e,P.fillRect(10*-J,10*-V,20*J,20*V),P.restore(),P.globalCompositeOperation="source-over",c.globalCompositeOperation="source-atop",c.drawImage(I,0,0,a,r),c.globalCompositeOperation="source-over"}}else if(s.strength>0){const t=32,i=18,n=a/t,o=r/i;for(let h=0;h<i;h++)for(let l=0;l<t;l++){const d=(l+.5)/t,m=(h+.5)/i,f=Math.min(s.width-1,Math.round(d*(s.width-1))),g=4*(Math.min(s.height-1,Math.round(m*(s.height-1)))*s.width+f),x=s.pixels,v=Math.min(l*n/u,(a-(l+1)*n)/u,h*o/p,(r-(h+1)*o)/p),M=Math.max(0,Math.min(1,(v-2)/6)),y=s.edgeSafe?M*M*(3-2*M):1,w=(256*x[g]+x[g+1]-32768)/32767*s.strength*y,S=(256*x[g+2]+x[g+3]-32768)/32767*s.strength*y;Math.abs(w)+Math.abs(S)<.02||(c.clearRect(l*n,h*o,n,o),c.drawImage(e,Math.max(0,Math.min(a-n,l*n-w))*e.width/a,Math.max(0,Math.min(r-o,h*o-S))*e.height/r,n*e.width/a,o*e.height/r,l*n,h*o,n,o))}}C.paint(c,H.textMask,a,r)}},destroy(){S=!0,C.destroy(),document.fonts?.removeEventListener("loadingdone",O),N.destroy(),h.removeEventListener("webglcontextlost",W),H(),h.remove(),_.width=_.height=I.width=I.height=1,e.style.visibility=r,l?.getExtension("WEBGL_lose_context")?.loseContext()}}}function w(e,s,n={}){let a,o,r={effect:"trail",strength:.55,radius:n.effect&&"trail"!==n.effect?.2:.45,animated:!1,fps:30,fontSize:7,charAspect:.58,charSpacing:1,paused:!1,charset:" .:-=+*#%@",customText:"",renderMode:"ascii",dotSizeRatio:.72,...n},h=new u(s.clientWidth/Math.max(1,s.clientHeight)),l=!1,c=!1,d=!0,m=0,f=0,g=-1/0,p=0,x=null,v=!1,M=[];const w=matchMedia("(prefers-reduced-motion: reduce)"),S=()=>{l||!c||document.hidden||m||(m=requestAnimationFrame(E))},b=y(e,s,()=>queueMicrotask(()=>{d=!0,S()}));function E(t){if(m=0,l||!c||document.hidden)return;const e=s.clientWidth,n=s.clientHeight;if(!e||!n)return;const u=w.matches||r.paused;h.configure(r.effect,r.strength,r.radius,r.edgeSafe);const y=h.active;if(!u){if(p+=f?Math.min(.1,(t-f)/1e3):0,v){const t=s.getBoundingClientRect();if(t.width&&t.height)for(const e of M)h.move((e.x-t.left)/t.width,(e.y-t.top)/t.height,e.time);M=[],x||h.leave(),v=!1}h.step(f?(t-f)/1e3:1/60)}const E=d||r.animated&&!u&&t-g>=1e3/Math.max(1,Math.min(60,r.fps))-.5;(E||y||h.active||!u&&"none"!==i(r.motion))&&(E&&(a=r.getFrame?.()),b.render(E,h.refraction,e,n,r.fontSize*r.charSpacing,r.fontSize/r.charAspect*r.charSpacing,a,{charset:r.charset,customText:r.customText,renderMode:r.renderMode,dotSizeRatio:r.dotSizeRatio,colorMode:r.colorMode??"fullcolor",accentColor:r.accentColor??"#888888",animationStyle:r.motion,animationSpeed:r.motionSpeed??1,artStyle:r.fineDither?"terminal":"classic",ditherStrength:r.ditherStrength??1},p,{filter:r.filter,edgeEffect:r.edgeEffect,edgeSoftness:r.edgeSoftness,textMask:o?.read()}),E&&(g=t),d=!1),f=t,u||!r.animated&&!h.active&&"none"===i(r.motion)||S()}r.textMask&&(o=t(e,r.textMask,()=>{d=!0,S()}));const T=()=>{h.clear(),x=null,M=[],v=!1,f=0,d=!0,cancelAnimationFrame(m),m=0,S()},A=t=>{if("touch"===t.pointerType||r.paused||w.matches)return;if(t.target instanceof Element&&t.target.closest("button,a,input,summary,select"))return void k();const e=s.getBoundingClientRect();t.clientX<e.left||t.clientX>e.right||t.clientY<e.top||t.clientY>e.bottom?k():(x={x:t.clientX,y:t.clientY,time:t.timeStamp},M.push(x),M.length>6&&M.shift(),v=!0,S())},k=()=>{x=null,v=!0,S()},z=new IntersectionObserver(([t])=>{c=t.isIntersecting,T()});z.observe(e);const R=new ResizeObserver(()=>{h=new u(s.clientWidth/Math.max(1,s.clientHeight)),T()});return R.observe(s),e.addEventListener("pointermove",A),e.addEventListener("pointerleave",k),e.addEventListener("pointercancel",k),document.addEventListener("visibilitychange",T),w.addEventListener("change",T),{get canvas(){return b.canvas},invalidate(){o?.invalidate(),d=!0,S()},update(i){"textMask"in i&&(o?.destroy(),o=i.textMask?t(e,i.textMask,()=>{d=!0,S()}):void 0),r={...r,...i},T()},destroy(){l||(l=!0,cancelAnimationFrame(m),z.disconnect(),R.disconnect(),b.destroy(),o?.destroy(),e.removeEventListener("pointermove",A),e.removeEventListener("pointerleave",k),e.removeEventListener("pointercancel",k),document.removeEventListener("visibilitychange",T),w.removeEventListener("change",T))}}}exports.createTextMask=t,exports.mountHover=w,exports.mountStudioHover=w;//# sourceMappingURL=hover.cjs.map
package/dist/hover.d.cts CHANGED
@@ -1,5 +1,12 @@
1
1
  import { d as AsciiFrame } from './types-w2CRgyk6.cjs';
2
2
 
3
+ type SurfaceFilter = 'clean' | 'prism' | 'etch' | 'signal';
4
+ interface SurfaceFinish {
5
+ filter?: SurfaceFilter;
6
+ edgeEffect?: number;
7
+ edgeSoftness?: number;
8
+ }
9
+
3
10
  type AmbientMotion = 'none' | 'print' | 'current' | 'reform';
4
11
 
5
12
  type HeroHover = 'none' | 'trail' | 'water' | 'contour' | 'dissolve' | 'silk' | 'vortex' | 'light' | 'scan';
@@ -7,6 +14,16 @@ type HeroHover = 'none' | 'trail' | 'water' | 'contour' | 'dissolve' | 'silk' |
7
14
  type StudioHoverEffect = Exclude<HeroHover, 'light' | 'scan'>;
8
15
  interface StudioHoverOptions {
9
16
  effect?: StudioHoverEffect;
17
+ /** 0..1 peripheral scanline/fringe treatment. Center stays sharp. */
18
+ edgeEffect?: number;
19
+ /** 0..1 Prism edge blur/fade. The center stays crisp; default 0. */
20
+ edgeSoftness?: number;
21
+ /** Stationary glyph finish. Defaults to Prism when edgeEffect is supplied. */
22
+ filter?: SurfaceFilter;
23
+ /** Plain horizontal HTML text to darken ASCII over; rebuilds on layout/font changes. */
24
+ textMask?: HTMLElement | readonly HTMLElement[];
25
+ colorMode?: 'fullcolor' | 'grayscale' | 'accent' | 'matrix';
26
+ accentColor?: string;
10
27
  /** Supply the current object frame when available for exact glyph identities/colors. */
11
28
  getFrame?: () => AsciiFrame | undefined;
12
29
  motion?: AmbientMotion;
@@ -15,6 +32,8 @@ interface StudioHoverOptions {
15
32
  ditherStrength?: number;
16
33
  strength?: number;
17
34
  radius?: number;
35
+ /** Pin outer glyph rows and soften displacement near borders. Default false. */
36
+ edgeSafe?: boolean;
18
37
  /** Enable when the backing canvas is drawn by a video/animation renderer. */
19
38
  animated?: boolean;
20
39
  fps?: number;
@@ -40,6 +59,17 @@ declare function mountStudioHover(host: HTMLElement, source: HTMLCanvasElement,
40
59
  destroy(): void;
41
60
  };
42
61
 
62
+ /** Cached mask for untransformed, horizontal HTML text. No per-frame DOM reads. */
63
+ interface TextMaskFrame {
64
+ canvas: HTMLCanvasElement;
65
+ revision: number;
66
+ }
67
+ declare function createTextMask(host: HTMLElement, targets: HTMLElement | readonly HTMLElement[], onChange?: () => void): {
68
+ invalidate: () => void;
69
+ read(): TextMaskFrame | undefined;
70
+ destroy(): void;
71
+ };
72
+
43
73
  type HoverController = ReturnType<typeof mountStudioHover>;
44
74
 
45
- export { type AmbientMotion, type HoverController, type StudioHoverOptions as HoverOptions, type StudioHoverEffect, type StudioHoverOptions, mountStudioHover as mountHover, mountStudioHover };
75
+ export { type AmbientMotion, type HoverController, type StudioHoverOptions as HoverOptions, type StudioHoverEffect, type StudioHoverOptions, type SurfaceFilter, type SurfaceFinish, type TextMaskFrame, createTextMask, mountStudioHover as mountHover, mountStudioHover };
package/dist/hover.d.ts CHANGED
@@ -1,5 +1,12 @@
1
1
  import { d as AsciiFrame } from './types-w2CRgyk6.js';
2
2
 
3
+ type SurfaceFilter = 'clean' | 'prism' | 'etch' | 'signal';
4
+ interface SurfaceFinish {
5
+ filter?: SurfaceFilter;
6
+ edgeEffect?: number;
7
+ edgeSoftness?: number;
8
+ }
9
+
3
10
  type AmbientMotion = 'none' | 'print' | 'current' | 'reform';
4
11
 
5
12
  type HeroHover = 'none' | 'trail' | 'water' | 'contour' | 'dissolve' | 'silk' | 'vortex' | 'light' | 'scan';
@@ -7,6 +14,16 @@ type HeroHover = 'none' | 'trail' | 'water' | 'contour' | 'dissolve' | 'silk' |
7
14
  type StudioHoverEffect = Exclude<HeroHover, 'light' | 'scan'>;
8
15
  interface StudioHoverOptions {
9
16
  effect?: StudioHoverEffect;
17
+ /** 0..1 peripheral scanline/fringe treatment. Center stays sharp. */
18
+ edgeEffect?: number;
19
+ /** 0..1 Prism edge blur/fade. The center stays crisp; default 0. */
20
+ edgeSoftness?: number;
21
+ /** Stationary glyph finish. Defaults to Prism when edgeEffect is supplied. */
22
+ filter?: SurfaceFilter;
23
+ /** Plain horizontal HTML text to darken ASCII over; rebuilds on layout/font changes. */
24
+ textMask?: HTMLElement | readonly HTMLElement[];
25
+ colorMode?: 'fullcolor' | 'grayscale' | 'accent' | 'matrix';
26
+ accentColor?: string;
10
27
  /** Supply the current object frame when available for exact glyph identities/colors. */
11
28
  getFrame?: () => AsciiFrame | undefined;
12
29
  motion?: AmbientMotion;
@@ -15,6 +32,8 @@ interface StudioHoverOptions {
15
32
  ditherStrength?: number;
16
33
  strength?: number;
17
34
  radius?: number;
35
+ /** Pin outer glyph rows and soften displacement near borders. Default false. */
36
+ edgeSafe?: boolean;
18
37
  /** Enable when the backing canvas is drawn by a video/animation renderer. */
19
38
  animated?: boolean;
20
39
  fps?: number;
@@ -40,6 +59,17 @@ declare function mountStudioHover(host: HTMLElement, source: HTMLCanvasElement,
40
59
  destroy(): void;
41
60
  };
42
61
 
62
+ /** Cached mask for untransformed, horizontal HTML text. No per-frame DOM reads. */
63
+ interface TextMaskFrame {
64
+ canvas: HTMLCanvasElement;
65
+ revision: number;
66
+ }
67
+ declare function createTextMask(host: HTMLElement, targets: HTMLElement | readonly HTMLElement[], onChange?: () => void): {
68
+ invalidate: () => void;
69
+ read(): TextMaskFrame | undefined;
70
+ destroy(): void;
71
+ };
72
+
43
73
  type HoverController = ReturnType<typeof mountStudioHover>;
44
74
 
45
- export { type AmbientMotion, type HoverController, type StudioHoverOptions as HoverOptions, type StudioHoverEffect, type StudioHoverOptions, mountStudioHover as mountHover, mountStudioHover };
75
+ export { type AmbientMotion, type HoverController, type StudioHoverOptions as HoverOptions, type StudioHoverEffect, type StudioHoverOptions, type SurfaceFilter, type SurfaceFinish, type TextMaskFrame, createTextMask, mountStudioHover as mountHover, mountStudioHover };