@aznabee/freehand-ui 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/react.cjs CHANGED
@@ -28,6 +28,265 @@ __export(react_exports, {
28
28
  module.exports = __toCommonJS(react_exports);
29
29
  var import_react = require("react");
30
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
+
31
290
  // src/utils.js
32
291
  var DEFAULT_OPTIONS = {
33
292
  border: true,
@@ -46,7 +305,17 @@ var DEFAULT_OPTIONS = {
46
305
  /** Override the handwriting stack, e.g. a next/font CSS variable. */
47
306
  fontFamily: null,
48
307
  /** Fetch Caveat when the page has not provided it. */
49
- autoLoadFont: true
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
50
319
  };
51
320
  function resolveElement(target) {
52
321
  if (!target) return null;
@@ -60,8 +329,15 @@ function resolveElement(target) {
60
329
  }
61
330
  function mergeOptions(options = {}) {
62
331
  const merged = { ...DEFAULT_OPTIONS, ...options };
63
- if (merged.note && typeof merged.note === "string") {
64
- merged.note = { text: merged.note, position: "top-right" };
332
+ if (merged.note) {
333
+ const config = typeof merged.note === "string" || isLocaleMap(merged.note) ? { text: merged.note } : merged.note;
334
+ merged.note = {
335
+ ...config,
336
+ position: config.position ?? "top-right",
337
+ // The note carries the chosen language, so `renderAnnotation` can resolve
338
+ // a multilingual note on its own.
339
+ locale: config.locale ?? merged.locale ?? null
340
+ };
65
341
  }
66
342
  if (merged.arrow) {
67
343
  const config = merged.arrow === true ? {} : merged.arrow;
@@ -71,7 +347,10 @@ function mergeOptions(options = {}) {
71
347
  // explicit `from` is honoured whether or not there is a note.
72
348
  from: config.from ?? (merged.note ? "note" : "top-right"),
73
349
  to: config.to ?? "edge",
74
- style: config.style ?? "curved"
350
+ style: config.style ?? "curved",
351
+ // Arrows draw themselves on and lean toward their target by default;
352
+ // `animate: false` opts out, an object tunes speed and gestures
353
+ animate: normalizeArrowAnimation(config.animate)
75
354
  };
76
355
  }
77
356
  if (merged.decorations === true) {
@@ -139,6 +418,40 @@ function getScrollableAncestors(element) {
139
418
  ancestors.push(window);
140
419
  return ancestors;
141
420
  }
421
+ function resolveOverlayZIndex(element, override) {
422
+ if (override != null) return override;
423
+ const { zIndex, position } = getComputedStyle(element);
424
+ if (zIndex !== "auto") {
425
+ const parsed = Number.parseInt(zIndex, 10);
426
+ if (Number.isFinite(parsed)) return parsed + 1;
427
+ }
428
+ let node = element.parentElement;
429
+ while (node && node !== document.documentElement) {
430
+ const style = getComputedStyle(node);
431
+ if (style.zIndex !== "auto" && style.position !== "static") {
432
+ const parsed = Number.parseInt(style.zIndex, 10);
433
+ if (Number.isFinite(parsed)) return parsed + 1;
434
+ }
435
+ node = node.parentElement;
436
+ }
437
+ return null;
438
+ }
439
+ function insertOverlaySvg(svg, element) {
440
+ const parent = element.parentNode;
441
+ if (!parent) {
442
+ document.body.appendChild(svg);
443
+ return;
444
+ }
445
+ parent.insertBefore(svg, element.nextSibling);
446
+ }
447
+ function syncOverlayStacking(svg, element, zIndexOverride) {
448
+ const zIndex = resolveOverlayZIndex(element, zIndexOverride);
449
+ if (zIndex == null) {
450
+ svg.style.zIndex = "";
451
+ } else {
452
+ svg.style.zIndex = String(zIndex);
453
+ }
454
+ }
142
455
 
143
456
  // src/geometry.js
144
457
  function averageCornerRadius(element) {
@@ -286,6 +599,7 @@ function annotationAnchor(position, rect, offset = 12) {
286
599
 
287
600
  // src/renderer.js
288
601
  var SVG_NS = "http://www.w3.org/2000/svg";
602
+ var XML_NS = "http://www.w3.org/XML/1998/namespace";
289
603
  var HANDWRITTEN_FONT = '"Caveat", "Kalam", "Patrick Hand", "Bradley Hand", "Segoe Script", "Comic Sans MS", cursive';
290
604
  var NOTE_FONT_SIZE = 19;
291
605
  function clearSvg(svg) {
@@ -531,6 +845,15 @@ function appendPath(svg, d, style) {
531
845
  svg.appendChild(path);
532
846
  return path;
533
847
  }
848
+ function appendGlow(svg, radius, style) {
849
+ const circle = document.createElementNS(SVG_NS, "circle");
850
+ circle.setAttribute("r", fmt(radius));
851
+ circle.setAttribute("fill", style.color);
852
+ circle.setAttribute("opacity", String(style.opacity));
853
+ circle.style.filter = `blur(${fmt(radius * 0.4)}px)`;
854
+ svg.appendChild(circle);
855
+ return circle;
856
+ }
534
857
  function rectPerimeter(width, height, radius) {
535
858
  const r = clamp(radius, 0, Math.min(width, height) / 2);
536
859
  return 2 * (width - 2 * r) + 2 * (height - 2 * r) + 2 * Math.PI * r;
@@ -617,20 +940,21 @@ function cubicTangentAt(p0, c1, c2, p3, t) {
617
940
  y: a * (c1.y - p0.y) + b * (c2.y - c1.y) + c * (p3.y - c2.y)
618
941
  };
619
942
  }
620
- function loopedCurvePoints(start, c1, c2, end, radius, random) {
943
+ var STRADDLE = 0.75;
944
+ function loopedCurvePoints(start, c1, c2, end, radius, random, side = 1) {
621
945
  const at = (t) => cubicAt(start, c1, c2, end, t);
622
- const tangentAt = (t) => normalize(cubicTangentAt(start, c1, c2, end, t));
623
946
  const knot = 0.46 + random() * 0.12;
624
947
  const points = [];
625
948
  const lead = Math.max(2, Math.round(knot * 16));
626
949
  for (let i = 0; i <= lead; i++) points.push(at(knot * i / lead));
627
950
  const pivot = at(knot);
628
- const heading = tangentAt(knot);
951
+ const derivative = cubicTangentAt(start, c1, c2, end, knot);
952
+ const speed = Math.hypot(derivative.x, derivative.y) || 1;
953
+ const heading = normalize(derivative);
629
954
  const normal = { x: -heading.y, y: heading.x };
630
- const side = random() < 0.5 ? 1 : -1;
631
955
  const centre = {
632
- x: pivot.x + normal.x * radius * side,
633
- y: pivot.y + normal.y * radius * side
956
+ x: pivot.x + normal.x * radius * STRADDLE * side,
957
+ y: pivot.y + normal.y * radius * STRADDLE * side
634
958
  };
635
959
  const from = Math.atan2(pivot.y - centre.y, pivot.x - centre.x);
636
960
  const turns = Math.PI * 2 * (0.97 + random() * 0.08);
@@ -638,15 +962,27 @@ function loopedCurvePoints(start, c1, c2, end, radius, random) {
638
962
  for (let i = 1; i <= steps; i++) {
639
963
  const progress = i / steps;
640
964
  const angle = from + turns * progress * side;
965
+ const spread = radius * (STRADDLE + (1 - STRADDLE) * progress);
641
966
  const drift = radius * 0.55 * progress;
642
967
  points.push({
643
- x: centre.x + Math.cos(angle) * radius + heading.x * drift,
644
- y: centre.y + Math.sin(angle) * radius + heading.y * drift
968
+ x: centre.x + Math.cos(angle) * spread + heading.x * drift,
969
+ y: centre.y + Math.sin(angle) * spread + heading.y * drift
645
970
  });
646
971
  }
972
+ const exit = points[points.length - 1];
973
+ const gained = (exit.x - pivot.x) * heading.x + (exit.y - pivot.y) * heading.y;
974
+ const resume = Math.min(knot + Math.max(0, gained) / speed, 0.985);
975
+ const rejoin = at(resume);
976
+ const residual = (exit.x - rejoin.x) * normal.x + (exit.y - rejoin.y) * normal.y;
977
+ const settle = 6;
647
978
  const tail = 14;
648
979
  for (let i = 1; i <= tail; i++) {
649
- points.push(at(knot + (1 - knot) * (i / tail)));
980
+ const point = at(resume + (1 - resume) * (i / tail));
981
+ const fade = Math.max(0, 1 - i / settle);
982
+ points.push({
983
+ x: point.x + normal.x * residual * fade,
984
+ y: point.y + normal.y * residual * fade
985
+ });
650
986
  }
651
987
  return points;
652
988
  }
@@ -720,21 +1056,29 @@ function arrowEndpoints(rect, arrowOptions, random, origin) {
720
1056
  function drawArrowHead(svg, tip, angle, style, random, shaftLength = Infinity) {
721
1057
  const length = Math.min(9 + style.strokeWidth * 1.4, shaftLength * 0.3);
722
1058
  const spread = 0.46 + random() * 0.1;
1059
+ const arms = [];
723
1060
  for (const side of [-1, 1]) {
724
1061
  const armAngle = angle + spread * side;
725
1062
  const tail = {
726
1063
  x: tip.x - length * Math.cos(armAngle),
727
1064
  y: tip.y - length * Math.sin(armAngle)
728
1065
  };
729
- appendPath(
730
- svg,
731
- roughLine(tail.x, tail.y, tip.x, tip.y, style.roughness * 0.5, random, {
732
- steps: 2,
733
- bow: 0.35 * side
734
- }),
735
- { ...style, dashed: false }
1066
+ arms.push(
1067
+ appendPath(
1068
+ svg,
1069
+ roughLine(tail.x, tail.y, tip.x, tip.y, style.roughness * 0.5, random, {
1070
+ steps: 2,
1071
+ bow: 0.35 * side
1072
+ }),
1073
+ { ...style, dashed: false }
1074
+ )
736
1075
  );
737
1076
  }
1077
+ return arms;
1078
+ }
1079
+ function bowOverTheTop(perpendicular) {
1080
+ if (Math.abs(perpendicular.y) < 0.02) return perpendicular.x > 0 ? 1 : -1;
1081
+ return perpendicular.y > 0 ? -1 : 1;
738
1082
  }
739
1083
  function boundsOf(points) {
740
1084
  const xs = points.map((p) => p.x);
@@ -746,7 +1090,7 @@ function boundsOf(points) {
746
1090
  height: Math.max(...ys) - Math.min(...ys)
747
1091
  };
748
1092
  }
749
- function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
1093
+ function drawArrow(svg, rect, arrowOptions, style, seed, origin = null, context = {}) {
750
1094
  const random = createRandom(seed + 17);
751
1095
  const { start, end } = arrowEndpoints(rect, arrowOptions, random, origin);
752
1096
  const dx = end.x - start.x;
@@ -755,6 +1099,7 @@ function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
755
1099
  const dashed = arrowOptions.style === "dotted";
756
1100
  let tipAngle = Math.atan2(dy, dx);
757
1101
  let shaft;
1102
+ let ghost = null;
758
1103
  const hull = [start, end];
759
1104
  if (arrowOptions.style === "straight") {
760
1105
  shaft = roughLine(start.x, start.y, end.x, end.y, style.roughness, random, {
@@ -768,7 +1113,12 @@ function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
768
1113
  y: rect.y + rect.height / 2 - mid.y
769
1114
  };
770
1115
  const lateral = perpendicular.x * towardCenter.x + perpendicular.y * towardCenter.y;
771
- const sign = Math.abs(lateral) > distance * 0.12 ? lateral > 0 ? -1 : 1 : random() < 0.5 ? -1 : 1;
1116
+ const sign = Math.abs(lateral) > distance * 0.12 ? lateral > 0 ? -1 : 1 : (
1117
+ // Nothing to steer around, so sweep over the top: a hand arcs above
1118
+ // the straight line and drops onto the target, and a coin flip here
1119
+ // used to mirror the whole gesture from one redraw to the next.
1120
+ bowOverTheTop(perpendicular)
1121
+ );
772
1122
  const bow = Math.min(distance * (0.16 + random() * 0.08), 30) * sign;
773
1123
  const c1x = start.x + dx * 0.28 + perpendicular.x * bow;
774
1124
  const c1y = start.y + dy * 0.28 + perpendicular.y * bow;
@@ -783,34 +1133,46 @@ function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
783
1133
  { x: c2x, y: c2y },
784
1134
  end,
785
1135
  radius,
786
- random
1136
+ random,
1137
+ -sign
787
1138
  );
788
1139
  shaft = catmullRomPath(points, false);
789
1140
  hull.push(...points);
790
1141
  const last = points[points.length - 1];
791
1142
  const prior = points[points.length - 2] ?? start;
792
1143
  tipAngle = Math.atan2(last.y - prior.y, last.x - prior.x);
793
- appendPath(svg, shaft, { ...style, dashed });
794
- drawArrowHead(svg, end, tipAngle, style, random, distance);
795
- return { start, end, bounds: boundsOf(hull) };
1144
+ } else {
1145
+ shaft = `M ${fmt(start.x)} ${fmt(start.y)} C ${fmt(c1x)} ${fmt(c1y)} ${fmt(c2x)} ${fmt(c2y)} ${fmt(end.x)} ${fmt(end.y)}`;
1146
+ if (style.roughness >= 1.1) {
1147
+ const drift = () => (random() - 0.5) * 1.4;
1148
+ 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)}`;
1149
+ }
1150
+ tipAngle = Math.atan2(end.y - c2y, end.x - c2x);
796
1151
  }
797
- shaft = `M ${fmt(start.x)} ${fmt(start.y)} C ${fmt(c1x)} ${fmt(c1y)} ${fmt(c2x)} ${fmt(c2y)} ${fmt(end.x)} ${fmt(end.y)}`;
798
- if (style.roughness >= 1.1) {
799
- const drift = () => (random() - 0.5) * 1.4;
800
- const 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)}`;
801
- appendPath(svg, ghost, {
1152
+ }
1153
+ const group = document.createElementNS(SVG_NS, "g");
1154
+ svg.appendChild(group);
1155
+ const shaftPaths = [];
1156
+ if (ghost) {
1157
+ shaftPaths.push(
1158
+ appendPath(group, ghost, {
802
1159
  ...style,
803
1160
  opacity: style.opacity * 0.3,
804
1161
  strokeWidth: style.strokeWidth * 0.7,
805
1162
  dashed
806
- });
807
- }
808
- tipAngle = Math.atan2(end.y - c2y, end.x - c2x);
1163
+ })
1164
+ );
809
1165
  }
810
- appendPath(svg, shaft, { ...style, dashed });
811
- drawArrowHead(svg, end, tipAngle, style, random, distance);
1166
+ shaftPaths.push(appendPath(group, shaft, { ...style, dashed }));
1167
+ const head = drawArrowHead(group, end, tipAngle, style, random, distance);
1168
+ animateArrow(
1169
+ group,
1170
+ { shaft: shaftPaths, head, start, end },
1171
+ arrowOptions.animate,
1172
+ context.elapsed
1173
+ );
812
1174
  const bounds = boundsOf(hull);
813
- return { start, end, bounds };
1175
+ return { start, end, bounds, group };
814
1176
  }
815
1177
  function drawUnderline(svg, x, y, width, style, random) {
816
1178
  if (width <= 4) return;
@@ -877,6 +1239,7 @@ function createNoteText(svg, text, style, options = {}) {
877
1239
  const fontSize = style.fontSize ?? NOTE_FONT_SIZE;
878
1240
  const lines = wrapWords(text, options.wordsPerLine ?? DEFAULT_WORDS_PER_LINE);
879
1241
  const lineHeight = fontSize * 1.05;
1242
+ const rtl = options.rtl ?? isRtlText(text);
880
1243
  const group = document.createElementNS(SVG_NS, "g");
881
1244
  const textEl = document.createElementNS(SVG_NS, "text");
882
1245
  textEl.setAttribute("x", "0");
@@ -889,6 +1252,13 @@ function createNoteText(svg, text, style, options = {}) {
889
1252
  textEl.setAttribute("letter-spacing", "0.4");
890
1253
  textEl.setAttribute("text-anchor", "start");
891
1254
  textEl.setAttribute("dominant-baseline", "auto");
1255
+ if (rtl) {
1256
+ textEl.setAttribute("direction", "rtl");
1257
+ textEl.setAttribute("unicode-bidi", "embed");
1258
+ }
1259
+ if (options.locale) {
1260
+ textEl.setAttributeNS(XML_NS, "xml:lang", options.locale);
1261
+ }
892
1262
  const tspans = lines.map((line, index) => {
893
1263
  const tspan = document.createElementNS(SVG_NS, "tspan");
894
1264
  tspan.textContent = line;
@@ -917,15 +1287,17 @@ function createNoteText(svg, text, style, options = {}) {
917
1287
  height,
918
1288
  ascent,
919
1289
  descent,
920
- fontSize
1290
+ fontSize,
1291
+ rtl
921
1292
  };
922
1293
  }
923
1294
  function placeNoteText(note, box, style, seed, options = {}) {
924
1295
  const random = createRandom(seed + 55);
925
1296
  const tilt = options.tilt ?? (random() - 0.5) * 7;
926
- note.textEl.setAttribute("x", fmt(box.x));
1297
+ const anchorX = note.rtl ? box.x + box.width : box.x;
1298
+ note.textEl.setAttribute("x", fmt(anchorX));
927
1299
  note.textEl.setAttribute("y", fmt(box.y + note.ascent));
928
- for (const tspan of note.tspans) tspan.setAttribute("x", fmt(box.x));
1300
+ for (const tspan of note.tspans) tspan.setAttribute("x", fmt(anchorX));
929
1301
  const cx = box.x + box.width / 2;
930
1302
  const cy = box.y + box.height / 2;
931
1303
  note.group.setAttribute(
@@ -934,9 +1306,10 @@ function placeNoteText(note, box, style, seed, options = {}) {
934
1306
  );
935
1307
  if (options.underline !== false) {
936
1308
  const lastWidth = note.lineWidths[note.lineWidths.length - 1] ?? box.width;
1309
+ const underlineX = note.rtl ? box.x + box.width - lastWidth + 1 : box.x + 1;
937
1310
  drawUnderline(
938
1311
  note.group,
939
- box.x + 1,
1312
+ underlineX,
940
1313
  box.y + box.height + 2,
941
1314
  lastWidth - 2,
942
1315
  {
@@ -964,7 +1337,7 @@ function createOverlaySvg(left, top, width, height) {
964
1337
  svg.style.height = `${height}px`;
965
1338
  svg.style.pointerEvents = "none";
966
1339
  svg.style.overflow = "visible";
967
- svg.style.zIndex = "2147483646";
1340
+ svg.style.willChange = "transform";
968
1341
  return svg;
969
1342
  }
970
1343
 
@@ -1011,12 +1384,17 @@ function nudgeIntoBand(box, guard, band) {
1011
1384
  return { ...box, x: box.x + dx };
1012
1385
  }
1013
1386
  function renderAnnotation(svg, rect, noteOptions, style, seed = 1, layout = {}) {
1014
- if (!noteOptions?.text) return null;
1387
+ const { text, locale } = resolveLocalizedText(
1388
+ noteOptions?.text,
1389
+ noteOptions?.locale
1390
+ );
1391
+ if (!text) return null;
1015
1392
  const band = layout.band ?? null;
1016
1393
  const position = isValidPosition(noteOptions.position) ? noteOptions.position : "top-right";
1017
1394
  const random = createRandom(seed + 9);
1018
- const note = createNoteText(svg, noteOptions.text, style, {
1019
- wordsPerLine: noteOptions.wordsPerLine
1395
+ const note = createNoteText(svg, text, style, {
1396
+ wordsPerLine: noteOptions.wordsPerLine,
1397
+ locale
1020
1398
  });
1021
1399
  const tilt = (random() - 0.45) * 7;
1022
1400
  const tiltPad = Math.abs(Math.sin(tilt * Math.PI / 180)) * note.width * 0.5;
@@ -1058,6 +1436,25 @@ function drawDot(parent, x, y, style, scale = 1.7) {
1058
1436
  strokeWidth: style.strokeWidth * scale
1059
1437
  });
1060
1438
  }
1439
+ function drawFleck(parent, x, y, radius, style, random) {
1440
+ const count = 6;
1441
+ const points = [];
1442
+ for (let i = 0; i < count; i++) {
1443
+ const angle = Math.PI * 2 * i / count + (random() - 0.5) * 0.5;
1444
+ const r = radius * (0.78 + random() * 0.34);
1445
+ points.push({
1446
+ x: x + Math.cos(angle) * r,
1447
+ y: y + Math.sin(angle) * r
1448
+ });
1449
+ }
1450
+ const blob = appendPath(parent, catmullRomPath(points, true), {
1451
+ ...style,
1452
+ fill: style.color,
1453
+ strokeWidth: 0
1454
+ });
1455
+ blob.style.filter = `blur(${(radius * 0.35).toFixed(2)}px)`;
1456
+ return blob;
1457
+ }
1061
1458
  function drawHeart(parent, size, style, random) {
1062
1459
  const s = size;
1063
1460
  const j = () => (random() - 0.5) * s * 0.12;
@@ -1081,26 +1478,61 @@ function twinkle(parent, radiusX, radiusY, pinch, style) {
1081
1478
  `Q ${(-c).toFixed(2)} ${(-c).toFixed(2)} 0 ${(-ry).toFixed(2)}`,
1082
1479
  "Z"
1083
1480
  ].join(" ");
1084
- appendPath(parent, path, style);
1481
+ return appendPath(parent, path, style);
1085
1482
  }
1086
1483
  function drawSparkle(parent, size, style, random) {
1087
1484
  twinkle(
1088
1485
  parent,
1089
- size * (0.34 + random() * 0.1),
1486
+ size * (0.62 + random() * 0.12),
1090
1487
  size,
1091
- size * (0.06 + random() * 0.06),
1488
+ size * (0.14 + random() * 0.08),
1092
1489
  style
1093
1490
  );
1094
1491
  }
1095
1492
  function drawStar(parent, size, style, random) {
1096
1493
  twinkle(
1097
1494
  parent,
1098
- size * (0.66 + random() * 0.12),
1099
- size,
1100
- size * (0.2 + random() * 0.08),
1495
+ size * (0.62 + random() * 0.12),
1496
+ size * (0.8 + random() * 0.12),
1497
+ size * (0.14 + random() * 0.08),
1101
1498
  style
1102
1499
  );
1103
1500
  }
1501
+ function drawGlimmer(parent, size, style, random) {
1502
+ appendGlow(parent, size * (1.2 + random() * 0.2), {
1503
+ color: style.color,
1504
+ opacity: style.opacity * 0.22
1505
+ });
1506
+ const armX = size * (0.5 + random() * 0.08);
1507
+ const armY = size * (0.45 + random() * 0.08);
1508
+ const bluntness = size * 0.19;
1509
+ const main = twinkle(parent, armX, armY, size * (0.08 + random() * 0.03), {
1510
+ ...style,
1511
+ fill: style.color,
1512
+ strokeWidth: bluntness
1513
+ });
1514
+ main.setAttribute("stroke-linejoin", "round");
1515
+ main.setAttribute("stroke-linecap", "round");
1516
+ main.style.filter = `blur(${(size * 0.05).toFixed(2)}px)`;
1517
+ const gap = bluntness / 2 + size * 0.2;
1518
+ const corners = [
1519
+ { x: 0, y: -(armY + gap) },
1520
+ { x: armX + gap, y: 0 },
1521
+ { x: 0, y: armY + gap },
1522
+ { x: -(armX + gap), y: 0 }
1523
+ ];
1524
+ for (const { x, y } of corners) {
1525
+ const jitter = 0.95 + random() * 0.1;
1526
+ drawFleck(
1527
+ parent,
1528
+ x * jitter,
1529
+ y * jitter,
1530
+ size * (0.1 + random() * 0.03),
1531
+ style,
1532
+ random
1533
+ );
1534
+ }
1535
+ }
1104
1536
  function drawTwinkle(parent, size, style, random) {
1105
1537
  const count = 5;
1106
1538
  const first = -Math.PI / 2 + (random() - 0.5) * 0.5;
@@ -1208,46 +1640,15 @@ function drawEmphasis(parent, size, style, random, opts = {}) {
1208
1640
  );
1209
1641
  }
1210
1642
  }
1211
- function drawDots(parent, size, style, random) {
1212
- let x = -size * 0.7;
1213
- for (let i = 0; i < 3; i++) {
1214
- drawDot(parent, x, (random() - 0.5) * size * 0.16, style);
1215
- x += size * (0.6 + random() * 0.25);
1216
- }
1217
- }
1218
- function drawStroke(parent, size, style, random) {
1219
- appendPath(
1220
- parent,
1221
- roughLine(-size, 0, size, 0, 1.2, random, {
1222
- bow: (random() - 0.5) * size * 0.35,
1223
- steps: 4
1224
- }),
1225
- style
1226
- );
1227
- }
1228
- function drawSteam(parent, size, style, random) {
1229
- const lean = random() < 0.5 ? -1 : 1;
1230
- for (let i = 0; i < 2; i++) {
1231
- const x = (i - 0.5) * size * 0.72;
1232
- const base = size * 0.62 - i * size * 0.14;
1233
- appendPath(
1234
- parent,
1235
- `M ${x.toFixed(2)} ${base.toFixed(2)} Q ${(x + lean * size * 0.52).toFixed(2)} ${(base - size * 0.62).toFixed(2)} ${(x + lean * size * 0.06).toFixed(2)} ${(base - size * 1.3).toFixed(2)}`,
1236
- style
1237
- );
1238
- }
1239
- }
1240
1643
  var DECORATION_DRAWERS = {
1241
1644
  heart: drawHeart,
1242
1645
  sparkle: drawSparkle,
1243
1646
  star: drawStar,
1244
1647
  twinkle: drawTwinkle,
1648
+ glimmer: drawGlimmer,
1245
1649
  smiley: drawSmiley,
1246
1650
  emphasis: drawEmphasis,
1247
- arcs: drawArcs,
1248
- dots: drawDots,
1249
- stroke: drawStroke,
1250
- steam: drawSteam
1651
+ arcs: drawArcs
1251
1652
  };
1252
1653
  var TEXT_TYPES = ["heart", "sparkle"];
1253
1654
  var CORNER_ACCENTS = ["arcs", "emphasis"];
@@ -1258,12 +1659,10 @@ var TYPE_TRAITS = {
1258
1659
  sparkle: { size: [6, 9], tilt: 20, standoff: 22 },
1259
1660
  star: { size: [8, 11], tilt: 25, standoff: 26 },
1260
1661
  twinkle: { size: [5, 7.5], tilt: 30, standoff: 26, brightness: 1.4 },
1662
+ glimmer: { size: [7, 10], tilt: 20, standoff: 28, brightness: 1.3 },
1261
1663
  smiley: { size: [8, 11], tilt: 10, standoff: 22 },
1262
1664
  emphasis: { size: [12, 17], tilt: 10, standoff: 13, outward: true },
1263
- arcs: { size: [11, 16], tilt: 12, standoff: 12, outward: true },
1264
- dots: { size: [6, 9], tilt: 8, standoff: 20 },
1265
- stroke: { size: [8, 12], tilt: 40, standoff: 20 },
1266
- steam: { size: [8, 11], tilt: 10, standoff: 20 }
1665
+ arcs: { size: [11, 16], tilt: 12, standoff: 12, outward: true }
1267
1666
  };
1268
1667
  var DEFAULT_TYPES = [
1269
1668
  "twinkle",
@@ -1466,6 +1865,9 @@ function ensureHandwrittenFont() {
1466
1865
 
1467
1866
  // src/doodle.js
1468
1867
  var instances = /* @__PURE__ */ new WeakMap();
1868
+ function now() {
1869
+ return typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
1870
+ }
1469
1871
  function localBand(overlayRect, inset = 8) {
1470
1872
  const width = window.innerWidth || document.documentElement.clientWidth || 0;
1471
1873
  return {
@@ -1482,19 +1884,24 @@ var DoodleOverlay = class {
1482
1884
  this.element = element;
1483
1885
  this.options = options;
1484
1886
  this.seed = Math.floor(Math.random() * 1e9);
1887
+ this.startedAt = now();
1485
1888
  this.svg = null;
1486
1889
  this.resizeObserver = null;
1487
1890
  this.mutationObserver = null;
1488
1891
  this.pendingFrame = null;
1489
- this.scrollTargets = [];
1490
- this.onScroll = this.scheduleUpdate.bind(this);
1892
+ this.pendingRepositionFrame = null;
1893
+ this.basePosition = { x: 0, y: 0 };
1894
+ this.listeners = [];
1895
+ this.onScroll = this.scheduleReposition.bind(this);
1491
1896
  this.onResize = this.scheduleUpdate.bind(this);
1897
+ this.onLanguageChange = this.scheduleUpdate.bind(this);
1492
1898
  this.childElements = [];
1493
1899
  this.mount();
1494
1900
  }
1495
1901
  mount() {
1496
1902
  this.svg = createOverlaySvg(0, 0, 0, 0);
1497
- document.body.appendChild(this.svg);
1903
+ insertOverlaySvg(this.svg, this.element);
1904
+ syncOverlayStacking(this.svg, this.element, this.options.zIndex);
1498
1905
  this.resizeObserver = new ResizeObserver(this.onResize);
1499
1906
  this.resizeObserver.observe(this.element);
1500
1907
  if (this.options.children) {
@@ -1508,10 +1915,20 @@ var DoodleOverlay = class {
1508
1915
  });
1509
1916
  for (const target of getScrollableAncestors(this.element)) {
1510
1917
  target.addEventListener("scroll", this.onScroll, { passive: true });
1511
- this.scrollTargets.push({ target, type: "scroll" });
1918
+ this.listeners.push({ target, type: "scroll", handler: this.onScroll });
1512
1919
  }
1513
1920
  window.addEventListener("resize", this.onResize, { passive: true });
1514
- this.scrollTargets.push({ target: window, type: "resize" });
1921
+ this.listeners.push({
1922
+ target: window,
1923
+ type: "resize",
1924
+ handler: this.onResize
1925
+ });
1926
+ window.addEventListener("languagechange", this.onLanguageChange);
1927
+ this.listeners.push({
1928
+ target: window,
1929
+ type: "languagechange",
1930
+ handler: this.onLanguageChange
1931
+ });
1515
1932
  if (this.options.note && this.options.autoLoadFont && !this.options.fontFamily) {
1516
1933
  ensureHandwrittenFont();
1517
1934
  }
@@ -1535,6 +1952,46 @@ var DoodleOverlay = class {
1535
1952
  this.update();
1536
1953
  });
1537
1954
  }
1955
+ scheduleReposition() {
1956
+ if (this.pendingFrame != null || this.pendingRepositionFrame != null) {
1957
+ return;
1958
+ }
1959
+ this.pendingRepositionFrame = scheduleFrame(() => {
1960
+ this.pendingRepositionFrame = null;
1961
+ this.reposition();
1962
+ });
1963
+ }
1964
+ /**
1965
+ * Cheap scroll-driven path: slide the overlay to the element's new
1966
+ * viewport position without touching its contents. Scrolling can't change
1967
+ * an element's size, so the rects, viewBox and every hand-drawn stroke
1968
+ * inside them are still valid - only the overlay's position needs to move.
1969
+ *
1970
+ * That move is a `transform`, not `left`/`top`. `left`/`top` sit in the
1971
+ * same box-geometry pass as layout, so the browser re-checks layout on
1972
+ * every write; `transform` is composited, so the scroll path never
1973
+ * touches layout at all.
1974
+ */
1975
+ reposition() {
1976
+ if (!this.svg || !this.element.isConnected) return;
1977
+ const { padding, radius } = this.options;
1978
+ const targets = this.getTargets();
1979
+ const margin = this.overlayMargin();
1980
+ const absoluteRects = targets.map(
1981
+ (target) => getElementBounds(target, padding, radius)
1982
+ );
1983
+ const overlayRect = unionRects(absoluteRects, margin);
1984
+ const currentWidth = parseFloat(this.svg.style.width) || 0;
1985
+ const currentHeight = parseFloat(this.svg.style.height) || 0;
1986
+ const resized = Math.abs(overlayRect.width - currentWidth) > 0.5 || Math.abs(overlayRect.height - currentHeight) > 0.5;
1987
+ if (resized) {
1988
+ this.update();
1989
+ return;
1990
+ }
1991
+ const dx = overlayRect.x - this.basePosition.x;
1992
+ const dy = overlayRect.y - this.basePosition.y;
1993
+ this.svg.style.transform = `translate3d(${dx}px, ${dy}px, 0)`;
1994
+ }
1538
1995
  getTargets() {
1539
1996
  if (this.options.children) {
1540
1997
  const nodes = this.element.querySelectorAll(this.options.children);
@@ -1547,11 +2004,13 @@ var DoodleOverlay = class {
1547
2004
  const { padding, radius, color, strokeWidth, roughness, opacity } = this.options;
1548
2005
  const targets = this.getTargets();
1549
2006
  const margin = this.overlayMargin();
2007
+ const elapsed = now() - this.startedAt;
1550
2008
  const absoluteRects = targets.map(
1551
2009
  (target) => getElementBounds(target, padding, radius)
1552
2010
  );
1553
2011
  const overlayRect = unionRects(absoluteRects, margin);
1554
2012
  clearSvg(this.svg);
2013
+ syncOverlayStacking(this.svg, this.element, this.options.zIndex);
1555
2014
  this.svg.setAttribute("width", String(overlayRect.width));
1556
2015
  this.svg.setAttribute("height", String(overlayRect.height));
1557
2016
  this.svg.setAttribute("viewBox", `0 0 ${overlayRect.width} ${overlayRect.height}`);
@@ -1559,6 +2018,8 @@ var DoodleOverlay = class {
1559
2018
  this.svg.style.top = `${overlayRect.y}px`;
1560
2019
  this.svg.style.width = `${overlayRect.width}px`;
1561
2020
  this.svg.style.height = `${overlayRect.height}px`;
2021
+ this.svg.style.transform = "";
2022
+ this.basePosition = { x: overlayRect.x, y: overlayRect.y };
1562
2023
  const strokeStyle = { color, strokeWidth, roughness, opacity };
1563
2024
  const noteStyle = {
1564
2025
  color,
@@ -1598,7 +2059,8 @@ var DoodleOverlay = class {
1598
2059
  this.options.arrow,
1599
2060
  strokeStyle,
1600
2061
  seed,
1601
- note ? note.box : null
2062
+ note ? note.box : null,
2063
+ { elapsed }
1602
2064
  ) : null;
1603
2065
  if (this.options.decorations) {
1604
2066
  renderDecorations(
@@ -1625,7 +2087,7 @@ var DoodleOverlay = class {
1625
2087
  overlayMargin() {
1626
2088
  const { note, arrow, decorations } = this.options;
1627
2089
  if (!note && !arrow && !decorations) return 48;
1628
- const text = typeof note?.text === "string" ? note.text : "";
2090
+ const { text } = resolveLocalizedText(note?.text, note?.locale);
1629
2091
  return Math.max(96, Math.min(240, 80 + text.length * 8));
1630
2092
  }
1631
2093
  destroy() {
@@ -1633,6 +2095,10 @@ var DoodleOverlay = class {
1633
2095
  cancelFrame(this.pendingFrame);
1634
2096
  this.pendingFrame = null;
1635
2097
  }
2098
+ if (this.pendingRepositionFrame != null) {
2099
+ cancelFrame(this.pendingRepositionFrame);
2100
+ this.pendingRepositionFrame = null;
2101
+ }
1636
2102
  if (this.resizeObserver) {
1637
2103
  this.resizeObserver.disconnect();
1638
2104
  this.resizeObserver = null;
@@ -1645,10 +2111,10 @@ var DoodleOverlay = class {
1645
2111
  document.fonts?.removeEventListener?.("loadingdone", this.onFontsLoaded);
1646
2112
  this.onFontsLoaded = null;
1647
2113
  }
1648
- for (const { target, type } of this.scrollTargets) {
1649
- target.removeEventListener(type, type === "scroll" ? this.onScroll : this.onResize);
2114
+ for (const { target, type, handler } of this.listeners) {
2115
+ target.removeEventListener(type, handler);
1650
2116
  }
1651
- this.scrollTargets = [];
2117
+ this.listeners = [];
1652
2118
  if (this.svg?.parentNode) {
1653
2119
  this.svg.parentNode.removeChild(this.svg);
1654
2120
  }
@@ -1689,7 +2155,8 @@ var OPTION_KEYS = [
1689
2155
  "decorations",
1690
2156
  "addBreaks",
1691
2157
  "fontFamily",
1692
- "autoLoadFont"
2158
+ "autoLoadFont",
2159
+ "locale"
1693
2160
  ];
1694
2161
  function collectOptions(props) {
1695
2162
  const options = {};
@@ -1741,6 +2208,7 @@ var Doodle = (0, import_react.forwardRef)(function Doodle2(props, forwardedRef)
1741
2208
  addBreaks,
1742
2209
  fontFamily,
1743
2210
  autoLoadFont,
2211
+ locale,
1744
2212
  childSelector,
1745
2213
  ...rest
1746
2214
  } = props;
@@ -1760,6 +2228,7 @@ var Doodle = (0, import_react.forwardRef)(function Doodle2(props, forwardedRef)
1760
2228
  addBreaks,
1761
2229
  fontFamily,
1762
2230
  autoLoadFont,
2231
+ locale,
1763
2232
  childSelector
1764
2233
  }),
1765
2234
  [
@@ -1777,6 +2246,7 @@ var Doodle = (0, import_react.forwardRef)(function Doodle2(props, forwardedRef)
1777
2246
  JSON.stringify(addBreaks ?? null),
1778
2247
  fontFamily,
1779
2248
  autoLoadFont,
2249
+ JSON.stringify(locale ?? null),
1780
2250
  childSelector
1781
2251
  ]
1782
2252
  );