pixi-effects 0.30.1 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +6 -5
  3. package/ai/chat-template.html +3 -3
  4. package/ai/tools/check.mjs +42 -2
  5. package/ai/tools/render.mjs +34 -4
  6. package/ai/tools/varArgs.mjs +75 -0
  7. package/dist/{Base-Ne74ztNl.d.ts → Base-CXsMdmJB.d.ts} +1 -1
  8. package/dist/{Base-CB1_FPZD.d.cts → Base-pNq6sVGl.d.cts} +1 -1
  9. package/dist/Composition-LNMKWXSL.cjs +16 -0
  10. package/dist/{Composition-MUKR3FYD.cjs.map → Composition-LNMKWXSL.cjs.map} +1 -1
  11. package/dist/Composition-RF7QOEXT.js +7 -0
  12. package/dist/{Composition-RJGA7KDU.js.map → Composition-RF7QOEXT.js.map} +1 -1
  13. package/dist/Controller.cjs +42 -1
  14. package/dist/Controller.cjs.map +1 -1
  15. package/dist/Controller.d.cts +10 -2
  16. package/dist/Controller.d.ts +10 -2
  17. package/dist/Controller.js +42 -1
  18. package/dist/Controller.js.map +1 -1
  19. package/dist/{Movie-DdnpLpw1.d.cts → Movie-CL6SsAWe.d.cts} +90 -3
  20. package/dist/{Movie-C2rZFkrO.d.ts → Movie-f0tQG9y5.d.ts} +90 -3
  21. package/dist/Spans-NQGKVMR5.cjs +55 -0
  22. package/dist/Spans-NQGKVMR5.cjs.map +1 -0
  23. package/dist/Spans-SDXABOV6.js +53 -0
  24. package/dist/Spans-SDXABOV6.js.map +1 -0
  25. package/dist/{blendModes-RIVZXL5A.js → blendModes-MVRLG3T7.js} +4 -4
  26. package/dist/{blendModes-RIVZXL5A.js.map → blendModes-MVRLG3T7.js.map} +1 -1
  27. package/dist/{blendModes-ETMYWATF.cjs → blendModes-VTKDXWEN.cjs} +5 -5
  28. package/dist/{blendModes-ETMYWATF.cjs.map → blendModes-VTKDXWEN.cjs.map} +1 -1
  29. package/dist/{chunk-ZAXYPXKW.js → chunk-AW5NPDE4.js} +1367 -1150
  30. package/dist/chunk-AW5NPDE4.js.map +1 -0
  31. package/dist/{chunk-RCVZX2JP.js → chunk-C5BNHVKK.js} +36 -3
  32. package/dist/chunk-C5BNHVKK.js.map +1 -0
  33. package/dist/{chunk-WHNADXXG.cjs → chunk-CRUDOKSD.cjs} +91 -91
  34. package/dist/chunk-CRUDOKSD.cjs.map +1 -0
  35. package/dist/{chunk-A3Q3SDSH.js → chunk-D5BFQVKL.js} +6 -6
  36. package/dist/chunk-D5BFQVKL.js.map +1 -0
  37. package/dist/{chunk-LYOKH7KE.cjs → chunk-FCGYJCVO.cjs} +36 -3
  38. package/dist/chunk-FCGYJCVO.cjs.map +1 -0
  39. package/dist/{chunk-CRQFDY7E.js → chunk-FCMOREIS.js} +467 -58
  40. package/dist/chunk-FCMOREIS.js.map +1 -0
  41. package/dist/{chunk-HREBQAY4.cjs → chunk-K3MAEKFM.cjs} +493 -84
  42. package/dist/chunk-K3MAEKFM.cjs.map +1 -0
  43. package/dist/{chunk-4KB3GUEK.js → chunk-QFNS6BFL.js} +4 -4
  44. package/dist/{chunk-4KB3GUEK.js.map → chunk-QFNS6BFL.js.map} +1 -1
  45. package/dist/{chunk-ZVUO325O.cjs → chunk-QHDN6TF5.cjs} +1382 -1160
  46. package/dist/chunk-QHDN6TF5.cjs.map +1 -0
  47. package/dist/{chunk-2DXPTKQZ.cjs → chunk-Z437UWI4.cjs} +5 -5
  48. package/dist/{chunk-2DXPTKQZ.cjs.map → chunk-Z437UWI4.cjs.map} +1 -1
  49. package/dist/index.cjs +33 -33
  50. package/dist/index.d.cts +5 -5
  51. package/dist/index.d.ts +5 -5
  52. package/dist/index.js +5 -5
  53. package/dist/loader.css +1 -1
  54. package/dist/present.cjs +5 -5
  55. package/dist/present.d.cts +2 -2
  56. package/dist/present.d.ts +2 -2
  57. package/dist/present.js +5 -5
  58. package/dist/three.cjs +6 -6
  59. package/dist/three.d.cts +2 -2
  60. package/dist/three.d.ts +2 -2
  61. package/dist/three.js +2 -2
  62. package/dist/{types-BzIB65Vk.d.ts → types-CavnUvd5.d.cts} +28 -1
  63. package/dist/{types-BzIB65Vk.d.cts → types-CavnUvd5.d.ts} +28 -1
  64. package/docs/api.md +28 -1
  65. package/docs/dsl.md +46 -5
  66. package/llms-full.txt +136 -10
  67. package/package.json +83 -20
  68. package/skills/pixi-effects/SKILL.md +1 -1
  69. package/skills/pixi-effects/reference/cheatsheet.md +5 -2
  70. package/skills/pixi-effects/reference/pitfalls.md +5 -1
  71. package/skills/pixi-effects/reference/recipes.md +52 -0
  72. package/skills/pixi-effects/template.html +6 -6
  73. package/dist/Composition-MUKR3FYD.cjs +0 -16
  74. package/dist/Composition-RJGA7KDU.js +0 -7
  75. package/dist/chunk-A3Q3SDSH.js.map +0 -1
  76. package/dist/chunk-CRQFDY7E.js.map +0 -1
  77. package/dist/chunk-HREBQAY4.cjs.map +0 -1
  78. package/dist/chunk-LYOKH7KE.cjs.map +0 -1
  79. package/dist/chunk-RCVZX2JP.js.map +0 -1
  80. package/dist/chunk-WHNADXXG.cjs.map +0 -1
  81. package/dist/chunk-ZAXYPXKW.js.map +0 -1
  82. package/dist/chunk-ZVUO325O.cjs.map +0 -1
@@ -1,7 +1,7 @@
1
- import { CompositionSequence, maskSourceOf } from './chunk-A3Q3SDSH.js';
2
- import { createTimeline, lengthen, ensureFilterLibrary, checkEase, parseEase, homeDistance, DEFAULT_FOV, random, noise, flattenSvgPath, rand, DEG, timeRemapOf, contentLength, resolveAt } from './chunk-ZAXYPXKW.js';
3
- import { usesAdvancedBlend, enableAdvancedBlend } from './chunk-4KB3GUEK.js';
4
- import { collectPropKeys } from './chunk-RCVZX2JP.js';
1
+ import { CompositionSequence, maskSourceOf } from './chunk-D5BFQVKL.js';
2
+ import { EXPRESSION_NAMES, createTimeline, lengthen, ensureFilterLibrary, checkEase, parseEase, homeDistance, DEFAULT_FOV, random, noise, flattenSvgPath, rand, DEG, timeRemapOf, contentLength, resolveAt, spansMeta } from './chunk-AW5NPDE4.js';
3
+ import { usesAdvancedBlend, enableAdvancedBlend } from './chunk-QFNS6BFL.js';
4
+ import { collectPropKeys } from './chunk-C5BNHVKK.js';
5
5
  import { warnUnknownOptions, suggestName } from './chunk-BXOCEZ4U.js';
6
6
  import { extensions, CullerPlugin, Application, Container, Rectangle, Culler, CanvasTextMetrics, TextStyle, Assets, ExtensionType, Filter, Texture, UniformGroup, GpuProgram, GlProgram, defaultFilterVert } from 'pixi.js';
7
7
  import { Output, BufferTarget, CanvasSource, AudioBufferSource, Input, ALL_FORMATS, BlobSource, WavOutputFormat, Conversion, VideoSampleSink, OggOutputFormat, QUALITY_VERY_HIGH, QUALITY_HIGH, QUALITY_MEDIUM, QUALITY_LOW, QUALITY_VERY_LOW, MkvOutputFormat, WebMOutputFormat, MovOutputFormat, Mp4OutputFormat, canEncodeVideo, getFirstEncodableAudioCodec } from 'mediabunny';
@@ -1996,6 +1996,8 @@ var round = (n) => Math.round(n * 1e6) / 1e6;
1996
1996
  var family = (name) => name.replace(/\d+/g, "#");
1997
1997
  function detailOf(seq) {
1998
1998
  const s = seq.spec;
1999
+ const spans = spansMeta.get(seq.spec)?.preview;
2000
+ if (spans) return spans.length > 60 ? spans.slice(0, 57) + "\u2026" : spans;
1999
2001
  if (typeof s.text === "string") return s.text.length > 60 ? s.text.slice(0, 57) + "\u2026" : s.text;
2000
2002
  if (typeof s.asset === "string") return `asset \u201C${s.asset}\u201D`;
2001
2003
  if (typeof s.sfx === "string") return `sfx \u201C${s.sfx}\u201D`;
@@ -2171,6 +2173,124 @@ function sheetLayout(n, columns, cellW, srcW, srcH, labelH) {
2171
2173
  return { cols, rows, cellW, cellH, width: cols * cellW, height: rows * (cellH + labelH), positions };
2172
2174
  }
2173
2175
 
2176
+ // src/core/vars.ts
2177
+ var RESERVED_NAMES = ["w", "h", "W", "H", "GW", "GH", "t", "d", "T", "contain", "cover", "value", ...EXPRESSION_NAMES];
2178
+ var NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
2179
+ var isPlain = (v) => !!v && typeof v === "object" && (Object.getPrototypeOf(v) === Object.prototype || Object.getPrototypeOf(v) === null);
2180
+ function normalizeVars(raw, where, warn = (m) => console.warn(m)) {
2181
+ if (raw === void 0) return {};
2182
+ if (!isPlain(raw)) {
2183
+ warn(`pixi-effects: ${where}: vars must be an object of names and values ({ title: 'NORD', shift: 40 }); ignored`);
2184
+ return {};
2185
+ }
2186
+ const out = {};
2187
+ for (const [name, value] of Object.entries(raw)) {
2188
+ if (!NAME.test(name)) {
2189
+ warn(`pixi-effects: ${where}: "${name}" is not a name for a variable (letters, digits and _, not starting with a digit); ignored`);
2190
+ continue;
2191
+ }
2192
+ if (RESERVED_NAMES.includes(name)) {
2193
+ warn(`pixi-effects: ${where}: "${name}" is reserved (an expression word, a function, PI or the {value} counter); choose another name; ignored`);
2194
+ continue;
2195
+ }
2196
+ if (typeof value === "boolean") {
2197
+ warn(`pixi-effects: ${where}: variable "${name}" is a boolean; write 0 or 1 (a variable is a number or a string); ignored`);
2198
+ continue;
2199
+ }
2200
+ if (typeof value === "number" && Number.isFinite(value)) out[name] = value;
2201
+ else if (typeof value === "string") out[name] = value;
2202
+ else warn(`pixi-effects: ${where}: variable "${name}" must be a number or a string, got ${JSON.stringify(value)}; ignored`);
2203
+ }
2204
+ return out;
2205
+ }
2206
+ function numericVars(vars) {
2207
+ const out = {};
2208
+ for (const [k, v] of Object.entries(vars)) if (typeof v === "number") out[k] = v;
2209
+ return out;
2210
+ }
2211
+ var TOKEN = /\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
2212
+ function substitute(s, vars) {
2213
+ return s.replace(TOKEN, (whole, name) => Object.prototype.hasOwnProperty.call(vars, name) ? String(vars[name]) : whole);
2214
+ }
2215
+ function applyVars(spec, vars) {
2216
+ if (Object.keys(vars).length === 0) return spec;
2217
+ const walk2 = (v) => {
2218
+ if (typeof v === "string") return substitute(v, vars);
2219
+ if (Array.isArray(v)) return v.map(walk2);
2220
+ if (isPlain(v)) {
2221
+ const o = {};
2222
+ for (const [k, x] of Object.entries(v)) o[k] = walk2(x);
2223
+ return o;
2224
+ }
2225
+ return v;
2226
+ };
2227
+ return walk2(spec);
2228
+ }
2229
+ var escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2230
+ function lintVars(spec, vars) {
2231
+ const declared = Object.keys(vars);
2232
+ const strings = [];
2233
+ const walk2 = (v) => {
2234
+ if (typeof v === "string") strings.push(v);
2235
+ else if (Array.isArray(v)) v.forEach(walk2);
2236
+ else if (isPlain(v)) Object.values(v).forEach(walk2);
2237
+ };
2238
+ walk2(spec);
2239
+ const out = [];
2240
+ for (const name of declared) {
2241
+ const word = new RegExp(`(?<![A-Za-z0-9_.])${escapeRe(name)}(?![A-Za-z0-9_])`);
2242
+ if (!strings.some((s) => word.test(s))) out.push(`pixi-effects: vars: variable "${name}" is declared but nothing uses it (write {${name}} in a string, or "${name}" in an expression); a typo on one side?`);
2243
+ }
2244
+ const seen = /* @__PURE__ */ new Set();
2245
+ for (const s of strings) {
2246
+ for (const m of s.matchAll(TOKEN)) {
2247
+ const name = m[1];
2248
+ if (seen.has(name) || Object.prototype.hasOwnProperty.call(vars, name) || RESERVED_NAMES.includes(name) || declared.length === 0) continue;
2249
+ seen.add(name);
2250
+ const guess = suggestName(name, declared);
2251
+ out.push(`pixi-effects: vars: {${name}} is not a declared variable${guess ? ` \u2014 did you mean "${guess}"?` : ` (declared: ${declared.join(", ")})`}`);
2252
+ }
2253
+ }
2254
+ return out;
2255
+ }
2256
+
2257
+ // src/core/mixCache.ts
2258
+ var ids = /* @__PURE__ */ new WeakMap();
2259
+ var nextId = 1;
2260
+ var idOf = (o) => {
2261
+ let n = ids.get(o);
2262
+ if (!n) {
2263
+ n = nextId++;
2264
+ ids.set(o, n);
2265
+ }
2266
+ return n;
2267
+ };
2268
+ function mixKey(audios, duration, sampleRate) {
2269
+ const parts = [];
2270
+ for (const a of audios) {
2271
+ if (a.warp || a.sourceMap) return null;
2272
+ const what = a.buffer ? `b${idOf(a.buffer)}` : a.synth ? `s${a.synth.key}` : null;
2273
+ if (what === null) return null;
2274
+ parts.push([what, a.loop, a.start, a.end, a.initialVolume, a.volumeKeyframes.map((k) => [k.time, k.value])]);
2275
+ }
2276
+ return JSON.stringify([duration, sampleRate, parts]);
2277
+ }
2278
+ var MixCache = class {
2279
+ key = null;
2280
+ buffer = null;
2281
+ extra;
2282
+ get(key) {
2283
+ if (key === null || key !== this.key || !this.buffer) return null;
2284
+ return { buffer: this.buffer, extra: this.extra };
2285
+ }
2286
+ /** Keep this mix for `key`; a null key (not comparable) keeps nothing. */
2287
+ set(key, buffer, extra) {
2288
+ this.key = key;
2289
+ this.buffer = key === null ? null : buffer;
2290
+ this.extra = extra;
2291
+ }
2292
+ };
2293
+
2174
2294
  // src/core/startWhenRunning.ts
2175
2295
  function startWhenRunning(ctx, start, gesture) {
2176
2296
  if (ctx.state === "running") {
@@ -2454,6 +2574,7 @@ var Movie = class {
2454
2574
  _quiet = 0;
2455
2575
  _volume = 1;
2456
2576
  _muted = false;
2577
+ _loop = false;
2457
2578
  isPlaying = false;
2458
2579
  currentFrame = 0;
2459
2580
  totalFrames = 0;
@@ -2475,6 +2596,21 @@ var Movie = class {
2475
2596
  }
2476
2597
  _rootContainer = null;
2477
2598
  _raf = null;
2599
+ /** The variables now, and the names that were declared (a `setVars` of any other name is refused). */
2600
+ _vars = {};
2601
+ _declared = /* @__PURE__ */ new Set();
2602
+ /** The composition as the author wrote it (before `{name}` is replaced): a rebuild starts from it. */
2603
+ _composition;
2604
+ /** True once `resize()` was called: the size of the movie then replaces an explicit `width` / `height` of the composition. */
2605
+ _resized = false;
2606
+ _mixCache = new MixCache();
2607
+ _rebuildRun = null;
2608
+ _rebuildNext = null;
2609
+ /** > 0 while a rebuild stops and restarts playback: `play` / `pause` are not announced then (a player's button must not flicker). */
2610
+ _silent = 0;
2611
+ /** render() / peek() in flight: a rebuild does not swap the composition under them. */
2612
+ _busy = 0;
2613
+ _idleWaiters = [];
2478
2614
  /** What went into the mix, and its peak before limiting — kept for inspectAudio(). */
2479
2615
  _audioSources = [];
2480
2616
  _mixStats = null;
@@ -2550,7 +2686,7 @@ var Movie = class {
2550
2686
  return this._audioContext;
2551
2687
  }
2552
2688
  async init(options = {}) {
2553
- warnUnknownOptions("movie.init()", options, ["width", "height", "duration", "frameRate", "background", "canvas", "assets", "composition", "poster", "motionBlur", "loader"]);
2689
+ warnUnknownOptions("movie.init()", options, ["width", "height", "duration", "frameRate", "background", "canvas", "assets", "composition", "vars", "poster", "motionBlur", "loader", "loop"]);
2554
2690
  this._initState = "pending";
2555
2691
  try {
2556
2692
  this.width = options.width ?? 1920;
@@ -2561,7 +2697,14 @@ var Movie = class {
2561
2697
  const poster = normalizePoster(options.poster, this.duration, this.frameRate);
2562
2698
  this.poster = poster.seconds;
2563
2699
  this.motionBlur = resolveMotionBlur(options.motionBlur, "movie.init()");
2564
- this.stops = normalizeStops(options.composition?.stops, this.duration, this.frameRate);
2700
+ this._loop = options.loop === true;
2701
+ this._vars = normalizeVars(options.vars, "movie.init()");
2702
+ this._declared = new Set(Object.keys(this._vars));
2703
+ this._composition = options.composition;
2704
+ this._resized = false;
2705
+ for (const w of lintVars(options.composition, this._vars)) console.warn(w);
2706
+ this._mixCache.set(null, null);
2707
+ this.stops = normalizeStops(applyVars(options.composition, this._vars)?.stops, this.duration, this.frameRate);
2565
2708
  this._posterFrame = poster.frame;
2566
2709
  this.background = options.background ?? "#000000";
2567
2710
  this.timeline = createTimeline({ paused: true, defaults: { ease: "none" } });
@@ -2584,7 +2727,6 @@ var Movie = class {
2584
2727
  this.app = new Application();
2585
2728
  await this.app.init({ ...baseAppOptions, preference: "webgl" });
2586
2729
  }
2587
- if (usesAdvancedBlend(options.composition)) await enableAdvancedBlend(this.app.renderer);
2588
2730
  const root = new Container();
2589
2731
  root.cullable = true;
2590
2732
  root.cullableChildren = true;
@@ -2608,51 +2750,13 @@ var Movie = class {
2608
2750
  assetsSeen = n;
2609
2751
  }
2610
2752
  });
2611
- const rootShape = {
2612
- width: this.width,
2613
- height: this.height,
2614
- duration: this.duration,
2615
- frameRate: this.frameRate,
2616
- onLayerBuilt: async () => {
2617
- lp.tick();
2618
- await lp.yieldIfDue();
2619
- }
2620
- };
2621
- const seededComposition = options.composition ? {
2622
- ...options.composition,
2623
- width: options.composition.width ?? this.width,
2624
- height: options.composition.height ?? this.height,
2625
- duration: options.composition.duration ?? this.duration
2626
- } : void 0;
2627
- await ensureFilterLibrary(seededComposition);
2628
- const userComposition = seededComposition ? expandTransitions(seededComposition) : void 0;
2629
- const rootSeqSpec = {
2630
- type: "composition",
2631
- width: this.width,
2632
- height: this.height,
2633
- duration: this.duration,
2634
- ...userComposition
2635
- };
2636
- if (userComposition) carryTransitionWindows(userComposition, rootSeqSpec);
2637
- const composition = new CompositionSequence(rootSeqSpec, null, rootShape);
2638
- lp.begin("build", countLayers(rootSeqSpec));
2639
- await composition.build();
2753
+ const composition = await this._construct(this._composition, this._vars, this.width, this.height, lp);
2640
2754
  this._rootSequence = composition;
2641
2755
  if (composition.target) root.addChild(composition.target);
2642
2756
  composition.bindTimeline(this.timeline);
2643
2757
  this._timeFilters = collectTimeFilters(composition);
2644
- const audios = [];
2645
- composition.collectAudio(audios, 0);
2646
2758
  lp.begin("sound", 1);
2647
- if (audios.length > 0) {
2648
- this.audioBuffer = await mixdown(audios, this.duration, audioContext.sampleRate, async (done) => {
2649
- lp.part(done);
2650
- await lp.yieldIfDue();
2651
- });
2652
- lp.tick();
2653
- if (this.audioBuffer) this._mixStats = limitMix(this.audioBuffer, audios);
2654
- }
2655
- this._audioSources = audios;
2759
+ await this._mixSound(composition, audioContext, lp);
2656
2760
  lp.begin("frames", 0);
2657
2761
  this.timeline.time(this._timeOf(0));
2658
2762
  await this._awaitVideoFrames();
@@ -2675,6 +2779,254 @@ var Movie = class {
2675
2779
  throw err;
2676
2780
  }
2677
2781
  }
2782
+ /**
2783
+ * Build the composition tree for these variables and this size, off stage: `{name}` replaced, the root shape (with the numeric variables for the
2784
+ * expressions), transitions expanded, every layer built. Used by `init` and by `rebuild`.
2785
+ */
2786
+ async _construct(composition, vars, width, height, lp, sizeWins = this._resized) {
2787
+ const spec = applyVars(composition, vars);
2788
+ const rootShape = {
2789
+ width,
2790
+ height,
2791
+ duration: this.duration,
2792
+ frameRate: this.frameRate,
2793
+ vars: numericVars(vars),
2794
+ ...lp ? { onLayerBuilt: async () => {
2795
+ lp.tick();
2796
+ await lp.yieldIfDue();
2797
+ } } : {}
2798
+ };
2799
+ const seededComposition = spec ? {
2800
+ ...spec,
2801
+ width: sizeWins ? width : spec.width ?? width,
2802
+ height: sizeWins ? height : spec.height ?? height,
2803
+ duration: spec.duration ?? this.duration
2804
+ } : void 0;
2805
+ await ensureFilterLibrary(seededComposition);
2806
+ if (usesAdvancedBlend(seededComposition)) await enableAdvancedBlend(this.app.renderer);
2807
+ const userComposition = seededComposition ? expandTransitions(seededComposition) : void 0;
2808
+ const rootSeqSpec = {
2809
+ type: "composition",
2810
+ width,
2811
+ height,
2812
+ duration: this.duration,
2813
+ ...userComposition
2814
+ };
2815
+ if (userComposition) carryTransitionWindows(userComposition, rootSeqSpec);
2816
+ const sequence = new CompositionSequence(rootSeqSpec, null, rootShape);
2817
+ lp?.begin("build", countLayers(rootSeqSpec));
2818
+ try {
2819
+ await sequence.build();
2820
+ } catch (err) {
2821
+ safeRun(() => sequence.destroy());
2822
+ throw err;
2823
+ }
2824
+ return sequence;
2825
+ }
2826
+ /** Make the sound of a built composition: the same mix as last time when the sounds are the same (`'reused'`), else mix it. */
2827
+ async _mixSound(sequence, audioContext, lp) {
2828
+ const audios = [];
2829
+ sequence.collectAudio(audios, 0);
2830
+ let status = "none";
2831
+ if (audios.length === 0) {
2832
+ this.audioBuffer = null;
2833
+ this._mixStats = null;
2834
+ this._mixCache.set(null, null);
2835
+ } else {
2836
+ const key = mixKey(audios, this.duration, audioContext.sampleRate);
2837
+ const hit = this._mixCache.get(key);
2838
+ if (hit) {
2839
+ this.audioBuffer = hit.buffer;
2840
+ this._mixStats = hit.extra;
2841
+ status = "reused";
2842
+ } else {
2843
+ this.audioBuffer = await mixdown(audios, this.duration, audioContext.sampleRate, lp ? async (done) => {
2844
+ lp.part(done);
2845
+ await lp.yieldIfDue();
2846
+ } : void 0);
2847
+ this._mixStats = this.audioBuffer ? limitMix(this.audioBuffer, audios) : null;
2848
+ this._mixCache.set(key, this.audioBuffer, this._mixStats);
2849
+ status = "mixed";
2850
+ }
2851
+ lp?.tick();
2852
+ }
2853
+ this._audioSources = audios;
2854
+ return status;
2855
+ }
2856
+ /** The variables now (a copy; change them with `setVars()`). */
2857
+ get vars() {
2858
+ return { ...this._vars };
2859
+ }
2860
+ /**
2861
+ * Change variables and rebuild at the same time. Only the declared names (`movie.init({ vars })`) are accepted; another name says which one it
2862
+ * looks like and is ignored. Values it already has change nothing: no rebuild (`unchanged: true`).
2863
+ */
2864
+ async setVars(changes) {
2865
+ this._requireReady("setVars");
2866
+ return this.rebuild({ vars: changes }, "vars");
2867
+ }
2868
+ /** Make the movie `width` x `height` pixels (the canvas, and `W` / `H` / `GW` / `GH` in every expression) and rebuild at the same time. */
2869
+ async resize(width, height) {
2870
+ this._requireReady("resize");
2871
+ return this.rebuild({ width, height }, "resize");
2872
+ }
2873
+ /**
2874
+ * Build the composition again, with what `change` replaces (variables, the composition, the size), and put the movie back where it was: the
2875
+ * same frame, playing or not, the poster if that was shown. The new composition is built first: when that fails the old one stays, `error` is
2876
+ * emitted and the promise rejects. The sound is mixed again only when it changed (`audio: 'reused'` otherwise). Calls that arrive while a
2877
+ * rebuild runs are joined into one more rebuild (the latest values win), so a slider can call it on every move.
2878
+ */
2879
+ async rebuild(change = {}, reason = "rebuild") {
2880
+ this._requireReady("rebuild");
2881
+ warnUnknownOptions("movie.rebuild()", change, ["composition", "vars", "width", "height"]);
2882
+ if (this._rebuildRun) {
2883
+ if (!this._rebuildNext) {
2884
+ let resolve, reject;
2885
+ const promise = new Promise((res, rej) => {
2886
+ resolve = res;
2887
+ reject = rej;
2888
+ });
2889
+ this._rebuildNext = { change: {}, reason, promise, resolve, reject };
2890
+ }
2891
+ const next = this._rebuildNext;
2892
+ next.change = { ...next.change, ...change, vars: { ...next.change.vars ?? {}, ...change.vars ?? {} } };
2893
+ if (reason !== "vars") next.reason = reason;
2894
+ return next.promise;
2895
+ }
2896
+ return this._runRebuild(change, reason);
2897
+ }
2898
+ _runRebuild(change, reason) {
2899
+ const run = this._doRebuild(change, reason);
2900
+ this._rebuildRun = run;
2901
+ const settled = () => {
2902
+ if (this._rebuildRun === run) this._rebuildRun = null;
2903
+ const next = this._rebuildNext;
2904
+ if (next && this._initState === "ready") {
2905
+ this._rebuildNext = null;
2906
+ this._runRebuild(next.change, next.reason).then(next.resolve, next.reject);
2907
+ } else if (next) {
2908
+ this._rebuildNext = null;
2909
+ next.reject(new Error("pixi-effects: the movie was destroyed before the rebuild could run"));
2910
+ }
2911
+ };
2912
+ run.then(settled, settled);
2913
+ return run;
2914
+ }
2915
+ async _doRebuild(change, reason) {
2916
+ const started = performance.now();
2917
+ const root = this._rootContainer, app = this.app;
2918
+ if (!root || !app) throw new Error("pixi-effects: movie.rebuild() needs a ready movie \u2014 await movie.init(...) first");
2919
+ const who = `movie.${reason === "vars" ? "setVars" : "rebuild"}()`;
2920
+ let sequence = null;
2921
+ const alive = () => {
2922
+ if (this._initState !== "ready") throw new Error("pixi-effects: the movie was destroyed while it was being rebuilt");
2923
+ };
2924
+ try {
2925
+ const vars = { ...this._vars };
2926
+ let changed = false;
2927
+ if (change.vars !== void 0) {
2928
+ const given = normalizeVars(change.vars, who);
2929
+ for (const [name, value] of Object.entries(given)) {
2930
+ if (!this._declared.has(name)) {
2931
+ const guess = suggestName(name, [...this._declared]);
2932
+ console.warn(`pixi-effects: ${who}: "${name}" is not a declared variable${guess ? ` \u2014 did you mean "${guess}"?` : ""} (declared in movie.init({ vars }): ${[...this._declared].join(", ") || "none"}); ignored`);
2933
+ continue;
2934
+ }
2935
+ if (vars[name] !== value) {
2936
+ vars[name] = value;
2937
+ changed = true;
2938
+ }
2939
+ }
2940
+ }
2941
+ for (const [k, v] of [["width", change.width], ["height", change.height]]) {
2942
+ if (v !== void 0 && !(Number.isFinite(v) && v >= 1)) throw new Error(`pixi-effects: movie.rebuild(): ${k} must be a number of pixels (1 or more), got ${JSON.stringify(v)}`);
2943
+ }
2944
+ const width = change.width ?? this.width, height = change.height ?? this.height;
2945
+ const sizeChanged = width !== this.width || height !== this.height;
2946
+ const composition = change.composition ?? this._composition;
2947
+ if (change.composition !== void 0) for (const w of lintVars(change.composition, vars)) console.warn(w);
2948
+ changed = changed || sizeChanged || change.composition !== void 0 || reason === "rebuild";
2949
+ if (!changed) return { reason, ms: 0, audio: "none", frame: this.currentFrame, playing: this.isPlaying, unchanged: true };
2950
+ sequence = await this._construct(composition, vars, width, height, void 0, this._resized || sizeChanged);
2951
+ alive();
2952
+ const timeline = createTimeline({ paused: true, defaults: { ease: "none" } });
2953
+ lengthen(timeline, this.duration);
2954
+ sequence.bindTimeline(timeline);
2955
+ const hadSound = this.audioBuffer !== null;
2956
+ const audio = await this._mixSound(sequence, this._ensureAudioContext());
2957
+ const soundChanged = audio === "mixed" || audio === "none" && hadSound;
2958
+ while (this._busy > 0) {
2959
+ await new Promise((r) => this._idleWaiters.push(r));
2960
+ }
2961
+ alive();
2962
+ const wasPlaying = this.isPlaying, frame = this.currentFrame, wasAtPoster = this._atPoster;
2963
+ const oldSize = { w: this.width, h: this.height }, oldResized = this._resized;
2964
+ const goOn = wasPlaying && !soundChanged;
2965
+ this._silent++;
2966
+ try {
2967
+ if (goOn) this._cancelUntil();
2968
+ else this.pause();
2969
+ if (sizeChanged) {
2970
+ try {
2971
+ this.width = width;
2972
+ this.height = height;
2973
+ this._resized = true;
2974
+ app.renderer.resize(width, height);
2975
+ root.cullArea = new Rectangle(0, 0, width, height);
2976
+ } catch (e) {
2977
+ this.width = oldSize.w;
2978
+ this.height = oldSize.h;
2979
+ this._resized = oldResized;
2980
+ safeRun(() => app.renderer.resize(oldSize.w, oldSize.h));
2981
+ root.cullArea = new Rectangle(0, 0, oldSize.w, oldSize.h);
2982
+ if (wasPlaying && !goOn) this.play();
2983
+ throw e;
2984
+ }
2985
+ }
2986
+ const old = this._rootSequence, oldTimeline = this.timeline;
2987
+ if (old?.target) root.removeChild(old.target);
2988
+ if (sequence.target) root.addChild(sequence.target);
2989
+ this._rootSequence = sequence;
2990
+ this.timeline = timeline;
2991
+ this._timeFilters = collectTimeFilters(sequence);
2992
+ const unmute = muteBoundTextureWarning();
2993
+ try {
2994
+ safeRun(() => old?.destroy());
2995
+ } finally {
2996
+ unmute();
2997
+ }
2998
+ safeRun(() => oldTimeline?.kill());
2999
+ this._vars = vars;
3000
+ this._composition = composition;
3001
+ this.stops = normalizeStops(applyVars(composition, vars)?.stops, this.duration, this.frameRate);
3002
+ sequence = null;
3003
+ } finally {
3004
+ this._silent--;
3005
+ }
3006
+ try {
3007
+ if (wasAtPoster) await this._showPoster();
3008
+ else await this._quietly(() => this.gotoFrame(frame, true));
3009
+ } catch (e) {
3010
+ console.warn("pixi-effects: could not draw the frame after a rebuild:", e);
3011
+ this._emitError("playback", e);
3012
+ }
3013
+ if (wasPlaying && !goOn && this._initState === "ready" && !this.isPlaying) {
3014
+ this._silent++;
3015
+ try {
3016
+ this.play();
3017
+ } finally {
3018
+ this._silent--;
3019
+ }
3020
+ }
3021
+ const report = { reason, ms: Math.round((performance.now() - started) * 10) / 10, audio, frame, playing: wasPlaying };
3022
+ this.emit("rebuilt", report);
3023
+ return report;
3024
+ } catch (err) {
3025
+ if (sequence) safeRun(() => sequence.destroy());
3026
+ this._emitError("rebuild", err);
3027
+ throw err;
3028
+ }
3029
+ }
2678
3030
  async gotoFrame(frame, force = false) {
2679
3031
  if (this._initState !== "ready") return;
2680
3032
  this._cancelUntil();
@@ -2987,13 +3339,30 @@ var Movie = class {
2987
3339
  */
2988
3340
  async peek(fn) {
2989
3341
  this._requireReady("peek");
2990
- this.pause();
2991
- const back = this.currentFrame, wasAtPoster = this._atPoster;
3342
+ return this._exclusive(async () => {
3343
+ this.pause();
3344
+ const back = this.currentFrame, wasAtPoster = this._atPoster;
3345
+ try {
3346
+ return await this._quietly(fn);
3347
+ } finally {
3348
+ if (wasAtPoster) await this._showPoster();
3349
+ else await this._quietly(() => this.gotoFrame(back, true));
3350
+ }
3351
+ });
3352
+ }
3353
+ /** Run `fn` as the only thing that moves the composition: waits for a rebuild in flight, and holds the next one's swap until `fn` is done. */
3354
+ async _exclusive(fn) {
3355
+ while (this._rebuildRun) await this._rebuildRun.catch(() => {
3356
+ });
3357
+ this._busy++;
2992
3358
  try {
2993
- return await this._quietly(fn);
3359
+ return await fn();
2994
3360
  } finally {
2995
- if (wasAtPoster) await this._showPoster();
2996
- else await this._quietly(() => this.gotoFrame(back, true));
3361
+ if (--this._busy === 0) {
3362
+ const waiting = this._idleWaiters;
3363
+ this._idleWaiters = [];
3364
+ waiting.forEach((w) => w());
3365
+ }
2997
3366
  }
2998
3367
  }
2999
3368
  _settleUntil(landed) {
@@ -3027,6 +3396,7 @@ var Movie = class {
3027
3396
  if (this.currentFrame >= this.totalFrames) this.currentFrame = 0;
3028
3397
  const wasPlaying = this.isPlaying;
3029
3398
  this.isPlaying = true;
3399
+ const startFrame = this.currentFrame;
3030
3400
  const startTime = performance.now() - this.currentFrame / this.frameRate * 1e3;
3031
3401
  let inFlight = false;
3032
3402
  const tick = (time) => {
@@ -3036,7 +3406,7 @@ var Movie = class {
3036
3406
  return;
3037
3407
  }
3038
3408
  const elapsed = (time - startTime) / 1e3;
3039
- const frame = Math.floor(elapsed * this.frameRate);
3409
+ const frame = Math.max(startFrame, Math.floor(elapsed * this.frameRate));
3040
3410
  if (this._until !== null && frame >= this._until) {
3041
3411
  void this._arriveUntil();
3042
3412
  return;
@@ -3050,6 +3420,18 @@ var Movie = class {
3050
3420
  inFlight = false;
3051
3421
  });
3052
3422
  this._raf = requestAnimationFrame(tick);
3423
+ } else if (this._loop && this._untilWaiters.length === 0) {
3424
+ this._silent++;
3425
+ try {
3426
+ this.pause();
3427
+ this._goto(0).catch((err) => {
3428
+ console.warn("pixi-effects: gotoFrame failed at the loop:", err);
3429
+ this._emitError("playback", err);
3430
+ });
3431
+ this.play();
3432
+ } finally {
3433
+ this._silent--;
3434
+ }
3053
3435
  } else {
3054
3436
  const toEnd = this._untilWaiters;
3055
3437
  this._untilWaiters = [];
@@ -3078,7 +3460,7 @@ var Movie = class {
3078
3460
  this.audioSource.start(0, Math.min(this.currentFrame / this.frameRate, buffer.duration));
3079
3461
  }, typeof window !== "undefined" ? window : void 0);
3080
3462
  }
3081
- if (!wasPlaying) this.emit("play");
3463
+ if (!wasPlaying && !this._silent) this.emit("play");
3082
3464
  }
3083
3465
  pause() {
3084
3466
  const wasPlaying = this.isPlaying;
@@ -3101,7 +3483,7 @@ var Movie = class {
3101
3483
  this.gainNode.disconnect();
3102
3484
  this.gainNode = null;
3103
3485
  }
3104
- if (wasPlaying) this.emit("pause");
3486
+ if (wasPlaying && !this._silent) this.emit("pause");
3105
3487
  this._cancelUntil();
3106
3488
  }
3107
3489
  set volume(v) {
@@ -3127,10 +3509,23 @@ var Movie = class {
3127
3509
  this.muted = !this.muted;
3128
3510
  return this.muted;
3129
3511
  }
3512
+ /**
3513
+ * At the end of an ordinary `play()` the movie goes back to the start and plays on (no `ended`, no `pause` / `play` events at the wrap; the sound
3514
+ * starts again). Default false. `playUntil()` and `render()` ignore it. Can be changed at any time; `loopchange` says so.
3515
+ */
3516
+ set loop(v) {
3517
+ const next = !!v;
3518
+ if (next === this._loop) return;
3519
+ this._loop = next;
3520
+ this.emit("loopchange", { loop: next });
3521
+ }
3522
+ get loop() {
3523
+ return this._loop;
3524
+ }
3130
3525
  async render(options) {
3131
3526
  warnUnknownOptions("movie.render()", options, ["format", "video", "audio", "motionBlur", "range", "scale", "draft"]);
3132
3527
  try {
3133
- return await this._quietly(() => exportFrames(this, options));
3528
+ return await this._exclusive(() => this._quietly(() => exportFrames(this, options)));
3134
3529
  } catch (err) {
3135
3530
  this._emitError("render", err);
3136
3531
  throw err;
@@ -3148,6 +3543,7 @@ var Movie = class {
3148
3543
  this.audioBuffer = null;
3149
3544
  this._audioSources = [];
3150
3545
  this._mixStats = null;
3546
+ this._mixCache.set(null, null);
3151
3547
  safeRun(() => this._audioContext?.close().catch(() => {
3152
3548
  }));
3153
3549
  this._audioContext = null;
@@ -3155,6 +3551,17 @@ var Movie = class {
3155
3551
  }
3156
3552
  };
3157
3553
  var END_MARGIN = 1e-4;
3554
+ function muteBoundTextureWarning() {
3555
+ const original = console.warn;
3556
+ const ours = (...args) => {
3557
+ if (args.some((a) => /destroyed while still bound/.test(String(a)))) return;
3558
+ original.apply(console, args);
3559
+ };
3560
+ console.warn = ours;
3561
+ return () => {
3562
+ if (console.warn === ours) console.warn = original;
3563
+ };
3564
+ }
3158
3565
  var clampFrame = (f, last) => Math.max(0, Math.min(Math.round(f), last));
3159
3566
  function encodeCanvas(canvas, type, as, quality) {
3160
3567
  if (as === "dataURL") return Promise.resolve(canvas.toDataURL(type, quality));
@@ -3183,6 +3590,8 @@ function countLayers(spec) {
3183
3590
  for (const s of spec?.sequences ?? []) {
3184
3591
  const mask = s.mask;
3185
3592
  n += 1 + countLayers(s);
3593
+ const spans = s.spans;
3594
+ if (Array.isArray(spans)) n += spans.length;
3186
3595
  const src = maskSourceOf(mask);
3187
3596
  if (src.kind === "inline") n += 1 + countLayers(src.spec);
3188
3597
  }
@@ -4178,5 +4587,5 @@ function withFade(spec, opts) {
4178
4587
  }
4179
4588
 
4180
4589
  export { Movie, animateText, audioEnvelope, bpmEnvelope, cameraPath, computeEnvelope, dismissLoader, failLoader, followPath, kenBurns, measureText, musicBuffer, musicEnvelope, musicEvents, orbit, parseEase2 as parseEase, particles, react, resolveLoader, splitText, stagger, wiggle, withFade };
4181
- //# sourceMappingURL=chunk-CRQFDY7E.js.map
4182
- //# sourceMappingURL=chunk-CRQFDY7E.js.map
4590
+ //# sourceMappingURL=chunk-FCMOREIS.js.map
4591
+ //# sourceMappingURL=chunk-FCMOREIS.js.map