pixi-effects 0.12.0 → 0.13.1
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 +22 -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 +75 -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 +75 -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,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.13.1
|
|
4
|
+
|
|
5
|
+
**Fixed**
|
|
6
|
+
|
|
7
|
+
- **A deck's first click played through the stops.** The guide's live demos and the gallery started a piece with `movie.play()`, which ignores stops; for a movie with stops they now call `movie.next()` and stop at the first stop.
|
|
8
|
+
|
|
9
|
+
**Changed**
|
|
10
|
+
|
|
11
|
+
- **The stop marks are dots on the seek bar** (they were thin bars): a small white dot with a dark ring for a step, a larger dot with an accent ring where a page starts.
|
|
12
|
+
|
|
13
|
+
## 0.13.0
|
|
14
|
+
|
|
15
|
+
**Changed**
|
|
16
|
+
|
|
17
|
+
- **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.
|
|
18
|
+
- **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.
|
|
19
|
+
|
|
20
|
+
**Added**
|
|
21
|
+
|
|
22
|
+
- **`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.
|
|
23
|
+
- **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.
|
|
24
|
+
|
|
3
25
|
## 0.12.0
|
|
4
26
|
|
|
5
27
|
**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.1`. The API may still change between minor versions (see [CHANGELOG](./CHANGELOG.md): `0.13.1` fixes decks starting past their first stop in the guide and gallery and draws the stop marks as dots, `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.1/dist/index.js",
|
|
46
|
+
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.13.1/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.1/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.1/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.1/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.1/dist/index.js",
|
|
43
|
+
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.13.1/dist/Controller.js",
|
|
44
|
+
"pixi-effects/presenter": "https://cdn.jsdelivr.net/npm/pixi-effects@0.13.1/dist/Presenter.js",
|
|
45
|
+
"pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.13.1/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,8 @@ 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:
|
|
52
|
+
.mc-stop-tick { position: absolute; top: 50%; width: 7px; height: 7px; transform: translate(-50%, -50%); border-radius: 50%; background: var(--mc-fg, #fff); box-shadow: 0 0 0 1.5px rgba(0,0,0,0.5); }
|
|
53
|
+
.mc-stop-page { width: 11px; height: 11px; box-shadow: 0 0 0 2.5px var(--mc-accent, #007AFF), 0 0 0 4px rgba(0,0,0,0.35); }
|
|
53
54
|
.mc-progress-fill {
|
|
54
55
|
position: absolute; left: 12px; top: 50%;
|
|
55
56
|
height: var(--mc-track-height, 3px);
|
|
@@ -271,8 +272,10 @@ function pxToFraction(clientX, rect, inset = 0) {
|
|
|
271
272
|
const ratio = (clientX - innerLeft) / innerWidth;
|
|
272
273
|
return Math.min(Math.max(ratio, 0), 1);
|
|
273
274
|
}
|
|
275
|
+
var PDF_QUALITY = { "low": 0.6, "medium": 0.75, "high": 0.88, "very-high": 0.95 };
|
|
274
276
|
function extensionForMimeType(mime) {
|
|
275
277
|
const t = (mime || "").toLowerCase();
|
|
278
|
+
if (t.includes("pdf")) return "pdf";
|
|
276
279
|
if (t.includes("webm")) return "webm";
|
|
277
280
|
if (t.includes("quicktime") || t.includes("mov")) return "mov";
|
|
278
281
|
if (t.includes("matroska") || t.includes("mkv")) return "mkv";
|
|
@@ -319,6 +322,7 @@ var Controller = class _Controller {
|
|
|
319
322
|
wasPlayingBeforeScrub = false;
|
|
320
323
|
activePointerId = null;
|
|
321
324
|
isExporting = false;
|
|
325
|
+
exportPages = null;
|
|
322
326
|
exportOverlay = null;
|
|
323
327
|
exportFillEl = null;
|
|
324
328
|
exportTextEl = null;
|
|
@@ -359,6 +363,7 @@ var Controller = class _Controller {
|
|
|
359
363
|
enableKeyboardShortcuts: options.enableKeyboardShortcuts ?? true,
|
|
360
364
|
clickToPlay: options.clickToPlay ?? true,
|
|
361
365
|
present: options.present ?? true,
|
|
366
|
+
pauseAtStops: options.pauseAtStops ?? true,
|
|
362
367
|
className: options.className ?? "movie-controller"
|
|
363
368
|
};
|
|
364
369
|
installStyles();
|
|
@@ -566,6 +571,7 @@ var Controller = class _Controller {
|
|
|
566
571
|
refreshStops() {
|
|
567
572
|
this.progressEl.querySelector(".mc-stops")?.remove();
|
|
568
573
|
const stops = this.movie.stops ?? [];
|
|
574
|
+
this.refreshPdfChoices(stops.length > 0);
|
|
569
575
|
if (stops.length === 0) {
|
|
570
576
|
this.presentBtn?.remove();
|
|
571
577
|
this.presentBtn = null;
|
|
@@ -576,7 +582,7 @@ var Controller = class _Controller {
|
|
|
576
582
|
holder.className = "mc-stops";
|
|
577
583
|
for (const s of stops) {
|
|
578
584
|
const tick = document.createElement("div");
|
|
579
|
-
tick.className = "mc-stop-tick";
|
|
585
|
+
tick.className = s.pageStart ? "mc-stop-tick mc-stop-page" : "mc-stop-tick";
|
|
580
586
|
tick.style.left = `${s.frame / total * 100}%`;
|
|
581
587
|
if (s.page) tick.title = s.page;
|
|
582
588
|
holder.appendChild(tick);
|
|
@@ -595,6 +601,41 @@ var Controller = class _Controller {
|
|
|
595
601
|
this.presentBtn = btn;
|
|
596
602
|
}
|
|
597
603
|
}
|
|
604
|
+
/** A deck can be downloaded as a PDF: the format list gets "PDF" choices while the movie has stops. */
|
|
605
|
+
refreshPdfChoices(has) {
|
|
606
|
+
const select = this.settingsFormatSelect;
|
|
607
|
+
if (!select) return;
|
|
608
|
+
const present = !!select.querySelector('option[value="pdf"]');
|
|
609
|
+
if (has && !present) {
|
|
610
|
+
for (const [value, label] of [["pdf", "PDF (pages)"], ["pdf-steps", "PDF (every step)"]]) {
|
|
611
|
+
const o = document.createElement("option");
|
|
612
|
+
o.value = value;
|
|
613
|
+
o.textContent = label;
|
|
614
|
+
select.appendChild(o);
|
|
615
|
+
}
|
|
616
|
+
} else if (!has && present) {
|
|
617
|
+
select.querySelectorAll('option[value="pdf"], option[value="pdf-steps"]').forEach((o) => o.remove());
|
|
618
|
+
if (this.exportFormat === "pdf" || this.exportFormat === "pdf-steps") {
|
|
619
|
+
this.exportFormat = "mp4";
|
|
620
|
+
select.value = "mp4";
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
/** Play, or pause: a movie with stops plays to the next stop (and from the end, again from the start). */
|
|
625
|
+
async togglePlay() {
|
|
626
|
+
if (this.movie.isPlaying) {
|
|
627
|
+
this.movie.pause();
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
await this.startPlaying();
|
|
631
|
+
}
|
|
632
|
+
/** Start playing: to the next stop for a movie with stops, to the end for any other (or with `pauseAtStops: false`). */
|
|
633
|
+
async startPlaying() {
|
|
634
|
+
if (this.options.pauseAtStops && (this.movie.stops?.length ?? 0) > 0) {
|
|
635
|
+
if (this.movie.currentFrame >= this.movie.totalFrames) await this.movie.gotoFrame(0, true);
|
|
636
|
+
await this.movie.next();
|
|
637
|
+
} else this.movie.play();
|
|
638
|
+
}
|
|
598
639
|
/** Hand the page over to a Presenter (fullscreen, keys, click and swipe move through the stops); Escape gives it back. */
|
|
599
640
|
async startPresenting() {
|
|
600
641
|
if (this.presenter || this.destroyed) return;
|
|
@@ -611,11 +652,7 @@ var Controller = class _Controller {
|
|
|
611
652
|
}
|
|
612
653
|
bindPlayButton() {
|
|
613
654
|
this.playBtn.addEventListener("click", () => {
|
|
614
|
-
|
|
615
|
-
this.movie.pause();
|
|
616
|
-
} else {
|
|
617
|
-
this.movie.play();
|
|
618
|
-
}
|
|
655
|
+
void this.togglePlay();
|
|
619
656
|
});
|
|
620
657
|
}
|
|
621
658
|
/** Clicking the picture plays / pauses (a tap on a phone too). */
|
|
@@ -632,8 +669,7 @@ var Controller = class _Controller {
|
|
|
632
669
|
this.swallowCanvasClick = false;
|
|
633
670
|
return;
|
|
634
671
|
}
|
|
635
|
-
|
|
636
|
-
else this.movie.play();
|
|
672
|
+
void this.togglePlay();
|
|
637
673
|
};
|
|
638
674
|
canvas.addEventListener("pointerdown", this.canvasPointerDownHandler);
|
|
639
675
|
canvas.addEventListener("click", this.canvasClickHandler);
|
|
@@ -789,7 +825,7 @@ var Controller = class _Controller {
|
|
|
789
825
|
} catch {
|
|
790
826
|
}
|
|
791
827
|
if (this.wasPlayingBeforeScrub) {
|
|
792
|
-
this.
|
|
828
|
+
void this.startPlaying();
|
|
793
829
|
this.refreshPlayIcon();
|
|
794
830
|
}
|
|
795
831
|
};
|
|
@@ -838,7 +874,7 @@ var Controller = class _Controller {
|
|
|
838
874
|
if (this.settingsFormatSelect) {
|
|
839
875
|
this.settingsFormatSelect.addEventListener("change", (e) => {
|
|
840
876
|
const v = e.target.value;
|
|
841
|
-
if (v === "mp4" || v === "webm" || v === "mov") this.exportFormat = v;
|
|
877
|
+
if (v === "mp4" || v === "webm" || v === "mov" || v === "pdf" || v === "pdf-steps") this.exportFormat = v;
|
|
842
878
|
});
|
|
843
879
|
}
|
|
844
880
|
if (this.settingsQualitySelect) {
|
|
@@ -888,9 +924,19 @@ var Controller = class _Controller {
|
|
|
888
924
|
this.movie.pause();
|
|
889
925
|
this.refreshPlayIcon();
|
|
890
926
|
}
|
|
891
|
-
this.
|
|
927
|
+
const pdf = this.exportFormat === "pdf" || this.exportFormat === "pdf-steps";
|
|
928
|
+
if (pdf) {
|
|
929
|
+
const stops = this.movie.stops ?? [];
|
|
930
|
+
const total = this.exportFormat === "pdf-steps" ? stops.filter((s) => s.pdf !== false).length : new Set(stops.map((s) => s.pageIndex)).size;
|
|
931
|
+
this.showExportOverlay("Exporting PDF...", `0% (0 / ${total} pages)`);
|
|
932
|
+
this.exportPages = { done: 0, total };
|
|
933
|
+
} else this.showExportOverlay();
|
|
892
934
|
try {
|
|
893
|
-
const blob = await this.movie.
|
|
935
|
+
const blob = pdf ? await this.movie.exportPDF({
|
|
936
|
+
which: this.exportFormat === "pdf-steps" ? "stops" : "pages",
|
|
937
|
+
quality: PDF_QUALITY[this.exportQuality],
|
|
938
|
+
onImage: () => this.pdfPageDone()
|
|
939
|
+
}) : await this.movie.render({
|
|
894
940
|
format: this.exportFormat,
|
|
895
941
|
video: { bitrate: this.exportQuality },
|
|
896
942
|
audio: { bitrate: this.exportQuality }
|
|
@@ -906,7 +952,7 @@ var Controller = class _Controller {
|
|
|
906
952
|
this.hideExportOverlay();
|
|
907
953
|
this.isExporting = false;
|
|
908
954
|
if (wasPlaying) {
|
|
909
|
-
this.
|
|
955
|
+
void this.startPlaying();
|
|
910
956
|
this.refreshPlayIcon();
|
|
911
957
|
}
|
|
912
958
|
}
|
|
@@ -929,16 +975,27 @@ var Controller = class _Controller {
|
|
|
929
975
|
const ts = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
|
|
930
976
|
return `movie-${ts}.${ext}`;
|
|
931
977
|
}
|
|
932
|
-
|
|
978
|
+
/** One more PDF page is ready: move the bar. */
|
|
979
|
+
pdfPageDone() {
|
|
980
|
+
const p = this.exportPages;
|
|
981
|
+
if (!p || !this.exportFillEl || !this.exportTextEl) return;
|
|
982
|
+
p.done++;
|
|
983
|
+
const percent = p.total ? Math.min(100, Math.round(p.done / p.total * 100)) : 100;
|
|
984
|
+
this.exportFillEl.style.width = `${percent}%`;
|
|
985
|
+
this.exportTextEl.textContent = `${percent}% (${p.done} / ${p.total} pages)`;
|
|
986
|
+
}
|
|
987
|
+
showExportOverlay(title = "Exporting video...", text = "0% (0 / 0 frames)") {
|
|
933
988
|
const overlay = document.createElement("div");
|
|
934
989
|
overlay.className = "mc-export-overlay";
|
|
935
990
|
overlay.innerHTML = `
|
|
936
991
|
<div class="mc-export-panel">
|
|
937
|
-
<div class="mc-export-title"
|
|
992
|
+
<div class="mc-export-title"></div>
|
|
938
993
|
<div class="mc-export-track"><div class="mc-export-fill"></div></div>
|
|
939
|
-
<div class="mc-export-text"
|
|
994
|
+
<div class="mc-export-text"></div>
|
|
940
995
|
</div>
|
|
941
996
|
`;
|
|
997
|
+
overlay.querySelector(".mc-export-title").textContent = title;
|
|
998
|
+
overlay.querySelector(".mc-export-text").textContent = text;
|
|
942
999
|
this.root.appendChild(overlay);
|
|
943
1000
|
this.exportOverlay = overlay;
|
|
944
1001
|
this.exportFillEl = overlay.querySelector(".mc-export-fill");
|
|
@@ -951,6 +1008,7 @@ var Controller = class _Controller {
|
|
|
951
1008
|
this.exportFillEl = null;
|
|
952
1009
|
this.exportTextEl = null;
|
|
953
1010
|
}
|
|
1011
|
+
this.exportPages = null;
|
|
954
1012
|
}
|
|
955
1013
|
bindVisibility() {
|
|
956
1014
|
const wrap = this.wrapper;
|