pixi-effects 0.25.0 → 0.26.1
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 +22 -0
- package/README.md +13 -13
- package/ai/CHAT.md +2 -2
- package/ai/chat-template.html +3 -3
- package/ai/tools/check.mjs +15 -3
- package/ai/tools/pixi-effects.mjs +48 -0
- package/ai/tools/render.mjs +3 -3
- package/ai/tools/view.mjs +3 -3
- package/dist/{Base-BgnRXbtJ.d.ts → Base-CpdDs4HT.d.ts} +1 -1
- package/dist/{Base-DHVKqA46.d.cts → Base-TbBhHPrG.d.cts} +1 -1
- package/dist/Composition-THQPG4E5.cjs +16 -0
- package/dist/{Composition-2GJSCHPP.cjs.map → Composition-THQPG4E5.cjs.map} +1 -1
- package/dist/Composition-Z2VY3KWZ.js +7 -0
- package/dist/{Composition-C4FSWJZF.js.map → Composition-Z2VY3KWZ.js.map} +1 -1
- package/dist/Controller.d.cts +2 -2
- package/dist/Controller.d.ts +2 -2
- package/dist/{Movie-FM5m67V0.d.ts → Movie-DVeDTdnc.d.ts} +25 -2
- package/dist/{Movie-CTpN1wYL.d.cts → Movie-i05Oh9mJ.d.cts} +25 -2
- package/dist/Presenter.d.cts +2 -2
- package/dist/Presenter.d.ts +2 -2
- package/dist/{chunk-DP37TTMO.cjs → chunk-EABL3UKJ.cjs} +5 -3
- package/dist/chunk-EABL3UKJ.cjs.map +1 -0
- package/dist/{chunk-SY27HZC5.cjs → chunk-H3RHEH5W.cjs} +59 -59
- package/dist/{chunk-SY27HZC5.cjs.map → chunk-H3RHEH5W.cjs.map} +1 -1
- package/dist/{chunk-PRZ4U6JN.js → chunk-JHJNEIDI.js} +5 -3
- package/dist/chunk-JHJNEIDI.js.map +1 -0
- package/dist/{chunk-XS6GSJCN.js → chunk-YEVAUQ6Y.js} +3 -3
- package/dist/{chunk-XS6GSJCN.js.map → chunk-YEVAUQ6Y.js.map} +1 -1
- package/dist/index.cjs +193 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +169 -8
- package/dist/index.js.map +1 -1
- package/dist/loader.css +35 -2
- 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-CoT0ypcx.d.cts → types-DSGwN5w1.d.cts} +2 -0
- package/dist/{types-CoT0ypcx.d.ts → types-DSGwN5w1.d.ts} +2 -0
- package/docs/api.md +4 -3
- package/docs/dsl.md +2 -2
- package/llms-full.txt +34 -25
- package/llms.txt +6 -5
- package/package.json +3 -3
- package/{ai → skills/pixi-effects}/SKILL.md +11 -9
- package/{ai → skills/pixi-effects}/reference/cheatsheet.md +3 -3
- package/{ai → skills/pixi-effects}/reference/pitfalls.md +9 -3
- package/{ai → skills/pixi-effects}/reference/recipes.md +2 -2
- package/{ai/tools → skills/pixi-effects/scripts}/save-image.py +1 -1
- package/{ai → skills/pixi-effects}/template.html +7 -6
- package/dist/Composition-2GJSCHPP.cjs +0 -16
- package/dist/Composition-C4FSWJZF.js +0 -7
- package/dist/chunk-DP37TTMO.cjs.map +0 -1
- package/dist/chunk-PRZ4U6JN.js.map +0 -1
package/dist/loader.css
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* pixi-effects loader: one line of HTML on the page, a few lines of CSS, no JavaScript of your own.
|
|
3
3
|
*
|
|
4
|
-
* <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
4
|
+
* <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/pixi-effects@0.26.1/dist/loader.css">
|
|
5
5
|
* <div class="stage" style="position: relative"> <!-- any box the canvas sits in, with position: relative -->
|
|
6
6
|
* <canvas id="stage" width="1280" height="720"></canvas>
|
|
7
7
|
* <div class="pe-loader" data-label="LOADING"></div> <!-- or class="pe-loader pe-loader--bar" -->
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
*
|
|
16
16
|
* Looks: .pe-loader (a ring) · .pe-loader--pulse (a breathing dot) · .pe-loader--bar (a sweeping line along the bottom)
|
|
17
17
|
* .pe-loader--custom: no built-in drawing; put your own markup inside (animate only transform / opacity there too).
|
|
18
|
+
* Progress: movie.init() also writes how far it is (--pe-progress 0..1, data-stage, data-percent): a bar along the bottom and the label
|
|
19
|
+
* "BUILDING LAYERS 62%" appear by themselves; the same numbers come as the movie's `loadprogress` event.
|
|
18
20
|
* Restyle: --pe-loader-color, --pe-loader-bg, --pe-loader-track, --pe-loader-size (set them on the element or on any ancestor).
|
|
19
21
|
*/
|
|
20
22
|
.pe-loader {
|
|
@@ -80,7 +82,37 @@
|
|
|
80
82
|
}
|
|
81
83
|
.pe-loader--bar[data-label]::after { top: calc(50% - 6px); }
|
|
82
84
|
|
|
83
|
-
/*
|
|
85
|
+
/* how far init is: a thin bar along the bottom edge and a label of the stage and the percent. The bar is scaled by --pe-progress (a number the library
|
|
86
|
+
writes; the browser eases between the steps), so it moves on the compositor like the rest and keeps its look while the page is busy. */
|
|
87
|
+
.pe-loader__bar {
|
|
88
|
+
position: absolute;
|
|
89
|
+
left: 0;
|
|
90
|
+
right: 0;
|
|
91
|
+
bottom: 0;
|
|
92
|
+
height: 3px;
|
|
93
|
+
background: var(--pe-loader-color, #ffffff);
|
|
94
|
+
transform-origin: 0 50%;
|
|
95
|
+
transform: scaleX(var(--pe-progress, 0));
|
|
96
|
+
transition: transform 0.35s linear;
|
|
97
|
+
}
|
|
98
|
+
.pe-loader[data-stage]::after {
|
|
99
|
+
content: attr(data-stage) ' ' attr(data-percent) '%';
|
|
100
|
+
position: absolute;
|
|
101
|
+
left: 0;
|
|
102
|
+
right: 0;
|
|
103
|
+
top: calc(50% + var(--pe-loader-size, 34px) / 2 + 18px);
|
|
104
|
+
padding-left: 0.4em;
|
|
105
|
+
text-align: center;
|
|
106
|
+
font: 600 11px/1 system-ui, -apple-system, 'Helvetica Neue', Arial, sans-serif;
|
|
107
|
+
letter-spacing: 0.4em;
|
|
108
|
+
opacity: 0.7;
|
|
109
|
+
animation: none;
|
|
110
|
+
}
|
|
111
|
+
.pe-loader--bar[data-stage]::before { display: none; }
|
|
112
|
+
.pe-loader--bar[data-stage]::after { top: 50%; }
|
|
113
|
+
|
|
114
|
+
/* your own drawing inside the box (the built-in bar and label stay out of it; --pe-progress is still there for your own markup) */
|
|
115
|
+
.pe-loader--custom .pe-loader__bar { display: none; }
|
|
84
116
|
.pe-loader--custom::before,
|
|
85
117
|
.pe-loader--custom::after { content: none; display: none; }
|
|
86
118
|
|
|
@@ -104,6 +136,7 @@
|
|
|
104
136
|
}
|
|
105
137
|
|
|
106
138
|
@media (prefers-reduced-motion: reduce) {
|
|
139
|
+
.pe-loader__bar { transition: none; }
|
|
107
140
|
.pe-loader::before { animation-duration: 3s; }
|
|
108
141
|
.pe-loader--pulse::before, .pe-loader--bar::before { animation-duration: 3.2s; }
|
|
109
142
|
.pe-loader[data-label]::after { animation: none; opacity: 0.6; }
|
package/dist/three.cjs
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var chunkEABL3UKJ_cjs = require('./chunk-EABL3UKJ.cjs');
|
|
4
4
|
require('./chunk-ECA2WNRX.cjs');
|
|
5
5
|
require('./chunk-TWZNOUM7.cjs');
|
|
6
6
|
var pixi_js = require('pixi.js');
|
|
7
7
|
var three$1 = require('three');
|
|
8
8
|
|
|
9
|
-
var ThreeSequence = class extends
|
|
9
|
+
var ThreeSequence = class extends chunkEABL3UKJ_cjs.Sequence {
|
|
10
10
|
_renderer = null;
|
|
11
11
|
_scene = null;
|
|
12
12
|
_camera = null;
|
|
@@ -25,7 +25,7 @@ var ThreeSequence = class extends chunkDP37TTMO_cjs.Sequence {
|
|
|
25
25
|
if (this.duration === void 0) {
|
|
26
26
|
this.duration = this.parent?.duration ?? this.root.duration;
|
|
27
27
|
}
|
|
28
|
-
const scope =
|
|
28
|
+
const scope = chunkEABL3UKJ_cjs.buildScope(this, this.parent, this.root);
|
|
29
29
|
const width = resolveDim(spec.width, scope) ?? this.parent?.width ?? this.root.width;
|
|
30
30
|
const height = resolveDim(spec.height, scope) ?? this.parent?.height ?? this.root.height;
|
|
31
31
|
const resolution = spec.resolution ?? 1;
|
|
@@ -145,12 +145,12 @@ var ThreeSequence = class extends chunkDP37TTMO_cjs.Sequence {
|
|
|
145
145
|
function resolveDim(v, scope) {
|
|
146
146
|
if (v === void 0) return void 0;
|
|
147
147
|
if (typeof v === "number") return v;
|
|
148
|
-
return
|
|
148
|
+
return chunkEABL3UKJ_cjs.evaluateExpr(v, scope);
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
// src/three/index.ts
|
|
152
152
|
function registerThree() {
|
|
153
|
-
|
|
153
|
+
chunkEABL3UKJ_cjs.registerSequenceType("three", ThreeSequence);
|
|
154
154
|
}
|
|
155
155
|
function three(spec) {
|
|
156
156
|
return spec;
|
package/dist/three.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { d as SequenceCommon, O as PropValue, S as SequenceSpec } from './types-
|
|
1
|
+
import { d as SequenceCommon, O as PropValue, S as SequenceSpec } from './types-DSGwN5w1.cjs';
|
|
2
2
|
import { Scene, Camera, WebGLRenderer } from 'three';
|
|
3
|
-
import { S as Sequence, P as PathRouters } from './Base-
|
|
3
|
+
import { S as Sequence, P as PathRouters } from './Base-TbBhHPrG.cjs';
|
|
4
4
|
import 'pixi.js';
|
|
5
5
|
import 'gsap';
|
|
6
6
|
|
package/dist/three.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { d as SequenceCommon, O as PropValue, S as SequenceSpec } from './types-
|
|
1
|
+
import { d as SequenceCommon, O as PropValue, S as SequenceSpec } from './types-DSGwN5w1.js';
|
|
2
2
|
import { Scene, Camera, WebGLRenderer } from 'three';
|
|
3
|
-
import { S as Sequence, P as PathRouters } from './Base-
|
|
3
|
+
import { S as Sequence, P as PathRouters } from './Base-CpdDs4HT.js';
|
|
4
4
|
import 'pixi.js';
|
|
5
5
|
import 'gsap';
|
|
6
6
|
|
package/dist/three.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Sequence, buildScope, evaluateExpr, registerSequenceType } from './chunk-
|
|
1
|
+
import { Sequence, buildScope, evaluateExpr, registerSequenceType } from './chunk-JHJNEIDI.js';
|
|
2
2
|
import './chunk-TLYTYBWX.js';
|
|
3
3
|
import './chunk-BXOCEZ4U.js';
|
|
4
4
|
import { Texture, Sprite } from 'pixi.js';
|
|
@@ -710,6 +710,8 @@ interface CompositionShape {
|
|
|
710
710
|
duration: number;
|
|
711
711
|
/** The movie's frame rate, on the root shape only (a shader layer's `iFrame` needs it). */
|
|
712
712
|
frameRate?: number;
|
|
713
|
+
/** Called after each layer is built (the root shape only): feeds `loadprogress` and lets the page paint now and then. */
|
|
714
|
+
onLayerBuilt?: () => void | Promise<void>;
|
|
713
715
|
}
|
|
714
716
|
/** Audio descriptor pushed to the mixdown queue by AudioSequence/VideoSequence. */
|
|
715
717
|
interface AudioDescriptor {
|
|
@@ -710,6 +710,8 @@ interface CompositionShape {
|
|
|
710
710
|
duration: number;
|
|
711
711
|
/** The movie's frame rate, on the root shape only (a shader layer's `iFrame` needs it). */
|
|
712
712
|
frameRate?: number;
|
|
713
|
+
/** Called after each layer is built (the root shape only): feeds `loadprogress` and lets the page paint now and then. */
|
|
714
|
+
onLayerBuilt?: () => void | Promise<void>;
|
|
713
715
|
}
|
|
714
716
|
/** Audio descriptor pushed to the mixdown queue by AudioSequence/VideoSequence. */
|
|
715
717
|
interface AudioDescriptor {
|
package/docs/api.md
CHANGED
|
@@ -45,7 +45,7 @@ interface MovieOptions {
|
|
|
45
45
|
composition?: CompositionSpec; // root composition (see DSL reference)
|
|
46
46
|
poster?: number; // seconds: the frame shown before play (negative: from the end), see movie.poster
|
|
47
47
|
motionBlur?: boolean | number | { samples?: number; shutter?: number }; // blur for render() / snapshot() / contactSheet(), not live playback; see Motion blur
|
|
48
|
-
loader?: HTMLElement | string | false; // the page's loader (see pixi-effects/loader.css): faded out when ready, stopped if init fails; default: a .pe-loader next to the canvas
|
|
48
|
+
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
|
|
49
49
|
}
|
|
50
50
|
```
|
|
51
51
|
|
|
@@ -223,6 +223,7 @@ movie.off(event, fn): this
|
|
|
223
223
|
| `'play'` | none | when `play()` actually transitions from paused |
|
|
224
224
|
| `'pause'` | none | when `pause()` actually transitions from playing |
|
|
225
225
|
| `'progress'`| `{ progress: number; frame: number; totalFrames: number }` | during `render()`, once per encoded frame |
|
|
226
|
+
| `'loadprogress'` | `{ stage: 'assets' \| 'build' \| 'sound' \| 'frames'; loaded: number; total: number; progress: number }` | during `init()`, after each asset and each layer built, once the sound is mixed; `progress` is 0 to 1 for the whole init. The page's `.pe-loader` already shows it; `movie.loadStages` gives the milliseconds per stage afterwards |
|
|
226
227
|
| `'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()` |
|
|
227
228
|
| `'stop'` | `{ index: number; stop: Stop; pageIndex: number }` | a presentation is on a stop: `next()` played to it, or `prev()` / `goToStop()` / `goToPage()` jumped to it |
|
|
228
229
|
| `'ended'` | none | playback ran off the end (after `'pause'`); pausing by hand does not emit it |
|
|
@@ -301,7 +302,7 @@ const pages = await movie.stopImages({ as: 'dataURL', type: 'image/jpeg', scale:
|
|
|
301
302
|
const pdf = await movie.exportPDF({ title: 'My talk' }); // a Blob: one PDF page per page of the deck (JPEG at the canvas size)
|
|
302
303
|
```
|
|
303
304
|
|
|
304
|
-
Both pause the movie and leave the playhead where it was; a movie with no stops returns an empty list / throws (a PDF needs pages). The picture of a page is, in order: its last stop flagged `pdf: true`, else its last (with `pick: 'first'`, first) stop that is not `pdf: false`; a page whose stops are all `pdf: false` is left out of a PDF (a page list still shows it by its last stop); `which: 'stops'` makes a page of every stop that is not `pdf: false`. `exportPDF` options: `which`, `pick`, `scale`, `quality` (JPEG, default 0.92), `title`, `motionBlur`, `onImage`. A transparent background comes out black in a JPEG: give the movie a `background`. From a script, `npx pixi-effects
|
|
305
|
+
Both pause the movie and leave the playhead where it was; a movie with no stops returns an empty list / throws (a PDF needs pages). The picture of a page is, in order: its last stop flagged `pdf: true`, else its last (with `pick: 'first'`, first) stop that is not `pdf: false`; a page whose stops are all `pdf: false` is left out of a PDF (a page list still shows it by its last stop); `which: 'stops'` makes a page of every stop that is not `pdf: false`. `exportPDF` options: `which`, `pick`, `scale`, `quality` (JPEG, default 0.92), `title`, `motionBlur`, `onImage`. A transparent background comes out black in a JPEG: give the movie a `background`. From a script, `npx pixi-effects render my-talk.html -o my-talk.pdf` (add `--all-stops` for a page per stop).
|
|
305
306
|
|
|
306
307
|
### Audio-reactive: `audioEnvelope()`, `react()`, `bpmEnvelope()`
|
|
307
308
|
|
|
@@ -372,7 +373,7 @@ el('play').onclick = () => (movie.isPlaying ? movie.pause() : movie.play());
|
|
|
372
373
|
el('seek').oninput = e => movie.gotoFrame(+e.target.value);
|
|
373
374
|
```
|
|
374
375
|
|
|
375
|
-
What you have: `play()`, `pause()`, `gotoFrame(frame)`, `volume`, `muted` / `toggleMute()`; the state `currentFrame`, `totalFrames`, `frameRate`, `duration`, `isPlaying`, `isReady
|
|
376
|
+
What you have: `play()`, `pause()`, `gotoFrame(frame)`, `volume`, `muted` / `toggleMute()`; the state `currentFrame`, `totalFrames`, `frameRate`, `duration`, `isPlaying`, `isReady`, `loadStages` (milliseconds `init()` spent in `assets` / `build` / `sound` / `frames`), `audioBlocked` (true while the browser keeps the movie's sound silent until a tap: show "tap for sound"; a tap or key press that arrives while `play()` is pending, or the next `play()` made from a tap, starts it); the events above; `render()` for a download button; `snapshot()` for a thumbnail; `timelineData()` / `timelineSvg()` for a timeline like `pixi-effects-view`'s. Keep the movie's `canvas` wherever your layout wants it (the page's CSS sizes it). To only change how `Controller` looks, see **Theme** below.
|
|
376
377
|
|
|
377
378
|
## `Controller`
|
|
378
379
|
|
package/docs/dsl.md
CHANGED
|
@@ -1290,7 +1290,7 @@ A filter by name, as plain data. `type` is the filter's class name without `Filt
|
|
|
1290
1290
|
keyframes: [{ at: 0, to: { 'filters.halo.outerStrength': 6 }, duration: 1 }] } // animate any scalar option by name
|
|
1291
1291
|
```
|
|
1292
1292
|
|
|
1293
|
-
Name a filter (`name: 'halo'`) to animate it with `'filters.halo.<option>'`. `blur`, `noise`, `alpha`, `colorMatrix` and `grain` need no extra package (`grain` is this library's own). All the others come from the **`pixi-filters`** package, which is loaded the first time a layer uses one (add `"pixi-filters": "https://esm.sh/pixi-filters@6.1.5?external=pixi.js"` to the import map, or `npm i pixi-filters`; `
|
|
1293
|
+
Name a filter (`name: 'halo'`) to animate it with `'filters.halo.<option>'`. `blur`, `noise`, `alpha`, `colorMatrix` and `grain` need no extra package (`grain` is this library's own). All the others come from the **`pixi-filters`** package, which is loaded the first time a layer uses one (add `"pixi-filters": "https://esm.sh/pixi-filters@6.1.5?external=pixi.js"` to the import map, or `npm i pixi-filters`; `skills/pixi-effects/template.html` already has it). A misspelt type says what you probably meant. Rendering and exporting were verified in a real browser for 40 of them (`examples/_checks/named-filters.html` and `named-filters-2.html`); `colorMap` and `simpleLightmap` need a texture, which JSON cannot hold, so use `custom` for those two:
|
|
1294
1294
|
|
|
1295
1295
|
| `type` | Options (the useful ones) | What it does |
|
|
1296
1296
|
|---|---|---|
|
|
@@ -1553,7 +1553,7 @@ import { cameraPath, wiggle } from 'pixi-effects';
|
|
|
1553
1553
|
| `from` / `to` | fractions 0–1 of the route to start and stop at (default 0 → 1; `from: 1, to: 0` flies it backwards) |
|
|
1554
1554
|
| `frameRate` | samples per second: your movie's `frameRate` (default 30) |
|
|
1555
1555
|
|
|
1556
|
-
Layers the camera passes take `hideBehindCamera: true`. The recipe `camera-fly-through` in `
|
|
1556
|
+
Layers the camera passes take `hideBehindCamera: true`. The recipe `camera-fly-through` in `skills/pixi-effects/reference/recipes.md` is a whole corridor.
|
|
1557
1557
|
|
|
1558
1558
|
### `followPath`
|
|
1559
1559
|
|
package/llms-full.txt
CHANGED
|
@@ -4,18 +4,20 @@ Concatenation of the skill, cheatsheet, pitfalls, recipes and the DSL / API refe
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
<!-- =====
|
|
7
|
+
<!-- ===== skills/pixi-effects/SKILL.md ===== -->
|
|
8
|
+
|
|
9
|
+
> Written for pixi-effects 0.26.1. Update this skill with `npx skills update`; the newest docs are at https://yjmtmtk.github.io/pixi-effects/
|
|
8
10
|
|
|
9
11
|
# pixi-effects — write a video as data
|
|
10
12
|
|
|
11
|
-
You describe a video as a tree of plain objects; the library plays it in a canvas and exports it (MP4/WebM/MOV). You never write per-frame code. **Read `reference/cheatsheet.md` once before writing anything
|
|
13
|
+
You describe a video as a tree of plain objects; the library plays it in a canvas and exports it (MP4/WebM/MOV). You never write per-frame code. **Read `reference/cheatsheet.md` once before writing anything** (about 35 KB: conventions, layer types, keyframes). `reference/recipes.md` (67 KB) is an index of tested recipes: `grep -n "^## " reference/recipes.md`, then read only the one that fits (a title, a lower third, a chart, a kinetic-type line, a 2.5D scene …). `reference/pitfalls.md` (41 KB, numbered) is for when a warning or the picture surprises you: search it for the word, do not read it end to end. A simple title needs the cheatsheet and one recipe, nothing more.
|
|
12
14
|
|
|
13
15
|
## Workflow
|
|
14
16
|
|
|
15
17
|
1. **Start from `template.html`** (copy it; it also has the shared loader (`<div class="pe-loader" data-label="LOADING">` + `pixi-effects/loader.css`: HTML and CSS only, animating just `transform` / `opacity`, so it keeps moving while the page builds the video; `movie.init()` fades it out). Keep it; a loader of your own is fine (`pe-loader--custom`) as long as it only animates `transform` / `opacity`, never with JavaScript or `requestAnimationFrame`; it loads the library from a CDN, collects every console warning into `window.__logs`, and exposes `window.movie`). One HTML file, plain JS. Default canvas 1280×720 @ 30 fps.
|
|
16
18
|
2. **Give the movie a poster time.** In `movie.init` add `poster: <seconds>`: the most striking moment (a title fully on screen, not the first frame, which is often black). The canvas shows that picture before play (the playhead stays at 0 and play starts from 0), and the check writes it as `poster.jpg`.
|
|
17
19
|
3. **Write the composition as data.** Anything repeated (letters, bars, particles, scenes) is a JS function or loop that returns specs — never copy-paste objects. Derive coordinates and times from constants/data, not magic numbers.
|
|
18
|
-
4. **Show the human:** `
|
|
20
|
+
4. **Show the human:** `npx pixi-effects view my-video.html` opens their browser on the page with a scrubbable, zoomable timeline under it (click / drag to seek, + / − or Ctrl/⌘ + wheel to zoom); it keeps running until Ctrl-C. **Deliver the file:** `npx pixi-effects render my-video.html -o my-video.mp4` renders the page headless to mp4 / webm / mov / mkv and exits 0 only when the file is written; run it AFTER the check below passes. **Run the check — one command does steps 3, 4 and the export:** `npx pixi-effects check my-video.html` (Node ≥ 22 and Chrome installed). **If `npx` says command not found, or prints nothing** (your npm may hold back a release that is a few days old, or an old release is cached): `npm i pixi-effects`, then `node node_modules/pixi-effects/ai/tools/check.mjs my-video.html` (the same for `render.mjs` and `view.mjs`). It starts its own private Chrome (nothing is shared with other sessions), waits for `window.__ready`, and writes `timeline.html` (every layer as a bar on a time axis: ◆ keyframes, shaded transitions; hand it to the human to open) and prints: console warnings, `movie.inspect` over the WHOLE timeline (grouped), `movie.inspectAudio()`, then exports an mp4 and decodes it again (video size and length, audio length, loudness per second, silence). It writes `check-out/<name>/sheet.png` — **open that picture and look at it** — plus the mp4 and `report.json`. Exit 0 = nothing to fix; exit 1 = a list of problems; fix them and run it again. A key a layer does not have (a typo in a layer key, a keyframe property or a text style key; `anchorX` outside `initial`; `pitch` outside `sfx`) is a warning that names the key and what you probably meant: treat it like any other warning. Text that overlaps text is listed under `review` (not a failure: ghost layers, glow copies and per-letter boxes are often intentional; look at the sheet; `--strict` makes them fail). The manual steps below are for looking at one frame closely. The same judgement is in the page: `await movie.review({ at: 'title@end' })` returns `{ frames, problems, review, fonts, audio, at }` (what `check` prints, as data) and `movie.resolveAt('3.5, title@end')` turns moments into frames.
|
|
19
21
|
**Or by hand: run it and read the logs first.** `window.__ready` must be `true` and `window.__logs` should be `[]`. A pixi-effects warning is an instruction: fix what it names.
|
|
20
22
|
5. **Look at it** — you cannot judge motion from code. The library has the tools built in:
|
|
21
23
|
- `await movie.contactSheet({ count: 6, as: 'dataURL' })` → ONE image of several labelled frames (pass `frames: [...]` / `times: [...]` to choose; include the middle of every transition and the last second). Save it and view it.
|
|
@@ -24,7 +26,7 @@ You describe a video as a tree of plain objects; the library plays it in a canva
|
|
|
24
26
|
- `movie.inspectAudio()` → **hear with numbers**: for every sound its `start`, `sound.length`, `sound.peakDb`, `sound.loudestAt` (line this up with the picture) and `sound.brightnessHz` (< 500 dark, > 8000 airy), plus `issues` (limited mix, inaudible or cut-off sounds). Check it whenever the movie has audio; `render()` encodes this same mix.
|
|
25
27
|
- **A moment, a movement, the sound (`pixi-effects-check` options):** `--at 3.5,title@end,50%,f120` writes those pictures to `check-out/<name>/frames/*.png` (and `at.png`): the last frame of a layer is `name@end`, the middle `name@mid`. `--onion 1:3` writes `onion.png`, eight frames of seconds 1–3 overlaid (the later the stronger): a trail that shows a movement's path and easing (even steps = constant speed, bunched = slow). `report.audio` now has `loudness` (`integratedLufs`, `truePeakDb`, `clippedSamples`, `silences`), `scenes` (per named top-level composition), `cues` (when each sound starts) and `notes` (advice: quiet or loud for web video, little headroom, silent stretches; web video is typically −14 to −16 LUFS); `waveform.png` draws the mix with the scene edges and the cues. A web font that failed to load fails the check; a text layer none of whose fonts is installed is listed for review.
|
|
26
28
|
- **A deck** (`composition.stops`, `deck()`): `await movie.contactSheet({ frames: movie.stops.map(s => s.frame), as: 'dataURL' })` is one picture of every stop (the check writes it as `stops.png`); what the audience reads while a page waits is that picture, so check every one. `await movie.inspectStops()` lists the stops where the picture is still changing (a stop landing before its animation ends); the check prints it as a review list: move the stop later or flag a settled one `pdf: true`. `await movie.stopImages({ which: 'pages' })` / `movie.exportPDF()` give the pages themselves.
|
|
27
|
-
From a browser tool the result is a `data:` URL string: `agent-browser eval "movie.contactSheet({ count: 6, as: 'dataURL' })" | python3
|
|
29
|
+
From a browser tool the result is a `data:` URL string: `agent-browser eval "movie.contactSheet({ count: 6, as: 'dataURL' })" | python3 scripts/save-image.py /absolute/sheet.png`, then open the PNG. Sheet tiles are small (~480 px): use `snapshot` for detail. `inspect` output is long on busy scenes: pass `{ layers: 'none' }` and read `issues` first. Give layers a `name` so inspect paths are readable. A moving ticker is not flagged as cut off.
|
|
28
30
|
6. **Iterate** on what you saw (clipped text, collisions, wrong timing). Fast motion looks choppy: add `motionBlur: true` to `movie.init` (render, snapshot and contact sheet then show a film-style blur; renders take ~8× longer). Then export: `const blob = await movie.render({ format: 'mp4' })` (about real time; `movie.on('progress', …)` reports 0–100). **While you iterate, do not export the whole movie:** `check --no-export --at <the moment you changed>` takes seconds (3 to 10 s on the gallery pieces: most of it is loading the page and measuring the sound); to see motion, `render({ range: [from, to] })` / `pixi-effects-render --range 10:15` / `--scene name` exports only that part (picture and sound); `draft: true` / `--draft` is half size, low quality and no motion blur: a much smaller file (a piece that asks for motion blur renders much faster as a draft: 4 s of `hanabi-night` took 14.0 s, and 8.2 s as a draft, about 7 s of either being the browser's start-up), but the drawing itself is not faster. Export the whole movie once, at the end.
|
|
29
31
|
|
|
30
32
|
## The rules that cause most failures
|
|
@@ -50,17 +52,17 @@ Centred text must be positioned by `anchorX/anchorY: 0.5`; estimate text width a
|
|
|
50
52
|
|
|
51
53
|
## Files
|
|
52
54
|
|
|
53
|
-
-
|
|
55
|
+
- For an AI in a chat with no shell, the short version is https://raw.githubusercontent.com/yjmtmtk/pixi-effects/main/ai/CHAT.md (it copies https://raw.githubusercontent.com/yjmtmtk/pixi-effects/main/ai/chat-template.html: one self-contained page that loads from a single CDN and shows warnings in a red box).
|
|
54
56
|
- `reference/cheatsheet.md` — every layer type, prop, default and rule on one page.
|
|
55
57
|
- `reference/recipes.md` — tested building blocks: slam type, lower-third, marquee, count-up, bar chart, 2.5D title, camera orbit, slideshow with transitions + music, looping, particles, gradients/vignette, three.js metal.
|
|
56
58
|
- `reference/pitfalls.md` — the full list of real mistakes, with status.
|
|
57
|
-
- `
|
|
58
|
-
- `template.html` — the starting file. (It loads the library from a CDN
|
|
59
|
-
- `
|
|
60
|
-
- Full reference:
|
|
59
|
+
- `npx pixi-effects check` — the one-command review (see Workflow step 5).
|
|
60
|
+
- `template.html` — the starting file. (It loads the released library from a CDN, so it works from any folder.)
|
|
61
|
+
- `scripts/save-image.py` — save a `dataURL` result (contact sheet / snapshot) to a PNG.
|
|
62
|
+
- Full reference: https://yjmtmtk.github.io/pixi-effects/docs/dsl.md and https://yjmtmtk.github.io/pixi-effects/docs/api.md
|
|
61
63
|
|
|
62
64
|
|
|
63
|
-
<!-- =====
|
|
65
|
+
<!-- ===== skills/pixi-effects/reference/cheatsheet.md ===== -->
|
|
64
66
|
|
|
65
67
|
# pixi-effects cheatsheet
|
|
66
68
|
|
|
@@ -73,7 +75,7 @@ A video is **data**: a tree of plain objects. Everything below is a JS object yo
|
|
|
73
75
|
| Canvas | pixels, origin top-left, `+x` right, `+y` down. Typical: 1280×720 @ 30 fps |
|
|
74
76
|
| Angles | **degrees**: `rotation`, `skew*`, `rotationX`, `rotationY` |
|
|
75
77
|
| Depth (`z`) | `+z` toward the viewer (bigger, nearer) |
|
|
76
|
-
| Numbers | any number may be an **expression string**: `'GW/2 - w/2'`, `'min(W,H)*0.4'` (build-time only; no per-frame time variable). Functions: `min max abs floor ceil round sqrt pow sin cos tan lerp clamp smoothstep mod step`, `PI`, and **seeded** `rand(seed)` (0…1) / `noise(x, seed)` (−1…1): give each layer its own seed, never `Math.random()` |
|
|
78
|
+
| Numbers | any number may be an **expression string**: `'GW/2 - w/2'`, `'min(W,H)*0.4'` (build-time only; no per-frame time variable). Functions: `min max abs floor ceil round sqrt pow sin cos tan lerp clamp smoothstep mod step`, `PI`, and **seeded** `rand(seed)` (0…1) / `noise(x, seed)` (−1…1): give each layer its own seed, never `Math.random()`. **No relative values** (`'+=36'` does not exist: write the absolute start, `from: { x: 'GW/2 - 36' }`). Expressions work in layer `x y` and the other numeric props, **not** in `line.from / to`, `polygon.points` or `path.d`: compute those in JS (`const W = 1920`) |
|
|
77
79
|
| Colours | `'#rrggbb'` strings or numbers; text colour is `style.fill`, shape colour is `fillColor`/`strokeColor`, image colour is `tint` |
|
|
78
80
|
| Z-order | **array order**, later = on top. A layer is hidden (not removed) outside `[at, at+duration)` and keeps its last values |
|
|
79
81
|
| Time of a sequence's `at` | seconds from the start of its **parent composition** (default 0) |
|
|
@@ -137,7 +139,7 @@ Keyframe kinds: `set` (jump at `at`; undone when you seek back), `to` (animate t
|
|
|
137
139
|
|
|
138
140
|
**Draw-on:** every shape but `arc` has `trimStart` / `trimEnd` (0–1 of the outline, default 0 / 1, animatable): `trimEnd: 0` + keyframe `to: { trimEnd: 1 }` draws a line / border / path on; `trimStart` 0 → 1 after it wipes it off; both = a travelling dash. The fill is NOT trimmed. Rect outline starts top-left, circle / ellipse at 12 o'clock, both clockwise. A `path` with several sub-paths walks them in order as one length; `trimEach: true` trims each on its own (all draw on together). Use `strokeCap: 'round'`. **Dashes:** there is no dash option; write the outline as alternating sub-paths (`d: 'M 0 0 L 10 0 M 20 0 L 30 0 …'`, or sample the curve) and `trimEnd` 0 → 1 walks them in order, so the dashes draw on one after another (as in `replay-highlight`).
|
|
139
141
|
|
|
140
|
-
Style (`initial` / keyframes): `fillColor fillAlpha strokeColor strokeAlpha strokeWidth`; `colorSpace: 'oklab'|'oklch'` for clean colour tweens. rect/circle/ellipse are **centred on `x,y` by default** (`anchorX/anchorY` default 0.5): for a bar growing from its base use `anchorY: 1` (or `anchorX: 0` for left-to-right). **`fillGradient`** (top level or `initial`; instead of `fillColor`): `{ type?: 'linear'|'radial', stops: [[0, '#000'], [1, 'rgba(0,0,0,.6)']], angle? /* linear, deg
|
|
142
|
+
Style (`initial` / keyframes): `fillColor fillAlpha strokeColor strokeAlpha strokeWidth`; `colorSpace: 'oklab'|'oklch'` for clean colour tweens. rect/circle/ellipse are **centred on `x,y` by default** (`anchorX/anchorY` default 0.5): for a bar growing from its base use `anchorY: 1` (or `anchorX: 0` for left-to-right). **`fillGradient`** (top level or `initial`; instead of `fillColor`): `{ type?: 'linear'|'radial', stops: [[0, '#000'], [1, 'rgba(0,0,0,.6)']], angle? /* linear, deg; DEFAULT 90 = top→bottom, 0 = left→right: a wide gradient slid sideways as a matte needs angle: 0 */, center?, innerRadius?, radius? /* radial, 0–1 of the SHAPE's own bounds, not the canvas: a glow on a 1500 px circle is placed with fractions of that circle */ }` — stops may have alpha, so a radial transparent→dark is a vignette. **Animate it** with a partial gradient in a keyframe (`to: { fillGradient: { angle: 360, stops: [...] } }`: keys you leave out stay; the same number of stops; `type` cannot change; a start gradient is required; use `colorSpace: 'oklch'` for a vivid fade). A text layer takes `fillGradient` too (LINEAR only: angle and stops; type / center / radius are ignored with a warning; letters re-rasterised per change). `from` alone runs from that gradient to the layer's own. The names `'fillGradient.angle'` / `gradientAngle` are not properties: write `fillGradient: { angle }`. Stroke ends and corners: `strokeCap: 'butt'|'round'|'square'`, `strokeJoin: 'miter'|'round'|'bevel'` (top level or in `initial`; not animated — `round` keeps sharp corners like an M from poking out).
|
|
141
143
|
|
|
142
144
|
**composition** — `width height duration sequences transitions`, **`speed`** (the composition's own time: its content plays backward, in slow motion, … — `time` keyframes give a freeze, a rewind, a ramp; a composition's sound and sfx follow; its own x / alpha keyframes stay in the outer time; **children are placed in the composition's local seconds**; the content is as long as the larger of `duration` and the farthest `time` it reaches (`speed: 2` → twice `duration`; `time` keyframes → their largest number), and children default to that length. A child whose life ends exactly where a held `time` stands is hidden during the hold (a life is [at, at + duration)): give it a `duration` that reaches past. `time` keyframes and ordinary ones (`x`, `alpha`, a `wiggle`) may share one `keyframes` list. A sound inside goes silent while the clock stands still). With `threeD: true` it is a **card**: its children are drawn into one texture (content outside its `width × height` is clipped: size it for a soft shadow too; a plain 2D composition does not clip, but is skipped when its whole rectangle is off screen) and move / rotate in depth together. Children use the composition's local coordinates and times. Position/rotate/scale it as a unit; to rotate/scale about its centre set `pivotX/pivotY` to the centre and `x/y` to where that point should sit.
|
|
143
145
|
|
|
@@ -167,7 +169,7 @@ Add `threeD: true` to any visual layer, then use `z`, `rotationX`, `rotationY` (
|
|
|
167
169
|
|
|
168
170
|
## Filters by name (no import)
|
|
169
171
|
|
|
170
|
-
`filters: [{ type: 'glow', name: 'halo', outerStrength: 3, color: '#ffd166' }]` — `type` = the pixi / pixi-filters class name without `Filter`, camelCase, or this library's own **`warp`** (bends the picture: `{ type: 'warp', kind: 'wave' | 'haze', strength: 6 (px), scale: 80 (px), speed: 0.5, angle: 0, seed: 0 }`; `wave` = a travelling wave, `haze` = drifting noise; moves with the frame's time by itself; options animate by name) or **`grain`** (film grain: `{ type: 'grain', amount: 0.08, size: 1.5, seed: 0, fps: 24, color: 0 }`, seeded, new every 1/fps s, mid-tone weighted, same grain pattern on every GPU (values within 1/255), anchored to the filter's frame (it moves with a layer), no pixi-filters needed; on a layer or on `composition.filters`; it costs video bitrate; `noise` is NOT film grain: it is frozen and lifts blacks) (`glow dropShadow outline blur noise alpha adjustment hslAdjustment grayscale pixelate crt rgbSplit oldFilm glitch bulgePinch twist zoomBlur kawaseBlur radialBlur motionBlur emboss ascii colorOverlay advancedBloom bevel dot crossHatch tiltShift …`), the other keys are its options. `colorMatrix` takes `preset: 'sepia'|'grayscale'|'negative'|'polaroid'|'technicolor'|'vintage'|'kodachrome'|'browni'`. Name it to animate: `'filters.halo.outerStrength'` (scalars only; `pixelate` via `sizeX`/`sizeY`). Options: `glow` distance outerStrength innerStrength color alpha · `dropShadow` offset{x,y} blur alpha color · `outline` thickness color · `blur` strength · `adjustment` gamma contrast saturation brightness · `hslAdjustment` hue lightness saturation · `pixelate` size · `crt` curvature lineWidth noise vignetting · `rgbSplit` red/green/blue {x,y} · `oldFilm` sepia noise scratch vignetting · `glitch` slices offset · `twist` radius angle offset · `bulgePinch` center radius strength · `zoomBlur` strength center innerRadius · `colorOverlay` color alpha · `colorGradient` gradientType('linear'|'radial'|'conic') stops[{offset,color,alpha}] angle · `colorReplace` originalColor targetColor tolerance · `convolution` matrix(9) · `shockwave` / `godray` are driven by `time` (animate it) · `colorMap` / `simpleLightmap` need a texture (use `custom`). **Centres/offsets are CANVAS pixels** (default top-left corner): put them on the layer. Glow / shadow / blur draw outside the layer: widen `filterArea` (not for `grayscale` / `oldFilm`, which fill the margin black). Full table:
|
|
172
|
+
`filters: [{ type: 'glow', name: 'halo', outerStrength: 3, color: '#ffd166' }]` — `type` = the pixi / pixi-filters class name without `Filter`, camelCase, or this library's own **`warp`** (bends the picture: `{ type: 'warp', kind: 'wave' | 'haze', strength: 6 (px), scale: 80 (px), speed: 0.5, angle: 0, seed: 0 }`; `wave` = a travelling wave, `haze` = drifting noise; moves with the frame's time by itself; options animate by name) or **`grain`** (film grain: `{ type: 'grain', amount: 0.08, size: 1.5, seed: 0, fps: 24, color: 0 }`, seeded, new every 1/fps s, mid-tone weighted, same grain pattern on every GPU (values within 1/255), anchored to the filter's frame (it moves with a layer), no pixi-filters needed; on a layer or on `composition.filters`; it costs video bitrate; `noise` is NOT film grain: it is frozen and lifts blacks) (`glow dropShadow outline blur noise alpha adjustment hslAdjustment grayscale pixelate crt rgbSplit oldFilm glitch bulgePinch twist zoomBlur kawaseBlur radialBlur motionBlur emboss ascii colorOverlay advancedBloom bevel dot crossHatch tiltShift …`), the other keys are its options. `colorMatrix` takes `preset: 'sepia'|'grayscale'|'negative'|'polaroid'|'technicolor'|'vintage'|'kodachrome'|'browni'`. Name it to animate: `'filters.halo.outerStrength'` (scalars only; `pixelate` via `sizeX`/`sizeY`). Options: `glow` distance outerStrength innerStrength color alpha · `dropShadow` offset{x,y} blur alpha color · `outline` thickness color · `blur` strength · `adjustment` gamma contrast saturation brightness · `hslAdjustment` hue lightness saturation · `pixelate` size · `crt` curvature lineWidth noise vignetting · `rgbSplit` red/green/blue {x,y} · `oldFilm` sepia noise scratch vignetting · `glitch` slices offset · `twist` radius angle offset · `bulgePinch` center radius strength · `zoomBlur` strength center innerRadius · `colorOverlay` color alpha · `colorGradient` gradientType('linear'|'radial'|'conic') stops[{offset,color,alpha}] angle · `colorReplace` originalColor targetColor tolerance · `convolution` matrix(9) · `shockwave` / `godray` are driven by `time` (animate it) · `colorMap` / `simpleLightmap` need a texture (use `custom`). **Centres/offsets are CANVAS pixels** (default top-left corner): put them on the layer. Glow / shadow / blur draw outside the layer: widen `filterArea` (not for `grayscale` / `oldFilm`, which fill the margin black). Full table: https://yjmtmtk.github.io/pixi-effects/docs/dsl.md → Named filters.
|
|
171
173
|
|
|
172
174
|
## Transitions (on the parent composition)
|
|
173
175
|
|
|
@@ -246,13 +248,13 @@ new Controller(movie, { canvas }) // optional player bar (overlay
|
|
|
246
248
|
```
|
|
247
249
|
|
|
248
250
|
|
|
249
|
-
<!-- =====
|
|
251
|
+
<!-- ===== skills/pixi-effects/reference/pitfalls.md ===== -->
|
|
250
252
|
|
|
251
253
|
# pixi-effects pitfalls — what actually went wrong
|
|
252
254
|
|
|
253
255
|
Collected by having fresh AI sessions build pieces from the docs alone: first six (kinetic type, news lower-third, slideshow, 2.5D showcase, three.js title, bar chart), then a portfolio of 16 more (items 32 and up), made by three different models. Each entry is a real stumble. **Status**: `fixed` = the library now handles or warns; `docs` = nothing can catch it, so remember it; `hand-roll` = the DSL has no feature, see the workaround.
|
|
254
256
|
|
|
255
|
-
A good habit that catches most of these: after `init`, read `window.__logs` (see `
|
|
257
|
+
A good habit that catches most of these: after `init`, read `window.__logs` (see `template.html`) — every pixi-effects warning says what to change.
|
|
256
258
|
|
|
257
259
|
## Time
|
|
258
260
|
|
|
@@ -324,7 +326,7 @@ A good habit that catches most of these: after `init`, read `window.__logs` (see
|
|
|
324
326
|
45. **The same mistake in a loop warns once**: the warning "keyframe starts after the layer ends" is summarised after three layers. The usual cause is an off-by-one (`f <= frames` instead of `f < frames`) that puts the last sample exactly at the layer's end. *(fixed)*
|
|
325
327
|
46. **Still not in the DSL (hand-roll):** `zIndex` (stack order is list order). *(Earlier round-two notes listed draw-on strokes, changing text, jitter and particle emitters here: they are built in now: `trimEnd`, `set: { text }` / `visibleChars`, `wiggle()`, `particles()`.)*
|
|
326
328
|
46b. **Blend modes exist now** (`blendMode`: `add` `screen` `multiply` and, since 0.22, 15 more CSS names (pitfalls 78–80), on any layer, threeD layers and compositions included; a composition's children inherit it). Additive white cores no longer read grey on a coloured background. It also works on a layer with `filters` (a blurred glow with `add` / `multiply`): the mode is applied by the last filter, because a container's blend inside a filter pass multiplied against nothing and rendered black. With `add`, use dim saturated colours: overlaps sum, and near-white sums to pure white. *(fixed)*
|
|
327
|
-
47. **Browser tooling: prefer `
|
|
329
|
+
47. **Browser tooling: prefer `npx pixi-effects check page.html`** — it runs its own private Chrome, so none of the following can bite. If you drive a browser by hand instead: the `agent-browser` daemon is shared, so a `contactSheet` can come back from another session: after saving an image, look at it and confirm it is YOUR piece. In zsh a variable holding `agent-browser --session x` is not split into words (use a small wrapper script). `inspect` now clips overlaps to the visible area, ignores the two scenes of a running transition, treats text inside a moving (panned) composition as moving, and does not report text scaled to 0 as "no size"; it still does not compare text with picture content or shapes.
|
|
328
330
|
48. **A composition's `alpha` is applied to each child separately, not to the flattened group** (measured: two overlapping opaque circles in a composition at `alpha: 0.5` show the lower one through the upper one in the overlap). Fading a group that has overlapping children therefore shows the overlap; fade the children themselves, or keep overlapping parts out of the group. Expressions have no way to read a sibling's size: measure text with `canvas.measureText` (the font string Pixi uses) and compute pill widths in JS. *(docs)*
|
|
329
331
|
49. **sfx timing and level.** An sfx layer's `duration` is the sound's length (it stretches, it does not trim). Align the loud point: a `riser` peaks at its end, a `swoosh` 0.16 s in. Many sounds at the same instant add up: above 0.9 the mix is limited and a warning names the layers to turn down. Preset names, knobs and voice fields are closed lists — a guess (`whoosh`, `ding`, `impact`, `sawtooth`, `lp`, `decay`) warns with the right word. *(fixed: warns)*
|
|
330
332
|
50. **An image (or video) works as a mask, `maskInverted` included** *(fixed)*: PIXI hides a sprite mask with `renderable = false` and the lifespan toggles used to switch it back on, so the mask picture was drawn over the layer (a white disc) and `maskInverted` looked ignored. Use the picture's alpha (or red channel) as the shape: an opaque word or logo on a transparent canvas image is a text-shaped mask with real glyph edges. A `text` layer also works as a mask (cut to its glyphs; measured with a heavy sans `I`). *(docs / fixed)*
|
|
@@ -346,7 +348,7 @@ A good habit that catches most of these: after `init`, read `window.__logs` (see
|
|
|
346
348
|
66. **Animating a gradient: one `fillGradient` object, partial, in the keyframe.** `to: { fillGradient: { angle: 360, stops: [...] } }` (keys you leave out stay). Not `'fillGradient.angle'`, not `gradientAngle`, not `fillGradient: 'red'` (each warns once with what to write). The layer must start with a `fillGradient`; `stops` keep their count; `type` cannot change. A fade between two colours in the default `rgb` passes through grey (green → magenta): set `colorSpace: 'oklch'`. Under a mask the stops still span the whole shape (see 39).
|
|
347
349
|
67. **A key the layer does not have is warned about now (it used to be ignored in silence).** `{ type: 'text', anchorX: 0.5 }` (anchors go in `initial`), `{ type: 'audio', sfx: 'click', pitch: 3 }` (the knob goes inside: `sfx: { preset: 'click', pitch: 3 }`), `{ type: 'shape', shape: 'rect', radius: 5 }` (that is a circle's key; a rect has `cornerRadius`), a mistyped property in a keyframe (`alhpa`), a mistyped text `style` key. The warning names the key, the kind of layer and what was probably meant; fix the key, do not silence the warning. Three gallery pieces had the `pitch` mistake for months: their sound effects were never pitched.
|
|
348
350
|
68. **A pitched sfx keeps its own length; a radial gradient is placed in the shape's bounds.** `sfx: { preset: 'chime', pitch: -5 }` lasts as long as the sound does, so near the end of the movie give the layer a `duration` that ends with it (`inspectAudio()` reports a sound cut off by the end). A radial `fillGradient`'s `center` / `radius` are fractions of the SHAPE, not the canvas: for a glow on a big circle, convert canvas pixels to fractions of the circle. `music.reverb` is 0–0.6.
|
|
349
|
-
69. **Inside a remapped composition, `at` is in THAT composition's seconds.** With `speed: 0.5` a 4 s composition shows only 2 s of its content; a layer at `at: 3` inside is never seen (warns: "its clock only reaches 0–2s"). With `speed: 2` the content is 8 s long, so children default to 8 s and `at: 5` is fine. The composition's own `at`, `duration`, x / alpha keyframes are outer time. To freeze, hold `time` at one value (`to` the same number); to rewind, `to: { time: 0 }`. A sound inside goes silent while the clock stands still, and an `sfx` longer than the clock's reach is reported as cut off by the clock: give it a shorter `duration` (the sfx limit is 10 s) or let the clock run on. A child whose life ends exactly where a held `time` stands is hidden during the hold: give it a `duration` that reaches past (the content is as long as the larger of `duration` and the largest `time`).
|
|
351
|
+
69. **Inside a remapped composition, `at` is in THAT composition's seconds.** With `speed: 0.5` a 4 s composition shows only 2 s of its content; a layer at `at: 3` inside is never seen (warns: "its clock only reaches 0–2s"). With `speed: 2` the content is 8 s long, so children default to 8 s and `at: 5` is fine. The composition's own `at`, `duration`, x / alpha keyframes are outer time. To freeze, hold `time` at one value (`to` the same number); to rewind, `to: { time: 0 }`. A sound inside goes silent while the clock stands still, and an `sfx` longer than the clock's reach is reported as cut off by the clock: give it a shorter `duration` (the sfx limit is 10 s) or let the clock run on. A child whose life ends exactly where a held `time` stands is hidden during the hold: give it a `duration` that reaches past (the content is as long as the larger of `duration` and the largest `time`). **With `time` keyframes the composition's own `duration` is its life in OUTER time and must reach the last keyframe** (`keyframes[1] starts at 2.3s, after the layer ends` means `duration` is too short): `at` of a `time` keyframe counts from the composition's start, the content length is a separate thing that comes out of the `time` values.
|
|
350
352
|
70. **`speed` is a setting, `time` is the property.** `speed: 2` on the layer; to change the speed over time animate `time` (a curve of seconds in the file), never `keyframes: [{ to: { speed: 3 } }]` (warns). `speed: 0` is not allowed (hold a moment with `time` to the same value twice); `speed` together with keyframed `time`: `time` wins. Other names (`playbackRate`, `timeScale`, `rate`, `reverse: true`, `timeRemap`) are warned about with what to write. `particles({ speed })` is something else: launch speed in px per second.
|
|
351
353
|
71. **Sound follows `speed` like a tape: the pitch moves with it.** An audio file or a video's sound at `speed: 2` is an octave up; backward plays reversed. Not for `sfx` / `music` layers (warns): change an sfx with `pitch` (semitones), or put the sfx **inside a remapped composition** and it follows that clock (slower and lower, or backward).
|
|
352
354
|
72. **A video at `speed: 2` with no `duration` is half as long; with a `duration` longer than the file it holds the last frame** (add `loop: true` to repeat). A negative `time` counts back from the end of the file (`initial: { time: -2 }` = two seconds before the end).
|
|
@@ -370,9 +372,15 @@ A good habit that catches most of these: after `init`, read `window.__logs` (see
|
|
|
370
372
|
90. **`uniforms` are declared for you; keyframes name them `'uniforms.speed'`, and vectors and colours by component `'uniforms.tint.0'`.** `uniforms: { tint: '#ff8040' }` is a `vec3` of 0..1, so animate its channels, not the string. Declare every uniform you animate with a starting value (a keyframe on a name that is not there warns with the likely name). Do not write `uniform float speed;` yourself.
|
|
371
373
|
91. **A shader that does not compile is a magenta checkerboard, and the warning has your line numbers.** Fix the line it names. Loops need a constant bound (`for (int i = 0; i < 8; i++)`): a `while`, or a bound that is a variable, can hang the GPU and warns. A heavy shader is cheaper with `resolution: 0.5`; many small shader layers cost more than one big one. A mask or matte cuts what the shader drew, it does not move the pattern: put the pattern's centre (a `vec2` uniform) where the matte is. A shader layer with a small `width` / `height` sits by its top-left corner at `initial.x / y`. With `resolution` below 1 the shader's own pixels are fewer: write positions as `fragCoord / iResolution.xy`, or multiply pixel uniforms by `resolution`. A shader can itself be a mask (`mask: { type: 'shader', transparent: true, fragment }`: its alpha is the mask).
|
|
372
374
|
92. **`warp` is for bending a layer's picture, a `shader` layer is for drawing something new.** `warp` takes `strength` in px (a ripple is 4 to 10, over 200 is refused) and `scale` in px; `speed: 0` stands still. It follows the time of the frame by itself: do not keyframe a time. Its options animate by the filter's name (`'filters.ripple.strength'`). For a swirl or a lens use `twist` or `bulgePinch`.
|
|
375
|
+
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.
|
|
376
|
+
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).
|
|
377
|
+
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.
|
|
378
|
+
96. **A filter option that is a point (`rgbSplit.red`, `glitch` offsets) cannot be tweened as a whole**, but a scalar can: `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.
|
|
379
|
+
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.
|
|
380
|
+
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`).
|
|
373
381
|
|
|
374
382
|
|
|
375
|
-
<!-- =====
|
|
383
|
+
<!-- ===== skills/pixi-effects/reference/recipes.md ===== -->
|
|
376
384
|
|
|
377
385
|
# pixi-effects recipes
|
|
378
386
|
|
|
@@ -1153,7 +1161,7 @@ return [
|
|
|
1153
1161
|
|
|
1154
1162
|
## Light leaks and colour casts (blend modes)
|
|
1155
1163
|
|
|
1156
|
-
`blendMode` mixes a layer with everything **below** it. A colour cast is one `soft-light` gradient over the whole picture, a light leak is a `color-dodge` glow that drifts across it, a vignette is a `multiply` radial gradient that goes from clear to dark. Each is ONE layer (a gradient), not a pile of shapes: the blend modes beyond `add` / `screen` / `multiply` cost a full-frame pass each.
|
|
1164
|
+
`blendMode` mixes a layer with everything **below** it. A colour cast is one `soft-light` gradient over the whole picture, a light leak is a `color-dodge` glow that drifts across it, a vignette is a `multiply` radial gradient that goes from clear to dark. Each is ONE layer (a gradient), not a pile of shapes: the blend modes beyond `add` / `screen` / `multiply` cost a full-frame pass each. Over a dark picture a `color-dodge` glow turns the near-black into a muddy red disc: cut it to the bright subject with a matte (`mask: 'letters'`) and put the warm haze in a separate `screen` radial.
|
|
1157
1165
|
|
|
1158
1166
|
```js
|
|
1159
1167
|
// @recipe light-leak
|
|
@@ -1503,7 +1511,7 @@ const sequences = [
|
|
|
1503
1511
|
# batch.sh: one video per row of table.csv (name,score). --draft first, to look; remove it for the real files.
|
|
1504
1512
|
mkdir -p out
|
|
1505
1513
|
while IFS=, read -r name score; do
|
|
1506
|
-
npx pixi-effects
|
|
1514
|
+
npx pixi-effects render batch.html --query "name=$(printf %s "$name" | jq -sRr @uri)&score=$score" -o "out/$name.mp4" --quiet
|
|
1507
1515
|
done < table.csv
|
|
1508
1516
|
```
|
|
1509
1517
|
|
|
@@ -2802,7 +2810,7 @@ A filter by name, as plain data. `type` is the filter's class name without `Filt
|
|
|
2802
2810
|
keyframes: [{ at: 0, to: { 'filters.halo.outerStrength': 6 }, duration: 1 }] } // animate any scalar option by name
|
|
2803
2811
|
```
|
|
2804
2812
|
|
|
2805
|
-
Name a filter (`name: 'halo'`) to animate it with `'filters.halo.<option>'`. `blur`, `noise`, `alpha`, `colorMatrix` and `grain` need no extra package (`grain` is this library's own). All the others come from the **`pixi-filters`** package, which is loaded the first time a layer uses one (add `"pixi-filters": "https://esm.sh/pixi-filters@6.1.5?external=pixi.js"` to the import map, or `npm i pixi-filters`; `
|
|
2813
|
+
Name a filter (`name: 'halo'`) to animate it with `'filters.halo.<option>'`. `blur`, `noise`, `alpha`, `colorMatrix` and `grain` need no extra package (`grain` is this library's own). All the others come from the **`pixi-filters`** package, which is loaded the first time a layer uses one (add `"pixi-filters": "https://esm.sh/pixi-filters@6.1.5?external=pixi.js"` to the import map, or `npm i pixi-filters`; `skills/pixi-effects/template.html` already has it). A misspelt type says what you probably meant. Rendering and exporting were verified in a real browser for 40 of them (`examples/_checks/named-filters.html` and `named-filters-2.html`); `colorMap` and `simpleLightmap` need a texture, which JSON cannot hold, so use `custom` for those two:
|
|
2806
2814
|
|
|
2807
2815
|
| `type` | Options (the useful ones) | What it does |
|
|
2808
2816
|
|---|---|---|
|
|
@@ -3065,7 +3073,7 @@ import { cameraPath, wiggle } from 'pixi-effects';
|
|
|
3065
3073
|
| `from` / `to` | fractions 0–1 of the route to start and stop at (default 0 → 1; `from: 1, to: 0` flies it backwards) |
|
|
3066
3074
|
| `frameRate` | samples per second: your movie's `frameRate` (default 30) |
|
|
3067
3075
|
|
|
3068
|
-
Layers the camera passes take `hideBehindCamera: true`. The recipe `camera-fly-through` in `
|
|
3076
|
+
Layers the camera passes take `hideBehindCamera: true`. The recipe `camera-fly-through` in `skills/pixi-effects/reference/recipes.md` is a whole corridor.
|
|
3069
3077
|
|
|
3070
3078
|
### `followPath`
|
|
3071
3079
|
|
|
@@ -3228,7 +3236,7 @@ interface MovieOptions {
|
|
|
3228
3236
|
composition?: CompositionSpec; // root composition (see DSL reference)
|
|
3229
3237
|
poster?: number; // seconds: the frame shown before play (negative: from the end), see movie.poster
|
|
3230
3238
|
motionBlur?: boolean | number | { samples?: number; shutter?: number }; // blur for render() / snapshot() / contactSheet(), not live playback; see Motion blur
|
|
3231
|
-
loader?: HTMLElement | string | false; // the page's loader (see pixi-effects/loader.css): faded out when ready, stopped if init fails; default: a .pe-loader next to the canvas
|
|
3239
|
+
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
|
|
3232
3240
|
}
|
|
3233
3241
|
```
|
|
3234
3242
|
|
|
@@ -3406,6 +3414,7 @@ movie.off(event, fn): this
|
|
|
3406
3414
|
| `'play'` | none | when `play()` actually transitions from paused |
|
|
3407
3415
|
| `'pause'` | none | when `pause()` actually transitions from playing |
|
|
3408
3416
|
| `'progress'`| `{ progress: number; frame: number; totalFrames: number }` | during `render()`, once per encoded frame |
|
|
3417
|
+
| `'loadprogress'` | `{ stage: 'assets' \| 'build' \| 'sound' \| 'frames'; loaded: number; total: number; progress: number }` | during `init()`, after each asset and each layer built, once the sound is mixed; `progress` is 0 to 1 for the whole init. The page's `.pe-loader` already shows it; `movie.loadStages` gives the milliseconds per stage afterwards |
|
|
3409
3418
|
| `'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()` |
|
|
3410
3419
|
| `'stop'` | `{ index: number; stop: Stop; pageIndex: number }` | a presentation is on a stop: `next()` played to it, or `prev()` / `goToStop()` / `goToPage()` jumped to it |
|
|
3411
3420
|
| `'ended'` | none | playback ran off the end (after `'pause'`); pausing by hand does not emit it |
|
|
@@ -3484,7 +3493,7 @@ const pages = await movie.stopImages({ as: 'dataURL', type: 'image/jpeg', scale:
|
|
|
3484
3493
|
const pdf = await movie.exportPDF({ title: 'My talk' }); // a Blob: one PDF page per page of the deck (JPEG at the canvas size)
|
|
3485
3494
|
```
|
|
3486
3495
|
|
|
3487
|
-
Both pause the movie and leave the playhead where it was; a movie with no stops returns an empty list / throws (a PDF needs pages). The picture of a page is, in order: its last stop flagged `pdf: true`, else its last (with `pick: 'first'`, first) stop that is not `pdf: false`; a page whose stops are all `pdf: false` is left out of a PDF (a page list still shows it by its last stop); `which: 'stops'` makes a page of every stop that is not `pdf: false`. `exportPDF` options: `which`, `pick`, `scale`, `quality` (JPEG, default 0.92), `title`, `motionBlur`, `onImage`. A transparent background comes out black in a JPEG: give the movie a `background`. From a script, `npx pixi-effects
|
|
3496
|
+
Both pause the movie and leave the playhead where it was; a movie with no stops returns an empty list / throws (a PDF needs pages). The picture of a page is, in order: its last stop flagged `pdf: true`, else its last (with `pick: 'first'`, first) stop that is not `pdf: false`; a page whose stops are all `pdf: false` is left out of a PDF (a page list still shows it by its last stop); `which: 'stops'` makes a page of every stop that is not `pdf: false`. `exportPDF` options: `which`, `pick`, `scale`, `quality` (JPEG, default 0.92), `title`, `motionBlur`, `onImage`. A transparent background comes out black in a JPEG: give the movie a `background`. From a script, `npx pixi-effects render my-talk.html -o my-talk.pdf` (add `--all-stops` for a page per stop).
|
|
3488
3497
|
|
|
3489
3498
|
### Audio-reactive: `audioEnvelope()`, `react()`, `bpmEnvelope()`
|
|
3490
3499
|
|
|
@@ -3555,7 +3564,7 @@ el('play').onclick = () => (movie.isPlaying ? movie.pause() : movie.play());
|
|
|
3555
3564
|
el('seek').oninput = e => movie.gotoFrame(+e.target.value);
|
|
3556
3565
|
```
|
|
3557
3566
|
|
|
3558
|
-
What you have: `play()`, `pause()`, `gotoFrame(frame)`, `volume`, `muted` / `toggleMute()`; the state `currentFrame`, `totalFrames`, `frameRate`, `duration`, `isPlaying`, `isReady
|
|
3567
|
+
What you have: `play()`, `pause()`, `gotoFrame(frame)`, `volume`, `muted` / `toggleMute()`; the state `currentFrame`, `totalFrames`, `frameRate`, `duration`, `isPlaying`, `isReady`, `loadStages` (milliseconds `init()` spent in `assets` / `build` / `sound` / `frames`), `audioBlocked` (true while the browser keeps the movie's sound silent until a tap: show "tap for sound"; a tap or key press that arrives while `play()` is pending, or the next `play()` made from a tap, starts it); the events above; `render()` for a download button; `snapshot()` for a thumbnail; `timelineData()` / `timelineSvg()` for a timeline like `pixi-effects-view`'s. Keep the movie's `canvas` wherever your layout wants it (the page's CSS sizes it). To only change how `Controller` looks, see **Theme** below.
|
|
3559
3568
|
|
|
3560
3569
|
## `Controller`
|
|
3561
3570
|
|
package/llms.txt
CHANGED
|
@@ -13,11 +13,12 @@ Rules that cause most failures (details in the skill and pitfalls):
|
|
|
13
13
|
## Start here
|
|
14
14
|
|
|
15
15
|
- **If you are an AI in a chat with no shell and no files** (a browser chat, a preview window): read only [Chat guide](https://raw.githubusercontent.com/yjmtmtk/pixi-effects/main/ai/CHAT.md) and copy [chat-template.html](https://raw.githubusercontent.com/yjmtmtk/pixi-effects/main/ai/chat-template.html); everything below is for an AI that can run commands.
|
|
16
|
-
-
|
|
17
|
-
- [
|
|
18
|
-
- [
|
|
19
|
-
- [
|
|
20
|
-
- [
|
|
16
|
+
- **If your agent supports skills:** `npx skills add yjmtmtk/pixi-effects` registers the workflow below as a skill (Claude Code also: `/plugin install pixi-effects --marketplace yjmtmtk/pixi-effects`)
|
|
17
|
+
- [Skill: workflow and rules](https://raw.githubusercontent.com/yjmtmtk/pixi-effects/main/skills/pixi-effects/SKILL.md): how to build, check and export a video; read first
|
|
18
|
+
- [Cheatsheet](https://raw.githubusercontent.com/yjmtmtk/pixi-effects/main/skills/pixi-effects/reference/cheatsheet.md): every layer type, prop, default and convention on one page
|
|
19
|
+
- [Recipes](https://raw.githubusercontent.com/yjmtmtk/pixi-effects/main/skills/pixi-effects/reference/recipes.md): tested building blocks (kinetic type, lower-third, count-up, bar chart, 2.5D title, camera orbit, slideshow with transitions, sound effects without files, music written as text, three.js, a presentation deck, visuals that follow music)
|
|
20
|
+
- [Pitfalls](https://raw.githubusercontent.com/yjmtmtk/pixi-effects/main/skills/pixi-effects/reference/pitfalls.md): real mistakes made by AI authors, with fixes
|
|
21
|
+
- [Starter template](https://raw.githubusercontent.com/yjmtmtk/pixi-effects/main/skills/pixi-effects/template.html): copy this file to begin
|
|
21
22
|
|
|
22
23
|
## Reference
|
|
23
24
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pixi-effects",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.1",
|
|
4
4
|
"description": "Declarative composition and video rendering for the web. Built on PixiJS v8.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
"main": "./dist/index.cjs",
|
|
26
26
|
"module": "./dist/index.js",
|
|
27
27
|
"types": "./dist/index.d.ts",
|
|
28
|
-
"bin": { "pixi-effects-check": "ai/tools/check.mjs", "pixi-effects-render": "ai/tools/render.mjs", "pixi-effects-view": "ai/tools/view.mjs" },
|
|
29
|
-
"files": ["dist", "ai", "docs/dsl.md", "docs/api.md", "llms.txt", "llms-full.txt", "CHANGELOG.md", "README.md", "LICENSE"],
|
|
28
|
+
"bin": { "pixi-effects": "ai/tools/pixi-effects.mjs", "pixi-effects-check": "ai/tools/check.mjs", "pixi-effects-render": "ai/tools/render.mjs", "pixi-effects-view": "ai/tools/view.mjs" },
|
|
29
|
+
"files": ["dist", "ai", "skills", "docs/dsl.md", "docs/api.md", "llms.txt", "llms-full.txt", "CHANGELOG.md", "README.md", "LICENSE"],
|
|
30
30
|
"keywords": [
|
|
31
31
|
"pixi", "pixijs", "video", "composition", "compositing", "rendering",
|
|
32
32
|
"after-effects", "motion-graphics", "timeline", "webgl", "gsap", "keyframe", "typescript",
|