animot-presenter 0.5.15 → 0.5.17
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 +39 -39
- package/dist/cdn/animot-presenter.esm.js +2131 -2134
- package/dist/cdn/animot-presenter.min.js +8 -8
- package/dist/types.d.ts +4 -0
- package/package.json +1 -1
|
@@ -155,7 +155,7 @@
|
|
|
155
155
|
let {
|
|
156
156
|
src, data, autoplay = false, loop = false, controls = true, arrows = false,
|
|
157
157
|
progress: showProgress = true, keyboard = true, duration: durationOverride,
|
|
158
|
-
startSlide = 0, class: className = '', onslidechange, oncomplete
|
|
158
|
+
startSlide = 0, muteNarration = false, class: className = '', onslidechange, oncomplete
|
|
159
159
|
}: AnimotPresenterProps = $props();
|
|
160
160
|
|
|
161
161
|
// State
|
|
@@ -190,13 +190,16 @@
|
|
|
190
190
|
// Single <audio> element for per-slide narration playback. Only used
|
|
191
191
|
// when `project.settings.narrationEnabled` is true. Lazy-allocated so
|
|
192
192
|
// decks without narration don't pay for it.
|
|
193
|
+
//
|
|
194
|
+
// Narration is bound to the deck's PLAY state (`isAutoplay`), not to
|
|
195
|
+
// arbitrary page clicks: the user pressing the play button is what
|
|
196
|
+
// starts narration, the pause button stops it. This keeps multiple
|
|
197
|
+
// decks on a page from playing each other's audio when one is clicked,
|
|
198
|
+
// and keeps autoplay-blocked browsers from silently doing nothing —
|
|
199
|
+
// the user's click on the play control is itself the unlocking gesture.
|
|
193
200
|
let narrationAudio: HTMLAudioElement | null = null;
|
|
194
|
-
// Set when the most recent play() was rejected by the browser autoplay
|
|
195
|
-
// policy. The first user gesture clears it and replays the current
|
|
196
|
-
// slide's clip — without this a single-slide deck would never play
|
|
197
|
-
// narration, since there's no navigation event to retry on.
|
|
198
|
-
let narrationPendingGesture = false;
|
|
199
201
|
function playNarrationForSlide(index: number) {
|
|
202
|
+
if (muteNarration) return;
|
|
200
203
|
if (!project?.settings?.narrationEnabled) return;
|
|
201
204
|
const slide = project?.slides?.[index];
|
|
202
205
|
if (narrationAudio) {
|
|
@@ -207,22 +210,13 @@
|
|
|
207
210
|
if (!src) return;
|
|
208
211
|
if (!narrationAudio) narrationAudio = new Audio();
|
|
209
212
|
narrationAudio.src = src;
|
|
210
|
-
|
|
211
|
-
// rejection so a subsequent user gesture (handled below) retries
|
|
212
|
-
// once instead of silently giving up.
|
|
213
|
-
narrationAudio.play().then(
|
|
214
|
-
() => { narrationPendingGesture = false; },
|
|
215
|
-
() => { narrationPendingGesture = true; }
|
|
216
|
-
);
|
|
213
|
+
narrationAudio.play().catch(() => {});
|
|
217
214
|
}
|
|
218
|
-
function
|
|
219
|
-
if (
|
|
220
|
-
playNarrationForSlide(currentSlideIndex);
|
|
221
|
-
}
|
|
215
|
+
function pauseNarration() {
|
|
216
|
+
if (narrationAudio) narrationAudio.pause();
|
|
222
217
|
}
|
|
223
218
|
function stopNarration() {
|
|
224
219
|
if (narrationAudio) { narrationAudio.pause(); narrationAudio = null; }
|
|
225
|
-
narrationPendingGesture = false;
|
|
226
220
|
}
|
|
227
221
|
|
|
228
222
|
const slides = $derived(project?.slides ?? []);
|
|
@@ -565,6 +559,12 @@
|
|
|
565
559
|
currentSlideIndex = 0;
|
|
566
560
|
isTransitioning = false;
|
|
567
561
|
|
|
562
|
+
// Restart narration on loop. Setting `currentSlideIndex = 0` above
|
|
563
|
+
// is a no-op for single-slide decks (was already 0) so the play-state
|
|
564
|
+
// effect doesn't re-fire on its own. Calling explicitly here covers
|
|
565
|
+
// both single- and multi-slide loops uniformly.
|
|
566
|
+
if (isAutoplay) playNarrationForSlide(0);
|
|
567
|
+
|
|
568
568
|
for (const element of firstSlide.canvas.elements) {
|
|
569
569
|
if (element.type === 'text') {
|
|
570
570
|
const textEl = element as TextElement;
|
|
@@ -585,12 +585,10 @@
|
|
|
585
585
|
const targetSlide = slides[targetIndex];
|
|
586
586
|
clearAllTypewriterAnimations();
|
|
587
587
|
cancelMotionPathLoops();
|
|
588
|
-
// Trigger per-slide narration
|
|
589
|
-
//
|
|
590
|
-
//
|
|
591
|
-
|
|
592
|
-
// (which gets us here) acts as the unlocking gesture.
|
|
593
|
-
playNarrationForSlide(targetIndex);
|
|
588
|
+
// Trigger per-slide narration only while the deck is actively
|
|
589
|
+
// playing. Manual nav while paused stays silent — narration is
|
|
590
|
+
// bound to the play button, not to every interaction.
|
|
591
|
+
if (isAutoplay) playNarrationForSlide(targetIndex);
|
|
594
592
|
const transition = targetSlide.transition;
|
|
595
593
|
const duration = durationOverride ?? transition.duration;
|
|
596
594
|
transitionDurationMs = duration;
|
|
@@ -1150,11 +1148,9 @@
|
|
|
1150
1148
|
await loadCodeHighlights();
|
|
1151
1149
|
loading = false;
|
|
1152
1150
|
if (currentSlide) setTimeout(() => animateMotionPaths(currentSlide!), 300);
|
|
1153
|
-
//
|
|
1154
|
-
//
|
|
1155
|
-
//
|
|
1156
|
-
// click that starts the deck doubles as the audio unlock.
|
|
1157
|
-
playNarrationForSlide(currentSlideIndex);
|
|
1151
|
+
// Narration starts via the play-state effect below — not on
|
|
1152
|
+
// mount. That way the user's click on Play is the gesture
|
|
1153
|
+
// that unlocks audio, and a paused deck stays silent.
|
|
1158
1154
|
if (autoplay) isAutoplay = true;
|
|
1159
1155
|
} catch (e: any) { error = e.message; loading = false; }
|
|
1160
1156
|
}
|
|
@@ -1173,25 +1169,29 @@
|
|
|
1173
1169
|
if (containerEl) resizeObserver.observe(containerEl);
|
|
1174
1170
|
resetMouseIdleTimer();
|
|
1175
1171
|
|
|
1176
|
-
// First-gesture retry for narration playback. Browsers block
|
|
1177
|
-
// audio.play() until the user has interacted; without this hook
|
|
1178
|
-
// a single-slide deck would never play its narration since there's
|
|
1179
|
-
// no slide change to retry on. Attached on document so any click
|
|
1180
|
-
// or key press in the page acts as the unlocking gesture.
|
|
1181
|
-
document.addEventListener('pointerdown', tryUnlockNarrationOnGesture, { capture: true });
|
|
1182
|
-
document.addEventListener('keydown', tryUnlockNarrationOnGesture, { capture: true });
|
|
1183
|
-
|
|
1184
1172
|
return () => {
|
|
1185
1173
|
resizeObserver?.disconnect();
|
|
1186
1174
|
clearAutoplayTimer();
|
|
1187
1175
|
clearAllTypewriterAnimations();
|
|
1188
1176
|
if (mouseIdleTimer) clearTimeout(mouseIdleTimer);
|
|
1189
1177
|
stopNarration();
|
|
1190
|
-
document.removeEventListener('pointerdown', tryUnlockNarrationOnGesture, { capture: true });
|
|
1191
|
-
document.removeEventListener('keydown', tryUnlockNarrationOnGesture, { capture: true });
|
|
1192
1178
|
};
|
|
1193
1179
|
});
|
|
1194
1180
|
|
|
1181
|
+
// Narration follows the deck's play/pause state. The play button click
|
|
1182
|
+
// flips `isAutoplay` true → this effect fires → audio starts. The
|
|
1183
|
+
// click itself is the user gesture that unlocks the browser audio
|
|
1184
|
+
// context. Pause/stop turns it back off. Each presenter instance
|
|
1185
|
+
// scopes its own audio, so multiple decks on a page never overlap.
|
|
1186
|
+
$effect(() => {
|
|
1187
|
+
if (!project?.settings?.narrationEnabled) return;
|
|
1188
|
+
if (isAutoplay) {
|
|
1189
|
+
playNarrationForSlide(currentSlideIndex);
|
|
1190
|
+
} else {
|
|
1191
|
+
pauseNarration();
|
|
1192
|
+
}
|
|
1193
|
+
});
|
|
1194
|
+
|
|
1195
1195
|
// Watch for prop changes
|
|
1196
1196
|
$effect(() => { if (data) { project = data; } });
|
|
1197
1197
|
</script>
|