pixi-effects 0.1.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/dist/index.cjs ADDED
@@ -0,0 +1,3045 @@
1
+ 'use strict';
2
+
3
+ var gsap = require('gsap');
4
+ var PIXI = require('pixi.js');
5
+ var PixiPlugin = require('gsap/PixiPlugin');
6
+ var mediabunny = require('mediabunny');
7
+
8
+ function _interopNamespace(e) {
9
+ if (e && e.__esModule) return e;
10
+ var n = Object.create(null);
11
+ if (e) {
12
+ Object.keys(e).forEach(function (k) {
13
+ if (k !== 'default') {
14
+ var d = Object.getOwnPropertyDescriptor(e, k);
15
+ Object.defineProperty(n, k, d.get ? d : {
16
+ enumerable: true,
17
+ get: function () { return e[k]; }
18
+ });
19
+ }
20
+ });
21
+ }
22
+ n.default = e;
23
+ return Object.freeze(n);
24
+ }
25
+
26
+ var PIXI__namespace = /*#__PURE__*/_interopNamespace(PIXI);
27
+
28
+ var __defProp = Object.defineProperty;
29
+ var __getOwnPropNames = Object.getOwnPropertyNames;
30
+ var __esm = (fn, res) => function __init() {
31
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
32
+ };
33
+ var __export = (target, all) => {
34
+ for (var name in all)
35
+ __defProp(target, name, { get: all[name], enumerable: true });
36
+ };
37
+
38
+ // src/expr/Parser.ts
39
+ function isDigit(c) {
40
+ return c >= "0" && c <= "9";
41
+ }
42
+ function isIdStart(c) {
43
+ return c >= "a" && c <= "z" || c >= "A" && c <= "Z" || c === "_";
44
+ }
45
+ function isIdCont(c) {
46
+ return isIdStart(c) || isDigit(c);
47
+ }
48
+ function tokenize(src) {
49
+ const out = [];
50
+ for (let i = 0; i < src.length; ) {
51
+ const c = src[i];
52
+ if (c === " " || c === " " || c === "\n" || c === "\r") {
53
+ i++;
54
+ continue;
55
+ }
56
+ if (isDigit(c) || c === "." && isDigit(src[i + 1] ?? "")) {
57
+ let j = i + 1;
58
+ while (j < src.length && (isDigit(src[j]) || src[j] === ".")) j++;
59
+ const value = parseFloat(src.slice(i, j));
60
+ if (Number.isNaN(value)) throw new Error(`bad number at ${i}`);
61
+ out.push({ type: "num", value });
62
+ i = j;
63
+ continue;
64
+ }
65
+ if (isIdStart(c)) {
66
+ let j = i + 1;
67
+ while (j < src.length && isIdCont(src[j])) j++;
68
+ out.push({ type: "id", name: src.slice(i, j) });
69
+ i = j;
70
+ continue;
71
+ }
72
+ if (c === "+" || c === "-" || c === "*" || c === "/") {
73
+ out.push({ type: "op", op: c });
74
+ i++;
75
+ continue;
76
+ }
77
+ if (c === "(") {
78
+ out.push({ type: "lparen" });
79
+ i++;
80
+ continue;
81
+ }
82
+ if (c === ")") {
83
+ out.push({ type: "rparen" });
84
+ i++;
85
+ continue;
86
+ }
87
+ if (c === ",") {
88
+ out.push({ type: "comma" });
89
+ i++;
90
+ continue;
91
+ }
92
+ throw new Error(`unexpected character "${c}" at ${i}`);
93
+ }
94
+ return out;
95
+ }
96
+ function parseExpr(c) {
97
+ return parseAdd(c);
98
+ }
99
+ function parseAdd(c) {
100
+ let node = parseMul(c);
101
+ for (; ; ) {
102
+ const t = c.peek();
103
+ if (t?.type !== "op" || t.op !== "+" && t.op !== "-") break;
104
+ c.next();
105
+ node = { kind: "bin", op: t.op, left: node, right: parseMul(c) };
106
+ }
107
+ return node;
108
+ }
109
+ function parseMul(c) {
110
+ let node = parseUnary(c);
111
+ for (; ; ) {
112
+ const t = c.peek();
113
+ if (t?.type !== "op" || t.op !== "*" && t.op !== "/") break;
114
+ c.next();
115
+ node = { kind: "bin", op: t.op, left: node, right: parseUnary(c) };
116
+ }
117
+ return node;
118
+ }
119
+ function parseUnary(c) {
120
+ const t = c.peek();
121
+ if (t?.type === "op" && t.op === "-") {
122
+ c.next();
123
+ return { kind: "neg", child: parseUnary(c) };
124
+ }
125
+ if (t?.type === "op" && t.op === "+") {
126
+ c.next();
127
+ return parseUnary(c);
128
+ }
129
+ return parsePrimary(c);
130
+ }
131
+ function parsePrimary(c) {
132
+ const t = c.next();
133
+ if (!t) throw new Error("unexpected end of expression");
134
+ if (t.type === "num") return { kind: "num", value: t.value };
135
+ if (t.type === "lparen") {
136
+ const node = parseExpr(c);
137
+ const close = c.next();
138
+ if (close?.type !== "rparen") throw new Error('expected ")"');
139
+ return node;
140
+ }
141
+ if (t.type === "id") {
142
+ if (c.peek()?.type === "lparen") {
143
+ c.next();
144
+ const args = [];
145
+ if (c.peek()?.type !== "rparen") {
146
+ args.push(parseExpr(c));
147
+ while (c.peek()?.type === "comma") {
148
+ c.next();
149
+ args.push(parseExpr(c));
150
+ }
151
+ }
152
+ const close = c.next();
153
+ if (close?.type !== "rparen") throw new Error('expected ")"');
154
+ return { kind: "call", name: t.name, args };
155
+ }
156
+ return { kind: "var", name: t.name };
157
+ }
158
+ throw new Error(`unexpected token "${t.type}"`);
159
+ }
160
+ function parse(src) {
161
+ const c = new Cursor(tokenize(src));
162
+ const node = parseExpr(c);
163
+ if (c.peek()) throw new Error("trailing input");
164
+ return node;
165
+ }
166
+ function evalNode(node, scope) {
167
+ switch (node.kind) {
168
+ case "num":
169
+ return node.value;
170
+ case "var": {
171
+ const v = scope[node.name];
172
+ if (typeof v !== "number") throw new Error(`undefined variable: ${node.name}`);
173
+ return v;
174
+ }
175
+ case "neg":
176
+ return -evalNode(node.child, scope);
177
+ case "bin": {
178
+ const l = evalNode(node.left, scope);
179
+ const r = evalNode(node.right, scope);
180
+ switch (node.op) {
181
+ case "+":
182
+ return l + r;
183
+ case "-":
184
+ return l - r;
185
+ case "*":
186
+ return l * r;
187
+ case "/":
188
+ return l / r;
189
+ }
190
+ }
191
+ /* c8 ignore next 5 */
192
+ // eslint-disable-next-line no-fallthrough
193
+ case "call": {
194
+ const fn = FUNCS[node.name];
195
+ if (!fn) throw new Error(`unknown function: ${node.name}`);
196
+ return fn(...node.args.map((a) => evalNode(a, scope)));
197
+ }
198
+ }
199
+ }
200
+ function isExpr(v) {
201
+ return typeof v === "string" && v.length > 0;
202
+ }
203
+ function evaluateExpr(source, scope) {
204
+ let ast = astCache.get(source);
205
+ if (ast === void 0) {
206
+ try {
207
+ ast = parse(source);
208
+ } catch (e) {
209
+ console.warn(`pixi-effects: expression failed: ${source}`, e);
210
+ ast = ZERO_NODE;
211
+ }
212
+ astCache.set(source, ast);
213
+ }
214
+ try {
215
+ return evalNode(ast, scope);
216
+ } catch (e) {
217
+ console.warn(`pixi-effects: expression failed: ${source}`, e);
218
+ return 0;
219
+ }
220
+ }
221
+ var FUNCS, Cursor, ZERO_NODE, astCache;
222
+ var init_Parser = __esm({
223
+ "src/expr/Parser.ts"() {
224
+ FUNCS = {
225
+ min: Math.min,
226
+ max: Math.max,
227
+ abs: Math.abs,
228
+ floor: Math.floor,
229
+ ceil: Math.ceil,
230
+ round: Math.round,
231
+ sqrt: Math.sqrt,
232
+ pow: Math.pow,
233
+ sin: Math.sin,
234
+ cos: Math.cos,
235
+ tan: Math.tan
236
+ };
237
+ Cursor = class {
238
+ constructor(tokens) {
239
+ this.tokens = tokens;
240
+ }
241
+ tokens;
242
+ i = 0;
243
+ peek() {
244
+ return this.tokens[this.i];
245
+ }
246
+ next() {
247
+ return this.tokens[this.i++];
248
+ }
249
+ };
250
+ ZERO_NODE = { kind: "num", value: 0 };
251
+ astCache = /* @__PURE__ */ new Map();
252
+ }
253
+ });
254
+
255
+ // src/expr/normalizeProps.ts
256
+ function normalizeProps(input, scope, options = {}) {
257
+ const skip = new Set(options.skipKeys ?? []);
258
+ return walk(input, scope, skip, null);
259
+ }
260
+ function walk(value, scope, skip, currentKey) {
261
+ if (Array.isArray(value)) {
262
+ return value.map((v) => walk(v, scope, skip, currentKey));
263
+ }
264
+ if (value !== null && typeof value === "object") {
265
+ const out = {};
266
+ for (const k of Object.keys(value)) {
267
+ out[k] = walk(value[k], scope, skip, k);
268
+ }
269
+ return out;
270
+ }
271
+ if (isExpr(value) && !(currentKey !== null && skip.has(currentKey))) {
272
+ if (looksLikeColorString(value)) return value;
273
+ return evaluateExpr(value, scope);
274
+ }
275
+ return value;
276
+ }
277
+ function looksLikeColorString(s) {
278
+ if (s.length === 0) return false;
279
+ if (s.charCodeAt(0) === 35) return true;
280
+ return /^(rgb|hsl|oklab|oklch|lab|lch)\b/i.test(s);
281
+ }
282
+ var init_normalizeProps = __esm({
283
+ "src/expr/normalizeProps.ts"() {
284
+ init_Parser();
285
+ }
286
+ });
287
+ function resolveAt(at, duration) {
288
+ if (at === void 0 || at === null) return 0;
289
+ return at < 0 ? duration + at : at;
290
+ }
291
+ function normalizeKeyframe(kf, parentDuration) {
292
+ const at = resolveAt(kf.at, parentDuration);
293
+ const duration = kf.duration ?? 0;
294
+ const ease = kf.ease ?? "none";
295
+ const hasSet = !!kf.set;
296
+ const hasFrom = !!kf.from;
297
+ const hasTo = !!kf.to;
298
+ let kind;
299
+ if (hasSet) kind = "set";
300
+ else if (hasFrom && hasTo) kind = "fromTo";
301
+ else if (hasFrom) kind = "from";
302
+ else if (hasTo) kind = "to";
303
+ else kind = "to";
304
+ return { at, duration, ease, kind, set: kf.set, from: kf.from, to: kf.to };
305
+ }
306
+ function partitionProps(props) {
307
+ const ownProps = {};
308
+ const filterProps = {};
309
+ for (const k of Object.keys(props)) {
310
+ const m = k.match(/^filters\.([^.]+)\.(.+)$/);
311
+ if (m) {
312
+ const [, name, sub] = m;
313
+ (filterProps[name] ??= {})[sub] = props[k];
314
+ } else {
315
+ ownProps[k] = props[k];
316
+ }
317
+ }
318
+ return { ownProps, filterProps };
319
+ }
320
+ function findFilter(target, name) {
321
+ if (!target?.filters) return null;
322
+ for (const f of target.filters) {
323
+ if (f?._name === name) return f;
324
+ }
325
+ return null;
326
+ }
327
+ function pixiwrap(props) {
328
+ const out = {};
329
+ const pixi = {};
330
+ let usedPixi = false;
331
+ for (const [k, v] of Object.entries(props)) {
332
+ if (PIXI_SHORTHANDS.has(k)) {
333
+ pixi[k] = v;
334
+ usedPixi = true;
335
+ } else {
336
+ out[k] = v;
337
+ }
338
+ }
339
+ if (usedPixi) out.pixi = pixi;
340
+ return out;
341
+ }
342
+ function applyKeyframes(timeline, target, keyframes, parentDuration, scope, skipKeys = [], offset = 0) {
343
+ for (const raw of keyframes ?? []) {
344
+ const kf = normalizeKeyframe(raw, parentDuration);
345
+ const at = offset + kf.at;
346
+ if (kf.kind === "set") {
347
+ const resolved = normalizeProps(kf.set, scope, { skipKeys });
348
+ const { ownProps, filterProps } = partitionProps(resolved);
349
+ if (Object.keys(ownProps).length > 0) timeline.set(target, pixiwrap(ownProps), at);
350
+ for (const [name, props] of Object.entries(filterProps)) {
351
+ const f = findFilter(target, name);
352
+ if (!f) continue;
353
+ timeline.set(f, props, at);
354
+ }
355
+ } else if (kf.kind === "to") {
356
+ const resolved = normalizeProps(kf.to, scope, { skipKeys });
357
+ const { ownProps, filterProps } = partitionProps(resolved);
358
+ if (Object.keys(ownProps).length > 0)
359
+ timeline.to(target, { ...pixiwrap(ownProps), duration: kf.duration, ease: kf.ease }, at);
360
+ for (const [name, props] of Object.entries(filterProps)) {
361
+ const f = findFilter(target, name);
362
+ if (!f) continue;
363
+ timeline.to(f, { ...props, duration: kf.duration, ease: kf.ease }, at);
364
+ }
365
+ } else if (kf.kind === "from") {
366
+ const resolved = normalizeProps(kf.from, scope, { skipKeys });
367
+ const { ownProps, filterProps } = partitionProps(resolved);
368
+ if (Object.keys(ownProps).length > 0)
369
+ timeline.from(target, { ...pixiwrap(ownProps), duration: kf.duration, ease: kf.ease }, at);
370
+ for (const [name, props] of Object.entries(filterProps)) {
371
+ const f = findFilter(target, name);
372
+ if (!f) continue;
373
+ timeline.from(f, { ...props, duration: kf.duration, ease: kf.ease }, at);
374
+ }
375
+ } else {
376
+ const fromResolved = normalizeProps(kf.from, scope, { skipKeys });
377
+ const toResolved = normalizeProps(kf.to, scope, { skipKeys });
378
+ const fromSplit = partitionProps(fromResolved);
379
+ const toSplit = partitionProps(toResolved);
380
+ const ownKeys = /* @__PURE__ */ new Set([...Object.keys(fromSplit.ownProps), ...Object.keys(toSplit.ownProps)]);
381
+ if (ownKeys.size > 0) {
382
+ timeline.fromTo(
383
+ target,
384
+ { ...pixiwrap(fromSplit.ownProps) },
385
+ { ...pixiwrap(toSplit.ownProps), duration: kf.duration, ease: kf.ease },
386
+ at
387
+ );
388
+ }
389
+ const filterNames = /* @__PURE__ */ new Set([
390
+ ...Object.keys(fromSplit.filterProps),
391
+ ...Object.keys(toSplit.filterProps)
392
+ ]);
393
+ for (const name of filterNames) {
394
+ const f = findFilter(target, name);
395
+ if (!f) continue;
396
+ timeline.fromTo(
397
+ f,
398
+ { ...fromSplit.filterProps[name] ?? {} },
399
+ { ...toSplit.filterProps[name] ?? {}, duration: kf.duration, ease: kf.ease },
400
+ at
401
+ );
402
+ }
403
+ }
404
+ }
405
+ }
406
+ function applyInitial(target, initial, scope, skipKeys = []) {
407
+ if (!initial) return;
408
+ const resolved = normalizeProps(initial, scope, { skipKeys });
409
+ const { ownProps, filterProps } = partitionProps(resolved);
410
+ if (Object.keys(ownProps).length > 0) gsap.gsap.set(target, pixiwrap(ownProps));
411
+ for (const [name, props] of Object.entries(filterProps)) {
412
+ const f = findFilter(target, name);
413
+ if (!f) continue;
414
+ gsap.gsap.set(f, props);
415
+ }
416
+ }
417
+ var PIXI_SHORTHANDS;
418
+ var init_Timeline = __esm({
419
+ "src/core/Timeline.ts"() {
420
+ init_normalizeProps();
421
+ PIXI_SHORTHANDS = /* @__PURE__ */ new Set([
422
+ "scale",
423
+ "scaleX",
424
+ "scaleY",
425
+ "anchor",
426
+ "anchorX",
427
+ "anchorY",
428
+ "pivot",
429
+ "pivotX",
430
+ "pivotY",
431
+ "skew",
432
+ "skewX",
433
+ "skewY",
434
+ "rotation",
435
+ "position",
436
+ "positionX",
437
+ "positionY",
438
+ "tilePosition",
439
+ "tilePositionX",
440
+ "tilePositionY",
441
+ "tileScale",
442
+ "tileScaleX",
443
+ "tileScaleY",
444
+ "tint",
445
+ "colorize",
446
+ "colorizeAmount",
447
+ "colorMatrixFilter",
448
+ "blur",
449
+ "blurX",
450
+ "blurY",
451
+ "blurPadding",
452
+ "autoAlpha",
453
+ "lineColor",
454
+ "lineAlpha",
455
+ "fillColor",
456
+ "fillAlpha"
457
+ ]);
458
+ }
459
+ });
460
+
461
+ // src/expr/Scope.ts
462
+ function buildScope(sequence, parent, root) {
463
+ const w = sequence.intrinsicWidth || 0;
464
+ const h = sequence.intrinsicHeight || 0;
465
+ const W = parent?.width ?? root.width;
466
+ const H = parent?.height ?? root.height;
467
+ const GW = root.width;
468
+ const GH = root.height;
469
+ const contain = w === 0 || h === 0 ? 1 : Math.min(W / w, H / h);
470
+ const cover = w === 0 || h === 0 ? 1 : Math.max(W / w, H / h);
471
+ return {
472
+ w,
473
+ h,
474
+ W,
475
+ H,
476
+ GW,
477
+ GH,
478
+ contain,
479
+ cover,
480
+ t: sequence.at ?? 0,
481
+ d: sequence.duration ?? parent?.duration ?? root.duration,
482
+ T: parent?.duration ?? root.duration
483
+ };
484
+ }
485
+ var init_Scope = __esm({
486
+ "src/expr/Scope.ts"() {
487
+ }
488
+ });
489
+ function toRgb01(input) {
490
+ if (Array.isArray(input)) return [input[0], input[1], input[2]];
491
+ const c = new PIXI.Color(input);
492
+ return [c.red, c.green, c.blue];
493
+ }
494
+ var GL_FRAGMENT, WGSL_SOURCE, ChromaKeyFilter;
495
+ var init_ChromaKey = __esm({
496
+ "src/filters/ChromaKey.ts"() {
497
+ GL_FRAGMENT = `
498
+ in vec2 vTextureCoord;
499
+ uniform sampler2D uTexture;
500
+ uniform vec3 uKeyColor;
501
+ uniform float uThreshold;
502
+ uniform float uSmoothing;
503
+ uniform float uSpill;
504
+
505
+ out vec4 finalColor;
506
+
507
+ void main(void) {
508
+ vec4 raw = texture(uTexture, vTextureCoord);
509
+
510
+ // Pixi passes pre-multiplied alpha into filters. Un-premultiply so the
511
+ // chromakey distance is computed against the source color, not a darkened
512
+ // version that drifts away from the key as sprite.alpha drops.
513
+ vec3 rgb = raw.a > 0.0 ? raw.rgb / raw.a : vec3(0.0);
514
+
515
+ vec3 ycbcr = vec3(
516
+ 0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b,
517
+ -0.169 * rgb.r - 0.331 * rgb.g + 0.5 * rgb.b + 0.5,
518
+ 0.5 * rgb.r - 0.419 * rgb.g - 0.081 * rgb.b + 0.5
519
+ );
520
+ vec3 keyYcbcr = vec3(
521
+ 0.299 * uKeyColor.r + 0.587 * uKeyColor.g + 0.114 * uKeyColor.b,
522
+ -0.169 * uKeyColor.r - 0.331 * uKeyColor.g + 0.5 * uKeyColor.b + 0.5,
523
+ 0.5 * uKeyColor.r - 0.419 * uKeyColor.g - 0.081 * uKeyColor.b + 0.5
524
+ );
525
+
526
+ float dist = length(ycbcr - keyYcbcr);
527
+ float alpha = smoothstep(uThreshold - uSmoothing, uThreshold + uSmoothing, dist);
528
+
529
+ vec3 despilled = rgb;
530
+ if (uSpill > 0.0) {
531
+ float spillAmount = max(0.0, rgb.g - max(rgb.r, rgb.b));
532
+ despilled.g -= spillAmount * uSpill;
533
+ }
534
+
535
+ float outA = raw.a * alpha;
536
+ finalColor = vec4(despilled * outA, outA);
537
+ }
538
+ `;
539
+ WGSL_SOURCE = `
540
+ struct GlobalFilterUniforms {
541
+ uInputSize: vec4<f32>,
542
+ uInputPixel: vec4<f32>,
543
+ uInputClamp: vec4<f32>,
544
+ uOutputFrame: vec4<f32>,
545
+ uGlobalFrame: vec4<f32>,
546
+ uOutputTexture: vec4<f32>,
547
+ };
548
+
549
+ struct ChromaUniforms {
550
+ uKeyColor: vec3<f32>,
551
+ uThreshold: f32,
552
+ uSmoothing: f32,
553
+ uSpill: f32,
554
+ };
555
+
556
+ @group(0) @binding(0) var<uniform> gfu: GlobalFilterUniforms;
557
+ @group(0) @binding(1) var uTexture: texture_2d<f32>;
558
+ @group(0) @binding(2) var uSampler : sampler;
559
+ @group(1) @binding(0) var<uniform> chromaUniforms : ChromaUniforms;
560
+
561
+ struct VSOutput {
562
+ @builtin(position) position: vec4<f32>,
563
+ @location(0) uv : vec2<f32>,
564
+ };
565
+
566
+ fn filterVertexPosition(aPosition : vec2<f32>) -> vec4<f32> {
567
+ var position = aPosition * gfu.uOutputFrame.zw + gfu.uOutputFrame.xy;
568
+ position.x = position.x * (2.0 / gfu.uOutputTexture.x) - 1.0;
569
+ position.y = position.y * (2.0 * gfu.uOutputTexture.z / gfu.uOutputTexture.y) - gfu.uOutputTexture.z;
570
+ return vec4<f32>(position, 0.0, 1.0);
571
+ }
572
+
573
+ fn filterTextureCoord(aPosition : vec2<f32>) -> vec2<f32> {
574
+ return aPosition * (gfu.uOutputFrame.zw * gfu.uInputSize.zw);
575
+ }
576
+
577
+ // NOTE on whitespace: PIXI v8's WGSL attribute extractor is regex-based and
578
+ // expects the noise/blur reference format \u2014 multi-line param list + space
579
+ // before the colon. A compact one-liner with no space before the colon parses
580
+ // as having NO vertex attributes, which then causes a "Vertex attribute slot 0
581
+ // not present in the VertexState" pipeline validation error.
582
+ @vertex
583
+ fn mainVertex(
584
+ @location(0) aPosition : vec2<f32>,
585
+ ) -> VSOutput {
586
+ return VSOutput(
587
+ filterVertexPosition(aPosition),
588
+ filterTextureCoord(aPosition),
589
+ );
590
+ }
591
+
592
+ @fragment
593
+ fn mainFragment(@location(0) uv : vec2<f32>) -> @location(0) vec4<f32> {
594
+ let raw = textureSample(uTexture, uSampler, uv);
595
+
596
+ // Un-premultiply (see comment in the GLSL version above).
597
+ var rgb: vec3<f32>;
598
+ if (raw.a > 0.0) {
599
+ rgb = raw.rgb / raw.a;
600
+ } else {
601
+ rgb = vec3<f32>(0.0);
602
+ }
603
+
604
+ let key = chromaUniforms.uKeyColor;
605
+
606
+ let ycbcr = vec3<f32>(
607
+ 0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b,
608
+ -0.169 * rgb.r - 0.331 * rgb.g + 0.5 * rgb.b + 0.5,
609
+ 0.5 * rgb.r - 0.419 * rgb.g - 0.081 * rgb.b + 0.5,
610
+ );
611
+ let keyYcbcr = vec3<f32>(
612
+ 0.299 * key.r + 0.587 * key.g + 0.114 * key.b,
613
+ -0.169 * key.r - 0.331 * key.g + 0.5 * key.b + 0.5,
614
+ 0.5 * key.r - 0.419 * key.g - 0.081 * key.b + 0.5,
615
+ );
616
+
617
+ let dist = length(ycbcr - keyYcbcr);
618
+ let alpha = smoothstep(
619
+ chromaUniforms.uThreshold - chromaUniforms.uSmoothing,
620
+ chromaUniforms.uThreshold + chromaUniforms.uSmoothing,
621
+ dist,
622
+ );
623
+
624
+ var despilled = rgb;
625
+ if (chromaUniforms.uSpill > 0.0) {
626
+ let spillAmount = max(0.0, rgb.g - max(rgb.r, rgb.b));
627
+ despilled.g = despilled.g - spillAmount * chromaUniforms.uSpill;
628
+ }
629
+
630
+ let outA = raw.a * alpha;
631
+ return vec4<f32>(despilled * outA, outA);
632
+ }
633
+ `;
634
+ ChromaKeyFilter = class extends PIXI.Filter {
635
+ constructor(options = {}) {
636
+ const {
637
+ keyColor = "#00ff00",
638
+ threshold = 0.4,
639
+ smoothing = 0.1,
640
+ spill = 0.2
641
+ } = options;
642
+ const glProgram = PIXI.GlProgram.from({
643
+ vertex: PIXI.defaultFilterVert,
644
+ fragment: GL_FRAGMENT,
645
+ name: "chroma-key-filter"
646
+ });
647
+ const gpuProgram = PIXI.GpuProgram.from({
648
+ vertex: { source: WGSL_SOURCE, entryPoint: "mainVertex" },
649
+ fragment: { source: WGSL_SOURCE, entryPoint: "mainFragment" }
650
+ });
651
+ super({
652
+ glProgram,
653
+ gpuProgram,
654
+ resources: {
655
+ chromaUniforms: new PIXI.UniformGroup({
656
+ uKeyColor: { value: toRgb01(keyColor), type: "vec3<f32>" },
657
+ uThreshold: { value: threshold, type: "f32" },
658
+ uSmoothing: { value: smoothing, type: "f32" },
659
+ uSpill: { value: spill, type: "f32" }
660
+ })
661
+ }
662
+ });
663
+ }
664
+ get threshold() {
665
+ return this.resources.chromaUniforms.uniforms.uThreshold;
666
+ }
667
+ set threshold(v) {
668
+ this.resources.chromaUniforms.uniforms.uThreshold = v;
669
+ }
670
+ get smoothing() {
671
+ return this.resources.chromaUniforms.uniforms.uSmoothing;
672
+ }
673
+ set smoothing(v) {
674
+ this.resources.chromaUniforms.uniforms.uSmoothing = v;
675
+ }
676
+ get spill() {
677
+ return this.resources.chromaUniforms.uniforms.uSpill;
678
+ }
679
+ set spill(v) {
680
+ this.resources.chromaUniforms.uniforms.uSpill = v;
681
+ }
682
+ setKeyColor(input) {
683
+ const rgb = toRgb01(input);
684
+ const u = this.resources.chromaUniforms.uniforms.uKeyColor;
685
+ u[0] = rgb[0];
686
+ u[1] = rgb[1];
687
+ u[2] = rgb[2];
688
+ }
689
+ };
690
+ }
691
+ });
692
+
693
+ // src/filters/index.ts
694
+ function looksLikePixiFilter(x) {
695
+ return !!x && typeof x === "object" && typeof x.apply === "function";
696
+ }
697
+ function createFilter(spec) {
698
+ if (spec.type === "custom") {
699
+ const inst2 = spec.filter;
700
+ if (!looksLikePixiFilter(inst2)) {
701
+ throw new Error('pixi-effects: { type: "custom" } requires a `filter` that is a PIXI Filter instance (duck-typed: must have an `apply` method).');
702
+ }
703
+ const named = inst2;
704
+ named._name = spec.name;
705
+ return named;
706
+ }
707
+ const Cls = registry[spec.type];
708
+ if (!Cls) throw new Error(`pixi-effects: unknown filter type "${spec.type}"`);
709
+ const { type: _t, name, ...params } = spec;
710
+ const inst = new Cls(params);
711
+ inst._name = name;
712
+ return inst;
713
+ }
714
+ var registry;
715
+ var init_filters = __esm({
716
+ "src/filters/index.ts"() {
717
+ init_ChromaKey();
718
+ registry = {
719
+ chromaKey: ChromaKeyFilter
720
+ };
721
+ }
722
+ });
723
+ var Sequence;
724
+ var init_Base = __esm({
725
+ "src/sequences/Base.ts"() {
726
+ init_Timeline();
727
+ init_Scope();
728
+ init_filters();
729
+ Sequence = class {
730
+ spec;
731
+ parent;
732
+ root;
733
+ target = null;
734
+ intrinsicWidth = 0;
735
+ intrinsicHeight = 0;
736
+ at;
737
+ duration;
738
+ filters = [];
739
+ /**
740
+ * Mask sequence built from `spec.mask`, if any. Built and added to the
741
+ * scene graph by the parent CompositionSequence; rendered as a mask
742
+ * (not a normal child) by PIXI when `target.mask = maskSequence.target`.
743
+ */
744
+ maskSequence = null;
745
+ constructor(spec, parent, root) {
746
+ this.spec = spec;
747
+ this.parent = parent;
748
+ this.root = root;
749
+ this.at = spec.at ?? 0;
750
+ this.duration = spec.duration;
751
+ }
752
+ buildFilters() {
753
+ const specs = this.spec.filters ?? [];
754
+ this.filters = specs.map(createFilter);
755
+ if (this.target && "filters" in this.target) {
756
+ this.target.filters = this.filters;
757
+ }
758
+ const fa = this.spec.filterArea;
759
+ if (fa && this.target) {
760
+ this.target.filterArea = new PIXI.Rectangle(fa.x, fa.y, fa.width, fa.height);
761
+ }
762
+ }
763
+ scope() {
764
+ return buildScope(this, this.parent, this.root);
765
+ }
766
+ bindTimeline(timeline, offset = 0) {
767
+ if (!this.target) return;
768
+ const scope = this.scope();
769
+ applyInitial(this.target, this.spec.initial, scope);
770
+ applyKeyframes(timeline, this.target, this.spec.keyframes, this.duration, scope, [], offset);
771
+ const startTime = offset + this.at;
772
+ const endTime = startTime + this.duration;
773
+ this.target.renderable = startTime <= 0;
774
+ timeline.set(this.target, { renderable: true }, startTime);
775
+ timeline.set(this.target, { renderable: false }, endTime);
776
+ }
777
+ collectAudio(_out, _baseTime) {
778
+ }
779
+ destroy() {
780
+ this.maskSequence?.destroy();
781
+ this.maskSequence = null;
782
+ this.target?.destroy?.();
783
+ this.target = null;
784
+ }
785
+ };
786
+ }
787
+ });
788
+
789
+ // src/expr/colorInterp.ts
790
+ function buildColorInterp(from, to, space) {
791
+ const fromRGBA = parseToRGBA(from);
792
+ const toRGBA = parseToRGBA(to);
793
+ const fromOk = rgbToOklab(fromRGBA);
794
+ const toOk = rgbToOklab(toRGBA);
795
+ if (space === "oklab") {
796
+ return (t) => {
797
+ const L = lerp(fromOk.L, toOk.L, t);
798
+ const a = lerp(fromOk.a, toOk.a, t);
799
+ const b = lerp(fromOk.b, toOk.b, t);
800
+ const alpha = lerp(fromRGBA.a, toRGBA.a, t);
801
+ return rgbaString(oklabToRgb({ L, a, b }), alpha);
802
+ };
803
+ }
804
+ const fromLCH = oklabToOklch(fromOk);
805
+ const toLCH = oklabToOklch(toOk);
806
+ if (fromLCH.C < 1e-4) fromLCH.h = toLCH.h;
807
+ if (toLCH.C < 1e-4) toLCH.h = fromLCH.h;
808
+ const hueDelta = shortestHueDelta(fromLCH.h, toLCH.h);
809
+ return (t) => {
810
+ const L = lerp(fromLCH.L, toLCH.L, t);
811
+ const C = lerp(fromLCH.C, toLCH.C, t);
812
+ const h = fromLCH.h + hueDelta * t;
813
+ const alpha = lerp(fromRGBA.a, toRGBA.a, t);
814
+ const ab = oklchToOklab({ L, C, h });
815
+ return rgbaString(oklabToRgb(ab), alpha);
816
+ };
817
+ }
818
+ function parseToRGBA(input) {
819
+ if (typeof input === "number") {
820
+ return { r: (input >> 16 & 255) / 255, g: (input >> 8 & 255) / 255, b: (input & 255) / 255, a: 1 };
821
+ }
822
+ const s = input.trim();
823
+ if (s.startsWith("#")) return parseHex(s);
824
+ if (s.startsWith("rgb")) return parseRgbFunc(s);
825
+ return { r: 0, g: 0, b: 0, a: 1 };
826
+ }
827
+ function parseHex(s) {
828
+ const hex = s.slice(1);
829
+ let r = 0, g = 0, b = 0, a = 1;
830
+ if (hex.length === 3 || hex.length === 4) {
831
+ r = parseInt(hex[0] + hex[0], 16) / 255;
832
+ g = parseInt(hex[1] + hex[1], 16) / 255;
833
+ b = parseInt(hex[2] + hex[2], 16) / 255;
834
+ if (hex.length === 4) a = parseInt(hex[3] + hex[3], 16) / 255;
835
+ } else if (hex.length === 6 || hex.length === 8) {
836
+ r = parseInt(hex.slice(0, 2), 16) / 255;
837
+ g = parseInt(hex.slice(2, 4), 16) / 255;
838
+ b = parseInt(hex.slice(4, 6), 16) / 255;
839
+ if (hex.length === 8) a = parseInt(hex.slice(6, 8), 16) / 255;
840
+ }
841
+ return { r, g, b, a };
842
+ }
843
+ function parseRgbFunc(s) {
844
+ const inner = s.replace(/^rgba?\(/, "").replace(/\)$/, "");
845
+ const parts = inner.split(/[,\s/]+/).filter(Boolean).map(parseFloat);
846
+ return {
847
+ r: (parts[0] ?? 0) / 255,
848
+ g: (parts[1] ?? 0) / 255,
849
+ b: (parts[2] ?? 0) / 255,
850
+ a: parts.length > 3 ? parts[3] ?? 1 : 1
851
+ };
852
+ }
853
+ function rgbaString(rgb, alpha) {
854
+ const r = Math.round(clamp01(rgb.r) * 255);
855
+ const g = Math.round(clamp01(rgb.g) * 255);
856
+ const b = Math.round(clamp01(rgb.b) * 255);
857
+ return `rgba(${r},${g},${b},${alpha})`;
858
+ }
859
+ function srgbToLinear(c) {
860
+ return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
861
+ }
862
+ function linearToSrgb(c) {
863
+ return c <= 31308e-7 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
864
+ }
865
+ function rgbToOklab(rgba) {
866
+ const r = srgbToLinear(rgba.r);
867
+ const g = srgbToLinear(rgba.g);
868
+ const b = srgbToLinear(rgba.b);
869
+ const l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b;
870
+ const m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b;
871
+ const s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b;
872
+ const l_ = Math.cbrt(l);
873
+ const m_ = Math.cbrt(m);
874
+ const s_ = Math.cbrt(s);
875
+ return {
876
+ L: 0.2104542553 * l_ + 0.793617785 * m_ - 0.0040720468 * s_,
877
+ a: 1.9779984951 * l_ - 2.428592205 * m_ + 0.4505937099 * s_,
878
+ b: 0.0259040371 * l_ + 0.7827717662 * m_ - 0.808675766 * s_
879
+ };
880
+ }
881
+ function oklabToRgb(lab) {
882
+ const l_ = lab.L + 0.3963377774 * lab.a + 0.2158037573 * lab.b;
883
+ const m_ = lab.L - 0.1055613458 * lab.a - 0.0638541728 * lab.b;
884
+ const s_ = lab.L - 0.0894841775 * lab.a - 1.291485548 * lab.b;
885
+ const l = l_ * l_ * l_;
886
+ const m = m_ * m_ * m_;
887
+ const s = s_ * s_ * s_;
888
+ return {
889
+ r: linearToSrgb(4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s),
890
+ g: linearToSrgb(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s),
891
+ b: linearToSrgb(-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s)
892
+ };
893
+ }
894
+ function oklabToOklch(lab) {
895
+ return { L: lab.L, C: Math.hypot(lab.a, lab.b), h: Math.atan2(lab.b, lab.a) };
896
+ }
897
+ function oklchToOklab(lch) {
898
+ return { L: lch.L, a: lch.C * Math.cos(lch.h), b: lch.C * Math.sin(lch.h) };
899
+ }
900
+ function lerp(a, b, t) {
901
+ return a + (b - a) * t;
902
+ }
903
+ function clamp01(v) {
904
+ return v < 0 ? 0 : v > 1 ? 1 : v;
905
+ }
906
+ function shortestHueDelta(from, to) {
907
+ const TWO_PI = Math.PI * 2;
908
+ let d = (to - from) % TWO_PI;
909
+ if (d > Math.PI) d -= TWO_PI;
910
+ if (d < -Math.PI) d += TWO_PI;
911
+ return d;
912
+ }
913
+ var init_colorInterp = __esm({
914
+ "src/expr/colorInterp.ts"() {
915
+ }
916
+ });
917
+ function tweenColor(timeline, target, key, fromValue, toValue, duration, ease, at, colorSpace, onUpdate) {
918
+ let interp = null;
919
+ const proxy = { p: 0 };
920
+ timeline.fromTo(
921
+ proxy,
922
+ { p: 0 },
923
+ {
924
+ p: 1,
925
+ duration,
926
+ ease,
927
+ onStart: () => {
928
+ const start = fromValue !== void 0 ? fromValue : target[key];
929
+ if (start === void 0) return;
930
+ interp = colorSpace === "rgb" ? gsap.gsap.utils.interpolate(start, toValue) : buildColorInterp(start, toValue, colorSpace);
931
+ },
932
+ onUpdate: () => {
933
+ if (interp) {
934
+ target[key] = interp(proxy.p);
935
+ if (onUpdate) onUpdate();
936
+ }
937
+ }
938
+ },
939
+ at
940
+ );
941
+ }
942
+ var init_colorTween = __esm({
943
+ "src/expr/colorTween.ts"() {
944
+ init_colorInterp();
945
+ }
946
+ });
947
+ function stripKeys(props, keys) {
948
+ if (!props) return props;
949
+ const drop = new Set(keys);
950
+ const out = {};
951
+ let changed = false;
952
+ for (const k of Object.keys(props)) {
953
+ if (drop.has(k)) {
954
+ changed = true;
955
+ continue;
956
+ }
957
+ out[k] = props[k];
958
+ }
959
+ return changed ? out : props;
960
+ }
961
+ function stripKeysInKeyframe(kf, keys) {
962
+ return {
963
+ ...kf,
964
+ set: stripKeys(kf.set, keys),
965
+ to: stripKeys(kf.to, keys),
966
+ from: stripKeys(kf.from, keys)
967
+ };
968
+ }
969
+ var ImageSequence;
970
+ var init_Image = __esm({
971
+ "src/sequences/Image.ts"() {
972
+ init_Base();
973
+ init_Timeline();
974
+ init_colorTween();
975
+ ImageSequence = class extends Sequence {
976
+ async build() {
977
+ const texture = await PIXI.Assets.get(this.spec.asset);
978
+ const sprite = new PIXI.Sprite({ texture, label: this.spec.name });
979
+ sprite.cullable = true;
980
+ this.target = sprite;
981
+ this.intrinsicWidth = texture.width;
982
+ this.intrinsicHeight = texture.height;
983
+ if (this.duration === void 0) {
984
+ this.duration = this.parent?.duration ?? this.root.duration;
985
+ }
986
+ this.buildFilters();
987
+ }
988
+ bindTimeline(timeline, offset = 0) {
989
+ if (!this.target) return;
990
+ const colorSpace = this.spec.colorSpace;
991
+ if (!colorSpace || colorSpace === "rgb") {
992
+ super.bindTimeline(timeline, offset);
993
+ return;
994
+ }
995
+ const scope = this.scope();
996
+ const initial = stripKeys(this.spec.initial, ["tint"]);
997
+ const keyframes = this.spec.keyframes ? this.spec.keyframes.map((kf) => stripKeysInKeyframe(kf, ["tint"])) : void 0;
998
+ applyInitial(this.target, initial, scope);
999
+ applyKeyframes(timeline, this.target, keyframes, this.duration, scope, [], offset);
1000
+ const initTint = this.spec.initial?.tint;
1001
+ if (initTint !== void 0) this.target.tint = initTint;
1002
+ for (const kf of this.spec.keyframes ?? []) {
1003
+ const at = offset + resolveAt(kf.at, this.duration);
1004
+ const duration = kf.duration ?? 0;
1005
+ const ease = kf.ease ?? "none";
1006
+ const fromTint = kf.from?.tint;
1007
+ const toTint = kf.to?.tint;
1008
+ const setTint = kf.set?.tint;
1009
+ if (setTint !== void 0) {
1010
+ timeline.call(() => {
1011
+ this.target.tint = setTint;
1012
+ }, [], at);
1013
+ }
1014
+ if (toTint !== void 0) {
1015
+ tweenColor(
1016
+ timeline,
1017
+ this.target,
1018
+ "tint",
1019
+ fromTint,
1020
+ toTint,
1021
+ duration,
1022
+ ease,
1023
+ at,
1024
+ colorSpace
1025
+ );
1026
+ }
1027
+ }
1028
+ const startTime = offset + this.at;
1029
+ const endTime = startTime + this.duration;
1030
+ this.target.renderable = startTime <= 0;
1031
+ timeline.set(this.target, { renderable: true }, startTime);
1032
+ timeline.set(this.target, { renderable: false }, endTime);
1033
+ }
1034
+ };
1035
+ }
1036
+ });
1037
+ function runSuperWithStrippedSpec(inst, strippedSpec, timeline, offset) {
1038
+ const originalSpec = inst.spec;
1039
+ inst.spec = strippedSpec;
1040
+ try {
1041
+ Sequence.prototype.bindTimeline.call(inst, timeline, offset);
1042
+ } finally {
1043
+ inst.spec = originalSpec;
1044
+ }
1045
+ }
1046
+ function stripFill(spec) {
1047
+ return {
1048
+ ...spec,
1049
+ initial: stripKey(spec.initial, "fill"),
1050
+ keyframes: spec.keyframes ? spec.keyframes.map((kf) => ({
1051
+ ...kf,
1052
+ set: stripKey(kf.set, "fill"),
1053
+ to: stripKey(kf.to, "fill"),
1054
+ from: stripKey(kf.from, "fill")
1055
+ })) : void 0
1056
+ };
1057
+ }
1058
+ function stripKey(props, key) {
1059
+ if (!props) return props;
1060
+ if (!(key in props)) return props;
1061
+ const out = {};
1062
+ for (const k of Object.keys(props)) if (k !== key) out[k] = props[k];
1063
+ return out;
1064
+ }
1065
+ function bindFillKeyframes(timeline, text, keyframes, parentDuration, offset, colorSpace) {
1066
+ const state = { fill: text.style.fill };
1067
+ const writeFill = () => {
1068
+ text.style.fill = state.fill;
1069
+ };
1070
+ for (const kf of keyframes) {
1071
+ const at = offset + resolveAt(kf.at, parentDuration);
1072
+ const duration = kf.duration ?? 0;
1073
+ const ease = kf.ease ?? "none";
1074
+ const setFill = pickFill(kf.set);
1075
+ const fromFill = pickFill(kf.from);
1076
+ const toFill = pickFill(kf.to);
1077
+ if (setFill !== void 0) {
1078
+ timeline.call(() => {
1079
+ state.fill = setFill;
1080
+ writeFill();
1081
+ }, [], at);
1082
+ }
1083
+ if (toFill !== void 0) {
1084
+ tweenColor(
1085
+ timeline,
1086
+ state,
1087
+ "fill",
1088
+ fromFill,
1089
+ toFill,
1090
+ duration,
1091
+ ease,
1092
+ at,
1093
+ colorSpace,
1094
+ writeFill
1095
+ );
1096
+ }
1097
+ }
1098
+ }
1099
+ function pickFill(props) {
1100
+ if (!props) return void 0;
1101
+ const v = props.fill;
1102
+ if (typeof v === "string" || typeof v === "number") return v;
1103
+ return void 0;
1104
+ }
1105
+ var STYLE_OPAQUE_KEYS, TextSequence;
1106
+ var init_Text = __esm({
1107
+ "src/sequences/Text.ts"() {
1108
+ init_Base();
1109
+ init_normalizeProps();
1110
+ init_Timeline();
1111
+ init_colorTween();
1112
+ STYLE_OPAQUE_KEYS = ["fontFamily", "fill", "align", "fontStyle", "fontWeight"];
1113
+ TextSequence = class extends Sequence {
1114
+ async build() {
1115
+ const baseStyle = {
1116
+ fontFamily: "Arial",
1117
+ fontSize: 36,
1118
+ fill: "#ffffff",
1119
+ align: "center"
1120
+ };
1121
+ const text = new PIXI.Text({
1122
+ text: this.spec.text ?? "",
1123
+ style: baseStyle,
1124
+ label: this.spec.name
1125
+ });
1126
+ text.cullable = true;
1127
+ this.target = text;
1128
+ this.intrinsicWidth = text.width;
1129
+ this.intrinsicHeight = text.height;
1130
+ if (this.duration === void 0) {
1131
+ this.duration = this.parent?.duration ?? this.root.duration;
1132
+ }
1133
+ if (this.spec.style) {
1134
+ const scope = this.scope();
1135
+ const resolved = normalizeProps(
1136
+ this.spec.style,
1137
+ scope,
1138
+ { skipKeys: STYLE_OPAQUE_KEYS }
1139
+ );
1140
+ for (const k of Object.keys(resolved)) {
1141
+ text.style[k] = resolved[k];
1142
+ }
1143
+ }
1144
+ this.buildFilters();
1145
+ }
1146
+ bindTimeline(timeline, offset = 0) {
1147
+ if (!this.target) return;
1148
+ const colorSpace = this.spec.colorSpace ?? "rgb";
1149
+ const stripped = stripFill(this.spec);
1150
+ runSuperWithStrippedSpec(this, stripped, timeline, offset);
1151
+ bindFillKeyframes(timeline, this.target, this.spec.keyframes ?? [], this.duration, offset, colorSpace);
1152
+ }
1153
+ };
1154
+ }
1155
+ });
1156
+ var AudioSequence;
1157
+ var init_Audio = __esm({
1158
+ "src/sequences/Audio.ts"() {
1159
+ init_Base();
1160
+ init_Timeline();
1161
+ AudioSequence = class extends Sequence {
1162
+ _audioBuffer = null;
1163
+ async build() {
1164
+ const data = await PIXI.Assets.get(this.spec.asset);
1165
+ this._audioBuffer = data.audioBuffer;
1166
+ if (this.duration === void 0) {
1167
+ this.duration = this.spec.duration ?? data.duration ?? this.root.duration;
1168
+ }
1169
+ this.target = null;
1170
+ }
1171
+ bindTimeline(_timeline) {
1172
+ }
1173
+ collectAudio(out, baseTime) {
1174
+ if (!this._audioBuffer) return;
1175
+ const initialVolume = this.spec.volume ?? 1;
1176
+ const volumeKeyframes = [];
1177
+ const dur = this.duration;
1178
+ for (const raw of this.spec.keyframes ?? []) {
1179
+ const kf = normalizeKeyframe(raw, dur);
1180
+ const set = kf.set;
1181
+ const to = kf.to;
1182
+ const from = kf.from;
1183
+ if (kf.kind === "set" && set && "volume" in set) {
1184
+ volumeKeyframes.push({ time: baseTime + this.at + kf.at, value: set.volume });
1185
+ } else if (kf.kind === "to" && to && "volume" in to) {
1186
+ volumeKeyframes.push({ time: baseTime + this.at + kf.at + kf.duration, value: to.volume });
1187
+ } else if (kf.kind === "fromTo" && to && "volume" in to) {
1188
+ volumeKeyframes.push({ time: baseTime + this.at + kf.at, value: from?.volume ?? initialVolume });
1189
+ volumeKeyframes.push({ time: baseTime + this.at + kf.at + kf.duration, value: to.volume });
1190
+ } else if (kf.kind === "from" && from && "volume" in from) {
1191
+ volumeKeyframes.push({ time: baseTime + this.at + kf.at, value: from.volume });
1192
+ volumeKeyframes.push({ time: baseTime + this.at + kf.at + kf.duration, value: initialVolume });
1193
+ }
1194
+ }
1195
+ out.push({
1196
+ buffer: this._audioBuffer,
1197
+ loop: !!this.spec.loop,
1198
+ start: baseTime + this.at,
1199
+ end: baseTime + this.at + dur,
1200
+ initialVolume,
1201
+ volumeKeyframes
1202
+ });
1203
+ }
1204
+ destroy() {
1205
+ this._audioBuffer = null;
1206
+ }
1207
+ };
1208
+ }
1209
+ });
1210
+
1211
+ // src/core/FrameCache.ts
1212
+ var FrameCache;
1213
+ var init_FrameCache = __esm({
1214
+ "src/core/FrameCache.ts"() {
1215
+ FrameCache = class {
1216
+ sink;
1217
+ capacity;
1218
+ cache = /* @__PURE__ */ new Map();
1219
+ _pending = /* @__PURE__ */ new Map();
1220
+ constructor(sink, options = {}) {
1221
+ this.sink = sink;
1222
+ this.capacity = options.capacity ?? 30;
1223
+ }
1224
+ _key(time) {
1225
+ return Math.round(time * 1e3);
1226
+ }
1227
+ async getFrameAt(time) {
1228
+ const key = this._key(time);
1229
+ if (this.cache.has(key)) {
1230
+ const v = this.cache.get(key);
1231
+ this.cache.delete(key);
1232
+ this.cache.set(key, v);
1233
+ return v;
1234
+ }
1235
+ const pending = this._pending.get(key);
1236
+ if (pending) return pending;
1237
+ const promise = this._fetch(time);
1238
+ this._pending.set(key, promise);
1239
+ try {
1240
+ const frame = await promise;
1241
+ if (frame) {
1242
+ if (this.cache.has(key)) {
1243
+ frame.close?.();
1244
+ const existing = this.cache.get(key);
1245
+ this.cache.delete(key);
1246
+ this.cache.set(key, existing);
1247
+ return existing;
1248
+ }
1249
+ this.cache.set(key, frame);
1250
+ this._evictIfNeeded();
1251
+ }
1252
+ return frame;
1253
+ } finally {
1254
+ this._pending.delete(key);
1255
+ }
1256
+ }
1257
+ async _fetch(time) {
1258
+ try {
1259
+ const sample = await this.sink.getSample(time);
1260
+ if (!sample) return null;
1261
+ const frame = sample.toVideoFrame();
1262
+ sample.close?.();
1263
+ return frame;
1264
+ } catch (err) {
1265
+ const isTransient = err instanceof DOMException && (err.name === "EncodingError" || err.name === "NotSupportedError");
1266
+ if (isTransient) {
1267
+ console.debug("pixi-effects: transient video decode failure at", time, "s \u2014", err.name);
1268
+ } else {
1269
+ console.warn("pixi-effects: video frame decode failed at", time, "s \u2014", err);
1270
+ }
1271
+ return null;
1272
+ }
1273
+ }
1274
+ _evictIfNeeded() {
1275
+ while (this.cache.size > this.capacity) {
1276
+ const oldestKey = this.cache.keys().next().value;
1277
+ if (oldestKey === void 0) break;
1278
+ const oldestFrame = this.cache.get(oldestKey);
1279
+ oldestFrame?.close?.();
1280
+ this.cache.delete(oldestKey);
1281
+ }
1282
+ }
1283
+ dispose() {
1284
+ for (const f of this.cache.values()) f?.close?.();
1285
+ this.cache.clear();
1286
+ this._pending.clear();
1287
+ }
1288
+ };
1289
+ }
1290
+ });
1291
+ var VideoSequence;
1292
+ var init_Video = __esm({
1293
+ "src/sequences/Video.ts"() {
1294
+ init_Base();
1295
+ init_FrameCache();
1296
+ VideoSequence = class extends Sequence {
1297
+ _sourceDuration = 0;
1298
+ _audioBuffer = null;
1299
+ _cache = null;
1300
+ _canvas = null;
1301
+ _ctx = null;
1302
+ _drawSeq = 0;
1303
+ async build() {
1304
+ const data = await PIXI.Assets.get(this.spec.asset);
1305
+ this._sourceDuration = data.duration;
1306
+ this._audioBuffer = this.spec.audio !== false ? data.audioBuffer : null;
1307
+ this._cache = new FrameCache(data.sink, { capacity: 30 });
1308
+ const probeFrame = await this._cache.getFrameAt(0);
1309
+ const w = probeFrame?.displayWidth ?? 1920;
1310
+ const h = probeFrame?.displayHeight ?? 1080;
1311
+ const canvas = document.createElement("canvas");
1312
+ canvas.width = w;
1313
+ canvas.height = h;
1314
+ this._canvas = canvas;
1315
+ this._ctx = canvas.getContext("2d", { alpha: true });
1316
+ if (probeFrame && this._ctx) this._ctx.drawImage(probeFrame, 0, 0);
1317
+ const texture = PIXI.Texture.from(canvas);
1318
+ const sprite = new PIXI.Sprite({ texture, label: this.spec.name });
1319
+ sprite.cullable = true;
1320
+ this.target = sprite;
1321
+ this.intrinsicWidth = w;
1322
+ this.intrinsicHeight = h;
1323
+ if (this.duration === void 0) {
1324
+ this.duration = this.spec.duration ?? data.duration ?? this.root.duration;
1325
+ }
1326
+ let currentTime = 0;
1327
+ const seq = this;
1328
+ Object.defineProperty(sprite, "currentTime", {
1329
+ get() {
1330
+ return currentTime;
1331
+ },
1332
+ set(v) {
1333
+ currentTime = v;
1334
+ const mySeq = ++seq._drawSeq;
1335
+ const lookup = seq.spec.loop && seq._sourceDuration > 0 ? v % seq._sourceDuration : v;
1336
+ seq._cache.getFrameAt(lookup).then((frame) => {
1337
+ if (mySeq !== seq._drawSeq) return;
1338
+ if (!frame || !seq._ctx) return;
1339
+ seq._ctx.drawImage(frame, 0, 0);
1340
+ texture.source.update();
1341
+ }).catch((err) => {
1342
+ console.warn("pixi-effects: video frame draw failed:", err);
1343
+ });
1344
+ }
1345
+ });
1346
+ this.buildFilters();
1347
+ }
1348
+ bindTimeline(timeline) {
1349
+ super.bindTimeline(timeline);
1350
+ const playDuration = this.spec.loop ? this.duration : Math.min(this.duration, this._sourceDuration);
1351
+ timeline.fromTo(
1352
+ this.target,
1353
+ { currentTime: 0 },
1354
+ { currentTime: playDuration, duration: playDuration, ease: "none" },
1355
+ this.at
1356
+ );
1357
+ }
1358
+ collectAudio(out, baseTime) {
1359
+ if (!this._audioBuffer) return;
1360
+ const initialVolume = this.spec.volume ?? 1;
1361
+ out.push({
1362
+ buffer: this._audioBuffer,
1363
+ loop: !!this.spec.loop,
1364
+ start: baseTime + this.at,
1365
+ end: baseTime + this.at + this.duration,
1366
+ initialVolume,
1367
+ volumeKeyframes: []
1368
+ });
1369
+ }
1370
+ async awaitFrameAt(time) {
1371
+ const mySeq = ++this._drawSeq;
1372
+ const lookup = this.spec.loop && this._sourceDuration > 0 ? time % this._sourceDuration : time;
1373
+ let frame = null;
1374
+ try {
1375
+ frame = await this._cache.getFrameAt(lookup);
1376
+ } catch (err) {
1377
+ console.warn("pixi-effects: awaitFrameAt failed at", time, "s \u2014", err);
1378
+ return;
1379
+ }
1380
+ if (mySeq !== this._drawSeq) return;
1381
+ if (frame && this._ctx) {
1382
+ try {
1383
+ this._ctx.drawImage(frame, 0, 0);
1384
+ this.target.texture.source.update();
1385
+ } catch (err) {
1386
+ console.warn("pixi-effects: drawImage failed at", time, "s \u2014", err);
1387
+ }
1388
+ }
1389
+ }
1390
+ destroy() {
1391
+ this._cache?.dispose();
1392
+ this._cache = null;
1393
+ super.destroy();
1394
+ }
1395
+ };
1396
+ }
1397
+ });
1398
+ function seedGeometry(state, spec, scope) {
1399
+ const keys = GEOMETRY_KEYS_BY_SHAPE[spec.shape];
1400
+ for (const k of keys) {
1401
+ const v = spec[k];
1402
+ if (v !== void 0) state[k] = num(v, scope);
1403
+ }
1404
+ }
1405
+ function seedStyle(state, initial, scope) {
1406
+ for (const k of STYLE_KEYS) {
1407
+ const v = initial[k];
1408
+ if (v === void 0) continue;
1409
+ state[k] = COLOR_KEYS.has(k) ? v : numOrZero(v, scope);
1410
+ }
1411
+ }
1412
+ function stripLive(props, liveKeys) {
1413
+ if (!props) return props;
1414
+ const out = {};
1415
+ let changed = false;
1416
+ for (const k of Object.keys(props)) {
1417
+ if (liveKeys.has(k)) {
1418
+ changed = true;
1419
+ continue;
1420
+ }
1421
+ out[k] = props[k];
1422
+ }
1423
+ return changed ? out : props;
1424
+ }
1425
+ function stripLiveKeyframe(kf, liveKeys) {
1426
+ return {
1427
+ ...kf,
1428
+ set: stripLive(kf.set, liveKeys),
1429
+ to: stripLive(kf.to, liveKeys),
1430
+ from: stripLive(kf.from, liveKeys)
1431
+ };
1432
+ }
1433
+ function pickLive(props, liveKeys) {
1434
+ if (!props) return null;
1435
+ const out = {};
1436
+ let any = false;
1437
+ for (const k of Object.keys(props)) {
1438
+ if (liveKeys.has(k)) {
1439
+ out[k] = props[k];
1440
+ any = true;
1441
+ }
1442
+ }
1443
+ return any ? out : null;
1444
+ }
1445
+ function resolveLiveValue(key, value, scope) {
1446
+ if (COLOR_KEYS.has(key)) return value;
1447
+ return numOrZero(value, scope);
1448
+ }
1449
+ function applyState(g, s) {
1450
+ if (s.fillColor !== void 0) {
1451
+ g.fill({ color: s.fillColor, alpha: s.fillAlpha });
1452
+ }
1453
+ if (s.strokeColor !== void 0 && s.strokeWidth > 0) {
1454
+ g.stroke({ color: s.strokeColor, alpha: s.strokeAlpha, width: s.strokeWidth });
1455
+ }
1456
+ }
1457
+ function drawRectFromState(g, s) {
1458
+ const w = s.width ?? 0;
1459
+ const h = s.height ?? 0;
1460
+ const r = s.cornerRadius ?? 0;
1461
+ const ax = s.anchorX ?? 0.5;
1462
+ const ay = s.anchorY ?? 0.5;
1463
+ const x = 0 - ax * w;
1464
+ const y = 0 - ay * h;
1465
+ if (r > 0) g.roundRect(x, y, w, h, r);
1466
+ else g.rect(x, y, w, h);
1467
+ }
1468
+ function drawCircleFromState(g, s) {
1469
+ const r = s.radius ?? 0;
1470
+ const ax = s.anchorX ?? 0.5;
1471
+ const ay = s.anchorY ?? 0.5;
1472
+ g.circle((0.5 - ax) * 2 * r, (0.5 - ay) * 2 * r, r);
1473
+ }
1474
+ function drawEllipseFromState(g, s) {
1475
+ const rx = s.radiusX ?? 0;
1476
+ const ry = s.radiusY ?? 0;
1477
+ const ax = s.anchorX ?? 0.5;
1478
+ const ay = s.anchorY ?? 0.5;
1479
+ g.ellipse((0.5 - ax) * 2 * rx, (0.5 - ay) * 2 * ry, rx, ry);
1480
+ }
1481
+ function makeLineDraw(spec, scope) {
1482
+ const fx = num(spec.from[0], scope);
1483
+ const fy = num(spec.from[1], scope);
1484
+ const tx = num(spec.to[0], scope);
1485
+ const ty = num(spec.to[1], scope);
1486
+ return (g) => {
1487
+ g.moveTo(fx, fy);
1488
+ g.lineTo(tx, ty);
1489
+ };
1490
+ }
1491
+ function makePolygonDraw(spec, scope) {
1492
+ if (spec.points.length === 0) return () => {
1493
+ };
1494
+ const flat = [];
1495
+ for (const [x, y] of spec.points) flat.push(num(x, scope), num(y, scope));
1496
+ const closed = !spec.open;
1497
+ return (g) => {
1498
+ g.poly(flat, closed);
1499
+ };
1500
+ }
1501
+ function makePathDraw(spec) {
1502
+ return (g) => {
1503
+ g.path(new PIXI.GraphicsPath(spec.d));
1504
+ };
1505
+ }
1506
+ function makeDrawGeometry(spec, scope, state) {
1507
+ const stateDrawer = STATE_DRAWERS[spec.shape];
1508
+ if (stateDrawer) return (g) => stateDrawer(g, state);
1509
+ switch (spec.shape) {
1510
+ case "line":
1511
+ return makeLineDraw(spec, scope);
1512
+ case "polygon":
1513
+ return makePolygonDraw(spec, scope);
1514
+ case "path":
1515
+ return makePathDraw(spec);
1516
+ }
1517
+ return () => {
1518
+ };
1519
+ }
1520
+ function bindLiveKeyframes(timeline, state, liveKeys, keyframes, parentDuration, scope, offset, colorSpace) {
1521
+ for (const kf of keyframes) {
1522
+ const at = offset + resolveAt(kf.at, parentDuration);
1523
+ const duration = kf.duration ?? 0;
1524
+ const ease = kf.ease ?? "none";
1525
+ if (kf.set) {
1526
+ const live = pickLive(kf.set, liveKeys);
1527
+ if (live) {
1528
+ for (const [k, v] of Object.entries(live)) {
1529
+ const resolved = resolveLiveValue(k, v, scope);
1530
+ timeline.call(() => {
1531
+ state[k] = resolved;
1532
+ }, [], at);
1533
+ }
1534
+ }
1535
+ }
1536
+ const fromLive = pickLive(kf.from, liveKeys);
1537
+ const toLive = pickLive(kf.to, liveKeys);
1538
+ const allKeys = /* @__PURE__ */ new Set([
1539
+ ...fromLive ? Object.keys(fromLive) : [],
1540
+ ...toLive ? Object.keys(toLive) : []
1541
+ ]);
1542
+ for (const k of allKeys) {
1543
+ const key = k;
1544
+ const fromRaw = fromLive?.[key];
1545
+ const toRaw = toLive?.[key];
1546
+ const fromValue = fromRaw !== void 0 ? resolveLiveValue(key, fromRaw, scope) : void 0;
1547
+ const toValue = toRaw !== void 0 ? resolveLiveValue(key, toRaw, scope) : void 0;
1548
+ tweenLiveKey(timeline, state, key, fromValue, toValue, duration, ease, at, colorSpace);
1549
+ }
1550
+ }
1551
+ }
1552
+ function tweenLiveKey(timeline, state, key, fromValue, toValue, duration, ease, at, colorSpace) {
1553
+ if (COLOR_KEYS.has(key) && toValue !== void 0) {
1554
+ tweenColor(
1555
+ timeline,
1556
+ state,
1557
+ key,
1558
+ fromValue,
1559
+ toValue,
1560
+ duration,
1561
+ ease,
1562
+ at,
1563
+ colorSpace
1564
+ );
1565
+ } else if (toValue !== void 0) {
1566
+ if (fromValue !== void 0) {
1567
+ timeline.fromTo(state, { [key]: fromValue }, { [key]: toValue, duration, ease }, at);
1568
+ } else {
1569
+ timeline.to(state, { [key]: toValue, duration, ease }, at);
1570
+ }
1571
+ }
1572
+ }
1573
+ function num(v, scope) {
1574
+ if (typeof v === "number") return v;
1575
+ if (isExpr(v)) return evaluateExpr(v, scope);
1576
+ const parsed = parseFloat(v);
1577
+ return Number.isNaN(parsed) ? 0 : parsed;
1578
+ }
1579
+ function numOrZero(v, scope) {
1580
+ if (typeof v === "number") return v;
1581
+ if (typeof v === "string") return num(v, scope);
1582
+ return 0;
1583
+ }
1584
+ var STYLE_KEYS, COLOR_KEYS, GEOMETRY_KEYS_BY_SHAPE, ANCHORED_SHAPES, ShapeSequence, STATE_DRAWERS;
1585
+ var init_Shape = __esm({
1586
+ "src/sequences/Shape.ts"() {
1587
+ init_Base();
1588
+ init_Parser();
1589
+ init_Timeline();
1590
+ init_colorTween();
1591
+ STYLE_KEYS = ["fillColor", "fillAlpha", "strokeColor", "strokeAlpha", "strokeWidth"];
1592
+ COLOR_KEYS = /* @__PURE__ */ new Set(["fillColor", "strokeColor"]);
1593
+ GEOMETRY_KEYS_BY_SHAPE = {
1594
+ rect: ["width", "height", "cornerRadius", "anchorX", "anchorY"],
1595
+ circle: ["radius", "anchorX", "anchorY"],
1596
+ ellipse: ["radiusX", "radiusY", "anchorX", "anchorY"],
1597
+ line: [],
1598
+ polygon: [],
1599
+ path: []
1600
+ };
1601
+ ANCHORED_SHAPES = /* @__PURE__ */ new Set(["rect", "circle", "ellipse"]);
1602
+ ShapeSequence = class extends Sequence {
1603
+ _state = {
1604
+ fillColor: void 0,
1605
+ fillAlpha: 1,
1606
+ strokeColor: void 0,
1607
+ strokeAlpha: 1,
1608
+ strokeWidth: 0
1609
+ };
1610
+ // Closure that draws the path commands for this shape kind from the
1611
+ // (possibly tweened) `_state`. Bound once at build; called on every frame
1612
+ // by `Container.onRender`.
1613
+ _drawGeometry = () => {
1614
+ };
1615
+ _liveKeys = new Set(STYLE_KEYS);
1616
+ async build() {
1617
+ const graphics = new PIXI.Graphics({ label: this.spec.name });
1618
+ graphics.cullable = true;
1619
+ this.target = graphics;
1620
+ if (this.duration === void 0) {
1621
+ this.duration = this.parent?.duration ?? this.root.duration;
1622
+ }
1623
+ const scope = this.scope();
1624
+ const geomKeys = GEOMETRY_KEYS_BY_SHAPE[this.spec.shape];
1625
+ for (const k of geomKeys) this._liveKeys.add(k);
1626
+ seedGeometry(this._state, this.spec, scope);
1627
+ seedStyle(this._state, this.spec.initial ?? {}, scope);
1628
+ this._drawGeometry = makeDrawGeometry(this.spec, scope, this._state);
1629
+ this._drawGeometry(graphics);
1630
+ applyState(graphics, this._state);
1631
+ const bounds = graphics.getLocalBounds();
1632
+ this.intrinsicWidth = bounds.width;
1633
+ this.intrinsicHeight = bounds.height;
1634
+ if (!ANCHORED_SHAPES.has(this.spec.shape)) {
1635
+ graphics.pivot.set(bounds.x + bounds.width / 2, bounds.y + bounds.height / 2);
1636
+ }
1637
+ graphics.onRender = () => {
1638
+ graphics.clear();
1639
+ this._drawGeometry(graphics);
1640
+ applyState(graphics, this._state);
1641
+ };
1642
+ this.buildFilters();
1643
+ }
1644
+ bindTimeline(timeline, offset = 0) {
1645
+ if (!this.target) return;
1646
+ const scope = this.scope();
1647
+ const initial = stripLive(this.spec.initial, this._liveKeys);
1648
+ const keyframes = this.spec.keyframes ? this.spec.keyframes.map((kf) => stripLiveKeyframe(kf, this._liveKeys)) : void 0;
1649
+ applyInitial(this.target, initial, scope);
1650
+ applyKeyframes(timeline, this.target, keyframes, this.duration, scope, [], offset);
1651
+ const colorSpace = this.spec.colorSpace ?? "rgb";
1652
+ bindLiveKeyframes(timeline, this._state, this._liveKeys, this.spec.keyframes ?? [], this.duration, scope, offset, colorSpace);
1653
+ const startTime = offset + this.at;
1654
+ const endTime = startTime + this.duration;
1655
+ this.target.renderable = startTime <= 0;
1656
+ timeline.set(this.target, { renderable: true }, startTime);
1657
+ timeline.set(this.target, { renderable: false }, endTime);
1658
+ }
1659
+ };
1660
+ STATE_DRAWERS = {
1661
+ rect: drawRectFromState,
1662
+ circle: drawCircleFromState,
1663
+ ellipse: drawEllipseFromState
1664
+ };
1665
+ }
1666
+ });
1667
+
1668
+ // src/core/Composition.ts
1669
+ async function getCompositionSequence() {
1670
+ if (!_CompositionSequence) {
1671
+ _CompositionSequence = (await Promise.resolve().then(() => (init_Composition2(), Composition_exports))).CompositionSequence;
1672
+ }
1673
+ return _CompositionSequence;
1674
+ }
1675
+ async function buildSequenceTree(specs, parent, root) {
1676
+ const out = [];
1677
+ for (const spec of specs) {
1678
+ let Cls = staticTypes[spec.type];
1679
+ if (!Cls && spec.type === "composition") {
1680
+ Cls = await getCompositionSequence();
1681
+ }
1682
+ if (!Cls) {
1683
+ console.warn(`pixi-effects: unknown sequence type "${spec.type}"`);
1684
+ continue;
1685
+ }
1686
+ const seq = new Cls(spec, parent, root);
1687
+ await seq.build();
1688
+ if (seq.duration === void 0) {
1689
+ seq.duration = parent?.duration ?? root.duration;
1690
+ }
1691
+ out.push(seq);
1692
+ }
1693
+ return out;
1694
+ }
1695
+ var _CompositionSequence, staticTypes;
1696
+ var init_Composition = __esm({
1697
+ "src/core/Composition.ts"() {
1698
+ init_Image();
1699
+ init_Text();
1700
+ init_Audio();
1701
+ init_Video();
1702
+ init_Shape();
1703
+ _CompositionSequence = null;
1704
+ staticTypes = {
1705
+ image: ImageSequence,
1706
+ text: TextSequence,
1707
+ audio: AudioSequence,
1708
+ video: VideoSequence,
1709
+ shape: ShapeSequence
1710
+ };
1711
+ }
1712
+ });
1713
+
1714
+ // src/sequences/Composition.ts
1715
+ var Composition_exports = {};
1716
+ __export(Composition_exports, {
1717
+ CompositionSequence: () => CompositionSequence
1718
+ });
1719
+ var CompositionSequence;
1720
+ var init_Composition2 = __esm({
1721
+ "src/sequences/Composition.ts"() {
1722
+ init_Base();
1723
+ init_Composition();
1724
+ CompositionSequence = class extends Sequence {
1725
+ _innerContainer = null;
1726
+ _compositionShape = null;
1727
+ _children = [];
1728
+ async build() {
1729
+ const width = this.spec.width ?? this.parent?.width ?? this.root.width;
1730
+ const height = this.spec.height ?? this.parent?.height ?? this.root.height;
1731
+ if (this.duration === void 0) {
1732
+ this.duration = this.spec.duration ?? this.parent?.duration ?? this.root.duration;
1733
+ }
1734
+ const wrapper = new PIXI.Container();
1735
+ wrapper.cullable = true;
1736
+ wrapper.cullableChildren = true;
1737
+ wrapper.cullArea = new PIXI.Rectangle(0, 0, width, height);
1738
+ const inner = new PIXI.Container();
1739
+ inner.cullable = true;
1740
+ inner.cullableChildren = true;
1741
+ wrapper.addChild(inner);
1742
+ this.target = wrapper;
1743
+ this.intrinsicWidth = width;
1744
+ this.intrinsicHeight = height;
1745
+ this._innerContainer = inner;
1746
+ this._compositionShape = { width, height, duration: this.duration };
1747
+ this._children = await buildSequenceTree(
1748
+ this.spec.sequences ?? [],
1749
+ this._compositionShape,
1750
+ this.root
1751
+ );
1752
+ for (const child of this._children) {
1753
+ if (child.target) inner.addChild(child.target);
1754
+ const maskSpec = child.spec.mask;
1755
+ if (maskSpec && child.target) {
1756
+ const built = await buildSequenceTree([maskSpec], this._compositionShape, this.root);
1757
+ const maskSeq = built[0];
1758
+ if (maskSeq?.target) {
1759
+ inner.addChild(maskSeq.target);
1760
+ const inverse = child.spec.maskInverted ?? false;
1761
+ const t = child.target;
1762
+ if (typeof t.setMask === "function") {
1763
+ t.setMask({ mask: maskSeq.target, inverse });
1764
+ } else {
1765
+ t.mask = maskSeq.target;
1766
+ }
1767
+ child.maskSequence = maskSeq;
1768
+ }
1769
+ }
1770
+ }
1771
+ this.buildFilters();
1772
+ }
1773
+ bindTimeline(timeline, offset = 0) {
1774
+ super.bindTimeline(timeline, offset);
1775
+ const childOffset = offset + this.at;
1776
+ for (const child of this._children) {
1777
+ child.bindTimeline(timeline, childOffset);
1778
+ child.maskSequence?.bindTimeline(timeline, childOffset);
1779
+ }
1780
+ }
1781
+ collectAudio(out, baseTime) {
1782
+ super.collectAudio(out, baseTime);
1783
+ const childBase = baseTime + this.at;
1784
+ for (const child of this._children) child.collectAudio(out, childBase);
1785
+ }
1786
+ destroy() {
1787
+ for (const c of this._children) c.destroy();
1788
+ super.destroy();
1789
+ }
1790
+ };
1791
+ }
1792
+ });
1793
+ var registered = false;
1794
+ function ensureLoadersRegistered(audioContext) {
1795
+ if (registered) return;
1796
+ registered = true;
1797
+ PIXI.extensions.add({
1798
+ extension: { type: PIXI.ExtensionType.LoadParser, name: "pixi-effects-audio" },
1799
+ test: (url) => /\.(mp3|wav|ogg)$/i.test(url),
1800
+ async load(url) {
1801
+ const res = await fetch(url);
1802
+ if (!res.ok) throw new Error(`pixi-effects: failed to load ${url}: ${res.statusText}`);
1803
+ const buffer = await res.arrayBuffer();
1804
+ const audioBuffer = await audioContext.decodeAudioData(buffer);
1805
+ return { audioBuffer, duration: audioBuffer.duration };
1806
+ }
1807
+ });
1808
+ PIXI.extensions.add({
1809
+ extension: { type: PIXI.ExtensionType.LoadParser, name: "pixi-effects-video", priority: 1 },
1810
+ test: (url) => /\.(mp4|webm|mov|mkv)$/i.test(url),
1811
+ async load(url) {
1812
+ const blob = await fetch(url).then((r) => r.blob());
1813
+ const input = new mediabunny.Input({ source: new mediabunny.BlobSource(blob), formats: mediabunny.ALL_FORMATS });
1814
+ const videoTrack = await input.getPrimaryVideoTrack();
1815
+ if (!videoTrack || !await videoTrack.canDecode()) {
1816
+ throw new Error("pixi-effects: video track cannot be decoded");
1817
+ }
1818
+ const audioTrack = await input.getPrimaryAudioTrack();
1819
+ const videoDuration = await videoTrack.computeDuration();
1820
+ const audioDuration = audioTrack ? await audioTrack.computeDuration() : 0;
1821
+ let audioBuffer = null;
1822
+ if (audioTrack) {
1823
+ try {
1824
+ const out = new mediabunny.Output({ format: new mediabunny.WavOutputFormat(), target: new mediabunny.BufferTarget() });
1825
+ const conv = await mediabunny.Conversion.init({ input, output: out, video: { discard: true } });
1826
+ await conv.execute();
1827
+ audioBuffer = await audioContext.decodeAudioData(out.target.buffer);
1828
+ } catch (e) {
1829
+ console.warn("pixi-effects: audio extraction failed:", e);
1830
+ }
1831
+ }
1832
+ return {
1833
+ videoTrack,
1834
+ audioBuffer,
1835
+ videoDuration,
1836
+ audioDuration,
1837
+ duration: Math.max(videoDuration, audioDuration),
1838
+ sink: new mediabunny.VideoSampleSink(videoTrack)
1839
+ };
1840
+ }
1841
+ });
1842
+ }
1843
+ async function loadAssetBundle(assets, audioContext) {
1844
+ ensureLoadersRegistered(audioContext);
1845
+ const bundle = assets.map((a) => ({ alias: a.name, src: a.src }));
1846
+ PIXI.Assets.addBundle("pixi-effects", bundle);
1847
+ return await PIXI.Assets.loadBundle("pixi-effects");
1848
+ }
1849
+
1850
+ // src/core/Movie.ts
1851
+ init_Composition2();
1852
+
1853
+ // src/core/AudioMixer.ts
1854
+ async function mixdown(audios, totalDuration, sampleRate = 44100) {
1855
+ if (audios.length === 0) return null;
1856
+ const ctx = new OfflineAudioContext(2, Math.ceil(sampleRate * totalDuration), sampleRate);
1857
+ for (const a of audios) {
1858
+ const src = ctx.createBufferSource();
1859
+ src.buffer = a.buffer;
1860
+ src.loop = !!a.loop;
1861
+ const gain = ctx.createGain();
1862
+ gain.gain.setValueAtTime(a.initialVolume ?? 1, a.start);
1863
+ for (const kf of a.volumeKeyframes ?? []) {
1864
+ gain.gain.linearRampToValueAtTime(kf.value, kf.time);
1865
+ }
1866
+ src.connect(gain).connect(ctx.destination);
1867
+ src.start(a.start);
1868
+ src.stop(a.end);
1869
+ }
1870
+ return await ctx.startRendering();
1871
+ }
1872
+ var VIDEO_CODEC_BY_FORMAT = {
1873
+ mp4: "avc",
1874
+ mov: "avc",
1875
+ webm: "vp9",
1876
+ mkv: "vp9"
1877
+ };
1878
+ var AUDIO_CODEC_BY_FORMAT = {
1879
+ mp4: "aac",
1880
+ mov: "aac",
1881
+ webm: "opus",
1882
+ mkv: "opus"
1883
+ };
1884
+ var qualityMap = {
1885
+ "very-low": mediabunny.QUALITY_VERY_LOW,
1886
+ "low": mediabunny.QUALITY_LOW,
1887
+ "medium": mediabunny.QUALITY_MEDIUM,
1888
+ "high": mediabunny.QUALITY_HIGH,
1889
+ "very-high": mediabunny.QUALITY_VERY_HIGH
1890
+ };
1891
+ function makeOutputFormat(name) {
1892
+ switch (name) {
1893
+ case "mp4":
1894
+ return new mediabunny.Mp4OutputFormat({ fastStart: "in-memory" });
1895
+ case "mov":
1896
+ return new mediabunny.MovOutputFormat({ fastStart: "in-memory" });
1897
+ case "webm":
1898
+ return new mediabunny.WebMOutputFormat();
1899
+ case "mkv":
1900
+ return new mediabunny.MkvOutputFormat();
1901
+ }
1902
+ }
1903
+ async function exportFrames(movie, options = {}) {
1904
+ const fmt = options.format ?? "mp4";
1905
+ const opts = {
1906
+ format: fmt,
1907
+ video: {
1908
+ codec: options.video?.codec ?? VIDEO_CODEC_BY_FORMAT[fmt],
1909
+ bitrate: qualityMap[options.video?.bitrate ?? "high"] ?? mediabunny.QUALITY_HIGH
1910
+ },
1911
+ audio: {
1912
+ codec: options.audio?.codec ?? AUDIO_CODEC_BY_FORMAT[fmt],
1913
+ bitrate: qualityMap[options.audio?.bitrate ?? "high"] ?? mediabunny.QUALITY_HIGH
1914
+ }
1915
+ };
1916
+ const output = new mediabunny.Output({
1917
+ format: makeOutputFormat(opts.format),
1918
+ target: new mediabunny.BufferTarget()
1919
+ });
1920
+ const canvasSource = new mediabunny.CanvasSource(movie.app.canvas, {
1921
+ codec: opts.video.codec,
1922
+ bitrate: opts.video.bitrate
1923
+ });
1924
+ output.addVideoTrack(canvasSource, { frameRate: movie.frameRate });
1925
+ if (movie.audioBuffer) {
1926
+ const audioSource = new mediabunny.AudioBufferSource({
1927
+ codec: opts.audio.codec,
1928
+ bitrate: opts.audio.bitrate
1929
+ });
1930
+ output.addAudioTrack(audioSource);
1931
+ await output.start();
1932
+ await audioSource.add(movie.audioBuffer);
1933
+ await audioSource.close();
1934
+ } else {
1935
+ await output.start();
1936
+ }
1937
+ movie.app.ticker.stop();
1938
+ const keyframeIntervalFrames = Math.max(1, Math.round(2 * movie.frameRate));
1939
+ try {
1940
+ for (let frame = 0; frame <= movie.totalFrames; frame++) {
1941
+ await movie.gotoFrame(frame, true);
1942
+ const isKey = frame === 0 || frame % keyframeIntervalFrames === 0;
1943
+ const addOpts = isKey ? { keyFrame: true } : void 0;
1944
+ await canvasSource.add(frame / movie.frameRate, 1 / movie.frameRate, addOpts);
1945
+ const progress = Math.floor(frame / movie.totalFrames * 100);
1946
+ movie.emit("progress", { progress, frame, totalFrames: movie.totalFrames });
1947
+ }
1948
+ await canvasSource.close();
1949
+ await output.finalize();
1950
+ return new Blob([output.target.buffer], { type: output.format.mimeType });
1951
+ } finally {
1952
+ movie.app.ticker.start();
1953
+ }
1954
+ }
1955
+
1956
+ // src/core/Transitions.ts
1957
+ init_Timeline();
1958
+ var MODE_CODES = {
1959
+ "wipe-left": 0,
1960
+ "wipe-right": 1,
1961
+ "wipe-up": 2,
1962
+ "wipe-down": 3,
1963
+ "iris-in": 4,
1964
+ "iris-out": 5,
1965
+ "dissolve": 6
1966
+ };
1967
+ var GL_FRAGMENT2 = `
1968
+ in vec2 vTextureCoord;
1969
+ uniform sampler2D uTexture;
1970
+ uniform vec4 uInputSize; // PIXI-bound: xy = sprite render texture size (with padding)
1971
+ uniform vec4 uInputClamp; // PIXI-bound: xy = min uv of unpadded content, zw = max uv
1972
+ uniform vec4 uOutputFrame; // PIXI-bound: xy = sprite top-left on output, zw = bbox size in px
1973
+ uniform vec4 uGlobalFrame; // PIXI-bound: xy = global frame offset, zw = canvas size in px
1974
+ uniform float uProgress;
1975
+ uniform float uSmoothing;
1976
+ uniform float uMode;
1977
+ uniform float uInvert;
1978
+ uniform float uScale;
1979
+ uniform float uSeed;
1980
+
1981
+ out vec4 finalColor;
1982
+
1983
+ // Classic 2D Perlin noise (Stefan Gustavson). Deterministic \u2014 same input
1984
+ // always returns the same output, which is what makes the dissolve pattern
1985
+ // reproducible across renders.
1986
+ vec4 _permute(vec4 x) { return mod(((x * 34.0) + 1.0) * x, 289.0); }
1987
+ vec2 _fade(vec2 t) { return t * t * t * (t * (t * 6.0 - 15.0) + 10.0); }
1988
+ float perlin2(vec2 P) {
1989
+ vec4 Pi = floor(P.xyxy) + vec4(0.0, 0.0, 1.0, 1.0);
1990
+ vec4 Pf = fract(P.xyxy) - vec4(0.0, 0.0, 1.0, 1.0);
1991
+ Pi = mod(Pi, 289.0);
1992
+ vec4 ix = Pi.xzxz;
1993
+ vec4 iy = Pi.yyww;
1994
+ vec4 fx = Pf.xzxz;
1995
+ vec4 fy = Pf.yyww;
1996
+ vec4 i = _permute(_permute(ix) + iy);
1997
+ vec4 gx = 2.0 * fract(i / 41.0) - 1.0;
1998
+ vec4 gy = abs(gx) - 0.5;
1999
+ vec4 tx = floor(gx + 0.5);
2000
+ gx = gx - tx;
2001
+ vec2 g00 = vec2(gx.x, gy.x);
2002
+ vec2 g10 = vec2(gx.y, gy.y);
2003
+ vec2 g01 = vec2(gx.z, gy.z);
2004
+ vec2 g11 = vec2(gx.w, gy.w);
2005
+ vec4 norm = 1.79284291400159 - 0.85373472095314 * vec4(dot(g00, g00), dot(g01, g01), dot(g10, g10), dot(g11, g11));
2006
+ g00 *= norm.x; g01 *= norm.y; g10 *= norm.z; g11 *= norm.w;
2007
+ float n00 = dot(g00, vec2(fx.x, fy.x));
2008
+ float n10 = dot(g10, vec2(fx.y, fy.y));
2009
+ float n01 = dot(g01, vec2(fx.z, fy.z));
2010
+ float n11 = dot(g11, vec2(fx.w, fy.w));
2011
+ vec2 fade_xy = _fade(Pf.xy);
2012
+ vec2 n_x = mix(vec2(n00, n01), vec2(n10, n11), fade_xy.x);
2013
+ return 2.3 * mix(n_x.x, n_x.y, fade_xy.y);
2014
+ }
2015
+
2016
+ // Map sprite-local vTextureCoord \u2192 canvas-pixel position. We do this in two
2017
+ // steps: first un-pad vTextureCoord (it spans 0..bbox/inputSize because the
2018
+ // filter target has padding) into a true 0..1 across the bbox, then offset
2019
+ // and scale by uOutputFrame to get canvas pixels. Operating in canvas space
2020
+ // (rather than sprite-uv space) is what makes the wipe / iris edge land at
2021
+ // the same canvas position for sprites of any size: at uProgress=0.25 the
2022
+ // wipe edge is at canvas x = 0.75 * canvasW for every sprite, regardless of
2023
+ // each sprite's bbox.
2024
+ vec2 toCanvasPx(vec2 uv) {
2025
+ vec2 bboxUV = (uv - uInputClamp.xy) / (uInputClamp.zw - uInputClamp.xy);
2026
+ return uOutputFrame.xy + bboxUV * uOutputFrame.zw;
2027
+ }
2028
+
2029
+ vec2 toCanvasUV(vec2 uv) {
2030
+ return toCanvasPx(uv) / uGlobalFrame.zw;
2031
+ }
2032
+
2033
+ // Aspect-corrected, corner-normalized distance from canvas center.
2034
+ // Returns 0 at canvas center, 1 at the canvas corner \u2014 so iris is a true
2035
+ // circle in pixel space and p=1 reveals the entire canvas.
2036
+ float irisDistCanvas(vec2 uv) {
2037
+ vec2 px = toCanvasPx(uv);
2038
+ vec2 canvasSize = uGlobalFrame.zw;
2039
+ vec2 offsetPx = px - canvasSize * 0.5;
2040
+ float halfDiag = 0.5 * length(canvasSize);
2041
+ return length(offsetPx) / max(halfDiag, 1.0);
2042
+ }
2043
+
2044
+ float wipeReveal(vec2 uv, float p, float s, float mode) {
2045
+ // Remap p so the smoothstep edge lies fully outside [0,1] at p=0 / p=1
2046
+ // (otherwise smoothstep returns 0.5 at the very edge instead of 0 or 1).
2047
+ float ep = p * (1.0 + 2.0 * s) - s;
2048
+ vec2 cuv = toCanvasUV(uv);
2049
+ if (mode < 0.5) {
2050
+ // wipe-left: B reveals from the right edge moving left
2051
+ return smoothstep(1.0 - ep - s, 1.0 - ep + s, cuv.x);
2052
+ } else if (mode < 1.5) {
2053
+ // wipe-right: B reveals from the left edge moving right
2054
+ return smoothstep(1.0 - ep - s, 1.0 - ep + s, 1.0 - cuv.x);
2055
+ } else if (mode < 2.5) {
2056
+ // wipe-up
2057
+ return smoothstep(1.0 - ep - s, 1.0 - ep + s, cuv.y);
2058
+ } else if (mode < 3.5) {
2059
+ // wipe-down: B reveals from the top edge moving down
2060
+ return smoothstep(1.0 - ep - s, 1.0 - ep + s, 1.0 - cuv.y);
2061
+ } else if (mode < 4.5) {
2062
+ // iris-in: circle grows from center
2063
+ float d = irisDistCanvas(uv);
2064
+ return 1.0 - smoothstep(ep - s, ep + s, d);
2065
+ } else if (mode < 5.5) {
2066
+ // iris-out: circle shrinks toward center
2067
+ float d = irisDistCanvas(uv);
2068
+ return smoothstep(ep - s, ep + s, d);
2069
+ } else {
2070
+ // dissolve: pixel-grain Perlin noise threshold. Pixels with a low noise
2071
+ // value reveal first; as p grows, more pixels reveal.
2072
+ vec2 sampleUV = cuv * uScale + vec2(uSeed, uSeed * 0.7);
2073
+ float n = perlin2(sampleUV) * 0.5 + 0.5; // remap to 0..1
2074
+ return 1.0 - smoothstep(ep - s, ep + s, n);
2075
+ }
2076
+ }
2077
+
2078
+ void main(void) {
2079
+ vec4 raw = texture(uTexture, vTextureCoord);
2080
+ float reveal = wipeReveal(vTextureCoord, uProgress, max(uSmoothing, 0.0001), uMode);
2081
+ // Inverted mode uses a HARD binary cutoff: A stays at alpha=1 across
2082
+ // B's smoothstep zone and snaps off only where B has fully revealed.
2083
+ if (uInvert > 0.5) reveal = 1.0 - step(0.99, reveal);
2084
+ finalColor = raw * reveal;
2085
+ }
2086
+ `;
2087
+ var WGSL_SOURCE2 = `
2088
+ struct GlobalFilterUniforms {
2089
+ uInputSize: vec4<f32>,
2090
+ uInputPixel: vec4<f32>,
2091
+ uInputClamp: vec4<f32>,
2092
+ uOutputFrame: vec4<f32>,
2093
+ uGlobalFrame: vec4<f32>,
2094
+ uOutputTexture: vec4<f32>,
2095
+ };
2096
+
2097
+ struct TransitionUniforms {
2098
+ uProgress: f32,
2099
+ uMode: f32,
2100
+ uSmoothing: f32,
2101
+ uInvert: f32,
2102
+ uScale: f32,
2103
+ uSeed: f32,
2104
+ };
2105
+
2106
+ @group(0) @binding(0) var<uniform> gfu: GlobalFilterUniforms;
2107
+ @group(0) @binding(1) var uTexture: texture_2d<f32>;
2108
+ @group(0) @binding(2) var uSampler : sampler;
2109
+ @group(1) @binding(0) var<uniform> transitionUniforms : TransitionUniforms;
2110
+
2111
+ struct VSOutput {
2112
+ @builtin(position) position: vec4<f32>,
2113
+ @location(0) uv : vec2<f32>,
2114
+ };
2115
+
2116
+ fn filterVertexPosition(aPosition : vec2<f32>) -> vec4<f32> {
2117
+ var position = aPosition * gfu.uOutputFrame.zw + gfu.uOutputFrame.xy;
2118
+ position.x = position.x * (2.0 / gfu.uOutputTexture.x) - 1.0;
2119
+ position.y = position.y * (2.0 * gfu.uOutputTexture.z / gfu.uOutputTexture.y) - gfu.uOutputTexture.z;
2120
+ return vec4<f32>(position, 0.0, 1.0);
2121
+ }
2122
+
2123
+ fn filterTextureCoord(aPosition : vec2<f32>) -> vec2<f32> {
2124
+ return aPosition * (gfu.uOutputFrame.zw * gfu.uInputSize.zw);
2125
+ }
2126
+
2127
+ // NOTE on whitespace: PIXI v8's WGSL attribute extractor is regex-based and
2128
+ // expects the noise/blur reference format \u2014 multi-line param list + space
2129
+ // before the colon. A compact one-liner with no space before the colon parses
2130
+ // as having NO vertex attributes, which then causes a "Vertex attribute slot 0
2131
+ // not present in the VertexState" pipeline validation error.
2132
+ @vertex
2133
+ fn mainVertex(
2134
+ @location(0) aPosition : vec2<f32>,
2135
+ ) -> VSOutput {
2136
+ return VSOutput(filterVertexPosition(aPosition), filterTextureCoord(aPosition));
2137
+ }
2138
+
2139
+ // Aspect-corrected, corner-normalized distance from sprite center.
2140
+ // Map sprite-local uv \u2192 canvas-pixel position (see GLSL note).
2141
+ fn toCanvasPx(uv: vec2<f32>) -> vec2<f32> {
2142
+ let bboxUV = (uv - gfu.uInputClamp.xy) / (gfu.uInputClamp.zw - gfu.uInputClamp.xy);
2143
+ return gfu.uOutputFrame.xy + bboxUV * gfu.uOutputFrame.zw;
2144
+ }
2145
+
2146
+ fn toCanvasUV(uv: vec2<f32>) -> vec2<f32> {
2147
+ return toCanvasPx(uv) / gfu.uGlobalFrame.zw;
2148
+ }
2149
+
2150
+ fn irisDistCanvas(uv: vec2<f32>) -> f32 {
2151
+ let px = toCanvasPx(uv);
2152
+ let canvasSize = gfu.uGlobalFrame.zw;
2153
+ let offsetPx = px - canvasSize * 0.5;
2154
+ let halfDiag = 0.5 * length(canvasSize);
2155
+ return length(offsetPx) / max(halfDiag, 1.0);
2156
+ }
2157
+
2158
+ // Classic 2D Perlin noise (Stefan Gustavson). Deterministic \u2014 same input
2159
+ // always returns the same output, so the dissolve pattern is reproducible.
2160
+ fn _wgsl_permute(x: vec4<f32>) -> vec4<f32> { return ((x * 34.0) + 1.0) * x % vec4<f32>(289.0); }
2161
+ fn _wgsl_fade(t: vec2<f32>) -> vec2<f32> { return t * t * t * (t * (t * 6.0 - 15.0) + 10.0); }
2162
+ fn perlin2(P: vec2<f32>) -> f32 {
2163
+ var Pi: vec4<f32> = floor(vec4<f32>(P, P)) + vec4<f32>(0.0, 0.0, 1.0, 1.0);
2164
+ var Pf: vec4<f32> = fract(vec4<f32>(P, P)) - vec4<f32>(0.0, 0.0, 1.0, 1.0);
2165
+ Pi = Pi % vec4<f32>(289.0);
2166
+ let ix = vec4<f32>(Pi.x, Pi.z, Pi.x, Pi.z);
2167
+ let iy = vec4<f32>(Pi.y, Pi.y, Pi.w, Pi.w);
2168
+ let fx = vec4<f32>(Pf.x, Pf.z, Pf.x, Pf.z);
2169
+ let fy = vec4<f32>(Pf.y, Pf.y, Pf.w, Pf.w);
2170
+ let i = _wgsl_permute(_wgsl_permute(ix) + iy);
2171
+ var gx = 2.0 * fract(i / 41.0) - 1.0;
2172
+ let gy = abs(gx) - 0.5;
2173
+ let tx = floor(gx + 0.5);
2174
+ gx = gx - tx;
2175
+ var g00 = vec2<f32>(gx.x, gy.x);
2176
+ var g10 = vec2<f32>(gx.y, gy.y);
2177
+ var g01 = vec2<f32>(gx.z, gy.z);
2178
+ var g11 = vec2<f32>(gx.w, gy.w);
2179
+ let norm = 1.79284291400159 - 0.85373472095314 * vec4<f32>(dot(g00, g00), dot(g01, g01), dot(g10, g10), dot(g11, g11));
2180
+ g00 = g00 * norm.x; g01 = g01 * norm.y; g10 = g10 * norm.z; g11 = g11 * norm.w;
2181
+ let n00 = dot(g00, vec2<f32>(fx.x, fy.x));
2182
+ let n10 = dot(g10, vec2<f32>(fx.y, fy.y));
2183
+ let n01 = dot(g01, vec2<f32>(fx.z, fy.z));
2184
+ let n11 = dot(g11, vec2<f32>(fx.w, fy.w));
2185
+ let fade_xy = _wgsl_fade(vec2<f32>(Pf.x, Pf.y));
2186
+ let n_x = mix(vec2<f32>(n00, n01), vec2<f32>(n10, n11), vec2<f32>(fade_xy.x));
2187
+ return 2.3 * mix(n_x.x, n_x.y, fade_xy.y);
2188
+ }
2189
+
2190
+ fn wipeReveal(uv: vec2<f32>, p: f32, s: f32, mode: f32) -> f32 {
2191
+ let ep = p * (1.0 + 2.0 * s) - s;
2192
+ let cuv = toCanvasUV(uv);
2193
+ if (mode < 0.5) {
2194
+ return smoothstep(1.0 - ep - s, 1.0 - ep + s, cuv.x);
2195
+ } else if (mode < 1.5) {
2196
+ // wipe-right: B reveals from the left edge moving right
2197
+ return smoothstep(1.0 - ep - s, 1.0 - ep + s, 1.0 - cuv.x);
2198
+ } else if (mode < 2.5) {
2199
+ return smoothstep(1.0 - ep - s, 1.0 - ep + s, cuv.y);
2200
+ } else if (mode < 3.5) {
2201
+ // wipe-down: B reveals from the top edge moving down
2202
+ return smoothstep(1.0 - ep - s, 1.0 - ep + s, 1.0 - cuv.y);
2203
+ } else if (mode < 4.5) {
2204
+ let d = irisDistCanvas(uv);
2205
+ return 1.0 - smoothstep(ep - s, ep + s, d);
2206
+ } else if (mode < 5.5) {
2207
+ let d = irisDistCanvas(uv);
2208
+ return smoothstep(ep - s, ep + s, d);
2209
+ } else {
2210
+ let sampleUV = cuv * transitionUniforms.uScale + vec2<f32>(transitionUniforms.uSeed, transitionUniforms.uSeed * 0.7);
2211
+ let n = perlin2(sampleUV) * 0.5 + 0.5;
2212
+ return 1.0 - smoothstep(ep - s, ep + s, n);
2213
+ }
2214
+ }
2215
+
2216
+ @fragment
2217
+ fn mainFragment(@location(0) uv : vec2<f32>) -> @location(0) vec4<f32> {
2218
+ let raw = textureSample(uTexture, uSampler, uv);
2219
+ var reveal = wipeReveal(uv, transitionUniforms.uProgress, max(transitionUniforms.uSmoothing, 0.0001), transitionUniforms.uMode);
2220
+ if (transitionUniforms.uInvert > 0.5) { reveal = 1.0 - step(0.99, reveal); }
2221
+ return raw * reveal;
2222
+ }
2223
+ `;
2224
+ var TransitionMaskFilter = class extends PIXI.Filter {
2225
+ constructor(options = {}) {
2226
+ const mode = options.mode ?? "wipe-left";
2227
+ const smoothing = options.smoothing ?? 0.02;
2228
+ const progress = options.progress ?? 0;
2229
+ const invert = options.invert ?? false;
2230
+ const scale = options.scale ?? 30;
2231
+ const seed = options.seed ?? 0;
2232
+ super({
2233
+ glProgram: PIXI.GlProgram.from({
2234
+ vertex: PIXI.defaultFilterVert,
2235
+ fragment: GL_FRAGMENT2,
2236
+ name: "transition-mask-filter"
2237
+ }),
2238
+ gpuProgram: PIXI.GpuProgram.from({
2239
+ vertex: { source: WGSL_SOURCE2, entryPoint: "mainVertex" },
2240
+ fragment: { source: WGSL_SOURCE2, entryPoint: "mainFragment" }
2241
+ }),
2242
+ resources: {
2243
+ transitionUniforms: new PIXI.UniformGroup({
2244
+ uProgress: { value: progress, type: "f32" },
2245
+ uMode: { value: MODE_CODES[mode], type: "f32" },
2246
+ uSmoothing: { value: smoothing, type: "f32" },
2247
+ uInvert: { value: invert ? 1 : 0, type: "f32" },
2248
+ uScale: { value: scale, type: "f32" },
2249
+ uSeed: { value: seed, type: "f32" }
2250
+ })
2251
+ }
2252
+ });
2253
+ }
2254
+ get uProgress() {
2255
+ return this.resources.transitionUniforms.uniforms.uProgress;
2256
+ }
2257
+ set uProgress(v) {
2258
+ this.resources.transitionUniforms.uniforms.uProgress = v;
2259
+ }
2260
+ get uMode() {
2261
+ return this.resources.transitionUniforms.uniforms.uMode;
2262
+ }
2263
+ set uMode(v) {
2264
+ this.resources.transitionUniforms.uniforms.uMode = v;
2265
+ }
2266
+ get uSmoothing() {
2267
+ return this.resources.transitionUniforms.uniforms.uSmoothing;
2268
+ }
2269
+ set uSmoothing(v) {
2270
+ this.resources.transitionUniforms.uniforms.uSmoothing = v;
2271
+ }
2272
+ get uInvert() {
2273
+ return this.resources.transitionUniforms.uniforms.uInvert;
2274
+ }
2275
+ set uInvert(v) {
2276
+ this.resources.transitionUniforms.uniforms.uInvert = v;
2277
+ }
2278
+ get uScale() {
2279
+ return this.resources.transitionUniforms.uniforms.uScale;
2280
+ }
2281
+ set uScale(v) {
2282
+ this.resources.transitionUniforms.uniforms.uScale = v;
2283
+ }
2284
+ get uSeed() {
2285
+ return this.resources.transitionUniforms.uniforms.uSeed;
2286
+ }
2287
+ set uSeed(v) {
2288
+ this.resources.transitionUniforms.uniforms.uSeed = v;
2289
+ }
2290
+ };
2291
+
2292
+ // src/core/Transitions.ts
2293
+ function expandTransitions(spec) {
2294
+ const out = {
2295
+ ...spec,
2296
+ sequences: spec.sequences ? spec.sequences.map((s) => {
2297
+ const cloned = {
2298
+ ...s,
2299
+ // Deep-clone the mutation surfaces only.
2300
+ initial: "initial" in s && s.initial ? { ...s.initial } : void 0,
2301
+ // Shallow: Keyframe / FilterSpec elements are shared references.
2302
+ // Expansion only pushes new literals — never mutate existing ones.
2303
+ keyframes: "keyframes" in s && s.keyframes ? s.keyframes.slice() : void 0,
2304
+ filters: "filters" in s && s.filters ? s.filters.slice() : void 0
2305
+ };
2306
+ if (cloned.type === "composition") {
2307
+ const c = cloned;
2308
+ if (c.width === void 0) c.width = spec.width;
2309
+ if (c.height === void 0) c.height = spec.height;
2310
+ return expandTransitions(c);
2311
+ }
2312
+ return cloned;
2313
+ }) : void 0
2314
+ };
2315
+ if (!out.transitions || out.transitions.length === 0) {
2316
+ delete out.transitions;
2317
+ return out;
2318
+ }
2319
+ const transitions = out.transitions;
2320
+ const sequences = out.sequences ?? [];
2321
+ const parentDuration = out.duration ?? Infinity;
2322
+ const compW = out.width;
2323
+ const compH = out.height;
2324
+ const masksParticipants = /* @__PURE__ */ new Set();
2325
+ for (const t of transitions) {
2326
+ if (t.kind === "wipe" || t.kind === "iris" || t.kind === "dissolve" || t.kind === "zoom") {
2327
+ masksParticipants.add(t.from);
2328
+ masksParticipants.add(t.to);
2329
+ }
2330
+ }
2331
+ if (masksParticipants.size > 0 && compW !== void 0 && compH !== void 0) {
2332
+ for (let i = 0; i < sequences.length; i++) {
2333
+ const s = sequences[i];
2334
+ if (!s.name || !masksParticipants.has(s.name)) continue;
2335
+ const userFilters = s.filters;
2336
+ if (userFilters) rejectReservedNameCollision(userFilters, s.name);
2337
+ sequences[i] = wrapAsFullComposition(s, compW, compH);
2338
+ }
2339
+ }
2340
+ const byName = /* @__PURE__ */ new Map();
2341
+ for (let i = 0; i < sequences.length; i++) {
2342
+ const s = sequences[i];
2343
+ if (!s.name) continue;
2344
+ if (byName.has(s.name)) {
2345
+ throw new Error(`pixi-effects: composition has two sequences named "${s.name}" (transitions need unique names)`);
2346
+ }
2347
+ byName.set(s.name, { index: i, seq: s });
2348
+ }
2349
+ const fromCount = /* @__PURE__ */ new Map();
2350
+ const toCount = /* @__PURE__ */ new Map();
2351
+ for (let i = 0; i < transitions.length; i++) {
2352
+ const t = transitions[i];
2353
+ const tag = `transitions[${i}] (${t.kind})`;
2354
+ if (t.from === t.to) {
2355
+ throw new Error(`pixi-effects: ${tag} cannot transition a sequence to itself ("${t.from}")`);
2356
+ }
2357
+ if (!(t.duration > 0)) {
2358
+ throw new Error(`pixi-effects: ${tag} duration must be > 0 (got ${t.duration})`);
2359
+ }
2360
+ const fromEntry = byName.get(t.from);
2361
+ if (!fromEntry) {
2362
+ throw new Error(`pixi-effects: ${tag} \`from\` "${t.from}" is not found among sibling sequences`);
2363
+ }
2364
+ const toEntry = byName.get(t.to);
2365
+ if (!toEntry) {
2366
+ throw new Error(`pixi-effects: ${tag} \`to\` "${t.to}" is not found among sibling sequences`);
2367
+ }
2368
+ if (toEntry.index <= fromEntry.index) {
2369
+ throw new Error(`pixi-effects: ${tag} \`to\` "${t.to}" must be declared after \`from\` "${t.from}" in sibling order`);
2370
+ }
2371
+ const fromUses = (fromCount.get(t.from) ?? 0) + 1;
2372
+ fromCount.set(t.from, fromUses);
2373
+ if (fromUses > 1) {
2374
+ throw new Error(`pixi-effects: ${tag} sequence "${t.from}" is used as \`from\` in more than one transition`);
2375
+ }
2376
+ const toUses = (toCount.get(t.to) ?? 0) + 1;
2377
+ toCount.set(t.to, toUses);
2378
+ if (toUses > 1) {
2379
+ throw new Error(`pixi-effects: ${tag} sequence "${t.to}" is used as \`to\` in more than one transition`);
2380
+ }
2381
+ const tStart = resolveAt(t.at, parentDuration);
2382
+ const tEnd = tStart + t.duration;
2383
+ const fromSeq = fromEntry.seq;
2384
+ const fromStart = resolveAt(fromSeq.at, parentDuration);
2385
+ const fromEnd = fromStart + (fromSeq.duration ?? parentDuration);
2386
+ if (tStart < fromStart || tEnd > fromEnd) {
2387
+ throw new Error(`pixi-effects: ${tag} window [${tStart}, ${tEnd}] is not covered by \`from\` "${t.from}" (lives [${fromStart}, ${fromEnd}], so it ends at ${fromEnd})`);
2388
+ }
2389
+ const toSeq = toEntry.seq;
2390
+ const toStart = resolveAt(toSeq.at, parentDuration);
2391
+ const toEnd = toStart + (toSeq.duration ?? parentDuration);
2392
+ if (tStart < toStart || tEnd > toEnd) {
2393
+ throw new Error(`pixi-effects: ${tag} window [${tStart}, ${tEnd}] is not covered by \`to\` "${t.to}" (lives [${toStart}, ${toEnd}], so it starts at ${toStart})`);
2394
+ }
2395
+ switch (t.kind) {
2396
+ case "crossfade":
2397
+ expandCrossfade(out, t, fromEntry.seq, toEntry.seq);
2398
+ break;
2399
+ case "wipe":
2400
+ expandMask(out, t, fromEntry.seq, toEntry.seq, i, wipeMode(t.direction), t.smoothing);
2401
+ break;
2402
+ case "iris":
2403
+ expandMask(out, t, fromEntry.seq, toEntry.seq, i, t.mode === "out" ? "iris-out" : "iris-in", t.smoothing);
2404
+ break;
2405
+ case "slide":
2406
+ expandSlide(out, t, fromEntry.seq, toEntry.seq);
2407
+ break;
2408
+ case "dip":
2409
+ expandDip(out, t, fromEntry.seq, toEntry.seq);
2410
+ break;
2411
+ case "zoom":
2412
+ expandZoom(out, t, fromEntry.seq, toEntry.seq);
2413
+ break;
2414
+ case "dissolve":
2415
+ expandDissolve(out, t, fromEntry.seq, toEntry.seq, i);
2416
+ break;
2417
+ }
2418
+ }
2419
+ delete out.transitions;
2420
+ return out;
2421
+ }
2422
+ function wrapAsFullComposition(seq, compW, compH) {
2423
+ const centring = {
2424
+ x: compW / 2,
2425
+ y: compH / 2,
2426
+ pivotX: compW / 2,
2427
+ pivotY: compH / 2
2428
+ };
2429
+ if (seq.type === "composition" && seq.width === compW && seq.height === compH) {
2430
+ const existing = seq.initial;
2431
+ seq.initial = { ...centring, ...existing ?? {} };
2432
+ return seq;
2433
+ }
2434
+ const inner = { ...seq };
2435
+ delete inner.name;
2436
+ delete inner.at;
2437
+ delete inner.duration;
2438
+ return {
2439
+ type: "composition",
2440
+ name: seq.name,
2441
+ at: seq.at,
2442
+ duration: seq.duration,
2443
+ width: compW,
2444
+ height: compH,
2445
+ initial: centring,
2446
+ sequences: [inner]
2447
+ };
2448
+ }
2449
+ function ensureKeyframes(seq) {
2450
+ const s = seq;
2451
+ if (!s.keyframes) s.keyframes = [];
2452
+ return s.keyframes;
2453
+ }
2454
+ function ensureInitial(seq) {
2455
+ const s = seq;
2456
+ if (!s.initial) s.initial = {};
2457
+ return s.initial;
2458
+ }
2459
+ function ensureFilters(seq) {
2460
+ const s = seq;
2461
+ if (!s.filters) s.filters = [];
2462
+ return s.filters;
2463
+ }
2464
+ function expandCrossfade(_comp, t, fromSeq, toSeq) {
2465
+ const ease = t.ease ?? "none";
2466
+ const fromKfs = ensureKeyframes(fromSeq);
2467
+ fromKfs.push({ at: t.at, to: { alpha: 0 }, duration: t.duration, ease });
2468
+ const toInitial = ensureInitial(toSeq);
2469
+ if (toInitial.alpha !== void 0 && toInitial.alpha !== 0) {
2470
+ throw new Error(
2471
+ `pixi-effects: crossfade "${t.from}"\u2192"${t.to}" requires "${t.to}" to start invisible, but it already has initial.alpha=${String(toInitial.alpha)}. Remove the manual setting.`
2472
+ );
2473
+ }
2474
+ toInitial.alpha = 0;
2475
+ const toKfs = ensureKeyframes(toSeq);
2476
+ toKfs.push({ at: t.at, to: { alpha: 1 }, duration: t.duration, ease });
2477
+ }
2478
+ function wipeMode(direction) {
2479
+ switch (direction) {
2480
+ case "left":
2481
+ return "wipe-left";
2482
+ case "right":
2483
+ return "wipe-right";
2484
+ case "up":
2485
+ return "wipe-up";
2486
+ case "down":
2487
+ return "wipe-down";
2488
+ }
2489
+ }
2490
+ function transitionFilterName(index) {
2491
+ return `_pe-transition-${index}`;
2492
+ }
2493
+ function expandSlide(_comp, t, fromSeq, toSeq) {
2494
+ const ease = t.ease ?? "none";
2495
+ const isHorizontal = t.direction === "left" || t.direction === "right";
2496
+ const axis = isHorizontal ? "x" : "y";
2497
+ const dim = isHorizontal ? "W" : "H";
2498
+ const sign = t.direction === "left" || t.direction === "up" ? -1 : 1;
2499
+ const fromInitial = ensureInitial(fromSeq);
2500
+ const fromNatural = fromInitial[axis];
2501
+ const fromTarget = composeDimOffset(fromNatural, sign, dim);
2502
+ const fromKfs = ensureKeyframes(fromSeq);
2503
+ fromKfs.push({ at: t.at, to: { [axis]: fromTarget }, duration: t.duration, ease });
2504
+ const toInitial = ensureInitial(toSeq);
2505
+ const toNatural = toInitial[axis];
2506
+ toInitial[axis] = composeDimOffset(toNatural, -sign, dim);
2507
+ const toKfs = ensureKeyframes(toSeq);
2508
+ toKfs.push({ at: t.at, to: { [axis]: toNatural ?? 0 }, duration: t.duration, ease });
2509
+ }
2510
+ function composeDimOffset(natural, sign, dim) {
2511
+ const signed = sign === 1 ? dim : `-${dim}`;
2512
+ if (natural === void 0 || natural === 0) return signed;
2513
+ if (typeof natural === "number") {
2514
+ return sign === 1 ? `${natural} + ${dim}` : `${natural} - ${dim}`;
2515
+ }
2516
+ return sign === 1 ? `(${String(natural)}) + ${dim}` : `(${String(natural)}) - ${dim}`;
2517
+ }
2518
+ function expandMask(comp, t, fromSeq, toSeq, transitionIndex, mode, smoothing) {
2519
+ const ease = t.ease ?? "none";
2520
+ const inName = transitionFilterName(transitionIndex);
2521
+ const outName = `${transitionFilterName(transitionIndex)}-out`;
2522
+ const sm = smoothing ?? 0.02;
2523
+ const compW = comp.width ?? 0;
2524
+ const compH = comp.height ?? 0;
2525
+ const fullArea = { x: 0, y: 0, width: compW, height: compH };
2526
+ const toFilters = ensureFilters(toSeq);
2527
+ rejectReservedNameCollision(toFilters, toSeq.name);
2528
+ toFilters.push({
2529
+ type: "custom",
2530
+ name: inName,
2531
+ filter: new TransitionMaskFilter({ mode, smoothing: sm, progress: 0 })
2532
+ });
2533
+ ensureKeyframes(toSeq).push({
2534
+ at: t.at,
2535
+ to: { [`filters.${inName}.uProgress`]: 1 },
2536
+ duration: t.duration,
2537
+ ease
2538
+ });
2539
+ toSeq.filterArea = fullArea;
2540
+ const fromFilters = ensureFilters(fromSeq);
2541
+ rejectReservedNameCollision(fromFilters, fromSeq.name);
2542
+ fromFilters.push({
2543
+ type: "custom",
2544
+ name: outName,
2545
+ filter: new TransitionMaskFilter({ mode, smoothing: sm, progress: 0, invert: true })
2546
+ });
2547
+ ensureKeyframes(fromSeq).push({
2548
+ at: t.at,
2549
+ to: { [`filters.${outName}.uProgress`]: 1 },
2550
+ duration: t.duration,
2551
+ ease
2552
+ });
2553
+ fromSeq.filterArea = fullArea;
2554
+ }
2555
+ function expandDip(_comp, t, fromSeq, toSeq) {
2556
+ const ease = t.ease ?? "none";
2557
+ const half = t.duration / 2;
2558
+ ensureKeyframes(fromSeq).push({
2559
+ at: t.at,
2560
+ to: { alpha: 0 },
2561
+ duration: half,
2562
+ ease
2563
+ });
2564
+ const toInitial = ensureInitial(toSeq);
2565
+ if (toInitial.alpha !== void 0 && toInitial.alpha !== 0) {
2566
+ throw new Error(
2567
+ `pixi-effects: dip "${t.from}"\u2192"${t.to}" requires "${t.to}" to start invisible, but it already has initial.alpha=${String(toInitial.alpha)}. Remove the manual setting.`
2568
+ );
2569
+ }
2570
+ toInitial.alpha = 0;
2571
+ ensureKeyframes(toSeq).push({
2572
+ at: t.at + half,
2573
+ to: { alpha: 1 },
2574
+ duration: half,
2575
+ ease
2576
+ });
2577
+ }
2578
+ function expandZoom(_comp, t, fromSeq, toSeq) {
2579
+ const ease = t.ease ?? "none";
2580
+ const startScale = t.fromScale ?? 4;
2581
+ const mode = t.mode ?? "in";
2582
+ if (mode === "in") {
2583
+ ensureKeyframes(fromSeq).push({
2584
+ at: t.at,
2585
+ to: { alpha: 0 },
2586
+ duration: t.duration,
2587
+ ease
2588
+ });
2589
+ const toInitial = ensureInitial(toSeq);
2590
+ toInitial.alpha = 0;
2591
+ toInitial.scale = startScale;
2592
+ ensureKeyframes(toSeq).push({
2593
+ at: t.at,
2594
+ to: { alpha: 1, scale: 1 },
2595
+ duration: t.duration,
2596
+ ease
2597
+ });
2598
+ } else {
2599
+ ensureKeyframes(fromSeq).push({
2600
+ at: t.at,
2601
+ to: { alpha: 0, scale: startScale },
2602
+ duration: t.duration,
2603
+ ease
2604
+ });
2605
+ const toInitial = ensureInitial(toSeq);
2606
+ toInitial.alpha = 0;
2607
+ ensureKeyframes(toSeq).push({
2608
+ at: t.at,
2609
+ to: { alpha: 1 },
2610
+ duration: t.duration,
2611
+ ease
2612
+ });
2613
+ }
2614
+ }
2615
+ function expandDissolve(comp, t, fromSeq, toSeq, transitionIndex) {
2616
+ const ease = t.ease ?? "none";
2617
+ const inName = transitionFilterName(transitionIndex);
2618
+ const outName = `${transitionFilterName(transitionIndex)}-out`;
2619
+ const sm = t.smoothing ?? 0.05;
2620
+ const scale = t.scale ?? 30;
2621
+ const seed = t.seed ?? 0;
2622
+ const compW = comp.width ?? 0;
2623
+ const compH = comp.height ?? 0;
2624
+ const fullArea = { x: 0, y: 0, width: compW, height: compH };
2625
+ const toFilters = ensureFilters(toSeq);
2626
+ rejectReservedNameCollision(toFilters, toSeq.name);
2627
+ toFilters.push({
2628
+ type: "custom",
2629
+ name: inName,
2630
+ filter: new TransitionMaskFilter({ mode: "dissolve", smoothing: sm, progress: 0, scale, seed })
2631
+ });
2632
+ ensureKeyframes(toSeq).push({
2633
+ at: t.at,
2634
+ to: { [`filters.${inName}.uProgress`]: 1 },
2635
+ duration: t.duration,
2636
+ ease
2637
+ });
2638
+ toSeq.filterArea = fullArea;
2639
+ const fromFilters = ensureFilters(fromSeq);
2640
+ rejectReservedNameCollision(fromFilters, fromSeq.name);
2641
+ fromFilters.push({
2642
+ type: "custom",
2643
+ name: outName,
2644
+ filter: new TransitionMaskFilter({ mode: "dissolve", smoothing: sm, progress: 0, scale, seed, invert: true })
2645
+ });
2646
+ ensureKeyframes(fromSeq).push({
2647
+ at: t.at,
2648
+ to: { [`filters.${outName}.uProgress`]: 1 },
2649
+ duration: t.duration,
2650
+ ease
2651
+ });
2652
+ fromSeq.filterArea = fullArea;
2653
+ }
2654
+ var OWN_NAME_PATTERN = /^_pe-transition-\d+(-out)?$/;
2655
+ function rejectReservedNameCollision(filters, seqName) {
2656
+ for (const f of filters) {
2657
+ if (typeof f.name === "string" && f.name.startsWith("_pe-transition-") && !OWN_NAME_PATTERN.test(f.name)) {
2658
+ throw new Error(
2659
+ `pixi-effects: sequence "${seqName}" already has a filter named "${f.name}" \u2014 the reserved name prefix "_pe-transition-" is for internally generated transition filters`
2660
+ );
2661
+ }
2662
+ }
2663
+ }
2664
+
2665
+ // src/core/Movie.ts
2666
+ PIXI.extensions.add(PIXI.CullerPlugin);
2667
+ gsap.gsap.registerPlugin(PixiPlugin.PixiPlugin);
2668
+ PixiPlugin.PixiPlugin.registerPIXI(PIXI__namespace);
2669
+ var Movie = class {
2670
+ _events = {};
2671
+ _initState = "idle";
2672
+ app = null;
2673
+ timeline = null;
2674
+ audioBuffer = null;
2675
+ audioSource = null;
2676
+ gainNode = null;
2677
+ _volume = 1;
2678
+ _muted = false;
2679
+ isPlaying = false;
2680
+ currentFrame = 0;
2681
+ totalFrames = 0;
2682
+ width = 0;
2683
+ height = 0;
2684
+ duration = 0;
2685
+ frameRate = 30;
2686
+ background = "#000000";
2687
+ _audioContext = null;
2688
+ _rootSequence = null;
2689
+ _rootContainer = null;
2690
+ _raf = null;
2691
+ on(event, fn) {
2692
+ (this._events[event] ??= []).push(fn);
2693
+ if (event === "ready" && this._initState === "ready") {
2694
+ try {
2695
+ fn();
2696
+ } catch (e) {
2697
+ console.warn("pixi-effects: ready listener threw:", e);
2698
+ }
2699
+ }
2700
+ return this;
2701
+ }
2702
+ off(event, fn) {
2703
+ const list = this._events[event];
2704
+ if (!list) return this;
2705
+ const i = list.indexOf(fn);
2706
+ if (i > -1) list.splice(i, 1);
2707
+ return this;
2708
+ }
2709
+ emit(event, ...args) {
2710
+ for (const fn of this._events[event] ?? []) fn(...args);
2711
+ }
2712
+ get isReady() {
2713
+ return this._initState === "ready";
2714
+ }
2715
+ _ensureAudioContext() {
2716
+ if (!this._audioContext) {
2717
+ const Ctx = window.AudioContext ?? window.webkitAudioContext;
2718
+ this._audioContext = new Ctx();
2719
+ }
2720
+ return this._audioContext;
2721
+ }
2722
+ async init(options = {}) {
2723
+ this._initState = "pending";
2724
+ try {
2725
+ this.width = options.width ?? 1920;
2726
+ this.height = options.height ?? 1080;
2727
+ this.duration = options.duration ?? 10;
2728
+ this.frameRate = options.frameRate ?? 30;
2729
+ this.totalFrames = Math.round(this.duration * this.frameRate);
2730
+ this.background = options.background ?? "#000000";
2731
+ this.timeline = gsap.gsap.timeline({ paused: true, defaults: { ease: "none" } });
2732
+ this.timeline.add(gsap.gsap.to({}, { duration: this.duration }));
2733
+ const baseAppOptions = {
2734
+ width: this.width,
2735
+ height: this.height,
2736
+ background: this.background,
2737
+ antialias: true,
2738
+ resolution: 1,
2739
+ autoDensity: false,
2740
+ preserveDrawingBuffer: true,
2741
+ canvas: options.canvas
2742
+ };
2743
+ this.app = new PIXI.Application();
2744
+ try {
2745
+ await this.app.init({ ...baseAppOptions, preference: "webgpu" });
2746
+ } catch (err) {
2747
+ console.warn("pixi-effects: WebGPU init failed, falling back to WebGL \u2014", err);
2748
+ this.app = new PIXI.Application();
2749
+ await this.app.init({ ...baseAppOptions, preference: "webgl" });
2750
+ }
2751
+ const root = new PIXI.Container();
2752
+ root.cullable = true;
2753
+ root.cullableChildren = true;
2754
+ root.cullArea = new PIXI.Rectangle(0, 0, this.width, this.height);
2755
+ this.app.stage.addChild(root);
2756
+ this._rootContainer = root;
2757
+ const audioContext = this._ensureAudioContext();
2758
+ await loadAssetBundle(options.assets ?? [], audioContext);
2759
+ const rootShape = { width: this.width, height: this.height, duration: this.duration };
2760
+ const seededComposition = options.composition ? {
2761
+ ...options.composition,
2762
+ width: options.composition.width ?? this.width,
2763
+ height: options.composition.height ?? this.height,
2764
+ duration: options.composition.duration ?? this.duration
2765
+ } : void 0;
2766
+ const userComposition = seededComposition ? expandTransitions(seededComposition) : void 0;
2767
+ const rootSeqSpec = {
2768
+ type: "composition",
2769
+ width: this.width,
2770
+ height: this.height,
2771
+ duration: this.duration,
2772
+ ...userComposition
2773
+ };
2774
+ const composition = new CompositionSequence(rootSeqSpec, null, rootShape);
2775
+ await composition.build();
2776
+ this._rootSequence = composition;
2777
+ if (composition.target) root.addChild(composition.target);
2778
+ composition.bindTimeline(this.timeline);
2779
+ const audios = [];
2780
+ composition.collectAudio(audios, 0);
2781
+ if (audios.length > 0) {
2782
+ this.audioBuffer = await mixdown(audios, this.duration, audioContext.sampleRate);
2783
+ }
2784
+ this.timeline.progress(1).progress(0);
2785
+ this.app.renderer.render({ container: this.app.stage });
2786
+ this._initState = "ready";
2787
+ this.emit("ready");
2788
+ } catch (err) {
2789
+ try {
2790
+ await this.destroy();
2791
+ } catch (cleanupErr) {
2792
+ console.warn("pixi-effects: cleanup after init failure also threw:", cleanupErr);
2793
+ }
2794
+ throw err;
2795
+ }
2796
+ }
2797
+ async gotoFrame(frame, force = false) {
2798
+ if (this._initState !== "ready") return;
2799
+ if (!force && this.currentFrame === frame) return;
2800
+ this.currentFrame = Math.max(0, Math.min(frame, this.totalFrames));
2801
+ this.timeline.time(this.currentFrame / this.frameRate);
2802
+ await this._awaitVideoFrames();
2803
+ this.app?.renderer?.render({ container: this.app.stage });
2804
+ this.emit("frame", { frame: this.currentFrame, totalFrames: this.totalFrames });
2805
+ }
2806
+ async _awaitVideoFrames() {
2807
+ if (!this._rootSequence) return;
2808
+ const collected = [];
2809
+ collectVideoSequences(this._rootSequence, collected);
2810
+ const t = this.currentFrame / this.frameRate;
2811
+ await Promise.all(collected.map((v) => {
2812
+ const local = t - v.at;
2813
+ if (local < 0 || local > (v.duration ?? 0)) return Promise.resolve();
2814
+ return v.awaitFrameAt(local);
2815
+ }));
2816
+ }
2817
+ play() {
2818
+ if (this._initState !== "ready") return;
2819
+ if (this.currentFrame >= this.totalFrames) this.currentFrame = 0;
2820
+ this.isPlaying = true;
2821
+ const startTime = performance.now() - this.currentFrame / this.frameRate * 1e3;
2822
+ let inFlight = false;
2823
+ const tick = (time) => {
2824
+ if (!this.isPlaying) return;
2825
+ if (inFlight) {
2826
+ this._raf = requestAnimationFrame(tick);
2827
+ return;
2828
+ }
2829
+ const elapsed = (time - startTime) / 1e3;
2830
+ const frame = Math.floor(elapsed * this.frameRate);
2831
+ if (frame <= this.totalFrames) {
2832
+ inFlight = true;
2833
+ this.gotoFrame(frame).catch((err) => {
2834
+ console.warn("pixi-effects: gotoFrame failed during playback:", err);
2835
+ }).finally(() => {
2836
+ inFlight = false;
2837
+ });
2838
+ this._raf = requestAnimationFrame(tick);
2839
+ } else {
2840
+ this.pause();
2841
+ this.gotoFrame(this.totalFrames).catch((err) => {
2842
+ console.warn("pixi-effects: final gotoFrame failed:", err);
2843
+ });
2844
+ }
2845
+ };
2846
+ this._raf = requestAnimationFrame(tick);
2847
+ if (this.audioBuffer) {
2848
+ const ctx = this._ensureAudioContext();
2849
+ this.audioSource = ctx.createBufferSource();
2850
+ this.audioSource.buffer = this.audioBuffer;
2851
+ this.gainNode = ctx.createGain();
2852
+ this.gainNode.gain.value = this._muted ? 0 : this._volume;
2853
+ this.audioSource.connect(this.gainNode).connect(ctx.destination);
2854
+ this.audioSource.start(0, this.currentFrame / this.frameRate);
2855
+ }
2856
+ }
2857
+ pause() {
2858
+ const wasPlaying = this.isPlaying;
2859
+ this.isPlaying = false;
2860
+ if (this._raf) {
2861
+ cancelAnimationFrame(this._raf);
2862
+ this._raf = null;
2863
+ }
2864
+ if (this.audioSource) {
2865
+ try {
2866
+ this.audioSource.stop();
2867
+ } catch {
2868
+ }
2869
+ this.audioSource.disconnect();
2870
+ this.audioSource = null;
2871
+ }
2872
+ if (this.gainNode) {
2873
+ this.gainNode.disconnect();
2874
+ this.gainNode = null;
2875
+ }
2876
+ if (wasPlaying) this.emit("pause");
2877
+ }
2878
+ set volume(v) {
2879
+ this._volume = Math.max(0, Math.min(1, v));
2880
+ if (this.gainNode) this.gainNode.gain.value = this._muted ? 0 : this._volume;
2881
+ }
2882
+ get volume() {
2883
+ return this._volume;
2884
+ }
2885
+ set muted(v) {
2886
+ this._muted = !!v;
2887
+ if (this.gainNode) this.gainNode.gain.value = this._muted ? 0 : this._volume;
2888
+ }
2889
+ get muted() {
2890
+ return this._muted;
2891
+ }
2892
+ toggleMute() {
2893
+ this.muted = !this.muted;
2894
+ return this.muted;
2895
+ }
2896
+ async render(options) {
2897
+ return await exportFrames(this, options);
2898
+ }
2899
+ async destroy() {
2900
+ safeRun(() => this.pause());
2901
+ safeRun(() => this._rootSequence?.destroy());
2902
+ this._rootSequence = null;
2903
+ safeRun(() => this.timeline?.kill());
2904
+ this.timeline = null;
2905
+ safeRun(() => this.app?.destroy(true, { children: true, texture: true }));
2906
+ this.app = null;
2907
+ this.audioBuffer = null;
2908
+ safeRun(() => this._audioContext?.close().catch(() => {
2909
+ }));
2910
+ this._audioContext = null;
2911
+ this._initState = "destroyed";
2912
+ }
2913
+ };
2914
+ function safeRun(fn) {
2915
+ try {
2916
+ fn();
2917
+ } catch (e) {
2918
+ console.warn("pixi-effects: cleanup step threw, continuing:", e);
2919
+ }
2920
+ }
2921
+ function collectVideoSequences(seq, out) {
2922
+ if (typeof seq.awaitFrameAt === "function") {
2923
+ out.push(seq);
2924
+ }
2925
+ for (const child of seq._children ?? []) {
2926
+ collectVideoSequences(child, out);
2927
+ }
2928
+ }
2929
+
2930
+ // src/presets/kenBurns.ts
2931
+ function kenBurns(opts) {
2932
+ const {
2933
+ asset,
2934
+ name,
2935
+ at,
2936
+ duration,
2937
+ fit = "cover",
2938
+ ease = "sine.inOut"
2939
+ } = opts;
2940
+ const baseScale = fit;
2941
+ const baseAt = at ?? 0;
2942
+ let initial;
2943
+ let keyframes;
2944
+ if (opts.motion === "still") {
2945
+ initial = {
2946
+ x: "W/2",
2947
+ y: "H/2",
2948
+ anchorX: 0.5,
2949
+ anchorY: 0.5,
2950
+ scale: baseScale
2951
+ };
2952
+ keyframes = [];
2953
+ } else if (opts.motion === "scale") {
2954
+ const origin = opts.origin ?? [0.5, 0.5];
2955
+ const zoom = opts.zoom ?? 1.15;
2956
+ const direction = opts.direction ?? "in";
2957
+ const fromZ = direction === "in" ? 1 : zoom;
2958
+ const toZ = direction === "in" ? zoom : 1;
2959
+ const pivotXExpr = `w * ${origin[0]}`;
2960
+ const pivotYExpr = `h * ${origin[1]}`;
2961
+ const initialScale = `${baseScale} * ${fromZ}`;
2962
+ const xExpr = `W/2 - (${baseScale}) * ${fromZ} * w * ${0.5 - origin[0]}`;
2963
+ const yExpr = `H/2 - (${baseScale}) * ${fromZ} * h * ${0.5 - origin[1]}`;
2964
+ initial = {
2965
+ x: xExpr,
2966
+ y: yExpr,
2967
+ pivotX: pivotXExpr,
2968
+ pivotY: pivotYExpr,
2969
+ scale: initialScale
2970
+ };
2971
+ keyframes = [
2972
+ { at: baseAt, to: { scale: `${baseScale} * ${toZ}` }, duration, ease }
2973
+ ];
2974
+ } else if (opts.motion === "rotation") {
2975
+ const angle = clamp(opts.angle ?? 8, 0, 30);
2976
+ const direction = opts.direction ?? "cw";
2977
+ const startAngle = direction === "cw" ? 0 : direction === "ccw" ? 0 : -angle / 2;
2978
+ const endAngle = direction === "cw" ? +angle : direction === "ccw" ? -angle : +angle / 2;
2979
+ const sampleStep = 1;
2980
+ const span = Math.abs(endAngle - startAngle);
2981
+ const sampleCount = Math.max(2, Math.ceil(span / sampleStep) + 1);
2982
+ const scaleAt = (deg) => {
2983
+ const \u03B8 = Math.abs(deg) * Math.PI / 180;
2984
+ const c = Math.cos(\u03B8);
2985
+ const s = Math.sin(\u03B8);
2986
+ return `max(${baseScale}, max((W*${c} + H*${s}) / w, (W*${s} + H*${c}) / h))`;
2987
+ };
2988
+ initial = {
2989
+ x: "W/2",
2990
+ y: "H/2",
2991
+ pivotX: "w * 0.5",
2992
+ pivotY: "h * 0.5",
2993
+ scale: scaleAt(startAngle),
2994
+ rotation: startAngle
2995
+ };
2996
+ keyframes = [];
2997
+ for (let i = 1; i < sampleCount; i++) {
2998
+ const tFrac = i / (sampleCount - 1);
2999
+ const angleHere = startAngle + (endAngle - startAngle) * tFrac;
3000
+ const tHere = duration * tFrac;
3001
+ const tPrev = duration * ((i - 1) / (sampleCount - 1));
3002
+ keyframes.push({
3003
+ at: baseAt + tPrev,
3004
+ to: { rotation: angleHere, scale: scaleAt(angleHere) },
3005
+ duration: tHere - tPrev,
3006
+ ease: i === 1 ? ease : "none"
3007
+ });
3008
+ }
3009
+ } else {
3010
+ const from = opts.from ?? [0.25, 0.25];
3011
+ const to = opts.to ?? [0.75, 0.75];
3012
+ const zoom = opts.zoom ?? 1.15;
3013
+ const scaleExpr = `(${baseScale}) * ${zoom}`;
3014
+ const xExpr = (panX) => `W/2 + (${0.5 - panX}) * (w * (${baseScale}) * ${zoom} - W)`;
3015
+ const yExpr = (panY) => `H/2 + (${0.5 - panY}) * (h * (${baseScale}) * ${zoom} - H)`;
3016
+ initial = {
3017
+ x: xExpr(from[0]),
3018
+ y: yExpr(from[1]),
3019
+ pivotX: "w * 0.5",
3020
+ pivotY: "h * 0.5",
3021
+ scale: scaleExpr
3022
+ };
3023
+ keyframes = [
3024
+ { at: baseAt, to: { x: xExpr(to[0]), y: yExpr(to[1]) }, duration, ease }
3025
+ ];
3026
+ }
3027
+ const spec = {
3028
+ type: "image",
3029
+ asset,
3030
+ duration,
3031
+ initial,
3032
+ keyframes
3033
+ };
3034
+ if (name !== void 0) spec.name = name;
3035
+ if (at !== void 0) spec.at = at;
3036
+ return spec;
3037
+ }
3038
+ function clamp(v, lo, hi) {
3039
+ return Math.max(lo, Math.min(hi, v));
3040
+ }
3041
+
3042
+ exports.Movie = Movie;
3043
+ exports.kenBurns = kenBurns;
3044
+ //# sourceMappingURL=index.cjs.map
3045
+ //# sourceMappingURL=index.cjs.map