pixi-effects 0.12.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 +12 -0
- package/README.md +5 -5
- package/ai/reference/cheatsheet.md +3 -3
- package/ai/reference/pitfalls.md +2 -2
- package/ai/template.html +5 -5
- package/ai/tools/check.mjs +1 -1
- package/dist/{Base-W3sQzGPO.d.cts → Base-C6ZnJ9pD.d.cts} +1 -1
- package/dist/{Base-CYH9vPCH.d.ts → Base-CQOlB2VH.d.ts} +1 -1
- package/dist/Controller.cjs +76 -17
- package/dist/Controller.cjs.map +1 -1
- package/dist/Controller.d.cts +15 -3
- package/dist/Controller.d.ts +15 -3
- package/dist/Controller.js +76 -17
- package/dist/Controller.js.map +1 -1
- package/dist/{Movie-CY2QPVVM.d.cts → Movie-DnR4H1OO.d.ts} +5 -2
- package/dist/{Movie-EBF6_RfX.d.ts → Movie-Tl5dRLJl.d.cts} +5 -2
- package/dist/Presenter.d.cts +2 -2
- package/dist/Presenter.d.ts +2 -2
- package/dist/index.cjs +32 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -5
- package/dist/index.d.ts +8 -5
- package/dist/index.js +32 -13
- 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-mpqQO8MJ.d.cts → types-CBSiUKau.d.cts} +18 -1
- package/dist/{types-mpqQO8MJ.d.ts → types-CBSiUKau.d.ts} +18 -1
- package/docs/api.md +4 -3
- package/docs/dsl.md +2 -2
- package/llms-full.txt +11 -10
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.13.0
|
|
4
|
+
|
|
5
|
+
**Changed**
|
|
6
|
+
|
|
7
|
+
- **The `Controller` plays a deck stop by stop.** For a movie with `stops`, the play button, Space and a click on the picture now play to the next stop and pause there (a second press while playing pauses; after the end, play starts again from the first page). `new Controller(movie, { pauseAtStops: false })` plays straight through as before; `movie.play()` itself still ignores stops. Scrubbing and the export resume use the same rule.
|
|
8
|
+
- **The stop marks are easier to see**: a taller white mark with a dark outline on the seek bar, and a page start is taller still, with a dot on top.
|
|
9
|
+
|
|
10
|
+
**Added**
|
|
11
|
+
|
|
12
|
+
- **`pdf` on a stop** chooses which moment stands for a page in a PDF and in the page overview: `{ at: 4, pdf: true }` is the page's picture, `{ at: 5, pdf: false }` keeps the stop out (default: the page's last stop). With `which: 'stops'` a PDF leaves out the stops flagged `false`; a page whose stops are all `false` is left out of the PDF. `deck()` takes `stops: [1.2, { at: 3, pdf: true }]`, and `pictureStops()` is the helper behind it. `movie.stopImages({ pdf: true })` applies the same rule for your own PDFs.
|
|
13
|
+
- **PDF in the player bar's download panel.** For a movie with stops the format list also offers PDF (pages) and PDF (every step); the quality choice becomes the JPEG quality, and the progress bar counts pages.
|
|
14
|
+
|
|
3
15
|
## 0.12.0
|
|
4
16
|
|
|
5
17
|
**Added**
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pixi-effects
|
|
2
2
|
|
|
3
|
-
> **Status**: experimental — current release `0.
|
|
3
|
+
> **Status**: experimental — current release `0.13.0`. The API may still change between minor versions (see [CHANGELOG](./CHANGELOG.md): `0.13.0` makes the player bar play a deck stop by stop (`pauseAtStops`), marks the stops more clearly, adds PDF to the download panel and a `pdf` flag on stops, `0.12.0` adds presentations (`stops`, `Presenter`, `deck()`, page overview, presenter view, PDF export) and audio-reactive visuals (`audioEnvelope()`, `react()`), `0.11.0` adds particles, motion blur, a shared loader and a much faster build for pieces with many layers, `0.10.0` adds seeded randomness, `wiggle` and `stagger`, null layers with `parent`, `animateText`, `followPath` and path morphing, and click-to-play in the player, `0.9.0` adds a poster time (`movie.init({ poster })`) and a guide site, `0.8.0` adds events and a theme for building your own player, `0.7.0` adds filters by name, the `pixi-effects-render` and `pixi-effects-view` commands and the timeline chart, `0.6.0` adds draw-on strokes (`trimEnd`) and text that changes over time, `0.5.0` adds synthesised sound effects (`sfx`, no files) and the `check` tool, `0.4.0` makes a mask share its layer's lifetime, `0.3.0` made keyframe `at` relative to the layer).
|
|
4
4
|
|
|
5
5
|
**[Guide →](https://yjmtmtk.github.io/pixi-effects/guide/)** · **[Gallery →](https://yjmtmtk.github.io/pixi-effects/examples/gallery/)** · 37 portfolio pieces written as plain data by AI models · **[Live demos →](https://yjmtmtk.github.io/pixi-effects/)** · 12 numbered examples + an in-browser playground.
|
|
6
6
|
|
|
@@ -42,8 +42,8 @@ Drop the imports into an [importmap](https://developer.mozilla.org/docs/Web/HTML
|
|
|
42
42
|
"gsap": "https://esm.sh/gsap@3.12.5",
|
|
43
43
|
"gsap/PixiPlugin": "https://esm.sh/gsap@3.12.5/PixiPlugin",
|
|
44
44
|
"mediabunny": "https://esm.sh/mediabunny",
|
|
45
|
-
"pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
46
|
-
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
45
|
+
"pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.13.0/dist/index.js",
|
|
46
|
+
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.13.0/dist/Controller.js"
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
</script>
|
|
@@ -54,7 +54,7 @@ Drop the imports into an [importmap](https://developer.mozilla.org/docs/Web/HTML
|
|
|
54
54
|
</script>
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
Load the `dist/` files **as they are** (jsDelivr's `/npm/…/dist/…`, or unpkg's `https://unpkg.com/pixi-effects@0.
|
|
57
|
+
Load the `dist/` files **as they are** (jsDelivr's `/npm/…/dist/…`, or unpkg's `https://unpkg.com/pixi-effects@0.13.0/dist/index.js`) rather than a CDN-rebundled build such as `esm.sh/pixi-effects` or jsDelivr's `+esm`: the entries (`pixi-effects`, `…/controller`, `…/three`) share internal chunks, which only works when each file is served untouched.
|
|
58
58
|
|
|
59
59
|
> **Using three.js?** Add two more entries (`three` and `pixi-effects/three`) to this importmap — see [Adding three.js](#adding-threejs-optional) below.
|
|
60
60
|
>
|
|
@@ -70,7 +70,7 @@ Add two more entries to the importmap above: three.js itself, and the `pixi-effe
|
|
|
70
70
|
"imports": {
|
|
71
71
|
"...": "(everything from the importmap above)",
|
|
72
72
|
"three": "https://esm.sh/three@0.178.0",
|
|
73
|
-
"pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
73
|
+
"pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.13.0/dist/three.js"
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
</script>
|
|
@@ -123,13 +123,13 @@ keyframes: followPath({ d: 'M 120 560 C 360 120 920 120 1160 560', duration: 4,
|
|
|
123
123
|
...particles({ count: 90, at: 2, emit: 0 /* 0 = burst; or seconds to spread births */, life: [1, 1.8], area: { x: 640, y: 330 /*, width, height */ }, angle: [0, 360] /* 0 right, 90 down, −90 up */,
|
|
124
124
|
speed: [120, 420], gravity: 240, wind: 0, drag: 1.2, sway: { amp: 30, freq: 0.6 }, size: [2, 4.5], scale: [1, 0], fade: { in: 0.2, out: 0.6 },
|
|
125
125
|
colors: ['#ffd166', '#ff6b9d'], shape: 'circle'|'rect'|'star', spin: [-200, 200], blendMode: 'add', seed: 1, name: 'burst', template: { type: 'image', asset } }) // seeded layers, paths baked into keyframes
|
|
126
|
-
// a presentation: stops pause playback (`composition.stops: [2, { at: 5, page: 'Name', notes, advance }]`; a `page` stop begins a page, the others are steps); deck() lays pages out
|
|
126
|
+
// a presentation: stops pause playback (`composition.stops: [2, { at: 5, page: 'Name', notes, advance, pdf }]`; `pdf: true` = the stop that pictures its page in a PDF, `pdf: false` = keep it out; default the page's last stop; a `page` stop begins a page, the others are steps); deck() lays pages out
|
|
127
127
|
await movie.init({ canvas, width, height, frameRate, ...deck({ transition: { kind: 'slide', direction: 'left', duration: 0.6 },
|
|
128
128
|
pages: [{ name: 'Hello', duration: 3.2 /* last stop + transition */, stops: [2.6] /* page-local */, notes, sequences: [ /* page-local time; a step = a layer starting at the previous stop */ ] }] }) });
|
|
129
129
|
new Presenter(movie, { canvas }); // import { Presenter } from 'pixi-effects/presenter': → ↓ Space Enter click = next, ← = back, Home / End, number+Enter, B / W, F, ?
|
|
130
|
-
movie.next() / prev() / goToStop(i) / goToPage(n) / movie.stops / movie.on('stop', …) // Controller
|
|
130
|
+
movie.next() / prev() / goToStop(i) / goToPage(n) / movie.stops / movie.on('stop', …) // Controller: marks on the seek bar, play / Space / click = play to the next stop (pauseAtStops: false = straight through), a Present button, PDF in the download panel
|
|
131
131
|
// Presenter extras: G = page overview (pictures), P = presenter view (a second window: live picture, next picture, notes, timer); start() makes the pictures before the audience sees anything
|
|
132
|
-
await movie.stopImages({ as: 'dataURL', scale: 0.25 }) // [{ stop, page, image }] one per page (its last stop); { which: 'stops' } one per stop
|
|
132
|
+
await movie.stopImages({ as: 'dataURL', scale: 0.25 }) // [{ stop, page, image }] one per page (its last stop); { which: 'stops' } one per stop (a page's picture honours `pdf` flags)
|
|
133
133
|
await movie.exportPDF({ title }) // Blob: a PDF page per page of the deck; CLI: pixi-effects-render talk.html -o talk.pdf (--all-stops)
|
|
134
134
|
const env = await audioEnvelope('music.mp3', { frameRate: 30 }) // BEFORE movie.init: .series.level|bass|mid|treble (0–1 per frame), .beats [s], .bpm, .at(t, band); bands: { name: [Hz, Hz] }
|
|
135
135
|
const env = bpmEnvelope(120, { duration: 12 }) // no audio file: kick / snare / hats exactly on a tempo
|
package/ai/reference/pitfalls.md
CHANGED
|
@@ -78,7 +78,7 @@ A good habit that catches most of these: after `init`, read `window.__logs` (see
|
|
|
78
78
|
51. **Arcs and rings are `shape: 'arc'`** (`radius`, `innerRadius`, `startAngle`, `endAngle`, degrees; 0° = 3 o'clock, clockwise). A progress ring is an arc with `startAngle: -90` and an animated `endAngle` (−90 → 270); `strokeCap: 'round'` rounds BOTH ends. Fill it for a pie slice, add `innerRadius` for a donut slice (a 120-polygon donut is no longer needed). Style (`strokeColor`, `strokeWidth`, `fillColor`) goes in `initial`; `strokeCap` / `strokeJoin` may be top level. *(fixed)*
|
|
79
79
|
52. **Measure and split text with `measureText` / `splitText`, not by hand.** `splitText(text, style, { by, x, y, align })` returns each piece's x / y / width in the renderer's own font metrics (kerning kept; verified: a whole line vs the same line in per-letter layers differs only by anti-aliasing). The pieces are for text layers with the default top-left anchor and the same `style`. Load a web font before measuring (`await document.fonts.load('24px "Name"')`) or the fallback font's widths are used. Size a pill from `measureText(...).width + padding` instead of `0.58 × fontSize × length`. *(fixed)*
|
|
80
80
|
53. **Draw-on and typewriter need no masks or per-letter layers.** `trimStart` / `trimEnd` (0–1 of the outline, animatable, every shape but `arc`) draw a stroke on: `trimEnd: 0` plus a keyframe `to: { trimEnd: 1 }`. The fill is not trimmed, so fade `fillAlpha` in afterwards; use `strokeCap: 'round'`. Text changes over time with `set: { text }` (swap at a time) and `visibleChars` (typewriter; left-align it so it grows rightward). A `to: { text }` cannot tween a string and warns. *(fixed)*
|
|
81
|
-
54. **A deck page lasts until its last stop plus the transition.** `deck()` lays pages out one after another in page-local time and a `transition` overlaps them: a page shorter than its last stop (or shorter than the transition) loses its final step or flashes. A step is a layer that starts at the previous stop (`at: <previous stop>`), so the thing that appears on a press is fully visible when the movie stops. The transition option is `kind`, not `type`.
|
|
82
|
-
55. **Presenter and Controller both listen to click and keys.** Use the `Presenter` for a talk (or let the Controller's Present button start one); do not run both on the same canvas yourself.
|
|
81
|
+
54. **A deck page lasts until its last stop plus the transition.** `deck()` lays pages out one after another in page-local time and a `transition` overlaps them: a page shorter than its last stop (or shorter than the transition) loses its final step or flashes. A step is a layer that starts at the previous stop (`at: <previous stop>`), so the thing that appears on a press is fully visible when the movie stops. The transition option is `kind`, not `type`. If the last stop is mid-exit on paper, flag the stop you want in the PDF: `{ at, pdf: true }` (in `deck()`: `stops: [1.2, { at: 3, pdf: true }]`), and `pdf: false` keeps one out.
|
|
82
|
+
55. **Presenter and Controller both listen to click and keys.** Use the `Presenter` for a talk (or let the Controller's Present button start one); do not run both on the same canvas yourself. `movie.play()` ignores stops, but the Controller's play button / Space / click-on-picture play to the next stop and pause (`pauseAtStops: false` plays through); `movie.next()` plays to the next stop (a second press skips to it), `prev()` jumps back (nothing plays in reverse).
|
|
83
83
|
56. **Analyse audio before `init`, and give it the movie's frame rate.** `const env = await audioEnvelope('music.mp3', { frameRate })` is async; `react()` is pure and bakes the keyframes, so there is no per-frame callback. A custom `bands` list replaces the defaults (include `bass` if you want it; beats are picked there). No file yet: `bpmEnvelope(120, { duration })` is a stand-in with a kick on every beat. Offset the music with `audioOffset` when the audio layer does not start at 0.
|
|
84
84
|
57. **Review a deck by its stops, not by its timeline.** `pixi-effects-check` writes `stops.png` (one picture per stop, in order) for a page with `stops`; the picture at a stop is what the audience reads while it waits.
|
package/ai/template.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
6
|
<title>pixi-effects video</title>
|
|
7
|
-
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
7
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/pixi-effects@0.13.0/dist/loader.css" /> <!-- the loader: HTML + CSS only, so it is on screen at once and keeps moving while the page builds the video -->
|
|
8
8
|
<style>
|
|
9
9
|
body { background: #0d1220; margin: 0; padding: 16px; }
|
|
10
10
|
.stage { position: relative; width: min(960px, 100%); } /* the player and the loader both sit inside this box */
|
|
@@ -39,10 +39,10 @@
|
|
|
39
39
|
"gsap": "https://esm.sh/gsap@3.12.5",
|
|
40
40
|
"gsap/PixiPlugin": "https://esm.sh/gsap@3.12.5/PixiPlugin",
|
|
41
41
|
"mediabunny": "https://esm.sh/mediabunny",
|
|
42
|
-
"pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
43
|
-
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
44
|
-
"pixi-effects/presenter": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
45
|
-
"pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
42
|
+
"pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.13.0/dist/index.js",
|
|
43
|
+
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.13.0/dist/Controller.js",
|
|
44
|
+
"pixi-effects/presenter": "https://cdn.jsdelivr.net/npm/pixi-effects@0.13.0/dist/Presenter.js",
|
|
45
|
+
"pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.13.0/dist/three.js",
|
|
46
46
|
"pixi-filters": "https://esm.sh/pixi-filters@6.1.5?external=pixi.js",
|
|
47
47
|
"three": "https://esm.sh/three@0.178.0"
|
|
48
48
|
}
|
package/ai/tools/check.mjs
CHANGED
|
@@ -280,7 +280,7 @@ export async function runCheck(opts, log = console.log) {
|
|
|
280
280
|
report.files.sheet = shown(sheetFile);
|
|
281
281
|
|
|
282
282
|
// a presentation (composition.stops): one picture of every stop, in order, so the pages and steps can be read at a glance
|
|
283
|
-
const stops = await cdp.eval(`(() => { const s = movie.stops || []; return s.length ? { count: s.length, pages: movie.pageCount, items: s.map(x => ({ at: Math.round(x.at * 100) / 100, frame: x.frame, page: x.page, pageStart: x.pageStart, notes: !!x.notes })) } : null; })()`).catch(() => null);
|
|
283
|
+
const stops = await cdp.eval(`(() => { const s = movie.stops || []; return s.length ? { count: s.length, pages: movie.pageCount, items: s.map(x => ({ at: Math.round(x.at * 100) / 100, frame: x.frame, page: x.page, pageStart: x.pageStart, notes: !!x.notes, pdf: x.pdf })) } : null; })()`).catch(() => null);
|
|
284
284
|
if (stops) {
|
|
285
285
|
report.stops = stops;
|
|
286
286
|
const stopsSheet = await cdp.eval(`movie.contactSheet({ frames: movie.stops.map(s => s.frame), as: 'dataURL' })`).catch(() => null);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Filter, Container, RenderTexture, Matrix } from 'pixi.js';
|
|
2
|
-
import { S as SequenceSpec, C as CompositionShape,
|
|
2
|
+
import { S as SequenceSpec, C as CompositionShape, O as AudioDescriptor } from './types-CBSiUKau.cjs';
|
|
3
3
|
import { gsap } from 'gsap';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Filter, Container, RenderTexture, Matrix } from 'pixi.js';
|
|
2
|
-
import { S as SequenceSpec, C as CompositionShape,
|
|
2
|
+
import { S as SequenceSpec, C as CompositionShape, O as AudioDescriptor } from './types-CBSiUKau.js';
|
|
3
3
|
import { gsap } from 'gsap';
|
|
4
4
|
|
|
5
5
|
/**
|
package/dist/Controller.cjs
CHANGED
|
@@ -49,7 +49,9 @@ var CONTROLLER_CSS = `
|
|
|
49
49
|
}
|
|
50
50
|
.mc-progress:hover::before, .mc-progress.mc-scrubbing::before { height: calc(var(--mc-track-height, 3px) + 2px); }
|
|
51
51
|
.mc-stops { position: absolute; left: 12px; right: 12px; top: 0; bottom: 0; pointer-events: none; }
|
|
52
|
-
.mc-stop-tick { position: absolute; top: 50%; width: 2px; height: calc(var(--mc-track-height, 3px) +
|
|
52
|
+
.mc-stop-tick { position: absolute; top: 50%; width: 2px; height: calc(var(--mc-track-height, 3px) + 10px); transform: translate(-50%, -50%); background: var(--mc-fg, #fff); opacity: 0.9; border-radius: 1px; box-shadow: 0 0 0 1px rgba(0,0,0,0.55); }
|
|
53
|
+
.mc-stop-page { width: 4px; height: calc(var(--mc-track-height, 3px) + 20px); opacity: 1; }
|
|
54
|
+
.mc-stop-page::before { content: ''; position: absolute; left: 50%; top: -3px; width: 8px; height: 8px; transform: translateX(-50%); border-radius: 50%; background: var(--mc-fg, #fff); box-shadow: 0 0 0 1px rgba(0,0,0,0.55); }
|
|
53
55
|
.mc-progress-fill {
|
|
54
56
|
position: absolute; left: 12px; top: 50%;
|
|
55
57
|
height: var(--mc-track-height, 3px);
|
|
@@ -271,8 +273,10 @@ function pxToFraction(clientX, rect, inset = 0) {
|
|
|
271
273
|
const ratio = (clientX - innerLeft) / innerWidth;
|
|
272
274
|
return Math.min(Math.max(ratio, 0), 1);
|
|
273
275
|
}
|
|
276
|
+
var PDF_QUALITY = { "low": 0.6, "medium": 0.75, "high": 0.88, "very-high": 0.95 };
|
|
274
277
|
function extensionForMimeType(mime) {
|
|
275
278
|
const t = (mime || "").toLowerCase();
|
|
279
|
+
if (t.includes("pdf")) return "pdf";
|
|
276
280
|
if (t.includes("webm")) return "webm";
|
|
277
281
|
if (t.includes("quicktime") || t.includes("mov")) return "mov";
|
|
278
282
|
if (t.includes("matroska") || t.includes("mkv")) return "mkv";
|
|
@@ -319,6 +323,7 @@ var Controller = class _Controller {
|
|
|
319
323
|
wasPlayingBeforeScrub = false;
|
|
320
324
|
activePointerId = null;
|
|
321
325
|
isExporting = false;
|
|
326
|
+
exportPages = null;
|
|
322
327
|
exportOverlay = null;
|
|
323
328
|
exportFillEl = null;
|
|
324
329
|
exportTextEl = null;
|
|
@@ -359,6 +364,7 @@ var Controller = class _Controller {
|
|
|
359
364
|
enableKeyboardShortcuts: options.enableKeyboardShortcuts ?? true,
|
|
360
365
|
clickToPlay: options.clickToPlay ?? true,
|
|
361
366
|
present: options.present ?? true,
|
|
367
|
+
pauseAtStops: options.pauseAtStops ?? true,
|
|
362
368
|
className: options.className ?? "movie-controller"
|
|
363
369
|
};
|
|
364
370
|
installStyles();
|
|
@@ -566,6 +572,7 @@ var Controller = class _Controller {
|
|
|
566
572
|
refreshStops() {
|
|
567
573
|
this.progressEl.querySelector(".mc-stops")?.remove();
|
|
568
574
|
const stops = this.movie.stops ?? [];
|
|
575
|
+
this.refreshPdfChoices(stops.length > 0);
|
|
569
576
|
if (stops.length === 0) {
|
|
570
577
|
this.presentBtn?.remove();
|
|
571
578
|
this.presentBtn = null;
|
|
@@ -576,7 +583,7 @@ var Controller = class _Controller {
|
|
|
576
583
|
holder.className = "mc-stops";
|
|
577
584
|
for (const s of stops) {
|
|
578
585
|
const tick = document.createElement("div");
|
|
579
|
-
tick.className = "mc-stop-tick";
|
|
586
|
+
tick.className = s.pageStart ? "mc-stop-tick mc-stop-page" : "mc-stop-tick";
|
|
580
587
|
tick.style.left = `${s.frame / total * 100}%`;
|
|
581
588
|
if (s.page) tick.title = s.page;
|
|
582
589
|
holder.appendChild(tick);
|
|
@@ -595,6 +602,41 @@ var Controller = class _Controller {
|
|
|
595
602
|
this.presentBtn = btn;
|
|
596
603
|
}
|
|
597
604
|
}
|
|
605
|
+
/** A deck can be downloaded as a PDF: the format list gets "PDF" choices while the movie has stops. */
|
|
606
|
+
refreshPdfChoices(has) {
|
|
607
|
+
const select = this.settingsFormatSelect;
|
|
608
|
+
if (!select) return;
|
|
609
|
+
const present = !!select.querySelector('option[value="pdf"]');
|
|
610
|
+
if (has && !present) {
|
|
611
|
+
for (const [value, label] of [["pdf", "PDF (pages)"], ["pdf-steps", "PDF (every step)"]]) {
|
|
612
|
+
const o = document.createElement("option");
|
|
613
|
+
o.value = value;
|
|
614
|
+
o.textContent = label;
|
|
615
|
+
select.appendChild(o);
|
|
616
|
+
}
|
|
617
|
+
} else if (!has && present) {
|
|
618
|
+
select.querySelectorAll('option[value="pdf"], option[value="pdf-steps"]').forEach((o) => o.remove());
|
|
619
|
+
if (this.exportFormat === "pdf" || this.exportFormat === "pdf-steps") {
|
|
620
|
+
this.exportFormat = "mp4";
|
|
621
|
+
select.value = "mp4";
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
/** Play, or pause: a movie with stops plays to the next stop (and from the end, again from the start). */
|
|
626
|
+
async togglePlay() {
|
|
627
|
+
if (this.movie.isPlaying) {
|
|
628
|
+
this.movie.pause();
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
await this.startPlaying();
|
|
632
|
+
}
|
|
633
|
+
/** Start playing: to the next stop for a movie with stops, to the end for any other (or with `pauseAtStops: false`). */
|
|
634
|
+
async startPlaying() {
|
|
635
|
+
if (this.options.pauseAtStops && (this.movie.stops?.length ?? 0) > 0) {
|
|
636
|
+
if (this.movie.currentFrame >= this.movie.totalFrames) await this.movie.gotoFrame(0, true);
|
|
637
|
+
await this.movie.next();
|
|
638
|
+
} else this.movie.play();
|
|
639
|
+
}
|
|
598
640
|
/** Hand the page over to a Presenter (fullscreen, keys, click and swipe move through the stops); Escape gives it back. */
|
|
599
641
|
async startPresenting() {
|
|
600
642
|
if (this.presenter || this.destroyed) return;
|
|
@@ -611,11 +653,7 @@ var Controller = class _Controller {
|
|
|
611
653
|
}
|
|
612
654
|
bindPlayButton() {
|
|
613
655
|
this.playBtn.addEventListener("click", () => {
|
|
614
|
-
|
|
615
|
-
this.movie.pause();
|
|
616
|
-
} else {
|
|
617
|
-
this.movie.play();
|
|
618
|
-
}
|
|
656
|
+
void this.togglePlay();
|
|
619
657
|
});
|
|
620
658
|
}
|
|
621
659
|
/** Clicking the picture plays / pauses (a tap on a phone too). */
|
|
@@ -632,8 +670,7 @@ var Controller = class _Controller {
|
|
|
632
670
|
this.swallowCanvasClick = false;
|
|
633
671
|
return;
|
|
634
672
|
}
|
|
635
|
-
|
|
636
|
-
else this.movie.play();
|
|
673
|
+
void this.togglePlay();
|
|
637
674
|
};
|
|
638
675
|
canvas.addEventListener("pointerdown", this.canvasPointerDownHandler);
|
|
639
676
|
canvas.addEventListener("click", this.canvasClickHandler);
|
|
@@ -789,7 +826,7 @@ var Controller = class _Controller {
|
|
|
789
826
|
} catch {
|
|
790
827
|
}
|
|
791
828
|
if (this.wasPlayingBeforeScrub) {
|
|
792
|
-
this.
|
|
829
|
+
void this.startPlaying();
|
|
793
830
|
this.refreshPlayIcon();
|
|
794
831
|
}
|
|
795
832
|
};
|
|
@@ -838,7 +875,7 @@ var Controller = class _Controller {
|
|
|
838
875
|
if (this.settingsFormatSelect) {
|
|
839
876
|
this.settingsFormatSelect.addEventListener("change", (e) => {
|
|
840
877
|
const v = e.target.value;
|
|
841
|
-
if (v === "mp4" || v === "webm" || v === "mov") this.exportFormat = v;
|
|
878
|
+
if (v === "mp4" || v === "webm" || v === "mov" || v === "pdf" || v === "pdf-steps") this.exportFormat = v;
|
|
842
879
|
});
|
|
843
880
|
}
|
|
844
881
|
if (this.settingsQualitySelect) {
|
|
@@ -888,9 +925,19 @@ var Controller = class _Controller {
|
|
|
888
925
|
this.movie.pause();
|
|
889
926
|
this.refreshPlayIcon();
|
|
890
927
|
}
|
|
891
|
-
this.
|
|
928
|
+
const pdf = this.exportFormat === "pdf" || this.exportFormat === "pdf-steps";
|
|
929
|
+
if (pdf) {
|
|
930
|
+
const stops = this.movie.stops ?? [];
|
|
931
|
+
const total = this.exportFormat === "pdf-steps" ? stops.filter((s) => s.pdf !== false).length : new Set(stops.map((s) => s.pageIndex)).size;
|
|
932
|
+
this.showExportOverlay("Exporting PDF...", `0% (0 / ${total} pages)`);
|
|
933
|
+
this.exportPages = { done: 0, total };
|
|
934
|
+
} else this.showExportOverlay();
|
|
892
935
|
try {
|
|
893
|
-
const blob = await this.movie.
|
|
936
|
+
const blob = pdf ? await this.movie.exportPDF({
|
|
937
|
+
which: this.exportFormat === "pdf-steps" ? "stops" : "pages",
|
|
938
|
+
quality: PDF_QUALITY[this.exportQuality],
|
|
939
|
+
onImage: () => this.pdfPageDone()
|
|
940
|
+
}) : await this.movie.render({
|
|
894
941
|
format: this.exportFormat,
|
|
895
942
|
video: { bitrate: this.exportQuality },
|
|
896
943
|
audio: { bitrate: this.exportQuality }
|
|
@@ -906,7 +953,7 @@ var Controller = class _Controller {
|
|
|
906
953
|
this.hideExportOverlay();
|
|
907
954
|
this.isExporting = false;
|
|
908
955
|
if (wasPlaying) {
|
|
909
|
-
this.
|
|
956
|
+
void this.startPlaying();
|
|
910
957
|
this.refreshPlayIcon();
|
|
911
958
|
}
|
|
912
959
|
}
|
|
@@ -929,16 +976,27 @@ var Controller = class _Controller {
|
|
|
929
976
|
const ts = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
|
|
930
977
|
return `movie-${ts}.${ext}`;
|
|
931
978
|
}
|
|
932
|
-
|
|
979
|
+
/** One more PDF page is ready: move the bar. */
|
|
980
|
+
pdfPageDone() {
|
|
981
|
+
const p = this.exportPages;
|
|
982
|
+
if (!p || !this.exportFillEl || !this.exportTextEl) return;
|
|
983
|
+
p.done++;
|
|
984
|
+
const percent = p.total ? Math.min(100, Math.round(p.done / p.total * 100)) : 100;
|
|
985
|
+
this.exportFillEl.style.width = `${percent}%`;
|
|
986
|
+
this.exportTextEl.textContent = `${percent}% (${p.done} / ${p.total} pages)`;
|
|
987
|
+
}
|
|
988
|
+
showExportOverlay(title = "Exporting video...", text = "0% (0 / 0 frames)") {
|
|
933
989
|
const overlay = document.createElement("div");
|
|
934
990
|
overlay.className = "mc-export-overlay";
|
|
935
991
|
overlay.innerHTML = `
|
|
936
992
|
<div class="mc-export-panel">
|
|
937
|
-
<div class="mc-export-title"
|
|
993
|
+
<div class="mc-export-title"></div>
|
|
938
994
|
<div class="mc-export-track"><div class="mc-export-fill"></div></div>
|
|
939
|
-
<div class="mc-export-text"
|
|
995
|
+
<div class="mc-export-text"></div>
|
|
940
996
|
</div>
|
|
941
997
|
`;
|
|
998
|
+
overlay.querySelector(".mc-export-title").textContent = title;
|
|
999
|
+
overlay.querySelector(".mc-export-text").textContent = text;
|
|
942
1000
|
this.root.appendChild(overlay);
|
|
943
1001
|
this.exportOverlay = overlay;
|
|
944
1002
|
this.exportFillEl = overlay.querySelector(".mc-export-fill");
|
|
@@ -951,6 +1009,7 @@ var Controller = class _Controller {
|
|
|
951
1009
|
this.exportFillEl = null;
|
|
952
1010
|
this.exportTextEl = null;
|
|
953
1011
|
}
|
|
1012
|
+
this.exportPages = null;
|
|
954
1013
|
}
|
|
955
1014
|
bindVisibility() {
|
|
956
1015
|
const wrap = this.wrapper;
|