pixi-effects 0.8.0 → 0.9.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 +7 -0
- package/README.md +6 -6
- package/ai/SKILL.md +6 -5
- package/ai/reference/cheatsheet.md +4 -0
- package/ai/template.html +4 -3
- package/ai/tools/check.mjs +8 -0
- package/dist/Controller.d.cts +1 -1
- package/dist/Controller.d.ts +1 -1
- package/dist/{Movie-HF3pSGjQ.d.ts → Movie-QMlFyHkf.d.ts} +25 -0
- package/dist/{Movie-CQOhUmko.d.cts → Movie-qGHp15c_.d.cts} +25 -0
- package/dist/index.cjs +60 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +60 -4
- package/dist/index.js.map +1 -1
- package/docs/api.md +16 -0
- package/llms-full.txt +26 -5
- package/package.json +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.0
|
|
4
|
+
|
|
5
|
+
**Added**
|
|
6
|
+
|
|
7
|
+
- **Poster time.** `movie.init({ poster: 9.5 })` names the moment that stands for the movie (a negative value counts back from the end). Once the movie is ready the canvas shows that frame while the playhead stays at 0, and play still starts from 0, like `<video poster>` but derived from the same data, with no separate image file. `movie.poster`, `movie.posterFrame` and `movie.posterImage()` (a `snapshot()` at the poster time that leaves the movie as it was); `pixi-effects-check` writes `poster.jpg`. The gallery uses it: every piece declares `poster` once (the `?poster` handling copied into all 33 pieces and `posterFrame` in their meta are gone), `node scripts/make-posters.mjs` takes the posters and writes `posters/manifest.json` (with a hash of each page, so a poster that is out of date fails a test).
|
|
8
|
+
- A guide for people, as a standalone site on GitHub Pages (`/guide/`, next to the gallery and the live demos): your first video, how it works, guides for text, shapes, images and video, audio, filters, transitions, 2.5D, exporting, the player and reviewing, a cookbook of tested recipes with live demos, a page on working with an AI, and a FAQ. It puts the AI-development tools up front: `check`, the contact sheet, the timeline and viewer, `inspect`, `inspectAudio` and `render`. Pages are markdown in `site/guide/`, built to HTML by `scripts/build-guide.mjs` (`npm run guide` previews it in `guide-preview/`); the recipes in them are built and linted by the tests, and the first-video page is a real file checked in Chrome.
|
|
9
|
+
|
|
3
10
|
## 0.8.0
|
|
4
11
|
|
|
5
12
|
**Added**
|
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# pixi-effects
|
|
2
2
|
|
|
3
|
-
> **Status**: experimental — current release `0.
|
|
3
|
+
> **Status**: experimental — current release `0.9.0`. The API may still change between minor versions (see [CHANGELOG](./CHANGELOG.md): `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
|
-
**[Gallery →](https://yjmtmtk.github.io/pixi-effects/examples/gallery/)** · 33 portfolio pieces written as plain data by AI models · **[Live demos →](https://yjmtmtk.github.io/pixi-effects/)** · 12 numbered examples + an in-browser playground.
|
|
5
|
+
**[Guide →](https://yjmtmtk.github.io/pixi-effects/guide/)** · **[Gallery →](https://yjmtmtk.github.io/pixi-effects/examples/gallery/)** · 33 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
|
|
|
7
7
|
Declarative composition and video rendering for the web. After Effects-style timelines on top of [PixiJS v8](https://pixijs.com/) and [GSAP](https://gsap.com/), with strict TypeScript types. Render to MP4 / WebM / MOV via [mediabunny](https://mediabunny.dev/).
|
|
8
8
|
|
|
@@ -40,8 +40,8 @@ Drop the imports into an [importmap](https://developer.mozilla.org/docs/Web/HTML
|
|
|
40
40
|
"gsap": "https://esm.sh/gsap@3.12.5",
|
|
41
41
|
"gsap/PixiPlugin": "https://esm.sh/gsap@3.12.5/PixiPlugin",
|
|
42
42
|
"mediabunny": "https://esm.sh/mediabunny",
|
|
43
|
-
"pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
44
|
-
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
43
|
+
"pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.9.0/dist/index.js",
|
|
44
|
+
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.9.0/dist/Controller.js"
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
</script>
|
|
@@ -52,7 +52,7 @@ Drop the imports into an [importmap](https://developer.mozilla.org/docs/Web/HTML
|
|
|
52
52
|
</script>
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
-
Load the `dist/` files **as they are** (jsDelivr's `/npm/…/dist/…`, or unpkg's `https://unpkg.com/pixi-effects@0.
|
|
55
|
+
Load the `dist/` files **as they are** (jsDelivr's `/npm/…/dist/…`, or unpkg's `https://unpkg.com/pixi-effects@0.9.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.
|
|
56
56
|
|
|
57
57
|
> **Using three.js?** Add two more entries (`three` and `pixi-effects/three`) to this importmap — see [Adding three.js](#adding-threejs-optional) below.
|
|
58
58
|
>
|
|
@@ -68,7 +68,7 @@ Add two more entries to the importmap above: three.js itself, and the `pixi-effe
|
|
|
68
68
|
"imports": {
|
|
69
69
|
"...": "(everything from the importmap above)",
|
|
70
70
|
"three": "https://esm.sh/three@0.178.0",
|
|
71
|
-
"pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
71
|
+
"pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.9.0/dist/three.js"
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
</script>
|
package/ai/SKILL.md
CHANGED
|
@@ -10,16 +10,17 @@ You describe a video as a tree of plain objects; the library plays it in a canva
|
|
|
10
10
|
## Workflow
|
|
11
11
|
|
|
12
12
|
1. **Start from `template.html`** (copy it; it loads the library from a CDN, collects every console warning into `window.__logs`, and exposes `window.movie`). One HTML file, plain JS. Default canvas 1280×720 @ 30 fps.
|
|
13
|
-
2. **
|
|
14
|
-
3. **
|
|
13
|
+
2. **Give the movie a poster time.** In `movie.init` add `poster: <seconds>`: the most striking moment (a title fully on screen, not the first frame, which is often black). The canvas shows that picture before play (the playhead stays at 0 and play starts from 0), and the check writes it as `poster.jpg`.
|
|
14
|
+
3. **Write the composition as data.** Anything repeated (letters, bars, particles, scenes) is a JS function or loop that returns specs — never copy-paste objects. Derive coordinates and times from constants/data, not magic numbers.
|
|
15
|
+
4. **Show the human:** `node ai/tools/view.mjs my-video.html` opens their browser on the page with a scrubbable, zoomable timeline under it (click / drag to seek, + / − or Ctrl/⌘ + wheel to zoom) (`npx pixi-effects-view …` in an installed package); it keeps running until Ctrl-C. **Deliver the file:** `node ai/tools/render.mjs my-video.html -o my-video.mp4` (`npx pixi-effects-render …` in an installed package) renders the page headless to mp4 / webm / mov / mkv and exits 0 only when the file is written; run it AFTER the check below passes. **Run the check — one command does steps 3, 4 and the export:** `node ai/tools/check.mjs my-video.html` (Node ≥ 22 and Chrome installed; no npm install; in an installed package it is `npx pixi-effects-check my-video.html`). It starts its own private Chrome (nothing is shared with other sessions), waits for `window.__ready`, and writes `timeline.html` (every layer as a bar on a time axis: ◆ keyframes, shaded transitions; hand it to the human to open) and prints: console warnings, `movie.inspect` over the WHOLE timeline (grouped), `movie.inspectAudio()`, then exports an mp4 and decodes it again (video size and length, audio length, loudness per second, silence). It writes `check-out/<name>/sheet.png` — **open that picture and look at it** — plus the mp4 and `report.json`. Exit 0 = nothing to fix; exit 1 = a list of problems; fix them and run it again. Text that overlaps text is listed under `review` (not a failure: ghost layers, glow copies and per-letter boxes are often intentional; look at the sheet; `--strict` makes them fail). The manual steps below are for looking at one frame closely.
|
|
15
16
|
**Or by hand: run it and read the logs first.** `window.__ready` must be `true` and `window.__logs` should be `[]`. A pixi-effects warning is an instruction: fix what it names.
|
|
16
|
-
|
|
17
|
+
5. **Look at it** — you cannot judge motion from code. The library has the tools built in:
|
|
17
18
|
- `await movie.contactSheet({ count: 6, as: 'dataURL' })` → ONE image of several labelled frames (pass `frames: [...]` / `times: [...]` to choose; include the middle of every transition and the last second). Save it and view it.
|
|
18
19
|
- `await movie.inspect(frame)` → every layer's canvas bounds and visibility, plus `issues` (text off the canvas, cut off by an edge, empty, or overlapping other text). Run it at several frames; fix every issue it names.
|
|
19
20
|
- `await movie.snapshot(frame, { as: 'dataURL' })` → one frame, canvas only (no player bar).
|
|
20
21
|
- `movie.inspectAudio()` → **hear with numbers**: for every sound its `start`, `sound.length`, `sound.peakDb`, `sound.loudestAt` (line this up with the picture) and `sound.brightnessHz` (< 500 dark, > 8000 airy), plus `issues` (limited mix, inaudible or cut-off sounds). Check it whenever the movie has audio; `render()` encodes this same mix.
|
|
21
22
|
From a browser tool the result is a `data:` URL string: `agent-browser eval "movie.contactSheet({ count: 6, as: 'dataURL' })" | python3 ai/tools/save-image.py /absolute/sheet.png`, then open the PNG. Sheet tiles are small (~480 px): use `snapshot` for detail. `inspect` output is long on busy scenes: pass `{ layers: 'none' }` and read `issues` first. Give layers a `name` so inspect paths are readable. A moving ticker is not flagged as cut off.
|
|
22
|
-
|
|
23
|
+
6. **Iterate** on what you saw (clipped text, collisions, wrong timing). Then export: `const blob = await movie.render({ format: 'mp4' })` (about real time; `movie.on('progress', …)` reports 0–100).
|
|
23
24
|
|
|
24
25
|
## The rules that cause most failures
|
|
25
26
|
|
|
@@ -43,7 +44,7 @@ Centred text must be positioned by `anchorX/anchorY: 0.5`; estimate text width a
|
|
|
43
44
|
- `reference/cheatsheet.md` — every layer type, prop, default and rule on one page.
|
|
44
45
|
- `reference/recipes.md` — tested building blocks: slam type, lower-third, marquee, count-up, bar chart, 2.5D title, camera orbit, slideshow with transitions + music, looping, particles, gradients/vignette, three.js metal.
|
|
45
46
|
- `reference/pitfalls.md` — the full list of real mistakes, with status.
|
|
46
|
-
- `tools/check.mjs` — the one-command review (see Workflow step
|
|
47
|
+
- `tools/check.mjs` — the one-command review (see Workflow step 5).
|
|
47
48
|
- `template.html` — the starting file. (It loads the library from a CDN; when testing against a local build, point the three `pixi-effects*` importmap entries at `../../dist/…`.)
|
|
48
49
|
- `tools/save-image.py` — save a `dataURL` result (contact sheet / snapshot) to a PNG.
|
|
49
50
|
- Full reference: `docs/dsl.md` and `docs/api.md` in the repository.
|
|
@@ -83,6 +83,10 @@ Style (`initial` / keyframes): `fillColor fillAlpha strokeColor strokeAlpha stro
|
|
|
83
83
|
|
|
84
84
|
Add `threeD: true` to any visual layer, then use `z`, `rotationX`, `rotationY` (centre rotation with `anchorX/Y: 0.5` or `pivotX/Y`). Add a `{ type: 'camera' }` layer for a view. A `threeD` layer at `z: 0` with the default camera looks identical to a 2D one. Consecutive `threeD` layers are drawn farthest-first (equal `z` keeps array order); non-`threeD` layers ignore the camera and keep array order. Layers at/behind the camera plane are hidden. Not supported on `threeD` layers: masks, transitions, `filterArea`.
|
|
85
85
|
|
|
86
|
+
## Poster
|
|
87
|
+
|
|
88
|
+
`movie.init({ …, poster: 9.5 })` names the moment (seconds; negative = from the end) that stands for the movie: the canvas shows it before play (the playhead stays at 0, play starts from 0), `movie.poster` / `movie.posterFrame` read it, `await movie.posterImage({ as: 'dataURL', type: 'image/jpeg', scale: 0.5 })` is the picture. Pick the most striking moment, not frame 0 (which is often black).
|
|
89
|
+
|
|
86
90
|
## Player
|
|
87
91
|
|
|
88
92
|
`new Controller(movie, { canvas, theme: { accent: '#ff4d6d' } })` (import from `pixi-effects/controller`; theme keys `accent foreground track barBackground trackHeight font`, or CSS `--mc-accent` …; `controller.setTheme(…)`). Your own player: `movie.play() pause() gotoFrame(f) volume muted toggleMute()`, state `currentFrame totalFrames frameRate duration isPlaying`, events `ready frame play pause ended seeking seeked volumechange error progress` (`<video>` names). `examples/15-custom-player.html` is a complete one.
|
package/ai/template.html
CHANGED
|
@@ -36,9 +36,9 @@
|
|
|
36
36
|
"gsap": "https://esm.sh/gsap@3.12.5",
|
|
37
37
|
"gsap/PixiPlugin": "https://esm.sh/gsap@3.12.5/PixiPlugin",
|
|
38
38
|
"mediabunny": "https://esm.sh/mediabunny",
|
|
39
|
-
"pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
40
|
-
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
41
|
-
"pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
39
|
+
"pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.9.0/dist/index.js",
|
|
40
|
+
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.9.0/dist/Controller.js",
|
|
41
|
+
"pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.9.0/dist/three.js",
|
|
42
42
|
"pixi-filters": "https://esm.sh/pixi-filters@6.1.5?external=pixi.js",
|
|
43
43
|
"three": "https://esm.sh/three@0.178.0"
|
|
44
44
|
}
|
|
@@ -73,6 +73,7 @@
|
|
|
73
73
|
await movie.init({
|
|
74
74
|
canvas: document.getElementById('stage'),
|
|
75
75
|
width: W, height: H, duration: DURATION, frameRate: FPS,
|
|
76
|
+
poster: DURATION * 0.75, // the moment that stands for the video before it plays (pick your most striking one)
|
|
76
77
|
background: '#0a0a0f',
|
|
77
78
|
// assets: [{ name: 'photo', src: './photo.jpg' }, { name: 'bgm', src: './bgm.mp3' }],
|
|
78
79
|
composition: { sequences },
|
package/ai/tools/check.mjs
CHANGED
|
@@ -279,6 +279,14 @@ export async function runCheck(opts, log = console.log) {
|
|
|
279
279
|
fs.writeFileSync(sheetFile, Buffer.from(sheet.slice(sheet.indexOf(',') + 1), 'base64'));
|
|
280
280
|
report.files.sheet = shown(sheetFile);
|
|
281
281
|
|
|
282
|
+
// the poster: the picture that stands for the movie (its poster time, or the first frame)
|
|
283
|
+
const poster = await cdp.eval(`movie.posterImage({ as: 'dataURL', type: 'image/jpeg', scale: 0.5 })`).catch(() => null);
|
|
284
|
+
if (poster) {
|
|
285
|
+
const posterFile = path.join(outDir, 'poster.jpg');
|
|
286
|
+
fs.writeFileSync(posterFile, Buffer.from(poster.slice(poster.indexOf(',') + 1), 'base64'));
|
|
287
|
+
report.files.poster = shown(posterFile);
|
|
288
|
+
}
|
|
289
|
+
|
|
282
290
|
// the timeline: every layer as a bar on a time axis (for a human to open in a browser)
|
|
283
291
|
const timeline = await cdp.eval(`movie.timelineChart({ title: ${JSON.stringify(name)} })`).catch(() => null);
|
|
284
292
|
if (timeline) {
|
package/dist/Controller.d.cts
CHANGED
package/dist/Controller.d.ts
CHANGED
|
@@ -140,6 +140,12 @@ interface MovieOptions {
|
|
|
140
140
|
canvas?: HTMLCanvasElement;
|
|
141
141
|
assets?: AssetSpec[];
|
|
142
142
|
composition?: CompositionSpec;
|
|
143
|
+
/**
|
|
144
|
+
* The poster time in seconds: the moment that stands for the movie before it plays. The canvas shows that frame once the movie is
|
|
145
|
+
* ready, the playhead stays at 0 and play starts from 0 (like `<video poster>`); `movie.posterImage()` is that picture. A negative
|
|
146
|
+
* value counts back from the end. Default: none (the first frame).
|
|
147
|
+
*/
|
|
148
|
+
poster?: number;
|
|
143
149
|
}
|
|
144
150
|
interface RenderOptions {
|
|
145
151
|
format?: 'mp4' | 'mov' | 'webm' | 'mkv';
|
|
@@ -203,6 +209,11 @@ declare class Movie {
|
|
|
203
209
|
audioSource: AudioBufferSourceNode | null;
|
|
204
210
|
gainNode: GainNode | null;
|
|
205
211
|
private _cancelAudioStart;
|
|
212
|
+
/** The poster time in seconds (`movie.init({ poster })`), or null. */
|
|
213
|
+
poster: number | null;
|
|
214
|
+
private _posterFrame;
|
|
215
|
+
/** The canvas shows the poster picture while the playhead is still at frame 0 (like `<video poster>`): the first seek or play replaces it. */
|
|
216
|
+
private _atPoster;
|
|
206
217
|
/** > 0 while the movie moves its own playhead (playback, render, snapshot, contactSheet): those are not the seeks `seeking` / `seeked` report. */
|
|
207
218
|
private _quiet;
|
|
208
219
|
private _volume;
|
|
@@ -236,6 +247,20 @@ declare class Movie {
|
|
|
236
247
|
off(event: string, fn: Listener): this;
|
|
237
248
|
emit(event: string, ...args: unknown[]): void;
|
|
238
249
|
get isReady(): boolean;
|
|
250
|
+
/** The poster time as a frame number, or null. */
|
|
251
|
+
get posterFrame(): number | null;
|
|
252
|
+
/** Draw the poster frame on the canvas and leave the playhead where it is (frame 0): the picture before the movie plays. */
|
|
253
|
+
private _showPoster;
|
|
254
|
+
/**
|
|
255
|
+
* The picture at the poster time (frame 0 if the movie has none), as `snapshot()` returns it, and the movie is left as it was:
|
|
256
|
+
* still showing the poster, or back at the frame it was at.
|
|
257
|
+
*/
|
|
258
|
+
posterImage(opts?: SnapshotOptions & {
|
|
259
|
+
as?: 'blob';
|
|
260
|
+
}): Promise<Blob>;
|
|
261
|
+
posterImage(opts: SnapshotOptions & {
|
|
262
|
+
as: 'dataURL';
|
|
263
|
+
}): Promise<string>;
|
|
239
264
|
private _ensureAudioContext;
|
|
240
265
|
init(options?: MovieOptions): Promise<void>;
|
|
241
266
|
gotoFrame(frame: number, force?: boolean): Promise<void>;
|
|
@@ -140,6 +140,12 @@ interface MovieOptions {
|
|
|
140
140
|
canvas?: HTMLCanvasElement;
|
|
141
141
|
assets?: AssetSpec[];
|
|
142
142
|
composition?: CompositionSpec;
|
|
143
|
+
/**
|
|
144
|
+
* The poster time in seconds: the moment that stands for the movie before it plays. The canvas shows that frame once the movie is
|
|
145
|
+
* ready, the playhead stays at 0 and play starts from 0 (like `<video poster>`); `movie.posterImage()` is that picture. A negative
|
|
146
|
+
* value counts back from the end. Default: none (the first frame).
|
|
147
|
+
*/
|
|
148
|
+
poster?: number;
|
|
143
149
|
}
|
|
144
150
|
interface RenderOptions {
|
|
145
151
|
format?: 'mp4' | 'mov' | 'webm' | 'mkv';
|
|
@@ -203,6 +209,11 @@ declare class Movie {
|
|
|
203
209
|
audioSource: AudioBufferSourceNode | null;
|
|
204
210
|
gainNode: GainNode | null;
|
|
205
211
|
private _cancelAudioStart;
|
|
212
|
+
/** The poster time in seconds (`movie.init({ poster })`), or null. */
|
|
213
|
+
poster: number | null;
|
|
214
|
+
private _posterFrame;
|
|
215
|
+
/** The canvas shows the poster picture while the playhead is still at frame 0 (like `<video poster>`): the first seek or play replaces it. */
|
|
216
|
+
private _atPoster;
|
|
206
217
|
/** > 0 while the movie moves its own playhead (playback, render, snapshot, contactSheet): those are not the seeks `seeking` / `seeked` report. */
|
|
207
218
|
private _quiet;
|
|
208
219
|
private _volume;
|
|
@@ -236,6 +247,20 @@ declare class Movie {
|
|
|
236
247
|
off(event: string, fn: Listener): this;
|
|
237
248
|
emit(event: string, ...args: unknown[]): void;
|
|
238
249
|
get isReady(): boolean;
|
|
250
|
+
/** The poster time as a frame number, or null. */
|
|
251
|
+
get posterFrame(): number | null;
|
|
252
|
+
/** Draw the poster frame on the canvas and leave the playhead where it is (frame 0): the picture before the movie plays. */
|
|
253
|
+
private _showPoster;
|
|
254
|
+
/**
|
|
255
|
+
* The picture at the poster time (frame 0 if the movie has none), as `snapshot()` returns it, and the movie is left as it was:
|
|
256
|
+
* still showing the poster, or back at the frame it was at.
|
|
257
|
+
*/
|
|
258
|
+
posterImage(opts?: SnapshotOptions & {
|
|
259
|
+
as?: 'blob';
|
|
260
|
+
}): Promise<Blob>;
|
|
261
|
+
posterImage(opts: SnapshotOptions & {
|
|
262
|
+
as: 'dataURL';
|
|
263
|
+
}): Promise<string>;
|
|
239
264
|
private _ensureAudioContext;
|
|
240
265
|
init(options?: MovieOptions): Promise<void>;
|
|
241
266
|
gotoFrame(frame: number, force?: boolean): Promise<void>;
|
package/dist/index.cjs
CHANGED
|
@@ -1449,6 +1449,21 @@ function startWhenRunning(ctx, start, gesture) {
|
|
|
1449
1449
|
}
|
|
1450
1450
|
var GESTURES = ["pointerdown", "mousedown", "touchend", "keydown"];
|
|
1451
1451
|
|
|
1452
|
+
// src/core/poster.ts
|
|
1453
|
+
function normalizePoster(poster, duration, frameRate) {
|
|
1454
|
+
if (poster === void 0 || poster === null) return { seconds: null, frame: null };
|
|
1455
|
+
if (typeof poster !== "number" || !Number.isFinite(poster)) {
|
|
1456
|
+
console.warn(`pixi-effects: movie.init({ poster }): ${JSON.stringify(poster)} is not a time in seconds; ignored`);
|
|
1457
|
+
return { seconds: null, frame: null };
|
|
1458
|
+
}
|
|
1459
|
+
let seconds = poster < 0 ? Math.max(0, duration + poster) : poster;
|
|
1460
|
+
if (seconds > duration) {
|
|
1461
|
+
console.warn(`pixi-effects: movie.init({ poster }): the poster time ${poster} s is past the end of the movie (${duration} s); using the last frame`);
|
|
1462
|
+
seconds = duration;
|
|
1463
|
+
}
|
|
1464
|
+
return { seconds, frame: Math.round(seconds * frameRate) };
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1452
1467
|
// src/core/Movie.ts
|
|
1453
1468
|
PIXI.extensions.add(PIXI.CullerPlugin);
|
|
1454
1469
|
gsap.gsap.registerPlugin(PixiPlugin.PixiPlugin);
|
|
@@ -1462,6 +1477,11 @@ var Movie = class {
|
|
|
1462
1477
|
audioSource = null;
|
|
1463
1478
|
gainNode = null;
|
|
1464
1479
|
_cancelAudioStart = null;
|
|
1480
|
+
/** The poster time in seconds (`movie.init({ poster })`), or null. */
|
|
1481
|
+
poster = null;
|
|
1482
|
+
_posterFrame = null;
|
|
1483
|
+
/** The canvas shows the poster picture while the playhead is still at frame 0 (like `<video poster>`): the first seek or play replaces it. */
|
|
1484
|
+
_atPoster = false;
|
|
1465
1485
|
/** > 0 while the movie moves its own playhead (playback, render, snapshot, contactSheet): those are not the seeks `seeking` / `seeked` report. */
|
|
1466
1486
|
_quiet = 0;
|
|
1467
1487
|
_volume = 1;
|
|
@@ -1505,6 +1525,36 @@ var Movie = class {
|
|
|
1505
1525
|
get isReady() {
|
|
1506
1526
|
return this._initState === "ready";
|
|
1507
1527
|
}
|
|
1528
|
+
/** The poster time as a frame number, or null. */
|
|
1529
|
+
get posterFrame() {
|
|
1530
|
+
return this._posterFrame;
|
|
1531
|
+
}
|
|
1532
|
+
/** Draw the poster frame on the canvas and leave the playhead where it is (frame 0): the picture before the movie plays. */
|
|
1533
|
+
async _showPoster() {
|
|
1534
|
+
if (this._posterFrame === null || !this.timeline) return;
|
|
1535
|
+
const back = this.currentFrame;
|
|
1536
|
+
this.currentFrame = this._posterFrame;
|
|
1537
|
+
try {
|
|
1538
|
+
this.timeline.time(this._posterFrame / this.frameRate);
|
|
1539
|
+
await this._awaitVideoFrames();
|
|
1540
|
+
this._updateSpace();
|
|
1541
|
+
this._renderNow();
|
|
1542
|
+
} finally {
|
|
1543
|
+
this.currentFrame = back;
|
|
1544
|
+
}
|
|
1545
|
+
this._atPoster = true;
|
|
1546
|
+
}
|
|
1547
|
+
async posterImage(opts = {}) {
|
|
1548
|
+
this._requireReady("posterImage");
|
|
1549
|
+
const back = this.currentFrame, wasAtPoster = this._atPoster;
|
|
1550
|
+
try {
|
|
1551
|
+
return await this.snapshot(this._posterFrame ?? 0, opts);
|
|
1552
|
+
} finally {
|
|
1553
|
+
this.currentFrame = back;
|
|
1554
|
+
if (wasAtPoster) await this._showPoster();
|
|
1555
|
+
else await this._quietly(() => this.gotoFrame(back, true));
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1508
1558
|
_ensureAudioContext() {
|
|
1509
1559
|
if (!this._audioContext) {
|
|
1510
1560
|
const Ctx = window.AudioContext ?? window.webkitAudioContext;
|
|
@@ -1513,7 +1563,7 @@ var Movie = class {
|
|
|
1513
1563
|
return this._audioContext;
|
|
1514
1564
|
}
|
|
1515
1565
|
async init(options = {}) {
|
|
1516
|
-
chunkUQBGW5DO_cjs.warnUnknownOptions("movie.init()", options, ["width", "height", "duration", "frameRate", "background", "canvas", "assets", "composition"]);
|
|
1566
|
+
chunkUQBGW5DO_cjs.warnUnknownOptions("movie.init()", options, ["width", "height", "duration", "frameRate", "background", "canvas", "assets", "composition", "poster"]);
|
|
1517
1567
|
this._initState = "pending";
|
|
1518
1568
|
try {
|
|
1519
1569
|
this.width = options.width ?? 1920;
|
|
@@ -1521,6 +1571,9 @@ var Movie = class {
|
|
|
1521
1571
|
this.duration = options.duration ?? 10;
|
|
1522
1572
|
this.frameRate = options.frameRate ?? 30;
|
|
1523
1573
|
this.totalFrames = Math.round(this.duration * this.frameRate);
|
|
1574
|
+
const poster = normalizePoster(options.poster, this.duration, this.frameRate);
|
|
1575
|
+
this.poster = poster.seconds;
|
|
1576
|
+
this._posterFrame = poster.frame;
|
|
1524
1577
|
this.background = options.background ?? "#000000";
|
|
1525
1578
|
this.timeline = gsap.gsap.timeline({ paused: true, defaults: { ease: "none" } });
|
|
1526
1579
|
this.timeline.add(gsap.gsap.to({}, { duration: this.duration }));
|
|
@@ -1584,6 +1637,7 @@ var Movie = class {
|
|
|
1584
1637
|
this._updateSpace();
|
|
1585
1638
|
this._renderNow();
|
|
1586
1639
|
this._initState = "ready";
|
|
1640
|
+
if (this._posterFrame !== null) await this._showPoster();
|
|
1587
1641
|
this.emit("ready");
|
|
1588
1642
|
} catch (err) {
|
|
1589
1643
|
try {
|
|
@@ -1597,7 +1651,7 @@ var Movie = class {
|
|
|
1597
1651
|
}
|
|
1598
1652
|
async gotoFrame(frame, force = false) {
|
|
1599
1653
|
if (this._initState !== "ready") return;
|
|
1600
|
-
if (!force && this.currentFrame === frame) return;
|
|
1654
|
+
if (!force && !this._atPoster && this.currentFrame === frame) return;
|
|
1601
1655
|
if (this._quiet > 0) return this._goto(frame);
|
|
1602
1656
|
const target = Math.max(0, Math.min(frame, this.totalFrames));
|
|
1603
1657
|
this.emit("seeking", { frame: target, totalFrames: this.totalFrames });
|
|
@@ -1617,6 +1671,7 @@ var Movie = class {
|
|
|
1617
1671
|
this.emit("error", { where, message: error instanceof Error ? error.message : String(error), error });
|
|
1618
1672
|
}
|
|
1619
1673
|
async _goto(frame) {
|
|
1674
|
+
this._atPoster = false;
|
|
1620
1675
|
this.currentFrame = Math.max(0, Math.min(frame, this.totalFrames));
|
|
1621
1676
|
this.timeline.time(this.currentFrame / this.frameRate);
|
|
1622
1677
|
await this._awaitVideoFrames();
|
|
@@ -1658,7 +1713,7 @@ var Movie = class {
|
|
|
1658
1713
|
const g = sheet.getContext("2d");
|
|
1659
1714
|
g.fillStyle = "#10131c";
|
|
1660
1715
|
g.fillRect(0, 0, L.width, L.height);
|
|
1661
|
-
const back = this.currentFrame;
|
|
1716
|
+
const back = this.currentFrame, wasAtPoster = this._atPoster;
|
|
1662
1717
|
try {
|
|
1663
1718
|
for (let i = 0; i < frames.length; i++) {
|
|
1664
1719
|
const f = frames[i];
|
|
@@ -1671,7 +1726,8 @@ var Movie = class {
|
|
|
1671
1726
|
g.fillText(`frame ${f} \xB7 ${(f / this.frameRate).toFixed(2)}s`, x + 8, y + LABEL_H / 2);
|
|
1672
1727
|
}
|
|
1673
1728
|
} finally {
|
|
1674
|
-
|
|
1729
|
+
if (wasAtPoster) await this._showPoster();
|
|
1730
|
+
else await this._quietly(() => this.gotoFrame(back, true));
|
|
1675
1731
|
}
|
|
1676
1732
|
return encodeCanvas(sheet, "image/png", opts.as ?? "blob");
|
|
1677
1733
|
}
|