animot-presenter 0.5.23 → 0.5.24
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 +119 -41
- package/dist/cdn/animot-presenter.esm.js +4607 -4530
- package/dist/cdn/animot-presenter.min.js +8 -8
- package/package.json +1 -1
|
@@ -29,6 +29,42 @@
|
|
|
29
29
|
|
|
30
30
|
type TweenValue = ReturnType<typeof tween<number>>;
|
|
31
31
|
|
|
32
|
+
// Svelte's underlying Tween.set() leaks: when retargeted before completion,
|
|
33
|
+
// the previous rAF task is aborted but its promise is NEVER fulfilled
|
|
34
|
+
// (see svelte/src/internal/client/loop.js — abort() only deletes the task,
|
|
35
|
+
// doesn't call fulfill). The presenter calls .to() on 15+ tween properties
|
|
36
|
+
// per element per slide transition, so a deck running in a loop accumulates
|
|
37
|
+
// hundreds of thousands of orphaned promises plus the async-function
|
|
38
|
+
// Contexts of any awaiter that's stuck on them. wrapTween() patches each
|
|
39
|
+
// tween instance so:
|
|
40
|
+
// 1) .to() on a value the tween is already at is a no-op (Promise.resolve)
|
|
41
|
+
// 2) Each .to() resolves the previous wrapper promise immediately on
|
|
42
|
+
// retarget, so any await chain unwinds and releases its Context even
|
|
43
|
+
// if the underlying Svelte promise is left dangling.
|
|
44
|
+
function wrapTween<TV extends { current: unknown; to: (v: never, o?: unknown) => Promise<void> }>(tv: TV): TV {
|
|
45
|
+
const origTo = tv.to.bind(tv);
|
|
46
|
+
let prevResolve: (() => void) | null = null;
|
|
47
|
+
(tv as unknown as { to: (v: unknown, o?: unknown) => Promise<void> }).to = (value, options) => {
|
|
48
|
+
if (tv.current === value) {
|
|
49
|
+
if (prevResolve) { const r = prevResolve; prevResolve = null; r(); }
|
|
50
|
+
return Promise.resolve();
|
|
51
|
+
}
|
|
52
|
+
if (prevResolve) { const r = prevResolve; prevResolve = null; r(); }
|
|
53
|
+
const inner = origTo(value as never, options);
|
|
54
|
+
return new Promise<void>((resolve) => {
|
|
55
|
+
prevResolve = resolve;
|
|
56
|
+
const done = () => {
|
|
57
|
+
if (prevResolve === resolve) { prevResolve = null; resolve(); }
|
|
58
|
+
};
|
|
59
|
+
inner.then(done, done);
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
return tv;
|
|
63
|
+
}
|
|
64
|
+
function mkTween<T>(value: T, options?: Parameters<typeof tween<T>>[1]): ReturnType<typeof tween<T>> {
|
|
65
|
+
return wrapTween(tween(value, options)) as ReturnType<typeof tween<T>>;
|
|
66
|
+
}
|
|
67
|
+
|
|
32
68
|
interface AnimatedElement {
|
|
33
69
|
x: TweenValue; y: TweenValue; width: TweenValue; height: TweenValue;
|
|
34
70
|
rotation: TweenValue; skewX: TweenValue; skewY: TweenValue;
|
|
@@ -49,6 +85,33 @@
|
|
|
49
85
|
|
|
50
86
|
interface ShapeMorphState { fromType: string; toType: string; }
|
|
51
87
|
|
|
88
|
+
// Race a promise against an AbortSignal so awaits unwind the instant a
|
|
89
|
+
// loop is cancelled — otherwise tween.to() / setTimeout promises keep
|
|
90
|
+
// pending and pin their async-function Context to the heap. Long-running
|
|
91
|
+
// loops without this leak millions of closure contexts (see commit notes).
|
|
92
|
+
function abortable<T>(p: Promise<T>, signal: AbortSignal): Promise<T> {
|
|
93
|
+
if (signal.aborted) return Promise.reject(new DOMException('aborted', 'AbortError'));
|
|
94
|
+
return new Promise<T>((resolve, reject) => {
|
|
95
|
+
const onAbort = () => reject(new DOMException('aborted', 'AbortError'));
|
|
96
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
97
|
+
p.then(
|
|
98
|
+
(v) => { signal.removeEventListener('abort', onAbort); resolve(v); },
|
|
99
|
+
(e) => { signal.removeEventListener('abort', onAbort); reject(e); }
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
function abortableSleep(ms: number, signal: AbortSignal): Promise<void> {
|
|
104
|
+
if (signal.aborted) return Promise.reject(new DOMException('aborted', 'AbortError'));
|
|
105
|
+
return new Promise<void>((resolve, reject) => {
|
|
106
|
+
const id = setTimeout(() => { signal.removeEventListener('abort', onAbort); resolve(); }, ms);
|
|
107
|
+
const onAbort = () => { clearTimeout(id); reject(new DOMException('aborted', 'AbortError')); };
|
|
108
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
function isAbortError(e: unknown): boolean {
|
|
112
|
+
return !!(e && typeof e === 'object' && (e as { name?: string }).name === 'AbortError');
|
|
113
|
+
}
|
|
114
|
+
|
|
52
115
|
// Active motion path loop cancellation tokens
|
|
53
116
|
let motionPathLoopAbort: AbortController | null = null;
|
|
54
117
|
function cancelMotionPathLoops() {
|
|
@@ -60,12 +123,20 @@
|
|
|
60
123
|
// tween-driven render. This implementation only retargets existing tweens
|
|
61
124
|
// via setTimeout; nothing here is reactive, nothing is read from render.
|
|
62
125
|
let keyframeLoopAbort: AbortController | null = null;
|
|
126
|
+
// Scheduled-but-not-yet-fired keyframe timeouts. We track these so cancel
|
|
127
|
+
// also clears them — otherwise their closures (which capture `signal`,
|
|
128
|
+
// `animated`, etc.) survive until natural firing time and pin a Context.
|
|
129
|
+
let keyframeTimeouts: ReturnType<typeof setTimeout>[] = [];
|
|
63
130
|
// Per-element overrides for keyframe-driven props that aren't tweened
|
|
64
131
|
// (backgroundColor, text color). The schedule writes these at each
|
|
65
132
|
// keyframe boundary; liveProps reads them at render time.
|
|
66
133
|
let keyframeOverrides = $state<Map<string, Record<string, any>>>(new Map());
|
|
67
134
|
function cancelKeyframeLoops() {
|
|
68
135
|
if (keyframeLoopAbort) { keyframeLoopAbort.abort(); keyframeLoopAbort = null; }
|
|
136
|
+
if (keyframeTimeouts.length) {
|
|
137
|
+
for (const id of keyframeTimeouts) clearTimeout(id);
|
|
138
|
+
keyframeTimeouts = [];
|
|
139
|
+
}
|
|
69
140
|
}
|
|
70
141
|
function setKeyframeOverride(elementId: string, prop: string, value: any) {
|
|
71
142
|
const cur = keyframeOverrides.get(elementId) ?? {};
|
|
@@ -115,7 +186,7 @@
|
|
|
115
186
|
if (first.backgroundColor !== undefined) setKeyframeOverride(element.id, 'backgroundColor', first.backgroundColor);
|
|
116
187
|
if (first.color !== undefined) setKeyframeOverride(element.id, 'color', first.color);
|
|
117
188
|
for (const kf of sorted.slice(1)) {
|
|
118
|
-
setTimeout(() => {
|
|
189
|
+
const tid = setTimeout(() => {
|
|
119
190
|
if (signal.aborted) return;
|
|
120
191
|
const easing = easingForTween(kf.easing);
|
|
121
192
|
const idx = sorted.indexOf(kf);
|
|
@@ -142,6 +213,7 @@
|
|
|
142
213
|
if (kf.backgroundColor !== undefined) setKeyframeOverride(element.id, 'backgroundColor', kf.backgroundColor);
|
|
143
214
|
if (kf.color !== undefined) setKeyframeOverride(element.id, 'color', kf.color);
|
|
144
215
|
}, Math.max(0, kf.time));
|
|
216
|
+
keyframeTimeouts.push(tid);
|
|
145
217
|
}
|
|
146
218
|
}
|
|
147
219
|
}
|
|
@@ -570,29 +642,29 @@
|
|
|
570
642
|
const seedGray = k0?.grayscale !== undefined ? k0.grayscale : (element.grayscale ?? 0);
|
|
571
643
|
|
|
572
644
|
animatedElements.set(element.id, {
|
|
573
|
-
x:
|
|
574
|
-
y:
|
|
575
|
-
width:
|
|
576
|
-
height:
|
|
577
|
-
rotation:
|
|
578
|
-
skewX:
|
|
579
|
-
skewY:
|
|
580
|
-
tiltX:
|
|
581
|
-
tiltY:
|
|
582
|
-
perspective:
|
|
583
|
-
opacity:
|
|
584
|
-
borderRadius:
|
|
585
|
-
fontSize: textEl ?
|
|
586
|
-
fillColor: shapeEl ?
|
|
587
|
-
strokeColor: shapeEl ?
|
|
588
|
-
strokeWidth: shapeEl ?
|
|
589
|
-
shapeMorph: shapeEl ?
|
|
590
|
-
motionPathProgress: element.motionPathConfig ?
|
|
591
|
-
blur:
|
|
592
|
-
brightness:
|
|
593
|
-
contrast:
|
|
594
|
-
saturate:
|
|
595
|
-
grayscale:
|
|
645
|
+
x: mkTween(seedX, { duration: 500 }),
|
|
646
|
+
y: mkTween(seedY, { duration: 500 }),
|
|
647
|
+
width: mkTween(seedW, { duration: 500 }),
|
|
648
|
+
height: mkTween(seedH, { duration: 500 }),
|
|
649
|
+
rotation: mkTween(seedRot, { duration: 500 }),
|
|
650
|
+
skewX: mkTween(seedSkewX, { duration: 500 }),
|
|
651
|
+
skewY: mkTween(seedSkewY, { duration: 500 }),
|
|
652
|
+
tiltX: mkTween(seedTiltX, { duration: 500 }),
|
|
653
|
+
tiltY: mkTween(seedTiltY, { duration: 500 }),
|
|
654
|
+
perspective: mkTween(element.perspective ?? 1000, { duration: 500 }),
|
|
655
|
+
opacity: mkTween(startOpacity, { duration: 300 }),
|
|
656
|
+
borderRadius: mkTween(seedBR, { duration: 500 }),
|
|
657
|
+
fontSize: textEl ? mkTween(seedFontSize, { duration: 500 }) : null,
|
|
658
|
+
fillColor: shapeEl ? mkTween(seedFill, { duration: 500 }) : null,
|
|
659
|
+
strokeColor: shapeEl ? mkTween(seedStroke, { duration: 500 }) : null,
|
|
660
|
+
strokeWidth: shapeEl ? mkTween(seedStrokeW, { duration: 500 }) : null,
|
|
661
|
+
shapeMorph: shapeEl ? mkTween(1, { duration: 500 }) : null,
|
|
662
|
+
motionPathProgress: element.motionPathConfig ? mkTween(0, { duration: 500 }) : null,
|
|
663
|
+
blur: mkTween(seedBlur, { duration: 500 }),
|
|
664
|
+
brightness: mkTween(seedBright, { duration: 500 }),
|
|
665
|
+
contrast: mkTween(seedContrast, { duration: 500 }),
|
|
666
|
+
saturate: mkTween(seedSat, { duration: 500 }),
|
|
667
|
+
grayscale: mkTween(seedGray, { duration: 500 })
|
|
596
668
|
});
|
|
597
669
|
const currentSlideEl = getElementInSlide(currentSlide, element.id);
|
|
598
670
|
elementContent.set(element.id, JSON.parse(JSON.stringify(currentSlideEl || element)));
|
|
@@ -631,16 +703,20 @@
|
|
|
631
703
|
if (shouldLoop) {
|
|
632
704
|
const laps = element.motionPathConfig.laps ?? 0;
|
|
633
705
|
(async () => {
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
706
|
+
try {
|
|
707
|
+
let lap = 0;
|
|
708
|
+
while (!signal.aborted && (laps === 0 || lap < laps)) {
|
|
709
|
+
await abortable(animated.motionPathProgress!.to(0, { duration: 0 }), signal);
|
|
710
|
+
await abortable(animated.motionPathProgress!.to(1, { duration, easing }), signal);
|
|
711
|
+
lap++;
|
|
712
|
+
if (!signal.aborted && (laps === 0 || lap < laps)) await abortableSleep(50, signal);
|
|
713
|
+
}
|
|
714
|
+
} catch (e) {
|
|
715
|
+
if (!isAbortError(e)) throw e;
|
|
640
716
|
}
|
|
641
717
|
})();
|
|
642
718
|
} else {
|
|
643
|
-
animated.motionPathProgress.to(1, { duration, easing });
|
|
719
|
+
animated.motionPathProgress.to(1, { duration, easing }).catch(() => {});
|
|
644
720
|
}
|
|
645
721
|
}
|
|
646
722
|
}
|
|
@@ -1593,15 +1669,15 @@
|
|
|
1593
1669
|
{@const morphProgress = animated.shapeMorph?.current ?? 1}
|
|
1594
1670
|
{@const effectiveShapeType = mState ? (morphProgress >= 1 ? mState.toType : (morphProgress <= 0 ? mState.fromType : null)) : shapeEl.shapeType}
|
|
1595
1671
|
{@const isMorphing = mState && morphProgress > 0 && morphProgress < 1}
|
|
1596
|
-
<svg class="animot-shape-element" viewBox="0 0 {animated.width.current} {animated.height.current}" fill-opacity={shapeEl.fillOpacity ?? 1} stroke-opacity={shapeEl.strokeOpacity ?? 1} style:filter={shapeEl.boxShadow?.enabled ? `drop-shadow(${shapeEl.boxShadow.offsetX}px ${shapeEl.boxShadow.offsetY}px ${shapeEl.boxShadow.blur}px ${shapeEl.boxShadow.color})` : 'none'}>
|
|
1672
|
+
<svg class="animot-shape-element" viewBox="0 0 {Math.max(0, animated.width.current)} {Math.max(0, animated.height.current)}" fill-opacity={shapeEl.fillOpacity ?? 1} stroke-opacity={shapeEl.strokeOpacity ?? 1} style:filter={shapeEl.boxShadow?.enabled ? `drop-shadow(${shapeEl.boxShadow.offsetX}px ${shapeEl.boxShadow.offsetY}px ${shapeEl.boxShadow.blur}px ${shapeEl.boxShadow.color})` : 'none'}>
|
|
1597
1673
|
{#if isMorphing}
|
|
1598
|
-
{@const w = animated.width.current}
|
|
1599
|
-
{@const h = animated.height.current}
|
|
1674
|
+
{@const w = Math.max(0, animated.width.current)}
|
|
1675
|
+
{@const h = Math.max(0, animated.height.current)}
|
|
1600
1676
|
{@const sw = animStrokeWidth}
|
|
1601
1677
|
<g style:opacity={1 - morphProgress}>{@html renderShape(mState!.fromType, w, h, animated.borderRadius.current, animFill, animStroke, sw, shapeEl.strokeStyle, shapeEl.strokeDashGap)}</g>
|
|
1602
1678
|
<g style:opacity={morphProgress}>{@html renderShape(mState!.toType, w, h, animated.borderRadius.current, animFill, animStroke, sw, shapeEl.strokeStyle, shapeEl.strokeDashGap)}</g>
|
|
1603
1679
|
{:else}
|
|
1604
|
-
{@html renderShape(effectiveShapeType ?? shapeEl.shapeType, animated.width.current, animated.height.current, animated.borderRadius.current, animFill, animStroke, animStrokeWidth, shapeEl.strokeStyle, shapeEl.strokeDashGap)}
|
|
1680
|
+
{@html renderShape(effectiveShapeType ?? shapeEl.shapeType, Math.max(0, animated.width.current), Math.max(0, animated.height.current), animated.borderRadius.current, animFill, animStroke, animStrokeWidth, shapeEl.strokeStyle, shapeEl.strokeDashGap)}
|
|
1605
1681
|
{/if}
|
|
1606
1682
|
</svg>
|
|
1607
1683
|
{:else if element.type === 'counter'}
|
|
@@ -1650,7 +1726,7 @@
|
|
|
1650
1726
|
{:else if element.type === 'motionPath'}
|
|
1651
1727
|
{@const mpEl = element as MotionPathElement}
|
|
1652
1728
|
{#if mpEl.showInPresentation}
|
|
1653
|
-
<svg width="100%" height="100%" viewBox="0 0 {animated.width.current} {animated.height.current}" style="position:absolute;top:0;left:0;pointer-events:none;overflow:visible;">
|
|
1729
|
+
<svg width="100%" height="100%" viewBox="0 0 {Math.max(0, animated.width.current)} {Math.max(0, animated.height.current)}" style="position:absolute;top:0;left:0;pointer-events:none;overflow:visible;">
|
|
1654
1730
|
<path d={buildPresenterPathD(mpEl.points, mpEl.closed)} stroke={mpEl.pathColor} stroke-width={mpEl.pathWidth} fill="none" stroke-dasharray="8 4" />
|
|
1655
1731
|
</svg>
|
|
1656
1732
|
{/if}
|
|
@@ -1719,6 +1795,8 @@
|
|
|
1719
1795
|
}
|
|
1720
1796
|
|
|
1721
1797
|
function renderShape(type: string, w: number, h: number, br: number, fill: string, stroke: string, sw: number, strokeStyle?: string, strokeDashGap?: number): string {
|
|
1798
|
+
const nn = (v: number) => (v > 0 ? v : 0);
|
|
1799
|
+
w = nn(w); h = nn(h); br = nn(br); sw = nn(sw);
|
|
1722
1800
|
let dashAttr = '';
|
|
1723
1801
|
if (strokeStyle && strokeStyle !== 'solid') {
|
|
1724
1802
|
const s = sw || 1;
|
|
@@ -1732,17 +1810,17 @@
|
|
|
1732
1810
|
return `<polygon points="${pts}" fill="${fill}" stroke="${stroke}" stroke-width="${sw}"${dashAttr} stroke-linejoin="round"/>`;
|
|
1733
1811
|
};
|
|
1734
1812
|
switch (type) {
|
|
1735
|
-
case 'rectangle': return `<rect x="${sw/2}" y="${sw/2}" width="${w-sw}" height="${h-sw}" rx="${br}" ry="${br}" fill="${fill}" stroke="${stroke}" stroke-width="${sw}"${dashAttr}/>`;
|
|
1736
|
-
case 'circle': return `<circle cx="${w/2}" cy="${h/2}" r="${Math.min(w,h)/2-sw/2}" fill="${fill}" stroke="${stroke}" stroke-width="${sw}"${dashAttr}/>`;
|
|
1737
|
-
case 'ellipse': return `<ellipse cx="${w/2}" cy="${h/2}" rx="${w/2-sw/2}" ry="${h/2-sw/2}" fill="${fill}" stroke="${stroke}" stroke-width="${sw}"${dashAttr}/>`;
|
|
1813
|
+
case 'rectangle': return `<rect x="${sw/2}" y="${sw/2}" width="${nn(w-sw)}" height="${nn(h-sw)}" rx="${nn(Math.min(br, (w-sw)/2, (h-sw)/2))}" ry="${nn(Math.min(br, (w-sw)/2, (h-sw)/2))}" fill="${fill}" stroke="${stroke}" stroke-width="${sw}"${dashAttr}/>`;
|
|
1814
|
+
case 'circle': return `<circle cx="${w/2}" cy="${h/2}" r="${nn(Math.min(w,h)/2-sw/2)}" fill="${fill}" stroke="${stroke}" stroke-width="${sw}"${dashAttr}/>`;
|
|
1815
|
+
case 'ellipse': return `<ellipse cx="${w/2}" cy="${h/2}" rx="${nn(w/2-sw/2)}" ry="${nn(h/2-sw/2)}" fill="${fill}" stroke="${stroke}" stroke-width="${sw}"${dashAttr}/>`;
|
|
1738
1816
|
case 'triangle': return polyOrPath(`${w/2},${sw/2} ${sw/2},${h-sw/2} ${w-sw/2},${h-sw/2}`);
|
|
1739
1817
|
case 'star': {
|
|
1740
|
-
const cx = w/2, cy = h/2, outerR = Math.min(w,h)/2-sw/2, innerR = outerR*0.4;
|
|
1818
|
+
const cx = w/2, cy = h/2, outerR = nn(Math.min(w,h)/2-sw/2), innerR = outerR*0.4;
|
|
1741
1819
|
const pts = Array.from({length:10},(_,i)=>{const a=(i*Math.PI/5)-Math.PI/2;const r=i%2===0?outerR:innerR;return`${cx+r*Math.cos(a)},${cy+r*Math.sin(a)}`;}).join(' ');
|
|
1742
1820
|
return polyOrPath(pts);
|
|
1743
1821
|
}
|
|
1744
1822
|
case 'hexagon': {
|
|
1745
|
-
const cx = w/2, cy = h/2, r = Math.min(w,h)/2-sw/2;
|
|
1823
|
+
const cx = w/2, cy = h/2, r = nn(Math.min(w,h)/2-sw/2);
|
|
1746
1824
|
const pts = Array.from({length:6},(_,i)=>{const a=(i*Math.PI/3)-Math.PI/2;return`${cx+r*Math.cos(a)},${cy+r*Math.sin(a)}`;}).join(' ');
|
|
1747
1825
|
return polyOrPath(pts);
|
|
1748
1826
|
}
|