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.
- package/dist/AnimotPresenter.svelte +1654 -1529
- package/dist/FlowMarkers.svelte +111 -0
- package/dist/FlowMarkers.svelte.d.ts +10 -0
- package/dist/cdn/animot-presenter.css +1 -1
- package/dist/cdn/animot-presenter.esm.js +4998 -4620
- package/dist/cdn/animot-presenter.min.js +9 -9
- package/dist/renderers/IconRenderer.svelte +19 -1
- package/dist/styles/presenter.css +53 -0
- package/dist/types.d.ts +29 -1
- package/dist/utils/arrow-clip-draw.d.ts +28 -0
- package/dist/utils/arrow-clip-draw.js +177 -0
- package/dist/utils/arrow-path.d.ts +15 -0
- package/dist/utils/arrow-path.js +73 -0
- package/dist/utils/trace-svg-paths.d.ts +28 -0
- package/dist/utils/trace-svg-paths.js +247 -0
- package/package.json +1 -1
|
@@ -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.
|
|
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",
|