@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.
@@ -23,6 +23,265 @@ __export(index_exports, {
23
23
  });
24
24
  module.exports = __toCommonJS(index_exports);
25
25
 
26
+ // src/animation.js
27
+ var STYLE_MARKER = "data-freehand-animation";
28
+ var CSS = `
29
+ @keyframes freehand-arrow-dash {
30
+ from { stroke-dashoffset: var(--freehand-dash, 0px); }
31
+ to { stroke-dashoffset: 0; }
32
+ }
33
+ @keyframes freehand-arrow-drift {
34
+ 0%, 100% { transform: translate(0px, 0px); }
35
+ 50% { transform: translate(var(--freehand-drift-x, 0px), var(--freehand-drift-y, 0px)); }
36
+ }
37
+ @keyframes freehand-arrow-settle {
38
+ from { transform: translate(0px, 0px); }
39
+ to { transform: translate(var(--freehand-drift-x, 0px), var(--freehand-drift-y, 0px)); }
40
+ }
41
+ @media (prefers-reduced-motion: reduce) {
42
+ .freehand-arrow, .freehand-arrow path { animation: none !important; }
43
+ .freehand-arrow path { stroke-dashoffset: 0 !important; }
44
+ }
45
+ `;
46
+ var ARROW_ANIMATION_DEFAULTS = {
47
+ /** Reveal the stroke with a dash sweep. */
48
+ draw: true,
49
+ /** Lean the arrow toward its tip. */
50
+ drift: true,
51
+ /** Multiplier over every duration - 2 plays twice as fast. */
52
+ speed: 1,
53
+ /** How long the shaft takes to draw, ms, before `speed`. */
54
+ duration: 900,
55
+ /** Wait before the first stroke appears, ms. */
56
+ delay: 150,
57
+ /** How far the arrow leans, px. */
58
+ distance: 5,
59
+ /** One full lean-and-return, ms. */
60
+ driftDuration: 2200,
61
+ /** Keep leaning. `false` settles once and stays put. */
62
+ repeat: true
63
+ };
64
+ function positive(value, fallback) {
65
+ const n = Number(value);
66
+ return Number.isFinite(n) && n > 0 ? n : fallback;
67
+ }
68
+ function atLeastZero(value, fallback) {
69
+ const n = Number(value);
70
+ return Number.isFinite(n) && n >= 0 ? n : fallback;
71
+ }
72
+ function normalizeArrowAnimation(config) {
73
+ if (config === false || config === null) return false;
74
+ const settings = config === true || config === void 0 || typeof config !== "object" ? {} : config;
75
+ const animation = {
76
+ draw: settings.draw !== false,
77
+ drift: settings.drift !== false,
78
+ speed: positive(settings.speed, ARROW_ANIMATION_DEFAULTS.speed),
79
+ duration: positive(settings.duration, ARROW_ANIMATION_DEFAULTS.duration),
80
+ delay: atLeastZero(settings.delay, ARROW_ANIMATION_DEFAULTS.delay),
81
+ distance: atLeastZero(settings.distance, ARROW_ANIMATION_DEFAULTS.distance),
82
+ driftDuration: positive(
83
+ settings.driftDuration,
84
+ ARROW_ANIMATION_DEFAULTS.driftDuration
85
+ ),
86
+ repeat: settings.repeat !== false
87
+ };
88
+ if (!animation.draw && (!animation.drift || !animation.distance)) return false;
89
+ return animation;
90
+ }
91
+ var FLOW_PERIOD = 260;
92
+ function arrowTiming(animation) {
93
+ const duration = animation.duration / animation.speed;
94
+ const delay = animation.delay / animation.speed;
95
+ const drawn = animation.draw ? duration : 0;
96
+ return {
97
+ shaft: { duration, delay },
98
+ head: { duration: duration * 0.22, delay: delay + drawn * 0.82 },
99
+ flow: { duration: FLOW_PERIOD / animation.speed, delay },
100
+ drift: {
101
+ duration: animation.driftDuration / animation.speed,
102
+ delay: delay + drawn * 1.04
103
+ }
104
+ };
105
+ }
106
+ var injected = false;
107
+ function ensureArrowAnimationStyles() {
108
+ if (injected) return;
109
+ if (typeof document === "undefined" || !document.head) return;
110
+ injected = true;
111
+ if (document.querySelector(`style[${STYLE_MARKER}]`)) return;
112
+ const style = document.createElement("style");
113
+ style.setAttribute(STYLE_MARKER, "");
114
+ style.textContent = CSS;
115
+ document.head.appendChild(style);
116
+ }
117
+ function time(ms) {
118
+ return `${Math.round(ms)}ms`;
119
+ }
120
+ function setAnimation(element, spec) {
121
+ const parts = [
122
+ spec.name,
123
+ time(spec.duration),
124
+ spec.easing,
125
+ time(spec.delay),
126
+ spec.iterations ?? "1"
127
+ ];
128
+ if (spec.fill) parts.push(spec.fill);
129
+ element.style.animation = parts.join(" ");
130
+ }
131
+ function pathLength(path) {
132
+ try {
133
+ const length = path.getTotalLength();
134
+ return Number.isFinite(length) && length > 0 ? length : 0;
135
+ } catch {
136
+ return 0;
137
+ }
138
+ }
139
+ function dashPeriod(path) {
140
+ const pattern = (path.getAttribute("stroke-dasharray") || "").split(/[\s,]+/).map(Number).filter((n) => Number.isFinite(n) && n > 0);
141
+ if (!pattern.length) return 0;
142
+ const sum = pattern.reduce((total, n) => total + n, 0);
143
+ return pattern.length % 2 === 0 ? sum : sum * 2;
144
+ }
145
+ function applyDash(path, timing, elapsed, flow) {
146
+ const length = pathLength(path);
147
+ if (!length) return;
148
+ const period = dashPeriod(path);
149
+ if (period) {
150
+ path.style.setProperty("--freehand-dash", `${period.toFixed(2)}px`);
151
+ setAnimation(path, {
152
+ name: "freehand-arrow-dash",
153
+ duration: flow.duration,
154
+ easing: "linear",
155
+ delay: flow.delay - elapsed,
156
+ iterations: "infinite"
157
+ });
158
+ return;
159
+ }
160
+ path.style.setProperty("--freehand-dash", `${length.toFixed(2)}px`);
161
+ path.style.strokeDasharray = `${length.toFixed(2)}px`;
162
+ setAnimation(path, {
163
+ name: "freehand-arrow-dash",
164
+ duration: timing.duration,
165
+ easing: "ease-out",
166
+ delay: timing.delay - elapsed,
167
+ fill: "both"
168
+ });
169
+ }
170
+ function applyDrift(group, parts, timing, elapsed, animation) {
171
+ const dx = parts.end.x - parts.start.x;
172
+ const dy = parts.end.y - parts.start.y;
173
+ const length = Math.hypot(dx, dy);
174
+ if (!length) return;
175
+ const scale = animation.distance / length;
176
+ group.style.setProperty("--freehand-drift-x", `${(dx * scale).toFixed(2)}px`);
177
+ group.style.setProperty("--freehand-drift-y", `${(dy * scale).toFixed(2)}px`);
178
+ setAnimation(
179
+ group,
180
+ animation.repeat ? {
181
+ name: "freehand-arrow-drift",
182
+ duration: timing.duration,
183
+ easing: "ease-in-out",
184
+ delay: timing.delay - elapsed,
185
+ iterations: "infinite"
186
+ } : {
187
+ // A single lean is a settle, not a round trip, so it takes half a cycle
188
+ name: "freehand-arrow-settle",
189
+ duration: timing.duration * 0.5,
190
+ easing: "ease-out",
191
+ delay: timing.delay - elapsed,
192
+ fill: "both"
193
+ }
194
+ );
195
+ }
196
+ function animateArrow(group, parts, animation, elapsed = 0) {
197
+ if (!animation || !group) return;
198
+ ensureArrowAnimationStyles();
199
+ group.setAttribute("class", "freehand-arrow");
200
+ const timing = arrowTiming(animation);
201
+ const age = Number.isFinite(elapsed) ? Math.max(0, elapsed) : 0;
202
+ if (animation.draw) {
203
+ for (const path of parts.shaft) {
204
+ applyDash(path, timing.shaft, age, timing.flow);
205
+ }
206
+ for (const path of parts.head) applyDash(path, timing.head, age, timing.flow);
207
+ }
208
+ if (animation.drift && animation.distance > 0) {
209
+ applyDrift(group, parts, timing.drift, age, animation);
210
+ }
211
+ }
212
+
213
+ // src/locale.js
214
+ var DEFAULT_KEY = "default";
215
+ var LANGUAGE_TAG = /^[a-z]{2,3}(-[a-z0-9]{2,8})*$/i;
216
+ var RTL_STRONG = /[֑-߿יִ-﷿ﹰ-ﻼ]/;
217
+ var LTR_STRONG = /[A-Za-zÀ-֏ऀ-῿Ⰰ-퟿豈-ﬗA-Za-z]/;
218
+ function normalizeTag(tag) {
219
+ if (typeof tag !== "string") return "";
220
+ return tag.trim().replace(/_/g, "-").toLowerCase();
221
+ }
222
+ function isLocaleMap(value) {
223
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
224
+ const entries = Object.entries(value);
225
+ if (!entries.length) return false;
226
+ return entries.every(([key, text]) => {
227
+ if (typeof text !== "string") return false;
228
+ const tag = normalizeTag(key);
229
+ return tag === DEFAULT_KEY || LANGUAGE_TAG.test(tag);
230
+ });
231
+ }
232
+ function preferredLocales(override) {
233
+ const tags = [];
234
+ const add = (value) => {
235
+ const tag = normalizeTag(value);
236
+ if (tag && !tags.includes(tag)) tags.push(tag);
237
+ };
238
+ if (Array.isArray(override)) override.forEach(add);
239
+ else add(override);
240
+ const nav = typeof navigator === "undefined" ? null : navigator;
241
+ if (nav) {
242
+ if (Array.isArray(nav.languages)) nav.languages.forEach(add);
243
+ add(nav.language);
244
+ }
245
+ if (typeof document !== "undefined") add(document.documentElement?.lang);
246
+ return tags;
247
+ }
248
+ function matchLocale(tags, preferred) {
249
+ const available = tags.map(normalizeTag);
250
+ for (const want of preferred) {
251
+ let tag = want;
252
+ while (tag) {
253
+ const exact = available.indexOf(tag);
254
+ if (exact !== -1) return exact;
255
+ const cut = tag.lastIndexOf("-");
256
+ tag = cut > 0 ? tag.slice(0, cut) : "";
257
+ }
258
+ const base = want.split("-")[0];
259
+ const sibling = available.findIndex((key) => key.split("-")[0] === base);
260
+ if (sibling !== -1) return sibling;
261
+ }
262
+ return -1;
263
+ }
264
+ function resolveLocalizedText(text, locale = null) {
265
+ if (typeof text === "string") return { text, locale: null };
266
+ if (!isLocaleMap(text)) return { text: "", locale: null };
267
+ const keys = Object.keys(text);
268
+ const tags = keys.filter((key) => normalizeTag(key) !== DEFAULT_KEY);
269
+ const index = matchLocale(tags, preferredLocales(locale));
270
+ if (index !== -1) {
271
+ return { text: text[tags[index]], locale: normalizeTag(tags[index]) };
272
+ }
273
+ const fallback = keys.find((key) => normalizeTag(key) === DEFAULT_KEY);
274
+ if (fallback) return { text: text[fallback], locale: null };
275
+ return { text: text[keys[0]], locale: normalizeTag(keys[0]) };
276
+ }
277
+ function isRtlText(text) {
278
+ if (typeof text !== "string") return false;
279
+ const rtl = text.search(RTL_STRONG);
280
+ if (rtl === -1) return false;
281
+ const ltr = text.search(LTR_STRONG);
282
+ return ltr === -1 || rtl < ltr;
283
+ }
284
+
26
285
  // src/utils.js
27
286
  var DEFAULT_OPTIONS = {
28
287
  border: true,
@@ -41,7 +300,17 @@ var DEFAULT_OPTIONS = {
41
300
  /** Override the handwriting stack, e.g. a next/font CSS variable. */
42
301
  fontFamily: null,
43
302
  /** Fetch Caveat when the page has not provided it. */
44
- autoLoadFont: true
303
+ autoLoadFont: true,
304
+ /**
305
+ * Language for a note written in more than one. Omit to follow the visitor's
306
+ * own languages, which is what their operating system is set to.
307
+ */
308
+ locale: null,
309
+ /**
310
+ * Overlay stacking order. Omit to mirror the target element's z-index so
311
+ * doodles stay above the annotated element but below modals and other UI.
312
+ */
313
+ zIndex: null
45
314
  };
46
315
  function resolveElement(target) {
47
316
  if (!target) return null;
@@ -55,8 +324,15 @@ function resolveElement(target) {
55
324
  }
56
325
  function mergeOptions(options = {}) {
57
326
  const merged = { ...DEFAULT_OPTIONS, ...options };
58
- if (merged.note && typeof merged.note === "string") {
59
- merged.note = { text: merged.note, position: "top-right" };
327
+ if (merged.note) {
328
+ const config = typeof merged.note === "string" || isLocaleMap(merged.note) ? { text: merged.note } : merged.note;
329
+ merged.note = {
330
+ ...config,
331
+ position: config.position ?? "top-right",
332
+ // The note carries the chosen language, so `renderAnnotation` can resolve
333
+ // a multilingual note on its own.
334
+ locale: config.locale ?? merged.locale ?? null
335
+ };
60
336
  }
61
337
  if (merged.arrow) {
62
338
  const config = merged.arrow === true ? {} : merged.arrow;
@@ -66,7 +342,10 @@ function mergeOptions(options = {}) {
66
342
  // explicit `from` is honoured whether or not there is a note.
67
343
  from: config.from ?? (merged.note ? "note" : "top-right"),
68
344
  to: config.to ?? "edge",
69
- style: config.style ?? "curved"
345
+ style: config.style ?? "curved",
346
+ // Arrows draw themselves on and lean toward their target by default;
347
+ // `animate: false` opts out, an object tunes speed and gestures
348
+ animate: normalizeArrowAnimation(config.animate)
70
349
  };
71
350
  }
72
351
  if (merged.decorations === true) {
@@ -134,6 +413,40 @@ function getScrollableAncestors(element) {
134
413
  ancestors.push(window);
135
414
  return ancestors;
136
415
  }
416
+ function resolveOverlayZIndex(element, override) {
417
+ if (override != null) return override;
418
+ const { zIndex, position } = getComputedStyle(element);
419
+ if (zIndex !== "auto") {
420
+ const parsed = Number.parseInt(zIndex, 10);
421
+ if (Number.isFinite(parsed)) return parsed + 1;
422
+ }
423
+ let node = element.parentElement;
424
+ while (node && node !== document.documentElement) {
425
+ const style = getComputedStyle(node);
426
+ if (style.zIndex !== "auto" && style.position !== "static") {
427
+ const parsed = Number.parseInt(style.zIndex, 10);
428
+ if (Number.isFinite(parsed)) return parsed + 1;
429
+ }
430
+ node = node.parentElement;
431
+ }
432
+ return null;
433
+ }
434
+ function insertOverlaySvg(svg, element) {
435
+ const parent = element.parentNode;
436
+ if (!parent) {
437
+ document.body.appendChild(svg);
438
+ return;
439
+ }
440
+ parent.insertBefore(svg, element.nextSibling);
441
+ }
442
+ function syncOverlayStacking(svg, element, zIndexOverride) {
443
+ const zIndex = resolveOverlayZIndex(element, zIndexOverride);
444
+ if (zIndex == null) {
445
+ svg.style.zIndex = "";
446
+ } else {
447
+ svg.style.zIndex = String(zIndex);
448
+ }
449
+ }
137
450
 
138
451
  // src/geometry.js
139
452
  function averageCornerRadius(element) {
@@ -281,6 +594,7 @@ function annotationAnchor(position, rect, offset = 12) {
281
594
 
282
595
  // src/renderer.js
283
596
  var SVG_NS = "http://www.w3.org/2000/svg";
597
+ var XML_NS = "http://www.w3.org/XML/1998/namespace";
284
598
  var HANDWRITTEN_FONT = '"Caveat", "Kalam", "Patrick Hand", "Bradley Hand", "Segoe Script", "Comic Sans MS", cursive';
285
599
  var NOTE_FONT_SIZE = 19;
286
600
  function clearSvg(svg) {
@@ -526,6 +840,15 @@ function appendPath(svg, d, style) {
526
840
  svg.appendChild(path);
527
841
  return path;
528
842
  }
843
+ function appendGlow(svg, radius, style) {
844
+ const circle = document.createElementNS(SVG_NS, "circle");
845
+ circle.setAttribute("r", fmt(radius));
846
+ circle.setAttribute("fill", style.color);
847
+ circle.setAttribute("opacity", String(style.opacity));
848
+ circle.style.filter = `blur(${fmt(radius * 0.4)}px)`;
849
+ svg.appendChild(circle);
850
+ return circle;
851
+ }
529
852
  function rectPerimeter(width, height, radius) {
530
853
  const r = clamp(radius, 0, Math.min(width, height) / 2);
531
854
  return 2 * (width - 2 * r) + 2 * (height - 2 * r) + 2 * Math.PI * r;
@@ -612,20 +935,21 @@ function cubicTangentAt(p0, c1, c2, p3, t) {
612
935
  y: a * (c1.y - p0.y) + b * (c2.y - c1.y) + c * (p3.y - c2.y)
613
936
  };
614
937
  }
615
- function loopedCurvePoints(start, c1, c2, end, radius, random) {
938
+ var STRADDLE = 0.75;
939
+ function loopedCurvePoints(start, c1, c2, end, radius, random, side = 1) {
616
940
  const at = (t) => cubicAt(start, c1, c2, end, t);
617
- const tangentAt = (t) => normalize(cubicTangentAt(start, c1, c2, end, t));
618
941
  const knot = 0.46 + random() * 0.12;
619
942
  const points = [];
620
943
  const lead = Math.max(2, Math.round(knot * 16));
621
944
  for (let i = 0; i <= lead; i++) points.push(at(knot * i / lead));
622
945
  const pivot = at(knot);
623
- const heading = tangentAt(knot);
946
+ const derivative = cubicTangentAt(start, c1, c2, end, knot);
947
+ const speed = Math.hypot(derivative.x, derivative.y) || 1;
948
+ const heading = normalize(derivative);
624
949
  const normal = { x: -heading.y, y: heading.x };
625
- const side = random() < 0.5 ? 1 : -1;
626
950
  const centre = {
627
- x: pivot.x + normal.x * radius * side,
628
- y: pivot.y + normal.y * radius * side
951
+ x: pivot.x + normal.x * radius * STRADDLE * side,
952
+ y: pivot.y + normal.y * radius * STRADDLE * side
629
953
  };
630
954
  const from = Math.atan2(pivot.y - centre.y, pivot.x - centre.x);
631
955
  const turns = Math.PI * 2 * (0.97 + random() * 0.08);
@@ -633,15 +957,27 @@ function loopedCurvePoints(start, c1, c2, end, radius, random) {
633
957
  for (let i = 1; i <= steps; i++) {
634
958
  const progress = i / steps;
635
959
  const angle = from + turns * progress * side;
960
+ const spread = radius * (STRADDLE + (1 - STRADDLE) * progress);
636
961
  const drift = radius * 0.55 * progress;
637
962
  points.push({
638
- x: centre.x + Math.cos(angle) * radius + heading.x * drift,
639
- y: centre.y + Math.sin(angle) * radius + heading.y * drift
963
+ x: centre.x + Math.cos(angle) * spread + heading.x * drift,
964
+ y: centre.y + Math.sin(angle) * spread + heading.y * drift
640
965
  });
641
966
  }
967
+ const exit = points[points.length - 1];
968
+ const gained = (exit.x - pivot.x) * heading.x + (exit.y - pivot.y) * heading.y;
969
+ const resume = Math.min(knot + Math.max(0, gained) / speed, 0.985);
970
+ const rejoin = at(resume);
971
+ const residual = (exit.x - rejoin.x) * normal.x + (exit.y - rejoin.y) * normal.y;
972
+ const settle = 6;
642
973
  const tail = 14;
643
974
  for (let i = 1; i <= tail; i++) {
644
- points.push(at(knot + (1 - knot) * (i / tail)));
975
+ const point = at(resume + (1 - resume) * (i / tail));
976
+ const fade = Math.max(0, 1 - i / settle);
977
+ points.push({
978
+ x: point.x + normal.x * residual * fade,
979
+ y: point.y + normal.y * residual * fade
980
+ });
645
981
  }
646
982
  return points;
647
983
  }
@@ -715,21 +1051,29 @@ function arrowEndpoints(rect, arrowOptions, random, origin) {
715
1051
  function drawArrowHead(svg, tip, angle, style, random, shaftLength = Infinity) {
716
1052
  const length = Math.min(9 + style.strokeWidth * 1.4, shaftLength * 0.3);
717
1053
  const spread = 0.46 + random() * 0.1;
1054
+ const arms = [];
718
1055
  for (const side of [-1, 1]) {
719
1056
  const armAngle = angle + spread * side;
720
1057
  const tail = {
721
1058
  x: tip.x - length * Math.cos(armAngle),
722
1059
  y: tip.y - length * Math.sin(armAngle)
723
1060
  };
724
- appendPath(
725
- svg,
726
- roughLine(tail.x, tail.y, tip.x, tip.y, style.roughness * 0.5, random, {
727
- steps: 2,
728
- bow: 0.35 * side
729
- }),
730
- { ...style, dashed: false }
1061
+ arms.push(
1062
+ appendPath(
1063
+ svg,
1064
+ roughLine(tail.x, tail.y, tip.x, tip.y, style.roughness * 0.5, random, {
1065
+ steps: 2,
1066
+ bow: 0.35 * side
1067
+ }),
1068
+ { ...style, dashed: false }
1069
+ )
731
1070
  );
732
1071
  }
1072
+ return arms;
1073
+ }
1074
+ function bowOverTheTop(perpendicular) {
1075
+ if (Math.abs(perpendicular.y) < 0.02) return perpendicular.x > 0 ? 1 : -1;
1076
+ return perpendicular.y > 0 ? -1 : 1;
733
1077
  }
734
1078
  function boundsOf(points) {
735
1079
  const xs = points.map((p) => p.x);
@@ -741,7 +1085,7 @@ function boundsOf(points) {
741
1085
  height: Math.max(...ys) - Math.min(...ys)
742
1086
  };
743
1087
  }
744
- function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
1088
+ function drawArrow(svg, rect, arrowOptions, style, seed, origin = null, context = {}) {
745
1089
  const random = createRandom(seed + 17);
746
1090
  const { start, end } = arrowEndpoints(rect, arrowOptions, random, origin);
747
1091
  const dx = end.x - start.x;
@@ -750,6 +1094,7 @@ function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
750
1094
  const dashed = arrowOptions.style === "dotted";
751
1095
  let tipAngle = Math.atan2(dy, dx);
752
1096
  let shaft;
1097
+ let ghost = null;
753
1098
  const hull = [start, end];
754
1099
  if (arrowOptions.style === "straight") {
755
1100
  shaft = roughLine(start.x, start.y, end.x, end.y, style.roughness, random, {
@@ -763,7 +1108,12 @@ function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
763
1108
  y: rect.y + rect.height / 2 - mid.y
764
1109
  };
765
1110
  const lateral = perpendicular.x * towardCenter.x + perpendicular.y * towardCenter.y;
766
- const sign = Math.abs(lateral) > distance * 0.12 ? lateral > 0 ? -1 : 1 : random() < 0.5 ? -1 : 1;
1111
+ const sign = Math.abs(lateral) > distance * 0.12 ? lateral > 0 ? -1 : 1 : (
1112
+ // Nothing to steer around, so sweep over the top: a hand arcs above
1113
+ // the straight line and drops onto the target, and a coin flip here
1114
+ // used to mirror the whole gesture from one redraw to the next.
1115
+ bowOverTheTop(perpendicular)
1116
+ );
767
1117
  const bow = Math.min(distance * (0.16 + random() * 0.08), 30) * sign;
768
1118
  const c1x = start.x + dx * 0.28 + perpendicular.x * bow;
769
1119
  const c1y = start.y + dy * 0.28 + perpendicular.y * bow;
@@ -778,34 +1128,46 @@ function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
778
1128
  { x: c2x, y: c2y },
779
1129
  end,
780
1130
  radius,
781
- random
1131
+ random,
1132
+ -sign
782
1133
  );
783
1134
  shaft = catmullRomPath(points, false);
784
1135
  hull.push(...points);
785
1136
  const last = points[points.length - 1];
786
1137
  const prior = points[points.length - 2] ?? start;
787
1138
  tipAngle = Math.atan2(last.y - prior.y, last.x - prior.x);
788
- appendPath(svg, shaft, { ...style, dashed });
789
- drawArrowHead(svg, end, tipAngle, style, random, distance);
790
- return { start, end, bounds: boundsOf(hull) };
1139
+ } else {
1140
+ shaft = `M ${fmt(start.x)} ${fmt(start.y)} C ${fmt(c1x)} ${fmt(c1y)} ${fmt(c2x)} ${fmt(c2y)} ${fmt(end.x)} ${fmt(end.y)}`;
1141
+ if (style.roughness >= 1.1) {
1142
+ const drift = () => (random() - 0.5) * 1.4;
1143
+ 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)}`;
1144
+ }
1145
+ tipAngle = Math.atan2(end.y - c2y, end.x - c2x);
791
1146
  }
792
- shaft = `M ${fmt(start.x)} ${fmt(start.y)} C ${fmt(c1x)} ${fmt(c1y)} ${fmt(c2x)} ${fmt(c2y)} ${fmt(end.x)} ${fmt(end.y)}`;
793
- if (style.roughness >= 1.1) {
794
- const drift = () => (random() - 0.5) * 1.4;
795
- 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)}`;
796
- appendPath(svg, ghost, {
1147
+ }
1148
+ const group = document.createElementNS(SVG_NS, "g");
1149
+ svg.appendChild(group);
1150
+ const shaftPaths = [];
1151
+ if (ghost) {
1152
+ shaftPaths.push(
1153
+ appendPath(group, ghost, {
797
1154
  ...style,
798
1155
  opacity: style.opacity * 0.3,
799
1156
  strokeWidth: style.strokeWidth * 0.7,
800
1157
  dashed
801
- });
802
- }
803
- tipAngle = Math.atan2(end.y - c2y, end.x - c2x);
1158
+ })
1159
+ );
804
1160
  }
805
- appendPath(svg, shaft, { ...style, dashed });
806
- drawArrowHead(svg, end, tipAngle, style, random, distance);
1161
+ shaftPaths.push(appendPath(group, shaft, { ...style, dashed }));
1162
+ const head = drawArrowHead(group, end, tipAngle, style, random, distance);
1163
+ animateArrow(
1164
+ group,
1165
+ { shaft: shaftPaths, head, start, end },
1166
+ arrowOptions.animate,
1167
+ context.elapsed
1168
+ );
807
1169
  const bounds = boundsOf(hull);
808
- return { start, end, bounds };
1170
+ return { start, end, bounds, group };
809
1171
  }
810
1172
  function drawUnderline(svg, x, y, width, style, random) {
811
1173
  if (width <= 4) return;
@@ -872,6 +1234,7 @@ function createNoteText(svg, text, style, options = {}) {
872
1234
  const fontSize = style.fontSize ?? NOTE_FONT_SIZE;
873
1235
  const lines = wrapWords(text, options.wordsPerLine ?? DEFAULT_WORDS_PER_LINE);
874
1236
  const lineHeight = fontSize * 1.05;
1237
+ const rtl = options.rtl ?? isRtlText(text);
875
1238
  const group = document.createElementNS(SVG_NS, "g");
876
1239
  const textEl = document.createElementNS(SVG_NS, "text");
877
1240
  textEl.setAttribute("x", "0");
@@ -884,6 +1247,13 @@ function createNoteText(svg, text, style, options = {}) {
884
1247
  textEl.setAttribute("letter-spacing", "0.4");
885
1248
  textEl.setAttribute("text-anchor", "start");
886
1249
  textEl.setAttribute("dominant-baseline", "auto");
1250
+ if (rtl) {
1251
+ textEl.setAttribute("direction", "rtl");
1252
+ textEl.setAttribute("unicode-bidi", "embed");
1253
+ }
1254
+ if (options.locale) {
1255
+ textEl.setAttributeNS(XML_NS, "xml:lang", options.locale);
1256
+ }
887
1257
  const tspans = lines.map((line, index) => {
888
1258
  const tspan = document.createElementNS(SVG_NS, "tspan");
889
1259
  tspan.textContent = line;
@@ -912,15 +1282,17 @@ function createNoteText(svg, text, style, options = {}) {
912
1282
  height,
913
1283
  ascent,
914
1284
  descent,
915
- fontSize
1285
+ fontSize,
1286
+ rtl
916
1287
  };
917
1288
  }
918
1289
  function placeNoteText(note, box, style, seed, options = {}) {
919
1290
  const random = createRandom(seed + 55);
920
1291
  const tilt = options.tilt ?? (random() - 0.5) * 7;
921
- note.textEl.setAttribute("x", fmt(box.x));
1292
+ const anchorX = note.rtl ? box.x + box.width : box.x;
1293
+ note.textEl.setAttribute("x", fmt(anchorX));
922
1294
  note.textEl.setAttribute("y", fmt(box.y + note.ascent));
923
- for (const tspan of note.tspans) tspan.setAttribute("x", fmt(box.x));
1295
+ for (const tspan of note.tspans) tspan.setAttribute("x", fmt(anchorX));
924
1296
  const cx = box.x + box.width / 2;
925
1297
  const cy = box.y + box.height / 2;
926
1298
  note.group.setAttribute(
@@ -929,9 +1301,10 @@ function placeNoteText(note, box, style, seed, options = {}) {
929
1301
  );
930
1302
  if (options.underline !== false) {
931
1303
  const lastWidth = note.lineWidths[note.lineWidths.length - 1] ?? box.width;
1304
+ const underlineX = note.rtl ? box.x + box.width - lastWidth + 1 : box.x + 1;
932
1305
  drawUnderline(
933
1306
  note.group,
934
- box.x + 1,
1307
+ underlineX,
935
1308
  box.y + box.height + 2,
936
1309
  lastWidth - 2,
937
1310
  {
@@ -959,7 +1332,7 @@ function createOverlaySvg(left, top, width, height) {
959
1332
  svg.style.height = `${height}px`;
960
1333
  svg.style.pointerEvents = "none";
961
1334
  svg.style.overflow = "visible";
962
- svg.style.zIndex = "2147483646";
1335
+ svg.style.willChange = "transform";
963
1336
  return svg;
964
1337
  }
965
1338
 
@@ -1006,12 +1379,17 @@ function nudgeIntoBand(box, guard, band) {
1006
1379
  return { ...box, x: box.x + dx };
1007
1380
  }
1008
1381
  function renderAnnotation(svg, rect, noteOptions, style, seed = 1, layout = {}) {
1009
- if (!noteOptions?.text) return null;
1382
+ const { text, locale } = resolveLocalizedText(
1383
+ noteOptions?.text,
1384
+ noteOptions?.locale
1385
+ );
1386
+ if (!text) return null;
1010
1387
  const band = layout.band ?? null;
1011
1388
  const position = isValidPosition(noteOptions.position) ? noteOptions.position : "top-right";
1012
1389
  const random = createRandom(seed + 9);
1013
- const note = createNoteText(svg, noteOptions.text, style, {
1014
- wordsPerLine: noteOptions.wordsPerLine
1390
+ const note = createNoteText(svg, text, style, {
1391
+ wordsPerLine: noteOptions.wordsPerLine,
1392
+ locale
1015
1393
  });
1016
1394
  const tilt = (random() - 0.45) * 7;
1017
1395
  const tiltPad = Math.abs(Math.sin(tilt * Math.PI / 180)) * note.width * 0.5;
@@ -1053,6 +1431,25 @@ function drawDot(parent, x, y, style, scale = 1.7) {
1053
1431
  strokeWidth: style.strokeWidth * scale
1054
1432
  });
1055
1433
  }
1434
+ function drawFleck(parent, x, y, radius, style, random) {
1435
+ const count = 6;
1436
+ const points = [];
1437
+ for (let i = 0; i < count; i++) {
1438
+ const angle = Math.PI * 2 * i / count + (random() - 0.5) * 0.5;
1439
+ const r = radius * (0.78 + random() * 0.34);
1440
+ points.push({
1441
+ x: x + Math.cos(angle) * r,
1442
+ y: y + Math.sin(angle) * r
1443
+ });
1444
+ }
1445
+ const blob = appendPath(parent, catmullRomPath(points, true), {
1446
+ ...style,
1447
+ fill: style.color,
1448
+ strokeWidth: 0
1449
+ });
1450
+ blob.style.filter = `blur(${(radius * 0.35).toFixed(2)}px)`;
1451
+ return blob;
1452
+ }
1056
1453
  function drawHeart(parent, size, style, random) {
1057
1454
  const s = size;
1058
1455
  const j = () => (random() - 0.5) * s * 0.12;
@@ -1076,26 +1473,61 @@ function twinkle(parent, radiusX, radiusY, pinch, style) {
1076
1473
  `Q ${(-c).toFixed(2)} ${(-c).toFixed(2)} 0 ${(-ry).toFixed(2)}`,
1077
1474
  "Z"
1078
1475
  ].join(" ");
1079
- appendPath(parent, path, style);
1476
+ return appendPath(parent, path, style);
1080
1477
  }
1081
1478
  function drawSparkle(parent, size, style, random) {
1082
1479
  twinkle(
1083
1480
  parent,
1084
- size * (0.34 + random() * 0.1),
1481
+ size * (0.62 + random() * 0.12),
1085
1482
  size,
1086
- size * (0.06 + random() * 0.06),
1483
+ size * (0.14 + random() * 0.08),
1087
1484
  style
1088
1485
  );
1089
1486
  }
1090
1487
  function drawStar(parent, size, style, random) {
1091
1488
  twinkle(
1092
1489
  parent,
1093
- size * (0.66 + random() * 0.12),
1094
- size,
1095
- size * (0.2 + random() * 0.08),
1490
+ size * (0.62 + random() * 0.12),
1491
+ size * (0.8 + random() * 0.12),
1492
+ size * (0.14 + random() * 0.08),
1096
1493
  style
1097
1494
  );
1098
1495
  }
1496
+ function drawGlimmer(parent, size, style, random) {
1497
+ appendGlow(parent, size * (1.2 + random() * 0.2), {
1498
+ color: style.color,
1499
+ opacity: style.opacity * 0.22
1500
+ });
1501
+ const armX = size * (0.5 + random() * 0.08);
1502
+ const armY = size * (0.45 + random() * 0.08);
1503
+ const bluntness = size * 0.19;
1504
+ const main = twinkle(parent, armX, armY, size * (0.08 + random() * 0.03), {
1505
+ ...style,
1506
+ fill: style.color,
1507
+ strokeWidth: bluntness
1508
+ });
1509
+ main.setAttribute("stroke-linejoin", "round");
1510
+ main.setAttribute("stroke-linecap", "round");
1511
+ main.style.filter = `blur(${(size * 0.05).toFixed(2)}px)`;
1512
+ const gap = bluntness / 2 + size * 0.2;
1513
+ const corners = [
1514
+ { x: 0, y: -(armY + gap) },
1515
+ { x: armX + gap, y: 0 },
1516
+ { x: 0, y: armY + gap },
1517
+ { x: -(armX + gap), y: 0 }
1518
+ ];
1519
+ for (const { x, y } of corners) {
1520
+ const jitter = 0.95 + random() * 0.1;
1521
+ drawFleck(
1522
+ parent,
1523
+ x * jitter,
1524
+ y * jitter,
1525
+ size * (0.1 + random() * 0.03),
1526
+ style,
1527
+ random
1528
+ );
1529
+ }
1530
+ }
1099
1531
  function drawTwinkle(parent, size, style, random) {
1100
1532
  const count = 5;
1101
1533
  const first = -Math.PI / 2 + (random() - 0.5) * 0.5;
@@ -1203,46 +1635,15 @@ function drawEmphasis(parent, size, style, random, opts = {}) {
1203
1635
  );
1204
1636
  }
1205
1637
  }
1206
- function drawDots(parent, size, style, random) {
1207
- let x = -size * 0.7;
1208
- for (let i = 0; i < 3; i++) {
1209
- drawDot(parent, x, (random() - 0.5) * size * 0.16, style);
1210
- x += size * (0.6 + random() * 0.25);
1211
- }
1212
- }
1213
- function drawStroke(parent, size, style, random) {
1214
- appendPath(
1215
- parent,
1216
- roughLine(-size, 0, size, 0, 1.2, random, {
1217
- bow: (random() - 0.5) * size * 0.35,
1218
- steps: 4
1219
- }),
1220
- style
1221
- );
1222
- }
1223
- function drawSteam(parent, size, style, random) {
1224
- const lean = random() < 0.5 ? -1 : 1;
1225
- for (let i = 0; i < 2; i++) {
1226
- const x = (i - 0.5) * size * 0.72;
1227
- const base = size * 0.62 - i * size * 0.14;
1228
- appendPath(
1229
- parent,
1230
- `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)}`,
1231
- style
1232
- );
1233
- }
1234
- }
1235
1638
  var DECORATION_DRAWERS = {
1236
1639
  heart: drawHeart,
1237
1640
  sparkle: drawSparkle,
1238
1641
  star: drawStar,
1239
1642
  twinkle: drawTwinkle,
1643
+ glimmer: drawGlimmer,
1240
1644
  smiley: drawSmiley,
1241
1645
  emphasis: drawEmphasis,
1242
- arcs: drawArcs,
1243
- dots: drawDots,
1244
- stroke: drawStroke,
1245
- steam: drawSteam
1646
+ arcs: drawArcs
1246
1647
  };
1247
1648
  var TEXT_TYPES = ["heart", "sparkle"];
1248
1649
  var CORNER_ACCENTS = ["arcs", "emphasis"];
@@ -1253,12 +1654,10 @@ var TYPE_TRAITS = {
1253
1654
  sparkle: { size: [6, 9], tilt: 20, standoff: 22 },
1254
1655
  star: { size: [8, 11], tilt: 25, standoff: 26 },
1255
1656
  twinkle: { size: [5, 7.5], tilt: 30, standoff: 26, brightness: 1.4 },
1657
+ glimmer: { size: [7, 10], tilt: 20, standoff: 28, brightness: 1.3 },
1256
1658
  smiley: { size: [8, 11], tilt: 10, standoff: 22 },
1257
1659
  emphasis: { size: [12, 17], tilt: 10, standoff: 13, outward: true },
1258
- arcs: { size: [11, 16], tilt: 12, standoff: 12, outward: true },
1259
- dots: { size: [6, 9], tilt: 8, standoff: 20 },
1260
- stroke: { size: [8, 12], tilt: 40, standoff: 20 },
1261
- steam: { size: [8, 11], tilt: 10, standoff: 20 }
1660
+ arcs: { size: [11, 16], tilt: 12, standoff: 12, outward: true }
1262
1661
  };
1263
1662
  var DEFAULT_TYPES = [
1264
1663
  "twinkle",
@@ -1461,6 +1860,9 @@ function ensureHandwrittenFont() {
1461
1860
 
1462
1861
  // src/doodle.js
1463
1862
  var instances = /* @__PURE__ */ new WeakMap();
1863
+ function now() {
1864
+ return typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
1865
+ }
1464
1866
  function localBand(overlayRect, inset = 8) {
1465
1867
  const width = window.innerWidth || document.documentElement.clientWidth || 0;
1466
1868
  return {
@@ -1477,19 +1879,24 @@ var DoodleOverlay = class {
1477
1879
  this.element = element;
1478
1880
  this.options = options;
1479
1881
  this.seed = Math.floor(Math.random() * 1e9);
1882
+ this.startedAt = now();
1480
1883
  this.svg = null;
1481
1884
  this.resizeObserver = null;
1482
1885
  this.mutationObserver = null;
1483
1886
  this.pendingFrame = null;
1484
- this.scrollTargets = [];
1485
- this.onScroll = this.scheduleUpdate.bind(this);
1887
+ this.pendingRepositionFrame = null;
1888
+ this.basePosition = { x: 0, y: 0 };
1889
+ this.listeners = [];
1890
+ this.onScroll = this.scheduleReposition.bind(this);
1486
1891
  this.onResize = this.scheduleUpdate.bind(this);
1892
+ this.onLanguageChange = this.scheduleUpdate.bind(this);
1487
1893
  this.childElements = [];
1488
1894
  this.mount();
1489
1895
  }
1490
1896
  mount() {
1491
1897
  this.svg = createOverlaySvg(0, 0, 0, 0);
1492
- document.body.appendChild(this.svg);
1898
+ insertOverlaySvg(this.svg, this.element);
1899
+ syncOverlayStacking(this.svg, this.element, this.options.zIndex);
1493
1900
  this.resizeObserver = new ResizeObserver(this.onResize);
1494
1901
  this.resizeObserver.observe(this.element);
1495
1902
  if (this.options.children) {
@@ -1503,10 +1910,20 @@ var DoodleOverlay = class {
1503
1910
  });
1504
1911
  for (const target of getScrollableAncestors(this.element)) {
1505
1912
  target.addEventListener("scroll", this.onScroll, { passive: true });
1506
- this.scrollTargets.push({ target, type: "scroll" });
1913
+ this.listeners.push({ target, type: "scroll", handler: this.onScroll });
1507
1914
  }
1508
1915
  window.addEventListener("resize", this.onResize, { passive: true });
1509
- this.scrollTargets.push({ target: window, type: "resize" });
1916
+ this.listeners.push({
1917
+ target: window,
1918
+ type: "resize",
1919
+ handler: this.onResize
1920
+ });
1921
+ window.addEventListener("languagechange", this.onLanguageChange);
1922
+ this.listeners.push({
1923
+ target: window,
1924
+ type: "languagechange",
1925
+ handler: this.onLanguageChange
1926
+ });
1510
1927
  if (this.options.note && this.options.autoLoadFont && !this.options.fontFamily) {
1511
1928
  ensureHandwrittenFont();
1512
1929
  }
@@ -1530,6 +1947,46 @@ var DoodleOverlay = class {
1530
1947
  this.update();
1531
1948
  });
1532
1949
  }
1950
+ scheduleReposition() {
1951
+ if (this.pendingFrame != null || this.pendingRepositionFrame != null) {
1952
+ return;
1953
+ }
1954
+ this.pendingRepositionFrame = scheduleFrame(() => {
1955
+ this.pendingRepositionFrame = null;
1956
+ this.reposition();
1957
+ });
1958
+ }
1959
+ /**
1960
+ * Cheap scroll-driven path: slide the overlay to the element's new
1961
+ * viewport position without touching its contents. Scrolling can't change
1962
+ * an element's size, so the rects, viewBox and every hand-drawn stroke
1963
+ * inside them are still valid - only the overlay's position needs to move.
1964
+ *
1965
+ * That move is a `transform`, not `left`/`top`. `left`/`top` sit in the
1966
+ * same box-geometry pass as layout, so the browser re-checks layout on
1967
+ * every write; `transform` is composited, so the scroll path never
1968
+ * touches layout at all.
1969
+ */
1970
+ reposition() {
1971
+ if (!this.svg || !this.element.isConnected) return;
1972
+ const { padding, radius } = this.options;
1973
+ const targets = this.getTargets();
1974
+ const margin = this.overlayMargin();
1975
+ const absoluteRects = targets.map(
1976
+ (target) => getElementBounds(target, padding, radius)
1977
+ );
1978
+ const overlayRect = unionRects(absoluteRects, margin);
1979
+ const currentWidth = parseFloat(this.svg.style.width) || 0;
1980
+ const currentHeight = parseFloat(this.svg.style.height) || 0;
1981
+ const resized = Math.abs(overlayRect.width - currentWidth) > 0.5 || Math.abs(overlayRect.height - currentHeight) > 0.5;
1982
+ if (resized) {
1983
+ this.update();
1984
+ return;
1985
+ }
1986
+ const dx = overlayRect.x - this.basePosition.x;
1987
+ const dy = overlayRect.y - this.basePosition.y;
1988
+ this.svg.style.transform = `translate3d(${dx}px, ${dy}px, 0)`;
1989
+ }
1533
1990
  getTargets() {
1534
1991
  if (this.options.children) {
1535
1992
  const nodes = this.element.querySelectorAll(this.options.children);
@@ -1542,11 +1999,13 @@ var DoodleOverlay = class {
1542
1999
  const { padding, radius, color, strokeWidth, roughness, opacity } = this.options;
1543
2000
  const targets = this.getTargets();
1544
2001
  const margin = this.overlayMargin();
2002
+ const elapsed = now() - this.startedAt;
1545
2003
  const absoluteRects = targets.map(
1546
2004
  (target) => getElementBounds(target, padding, radius)
1547
2005
  );
1548
2006
  const overlayRect = unionRects(absoluteRects, margin);
1549
2007
  clearSvg(this.svg);
2008
+ syncOverlayStacking(this.svg, this.element, this.options.zIndex);
1550
2009
  this.svg.setAttribute("width", String(overlayRect.width));
1551
2010
  this.svg.setAttribute("height", String(overlayRect.height));
1552
2011
  this.svg.setAttribute("viewBox", `0 0 ${overlayRect.width} ${overlayRect.height}`);
@@ -1554,6 +2013,8 @@ var DoodleOverlay = class {
1554
2013
  this.svg.style.top = `${overlayRect.y}px`;
1555
2014
  this.svg.style.width = `${overlayRect.width}px`;
1556
2015
  this.svg.style.height = `${overlayRect.height}px`;
2016
+ this.svg.style.transform = "";
2017
+ this.basePosition = { x: overlayRect.x, y: overlayRect.y };
1557
2018
  const strokeStyle = { color, strokeWidth, roughness, opacity };
1558
2019
  const noteStyle = {
1559
2020
  color,
@@ -1593,7 +2054,8 @@ var DoodleOverlay = class {
1593
2054
  this.options.arrow,
1594
2055
  strokeStyle,
1595
2056
  seed,
1596
- note ? note.box : null
2057
+ note ? note.box : null,
2058
+ { elapsed }
1597
2059
  ) : null;
1598
2060
  if (this.options.decorations) {
1599
2061
  renderDecorations(
@@ -1620,7 +2082,7 @@ var DoodleOverlay = class {
1620
2082
  overlayMargin() {
1621
2083
  const { note, arrow, decorations } = this.options;
1622
2084
  if (!note && !arrow && !decorations) return 48;
1623
- const text = typeof note?.text === "string" ? note.text : "";
2085
+ const { text } = resolveLocalizedText(note?.text, note?.locale);
1624
2086
  return Math.max(96, Math.min(240, 80 + text.length * 8));
1625
2087
  }
1626
2088
  destroy() {
@@ -1628,6 +2090,10 @@ var DoodleOverlay = class {
1628
2090
  cancelFrame(this.pendingFrame);
1629
2091
  this.pendingFrame = null;
1630
2092
  }
2093
+ if (this.pendingRepositionFrame != null) {
2094
+ cancelFrame(this.pendingRepositionFrame);
2095
+ this.pendingRepositionFrame = null;
2096
+ }
1631
2097
  if (this.resizeObserver) {
1632
2098
  this.resizeObserver.disconnect();
1633
2099
  this.resizeObserver = null;
@@ -1640,10 +2106,10 @@ var DoodleOverlay = class {
1640
2106
  document.fonts?.removeEventListener?.("loadingdone", this.onFontsLoaded);
1641
2107
  this.onFontsLoaded = null;
1642
2108
  }
1643
- for (const { target, type } of this.scrollTargets) {
1644
- target.removeEventListener(type, type === "scroll" ? this.onScroll : this.onResize);
2109
+ for (const { target, type, handler } of this.listeners) {
2110
+ target.removeEventListener(type, handler);
1645
2111
  }
1646
- this.scrollTargets = [];
2112
+ this.listeners = [];
1647
2113
  if (this.svg?.parentNode) {
1648
2114
  this.svg.parentNode.removeChild(this.svg);
1649
2115
  }