animot-presenter 0.5.7 → 0.5.10
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 +10 -4
- package/dist/EmbedPlayer.svelte +239 -0
- package/dist/EmbedPlayer.svelte.d.ts +17 -0
- package/dist/cdn/animot-presenter.css +1 -1
- package/dist/cdn/animot-presenter.esm.js +4334 -4039
- package/dist/cdn/animot-presenter.min.js +9 -9
- package/dist/types.d.ts +2 -0
- package/dist/utils/camera.js +12 -3
- package/dist/utils/decorations.js +130 -19
- package/dist/utils/embed-players.d.ts +50 -0
- package/dist/utils/embed-players.js +152 -0
- package/package.json +1 -1
package/dist/types.d.ts
CHANGED
|
@@ -223,12 +223,14 @@ export interface DecorationsConfig {
|
|
|
223
223
|
angle?: number;
|
|
224
224
|
speedMs?: number;
|
|
225
225
|
widthPct?: number;
|
|
226
|
+
randomness?: number;
|
|
226
227
|
};
|
|
227
228
|
gradientShift?: {
|
|
228
229
|
enabled: boolean;
|
|
229
230
|
colors?: string[];
|
|
230
231
|
speedMs?: number;
|
|
231
232
|
angle?: number;
|
|
233
|
+
direction?: 'forward' | 'reverse' | 'snake' | 'chase';
|
|
232
234
|
};
|
|
233
235
|
rgbSplit?: {
|
|
234
236
|
enabled: boolean;
|
package/dist/utils/camera.js
CHANGED
|
@@ -59,8 +59,17 @@ export function parallaxOffset(camera, depth, worldWidth, worldHeight) {
|
|
|
59
59
|
const cy = camera.y + camera.height / 2;
|
|
60
60
|
const wx = worldWidth / 2;
|
|
61
61
|
const wy = worldHeight / 2;
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
|
|
62
|
+
// Parallax magnitude is `depth × camera-offset-from-world-center × factor`.
|
|
63
|
+
// The earlier 0.4 factor blew up on large worlds (6000×4000) because the
|
|
64
|
+
// camera-offset term scales with world size — depth=0.7 gave ±840px shifts,
|
|
65
|
+
// destroying composition.
|
|
66
|
+
//
|
|
67
|
+
// Fix: scale the factor by the CAMERA WIDTH ratio to a 1920-baseline so the
|
|
68
|
+
// effect feels the same regardless of world size. A 1920-wide camera gets
|
|
69
|
+
// the original feel; a 3200-wide camera (zoomed out) gets a smaller offset
|
|
70
|
+
// per unit-of-world-distance so the layout doesn't fly apart.
|
|
71
|
+
const baseFactor = depth * 0.15;
|
|
72
|
+
const widthScale = Math.min(1, 1920 / Math.max(1, camera.width));
|
|
73
|
+
const factor = baseFactor * widthScale;
|
|
65
74
|
return { x: -(cx - wx) * factor, y: -(cy - wy) * factor };
|
|
66
75
|
}
|
|
@@ -61,19 +61,21 @@ export function decorations(node, params) {
|
|
|
61
61
|
if (!anyEnabled)
|
|
62
62
|
return;
|
|
63
63
|
saveOriginalStyles();
|
|
64
|
-
// Shimmer
|
|
65
|
-
//
|
|
66
|
-
// the
|
|
67
|
-
//
|
|
64
|
+
// Shimmer: original implementation — a gradient overlay sized 300% of
|
|
65
|
+
// the host with the stripe at 50%. Animating backgroundPosition slides
|
|
66
|
+
// the visible window across the gradient. User confirmed this looked
|
|
67
|
+
// right on the first cycle; the only issue was a perceived gap at the
|
|
68
|
+
// loop boundary. Fix below is in the tick loop (we time-shift `phase`
|
|
69
|
+
// so the empty/offscreen portion lands at the wrap, hiding the jump).
|
|
68
70
|
if (cfg.shimmer?.enabled) {
|
|
69
71
|
if (getComputedStyle(node).position === 'static') {
|
|
70
72
|
node.style.position = 'relative';
|
|
71
73
|
}
|
|
72
|
-
shimmerEl = document.createElement('div');
|
|
73
|
-
shimmerEl.className = 'animot-shimmer';
|
|
74
74
|
const angle = cfg.shimmer.angle ?? 110;
|
|
75
75
|
const widthPct = cfg.shimmer.widthPct ?? 25;
|
|
76
76
|
const color = cfg.shimmer.color ?? 'rgba(255, 255, 255, 0.4)';
|
|
77
|
+
shimmerEl = document.createElement('div');
|
|
78
|
+
shimmerEl.className = 'animot-shimmer';
|
|
77
79
|
Object.assign(shimmerEl.style, {
|
|
78
80
|
position: 'absolute',
|
|
79
81
|
inset: '0',
|
|
@@ -82,7 +84,12 @@ export function decorations(node, params) {
|
|
|
82
84
|
borderRadius: 'inherit',
|
|
83
85
|
background: `linear-gradient(${angle}deg, transparent 0%, transparent ${50 - widthPct / 2}%, ${color} 50%, transparent ${50 + widthPct / 2}%, transparent 100%)`,
|
|
84
86
|
backgroundSize: '300% 300%',
|
|
85
|
-
|
|
87
|
+
// Default `repeat` would tile the gradient — when the bg slides past
|
|
88
|
+
// an edge, the next tile's stripe enters from the opposite side
|
|
89
|
+
// (the "two squares touching corners" artifact). One stripe per cycle.
|
|
90
|
+
backgroundRepeat: 'no-repeat',
|
|
91
|
+
backgroundPosition: '-100% -100%',
|
|
92
|
+
willChange: 'background-position'
|
|
86
93
|
});
|
|
87
94
|
node.appendChild(shimmerEl);
|
|
88
95
|
}
|
|
@@ -90,8 +97,23 @@ export function decorations(node, params) {
|
|
|
90
97
|
if (cfg.gradientShift?.enabled) {
|
|
91
98
|
const colors = cfg.gradientShift.colors ?? ['#7c3aed', '#06b6d4', '#ec4899', '#7c3aed'];
|
|
92
99
|
const angle = cfg.gradientShift.angle ?? 135;
|
|
93
|
-
|
|
94
|
-
|
|
100
|
+
const direction = cfg.gradientShift.direction ?? 'forward';
|
|
101
|
+
if (direction === 'chase') {
|
|
102
|
+
// Conic gradient: each color is a slice of the pie. Repeat the first
|
|
103
|
+
// color at the end so the seam where it wraps doesn't show. The tick
|
|
104
|
+
// loop animates the `from` angle every frame.
|
|
105
|
+
const stops = [...colors, colors[0]].join(', ');
|
|
106
|
+
node.style.backgroundImage = `conic-gradient(from 0deg at 50% 50%, ${stops})`;
|
|
107
|
+
node.style.backgroundSize = '100% 100%';
|
|
108
|
+
node.style.backgroundPosition = '0 0';
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
node.style.backgroundImage = `linear-gradient(${angle}deg, ${colors.join(', ')})`;
|
|
112
|
+
// Bigger backgroundSize means more "headroom" for the position to
|
|
113
|
+
// sweep before the gradient repeats. 400% lets snake mode swing back
|
|
114
|
+
// and forth without ever showing a tile seam, even at 45° angles.
|
|
115
|
+
node.style.backgroundSize = '400% 400%';
|
|
116
|
+
}
|
|
95
117
|
}
|
|
96
118
|
const start = performance.now();
|
|
97
119
|
function tick(now) {
|
|
@@ -106,20 +128,109 @@ export function decorations(node, params) {
|
|
|
106
128
|
const spread = 2 + wave * 8 * intensity;
|
|
107
129
|
node.style.boxShadow = `0 0 ${blur}px ${spread}px ${cfg.glow.color}`;
|
|
108
130
|
}
|
|
109
|
-
// SHIMMER — sweep the gradient
|
|
131
|
+
// SHIMMER — sweep the stretched gradient ALONG its own gradient
|
|
132
|
+
// direction (derived from `angle`) so the stripe glides perpendicular
|
|
133
|
+
// to itself for any angle.
|
|
134
|
+
//
|
|
135
|
+
// Optional `randomness` (0–1) jitters each cycle's duration and
|
|
136
|
+
// inserts a random pause between sweeps so the effect feels alive
|
|
137
|
+
// instead of metronomic. Per-cycle state (start time, duration,
|
|
138
|
+
// pause length) is stashed on the wrapper so we don't burn a closure
|
|
139
|
+
// rebuild every frame.
|
|
110
140
|
if (cfg.shimmer?.enabled && shimmerEl) {
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
|
|
141
|
+
const baseSpeed = cfg.shimmer.speedMs ?? 3000;
|
|
142
|
+
const baseCycle = effectiveCycle(baseSpeed, params.slideDuration);
|
|
143
|
+
const randomness = Math.max(0, Math.min(1, cfg.shimmer.randomness ?? 0));
|
|
144
|
+
const w = shimmerEl;
|
|
145
|
+
if (typeof w.__shimCycleStart !== 'number') {
|
|
146
|
+
w.__shimCycleStart = elapsed;
|
|
147
|
+
w.__shimCycleDur = baseCycle;
|
|
148
|
+
w.__shimCyclePause = 0;
|
|
149
|
+
w.__shimSeed = 1;
|
|
150
|
+
}
|
|
151
|
+
let local = elapsed - w.__shimCycleStart;
|
|
152
|
+
const total = w.__shimCycleDur + w.__shimCyclePause;
|
|
153
|
+
if (local >= total) {
|
|
154
|
+
// Roll a new cycle. Hash-based pseudo-random keeps the sequence
|
|
155
|
+
// deterministic for a given seed — same shimmer plays back the
|
|
156
|
+
// same way on /present reload, useful when reviewing.
|
|
157
|
+
w.__shimSeed = (w.__shimSeed * 1103515245 + 12345) & 0x7fffffff;
|
|
158
|
+
const r1 = (w.__shimSeed % 1000) / 1000;
|
|
159
|
+
const r2 = ((w.__shimSeed >> 8) % 1000) / 1000;
|
|
160
|
+
// Duration multiplier: at randomness=1 it's 0.5×–2× base; at
|
|
161
|
+
// randomness=0 it's exactly base.
|
|
162
|
+
const jitter = (r1 * 2 - 1) * randomness; // -randomness..+randomness
|
|
163
|
+
const durMul = jitter >= 0 ? 1 + jitter : 1 / (1 - jitter * 0.5);
|
|
164
|
+
w.__shimCycleDur = Math.max(300, baseCycle * durMul);
|
|
165
|
+
// Pause between sweeps: 0 to randomness × baseCycle.
|
|
166
|
+
w.__shimCyclePause = baseCycle * randomness * r2;
|
|
167
|
+
w.__shimCycleStart = elapsed;
|
|
168
|
+
local = 0;
|
|
169
|
+
}
|
|
170
|
+
const angle = cfg.shimmer.angle ?? 110;
|
|
171
|
+
const rad = angle * Math.PI / 180;
|
|
172
|
+
const dirX = Math.sin(rad);
|
|
173
|
+
const dirY = -Math.cos(rad);
|
|
174
|
+
if (local > w.__shimCycleDur) {
|
|
175
|
+
// Pause window — park the stripe far offscreen so nothing renders.
|
|
176
|
+
shimmerEl.style.backgroundPosition = `-300% -300%`;
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
const phase = local / w.__shimCycleDur;
|
|
180
|
+
const offset = (phase - 0.5) * 300;
|
|
181
|
+
const posX = 50 + offset * dirX;
|
|
182
|
+
const posY = 50 + offset * dirY;
|
|
183
|
+
shimmerEl.style.backgroundPosition = `${posX}% ${posY}%`;
|
|
184
|
+
}
|
|
116
185
|
}
|
|
117
|
-
// GRADIENT SHIFT —
|
|
186
|
+
// GRADIENT SHIFT — direction modes:
|
|
187
|
+
// • forward — phase ramps 0 → 1, position increases monotonically
|
|
188
|
+
// • reverse — phase ramps 1 → 0
|
|
189
|
+
// • snake — phase oscillates via sin so the gradient slithers
|
|
190
|
+
// back and forth along its own angle
|
|
191
|
+
// • chase — colors rotate around the element via a conic gradient
|
|
192
|
+
// (with 4 colors this is the "each color shifts to the
|
|
193
|
+
// next side" pinwheel)
|
|
194
|
+
// Sweep direction (for non-chase modes) is derived from `angle` so
|
|
195
|
+
// the gradient flows perpendicular to its own bands.
|
|
118
196
|
if (cfg.gradientShift?.enabled) {
|
|
119
197
|
const cycle = effectiveCycle(cfg.gradientShift.speedMs ?? 6000, params.slideDuration);
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
198
|
+
const direction = cfg.gradientShift.direction ?? 'forward';
|
|
199
|
+
if (direction === 'chase') {
|
|
200
|
+
// Rebuild the conic gradient each frame with a rotating `from`
|
|
201
|
+
// angle. Cheap to render — modern browsers compose conic
|
|
202
|
+
// gradients on the GPU. Color list is closed (first color
|
|
203
|
+
// repeated at end) so the seam where 360° wraps to 0° is
|
|
204
|
+
// invisible.
|
|
205
|
+
const colors = cfg.gradientShift.colors ?? ['#7c3aed', '#06b6d4', '#ec4899', '#7c3aed'];
|
|
206
|
+
const stops = [...colors, colors[0]].join(', ');
|
|
207
|
+
const rotateDeg = ((elapsed % cycle) / cycle) * 360;
|
|
208
|
+
node.style.backgroundImage = `conic-gradient(from ${rotateDeg}deg at 50% 50%, ${stops})`;
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
const angle = cfg.gradientShift.angle ?? 135;
|
|
212
|
+
const rad = angle * Math.PI / 180;
|
|
213
|
+
const dirX = Math.sin(rad);
|
|
214
|
+
const dirY = -Math.cos(rad);
|
|
215
|
+
let phase;
|
|
216
|
+
if (direction === 'snake') {
|
|
217
|
+
phase = (Math.sin((elapsed / cycle) * Math.PI * 2) + 1) / 2;
|
|
218
|
+
}
|
|
219
|
+
else if (direction === 'reverse') {
|
|
220
|
+
phase = 1 - ((elapsed % cycle) / cycle);
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
phase = (elapsed % cycle) / cycle;
|
|
224
|
+
}
|
|
225
|
+
// Sweep range = 200% of host (centered at 50%). With
|
|
226
|
+
// backgroundSize 400%, position values from -50% to 250% all
|
|
227
|
+
// keep the gradient fully covering the element across all angles.
|
|
228
|
+
const range = 200;
|
|
229
|
+
const offset = (phase - 0.5) * range;
|
|
230
|
+
const posX = 50 + offset * dirX;
|
|
231
|
+
const posY = 50 + offset * dirY;
|
|
232
|
+
node.style.backgroundPosition = `${posX}% ${posY}%`;
|
|
233
|
+
}
|
|
123
234
|
}
|
|
124
235
|
// RGB SPLIT — chromatic aberration via stacked drop-shadows.
|
|
125
236
|
if (cfg.rgbSplit?.enabled) {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified wrapper around YouTube IFrame Player API and Vimeo Player.js so the
|
|
3
|
+
* rest of the app can drive embedded videos with the same play/pause/seek/
|
|
4
|
+
* volume calls used for `<video>` elements. The provider libraries are lazy-
|
|
5
|
+
* loaded on first use so a project with no embeds doesn't pay the network cost.
|
|
6
|
+
*
|
|
7
|
+
* What this enables:
|
|
8
|
+
* • Auto-pause embeds when the user navigates away from a slide.
|
|
9
|
+
* • Reset embed `currentTime` to startTime on slide enter (otherwise YouTube
|
|
10
|
+
* persists position across iframe rebuilds).
|
|
11
|
+
* • Render OUR own play/pause/scrub controls on top of the iframe (we hide
|
|
12
|
+
* YouTube's chrome via `controls=0` in the embed URL).
|
|
13
|
+
*
|
|
14
|
+
* What this does NOT enable: server-side export capture. YouTube/Vimeo render
|
|
15
|
+
* frames inside their player; Puppeteer can't screenshot cross-origin iframe
|
|
16
|
+
* content, so exports of slides containing embeds still fall back to the
|
|
17
|
+
* provider-supplied thumbnail (handled in /present's render path).
|
|
18
|
+
*/
|
|
19
|
+
export declare function loadYouTubeAPI(): Promise<typeof window['YT']>;
|
|
20
|
+
export declare function loadVimeoAPI(): Promise<any>;
|
|
21
|
+
export interface UnifiedPlayer {
|
|
22
|
+
play(): Promise<void> | void;
|
|
23
|
+
pause(): Promise<void> | void;
|
|
24
|
+
seekTo(seconds: number): Promise<void> | void;
|
|
25
|
+
getCurrentTime(): Promise<number>;
|
|
26
|
+
getDuration(): Promise<number>;
|
|
27
|
+
setVolume(v: number): void;
|
|
28
|
+
setMuted(m: boolean): void;
|
|
29
|
+
setPlaybackRate(r: number): void;
|
|
30
|
+
destroy(): void;
|
|
31
|
+
/** Subscribe to playing-state changes. Call returned fn to unsubscribe. */
|
|
32
|
+
onStateChange(cb: (state: 'playing' | 'paused' | 'ended' | 'buffering' | 'cued' | 'unstarted') => void): () => void;
|
|
33
|
+
}
|
|
34
|
+
interface CreateOptions {
|
|
35
|
+
provider: 'youtube' | 'vimeo';
|
|
36
|
+
/** The actual <iframe> element. We attach the player to it. */
|
|
37
|
+
iframe: HTMLIFrameElement;
|
|
38
|
+
/** Provider video ID (used for the YouTube ctor; Vimeo binds straight to iframe). */
|
|
39
|
+
videoId?: string;
|
|
40
|
+
startTime?: number;
|
|
41
|
+
muted?: boolean;
|
|
42
|
+
}
|
|
43
|
+
export declare function createEmbedPlayer(opts: CreateOptions): Promise<UnifiedPlayer>;
|
|
44
|
+
declare global {
|
|
45
|
+
interface Window {
|
|
46
|
+
YT: any;
|
|
47
|
+
onYouTubeIframeAPIReady?: () => void;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export {};
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified wrapper around YouTube IFrame Player API and Vimeo Player.js so the
|
|
3
|
+
* rest of the app can drive embedded videos with the same play/pause/seek/
|
|
4
|
+
* volume calls used for `<video>` elements. The provider libraries are lazy-
|
|
5
|
+
* loaded on first use so a project with no embeds doesn't pay the network cost.
|
|
6
|
+
*
|
|
7
|
+
* What this enables:
|
|
8
|
+
* • Auto-pause embeds when the user navigates away from a slide.
|
|
9
|
+
* • Reset embed `currentTime` to startTime on slide enter (otherwise YouTube
|
|
10
|
+
* persists position across iframe rebuilds).
|
|
11
|
+
* • Render OUR own play/pause/scrub controls on top of the iframe (we hide
|
|
12
|
+
* YouTube's chrome via `controls=0` in the embed URL).
|
|
13
|
+
*
|
|
14
|
+
* What this does NOT enable: server-side export capture. YouTube/Vimeo render
|
|
15
|
+
* frames inside their player; Puppeteer can't screenshot cross-origin iframe
|
|
16
|
+
* content, so exports of slides containing embeds still fall back to the
|
|
17
|
+
* provider-supplied thumbnail (handled in /present's render path).
|
|
18
|
+
*/
|
|
19
|
+
let ytApiPromise = null;
|
|
20
|
+
export function loadYouTubeAPI() {
|
|
21
|
+
if (typeof window === 'undefined')
|
|
22
|
+
return Promise.reject(new Error('SSR'));
|
|
23
|
+
if (window.YT && window.YT.Player)
|
|
24
|
+
return Promise.resolve(window.YT);
|
|
25
|
+
if (ytApiPromise)
|
|
26
|
+
return ytApiPromise;
|
|
27
|
+
ytApiPromise = new Promise((resolve, reject) => {
|
|
28
|
+
// The IFrame API expects a global callback. Chain any pre-existing one
|
|
29
|
+
// so we play nicely with other libraries on the page.
|
|
30
|
+
const prev = window.onYouTubeIframeAPIReady;
|
|
31
|
+
window.onYouTubeIframeAPIReady = () => {
|
|
32
|
+
if (typeof prev === 'function') {
|
|
33
|
+
try {
|
|
34
|
+
prev();
|
|
35
|
+
}
|
|
36
|
+
catch { }
|
|
37
|
+
}
|
|
38
|
+
resolve(window.YT);
|
|
39
|
+
};
|
|
40
|
+
const tag = document.createElement('script');
|
|
41
|
+
tag.src = 'https://www.youtube.com/iframe_api';
|
|
42
|
+
tag.async = true;
|
|
43
|
+
tag.onerror = () => reject(new Error('Failed to load YouTube IFrame API'));
|
|
44
|
+
document.head.appendChild(tag);
|
|
45
|
+
});
|
|
46
|
+
return ytApiPromise;
|
|
47
|
+
}
|
|
48
|
+
let vimeoApiPromise = null;
|
|
49
|
+
export function loadVimeoAPI() {
|
|
50
|
+
if (typeof window === 'undefined')
|
|
51
|
+
return Promise.reject(new Error('SSR'));
|
|
52
|
+
const w = window;
|
|
53
|
+
if (w.Vimeo && w.Vimeo.Player)
|
|
54
|
+
return Promise.resolve(w.Vimeo);
|
|
55
|
+
if (vimeoApiPromise)
|
|
56
|
+
return vimeoApiPromise;
|
|
57
|
+
vimeoApiPromise = new Promise((resolve, reject) => {
|
|
58
|
+
const tag = document.createElement('script');
|
|
59
|
+
tag.src = 'https://player.vimeo.com/api/player.js';
|
|
60
|
+
tag.async = true;
|
|
61
|
+
tag.onload = () => resolve(window.Vimeo);
|
|
62
|
+
tag.onerror = () => reject(new Error('Failed to load Vimeo Player API'));
|
|
63
|
+
document.head.appendChild(tag);
|
|
64
|
+
});
|
|
65
|
+
return vimeoApiPromise;
|
|
66
|
+
}
|
|
67
|
+
export async function createEmbedPlayer(opts) {
|
|
68
|
+
if (opts.provider === 'youtube')
|
|
69
|
+
return createYouTubePlayer(opts);
|
|
70
|
+
return createVimeoPlayer(opts);
|
|
71
|
+
}
|
|
72
|
+
async function createYouTubePlayer(opts) {
|
|
73
|
+
const YT = await loadYouTubeAPI();
|
|
74
|
+
let stateCbs = [];
|
|
75
|
+
const player = await new Promise((resolve) => {
|
|
76
|
+
const p = new YT.Player(opts.iframe, {
|
|
77
|
+
events: {
|
|
78
|
+
onReady: () => resolve(p),
|
|
79
|
+
onStateChange: (e) => {
|
|
80
|
+
const map = {
|
|
81
|
+
[-1]: 'unstarted',
|
|
82
|
+
[0]: 'ended',
|
|
83
|
+
[1]: 'playing',
|
|
84
|
+
[2]: 'paused',
|
|
85
|
+
[3]: 'buffering',
|
|
86
|
+
[5]: 'cued'
|
|
87
|
+
};
|
|
88
|
+
const state = map[e.data] ?? 'unstarted';
|
|
89
|
+
for (const cb of stateCbs)
|
|
90
|
+
try {
|
|
91
|
+
cb(state);
|
|
92
|
+
}
|
|
93
|
+
catch { }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
if (opts.muted)
|
|
99
|
+
player.mute();
|
|
100
|
+
if (opts.startTime)
|
|
101
|
+
try {
|
|
102
|
+
player.seekTo(opts.startTime, true);
|
|
103
|
+
}
|
|
104
|
+
catch { }
|
|
105
|
+
return {
|
|
106
|
+
play: () => player.playVideo(),
|
|
107
|
+
pause: () => player.pauseVideo(),
|
|
108
|
+
seekTo: (s) => player.seekTo(s, true),
|
|
109
|
+
getCurrentTime: async () => Number(player.getCurrentTime()) || 0,
|
|
110
|
+
getDuration: async () => Number(player.getDuration()) || 0,
|
|
111
|
+
setVolume: (v) => player.setVolume(Math.max(0, Math.min(100, v * 100))),
|
|
112
|
+
setMuted: (m) => (m ? player.mute() : player.unMute()),
|
|
113
|
+
setPlaybackRate: (r) => player.setPlaybackRate(r),
|
|
114
|
+
destroy: () => { try {
|
|
115
|
+
player.destroy();
|
|
116
|
+
}
|
|
117
|
+
catch { } },
|
|
118
|
+
onStateChange: (cb) => { stateCbs.push(cb); return () => { stateCbs = stateCbs.filter((f) => f !== cb); }; }
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
async function createVimeoPlayer(opts) {
|
|
122
|
+
const Vimeo = await loadVimeoAPI();
|
|
123
|
+
const player = new Vimeo.Player(opts.iframe);
|
|
124
|
+
let stateCbs = [];
|
|
125
|
+
await player.ready();
|
|
126
|
+
if (opts.muted)
|
|
127
|
+
await player.setMuted(true);
|
|
128
|
+
if (opts.startTime)
|
|
129
|
+
try {
|
|
130
|
+
await player.setCurrentTime(opts.startTime);
|
|
131
|
+
}
|
|
132
|
+
catch { }
|
|
133
|
+
player.on('play', () => stateCbs.forEach((cb) => cb('playing')));
|
|
134
|
+
player.on('pause', () => stateCbs.forEach((cb) => cb('paused')));
|
|
135
|
+
player.on('ended', () => stateCbs.forEach((cb) => cb('ended')));
|
|
136
|
+
player.on('bufferstart', () => stateCbs.forEach((cb) => cb('buffering')));
|
|
137
|
+
return {
|
|
138
|
+
play: () => player.play(),
|
|
139
|
+
pause: () => player.pause(),
|
|
140
|
+
seekTo: (s) => player.setCurrentTime(s),
|
|
141
|
+
getCurrentTime: () => player.getCurrentTime(),
|
|
142
|
+
getDuration: () => player.getDuration(),
|
|
143
|
+
setVolume: (v) => player.setVolume(Math.max(0, Math.min(1, v))),
|
|
144
|
+
setMuted: (m) => player.setMuted(m),
|
|
145
|
+
setPlaybackRate: (r) => player.setPlaybackRate(r),
|
|
146
|
+
destroy: () => { try {
|
|
147
|
+
player.destroy();
|
|
148
|
+
}
|
|
149
|
+
catch { } },
|
|
150
|
+
onStateChange: (cb) => { stateCbs.push(cb); return () => { stateCbs = stateCbs.filter((f) => f !== cb); }; }
|
|
151
|
+
};
|
|
152
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "animot-presenter",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.10",
|
|
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",
|