pixi-effects 0.47.4 → 0.48.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 +18 -0
- package/README.md +8 -6
- package/ai/chat-template.html +3 -3
- package/dist/Composition-PMWLJQPI.cjs +16 -0
- package/dist/{Composition-ZYRDRLYB.cjs.map → Composition-PMWLJQPI.cjs.map} +1 -1
- package/dist/Composition-ZCRJHBB5.js +7 -0
- package/dist/{Composition-HTHQ2RLF.js.map → Composition-ZCRJHBB5.js.map} +1 -1
- package/dist/Controller.cjs +3 -0
- package/dist/Controller.cjs.map +1 -1
- package/dist/Controller.d.cts +1 -1
- package/dist/Controller.d.ts +1 -1
- package/dist/Controller.js +3 -0
- package/dist/Controller.js.map +1 -1
- package/dist/{Movie-sV8nJr_f.d.ts → Movie--qSYKgAQ.d.ts} +2 -2
- package/dist/{Movie-BqrAWN0g.d.cts → Movie-svZQF-9Z.d.cts} +2 -2
- package/dist/{Spans-JYCTJV4V.js → Spans-FMAVPATS.js} +4 -4
- package/dist/{Spans-JYCTJV4V.js.map → Spans-FMAVPATS.js.map} +1 -1
- package/dist/{Spans-I7P7ZVZO.cjs → Spans-KTUPDFSR.cjs} +9 -9
- package/dist/{Spans-I7P7ZVZO.cjs.map → Spans-KTUPDFSR.cjs.map} +1 -1
- package/dist/{chunk-34YNRCMT.js → chunk-4PYVHLIK.js} +20 -5
- package/dist/chunk-4PYVHLIK.js.map +1 -0
- package/dist/{chunk-CJS2S5XU.cjs → chunk-ECLBZV4N.cjs} +20 -5
- package/dist/chunk-ECLBZV4N.cjs.map +1 -0
- package/dist/{chunk-NCM2WU7R.js → chunk-GX2WQV4A.js} +58 -35
- package/dist/chunk-GX2WQV4A.js.map +1 -0
- package/dist/chunk-K3XOEC3C.js +53 -0
- package/dist/chunk-K3XOEC3C.js.map +1 -0
- package/dist/{chunk-AES2L75M.cjs → chunk-NGK4E536.cjs} +103 -80
- package/dist/chunk-NGK4E536.cjs.map +1 -0
- package/dist/{chunk-DS2EWRV4.cjs → chunk-OGLRSR6C.cjs} +63 -63
- package/dist/{chunk-DS2EWRV4.cjs.map → chunk-OGLRSR6C.cjs.map} +1 -1
- package/dist/{chunk-RTZ5MW3E.js → chunk-UI7CEUTP.js} +3 -3
- package/dist/{chunk-RTZ5MW3E.js.map → chunk-UI7CEUTP.js.map} +1 -1
- package/dist/chunk-VQXZ2NOI.cjs +56 -0
- package/dist/chunk-VQXZ2NOI.cjs.map +1 -0
- package/dist/index.cjs +38 -33
- package/dist/index.d.cts +19 -2
- package/dist/index.d.ts +19 -2
- package/dist/index.js +4 -3
- package/dist/loader.css +1 -1
- package/dist/present.cjs +4 -3
- package/dist/present.cjs.map +1 -1
- package/dist/present.d.cts +1 -1
- package/dist/present.d.ts +1 -1
- package/dist/present.js +4 -3
- package/dist/present.js.map +1 -1
- package/dist/three.cjs +5 -5
- package/dist/three.js +1 -1
- package/docs/api.md +17 -2
- package/docs/dsl.md +7 -7
- package/llms-full.txt +26 -11
- package/package.json +1 -1
- package/skills/pixi-effects/SKILL.md +1 -1
- package/skills/pixi-effects/reference/pitfalls.md +1 -1
- package/skills/pixi-effects/template.html +6 -6
- package/dist/Composition-HTHQ2RLF.js +0 -7
- package/dist/Composition-ZYRDRLYB.cjs +0 -16
- package/dist/chunk-34YNRCMT.js.map +0 -1
- package/dist/chunk-AES2L75M.cjs.map +0 -1
- package/dist/chunk-CJS2S5XU.cjs.map +0 -1
- package/dist/chunk-NCM2WU7R.js.map +0 -1
package/docs/api.md
CHANGED
|
@@ -75,7 +75,7 @@ Seeks to a specific frame. Pauses if currently playing? **No** — does not chan
|
|
|
75
75
|
|
|
76
76
|
### `movie.playUntil(seconds?): Promise<boolean>`
|
|
77
77
|
|
|
78
|
-
Play, and pause exactly at `seconds` (the frame `round(seconds × frameRate)`). It resolves `true` when the playhead has landed there; `false` when something else ended it first (a `pause()`, a seek, a newer `playUntil()`) or when there is nothing to play toward (a time at or before the playhead: it also pauses the movie where it is; a movie that is not ready). A time at or past the end lands on the last frame; with no time at all it plays to the end, says `ended`, and resolves `true`. An ordinary `play()` is not affected. It is the one move a presentation (`pixi-effects/present`) is built on; it is also the way to "play the intro, then wait": `await movie.playUntil(4.5)`.
|
|
78
|
+
Play, and pause exactly at `seconds` (the frame `round(seconds × frameRate)`). **A frame drawn while playing is the picture `snapshot(frame)` gives** (within one renderer, pixel for pixel: pinned by `tests/films/determinism-playback-vs-seek`); which frames a playing movie draws is not promised (it may draw the start frame again). It resolves `true` when the playhead has landed there; `false` when something else ended it first (a `pause()`, a seek, a newer `playUntil()`) or when there is nothing to play toward (a time at or before the playhead: it also pauses the movie where it is; a movie that is not ready). A time at or past the end lands on the last frame; with no time at all it plays to the end, says `ended`, and resolves `true`. An ordinary `play()` is not affected. It is the one move a presentation (`pixi-effects/present`) is built on; it is also the way to "play the intro, then wait": `await movie.playUntil(4.5)`.
|
|
79
79
|
|
|
80
80
|
### `movie.peek(fn): Promise<T>`
|
|
81
81
|
|
|
@@ -149,7 +149,7 @@ splitText('Hello world', style, { by: 'words', x: 640, y: 300, align: 'center' }
|
|
|
149
149
|
|
|
150
150
|
### `movie.inspect(frame?, options?): Promise<InspectReport>`
|
|
151
151
|
|
|
152
|
-
Where every layer is drawn at `frame`, as JSON — for checking layout without eyes. Seeks there and stays there. `options.layers`: `'visible'` (default, only layers drawn at this frame), `'all'`, or `'none'` (just `summary` and `issues`). Faint layers (alpha < 0.3) and text
|
|
152
|
+
Where every layer is drawn at `frame`, as JSON — for checking layout without eyes. Seeks there and stays there. `options.layers`: `'visible'` (default, only layers drawn at this frame), `'all'`, or `'none'` (just `summary` and `issues`). Faint layers (alpha < 0.3) are not reported, and neither is text WHILE a geometry keyframe (x, y, scale, rotation …) runs on it: once it has settled it is (a title that rises is reported when it stops). `check` looks at the first and last frame, scene edges, and one frame every 0.25 s, so a fault that lasts less than that between samples can go unseen; the layout issues are `text layer "X" is cut off by the canvas edge: …` and `text layer "X" is entirely outside the canvas (…)`.
|
|
153
153
|
|
|
154
154
|
```ts
|
|
155
155
|
interface InspectReport {
|
|
@@ -287,6 +287,21 @@ Returns a `Blob` whose `type` is the container's MIME (e.g. `video/mp4`). Emits
|
|
|
287
287
|
|
|
288
288
|
The renderer also forces a keyframe every ~2 seconds so the resulting file scrubs efficiently in standard players.
|
|
289
289
|
|
|
290
|
+
#### Where the encoder comes from: `useMediabunny(source)`
|
|
291
|
+
|
|
292
|
+
Writing a file (and decoding a `video` layer) is done by [mediabunny](https://mediabunny.dev/), about 180 KB gzipped. It is **fetched the first time something needs it**: the first `render()` (or the download button), or a movie with a `video` asset at `init`. A page that only plays never fetches it and needs no import map entry for it, so a published movie can leave the `"mediabunny"` line out.
|
|
293
|
+
|
|
294
|
+
With nothing said, the bare name `'mediabunny'` is imported, which a bundler (it becomes a chunk of its own) or the page's import map resolves. `useMediabunny()` says where it comes from instead; call it before the first export or `init` with a video:
|
|
295
|
+
|
|
296
|
+
```ts
|
|
297
|
+
import { useMediabunny } from 'pixi-effects';
|
|
298
|
+
useMediabunny('https://cdn.jsdelivr.net/npm/mediabunny@1.61.3/+esm'); // a URL to import(): a page with no import map entry for it
|
|
299
|
+
useMediabunny(() => import('mediabunny')); // a function that returns the module: your own copy, a chunk
|
|
300
|
+
import * as mb from 'mediabunny'; useMediabunny(mb); // the module itself: ready, nothing is fetched later
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
When it cannot be loaded, `render()` (or `init` with a video) rejects with `could not load mediabunny from …`, saying what to add; the next call tries again.
|
|
304
|
+
|
|
290
305
|
### Poster time: `movie.init({ poster })`, `movie.poster`, `movie.posterImage()`
|
|
291
306
|
|
|
292
307
|
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:
|
package/docs/dsl.md
CHANGED
|
@@ -224,7 +224,7 @@ Audio-only sequence. No visual. Volume is animatable via keyframes.
|
|
|
224
224
|
|
|
225
225
|
Audio is mixed during `Movie.init()` and during `Movie.render()`. Volume keyframes interpolate linearly.
|
|
226
226
|
|
|
227
|
-
**The loudness of the whole mix: `movie.init({ loudness: -16 })
|
|
227
|
+
**The loudness of the whole mix: `movie.init({ loudness: -16 })`** (reached within 0.15 LU when it is reachable; the loudness of the FILE after lossy encoding is within about 1 LU and its true peak about 1 dB lower than the mix's: AAC measured −2.1 dBTP and Opus −1.6 for a mix at −1.0; an mp4's sound track is padded to a whole AAC frame, about 28 ms longer than the movie). Layers make whatever level their `volume` gives (a few `sfx` at volume 0.15 mix to about -40 LUFS, far quieter than web video's -14 to -16). `loudness` is a target for the finished mix in LUFS (ITU-R BS.1770): one gain, up or down, and a look-ahead limiter that takes a click or a hit on top of a quiet bed down so that no peak passes -1 dBTP (true peak), the same for playback and export. A mix whose peaks are within 10 dB of what the limiter can take reaches the target (`check` says where the limiter worked hardest and which sounds play there when it took 3 dB or more off); a click 30 dB over its bed is out of reach and stops short (the note says `the mix could not reach -16 LUFS: it stopped at -19.4 LUFS because …`; it is never quieter than the plain gain would make it), and `check` / `inspectAudio()` name the moment and the sounds that make it, with the fix (lower that sound, or ask for less). **Raising a `sfx` over music is safe:** the limiter keeps the peak, so give an accent the volume the `buried` note suggests (an accent over music needs 2 to 3). `false` or no option leaves the mix as the layers make it; `check` then says when it is quiet (found in a trial of nine films with sound: eight were -22 to -26 LUFS and only one was raised).
|
|
228
228
|
|
|
229
229
|
#### Sound effects without files: `sfx`
|
|
230
230
|
|
|
@@ -352,7 +352,7 @@ Every primitive draws centred on its local origin (so `anchorX`/`anchorY` and `p
|
|
|
352
352
|
| `polygon` | `points: [[x,y], …]` | `open` (default false), `trimStart`, `trimEnd` |
|
|
353
353
|
| `path` | `d` (SVG path data) | `fillRule`, `trimStart`, `trimEnd`, `morphTo`, `morph`, `morphPoints` |
|
|
354
354
|
|
|
355
|
-
**Holes (`fillRule: 'evenodd'`) on a `path`.** Several sub-paths in one `d` are each filled on their own: a donut written as two circles is a solid disc. With `fillRule: 'evenodd'` the biggest sub-path is the shape and every other one is a hole cut out of it (the stroke goes round the shape and each hole): `{ shape: 'path', fillRule: 'evenodd', d: 'M 0 -100 A 100 100 0 1 1 0 100 A 100 100 0 1 1 0 -100 Z M 0 -50 A 50 50 0 1 0 0 50 A 50 50 0 1 0 0 -50 Z', initial: { x: 640, y: 360, fillColor: '#ffcc00' } }`. Write the sub-paths in absolute coordinates (each starts with `M`); an island inside a hole is not drawn, and a path that morphs (`morphTo`) has no holes.
|
|
355
|
+
**Holes (`fillRule: 'evenodd'`) on a `path`.** Several sub-paths in one `d` are each filled on their own: a donut written as two circles is a solid disc. With `fillRule: 'evenodd'` the biggest sub-path is the shape and every other one is a hole cut out of it (the stroke goes round the shape and each hole): `{ shape: 'path', fillRule: 'evenodd', d: 'M 0 -100 A 100 100 0 1 1 0 100 A 100 100 0 1 1 0 -100 Z M 0 -50 A 50 50 0 1 0 0 50 A 50 50 0 1 0 0 -50 Z', initial: { x: 640, y: 360, fillColor: '#ffcc00' } }`. Write the sub-paths in absolute coordinates (each starts with `M`); a **stroke is centred on the outline** (half of `strokeWidth` each side), an island inside a hole is not drawn (nor stroked: it is as if it were not in `d`; real SVG `evenodd` would fill it), and a path that morphs (`morphTo`) has no holes.
|
|
356
356
|
|
|
357
357
|
**`arc`** is an arc around its local origin (the centre). Angles are in **degrees: 0° is 3 o'clock and positive is clockwise**, so a progress ring starts at `startAngle: -90` (12 o'clock) and animates `endAngle` from `-90` to `270`. With a stroke and no fill it is an open arc line (`strokeCap: 'round'` for rounded ends); with a fill it is a sector (a pie slice), and with `innerRadius` a ring segment (a donut slice). A sweep of 360° or more is a full circle; `endAngle < startAngle` sweeps counter-clockwise. `radius`, `innerRadius`, `startAngle` and `endAngle` are all animatable.
|
|
358
358
|
|
|
@@ -547,7 +547,7 @@ It is a layer like any other: `name`, `at`, `duration`, `initial`, `keyframes`,
|
|
|
547
547
|
|
|
548
548
|
**Helpers you can import** (they are plain functions of their input, so they are safe in `draw`): `import { ease, noise, noise2, random, rand, samplePath } from 'pixi-effects'`. `ease('power3.out')` returns the function `x => eased x` for any ease name a keyframe takes (`back.out`, `elastic.out`, `cubic-bezier(…)`, `spring.…`), so a canvas moves like the other layers. `noise(x, seed)` and `noise2(x, y, seed)` are smooth noise in [-1, 1] (sum a few doublings of `x` and `y` for clouds and terrain); `random(seed)` is a repeatable stream and `rand(seed)` one number for a seed. `samplePath(d)` walks SVG path data by length: `.length`, `.at(u)` gives `{ x, y, angle }` (radians) a fraction `u` along, `.trim(a, b)` gives the part between two fractions as flat `[x, y, …]` pieces for `moveTo` / `lineTo` (a line that draws on, a string set along a curve, a petal that falls along a route). Build a sampler once (module scope or `setup`) and ask it every frame.
|
|
549
549
|
|
|
550
|
-
**The contract: a function of the time.** The same `t` and the same `params` must give the same pixels, in playback, in a seek and in an export. So: no `Math.random`, `Date.now`, `performance.now` or `new Date()` (a warning at build says so, and `check` draws each canvas layer twice at a few moments and reports a layer whose two pictures differ), and no state that outlives a call (a cache of a pure function, same input same output, is fine: a palette per phase, a table filled on the first call; state that depends on which frames were drawn before is not). A trail is drawn from the positions of the past computed from `t`, not from the last picture. A **simulation** (a game of life, a physics sim, a growth process) is computed whole in `setup` into an array and read by `info.frame` in `draw`. **Data that several canvas layers share** (the same plant, the same city, a palette phase) belongs in module scope, computed before `movie.init` from `random(seed)` and plain functions of the time: the state `setup` returns is private to its layer, and every layer reads the same clock through `info.time`. **Sprites, masks and scratch pictures** are made in `setup` with `info.canvas(w, h)` and drawn back with `drawImage` (a word rendered once, a paper texture, a mask for `destination-in`); not with `document.createElement('canvas')`: a GPU canvas used as a source can draw a little differently the first time (measured: up to 128 of 255 over 40 000 pixels on a 1280×720 layer), which is what `info.canvas` avoids. Everything of the 2D context works (`ctx.filter`, composite operations such as `source-atop` and `destination-in`, `drawImage` of another canvas, `measureText`: in `setup`, `info.canvas(1, 1).getContext('2d')` is a context to measure with). A web font must be loaded before it is measured or drawn: `await document.fonts.load('40px "Name"')` in `setup`. A type effect that shows words through a moving texture is: draw the words (the mask) first, then `ctx.globalCompositeOperation = 'source-atop'` and the texture, which paints only where the words are.
|
|
550
|
+
**The contract: a function of the time.** The same `t` and the same `params` must give the same pixels, in playback, in a seek and in an export. So: no `Math.random`, `Date.now`, `performance.now` or `new Date()` (a warning at build says so — it only catches a direct `Math.random()` in the draw function's own text, not one taken by reference — and `check` draws each canvas layer twice at a few moments and reports a layer whose two pictures differ), and no state that outlives a call (a cache of a pure function, same input same output, is fine: a palette per phase, a table filled on the first call; state that depends on which frames were drawn before is not). A trail is drawn from the positions of the past computed from `t`, not from the last picture. A **simulation** (a game of life, a physics sim, a growth process) is computed whole in `setup` into an array and read by `info.frame` in `draw`. **Data that several canvas layers share** (the same plant, the same city, a palette phase) belongs in module scope, computed before `movie.init` from `random(seed)` and plain functions of the time: the state `setup` returns is private to its layer, and every layer reads the same clock through `info.time`. **Sprites, masks and scratch pictures** are made in `setup` with `info.canvas(w, h)` and drawn back with `drawImage` (a word rendered once, a paper texture, a mask for `destination-in`); not with `document.createElement('canvas')`: a GPU canvas used as a source can draw a little differently the first time (measured: up to 128 of 255 over 40 000 pixels on a 1280×720 layer), which is what `info.canvas` avoids. Everything of the 2D context works (`ctx.filter`, composite operations such as `source-atop` and `destination-in`, `drawImage` of another canvas, `measureText`: in `setup`, `info.canvas(1, 1).getContext('2d')` is a context to measure with). A web font must be loaded before it is measured or drawn: `await document.fonts.load('40px "Name"')` in `setup`. A type effect that shows words through a moving texture is: draw the words (the mask) first, then `ctx.globalCompositeOperation = 'source-atop'` and the texture, which paints only where the words are.
|
|
551
551
|
|
|
552
552
|
**What it cannot do.** It cannot see or paint on what is below it in the stack (the stage is drawn by the GPU, not by a 2D context): use `filters`, a matte or a composition that holds the layers for that. **`check` does not see text drawn in a canvas**: its layout test (cut-off, off-canvas, overlap) reads the text layers only, so labels and captions you draw yourself can collide without a word; look at the contact sheet and at `--crop` enlargements. It is not audio. For a picture made by a formula on the GPU, the `shader` layer is faster. A spec that holds a `canvas` layer holds functions, so it is not pure JSON (the same standing as `three` and the `custom` filter).
|
|
553
553
|
|
|
@@ -1096,7 +1096,7 @@ interface Keyframe {
|
|
|
1096
1096
|
The four kinds are mutually exclusive per keyframe:
|
|
1097
1097
|
|
|
1098
1098
|
- **`set`** — instantaneous property assignment at `at`.
|
|
1099
|
-
- **`to`** — tween from whatever the property is at `at` to the given values, over `duration`. **Keyframes on one property run in the order of their `at`, not of the list** (written backwards they play the same). A `to` starts from where the keyframe before it ENDED, so two keyframes of one property that overlap in time make the picture jump: the later one takes over at its `at`, starting from the earlier one's end value (A: x 40 → 160 over 0.5–1.5 s, B: → 280 from 1 s: the layer is at 96 just before 1 s and at 160 just after). Start the second after the first ends. Pinned by `tests/films/time-keyframe-order`.
|
|
1099
|
+
- **`to`** — tween from whatever the property is at `at` to the given values, over `duration`. **Keyframes on one property run in the order of their `at`, not of the list** (written backwards they play the same). A `to` starts from where the keyframe before it ENDED, so two keyframes of one property that overlap in time make the picture jump: the later one takes over at its `at`, starting from the earlier one's end value (A: x 40 → 160 over 0.5–1.5 s, B: → 280 from 1 s: the layer is at 96 just before 1 s and at 160 just after). Start the second after the first ends. Two keyframes of one property with the SAME `at` are decided by the list: the one listed later writes the property from that `at` on, starting from the end value of the one before (a jump, silently). Pinned by `tests/films/time-keyframe-order` and `time-same-at-and-speed-edges`.
|
|
1100
1100
|
- **`from`** — tween from the given values back to the current property, over `duration`.
|
|
1101
1101
|
- **`from` + `to`** — full fromTo tween, with explicit start and end values.
|
|
1102
1102
|
|
|
@@ -1339,7 +1339,7 @@ Validation runs at composition build time. Errors throw with the offending `tran
|
|
|
1339
1339
|
|
|
1340
1340
|
### `crossfade`
|
|
1341
1341
|
|
|
1342
|
-
Alpha cross-dissolve. `from` fades to `alpha: 0`, `to` starts at `alpha: 0` and fades to `1`, both over `[at, at + duration]`.
|
|
1342
|
+
Alpha cross-dissolve. `from` fades to `alpha: 0`, `to` starts at `alpha: 0` and fades to `1`, both over `[at, at + duration]`. **It is not a linear mix of the two pictures:** `to` is laid over a fading `from`, so over black the middle is darker (a red `from` and a blue `to` at the half way are (64, 0, 127), not (128, 0, 127)), and a composition's `alpha` is multiplied into each of its children (not one group opacity as in After Effects): the composition's own background shows through its top layers while it fades. Put what must stay solid on its own layer outside the two scenes. Pinned by `tests/films/transition-under-matte`.
|
|
1343
1343
|
|
|
1344
1344
|
```ts
|
|
1345
1345
|
{ kind: 'crossfade', from: 'A', to: 'B', at: 4, duration: 1, ease: 'sine.inOut' }
|
|
@@ -1349,7 +1349,7 @@ If `to` already has an explicit `initial.alpha` (other than 0), the expander thr
|
|
|
1349
1349
|
|
|
1350
1350
|
### `wipe`
|
|
1351
1351
|
|
|
1352
|
-
A directional reveal. `to` is masked by a soft edge that travels across the screen.
|
|
1352
|
+
A directional reveal. `to` is masked by a soft edge that travels across the screen: at progress 0 nothing of `to` shows, at 1 nothing of `from`, and the edge travels the whole width as the progress goes from 0 to 1; `from` is cut away hard where `to` is fully revealed.
|
|
1353
1353
|
|
|
1354
1354
|
```ts
|
|
1355
1355
|
{
|
|
@@ -1418,7 +1418,7 @@ A scaled punch-in / punch-out. By default `B` opens up: it starts large and zoom
|
|
|
1418
1418
|
|
|
1419
1419
|
### `dissolve`
|
|
1420
1420
|
|
|
1421
|
-
A noise reveal driven by deterministic 2D Perlin noise. Pixels with a low noise value reveal first; as `uProgress` advances, more pixels reveal. At the default `scale` of 30 it reads as large blotches (about 40 px each), not as grain: `scale: 200` or more gives a fine grain. The same `seed` always produces the same dissolve pattern, so a render is bit-exact reproducible.
|
|
1421
|
+
A noise reveal driven by deterministic 2D Perlin noise (`from` loses exactly the pixels `to` reveals: the same noise, a hard cutoff, like wipe and iris). Pixels with a low noise value reveal first; as `uProgress` advances, more pixels reveal. At the default `scale` of 30 it reads as large blotches (about 40 px each), not as grain: `scale: 200` or more gives a fine grain. The same `seed` always produces the same dissolve pattern, so a render is bit-exact reproducible.
|
|
1422
1422
|
|
|
1423
1423
|
```ts
|
|
1424
1424
|
{
|
package/llms-full.txt
CHANGED
|
@@ -6,7 +6,7 @@ Concatenation of the skill, cheatsheet, pitfalls, recipes and the DSL / API / me
|
|
|
6
6
|
|
|
7
7
|
<!-- ===== skills/pixi-effects/SKILL.md ===== -->
|
|
8
8
|
|
|
9
|
-
> Written for pixi-effects 0.
|
|
9
|
+
> Written for pixi-effects 0.48.0. Update this skill with `npx skills update`; the newest docs are at https://yjmtmtk.github.io/pixi-effects/
|
|
10
10
|
|
|
11
11
|
# pixi-effects — write a video as data
|
|
12
12
|
|
|
@@ -412,7 +412,7 @@ A good habit that catches most of these: after `init`, read `window.__logs` (see
|
|
|
412
412
|
64. **`animateText()`: `out: { preset, at: T }` starts the exit at composition second T; without `at` the exit is the end of the layer run backwards.** `duration` is still how long until the layer ends (every piece is gone by `at + duration`): with `out.at`, give `duration` room for the exit and the wave after T (a warning says `out.at ≤ …` when it does not fit, or when T is before the entrance has finished). Without `out.at`, to be gone at time T set `duration: T − at` (title at 0.4 s that must be gone by 8.0 s: `duration: 7.6`). A hand-written exit keyframe works the other way: a negative `at` counts back from the end of its own layer, a positive one from the start of its own layer, never from the movie.
|
|
413
413
|
65. **A spring that does not spring, and `'auto'` that is not a spring.** `spring(1,170,26)` is critically damped (damping ratio 1): it never overshoots; at stiffness 170 use damping below 20 (`spring(1, 170, 12)`), or pick a preset (`spring.bouncy`, `spring.wobbly`). With a number `duration`, that number is the settle time (mass and stiffness only matter with `duration: 'auto'`). `duration: 'auto'` needs a spring ease (otherwise it warns and uses 0.5 s) and is for keyframes, not for `animateText` / `orbit` tweens. A colour that overshoots is clamped in its colour space; `alpha` past 1 shows nothing.
|
|
414
414
|
66. **Animating a gradient: one `fillGradient` object, partial, in the keyframe.** `to: { fillGradient: { angle: 360, stops: [...] } }` (keys you leave out stay). Not `'fillGradient.angle'`, not `gradientAngle`, not `fillGradient: 'red'` (each warns once with what to write). The layer must start with a `fillGradient`; `stops` keep their count; `type` cannot change. A fade between two colours in the default `rgb` passes through grey (green → magenta): set `colorSpace: 'oklch'`. Under a mask the stops still span the whole shape (see 39).
|
|
415
|
-
67. **A key the layer does not have is warned about now (it used to be ignored in silence).** `{ type: 'text', anchorX: 0.5 }` (anchors go in `initial`), `{ type: 'audio', sfx: 'click', pitch: 3 }` (the knob goes inside: `sfx: { preset: 'click', pitch: 3 }`), `{ type: 'shape', shape: 'rect', radius: 5 }` (that is a circle's key; a rect has `cornerRadius`), a mistyped property in a keyframe (`alhpa`), a mistyped text `style` key. The warning names the
|
|
415
|
+
67. **A key the layer does not have is warned about now (it used to be ignored in silence).** `{ type: 'text', anchorX: 0.5 }` (anchors go in `initial`), `{ type: 'audio', sfx: 'click', pitch: 3 }` (the knob goes inside: `sfx: { preset: 'click', pitch: 3 }`), `{ type: 'shape', shape: 'rect', radius: 5 }` (that is a circle's key; a rect has `cornerRadius`), a mistyped property in a keyframe (`alhpa`), a mistyped text `style` key. The warning names the layer, the key and what was probably meant (or what to write: an sfx knob says `sfx: { preset: 'click', pitch: 3 }`); fix the key, do not silence the warning. Three gallery pieces had the `pitch` mistake for months: their sound effects were never pitched.
|
|
416
416
|
68. **A pitched sfx keeps its own length; a radial gradient is placed in the shape's bounds.** `sfx: { preset: 'chime', pitch: -5 }` lasts as long as the sound does, so near the end of the movie give the layer a `duration` that ends with it (`inspectAudio()` reports a sound cut off by the end). A radial `fillGradient`'s `center` / `radius` are fractions of the SHAPE, not the canvas: for a glow on a big circle, convert canvas pixels to fractions of the circle. `music.reverb` is 0–0.6.
|
|
417
417
|
69. **Inside a remapped composition, `at` is in THAT composition's seconds.** With `speed: 0.5` a 4 s composition shows only 2 s of its content; a layer at `at: 3` inside is never seen (warns: "its clock only reaches 0–2s"). With `speed: 2` the content is 8 s long, so children default to 8 s and `at: 5` is fine. The composition's own `at`, `duration`, x / alpha keyframes are outer time. To freeze, hold `time` at one value (`to` the same number); to rewind, `to: { time: 0 }`. A sound inside goes silent while the clock stands still, and an `sfx` longer than the clock's reach is reported as cut off by the clock: give it a shorter `duration` (the sfx limit is 10 s) or let the clock run on. A child whose life ends exactly where a held `time` stands is hidden during the hold: give it a `duration` that reaches past (the content is as long as the larger of `duration` and the largest `time`). **With `time` keyframes the composition's own `duration` is its life in OUTER time and must reach the last keyframe** (`keyframes[1] starts at 2.3s, after the layer ends` means `duration` is too short): `at` of a `time` keyframe counts from the composition's start, the content length is a separate thing that comes out of the `time` values.
|
|
418
418
|
70. **`speed` is a setting, `time` is the property.** `speed: 2` on the layer; to change the speed over time animate `time` (a curve of seconds in the file), never `keyframes: [{ to: { speed: 3 } }]` (warns). `speed: 0` is not allowed (hold a moment with `time` to the same value twice); `speed` together with keyframed `time`: `time` wins. Other names (`playbackRate`, `timeScale`, `rate`, `reverse: true`, `timeRemap`) are warned about with what to write. `particles({ speed })` is something else: launch speed in px per second.
|
|
@@ -2534,7 +2534,7 @@ Audio-only sequence. No visual. Volume is animatable via keyframes.
|
|
|
2534
2534
|
|
|
2535
2535
|
Audio is mixed during `Movie.init()` and during `Movie.render()`. Volume keyframes interpolate linearly.
|
|
2536
2536
|
|
|
2537
|
-
**The loudness of the whole mix: `movie.init({ loudness: -16 })
|
|
2537
|
+
**The loudness of the whole mix: `movie.init({ loudness: -16 })`** (reached within 0.15 LU when it is reachable; the loudness of the FILE after lossy encoding is within about 1 LU and its true peak about 1 dB lower than the mix's: AAC measured −2.1 dBTP and Opus −1.6 for a mix at −1.0; an mp4's sound track is padded to a whole AAC frame, about 28 ms longer than the movie). Layers make whatever level their `volume` gives (a few `sfx` at volume 0.15 mix to about -40 LUFS, far quieter than web video's -14 to -16). `loudness` is a target for the finished mix in LUFS (ITU-R BS.1770): one gain, up or down, and a look-ahead limiter that takes a click or a hit on top of a quiet bed down so that no peak passes -1 dBTP (true peak), the same for playback and export. A mix whose peaks are within 10 dB of what the limiter can take reaches the target (`check` says where the limiter worked hardest and which sounds play there when it took 3 dB or more off); a click 30 dB over its bed is out of reach and stops short (the note says `the mix could not reach -16 LUFS: it stopped at -19.4 LUFS because …`; it is never quieter than the plain gain would make it), and `check` / `inspectAudio()` name the moment and the sounds that make it, with the fix (lower that sound, or ask for less). **Raising a `sfx` over music is safe:** the limiter keeps the peak, so give an accent the volume the `buried` note suggests (an accent over music needs 2 to 3). `false` or no option leaves the mix as the layers make it; `check` then says when it is quiet (found in a trial of nine films with sound: eight were -22 to -26 LUFS and only one was raised).
|
|
2538
2538
|
|
|
2539
2539
|
#### Sound effects without files: `sfx`
|
|
2540
2540
|
|
|
@@ -2662,7 +2662,7 @@ Every primitive draws centred on its local origin (so `anchorX`/`anchorY` and `p
|
|
|
2662
2662
|
| `polygon` | `points: [[x,y], …]` | `open` (default false), `trimStart`, `trimEnd` |
|
|
2663
2663
|
| `path` | `d` (SVG path data) | `fillRule`, `trimStart`, `trimEnd`, `morphTo`, `morph`, `morphPoints` |
|
|
2664
2664
|
|
|
2665
|
-
**Holes (`fillRule: 'evenodd'`) on a `path`.** Several sub-paths in one `d` are each filled on their own: a donut written as two circles is a solid disc. With `fillRule: 'evenodd'` the biggest sub-path is the shape and every other one is a hole cut out of it (the stroke goes round the shape and each hole): `{ shape: 'path', fillRule: 'evenodd', d: 'M 0 -100 A 100 100 0 1 1 0 100 A 100 100 0 1 1 0 -100 Z M 0 -50 A 50 50 0 1 0 0 50 A 50 50 0 1 0 0 -50 Z', initial: { x: 640, y: 360, fillColor: '#ffcc00' } }`. Write the sub-paths in absolute coordinates (each starts with `M`); an island inside a hole is not drawn, and a path that morphs (`morphTo`) has no holes.
|
|
2665
|
+
**Holes (`fillRule: 'evenodd'`) on a `path`.** Several sub-paths in one `d` are each filled on their own: a donut written as two circles is a solid disc. With `fillRule: 'evenodd'` the biggest sub-path is the shape and every other one is a hole cut out of it (the stroke goes round the shape and each hole): `{ shape: 'path', fillRule: 'evenodd', d: 'M 0 -100 A 100 100 0 1 1 0 100 A 100 100 0 1 1 0 -100 Z M 0 -50 A 50 50 0 1 0 0 50 A 50 50 0 1 0 0 -50 Z', initial: { x: 640, y: 360, fillColor: '#ffcc00' } }`. Write the sub-paths in absolute coordinates (each starts with `M`); a **stroke is centred on the outline** (half of `strokeWidth` each side), an island inside a hole is not drawn (nor stroked: it is as if it were not in `d`; real SVG `evenodd` would fill it), and a path that morphs (`morphTo`) has no holes.
|
|
2666
2666
|
|
|
2667
2667
|
**`arc`** is an arc around its local origin (the centre). Angles are in **degrees: 0° is 3 o'clock and positive is clockwise**, so a progress ring starts at `startAngle: -90` (12 o'clock) and animates `endAngle` from `-90` to `270`. With a stroke and no fill it is an open arc line (`strokeCap: 'round'` for rounded ends); with a fill it is a sector (a pie slice), and with `innerRadius` a ring segment (a donut slice). A sweep of 360° or more is a full circle; `endAngle < startAngle` sweeps counter-clockwise. `radius`, `innerRadius`, `startAngle` and `endAngle` are all animatable.
|
|
2668
2668
|
|
|
@@ -2857,7 +2857,7 @@ It is a layer like any other: `name`, `at`, `duration`, `initial`, `keyframes`,
|
|
|
2857
2857
|
|
|
2858
2858
|
**Helpers you can import** (they are plain functions of their input, so they are safe in `draw`): `import { ease, noise, noise2, random, rand, samplePath } from 'pixi-effects'`. `ease('power3.out')` returns the function `x => eased x` for any ease name a keyframe takes (`back.out`, `elastic.out`, `cubic-bezier(…)`, `spring.…`), so a canvas moves like the other layers. `noise(x, seed)` and `noise2(x, y, seed)` are smooth noise in [-1, 1] (sum a few doublings of `x` and `y` for clouds and terrain); `random(seed)` is a repeatable stream and `rand(seed)` one number for a seed. `samplePath(d)` walks SVG path data by length: `.length`, `.at(u)` gives `{ x, y, angle }` (radians) a fraction `u` along, `.trim(a, b)` gives the part between two fractions as flat `[x, y, …]` pieces for `moveTo` / `lineTo` (a line that draws on, a string set along a curve, a petal that falls along a route). Build a sampler once (module scope or `setup`) and ask it every frame.
|
|
2859
2859
|
|
|
2860
|
-
**The contract: a function of the time.** The same `t` and the same `params` must give the same pixels, in playback, in a seek and in an export. So: no `Math.random`, `Date.now`, `performance.now` or `new Date()` (a warning at build says so, and `check` draws each canvas layer twice at a few moments and reports a layer whose two pictures differ), and no state that outlives a call (a cache of a pure function, same input same output, is fine: a palette per phase, a table filled on the first call; state that depends on which frames were drawn before is not). A trail is drawn from the positions of the past computed from `t`, not from the last picture. A **simulation** (a game of life, a physics sim, a growth process) is computed whole in `setup` into an array and read by `info.frame` in `draw`. **Data that several canvas layers share** (the same plant, the same city, a palette phase) belongs in module scope, computed before `movie.init` from `random(seed)` and plain functions of the time: the state `setup` returns is private to its layer, and every layer reads the same clock through `info.time`. **Sprites, masks and scratch pictures** are made in `setup` with `info.canvas(w, h)` and drawn back with `drawImage` (a word rendered once, a paper texture, a mask for `destination-in`); not with `document.createElement('canvas')`: a GPU canvas used as a source can draw a little differently the first time (measured: up to 128 of 255 over 40 000 pixels on a 1280×720 layer), which is what `info.canvas` avoids. Everything of the 2D context works (`ctx.filter`, composite operations such as `source-atop` and `destination-in`, `drawImage` of another canvas, `measureText`: in `setup`, `info.canvas(1, 1).getContext('2d')` is a context to measure with). A web font must be loaded before it is measured or drawn: `await document.fonts.load('40px "Name"')` in `setup`. A type effect that shows words through a moving texture is: draw the words (the mask) first, then `ctx.globalCompositeOperation = 'source-atop'` and the texture, which paints only where the words are.
|
|
2860
|
+
**The contract: a function of the time.** The same `t` and the same `params` must give the same pixels, in playback, in a seek and in an export. So: no `Math.random`, `Date.now`, `performance.now` or `new Date()` (a warning at build says so — it only catches a direct `Math.random()` in the draw function's own text, not one taken by reference — and `check` draws each canvas layer twice at a few moments and reports a layer whose two pictures differ), and no state that outlives a call (a cache of a pure function, same input same output, is fine: a palette per phase, a table filled on the first call; state that depends on which frames were drawn before is not). A trail is drawn from the positions of the past computed from `t`, not from the last picture. A **simulation** (a game of life, a physics sim, a growth process) is computed whole in `setup` into an array and read by `info.frame` in `draw`. **Data that several canvas layers share** (the same plant, the same city, a palette phase) belongs in module scope, computed before `movie.init` from `random(seed)` and plain functions of the time: the state `setup` returns is private to its layer, and every layer reads the same clock through `info.time`. **Sprites, masks and scratch pictures** are made in `setup` with `info.canvas(w, h)` and drawn back with `drawImage` (a word rendered once, a paper texture, a mask for `destination-in`); not with `document.createElement('canvas')`: a GPU canvas used as a source can draw a little differently the first time (measured: up to 128 of 255 over 40 000 pixels on a 1280×720 layer), which is what `info.canvas` avoids. Everything of the 2D context works (`ctx.filter`, composite operations such as `source-atop` and `destination-in`, `drawImage` of another canvas, `measureText`: in `setup`, `info.canvas(1, 1).getContext('2d')` is a context to measure with). A web font must be loaded before it is measured or drawn: `await document.fonts.load('40px "Name"')` in `setup`. A type effect that shows words through a moving texture is: draw the words (the mask) first, then `ctx.globalCompositeOperation = 'source-atop'` and the texture, which paints only where the words are.
|
|
2861
2861
|
|
|
2862
2862
|
**What it cannot do.** It cannot see or paint on what is below it in the stack (the stage is drawn by the GPU, not by a 2D context): use `filters`, a matte or a composition that holds the layers for that. **`check` does not see text drawn in a canvas**: its layout test (cut-off, off-canvas, overlap) reads the text layers only, so labels and captions you draw yourself can collide without a word; look at the contact sheet and at `--crop` enlargements. It is not audio. For a picture made by a formula on the GPU, the `shader` layer is faster. A spec that holds a `canvas` layer holds functions, so it is not pure JSON (the same standing as `three` and the `custom` filter).
|
|
2863
2863
|
|
|
@@ -3406,7 +3406,7 @@ interface Keyframe {
|
|
|
3406
3406
|
The four kinds are mutually exclusive per keyframe:
|
|
3407
3407
|
|
|
3408
3408
|
- **`set`** — instantaneous property assignment at `at`.
|
|
3409
|
-
- **`to`** — tween from whatever the property is at `at` to the given values, over `duration`. **Keyframes on one property run in the order of their `at`, not of the list** (written backwards they play the same). A `to` starts from where the keyframe before it ENDED, so two keyframes of one property that overlap in time make the picture jump: the later one takes over at its `at`, starting from the earlier one's end value (A: x 40 → 160 over 0.5–1.5 s, B: → 280 from 1 s: the layer is at 96 just before 1 s and at 160 just after). Start the second after the first ends. Pinned by `tests/films/time-keyframe-order`.
|
|
3409
|
+
- **`to`** — tween from whatever the property is at `at` to the given values, over `duration`. **Keyframes on one property run in the order of their `at`, not of the list** (written backwards they play the same). A `to` starts from where the keyframe before it ENDED, so two keyframes of one property that overlap in time make the picture jump: the later one takes over at its `at`, starting from the earlier one's end value (A: x 40 → 160 over 0.5–1.5 s, B: → 280 from 1 s: the layer is at 96 just before 1 s and at 160 just after). Start the second after the first ends. Two keyframes of one property with the SAME `at` are decided by the list: the one listed later writes the property from that `at` on, starting from the end value of the one before (a jump, silently). Pinned by `tests/films/time-keyframe-order` and `time-same-at-and-speed-edges`.
|
|
3410
3410
|
- **`from`** — tween from the given values back to the current property, over `duration`.
|
|
3411
3411
|
- **`from` + `to`** — full fromTo tween, with explicit start and end values.
|
|
3412
3412
|
|
|
@@ -3649,7 +3649,7 @@ Validation runs at composition build time. Errors throw with the offending `tran
|
|
|
3649
3649
|
|
|
3650
3650
|
### `crossfade`
|
|
3651
3651
|
|
|
3652
|
-
Alpha cross-dissolve. `from` fades to `alpha: 0`, `to` starts at `alpha: 0` and fades to `1`, both over `[at, at + duration]`.
|
|
3652
|
+
Alpha cross-dissolve. `from` fades to `alpha: 0`, `to` starts at `alpha: 0` and fades to `1`, both over `[at, at + duration]`. **It is not a linear mix of the two pictures:** `to` is laid over a fading `from`, so over black the middle is darker (a red `from` and a blue `to` at the half way are (64, 0, 127), not (128, 0, 127)), and a composition's `alpha` is multiplied into each of its children (not one group opacity as in After Effects): the composition's own background shows through its top layers while it fades. Put what must stay solid on its own layer outside the two scenes. Pinned by `tests/films/transition-under-matte`.
|
|
3653
3653
|
|
|
3654
3654
|
```ts
|
|
3655
3655
|
{ kind: 'crossfade', from: 'A', to: 'B', at: 4, duration: 1, ease: 'sine.inOut' }
|
|
@@ -3659,7 +3659,7 @@ If `to` already has an explicit `initial.alpha` (other than 0), the expander thr
|
|
|
3659
3659
|
|
|
3660
3660
|
### `wipe`
|
|
3661
3661
|
|
|
3662
|
-
A directional reveal. `to` is masked by a soft edge that travels across the screen.
|
|
3662
|
+
A directional reveal. `to` is masked by a soft edge that travels across the screen: at progress 0 nothing of `to` shows, at 1 nothing of `from`, and the edge travels the whole width as the progress goes from 0 to 1; `from` is cut away hard where `to` is fully revealed.
|
|
3663
3663
|
|
|
3664
3664
|
```ts
|
|
3665
3665
|
{
|
|
@@ -3728,7 +3728,7 @@ A scaled punch-in / punch-out. By default `B` opens up: it starts large and zoom
|
|
|
3728
3728
|
|
|
3729
3729
|
### `dissolve`
|
|
3730
3730
|
|
|
3731
|
-
A noise reveal driven by deterministic 2D Perlin noise. Pixels with a low noise value reveal first; as `uProgress` advances, more pixels reveal. At the default `scale` of 30 it reads as large blotches (about 40 px each), not as grain: `scale: 200` or more gives a fine grain. The same `seed` always produces the same dissolve pattern, so a render is bit-exact reproducible.
|
|
3731
|
+
A noise reveal driven by deterministic 2D Perlin noise (`from` loses exactly the pixels `to` reveals: the same noise, a hard cutoff, like wipe and iris). Pixels with a low noise value reveal first; as `uProgress` advances, more pixels reveal. At the default `scale` of 30 it reads as large blotches (about 40 px each), not as grain: `scale: 200` or more gives a fine grain. The same `seed` always produces the same dissolve pattern, so a render is bit-exact reproducible.
|
|
3732
3732
|
|
|
3733
3733
|
```ts
|
|
3734
3734
|
{
|
|
@@ -4249,7 +4249,7 @@ Seeks to a specific frame. Pauses if currently playing? **No** — does not chan
|
|
|
4249
4249
|
|
|
4250
4250
|
### `movie.playUntil(seconds?): Promise<boolean>`
|
|
4251
4251
|
|
|
4252
|
-
Play, and pause exactly at `seconds` (the frame `round(seconds × frameRate)`). It resolves `true` when the playhead has landed there; `false` when something else ended it first (a `pause()`, a seek, a newer `playUntil()`) or when there is nothing to play toward (a time at or before the playhead: it also pauses the movie where it is; a movie that is not ready). A time at or past the end lands on the last frame; with no time at all it plays to the end, says `ended`, and resolves `true`. An ordinary `play()` is not affected. It is the one move a presentation (`pixi-effects/present`) is built on; it is also the way to "play the intro, then wait": `await movie.playUntil(4.5)`.
|
|
4252
|
+
Play, and pause exactly at `seconds` (the frame `round(seconds × frameRate)`). **A frame drawn while playing is the picture `snapshot(frame)` gives** (within one renderer, pixel for pixel: pinned by `tests/films/determinism-playback-vs-seek`); which frames a playing movie draws is not promised (it may draw the start frame again). It resolves `true` when the playhead has landed there; `false` when something else ended it first (a `pause()`, a seek, a newer `playUntil()`) or when there is nothing to play toward (a time at or before the playhead: it also pauses the movie where it is; a movie that is not ready). A time at or past the end lands on the last frame; with no time at all it plays to the end, says `ended`, and resolves `true`. An ordinary `play()` is not affected. It is the one move a presentation (`pixi-effects/present`) is built on; it is also the way to "play the intro, then wait": `await movie.playUntil(4.5)`.
|
|
4253
4253
|
|
|
4254
4254
|
### `movie.peek(fn): Promise<T>`
|
|
4255
4255
|
|
|
@@ -4323,7 +4323,7 @@ splitText('Hello world', style, { by: 'words', x: 640, y: 300, align: 'center' }
|
|
|
4323
4323
|
|
|
4324
4324
|
### `movie.inspect(frame?, options?): Promise<InspectReport>`
|
|
4325
4325
|
|
|
4326
|
-
Where every layer is drawn at `frame`, as JSON — for checking layout without eyes. Seeks there and stays there. `options.layers`: `'visible'` (default, only layers drawn at this frame), `'all'`, or `'none'` (just `summary` and `issues`). Faint layers (alpha < 0.3) and text
|
|
4326
|
+
Where every layer is drawn at `frame`, as JSON — for checking layout without eyes. Seeks there and stays there. `options.layers`: `'visible'` (default, only layers drawn at this frame), `'all'`, or `'none'` (just `summary` and `issues`). Faint layers (alpha < 0.3) are not reported, and neither is text WHILE a geometry keyframe (x, y, scale, rotation …) runs on it: once it has settled it is (a title that rises is reported when it stops). `check` looks at the first and last frame, scene edges, and one frame every 0.25 s, so a fault that lasts less than that between samples can go unseen; the layout issues are `text layer "X" is cut off by the canvas edge: …` and `text layer "X" is entirely outside the canvas (…)`.
|
|
4327
4327
|
|
|
4328
4328
|
```ts
|
|
4329
4329
|
interface InspectReport {
|
|
@@ -4461,6 +4461,21 @@ Returns a `Blob` whose `type` is the container's MIME (e.g. `video/mp4`). Emits
|
|
|
4461
4461
|
|
|
4462
4462
|
The renderer also forces a keyframe every ~2 seconds so the resulting file scrubs efficiently in standard players.
|
|
4463
4463
|
|
|
4464
|
+
#### Where the encoder comes from: `useMediabunny(source)`
|
|
4465
|
+
|
|
4466
|
+
Writing a file (and decoding a `video` layer) is done by [mediabunny](https://mediabunny.dev/), about 180 KB gzipped. It is **fetched the first time something needs it**: the first `render()` (or the download button), or a movie with a `video` asset at `init`. A page that only plays never fetches it and needs no import map entry for it, so a published movie can leave the `"mediabunny"` line out.
|
|
4467
|
+
|
|
4468
|
+
With nothing said, the bare name `'mediabunny'` is imported, which a bundler (it becomes a chunk of its own) or the page's import map resolves. `useMediabunny()` says where it comes from instead; call it before the first export or `init` with a video:
|
|
4469
|
+
|
|
4470
|
+
```ts
|
|
4471
|
+
import { useMediabunny } from 'pixi-effects';
|
|
4472
|
+
useMediabunny('https://cdn.jsdelivr.net/npm/mediabunny@1.61.3/+esm'); // a URL to import(): a page with no import map entry for it
|
|
4473
|
+
useMediabunny(() => import('mediabunny')); // a function that returns the module: your own copy, a chunk
|
|
4474
|
+
import * as mb from 'mediabunny'; useMediabunny(mb); // the module itself: ready, nothing is fetched later
|
|
4475
|
+
```
|
|
4476
|
+
|
|
4477
|
+
When it cannot be loaded, `render()` (or `init` with a video) rejects with `could not load mediabunny from …`, saying what to add; the next call tries again.
|
|
4478
|
+
|
|
4464
4479
|
### Poster time: `movie.init({ poster })`, `movie.poster`, `movie.posterImage()`
|
|
4465
4480
|
|
|
4466
4481
|
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:
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@ name: pixi-effects
|
|
|
3
3
|
description: Use when asked to make, animate, preview or export a video, motion graphics, an animated title, a lower third, kinetic type, a promo, a slideshow with transitions, an animated chart or data story, a social clip, a 2.5D / depth scene, or music and sound for a video, written as code in JavaScript or HTML. pixi-effects is a JS library where a video is a plain-object composition (text, shape, image, video, audio and file-free sfx / music, camera, light and shader layers with keyframes) that plays in the browser, is checked by a tool the agent can run, and exports MP4 / WebM / MOV with no server. Not for a project that already uses another video tool (Remotion, Motion Canvas, ffmpeg); follow that tool instead.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
> Written for pixi-effects 0.
|
|
6
|
+
> Written for pixi-effects 0.48.0. Update this skill with `npx skills update`; the newest docs are at https://yjmtmtk.github.io/pixi-effects/
|
|
7
7
|
|
|
8
8
|
# pixi-effects — write a video as data
|
|
9
9
|
|
|
@@ -94,7 +94,7 @@ A good habit that catches most of these: after `init`, read `window.__logs` (see
|
|
|
94
94
|
64. **`animateText()`: `out: { preset, at: T }` starts the exit at composition second T; without `at` the exit is the end of the layer run backwards.** `duration` is still how long until the layer ends (every piece is gone by `at + duration`): with `out.at`, give `duration` room for the exit and the wave after T (a warning says `out.at ≤ …` when it does not fit, or when T is before the entrance has finished). Without `out.at`, to be gone at time T set `duration: T − at` (title at 0.4 s that must be gone by 8.0 s: `duration: 7.6`). A hand-written exit keyframe works the other way: a negative `at` counts back from the end of its own layer, a positive one from the start of its own layer, never from the movie.
|
|
95
95
|
65. **A spring that does not spring, and `'auto'` that is not a spring.** `spring(1,170,26)` is critically damped (damping ratio 1): it never overshoots; at stiffness 170 use damping below 20 (`spring(1, 170, 12)`), or pick a preset (`spring.bouncy`, `spring.wobbly`). With a number `duration`, that number is the settle time (mass and stiffness only matter with `duration: 'auto'`). `duration: 'auto'` needs a spring ease (otherwise it warns and uses 0.5 s) and is for keyframes, not for `animateText` / `orbit` tweens. A colour that overshoots is clamped in its colour space; `alpha` past 1 shows nothing.
|
|
96
96
|
66. **Animating a gradient: one `fillGradient` object, partial, in the keyframe.** `to: { fillGradient: { angle: 360, stops: [...] } }` (keys you leave out stay). Not `'fillGradient.angle'`, not `gradientAngle`, not `fillGradient: 'red'` (each warns once with what to write). The layer must start with a `fillGradient`; `stops` keep their count; `type` cannot change. A fade between two colours in the default `rgb` passes through grey (green → magenta): set `colorSpace: 'oklch'`. Under a mask the stops still span the whole shape (see 39).
|
|
97
|
-
67. **A key the layer does not have is warned about now (it used to be ignored in silence).** `{ type: 'text', anchorX: 0.5 }` (anchors go in `initial`), `{ type: 'audio', sfx: 'click', pitch: 3 }` (the knob goes inside: `sfx: { preset: 'click', pitch: 3 }`), `{ type: 'shape', shape: 'rect', radius: 5 }` (that is a circle's key; a rect has `cornerRadius`), a mistyped property in a keyframe (`alhpa`), a mistyped text `style` key. The warning names the
|
|
97
|
+
67. **A key the layer does not have is warned about now (it used to be ignored in silence).** `{ type: 'text', anchorX: 0.5 }` (anchors go in `initial`), `{ type: 'audio', sfx: 'click', pitch: 3 }` (the knob goes inside: `sfx: { preset: 'click', pitch: 3 }`), `{ type: 'shape', shape: 'rect', radius: 5 }` (that is a circle's key; a rect has `cornerRadius`), a mistyped property in a keyframe (`alhpa`), a mistyped text `style` key. The warning names the layer, the key and what was probably meant (or what to write: an sfx knob says `sfx: { preset: 'click', pitch: 3 }`); fix the key, do not silence the warning. Three gallery pieces had the `pitch` mistake for months: their sound effects were never pitched.
|
|
98
98
|
68. **A pitched sfx keeps its own length; a radial gradient is placed in the shape's bounds.** `sfx: { preset: 'chime', pitch: -5 }` lasts as long as the sound does, so near the end of the movie give the layer a `duration` that ends with it (`inspectAudio()` reports a sound cut off by the end). A radial `fillGradient`'s `center` / `radius` are fractions of the SHAPE, not the canvas: for a glow on a big circle, convert canvas pixels to fractions of the circle. `music.reverb` is 0–0.6.
|
|
99
99
|
69. **Inside a remapped composition, `at` is in THAT composition's seconds.** With `speed: 0.5` a 4 s composition shows only 2 s of its content; a layer at `at: 3` inside is never seen (warns: "its clock only reaches 0–2s"). With `speed: 2` the content is 8 s long, so children default to 8 s and `at: 5` is fine. The composition's own `at`, `duration`, x / alpha keyframes are outer time. To freeze, hold `time` at one value (`to` the same number); to rewind, `to: { time: 0 }`. A sound inside goes silent while the clock stands still, and an `sfx` longer than the clock's reach is reported as cut off by the clock: give it a shorter `duration` (the sfx limit is 10 s) or let the clock run on. A child whose life ends exactly where a held `time` stands is hidden during the hold: give it a `duration` that reaches past (the content is as long as the larger of `duration` and the largest `time`). **With `time` keyframes the composition's own `duration` is its life in OUTER time and must reach the last keyframe** (`keyframes[1] starts at 2.3s, after the layer ends` means `duration` is too short): `at` of a `time` keyframe counts from the composition's start, the content length is a separate thing that comes out of the `time` values.
|
|
100
100
|
70. **`speed` is a setting, `time` is the property.** `speed: 2` on the layer; to change the speed over time animate `time` (a curve of seconds in the file), never `keyframes: [{ to: { speed: 3 } }]` (warns). `speed: 0` is not allowed (hold a moment with `time` to the same value twice); `speed` together with keyframed `time`: `time` wins. Other names (`playbackRate`, `timeScale`, `rate`, `reverse: true`, `timeRemap`) are warned about with what to write. `particles({ speed })` is something else: launch speed in px per second.
|
|
@@ -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.48.0/dist/loader.css" /> <!-- the loader: HTML + CSS only, so it is on screen at once and keeps moving while the page builds the video -->
|
|
8
8
|
<style>
|
|
9
9
|
body { background: #0d1220; margin: 0; padding: 16px; }
|
|
10
10
|
.stage { position: relative; width: min(960px, 100%); } /* the player and the loader both sit inside this box */
|
|
@@ -37,17 +37,17 @@
|
|
|
37
37
|
"imports": {
|
|
38
38
|
"pixi.js": "https://esm.sh/pixi.js@8.22.0?bundle-deps",
|
|
39
39
|
"mediabunny": "https://esm.sh/mediabunny",
|
|
40
|
-
"pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
41
|
-
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
42
|
-
"pixi-effects/present": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
43
|
-
"pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
40
|
+
"pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.48.0/dist/index.js",
|
|
41
|
+
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.48.0/dist/Controller.js",
|
|
42
|
+
"pixi-effects/present": "https://cdn.jsdelivr.net/npm/pixi-effects@0.48.0/dist/present.js",
|
|
43
|
+
"pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.48.0/dist/three.js",
|
|
44
44
|
"pixi-filters": "https://esm.sh/pixi-filters@6.1.5?external=pixi.js",
|
|
45
45
|
"three": "https://esm.sh/three@0.178.0",
|
|
46
46
|
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.178.0/examples/jsm/"
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
</script>
|
|
50
|
-
<link rel="modulepreload" href="https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
50
|
+
<link rel="modulepreload" href="https://cdn.jsdelivr.net/npm/pixi-effects@0.48.0/dist/index.js" /> <!-- after the import map: the library downloads while the page is parsed -->
|
|
51
51
|
|
|
52
52
|
<script type="module">
|
|
53
53
|
import { Movie, kenBurns, withFade } from 'pixi-effects';
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
export { CompositionSequence } from './chunk-RTZ5MW3E.js';
|
|
2
|
-
import './chunk-34YNRCMT.js';
|
|
3
|
-
import './chunk-JBDBICS2.js';
|
|
4
|
-
import './chunk-BFRWSV3R.js';
|
|
5
|
-
import './chunk-WGRWFQOB.js';
|
|
6
|
-
//# sourceMappingURL=Composition-HTHQ2RLF.js.map
|
|
7
|
-
//# sourceMappingURL=Composition-HTHQ2RLF.js.map
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
var chunkDS2EWRV4_cjs = require('./chunk-DS2EWRV4.cjs');
|
|
4
|
-
require('./chunk-CJS2S5XU.cjs');
|
|
5
|
-
require('./chunk-WYR2AU4W.cjs');
|
|
6
|
-
require('./chunk-NGJXVJB7.cjs');
|
|
7
|
-
require('./chunk-DOLTCXCL.cjs');
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
Object.defineProperty(exports, "CompositionSequence", {
|
|
12
|
-
enumerable: true,
|
|
13
|
-
get: function () { return chunkDS2EWRV4_cjs.CompositionSequence; }
|
|
14
|
-
});
|
|
15
|
-
//# sourceMappingURL=Composition-ZYRDRLYB.cjs.map
|
|
16
|
-
//# sourceMappingURL=Composition-ZYRDRLYB.cjs.map
|