pixi-effects 0.11.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +11 -8
  3. package/ai/SKILL.md +2 -1
  4. package/ai/reference/cheatsheet.md +12 -1
  5. package/ai/reference/pitfalls.md +4 -0
  6. package/ai/reference/recipes.md +57 -0
  7. package/ai/template.html +8 -4
  8. package/ai/tools/check.mjs +17 -1
  9. package/ai/tools/render.mjs +13 -9
  10. package/dist/{Base-CwUxTfja.d.cts → Base-C6ZnJ9pD.d.cts} +1 -1
  11. package/dist/{Base-CyvSLoCP.d.ts → Base-CQOlB2VH.d.ts} +1 -1
  12. package/dist/Controller.cjs +132 -15
  13. package/dist/Controller.cjs.map +1 -1
  14. package/dist/Controller.d.cts +25 -3
  15. package/dist/Controller.d.ts +25 -3
  16. package/dist/Controller.js +132 -15
  17. package/dist/Controller.js.map +1 -1
  18. package/dist/{Movie-DSSNOF3T.d.cts → Movie-DnR4H1OO.d.ts} +93 -2
  19. package/dist/{Movie-nzmWsxWX.d.ts → Movie-Tl5dRLJl.d.cts} +93 -2
  20. package/dist/Presenter.cjs +671 -0
  21. package/dist/Presenter.cjs.map +1 -0
  22. package/dist/Presenter.d.cts +123 -0
  23. package/dist/Presenter.d.ts +123 -0
  24. package/dist/Presenter.js +669 -0
  25. package/dist/Presenter.js.map +1 -0
  26. package/dist/index.cjs +665 -36
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.cts +165 -5
  29. package/dist/index.d.ts +165 -5
  30. package/dist/index.js +659 -37
  31. package/dist/index.js.map +1 -1
  32. package/dist/loader.css +1 -1
  33. package/dist/three.d.cts +2 -2
  34. package/dist/three.d.ts +2 -2
  35. package/dist/{types-BXu28nJj.d.cts → types-CBSiUKau.d.cts} +56 -1
  36. package/dist/{types-BXu28nJj.d.ts → types-CBSiUKau.d.ts} +56 -1
  37. package/docs/api.md +80 -0
  38. package/docs/dsl.md +48 -0
  39. package/llms-full.txt +203 -2
  40. package/llms.txt +3 -3
  41. package/package.json +5 -1
@@ -10,6 +10,7 @@ var ICONS = {
10
10
  volumeOff: '<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M2 6 H5 L9 2 V14 L5 10 H2 Z"/><path d="M11 5 L15 11 M15 5 L11 11" fill="none" stroke="currentColor" stroke-width="1.4"/></svg>',
11
11
  download: '<svg viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2 V11 M4 7 L8 11 L12 7 M3 13 H13"/></svg>',
12
12
  fullscreenEnter: '<svg viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M2 6 V2 H6 M14 6 V2 H10 M2 10 V14 H6 M14 10 V14 H10"/></svg>',
13
+ present: '<svg viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M2 2.5 H14 V10.5 H2 Z M8 10.5 V13.5 M5 13.5 H11"/></svg>',
13
14
  fullscreenExit: '<svg viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2 V6 H2 M10 2 V6 H14 M6 14 V10 H2 M10 14 V10 H14"/></svg>'
14
15
  };
15
16
  var STYLE_ATTR = "data-movie-controller";
@@ -47,6 +48,10 @@ var CONTROLLER_CSS = `
47
48
  transition: height 120ms ease;
48
49
  }
49
50
  .mc-progress:hover::before, .mc-progress.mc-scrubbing::before { height: calc(var(--mc-track-height, 3px) + 2px); }
51
+ .mc-stops { position: absolute; left: 12px; right: 12px; top: 0; bottom: 0; pointer-events: none; }
52
+ .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); }
53
+ .mc-stop-page { width: 4px; height: calc(var(--mc-track-height, 3px) + 20px); opacity: 1; }
54
+ .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); }
50
55
  .mc-progress-fill {
51
56
  position: absolute; left: 12px; top: 50%;
52
57
  height: var(--mc-track-height, 3px);
@@ -268,8 +273,10 @@ function pxToFraction(clientX, rect, inset = 0) {
268
273
  const ratio = (clientX - innerLeft) / innerWidth;
269
274
  return Math.min(Math.max(ratio, 0), 1);
270
275
  }
276
+ var PDF_QUALITY = { "low": 0.6, "medium": 0.75, "high": 0.88, "very-high": 0.95 };
271
277
  function extensionForMimeType(mime) {
272
278
  const t = (mime || "").toLowerCase();
279
+ if (t.includes("pdf")) return "pdf";
273
280
  if (t.includes("webm")) return "webm";
274
281
  if (t.includes("quicktime") || t.includes("mov")) return "mov";
275
282
  if (t.includes("matroska") || t.includes("mkv")) return "mkv";
@@ -316,6 +323,7 @@ var Controller = class _Controller {
316
323
  wasPlayingBeforeScrub = false;
317
324
  activePointerId = null;
318
325
  isExporting = false;
326
+ exportPages = null;
319
327
  exportOverlay = null;
320
328
  exportFillEl = null;
321
329
  exportTextEl = null;
@@ -323,6 +331,8 @@ var Controller = class _Controller {
323
331
  settingsKeyHandler = null;
324
332
  settingsOutsideHandler = null;
325
333
  onReady = null;
334
+ presenter = null;
335
+ presentBtn = null;
326
336
  onFrame = null;
327
337
  onPlay = null;
328
338
  onPause = null;
@@ -353,6 +363,8 @@ var Controller = class _Controller {
353
363
  showExportButton: options.showExportButton ?? true,
354
364
  enableKeyboardShortcuts: options.enableKeyboardShortcuts ?? true,
355
365
  clickToPlay: options.clickToPlay ?? true,
366
+ present: options.present ?? true,
367
+ pauseAtStops: options.pauseAtStops ?? true,
356
368
  className: options.className ?? "movie-controller"
357
369
  };
358
370
  installStyles();
@@ -371,6 +383,7 @@ var Controller = class _Controller {
371
383
  }
372
384
  this.root.setAttribute("data-state", "visible");
373
385
  this.bindMovieEvents();
386
+ this.refreshStops();
374
387
  this.bindPlayButton();
375
388
  if (this.options.clickToPlay) this.bindCanvasClick();
376
389
  this.bindScrubbing();
@@ -528,6 +541,7 @@ var Controller = class _Controller {
528
541
  this.refreshTime(0);
529
542
  this.refreshProgress(0);
530
543
  this.refreshVolumeUI();
544
+ this.refreshStops();
531
545
  };
532
546
  this.onFrame = ({ frame, totalFrames }) => {
533
547
  if (!this.isScrubbing) {
@@ -554,13 +568,92 @@ var Controller = class _Controller {
554
568
  this.movie.on("pause", this.onPause);
555
569
  this.movie.on("progress", this.onProgress);
556
570
  }
571
+ /** The stops of a presentation as marks on the seek bar, and the Present button. */
572
+ refreshStops() {
573
+ this.progressEl.querySelector(".mc-stops")?.remove();
574
+ const stops = this.movie.stops ?? [];
575
+ this.refreshPdfChoices(stops.length > 0);
576
+ if (stops.length === 0) {
577
+ this.presentBtn?.remove();
578
+ this.presentBtn = null;
579
+ return;
580
+ }
581
+ const total = this.movie.totalFrames || 1;
582
+ const holder = document.createElement("div");
583
+ holder.className = "mc-stops";
584
+ for (const s of stops) {
585
+ const tick = document.createElement("div");
586
+ tick.className = s.pageStart ? "mc-stop-tick mc-stop-page" : "mc-stop-tick";
587
+ tick.style.left = `${s.frame / total * 100}%`;
588
+ if (s.page) tick.title = s.page;
589
+ holder.appendChild(tick);
590
+ }
591
+ this.progressEl.appendChild(holder);
592
+ if (this.options.present && !this.presentBtn && this.fullscreenBtn) {
593
+ const btn = document.createElement("button");
594
+ btn.className = "mc-btn mc-present";
595
+ btn.setAttribute("aria-label", "Present");
596
+ btn.title = "Present";
597
+ btn.innerHTML = ICONS.present;
598
+ btn.addEventListener("click", () => {
599
+ void this.startPresenting();
600
+ });
601
+ this.fullscreenBtn.parentElement?.insertBefore(btn, this.fullscreenBtn);
602
+ this.presentBtn = btn;
603
+ }
604
+ }
605
+ /** A deck can be downloaded as a PDF: the format list gets "PDF" choices while the movie has stops. */
606
+ refreshPdfChoices(has) {
607
+ const select = this.settingsFormatSelect;
608
+ if (!select) return;
609
+ const present = !!select.querySelector('option[value="pdf"]');
610
+ if (has && !present) {
611
+ for (const [value, label] of [["pdf", "PDF (pages)"], ["pdf-steps", "PDF (every step)"]]) {
612
+ const o = document.createElement("option");
613
+ o.value = value;
614
+ o.textContent = label;
615
+ select.appendChild(o);
616
+ }
617
+ } else if (!has && present) {
618
+ select.querySelectorAll('option[value="pdf"], option[value="pdf-steps"]').forEach((o) => o.remove());
619
+ if (this.exportFormat === "pdf" || this.exportFormat === "pdf-steps") {
620
+ this.exportFormat = "mp4";
621
+ select.value = "mp4";
622
+ }
623
+ }
624
+ }
625
+ /** Play, or pause: a movie with stops plays to the next stop (and from the end, again from the start). */
626
+ async togglePlay() {
627
+ if (this.movie.isPlaying) {
628
+ this.movie.pause();
629
+ return;
630
+ }
631
+ await this.startPlaying();
632
+ }
633
+ /** Start playing: to the next stop for a movie with stops, to the end for any other (or with `pauseAtStops: false`). */
634
+ async startPlaying() {
635
+ if (this.options.pauseAtStops && (this.movie.stops?.length ?? 0) > 0) {
636
+ if (this.movie.currentFrame >= this.movie.totalFrames) await this.movie.gotoFrame(0, true);
637
+ await this.movie.next();
638
+ } else this.movie.play();
639
+ }
640
+ /** Hand the page over to a Presenter (fullscreen, keys, click and swipe move through the stops); Escape gives it back. */
641
+ async startPresenting() {
642
+ if (this.presenter || this.destroyed) return;
643
+ const { Presenter } = await import('./Presenter.cjs');
644
+ if (this.presenter || this.destroyed) return;
645
+ const p = new Presenter(this.movie, { canvas: this.options.canvas, onExit: () => this.stopPresenting() });
646
+ this.presenter = p;
647
+ await p.start();
648
+ }
649
+ stopPresenting() {
650
+ const p = this.presenter;
651
+ this.presenter = null;
652
+ p?.destroy();
653
+ }
557
654
  bindPlayButton() {
558
655
  this.playBtn.addEventListener("click", () => {
559
- if (this.movie.isPlaying) {
560
- this.movie.pause();
561
- } else {
562
- this.movie.play();
563
- }
656
+ void this.togglePlay();
564
657
  });
565
658
  }
566
659
  /** Clicking the picture plays / pauses (a tap on a phone too). */
@@ -572,12 +665,12 @@ var Controller = class _Controller {
572
665
  this.swallowCanvasClick = false;
573
666
  };
574
667
  this.canvasClickHandler = () => {
668
+ if (this.presenter) return;
575
669
  if (this.swallowCanvasClick) {
576
670
  this.swallowCanvasClick = false;
577
671
  return;
578
672
  }
579
- if (this.movie.isPlaying) this.movie.pause();
580
- else this.movie.play();
673
+ void this.togglePlay();
581
674
  };
582
675
  canvas.addEventListener("pointerdown", this.canvasPointerDownHandler);
583
676
  canvas.addEventListener("click", this.canvasClickHandler);
@@ -733,7 +826,7 @@ var Controller = class _Controller {
733
826
  } catch {
734
827
  }
735
828
  if (this.wasPlayingBeforeScrub) {
736
- this.movie.play();
829
+ void this.startPlaying();
737
830
  this.refreshPlayIcon();
738
831
  }
739
832
  };
@@ -782,7 +875,7 @@ var Controller = class _Controller {
782
875
  if (this.settingsFormatSelect) {
783
876
  this.settingsFormatSelect.addEventListener("change", (e) => {
784
877
  const v = e.target.value;
785
- if (v === "mp4" || v === "webm" || v === "mov") this.exportFormat = v;
878
+ if (v === "mp4" || v === "webm" || v === "mov" || v === "pdf" || v === "pdf-steps") this.exportFormat = v;
786
879
  });
787
880
  }
788
881
  if (this.settingsQualitySelect) {
@@ -832,9 +925,19 @@ var Controller = class _Controller {
832
925
  this.movie.pause();
833
926
  this.refreshPlayIcon();
834
927
  }
835
- this.showExportOverlay();
928
+ const pdf = this.exportFormat === "pdf" || this.exportFormat === "pdf-steps";
929
+ if (pdf) {
930
+ const stops = this.movie.stops ?? [];
931
+ const total = this.exportFormat === "pdf-steps" ? stops.filter((s) => s.pdf !== false).length : new Set(stops.map((s) => s.pageIndex)).size;
932
+ this.showExportOverlay("Exporting PDF...", `0% (0 / ${total} pages)`);
933
+ this.exportPages = { done: 0, total };
934
+ } else this.showExportOverlay();
836
935
  try {
837
- const blob = await this.movie.render({
936
+ const blob = pdf ? await this.movie.exportPDF({
937
+ which: this.exportFormat === "pdf-steps" ? "stops" : "pages",
938
+ quality: PDF_QUALITY[this.exportQuality],
939
+ onImage: () => this.pdfPageDone()
940
+ }) : await this.movie.render({
838
941
  format: this.exportFormat,
839
942
  video: { bitrate: this.exportQuality },
840
943
  audio: { bitrate: this.exportQuality }
@@ -850,7 +953,7 @@ var Controller = class _Controller {
850
953
  this.hideExportOverlay();
851
954
  this.isExporting = false;
852
955
  if (wasPlaying) {
853
- this.movie.play();
956
+ void this.startPlaying();
854
957
  this.refreshPlayIcon();
855
958
  }
856
959
  }
@@ -873,16 +976,27 @@ var Controller = class _Controller {
873
976
  const ts = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
874
977
  return `movie-${ts}.${ext}`;
875
978
  }
876
- showExportOverlay() {
979
+ /** One more PDF page is ready: move the bar. */
980
+ pdfPageDone() {
981
+ const p = this.exportPages;
982
+ if (!p || !this.exportFillEl || !this.exportTextEl) return;
983
+ p.done++;
984
+ const percent = p.total ? Math.min(100, Math.round(p.done / p.total * 100)) : 100;
985
+ this.exportFillEl.style.width = `${percent}%`;
986
+ this.exportTextEl.textContent = `${percent}% (${p.done} / ${p.total} pages)`;
987
+ }
988
+ showExportOverlay(title = "Exporting video...", text = "0% (0 / 0 frames)") {
877
989
  const overlay = document.createElement("div");
878
990
  overlay.className = "mc-export-overlay";
879
991
  overlay.innerHTML = `
880
992
  <div class="mc-export-panel">
881
- <div class="mc-export-title">Exporting video...</div>
993
+ <div class="mc-export-title"></div>
882
994
  <div class="mc-export-track"><div class="mc-export-fill"></div></div>
883
- <div class="mc-export-text">0% (0 / 0 frames)</div>
995
+ <div class="mc-export-text"></div>
884
996
  </div>
885
997
  `;
998
+ overlay.querySelector(".mc-export-title").textContent = title;
999
+ overlay.querySelector(".mc-export-text").textContent = text;
886
1000
  this.root.appendChild(overlay);
887
1001
  this.exportOverlay = overlay;
888
1002
  this.exportFillEl = overlay.querySelector(".mc-export-fill");
@@ -895,6 +1009,7 @@ var Controller = class _Controller {
895
1009
  this.exportFillEl = null;
896
1010
  this.exportTextEl = null;
897
1011
  }
1012
+ this.exportPages = null;
898
1013
  }
899
1014
  bindVisibility() {
900
1015
  const wrap = this.wrapper;
@@ -937,6 +1052,7 @@ var Controller = class _Controller {
937
1052
  }
938
1053
  bindKeyboard() {
939
1054
  this.keyHandler = (e) => {
1055
+ if (this.presenter) return;
940
1056
  const target = e.target;
941
1057
  if (target) {
942
1058
  const tag = target.tagName;
@@ -998,6 +1114,7 @@ var Controller = class _Controller {
998
1114
  destroy() {
999
1115
  if (this.destroyed) return;
1000
1116
  this.destroyed = true;
1117
+ this.stopPresenting();
1001
1118
  if (this.onReady) {
1002
1119
  this.movie.off("ready", this.onReady);
1003
1120
  this.onReady = null;