pixi-effects 0.2.0 → 0.3.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 +31 -0
- package/README.md +15 -5
- package/ai/SKILL.md +44 -0
- package/ai/reference/cheatsheet.md +120 -0
- package/ai/reference/pitfalls.md +54 -0
- package/ai/reference/recipes.md +375 -0
- package/ai/template.html +82 -0
- package/ai/tools/save-image.py +24 -0
- package/dist/{Base-B1Tdn0Cv.d.cts → Base-BDw6dKRB.d.cts} +1 -1
- package/dist/{Base-Bh_VSusi.d.ts → Base-Ba4Ta4ap.d.ts} +1 -1
- package/dist/Composition-6FDH5OPM.cjs +13 -0
- package/dist/{Composition-BI5HZJFL.cjs.map → Composition-6FDH5OPM.cjs.map} +1 -1
- package/dist/Composition-FEYAUMFI.js +4 -0
- package/dist/{Composition-IO7ZN32J.js.map → Composition-FEYAUMFI.js.map} +1 -1
- package/dist/Controller.d.cts +2 -2
- package/dist/Controller.d.ts +2 -2
- package/dist/Movie-DMpaT67V.d.ts +179 -0
- package/dist/Movie-W8ISiEBB.d.cts +179 -0
- package/dist/{chunk-H55V3U56.js → chunk-DIJG2RSF.js} +28 -11
- package/dist/chunk-DIJG2RSF.js.map +1 -0
- package/dist/{chunk-VJCDG6YG.js → chunk-PN5A6QA7.js} +248 -64
- package/dist/chunk-PN5A6QA7.js.map +1 -0
- package/dist/{chunk-7OIWYXGV.cjs → chunk-SIRVULFF.cjs} +251 -62
- package/dist/chunk-SIRVULFF.cjs.map +1 -0
- package/dist/{chunk-64IHCYYN.cjs → chunk-ZL262ZRM.cjs} +37 -20
- package/dist/chunk-ZL262ZRM.cjs.map +1 -0
- package/dist/index.cjs +324 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +61 -31
- package/dist/index.d.ts +61 -31
- package/dist/index.js +321 -22
- package/dist/index.js.map +1 -1
- package/dist/three.cjs +5 -5
- package/dist/three.d.cts +2 -2
- package/dist/three.d.ts +2 -2
- package/dist/three.js +1 -1
- package/dist/{types-CNBilhpz.d.cts → types-3c8Vymgw.d.cts} +53 -6
- package/dist/{types-CNBilhpz.d.ts → types-3c8Vymgw.d.ts} +53 -6
- package/docs/api.md +333 -0
- package/docs/dsl.md +1017 -0
- package/llms-full.txt +1966 -0
- package/llms.txt +30 -0
- package/package.json +4 -3
- package/dist/Composition-BI5HZJFL.cjs +0 -13
- package/dist/Composition-IO7ZN32J.js +0 -4
- package/dist/Movie-CcR6h2jO.d.cts +0 -82
- package/dist/Movie-D-n8glA6.d.ts +0 -82
- package/dist/chunk-64IHCYYN.cjs.map +0 -1
- package/dist/chunk-7OIWYXGV.cjs.map +0 -1
- package/dist/chunk-H55V3U56.js.map +0 -1
- package/dist/chunk-VJCDG6YG.js.map +0 -1
package/docs/dsl.md
ADDED
|
@@ -0,0 +1,1017 @@
|
|
|
1
|
+
# DSL Reference
|
|
2
|
+
|
|
3
|
+
This document describes the declarative composition spec that you pass to `Movie.init({ composition })`. Every shape is exported as a TypeScript type from `pixi-effects` for editor autocomplete.
|
|
4
|
+
|
|
5
|
+
- [Composition](#composition)
|
|
6
|
+
- [Sequences](#sequences)
|
|
7
|
+
- [3D layers & camera](#3d-layers--camera)
|
|
8
|
+
- [Assets](#assets)
|
|
9
|
+
- [Keyframes](#keyframes)
|
|
10
|
+
- [Expressions](#expressions)
|
|
11
|
+
- [Filters](#filters)
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Composition
|
|
16
|
+
|
|
17
|
+
A composition is a tree node that has a width, a height, a duration, and a list of child sequences. The root composition is what you pass to `Movie.init({ composition })`. Nested compositions appear inside a parent as a `{ type: 'composition' }` sequence.
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
interface CompositionSpec {
|
|
21
|
+
width?: number; // pixels; defaults to Movie width
|
|
22
|
+
height?: number; // pixels; defaults to Movie height
|
|
23
|
+
duration?: number; // seconds; defaults to Movie duration
|
|
24
|
+
sequences?: SequenceSpec[];
|
|
25
|
+
// (also: name, at, initial, keyframes, filters — same as SequenceCommon below)
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Children of a nested composition use that composition's local coordinate system. `W`/`H` in the [scope](#expressions) refer to the **immediate parent**; `GW`/`GH` always refer to the root.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Sequences
|
|
34
|
+
|
|
35
|
+
Every sequence shares this base shape:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
interface SequenceCommon {
|
|
39
|
+
name?: string; // optional id, used for cross-references and debugging
|
|
40
|
+
at?: number; // start time in seconds, measured from the start of the PARENT composition (default 0)
|
|
41
|
+
duration?: number; // seconds; defaults to the parent's duration
|
|
42
|
+
initial?: Props; // properties applied before any keyframes evaluate
|
|
43
|
+
keyframes?: Keyframe[];
|
|
44
|
+
filters?: FilterSpec[];
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Timing at a glance
|
|
49
|
+
|
|
50
|
+
| What | Measured from |
|
|
51
|
+
|---|---|
|
|
52
|
+
| a sequence's `at` | the start of its **parent composition** (the root composition starts at 0) |
|
|
53
|
+
| a **keyframe's** `at` | the start of **its own sequence** (After Effects style): `at: 0` = the moment the layer appears. **Negative = back from the sequence's end** (`-0.5` = 0.5 s before it ends) |
|
|
54
|
+
| a transition's `at` | the start of the **parent composition** (like a sequence's `at`) |
|
|
55
|
+
| `audio` volume keyframes | the start of their own sequence (same rule as every other keyframe) |
|
|
56
|
+
|
|
57
|
+
### Anchors and origins
|
|
58
|
+
|
|
59
|
+
| Layer | Where `x, y` land by default | To change it |
|
|
60
|
+
|---|---|---|
|
|
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 | — |
|
|
63
|
+
| `text`, `image`, `video` | the **top-left** corner | `anchorX` / `anchorY: 0.5` to centre |
|
|
64
|
+
| `composition` | its top-left corner | `pivotX` / `pivotY` = the point that sits at `x, y` and that rotation / scale turn about |
|
|
65
|
+
|
|
66
|
+
Shape geometry (`width height radius anchorX …`) may be written at the top level or in `initial`; the top level wins. `rotation`, `skew*` and `rotationX/Y` are in **degrees**.
|
|
67
|
+
|
|
68
|
+
A layer is **not removed** when its lifespan `[at, at + duration)` ends: it is only hidden, and it keeps its last animated values. Outside the lifespan it is invisible. **z-order is array order** (later = on top). Build scenes by giving layers `at` / `duration` and stacking them; a full-screen background rect in each scene hides what is below it.
|
|
69
|
+
|
|
70
|
+
`Props` is `Record<string, number | string>`. String values are evaluated as [expressions](#expressions) unless the prop is a textual one (e.g. `fill`, `fontFamily`).
|
|
71
|
+
|
|
72
|
+
### `text`
|
|
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 cannot change over time (for a count-up, show one text layer per value). In expressions `w` / `h` are the size of the styled text, so `x: '-w'` places it just off the left edge.
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
{
|
|
78
|
+
type: 'text',
|
|
79
|
+
text: 'hello',
|
|
80
|
+
initial: { x: 'GW/2', y: 'GH/2', anchorX: 0.5, anchorY: 0.5 },
|
|
81
|
+
keyframes: [
|
|
82
|
+
{ at: 0, from: { alpha: 0 }, to: { alpha: 1 }, duration: 0.5 },
|
|
83
|
+
],
|
|
84
|
+
style: {
|
|
85
|
+
fontSize: 'GW * 0.05', // expression OK
|
|
86
|
+
fill: '#ffffff', // verbatim string
|
|
87
|
+
fontWeight: 'bold',
|
|
88
|
+
fontFamily: 'Inter',
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The text's `fill` is also animatable via keyframes (under the `to`/`from`/`set` keys, not under `style`). Set `colorSpace` for perceptual interpolation:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
{
|
|
97
|
+
type: 'text', text: 'COLORSPACE',
|
|
98
|
+
colorSpace: 'oklch',
|
|
99
|
+
style: { fill: '#ff0000', fontSize: 48, fontWeight: 'bold' },
|
|
100
|
+
keyframes: [
|
|
101
|
+
{ at: 1, to: { fill: '#00ff00' }, duration: 2, ease: 'sine.inOut' },
|
|
102
|
+
],
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
#### Counters (`{value}`)
|
|
107
|
+
|
|
108
|
+
A text layer has one animatable number, `value`, printed wherever the text contains `{value}`. Animate it with the normal keyframes (any ease, `from`/`to`/`set`, `repeat`):
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
{
|
|
112
|
+
type: 'text', text: '{value} users', format: { decimals: 0, grouping: true }, // → "2,480 users"
|
|
113
|
+
initial: { value: 0 },
|
|
114
|
+
keyframes: [{ at: 1, to: { value: 2480 }, duration: 1.2, ease: 'power3.out' }],
|
|
115
|
+
style: { fontSize: 72, fill: '#fff' }, ...
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`format`: `decimals` (default 0) and thousands `grouping` (default false). Put prefixes and suffixes in the text (`'${value}'`, `'{value}%'`). If `value` is animated but the text has no `{value}`, a warning says so.
|
|
120
|
+
|
|
121
|
+
### `image`
|
|
122
|
+
|
|
123
|
+
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.
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
{
|
|
127
|
+
type: 'image',
|
|
128
|
+
asset: 'logo',
|
|
129
|
+
initial: { x: 'GW/2 - w/2', y: 'GH/2 - h/2' },
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`tint` is animatable via keyframes. Optionally set `colorSpace` to interpolate the tint perceptually:
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
{
|
|
137
|
+
type: 'image', asset: 'logo',
|
|
138
|
+
colorSpace: 'oklch', // smooth hue sweep instead of muddy sRGB
|
|
139
|
+
initial: { tint: '#ff0000' },
|
|
140
|
+
keyframes: [
|
|
141
|
+
{ at: 1, to: { tint: '#00ff00' }, duration: 2, ease: 'sine.inOut' },
|
|
142
|
+
],
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### `video`
|
|
147
|
+
|
|
148
|
+
Renders a video. Intrinsic `w`/`h` come from the video's natural size. `currentTime` is driven by the timeline (so seeking the Movie scrubs the video).
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
{
|
|
152
|
+
type: 'video',
|
|
153
|
+
asset: 'green',
|
|
154
|
+
loop?: false, // loop back to start when finished (default false)
|
|
155
|
+
audio?: true, // route audio track into the mix (default true)
|
|
156
|
+
volume?: 1, // initial volume 0..1 (animatable via volume keyframes)
|
|
157
|
+
initial: { x: 0, y: 0, scale: 'cover' },
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Use `scale: 'cover'` or `scale: 'contain'` (these resolve via the [scope](#expressions)) to fit the video to the parent composition.
|
|
162
|
+
|
|
163
|
+
### `audio`
|
|
164
|
+
|
|
165
|
+
Audio-only sequence. No visual. Volume is animatable via keyframes.
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
{
|
|
169
|
+
type: 'audio',
|
|
170
|
+
asset: 'bgm',
|
|
171
|
+
loop?: false,
|
|
172
|
+
volume: 0,
|
|
173
|
+
keyframes: [
|
|
174
|
+
{ at: 0, to: { volume: 0.6 }, duration: 1 }, // fade in
|
|
175
|
+
{ at: -1, to: { volume: 0 }, duration: 1 }, // fade out (negative `at` = relative to end)
|
|
176
|
+
],
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Audio is mixed during `Movie.init()` and during `Movie.render()`. Volume keyframes interpolate linearly.
|
|
181
|
+
|
|
182
|
+
### `composition`
|
|
183
|
+
|
|
184
|
+
Nested composition. Same shape as the root spec but with `type: 'composition'` and an explicit `width`/`height`. Children animate within the local coordinate system; the composition itself can be positioned, scaled, and rotated as a unit.
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
{
|
|
188
|
+
type: 'composition',
|
|
189
|
+
width: 600, height: 120,
|
|
190
|
+
initial: { x: 'GW/2 - 300', y: 'GH * 0.86' },
|
|
191
|
+
keyframes: [
|
|
192
|
+
{ at: 2.4, from: { alpha: 0, rotation: -12 },
|
|
193
|
+
to: { alpha: 1, rotation: 0 },
|
|
194
|
+
duration: 0.7, ease: 'elastic.out(1, 0.5)' },
|
|
195
|
+
],
|
|
196
|
+
sequences: [
|
|
197
|
+
{ type: 'text', text: 'inside', initial: { x: 300, y: 60, anchorX: 0.5, anchorY: 0.5 }, style: { fontSize: 28, fill: '#fff' } },
|
|
198
|
+
],
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### `shape`
|
|
203
|
+
|
|
204
|
+
Parametric primitive backed by PIXI v8 `Graphics`. Six kinds, discriminated by `shape`. All geometry props accept the [expression language](#expressions), so dimensions can follow the canvas:
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
// Centered rounded panel that fills 80% of the canvas
|
|
208
|
+
{
|
|
209
|
+
type: 'shape', shape: 'rect',
|
|
210
|
+
width: 'W * 0.8', height: 'H * 0.6', cornerRadius: 24,
|
|
211
|
+
initial: {
|
|
212
|
+
x: 'W/2', y: 'H/2',
|
|
213
|
+
fillColor: '#1a2640', fillAlpha: 0.85,
|
|
214
|
+
strokeColor: '#3a5680', strokeWidth: 2,
|
|
215
|
+
},
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Every primitive draws centred on its local origin (so `anchorX`/`anchorY` and `pivotX`/`pivotY` semantics line up with the other sequence types).
|
|
220
|
+
|
|
221
|
+
| `shape` | Required props | Optional |
|
|
222
|
+
|-----------|------------------------------------------|------------------------|
|
|
223
|
+
| `rect` | `width`, `height` | `cornerRadius` |
|
|
224
|
+
| `circle` | `radius` | — |
|
|
225
|
+
| `ellipse` | `radiusX`, `radiusY` | — |
|
|
226
|
+
| `line` | `from: [x,y]`, `to: [x,y]` (canvas coordinates; stroke in `initial`) | — |
|
|
227
|
+
| `polygon` | `points: [[x,y], …]` | `open` (default false) |
|
|
228
|
+
| `path` | `d` (SVG path data) | — |
|
|
229
|
+
|
|
230
|
+
**Style** is set on `initial` and animatable via keyframes:
|
|
231
|
+
|
|
232
|
+
| Key | Notes |
|
|
233
|
+
|---------------|--------------------------------------------------------------|
|
|
234
|
+
| `fillColor` | Hex string (`'#3399ff'`) or number (`0x3399ff`). Omit = no fill. |
|
|
235
|
+
| `fillAlpha` | 0..1, default 1 |
|
|
236
|
+
| `strokeColor` | Hex string or number. Requires `strokeWidth > 0` to render. |
|
|
237
|
+
| `strokeAlpha` | 0..1, default 1 |
|
|
238
|
+
| `strokeWidth` | Pixels. Default 0 (no stroke). |
|
|
239
|
+
|
|
240
|
+
Colour keys (`fillColor`, `strokeColor`) tween smoothly between hues — no snap at the end. Numeric keys (`fillAlpha` / `strokeAlpha` / `strokeWidth`) animate linearly.
|
|
241
|
+
|
|
242
|
+
#### `fillGradient`
|
|
243
|
+
|
|
244
|
+
Fill a shape with a gradient instead of `fillColor` (top level or in `initial`). Positions are in 0–1 of the shape's own bounds, so the gradient follows the shape's size. Colours may carry alpha, so a radial gradient from transparent to dark is a vignette. Not animatable.
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
{ type: 'shape', shape: 'rect', width: 'GW', height: 'GH', initial: { x: 'GW/2', y: 'GH/2' },
|
|
248
|
+
fillGradient: { stops: [[0, '#1b2a6b'], [0.6, '#7b3fe4'], [1, '#ff6a88']] } } // linear, top → bottom
|
|
249
|
+
{ ..., fillGradient: { angle: 0, stops: [[0, '#00f5a0'], [1, '#00d9f5']] } } // left → right
|
|
250
|
+
{ ..., fillGradient: { type: 'radial', radius: 0.75, stops: [[0.45, 'rgba(0,0,0,0)'], [1, 'rgba(0,0,0,0.7)']] } } // vignette
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
| Field | Notes |
|
|
254
|
+
|---|---|
|
|
255
|
+
| `type` | `'linear'` (default) or `'radial'` |
|
|
256
|
+
| `stops` | at least two: `[offset 0–1, colour]` or `{ offset, color }` |
|
|
257
|
+
| `angle` | linear: degrees, `0` = left → right, `90` = top → bottom (default) |
|
|
258
|
+
| `center`, `innerRadius`, `radius` | radial: centre in 0–1 (default `[0.5, 0.5]`), inner radius (default 0), outer radius (default 0.5); an ellipse on a non-square shape |
|
|
259
|
+
|
|
260
|
+
#### `colorSpace`
|
|
261
|
+
|
|
262
|
+
Per-shape choice of how colour keyframes are interpolated. Default `'rgb'` (linear sRGB lerp via `gsap.utils.interpolate`) is fast but classic — a red → green ramp passes through muddy olive at the midpoint. The two perceptually uniform options keep saturation through the transition:
|
|
263
|
+
|
|
264
|
+
| Value | Behaviour |
|
|
265
|
+
|-----------|---------------------------------------------------------------------------------------------------|
|
|
266
|
+
| `'rgb'` | Default. Linear sRGB lerp. |
|
|
267
|
+
| `'oklab'` | Straight line in OKLab's chromaticity plane. Brighter, more chromatic midpoints. |
|
|
268
|
+
| `'oklch'` | (L, C, h) with hue along the shorter angular path. Smooth rainbow-style sweeps; ideal for hue cycling. |
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
{
|
|
272
|
+
type: 'shape', shape: 'circle', radius: 40,
|
|
273
|
+
colorSpace: 'oklch', // ← red → green via vibrant orange
|
|
274
|
+
initial: { fillColor: '#ff0000' },
|
|
275
|
+
keyframes: [
|
|
276
|
+
{ at: 1, to: { fillColor: '#00ff00' }, duration: 2, ease: 'sine.inOut' },
|
|
277
|
+
],
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
{
|
|
283
|
+
type: 'shape', shape: 'circle', radius: 40,
|
|
284
|
+
initial: { x: 'W/2', y: 'H/2', fillColor: '#ff5577' },
|
|
285
|
+
keyframes: [
|
|
286
|
+
{ at: 1, to: { fillColor: '#55ddaa' }, duration: 1.0, ease: 'sine.inOut' },
|
|
287
|
+
{ at: 2, to: { strokeColor: '#fff', strokeWidth: 6 }, duration: 0.5 },
|
|
288
|
+
],
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
**Transforms animate normally.** `x`, `y`, `scale`, `scaleX`, `scaleY`, `rotation`, `alpha` etc. all go through the standard keyframe pipeline.
|
|
293
|
+
|
|
294
|
+
```ts
|
|
295
|
+
// SVG-path heart that scale-pops on entry, then beats twice
|
|
296
|
+
{
|
|
297
|
+
type: 'shape', shape: 'path',
|
|
298
|
+
d: 'M 0 -20 C -30 -50 -70 -10 0 30 C 70 -10 30 -50 0 -20 Z',
|
|
299
|
+
initial: { x: 'W/2', y: 'H/2', fillColor: '#ff3366' },
|
|
300
|
+
keyframes: [
|
|
301
|
+
{ at: 0, from: { alpha: 0, scale: 0 },
|
|
302
|
+
to: { alpha: 1, scale: 1 },
|
|
303
|
+
duration: 0.5, ease: 'back.out(2.5)' },
|
|
304
|
+
{ at: 1.5, to: { scale: 1.2 }, duration: 0.3, ease: 'power2.out' },
|
|
305
|
+
{ at: 1.8, to: { scale: 1.0 }, duration: 0.3, ease: 'power2.in' },
|
|
306
|
+
],
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
**Scalar geometry is animatable.** `width`, `height`, `cornerRadius` (rect), `radius` (circle), `radiusX` / `radiusY` (ellipse) all flow through the same per-frame redraw and can be tweened via keyframes — useful for progress bars (animate `width`), pulsing icons (animate `radius`), or shape-morph callouts. Array geometry (polygon `points`, line endpoints, path `d`) is baked at build time; use `scale` / `scaleX` / `scaleY` for those.
|
|
311
|
+
|
|
312
|
+
`anchorX` / `anchorY` (default `0.5` each) control which point on the bbox sits at the local origin — `0` is left/top, `1` is right/bottom. They're animatable too. Critical for "grow from one edge" effects:
|
|
313
|
+
|
|
314
|
+
```ts
|
|
315
|
+
// Left-anchored progress bar — width animates 0 → W, left edge stays at x
|
|
316
|
+
{
|
|
317
|
+
type: 'shape', shape: 'rect', width: 0, height: 18, cornerRadius: 9,
|
|
318
|
+
anchorX: 0, // ← left edge at x
|
|
319
|
+
initial: { x: 0, y: 'H/2', fillColor: '#5599ff' },
|
|
320
|
+
keyframes: [
|
|
321
|
+
{ at: 0, to: { width: 'W' }, duration: 4, ease: 'sine.inOut' },
|
|
322
|
+
],
|
|
323
|
+
}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
328
|
+
## 3D layers & camera
|
|
329
|
+
|
|
330
|
+
Place any 2D layer in depth and view it through a camera — After Effects style, no three.js. Everything uses the normal `initial` / `keyframes` / expression vocabulary.
|
|
331
|
+
|
|
332
|
+
### Conventions (read this first)
|
|
333
|
+
|
|
334
|
+
| Thing | Convention |
|
|
335
|
+
|---|---|
|
|
336
|
+
| Axes | `+x` right, `+y` down, **`+z` toward the viewer** (like CSS `translateZ`). Bigger `z` = nearer = larger on screen. |
|
|
337
|
+
| Units | Positions in composition pixels. **Rotations in degrees.** |
|
|
338
|
+
| Rotation signs | Same as CSS: `rotationY: 30` swings the right edge away; `rotationX: 30` brings the bottom edge toward you; `rotation` is the Z axis. |
|
|
339
|
+
| Rotation centre | The same point 2D `rotation` uses: `anchorX/anchorY` (sprites, text, shapes) or `pivotX/pivotY` (compositions). Set `anchorX: 0.5, anchorY: 0.5` to spin about the centre. |
|
|
340
|
+
| Default camera | Centred, looking straight at the `z = 0` plane, which maps 1:1 to pixels. A `threeD` layer at `z = 0` looks identical to a 2D layer. |
|
|
341
|
+
| Camera props | `x`, `y`, `z`, `lookAtX`, `lookAtY`, `lookAtZ`, `fov` — all in `initial` / `keyframes`, never on the camera itself. |
|
|
342
|
+
|
|
343
|
+
### `threeD` layers
|
|
344
|
+
|
|
345
|
+
Add `threeD: true` to any visual layer, then use `z`, `rotationX`, `rotationY`:
|
|
346
|
+
|
|
347
|
+
```json
|
|
348
|
+
{
|
|
349
|
+
"sequences": [
|
|
350
|
+
{ "type": "text", "text": "tilted", "threeD": true,
|
|
351
|
+
"style": { "fontSize": 64, "fill": "#ffffff" },
|
|
352
|
+
"initial": { "x": "GW/2", "y": "GH/2", "anchorX": 0.5, "anchorY": 0.5, "rotationY": -35 },
|
|
353
|
+
"keyframes": [{ "at": 0, "to": { "rotationY": 0 }, "duration": 1, "ease": "power2.out" }] }
|
|
354
|
+
]
|
|
355
|
+
}
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
`z`, `rotationX` and `rotationY` are ignored on a layer without `threeD: true` (a warning tells you). `threeD: false` layers ignore the camera and keep stack order.
|
|
359
|
+
|
|
360
|
+
### Camera
|
|
361
|
+
|
|
362
|
+
The camera is a layer: `{ "type": "camera" }`. It has no visuals. With nothing set it is the default camera.
|
|
363
|
+
|
|
364
|
+
| Prop | Meaning | Default |
|
|
365
|
+
|---|---|---|
|
|
366
|
+
| `x`, `y` | camera position | `W/2`, `H/2` |
|
|
367
|
+
| `z` | camera depth. **Auto:** if you never set it, it follows `fov` so the `z = 0` plane stays 1:1 | `(H/2) / tan(fov/2)` |
|
|
368
|
+
| `lookAtX`, `lookAtY`, `lookAtZ` | the point it looks at | `W/2`, `H/2`, `0` |
|
|
369
|
+
| `fov` | vertical field of view, degrees (clamped to 1–179) | 40 |
|
|
370
|
+
|
|
371
|
+
```json
|
|
372
|
+
{
|
|
373
|
+
"sequences": [
|
|
374
|
+
{ "type": "camera", "name": "cam",
|
|
375
|
+
"keyframes": [
|
|
376
|
+
{ "at": 0, "from": { "x": "GW/2 - 260", "lookAtX": "GW/2 - 260" },
|
|
377
|
+
"to": { "x": "GW/2 + 260", "lookAtX": "GW/2 + 260" },
|
|
378
|
+
"duration": 4, "ease": "sine.inOut" }
|
|
379
|
+
] },
|
|
380
|
+
{ "type": "shape", "shape": "rect", "width": 300, "height": 200, "threeD": true,
|
|
381
|
+
"initial": { "x": "GW/2", "y": "GH/2", "z": -400, "fillColor": "#3a6ea5" } },
|
|
382
|
+
{ "type": "shape", "shape": "rect", "width": 300, "height": 200, "threeD": true,
|
|
383
|
+
"initial": { "x": "GW/2", "y": "GH/2", "z": 250, "fillColor": "#d96a3a" } }
|
|
384
|
+
]
|
|
385
|
+
}
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
Moving the camera sideways makes the near rectangle slide faster than the far one (parallax). Animating only `fov` keeps the `z = 0` plane fixed and changes how strong the perspective is (a dolly zoom):
|
|
389
|
+
|
|
390
|
+
```json
|
|
391
|
+
{
|
|
392
|
+
"sequences": [
|
|
393
|
+
{ "type": "camera",
|
|
394
|
+
"keyframes": [{ "at": 0, "to": { "fov": 70 }, "duration": 3, "ease": "sine.inOut" }] },
|
|
395
|
+
{ "type": "shape", "shape": "circle", "radius": 120, "threeD": true,
|
|
396
|
+
"initial": { "x": "GW/2", "y": "GH/2", "z": 300, "fillColor": "#38a169" } }
|
|
397
|
+
]
|
|
398
|
+
}
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
- A camera affects the `threeD` layers that are its **siblings** (same composition). A nested composition has its own camera for its children, and is itself a layer in its parent's space when it has `threeD: true`.
|
|
402
|
+
- Several cameras may exist if their lifespans (`at` / `duration`) do not overlap — each is a camera cut. Overlapping cameras warn; the last-listed one wins.
|
|
403
|
+
- With no camera layer the default camera is used.
|
|
404
|
+
|
|
405
|
+
### Depth order and limits
|
|
406
|
+
|
|
407
|
+
- Consecutive `threeD` layers are drawn farthest-first. A non-`threeD` layer between them splits the group (like After Effects).
|
|
408
|
+
- A layer at or behind the camera is hidden for that frame.
|
|
409
|
+
- v1 limits: planes do not intersect (whole layers are sorted); masks, transitions and `filterArea` are not supported on `threeD` layers (a mask is ignored with a warning); each `threeD` layer costs one extra render pass per frame.
|
|
410
|
+
- Wrong-but-likely names (`rotateY`, `translateZ`, `depth`, `perspective`, `zoom`) are not accepted; the console tells you the right name.
|
|
411
|
+
|
|
412
|
+
---
|
|
413
|
+
|
|
414
|
+
## three
|
|
415
|
+
|
|
416
|
+
Composites a [three.js](https://threejs.org/) scene as a normal layer. Each frame, the scene is rendered into an offscreen WebGL canvas and uploaded into the sequence's PIXI texture — so once built, the layer is a plain sprite: 2D `initial` / keyframe props, [masks](#masks), and [filters](#filters) all apply to it exactly like any other sequence type.
|
|
417
|
+
|
|
418
|
+
`type: 'three'` lives in the optional `pixi-effects/three` entry, not core `pixi-effects` — see [API reference](./api.md#pixi-effectsthree) for install/import details.
|
|
419
|
+
|
|
420
|
+
```ts
|
|
421
|
+
import { registerThree, three } from 'pixi-effects/three';
|
|
422
|
+
import * as THREE from 'three';
|
|
423
|
+
|
|
424
|
+
registerThree(); // once, before Movie.init
|
|
425
|
+
|
|
426
|
+
three({
|
|
427
|
+
type: 'three',
|
|
428
|
+
name: 'knot',
|
|
429
|
+
width: 'GW * 0.6', height: 'GH * 0.6',
|
|
430
|
+
initial: { x: 'GW/2', y: 'GH/2', anchorX: 0.5, anchorY: 0.5 },
|
|
431
|
+
setup: (ctx) => {
|
|
432
|
+
const knot = new THREE.Mesh(
|
|
433
|
+
new THREE.TorusKnotGeometry(1, 0.35, 128, 32),
|
|
434
|
+
new THREE.MeshStandardMaterial({ color: 0x7fb4ff }),
|
|
435
|
+
);
|
|
436
|
+
ctx.scene.add(knot);
|
|
437
|
+
ctx.camera.position.z = 4;
|
|
438
|
+
return { objects: { knot } }; // exposes `knot` to keyframes, see below
|
|
439
|
+
},
|
|
440
|
+
keyframes: [
|
|
441
|
+
{ at: 0, to: { 'three.knot.rotation.y': Math.PI * 2 }, duration: 6 },
|
|
442
|
+
],
|
|
443
|
+
})
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
### Spec fields
|
|
447
|
+
|
|
448
|
+
| Field | Type | Notes |
|
|
449
|
+
| ------------ | ------------------------------------------------------------ | ---------------------------------------------------------------------------------------- |
|
|
450
|
+
| `width?` | `PropValue` | layer size in composition px; expressions allowed (e.g. `'W * 0.5'`). Default: parent composition size. |
|
|
451
|
+
| `height?` | `PropValue` | see `width`. |
|
|
452
|
+
| `resolution?` | `number` | supersampling factor for the offscreen canvas. Default `1`. |
|
|
453
|
+
| `setup` | `(ctx: ThreeContext) => Promise<ThreeSetupResult \| void> \| ThreeSetupResult \| void` | builds the scene once. Add objects to `ctx.scene`, position `ctx.camera` (or replace it via `{ camera }`). Async, so `Movie.init` can await GLTF / texture loading. |
|
|
454
|
+
| `update?` | `(t: number, ctx: ThreeContext) => void` | optional per-frame hook; `t` is sequence-local time in seconds. |
|
|
455
|
+
| `dispose?` | `(ctx: ThreeContext) => void` | cleanup for user-created GPU resources (geometries, materials, textures). Called from `destroy()`. |
|
|
456
|
+
|
|
457
|
+
`ThreeContext` is `{ scene, camera, renderer, width, height }` — the three.js `Scene`, `Camera`, `WebGLRenderer`, and the resolved layer size in composition px.
|
|
458
|
+
|
|
459
|
+
### Keyframe paths: `three.<name>.<path>`
|
|
460
|
+
|
|
461
|
+
Objects returned from `setup`'s `{ objects }` are addressable from keyframes as `three.<name>.<path>`, using the same `from`/`to`/`set` vocabulary as every other prop:
|
|
462
|
+
|
|
463
|
+
```ts
|
|
464
|
+
setup: (ctx) => {
|
|
465
|
+
const knot = new THREE.Mesh(/* ... */);
|
|
466
|
+
ctx.scene.add(knot);
|
|
467
|
+
return { objects: { knot } };
|
|
468
|
+
},
|
|
469
|
+
keyframes: [
|
|
470
|
+
{ at: 0, to: { 'three.knot.rotation.y': Math.PI * 2 }, duration: 6 },
|
|
471
|
+
{ at: 0, to: { 'three.knot.position.x': 1.5 }, duration: 3 },
|
|
472
|
+
],
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
`camera` is exposed implicitly as `three.camera.<path>` unless `setup`'s `objects` already defines a `camera` key.
|
|
476
|
+
|
|
477
|
+
### Rules
|
|
478
|
+
|
|
479
|
+
1. Call `registerThree()` before `Movie.init` builds a composition containing a `type: 'three'` sequence — the sequence type is looked up by name at build time.
|
|
480
|
+
2. `update(t)` must derive all state purely from `t` — no wall clock, no unseeded randomness. Playback and export both seek arbitrarily; anything else in `update` means identical timestamps can render different pixels.
|
|
481
|
+
3. Each three sequence owns one WebGL context (an offscreen canvas + renderer). Browsers cap live WebGL contexts at roughly 8–16 — budget accordingly if a composition uses several three layers.
|
|
482
|
+
4. Like `custom` filters, the spec carries functions (`setup`, optionally `update` / `dispose`) and is **not** JSON-serializable.
|
|
483
|
+
5. The scene renders into the layer rectangle: anything that projects outside it is clipped at the layer's edges. Frame the camera with margin for the whole animation — rotating objects often project taller/wider than their resting pose (a `TorusKnotGeometry(radius, tube)`, for instance, extends to `1.5 * radius + tube`, not `radius`).
|
|
484
|
+
|
|
485
|
+
---
|
|
486
|
+
|
|
487
|
+
## Masks
|
|
488
|
+
|
|
489
|
+
> A mask is a layer in the **parent's** coordinate space and does **not** follow the layer it masks: slide the content and the mask separately. Reveal by growing the mask's `width` from a fixed edge (`anchorX: 0`); exit by tweening `x` and `width` together.
|
|
490
|
+
|
|
491
|
+
Any sequence can carry an inline `mask` — itself a full sequence — that shapes which pixels of the maskee are visible. The mask runs in the same coordinate space as the maskee (added to the same parent composition) and shares its lifetime, so a circular avatar crop is just:
|
|
492
|
+
|
|
493
|
+
```ts
|
|
494
|
+
{
|
|
495
|
+
type: 'image', asset: 'photo',
|
|
496
|
+
initial: { x: 'W/2', y: 'H/2', anchorX: 0.5, anchorY: 0.5 },
|
|
497
|
+
mask: {
|
|
498
|
+
type: 'shape', shape: 'circle', radius: 130,
|
|
499
|
+
initial: { x: 'W/2', y: 'H/2', fillColor: '#ffffff' },
|
|
500
|
+
},
|
|
501
|
+
}
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
The mask is itself a sequence, so it can have `keyframes` of its own — useful for reveal animations (a circle growing from `scale: 0` to full size, a rect wiping across, …):
|
|
505
|
+
|
|
506
|
+
```ts
|
|
507
|
+
// Iris reveal — image is wiped in by a growing circle
|
|
508
|
+
{
|
|
509
|
+
type: 'image', asset: 'photo',
|
|
510
|
+
initial: { x: 'W/2', y: 'H/2', anchorX: 0.5, anchorY: 0.5 },
|
|
511
|
+
mask: {
|
|
512
|
+
type: 'shape', shape: 'circle', radius: 200,
|
|
513
|
+
initial: { x: 'W/2', y: 'H/2', fillColor: '#ffffff', scale: 0 },
|
|
514
|
+
keyframes: [
|
|
515
|
+
{ at: 0, to: { scale: 1 }, duration: 1.0, ease: 'power2.out' },
|
|
516
|
+
],
|
|
517
|
+
},
|
|
518
|
+
}
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
Notes:
|
|
522
|
+
- The mask sequence is rendered as a mask, not as a normal child — its `fillColor` / `strokeColor` only matter for which pixels are kept, not for the visible colour.
|
|
523
|
+
- Any sequence type works as a mask (shape / image / text / nested composition); shapes are the natural choice for geometric reveals.
|
|
524
|
+
- For a left-to-right wipe, give the mask `anchorX: 0` (rect/circle/ellipse) so `width: 0 → full` grows rightward from the left edge.
|
|
525
|
+
|
|
526
|
+
#### `maskInverted`
|
|
527
|
+
|
|
528
|
+
When `true`, flip the mask sense: pixels INSIDE the mask shape become transparent, pixels OUTSIDE stay visible. Useful for knockout / cutout effects (punch a circular hole through a panel, etc.).
|
|
529
|
+
|
|
530
|
+
```ts
|
|
531
|
+
// Photo with a circular hole punched through the middle
|
|
532
|
+
{
|
|
533
|
+
type: 'image', asset: 'photo',
|
|
534
|
+
initial: { x: 'W/2', y: 'H/2', anchorX: 0.5, anchorY: 0.5 },
|
|
535
|
+
maskInverted: true,
|
|
536
|
+
mask: {
|
|
537
|
+
type: 'shape', shape: 'circle', radius: 80,
|
|
538
|
+
initial: { x: 'W/2', y: 'H/2', fillColor: '#ffffff' },
|
|
539
|
+
keyframes: [
|
|
540
|
+
{ at: 0, to: { radius: 120 }, duration: 1.5, ease: 'sine.inOut' },
|
|
541
|
+
{ at: 1.5, to: { radius: 80 }, duration: 1.5, ease: 'sine.inOut' },
|
|
542
|
+
],
|
|
543
|
+
},
|
|
544
|
+
}
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
Routed through PIXI v8's native `setMask({ inverse: true })`.
|
|
548
|
+
|
|
549
|
+
---
|
|
550
|
+
|
|
551
|
+
## Assets
|
|
552
|
+
|
|
553
|
+
Pass a flat list of named assets to `Movie.init({ assets })`. Sequences refer to them by `name`.
|
|
554
|
+
|
|
555
|
+
```ts
|
|
556
|
+
await movie.init({
|
|
557
|
+
assets: [
|
|
558
|
+
{ name: 'logo', src: '/img/logo.png' },
|
|
559
|
+
{ name: 'bgm', src: '/audio/bgm.mp3' },
|
|
560
|
+
{ name: 'green', src: '/video/green.mp4' },
|
|
561
|
+
],
|
|
562
|
+
composition: { /* sequences reference 'logo', 'bgm', 'green' */ },
|
|
563
|
+
});
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
Supported formats are whatever PixiJS Assets and the browser's audio/video decoders accept (typically PNG/JPG/WebP for images; MP3/AAC/Opus for audio; MP4/WebM for video).
|
|
567
|
+
|
|
568
|
+
---
|
|
569
|
+
|
|
570
|
+
## Keyframes
|
|
571
|
+
|
|
572
|
+
```ts
|
|
573
|
+
interface Keyframe {
|
|
574
|
+
at?: number; // start, in seconds from the start of THIS sequence; negative = back from the sequence's end (-0.5 = 0.5 s before it ends)
|
|
575
|
+
duration?: number; // seconds (default 0 — instantaneous)
|
|
576
|
+
ease?: string; // GSAP easing name (default 'none')
|
|
577
|
+
set?: Props; // jump to these values at `at`
|
|
578
|
+
to?: Props; // animate from current to these values over `duration`
|
|
579
|
+
from?: Props; // animate from these values to current
|
|
580
|
+
// `from` + `to` together is a 'fromTo' tween
|
|
581
|
+
}
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
The four kinds are mutually exclusive per keyframe:
|
|
585
|
+
|
|
586
|
+
- **`set`** — instantaneous property assignment at `at`.
|
|
587
|
+
- **`to`** — tween from whatever the property is at `at` to the given values, over `duration`.
|
|
588
|
+
- **`from`** — tween from the given values back to the current property, over `duration`.
|
|
589
|
+
- **`from` + `to`** — full fromTo tween, with explicit start and end values.
|
|
590
|
+
|
|
591
|
+
### Repeating
|
|
592
|
+
|
|
593
|
+
`repeat` (extra plays, a finite whole number), `yoyo` (every other play runs backwards) and `repeatDelay` (seconds between plays) work on every kind of animation — position, alpha, colour, shape geometry, filters, text `fill`/`value`:
|
|
594
|
+
|
|
595
|
+
```ts
|
|
596
|
+
{ at: 0, to: { scale: 1.15 }, duration: 0.5, ease: 'sine.inOut', repeat: 5, yoyo: true } // a heartbeat: 6 plays, 6 s total
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
Total time = `duration × (repeat + 1)` (+ delays). Endless repeats are not allowed (the timeline needs a fixed length); `repeat: -1` / `Infinity` is ignored with a warning — repeat as many times as the layer lasts.
|
|
600
|
+
|
|
601
|
+
### Negative `at`
|
|
602
|
+
|
|
603
|
+
If `at < 0`, it's interpreted as `duration + at` — measured back from the end of **the sequence the keyframe belongs to**. Useful for fade-outs:
|
|
604
|
+
|
|
605
|
+
```ts
|
|
606
|
+
{ type: 'text', text: 'bye', at: 2, duration: 3,
|
|
607
|
+
keyframes: [{ at: -0.5, to: { alpha: 0 }, duration: 0.5 }] } // fades during the last 500 ms of this text (global 4.5 s – 5 s)
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
### Easing
|
|
611
|
+
|
|
612
|
+
Standard GSAP easing strings: `'none'`, `'linear'`, `'power1.in'` ... `'power4.inOut'`, `'sine.in/out/inOut'`, `'expo.in/out/inOut'`, `'circ.in/out/inOut'`, `'back.in/out/inOut(overshoot)'`, `'elastic.in/out/inOut(amplitude, period)'`, `'bounce.in/out/inOut'`. See [GSAP easing docs](https://gsap.com/docs/v3/Eases/).
|
|
613
|
+
|
|
614
|
+
### PIXI shorthands
|
|
615
|
+
|
|
616
|
+
These keys are auto-routed through GSAP's PixiPlugin when used in `initial` / `set` / `to` / `from` / `keyframes`:
|
|
617
|
+
|
|
618
|
+
```
|
|
619
|
+
scale, scaleX, scaleY
|
|
620
|
+
anchor, anchorX, anchorY
|
|
621
|
+
pivot, pivotX, pivotY
|
|
622
|
+
skew, skewX, skewY
|
|
623
|
+
position, positionX, positionY
|
|
624
|
+
tilePosition, tilePositionX, tilePositionY
|
|
625
|
+
tileScale, tileScaleX, tileScaleY
|
|
626
|
+
tint, autoAlpha
|
|
627
|
+
colorize, colorizeAmount, colorMatrixFilter
|
|
628
|
+
blur, blurX, blurY, blurPadding
|
|
629
|
+
lineColor, lineAlpha, fillColor, fillAlpha
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
(In addition to plain DisplayObject props like `x`, `y`, `rotation`, `alpha`, `width`, `height`, `visible`.) **Angles are in degrees**: `rotation`, `skew` / `skewX` / `skewY` (PixiPlugin converts them), and `rotationX` / `rotationY` for 3D layers.
|
|
633
|
+
|
|
634
|
+
### Filter keyframe paths
|
|
635
|
+
|
|
636
|
+
Animate a named filter's parameter using a dot-path key:
|
|
637
|
+
|
|
638
|
+
```ts
|
|
639
|
+
import { BlurFilter } from 'pixi.js';
|
|
640
|
+
|
|
641
|
+
{
|
|
642
|
+
type: 'video',
|
|
643
|
+
asset: 'green',
|
|
644
|
+
filters: [
|
|
645
|
+
{ name: 'k', type: 'chromaKey', keyColor: '#00ff00' },
|
|
646
|
+
{ name: 'b', type: 'custom', filter: new BlurFilter({ strength: 0 }) },
|
|
647
|
+
],
|
|
648
|
+
keyframes: [
|
|
649
|
+
{ at: 2, to: { 'filters.b.strength': 8 }, duration: 1 },
|
|
650
|
+
{ at: 4, to: { 'filters.k.threshold': 0.5 }, duration: 1 },
|
|
651
|
+
],
|
|
652
|
+
}
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
The path is `filters.<filter-name>.<animatable-param>`. A filter must have a `name` to be addressable.
|
|
656
|
+
|
|
657
|
+
---
|
|
658
|
+
|
|
659
|
+
## Expressions
|
|
660
|
+
|
|
661
|
+
Any string `Props` value may be an arithmetic expression. Strings whose first character is a letter or digit are tried as expressions; verbatim strings (color hex, font names, etc.) are passed through when used in fields that don't expect a number.
|
|
662
|
+
|
|
663
|
+
The expression parser is in-tree (no eval, CSP-safe). See [`src/expr/Parser.ts`](../src/expr/Parser.ts).
|
|
664
|
+
|
|
665
|
+
### Operators and functions
|
|
666
|
+
|
|
667
|
+
| Form | Notes |
|
|
668
|
+
| ----------------------- | ---------------------------------------------------- |
|
|
669
|
+
| `+ - * /` | binary arithmetic |
|
|
670
|
+
| `-x`, `+x` | unary |
|
|
671
|
+
| `( ... )` | parens |
|
|
672
|
+
| `1`, `1.5`, `.25` | decimals |
|
|
673
|
+
| `min(a, b)`, `max(a, b)`| variadic |
|
|
674
|
+
| `abs(x)` | |
|
|
675
|
+
| `floor(x)`, `ceil(x)`, `round(x)` | |
|
|
676
|
+
| `sqrt(x)` | |
|
|
677
|
+
| `pow(a, b)` | power |
|
|
678
|
+
| `sin(x)`, `cos(x)`, `tan(x)` | radians |
|
|
679
|
+
|
|
680
|
+
No comparison, conditional, bitwise, or string operators — keep it numeric.
|
|
681
|
+
|
|
682
|
+
### Scope variables
|
|
683
|
+
|
|
684
|
+
Each sequence has its own scope, computed at build time:
|
|
685
|
+
|
|
686
|
+
| Name | Meaning |
|
|
687
|
+
| --------- | ------------------------------------------------------------------------------------- |
|
|
688
|
+
| `w` | sequence intrinsic width (e.g. video natural width). 0 if not applicable. |
|
|
689
|
+
| `h` | sequence intrinsic height. |
|
|
690
|
+
| `W` | parent composition width (or root if no parent). |
|
|
691
|
+
| `H` | parent composition height. |
|
|
692
|
+
| `GW` | global (root) composition width. |
|
|
693
|
+
| `GH` | global (root) composition height. |
|
|
694
|
+
| `contain` | scale factor that makes the sequence fit inside the parent (preserve aspect, no crop) |
|
|
695
|
+
| `cover` | scale factor that makes the sequence cover the parent (preserve aspect, may crop) |
|
|
696
|
+
| `t` | sequence start time (the `at` after negative-`at` resolution), seconds |
|
|
697
|
+
| `d` | sequence duration, seconds |
|
|
698
|
+
| `T` | parent (or root) duration, seconds |
|
|
699
|
+
|
|
700
|
+
### Examples
|
|
701
|
+
|
|
702
|
+
```ts
|
|
703
|
+
// Center an image
|
|
704
|
+
initial: { x: 'GW/2 - w/2', y: 'GH/2 - h/2' }
|
|
705
|
+
|
|
706
|
+
// Fit a video without cropping
|
|
707
|
+
initial: { x: 0, y: 0, scale: 'contain' }
|
|
708
|
+
|
|
709
|
+
// Fill a video, may crop
|
|
710
|
+
initial: { x: 0, y: 0, scale: 'cover' }
|
|
711
|
+
|
|
712
|
+
// Responsive font size
|
|
713
|
+
style: { fontSize: 'min(GW, GH) * 0.06' }
|
|
714
|
+
|
|
715
|
+
// Subtitle 4% above bottom
|
|
716
|
+
initial: { x: 'GW/2', y: 'GH * 0.96', anchorX: 0.5, anchorY: 1 }
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
---
|
|
720
|
+
|
|
721
|
+
## Transitions
|
|
722
|
+
|
|
723
|
+
A composition can declare scene-to-scene `transitions` that compress paired keyframes into a single line and add visual effects (mask wipes, iris reveals) that aren't expressible at the keyframe level.
|
|
724
|
+
|
|
725
|
+
```ts
|
|
726
|
+
{
|
|
727
|
+
sequences: [
|
|
728
|
+
{ type: 'video', name: 'A', asset: 'a', at: 0, duration: 5 },
|
|
729
|
+
{ type: 'video', name: 'B', asset: 'b', at: 4, duration: 5 },
|
|
730
|
+
],
|
|
731
|
+
transitions: [
|
|
732
|
+
{ kind: 'crossfade', from: 'A', to: 'B', at: 4, duration: 1, ease: 'sine.inOut' },
|
|
733
|
+
],
|
|
734
|
+
}
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
Common fields (`TransitionCommon`):
|
|
738
|
+
|
|
739
|
+
| Field | Type | Notes |
|
|
740
|
+
| ---------- | ------- | -------------------------------------------------------------------------------------- |
|
|
741
|
+
| `from` | string | sibling sequence's `name`. Must exist in the same composition. |
|
|
742
|
+
| `to` | string | sibling sequence's `name`. Must be declared **after** `from` in `sequences[]`. |
|
|
743
|
+
| `at` | number | start of the transition, in the **parent composition's** time (like a sequence's `at`, not sequence-local); negative = back from the parent's end. |
|
|
744
|
+
| `duration` | number | seconds, must be > 0. |
|
|
745
|
+
| `ease` | string? | GSAP easing name. Default `'none'` (linear). |
|
|
746
|
+
|
|
747
|
+
Validation runs at composition build time. Errors throw with the offending `transitions[<index>]` quoted in the message: missing names, `to` before `from`, transition window outside either sequence's lifespan, duplicate use of one sequence as `from`, `from === to`, `duration <= 0`.
|
|
748
|
+
|
|
749
|
+
### `crossfade`
|
|
750
|
+
|
|
751
|
+
Alpha cross-dissolve. `from` fades to `alpha: 0`, `to` starts at `alpha: 0` and fades to `1`, both over `[at, at + duration]`.
|
|
752
|
+
|
|
753
|
+
```ts
|
|
754
|
+
{ kind: 'crossfade', from: 'A', to: 'B', at: 4, duration: 1, ease: 'sine.inOut' }
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
If `to` already has an explicit `initial.alpha` (other than 0), the expander throws — remove the manual setting.
|
|
758
|
+
|
|
759
|
+
### `wipe`
|
|
760
|
+
|
|
761
|
+
A directional reveal. `to` is masked by a soft edge that travels across the screen.
|
|
762
|
+
|
|
763
|
+
```ts
|
|
764
|
+
{
|
|
765
|
+
kind: 'wipe', from: 'A', to: 'B', at: 4, duration: 1,
|
|
766
|
+
direction: 'left' | 'right' | 'up' | 'down',
|
|
767
|
+
smoothing: 0.04, // 0..1 edge softness (default 0.02)
|
|
768
|
+
}
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
`direction` is the direction the wipe edge travels — `'left'` means the edge moves leftward across the canvas, exposing B starting from the right side. Mirror that for `'right'` / `'up'` / `'down'`.
|
|
772
|
+
|
|
773
|
+
### `iris`
|
|
774
|
+
|
|
775
|
+
A circular reveal centered on the canvas.
|
|
776
|
+
|
|
777
|
+
```ts
|
|
778
|
+
{
|
|
779
|
+
kind: 'iris', from: 'A', to: 'B', at: 4, duration: 1,
|
|
780
|
+
mode: 'in', // default — B opens up from a point. 'out' = A closes down to a point.
|
|
781
|
+
smoothing: 0.03, // 0..1 edge softness (default 0.02)
|
|
782
|
+
}
|
|
783
|
+
```
|
|
784
|
+
|
|
785
|
+
`mode: 'in'` (default): B emerges from the center and grows outward.
|
|
786
|
+
`mode: 'out'`: A disappears from the outside in, exposing B.
|
|
787
|
+
|
|
788
|
+
### `slide`
|
|
789
|
+
|
|
790
|
+
Both sequences slide together; the new scene comes in from the opposite side.
|
|
791
|
+
|
|
792
|
+
```ts
|
|
793
|
+
{
|
|
794
|
+
kind: 'slide', from: 'A', to: 'B', at: 4, duration: 1,
|
|
795
|
+
direction: 'left' | 'right' | 'up' | 'down',
|
|
796
|
+
}
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
`direction` is the direction of motion. `'left'` means A slides off to the left and B enters from the right.
|
|
800
|
+
|
|
801
|
+
The slide macro reads each sequence's existing `initial.x` / `initial.y` (if any) and treats it as the natural resting position. `B` is shifted off-screen by ±W or ±H from that position and slides back to it; `A` slides from its position to off-screen on the opposite side. So a centered text with `initial: { x: 'GW/2', anchorX: 0.5 }` ends the slide centered, not at `x: 0`.
|
|
802
|
+
|
|
803
|
+
If you've manually keyframed `x` / `y` on `A` or `B`, the slide expansion appends new keyframes alongside — your existing motion is not overwritten. Behavior with conflicting motion is the user's responsibility.
|
|
804
|
+
|
|
805
|
+
### `dip`
|
|
806
|
+
|
|
807
|
+
"Dip through": A fades out across the first half of the window and B fades in across the second half. The visible color during the dip is whatever sits behind A and B — set `Movie.background` (or place a persistent layer beneath them) for dip-to-black / dip-to-white / dip-to-color.
|
|
808
|
+
|
|
809
|
+
```ts
|
|
810
|
+
{ kind: 'dip', from: 'A', to: 'B', at: 4, duration: 1, ease: 'sine.inOut' }
|
|
811
|
+
```
|
|
812
|
+
|
|
813
|
+
If `to` already has a non-zero `initial.alpha`, the expander throws — remove the manual setting.
|
|
814
|
+
|
|
815
|
+
### `zoom`
|
|
816
|
+
|
|
817
|
+
A scaled punch-in / punch-out. By default `B` opens up: it starts large and zooms back to scale 1 while fading in; `A` simply fades. With `mode: 'out'` it's the opposite — `A` zooms outward as it fades, and `B` fades in at scale 1.
|
|
818
|
+
|
|
819
|
+
```ts
|
|
820
|
+
{
|
|
821
|
+
kind: 'zoom', from: 'A', to: 'B', at: 4, duration: 1,
|
|
822
|
+
mode: 'in', // default — B opens up. 'out' = A closes outward.
|
|
823
|
+
fromScale: 4, // starting scale of the zoomed sequence (default 4)
|
|
824
|
+
ease: 'power2.out',
|
|
825
|
+
}
|
|
826
|
+
```
|
|
827
|
+
|
|
828
|
+
### `dissolve`
|
|
829
|
+
|
|
830
|
+
Pixel-grain noise reveal driven by deterministic 2D Perlin noise. Pixels with a low noise value reveal first; as `uProgress` advances, more pixels reveal. The same `seed` always produces the same dissolve pattern, so a render is bit-exact reproducible.
|
|
831
|
+
|
|
832
|
+
```ts
|
|
833
|
+
{
|
|
834
|
+
kind: 'dissolve', from: 'A', to: 'B', at: 4, duration: 1,
|
|
835
|
+
scale: 30, // pattern frequency (higher = finer grain). Default 30.
|
|
836
|
+
seed: 0, // pattern offset. Different seeds → different reveal patterns.
|
|
837
|
+
smoothing: 0.05, // edge softness within each chunk. Default 0.05.
|
|
838
|
+
}
|
|
839
|
+
```
|
|
840
|
+
|
|
841
|
+
---
|
|
842
|
+
|
|
843
|
+
## Filters
|
|
844
|
+
|
|
845
|
+
> **`filterArea`** (on any layer) is a rectangle in the layer's **own** coordinates — origin = its local origin, e.g. a circle's centre — that widens the area a filter may draw into. Without it a blur or glow is clipped to the layer's bounding box: `filterArea: { x: -(r + 260), y: -(r + 260), width: 2 * (r + 260), height: 2 * (r + 260) }` for a circle of radius `r` blurred by up to 260 px. `threeD` layers add the filters' padding automatically.
|
|
846
|
+
|
|
847
|
+
Filters are named, ordered, and per-sequence. Animate parameters via `'filters.<name>.<param>'` keyframe paths.
|
|
848
|
+
|
|
849
|
+
### `chromaKey`
|
|
850
|
+
|
|
851
|
+
Removes a key color from the source. Works on video, image, or composition layers.
|
|
852
|
+
|
|
853
|
+
```ts
|
|
854
|
+
{
|
|
855
|
+
name: 'k',
|
|
856
|
+
type: 'chromaKey',
|
|
857
|
+
keyColor?: string | [number, number, number], // hex '#00ff00' or RGB 0..1; default green
|
|
858
|
+
threshold?: number, // default 0.4 — distance from key color counted as transparent
|
|
859
|
+
smoothing?: number, // default 0.1 — softness of the cutoff edge
|
|
860
|
+
spill?: number, // default 0.2 — green-tint suppression
|
|
861
|
+
}
|
|
862
|
+
```
|
|
863
|
+
|
|
864
|
+
Animatable: `threshold`, `smoothing`, `spill`. `keyColor` is set at build time.
|
|
865
|
+
|
|
866
|
+
### `custom`
|
|
867
|
+
|
|
868
|
+
Escape hatch for any PIXI `Filter` instance — including PIXI's own built-ins (`BlurFilter`, `ColorMatrixFilter`, `NoiseFilter`, etc.), [pixi-filters](https://github.com/pixijs/filters), community packages, or your own `Filter` subclass. The instance is used as-is; animation works the same way as for `chromaKey` as long as the filter has writable scalar properties at the addressed paths.
|
|
869
|
+
|
|
870
|
+
```ts
|
|
871
|
+
import { BlurFilter } from 'pixi.js';
|
|
872
|
+
import { GlowFilter, OldFilmFilter } from 'pixi-filters';
|
|
873
|
+
|
|
874
|
+
{
|
|
875
|
+
type: 'image',
|
|
876
|
+
asset: 'photo',
|
|
877
|
+
filters: [
|
|
878
|
+
{ type: 'custom', name: 'b', filter: new BlurFilter({ strength: 0 }) },
|
|
879
|
+
{ type: 'custom', name: 'glow', filter: new GlowFilter({ outerStrength: 1, color: 0xffaa00 }) },
|
|
880
|
+
{ type: 'custom', name: 'film', filter: new OldFilmFilter() },
|
|
881
|
+
],
|
|
882
|
+
keyframes: [
|
|
883
|
+
{ at: 1, to: { 'filters.b.strength': 8 }, duration: 0.5 },
|
|
884
|
+
{ at: 2, to: { 'filters.glow.outerStrength': 4 }, duration: 1 },
|
|
885
|
+
{ at: -0.5, to: { 'filters.film.noise': 0 }, duration: 0.5 },
|
|
886
|
+
],
|
|
887
|
+
}
|
|
888
|
+
```
|
|
889
|
+
|
|
890
|
+
Notes:
|
|
891
|
+
|
|
892
|
+
- `filter` must be a `Filter` instance (constructor must have run on the consumer side).
|
|
893
|
+
- Animation paths use scalar property setters. Filters whose properties are PointData (e.g. `pixi-filters` `RGBSplitFilter` exposes `red: { x, y }`) currently don't propagate the change to the GPU uniform when only `.x` is mutated — replace the whole point in an `onUpdate` callback or use a scalar-API filter instead.
|
|
894
|
+
- Without a `name`, the filter still applies but cannot be addressed via `filters.<name>.<prop>` keyframe paths.
|
|
895
|
+
|
|
896
|
+
Notes:
|
|
897
|
+
|
|
898
|
+
- `filter` must be a PIXI `Filter` instance (constructor must have run on the consumer side). Plain object literals throw.
|
|
899
|
+
- `pixi-filters` is **not** a dependency of pixi-effects — install it on your side if you want to use it.
|
|
900
|
+
- Without a `name`, the filter still applies but cannot be addressed via `filters.<name>.<prop>` keyframe paths.
|
|
901
|
+
|
|
902
|
+
---
|
|
903
|
+
|
|
904
|
+
## Presets
|
|
905
|
+
|
|
906
|
+
Presets are pure helpers that return a ready-to-use `SequenceSpec`. They expand into the existing keyframe / initial primitives — no engine surgery — so anything you can do with a preset you can also write by hand.
|
|
907
|
+
|
|
908
|
+
### `kenBurns`
|
|
909
|
+
|
|
910
|
+
Per-image motion preset for slideshows. Returns an `ImageSequenceSpec`; drop the result straight into `sequences[]`. Pair with `crossfade` / `dip` etc. transitions for the cuts between images — `kenBurns` itself emits no fade.
|
|
911
|
+
|
|
912
|
+
```ts
|
|
913
|
+
import { kenBurns } from 'pixi-effects';
|
|
914
|
+
|
|
915
|
+
sequences: [
|
|
916
|
+
kenBurns({ asset: 'photo1', name: 'p1', at: 0, duration: 6, motion: 'scale', origin: [0.25, 0.25], zoom: 1.2 }),
|
|
917
|
+
kenBurns({ asset: 'photo2', name: 'p2', at: 5, duration: 6, motion: 'rotation', angle: 6 }),
|
|
918
|
+
kenBurns({ asset: 'photo3', name: 'p3', at: 10, duration: 6, motion: 'position', from: [0, 0], to: [1, 1] }),
|
|
919
|
+
kenBurns({ asset: 'photo4', name: 'p4', at: 15, duration: 6, motion: 'still' }),
|
|
920
|
+
],
|
|
921
|
+
```
|
|
922
|
+
|
|
923
|
+
The image is centred on the canvas; `fit` (default `'cover'`) controls how the texture is scaled to fill. The fitted scale is computed at runtime from the texture's intrinsic size, so you don't pass `imageWidth` / `imageHeight`.
|
|
924
|
+
|
|
925
|
+
#### Common fields
|
|
926
|
+
|
|
927
|
+
| Field | Type | Notes |
|
|
928
|
+
| ---------- | --------------------- | ------------------------------------------------------------------------------------ |
|
|
929
|
+
| `asset` | string | image asset name (registered via `Movie.init({ assets })`) |
|
|
930
|
+
| `duration` | number | seconds of animation (required) |
|
|
931
|
+
| `name?` | string | sequence name so transitions can reference it |
|
|
932
|
+
| `at?` | number | start time, parent-relative seconds |
|
|
933
|
+
| `fit?` | `'cover'` \| `'contain'` | how the texture fills the canvas. Default `'cover'`. |
|
|
934
|
+
| `ease?` | string | GSAP easing name. Default `'sine.inOut'`. |
|
|
935
|
+
|
|
936
|
+
#### `motion: 'still'`
|
|
937
|
+
|
|
938
|
+
Image sits at the canvas centre, fitted but unanimated. Useful as a stable "rest" in between motion-heavy frames.
|
|
939
|
+
|
|
940
|
+
#### `motion: 'scale'`
|
|
941
|
+
|
|
942
|
+
Zoom in or out around an arbitrary 9-point pivot.
|
|
943
|
+
|
|
944
|
+
| Field | Type | Notes |
|
|
945
|
+
| ------------ | -------------------------- | ------------------------------------------------------------------------------------------- |
|
|
946
|
+
| `origin?` | `[number, number]` | pivot in [0..1] image coords. Default `[0.5, 0.5]` (centre). The original convention uses the 9-point grid `0.25 / 0.5 / 0.75`. |
|
|
947
|
+
| `zoom?` | number | zoom factor relative to the fitted base. Default `1.15`. |
|
|
948
|
+
| `direction?` | `'in'` \| `'out'` | `'in'`: 1 → zoom (default). `'out'`: zoom → 1. |
|
|
949
|
+
|
|
950
|
+
The pivot point stays pinned at its world position; the rest of the image grows / shrinks around it. This is the "focal-point" zoom you want for ken-burns slideshows — the eye anchors on the pivot while the surrounding pixels move.
|
|
951
|
+
|
|
952
|
+
#### `motion: 'rotation'`
|
|
953
|
+
|
|
954
|
+
Gentle rotation while keeping the image filling the canvas.
|
|
955
|
+
|
|
956
|
+
| Field | Type | Notes |
|
|
957
|
+
| ------------ | ----------------------------------- | ------------------------------------------------------------------------------------------- |
|
|
958
|
+
| `angle?` | number | total rotation in degrees. Default `8`. Capped at 30. |
|
|
959
|
+
| `direction?` | `'cw'` \| `'ccw'` \| `'through'` | `'cw'` (default) = 0 → +angle. `'ccw'` = 0 → −angle. `'through'` = −angle/2 → +angle/2. |
|
|
960
|
+
|
|
961
|
+
The scale is over-set so the rotated bounding box still covers the canvas — no background gaps as the image tilts.
|
|
962
|
+
|
|
963
|
+
#### `motion: 'position'`
|
|
964
|
+
|
|
965
|
+
Pan the image between two points within its over-scaled bounds.
|
|
966
|
+
|
|
967
|
+
| Field | Type | Notes |
|
|
968
|
+
| ------- | ------------------- | -------------------------------------------------------------------------------------- |
|
|
969
|
+
| `from?` | `[number, number]` | start position in [0..1] of the over-scaled bounds. Default `[0.25, 0.25]`. |
|
|
970
|
+
| `to?` | `[number, number]` | end position. Default `[0.75, 0.75]`. |
|
|
971
|
+
| `zoom?` | number | over-scale factor (must be > 1 for any pan to be visible). Default `1.15`. |
|
|
972
|
+
|
|
973
|
+
`[0, 0]` looks at the top-left of the image; `[1, 1]` looks at the bottom-right. The default pans diagonally across the upper-left and lower-right quarters of the over-scaled image (matches the Yajima-Motion preset).
|
|
974
|
+
|
|
975
|
+
### `orbit`
|
|
976
|
+
|
|
977
|
+
A camera that circles a point. There is no per-frame expression in the DSL, so the circle is sampled into short linear keyframes (one per 0.1 s; the easing is applied to the angle). Returns a `camera` layer for `sequences[]`.
|
|
978
|
+
|
|
979
|
+
```ts
|
|
980
|
+
import { orbit } from 'pixi-effects';
|
|
981
|
+
|
|
982
|
+
sequences: [
|
|
983
|
+
orbit({ duration: 6, degrees: 40 }), // ±20° around the centre of a 1280×720 canvas
|
|
984
|
+
// …threeD layers…
|
|
985
|
+
]
|
|
986
|
+
```
|
|
987
|
+
|
|
988
|
+
| Option | Notes |
|
|
989
|
+
|---|---|
|
|
990
|
+
| `duration`, `degrees` | required. `degrees` is the total sweep (negative = the other way) |
|
|
991
|
+
| `start` | start angle in degrees; default `-degrees / 2` (0 = straight in front of the centre) |
|
|
992
|
+
| `radius` | distance from the centre; default = the default camera distance for `height` and `fov`, so `z = 0` is 1:1 at angle 0 |
|
|
993
|
+
| `center` | `[x, y]` to circle and look at; default the canvas centre |
|
|
994
|
+
| `width`, `height` | canvas size for the defaults (1280 × 720) |
|
|
995
|
+
| `fov`, `ease`, `at`, `name`, `stepsPerSecond` | `ease` default `'sine.inOut'` |
|
|
996
|
+
|
|
997
|
+
`z` is specified, so it stops following `fov`; for an orbit **plus** a dolly zoom write your own `fov` and `z = (H/2) / tan(fov/2)` keyframes.
|
|
998
|
+
|
|
999
|
+
### `withFade`
|
|
1000
|
+
|
|
1001
|
+
Spec helper that adds alpha fade-in / fade-out to **any** sequence spec. Mutates and returns the spec, so it composes with other presets (`kenBurns`, …) and drops straight into `sequences[]`.
|
|
1002
|
+
|
|
1003
|
+
```ts
|
|
1004
|
+
import { kenBurns, withFade } from 'pixi-effects';
|
|
1005
|
+
|
|
1006
|
+
sequences: [
|
|
1007
|
+
withFade(kenBurns({ asset: 'photo', at: 0, duration: 6, motion: 'scale' }), { in: 0.5, out: 0.5 }),
|
|
1008
|
+
withFade({ type: 'text', text: 'hi', at: 5, duration: 3, initial: { x: 'GW/2' } }, { in: 0.4 }),
|
|
1009
|
+
]
|
|
1010
|
+
```
|
|
1011
|
+
|
|
1012
|
+
| Option | Type | Notes |
|
|
1013
|
+
| ------ | ------ | --------------------------------------------------------------------------------------------- |
|
|
1014
|
+
| `in?` | number | fade-in length in seconds, at the start of the sequence. Sets `initial.alpha = 0`. |
|
|
1015
|
+
| `out?` | number | fade-out length in seconds, anchored to `at + duration`. **Requires `duration` on the spec** (throws otherwise). |
|
|
1016
|
+
|
|
1017
|
+
The added keyframes are layered on top of any keyframes the spec already has. `withFade` is available since `0.2.0`.
|