@overpunch/threadtext 0.4.6
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/LICENSE +21 -0
- package/README.md +208 -0
- package/dist/index.cjs +7 -0
- package/dist/index.d.ts +179 -0
- package/dist/index.js +747 -0
- package/dist/threadtext.webflow.min.js +7 -0
- package/package.json +88 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Liiift Studio
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
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
|
+
[](https://www.npmjs.com/package/@overpunch/threadtext)
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+

|
|
8
|
+

|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
Homepage: **[threadtext.com](https://threadtext.com)** (live, interactive demo) · npm: [`@overpunch/threadtext`](https://www.npmjs.com/package/@overpunch/threadtext)
|
|
12
|
+
|
|
13
|
+
Framework-agnostic core (`createThreadText`), a React hook + component (`useThreadText` / `<ThreadText>`), and Framer / Webflow ports. Zero required dependencies; `react`/`react-dom` are optional peers.
|
|
14
|
+
|
|
15
|
+
<p align="center">
|
|
16
|
+
<img src="https://raw.githubusercontent.com/over-punch/ThreadText/main/assets/hero.png?v=1" alt="The word “Thread” rendered as raised gold satin-stitch embroidery, the floss running across each stroke" width="820" />
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
> **See it move.** It sews itself in stitch-by-stitch and reacts to the cursor, so a still can't fully show it — the [**live demo at threadtext.com**](https://threadtext.com) is the fastest way to judge it.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## What it is
|
|
24
|
+
|
|
25
|
+
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.
|
|
26
|
+
|
|
27
|
+
It's built for **single words / short display type** — a hero word, a logotype, a stitched headline — not body copy.
|
|
28
|
+
|
|
29
|
+
## How it works
|
|
30
|
+
|
|
31
|
+
Each step below is what actually produces the thread look — the plain-language idea first, the precise technique in parentheses:
|
|
32
|
+
|
|
33
|
+
1. **Draw the glyphs** to an offscreen canvas from the loaded font (rasterise; variable-font axes applied via canvas `fontVariationSettings`).
|
|
34
|
+
2. **Measure how deep inside each stroke every pixel is** (an exact Euclidean *signed-distance field*).
|
|
35
|
+
3. **Work out which way the thread should run** — always *across* the stroke, fanning smoothly around curves (a *flow field*, smoothed in double-angle orientation space so the two opposite edges of a stroke agree instead of cancelling).
|
|
36
|
+
4. **Lay the stitches** by stamping many small, pre-lit thread images along that flow (a reused set of *pre-shaded thread sprites* — ~20 brightness variants placed as thousands of individual stitches).
|
|
37
|
+
5. **Lift it into 3D** so each thread catches light and casts a soft crease (a *dome-shade / normal map* over the transparent ground).
|
|
38
|
+
6. **Sew it in** one cross-row at a time as an animation, and add a **subtle cursor-driven sheen** as you move over it.
|
|
39
|
+
|
|
40
|
+
The heavy geometry pass (steps 2–5) runs in a **Web Worker** so typing and live edits stay smooth; where a Worker isn't available it falls back to running synchronously on the main thread (see [Requirements](#requirements--browser-support)).
|
|
41
|
+
|
|
42
|
+
<p align="center">
|
|
43
|
+
<img src="https://raw.githubusercontent.com/over-punch/ThreadText/main/assets/sew-in.gif?v=1" alt="Animation of the word “Sew” being stitched in one satin row at a time" width="480" />
|
|
44
|
+
<br />
|
|
45
|
+
<em>The sew-in animation (satin, machine style).</em>
|
|
46
|
+
</p>
|
|
47
|
+
|
|
48
|
+
## Use it
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { createThreadText } from '@overpunch/threadtext'
|
|
52
|
+
|
|
53
|
+
// Declare the face (any @font-face / next/font / CSS Font Loading API) — the core
|
|
54
|
+
// waits for it to load before drawing, so you don't have to await it yourself.
|
|
55
|
+
const host = document.getElementById('host')
|
|
56
|
+
if (host) {
|
|
57
|
+
const thread = createThreadText(host, {
|
|
58
|
+
text: 'Thread',
|
|
59
|
+
font: '"Your Font", Georgia, serif', // glyph geometry drives the stitch flow
|
|
60
|
+
weight: 680,
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
thread.setText('Threads') // re-fit to width and redraw instantly
|
|
64
|
+
thread.update({ threadColor: '#e6c200', fill: 0.8 }) // live changes — instant, no re-sew
|
|
65
|
+
thread.replay() // re-run the sew-in animation
|
|
66
|
+
thread.resize() // re-fit to the container
|
|
67
|
+
thread.destroy() // cancel rAF, remove listeners, free canvases
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The host element needs a resolved width — the word is fitted to `host.getBoundingClientRect().width`. `createThreadText` is **SSR-safe**: called without a DOM it returns an inert no-op handle, so it won't crash a server render.
|
|
72
|
+
|
|
73
|
+
### React
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
import { ThreadText } from '@overpunch/threadtext'
|
|
77
|
+
|
|
78
|
+
<ThreadText text="Thread" font='"Your Font", serif' weight={680} />
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**Next.js / App Router:** the React entry uses `useRef`/`useLayoutEffect`, so it's a Client Component — put `'use client'` at the top of the file that imports `<ThreadText>` (or your own wrapper). The framework-agnostic core is SSR-safe on its own; only the React bindings need the client boundary.
|
|
82
|
+
|
|
83
|
+
For a custom container or your own imperative control, use the hook — it creates the instance, applies option changes live, and tears everything down (ResizeObserver + `destroy()`) on unmount:
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
import { useThreadText } from '@overpunch/threadtext'
|
|
87
|
+
|
|
88
|
+
function Stitched() {
|
|
89
|
+
const ref = useThreadText<HTMLDivElement>({ text: 'Thread', weight: 680, threadColor: '#e6c200' })
|
|
90
|
+
return <div ref={ref} style={{ width: '100%' }} />
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Framer & Webflow
|
|
95
|
+
|
|
96
|
+
**Framer** — add [`src/framer/ThreadText.tsx`](./src/framer/ThreadText.tsx) as a code component (Insert → Code → New Component, or host it and add by URL). It imports the core straight from `esm.sh`, so there's no build step, and every option is exposed as a Framer property control.
|
|
97
|
+
|
|
98
|
+
**Webflow** — drop the self-contained embed bundle (no React, no module loader) into an Embed element and mark any element with `data-threadtext`:
|
|
99
|
+
|
|
100
|
+
```html
|
|
101
|
+
<script src="https://unpkg.com/@overpunch/threadtext/dist/threadtext.webflow.min.js"></script>
|
|
102
|
+
|
|
103
|
+
<div data-threadtext
|
|
104
|
+
data-tt-text="Thread"
|
|
105
|
+
data-tt-font='"Your Font", serif'
|
|
106
|
+
data-tt-weight="680"
|
|
107
|
+
data-tt-thread-color="#e6c200"
|
|
108
|
+
data-tt-stitch-mode="satin"
|
|
109
|
+
data-tt-sew-style="machine"></div>
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The bundle auto-initialises every `[data-threadtext]` element and exposes a small `window.ThreadText` API for manual control. Supported attributes: `data-tt-text`, `data-tt-font`, `data-tt-weight`, `data-tt-thread-color`, `data-tt-thread-color2`, `data-tt-color-mode`, `data-tt-backstitch`, `data-tt-outline-color`, `data-tt-fill`, `data-tt-align`, `data-tt-pitch`, `data-tt-stitch-mode`, `data-tt-sew-style`, `data-tt-sew-rate`, `data-tt-sheen`, `data-tt-animate`, `data-tt-editable`, `data-tt-axes` (JSON).
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Options
|
|
117
|
+
|
|
118
|
+
All fields on `ThreadTextOptions`. Most can be changed live with `instance.update(...)` — colour, font, weight, size (`fill`), pitch, stitch mode, sew style/rate, sheen, axes and editability all redraw instantly without re-running the sew-in. Two exceptions: **`text`** changes go through `instance.setText(...)`, and **`reducedMotion`** is fixed at construction (changing it recreates the instance).
|
|
119
|
+
|
|
120
|
+
- **`text`** — the word to embroider.
|
|
121
|
+
- **`font`** — CSS `font-family` of an already-loaded font; its glyph geometry drives the stitch flow.
|
|
122
|
+
- **`weight`** — numeric font weight (drives the `wght` axis via the standard font shorthand).
|
|
123
|
+
- **`axes`** — variable-font axes, e.g. `{ opsz: 40, SOFT: 60 }`, applied via canvas `fontVariationSettings` (Chrome/Edge/Safari); ignored where unsupported.
|
|
124
|
+
- **`threadColor`** — floss colour.
|
|
125
|
+
- **`colorMode`** — `'solid'` (default) · `'twotone'` (two colours as alternating threads packed side by side) · `'gradient'` (a smooth colour transition across the word). `'twotone'`/`'gradient'` use **`threadColor2`** as the second colour.
|
|
126
|
+
- **`backstitch`** — add a darker running-stitch **outline** traced around each glyph (sews in last); **`outlineColor`** sets its colour (defaults to a darkened `threadColor`).
|
|
127
|
+
- **`fill`** — size: the fraction of the container width the word spans (it re-fits on load/resize).
|
|
128
|
+
- **`align`** — horizontal placement of the word in the canvas when `fill` leaves spare width: `'center'` (default) · `'left'` · `'right'`.
|
|
129
|
+
- **`pitch`** — thread spacing (how tightly the stitches are packed across each stroke).
|
|
130
|
+
- **`stitchMode`** — `'satin'` · `'cross'` · `'chain'` · `'running'` textures (below).
|
|
131
|
+
- **`sewStyle`** — `'machine'` (satin rows in parallel) or `'hand'` (one letter at a time, widest threads near the top first, thin edges last).
|
|
132
|
+
- **`sewRate`** · **`animate`** — sew-in speed and whether it plays.
|
|
133
|
+
- **`reducedMotion`** — force-skip the sew-in animation and draw the finished piece in one frame (also honoured automatically when the OS `prefers-reduced-motion` is set).
|
|
134
|
+
- **`sheen`** — cursor-following highlight.
|
|
135
|
+
- **`editable`** + **`onTextChange`** — make the surface typeable (backed by a real `<input>`, so touch keyboards and IME work). Call `instance.focus()` to focus it programmatically.
|
|
136
|
+
|
|
137
|
+
<p align="center">
|
|
138
|
+
<img src="https://raw.githubusercontent.com/over-punch/ThreadText/main/assets/stitch-modes.png?v=1" alt="The word “Sew” in the four stitch modes: satin, cross, chain, and running" width="820" />
|
|
139
|
+
<br />
|
|
140
|
+
<em>The four <code>stitchMode</code> textures.</em>
|
|
141
|
+
</p>
|
|
142
|
+
|
|
143
|
+
<p align="center">
|
|
144
|
+
<img src="https://raw.githubusercontent.com/over-punch/ThreadText/main/assets/thread-styles.png?v=1" alt="The word “Sew” in four floss styles: solid, two-tone, gradient, and with a backstitch outline" width="820" />
|
|
145
|
+
<br />
|
|
146
|
+
<em>Floss styles: <code>colorMode</code> (solid / two-tone / gradient) and the <code>backstitch</code> outline.</em>
|
|
147
|
+
</p>
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Requirements & browser support
|
|
152
|
+
|
|
153
|
+
- **Canvas 2D** — required. The whole render is drawn to stacked `<canvas>` elements.
|
|
154
|
+
- **Web Worker + `Blob` + `URL.createObjectURL`** — used to run the geometry pass off the main thread. **Under a strict Content-Security-Policy the Worker is built from a `blob:` URL**, so you need `worker-src blob:` (or `script-src blob:`). If Worker creation is blocked or unavailable, threadText **degrades gracefully** and runs the same pass synchronously on the main thread — correct output, but heavy edits can jank on very large words.
|
|
155
|
+
- **`fontVariationSettings` on canvas** (Chrome/Edge/Safari) — needed only for the `axes` option; feature-detected and skipped where unsupported.
|
|
156
|
+
- **`prefers-reduced-motion`** — honoured automatically (skips the sew-in animation).
|
|
157
|
+
- **Size** — ~10 kB gzipped (ESM), ~9 kB for the standalone Webflow bundle. **Zero runtime dependencies** (`react`/`react-dom` are optional peers), tree-shakeable (`sideEffects: false`).
|
|
158
|
+
- **Stability** — pre-1.0 (`0.x`); the option and instance API above is what's shipped, but pin the version if you depend on exact visual output.
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Development
|
|
163
|
+
|
|
164
|
+
This repo is a git submodule of the [type-tools](https://github.com/over-punch/type-tools) monorepo. To work on the package itself:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
git clone https://github.com/over-punch/ThreadText.git
|
|
168
|
+
cd ThreadText
|
|
169
|
+
npm install
|
|
170
|
+
npm test # vitest (happy-dom)
|
|
171
|
+
npm run build # ESM + CJS + .d.ts (vite)
|
|
172
|
+
npm run build:webflow # standalone window.ThreadText IIFE bundle
|
|
173
|
+
npm run lint # tsc --noEmit
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Source layout:
|
|
177
|
+
|
|
178
|
+
| Path | What it is |
|
|
179
|
+
|---|---|
|
|
180
|
+
| `src/index.ts` | Public exports (`createThreadText`, `useThreadText`, `ThreadText`, `THREAD_TEXT_CLASSES`) |
|
|
181
|
+
| `src/core/threadText.ts` | Framework-agnostic renderer (`createThreadText`) — no React imports |
|
|
182
|
+
| `src/core/types.ts` | `ThreadTextOptions`, `ThreadTextInstance`, `THREAD_TEXT_CLASSES` |
|
|
183
|
+
| `src/react/` | `useThreadText` hook + `<ThreadText>` component |
|
|
184
|
+
| `src/framer/ThreadText.tsx` | Framer code component (imports core from esm.sh) |
|
|
185
|
+
| `src/webflow/embed.ts` | Webflow auto-init embed → `dist/threadtext.webflow.min.js` |
|
|
186
|
+
| `src/__tests__/` | vitest suites (core, worker assembly, webflow embed) |
|
|
187
|
+
|
|
188
|
+
The landing site + interactive demo live in `site/` (Next.js). Issues and PRs: [github.com/over-punch/ThreadText/issues](https://github.com/over-punch/ThreadText/issues).
|
|
189
|
+
|
|
190
|
+
> **Contributing note:** this repo is a submodule of the [type-tools](https://github.com/over-punch/type-tools) monorepo, and the files under `site/` config, `vercel.json`, and `.gitignore` are **auto-synced from the parent** (`type-tools/shared/`) — edits to those here are overwritten on the next sync. The package source (`src/`) and the per-tool site files (`site/src/app/page.tsx`, `Demo.tsx`, `layout.tsx`, `globals.css`) are safe to edit directly.
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## The type-tools family
|
|
195
|
+
|
|
196
|
+
threadText is one of [**type-tools**](https://github.com/over-punch/type-tools) — a suite of small, focused typographic libraries by Liiift Studio, each doing one thing that's hard or impossible in CSS alone. A few siblings:
|
|
197
|
+
|
|
198
|
+
- [**ragtooth**](https://ragtooth.com) — deliberate sawtooth ragged-edge line breaking.
|
|
199
|
+
- [**floodText**](https://floodtext.com) — per-character variable-font wave animation.
|
|
200
|
+
- [**magnetType**](https://magnettype.com) — cursor-field per-character axis variation.
|
|
201
|
+
- [**opszStepper**](https://opszstepper.com) — optical-size font hot-swap by font-size.
|
|
202
|
+
- [**hoverBoldly**](https://hoverboldly.com) — bold-on-hover with no layout shift.
|
|
203
|
+
|
|
204
|
+
See the [full family](https://github.com/over-punch/type-tools) for all of them.
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
Part of [type-tools](https://github.com/over-punch/type-tools) by [Liiift Studio](https://liiift.studio). MIT.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const ie=require("react"),Zt=require("react/jsx-runtime"),Ye={bg:"tt-bg",fx:"tt-fx",caret:"tt-caret"};function ae(M,n){let r=Math.imul(M|0,374761393)+Math.imul(n|0,668265263)|0;return r=Math.imul(r^r>>>13,1274126177),((r^r>>>16)>>>0)/4294967295}function ct(M,n){const r=Math.floor(M),u=Math.floor(n),f=M-r,C=n-u,h=f*f*(3-2*f),y=C*C*(3-2*C),S=ae(r,u),I=ae(r+1,u),b=ae(r,u+1),p=ae(r+1,u+1);return S+(I-S)*h+(b-S)*y+(S-I-b+p)*h*y}function lt(M,n,r,u,f){let h=0;u[0]=0,f[0]=-1e20,f[1]=1e20;for(let y=1;y<n;y++){let S=(M[y]+y*y-(M[u[h]]+u[h]*u[h]))/(2*y-2*u[h]);for(;S<=f[h];)h--,S=(M[y]+y*y-(M[u[h]]+u[h]*u[h]))/(2*y-2*u[h]);h++,u[h]=y,f[h]=S,f[h+1]=1e20}h=0;for(let y=0;y<n;y++){for(;f[h+1]<y;)h++;r[y]=(y-u[h])*(y-u[h])+M[u[h]]}}function Lt(M,n,r){const f=new Float64Array(n*r);for(let p=0;p<n*r;p++)f[p]=M[p]?1e20:0;const C=Math.max(n,r),h=new Float64Array(C),y=new Float64Array(C),S=new Int32Array(C),I=new Float64Array(C+1);for(let p=0;p<n;p++){for(let w=0;w<r;w++)h[w]=f[w*n+p];lt(h,r,y,S,I);for(let w=0;w<r;w++)f[w*n+p]=y[w]}for(let p=0;p<r;p++){for(let w=0;w<n;w++)h[w]=f[p*n+w];lt(h,n,y,S,I);for(let w=0;w<n;w++)f[p*n+w]=y[w]}const b=new Float32Array(n*r);for(let p=0;p<n*r;p++)b[p]=Math.sqrt(f[p]);return b}function ft(M,n,r,u,f){let C=Float32Array.from(M);const h=new Float32Array(n*r),y=1/(2*u+1),S=(I,b)=>I<0?0:I>b?b:I;for(let I=0;I<f;I++){for(let b=0;b<r;b++){const p=b*n;let w=0;for(let O=-u;O<=u;O++)w+=C[p+S(O,n-1)];for(let O=0;O<n;O++)h[p+O]=w*y,w+=C[p+S(O+u+1,n-1)]-C[p+S(O-u,n-1)]}for(let b=0;b<n;b++){let p=0;for(let w=-u;w<=u;w++)p+=h[S(w,r-1)*n+b];for(let w=0;w<r;w++)C[w*n+b]=p*y,p+=h[S(w+u+1,r-1)*n+b]-h[S(w-u,r-1)*n+b]}}return C}function at(M,n){if(!M)return n;const r=M.trim();if(r[0]==="#"){let f=r.slice(1);if(f.length===3&&(f=f[0]+f[0]+f[1]+f[1]+f[2]+f[2]),f.length>=6){const C=parseInt(f.slice(0,2),16),h=parseInt(f.slice(2,4),16),y=parseInt(f.slice(4,6),16);if(!Number.isNaN(C)&&!Number.isNaN(h)&&!Number.isNaN(y))return[C,h,y]}return n}const u=r.match(/rgba?\(([^)]+)\)/i);if(u){const f=u[1].split(",").map(C=>parseFloat(C));if(f.length>=3&&f.slice(0,3).every(C=>!Number.isNaN(C)))return[Math.round(f[0]),Math.round(f[1]),Math.round(f[2])]}return n}function re(M){return M<0?0:M>255?255:Math.round(M)}function Ve(M,n,r){return M<n?n:M>r?r:M}function kt(M){return(M.split(",")[0]||"serif").trim().replace(/^["']|["']$/g,"")}function st(M,n,r){const u=new Uint8Array(n*r);for(let v=0;v<r;v++)for(let F=0;F<n;F++){const E=v*n+F,B=M[E*4+3]/255,U=ct(F*.5,v*.5)*.22+ct(F*1.8,v*1.8)*.1;u[E]=B>.5+(U-.16)?1:0}const f=Lt(u,n,r),C=new Float32Array(n*r);for(let v=1;v<r-1;v++)for(let F=1;F<n-1;F++){const E=v*n+F;C[E]=(f[E]*4+f[E-1]+f[E+1]+f[E-n]+f[E+n])/8}const h=new Float32Array(n*r),y=new Float32Array(n*r),S=new Float32Array(n*r),I=new Float32Array(n*r);for(let v=1;v<r-1;v++)for(let F=1;F<n-1;F++){const E=v*n+F;if(!u[E])continue;const B=(C[E+1]-C[E-1])*.5,U=(C[E+n]-C[E-n])*.5;S[E]=B,I[E]=U,h[E]=B*B-U*U,y[E]=2*B*U}const b=Math.max(7,Math.round(r*.05)),p=ft(h,n,r,b,2),w=ft(y,n,r,b,2),O=new Float32Array(n*r),Q=new Float32Array(n*r);for(let v=0;v<n*r;v++){if(!u[v])continue;const F=.5*Math.atan2(w[v],p[v]);O[v]=Math.cos(F),Q[v]=Math.sin(F)}const se=Math.max(9,r*.04),G=new Float32Array(n*r),ce=-.45,le=-.62,ee=.64;for(let v=1;v<r-1;v++)for(let F=1;F<n-1;F++){const E=v*n+F;if(!u[E])continue;let B=S[E],U=I[E];const K=Math.hypot(B,U)||1;B/=K,U/=K;const fe=C[E],D=(1-Math.min(fe/se,1))*1.25;let ue=-B*D,te=-U*D;const Z=1.5,V=Math.hypot(ue,te,Z);ue/=V,te/=V;let xe=ue*ce+te*le+Z/V*ee;xe<0&&(xe=0);const $e=Math.min(1,.45+.55*Math.min(fe/(se*.7),1));G[E]=Math.min(1.12,(.5+.75*xe)*$e)}return{mask:u,ex:O,ey:Q,shade:G}}function Ft(M,n){if(typeof window>"u"||typeof document>"u"||!M)return{setText(){},replay(){},resize(){},update(){},focus(){},destroy(){},get text(){return n.text??""}};let r=n.text??"",u=n.font??"Georgia, serif",f=Ve(n.weight??680,1,1e3),C=Ve(n.fill??.9,.05,1),h=n.align==="left"||n.align==="right"?n.align:"center",y=Math.max(1,n.sewRate??110),S=n.sewStyle==="hand"?"hand":"machine";const I=["satin","cross","chain","running"];let b=I.includes(n.stitchMode??"")?n.stitchMode:"satin",p=n.sheen??!0,w=n.animate??!0,O=n.editable??!1,Q=n.pitch;const se=["solid","twotone","gradient"];let G=se.includes(n.colorMode??"")?n.colorMode:"solid",ce=n.threadColor??"#fffbf3",le=n.threadColor2??ce,ee=n.backstitch??!1,v=n.outlineColor,F=n.axes,E=n.onTextChange,B=kt(u);function U(){if(!F)return"";const e=[];for(const t in F){const o=F[t];typeof o=="number"&&isFinite(o)&&e.push(`"${t}" ${o}`)}return e.join(", ")}function K(e){"fontVariationSettings"in e&&(e.fontVariationSettings=U()||"normal")}const fe=n.reducedMotion??!!(window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches),D=e=>({peak:e,mid:[e[0]*.585,e[1]*.585,e[2]*.585],end:[e[0]*.47,e[1]*.47,e[2]*.47]}),ue=(e,t,o)=>[e[0]+(t[0]-e[0])*o,e[1]+(t[1]-e[1])*o,e[2]+(t[2]-e[2])*o],te=6;let Z=[255,251,243],V=[],xe=D([0,0,0]);function $e(){const e=at(ce,[255,251,243]),t=at(le,e);if(G==="twotone")V=[D(e),D(t)];else if(G==="gradient"){V=[];for(let o=0;o<te;o++)V.push(D(ue(e,t,o/(te-1))))}else V=[D(e)];Z=V[0].peak,xe=D(v?at(v,[0,0,0]):[e[0]*.32,e[1]*.32,e[2]*.32])}$e();const Ae=[];let q,N;M instanceof HTMLCanvasElement?(q=M,N=M.parentElement??M):(N=M,q=document.createElement("canvas"),Ae.push(q),N.appendChild(q)),q.classList.add(Ye.bg);const pe=document.createElement("canvas");Ae.push(pe),pe.classList.add(Ye.fx),N.appendChild(pe),getComputedStyle(N).position==="static"&&(N.style.position="relative"),q.style.display="block",q.style.width="100%",Object.assign(pe.style,{position:"absolute",inset:"0",width:"100%",pointerEvents:"none",mixBlendMode:"screen"});let k=null,j=null,Pe=!1,A=0,R=0,Me=0,Je=0,H=4,ke=[],be=0,Te=0,Qe=[],ut=0,dt=-1,ht="",mt=-1,gt=-1,Y=new Uint8Array(0),Ne=new Float32Array(0),Ze=new Float32Array(0),He=new Float32Array(0),de=null,Se=null,je=null,Le=[],he=[],Fe=[],we=3;const $={rows:[],idx:0,acc:0,rate:0,on:!1},ne={x:0,y:0,set:!1};let Ce=!0,We=0,Re=!1,Ie=0,z=!1,me=!1,W=null,Oe="",Be=!1,et=0,tt=!1,J;function Ot(){const e=N.getBoundingClientRect(),t=Math.max(1,Math.round(e.width||Math.min(window.innerWidth*.95,1240))),o=r||"Ag",i=q.getContext("2d");i&&(i.textBaseline="middle");let s=100;i&&(i.font=`${f} 100px ${u}`,K(i),s=i.measureText(o).width||100);const a=Math.max(8,100*(t*C)/s);let c=a*.62,T=a*.62,l=t*C;if(i){i.font=`${f} ${a}px ${u}`,K(i);const x=i.measureText(o);l=x.width||l,x.actualBoundingBoxAscent>0&&(c=x.actualBoundingBoxAscent),x.actualBoundingBoxDescent>0&&(T=x.actualBoundingBoxDescent)}const m=Math.max(1,Math.round(2*Math.max(c,T)*1.1));let g=Math.min(window.devicePixelRatio||1,1.7);for(;t*g*m*g>9e5&&g>.4;)g-=.1;A=Math.max(1,Math.round(t*g)),R=Math.max(1,Math.round(m*g)),Me=Math.max(4,Math.round(a*g));const d=l*g,L=Math.max(2,Math.round(Me*.08));Je=Math.round(h==="left"?L:h==="right"?A-d-L:A/2-d/2);for(const x of[q,pe])x.width=A,x.height=R,x.style.height=m+"px";N.style.height=m+"px",H=Q??Math.max(3,R*.0095)}function $t(){Se&&mt===A&>===R||(Se=document.createElement("canvas"),Se.width=A,Se.height=R,je=Se.getContext("2d",{willReadFrequently:!0}),mt=A,gt=R)}function yt(){return V.map(e=>e.peak.join(",")).join(";")+"|"+b+"|"+G+(ee?"|bs"+xe.peak.join(","):"")}function xt(e,t){const o=Math.max(7,H*2.5),i=Math.max(3.2,H*1.3),s=[];for(let a=0;a<20;a++){const c=.34+a/19*.82,T=document.createElement("canvas");T.width=be,T.height=Te;const l=T.getContext("2d");if(!l){s.push(T);continue}const m=be/2,g=Te/2,d=(x,P)=>`rgba(${re(x[0]*c)},${re(x[1]*c)},${re(x[2]*c)},${P})`,L=(x,P,ge,oe)=>{l.save(),l.translate(m,g),l.rotate(ge);const X=l.createLinearGradient(0,-P/2,0,P/2);X.addColorStop(0,d(e.end,0)),X.addColorStop(.16,d(e.mid,1)),X.addColorStop(.5,d(e.peak,1)),X.addColorStop(.84,d(e.mid,1)),X.addColorStop(1,d(e.end,0)),l.fillStyle=X;const _=P/2;if(l.beginPath(),l.moveTo(-x/2+_,-_),l.lineTo(x/2-_,-_),l.arc(x/2-_,0,_,-Math.PI/2,Math.PI/2),l.lineTo(-x/2+_,_),l.arc(-x/2+_,0,_,Math.PI/2,-Math.PI/2),l.closePath(),l.fill(),oe){l.globalCompositeOperation="destination-in";const ye=l.createLinearGradient(-x/2,0,x/2,0);ye.addColorStop(0,"rgba(0,0,0,0)"),ye.addColorStop(.22,"rgba(0,0,0,1)"),ye.addColorStop(.78,"rgba(0,0,0,1)"),ye.addColorStop(1,"rgba(0,0,0,0)"),l.fillStyle=ye,l.fillRect(-x/2-1,-P/2-1,x+2,P+2),l.globalCompositeOperation="source-over"}l.restore()};if(t==="cross")L(o*.92,i*.82,Math.PI/4,!1),L(o*.92,i*.82,-Math.PI/4,!1);else if(t==="chain"){l.save(),l.translate(m,g);const x=l.createLinearGradient(0,-i,0,i);x.addColorStop(0,d(e.mid,1)),x.addColorStop(.5,d(e.peak,1)),x.addColorStop(1,d(e.mid,1)),l.strokeStyle=x,l.lineWidth=Math.max(1.6,i*.7),l.lineCap="round",l.beginPath(),l.ellipse(0,0,o*.34,i*1.05,0,0,Math.PI*2),l.stroke(),l.restore()}else t==="running"?L(o*.5,i,0,!1):L(o,i,0,!0);s.push(T)}return s}function nt(){const e=Math.max(7,H*2.5),t=Math.max(3.2,H*1.3);b==="cross"||b==="chain"?be=Te=Math.ceil(e)+2:(be=Math.ceil(e)+2,Te=Math.ceil(t)+2),ut=b==="chain"||b==="running"?Math.PI/2:0,ke=[];for(const o of V)for(const i of xt(o,b))ke.push(i);Qe=ee?xt(xe,"running"):[],dt=H,ht=yt()}function Pt(){ke.length&&dt===H&&ht===yt()||nt()}function pt(){if(!je)return null;const e=je;return e.clearRect(0,0,A,R),e.fillStyle="#fff",e.textAlign="left",e.textBaseline="middle",e.font=`${f} ${Me}px ${u}`,K(e),e.fillText(r,Je,R*.5),e.getImageData(0,0,A,R).data}function ot(e){Y=e.mask,Ze=e.ex,He=e.ey,Ne=e.shade,de=document.createElement("canvas"),de.width=A,de.height=R;const t=de.getContext("2d");if(t){const o=t.createImageData(A,R);for(let i=0;i<A*R;i++)Y[i]&&(o.data[i*4]=255,o.data[i*4+1]=255,o.data[i*4+2]=255,o.data[i*4+3]=255);t.putImageData(o,0,0)}}function Mt(){const e=Math.max(1.6,H*.6);we=e;const t=[],o=[];let i=1/0,s=-1/0;for(let a=0;a<R;a+=e)for(let c=0;c<A;c+=e){const T=(ae(Math.floor(c),Math.floor(a))-.5)*e*.9,l=(ae(Math.floor(a),Math.floor(c))-.5)*e*.9,m=Math.round(c+T),g=Math.round(a+l);if(m<1||g<1||m>=A-1||g>=R-1)continue;const d=g*A+m;if(!Y[d])continue;const L=Math.atan2(He[d],Ze[d]),x=.86+ae(m*7,g*13)*.18;let P=Ne[d]*x;P=P<0?0:P>1.12?1.12:P,t.push({x:m,y:g,ang:L,idx:Math.min(19,Math.max(0,Math.round(P/1.12*19)))}),m<i&&(i=m),m>s&&(s=m)}if(G==="twotone")if(b==="satin"){const a=Math.max(4,H*1.6);for(const c of t){const T=-Math.sin(c.ang)*c.x+Math.cos(c.ang)*c.y;c.pal=Math.floor(T/a)&1}}else for(const a of t)a.pal=Math.round(a.x/e)+Math.round(a.y/e)&1;else if(G==="gradient"){const a=Math.max(1,s-i);for(const c of t){const T=(c.x-i)/a*(te-1),l=Math.min(te-1,Math.max(0,Math.floor(T)));c.pal=l,l<te-1&&ae(c.x*3+1,c.y*5+2)<T-l&&o.push({x:c.x,y:c.y,ang:c.ang,idx:c.idx,pal:l+1})}}Le=t,Fe=o,Nt()}function Nt(){if(he=[],!ee||!Y.length)return;const e=Math.max(3,H*1.5),t=Math.max(1,Math.ceil(A/e)),o=new Set,i=(s,a)=>Y[a*A+s]?1:0;for(let s=1;s<R-1;s++)for(let a=1;a<A-1;a++){const c=s*A+a;if(!Y[c]||Y[c-1]&&Y[c+1]&&Y[c-A]&&Y[c+A])continue;const T=Math.floor(s/e)*t+Math.floor(a/e);if(o.has(T))continue;o.add(T);const l=i(a+1,s-1)+2*i(a+1,s)+i(a+1,s+1)-(i(a-1,s-1)+2*i(a-1,s)+i(a-1,s+1)),m=i(a-1,s+1)+2*i(a,s+1)+i(a+1,s+1)-(i(a-1,s-1)+2*i(a,s-1)+i(a+1,s-1)),g=Math.atan2(l,-m);let d=Ne[c];d=d<0?0:d>1.12?1.12:d,he.push({x:a,y:s,ang:g,idx:Math.min(19,Math.max(8,Math.round(d/1.12*19))),outline:1})}}function De(e,t){e.save(),e.translate(t.x,t.y),t.outline?(e.rotate(t.ang),e.drawImage(Qe[t.idx],-be/2,-Te/2)):(e.rotate(t.ang+ut),e.drawImage(ke[(t.pal||0)*20+t.idx],-be/2,-Te/2)),e.restore()}function _e(){const e=q.getContext("2d");if(e){for(const t of Le)De(e,t);for(const t of Fe)De(e,t);for(const t of he)De(e,t)}}function Ue(){const e=q.getContext("2d");e&&e.clearRect(0,0,A,R)}function wt(e){if(!e.length)return[];const t=Math.max(4,we*1.5),o=new Map,i=(m,g)=>m+","+g;e.forEach((m,g)=>{const d=i(Math.floor(m.x/t),Math.floor(m.y/t));let L=o.get(d);L||o.set(d,L=[]),L.push(g)});const s=we*1.8*(we*1.8);function a(m){const g=e[m],d=Math.floor(g.x/t),L=Math.floor(g.y/t),x=[];for(let P=-1;P<=1;P++)for(let ge=-1;ge<=1;ge++){const oe=o.get(i(d+P,L+ge));if(oe)for(const X of oe){if(X===m)continue;const _=e[X].x-g.x,ye=e[X].y-g.y;_*_+ye*ye<=s&&x.push(X)}}return x}const c=new Uint8Array(e.length),T=[],l=e.map((m,g)=>g).sort((m,g)=>e[m].x+e[m].y-(e[g].x+e[g].y));for(const m of l){if(c[m])continue;let g=[m];for(c[m]=1;g.length;){T.push(g.map(L=>e[L]));const d=[];for(const L of g)for(const x of a(L))c[x]||(c[x]=1,d.push(x));g=d}}return T}function jt(e){const t=Math.max(4,we*1.5),o=new Map,i=(l,m)=>l+","+m;e.forEach((l,m)=>{const g=i(Math.floor(l.x/t),Math.floor(l.y/t));let d=o.get(g);d||o.set(g,d=[]),d.push(m)});const s=we*1.8*(we*1.8),a=l=>{const m=e[l],g=Math.floor(m.x/t),d=Math.floor(m.y/t),L=[];for(let x=-1;x<=1;x++)for(let P=-1;P<=1;P++){const ge=o.get(i(g+x,d+P));if(ge)for(const oe of ge){if(oe===l)continue;const X=e[oe].x-m.x,_=e[oe].y-m.y;X*X+_*_<=s&&L.push(oe)}}return L},c=new Uint8Array(e.length),T=[];for(let l=0;l<e.length;l++){if(c[l])continue;const m=[];let g=[l];for(c[l]=1;g.length;){const d=[];for(const L of g){m.push(e[L]);for(const x of a(L))c[x]||(c[x]=1,d.push(x))}g=d}T.push(m)}return T}function Bt(e){if(!e.length)return[];const t=a=>{let c=1/0;for(const T of a)T.x<c&&(c=T.x);return c},o=a=>{let c=0;for(const T of a)c+=T.y;return c/a.length},i=jt(e).sort((a,c)=>t(a)-t(c)),s=[];for(const a of i){const c=wt(a);let T=1;for(const d of c)d.length>T&&(T=d.length);const l=Math.max(2,T*.45),m=c.filter(d=>d.length>=l).sort((d,L)=>o(d)-o(L)),g=c.filter(d=>d.length<l).sort((d,L)=>o(d)-o(L));for(const d of m)s.push(d);for(const d of g)s.push(d)}return s}function Dt(){if(Ue(),fe||!w){_e(),$.on=!1;return}const e=S==="hand"?Bt:wt;let t;if(G==="gradient"&&V.length>1){t=[];const o=new Map;for(const i of Le){const s=i.pal||0,a=o.get(s);a?a.push(i):o.set(s,[i])}for(const i of[...o.keys()].sort((s,a)=>s-a))for(const s of e(o.get(i)))t.push(s)}else t=e(Le);if(Fe.length)for(const o of e(Fe))t.push(o);if(he.length){const i=Math.ceil(he.length/24);for(let s=0;s<he.length;s+=i)t.push(he.slice(s,s+i))}$.rows=t,$.idx=0,$.acc=0,$.rate=y,$.on=$.rows.length>0}function _t(e){const t=q.getContext("2d");if(t)for(e>$.rows.length&&(e=$.rows.length);$.idx<e;){const o=$.rows[$.idx++];for(const i of o)De(t,i)}}function Ut(e,t){const o=pe.getContext("2d");if(!o||(o.clearRect(0,0,A,R),!p||!de))return;const i=A*.32,s=o.createRadialGradient(e,t,0,e,t,i);s.addColorStop(0,"rgba(255,250,238,0.18)"),s.addColorStop(.5,"rgba(255,250,238,0.055)"),s.addColorStop(1,"rgba(255,250,238,0)"),o.fillStyle=s,o.fillRect(0,0,A,R),o.globalCompositeOperation="destination-in",o.drawImage(de,0,0),o.globalCompositeOperation="source-over"}function qt(){j||!O||(j=document.createElement("span"),j.className=Ye.caret,j.setAttribute("aria-hidden","true"),Object.assign(j.style,{position:"absolute",pointerEvents:"none",borderRadius:"1px",opacity:"0",background:`rgb(${re(Z[0])},${re(Z[1])},${re(Z[2])})`}),N.appendChild(j),Ae.push(j))}function qe(){if(!O||!j)return;const e=q.getContext("2d");if(!e)return;e.font=`${f} ${Me}px ${u}`,K(e);const t=e.measureText(r).width,o=(q.clientWidth||A)/A,i=Me*.7;j.style.left=(Je+t+Me*.05)*o+"px",j.style.top=.5*(q.clientHeight||R)-i*o/2+"px",j.style.height=i*o+"px",j.style.width=Math.max(2,Math.round(Me*.02*o))+"px",j.style.background=`rgb(${re(Z[0])},${re(Z[1])},${re(Z[2])})`}function ve(){z||Re||(Re=!0,Ie=0,We=requestAnimationFrame(Ct))}function Ct(e){if(z){Re=!1;return}Ie||(Ie=e);const t=Math.min(.25,(e-Ie)/1e3);Ie=e,$.on&&($.acc+=$.rate*t,_t($.acc),$.idx>=$.rows.length&&($.on=!1)),Ce&&de&&(Ut(ne.set?ne.x:A*.5,ne.set?ne.y:R*.45),Ce=!1);const o=O&&Pe;if(j&&(j.style.opacity=o&&Math.floor(e/530)%2===0?"1":"0"),!$.on&&!Ce&&!o){Re=!1;return}We=requestAnimationFrame(Ct)}function ze(e){const t=N.getBoundingClientRect();ne.x=(e.clientX-t.left)/(t.width||1)*A,ne.y=(e.clientY-t.top)/(t.height||1)*R,ne.set=!0,Ce=!0,ve()}function Ge(){ne.set=!1,Ce=!0,ve()}function zt(){k&&Tt(k.value,!0)}function Gt(e){e.key==="Enter"&&(e.preventDefault(),it.replay())}function Xt(){Pe=!0,ve()}function Kt(){Pe=!1,j&&(j.style.opacity="0")}function vt(e){e===O&&k||(O=e,e?(k||(k=document.createElement("input"),k.type="text",k.value=r,k.maxLength=64,k.setAttribute("aria-label","Embroidered text — type to change it"),k.setAttribute("autocomplete","off"),k.setAttribute("autocapitalize","off"),k.spellcheck=!1,Object.assign(k.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"}),k.addEventListener("input",zt),k.addEventListener("keydown",Gt),k.addEventListener("focus",Xt),k.addEventListener("blur",Kt),N.appendChild(k),Ae.push(k)),qt(),qe()):(k&&(k.remove(),k=null),Pe=!1,j&&(j.style.opacity="0")))}function Vt(e){if(e!==p)if(p=e,e)N.addEventListener("pointermove",ze,{passive:!0}),N.addEventListener("pointerleave",Ge,{passive:!0}),ne.set=!1,Ce=!0,ve();else{N.removeEventListener("pointermove",ze),N.removeEventListener("pointerleave",Ge);const t=pe.getContext("2d");t&&t.clearRect(0,0,A,R)}}function Xe(e){Mt(),e?Dt():(Ue(),_e(),$.on=!1),k&&k.value!==r&&(k.value=r),qe(),Ce=!0,ve()}function Ee(e){Ot(),$t(),Pt();const t=pt();if(!t){Xe(e);return}if(W&&Be){const o=++et;tt=e,J&&clearTimeout(J),J=setTimeout(()=>bt(o),800);try{W.postMessage({id:o,W:A,H:R,alpha:t},[t.buffer])}catch{bt(o)}return}ot(st(t,A,R)),Xe(e)}function bt(e){if(e!==et||z)return;J&&(clearTimeout(J),J=void 0),Be=!1;const t=pt();t&&(ot(st(t,A,R)),Xe(tt))}function Tt(e,t){e=e??"",!(e===r&&me)&&(r=e,me&&(Ee(!1),t&&E&&E(r)))}const Ke=new Set;function St(e,t,o){const i=`${t} 200px ${e}`;let s=!1;try{s=!!document.fonts&&document.fonts.check(i)}catch{s=!0}if(!document.fonts||s){o();return}let a=!1,c;const T=()=>{a||(a=!0,clearTimeout(c),Ke.delete(c),z||o())};document.fonts.load(i,r).then(()=>document.fonts.ready).then(T,T),c=setTimeout(T,1500),Ke.add(c)}function Yt(e){const t=e.data;if(t&&t.type==="pong"){Be=!0;return}!t||z||t.id!==et||(J&&(clearTimeout(J),J=void 0),ot({mask:t.mask,ex:t.ex,ey:t.ey,shade:t.shade}),Xe(tt))}function Jt(){if(!(typeof Worker>"u"||typeof Blob>"u"||typeof URL>"u"||!URL.createObjectURL))try{const e=`${ae}
|
|
2
|
+
${ct}
|
|
3
|
+
${lt}
|
|
4
|
+
${Lt}
|
|
5
|
+
${ft}
|
|
6
|
+
var __cg=${st};
|
|
7
|
+
self.onmessage=function(ev){var d=ev.data;if(d&&d.type==='ping'){try{__cg(new Uint8ClampedArray(4),1,1);self.postMessage({type:'pong'})}catch(e){}return}var g=__cg(d.alpha,d.W,d.H);self.postMessage({id:d.id,mask:g.mask,ex:g.ex,ey:g.ey,shade:g.shade},[g.mask.buffer,g.ex.buffer,g.ey.buffer,g.shade.buffer])};`;Oe=URL.createObjectURL(new Blob([e],{type:"application/javascript"})),W=new Worker(Oe),W.onmessage=Yt,W.onerror=()=>{Be=!1},W.postMessage({type:"ping"})}catch{W=null}}function Qt(){z||(Ee(!0),me=!0)}Jt(),O&&(O=!1,vt(!0)),p&&(N.addEventListener("pointermove",ze,{passive:!0}),N.addEventListener("pointerleave",Ge,{passive:!0})),St(B,f,Qt);let rt,Et=-1;function At(){clearTimeout(rt),rt=setTimeout(()=>{if(z||!me)return;const e=Math.round(N.getBoundingClientRect().width);e!==Et&&(Et=e,Ee(!1))},200)}window.addEventListener("resize",At);const it={get text(){return r},setText(e){if(!z){if(!me){r=e??"",k&&(k.value=r);return}Tt(e,!1)}},replay(){z||!me||Ee(!0)},resize(){z||!me||Ee(!1)},update(e){if(z)return;e.text!==void 0&&it.setText(e.text);let t=!1,o=!1,i=!1,s=!1;if(e.font!==void 0&&e.font!==u&&(u=e.font,B=kt(u),t=!0),e.weight!==void 0){const a=Ve(e.weight,1,1e3);a!==f&&(f=a,t=!0)}if(e.fill!==void 0){const a=Ve(e.fill,.05,1);a!==C&&(C=a,t=!0)}if(e.align!==void 0){const a=e.align==="left"||e.align==="right"?e.align:"center";a!==h&&(h=a,t=!0)}e.pitch!==void 0&&e.pitch!==Q&&(Q=e.pitch,t=!0),e.axes!==void 0&&(F=e.axes,t=!0),e.threadColor!==void 0&&e.threadColor!==ce&&(ce=e.threadColor,s=!0),e.threadColor2!==void 0&&e.threadColor2!==le&&(le=e.threadColor2,s=!0),e.outlineColor!==void 0&&e.outlineColor!==v&&(v=e.outlineColor,s=!0),e.colorMode!==void 0&&e.colorMode!==G&&se.includes(e.colorMode)&&(G=e.colorMode,s=!0,i=!0),e.backstitch!==void 0&&e.backstitch!==ee&&(ee=e.backstitch,i=!0),e.stitchMode!==void 0&&e.stitchMode!==b&&I.includes(e.stitchMode)&&(b=e.stitchMode,o=!0),e.sewRate!==void 0&&(y=Math.max(1,e.sewRate)),e.sewStyle!==void 0&&(S=e.sewStyle==="hand"?"hand":"machine"),e.animate!==void 0&&(w=e.animate),e.onTextChange!==void 0&&(E=e.onTextChange),e.sheen!==void 0&&Vt(e.sheen),e.editable!==void 0&&vt(e.editable),s&&$e(),me&&(t?St(B,f,()=>{z||Ee(!1)}):i?(nt(),Mt(),Ue(),_e(),qe(),ve()):(o||s)&&(nt(),Ue(),_e(),qe(),ve()))},focus(){!z&&O&&(k==null||k.focus())},destroy(){if(!z){z=!0,Re=!1,cancelAnimationFrame(We),clearTimeout(rt),J&&clearTimeout(J);for(const e of Ke)clearTimeout(e);if(Ke.clear(),W&&(W.terminate(),W=null),Oe){try{URL.revokeObjectURL(Oe)}catch{}Oe=""}window.removeEventListener("resize",At),N.removeEventListener("pointermove",ze),N.removeEventListener("pointerleave",Ge);for(const e of Ae)e.remove();ke=[],Qe=[],Le=[],he=[],Fe=[],$.rows=[],Y=new Uint8Array(0),Ne=new Float32Array(0),Ze=new Float32Array(0),He=new Float32Array(0),de=Se=null,je=null,j=null,k=null}}};return it}function Rt(M){const n=ie.useRef(null),r=ie.useRef(null),u=ie.useRef(M);u.current=M;const f=ie.useRef(M.onTextChange);f.current=M.onTextChange;const{text:C,font:h,weight:y,threadColor:S,pitch:I,fill:b,animate:p,sewStyle:w,stitchMode:O,sewRate:Q,sheen:se,editable:G,reducedMotion:ce,axes:le}=M,ee=JSON.stringify(le??null);return ie.useLayoutEffect(()=>{const v=n.current;if(!v)return;const F=Ft(v,{...u.current,onTextChange:fe=>{var D;return(D=f.current)==null?void 0:D.call(f,fe)}});r.current=F;let E=0,B=0,U=!0,K;return typeof ResizeObserver<"u"&&(K=new ResizeObserver(fe=>{const D=Math.round(fe[0].contentRect.width);if(U){U=!1,B=D;return}D!==B&&(B=D,cancelAnimationFrame(E),E=requestAnimationFrame(()=>{var ue;return(ue=r.current)==null?void 0:ue.resize()}))}),K.observe(v)),()=>{K==null||K.disconnect(),cancelAnimationFrame(E),F.destroy(),r.current=null}},[ce]),ie.useEffect(()=>{var v;(v=r.current)==null||v.update({font:h,weight:y,threadColor:S,pitch:I,fill:b,animate:p,sewStyle:w,stitchMode:O,sewRate:Q,sheen:se,editable:G,axes:le})},[h,y,S,I,b,p,w,O,Q,se,G,ee]),ie.useEffect(()=>{var v;(v=r.current)==null||v.setText(C)},[C]),n}const It=ie.forwardRef(function({className:n,style:r,as:u="div","aria-label":f,role:C,"aria-describedby":h,...y},S){const I=Rt(y),b=ie.useCallback(p=>{I.current=p,typeof S=="function"?S(p):S&&(S.current=p)},[I,S]);return Zt.jsx(u,{ref:b,className:n,style:r,role:y.editable?C:C??"img","aria-label":y.editable?f:f??y.text,"aria-describedby":h})});It.displayName="ThreadText";exports.THREAD_TEXT_CLASSES=Ye;exports.ThreadText=It;exports.createThreadText=Ft;exports.useThreadText=Rt;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
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: the baked satin stitches (transparent ground). */
|
|
16
|
+
readonly bg: "tt-bg";
|
|
17
|
+
/** Overlay canvas: cursor sheen (`mix-blend-mode: screen`). */
|
|
18
|
+
readonly fx: "tt-fx";
|
|
19
|
+
/** Blinking text caret shown in editable mode. */
|
|
20
|
+
readonly caret: "tt-caret";
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Drop-in component that embroiders `text` inside a self-sizing container. The canvas
|
|
25
|
+
* output is decorative; the word is exposed to assistive tech via `aria-label`.
|
|
26
|
+
*/
|
|
27
|
+
export declare const ThreadText: default_2.ForwardRefExoticComponent<ThreadTextProps & default_2.RefAttributes<HTMLElement>>;
|
|
28
|
+
|
|
29
|
+
/** Live handle to a mounted threadText renderer. */
|
|
30
|
+
export declare interface ThreadTextInstance {
|
|
31
|
+
/** Re-embroider with new text — re-fits to width and redraws instantly (no sew-in). */
|
|
32
|
+
setText(text: string): void;
|
|
33
|
+
/** Re-run the sew-in animation for the current word (when `animate` is on). */
|
|
34
|
+
replay(): void;
|
|
35
|
+
/** Re-fit the render surface to its container and redraw. */
|
|
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;
|
|
44
|
+
/** Cancel the animation loop, remove listeners, and free the created canvases. */
|
|
45
|
+
destroy(): void;
|
|
46
|
+
/** The current embroidered text. */
|
|
47
|
+
readonly text: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Options controlling the procedural satin-stitch embroidery renderer.
|
|
52
|
+
* Colours accept any CSS-ish hex (`#rgb` / `#rrggbb`) or `rgb()/rgba()` string.
|
|
53
|
+
*/
|
|
54
|
+
export declare interface ThreadTextOptions {
|
|
55
|
+
/** The word (or short phrase) to embroider. */
|
|
56
|
+
text: string;
|
|
57
|
+
/**
|
|
58
|
+
* CSS `font-family` of an already-loaded font used to rasterise the glyphs.
|
|
59
|
+
* The renderer is font-agnostic — load the face however you like (`@font-face`,
|
|
60
|
+
* `next/font`, the CSS Font Loading API) before calling. (default: 'Georgia, serif')
|
|
61
|
+
*/
|
|
62
|
+
font?: string;
|
|
63
|
+
/** Numeric font weight passed to the canvas `font` shorthand (100–900). (default: 680) */
|
|
64
|
+
weight?: number;
|
|
65
|
+
/**
|
|
66
|
+
* Variable-font axis values applied to the rasterised glyphs, e.g. `{ opsz: 40, SOFT: 60 }`.
|
|
67
|
+
* Uses the canvas `fontVariationSettings` API (Chrome/Edge/Safari) so the browser's own
|
|
68
|
+
* variable rendering drives the stitch shapes; silently ignored where unsupported (the font's
|
|
69
|
+
* default instance is used). Note: numeric `weight` already drives the `wght` axis via the
|
|
70
|
+
* standard font shorthand — reach for `axes` for `opsz` and custom axes (`SOFT`, `WONK`, …).
|
|
71
|
+
*/
|
|
72
|
+
axes?: Record<string, number>;
|
|
73
|
+
/** Floss (thread) colour — the lit crest of each thread. (default: warm white '#fffbf3') */
|
|
74
|
+
threadColor?: string;
|
|
75
|
+
/**
|
|
76
|
+
* Second floss colour, used by `colorMode: 'twotone'` and `'gradient'`. Ignored when
|
|
77
|
+
* `colorMode` is `'solid'` (the default). (default: falls back to `threadColor`)
|
|
78
|
+
*/
|
|
79
|
+
threadColor2?: string;
|
|
80
|
+
/**
|
|
81
|
+
* How the floss is coloured:
|
|
82
|
+
* - **'solid'** (default) — one colour (`threadColor`).
|
|
83
|
+
* - **'twotone'** — two colours (`threadColor` + `threadColor2`) laid as alternating threads
|
|
84
|
+
* packed side by side across each stroke.
|
|
85
|
+
* - **'gradient'** — a smooth colour transition from `threadColor` to `threadColor2` across the word.
|
|
86
|
+
*/
|
|
87
|
+
colorMode?: 'solid' | 'twotone' | 'gradient';
|
|
88
|
+
/**
|
|
89
|
+
* Add a darker running-stitch **backstitch outline** traced around each glyph — the way a piece is
|
|
90
|
+
* often finished by hand. Sews in last. (default: false)
|
|
91
|
+
*/
|
|
92
|
+
backstitch?: boolean;
|
|
93
|
+
/** Backstitch outline colour. (default: a darkened shade of `threadColor`) */
|
|
94
|
+
outlineColor?: string;
|
|
95
|
+
/**
|
|
96
|
+
* Thread spacing in internal pixels. Smaller = finer, denser stitching (and more work).
|
|
97
|
+
* (default: auto — derived from the fitted text height)
|
|
98
|
+
*/
|
|
99
|
+
pitch?: number;
|
|
100
|
+
/**
|
|
101
|
+
* Fraction of the container width the word spans — the effective text size. The word is
|
|
102
|
+
* re-fitted to `fill × containerWidth` on load and resize, leaving the remainder as
|
|
103
|
+
* horizontal padding. Range ~0.3–1. (default: 0.9)
|
|
104
|
+
*/
|
|
105
|
+
fill?: number;
|
|
106
|
+
/**
|
|
107
|
+
* Horizontal alignment of the word within the canvas when `fill` leaves spare width.
|
|
108
|
+
* `'center'` (default) · `'left'` · `'right'`.
|
|
109
|
+
*/
|
|
110
|
+
align?: 'left' | 'center' | 'right';
|
|
111
|
+
/** Play the sew-in animation on mount and on `replay()`. (default: true) */
|
|
112
|
+
animate?: boolean;
|
|
113
|
+
/**
|
|
114
|
+
* How the word is sewn in:
|
|
115
|
+
* - **'machine'** (default) — satin cross-rows appear in parallel across each stroke, like
|
|
116
|
+
* machine embroidery (a BFS over the stitch graph).
|
|
117
|
+
* - **'hand'** — works one letter at a time (left to right). Within each letter it enters at
|
|
118
|
+
* the widest region near the top and works its way down the strokes, then cleans up the thin
|
|
119
|
+
* serifs and terminals last — the way a person embroiders a shape.
|
|
120
|
+
*/
|
|
121
|
+
sewStyle?: 'machine' | 'hand';
|
|
122
|
+
/**
|
|
123
|
+
* The stitch texture filling each stroke:
|
|
124
|
+
* - **'satin'** (default) — smooth parallel threads across the stroke (raised satin floss).
|
|
125
|
+
* - **'cross'** — little X's, like cross-stitch.
|
|
126
|
+
* - **'chain'** — a field of looped links.
|
|
127
|
+
* - **'running'** — short dashes, a sparser hand-run look.
|
|
128
|
+
*/
|
|
129
|
+
stitchMode?: 'satin' | 'cross' | 'chain' | 'running';
|
|
130
|
+
/** Satin cross-rows (machine) or stitches (hand) laid per second during the sew-in. (default: 110) */
|
|
131
|
+
sewRate?: number;
|
|
132
|
+
/** Enable the cursor-following radial sheen on the overlay canvas. (default: true) */
|
|
133
|
+
sheen?: boolean;
|
|
134
|
+
/**
|
|
135
|
+
* Make the surface focusable and typeable — click (or Tab) to focus, then type to edit
|
|
136
|
+
* the word, Backspace to delete, Enter to replay the sew-in. Shows a blinking caret.
|
|
137
|
+
* (default: false)
|
|
138
|
+
*/
|
|
139
|
+
editable?: boolean;
|
|
140
|
+
/** Called with the new text whenever the user edits it directly (editable mode only). */
|
|
141
|
+
onTextChange?: (text: string) => void;
|
|
142
|
+
/**
|
|
143
|
+
* Force reduced-motion (skip the sew-in, draw instantly). If omitted, the value is
|
|
144
|
+
* auto-detected from `prefers-reduced-motion`.
|
|
145
|
+
*/
|
|
146
|
+
reducedMotion?: boolean;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
declare interface ThreadTextProps extends ThreadTextOptions {
|
|
150
|
+
className?: string;
|
|
151
|
+
style?: default_2.CSSProperties;
|
|
152
|
+
/** Container element type. (default: 'div') */
|
|
153
|
+
as?: default_2.ElementType;
|
|
154
|
+
/**
|
|
155
|
+
* Accessible label for the embroidery. Defaults to the embroidered `text`, so
|
|
156
|
+
* screen readers announce the word even though it is drawn on a canvas.
|
|
157
|
+
*/
|
|
158
|
+
'aria-label'?: string;
|
|
159
|
+
role?: string;
|
|
160
|
+
'aria-describedby'?: string;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* React hook that mounts a threadText renderer inside a ref'd container.
|
|
165
|
+
*
|
|
166
|
+
* The instance is created once and kept alive; option changes (font, weight, colour,
|
|
167
|
+
* size, sew rate, sheen, editability) are applied live via `instance.update()` — no
|
|
168
|
+
* remount, no re-sew. Text changes route through `setText`. The surface re-fits to the
|
|
169
|
+
* container width via ResizeObserver, and tears down on unmount. Only `reducedMotion`
|
|
170
|
+
* (fixed at creation) forces a fresh instance.
|
|
171
|
+
*
|
|
172
|
+
* @typeParam T - the container element type (default `HTMLElement`); pass e.g.
|
|
173
|
+
* `useThreadText<HTMLDivElement>(...)` so the returned ref attaches cleanly to a `<div>`.
|
|
174
|
+
* @param options - {@link ThreadTextOptions} for the renderer.
|
|
175
|
+
* @returns A ref to attach to the target container element.
|
|
176
|
+
*/
|
|
177
|
+
export declare function useThreadText<T extends HTMLElement = HTMLElement>(options: ThreadTextOptions): RefObject<T | null>;
|
|
178
|
+
|
|
179
|
+
export { }
|