pixi-effects 0.28.2 → 0.30.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 (93) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +11 -10
  3. package/ai/chat-template.html +5 -5
  4. package/ai/tools/check.mjs +18 -3
  5. package/ai/tools/lookup.mjs +208 -0
  6. package/ai/tools/pixi-effects.mjs +16 -5
  7. package/ai/tools/render.mjs +14 -9
  8. package/ai/tools/view.mjs +6 -2
  9. package/dist/Base-CB1_FPZD.d.cts +60 -0
  10. package/dist/Base-Ne74ztNl.d.ts +60 -0
  11. package/dist/Composition-MUKR3FYD.cjs +16 -0
  12. package/dist/{Composition-K6B3APM5.cjs.map → Composition-MUKR3FYD.cjs.map} +1 -1
  13. package/dist/Composition-RJGA7KDU.js +7 -0
  14. package/dist/{Composition-H7UNR5F5.js.map → Composition-RJGA7KDU.js.map} +1 -1
  15. package/dist/Controller.cjs +73 -45
  16. package/dist/Controller.cjs.map +1 -1
  17. package/dist/Controller.d.cts +16 -22
  18. package/dist/Controller.d.ts +16 -22
  19. package/dist/Controller.js +74 -40
  20. package/dist/Controller.js.map +1 -1
  21. package/dist/{Movie-CHcM8HNB.d.ts → Movie-C2rZFkrO.d.ts} +227 -119
  22. package/dist/{Movie-Cx5ozUGb.d.cts → Movie-DdnpLpw1.d.cts} +227 -119
  23. package/dist/{blendModes-HDOP6R7T.cjs → blendModes-ETMYWATF.cjs} +5 -5
  24. package/dist/{blendModes-HDOP6R7T.cjs.map → blendModes-ETMYWATF.cjs.map} +1 -1
  25. package/dist/{blendModes-PAVTW3BO.js → blendModes-RIVZXL5A.js} +4 -4
  26. package/dist/{blendModes-PAVTW3BO.js.map → blendModes-RIVZXL5A.js.map} +1 -1
  27. package/dist/{chunk-P4GY3Z4G.cjs → chunk-2DXPTKQZ.cjs} +6 -6
  28. package/dist/chunk-2DXPTKQZ.cjs.map +1 -0
  29. package/dist/{chunk-TYWFKCPY.js → chunk-4KB3GUEK.js} +5 -5
  30. package/dist/chunk-4KB3GUEK.js.map +1 -0
  31. package/dist/{chunk-5MZWXVTT.js → chunk-A3Q3SDSH.js} +35 -28
  32. package/dist/chunk-A3Q3SDSH.js.map +1 -0
  33. package/dist/chunk-CRQFDY7E.js +4182 -0
  34. package/dist/chunk-CRQFDY7E.js.map +1 -0
  35. package/dist/chunk-HREBQAY4.cjs +4206 -0
  36. package/dist/chunk-HREBQAY4.cjs.map +1 -0
  37. package/dist/{chunk-ZMAD3KJU.cjs → chunk-LYOKH7KE.cjs} +22 -3
  38. package/dist/chunk-LYOKH7KE.cjs.map +1 -0
  39. package/dist/{chunk-45QKLD6M.js → chunk-RCVZX2JP.js} +22 -3
  40. package/dist/chunk-RCVZX2JP.js.map +1 -0
  41. package/dist/{chunk-KBH33IQX.cjs → chunk-WHNADXXG.cjs} +121 -114
  42. package/dist/chunk-WHNADXXG.cjs.map +1 -0
  43. package/dist/{chunk-7VN2KI3U.js → chunk-ZAXYPXKW.js} +14 -6
  44. package/dist/chunk-ZAXYPXKW.js.map +1 -0
  45. package/dist/{chunk-MJW7KILM.cjs → chunk-ZVUO325O.cjs} +32 -24
  46. package/dist/chunk-ZVUO325O.cjs.map +1 -0
  47. package/dist/index.cjs +106 -4482
  48. package/dist/index.cjs.map +1 -1
  49. package/dist/index.d.cts +15 -60
  50. package/dist/index.d.ts +15 -60
  51. package/dist/index.js +6 -4457
  52. package/dist/index.js.map +1 -1
  53. package/dist/loader.css +1 -1
  54. package/dist/{Presenter.cjs → present.cjs} +370 -31
  55. package/dist/present.cjs.map +1 -0
  56. package/dist/present.d.cts +278 -0
  57. package/dist/present.d.ts +278 -0
  58. package/dist/{Presenter.js → present.js} +358 -32
  59. package/dist/present.js.map +1 -0
  60. package/dist/three.cjs +6 -6
  61. package/dist/three.d.cts +2 -2
  62. package/dist/three.d.ts +2 -2
  63. package/dist/three.js +2 -2
  64. package/dist/{types-DpK1vwcl.d.ts → types-BzIB65Vk.d.cts} +11 -265
  65. package/dist/{types-DpK1vwcl.d.cts → types-BzIB65Vk.d.ts} +11 -265
  66. package/docs/api.md +44 -77
  67. package/docs/dsl.md +16 -14
  68. package/llms-full.txt +121 -117
  69. package/llms.txt +3 -2
  70. package/package.json +5 -5
  71. package/skills/pixi-effects/SKILL.md +7 -7
  72. package/skills/pixi-effects/reference/cheatsheet.md +9 -9
  73. package/skills/pixi-effects/reference/direction.md +2 -0
  74. package/skills/pixi-effects/reference/pitfalls.md +6 -6
  75. package/skills/pixi-effects/reference/quickstart.md +29 -0
  76. package/skills/pixi-effects/reference/recipes.md +4 -4
  77. package/skills/pixi-effects/template.html +7 -7
  78. package/dist/Base-C6f0fE8j.d.ts +0 -109
  79. package/dist/Base-DI1FCGcH.d.cts +0 -109
  80. package/dist/Composition-H7UNR5F5.js +0 -7
  81. package/dist/Composition-K6B3APM5.cjs +0 -16
  82. package/dist/Presenter.cjs.map +0 -1
  83. package/dist/Presenter.d.cts +0 -122
  84. package/dist/Presenter.d.ts +0 -122
  85. package/dist/Presenter.js.map +0 -1
  86. package/dist/chunk-45QKLD6M.js.map +0 -1
  87. package/dist/chunk-5MZWXVTT.js.map +0 -1
  88. package/dist/chunk-7VN2KI3U.js.map +0 -1
  89. package/dist/chunk-KBH33IQX.cjs.map +0 -1
  90. package/dist/chunk-MJW7KILM.cjs.map +0 -1
  91. package/dist/chunk-P4GY3Z4G.cjs.map +0 -1
  92. package/dist/chunk-TYWFKCPY.js.map +0 -1
  93. package/dist/chunk-ZMAD3KJU.cjs.map +0 -1
@@ -1,6 +1,268 @@
1
+ import './chunk-CRQFDY7E.js';
2
+ import './chunk-A3Q3SDSH.js';
3
+ import './chunk-ZAXYPXKW.js';
4
+ import './chunk-4KB3GUEK.js';
5
+ import './chunk-RCVZX2JP.js';
1
6
  import { warnUnknownOptions } from './chunk-BXOCEZ4U.js';
2
7
 
3
- // src/Presenter.ts
8
+ // src/present/stops.ts
9
+ function nextStopAfter(stops, frame) {
10
+ return stops.find((s) => s.frame > frame) ?? null;
11
+ }
12
+ function previousStopBefore(stops, frame) {
13
+ for (let i = stops.length - 1; i >= 0; i--) if (stops[i].frame < frame) return stops[i];
14
+ return null;
15
+ }
16
+ function stopAtOrBefore(stops, frame) {
17
+ for (let i = stops.length - 1; i >= 0; i--) if (stops[i].frame <= frame) return stops[i];
18
+ return null;
19
+ }
20
+ function pageStarts(stops) {
21
+ return stops.filter((s) => s.pageStart);
22
+ }
23
+ function pictureStops(stops, opts = {}) {
24
+ if (opts.which === "stops") return opts.pdf ? stops.filter((s) => s.pdf !== false) : [...stops];
25
+ const byPage = /* @__PURE__ */ new Map();
26
+ for (const s of stops) byPage.set(s.pageIndex, [...byPage.get(s.pageIndex) ?? [], s]);
27
+ const take = (list) => opts.pick === "first" ? list[0] : list[list.length - 1];
28
+ const picks = [];
29
+ for (const list of byPage.values()) {
30
+ const flagged = list.filter((s) => s.pdf === true);
31
+ const usable = list.filter((s) => s.pdf !== false);
32
+ const one = take(flagged.length ? flagged : usable.length ? usable : opts.pdf ? [] : list);
33
+ if (one) picks.push(one);
34
+ }
35
+ return picks;
36
+ }
37
+ function changedFraction(a, b, threshold = 12) {
38
+ const n = Math.min(a.length, b.length) >> 2;
39
+ if (n === 0) return 0;
40
+ let changed = 0;
41
+ for (let i = 0; i < n; i++) {
42
+ const k = i << 2;
43
+ if (Math.abs(a[k] - b[k]) > threshold || Math.abs(a[k + 1] - b[k + 1]) > threshold || Math.abs(a[k + 2] - b[k + 2]) > threshold) changed++;
44
+ }
45
+ return changed / n;
46
+ }
47
+
48
+ // src/present/navigation.ts
49
+ var listeners = /* @__PURE__ */ new WeakMap();
50
+ var toward = /* @__PURE__ */ new WeakMap();
51
+ function onStop(movie, fn) {
52
+ let set = listeners.get(movie);
53
+ if (!set) {
54
+ set = /* @__PURE__ */ new Set();
55
+ listeners.set(movie, set);
56
+ }
57
+ set.add(fn);
58
+ return () => {
59
+ set.delete(fn);
60
+ };
61
+ }
62
+ function emit(movie, stop) {
63
+ const e = { index: stop.index, stop, pageIndex: stop.pageIndex };
64
+ for (const fn of [...listeners.get(movie) ?? []]) fn(e);
65
+ }
66
+ var stopIndex = (movie) => stopAtOrBefore(movie.stops, movie.currentFrame)?.index ?? -1;
67
+ var currentStop = (movie) => stopAtOrBefore(movie.stops, movie.currentFrame);
68
+ var pageIndex = (movie) => currentStop(movie)?.pageIndex ?? -1;
69
+ var pageCount = (movie) => pageStarts(movie.stops).length;
70
+ async function next(movie) {
71
+ if (!movie.isReady) return;
72
+ const going = toward.get(movie);
73
+ if (going && movie.isPlaying) {
74
+ toward.delete(movie);
75
+ movie.pause();
76
+ await movie.gotoFrame(going.frame, true);
77
+ emit(movie, going);
78
+ return;
79
+ }
80
+ toward.delete(movie);
81
+ if (movie.currentFrame >= movie.totalFrames) return;
82
+ const target = nextStopAfter(movie.stops, movie.currentFrame);
83
+ if (!target) {
84
+ await movie.playUntil();
85
+ return;
86
+ }
87
+ toward.set(movie, target);
88
+ const landed = await movie.playUntil(target.frame / movie.frameRate);
89
+ if (toward.get(movie) === target) toward.delete(movie);
90
+ if (landed) emit(movie, target);
91
+ }
92
+ async function prev(movie) {
93
+ if (!movie.isReady) return;
94
+ toward.delete(movie);
95
+ movie.pause();
96
+ const target = previousStopBefore(movie.stops, movie.currentFrame);
97
+ await movie.gotoFrame(target ? target.frame : 0, true);
98
+ if (target) emit(movie, target);
99
+ }
100
+ async function goToStop(movie, index) {
101
+ if (!movie.isReady) return;
102
+ const stop = movie.stops[index];
103
+ if (!stop) {
104
+ console.warn(`pixi-effects: goToStop(movie, ${index}): there are ${movie.stops.length} stops (0 to ${movie.stops.length - 1})`);
105
+ return;
106
+ }
107
+ toward.delete(movie);
108
+ movie.pause();
109
+ await movie.gotoFrame(stop.frame, true);
110
+ emit(movie, stop);
111
+ }
112
+ async function goToPage(movie, page) {
113
+ const starts = pageStarts(movie.stops);
114
+ const stop = starts[page];
115
+ if (!stop) {
116
+ console.warn(`pixi-effects: goToPage(movie, ${page}): there are ${starts.length} pages (0 to ${starts.length - 1})`);
117
+ return;
118
+ }
119
+ await goToStop(movie, stop.index);
120
+ }
121
+
122
+ // src/present/pdf.ts
123
+ var enc = (s) => {
124
+ const out = new Uint8Array(s.length);
125
+ for (let i = 0; i < s.length; i++) out[i] = s.charCodeAt(i) & 255;
126
+ return out;
127
+ };
128
+ var num = (n) => String(Math.round(n * 1e3) / 1e3);
129
+ var textString = (s) => {
130
+ if (/^[\x20-\x7e]*$/.test(s)) return `(${s.replace(/[\\()]/g, (m) => `\\${m}`)})`;
131
+ let hex = "FEFF";
132
+ for (let i = 0; i < s.length; i++) hex += s.charCodeAt(i).toString(16).toUpperCase().padStart(4, "0");
133
+ return `<${hex}>`;
134
+ };
135
+ function buildPdf(pages, opts = {}) {
136
+ if (pages.length === 0) throw new Error("pixi-effects: a PDF needs at least one page");
137
+ const first = (i) => 3 + i * 3;
138
+ const objects = [];
139
+ objects[1] = [enc("<< /Type /Catalog /Pages 2 0 R >>")];
140
+ objects[2] = [enc(`<< /Type /Pages /Kids [${pages.map((_, i) => `${first(i)} 0 R`).join(" ")}] /Count ${pages.length} >>`)];
141
+ pages.forEach((p, i) => {
142
+ const w = p.width * 0.75, h = p.height * 0.75, n = first(i);
143
+ objects[n] = [enc(`<< /Type /Page /Parent 2 0 R /MediaBox [0 0 ${num(w)} ${num(h)}] /Resources << /XObject << /Im0 ${n + 2} 0 R >> >> /Contents ${n + 1} 0 R >>`)];
144
+ const content = `q ${num(w)} 0 0 ${num(h)} 0 0 cm /Im0 Do Q`;
145
+ objects[n + 1] = [enc(`<< /Length ${content.length} >>
146
+ stream
147
+ ${content}
148
+ endstream`)];
149
+ objects[n + 2] = [enc(`<< /Type /XObject /Subtype /Image /Width ${p.width} /Height ${p.height} /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /DCTDecode /Length ${p.jpeg.length} >>
150
+ stream
151
+ `), p.jpeg, enc("\nendstream")];
152
+ });
153
+ const infoNo = opts.title !== void 0 ? first(pages.length) : 0;
154
+ if (infoNo) objects[infoNo] = [enc(`<< /Title ${textString(opts.title)} /Producer (pixi-effects) >>`)];
155
+ const count = infoNo || first(pages.length);
156
+ const chunks = [enc("%PDF-1.4\n%\xE2\xE3\xCF\xD3\n")];
157
+ let offset = chunks[0].length;
158
+ const offsets = [];
159
+ for (let n = 1; n < count + (infoNo ? 1 : 0); n++) {
160
+ const body = objects[n];
161
+ if (!body) continue;
162
+ offsets[n] = offset;
163
+ const parts = [enc(`${n} 0 obj
164
+ `), ...body, enc("\nendobj\n")];
165
+ for (const part of parts) {
166
+ chunks.push(part);
167
+ offset += part.length;
168
+ }
169
+ }
170
+ const size = (infoNo || first(pages.length)) + (infoNo ? 1 : 0);
171
+ let xref = `xref
172
+ 0 ${size}
173
+ 0000000000 65535 f
174
+ `;
175
+ for (let n = 1; n < size; n++) xref += `${String(offsets[n] ?? 0).padStart(10, "0")} 00000 n
176
+ `;
177
+ xref += `trailer
178
+ << /Size ${size} /Root 1 0 R${infoNo ? ` /Info ${infoNo} 0 R` : ""} >>
179
+ startxref
180
+ ${offset}
181
+ %%EOF
182
+ `;
183
+ chunks.push(enc(xref));
184
+ const total = chunks.reduce((sum, c) => sum + c.length, 0);
185
+ const out = new Uint8Array(total);
186
+ let at = 0;
187
+ for (const c of chunks) {
188
+ out.set(c, at);
189
+ at += c.length;
190
+ }
191
+ return out;
192
+ }
193
+
194
+ // src/present/images.ts
195
+ function requireReady(movie, what) {
196
+ if (!movie.isReady) throw new Error(`pixi-effects: ${what}() needs a ready movie \u2014 await movie.init(...) first`);
197
+ }
198
+ async function stopImages(movie, opts = {}) {
199
+ warnUnknownOptions("stopImages()", opts, ["which", "pick", "scale", "type", "quality", "as", "motionBlur", "pdf", "onImage"]);
200
+ requireReady(movie, "stopImages");
201
+ if (movie.stops.length === 0) return [];
202
+ const picks = pictureStops(movie.stops, { which: opts.which, pick: opts.pick, pdf: opts.pdf });
203
+ const shot = { scale: opts.scale ?? 1, type: opts.type ?? "image/png", quality: opts.quality, as: opts.as ?? "blob", motionBlur: opts.motionBlur === void 0 ? false : opts.motionBlur };
204
+ return movie.peek(async () => {
205
+ const out = [];
206
+ for (const stop of picks) {
207
+ const image = await movie.snapshot(stop.frame, shot);
208
+ const item = { stop, page: stop.pageIndex, image };
209
+ out.push(item);
210
+ opts.onImage?.(item);
211
+ }
212
+ return out;
213
+ });
214
+ }
215
+ async function inspectStops(movie, opts = {}) {
216
+ warnUnknownOptions("inspectStops()", opts, ["lookback", "tolerance"]);
217
+ requireReady(movie, "inspectStops");
218
+ const out = { stops: [], issues: [] };
219
+ if (movie.stops.length === 0) return out;
220
+ const lookback = Math.max(1, Math.round(opts.lookback ?? 3));
221
+ const tolerance = opts.tolerance ?? 4e-3;
222
+ const small = Math.min(1, 320 / Math.max(1, movie.width));
223
+ const pixels = async (frame) => (await movie.snapshot(frame, { scale: small, motionBlur: false, as: "imageData" })).data;
224
+ return movie.peek(async () => {
225
+ for (const stop of movie.stops) {
226
+ const before = Math.max(0, stop.frame - lookback);
227
+ let moving = 0;
228
+ if (before < stop.frame) {
229
+ const now = await pixels(stop.frame);
230
+ moving = changedFraction(now, await pixels(before));
231
+ }
232
+ const settled = moving <= tolerance;
233
+ out.stops.push({ index: stop.index, page: stop.page, at: stop.at, frame: stop.frame, moving, settled });
234
+ if (!settled) {
235
+ out.issues.push(`stop ${stop.index + 1}${stop.page ? ` (page "${stop.page}")` : ""} at ${stop.at.toFixed(1)} s: the picture is still changing there (${moving < 0.1 ? (moving * 100).toFixed(1) : Math.round(moving * 100)}% of it changed in the last ${stop.frame - before} frames), so the audience, the page overview and a PDF see it mid-animation. Move the stop to after the animation has finished, or flag a settled stop of the page with \`pdf: true\`.`);
236
+ }
237
+ }
238
+ return out;
239
+ });
240
+ }
241
+ async function exportPDF(movie, opts = {}) {
242
+ warnUnknownOptions("exportPDF()", opts, ["which", "pick", "scale", "quality", "title", "motionBlur", "onImage"]);
243
+ requireReady(movie, "exportPDF");
244
+ if (movie.stops.length === 0) throw new Error("pixi-effects: exportPDF(): this movie has no stops, so there are no pages; add `stops` to the composition (or build it with deck())");
245
+ const scale = opts.scale ?? 1;
246
+ const images = await stopImages(movie, {
247
+ which: opts.which,
248
+ pick: opts.pick,
249
+ scale,
250
+ type: "image/jpeg",
251
+ quality: opts.quality ?? 0.92,
252
+ as: "blob",
253
+ motionBlur: opts.motionBlur,
254
+ pdf: true,
255
+ onImage: opts.onImage
256
+ });
257
+ const pages = await Promise.all(images.map(async (i) => ({
258
+ jpeg: new Uint8Array(await i.image.arrayBuffer()),
259
+ width: Math.max(1, Math.round(movie.width * scale)),
260
+ height: Math.max(1, Math.round(movie.height * scale))
261
+ })));
262
+ return new Blob([buildPdf(pages, { title: opts.title })], { type: "application/pdf" });
263
+ }
264
+
265
+ // src/present/Presenter.ts
4
266
  var OPTION_KEYS = ["canvas", "keyboard", "clickToAdvance", "swipe", "indicator", "autoAdvance", "loop", "accent", "onExit"];
5
267
  var NEXT_KEYS = /* @__PURE__ */ new Set(["ArrowRight", "ArrowDown", " ", "Enter", "PageDown"]);
6
268
  var PREV_KEYS = /* @__PURE__ */ new Set(["ArrowLeft", "ArrowUp", "Backspace", "PageUp"]);
@@ -127,6 +389,7 @@ var Presenter = class {
127
389
  wasWrapAttr = false;
128
390
  onKey = (e) => this.handleKey(e);
129
391
  onStop = (e) => this.handleStop(e);
392
+ offStop = null;
130
393
  onSeeked = () => this.refresh();
131
394
  onFrame = () => this.mirror();
132
395
  onEnded = () => this.handleEnded();
@@ -200,7 +463,7 @@ var Presenter = class {
200
463
  canvas.addEventListener("pointermove", this.onPointerMove);
201
464
  if (this.options.keyboard) document.addEventListener("keydown", this.onKey);
202
465
  document.addEventListener("fullscreenchange", this.onFullscreenChange);
203
- movie.on("stop", this.onStop);
466
+ this.offStop = onStop(movie, this.onStop);
204
467
  movie.on("seeked", this.onSeeked);
205
468
  movie.on("ended", this.onEnded);
206
469
  this.refresh();
@@ -224,11 +487,11 @@ var Presenter = class {
224
487
  }
225
488
  next() {
226
489
  this.cancelAdvance();
227
- return this.run(() => this.movie.next());
490
+ return this.run(() => next(this.movie));
228
491
  }
229
492
  prev() {
230
493
  this.cancelAdvance();
231
- return this.run(() => this.movie.prev());
494
+ return this.run(() => prev(this.movie));
232
495
  }
233
496
  /** Black out (`'black'`), white out (`'white'`) or bring the picture back (`'off'`). */
234
497
  setCover(mode) {
@@ -246,7 +509,8 @@ var Presenter = class {
246
509
  canvas.removeEventListener("pointerup", this.onPointerUp);
247
510
  canvas.removeEventListener("pointermove", this.onPointerMove);
248
511
  this.closePresenterView();
249
- this.movie.off("stop", this.onStop);
512
+ this.offStop?.();
513
+ this.offStop = null;
250
514
  this.movie.off("seeked", this.onSeeked);
251
515
  this.movie.off("ended", this.onEnded);
252
516
  for (const t of [this.idleTimer, this.digitsTimer, this.advanceTimer, this.loopTimer]) if (t) clearTimeout(t);
@@ -319,19 +583,19 @@ var Presenter = class {
319
583
  this.loopTimer = null;
320
584
  void this.run(async () => {
321
585
  await this.movie.gotoFrame(0, true);
322
- await this.movie.next();
586
+ await next(this.movie);
323
587
  });
324
588
  }, 800);
325
589
  }
326
590
  refresh() {
327
591
  const m = this.movie;
328
- const pages = m.pageCount ?? 0, stops = m.stops?.length ?? 0;
592
+ const pages = pageCount(m), stops = m.stops?.length ?? 0;
329
593
  if (this.counterEl) {
330
- this.counterEl.textContent = pages > 0 ? `${m.pageIndex >= 0 ? m.pageIndex + 1 : "\u2013"} / ${pages}` : "";
594
+ this.counterEl.textContent = pages > 0 ? `${pageIndex(m) >= 0 ? pageIndex(m) + 1 : "\u2013"} / ${pages}` : "";
331
595
  this.counterEl.style.display = pages > 0 ? "" : "none";
332
596
  }
333
- if (this.titleEl) this.titleEl.textContent = m.currentStop?.page ?? "";
334
- if (this.progressEl) this.progressEl.style.setProperty("--mp-fill", String(stops > 0 ? (m.stopIndex + 1) / stops : 0));
597
+ if (this.titleEl) this.titleEl.textContent = currentStop(m)?.page ?? "";
598
+ if (this.progressEl) this.progressEl.style.setProperty("--mp-fill", String(stops > 0 ? (stopIndex(m) + 1) / stops : 0));
335
599
  this.refreshView();
336
600
  }
337
601
  wake() {
@@ -389,7 +653,7 @@ var Presenter = class {
389
653
  this.showDigits();
390
654
  this.navigate(() => {
391
655
  this.cancelAdvance();
392
- void this.run(() => this.movie.goToPage(page));
656
+ void this.run(() => goToPage(this.movie, page));
393
657
  });
394
658
  } else if (k === "Backspace" && this.digits) {
395
659
  this.digits = this.digits.slice(0, -1);
@@ -398,11 +662,11 @@ var Presenter = class {
398
662
  else if (PREV_KEYS.has(k)) this.navigate(() => void this.prev());
399
663
  else if (k === "Home") this.navigate(() => {
400
664
  this.cancelAdvance();
401
- void this.run(() => this.movie.goToPage(0));
665
+ void this.run(() => goToPage(this.movie, 0));
402
666
  });
403
667
  else if (k === "End") this.navigate(() => {
404
668
  this.cancelAdvance();
405
- void this.run(() => this.movie.goToStop(Math.max(0, (this.movie.stops?.length ?? 1) - 1)));
669
+ void this.run(() => goToStop(this.movie, Math.max(0, (this.movie.stops?.length ?? 1) - 1)));
406
670
  });
407
671
  else if (k === "b" || k === "B" || k === ".") this.setCover(this.coverEl.getAttribute("data-mode") === "black" ? "off" : "black");
408
672
  else if (k === "w" || k === "W" || k === ",") this.setCover(this.coverEl.getAttribute("data-mode") === "white" ? "off" : "white");
@@ -505,18 +769,18 @@ var Presenter = class {
505
769
  if (!v || v.win.closed) return;
506
770
  const m = this.movie;
507
771
  const stops = m.stops ?? [];
508
- const pages = m.pageCount ?? 0;
509
- const cur = m.stopIndex ?? -1;
510
- v.els.count.textContent = pages > 0 ? `${m.pageIndex >= 0 ? m.pageIndex + 1 : "\u2013"} / ${pages}` : "";
511
- v.els.title.textContent = m.currentStop?.page ?? (cur < 0 ? stops[0]?.page ?? "" : "");
512
- const upTo = cur < 0 ? 0 : cur, pageIndex = stops[upTo]?.pageIndex;
772
+ const pages = pageCount(m);
773
+ const cur = stopIndex(m);
774
+ v.els.count.textContent = pages > 0 ? `${pageIndex(m) >= 0 ? pageIndex(m) + 1 : "\u2013"} / ${pages}` : "";
775
+ v.els.title.textContent = currentStop(m)?.page ?? (cur < 0 ? stops[0]?.page ?? "" : "");
776
+ const upTo = cur < 0 ? 0 : cur, pageIndex2 = stops[upTo]?.pageIndex;
513
777
  let notes = "";
514
- for (const s of stops) if (s.pageIndex === pageIndex && s.index <= upTo && s.notes) notes = s.notes;
778
+ for (const s of stops) if (s.pageIndex === pageIndex2 && s.index <= upTo && s.notes) notes = s.notes;
515
779
  v.els.notes.textContent = notes;
516
780
  v.els.notes.setAttribute("data-empty", String(notes === ""));
517
- const next = stops[cur + 1];
518
- v.els.nextbox.setAttribute("data-end", String(!next));
519
- const src = next ? this.thumbs.get(next.index) : void 0;
781
+ const next2 = stops[cur + 1];
782
+ v.els.nextbox.setAttribute("data-end", String(!next2));
783
+ const src = next2 ? this.thumbs.get(next2.index) : void 0;
520
784
  if (src) v.els.next.setAttribute("src", src);
521
785
  else v.els.next.removeAttribute("src");
522
786
  }
@@ -544,10 +808,11 @@ var Presenter = class {
544
808
  * Make a picture of every stop, once. Taking them moves the playhead through the stops and back, which the audience would see:
545
809
  * `start()` does it before anything is shown (behind a "Preparing" cover), and an overview opened before that does it on the spot.
546
810
  */
811
+ /** Make the page pictures now (the overview and the presenter view use them): `start()` does it before the audience sees anything. */
547
812
  ensureThumbs() {
548
813
  this.thumbsPromise ??= (async () => {
549
814
  try {
550
- await this.movie.stopImages({
815
+ await stopImages(this.movie, {
551
816
  which: "stops",
552
817
  as: "dataURL",
553
818
  type: "image/jpeg",
@@ -570,8 +835,8 @@ var Presenter = class {
570
835
  const stops = (this.movie.stops ?? []).filter((s) => s.pageIndex === page);
571
836
  return stops.length ? this.thumbs.get(stops[stops.length - 1].index) : void 0;
572
837
  }
573
- onThumb(stopIndex) {
574
- const stop = this.movie.stops?.[stopIndex];
838
+ onThumb(stopIndex2) {
839
+ const stop = this.movie.stops?.[stopIndex2];
575
840
  if (stop) this.fillThumb(stop.pageIndex);
576
841
  this.refreshView();
577
842
  }
@@ -582,18 +847,18 @@ var Presenter = class {
582
847
  this.gridEl.style.setProperty("--mp-cols", String(cols));
583
848
  this.gridEl.style.setProperty("--mp-ratio", `${width} / ${height}`);
584
849
  this.gridEl.innerHTML = "";
585
- const current = Math.max(0, this.movie.pageIndex ?? 0);
850
+ const current = Math.max(0, pageIndex(this.movie));
586
851
  pages.forEach((s, i) => {
587
852
  const cell = document.createElement("div");
588
853
  cell.className = "mp-cell";
589
- cell.setAttribute("data-current", String(i === this.movie.pageIndex));
854
+ cell.setAttribute("data-current", String(i === pageIndex(this.movie)));
590
855
  cell.setAttribute("data-selected", String(i === current));
591
856
  cell.innerHTML = `<div class="mp-cell-label"><span class="mp-cell-num">${i + 1}</span><span class="mp-cell-name"></span></div>`;
592
857
  cell.querySelector(".mp-cell-name").textContent = s.page ?? "";
593
858
  cell.addEventListener("click", () => {
594
859
  this.overviewEl.setAttribute("data-open", "false");
595
860
  this.cancelAdvance();
596
- void this.run(() => this.movie.goToPage(i));
861
+ void this.run(() => goToPage(this.movie, i));
597
862
  });
598
863
  this.gridEl.appendChild(cell);
599
864
  this.fillThumb(i);
@@ -634,7 +899,7 @@ var Presenter = class {
634
899
  const page = this.overviewSel;
635
900
  this.overviewEl.setAttribute("data-open", "false");
636
901
  this.cancelAdvance();
637
- void this.run(() => this.movie.goToPage(page));
902
+ void this.run(() => goToPage(this.movie, page));
638
903
  return true;
639
904
  }
640
905
  case "Escape":
@@ -664,6 +929,67 @@ var Presenter = class {
664
929
  }
665
930
  };
666
931
 
667
- export { Presenter };
668
- //# sourceMappingURL=Presenter.js.map
669
- //# sourceMappingURL=Presenter.js.map
932
+ // src/present/deck.ts
933
+ var PAGE_KEYS = ["name", "duration", "sequences", "stops", "notes", "advance", "initial", "keyframes", "filters", "filterArea", "blendMode", "mask"];
934
+ var DECK_KEYS = ["pages", "transition", "width", "height", "frameRate", "background", "canvas", "assets", "poster", "motionBlur", "loader"];
935
+ function deck(options) {
936
+ if (!options || !Array.isArray(options.pages) || options.pages.length === 0) throw new Error("deck(): pages must be a list with at least one page: { pages: [{ name, duration, stops, sequences }] }");
937
+ warnUnknownOptions("deck()", options, DECK_KEYS);
938
+ const { pages, transition, ...movieOptions } = options;
939
+ pages.forEach((p, i) => {
940
+ if (!p || typeof p !== "object") throw new Error(`deck(): pages[${i}] must be an object`);
941
+ warnUnknownOptions(`deck() pages[${i}]`, p, PAGE_KEYS);
942
+ if (!(typeof p.duration === "number" && Number.isFinite(p.duration) && p.duration > 0)) throw new Error(`deck(): pages[${i}].duration must be a positive number of seconds, got ${String(p.duration)}`);
943
+ if (!Array.isArray(p.sequences)) throw new Error(`deck(): pages[${i}].sequences must be a list of layers (it may be empty), got ${typeof p.sequences}`);
944
+ });
945
+ let overlap = 0;
946
+ if (transition) {
947
+ if (!(typeof transition.duration === "number" && transition.duration > 0)) throw new Error("deck(): transition needs a duration in seconds: { kind: 'crossfade', duration: 0.6 }");
948
+ const shortest = Math.min(...pages.map((p) => p.duration));
949
+ if (pages.length > 1 && transition.duration >= shortest) throw new Error(`deck(): the transition (${transition.duration} s) must be shorter than every page (the shortest is ${shortest} s)`);
950
+ overlap = pages.length > 1 ? transition.duration : 0;
951
+ }
952
+ const sequences = [];
953
+ const transitions = [];
954
+ const stops = [];
955
+ let start = 0;
956
+ pages.forEach((page, i) => {
957
+ const id = `page-${i + 1}`;
958
+ const label = page.name ? `"${page.name}"` : `pages[${i}]`;
959
+ const isLast = i === pages.length - 1;
960
+ const entries = (page.stops ?? [isLast ? page.duration : page.duration - overlap]).map((e, k) => {
961
+ if (e && typeof e === "object") warnUnknownOptions(`deck() page ${label} stops[${k}]`, e, ["at", "pdf"]);
962
+ return typeof e === "object" && e !== null ? e : { at: e };
963
+ });
964
+ const local = entries.map((e) => e.at);
965
+ local.forEach((s, k) => {
966
+ if (!(typeof s === "number" && Number.isFinite(s) && s > 0 && s <= page.duration + 1e-9)) throw new Error(`deck(): page ${label} has stop ${String(s)}, outside the page (0 to ${page.duration} s)`);
967
+ if (k > 0 && s <= local[k - 1]) throw new Error(`deck(): page ${label} stops must be in order, earliest first (got ${local.join(", ")})`);
968
+ });
969
+ const { name: _n, duration: _d, sequences: inner, stops: _s, notes: _no, advance: _a, ...layerProps } = page;
970
+ sequences.push({ type: "composition", name: id, at: round(start), duration: page.duration, ...layerProps, sequences: inner });
971
+ local.forEach((s, k) => {
972
+ const stop = { at: round(start + s) };
973
+ if (k === 0) {
974
+ stop.page = page.name ?? true;
975
+ if (page.notes !== void 0) stop.notes = page.notes;
976
+ }
977
+ if (k === local.length - 1 && page.advance !== void 0) stop.advance = page.advance;
978
+ if (entries[k].pdf !== void 0) stop.pdf = entries[k].pdf;
979
+ stops.push(stop);
980
+ });
981
+ if (transition && !isLast) {
982
+ const next2 = start + page.duration - overlap;
983
+ transitions.push({ ...transition, from: id, to: `page-${i + 2}`, at: round(next2), duration: transition.duration });
984
+ }
985
+ start = start + page.duration - (isLast ? 0 : overlap);
986
+ });
987
+ const composition = { sequences, stops };
988
+ if (transitions.length > 0) composition.transitions = transitions;
989
+ return { ...movieOptions, duration: round(start), composition };
990
+ }
991
+ var round = (v) => Math.round(v * 1e6) / 1e6;
992
+
993
+ export { Presenter, currentStop, deck, exportPDF, goToPage, goToStop, inspectStops, next, onStop, pageCount, pageIndex, prev, stopImages, stopIndex };
994
+ //# sourceMappingURL=present.js.map
995
+ //# sourceMappingURL=present.js.map