@aznabee/freehand-ui 0.1.8 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2422 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __export = (target, all) => {
6
+ for (var name in all)
7
+ __defProp(target, name, { get: all[name], enumerable: true });
8
+ };
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
18
+
19
+ // src/native/react-native.js
20
+ var react_native_exports = {};
21
+ __export(react_native_exports, {
22
+ Doodle: () => Doodle,
23
+ buildDoodle: () => buildDoodle,
24
+ default: () => react_native_default
25
+ });
26
+ module.exports = __toCommonJS(react_native_exports);
27
+ var import_react = require("react");
28
+ var import_react_native = require("react-native");
29
+ var import_react_native_svg = require("react-native-svg");
30
+
31
+ // src/animation.js
32
+ var STYLE_MARKER = "data-freehand-animation";
33
+ var CSS = `
34
+ @keyframes freehand-arrow-dash {
35
+ from { stroke-dashoffset: var(--freehand-dash, 0px); }
36
+ to { stroke-dashoffset: 0; }
37
+ }
38
+ @keyframes freehand-arrow-drift {
39
+ 0%, 100% { transform: translate(0px, 0px); }
40
+ 50% { transform: translate(var(--freehand-drift-x, 0px), var(--freehand-drift-y, 0px)); }
41
+ }
42
+ @keyframes freehand-arrow-settle {
43
+ from { transform: translate(0px, 0px); }
44
+ to { transform: translate(var(--freehand-drift-x, 0px), var(--freehand-drift-y, 0px)); }
45
+ }
46
+ @media (prefers-reduced-motion: reduce) {
47
+ .freehand-arrow, .freehand-arrow path { animation: none !important; }
48
+ .freehand-arrow path { stroke-dashoffset: 0 !important; }
49
+ }
50
+ `;
51
+ var ARROW_ANIMATION_DEFAULTS = {
52
+ /** Reveal the stroke with a dash sweep. */
53
+ draw: true,
54
+ /** Lean the arrow toward its tip. */
55
+ drift: true,
56
+ /** Multiplier over every duration - 2 plays twice as fast. */
57
+ speed: 1,
58
+ /** How long the shaft takes to draw, ms, before `speed`. */
59
+ duration: 900,
60
+ /** Wait before the first stroke appears, ms. */
61
+ delay: 150,
62
+ /** How far the arrow leans, px. */
63
+ distance: 5,
64
+ /** One full lean-and-return, ms. */
65
+ driftDuration: 2200,
66
+ /** Keep leaning. `false` settles once and stays put. */
67
+ repeat: true
68
+ };
69
+ function positive(value, fallback) {
70
+ const n = Number(value);
71
+ return Number.isFinite(n) && n > 0 ? n : fallback;
72
+ }
73
+ function atLeastZero(value, fallback) {
74
+ const n = Number(value);
75
+ return Number.isFinite(n) && n >= 0 ? n : fallback;
76
+ }
77
+ function normalizeArrowAnimation(config) {
78
+ if (config === false || config === null) return false;
79
+ const settings = config === true || config === void 0 || typeof config !== "object" ? {} : config;
80
+ const animation = {
81
+ draw: settings.draw !== false,
82
+ drift: settings.drift !== false,
83
+ speed: positive(settings.speed, ARROW_ANIMATION_DEFAULTS.speed),
84
+ duration: positive(settings.duration, ARROW_ANIMATION_DEFAULTS.duration),
85
+ delay: atLeastZero(settings.delay, ARROW_ANIMATION_DEFAULTS.delay),
86
+ distance: atLeastZero(settings.distance, ARROW_ANIMATION_DEFAULTS.distance),
87
+ driftDuration: positive(
88
+ settings.driftDuration,
89
+ ARROW_ANIMATION_DEFAULTS.driftDuration
90
+ ),
91
+ repeat: settings.repeat !== false
92
+ };
93
+ if (!animation.draw && (!animation.drift || !animation.distance)) return false;
94
+ return animation;
95
+ }
96
+ var FLOW_PERIOD = 260;
97
+ function arrowTiming(animation) {
98
+ const duration = animation.duration / animation.speed;
99
+ const delay = animation.delay / animation.speed;
100
+ const drawn = animation.draw ? duration : 0;
101
+ return {
102
+ shaft: { duration, delay },
103
+ head: { duration: duration * 0.22, delay: delay + drawn * 0.82 },
104
+ flow: { duration: FLOW_PERIOD / animation.speed, delay },
105
+ drift: {
106
+ duration: animation.driftDuration / animation.speed,
107
+ delay: delay + drawn * 1.04
108
+ }
109
+ };
110
+ }
111
+ var injected = false;
112
+ function ensureArrowAnimationStyles() {
113
+ if (injected) return;
114
+ if (typeof document === "undefined" || !document.head) return;
115
+ injected = true;
116
+ if (document.querySelector(`style[${STYLE_MARKER}]`)) return;
117
+ const style = document.createElement("style");
118
+ style.setAttribute(STYLE_MARKER, "");
119
+ style.textContent = CSS;
120
+ document.head.appendChild(style);
121
+ }
122
+ function time(ms) {
123
+ return `${Math.round(ms)}ms`;
124
+ }
125
+ function setAnimation(element, spec) {
126
+ const parts = [
127
+ spec.name,
128
+ time(spec.duration),
129
+ spec.easing,
130
+ time(spec.delay),
131
+ spec.iterations ?? "1"
132
+ ];
133
+ if (spec.fill) parts.push(spec.fill);
134
+ element.style.animation = parts.join(" ");
135
+ }
136
+ function pathLength(path) {
137
+ try {
138
+ const length = path.getTotalLength();
139
+ return Number.isFinite(length) && length > 0 ? length : 0;
140
+ } catch {
141
+ return 0;
142
+ }
143
+ }
144
+ function dashPeriod(path) {
145
+ const pattern = (path.getAttribute("stroke-dasharray") || "").split(/[\s,]+/).map(Number).filter((n) => Number.isFinite(n) && n > 0);
146
+ if (!pattern.length) return 0;
147
+ const sum = pattern.reduce((total, n) => total + n, 0);
148
+ return pattern.length % 2 === 0 ? sum : sum * 2;
149
+ }
150
+ function applyDash(path, timing, elapsed, flow) {
151
+ const length = pathLength(path);
152
+ if (!length) return;
153
+ const period = dashPeriod(path);
154
+ if (period) {
155
+ path.style.setProperty("--freehand-dash", `${period.toFixed(2)}px`);
156
+ setAnimation(path, {
157
+ name: "freehand-arrow-dash",
158
+ duration: flow.duration,
159
+ easing: "linear",
160
+ delay: flow.delay - elapsed,
161
+ iterations: "infinite"
162
+ });
163
+ return;
164
+ }
165
+ path.style.setProperty("--freehand-dash", `${length.toFixed(2)}px`);
166
+ path.style.strokeDasharray = `${length.toFixed(2)}px`;
167
+ setAnimation(path, {
168
+ name: "freehand-arrow-dash",
169
+ duration: timing.duration,
170
+ easing: "ease-out",
171
+ delay: timing.delay - elapsed,
172
+ fill: "both"
173
+ });
174
+ }
175
+ function applyDrift(group, parts, timing, elapsed, animation) {
176
+ const dx = parts.end.x - parts.start.x;
177
+ const dy = parts.end.y - parts.start.y;
178
+ const length = Math.hypot(dx, dy);
179
+ if (!length) return;
180
+ const scale = animation.distance / length;
181
+ group.style.setProperty("--freehand-drift-x", `${(dx * scale).toFixed(2)}px`);
182
+ group.style.setProperty("--freehand-drift-y", `${(dy * scale).toFixed(2)}px`);
183
+ setAnimation(
184
+ group,
185
+ animation.repeat ? {
186
+ name: "freehand-arrow-drift",
187
+ duration: timing.duration,
188
+ easing: "ease-in-out",
189
+ delay: timing.delay - elapsed,
190
+ iterations: "infinite"
191
+ } : {
192
+ // A single lean is a settle, not a round trip, so it takes half a cycle
193
+ name: "freehand-arrow-settle",
194
+ duration: timing.duration * 0.5,
195
+ easing: "ease-out",
196
+ delay: timing.delay - elapsed,
197
+ fill: "both"
198
+ }
199
+ );
200
+ }
201
+ function animateArrow(group, parts, animation, elapsed = 0) {
202
+ if (!animation || !group) return;
203
+ ensureArrowAnimationStyles();
204
+ group.setAttribute("class", "freehand-arrow");
205
+ const timing = arrowTiming(animation);
206
+ const age = Number.isFinite(elapsed) ? Math.max(0, elapsed) : 0;
207
+ if (animation.draw) {
208
+ for (const path of parts.shaft) {
209
+ applyDash(path, timing.shaft, age, timing.flow);
210
+ }
211
+ for (const path of parts.head) applyDash(path, timing.head, age, timing.flow);
212
+ }
213
+ if (animation.drift && animation.distance > 0) {
214
+ applyDrift(group, parts, timing.drift, age, animation);
215
+ }
216
+ }
217
+
218
+ // src/locale.js
219
+ var DEFAULT_KEY = "default";
220
+ var LANGUAGE_TAG = /^[a-z]{2,3}(-[a-z0-9]{2,8})*$/i;
221
+ var RTL_STRONG = /[֑-߿יִ-﷿ﹰ-ﻼ]/;
222
+ var LTR_STRONG = /[A-Za-zÀ-֏ऀ-῿Ⰰ-퟿豈-ﬗA-Za-z]/;
223
+ function normalizeTag(tag) {
224
+ if (typeof tag !== "string") return "";
225
+ return tag.trim().replace(/_/g, "-").toLowerCase();
226
+ }
227
+ function isLocaleMap(value) {
228
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
229
+ const entries = Object.entries(value);
230
+ if (!entries.length) return false;
231
+ return entries.every(([key, text]) => {
232
+ if (typeof text !== "string") return false;
233
+ const tag = normalizeTag(key);
234
+ return tag === DEFAULT_KEY || LANGUAGE_TAG.test(tag);
235
+ });
236
+ }
237
+ function preferredLocales(override) {
238
+ const tags = [];
239
+ const add = (value) => {
240
+ const tag = normalizeTag(value);
241
+ if (tag && !tags.includes(tag)) tags.push(tag);
242
+ };
243
+ if (Array.isArray(override)) override.forEach(add);
244
+ else add(override);
245
+ const nav = typeof navigator === "undefined" ? null : navigator;
246
+ if (nav) {
247
+ if (Array.isArray(nav.languages)) nav.languages.forEach(add);
248
+ add(nav.language);
249
+ }
250
+ if (typeof document !== "undefined") add(document.documentElement?.lang);
251
+ return tags;
252
+ }
253
+ function matchLocale(tags, preferred) {
254
+ const available = tags.map(normalizeTag);
255
+ for (const want of preferred) {
256
+ let tag = want;
257
+ while (tag) {
258
+ const exact = available.indexOf(tag);
259
+ if (exact !== -1) return exact;
260
+ const cut = tag.lastIndexOf("-");
261
+ tag = cut > 0 ? tag.slice(0, cut) : "";
262
+ }
263
+ const base = want.split("-")[0];
264
+ const sibling = available.findIndex((key) => key.split("-")[0] === base);
265
+ if (sibling !== -1) return sibling;
266
+ }
267
+ return -1;
268
+ }
269
+ function resolveLocalizedText(text, locale = null) {
270
+ if (typeof text === "string") return { text, locale: null };
271
+ if (!isLocaleMap(text)) return { text: "", locale: null };
272
+ const keys = Object.keys(text);
273
+ const tags = keys.filter((key) => normalizeTag(key) !== DEFAULT_KEY);
274
+ const index = matchLocale(tags, preferredLocales(locale));
275
+ if (index !== -1) {
276
+ return { text: text[tags[index]], locale: normalizeTag(tags[index]) };
277
+ }
278
+ const fallback = keys.find((key) => normalizeTag(key) === DEFAULT_KEY);
279
+ if (fallback) return { text: text[fallback], locale: null };
280
+ return { text: text[keys[0]], locale: normalizeTag(keys[0]) };
281
+ }
282
+ function isRtlText(text) {
283
+ if (typeof text !== "string") return false;
284
+ const rtl = text.search(RTL_STRONG);
285
+ if (rtl === -1) return false;
286
+ const ltr = text.search(LTR_STRONG);
287
+ return ltr === -1 || rtl < ltr;
288
+ }
289
+
290
+ // src/utils.js
291
+ var DEFAULT_OPTIONS = {
292
+ border: true,
293
+ color: "#ffffff",
294
+ strokeWidth: 1.5,
295
+ roughness: 1.5,
296
+ // The frame traces the element's own edge. Raise this to stand it off.
297
+ padding: 0,
298
+ radius: null,
299
+ opacity: 0.9,
300
+ children: null,
301
+ note: null,
302
+ arrow: false,
303
+ decorations: false,
304
+ addBreaks: false,
305
+ /** Override the handwriting stack, e.g. a next/font CSS variable. */
306
+ fontFamily: null,
307
+ /** Fetch Caveat when the page has not provided it. */
308
+ autoLoadFont: true,
309
+ /**
310
+ * Language for a note written in more than one. Omit to follow the visitor's
311
+ * own languages, which is what their operating system is set to.
312
+ */
313
+ locale: null,
314
+ /**
315
+ * Overlay stacking order. Omit to mirror the target element's z-index so
316
+ * doodles stay above the annotated element but below modals and other UI.
317
+ */
318
+ zIndex: null
319
+ };
320
+ function mergeOptions(options = {}) {
321
+ const merged = { ...DEFAULT_OPTIONS, ...options };
322
+ if (merged.note) {
323
+ const config = typeof merged.note === "string" || isLocaleMap(merged.note) ? { text: merged.note } : merged.note;
324
+ merged.note = {
325
+ ...config,
326
+ position: config.position ?? "top-right",
327
+ // The note carries the chosen language, so `renderAnnotation` can resolve
328
+ // a multilingual note on its own.
329
+ locale: config.locale ?? merged.locale ?? null
330
+ };
331
+ }
332
+ if (merged.arrow) {
333
+ const config = merged.arrow === true ? {} : merged.arrow;
334
+ merged.arrow = {
335
+ // "note" launches from the annotation, a position name from that side of
336
+ // the element. Normalised for every arrow, not just `arrow: true`, so an
337
+ // explicit `from` is honoured whether or not there is a note.
338
+ from: config.from ?? (merged.note ? "note" : "top-right"),
339
+ to: config.to ?? "edge",
340
+ style: config.style ?? "curved",
341
+ // Arrows draw themselves on and lean toward their target by default;
342
+ // `animate: false` opts out, an object tunes speed and gestures
343
+ animate: normalizeArrowAnimation(config.animate)
344
+ };
345
+ }
346
+ if (merged.decorations === true) {
347
+ merged.decorations = { count: null, types: null };
348
+ }
349
+ if (merged.addBreaks) {
350
+ const config = typeof merged.addBreaks === "number" ? { count: merged.addBreaks } : merged.addBreaks === true ? {} : merged.addBreaks;
351
+ merged.addBreaks = {
352
+ count: config.count ?? null,
353
+ min: config.min ?? 6,
354
+ max: config.max ?? 30
355
+ };
356
+ }
357
+ return merged;
358
+ }
359
+ function createRandom(seed) {
360
+ let state = seed >>> 0;
361
+ return () => {
362
+ state = state + 1831565813 >>> 0;
363
+ let t = Math.imul(state ^ state >>> 15, 1 | state);
364
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
365
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
366
+ };
367
+ }
368
+ function clamp(value, min, max) {
369
+ return Math.min(max, Math.max(min, value));
370
+ }
371
+ function isValidPosition(position) {
372
+ return [
373
+ "top",
374
+ "top-right",
375
+ "right",
376
+ "bottom-right",
377
+ "bottom",
378
+ "bottom-left",
379
+ "left",
380
+ "top-left",
381
+ "center"
382
+ ].includes(position);
383
+ }
384
+
385
+ // src/geometry.js
386
+ function unionRects(rects, margin = 0) {
387
+ if (rects.length === 0) {
388
+ return { x: 0, y: 0, width: 0, height: 0, radius: 0 };
389
+ }
390
+ const minX = Math.min(...rects.map((r) => r.x)) - margin;
391
+ const minY = Math.min(...rects.map((r) => r.y)) - margin;
392
+ const maxX = Math.max(...rects.map((r) => r.x + r.width)) + margin;
393
+ const maxY = Math.max(...rects.map((r) => r.y + r.height)) + margin;
394
+ return {
395
+ x: minX,
396
+ y: minY,
397
+ width: maxX - minX,
398
+ height: maxY - minY,
399
+ radius: 0
400
+ };
401
+ }
402
+ function inflateRect(rect, amount) {
403
+ return {
404
+ x: rect.x - amount,
405
+ y: rect.y - amount,
406
+ width: rect.width + amount * 2,
407
+ height: rect.height + amount * 2
408
+ };
409
+ }
410
+ function intersectionArea(a, b) {
411
+ if (!a || !b) return 0;
412
+ const w = Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x);
413
+ const h2 = Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y);
414
+ return w > 0 && h2 > 0 ? w * h2 : 0;
415
+ }
416
+ function nearestPointOnRect(rect, point) {
417
+ return {
418
+ x: clamp(point.x, rect.x, rect.x + rect.width),
419
+ y: clamp(point.y, rect.y, rect.y + rect.height)
420
+ };
421
+ }
422
+ function distanceToRoundedRect(point, rect, radius = 0) {
423
+ const r = clamp(radius, 0, Math.min(rect.width, rect.height) / 2);
424
+ const cx = clamp(point.x, rect.x + r, rect.x + rect.width - r);
425
+ const cy = clamp(point.y, rect.y + r, rect.y + rect.height - r);
426
+ return Math.hypot(point.x - cx, point.y - cy) - r;
427
+ }
428
+ function pointOnBoxToward(box, target) {
429
+ const cx = box.x + box.width / 2;
430
+ const cy = box.y + box.height / 2;
431
+ const dx = target.x - cx;
432
+ const dy = target.y - cy;
433
+ if (dx === 0 && dy === 0) return { x: cx, y: cy };
434
+ const hw = box.width / 2 || 1e-3;
435
+ const hh = box.height / 2 || 1e-3;
436
+ const scale = 1 / Math.max(Math.abs(dx) / hw, Math.abs(dy) / hh);
437
+ return { x: cx + dx * scale, y: cy + dy * scale };
438
+ }
439
+ function directionFromPosition(position) {
440
+ return {
441
+ x: position.includes("right") ? 1 : position.includes("left") ? -1 : 0,
442
+ y: position.includes("top") ? -1 : position.includes("bottom") ? 1 : 0
443
+ };
444
+ }
445
+ function pointFromPosition(position, rect) {
446
+ const cx = rect.x + rect.width / 2;
447
+ const cy = rect.y + rect.height / 2;
448
+ switch (position) {
449
+ case "top":
450
+ return { x: cx, y: rect.y };
451
+ case "top-right":
452
+ return { x: rect.x + rect.width, y: rect.y };
453
+ case "right":
454
+ return { x: rect.x + rect.width, y: cy };
455
+ case "bottom-right":
456
+ return { x: rect.x + rect.width, y: rect.y + rect.height };
457
+ case "bottom":
458
+ return { x: cx, y: rect.y + rect.height };
459
+ case "bottom-left":
460
+ return { x: rect.x, y: rect.y + rect.height };
461
+ case "left":
462
+ return { x: rect.x, y: cy };
463
+ case "top-left":
464
+ return { x: rect.x, y: rect.y };
465
+ case "center":
466
+ default:
467
+ return { x: cx, y: cy };
468
+ }
469
+ }
470
+ function annotationAnchor(position, rect, offset = 12) {
471
+ const point = pointFromPosition(position, rect);
472
+ if (position.includes("top")) point.y -= offset;
473
+ if (position.includes("bottom")) point.y += offset;
474
+ if (position.includes("left")) point.x -= offset;
475
+ if (position.includes("right")) point.x += offset;
476
+ if (position === "top") point.y -= offset * 0.5;
477
+ if (position === "bottom") point.y += offset * 0.5;
478
+ if (position === "left") point.x -= offset * 0.5;
479
+ if (position === "right") point.x += offset * 0.5;
480
+ return point;
481
+ }
482
+
483
+ // src/native/svg-dom.js
484
+ var CAVEAT_ADVANCES = [
485
+ 0.242,
486
+ 0.21,
487
+ 0.237,
488
+ 0.559,
489
+ 0.45,
490
+ 0.6,
491
+ 0.564,
492
+ 0.117,
493
+ 0.33,
494
+ 0.33,
495
+ 0.363,
496
+ 0.446,
497
+ 0.198,
498
+ 0.326,
499
+ 0.198,
500
+ 0.33,
501
+ 0.45,
502
+ 0.45,
503
+ 0.45,
504
+ 0.45,
505
+ 0.45,
506
+ 0.439,
507
+ 0.464,
508
+ 0.447,
509
+ 0.45,
510
+ 0.442,
511
+ 0.198,
512
+ 0.198,
513
+ 0.446,
514
+ 0.446,
515
+ 0.446,
516
+ 0.374,
517
+ 0.643,
518
+ 0.508,
519
+ 0.522,
520
+ 0.475,
521
+ 0.584,
522
+ 0.532,
523
+ 0.462,
524
+ 0.492,
525
+ 0.57,
526
+ 0.406,
527
+ 0.312,
528
+ 0.505,
529
+ 0.426,
530
+ 0.726,
531
+ 0.612,
532
+ 0.502,
533
+ 0.467,
534
+ 0.495,
535
+ 0.542,
536
+ 0.492,
537
+ 0.458,
538
+ 0.482,
539
+ 0.49,
540
+ 0.72,
541
+ 0.524,
542
+ 0.504,
543
+ 0.522,
544
+ 0.33,
545
+ 0.33,
546
+ 0.33,
547
+ 0.446,
548
+ 0.446,
549
+ 0.353,
550
+ 0.443,
551
+ 0.441,
552
+ 0.361,
553
+ 0.399,
554
+ 0.331,
555
+ 0.297,
556
+ 0.361,
557
+ 0.47,
558
+ 0.193,
559
+ 0.21,
560
+ 0.371,
561
+ 0.175,
562
+ 0.566,
563
+ 0.456,
564
+ 0.359,
565
+ 0.378,
566
+ 0.377,
567
+ 0.36,
568
+ 0.347,
569
+ 0.331,
570
+ 0.37,
571
+ 0.331,
572
+ 0.517,
573
+ 0.343,
574
+ 0.341,
575
+ 0.315,
576
+ 0.33,
577
+ 0.33,
578
+ 0.33,
579
+ 0.446
580
+ ];
581
+ var CAVEAT_FALLBACK_EM = 0.37;
582
+ var NON_LATIN_EM = 0.62;
583
+ var LETTER_SPACING = 0.4;
584
+ var FAMILY_WIDTH = {
585
+ caveat: 1,
586
+ noteworthy: 1.4,
587
+ casual: 1.5
588
+ };
589
+ var OTHER_FAMILY_WIDTH = 1.3;
590
+ function familyWidth(stack) {
591
+ if (!stack) return 1;
592
+ const first = String(stack).split(",")[0].trim().replace(/^["']|["']$/g, "").toLowerCase();
593
+ return FAMILY_WIDTH[first] ?? OTHER_FAMILY_WIDTH;
594
+ }
595
+ function caveatAdvance(char) {
596
+ const code = char.codePointAt(0);
597
+ if (code >= 32 && code <= 126) return CAVEAT_ADVANCES[code - 32];
598
+ return code > 767 ? NON_LATIN_EM : CAVEAT_FALLBACK_EM;
599
+ }
600
+ function estimateTextWidth(text, fontSize, fontFamily = null) {
601
+ let em = 0;
602
+ let glyphs = 0;
603
+ for (const char of String(text)) {
604
+ glyphs += 1;
605
+ em += caveatAdvance(char);
606
+ }
607
+ return em * fontSize * familyWidth(fontFamily) + Math.max(0, glyphs - 1) * LETTER_SPACING;
608
+ }
609
+ function bezierPoint(p, t) {
610
+ const u = 1 - t;
611
+ if (p.length === 6) {
612
+ return {
613
+ x: u * u * p[0] + 2 * u * t * p[2] + t * t * p[4],
614
+ y: u * u * p[1] + 2 * u * t * p[3] + t * t * p[5]
615
+ };
616
+ }
617
+ return {
618
+ x: u * u * u * p[0] + 3 * u * u * t * p[2] + 3 * u * t * t * p[4] + t * t * t * p[6],
619
+ y: u * u * u * p[1] + 3 * u * u * t * p[3] + 3 * u * t * t * p[5] + t * t * t * p[7]
620
+ };
621
+ }
622
+ function curveLength(p) {
623
+ const steps = 16;
624
+ let length = 0;
625
+ let previous = { x: p[0], y: p[1] };
626
+ for (let i = 1; i <= steps; i++) {
627
+ const point = bezierPoint(p, i / steps);
628
+ length += Math.hypot(point.x - previous.x, point.y - previous.y);
629
+ previous = point;
630
+ }
631
+ return length;
632
+ }
633
+ function pathDataLength(d) {
634
+ const commands = String(d).match(/[MLCQZmlcqz][^MLCQZmlcqz]*/g) ?? [];
635
+ let x = 0;
636
+ let y = 0;
637
+ let startX = 0;
638
+ let startY = 0;
639
+ let length = 0;
640
+ for (const command of commands) {
641
+ const type = command[0];
642
+ const upper = type.toUpperCase();
643
+ const relative = type !== upper;
644
+ const numbers = (command.slice(1).match(/-?\d*\.?\d+(?:e[-+]?\d+)?/gi) ?? []).map(
645
+ Number
646
+ );
647
+ if (upper === "Z") {
648
+ length += Math.hypot(startX - x, startY - y);
649
+ x = startX;
650
+ y = startY;
651
+ continue;
652
+ }
653
+ const arity = { M: 2, L: 2, Q: 4, C: 6 }[upper];
654
+ for (let i = 0; i + arity <= numbers.length; i += arity) {
655
+ const args = numbers.slice(i, i + arity);
656
+ const ox = relative ? x : 0;
657
+ const oy = relative ? y : 0;
658
+ const points = [];
659
+ for (let j = 0; j < args.length; j += 2) {
660
+ points.push(args[j] + ox, args[j + 1] + oy);
661
+ }
662
+ const endX = points[points.length - 2];
663
+ const endY = points[points.length - 1];
664
+ if (upper === "M" && i === 0) {
665
+ startX = endX;
666
+ startY = endY;
667
+ } else if (upper === "M" || upper === "L") {
668
+ length += Math.hypot(endX - x, endY - y);
669
+ } else {
670
+ length += curveLength([x, y, ...points]);
671
+ }
672
+ x = endX;
673
+ y = endY;
674
+ }
675
+ }
676
+ return length;
677
+ }
678
+ function createStyle() {
679
+ const custom = {};
680
+ return Object.defineProperties(
681
+ {},
682
+ {
683
+ setProperty: {
684
+ value: (name, value) => {
685
+ custom[name] = String(value);
686
+ }
687
+ },
688
+ getPropertyValue: { value: (name) => custom[name] ?? "" },
689
+ custom: { value: custom }
690
+ }
691
+ );
692
+ }
693
+ var VirtualElement = class {
694
+ /**
695
+ * @param {string} tagName
696
+ */
697
+ constructor(tagName) {
698
+ this.tagName = tagName;
699
+ this.nodeName = tagName;
700
+ this.attributes = {};
701
+ this.children = [];
702
+ this.parentNode = null;
703
+ this.style = createStyle();
704
+ this._text = "";
705
+ }
706
+ get childNodes() {
707
+ return this.children;
708
+ }
709
+ get firstChild() {
710
+ return this.children[0] ?? null;
711
+ }
712
+ get textContent() {
713
+ if (this.children.length) {
714
+ return this.children.map((child) => child.textContent).join("");
715
+ }
716
+ return this._text;
717
+ }
718
+ set textContent(value) {
719
+ this.children = [];
720
+ this._text = String(value ?? "");
721
+ }
722
+ setAttribute(name, value) {
723
+ this.attributes[name] = String(value);
724
+ }
725
+ setAttributeNS(_namespace, name, value) {
726
+ this.setAttribute(name, value);
727
+ }
728
+ getAttribute(name) {
729
+ return name in this.attributes ? this.attributes[name] : null;
730
+ }
731
+ removeAttribute(name) {
732
+ delete this.attributes[name];
733
+ }
734
+ /**
735
+ * Moves the node if it already has a parent, like the DOM does - the note
736
+ * groups rely on that to be re-appended on top of every stroke.
737
+ * @param {VirtualElement} child
738
+ */
739
+ appendChild(child) {
740
+ if (child.parentNode) child.parentNode.removeChild(child);
741
+ child.parentNode = this;
742
+ this.children.push(child);
743
+ return child;
744
+ }
745
+ removeChild(child) {
746
+ const index = this.children.indexOf(child);
747
+ if (index !== -1) this.children.splice(index, 1);
748
+ child.parentNode = null;
749
+ return child;
750
+ }
751
+ contains(node) {
752
+ for (let current = node; current; current = current.parentNode) {
753
+ if (current === this) return true;
754
+ }
755
+ return false;
756
+ }
757
+ getTotalLength() {
758
+ return pathDataLength(this.getAttribute("d") ?? "");
759
+ }
760
+ /**
761
+ * @param {string} name
762
+ */
763
+ inherited(name) {
764
+ for (let node = this; node; node = node.parentNode) {
765
+ const value = node.getAttribute(name);
766
+ if (value != null) return value;
767
+ }
768
+ return null;
769
+ }
770
+ getComputedTextLength() {
771
+ return estimateTextWidth(
772
+ this.textContent,
773
+ Number(this.inherited("font-size")) || 19,
774
+ this.inherited("font-family")
775
+ );
776
+ }
777
+ getBBox() {
778
+ return { x: 0, y: 0, width: this.getComputedTextLength(), height: 0 };
779
+ }
780
+ };
781
+ function createSvgElement(tag) {
782
+ return new VirtualElement(tag);
783
+ }
784
+
785
+ // src/renderer.js
786
+ var XML_NS = "http://www.w3.org/XML/1998/namespace";
787
+ var HANDWRITTEN_FONT = '"Caveat", "Kalam", "Patrick Hand", "Bradley Hand", "Segoe Script", "Comic Sans MS", cursive';
788
+ var NOTE_FONT_SIZE = 19;
789
+ function fmt(n) {
790
+ return (Number.isFinite(n) ? n : 0).toFixed(2);
791
+ }
792
+ function catmullRomPath(points, closed = false) {
793
+ if (!points.length) return "";
794
+ if (points.length === 1) {
795
+ return `M ${fmt(points[0].x)} ${fmt(points[0].y)}`;
796
+ }
797
+ const n = points.length;
798
+ const at = (i) => closed ? points[(i % n + n) % n] : points[clamp(i, 0, n - 1)];
799
+ let path = `M ${fmt(points[0].x)} ${fmt(points[0].y)}`;
800
+ const segments = closed ? n : n - 1;
801
+ for (let i = 0; i < segments; i++) {
802
+ const p0 = at(i - 1);
803
+ const p1 = at(i);
804
+ const p2 = at(i + 1);
805
+ const p3 = at(i + 2);
806
+ const c1x = p1.x + (p2.x - p0.x) / 6;
807
+ const c1y = p1.y + (p2.y - p0.y) / 6;
808
+ const c2x = p2.x - (p3.x - p1.x) / 6;
809
+ const c2y = p2.y - (p3.y - p1.y) / 6;
810
+ path += ` C ${fmt(c1x)} ${fmt(c1y)} ${fmt(c2x)} ${fmt(c2y)} ${fmt(p2.x)} ${fmt(p2.y)}`;
811
+ }
812
+ if (closed) path += " Z";
813
+ return path;
814
+ }
815
+ function createWave(random, period, amplitude) {
816
+ const p = period || 1;
817
+ const tau = Math.PI * 2 / p;
818
+ const f1 = tau * (2 + Math.floor(random() * 2));
819
+ const f2 = tau * (5 + Math.floor(random() * 3));
820
+ const phase1 = random() * Math.PI * 2;
821
+ const phase2 = random() * Math.PI * 2;
822
+ return (distance) => Math.sin(distance * f1 + phase1) * amplitude + Math.sin(distance * f2 + phase2) * amplitude * 0.4;
823
+ }
824
+ function roughLine(x1, y1, x2, y2, roughness, random, opts = {}) {
825
+ const dx = x2 - x1;
826
+ const dy = y2 - y1;
827
+ const length = Math.hypot(dx, dy) || 1;
828
+ const steps = opts.steps ?? clamp(Math.round(length / 22), 2, 14);
829
+ const bow = opts.bow ?? 0;
830
+ const nx = -dy / length;
831
+ const ny = dx / length;
832
+ const amplitude = roughness * 0.32;
833
+ const frequency = 1 + random() * 1.4;
834
+ const phase = random() * Math.PI * 2;
835
+ const points = [];
836
+ for (let i = 0; i <= steps; i++) {
837
+ const t = i / steps;
838
+ const envelope = Math.sin(t * Math.PI);
839
+ const offset = bow * envelope + Math.sin(t * Math.PI * frequency + phase) * amplitude * envelope;
840
+ points.push({
841
+ x: x1 + dx * t + nx * offset,
842
+ y: y1 + dy * t + ny * offset
843
+ });
844
+ }
845
+ return catmullRomPath(points, false);
846
+ }
847
+ function roundedRectOutline(x, y, width, height, radius, spacing) {
848
+ const r = clamp(radius, 0, Math.min(width, height) / 2);
849
+ const right = x + width;
850
+ const bottom = y + height;
851
+ const HALF_PI = Math.PI / 2;
852
+ const line = (x1, y1, x2, y2, nx, ny) => ({
853
+ type: "line",
854
+ x1,
855
+ y1,
856
+ x2,
857
+ y2,
858
+ nx,
859
+ ny,
860
+ length: Math.hypot(x2 - x1, y2 - y1)
861
+ });
862
+ const arc = (cx, cy, start, end) => ({
863
+ type: "arc",
864
+ cx,
865
+ cy,
866
+ start,
867
+ end,
868
+ r,
869
+ length: Math.abs(end - start) * r
870
+ });
871
+ const segments = [
872
+ line(x + r, y, right - r, y, 0, -1),
873
+ arc(right - r, y + r, -HALF_PI, 0),
874
+ line(right, y + r, right, bottom - r, 1, 0),
875
+ arc(right - r, bottom - r, 0, HALF_PI),
876
+ line(right - r, bottom, x + r, bottom, 0, 1),
877
+ arc(x + r, bottom - r, HALF_PI, Math.PI),
878
+ line(x, bottom - r, x, y + r, -1, 0),
879
+ arc(x + r, y + r, Math.PI, Math.PI + HALF_PI)
880
+ ];
881
+ const total = segments.reduce((sum, seg) => sum + seg.length, 0);
882
+ const points = [];
883
+ const edgeStarts = [];
884
+ for (const seg of segments) {
885
+ if (seg.length <= 0.01) continue;
886
+ const steps = Math.max(1, Math.round(seg.length / spacing));
887
+ if (seg.type === "line") edgeStarts.push(points.length);
888
+ for (let i = 0; i < steps; i++) {
889
+ const t = i / steps;
890
+ if (seg.type === "line") {
891
+ points.push({
892
+ x: seg.x1 + (seg.x2 - seg.x1) * t,
893
+ y: seg.y1 + (seg.y2 - seg.y1) * t,
894
+ nx: seg.nx,
895
+ ny: seg.ny
896
+ });
897
+ } else {
898
+ const angle = seg.start + (seg.end - seg.start) * t;
899
+ const nx = Math.cos(angle);
900
+ const ny = Math.sin(angle);
901
+ points.push({
902
+ x: seg.cx + nx * seg.r,
903
+ y: seg.cy + ny * seg.r,
904
+ nx,
905
+ ny
906
+ });
907
+ }
908
+ }
909
+ }
910
+ return { points, total, edgeStarts };
911
+ }
912
+ function generateHandDrawnRectPaths(x, y, width, height, radius, roughness, random, opts = {}) {
913
+ if (width <= 0 || height <= 0) return [];
914
+ const spacing = opts.spacing ?? 11;
915
+ const { points, total, edgeStarts } = roundedRectOutline(
916
+ x,
917
+ y,
918
+ width,
919
+ height,
920
+ radius,
921
+ spacing
922
+ );
923
+ const count = points.length;
924
+ if (count < 3) return [];
925
+ const amplitude = opts.amplitude ?? roughness * 0.45;
926
+ const wave = createWave(random, total, amplitude);
927
+ const step = total / count;
928
+ const portion = clamp(opts.portion ?? 1, 0.05, 1);
929
+ const overshoot = opts.overshoot ?? 0;
930
+ let startIndex = Math.floor((opts.startAt ?? random()) * count);
931
+ if (opts.snapToEdge && edgeStarts.length) {
932
+ startIndex = edgeStarts.reduce(
933
+ (best, index) => Math.abs(index - startIndex) < Math.abs(best - startIndex) ? index : best
934
+ );
935
+ }
936
+ let drawn = Math.round(count * portion);
937
+ if (opts.snapToEdge && edgeStarts.length && portion < 1) {
938
+ const target = startIndex + drawn;
939
+ let snapped = target;
940
+ let bestDelta = Infinity;
941
+ for (let lap = 0; lap <= 2; lap++) {
942
+ for (const index of edgeStarts) {
943
+ const candidate = index + lap * count;
944
+ const delta = Math.abs(candidate - target);
945
+ if (candidate > startIndex + 2 && delta < bestDelta) {
946
+ bestDelta = delta;
947
+ snapped = candidate;
948
+ }
949
+ }
950
+ }
951
+ drawn = Math.min(snapped - startIndex, count);
952
+ }
953
+ const tail = portion >= 1 ? Math.max(1, Math.round(overshoot / step)) : 0;
954
+ const breaks = opts.breaks ?? [];
955
+ const runs = [];
956
+ let run = [];
957
+ for (let k = 0; k <= drawn + tail; k++) {
958
+ const index = (startIndex + k) % count;
959
+ if (breaks.length && isBroken(index / count, breaks)) {
960
+ if (run.length > 1) runs.push(run);
961
+ run = [];
962
+ continue;
963
+ }
964
+ const point = points[index];
965
+ let offset = wave((startIndex + k) * step);
966
+ if (tail > 0 && k > drawn) {
967
+ offset += (k - drawn) / tail * 0.5;
968
+ }
969
+ run.push({
970
+ x: point.x + point.nx * offset,
971
+ y: point.y + point.ny * offset
972
+ });
973
+ }
974
+ if (run.length > 1) runs.push(run);
975
+ return runs.map((points2) => catmullRomPath(points2, false));
976
+ }
977
+ function isBroken(fraction, breaks) {
978
+ return breaks.some(
979
+ (gap) => gap.end > gap.start ? fraction >= gap.start && fraction < gap.end : fraction >= gap.start || fraction < gap.end
980
+ );
981
+ }
982
+ function createBorderBreaks(perimeter, config, random) {
983
+ if (!config || perimeter <= 0) return [];
984
+ const settings = config === true ? {} : config;
985
+ const count = Math.max(1, Math.round(settings.count ?? 2 + random() * 3));
986
+ const min = Math.max(2, settings.min ?? 6);
987
+ const max = Math.max(min, settings.max ?? 30);
988
+ const slice = perimeter / count;
989
+ let budget = perimeter * 0.35;
990
+ const breaks = [];
991
+ for (let i = 0; i < count; i++) {
992
+ const length = Math.min(min + random() * (max - min), slice * 0.7, budget);
993
+ if (length < 2) break;
994
+ budget -= length;
995
+ const start = i * slice + random() * Math.max(0, slice - length);
996
+ breaks.push({
997
+ start: start / perimeter,
998
+ end: (start + length) / perimeter
999
+ });
1000
+ }
1001
+ return breaks;
1002
+ }
1003
+ function createGroup(parent, x, y, rotation = 0) {
1004
+ const group = createSvgElement("g");
1005
+ group.setAttribute(
1006
+ "transform",
1007
+ `translate(${fmt(x)} ${fmt(y)}) rotate(${rotation.toFixed(1)})`
1008
+ );
1009
+ parent.appendChild(group);
1010
+ return group;
1011
+ }
1012
+ function appendPath(svg, d, style) {
1013
+ const path = createSvgElement("path");
1014
+ path.setAttribute("d", d);
1015
+ path.setAttribute("fill", style.fill ?? "none");
1016
+ path.setAttribute("stroke", style.color);
1017
+ path.setAttribute("stroke-width", String(style.strokeWidth));
1018
+ path.setAttribute("stroke-linecap", "round");
1019
+ path.setAttribute("stroke-linejoin", "round");
1020
+ path.setAttribute("opacity", String(style.opacity));
1021
+ if (style.dashed) {
1022
+ path.setAttribute("stroke-dasharray", "2.5 4.5");
1023
+ }
1024
+ svg.appendChild(path);
1025
+ return path;
1026
+ }
1027
+ function appendGlow(svg, radius, style) {
1028
+ const circle = createSvgElement("circle");
1029
+ circle.setAttribute("r", fmt(radius));
1030
+ circle.setAttribute("fill", style.color);
1031
+ circle.setAttribute("opacity", String(style.opacity));
1032
+ circle.style.filter = `blur(${fmt(radius * 0.4)}px)`;
1033
+ svg.appendChild(circle);
1034
+ return circle;
1035
+ }
1036
+ function rectPerimeter(width, height, radius) {
1037
+ const r = clamp(radius, 0, Math.min(width, height) / 2);
1038
+ return 2 * (width - 2 * r) + 2 * (height - 2 * r) + 2 * Math.PI * r;
1039
+ }
1040
+ function drawBorder(svg, rect, options, seed) {
1041
+ const random = createRandom(seed);
1042
+ const roughness = clamp(options.roughness, 0, 3);
1043
+ const radius = rect.radius || 0;
1044
+ const breaks = options.breaks ? createBorderBreaks(
1045
+ rectPerimeter(rect.width, rect.height, radius),
1046
+ options.breaks,
1047
+ random
1048
+ ) : [];
1049
+ const spacing = breaks.length ? 5.5 : 11;
1050
+ const main = generateHandDrawnRectPaths(
1051
+ rect.x,
1052
+ rect.y,
1053
+ rect.width,
1054
+ rect.height,
1055
+ radius,
1056
+ roughness,
1057
+ random,
1058
+ {
1059
+ amplitude: roughness * 0.5,
1060
+ overshoot: 9 + roughness * 5,
1061
+ startAt: random(),
1062
+ snapToEdge: true,
1063
+ spacing,
1064
+ breaks
1065
+ }
1066
+ );
1067
+ for (const d of main) {
1068
+ appendPath(svg, d, {
1069
+ color: options.color,
1070
+ strokeWidth: options.strokeWidth,
1071
+ opacity: options.opacity
1072
+ });
1073
+ }
1074
+ if (roughness < 0.9) return;
1075
+ const offset = 1.1 + random() * 0.9;
1076
+ const ghost = generateHandDrawnRectPaths(
1077
+ rect.x - offset,
1078
+ rect.y - offset,
1079
+ rect.width + offset * 2,
1080
+ rect.height + offset * 2,
1081
+ radius + offset,
1082
+ roughness,
1083
+ random,
1084
+ {
1085
+ amplitude: roughness * 0.45,
1086
+ portion: 0.45 + random() * 0.25,
1087
+ startAt: random(),
1088
+ snapToEdge: true,
1089
+ spacing,
1090
+ breaks
1091
+ }
1092
+ );
1093
+ for (const d of ghost) {
1094
+ appendPath(svg, d, {
1095
+ color: options.color,
1096
+ strokeWidth: options.strokeWidth * 0.7,
1097
+ opacity: options.opacity * 0.4
1098
+ });
1099
+ }
1100
+ }
1101
+ function cubicAt(p0, c1, c2, p3, t) {
1102
+ const u = 1 - t;
1103
+ const a = u * u * u;
1104
+ const b = 3 * u * u * t;
1105
+ const c = 3 * u * t * t;
1106
+ const d = t * t * t;
1107
+ return {
1108
+ x: a * p0.x + b * c1.x + c * c2.x + d * p3.x,
1109
+ y: a * p0.y + b * c1.y + c * c2.y + d * p3.y
1110
+ };
1111
+ }
1112
+ function cubicTangentAt(p0, c1, c2, p3, t) {
1113
+ const u = 1 - t;
1114
+ const a = 3 * u * u;
1115
+ const b = 6 * u * t;
1116
+ const c = 3 * t * t;
1117
+ return {
1118
+ x: a * (c1.x - p0.x) + b * (c2.x - c1.x) + c * (p3.x - c2.x),
1119
+ y: a * (c1.y - p0.y) + b * (c2.y - c1.y) + c * (p3.y - c2.y)
1120
+ };
1121
+ }
1122
+ var STRADDLE = 0.75;
1123
+ function loopedCurvePoints(start, c1, c2, end, radius, random, side = 1) {
1124
+ const at = (t) => cubicAt(start, c1, c2, end, t);
1125
+ const knot = 0.46 + random() * 0.12;
1126
+ const points = [];
1127
+ const lead = Math.max(2, Math.round(knot * 16));
1128
+ for (let i = 0; i <= lead; i++) points.push(at(knot * i / lead));
1129
+ const pivot = at(knot);
1130
+ const derivative = cubicTangentAt(start, c1, c2, end, knot);
1131
+ const speed = Math.hypot(derivative.x, derivative.y) || 1;
1132
+ const heading = normalize(derivative);
1133
+ const normal = { x: -heading.y, y: heading.x };
1134
+ const centre = {
1135
+ x: pivot.x + normal.x * radius * STRADDLE * side,
1136
+ y: pivot.y + normal.y * radius * STRADDLE * side
1137
+ };
1138
+ const from = Math.atan2(pivot.y - centre.y, pivot.x - centre.x);
1139
+ const turns = Math.PI * 2 * (0.97 + random() * 0.08);
1140
+ const steps = 18;
1141
+ for (let i = 1; i <= steps; i++) {
1142
+ const progress = i / steps;
1143
+ const angle = from + turns * progress * side;
1144
+ const spread = radius * (STRADDLE + (1 - STRADDLE) * progress);
1145
+ const drift = radius * 0.55 * progress;
1146
+ points.push({
1147
+ x: centre.x + Math.cos(angle) * spread + heading.x * drift,
1148
+ y: centre.y + Math.sin(angle) * spread + heading.y * drift
1149
+ });
1150
+ }
1151
+ const exit = points[points.length - 1];
1152
+ const gained = (exit.x - pivot.x) * heading.x + (exit.y - pivot.y) * heading.y;
1153
+ const resume = Math.min(knot + Math.max(0, gained) / speed, 0.985);
1154
+ const rejoin = at(resume);
1155
+ const residual = (exit.x - rejoin.x) * normal.x + (exit.y - rejoin.y) * normal.y;
1156
+ const settle = 6;
1157
+ const tail = 14;
1158
+ for (let i = 1; i <= tail; i++) {
1159
+ const point = at(resume + (1 - resume) * (i / tail));
1160
+ const fade = Math.max(0, 1 - i / settle);
1161
+ points.push({
1162
+ x: point.x + normal.x * residual * fade,
1163
+ y: point.y + normal.y * residual * fade
1164
+ });
1165
+ }
1166
+ return points;
1167
+ }
1168
+ function normalize(vector) {
1169
+ const length = Math.hypot(vector.x, vector.y);
1170
+ if (!length) return { x: 0, y: 0 };
1171
+ return { x: vector.x / length, y: vector.y / length };
1172
+ }
1173
+ var NOTE_CLEARANCE = 10;
1174
+ var TIP_GAP = 11;
1175
+ function arrowStartFromNote(origin, target, from) {
1176
+ const cx = origin.x + origin.width / 2;
1177
+ const cy = origin.y + origin.height / 2;
1178
+ if (from && from !== "note") {
1179
+ const point = pointFromPosition(from, origin);
1180
+ const away = directionFromPosition(from);
1181
+ if (!away.x && !away.y) {
1182
+ const edge = pointOnBoxToward(origin, target);
1183
+ const outward = normalize({ x: edge.x - cx, y: edge.y - cy });
1184
+ return {
1185
+ x: edge.x + outward.x * NOTE_CLEARANCE,
1186
+ y: edge.y + outward.y * NOTE_CLEARANCE
1187
+ };
1188
+ }
1189
+ return {
1190
+ x: point.x + away.x * NOTE_CLEARANCE,
1191
+ y: point.y + away.y * NOTE_CLEARANCE
1192
+ };
1193
+ }
1194
+ const dx = target.x - cx;
1195
+ const dy = target.y - cy;
1196
+ const hw = origin.width / 2 || 1e-3;
1197
+ const hh = origin.height / 2 || 1e-3;
1198
+ const sx = Math.sign(dx) || 1;
1199
+ const sy = Math.sign(dy) || 1;
1200
+ if (Math.abs(dy) / hh >= Math.abs(dx) / hw) {
1201
+ return { x: cx - sx * (hw + 10), y: cy + sy * (hh + 4) };
1202
+ }
1203
+ return { x: cx + sx * (hw + 8), y: cy + sy * (hh + 6) };
1204
+ }
1205
+ function arrowEndpoints(rect, arrowOptions, random, origin) {
1206
+ const center = { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 };
1207
+ const from = arrowOptions.from || "note";
1208
+ const anchored = Boolean(origin && origin.width > 0 && origin.height > 0);
1209
+ let start = anchored ? arrowStartFromNote(origin, center, from) : annotationAnchor(
1210
+ from === "note" ? "top-right" : from,
1211
+ rect,
1212
+ 38 + random() * 12
1213
+ );
1214
+ const toPos = arrowOptions.to || "edge";
1215
+ let end;
1216
+ if (toPos === "center") {
1217
+ end = center;
1218
+ } else if (toPos === "edge") {
1219
+ const near = nearestPointOnRect(rect, start);
1220
+ let outward = normalize({ x: start.x - near.x, y: start.y - near.y });
1221
+ if (!outward.x && !outward.y) outward = { x: 0, y: -1 };
1222
+ const gap = TIP_GAP + random() * 5;
1223
+ end = { x: near.x + outward.x * gap, y: near.y + outward.y * gap };
1224
+ } else {
1225
+ end = pointFromPosition(toPos, rect);
1226
+ }
1227
+ const distance = Math.hypot(end.x - start.x, end.y - start.y);
1228
+ const minimum = 34;
1229
+ if (!anchored && distance < minimum) {
1230
+ const unit = distance > 0 ? { x: (start.x - end.x) / distance, y: (start.y - end.y) / distance } : { x: 0, y: -1 };
1231
+ start = { x: end.x + unit.x * minimum, y: end.y + unit.y * minimum };
1232
+ }
1233
+ return { start, end };
1234
+ }
1235
+ function drawArrowHead(svg, tip, angle, style, random, shaftLength = Infinity) {
1236
+ const length = Math.min(9 + style.strokeWidth * 1.4, shaftLength * 0.3);
1237
+ const spread = 0.46 + random() * 0.1;
1238
+ const arms = [];
1239
+ for (const side of [-1, 1]) {
1240
+ const armAngle = angle + spread * side;
1241
+ const tail = {
1242
+ x: tip.x - length * Math.cos(armAngle),
1243
+ y: tip.y - length * Math.sin(armAngle)
1244
+ };
1245
+ arms.push(
1246
+ appendPath(
1247
+ svg,
1248
+ roughLine(tail.x, tail.y, tip.x, tip.y, style.roughness * 0.5, random, {
1249
+ steps: 2,
1250
+ bow: 0.35 * side
1251
+ }),
1252
+ { ...style, dashed: false }
1253
+ )
1254
+ );
1255
+ }
1256
+ return arms;
1257
+ }
1258
+ function bowOverTheTop(perpendicular) {
1259
+ if (Math.abs(perpendicular.y) < 0.02) return perpendicular.x > 0 ? 1 : -1;
1260
+ return perpendicular.y > 0 ? -1 : 1;
1261
+ }
1262
+ function boundsOf(points) {
1263
+ const xs = points.map((p) => p.x);
1264
+ const ys = points.map((p) => p.y);
1265
+ return {
1266
+ x: Math.min(...xs),
1267
+ y: Math.min(...ys),
1268
+ width: Math.max(...xs) - Math.min(...xs),
1269
+ height: Math.max(...ys) - Math.min(...ys)
1270
+ };
1271
+ }
1272
+ function drawArrow(svg, rect, arrowOptions, style, seed, origin = null, context = {}) {
1273
+ const random = createRandom(seed + 17);
1274
+ const { start, end } = arrowEndpoints(rect, arrowOptions, random, origin);
1275
+ const dx = end.x - start.x;
1276
+ const dy = end.y - start.y;
1277
+ const distance = Math.hypot(dx, dy) || 1;
1278
+ const dashed = arrowOptions.style === "dotted";
1279
+ let tipAngle = Math.atan2(dy, dx);
1280
+ let shaft;
1281
+ let ghost = null;
1282
+ const hull = [start, end];
1283
+ if (arrowOptions.style === "straight") {
1284
+ shaft = roughLine(start.x, start.y, end.x, end.y, style.roughness, random, {
1285
+ bow: (random() - 0.5) * style.roughness
1286
+ });
1287
+ } else {
1288
+ const perpendicular = { x: -dy / distance, y: dx / distance };
1289
+ const mid = { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 };
1290
+ const towardCenter = {
1291
+ x: rect.x + rect.width / 2 - mid.x,
1292
+ y: rect.y + rect.height / 2 - mid.y
1293
+ };
1294
+ const lateral = perpendicular.x * towardCenter.x + perpendicular.y * towardCenter.y;
1295
+ const sign = Math.abs(lateral) > distance * 0.12 ? lateral > 0 ? -1 : 1 : (
1296
+ // Nothing to steer around, so sweep over the top: a hand arcs above
1297
+ // the straight line and drops onto the target, and a coin flip here
1298
+ // used to mirror the whole gesture from one redraw to the next.
1299
+ bowOverTheTop(perpendicular)
1300
+ );
1301
+ const bow = Math.min(distance * (0.16 + random() * 0.08), 30) * sign;
1302
+ const c1x = start.x + dx * 0.28 + perpendicular.x * bow;
1303
+ const c1y = start.y + dy * 0.28 + perpendicular.y * bow;
1304
+ const c2x = start.x + dx * 0.72 + perpendicular.x * bow * 0.85;
1305
+ const c2y = start.y + dy * 0.72 + perpendicular.y * bow * 0.85;
1306
+ hull.push({ x: c1x, y: c1y }, { x: c2x, y: c2y });
1307
+ if (arrowOptions.style === "looped") {
1308
+ const radius = clamp(distance * 0.11, 7, 17);
1309
+ const points = loopedCurvePoints(
1310
+ start,
1311
+ { x: c1x, y: c1y },
1312
+ { x: c2x, y: c2y },
1313
+ end,
1314
+ radius,
1315
+ random,
1316
+ -sign
1317
+ );
1318
+ shaft = catmullRomPath(points, false);
1319
+ hull.push(...points);
1320
+ const last = points[points.length - 1];
1321
+ const prior = points[points.length - 2] ?? start;
1322
+ tipAngle = Math.atan2(last.y - prior.y, last.x - prior.x);
1323
+ } else {
1324
+ shaft = `M ${fmt(start.x)} ${fmt(start.y)} C ${fmt(c1x)} ${fmt(c1y)} ${fmt(c2x)} ${fmt(c2y)} ${fmt(end.x)} ${fmt(end.y)}`;
1325
+ if (style.roughness >= 1.1) {
1326
+ const drift = () => (random() - 0.5) * 1.4;
1327
+ ghost = `M ${fmt(start.x + drift())} ${fmt(start.y + drift())} C ${fmt(c1x + drift())} ${fmt(c1y + drift())} ${fmt(c2x + drift())} ${fmt(c2y + drift())} ${fmt(end.x + drift() * 0.4)} ${fmt(end.y + drift() * 0.4)}`;
1328
+ }
1329
+ tipAngle = Math.atan2(end.y - c2y, end.x - c2x);
1330
+ }
1331
+ }
1332
+ const group = createSvgElement("g");
1333
+ svg.appendChild(group);
1334
+ const shaftPaths = [];
1335
+ if (ghost) {
1336
+ shaftPaths.push(
1337
+ appendPath(group, ghost, {
1338
+ ...style,
1339
+ opacity: style.opacity * 0.3,
1340
+ strokeWidth: style.strokeWidth * 0.7,
1341
+ dashed
1342
+ })
1343
+ );
1344
+ }
1345
+ shaftPaths.push(appendPath(group, shaft, { ...style, dashed }));
1346
+ const head = drawArrowHead(group, end, tipAngle, style, random, distance);
1347
+ animateArrow(
1348
+ group,
1349
+ { shaft: shaftPaths, head, start, end },
1350
+ arrowOptions.animate,
1351
+ context.elapsed
1352
+ );
1353
+ const bounds = boundsOf(hull);
1354
+ return { start, end, bounds, group };
1355
+ }
1356
+ function drawUnderline(svg, x, y, width, style, random) {
1357
+ if (width <= 4) return;
1358
+ const slope = (random() - 0.5) * 2.4;
1359
+ appendPath(
1360
+ svg,
1361
+ roughLine(x, y, x + width, y + slope, style.roughness * 0.6, random, {
1362
+ bow: (random() - 0.5) * 1.4,
1363
+ steps: 5
1364
+ }),
1365
+ {
1366
+ color: style.color,
1367
+ strokeWidth: style.strokeWidth * 0.95,
1368
+ opacity: style.opacity * 0.9
1369
+ }
1370
+ );
1371
+ const inset = width * (0.06 + random() * 0.12);
1372
+ const length = width * (0.62 + random() * 0.24);
1373
+ appendPath(
1374
+ svg,
1375
+ roughLine(
1376
+ x + inset,
1377
+ y + 2.6,
1378
+ x + Math.min(width, inset + length),
1379
+ y + 2.6 + slope * 0.6,
1380
+ style.roughness * 0.5,
1381
+ random,
1382
+ { bow: (random() - 0.5) * 1.1, steps: 4 }
1383
+ ),
1384
+ {
1385
+ color: style.color,
1386
+ strokeWidth: style.strokeWidth * 0.7,
1387
+ opacity: style.opacity * 0.38
1388
+ }
1389
+ );
1390
+ }
1391
+ var DEFAULT_WORDS_PER_LINE = 2;
1392
+ function wrapWords(text, perLine) {
1393
+ const lines = [];
1394
+ for (const paragraph of String(text).split(/\r?\n/)) {
1395
+ const words = paragraph.trim().split(/\s+/).filter(Boolean);
1396
+ if (!words.length) continue;
1397
+ const size = Number.isFinite(perLine) && perLine >= 1 ? Math.round(perLine) : words.length;
1398
+ for (let i = 0; i < words.length; i += size) {
1399
+ lines.push(words.slice(i, i + size).join(" "));
1400
+ }
1401
+ }
1402
+ return lines.length ? lines : [String(text)];
1403
+ }
1404
+ function measureWidth(element, content, fontSize) {
1405
+ try {
1406
+ const box = element.getBBox();
1407
+ if (box && box.width > 0) return box.width;
1408
+ } catch {
1409
+ }
1410
+ try {
1411
+ const measured = element.getComputedTextLength();
1412
+ if (Number.isFinite(measured) && measured > 0) return measured;
1413
+ } catch {
1414
+ }
1415
+ return content.length * fontSize * 0.45;
1416
+ }
1417
+ function createNoteText(svg, text, style, options = {}) {
1418
+ const fontSize = style.fontSize ?? NOTE_FONT_SIZE;
1419
+ const lines = wrapWords(text, options.wordsPerLine ?? DEFAULT_WORDS_PER_LINE);
1420
+ const lineHeight = fontSize * 1.05;
1421
+ const rtl = options.rtl ?? isRtlText(text);
1422
+ const group = createSvgElement("g");
1423
+ const textEl = createSvgElement("text");
1424
+ textEl.setAttribute("x", "0");
1425
+ textEl.setAttribute("y", "0");
1426
+ textEl.setAttribute("fill", style.color);
1427
+ textEl.setAttribute("opacity", String(style.opacity));
1428
+ textEl.setAttribute("font-size", String(fontSize));
1429
+ textEl.setAttribute("font-family", style.fontFamily || HANDWRITTEN_FONT);
1430
+ textEl.setAttribute("font-weight", "600");
1431
+ textEl.setAttribute("letter-spacing", "0.4");
1432
+ textEl.setAttribute("text-anchor", "start");
1433
+ textEl.setAttribute("dominant-baseline", "auto");
1434
+ if (rtl) {
1435
+ textEl.setAttribute("direction", "rtl");
1436
+ textEl.setAttribute("unicode-bidi", "embed");
1437
+ }
1438
+ if (options.locale) {
1439
+ textEl.setAttributeNS(XML_NS, "xml:lang", options.locale);
1440
+ }
1441
+ const tspans = lines.map((line, index) => {
1442
+ const tspan = createSvgElement("tspan");
1443
+ tspan.textContent = line;
1444
+ tspan.setAttribute("x", "0");
1445
+ tspan.setAttribute("dy", index === 0 ? "0" : String(lineHeight));
1446
+ textEl.appendChild(tspan);
1447
+ return tspan;
1448
+ });
1449
+ group.appendChild(textEl);
1450
+ svg.appendChild(group);
1451
+ const lineWidths = tspans.map(
1452
+ (tspan, index) => measureWidth(tspan, lines[index], fontSize)
1453
+ );
1454
+ const width = Math.max(...lineWidths);
1455
+ const ascent = fontSize * 0.74;
1456
+ const descent = fontSize * 0.26;
1457
+ const height = (lines.length - 1) * lineHeight + ascent + descent;
1458
+ return {
1459
+ group,
1460
+ textEl,
1461
+ tspans,
1462
+ lines,
1463
+ lineWidths,
1464
+ lineHeight,
1465
+ width,
1466
+ height,
1467
+ ascent,
1468
+ descent,
1469
+ fontSize,
1470
+ rtl
1471
+ };
1472
+ }
1473
+ function placeNoteText(note, box, style, seed, options = {}) {
1474
+ const random = createRandom(seed + 55);
1475
+ const tilt = options.tilt ?? (random() - 0.5) * 7;
1476
+ const anchorX = note.rtl ? box.x + box.width : box.x;
1477
+ note.textEl.setAttribute("x", fmt(anchorX));
1478
+ note.textEl.setAttribute("y", fmt(box.y + note.ascent));
1479
+ for (const tspan of note.tspans) tspan.setAttribute("x", fmt(anchorX));
1480
+ const cx = box.x + box.width / 2;
1481
+ const cy = box.y + box.height / 2;
1482
+ note.group.setAttribute(
1483
+ "transform",
1484
+ `rotate(${tilt.toFixed(2)} ${fmt(cx)} ${fmt(cy)})`
1485
+ );
1486
+ if (options.underline !== false) {
1487
+ const lastWidth = note.lineWidths[note.lineWidths.length - 1] ?? box.width;
1488
+ const underlineX = note.rtl ? box.x + box.width - lastWidth + 1 : box.x + 1;
1489
+ drawUnderline(
1490
+ note.group,
1491
+ underlineX,
1492
+ box.y + box.height + 2,
1493
+ lastWidth - 2,
1494
+ {
1495
+ color: style.color,
1496
+ strokeWidth: style.strokeWidth || 1.4,
1497
+ roughness: style.roughness || 1.4,
1498
+ opacity: style.opacity
1499
+ },
1500
+ random
1501
+ );
1502
+ }
1503
+ return note.group;
1504
+ }
1505
+
1506
+ // src/annotations.js
1507
+ var NOTE_GAP = 14;
1508
+ function boxForPosition(rect, position, size, gap) {
1509
+ const direction = directionFromPosition(position);
1510
+ const guard = inflateRect(rect, gap);
1511
+ const cx = rect.x + rect.width / 2;
1512
+ const cy = rect.y + rect.height / 2;
1513
+ let x;
1514
+ let y;
1515
+ if (direction.y !== 0) {
1516
+ y = direction.y > 0 ? guard.y + guard.height : guard.y - size.height;
1517
+ if (direction.x > 0) x = guard.x + guard.width - size.width * 0.35;
1518
+ else if (direction.x < 0) x = guard.x - size.width * 0.65;
1519
+ else x = cx - size.width / 2;
1520
+ } else {
1521
+ x = direction.x > 0 ? guard.x + guard.width : guard.x - size.width;
1522
+ y = cy - size.height / 2;
1523
+ }
1524
+ return { x, y, width: size.width, height: size.height };
1525
+ }
1526
+ function limitAgainstElement(box, guard, dx) {
1527
+ const level = box.y < guard.y + guard.height && box.y + box.height > guard.y;
1528
+ if (!level) return dx;
1529
+ if (dx < 0) {
1530
+ const room2 = box.x - (guard.x + guard.width);
1531
+ return room2 <= 0 ? 0 : -Math.min(-dx, room2);
1532
+ }
1533
+ const room = guard.x - (box.x + box.width);
1534
+ return room <= 0 ? 0 : Math.min(dx, room);
1535
+ }
1536
+ function nudgeIntoBand(box, guard, band) {
1537
+ if (!band || band.width <= 0) return box;
1538
+ const overhangLeft = band.x - box.x;
1539
+ const overhangRight = box.x + box.width - (band.x + band.width);
1540
+ let dx = 0;
1541
+ if (overhangLeft > 0) dx = overhangLeft;
1542
+ else if (overhangRight > 0) dx = -overhangRight;
1543
+ if (!dx) return box;
1544
+ dx = limitAgainstElement(box, guard, dx);
1545
+ if (!dx) return box;
1546
+ return { ...box, x: box.x + dx };
1547
+ }
1548
+ function renderAnnotation(svg, rect, noteOptions, style, seed = 1, layout = {}) {
1549
+ const { text, locale } = resolveLocalizedText(
1550
+ noteOptions?.text,
1551
+ noteOptions?.locale
1552
+ );
1553
+ if (!text) return null;
1554
+ const band = layout.band ?? null;
1555
+ const position = isValidPosition(noteOptions.position) ? noteOptions.position : "top-right";
1556
+ const random = createRandom(seed + 9);
1557
+ const note = createNoteText(svg, text, style, {
1558
+ wordsPerLine: noteOptions.wordsPerLine,
1559
+ locale
1560
+ });
1561
+ const tilt = (random() - 0.45) * 7;
1562
+ const tiltPad = Math.abs(Math.sin(tilt * Math.PI / 180)) * note.width * 0.5;
1563
+ const size = {
1564
+ width: note.width,
1565
+ height: note.height + (noteOptions.underline === false ? 0 : 5)
1566
+ };
1567
+ const gap = (layout.gap ?? NOTE_GAP) + random() * 6;
1568
+ const placed = boxForPosition(
1569
+ rect,
1570
+ position,
1571
+ { width: size.width, height: size.height + tiltPad },
1572
+ gap
1573
+ );
1574
+ const shifted = {
1575
+ ...placed,
1576
+ x: placed.x + (Number(noteOptions.offset?.x) || 0),
1577
+ y: placed.y + (Number(noteOptions.offset?.y) || 0)
1578
+ };
1579
+ const guard = inflateRect(rect, gap * 0.7);
1580
+ const outer = nudgeIntoBand(shifted, guard, band);
1581
+ const box = {
1582
+ x: outer.x,
1583
+ y: outer.y + tiltPad / 2,
1584
+ width: size.width,
1585
+ height: note.height
1586
+ };
1587
+ placeNoteText(note, box, style, seed, {
1588
+ tilt,
1589
+ underline: noteOptions.underline !== false
1590
+ });
1591
+ return { group: note.group, box: inflateRect(outer, 4), position };
1592
+ }
1593
+
1594
+ // src/decorations.js
1595
+ function drawDot(parent, x, y, style, scale = 1.7) {
1596
+ appendPath(parent, `M ${x.toFixed(2)} ${y.toFixed(2)} l 0.01 0`, {
1597
+ ...style,
1598
+ strokeWidth: style.strokeWidth * scale
1599
+ });
1600
+ }
1601
+ function drawFleck(parent, x, y, radius, style, random) {
1602
+ const count = 6;
1603
+ const points = [];
1604
+ for (let i = 0; i < count; i++) {
1605
+ const angle = Math.PI * 2 * i / count + (random() - 0.5) * 0.5;
1606
+ const r = radius * (0.78 + random() * 0.34);
1607
+ points.push({
1608
+ x: x + Math.cos(angle) * r,
1609
+ y: y + Math.sin(angle) * r
1610
+ });
1611
+ }
1612
+ const blob = appendPath(parent, catmullRomPath(points, true), {
1613
+ ...style,
1614
+ fill: style.color,
1615
+ strokeWidth: 0
1616
+ });
1617
+ blob.style.filter = `blur(${(radius * 0.35).toFixed(2)}px)`;
1618
+ return blob;
1619
+ }
1620
+ function drawHeart(parent, size, style, random) {
1621
+ const s = size;
1622
+ const j = () => (random() - 0.5) * s * 0.12;
1623
+ const path = [
1624
+ `M 0 ${(s * 0.95).toFixed(2)}`,
1625
+ `C ${(-s * 1.16 + j()).toFixed(2)} ${(s * 0.08).toFixed(2)}, ${(-s * 0.94 + j()).toFixed(2)} ${(-s * 0.78).toFixed(2)}, 0 ${(-s * 0.3).toFixed(2)}`,
1626
+ `C ${(s * 0.94 + j()).toFixed(2)} ${(-s * 0.78).toFixed(2)}, ${(s * 1.16 + j()).toFixed(2)} ${(s * 0.08).toFixed(2)}, 0 ${(s * 0.95).toFixed(2)}`,
1627
+ "Z"
1628
+ ].join(" ");
1629
+ appendPath(parent, path, style);
1630
+ }
1631
+ function twinkle(parent, radiusX, radiusY, pinch, style) {
1632
+ const rx = radiusX;
1633
+ const ry = radiusY;
1634
+ const c = pinch;
1635
+ const path = [
1636
+ `M 0 ${(-ry).toFixed(2)}`,
1637
+ `Q ${c.toFixed(2)} ${(-c).toFixed(2)} ${rx.toFixed(2)} 0`,
1638
+ `Q ${c.toFixed(2)} ${c.toFixed(2)} 0 ${ry.toFixed(2)}`,
1639
+ `Q ${(-c).toFixed(2)} ${c.toFixed(2)} ${(-rx).toFixed(2)} 0`,
1640
+ `Q ${(-c).toFixed(2)} ${(-c).toFixed(2)} 0 ${(-ry).toFixed(2)}`,
1641
+ "Z"
1642
+ ].join(" ");
1643
+ return appendPath(parent, path, style);
1644
+ }
1645
+ function drawSparkle(parent, size, style, random) {
1646
+ twinkle(
1647
+ parent,
1648
+ size * (0.62 + random() * 0.12),
1649
+ size,
1650
+ size * (0.14 + random() * 0.08),
1651
+ style
1652
+ );
1653
+ }
1654
+ function drawStar(parent, size, style, random) {
1655
+ twinkle(
1656
+ parent,
1657
+ size * (0.62 + random() * 0.12),
1658
+ size * (0.8 + random() * 0.12),
1659
+ size * (0.14 + random() * 0.08),
1660
+ style
1661
+ );
1662
+ }
1663
+ function drawGlimmer(parent, size, style, random) {
1664
+ appendGlow(parent, size * (1.2 + random() * 0.2), {
1665
+ color: style.color,
1666
+ opacity: style.opacity * 0.22
1667
+ });
1668
+ const armX = size * (0.5 + random() * 0.08);
1669
+ const armY = size * (0.45 + random() * 0.08);
1670
+ const bluntness = size * 0.19;
1671
+ const main = twinkle(parent, armX, armY, size * (0.08 + random() * 0.03), {
1672
+ ...style,
1673
+ fill: style.color,
1674
+ strokeWidth: bluntness
1675
+ });
1676
+ main.setAttribute("stroke-linejoin", "round");
1677
+ main.setAttribute("stroke-linecap", "round");
1678
+ main.style.filter = `blur(${(size * 0.05).toFixed(2)}px)`;
1679
+ const gap = bluntness / 2 + size * 0.2;
1680
+ const corners = [
1681
+ { x: 0, y: -(armY + gap) },
1682
+ { x: armX + gap, y: 0 },
1683
+ { x: 0, y: armY + gap },
1684
+ { x: -(armX + gap), y: 0 }
1685
+ ];
1686
+ for (const { x, y } of corners) {
1687
+ const jitter = 0.95 + random() * 0.1;
1688
+ drawFleck(
1689
+ parent,
1690
+ x * jitter,
1691
+ y * jitter,
1692
+ size * (0.1 + random() * 0.03),
1693
+ style,
1694
+ random
1695
+ );
1696
+ }
1697
+ }
1698
+ function drawTwinkle(parent, size, style, random) {
1699
+ const count = 5;
1700
+ const first = -Math.PI / 2 + (random() - 0.5) * 0.5;
1701
+ const at = (angle, radius) => ({
1702
+ x: Math.cos(angle) * radius,
1703
+ y: Math.sin(angle) * radius
1704
+ });
1705
+ const tips = [];
1706
+ for (let i = 0; i < count; i++) {
1707
+ const angle = first + Math.PI * 2 * i / count + (random() - 0.5) * 0.24;
1708
+ tips.push({ angle, ...at(angle, size * (0.8 + random() * 0.4)) });
1709
+ }
1710
+ let path = `M ${tips[0].x.toFixed(2)} ${tips[0].y.toFixed(2)}`;
1711
+ for (let i = 0; i < count; i++) {
1712
+ const from = tips[i];
1713
+ const to = tips[(i + 1) % count];
1714
+ let delta = to.angle - from.angle;
1715
+ while (delta <= 0) delta += Math.PI * 2;
1716
+ const control = at(
1717
+ from.angle + delta / 2 + (random() - 0.5) * 0.16,
1718
+ size * (0.02 + random() * 0.13)
1719
+ );
1720
+ path += ` Q ${control.x.toFixed(2)} ${control.y.toFixed(2)} ${to.x.toFixed(2)} ${to.y.toFixed(2)}`;
1721
+ }
1722
+ path += " Z";
1723
+ const filled = appendPath(parent, path, {
1724
+ ...style,
1725
+ fill: style.color,
1726
+ strokeWidth: style.strokeWidth * 0.45
1727
+ });
1728
+ filled.setAttribute("stroke-linejoin", "miter");
1729
+ filled.setAttribute("stroke-linecap", "butt");
1730
+ }
1731
+ function drawArcs(parent, size, style, random) {
1732
+ const count = 2 + Math.floor(random() * 2);
1733
+ const focus = size * 0.55;
1734
+ for (let i = 0; i < count; i++) {
1735
+ const radius = size * (0.75 + i * 0.4);
1736
+ const sweep = 0.8 + random() * 0.3 - i * 0.08;
1737
+ const steps = 7;
1738
+ const points = [];
1739
+ for (let s = 0; s <= steps; s++) {
1740
+ const angle = -Math.PI / 2 - sweep / 2 + sweep * (s / steps);
1741
+ const wobble = radius + (random() - 0.5) * size * 0.06;
1742
+ points.push({
1743
+ x: Math.cos(angle) * wobble,
1744
+ y: focus + Math.sin(angle) * wobble
1745
+ });
1746
+ }
1747
+ appendPath(parent, catmullRomPath(points, false), {
1748
+ ...style,
1749
+ strokeWidth: style.strokeWidth * (1 - i * 0.12),
1750
+ opacity: style.opacity * (1 - i * 0.14)
1751
+ });
1752
+ }
1753
+ }
1754
+ function drawSmiley(parent, size, style, random) {
1755
+ const r = size * 0.92;
1756
+ const circle = generateHandDrawnRectPaths(
1757
+ -r,
1758
+ -r,
1759
+ r * 2,
1760
+ r * 2,
1761
+ r,
1762
+ style.roughness * 0.7,
1763
+ random,
1764
+ {
1765
+ amplitude: style.roughness * 0.22,
1766
+ overshoot: r * 0.55,
1767
+ spacing: Math.max(3, r * 0.45),
1768
+ startAt: random()
1769
+ }
1770
+ );
1771
+ for (const d of circle) {
1772
+ appendPath(parent, d, { ...style, strokeWidth: style.strokeWidth * 0.9 });
1773
+ }
1774
+ drawDot(parent, -r * 0.36, -r * 0.22, style, 1.5);
1775
+ drawDot(parent, r * 0.36, -r * 0.22, style, 1.5);
1776
+ appendPath(
1777
+ parent,
1778
+ `M ${(-r * 0.44).toFixed(2)} ${(r * 0.18).toFixed(2)} Q 0 ${(r * 0.72).toFixed(2)} ${(r * 0.44).toFixed(2)} ${(r * 0.18).toFixed(2)}`,
1779
+ style
1780
+ );
1781
+ }
1782
+ function drawEmphasis(parent, size, style, random, opts = {}) {
1783
+ const count = opts.count ?? 3 + Math.floor(random() * 2);
1784
+ const spread = opts.spread ?? 0.62 + random() * 0.3;
1785
+ for (let i = 0; i < count; i++) {
1786
+ const t = count === 1 ? 0 : i / (count - 1) - 0.5;
1787
+ const angle = -Math.PI / 2 + t * spread * 2;
1788
+ const inner = size * (0.34 + random() * 0.12);
1789
+ const outer = size * (0.86 + random() * 0.36);
1790
+ appendPath(
1791
+ parent,
1792
+ roughLine(
1793
+ Math.cos(angle) * inner,
1794
+ Math.sin(angle) * inner,
1795
+ Math.cos(angle) * outer,
1796
+ Math.sin(angle) * outer,
1797
+ 0.7,
1798
+ random,
1799
+ { steps: 2, bow: (random() - 0.5) * 0.9 }
1800
+ ),
1801
+ { ...style, strokeWidth: style.strokeWidth * (0.85 + random() * 0.4) }
1802
+ );
1803
+ }
1804
+ }
1805
+ var DECORATION_DRAWERS = {
1806
+ heart: drawHeart,
1807
+ sparkle: drawSparkle,
1808
+ star: drawStar,
1809
+ twinkle: drawTwinkle,
1810
+ glimmer: drawGlimmer,
1811
+ smiley: drawSmiley,
1812
+ emphasis: drawEmphasis,
1813
+ arcs: drawArcs
1814
+ };
1815
+ var TEXT_TYPES = ["heart", "sparkle"];
1816
+ var CORNER_ACCENTS = ["arcs", "emphasis"];
1817
+ var CORNER_STARS = ["twinkle", "star"];
1818
+ var MAX_BORDER_MARKS = 2;
1819
+ var TYPE_TRAITS = {
1820
+ heart: { size: [7, 10], tilt: 14, standoff: 22 },
1821
+ sparkle: { size: [6, 9], tilt: 20, standoff: 22 },
1822
+ star: { size: [8, 11], tilt: 25, standoff: 26 },
1823
+ twinkle: { size: [5, 7.5], tilt: 30, standoff: 26, brightness: 1.4 },
1824
+ glimmer: { size: [7, 10], tilt: 20, standoff: 28, brightness: 1.3 },
1825
+ smiley: { size: [8, 11], tilt: 10, standoff: 22 },
1826
+ emphasis: { size: [12, 17], tilt: 10, standoff: 13, outward: true },
1827
+ arcs: { size: [11, 16], tilt: 12, standoff: 12, outward: true }
1828
+ };
1829
+ var DEFAULT_TYPES = [
1830
+ "twinkle",
1831
+ "arcs",
1832
+ "emphasis",
1833
+ "heart",
1834
+ "sparkle"
1835
+ ];
1836
+ function isClear(point, radius, blockers) {
1837
+ const box = {
1838
+ x: point.x - radius,
1839
+ y: point.y - radius,
1840
+ width: radius * 2,
1841
+ height: radius * 2
1842
+ };
1843
+ return !blockers.some((blocker) => intersectionArea(box, blocker) > 0);
1844
+ }
1845
+ function pickFrom(types, family, random) {
1846
+ const pool = types.filter(
1847
+ (type) => family.includes(type) && DECORATION_DRAWERS[type]
1848
+ );
1849
+ if (!pool.length) return null;
1850
+ return pool[Math.floor(random() * pool.length)];
1851
+ }
1852
+ function sizeFrom(range, random) {
1853
+ return range[0] + random() * (range[1] - range[0]);
1854
+ }
1855
+ function markOpacity(style, traits, variation) {
1856
+ return clamp(style.opacity * variation * (traits.brightness ?? 1), 0, 1);
1857
+ }
1858
+ function cornerAnchors(rect) {
1859
+ const radius = clamp(
1860
+ rect.radius ?? 0,
1861
+ 0,
1862
+ Math.min(rect.width, rect.height) / 2
1863
+ );
1864
+ const d = Math.SQRT1_2;
1865
+ const right = rect.x + rect.width - radius;
1866
+ const bottom = rect.y + rect.height - radius;
1867
+ const left = rect.x + radius;
1868
+ const top = rect.y + radius;
1869
+ return [
1870
+ { cx: left, cy: top, dir: { x: -d, y: -d } },
1871
+ { cx: right, cy: top, dir: { x: d, y: -d } },
1872
+ { cx: right, cy: bottom, dir: { x: d, y: d } },
1873
+ { cx: left, cy: bottom, dir: { x: -d, y: d } }
1874
+ ].map((anchor) => ({ ...anchor, radius }));
1875
+ }
1876
+ function cornerPoint(anchor, standoff) {
1877
+ return {
1878
+ x: anchor.cx + anchor.dir.x * (anchor.radius + standoff),
1879
+ y: anchor.cy + anchor.dir.y * (anchor.radius + standoff)
1880
+ };
1881
+ }
1882
+ function outwardTilt(dir) {
1883
+ return Math.atan2(dir.y, dir.x) * 180 / Math.PI + 90;
1884
+ }
1885
+ function placeAtCorner(svg, type, anchor, style, random, hasRoom) {
1886
+ const traits = TYPE_TRAITS[type] ?? { size: [8, 12], tilt: 15 };
1887
+ const size = sizeFrom(traits.size, random);
1888
+ const point = cornerPoint(anchor, traits.standoff ?? 20);
1889
+ const needed = (traits.outward ? size * 0.35 : size) + 4;
1890
+ if (!hasRoom(point, needed)) return false;
1891
+ const tilt = traits.outward ? outwardTilt(anchor.dir) + (random() - 0.5) * traits.tilt : (random() - 0.5) * traits.tilt;
1892
+ DECORATION_DRAWERS[type](
1893
+ createGroup(svg, point.x, point.y, tilt),
1894
+ size,
1895
+ { ...style, opacity: markOpacity(style, traits, 0.8 + random() * 0.18) },
1896
+ random
1897
+ );
1898
+ return true;
1899
+ }
1900
+ function renderCornerMarks(svg, rect, types, budget, style, random, hasRoom) {
1901
+ const accent = pickFrom(types, CORNER_ACCENTS, random);
1902
+ const star = pickFrom(types, CORNER_STARS, random);
1903
+ const picks = [accent, star].filter(Boolean);
1904
+ if (picks.length < budget) {
1905
+ const rest = types.filter(
1906
+ (type) => DECORATION_DRAWERS[type] && !TEXT_TYPES.includes(type) && !CORNER_ACCENTS.includes(type) && !CORNER_STARS.includes(type)
1907
+ );
1908
+ picks.push(...rest);
1909
+ }
1910
+ if (!picks.length) return;
1911
+ for (let i = 0; picks.length < budget; i++) {
1912
+ picks.push(picks[i % picks.length]);
1913
+ }
1914
+ const anchors = cornerAnchors(rect).map((anchor) => ({ anchor, rank: random() })).sort((a, b) => a.rank - b.rank).map((entry) => entry.anchor);
1915
+ let placed = 0;
1916
+ let previous = null;
1917
+ for (const type of picks.slice(0, budget)) {
1918
+ const ordered = previous ? [...anchors].sort(
1919
+ (a, b) => Math.hypot(b.cx - previous.cx, b.cy - previous.cy) - Math.hypot(a.cx - previous.cx, a.cy - previous.cy)
1920
+ ) : anchors;
1921
+ const anchor = ordered.find(
1922
+ (candidate) => candidate !== previous && placeAtCorner(svg, type, candidate, style, random, hasRoom)
1923
+ );
1924
+ if (anchor) {
1925
+ previous = anchor;
1926
+ placed += 1;
1927
+ if (placed >= budget) break;
1928
+ }
1929
+ }
1930
+ }
1931
+ function renderSideMarks(svg, rect, style, random, hasRoom) {
1932
+ const cy = rect.y + rect.height / 2;
1933
+ for (const side of [-1, 1]) {
1934
+ const size = 10 + random() * 4;
1935
+ const dir = { x: side, y: 0 };
1936
+ const point = {
1937
+ x: side < 0 ? rect.x - (13 + size * 0.35) : rect.x + rect.width + (13 + size * 0.35),
1938
+ y: cy + (random() - 0.5) * rect.height * 0.45
1939
+ };
1940
+ if (!hasRoom(point, size * 0.35 + 4)) continue;
1941
+ drawEmphasis(
1942
+ createGroup(
1943
+ svg,
1944
+ point.x,
1945
+ point.y,
1946
+ outwardTilt(dir) + (random() - 0.5) * 12
1947
+ ),
1948
+ size,
1949
+ { ...style, opacity: style.opacity * (0.82 + random() * 0.15) },
1950
+ random,
1951
+ { count: 2, spread: 0.3 + random() * 0.14 }
1952
+ );
1953
+ }
1954
+ }
1955
+ function renderTextMark(svg, rect, note, types, style, random) {
1956
+ if (!note) return null;
1957
+ const type = pickFrom(types, TEXT_TYPES, random);
1958
+ if (!type) return null;
1959
+ const traits = TYPE_TRAITS[type];
1960
+ const size = sizeFrom(traits.size, random);
1961
+ const trailing = note.x + note.width / 2 >= rect.x + rect.width / 2 ? 1 : -1;
1962
+ const x = trailing > 0 ? note.x + note.width + size + 2 : note.x - size - 2;
1963
+ const y = note.y + note.height * 0.44;
1964
+ DECORATION_DRAWERS[type](
1965
+ createGroup(svg, x, y, (random() - 0.5) * traits.tilt),
1966
+ size,
1967
+ { ...style, opacity: markOpacity(style, traits, 0.85 + random() * 0.15) },
1968
+ random
1969
+ );
1970
+ return {
1971
+ x: x - size - 2,
1972
+ y: y - size - 2,
1973
+ width: size * 2 + 4,
1974
+ height: size * 2 + 4
1975
+ };
1976
+ }
1977
+ function isSmall(rect) {
1978
+ return Math.min(rect.width, rect.height) < 48 || rect.width < 130;
1979
+ }
1980
+ function renderDecorations(svg, rect, decorationOptions, style, seed, context = {}) {
1981
+ const random = createRandom(seed + 101);
1982
+ const types = decorationOptions.types?.length ? decorationOptions.types : DEFAULT_TYPES;
1983
+ const avoid = [...context.avoid ?? []];
1984
+ const textMark = renderTextMark(
1985
+ svg,
1986
+ rect,
1987
+ context.note ?? null,
1988
+ types,
1989
+ style,
1990
+ random
1991
+ );
1992
+ if (textMark) avoid.push(textMark);
1993
+ const budget = Math.min(
1994
+ decorationOptions.count ?? MAX_BORDER_MARKS,
1995
+ MAX_BORDER_MARKS
1996
+ );
1997
+ if (budget < 1) return;
1998
+ const hasRoom = (point, radius) => distanceToRoundedRect(point, rect, rect.radius) >= radius && isClear(point, radius, avoid);
1999
+ const layout = decorationOptions.style ?? (isSmall(rect) && types.includes("emphasis") ? "sides" : "corners");
2000
+ if (layout === "sides") {
2001
+ renderSideMarks(svg, rect, style, random, hasRoom);
2002
+ } else {
2003
+ renderCornerMarks(svg, rect, types, budget, style, random, hasRoom);
2004
+ }
2005
+ }
2006
+
2007
+ // src/native/layout.js
2008
+ var INK_MARGIN = 48;
2009
+ function buildDoodle({
2010
+ width,
2011
+ height,
2012
+ options = {},
2013
+ seed = 1,
2014
+ band = null,
2015
+ elapsed = 0
2016
+ }) {
2017
+ const merged = mergeOptions(options);
2018
+ const { padding, color, strokeWidth, roughness, opacity } = merged;
2019
+ const root = createSvgElement("svg");
2020
+ const outerWidth = Math.max(0, width) + padding * 2;
2021
+ const outerHeight = Math.max(0, height) + padding * 2;
2022
+ const rect = {
2023
+ x: -padding,
2024
+ y: -padding,
2025
+ width: outerWidth,
2026
+ height: outerHeight,
2027
+ // Grown with the padding so the frame stays concentric with the element,
2028
+ // as `getElementBounds` does on the web
2029
+ radius: clamp(
2030
+ (Number(merged.radius) || 0) + padding * 0.6,
2031
+ 0,
2032
+ Math.max(0, Math.min(outerWidth, outerHeight) / 2)
2033
+ )
2034
+ };
2035
+ const strokeStyle = { color, strokeWidth, roughness, opacity };
2036
+ const noteStyle = {
2037
+ color,
2038
+ opacity,
2039
+ strokeWidth,
2040
+ roughness,
2041
+ fontFamily: merged.fontFamily
2042
+ };
2043
+ const note = merged.note ? renderAnnotation(root, rect, merged.note, noteStyle, seed, {
2044
+ band,
2045
+ gap: merged.arrow ? 40 : 14
2046
+ }) : null;
2047
+ if (merged.border) {
2048
+ drawBorder(root, rect, { ...strokeStyle, breaks: merged.addBreaks }, seed);
2049
+ }
2050
+ const arrow = merged.arrow ? drawArrow(root, rect, merged.arrow, strokeStyle, seed, note?.box ?? null, {
2051
+ elapsed
2052
+ }) : null;
2053
+ if (merged.decorations) {
2054
+ renderDecorations(root, rect, merged.decorations, strokeStyle, seed, {
2055
+ avoid: [note?.box, arrow?.bounds].filter(Boolean),
2056
+ note: note?.box ?? null
2057
+ });
2058
+ }
2059
+ if (note) root.appendChild(note.group);
2060
+ const bounds = inflateRect(
2061
+ unionRects([rect, note?.box, arrow?.bounds].filter(Boolean)),
2062
+ INK_MARGIN
2063
+ );
2064
+ return { root, bounds };
2065
+ }
2066
+
2067
+ // src/native/svg-props.js
2068
+ var ATTRIBUTES = {
2069
+ d: "d",
2070
+ fill: "fill",
2071
+ stroke: "stroke",
2072
+ "stroke-width": "strokeWidth",
2073
+ "stroke-linecap": "strokeLinecap",
2074
+ "stroke-linejoin": "strokeLinejoin",
2075
+ "stroke-dasharray": "strokeDasharray",
2076
+ opacity: "opacity",
2077
+ transform: "transform",
2078
+ r: "r",
2079
+ cx: "cx",
2080
+ cy: "cy",
2081
+ x: "x",
2082
+ y: "y",
2083
+ dy: "dy",
2084
+ "font-size": "fontSize",
2085
+ "font-weight": "fontWeight",
2086
+ "letter-spacing": "letterSpacing",
2087
+ "text-anchor": "textAnchor"
2088
+ };
2089
+ function firstFamily(stack) {
2090
+ const first = String(stack ?? "").split(",")[0].trim().replace(/^["']|["']$/g, "");
2091
+ return first && first !== "cursive" ? first : void 0;
2092
+ }
2093
+ function parseAnimation(value) {
2094
+ const parts = String(value ?? "").trim().split(/\s+/);
2095
+ if (parts.length < 5) return null;
2096
+ const [name, duration, easing, delay, iterations] = parts;
2097
+ return {
2098
+ name,
2099
+ duration: parseFloat(duration) || 0,
2100
+ easing,
2101
+ delay: parseFloat(delay) || 0,
2102
+ infinite: iterations === "infinite"
2103
+ };
2104
+ }
2105
+ function propsOf(node) {
2106
+ const props = {};
2107
+ for (const [name, value] of Object.entries(node.attributes)) {
2108
+ const prop = ATTRIBUTES[name];
2109
+ if (prop) props[prop] = value;
2110
+ }
2111
+ const family = firstFamily(node.getAttribute("font-family"));
2112
+ if (family) props.fontFamily = family;
2113
+ if (node.getAttribute("direction") === "rtl") props.textAnchor = "end";
2114
+ if (node.tagName === "text" && node.children.length && node.children.every((child) => child.getAttribute("x") != null)) {
2115
+ delete props.x;
2116
+ }
2117
+ return props;
2118
+ }
2119
+
2120
+ // src/native/react-native.js
2121
+ var AnimatedPath = import_react_native.Animated.createAnimatedComponent(import_react_native_svg.Path);
2122
+ var OPTION_KEYS = [
2123
+ "border",
2124
+ "color",
2125
+ "strokeWidth",
2126
+ "roughness",
2127
+ "padding",
2128
+ "radius",
2129
+ "opacity",
2130
+ "note",
2131
+ "arrow",
2132
+ "decorations",
2133
+ "addBreaks",
2134
+ "fontFamily",
2135
+ "locale",
2136
+ // Web-only, accepted so shared doodle configs can be spread onto either
2137
+ "autoLoadFont",
2138
+ "zIndex"
2139
+ ];
2140
+ var BAND_INSET = 8;
2141
+ var PLATFORM_HANDWRITING = import_react_native.Platform.select({
2142
+ ios: "Noteworthy",
2143
+ android: "casual",
2144
+ default: void 0
2145
+ });
2146
+ var ELEMENTS = { g: import_react_native_svg.G, path: import_react_native_svg.Path, circle: import_react_native_svg.Circle, text: import_react_native_svg.Text };
2147
+ var EASINGS = {
2148
+ linear: import_react_native.Easing.linear,
2149
+ // The CSS keywords' own curves
2150
+ "ease-out": import_react_native.Easing.bezier(0, 0, 0.58, 1),
2151
+ "ease-in-out": import_react_native.Easing.bezier(0.42, 0, 0.58, 1)
2152
+ };
2153
+ function blurOf(node) {
2154
+ const match = /blur\(\s*([\d.]+)px\s*\)/.exec(node.style.filter ?? "");
2155
+ return match ? Number(match[1]) : 0;
2156
+ }
2157
+ function renderNode(node, key, context) {
2158
+ const props = { key, ...propsOf(node) };
2159
+ if (node.tagName === "tspan") return (0, import_react.createElement)(import_react_native_svg.TSpan, props, node.textContent);
2160
+ if (node.tagName === "circle" && blurOf(node) > 0) {
2161
+ const id = `${context.prefix}-glow-${context.defs.length}`;
2162
+ const color = props.fill;
2163
+ context.defs.push(
2164
+ (0, import_react.createElement)(
2165
+ import_react_native_svg.RadialGradient,
2166
+ { key: id, id, cx: "50%", cy: "50%", r: "50%" },
2167
+ (0, import_react.createElement)(import_react_native_svg.Stop, { offset: "0", stopColor: color, stopOpacity: 1 }),
2168
+ (0, import_react.createElement)(import_react_native_svg.Stop, { offset: "0.55", stopColor: color, stopOpacity: 0.55 }),
2169
+ (0, import_react.createElement)(import_react_native_svg.Stop, { offset: "1", stopColor: color, stopOpacity: 0 })
2170
+ )
2171
+ );
2172
+ props.fill = `url(#${id})`;
2173
+ props.r = String(Number(props.r) + blurOf(node) * 1.5);
2174
+ }
2175
+ const animation = parseAnimation(node.style.animation);
2176
+ if (node.tagName === "path" && context.clock && animation?.name === "freehand-arrow-dash" && // A dotted shaft crawls forever on the web; that would keep the JS thread
2177
+ // busy for as long as the doodle is on screen, so it is left still here
2178
+ !animation.infinite && animation.duration > 0) {
2179
+ const length = node.getTotalLength();
2180
+ if (length > 0) {
2181
+ return (0, import_react.createElement)(AnimatedPath, {
2182
+ ...props,
2183
+ strokeDasharray: [length, length],
2184
+ strokeDashoffset: context.clock.interpolate({
2185
+ inputRange: [animation.delay, animation.delay + animation.duration],
2186
+ outputRange: [length, 0],
2187
+ easing: EASINGS[animation.easing] ?? EASINGS.linear,
2188
+ extrapolate: "clamp"
2189
+ })
2190
+ });
2191
+ }
2192
+ }
2193
+ const Component = ELEMENTS[node.tagName];
2194
+ if (!Component) return null;
2195
+ return (0, import_react.createElement)(
2196
+ Component,
2197
+ props,
2198
+ node.children.map((child, index) => renderNode(child, index, context))
2199
+ );
2200
+ }
2201
+ function splitLayers(root) {
2202
+ const layers = [];
2203
+ let run = null;
2204
+ for (const child of root.children) {
2205
+ if (child.getAttribute("class") === "freehand-arrow") {
2206
+ layers.push({ arrow: true, nodes: [child] });
2207
+ run = null;
2208
+ } else {
2209
+ if (!run) {
2210
+ run = { arrow: false, nodes: [] };
2211
+ layers.push(run);
2212
+ }
2213
+ run.nodes.push(child);
2214
+ }
2215
+ }
2216
+ return layers;
2217
+ }
2218
+ function LayerSvg({ nodes, bounds, clock, prefix }) {
2219
+ const context = { defs: [], clock, prefix };
2220
+ const content = nodes.map((node, index) => renderNode(node, index, context));
2221
+ return (0, import_react.createElement)(
2222
+ import_react_native_svg.Svg,
2223
+ {
2224
+ style: import_react_native.StyleSheet.absoluteFill,
2225
+ width: bounds.width,
2226
+ height: bounds.height
2227
+ },
2228
+ context.defs.length ? (0, import_react.createElement)(import_react_native_svg.Defs, null, context.defs) : null,
2229
+ // Drawing coordinates are the element's own; the canvas starts further out
2230
+ (0, import_react.createElement)(import_react_native_svg.G, { transform: `translate(${-bounds.x} ${-bounds.y})` }, content)
2231
+ );
2232
+ }
2233
+ function arrowPlan(group) {
2234
+ let drawEnd = 0;
2235
+ for (const path of group.children) {
2236
+ const animation = parseAnimation(path.style.animation);
2237
+ if (animation && !animation.infinite) {
2238
+ drawEnd = Math.max(drawEnd, animation.delay + animation.duration);
2239
+ }
2240
+ }
2241
+ const lean = parseAnimation(group.style.animation);
2242
+ const dx = parseFloat(group.style.getPropertyValue("--freehand-drift-x")) || 0;
2243
+ const dy = parseFloat(group.style.getPropertyValue("--freehand-drift-y")) || 0;
2244
+ return {
2245
+ drawEnd,
2246
+ drift: lean && (dx || dy) ? {
2247
+ dx,
2248
+ dy,
2249
+ duration: lean.duration,
2250
+ delay: Math.max(0, lean.delay),
2251
+ repeat: lean.infinite
2252
+ } : null
2253
+ };
2254
+ }
2255
+ function ArrowLayer({ node, bounds, prefix }) {
2256
+ const clock = (0, import_react.useRef)(new import_react_native.Animated.Value(0)).current;
2257
+ const lean = (0, import_react.useRef)(new import_react_native.Animated.Value(0)).current;
2258
+ const plan = arrowPlan(node);
2259
+ const initialPlan = (0, import_react.useRef)(plan).current;
2260
+ (0, import_react.useEffect)(() => {
2261
+ const running = [];
2262
+ let timer = null;
2263
+ if (initialPlan.drawEnd > 0) {
2264
+ const draw = import_react_native.Animated.timing(clock, {
2265
+ toValue: initialPlan.drawEnd,
2266
+ duration: initialPlan.drawEnd,
2267
+ easing: import_react_native.Easing.linear,
2268
+ useNativeDriver: false
2269
+ });
2270
+ draw.start();
2271
+ running.push(draw);
2272
+ }
2273
+ const drift2 = initialPlan.drift;
2274
+ if (drift2) {
2275
+ const step = (toValue, duration, easing) => import_react_native.Animated.timing(lean, { toValue, duration, easing, useNativeDriver: true });
2276
+ const gesture = drift2.repeat ? import_react_native.Animated.loop(
2277
+ import_react_native.Animated.sequence([
2278
+ step(1, drift2.duration / 2, EASINGS["ease-in-out"]),
2279
+ step(0, drift2.duration / 2, EASINGS["ease-in-out"])
2280
+ ])
2281
+ ) : step(1, drift2.duration, EASINGS["ease-out"]);
2282
+ timer = setTimeout(() => gesture.start(), drift2.delay);
2283
+ running.push(gesture);
2284
+ }
2285
+ return () => {
2286
+ if (timer != null) clearTimeout(timer);
2287
+ for (const animation of running) animation.stop();
2288
+ };
2289
+ }, [clock, lean, initialPlan]);
2290
+ const drift = plan.drift;
2291
+ const transform = drift ? [
2292
+ { translateX: lean.interpolate({ inputRange: [0, 1], outputRange: [0, drift.dx] }) },
2293
+ { translateY: lean.interpolate({ inputRange: [0, 1], outputRange: [0, drift.dy] }) }
2294
+ ] : void 0;
2295
+ return (0, import_react.createElement)(
2296
+ import_react_native.Animated.View,
2297
+ { pointerEvents: "none", style: [import_react_native.StyleSheet.absoluteFill, { transform }] },
2298
+ (0, import_react.createElement)(LayerSvg, { nodes: [node], bounds, clock, prefix })
2299
+ );
2300
+ }
2301
+ function DoodleLayers({ doodle, session, animate }) {
2302
+ const { root, bounds } = doodle;
2303
+ const layers = splitLayers(root);
2304
+ return (0, import_react.createElement)(
2305
+ import_react_native.View,
2306
+ {
2307
+ pointerEvents: "none",
2308
+ accessibilityElementsHidden: true,
2309
+ importantForAccessibility: "no-hide-descendants",
2310
+ style: {
2311
+ position: "absolute",
2312
+ left: bounds.x,
2313
+ top: bounds.y,
2314
+ width: bounds.width,
2315
+ height: bounds.height
2316
+ }
2317
+ },
2318
+ layers.map((layer, index) => {
2319
+ const prefix = `fh${session.id}-${index}`;
2320
+ return layer.arrow && animate ? (
2321
+ // Keyed by session: new options restart the draw, a relayout does not
2322
+ (0, import_react.createElement)(ArrowLayer, { key: `a${session.id}-${index}`, node: layer.nodes[0], bounds, prefix })
2323
+ ) : (0, import_react.createElement)(LayerSvg, { key: `s${index}`, nodes: layer.nodes, bounds, clock: null, prefix });
2324
+ })
2325
+ );
2326
+ }
2327
+ function useReducedMotion() {
2328
+ const [reduced, setReduced] = (0, import_react.useState)(false);
2329
+ (0, import_react.useEffect)(() => {
2330
+ let alive = true;
2331
+ import_react_native.AccessibilityInfo.isReduceMotionEnabled?.().then((value) => alive && setReduced(Boolean(value))).catch(() => {
2332
+ });
2333
+ const subscription = import_react_native.AccessibilityInfo.addEventListener?.(
2334
+ "reduceMotionChanged",
2335
+ (value) => setReduced(Boolean(value))
2336
+ );
2337
+ return () => {
2338
+ alive = false;
2339
+ subscription?.remove?.();
2340
+ };
2341
+ }, []);
2342
+ return reduced;
2343
+ }
2344
+ function mergeRefs(...refs) {
2345
+ return (value) => {
2346
+ for (const ref of refs) {
2347
+ if (!ref) continue;
2348
+ if (typeof ref === "function") ref(value);
2349
+ else ref.current = value;
2350
+ }
2351
+ };
2352
+ }
2353
+ var sessions = 0;
2354
+ var Doodle = (0, import_react.forwardRef)(function Doodle2(props, forwardedRef) {
2355
+ const { children, style, disabled = false, onLayout, ...rest } = props;
2356
+ const options = {};
2357
+ const viewProps = {};
2358
+ for (const [name, value] of Object.entries(rest)) {
2359
+ if (OPTION_KEYS.includes(name)) {
2360
+ if (value !== void 0) options[name] = value;
2361
+ } else {
2362
+ viewProps[name] = value;
2363
+ }
2364
+ }
2365
+ if (options.fontFamily == null && PLATFORM_HANDWRITING) {
2366
+ options.fontFamily = PLATFORM_HANDWRITING;
2367
+ }
2368
+ if (options.radius == null) {
2369
+ const radius = import_react_native.StyleSheet.flatten(style)?.borderRadius;
2370
+ if (typeof radius === "number") options.radius = radius;
2371
+ }
2372
+ const key = JSON.stringify(options);
2373
+ const session = (0, import_react.useMemo)(
2374
+ () => ({ id: ++sessions, seed: Math.floor(Math.random() * 1e9) }),
2375
+ [key]
2376
+ );
2377
+ const viewRef = (0, import_react.useRef)(null);
2378
+ const onLayoutRef = (0, import_react.useRef)(onLayout);
2379
+ onLayoutRef.current = onLayout;
2380
+ const [frame, setFrame] = (0, import_react.useState)(null);
2381
+ const { width: windowWidth } = (0, import_react_native.useWindowDimensions)();
2382
+ const reducedMotion = useReducedMotion();
2383
+ const handleLayout = (0, import_react.useCallback)((event) => {
2384
+ onLayoutRef.current?.(event);
2385
+ const { width, height } = event.nativeEvent.layout;
2386
+ setFrame(
2387
+ (previous) => previous && previous.width === width && previous.height === height ? previous : { left: previous?.left ?? null, width, height }
2388
+ );
2389
+ viewRef.current?.measureInWindow?.((left) => {
2390
+ if (!Number.isFinite(left)) return;
2391
+ setFrame(
2392
+ (previous) => previous && Math.round(previous.left ?? NaN) !== Math.round(left) ? { ...previous, left } : previous
2393
+ );
2394
+ });
2395
+ }, []);
2396
+ const doodle = (0, import_react.useMemo)(() => {
2397
+ if (disabled || !frame || frame.width <= 0 || frame.height <= 0) return null;
2398
+ const band = frame.left == null ? null : {
2399
+ x: -frame.left + BAND_INSET,
2400
+ width: Math.max(0, windowWidth - BAND_INSET * 2)
2401
+ };
2402
+ return buildDoodle({
2403
+ width: frame.width,
2404
+ height: frame.height,
2405
+ options: JSON.parse(key),
2406
+ seed: session.seed,
2407
+ band
2408
+ });
2409
+ }, [disabled, frame, key, session, windowWidth]);
2410
+ return (0, import_react.createElement)(
2411
+ import_react_native.View,
2412
+ {
2413
+ ...viewProps,
2414
+ ref: mergeRefs(viewRef, forwardedRef),
2415
+ style,
2416
+ onLayout: handleLayout
2417
+ },
2418
+ children,
2419
+ doodle ? (0, import_react.createElement)(DoodleLayers, { doodle, session, animate: !reducedMotion }) : null
2420
+ );
2421
+ });
2422
+ var react_native_default = Doodle;