pixi-effects 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +117 -7
  3. package/ai/SKILL.md +44 -0
  4. package/ai/reference/cheatsheet.md +120 -0
  5. package/ai/reference/pitfalls.md +54 -0
  6. package/ai/reference/recipes.md +375 -0
  7. package/ai/template.html +82 -0
  8. package/ai/tools/save-image.py +24 -0
  9. package/dist/Base-BDw6dKRB.d.cts +92 -0
  10. package/dist/Base-Ba4Ta4ap.d.ts +92 -0
  11. package/dist/Composition-6FDH5OPM.cjs +13 -0
  12. package/dist/Composition-6FDH5OPM.cjs.map +1 -0
  13. package/dist/Composition-FEYAUMFI.js +4 -0
  14. package/dist/Composition-FEYAUMFI.js.map +1 -0
  15. package/dist/Controller.d.cts +2 -1
  16. package/dist/Controller.d.ts +2 -1
  17. package/dist/Movie-DMpaT67V.d.ts +179 -0
  18. package/dist/Movie-W8ISiEBB.d.cts +179 -0
  19. package/dist/chunk-DIJG2RSF.js +370 -0
  20. package/dist/chunk-DIJG2RSF.js.map +1 -0
  21. package/dist/chunk-PN5A6QA7.js +2031 -0
  22. package/dist/chunk-PN5A6QA7.js.map +1 -0
  23. package/dist/chunk-SIRVULFF.cjs +2049 -0
  24. package/dist/chunk-SIRVULFF.cjs.map +1 -0
  25. package/dist/chunk-ZL262ZRM.cjs +372 -0
  26. package/dist/chunk-ZL262ZRM.cjs.map +1 -0
  27. package/dist/index.cjs +371 -1798
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.d.cts +95 -24
  30. package/dist/index.d.ts +95 -24
  31. package/dist/index.js +364 -1796
  32. package/dist/index.js.map +1 -1
  33. package/dist/three.cjs +161 -0
  34. package/dist/three.cjs.map +1 -0
  35. package/dist/three.d.cts +79 -0
  36. package/dist/three.d.ts +79 -0
  37. package/dist/three.js +157 -0
  38. package/dist/three.js.map +1 -0
  39. package/dist/{Movie-Bp66rkaC.d.cts → types-3c8Vymgw.d.cts} +84 -81
  40. package/dist/{Movie-Bp66rkaC.d.ts → types-3c8Vymgw.d.ts} +84 -81
  41. package/docs/api.md +333 -0
  42. package/docs/dsl.md +1017 -0
  43. package/llms-full.txt +1966 -0
  44. package/llms.txt +30 -0
  45. package/package.json +19 -7
package/dist/index.js CHANGED
@@ -1,1774 +1,12 @@
1
- import { gsap } from 'gsap';
1
+ import { CompositionSequence } from './chunk-DIJG2RSF.js';
2
+ import { homeDistance, DEFAULT_FOV, DEG, resolveAt, collectPropKeys } from './chunk-PN5A6QA7.js';
3
+ export { registerSequenceType } from './chunk-PN5A6QA7.js';
2
4
  import * as PIXI from 'pixi.js';
3
- import { extensions, CullerPlugin, Container, Rectangle, Application, Assets, ExtensionType, Filter, UniformGroup, GpuProgram, GlProgram, defaultFilterVert, Sprite, Text, Texture, Graphics, Color, GraphicsPath } from 'pixi.js';
5
+ import { extensions, CullerPlugin, Application, Container, Rectangle, Culler, Assets, ExtensionType, Filter, UniformGroup, GpuProgram, GlProgram, defaultFilterVert } from 'pixi.js';
6
+ import { gsap } from 'gsap';
4
7
  import { PixiPlugin } from 'gsap/PixiPlugin';
5
8
  import { QUALITY_VERY_HIGH, QUALITY_HIGH, QUALITY_MEDIUM, QUALITY_LOW, QUALITY_VERY_LOW, Output, BufferTarget, CanvasSource, AudioBufferSource, Input, ALL_FORMATS, BlobSource, WavOutputFormat, Conversion, VideoSampleSink, MkvOutputFormat, WebMOutputFormat, MovOutputFormat, Mp4OutputFormat } from 'mediabunny';
6
9
 
7
- var __defProp = Object.defineProperty;
8
- var __getOwnPropNames = Object.getOwnPropertyNames;
9
- var __esm = (fn, res) => function __init() {
10
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
11
- };
12
- var __export = (target, all) => {
13
- for (var name in all)
14
- __defProp(target, name, { get: all[name], enumerable: true });
15
- };
16
-
17
- // src/expr/Parser.ts
18
- function isDigit(c) {
19
- return c >= "0" && c <= "9";
20
- }
21
- function isIdStart(c) {
22
- return c >= "a" && c <= "z" || c >= "A" && c <= "Z" || c === "_";
23
- }
24
- function isIdCont(c) {
25
- return isIdStart(c) || isDigit(c);
26
- }
27
- function tokenize(src) {
28
- const out = [];
29
- for (let i = 0; i < src.length; ) {
30
- const c = src[i];
31
- if (c === " " || c === " " || c === "\n" || c === "\r") {
32
- i++;
33
- continue;
34
- }
35
- if (isDigit(c) || c === "." && isDigit(src[i + 1] ?? "")) {
36
- let j = i + 1;
37
- while (j < src.length && (isDigit(src[j]) || src[j] === ".")) j++;
38
- const value = parseFloat(src.slice(i, j));
39
- if (Number.isNaN(value)) throw new Error(`bad number at ${i}`);
40
- out.push({ type: "num", value });
41
- i = j;
42
- continue;
43
- }
44
- if (isIdStart(c)) {
45
- let j = i + 1;
46
- while (j < src.length && isIdCont(src[j])) j++;
47
- out.push({ type: "id", name: src.slice(i, j) });
48
- i = j;
49
- continue;
50
- }
51
- if (c === "+" || c === "-" || c === "*" || c === "/") {
52
- out.push({ type: "op", op: c });
53
- i++;
54
- continue;
55
- }
56
- if (c === "(") {
57
- out.push({ type: "lparen" });
58
- i++;
59
- continue;
60
- }
61
- if (c === ")") {
62
- out.push({ type: "rparen" });
63
- i++;
64
- continue;
65
- }
66
- if (c === ",") {
67
- out.push({ type: "comma" });
68
- i++;
69
- continue;
70
- }
71
- throw new Error(`unexpected character "${c}" at ${i}`);
72
- }
73
- return out;
74
- }
75
- function parseExpr(c) {
76
- return parseAdd(c);
77
- }
78
- function parseAdd(c) {
79
- let node = parseMul(c);
80
- for (; ; ) {
81
- const t = c.peek();
82
- if (t?.type !== "op" || t.op !== "+" && t.op !== "-") break;
83
- c.next();
84
- node = { kind: "bin", op: t.op, left: node, right: parseMul(c) };
85
- }
86
- return node;
87
- }
88
- function parseMul(c) {
89
- let node = parseUnary(c);
90
- for (; ; ) {
91
- const t = c.peek();
92
- if (t?.type !== "op" || t.op !== "*" && t.op !== "/") break;
93
- c.next();
94
- node = { kind: "bin", op: t.op, left: node, right: parseUnary(c) };
95
- }
96
- return node;
97
- }
98
- function parseUnary(c) {
99
- const t = c.peek();
100
- if (t?.type === "op" && t.op === "-") {
101
- c.next();
102
- return { kind: "neg", child: parseUnary(c) };
103
- }
104
- if (t?.type === "op" && t.op === "+") {
105
- c.next();
106
- return parseUnary(c);
107
- }
108
- return parsePrimary(c);
109
- }
110
- function parsePrimary(c) {
111
- const t = c.next();
112
- if (!t) throw new Error("unexpected end of expression");
113
- if (t.type === "num") return { kind: "num", value: t.value };
114
- if (t.type === "lparen") {
115
- const node = parseExpr(c);
116
- const close = c.next();
117
- if (close?.type !== "rparen") throw new Error('expected ")"');
118
- return node;
119
- }
120
- if (t.type === "id") {
121
- if (c.peek()?.type === "lparen") {
122
- c.next();
123
- const args = [];
124
- if (c.peek()?.type !== "rparen") {
125
- args.push(parseExpr(c));
126
- while (c.peek()?.type === "comma") {
127
- c.next();
128
- args.push(parseExpr(c));
129
- }
130
- }
131
- const close = c.next();
132
- if (close?.type !== "rparen") throw new Error('expected ")"');
133
- return { kind: "call", name: t.name, args };
134
- }
135
- return { kind: "var", name: t.name };
136
- }
137
- throw new Error(`unexpected token "${t.type}"`);
138
- }
139
- function parse(src) {
140
- const c = new Cursor(tokenize(src));
141
- const node = parseExpr(c);
142
- if (c.peek()) throw new Error("trailing input");
143
- return node;
144
- }
145
- function evalNode(node, scope) {
146
- switch (node.kind) {
147
- case "num":
148
- return node.value;
149
- case "var": {
150
- const v = scope[node.name];
151
- if (typeof v !== "number") throw new Error(`undefined variable: ${node.name}`);
152
- return v;
153
- }
154
- case "neg":
155
- return -evalNode(node.child, scope);
156
- case "bin": {
157
- const l = evalNode(node.left, scope);
158
- const r = evalNode(node.right, scope);
159
- switch (node.op) {
160
- case "+":
161
- return l + r;
162
- case "-":
163
- return l - r;
164
- case "*":
165
- return l * r;
166
- case "/":
167
- return l / r;
168
- }
169
- }
170
- /* c8 ignore next 5 */
171
- // eslint-disable-next-line no-fallthrough
172
- case "call": {
173
- const fn = FUNCS[node.name];
174
- if (!fn) throw new Error(`unknown function: ${node.name}`);
175
- return fn(...node.args.map((a) => evalNode(a, scope)));
176
- }
177
- }
178
- }
179
- function isExpr(v) {
180
- return typeof v === "string" && v.length > 0;
181
- }
182
- function evaluateExpr(source, scope) {
183
- let ast = astCache.get(source);
184
- if (ast === void 0) {
185
- try {
186
- ast = parse(source);
187
- } catch (e) {
188
- console.warn(`pixi-effects: expression failed: ${source}`, e);
189
- ast = ZERO_NODE;
190
- }
191
- astCache.set(source, ast);
192
- }
193
- try {
194
- return evalNode(ast, scope);
195
- } catch (e) {
196
- console.warn(`pixi-effects: expression failed: ${source}`, e);
197
- return 0;
198
- }
199
- }
200
- var FUNCS, Cursor, ZERO_NODE, astCache;
201
- var init_Parser = __esm({
202
- "src/expr/Parser.ts"() {
203
- FUNCS = {
204
- min: Math.min,
205
- max: Math.max,
206
- abs: Math.abs,
207
- floor: Math.floor,
208
- ceil: Math.ceil,
209
- round: Math.round,
210
- sqrt: Math.sqrt,
211
- pow: Math.pow,
212
- sin: Math.sin,
213
- cos: Math.cos,
214
- tan: Math.tan
215
- };
216
- Cursor = class {
217
- constructor(tokens) {
218
- this.tokens = tokens;
219
- }
220
- tokens;
221
- i = 0;
222
- peek() {
223
- return this.tokens[this.i];
224
- }
225
- next() {
226
- return this.tokens[this.i++];
227
- }
228
- };
229
- ZERO_NODE = { kind: "num", value: 0 };
230
- astCache = /* @__PURE__ */ new Map();
231
- }
232
- });
233
-
234
- // src/expr/normalizeProps.ts
235
- function normalizeProps(input, scope, options = {}) {
236
- const skip = new Set(options.skipKeys ?? []);
237
- return walk(input, scope, skip, null);
238
- }
239
- function walk(value, scope, skip, currentKey) {
240
- if (Array.isArray(value)) {
241
- return value.map((v) => walk(v, scope, skip, currentKey));
242
- }
243
- if (value !== null && typeof value === "object") {
244
- const out = {};
245
- for (const k of Object.keys(value)) {
246
- out[k] = walk(value[k], scope, skip, k);
247
- }
248
- return out;
249
- }
250
- if (isExpr(value) && !(currentKey !== null && skip.has(currentKey))) {
251
- if (looksLikeColorString(value)) return value;
252
- return evaluateExpr(value, scope);
253
- }
254
- return value;
255
- }
256
- function looksLikeColorString(s) {
257
- if (s.length === 0) return false;
258
- if (s.charCodeAt(0) === 35) return true;
259
- return /^(rgb|hsl|oklab|oklch|lab|lch)\b/i.test(s);
260
- }
261
- var init_normalizeProps = __esm({
262
- "src/expr/normalizeProps.ts"() {
263
- init_Parser();
264
- }
265
- });
266
- function resolveAt(at, duration) {
267
- if (at === void 0 || at === null) return 0;
268
- return at < 0 ? duration + at : at;
269
- }
270
- function normalizeKeyframe(kf, parentDuration) {
271
- const at = resolveAt(kf.at, parentDuration);
272
- const duration = kf.duration ?? 0;
273
- const ease = kf.ease ?? "none";
274
- const hasSet = !!kf.set;
275
- const hasFrom = !!kf.from;
276
- const hasTo = !!kf.to;
277
- let kind;
278
- if (hasSet) kind = "set";
279
- else if (hasFrom && hasTo) kind = "fromTo";
280
- else if (hasFrom) kind = "from";
281
- else if (hasTo) kind = "to";
282
- else kind = "to";
283
- return { at, duration, ease, kind, set: kf.set, from: kf.from, to: kf.to };
284
- }
285
- function partitionProps(props) {
286
- const ownProps = {};
287
- const filterProps = {};
288
- for (const k of Object.keys(props)) {
289
- const m = k.match(/^filters\.([^.]+)\.(.+)$/);
290
- if (m) {
291
- const [, name, sub] = m;
292
- (filterProps[name] ??= {})[sub] = props[k];
293
- } else {
294
- ownProps[k] = props[k];
295
- }
296
- }
297
- return { ownProps, filterProps };
298
- }
299
- function findFilter(target, name) {
300
- if (!target?.filters) return null;
301
- for (const f of target.filters) {
302
- if (f?._name === name) return f;
303
- }
304
- return null;
305
- }
306
- function pixiwrap(props) {
307
- const out = {};
308
- const pixi = {};
309
- let usedPixi = false;
310
- for (const [k, v] of Object.entries(props)) {
311
- if (PIXI_SHORTHANDS.has(k)) {
312
- pixi[k] = v;
313
- usedPixi = true;
314
- } else {
315
- out[k] = v;
316
- }
317
- }
318
- if (usedPixi) out.pixi = pixi;
319
- return out;
320
- }
321
- function applyKeyframes(timeline, target, keyframes, parentDuration, scope, skipKeys = [], offset = 0) {
322
- for (const raw of keyframes ?? []) {
323
- const kf = normalizeKeyframe(raw, parentDuration);
324
- const at = offset + kf.at;
325
- if (kf.kind === "set") {
326
- const resolved = normalizeProps(kf.set, scope, { skipKeys });
327
- const { ownProps, filterProps } = partitionProps(resolved);
328
- if (Object.keys(ownProps).length > 0) timeline.set(target, pixiwrap(ownProps), at);
329
- for (const [name, props] of Object.entries(filterProps)) {
330
- const f = findFilter(target, name);
331
- if (!f) continue;
332
- timeline.set(f, props, at);
333
- }
334
- } else if (kf.kind === "to") {
335
- const resolved = normalizeProps(kf.to, scope, { skipKeys });
336
- const { ownProps, filterProps } = partitionProps(resolved);
337
- if (Object.keys(ownProps).length > 0)
338
- timeline.to(target, { ...pixiwrap(ownProps), duration: kf.duration, ease: kf.ease }, at);
339
- for (const [name, props] of Object.entries(filterProps)) {
340
- const f = findFilter(target, name);
341
- if (!f) continue;
342
- timeline.to(f, { ...props, duration: kf.duration, ease: kf.ease }, at);
343
- }
344
- } else if (kf.kind === "from") {
345
- const resolved = normalizeProps(kf.from, scope, { skipKeys });
346
- const { ownProps, filterProps } = partitionProps(resolved);
347
- if (Object.keys(ownProps).length > 0)
348
- timeline.from(target, { ...pixiwrap(ownProps), duration: kf.duration, ease: kf.ease }, at);
349
- for (const [name, props] of Object.entries(filterProps)) {
350
- const f = findFilter(target, name);
351
- if (!f) continue;
352
- timeline.from(f, { ...props, duration: kf.duration, ease: kf.ease }, at);
353
- }
354
- } else {
355
- const fromResolved = normalizeProps(kf.from, scope, { skipKeys });
356
- const toResolved = normalizeProps(kf.to, scope, { skipKeys });
357
- const fromSplit = partitionProps(fromResolved);
358
- const toSplit = partitionProps(toResolved);
359
- const ownKeys = /* @__PURE__ */ new Set([...Object.keys(fromSplit.ownProps), ...Object.keys(toSplit.ownProps)]);
360
- if (ownKeys.size > 0) {
361
- timeline.fromTo(
362
- target,
363
- { ...pixiwrap(fromSplit.ownProps) },
364
- { ...pixiwrap(toSplit.ownProps), duration: kf.duration, ease: kf.ease },
365
- at
366
- );
367
- }
368
- const filterNames = /* @__PURE__ */ new Set([
369
- ...Object.keys(fromSplit.filterProps),
370
- ...Object.keys(toSplit.filterProps)
371
- ]);
372
- for (const name of filterNames) {
373
- const f = findFilter(target, name);
374
- if (!f) continue;
375
- timeline.fromTo(
376
- f,
377
- { ...fromSplit.filterProps[name] ?? {} },
378
- { ...toSplit.filterProps[name] ?? {}, duration: kf.duration, ease: kf.ease },
379
- at
380
- );
381
- }
382
- }
383
- }
384
- }
385
- function applyInitial(target, initial, scope, skipKeys = []) {
386
- if (!initial) return;
387
- const resolved = normalizeProps(initial, scope, { skipKeys });
388
- const { ownProps, filterProps } = partitionProps(resolved);
389
- if (Object.keys(ownProps).length > 0) gsap.set(target, pixiwrap(ownProps));
390
- for (const [name, props] of Object.entries(filterProps)) {
391
- const f = findFilter(target, name);
392
- if (!f) continue;
393
- gsap.set(f, props);
394
- }
395
- }
396
- var PIXI_SHORTHANDS;
397
- var init_Timeline = __esm({
398
- "src/core/Timeline.ts"() {
399
- init_normalizeProps();
400
- PIXI_SHORTHANDS = /* @__PURE__ */ new Set([
401
- "scale",
402
- "scaleX",
403
- "scaleY",
404
- "anchor",
405
- "anchorX",
406
- "anchorY",
407
- "pivot",
408
- "pivotX",
409
- "pivotY",
410
- "skew",
411
- "skewX",
412
- "skewY",
413
- "rotation",
414
- "position",
415
- "positionX",
416
- "positionY",
417
- "tilePosition",
418
- "tilePositionX",
419
- "tilePositionY",
420
- "tileScale",
421
- "tileScaleX",
422
- "tileScaleY",
423
- "tint",
424
- "colorize",
425
- "colorizeAmount",
426
- "colorMatrixFilter",
427
- "blur",
428
- "blurX",
429
- "blurY",
430
- "blurPadding",
431
- "autoAlpha",
432
- "lineColor",
433
- "lineAlpha",
434
- "fillColor",
435
- "fillAlpha"
436
- ]);
437
- }
438
- });
439
-
440
- // src/expr/Scope.ts
441
- function buildScope(sequence, parent, root) {
442
- const w = sequence.intrinsicWidth || 0;
443
- const h = sequence.intrinsicHeight || 0;
444
- const W = parent?.width ?? root.width;
445
- const H = parent?.height ?? root.height;
446
- const GW = root.width;
447
- const GH = root.height;
448
- const contain = w === 0 || h === 0 ? 1 : Math.min(W / w, H / h);
449
- const cover = w === 0 || h === 0 ? 1 : Math.max(W / w, H / h);
450
- return {
451
- w,
452
- h,
453
- W,
454
- H,
455
- GW,
456
- GH,
457
- contain,
458
- cover,
459
- t: sequence.at ?? 0,
460
- d: sequence.duration ?? parent?.duration ?? root.duration,
461
- T: parent?.duration ?? root.duration
462
- };
463
- }
464
- var init_Scope = __esm({
465
- "src/expr/Scope.ts"() {
466
- }
467
- });
468
- function toRgb01(input) {
469
- if (Array.isArray(input)) return [input[0], input[1], input[2]];
470
- const c = new Color(input);
471
- return [c.red, c.green, c.blue];
472
- }
473
- var GL_FRAGMENT, WGSL_SOURCE, ChromaKeyFilter;
474
- var init_ChromaKey = __esm({
475
- "src/filters/ChromaKey.ts"() {
476
- GL_FRAGMENT = `
477
- in vec2 vTextureCoord;
478
- uniform sampler2D uTexture;
479
- uniform vec3 uKeyColor;
480
- uniform float uThreshold;
481
- uniform float uSmoothing;
482
- uniform float uSpill;
483
-
484
- out vec4 finalColor;
485
-
486
- void main(void) {
487
- vec4 raw = texture(uTexture, vTextureCoord);
488
-
489
- // Pixi passes pre-multiplied alpha into filters. Un-premultiply so the
490
- // chromakey distance is computed against the source color, not a darkened
491
- // version that drifts away from the key as sprite.alpha drops.
492
- vec3 rgb = raw.a > 0.0 ? raw.rgb / raw.a : vec3(0.0);
493
-
494
- vec3 ycbcr = vec3(
495
- 0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b,
496
- -0.169 * rgb.r - 0.331 * rgb.g + 0.5 * rgb.b + 0.5,
497
- 0.5 * rgb.r - 0.419 * rgb.g - 0.081 * rgb.b + 0.5
498
- );
499
- vec3 keyYcbcr = vec3(
500
- 0.299 * uKeyColor.r + 0.587 * uKeyColor.g + 0.114 * uKeyColor.b,
501
- -0.169 * uKeyColor.r - 0.331 * uKeyColor.g + 0.5 * uKeyColor.b + 0.5,
502
- 0.5 * uKeyColor.r - 0.419 * uKeyColor.g - 0.081 * uKeyColor.b + 0.5
503
- );
504
-
505
- float dist = length(ycbcr - keyYcbcr);
506
- float alpha = smoothstep(uThreshold - uSmoothing, uThreshold + uSmoothing, dist);
507
-
508
- vec3 despilled = rgb;
509
- if (uSpill > 0.0) {
510
- float spillAmount = max(0.0, rgb.g - max(rgb.r, rgb.b));
511
- despilled.g -= spillAmount * uSpill;
512
- }
513
-
514
- float outA = raw.a * alpha;
515
- finalColor = vec4(despilled * outA, outA);
516
- }
517
- `;
518
- WGSL_SOURCE = `
519
- struct GlobalFilterUniforms {
520
- uInputSize: vec4<f32>,
521
- uInputPixel: vec4<f32>,
522
- uInputClamp: vec4<f32>,
523
- uOutputFrame: vec4<f32>,
524
- uGlobalFrame: vec4<f32>,
525
- uOutputTexture: vec4<f32>,
526
- };
527
-
528
- struct ChromaUniforms {
529
- uKeyColor: vec3<f32>,
530
- uThreshold: f32,
531
- uSmoothing: f32,
532
- uSpill: f32,
533
- };
534
-
535
- @group(0) @binding(0) var<uniform> gfu: GlobalFilterUniforms;
536
- @group(0) @binding(1) var uTexture: texture_2d<f32>;
537
- @group(0) @binding(2) var uSampler : sampler;
538
- @group(1) @binding(0) var<uniform> chromaUniforms : ChromaUniforms;
539
-
540
- struct VSOutput {
541
- @builtin(position) position: vec4<f32>,
542
- @location(0) uv : vec2<f32>,
543
- };
544
-
545
- fn filterVertexPosition(aPosition : vec2<f32>) -> vec4<f32> {
546
- var position = aPosition * gfu.uOutputFrame.zw + gfu.uOutputFrame.xy;
547
- position.x = position.x * (2.0 / gfu.uOutputTexture.x) - 1.0;
548
- position.y = position.y * (2.0 * gfu.uOutputTexture.z / gfu.uOutputTexture.y) - gfu.uOutputTexture.z;
549
- return vec4<f32>(position, 0.0, 1.0);
550
- }
551
-
552
- fn filterTextureCoord(aPosition : vec2<f32>) -> vec2<f32> {
553
- return aPosition * (gfu.uOutputFrame.zw * gfu.uInputSize.zw);
554
- }
555
-
556
- // NOTE on whitespace: PIXI v8's WGSL attribute extractor is regex-based and
557
- // expects the noise/blur reference format \u2014 multi-line param list + space
558
- // before the colon. A compact one-liner with no space before the colon parses
559
- // as having NO vertex attributes, which then causes a "Vertex attribute slot 0
560
- // not present in the VertexState" pipeline validation error.
561
- @vertex
562
- fn mainVertex(
563
- @location(0) aPosition : vec2<f32>,
564
- ) -> VSOutput {
565
- return VSOutput(
566
- filterVertexPosition(aPosition),
567
- filterTextureCoord(aPosition),
568
- );
569
- }
570
-
571
- @fragment
572
- fn mainFragment(@location(0) uv : vec2<f32>) -> @location(0) vec4<f32> {
573
- let raw = textureSample(uTexture, uSampler, uv);
574
-
575
- // Un-premultiply (see comment in the GLSL version above).
576
- var rgb: vec3<f32>;
577
- if (raw.a > 0.0) {
578
- rgb = raw.rgb / raw.a;
579
- } else {
580
- rgb = vec3<f32>(0.0);
581
- }
582
-
583
- let key = chromaUniforms.uKeyColor;
584
-
585
- let ycbcr = vec3<f32>(
586
- 0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b,
587
- -0.169 * rgb.r - 0.331 * rgb.g + 0.5 * rgb.b + 0.5,
588
- 0.5 * rgb.r - 0.419 * rgb.g - 0.081 * rgb.b + 0.5,
589
- );
590
- let keyYcbcr = vec3<f32>(
591
- 0.299 * key.r + 0.587 * key.g + 0.114 * key.b,
592
- -0.169 * key.r - 0.331 * key.g + 0.5 * key.b + 0.5,
593
- 0.5 * key.r - 0.419 * key.g - 0.081 * key.b + 0.5,
594
- );
595
-
596
- let dist = length(ycbcr - keyYcbcr);
597
- let alpha = smoothstep(
598
- chromaUniforms.uThreshold - chromaUniforms.uSmoothing,
599
- chromaUniforms.uThreshold + chromaUniforms.uSmoothing,
600
- dist,
601
- );
602
-
603
- var despilled = rgb;
604
- if (chromaUniforms.uSpill > 0.0) {
605
- let spillAmount = max(0.0, rgb.g - max(rgb.r, rgb.b));
606
- despilled.g = despilled.g - spillAmount * chromaUniforms.uSpill;
607
- }
608
-
609
- let outA = raw.a * alpha;
610
- return vec4<f32>(despilled * outA, outA);
611
- }
612
- `;
613
- ChromaKeyFilter = class extends Filter {
614
- constructor(options = {}) {
615
- const {
616
- keyColor = "#00ff00",
617
- threshold = 0.4,
618
- smoothing = 0.1,
619
- spill = 0.2
620
- } = options;
621
- const glProgram = GlProgram.from({
622
- vertex: defaultFilterVert,
623
- fragment: GL_FRAGMENT,
624
- name: "chroma-key-filter"
625
- });
626
- const gpuProgram = GpuProgram.from({
627
- vertex: { source: WGSL_SOURCE, entryPoint: "mainVertex" },
628
- fragment: { source: WGSL_SOURCE, entryPoint: "mainFragment" }
629
- });
630
- super({
631
- glProgram,
632
- gpuProgram,
633
- resources: {
634
- chromaUniforms: new UniformGroup({
635
- uKeyColor: { value: toRgb01(keyColor), type: "vec3<f32>" },
636
- uThreshold: { value: threshold, type: "f32" },
637
- uSmoothing: { value: smoothing, type: "f32" },
638
- uSpill: { value: spill, type: "f32" }
639
- })
640
- }
641
- });
642
- }
643
- get threshold() {
644
- return this.resources.chromaUniforms.uniforms.uThreshold;
645
- }
646
- set threshold(v) {
647
- this.resources.chromaUniforms.uniforms.uThreshold = v;
648
- }
649
- get smoothing() {
650
- return this.resources.chromaUniforms.uniforms.uSmoothing;
651
- }
652
- set smoothing(v) {
653
- this.resources.chromaUniforms.uniforms.uSmoothing = v;
654
- }
655
- get spill() {
656
- return this.resources.chromaUniforms.uniforms.uSpill;
657
- }
658
- set spill(v) {
659
- this.resources.chromaUniforms.uniforms.uSpill = v;
660
- }
661
- setKeyColor(input) {
662
- const rgb = toRgb01(input);
663
- const u = this.resources.chromaUniforms.uniforms.uKeyColor;
664
- u[0] = rgb[0];
665
- u[1] = rgb[1];
666
- u[2] = rgb[2];
667
- }
668
- };
669
- }
670
- });
671
-
672
- // src/filters/index.ts
673
- function looksLikePixiFilter(x) {
674
- return !!x && typeof x === "object" && typeof x.apply === "function";
675
- }
676
- function createFilter(spec) {
677
- if (spec.type === "custom") {
678
- const inst2 = spec.filter;
679
- if (!looksLikePixiFilter(inst2)) {
680
- throw new Error('pixi-effects: { type: "custom" } requires a `filter` that is a PIXI Filter instance (duck-typed: must have an `apply` method).');
681
- }
682
- const named = inst2;
683
- named._name = spec.name;
684
- return named;
685
- }
686
- const Cls = registry[spec.type];
687
- if (!Cls) throw new Error(`pixi-effects: unknown filter type "${spec.type}"`);
688
- const { type: _t, name, ...params } = spec;
689
- const inst = new Cls(params);
690
- inst._name = name;
691
- return inst;
692
- }
693
- var registry;
694
- var init_filters = __esm({
695
- "src/filters/index.ts"() {
696
- init_ChromaKey();
697
- registry = {
698
- chromaKey: ChromaKeyFilter
699
- };
700
- }
701
- });
702
- var Sequence;
703
- var init_Base = __esm({
704
- "src/sequences/Base.ts"() {
705
- init_Timeline();
706
- init_Scope();
707
- init_filters();
708
- Sequence = class {
709
- spec;
710
- parent;
711
- root;
712
- target = null;
713
- intrinsicWidth = 0;
714
- intrinsicHeight = 0;
715
- at;
716
- duration;
717
- filters = [];
718
- /**
719
- * Mask sequence built from `spec.mask`, if any. Built and added to the
720
- * scene graph by the parent CompositionSequence; rendered as a mask
721
- * (not a normal child) by PIXI when `target.mask = maskSequence.target`.
722
- */
723
- maskSequence = null;
724
- constructor(spec, parent, root) {
725
- this.spec = spec;
726
- this.parent = parent;
727
- this.root = root;
728
- this.at = spec.at ?? 0;
729
- this.duration = spec.duration;
730
- }
731
- buildFilters() {
732
- const specs = this.spec.filters ?? [];
733
- this.filters = specs.map(createFilter);
734
- if (this.target && "filters" in this.target) {
735
- this.target.filters = this.filters;
736
- }
737
- const fa = this.spec.filterArea;
738
- if (fa && this.target) {
739
- this.target.filterArea = new Rectangle(fa.x, fa.y, fa.width, fa.height);
740
- }
741
- }
742
- scope() {
743
- return buildScope(this, this.parent, this.root);
744
- }
745
- bindTimeline(timeline, offset = 0) {
746
- if (!this.target) return;
747
- const scope = this.scope();
748
- applyInitial(this.target, this.spec.initial, scope);
749
- applyKeyframes(timeline, this.target, this.spec.keyframes, this.duration, scope, [], offset);
750
- const startTime = offset + this.at;
751
- const endTime = startTime + this.duration;
752
- this.target.renderable = startTime <= 0;
753
- timeline.set(this.target, { renderable: true }, startTime);
754
- timeline.set(this.target, { renderable: false }, endTime);
755
- }
756
- collectAudio(_out, _baseTime) {
757
- }
758
- destroy() {
759
- this.maskSequence?.destroy();
760
- this.maskSequence = null;
761
- this.target?.destroy?.();
762
- this.target = null;
763
- }
764
- };
765
- }
766
- });
767
-
768
- // src/expr/colorInterp.ts
769
- function buildColorInterp(from, to, space) {
770
- const fromRGBA = parseToRGBA(from);
771
- const toRGBA = parseToRGBA(to);
772
- const fromOk = rgbToOklab(fromRGBA);
773
- const toOk = rgbToOklab(toRGBA);
774
- if (space === "oklab") {
775
- return (t) => {
776
- const L = lerp(fromOk.L, toOk.L, t);
777
- const a = lerp(fromOk.a, toOk.a, t);
778
- const b = lerp(fromOk.b, toOk.b, t);
779
- const alpha = lerp(fromRGBA.a, toRGBA.a, t);
780
- return rgbaString(oklabToRgb({ L, a, b }), alpha);
781
- };
782
- }
783
- const fromLCH = oklabToOklch(fromOk);
784
- const toLCH = oklabToOklch(toOk);
785
- if (fromLCH.C < 1e-4) fromLCH.h = toLCH.h;
786
- if (toLCH.C < 1e-4) toLCH.h = fromLCH.h;
787
- const hueDelta = shortestHueDelta(fromLCH.h, toLCH.h);
788
- return (t) => {
789
- const L = lerp(fromLCH.L, toLCH.L, t);
790
- const C = lerp(fromLCH.C, toLCH.C, t);
791
- const h = fromLCH.h + hueDelta * t;
792
- const alpha = lerp(fromRGBA.a, toRGBA.a, t);
793
- const ab = oklchToOklab({ L, C, h });
794
- return rgbaString(oklabToRgb(ab), alpha);
795
- };
796
- }
797
- function parseToRGBA(input) {
798
- if (typeof input === "number") {
799
- return { r: (input >> 16 & 255) / 255, g: (input >> 8 & 255) / 255, b: (input & 255) / 255, a: 1 };
800
- }
801
- const s = input.trim();
802
- if (s.startsWith("#")) return parseHex(s);
803
- if (s.startsWith("rgb")) return parseRgbFunc(s);
804
- return { r: 0, g: 0, b: 0, a: 1 };
805
- }
806
- function parseHex(s) {
807
- const hex = s.slice(1);
808
- let r = 0, g = 0, b = 0, a = 1;
809
- if (hex.length === 3 || hex.length === 4) {
810
- r = parseInt(hex[0] + hex[0], 16) / 255;
811
- g = parseInt(hex[1] + hex[1], 16) / 255;
812
- b = parseInt(hex[2] + hex[2], 16) / 255;
813
- if (hex.length === 4) a = parseInt(hex[3] + hex[3], 16) / 255;
814
- } else if (hex.length === 6 || hex.length === 8) {
815
- r = parseInt(hex.slice(0, 2), 16) / 255;
816
- g = parseInt(hex.slice(2, 4), 16) / 255;
817
- b = parseInt(hex.slice(4, 6), 16) / 255;
818
- if (hex.length === 8) a = parseInt(hex.slice(6, 8), 16) / 255;
819
- }
820
- return { r, g, b, a };
821
- }
822
- function parseRgbFunc(s) {
823
- const inner = s.replace(/^rgba?\(/, "").replace(/\)$/, "");
824
- const parts = inner.split(/[,\s/]+/).filter(Boolean).map(parseFloat);
825
- return {
826
- r: (parts[0] ?? 0) / 255,
827
- g: (parts[1] ?? 0) / 255,
828
- b: (parts[2] ?? 0) / 255,
829
- a: parts.length > 3 ? parts[3] ?? 1 : 1
830
- };
831
- }
832
- function rgbaString(rgb, alpha) {
833
- const r = Math.round(clamp01(rgb.r) * 255);
834
- const g = Math.round(clamp01(rgb.g) * 255);
835
- const b = Math.round(clamp01(rgb.b) * 255);
836
- return `rgba(${r},${g},${b},${alpha})`;
837
- }
838
- function srgbToLinear(c) {
839
- return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
840
- }
841
- function linearToSrgb(c) {
842
- return c <= 31308e-7 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
843
- }
844
- function rgbToOklab(rgba) {
845
- const r = srgbToLinear(rgba.r);
846
- const g = srgbToLinear(rgba.g);
847
- const b = srgbToLinear(rgba.b);
848
- const l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b;
849
- const m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b;
850
- const s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b;
851
- const l_ = Math.cbrt(l);
852
- const m_ = Math.cbrt(m);
853
- const s_ = Math.cbrt(s);
854
- return {
855
- L: 0.2104542553 * l_ + 0.793617785 * m_ - 0.0040720468 * s_,
856
- a: 1.9779984951 * l_ - 2.428592205 * m_ + 0.4505937099 * s_,
857
- b: 0.0259040371 * l_ + 0.7827717662 * m_ - 0.808675766 * s_
858
- };
859
- }
860
- function oklabToRgb(lab) {
861
- const l_ = lab.L + 0.3963377774 * lab.a + 0.2158037573 * lab.b;
862
- const m_ = lab.L - 0.1055613458 * lab.a - 0.0638541728 * lab.b;
863
- const s_ = lab.L - 0.0894841775 * lab.a - 1.291485548 * lab.b;
864
- const l = l_ * l_ * l_;
865
- const m = m_ * m_ * m_;
866
- const s = s_ * s_ * s_;
867
- return {
868
- r: linearToSrgb(4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s),
869
- g: linearToSrgb(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s),
870
- b: linearToSrgb(-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s)
871
- };
872
- }
873
- function oklabToOklch(lab) {
874
- return { L: lab.L, C: Math.hypot(lab.a, lab.b), h: Math.atan2(lab.b, lab.a) };
875
- }
876
- function oklchToOklab(lch) {
877
- return { L: lch.L, a: lch.C * Math.cos(lch.h), b: lch.C * Math.sin(lch.h) };
878
- }
879
- function lerp(a, b, t) {
880
- return a + (b - a) * t;
881
- }
882
- function clamp01(v) {
883
- return v < 0 ? 0 : v > 1 ? 1 : v;
884
- }
885
- function shortestHueDelta(from, to) {
886
- const TWO_PI = Math.PI * 2;
887
- let d = (to - from) % TWO_PI;
888
- if (d > Math.PI) d -= TWO_PI;
889
- if (d < -Math.PI) d += TWO_PI;
890
- return d;
891
- }
892
- var init_colorInterp = __esm({
893
- "src/expr/colorInterp.ts"() {
894
- }
895
- });
896
- function tweenColor(timeline, target, key, fromValue, toValue, duration, ease, at, colorSpace, onUpdate) {
897
- let interp = null;
898
- const proxy = { p: 0 };
899
- timeline.fromTo(
900
- proxy,
901
- { p: 0 },
902
- {
903
- p: 1,
904
- duration,
905
- ease,
906
- onStart: () => {
907
- const start = fromValue !== void 0 ? fromValue : target[key];
908
- if (start === void 0) return;
909
- interp = colorSpace === "rgb" ? gsap.utils.interpolate(start, toValue) : buildColorInterp(start, toValue, colorSpace);
910
- },
911
- onUpdate: () => {
912
- if (interp) {
913
- target[key] = interp(proxy.p);
914
- if (onUpdate) onUpdate();
915
- }
916
- }
917
- },
918
- at
919
- );
920
- }
921
- var init_colorTween = __esm({
922
- "src/expr/colorTween.ts"() {
923
- init_colorInterp();
924
- }
925
- });
926
- function stripKeys(props, keys) {
927
- if (!props) return props;
928
- const drop = new Set(keys);
929
- const out = {};
930
- let changed = false;
931
- for (const k of Object.keys(props)) {
932
- if (drop.has(k)) {
933
- changed = true;
934
- continue;
935
- }
936
- out[k] = props[k];
937
- }
938
- return changed ? out : props;
939
- }
940
- function stripKeysInKeyframe(kf, keys) {
941
- return {
942
- ...kf,
943
- set: stripKeys(kf.set, keys),
944
- to: stripKeys(kf.to, keys),
945
- from: stripKeys(kf.from, keys)
946
- };
947
- }
948
- var ImageSequence;
949
- var init_Image = __esm({
950
- "src/sequences/Image.ts"() {
951
- init_Base();
952
- init_Timeline();
953
- init_colorTween();
954
- ImageSequence = class extends Sequence {
955
- async build() {
956
- const texture = await Assets.get(this.spec.asset);
957
- const sprite = new Sprite({ texture, label: this.spec.name });
958
- sprite.cullable = true;
959
- this.target = sprite;
960
- this.intrinsicWidth = texture.width;
961
- this.intrinsicHeight = texture.height;
962
- if (this.duration === void 0) {
963
- this.duration = this.parent?.duration ?? this.root.duration;
964
- }
965
- this.buildFilters();
966
- }
967
- bindTimeline(timeline, offset = 0) {
968
- if (!this.target) return;
969
- const colorSpace = this.spec.colorSpace;
970
- if (!colorSpace || colorSpace === "rgb") {
971
- super.bindTimeline(timeline, offset);
972
- return;
973
- }
974
- const scope = this.scope();
975
- const initial = stripKeys(this.spec.initial, ["tint"]);
976
- const keyframes = this.spec.keyframes ? this.spec.keyframes.map((kf) => stripKeysInKeyframe(kf, ["tint"])) : void 0;
977
- applyInitial(this.target, initial, scope);
978
- applyKeyframes(timeline, this.target, keyframes, this.duration, scope, [], offset);
979
- const initTint = this.spec.initial?.tint;
980
- if (initTint !== void 0) this.target.tint = initTint;
981
- for (const kf of this.spec.keyframes ?? []) {
982
- const at = offset + resolveAt(kf.at, this.duration);
983
- const duration = kf.duration ?? 0;
984
- const ease = kf.ease ?? "none";
985
- const fromTint = kf.from?.tint;
986
- const toTint = kf.to?.tint;
987
- const setTint = kf.set?.tint;
988
- if (setTint !== void 0) {
989
- timeline.call(() => {
990
- this.target.tint = setTint;
991
- }, [], at);
992
- }
993
- if (toTint !== void 0) {
994
- tweenColor(
995
- timeline,
996
- this.target,
997
- "tint",
998
- fromTint,
999
- toTint,
1000
- duration,
1001
- ease,
1002
- at,
1003
- colorSpace
1004
- );
1005
- }
1006
- }
1007
- const startTime = offset + this.at;
1008
- const endTime = startTime + this.duration;
1009
- this.target.renderable = startTime <= 0;
1010
- timeline.set(this.target, { renderable: true }, startTime);
1011
- timeline.set(this.target, { renderable: false }, endTime);
1012
- }
1013
- };
1014
- }
1015
- });
1016
- function runSuperWithStrippedSpec(inst, strippedSpec, timeline, offset) {
1017
- const originalSpec = inst.spec;
1018
- inst.spec = strippedSpec;
1019
- try {
1020
- Sequence.prototype.bindTimeline.call(inst, timeline, offset);
1021
- } finally {
1022
- inst.spec = originalSpec;
1023
- }
1024
- }
1025
- function stripFill(spec) {
1026
- return {
1027
- ...spec,
1028
- initial: stripKey(spec.initial, "fill"),
1029
- keyframes: spec.keyframes ? spec.keyframes.map((kf) => ({
1030
- ...kf,
1031
- set: stripKey(kf.set, "fill"),
1032
- to: stripKey(kf.to, "fill"),
1033
- from: stripKey(kf.from, "fill")
1034
- })) : void 0
1035
- };
1036
- }
1037
- function stripKey(props, key) {
1038
- if (!props) return props;
1039
- if (!(key in props)) return props;
1040
- const out = {};
1041
- for (const k of Object.keys(props)) if (k !== key) out[k] = props[k];
1042
- return out;
1043
- }
1044
- function bindFillKeyframes(timeline, text, keyframes, parentDuration, offset, colorSpace) {
1045
- const state = { fill: text.style.fill };
1046
- const writeFill = () => {
1047
- text.style.fill = state.fill;
1048
- };
1049
- for (const kf of keyframes) {
1050
- const at = offset + resolveAt(kf.at, parentDuration);
1051
- const duration = kf.duration ?? 0;
1052
- const ease = kf.ease ?? "none";
1053
- const setFill = pickFill(kf.set);
1054
- const fromFill = pickFill(kf.from);
1055
- const toFill = pickFill(kf.to);
1056
- if (setFill !== void 0) {
1057
- timeline.call(() => {
1058
- state.fill = setFill;
1059
- writeFill();
1060
- }, [], at);
1061
- }
1062
- if (toFill !== void 0) {
1063
- tweenColor(
1064
- timeline,
1065
- state,
1066
- "fill",
1067
- fromFill,
1068
- toFill,
1069
- duration,
1070
- ease,
1071
- at,
1072
- colorSpace,
1073
- writeFill
1074
- );
1075
- }
1076
- }
1077
- }
1078
- function pickFill(props) {
1079
- if (!props) return void 0;
1080
- const v = props.fill;
1081
- if (typeof v === "string" || typeof v === "number") return v;
1082
- return void 0;
1083
- }
1084
- var STYLE_OPAQUE_KEYS, TextSequence;
1085
- var init_Text = __esm({
1086
- "src/sequences/Text.ts"() {
1087
- init_Base();
1088
- init_normalizeProps();
1089
- init_Timeline();
1090
- init_colorTween();
1091
- STYLE_OPAQUE_KEYS = ["fontFamily", "fill", "align", "fontStyle", "fontWeight"];
1092
- TextSequence = class extends Sequence {
1093
- async build() {
1094
- const baseStyle = {
1095
- fontFamily: "Arial",
1096
- fontSize: 36,
1097
- fill: "#ffffff",
1098
- align: "center"
1099
- };
1100
- const text = new Text({
1101
- text: this.spec.text ?? "",
1102
- style: baseStyle,
1103
- label: this.spec.name
1104
- });
1105
- text.cullable = true;
1106
- this.target = text;
1107
- this.intrinsicWidth = text.width;
1108
- this.intrinsicHeight = text.height;
1109
- if (this.duration === void 0) {
1110
- this.duration = this.parent?.duration ?? this.root.duration;
1111
- }
1112
- if (this.spec.style) {
1113
- const scope = this.scope();
1114
- const resolved = normalizeProps(
1115
- this.spec.style,
1116
- scope,
1117
- { skipKeys: STYLE_OPAQUE_KEYS }
1118
- );
1119
- for (const k of Object.keys(resolved)) {
1120
- text.style[k] = resolved[k];
1121
- }
1122
- }
1123
- this.buildFilters();
1124
- }
1125
- bindTimeline(timeline, offset = 0) {
1126
- if (!this.target) return;
1127
- const colorSpace = this.spec.colorSpace ?? "rgb";
1128
- const stripped = stripFill(this.spec);
1129
- runSuperWithStrippedSpec(this, stripped, timeline, offset);
1130
- bindFillKeyframes(timeline, this.target, this.spec.keyframes ?? [], this.duration, offset, colorSpace);
1131
- }
1132
- };
1133
- }
1134
- });
1135
- var AudioSequence;
1136
- var init_Audio = __esm({
1137
- "src/sequences/Audio.ts"() {
1138
- init_Base();
1139
- init_Timeline();
1140
- AudioSequence = class extends Sequence {
1141
- _audioBuffer = null;
1142
- async build() {
1143
- const data = await Assets.get(this.spec.asset);
1144
- this._audioBuffer = data.audioBuffer;
1145
- if (this.duration === void 0) {
1146
- this.duration = this.spec.duration ?? data.duration ?? this.root.duration;
1147
- }
1148
- this.target = null;
1149
- }
1150
- bindTimeline(_timeline) {
1151
- }
1152
- collectAudio(out, baseTime) {
1153
- if (!this._audioBuffer) return;
1154
- const initialVolume = this.spec.volume ?? 1;
1155
- const volumeKeyframes = [];
1156
- const dur = this.duration;
1157
- for (const raw of this.spec.keyframes ?? []) {
1158
- const kf = normalizeKeyframe(raw, dur);
1159
- const set = kf.set;
1160
- const to = kf.to;
1161
- const from = kf.from;
1162
- if (kf.kind === "set" && set && "volume" in set) {
1163
- volumeKeyframes.push({ time: baseTime + this.at + kf.at, value: set.volume });
1164
- } else if (kf.kind === "to" && to && "volume" in to) {
1165
- volumeKeyframes.push({ time: baseTime + this.at + kf.at + kf.duration, value: to.volume });
1166
- } else if (kf.kind === "fromTo" && to && "volume" in to) {
1167
- volumeKeyframes.push({ time: baseTime + this.at + kf.at, value: from?.volume ?? initialVolume });
1168
- volumeKeyframes.push({ time: baseTime + this.at + kf.at + kf.duration, value: to.volume });
1169
- } else if (kf.kind === "from" && from && "volume" in from) {
1170
- volumeKeyframes.push({ time: baseTime + this.at + kf.at, value: from.volume });
1171
- volumeKeyframes.push({ time: baseTime + this.at + kf.at + kf.duration, value: initialVolume });
1172
- }
1173
- }
1174
- out.push({
1175
- buffer: this._audioBuffer,
1176
- loop: !!this.spec.loop,
1177
- start: baseTime + this.at,
1178
- end: baseTime + this.at + dur,
1179
- initialVolume,
1180
- volumeKeyframes
1181
- });
1182
- }
1183
- destroy() {
1184
- this._audioBuffer = null;
1185
- }
1186
- };
1187
- }
1188
- });
1189
-
1190
- // src/core/FrameCache.ts
1191
- var FrameCache;
1192
- var init_FrameCache = __esm({
1193
- "src/core/FrameCache.ts"() {
1194
- FrameCache = class {
1195
- sink;
1196
- capacity;
1197
- cache = /* @__PURE__ */ new Map();
1198
- _pending = /* @__PURE__ */ new Map();
1199
- constructor(sink, options = {}) {
1200
- this.sink = sink;
1201
- this.capacity = options.capacity ?? 30;
1202
- }
1203
- _key(time) {
1204
- return Math.round(time * 1e3);
1205
- }
1206
- async getFrameAt(time) {
1207
- const key = this._key(time);
1208
- if (this.cache.has(key)) {
1209
- const v = this.cache.get(key);
1210
- this.cache.delete(key);
1211
- this.cache.set(key, v);
1212
- return v;
1213
- }
1214
- const pending = this._pending.get(key);
1215
- if (pending) return pending;
1216
- const promise = this._fetch(time);
1217
- this._pending.set(key, promise);
1218
- try {
1219
- const frame = await promise;
1220
- if (frame) {
1221
- if (this.cache.has(key)) {
1222
- frame.close?.();
1223
- const existing = this.cache.get(key);
1224
- this.cache.delete(key);
1225
- this.cache.set(key, existing);
1226
- return existing;
1227
- }
1228
- this.cache.set(key, frame);
1229
- this._evictIfNeeded();
1230
- }
1231
- return frame;
1232
- } finally {
1233
- this._pending.delete(key);
1234
- }
1235
- }
1236
- async _fetch(time) {
1237
- try {
1238
- const sample = await this.sink.getSample(time);
1239
- if (!sample) return null;
1240
- const frame = sample.toVideoFrame();
1241
- sample.close?.();
1242
- return frame;
1243
- } catch (err) {
1244
- const isTransient = err instanceof DOMException && (err.name === "EncodingError" || err.name === "NotSupportedError");
1245
- if (isTransient) {
1246
- console.debug("pixi-effects: transient video decode failure at", time, "s \u2014", err.name);
1247
- } else {
1248
- console.warn("pixi-effects: video frame decode failed at", time, "s \u2014", err);
1249
- }
1250
- return null;
1251
- }
1252
- }
1253
- _evictIfNeeded() {
1254
- while (this.cache.size > this.capacity) {
1255
- const oldestKey = this.cache.keys().next().value;
1256
- if (oldestKey === void 0) break;
1257
- const oldestFrame = this.cache.get(oldestKey);
1258
- oldestFrame?.close?.();
1259
- this.cache.delete(oldestKey);
1260
- }
1261
- }
1262
- dispose() {
1263
- for (const f of this.cache.values()) f?.close?.();
1264
- this.cache.clear();
1265
- this._pending.clear();
1266
- }
1267
- };
1268
- }
1269
- });
1270
- var VideoSequence;
1271
- var init_Video = __esm({
1272
- "src/sequences/Video.ts"() {
1273
- init_Base();
1274
- init_FrameCache();
1275
- VideoSequence = class extends Sequence {
1276
- _sourceDuration = 0;
1277
- _audioBuffer = null;
1278
- _cache = null;
1279
- _canvas = null;
1280
- _ctx = null;
1281
- _drawSeq = 0;
1282
- async build() {
1283
- const data = await Assets.get(this.spec.asset);
1284
- this._sourceDuration = data.duration;
1285
- this._audioBuffer = this.spec.audio !== false ? data.audioBuffer : null;
1286
- this._cache = new FrameCache(data.sink, { capacity: 30 });
1287
- const probeFrame = await this._cache.getFrameAt(0);
1288
- const w = probeFrame?.displayWidth ?? 1920;
1289
- const h = probeFrame?.displayHeight ?? 1080;
1290
- const canvas = document.createElement("canvas");
1291
- canvas.width = w;
1292
- canvas.height = h;
1293
- this._canvas = canvas;
1294
- this._ctx = canvas.getContext("2d", { alpha: true });
1295
- if (probeFrame && this._ctx) this._ctx.drawImage(probeFrame, 0, 0);
1296
- const texture = Texture.from(canvas);
1297
- const sprite = new Sprite({ texture, label: this.spec.name });
1298
- sprite.cullable = true;
1299
- this.target = sprite;
1300
- this.intrinsicWidth = w;
1301
- this.intrinsicHeight = h;
1302
- if (this.duration === void 0) {
1303
- this.duration = this.spec.duration ?? data.duration ?? this.root.duration;
1304
- }
1305
- let currentTime = 0;
1306
- const seq = this;
1307
- Object.defineProperty(sprite, "currentTime", {
1308
- get() {
1309
- return currentTime;
1310
- },
1311
- set(v) {
1312
- currentTime = v;
1313
- const mySeq = ++seq._drawSeq;
1314
- const lookup = seq.spec.loop && seq._sourceDuration > 0 ? v % seq._sourceDuration : v;
1315
- seq._cache.getFrameAt(lookup).then((frame) => {
1316
- if (mySeq !== seq._drawSeq) return;
1317
- if (!frame || !seq._ctx) return;
1318
- seq._ctx.drawImage(frame, 0, 0);
1319
- texture.source.update();
1320
- }).catch((err) => {
1321
- console.warn("pixi-effects: video frame draw failed:", err);
1322
- });
1323
- }
1324
- });
1325
- this.buildFilters();
1326
- }
1327
- bindTimeline(timeline) {
1328
- super.bindTimeline(timeline);
1329
- const playDuration = this.spec.loop ? this.duration : Math.min(this.duration, this._sourceDuration);
1330
- timeline.fromTo(
1331
- this.target,
1332
- { currentTime: 0 },
1333
- { currentTime: playDuration, duration: playDuration, ease: "none" },
1334
- this.at
1335
- );
1336
- }
1337
- collectAudio(out, baseTime) {
1338
- if (!this._audioBuffer) return;
1339
- const initialVolume = this.spec.volume ?? 1;
1340
- out.push({
1341
- buffer: this._audioBuffer,
1342
- loop: !!this.spec.loop,
1343
- start: baseTime + this.at,
1344
- end: baseTime + this.at + this.duration,
1345
- initialVolume,
1346
- volumeKeyframes: []
1347
- });
1348
- }
1349
- async awaitFrameAt(time) {
1350
- const mySeq = ++this._drawSeq;
1351
- const lookup = this.spec.loop && this._sourceDuration > 0 ? time % this._sourceDuration : time;
1352
- let frame = null;
1353
- try {
1354
- frame = await this._cache.getFrameAt(lookup);
1355
- } catch (err) {
1356
- console.warn("pixi-effects: awaitFrameAt failed at", time, "s \u2014", err);
1357
- return;
1358
- }
1359
- if (mySeq !== this._drawSeq) return;
1360
- if (frame && this._ctx) {
1361
- try {
1362
- this._ctx.drawImage(frame, 0, 0);
1363
- this.target.texture.source.update();
1364
- } catch (err) {
1365
- console.warn("pixi-effects: drawImage failed at", time, "s \u2014", err);
1366
- }
1367
- }
1368
- }
1369
- destroy() {
1370
- this._cache?.dispose();
1371
- this._cache = null;
1372
- super.destroy();
1373
- }
1374
- };
1375
- }
1376
- });
1377
- function seedGeometry(state, spec, scope) {
1378
- const keys = GEOMETRY_KEYS_BY_SHAPE[spec.shape];
1379
- for (const k of keys) {
1380
- const v = spec[k];
1381
- if (v !== void 0) state[k] = num(v, scope);
1382
- }
1383
- }
1384
- function seedStyle(state, initial, scope) {
1385
- for (const k of STYLE_KEYS) {
1386
- const v = initial[k];
1387
- if (v === void 0) continue;
1388
- state[k] = COLOR_KEYS.has(k) ? v : numOrZero(v, scope);
1389
- }
1390
- }
1391
- function stripLive(props, liveKeys) {
1392
- if (!props) return props;
1393
- const out = {};
1394
- let changed = false;
1395
- for (const k of Object.keys(props)) {
1396
- if (liveKeys.has(k)) {
1397
- changed = true;
1398
- continue;
1399
- }
1400
- out[k] = props[k];
1401
- }
1402
- return changed ? out : props;
1403
- }
1404
- function stripLiveKeyframe(kf, liveKeys) {
1405
- return {
1406
- ...kf,
1407
- set: stripLive(kf.set, liveKeys),
1408
- to: stripLive(kf.to, liveKeys),
1409
- from: stripLive(kf.from, liveKeys)
1410
- };
1411
- }
1412
- function pickLive(props, liveKeys) {
1413
- if (!props) return null;
1414
- const out = {};
1415
- let any = false;
1416
- for (const k of Object.keys(props)) {
1417
- if (liveKeys.has(k)) {
1418
- out[k] = props[k];
1419
- any = true;
1420
- }
1421
- }
1422
- return any ? out : null;
1423
- }
1424
- function resolveLiveValue(key, value, scope) {
1425
- if (COLOR_KEYS.has(key)) return value;
1426
- return numOrZero(value, scope);
1427
- }
1428
- function applyState(g, s) {
1429
- if (s.fillColor !== void 0) {
1430
- g.fill({ color: s.fillColor, alpha: s.fillAlpha });
1431
- }
1432
- if (s.strokeColor !== void 0 && s.strokeWidth > 0) {
1433
- g.stroke({ color: s.strokeColor, alpha: s.strokeAlpha, width: s.strokeWidth });
1434
- }
1435
- }
1436
- function drawRectFromState(g, s) {
1437
- const w = s.width ?? 0;
1438
- const h = s.height ?? 0;
1439
- const r = s.cornerRadius ?? 0;
1440
- const ax = s.anchorX ?? 0.5;
1441
- const ay = s.anchorY ?? 0.5;
1442
- const x = 0 - ax * w;
1443
- const y = 0 - ay * h;
1444
- if (r > 0) g.roundRect(x, y, w, h, r);
1445
- else g.rect(x, y, w, h);
1446
- }
1447
- function drawCircleFromState(g, s) {
1448
- const r = s.radius ?? 0;
1449
- const ax = s.anchorX ?? 0.5;
1450
- const ay = s.anchorY ?? 0.5;
1451
- g.circle((0.5 - ax) * 2 * r, (0.5 - ay) * 2 * r, r);
1452
- }
1453
- function drawEllipseFromState(g, s) {
1454
- const rx = s.radiusX ?? 0;
1455
- const ry = s.radiusY ?? 0;
1456
- const ax = s.anchorX ?? 0.5;
1457
- const ay = s.anchorY ?? 0.5;
1458
- g.ellipse((0.5 - ax) * 2 * rx, (0.5 - ay) * 2 * ry, rx, ry);
1459
- }
1460
- function makeLineDraw(spec, scope) {
1461
- const fx = num(spec.from[0], scope);
1462
- const fy = num(spec.from[1], scope);
1463
- const tx = num(spec.to[0], scope);
1464
- const ty = num(spec.to[1], scope);
1465
- return (g) => {
1466
- g.moveTo(fx, fy);
1467
- g.lineTo(tx, ty);
1468
- };
1469
- }
1470
- function makePolygonDraw(spec, scope) {
1471
- if (spec.points.length === 0) return () => {
1472
- };
1473
- const flat = [];
1474
- for (const [x, y] of spec.points) flat.push(num(x, scope), num(y, scope));
1475
- const closed = !spec.open;
1476
- return (g) => {
1477
- g.poly(flat, closed);
1478
- };
1479
- }
1480
- function makePathDraw(spec) {
1481
- return (g) => {
1482
- g.path(new GraphicsPath(spec.d));
1483
- };
1484
- }
1485
- function makeDrawGeometry(spec, scope, state) {
1486
- const stateDrawer = STATE_DRAWERS[spec.shape];
1487
- if (stateDrawer) return (g) => stateDrawer(g, state);
1488
- switch (spec.shape) {
1489
- case "line":
1490
- return makeLineDraw(spec, scope);
1491
- case "polygon":
1492
- return makePolygonDraw(spec, scope);
1493
- case "path":
1494
- return makePathDraw(spec);
1495
- }
1496
- return () => {
1497
- };
1498
- }
1499
- function bindLiveKeyframes(timeline, state, liveKeys, keyframes, parentDuration, scope, offset, colorSpace) {
1500
- for (const kf of keyframes) {
1501
- const at = offset + resolveAt(kf.at, parentDuration);
1502
- const duration = kf.duration ?? 0;
1503
- const ease = kf.ease ?? "none";
1504
- if (kf.set) {
1505
- const live = pickLive(kf.set, liveKeys);
1506
- if (live) {
1507
- for (const [k, v] of Object.entries(live)) {
1508
- const resolved = resolveLiveValue(k, v, scope);
1509
- timeline.call(() => {
1510
- state[k] = resolved;
1511
- }, [], at);
1512
- }
1513
- }
1514
- }
1515
- const fromLive = pickLive(kf.from, liveKeys);
1516
- const toLive = pickLive(kf.to, liveKeys);
1517
- const allKeys = /* @__PURE__ */ new Set([
1518
- ...fromLive ? Object.keys(fromLive) : [],
1519
- ...toLive ? Object.keys(toLive) : []
1520
- ]);
1521
- for (const k of allKeys) {
1522
- const key = k;
1523
- const fromRaw = fromLive?.[key];
1524
- const toRaw = toLive?.[key];
1525
- const fromValue = fromRaw !== void 0 ? resolveLiveValue(key, fromRaw, scope) : void 0;
1526
- const toValue = toRaw !== void 0 ? resolveLiveValue(key, toRaw, scope) : void 0;
1527
- tweenLiveKey(timeline, state, key, fromValue, toValue, duration, ease, at, colorSpace);
1528
- }
1529
- }
1530
- }
1531
- function tweenLiveKey(timeline, state, key, fromValue, toValue, duration, ease, at, colorSpace) {
1532
- if (COLOR_KEYS.has(key) && toValue !== void 0) {
1533
- tweenColor(
1534
- timeline,
1535
- state,
1536
- key,
1537
- fromValue,
1538
- toValue,
1539
- duration,
1540
- ease,
1541
- at,
1542
- colorSpace
1543
- );
1544
- } else if (toValue !== void 0) {
1545
- if (fromValue !== void 0) {
1546
- timeline.fromTo(state, { [key]: fromValue }, { [key]: toValue, duration, ease }, at);
1547
- } else {
1548
- timeline.to(state, { [key]: toValue, duration, ease }, at);
1549
- }
1550
- }
1551
- }
1552
- function num(v, scope) {
1553
- if (typeof v === "number") return v;
1554
- if (isExpr(v)) return evaluateExpr(v, scope);
1555
- const parsed = parseFloat(v);
1556
- return Number.isNaN(parsed) ? 0 : parsed;
1557
- }
1558
- function numOrZero(v, scope) {
1559
- if (typeof v === "number") return v;
1560
- if (typeof v === "string") return num(v, scope);
1561
- return 0;
1562
- }
1563
- var STYLE_KEYS, COLOR_KEYS, GEOMETRY_KEYS_BY_SHAPE, ANCHORED_SHAPES, ShapeSequence, STATE_DRAWERS;
1564
- var init_Shape = __esm({
1565
- "src/sequences/Shape.ts"() {
1566
- init_Base();
1567
- init_Parser();
1568
- init_Timeline();
1569
- init_colorTween();
1570
- STYLE_KEYS = ["fillColor", "fillAlpha", "strokeColor", "strokeAlpha", "strokeWidth"];
1571
- COLOR_KEYS = /* @__PURE__ */ new Set(["fillColor", "strokeColor"]);
1572
- GEOMETRY_KEYS_BY_SHAPE = {
1573
- rect: ["width", "height", "cornerRadius", "anchorX", "anchorY"],
1574
- circle: ["radius", "anchorX", "anchorY"],
1575
- ellipse: ["radiusX", "radiusY", "anchorX", "anchorY"],
1576
- line: [],
1577
- polygon: [],
1578
- path: []
1579
- };
1580
- ANCHORED_SHAPES = /* @__PURE__ */ new Set(["rect", "circle", "ellipse"]);
1581
- ShapeSequence = class extends Sequence {
1582
- _state = {
1583
- fillColor: void 0,
1584
- fillAlpha: 1,
1585
- strokeColor: void 0,
1586
- strokeAlpha: 1,
1587
- strokeWidth: 0
1588
- };
1589
- // Closure that draws the path commands for this shape kind from the
1590
- // (possibly tweened) `_state`. Bound once at build; called on every frame
1591
- // by `Container.onRender`.
1592
- _drawGeometry = () => {
1593
- };
1594
- _liveKeys = new Set(STYLE_KEYS);
1595
- async build() {
1596
- const graphics = new Graphics({ label: this.spec.name });
1597
- graphics.cullable = true;
1598
- this.target = graphics;
1599
- if (this.duration === void 0) {
1600
- this.duration = this.parent?.duration ?? this.root.duration;
1601
- }
1602
- const scope = this.scope();
1603
- const geomKeys = GEOMETRY_KEYS_BY_SHAPE[this.spec.shape];
1604
- for (const k of geomKeys) this._liveKeys.add(k);
1605
- seedGeometry(this._state, this.spec, scope);
1606
- seedStyle(this._state, this.spec.initial ?? {}, scope);
1607
- this._drawGeometry = makeDrawGeometry(this.spec, scope, this._state);
1608
- this._drawGeometry(graphics);
1609
- applyState(graphics, this._state);
1610
- const bounds = graphics.getLocalBounds();
1611
- this.intrinsicWidth = bounds.width;
1612
- this.intrinsicHeight = bounds.height;
1613
- if (!ANCHORED_SHAPES.has(this.spec.shape)) {
1614
- graphics.pivot.set(bounds.x + bounds.width / 2, bounds.y + bounds.height / 2);
1615
- }
1616
- graphics.onRender = () => {
1617
- graphics.clear();
1618
- this._drawGeometry(graphics);
1619
- applyState(graphics, this._state);
1620
- };
1621
- this.buildFilters();
1622
- }
1623
- bindTimeline(timeline, offset = 0) {
1624
- if (!this.target) return;
1625
- const scope = this.scope();
1626
- const initial = stripLive(this.spec.initial, this._liveKeys);
1627
- const keyframes = this.spec.keyframes ? this.spec.keyframes.map((kf) => stripLiveKeyframe(kf, this._liveKeys)) : void 0;
1628
- applyInitial(this.target, initial, scope);
1629
- applyKeyframes(timeline, this.target, keyframes, this.duration, scope, [], offset);
1630
- const colorSpace = this.spec.colorSpace ?? "rgb";
1631
- bindLiveKeyframes(timeline, this._state, this._liveKeys, this.spec.keyframes ?? [], this.duration, scope, offset, colorSpace);
1632
- const startTime = offset + this.at;
1633
- const endTime = startTime + this.duration;
1634
- this.target.renderable = startTime <= 0;
1635
- timeline.set(this.target, { renderable: true }, startTime);
1636
- timeline.set(this.target, { renderable: false }, endTime);
1637
- }
1638
- };
1639
- STATE_DRAWERS = {
1640
- rect: drawRectFromState,
1641
- circle: drawCircleFromState,
1642
- ellipse: drawEllipseFromState
1643
- };
1644
- }
1645
- });
1646
-
1647
- // src/core/Composition.ts
1648
- async function getCompositionSequence() {
1649
- if (!_CompositionSequence) {
1650
- _CompositionSequence = (await Promise.resolve().then(() => (init_Composition2(), Composition_exports))).CompositionSequence;
1651
- }
1652
- return _CompositionSequence;
1653
- }
1654
- async function buildSequenceTree(specs, parent, root) {
1655
- const out = [];
1656
- for (const spec of specs) {
1657
- let Cls = staticTypes[spec.type];
1658
- if (!Cls && spec.type === "composition") {
1659
- Cls = await getCompositionSequence();
1660
- }
1661
- if (!Cls) {
1662
- console.warn(`pixi-effects: unknown sequence type "${spec.type}"`);
1663
- continue;
1664
- }
1665
- const seq = new Cls(spec, parent, root);
1666
- await seq.build();
1667
- if (seq.duration === void 0) {
1668
- seq.duration = parent?.duration ?? root.duration;
1669
- }
1670
- out.push(seq);
1671
- }
1672
- return out;
1673
- }
1674
- var _CompositionSequence, staticTypes;
1675
- var init_Composition = __esm({
1676
- "src/core/Composition.ts"() {
1677
- init_Image();
1678
- init_Text();
1679
- init_Audio();
1680
- init_Video();
1681
- init_Shape();
1682
- _CompositionSequence = null;
1683
- staticTypes = {
1684
- image: ImageSequence,
1685
- text: TextSequence,
1686
- audio: AudioSequence,
1687
- video: VideoSequence,
1688
- shape: ShapeSequence
1689
- };
1690
- }
1691
- });
1692
-
1693
- // src/sequences/Composition.ts
1694
- var Composition_exports = {};
1695
- __export(Composition_exports, {
1696
- CompositionSequence: () => CompositionSequence
1697
- });
1698
- var CompositionSequence;
1699
- var init_Composition2 = __esm({
1700
- "src/sequences/Composition.ts"() {
1701
- init_Base();
1702
- init_Composition();
1703
- CompositionSequence = class extends Sequence {
1704
- _innerContainer = null;
1705
- _compositionShape = null;
1706
- _children = [];
1707
- async build() {
1708
- const width = this.spec.width ?? this.parent?.width ?? this.root.width;
1709
- const height = this.spec.height ?? this.parent?.height ?? this.root.height;
1710
- if (this.duration === void 0) {
1711
- this.duration = this.spec.duration ?? this.parent?.duration ?? this.root.duration;
1712
- }
1713
- const wrapper = new Container();
1714
- wrapper.cullable = true;
1715
- wrapper.cullableChildren = true;
1716
- wrapper.cullArea = new Rectangle(0, 0, width, height);
1717
- const inner = new Container();
1718
- inner.cullable = true;
1719
- inner.cullableChildren = true;
1720
- wrapper.addChild(inner);
1721
- this.target = wrapper;
1722
- this.intrinsicWidth = width;
1723
- this.intrinsicHeight = height;
1724
- this._innerContainer = inner;
1725
- this._compositionShape = { width, height, duration: this.duration };
1726
- this._children = await buildSequenceTree(
1727
- this.spec.sequences ?? [],
1728
- this._compositionShape,
1729
- this.root
1730
- );
1731
- for (const child of this._children) {
1732
- if (child.target) inner.addChild(child.target);
1733
- const maskSpec = child.spec.mask;
1734
- if (maskSpec && child.target) {
1735
- const built = await buildSequenceTree([maskSpec], this._compositionShape, this.root);
1736
- const maskSeq = built[0];
1737
- if (maskSeq?.target) {
1738
- inner.addChild(maskSeq.target);
1739
- const inverse = child.spec.maskInverted ?? false;
1740
- const t = child.target;
1741
- if (typeof t.setMask === "function") {
1742
- t.setMask({ mask: maskSeq.target, inverse });
1743
- } else {
1744
- t.mask = maskSeq.target;
1745
- }
1746
- child.maskSequence = maskSeq;
1747
- }
1748
- }
1749
- }
1750
- this.buildFilters();
1751
- }
1752
- bindTimeline(timeline, offset = 0) {
1753
- super.bindTimeline(timeline, offset);
1754
- const childOffset = offset + this.at;
1755
- for (const child of this._children) {
1756
- child.bindTimeline(timeline, childOffset);
1757
- child.maskSequence?.bindTimeline(timeline, childOffset);
1758
- }
1759
- }
1760
- collectAudio(out, baseTime) {
1761
- super.collectAudio(out, baseTime);
1762
- const childBase = baseTime + this.at;
1763
- for (const child of this._children) child.collectAudio(out, childBase);
1764
- }
1765
- destroy() {
1766
- for (const c of this._children) c.destroy();
1767
- super.destroy();
1768
- }
1769
- };
1770
- }
1771
- });
1772
10
  var registered = false;
1773
11
  function ensureLoadersRegistered(audioContext) {
1774
12
  if (registered) return;
@@ -1826,9 +64,6 @@ async function loadAssetBundle(assets, audioContext) {
1826
64
  return await Assets.loadBundle("pixi-effects");
1827
65
  }
1828
66
 
1829
- // src/core/Movie.ts
1830
- init_Composition2();
1831
-
1832
67
  // src/core/AudioMixer.ts
1833
68
  async function mixdown(audios, totalDuration, sampleRate = 44100) {
1834
69
  if (audios.length === 0) return null;
@@ -1931,9 +166,6 @@ async function exportFrames(movie, options = {}) {
1931
166
  movie.app.ticker.start();
1932
167
  }
1933
168
  }
1934
-
1935
- // src/core/Transitions.ts
1936
- init_Timeline();
1937
169
  var MODE_CODES = {
1938
170
  "wipe-left": 0,
1939
171
  "wipe-right": 1,
@@ -1943,7 +175,7 @@ var MODE_CODES = {
1943
175
  "iris-out": 5,
1944
176
  "dissolve": 6
1945
177
  };
1946
- var GL_FRAGMENT2 = `
178
+ var GL_FRAGMENT = `
1947
179
  in vec2 vTextureCoord;
1948
180
  uniform sampler2D uTexture;
1949
181
  uniform vec4 uInputSize; // PIXI-bound: xy = sprite render texture size (with padding)
@@ -2063,7 +295,7 @@ void main(void) {
2063
295
  finalColor = raw * reveal;
2064
296
  }
2065
297
  `;
2066
- var WGSL_SOURCE2 = `
298
+ var WGSL_SOURCE = `
2067
299
  struct GlobalFilterUniforms {
2068
300
  uInputSize: vec4<f32>,
2069
301
  uInputPixel: vec4<f32>,
@@ -2211,12 +443,12 @@ var TransitionMaskFilter = class extends Filter {
2211
443
  super({
2212
444
  glProgram: GlProgram.from({
2213
445
  vertex: defaultFilterVert,
2214
- fragment: GL_FRAGMENT2,
446
+ fragment: GL_FRAGMENT,
2215
447
  name: "transition-mask-filter"
2216
448
  }),
2217
449
  gpuProgram: GpuProgram.from({
2218
- vertex: { source: WGSL_SOURCE2, entryPoint: "mainVertex" },
2219
- fragment: { source: WGSL_SOURCE2, entryPoint: "mainFragment" }
450
+ vertex: { source: WGSL_SOURCE, entryPoint: "mainVertex" },
451
+ fragment: { source: WGSL_SOURCE, entryPoint: "mainFragment" }
2220
452
  }),
2221
453
  resources: {
2222
454
  transitionUniforms: new UniformGroup({
@@ -2300,6 +532,14 @@ function expandTransitions(spec) {
2300
532
  const parentDuration = out.duration ?? Infinity;
2301
533
  const compW = out.width;
2302
534
  const compH = out.height;
535
+ const threeDByName = new Set(sequences.filter((s) => s.name && s.threeD).map((s) => s.name));
536
+ transitions.forEach((t, i) => {
537
+ for (const name of [t.from, t.to]) {
538
+ if (threeDByName.has(name)) {
539
+ console.warn(`pixi-effects: transitions[${i}]: "${name}" is threeD, but transitions are not supported on threeD layers yet \u2014 it will render without the camera`);
540
+ }
541
+ }
542
+ });
2303
543
  const masksParticipants = /* @__PURE__ */ new Set();
2304
544
  for (const t of transitions) {
2305
545
  if (t.kind === "wipe" || t.kind === "iris" || t.kind === "dissolve" || t.kind === "zoom") {
@@ -2371,29 +611,34 @@ function expandTransitions(spec) {
2371
611
  if (tStart < toStart || tEnd > toEnd) {
2372
612
  throw new Error(`pixi-effects: ${tag} window [${tStart}, ${tEnd}] is not covered by \`to\` "${t.to}" (lives [${toStart}, ${toEnd}], so it starts at ${toStart})`);
2373
613
  }
2374
- switch (t.kind) {
614
+ const abs = { ...t, at: tStart };
615
+ const nFrom = keyframeCount(fromEntry.seq);
616
+ const nTo = keyframeCount(toEntry.seq);
617
+ switch (abs.kind) {
2375
618
  case "crossfade":
2376
- expandCrossfade(out, t, fromEntry.seq, toEntry.seq);
619
+ expandCrossfade(out, abs, fromEntry.seq, toEntry.seq);
2377
620
  break;
2378
621
  case "wipe":
2379
- expandMask(out, t, fromEntry.seq, toEntry.seq, i, wipeMode(t.direction), t.smoothing);
622
+ expandMask(out, abs, fromEntry.seq, toEntry.seq, i, wipeMode(abs.direction), abs.smoothing);
2380
623
  break;
2381
624
  case "iris":
2382
- expandMask(out, t, fromEntry.seq, toEntry.seq, i, t.mode === "out" ? "iris-out" : "iris-in", t.smoothing);
625
+ expandMask(out, abs, fromEntry.seq, toEntry.seq, i, abs.mode === "out" ? "iris-out" : "iris-in", abs.smoothing);
2383
626
  break;
2384
627
  case "slide":
2385
- expandSlide(out, t, fromEntry.seq, toEntry.seq);
628
+ expandSlide(out, abs, fromEntry.seq, toEntry.seq);
2386
629
  break;
2387
630
  case "dip":
2388
- expandDip(out, t, fromEntry.seq, toEntry.seq);
631
+ expandDip(out, abs, fromEntry.seq, toEntry.seq);
2389
632
  break;
2390
633
  case "zoom":
2391
- expandZoom(out, t, fromEntry.seq, toEntry.seq);
634
+ expandZoom(out, abs, fromEntry.seq, toEntry.seq);
2392
635
  break;
2393
636
  case "dissolve":
2394
- expandDissolve(out, t, fromEntry.seq, toEntry.seq, i);
637
+ expandDissolve(out, abs, fromEntry.seq, toEntry.seq, i);
2395
638
  break;
2396
639
  }
640
+ rebaseNewKeyframes(fromEntry.seq, nFrom, fromStart);
641
+ rebaseNewKeyframes(toEntry.seq, nTo, toStart);
2397
642
  }
2398
643
  delete out.transitions;
2399
644
  return out;
@@ -2425,6 +670,17 @@ function wrapAsFullComposition(seq, compW, compH) {
2425
670
  sequences: [inner]
2426
671
  };
2427
672
  }
673
+ function keyframeCount(seq) {
674
+ return seq.keyframes?.length ?? 0;
675
+ }
676
+ function rebaseNewKeyframes(seq, from, seqStart) {
677
+ const kfs = seq.keyframes;
678
+ if (!kfs) return;
679
+ for (let i = from; i < kfs.length; i++) {
680
+ const kf = kfs[i];
681
+ if (typeof kf.at === "number") kfs[i] = { ...kf, at: kf.at - seqStart };
682
+ }
683
+ }
2428
684
  function ensureKeyframes(seq) {
2429
685
  const s = seq;
2430
686
  if (!s.keyframes) s.keyframes = [];
@@ -2641,6 +897,153 @@ function rejectReservedNameCollision(filters, seqName) {
2641
897
  }
2642
898
  }
2643
899
 
900
+ // src/core/inspect.ts
901
+ var FAINT = 0.3;
902
+ var intersection = (a, b) => {
903
+ const w = Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x);
904
+ const h = Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y);
905
+ return w > 0 && h > 0 ? w * h : 0;
906
+ };
907
+ function inspectScene(root, frame, time, canvas, opts = {}) {
908
+ const layers = [];
909
+ const view = { x: 0, y: 0, width: canvas.width, height: canvas.height };
910
+ const walk = (comp, prefix, insideThreeD, parentVisible) => {
911
+ comp.layers().forEach(({ seq, display, threeD }, i) => {
912
+ const t = seq.target;
913
+ const name = seq.spec.name;
914
+ const label = name ?? `${seq.spec.type}#${i}`;
915
+ const alpha = t?.alpha ?? 1;
916
+ const alive = threeD ? display.visible : t?.renderable ?? true;
917
+ const visible = parentVisible && alive && alpha > 0;
918
+ let bounds = null;
919
+ if (!insideThreeD) {
920
+ const b = display.getBounds();
921
+ bounds = { x: b.x, y: b.y, width: b.width, height: b.height };
922
+ }
923
+ let onCanvas = null;
924
+ if (bounds) {
925
+ const inter = intersection(bounds, view);
926
+ const area = bounds.width * bounds.height;
927
+ onCanvas = inter === 0 && area > 0 ? "none" : inter >= area - 1e-6 ? "full" : "partial";
928
+ if (area === 0) onCanvas = "none";
929
+ }
930
+ const keys = collectPropKeys(seq.spec);
931
+ const moving = (seq.spec.keyframes ?? []).length > 0 && (keys.has("x") || keys.has("y"));
932
+ layers.push({ path: prefix + label, name, type: seq.spec.type, threeD, visible, alpha, moving, bounds, onCanvas });
933
+ if (seq instanceof CompositionSequence) walk(seq, prefix + label + "/", insideThreeD || threeD, visible);
934
+ });
935
+ };
936
+ walk(root, "", false, true);
937
+ const issues = [];
938
+ const texts = layers.filter((l) => l.type === "text" && l.visible && l.alpha >= FAINT && l.bounds);
939
+ for (const l of texts) {
940
+ const b = l.bounds;
941
+ const who = `text layer "${l.path}"`;
942
+ if (b.width < 1 || b.height < 1) {
943
+ issues.push(`${who} has no size (empty text, or not drawn yet)`);
944
+ } else if (l.moving) ; else if (l.onCanvas === "none") {
945
+ issues.push(`${who} is entirely outside the canvas (at ${Math.round(b.x)},${Math.round(b.y)}, ${Math.round(b.width)}\xD7${Math.round(b.height)})`);
946
+ } else if (l.onCanvas === "partial") {
947
+ const over = [];
948
+ if (b.x < 0) over.push(`${Math.round(-b.x)}px beyond the left edge`);
949
+ if (b.x + b.width > canvas.width) over.push(`${Math.round(b.x + b.width - canvas.width)}px beyond the right edge`);
950
+ if (b.y < 0) over.push(`${Math.round(-b.y)}px beyond the top edge`);
951
+ if (b.y + b.height > canvas.height) over.push(`${Math.round(b.y + b.height - canvas.height)}px beyond the bottom edge`);
952
+ issues.push(`${who} is cut off by the canvas edge: ${over.join(", ")}`);
953
+ }
954
+ }
955
+ for (let i = 0; i < texts.length; i++) {
956
+ for (let j = i + 1; j < texts.length; j++) {
957
+ const a = texts[i].bounds, b = texts[j].bounds;
958
+ const smaller = Math.min(a.width * a.height, b.width * b.height);
959
+ if (smaller <= 0) continue;
960
+ const share = intersection(a, b) / smaller;
961
+ if (share > 0.25) issues.push(`text layers "${texts[i].path}" and "${texts[j].path}" overlap by ${Math.round(share * 100)}% of the smaller one`);
962
+ }
963
+ }
964
+ const mode = opts.layers ?? "all";
965
+ const listed = mode === "none" ? [] : mode === "visible" ? layers.filter((l) => l.visible) : layers;
966
+ return { frame, time, canvas, summary: { layers: layers.length, visible: layers.filter((l) => l.visible).length }, issues, layers: listed };
967
+ }
968
+
969
+ // src/core/frames.ts
970
+ function pickFrames(lastFrame, count) {
971
+ const last = Math.max(0, Math.floor(lastFrame));
972
+ const n = Math.max(1, Math.floor(count));
973
+ if (n === 1) return [0];
974
+ const out = [];
975
+ for (let i = 0; i < n; i++) {
976
+ const f = Math.round(last * i / (n - 1));
977
+ if (out[out.length - 1] !== f) out.push(f);
978
+ }
979
+ return out;
980
+ }
981
+ function sheetLayout(n, columns, cellW, srcW, srcH, labelH) {
982
+ const cols = Math.max(1, Math.min(Math.floor(columns), n));
983
+ const rows = Math.ceil(n / cols);
984
+ const cellH = Math.round(cellW * srcH / srcW);
985
+ const positions = Array.from({ length: n }, (_, i) => ({ x: i % cols * cellW, y: Math.floor(i / cols) * (cellH + labelH) }));
986
+ return { cols, rows, cellW, cellH, width: cols * cellW, height: rows * (cellH + labelH), positions };
987
+ }
988
+
989
+ // src/core/options.ts
990
+ function distance(a, b) {
991
+ const dp = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
992
+ for (let j = 1; j <= b.length; j++) dp[0][j] = j;
993
+ for (let i = 1; i <= a.length; i++) {
994
+ for (let j = 1; j <= b.length; j++) {
995
+ dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
996
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) dp[i][j] = Math.min(dp[i][j], dp[i - 2][j - 2] + 1);
997
+ }
998
+ }
999
+ return dp[a.length][b.length];
1000
+ }
1001
+ var isSubsequence = (small, big) => {
1002
+ let i = 0;
1003
+ for (const ch of big) if (i < small.length && ch === small[i]) i++;
1004
+ return i === small.length;
1005
+ };
1006
+ var COMMON_ALIASES = {
1007
+ fps: "frameRate",
1008
+ framerate: "frameRate",
1009
+ frame_rate: "frameRate",
1010
+ bg: "background",
1011
+ backgroundcolor: "background",
1012
+ bgcolor: "background",
1013
+ seconds: "duration",
1014
+ length: "duration",
1015
+ time: "duration",
1016
+ w: "width",
1017
+ h: "height",
1018
+ cols: "columns",
1019
+ sweep: "degrees",
1020
+ angle: "degrees"
1021
+ };
1022
+ function suggestName(name, valid) {
1023
+ const n = name.toLowerCase();
1024
+ const alias = COMMON_ALIASES[n];
1025
+ if (alias && valid.includes(alias)) return alias;
1026
+ let best = null;
1027
+ for (const v of valid) {
1028
+ const lv = v.toLowerCase();
1029
+ const d = distance(n, lv);
1030
+ const close = d <= Math.max(1, Math.floor(Math.min(n.length, lv.length) / 3));
1031
+ const abbreviation = n.length >= 3 && isSubsequence(n, lv) || lv.length >= 2 && n.length > lv.length && n.includes(lv);
1032
+ if (!close && !abbreviation) continue;
1033
+ const score = close ? d : 3 + (lv.length - n.length) / 100;
1034
+ if (!best || score < best.score) best = { v, score };
1035
+ }
1036
+ return best ? best.v : null;
1037
+ }
1038
+ function warnUnknownOptions(where, opts, valid) {
1039
+ if (!opts || typeof opts !== "object") return;
1040
+ for (const key of Object.keys(opts)) {
1041
+ if (valid.includes(key)) continue;
1042
+ const guess = suggestName(key, valid);
1043
+ console.warn(`pixi-effects: ${where}: unknown option "${key}"${guess ? ` \u2014 did you mean "${guess}"?` : ""} (valid: ${valid.join(", ")})`);
1044
+ }
1045
+ }
1046
+
2644
1047
  // src/core/Movie.ts
2645
1048
  extensions.add(CullerPlugin);
2646
1049
  gsap.registerPlugin(PixiPlugin);
@@ -2699,6 +1102,7 @@ var Movie = class {
2699
1102
  return this._audioContext;
2700
1103
  }
2701
1104
  async init(options = {}) {
1105
+ warnUnknownOptions("movie.init()", options, ["width", "height", "duration", "frameRate", "background", "canvas", "assets", "composition"]);
2702
1106
  this._initState = "pending";
2703
1107
  try {
2704
1108
  this.width = options.width ?? 1920;
@@ -2761,7 +1165,9 @@ var Movie = class {
2761
1165
  this.audioBuffer = await mixdown(audios, this.duration, audioContext.sampleRate);
2762
1166
  }
2763
1167
  this.timeline.progress(1).progress(0);
2764
- this.app.renderer.render({ container: this.app.stage });
1168
+ await this._awaitVideoFrames();
1169
+ this._updateSpace();
1170
+ this._renderNow();
2765
1171
  this._initState = "ready";
2766
1172
  this.emit("ready");
2767
1173
  } catch (err) {
@@ -2779,7 +1185,8 @@ var Movie = class {
2779
1185
  this.currentFrame = Math.max(0, Math.min(frame, this.totalFrames));
2780
1186
  this.timeline.time(this.currentFrame / this.frameRate);
2781
1187
  await this._awaitVideoFrames();
2782
- this.app?.renderer?.render({ container: this.app.stage });
1188
+ this._updateSpace();
1189
+ this._renderNow();
2783
1190
  this.emit("frame", { frame: this.currentFrame, totalFrames: this.totalFrames });
2784
1191
  }
2785
1192
  async _awaitVideoFrames() {
@@ -2788,11 +1195,85 @@ var Movie = class {
2788
1195
  collectVideoSequences(this._rootSequence, collected);
2789
1196
  const t = this.currentFrame / this.frameRate;
2790
1197
  await Promise.all(collected.map((v) => {
2791
- const local = t - v.at;
1198
+ const local = t - (v.absoluteStart ?? v.at);
2792
1199
  if (local < 0 || local > (v.duration ?? 0)) return Promise.resolve();
2793
1200
  return v.awaitFrameAt(local);
2794
1201
  }));
2795
1202
  }
1203
+ async snapshot(frame = this.currentFrame, opts = {}) {
1204
+ warnUnknownOptions("movie.snapshot()", opts, ["scale", "type", "as"]);
1205
+ this._requireReady("snapshot");
1206
+ await this.gotoFrame(clampFrame(frame, this.totalFrames), true);
1207
+ const scale = opts.scale ?? 1;
1208
+ const out = document.createElement("canvas");
1209
+ out.width = Math.max(1, Math.round(this.width * scale));
1210
+ out.height = Math.max(1, Math.round(this.height * scale));
1211
+ out.getContext("2d").drawImage(this.app.canvas, 0, 0, out.width, out.height);
1212
+ return encodeCanvas(out, opts.type ?? "image/png", opts.as ?? "blob");
1213
+ }
1214
+ async contactSheet(opts = {}) {
1215
+ warnUnknownOptions("movie.contactSheet()", opts, ["frames", "times", "count", "columns", "cellWidth", "as"]);
1216
+ this._requireReady("contactSheet");
1217
+ const frames = (opts.frames ?? opts.times?.map((t) => Math.round(t * this.frameRate)) ?? pickFrames(this.totalFrames, opts.count ?? 6)).map((f) => clampFrame(f, this.totalFrames));
1218
+ const LABEL_H = 26;
1219
+ const L = sheetLayout(frames.length, opts.columns ?? 3, opts.cellWidth ?? 480, this.width, this.height, LABEL_H);
1220
+ const sheet = document.createElement("canvas");
1221
+ sheet.width = L.width;
1222
+ sheet.height = L.height;
1223
+ const g = sheet.getContext("2d");
1224
+ g.fillStyle = "#10131c";
1225
+ g.fillRect(0, 0, L.width, L.height);
1226
+ const back = this.currentFrame;
1227
+ try {
1228
+ for (let i = 0; i < frames.length; i++) {
1229
+ const f = frames[i];
1230
+ await this.gotoFrame(f, true);
1231
+ const { x, y } = L.positions[i];
1232
+ g.drawImage(this.app.canvas, x, y + LABEL_H, L.cellW, L.cellH);
1233
+ g.fillStyle = "#e8ecf8";
1234
+ g.font = "600 15px system-ui, sans-serif";
1235
+ g.textBaseline = "middle";
1236
+ g.fillText(`frame ${f} \xB7 ${(f / this.frameRate).toFixed(2)}s`, x + 8, y + LABEL_H / 2);
1237
+ }
1238
+ } finally {
1239
+ await this.gotoFrame(back, true);
1240
+ }
1241
+ return encodeCanvas(sheet, "image/png", opts.as ?? "blob");
1242
+ }
1243
+ /**
1244
+ * Where every layer is drawn at `frame` (canvas pixels, visibility) plus `issues` — text that is off
1245
+ * the canvas, cut by an edge, empty, or overlapping other text. Seeks there and stays there.
1246
+ */
1247
+ async inspect(frame = this.currentFrame, opts = { layers: "visible" }) {
1248
+ warnUnknownOptions("movie.inspect()", opts, ["layers"]);
1249
+ this._requireReady("inspect");
1250
+ const f = clampFrame(frame, this.totalFrames);
1251
+ await this.gotoFrame(f, true);
1252
+ return inspectScene(this._rootSequence, f, f / this.frameRate, { width: this.width, height: this.height }, opts);
1253
+ }
1254
+ /**
1255
+ * Draw the stage now. PixiJS's CullerPlugin only culls inside `app.render()` (the ticker), and we render
1256
+ * by hand — while seeking and especially during `render()`, which stops the ticker — so without this the
1257
+ * `culled` flags are whatever the last tick left: a layer that is now on screen can stay culled and be
1258
+ * missing from the frame or from the exported video. Cull again (updating transforms: the timeline just
1259
+ * moved things) right before drawing.
1260
+ */
1261
+ _renderNow() {
1262
+ const app = this.app;
1263
+ if (!app?.renderer) return;
1264
+ Culler.shared.cull(app.stage, app.renderer.screen, false);
1265
+ app.renderer.render({ container: app.stage });
1266
+ }
1267
+ _requireReady(what) {
1268
+ if (this._initState !== "ready" || !this._rootSequence || !this.app) {
1269
+ throw new Error(`pixi-effects: movie.${what}() needs a ready movie \u2014 await movie.init(...) first`);
1270
+ }
1271
+ }
1272
+ /** Projects every `threeD` layer for the current frame (see src/space). */
1273
+ _updateSpace() {
1274
+ if (!this._rootSequence || !this.app) return;
1275
+ this._rootSequence.updateSpace(this.currentFrame / this.frameRate, this.app.renderer);
1276
+ }
2796
1277
  play() {
2797
1278
  if (this._initState !== "ready") return;
2798
1279
  if (this.currentFrame >= this.totalFrames) this.currentFrame = 0;
@@ -2873,6 +1354,7 @@ var Movie = class {
2873
1354
  return this.muted;
2874
1355
  }
2875
1356
  async render(options) {
1357
+ warnUnknownOptions("movie.render()", options, ["format", "video", "audio"]);
2876
1358
  return await exportFrames(this, options);
2877
1359
  }
2878
1360
  async destroy() {
@@ -2890,6 +1372,11 @@ var Movie = class {
2890
1372
  this._initState = "destroyed";
2891
1373
  }
2892
1374
  };
1375
+ var clampFrame = (f, last) => Math.max(0, Math.min(Math.round(f), last));
1376
+ function encodeCanvas(canvas, type, as) {
1377
+ if (as === "dataURL") return Promise.resolve(canvas.toDataURL(type));
1378
+ return new Promise((resolve, reject) => canvas.toBlob((b) => b ? resolve(b) : reject(new Error("pixi-effects: could not encode the image")), type));
1379
+ }
2893
1380
  function safeRun(fn) {
2894
1381
  try {
2895
1382
  fn();
@@ -2901,13 +1388,16 @@ function collectVideoSequences(seq, out) {
2901
1388
  if (typeof seq.awaitFrameAt === "function") {
2902
1389
  out.push(seq);
2903
1390
  }
1391
+ if (seq.maskSequence) collectVideoSequences(seq.maskSequence, out);
2904
1392
  for (const child of seq._children ?? []) {
2905
1393
  collectVideoSequences(child, out);
2906
1394
  }
2907
1395
  }
2908
1396
 
2909
1397
  // src/presets/kenBurns.ts
1398
+ var KEN_BURNS_KEYS = ["asset", "name", "at", "duration", "fit", "ease", "motion", "origin", "zoom", "direction", "angle", "from", "to"];
2910
1399
  function kenBurns(opts) {
1400
+ warnUnknownOptions("kenBurns()", opts, KEN_BURNS_KEYS);
2911
1401
  const {
2912
1402
  asset,
2913
1403
  name,
@@ -2917,7 +1407,6 @@ function kenBurns(opts) {
2917
1407
  ease = "sine.inOut"
2918
1408
  } = opts;
2919
1409
  const baseScale = fit;
2920
- const baseAt = at ?? 0;
2921
1410
  let initial;
2922
1411
  let keyframes;
2923
1412
  if (opts.motion === "still") {
@@ -2938,8 +1427,8 @@ function kenBurns(opts) {
2938
1427
  const pivotXExpr = `w * ${origin[0]}`;
2939
1428
  const pivotYExpr = `h * ${origin[1]}`;
2940
1429
  const initialScale = `${baseScale} * ${fromZ}`;
2941
- const xExpr = `W/2 - (${baseScale}) * ${fromZ} * w * ${0.5 - origin[0]}`;
2942
- const yExpr = `H/2 - (${baseScale}) * ${fromZ} * h * ${0.5 - origin[1]}`;
1430
+ const xExpr = `W * ${origin[0]}`;
1431
+ const yExpr = `H * ${origin[1]}`;
2943
1432
  initial = {
2944
1433
  x: xExpr,
2945
1434
  y: yExpr,
@@ -2948,7 +1437,7 @@ function kenBurns(opts) {
2948
1437
  scale: initialScale
2949
1438
  };
2950
1439
  keyframes = [
2951
- { at: baseAt, to: { scale: `${baseScale} * ${toZ}` }, duration, ease }
1440
+ { at: 0, to: { scale: `${baseScale} * ${toZ}` }, duration, ease }
2952
1441
  ];
2953
1442
  } else if (opts.motion === "rotation") {
2954
1443
  const angle = clamp(opts.angle ?? 8, 0, 30);
@@ -2979,7 +1468,7 @@ function kenBurns(opts) {
2979
1468
  const tHere = duration * tFrac;
2980
1469
  const tPrev = duration * ((i - 1) / (sampleCount - 1));
2981
1470
  keyframes.push({
2982
- at: baseAt + tPrev,
1471
+ at: tPrev,
2983
1472
  to: { rotation: angleHere, scale: scaleAt(angleHere) },
2984
1473
  duration: tHere - tPrev,
2985
1474
  ease: i === 1 ? ease : "none"
@@ -3000,7 +1489,7 @@ function kenBurns(opts) {
3000
1489
  scale: scaleExpr
3001
1490
  };
3002
1491
  keyframes = [
3003
- { at: baseAt, to: { x: xExpr(to[0]), y: yExpr(to[1]) }, duration, ease }
1492
+ { at: 0, to: { x: xExpr(to[0]), y: yExpr(to[1]) }, duration, ease }
3004
1493
  ];
3005
1494
  }
3006
1495
  const spec = {
@@ -3017,7 +1506,86 @@ function kenBurns(opts) {
3017
1506
  function clamp(v, lo, hi) {
3018
1507
  return Math.max(lo, Math.min(hi, v));
3019
1508
  }
1509
+ function parseEase(name) {
1510
+ return gsap.parseEase(name);
1511
+ }
1512
+
1513
+ // src/presets/orbit.ts
1514
+ var ORBIT_KEYS = ["duration", "degrees", "start", "radius", "center", "width", "height", "fov", "ease", "dollyZoom", "at", "name", "stepsPerSecond"];
1515
+ function orbit(opts) {
1516
+ warnUnknownOptions("orbit()", opts, ORBIT_KEYS);
1517
+ const {
1518
+ duration,
1519
+ degrees,
1520
+ width = 1280,
1521
+ height = 720,
1522
+ fov,
1523
+ ease = "sine.inOut",
1524
+ stepsPerSecond = 10
1525
+ } = opts;
1526
+ if (!(duration > 0)) throw new Error(`pixi-effects: orbit duration must be > 0 (got ${duration})`);
1527
+ const dolly = opts.dollyZoom;
1528
+ if (dolly && opts.radius !== void 0) {
1529
+ throw new Error("pixi-effects: orbit() cannot combine `radius` with `dollyZoom` \u2014 the radius follows the animated fov");
1530
+ }
1531
+ const [cx, cy] = opts.center ?? [width / 2, height / 2];
1532
+ const fixedR = opts.radius ?? homeDistance(height, fov ?? DEFAULT_FOV);
1533
+ const start = opts.start ?? -degrees / 2;
1534
+ const steps = Math.max(1, Math.round(duration * stepsPerSecond));
1535
+ const easeFn = parseEase(ease);
1536
+ const dollyEase = dolly ? parseEase(dolly.ease ?? ease) : null;
1537
+ const pos = (i) => {
1538
+ const p = i / steps;
1539
+ const a = (start + degrees * easeFn(p)) * DEG;
1540
+ if (!dolly) return { x: cx + fixedR * Math.sin(a), z: fixedR * Math.cos(a) };
1541
+ const f = dolly.from + (dolly.to - dolly.from) * dollyEase(p);
1542
+ const R = homeDistance(height, f);
1543
+ return { x: cx + R * Math.sin(a), z: R * Math.cos(a), fov: f };
1544
+ };
1545
+ const keyframes = [];
1546
+ for (let i = 0; i < steps; i++) {
1547
+ keyframes.push({ at: duration * i / steps, to: pos(i + 1), duration: duration / steps, ease: "none" });
1548
+ }
1549
+ const p0 = pos(0);
1550
+ const spec = {
1551
+ type: "camera",
1552
+ duration,
1553
+ initial: {
1554
+ x: p0.x,
1555
+ y: cy,
1556
+ z: p0.z,
1557
+ lookAtX: cx,
1558
+ lookAtY: cy,
1559
+ lookAtZ: 0,
1560
+ ...p0.fov !== void 0 ? { fov: p0.fov } : fov !== void 0 ? { fov } : {}
1561
+ },
1562
+ keyframes
1563
+ };
1564
+ if (opts.at !== void 0) spec.at = opts.at;
1565
+ if (opts.name !== void 0) spec.name = opts.name;
1566
+ return spec;
1567
+ }
1568
+
1569
+ // src/transforms/withFade.ts
1570
+ function withFade(spec, opts) {
1571
+ warnUnknownOptions("withFade()", opts, ["in", "out"]);
1572
+ if (opts.in != null && opts.in > 0) {
1573
+ spec.initial = { ...spec.initial ?? {}, alpha: 0 };
1574
+ const kf = { at: 0, to: { alpha: 1 }, duration: opts.in };
1575
+ spec.keyframes = [...spec.keyframes ?? [], kf];
1576
+ }
1577
+ if (opts.out != null && opts.out > 0) {
1578
+ if (spec.duration == null) {
1579
+ throw new Error(
1580
+ "withFade(spec, { out }) requires spec.duration to be set so the fade-out can be anchored to the sequence end. Set spec.duration before passing the spec in."
1581
+ );
1582
+ }
1583
+ const kf = { at: spec.duration - opts.out, to: { alpha: 0 }, duration: opts.out };
1584
+ spec.keyframes = [...spec.keyframes ?? [], kf];
1585
+ }
1586
+ return spec;
1587
+ }
3020
1588
 
3021
- export { Movie, kenBurns };
1589
+ export { Movie, kenBurns, orbit, withFade };
3022
1590
  //# sourceMappingURL=index.js.map
3023
1591
  //# sourceMappingURL=index.js.map