@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.js CHANGED
@@ -11,6 +11,265 @@ import {
11
11
  useRef
12
12
  } from "react";
13
13
 
14
+ // src/animation.js
15
+ var STYLE_MARKER = "data-freehand-animation";
16
+ var CSS = `
17
+ @keyframes freehand-arrow-dash {
18
+ from { stroke-dashoffset: var(--freehand-dash, 0px); }
19
+ to { stroke-dashoffset: 0; }
20
+ }
21
+ @keyframes freehand-arrow-drift {
22
+ 0%, 100% { transform: translate(0px, 0px); }
23
+ 50% { transform: translate(var(--freehand-drift-x, 0px), var(--freehand-drift-y, 0px)); }
24
+ }
25
+ @keyframes freehand-arrow-settle {
26
+ from { transform: translate(0px, 0px); }
27
+ to { transform: translate(var(--freehand-drift-x, 0px), var(--freehand-drift-y, 0px)); }
28
+ }
29
+ @media (prefers-reduced-motion: reduce) {
30
+ .freehand-arrow, .freehand-arrow path { animation: none !important; }
31
+ .freehand-arrow path { stroke-dashoffset: 0 !important; }
32
+ }
33
+ `;
34
+ var ARROW_ANIMATION_DEFAULTS = {
35
+ /** Reveal the stroke with a dash sweep. */
36
+ draw: true,
37
+ /** Lean the arrow toward its tip. */
38
+ drift: true,
39
+ /** Multiplier over every duration - 2 plays twice as fast. */
40
+ speed: 1,
41
+ /** How long the shaft takes to draw, ms, before `speed`. */
42
+ duration: 900,
43
+ /** Wait before the first stroke appears, ms. */
44
+ delay: 150,
45
+ /** How far the arrow leans, px. */
46
+ distance: 5,
47
+ /** One full lean-and-return, ms. */
48
+ driftDuration: 2200,
49
+ /** Keep leaning. `false` settles once and stays put. */
50
+ repeat: true
51
+ };
52
+ function positive(value, fallback) {
53
+ const n = Number(value);
54
+ return Number.isFinite(n) && n > 0 ? n : fallback;
55
+ }
56
+ function atLeastZero(value, fallback) {
57
+ const n = Number(value);
58
+ return Number.isFinite(n) && n >= 0 ? n : fallback;
59
+ }
60
+ function normalizeArrowAnimation(config) {
61
+ if (config === false || config === null) return false;
62
+ const settings = config === true || config === void 0 || typeof config !== "object" ? {} : config;
63
+ const animation = {
64
+ draw: settings.draw !== false,
65
+ drift: settings.drift !== false,
66
+ speed: positive(settings.speed, ARROW_ANIMATION_DEFAULTS.speed),
67
+ duration: positive(settings.duration, ARROW_ANIMATION_DEFAULTS.duration),
68
+ delay: atLeastZero(settings.delay, ARROW_ANIMATION_DEFAULTS.delay),
69
+ distance: atLeastZero(settings.distance, ARROW_ANIMATION_DEFAULTS.distance),
70
+ driftDuration: positive(
71
+ settings.driftDuration,
72
+ ARROW_ANIMATION_DEFAULTS.driftDuration
73
+ ),
74
+ repeat: settings.repeat !== false
75
+ };
76
+ if (!animation.draw && (!animation.drift || !animation.distance)) return false;
77
+ return animation;
78
+ }
79
+ var FLOW_PERIOD = 260;
80
+ function arrowTiming(animation) {
81
+ const duration = animation.duration / animation.speed;
82
+ const delay = animation.delay / animation.speed;
83
+ const drawn = animation.draw ? duration : 0;
84
+ return {
85
+ shaft: { duration, delay },
86
+ head: { duration: duration * 0.22, delay: delay + drawn * 0.82 },
87
+ flow: { duration: FLOW_PERIOD / animation.speed, delay },
88
+ drift: {
89
+ duration: animation.driftDuration / animation.speed,
90
+ delay: delay + drawn * 1.04
91
+ }
92
+ };
93
+ }
94
+ var injected = false;
95
+ function ensureArrowAnimationStyles() {
96
+ if (injected) return;
97
+ if (typeof document === "undefined" || !document.head) return;
98
+ injected = true;
99
+ if (document.querySelector(`style[${STYLE_MARKER}]`)) return;
100
+ const style = document.createElement("style");
101
+ style.setAttribute(STYLE_MARKER, "");
102
+ style.textContent = CSS;
103
+ document.head.appendChild(style);
104
+ }
105
+ function time(ms) {
106
+ return `${Math.round(ms)}ms`;
107
+ }
108
+ function setAnimation(element, spec) {
109
+ const parts = [
110
+ spec.name,
111
+ time(spec.duration),
112
+ spec.easing,
113
+ time(spec.delay),
114
+ spec.iterations ?? "1"
115
+ ];
116
+ if (spec.fill) parts.push(spec.fill);
117
+ element.style.animation = parts.join(" ");
118
+ }
119
+ function pathLength(path) {
120
+ try {
121
+ const length = path.getTotalLength();
122
+ return Number.isFinite(length) && length > 0 ? length : 0;
123
+ } catch {
124
+ return 0;
125
+ }
126
+ }
127
+ function dashPeriod(path) {
128
+ const pattern = (path.getAttribute("stroke-dasharray") || "").split(/[\s,]+/).map(Number).filter((n) => Number.isFinite(n) && n > 0);
129
+ if (!pattern.length) return 0;
130
+ const sum = pattern.reduce((total, n) => total + n, 0);
131
+ return pattern.length % 2 === 0 ? sum : sum * 2;
132
+ }
133
+ function applyDash(path, timing, elapsed, flow) {
134
+ const length = pathLength(path);
135
+ if (!length) return;
136
+ const period = dashPeriod(path);
137
+ if (period) {
138
+ path.style.setProperty("--freehand-dash", `${period.toFixed(2)}px`);
139
+ setAnimation(path, {
140
+ name: "freehand-arrow-dash",
141
+ duration: flow.duration,
142
+ easing: "linear",
143
+ delay: flow.delay - elapsed,
144
+ iterations: "infinite"
145
+ });
146
+ return;
147
+ }
148
+ path.style.setProperty("--freehand-dash", `${length.toFixed(2)}px`);
149
+ path.style.strokeDasharray = `${length.toFixed(2)}px`;
150
+ setAnimation(path, {
151
+ name: "freehand-arrow-dash",
152
+ duration: timing.duration,
153
+ easing: "ease-out",
154
+ delay: timing.delay - elapsed,
155
+ fill: "both"
156
+ });
157
+ }
158
+ function applyDrift(group, parts, timing, elapsed, animation) {
159
+ const dx = parts.end.x - parts.start.x;
160
+ const dy = parts.end.y - parts.start.y;
161
+ const length = Math.hypot(dx, dy);
162
+ if (!length) return;
163
+ const scale = animation.distance / length;
164
+ group.style.setProperty("--freehand-drift-x", `${(dx * scale).toFixed(2)}px`);
165
+ group.style.setProperty("--freehand-drift-y", `${(dy * scale).toFixed(2)}px`);
166
+ setAnimation(
167
+ group,
168
+ animation.repeat ? {
169
+ name: "freehand-arrow-drift",
170
+ duration: timing.duration,
171
+ easing: "ease-in-out",
172
+ delay: timing.delay - elapsed,
173
+ iterations: "infinite"
174
+ } : {
175
+ // A single lean is a settle, not a round trip, so it takes half a cycle
176
+ name: "freehand-arrow-settle",
177
+ duration: timing.duration * 0.5,
178
+ easing: "ease-out",
179
+ delay: timing.delay - elapsed,
180
+ fill: "both"
181
+ }
182
+ );
183
+ }
184
+ function animateArrow(group, parts, animation, elapsed = 0) {
185
+ if (!animation || !group) return;
186
+ ensureArrowAnimationStyles();
187
+ group.setAttribute("class", "freehand-arrow");
188
+ const timing = arrowTiming(animation);
189
+ const age = Number.isFinite(elapsed) ? Math.max(0, elapsed) : 0;
190
+ if (animation.draw) {
191
+ for (const path of parts.shaft) {
192
+ applyDash(path, timing.shaft, age, timing.flow);
193
+ }
194
+ for (const path of parts.head) applyDash(path, timing.head, age, timing.flow);
195
+ }
196
+ if (animation.drift && animation.distance > 0) {
197
+ applyDrift(group, parts, timing.drift, age, animation);
198
+ }
199
+ }
200
+
201
+ // src/locale.js
202
+ var DEFAULT_KEY = "default";
203
+ var LANGUAGE_TAG = /^[a-z]{2,3}(-[a-z0-9]{2,8})*$/i;
204
+ var RTL_STRONG = /[֑-߿יִ-﷿ﹰ-ﻼ]/;
205
+ var LTR_STRONG = /[A-Za-zÀ-֏ऀ-῿Ⰰ-퟿豈-ﬗA-Za-z]/;
206
+ function normalizeTag(tag) {
207
+ if (typeof tag !== "string") return "";
208
+ return tag.trim().replace(/_/g, "-").toLowerCase();
209
+ }
210
+ function isLocaleMap(value) {
211
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
212
+ const entries = Object.entries(value);
213
+ if (!entries.length) return false;
214
+ return entries.every(([key, text]) => {
215
+ if (typeof text !== "string") return false;
216
+ const tag = normalizeTag(key);
217
+ return tag === DEFAULT_KEY || LANGUAGE_TAG.test(tag);
218
+ });
219
+ }
220
+ function preferredLocales(override) {
221
+ const tags = [];
222
+ const add = (value) => {
223
+ const tag = normalizeTag(value);
224
+ if (tag && !tags.includes(tag)) tags.push(tag);
225
+ };
226
+ if (Array.isArray(override)) override.forEach(add);
227
+ else add(override);
228
+ const nav = typeof navigator === "undefined" ? null : navigator;
229
+ if (nav) {
230
+ if (Array.isArray(nav.languages)) nav.languages.forEach(add);
231
+ add(nav.language);
232
+ }
233
+ if (typeof document !== "undefined") add(document.documentElement?.lang);
234
+ return tags;
235
+ }
236
+ function matchLocale(tags, preferred) {
237
+ const available = tags.map(normalizeTag);
238
+ for (const want of preferred) {
239
+ let tag = want;
240
+ while (tag) {
241
+ const exact = available.indexOf(tag);
242
+ if (exact !== -1) return exact;
243
+ const cut = tag.lastIndexOf("-");
244
+ tag = cut > 0 ? tag.slice(0, cut) : "";
245
+ }
246
+ const base = want.split("-")[0];
247
+ const sibling = available.findIndex((key) => key.split("-")[0] === base);
248
+ if (sibling !== -1) return sibling;
249
+ }
250
+ return -1;
251
+ }
252
+ function resolveLocalizedText(text, locale = null) {
253
+ if (typeof text === "string") return { text, locale: null };
254
+ if (!isLocaleMap(text)) return { text: "", locale: null };
255
+ const keys = Object.keys(text);
256
+ const tags = keys.filter((key) => normalizeTag(key) !== DEFAULT_KEY);
257
+ const index = matchLocale(tags, preferredLocales(locale));
258
+ if (index !== -1) {
259
+ return { text: text[tags[index]], locale: normalizeTag(tags[index]) };
260
+ }
261
+ const fallback = keys.find((key) => normalizeTag(key) === DEFAULT_KEY);
262
+ if (fallback) return { text: text[fallback], locale: null };
263
+ return { text: text[keys[0]], locale: normalizeTag(keys[0]) };
264
+ }
265
+ function isRtlText(text) {
266
+ if (typeof text !== "string") return false;
267
+ const rtl = text.search(RTL_STRONG);
268
+ if (rtl === -1) return false;
269
+ const ltr = text.search(LTR_STRONG);
270
+ return ltr === -1 || rtl < ltr;
271
+ }
272
+
14
273
  // src/utils.js
15
274
  var DEFAULT_OPTIONS = {
16
275
  border: true,
@@ -29,7 +288,17 @@ var DEFAULT_OPTIONS = {
29
288
  /** Override the handwriting stack, e.g. a next/font CSS variable. */
30
289
  fontFamily: null,
31
290
  /** Fetch Caveat when the page has not provided it. */
32
- autoLoadFont: true
291
+ autoLoadFont: true,
292
+ /**
293
+ * Language for a note written in more than one. Omit to follow the visitor's
294
+ * own languages, which is what their operating system is set to.
295
+ */
296
+ locale: null,
297
+ /**
298
+ * Overlay stacking order. Omit to mirror the target element's z-index so
299
+ * doodles stay above the annotated element but below modals and other UI.
300
+ */
301
+ zIndex: null
33
302
  };
34
303
  function resolveElement(target) {
35
304
  if (!target) return null;
@@ -43,8 +312,15 @@ function resolveElement(target) {
43
312
  }
44
313
  function mergeOptions(options = {}) {
45
314
  const merged = { ...DEFAULT_OPTIONS, ...options };
46
- if (merged.note && typeof merged.note === "string") {
47
- merged.note = { text: merged.note, position: "top-right" };
315
+ if (merged.note) {
316
+ const config = typeof merged.note === "string" || isLocaleMap(merged.note) ? { text: merged.note } : merged.note;
317
+ merged.note = {
318
+ ...config,
319
+ position: config.position ?? "top-right",
320
+ // The note carries the chosen language, so `renderAnnotation` can resolve
321
+ // a multilingual note on its own.
322
+ locale: config.locale ?? merged.locale ?? null
323
+ };
48
324
  }
49
325
  if (merged.arrow) {
50
326
  const config = merged.arrow === true ? {} : merged.arrow;
@@ -54,7 +330,10 @@ function mergeOptions(options = {}) {
54
330
  // explicit `from` is honoured whether or not there is a note.
55
331
  from: config.from ?? (merged.note ? "note" : "top-right"),
56
332
  to: config.to ?? "edge",
57
- style: config.style ?? "curved"
333
+ style: config.style ?? "curved",
334
+ // Arrows draw themselves on and lean toward their target by default;
335
+ // `animate: false` opts out, an object tunes speed and gestures
336
+ animate: normalizeArrowAnimation(config.animate)
58
337
  };
59
338
  }
60
339
  if (merged.decorations === true) {
@@ -122,6 +401,40 @@ function getScrollableAncestors(element) {
122
401
  ancestors.push(window);
123
402
  return ancestors;
124
403
  }
404
+ function resolveOverlayZIndex(element, override) {
405
+ if (override != null) return override;
406
+ const { zIndex, position } = getComputedStyle(element);
407
+ if (zIndex !== "auto") {
408
+ const parsed = Number.parseInt(zIndex, 10);
409
+ if (Number.isFinite(parsed)) return parsed + 1;
410
+ }
411
+ let node = element.parentElement;
412
+ while (node && node !== document.documentElement) {
413
+ const style = getComputedStyle(node);
414
+ if (style.zIndex !== "auto" && style.position !== "static") {
415
+ const parsed = Number.parseInt(style.zIndex, 10);
416
+ if (Number.isFinite(parsed)) return parsed + 1;
417
+ }
418
+ node = node.parentElement;
419
+ }
420
+ return null;
421
+ }
422
+ function insertOverlaySvg(svg, element) {
423
+ const parent = element.parentNode;
424
+ if (!parent) {
425
+ document.body.appendChild(svg);
426
+ return;
427
+ }
428
+ parent.insertBefore(svg, element.nextSibling);
429
+ }
430
+ function syncOverlayStacking(svg, element, zIndexOverride) {
431
+ const zIndex = resolveOverlayZIndex(element, zIndexOverride);
432
+ if (zIndex == null) {
433
+ svg.style.zIndex = "";
434
+ } else {
435
+ svg.style.zIndex = String(zIndex);
436
+ }
437
+ }
125
438
 
126
439
  // src/geometry.js
127
440
  function averageCornerRadius(element) {
@@ -269,6 +582,7 @@ function annotationAnchor(position, rect, offset = 12) {
269
582
 
270
583
  // src/renderer.js
271
584
  var SVG_NS = "http://www.w3.org/2000/svg";
585
+ var XML_NS = "http://www.w3.org/XML/1998/namespace";
272
586
  var HANDWRITTEN_FONT = '"Caveat", "Kalam", "Patrick Hand", "Bradley Hand", "Segoe Script", "Comic Sans MS", cursive';
273
587
  var NOTE_FONT_SIZE = 19;
274
588
  function clearSvg(svg) {
@@ -514,6 +828,15 @@ function appendPath(svg, d, style) {
514
828
  svg.appendChild(path);
515
829
  return path;
516
830
  }
831
+ function appendGlow(svg, radius, style) {
832
+ const circle = document.createElementNS(SVG_NS, "circle");
833
+ circle.setAttribute("r", fmt(radius));
834
+ circle.setAttribute("fill", style.color);
835
+ circle.setAttribute("opacity", String(style.opacity));
836
+ circle.style.filter = `blur(${fmt(radius * 0.4)}px)`;
837
+ svg.appendChild(circle);
838
+ return circle;
839
+ }
517
840
  function rectPerimeter(width, height, radius) {
518
841
  const r = clamp(radius, 0, Math.min(width, height) / 2);
519
842
  return 2 * (width - 2 * r) + 2 * (height - 2 * r) + 2 * Math.PI * r;
@@ -600,20 +923,21 @@ function cubicTangentAt(p0, c1, c2, p3, t) {
600
923
  y: a * (c1.y - p0.y) + b * (c2.y - c1.y) + c * (p3.y - c2.y)
601
924
  };
602
925
  }
603
- function loopedCurvePoints(start, c1, c2, end, radius, random) {
926
+ var STRADDLE = 0.75;
927
+ function loopedCurvePoints(start, c1, c2, end, radius, random, side = 1) {
604
928
  const at = (t) => cubicAt(start, c1, c2, end, t);
605
- const tangentAt = (t) => normalize(cubicTangentAt(start, c1, c2, end, t));
606
929
  const knot = 0.46 + random() * 0.12;
607
930
  const points = [];
608
931
  const lead = Math.max(2, Math.round(knot * 16));
609
932
  for (let i = 0; i <= lead; i++) points.push(at(knot * i / lead));
610
933
  const pivot = at(knot);
611
- const heading = tangentAt(knot);
934
+ const derivative = cubicTangentAt(start, c1, c2, end, knot);
935
+ const speed = Math.hypot(derivative.x, derivative.y) || 1;
936
+ const heading = normalize(derivative);
612
937
  const normal = { x: -heading.y, y: heading.x };
613
- const side = random() < 0.5 ? 1 : -1;
614
938
  const centre = {
615
- x: pivot.x + normal.x * radius * side,
616
- y: pivot.y + normal.y * radius * side
939
+ x: pivot.x + normal.x * radius * STRADDLE * side,
940
+ y: pivot.y + normal.y * radius * STRADDLE * side
617
941
  };
618
942
  const from = Math.atan2(pivot.y - centre.y, pivot.x - centre.x);
619
943
  const turns = Math.PI * 2 * (0.97 + random() * 0.08);
@@ -621,15 +945,27 @@ function loopedCurvePoints(start, c1, c2, end, radius, random) {
621
945
  for (let i = 1; i <= steps; i++) {
622
946
  const progress = i / steps;
623
947
  const angle = from + turns * progress * side;
948
+ const spread = radius * (STRADDLE + (1 - STRADDLE) * progress);
624
949
  const drift = radius * 0.55 * progress;
625
950
  points.push({
626
- x: centre.x + Math.cos(angle) * radius + heading.x * drift,
627
- y: centre.y + Math.sin(angle) * radius + heading.y * drift
951
+ x: centre.x + Math.cos(angle) * spread + heading.x * drift,
952
+ y: centre.y + Math.sin(angle) * spread + heading.y * drift
628
953
  });
629
954
  }
955
+ const exit = points[points.length - 1];
956
+ const gained = (exit.x - pivot.x) * heading.x + (exit.y - pivot.y) * heading.y;
957
+ const resume = Math.min(knot + Math.max(0, gained) / speed, 0.985);
958
+ const rejoin = at(resume);
959
+ const residual = (exit.x - rejoin.x) * normal.x + (exit.y - rejoin.y) * normal.y;
960
+ const settle = 6;
630
961
  const tail = 14;
631
962
  for (let i = 1; i <= tail; i++) {
632
- points.push(at(knot + (1 - knot) * (i / tail)));
963
+ const point = at(resume + (1 - resume) * (i / tail));
964
+ const fade = Math.max(0, 1 - i / settle);
965
+ points.push({
966
+ x: point.x + normal.x * residual * fade,
967
+ y: point.y + normal.y * residual * fade
968
+ });
633
969
  }
634
970
  return points;
635
971
  }
@@ -703,21 +1039,29 @@ function arrowEndpoints(rect, arrowOptions, random, origin) {
703
1039
  function drawArrowHead(svg, tip, angle, style, random, shaftLength = Infinity) {
704
1040
  const length = Math.min(9 + style.strokeWidth * 1.4, shaftLength * 0.3);
705
1041
  const spread = 0.46 + random() * 0.1;
1042
+ const arms = [];
706
1043
  for (const side of [-1, 1]) {
707
1044
  const armAngle = angle + spread * side;
708
1045
  const tail = {
709
1046
  x: tip.x - length * Math.cos(armAngle),
710
1047
  y: tip.y - length * Math.sin(armAngle)
711
1048
  };
712
- appendPath(
713
- svg,
714
- roughLine(tail.x, tail.y, tip.x, tip.y, style.roughness * 0.5, random, {
715
- steps: 2,
716
- bow: 0.35 * side
717
- }),
718
- { ...style, dashed: false }
1049
+ arms.push(
1050
+ appendPath(
1051
+ svg,
1052
+ roughLine(tail.x, tail.y, tip.x, tip.y, style.roughness * 0.5, random, {
1053
+ steps: 2,
1054
+ bow: 0.35 * side
1055
+ }),
1056
+ { ...style, dashed: false }
1057
+ )
719
1058
  );
720
1059
  }
1060
+ return arms;
1061
+ }
1062
+ function bowOverTheTop(perpendicular) {
1063
+ if (Math.abs(perpendicular.y) < 0.02) return perpendicular.x > 0 ? 1 : -1;
1064
+ return perpendicular.y > 0 ? -1 : 1;
721
1065
  }
722
1066
  function boundsOf(points) {
723
1067
  const xs = points.map((p) => p.x);
@@ -729,7 +1073,7 @@ function boundsOf(points) {
729
1073
  height: Math.max(...ys) - Math.min(...ys)
730
1074
  };
731
1075
  }
732
- function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
1076
+ function drawArrow(svg, rect, arrowOptions, style, seed, origin = null, context = {}) {
733
1077
  const random = createRandom(seed + 17);
734
1078
  const { start, end } = arrowEndpoints(rect, arrowOptions, random, origin);
735
1079
  const dx = end.x - start.x;
@@ -738,6 +1082,7 @@ function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
738
1082
  const dashed = arrowOptions.style === "dotted";
739
1083
  let tipAngle = Math.atan2(dy, dx);
740
1084
  let shaft;
1085
+ let ghost = null;
741
1086
  const hull = [start, end];
742
1087
  if (arrowOptions.style === "straight") {
743
1088
  shaft = roughLine(start.x, start.y, end.x, end.y, style.roughness, random, {
@@ -751,7 +1096,12 @@ function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
751
1096
  y: rect.y + rect.height / 2 - mid.y
752
1097
  };
753
1098
  const lateral = perpendicular.x * towardCenter.x + perpendicular.y * towardCenter.y;
754
- const sign = Math.abs(lateral) > distance * 0.12 ? lateral > 0 ? -1 : 1 : random() < 0.5 ? -1 : 1;
1099
+ const sign = Math.abs(lateral) > distance * 0.12 ? lateral > 0 ? -1 : 1 : (
1100
+ // Nothing to steer around, so sweep over the top: a hand arcs above
1101
+ // the straight line and drops onto the target, and a coin flip here
1102
+ // used to mirror the whole gesture from one redraw to the next.
1103
+ bowOverTheTop(perpendicular)
1104
+ );
755
1105
  const bow = Math.min(distance * (0.16 + random() * 0.08), 30) * sign;
756
1106
  const c1x = start.x + dx * 0.28 + perpendicular.x * bow;
757
1107
  const c1y = start.y + dy * 0.28 + perpendicular.y * bow;
@@ -766,34 +1116,46 @@ function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
766
1116
  { x: c2x, y: c2y },
767
1117
  end,
768
1118
  radius,
769
- random
1119
+ random,
1120
+ -sign
770
1121
  );
771
1122
  shaft = catmullRomPath(points, false);
772
1123
  hull.push(...points);
773
1124
  const last = points[points.length - 1];
774
1125
  const prior = points[points.length - 2] ?? start;
775
1126
  tipAngle = Math.atan2(last.y - prior.y, last.x - prior.x);
776
- appendPath(svg, shaft, { ...style, dashed });
777
- drawArrowHead(svg, end, tipAngle, style, random, distance);
778
- return { start, end, bounds: boundsOf(hull) };
1127
+ } else {
1128
+ shaft = `M ${fmt(start.x)} ${fmt(start.y)} C ${fmt(c1x)} ${fmt(c1y)} ${fmt(c2x)} ${fmt(c2y)} ${fmt(end.x)} ${fmt(end.y)}`;
1129
+ if (style.roughness >= 1.1) {
1130
+ const drift = () => (random() - 0.5) * 1.4;
1131
+ 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)}`;
1132
+ }
1133
+ tipAngle = Math.atan2(end.y - c2y, end.x - c2x);
779
1134
  }
780
- shaft = `M ${fmt(start.x)} ${fmt(start.y)} C ${fmt(c1x)} ${fmt(c1y)} ${fmt(c2x)} ${fmt(c2y)} ${fmt(end.x)} ${fmt(end.y)}`;
781
- if (style.roughness >= 1.1) {
782
- const drift = () => (random() - 0.5) * 1.4;
783
- 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)}`;
784
- appendPath(svg, ghost, {
1135
+ }
1136
+ const group = document.createElementNS(SVG_NS, "g");
1137
+ svg.appendChild(group);
1138
+ const shaftPaths = [];
1139
+ if (ghost) {
1140
+ shaftPaths.push(
1141
+ appendPath(group, ghost, {
785
1142
  ...style,
786
1143
  opacity: style.opacity * 0.3,
787
1144
  strokeWidth: style.strokeWidth * 0.7,
788
1145
  dashed
789
- });
790
- }
791
- tipAngle = Math.atan2(end.y - c2y, end.x - c2x);
1146
+ })
1147
+ );
792
1148
  }
793
- appendPath(svg, shaft, { ...style, dashed });
794
- drawArrowHead(svg, end, tipAngle, style, random, distance);
1149
+ shaftPaths.push(appendPath(group, shaft, { ...style, dashed }));
1150
+ const head = drawArrowHead(group, end, tipAngle, style, random, distance);
1151
+ animateArrow(
1152
+ group,
1153
+ { shaft: shaftPaths, head, start, end },
1154
+ arrowOptions.animate,
1155
+ context.elapsed
1156
+ );
795
1157
  const bounds = boundsOf(hull);
796
- return { start, end, bounds };
1158
+ return { start, end, bounds, group };
797
1159
  }
798
1160
  function drawUnderline(svg, x, y, width, style, random) {
799
1161
  if (width <= 4) return;
@@ -860,6 +1222,7 @@ function createNoteText(svg, text, style, options = {}) {
860
1222
  const fontSize = style.fontSize ?? NOTE_FONT_SIZE;
861
1223
  const lines = wrapWords(text, options.wordsPerLine ?? DEFAULT_WORDS_PER_LINE);
862
1224
  const lineHeight = fontSize * 1.05;
1225
+ const rtl = options.rtl ?? isRtlText(text);
863
1226
  const group = document.createElementNS(SVG_NS, "g");
864
1227
  const textEl = document.createElementNS(SVG_NS, "text");
865
1228
  textEl.setAttribute("x", "0");
@@ -872,6 +1235,13 @@ function createNoteText(svg, text, style, options = {}) {
872
1235
  textEl.setAttribute("letter-spacing", "0.4");
873
1236
  textEl.setAttribute("text-anchor", "start");
874
1237
  textEl.setAttribute("dominant-baseline", "auto");
1238
+ if (rtl) {
1239
+ textEl.setAttribute("direction", "rtl");
1240
+ textEl.setAttribute("unicode-bidi", "embed");
1241
+ }
1242
+ if (options.locale) {
1243
+ textEl.setAttributeNS(XML_NS, "xml:lang", options.locale);
1244
+ }
875
1245
  const tspans = lines.map((line, index) => {
876
1246
  const tspan = document.createElementNS(SVG_NS, "tspan");
877
1247
  tspan.textContent = line;
@@ -900,15 +1270,17 @@ function createNoteText(svg, text, style, options = {}) {
900
1270
  height,
901
1271
  ascent,
902
1272
  descent,
903
- fontSize
1273
+ fontSize,
1274
+ rtl
904
1275
  };
905
1276
  }
906
1277
  function placeNoteText(note, box, style, seed, options = {}) {
907
1278
  const random = createRandom(seed + 55);
908
1279
  const tilt = options.tilt ?? (random() - 0.5) * 7;
909
- note.textEl.setAttribute("x", fmt(box.x));
1280
+ const anchorX = note.rtl ? box.x + box.width : box.x;
1281
+ note.textEl.setAttribute("x", fmt(anchorX));
910
1282
  note.textEl.setAttribute("y", fmt(box.y + note.ascent));
911
- for (const tspan of note.tspans) tspan.setAttribute("x", fmt(box.x));
1283
+ for (const tspan of note.tspans) tspan.setAttribute("x", fmt(anchorX));
912
1284
  const cx = box.x + box.width / 2;
913
1285
  const cy = box.y + box.height / 2;
914
1286
  note.group.setAttribute(
@@ -917,9 +1289,10 @@ function placeNoteText(note, box, style, seed, options = {}) {
917
1289
  );
918
1290
  if (options.underline !== false) {
919
1291
  const lastWidth = note.lineWidths[note.lineWidths.length - 1] ?? box.width;
1292
+ const underlineX = note.rtl ? box.x + box.width - lastWidth + 1 : box.x + 1;
920
1293
  drawUnderline(
921
1294
  note.group,
922
- box.x + 1,
1295
+ underlineX,
923
1296
  box.y + box.height + 2,
924
1297
  lastWidth - 2,
925
1298
  {
@@ -947,7 +1320,7 @@ function createOverlaySvg(left, top, width, height) {
947
1320
  svg.style.height = `${height}px`;
948
1321
  svg.style.pointerEvents = "none";
949
1322
  svg.style.overflow = "visible";
950
- svg.style.zIndex = "2147483646";
1323
+ svg.style.willChange = "transform";
951
1324
  return svg;
952
1325
  }
953
1326
 
@@ -994,12 +1367,17 @@ function nudgeIntoBand(box, guard, band) {
994
1367
  return { ...box, x: box.x + dx };
995
1368
  }
996
1369
  function renderAnnotation(svg, rect, noteOptions, style, seed = 1, layout = {}) {
997
- if (!noteOptions?.text) return null;
1370
+ const { text, locale } = resolveLocalizedText(
1371
+ noteOptions?.text,
1372
+ noteOptions?.locale
1373
+ );
1374
+ if (!text) return null;
998
1375
  const band = layout.band ?? null;
999
1376
  const position = isValidPosition(noteOptions.position) ? noteOptions.position : "top-right";
1000
1377
  const random = createRandom(seed + 9);
1001
- const note = createNoteText(svg, noteOptions.text, style, {
1002
- wordsPerLine: noteOptions.wordsPerLine
1378
+ const note = createNoteText(svg, text, style, {
1379
+ wordsPerLine: noteOptions.wordsPerLine,
1380
+ locale
1003
1381
  });
1004
1382
  const tilt = (random() - 0.45) * 7;
1005
1383
  const tiltPad = Math.abs(Math.sin(tilt * Math.PI / 180)) * note.width * 0.5;
@@ -1041,6 +1419,25 @@ function drawDot(parent, x, y, style, scale = 1.7) {
1041
1419
  strokeWidth: style.strokeWidth * scale
1042
1420
  });
1043
1421
  }
1422
+ function drawFleck(parent, x, y, radius, style, random) {
1423
+ const count = 6;
1424
+ const points = [];
1425
+ for (let i = 0; i < count; i++) {
1426
+ const angle = Math.PI * 2 * i / count + (random() - 0.5) * 0.5;
1427
+ const r = radius * (0.78 + random() * 0.34);
1428
+ points.push({
1429
+ x: x + Math.cos(angle) * r,
1430
+ y: y + Math.sin(angle) * r
1431
+ });
1432
+ }
1433
+ const blob = appendPath(parent, catmullRomPath(points, true), {
1434
+ ...style,
1435
+ fill: style.color,
1436
+ strokeWidth: 0
1437
+ });
1438
+ blob.style.filter = `blur(${(radius * 0.35).toFixed(2)}px)`;
1439
+ return blob;
1440
+ }
1044
1441
  function drawHeart(parent, size, style, random) {
1045
1442
  const s = size;
1046
1443
  const j = () => (random() - 0.5) * s * 0.12;
@@ -1064,26 +1461,61 @@ function twinkle(parent, radiusX, radiusY, pinch, style) {
1064
1461
  `Q ${(-c).toFixed(2)} ${(-c).toFixed(2)} 0 ${(-ry).toFixed(2)}`,
1065
1462
  "Z"
1066
1463
  ].join(" ");
1067
- appendPath(parent, path, style);
1464
+ return appendPath(parent, path, style);
1068
1465
  }
1069
1466
  function drawSparkle(parent, size, style, random) {
1070
1467
  twinkle(
1071
1468
  parent,
1072
- size * (0.34 + random() * 0.1),
1469
+ size * (0.62 + random() * 0.12),
1073
1470
  size,
1074
- size * (0.06 + random() * 0.06),
1471
+ size * (0.14 + random() * 0.08),
1075
1472
  style
1076
1473
  );
1077
1474
  }
1078
1475
  function drawStar(parent, size, style, random) {
1079
1476
  twinkle(
1080
1477
  parent,
1081
- size * (0.66 + random() * 0.12),
1082
- size,
1083
- size * (0.2 + random() * 0.08),
1478
+ size * (0.62 + random() * 0.12),
1479
+ size * (0.8 + random() * 0.12),
1480
+ size * (0.14 + random() * 0.08),
1084
1481
  style
1085
1482
  );
1086
1483
  }
1484
+ function drawGlimmer(parent, size, style, random) {
1485
+ appendGlow(parent, size * (1.2 + random() * 0.2), {
1486
+ color: style.color,
1487
+ opacity: style.opacity * 0.22
1488
+ });
1489
+ const armX = size * (0.5 + random() * 0.08);
1490
+ const armY = size * (0.45 + random() * 0.08);
1491
+ const bluntness = size * 0.19;
1492
+ const main = twinkle(parent, armX, armY, size * (0.08 + random() * 0.03), {
1493
+ ...style,
1494
+ fill: style.color,
1495
+ strokeWidth: bluntness
1496
+ });
1497
+ main.setAttribute("stroke-linejoin", "round");
1498
+ main.setAttribute("stroke-linecap", "round");
1499
+ main.style.filter = `blur(${(size * 0.05).toFixed(2)}px)`;
1500
+ const gap = bluntness / 2 + size * 0.2;
1501
+ const corners = [
1502
+ { x: 0, y: -(armY + gap) },
1503
+ { x: armX + gap, y: 0 },
1504
+ { x: 0, y: armY + gap },
1505
+ { x: -(armX + gap), y: 0 }
1506
+ ];
1507
+ for (const { x, y } of corners) {
1508
+ const jitter = 0.95 + random() * 0.1;
1509
+ drawFleck(
1510
+ parent,
1511
+ x * jitter,
1512
+ y * jitter,
1513
+ size * (0.1 + random() * 0.03),
1514
+ style,
1515
+ random
1516
+ );
1517
+ }
1518
+ }
1087
1519
  function drawTwinkle(parent, size, style, random) {
1088
1520
  const count = 5;
1089
1521
  const first = -Math.PI / 2 + (random() - 0.5) * 0.5;
@@ -1191,46 +1623,15 @@ function drawEmphasis(parent, size, style, random, opts = {}) {
1191
1623
  );
1192
1624
  }
1193
1625
  }
1194
- function drawDots(parent, size, style, random) {
1195
- let x = -size * 0.7;
1196
- for (let i = 0; i < 3; i++) {
1197
- drawDot(parent, x, (random() - 0.5) * size * 0.16, style);
1198
- x += size * (0.6 + random() * 0.25);
1199
- }
1200
- }
1201
- function drawStroke(parent, size, style, random) {
1202
- appendPath(
1203
- parent,
1204
- roughLine(-size, 0, size, 0, 1.2, random, {
1205
- bow: (random() - 0.5) * size * 0.35,
1206
- steps: 4
1207
- }),
1208
- style
1209
- );
1210
- }
1211
- function drawSteam(parent, size, style, random) {
1212
- const lean = random() < 0.5 ? -1 : 1;
1213
- for (let i = 0; i < 2; i++) {
1214
- const x = (i - 0.5) * size * 0.72;
1215
- const base = size * 0.62 - i * size * 0.14;
1216
- appendPath(
1217
- parent,
1218
- `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)}`,
1219
- style
1220
- );
1221
- }
1222
- }
1223
1626
  var DECORATION_DRAWERS = {
1224
1627
  heart: drawHeart,
1225
1628
  sparkle: drawSparkle,
1226
1629
  star: drawStar,
1227
1630
  twinkle: drawTwinkle,
1631
+ glimmer: drawGlimmer,
1228
1632
  smiley: drawSmiley,
1229
1633
  emphasis: drawEmphasis,
1230
- arcs: drawArcs,
1231
- dots: drawDots,
1232
- stroke: drawStroke,
1233
- steam: drawSteam
1634
+ arcs: drawArcs
1234
1635
  };
1235
1636
  var TEXT_TYPES = ["heart", "sparkle"];
1236
1637
  var CORNER_ACCENTS = ["arcs", "emphasis"];
@@ -1241,12 +1642,10 @@ var TYPE_TRAITS = {
1241
1642
  sparkle: { size: [6, 9], tilt: 20, standoff: 22 },
1242
1643
  star: { size: [8, 11], tilt: 25, standoff: 26 },
1243
1644
  twinkle: { size: [5, 7.5], tilt: 30, standoff: 26, brightness: 1.4 },
1645
+ glimmer: { size: [7, 10], tilt: 20, standoff: 28, brightness: 1.3 },
1244
1646
  smiley: { size: [8, 11], tilt: 10, standoff: 22 },
1245
1647
  emphasis: { size: [12, 17], tilt: 10, standoff: 13, outward: true },
1246
- arcs: { size: [11, 16], tilt: 12, standoff: 12, outward: true },
1247
- dots: { size: [6, 9], tilt: 8, standoff: 20 },
1248
- stroke: { size: [8, 12], tilt: 40, standoff: 20 },
1249
- steam: { size: [8, 11], tilt: 10, standoff: 20 }
1648
+ arcs: { size: [11, 16], tilt: 12, standoff: 12, outward: true }
1250
1649
  };
1251
1650
  var DEFAULT_TYPES = [
1252
1651
  "twinkle",
@@ -1449,6 +1848,9 @@ function ensureHandwrittenFont() {
1449
1848
 
1450
1849
  // src/doodle.js
1451
1850
  var instances = /* @__PURE__ */ new WeakMap();
1851
+ function now() {
1852
+ return typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
1853
+ }
1452
1854
  function localBand(overlayRect, inset = 8) {
1453
1855
  const width = window.innerWidth || document.documentElement.clientWidth || 0;
1454
1856
  return {
@@ -1465,19 +1867,24 @@ var DoodleOverlay = class {
1465
1867
  this.element = element;
1466
1868
  this.options = options;
1467
1869
  this.seed = Math.floor(Math.random() * 1e9);
1870
+ this.startedAt = now();
1468
1871
  this.svg = null;
1469
1872
  this.resizeObserver = null;
1470
1873
  this.mutationObserver = null;
1471
1874
  this.pendingFrame = null;
1472
- this.scrollTargets = [];
1473
- this.onScroll = this.scheduleUpdate.bind(this);
1875
+ this.pendingRepositionFrame = null;
1876
+ this.basePosition = { x: 0, y: 0 };
1877
+ this.listeners = [];
1878
+ this.onScroll = this.scheduleReposition.bind(this);
1474
1879
  this.onResize = this.scheduleUpdate.bind(this);
1880
+ this.onLanguageChange = this.scheduleUpdate.bind(this);
1475
1881
  this.childElements = [];
1476
1882
  this.mount();
1477
1883
  }
1478
1884
  mount() {
1479
1885
  this.svg = createOverlaySvg(0, 0, 0, 0);
1480
- document.body.appendChild(this.svg);
1886
+ insertOverlaySvg(this.svg, this.element);
1887
+ syncOverlayStacking(this.svg, this.element, this.options.zIndex);
1481
1888
  this.resizeObserver = new ResizeObserver(this.onResize);
1482
1889
  this.resizeObserver.observe(this.element);
1483
1890
  if (this.options.children) {
@@ -1491,10 +1898,20 @@ var DoodleOverlay = class {
1491
1898
  });
1492
1899
  for (const target of getScrollableAncestors(this.element)) {
1493
1900
  target.addEventListener("scroll", this.onScroll, { passive: true });
1494
- this.scrollTargets.push({ target, type: "scroll" });
1901
+ this.listeners.push({ target, type: "scroll", handler: this.onScroll });
1495
1902
  }
1496
1903
  window.addEventListener("resize", this.onResize, { passive: true });
1497
- this.scrollTargets.push({ target: window, type: "resize" });
1904
+ this.listeners.push({
1905
+ target: window,
1906
+ type: "resize",
1907
+ handler: this.onResize
1908
+ });
1909
+ window.addEventListener("languagechange", this.onLanguageChange);
1910
+ this.listeners.push({
1911
+ target: window,
1912
+ type: "languagechange",
1913
+ handler: this.onLanguageChange
1914
+ });
1498
1915
  if (this.options.note && this.options.autoLoadFont && !this.options.fontFamily) {
1499
1916
  ensureHandwrittenFont();
1500
1917
  }
@@ -1518,6 +1935,46 @@ var DoodleOverlay = class {
1518
1935
  this.update();
1519
1936
  });
1520
1937
  }
1938
+ scheduleReposition() {
1939
+ if (this.pendingFrame != null || this.pendingRepositionFrame != null) {
1940
+ return;
1941
+ }
1942
+ this.pendingRepositionFrame = scheduleFrame(() => {
1943
+ this.pendingRepositionFrame = null;
1944
+ this.reposition();
1945
+ });
1946
+ }
1947
+ /**
1948
+ * Cheap scroll-driven path: slide the overlay to the element's new
1949
+ * viewport position without touching its contents. Scrolling can't change
1950
+ * an element's size, so the rects, viewBox and every hand-drawn stroke
1951
+ * inside them are still valid - only the overlay's position needs to move.
1952
+ *
1953
+ * That move is a `transform`, not `left`/`top`. `left`/`top` sit in the
1954
+ * same box-geometry pass as layout, so the browser re-checks layout on
1955
+ * every write; `transform` is composited, so the scroll path never
1956
+ * touches layout at all.
1957
+ */
1958
+ reposition() {
1959
+ if (!this.svg || !this.element.isConnected) return;
1960
+ const { padding, radius } = this.options;
1961
+ const targets = this.getTargets();
1962
+ const margin = this.overlayMargin();
1963
+ const absoluteRects = targets.map(
1964
+ (target) => getElementBounds(target, padding, radius)
1965
+ );
1966
+ const overlayRect = unionRects(absoluteRects, margin);
1967
+ const currentWidth = parseFloat(this.svg.style.width) || 0;
1968
+ const currentHeight = parseFloat(this.svg.style.height) || 0;
1969
+ const resized = Math.abs(overlayRect.width - currentWidth) > 0.5 || Math.abs(overlayRect.height - currentHeight) > 0.5;
1970
+ if (resized) {
1971
+ this.update();
1972
+ return;
1973
+ }
1974
+ const dx = overlayRect.x - this.basePosition.x;
1975
+ const dy = overlayRect.y - this.basePosition.y;
1976
+ this.svg.style.transform = `translate3d(${dx}px, ${dy}px, 0)`;
1977
+ }
1521
1978
  getTargets() {
1522
1979
  if (this.options.children) {
1523
1980
  const nodes = this.element.querySelectorAll(this.options.children);
@@ -1530,11 +1987,13 @@ var DoodleOverlay = class {
1530
1987
  const { padding, radius, color, strokeWidth, roughness, opacity } = this.options;
1531
1988
  const targets = this.getTargets();
1532
1989
  const margin = this.overlayMargin();
1990
+ const elapsed = now() - this.startedAt;
1533
1991
  const absoluteRects = targets.map(
1534
1992
  (target) => getElementBounds(target, padding, radius)
1535
1993
  );
1536
1994
  const overlayRect = unionRects(absoluteRects, margin);
1537
1995
  clearSvg(this.svg);
1996
+ syncOverlayStacking(this.svg, this.element, this.options.zIndex);
1538
1997
  this.svg.setAttribute("width", String(overlayRect.width));
1539
1998
  this.svg.setAttribute("height", String(overlayRect.height));
1540
1999
  this.svg.setAttribute("viewBox", `0 0 ${overlayRect.width} ${overlayRect.height}`);
@@ -1542,6 +2001,8 @@ var DoodleOverlay = class {
1542
2001
  this.svg.style.top = `${overlayRect.y}px`;
1543
2002
  this.svg.style.width = `${overlayRect.width}px`;
1544
2003
  this.svg.style.height = `${overlayRect.height}px`;
2004
+ this.svg.style.transform = "";
2005
+ this.basePosition = { x: overlayRect.x, y: overlayRect.y };
1545
2006
  const strokeStyle = { color, strokeWidth, roughness, opacity };
1546
2007
  const noteStyle = {
1547
2008
  color,
@@ -1581,7 +2042,8 @@ var DoodleOverlay = class {
1581
2042
  this.options.arrow,
1582
2043
  strokeStyle,
1583
2044
  seed,
1584
- note ? note.box : null
2045
+ note ? note.box : null,
2046
+ { elapsed }
1585
2047
  ) : null;
1586
2048
  if (this.options.decorations) {
1587
2049
  renderDecorations(
@@ -1608,7 +2070,7 @@ var DoodleOverlay = class {
1608
2070
  overlayMargin() {
1609
2071
  const { note, arrow, decorations } = this.options;
1610
2072
  if (!note && !arrow && !decorations) return 48;
1611
- const text = typeof note?.text === "string" ? note.text : "";
2073
+ const { text } = resolveLocalizedText(note?.text, note?.locale);
1612
2074
  return Math.max(96, Math.min(240, 80 + text.length * 8));
1613
2075
  }
1614
2076
  destroy() {
@@ -1616,6 +2078,10 @@ var DoodleOverlay = class {
1616
2078
  cancelFrame(this.pendingFrame);
1617
2079
  this.pendingFrame = null;
1618
2080
  }
2081
+ if (this.pendingRepositionFrame != null) {
2082
+ cancelFrame(this.pendingRepositionFrame);
2083
+ this.pendingRepositionFrame = null;
2084
+ }
1619
2085
  if (this.resizeObserver) {
1620
2086
  this.resizeObserver.disconnect();
1621
2087
  this.resizeObserver = null;
@@ -1628,10 +2094,10 @@ var DoodleOverlay = class {
1628
2094
  document.fonts?.removeEventListener?.("loadingdone", this.onFontsLoaded);
1629
2095
  this.onFontsLoaded = null;
1630
2096
  }
1631
- for (const { target, type } of this.scrollTargets) {
1632
- target.removeEventListener(type, type === "scroll" ? this.onScroll : this.onResize);
2097
+ for (const { target, type, handler } of this.listeners) {
2098
+ target.removeEventListener(type, handler);
1633
2099
  }
1634
- this.scrollTargets = [];
2100
+ this.listeners = [];
1635
2101
  if (this.svg?.parentNode) {
1636
2102
  this.svg.parentNode.removeChild(this.svg);
1637
2103
  }
@@ -1672,7 +2138,8 @@ var OPTION_KEYS = [
1672
2138
  "decorations",
1673
2139
  "addBreaks",
1674
2140
  "fontFamily",
1675
- "autoLoadFont"
2141
+ "autoLoadFont",
2142
+ "locale"
1676
2143
  ];
1677
2144
  function collectOptions(props) {
1678
2145
  const options = {};
@@ -1724,6 +2191,7 @@ var Doodle = forwardRef(function Doodle2(props, forwardedRef) {
1724
2191
  addBreaks,
1725
2192
  fontFamily,
1726
2193
  autoLoadFont,
2194
+ locale,
1727
2195
  childSelector,
1728
2196
  ...rest
1729
2197
  } = props;
@@ -1743,6 +2211,7 @@ var Doodle = forwardRef(function Doodle2(props, forwardedRef) {
1743
2211
  addBreaks,
1744
2212
  fontFamily,
1745
2213
  autoLoadFont,
2214
+ locale,
1746
2215
  childSelector
1747
2216
  }),
1748
2217
  [
@@ -1760,6 +2229,7 @@ var Doodle = forwardRef(function Doodle2(props, forwardedRef) {
1760
2229
  JSON.stringify(addBreaks ?? null),
1761
2230
  fontFamily,
1762
2231
  autoLoadFont,
2232
+ JSON.stringify(locale ?? null),
1763
2233
  childSelector
1764
2234
  ]
1765
2235
  );