@liiift-studio/threadtext 0.0.1 → 0.1.1

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
@@ -2,12 +2,12 @@
2
2
 
3
3
  **Render any text as photorealistic, procedural satin-stitch embroidery — in real time, in the browser, from the font's actual glyph geometry.**
4
4
 
5
- > Status: **in-progress.** The framework-agnostic core (`createThreadText`) + React
6
- > hook/component are now extracted, typed, tested, and building to ESM/CJS/`.d.ts`. Still
7
- > to come: Framer/Webflow bindings and the landing site (and a free-licensed demo font).
8
- > The original reference renderer lives in [`demo/index.html`](./demo/index.html) and stays
9
- > the visual oracle. See [`HANDOFF.md`](./HANDOFF.md) for what's left; tool-build conventions
10
- > are in the monorepo [`../GUIDE.md`](../GUIDE.md).
5
+ > Status: **shipped.** Published to npm (`@liiift-studio/threadtext`) with a framework-agnostic
6
+ > core (`createThreadText`), a React hook + component, Framer and Webflow ports, tests, and a
7
+ > live landing site at [threadtext.com](https://threadtext.com) (demo font: the OFL Fraunces
8
+ > variable). The original single-file reference renderer lives in
9
+ > [`demo/index.html`](./demo/index.html) as the visual oracle; tool-build conventions are in the
10
+ > monorepo [`../GUIDE.md`](../GUIDE.md).
11
11
 
12
12
  Homepage: [threadtext.com](https://threadtext.com) · npm: `@liiift-studio/threadtext`
13
13
 
@@ -15,7 +15,7 @@ Homepage: [threadtext.com](https://threadtext.com) · npm: `@liiift-studio/threa
15
15
 
16
16
  ## What it is
17
17
 
18
- Give it a word set in any loaded (variable) font and it renders it as **raised white satin floss on woven fabric** — threads that run *across* each stroke and fan around the curves, lifted into 3D, seated with a contact shadow, and sewn in one satin stitch at a time. It's photorealistic **by construction** (real thread geometry + lighting), not a raster filter and not an AI image.
18
+ Give it a word set in any loaded (variable) font and it renders it as **raised satin floss on a transparent ground** — threads that run *across* each stroke and fan around the curves, lifted into 3D, and sewn in one satin stitch at a time. Drop it over any background. It's photorealistic **by construction** (real thread geometry + lighting), not a raster filter and not an AI image.
19
19
 
20
20
  ## Why it's worth building (the niche is empty)
21
21
 
@@ -44,7 +44,7 @@ Type to embroider a new word · Backspace to unpick · Enter to replay the sew-i
44
44
 
45
45
  ## How it works (one paragraph)
46
46
 
47
- Rasterise the glyphs → compute a signed-distance field → derive a **flow field** (smoothed in double-angle orientation space so opposite edge-normals reinforce) so threads run across each stroke → lay thousands of discrete pre-shaded **thread sprites** oriented by the flow → lift to 3D with a dome-shade/normal map → seat on procedural woven fabric with a contact shadow → **sew it in** one BFS cross-row at a time (the needle follows the stroke) → a subtle cursor-driven sheen. Full walkthrough with line references in `HANDOFF.md`.
47
+ Rasterise the glyphs → compute a signed-distance field → derive a **flow field** (smoothed in double-angle orientation space so opposite edge-normals reinforce) so threads run across each stroke → lay thousands of discrete pre-shaded **thread sprites** oriented by the flow → lift to 3D with a dome-shade/normal map on a transparent ground → **sew it in** one BFS cross-row at a time (the needle follows the stroke) → a subtle cursor-driven sheen. Full walkthrough with line references in `HANDOFF.md`.
48
48
 
49
49
  ## Use it (extracted API)
50
50
 
@@ -58,10 +58,12 @@ const thread = createThreadText(document.getElementById('host'), {
58
58
  weight: 680,
59
59
  })
60
60
 
61
- thread.setText('Threads') // appended letters sew in; unrelated text re-sews the word
62
- thread.replay() // re-run the full sew-in
63
- thread.resize() // re-fit to the container
64
- thread.destroy() // cancel rAF, remove listeners, free canvases
61
+ thread.setText('Threads') // re-fit to width and redraw instantly (no sew-in)
62
+ thread.update({ threadColor: '#e6c200', fill: 0.8 }) // live changes instant, no re-sew
63
+ thread.replay() // re-run the sew-in animation
64
+ thread.resize() // re-fit to the container
65
+ thread.focus() // focus the surface for typing (editable mode)
66
+ thread.destroy() // cancel rAF, remove listeners, free canvases
65
67
  ```
66
68
 
67
69
  React:
@@ -72,13 +74,19 @@ import { ThreadText } from '@liiift-studio/threadtext'
72
74
  <ThreadText text="Thread" font='"Your Font", serif' weight={680} />
73
75
  ```
74
76
 
75
- Colours (`threadColor`, `fabricColor`), thread `pitch`, `sewRate`, `sheen`, `animate`, and
76
- `reducedMotion` are all options see `ThreadTextOptions`. `react`/`react-dom` are optional
77
- peers; the core is framework-free.
77
+ Thread colour, `fill` (size the word re-fits to the container width on load/resize),
78
+ `weight`, `font`, `sewRate`, `sheen`, `animate`, and `editable` (type straight on the
79
+ artwork backed by a real input, so touch keyboards and IME work) with an `onTextChange`
80
+ callback are all options — see `ThreadTextOptions`. Change any of them live with
81
+ `instance.update(...)` — it redraws instantly, never re-running the sew-in. `react`/`react-dom`
82
+ are optional peers; the core is framework-free.
78
83
 
79
84
  ## What's next
80
85
 
81
- The renderer is proven. Finishing it means productionising per [`../GUIDE.md`](../GUIDE.md): extract a framework-agnostic **core** with a typed API, add the **React hook + component**, **Framer** + **Webflow** bindings, tests, a landing **site**, then ship (npm + Vercel). A proposed core API and step-by-step plan are in `HANDOFF.md`.
86
+ Now shipped. Remaining ideas (see `HANDOFF.md`): region-limit or move the per-keystroke geometry
87
+ rebuild to a Web Worker/WASM (it currently re-runs the full distance-transform pipeline on every
88
+ edit); real variable-font axis control (`opsz`/`wght`) via path-based glyph rendering, since canvas
89
+ `fillText` can't set `font-variation-settings`; and additional stitch modes (chain, cross, running).
82
90
 
83
91
  ---
84
92
 
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const Q=require("react"),le=require("react/jsx-runtime"),Dt={bg:"tt-bg",fx:"tt-fx"};function Z(x,r){let a=Math.imul(x|0,374761393)+Math.imul(r|0,668265263)|0;return a=Math.imul(a^a>>>13,1274126177),((a^a>>>16)>>>0)/4294967295}function Nt(x,r){const a=Math.floor(x),c=Math.floor(r),l=x-a,s=r-c,h=l*l*(3-2*l),T=s*s*(3-2*s),S=Z(a,c),I=Z(a+1,c),f=Z(a,c+1),u=Z(a+1,c+1);return S+(I-S)*h+(f-S)*T+(S-I-f+u)*h*T}const wt=1e20;function Qt(x,r,a,c,l){let s=0;c[0]=0,l[0]=-wt,l[1]=wt;for(let h=1;h<r;h++){let T=(x[h]+h*h-(x[c[s]]+c[s]*c[s]))/(2*h-2*c[s]);for(;T<=l[s];)s--,T=(x[h]+h*h-(x[c[s]]+c[s]*c[s]))/(2*h-2*c[s]);s++,c[s]=h,l[s]=T,l[s+1]=wt}s=0;for(let h=0;h<r;h++){for(;l[s+1]<h;)s++;a[h]=(h-c[s])*(h-c[s])+x[c[s]]}}function fe(x,r,a){const c=new Float64Array(r*a);for(let f=0;f<r*a;f++)c[f]=x[f]?wt:0;const l=Math.max(r,a),s=new Float64Array(l),h=new Float64Array(l),T=new Int32Array(l),S=new Float64Array(l+1);for(let f=0;f<r;f++){for(let u=0;u<a;u++)s[u]=c[u*r+f];Qt(s,a,h,T,S);for(let u=0;u<a;u++)c[u*r+f]=h[u]}for(let f=0;f<a;f++){for(let u=0;u<r;u++)s[u]=c[f*r+u];Qt(s,r,h,T,S);for(let u=0;u<r;u++)c[f*r+u]=h[u]}const I=new Float32Array(r*a);for(let f=0;f<r*a;f++)I[f]=Math.sqrt(c[f]);return I}function Zt(x,r,a,c,l){let s=Float32Array.from(x);const h=new Float32Array(r*a),T=1/(2*c+1),S=(I,f)=>I<0?0:I>f?f:I;for(let I=0;I<l;I++){for(let f=0;f<a;f++){const u=f*r;let R=0;for(let P=-c;P<=c;P++)R+=s[u+S(P,r-1)];for(let P=0;P<r;P++)h[u+P]=R*T,R+=s[u+S(P+c+1,r-1)]-s[u+S(P-c,r-1)]}for(let f=0;f<r;f++){let u=0;for(let R=-c;R<=c;R++)u+=h[S(R,a-1)*r+f];for(let R=0;R<a;R++)s[R*r+f]=u*T,u+=h[S(R+c+1,a-1)*r+f]-h[S(R-c,a-1)*r+f]}}return s}function Ht(x,r){if(!x)return r;const a=x.trim();if(a[0]==="#"){let l=a.slice(1);if(l.length===3&&(l=l[0]+l[0]+l[1]+l[1]+l[2]+l[2]),l.length>=6){const s=parseInt(l.slice(0,2),16),h=parseInt(l.slice(2,4),16),T=parseInt(l.slice(4,6),16);if(!Number.isNaN(s)&&!Number.isNaN(h)&&!Number.isNaN(T))return[s,h,T]}return r}const c=a.match(/rgba?\(([^)]+)\)/i);if(c){const l=c[1].split(",").map(s=>parseFloat(s));if(l.length>=3&&l.slice(0,3).every(s=>!Number.isNaN(s)))return[Math.round(l[0]),Math.round(l[1]),Math.round(l[2])]}return r}function D(x){return x<0?0:x>255?255:Math.round(x)}function Wt(x,r){if(typeof window>"u"||typeof document>"u"||!x)return{setText(){},replay(){},resize(){},destroy(){},get text(){return r.text??""}};let a=r.text??"";const c=r.font??"Georgia, serif",l=r.weight??680,s=r.aspect??.46,h=r.sewRate??110,T=r.sheen??!0,S=r.animate??!0,I=(c.split(",")[0]||"serif").trim().replace(/^["']|["']$/g,""),f=r.reducedMotion??!!(window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches),u=Ht(r.threadColor??"#fffbf3",[255,251,243]),R=[u[0]*.585,u[1]*.585,u[2]*.585],P=[u[0]*.47,u[1]*.47,u[2]*.47],H=Ht(r.fabricColor??"#08080a",[8,8,10]),pt=[D(H[0]+14),D(H[1]+13),D(H[2]+15)],j=t=>`rgb(${D(t[0])},${D(t[1])},${D(t[2])})`,W=[];let L,O;x instanceof HTMLCanvasElement?(L=x,O=x.parentElement??x):(O=x,L=document.createElement("canvas"),W.push(L),O.appendChild(L)),L.classList.add(Dt.bg);const N=document.createElement("canvas");W.push(N),N.classList.add(Dt.fx),O.appendChild(N),getComputedStyle(O).position==="static"&&(O.style.position="relative"),L.style.display="block",L.style.width="100%",Object.assign(N.style,{position:"absolute",inset:"0",width:"100%",pointerEvents:"none",mixBlendMode:"screen"});let e=0,i=0,G=0,Mt=0,et=4,nt=[],ot=0,rt=0,Ct=new Uint8Array(0),St=new Float32Array(0),ft=new Float32Array(0),dt=new Float32Array(0),k=null,U=null,V=null,at=null,ut=null,X=[],ht=3;const A={rows:[],idx:0,acc:0,rate:0,on:!1},K={x:0,y:0,set:!1};let Y=!0,Tt=0,Ot=!1,mt=0,J=!1,At=!1;function qt(){const t=O.getBoundingClientRect(),n=Math.round(t.width||Math.min(window.innerWidth*.95,1240)),o=Math.round(n*s);let d=Math.min(window.devicePixelRatio||1,1.7);for(;n*d*o*d>9e5&&d>.55;)d-=.1;e=Math.max(1,Math.round(n*d)),i=Math.max(1,Math.round(o*d));for(const m of[L,N])m.width=e,m.height=i,m.style.height=o+"px";O.style.height=o+"px"}function zt(){const t=Math.max(7,et*2.5),n=Math.max(3.2,et*1.3);ot=Math.ceil(t)+2,rt=Math.ceil(n)+2,nt=[];for(let o=0;o<20;o++){const d=.34+o/19*.82,m=document.createElement("canvas");m.width=ot,m.height=rt;const M=m.getContext("2d");if(!M){nt.push(m);continue}const C=ot/2,F=rt/2,v=M.createLinearGradient(0,F-n/2,0,F+n/2),p=($,q)=>`rgba(${D($[0]*d)},${D($[1]*d)},${D($[2]*d)},${q})`;v.addColorStop(0,p(P,0)),v.addColorStop(.16,p(R,1)),v.addColorStop(.5,p(u,1)),v.addColorStop(.84,p(R,1)),v.addColorStop(1,p(P,0)),M.fillStyle=v,M.beginPath();const w=n/2;M.moveTo(C-t/2+w,F-w),M.lineTo(C+t/2-w,F-w),M.arc(C+t/2-w,F,w,-Math.PI/2,Math.PI/2),M.lineTo(C-t/2+w,F+w),M.arc(C-t/2+w,F,w,Math.PI/2,-Math.PI/2),M.closePath(),M.fill(),M.globalCompositeOperation="destination-in";const E=M.createLinearGradient(C-t/2,0,C+t/2,0);E.addColorStop(0,"rgba(0,0,0,0)"),E.addColorStop(.22,"rgba(0,0,0,1)"),E.addColorStop(.78,"rgba(0,0,0,1)"),E.addColorStop(1,"rgba(0,0,0,0)"),M.fillStyle=E,M.fillRect(0,0,ot,rt),nt.push(m)}}function bt(){if(!ut)return;const t=ut;t.clearRect(0,0,e,i),t.fillStyle="#fff",t.textAlign="left",t.textBaseline="middle",t.font=`${l} ${G}px ${c}`,t.fillText(a,Mt,i*.5);const n=t.getImageData(0,0,e,i).data,o=new Uint8Array(e*i);for(let g=0;g<i;g++)for(let b=0;b<e;b++){const y=g*e+b,z=n[y*4+3]/255,_=Nt(b*.5,g*.5)*.22+Nt(b*1.8,g*1.8)*.1;o[y]=z>.5+(_-.16)?1:0}Ct=o;const d=fe(o,e,i),m=new Float32Array(e*i);for(let g=1;g<i-1;g++)for(let b=1;b<e-1;b++){const y=g*e+b;m[y]=(d[y]*4+d[y-1]+d[y+1]+d[y-e]+d[y+e])/8}const M=new Float32Array(e*i),C=new Float32Array(e*i),F=new Float32Array(e*i),v=new Float32Array(e*i);for(let g=1;g<i-1;g++)for(let b=1;b<e-1;b++){const y=g*e+b;if(!o[y])continue;const z=(m[y+1]-m[y-1])*.5,_=(m[y+e]-m[y-e])*.5;F[y]=z,v[y]=_,M[y]=z*z-_*_,C[y]=2*z*_}const p=Math.max(7,Math.round(i*.05)),w=Zt(M,e,i,p,2),E=Zt(C,e,i,p,2);ft=new Float32Array(e*i),dt=new Float32Array(e*i);for(let g=0;g<e*i;g++){if(!o[g])continue;const b=.5*Math.atan2(E[g],w[g]);ft[g]=Math.cos(b),dt[g]=Math.sin(b)}const $=Math.max(9,i*.04),q=new Float32Array(e*i),st=-.45,lt=-.62,xt=.64;for(let g=1;g<i-1;g++)for(let b=1;b<e-1;b++){const y=g*e+b;if(!o[y])continue;let z=F[y],_=v[y];const Vt=Math.hypot(z,_)||1;z/=Vt,_/=Vt;const Kt=m[y],Yt=(1-Math.min(Kt/$,1))*1.25;let vt=-z*Yt,Lt=-_*Yt;const Jt=1.5,$t=Math.hypot(vt,Lt,Jt);vt/=$t,Lt/=$t;let Pt=vt*st+Lt*lt+Jt/$t*xt;Pt<0&&(Pt=0);const se=Math.min(1,.45+.55*Math.min(Kt/($*.7),1));q[y]=Math.min(1.12,(.5+.75*Pt)*se)}St=q,k=document.createElement("canvas"),k.width=e,k.height=i;const B=k.getContext("2d");U=document.createElement("canvas"),U.width=e,U.height=i;const tt=U.getContext("2d");if(B&&tt){const g=B.createImageData(e,i),b=tt.createImageData(e,i);for(let y=0;y<e*i;y++)o[y]&&(g.data[y*4]=255,g.data[y*4+1]=255,g.data[y*4+2]=255,g.data[y*4+3]=255,b.data[y*4+3]=255);B.putImageData(g,0,0),tt.putImageData(b,0,0)}}function _t(){V=document.createElement("canvas"),V.width=e,V.height=i;const t=V.getContext("2d",{willReadFrequently:!0});if(t){const n=t.createRadialGradient(e*.5,i*.4,0,e*.5,i*.5,e*.72);n.addColorStop(0,j(pt)),n.addColorStop(1,j(H)),t.fillStyle=n,t.fillRect(0,0,e,i);const o=t.getImageData(0,0,e,i),d=o.data;for(let m=0;m<i;m++)for(let M=0;M<e;M++){const C=(m*e+M)*4,F=Math.sin(M*1.9)*.5+.5,v=Math.sin(m*1.9)*.5+.5,p=F*v*9-4.5+(Nt(M*.18,m*.18)-.5)*15;d[C]=D(d[C]+p),d[C+1]=D(d[C+1]+p),d[C+2]=D(d[C+2]+p*1.1)}t.putImageData(o,0,0)}at=document.createElement("canvas"),at.width=e,at.height=i,ut=at.getContext("2d",{willReadFrequently:!0})}function Ft(){const t=Math.max(1.6,et*.6);ht=t;const n=[];for(let o=0;o<i;o+=t)for(let d=0;d<e;d+=t){const m=(Z(Math.floor(d),Math.floor(o))-.5)*t*.9,M=(Z(Math.floor(o),Math.floor(d))-.5)*t*.9,C=Math.round(d+m),F=Math.round(o+M);if(C<1||F<1||C>=e-1||F>=i-1)continue;const v=F*e+C;if(!Ct[v])continue;const p=Math.atan2(dt[v],ft[v]),w=.86+Z(C*7,F*13)*.18;let E=St[v]*w;E=E<0?0:E>1.12?1.12:E,n.push({x:C,y:F,ang:p,idx:Math.min(19,Math.max(0,Math.round(E/1.12*19)))})}n.sort((o,d)=>o.x-d.x),X=n}function gt(t,n){t.save(),t.translate(n.x,n.y),t.rotate(n.ang),t.drawImage(nt[n.idx],-ot/2,-rt/2),t.restore()}function yt(){const t=L.getContext("2d");if(t)for(const n of X)gt(t,n)}function jt(){return X.length?X[X.length-1].x:0}function Et(){return X.length?X[0].x:0}function it(){const t=L.getContext("2d");!t||!V||(t.drawImage(V,0,0),U&&(t.save(),t.globalAlpha=.85,t.filter=`blur(${Math.round(i*.018)}px)`,t.drawImage(U,0,Math.round(i*.02)),t.restore()))}function ne(t){if(!t.length)return[];const n=Math.max(4,ht*1.5),o=new Map,d=(p,w)=>p+","+w;t.forEach((p,w)=>{const E=d(Math.floor(p.x/n),Math.floor(p.y/n));let $=o.get(E);$||o.set(E,$=[]),$.push(w)});const m=ht*1.8*(ht*1.8);function M(p){const w=t[p],E=Math.floor(w.x/n),$=Math.floor(w.y/n),q=[];for(let st=-1;st<=1;st++)for(let lt=-1;lt<=1;lt++){const xt=o.get(d(E+st,$+lt));if(xt)for(const B of xt){if(B===p)continue;const tt=t[B].x-w.x,g=t[B].y-w.y;tt*tt+g*g<=m&&q.push(B)}}return q}const C=new Uint8Array(t.length),F=[],v=t.map((p,w)=>w).sort((p,w)=>t[p].x+t[p].y-(t[w].x+t[w].y));for(const p of v){if(C[p])continue;let w=[p];for(C[p]=1;w.length;){F.push(w.map($=>t[$]));const E=[];for(const $ of w)for(const q of M($))C[q]||(C[q]=1,E.push(q));w=E}}return F}function It(t,n){it();const o=L.getContext("2d");if(!o)return;const d=[];for(const m of X)m.x<=t?gt(o,m):d.push(m);if(f){for(const m of d)gt(o,m);A.on=!1;return}A.rows=ne(d),A.idx=0,A.acc=0,A.rate=n||h,A.on=A.rows.length>0}function oe(t){const n=L.getContext("2d");if(n)for(t>A.rows.length&&(t=A.rows.length);A.idx<t;){const o=A.rows[A.idx++];for(const d of o)gt(n,d)}}function re(t,n){const o=N.getContext("2d");if(!o||(o.clearRect(0,0,e,i),!T||!k))return;const d=e*.32,m=o.createRadialGradient(t,n,0,t,n,d);m.addColorStop(0,"rgba(255,250,238,0.18)"),m.addColorStop(.5,"rgba(255,250,238,0.055)"),m.addColorStop(1,"rgba(255,250,238,0)"),o.fillStyle=m,o.fillRect(0,0,e,i),o.globalCompositeOperation="destination-in",o.drawImage(k,0,0),o.globalCompositeOperation="source-over"}function Xt(){const t=L.getContext("2d");if(!t){G=Math.round(i*.6),Mt=Math.round(e*.14);return}const n=a||"Ag";t.font=`${l} 100px ${c}`;const o=t.measureText(n).width||100;G=Math.round(100*(e*.72)/o),t.font=`${l} ${G}px ${c}`,Mt=Math.round(e/2-t.measureText(n).width/2)}function kt(t){if(J)return;mt||(mt=t);const n=Math.min(.25,(t-mt)/1e3);mt=t,A.on&&(A.acc+=A.rate*n,oe(A.acc),A.idx>=A.rows.length&&(A.on=!1)),Y&&k&&(re(K.set?K.x:e*.5,K.set?K.y:i*.34),Y=!1),Tt=requestAnimationFrame(kt)}function Bt(t){const n=N.getBoundingClientRect();K.x=(t.clientX-n.left)/(n.width||1)*e,K.y=(t.clientY-n.top)/(n.height||1)*i,K.set=!0,Y=!0}T&&window.addEventListener("pointermove",Bt,{passive:!0});function ae(t){bt(),Ft(),S&&!f?It(Et()-1,h):(it(),yt(),A.on=!1),Y=!0}function ie(){J||(qt(),_t(),et=Math.max(3.4,i*.0095),zt(),Xt(),ae(),At=!0,Ot||(Ot=!0,Tt=requestAnimationFrame(kt)))}let ct;function ce(t){const n=`${l} 200px ${I}`;let o=!1;try{o=!!document.fonts&&document.fonts.check(n)}catch{o=!0}if(!document.fonts||o){t();return}let d=!1;const m=()=>{d||(d=!0,ct&&clearTimeout(ct),t())};document.fonts.load(n,a).then(()=>document.fonts.ready).then(m,m),ct=setTimeout(m,1500)}ce(ie);let Rt;function Gt(){clearTimeout(Rt),Rt=setTimeout(()=>Ut.resize(),200)}window.addEventListener("resize",Gt);const Ut={get text(){return a},setText(t){if(J||!At){a=t??"";return}t=t??"";const n=t.length>a.length&&a.length>0&&t.startsWith(a),o=n?jt():-1;a=t,bt(),Ft(),f||!S?(it(),yt(),A.on=!1):It(n&&o>0?Math.min(o,jt()):Et()-1,n?320:h),Y=!0},replay(){J||!At||(f||!S?(it(),yt(),A.on=!1):It(Et()-1,h),Y=!0)},resize(){J||(qt(),_t(),et=Math.max(3.4,i*.0095),zt(),Xt(),bt(),Ft(),it(),yt(),A.on=!1,Y=!0)},destroy(){if(!J){J=!0,cancelAnimationFrame(Tt),clearTimeout(Rt),ct&&clearTimeout(ct),window.removeEventListener("resize",Gt),T&&window.removeEventListener("pointermove",Bt);for(const t of W)t.remove();nt=[],X=[],A.rows=[],Ct=new Uint8Array(0),St=new Float32Array(0),ft=new Float32Array(0),dt=new Float32Array(0),k=U=V=at=null,ut=null}}};return Ut}function te(x){const r=Q.useRef(null),a=Q.useRef(null),c=Q.useRef(x);c.current=x;const{text:l,font:s,weight:h,threadColor:T,fabricColor:S,pitch:I,animate:f,sewRate:u,sheen:R,reducedMotion:P,aspect:H}=x,pt=JSON.stringify({font:s,weight:h,threadColor:T,fabricColor:S,pitch:I,animate:f,sewRate:u,sheen:R,reducedMotion:P,aspect:H});return Q.useLayoutEffect(()=>{const j=r.current;if(!j)return;const W=Wt(j,c.current);a.current=W;let L=0,O=0,N;return typeof ResizeObserver<"u"&&(N=new ResizeObserver(e=>{const i=Math.round(e[0].contentRect.width);i!==O&&(O=i,cancelAnimationFrame(L),L=requestAnimationFrame(()=>{var G;return(G=a.current)==null?void 0:G.resize()}))}),N.observe(j)),()=>{N==null||N.disconnect(),cancelAnimationFrame(L),W.destroy(),a.current=null}},[pt]),Q.useEffect(()=>{var j;(j=a.current)==null||j.setText(l)},[l]),r}const ee=Q.forwardRef(function({className:r,style:a,as:c="div","aria-label":l,role:s,"aria-describedby":h,...T},S){const I=te(T),f=Q.useCallback(u=>{I.current=u,typeof S=="function"?S(u):S&&(S.current=u)},[I,S]);return le.jsx(c,{ref:f,className:r,style:a,role:s??"img","aria-label":l??T.text,"aria-describedby":h})});ee.displayName="ThreadText";exports.THREAD_TEXT_CLASSES=Dt;exports.ThreadText=ee;exports.createThreadText=Wt;exports.useThreadText=te;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const V=require("react"),At=require("react/jsx-runtime"),Te={bg:"tt-bg",fx:"tt-fx",caret:"tt-caret"};function W(h,t){let n=Math.imul(h|0,374761393)+Math.imul(t|0,668265263)|0;return n=Math.imul(n^n>>>13,1274126177),((n^n>>>16)>>>0)/4294967295}function tt(h,t){const n=Math.floor(h),r=Math.floor(t),i=h-n,a=t-r,u=i*i*(3-2*i),b=a*a*(3-2*a),S=W(n,r),E=W(n+1,r),c=W(n,r+1),m=W(n+1,r+1);return S+(E-S)*u+(c-S)*b+(S-E-c+m)*u*b}const Ee=1e20;function nt(h,t,n,r,i){let a=0;r[0]=0,i[0]=-Ee,i[1]=Ee;for(let u=1;u<t;u++){let b=(h[u]+u*u-(h[r[a]]+r[a]*r[a]))/(2*u-2*r[a]);for(;b<=i[a];)a--,b=(h[u]+u*u-(h[r[a]]+r[a]*r[a]))/(2*u-2*r[a]);a++,r[a]=u,i[a]=b,i[a+1]=Ee}a=0;for(let u=0;u<t;u++){for(;i[a+1]<u;)a++;n[u]=(u-r[a])*(u-r[a])+h[r[a]]}}function St(h,t,n){const r=new Float64Array(t*n);for(let c=0;c<t*n;c++)r[c]=h[c]?Ee:0;const i=Math.max(t,n),a=new Float64Array(i),u=new Float64Array(i),b=new Int32Array(i),S=new Float64Array(i+1);for(let c=0;c<t;c++){for(let m=0;m<n;m++)a[m]=r[m*t+c];nt(a,n,u,b,S);for(let m=0;m<n;m++)r[m*t+c]=u[m]}for(let c=0;c<n;c++){for(let m=0;m<t;m++)a[m]=r[c*t+m];nt(a,t,u,b,S);for(let m=0;m<t;m++)r[c*t+m]=u[m]}const E=new Float32Array(t*n);for(let c=0;c<t*n;c++)E[c]=Math.sqrt(r[c]);return E}function ot(h,t,n,r,i){let a=Float32Array.from(h);const u=new Float32Array(t*n),b=1/(2*r+1),S=(E,c)=>E<0?0:E>c?c:E;for(let E=0;E<i;E++){for(let c=0;c<n;c++){const m=c*t;let I=0;for(let N=-r;N<=r;N++)I+=a[m+S(N,t-1)];for(let N=0;N<t;N++)u[m+N]=I*b,I+=a[m+S(N+r+1,t-1)]-a[m+S(N-r,t-1)]}for(let c=0;c<t;c++){let m=0;for(let I=-r;I<=r;I++)m+=u[S(I,n-1)*t+c];for(let I=0;I<n;I++)a[I*t+c]=m*b,m+=u[S(I+r+1,n-1)*t+c]-u[S(I-r,n-1)*t+c]}}return a}function rt(h,t){if(!h)return t;const n=h.trim();if(n[0]==="#"){let i=n.slice(1);if(i.length===3&&(i=i[0]+i[0]+i[1]+i[1]+i[2]+i[2]),i.length>=6){const a=parseInt(i.slice(0,2),16),u=parseInt(i.slice(2,4),16),b=parseInt(i.slice(4,6),16);if(!Number.isNaN(a)&&!Number.isNaN(u)&&!Number.isNaN(b))return[a,u,b]}return t}const r=n.match(/rgba?\(([^)]+)\)/i);if(r){const i=r[1].split(",").map(a=>parseFloat(a));if(i.length>=3&&i.slice(0,3).every(a=>!Number.isNaN(a)))return[Math.round(i[0]),Math.round(i[1]),Math.round(i[2])]}return t}function K(h){return h<0?0:h>255?255:Math.round(h)}function be(h,t,n){return h<t?t:h>n?n:h}function it(h){return(h.split(",")[0]||"serif").trim().replace(/^["']|["']$/g,"")}function at(h,t){if(typeof window>"u"||typeof document>"u"||!h)return{setText(){},replay(){},resize(){},update(){},focus(){},destroy(){},get text(){return t.text??""}};let n=t.text??"",r=t.font??"Georgia, serif",i=be(t.weight??680,1,1e3),a=be(t.fill??.9,.05,1),u=Math.max(1,t.sewRate??110),b=t.sheen??!0,S=t.animate??!0,E=t.editable??!1,c=t.pitch,m=t.onTextChange,I=it(r);const N=t.reducedMotion??!!(window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches);let B=rt(t.threadColor??"#fffbf3",[255,251,243]),ae=[0,0,0],D=[0,0,0];function le(e){B=rt(e,[255,251,243]),ae=[B[0]*.585,B[1]*.585,B[2]*.585],D=[B[0]*.47,B[1]*.47,B[2]*.47]}le(t.threadColor??"#fffbf3");const G=[];let O,L;h instanceof HTMLCanvasElement?(O=h,L=h.parentElement??h):(L=h,O=document.createElement("canvas"),G.push(O),L.appendChild(O)),O.classList.add(Te.bg);const j=document.createElement("canvas");G.push(j),j.classList.add(Te.fx),L.appendChild(j),getComputedStyle(L).position==="static"&&(L.style.position="relative"),O.style.display="block",O.style.width="100%",Object.assign(j.style,{position:"absolute",inset:"0",width:"100%",pointerEvents:"none",mixBlendMode:"screen"});let y=null,A=null,Q=!1,s=0,x=0,ee=0,Ae=0,te=4,ne=[],se=0,ce=0,je=-1,qe="",ze=-1,_e=-1,Se=new Uint8Array(0),Fe=new Float32Array(0),me=new Float32Array(0),xe=new Float32Array(0),Y=null,oe=null,ye=null,ge=[],pe=3;const $={rows:[],idx:0,acc:0,rate:0,on:!1},U={x:0,y:0,set:!1};let Z=!0,Re=0,fe=!1,ue=0,q=!1,H=!1;function ct(){const e=L.getBoundingClientRect(),o=Math.max(1,Math.round(e.width||Math.min(window.innerWidth*.95,1240))),l=n||"Ag",f=O.getContext("2d");f&&(f.textBaseline="middle");let T=100;f&&(f.font=`${i} 100px ${r}`,T=f.measureText(l).width||100);const C=Math.max(8,100*(o*a)/T);let v=C*.62,F=C*.62,P=o*a;if(f){f.font=`${i} ${C}px ${r}`;const w=f.measureText(l);P=w.width||P,w.actualBoundingBoxAscent>0&&(v=w.actualBoundingBoxAscent),w.actualBoundingBoxDescent>0&&(F=w.actualBoundingBoxDescent)}const p=Math.max(1,Math.round(2*Math.max(v,F)*1.1));let d=Math.min(window.devicePixelRatio||1,1.7);for(;o*d*p*d>9e5&&d>.4;)d-=.1;s=Math.max(1,Math.round(o*d)),x=Math.max(1,Math.round(p*d)),ee=Math.max(4,Math.round(C*d)),Ae=Math.round(s/2-P*d/2);for(const w of[O,j])w.width=s,w.height=x,w.style.height=p+"px";L.style.height=p+"px",te=c??Math.max(3,x*.0095)}function ft(){oe&&ze===s&&_e===x||(oe=document.createElement("canvas"),oe.width=s,oe.height=x,ye=oe.getContext("2d",{willReadFrequently:!0}),ze=s,_e=x)}function Xe(){const e=Math.max(7,te*2.5),o=Math.max(3.2,te*1.3);se=Math.ceil(e)+2,ce=Math.ceil(o)+2,ne=[];for(let l=0;l<20;l++){const f=.34+l/19*.82,T=document.createElement("canvas");T.width=se,T.height=ce;const C=T.getContext("2d");if(!C){ne.push(T);continue}const v=se/2,F=ce/2,P=C.createLinearGradient(0,F-o/2,0,F+o/2),p=(k,z)=>`rgba(${K(k[0]*f)},${K(k[1]*f)},${K(k[2]*f)},${z})`;P.addColorStop(0,p(D,0)),P.addColorStop(.16,p(ae,1)),P.addColorStop(.5,p(B,1)),P.addColorStop(.84,p(ae,1)),P.addColorStop(1,p(D,0)),C.fillStyle=P,C.beginPath();const d=o/2;C.moveTo(v-e/2+d,F-d),C.lineTo(v+e/2-d,F-d),C.arc(v+e/2-d,F,d,-Math.PI/2,Math.PI/2),C.lineTo(v-e/2+d,F+d),C.arc(v-e/2+d,F,d,Math.PI/2,-Math.PI/2),C.closePath(),C.fill(),C.globalCompositeOperation="destination-in";const w=C.createLinearGradient(v-e/2,0,v+e/2,0);w.addColorStop(0,"rgba(0,0,0,0)"),w.addColorStop(.22,"rgba(0,0,0,1)"),w.addColorStop(.78,"rgba(0,0,0,1)"),w.addColorStop(1,"rgba(0,0,0,0)"),C.fillStyle=w,C.fillRect(0,0,se,ce),ne.push(T)}je=te,qe=B.join(",")}function ut(){ne.length&&je===te&&qe===B.join(",")||Xe()}function dt(){if(!ye)return;const e=ye;e.clearRect(0,0,s,x),e.fillStyle="#fff",e.textAlign="left",e.textBaseline="middle",e.font=`${i} ${ee}px ${r}`,e.fillText(n,Ae,x*.5);const o=e.getImageData(0,0,s,x).data,l=new Uint8Array(s*x);for(let g=0;g<x;g++)for(let M=0;M<s;M++){const R=g*s+M,_=o[R*4+3]/255,X=tt(M*.5,g*.5)*.22+tt(M*1.8,g*1.8)*.1;l[R]=_>.5+(X-.16)?1:0}Se=l;const f=St(l,s,x),T=new Float32Array(s*x);for(let g=1;g<x-1;g++)for(let M=1;M<s-1;M++){const R=g*s+M;T[R]=(f[R]*4+f[R-1]+f[R+1]+f[R-s]+f[R+s])/8}const C=new Float32Array(s*x),v=new Float32Array(s*x),F=new Float32Array(s*x),P=new Float32Array(s*x);for(let g=1;g<x-1;g++)for(let M=1;M<s-1;M++){const R=g*s+M;if(!l[R])continue;const _=(T[R+1]-T[R-1])*.5,X=(T[R+s]-T[R-s])*.5;F[R]=_,P[R]=X,C[R]=_*_-X*X,v[R]=2*_*X}const p=Math.max(7,Math.round(x*.05)),d=ot(C,s,x,p,2),w=ot(v,s,x,p,2);me=new Float32Array(s*x),xe=new Float32Array(s*x);for(let g=0;g<s*x;g++){if(!l[g])continue;const M=.5*Math.atan2(w[g],d[g]);me[g]=Math.cos(M),xe[g]=Math.sin(M)}const k=Math.max(9,x*.04),z=new Float32Array(s*x),de=-.45,he=-.62,ve=.64;for(let g=1;g<x-1;g++)for(let M=1;M<s-1;M++){const R=g*s+M;if(!l[R])continue;let _=F[R],X=P[R];const Qe=Math.hypot(_,X)||1;_/=Qe,X/=Qe;const Ze=T[R],We=(1-Math.min(Ze/k,1))*1.25;let Oe=-_*We,ke=-X*We;const et=1.5,Ne=Math.hypot(Oe,ke,et);Oe/=Ne,ke/=Ne;let De=Oe*de+ke*he+et/Ne*ve;De<0&&(De=0);const Et=Math.min(1,.45+.55*Math.min(Ze/(k*.7),1));z[R]=Math.min(1.12,(.5+.75*De)*Et)}Fe=z,Y=document.createElement("canvas"),Y.width=s,Y.height=x;const J=Y.getContext("2d");if(J){const g=J.createImageData(s,x);for(let M=0;M<s*x;M++)l[M]&&(g.data[M*4]=255,g.data[M*4+1]=255,g.data[M*4+2]=255,g.data[M*4+3]=255);J.putImageData(g,0,0)}}function ht(){const e=Math.max(1.6,te*.6);pe=e;const o=[];for(let l=0;l<x;l+=e)for(let f=0;f<s;f+=e){const T=(W(Math.floor(f),Math.floor(l))-.5)*e*.9,C=(W(Math.floor(l),Math.floor(f))-.5)*e*.9,v=Math.round(f+T),F=Math.round(l+C);if(v<1||F<1||v>=s-1||F>=x-1)continue;const P=F*s+v;if(!Se[P])continue;const p=Math.atan2(xe[P],me[P]),d=.86+W(v*7,F*13)*.18;let w=Fe[P]*d;w=w<0?0:w>1.12?1.12:w,o.push({x:v,y:F,ang:p,idx:Math.min(19,Math.max(0,Math.round(w/1.12*19)))})}ge=o}function Ge(e,o){e.save(),e.translate(o.x,o.y),e.rotate(o.ang),e.drawImage(ne[o.idx],-se/2,-ce/2),e.restore()}function Le(){const e=O.getContext("2d");if(e)for(const o of ge)Ge(e,o)}function $e(){const e=O.getContext("2d");e&&e.clearRect(0,0,s,x)}function mt(e){if(!e.length)return[];const o=Math.max(4,pe*1.5),l=new Map,f=(p,d)=>p+","+d;e.forEach((p,d)=>{const w=f(Math.floor(p.x/o),Math.floor(p.y/o));let k=l.get(w);k||l.set(w,k=[]),k.push(d)});const T=pe*1.8*(pe*1.8);function C(p){const d=e[p],w=Math.floor(d.x/o),k=Math.floor(d.y/o),z=[];for(let de=-1;de<=1;de++)for(let he=-1;he<=1;he++){const ve=l.get(f(w+de,k+he));if(ve)for(const J of ve){if(J===p)continue;const g=e[J].x-d.x,M=e[J].y-d.y;g*g+M*M<=T&&z.push(J)}}return z}const v=new Uint8Array(e.length),F=[],P=e.map((p,d)=>d).sort((p,d)=>e[p].x+e[p].y-(e[d].x+e[d].y));for(const p of P){if(v[p])continue;let d=[p];for(v[p]=1;d.length;){F.push(d.map(k=>e[k]));const w=[];for(const k of d)for(const z of C(k))v[z]||(v[z]=1,w.push(z));d=w}}return F}function xt(){if($e(),N||!S){Le(),$.on=!1;return}$.rows=mt(ge),$.idx=0,$.acc=0,$.rate=u,$.on=$.rows.length>0}function yt(e){const o=O.getContext("2d");if(o)for(e>$.rows.length&&(e=$.rows.length);$.idx<e;){const l=$.rows[$.idx++];for(const f of l)Ge(o,f)}}function gt(e,o){const l=j.getContext("2d");if(!l||(l.clearRect(0,0,s,x),!b||!Y))return;const f=s*.32,T=l.createRadialGradient(e,o,0,e,o,f);T.addColorStop(0,"rgba(255,250,238,0.18)"),T.addColorStop(.5,"rgba(255,250,238,0.055)"),T.addColorStop(1,"rgba(255,250,238,0)"),l.fillStyle=T,l.fillRect(0,0,s,x),l.globalCompositeOperation="destination-in",l.drawImage(Y,0,0),l.globalCompositeOperation="source-over"}function pt(){A||!E||(A=document.createElement("span"),A.className=Te.caret,A.setAttribute("aria-hidden","true"),Object.assign(A.style,{position:"absolute",pointerEvents:"none",borderRadius:"1px",opacity:"0",background:`rgb(${K(B[0])},${K(B[1])},${K(B[2])})`}),L.appendChild(A),G.push(A))}function Ie(){if(!E||!A)return;const e=O.getContext("2d");if(!e)return;e.font=`${i} ${ee}px ${r}`;const o=e.measureText(n).width,l=(O.clientWidth||s)/s,f=ee*.7;A.style.left=(Ae+o+ee*.05)*l+"px",A.style.top=.5*(O.clientHeight||x)-f*l/2+"px",A.style.height=f*l+"px",A.style.width=Math.max(2,Math.round(ee*.02*l))+"px",A.style.background=`rgb(${K(B[0])},${K(B[1])},${K(B[2])})`}function re(){q||fe||(fe=!0,ue=0,Re=requestAnimationFrame(Ue))}function Ue(e){if(q){fe=!1;return}ue||(ue=e);const o=Math.min(.25,(e-ue)/1e3);ue=e,$.on&&($.acc+=$.rate*o,yt($.acc),$.idx>=$.rows.length&&($.on=!1)),Z&&Y&&(gt(U.set?U.x:s*.5,U.set?U.y:x*.45),Z=!1);const l=E&&Q;if(A&&(A.style.opacity=l&&Math.floor(e/530)%2===0?"1":"0"),!$.on&&!Z&&!l){fe=!1;return}Re=requestAnimationFrame(Ue)}function we(e){const o=L.getBoundingClientRect();U.x=(e.clientX-o.left)/(o.width||1)*s,U.y=(e.clientY-o.top)/(o.height||1)*x,U.set=!0,Z=!0,re()}function Me(){U.set=!1,Z=!0,re()}function wt(){y&&Ve(y.value,!0)}function Mt(e){e.key==="Enter"&&(e.preventDefault(),Be.replay())}function Ct(){Q=!0,re()}function vt(){Q=!1,A&&(A.style.opacity="0")}function Ke(e){e===E&&y||(E=e,e?(y||(y=document.createElement("input"),y.type="text",y.value=n,y.maxLength=64,y.setAttribute("aria-label","Embroidered text — type to change it"),y.setAttribute("autocomplete","off"),y.setAttribute("autocapitalize","off"),y.spellcheck=!1,Object.assign(y.style,{position:"absolute",inset:"0",width:"100%",height:"100%",margin:"0",padding:"0",border:"0",outline:"none",background:"transparent",color:"transparent",caretColor:"transparent",font:"inherit",textAlign:"center",cursor:"text"}),y.addEventListener("input",wt),y.addEventListener("keydown",Mt),y.addEventListener("focus",Ct),y.addEventListener("blur",vt),L.appendChild(y),G.push(y)),pt(),Ie()):(y&&(y.remove(),y=null),Q=!1,A&&(A.style.opacity="0")))}function bt(e){if(e!==b)if(b=e,e)L.addEventListener("pointermove",we,{passive:!0}),L.addEventListener("pointerleave",Me,{passive:!0}),U.set=!1,Z=!0,re();else{L.removeEventListener("pointermove",we),L.removeEventListener("pointerleave",Me);const o=j.getContext("2d");o&&o.clearRect(0,0,s,x)}}function ie(e){ct(),ft(),ut(),dt(),ht(),e?xt():($e(),Le(),$.on=!1),y&&y.value!==n&&(y.value=n),Ie(),Z=!0,re()}function Ve(e,o){e=e??"",!(e===n&&H)&&(n=e,H&&(ie(!1),o&&m&&m(n)))}const Ce=new Set;function Ye(e,o,l){const f=`${o} 200px ${e}`;let T=!1;try{T=!!document.fonts&&document.fonts.check(f)}catch{T=!0}if(!document.fonts||T){l();return}let C=!1,v;const F=()=>{C||(C=!0,clearTimeout(v),Ce.delete(v),q||l())};document.fonts.load(f,n).then(()=>document.fonts.ready).then(F,F),v=setTimeout(F,1500),Ce.add(v)}function Tt(){q||(ie(!0),H=!0)}E&&(E=!1,Ke(!0)),b&&(L.addEventListener("pointermove",we,{passive:!0}),L.addEventListener("pointerleave",Me,{passive:!0})),Ye(I,i,Tt);let Pe,He=-1;function Je(){clearTimeout(Pe),Pe=setTimeout(()=>{if(q||!H)return;const e=Math.round(L.getBoundingClientRect().width);e!==He&&(He=e,ie(!1))},200)}window.addEventListener("resize",Je);const Be={get text(){return n},setText(e){if(!q){if(!H){n=e??"",y&&(y.value=n);return}Ve(e,!1)}},replay(){q||!H||ie(!0)},resize(){q||!H||ie(!1)},update(e){if(q)return;e.text!==void 0&&Be.setText(e.text);let o=!1,l=!1;if(e.font!==void 0&&e.font!==r&&(r=e.font,I=it(r),o=!0),e.weight!==void 0){const f=be(e.weight,1,1e3);f!==i&&(i=f,o=!0)}if(e.fill!==void 0){const f=be(e.fill,.05,1);f!==a&&(a=f,o=!0)}e.pitch!==void 0&&e.pitch!==c&&(c=e.pitch,o=!0),e.threadColor!==void 0&&(le(e.threadColor),l=!0),e.sewRate!==void 0&&(u=Math.max(1,e.sewRate)),e.animate!==void 0&&(S=e.animate),e.onTextChange!==void 0&&(m=e.onTextChange),e.sheen!==void 0&&bt(e.sheen),e.editable!==void 0&&Ke(e.editable),H&&(o?Ye(I,i,()=>{q||ie(!1)}):l&&(Xe(),$e(),Le(),Ie(),re()))},focus(){!q&&E&&(y==null||y.focus())},destroy(){if(!q){q=!0,fe=!1,cancelAnimationFrame(Re),clearTimeout(Pe);for(const e of Ce)clearTimeout(e);Ce.clear(),window.removeEventListener("resize",Je),L.removeEventListener("pointermove",we),L.removeEventListener("pointerleave",Me);for(const e of G)e.remove();ne=[],ge=[],$.rows=[],Se=new Uint8Array(0),Fe=new Float32Array(0),me=new Float32Array(0),xe=new Float32Array(0),Y=oe=null,ye=null,A=null,y=null}}};return Be}function lt(h){const t=V.useRef(null),n=V.useRef(null),r=V.useRef(h);r.current=h;const i=V.useRef(h.onTextChange);i.current=h.onTextChange;const{text:a,font:u,weight:b,threadColor:S,pitch:E,fill:c,animate:m,sewRate:I,sheen:N,editable:B,reducedMotion:ae}=h;return V.useLayoutEffect(()=>{const D=t.current;if(!D)return;const le=at(D,{...r.current,onTextChange:y=>{var A;return(A=i.current)==null?void 0:A.call(i,y)}});n.current=le;let G=0,O=0,L=!0,j;return typeof ResizeObserver<"u"&&(j=new ResizeObserver(y=>{const A=Math.round(y[0].contentRect.width);if(L){L=!1,O=A;return}A!==O&&(O=A,cancelAnimationFrame(G),G=requestAnimationFrame(()=>{var Q;return(Q=n.current)==null?void 0:Q.resize()}))}),j.observe(D)),()=>{j==null||j.disconnect(),cancelAnimationFrame(G),le.destroy(),n.current=null}},[ae]),V.useEffect(()=>{var D;(D=n.current)==null||D.update({font:u,weight:b,threadColor:S,pitch:E,fill:c,animate:m,sewRate:I,sheen:N,editable:B})},[u,b,S,E,c,m,I,N,B]),V.useEffect(()=>{var D;(D=n.current)==null||D.setText(a)},[a]),t}const st=V.forwardRef(function({className:t,style:n,as:r="div","aria-label":i,role:a,"aria-describedby":u,...b},S){const E=lt(b),c=V.useCallback(m=>{E.current=m,typeof S=="function"?S(m):S&&(S.current=m)},[E,S]);return At.jsx(r,{ref:c,className:t,style:n,role:b.editable?a:a??"img","aria-label":b.editable?i:i??b.text,"aria-describedby":u})});st.displayName="ThreadText";exports.THREAD_TEXT_CLASSES=Te;exports.ThreadText=st;exports.createThreadText=at;exports.useThreadText=lt;
package/dist/index.d.ts CHANGED
@@ -12,10 +12,12 @@ export declare function createThreadText(target: HTMLElement, opts: ThreadTextOp
12
12
 
13
13
  /** CSS class names applied to the canvases threadText creates inside a container. */
14
14
  export declare const THREAD_TEXT_CLASSES: {
15
- /** Base canvas: woven fabric + baked satin stitches. */
15
+ /** Base canvas: the baked satin stitches (transparent ground). */
16
16
  readonly bg: "tt-bg";
17
17
  /** Overlay canvas: cursor sheen (`mix-blend-mode: screen`). */
18
18
  readonly fx: "tt-fx";
19
+ /** Blinking text caret shown in editable mode. */
20
+ readonly caret: "tt-caret";
19
21
  };
20
22
 
21
23
  /**
@@ -26,15 +28,19 @@ export declare const ThreadText: default_2.ForwardRefExoticComponent<ThreadTextP
26
28
 
27
29
  /** Live handle to a mounted threadText renderer. */
28
30
  export declare interface ThreadTextInstance {
29
- /**
30
- * Re-embroider with new text. When `text` extends the current word (an append),
31
- * only the newly-added letters animate in; otherwise the whole word is re-sewn.
32
- */
31
+ /** Re-embroider with new text — re-fits to width and redraws instantly (no sew-in). */
33
32
  setText(text: string): void;
34
- /** Re-run the full sew-in animation for the current word. */
33
+ /** Re-run the sew-in animation for the current word (when `animate` is on). */
35
34
  replay(): void;
36
- /** Re-fit the render surface to its container and rebuild. */
35
+ /** Re-fit the render surface to its container and redraw. */
37
36
  resize(): void;
37
+ /**
38
+ * Apply option changes live and redraw instantly — never re-runs the sew-in. Use for
39
+ * colour, font, weight, size (`fill`), sew rate, sheen, and editability changes.
40
+ */
41
+ update(options: Partial<ThreadTextOptions>): void;
42
+ /** Focus the surface for typing (editable mode). */
43
+ focus(): void;
38
44
  /** Cancel the animation loop, remove listeners, and free the created canvases. */
39
45
  destroy(): void;
40
46
  /** The current embroidered text. */
@@ -58,29 +64,36 @@ export declare interface ThreadTextOptions {
58
64
  weight?: number;
59
65
  /** Floss (thread) colour — the lit crest of each thread. (default: warm white '#fffbf3') */
60
66
  threadColor?: string;
61
- /** Woven-ground base colour — the outer edge of the fabric. (default near-black linen '#08080a') */
62
- fabricColor?: string;
63
67
  /**
64
68
  * Thread spacing in internal pixels. Smaller = finer, denser stitching (and more work).
65
- * (default: auto — derived from the canvas height)
69
+ * (default: auto — derived from the fitted text height)
66
70
  */
67
71
  pitch?: number;
68
- /** Play the sew-in animation when the instance mounts. (default: true) */
72
+ /**
73
+ * Fraction of the container width the word spans — the effective text size. The word is
74
+ * re-fitted to `fill × containerWidth` on load and resize, leaving the remainder as
75
+ * horizontal padding. Range ~0.3–1. (default: 0.9)
76
+ */
77
+ fill?: number;
78
+ /** Play the sew-in animation on mount and on `replay()`. (default: true) */
69
79
  animate?: boolean;
70
80
  /** Satin cross-rows laid per second during the sew-in. (default: 110) */
71
81
  sewRate?: number;
72
82
  /** Enable the cursor-following radial sheen on the overlay canvas. (default: true) */
73
83
  sheen?: boolean;
84
+ /**
85
+ * Make the surface focusable and typeable — click (or Tab) to focus, then type to edit
86
+ * the word, Backspace to delete, Enter to replay the sew-in. Shows a blinking caret.
87
+ * (default: false)
88
+ */
89
+ editable?: boolean;
90
+ /** Called with the new text whenever the user edits it directly (editable mode only). */
91
+ onTextChange?: (text: string) => void;
74
92
  /**
75
93
  * Force reduced-motion (skip the sew-in, draw instantly). If omitted, the value is
76
94
  * auto-detected from `prefers-reduced-motion`.
77
95
  */
78
96
  reducedMotion?: boolean;
79
- /**
80
- * Aspect ratio (height / width) of the render surface when the target is a plain
81
- * container and its own canvases are created. (default: 0.46)
82
- */
83
- aspect?: number;
84
97
  }
85
98
 
86
99
  declare interface ThreadTextProps extends ThreadTextOptions {
@@ -100,10 +113,11 @@ declare interface ThreadTextProps extends ThreadTextOptions {
100
113
  /**
101
114
  * React hook that mounts a threadText renderer inside a ref'd container.
102
115
  *
103
- * The instance is (re)created only when a *structural* option changes (font, weight,
104
- * colours, pitch, sizing, motion). Changing just `text` routes through
105
- * `instance.setText()` so appended letters animate in without a full remount.
106
- * Re-fits to the container via ResizeObserver and tears down on unmount.
116
+ * The instance is created once and kept alive; option changes (font, weight, colour,
117
+ * size, sew rate, sheen, editability) are applied live via `instance.update()` no
118
+ * remount, no re-sew. Text changes route through `setText`. The surface re-fits to the
119
+ * container width via ResizeObserver, and tears down on unmount. Only `reducedMotion`
120
+ * (fixed at creation) forces a fresh instance.
107
121
  *
108
122
  * @param options - {@link ThreadTextOptions} for the renderer.
109
123
  * @returns A ref to attach to the target container element.