pixi-effects 0.12.0 → 0.13.1

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: 7px; height: 7px; transform: translate(-50%, -50%); border-radius: 50%; background: var(--mc-fg, #fff); box-shadow: 0 0 0 1.5px rgba(0,0,0,0.5); }\n.mc-stop-page { width: 11px; height: 11px; box-shadow: 0 0 0 2.5px var(--mc-accent, #007AFF), 0 0 0 4px rgba(0,0,0,0.35); }\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,8 @@ 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: 7px; height: 7px; transform: translate(-50%, -50%); border-radius: 50%; background: var(--mc-fg, #fff); box-shadow: 0 0 0 1.5px rgba(0,0,0,0.5); }
51
+ .mc-stop-page { width: 11px; height: 11px; box-shadow: 0 0 0 2.5px var(--mc-accent, #007AFF), 0 0 0 4px rgba(0,0,0,0.35); }
51
52
  .mc-progress-fill {
52
53
  position: absolute; left: 12px; top: 50%;
53
54
  height: var(--mc-track-height, 3px);
@@ -269,8 +270,10 @@ function pxToFraction(clientX, rect, inset = 0) {
269
270
  const ratio = (clientX - innerLeft) / innerWidth;
270
271
  return Math.min(Math.max(ratio, 0), 1);
271
272
  }
273
+ var PDF_QUALITY = { "low": 0.6, "medium": 0.75, "high": 0.88, "very-high": 0.95 };
272
274
  function extensionForMimeType(mime) {
273
275
  const t = (mime || "").toLowerCase();
276
+ if (t.includes("pdf")) return "pdf";
274
277
  if (t.includes("webm")) return "webm";
275
278
  if (t.includes("quicktime") || t.includes("mov")) return "mov";
276
279
  if (t.includes("matroska") || t.includes("mkv")) return "mkv";
@@ -317,6 +320,7 @@ var Controller = class _Controller {
317
320
  wasPlayingBeforeScrub = false;
318
321
  activePointerId = null;
319
322
  isExporting = false;
323
+ exportPages = null;
320
324
  exportOverlay = null;
321
325
  exportFillEl = null;
322
326
  exportTextEl = null;
@@ -357,6 +361,7 @@ var Controller = class _Controller {
357
361
  enableKeyboardShortcuts: options.enableKeyboardShortcuts ?? true,
358
362
  clickToPlay: options.clickToPlay ?? true,
359
363
  present: options.present ?? true,
364
+ pauseAtStops: options.pauseAtStops ?? true,
360
365
  className: options.className ?? "movie-controller"
361
366
  };
362
367
  installStyles();
@@ -564,6 +569,7 @@ var Controller = class _Controller {
564
569
  refreshStops() {
565
570
  this.progressEl.querySelector(".mc-stops")?.remove();
566
571
  const stops = this.movie.stops ?? [];
572
+ this.refreshPdfChoices(stops.length > 0);
567
573
  if (stops.length === 0) {
568
574
  this.presentBtn?.remove();
569
575
  this.presentBtn = null;
@@ -574,7 +580,7 @@ var Controller = class _Controller {
574
580
  holder.className = "mc-stops";
575
581
  for (const s of stops) {
576
582
  const tick = document.createElement("div");
577
- tick.className = "mc-stop-tick";
583
+ tick.className = s.pageStart ? "mc-stop-tick mc-stop-page" : "mc-stop-tick";
578
584
  tick.style.left = `${s.frame / total * 100}%`;
579
585
  if (s.page) tick.title = s.page;
580
586
  holder.appendChild(tick);
@@ -593,6 +599,41 @@ var Controller = class _Controller {
593
599
  this.presentBtn = btn;
594
600
  }
595
601
  }
602
+ /** A deck can be downloaded as a PDF: the format list gets "PDF" choices while the movie has stops. */
603
+ refreshPdfChoices(has) {
604
+ const select = this.settingsFormatSelect;
605
+ if (!select) return;
606
+ const present = !!select.querySelector('option[value="pdf"]');
607
+ if (has && !present) {
608
+ for (const [value, label] of [["pdf", "PDF (pages)"], ["pdf-steps", "PDF (every step)"]]) {
609
+ const o = document.createElement("option");
610
+ o.value = value;
611
+ o.textContent = label;
612
+ select.appendChild(o);
613
+ }
614
+ } else if (!has && present) {
615
+ select.querySelectorAll('option[value="pdf"], option[value="pdf-steps"]').forEach((o) => o.remove());
616
+ if (this.exportFormat === "pdf" || this.exportFormat === "pdf-steps") {
617
+ this.exportFormat = "mp4";
618
+ select.value = "mp4";
619
+ }
620
+ }
621
+ }
622
+ /** Play, or pause: a movie with stops plays to the next stop (and from the end, again from the start). */
623
+ async togglePlay() {
624
+ if (this.movie.isPlaying) {
625
+ this.movie.pause();
626
+ return;
627
+ }
628
+ await this.startPlaying();
629
+ }
630
+ /** Start playing: to the next stop for a movie with stops, to the end for any other (or with `pauseAtStops: false`). */
631
+ async startPlaying() {
632
+ if (this.options.pauseAtStops && (this.movie.stops?.length ?? 0) > 0) {
633
+ if (this.movie.currentFrame >= this.movie.totalFrames) await this.movie.gotoFrame(0, true);
634
+ await this.movie.next();
635
+ } else this.movie.play();
636
+ }
596
637
  /** Hand the page over to a Presenter (fullscreen, keys, click and swipe move through the stops); Escape gives it back. */
597
638
  async startPresenting() {
598
639
  if (this.presenter || this.destroyed) return;
@@ -609,11 +650,7 @@ var Controller = class _Controller {
609
650
  }
610
651
  bindPlayButton() {
611
652
  this.playBtn.addEventListener("click", () => {
612
- if (this.movie.isPlaying) {
613
- this.movie.pause();
614
- } else {
615
- this.movie.play();
616
- }
653
+ void this.togglePlay();
617
654
  });
618
655
  }
619
656
  /** Clicking the picture plays / pauses (a tap on a phone too). */
@@ -630,8 +667,7 @@ var Controller = class _Controller {
630
667
  this.swallowCanvasClick = false;
631
668
  return;
632
669
  }
633
- if (this.movie.isPlaying) this.movie.pause();
634
- else this.movie.play();
670
+ void this.togglePlay();
635
671
  };
636
672
  canvas.addEventListener("pointerdown", this.canvasPointerDownHandler);
637
673
  canvas.addEventListener("click", this.canvasClickHandler);
@@ -787,7 +823,7 @@ var Controller = class _Controller {
787
823
  } catch {
788
824
  }
789
825
  if (this.wasPlayingBeforeScrub) {
790
- this.movie.play();
826
+ void this.startPlaying();
791
827
  this.refreshPlayIcon();
792
828
  }
793
829
  };
@@ -836,7 +872,7 @@ var Controller = class _Controller {
836
872
  if (this.settingsFormatSelect) {
837
873
  this.settingsFormatSelect.addEventListener("change", (e) => {
838
874
  const v = e.target.value;
839
- if (v === "mp4" || v === "webm" || v === "mov") this.exportFormat = v;
875
+ if (v === "mp4" || v === "webm" || v === "mov" || v === "pdf" || v === "pdf-steps") this.exportFormat = v;
840
876
  });
841
877
  }
842
878
  if (this.settingsQualitySelect) {
@@ -886,9 +922,19 @@ var Controller = class _Controller {
886
922
  this.movie.pause();
887
923
  this.refreshPlayIcon();
888
924
  }
889
- this.showExportOverlay();
925
+ const pdf = this.exportFormat === "pdf" || this.exportFormat === "pdf-steps";
926
+ if (pdf) {
927
+ const stops = this.movie.stops ?? [];
928
+ const total = this.exportFormat === "pdf-steps" ? stops.filter((s) => s.pdf !== false).length : new Set(stops.map((s) => s.pageIndex)).size;
929
+ this.showExportOverlay("Exporting PDF...", `0% (0 / ${total} pages)`);
930
+ this.exportPages = { done: 0, total };
931
+ } else this.showExportOverlay();
890
932
  try {
891
- const blob = await this.movie.render({
933
+ const blob = pdf ? await this.movie.exportPDF({
934
+ which: this.exportFormat === "pdf-steps" ? "stops" : "pages",
935
+ quality: PDF_QUALITY[this.exportQuality],
936
+ onImage: () => this.pdfPageDone()
937
+ }) : await this.movie.render({
892
938
  format: this.exportFormat,
893
939
  video: { bitrate: this.exportQuality },
894
940
  audio: { bitrate: this.exportQuality }
@@ -904,7 +950,7 @@ var Controller = class _Controller {
904
950
  this.hideExportOverlay();
905
951
  this.isExporting = false;
906
952
  if (wasPlaying) {
907
- this.movie.play();
953
+ void this.startPlaying();
908
954
  this.refreshPlayIcon();
909
955
  }
910
956
  }
@@ -927,16 +973,27 @@ var Controller = class _Controller {
927
973
  const ts = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
928
974
  return `movie-${ts}.${ext}`;
929
975
  }
930
- showExportOverlay() {
976
+ /** One more PDF page is ready: move the bar. */
977
+ pdfPageDone() {
978
+ const p = this.exportPages;
979
+ if (!p || !this.exportFillEl || !this.exportTextEl) return;
980
+ p.done++;
981
+ const percent = p.total ? Math.min(100, Math.round(p.done / p.total * 100)) : 100;
982
+ this.exportFillEl.style.width = `${percent}%`;
983
+ this.exportTextEl.textContent = `${percent}% (${p.done} / ${p.total} pages)`;
984
+ }
985
+ showExportOverlay(title = "Exporting video...", text = "0% (0 / 0 frames)") {
931
986
  const overlay = document.createElement("div");
932
987
  overlay.className = "mc-export-overlay";
933
988
  overlay.innerHTML = `
934
989
  <div class="mc-export-panel">
935
- <div class="mc-export-title">Exporting video...</div>
990
+ <div class="mc-export-title"></div>
936
991
  <div class="mc-export-track"><div class="mc-export-fill"></div></div>
937
- <div class="mc-export-text">0% (0 / 0 frames)</div>
992
+ <div class="mc-export-text"></div>
938
993
  </div>
939
994
  `;
995
+ overlay.querySelector(".mc-export-title").textContent = title;
996
+ overlay.querySelector(".mc-export-text").textContent = text;
940
997
  this.root.appendChild(overlay);
941
998
  this.exportOverlay = overlay;
942
999
  this.exportFillEl = overlay.querySelector(".mc-export-fill");
@@ -949,6 +1006,7 @@ var Controller = class _Controller {
949
1006
  this.exportFillEl = null;
950
1007
  this.exportTextEl = null;
951
1008
  }
1009
+ this.exportPages = null;
952
1010
  }
953
1011
  bindVisibility() {
954
1012
  const wrap = this.wrapper;