pixi-effects 0.30.1 → 0.32.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 +33 -0
- package/README.md +6 -5
- package/ai/chat-template.html +3 -3
- package/ai/tools/check.mjs +42 -2
- package/ai/tools/render.mjs +34 -4
- package/ai/tools/varArgs.mjs +75 -0
- package/dist/{Base-Ne74ztNl.d.ts → Base-CXsMdmJB.d.ts} +1 -1
- package/dist/{Base-CB1_FPZD.d.cts → Base-pNq6sVGl.d.cts} +1 -1
- package/dist/Composition-LNMKWXSL.cjs +16 -0
- package/dist/{Composition-MUKR3FYD.cjs.map → Composition-LNMKWXSL.cjs.map} +1 -1
- package/dist/Composition-RF7QOEXT.js +7 -0
- package/dist/{Composition-RJGA7KDU.js.map → Composition-RF7QOEXT.js.map} +1 -1
- package/dist/Controller.cjs +42 -1
- package/dist/Controller.cjs.map +1 -1
- package/dist/Controller.d.cts +10 -2
- package/dist/Controller.d.ts +10 -2
- package/dist/Controller.js +42 -1
- package/dist/Controller.js.map +1 -1
- package/dist/{Movie-DdnpLpw1.d.cts → Movie-CL6SsAWe.d.cts} +90 -3
- package/dist/{Movie-C2rZFkrO.d.ts → Movie-f0tQG9y5.d.ts} +90 -3
- package/dist/Spans-NQGKVMR5.cjs +55 -0
- package/dist/Spans-NQGKVMR5.cjs.map +1 -0
- package/dist/Spans-SDXABOV6.js +53 -0
- package/dist/Spans-SDXABOV6.js.map +1 -0
- package/dist/{blendModes-RIVZXL5A.js → blendModes-MVRLG3T7.js} +4 -4
- package/dist/{blendModes-RIVZXL5A.js.map → blendModes-MVRLG3T7.js.map} +1 -1
- package/dist/{blendModes-ETMYWATF.cjs → blendModes-VTKDXWEN.cjs} +5 -5
- package/dist/{blendModes-ETMYWATF.cjs.map → blendModes-VTKDXWEN.cjs.map} +1 -1
- package/dist/{chunk-ZAXYPXKW.js → chunk-AW5NPDE4.js} +1367 -1150
- package/dist/chunk-AW5NPDE4.js.map +1 -0
- package/dist/{chunk-RCVZX2JP.js → chunk-C5BNHVKK.js} +36 -3
- package/dist/chunk-C5BNHVKK.js.map +1 -0
- package/dist/{chunk-WHNADXXG.cjs → chunk-CRUDOKSD.cjs} +91 -91
- package/dist/chunk-CRUDOKSD.cjs.map +1 -0
- package/dist/{chunk-A3Q3SDSH.js → chunk-D5BFQVKL.js} +6 -6
- package/dist/chunk-D5BFQVKL.js.map +1 -0
- package/dist/{chunk-LYOKH7KE.cjs → chunk-FCGYJCVO.cjs} +36 -3
- package/dist/chunk-FCGYJCVO.cjs.map +1 -0
- package/dist/{chunk-CRQFDY7E.js → chunk-FCMOREIS.js} +467 -58
- package/dist/chunk-FCMOREIS.js.map +1 -0
- package/dist/{chunk-HREBQAY4.cjs → chunk-K3MAEKFM.cjs} +493 -84
- package/dist/chunk-K3MAEKFM.cjs.map +1 -0
- package/dist/{chunk-4KB3GUEK.js → chunk-QFNS6BFL.js} +4 -4
- package/dist/{chunk-4KB3GUEK.js.map → chunk-QFNS6BFL.js.map} +1 -1
- package/dist/{chunk-ZVUO325O.cjs → chunk-QHDN6TF5.cjs} +1382 -1160
- package/dist/chunk-QHDN6TF5.cjs.map +1 -0
- package/dist/{chunk-2DXPTKQZ.cjs → chunk-Z437UWI4.cjs} +5 -5
- package/dist/{chunk-2DXPTKQZ.cjs.map → chunk-Z437UWI4.cjs.map} +1 -1
- package/dist/index.cjs +33 -33
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +5 -5
- package/dist/loader.css +1 -1
- package/dist/present.cjs +5 -5
- package/dist/present.d.cts +2 -2
- package/dist/present.d.ts +2 -2
- package/dist/present.js +5 -5
- package/dist/three.cjs +6 -6
- package/dist/three.d.cts +2 -2
- package/dist/three.d.ts +2 -2
- package/dist/three.js +2 -2
- package/dist/{types-BzIB65Vk.d.ts → types-CavnUvd5.d.cts} +28 -1
- package/dist/{types-BzIB65Vk.d.cts → types-CavnUvd5.d.ts} +28 -1
- package/docs/api.md +28 -1
- package/docs/dsl.md +46 -5
- package/llms-full.txt +136 -10
- package/package.json +83 -20
- package/skills/pixi-effects/SKILL.md +1 -1
- package/skills/pixi-effects/reference/cheatsheet.md +5 -2
- package/skills/pixi-effects/reference/pitfalls.md +5 -1
- package/skills/pixi-effects/reference/recipes.md +52 -0
- package/skills/pixi-effects/template.html +6 -6
- package/dist/Composition-MUKR3FYD.cjs +0 -16
- package/dist/Composition-RJGA7KDU.js +0 -7
- package/dist/chunk-A3Q3SDSH.js.map +0 -1
- package/dist/chunk-CRQFDY7E.js.map +0 -1
- package/dist/chunk-HREBQAY4.cjs.map +0 -1
- package/dist/chunk-LYOKH7KE.cjs.map +0 -1
- package/dist/chunk-RCVZX2JP.js.map +0 -1
- package/dist/chunk-WHNADXXG.cjs.map +0 -1
- package/dist/chunk-ZAXYPXKW.js.map +0 -1
- package/dist/chunk-ZVUO325O.cjs.map +0 -1
package/docs/api.md
CHANGED
|
@@ -42,6 +42,8 @@ interface MovieOptions {
|
|
|
42
42
|
canvas?: HTMLCanvasElement; // existing canvas to render into; otherwise PixiJS creates one
|
|
43
43
|
assets?: AssetSpec[]; // [{ name, src }]
|
|
44
44
|
composition?: CompositionSpec; // root composition (see DSL reference)
|
|
45
|
+
vars?: Record<string, number | string>; // named values the composition reads: a number as a name in an expression, any as {name} in a string (see Variables)
|
|
46
|
+
loop?: boolean; // start with movie.loop on: playing goes back to the start at the end (default false, like a <video>)
|
|
45
47
|
poster?: number; // seconds: the frame shown before play (negative: from the end), see movie.poster
|
|
46
48
|
motionBlur?: boolean | number | { samples?: number; shutter?: number }; // blur for render() / snapshot() / contactSheet(), not live playback; see Motion blur
|
|
47
49
|
loader?: HTMLElement | string | false; // the page's loader (see pixi-effects/loader.css): shows how far init is (a bar, "BUILDING LAYERS 62%"), faded out when ready, stopped if init fails; default: a .pe-loader next to the canvas
|
|
@@ -78,6 +80,25 @@ Play, and pause exactly at `seconds` (the frame `round(seconds × frameRate)`).
|
|
|
78
80
|
|
|
79
81
|
Look around and put the movie back: pauses, runs `fn` (seek, take pictures with `snapshot()`; the seeks made inside are not reported as `seeking` / `seeked`), then puts the playhead (or the poster) back where it was, whatever `fn` did, and returns what `fn` returned. `stopImages()` and `inspectStops()` (`pixi-effects/present`) are made with it.
|
|
80
82
|
|
|
83
|
+
### Movie variables and rebuilding: `vars`, `setVars()`, `resize()`, `rebuild()`
|
|
84
|
+
|
|
85
|
+
Variables are declared in `init({ vars: { title: 'NORD', shift: 0 } })` and read by the composition (see [Variables](dsl.md#variables)). Changing one builds the composition again: the new one is built off the stage first and swapped in when it is whole, at the same frame; a failure leaves the old one in place.
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
movie.vars // the current values (a copy)
|
|
89
|
+
await movie.setVars({ shift: 40 }); // the declared names only (others: ignored with a did-you-mean); the same values: nothing happens
|
|
90
|
+
await movie.resize(1080, 1920); // the movie's size; W / H / GW / GH follow. Beats a width / height the composition names
|
|
91
|
+
await movie.rebuild({ composition, vars, width, height }); // the general form: any of the four; `composition` replaces the whole spec (your layout function, called again)
|
|
92
|
+
// all three resolve to a report, and fire 'rebuilt' with it:
|
|
93
|
+
interface RebuildReport { reason: 'vars' | 'resize' | 'rebuild'; ms: number; audio: 'reused' | 'mixed' | 'none'; frame: number; playing: boolean; unchanged?: true }
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
- The new composition is built while the old one goes on; at the swap the movie keeps doing what it is doing at that moment: the same frame, playing or paused (a `pause()`, a seek or a `play()` you made during the build is kept), the poster if it was shown. A playing movie whose sound is unchanged (`audio: 'reused'`) is not stopped at all: the frame clock and the sound go on and only the picture is swapped, so rebuilds in a row (a pointer) do not hold the playhead back. When the sound changed, the movie stops and plays on from the same frame with the new sound. A `playUntil()` in flight ends. The canvas changes size at the swap, not before. A `render()` or `peek()` in flight is not interrupted: the swap waits for it.
|
|
97
|
+
- The sound is mixed only when it changed: `audio: 'reused'` means the same mix as before (the mix is the slow part, 0.6 to 0.8 s for a film; the rest takes about 20 ms). A time remap (`warp` / `sourceMap`) turns the cache off.
|
|
98
|
+
- Calls that arrive during a rebuild are joined: at most one rebuild runs and one waits, with the latest values. Safe to call from `pointermove` or `scroll`: a rebuild that leaves the sound alone takes tens of milliseconds (11 to 42 ms on four gallery pieces), one that mixes the sound again 0.6 to 0.8 s, so a slider that changes the sound will lag behind the hand.
|
|
99
|
+
- A rebuild that fails (a bad expression, a missing asset, a size that is not pixels) rejects, emits `error` with `where: 'rebuild'`, and the movie goes on with what it had. A `destroy()` during a rebuild ends it the same way.
|
|
100
|
+
- To follow a window: observe a CSS-sized box around the canvas, not the canvas itself (the movie sets the canvas's pixel size): `new ResizeObserver(() => movie.resize(Math.round(box.clientWidth * devicePixelRatio), Math.round(box.clientHeight * devicePixelRatio))).observe(box)` (the `liquid-layout` recipe).
|
|
101
|
+
|
|
81
102
|
### `movie.snapshot(frame?, options?): Promise<Blob | string>`
|
|
82
103
|
|
|
83
104
|
```ts
|
|
@@ -261,7 +282,9 @@ movie.off(event, fn): this
|
|
|
261
282
|
| `'seeking'` / `'seeked'` | `{ frame, totalFrames }` (the frame it lands on) | a `gotoFrame()` to another frame starts / has finished. NOT emitted for playback ticks, `render()`, `snapshot()` or `contactSheet()` |
|
|
262
283
|
| `'ended'` | none | playback ran off the end (after `'pause'`); pausing by hand does not emit it |
|
|
263
284
|
| `'volumechange'` | `{ volume: number; muted: boolean }` | `volume` or `muted` really changed (clamping counts) |
|
|
264
|
-
| `'
|
|
285
|
+
| `'loopchange'` | `{ loop: boolean }` | `movie.loop` really changed (the first value from `init({ loop })` is not a change) |
|
|
286
|
+
| `'error'` | `{ where: 'init' \| 'render' \| 'playback' \| 'rebuild'; message: string; error: unknown }` | `init()` / `render()` / a rebuild failed (the call still rejects), or a frame failed during playback |
|
|
287
|
+
| `'rebuilt'` | `RebuildReport` (see above) | a `setVars()` / `resize()` / `rebuild()` built the composition again (not for a call that changed nothing) |
|
|
265
288
|
|
|
266
289
|
The names are the ones an HTML5 `<video>` uses, so a player written for `<video>` ports over.
|
|
267
290
|
|
|
@@ -280,8 +303,10 @@ The names are the ones an HTML5 `<video>` uses, so a player written for `<video>
|
|
|
280
303
|
| `background` | string | CSS color |
|
|
281
304
|
| `volume` | number | 0..1 getter/setter; immediate. Setter clamps and applies to active audio |
|
|
282
305
|
| `muted` | boolean | getter/setter; immediate |
|
|
306
|
+
| `loop` | boolean | getter/setter, default `false`. When true, an ordinary `play()` that reaches the end goes back to the start and plays on: no `ended`, no `pause` / `play` events at the wrap, the sound starts again (a short gap). `playUntil()` and `render()` ignore it. Say `loopchange` when it changes |
|
|
283
307
|
| `app` | `pixi.js Application \| null` | PIXI Application instance (advanced/escape hatch) |
|
|
284
308
|
| `timeline` | `Timeline \| null` | the movie's timeline (advanced): `time(t)`, `duration()`, `segments()` |
|
|
309
|
+
| `vars` | `Record<string, number \| string>` | the current variable values (a copy) |
|
|
285
310
|
| `stops` | `Stop[]` | the composition's stops: named times (see Presentations); empty for an ordinary movie |
|
|
286
311
|
|
|
287
312
|
### Presentations: `stops` and `pixi-effects/present`
|
|
@@ -455,6 +480,7 @@ interface ControllerOptions {
|
|
|
455
480
|
exportButton?: boolean; // default true; false hides ⬇ + popover
|
|
456
481
|
keyboard?: boolean; // default true
|
|
457
482
|
presentButton?: boolean; // default true: a movie with stops gets a Present button (marks on the seek bar are always shown)
|
|
483
|
+
loopButton?: boolean; // default true: a loop button next to the volume (and the L key) that flips movie.loop
|
|
458
484
|
pauseAtStops?: boolean; // default true: with stops, play / Space / click on the picture play to the next stop and pause; false plays straight through
|
|
459
485
|
clickToPlay?: boolean; // default true: a click / tap on the picture plays / pauses (like <video>); false leaves the canvas alone
|
|
460
486
|
className?: string; // default 'movie-controller'
|
|
@@ -523,6 +549,7 @@ Active when `keyboard: true` and the key target is not `<input>`/`<textarea>`/`<
|
|
|
523
549
|
| `←` / `→` | step ±1 frame |
|
|
524
550
|
| `↑` / `↓` | volume ±5% (clears mute when increasing past zero) |
|
|
525
551
|
| `M` | toggle mute |
|
|
552
|
+
| `L` | toggle loop (`movie.loop`) |
|
|
526
553
|
| `Shift+E` | export with current settings (skips the popover) |
|
|
527
554
|
| `F` | toggle fullscreen |
|
|
528
555
|
| `Esc` | close the export popover or exit fullscreen (browser) |
|
package/docs/dsl.md
CHANGED
|
@@ -59,7 +59,7 @@ interface SequenceCommon {
|
|
|
59
59
|
| Layer | Where `x, y` land by default | To change it |
|
|
60
60
|
|---|---|---|
|
|
61
61
|
| `shape` rect / circle / ellipse | the **centre** of the shape | `anchorX` / `anchorY` (0–1). A bar growing from its base: `anchorY: 1`; left to right: `anchorX: 0` |
|
|
62
|
-
| `shape` line / polygon / path | the centre of the shape's bounds | `pivotX` / `pivotY` (in the shape's own canvas coordinates): the point that sits at `x, y` and that rotation / scale turn about. **`anchorX` / `anchorY` do nothing on these.** |
|
|
62
|
+
| `shape` line / polygon / path | the centre of the shape's bounds | `pivotX` / `pivotY` (in the shape's own canvas coordinates): the point that sits at `x, y` and that rotation / scale turn about. **`anchorX` / `anchorY` do nothing on these.** A `path` whose `d` is already written in canvas coordinates takes no `x` / `y` at all (they would move its centre to that point); to move it, change the coordinates or wrap it in a composition. |
|
|
63
63
|
| `text`, `image`, `video` | the **top-left** corner | `anchorX` / `anchorY: 0.5` to centre |
|
|
64
64
|
| `composition` | its top-left corner | `pivotX` / `pivotY` = the point that sits at `x, y` and that rotation / scale turn about |
|
|
65
65
|
|
|
@@ -71,7 +71,7 @@ A layer is **not removed** when its lifespan `[at, at + duration)` ends: it is o
|
|
|
71
71
|
|
|
72
72
|
### `text`
|
|
73
73
|
|
|
74
|
-
Renders a [PIXI.Text](https://pixijs.com/8.x/guides/components/scene-objects/text/text). Animatable position, rotation, scale, opacity and `fill`. `style` accepts any PixiJS `TextStyle` field and is applied once at build time — notably `fontSize fontFamily fontWeight fill letterSpacing lineHeight align wordWrap wordWrapWidth stroke dropShadow padding` (numbers may be expressions). A `dropShadow` glow is clipped unless `padding` is at least twice its `blur`. Text content can change over time (see *Changing text* below): a `{value}` placeholder prints an animatable number (counters), a keyframe `set: { text: '…' }` swaps the string, and `visibleChars` types it out. Multi-line text is centre-aligned unless `style.align` says otherwise. In expressions `w` / `h` are the size of the styled text, so `x: '-w'` places it just off the left edge.
|
|
74
|
+
Renders a [PIXI.Text](https://pixijs.com/8.x/guides/components/scene-objects/text/text). Animatable position, rotation, scale, opacity and `fill`. `style` accepts any PixiJS `TextStyle` field and is applied once at build time (an outline `stroke: { color, width }` is **round-joined** unless you give `join`: PixiJS's own default, `miter`, grows spikes on the sharp corners of W, V, A, k; the stroke is centred on the glyph edge, so half its width covers the fill: keep `width` to about a tenth of `fontSize`, or put a second, stroked copy of the text under the plain one for a thick outline) — notably `fontSize fontFamily fontWeight fill letterSpacing lineHeight align wordWrap wordWrapWidth stroke dropShadow padding` (numbers may be expressions). A `dropShadow` glow is clipped unless `padding` is at least twice its `blur`. Text content can change over time (see *Changing text* below): a `{value}` placeholder prints an animatable number (counters), a keyframe `set: { text: '…' }` swaps the string, and `visibleChars` types it out. Multi-line text is centre-aligned unless `style.align` says otherwise. In expressions `w` / `h` are the size of the styled text, so `x: '-w'` places it just off the left edge.
|
|
75
75
|
|
|
76
76
|
```ts
|
|
77
77
|
{
|
|
@@ -129,6 +129,16 @@ A text layer has one animatable number, `value`, printed wherever the text conta
|
|
|
129
129
|
|
|
130
130
|
`format`: `decimals` (default 0), thousands `grouping` (default false) and `pad` (zero-pad the whole part to that many digits: `pad: 2` gives `05`, so a clock is `text: '18:42:{value}'` with `format: { pad: 2 }`). Put prefixes and suffixes in the text (`'${value}'`, `'{value}%'`). If `value` is animated but the text has no `{value}`, a warning says so.
|
|
131
131
|
|
|
132
|
+
**Colour inside one text layer: `spans`.** Instead of `text`, write an array of strings (drawn in the layer's own `style`) and `{ text, fill, … }` objects; they are shown one after the other, and `\n` inside a span starts a new line:
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
{ type: 'text', name: 'code', style: { fontFamily: 'Menlo', fontSize: 30, fill: '#e8f5e8', align: 'left' },
|
|
136
|
+
spans: [{ text: 'type', fill: '#7fa07f' }, ': ', { text: "'text'", fill: '#ffd166', fontWeight: '700' }, ',\n at: 0.5'],
|
|
137
|
+
initial: { x: 120, y: 200 } }
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
A span may change `fill`, `fontWeight`, `fontStyle`, `fontVariant`, `letterSpacing` and `dropShadow`; a size, a font, `lineHeight`, `align` or a `stroke` belongs to the layer's `style` (one outline per layer: a stroke shifts a piece by half its width; a span that sets one of these is warned about once and the key is ignored; `style.wordWrap` and `style.leading` do not apply either: the lines are the `\n` of the spans). `style.align` places the lines on the width of the block (`'left'`, `'center'` — the default, as for every text — or `'right'`). At build time the layer becomes a group (a composition) of ordinary text layers, one per piece of a line, named `<name>.0`, `<name>.1` …, each at the x it has when the pieces are measured one after the other (the same measuring as `splitText`), so `inspect`, the timeline view and `check` see them. The group carries the layer's `name`, `at`, `duration`, `initial` (`anchorX` / `anchorY` are numbers, 0.5 centres the whole block, and cannot be keyframed), `keyframes` (`x`, `y`, `alpha`, `scale`, `rotation`, …), `filters`, `mask` and `blendMode`. A **typewriter** works over all the spans: `initial: { visibleChars: 0 }` and a keyframe `to: { visibleChars: N }` count every character in order, the newline included. `fill`, `text`, `value`, `fillGradient`, `format` and `colorSpace` do not apply to a group (a warning says so): use a separate text layer, or put the colour in a span. Measuring needs the font loaded (`await document.fonts.load('30px "Name"')` for a web font), as for `splitText`; the size and spacing in `style` may be numbers, expressions (`'GH * 0.05'`) or declared variables: they are resolved before the pieces are measured, and a missing `fontSize` is measured as the text layer's own default (Arial, 36).
|
|
141
|
+
|
|
132
142
|
### `image`
|
|
133
143
|
|
|
134
144
|
Renders a [PIXI.Sprite](https://pixijs.com/8.x/guides/components/scene-objects/sprite/sprite) from a registered asset. Intrinsic `w`/`h` come from the loaded texture.
|
|
@@ -1084,6 +1094,8 @@ No comparison, conditional, bitwise, or string operators — keep it numeric (`s
|
|
|
1084
1094
|
|
|
1085
1095
|
### Scope variables
|
|
1086
1096
|
|
|
1097
|
+
(Your own names, set from outside, are [Variables](#variables) below.)
|
|
1098
|
+
|
|
1087
1099
|
Each sequence has its own scope, computed at build time:
|
|
1088
1100
|
|
|
1089
1101
|
| Name | Meaning |
|
|
@@ -1119,6 +1131,35 @@ style: { fontSize: 'min(GW, GH) * 0.06' }
|
|
|
1119
1131
|
initial: { x: 'GW/2', y: 'GH * 0.96', anchorX: 0.5, anchorY: 1 }
|
|
1120
1132
|
```
|
|
1121
1133
|
|
|
1134
|
+
### Variables
|
|
1135
|
+
|
|
1136
|
+
A **variable** is a name with a value you can change after the movie is built: the language of a title, a brand colour, an offset, the screen size. Declare them once in `movie.init`; the composition reads them by name.
|
|
1137
|
+
|
|
1138
|
+
```ts
|
|
1139
|
+
await movie.init({
|
|
1140
|
+
vars: { title: 'NORD', accent: '#e5399a', shift: 0 }, // a number or a string; no booleans (write 0 / 1)
|
|
1141
|
+
composition: { sequences: [
|
|
1142
|
+
{ type: 'shape', shape: 'rect', width: 'GW', height: 'GH', initial: { x: 'GW/2', y: 'GH/2', fillColor: '{accent}' } },
|
|
1143
|
+
{ type: 'text', text: '{title}', initial: { x: 'W/2 + shift', y: 'H/2' } },
|
|
1144
|
+
] },
|
|
1145
|
+
});
|
|
1146
|
+
await movie.setVars({ title: 'SÜD', shift: 40 }); // builds the composition again, at the same frame
|
|
1147
|
+
```
|
|
1148
|
+
|
|
1149
|
+
| Where | How it reads a variable |
|
|
1150
|
+
| --- | --- |
|
|
1151
|
+
| a numeric expression (`x`, `scale`, `width`, `fontSize` … any value that takes an [expression](#expressions)) | the **name**, like `W` or `GW`: `'W/2 + shift'`. Only number variables. |
|
|
1152
|
+
| any string anywhere in the composition (`text`, a colour, `asset`, a layer `name`, nested layers) | **`{name}`**: `'{title}'`, `'#{hex}'`, `'{lang}-logo'`. The text is replaced before the layer is built. A `{name}` that was not declared stays as written (`{value}` is the [counter](#counters-value) of a text layer, so it is also a name no variable can take). `$` is not used: it is a price (`'$5'`). |
|
|
1153
|
+
|
|
1154
|
+
Rules, all checked when the movie is built (each warning says what to change):
|
|
1155
|
+
- A name is letters, digits and `_`, not starting with a digit. These are **reserved** and are refused: `w h W H GW GH t d T contain cover value`, every expression function (`min max abs … step rand noise lerp clamp smoothstep mod`) and `PI`.
|
|
1156
|
+
- A variable that nothing reads (no `{name}` in a string, no use in an expression) is said: it is usually a typo on one side. A `{name}` that was not declared says the declared name it looks like.
|
|
1157
|
+
- Values are read **when the composition is built**, once (not every frame). `at` and `duration` are plain numbers, so a variable moves values, not timing; to change the structure in time, call `movie.rebuild({ composition })` with a composition made by your own function.
|
|
1158
|
+
- `setVars` of a name that was not declared is ignored with a did-you-mean; `setVars` of the values it already has does nothing.
|
|
1159
|
+
- Everything is built again, the playhead stays at the same frame, a playing movie keeps playing, and the sound is mixed again **only if** the sound changed. See [`movie.rebuild`](api.md#movie-variables-and-rebuilding-vars-setvars-resize-rebuild).
|
|
1160
|
+
- A video file is a function of the script **and** the variables: `pixi-effects render page.html --var title=SÜD --var shift=40`, `check --size 1920x1080,1080x1920` (a contact sheet per size).
|
|
1161
|
+
- Layout that should follow the screen size uses `W` / `H` / `GW` / `GH`, not pixels: after `movie.resize(w, h)` they are the new size.
|
|
1162
|
+
|
|
1122
1163
|
---
|
|
1123
1164
|
|
|
1124
1165
|
## Stops (presentations)
|
|
@@ -1234,7 +1275,7 @@ A scaled punch-in / punch-out. By default `B` opens up: it starts large and zoom
|
|
|
1234
1275
|
|
|
1235
1276
|
### `dissolve`
|
|
1236
1277
|
|
|
1237
|
-
|
|
1278
|
+
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.
|
|
1238
1279
|
|
|
1239
1280
|
```ts
|
|
1240
1281
|
{
|
|
@@ -1344,7 +1385,7 @@ Gotchas, all measured:
|
|
|
1344
1385
|
- **Distortion:** `warp` (`wave`: a flag or the surface of water, `haze`: heat shimmer; moves with the frame's time, no keyframes needed, and pads the layer by `strength` so a shifted edge is not cut), `twist` (a swirl), `bulgePinch` (a lens), `shockwave` (a ring that spreads), `rgbSplit` (colour fringes), `glitch` (slices). `warp`'s time is the time of the frame (inside a time-remapped composition, that composition's clock), `speed: 0` stands still, `strength` is in px and is capped at 200. It bends the layer's picture; to draw something that does not exist yet, use a `shader` layer.
|
|
1345
1386
|
- **Centres and offsets are in canvas pixels, not the layer's:** `twist.offset`, `bulgePinch.center`, `zoomBlur.center`, `radialBlur.center`, `shockwave.center` default to the canvas's top-left corner, so a layer in the middle looks unaffected. Put the centre on the layer: `{ type: 'twist', offset: { x: 640, y: 360 } }` for a layer centred on a 1280×720 canvas.
|
|
1346
1387
|
- **A filter works inside the layer's bounds.** Glow, dropShadow, outline and blur draw outside it and are clipped unless you widen the area (`filterArea: { x: -24, y: -24, width: w + 48, height: h + 48 }`, in the layer's own coordinates). Do not widen it for `grayscale`, `oldFilm` or other filters that paint the whole area: they fill the extra margin with black.
|
|
1347
|
-
- **Animate scalars by name** (`'filters.halo.outerStrength'`). Options that are points (`rgbSplit.red`, `dropShadow.offset`)
|
|
1388
|
+
- **Animate scalars by name** (`'filters.halo.outerStrength'`). Options that are points (`rgbSplit.red`, `dropShadow.offset`) are animated one leaf at a time: `'filters.<name>.red.x'` / `'filters.<name>.red.y'` tween like any number (the whole point `red: { x, y }` as a keyframe value switches at its keyframe and does not tween; a path that leads nowhere is warned about once and ignored). `pixelate` animates through `sizeX` / `sizeY`, not `size`.
|
|
1348
1389
|
- `shockwave` and `godray` are driven by `time` (a still frame at `time: 0` looks like nothing): animate it. `radialBlur`, `shockwave` and `tiltShift` positions are canvas pixels too.
|
|
1349
1390
|
- Some filters take their value as one argument; the DSL hides that (`{ type: 'pixelate', size: 8 }`, `{ type: 'emboss', strength: 6 }`).
|
|
1350
1391
|
|
|
@@ -1375,7 +1416,7 @@ import { GlowFilter, OldFilmFilter } from 'pixi-filters';
|
|
|
1375
1416
|
Notes:
|
|
1376
1417
|
|
|
1377
1418
|
- `filter` must be a `Filter` instance (constructor must have run on the consumer side).
|
|
1378
|
-
-
|
|
1419
|
+
- A dotted key (`'filters.s.red.x'`) is a path into the filter: the leaf of the point is written in place, which `pixi-filters` reads when it draws (checked on `RGBSplitFilter` in a real browser, `tests/tools/pointOptions.test.ts`). Before 0.32 such a key was written as a property literally named `red.x` and nothing moved.
|
|
1379
1420
|
- Without a `name`, the filter still applies but cannot be addressed via `filters.<name>.<prop>` keyframe paths.
|
|
1380
1421
|
|
|
1381
1422
|
Notes:
|
package/llms-full.txt
CHANGED
|
@@ -6,7 +6,7 @@ Concatenation of the skill, cheatsheet, pitfalls, recipes and the DSL / API refe
|
|
|
6
6
|
|
|
7
7
|
<!-- ===== skills/pixi-effects/SKILL.md ===== -->
|
|
8
8
|
|
|
9
|
-
> Written for pixi-effects 0.
|
|
9
|
+
> Written for pixi-effects 0.32.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
|
|
|
@@ -138,6 +138,7 @@ Operators `+ - * /`, parentheses, unary `-`. Functions `min max abs floor ceil r
|
|
|
138
138
|
| `w`, `h` | this layer's own size (image/video natural size, text size **after** its style is applied, shape bounds) |
|
|
139
139
|
| `cover`, `contain` | scale that makes the layer cover / fit the parent (`scale: 'cover'`) |
|
|
140
140
|
| `t`, `d`, `T` | layer start time, layer duration, parent duration |
|
|
141
|
+
| your own names | `movie.init({ vars: { shift: 0, title: 'NORD' } })`: a number is a name in an expression (`'W/2 + shift'`), any value is `{title}` inside a string (`text: '{title}'`, `fillColor: '{accent}'`). Reserved: the names above, `value`, the functions, `PI`. `await movie.setVars({ shift: 40 })` builds again at the same frame; `await movie.resize(w, h)` makes `W` / `H` follow a window. `render --var title=SÜD --size 1080x1920`, `check --size 1920x1080,1080x1920` |
|
|
141
142
|
|
|
142
143
|
## Common fields (every layer)
|
|
143
144
|
|
|
@@ -158,7 +159,7 @@ Keyframe kinds: `set` (jump at `at`; undone when you seek back), `to` (animate t
|
|
|
158
159
|
|
|
159
160
|
## Layer types
|
|
160
161
|
|
|
161
|
-
**text** — `text`, `style` (any PixiJS TextStyle field: `fontSize fontFamily fontWeight fill letterSpacing lineHeight align wordWrap wordWrapWidth stroke dropShadow padding`; expressions OK for numbers), `colorSpace`. Default anchor is **top-left**; use `anchorX/anchorY: 0.5` to centre on `x,y`. A text layer has one animatable number, `value`, printed where the text contains `{value}` (`text: '{value} users'`, `initial: { value: 0 }`, keyframe `to: { value: 2480 }`; `format: { decimals, grouping, pad /* zero-pad the whole part: pad 2 → 05 */ }`) — that is how counters work. **Changing text:** a keyframe `set: { text: 'Two' }` swaps the string at that time (undone on seek back; `to` / `from` cannot tween a string); `visibleChars` (number, first N characters; starts at 0 once animated) is a typewriter: `to: { visibleChars: 30 }, duration: 2.4, ease: 'none'` — use `anchorX: 0` + `style.align: 'left'` so it grows rightward. Multi-line text is centre-aligned by default (`style.align: 'left'` to change).
|
|
162
|
+
**text** — `text`, `style` (any PixiJS TextStyle field: `fontSize fontFamily fontWeight fill letterSpacing lineHeight align wordWrap wordWrapWidth stroke dropShadow padding`; expressions OK for numbers), `colorSpace`. Default anchor is **top-left**; use `anchorX/anchorY: 0.5` to centre on `x,y`. A text layer has one animatable number, `value`, printed where the text contains `{value}` (`text: '{value} users'`, `initial: { value: 0 }`, keyframe `to: { value: 2480 }`; `format: { decimals, grouping, pad /* zero-pad the whole part: pad 2 → 05 */ }`) — that is how counters work. **Changing text:** a keyframe `set: { text: 'Two' }` swaps the string at that time (undone on seek back; `to` / `from` cannot tween a string); `visibleChars` (number, first N characters; starts at 0 once animated) is a typewriter: `to: { visibleChars: 30 }, duration: 2.4, ease: 'none'` — use `anchorX: 0` + `style.align: 'left'` so it grows rightward. Multi-line text is centre-aligned by default (`style.align: 'left'` to change). **Colour inside one line:** `spans: [{ text: 'type', fill: '#7fa07f' }, ': ', { text: "'a'", fill: '#ffd166', fontWeight: '700' }, ',\n next line']` instead of `text` (strings use the layer's `style`; a span changes only fill / fontWeight / fontStyle / fontVariant / letterSpacing / dropShadow, never a size, font or stroke (those are the layer's `style`); `\n` starts a line; `style.align: 'left'` for code; `visibleChars` types over all the spans; it becomes a group of text layers `<name>.0…`).
|
|
162
163
|
|
|
163
164
|
**image** — `asset`, `tint`, `colorSpace: 'rgb'|'oklab'|'oklch'`. Default anchor top-left; natural size = `w`,`h`.
|
|
164
165
|
|
|
@@ -263,7 +264,7 @@ measureText('Hello', style) // → { width, height,
|
|
|
263
264
|
splitText('Hello world', style, { by: 'chars' | 'words' | 'lines', x, y, align: 'left' | 'center' | 'right' })
|
|
264
265
|
// → [{ text, x, y, width, index, line }] — one text layer per piece, same style, anchor top-left
|
|
265
266
|
```
|
|
266
|
-
Per-letter / per-word / per-line animation in one call: `...animateText(text, style, { x, y, align, by: 'chars'|'words'|'lines', at, duration /* required: until the last piece is gone */, in: 'rise'|'drop'|'fade'|'pop'|'zoom'|'slide'|'spin'|{ from, to, duration, ease }|false, out: <same, in the same wave; by default BACKWARDS FROM THE LAYER'S END (at + duration, so to be gone at time T set duration = T − at); `out: { preset: 'fade', at: 6 }` starts the exit at composition second 6 instead>, stagger: { each, from, … }, idle: { y: 4, rotation: 1 }, styleFor: p => ({ fill }) })` → text layers `<name>-0…` (anchor centre; `x`/`y` in `from` are offsets) to spread into `sequences`. **`x` / `y` are pixel NUMBERS (an expression like `'GW/2'` throws: compute `W / 2`), and `y` is the TOP of the line box**: the visible text is centred `lineHeight / 2` below it, so an underline goes at `y + lineHeight`, not at `y` (recipe `animate-text`). By hand: `splitText(...)` + one layer per piece (recipe `split-text`). A web font must be loaded before measuring.
|
|
267
|
+
Per-letter / per-word / per-line animation in one call: `...animateText(text, style, { x, y, align, by: 'chars'|'words'|'lines', at, duration /* required: until the last piece is gone */, in: 'rise'|'drop'|'fade'|'pop'|'zoom'|'slide'|'spin'|{ from, to, duration, ease }|false, out: <same, in the same wave (leave `out` out and the pieces stay until the layer ends); by default BACKWARDS FROM THE LAYER'S END (at + duration, so to be gone at time T set duration = T − at); `out: { preset: 'fade', at: 6 }` starts the exit at composition second 6 instead>, stagger: { each, from, … }, idle: { y: 4, rotation: 1 }, styleFor: p => ({ fill }) })` → text layers `<name>-0…` (anchor centre; `x`/`y` in `from` are offsets) to spread into `sequences`. **`x` / `y` are pixel NUMBERS (an expression like `'GW/2'` throws: compute `W / 2`), and `y` is the TOP of the line box**: the visible text is centred `lineHeight / 2` below it, so an underline goes at `y + lineHeight`, not at `y` (recipe `animate-text`). By hand: `splitText(...)` + one layer per piece (recipe `split-text`). A web font must be loaded before measuring.
|
|
267
268
|
|
|
268
269
|
## Assets
|
|
269
270
|
|
|
@@ -278,6 +279,8 @@ movie.play(); movie.pause();
|
|
|
278
279
|
await movie.gotoFrame(n, true); // seek (frames; 30 fps => t = n/30)
|
|
279
280
|
const blob = await movie.render({ format: 'mp4' }); // 'mp4' | 'webm' | 'mov' | 'mkv'; ~real-time. `format: 'wav'` / `'ogg'` writes only the movie's SOUND (a `music` score as an audio file). `video: { codec: 'hevc' | 'av1' | 'vp9' | 'avc', bitrate: '8M', hardware, keyFrameInterval }` (a codec this browser cannot encode names the one that works). Part of it: `range: [2, 5]` (seconds) or `range: 'title'` (a top-level layer's span; picture and sound); `scale: 0.5` / `draft: true` (half size, low quality, no motion blur: a smaller file, not a faster drawing). Fast motion: `movie.init({ motionBlur: true })` (8 samples, 180° shutter; or a number / { samples: 2–64, shutter: 0–1 }) blurs render / snapshot / contactSheet, not live playback; a render takes `samples`× as long
|
|
280
281
|
movie.on('progress', e => e.progress /* 0–100 */);
|
|
282
|
+
await movie.setVars({ shift: 40 }); // declared variables → build again, same frame, sound mixed again only if it changed. `resize(w, h)`, `rebuild({ composition, vars, width, height })`; `movie.on('rebuilt', report => …)`; `movie.vars`
|
|
283
|
+
movie.loop = true // (or init({ loop: true })) playing goes back to the start at the end instead of stopping; the Controller bar has a loop button (L). playUntil() and render() ignore it
|
|
281
284
|
movie.audioBuffer // mixed audio (after init), if any audio layers
|
|
282
285
|
await movie.contactSheet({ count: 6, as: 'dataURL' }) // ONE image of several labelled frames — look at it. Options: frames | times | count, columns, cellWidth, as
|
|
283
286
|
await movie.snapshot(60, { as: 'dataURL' }) // one frame, canvas only (no player bar) — use it for detail; sheet tiles are small
|
|
@@ -418,7 +421,7 @@ A good habit that catches most of these: after `init`, read `window.__logs` (see
|
|
|
418
421
|
93. **A `threeD` layer's shadow budget is four casters per receiver, counted in layer order, not geometry.** Five letters as five `castsShadows` layers over one floor: the fifth casts nothing (the warning names it). Make the word ONE text layer (the light is per pixel, each letter is still lit on its own) or give only the four that matter `castsShadows`. Splitting the floor does not help: each half takes the first four.
|
|
419
422
|
94. **`resolution` below 1 softens crisp lines in a shader.** `fwidth` anti-aliased edges come out a little blurry after the upscale: keep `resolution: 1` for line art and use 0.5 only for soft fields (flowing colour, glow).
|
|
420
423
|
95. **A `color-dodge` leak over a dark picture is a muddy red disc.** Dodge divides each channel of near-black separately, red first. Cut the dodge to the bright subject with a matte (`mask: 'letters'`, a matte-only copy of the word) and put the warm haze in a separate `screen` radial.
|
|
421
|
-
96. **A filter option that is a point (`rgbSplit.red`, `glitch` offsets)
|
|
424
|
+
96. **A filter option that is a point (`rgbSplit.red`, `glitch` offsets) is tweened one leaf at a time** (`'filters.s.red.x'`; the whole point as a value switches, it does not tween), and a scalar too: `set: { 'filters.tear.seed': n }` steps `glitch.seed` (a `set` keyframe works on filter options; no warning, visible tearing). To switch between looks on one subject, put the subject in time-windowed `composition` copies, each carrying the filters of its look.
|
|
422
425
|
97. **A deck inside your own composition loses its `transitions` and `stops`** unless you take them across: `const d = deck({ width: 1920, height: 1080, pages })`, then `{ type: 'composition', sequences: d.composition.sequences, transitions: d.composition.transitions }`. Pass `width` / `height` yourself for a page that is not the movie's own size.
|
|
423
426
|
98. **A matte layer needs a fill.** A named matte (`mask: 'glass'`) that is a shape with no `fillColor` / `fillGradient` (or a text with no fill) cuts everything away and nothing warns: the layers it should show simply vanish. Give the matte a fill (white for `alpha`, a gradient for `luma`).
|
|
424
427
|
99. **In a `music` score a capital letter is a chord and a lowercase one a note.** `G7` is G dominant seventh, `C5` a power chord, `C6` a sixth chord; `g7`, `c5`, `c6` are notes (octave 7 is a very high note: it used to be what `G7` meant, silently). A bare note in capitals (`Bb2`) is refused with the lowercase spelling (`bb2`); inside brackets (`[C4 E4 G4]`) any case is a note. An unknown chord kind suggests the nearest (`Cmin7` → `Cm7`, `Csus` → `Csus4`, `CM7` → `Cmaj7`).
|
|
@@ -426,6 +429,10 @@ A good habit that catches most of these: after `init`, read `window.__logs` (see
|
|
|
426
429
|
101. **A film built from one adjective looks like every other one.** Read `reference/direction.md` and write `brief.md` / `style-guide.md` / `shotlist.md` first: real assets with paths, the one sentence to remember, a row per beat with entry and exit state. If a real asset is missing, ask: a drawn fake (a logo, a dashboard, a figure) is the failure that matters. Judge the contact sheet cold: three defects, each with a timestamp, evidence and a local fix.
|
|
427
430
|
102. **A vertical cut is a second composition, not a crop, and only when someone asked for it.** One source, a layout function, `?f=9x16`; `check` and `render` take `--query f=9x16` (recipe `two-formats`). Look at a contact sheet of each format: type that fits wide is tiny tall. Ask which formats are wanted; make one if the answer is open. In a 1080p film keep every line of text at 34 px or more; if you cannot read it in a sheet tile, a phone cannot.
|
|
428
431
|
103. **Load a web font BEFORE you measure with it.** `measureText` / `splitText` / `animateText` measure once, when the composition is built. Measured in Chrome: "Respect the Local, Loop the Value." at 80 px Lato 900 is 1487 px wide before the Google font has arrived (the fallback) and 1231 px after; Japanese text in a similar fallback was within 3 px. Every box, line break and centred position built from the wrong width stays wrong. `await document.fonts.load('900 80px "Lato"', theTextYouUse)` first (the text argument loads exactly the glyphs you need), then build the composition.
|
|
432
|
+
104. **A variable moves values, not timing, and it is read when the composition is built.** `at` and `duration` are plain numbers; `vars: { shift }` works in `x: 'W/2 + shift'` and `'{title}'` inside any string, not in `at: 'shift'`. `setVars` builds everything again (about 20 ms, plus the sound only if it changed): for a continuous input call it as often as you like, calls that overlap are joined, but do not use it for something that should move every frame (that is a keyframe).
|
|
433
|
+
105. **Layout that follows the screen is written in `W` / `H` / `GW` / `GH`, never in pixels.** After `movie.resize(w, h)` an expression sees the new size; a literal `x: 960` stays at 960. A string variable cannot be read in an expression (`x: 'title'` is a number context): use `{title}` in a string, or declare the number. A name that is declared but never read, and a `{name}` that was not declared, are both said at `init` (usually a typo). Load a web font BEFORE the first `setVars` that changes a text (see 103): the text is measured again each rebuild.
|
|
434
|
+
106. **A text outline used to spike; it is round-joined now.** `style: { stroke: { color: '#000', width: 12 } }` was drawn with PixiJS's default `miter` join, so the sharp corners of W, V, A, k and 4 grew spikes ("the outline sticks out"). The library now adds `join: 'round'` to an outline that names no join; say `join: 'miter'` or `'bevel'` if you want one. The stroke is centred on the glyph edge, so half of it eats into the letter: keep `width` to about a tenth of `fontSize`, or for a thick outline put a second copy of the text with the stroke UNDER the plain one.
|
|
435
|
+
107. **`spans` places pieces by measuring, so the font must be loaded first, and a span cannot change size.** A web font that is not loaded yet is measured as the fallback font and the pieces drift apart: `await document.fonts.load('30px "Name"')` before `movie.init`. `fontSize` / `fontFamily` / `lineHeight` / `align` on a span are ignored (warned): they belong to the layer's `style`. `fill`, `text` and `value` keyframes do not reach a spans layer (it is a group of text layers).
|
|
429
436
|
|
|
430
437
|
|
|
431
438
|
<!-- ===== skills/pixi-effects/reference/recipes.md ===== -->
|
|
@@ -1607,6 +1614,57 @@ while IFS=, read -r name score; do
|
|
|
1607
1614
|
done < table.csv
|
|
1608
1615
|
```
|
|
1609
1616
|
|
|
1617
|
+
---
|
|
1618
|
+
|
|
1619
|
+
## One film, several languages and colours (variables)
|
|
1620
|
+
|
|
1621
|
+
Declare what changes as `vars` and write `{name}` in the strings and the names as identifiers in the expressions. One page then makes every variant: `pixi-effects render film.html --var lang=ja --var accent=#3949ab -o film-ja.mp4`, and a page can switch live with `movie.setVars(...)`. Load the fonts of every language before the first build (pitfall 103); `check` says a variable nothing reads, and a `{name}` nobody declared.
|
|
1622
|
+
|
|
1623
|
+
```js
|
|
1624
|
+
// @docs-only variants
|
|
1625
|
+
const COPY = { en: { head: 'Spring sale', sub: 'Up to 40% off' }, ja: { head: '春のセール', sub: '最大40%オフ' } };
|
|
1626
|
+
const lang = new URLSearchParams(location.search).get('lang') ?? 'en';
|
|
1627
|
+
await movie.init({
|
|
1628
|
+
canvas, width: 1920, height: 1080, duration: 5,
|
|
1629
|
+
vars: { head: COPY[lang].head, sub: COPY[lang].sub, accent: '#e5399a', lift: 0 }, // numbers can be read in expressions, any value in {name}
|
|
1630
|
+
composition: { sequences: [
|
|
1631
|
+
{ type: 'shape', shape: 'rect', width: 'GW', height: 'GH', initial: { x: 'GW/2', y: 'GH/2', fillColor: '#12151f' } },
|
|
1632
|
+
{ type: 'text', text: '{head}', at: 0.3, duration: 4.7, style: { fontSize: 120, fontWeight: 'bold', fill: '{accent}' },
|
|
1633
|
+
initial: { x: 'GW/2', y: 'GH/2 - 40 - lift', anchorX: 0.5, anchorY: 0.5 }, keyframes: [{ at: 0, from: { alpha: 0 }, to: { alpha: 1 }, duration: 0.6 }] },
|
|
1634
|
+
{ type: 'text', text: '{sub}', at: 0.9, duration: 4.1, style: { fontSize: 56, fill: '#ffffff' },
|
|
1635
|
+
initial: { x: 'GW/2', y: 'GH/2 + 80', anchorX: 0.5, anchorY: 0.5 }, keyframes: [{ at: 0, from: { alpha: 0 }, to: { alpha: 1 }, duration: 0.6 }] },
|
|
1636
|
+
] },
|
|
1637
|
+
});
|
|
1638
|
+
// later, from a button: await movie.setVars({ head: COPY.ja.head, sub: COPY.ja.sub, accent: '#3949ab' }); // same frame, sound untouched
|
|
1639
|
+
```
|
|
1640
|
+
|
|
1641
|
+
---
|
|
1642
|
+
|
|
1643
|
+
## A layout that follows the window (liquid)
|
|
1644
|
+
|
|
1645
|
+
`resize(w, h)` builds the composition again at that size, and every expression sees it as `W` / `H` / `GW` / `GH`: write the layout in those, never in pixels. For a different arrangement per shape (wide / tall) write the composition in a function of the size and hand it to `rebuild`. Watch a **box** that you size with CSS (`#box { width: 100vw; height: 100vh }`, the canvas inside it `width: 100%; height: 100%`), not the canvas: the movie sets the canvas's pixel size, and an observer on the canvas would see its own change and grow it again. `check --size 1920x1080,1080x1920` makes a contact sheet and a layout review for each size without a browser window.
|
|
1646
|
+
|
|
1647
|
+
```js
|
|
1648
|
+
// @docs-only liquid-layout
|
|
1649
|
+
const layout = (w, h) => {
|
|
1650
|
+
const tall = h > w;
|
|
1651
|
+
return { sequences: [
|
|
1652
|
+
{ type: 'shape', shape: 'rect', width: 'GW', height: 'GH', initial: { x: 'GW/2', y: 'GH/2', fillColor: '#12151f' } },
|
|
1653
|
+
{ type: 'text', text: 'Headline', style: { fontSize: tall ? 'GW * 0.12' : 'GH * 0.14', fontWeight: 'bold', fill: '#ffffff' },
|
|
1654
|
+
initial: { x: tall ? 'GW * 0.08' : 'GW/2', y: tall ? 'GH * 0.7' : 'GH/2', anchorX: tall ? 0 : 0.5, anchorY: 0.5 } },
|
|
1655
|
+
] };
|
|
1656
|
+
};
|
|
1657
|
+
// <div id="box" style="width:100vw;height:100vh"><canvas id="stage" style="width:100%;height:100%;display:block"></canvas></div>
|
|
1658
|
+
const box = document.getElementById('box');
|
|
1659
|
+
const pixels = () => [Math.round(box.clientWidth * devicePixelRatio), Math.round(box.clientHeight * devicePixelRatio)];
|
|
1660
|
+
const [w0, h0] = pixels();
|
|
1661
|
+
await movie.init({ canvas, width: w0, height: h0, duration: 6, composition: layout(w0, h0) });
|
|
1662
|
+
new ResizeObserver(() => {
|
|
1663
|
+
const [w, h] = pixels();
|
|
1664
|
+
movie.rebuild({ composition: layout(w, h), width: w, height: h }).catch(console.error); // overlapping calls are joined: only the latest size is built
|
|
1665
|
+
}).observe(box);
|
|
1666
|
+
```
|
|
1667
|
+
|
|
1610
1668
|
|
|
1611
1669
|
<!-- ===== skills/pixi-effects/reference/direction.md ===== -->
|
|
1612
1670
|
|
|
@@ -1813,7 +1871,7 @@ interface SequenceCommon {
|
|
|
1813
1871
|
| Layer | Where `x, y` land by default | To change it |
|
|
1814
1872
|
|---|---|---|
|
|
1815
1873
|
| `shape` rect / circle / ellipse | the **centre** of the shape | `anchorX` / `anchorY` (0–1). A bar growing from its base: `anchorY: 1`; left to right: `anchorX: 0` |
|
|
1816
|
-
| `shape` line / polygon / path | the centre of the shape's bounds | `pivotX` / `pivotY` (in the shape's own canvas coordinates): the point that sits at `x, y` and that rotation / scale turn about. **`anchorX` / `anchorY` do nothing on these.** |
|
|
1874
|
+
| `shape` line / polygon / path | the centre of the shape's bounds | `pivotX` / `pivotY` (in the shape's own canvas coordinates): the point that sits at `x, y` and that rotation / scale turn about. **`anchorX` / `anchorY` do nothing on these.** A `path` whose `d` is already written in canvas coordinates takes no `x` / `y` at all (they would move its centre to that point); to move it, change the coordinates or wrap it in a composition. |
|
|
1817
1875
|
| `text`, `image`, `video` | the **top-left** corner | `anchorX` / `anchorY: 0.5` to centre |
|
|
1818
1876
|
| `composition` | its top-left corner | `pivotX` / `pivotY` = the point that sits at `x, y` and that rotation / scale turn about |
|
|
1819
1877
|
|
|
@@ -1825,7 +1883,7 @@ A layer is **not removed** when its lifespan `[at, at + duration)` ends: it is o
|
|
|
1825
1883
|
|
|
1826
1884
|
### `text`
|
|
1827
1885
|
|
|
1828
|
-
Renders a [PIXI.Text](https://pixijs.com/8.x/guides/components/scene-objects/text/text). Animatable position, rotation, scale, opacity and `fill`. `style` accepts any PixiJS `TextStyle` field and is applied once at build time — notably `fontSize fontFamily fontWeight fill letterSpacing lineHeight align wordWrap wordWrapWidth stroke dropShadow padding` (numbers may be expressions). A `dropShadow` glow is clipped unless `padding` is at least twice its `blur`. Text content can change over time (see *Changing text* below): a `{value}` placeholder prints an animatable number (counters), a keyframe `set: { text: '…' }` swaps the string, and `visibleChars` types it out. Multi-line text is centre-aligned unless `style.align` says otherwise. In expressions `w` / `h` are the size of the styled text, so `x: '-w'` places it just off the left edge.
|
|
1886
|
+
Renders a [PIXI.Text](https://pixijs.com/8.x/guides/components/scene-objects/text/text). Animatable position, rotation, scale, opacity and `fill`. `style` accepts any PixiJS `TextStyle` field and is applied once at build time (an outline `stroke: { color, width }` is **round-joined** unless you give `join`: PixiJS's own default, `miter`, grows spikes on the sharp corners of W, V, A, k; the stroke is centred on the glyph edge, so half its width covers the fill: keep `width` to about a tenth of `fontSize`, or put a second, stroked copy of the text under the plain one for a thick outline) — notably `fontSize fontFamily fontWeight fill letterSpacing lineHeight align wordWrap wordWrapWidth stroke dropShadow padding` (numbers may be expressions). A `dropShadow` glow is clipped unless `padding` is at least twice its `blur`. Text content can change over time (see *Changing text* below): a `{value}` placeholder prints an animatable number (counters), a keyframe `set: { text: '…' }` swaps the string, and `visibleChars` types it out. Multi-line text is centre-aligned unless `style.align` says otherwise. In expressions `w` / `h` are the size of the styled text, so `x: '-w'` places it just off the left edge.
|
|
1829
1887
|
|
|
1830
1888
|
```ts
|
|
1831
1889
|
{
|
|
@@ -1883,6 +1941,16 @@ A text layer has one animatable number, `value`, printed wherever the text conta
|
|
|
1883
1941
|
|
|
1884
1942
|
`format`: `decimals` (default 0), thousands `grouping` (default false) and `pad` (zero-pad the whole part to that many digits: `pad: 2` gives `05`, so a clock is `text: '18:42:{value}'` with `format: { pad: 2 }`). Put prefixes and suffixes in the text (`'${value}'`, `'{value}%'`). If `value` is animated but the text has no `{value}`, a warning says so.
|
|
1885
1943
|
|
|
1944
|
+
**Colour inside one text layer: `spans`.** Instead of `text`, write an array of strings (drawn in the layer's own `style`) and `{ text, fill, … }` objects; they are shown one after the other, and `\n` inside a span starts a new line:
|
|
1945
|
+
|
|
1946
|
+
```ts
|
|
1947
|
+
{ type: 'text', name: 'code', style: { fontFamily: 'Menlo', fontSize: 30, fill: '#e8f5e8', align: 'left' },
|
|
1948
|
+
spans: [{ text: 'type', fill: '#7fa07f' }, ': ', { text: "'text'", fill: '#ffd166', fontWeight: '700' }, ',\n at: 0.5'],
|
|
1949
|
+
initial: { x: 120, y: 200 } }
|
|
1950
|
+
```
|
|
1951
|
+
|
|
1952
|
+
A span may change `fill`, `fontWeight`, `fontStyle`, `fontVariant`, `letterSpacing` and `dropShadow`; a size, a font, `lineHeight`, `align` or a `stroke` belongs to the layer's `style` (one outline per layer: a stroke shifts a piece by half its width; a span that sets one of these is warned about once and the key is ignored; `style.wordWrap` and `style.leading` do not apply either: the lines are the `\n` of the spans). `style.align` places the lines on the width of the block (`'left'`, `'center'` — the default, as for every text — or `'right'`). At build time the layer becomes a group (a composition) of ordinary text layers, one per piece of a line, named `<name>.0`, `<name>.1` …, each at the x it has when the pieces are measured one after the other (the same measuring as `splitText`), so `inspect`, the timeline view and `check` see them. The group carries the layer's `name`, `at`, `duration`, `initial` (`anchorX` / `anchorY` are numbers, 0.5 centres the whole block, and cannot be keyframed), `keyframes` (`x`, `y`, `alpha`, `scale`, `rotation`, …), `filters`, `mask` and `blendMode`. A **typewriter** works over all the spans: `initial: { visibleChars: 0 }` and a keyframe `to: { visibleChars: N }` count every character in order, the newline included. `fill`, `text`, `value`, `fillGradient`, `format` and `colorSpace` do not apply to a group (a warning says so): use a separate text layer, or put the colour in a span. Measuring needs the font loaded (`await document.fonts.load('30px "Name"')` for a web font), as for `splitText`; the size and spacing in `style` may be numbers, expressions (`'GH * 0.05'`) or declared variables: they are resolved before the pieces are measured, and a missing `fontSize` is measured as the text layer's own default (Arial, 36).
|
|
1953
|
+
|
|
1886
1954
|
### `image`
|
|
1887
1955
|
|
|
1888
1956
|
Renders a [PIXI.Sprite](https://pixijs.com/8.x/guides/components/scene-objects/sprite/sprite) from a registered asset. Intrinsic `w`/`h` come from the loaded texture.
|
|
@@ -2838,6 +2906,8 @@ No comparison, conditional, bitwise, or string operators — keep it numeric (`s
|
|
|
2838
2906
|
|
|
2839
2907
|
### Scope variables
|
|
2840
2908
|
|
|
2909
|
+
(Your own names, set from outside, are [Variables](#variables) below.)
|
|
2910
|
+
|
|
2841
2911
|
Each sequence has its own scope, computed at build time:
|
|
2842
2912
|
|
|
2843
2913
|
| Name | Meaning |
|
|
@@ -2873,6 +2943,35 @@ style: { fontSize: 'min(GW, GH) * 0.06' }
|
|
|
2873
2943
|
initial: { x: 'GW/2', y: 'GH * 0.96', anchorX: 0.5, anchorY: 1 }
|
|
2874
2944
|
```
|
|
2875
2945
|
|
|
2946
|
+
### Variables
|
|
2947
|
+
|
|
2948
|
+
A **variable** is a name with a value you can change after the movie is built: the language of a title, a brand colour, an offset, the screen size. Declare them once in `movie.init`; the composition reads them by name.
|
|
2949
|
+
|
|
2950
|
+
```ts
|
|
2951
|
+
await movie.init({
|
|
2952
|
+
vars: { title: 'NORD', accent: '#e5399a', shift: 0 }, // a number or a string; no booleans (write 0 / 1)
|
|
2953
|
+
composition: { sequences: [
|
|
2954
|
+
{ type: 'shape', shape: 'rect', width: 'GW', height: 'GH', initial: { x: 'GW/2', y: 'GH/2', fillColor: '{accent}' } },
|
|
2955
|
+
{ type: 'text', text: '{title}', initial: { x: 'W/2 + shift', y: 'H/2' } },
|
|
2956
|
+
] },
|
|
2957
|
+
});
|
|
2958
|
+
await movie.setVars({ title: 'SÜD', shift: 40 }); // builds the composition again, at the same frame
|
|
2959
|
+
```
|
|
2960
|
+
|
|
2961
|
+
| Where | How it reads a variable |
|
|
2962
|
+
| --- | --- |
|
|
2963
|
+
| a numeric expression (`x`, `scale`, `width`, `fontSize` … any value that takes an [expression](#expressions)) | the **name**, like `W` or `GW`: `'W/2 + shift'`. Only number variables. |
|
|
2964
|
+
| any string anywhere in the composition (`text`, a colour, `asset`, a layer `name`, nested layers) | **`{name}`**: `'{title}'`, `'#{hex}'`, `'{lang}-logo'`. The text is replaced before the layer is built. A `{name}` that was not declared stays as written (`{value}` is the [counter](#counters-value) of a text layer, so it is also a name no variable can take). `$` is not used: it is a price (`'$5'`). |
|
|
2965
|
+
|
|
2966
|
+
Rules, all checked when the movie is built (each warning says what to change):
|
|
2967
|
+
- A name is letters, digits and `_`, not starting with a digit. These are **reserved** and are refused: `w h W H GW GH t d T contain cover value`, every expression function (`min max abs … step rand noise lerp clamp smoothstep mod`) and `PI`.
|
|
2968
|
+
- A variable that nothing reads (no `{name}` in a string, no use in an expression) is said: it is usually a typo on one side. A `{name}` that was not declared says the declared name it looks like.
|
|
2969
|
+
- Values are read **when the composition is built**, once (not every frame). `at` and `duration` are plain numbers, so a variable moves values, not timing; to change the structure in time, call `movie.rebuild({ composition })` with a composition made by your own function.
|
|
2970
|
+
- `setVars` of a name that was not declared is ignored with a did-you-mean; `setVars` of the values it already has does nothing.
|
|
2971
|
+
- Everything is built again, the playhead stays at the same frame, a playing movie keeps playing, and the sound is mixed again **only if** the sound changed. See [`movie.rebuild`](api.md#movie-variables-and-rebuilding-vars-setvars-resize-rebuild).
|
|
2972
|
+
- A video file is a function of the script **and** the variables: `pixi-effects render page.html --var title=SÜD --var shift=40`, `check --size 1920x1080,1080x1920` (a contact sheet per size).
|
|
2973
|
+
- Layout that should follow the screen size uses `W` / `H` / `GW` / `GH`, not pixels: after `movie.resize(w, h)` they are the new size.
|
|
2974
|
+
|
|
2876
2975
|
---
|
|
2877
2976
|
|
|
2878
2977
|
## Stops (presentations)
|
|
@@ -2988,7 +3087,7 @@ A scaled punch-in / punch-out. By default `B` opens up: it starts large and zoom
|
|
|
2988
3087
|
|
|
2989
3088
|
### `dissolve`
|
|
2990
3089
|
|
|
2991
|
-
|
|
3090
|
+
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.
|
|
2992
3091
|
|
|
2993
3092
|
```ts
|
|
2994
3093
|
{
|
|
@@ -3098,7 +3197,7 @@ Gotchas, all measured:
|
|
|
3098
3197
|
- **Distortion:** `warp` (`wave`: a flag or the surface of water, `haze`: heat shimmer; moves with the frame's time, no keyframes needed, and pads the layer by `strength` so a shifted edge is not cut), `twist` (a swirl), `bulgePinch` (a lens), `shockwave` (a ring that spreads), `rgbSplit` (colour fringes), `glitch` (slices). `warp`'s time is the time of the frame (inside a time-remapped composition, that composition's clock), `speed: 0` stands still, `strength` is in px and is capped at 200. It bends the layer's picture; to draw something that does not exist yet, use a `shader` layer.
|
|
3099
3198
|
- **Centres and offsets are in canvas pixels, not the layer's:** `twist.offset`, `bulgePinch.center`, `zoomBlur.center`, `radialBlur.center`, `shockwave.center` default to the canvas's top-left corner, so a layer in the middle looks unaffected. Put the centre on the layer: `{ type: 'twist', offset: { x: 640, y: 360 } }` for a layer centred on a 1280×720 canvas.
|
|
3100
3199
|
- **A filter works inside the layer's bounds.** Glow, dropShadow, outline and blur draw outside it and are clipped unless you widen the area (`filterArea: { x: -24, y: -24, width: w + 48, height: h + 48 }`, in the layer's own coordinates). Do not widen it for `grayscale`, `oldFilm` or other filters that paint the whole area: they fill the extra margin with black.
|
|
3101
|
-
- **Animate scalars by name** (`'filters.halo.outerStrength'`). Options that are points (`rgbSplit.red`, `dropShadow.offset`)
|
|
3200
|
+
- **Animate scalars by name** (`'filters.halo.outerStrength'`). Options that are points (`rgbSplit.red`, `dropShadow.offset`) are animated one leaf at a time: `'filters.<name>.red.x'` / `'filters.<name>.red.y'` tween like any number (the whole point `red: { x, y }` as a keyframe value switches at its keyframe and does not tween; a path that leads nowhere is warned about once and ignored). `pixelate` animates through `sizeX` / `sizeY`, not `size`.
|
|
3102
3201
|
- `shockwave` and `godray` are driven by `time` (a still frame at `time: 0` looks like nothing): animate it. `radialBlur`, `shockwave` and `tiltShift` positions are canvas pixels too.
|
|
3103
3202
|
- Some filters take their value as one argument; the DSL hides that (`{ type: 'pixelate', size: 8 }`, `{ type: 'emboss', strength: 6 }`).
|
|
3104
3203
|
|
|
@@ -3129,7 +3228,7 @@ import { GlowFilter, OldFilmFilter } from 'pixi-filters';
|
|
|
3129
3228
|
Notes:
|
|
3130
3229
|
|
|
3131
3230
|
- `filter` must be a `Filter` instance (constructor must have run on the consumer side).
|
|
3132
|
-
-
|
|
3231
|
+
- A dotted key (`'filters.s.red.x'`) is a path into the filter: the leaf of the point is written in place, which `pixi-filters` reads when it draws (checked on `RGBSplitFilter` in a real browser, `tests/tools/pointOptions.test.ts`). Before 0.32 such a key was written as a property literally named `red.x` and nothing moved.
|
|
3133
3232
|
- Without a `name`, the filter still applies but cannot be addressed via `filters.<name>.<prop>` keyframe paths.
|
|
3134
3233
|
|
|
3135
3234
|
Notes:
|
|
@@ -3473,6 +3572,8 @@ interface MovieOptions {
|
|
|
3473
3572
|
canvas?: HTMLCanvasElement; // existing canvas to render into; otherwise PixiJS creates one
|
|
3474
3573
|
assets?: AssetSpec[]; // [{ name, src }]
|
|
3475
3574
|
composition?: CompositionSpec; // root composition (see DSL reference)
|
|
3575
|
+
vars?: Record<string, number | string>; // named values the composition reads: a number as a name in an expression, any as {name} in a string (see Variables)
|
|
3576
|
+
loop?: boolean; // start with movie.loop on: playing goes back to the start at the end (default false, like a <video>)
|
|
3476
3577
|
poster?: number; // seconds: the frame shown before play (negative: from the end), see movie.poster
|
|
3477
3578
|
motionBlur?: boolean | number | { samples?: number; shutter?: number }; // blur for render() / snapshot() / contactSheet(), not live playback; see Motion blur
|
|
3478
3579
|
loader?: HTMLElement | string | false; // the page's loader (see pixi-effects/loader.css): shows how far init is (a bar, "BUILDING LAYERS 62%"), faded out when ready, stopped if init fails; default: a .pe-loader next to the canvas
|
|
@@ -3509,6 +3610,25 @@ Play, and pause exactly at `seconds` (the frame `round(seconds × frameRate)`).
|
|
|
3509
3610
|
|
|
3510
3611
|
Look around and put the movie back: pauses, runs `fn` (seek, take pictures with `snapshot()`; the seeks made inside are not reported as `seeking` / `seeked`), then puts the playhead (or the poster) back where it was, whatever `fn` did, and returns what `fn` returned. `stopImages()` and `inspectStops()` (`pixi-effects/present`) are made with it.
|
|
3511
3612
|
|
|
3613
|
+
### Movie variables and rebuilding: `vars`, `setVars()`, `resize()`, `rebuild()`
|
|
3614
|
+
|
|
3615
|
+
Variables are declared in `init({ vars: { title: 'NORD', shift: 0 } })` and read by the composition (see [Variables](dsl.md#variables)). Changing one builds the composition again: the new one is built off the stage first and swapped in when it is whole, at the same frame; a failure leaves the old one in place.
|
|
3616
|
+
|
|
3617
|
+
```ts
|
|
3618
|
+
movie.vars // the current values (a copy)
|
|
3619
|
+
await movie.setVars({ shift: 40 }); // the declared names only (others: ignored with a did-you-mean); the same values: nothing happens
|
|
3620
|
+
await movie.resize(1080, 1920); // the movie's size; W / H / GW / GH follow. Beats a width / height the composition names
|
|
3621
|
+
await movie.rebuild({ composition, vars, width, height }); // the general form: any of the four; `composition` replaces the whole spec (your layout function, called again)
|
|
3622
|
+
// all three resolve to a report, and fire 'rebuilt' with it:
|
|
3623
|
+
interface RebuildReport { reason: 'vars' | 'resize' | 'rebuild'; ms: number; audio: 'reused' | 'mixed' | 'none'; frame: number; playing: boolean; unchanged?: true }
|
|
3624
|
+
```
|
|
3625
|
+
|
|
3626
|
+
- The new composition is built while the old one goes on; at the swap the movie keeps doing what it is doing at that moment: the same frame, playing or paused (a `pause()`, a seek or a `play()` you made during the build is kept), the poster if it was shown. A playing movie whose sound is unchanged (`audio: 'reused'`) is not stopped at all: the frame clock and the sound go on and only the picture is swapped, so rebuilds in a row (a pointer) do not hold the playhead back. When the sound changed, the movie stops and plays on from the same frame with the new sound. A `playUntil()` in flight ends. The canvas changes size at the swap, not before. A `render()` or `peek()` in flight is not interrupted: the swap waits for it.
|
|
3627
|
+
- The sound is mixed only when it changed: `audio: 'reused'` means the same mix as before (the mix is the slow part, 0.6 to 0.8 s for a film; the rest takes about 20 ms). A time remap (`warp` / `sourceMap`) turns the cache off.
|
|
3628
|
+
- Calls that arrive during a rebuild are joined: at most one rebuild runs and one waits, with the latest values. Safe to call from `pointermove` or `scroll`: a rebuild that leaves the sound alone takes tens of milliseconds (11 to 42 ms on four gallery pieces), one that mixes the sound again 0.6 to 0.8 s, so a slider that changes the sound will lag behind the hand.
|
|
3629
|
+
- A rebuild that fails (a bad expression, a missing asset, a size that is not pixels) rejects, emits `error` with `where: 'rebuild'`, and the movie goes on with what it had. A `destroy()` during a rebuild ends it the same way.
|
|
3630
|
+
- To follow a window: observe a CSS-sized box around the canvas, not the canvas itself (the movie sets the canvas's pixel size): `new ResizeObserver(() => movie.resize(Math.round(box.clientWidth * devicePixelRatio), Math.round(box.clientHeight * devicePixelRatio))).observe(box)` (the `liquid-layout` recipe).
|
|
3631
|
+
|
|
3512
3632
|
### `movie.snapshot(frame?, options?): Promise<Blob | string>`
|
|
3513
3633
|
|
|
3514
3634
|
```ts
|
|
@@ -3692,7 +3812,9 @@ movie.off(event, fn): this
|
|
|
3692
3812
|
| `'seeking'` / `'seeked'` | `{ frame, totalFrames }` (the frame it lands on) | a `gotoFrame()` to another frame starts / has finished. NOT emitted for playback ticks, `render()`, `snapshot()` or `contactSheet()` |
|
|
3693
3813
|
| `'ended'` | none | playback ran off the end (after `'pause'`); pausing by hand does not emit it |
|
|
3694
3814
|
| `'volumechange'` | `{ volume: number; muted: boolean }` | `volume` or `muted` really changed (clamping counts) |
|
|
3695
|
-
| `'
|
|
3815
|
+
| `'loopchange'` | `{ loop: boolean }` | `movie.loop` really changed (the first value from `init({ loop })` is not a change) |
|
|
3816
|
+
| `'error'` | `{ where: 'init' \| 'render' \| 'playback' \| 'rebuild'; message: string; error: unknown }` | `init()` / `render()` / a rebuild failed (the call still rejects), or a frame failed during playback |
|
|
3817
|
+
| `'rebuilt'` | `RebuildReport` (see above) | a `setVars()` / `resize()` / `rebuild()` built the composition again (not for a call that changed nothing) |
|
|
3696
3818
|
|
|
3697
3819
|
The names are the ones an HTML5 `<video>` uses, so a player written for `<video>` ports over.
|
|
3698
3820
|
|
|
@@ -3711,8 +3833,10 @@ The names are the ones an HTML5 `<video>` uses, so a player written for `<video>
|
|
|
3711
3833
|
| `background` | string | CSS color |
|
|
3712
3834
|
| `volume` | number | 0..1 getter/setter; immediate. Setter clamps and applies to active audio |
|
|
3713
3835
|
| `muted` | boolean | getter/setter; immediate |
|
|
3836
|
+
| `loop` | boolean | getter/setter, default `false`. When true, an ordinary `play()` that reaches the end goes back to the start and plays on: no `ended`, no `pause` / `play` events at the wrap, the sound starts again (a short gap). `playUntil()` and `render()` ignore it. Say `loopchange` when it changes |
|
|
3714
3837
|
| `app` | `pixi.js Application \| null` | PIXI Application instance (advanced/escape hatch) |
|
|
3715
3838
|
| `timeline` | `Timeline \| null` | the movie's timeline (advanced): `time(t)`, `duration()`, `segments()` |
|
|
3839
|
+
| `vars` | `Record<string, number \| string>` | the current variable values (a copy) |
|
|
3716
3840
|
| `stops` | `Stop[]` | the composition's stops: named times (see Presentations); empty for an ordinary movie |
|
|
3717
3841
|
|
|
3718
3842
|
### Presentations: `stops` and `pixi-effects/present`
|
|
@@ -3886,6 +4010,7 @@ interface ControllerOptions {
|
|
|
3886
4010
|
exportButton?: boolean; // default true; false hides ⬇ + popover
|
|
3887
4011
|
keyboard?: boolean; // default true
|
|
3888
4012
|
presentButton?: boolean; // default true: a movie with stops gets a Present button (marks on the seek bar are always shown)
|
|
4013
|
+
loopButton?: boolean; // default true: a loop button next to the volume (and the L key) that flips movie.loop
|
|
3889
4014
|
pauseAtStops?: boolean; // default true: with stops, play / Space / click on the picture play to the next stop and pause; false plays straight through
|
|
3890
4015
|
clickToPlay?: boolean; // default true: a click / tap on the picture plays / pauses (like <video>); false leaves the canvas alone
|
|
3891
4016
|
className?: string; // default 'movie-controller'
|
|
@@ -3954,6 +4079,7 @@ Active when `keyboard: true` and the key target is not `<input>`/`<textarea>`/`<
|
|
|
3954
4079
|
| `←` / `→` | step ±1 frame |
|
|
3955
4080
|
| `↑` / `↓` | volume ±5% (clears mute when increasing past zero) |
|
|
3956
4081
|
| `M` | toggle mute |
|
|
4082
|
+
| `L` | toggle loop (`movie.loop`) |
|
|
3957
4083
|
| `Shift+E` | export with current settings (skips the popover) |
|
|
3958
4084
|
| `F` | toggle fullscreen |
|
|
3959
4085
|
| `Esc` | close the export popover or exit fullscreen (browser) |
|