animot-presenter 0.5.4 → 0.5.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.
@@ -15,13 +15,22 @@ export declare function computeFloatSpeed(cfg: {
15
15
  speed: number;
16
16
  speedRandomness?: number;
17
17
  }, seed: string): number;
18
- export declare function getBackgroundStyle(bg: {
18
+ interface GradientStop {
19
+ color: string;
20
+ position: number;
21
+ }
22
+ interface BgInput {
19
23
  type: string;
20
24
  color?: string;
21
25
  gradient?: {
22
26
  type: string;
23
27
  angle?: number;
24
28
  colors: string[];
29
+ stops?: GradientStop[];
30
+ radialShape?: 'circle' | 'ellipse';
31
+ radialPosition?: string;
25
32
  };
26
33
  image?: string;
27
- }): string;
34
+ }
35
+ export declare function getBackgroundStyle(bg: BgInput): string;
36
+ export {};
@@ -73,16 +73,28 @@ export function computeFloatSpeed(cfg, seed) {
73
73
  return cfg.speed;
74
74
  return cfg.speed * (1 - r + r * hashFraction(seed, 2));
75
75
  }
76
+ function buildStops(colors, stops) {
77
+ if (stops && stops.length > 0) {
78
+ const sorted = [...stops].sort((a, b) => a.position - b.position);
79
+ return sorted.map((s) => `${s.color} ${s.position}%`).join(', ');
80
+ }
81
+ if (colors.length <= 1)
82
+ return colors[0] ?? '#000';
83
+ return colors.map((c, i) => `${c} ${(i / (colors.length - 1)) * 100}%`).join(', ');
84
+ }
76
85
  export function getBackgroundStyle(bg) {
77
86
  if (bg.type === 'transparent')
78
87
  return 'background: transparent';
79
88
  if (bg.type === 'solid')
80
89
  return `background-color: ${bg.color ?? 'transparent'}`;
81
90
  if (bg.type === 'gradient' && bg.gradient) {
82
- const { type, angle = 135, colors } = bg.gradient;
83
- if (type === 'linear')
84
- return `background: linear-gradient(${angle}deg, ${colors.join(', ')})`;
85
- return `background: radial-gradient(circle, ${colors.join(', ')})`;
91
+ const { type, angle = 135, colors, stops, radialShape = 'circle', radialPosition = 'center' } = bg.gradient;
92
+ const stopList = buildStops(colors, stops);
93
+ if (type === 'conic')
94
+ return `background: conic-gradient(from ${angle}deg at ${radialPosition}, ${stopList})`;
95
+ if (type === 'radial')
96
+ return `background: radial-gradient(${radialShape} at ${radialPosition}, ${stopList})`;
97
+ return `background: linear-gradient(${angle}deg, ${stopList})`;
86
98
  }
87
99
  if (bg.type === 'image' && bg.image)
88
100
  return `background-image: url(${bg.image}); background-size: cover; background-position: center`;
package/dist/types.d.ts CHANGED
@@ -107,10 +107,13 @@ export interface CodeElement extends BaseElement {
107
107
  headerRadius?: number;
108
108
  tabRadius?: number;
109
109
  }
110
- export type TextAnimationMode = 'instant' | 'typewriter' | 'fade-words';
110
+ export type TextAnimationMode = 'instant' | 'typewriter' | 'fade-words' | 'fade-letters' | 'handwriting' | 'bounce-in';
111
111
  export interface TextAnimationConfig {
112
112
  mode: TextAnimationMode;
113
113
  typewriterSpeed: number;
114
+ duration?: number;
115
+ stagger?: number;
116
+ loop?: boolean;
114
117
  }
115
118
  export interface TextElement extends BaseElement {
116
119
  type: 'text';
@@ -203,7 +206,9 @@ export interface ShapeElement extends BaseElement {
203
206
  type: 'shape';
204
207
  shapeType: ShapeType;
205
208
  fillColor: string;
209
+ fillOpacity?: number;
206
210
  strokeColor: string;
211
+ strokeOpacity?: number;
207
212
  strokeWidth: number;
208
213
  strokeStyle?: StrokeStyle;
209
214
  strokeDashGap?: number;
@@ -314,13 +319,20 @@ export interface ConfettiConfig {
314
319
  startVelocity: number;
315
320
  scalar: number;
316
321
  }
322
+ export interface GradientStop {
323
+ color: string;
324
+ position: number;
325
+ }
317
326
  export interface CanvasBackground {
318
327
  type: 'solid' | 'gradient' | 'image' | 'transparent';
319
328
  color?: string;
320
329
  gradient?: {
321
- type: 'linear' | 'radial';
330
+ type: 'linear' | 'radial' | 'conic';
322
331
  angle?: number;
323
332
  colors: string[];
333
+ stops?: GradientStop[];
334
+ radialShape?: 'circle' | 'ellipse';
335
+ radialPosition?: string;
324
336
  };
325
337
  image?: string;
326
338
  particles?: ParticlesConfig;
@@ -20,6 +20,10 @@ export interface ArrowClipDrawParams {
20
20
  endY: number;
21
21
  loop?: boolean;
22
22
  reverse?: boolean;
23
+ /** Slide loop duration in ms. When provided AND loop=true (or mode='flow'),
24
+ * the effective animation duration is rounded so an integer number fits in
25
+ * slide_duration — guarantees seamless GIF loop. */
26
+ slideDuration?: number;
23
27
  key?: unknown;
24
28
  }
25
29
  export declare function arrowClipDraw(node: SVGSVGElement, params: ArrowClipDrawParams): {
@@ -59,7 +59,13 @@ export function arrowClipDraw(node, params) {
59
59
  }
60
60
  const clipFull = clip(100); // fully hidden (100% inset on hide side)
61
61
  const clipNone = clip(0);
62
- const dur = Math.max(50, params.duration);
62
+ // Round duration so an integer number of cycles fits in slide_duration.
63
+ // Without this, GIF loop boundary shows the arrow mid-draw → snap to
64
+ // invisible → re-draw, which the user perceives as a reset.
65
+ const requested = Math.max(50, params.duration);
66
+ const dur = params.slideDuration && params.slideDuration > 0 && (params.loop || params.mode === 'flow')
67
+ ? params.slideDuration / Math.max(1, Math.round(params.slideDuration / requested))
68
+ : requested;
63
69
  const start = performance.now();
64
70
  const m = params.mode;
65
71
  // FLOW: marching ants — animate dashoffset continuously, keep base pattern.
@@ -156,10 +162,17 @@ export function arrowClipDraw(node, params) {
156
162
  reg.push(run);
157
163
  node.__svgAnimRestart = run;
158
164
  }
165
+ let lastKey = params.key;
159
166
  return {
160
167
  update(p) {
168
+ // Only restart when the explicit key actually changed (avoids constant
169
+ // resets from Svelte's per-render fresh params object).
170
+ const keyChanged = p.key !== lastKey;
161
171
  params = p;
162
- queueMicrotask(run);
172
+ if (keyChanged) {
173
+ lastKey = p.key;
174
+ queueMicrotask(run);
175
+ }
163
176
  },
164
177
  destroy() {
165
178
  reset();
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Svelte action: animates a text element's content via JS RAF.
3
+ *
4
+ * Implemented modes:
5
+ * • fade-letters — wraps each character in a <span> and fades them in with stagger
6
+ * • bounce-in — same wrapping, scale+translate pop per character
7
+ * • handwriting — re-renders the text inside an <svg><text> with stroke + transparent
8
+ * fill and animates `stroke-dashoffset` so the text appears to be
9
+ * drawn left-to-right (works best with cursive/script fonts)
10
+ *
11
+ * Other modes (instant, typewriter, fade-words) are handled elsewhere by the
12
+ * /present render path — this action no-ops for those.
13
+ *
14
+ * The action registers a restart fn into `window.__svgAnimRestart` so the
15
+ * server-side video export pipeline can reset all animations under the
16
+ * virtual clock at the start of the slide hold (same pattern as FlowMarkers
17
+ * and arrowClipDraw).
18
+ */
19
+ export type TextAnimateMode = 'instant' | 'typewriter' | 'fade-words' | 'fade-letters' | 'handwriting' | 'bounce-in';
20
+ export interface TextAnimateParams {
21
+ enabled: boolean;
22
+ mode: TextAnimateMode;
23
+ content: string;
24
+ /** Total duration in ms for handwriting; for stagger modes it's the per-letter
25
+ * delay budget — actual total = stagger * letterCount + perLetterDuration. */
26
+ duration: number;
27
+ stagger?: number;
28
+ loop?: boolean;
29
+ /** Cosmetic: applied to the inner SVG text in handwriting mode. */
30
+ color?: string;
31
+ fontSize?: number;
32
+ fontFamily?: string;
33
+ fontWeight?: number | string;
34
+ fontStyle?: string;
35
+ textAlign?: 'left' | 'center' | 'right';
36
+ /** Slide loop duration; when present the effective duration aligns to a
37
+ * cycle that fits an integer number of times into slide_duration so GIF
38
+ * loops are seamless in Flow mode. */
39
+ slideDuration?: number;
40
+ /** Bumped by the host component when ANY meaningful prop changes — without
41
+ * this the action would silently keep running with stale values. */
42
+ key?: unknown;
43
+ }
44
+ export declare function textAnimate(node: HTMLElement, params: TextAnimateParams): {
45
+ update(p: TextAnimateParams): void;
46
+ destroy(): void;
47
+ };
@@ -0,0 +1,348 @@
1
+ /**
2
+ * Svelte action: animates a text element's content via JS RAF.
3
+ *
4
+ * Implemented modes:
5
+ * • fade-letters — wraps each character in a <span> and fades them in with stagger
6
+ * • bounce-in — same wrapping, scale+translate pop per character
7
+ * • handwriting — re-renders the text inside an <svg><text> with stroke + transparent
8
+ * fill and animates `stroke-dashoffset` so the text appears to be
9
+ * drawn left-to-right (works best with cursive/script fonts)
10
+ *
11
+ * Other modes (instant, typewriter, fade-words) are handled elsewhere by the
12
+ * /present render path — this action no-ops for those.
13
+ *
14
+ * The action registers a restart fn into `window.__svgAnimRestart` so the
15
+ * server-side video export pipeline can reset all animations under the
16
+ * virtual clock at the start of the slide hold (same pattern as FlowMarkers
17
+ * and arrowClipDraw).
18
+ */
19
+ export function textAnimate(node, params) {
20
+ let raf = 0;
21
+ let originalContent = null;
22
+ let activeMode = null;
23
+ function clearAnim() {
24
+ if (raf)
25
+ cancelAnimationFrame(raf);
26
+ raf = 0;
27
+ }
28
+ function restore() {
29
+ clearAnim();
30
+ // Replace whatever we injected with the original content. Doing this on
31
+ // every (re)start keeps the DOM clean when the user toggles modes.
32
+ if (originalContent !== null)
33
+ node.textContent = originalContent;
34
+ }
35
+ function effectiveDuration() {
36
+ const requested = Math.max(50, params.duration || 800);
37
+ if (params.slideDuration && params.slideDuration > 0 && params.loop) {
38
+ return params.slideDuration / Math.max(1, Math.round(params.slideDuration / requested));
39
+ }
40
+ return requested;
41
+ }
42
+ /** Wraps each character of `text` in a <span class="ta-char">. Spaces are
43
+ * also wrapped (with a special class) so the per-letter animation runs
44
+ * uniformly across word boundaries while still preserving spacing. */
45
+ function wrapChars(text) {
46
+ const frag = document.createElement('span');
47
+ frag.className = 'ta-wrap';
48
+ for (const ch of text) {
49
+ const s = document.createElement('span');
50
+ s.className = ch === ' ' ? 'ta-char ta-space' : 'ta-char';
51
+ s.textContent = ch === ' ' ? ' ' : ch; // nbsp so spaces render as fixed-width in inline-block
52
+ s.style.display = 'inline-block';
53
+ frag.appendChild(s);
54
+ }
55
+ return frag;
56
+ }
57
+ function runFadeLetters() {
58
+ // Source the text from params.content, not node.textContent — the host
59
+ // template intentionally renders nothing when an action mode is active
60
+ // (so we don't get a flash of unstyled text), which means the DOM is
61
+ // empty when this runs. Reading textContent here would yield ''.
62
+ originalContent = params.content ?? '';
63
+ node.innerHTML = '';
64
+ const wrap = wrapChars(originalContent);
65
+ node.appendChild(wrap);
66
+ const chars = Array.from(wrap.querySelectorAll('.ta-char'));
67
+ const stagger = params.stagger ?? Math.max(20, Math.min(60, 600 / Math.max(1, chars.length)));
68
+ const perLetter = 280;
69
+ const total = stagger * chars.length + perLetter;
70
+ const dur = effectiveDuration();
71
+ const totalEffective = params.loop ? dur : total;
72
+ for (const c of chars)
73
+ c.style.opacity = '0';
74
+ const start = performance.now();
75
+ function step(now) {
76
+ const elapsed = (now - start) % (params.loop ? totalEffective : Number.POSITIVE_INFINITY);
77
+ let allDone = true;
78
+ for (let i = 0; i < chars.length; i++) {
79
+ const localStart = i * stagger;
80
+ const t = Math.min(1, Math.max(0, (elapsed - localStart) / perLetter));
81
+ if (t < 1)
82
+ allDone = false;
83
+ chars[i].style.opacity = String(t);
84
+ }
85
+ if (params.loop || !allDone)
86
+ raf = requestAnimationFrame(step);
87
+ else
88
+ raf = 0;
89
+ }
90
+ raf = requestAnimationFrame(step);
91
+ }
92
+ function runBounceIn() {
93
+ // Source the text from params.content, not node.textContent — the host
94
+ // template intentionally renders nothing when an action mode is active
95
+ // (so we don't get a flash of unstyled text), which means the DOM is
96
+ // empty when this runs. Reading textContent here would yield ''.
97
+ originalContent = params.content ?? '';
98
+ node.innerHTML = '';
99
+ const wrap = wrapChars(originalContent);
100
+ node.appendChild(wrap);
101
+ const chars = Array.from(wrap.querySelectorAll('.ta-char'));
102
+ const stagger = params.stagger ?? Math.max(30, Math.min(80, 800 / Math.max(1, chars.length)));
103
+ const perLetter = 380;
104
+ for (const c of chars) {
105
+ c.style.opacity = '0';
106
+ c.style.transform = 'translateY(0.4em) scale(0.6)';
107
+ c.style.transformOrigin = '50% 80%';
108
+ }
109
+ // `let` because the loop branch resets `start = now` to begin a new cycle.
110
+ let start = performance.now();
111
+ function step(now) {
112
+ const elapsed = now - start;
113
+ let allDone = true;
114
+ for (let i = 0; i < chars.length; i++) {
115
+ const t = Math.min(1, Math.max(0, (elapsed - i * stagger) / perLetter));
116
+ if (t < 1)
117
+ allDone = false;
118
+ // Spring-ish easing — overshoots slightly then settles.
119
+ const eased = 1 - Math.pow(1 - t, 3);
120
+ const overshoot = Math.sin(t * Math.PI) * 0.1;
121
+ const scale = 0.6 + (1 - 0.6) * eased + overshoot;
122
+ const translate = (1 - eased) * 0.4;
123
+ chars[i].style.opacity = String(eased);
124
+ chars[i].style.transform = `translateY(${translate}em) scale(${scale})`;
125
+ }
126
+ if (!allDone)
127
+ raf = requestAnimationFrame(step);
128
+ else if (params.loop) {
129
+ // Reset and loop — a small pause at the end keeps it readable.
130
+ const totalCycle = effectiveDuration();
131
+ if (now - start >= totalCycle) {
132
+ for (const c of chars) {
133
+ c.style.opacity = '0';
134
+ c.style.transform = 'translateY(0.4em) scale(0.6)';
135
+ }
136
+ start = now;
137
+ }
138
+ raf = requestAnimationFrame(step);
139
+ }
140
+ else {
141
+ raf = 0;
142
+ }
143
+ }
144
+ raf = requestAnimationFrame(step);
145
+ }
146
+ function runHandwriting() {
147
+ // Real per-character handwriting: each glyph is its own <text> element
148
+ // with its own stroke-dasharray. We measure char positions from a single
149
+ // hidden <text> first (the only reliable way to get kerning/spacing
150
+ // matching what the browser would draw) then build per-glyph elements
151
+ // laid out in absolute SVG coordinates. Each glyph is revealed in turn
152
+ // with a small overlap so the effect reads like a continuous pen, not
153
+ // a typewriter.
154
+ originalContent = params.content ?? '';
155
+ node.innerHTML = '';
156
+ if (!originalContent)
157
+ return;
158
+ const color = params.color ?? '#ffffff';
159
+ const fs = params.fontSize ?? 48;
160
+ const ff = params.fontFamily ?? 'inherit';
161
+ const fw = params.fontWeight ?? 400;
162
+ const fst = params.fontStyle ?? 'normal';
163
+ const align = params.textAlign ?? 'left';
164
+ const svgNS = 'http://www.w3.org/2000/svg';
165
+ const svg = document.createElementNS(svgNS, 'svg');
166
+ svg.setAttribute('width', '100%');
167
+ svg.setAttribute('height', '100%');
168
+ const w = Math.max(1, node.clientWidth);
169
+ const h = Math.max(1, node.clientHeight);
170
+ svg.setAttribute('viewBox', `0 0 ${w} ${h}`);
171
+ svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
172
+ svg.style.overflow = 'visible';
173
+ node.appendChild(svg);
174
+ // Step 1 — measure char positions from a hidden full-string render.
175
+ const measure = document.createElementNS(svgNS, 'text');
176
+ measure.setAttribute('x', '0');
177
+ measure.setAttribute('y', '50%');
178
+ measure.setAttribute('dominant-baseline', 'middle');
179
+ measure.setAttribute('text-anchor', 'start');
180
+ measure.setAttribute('visibility', 'hidden');
181
+ measure.style.fontSize = `${fs}px`;
182
+ measure.style.fontFamily = ff;
183
+ measure.style.fontWeight = String(fw);
184
+ measure.style.fontStyle = fst;
185
+ measure.textContent = originalContent;
186
+ svg.appendChild(measure);
187
+ const charPositions = [];
188
+ const charLengths = [];
189
+ let totalWidth = 0;
190
+ try {
191
+ const t = measure;
192
+ totalWidth = t.getComputedTextLength();
193
+ for (let i = 0; i < originalContent.length; i++) {
194
+ const pt = t.getStartPositionOfChar(i);
195
+ charPositions.push(pt.x);
196
+ const nextX = i + 1 < originalContent.length
197
+ ? t.getStartPositionOfChar(i + 1).x
198
+ : pt.x + (totalWidth - pt.x);
199
+ charLengths.push(Math.max(1, nextX - pt.x));
200
+ }
201
+ }
202
+ catch {
203
+ // Fallback: even spacing across host width.
204
+ totalWidth = w * 0.9;
205
+ const stepW = totalWidth / Math.max(1, originalContent.length);
206
+ for (let i = 0; i < originalContent.length; i++) {
207
+ charPositions.push(i * stepW);
208
+ charLengths.push(stepW);
209
+ }
210
+ }
211
+ // Honor textAlign by shifting the whole row in absolute coordinates.
212
+ const baseX = align === 'center' ? (w - totalWidth) / 2
213
+ : align === 'right' ? (w - totalWidth)
214
+ : 0;
215
+ svg.removeChild(measure);
216
+ // Step 2 — build per-character <text> elements.
217
+ const glyphs = [];
218
+ for (let i = 0; i < originalContent.length; i++) {
219
+ const ch = originalContent[i];
220
+ const t = document.createElementNS(svgNS, 'text');
221
+ t.setAttribute('x', String(baseX + charPositions[i]));
222
+ t.setAttribute('y', '50%');
223
+ t.setAttribute('dominant-baseline', 'middle');
224
+ t.setAttribute('text-anchor', 'start');
225
+ t.setAttribute('fill', 'transparent');
226
+ t.setAttribute('stroke', color);
227
+ t.setAttribute('stroke-width', '1');
228
+ t.setAttribute('stroke-linecap', 'round');
229
+ t.setAttribute('stroke-linejoin', 'round');
230
+ t.style.fontSize = `${fs}px`;
231
+ t.style.fontFamily = ff;
232
+ t.style.fontWeight = String(fw);
233
+ t.style.fontStyle = fst;
234
+ t.textContent = ch;
235
+ svg.appendChild(t);
236
+ // Spaces have no visible stroke — skip dashoffset math, they reveal
237
+ // instantly. For everything else, we don't know the exact outline
238
+ // length per glyph, so we pick a generous upper bound that comfortably
239
+ // exceeds any realistic glyph contour at this font size, then use an
240
+ // explicit *huge* gap so the dash pattern can't repeat. Without the
241
+ // huge gap, complex letters (g, B, &, ampersand-heavy scripts) whose
242
+ // outline is longer than our `len` estimate would show a second dash
243
+ // cycle peeking through before the animation reaches them.
244
+ const len = ch === ' ' ? 0 : Math.max(40, fs * 4 + charLengths[i] * 2.5);
245
+ const gap = len * 4;
246
+ t.style.strokeDasharray = `${len || 1} ${gap || 1}`;
247
+ t.style.strokeDashoffset = String(len || 1);
248
+ glyphs.push({ el: t, len });
249
+ }
250
+ const dur = effectiveDuration();
251
+ // Per-char duration with overlap. overlap=0.6 means each char starts after
252
+ // the previous is 60% drawn — looks like a continuous pen rather than a
253
+ // typewriter (overlap=1) or strict per-letter sequence (overlap=0).
254
+ const n = originalContent.length;
255
+ const overlap = 0.6;
256
+ // Solve: stagger * (n - 1) + perChar = dur, where stagger = perChar * overlap.
257
+ // → perChar * (overlap * (n - 1) + 1) = dur.
258
+ const perChar = dur / Math.max(1, overlap * (n - 1) + 1);
259
+ const stagger = perChar * overlap;
260
+ let start = performance.now();
261
+ function step(now) {
262
+ const elapsed = now - start;
263
+ let allDone = true;
264
+ for (let i = 0; i < glyphs.length; i++) {
265
+ const g = glyphs[i];
266
+ if (g.len === 0)
267
+ continue; // space — already invisible
268
+ const localStart = i * stagger;
269
+ const t = Math.min(1, Math.max(0, (elapsed - localStart) / perChar));
270
+ if (t < 1)
271
+ allDone = false;
272
+ const eased = 1 - Math.pow(1 - t, 3);
273
+ g.el.style.strokeDashoffset = String(g.len * (1 - eased));
274
+ // Cross-fade fill in over the last 30% of the glyph's draw so the
275
+ // finished letter reads cleanly. Otherwise thin script fonts look
276
+ // hollow and don't match the rest of the slide's text.
277
+ if (t > 0.7) {
278
+ const fillT = (t - 0.7) / 0.3;
279
+ g.el.setAttribute('fill', color);
280
+ g.el.setAttribute('fill-opacity', String(fillT));
281
+ }
282
+ }
283
+ if (!allDone) {
284
+ raf = requestAnimationFrame(step);
285
+ }
286
+ else if (params.loop) {
287
+ for (const g of glyphs) {
288
+ g.el.style.strokeDashoffset = String(g.len || 1);
289
+ g.el.setAttribute('fill', 'transparent');
290
+ g.el.removeAttribute('fill-opacity');
291
+ }
292
+ start = now;
293
+ raf = requestAnimationFrame(step);
294
+ }
295
+ else {
296
+ raf = 0;
297
+ }
298
+ }
299
+ raf = requestAnimationFrame(step);
300
+ }
301
+ function run() {
302
+ restore();
303
+ activeMode = params.mode;
304
+ if (!params.enabled)
305
+ return;
306
+ switch (params.mode) {
307
+ case 'fade-letters':
308
+ runFadeLetters();
309
+ break;
310
+ case 'bounce-in':
311
+ runBounceIn();
312
+ break;
313
+ case 'handwriting':
314
+ runHandwriting();
315
+ break;
316
+ default: /* other modes handled elsewhere */ break;
317
+ }
318
+ }
319
+ queueMicrotask(run);
320
+ if (typeof window !== 'undefined') {
321
+ const reg = (window.__svgAnimRestart ||= []);
322
+ reg.push(run);
323
+ node.__svgAnimRestart = run;
324
+ }
325
+ let lastKey = params.key;
326
+ return {
327
+ update(p) {
328
+ const keyChanged = p.key !== lastKey || p.mode !== activeMode;
329
+ params = p;
330
+ if (keyChanged) {
331
+ lastKey = p.key;
332
+ queueMicrotask(run);
333
+ }
334
+ },
335
+ destroy() {
336
+ restore();
337
+ if (typeof window !== 'undefined') {
338
+ const reg = window.__svgAnimRestart;
339
+ const fn = node.__svgAnimRestart;
340
+ if (reg && fn) {
341
+ const idx = reg.indexOf(fn);
342
+ if (idx >= 0)
343
+ reg.splice(idx, 1);
344
+ }
345
+ }
346
+ }
347
+ };
348
+ }