pixi-effects 0.28.2 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +10 -10
  3. package/ai/chat-template.html +5 -5
  4. package/ai/tools/check.mjs +18 -3
  5. package/ai/tools/pixi-effects.mjs +1 -1
  6. package/ai/tools/render.mjs +14 -9
  7. package/ai/tools/view.mjs +6 -2
  8. package/dist/Base-CB1_FPZD.d.cts +60 -0
  9. package/dist/Base-Ne74ztNl.d.ts +60 -0
  10. package/dist/Composition-MUKR3FYD.cjs +16 -0
  11. package/dist/{Composition-K6B3APM5.cjs.map → Composition-MUKR3FYD.cjs.map} +1 -1
  12. package/dist/Composition-RJGA7KDU.js +7 -0
  13. package/dist/{Composition-H7UNR5F5.js.map → Composition-RJGA7KDU.js.map} +1 -1
  14. package/dist/Controller.cjs +73 -45
  15. package/dist/Controller.cjs.map +1 -1
  16. package/dist/Controller.d.cts +16 -22
  17. package/dist/Controller.d.ts +16 -22
  18. package/dist/Controller.js +74 -40
  19. package/dist/Controller.js.map +1 -1
  20. package/dist/{Movie-CHcM8HNB.d.ts → Movie-C2rZFkrO.d.ts} +227 -119
  21. package/dist/{Movie-Cx5ozUGb.d.cts → Movie-DdnpLpw1.d.cts} +227 -119
  22. package/dist/{blendModes-HDOP6R7T.cjs → blendModes-ETMYWATF.cjs} +5 -5
  23. package/dist/{blendModes-HDOP6R7T.cjs.map → blendModes-ETMYWATF.cjs.map} +1 -1
  24. package/dist/{blendModes-PAVTW3BO.js → blendModes-RIVZXL5A.js} +4 -4
  25. package/dist/{blendModes-PAVTW3BO.js.map → blendModes-RIVZXL5A.js.map} +1 -1
  26. package/dist/{chunk-P4GY3Z4G.cjs → chunk-2DXPTKQZ.cjs} +6 -6
  27. package/dist/chunk-2DXPTKQZ.cjs.map +1 -0
  28. package/dist/{chunk-TYWFKCPY.js → chunk-4KB3GUEK.js} +5 -5
  29. package/dist/chunk-4KB3GUEK.js.map +1 -0
  30. package/dist/{chunk-5MZWXVTT.js → chunk-A3Q3SDSH.js} +35 -28
  31. package/dist/chunk-A3Q3SDSH.js.map +1 -0
  32. package/dist/chunk-CRQFDY7E.js +4182 -0
  33. package/dist/chunk-CRQFDY7E.js.map +1 -0
  34. package/dist/chunk-HREBQAY4.cjs +4206 -0
  35. package/dist/chunk-HREBQAY4.cjs.map +1 -0
  36. package/dist/{chunk-ZMAD3KJU.cjs → chunk-LYOKH7KE.cjs} +22 -3
  37. package/dist/chunk-LYOKH7KE.cjs.map +1 -0
  38. package/dist/{chunk-45QKLD6M.js → chunk-RCVZX2JP.js} +22 -3
  39. package/dist/chunk-RCVZX2JP.js.map +1 -0
  40. package/dist/{chunk-KBH33IQX.cjs → chunk-WHNADXXG.cjs} +121 -114
  41. package/dist/chunk-WHNADXXG.cjs.map +1 -0
  42. package/dist/{chunk-7VN2KI3U.js → chunk-ZAXYPXKW.js} +14 -6
  43. package/dist/chunk-ZAXYPXKW.js.map +1 -0
  44. package/dist/{chunk-MJW7KILM.cjs → chunk-ZVUO325O.cjs} +32 -24
  45. package/dist/chunk-ZVUO325O.cjs.map +1 -0
  46. package/dist/index.cjs +106 -4482
  47. package/dist/index.cjs.map +1 -1
  48. package/dist/index.d.cts +15 -60
  49. package/dist/index.d.ts +15 -60
  50. package/dist/index.js +6 -4457
  51. package/dist/index.js.map +1 -1
  52. package/dist/loader.css +1 -1
  53. package/dist/{Presenter.cjs → present.cjs} +370 -31
  54. package/dist/present.cjs.map +1 -0
  55. package/dist/present.d.cts +278 -0
  56. package/dist/present.d.ts +278 -0
  57. package/dist/{Presenter.js → present.js} +358 -32
  58. package/dist/present.js.map +1 -0
  59. package/dist/three.cjs +6 -6
  60. package/dist/three.d.cts +2 -2
  61. package/dist/three.d.ts +2 -2
  62. package/dist/three.js +2 -2
  63. package/dist/{types-DpK1vwcl.d.ts → types-BzIB65Vk.d.cts} +11 -265
  64. package/dist/{types-DpK1vwcl.d.cts → types-BzIB65Vk.d.ts} +11 -265
  65. package/docs/api.md +44 -77
  66. package/docs/dsl.md +16 -14
  67. package/llms-full.txt +86 -115
  68. package/llms.txt +1 -1
  69. package/package.json +5 -5
  70. package/skills/pixi-effects/SKILL.md +5 -5
  71. package/skills/pixi-effects/reference/cheatsheet.md +9 -9
  72. package/skills/pixi-effects/reference/direction.md +2 -0
  73. package/skills/pixi-effects/reference/pitfalls.md +6 -6
  74. package/skills/pixi-effects/reference/recipes.md +4 -4
  75. package/skills/pixi-effects/template.html +7 -7
  76. package/dist/Base-C6f0fE8j.d.ts +0 -109
  77. package/dist/Base-DI1FCGcH.d.cts +0 -109
  78. package/dist/Composition-H7UNR5F5.js +0 -7
  79. package/dist/Composition-K6B3APM5.cjs +0 -16
  80. package/dist/Presenter.cjs.map +0 -1
  81. package/dist/Presenter.d.cts +0 -122
  82. package/dist/Presenter.d.ts +0 -122
  83. package/dist/Presenter.js.map +0 -1
  84. package/dist/chunk-45QKLD6M.js.map +0 -1
  85. package/dist/chunk-5MZWXVTT.js.map +0 -1
  86. package/dist/chunk-7VN2KI3U.js.map +0 -1
  87. package/dist/chunk-KBH33IQX.cjs.map +0 -1
  88. package/dist/chunk-MJW7KILM.cjs.map +0 -1
  89. package/dist/chunk-P4GY3Z4G.cjs.map +0 -1
  90. package/dist/chunk-TYWFKCPY.js.map +0 -1
  91. package/dist/chunk-ZMAD3KJU.cjs.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,57 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.29.0
4
+
5
+ **Changed (breaking): presentation leaves the core**
6
+
7
+ The talk side of the library is now its own entry, `pixi-effects/present`, built on the core's public API only (a test enforces the import rule). The core keeps `composition.stops` (named times, `movie.stops`) and gains two general moves; everything else that was about a talk moved out.
8
+
9
+ | before | now |
10
+ |---|---|
11
+ | `movie.next()` `prev()` `goToStop(i)` `goToPage(n)` | `next(movie)` `prev(movie)` `goToStop(movie, i)` `goToPage(movie, n)` from `pixi-effects/present` |
12
+ | `movie.stopIndex` `pageIndex` `pageCount` `currentStop` | `stopIndex(movie)` `pageIndex(movie)` `pageCount(movie)` `currentStop(movie)` |
13
+ | `movie.on('stop', fn)` | `const off = onStop(movie, fn)` |
14
+ | `movie.stopImages(o)` `movie.inspectStops(o)` `movie.exportPDF(o)` | `stopImages(movie, o)` `inspectStops(movie, o)` `exportPDF(movie, o)` |
15
+ | `import { deck } from 'pixi-effects'` | `import { deck } from 'pixi-effects/present'` |
16
+ | `import { Presenter } from 'pixi-effects/presenter'` (`dist/Presenter.js`) | `import { Presenter } from 'pixi-effects/present'` (`dist/present.js`; the import map line is `"pixi-effects/present"`) |
17
+ | `normalizeStops`, `pictureStops` exported from `pixi-effects` | not exported (the core normalises `composition.stops` itself; `pictureStops` is inside `pixi-effects/present`) |
18
+ | the types `StopEvent`, `StopImage`, `StopImagesOptions` from `pixi-effects` | the same names from `pixi-effects/present` (`Stop` and `StopSpec` stay in `pixi-effects`) |
19
+
20
+ A page that only maps `"pixi-effects"` still gets its stops reviewed and its PDF exported by `check` / `render`: they load `present.js` from beside the library.
21
+
22
+ **Changed (breaking): the API list for 0.29** (`docs/api-surface.txt` is the whole public surface, read from the sources and frozen by a test; the decisions are in `docs/superpowers/specs/2026-10-11-api-list-0.29.md`)
23
+
24
+ | before | now |
25
+ |---|---|
26
+ | `maskInverted: true` next to `mask: { …a layer… }` | `mask: { layer: { …the mask layer… }, invert: true }` (the one inverted-mask form, the same as `{ layer: 'name', invert: true }` for a named matte; `channel: 'luma'` works on a layer written in place too). The old key warns and says this. Inside a list of masks only names are allowed |
27
+ | `Controller` options `enableKeyboardShortcuts`, `showExportButton`, `present` | `keyboard`, `exportButton`, `presentButton` (the Presenter's names). An unknown `Controller` option now warns, with did-you-mean |
28
+ | `pixi-effects/controller` also exported `CONTROLLER_CSS`, `formatTime`, `frameToPercent`, `pxToFrame`, `pxToFraction`, `extensionForMimeType` | `Controller`, `ControllerOptions`, `ControllerTheme` only |
29
+ | the bins `pixi-effects-check`, `pixi-effects-render`, `pixi-effects-view` | `pixi-effects check|render|view` (the one bin; `npx pixi-effects-check` never worked) |
30
+ | `pixi-effects render --fail-on-warn` | `--strict`, the word `check` already used (the old flag says so) |
31
+ | `Movie.emit`, `audioSource`, `gainNode`; `Sequence.bindTimeline`, `collectAudio`, `absoluteStart`, `keepHidden`, `maskSequence`, `filters`, `updateSpace`, `syncFrame` in the type declarations | marked `@internal` and stripped from the declarations (the library still uses them) |
32
+
33
+ **Added**
34
+
35
+ - `movie.playUntil(seconds?)`: play and pause exactly at a time (`true` when it landed; `false` when a pause, a seek or a newer `playUntil` ended it first; no time = to the end). It is what `next(movie)` is made of, and the way to "play the intro, then wait".
36
+ - `movie.peek(fn)`: run a function that looks around (seeks, pictures) and put the playhead or the poster back; the seeks inside are not reported as `seeking` / `seeked`.
37
+ - `movie.snapshot(frame, { quality })` (JPEG quality) and `{ as: 'imageData' }` (the pixels, to measure or compare).
38
+ - `warnUnknownOptions(where, options, validNames)` is exported: the did-you-mean warning for an unknown option, for presets and layer types written outside the library.
39
+ - `animateText({ out: { preset, at: T } })`: the exit starts at composition second T (the pieces leave in the same wave and stay gone until the layer ends). Without `at` it is the end of the layer run backwards, as before; a warning says `out.at ≤ …` when the exit does not fit, or when T is before the entrance has finished.
40
+ - `pixi-effects render --help` and `pixi-effects view --help` (usage, exit 0), like `check`.
41
+ - A warning when a picture key (`filters`, `filterArea`, `mask`, `blendMode`, `threeD`, `hideBehindCamera`, `lit`, `castsShadows`) is put on a layer that is never drawn: an audio layer, a camera, a light (a light keeps `castsShadows`, its own switch). The key did nothing there, silently.
42
+
43
+ **Fixed**
44
+
45
+ - `NamedFilterType` lacked `'warp'`: `{ type: 'warp' }` was a TypeScript error though it worked at run time. The compiler now checks the type and the runtime filter tables against each other, both ways.
46
+
47
+ **Checked, not reproduced**
48
+
49
+ - The two bugs the old roadmap listed (an inline shape mask under a transition; a 4000 x 2400 `threeD` layer black on WebGPU) do not reproduce on this code. Real-browser tests pin both, on both backends.
50
+
51
+ **Docs**
52
+
53
+ - **The film guide says who it is for.** `direction.md` opens with a note for an AI in a browser chat (no shell, no files): this guide is not yours, follow `ai/CHAT.md`; you can still put a three-line plan in your reply and ask. The `llms.txt` line for it says "for an AI with a shell and files; a browser chat skips it".
54
+
3
55
  ## 0.28.2
4
56
 
5
57
  **Added**
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # pixi-effects
2
2
 
3
- > **Status**: experimental — current release `0.28.2`. The API may still change between minor versions; see the [CHANGELOG](./CHANGELOG.md) for what each release changed.
3
+ > **Status**: experimental — current release `0.29.0`. The API may still change between minor versions; see the [CHANGELOG](./CHANGELOG.md) for what each release changed.
4
4
 
5
5
  **[Guide →](https://yjmtmtk.github.io/pixi-effects/guide/)** · **[Gallery →](https://yjmtmtk.github.io/pixi-effects/examples/gallery/)** · 52 portfolio pieces written as plain data by AI models · **[Examples →](https://yjmtmtk.github.io/pixi-effects/examples/)** · 15 numbered examples, a music lab + an in-browser playground.
6
6
 
@@ -21,14 +21,14 @@ Declarative composition and video rendering for the web. After Effects-style tim
21
21
  - **Draw-on strokes and changing text** — `trimEnd` 0 → 1 draws a line, border or SVG path on; a keyframe `set: { text }` swaps a string and `visibleChars` is a typewriter.
22
22
  - **Masks, transitions, chroma key** — inline masks; chroma key; eight scene transitions (`crossfade`, `wipe`, `iris`, `slide`, `dip`, `zoom`, `dissolve`, `luma`).
23
23
  - **Presets** — `kenBurns` for stills, `withFade` for fade-in/out; `wiggle`, `stagger`, `animateText`, `followPath`, `particles` and `react` return keyframes or layers (seeded, so every render is the same).
24
- - **Presentations** — `stops` in the composition pause the movie where you say; `Presenter` (`pixi-effects/presenter`) moves on with arrow keys, a click or a swipe, with a page overview (G), a presenter view with notes and a timer (P), and `deck()` to lay out pages and steps. Export a deck as PDF (`movie.exportPDF()`, `pixi-effects-render talk.html -o talk.pdf`). See the [Presenting guide](https://yjmtmtk.github.io/pixi-effects/guide/presenting.html).
24
+ - **Presentations** — `stops` in the composition pause the movie where you say; `Presenter` (`pixi-effects/present`) moves on with arrow keys, a click or a swipe, with a page overview (G), a presenter view with notes and a timer (P), and `deck()` to lay out pages and steps. Export a deck as PDF (`exportPDF(movie)`, `pixi-effects render talk.html -o talk.pdf`). See the [Presenting guide](https://yjmtmtk.github.io/pixi-effects/guide/presenting.html).
25
25
  - **Music written as text** — `{ type: 'audio', music: { bpm: 96, tracks: [{ inst: 'keys', notes: 'Cmaj7:4 Am7:4' }], drums: { kick: 'x...x...' } } }`: a tune with no audio file, played by a small built-in synthesiser (keys, pluck, pad, bass, lead, bell, music box, drums; chords by name, swing, fades, a ritardando). Deterministic, in the exported file at the same moment; an AI writes it as easily as a title.
26
26
  - **Audio-reactive visuals** — `audioEnvelope('music.mp3')` analyses loudness, bass / mid / treble and the beats per frame; `react()` turns them into keyframes, so the picture follows the music and still renders identically.
27
27
  - **2.5D layers & camera** — add `threeD: true`, `z`, `rotationX/Y` and a `{ type: 'camera' }` layer for parallax, flips and dolly zooms; no three.js needed.
28
28
  - **three.js layer** *(optional)* — drop a real three.js scene in as a layer via `pixi-effects/three`; keyframes drive its objects (`three.cube.rotation.y`).
29
29
  - **Built-in player UI — or your own** — a drop-in HTML5-`<video>`-style overlay controller (play, scrub, mute, volume, fullscreen, export-to-file) whose colours are one option (`theme: { accent: '#ff4d6d' }`); or build a player yourself on the movie's `<video>`-named events (`play pause ended seeking seeked volumechange error`).
30
- - **MP4 / WebM / MOV export** — pick container and quality from the controller, call `movie.render()` from code, or render headless from the command line (`pixi-effects-render`).
31
- - **A timeline you can read and scrub** — `movie.timelineChart()` draws every layer as a bar on a time axis; `pixi-effects-view` opens the page with that timeline under it, zoomable, with a playhead you can drag.
30
+ - **MP4 / WebM / MOV export** — pick container and quality from the controller, call `movie.render()` from code, or render headless from the command line (`pixi-effects render`).
31
+ - **A timeline you can read and scrub** — `movie.timelineChart()` draws every layer as a bar on a time axis; `pixi-effects view` opens the page with that timeline under it, zoomable, with a playhead you can drag.
32
32
  - **Tiny dependency surface** — only `mediabunny` (runtime) plus PixiJS (a peer dep). three.js is an *optional* peer, needed only if you import `pixi-effects/three`.
33
33
 
34
34
  ## Install
@@ -49,8 +49,8 @@ Drop the imports into an [importmap](https://developer.mozilla.org/docs/Web/HTML
49
49
  "imports": {
50
50
  "pixi.js": "https://esm.sh/pixi.js@8.22.0?bundle-deps",
51
51
  "mediabunny": "https://esm.sh/mediabunny",
52
- "pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.28.2/dist/index.js",
53
- "pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.28.2/dist/Controller.js"
52
+ "pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.29.0/dist/index.js",
53
+ "pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.29.0/dist/Controller.js"
54
54
  }
55
55
  }
56
56
  </script>
@@ -61,7 +61,7 @@ Drop the imports into an [importmap](https://developer.mozilla.org/docs/Web/HTML
61
61
  </script>
62
62
  ```
63
63
 
64
- Load the `dist/` files **as they are** (jsDelivr's `/npm/…/dist/…`, or unpkg's `https://unpkg.com/pixi-effects@0.28.2/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.
64
+ Load the `dist/` files **as they are** (jsDelivr's `/npm/…/dist/…`, or unpkg's `https://unpkg.com/pixi-effects@0.29.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.
65
65
 
66
66
  > **Using three.js?** Add two more entries (`three` and `pixi-effects/three`) to this importmap — see [Adding three.js](#adding-threejs-optional) below.
67
67
  >
@@ -77,7 +77,7 @@ Add two more entries to the importmap above: three.js itself, and the `pixi-effe
77
77
  "imports": {
78
78
  "...": "(everything from the importmap above)",
79
79
  "three": "https://esm.sh/three@0.178.0",
80
- "pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.28.2/dist/three.js"
80
+ "pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.29.0/dist/three.js"
81
81
  }
82
82
  }
83
83
  </script>
@@ -175,7 +175,7 @@ This library is designed to be written by AI: a video is plain data, and every m
175
175
  - [`skills/pixi-effects/reference/cheatsheet.md`](./skills/pixi-effects/reference/cheatsheet.md), [`recipes.md`](./skills/pixi-effects/reference/recipes.md) (tested), [`pitfalls.md`](./skills/pixi-effects/reference/pitfalls.md), and a starter [`skills/pixi-effects/template.html`](./skills/pixi-effects/template.html).
176
176
 
177
177
  - **Look at a page with its timeline, and scrub one with the other:** `npx pixi-effects view my-video.html` (it also watches the folder: when the work changes it shows "Updated hh:mm:ss — reload (R)" and never switches by itself; reload comes back to the same frame) (or `node ai/tools/view.mjs …`) opens your browser on a viewer: the page on top, the timeline of every layer under it with a playhead that follows the movie. Click or drag the timeline to seek, click a layer's name to jump to where it starts, Space plays, ← / → step a frame, + / − (or Ctrl/⌘ + wheel) zoom, the names stay in place. For the human who wants to see what the AI made.
178
- - **Render a page to a video file, headless:** `npx pixi-effects render my-video.html -o my-video.mp4` (or `node ai/tools/render.mjs …`; Node ≥ 22 and Chrome installed, no dependencies). The container follows the extension (`mp4 webm mov mkv`); `--quality very-low…very-high`, `--query lang=ja` (added to the page URL), `--fail-on-warn` (exit 1 when the page logged a warning), `--quiet`. It waits for `window.__ready`, runs `movie.render()` and streams the file to disk; exit 0 = written, 1 = the page or the render failed (no file). For scripts, CI and batches; `pixi-effects-check` below is the review.
178
+ - **Render a page to a video file, headless:** `npx pixi-effects render my-video.html -o my-video.mp4` (or `node ai/tools/render.mjs …`; Node ≥ 22 and Chrome installed, no dependencies). The container follows the extension (`mp4 webm mov mkv`); `--quality very-low…very-high`, `--query lang=ja` (added to the page URL), `--strict` (exit 1 when the page logged a warning), `--quiet`. It waits for `window.__ready`, runs `movie.render()` and streams the file to disk; exit 0 = written, 1 = the page or the render failed (no file). For scripts, CI and batches; `pixi-effects check` below is the review.
179
179
  - **A talk as a PDF:** `npx pixi-effects render my-talk.html -o my-talk.pdf` (one page per page of a `deck()` / `stops` movie; `--all-stops` makes a page of every step). The review tool writes `stops.png` for such a page, one picture per stop.
180
180
  - **One-command review for the AI that wrote the page:** `npx pixi-effects check my-video.html` (or `node ai/tools/check.mjs my-video.html`; Node ≥ 22 and Chrome installed, no dependencies). It opens the page in its own headless Chrome and reports warnings, layout (`movie.inspect` over the whole timeline), the soundtrack (`movie.inspectAudio`) and a real export decoded again, and writes a contact sheet PNG and a `timeline.html` (every layer as a bar on a time axis) to look at. Exit code 0 / 1.
181
181
 
@@ -184,7 +184,7 @@ They ship in the npm package (`node_modules/pixi-effects/ai/`).
184
184
  ## Documentation
185
185
 
186
186
  - [**DSL reference**](./docs/dsl.md) — composition, sequences, 3D layers & camera, three.js layer, keyframes, expressions, filters
187
- - [**API reference**](./docs/api.md) — `Movie`, `Controller`, `pixi-effects/presenter`, `pixi-effects/three`, events, render options
187
+ - [**API reference**](./docs/api.md) — `Movie`, `Controller`, `pixi-effects/present`, `pixi-effects/three`, events, render options
188
188
  - [**Examples**](./examples/) — runnable HTML files (read in order):
189
189
  - `01-hello.html` — minimum viable composition
190
190
  - `02-keyframes.html` — keyframes, easings, expressions
@@ -59,17 +59,17 @@
59
59
  "imports": {
60
60
  "pixi.js": "https://cdn.jsdelivr.net/npm/pixi.js@8.22.0/dist/pixi.min.mjs",
61
61
  "mediabunny": "https://cdn.jsdelivr.net/npm/mediabunny@1.61.3/+esm",
62
- "pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.28.2/dist/index.js",
63
- "pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.28.2/dist/Controller.js",
64
- "pixi-effects/presenter": "https://cdn.jsdelivr.net/npm/pixi-effects@0.28.2/dist/Presenter.js"
62
+ "pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.29.0/dist/index.js",
63
+ "pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.29.0/dist/Controller.js",
64
+ "pixi-effects/present": "https://cdn.jsdelivr.net/npm/pixi-effects@0.29.0/dist/present.js"
65
65
  }
66
66
  }
67
67
  </script>
68
68
 
69
69
  <script type="module">
70
- import { Movie, kenBurns, withFade, wiggle, stagger, animateText, followPath, particles, deck } from 'pixi-effects';
70
+ import { Movie, kenBurns, withFade, wiggle, stagger, animateText, followPath, particles } from 'pixi-effects';
71
71
  import { Controller } from 'pixi-effects/controller';
72
- // A talk instead of a video: import { Presenter } from 'pixi-effects/presenter'; use deck({ pages: [...] }) and new Presenter(movie, { canvas }) instead of the Controller.
72
+ // A talk instead of a video: import { deck, Presenter } from 'pixi-effects/present'; use deck({ pages: [...] }) and new Presenter(movie, { canvas }) instead of the Controller.
73
73
 
74
74
  // ===================== EDIT FROM HERE =====================
75
75
  const W = 1280, H = 720, FPS = 30, DURATION = 6;
@@ -286,6 +286,18 @@ const waveformScript = (audio, duration) => `(() => {
286
286
  // ───────────────────────────── the check ─────────────────────────────
287
287
 
288
288
  export const sleep = ms => new Promise(r => setTimeout(r, ms));
289
+
290
+ /**
291
+ * JavaScript, as text, for the page: it resolves to the page's own `pixi-effects/present` module (stops, pages, PDF). The page's import map names it
292
+ * (`pixi-effects/present`); a page that only maps `pixi-effects` gets the `present.js` that sits beside the library.
293
+ */
294
+ export const LOAD_PRESENT = `(async () => {
295
+ try { return await import('pixi-effects/present'); } catch (e) { /* not in the import map: look beside the library */ }
296
+ let lib = null;
297
+ try { lib = JSON.parse(document.querySelector('script[type="importmap"]').textContent).imports['pixi-effects']; } catch (e) { /* none */ }
298
+ if (!lib) throw new Error('the page has no "pixi-effects" in its import map');
299
+ return import(new URL('present.js', new URL(lib, document.baseURI)).href);
300
+ })()`;
289
301
  /** A path for display: relative to the cwd when it is inside it, absolute otherwise. */
290
302
  export const shown = f => { const r = path.relative(process.cwd(), f); return r.startsWith('..') || path.isAbsolute(r) ? f : r; };
291
303
  const kb = n => (n >= 1048576 ? `${(n / 1048576).toFixed(2)} MB` : `${Math.max(1, Math.round(n / 1024))} KB`);
@@ -404,11 +416,13 @@ export async function runCheck(opts, log = console.log) {
404
416
  }
405
417
 
406
418
  // a presentation (composition.stops): one picture of every stop, in order, so the pages and steps can be read at a glance
407
- 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);
419
+ const stops = await cdp.eval(`(() => { const s = movie.stops || []; return s.length ? { count: s.length, pages: new Set(s.map(x => x.pageIndex)).size, 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);
408
420
  if (stops) {
409
421
  report.stops = stops;
410
422
  // is the picture at each stop still changing? (the audience, the page overview and a PDF would see a half-finished animation)
411
- const settled = await cdp.eval('movie.inspectStops()').catch(() => null);
423
+ let skipped = '';
424
+ const settled = await cdp.eval(`${LOAD_PRESENT}.then(p => p.inspectStops(movie))`).catch(e => { skipped = String((e && e.message) || e); return null; });
425
+ if (!settled) report.stops.reviewSkipped = `${skipped || 'no answer'} (the page needs "pixi-effects/present": ".../dist/present.js" in its import map, from the same version as the library)`;
412
426
  if (settled) {
413
427
  report.stops.review = settled.issues;
414
428
  report.stops.moving = settled.stops.map(s => Math.round(s.moving * 10000) / 10000);
@@ -529,7 +543,8 @@ function finish(report, outDir, log) {
529
543
  }
530
544
  if (report.stops) {
531
545
  const s = report.stops;
532
- L.push(` stops ${s.count} stop(s) on ${s.pages} page(s) — stops.png shows each one in order; the picture at a stop is what the audience sees while it waits`);
546
+ L.push(` stops ${s.count} stop(s) on ${s.pages} page(s) — stops.png shows each one in order; the picture at a stop is what the audience sees while it waits`)
547
+ if (s.reviewSkipped) L.push(` not reviewed for stops that are still moving: ${s.reviewSkipped}`);
533
548
  if (s.review?.length) {
534
549
  L.push(` review ${s.review.length} stop(s) where the picture is still changing (may be on purpose, e.g. something always moving; use --strict to fail on them):`);
535
550
  for (const r of s.review.slice(0, 4)) L.push(` - ${r}`);
@@ -7,7 +7,7 @@
7
7
  * npx pixi-effects view my-video.html open it in a browser with a timeline
8
8
  *
9
9
  * Everything after the command goes to the tool unchanged (`check.mjs`, `render.mjs`, `view.mjs`; their own headers list every option), and so do the
10
- * exit code and the output. The three older bins (`pixi-effects-check|render|view`) still work.
10
+ * exit code and the output. It is the package's only bin (the older `pixi-effects-check|render|view` are gone).
11
11
  */
12
12
  import { spawn } from 'node:child_process';
13
13
  import { realpathSync } from 'node:fs';
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * It starts a private static server and a private headless Chrome, opens the page, waits for `window.__ready === true`
8
8
  * (the contract of skills/pixi-effects/template.html), runs `movie.render()` in the page and streams the file to disk. Nothing is decoded or
9
- * reviewed: that is `pixi-effects-check`'s job. Use this one to produce the file (a script, CI, a batch).
9
+ * reviewed: that is `pixi-effects check`'s job. Use this one to produce the file (a script, CI, a batch).
10
10
  *
11
11
  * Needs: Node >= 22 (built-in WebSocket), Chrome / Chromium installed (or CHROME=PATH / --chrome PATH). No npm dependencies.
12
12
  * Exit code: 0 = the file was written, 1 = the page or the render failed (no file), 2 = bad usage.
@@ -17,7 +17,7 @@
17
17
  * --draft (for looking: half size, low quality, no motion blur; a --quality you give still wins) ·
18
18
  * --motion-blur SAMPLES (2-64: each frame is drawn that many times over the shutter and averaged; overrides the page's own motionBlur) ·
19
19
  * --shutter FRACTION (with --motion-blur: how long the shutter is open, 0-1 of a frame, default 0.5) ·
20
- * --video-codec C (avc hevc vp9 av1 vp8) · --video-bitrate B (a quality or bits a second: 8M, 800k) · --audio-codec C · --query "a=1&b=2" (added to the page URL) · --fail-on-warn (exit 1 when the page
20
+ * --video-codec C (avc hevc vp9 av1 vp8) · --video-bitrate B (a quality or bits a second: 8M, 800k) · --audio-codec C · --query "a=1&b=2" (added to the page URL) · --strict (exit 1 when the page
21
21
  * logged a warning; the file is still written) · --quiet (no progress line) · --timeout SECONDS (default 900) ·
22
22
  * --root DIR (static server root; default: the nearest folder above the page with dist/) · --chrome PATH
23
23
  */
@@ -25,7 +25,7 @@ import fs from 'node:fs';
25
25
  import os from 'node:os';
26
26
  import path from 'node:path';
27
27
  import { pathToFileURL } from 'node:url';
28
- import { findChrome, findRoot, serve, launchChrome, shown, sleep } from './check.mjs';
28
+ import { findChrome, findRoot, serve, launchChrome, shown, sleep, LOAD_PRESENT } from './check.mjs';
29
29
 
30
30
  const FORMATS = ['mp4', 'webm', 'mov', 'mkv', 'wav', 'ogg', 'pdf'];
31
31
  const SOUND_ONLY = ['wav', 'ogg'];
@@ -43,12 +43,15 @@ export function defaultOutput(page, format) {
43
43
  return path.resolve(path.basename(page).replace(/\.[^.]+$/, '') + '.' + format);
44
44
  }
45
45
 
46
+ export const USAGE = 'usage: node ai/tools/render.mjs <page.html> [-o out.mp4] [--format mp4|webm|mov|mkv|pdf] [--range A:B | --scene NAME] [--scale S] [--draft] [--quality very-low|low|medium|high|very-high] [--query a=1] [--strict] [--quiet] [--timeout S] [--root DIR] [--chrome PATH]';
47
+
46
48
  export function parseRenderArgs(argv) {
47
- const o = { range: null, scene: null, scale: null, draft: false, qualityGiven: false, page: null, out: null, format: null, quality: 'high', videoCodec: null, videoBitrate: null, audioCodec: null, allStops: false, motionBlur: null, shutter: null, query: null, failOnWarn: false, quiet: false, timeout: 900, root: null, chrome: null };
49
+ const o = { range: null, scene: null, scale: null, draft: false, qualityGiven: false, page: null, out: null, format: null, quality: 'high', videoCodec: null, videoBitrate: null, audioCodec: null, allStops: false, motionBlur: null, shutter: null, query: null, strict: false, quiet: false, timeout: 900, root: null, chrome: null };
48
50
  const need = (i, name) => { if (i + 1 >= argv.length) throw new Error(`${name} needs a value`); return argv[i + 1]; };
49
51
  for (let i = 0; i < argv.length; i++) {
50
52
  const a = argv[i];
51
- if (a === '-o' || a === '--out') o.out = need(i++, a);
53
+ if (a === '--help' || a === '-h') o.help = true;
54
+ else if (a === '-o' || a === '--out') o.out = need(i++, a);
52
55
  else if (a === '--format') {
53
56
  o.format = need(i++, a).toLowerCase();
54
57
  if (!FORMATS.includes(o.format)) throw new Error(`--format must be mp4, webm, mov, mkv, wav, ogg or pdf (got "${o.format}")`);
@@ -79,7 +82,8 @@ export function parseRenderArgs(argv) {
79
82
  } else if (a === '--draft') o.draft = true;
80
83
  else if (a === '--query') o.query = need(i++, a).replace(/^\?/, '');
81
84
  else if (a === '--all-stops') o.allStops = true;
82
- else if (a === '--fail-on-warn') o.failOnWarn = true;
85
+ else if (a === '--strict') o.strict = true;
86
+ else if (a === '--fail-on-warn') throw new Error('--fail-on-warn is now --strict (the same word as in check: a warning, or anything it would only list, fails the run)');
83
87
  else if (a === '--quiet') o.quiet = true;
84
88
  else if (a === '--timeout') o.timeout = Math.max(10, Number(need(i++, a)) || 900);
85
89
  else if (a === '--root') o.root = need(i++, a);
@@ -109,7 +113,7 @@ const renderScript = (format, quality, videoCodec, audioCodec, motionBlur, shutt
109
113
  const video = { ${[videoBitrate ? `bitrate: ${JSON.stringify(/^\d+(\.\d+)?$/.test(videoBitrate) ? Number(videoBitrate) : videoBitrate)}` : part.draft && !part.qualityGiven ? null : `bitrate: ${JSON.stringify(quality)}`, videoCodec ? `codec: ${JSON.stringify(videoCodec)}` : null].filter(Boolean).join(', ')} };
110
114
  const audio = { bitrate: ${JSON.stringify(quality)}${audioCodec ? `, codec: ${JSON.stringify(audioCodec)}` : ''} };
111
115
  const blob = ${JSON.stringify(format)} === 'pdf'
112
- ? await movie.exportPDF({ which: ${allStops ? "'stops'" : "'pages'"}, title: document.title })
116
+ ? await (${LOAD_PRESENT}).catch(e => { throw new Error('a PDF needs pixi-effects/present in the page (add "pixi-effects/present": ".../dist/present.js" to its import map, from the same version as the library): ' + ((e && e.message) || e)); }).then(p => p.exportPDF(movie, { which: ${allStops ? "'stops'" : "'pages'"}, title: document.title }))
113
117
  : await movie.render({ format: ${JSON.stringify(format)}, ${SOUND_ONLY.includes(format) ? '' : 'video, '}audio${part.range ? `, range: [${part.range[0] ?? 0}, ${part.range[1] ?? 'movie.duration'}]` : ''}${part.scene ? `, range: ${JSON.stringify(part.scene)}` : ''}${part.scale ? `, scale: ${part.scale}` : ''}${part.draft ? ', draft: true' : ''}${motionBlur ? `, motionBlur: { samples: ${motionBlur}${shutter ? `, shutter: ${shutter}` : ''} }` : ''} });
114
118
  const res = await fetch(${JSON.stringify(UPLOAD)}, { method: 'PUT', body: blob });
115
119
  if (!res.ok) throw new Error('could not hand the file to the command (' + res.status + ')');
@@ -209,8 +213,9 @@ if (process.argv[1] && import.meta.url === pathToFileURL(fs.realpathSync(process
209
213
  try {
210
214
  if (typeof WebSocket === 'undefined') throw new Error('Node >= 22 is needed (built-in WebSocket)');
211
215
  const opts = parseRenderArgs(process.argv.slice(2));
216
+ if (opts.help) { console.log(USAGE); process.exit(0); }
212
217
  if (!opts.page) {
213
- console.error('usage: node ai/tools/render.mjs <page.html> [-o out.mp4] [--format mp4|webm|mov|mkv|pdf] [--range A:B | --scene NAME] [--scale S] [--draft] [--quality very-low|low|medium|high|very-high] [--query a=1] [--fail-on-warn] [--quiet] [--timeout S] [--root DIR] [--chrome PATH]');
218
+ console.error(USAGE);
214
219
  process.exit(2);
215
220
  }
216
221
  let last = -1;
@@ -227,7 +232,7 @@ if (process.argv[1] && import.meta.url === pathToFileURL(fs.realpathSync(process
227
232
  process.exit(1);
228
233
  }
229
234
  console.log(`${shown(r.file)} ${kb(r.bytes)} · ${r.duration} s · ${SOUND_ONLY.includes(opts.format) ? 'sound only' : `${r.width}×${r.height} @ ${r.frameRate} fps`} · ${opts.format} · rendered in ${r.seconds} s`);
230
- process.exit(opts.failOnWarn && r.logs.length ? 1 : 0);
235
+ process.exit(opts.strict && r.logs.length ? 1 : 0);
231
236
  } catch (e) {
232
237
  console.error(`pixi-effects render failed: ${e.message}`);
233
238
  process.exit(2);
package/ai/tools/view.mjs CHANGED
@@ -26,12 +26,15 @@ import { findRoot, serve } from './check.mjs';
26
26
 
27
27
  export const VIEWER = '/__viewer';
28
28
 
29
+ export const USAGE = 'usage: node ai/tools/view.mjs <page.html> [--port N] [--no-open] [--query a=1] [--root DIR]';
30
+
29
31
  export function parseViewArgs(argv) {
30
32
  const o = { page: null, port: 0, open: true, query: null, root: null };
31
33
  const need = (i, name) => { if (i + 1 >= argv.length) throw new Error(`${name} needs a value`); return argv[i + 1]; };
32
34
  for (let i = 0; i < argv.length; i++) {
33
35
  const a = argv[i];
34
- if (a === '--port') o.port = Math.max(0, Math.round(Number(need(i++, a))) || 0);
36
+ if (a === '--help' || a === '-h') o.help = true;
37
+ else if (a === '--port') o.port = Math.max(0, Math.round(Number(need(i++, a))) || 0);
35
38
  else if (a === '--no-open') o.open = false;
36
39
  else if (a === '--query') o.query = need(i++, a).replace(/^\?/, '');
37
40
  else if (a === '--root') o.root = need(i++, a);
@@ -343,8 +346,9 @@ export async function startViewer(opts) {
343
346
  if (process.argv[1] && import.meta.url === pathToFileURL(fs.realpathSync(process.argv[1])).href) { // the real path: npm starts a bin through a symlink
344
347
  try {
345
348
  const opts = parseViewArgs(process.argv.slice(2));
349
+ if (opts.help) { console.log(USAGE); process.exit(0); }
346
350
  if (!opts.page) {
347
- console.error('usage: node ai/tools/view.mjs <page.html> [--port N] [--no-open] [--query a=1] [--root DIR]');
351
+ console.error(USAGE);
348
352
  process.exit(2);
349
353
  }
350
354
  const { url } = await startViewer(opts);
@@ -0,0 +1,60 @@
1
+ import { Container } from 'pixi.js';
2
+ import { S as SequenceSpec, C as CompositionShape } from './types-BzIB65Vk.cjs';
3
+
4
+ /**
5
+ * Resolves a routed keyframe path to a concrete tween target.
6
+ * `path` is everything after the registered prefix + dot (for
7
+ * `three.cube.rotation.y` under prefix `three`, path = `cube.rotation.y`).
8
+ * Return null to skip the key (the router is expected to have warned).
9
+ */
10
+ type PathRouter = (path: string) => {
11
+ target: object;
12
+ prop: string;
13
+ } | null;
14
+ type PathRouters = Record<string, PathRouter>;
15
+
16
+ /** Variables available to expression evaluation. */
17
+ interface Scope {
18
+ w: number;
19
+ h: number;
20
+ W: number;
21
+ H: number;
22
+ GW: number;
23
+ GH: number;
24
+ contain: number;
25
+ cover: number;
26
+ t: number;
27
+ d: number;
28
+ T: number;
29
+ }
30
+
31
+ declare abstract class Sequence {
32
+ spec: SequenceSpec;
33
+ parent: CompositionShape | null;
34
+ root: CompositionShape;
35
+ target: Container | null;
36
+ intrinsicWidth: number;
37
+ intrinsicHeight: number;
38
+ at: number;
39
+ duration: number | undefined;
40
+ constructor(spec: SequenceSpec, parent: CompositionShape | null, root: CompositionShape);
41
+ abstract build(): Promise<void>;
42
+ buildFilters(): void;
43
+ scope(): Scope;
44
+ /**
45
+ * Prefix routers contributed to the keyframe pipeline (e.g. `three.` in
46
+ * pixi-effects/three). Base sequences route nothing extra.
47
+ */
48
+ protected pathRouters(): PathRouters;
49
+ /**
50
+ * The `initial` and `keyframes` that go onto the display object. A layer whose `time` runs a clock (video, composition) takes `time`
51
+ * out of them: it is not a property of the display object.
52
+ */
53
+ protected displayProps(): {
54
+ initial: SequenceSpec['initial'];
55
+ keyframes: SequenceSpec['keyframes'];
56
+ };
57
+ destroy(): void;
58
+ }
59
+
60
+ export { type PathRouters as P, Sequence as S };
@@ -0,0 +1,60 @@
1
+ import { Container } from 'pixi.js';
2
+ import { S as SequenceSpec, C as CompositionShape } from './types-BzIB65Vk.js';
3
+
4
+ /**
5
+ * Resolves a routed keyframe path to a concrete tween target.
6
+ * `path` is everything after the registered prefix + dot (for
7
+ * `three.cube.rotation.y` under prefix `three`, path = `cube.rotation.y`).
8
+ * Return null to skip the key (the router is expected to have warned).
9
+ */
10
+ type PathRouter = (path: string) => {
11
+ target: object;
12
+ prop: string;
13
+ } | null;
14
+ type PathRouters = Record<string, PathRouter>;
15
+
16
+ /** Variables available to expression evaluation. */
17
+ interface Scope {
18
+ w: number;
19
+ h: number;
20
+ W: number;
21
+ H: number;
22
+ GW: number;
23
+ GH: number;
24
+ contain: number;
25
+ cover: number;
26
+ t: number;
27
+ d: number;
28
+ T: number;
29
+ }
30
+
31
+ declare abstract class Sequence {
32
+ spec: SequenceSpec;
33
+ parent: CompositionShape | null;
34
+ root: CompositionShape;
35
+ target: Container | null;
36
+ intrinsicWidth: number;
37
+ intrinsicHeight: number;
38
+ at: number;
39
+ duration: number | undefined;
40
+ constructor(spec: SequenceSpec, parent: CompositionShape | null, root: CompositionShape);
41
+ abstract build(): Promise<void>;
42
+ buildFilters(): void;
43
+ scope(): Scope;
44
+ /**
45
+ * Prefix routers contributed to the keyframe pipeline (e.g. `three.` in
46
+ * pixi-effects/three). Base sequences route nothing extra.
47
+ */
48
+ protected pathRouters(): PathRouters;
49
+ /**
50
+ * The `initial` and `keyframes` that go onto the display object. A layer whose `time` runs a clock (video, composition) takes `time`
51
+ * out of them: it is not a property of the display object.
52
+ */
53
+ protected displayProps(): {
54
+ initial: SequenceSpec['initial'];
55
+ keyframes: SequenceSpec['keyframes'];
56
+ };
57
+ destroy(): void;
58
+ }
59
+
60
+ export { type PathRouters as P, Sequence as S };
@@ -0,0 +1,16 @@
1
+ 'use strict';
2
+
3
+ var chunkWHNADXXG_cjs = require('./chunk-WHNADXXG.cjs');
4
+ require('./chunk-ZVUO325O.cjs');
5
+ require('./chunk-2DXPTKQZ.cjs');
6
+ require('./chunk-LYOKH7KE.cjs');
7
+ require('./chunk-TWZNOUM7.cjs');
8
+
9
+
10
+
11
+ Object.defineProperty(exports, "CompositionSequence", {
12
+ enumerable: true,
13
+ get: function () { return chunkWHNADXXG_cjs.CompositionSequence; }
14
+ });
15
+ //# sourceMappingURL=Composition-MUKR3FYD.cjs.map
16
+ //# sourceMappingURL=Composition-MUKR3FYD.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-K6B3APM5.cjs"}
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-MUKR3FYD.cjs"}
@@ -0,0 +1,7 @@
1
+ export { CompositionSequence } from './chunk-A3Q3SDSH.js';
2
+ import './chunk-ZAXYPXKW.js';
3
+ import './chunk-4KB3GUEK.js';
4
+ import './chunk-RCVZX2JP.js';
5
+ import './chunk-BXOCEZ4U.js';
6
+ //# sourceMappingURL=Composition-RJGA7KDU.js.map
7
+ //# sourceMappingURL=Composition-RJGA7KDU.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-H7UNR5F5.js"}
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-RJGA7KDU.js"}