@yewdiel21/motion-kit 0.17.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +240 -199
- package/dist/auto/index.d.ts +19 -3
- package/dist/auto/index.js +35 -3
- package/dist/auto/index.js.map +1 -1
- package/dist/{chunk-QFBOHPWZ.js → chunk-CQP66AZK.js} +2 -2
- package/dist/{chunk-QP4QJOEN.js → chunk-H4STLMCM.js} +2 -2
- package/dist/{chunk-BZLMCHUD.js → chunk-KDIJX6G4.js} +3 -3
- package/dist/{chunk-DOOMULVV.js → chunk-OLT3CWDJ.js} +4 -2
- package/dist/chunk-OLT3CWDJ.js.map +1 -0
- package/dist/{chunk-VJFPTLAK.js → chunk-PEDYJ2TV.js} +8 -22
- package/dist/chunk-PEDYJ2TV.js.map +1 -0
- package/dist/{chunk-7IQTMF7Y.js → chunk-Y4GIBTRG.js} +5130 -2153
- package/dist/chunk-Y4GIBTRG.js.map +1 -0
- package/dist/css/components/badge-button.css +16 -0
- package/dist/css/components/bento-grid.css +13 -0
- package/dist/css/components/code-block.css +267 -0
- package/dist/css/components/command-search.css +274 -2
- package/dist/css/components/dialog.css +264 -0
- package/dist/css/components/drilldown-menu.css +11 -7
- package/dist/css/components/equalizer-slider.css +26 -0
- package/dist/css/components/handwriting-text.css +15 -0
- package/dist/css/components/heatmap-chart.css +267 -1
- package/dist/css/components/magnetize-button.css +18 -0
- package/dist/css/components/morphing-page-dots.css +13 -0
- package/dist/css/components/not-found.css +22 -0
- package/dist/css/components/notification-stack.css +42 -0
- package/dist/css/components/particle-button.css +14 -0
- package/dist/css/components/radio-group.css +29 -0
- package/dist/css/components/rating-scale.css +23 -0
- package/dist/css/components/scroll-area.css +261 -0
- package/dist/css/components/scroll-preview-dock.css +18 -0
- package/dist/css/components/select.css +300 -4
- package/dist/css/components/signature-pad.css +130 -103
- package/dist/css/components/stepper.css +36 -0
- package/dist/css/components/sticky-header.css +10 -0
- package/dist/css/components/temperature-slider.css +29 -0
- package/dist/css/components/voice-message-bubble.css +62 -45
- package/dist/css/components/waveform-player.css +49 -34
- package/dist/css/components/wheel-picker.css +16 -15
- package/dist/css/tokens.css +4 -0
- package/dist/elements/index.d.ts +217 -4
- package/dist/elements/index.js +614 -9
- package/dist/elements/index.js.map +1 -1
- package/dist/{engine-Bn6hC87X.d.ts → engine-D6XV-WNt.d.ts} +390 -1
- package/dist/gl/index.js +2 -2
- package/dist/index.css +886 -177
- package/dist/index.d.ts +23 -23
- package/dist/index.js +37 -5
- package/dist/index.js.map +1 -1
- package/dist/react/index.d.ts +126 -7
- package/dist/react/index.js +722 -7
- package/dist/react/index.js.map +1 -1
- package/dist/{reduced-YS6TZjpa.d.ts → reduced-4croIXl-.d.ts} +1 -1
- package/dist/{store-CZ6vdtIE.d.ts → store-CAHWkurn.d.ts} +1 -1
- package/package.json +110 -110
- package/registry.json +1357 -1160
- package/src/auto/auto-init.ts +341 -308
- package/src/components/badge-button/README.md +28 -0
- package/src/components/badge-button/badge-button.css +15 -0
- package/src/components/badge-button/element.ts +16 -0
- package/src/components/badge-button/engine.ts +85 -0
- package/src/components/badge-button/react.tsx +31 -0
- package/src/components/bento-grid/README.md +26 -0
- package/src/components/bento-grid/bento-grid.css +12 -0
- package/src/components/bento-grid/element.ts +22 -0
- package/src/components/bento-grid/engine.ts +153 -0
- package/src/components/bento-grid/react.tsx +25 -0
- package/src/components/code-block/README.md +1 -1
- package/src/components/code-block/code-block.css +5 -0
- package/src/components/code-block/engine.ts +5 -0
- package/src/components/command-search/README.md +1 -1
- package/src/components/command-search/command-search.css +12 -2
- package/src/components/command-search/engine.ts +6 -0
- package/src/components/dialog/README.md +104 -103
- package/src/components/dialog/dialog.css +2 -0
- package/src/components/dialog/engine.ts +465 -449
- package/src/components/drilldown-menu/drilldown-menu.css +11 -7
- package/src/components/drilldown-menu/engine.ts +44 -4
- package/src/components/equalizer-slider/README.md +43 -0
- package/src/components/equalizer-slider/element.ts +41 -0
- package/src/components/equalizer-slider/engine.ts +32 -0
- package/src/components/equalizer-slider/equalizer-slider.css +26 -0
- package/src/components/equalizer-slider/react.tsx +60 -0
- package/src/components/equalizer-slider/shared.ts +161 -0
- package/src/components/handwriting-text/README.md +29 -0
- package/src/components/handwriting-text/element.ts +14 -0
- package/src/components/handwriting-text/engine.ts +108 -0
- package/src/components/handwriting-text/glyphs.ts +71 -0
- package/src/components/handwriting-text/handwriting-text.css +14 -0
- package/src/components/handwriting-text/react.tsx +23 -0
- package/src/components/heatmap-chart/README.md +48 -48
- package/src/components/heatmap-chart/engine.ts +6 -1
- package/src/components/heatmap-chart/heatmap-chart.css +5 -1
- package/src/components/icons.ts +1 -0
- package/src/components/magnetize-button/README.md +26 -0
- package/src/components/magnetize-button/element.ts +13 -0
- package/src/components/magnetize-button/engine.ts +84 -0
- package/src/components/magnetize-button/magnetize-button.css +17 -0
- package/src/components/magnetize-button/react.tsx +22 -0
- package/src/components/morphing-page-dots/README.md +28 -0
- package/src/components/morphing-page-dots/element.ts +16 -0
- package/src/components/morphing-page-dots/engine.ts +89 -0
- package/src/components/morphing-page-dots/morphing-page-dots.css +12 -0
- package/src/components/morphing-page-dots/react.tsx +32 -0
- package/src/components/not-found/README.md +26 -0
- package/src/components/not-found/element.ts +15 -0
- package/src/components/not-found/engine.ts +57 -0
- package/src/components/not-found/not-found.css +21 -0
- package/src/components/not-found/react.tsx +23 -0
- package/src/components/notification-stack/README.md +24 -0
- package/src/components/notification-stack/element.ts +26 -0
- package/src/components/notification-stack/engine.ts +329 -0
- package/src/components/notification-stack/notification-stack.css +41 -0
- package/src/components/notification-stack/react.tsx +28 -0
- package/src/components/particle-button/README.md +26 -0
- package/src/components/particle-button/element.ts +13 -0
- package/src/components/particle-button/engine.ts +52 -0
- package/src/components/particle-button/particle-button.css +13 -0
- package/src/components/particle-button/react.tsx +22 -0
- package/src/components/radio-group/README.md +43 -0
- package/src/components/radio-group/element.ts +41 -0
- package/src/components/radio-group/engine.ts +11 -0
- package/src/components/radio-group/radio-group.css +28 -0
- package/src/components/radio-group/react.tsx +60 -0
- package/src/components/radio-group/shared.ts +118 -0
- package/src/components/rating-scale/README.md +43 -0
- package/src/components/rating-scale/element.ts +41 -0
- package/src/components/rating-scale/engine.ts +63 -0
- package/src/components/rating-scale/rating-scale.css +22 -0
- package/src/components/rating-scale/react.tsx +60 -0
- package/src/components/scroll-area/README.md +89 -0
- package/src/components/scroll-area/element.ts +35 -0
- package/src/components/scroll-area/engine.ts +528 -0
- package/src/components/scroll-area/react.tsx +67 -0
- package/src/components/scroll-area/scroll-area.css +260 -0
- package/src/components/scroll-preview-dock/README.md +24 -0
- package/src/components/scroll-preview-dock/element.ts +20 -0
- package/src/components/scroll-preview-dock/engine.ts +228 -0
- package/src/components/scroll-preview-dock/react.tsx +26 -0
- package/src/components/scroll-preview-dock/scroll-preview-dock.css +17 -0
- package/src/components/select/README.md +23 -9
- package/src/components/select/engine.ts +16 -4
- package/src/components/select/select.css +38 -4
- package/src/components/signature-pad/engine.ts +550 -437
- package/src/components/signature-pad/signature-pad.css +130 -103
- package/src/components/stepper/README.md +46 -0
- package/src/components/stepper/element.ts +42 -0
- package/src/components/stepper/engine.ts +103 -0
- package/src/components/stepper/react.tsx +60 -0
- package/src/components/stepper/stepper.css +35 -0
- package/src/components/sticky-header/README.md +30 -0
- package/src/components/sticky-header/element.ts +19 -0
- package/src/components/sticky-header/engine.ts +95 -0
- package/src/components/sticky-header/react.tsx +25 -0
- package/src/components/sticky-header/sticky-header.css +9 -0
- package/src/components/temperature-slider/README.md +42 -0
- package/src/components/temperature-slider/element.ts +53 -0
- package/src/components/temperature-slider/engine.ts +44 -0
- package/src/components/temperature-slider/react.tsx +66 -0
- package/src/components/temperature-slider/temperature-slider.css +29 -0
- package/src/components/voice-message-bubble/engine.ts +11 -0
- package/src/components/voice-message-bubble/voice-message-bubble.css +14 -12
- package/src/components/waveform-player/README.md +1 -1
- package/src/components/waveform-player/engine.ts +344 -261
- package/src/components/waveform-player/waveform-player.css +50 -35
- package/src/components/wheel-picker/README.md +3 -1
- package/src/components/wheel-picker/element.ts +12 -5
- package/src/components/wheel-picker/engine.ts +37 -8
- package/src/components/wheel-picker/wheel-picker.css +17 -16
- package/src/core/timing.ts +2 -0
- package/src/css/tokens.css +4 -0
- package/src/elements/index.ts +321 -273
- package/src/index.ts +312 -295
- package/src/motion/morph.ts +5 -2
- package/src/react/index.ts +17 -0
- package/dist/chunk-7IQTMF7Y.js.map +0 -1
- package/dist/chunk-DOOMULVV.js.map +0 -1
- package/dist/chunk-VJFPTLAK.js.map +0 -1
- /package/dist/{chunk-QFBOHPWZ.js.map → chunk-CQP66AZK.js.map} +0 -0
- /package/dist/{chunk-QP4QJOEN.js.map → chunk-H4STLMCM.js.map} +0 -0
- /package/dist/{chunk-BZLMCHUD.js.map → chunk-KDIJX6G4.js.map} +0 -0
|
@@ -1,437 +1,550 @@
|
|
|
1
|
-
import { gsap, EASE, registerMotion } from '../../core/gsap';
|
|
2
|
-
import { reduced } from '../../core/reduced';
|
|
3
|
-
import { createScope } from '../../core/scope';
|
|
4
|
-
import { inert, type Instance } from '../../core/types';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
export interface
|
|
9
|
-
export interface
|
|
10
|
-
export
|
|
11
|
-
export
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
color
|
|
36
|
-
|
|
37
|
-
width
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
readonly
|
|
51
|
-
readonly
|
|
52
|
-
readonly
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if (
|
|
82
|
-
if (
|
|
83
|
-
if (
|
|
84
|
-
if (
|
|
85
|
-
|
|
86
|
-
probe
|
|
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
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
path.
|
|
132
|
-
path.setAttribute('
|
|
133
|
-
path.setAttribute('
|
|
134
|
-
path.setAttribute('stroke
|
|
135
|
-
path.setAttribute('stroke-
|
|
136
|
-
path.setAttribute('stroke-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
let
|
|
155
|
-
let
|
|
156
|
-
let
|
|
157
|
-
|
|
158
|
-
const
|
|
159
|
-
const
|
|
160
|
-
const
|
|
161
|
-
wrapper
|
|
162
|
-
|
|
163
|
-
surface
|
|
164
|
-
|
|
165
|
-
svg
|
|
166
|
-
svg.
|
|
167
|
-
svg.setAttribute('
|
|
168
|
-
svg.setAttribute('
|
|
169
|
-
|
|
170
|
-
placeholder
|
|
171
|
-
placeholder.
|
|
172
|
-
placeholder.
|
|
173
|
-
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
let
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
};
|
|
234
|
-
const
|
|
235
|
-
const
|
|
236
|
-
if (
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
if (
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
};
|
|
253
|
-
const
|
|
254
|
-
if (
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
if (
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
if (!
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
if (scope.disposed
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
}
|
|
1
|
+
import { gsap, EASE, registerMotion } from '../../core/gsap';
|
|
2
|
+
import { reduced } from '../../core/reduced';
|
|
3
|
+
import { createScope } from '../../core/scope';
|
|
4
|
+
import { inert, type Instance } from '../../core/types';
|
|
5
|
+
import { press, blurIn, rollDigits } from '../../motion/micro';
|
|
6
|
+
|
|
7
|
+
/** Coordinates are fractions of the drawing area, independent of its size. */
|
|
8
|
+
export interface SignaturePoint { x: number; y: number }
|
|
9
|
+
export interface SignatureStroke { points: SignaturePoint[]; color: string; width: number }
|
|
10
|
+
export interface SignatureData { version: 1; strokes: SignatureStroke[] }
|
|
11
|
+
export type SignaturePadChangeReason = 'draw' | 'undo' | 'redo' | 'clear' | 'import' | 'reset';
|
|
12
|
+
export interface SignaturePadChangeDetail {
|
|
13
|
+
data: SignatureData;
|
|
14
|
+
reason: SignaturePadChangeReason;
|
|
15
|
+
isEmpty: boolean;
|
|
16
|
+
canUndo: boolean;
|
|
17
|
+
canRedo: boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface SignatureExportOptions {
|
|
20
|
+
/** CSS pixels; default is the current drawing area, or 640 × 240 before layout. */
|
|
21
|
+
width?: number;
|
|
22
|
+
height?: number;
|
|
23
|
+
/** Omit for transparency. Accepts a literal CSS color, without variables or URLs. */
|
|
24
|
+
background?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface SignaturePNGOptions extends SignatureExportOptions {
|
|
27
|
+
/** Raster resolution multiplier, clamped to 1–4 (default 2). */
|
|
28
|
+
scale?: number;
|
|
29
|
+
}
|
|
30
|
+
export interface SignaturePadOptions {
|
|
31
|
+
data?: SignatureData;
|
|
32
|
+
label?: string;
|
|
33
|
+
hint?: string;
|
|
34
|
+
placeholder?: string;
|
|
35
|
+
/** Literal CSS ink color; defaults to the theme's text color. */
|
|
36
|
+
color?: string;
|
|
37
|
+
/** Ink width in CSS pixels, default 2.4. */
|
|
38
|
+
width?: number;
|
|
39
|
+
disabled?: boolean;
|
|
40
|
+
readOnly?: boolean;
|
|
41
|
+
/** Include native undo, redo and clear buttons (default true). */
|
|
42
|
+
toolbar?: boolean;
|
|
43
|
+
/** Adds a hidden input containing the versioned JSON data. */
|
|
44
|
+
name?: string;
|
|
45
|
+
/** Maximum undo steps, clamped to 1–100 (default 50). */
|
|
46
|
+
historyLimit?: number;
|
|
47
|
+
onChange?: (data: SignatureData, detail: SignaturePadChangeDetail) => void;
|
|
48
|
+
}
|
|
49
|
+
export interface SignaturePadInstance extends Instance {
|
|
50
|
+
readonly data: SignatureData;
|
|
51
|
+
readonly isEmpty: boolean;
|
|
52
|
+
readonly canUndo: boolean;
|
|
53
|
+
readonly canRedo: boolean;
|
|
54
|
+
undo(): void;
|
|
55
|
+
redo(): void;
|
|
56
|
+
clear(): void;
|
|
57
|
+
/** Invalid input returns false and leaves the signature untouched. Import resets history. */
|
|
58
|
+
importData(data: unknown, notify?: boolean): boolean;
|
|
59
|
+
exportData(): SignatureData;
|
|
60
|
+
exportSVG(options?: SignatureExportOptions): string;
|
|
61
|
+
/** Returns null when rasterization is unavailable or fails. */
|
|
62
|
+
exportPNG(options?: SignaturePNGOptions): Promise<Blob | null>;
|
|
63
|
+
setDisabled(disabled: boolean): void;
|
|
64
|
+
setReadOnly(readOnly: boolean): void;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
68
|
+
const MAX_STROKES = 500;
|
|
69
|
+
const MAX_POINTS = 50_000;
|
|
70
|
+
const emptyData = (): SignatureData => ({ version: 1, strokes: [] });
|
|
71
|
+
const cloneStrokes = (strokes: SignatureStroke[]) => strokes.map((stroke) => ({ ...stroke, points: stroke.points.map((point) => ({ ...point })) }));
|
|
72
|
+
const sameStrokes = (left: SignatureStroke[], right: SignatureStroke[]) => left.length === right.length && left.every((stroke, index) => {
|
|
73
|
+
const other = right[index]!;
|
|
74
|
+
return stroke.color === other.color && stroke.width === other.width && stroke.points.length === other.points.length && stroke.points.every((point, index) => point.x === other.points[index]!.x && point.y === other.points[index]!.y);
|
|
75
|
+
});
|
|
76
|
+
const finite = (value: unknown): value is number => typeof value === 'number' && Number.isFinite(value);
|
|
77
|
+
const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));
|
|
78
|
+
|
|
79
|
+
/** Reject paint servers, CSS variables and markup so exports stay self-contained. */
|
|
80
|
+
function validColor(value: unknown): value is string {
|
|
81
|
+
if (typeof value !== 'string' || value.length > 100) return false;
|
|
82
|
+
if (/^#(?:[\da-f]{3,4}|[\da-f]{6}|[\da-f]{8})$/i.test(value)) return true;
|
|
83
|
+
if (!/^(?:rgba?|hsla?)\([\d\s.,%/+\-degratun]+\)$/i.test(value) && !/^[a-z]+$/i.test(value)) return false;
|
|
84
|
+
if (/^(?:inherit|initial|unset|revert|revertlayer|currentcolor)$/i.test(value)) return false;
|
|
85
|
+
if (typeof document === 'undefined') return /^(?:black|white|transparent)$/i.test(value);
|
|
86
|
+
const probe = document.createElement('span');
|
|
87
|
+
probe.style.color = value;
|
|
88
|
+
return probe.style.color !== '';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function validatedData(value: unknown): SignatureData | null {
|
|
92
|
+
if (!value || typeof value !== 'object') return null;
|
|
93
|
+
const data = value as Record<string, unknown>;
|
|
94
|
+
if (data.version !== 1 || !Array.isArray(data.strokes) || data.strokes.length > MAX_STROKES) return null;
|
|
95
|
+
const strokes: SignatureStroke[] = [];
|
|
96
|
+
let count = 0;
|
|
97
|
+
for (const candidate of data.strokes) {
|
|
98
|
+
if (!candidate || typeof candidate !== 'object') return null;
|
|
99
|
+
const stroke = candidate as Record<string, unknown>;
|
|
100
|
+
if (!validColor(stroke.color) || !finite(stroke.width) || stroke.width < 0.1 || stroke.width > 64 || !Array.isArray(stroke.points) || !stroke.points.length) return null;
|
|
101
|
+
count += stroke.points.length;
|
|
102
|
+
if (count > MAX_POINTS) return null;
|
|
103
|
+
const points: SignaturePoint[] = [];
|
|
104
|
+
for (const candidate of stroke.points) {
|
|
105
|
+
if (!candidate || typeof candidate !== 'object') return null;
|
|
106
|
+
const point = candidate as Record<string, unknown>;
|
|
107
|
+
if (!finite(point.x) || !finite(point.y) || point.x < 0 || point.x > 1 || point.y < 0 || point.y > 1) return null;
|
|
108
|
+
points.push({ x: point.x, y: point.y });
|
|
109
|
+
}
|
|
110
|
+
strokes.push({ color: stroke.color, width: stroke.width, points });
|
|
111
|
+
}
|
|
112
|
+
return { version: 1, strokes };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const rounded = (value: number) => Number(value.toFixed(3));
|
|
116
|
+
function strokePath(points: SignaturePoint[], width: number, height: number) {
|
|
117
|
+
const coordinates = points.map((point) => [rounded(point.x * width), rounded(point.y * height)] as const);
|
|
118
|
+
const first = coordinates[0]!;
|
|
119
|
+
let path = `M ${first[0]} ${first[1]}`;
|
|
120
|
+
if (coordinates.length === 1) return `${path} l 0.001 0`;
|
|
121
|
+
for (let i = 1; i < coordinates.length - 1; i++) {
|
|
122
|
+
const current = coordinates[i]!;
|
|
123
|
+
const next = coordinates[i + 1]!;
|
|
124
|
+
path += ` Q ${current[0]} ${current[1]} ${rounded((current[0] + next[0]) / 2)} ${rounded((current[1] + next[1]) / 2)}`;
|
|
125
|
+
}
|
|
126
|
+
const last = coordinates[coordinates.length - 1]!;
|
|
127
|
+
return `${path} L ${last[0]} ${last[1]}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function makePath(stroke: SignatureStroke, width: number, height: number) {
|
|
131
|
+
const path = document.createElementNS(SVG_NS, 'path');
|
|
132
|
+
path.setAttribute('d', strokePath(stroke.points, width, height));
|
|
133
|
+
path.setAttribute('fill', 'none');
|
|
134
|
+
path.setAttribute('stroke', stroke.color);
|
|
135
|
+
path.setAttribute('stroke-width', String(stroke.width));
|
|
136
|
+
path.setAttribute('stroke-linecap', 'round');
|
|
137
|
+
path.setAttribute('stroke-linejoin', 'round');
|
|
138
|
+
return path;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let nextId = 0;
|
|
142
|
+
|
|
143
|
+
/** A responsive SVG drawing pad. Drawing is immediate; only toolbar feedback uses motion. */
|
|
144
|
+
export function createSignaturePad(root: HTMLElement | null, options: SignaturePadOptions = {}): SignaturePadInstance {
|
|
145
|
+
if (!root) return inert<SignaturePadInstance>({
|
|
146
|
+
data: emptyData(), isEmpty: true, canUndo: false, canRedo: false,
|
|
147
|
+
undo() {}, redo() {}, clear() {}, importData: () => false, exportData: emptyData,
|
|
148
|
+
exportSVG: () => '', exportPNG: async () => null, setDisabled() {}, setReadOnly() {},
|
|
149
|
+
});
|
|
150
|
+
registerMotion();
|
|
151
|
+
const scope = createScope();
|
|
152
|
+
let strokes = validatedData(options.data)?.strokes ?? [];
|
|
153
|
+
const initialStrokes = cloneStrokes(strokes);
|
|
154
|
+
let disabled = Boolean(options.disabled);
|
|
155
|
+
let readOnly = Boolean(options.readOnly);
|
|
156
|
+
let undoStack: SignatureStroke[][] = [];
|
|
157
|
+
let redoStack: SignatureStroke[][] = [];
|
|
158
|
+
const historyLimit = finite(options.historyLimit) ? clamp(Math.floor(options.historyLimit), 1, 100) : 50;
|
|
159
|
+
const inkWidth = finite(options.width) ? clamp(options.width, 0.1, 64) : 2.4;
|
|
160
|
+
const exportData = (): SignatureData => ({ version: 1, strokes: cloneStrokes(strokes) });
|
|
161
|
+
const wrapper = document.createElement('div');
|
|
162
|
+
wrapper.dataset.mkSignatureBody = '';
|
|
163
|
+
const surface = document.createElement('div');
|
|
164
|
+
surface.dataset.mkSignatureSurface = '';
|
|
165
|
+
const svg = document.createElementNS(SVG_NS, 'svg');
|
|
166
|
+
svg.dataset.mkSignatureDrawing = '';
|
|
167
|
+
svg.setAttribute('role', 'img');
|
|
168
|
+
svg.setAttribute('aria-label', options.label ?? 'Área de firma');
|
|
169
|
+
svg.setAttribute('preserveAspectRatio', 'none');
|
|
170
|
+
const placeholder = document.createElement('span');
|
|
171
|
+
placeholder.dataset.mkSignaturePlaceholder = '';
|
|
172
|
+
placeholder.setAttribute('aria-hidden', 'true');
|
|
173
|
+
placeholder.textContent = options.placeholder ?? 'Firma aquí';
|
|
174
|
+
const pen = document.createElement('span');
|
|
175
|
+
pen.dataset.mkSignatureCursor = ''; pen.setAttribute('aria-hidden', 'true');
|
|
176
|
+
// Removed ink leaves through this overlay, so the drawing always holds the current value.
|
|
177
|
+
const ghost = document.createElementNS(SVG_NS, 'svg');
|
|
178
|
+
ghost.setAttribute('data-mk-signature-ghost', '');
|
|
179
|
+
ghost.setAttribute('aria-hidden', 'true');
|
|
180
|
+
ghost.setAttribute('preserveAspectRatio', 'none');
|
|
181
|
+
surface.append(svg, ghost, placeholder, pen);
|
|
182
|
+
const footer = document.createElement('div');
|
|
183
|
+
footer.dataset.mkSignatureFooter = '';
|
|
184
|
+
const toolbar = document.createElement('div');
|
|
185
|
+
toolbar.dataset.mkSignatureToolbar = '';
|
|
186
|
+
toolbar.setAttribute('role', 'group');
|
|
187
|
+
toolbar.setAttribute('aria-label', 'Herramientas de firma');
|
|
188
|
+
const makeButton = (action: string, text: string) => {
|
|
189
|
+
const button = document.createElement('button');
|
|
190
|
+
button.type = 'button';
|
|
191
|
+
button.dataset.mkSignatureAction = action;
|
|
192
|
+
const paths: Record<string, string> = { undo: 'M9 5 4 10l5 5M4 10h9a6 6 0 0 1 6 6', redo: 'm15 5 5 5-5 5m5-5h-9a6 6 0 0 0-6 6', clear: 'm9 5 1-2h4l1 2M4 6h16M7 6l1 15h8l1-15M10 10v7m4-7v7' };
|
|
193
|
+
button.innerHTML = `<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="${paths[action]}"/></svg>`;
|
|
194
|
+
const caption = document.createElement('span'); caption.textContent = text; button.append(caption);
|
|
195
|
+
toolbar.append(button);
|
|
196
|
+
return button;
|
|
197
|
+
};
|
|
198
|
+
const undoButton = makeButton('undo', 'Deshacer');
|
|
199
|
+
const redoButton = makeButton('redo', 'Rehacer');
|
|
200
|
+
const clearButton = makeButton('clear', 'Limpiar');
|
|
201
|
+
if (options.toolbar !== false) footer.append(toolbar);
|
|
202
|
+
const status = document.createElement('span');
|
|
203
|
+
status.dataset.mkSignatureStatus = '';
|
|
204
|
+
status.setAttribute('role', 'status');
|
|
205
|
+
status.setAttribute('aria-live', 'polite');
|
|
206
|
+
const heading = document.createElement('div'); heading.dataset.mkSignatureHeader = '';
|
|
207
|
+
const title = document.createElement('span'); title.textContent = options.label ?? 'Tu firma';
|
|
208
|
+
heading.append(title, status);
|
|
209
|
+
const hint = document.createElement('p');
|
|
210
|
+
hint.dataset.mkSignatureHint = '';
|
|
211
|
+
hint.id = `mk-signature-hint-${++nextId}`;
|
|
212
|
+
hint.textContent = options.hint ?? 'Dibuja con el ratón, el dedo o un lápiz. Puedes deshacer cada trazo.';
|
|
213
|
+
svg.setAttribute('aria-describedby', hint.id);
|
|
214
|
+
wrapper.append(heading, surface, footer, hint);
|
|
215
|
+
let hidden: HTMLInputElement | null = null;
|
|
216
|
+
if (options.name) {
|
|
217
|
+
hidden = document.createElement('input');
|
|
218
|
+
hidden.type = 'hidden';
|
|
219
|
+
hidden.name = options.name;
|
|
220
|
+
wrapper.append(hidden);
|
|
221
|
+
}
|
|
222
|
+
root.append(wrapper);
|
|
223
|
+
scope.add(() => wrapper.remove());
|
|
224
|
+
for (const name of ['data-mk-disabled', 'data-mk-readonly', 'data-mk-drawing', 'data-mk-empty', 'data-mk-pen-visible']) scope.attr(root, name, null);
|
|
225
|
+
scope.attr(root, 'data-mk-ready', '');
|
|
226
|
+
let dimensions = { width: 640, height: 240 };
|
|
227
|
+
let active: { pointerId: number; stroke: SignatureStroke; path: SVGPathElement } | null = null;
|
|
228
|
+
const effects = new Set<gsap.core.Animation>();
|
|
229
|
+
// Only the stroke count rolls; the status line is rewritten in full when the pad
|
|
230
|
+
// turns empty or signed, so repeated strokes never blur the whole label.
|
|
231
|
+
const statusCount = document.createElement('span');
|
|
232
|
+
const statusLabel = document.createElement('span');
|
|
233
|
+
let shown: { count: number; readOnly: boolean } | null = null;
|
|
234
|
+
const paintStatus = () => {
|
|
235
|
+
const count = strokes.length;
|
|
236
|
+
if (shown && shown.count === count && shown.readOnly === readOnly) return;
|
|
237
|
+
const before = shown;
|
|
238
|
+
shown = { count, readOnly };
|
|
239
|
+
const animate = before !== null && !reduced();
|
|
240
|
+
if (!count) {
|
|
241
|
+
status.textContent = 'Sin firma';
|
|
242
|
+
if (animate && before.count) keep(blurIn(status, { from: 'soft', duration: .25 }));
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
statusLabel.replaceChildren(statusCount, ` ${count === 1 ? 'trazo' : 'trazos'} · ${readOnly ? 'Solo lectura' : 'Firma capturada'}`);
|
|
246
|
+
status.replaceChildren(statusLabel);
|
|
247
|
+
if (animate && before.count) keep(rollDigits(statusCount, String(count), count > before.count ? 1 : -1, .3));
|
|
248
|
+
else {
|
|
249
|
+
statusCount.textContent = String(count);
|
|
250
|
+
if (animate) keep(blurIn(status, { from: 'soft', duration: .25 }));
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
const keep = (effect: gsap.core.Animation | null) => {
|
|
254
|
+
for (const animation of effects) if (animation.totalProgress() === 1) effects.delete(animation);
|
|
255
|
+
if (effect) effects.add(effect);
|
|
256
|
+
};
|
|
257
|
+
const locked = () => disabled || readOnly;
|
|
258
|
+
|
|
259
|
+
const update = () => {
|
|
260
|
+
root.toggleAttribute('data-mk-empty', !strokes.length && !active);
|
|
261
|
+
root.toggleAttribute('data-mk-disabled', disabled);
|
|
262
|
+
root.toggleAttribute('data-mk-readonly', readOnly);
|
|
263
|
+
svg.setAttribute('aria-disabled', String(disabled));
|
|
264
|
+
placeholder.hidden = strokes.length > 0 || active !== null;
|
|
265
|
+
undoButton.disabled = locked() || !undoStack.length;
|
|
266
|
+
redoButton.disabled = locked() || !redoStack.length;
|
|
267
|
+
clearButton.disabled = locked() || !strokes.length;
|
|
268
|
+
paintStatus();
|
|
269
|
+
if (hidden) {
|
|
270
|
+
hidden.disabled = disabled;
|
|
271
|
+
hidden.value = JSON.stringify(exportData());
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
const render = () => {
|
|
275
|
+
const rect = surface.getBoundingClientRect();
|
|
276
|
+
if (rect.width > 0 && rect.height > 0) dimensions = { width: rect.width, height: rect.height };
|
|
277
|
+
svg.setAttribute('viewBox', `0 0 ${dimensions.width} ${dimensions.height}`);
|
|
278
|
+
svg.replaceChildren(...strokes.map((stroke) => makePath(stroke, dimensions.width, dimensions.height)));
|
|
279
|
+
if (active) {
|
|
280
|
+
active.path = makePath(active.stroke, dimensions.width, dimensions.height);
|
|
281
|
+
svg.append(active.path);
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
const emit = (reason: SignaturePadChangeReason) => {
|
|
285
|
+
const detail: SignaturePadChangeDetail = { data: exportData(), reason, isEmpty: !strokes.length, canUndo: !!undoStack.length, canRedo: !!redoStack.length };
|
|
286
|
+
options.onChange?.(exportData(), { ...detail, data: exportData() });
|
|
287
|
+
if (scope.disposed) return;
|
|
288
|
+
root.dispatchEvent(new CustomEvent('mk:change', { bubbles: true, detail }));
|
|
289
|
+
};
|
|
290
|
+
const release = (pointerId: number) => {
|
|
291
|
+
try { surface.releasePointerCapture?.(pointerId); } catch { /* The pointer may already be released. */ }
|
|
292
|
+
};
|
|
293
|
+
const cancel = () => {
|
|
294
|
+
if (!active) return;
|
|
295
|
+
const pointerId = active.pointerId;
|
|
296
|
+
active = null;
|
|
297
|
+
root.removeAttribute('data-mk-drawing');
|
|
298
|
+
release(pointerId);
|
|
299
|
+
render();
|
|
300
|
+
update();
|
|
301
|
+
};
|
|
302
|
+
const remember = () => {
|
|
303
|
+
undoStack.push(strokes);
|
|
304
|
+
if (undoStack.length > historyLimit) undoStack.shift();
|
|
305
|
+
redoStack = [];
|
|
306
|
+
};
|
|
307
|
+
const pulse = (button: HTMLButtonElement) => {
|
|
308
|
+
keep(press(button));
|
|
309
|
+
// Undo and redo rewind their arrow; clear tips the bin's lid.
|
|
310
|
+
const icon = button.querySelector('svg');
|
|
311
|
+
const turn = button === undoButton ? -50 : button === redoButton ? 50 : -14;
|
|
312
|
+
if (icon && !reduced()) keep(gsap.fromTo(icon, { rotate: turn }, { rotate: 0, duration: .5, ease: EASE.click, overwrite: 'auto', clearProps: 'transform' }));
|
|
313
|
+
};
|
|
314
|
+
// The value changes first. An undone stroke retracts along its path back to where
|
|
315
|
+
// it began, a clear sweeps the page like an eraser from left to right, and
|
|
316
|
+
// returning ink is written again at pen pace. The overlay never takes input.
|
|
317
|
+
const wipeId = `mk-signature-wipe-${++nextId}`;
|
|
318
|
+
let wipes = 0;
|
|
319
|
+
const animateInk = (before: SignatureStroke[], reason: 'undo' | 'redo' | 'clear') => {
|
|
320
|
+
if (reduced()) return;
|
|
321
|
+
const { width, height } = dimensions;
|
|
322
|
+
const byX = (a: SignatureStroke, b: SignatureStroke) => (a.points[0]?.x ?? 0) - (b.points[0]?.x ?? 0);
|
|
323
|
+
const removed = before.filter((stroke) => !strokes.includes(stroke)).sort(byX);
|
|
324
|
+
const added = strokes.filter((stroke) => !before.includes(stroke)).sort(byX);
|
|
325
|
+
ghost.setAttribute('viewBox', `0 0 ${width} ${height}`);
|
|
326
|
+
if (removed.length && reason === 'clear') {
|
|
327
|
+
const svgNode = <K extends keyof SVGElementTagNameMap>(tag: K, attributes: Record<string, string | number>) => {
|
|
328
|
+
const node = document.createElementNS(SVG_NS, tag);
|
|
329
|
+
for (const [name, value] of Object.entries(attributes)) node.setAttribute(name, String(value));
|
|
330
|
+
return node;
|
|
331
|
+
};
|
|
332
|
+
// The eraser only travels across the ink, so its time is spent on the signature.
|
|
333
|
+
const points = removed.flatMap((stroke) => stroke.points);
|
|
334
|
+
const pad = inkWidth * 2 + 8;
|
|
335
|
+
const left = Math.min(...points.map((point) => point.x)) * width - pad;
|
|
336
|
+
const right = Math.max(...points.map((point) => point.x)) * width + pad;
|
|
337
|
+
const top = Math.max(0, Math.min(...points.map((point) => point.y)) * height - pad);
|
|
338
|
+
const bottom = Math.min(height, Math.max(...points.map((point) => point.y)) * height + pad);
|
|
339
|
+
const id = `${wipeId}-${++wipes}`;
|
|
340
|
+
const clip = svgNode('clipPath', { id });
|
|
341
|
+
const reveal = svgNode('rect', { x: left, y: 0, width: width * 2, height });
|
|
342
|
+
clip.append(reveal);
|
|
343
|
+
const ink = svgNode('g', { 'clip-path': `url(#${id})` });
|
|
344
|
+
ink.append(...removed.map((stroke) => makePath(stroke, width, height)));
|
|
345
|
+
const edge = svgNode('rect', { x: left, y: top, width: 2, height: Math.max(24, bottom - top), rx: 1, fill: 'currentColor', opacity: 0 });
|
|
346
|
+
ghost.append(clip, ink, edge);
|
|
347
|
+
const done = () => { clip.remove(); ink.remove(); edge.remove(); };
|
|
348
|
+
const sweep = Math.min(.6, Math.max(.36, (right - left) / 900));
|
|
349
|
+
keep(gsap.timeline({ onComplete: done, onInterrupt: done })
|
|
350
|
+
.to([reveal, edge], { attr: { x: right }, duration: sweep, ease: 'power2.inOut' }, 0)
|
|
351
|
+
.to(edge, { opacity: .3, duration: .08 }, 0)
|
|
352
|
+
.to(edge, { opacity: 0, duration: .14 }, sweep - .12));
|
|
353
|
+
} else removed.forEach((stroke) => {
|
|
354
|
+
const path = makePath(stroke, width, height);
|
|
355
|
+
ghost.append(path);
|
|
356
|
+
const length = path.getTotalLength?.() ?? 0;
|
|
357
|
+
const done = () => path.remove();
|
|
358
|
+
if (!length) { keep(gsap.to(path, { opacity: 0, duration: .25, onComplete: done })); return; }
|
|
359
|
+
path.style.strokeDasharray = `${length} ${length}`;
|
|
360
|
+
keep(gsap.timeline({ onComplete: done, onInterrupt: done })
|
|
361
|
+
.fromTo(path, { strokeDashoffset: 0 }, { strokeDashoffset: length, duration: .5, ease: 'power2.inOut' }, 0)
|
|
362
|
+
.to(path, { opacity: 0, duration: .16 }, .36));
|
|
363
|
+
});
|
|
364
|
+
const rendered = [...svg.children] as SVGPathElement[];
|
|
365
|
+
added.forEach((stroke, index) => {
|
|
366
|
+
const path = rendered[strokes.indexOf(stroke)];
|
|
367
|
+
const length = path?.getTotalLength?.() ?? 0;
|
|
368
|
+
if (!path || !length) return;
|
|
369
|
+
path.style.strokeDasharray = `${length} ${length}`;
|
|
370
|
+
keep(gsap.fromTo(path, { strokeDashoffset: length }, { strokeDashoffset: 0, duration: .6, delay: Math.min(index * .08, .48), ease: EASE.wipe, clearProps: 'strokeDasharray,strokeDashoffset' }));
|
|
371
|
+
});
|
|
372
|
+
};
|
|
373
|
+
const undo = () => {
|
|
374
|
+
if (scope.disposed) return;
|
|
375
|
+
cancel();
|
|
376
|
+
if (!undoStack.length) return;
|
|
377
|
+
const before = strokes;
|
|
378
|
+
redoStack.push(strokes);
|
|
379
|
+
strokes = undoStack.pop()!;
|
|
380
|
+
render(); update(); emit('undo'); animateInk(before, 'undo');
|
|
381
|
+
};
|
|
382
|
+
const redo = () => {
|
|
383
|
+
if (scope.disposed) return;
|
|
384
|
+
cancel();
|
|
385
|
+
if (!redoStack.length) return;
|
|
386
|
+
const before = strokes;
|
|
387
|
+
undoStack.push(strokes);
|
|
388
|
+
strokes = redoStack.pop()!;
|
|
389
|
+
render(); update(); emit('redo'); animateInk(before, 'redo');
|
|
390
|
+
};
|
|
391
|
+
const clear = () => {
|
|
392
|
+
if (scope.disposed) return;
|
|
393
|
+
cancel();
|
|
394
|
+
if (!strokes.length) return;
|
|
395
|
+
const before = strokes;
|
|
396
|
+
remember(); strokes = [];
|
|
397
|
+
render(); update(); emit('clear'); animateInk(before, 'clear');
|
|
398
|
+
};
|
|
399
|
+
const form = hidden?.form;
|
|
400
|
+
if (form) scope.on(form, 'reset', (event) => {
|
|
401
|
+
// Native reset runs after dispatch. Waiting also observes preventDefault from later listeners.
|
|
402
|
+
scope.timeout(() => {
|
|
403
|
+
if (scope.disposed || event.defaultPrevented || hidden?.form !== form) return;
|
|
404
|
+
const changed = !sameStrokes(strokes, initialStrokes);
|
|
405
|
+
cancel(); strokes = cloneStrokes(initialStrokes); undoStack = []; redoStack = [];
|
|
406
|
+
render(); update();
|
|
407
|
+
if (changed) emit('reset');
|
|
408
|
+
}, 0);
|
|
409
|
+
});
|
|
410
|
+
const pointFrom = (event: PointerEvent): SignaturePoint | null => {
|
|
411
|
+
const rect = surface.getBoundingClientRect();
|
|
412
|
+
if (!rect.width || !rect.height || !finite(event.clientX) || !finite(event.clientY)) return null;
|
|
413
|
+
return { x: clamp((event.clientX - rect.left) / rect.width, 0, 1), y: clamp((event.clientY - rect.top) / rect.height, 0, 1) };
|
|
414
|
+
};
|
|
415
|
+
const addPoint = (event: PointerEvent) => {
|
|
416
|
+
if (!active) return;
|
|
417
|
+
const point = pointFrom(event);
|
|
418
|
+
if (!point || active.stroke.points.length >= MAX_POINTS - strokes.reduce((sum, stroke) => sum + stroke.points.length, 0)) return;
|
|
419
|
+
const last = active.stroke.points[active.stroke.points.length - 1]!;
|
|
420
|
+
if (point.x === last.x && point.y === last.y) return;
|
|
421
|
+
active.stroke.points.push(point);
|
|
422
|
+
active.path.setAttribute('d', strokePath(active.stroke.points, dimensions.width, dimensions.height));
|
|
423
|
+
};
|
|
424
|
+
scope.on(surface, 'pointerdown', (event) => {
|
|
425
|
+
const pointer = event as PointerEvent;
|
|
426
|
+
if (locked() || active || pointer.button !== 0 || pointer.isPrimary === false || strokes.length >= MAX_STROKES || strokes.reduce((sum, stroke) => sum + stroke.points.length, 0) >= MAX_POINTS) return;
|
|
427
|
+
const point = pointFrom(pointer);
|
|
428
|
+
if (!point) return;
|
|
429
|
+
pointer.preventDefault();
|
|
430
|
+
const themed = getComputedStyle(root).getPropertyValue('--mk-signature-ink').trim() || getComputedStyle(root).color;
|
|
431
|
+
const color = validColor(options.color) ? options.color : validColor(themed) ? themed : '#252735';
|
|
432
|
+
const stroke: SignatureStroke = { points: [point], color, width: inkWidth };
|
|
433
|
+
active = { pointerId: pointer.pointerId, stroke, path: makePath(stroke, dimensions.width, dimensions.height) };
|
|
434
|
+
render(); update();
|
|
435
|
+
root.setAttribute('data-mk-drawing', '');
|
|
436
|
+
try { surface.setPointerCapture?.(pointer.pointerId); } catch { /* Synthetic events have no active pointer. */ }
|
|
437
|
+
});
|
|
438
|
+
// Window listeners also finish safely when capture is unavailable in an embedded browser.
|
|
439
|
+
scope.on(window, 'pointermove', (event) => {
|
|
440
|
+
const pointer = event as PointerEvent;
|
|
441
|
+
if (!active || pointer.pointerId !== active.pointerId) return;
|
|
442
|
+
pointer.preventDefault();
|
|
443
|
+
const samples = typeof pointer.getCoalescedEvents === 'function' ? pointer.getCoalescedEvents() : [];
|
|
444
|
+
for (const sample of samples) addPoint(sample);
|
|
445
|
+
addPoint(pointer);
|
|
446
|
+
}, { passive: false });
|
|
447
|
+
scope.on(window, 'pointerup', (event) => {
|
|
448
|
+
const pointer = event as PointerEvent;
|
|
449
|
+
if (!active || pointer.pointerId !== active.pointerId) return;
|
|
450
|
+
addPoint(pointer);
|
|
451
|
+
const completed = active;
|
|
452
|
+
active = null;
|
|
453
|
+
root.removeAttribute('data-mk-drawing');
|
|
454
|
+
release(completed.pointerId);
|
|
455
|
+
remember(); strokes = [...strokes, completed.stroke];
|
|
456
|
+
update(); emit('draw');
|
|
457
|
+
});
|
|
458
|
+
for (const type of ['pointercancel', 'lostpointercapture']) scope.on(type === 'pointercancel' ? window : surface, type, (event) => {
|
|
459
|
+
if (active?.pointerId === (event as PointerEvent).pointerId) cancel();
|
|
460
|
+
});
|
|
461
|
+
scope.on(window, 'blur', cancel);
|
|
462
|
+
const movePen = (event: Event) => {
|
|
463
|
+
const pointer = event as PointerEvent;
|
|
464
|
+
const rect = surface.getBoundingClientRect();
|
|
465
|
+
root.toggleAttribute('data-mk-pen-visible', pointer.pointerType !== 'touch' && !locked());
|
|
466
|
+
pen.style.left = `${pointer.clientX - rect.left}px`; pen.style.top = `${pointer.clientY - rect.top}px`;
|
|
467
|
+
};
|
|
468
|
+
scope.on(surface, 'pointerenter', movePen);
|
|
469
|
+
scope.on(surface, 'pointermove', movePen);
|
|
470
|
+
scope.on(surface, 'pointerleave', () => root.removeAttribute('data-mk-pen-visible'));
|
|
471
|
+
scope.on(document, 'visibilitychange', () => { if (document.hidden) cancel(); });
|
|
472
|
+
for (const [button, action] of [[undoButton, undo], [redoButton, redo], [clearButton, clear]] as const) scope.on(button, 'click', () => {
|
|
473
|
+
if (locked() || button.disabled) return;
|
|
474
|
+
pulse(button); action();
|
|
475
|
+
});
|
|
476
|
+
if (typeof ResizeObserver === 'function') scope.observe(new ResizeObserver(render)).observe(surface);
|
|
477
|
+
scope.on(window, 'resize', render);
|
|
478
|
+
scope.add(() => { cancel(); for (const effect of effects) effect.kill(); });
|
|
479
|
+
render(); update();
|
|
480
|
+
|
|
481
|
+
const exportSVG = (settings: SignatureExportOptions = {}) => {
|
|
482
|
+
const width = finite(settings.width) ? clamp(settings.width, 1, 4096) : dimensions.width;
|
|
483
|
+
const height = finite(settings.height) ? clamp(settings.height, 1, 4096) : dimensions.height;
|
|
484
|
+
const output = document.createElementNS(SVG_NS, 'svg');
|
|
485
|
+
output.setAttribute('width', String(rounded(width)));
|
|
486
|
+
output.setAttribute('height', String(rounded(height)));
|
|
487
|
+
output.setAttribute('viewBox', `0 0 ${width} ${height}`);
|
|
488
|
+
if (validColor(settings.background)) {
|
|
489
|
+
const background = document.createElementNS(SVG_NS, 'rect');
|
|
490
|
+
background.setAttribute('width', '100%'); background.setAttribute('height', '100%'); background.setAttribute('fill', settings.background);
|
|
491
|
+
output.append(background);
|
|
492
|
+
}
|
|
493
|
+
output.append(...strokes.map((stroke) => makePath(stroke, width, height)));
|
|
494
|
+
return new XMLSerializer().serializeToString(output);
|
|
495
|
+
};
|
|
496
|
+
const pending = new Set<() => void>();
|
|
497
|
+
scope.add(() => { for (const cancelExport of pending) cancelExport(); pending.clear(); });
|
|
498
|
+
return {
|
|
499
|
+
get data() { return exportData(); },
|
|
500
|
+
get isEmpty() { return !strokes.length; },
|
|
501
|
+
get canUndo() { return !!undoStack.length; },
|
|
502
|
+
get canRedo() { return !!redoStack.length; },
|
|
503
|
+
undo, redo, clear, exportData,
|
|
504
|
+
importData(value, notify = true) {
|
|
505
|
+
if (scope.disposed) return false;
|
|
506
|
+
const next = validatedData(value);
|
|
507
|
+
if (!next) return false;
|
|
508
|
+
if (sameStrokes(next.strokes, strokes)) return true;
|
|
509
|
+
cancel(); strokes = next.strokes; undoStack = []; redoStack = [];
|
|
510
|
+
render(); update();
|
|
511
|
+
if (notify) emit('import');
|
|
512
|
+
return true;
|
|
513
|
+
},
|
|
514
|
+
exportSVG: (settings) => scope.disposed ? '' : exportSVG(settings),
|
|
515
|
+
exportPNG(settings = {}) {
|
|
516
|
+
if (scope.disposed) return Promise.resolve(null);
|
|
517
|
+
const canvas = document.createElement('canvas');
|
|
518
|
+
let context: CanvasRenderingContext2D | null;
|
|
519
|
+
try { context = canvas.getContext('2d'); }
|
|
520
|
+
catch { return Promise.resolve(null); }
|
|
521
|
+
if (!context || typeof canvas.toBlob !== 'function') return Promise.resolve(null);
|
|
522
|
+
const scale = finite(settings.scale) ? clamp(settings.scale, 1, 4) : 2;
|
|
523
|
+
canvas.width = Math.round((finite(settings.width) ? clamp(settings.width, 1, 4096) : dimensions.width) * scale);
|
|
524
|
+
canvas.height = Math.round((finite(settings.height) ? clamp(settings.height, 1, 4096) : dimensions.height) * scale);
|
|
525
|
+
return new Promise<Blob | null>((resolve) => {
|
|
526
|
+
const image = new Image();
|
|
527
|
+
let finished = false;
|
|
528
|
+
const done = (blob: Blob | null) => {
|
|
529
|
+
if (finished) return;
|
|
530
|
+
finished = true; image.onload = null; image.onerror = null;
|
|
531
|
+
pending.delete(cancelExport); clearTimeout(timer); resolve(scope.disposed ? null : blob);
|
|
532
|
+
};
|
|
533
|
+
const cancelExport = () => { done(null); image.src = ''; };
|
|
534
|
+
const timer = setTimeout(cancelExport, 10_000);
|
|
535
|
+
pending.add(cancelExport);
|
|
536
|
+
image.onerror = () => done(null);
|
|
537
|
+
image.onload = () => {
|
|
538
|
+
try {
|
|
539
|
+
context.drawImage(image, 0, 0, canvas.width, canvas.height);
|
|
540
|
+
canvas.toBlob(done, 'image/png');
|
|
541
|
+
} catch { done(null); }
|
|
542
|
+
};
|
|
543
|
+
image.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(exportSVG(settings))}`;
|
|
544
|
+
});
|
|
545
|
+
},
|
|
546
|
+
setDisabled(value) { if (!scope.disposed) { cancel(); disabled = Boolean(value); update(); } },
|
|
547
|
+
setReadOnly(value) { if (!scope.disposed) { cancel(); readOnly = Boolean(value); update(); } },
|
|
548
|
+
destroy: () => scope.dispose(),
|
|
549
|
+
};
|
|
550
|
+
}
|