animot-presenter 0.2.8 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,247 @@
1
+ /**
2
+ * Svelte action: animates SVG paths inside the host node using a JS RAF loop.
3
+ *
4
+ * Why JS instead of CSS animations: CSS animations had timing issues — by the
5
+ * time the action finished computing each path's actual length via
6
+ * getTotalLength(), the CSS animation had already started using a fallback
7
+ * value (1000) which doesn't match the path's real length, causing the path
8
+ * to either flicker or stay fully visible. Doing everything in JS is reliable.
9
+ *
10
+ * Modes:
11
+ * • draw — strokes appear from 0 → length (handle reverse to flip)
12
+ * • undraw — strokes disappear from length → 0
13
+ * • draw-undraw — appear then disappear, repeats if loop=true
14
+ * • flow — marching ants. Keeps base dasharray, shifts offset.
15
+ * • none — no animation; restore original attributes.
16
+ */
17
+ /** Returns true if the element has a visible stroke (so dashoffset animation
18
+ * will actually look like something). Fill-only paths can't be path-traced. */
19
+ function hasStroke(el) {
20
+ const stroke = el.getAttribute('stroke') ?? getComputedStyle(el).stroke;
21
+ if (!stroke || stroke === 'none' || stroke === 'transparent')
22
+ return false;
23
+ // rgba(0,0,0,0) == transparent
24
+ if (/^rgba?\([^)]*,\s*0(\.0+)?\s*\)$/i.test(stroke))
25
+ return false;
26
+ return true;
27
+ }
28
+ export function traceSvgPaths(node, params) {
29
+ let raf = 0;
30
+ let states = [];
31
+ /** When true, the SVG is fill-based and we use clip-path on the wrapper
32
+ * instead of per-path dashoffset (which does nothing on filled shapes). */
33
+ let useClipPath = false;
34
+ function gather() {
35
+ const els = node.querySelectorAll('path, circle, rect, line, polyline, polygon, ellipse');
36
+ states = [];
37
+ let strokeCount = 0;
38
+ let totalCount = 0;
39
+ for (const el of els) {
40
+ let len = 0;
41
+ try {
42
+ if (typeof el.getTotalLength === 'function')
43
+ len = el.getTotalLength();
44
+ }
45
+ catch {
46
+ len = 0;
47
+ }
48
+ if (len > 0) {
49
+ totalCount++;
50
+ if (hasStroke(el)) {
51
+ strokeCount++;
52
+ states.push({
53
+ el,
54
+ length: len,
55
+ baseDash: el.getAttribute('stroke-dasharray'),
56
+ baseOffset: el.getAttribute('stroke-dashoffset')
57
+ });
58
+ }
59
+ }
60
+ }
61
+ // If most/all paths are fill-based (no stroke), fall back to wrapper clip-path.
62
+ useClipPath = totalCount > 0 && strokeCount < totalCount * 0.5;
63
+ }
64
+ function restore() {
65
+ for (const s of states) {
66
+ if (s.baseDash)
67
+ s.el.style.strokeDasharray = s.baseDash;
68
+ else
69
+ s.el.style.removeProperty('stroke-dasharray');
70
+ if (s.baseOffset)
71
+ s.el.style.strokeDashoffset = s.baseOffset;
72
+ else
73
+ s.el.style.removeProperty('stroke-dashoffset');
74
+ }
75
+ // Clear any clip-path we may have set on the wrapper.
76
+ node.style.clipPath = '';
77
+ }
78
+ function reset() {
79
+ if (raf)
80
+ cancelAnimationFrame(raf);
81
+ raf = 0;
82
+ restore();
83
+ }
84
+ function run() {
85
+ reset();
86
+ if (!params.enabled || params.mode === 'none')
87
+ return;
88
+ gather();
89
+ const dur = Math.max(50, params.duration);
90
+ let start = null;
91
+ const m = params.mode;
92
+ // Fill-based SVGs (e.g. complex logos/glyphs with fill="#fff") can't be
93
+ // meaningfully animated via stroke-dashoffset. Fall back to a clip-path
94
+ // reveal on the wrapper element. The flow/marching-ants mode also can't
95
+ // work without a stroke, so for fill-only SVGs we treat all draw modes
96
+ // as a wipe reveal.
97
+ if (useClipPath) {
98
+ const reveal = m === 'flow' ? 'draw' : m; // flow → wipe-in for fill icons
99
+ const dir = params.reverse ? 'right' : 'left';
100
+ function clip(insetPct) {
101
+ switch (dir) {
102
+ case 'left': return `inset(0 ${insetPct}% 0 0)`;
103
+ case 'right': return `inset(0 0 0 ${insetPct}%)`;
104
+ case 'top': return `inset(0 0 ${insetPct}% 0)`;
105
+ case 'bottom': return `inset(${insetPct}% 0 0 0)`;
106
+ }
107
+ }
108
+ node.style.clipPath = reveal === 'draw' || reveal === 'draw-undraw' ? clip(100) : clip(0);
109
+ function clipStep(t) {
110
+ if (start === null)
111
+ start = t;
112
+ const elapsed = t - start;
113
+ const progress = Math.min(elapsed / dur, 1);
114
+ const eased = 1 - Math.pow(1 - progress, 3);
115
+ let inset = 0;
116
+ if (reveal === 'draw')
117
+ inset = 100 * (1 - eased);
118
+ else if (reveal === 'undraw')
119
+ inset = 100 * eased;
120
+ else if (reveal === 'draw-undraw') {
121
+ if (progress < 0.5) {
122
+ const lt = progress / 0.5;
123
+ const le = 1 - Math.pow(1 - lt, 3);
124
+ inset = 100 * (1 - le);
125
+ }
126
+ else {
127
+ const lt = (progress - 0.5) / 0.5;
128
+ const le = 1 - Math.pow(1 - lt, 3);
129
+ inset = 100 * le;
130
+ }
131
+ }
132
+ node.style.clipPath = clip(inset);
133
+ if (progress < 1)
134
+ raf = requestAnimationFrame(clipStep);
135
+ else if (params.loop) {
136
+ start = null;
137
+ raf = requestAnimationFrame(clipStep);
138
+ }
139
+ else {
140
+ raf = 0;
141
+ }
142
+ }
143
+ raf = requestAnimationFrame(clipStep);
144
+ return;
145
+ }
146
+ if (states.length === 0)
147
+ return;
148
+ if (m === 'flow') {
149
+ // Marching ants — keep each path's existing dash pattern. If a path
150
+ // has none, give it one so the flow is visible.
151
+ for (const s of states) {
152
+ if (!s.baseDash || s.baseDash === 'none')
153
+ s.el.style.strokeDasharray = '8 5';
154
+ }
155
+ const cycle = 24;
156
+ const dir = params.reverse ? 1 : -1;
157
+ function flowStep(t) {
158
+ if (start === null)
159
+ start = t;
160
+ const elapsed = t - start;
161
+ const offset = (dir * (elapsed / dur) * cycle) % (cycle * 1000);
162
+ for (const s of states)
163
+ s.el.style.strokeDashoffset = String(offset);
164
+ raf = requestAnimationFrame(flowStep);
165
+ }
166
+ raf = requestAnimationFrame(flowStep);
167
+ return;
168
+ }
169
+ // Draw / undraw / draw-undraw: dasharray = each path's actual length so
170
+ // the animation spans the full configured duration.
171
+ for (const s of states)
172
+ s.el.style.strokeDasharray = String(s.length);
173
+ function drawStep(t) {
174
+ if (start === null)
175
+ start = t;
176
+ const elapsed = t - start;
177
+ const progress = Math.min(elapsed / dur, 1);
178
+ let offsetFrac; // 0 = fully drawn, 1 = fully invisible
179
+ if (m === 'draw') {
180
+ const eased = 1 - Math.pow(1 - progress, 3);
181
+ offsetFrac = 1 - eased;
182
+ }
183
+ else if (m === 'undraw') {
184
+ const eased = 1 - Math.pow(1 - progress, 3);
185
+ offsetFrac = eased;
186
+ }
187
+ else {
188
+ // draw-undraw
189
+ if (progress < 0.5) {
190
+ const lt = progress / 0.5;
191
+ const le = 1 - Math.pow(1 - lt, 3);
192
+ offsetFrac = 1 - le;
193
+ }
194
+ else {
195
+ const lt = (progress - 0.5) / 0.5;
196
+ const le = 1 - Math.pow(1 - lt, 3);
197
+ offsetFrac = le;
198
+ }
199
+ }
200
+ if (params.reverse)
201
+ offsetFrac = 1 - offsetFrac;
202
+ for (const s of states)
203
+ s.el.style.strokeDashoffset = String(s.length * offsetFrac);
204
+ if (progress < 1) {
205
+ raf = requestAnimationFrame(drawStep);
206
+ }
207
+ else if (params.loop) {
208
+ start = null;
209
+ raf = requestAnimationFrame(drawStep);
210
+ }
211
+ else {
212
+ raf = 0;
213
+ }
214
+ }
215
+ raf = requestAnimationFrame(drawStep);
216
+ }
217
+ // Defer to a microtask so injected (@html) SVG content is in the DOM.
218
+ queueMicrotask(run);
219
+ // Register this action so the video-export pipeline can restart it after
220
+ // activating the virtual clock. Without this hook, RAFs scheduled before
221
+ // __startVirtualClock() never fire under the virtual system and the
222
+ // animation appears frozen or drifts.
223
+ if (typeof window !== 'undefined') {
224
+ const reg = (window.__svgAnimRestart ||= []);
225
+ reg.push(run);
226
+ // Stash the restart fn on the destroy path.
227
+ node.__svgAnimRestart = run;
228
+ }
229
+ return {
230
+ update(p) {
231
+ params = p;
232
+ queueMicrotask(run);
233
+ },
234
+ destroy() {
235
+ reset();
236
+ if (typeof window !== 'undefined') {
237
+ const reg = window.__svgAnimRestart;
238
+ const fn = node.__svgAnimRestart;
239
+ if (reg && fn) {
240
+ const idx = reg.indexOf(fn);
241
+ if (idx >= 0)
242
+ reg.splice(idx, 1);
243
+ }
244
+ }
245
+ }
246
+ };
247
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "animot-presenter",
3
- "version": "0.2.8",
3
+ "version": "0.5.4",
4
4
  "description": "Embed animated presentations anywhere. Works with vanilla JS, React, Vue, Angular, Svelte, and any frontend framework. Morphing animations, code highlighting, charts, particles, and more.",
5
5
  "type": "module",
6
6
  "svelte": "./dist/index.js",