@liiift-studio/threadtext 0.0.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 ADDED
@@ -0,0 +1,85 @@
1
+ # threadText
2
+
3
+ **Render any text as photorealistic, procedural satin-stitch embroidery — in real time, in the browser, from the font's actual glyph geometry.**
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).
11
+
12
+ Homepage: [threadtext.com](https://threadtext.com) · npm: `@liiift-studio/threadtext`
13
+
14
+ ---
15
+
16
+ ## What it is
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.
19
+
20
+ ## Why it's worth building (the niche is empty)
21
+
22
+ A survey of the field (embroidery-digitizing software, JS libraries, mockup generators, CSS tricks, creative-coding shaders, AI generators, and the graphics literature) found **no existing tool that does all of**: real-time · arbitrary text from a font · procedural (no pre-baked assets) · photorealistic satin · in a plain browser. Every existing thing drops at least one pillar:
23
+
24
+ - **Digitizing software** (Ink/Stitch, PEmbroider, Wilcom/Hatch **TrueView**) — real stitch logic, but desktop, machine-file-oriented; previews are schematic (TrueView's photoreal render is an exported still, not live procedural web).
25
+ - **The one close browser lib** (`p5.embroider`) — right platform, but alpha & non-photoreal.
26
+ - **Fakes** (Photoshop actions, Placeit/Printful mockups, CSS, AI diffusion "embroidery" generators) — raster or generative, not procedural-from-font, not real-time-relightable.
27
+ - **Research/shaders** (Chen–McCool–Kitamoto 2012; Blender knit shaders; Shadertoy stitch patterns) — closest ideas, but not text-from-font, not browser, and either non-photoreal or non-real-time.
28
+
29
+ Defensible novelty to lead with: **(a)** font/variable-font glyph geometry drives the stitch flow field, **(b)** photoreal-by-construction (not AI, not a raster filter), **(c)** fully real-time + relightable + animated in a plain browser with zero pre-baked assets. (Full source list in `HANDOFF.md`.)
30
+
31
+ ## Run the demo
32
+
33
+ It's a single self-contained file — no build step:
34
+
35
+ ```bash
36
+ # from this folder
37
+ python3 -m http.server 8080
38
+ # open http://localhost:8080/demo/
39
+ ```
40
+
41
+ Type to embroider a new word · Backspace to unpick · Enter to replay the sew-in · move the cursor for the sheen.
42
+
43
+ > The demo font (`demo/DaithVF.woff2`) is **Darden Studio proprietary** — fine for local dev, but **swap it for a freely-licensed variable font before this repo is made public or published.** The renderer works with any loaded font.
44
+
45
+ ## How it works (one paragraph)
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`.
48
+
49
+ ## Use it (extracted API)
50
+
51
+ ```ts
52
+ import { createThreadText } from '@liiift-studio/threadtext'
53
+
54
+ // Load the face first (any @font-face / next/font / CSS Font Loading API), then:
55
+ const thread = createThreadText(document.getElementById('host'), {
56
+ text: 'Thread',
57
+ font: '"Your Font", Georgia, serif',
58
+ weight: 680,
59
+ })
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
65
+ ```
66
+
67
+ React:
68
+
69
+ ```tsx
70
+ import { ThreadText } from '@liiift-studio/threadtext'
71
+
72
+ <ThreadText text="Thread" font='"Your Font", serif' weight={680} />
73
+ ```
74
+
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.
78
+
79
+ ## What's next
80
+
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`.
82
+
83
+ ---
84
+
85
+ Part of [type-tools](https://github.com/Liiift-Studio/type-tools) by [Liiift Studio](https://liiift.studio). MIT (intended).
package/dist/index.cjs ADDED
@@ -0,0 +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;
@@ -0,0 +1,113 @@
1
+ import { default as default_2 } from 'react';
2
+ import { RefObject } from 'react';
3
+
4
+ /**
5
+ * Mount a procedural satin-stitch embroidery renderer inside `target`.
6
+ *
7
+ * When `target` is a plain element, two stacked canvases (base + sheen overlay) are
8
+ * created inside it. When `target` is itself a `<canvas>`, it is used as the base and a
9
+ * sheen overlay is added as a sibling. Returns a {@link ThreadTextInstance} handle.
10
+ */
11
+ export declare function createThreadText(target: HTMLElement, opts: ThreadTextOptions): ThreadTextInstance;
12
+
13
+ /** CSS class names applied to the canvases threadText creates inside a container. */
14
+ export declare const THREAD_TEXT_CLASSES: {
15
+ /** Base canvas: woven fabric + baked satin stitches. */
16
+ readonly bg: "tt-bg";
17
+ /** Overlay canvas: cursor sheen (`mix-blend-mode: screen`). */
18
+ readonly fx: "tt-fx";
19
+ };
20
+
21
+ /**
22
+ * Drop-in component that embroiders `text` inside a self-sizing container. The canvas
23
+ * output is decorative; the word is exposed to assistive tech via `aria-label`.
24
+ */
25
+ export declare const ThreadText: default_2.ForwardRefExoticComponent<ThreadTextProps & default_2.RefAttributes<HTMLElement>>;
26
+
27
+ /** Live handle to a mounted threadText renderer. */
28
+ 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
+ */
33
+ setText(text: string): void;
34
+ /** Re-run the full sew-in animation for the current word. */
35
+ replay(): void;
36
+ /** Re-fit the render surface to its container and rebuild. */
37
+ resize(): void;
38
+ /** Cancel the animation loop, remove listeners, and free the created canvases. */
39
+ destroy(): void;
40
+ /** The current embroidered text. */
41
+ readonly text: string;
42
+ }
43
+
44
+ /**
45
+ * Options controlling the procedural satin-stitch embroidery renderer.
46
+ * Colours accept any CSS-ish hex (`#rgb` / `#rrggbb`) or `rgb()/rgba()` string.
47
+ */
48
+ export declare interface ThreadTextOptions {
49
+ /** The word (or short phrase) to embroider. */
50
+ text: string;
51
+ /**
52
+ * CSS `font-family` of an already-loaded font used to rasterise the glyphs.
53
+ * The renderer is font-agnostic — load the face however you like (`@font-face`,
54
+ * `next/font`, the CSS Font Loading API) before calling. (default: 'Georgia, serif')
55
+ */
56
+ font?: string;
57
+ /** Numeric font weight passed to the canvas `font` shorthand (100–900). (default: 680) */
58
+ weight?: number;
59
+ /** Floss (thread) colour — the lit crest of each thread. (default: warm white '#fffbf3') */
60
+ threadColor?: string;
61
+ /** Woven-ground base colour — the outer edge of the fabric. (default near-black linen '#08080a') */
62
+ fabricColor?: string;
63
+ /**
64
+ * Thread spacing in internal pixels. Smaller = finer, denser stitching (and more work).
65
+ * (default: auto — derived from the canvas height)
66
+ */
67
+ pitch?: number;
68
+ /** Play the sew-in animation when the instance mounts. (default: true) */
69
+ animate?: boolean;
70
+ /** Satin cross-rows laid per second during the sew-in. (default: 110) */
71
+ sewRate?: number;
72
+ /** Enable the cursor-following radial sheen on the overlay canvas. (default: true) */
73
+ sheen?: boolean;
74
+ /**
75
+ * Force reduced-motion (skip the sew-in, draw instantly). If omitted, the value is
76
+ * auto-detected from `prefers-reduced-motion`.
77
+ */
78
+ 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
+ }
85
+
86
+ declare interface ThreadTextProps extends ThreadTextOptions {
87
+ className?: string;
88
+ style?: default_2.CSSProperties;
89
+ /** Container element type. (default: 'div') */
90
+ as?: default_2.ElementType;
91
+ /**
92
+ * Accessible label for the embroidery. Defaults to the embroidered `text`, so
93
+ * screen readers announce the word even though it is drawn on a canvas.
94
+ */
95
+ 'aria-label'?: string;
96
+ role?: string;
97
+ 'aria-describedby'?: string;
98
+ }
99
+
100
+ /**
101
+ * React hook that mounts a threadText renderer inside a ref'd container.
102
+ *
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.
107
+ *
108
+ * @param options - {@link ThreadTextOptions} for the renderer.
109
+ * @returns A ref to attach to the target container element.
110
+ */
111
+ export declare function useThreadText(options: ThreadTextOptions): RefObject<HTMLElement | null>;
112
+
113
+ export { }
package/dist/index.js ADDED
@@ -0,0 +1,439 @@
1
+ import { useRef as Pt, useLayoutEffect as ie, useEffect as ce, forwardRef as se, useCallback as le } from "react";
2
+ import { jsx as fe } from "react/jsx-runtime";
3
+ const Jt = {
4
+ /** Base canvas: woven fabric + baked satin stitches. */
5
+ bg: "tt-bg",
6
+ /** Overlay canvas: cursor sheen (`mix-blend-mode: screen`). */
7
+ fx: "tt-fx"
8
+ };
9
+ function Q(x, r) {
10
+ let a = Math.imul(x | 0, 374761393) + Math.imul(r | 0, 668265263) | 0;
11
+ return a = Math.imul(a ^ a >>> 13, 1274126177), ((a ^ a >>> 16) >>> 0) / 4294967295;
12
+ }
13
+ function Nt(x, r) {
14
+ const a = Math.floor(x), c = Math.floor(r), l = x - a, s = r - c, h = l * l * (3 - 2 * l), A = s * s * (3 - 2 * s), S = Q(a, c), I = Q(a + 1, c), f = Q(a, c + 1), u = Q(a + 1, c + 1);
15
+ return S + (I - S) * h + (f - S) * A + (S - I - f + u) * h * A;
16
+ }
17
+ const xt = 1e20;
18
+ function Qt(x, r, a, c, l) {
19
+ let s = 0;
20
+ c[0] = 0, l[0] = -xt, l[1] = xt;
21
+ for (let h = 1; h < r; h++) {
22
+ let A = (x[h] + h * h - (x[c[s]] + c[s] * c[s])) / (2 * h - 2 * c[s]);
23
+ for (; A <= l[s]; )
24
+ s--, A = (x[h] + h * h - (x[c[s]] + c[s] * c[s])) / (2 * h - 2 * c[s]);
25
+ s++, c[s] = h, l[s] = A, l[s + 1] = xt;
26
+ }
27
+ s = 0;
28
+ for (let h = 0; h < r; h++) {
29
+ for (; l[s + 1] < h; ) s++;
30
+ a[h] = (h - c[s]) * (h - c[s]) + x[c[s]];
31
+ }
32
+ }
33
+ function de(x, r, a) {
34
+ const c = new Float64Array(r * a);
35
+ for (let f = 0; f < r * a; f++) c[f] = x[f] ? xt : 0;
36
+ const l = Math.max(r, a), s = new Float64Array(l), h = new Float64Array(l), A = new Int32Array(l), S = new Float64Array(l + 1);
37
+ for (let f = 0; f < r; f++) {
38
+ for (let u = 0; u < a; u++) s[u] = c[u * r + f];
39
+ Qt(s, a, h, A, S);
40
+ for (let u = 0; u < a; u++) c[u * r + f] = h[u];
41
+ }
42
+ for (let f = 0; f < a; f++) {
43
+ for (let u = 0; u < r; u++) s[u] = c[f * r + u];
44
+ Qt(s, r, h, A, S);
45
+ for (let u = 0; u < r; u++) c[f * r + u] = h[u];
46
+ }
47
+ const I = new Float32Array(r * a);
48
+ for (let f = 0; f < r * a; f++) I[f] = Math.sqrt(c[f]);
49
+ return I;
50
+ }
51
+ function Zt(x, r, a, c, l) {
52
+ let s = Float32Array.from(x);
53
+ const h = new Float32Array(r * a), A = 1 / (2 * c + 1), S = (I, f) => I < 0 ? 0 : I > f ? f : I;
54
+ for (let I = 0; I < l; I++) {
55
+ for (let f = 0; f < a; f++) {
56
+ const u = f * r;
57
+ let v = 0;
58
+ for (let P = -c; P <= c; P++) v += s[u + S(P, r - 1)];
59
+ for (let P = 0; P < r; P++)
60
+ h[u + P] = v * A, v += s[u + S(P + c + 1, r - 1)] - s[u + S(P - c, r - 1)];
61
+ }
62
+ for (let f = 0; f < r; f++) {
63
+ let u = 0;
64
+ for (let v = -c; v <= c; v++) u += h[S(v, a - 1) * r + f];
65
+ for (let v = 0; v < a; v++)
66
+ s[v * r + f] = u * A, u += h[S(v + c + 1, a - 1) * r + f] - h[S(v - c, a - 1) * r + f];
67
+ }
68
+ }
69
+ return s;
70
+ }
71
+ function Ht(x, r) {
72
+ if (!x) return r;
73
+ const a = x.trim();
74
+ if (a[0] === "#") {
75
+ let l = a.slice(1);
76
+ if (l.length === 3 && (l = l[0] + l[0] + l[1] + l[1] + l[2] + l[2]), l.length >= 6) {
77
+ const s = parseInt(l.slice(0, 2), 16), h = parseInt(l.slice(2, 4), 16), A = parseInt(l.slice(4, 6), 16);
78
+ if (!Number.isNaN(s) && !Number.isNaN(h) && !Number.isNaN(A)) return [s, h, A];
79
+ }
80
+ return r;
81
+ }
82
+ const c = a.match(/rgba?\(([^)]+)\)/i);
83
+ if (c) {
84
+ const l = c[1].split(",").map((s) => parseFloat(s));
85
+ if (l.length >= 3 && l.slice(0, 3).every((s) => !Number.isNaN(s)))
86
+ return [Math.round(l[0]), Math.round(l[1]), Math.round(l[2])];
87
+ }
88
+ return r;
89
+ }
90
+ function D(x) {
91
+ return x < 0 ? 0 : x > 255 ? 255 : Math.round(x);
92
+ }
93
+ function ue(x, r) {
94
+ if (typeof window > "u" || typeof document > "u" || !x)
95
+ return { setText() {
96
+ }, replay() {
97
+ }, resize() {
98
+ }, destroy() {
99
+ }, get text() {
100
+ return r.text ?? "";
101
+ } };
102
+ let a = r.text ?? "";
103
+ const c = r.font ?? "Georgia, serif", l = r.weight ?? 680, s = r.aspect ?? 0.46, h = r.sewRate ?? 110, A = 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]), v = [u[0] * 0.585, u[1] * 0.585, u[2] * 0.585], P = [u[0] * 0.47, u[1] * 0.47, u[2] * 0.47], Z = Ht(r.fabricColor ?? "#08080a", [8, 8, 10]), wt = [D(Z[0] + 14), D(Z[1] + 13), D(Z[2] + 15)], j = (t) => `rgb(${D(t[0])},${D(t[1])},${D(t[2])})`, H = [];
104
+ let $, O;
105
+ x instanceof HTMLCanvasElement ? ($ = x, O = x.parentElement ?? x) : (O = x, $ = document.createElement("canvas"), H.push($), O.appendChild($)), $.classList.add(Jt.bg);
106
+ const N = document.createElement("canvas");
107
+ H.push(N), N.classList.add(Jt.fx), O.appendChild(N), getComputedStyle(O).position === "static" && (O.style.position = "relative"), $.style.display = "block", $.style.width = "100%", Object.assign(N.style, { position: "absolute", inset: "0", width: "100%", pointerEvents: "none", mixBlendMode: "screen" });
108
+ let e = 0, i = 0, G = 0, pt = 0, tt = 4, et = [], nt = 0, ot = 0, Mt = new Uint8Array(0), Ct = new Float32Array(0), lt = new Float32Array(0), ft = new Float32Array(0), B = null, U = null, V = null, rt = null, dt = null, k = [], ut = 3;
109
+ const b = { rows: [], idx: 0, acc: 0, rate: 0, on: !1 }, K = { x: 0, y: 0, set: !1 };
110
+ let Y = !0, St = 0, Dt = !1, ht = 0, J = !1, At = !1;
111
+ function Ot() {
112
+ const t = O.getBoundingClientRect(), n = Math.round(t.width || Math.min(window.innerWidth * 0.95, 1240)), o = Math.round(n * s);
113
+ let d = Math.min(window.devicePixelRatio || 1, 1.7);
114
+ for (; n * d * o * d > 9e5 && d > 0.55; ) d -= 0.1;
115
+ e = Math.max(1, Math.round(n * d)), i = Math.max(1, Math.round(o * d));
116
+ for (const m of [$, N])
117
+ m.width = e, m.height = i, m.style.height = o + "px";
118
+ O.style.height = o + "px";
119
+ }
120
+ function zt() {
121
+ const t = Math.max(7, tt * 2.5), n = Math.max(3.2, tt * 1.3);
122
+ nt = Math.ceil(t) + 2, ot = Math.ceil(n) + 2, et = [];
123
+ for (let o = 0; o < 20; o++) {
124
+ const d = 0.34 + o / 19 * 0.82, m = document.createElement("canvas");
125
+ m.width = nt, m.height = ot;
126
+ const M = m.getContext("2d");
127
+ if (!M) {
128
+ et.push(m);
129
+ continue;
130
+ }
131
+ const C = nt / 2, T = ot / 2, R = M.createLinearGradient(0, T - n / 2, 0, T + n / 2), p = (L, z) => `rgba(${D(L[0] * d)},${D(L[1] * d)},${D(L[2] * d)},${z})`;
132
+ R.addColorStop(0, p(P, 0)), R.addColorStop(0.16, p(v, 1)), R.addColorStop(0.5, p(u, 1)), R.addColorStop(0.84, p(v, 1)), R.addColorStop(1, p(P, 0)), M.fillStyle = R, M.beginPath();
133
+ const w = n / 2;
134
+ M.moveTo(C - t / 2 + w, T - w), M.lineTo(C + t / 2 - w, T - w), M.arc(C + t / 2 - w, T, w, -Math.PI / 2, Math.PI / 2), M.lineTo(C - t / 2 + w, T + w), M.arc(C - t / 2 + w, T, w, Math.PI / 2, -Math.PI / 2), M.closePath(), M.fill(), M.globalCompositeOperation = "destination-in";
135
+ const E = M.createLinearGradient(C - t / 2, 0, C + t / 2, 0);
136
+ E.addColorStop(0, "rgba(0,0,0,0)"), E.addColorStop(0.22, "rgba(0,0,0,1)"), E.addColorStop(0.78, "rgba(0,0,0,1)"), E.addColorStop(1, "rgba(0,0,0,0)"), M.fillStyle = E, M.fillRect(0, 0, nt, ot), et.push(m);
137
+ }
138
+ }
139
+ function bt() {
140
+ if (!dt) return;
141
+ const t = dt;
142
+ t.clearRect(0, 0, e, i), t.fillStyle = "#fff", t.textAlign = "left", t.textBaseline = "middle", t.font = `${l} ${G}px ${c}`, t.fillText(a, pt, i * 0.5);
143
+ const n = t.getImageData(0, 0, e, i).data, o = new Uint8Array(e * i);
144
+ for (let g = 0; g < i; g++) for (let F = 0; F < e; F++) {
145
+ const y = g * e + F, q = n[y * 4 + 3] / 255, _ = Nt(F * 0.5, g * 0.5) * 0.22 + Nt(F * 1.8, g * 1.8) * 0.1;
146
+ o[y] = q > 0.5 + (_ - 0.16) ? 1 : 0;
147
+ }
148
+ Mt = o;
149
+ const d = de(o, e, i), m = new Float32Array(e * i);
150
+ for (let g = 1; g < i - 1; g++) for (let F = 1; F < e - 1; F++) {
151
+ const y = g * e + F;
152
+ m[y] = (d[y] * 4 + d[y - 1] + d[y + 1] + d[y - e] + d[y + e]) / 8;
153
+ }
154
+ const M = new Float32Array(e * i), C = new Float32Array(e * i), T = new Float32Array(e * i), R = new Float32Array(e * i);
155
+ for (let g = 1; g < i - 1; g++) for (let F = 1; F < e - 1; F++) {
156
+ const y = g * e + F;
157
+ if (!o[y]) continue;
158
+ const q = (m[y + 1] - m[y - 1]) * 0.5, _ = (m[y + e] - m[y - e]) * 0.5;
159
+ T[y] = q, R[y] = _, M[y] = q * q - _ * _, C[y] = 2 * q * _;
160
+ }
161
+ const p = Math.max(7, Math.round(i * 0.05)), w = Zt(M, e, i, p, 2), E = Zt(C, e, i, p, 2);
162
+ lt = new Float32Array(e * i), ft = new Float32Array(e * i);
163
+ for (let g = 0; g < e * i; g++) {
164
+ if (!o[g]) continue;
165
+ const F = 0.5 * Math.atan2(E[g], w[g]);
166
+ lt[g] = Math.cos(F), ft[g] = Math.sin(F);
167
+ }
168
+ const L = Math.max(9, i * 0.04), z = new Float32Array(e * i), ct = -0.45, st = -0.62, yt = 0.64;
169
+ for (let g = 1; g < i - 1; g++) for (let F = 1; F < e - 1; F++) {
170
+ const y = g * e + F;
171
+ if (!o[y]) continue;
172
+ let q = T[y], _ = R[y];
173
+ const Ut = Math.hypot(q, _) || 1;
174
+ q /= Ut, _ /= Ut;
175
+ const Vt = m[y], Kt = (1 - Math.min(Vt / L, 1)) * 1.25;
176
+ let vt = -q * Kt, Rt = -_ * Kt;
177
+ const Yt = 1.5, $t = Math.hypot(vt, Rt, Yt);
178
+ vt /= $t, Rt /= $t;
179
+ let Lt = vt * ct + Rt * st + Yt / $t * yt;
180
+ Lt < 0 && (Lt = 0);
181
+ const ae = Math.min(1, 0.45 + 0.55 * Math.min(Vt / (L * 0.7), 1));
182
+ z[y] = Math.min(1.12, (0.5 + 0.75 * Lt) * ae);
183
+ }
184
+ Ct = z, B = document.createElement("canvas"), B.width = e, B.height = i;
185
+ const X = B.getContext("2d");
186
+ U = document.createElement("canvas"), U.width = e, U.height = i;
187
+ const W = U.getContext("2d");
188
+ if (X && W) {
189
+ const g = X.createImageData(e, i), F = W.createImageData(e, i);
190
+ 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, F.data[y * 4 + 3] = 255);
191
+ X.putImageData(g, 0, 0), W.putImageData(F, 0, 0);
192
+ }
193
+ }
194
+ function qt() {
195
+ V = document.createElement("canvas"), V.width = e, V.height = i;
196
+ const t = V.getContext("2d", { willReadFrequently: !0 });
197
+ if (t) {
198
+ const n = t.createRadialGradient(e * 0.5, i * 0.4, 0, e * 0.5, i * 0.5, e * 0.72);
199
+ n.addColorStop(0, j(wt)), n.addColorStop(1, j(Z)), t.fillStyle = n, t.fillRect(0, 0, e, i);
200
+ const o = t.getImageData(0, 0, e, i), d = o.data;
201
+ for (let m = 0; m < i; m++) for (let M = 0; M < e; M++) {
202
+ const C = (m * e + M) * 4, T = Math.sin(M * 1.9) * 0.5 + 0.5, R = Math.sin(m * 1.9) * 0.5 + 0.5, p = T * R * 9 - 4.5 + (Nt(M * 0.18, m * 0.18) - 0.5) * 15;
203
+ d[C] = D(d[C] + p), d[C + 1] = D(d[C + 1] + p), d[C + 2] = D(d[C + 2] + p * 1.1);
204
+ }
205
+ t.putImageData(o, 0, 0);
206
+ }
207
+ rt = document.createElement("canvas"), rt.width = e, rt.height = i, dt = rt.getContext("2d", { willReadFrequently: !0 });
208
+ }
209
+ function Ft() {
210
+ const t = Math.max(1.6, tt * 0.6);
211
+ ut = t;
212
+ const n = [];
213
+ for (let o = 0; o < i; o += t) for (let d = 0; d < e; d += t) {
214
+ const m = (Q(Math.floor(d), Math.floor(o)) - 0.5) * t * 0.9, M = (Q(Math.floor(o), Math.floor(d)) - 0.5) * t * 0.9, C = Math.round(d + m), T = Math.round(o + M);
215
+ if (C < 1 || T < 1 || C >= e - 1 || T >= i - 1) continue;
216
+ const R = T * e + C;
217
+ if (!Mt[R]) continue;
218
+ const p = Math.atan2(ft[R], lt[R]), w = 0.86 + Q(C * 7, T * 13) * 0.18;
219
+ let E = Ct[R] * w;
220
+ E = E < 0 ? 0 : E > 1.12 ? 1.12 : E, n.push({ x: C, y: T, ang: p, idx: Math.min(19, Math.max(0, Math.round(E / 1.12 * 19))) });
221
+ }
222
+ n.sort((o, d) => o.x - d.x), k = n;
223
+ }
224
+ function mt(t, n) {
225
+ t.save(), t.translate(n.x, n.y), t.rotate(n.ang), t.drawImage(et[n.idx], -nt / 2, -ot / 2), t.restore();
226
+ }
227
+ function gt() {
228
+ const t = $.getContext("2d");
229
+ if (t)
230
+ for (const n of k) mt(t, n);
231
+ }
232
+ function _t() {
233
+ return k.length ? k[k.length - 1].x : 0;
234
+ }
235
+ function Tt() {
236
+ return k.length ? k[0].x : 0;
237
+ }
238
+ function at() {
239
+ const t = $.getContext("2d");
240
+ !t || !V || (t.drawImage(V, 0, 0), U && (t.save(), t.globalAlpha = 0.85, t.filter = `blur(${Math.round(i * 0.018)}px)`, t.drawImage(U, 0, Math.round(i * 0.02)), t.restore()));
241
+ }
242
+ function Wt(t) {
243
+ if (!t.length) return [];
244
+ const n = Math.max(4, ut * 1.5), o = /* @__PURE__ */ new Map(), d = (p, w) => p + "," + w;
245
+ t.forEach((p, w) => {
246
+ const E = d(Math.floor(p.x / n), Math.floor(p.y / n));
247
+ let L = o.get(E);
248
+ L || o.set(E, L = []), L.push(w);
249
+ });
250
+ const m = ut * 1.8 * (ut * 1.8);
251
+ function M(p) {
252
+ const w = t[p], E = Math.floor(w.x / n), L = Math.floor(w.y / n), z = [];
253
+ for (let ct = -1; ct <= 1; ct++) for (let st = -1; st <= 1; st++) {
254
+ const yt = o.get(d(E + ct, L + st));
255
+ if (yt)
256
+ for (const X of yt) {
257
+ if (X === p) continue;
258
+ const W = t[X].x - w.x, g = t[X].y - w.y;
259
+ W * W + g * g <= m && z.push(X);
260
+ }
261
+ }
262
+ return z;
263
+ }
264
+ const C = new Uint8Array(t.length), T = [], R = t.map((p, w) => w).sort((p, w) => t[p].x + t[p].y - (t[w].x + t[w].y));
265
+ for (const p of R) {
266
+ if (C[p]) continue;
267
+ let w = [p];
268
+ for (C[p] = 1; w.length; ) {
269
+ T.push(w.map((L) => t[L]));
270
+ const E = [];
271
+ for (const L of w) for (const z of M(L)) C[z] || (C[z] = 1, E.push(z));
272
+ w = E;
273
+ }
274
+ }
275
+ return T;
276
+ }
277
+ function Et(t, n) {
278
+ at();
279
+ const o = $.getContext("2d");
280
+ if (!o) return;
281
+ const d = [];
282
+ for (const m of k)
283
+ m.x <= t ? mt(o, m) : d.push(m);
284
+ if (f) {
285
+ for (const m of d) mt(o, m);
286
+ b.on = !1;
287
+ return;
288
+ }
289
+ b.rows = Wt(d), b.idx = 0, b.acc = 0, b.rate = n || h, b.on = b.rows.length > 0;
290
+ }
291
+ function te(t) {
292
+ const n = $.getContext("2d");
293
+ if (n)
294
+ for (t > b.rows.length && (t = b.rows.length); b.idx < t; ) {
295
+ const o = b.rows[b.idx++];
296
+ for (const d of o) mt(n, d);
297
+ }
298
+ }
299
+ function ee(t, n) {
300
+ const o = N.getContext("2d");
301
+ if (!o || (o.clearRect(0, 0, e, i), !A || !B)) return;
302
+ const d = e * 0.32, m = o.createRadialGradient(t, n, 0, t, n, d);
303
+ m.addColorStop(0, "rgba(255,250,238,0.18)"), m.addColorStop(0.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(B, 0, 0), o.globalCompositeOperation = "source-over";
304
+ }
305
+ function jt() {
306
+ const t = $.getContext("2d");
307
+ if (!t) {
308
+ G = Math.round(i * 0.6), pt = Math.round(e * 0.14);
309
+ return;
310
+ }
311
+ const n = a || "Ag";
312
+ t.font = `${l} 100px ${c}`;
313
+ const o = t.measureText(n).width || 100;
314
+ G = Math.round(100 * (e * 0.72) / o), t.font = `${l} ${G}px ${c}`, pt = Math.round(e / 2 - t.measureText(n).width / 2);
315
+ }
316
+ function kt(t) {
317
+ if (J) return;
318
+ ht || (ht = t);
319
+ const n = Math.min(0.25, (t - ht) / 1e3);
320
+ ht = t, b.on && (b.acc += b.rate * n, te(b.acc), b.idx >= b.rows.length && (b.on = !1)), Y && B && (ee(K.set ? K.x : e * 0.5, K.set ? K.y : i * 0.34), Y = !1), St = requestAnimationFrame(kt);
321
+ }
322
+ function Bt(t) {
323
+ const n = N.getBoundingClientRect();
324
+ 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;
325
+ }
326
+ A && window.addEventListener("pointermove", Bt, { passive: !0 });
327
+ function ne(t) {
328
+ bt(), Ft(), S && !f ? Et(Tt() - 1, h) : (at(), gt(), b.on = !1), Y = !0;
329
+ }
330
+ function oe() {
331
+ J || (Ot(), qt(), tt = Math.max(3.4, i * 95e-4), zt(), jt(), ne(), At = !0, Dt || (Dt = !0, St = requestAnimationFrame(kt)));
332
+ }
333
+ let it;
334
+ function re(t) {
335
+ const n = `${l} 200px ${I}`;
336
+ let o = !1;
337
+ try {
338
+ o = !!document.fonts && document.fonts.check(n);
339
+ } catch {
340
+ o = !0;
341
+ }
342
+ if (!document.fonts || o) {
343
+ t();
344
+ return;
345
+ }
346
+ let d = !1;
347
+ const m = () => {
348
+ d || (d = !0, it && clearTimeout(it), t());
349
+ };
350
+ document.fonts.load(n, a).then(() => document.fonts.ready).then(m, m), it = setTimeout(m, 1500);
351
+ }
352
+ re(oe);
353
+ let It;
354
+ function Xt() {
355
+ clearTimeout(It), It = setTimeout(() => Gt.resize(), 200);
356
+ }
357
+ window.addEventListener("resize", Xt);
358
+ const Gt = {
359
+ get text() {
360
+ return a;
361
+ },
362
+ setText(t) {
363
+ if (J || !At) {
364
+ a = t ?? "";
365
+ return;
366
+ }
367
+ t = t ?? "";
368
+ const n = t.length > a.length && a.length > 0 && t.startsWith(a), o = n ? _t() : -1;
369
+ a = t, bt(), Ft(), f || !S ? (at(), gt(), b.on = !1) : Et(n && o > 0 ? Math.min(o, _t()) : Tt() - 1, n ? 320 : h), Y = !0;
370
+ },
371
+ replay() {
372
+ J || !At || (f || !S ? (at(), gt(), b.on = !1) : Et(Tt() - 1, h), Y = !0);
373
+ },
374
+ resize() {
375
+ J || (Ot(), qt(), tt = Math.max(3.4, i * 95e-4), zt(), jt(), bt(), Ft(), at(), gt(), b.on = !1, Y = !0);
376
+ },
377
+ destroy() {
378
+ if (!J) {
379
+ J = !0, cancelAnimationFrame(St), clearTimeout(It), it && clearTimeout(it), window.removeEventListener("resize", Xt), A && window.removeEventListener("pointermove", Bt);
380
+ for (const t of H) t.remove();
381
+ et = [], k = [], b.rows = [], Mt = new Uint8Array(0), Ct = new Float32Array(0), lt = new Float32Array(0), ft = new Float32Array(0), B = U = V = rt = null, dt = null;
382
+ }
383
+ }
384
+ };
385
+ return Gt;
386
+ }
387
+ function he(x) {
388
+ const r = Pt(null), a = Pt(null), c = Pt(x);
389
+ c.current = x;
390
+ const { text: l, font: s, weight: h, threadColor: A, fabricColor: S, pitch: I, animate: f, sewRate: u, sheen: v, reducedMotion: P, aspect: Z } = x, wt = JSON.stringify({ font: s, weight: h, threadColor: A, fabricColor: S, pitch: I, animate: f, sewRate: u, sheen: v, reducedMotion: P, aspect: Z });
391
+ return ie(() => {
392
+ const j = r.current;
393
+ if (!j) return;
394
+ const H = ue(j, c.current);
395
+ a.current = H;
396
+ let $ = 0, O = 0, N;
397
+ return typeof ResizeObserver < "u" && (N = new ResizeObserver((e) => {
398
+ const i = Math.round(e[0].contentRect.width);
399
+ i !== O && (O = i, cancelAnimationFrame($), $ = requestAnimationFrame(() => {
400
+ var G;
401
+ return (G = a.current) == null ? void 0 : G.resize();
402
+ }));
403
+ }), N.observe(j)), () => {
404
+ N == null || N.disconnect(), cancelAnimationFrame($), H.destroy(), a.current = null;
405
+ };
406
+ }, [wt]), ce(() => {
407
+ var j;
408
+ (j = a.current) == null || j.setText(l);
409
+ }, [l]), r;
410
+ }
411
+ const me = se(
412
+ function({ className: r, style: a, as: c = "div", "aria-label": l, role: s, "aria-describedby": h, ...A }, S) {
413
+ const I = he(A), f = le(
414
+ (u) => {
415
+ I.current = u, typeof S == "function" ? S(u) : S && (S.current = u);
416
+ },
417
+ // eslint-disable-next-line react-hooks/exhaustive-deps
418
+ [I, S]
419
+ );
420
+ return /* @__PURE__ */ fe(
421
+ c,
422
+ {
423
+ ref: f,
424
+ className: r,
425
+ style: a,
426
+ role: s ?? "img",
427
+ "aria-label": l ?? A.text,
428
+ "aria-describedby": h
429
+ }
430
+ );
431
+ }
432
+ );
433
+ me.displayName = "ThreadText";
434
+ export {
435
+ Jt as THREAD_TEXT_CLASSES,
436
+ me as ThreadText,
437
+ ue as createThreadText,
438
+ he as useThreadText
439
+ };
@@ -0,0 +1 @@
1
+ var ThreadText=(function(at){"use strict";const Dt={bg:"tt-bg",fx:"tt-fx"};function B(s,n){let e=Math.imul(s|0,374761393)+Math.imul(n|0,668265263)|0;return e=Math.imul(e^e>>>13,1274126177),((e^e>>>16)>>>0)/4294967295}function xt(s,n){const e=Math.floor(s),o=Math.floor(n),r=s-e,d=n-o,m=r*r*(3-2*r),b=d*d*(3-2*d),E=B(e,o),L=B(e+1,o),u=B(e,o+1),g=B(e+1,o+1);return E+(L-E)*m+(u-E)*b+(E-L-u+g)*m*b}const it=1e20;function Ot(s,n,e,o,r){let d=0;o[0]=0,r[0]=-it,r[1]=it;for(let m=1;m<n;m++){let b=(s[m]+m*m-(s[o[d]]+o[d]*o[d]))/(2*m-2*o[d]);for(;b<=r[d];)d--,b=(s[m]+m*m-(s[o[d]]+o[d]*o[d]))/(2*m-2*o[d]);d++,o[d]=m,r[d]=b,r[d+1]=it}d=0;for(let m=0;m<n;m++){for(;r[d+1]<m;)d++;e[m]=(m-o[d])*(m-o[d])+s[o[d]]}}function ee(s,n,e){const o=new Float64Array(n*e);for(let u=0;u<n*e;u++)o[u]=s[u]?it:0;const r=Math.max(n,e),d=new Float64Array(r),m=new Float64Array(r),b=new Int32Array(r),E=new Float64Array(r+1);for(let u=0;u<n;u++){for(let g=0;g<e;g++)d[g]=o[g*n+u];Ot(d,e,m,b,E);for(let g=0;g<e;g++)o[g*n+u]=m[g]}for(let u=0;u<e;u++){for(let g=0;g<n;g++)d[g]=o[u*n+g];Ot(d,n,m,b,E);for(let g=0;g<n;g++)o[u*n+g]=m[g]}const L=new Float32Array(n*e);for(let u=0;u<n*e;u++)L[u]=Math.sqrt(o[u]);return L}function _t(s,n,e,o,r){let d=Float32Array.from(s);const m=new Float32Array(n*e),b=1/(2*o+1),E=(L,u)=>L<0?0:L>u?u:L;for(let L=0;L<r;L++){for(let u=0;u<e;u++){const g=u*n;let v=0;for(let $=-o;$<=o;$++)v+=d[g+E($,n-1)];for(let $=0;$<n;$++)m[g+$]=v*b,v+=d[g+E($+o+1,n-1)]-d[g+E($-o,n-1)]}for(let u=0;u<n;u++){let g=0;for(let v=-o;v<=o;v++)g+=m[E(v,e-1)*n+u];for(let v=0;v<e;v++)d[v*n+u]=g*b,g+=m[E(v+o+1,e-1)*n+u]-m[E(v-o,e-1)*n+u]}}return d}function qt(s,n){if(!s)return n;const e=s.trim();if(e[0]==="#"){let r=e.slice(1);if(r.length===3&&(r=r[0]+r[0]+r[1]+r[1]+r[2]+r[2]),r.length>=6){const d=parseInt(r.slice(0,2),16),m=parseInt(r.slice(2,4),16),b=parseInt(r.slice(4,6),16);if(!Number.isNaN(d)&&!Number.isNaN(m)&&!Number.isNaN(b))return[d,m,b]}return n}const o=e.match(/rgba?\(([^)]+)\)/i);if(o){const r=o[1].split(",").map(d=>parseFloat(d));if(r.length>=3&&r.slice(0,3).every(d=>!Number.isNaN(d)))return[Math.round(r[0]),Math.round(r[1]),Math.round(r[2])]}return n}function P(s){return s<0?0:s>255?255:Math.round(s)}function ne(s,n){if(typeof window>"u"||typeof document>"u"||!s)return{setText(){},replay(){},resize(){},destroy(){},get text(){return n.text??""}};let e=n.text??"";const o=n.font??"Georgia, serif",r=n.weight??680,d=n.aspect??.46,m=n.sewRate??110,b=n.sheen??!0,E=n.animate??!0,L=(o.split(",")[0]||"serif").trim().replace(/^["']|["']$/g,""),u=n.reducedMotion??!!(window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches),g=qt(n.threadColor??"#fffbf3",[255,251,243]),v=[g[0]*.585,g[1]*.585,g[2]*.585],$=[g[0]*.47,g[1]*.47,g[2]*.47],st=qt(n.fabricColor??"#08080a",[8,8,10]),ce=[P(st[0]+14),P(st[1]+13),P(st[2]+15)],kt=t=>`rgb(${P(t[0])},${P(t[1])},${P(t[2])})`,Mt=[];let R,j;s instanceof HTMLCanvasElement?(R=s,j=s.parentElement??s):(j=s,R=document.createElement("canvas"),Mt.push(R),j.appendChild(R)),R.classList.add(Dt.bg);const X=document.createElement("canvas");Mt.push(X),X.classList.add(Dt.fx),j.appendChild(X),getComputedStyle(j).position==="static"&&(j.style.position="relative"),R.style.display="block",R.style.width="100%",Object.assign(X.style,{position:"absolute",inset:"0",width:"100%",pointerEvents:"none",mixBlendMode:"screen"});let c=0,l=0,lt=0,Ct=0,J=4,Q=[],Z=0,W=0,St=new Uint8Array(0),At=new Float32Array(0),ft=new Float32Array(0),dt=new Float32Array(0),k=null,G=null,U=null,tt=null,ut=null,q=[],ht=3;const S={rows:[],idx:0,acc:0,rate:0,on:!1},V={x:0,y:0,set:!1};let H=!0,Tt=0,zt=!1,mt=0,K=!1,Ft=!1;function Bt(){const t=j.getBoundingClientRect(),a=Math.round(t.width||Math.min(window.innerWidth*.95,1240)),i=Math.round(a*d);let f=Math.min(window.devicePixelRatio||1,1.7);for(;a*f*i*f>9e5&&f>.55;)f-=.1;c=Math.max(1,Math.round(a*f)),l=Math.max(1,Math.round(i*f));for(const h of[R,X])h.width=c,h.height=l,h.style.height=i+"px";j.style.height=i+"px"}function Xt(){const t=Math.max(7,J*2.5),a=Math.max(3.2,J*1.3);Z=Math.ceil(t)+2,W=Math.ceil(a)+2,Q=[];for(let i=0;i<20;i++){const f=.34+i/19*.82,h=document.createElement("canvas");h.width=Z,h.height=W;const M=h.getContext("2d");if(!M){Q.push(h);continue}const C=Z/2,T=W/2,I=M.createLinearGradient(0,T-a/2,0,T+a/2),p=(N,D)=>`rgba(${P(N[0]*f)},${P(N[1]*f)},${P(N[2]*f)},${D})`;I.addColorStop(0,p($,0)),I.addColorStop(.16,p(v,1)),I.addColorStop(.5,p(g,1)),I.addColorStop(.84,p(v,1)),I.addColorStop(1,p($,0)),M.fillStyle=I,M.beginPath();const x=a/2;M.moveTo(C-t/2+x,T-x),M.lineTo(C+t/2-x,T-x),M.arc(C+t/2-x,T,x,-Math.PI/2,Math.PI/2),M.lineTo(C-t/2+x,T+x),M.arc(C-t/2+x,T,x,Math.PI/2,-Math.PI/2),M.closePath(),M.fill(),M.globalCompositeOperation="destination-in";const F=M.createLinearGradient(C-t/2,0,C+t/2,0);F.addColorStop(0,"rgba(0,0,0,0)"),F.addColorStop(.22,"rgba(0,0,0,1)"),F.addColorStop(.78,"rgba(0,0,0,1)"),F.addColorStop(1,"rgba(0,0,0,0)"),M.fillStyle=F,M.fillRect(0,0,Z,W),Q.push(h)}}function bt(){if(!ut)return;const t=ut;t.clearRect(0,0,c,l),t.fillStyle="#fff",t.textAlign="left",t.textBaseline="middle",t.font=`${r} ${lt}px ${o}`,t.fillText(e,Ct,l*.5);const a=t.getImageData(0,0,c,l).data,i=new Uint8Array(c*l);for(let y=0;y<l;y++)for(let A=0;A<c;A++){const w=y*c+A,O=a[w*4+3]/255,_=xt(A*.5,y*.5)*.22+xt(A*1.8,y*1.8)*.1;i[w]=O>.5+(_-.16)?1:0}St=i;const f=ee(i,c,l),h=new Float32Array(c*l);for(let y=1;y<l-1;y++)for(let A=1;A<c-1;A++){const w=y*c+A;h[w]=(f[w]*4+f[w-1]+f[w+1]+f[w-c]+f[w+c])/8}const M=new Float32Array(c*l),C=new Float32Array(c*l),T=new Float32Array(c*l),I=new Float32Array(c*l);for(let y=1;y<l-1;y++)for(let A=1;A<c-1;A++){const w=y*c+A;if(!i[w])continue;const O=(h[w+1]-h[w-1])*.5,_=(h[w+c]-h[w-c])*.5;T[w]=O,I[w]=_,M[w]=O*O-_*_,C[w]=2*O*_}const p=Math.max(7,Math.round(l*.05)),x=_t(M,c,l,p,2),F=_t(C,c,l,p,2);ft=new Float32Array(c*l),dt=new Float32Array(c*l);for(let y=0;y<c*l;y++){if(!i[y])continue;const A=.5*Math.atan2(F[y],x[y]);ft[y]=Math.cos(A),dt[y]=Math.sin(A)}const N=Math.max(9,l*.04),D=new Float32Array(c*l),ot=-.45,rt=-.62,wt=.64;for(let y=1;y<l-1;y++)for(let A=1;A<c-1;A++){const w=y*c+A;if(!i[w])continue;let O=T[w],_=I[w];const Qt=Math.hypot(O,_)||1;O/=Qt,_/=Qt;const Zt=h[w],Wt=(1-Math.min(Zt/N,1))*1.25;let Lt=-O*Wt,Rt=-_*Wt;const te=1.5,Pt=Math.hypot(Lt,Rt,te);Lt/=Pt,Rt/=Pt;let $t=Lt*ot+Rt*rt+te/Pt*wt;$t<0&&($t=0);const me=Math.min(1,.45+.55*Math.min(Zt/(N*.7),1));D[w]=Math.min(1.12,(.5+.75*$t)*me)}At=D,k=document.createElement("canvas"),k.width=c,k.height=l;const z=k.getContext("2d");G=document.createElement("canvas"),G.width=c,G.height=l;const Y=G.getContext("2d");if(z&&Y){const y=z.createImageData(c,l),A=Y.createImageData(c,l);for(let w=0;w<c*l;w++)i[w]&&(y.data[w*4]=255,y.data[w*4+1]=255,y.data[w*4+2]=255,y.data[w*4+3]=255,A.data[w*4+3]=255);z.putImageData(y,0,0),Y.putImageData(A,0,0)}}function Gt(){U=document.createElement("canvas"),U.width=c,U.height=l;const t=U.getContext("2d",{willReadFrequently:!0});if(t){const a=t.createRadialGradient(c*.5,l*.4,0,c*.5,l*.5,c*.72);a.addColorStop(0,kt(ce)),a.addColorStop(1,kt(st)),t.fillStyle=a,t.fillRect(0,0,c,l);const i=t.getImageData(0,0,c,l),f=i.data;for(let h=0;h<l;h++)for(let M=0;M<c;M++){const C=(h*c+M)*4,T=Math.sin(M*1.9)*.5+.5,I=Math.sin(h*1.9)*.5+.5,p=T*I*9-4.5+(xt(M*.18,h*.18)-.5)*15;f[C]=P(f[C]+p),f[C+1]=P(f[C+1]+p),f[C+2]=P(f[C+2]+p*1.1)}t.putImageData(i,0,0)}tt=document.createElement("canvas"),tt.width=c,tt.height=l,ut=tt.getContext("2d",{willReadFrequently:!0})}function Et(){const t=Math.max(1.6,J*.6);ht=t;const a=[];for(let i=0;i<l;i+=t)for(let f=0;f<c;f+=t){const h=(B(Math.floor(f),Math.floor(i))-.5)*t*.9,M=(B(Math.floor(i),Math.floor(f))-.5)*t*.9,C=Math.round(f+h),T=Math.round(i+M);if(C<1||T<1||C>=c-1||T>=l-1)continue;const I=T*c+C;if(!St[I])continue;const p=Math.atan2(dt[I],ft[I]),x=.86+B(C*7,T*13)*.18;let F=At[I]*x;F=F<0?0:F>1.12?1.12:F,a.push({x:C,y:T,ang:p,idx:Math.min(19,Math.max(0,Math.round(F/1.12*19)))})}a.sort((i,f)=>i.x-f.x),q=a}function gt(t,a){t.save(),t.translate(a.x,a.y),t.rotate(a.ang),t.drawImage(Q[a.idx],-Z/2,-W/2),t.restore()}function yt(){const t=R.getContext("2d");if(t)for(const a of q)gt(t,a)}function Ut(){return q.length?q[q.length-1].x:0}function It(){return q.length?q[0].x:0}function et(){const t=R.getContext("2d");!t||!U||(t.drawImage(U,0,0),G&&(t.save(),t.globalAlpha=.85,t.filter=`blur(${Math.round(l*.018)}px)`,t.drawImage(G,0,Math.round(l*.02)),t.restore()))}function se(t){if(!t.length)return[];const a=Math.max(4,ht*1.5),i=new Map,f=(p,x)=>p+","+x;t.forEach((p,x)=>{const F=f(Math.floor(p.x/a),Math.floor(p.y/a));let N=i.get(F);N||i.set(F,N=[]),N.push(x)});const h=ht*1.8*(ht*1.8);function M(p){const x=t[p],F=Math.floor(x.x/a),N=Math.floor(x.y/a),D=[];for(let ot=-1;ot<=1;ot++)for(let rt=-1;rt<=1;rt++){const wt=i.get(f(F+ot,N+rt));if(wt)for(const z of wt){if(z===p)continue;const Y=t[z].x-x.x,y=t[z].y-x.y;Y*Y+y*y<=h&&D.push(z)}}return D}const C=new Uint8Array(t.length),T=[],I=t.map((p,x)=>x).sort((p,x)=>t[p].x+t[p].y-(t[x].x+t[x].y));for(const p of I){if(C[p])continue;let x=[p];for(C[p]=1;x.length;){T.push(x.map(N=>t[N]));const F=[];for(const N of x)for(const D of M(N))C[D]||(C[D]=1,F.push(D));x=F}}return T}function vt(t,a){et();const i=R.getContext("2d");if(!i)return;const f=[];for(const h of q)h.x<=t?gt(i,h):f.push(h);if(u){for(const h of f)gt(i,h);S.on=!1;return}S.rows=se(f),S.idx=0,S.acc=0,S.rate=a||m,S.on=S.rows.length>0}function le(t){const a=R.getContext("2d");if(a)for(t>S.rows.length&&(t=S.rows.length);S.idx<t;){const i=S.rows[S.idx++];for(const f of i)gt(a,f)}}function fe(t,a){const i=X.getContext("2d");if(!i||(i.clearRect(0,0,c,l),!b||!k))return;const f=c*.32,h=i.createRadialGradient(t,a,0,t,a,f);h.addColorStop(0,"rgba(255,250,238,0.18)"),h.addColorStop(.5,"rgba(255,250,238,0.055)"),h.addColorStop(1,"rgba(255,250,238,0)"),i.fillStyle=h,i.fillRect(0,0,c,l),i.globalCompositeOperation="destination-in",i.drawImage(k,0,0),i.globalCompositeOperation="source-over"}function Vt(){const t=R.getContext("2d");if(!t){lt=Math.round(l*.6),Ct=Math.round(c*.14);return}const a=e||"Ag";t.font=`${r} 100px ${o}`;const i=t.measureText(a).width||100;lt=Math.round(100*(c*.72)/i),t.font=`${r} ${lt}px ${o}`,Ct=Math.round(c/2-t.measureText(a).width/2)}function Ht(t){if(K)return;mt||(mt=t);const a=Math.min(.25,(t-mt)/1e3);mt=t,S.on&&(S.acc+=S.rate*a,le(S.acc),S.idx>=S.rows.length&&(S.on=!1)),H&&k&&(fe(V.set?V.x:c*.5,V.set?V.y:l*.34),H=!1),Tt=requestAnimationFrame(Ht)}function Kt(t){const a=X.getBoundingClientRect();V.x=(t.clientX-a.left)/(a.width||1)*c,V.y=(t.clientY-a.top)/(a.height||1)*l,V.set=!0,H=!0}b&&window.addEventListener("pointermove",Kt,{passive:!0});function de(t){bt(),Et(),E&&!u?vt(It()-1,m):(et(),yt(),S.on=!1),H=!0}function ue(){K||(Bt(),Gt(),J=Math.max(3.4,l*.0095),Xt(),Vt(),de(),Ft=!0,zt||(zt=!0,Tt=requestAnimationFrame(Ht)))}let nt;function he(t){const a=`${r} 200px ${L}`;let i=!1;try{i=!!document.fonts&&document.fonts.check(a)}catch{i=!0}if(!document.fonts||i){t();return}let f=!1;const h=()=>{f||(f=!0,nt&&clearTimeout(nt),t())};document.fonts.load(a,e).then(()=>document.fonts.ready).then(h,h),nt=setTimeout(h,1500)}he(ue);let Nt;function Yt(){clearTimeout(Nt),Nt=setTimeout(()=>Jt.resize(),200)}window.addEventListener("resize",Yt);const Jt={get text(){return e},setText(t){if(K||!Ft){e=t??"";return}t=t??"";const a=t.length>e.length&&e.length>0&&t.startsWith(e),i=a?Ut():-1;e=t,bt(),Et(),u||!E?(et(),yt(),S.on=!1):vt(a&&i>0?Math.min(i,Ut()):It()-1,a?320:m),H=!0},replay(){K||!Ft||(u||!E?(et(),yt(),S.on=!1):vt(It()-1,m),H=!0)},resize(){K||(Bt(),Gt(),J=Math.max(3.4,l*.0095),Xt(),Vt(),bt(),Et(),et(),yt(),S.on=!1,H=!0)},destroy(){if(!K){K=!0,cancelAnimationFrame(Tt),clearTimeout(Nt),nt&&clearTimeout(nt),window.removeEventListener("resize",Yt),b&&window.removeEventListener("pointermove",Kt);for(const t of Mt)t.remove();Q=[],q=[],S.rows=[],St=new Uint8Array(0),At=new Float32Array(0),ft=new Float32Array(0),dt=new Float32Array(0),k=G=U=tt=null,ut=null}}};return Jt}const oe="data-threadtext",pt=new WeakMap;function re(s,n){const e=s.dataset,o={text:e.ttText??n};if(e.ttFont&&(o.font=e.ttFont),e.ttWeight!==void 0){const r=parseFloat(e.ttWeight);isNaN(r)||(o.weight=r)}if(e.ttThreadColor&&(o.threadColor=e.ttThreadColor),e.ttFabricColor&&(o.fabricColor=e.ttFabricColor),e.ttPitch!==void 0){const r=parseFloat(e.ttPitch);isNaN(r)||(o.pitch=r)}if(e.ttSewRate!==void 0){const r=parseFloat(e.ttSewRate);isNaN(r)||(o.sewRate=r)}if(e.ttAspect!==void 0){const r=parseFloat(e.ttAspect);isNaN(r)||(o.aspect=r)}return e.ttSheen==="false"&&(o.sheen=!1),e.ttAnimate==="false"&&(o.animate=!1),o}function ae(s){jt(s);const n=s.innerHTML,e=(s.dataset.ttText??s.textContent??"").trim();if(!e)return;const o=re(s,e);s.setAttribute("role","img"),s.setAttribute("aria-label",e),s.innerHTML="";const r=ne(s,o);pt.set(s,{instance:r,originalHTML:n})}function jt(s){const n=pt.get(s);n&&(n.instance.destroy(),s.removeAttribute("role"),s.removeAttribute("aria-label"),s.innerHTML=n.originalHTML,pt.delete(s))}function ct(s=document){s.querySelectorAll(`[${oe}]`).forEach(ae)}function ie(){const s=()=>{var n;typeof document<"u"&&((n=document.fonts)!=null&&n.ready)?document.fonts.ready.then(()=>ct()).catch(()=>ct()):ct()};typeof document<"u"&&document.readyState==="loading"?document.addEventListener("DOMContentLoaded",s,{once:!0}):s()}return ie(),at.destroy=jt,at.init=ct,Object.defineProperty(at,Symbol.toStringTag,{value:"Module"}),at})({});
package/package.json ADDED
@@ -0,0 +1,85 @@
1
+ {
2
+ "name": "@liiift-studio/threadtext",
3
+ "version": "0.0.1",
4
+ "description": "Render any text as photorealistic procedural satin-stitch embroidery, in real time in the browser, from the font's glyph geometry",
5
+ "type": "module",
6
+ "main": "dist/index.cjs",
7
+ "module": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "scripts": {
20
+ "build": "vite build",
21
+ "build:webflow": "vite build --config vite.webflow.config.ts",
22
+ "test": "vitest",
23
+ "test:run": "vitest run",
24
+ "lint": "tsc --noEmit",
25
+ "demo": "python3 -m http.server 8080",
26
+ "prepublishOnly": "npm run test:run && npm run build && npm run build:webflow"
27
+ },
28
+ "peerDependencies": {
29
+ "react": ">=17",
30
+ "react-dom": ">=17"
31
+ },
32
+ "peerDependenciesMeta": {
33
+ "react": {
34
+ "optional": true
35
+ },
36
+ "react-dom": {
37
+ "optional": true
38
+ }
39
+ },
40
+ "devDependencies": {
41
+ "@testing-library/react": "^16.3.2",
42
+ "@types/react": "^19.0.0",
43
+ "@vitejs/plugin-react": "^4.0.0",
44
+ "happy-dom": "^20.10.6",
45
+ "react": "^19.0.0",
46
+ "react-dom": "^19.0.0",
47
+ "typescript": "^5.0.0",
48
+ "vite": "^6.0.0",
49
+ "vite-plugin-dts": "^4.0.0",
50
+ "vitest": "^3.0.0"
51
+ },
52
+ "keywords": [
53
+ "typography",
54
+ "web-typography",
55
+ "embroidery",
56
+ "satin-stitch",
57
+ "cross-stitch",
58
+ "thread",
59
+ "thread-text",
60
+ "procedural",
61
+ "canvas",
62
+ "real-time",
63
+ "variable-font",
64
+ "text-effect",
65
+ "liiift-studio",
66
+ "type-tools",
67
+ "frontend",
68
+ "react",
69
+ "typescript"
70
+ ],
71
+ "author": "Quinn Keaveney <quinn@liiift.studio>",
72
+ "license": "MIT",
73
+ "homepage": "https://threadtext.com",
74
+ "repository": {
75
+ "type": "git",
76
+ "url": "https://github.com/Liiift-Studio/ThreadText.git"
77
+ },
78
+ "bugs": {
79
+ "url": "https://github.com/Liiift-Studio/ThreadText/issues"
80
+ },
81
+ "sideEffects": false,
82
+ "publishConfig": {
83
+ "access": "public"
84
+ }
85
+ }