pixi-effects 0.11.0 → 0.12.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 +16 -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-CyvSLoCP.d.ts → Base-CYH9vPCH.d.ts} +1 -1
  11. package/dist/{Base-CwUxTfja.d.cts → Base-W3sQzGPO.d.cts} +1 -1
  12. package/dist/Controller.cjs +58 -0
  13. package/dist/Controller.cjs.map +1 -1
  14. package/dist/Controller.d.cts +13 -3
  15. package/dist/Controller.d.ts +13 -3
  16. package/dist/Controller.js +58 -0
  17. package/dist/Controller.js.map +1 -1
  18. package/dist/{Movie-nzmWsxWX.d.ts → Movie-CY2QPVVM.d.cts} +90 -2
  19. package/dist/{Movie-DSSNOF3T.d.cts → Movie-EBF6_RfX.d.ts} +90 -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 +645 -36
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.cts +162 -5
  29. package/dist/index.d.ts +162 -5
  30. package/dist/index.js +640 -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-mpqQO8MJ.d.cts} +39 -1
  36. package/dist/{types-BXu28nJj.d.ts → types-mpqQO8MJ.d.ts} +39 -1
  37. package/docs/api.md +79 -0
  38. package/docs/dsl.md +48 -0
  39. package/llms-full.txt +202 -2
  40. package/llms.txt +3 -3
  41. package/package.json +5 -1
@@ -0,0 +1,669 @@
1
+ import { warnUnknownOptions } from './chunk-DKGG7WD4.js';
2
+
3
+ // src/Presenter.ts
4
+ var OPTION_KEYS = ["canvas", "keyboard", "clickToAdvance", "swipe", "indicator", "autoAdvance", "loop", "accent", "onExit"];
5
+ var NEXT_KEYS = /* @__PURE__ */ new Set(["ArrowRight", "ArrowDown", " ", "Enter", "PageDown"]);
6
+ var PREV_KEYS = /* @__PURE__ */ new Set(["ArrowLeft", "ArrowUp", "Backspace", "PageUp"]);
7
+ var clock = (ms) => {
8
+ const s = Math.floor(ms / 1e3);
9
+ return `${String(Math.floor(s / 60)).padStart(2, "0")}:${String(s % 60).padStart(2, "0")}`;
10
+ };
11
+ var IDLE_MS = 2500;
12
+ var DIGITS_MS = 2e3;
13
+ var SWIPE_PX = 50;
14
+ var CSS = `
15
+ .movie-presenter { position: absolute; inset: 0; z-index: 3; pointer-events: none; font: 500 13px/1.3 system-ui, -apple-system, 'Helvetica Neue', Arial, sans-serif; color: #fff; }
16
+ .movie-presenter .mp-info { position: absolute; left: 50%; bottom: 14px; transform: translateX(-50%); white-space: nowrap; display: flex; align-items: baseline; gap: 12px; padding: 6px 12px; border-radius: 999px; background: rgba(0, 0, 0, 0.45); transition: opacity 0.4s ease; }
17
+ .movie-presenter .mp-title { opacity: 0.75; }
18
+ .movie-presenter .mp-title:empty { display: none; }
19
+ .movie-presenter .mp-counter { font-variant-numeric: tabular-nums; letter-spacing: 0.04em; }
20
+ .movie-presenter .mp-progress { position: absolute; left: 0; right: 0; bottom: 0; height: 3px; background: rgba(255, 255, 255, 0.14); transition: opacity 0.4s ease; }
21
+ .movie-presenter .mp-progress::after { content: ''; position: absolute; inset: 0; background: var(--mp-accent, #ffffff); transform-origin: 0 50%; transform: scaleX(var(--mp-fill, 0)); transition: transform 0.4s ease; }
22
+ .movie-presenter[data-idle="true"] .mp-info, .movie-presenter[data-idle="true"] .mp-progress { opacity: 0; }
23
+ .movie-presenter .mp-cover { position: absolute; inset: 0; z-index: 4; opacity: 0; pointer-events: none; background: #000; transition: opacity 0.25s ease; }
24
+ .movie-presenter .mp-cover[data-mode="black"] { opacity: 1; pointer-events: auto; background: #000; }
25
+ .movie-presenter .mp-cover[data-mode="white"] { opacity: 1; pointer-events: auto; background: #fff; }
26
+ .movie-presenter .mp-jump { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); padding: 10px 22px; border-radius: 12px; background: rgba(0, 0, 0, 0.6); font-size: 34px; font-weight: 600; letter-spacing: 0.06em; opacity: 0; transition: opacity 0.15s ease; }
27
+ .movie-presenter .mp-jump[data-active="true"] { opacity: 1; }
28
+ .movie-presenter .mp-help { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); z-index: 5; padding: 18px 26px; border-radius: 14px; background: rgba(12, 14, 24, 0.92); box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); pointer-events: auto; }
29
+ .movie-presenter .mp-help[data-open="false"] { display: none; }
30
+ .movie-presenter .mp-help h3 { margin: 0 0 10px; font-size: 13px; letter-spacing: 0.12em; opacity: 0.7; }
31
+ .movie-presenter .mp-help dl { display: grid; grid-template-columns: auto auto; gap: 6px 22px; margin: 0; }
32
+ .movie-presenter .mp-help dt { opacity: 0.95; font-weight: 600; white-space: nowrap; }
33
+ .movie-presenter .mp-help dd { margin: 0; opacity: 0.75; }
34
+ .movie-presenter .mp-wait { position: absolute; inset: 0; z-index: 7; display: grid; place-items: center; background: #000; font-size: 13px; letter-spacing: 0.4em; opacity: 0.9; padding-left: 0.4em; }
35
+ .movie-presenter .mp-wait[data-open="false"] { display: none; }
36
+ .movie-presenter .mp-overview { position: absolute; inset: 0; z-index: 6; display: flex; flex-direction: column; gap: 14px; padding: 26px 30px 18px; overflow: auto; background: rgba(8, 10, 18, 0.985); pointer-events: auto; }
37
+ .movie-presenter .mp-overview[data-open="false"] { display: none; }
38
+ .movie-presenter .mp-grid { display: grid; grid-template-columns: repeat(var(--mp-cols, 3), minmax(0, 1fr)); gap: 14px; }
39
+ .movie-presenter .mp-cell { position: relative; aspect-ratio: var(--mp-ratio, 16 / 9); overflow: hidden; border-radius: 8px; background: #000; border: 2px solid transparent; cursor: pointer; }
40
+ .movie-presenter .mp-cell img { display: block; width: 100%; height: 100%; object-fit: cover; }
41
+ .movie-presenter .mp-cell[data-current="true"] { border-color: rgba(255, 255, 255, 0.45); }
42
+ .movie-presenter .mp-cell[data-selected="true"] { border-color: var(--mp-accent, #ffffff); box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.18); }
43
+ .movie-presenter .mp-cell-label { position: absolute; left: 0; right: 0; bottom: 0; display: flex; gap: 8px; align-items: baseline; padding: 18px 10px 7px; background: linear-gradient(to top, rgba(0, 0, 0, 0.7), transparent); font-size: 12px; }
44
+ .movie-presenter .mp-cell-num { font-weight: 700; font-variant-numeric: tabular-nums; }
45
+ .movie-presenter .mp-cell-name { opacity: 0.85; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
46
+ .movie-presenter .mp-overview-hint { margin-top: auto; opacity: 0.55; font-size: 12px; letter-spacing: 0.04em; }
47
+ [data-pr-wrap] .movie-controller { display: none !important; }
48
+ [data-pr-wrap]:fullscreen { width: 100vw; height: 100vh; display: flex; align-items: center; justify-content: center; background: #000; }
49
+ [data-pr-wrap]:fullscreen > canvas { width: 100% !important; height: 100% !important; max-width: none !important; max-height: none !important; object-fit: contain; border-radius: 0; }
50
+ @media (prefers-reduced-motion: reduce) { .movie-presenter * { transition: none !important; } }
51
+ `;
52
+ function installStyles() {
53
+ if (typeof document === "undefined" || document.head.querySelector("style[data-movie-presenter]")) return;
54
+ const el = document.createElement("style");
55
+ el.setAttribute("data-movie-presenter", "");
56
+ el.textContent = CSS;
57
+ document.head.appendChild(el);
58
+ }
59
+ var HELP = [
60
+ ["\u2192 \u2193 Space Enter PageDown click", "next (plays to the next stop)"],
61
+ ["\u2190 \u2191 Backspace PageUp", "back one stop"],
62
+ ["Home / End", "first page / last stop"],
63
+ ["number, then Enter", "jump to that page"],
64
+ ["B or .", "black screen"],
65
+ ["W or ,", "white screen"],
66
+ ["G", "overview of all pages (arrows + Enter, or click)"],
67
+ ["P", "presenter view: notes, next picture, timer (a second window)"],
68
+ ["F", "fullscreen"],
69
+ ["Esc", "close this / black screen / fullscreen"],
70
+ ["swipe \u2190 \u2192", "next / back (touch)"]
71
+ ];
72
+ var VIEW_CSS = `
73
+ html, body { margin: 0; height: 100%; background: #0c0e16; color: #e8ecf8; font: 500 15px/1.4 system-ui, -apple-system, 'Helvetica Neue', Arial, sans-serif; }
74
+ .pv { display: grid; grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr); gap: 18px; height: 100%; padding: 18px; box-sizing: border-box; }
75
+ .pv-label { margin: 0 0 6px; font-size: 11px; font-weight: 700; letter-spacing: 0.18em; opacity: 0.55; }
76
+ .pv-main { display: flex; flex-direction: column; min-height: 0; }
77
+ .pv-now { width: 100%; height: auto; background: #000; border-radius: 8px; }
78
+ .pv-side { display: flex; flex-direction: column; gap: 12px; min-height: 0; }
79
+ .pv-top { display: flex; align-items: baseline; gap: 14px; }
80
+ .pv-count { font-size: 30px; font-weight: 700; font-variant-numeric: tabular-nums; }
81
+ .pv-title { flex: 1; opacity: 0.75; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
82
+ .pv-timer { font-size: 30px; font-weight: 600; font-variant-numeric: tabular-nums; letter-spacing: 0.04em; }
83
+ .pv-nextbox { position: relative; aspect-ratio: var(--pv-ratio, 16 / 9); background: #000; border-radius: 8px; overflow: hidden; }
84
+ .pv-next { display: block; width: 100%; height: 100%; object-fit: cover; }
85
+ .pv-nextbox[data-end="true"] .pv-next { display: none; }
86
+ .pv-end { position: absolute; inset: 0; display: none; place-items: center; opacity: 0.6; letter-spacing: 0.2em; font-size: 12px; }
87
+ .pv-nextbox[data-end="true"] .pv-end { display: grid; }
88
+ .pv-notes { flex: 1; min-height: 80px; overflow: auto; padding: 12px 14px; border-radius: 8px; background: #151927; font-size: 20px; line-height: 1.5; white-space: pre-wrap; }
89
+ .pv-notes[data-empty="true"] { opacity: 0.4; font-size: 14px; }
90
+ .pv-notes[data-empty="true"]::before { content: 'No notes for this page.'; }
91
+ .pv-buttons { display: flex; gap: 10px; }
92
+ .pv button { font: inherit; color: inherit; border: 0; border-radius: 8px; padding: 10px 16px; background: #232a40; cursor: pointer; }
93
+ .pv button:hover { background: #2e3754; }
94
+ .pv .pv-nextbtn { flex: 1; background: #3b5bdb; font-weight: 600; }
95
+ .pv .pv-nextbtn:hover { background: #4c6ef5; }
96
+ .pv .pv-reset { padding: 4px 10px; font-size: 12px; opacity: 0.7; }
97
+ `;
98
+ var Presenter = class {
99
+ movie;
100
+ options;
101
+ box;
102
+ wrappedHere = false;
103
+ root;
104
+ counterEl = null;
105
+ titleEl = null;
106
+ progressEl = null;
107
+ coverEl;
108
+ helpEl;
109
+ jumpEl;
110
+ idleTimer = null;
111
+ digitsTimer = null;
112
+ advanceTimer = null;
113
+ loopTimer = null;
114
+ overviewEl;
115
+ gridEl;
116
+ overviewSel = 0;
117
+ /** A picture of every stop (by stop index), made once before the talk: for the overview and the presenter view's "next". */
118
+ thumbs = /* @__PURE__ */ new Map();
119
+ thumbsPromise = null;
120
+ waitEl;
121
+ view = null;
122
+ digits = "";
123
+ downAt = null;
124
+ swiped = false;
125
+ canvasCursor = "";
126
+ destroyed = false;
127
+ wasWrapAttr = false;
128
+ onKey = (e) => this.handleKey(e);
129
+ onStop = (e) => this.handleStop(e);
130
+ onSeeked = () => this.refresh();
131
+ onFrame = () => this.mirror();
132
+ onEnded = () => this.handleEnded();
133
+ onPointerDown = (e) => {
134
+ this.downAt = { x: e.clientX, y: e.clientY };
135
+ this.swiped = false;
136
+ };
137
+ onPointerUp = (e) => this.handlePointerUp(e);
138
+ onPointerMove = () => this.wake();
139
+ onClick = () => this.handleClick();
140
+ wasFullscreen = false;
141
+ onFullscreenChange = () => {
142
+ const now = document.fullscreenElement === this.box;
143
+ if (this.wasFullscreen && !now) this.options.onExit?.();
144
+ this.wasFullscreen = now;
145
+ };
146
+ constructor(movie, options) {
147
+ if (!options || !options.canvas) throw new Error("Presenter requires options.canvas (HTMLCanvasElement).");
148
+ warnUnknownOptions("Presenter", options, OPTION_KEYS);
149
+ this.movie = movie;
150
+ this.options = {
151
+ canvas: options.canvas,
152
+ keyboard: options.keyboard ?? true,
153
+ clickToAdvance: options.clickToAdvance ?? true,
154
+ swipe: options.swipe ?? true,
155
+ indicator: options.indicator ?? true,
156
+ autoAdvance: options.autoAdvance ?? true,
157
+ loop: options.loop ?? false,
158
+ accent: options.accent,
159
+ onExit: options.onExit
160
+ };
161
+ if ((movie.stops?.length ?? 0) === 0) {
162
+ console.warn("pixi-effects: Presenter: this movie has no stops, so there is nowhere to pause; add `stops: [2, 5, 9]` (seconds) to the composition. next() and back still work.");
163
+ }
164
+ installStyles();
165
+ const canvas = this.options.canvas;
166
+ this.box = this.positioningBox(canvas);
167
+ this.wasWrapAttr = this.box.hasAttribute("data-pr-wrap");
168
+ this.box.setAttribute("data-pr-wrap", "");
169
+ this.root = document.createElement("div");
170
+ this.root.className = "movie-presenter";
171
+ this.root.setAttribute("data-idle", "false");
172
+ if (this.options.accent) this.root.style.setProperty("--mp-accent", this.options.accent);
173
+ this.root.innerHTML = `
174
+ ${this.options.indicator ? '<div class="mp-progress"></div><div class="mp-info"><span class="mp-title"></span><span class="mp-counter"></span></div>' : ""}
175
+ <div class="mp-wait" data-open="false">Preparing\u2026</div>
176
+ <div class="mp-jump" data-active="false"></div>
177
+ <div class="mp-overview" data-open="false" role="dialog" aria-label="Pages"><div class="mp-grid"></div><div class="mp-overview-hint">\u2190 \u2192 \u2191 \u2193 choose \xB7 Enter or click: go \xB7 G or Esc: close</div></div>
178
+ <div class="mp-cover" data-mode="off"></div>
179
+ <div class="mp-help" data-open="false" role="dialog" aria-label="Keys"><h3>KEYS</h3><dl>${HELP.map(([k, d]) => `<dt>${k}</dt><dd>${d}</dd>`).join("")}</dl></div>`;
180
+ this.box.appendChild(this.root);
181
+ this.counterEl = this.root.querySelector(".mp-counter");
182
+ this.titleEl = this.root.querySelector(".mp-title");
183
+ this.progressEl = this.root.querySelector(".mp-progress");
184
+ this.coverEl = this.root.querySelector(".mp-cover");
185
+ this.helpEl = this.root.querySelector(".mp-help");
186
+ this.jumpEl = this.root.querySelector(".mp-jump");
187
+ this.waitEl = this.root.querySelector(".mp-wait");
188
+ this.overviewEl = this.root.querySelector(".mp-overview");
189
+ this.gridEl = this.root.querySelector(".mp-grid");
190
+ this.coverEl.addEventListener("click", () => this.setCover("off"));
191
+ this.canvasCursor = canvas.style.cursor;
192
+ if (this.options.clickToAdvance) {
193
+ canvas.style.cursor = "pointer";
194
+ canvas.addEventListener("click", this.onClick);
195
+ }
196
+ if (this.options.swipe) {
197
+ canvas.addEventListener("pointerdown", this.onPointerDown);
198
+ canvas.addEventListener("pointerup", this.onPointerUp);
199
+ }
200
+ canvas.addEventListener("pointermove", this.onPointerMove);
201
+ if (this.options.keyboard) document.addEventListener("keydown", this.onKey);
202
+ document.addEventListener("fullscreenchange", this.onFullscreenChange);
203
+ movie.on("stop", this.onStop);
204
+ movie.on("seeked", this.onSeeked);
205
+ movie.on("ended", this.onEnded);
206
+ this.refresh();
207
+ this.wake();
208
+ }
209
+ /** Enter presentation: ask for fullscreen (a browser needs a click or key press for that) and play to the first stop. */
210
+ async start() {
211
+ this.waitEl.setAttribute("data-open", "true");
212
+ try {
213
+ await this.ensureThumbs();
214
+ } finally {
215
+ this.waitEl.setAttribute("data-open", "false");
216
+ }
217
+ await this.enterFullscreen();
218
+ await this.next();
219
+ }
220
+ /** Leave fullscreen. */
221
+ async exit() {
222
+ if (document.fullscreenElement === this.box) await document.exitFullscreen?.().catch(() => {
223
+ });
224
+ }
225
+ next() {
226
+ this.cancelAdvance();
227
+ return this.run(() => this.movie.next());
228
+ }
229
+ prev() {
230
+ this.cancelAdvance();
231
+ return this.run(() => this.movie.prev());
232
+ }
233
+ /** Black out (`'black'`), white out (`'white'`) or bring the picture back (`'off'`). */
234
+ setCover(mode) {
235
+ this.coverEl.setAttribute("data-mode", mode);
236
+ if (mode !== "off") this.cancelAdvance();
237
+ }
238
+ destroy() {
239
+ if (this.destroyed) return;
240
+ this.destroyed = true;
241
+ const canvas = this.options.canvas;
242
+ document.removeEventListener("keydown", this.onKey);
243
+ document.removeEventListener("fullscreenchange", this.onFullscreenChange);
244
+ canvas.removeEventListener("click", this.onClick);
245
+ canvas.removeEventListener("pointerdown", this.onPointerDown);
246
+ canvas.removeEventListener("pointerup", this.onPointerUp);
247
+ canvas.removeEventListener("pointermove", this.onPointerMove);
248
+ this.closePresenterView();
249
+ this.movie.off("stop", this.onStop);
250
+ this.movie.off("seeked", this.onSeeked);
251
+ this.movie.off("ended", this.onEnded);
252
+ for (const t of [this.idleTimer, this.digitsTimer, this.advanceTimer, this.loopTimer]) if (t) clearTimeout(t);
253
+ canvas.style.cursor = this.canvasCursor;
254
+ this.root.remove();
255
+ if (!this.wasWrapAttr) this.box.removeAttribute("data-pr-wrap");
256
+ if (this.wrappedHere) {
257
+ const parent = this.box.parentElement;
258
+ if (parent) {
259
+ parent.insertBefore(canvas, this.box);
260
+ this.box.remove();
261
+ }
262
+ }
263
+ }
264
+ // ── internals ──
265
+ positioningBox(canvas) {
266
+ const parent = canvas.parentElement;
267
+ if (!parent) throw new Error("Presenter: canvas must be attached to the DOM before constructing.");
268
+ const pos = getComputedStyle(parent).position;
269
+ if (pos === "relative" || pos === "absolute" || pos === "fixed" || pos === "sticky") return parent;
270
+ const wrap = document.createElement("div");
271
+ wrap.className = "movie-presenter-wrap";
272
+ wrap.style.cssText = "position: relative; display: inline-block; line-height: 0;";
273
+ parent.insertBefore(wrap, canvas);
274
+ wrap.appendChild(canvas);
275
+ this.wrappedHere = true;
276
+ return wrap;
277
+ }
278
+ run(fn) {
279
+ return fn().catch((err) => {
280
+ console.warn("pixi-effects: Presenter:", err);
281
+ });
282
+ }
283
+ async enterFullscreen() {
284
+ const box = this.box;
285
+ try {
286
+ await box.requestFullscreen?.();
287
+ } catch {
288
+ }
289
+ }
290
+ toggleFullscreen() {
291
+ if (document.fullscreenElement === this.box) void this.exit();
292
+ else void this.enterFullscreen();
293
+ }
294
+ cancelAdvance() {
295
+ if (this.advanceTimer) {
296
+ clearTimeout(this.advanceTimer);
297
+ this.advanceTimer = null;
298
+ }
299
+ if (this.loopTimer) {
300
+ clearTimeout(this.loopTimer);
301
+ this.loopTimer = null;
302
+ }
303
+ }
304
+ handleStop(e) {
305
+ this.refresh();
306
+ this.cancelAdvance();
307
+ if (this.options.autoAdvance && e.stop.advance !== void 0 && this.coverEl.getAttribute("data-mode") === "off") {
308
+ this.advanceTimer = setTimeout(() => {
309
+ this.advanceTimer = null;
310
+ void this.next();
311
+ }, e.stop.advance * 1e3);
312
+ }
313
+ }
314
+ handleEnded() {
315
+ this.refresh();
316
+ if (!this.options.loop) return;
317
+ this.cancelAdvance();
318
+ this.loopTimer = setTimeout(() => {
319
+ this.loopTimer = null;
320
+ void this.run(async () => {
321
+ await this.movie.gotoFrame(0, true);
322
+ await this.movie.next();
323
+ });
324
+ }, 800);
325
+ }
326
+ refresh() {
327
+ const m = this.movie;
328
+ const pages = m.pageCount ?? 0, stops = m.stops?.length ?? 0;
329
+ if (this.counterEl) {
330
+ this.counterEl.textContent = pages > 0 ? `${m.pageIndex >= 0 ? m.pageIndex + 1 : "\u2013"} / ${pages}` : "";
331
+ this.counterEl.style.display = pages > 0 ? "" : "none";
332
+ }
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));
335
+ this.refreshView();
336
+ }
337
+ wake() {
338
+ this.root.setAttribute("data-idle", "false");
339
+ if (this.options.clickToAdvance) this.options.canvas.style.cursor = "pointer";
340
+ if (this.idleTimer) clearTimeout(this.idleTimer);
341
+ this.idleTimer = setTimeout(() => {
342
+ this.root.setAttribute("data-idle", "true");
343
+ this.options.canvas.style.cursor = "none";
344
+ }, IDLE_MS);
345
+ }
346
+ showDigits() {
347
+ this.jumpEl.textContent = this.digits ? `\u2192 ${this.digits}` : "";
348
+ this.jumpEl.setAttribute("data-active", this.digits ? "true" : "false");
349
+ if (this.digitsTimer) clearTimeout(this.digitsTimer);
350
+ this.digitsTimer = this.digits ? setTimeout(() => {
351
+ this.digits = "";
352
+ this.showDigits();
353
+ }, DIGITS_MS) : null;
354
+ }
355
+ /** Something that moves the presentation: if the screen is blacked out (or the key list is open) the first press only brings it back. */
356
+ navigate(go) {
357
+ if (this.coverEl.getAttribute("data-mode") !== "off") {
358
+ this.setCover("off");
359
+ return;
360
+ }
361
+ if (this.helpEl.getAttribute("data-open") === "true") {
362
+ this.helpEl.setAttribute("data-open", "false");
363
+ return;
364
+ }
365
+ go();
366
+ }
367
+ handleKey(e) {
368
+ if (this.destroyed || e.ctrlKey || e.metaKey || e.altKey) return;
369
+ const t = e.target;
370
+ if (t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.tagName === "SELECT" || t.isContentEditable)) return;
371
+ const k = e.key;
372
+ let handled = true;
373
+ this.wake();
374
+ if (k === "g" || k === "G") {
375
+ this.toggleOverview();
376
+ e.preventDefault();
377
+ return;
378
+ }
379
+ if (this.overviewOpen()) {
380
+ if (this.handleOverviewKey(e)) e.preventDefault();
381
+ return;
382
+ }
383
+ if (k >= "0" && k <= "9" && k.length === 1) {
384
+ this.digits = (this.digits + k).slice(0, 4);
385
+ this.showDigits();
386
+ } else if (k === "Enter" && this.digits) {
387
+ const page = Number(this.digits) - 1;
388
+ this.digits = "";
389
+ this.showDigits();
390
+ this.navigate(() => {
391
+ this.cancelAdvance();
392
+ void this.run(() => this.movie.goToPage(page));
393
+ });
394
+ } else if (k === "Backspace" && this.digits) {
395
+ this.digits = this.digits.slice(0, -1);
396
+ this.showDigits();
397
+ } else if (NEXT_KEYS.has(k)) this.navigate(() => void this.next());
398
+ else if (PREV_KEYS.has(k)) this.navigate(() => void this.prev());
399
+ else if (k === "Home") this.navigate(() => {
400
+ this.cancelAdvance();
401
+ void this.run(() => this.movie.goToPage(0));
402
+ });
403
+ else if (k === "End") this.navigate(() => {
404
+ this.cancelAdvance();
405
+ void this.run(() => this.movie.goToStop(Math.max(0, (this.movie.stops?.length ?? 1) - 1)));
406
+ });
407
+ else if (k === "b" || k === "B" || k === ".") this.setCover(this.coverEl.getAttribute("data-mode") === "black" ? "off" : "black");
408
+ else if (k === "w" || k === "W" || k === ",") this.setCover(this.coverEl.getAttribute("data-mode") === "white" ? "off" : "white");
409
+ else if (k === "p" || k === "P") this.togglePresenterView();
410
+ else if (k === "f" || k === "F") this.toggleFullscreen();
411
+ else if (k === "?" || k === "h" || k === "H") this.helpEl.setAttribute("data-open", this.helpEl.getAttribute("data-open") === "true" ? "false" : "true");
412
+ else if (k === "Escape") {
413
+ if (this.helpEl.getAttribute("data-open") === "true") this.helpEl.setAttribute("data-open", "false");
414
+ else if (this.coverEl.getAttribute("data-mode") !== "off") this.setCover("off");
415
+ else if (this.digits) {
416
+ this.digits = "";
417
+ this.showDigits();
418
+ } else if (document.fullscreenElement === this.box) handled = false;
419
+ else if (this.options.onExit) this.options.onExit();
420
+ } else handled = false;
421
+ if (handled) e.preventDefault();
422
+ }
423
+ // ── the presenter view: a second window for the speaker ──
424
+ togglePresenterView() {
425
+ if (this.view && !this.view.win.closed) this.closePresenterView();
426
+ else this.openPresenterView();
427
+ }
428
+ /** Open the speaker's window: what is on screen (live), the next picture, the notes of the page, a timer, and next / back. Returns it (null if the browser blocked the popup). */
429
+ openPresenterView() {
430
+ if (this.view && !this.view.win.closed) {
431
+ this.view.win.focus();
432
+ return this.view.win;
433
+ }
434
+ this.closePresenterView();
435
+ const win = window.open("", "pixi-effects-presenter-view", "popup=yes,width=1180,height=720");
436
+ if (!win) {
437
+ console.warn("pixi-effects: Presenter: the browser blocked the presenter view popup; allow popups for this page and press P again.");
438
+ return null;
439
+ }
440
+ const doc = win.document;
441
+ doc.title = "Presenter view";
442
+ const style = doc.createElement("style");
443
+ style.textContent = VIEW_CSS;
444
+ doc.head.appendChild(style);
445
+ const { width, height } = this.options.canvas;
446
+ doc.body.innerHTML = `<div class="pv" style="--pv-ratio: ${width} / ${height}">
447
+ <div class="pv-main"><div class="pv-label">NOW</div><canvas class="pv-now" width="${Math.min(width, 960)}" height="${Math.round(height * Math.min(width, 960) / width)}"></canvas></div>
448
+ <div class="pv-side">
449
+ <div class="pv-top"><span class="pv-count"></span><span class="pv-title"></span><span class="pv-timer">00:00</span><button class="pv-reset" type="button">reset</button></div>
450
+ <div><div class="pv-label">NEXT</div><div class="pv-nextbox" data-end="false"><img class="pv-next" alt=""><div class="pv-end">END OF THE TALK</div></div></div>
451
+ <div class="pv-notes" data-empty="true"></div>
452
+ <div class="pv-buttons"><button class="pv-prev" type="button">\u25C0 Back</button><button class="pv-nextbtn" type="button">Next \u25B6</button></div>
453
+ </div></div>`;
454
+ const els = {};
455
+ for (const c of ["count", "title", "timer", "next", "nextbox", "notes", "now"]) els[c] = doc.querySelector(`.pv-${c}`);
456
+ const started = Date.now();
457
+ const onClose = () => {
458
+ if (this.view && this.view.win === win) this.closePresenterView();
459
+ };
460
+ const timer = setInterval(() => {
461
+ els.timer.textContent = clock(Date.now() - this.view.started);
462
+ }, 1e3);
463
+ this.view = { win, timer, started, els, onClose };
464
+ doc.querySelector(".pv-nextbtn").addEventListener("click", () => void this.next());
465
+ doc.querySelector(".pv-prev").addEventListener("click", () => void this.prev());
466
+ doc.querySelector(".pv-reset").addEventListener("click", () => {
467
+ this.view.started = Date.now();
468
+ els.timer.textContent = "00:00";
469
+ });
470
+ if (this.options.keyboard) doc.addEventListener("keydown", this.onKey);
471
+ win.addEventListener?.("pagehide", onClose);
472
+ this.movie.on("frame", this.onFrame);
473
+ void this.ensureThumbs();
474
+ this.refreshView();
475
+ this.mirror();
476
+ return win;
477
+ }
478
+ closePresenterView() {
479
+ const v = this.view;
480
+ if (!v) return;
481
+ this.view = null;
482
+ clearInterval(v.timer);
483
+ this.movie.off("frame", this.onFrame);
484
+ try {
485
+ v.win.document.removeEventListener("keydown", this.onKey);
486
+ v.win.removeEventListener?.("pagehide", v.onClose);
487
+ } catch {
488
+ }
489
+ if (!v.win.closed) {
490
+ try {
491
+ v.win.close();
492
+ } catch {
493
+ }
494
+ }
495
+ }
496
+ /** Copy what the movie has just drawn into the view (called from the `frame` event, right after the render, when the canvas can be read). */
497
+ mirror() {
498
+ const v = this.view;
499
+ if (!v || v.win.closed) return;
500
+ const c = v.els.now;
501
+ c.getContext("2d")?.drawImage(this.options.canvas, 0, 0, c.width, c.height);
502
+ }
503
+ refreshView() {
504
+ const v = this.view;
505
+ if (!v || v.win.closed) return;
506
+ const m = this.movie;
507
+ 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;
513
+ let notes = "";
514
+ for (const s of stops) if (s.pageIndex === pageIndex && s.index <= upTo && s.notes) notes = s.notes;
515
+ v.els.notes.textContent = notes;
516
+ 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;
520
+ if (src) v.els.next.setAttribute("src", src);
521
+ else v.els.next.removeAttribute("src");
522
+ }
523
+ // ── the overview ──
524
+ pageStops() {
525
+ return (this.movie.stops ?? []).filter((s) => s.pageStart);
526
+ }
527
+ overviewOpen() {
528
+ return this.overviewEl.getAttribute("data-open") === "true";
529
+ }
530
+ toggleOverview() {
531
+ if (this.overviewOpen()) {
532
+ this.overviewEl.setAttribute("data-open", "false");
533
+ return;
534
+ }
535
+ if (this.pageStops().length === 0) return;
536
+ this.cancelAdvance();
537
+ if (this.movie.isPlaying) this.movie.pause();
538
+ this.helpEl.setAttribute("data-open", "false");
539
+ this.buildOverview();
540
+ this.overviewEl.setAttribute("data-open", "true");
541
+ void this.ensureThumbs();
542
+ }
543
+ /**
544
+ * Make a picture of every stop, once. Taking them moves the playhead through the stops and back, which the audience would see:
545
+ * `start()` does it before anything is shown (behind a "Preparing" cover), and an overview opened before that does it on the spot.
546
+ */
547
+ ensureThumbs() {
548
+ this.thumbsPromise ??= (async () => {
549
+ try {
550
+ await this.movie.stopImages({
551
+ which: "stops",
552
+ as: "dataURL",
553
+ type: "image/jpeg",
554
+ quality: 0.72,
555
+ scale: Math.min(1, 480 / Math.max(1, this.options.canvas.width)),
556
+ onImage: (img) => {
557
+ this.thumbs.set(img.stop.index, img.image);
558
+ this.onThumb(img.stop.index);
559
+ }
560
+ });
561
+ } catch (err) {
562
+ console.warn("pixi-effects: Presenter: could not make the page pictures:", err);
563
+ this.thumbsPromise = null;
564
+ }
565
+ })();
566
+ return this.thumbsPromise;
567
+ }
568
+ /** A page's picture is its last stop's (the page fully built). */
569
+ pageThumb(page) {
570
+ const stops = (this.movie.stops ?? []).filter((s) => s.pageIndex === page);
571
+ return stops.length ? this.thumbs.get(stops[stops.length - 1].index) : void 0;
572
+ }
573
+ onThumb(stopIndex) {
574
+ const stop = this.movie.stops?.[stopIndex];
575
+ if (stop) this.fillThumb(stop.pageIndex);
576
+ this.refreshView();
577
+ }
578
+ buildOverview() {
579
+ const pages = this.pageStops();
580
+ const { width, height } = this.options.canvas;
581
+ const cols = Math.min(5, Math.max(2, Math.ceil(Math.sqrt(pages.length))));
582
+ this.gridEl.style.setProperty("--mp-cols", String(cols));
583
+ this.gridEl.style.setProperty("--mp-ratio", `${width} / ${height}`);
584
+ this.gridEl.innerHTML = "";
585
+ const current = Math.max(0, this.movie.pageIndex ?? 0);
586
+ pages.forEach((s, i) => {
587
+ const cell = document.createElement("div");
588
+ cell.className = "mp-cell";
589
+ cell.setAttribute("data-current", String(i === this.movie.pageIndex));
590
+ cell.setAttribute("data-selected", String(i === current));
591
+ cell.innerHTML = `<div class="mp-cell-label"><span class="mp-cell-num">${i + 1}</span><span class="mp-cell-name"></span></div>`;
592
+ cell.querySelector(".mp-cell-name").textContent = s.page ?? "";
593
+ cell.addEventListener("click", () => {
594
+ this.overviewEl.setAttribute("data-open", "false");
595
+ this.cancelAdvance();
596
+ void this.run(() => this.movie.goToPage(i));
597
+ });
598
+ this.gridEl.appendChild(cell);
599
+ this.fillThumb(i);
600
+ });
601
+ this.overviewSel = current;
602
+ }
603
+ fillThumb(page) {
604
+ const cell = this.gridEl.children[page];
605
+ const src = this.pageThumb(page);
606
+ if (!cell || !src || cell.querySelector("img")) return;
607
+ const img = document.createElement("img");
608
+ img.alt = "";
609
+ img.src = src;
610
+ cell.insertBefore(img, cell.firstChild);
611
+ }
612
+ select(i) {
613
+ const cells = [...this.gridEl.children];
614
+ this.overviewSel = Math.min(cells.length - 1, Math.max(0, i));
615
+ cells.forEach((c, k) => c.setAttribute("data-selected", String(k === this.overviewSel)));
616
+ cells[this.overviewSel]?.scrollIntoView?.({ block: "nearest" });
617
+ }
618
+ handleOverviewKey(e) {
619
+ const cols = Number(this.gridEl.style.getPropertyValue("--mp-cols")) || 3;
620
+ switch (e.key) {
621
+ case "ArrowRight":
622
+ this.select(this.overviewSel + 1);
623
+ return true;
624
+ case "ArrowLeft":
625
+ this.select(this.overviewSel - 1);
626
+ return true;
627
+ case "ArrowDown":
628
+ this.select(this.overviewSel + cols);
629
+ return true;
630
+ case "ArrowUp":
631
+ this.select(this.overviewSel - cols);
632
+ return true;
633
+ case "Enter": {
634
+ const page = this.overviewSel;
635
+ this.overviewEl.setAttribute("data-open", "false");
636
+ this.cancelAdvance();
637
+ void this.run(() => this.movie.goToPage(page));
638
+ return true;
639
+ }
640
+ case "Escape":
641
+ this.overviewEl.setAttribute("data-open", "false");
642
+ return true;
643
+ default:
644
+ return NEXT_KEYS.has(e.key) || PREV_KEYS.has(e.key);
645
+ }
646
+ }
647
+ handleClick() {
648
+ if (this.swiped) {
649
+ this.swiped = false;
650
+ return;
651
+ }
652
+ this.wake();
653
+ this.navigate(() => void this.next());
654
+ }
655
+ handlePointerUp(e) {
656
+ const d = this.downAt;
657
+ this.downAt = null;
658
+ if (!d) return;
659
+ const dx = e.clientX - d.x, dy = e.clientY - d.y;
660
+ if (Math.abs(dx) < SWIPE_PX || Math.abs(dy) > Math.abs(dx)) return;
661
+ this.swiped = true;
662
+ this.wake();
663
+ this.navigate(() => void (dx < 0 ? this.next() : this.prev()));
664
+ }
665
+ };
666
+
667
+ export { Presenter };
668
+ //# sourceMappingURL=Presenter.js.map
669
+ //# sourceMappingURL=Presenter.js.map