animot-presenter 0.2.9 → 0.5.5

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.
@@ -1,5 +1,7 @@
1
1
  <script lang="ts">
2
2
  import type { IconElement } from '../types';
3
+ import { traceSvgPaths } from '../utils/trace-svg-paths';
4
+
3
5
  interface Props { element: IconElement; }
4
6
  let { element }: Props = $props();
5
7
 
@@ -8,9 +10,25 @@
8
10
  const stroke = element.fillMode === 'stroke' || element.fillMode === 'both' ? element.color : 'none';
9
11
  return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="${fill}" stroke="${stroke}" stroke-width="${element.strokeWidth}" stroke-linecap="round" stroke-linejoin="round">${element.svgContent}</svg>`;
10
12
  });
13
+
14
+ const animMode = $derived(element.animation?.mode ?? 'none');
15
+ const animDur = $derived(element.animation?.duration ?? 800);
16
+ const animLoop = $derived(element.animation?.loop ?? false);
17
+ const animReverse = $derived(element.animation?.direction === 'reverse');
18
+ const animKey = $derived(`${element.iconName}-${element.iconLibrary}-${animMode}-${animDur}-${animLoop}-${animReverse}`);
11
19
  </script>
12
20
 
13
- <div class="icon-element">{@html svgMarkup()}</div>
21
+ <div
22
+ class="icon-element"
23
+ use:traceSvgPaths={{
24
+ enabled: animMode !== 'none',
25
+ mode: animMode as 'none' | 'draw' | 'undraw' | 'draw-undraw' | 'flow',
26
+ duration: animDur,
27
+ loop: animLoop,
28
+ reverse: animReverse,
29
+ key: animKey
30
+ }}
31
+ >{@html svgMarkup()}</div>
14
32
 
15
33
  <style>
16
34
  .icon-element { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; }
@@ -46,3 +46,56 @@
46
46
  83% { translate: calc(-0.2 * var(--float-amp, 10px)) calc(0.8 * var(--float-amp, 10px)); }
47
47
  100% { translate: 0 0; }
48
48
  }
49
+
50
+ /* SVG path-trace animations (Icon + Svg elements) */
51
+ .animot-svg-element.icon-anim-draw path,
52
+ .animot-svg-element.icon-anim-draw circle,
53
+ .animot-svg-element.icon-anim-draw rect,
54
+ .animot-svg-element.icon-anim-draw line,
55
+ .animot-svg-element.icon-anim-draw polyline,
56
+ .animot-svg-element.icon-anim-draw polygon,
57
+ .animot-svg-element.icon-anim-draw ellipse {
58
+ stroke-dashoffset: var(--path-len, 1000);
59
+ animation: animot-svg-draw var(--icon-anim-duration, 800ms) ease-out forwards;
60
+ }
61
+ .animot-svg-element.icon-anim-undraw path,
62
+ .animot-svg-element.icon-anim-undraw circle,
63
+ .animot-svg-element.icon-anim-undraw rect,
64
+ .animot-svg-element.icon-anim-undraw line,
65
+ .animot-svg-element.icon-anim-undraw polyline,
66
+ .animot-svg-element.icon-anim-undraw polygon,
67
+ .animot-svg-element.icon-anim-undraw ellipse {
68
+ stroke-dashoffset: 0;
69
+ animation: animot-svg-undraw var(--icon-anim-duration, 800ms) ease-out forwards;
70
+ }
71
+ .animot-svg-element.icon-anim-draw-undraw path,
72
+ .animot-svg-element.icon-anim-draw-undraw circle,
73
+ .animot-svg-element.icon-anim-draw-undraw rect,
74
+ .animot-svg-element.icon-anim-draw-undraw line,
75
+ .animot-svg-element.icon-anim-draw-undraw polyline,
76
+ .animot-svg-element.icon-anim-draw-undraw polygon,
77
+ .animot-svg-element.icon-anim-draw-undraw ellipse {
78
+ stroke-dashoffset: var(--path-len, 1000);
79
+ animation: animot-svg-draw-undraw var(--icon-anim-duration, 800ms) ease-out forwards;
80
+ }
81
+ .animot-svg-element.icon-anim-loop path,
82
+ .animot-svg-element.icon-anim-loop circle,
83
+ .animot-svg-element.icon-anim-loop rect,
84
+ .animot-svg-element.icon-anim-loop line,
85
+ .animot-svg-element.icon-anim-loop polyline,
86
+ .animot-svg-element.icon-anim-loop polygon,
87
+ .animot-svg-element.icon-anim-loop ellipse { animation-iteration-count: infinite !important; }
88
+ .animot-svg-element.icon-anim-reverse path,
89
+ .animot-svg-element.icon-anim-reverse circle,
90
+ .animot-svg-element.icon-anim-reverse rect,
91
+ .animot-svg-element.icon-anim-reverse line,
92
+ .animot-svg-element.icon-anim-reverse polyline,
93
+ .animot-svg-element.icon-anim-reverse polygon,
94
+ .animot-svg-element.icon-anim-reverse ellipse { animation-direction: reverse !important; }
95
+ @keyframes animot-svg-draw { to { stroke-dashoffset: 0; } }
96
+ @keyframes animot-svg-undraw { from { stroke-dashoffset: 0; } to { stroke-dashoffset: var(--path-len, 1000); } }
97
+ @keyframes animot-svg-draw-undraw {
98
+ 0% { stroke-dashoffset: var(--path-len, 1000); }
99
+ 50% { stroke-dashoffset: 0; }
100
+ 100% { stroke-dashoffset: var(--path-len, 1000); }
101
+ }
package/dist/types.d.ts CHANGED
@@ -145,10 +145,30 @@ export interface TextElement extends BaseElement {
145
145
  backgroundPositionY?: number;
146
146
  backgroundScale?: number;
147
147
  }
148
- export type ArrowAnimationMode = 'none' | 'grow' | 'draw' | 'undraw' | 'draw-undraw';
148
+ export type ArrowAnimationMode = 'none' | 'grow' | 'draw' | 'undraw' | 'draw-undraw' | 'flow';
149
149
  export interface ArrowAnimationConfig {
150
150
  mode: ArrowAnimationMode;
151
151
  duration: number;
152
+ loop?: boolean;
153
+ direction?: 'forward' | 'reverse';
154
+ }
155
+ export type SvgPathAnimationMode = 'none' | 'draw' | 'undraw' | 'draw-undraw' | 'flow';
156
+ export interface SvgPathAnimationConfig {
157
+ mode: SvgPathAnimationMode;
158
+ duration: number;
159
+ loop?: boolean;
160
+ direction?: 'forward' | 'reverse';
161
+ }
162
+ export interface FlowMarkerConfig {
163
+ enabled: boolean;
164
+ count: number;
165
+ size: number;
166
+ color: string;
167
+ speedMs: number;
168
+ direction: 'forward' | 'reverse' | 'bidirectional';
169
+ pattern: 'loop' | 'once' | 'bounce';
170
+ shape: 'dot' | 'square' | 'pulse';
171
+ easing: 'linear' | 'ease-in-out';
152
172
  }
153
173
  export interface ArrowElement extends BaseElement {
154
174
  type: 'arrow';
@@ -162,6 +182,7 @@ export interface ArrowElement extends BaseElement {
162
182
  showHead: boolean;
163
183
  opacity: number;
164
184
  animation?: ArrowAnimationConfig;
185
+ flowMarkers?: FlowMarkerConfig;
165
186
  }
166
187
  export interface ImageElement extends BaseElement {
167
188
  type: 'image';
@@ -182,7 +203,9 @@ export interface ShapeElement extends BaseElement {
182
203
  type: 'shape';
183
204
  shapeType: ShapeType;
184
205
  fillColor: string;
206
+ fillOpacity?: number;
185
207
  strokeColor: string;
208
+ strokeOpacity?: number;
186
209
  strokeWidth: number;
187
210
  strokeStyle?: StrokeStyle;
188
211
  strokeDashGap?: number;
@@ -248,6 +271,7 @@ export interface IconElement extends BaseElement {
248
271
  color: string;
249
272
  strokeWidth: number;
250
273
  fillMode: 'stroke' | 'fill' | 'both';
274
+ animation?: SvgPathAnimationConfig;
251
275
  }
252
276
  export interface SvgElement extends BaseElement {
253
277
  type: 'svg';
@@ -256,6 +280,7 @@ export interface SvgElement extends BaseElement {
256
280
  opacity: number;
257
281
  preserveAspectRatio: string;
258
282
  viewBox?: string;
283
+ animation?: SvgPathAnimationConfig;
259
284
  }
260
285
  export interface MotionPathElement extends BaseElement {
261
286
  type: 'motionPath';
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Svelte action for arrow animations. Two strategies depending on mode:
3
+ *
4
+ * • 'draw' / 'undraw' / 'draw-undraw' — clip-path inset on the host <svg>.
5
+ * Doesn't touch stroke-dasharray, so dashed/dotted patterns are preserved
6
+ * while the path is progressively revealed/hidden from one side.
7
+ *
8
+ * • 'flow' — marching ants. Continuously shifts stroke-dashoffset on the
9
+ * inner .arrow-path, so the dash pattern appears to flow along the path
10
+ * like a current. Forward/reverse controls flow direction. Inherently
11
+ * looping; the loop flag is ignored.
12
+ */
13
+ export interface ArrowClipDrawParams {
14
+ enabled: boolean;
15
+ mode: 'draw' | 'undraw' | 'draw-undraw' | 'flow' | 'none' | string;
16
+ duration: number;
17
+ startX: number;
18
+ startY: number;
19
+ endX: number;
20
+ endY: number;
21
+ loop?: boolean;
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;
27
+ key?: unknown;
28
+ }
29
+ export declare function arrowClipDraw(node: SVGSVGElement, params: ArrowClipDrawParams): {
30
+ update(p: ArrowClipDrawParams): void;
31
+ destroy(): void;
32
+ };
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Svelte action for arrow animations. Two strategies depending on mode:
3
+ *
4
+ * • 'draw' / 'undraw' / 'draw-undraw' — clip-path inset on the host <svg>.
5
+ * Doesn't touch stroke-dasharray, so dashed/dotted patterns are preserved
6
+ * while the path is progressively revealed/hidden from one side.
7
+ *
8
+ * • 'flow' — marching ants. Continuously shifts stroke-dashoffset on the
9
+ * inner .arrow-path, so the dash pattern appears to flow along the path
10
+ * like a current. Forward/reverse controls flow direction. Inherently
11
+ * looping; the loop flag is ignored.
12
+ */
13
+ export function arrowClipDraw(node, params) {
14
+ let raf = 0;
15
+ let pathEl = null;
16
+ let baseDash = '';
17
+ let baseOffset = '';
18
+ function clearArrowPath() {
19
+ if (!pathEl)
20
+ return;
21
+ pathEl.style.strokeDasharray = baseDash;
22
+ pathEl.style.strokeDashoffset = baseOffset;
23
+ }
24
+ function reset() {
25
+ if (raf)
26
+ cancelAnimationFrame(raf);
27
+ raf = 0;
28
+ node.style.clipPath = '';
29
+ clearArrowPath();
30
+ }
31
+ function run() {
32
+ reset();
33
+ if (!params.enabled || params.mode === 'none')
34
+ return;
35
+ // Capture the inner arrow path (used by 'flow' mode).
36
+ pathEl = node.querySelector('.arrow-path');
37
+ if (pathEl) {
38
+ baseDash = pathEl.style.strokeDasharray || '';
39
+ baseOffset = pathEl.style.strokeDashoffset || '';
40
+ }
41
+ // Pick the dominant axis so vertical arrows reveal top↔bottom and horizontal
42
+ // arrows reveal left↔right. clip-path inset(top right bottom left).
43
+ const dx = params.endX - params.startX;
44
+ const dy = params.endY - params.startY;
45
+ const horizontal = Math.abs(dx) >= Math.abs(dy);
46
+ const positive = horizontal ? dx >= 0 : dy >= 0;
47
+ const goesPositive = params.reverse ? !positive : positive;
48
+ // Build the clip-path string for a given progress (0 = fully hidden,
49
+ // 100 = fully visible) on the dominant axis.
50
+ function clip(insetPct) {
51
+ if (horizontal) {
52
+ return goesPositive
53
+ ? `inset(0 ${insetPct}% 0 0)` // hide from right edge
54
+ : `inset(0 0 0 ${insetPct}%)`; // hide from left edge
55
+ }
56
+ return goesPositive
57
+ ? `inset(0 0 ${insetPct}% 0)` // hide from bottom
58
+ : `inset(${insetPct}% 0 0 0)`; // hide from top
59
+ }
60
+ const clipFull = clip(100); // fully hidden (100% inset on hide side)
61
+ const clipNone = clip(0);
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;
69
+ const start = performance.now();
70
+ const m = params.mode;
71
+ // FLOW: marching ants — animate dashoffset continuously, keep base pattern.
72
+ if (m === 'flow') {
73
+ if (!pathEl)
74
+ return;
75
+ // If the arrow has no inline dasharray (i.e. it's solid), give it one
76
+ // so dashes are visible to flow. Otherwise keep the user's pattern.
77
+ const dashAttr = pathEl.getAttribute('stroke-dasharray');
78
+ if (!dashAttr || dashAttr === 'none') {
79
+ pathEl.style.strokeDasharray = '8 5';
80
+ }
81
+ // One pixel of dashoffset shift per ms feels like a steady current; use
82
+ // `duration` as the ms per cycle of 24px (one base dash repeat-ish).
83
+ const cycle = 24;
84
+ const dir = params.reverse ? 1 : -1;
85
+ function flowStep(now) {
86
+ const elapsed = now - start;
87
+ const offset = (dir * (elapsed / dur) * cycle) % (cycle * 1000); // huge mod just to keep it bounded
88
+ if (pathEl)
89
+ pathEl.style.strokeDashoffset = String(offset);
90
+ raf = requestAnimationFrame(flowStep);
91
+ }
92
+ raf = requestAnimationFrame(flowStep);
93
+ return;
94
+ }
95
+ // Initial clip-path state for draw modes.
96
+ if (m === 'draw' || m === 'draw-undraw') {
97
+ node.style.clipPath = clipFull;
98
+ }
99
+ else if (m === 'undraw') {
100
+ node.style.clipPath = clipNone;
101
+ }
102
+ function step(now) {
103
+ const elapsed = now - start;
104
+ if (m === 'draw') {
105
+ const t = Math.min(elapsed / dur, 1);
106
+ const eased = 1 - Math.pow(1 - t, 3);
107
+ node.style.clipPath = clip(100 * (1 - eased));
108
+ if (t < 1)
109
+ raf = requestAnimationFrame(step);
110
+ else if (params.loop)
111
+ run();
112
+ else {
113
+ node.style.clipPath = clipNone;
114
+ raf = 0;
115
+ }
116
+ }
117
+ else if (m === 'undraw') {
118
+ const t = Math.min(elapsed / dur, 1);
119
+ const eased = 1 - Math.pow(1 - t, 3);
120
+ node.style.clipPath = clip(100 * eased);
121
+ if (t < 1)
122
+ raf = requestAnimationFrame(step);
123
+ else if (params.loop)
124
+ run();
125
+ else {
126
+ node.style.clipPath = clipFull;
127
+ raf = 0;
128
+ }
129
+ }
130
+ else if (m === 'draw-undraw') {
131
+ const half = dur / 2;
132
+ if (elapsed < half) {
133
+ const t = Math.min(elapsed / half, 1);
134
+ const eased = 1 - Math.pow(1 - t, 3);
135
+ node.style.clipPath = clip(100 * (1 - eased));
136
+ raf = requestAnimationFrame(step);
137
+ }
138
+ else {
139
+ const t = Math.min((elapsed - half) / half, 1);
140
+ const eased = 1 - Math.pow(1 - t, 3);
141
+ node.style.clipPath = clip(100 * eased);
142
+ if (t < 1)
143
+ raf = requestAnimationFrame(step);
144
+ else if (params.loop)
145
+ run();
146
+ else {
147
+ node.style.clipPath = clipFull;
148
+ raf = 0;
149
+ }
150
+ }
151
+ }
152
+ }
153
+ raf = requestAnimationFrame(step);
154
+ }
155
+ // Defer to next tick so the inner <path class="arrow-path"> is already
156
+ // rendered when we query it.
157
+ queueMicrotask(run);
158
+ // Register so the video-export pipeline can restart this animation after
159
+ // activating the virtual clock (see initFirstSlideAnimations).
160
+ if (typeof window !== 'undefined') {
161
+ const reg = (window.__svgAnimRestart ||= []);
162
+ reg.push(run);
163
+ node.__svgAnimRestart = run;
164
+ }
165
+ let lastKey = params.key;
166
+ return {
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;
171
+ params = p;
172
+ if (keyChanged) {
173
+ lastKey = p.key;
174
+ queueMicrotask(run);
175
+ }
176
+ },
177
+ destroy() {
178
+ reset();
179
+ if (typeof window !== 'undefined') {
180
+ const reg = window.__svgAnimRestart;
181
+ const fn = node.__svgAnimRestart;
182
+ if (reg && fn) {
183
+ const idx = reg.indexOf(fn);
184
+ if (idx >= 0)
185
+ reg.splice(idx, 1);
186
+ }
187
+ }
188
+ }
189
+ };
190
+ }
@@ -0,0 +1,15 @@
1
+ export interface PathPoint {
2
+ x: number;
3
+ y: number;
4
+ angle: number;
5
+ }
6
+ interface Vec {
7
+ x: number;
8
+ y: number;
9
+ }
10
+ /**
11
+ * Sample a single point along an arrow's path at parameter t in [0, 1].
12
+ * Returns x, y and the tangent angle in radians (useful for orienting markers).
13
+ */
14
+ export declare function arrowPointAt(start: Vec, end: Vec, control: Vec[], t: number): PathPoint;
15
+ export {};
@@ -0,0 +1,73 @@
1
+ // Pure helpers to compute a point (and its tangent direction) along an arrow's
2
+ // path at parameter t in [0, 1]. Mirrors the path-construction rules used by
3
+ // Arrow.svelte (linear, quadratic, cubic, Catmull-Rom for 3+ control points).
4
+ //
5
+ // Used by FlowMarkers and any other consumer that needs to follow the curve.
6
+ function lerp(a, b, t) {
7
+ return { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t };
8
+ }
9
+ function quadratic(p0, p1, p2, t) {
10
+ const a = lerp(p0, p1, t);
11
+ const b = lerp(p1, p2, t);
12
+ return lerp(a, b, t);
13
+ }
14
+ function quadraticTangent(p0, p1, p2, t) {
15
+ // derivative of quadratic Bezier
16
+ return {
17
+ x: 2 * (1 - t) * (p1.x - p0.x) + 2 * t * (p2.x - p1.x),
18
+ y: 2 * (1 - t) * (p1.y - p0.y) + 2 * t * (p2.y - p1.y)
19
+ };
20
+ }
21
+ function cubic(p0, p1, p2, p3, t) {
22
+ const a = lerp(p0, p1, t);
23
+ const b = lerp(p1, p2, t);
24
+ const c = lerp(p2, p3, t);
25
+ const d = lerp(a, b, t);
26
+ const e = lerp(b, c, t);
27
+ return lerp(d, e, t);
28
+ }
29
+ function cubicTangent(p0, p1, p2, p3, t) {
30
+ const u = 1 - t;
31
+ return {
32
+ x: 3 * u * u * (p1.x - p0.x) + 6 * u * t * (p2.x - p1.x) + 3 * t * t * (p3.x - p2.x),
33
+ y: 3 * u * u * (p1.y - p0.y) + 6 * u * t * (p2.y - p1.y) + 3 * t * t * (p3.y - p2.y)
34
+ };
35
+ }
36
+ /**
37
+ * Sample a single point along an arrow's path at parameter t in [0, 1].
38
+ * Returns x, y and the tangent angle in radians (useful for orienting markers).
39
+ */
40
+ export function arrowPointAt(start, end, control, t) {
41
+ t = Math.max(0, Math.min(1, t));
42
+ if (control.length === 0) {
43
+ // straight line
44
+ const p = lerp(start, end, t);
45
+ return { x: p.x, y: p.y, angle: Math.atan2(end.y - start.y, end.x - start.x) };
46
+ }
47
+ if (control.length === 1) {
48
+ const p = quadratic(start, control[0], end, t);
49
+ const tan = quadraticTangent(start, control[0], end, t);
50
+ return { x: p.x, y: p.y, angle: Math.atan2(tan.y, tan.x) };
51
+ }
52
+ if (control.length === 2) {
53
+ const p = cubic(start, control[0], control[1], end, t);
54
+ const tan = cubicTangent(start, control[0], control[1], end, t);
55
+ return { x: p.x, y: p.y, angle: Math.atan2(tan.y, tan.x) };
56
+ }
57
+ // 3+ control points: Catmull-Rom approximated as cubic Bezier segments
58
+ // (matches Arrow.svelte's pathD rendering).
59
+ const pts = [start, ...control, end];
60
+ const segCount = pts.length - 1;
61
+ const segLen = 1 / segCount;
62
+ const segIndex = Math.min(segCount - 1, Math.floor(t / segLen));
63
+ const localT = (t - segIndex * segLen) / segLen;
64
+ const p0 = pts[segIndex === 0 ? 0 : segIndex - 1];
65
+ const p1 = pts[segIndex];
66
+ const p2 = pts[segIndex + 1];
67
+ const p3 = pts[segIndex + 2 < pts.length ? segIndex + 2 : pts.length - 1];
68
+ const c1 = { x: p1.x + (p2.x - p0.x) / 6, y: p1.y + (p2.y - p0.y) / 6 };
69
+ const c2 = { x: p2.x - (p3.x - p1.x) / 6, y: p2.y - (p3.y - p1.y) / 6 };
70
+ const p = cubic(p1, c1, c2, p2, localT);
71
+ const tan = cubicTangent(p1, c1, c2, p2, localT);
72
+ return { x: p.x, y: p.y, angle: Math.atan2(tan.y, tan.x) };
73
+ }
@@ -0,0 +1,28 @@
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
+ export interface SvgPathDrawParams {
18
+ enabled: boolean;
19
+ mode: 'none' | 'draw' | 'undraw' | 'draw-undraw' | 'flow';
20
+ duration: number;
21
+ loop?: boolean;
22
+ reverse?: boolean;
23
+ key?: unknown;
24
+ }
25
+ export declare function traceSvgPaths(node: HTMLElement | SVGElement, params: SvgPathDrawParams): {
26
+ update(p: SvgPathDrawParams): void;
27
+ destroy(): void;
28
+ };