pixi-effects 0.28.2 → 0.29.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 +52 -0
- package/README.md +10 -10
- package/ai/chat-template.html +5 -5
- package/ai/tools/check.mjs +18 -3
- package/ai/tools/pixi-effects.mjs +1 -1
- package/ai/tools/render.mjs +14 -9
- package/ai/tools/view.mjs +6 -2
- package/dist/Base-CB1_FPZD.d.cts +60 -0
- package/dist/Base-Ne74ztNl.d.ts +60 -0
- package/dist/Composition-MUKR3FYD.cjs +16 -0
- package/dist/{Composition-K6B3APM5.cjs.map → Composition-MUKR3FYD.cjs.map} +1 -1
- package/dist/Composition-RJGA7KDU.js +7 -0
- package/dist/{Composition-H7UNR5F5.js.map → Composition-RJGA7KDU.js.map} +1 -1
- package/dist/Controller.cjs +73 -45
- package/dist/Controller.cjs.map +1 -1
- package/dist/Controller.d.cts +16 -22
- package/dist/Controller.d.ts +16 -22
- package/dist/Controller.js +74 -40
- package/dist/Controller.js.map +1 -1
- package/dist/{Movie-CHcM8HNB.d.ts → Movie-C2rZFkrO.d.ts} +227 -119
- package/dist/{Movie-Cx5ozUGb.d.cts → Movie-DdnpLpw1.d.cts} +227 -119
- package/dist/{blendModes-HDOP6R7T.cjs → blendModes-ETMYWATF.cjs} +5 -5
- package/dist/{blendModes-HDOP6R7T.cjs.map → blendModes-ETMYWATF.cjs.map} +1 -1
- package/dist/{blendModes-PAVTW3BO.js → blendModes-RIVZXL5A.js} +4 -4
- package/dist/{blendModes-PAVTW3BO.js.map → blendModes-RIVZXL5A.js.map} +1 -1
- package/dist/{chunk-P4GY3Z4G.cjs → chunk-2DXPTKQZ.cjs} +6 -6
- package/dist/chunk-2DXPTKQZ.cjs.map +1 -0
- package/dist/{chunk-TYWFKCPY.js → chunk-4KB3GUEK.js} +5 -5
- package/dist/chunk-4KB3GUEK.js.map +1 -0
- package/dist/{chunk-5MZWXVTT.js → chunk-A3Q3SDSH.js} +35 -28
- package/dist/chunk-A3Q3SDSH.js.map +1 -0
- package/dist/chunk-CRQFDY7E.js +4182 -0
- package/dist/chunk-CRQFDY7E.js.map +1 -0
- package/dist/chunk-HREBQAY4.cjs +4206 -0
- package/dist/chunk-HREBQAY4.cjs.map +1 -0
- package/dist/{chunk-ZMAD3KJU.cjs → chunk-LYOKH7KE.cjs} +22 -3
- package/dist/chunk-LYOKH7KE.cjs.map +1 -0
- package/dist/{chunk-45QKLD6M.js → chunk-RCVZX2JP.js} +22 -3
- package/dist/chunk-RCVZX2JP.js.map +1 -0
- package/dist/{chunk-KBH33IQX.cjs → chunk-WHNADXXG.cjs} +121 -114
- package/dist/chunk-WHNADXXG.cjs.map +1 -0
- package/dist/{chunk-7VN2KI3U.js → chunk-ZAXYPXKW.js} +14 -6
- package/dist/chunk-ZAXYPXKW.js.map +1 -0
- package/dist/{chunk-MJW7KILM.cjs → chunk-ZVUO325O.cjs} +32 -24
- package/dist/chunk-ZVUO325O.cjs.map +1 -0
- package/dist/index.cjs +106 -4482
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -60
- package/dist/index.d.ts +15 -60
- package/dist/index.js +6 -4457
- package/dist/index.js.map +1 -1
- package/dist/loader.css +1 -1
- package/dist/{Presenter.cjs → present.cjs} +370 -31
- package/dist/present.cjs.map +1 -0
- package/dist/present.d.cts +278 -0
- package/dist/present.d.ts +278 -0
- package/dist/{Presenter.js → present.js} +358 -32
- package/dist/present.js.map +1 -0
- package/dist/three.cjs +6 -6
- package/dist/three.d.cts +2 -2
- package/dist/three.d.ts +2 -2
- package/dist/three.js +2 -2
- package/dist/{types-DpK1vwcl.d.ts → types-BzIB65Vk.d.cts} +11 -265
- package/dist/{types-DpK1vwcl.d.cts → types-BzIB65Vk.d.ts} +11 -265
- package/docs/api.md +44 -77
- package/docs/dsl.md +16 -14
- package/llms-full.txt +86 -115
- package/llms.txt +1 -1
- package/package.json +5 -5
- package/skills/pixi-effects/SKILL.md +5 -5
- package/skills/pixi-effects/reference/cheatsheet.md +9 -9
- package/skills/pixi-effects/reference/direction.md +2 -0
- package/skills/pixi-effects/reference/pitfalls.md +6 -6
- package/skills/pixi-effects/reference/recipes.md +4 -4
- package/skills/pixi-effects/template.html +7 -7
- package/dist/Base-C6f0fE8j.d.ts +0 -109
- package/dist/Base-DI1FCGcH.d.cts +0 -109
- package/dist/Composition-H7UNR5F5.js +0 -7
- package/dist/Composition-K6B3APM5.cjs +0 -16
- package/dist/Presenter.cjs.map +0 -1
- package/dist/Presenter.d.cts +0 -122
- package/dist/Presenter.d.ts +0 -122
- package/dist/Presenter.js.map +0 -1
- package/dist/chunk-45QKLD6M.js.map +0 -1
- package/dist/chunk-5MZWXVTT.js.map +0 -1
- package/dist/chunk-7VN2KI3U.js.map +0 -1
- package/dist/chunk-KBH33IQX.cjs.map +0 -1
- package/dist/chunk-MJW7KILM.cjs.map +0 -1
- package/dist/chunk-P4GY3Z4G.cjs.map +0 -1
- package/dist/chunk-TYWFKCPY.js.map +0 -1
- package/dist/chunk-ZMAD3KJU.cjs.map +0 -1
|
@@ -1,128 +1,3 @@
|
|
|
1
|
-
type EaseFn = (p: number) => number;
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* A timeline that is a pure function of time (spike: see docs/superpowers/specs/2026-10-10-pure-timeline-design.md).
|
|
5
|
-
*
|
|
6
|
-
* For every (target, property) it keeps the segments that write it, ordered by start time. The value at `t`
|
|
7
|
-
* is the value of the last segment that has started, computed from `t` alone; nothing is remembered between
|
|
8
|
-
* two calls except the start values captured when the segment was built. GSAP is the oracle: the rules below
|
|
9
|
-
* were taken from black-box probes of `gsap.timeline` and are checked against it by tests/core/pure/timelineParity.test.ts.
|
|
10
|
-
*/
|
|
11
|
-
interface PureVars {
|
|
12
|
-
duration?: number;
|
|
13
|
-
ease?: string | EaseFn;
|
|
14
|
-
repeat?: number;
|
|
15
|
-
yoyo?: boolean;
|
|
16
|
-
repeatDelay?: number;
|
|
17
|
-
onUpdate?: () => void;
|
|
18
|
-
/** The Pixi plugin's shorthands: `scale`, `anchor`, `rotation` (degrees), `tint`, `autoAlpha` ... */
|
|
19
|
-
pixi?: Record<string, unknown>;
|
|
20
|
-
[prop: string]: unknown;
|
|
21
|
-
}
|
|
22
|
-
/** One thing a timeline writes: see `segments()`. */
|
|
23
|
-
interface SegmentInfo {
|
|
24
|
-
target: object;
|
|
25
|
-
/** The property (`'alpha'`, `'scale.x'`, `'fill'`). */
|
|
26
|
-
id: string;
|
|
27
|
-
start: number;
|
|
28
|
-
duration: number;
|
|
29
|
-
repeat: number;
|
|
30
|
-
yoyo: boolean;
|
|
31
|
-
repeatDelay: number;
|
|
32
|
-
kind: 'set' | 'to' | 'from' | 'fromTo';
|
|
33
|
-
}
|
|
34
|
-
/** A marker that only lengthens a timeline (what `gsap.to({}, { duration })` is used for). */
|
|
35
|
-
interface Spacer {
|
|
36
|
-
readonly spacer: true;
|
|
37
|
-
readonly duration: number;
|
|
38
|
-
}
|
|
39
|
-
declare class PureTimeline {
|
|
40
|
-
private readonly defaultEase;
|
|
41
|
-
private readonly channels;
|
|
42
|
-
private readonly channelList;
|
|
43
|
-
private readonly children;
|
|
44
|
-
private spans;
|
|
45
|
-
private now;
|
|
46
|
-
private order;
|
|
47
|
-
private killed;
|
|
48
|
-
private prepared;
|
|
49
|
-
constructor(opts?: {
|
|
50
|
-
paused?: boolean;
|
|
51
|
-
defaults?: {
|
|
52
|
-
ease?: string | EaseFn;
|
|
53
|
-
};
|
|
54
|
-
});
|
|
55
|
-
/** Adds a child timeline (played with its local time) or a spacer that lengthens this one. */
|
|
56
|
-
add(child: PureTimeline | Spacer, at?: number): this;
|
|
57
|
-
set(target: object, vars: PureVars, at?: number): this;
|
|
58
|
-
to(target: object, vars: PureVars, at?: number): this;
|
|
59
|
-
from(target: object, vars: PureVars, at?: number): this;
|
|
60
|
-
fromTo(target: object, fromVars: PureVars, toVars: PureVars, at?: number): this;
|
|
61
|
-
private build;
|
|
62
|
-
private channel;
|
|
63
|
-
/**
|
|
64
|
-
* A property moved by a function of the caller (a colour in OKLab, a gradient): `to` and `from` say how to get the two ends from
|
|
65
|
-
* the value before the segment, `make` the interpolator between them. The value is only written while the segment is the last
|
|
66
|
-
* one that has started, so nothing is read or kept between two frames.
|
|
67
|
-
*/
|
|
68
|
-
tweenValue<T>(spec: {
|
|
69
|
-
holder: object;
|
|
70
|
-
id: string;
|
|
71
|
-
get(): T;
|
|
72
|
-
set(v: T): void;
|
|
73
|
-
from?: (prev: T) => T;
|
|
74
|
-
to: (start: T, prev: T, base: T) => T;
|
|
75
|
-
make: (a: T, b: T) => (p: number) => T;
|
|
76
|
-
duration: number;
|
|
77
|
-
ease: string | EaseFn;
|
|
78
|
-
at: number;
|
|
79
|
-
repeat?: number;
|
|
80
|
-
yoyo?: boolean;
|
|
81
|
-
repeatDelay?: number;
|
|
82
|
-
}): this;
|
|
83
|
-
/**
|
|
84
|
-
* A write that is undone when time goes back before it: the property is `value` from `at` on, and what it was before otherwise.
|
|
85
|
-
* `holder` and `id` name the property, so a set and the tweens of the same property are one channel; `write` is the way the caller
|
|
86
|
-
* writes it (it may do more than assign: redraw), and runs again whenever this set is what the property shows.
|
|
87
|
-
*/
|
|
88
|
-
setValue<T>(holder: object, id: string, at: number, read: () => T, write: (v: T) => void, value: T): this;
|
|
89
|
-
/** What the timeline will write, for tools and tests: every segment of this timeline and of the ones under it, starts in this timeline's time. */
|
|
90
|
-
segments(offset?: number): SegmentInfo[];
|
|
91
|
-
/** How many timelines are directly under this one. */
|
|
92
|
-
timelines(): number;
|
|
93
|
-
duration(): number;
|
|
94
|
-
time(): number;
|
|
95
|
-
time(value: number): this;
|
|
96
|
-
/** GSAP's name for `time(t)`. */
|
|
97
|
-
seek(t: number): this;
|
|
98
|
-
progress(): number;
|
|
99
|
-
progress(p: number): this;
|
|
100
|
-
kill(): void;
|
|
101
|
-
/**
|
|
102
|
-
* What GSAP ended up with once it had been played to the end and back (what Movie used to do at build), as rules that depend
|
|
103
|
-
* on the segments of a property only:
|
|
104
|
-
* - a `from` / `fromTo` shows its start from the moment it is built, so a `to` that comes first starts at the start of the
|
|
105
|
-
* `from` built last, else at the property's own value;
|
|
106
|
-
* - a `to` (and a `set`) starts where the segment before it ended;
|
|
107
|
-
* - a `from` ends where the segment before it ended, or, with none before, where the property was when it was built;
|
|
108
|
-
* - before the first segment the property shows that segment's start.
|
|
109
|
-
*/
|
|
110
|
-
private prepare;
|
|
111
|
-
/** Every channel of this timeline and of its children reads the property it starts from before any of them writes. */
|
|
112
|
-
private prepareDeep;
|
|
113
|
-
/** How many channels write each (target, property) in this timeline and everything under it. */
|
|
114
|
-
private countChannels;
|
|
115
|
-
private isExclusive;
|
|
116
|
-
private markExclusive;
|
|
117
|
-
private evaluate;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* The timeline of the library and the few things around it: a timeline is a pure function of time (a value at a time is computed from the
|
|
122
|
-
* script and the time alone, never from the way the playhead came), see docs/superpowers/specs/2026-10-10-pure-timeline-design.md.
|
|
123
|
-
*/
|
|
124
|
-
type Timeline = PureTimeline;
|
|
125
|
-
|
|
126
1
|
/** What an author writes in `composition.stops`: a time in seconds, or an object. */
|
|
127
2
|
interface StopSpec {
|
|
128
3
|
/** Where playback stops, in seconds (negative counts back from the end). */
|
|
@@ -154,97 +29,6 @@ interface Stop {
|
|
|
154
29
|
advance?: number;
|
|
155
30
|
pdf?: boolean;
|
|
156
31
|
}
|
|
157
|
-
/**
|
|
158
|
-
* The stops of a composition, checked and in order. Numbers are stops; with no `page` anywhere every stop is a page of its
|
|
159
|
-
* own, otherwise only the stops that say `page` start one (the first stop always does). Negative times count back from the end;
|
|
160
|
-
* two stops on the same frame are one; anything outside the movie or not a number is dropped, with a warning that names it.
|
|
161
|
-
*/
|
|
162
|
-
declare function normalizeStops(raw: ReadonlyArray<number | StopSpec> | undefined, duration: number, frameRate: number): Stop[];
|
|
163
|
-
/**
|
|
164
|
-
* The stops whose pictures stand for the pages (`which: 'pages'`, default) or for every stop. A page's picture is, in order: the stop flagged
|
|
165
|
-
* `pdf: true` (the last such one, or the first with `pick: 'first'`), else the last (or first) stop not flagged `pdf: false`, else, for a page
|
|
166
|
-
* list, its last (or first) stop, and for a PDF (`pdf: true` here) nothing: the page is left out. With `which: 'stops'` a PDF leaves out the
|
|
167
|
-
* stops flagged `pdf: false`.
|
|
168
|
-
*/
|
|
169
|
-
declare function pictureStops(stops: readonly Stop[], opts?: {
|
|
170
|
-
which?: 'pages' | 'stops';
|
|
171
|
-
pick?: 'last' | 'first';
|
|
172
|
-
pdf?: boolean;
|
|
173
|
-
}): Stop[];
|
|
174
|
-
|
|
175
|
-
/** One note or chord of a track, as a score reader sees it. */
|
|
176
|
-
interface MusicNote {
|
|
177
|
-
/** Where it starts: beats from the start, and the same moment in seconds (the tempo map applied; swing and humanize are not). */
|
|
178
|
-
beat: number;
|
|
179
|
-
time: number;
|
|
180
|
-
/** How long it is held, in beats and in seconds. */
|
|
181
|
-
duration: number;
|
|
182
|
-
seconds: number;
|
|
183
|
-
/** The bar it starts in (1-based, from `meter`) and its beat inside that bar (1 = the downbeat, 1.5 = the "and" of 1). */
|
|
184
|
-
bar: number;
|
|
185
|
-
beatInBar: number;
|
|
186
|
-
/** The audible MIDI notes (`music.transpose` and the track's `transpose` are in). */
|
|
187
|
-
midi: number[];
|
|
188
|
-
/** What the score called it: `Am7`, `c4`, `[c4 e4 g4]`. */
|
|
189
|
-
name: string;
|
|
190
|
-
/** More than one note at once. */
|
|
191
|
-
chord: boolean;
|
|
192
|
-
/** 0–1 (0.8 normal, 1 accented with `!`, 0.55 soft with `,`). */
|
|
193
|
-
velocity: number;
|
|
194
|
-
}
|
|
195
|
-
interface MusicTrackEvents {
|
|
196
|
-
inst: string;
|
|
197
|
-
/** The track's length in beats and in bars (not a whole number when a bar is short: that is what to look for). */
|
|
198
|
-
beats: number;
|
|
199
|
-
bars: number;
|
|
200
|
-
notes: MusicNote[];
|
|
201
|
-
}
|
|
202
|
-
interface MusicDrumHit {
|
|
203
|
-
beat: number;
|
|
204
|
-
time: number;
|
|
205
|
-
bar: number;
|
|
206
|
-
beatInBar: number;
|
|
207
|
-
velocity: number;
|
|
208
|
-
}
|
|
209
|
-
interface MusicDrumEvents {
|
|
210
|
-
kind: string;
|
|
211
|
-
hits: MusicDrumHit[];
|
|
212
|
-
}
|
|
213
|
-
interface MusicEvents {
|
|
214
|
-
/** The tempo at the start. */
|
|
215
|
-
bpm: number;
|
|
216
|
-
meter: number;
|
|
217
|
-
beats: number;
|
|
218
|
-
bars: number;
|
|
219
|
-
/** The music without its reverb tail. */
|
|
220
|
-
seconds: number;
|
|
221
|
-
tracks: MusicTrackEvents[];
|
|
222
|
-
/** Every drum that is hit, over all sections, with every hit written out. */
|
|
223
|
-
drums: MusicDrumEvents[];
|
|
224
|
-
}
|
|
225
|
-
/** What the `check` tool prints about a music layer: lengths in bars (a short track shows), what starts in each bar, where the drums play. */
|
|
226
|
-
interface MusicSummary {
|
|
227
|
-
bpm: number;
|
|
228
|
-
meter: number;
|
|
229
|
-
/** The score set `meter`; when false 4 is only assumed. */
|
|
230
|
-
meterSet: boolean;
|
|
231
|
-
beats: number;
|
|
232
|
-
bars: number;
|
|
233
|
-
seconds: number;
|
|
234
|
-
tail: number;
|
|
235
|
-
tracks: Array<{
|
|
236
|
-
inst: string;
|
|
237
|
-
beats: number;
|
|
238
|
-
bars: number;
|
|
239
|
-
events: number;
|
|
240
|
-
perBar: string[];
|
|
241
|
-
}>;
|
|
242
|
-
drums: Array<{
|
|
243
|
-
from: number;
|
|
244
|
-
to: number;
|
|
245
|
-
kinds: string[];
|
|
246
|
-
}>;
|
|
247
|
-
}
|
|
248
32
|
|
|
249
33
|
/**
|
|
250
34
|
* pixi-effects public DSL types.
|
|
@@ -325,7 +109,7 @@ interface ChromaKeyFilterSpec {
|
|
|
325
109
|
/**
|
|
326
110
|
* A filter by name: `{ type: 'glow', outerStrength: 3, color: '#ffd166' }`. The type is the filter's class name without
|
|
327
111
|
* `Filter` in camelCase (case does not matter); every other key is the filter's own option. `blur`, `noise`, `alpha` and
|
|
328
|
-
* `colorMatrix` (`preset: 'sepia' | 'grayscale' | …` or a 20-number `matrix`) come from pixi.js; the rest are the filters of
|
|
112
|
+
* `colorMatrix` (`preset: 'sepia' | 'grayscale' | …` or a 20-number `matrix`) come from pixi.js; `grain` and `warp` are this library's own, the rest are the filters of
|
|
329
113
|
* the `pixi-filters` package (add it to the import map, or `npm i pixi-filters`), loaded on first use. Animate an option with
|
|
330
114
|
* `'filters.<name>.<option>'` like any other filter.
|
|
331
115
|
*/
|
|
@@ -334,7 +118,7 @@ interface NamedFilterSpec {
|
|
|
334
118
|
name?: string;
|
|
335
119
|
[option: string]: unknown;
|
|
336
120
|
}
|
|
337
|
-
type NamedFilterType = 'blur' | 'noise' | 'alpha' | 'colorMatrix' | 'grain' | 'adjustment' | 'advancedBloom' | 'ascii' | 'backdropBlur' | 'bevel' | 'bloom' | 'bulgePinch' | 'colorGradient' | 'colorMap' | 'colorOverlay' | 'colorReplace' | 'convolution' | 'crossHatch' | 'crt' | 'dot' | 'dropShadow' | 'emboss' | 'glitch' | 'glow' | 'godray' | 'grayscale' | 'hslAdjustment' | 'kawaseBlur' | 'motionBlur' | 'multiColorReplace' | 'oldFilm' | 'outline' | 'pixelate' | 'radialBlur' | 'reflection' | 'rgbSplit' | 'shockwave' | 'simpleLightmap' | 'simplexNoise' | 'tiltShift' | 'tiltShiftAxis' | 'twist' | 'zoomBlur';
|
|
121
|
+
type NamedFilterType = 'blur' | 'noise' | 'alpha' | 'colorMatrix' | 'grain' | 'warp' | 'adjustment' | 'advancedBloom' | 'ascii' | 'backdropBlur' | 'bevel' | 'bloom' | 'bulgePinch' | 'colorGradient' | 'colorMap' | 'colorOverlay' | 'colorReplace' | 'convolution' | 'crossHatch' | 'crt' | 'dot' | 'dropShadow' | 'emboss' | 'glitch' | 'glow' | 'godray' | 'grayscale' | 'hslAdjustment' | 'kawaseBlur' | 'motionBlur' | 'multiColorReplace' | 'oldFilm' | 'outline' | 'pixelate' | 'radialBlur' | 'reflection' | 'rgbSplit' | 'shockwave' | 'simpleLightmap' | 'simplexNoise' | 'tiltShift' | 'tiltShiftAxis' | 'twist' | 'zoomBlur';
|
|
338
122
|
type FilterSpec = ChromaKeyFilterSpec | CustomFilterSpec | NamedFilterSpec;
|
|
339
123
|
interface TransitionCommon {
|
|
340
124
|
/** Sibling sequence's `name` — the outgoing scene. */
|
|
@@ -407,9 +191,12 @@ interface LumaTransition extends TransitionCommon {
|
|
|
407
191
|
type TransitionSpec = CrossfadeTransition | WipeTransition | IrisTransition | SlideTransition | DipTransition | ZoomTransition | DissolveTransition | LumaTransition;
|
|
408
192
|
/** How a layer blends with what is behind it: the CSS `mix-blend-mode` names, plus `add` and `linear-burn`. */
|
|
409
193
|
type BlendModeName = 'normal' | 'add' | 'screen' | 'multiply' | 'overlay' | 'soft-light' | 'hard-light' | 'color-dodge' | 'color-burn' | 'darken' | 'lighten' | 'difference' | 'exclusion' | 'hue' | 'saturation' | 'color' | 'luminosity' | 'linear-burn';
|
|
410
|
-
/**
|
|
194
|
+
/**
|
|
195
|
+
* A matte with options: `layer` is another layer of the same composition by name, or a layer written in place. `channel` is how it is read (`'alpha'` by
|
|
196
|
+
* default, or `'luma'`: its brightness); `invert` uses 1 − matte (a hole instead of a crop). A layer written in place cannot go inside a list.
|
|
197
|
+
*/
|
|
411
198
|
interface MatteRefSpec {
|
|
412
|
-
layer: string;
|
|
199
|
+
layer: string | SequenceSpec;
|
|
413
200
|
channel?: 'alpha' | 'luma';
|
|
414
201
|
invert?: boolean;
|
|
415
202
|
}
|
|
@@ -457,18 +244,10 @@ interface SequenceCommon {
|
|
|
457
244
|
* geometric reveals.
|
|
458
245
|
*
|
|
459
246
|
* Instead of a layer written in place, `mask` can name another layer of the same composition (a matte: it is not drawn, and several
|
|
460
|
-
* layers can share it), `{ layer, channel, invert }
|
|
461
|
-
* with `invert: true`).
|
|
247
|
+
* layers can share it), `{ layer, channel, invert }` (`layer` is a name, or a layer written in place), or a list of names and `{ layer: name }`
|
|
248
|
+
* (all of them must let the layer through: intersect; subtract with `invert: true`). A hole instead of a crop: `mask: { layer: { …the shape… }, invert: true }`.
|
|
462
249
|
*/
|
|
463
250
|
mask?: SequenceSpec | MaskRefSpec | MaskRefSpec[];
|
|
464
|
-
/**
|
|
465
|
-
* When true, the mask is inverted: pixels inside the mask shape become
|
|
466
|
-
* transparent and pixels outside become visible. Useful for "knockout"
|
|
467
|
-
* effects (cut a hole through an image / panel). Default false.
|
|
468
|
-
*
|
|
469
|
-
* Routed through PIXI's native `setMask({ inverse: true })`.
|
|
470
|
-
*/
|
|
471
|
-
maskInverted?: boolean;
|
|
472
251
|
/**
|
|
473
252
|
* How this layer blends with what is behind it (everything drawn below it). `'add'` and `'screen'` brighten (glows, light
|
|
474
253
|
* leaks, overlapping halos add up instead of covering each other), `'multiply'` darkens; `'overlay'`, `'soft-light'`,
|
|
@@ -899,7 +678,7 @@ interface CompositionSpec extends SequenceCommon {
|
|
|
899
678
|
transitions?: TransitionSpec[];
|
|
900
679
|
/**
|
|
901
680
|
* Where a presentation pauses: seconds, or `{ at, page?, notes?, advance? }` (see `StopSpec`). A stop with `page` begins a page, the others
|
|
902
|
-
* are steps of it; with no `page` anywhere each stop is a page. Nothing changes for an ordinary player: `Presenter` (and `
|
|
681
|
+
* are steps of it; with no `page` anywhere each stop is a page. Nothing changes for an ordinary player: `Presenter` (and `next(movie)`, from `pixi-effects/present`)
|
|
903
682
|
* stop at them, `Controller` shows them as marks on the seek bar.
|
|
904
683
|
*/
|
|
905
684
|
stops?: Array<number | StopSpec>;
|
|
@@ -914,38 +693,5 @@ interface CompositionShape {
|
|
|
914
693
|
/** Called after each layer is built (the root shape only): feeds `loadprogress` and lets the page paint now and then. */
|
|
915
694
|
onLayerBuilt?: () => void | Promise<void>;
|
|
916
695
|
}
|
|
917
|
-
/** Audio descriptor pushed to the mixdown queue by AudioSequence/VideoSequence. */
|
|
918
|
-
interface AudioDescriptor {
|
|
919
|
-
/** Decoded audio (a file or a video's soundtrack). Exactly one of `buffer` / `synth` is set. */
|
|
920
|
-
buffer?: AudioBuffer;
|
|
921
|
-
/** Stereo samples (left, right) made on demand at the mix's sample rate (a synthesised sfx). Equal keys render equal samples. */
|
|
922
|
-
synth?: {
|
|
923
|
-
key: string;
|
|
924
|
-
render: (sampleRate: number) => [Float32Array, Float32Array];
|
|
925
|
-
/** The same samples in slices: `pace(done)` (0..1) runs between them and may wait, so a long piece does not freeze the page. */
|
|
926
|
-
renderAsync?: (sampleRate: number, pace?: (done: number) => Promise<void> | void) => Promise<[Float32Array, Float32Array]>;
|
|
927
|
-
};
|
|
928
|
-
/** Who made it, for warnings and inspectAudio: `layer "pop-1"`. */
|
|
929
|
-
layer?: string;
|
|
930
|
-
/** What it plays: `sfx "pop"`, `asset "bgm"`, `video "green"`. */
|
|
931
|
-
source?: string;
|
|
932
|
-
/** A music layer's score read back: bars per track, what starts in each bar, the drum spans (for `movie.inspectAudio()` and the `check` tool). */
|
|
933
|
-
music?: MusicSummary;
|
|
934
|
-
loop: boolean;
|
|
935
|
-
start: number;
|
|
936
|
-
end: number;
|
|
937
|
-
initialVolume: number;
|
|
938
|
-
volumeKeyframes: {
|
|
939
|
-
time: number;
|
|
940
|
-
value: number;
|
|
941
|
-
}[];
|
|
942
|
-
/**
|
|
943
|
-
* Time remap. `warp` maps the movie's time to the time `start` / `end` / `volumeKeyframes` are measured in (a remapped composition's
|
|
944
|
-
* local time), NaN where it is silent. `sourceMap` maps that time to a position in the sound (the layer's own speed / `time`); absent:
|
|
945
|
-
* the sound plays straight from `start`. The mixer resamples through them, so a speed other than 1 changes the pitch.
|
|
946
|
-
*/
|
|
947
|
-
warp?: (t: number) => number;
|
|
948
|
-
sourceMap?: (u: number) => number;
|
|
949
|
-
}
|
|
950
696
|
|
|
951
|
-
export {
|
|
697
|
+
export type { TransitionCommon as $, ArcShapeSpec as A, BlendModeName as B, CompositionShape as C, DipTransition as D, EllipseShapeSpec as E, FilterSpec as F, GradientSpec as G, PropValue as H, ImageSequenceSpec as I, Props as J, Keyframe as K, LightSequenceSpec as L, MusicOptions as M, NullSequenceSpec as N, SfxKnobs as O, PathShapeSpec as P, SfxOptions as Q, RectShapeSpec as R, SequenceSpec as S, TextSequenceSpec as T, SfxPreset as U, SfxVoice as V, ShaderSequenceSpec as W, ShapeSequenceSpec as X, SlideTransition as Y, Stop as Z, StopSpec as _, CameraSequenceSpec as a, TransitionSpec as a0, VideoSequenceSpec as a1, WipeTransition as a2, ZoomTransition as a3, SequenceCommon as b, AssetSpec as c, AudioAssetSpec as d, AudioMusicSpec as e, AudioSequenceSpec as f, AudioSfxSpec as g, ChromaKeyFilterSpec as h, CircleShapeSpec as i, CompositionSequenceSpec as j, CompositionSpec as k, CrossfadeTransition as l, CustomFilterSpec as m, DissolveTransition as n, Expr as o, IrisTransition as p, LineShapeSpec as q, LumaTransition as r, MaskRefSpec as s, MatteRefSpec as t, MusicDrum as u, MusicDrums as v, MusicInstrument as w, MusicLevel as x, MusicTrack as y, PolygonShapeSpec as z };
|
package/docs/api.md
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
- [`Movie`](#movie) — composition runtime: init, playback, render
|
|
4
4
|
- [`Controller`](#controller) — drop-in player UI overlay
|
|
5
|
-
- [Helpers](#helpers) — pure utilities exported from `pixi-effects/controller`
|
|
6
5
|
- [`pixi-effects/three`](#pixi-effectsthree) — optional three.js integration
|
|
7
6
|
|
|
8
7
|
For DSL types (composition spec, sequences, filters, keyframes, expressions), see [DSL reference](./dsl.md).
|
|
@@ -71,13 +70,21 @@ Seeks to a specific frame. Pauses if currently playing? **No** — does not chan
|
|
|
71
70
|
|
|
72
71
|
`force=true` skips the early-return when the requested frame equals `currentFrame`. Use it after a composition rebuild.
|
|
73
72
|
|
|
73
|
+
### `movie.playUntil(seconds?): Promise<boolean>`
|
|
74
|
+
|
|
75
|
+
Play, and pause exactly at `seconds` (the frame `round(seconds × frameRate)`). It resolves `true` when the playhead has landed there; `false` when something else ended it first (a `pause()`, a seek, a newer `playUntil()`) or when there is nothing to play toward (a time at or before the playhead: it also pauses the movie where it is; a movie that is not ready). A time at or past the end lands on the last frame; with no time at all it plays to the end, says `ended`, and resolves `true`. An ordinary `play()` is not affected. It is the one move a presentation (`pixi-effects/present`) is built on; it is also the way to "play the intro, then wait": `await movie.playUntil(4.5)`.
|
|
76
|
+
|
|
77
|
+
### `movie.peek(fn): Promise<T>`
|
|
78
|
+
|
|
79
|
+
Look around and put the movie back: pauses, runs `fn` (seek, take pictures with `snapshot()`; the seeks made inside are not reported as `seeking` / `seeked`), then puts the playhead (or the poster) back where it was, whatever `fn` did, and returns what `fn` returned. `stopImages()` and `inspectStops()` (`pixi-effects/present`) are made with it.
|
|
80
|
+
|
|
74
81
|
### `movie.snapshot(frame?, options?): Promise<Blob | string>`
|
|
75
82
|
|
|
76
83
|
```ts
|
|
77
|
-
snapshot(frame?: number, options?: { scale?: number; type?: 'image/png' | 'image/jpeg'; as?: 'blob' | 'dataURL'; motionBlur?: MotionBlurSpec }): Promise<Blob | string>
|
|
84
|
+
snapshot(frame?: number, options?: { scale?: number; type?: 'image/png' | 'image/jpeg'; quality?: number; as?: 'blob' | 'dataURL' | 'imageData'; motionBlur?: MotionBlurSpec }): Promise<Blob | string | ImageData>
|
|
78
85
|
```
|
|
79
86
|
|
|
80
|
-
A picture of one frame: **the canvas only** (the player bar is not in it). Seeks to `frame` (default: the current frame) and stays there. `as: 'dataURL'` returns a `data:` URL string, handy when a script can only return text. Use it to look at what you built.
|
|
87
|
+
A picture of one frame: **the canvas only** (the player bar is not in it). Seeks to `frame` (default: the current frame) and stays there. `as: 'dataURL'` returns a `data:` URL string, `as: 'imageData'` the pixels as an `ImageData` (to measure or compare; `quality` is the JPEG quality, 0 to 1), handy when a script can only return text. Use it to look at what you built.
|
|
81
88
|
|
|
82
89
|
### `movie.contactSheet(options?): Promise<Blob | string>`
|
|
83
90
|
|
|
@@ -174,10 +181,10 @@ interface RenderOptions {
|
|
|
174
181
|
|
|
175
182
|
`movie.init({ motionBlur })` (or the same option on a single `render()`, `snapshot()` or `contactSheet()` call) exposes each frame the way a film camera does: the frame is drawn `samples` times at moments spread over the shutter interval around its time, and the pictures are averaged. Fast motion smears along its path; a still picture is unchanged. `true` is 8 samples at a 180° shutter, a number is the sample count (2–64), `{ samples, shutter }` sets both (`shutter` is the fraction of a frame the shutter stays open, above 0 and up to 1; `0.5` is the film look, `1` blurs more). `false` turns it off for one call.
|
|
176
183
|
|
|
177
|
-
- It applies to what is **made**: the exported file, snapshots, contact sheets (so `pixi-effects
|
|
184
|
+
- It applies to what is **made**: the exported file, snapshots, contact sheets (so `pixi-effects check` and `inspect`-style reviews show what the file will have). Live playback is not blurred.
|
|
178
185
|
- Cost: a render takes about `samples` times as long (video layers are decoded once per sample). Eight samples is enough for most motion; raise it for very fast or large movement, where fewer samples show as separate ghost images instead of a smear.
|
|
179
186
|
- After a blurred snapshot, contact sheet or render the playhead and the stage are on an exact frame again, not a sample.
|
|
180
|
-
- `pixi-effects
|
|
187
|
+
- `pixi-effects render --motion-blur 8 [--shutter 0.5]` sets it for one file.
|
|
181
188
|
|
|
182
189
|
**Video codec:** unless you name `video.codec`, the usual one for the format is used (mp4 / mov: `avc`; webm: `vp9`); if this browser cannot encode it, the next that works is used and a warning says so. A codec you name is never swapped: it is used, or the error names the one that would work (`"av1" ... "avc" would work: render({ format: 'mp4', video: { codec: 'avc' } })`). `hevc` and `av1` make smaller files at the same quality but play in fewer places than `avc`; `hardware: 'prefer-hardware'` asks the GPU's encoder.
|
|
183
190
|
|
|
@@ -203,13 +210,13 @@ const blob = await movie.posterImage(); // the picture at that time
|
|
|
203
210
|
- Once the movie is ready the **canvas shows the poster frame**, while the playhead stays at frame 0 (`currentFrame` is 0, the player shows 0:00). The first `play()` or `gotoFrame()` replaces it, and **play starts from 0**.
|
|
204
211
|
- `posterImage()` leaves the movie as it was: still showing the poster, or back at the frame it was at. `contactSheet()` does the same.
|
|
205
212
|
- A time past the end is clamped to the last frame, and something that is not a number is ignored; both warn.
|
|
206
|
-
- `pixi-effects
|
|
213
|
+
- `pixi-effects check` writes the poster as `poster.jpg`.
|
|
207
214
|
|
|
208
215
|
### `movie.timelineData(): TimelineData` and `movie.timelineChart(options?): string`
|
|
209
216
|
|
|
210
|
-
The whole movie as rows on a time axis, for a person to read at a glance. `timelineData()` is the data: `{ duration, rows, transitions }`, a row per layer with `name`, `type`, `depth` (1 inside a composition), absolute `start` / `end` in seconds, `keys` (when each keyframe starts) and, for a family of generated layers, `parts` (one span per layer). Layers of one type whose names differ only by numbers (`ring10-0`, `ring9-1`, `pop-3` …), four or more, become ONE row (`ring#-# ×19`), so a movie with 700 generated layers is a screenful. `timelineChart({ title })` returns one self-contained HTML page (an inline SVG, no scripts): bars coloured by type, ◆ at each keyframe start, a shaded band per transition, a tooltip per bar. `pixi-effects
|
|
217
|
+
The whole movie as rows on a time axis, for a person to read at a glance. `timelineData()` is the data: `{ duration, rows, transitions }`, a row per layer with `name`, `type`, `depth` (1 inside a composition), absolute `start` / `end` in seconds, `keys` (when each keyframe starts) and, for a family of generated layers, `parts` (one span per layer). Layers of one type whose names differ only by numbers (`ring10-0`, `ring9-1`, `pop-3` …), four or more, become ONE row (`ring#-# ×19`), so a movie with 700 generated layers is a screenful. `timelineChart({ title })` returns one self-contained HTML page (an inline SVG, no scripts): bars coloured by type, ◆ at each keyframe start, a shaded band per transition, a tooltip per bar. `pixi-effects check` writes it next to the contact sheet as `timeline.html`.
|
|
211
218
|
|
|
212
|
-
`movie.timelineSvg()` is the chart alone (one `<svg>`, geometry in `data-*` attributes, each row `<g class="row" data-start>`): `pixi-effects
|
|
219
|
+
`movie.timelineSvg()` is the chart alone (one `<svg>`, geometry in `data-*` attributes, each row `<g class="row" data-start>`): `pixi-effects view` puts a playhead on it and turns a click into a seek.
|
|
213
220
|
|
|
214
221
|
### Your own layer type: `Sequence` and `registerSequenceType(type, ctor)`
|
|
215
222
|
|
|
@@ -252,7 +259,6 @@ movie.off(event, fn): this
|
|
|
252
259
|
| `'progress'`| `{ progress: number; frame: number; totalFrames: number }` | during `render()`, once per encoded frame |
|
|
253
260
|
| `'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 |
|
|
254
261
|
| `'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()` |
|
|
255
|
-
| `'stop'` | `{ index: number; stop: Stop; pageIndex: number }` | a presentation is on a stop: `next()` played to it, or `prev()` / `goToStop()` / `goToPage()` jumped to it |
|
|
256
262
|
| `'ended'` | none | playback ran off the end (after `'pause'`); pausing by hand does not emit it |
|
|
257
263
|
| `'volumechange'` | `{ volume: number; muted: boolean }` | `volume` or `muted` really changed (clamping counts) |
|
|
258
264
|
| `'error'` | `{ where: 'init' \| 'render' \| 'playback'; message: string; error: unknown }` | `init()` / `render()` failed (the call still rejects), or a frame failed during playback |
|
|
@@ -276,28 +282,32 @@ The names are the ones an HTML5 `<video>` uses, so a player written for `<video>
|
|
|
276
282
|
| `muted` | boolean | getter/setter; immediate |
|
|
277
283
|
| `app` | `pixi.js Application \| null` | PIXI Application instance (advanced/escape hatch) |
|
|
278
284
|
| `timeline` | `Timeline \| null` | the movie's timeline (advanced): `time(t)`, `duration()`, `segments()` |
|
|
279
|
-
| `stops` | `Stop[]` | the composition's stops (see Presentations); empty for an ordinary movie |
|
|
280
|
-
| `stopIndex`, `pageIndex`, `pageCount`, `currentStop` | number, number, number, `Stop \| null` | where the playhead is among the stops and pages (`-1` / `null` before the first stop) |
|
|
285
|
+
| `stops` | `Stop[]` | the composition's stops: named times (see Presentations); empty for an ordinary movie |
|
|
281
286
|
|
|
282
|
-
### Presentations: `stops
|
|
287
|
+
### Presentations: `stops` and `pixi-effects/present`
|
|
283
288
|
|
|
284
289
|
A composition can say where a presentation pauses: `composition: { stops: [...] }`. A stop is a time in seconds or `{ at, page?, notes?, advance?, pdf? }`; a stop with `page` (a name, or `true`) begins a page, the others are steps of it; with no `page` anywhere each stop is a page; the first stop always begins one. A negative `at` counts back from the end; two stops on one frame are one; a stop outside the movie is ignored with a warning. `movie.play()` ignores the stops and plays to the end; the `Controller`'s play button, Space and a click on the picture play to the next stop and pause there (see `pauseAtStops`). `pdf: true` marks the stop whose picture stands for its page in a PDF (and in the page overview); `pdf: false` keeps a stop's picture out of the PDF (a last stop that is mid-exit, a step you do not want on paper). Without flags a page is pictured by its last stop.
|
|
285
290
|
|
|
286
291
|
```ts
|
|
287
|
-
|
|
288
|
-
|
|
292
|
+
import { next, prev, goToStop, goToPage, onStop, stopIndex, pageIndex, pageCount, currentStop } from 'pixi-effects/present';
|
|
293
|
+
|
|
294
|
+
movie.stops // (the core) Stop[]: { index, at, frame, page: string | null, pageIndex, pageStart, notes?, advance?, pdf? }
|
|
295
|
+
await next(movie); // play to the next stop and pause exactly on its frame; pressed again while playing, skip to that stop at once;
|
|
289
296
|
// after the last stop play to the end. Resolves when it has landed (or stopped for another reason).
|
|
290
|
-
await
|
|
291
|
-
await
|
|
292
|
-
await
|
|
293
|
-
movie
|
|
294
|
-
movie
|
|
297
|
+
await prev(movie); // jump back to the previous stop at once (no reverse playback); from the first stop, to the start
|
|
298
|
+
await goToStop(movie, i); // 0-based
|
|
299
|
+
await goToPage(movie, n); // 0-based: the first stop of page n
|
|
300
|
+
stopIndex(movie) / pageIndex(movie) / pageCount(movie) / currentStop(movie) // where the playhead is (-1 / null before the first stop)
|
|
301
|
+
const off = onStop(movie, ({ index, stop, pageIndex }) => …); // a stop was landed on; off() stops listening
|
|
295
302
|
```
|
|
296
303
|
|
|
297
|
-
`
|
|
304
|
+
The presentation side is a separate entry, `pixi-effects/present`, built on the core's public API only (an import map line: `"pixi-effects/present": ".../dist/present.js"`). The core knows `stops` (named times) and two general moves it is made of: `movie.playUntil(seconds)` and `movie.peek(fn)` (below). An ordinary `movie.play()` ignores the stops. `onStop` listeners belong to the `pixi-effects/present` module the page imported: the `Controller` loads `present.js` from beside itself, so map `"pixi-effects/present"` to the same folder as `Controller.js`, or a Controller landing will not reach your `onStop`.
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
`deck({ pages, transition? })` (from `pixi-effects/present`) builds such a movie from pages (see the DSL reference). **`Presenter`** (also `pixi-effects/present`) is the player for it:
|
|
298
308
|
|
|
299
309
|
```ts
|
|
300
|
-
import { Presenter } from 'pixi-effects/
|
|
310
|
+
import { Presenter } from 'pixi-effects/present';
|
|
301
311
|
const presenter = new Presenter(movie, { canvas }); // instead of Controller; await presenter.start() to go fullscreen and play to the first stop
|
|
302
312
|
```
|
|
303
313
|
|
|
@@ -315,18 +325,19 @@ const presenter = new Presenter(movie, { canvas }); // instead of Controller
|
|
|
315
325
|
|
|
316
326
|
**The page overview (`G`)** is a grid of every page with its picture, the current page marked: arrows choose, Enter or a click goes there, `G` or Esc closes. **The presenter view (`P`)** opens a second window for the speaker: the picture that is on screen (live), the picture of the next stop, the notes of the page, a timer (with reset), the page counter and Back / Next buttons; the keys work there too. Both use a picture of every stop that `start()` makes **before** the audience sees anything (behind a "Preparing…" cover; taking them moves the playhead through the stops and back, so it must not happen on screen); `presenter.ensureThumbs()` makes them on demand (an overview opened before that does it on the spot, with a flicker). A browser needs a click or key press to open a popup: `P` is one; if it is blocked a warning says so.
|
|
317
327
|
|
|
318
|
-
`presenter.start()`, `presenter.openPresenterView()`, `presenter.closePresenterView()`, `presenter.next()`, `presenter.prev()`, `presenter.setCover('black' \| 'white' \| 'off')`, `presenter.exit()`, `presenter.destroy()`. It uses the canvas's positioned parent, or wraps the canvas like `Controller` does. A movie with stops shown through a **`Controller`** gets a mark per stop on the seek bar and a Present button (`
|
|
328
|
+
`presenter.start()`, `presenter.openPresenterView()`, `presenter.closePresenterView()`, `presenter.next()`, `presenter.prev()`, `presenter.setCover('black' \| 'white' \| 'off')`, `presenter.exit()`, `presenter.destroy()`. It uses the canvas's positioned parent, or wraps the canvas like `Controller` does. A movie with stops shown through a **`Controller`** gets a mark per stop on the seek bar and a Present button (`presentButton: false` leaves the marks only) that hands the page to a `Presenter` (fullscreen, keys, click and swipe move through the stops; Esc gives the bar back).
|
|
319
329
|
|
|
320
|
-
### `
|
|
330
|
+
### `inspectStops(movie, options?)`
|
|
321
331
|
|
|
322
|
-
Is the picture at each stop still changing? Each stop is compared with the picture `lookback` frames before it (default 3); when more than `tolerance` (default 0.004, a share of the pixels) differs, the audience, the page overview and a PDF would see a half-finished animation. Returns `{ stops: [{ index, page, at, frame, moving, settled }], issues: string[] }`; each issue names the stop and the page and says what to do (move the stop to after the animation has ended, or flag a settled stop of the page `pdf: true`). Leaves the playhead where it was. `pixi-effects
|
|
332
|
+
Is the picture at each stop still changing? Each stop is compared with the picture `lookback` frames before it (default 3); when more than `tolerance` (default 0.004, a share of the pixels) differs, the audience, the page overview and a PDF would see a half-finished animation. Returns `{ stops: [{ index, page, at, frame, moving, settled }], issues: string[] }`; each issue names the stop and the page and says what to do (move the stop to after the animation has ended, or flag a settled stop of the page `pdf: true`). Leaves the playhead where it was. `pixi-effects check` runs it for any page with `stops` (it loads the page's own `pixi-effects/present`; add that line to the import map).
|
|
323
333
|
|
|
324
|
-
### Pictures of the stops and the deck as a PDF: `
|
|
334
|
+
### Pictures of the stops and the deck as a PDF: `stopImages(movie)`, `exportPDF(movie)`
|
|
325
335
|
|
|
326
336
|
```ts
|
|
327
|
-
|
|
337
|
+
import { stopImages, exportPDF } from 'pixi-effects/present';
|
|
338
|
+
const pages = await stopImages(movie, { as: 'dataURL', type: 'image/jpeg', scale: 0.25 }); // one picture per PAGE: its last stop, fully built
|
|
328
339
|
// [{ stop, page, image }, …]; { which: 'stops' } is one per stop, { pick: 'first' } the first stop of each page; onImage(img) is called as each one is ready
|
|
329
|
-
const pdf = await
|
|
340
|
+
const pdf = await exportPDF(movie, { title: 'My talk' }); // a Blob: one PDF page per page of the deck (JPEG at the canvas size)
|
|
330
341
|
```
|
|
331
342
|
|
|
332
343
|
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).
|
|
@@ -411,7 +422,7 @@ el('play').onclick = () => (movie.isPlaying ? movie.pause() : movie.play());
|
|
|
411
422
|
el('seek').oninput = e => movie.gotoFrame(+e.target.value);
|
|
412
423
|
```
|
|
413
424
|
|
|
414
|
-
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
|
|
425
|
+
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.
|
|
415
426
|
|
|
416
427
|
## `Controller`
|
|
417
428
|
|
|
@@ -441,9 +452,9 @@ new Controller(movie: Movie, options: ControllerOptions)
|
|
|
441
452
|
|
|
442
453
|
interface ControllerOptions {
|
|
443
454
|
canvas: HTMLCanvasElement; // required
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
455
|
+
exportButton?: boolean; // default true; false hides ⬇ + popover
|
|
456
|
+
keyboard?: boolean; // default true
|
|
457
|
+
presentButton?: boolean; // default true: a movie with stops gets a Present button (marks on the seek bar are always shown)
|
|
447
458
|
pauseAtStops?: boolean; // default true: with stops, play / Space / click on the picture play to the next stop and pause; false plays straight through
|
|
448
459
|
clickToPlay?: boolean; // default true: a click / tap on the picture plays / pauses (like <video>); false leaves the canvas alone
|
|
449
460
|
className?: string; // default 'movie-controller'
|
|
@@ -478,7 +489,7 @@ controller.setTheme({ accent: null }); // null = back to the defau
|
|
|
478
489
|
| `trackHeight` | `--mc-track-height` | thickness of the progress bar (a number is px) | `3px` |
|
|
479
490
|
| `font` | `--mc-font` | `font-family` of the bar | system UI font |
|
|
480
491
|
|
|
481
|
-
A misspelt key warns with the valid ones.
|
|
492
|
+
A misspelt key warns with the valid ones. To restyle more than the theme covers, set the `--mc-*` custom properties (or override the `.movie-controller` classes) in your own CSS.
|
|
482
493
|
|
|
483
494
|
### `controller.destroy(): void`
|
|
484
495
|
|
|
@@ -504,7 +515,7 @@ The download is triggered by an in-page `<a download>` click, so the file lands
|
|
|
504
515
|
|
|
505
516
|
### Keyboard shortcuts
|
|
506
517
|
|
|
507
|
-
Active when `
|
|
518
|
+
Active when `keyboard: true` and the key target is not `<input>`/`<textarea>`/`<select>`/contenteditable.
|
|
508
519
|
|
|
509
520
|
| Key | Action |
|
|
510
521
|
| ----------- | --------------------------------------------------- |
|
|
@@ -520,50 +531,6 @@ Active when `enableKeyboardShortcuts: true` and the key target is not `<input>`/
|
|
|
520
531
|
|
|
521
532
|
The bar uses fixed colors (`#007AFF` for the active track / fill / confirm button, white for icons, `rgba(0,0,0,0.75)` gradient background). Override by adding stricter CSS rules under `.movie-controller`. A theming API (CSS custom properties) is on the roadmap.
|
|
522
533
|
|
|
523
|
-
---
|
|
524
|
-
|
|
525
|
-
## Helpers
|
|
526
|
-
|
|
527
|
-
These pure functions are exported from `pixi-effects/controller` for consumers who want to build custom controls or reuse the parsing utilities. All are side-effect-free.
|
|
528
|
-
|
|
529
|
-
```ts
|
|
530
|
-
import { formatTime, frameToPercent, pxToFrame, pxToFraction, extensionForMimeType }
|
|
531
|
-
from 'pixi-effects/controller';
|
|
532
|
-
```
|
|
533
|
-
|
|
534
|
-
### `formatTime(seconds: number): string`
|
|
535
|
-
|
|
536
|
-
Returns `M:SS` (no leading zero on minutes). `formatTime(125)` → `"2:05"`. Negatives clamp to zero.
|
|
537
|
-
|
|
538
|
-
### `frameToPercent(frame: number, totalFrames: number): number`
|
|
539
|
-
|
|
540
|
-
Returns 0..100 (clamped). `totalFrames <= 0` returns 0.
|
|
541
|
-
|
|
542
|
-
### `pxToFrame(clientX, rect, totalFrames): number`
|
|
543
|
-
|
|
544
|
-
Maps a pointer X coordinate (relative to viewport) inside a `DOMRect`-shaped object to a frame index 0..totalFrames. Rounded.
|
|
545
|
-
|
|
546
|
-
```ts
|
|
547
|
-
pxToFrame(150, { left: 100, width: 200 } as DOMRect, 100) // 25
|
|
548
|
-
```
|
|
549
|
-
|
|
550
|
-
### `pxToFraction(clientX, rect, inset?): number`
|
|
551
|
-
|
|
552
|
-
Same as `pxToFrame` but returns a normalized 0..1 fraction. The optional `inset` shrinks the active range by that many pixels on each side (used internally for the volume slider).
|
|
553
|
-
|
|
554
|
-
### `extensionForMimeType(mime: string): string`
|
|
555
|
-
|
|
556
|
-
Maps common video MIME types to file extensions. Falls back to `'mp4'`.
|
|
557
|
-
|
|
558
|
-
| MIME contains | Extension |
|
|
559
|
-
| ------------------- | --------- |
|
|
560
|
-
| `webm` | `webm` |
|
|
561
|
-
| `quicktime` / `mov` | `mov` |
|
|
562
|
-
| `matroska` / `mkv` | `mkv` |
|
|
563
|
-
| anything else | `mp4` |
|
|
564
|
-
|
|
565
|
-
---
|
|
566
|
-
|
|
567
534
|
## `pixi-effects/three`
|
|
568
535
|
|
|
569
536
|
Optional three.js integration, imported from its own entry so the core `pixi-effects` entry never touches three.js:
|