pixi-effects 0.12.0 → 0.13.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.
@@ -1,10 +1,10 @@
1
- import { f as Movie } from './Movie-EBF6_RfX.js';
1
+ import { f as Movie } from './Movie-DnR4H1OO.js';
2
2
  import 'pixi.js';
3
3
  import 'gsap';
4
- import './types-mpqQO8MJ.js';
4
+ import './types-CBSiUKau.js';
5
5
 
6
6
  /** The bar's default stylesheet. Its colours and sizes are CSS custom properties (`--mc-accent`, `--mc-fg`, `--mc-track`, `--mc-bar-bg`, `--mc-track-height`, `--mc-font`): set them in your page CSS, or with the `theme` option. */
7
- declare const CONTROLLER_CSS = "\n.movie-controller-wrap { position: relative; display: inline-block; line-height: 0; }\n.movie-controller {\n position: absolute;\n pointer-events: none;\n font-family: var(--mc-font, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif);\n display: flex;\n flex-direction: column;\n justify-content: flex-end;\n opacity: 1;\n transition: opacity 200ms ease;\n}\n.movie-controller[data-state=\"hidden\"] { opacity: 0; }\n.movie-controller > * { pointer-events: auto; }\n\n.mc-progress {\n position: relative;\n width: 100%;\n height: 16px;\n cursor: pointer;\n display: flex;\n align-items: center;\n outline: none;\n}\n.mc-progress:focus-visible {\n outline: 2px solid var(--mc-accent, #007AFF);\n outline-offset: 2px;\n}\n.mc-progress::before {\n content: \"\"; position: absolute; left: 12px; right: 12px;\n height: var(--mc-track-height, 3px); background: var(--mc-track, rgba(255,255,255,0.25));\n transition: height 120ms ease;\n}\n.mc-progress:hover::before, .mc-progress.mc-scrubbing::before { height: calc(var(--mc-track-height, 3px) + 2px); }\n.mc-stops { position: absolute; left: 12px; right: 12px; top: 0; bottom: 0; pointer-events: none; }\n.mc-stop-tick { position: absolute; top: 50%; width: 2px; height: calc(var(--mc-track-height, 3px) + 6px); transform: translate(-50%, -50%); background: var(--mc-fg, #fff); opacity: 0.6; border-radius: 1px; }\n.mc-progress-fill {\n position: absolute; left: 12px; top: 50%;\n height: var(--mc-track-height, 3px);\n width: calc((100% - 24px) * var(--mc-fill, 0));\n background: var(--mc-accent, #007AFF);\n transform: translateY(-50%);\n transition: height 120ms ease;\n pointer-events: none;\n}\n.mc-progress:hover .mc-progress-fill,\n.mc-progress.mc-scrubbing .mc-progress-fill { height: calc(var(--mc-track-height, 3px) + 2px); }\n.mc-progress-thumb {\n position: absolute; top: 50%;\n left: calc(12px + (100% - 24px) * var(--mc-fill, 0));\n width: 12px; height: 12px; border-radius: 50%;\n background: var(--mc-accent, #007AFF);\n transform: translate(-50%, -50%) scale(0);\n transition: transform 120ms ease;\n pointer-events: none;\n}\n.mc-progress:hover .mc-progress-thumb,\n.mc-progress.mc-scrubbing .mc-progress-thumb { transform: translate(-50%, -50%) scale(1); }\n\n.mc-bar {\n position: relative;\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 6px 12px 8px 12px;\n background: var(--mc-bar-bg, linear-gradient(to top, rgba(0,0,0,0.75) 0%, rgba(0,0,0,0) 100%));\n color: var(--mc-fg, #fff);\n line-height: 1;\n}\n.mc-btn {\n background: none; border: 0; padding: 0; margin: 0;\n width: 28px; height: 28px;\n display: inline-flex; align-items: center; justify-content: center;\n color: var(--mc-fg, #fff); opacity: 0.85; cursor: pointer;\n transition: opacity 120ms ease;\n}\n.mc-btn:hover { opacity: 1; }\n.mc-btn:disabled { opacity: 0.4; cursor: not-allowed; }\n.mc-time {\n font-size: 12px; font-family: Menlo, Monaco, monospace;\n color: var(--mc-fg, #fff); opacity: 0.85;\n min-width: 90px;\n}\n.mc-spacer { flex: 1; }\n\n.mc-volume { display: flex; align-items: center; }\n.mc-vol-slider {\n position: relative;\n width: 0;\n height: 28px;\n display: flex;\n align-items: center;\n cursor: pointer;\n margin-left: 0;\n outline: none;\n transition: width 200ms ease, margin-left 200ms ease;\n overflow: hidden;\n}\n.mc-volume:hover .mc-vol-slider,\n.mc-volume:focus-within .mc-vol-slider,\n.mc-vol-slider.mc-scrubbing {\n width: 70px;\n margin-left: 6px;\n}\n.mc-vol-slider:focus-visible { outline: 2px solid var(--mc-accent, #007AFF); outline-offset: 2px; }\n.mc-vol-slider::before {\n content: \"\"; position: absolute; left: 4px; right: 4px;\n height: 3px; background: var(--mc-track, rgba(255,255,255,0.25)); border-radius: 2px;\n}\n.mc-vol-fill {\n position: absolute; left: 4px; top: 50%;\n height: 3px;\n width: calc((100% - 8px) * var(--mc-volume, 1));\n background: var(--mc-fg, #fff);\n transform: translateY(-50%);\n border-radius: 2px;\n pointer-events: none;\n}\n.mc-vol-thumb {\n position: absolute; top: 50%;\n left: calc(4px + (100% - 8px) * var(--mc-volume, 1));\n width: 10px; height: 10px; border-radius: 50%;\n background: var(--mc-fg, #fff);\n transform: translate(-50%, -50%);\n pointer-events: none;\n}\n\n[data-mc-wrap]:fullscreen {\n width: 100vw; height: 100vh;\n display: flex; align-items: center; justify-content: center;\n background: #000;\n}\n[data-mc-wrap]:fullscreen > canvas {\n width: 100% !important;\n height: 100% !important;\n max-width: none !important;\n max-height: none !important;\n object-fit: contain;\n border-radius: 0;\n}\n\n.mc-settings-popover {\n position: absolute;\n bottom: calc(100% + 8px);\n right: 12px;\n background: rgba(20, 20, 20, 0.96);\n border-radius: 8px;\n padding: 10px 12px;\n min-width: 180px;\n color: #fff;\n font-size: 12px;\n box-shadow: 0 4px 16px rgba(0,0,0,0.4);\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n.mc-settings-popover[data-open=\"false\"] { display: none; }\n.mc-settings-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n}\n.mc-settings-row > span { color: rgba(255,255,255,0.85); }\n.mc-settings-row > select {\n font: inherit;\n background: rgba(255,255,255,0.1);\n color: #fff;\n border: 1px solid rgba(255,255,255,0.18);\n border-radius: 4px;\n padding: 2px 6px;\n cursor: pointer;\n}\n.mc-export-confirm {\n margin-top: 4px;\n width: 100%;\n background: var(--mc-accent, #007AFF);\n color: #fff;\n border: 0;\n border-radius: 4px;\n padding: 6px 10px;\n font: inherit;\n font-weight: 600;\n cursor: pointer;\n transition: background 120ms ease;\n}\n.mc-export-confirm:hover { background: #0066d6; }\n.mc-export-confirm:disabled { background: rgba(255,255,255,0.18); cursor: not-allowed; }\n\n.mc-export-overlay {\n position: absolute; inset: 0;\n background: rgba(0,0,0,0.7);\n display: flex; justify-content: center; align-items: center;\n z-index: 2;\n}\n.mc-export-panel {\n background: rgba(20,20,20,0.96); color: #fff;\n border-radius: 8px; padding: 16px 20px;\n text-align: center; min-width: 200px; max-width: 80%;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n}\n.mc-export-title { font-size: 13px; margin-bottom: 10px; opacity: 0.9; }\n.mc-export-track {\n width: 100%; height: 6px;\n background: rgba(255,255,255,0.15); border-radius: 3px;\n margin-bottom: 8px; overflow: hidden;\n}\n.mc-export-fill {\n height: 100%; width: 0%;\n background: linear-gradient(90deg, var(--mc-accent, #007AFF), var(--mc-accent, #0056CC));\n transition: width 0.3s ease; border-radius: 3px;\n}\n.mc-export-text {\n color: rgba(255,255,255,0.7); font-size: 11px;\n font-family: Menlo, Monaco, monospace;\n}\n";
7
+ declare const CONTROLLER_CSS = "\n.movie-controller-wrap { position: relative; display: inline-block; line-height: 0; }\n.movie-controller {\n position: absolute;\n pointer-events: none;\n font-family: var(--mc-font, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif);\n display: flex;\n flex-direction: column;\n justify-content: flex-end;\n opacity: 1;\n transition: opacity 200ms ease;\n}\n.movie-controller[data-state=\"hidden\"] { opacity: 0; }\n.movie-controller > * { pointer-events: auto; }\n\n.mc-progress {\n position: relative;\n width: 100%;\n height: 16px;\n cursor: pointer;\n display: flex;\n align-items: center;\n outline: none;\n}\n.mc-progress:focus-visible {\n outline: 2px solid var(--mc-accent, #007AFF);\n outline-offset: 2px;\n}\n.mc-progress::before {\n content: \"\"; position: absolute; left: 12px; right: 12px;\n height: var(--mc-track-height, 3px); background: var(--mc-track, rgba(255,255,255,0.25));\n transition: height 120ms ease;\n}\n.mc-progress:hover::before, .mc-progress.mc-scrubbing::before { height: calc(var(--mc-track-height, 3px) + 2px); }\n.mc-stops { position: absolute; left: 12px; right: 12px; top: 0; bottom: 0; pointer-events: none; }\n.mc-stop-tick { position: absolute; top: 50%; width: 2px; height: calc(var(--mc-track-height, 3px) + 10px); transform: translate(-50%, -50%); background: var(--mc-fg, #fff); opacity: 0.9; border-radius: 1px; box-shadow: 0 0 0 1px rgba(0,0,0,0.55); }\n.mc-stop-page { width: 4px; height: calc(var(--mc-track-height, 3px) + 20px); opacity: 1; }\n.mc-stop-page::before { content: ''; position: absolute; left: 50%; top: -3px; width: 8px; height: 8px; transform: translateX(-50%); border-radius: 50%; background: var(--mc-fg, #fff); box-shadow: 0 0 0 1px rgba(0,0,0,0.55); }\n.mc-progress-fill {\n position: absolute; left: 12px; top: 50%;\n height: var(--mc-track-height, 3px);\n width: calc((100% - 24px) * var(--mc-fill, 0));\n background: var(--mc-accent, #007AFF);\n transform: translateY(-50%);\n transition: height 120ms ease;\n pointer-events: none;\n}\n.mc-progress:hover .mc-progress-fill,\n.mc-progress.mc-scrubbing .mc-progress-fill { height: calc(var(--mc-track-height, 3px) + 2px); }\n.mc-progress-thumb {\n position: absolute; top: 50%;\n left: calc(12px + (100% - 24px) * var(--mc-fill, 0));\n width: 12px; height: 12px; border-radius: 50%;\n background: var(--mc-accent, #007AFF);\n transform: translate(-50%, -50%) scale(0);\n transition: transform 120ms ease;\n pointer-events: none;\n}\n.mc-progress:hover .mc-progress-thumb,\n.mc-progress.mc-scrubbing .mc-progress-thumb { transform: translate(-50%, -50%) scale(1); }\n\n.mc-bar {\n position: relative;\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 6px 12px 8px 12px;\n background: var(--mc-bar-bg, linear-gradient(to top, rgba(0,0,0,0.75) 0%, rgba(0,0,0,0) 100%));\n color: var(--mc-fg, #fff);\n line-height: 1;\n}\n.mc-btn {\n background: none; border: 0; padding: 0; margin: 0;\n width: 28px; height: 28px;\n display: inline-flex; align-items: center; justify-content: center;\n color: var(--mc-fg, #fff); opacity: 0.85; cursor: pointer;\n transition: opacity 120ms ease;\n}\n.mc-btn:hover { opacity: 1; }\n.mc-btn:disabled { opacity: 0.4; cursor: not-allowed; }\n.mc-time {\n font-size: 12px; font-family: Menlo, Monaco, monospace;\n color: var(--mc-fg, #fff); opacity: 0.85;\n min-width: 90px;\n}\n.mc-spacer { flex: 1; }\n\n.mc-volume { display: flex; align-items: center; }\n.mc-vol-slider {\n position: relative;\n width: 0;\n height: 28px;\n display: flex;\n align-items: center;\n cursor: pointer;\n margin-left: 0;\n outline: none;\n transition: width 200ms ease, margin-left 200ms ease;\n overflow: hidden;\n}\n.mc-volume:hover .mc-vol-slider,\n.mc-volume:focus-within .mc-vol-slider,\n.mc-vol-slider.mc-scrubbing {\n width: 70px;\n margin-left: 6px;\n}\n.mc-vol-slider:focus-visible { outline: 2px solid var(--mc-accent, #007AFF); outline-offset: 2px; }\n.mc-vol-slider::before {\n content: \"\"; position: absolute; left: 4px; right: 4px;\n height: 3px; background: var(--mc-track, rgba(255,255,255,0.25)); border-radius: 2px;\n}\n.mc-vol-fill {\n position: absolute; left: 4px; top: 50%;\n height: 3px;\n width: calc((100% - 8px) * var(--mc-volume, 1));\n background: var(--mc-fg, #fff);\n transform: translateY(-50%);\n border-radius: 2px;\n pointer-events: none;\n}\n.mc-vol-thumb {\n position: absolute; top: 50%;\n left: calc(4px + (100% - 8px) * var(--mc-volume, 1));\n width: 10px; height: 10px; border-radius: 50%;\n background: var(--mc-fg, #fff);\n transform: translate(-50%, -50%);\n pointer-events: none;\n}\n\n[data-mc-wrap]:fullscreen {\n width: 100vw; height: 100vh;\n display: flex; align-items: center; justify-content: center;\n background: #000;\n}\n[data-mc-wrap]:fullscreen > canvas {\n width: 100% !important;\n height: 100% !important;\n max-width: none !important;\n max-height: none !important;\n object-fit: contain;\n border-radius: 0;\n}\n\n.mc-settings-popover {\n position: absolute;\n bottom: calc(100% + 8px);\n right: 12px;\n background: rgba(20, 20, 20, 0.96);\n border-radius: 8px;\n padding: 10px 12px;\n min-width: 180px;\n color: #fff;\n font-size: 12px;\n box-shadow: 0 4px 16px rgba(0,0,0,0.4);\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n.mc-settings-popover[data-open=\"false\"] { display: none; }\n.mc-settings-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n}\n.mc-settings-row > span { color: rgba(255,255,255,0.85); }\n.mc-settings-row > select {\n font: inherit;\n background: rgba(255,255,255,0.1);\n color: #fff;\n border: 1px solid rgba(255,255,255,0.18);\n border-radius: 4px;\n padding: 2px 6px;\n cursor: pointer;\n}\n.mc-export-confirm {\n margin-top: 4px;\n width: 100%;\n background: var(--mc-accent, #007AFF);\n color: #fff;\n border: 0;\n border-radius: 4px;\n padding: 6px 10px;\n font: inherit;\n font-weight: 600;\n cursor: pointer;\n transition: background 120ms ease;\n}\n.mc-export-confirm:hover { background: #0066d6; }\n.mc-export-confirm:disabled { background: rgba(255,255,255,0.18); cursor: not-allowed; }\n\n.mc-export-overlay {\n position: absolute; inset: 0;\n background: rgba(0,0,0,0.7);\n display: flex; justify-content: center; align-items: center;\n z-index: 2;\n}\n.mc-export-panel {\n background: rgba(20,20,20,0.96); color: #fff;\n border-radius: 8px; padding: 16px 20px;\n text-align: center; min-width: 200px; max-width: 80%;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n}\n.mc-export-title { font-size: 13px; margin-bottom: 10px; opacity: 0.9; }\n.mc-export-track {\n width: 100%; height: 6px;\n background: rgba(255,255,255,0.15); border-radius: 3px;\n margin-bottom: 8px; overflow: hidden;\n}\n.mc-export-fill {\n height: 100%; width: 0%;\n background: linear-gradient(90deg, var(--mc-accent, #007AFF), var(--mc-accent, #0056CC));\n transition: width 0.3s ease; border-radius: 3px;\n}\n.mc-export-text {\n color: rgba(255,255,255,0.7); font-size: 11px;\n font-family: Menlo, Monaco, monospace;\n}\n";
8
8
  declare function formatTime(seconds: number): string;
9
9
  declare function frameToPercent(frame: number, totalFrames: number): number;
10
10
  declare function pxToFrame(clientX: number, rect: {
@@ -44,6 +44,8 @@ interface ControllerOptions {
44
44
  clickToPlay?: boolean;
45
45
  /** A movie with `stops` gets a Present button that starts a {@link Presenter} (keys, click and swipe move through the stops). Default true; `false` keeps only the marks on the seek bar. */
46
46
  present?: boolean;
47
+ /** A movie with `stops`: the play button (and a click on the picture, and Space) plays to the next stop and pauses there, like a slide. Default true; `false` plays straight through to the end. */
48
+ pauseAtStops?: boolean;
47
49
  className?: string;
48
50
  }
49
51
  interface ResolvedOptions {
@@ -52,6 +54,7 @@ interface ResolvedOptions {
52
54
  enableKeyboardShortcuts: boolean;
53
55
  clickToPlay: boolean;
54
56
  present: boolean;
57
+ pauseAtStops: boolean;
55
58
  className: string;
56
59
  }
57
60
  declare class Controller {
@@ -87,6 +90,7 @@ declare class Controller {
87
90
  private wasPlayingBeforeScrub;
88
91
  private activePointerId;
89
92
  private isExporting;
93
+ private exportPages;
90
94
  private exportOverlay;
91
95
  private exportFillEl;
92
96
  private exportTextEl;
@@ -132,6 +136,12 @@ declare class Controller {
132
136
  private bindMovieEvents;
133
137
  /** The stops of a presentation as marks on the seek bar, and the Present button. */
134
138
  private refreshStops;
139
+ /** A deck can be downloaded as a PDF: the format list gets "PDF" choices while the movie has stops. */
140
+ private refreshPdfChoices;
141
+ /** Play, or pause: a movie with stops plays to the next stop (and from the end, again from the start). */
142
+ private togglePlay;
143
+ /** Start playing: to the next stop for a movie with stops, to the end for any other (or with `pauseAtStops: false`). */
144
+ private startPlaying;
135
145
  /** Hand the page over to a Presenter (fullscreen, keys, click and swipe move through the stops); Escape gives it back. */
136
146
  private startPresenting;
137
147
  private stopPresenting;
@@ -160,6 +170,8 @@ declare class Controller {
160
170
  private handleExport;
161
171
  private triggerDownload;
162
172
  private makeFilename;
173
+ /** One more PDF page is ready: move the bar. */
174
+ private pdfPageDone;
163
175
  private showExportOverlay;
164
176
  private hideExportOverlay;
165
177
  private bindVisibility;
@@ -47,7 +47,9 @@ var CONTROLLER_CSS = `
47
47
  }
48
48
  .mc-progress:hover::before, .mc-progress.mc-scrubbing::before { height: calc(var(--mc-track-height, 3px) + 2px); }
49
49
  .mc-stops { position: absolute; left: 12px; right: 12px; top: 0; bottom: 0; pointer-events: none; }
50
- .mc-stop-tick { position: absolute; top: 50%; width: 2px; height: calc(var(--mc-track-height, 3px) + 6px); transform: translate(-50%, -50%); background: var(--mc-fg, #fff); opacity: 0.6; border-radius: 1px; }
50
+ .mc-stop-tick { position: absolute; top: 50%; width: 2px; height: calc(var(--mc-track-height, 3px) + 10px); transform: translate(-50%, -50%); background: var(--mc-fg, #fff); opacity: 0.9; border-radius: 1px; box-shadow: 0 0 0 1px rgba(0,0,0,0.55); }
51
+ .mc-stop-page { width: 4px; height: calc(var(--mc-track-height, 3px) + 20px); opacity: 1; }
52
+ .mc-stop-page::before { content: ''; position: absolute; left: 50%; top: -3px; width: 8px; height: 8px; transform: translateX(-50%); border-radius: 50%; background: var(--mc-fg, #fff); box-shadow: 0 0 0 1px rgba(0,0,0,0.55); }
51
53
  .mc-progress-fill {
52
54
  position: absolute; left: 12px; top: 50%;
53
55
  height: var(--mc-track-height, 3px);
@@ -269,8 +271,10 @@ function pxToFraction(clientX, rect, inset = 0) {
269
271
  const ratio = (clientX - innerLeft) / innerWidth;
270
272
  return Math.min(Math.max(ratio, 0), 1);
271
273
  }
274
+ var PDF_QUALITY = { "low": 0.6, "medium": 0.75, "high": 0.88, "very-high": 0.95 };
272
275
  function extensionForMimeType(mime) {
273
276
  const t = (mime || "").toLowerCase();
277
+ if (t.includes("pdf")) return "pdf";
274
278
  if (t.includes("webm")) return "webm";
275
279
  if (t.includes("quicktime") || t.includes("mov")) return "mov";
276
280
  if (t.includes("matroska") || t.includes("mkv")) return "mkv";
@@ -317,6 +321,7 @@ var Controller = class _Controller {
317
321
  wasPlayingBeforeScrub = false;
318
322
  activePointerId = null;
319
323
  isExporting = false;
324
+ exportPages = null;
320
325
  exportOverlay = null;
321
326
  exportFillEl = null;
322
327
  exportTextEl = null;
@@ -357,6 +362,7 @@ var Controller = class _Controller {
357
362
  enableKeyboardShortcuts: options.enableKeyboardShortcuts ?? true,
358
363
  clickToPlay: options.clickToPlay ?? true,
359
364
  present: options.present ?? true,
365
+ pauseAtStops: options.pauseAtStops ?? true,
360
366
  className: options.className ?? "movie-controller"
361
367
  };
362
368
  installStyles();
@@ -564,6 +570,7 @@ var Controller = class _Controller {
564
570
  refreshStops() {
565
571
  this.progressEl.querySelector(".mc-stops")?.remove();
566
572
  const stops = this.movie.stops ?? [];
573
+ this.refreshPdfChoices(stops.length > 0);
567
574
  if (stops.length === 0) {
568
575
  this.presentBtn?.remove();
569
576
  this.presentBtn = null;
@@ -574,7 +581,7 @@ var Controller = class _Controller {
574
581
  holder.className = "mc-stops";
575
582
  for (const s of stops) {
576
583
  const tick = document.createElement("div");
577
- tick.className = "mc-stop-tick";
584
+ tick.className = s.pageStart ? "mc-stop-tick mc-stop-page" : "mc-stop-tick";
578
585
  tick.style.left = `${s.frame / total * 100}%`;
579
586
  if (s.page) tick.title = s.page;
580
587
  holder.appendChild(tick);
@@ -593,6 +600,41 @@ var Controller = class _Controller {
593
600
  this.presentBtn = btn;
594
601
  }
595
602
  }
603
+ /** A deck can be downloaded as a PDF: the format list gets "PDF" choices while the movie has stops. */
604
+ refreshPdfChoices(has) {
605
+ const select = this.settingsFormatSelect;
606
+ if (!select) return;
607
+ const present = !!select.querySelector('option[value="pdf"]');
608
+ if (has && !present) {
609
+ for (const [value, label] of [["pdf", "PDF (pages)"], ["pdf-steps", "PDF (every step)"]]) {
610
+ const o = document.createElement("option");
611
+ o.value = value;
612
+ o.textContent = label;
613
+ select.appendChild(o);
614
+ }
615
+ } else if (!has && present) {
616
+ select.querySelectorAll('option[value="pdf"], option[value="pdf-steps"]').forEach((o) => o.remove());
617
+ if (this.exportFormat === "pdf" || this.exportFormat === "pdf-steps") {
618
+ this.exportFormat = "mp4";
619
+ select.value = "mp4";
620
+ }
621
+ }
622
+ }
623
+ /** Play, or pause: a movie with stops plays to the next stop (and from the end, again from the start). */
624
+ async togglePlay() {
625
+ if (this.movie.isPlaying) {
626
+ this.movie.pause();
627
+ return;
628
+ }
629
+ await this.startPlaying();
630
+ }
631
+ /** Start playing: to the next stop for a movie with stops, to the end for any other (or with `pauseAtStops: false`). */
632
+ async startPlaying() {
633
+ if (this.options.pauseAtStops && (this.movie.stops?.length ?? 0) > 0) {
634
+ if (this.movie.currentFrame >= this.movie.totalFrames) await this.movie.gotoFrame(0, true);
635
+ await this.movie.next();
636
+ } else this.movie.play();
637
+ }
596
638
  /** Hand the page over to a Presenter (fullscreen, keys, click and swipe move through the stops); Escape gives it back. */
597
639
  async startPresenting() {
598
640
  if (this.presenter || this.destroyed) return;
@@ -609,11 +651,7 @@ var Controller = class _Controller {
609
651
  }
610
652
  bindPlayButton() {
611
653
  this.playBtn.addEventListener("click", () => {
612
- if (this.movie.isPlaying) {
613
- this.movie.pause();
614
- } else {
615
- this.movie.play();
616
- }
654
+ void this.togglePlay();
617
655
  });
618
656
  }
619
657
  /** Clicking the picture plays / pauses (a tap on a phone too). */
@@ -630,8 +668,7 @@ var Controller = class _Controller {
630
668
  this.swallowCanvasClick = false;
631
669
  return;
632
670
  }
633
- if (this.movie.isPlaying) this.movie.pause();
634
- else this.movie.play();
671
+ void this.togglePlay();
635
672
  };
636
673
  canvas.addEventListener("pointerdown", this.canvasPointerDownHandler);
637
674
  canvas.addEventListener("click", this.canvasClickHandler);
@@ -787,7 +824,7 @@ var Controller = class _Controller {
787
824
  } catch {
788
825
  }
789
826
  if (this.wasPlayingBeforeScrub) {
790
- this.movie.play();
827
+ void this.startPlaying();
791
828
  this.refreshPlayIcon();
792
829
  }
793
830
  };
@@ -836,7 +873,7 @@ var Controller = class _Controller {
836
873
  if (this.settingsFormatSelect) {
837
874
  this.settingsFormatSelect.addEventListener("change", (e) => {
838
875
  const v = e.target.value;
839
- if (v === "mp4" || v === "webm" || v === "mov") this.exportFormat = v;
876
+ if (v === "mp4" || v === "webm" || v === "mov" || v === "pdf" || v === "pdf-steps") this.exportFormat = v;
840
877
  });
841
878
  }
842
879
  if (this.settingsQualitySelect) {
@@ -886,9 +923,19 @@ var Controller = class _Controller {
886
923
  this.movie.pause();
887
924
  this.refreshPlayIcon();
888
925
  }
889
- this.showExportOverlay();
926
+ const pdf = this.exportFormat === "pdf" || this.exportFormat === "pdf-steps";
927
+ if (pdf) {
928
+ const stops = this.movie.stops ?? [];
929
+ const total = this.exportFormat === "pdf-steps" ? stops.filter((s) => s.pdf !== false).length : new Set(stops.map((s) => s.pageIndex)).size;
930
+ this.showExportOverlay("Exporting PDF...", `0% (0 / ${total} pages)`);
931
+ this.exportPages = { done: 0, total };
932
+ } else this.showExportOverlay();
890
933
  try {
891
- const blob = await this.movie.render({
934
+ const blob = pdf ? await this.movie.exportPDF({
935
+ which: this.exportFormat === "pdf-steps" ? "stops" : "pages",
936
+ quality: PDF_QUALITY[this.exportQuality],
937
+ onImage: () => this.pdfPageDone()
938
+ }) : await this.movie.render({
892
939
  format: this.exportFormat,
893
940
  video: { bitrate: this.exportQuality },
894
941
  audio: { bitrate: this.exportQuality }
@@ -904,7 +951,7 @@ var Controller = class _Controller {
904
951
  this.hideExportOverlay();
905
952
  this.isExporting = false;
906
953
  if (wasPlaying) {
907
- this.movie.play();
954
+ void this.startPlaying();
908
955
  this.refreshPlayIcon();
909
956
  }
910
957
  }
@@ -927,16 +974,27 @@ var Controller = class _Controller {
927
974
  const ts = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
928
975
  return `movie-${ts}.${ext}`;
929
976
  }
930
- showExportOverlay() {
977
+ /** One more PDF page is ready: move the bar. */
978
+ pdfPageDone() {
979
+ const p = this.exportPages;
980
+ if (!p || !this.exportFillEl || !this.exportTextEl) return;
981
+ p.done++;
982
+ const percent = p.total ? Math.min(100, Math.round(p.done / p.total * 100)) : 100;
983
+ this.exportFillEl.style.width = `${percent}%`;
984
+ this.exportTextEl.textContent = `${percent}% (${p.done} / ${p.total} pages)`;
985
+ }
986
+ showExportOverlay(title = "Exporting video...", text = "0% (0 / 0 frames)") {
931
987
  const overlay = document.createElement("div");
932
988
  overlay.className = "mc-export-overlay";
933
989
  overlay.innerHTML = `
934
990
  <div class="mc-export-panel">
935
- <div class="mc-export-title">Exporting video...</div>
991
+ <div class="mc-export-title"></div>
936
992
  <div class="mc-export-track"><div class="mc-export-fill"></div></div>
937
- <div class="mc-export-text">0% (0 / 0 frames)</div>
993
+ <div class="mc-export-text"></div>
938
994
  </div>
939
995
  `;
996
+ overlay.querySelector(".mc-export-title").textContent = title;
997
+ overlay.querySelector(".mc-export-text").textContent = text;
940
998
  this.root.appendChild(overlay);
941
999
  this.exportOverlay = overlay;
942
1000
  this.exportFillEl = overlay.querySelector(".mc-export-fill");
@@ -949,6 +1007,7 @@ var Controller = class _Controller {
949
1007
  this.exportFillEl = null;
950
1008
  this.exportTextEl = null;
951
1009
  }
1010
+ this.exportPages = null;
952
1011
  }
953
1012
  bindVisibility() {
954
1013
  const wrap = this.wrapper;