pptx-browser 4.1.0
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/LICENSE +9 -0
- package/README.md +209 -0
- package/package.json +53 -0
- package/src/animation.js +817 -0
- package/src/charts.js +989 -0
- package/src/clipboard.js +416 -0
- package/src/colors.js +297 -0
- package/src/effects3d.js +312 -0
- package/src/extract.js +535 -0
- package/src/fntdata.js +265 -0
- package/src/fonts.js +676 -0
- package/src/index.js +751 -0
- package/src/pdf.js +298 -0
- package/src/render.js +1964 -0
- package/src/shapes.js +666 -0
- package/src/slideshow.js +492 -0
- package/src/smartart.js +696 -0
- package/src/svg.js +732 -0
- package/src/theme.js +88 -0
- package/src/utils.js +50 -0
- package/src/writer.js +1015 -0
- package/src/zip-writer.js +214 -0
- package/src/zip.js +194 -0
package/src/slideshow.js
ADDED
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* slideshow.js — Full-screen slide show player.
|
|
3
|
+
*
|
|
4
|
+
* Creates a browser-native presentation mode with:
|
|
5
|
+
* - Fullscreen API integration (F11 / Escape to exit)
|
|
6
|
+
* - Keyboard navigation (←→ arrow keys, Space, PageUp/Down, Home, End)
|
|
7
|
+
* - Touch / swipe support
|
|
8
|
+
* - Slide counter HUD
|
|
9
|
+
* - Presenter notes panel (optional)
|
|
10
|
+
* - Animation engine integration (PptxPlayer)
|
|
11
|
+
* - Transition effects between slides
|
|
12
|
+
* - Thumbnail strip for quick navigation
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* import { SlideShow } from 'pptx-canvas-renderer';
|
|
16
|
+
*
|
|
17
|
+
* const show = new SlideShow(renderer, container);
|
|
18
|
+
* await show.start(0); // start from slide 0, request fullscreen
|
|
19
|
+
* show.stop(); // exit and clean up
|
|
20
|
+
*
|
|
21
|
+
* // Or without fullscreen:
|
|
22
|
+
* const show = new SlideShow(renderer, container, { fullscreen: false });
|
|
23
|
+
* await show.start();
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
export class SlideShow {
|
|
27
|
+
/**
|
|
28
|
+
* @param {object} renderer — loaded PptxRenderer instance
|
|
29
|
+
* @param {HTMLElement} container — DOM element to attach to
|
|
30
|
+
* @param {object} [opts]
|
|
31
|
+
* @param {boolean} [opts.fullscreen=true] — request fullscreen on start
|
|
32
|
+
* @param {boolean} [opts.showNotes=false] — show presenter notes panel
|
|
33
|
+
* @param {boolean} [opts.showThumbs=false] — show thumbnail strip
|
|
34
|
+
* @param {boolean} [opts.showHud=true] — show slide counter HUD
|
|
35
|
+
* @param {boolean} [opts.loop=false] — loop back to start at end
|
|
36
|
+
* @param {boolean} [opts.autoAdvance=0] — ms between slides (0=manual)
|
|
37
|
+
* @param {function} [opts.onSlideChange] — (index) => void
|
|
38
|
+
*/
|
|
39
|
+
constructor(renderer, container, opts = {}) {
|
|
40
|
+
this.renderer = renderer;
|
|
41
|
+
this.container = container;
|
|
42
|
+
this.opts = {
|
|
43
|
+
fullscreen: true,
|
|
44
|
+
showNotes: false,
|
|
45
|
+
showThumbs: false,
|
|
46
|
+
showHud: true,
|
|
47
|
+
loop: false,
|
|
48
|
+
autoAdvance: 0,
|
|
49
|
+
...opts,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
this._index = 0;
|
|
53
|
+
this._playing = false;
|
|
54
|
+
this._player = null;
|
|
55
|
+
this._autoTimer = null;
|
|
56
|
+
this._el = null; // root overlay element
|
|
57
|
+
this._canvas = null;
|
|
58
|
+
this._notesEl = null;
|
|
59
|
+
this._hudEl = null;
|
|
60
|
+
this._thumbsEl = null;
|
|
61
|
+
this._thumbnails = []; // low-res canvas elements
|
|
62
|
+
|
|
63
|
+
// Touch tracking
|
|
64
|
+
this._touchStartX = 0;
|
|
65
|
+
this._touchStartY = 0;
|
|
66
|
+
|
|
67
|
+
// Bound handlers (for removeEventListener)
|
|
68
|
+
this._onKey = this._onKey.bind(this);
|
|
69
|
+
this._onResize = this._onResize.bind(this);
|
|
70
|
+
this._onFsChange = this._onFsChange.bind(this);
|
|
71
|
+
this._onTouchStart = this._onTouchStart.bind(this);
|
|
72
|
+
this._onTouchEnd = this._onTouchEnd.bind(this);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── Lifecycle ───────────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
/** Start the slide show, optionally requesting fullscreen. */
|
|
78
|
+
async start(slideIndex = 0) {
|
|
79
|
+
this._playing = true;
|
|
80
|
+
this._index = Math.max(0, Math.min(slideIndex, this.renderer.slideCount - 1));
|
|
81
|
+
|
|
82
|
+
this._buildDOM();
|
|
83
|
+
this._attachEvents();
|
|
84
|
+
|
|
85
|
+
if (this.opts.fullscreen) {
|
|
86
|
+
await this._requestFullscreen();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Pre-generate thumbnails in background
|
|
90
|
+
if (this.opts.showThumbs) {
|
|
91
|
+
this._generateThumbnails(); // don't await — background task
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
await this._renderCurrent();
|
|
95
|
+
this._updateHud();
|
|
96
|
+
|
|
97
|
+
if (this.opts.autoAdvance > 0) {
|
|
98
|
+
this._startAutoAdvance();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Stop and clean up. */
|
|
103
|
+
stop() {
|
|
104
|
+
this._playing = false;
|
|
105
|
+
this._stopAutoAdvance();
|
|
106
|
+
this._detachEvents();
|
|
107
|
+
if (document.fullscreenElement === this._el) {
|
|
108
|
+
document.exitFullscreen().catch(() => {});
|
|
109
|
+
}
|
|
110
|
+
if (this._el && this._el.parentNode) {
|
|
111
|
+
this._el.parentNode.removeChild(this._el);
|
|
112
|
+
}
|
|
113
|
+
this._el = null;
|
|
114
|
+
this._canvas = null;
|
|
115
|
+
this._player = null;
|
|
116
|
+
this.opts.onSlideChange?.(null);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Go to a specific slide. */
|
|
120
|
+
async goto(index) {
|
|
121
|
+
if (!this._playing) return;
|
|
122
|
+
const newIndex = Math.max(0, Math.min(index, this.renderer.slideCount - 1));
|
|
123
|
+
if (newIndex === this._index) return;
|
|
124
|
+
|
|
125
|
+
const prevIndex = this._index;
|
|
126
|
+
this._index = newIndex;
|
|
127
|
+
this._resetAutoAdvance();
|
|
128
|
+
await this._renderCurrent(prevIndex);
|
|
129
|
+
this._updateHud();
|
|
130
|
+
this.opts.onSlideChange?.(this._index);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Advance to next slide (or next animation click). */
|
|
134
|
+
async next() {
|
|
135
|
+
if (!this._playing) return;
|
|
136
|
+
if (this._player) {
|
|
137
|
+
// If animations remain, advance click
|
|
138
|
+
const steps = this.renderer.getAnimations?.(this._index) || [];
|
|
139
|
+
const maxClick = steps.length ? Math.max(...steps.map(s => s.clickNum), 0) : 0;
|
|
140
|
+
// Simple heuristic — if player exists and not at end, click
|
|
141
|
+
await this._player.nextClick?.();
|
|
142
|
+
}
|
|
143
|
+
if (this._index < this.renderer.slideCount - 1) {
|
|
144
|
+
await this.goto(this._index + 1);
|
|
145
|
+
} else if (this.opts.loop) {
|
|
146
|
+
await this.goto(0);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Go to previous slide. */
|
|
151
|
+
async prev() {
|
|
152
|
+
if (!this._playing) return;
|
|
153
|
+
if (this._index > 0) {
|
|
154
|
+
await this.goto(this._index - 1);
|
|
155
|
+
} else if (this.opts.loop) {
|
|
156
|
+
await this.goto(this.renderer.slideCount - 1);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
get currentIndex() { return this._index; }
|
|
161
|
+
get isPlaying() { return this._playing; }
|
|
162
|
+
|
|
163
|
+
// ── DOM construction ────────────────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
_buildDOM() {
|
|
166
|
+
// Root overlay
|
|
167
|
+
const el = document.createElement('div');
|
|
168
|
+
el.style.cssText = [
|
|
169
|
+
'position:fixed', 'inset:0', 'z-index:999999',
|
|
170
|
+
'background:#000', 'display:flex', 'flex-direction:column',
|
|
171
|
+
'align-items:center', 'justify-content:center',
|
|
172
|
+
'user-select:none', 'touch-action:none',
|
|
173
|
+
].join(';');
|
|
174
|
+
el.setAttribute('tabindex', '0');
|
|
175
|
+
this._el = el;
|
|
176
|
+
|
|
177
|
+
// Main canvas area
|
|
178
|
+
const canvasWrap = document.createElement('div');
|
|
179
|
+
canvasWrap.style.cssText = 'position:relative; flex:1; display:flex; align-items:center; justify-content:center; width:100%; overflow:hidden;';
|
|
180
|
+
|
|
181
|
+
const canvas = document.createElement('canvas');
|
|
182
|
+
canvas.style.cssText = 'display:block; box-shadow:0 4px 32px rgba(0,0,0,0.6);';
|
|
183
|
+
this._canvas = canvas;
|
|
184
|
+
canvasWrap.appendChild(canvas);
|
|
185
|
+
el.appendChild(canvasWrap);
|
|
186
|
+
|
|
187
|
+
// HUD (slide counter + controls)
|
|
188
|
+
if (this.opts.showHud) {
|
|
189
|
+
const hud = document.createElement('div');
|
|
190
|
+
hud.style.cssText = [
|
|
191
|
+
'position:absolute', 'bottom:24px', 'left:50%', 'transform:translateX(-50%)',
|
|
192
|
+
'display:flex', 'align-items:center', 'gap:12px',
|
|
193
|
+
'background:rgba(0,0,0,0.55)', 'backdrop-filter:blur(8px)',
|
|
194
|
+
'border-radius:24px', 'padding:8px 20px',
|
|
195
|
+
'color:white', 'font:500 14px/1 system-ui,sans-serif',
|
|
196
|
+
'pointer-events:none',
|
|
197
|
+
].join(';');
|
|
198
|
+
this._hudEl = hud;
|
|
199
|
+
el.appendChild(hud);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Notes panel
|
|
203
|
+
if (this.opts.showNotes) {
|
|
204
|
+
const notes = document.createElement('div');
|
|
205
|
+
notes.style.cssText = [
|
|
206
|
+
'width:100%', 'max-height:22vh', 'overflow-y:auto',
|
|
207
|
+
'background:rgba(0,0,0,0.7)', 'backdrop-filter:blur(8px)',
|
|
208
|
+
'color:#e0e0e0', 'font:14px/1.5 system-ui,sans-serif',
|
|
209
|
+
'padding:12px 24px', 'white-space:pre-wrap', 'flex-shrink:0',
|
|
210
|
+
].join(';');
|
|
211
|
+
this._notesEl = notes;
|
|
212
|
+
el.appendChild(notes);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Thumbnail strip
|
|
216
|
+
if (this.opts.showThumbs) {
|
|
217
|
+
const thumbs = document.createElement('div');
|
|
218
|
+
thumbs.style.cssText = [
|
|
219
|
+
'display:flex', 'gap:6px', 'padding:8px 16px',
|
|
220
|
+
'overflow-x:auto', 'background:rgba(0,0,0,0.7)',
|
|
221
|
+
'width:100%', 'flex-shrink:0',
|
|
222
|
+
].join(';');
|
|
223
|
+
this._thumbsEl = thumbs;
|
|
224
|
+
el.appendChild(thumbs);
|
|
225
|
+
// Placeholder thumbnails
|
|
226
|
+
for (let i = 0; i < this.renderer.slideCount; i++) {
|
|
227
|
+
const thumb = document.createElement('canvas');
|
|
228
|
+
thumb.width = 120;
|
|
229
|
+
thumb.height = Math.round(120 / (this.renderer.slideSize.cx / this.renderer.slideSize.cy));
|
|
230
|
+
thumb.style.cssText = 'flex-shrink:0; cursor:pointer; border:2px solid transparent; border-radius:3px; opacity:0.6; transition:opacity 0.2s,border-color 0.2s;';
|
|
231
|
+
thumb.title = `Slide ${i + 1}`;
|
|
232
|
+
const idx = i;
|
|
233
|
+
thumb.addEventListener('click', () => this.goto(idx));
|
|
234
|
+
thumbs.appendChild(thumb);
|
|
235
|
+
this._thumbnails.push(thumb);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Click to advance (on canvas wrap)
|
|
240
|
+
canvasWrap.addEventListener('click', () => this.next());
|
|
241
|
+
|
|
242
|
+
// Nav arrow buttons (hidden unless hover)
|
|
243
|
+
this._buildNavArrows(canvasWrap);
|
|
244
|
+
|
|
245
|
+
// Close button
|
|
246
|
+
const closeBtn = document.createElement('button');
|
|
247
|
+
closeBtn.textContent = '✕';
|
|
248
|
+
closeBtn.style.cssText = [
|
|
249
|
+
'position:absolute', 'top:16px', 'right:20px',
|
|
250
|
+
'background:rgba(255,255,255,0.15)', 'border:none',
|
|
251
|
+
'color:white', 'font-size:18px', 'width:36px', 'height:36px',
|
|
252
|
+
'border-radius:50%', 'cursor:pointer', 'opacity:0.7',
|
|
253
|
+
'transition:opacity 0.2s',
|
|
254
|
+
].join(';');
|
|
255
|
+
closeBtn.addEventListener('mouseover', () => closeBtn.style.opacity = '1');
|
|
256
|
+
closeBtn.addEventListener('mouseout', () => closeBtn.style.opacity = '0.7');
|
|
257
|
+
closeBtn.addEventListener('click', (e) => { e.stopPropagation(); this.stop(); });
|
|
258
|
+
el.appendChild(closeBtn);
|
|
259
|
+
|
|
260
|
+
this.container.appendChild(el);
|
|
261
|
+
el.focus();
|
|
262
|
+
this._resizeCanvas();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
_buildNavArrows(parent) {
|
|
266
|
+
const makeArrow = (dir) => {
|
|
267
|
+
const btn = document.createElement('button');
|
|
268
|
+
btn.textContent = dir === 'prev' ? '❮' : '❯';
|
|
269
|
+
btn.style.cssText = [
|
|
270
|
+
'position:absolute',
|
|
271
|
+
dir === 'prev' ? 'left:16px' : 'right:16px',
|
|
272
|
+
'top:50%', 'transform:translateY(-50%)',
|
|
273
|
+
'background:rgba(255,255,255,0.18)', 'border:none',
|
|
274
|
+
'color:white', 'font-size:22px', 'width:48px', 'height:64px',
|
|
275
|
+
'border-radius:8px', 'cursor:pointer',
|
|
276
|
+
'opacity:0', 'transition:opacity 0.2s',
|
|
277
|
+
].join(';');
|
|
278
|
+
btn.addEventListener('mouseenter', () => btn.style.opacity = '1');
|
|
279
|
+
btn.addEventListener('mouseleave', () => btn.style.opacity = '0');
|
|
280
|
+
btn.addEventListener('click', (e) => {
|
|
281
|
+
e.stopPropagation();
|
|
282
|
+
dir === 'prev' ? this.prev() : this.next();
|
|
283
|
+
});
|
|
284
|
+
parent.addEventListener('mouseenter', () => btn.style.opacity = '0.5');
|
|
285
|
+
parent.addEventListener('mouseleave', () => btn.style.opacity = '0');
|
|
286
|
+
parent.appendChild(btn);
|
|
287
|
+
};
|
|
288
|
+
makeArrow('prev');
|
|
289
|
+
makeArrow('next');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ── Canvas sizing ───────────────────────────────────────────────────────────
|
|
293
|
+
|
|
294
|
+
_resizeCanvas() {
|
|
295
|
+
if (!this._canvas || !this._el) return;
|
|
296
|
+
const { cx, cy } = this.renderer.slideSize;
|
|
297
|
+
const aspect = cx / cy;
|
|
298
|
+
|
|
299
|
+
const container = this._canvas.parentElement;
|
|
300
|
+
const availW = container.clientWidth || window.innerWidth;
|
|
301
|
+
const availH = container.clientHeight || window.innerHeight * 0.75;
|
|
302
|
+
|
|
303
|
+
let w = availW, h = w / aspect;
|
|
304
|
+
if (h > availH) { h = availH; w = h * aspect; }
|
|
305
|
+
|
|
306
|
+
this._canvas.width = Math.round(w * window.devicePixelRatio);
|
|
307
|
+
this._canvas.height = Math.round(h * window.devicePixelRatio);
|
|
308
|
+
this._canvas.style.width = Math.round(w) + 'px';
|
|
309
|
+
this._canvas.style.height = Math.round(h) + 'px';
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ── Rendering ───────────────────────────────────────────────────────────────
|
|
313
|
+
|
|
314
|
+
async _renderCurrent(prevIndex = null) {
|
|
315
|
+
if (!this._canvas) return;
|
|
316
|
+
this._resizeCanvas();
|
|
317
|
+
|
|
318
|
+
// Transition if we have a prev canvas snapshot
|
|
319
|
+
if (prevIndex !== null && this.renderer.getTransition) {
|
|
320
|
+
const transition = this.renderer.getTransition(this._index);
|
|
321
|
+
if (transition && transition.type !== 'cut' && typeof OffscreenCanvas !== 'undefined') {
|
|
322
|
+
const { renderTransitionFrame } = await import('./animation.js');
|
|
323
|
+
const from = new OffscreenCanvas(this._canvas.width, this._canvas.height);
|
|
324
|
+
from.getContext('2d').drawImage(this._canvas, 0, 0);
|
|
325
|
+
|
|
326
|
+
await this.renderer.renderSlide(this._index, this._canvas, this._canvas.width / window.devicePixelRatio);
|
|
327
|
+
|
|
328
|
+
const to = new OffscreenCanvas(this._canvas.width, this._canvas.height);
|
|
329
|
+
to.getContext('2d').drawImage(this._canvas, 0, 0);
|
|
330
|
+
|
|
331
|
+
const ctx = this._canvas.getContext('2d');
|
|
332
|
+
const dur = transition.duration || 700;
|
|
333
|
+
const start = performance.now();
|
|
334
|
+
await new Promise(resolve => {
|
|
335
|
+
const frame = (now) => {
|
|
336
|
+
const p = Math.min(1, (now - start) / dur);
|
|
337
|
+
renderTransitionFrame(ctx, from, to, transition, p);
|
|
338
|
+
if (p < 1) requestAnimationFrame(frame); else resolve();
|
|
339
|
+
};
|
|
340
|
+
requestAnimationFrame(frame);
|
|
341
|
+
});
|
|
342
|
+
this._updateThumbnail(this._index);
|
|
343
|
+
this._updateNotes();
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
await this.renderer.renderSlide(this._index, this._canvas, this._canvas.width / window.devicePixelRatio);
|
|
349
|
+
this._updateThumbnail(this._index);
|
|
350
|
+
this._updateNotes();
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
_updateThumbnail(index) {
|
|
354
|
+
const thumb = this._thumbnails[index];
|
|
355
|
+
if (!thumb || !this._canvas) return;
|
|
356
|
+
thumb.getContext('2d').drawImage(this._canvas, 0, 0, thumb.width, thumb.height);
|
|
357
|
+
// Update active state
|
|
358
|
+
this._thumbnails.forEach((t, i) => {
|
|
359
|
+
t.style.borderColor = i === this._index ? '#4af' : 'transparent';
|
|
360
|
+
t.style.opacity = i === this._index ? '1' : '0.6';
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
_updateHud() {
|
|
365
|
+
if (!this._hudEl) return;
|
|
366
|
+
const n = this.renderer.slideCount;
|
|
367
|
+
this._hudEl.innerHTML = `
|
|
368
|
+
<span style="opacity:0.7">Slide</span>
|
|
369
|
+
<strong>${this._index + 1}</strong>
|
|
370
|
+
<span style="opacity:0.5">of ${n}</span>
|
|
371
|
+
`;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async _updateNotes() {
|
|
375
|
+
if (!this._notesEl) return;
|
|
376
|
+
try {
|
|
377
|
+
const notes = await this.renderer.getSlideNotes(this._index);
|
|
378
|
+
this._notesEl.textContent = notes || '(no notes)';
|
|
379
|
+
} catch (_) {
|
|
380
|
+
this._notesEl.textContent = '';
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async _generateThumbnails() {
|
|
385
|
+
for (let i = 0; i < this.renderer.slideCount; i++) {
|
|
386
|
+
if (!this._playing) break;
|
|
387
|
+
const thumb = this._thumbnails[i];
|
|
388
|
+
if (!thumb) continue;
|
|
389
|
+
try {
|
|
390
|
+
await this.renderer.renderSlide(i, thumb, thumb.width);
|
|
391
|
+
} catch (_) {}
|
|
392
|
+
// Small delay to avoid blocking the main render
|
|
393
|
+
await new Promise(r => setTimeout(r, 50));
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// ── Keyboard & touch ────────────────────────────────────────────────────────
|
|
398
|
+
|
|
399
|
+
_attachEvents() {
|
|
400
|
+
document.addEventListener('keydown', this._onKey);
|
|
401
|
+
window.addEventListener('resize', this._onResize);
|
|
402
|
+
document.addEventListener('fullscreenchange', this._onFsChange);
|
|
403
|
+
if (this._el) {
|
|
404
|
+
this._el.addEventListener('touchstart', this._onTouchStart, { passive: true });
|
|
405
|
+
this._el.addEventListener('touchend', this._onTouchEnd, { passive: true });
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
_detachEvents() {
|
|
410
|
+
document.removeEventListener('keydown', this._onKey);
|
|
411
|
+
window.removeEventListener('resize', this._onResize);
|
|
412
|
+
document.removeEventListener('fullscreenchange', this._onFsChange);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
_onKey(e) {
|
|
416
|
+
if (!this._playing) return;
|
|
417
|
+
switch (e.key) {
|
|
418
|
+
case 'ArrowRight': case 'ArrowDown': case ' ': case 'PageDown':
|
|
419
|
+
e.preventDefault(); this.next(); break;
|
|
420
|
+
case 'ArrowLeft': case 'ArrowUp': case 'PageUp': case 'Backspace':
|
|
421
|
+
e.preventDefault(); this.prev(); break;
|
|
422
|
+
case 'Home': e.preventDefault(); this.goto(0); break;
|
|
423
|
+
case 'End': e.preventDefault(); this.goto(this.renderer.slideCount - 1); break;
|
|
424
|
+
case 'Escape': case 'q': case 'Q':
|
|
425
|
+
e.preventDefault(); this.stop(); break;
|
|
426
|
+
case 'f': case 'F':
|
|
427
|
+
e.preventDefault(); this._toggleFullscreen(); break;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
_onResize() {
|
|
432
|
+
this._resizeCanvas();
|
|
433
|
+
this._renderCurrent(); // re-render at new size
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
_onFsChange() {
|
|
437
|
+
if (!document.fullscreenElement) {
|
|
438
|
+
// User pressed Esc in fullscreen — stop show
|
|
439
|
+
this.stop();
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
_onTouchStart(e) {
|
|
444
|
+
this._touchStartX = e.touches[0].clientX;
|
|
445
|
+
this._touchStartY = e.touches[0].clientY;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
_onTouchEnd(e) {
|
|
449
|
+
const dx = e.changedTouches[0].clientX - this._touchStartX;
|
|
450
|
+
const dy = e.changedTouches[0].clientY - this._touchStartY;
|
|
451
|
+
if (Math.abs(dx) > 40 && Math.abs(dx) > Math.abs(dy)) {
|
|
452
|
+
if (dx < 0) this.next(); else this.prev();
|
|
453
|
+
} else if (Math.abs(dx) < 10 && Math.abs(dy) < 10) {
|
|
454
|
+
this.next(); // tap to advance
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// ── Fullscreen ──────────────────────────────────────────────────────────────
|
|
459
|
+
|
|
460
|
+
async _requestFullscreen() {
|
|
461
|
+
try {
|
|
462
|
+
const el = this._el;
|
|
463
|
+
if (el.requestFullscreen) await el.requestFullscreen();
|
|
464
|
+
else if (el.webkitRequestFullscreen) await el.webkitRequestFullscreen();
|
|
465
|
+
} catch (_) { /* ignore if not allowed */ }
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
async _toggleFullscreen() {
|
|
469
|
+
if (document.fullscreenElement) {
|
|
470
|
+
await document.exitFullscreen().catch(() => {});
|
|
471
|
+
} else {
|
|
472
|
+
await this._requestFullscreen();
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// ── Auto-advance ─────────────────────────────────────────────────────────────
|
|
477
|
+
|
|
478
|
+
_startAutoAdvance() {
|
|
479
|
+
this._stopAutoAdvance();
|
|
480
|
+
if (this.opts.autoAdvance > 0) {
|
|
481
|
+
this._autoTimer = setInterval(() => this.next(), this.opts.autoAdvance);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
_stopAutoAdvance() {
|
|
486
|
+
if (this._autoTimer) { clearInterval(this._autoTimer); this._autoTimer = null; }
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
_resetAutoAdvance() {
|
|
490
|
+
if (this.opts.autoAdvance > 0) this._startAutoAdvance();
|
|
491
|
+
}
|
|
492
|
+
}
|