@overpunch/threadtext 0.4.6 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +218 -208
- package/dist/core.cjs +7 -0
- package/dist/core.d.ts +142 -0
- package/dist/core.js +778 -0
- package/dist/index.cjs +1 -7
- package/dist/index.d.ts +12 -16
- package/dist/index.js +74 -736
- package/dist/threadtext.webflow.min.js +7 -7
- package/package.json +93 -88
package/LICENSE
CHANGED
|
@@ -1,21 +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.
|
|
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
CHANGED
|
@@ -1,208 +1,218 @@
|
|
|
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
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
- **`
|
|
131
|
-
- **`
|
|
132
|
-
- **`
|
|
133
|
-
- **`
|
|
134
|
-
- **`
|
|
135
|
-
- **`
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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`, and re-fitted whenever that width changes (window resize or the container alone). A font that finishes loading late is picked up and redrawn. `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
|
+
**Without React:** import from `@overpunch/threadtext/core` — the same renderer with no React import at all.
|
|
74
|
+
|
|
75
|
+
### Accessibility
|
|
76
|
+
|
|
77
|
+
The embroidery is drawn on canvases, which are marked `aria-hidden`. The text itself stays in the element, visually hidden, so screen readers read it, find-in-page finds it and translation tools translate it — and the element keeps its own role, so `<h1>` stays a heading and links inside it stay links (and work). If the element was empty, threadText adds a visually hidden copy of `text`. `destroy()` puts the element's own content back (the same nodes, listeners and all) and removes the inline styles it set. In `editable` mode a real text input carries the value instead.
|
|
78
|
+
|
|
79
|
+
Invalid numbers (`NaN`, `Infinity`, negative pitch or rate) fall back to the defaults, an unusable `font` falls back to `Georgia, serif` with a console warning, and empty text reserves no space.
|
|
80
|
+
|
|
81
|
+
### React
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
import { ThreadText } from '@overpunch/threadtext'
|
|
85
|
+
|
|
86
|
+
<ThreadText text="Thread" font='"Your Font", serif' weight={680} />
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**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.
|
|
90
|
+
|
|
91
|
+
Use `as` for the element type (`<ThreadText as="h1" text="Thread" />` stays a heading). HTML attributes and handlers (`id`, `aria-*`, `data-*`, `lang`, `onClick`…) are passed through to the element.
|
|
92
|
+
|
|
93
|
+
For a custom container or your own imperative control, use the hook — it creates the instance, applies every option change live, and calls `destroy()` on unmount:
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
import { useThreadText } from '@overpunch/threadtext'
|
|
97
|
+
|
|
98
|
+
function Stitched() {
|
|
99
|
+
const ref = useThreadText<HTMLDivElement>({ text: 'Thread', weight: 680, threadColor: '#e6c200' })
|
|
100
|
+
return <div ref={ref} style={{ width: '100%' }} />
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Framer & Webflow
|
|
105
|
+
|
|
106
|
+
**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.
|
|
107
|
+
|
|
108
|
+
**Webflow** — drop the self-contained embed bundle (no React, no module loader) into an Embed element and mark any element with `data-threadtext`:
|
|
109
|
+
|
|
110
|
+
```html
|
|
111
|
+
<script src="https://unpkg.com/@overpunch/threadtext/dist/threadtext.webflow.min.js"></script>
|
|
112
|
+
|
|
113
|
+
<div data-threadtext
|
|
114
|
+
data-tt-text="Thread"
|
|
115
|
+
data-tt-font='"Your Font", serif'
|
|
116
|
+
data-tt-weight="680"
|
|
117
|
+
data-tt-thread-color="#e6c200"
|
|
118
|
+
data-tt-stitch-mode="satin"
|
|
119
|
+
data-tt-sew-style="machine"></div>
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
The bundle auto-initialises every `[data-threadtext]` element — including ones added to the page later (CMS lists, tabs) — and frees the renderer of any that are removed. It exposes `window.ThreadText`: `init(root?)` sets up new elements under (and including) `root`, leaving running ones alone; `restart(el)` rebuilds one after you change its attributes; `destroy(el)` restores it. 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).
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Options
|
|
127
|
+
|
|
128
|
+
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).
|
|
129
|
+
|
|
130
|
+
- **`text`** — the word to embroider.
|
|
131
|
+
- **`font`** — CSS `font-family` of an already-loaded font; its glyph geometry drives the stitch flow.
|
|
132
|
+
- **`weight`** — numeric font weight (drives the `wght` axis via the standard font shorthand).
|
|
133
|
+
- **`axes`** — variable-font axes, e.g. `{ opsz: 40, SOFT: 60 }`, applied via canvas `fontVariationSettings` (Chrome/Edge/Safari); ignored where unsupported.
|
|
134
|
+
- **`threadColor`** — floss colour.
|
|
135
|
+
- **`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.
|
|
136
|
+
- **`backstitch`** — add a darker running-stitch **outline** traced around each glyph (sews in last); **`outlineColor`** sets its colour (defaults to a darkened `threadColor`).
|
|
137
|
+
- **`fill`** — size: the fraction of the container width the word spans (it re-fits on load/resize).
|
|
138
|
+
- **`align`** — horizontal placement of the word in the canvas when `fill` leaves spare width: `'center'` (default) · `'left'` · `'right'`.
|
|
139
|
+
- **`pitch`** — thread spacing (how tightly the stitches are packed across each stroke).
|
|
140
|
+
- **`stitchMode`** — `'satin'` · `'cross'` · `'chain'` · `'running'` textures (below).
|
|
141
|
+
- **`sewStyle`** — `'machine'` (satin rows in parallel) or `'hand'` (one letter at a time, widest threads near the top first, thin edges last).
|
|
142
|
+
- **`sewRate`** · **`animate`** — sew-in speed and whether it plays.
|
|
143
|
+
- **`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).
|
|
144
|
+
- **`sheen`** — cursor-following highlight.
|
|
145
|
+
- **`editable`** + **`onTextChange`** — make the surface typeable (backed by a real `<input>`, so touch keyboards and IME work). Call `instance.focus()` to focus it programmatically.
|
|
146
|
+
|
|
147
|
+
<p align="center">
|
|
148
|
+
<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" />
|
|
149
|
+
<br />
|
|
150
|
+
<em>The four <code>stitchMode</code> textures.</em>
|
|
151
|
+
</p>
|
|
152
|
+
|
|
153
|
+
<p align="center">
|
|
154
|
+
<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" />
|
|
155
|
+
<br />
|
|
156
|
+
<em>Floss styles: <code>colorMode</code> (solid / two-tone / gradient) and the <code>backstitch</code> outline.</em>
|
|
157
|
+
</p>
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Requirements & browser support
|
|
162
|
+
|
|
163
|
+
- **Canvas 2D** — required. The whole render is drawn to stacked `<canvas>` elements.
|
|
164
|
+
- **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.
|
|
165
|
+
- **`fontVariationSettings` on canvas** (Chrome/Edge/Safari) — needed only for the `axes` option; feature-detected and skipped where unsupported.
|
|
166
|
+
- **`prefers-reduced-motion`** — honoured automatically and live: no sew-in, no moving sheen, a steady caret. Turning it on mid-sew finishes the word at once. The `reducedMotion` option overrides it.
|
|
167
|
+
- **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`).
|
|
168
|
+
- **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.
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Development
|
|
173
|
+
|
|
174
|
+
This repo is a git submodule of the [type-tools](https://github.com/over-punch/type-tools) monorepo. To work on the package itself:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
git clone https://github.com/over-punch/ThreadText.git
|
|
178
|
+
cd ThreadText
|
|
179
|
+
npm install
|
|
180
|
+
npm test # vitest (happy-dom)
|
|
181
|
+
npm run build # ESM + CJS + .d.ts (vite)
|
|
182
|
+
npm run build:webflow # standalone window.ThreadText IIFE bundle
|
|
183
|
+
npm run lint # tsc --noEmit
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Source layout:
|
|
187
|
+
|
|
188
|
+
| Path | What it is |
|
|
189
|
+
|---|---|
|
|
190
|
+
| `src/index.ts` | Public exports (`createThreadText`, `useThreadText`, `ThreadText`, `THREAD_TEXT_CLASSES`) |
|
|
191
|
+
| `src/core/threadText.ts` | Framework-agnostic renderer (`createThreadText`) — no React imports |
|
|
192
|
+
| `src/core/types.ts` | `ThreadTextOptions`, `ThreadTextInstance`, `THREAD_TEXT_CLASSES` |
|
|
193
|
+
| `src/react/` | `useThreadText` hook + `<ThreadText>` component |
|
|
194
|
+
| `src/framer/ThreadText.tsx` | Framer code component (imports core from esm.sh) |
|
|
195
|
+
| `src/webflow/embed.ts` | Webflow auto-init embed → `dist/threadtext.webflow.min.js` |
|
|
196
|
+
| `src/__tests__/` | vitest suites (core, worker assembly, webflow embed) |
|
|
197
|
+
|
|
198
|
+
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).
|
|
199
|
+
|
|
200
|
+
> **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.
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## The type-tools family
|
|
205
|
+
|
|
206
|
+
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:
|
|
207
|
+
|
|
208
|
+
- [**ragtooth**](https://ragtooth.com) — deliberate sawtooth ragged-edge line breaking.
|
|
209
|
+
- [**floodText**](https://floodtext.com) — per-character variable-font wave animation.
|
|
210
|
+
- [**magnetType**](https://magnettype.com) — cursor-field per-character axis variation.
|
|
211
|
+
- [**opszStepper**](https://opszstepper.com) — optical-size font hot-swap by font-size.
|
|
212
|
+
- [**hoverBoldly**](https://hoverboldly.com) — bold-on-hover with no layout shift.
|
|
213
|
+
|
|
214
|
+
See the [full family](https://github.com/over-punch/type-tools) for all of them.
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
Part of [type-tools](https://github.com/over-punch/type-tools) by [Liiift Studio](https://overpunch.ca). MIT.
|
package/dist/core.cjs
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const Lt={bg:"tt-bg",fx:"tt-fx",text:"tt-text",caret:"tt-caret"};function lt(m,o){let i=Math.imul(m|0,374761393)+Math.imul(o|0,668265263)|0;return i=Math.imul(i^i>>>13,1274126177),((i^i>>>16)>>>0)/4294967295}function xe(m,o){const i=Math.floor(m),u=Math.floor(o),f=m-i,b=o-u,p=f*f*(3-2*f),w=b*b*(3-2*b),F=lt(i,u),j=lt(i+1,u),R=lt(i,u+1),x=lt(i+1,u+1);return F+(j-F)*p+(R-F)*w+(F-j-R+x)*p*w}function Me(m,o,i,u,f){let p=0;u[0]=0,f[0]=-1e20,f[1]=1e20;for(let w=1;w<o;w++){let F=(m[w]+w*w-(m[u[p]]+u[p]*u[p]))/(2*w-2*u[p]);for(;F<=f[p];)p--,F=(m[w]+w*w-(m[u[p]]+u[p]*u[p]))/(2*w-2*u[p]);p++,u[p]=w,f[p]=F,f[p+1]=1e20}p=0;for(let w=0;w<o;w++){for(;f[p+1]<w;)p++;i[w]=(w-u[p])*(w-u[p])+m[u[p]]}}function qe(m,o,i){const f=new Float64Array(o*i);for(let x=0;x<o*i;x++)f[x]=m[x]?1e20:0;const b=Math.max(o,i),p=new Float64Array(b),w=new Float64Array(b),F=new Int32Array(b),j=new Float64Array(b+1);for(let x=0;x<o;x++){for(let M=0;M<i;M++)p[M]=f[M*o+x];Me(p,i,w,F,j);for(let M=0;M<i;M++)f[M*o+x]=w[M]}for(let x=0;x<i;x++){for(let M=0;M<o;M++)p[M]=f[x*o+M];Me(p,o,w,F,j);for(let M=0;M<o;M++)f[x*o+M]=w[M]}const R=new Float32Array(o*i);for(let x=0;x<o*i;x++)R[x]=Math.sqrt(f[x]);return R}function we(m,o,i,u,f){let b=Float32Array.from(m);const p=new Float32Array(o*i),w=1/(2*u+1),F=(j,R)=>j<0?0:j>R?R:j;for(let j=0;j<f;j++){for(let R=0;R<i;R++){const x=R*o;let M=0;for(let z=-u;z<=u;z++)M+=b[x+F(z,o-1)];for(let z=0;z<o;z++)p[x+z]=M*w,M+=b[x+F(z+u+1,o-1)]-b[x+F(z-u,o-1)]}for(let R=0;R<o;R++){let x=0;for(let M=-u;M<=u;M++)x+=p[F(M,i-1)*o+R];for(let M=0;M<i;M++)b[M*o+R]=x*w,x+=p[F(M+u+1,i-1)*o+R]-p[F(M-u,i-1)*o+R]}}return b}function me(m,o){if(!m)return o;const i=m.trim();if(i[0]==="#"){let f=i.slice(1);if(f.length===3&&(f=f[0]+f[0]+f[1]+f[1]+f[2]+f[2]),f.length>=6){const b=parseInt(f.slice(0,2),16),p=parseInt(f.slice(2,4),16),w=parseInt(f.slice(4,6),16);if(!Number.isNaN(b)&&!Number.isNaN(p)&&!Number.isNaN(w))return[b,p,w]}return o}const u=i.match(/rgba?\(([^)]+)\)/i);if(u){const f=u[1].split(",").map(b=>parseFloat(b));if(f.length>=3&&f.slice(0,3).every(b=>!Number.isNaN(b)))return[Math.round(f[0]),Math.round(f[1]),Math.round(f[2])]}return o}function at(m){return m<0?0:m>255?255:Math.round(m)}function ee(m,o,i){return m<o?o:m>i?i:m}function Ut(m,o){return typeof m=="number"&&Number.isFinite(m)?m:o}function ye(m){const o="Georgia, serif";if(typeof m!="string"||!m.trim())return o;try{const i=document.createElement("canvas").getContext("2d");if(!i)return m;if(i.font="13px serif",i.font=`100px ${m}`,/13px/.test(i.font))return console.warn(`[threadText] the font ${JSON.stringify(m)} isn't a valid font-family list; using ${o}`),o}catch{}return m}const ze="position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0";function Xe(m){return(m.split(",")[0]||"serif").trim().replace(/^["']|["']$/g,"")}function pe(m,o,i){const u=new Uint8Array(o*i);for(let A=0;A<i;A++)for(let L=0;L<o;L++){const E=A*o+L,X=m[E*4+3]/255,K=xe(L*.5,A*.5)*.22+xe(L*1.8,A*1.8)*.1;u[E]=X>.5+(K-.16)?1:0}const f=qe(u,o,i),b=new Float32Array(o*i);for(let A=1;A<i-1;A++)for(let L=1;L<o-1;L++){const E=A*o+L;b[E]=(f[E]*4+f[E-1]+f[E+1]+f[E-o]+f[E+o])/8}const p=new Float32Array(o*i),w=new Float32Array(o*i),F=new Float32Array(o*i),j=new Float32Array(o*i);for(let A=1;A<i-1;A++)for(let L=1;L<o-1;L++){const E=A*o+L;if(!u[E])continue;const X=(b[E+1]-b[E-1])*.5,K=(b[E+o]-b[E-o])*.5;F[E]=X,j[E]=K,p[E]=X*X-K*K,w[E]=2*X*K}const R=Math.max(7,Math.round(i*.05)),x=we(p,o,i,R,2),M=we(w,o,i,R,2),z=new Float32Array(o*i),q=new Float32Array(o*i);for(let A=0;A<o*i;A++){if(!u[A])continue;const L=.5*Math.atan2(M[A],x[A]);z[A]=Math.cos(L),q[A]=Math.sin(L)}const Ct=Math.max(9,i*.04),yt=new Float32Array(o*i),Gt=-.45,W=-.62,vt=.64;for(let A=1;A<i-1;A++)for(let L=1;L<o-1;L++){const E=A*o+L;if(!u[E])continue;let X=F[E],K=j[E];const bt=Math.hypot(X,K)||1;X/=bt,K/=bt;const zt=b[E],pt=(1-Math.min(zt/Ct,1))*1.25;let B=-X*pt,J=-K*pt;const ot=1.5,Ft=Math.hypot(B,J,ot);B/=Ft,J/=Ft;let rt=B*Gt+J*W+ot/Ft*vt;rt<0&&(rt=0);const it=Math.min(1,.45+.55*Math.min(zt/(Ct*.7),1));yt[E]=Math.min(1.12,(.5+.75*rt)*it)}return{mask:u,ex:z,ey:q,shade:yt}}function dn(m,o){var te,Ue,Ge;if(typeof window>"u"||typeof document>"u"||!m)return{setText(){},replay(){},resize(){},update(){},focus(){},destroy(){},get text(){return o.text??""}};let i=o.text??"",u=ye(o.font),f=ee(Ut(o.weight,680),1,1e3),b=ee(Ut(o.fill,.9),.05,1),p=o.align==="left"||o.align==="right"?o.align:"center";const w=(t,e)=>{const n=Ut(t,e);return n>=1?Math.min(n,1e5):e};let F=w(o.sewRate,110),j=o.sewStyle==="hand"?"hand":"machine";const R=["satin","cross","chain","running"];let x=R.includes(o.stitchMode??"")?o.stitchMode:"satin",M=o.sheen??!0,z=o.animate??!0,q=o.editable??!1;const Ct=t=>typeof t=="number"&&Number.isFinite(t)&&t>=1&&t<=200?t:void 0;let yt=Ct(o.pitch);const Gt=["solid","twotone","gradient"];let W=Gt.includes(o.colorMode??"")?o.colorMode:"solid",vt=o.threadColor??"#fffbf3",A=o.threadColor2??vt,L=o.backstitch??!1,E=o.outlineColor,X=o.axes,K=o.onTextChange,bt=Xe(u);function zt(){if(!X)return"";const t=[];for(const e in X){const n=X[e];typeof n=="number"&&isFinite(n)&&t.push(`"${e}" ${n}`)}return t.join(", ")}function pt(t){"fontVariationSettings"in t&&(t.fontVariationSettings=zt()||"normal")}const B=o.reducedMotion===void 0&&window.matchMedia?window.matchMedia("(prefers-reduced-motion: reduce)"):null;let J=o.reducedMotion??!!(B!=null&&B.matches);const ot=t=>({peak:t,mid:[t[0]*.585,t[1]*.585,t[2]*.585],end:[t[0]*.47,t[1]*.47,t[2]*.47]}),Ft=(t,e,n)=>[t[0]+(e[0]-t[0])*n,t[1]+(e[1]-t[1])*n,t[2]+(e[2]-t[2])*n],rt=6;let it=[255,251,243],ct=[],ne=ot([0,0,0]);function Ce(){const t=me(vt,[255,251,243]),e=me(A,t);if(W==="twotone")ct=[ot(t),ot(e)];else if(W==="gradient"){ct=[];for(let n=0;n<rt;n++)ct.push(ot(Ft(t,e,n/(rt-1))))}else ct=[ot(t)];it=ct[0].peak,ne=ot(E?me(E,[0,0,0]):[t[0]*.32,t[1]*.32,t[2]*.32])}Ce();const Rt=[];let O,C,_=null,U=null;if(m instanceof HTMLCanvasElement)O=m,C=m.parentElement??m;else{C=m;const t=n=>n.replace(/\s+/g," ").trim(),e=Array.from(C.childNodes);if(e.some(n=>n.nodeType===1||(n.textContent??"").trim())){_=document.createElement("span"),_.className=Lt.text,_.style.cssText=ze;for(const n of e)_.appendChild(n);C.appendChild(_),t(i)&&t(_.textContent??"")!==t(i)&&(_.style.display="none")}(!_||_.style.display==="none")&&(U=document.createElement("span"),U.className=Lt.text,U.style.cssText=ze,U.textContent=i,C.appendChild(U)),O=document.createElement("canvas"),Rt.push(O),C.appendChild(O)}const It={height:C.style.height,position:C.style.position,hadStyle:C.hasAttribute("style")};O.setAttribute("aria-hidden","true"),O.classList.add(Lt.bg);const Y=document.createElement("canvas");Rt.push(Y),Y.classList.add(Lt.fx),Y.setAttribute("aria-hidden","true"),C.appendChild(Y),getComputedStyle(C).position==="static"&&(C.style.position="relative"),O.style.display="block",O.style.width="100%",Object.assign(Y.style,{position:"absolute",top:"0",left:"0",width:"100%",pointerEvents:"none",mixBlendMode:"screen"});let T=null,N=null,St=!1,S=0,I=0,xt=0,oe=0,tt=4,$t=[],Tt=0,Et=0,re=[],ve=0,be=-1,Se="",Te=-1,Ee=-1,Z=new Uint8Array(0),Xt=new Float32Array(0),ie=new Float32Array(0),se=new Float32Array(0),ft=null,At=null,qt=null,Ot=[],dt=[],Nt=[],Mt=3;const $={rows:[],idx:0,acc:0,rate:0,on:!1},et={x:0,y:0,set:!1};let ut=!0,ae=0,Pt=!1,Bt=0,le=-1,D=!1,Q=!1,nt=null,jt="",Kt=!1,ce=0,fe=!1,H;function Ke(){const t=C.getBoundingClientRect(),e=Math.max(1,Math.round(t.width||Math.min(window.innerWidth*.95,1240))),n=i||"Ag",r=O.getContext("2d");r&&(r.textBaseline="middle",r.direction=Le());let a=100;r&&(r.font=`${f} 100px ${u}`,pt(r),a=r.measureText(n).width||100);const s=Math.max(8,100*(e*b)/a);let l=s*.62,v=s*.62,c=e*b;if(r){r.font=`${f} ${s}px ${u}`,pt(r);const y=r.measureText(n);c=y.width||c,y.actualBoundingBoxAscent>0&&(l=y.actualBoundingBoxAscent),y.actualBoundingBoxDescent>0&&(v=y.actualBoundingBoxDescent)}const h=Math.max(1,Math.round(2*Math.max(l,v)*1.1));let g=Math.min(window.devicePixelRatio||1,1.7);for(;e*g*h*g>9e5&&g>.4;)g-=.1;S=Math.max(1,Math.round(e*g)),I=Math.max(1,Math.round(h*g)),xt=Math.max(4,Math.round(s*g));const d=c*g,k=Math.max(2,Math.round(xt*.08));oe=Math.round(p==="left"?k:p==="right"?S-d-k:S/2-d/2);for(const y of[O,Y])y.width=S,y.height=I,y.style.height=h+"px";C.style.height=h+"px",Y.style.left=O.offsetLeft+"px",Y.style.top=O.offsetTop+"px",Y.style.width=(O.offsetWidth||O.getBoundingClientRect().width)+"px",tt=yt??Math.max(3,I*.0095)}function Ve(){At&&Te===S&&Ee===I||(At=document.createElement("canvas"),At.width=S,At.height=I,qt=At.getContext("2d",{willReadFrequently:!0}),Te=S,Ee=I)}function Ae(){return ct.map(t=>t.peak.join(",")).join(";")+"|"+x+"|"+W+(L?"|bs"+ne.peak.join(","):"")}function ke(t,e){const n=Math.max(7,tt*2.5),r=Math.max(3.2,tt*1.3),a=[];for(let s=0;s<20;s++){const l=.34+s/19*.82,v=document.createElement("canvas");v.width=Tt,v.height=Et;const c=v.getContext("2d");if(!c){a.push(v);continue}const h=Tt/2,g=Et/2,d=(y,P)=>`rgba(${at(y[0]*l)},${at(y[1]*l)},${at(y[2]*l)},${P})`,k=(y,P,gt,st)=>{c.save(),c.translate(h,g),c.rotate(gt);const V=c.createLinearGradient(0,-P/2,0,P/2);V.addColorStop(0,d(t.end,0)),V.addColorStop(.16,d(t.mid,1)),V.addColorStop(.5,d(t.peak,1)),V.addColorStop(.84,d(t.mid,1)),V.addColorStop(1,d(t.end,0)),c.fillStyle=V;const G=P/2;if(c.beginPath(),c.moveTo(-y/2+G,-G),c.lineTo(y/2-G,-G),c.arc(y/2-G,0,G,-Math.PI/2,Math.PI/2),c.lineTo(-y/2+G,G),c.arc(-y/2+G,0,G,Math.PI/2,-Math.PI/2),c.closePath(),c.fill(),st){c.globalCompositeOperation="destination-in";const mt=c.createLinearGradient(-y/2,0,y/2,0);mt.addColorStop(0,"rgba(0,0,0,0)"),mt.addColorStop(.22,"rgba(0,0,0,1)"),mt.addColorStop(.78,"rgba(0,0,0,1)"),mt.addColorStop(1,"rgba(0,0,0,0)"),c.fillStyle=mt,c.fillRect(-y/2-1,-P/2-1,y+2,P+2),c.globalCompositeOperation="source-over"}c.restore()};if(e==="cross")k(n*.92,r*.82,Math.PI/4,!1),k(n*.92,r*.82,-Math.PI/4,!1);else if(e==="chain"){c.save(),c.translate(h,g);const y=c.createLinearGradient(0,-r,0,r);y.addColorStop(0,d(t.mid,1)),y.addColorStop(.5,d(t.peak,1)),y.addColorStop(1,d(t.mid,1)),c.strokeStyle=y,c.lineWidth=Math.max(1.6,r*.7),c.lineCap="round",c.beginPath(),c.ellipse(0,0,n*.34,r*1.05,0,0,Math.PI*2),c.stroke(),c.restore()}else e==="running"?k(n*.5,r,0,!1):k(n,r,0,!0);a.push(v)}return a}function de(){const t=Math.max(7,tt*2.5),e=Math.max(3.2,tt*1.3);x==="cross"||x==="chain"?Tt=Et=Math.ceil(t)+2:(Tt=Math.ceil(t)+2,Et=Math.ceil(e)+2),ve=x==="chain"||x==="running"?Math.PI/2:0,$t=[];for(const n of ct)for(const r of ke(n,x))$t.push(r);re=L?ke(ne,"running"):[],be=tt,Se=Ae()}function Ye(){$t.length&&be===tt&&Se===Ae()||de()}function Le(){try{return getComputedStyle(C).direction==="rtl"?"rtl":"ltr"}catch{return"ltr"}}function Fe(){if(!qt)return null;const t=qt;return t.clearRect(0,0,S,I),t.fillStyle="#fff",t.textAlign="left",t.textBaseline="middle",t.direction=Le(),t.font=`${f} ${xt}px ${u}`,pt(t),t.fillText(i,oe,I*.5),t.getImageData(0,0,S,I).data}function ue(t){Z=t.mask,ie=t.ex,se=t.ey,Xt=t.shade,ft=document.createElement("canvas"),ft.width=S,ft.height=I;const e=ft.getContext("2d");if(e){const n=e.createImageData(S,I);for(let r=0;r<S*I;r++)Z[r]&&(n.data[r*4]=255,n.data[r*4+1]=255,n.data[r*4+2]=255,n.data[r*4+3]=255);e.putImageData(n,0,0)}}function Re(){const t=Math.max(1.6,tt*.6);Mt=t;const e=[],n=[];let r=1/0,a=-1/0;for(let s=0;s<I;s+=t)for(let l=0;l<S;l+=t){const v=(lt(Math.floor(l),Math.floor(s))-.5)*t*.9,c=(lt(Math.floor(s),Math.floor(l))-.5)*t*.9,h=Math.round(l+v),g=Math.round(s+c);if(h<1||g<1||h>=S-1||g>=I-1)continue;const d=g*S+h;if(!Z[d])continue;const k=Math.atan2(se[d],ie[d]),y=.86+lt(h*7,g*13)*.18;let P=Xt[d]*y;P=P<0?0:P>1.12?1.12:P,e.push({x:h,y:g,ang:k,idx:Math.min(19,Math.max(0,Math.round(P/1.12*19)))}),h<r&&(r=h),h>a&&(a=h)}if(W==="twotone")if(x==="satin"){const s=Math.max(4,tt*1.6);for(const l of e){const v=-Math.sin(l.ang)*l.x+Math.cos(l.ang)*l.y;l.pal=Math.floor(v/s)&1}}else for(const s of e)s.pal=Math.round(s.x/t)+Math.round(s.y/t)&1;else if(W==="gradient"){const s=Math.max(1,a-r);for(const l of e){const v=(l.x-r)/s*(rt-1),c=Math.min(rt-1,Math.max(0,Math.floor(v)));l.pal=c,c<rt-1&<(l.x*3+1,l.y*5+2)<v-c&&n.push({x:l.x,y:l.y,ang:l.ang,idx:l.idx,pal:c+1})}}Ot=e,Nt=n,Je()}function Je(){if(dt=[],!L||!Z.length)return;const t=Math.max(3,tt*1.5),e=Math.max(1,Math.ceil(S/t)),n=new Set,r=(a,s)=>Z[s*S+a]?1:0;for(let a=1;a<I-1;a++)for(let s=1;s<S-1;s++){const l=a*S+s;if(!Z[l]||Z[l-1]&&Z[l+1]&&Z[l-S]&&Z[l+S])continue;const v=Math.floor(a/t)*e+Math.floor(s/t);if(n.has(v))continue;n.add(v);const c=r(s+1,a-1)+2*r(s+1,a)+r(s+1,a+1)-(r(s-1,a-1)+2*r(s-1,a)+r(s-1,a+1)),h=r(s-1,a+1)+2*r(s,a+1)+r(s+1,a+1)-(r(s-1,a-1)+2*r(s,a-1)+r(s+1,a-1)),g=Math.atan2(c,-h);let d=Xt[l];d=d<0?0:d>1.12?1.12:d,dt.push({x:s,y:a,ang:g,idx:Math.min(19,Math.max(8,Math.round(d/1.12*19))),outline:1})}}function Vt(t,e){t.save(),t.translate(e.x,e.y),e.outline?(t.rotate(e.ang),t.drawImage(re[e.idx],-Tt/2,-Et/2)):(t.rotate(e.ang+ve),t.drawImage($t[(e.pal||0)*20+e.idx],-Tt/2,-Et/2)),t.restore()}function _t(){const t=O.getContext("2d");if(t){for(const e of Ot)Vt(t,e);for(const e of Nt)Vt(t,e);for(const e of dt)Vt(t,e)}}function Dt(){const t=O.getContext("2d");t&&t.clearRect(0,0,S,I)}function Ie(t){if(!t.length)return[];const e=Math.max(4,Mt*1.5),n=new Map,r=(h,g)=>h+","+g;t.forEach((h,g)=>{const d=r(Math.floor(h.x/e),Math.floor(h.y/e));let k=n.get(d);k||n.set(d,k=[]),k.push(g)});const a=Mt*1.8*(Mt*1.8);function s(h){const g=t[h],d=Math.floor(g.x/e),k=Math.floor(g.y/e),y=[];for(let P=-1;P<=1;P++)for(let gt=-1;gt<=1;gt++){const st=n.get(r(d+P,k+gt));if(st)for(const V of st){if(V===h)continue;const G=t[V].x-g.x,mt=t[V].y-g.y;G*G+mt*mt<=a&&y.push(V)}}return y}const l=new Uint8Array(t.length),v=[],c=t.map((h,g)=>g).sort((h,g)=>t[h].x+t[h].y-(t[g].x+t[g].y));for(const h of c){if(l[h])continue;let g=[h];for(l[h]=1;g.length;){v.push(g.map(k=>t[k]));const d=[];for(const k of g)for(const y of s(k))l[y]||(l[y]=1,d.push(y));g=d}}return v}function Ze(t){const e=Math.max(4,Mt*1.5),n=new Map,r=(c,h)=>c+","+h;t.forEach((c,h)=>{const g=r(Math.floor(c.x/e),Math.floor(c.y/e));let d=n.get(g);d||n.set(g,d=[]),d.push(h)});const a=Mt*1.8*(Mt*1.8),s=c=>{const h=t[c],g=Math.floor(h.x/e),d=Math.floor(h.y/e),k=[];for(let y=-1;y<=1;y++)for(let P=-1;P<=1;P++){const gt=n.get(r(g+y,d+P));if(gt)for(const st of gt){if(st===c)continue;const V=t[st].x-h.x,G=t[st].y-h.y;V*V+G*G<=a&&k.push(st)}}return k},l=new Uint8Array(t.length),v=[];for(let c=0;c<t.length;c++){if(l[c])continue;const h=[];let g=[c];for(l[c]=1;g.length;){const d=[];for(const k of g){h.push(t[k]);for(const y of s(k))l[y]||(l[y]=1,d.push(y))}g=d}v.push(h)}return v}function Qe(t){if(!t.length)return[];const e=s=>{let l=1/0;for(const v of s)v.x<l&&(l=v.x);return l},n=s=>{let l=0;for(const v of s)l+=v.y;return l/s.length},r=Ze(t).sort((s,l)=>e(s)-e(l)),a=[];for(const s of r){const l=Ie(s);let v=1;for(const d of l)d.length>v&&(v=d.length);const c=Math.max(2,v*.45),h=l.filter(d=>d.length>=c).sort((d,k)=>n(d)-n(k)),g=l.filter(d=>d.length<c).sort((d,k)=>n(d)-n(k));for(const d of h)a.push(d);for(const d of g)a.push(d)}return a}function He(){if(Dt(),J||!z){_t(),$.on=!1;return}const t=j==="hand"?Qe:Ie;let e;if(W==="gradient"&&ct.length>1){e=[];const n=new Map;for(const r of Ot){const a=r.pal||0,s=n.get(a);s?s.push(r):n.set(a,[r])}for(const r of[...n.keys()].sort((a,s)=>a-s))for(const a of t(n.get(r)))e.push(a)}else e=t(Ot);if(Nt.length)for(const n of t(Nt))e.push(n);if(dt.length){const r=Math.ceil(dt.length/24);for(let a=0;a<dt.length;a+=r)e.push(dt.slice(a,a+r))}$.rows=e,$.idx=0,$.acc=0,$.rate=F,$.on=$.rows.length>0}function We(t){const e=O.getContext("2d");if(e)for(t>$.rows.length&&(t=$.rows.length);$.idx<t;){const n=$.rows[$.idx++];for(const r of n)Vt(e,r)}}function tn(t,e){const n=Y.getContext("2d");if(!n||(n.clearRect(0,0,S,I),!M||!ft))return;const r=S*.32,a=n.createRadialGradient(t,e,0,t,e,r);a.addColorStop(0,"rgba(255,250,238,0.18)"),a.addColorStop(.5,"rgba(255,250,238,0.055)"),a.addColorStop(1,"rgba(255,250,238,0)"),n.fillStyle=a,n.fillRect(0,0,S,I),n.globalCompositeOperation="destination-in",n.drawImage(ft,0,0),n.globalCompositeOperation="source-over"}function en(){N||!q||(N=document.createElement("span"),N.className=Lt.caret,N.setAttribute("aria-hidden","true"),Object.assign(N.style,{position:"absolute",pointerEvents:"none",borderRadius:"1px",opacity:"0",background:`rgb(${at(it[0])},${at(it[1])},${at(it[2])})`}),C.appendChild(N),Rt.push(N))}function Yt(){if(!q||!N)return;const t=O.getContext("2d");if(!t)return;t.font=`${f} ${xt}px ${u}`,pt(t);const e=t.measureText(i).width,n=(O.clientWidth||S)/S,r=xt*.7;N.style.left=(oe+e+xt*.05)*n+"px",N.style.top=.5*(O.clientHeight||I)-r*n/2+"px",N.style.height=r*n+"px",N.style.width=Math.max(2,Math.round(xt*.02*n))+"px",N.style.background=`rgb(${at(it[0])},${at(it[1])},${at(it[2])})`}function ht(){D||Pt||(Pt=!0,Bt=0,ae=requestAnimationFrame($e))}function $e(t){if(D){Pt=!1;return}Bt||(Bt=t);const e=Math.min(.25,(t-Bt)/1e3);Bt=t,$.on&&($.acc+=$.rate*e,We($.acc),$.idx>=$.rows.length&&($.on=!1)),ut&&ft&&(tn(et.set?et.x:S*.5,et.set?et.y:I*.45),ut=!1);const n=q&&St&&!J;if(N&&(N.style.opacity=q&&St&&(J||Math.floor(t/530)%2===0)?"1":"0"),!$.on&&!ut&&!n){Pt=!1;return}ae=requestAnimationFrame($e)}function Jt(t){if(J)return;const e=C.getBoundingClientRect();et.x=(t.clientX-e.left)/(e.width||1)*S,et.y=(t.clientY-e.top)/(e.height||1)*I,et.set=!0,ut=!0,ht()}function Zt(){J||(et.set=!1,ut=!0,ht())}function nn(){T&&Be(T.value,!0)}function on(t){t.key==="Enter"&&(t.preventDefault(),Wt.replay())}function rn(){St=!0,ht(),Oe()}function Oe(){J&&N&&q&&St&&(N.style.opacity="1")}function sn(){St=!1,N&&(N.style.opacity="0")}function Ne(t){if(!(t===q&&T))if(q=t,t){T||(T=document.createElement("input"),T.type="text",T.value=i,T.maxLength=64,T.setAttribute("aria-label","Embroidered text — type to change it"),T.setAttribute("autocomplete","off"),T.setAttribute("autocapitalize","off"),T.spellcheck=!1,Object.assign(T.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"}),T.addEventListener("input",nn),T.addEventListener("keydown",on),T.addEventListener("focus",rn),T.addEventListener("blur",sn),C.appendChild(T),Rt.push(T)),en(),Yt();for(const e of[_,U])e==null||e.setAttribute("aria-hidden","true")}else{for(const e of[_,U])e==null||e.removeAttribute("aria-hidden");T&&(T.remove(),T=null),St=!1,N&&(N.style.opacity="0")}}function an(t){if(t!==M)if(M=t,t)C.addEventListener("pointermove",Jt,{passive:!0}),C.addEventListener("pointerleave",Zt,{passive:!0}),et.set=!1,ut=!0,ht();else{C.removeEventListener("pointermove",Jt),C.removeEventListener("pointerleave",Zt);const e=Y.getContext("2d");e&&e.clearRect(0,0,S,I)}}function Qt(t){Re(),t?He():(Dt(),_t(),$.on=!1),T&&T.value!==i&&(T.value=i),Yt(),ut=!0,ht()}function wt(t){if(!i.trim()&&!q){for(const n of[O,Y])n.width=0,n.height=0,n.style.height="0px";C.style.height=It.height;return}Ke(),Ve(),Ye();const e=Fe();if(!e){Qt(t);return}if(nt&&Kt){const n=++ce;fe=t,H&&clearTimeout(H),H=setTimeout(()=>Pe(n),800);try{nt.postMessage({id:n,W:S,H:I,alpha:e},[e.buffer])}catch{Pe(n)}return}ue(pe(e,S,I)),Qt(t)}function Pe(t){if(t!==ce||D)return;H&&(clearTimeout(H),H=void 0),Kt=!1;const e=Fe();e&&(ue(pe(e,S,I)),Qt(fe))}function Be(t,e){t=t??"",U&&(U.textContent=t),!(t===i&&Q)&&(i=t,Q&&(wt(!1),e&&K&&K(i)))}const Ht=new Set;function je(t,e,n){const r=`${e} 200px ${t}`;let a=!1;try{a=!!document.fonts&&document.fonts.check(r)}catch{a=!0}if(!document.fonts||a){n();return}let s=!1,l;const v=()=>{s||(s=!0,clearTimeout(l),Ht.delete(l),D||n())};document.fonts.load(r,i).then(()=>document.fonts.ready).then(v,v),l=setTimeout(v,1500),Ht.add(l)}function ln(t){const e=t.data;if(e&&e.type==="pong"){Kt=!0;return}!e||D||e.id!==ce||(H&&(clearTimeout(H),H=void 0),ue({mask:e.mask,ex:e.ex,ey:e.ey,shade:e.shade}),Qt(fe))}function cn(){if(!(typeof Worker>"u"||typeof Blob>"u"||typeof URL>"u"||!URL.createObjectURL))try{const t=`${lt}
|
|
2
|
+
${xe}
|
|
3
|
+
${Me}
|
|
4
|
+
${qe}
|
|
5
|
+
${we}
|
|
6
|
+
var __cg=${pe};
|
|
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])};`;jt=URL.createObjectURL(new Blob([t],{type:"application/javascript"})),nt=new Worker(jt),nt.onmessage=ln,nt.onerror=()=>{Kt=!1},nt.postMessage({type:"ping"})}catch{nt=null}}function fn(){if(!D){if(!O.getContext("2d")){const t=U??_;t&&(t.style.cssText="");for(const e of[O,Y])e.style.display="none";C.style.height=It.height,Q=!0;return}wt(!0),le=Math.round(C.getBoundingClientRect().width),Q=!0}}cn(),q&&(q=!1,Ne(!0)),M&&(C.addEventListener("pointermove",Jt,{passive:!0}),C.addEventListener("pointerleave",Zt,{passive:!0})),je(bt,f,fn);const _e=()=>{if(!(D||!Q))try{document.fonts.check(`${f} 200px ${u}`)&&wt(!1)}catch{}};(Ue=(te=document.fonts)==null?void 0:te.addEventListener)==null||Ue.call(te,"loadingdone",_e);const De=()=>{D||(J=!!(B!=null&&B.matches),J&&Q&&($.on&&($.on=!1,Dt(),_t()),et.set=!1,ut=!0,ht(),Oe()))};(Ge=B==null?void 0:B.addEventListener)==null||Ge.call(B,"change",De);let he;function ge(){clearTimeout(he),he=setTimeout(()=>{if(D)return;if(!C.isConnected){Wt.destroy();return}if(!Q)return;const t=Math.round(C.getBoundingClientRect().width);t!==le&&(le=t,wt(!1))},200)}window.addEventListener("resize",ge);const kt=typeof ResizeObserver<"u"?new ResizeObserver(ge):null;kt==null||kt.observe(C);const Wt={get text(){return i},setText(t){if(!D){if(!Q){i=t??"",T&&(T.value=i),U&&(U.textContent=i);return}Be(t,!1)}},replay(){D||!Q||wt(!0)},resize(){D||!Q||wt(!1)},update(t){if(D)return;t.text!==void 0&&Wt.setText(t.text);let e=!1,n=!1,r=!1,a=!1;if(t.font!==void 0&&ye(t.font)!==u&&(u=ye(t.font),bt=Xe(u),e=!0),t.weight!==void 0){const s=ee(Ut(t.weight,f),1,1e3);s!==f&&(f=s,e=!0)}if(t.fill!==void 0){const s=ee(Ut(t.fill,b),.05,1);s!==b&&(b=s,e=!0)}if(t.align!==void 0){const s=t.align==="left"||t.align==="right"?t.align:"center";s!==p&&(p=s,e=!0)}t.pitch!==void 0&&Ct(t.pitch)!==yt&&(yt=Ct(t.pitch),e=!0),t.axes!==void 0&&(X=t.axes,e=!0),t.threadColor!==void 0&&t.threadColor!==vt&&(vt=t.threadColor,a=!0),t.threadColor2!==void 0&&t.threadColor2!==A&&(A=t.threadColor2,a=!0),t.outlineColor!==void 0&&t.outlineColor!==E&&(E=t.outlineColor,a=!0),t.colorMode!==void 0&&t.colorMode!==W&&Gt.includes(t.colorMode)&&(W=t.colorMode,a=!0,r=!0),t.backstitch!==void 0&&t.backstitch!==L&&(L=t.backstitch,r=!0),t.stitchMode!==void 0&&t.stitchMode!==x&&R.includes(t.stitchMode)&&(x=t.stitchMode,n=!0),t.sewRate!==void 0&&(F=w(t.sewRate,F)),t.sewStyle!==void 0&&(j=t.sewStyle==="hand"?"hand":"machine"),t.animate!==void 0&&(z=t.animate),t.onTextChange!==void 0&&(K=t.onTextChange),t.sheen!==void 0&&an(t.sheen),t.editable!==void 0&&Ne(t.editable),a&&Ce(),Q&&(e?je(bt,f,()=>{D||wt(!1)}):r?(de(),Re(),Dt(),_t(),Yt(),ht()):(n||a)&&(de(),Dt(),_t(),Yt(),ht()))},focus(){!D&&q&&(T==null||T.focus())},destroy(){var t,e,n;if(!D){D=!0,Pt=!1,cancelAnimationFrame(ae),clearTimeout(he),H&&clearTimeout(H);for(const r of Ht)clearTimeout(r);if(Ht.clear(),nt&&(nt.terminate(),nt=null),jt){try{URL.revokeObjectURL(jt)}catch{}jt=""}window.removeEventListener("resize",ge),kt==null||kt.disconnect(),(t=B==null?void 0:B.removeEventListener)==null||t.call(B,"change",De),(n=(e=document.fonts)==null?void 0:e.removeEventListener)==null||n.call(e,"loadingdone",_e),C.removeEventListener("pointermove",Jt),C.removeEventListener("pointerleave",Zt);for(const r of Rt)r.remove();if(_){for(;_.firstChild;)C.insertBefore(_.firstChild,_);_.remove(),_=null}U==null||U.remove(),U=null,C.style.height=It.height,C.style.position=It.position,!It.hadStyle&&!C.getAttribute("style")&&C.removeAttribute("style"),$t=[],re=[],Ot=[],dt=[],Nt=[],$.rows=[],Z=new Uint8Array(0),Xt=new Float32Array(0),ie=new Float32Array(0),se=new Float32Array(0),ft=At=null,qt=null,N=null,T=null}}};return Wt}exports.THREAD_TEXT_CLASSES=Lt;exports.createThreadText=dn;
|