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.
- package/CHANGELOG.md +28 -0
- package/README.md +11 -8
- package/ai/SKILL.md +2 -1
- package/ai/reference/cheatsheet.md +12 -1
- package/ai/reference/pitfalls.md +4 -0
- package/ai/reference/recipes.md +57 -0
- package/ai/template.html +8 -4
- package/ai/tools/check.mjs +17 -1
- package/ai/tools/render.mjs +13 -9
- package/dist/{Base-CwUxTfja.d.cts → Base-C6ZnJ9pD.d.cts} +1 -1
- package/dist/{Base-CyvSLoCP.d.ts → Base-CQOlB2VH.d.ts} +1 -1
- package/dist/Controller.cjs +132 -15
- package/dist/Controller.cjs.map +1 -1
- package/dist/Controller.d.cts +25 -3
- package/dist/Controller.d.ts +25 -3
- package/dist/Controller.js +132 -15
- package/dist/Controller.js.map +1 -1
- package/dist/{Movie-DSSNOF3T.d.cts → Movie-DnR4H1OO.d.ts} +93 -2
- package/dist/{Movie-nzmWsxWX.d.ts → Movie-Tl5dRLJl.d.cts} +93 -2
- package/dist/Presenter.cjs +671 -0
- package/dist/Presenter.cjs.map +1 -0
- package/dist/Presenter.d.cts +123 -0
- package/dist/Presenter.d.ts +123 -0
- package/dist/Presenter.js +669 -0
- package/dist/Presenter.js.map +1 -0
- package/dist/index.cjs +665 -36
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +165 -5
- package/dist/index.d.ts +165 -5
- package/dist/index.js +659 -37
- package/dist/index.js.map +1 -1
- package/dist/loader.css +1 -1
- package/dist/three.d.cts +2 -2
- package/dist/three.d.ts +2 -2
- package/dist/{types-BXu28nJj.d.cts → types-CBSiUKau.d.cts} +56 -1
- package/dist/{types-BXu28nJj.d.ts → types-CBSiUKau.d.ts} +56 -1
- package/docs/api.md +80 -0
- package/docs/dsl.md +48 -0
- package/llms-full.txt +203 -2
- package/llms.txt +3 -3
- package/package.json +5 -1
package/dist/Controller.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { f as Movie } from './Movie-
|
|
1
|
+
import { f as Movie } from './Movie-DnR4H1OO.js';
|
|
2
2
|
import 'pixi.js';
|
|
3
3
|
import 'gsap';
|
|
4
|
-
import './types-
|
|
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-progress-fill {\n position: absolute; left: 12px; top: 50%;\n height: var(--mc-track-height, 3px);\n width: calc((100% - 24px) * var(--mc-fill, 0));\n background: var(--mc-accent, #007AFF);\n transform: translateY(-50%);\n transition: height 120ms ease;\n pointer-events: none;\n}\n.mc-progress:hover .mc-progress-fill,\n.mc-progress.mc-scrubbing .mc-progress-fill { height: calc(var(--mc-track-height, 3px) + 2px); }\n.mc-progress-thumb {\n position: absolute; top: 50%;\n left: calc(12px + (100% - 24px) * var(--mc-fill, 0));\n width: 12px; height: 12px; border-radius: 50%;\n background: var(--mc-accent, #007AFF);\n transform: translate(-50%, -50%) scale(0);\n transition: transform 120ms ease;\n pointer-events: none;\n}\n.mc-progress:hover .mc-progress-thumb,\n.mc-progress.mc-scrubbing .mc-progress-thumb { transform: translate(-50%, -50%) scale(1); }\n\n.mc-bar {\n position: relative;\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 6px 12px 8px 12px;\n background: var(--mc-bar-bg, linear-gradient(to top, rgba(0,0,0,0.75) 0%, rgba(0,0,0,0) 100%));\n color: var(--mc-fg, #fff);\n line-height: 1;\n}\n.mc-btn {\n background: none; border: 0; padding: 0; margin: 0;\n width: 28px; height: 28px;\n display: inline-flex; align-items: center; justify-content: center;\n color: var(--mc-fg, #fff); opacity: 0.85; cursor: pointer;\n transition: opacity 120ms ease;\n}\n.mc-btn:hover { opacity: 1; }\n.mc-btn:disabled { opacity: 0.4; cursor: not-allowed; }\n.mc-time {\n font-size: 12px; font-family: Menlo, Monaco, monospace;\n color: var(--mc-fg, #fff); opacity: 0.85;\n min-width: 90px;\n}\n.mc-spacer { flex: 1; }\n\n.mc-volume { display: flex; align-items: center; }\n.mc-vol-slider {\n position: relative;\n width: 0;\n height: 28px;\n display: flex;\n align-items: center;\n cursor: pointer;\n margin-left: 0;\n outline: none;\n transition: width 200ms ease, margin-left 200ms ease;\n overflow: hidden;\n}\n.mc-volume:hover .mc-vol-slider,\n.mc-volume:focus-within .mc-vol-slider,\n.mc-vol-slider.mc-scrubbing {\n width: 70px;\n margin-left: 6px;\n}\n.mc-vol-slider:focus-visible { outline: 2px solid var(--mc-accent, #007AFF); outline-offset: 2px; }\n.mc-vol-slider::before {\n content: \"\"; position: absolute; left: 4px; right: 4px;\n height: 3px; background: var(--mc-track, rgba(255,255,255,0.25)); border-radius: 2px;\n}\n.mc-vol-fill {\n position: absolute; left: 4px; top: 50%;\n height: 3px;\n width: calc((100% - 8px) * var(--mc-volume, 1));\n background: var(--mc-fg, #fff);\n transform: translateY(-50%);\n border-radius: 2px;\n pointer-events: none;\n}\n.mc-vol-thumb {\n position: absolute; top: 50%;\n left: calc(4px + (100% - 8px) * var(--mc-volume, 1));\n width: 10px; height: 10px; border-radius: 50%;\n background: var(--mc-fg, #fff);\n transform: translate(-50%, -50%);\n pointer-events: none;\n}\n\n[data-mc-wrap]:fullscreen {\n width: 100vw; height: 100vh;\n display: flex; align-items: center; justify-content: center;\n background: #000;\n}\n[data-mc-wrap]:fullscreen > canvas {\n width: 100% !important;\n height: 100% !important;\n max-width: none !important;\n max-height: none !important;\n object-fit: contain;\n border-radius: 0;\n}\n\n.mc-settings-popover {\n position: absolute;\n bottom: calc(100% + 8px);\n right: 12px;\n background: rgba(20, 20, 20, 0.96);\n border-radius: 8px;\n padding: 10px 12px;\n min-width: 180px;\n color: #fff;\n font-size: 12px;\n box-shadow: 0 4px 16px rgba(0,0,0,0.4);\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n.mc-settings-popover[data-open=\"false\"] { display: none; }\n.mc-settings-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n}\n.mc-settings-row > span { color: rgba(255,255,255,0.85); }\n.mc-settings-row > select {\n font: inherit;\n background: rgba(255,255,255,0.1);\n color: #fff;\n border: 1px solid rgba(255,255,255,0.18);\n border-radius: 4px;\n padding: 2px 6px;\n cursor: pointer;\n}\n.mc-export-confirm {\n margin-top: 4px;\n width: 100%;\n background: var(--mc-accent, #007AFF);\n color: #fff;\n border: 0;\n border-radius: 4px;\n padding: 6px 10px;\n font: inherit;\n font-weight: 600;\n cursor: pointer;\n transition: background 120ms ease;\n}\n.mc-export-confirm:hover { background: #0066d6; }\n.mc-export-confirm:disabled { background: rgba(255,255,255,0.18); cursor: not-allowed; }\n\n.mc-export-overlay {\n position: absolute; inset: 0;\n background: rgba(0,0,0,0.7);\n display: flex; justify-content: center; align-items: center;\n z-index: 2;\n}\n.mc-export-panel {\n background: rgba(20,20,20,0.96); color: #fff;\n border-radius: 8px; padding: 16px 20px;\n text-align: center; min-width: 200px; max-width: 80%;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n}\n.mc-export-title { font-size: 13px; margin-bottom: 10px; opacity: 0.9; }\n.mc-export-track {\n width: 100%; height: 6px;\n background: rgba(255,255,255,0.15); border-radius: 3px;\n margin-bottom: 8px; overflow: hidden;\n}\n.mc-export-fill {\n height: 100%; width: 0%;\n background: linear-gradient(90deg, var(--mc-accent, #007AFF), var(--mc-accent, #0056CC));\n transition: width 0.3s ease; border-radius: 3px;\n}\n.mc-export-text {\n color: rgba(255,255,255,0.7); font-size: 11px;\n font-family: Menlo, Monaco, monospace;\n}\n";
|
|
7
|
+
declare const CONTROLLER_CSS = "\n.movie-controller-wrap { position: relative; display: inline-block; line-height: 0; }\n.movie-controller {\n position: absolute;\n pointer-events: none;\n font-family: var(--mc-font, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif);\n display: flex;\n flex-direction: column;\n justify-content: flex-end;\n opacity: 1;\n transition: opacity 200ms ease;\n}\n.movie-controller[data-state=\"hidden\"] { opacity: 0; }\n.movie-controller > * { pointer-events: auto; }\n\n.mc-progress {\n position: relative;\n width: 100%;\n height: 16px;\n cursor: pointer;\n display: flex;\n align-items: center;\n outline: none;\n}\n.mc-progress:focus-visible {\n outline: 2px solid var(--mc-accent, #007AFF);\n outline-offset: 2px;\n}\n.mc-progress::before {\n content: \"\"; position: absolute; left: 12px; right: 12px;\n height: var(--mc-track-height, 3px); background: var(--mc-track, rgba(255,255,255,0.25));\n transition: height 120ms ease;\n}\n.mc-progress:hover::before, .mc-progress.mc-scrubbing::before { height: calc(var(--mc-track-height, 3px) + 2px); }\n.mc-stops { position: absolute; left: 12px; right: 12px; top: 0; bottom: 0; pointer-events: none; }\n.mc-stop-tick { position: absolute; top: 50%; width: 2px; height: calc(var(--mc-track-height, 3px) + 10px); transform: translate(-50%, -50%); background: var(--mc-fg, #fff); opacity: 0.9; border-radius: 1px; box-shadow: 0 0 0 1px rgba(0,0,0,0.55); }\n.mc-stop-page { width: 4px; height: calc(var(--mc-track-height, 3px) + 20px); opacity: 1; }\n.mc-stop-page::before { content: ''; position: absolute; left: 50%; top: -3px; width: 8px; height: 8px; transform: translateX(-50%); border-radius: 50%; background: var(--mc-fg, #fff); box-shadow: 0 0 0 1px rgba(0,0,0,0.55); }\n.mc-progress-fill {\n position: absolute; left: 12px; top: 50%;\n height: var(--mc-track-height, 3px);\n width: calc((100% - 24px) * var(--mc-fill, 0));\n background: var(--mc-accent, #007AFF);\n transform: translateY(-50%);\n transition: height 120ms ease;\n pointer-events: none;\n}\n.mc-progress:hover .mc-progress-fill,\n.mc-progress.mc-scrubbing .mc-progress-fill { height: calc(var(--mc-track-height, 3px) + 2px); }\n.mc-progress-thumb {\n position: absolute; top: 50%;\n left: calc(12px + (100% - 24px) * var(--mc-fill, 0));\n width: 12px; height: 12px; border-radius: 50%;\n background: var(--mc-accent, #007AFF);\n transform: translate(-50%, -50%) scale(0);\n transition: transform 120ms ease;\n pointer-events: none;\n}\n.mc-progress:hover .mc-progress-thumb,\n.mc-progress.mc-scrubbing .mc-progress-thumb { transform: translate(-50%, -50%) scale(1); }\n\n.mc-bar {\n position: relative;\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 6px 12px 8px 12px;\n background: var(--mc-bar-bg, linear-gradient(to top, rgba(0,0,0,0.75) 0%, rgba(0,0,0,0) 100%));\n color: var(--mc-fg, #fff);\n line-height: 1;\n}\n.mc-btn {\n background: none; border: 0; padding: 0; margin: 0;\n width: 28px; height: 28px;\n display: inline-flex; align-items: center; justify-content: center;\n color: var(--mc-fg, #fff); opacity: 0.85; cursor: pointer;\n transition: opacity 120ms ease;\n}\n.mc-btn:hover { opacity: 1; }\n.mc-btn:disabled { opacity: 0.4; cursor: not-allowed; }\n.mc-time {\n font-size: 12px; font-family: Menlo, Monaco, monospace;\n color: var(--mc-fg, #fff); opacity: 0.85;\n min-width: 90px;\n}\n.mc-spacer { flex: 1; }\n\n.mc-volume { display: flex; align-items: center; }\n.mc-vol-slider {\n position: relative;\n width: 0;\n height: 28px;\n display: flex;\n align-items: center;\n cursor: pointer;\n margin-left: 0;\n outline: none;\n transition: width 200ms ease, margin-left 200ms ease;\n overflow: hidden;\n}\n.mc-volume:hover .mc-vol-slider,\n.mc-volume:focus-within .mc-vol-slider,\n.mc-vol-slider.mc-scrubbing {\n width: 70px;\n margin-left: 6px;\n}\n.mc-vol-slider:focus-visible { outline: 2px solid var(--mc-accent, #007AFF); outline-offset: 2px; }\n.mc-vol-slider::before {\n content: \"\"; position: absolute; left: 4px; right: 4px;\n height: 3px; background: var(--mc-track, rgba(255,255,255,0.25)); border-radius: 2px;\n}\n.mc-vol-fill {\n position: absolute; left: 4px; top: 50%;\n height: 3px;\n width: calc((100% - 8px) * var(--mc-volume, 1));\n background: var(--mc-fg, #fff);\n transform: translateY(-50%);\n border-radius: 2px;\n pointer-events: none;\n}\n.mc-vol-thumb {\n position: absolute; top: 50%;\n left: calc(4px + (100% - 8px) * var(--mc-volume, 1));\n width: 10px; height: 10px; border-radius: 50%;\n background: var(--mc-fg, #fff);\n transform: translate(-50%, -50%);\n pointer-events: none;\n}\n\n[data-mc-wrap]:fullscreen {\n width: 100vw; height: 100vh;\n display: flex; align-items: center; justify-content: center;\n background: #000;\n}\n[data-mc-wrap]:fullscreen > canvas {\n width: 100% !important;\n height: 100% !important;\n max-width: none !important;\n max-height: none !important;\n object-fit: contain;\n border-radius: 0;\n}\n\n.mc-settings-popover {\n position: absolute;\n bottom: calc(100% + 8px);\n right: 12px;\n background: rgba(20, 20, 20, 0.96);\n border-radius: 8px;\n padding: 10px 12px;\n min-width: 180px;\n color: #fff;\n font-size: 12px;\n box-shadow: 0 4px 16px rgba(0,0,0,0.4);\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n.mc-settings-popover[data-open=\"false\"] { display: none; }\n.mc-settings-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n}\n.mc-settings-row > span { color: rgba(255,255,255,0.85); }\n.mc-settings-row > select {\n font: inherit;\n background: rgba(255,255,255,0.1);\n color: #fff;\n border: 1px solid rgba(255,255,255,0.18);\n border-radius: 4px;\n padding: 2px 6px;\n cursor: pointer;\n}\n.mc-export-confirm {\n margin-top: 4px;\n width: 100%;\n background: var(--mc-accent, #007AFF);\n color: #fff;\n border: 0;\n border-radius: 4px;\n padding: 6px 10px;\n font: inherit;\n font-weight: 600;\n cursor: pointer;\n transition: background 120ms ease;\n}\n.mc-export-confirm:hover { background: #0066d6; }\n.mc-export-confirm:disabled { background: rgba(255,255,255,0.18); cursor: not-allowed; }\n\n.mc-export-overlay {\n position: absolute; inset: 0;\n background: rgba(0,0,0,0.7);\n display: flex; justify-content: center; align-items: center;\n z-index: 2;\n}\n.mc-export-panel {\n background: rgba(20,20,20,0.96); color: #fff;\n border-radius: 8px; padding: 16px 20px;\n text-align: center; min-width: 200px; max-width: 80%;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n}\n.mc-export-title { font-size: 13px; margin-bottom: 10px; opacity: 0.9; }\n.mc-export-track {\n width: 100%; height: 6px;\n background: rgba(255,255,255,0.15); border-radius: 3px;\n margin-bottom: 8px; overflow: hidden;\n}\n.mc-export-fill {\n height: 100%; width: 0%;\n background: linear-gradient(90deg, var(--mc-accent, #007AFF), var(--mc-accent, #0056CC));\n transition: width 0.3s ease; border-radius: 3px;\n}\n.mc-export-text {\n color: rgba(255,255,255,0.7); font-size: 11px;\n font-family: Menlo, Monaco, monospace;\n}\n";
|
|
8
8
|
declare function formatTime(seconds: number): string;
|
|
9
9
|
declare function frameToPercent(frame: number, totalFrames: number): number;
|
|
10
10
|
declare function pxToFrame(clientX: number, rect: {
|
|
@@ -42,6 +42,10 @@ interface ControllerOptions {
|
|
|
42
42
|
enableKeyboardShortcuts?: boolean;
|
|
43
43
|
/** A click (or tap) on the picture plays / pauses, like a `<video>` element. Default true; `false` leaves the canvas alone. */
|
|
44
44
|
clickToPlay?: boolean;
|
|
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
|
+
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;
|
|
45
49
|
className?: string;
|
|
46
50
|
}
|
|
47
51
|
interface ResolvedOptions {
|
|
@@ -49,6 +53,8 @@ interface ResolvedOptions {
|
|
|
49
53
|
showExportButton: boolean;
|
|
50
54
|
enableKeyboardShortcuts: boolean;
|
|
51
55
|
clickToPlay: boolean;
|
|
56
|
+
present: boolean;
|
|
57
|
+
pauseAtStops: boolean;
|
|
52
58
|
className: string;
|
|
53
59
|
}
|
|
54
60
|
declare class Controller {
|
|
@@ -84,6 +90,7 @@ declare class Controller {
|
|
|
84
90
|
private wasPlayingBeforeScrub;
|
|
85
91
|
private activePointerId;
|
|
86
92
|
private isExporting;
|
|
93
|
+
private exportPages;
|
|
87
94
|
private exportOverlay;
|
|
88
95
|
private exportFillEl;
|
|
89
96
|
private exportTextEl;
|
|
@@ -91,6 +98,8 @@ declare class Controller {
|
|
|
91
98
|
private settingsKeyHandler;
|
|
92
99
|
private settingsOutsideHandler;
|
|
93
100
|
private onReady;
|
|
101
|
+
private presenter;
|
|
102
|
+
private presentBtn;
|
|
94
103
|
private onFrame;
|
|
95
104
|
private onPlay;
|
|
96
105
|
private onPause;
|
|
@@ -125,6 +134,17 @@ declare class Controller {
|
|
|
125
134
|
private fitWrap;
|
|
126
135
|
private bindFitWrap;
|
|
127
136
|
private bindMovieEvents;
|
|
137
|
+
/** The stops of a presentation as marks on the seek bar, and the Present button. */
|
|
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;
|
|
145
|
+
/** Hand the page over to a Presenter (fullscreen, keys, click and swipe move through the stops); Escape gives it back. */
|
|
146
|
+
private startPresenting;
|
|
147
|
+
private stopPresenting;
|
|
128
148
|
private bindPlayButton;
|
|
129
149
|
/** Clicking the picture plays / pauses (a tap on a phone too). */
|
|
130
150
|
private bindCanvasClick;
|
|
@@ -150,6 +170,8 @@ declare class Controller {
|
|
|
150
170
|
private handleExport;
|
|
151
171
|
private triggerDownload;
|
|
152
172
|
private makeFilename;
|
|
173
|
+
/** One more PDF page is ready: move the bar. */
|
|
174
|
+
private pdfPageDone;
|
|
153
175
|
private showExportOverlay;
|
|
154
176
|
private hideExportOverlay;
|
|
155
177
|
private bindVisibility;
|
package/dist/Controller.js
CHANGED
|
@@ -8,6 +8,7 @@ var ICONS = {
|
|
|
8
8
|
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>',
|
|
9
9
|
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>',
|
|
10
10
|
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>',
|
|
11
|
+
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>',
|
|
11
12
|
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>'
|
|
12
13
|
};
|
|
13
14
|
var STYLE_ATTR = "data-movie-controller";
|
|
@@ -45,6 +46,10 @@ var CONTROLLER_CSS = `
|
|
|
45
46
|
transition: height 120ms ease;
|
|
46
47
|
}
|
|
47
48
|
.mc-progress:hover::before, .mc-progress.mc-scrubbing::before { height: calc(var(--mc-track-height, 3px) + 2px); }
|
|
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) + 10px); transform: translate(-50%, -50%); background: var(--mc-fg, #fff); opacity: 0.9; border-radius: 1px; box-shadow: 0 0 0 1px rgba(0,0,0,0.55); }
|
|
51
|
+
.mc-stop-page { width: 4px; height: calc(var(--mc-track-height, 3px) + 20px); opacity: 1; }
|
|
52
|
+
.mc-stop-page::before { content: ''; position: absolute; left: 50%; top: -3px; width: 8px; height: 8px; transform: translateX(-50%); border-radius: 50%; background: var(--mc-fg, #fff); box-shadow: 0 0 0 1px rgba(0,0,0,0.55); }
|
|
48
53
|
.mc-progress-fill {
|
|
49
54
|
position: absolute; left: 12px; top: 50%;
|
|
50
55
|
height: var(--mc-track-height, 3px);
|
|
@@ -266,8 +271,10 @@ function pxToFraction(clientX, rect, inset = 0) {
|
|
|
266
271
|
const ratio = (clientX - innerLeft) / innerWidth;
|
|
267
272
|
return Math.min(Math.max(ratio, 0), 1);
|
|
268
273
|
}
|
|
274
|
+
var PDF_QUALITY = { "low": 0.6, "medium": 0.75, "high": 0.88, "very-high": 0.95 };
|
|
269
275
|
function extensionForMimeType(mime) {
|
|
270
276
|
const t = (mime || "").toLowerCase();
|
|
277
|
+
if (t.includes("pdf")) return "pdf";
|
|
271
278
|
if (t.includes("webm")) return "webm";
|
|
272
279
|
if (t.includes("quicktime") || t.includes("mov")) return "mov";
|
|
273
280
|
if (t.includes("matroska") || t.includes("mkv")) return "mkv";
|
|
@@ -314,6 +321,7 @@ var Controller = class _Controller {
|
|
|
314
321
|
wasPlayingBeforeScrub = false;
|
|
315
322
|
activePointerId = null;
|
|
316
323
|
isExporting = false;
|
|
324
|
+
exportPages = null;
|
|
317
325
|
exportOverlay = null;
|
|
318
326
|
exportFillEl = null;
|
|
319
327
|
exportTextEl = null;
|
|
@@ -321,6 +329,8 @@ var Controller = class _Controller {
|
|
|
321
329
|
settingsKeyHandler = null;
|
|
322
330
|
settingsOutsideHandler = null;
|
|
323
331
|
onReady = null;
|
|
332
|
+
presenter = null;
|
|
333
|
+
presentBtn = null;
|
|
324
334
|
onFrame = null;
|
|
325
335
|
onPlay = null;
|
|
326
336
|
onPause = null;
|
|
@@ -351,6 +361,8 @@ var Controller = class _Controller {
|
|
|
351
361
|
showExportButton: options.showExportButton ?? true,
|
|
352
362
|
enableKeyboardShortcuts: options.enableKeyboardShortcuts ?? true,
|
|
353
363
|
clickToPlay: options.clickToPlay ?? true,
|
|
364
|
+
present: options.present ?? true,
|
|
365
|
+
pauseAtStops: options.pauseAtStops ?? true,
|
|
354
366
|
className: options.className ?? "movie-controller"
|
|
355
367
|
};
|
|
356
368
|
installStyles();
|
|
@@ -369,6 +381,7 @@ var Controller = class _Controller {
|
|
|
369
381
|
}
|
|
370
382
|
this.root.setAttribute("data-state", "visible");
|
|
371
383
|
this.bindMovieEvents();
|
|
384
|
+
this.refreshStops();
|
|
372
385
|
this.bindPlayButton();
|
|
373
386
|
if (this.options.clickToPlay) this.bindCanvasClick();
|
|
374
387
|
this.bindScrubbing();
|
|
@@ -526,6 +539,7 @@ var Controller = class _Controller {
|
|
|
526
539
|
this.refreshTime(0);
|
|
527
540
|
this.refreshProgress(0);
|
|
528
541
|
this.refreshVolumeUI();
|
|
542
|
+
this.refreshStops();
|
|
529
543
|
};
|
|
530
544
|
this.onFrame = ({ frame, totalFrames }) => {
|
|
531
545
|
if (!this.isScrubbing) {
|
|
@@ -552,13 +566,92 @@ var Controller = class _Controller {
|
|
|
552
566
|
this.movie.on("pause", this.onPause);
|
|
553
567
|
this.movie.on("progress", this.onProgress);
|
|
554
568
|
}
|
|
569
|
+
/** The stops of a presentation as marks on the seek bar, and the Present button. */
|
|
570
|
+
refreshStops() {
|
|
571
|
+
this.progressEl.querySelector(".mc-stops")?.remove();
|
|
572
|
+
const stops = this.movie.stops ?? [];
|
|
573
|
+
this.refreshPdfChoices(stops.length > 0);
|
|
574
|
+
if (stops.length === 0) {
|
|
575
|
+
this.presentBtn?.remove();
|
|
576
|
+
this.presentBtn = null;
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
const total = this.movie.totalFrames || 1;
|
|
580
|
+
const holder = document.createElement("div");
|
|
581
|
+
holder.className = "mc-stops";
|
|
582
|
+
for (const s of stops) {
|
|
583
|
+
const tick = document.createElement("div");
|
|
584
|
+
tick.className = s.pageStart ? "mc-stop-tick mc-stop-page" : "mc-stop-tick";
|
|
585
|
+
tick.style.left = `${s.frame / total * 100}%`;
|
|
586
|
+
if (s.page) tick.title = s.page;
|
|
587
|
+
holder.appendChild(tick);
|
|
588
|
+
}
|
|
589
|
+
this.progressEl.appendChild(holder);
|
|
590
|
+
if (this.options.present && !this.presentBtn && this.fullscreenBtn) {
|
|
591
|
+
const btn = document.createElement("button");
|
|
592
|
+
btn.className = "mc-btn mc-present";
|
|
593
|
+
btn.setAttribute("aria-label", "Present");
|
|
594
|
+
btn.title = "Present";
|
|
595
|
+
btn.innerHTML = ICONS.present;
|
|
596
|
+
btn.addEventListener("click", () => {
|
|
597
|
+
void this.startPresenting();
|
|
598
|
+
});
|
|
599
|
+
this.fullscreenBtn.parentElement?.insertBefore(btn, this.fullscreenBtn);
|
|
600
|
+
this.presentBtn = btn;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
/** A deck can be downloaded as a PDF: the format list gets "PDF" choices while the movie has stops. */
|
|
604
|
+
refreshPdfChoices(has) {
|
|
605
|
+
const select = this.settingsFormatSelect;
|
|
606
|
+
if (!select) return;
|
|
607
|
+
const present = !!select.querySelector('option[value="pdf"]');
|
|
608
|
+
if (has && !present) {
|
|
609
|
+
for (const [value, label] of [["pdf", "PDF (pages)"], ["pdf-steps", "PDF (every step)"]]) {
|
|
610
|
+
const o = document.createElement("option");
|
|
611
|
+
o.value = value;
|
|
612
|
+
o.textContent = label;
|
|
613
|
+
select.appendChild(o);
|
|
614
|
+
}
|
|
615
|
+
} else if (!has && present) {
|
|
616
|
+
select.querySelectorAll('option[value="pdf"], option[value="pdf-steps"]').forEach((o) => o.remove());
|
|
617
|
+
if (this.exportFormat === "pdf" || this.exportFormat === "pdf-steps") {
|
|
618
|
+
this.exportFormat = "mp4";
|
|
619
|
+
select.value = "mp4";
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
/** Play, or pause: a movie with stops plays to the next stop (and from the end, again from the start). */
|
|
624
|
+
async togglePlay() {
|
|
625
|
+
if (this.movie.isPlaying) {
|
|
626
|
+
this.movie.pause();
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
await this.startPlaying();
|
|
630
|
+
}
|
|
631
|
+
/** Start playing: to the next stop for a movie with stops, to the end for any other (or with `pauseAtStops: false`). */
|
|
632
|
+
async startPlaying() {
|
|
633
|
+
if (this.options.pauseAtStops && (this.movie.stops?.length ?? 0) > 0) {
|
|
634
|
+
if (this.movie.currentFrame >= this.movie.totalFrames) await this.movie.gotoFrame(0, true);
|
|
635
|
+
await this.movie.next();
|
|
636
|
+
} else this.movie.play();
|
|
637
|
+
}
|
|
638
|
+
/** Hand the page over to a Presenter (fullscreen, keys, click and swipe move through the stops); Escape gives it back. */
|
|
639
|
+
async startPresenting() {
|
|
640
|
+
if (this.presenter || this.destroyed) return;
|
|
641
|
+
const { Presenter } = await import('./Presenter.js');
|
|
642
|
+
if (this.presenter || this.destroyed) return;
|
|
643
|
+
const p = new Presenter(this.movie, { canvas: this.options.canvas, onExit: () => this.stopPresenting() });
|
|
644
|
+
this.presenter = p;
|
|
645
|
+
await p.start();
|
|
646
|
+
}
|
|
647
|
+
stopPresenting() {
|
|
648
|
+
const p = this.presenter;
|
|
649
|
+
this.presenter = null;
|
|
650
|
+
p?.destroy();
|
|
651
|
+
}
|
|
555
652
|
bindPlayButton() {
|
|
556
653
|
this.playBtn.addEventListener("click", () => {
|
|
557
|
-
|
|
558
|
-
this.movie.pause();
|
|
559
|
-
} else {
|
|
560
|
-
this.movie.play();
|
|
561
|
-
}
|
|
654
|
+
void this.togglePlay();
|
|
562
655
|
});
|
|
563
656
|
}
|
|
564
657
|
/** Clicking the picture plays / pauses (a tap on a phone too). */
|
|
@@ -570,12 +663,12 @@ var Controller = class _Controller {
|
|
|
570
663
|
this.swallowCanvasClick = false;
|
|
571
664
|
};
|
|
572
665
|
this.canvasClickHandler = () => {
|
|
666
|
+
if (this.presenter) return;
|
|
573
667
|
if (this.swallowCanvasClick) {
|
|
574
668
|
this.swallowCanvasClick = false;
|
|
575
669
|
return;
|
|
576
670
|
}
|
|
577
|
-
|
|
578
|
-
else this.movie.play();
|
|
671
|
+
void this.togglePlay();
|
|
579
672
|
};
|
|
580
673
|
canvas.addEventListener("pointerdown", this.canvasPointerDownHandler);
|
|
581
674
|
canvas.addEventListener("click", this.canvasClickHandler);
|
|
@@ -731,7 +824,7 @@ var Controller = class _Controller {
|
|
|
731
824
|
} catch {
|
|
732
825
|
}
|
|
733
826
|
if (this.wasPlayingBeforeScrub) {
|
|
734
|
-
this.
|
|
827
|
+
void this.startPlaying();
|
|
735
828
|
this.refreshPlayIcon();
|
|
736
829
|
}
|
|
737
830
|
};
|
|
@@ -780,7 +873,7 @@ var Controller = class _Controller {
|
|
|
780
873
|
if (this.settingsFormatSelect) {
|
|
781
874
|
this.settingsFormatSelect.addEventListener("change", (e) => {
|
|
782
875
|
const v = e.target.value;
|
|
783
|
-
if (v === "mp4" || v === "webm" || v === "mov") this.exportFormat = v;
|
|
876
|
+
if (v === "mp4" || v === "webm" || v === "mov" || v === "pdf" || v === "pdf-steps") this.exportFormat = v;
|
|
784
877
|
});
|
|
785
878
|
}
|
|
786
879
|
if (this.settingsQualitySelect) {
|
|
@@ -830,9 +923,19 @@ var Controller = class _Controller {
|
|
|
830
923
|
this.movie.pause();
|
|
831
924
|
this.refreshPlayIcon();
|
|
832
925
|
}
|
|
833
|
-
this.
|
|
926
|
+
const pdf = this.exportFormat === "pdf" || this.exportFormat === "pdf-steps";
|
|
927
|
+
if (pdf) {
|
|
928
|
+
const stops = this.movie.stops ?? [];
|
|
929
|
+
const total = this.exportFormat === "pdf-steps" ? stops.filter((s) => s.pdf !== false).length : new Set(stops.map((s) => s.pageIndex)).size;
|
|
930
|
+
this.showExportOverlay("Exporting PDF...", `0% (0 / ${total} pages)`);
|
|
931
|
+
this.exportPages = { done: 0, total };
|
|
932
|
+
} else this.showExportOverlay();
|
|
834
933
|
try {
|
|
835
|
-
const blob = await this.movie.
|
|
934
|
+
const blob = pdf ? await this.movie.exportPDF({
|
|
935
|
+
which: this.exportFormat === "pdf-steps" ? "stops" : "pages",
|
|
936
|
+
quality: PDF_QUALITY[this.exportQuality],
|
|
937
|
+
onImage: () => this.pdfPageDone()
|
|
938
|
+
}) : await this.movie.render({
|
|
836
939
|
format: this.exportFormat,
|
|
837
940
|
video: { bitrate: this.exportQuality },
|
|
838
941
|
audio: { bitrate: this.exportQuality }
|
|
@@ -848,7 +951,7 @@ var Controller = class _Controller {
|
|
|
848
951
|
this.hideExportOverlay();
|
|
849
952
|
this.isExporting = false;
|
|
850
953
|
if (wasPlaying) {
|
|
851
|
-
this.
|
|
954
|
+
void this.startPlaying();
|
|
852
955
|
this.refreshPlayIcon();
|
|
853
956
|
}
|
|
854
957
|
}
|
|
@@ -871,16 +974,27 @@ var Controller = class _Controller {
|
|
|
871
974
|
const ts = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
|
|
872
975
|
return `movie-${ts}.${ext}`;
|
|
873
976
|
}
|
|
874
|
-
|
|
977
|
+
/** One more PDF page is ready: move the bar. */
|
|
978
|
+
pdfPageDone() {
|
|
979
|
+
const p = this.exportPages;
|
|
980
|
+
if (!p || !this.exportFillEl || !this.exportTextEl) return;
|
|
981
|
+
p.done++;
|
|
982
|
+
const percent = p.total ? Math.min(100, Math.round(p.done / p.total * 100)) : 100;
|
|
983
|
+
this.exportFillEl.style.width = `${percent}%`;
|
|
984
|
+
this.exportTextEl.textContent = `${percent}% (${p.done} / ${p.total} pages)`;
|
|
985
|
+
}
|
|
986
|
+
showExportOverlay(title = "Exporting video...", text = "0% (0 / 0 frames)") {
|
|
875
987
|
const overlay = document.createElement("div");
|
|
876
988
|
overlay.className = "mc-export-overlay";
|
|
877
989
|
overlay.innerHTML = `
|
|
878
990
|
<div class="mc-export-panel">
|
|
879
|
-
<div class="mc-export-title"
|
|
991
|
+
<div class="mc-export-title"></div>
|
|
880
992
|
<div class="mc-export-track"><div class="mc-export-fill"></div></div>
|
|
881
|
-
<div class="mc-export-text"
|
|
993
|
+
<div class="mc-export-text"></div>
|
|
882
994
|
</div>
|
|
883
995
|
`;
|
|
996
|
+
overlay.querySelector(".mc-export-title").textContent = title;
|
|
997
|
+
overlay.querySelector(".mc-export-text").textContent = text;
|
|
884
998
|
this.root.appendChild(overlay);
|
|
885
999
|
this.exportOverlay = overlay;
|
|
886
1000
|
this.exportFillEl = overlay.querySelector(".mc-export-fill");
|
|
@@ -893,6 +1007,7 @@ var Controller = class _Controller {
|
|
|
893
1007
|
this.exportFillEl = null;
|
|
894
1008
|
this.exportTextEl = null;
|
|
895
1009
|
}
|
|
1010
|
+
this.exportPages = null;
|
|
896
1011
|
}
|
|
897
1012
|
bindVisibility() {
|
|
898
1013
|
const wrap = this.wrapper;
|
|
@@ -935,6 +1050,7 @@ var Controller = class _Controller {
|
|
|
935
1050
|
}
|
|
936
1051
|
bindKeyboard() {
|
|
937
1052
|
this.keyHandler = (e) => {
|
|
1053
|
+
if (this.presenter) return;
|
|
938
1054
|
const target = e.target;
|
|
939
1055
|
if (target) {
|
|
940
1056
|
const tag = target.tagName;
|
|
@@ -996,6 +1112,7 @@ var Controller = class _Controller {
|
|
|
996
1112
|
destroy() {
|
|
997
1113
|
if (this.destroyed) return;
|
|
998
1114
|
this.destroyed = true;
|
|
1115
|
+
this.stopPresenting();
|
|
999
1116
|
if (this.onReady) {
|
|
1000
1117
|
this.movie.off("ready", this.onReady);
|
|
1001
1118
|
this.onReady = null;
|