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/docs/api.md
CHANGED
|
@@ -169,6 +169,22 @@ Returns a `Blob` whose `type` is the container's MIME (e.g. `video/mp4`). Emits
|
|
|
169
169
|
|
|
170
170
|
The renderer also forces a keyframe every ~2 seconds so the resulting file scrubs efficiently in standard players.
|
|
171
171
|
|
|
172
|
+
### Poster time: `movie.init({ poster })`, `movie.poster`, `movie.posterImage()`
|
|
173
|
+
|
|
174
|
+
A movie can say which moment stands for it before it plays, the way `<video poster>` names a picture, except that here it is a **time**, so the picture comes from the same data and needs no separate image file:
|
|
175
|
+
|
|
176
|
+
```js
|
|
177
|
+
await movie.init({ /* … */ poster: 9.5 }); // seconds; a negative value counts back from the end (-2 = two seconds before the end)
|
|
178
|
+
movie.poster; // 9.5, or null
|
|
179
|
+
movie.posterFrame; // 285 at 30 fps, or null
|
|
180
|
+
const blob = await movie.posterImage(); // the picture at that time (frame 0 if there is no poster); { as: 'dataURL', type: 'image/jpeg', scale: 0.5 } like snapshot()
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
- Once the movie is ready the **canvas shows the poster frame**, while the playhead stays at frame 0 (`currentFrame` is 0, the player shows 0:00). The first `play()` or `gotoFrame()` replaces it, and **play starts from 0**.
|
|
184
|
+
- `posterImage()` leaves the movie as it was: still showing the poster, or back at the frame it was at. `contactSheet()` does the same.
|
|
185
|
+
- A time past the end is clamped to the last frame, and something that is not a number is ignored; both warn.
|
|
186
|
+
- `pixi-effects-check` writes the poster as `poster.jpg`.
|
|
187
|
+
|
|
172
188
|
### `movie.timelineData(): TimelineData` and `movie.timelineChart(options?): string`
|
|
173
189
|
|
|
174
190
|
The whole movie as rows on a time axis, for a person to read at a glance. `timelineData()` is the data: `{ duration, rows, transitions }`, a row per layer with `name`, `type`, `depth` (1 inside a composition), absolute `start` / `end` in seconds, `keys` (when each keyframe starts) and, for a family of generated layers, `parts` (one span per layer). Layers of one type whose names differ only by numbers (`ring10-0`, `ring9-1`, `pop-3` …), four or more, become ONE row (`ring#-# ×19`), so a movie with 700 generated layers is a screenful. `timelineChart({ title })` returns one self-contained HTML page (an inline SVG, no scripts): bars coloured by type, ◆ at each keyframe start, a shaded band per transition, a tooltip per bar. `pixi-effects-check` writes it next to the contact sheet as `timeline.html`.
|
package/llms-full.txt
CHANGED
|
@@ -13,16 +13,17 @@ You describe a video as a tree of plain objects; the library plays it in a canva
|
|
|
13
13
|
## Workflow
|
|
14
14
|
|
|
15
15
|
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.
|
|
16
|
-
2. **
|
|
17
|
-
3. **
|
|
16
|
+
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`.
|
|
17
|
+
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.
|
|
18
|
+
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.
|
|
18
19
|
**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.
|
|
19
|
-
|
|
20
|
+
5. **Look at it** — you cannot judge motion from code. The library has the tools built in:
|
|
20
21
|
- `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.
|
|
21
22
|
- `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.
|
|
22
23
|
- `await movie.snapshot(frame, { as: 'dataURL' })` → one frame, canvas only (no player bar).
|
|
23
24
|
- `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.
|
|
24
25
|
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.
|
|
25
|
-
|
|
26
|
+
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).
|
|
26
27
|
|
|
27
28
|
## The rules that cause most failures
|
|
28
29
|
|
|
@@ -46,7 +47,7 @@ Centred text must be positioned by `anchorX/anchorY: 0.5`; estimate text width a
|
|
|
46
47
|
- `reference/cheatsheet.md` — every layer type, prop, default and rule on one page.
|
|
47
48
|
- `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.
|
|
48
49
|
- `reference/pitfalls.md` — the full list of real mistakes, with status.
|
|
49
|
-
- `tools/check.mjs` — the one-command review (see Workflow step
|
|
50
|
+
- `tools/check.mjs` — the one-command review (see Workflow step 5).
|
|
50
51
|
- `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/…`.)
|
|
51
52
|
- `tools/save-image.py` — save a `dataURL` result (contact sheet / snapshot) to a PNG.
|
|
52
53
|
- Full reference: `docs/dsl.md` and `docs/api.md` in the repository.
|
|
@@ -139,6 +140,10 @@ Style (`initial` / keyframes): `fillColor fillAlpha strokeColor strokeAlpha stro
|
|
|
139
140
|
|
|
140
141
|
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`.
|
|
141
142
|
|
|
143
|
+
## Poster
|
|
144
|
+
|
|
145
|
+
`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).
|
|
146
|
+
|
|
142
147
|
## Player
|
|
143
148
|
|
|
144
149
|
`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.
|
|
@@ -2137,6 +2142,22 @@ Returns a `Blob` whose `type` is the container's MIME (e.g. `video/mp4`). Emits
|
|
|
2137
2142
|
|
|
2138
2143
|
The renderer also forces a keyframe every ~2 seconds so the resulting file scrubs efficiently in standard players.
|
|
2139
2144
|
|
|
2145
|
+
### Poster time: `movie.init({ poster })`, `movie.poster`, `movie.posterImage()`
|
|
2146
|
+
|
|
2147
|
+
A movie can say which moment stands for it before it plays, the way `<video poster>` names a picture, except that here it is a **time**, so the picture comes from the same data and needs no separate image file:
|
|
2148
|
+
|
|
2149
|
+
```js
|
|
2150
|
+
await movie.init({ /* … */ poster: 9.5 }); // seconds; a negative value counts back from the end (-2 = two seconds before the end)
|
|
2151
|
+
movie.poster; // 9.5, or null
|
|
2152
|
+
movie.posterFrame; // 285 at 30 fps, or null
|
|
2153
|
+
const blob = await movie.posterImage(); // the picture at that time (frame 0 if there is no poster); { as: 'dataURL', type: 'image/jpeg', scale: 0.5 } like snapshot()
|
|
2154
|
+
```
|
|
2155
|
+
|
|
2156
|
+
- Once the movie is ready the **canvas shows the poster frame**, while the playhead stays at frame 0 (`currentFrame` is 0, the player shows 0:00). The first `play()` or `gotoFrame()` replaces it, and **play starts from 0**.
|
|
2157
|
+
- `posterImage()` leaves the movie as it was: still showing the poster, or back at the frame it was at. `contactSheet()` does the same.
|
|
2158
|
+
- A time past the end is clamped to the last frame, and something that is not a number is ignored; both warn.
|
|
2159
|
+
- `pixi-effects-check` writes the poster as `poster.jpg`.
|
|
2160
|
+
|
|
2140
2161
|
### `movie.timelineData(): TimelineData` and `movie.timelineChart(options?): string`
|
|
2141
2162
|
|
|
2142
2163
|
The whole movie as rows on a time axis, for a person to read at a glance. `timelineData()` is the data: `{ duration, rows, transitions }`, a row per layer with `name`, `type`, `depth` (1 inside a composition), absolute `start` / `end` in seconds, `keys` (when each keyframe starts) and, for a family of generated layers, `parts` (one span per layer). Layers of one type whose names differ only by numbers (`ring10-0`, `ring9-1`, `pop-3` …), four or more, become ONE row (`ring#-# ×19`), so a movie with 700 generated layers is a screenful. `timelineChart({ title })` returns one self-contained HTML page (an inline SVG, no scripts): bars coloured by type, ◆ at each keyframe start, a shaded band per transition, a tooltip per bar. `pixi-effects-check` writes it next to the contact sheet as `timeline.html`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pixi-effects",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Declarative composition and video rendering for the web. Built on PixiJS v8.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -41,6 +41,8 @@
|
|
|
41
41
|
"typecheck": "tsc --noEmit",
|
|
42
42
|
"build:ai": "node scripts/build-llms.mjs",
|
|
43
43
|
"build:gallery": "node scripts/build-gallery.mjs",
|
|
44
|
+
"posters": "node scripts/make-posters.mjs",
|
|
45
|
+
"guide": "node scripts/build-guide.mjs guide-preview",
|
|
44
46
|
"prepublishOnly": "npm run build:ai && npm run typecheck && npm test && npm run build"
|
|
45
47
|
},
|
|
46
48
|
"dependencies": {
|
|
@@ -62,6 +64,7 @@
|
|
|
62
64
|
"vitest": "^2.1.0",
|
|
63
65
|
"happy-dom": "^15.0.0",
|
|
64
66
|
"gsap": "^3.12.0",
|
|
67
|
+
"marked": "^18.0.14",
|
|
65
68
|
"pixi.js": "^8.22.0",
|
|
66
69
|
"pixi-filters": "^6.1.5",
|
|
67
70
|
"three": "^0.185.1",
|