pixi-effects 0.4.0 → 0.5.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 (61) hide show
  1. package/CHANGELOG.md +30 -1
  2. package/README.md +9 -5
  3. package/ai/SKILL.md +7 -4
  4. package/ai/reference/cheatsheet.md +16 -5
  5. package/ai/reference/pitfalls.md +15 -11
  6. package/ai/reference/recipes.md +117 -0
  7. package/ai/template.html +6 -3
  8. package/ai/tools/check.mjs +385 -0
  9. package/dist/{Base-5R7C5pHR.d.cts → Base-CPSFBkPm.d.ts} +6 -1
  10. package/dist/{Base-DhY9oMxr.d.ts → Base-DnRAxroQ.d.cts} +6 -1
  11. package/dist/Composition-63EXHNJJ.cjs +13 -0
  12. package/dist/{Composition-D7RCAGEA.cjs.map → Composition-63EXHNJJ.cjs.map} +1 -1
  13. package/dist/Composition-TUCRSMJU.js +4 -0
  14. package/dist/{Composition-RGTRZ2JX.js.map → Composition-TUCRSMJU.js.map} +1 -1
  15. package/dist/Controller.cjs +37 -13
  16. package/dist/Controller.cjs.map +1 -1
  17. package/dist/Controller.d.cts +4 -2
  18. package/dist/Controller.d.ts +4 -2
  19. package/dist/Controller.js +37 -13
  20. package/dist/Controller.js.map +1 -1
  21. package/dist/{Movie-DRZtIGU7.d.ts → Movie-CuU_aaCm.d.cts} +56 -2
  22. package/dist/{Movie-CghFXprP.d.cts → Movie-VyDiUYOj.d.ts} +56 -2
  23. package/dist/{chunk-QGBOC5YL.cjs → chunk-23M5QTWW.cjs} +33 -21
  24. package/dist/chunk-23M5QTWW.cjs.map +1 -0
  25. package/dist/{chunk-6BQ6IEW2.cjs → chunk-2RR43OIN.cjs} +143 -30
  26. package/dist/chunk-2RR43OIN.cjs.map +1 -0
  27. package/dist/{chunk-A574IA4F.js → chunk-5Q5XZG7Z.js} +18 -6
  28. package/dist/chunk-5Q5XZG7Z.js.map +1 -0
  29. package/dist/chunk-DKGG7WD4.js +61 -0
  30. package/dist/chunk-DKGG7WD4.js.map +1 -0
  31. package/dist/{chunk-ZQX7WIYL.js → chunk-UBPZLGLU.js} +143 -31
  32. package/dist/chunk-UBPZLGLU.js.map +1 -0
  33. package/dist/chunk-UQBGW5DO.cjs +64 -0
  34. package/dist/chunk-UQBGW5DO.cjs.map +1 -0
  35. package/dist/index.cjs +291 -86
  36. package/dist/index.cjs.map +1 -1
  37. package/dist/index.d.cts +54 -5
  38. package/dist/index.d.ts +54 -5
  39. package/dist/index.js +275 -72
  40. package/dist/index.js.map +1 -1
  41. package/dist/sfx-MZJZXYY3.cjs +446 -0
  42. package/dist/sfx-MZJZXYY3.cjs.map +1 -0
  43. package/dist/sfx-S37DDC66.js +434 -0
  44. package/dist/sfx-S37DDC66.js.map +1 -0
  45. package/dist/three.cjs +5 -5
  46. package/dist/three.d.cts +2 -2
  47. package/dist/three.d.ts +2 -2
  48. package/dist/three.js +1 -1
  49. package/dist/{types-CRE9WKd4.d.ts → types-7VtTEKUW.d.cts} +96 -6
  50. package/dist/{types-CRE9WKd4.d.cts → types-7VtTEKUW.d.ts} +96 -6
  51. package/docs/api.md +36 -0
  52. package/docs/dsl.md +44 -3
  53. package/llms-full.txt +234 -22
  54. package/llms.txt +1 -1
  55. package/package.json +2 -1
  56. package/dist/Composition-D7RCAGEA.cjs +0 -13
  57. package/dist/Composition-RGTRZ2JX.js +0 -4
  58. package/dist/chunk-6BQ6IEW2.cjs.map +0 -1
  59. package/dist/chunk-A574IA4F.js.map +0 -1
  60. package/dist/chunk-QGBOC5YL.cjs.map +0 -1
  61. package/dist/chunk-ZQX7WIYL.js.map +0 -1
package/dist/index.cjs CHANGED
@@ -1,7 +1,8 @@
1
1
  'use strict';
2
2
 
3
- var chunkQGBOC5YL_cjs = require('./chunk-QGBOC5YL.cjs');
4
- var chunk6BQ6IEW2_cjs = require('./chunk-6BQ6IEW2.cjs');
3
+ var chunkUQBGW5DO_cjs = require('./chunk-UQBGW5DO.cjs');
4
+ var chunk23M5QTWW_cjs = require('./chunk-23M5QTWW.cjs');
5
+ var chunk2RR43OIN_cjs = require('./chunk-2RR43OIN.cjs');
5
6
  var PIXI = require('pixi.js');
6
7
  var gsap = require('gsap');
7
8
  var PixiPlugin = require('gsap/PixiPlugin');
@@ -88,21 +89,213 @@ async function loadAssetBundle(assets, audioContext) {
88
89
  async function mixdown(audios, totalDuration, sampleRate = 44100) {
89
90
  if (audios.length === 0) return null;
90
91
  const ctx = new OfflineAudioContext(2, Math.ceil(sampleRate * totalDuration), sampleRate);
92
+ const synthesised = /* @__PURE__ */ new Map();
93
+ const bufferOf = (a) => {
94
+ if (a.buffer) return a.buffer;
95
+ if (!a.synth) return null;
96
+ let buf = synthesised.get(a.synth.key);
97
+ if (!buf) {
98
+ const [left, right] = a.synth.render(sampleRate);
99
+ buf = ctx.createBuffer(2, left.length, sampleRate);
100
+ buf.getChannelData(0).set(left);
101
+ buf.getChannelData(1).set(right);
102
+ synthesised.set(a.synth.key, buf);
103
+ }
104
+ return buf;
105
+ };
91
106
  for (const a of audios) {
107
+ const buffer = bufferOf(a);
108
+ if (!buffer) continue;
109
+ const begin = Math.max(0, a.start);
110
+ const skipped = begin - a.start;
111
+ if (skipped > 0) {
112
+ console.warn(
113
+ `pixi-effects: ${a.layer ?? "an audio layer"} starts at ${a.start.toFixed(2)}s, before the movie starts, so its first ${skipped.toFixed(2)}s are cut off. Start it at 0 or later.`
114
+ );
115
+ }
92
116
  const src = ctx.createBufferSource();
93
- src.buffer = a.buffer;
117
+ src.buffer = buffer;
94
118
  src.loop = !!a.loop;
95
119
  const gain = ctx.createGain();
96
- gain.gain.setValueAtTime(a.initialVolume ?? 1, a.start);
120
+ gain.gain.setValueAtTime(a.initialVolume ?? 1, begin);
97
121
  for (const kf of a.volumeKeyframes ?? []) {
98
- gain.gain.linearRampToValueAtTime(kf.value, kf.time);
122
+ gain.gain.linearRampToValueAtTime(kf.value, Math.max(0, kf.time));
99
123
  }
100
124
  src.connect(gain).connect(ctx.destination);
101
- src.start(a.start);
102
- src.stop(a.end);
125
+ src.start(begin, skipped);
126
+ src.stop(Math.max(begin, a.end));
103
127
  }
104
128
  return await ctx.startRendering();
105
129
  }
130
+ var KNEE = 0.9;
131
+ function limitMix(buffer, audios) {
132
+ let peak = 0;
133
+ let peakIndex = 0;
134
+ for (let ch = 0; ch < buffer.numberOfChannels; ch++) {
135
+ const d = buffer.getChannelData(ch);
136
+ for (let i = 0; i < d.length; i++) {
137
+ const x = Math.abs(d[i]);
138
+ if (x > peak) {
139
+ peak = x;
140
+ peakIndex = i;
141
+ }
142
+ if (x > KNEE) d[i] = Math.sign(d[i]) * (KNEE + (1 - KNEE) * Math.tanh((x - KNEE) / (1 - KNEE)));
143
+ }
144
+ }
145
+ const peakAt = peakIndex / buffer.sampleRate;
146
+ if (peak > 1) {
147
+ const playing = audios.filter((a) => a.start <= peakAt && peakAt < a.end).map((a) => a.layer ?? "an audio layer");
148
+ console.warn(
149
+ `pixi-effects: the audio mix peaks at ${peak.toFixed(2)} (${(20 * Math.log10(peak)).toFixed(1)} dBFS) at ${peakAt.toFixed(2)}s, so it was limited (it would distort). Playing there: ${[...new Set(playing)].join(", ")}. Lower their volume.`
150
+ );
151
+ }
152
+ return { peak, peakAt };
153
+ }
154
+
155
+ // src/core/spectrum.ts
156
+ function fft(re, im) {
157
+ const n = re.length;
158
+ for (let i = 1, j = 0; i < n; i++) {
159
+ let bit = n >> 1;
160
+ for (; j & bit; bit >>= 1) j ^= bit;
161
+ j ^= bit;
162
+ if (i < j) {
163
+ [re[i], re[j]] = [re[j], re[i]];
164
+ [im[i], im[j]] = [im[j], im[i]];
165
+ }
166
+ }
167
+ for (let len = 2; len <= n; len <<= 1) {
168
+ const ang = -2 * Math.PI / len;
169
+ for (let i = 0; i < n; i += len) {
170
+ for (let k = 0; k < len / 2; k++) {
171
+ const wr = Math.cos(ang * k), wi = Math.sin(ang * k);
172
+ const a = i + k, b = a + len / 2;
173
+ const xr = re[b] * wr - im[b] * wi, xi = re[b] * wi + im[b] * wr;
174
+ re[b] = re[a] - xr;
175
+ im[b] = im[a] - xi;
176
+ re[a] = re[a] + xr;
177
+ im[a] = im[a] + xi;
178
+ }
179
+ }
180
+ }
181
+ }
182
+ var FRAME = 2048;
183
+ function spectralCentroid(samples, sampleRate, from = 0, to = samples.length) {
184
+ let num = 0;
185
+ let den = 0;
186
+ const re = new Float64Array(FRAME);
187
+ const im = new Float64Array(FRAME);
188
+ const end = Math.min(to, samples.length);
189
+ for (let start = Math.max(0, from); start < end; start += FRAME / 2) {
190
+ for (let i = 0; i < FRAME; i++) {
191
+ const k = start + i;
192
+ const w = 0.5 - 0.5 * Math.cos(2 * Math.PI * i / (FRAME - 1));
193
+ re[i] = k < end ? samples[k] * w : 0;
194
+ im[i] = 0;
195
+ }
196
+ fft(re, im);
197
+ for (let b = 1; b < FRAME / 2; b++) {
198
+ const power = re[b] * re[b] + im[b] * im[b];
199
+ num += power * (b * sampleRate / FRAME);
200
+ den += power;
201
+ }
202
+ }
203
+ return den > 1e-9 ? Math.round(num / den / 10) * 10 : 0;
204
+ }
205
+
206
+ // src/core/inspectAudio.ts
207
+ var INAUDIBLE_DB = -50;
208
+ var db = (x) => Math.round(10 * Math.max(-120, 20 * Math.log10(x))) / 10;
209
+ var mono = (chans) => {
210
+ if (chans.length === 1) return chans[0];
211
+ const out = new Float32Array(chans[0].length);
212
+ for (const d of chans) for (let i = 0; i < out.length; i++) out[i] = out[i] + d[i] / chans.length;
213
+ return out;
214
+ };
215
+ function measure(chans, sampleRate) {
216
+ let peak = 0;
217
+ let at = 0;
218
+ for (const d of chans) for (let i = 0; i < d.length; i++) if (Math.abs(d[i]) > peak) {
219
+ peak = Math.abs(d[i]);
220
+ at = i;
221
+ }
222
+ return {
223
+ length: Math.round(chans[0].length / sampleRate * 1e3) / 1e3,
224
+ peakDb: db(peak),
225
+ loudestAt: Math.round(at / sampleRate * 1e3) / 1e3,
226
+ brightnessHz: spectralCentroid(mono(chans), sampleRate)
227
+ };
228
+ }
229
+ function analyzeAudio(mix, sources, stats, movieDuration, opts = {}) {
230
+ if (!mix) {
231
+ return { duration: movieDuration, sampleRate: 0, peakDb: -120, peakAt: 0, sources: [], windows: [], issues: ['no audio: the movie has no audio layers (sound effects need no files: { type: "audio", sfx: "pop", at: 1 })'] };
232
+ }
233
+ const sr = mix.sampleRate;
234
+ const chans = Array.from({ length: mix.numberOfChannels }, (_, c) => mix.getChannelData(c));
235
+ const mixMono = mono(chans);
236
+ const peakIn = (t0, t1) => {
237
+ let p = 0;
238
+ const i1 = Math.min(mix.length, Math.ceil(t1 * sr));
239
+ for (const d of chans) for (let i = Math.max(0, Math.floor(t0 * sr)); i < i1; i++) p = Math.max(p, Math.abs(d[i]));
240
+ return p;
241
+ };
242
+ const win = opts.window && opts.window > 0 ? Math.max(0.01, opts.window) : 0.1;
243
+ const windows2 = [];
244
+ for (let t = 0; t < movieDuration - 1e-9; t += win) {
245
+ const i0 = Math.floor(t * sr);
246
+ const i1 = Math.min(mix.length, Math.floor((t + win) * sr));
247
+ let sum = 0;
248
+ let peak = 0;
249
+ for (const d of chans) {
250
+ for (let i = i0; i < i1; i++) {
251
+ sum += d[i] * d[i];
252
+ peak = Math.max(peak, Math.abs(d[i]));
253
+ }
254
+ }
255
+ const n = Math.max(1, (i1 - i0) * chans.length);
256
+ windows2.push({ t: Math.round(t * 1e3) / 1e3, rmsDb: db(Math.sqrt(sum / n)), peakDb: db(peak), brightnessHz: spectralCentroid(mixMono, sr, i0, i1) });
257
+ }
258
+ const issues = [];
259
+ if (stats && stats.peak > 1) {
260
+ issues.push(`the mix peaks at ${db(stats.peak)} dBFS at ${stats.peakAt.toFixed(2)}s and was limited \u2014 lower the volume of the layers playing there`);
261
+ }
262
+ const rendered = /* @__PURE__ */ new Map();
263
+ const soundOf = (s) => {
264
+ if (s.synth) {
265
+ let m = rendered.get(s.synth.key);
266
+ if (!m) rendered.set(s.synth.key, m = measure(s.synth.render(sr), sr));
267
+ return m;
268
+ }
269
+ const b = s.buffer;
270
+ return measure(Array.from({ length: b.numberOfChannels }, (_, c) => b.getChannelData(c)), b.sampleRate);
271
+ };
272
+ const list = [...sources].sort((a, b) => a.start - b.start).map((s) => {
273
+ const layer = s.layer ?? "an audio layer";
274
+ const source = s.source ?? "audio";
275
+ const peakDb = db(peakIn(s.start, Math.min(s.end, movieDuration)));
276
+ const sound = soundOf(s);
277
+ const topVolume = Math.max(s.initialVolume ?? 1, ...(s.volumeKeyframes ?? []).map((k) => k.value));
278
+ const ownDb = db(Math.pow(10, sound.peakDb / 20) * Math.max(0, topVolume));
279
+ if (s.start >= movieDuration) {
280
+ issues.push(`${layer} (${source}) starts at ${s.start.toFixed(2)}s, after the movie ends (${movieDuration}s)`);
281
+ } else if (ownDb < INAUDIBLE_DB || peakDb < INAUDIBLE_DB) {
282
+ issues.push(`${layer} (${source}) is inaudible from ${s.start.toFixed(2)}s to ${s.end.toFixed(2)}s (its own peak ${ownDb} dBFS at volume ${topVolume}; mix peak there ${peakDb} dBFS) \u2014 check its volume`);
283
+ }
284
+ if (s.synth && s.end > movieDuration + 1e-6 && s.start < movieDuration) {
285
+ issues.push(`${layer} (${source}) is cut off by the end of the movie at ${movieDuration}s (it runs to ${s.end.toFixed(2)}s)`);
286
+ }
287
+ return { layer, source, start: s.start, end: s.end, peakDb, sound };
288
+ });
289
+ return {
290
+ duration: movieDuration,
291
+ sampleRate: sr,
292
+ peakDb: db(stats?.peak ?? peakIn(0, movieDuration)),
293
+ peakAt: stats?.peakAt ?? 0,
294
+ sources: list,
295
+ windows: windows2,
296
+ issues
297
+ };
298
+ }
106
299
  var VIDEO_CODEC_BY_FORMAT = {
107
300
  mp4: "avc",
108
301
  mov: "avc",
@@ -160,7 +353,7 @@ async function exportFrames(movie, options = {}) {
160
353
  const audioSource = new mediabunny.AudioBufferSource({
161
354
  codec: opts.audio.codec,
162
355
  bitrate: opts.audio.bitrate
163
- });
356
+ }, { startTimestamp: opts.audio.codec === "aac" ? -2112 / movie.audioBuffer.sampleRate : 0 });
164
357
  output.addAudioTrack(audioSource);
165
358
  await output.start();
166
359
  await audioSource.add(movie.audioBuffer);
@@ -626,16 +819,16 @@ function expandTransitions(spec) {
626
819
  if (toUses > 1) {
627
820
  throw new Error(`pixi-effects: ${tag} sequence "${t.to}" is used as \`to\` in more than one transition`);
628
821
  }
629
- const tStart = chunk6BQ6IEW2_cjs.resolveAt(t.at, parentDuration);
822
+ const tStart = chunk2RR43OIN_cjs.resolveAt(t.at, parentDuration);
630
823
  const tEnd = tStart + t.duration;
631
824
  const fromSeq = fromEntry.seq;
632
- const fromStart = chunk6BQ6IEW2_cjs.resolveAt(fromSeq.at, parentDuration);
825
+ const fromStart = chunk2RR43OIN_cjs.resolveAt(fromSeq.at, parentDuration);
633
826
  const fromEnd = fromStart + (fromSeq.duration ?? parentDuration);
634
827
  if (tStart < fromStart || tEnd > fromEnd) {
635
828
  throw new Error(`pixi-effects: ${tag} window [${tStart}, ${tEnd}] is not covered by \`from\` "${t.from}" (lives [${fromStart}, ${fromEnd}], so it ends at ${fromEnd})`);
636
829
  }
637
830
  const toSeq = toEntry.seq;
638
- const toStart = chunk6BQ6IEW2_cjs.resolveAt(toSeq.at, parentDuration);
831
+ const toStart = chunk2RR43OIN_cjs.resolveAt(toSeq.at, parentDuration);
639
832
  const toEnd = toStart + (toSeq.duration ?? parentDuration);
640
833
  if (tStart < toStart || tEnd > toEnd) {
641
834
  throw new Error(`pixi-effects: ${tag} window [${tStart}, ${tEnd}] is not covered by \`to\` "${t.to}" (lives [${toStart}, ${toEnd}], so it starts at ${toStart})`);
@@ -946,7 +1139,8 @@ function inspectScene(root, frame, time, canvas, opts = {}) {
946
1139
  const view = { x: 0, y: 0, width: canvas.width, height: canvas.height };
947
1140
  const seen = /* @__PURE__ */ new Map();
948
1141
  const scene = /* @__PURE__ */ new Map();
949
- const walk = (comp, prefix, insideThreeD, parentVisible, parentScene) => {
1142
+ const scaledToNothing = /* @__PURE__ */ new Set();
1143
+ const walk = (comp, prefix, insideThreeD, parentVisible, parentScene, parentMoving = false, parentZero = false) => {
950
1144
  const blending = /* @__PURE__ */ new Set();
951
1145
  for (const w of transitionWindowsOf(comp.spec)) {
952
1146
  const start = (comp.absoluteStart ?? 0) + w.start, end = (comp.absoluteStart ?? 0) + w.end;
@@ -974,8 +1168,10 @@ function inspectScene(root, frame, time, canvas, opts = {}) {
974
1168
  onCanvas = inter === 0 && area > 0 ? "none" : inter >= area - 1e-6 ? "full" : "partial";
975
1169
  if (area === 0) onCanvas = "none";
976
1170
  }
977
- const keys = chunk6BQ6IEW2_cjs.collectPropKeys(seq.spec);
978
- const moving = (seq.spec.keyframes ?? []).length > 0 && (keys.has("x") || keys.has("y"));
1171
+ const keys = chunk2RR43OIN_cjs.collectPropKeys(seq.spec);
1172
+ const moving = parentMoving || (seq.spec.keyframes ?? []).length > 0 && (keys.has("x") || keys.has("y"));
1173
+ const scale = t?.scale;
1174
+ const zero = parentZero || !!scale && (scale.x === 0 || scale.y === 0);
979
1175
  const info = { path: prefix + label, name, type: seq.spec.type, threeD, visible, alpha, moving, bounds, onCanvas };
980
1176
  layers.push(info);
981
1177
  let shown = bounds ? clip(bounds, view) : null;
@@ -987,7 +1183,8 @@ function inspectScene(root, frame, time, canvas, opts = {}) {
987
1183
  seen.set(info, shown);
988
1184
  const group = parentScene ?? (name !== void 0 && blending.has(name) ? name : void 0);
989
1185
  if (group) scene.set(info, group);
990
- if (seq instanceof chunkQGBOC5YL_cjs.CompositionSequence) walk(seq, prefix + label + "/", insideThreeD || threeD, visible, group);
1186
+ if (zero) scaledToNothing.add(info);
1187
+ if (seq instanceof chunk23M5QTWW_cjs.CompositionSequence) walk(seq, prefix + label + "/", insideThreeD || threeD, visible, group, moving, zero);
991
1188
  });
992
1189
  };
993
1190
  walk(root, "", false, true);
@@ -997,6 +1194,7 @@ function inspectScene(root, frame, time, canvas, opts = {}) {
997
1194
  const b = l.bounds;
998
1195
  const who = `text layer "${l.path}"`;
999
1196
  if (b.width < 1 || b.height < 1) {
1197
+ if (scaledToNothing.has(l)) continue;
1000
1198
  issues.push(`${who} has no size (empty text, or not drawn yet)`);
1001
1199
  } else if (l.moving || scene.has(l)) ; else if (l.onCanvas === "none") {
1002
1200
  issues.push(`${who} is entirely outside the canvas (at ${Math.round(b.x)},${Math.round(b.y)}, ${Math.round(b.width)}\xD7${Math.round(b.height)})`);
@@ -1046,64 +1244,6 @@ function sheetLayout(n, columns, cellW, srcW, srcH, labelH) {
1046
1244
  return { cols, rows, cellW, cellH, width: cols * cellW, height: rows * (cellH + labelH), positions };
1047
1245
  }
1048
1246
 
1049
- // src/core/options.ts
1050
- function distance(a, b) {
1051
- const dp = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
1052
- for (let j = 1; j <= b.length; j++) dp[0][j] = j;
1053
- for (let i = 1; i <= a.length; i++) {
1054
- for (let j = 1; j <= b.length; j++) {
1055
- dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
1056
- if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) dp[i][j] = Math.min(dp[i][j], dp[i - 2][j - 2] + 1);
1057
- }
1058
- }
1059
- return dp[a.length][b.length];
1060
- }
1061
- var isSubsequence = (small, big) => {
1062
- let i = 0;
1063
- for (const ch of big) if (i < small.length && ch === small[i]) i++;
1064
- return i === small.length;
1065
- };
1066
- var COMMON_ALIASES = {
1067
- fps: "frameRate",
1068
- framerate: "frameRate",
1069
- frame_rate: "frameRate",
1070
- bg: "background",
1071
- backgroundcolor: "background",
1072
- bgcolor: "background",
1073
- seconds: "duration",
1074
- length: "duration",
1075
- time: "duration",
1076
- w: "width",
1077
- h: "height",
1078
- cols: "columns",
1079
- sweep: "degrees",
1080
- angle: "degrees"
1081
- };
1082
- function suggestName(name, valid) {
1083
- const n = name.toLowerCase();
1084
- const alias = COMMON_ALIASES[n];
1085
- if (alias && valid.includes(alias)) return alias;
1086
- let best = null;
1087
- for (const v of valid) {
1088
- const lv = v.toLowerCase();
1089
- const d = distance(n, lv);
1090
- const close = d <= Math.max(1, Math.floor(Math.min(n.length, lv.length) / 3));
1091
- const abbreviation = n.length >= 3 && isSubsequence(n, lv) || lv.length >= 2 && n.length > lv.length && n.includes(lv);
1092
- if (!close && !abbreviation) continue;
1093
- const score = close ? d : 3 + (lv.length - n.length) / 100;
1094
- if (!best || score < best.score) best = { v, score };
1095
- }
1096
- return best ? best.v : null;
1097
- }
1098
- function warnUnknownOptions(where, opts, valid) {
1099
- if (!opts || typeof opts !== "object") return;
1100
- for (const key of Object.keys(opts)) {
1101
- if (valid.includes(key)) continue;
1102
- const guess = suggestName(key, valid);
1103
- console.warn(`pixi-effects: ${where}: unknown option "${key}"${guess ? ` \u2014 did you mean "${guess}"?` : ""} (valid: ${valid.join(", ")})`);
1104
- }
1105
- }
1106
-
1107
1247
  // src/core/Movie.ts
1108
1248
  PIXI.extensions.add(PIXI.CullerPlugin);
1109
1249
  gsap.gsap.registerPlugin(PixiPlugin.PixiPlugin);
@@ -1130,6 +1270,9 @@ var Movie = class {
1130
1270
  _rootSequence = null;
1131
1271
  _rootContainer = null;
1132
1272
  _raf = null;
1273
+ /** What went into the mix, and its peak before limiting — kept for inspectAudio(). */
1274
+ _audioSources = [];
1275
+ _mixStats = null;
1133
1276
  on(event, fn) {
1134
1277
  (this._events[event] ??= []).push(fn);
1135
1278
  if (event === "ready" && this._initState === "ready") {
@@ -1162,7 +1305,7 @@ var Movie = class {
1162
1305
  return this._audioContext;
1163
1306
  }
1164
1307
  async init(options = {}) {
1165
- warnUnknownOptions("movie.init()", options, ["width", "height", "duration", "frameRate", "background", "canvas", "assets", "composition"]);
1308
+ chunkUQBGW5DO_cjs.warnUnknownOptions("movie.init()", options, ["width", "height", "duration", "frameRate", "background", "canvas", "assets", "composition"]);
1166
1309
  this._initState = "pending";
1167
1310
  try {
1168
1311
  this.width = options.width ?? 1920;
@@ -1215,7 +1358,7 @@ var Movie = class {
1215
1358
  ...userComposition
1216
1359
  };
1217
1360
  if (userComposition) carryTransitionWindows(userComposition, rootSeqSpec);
1218
- const composition = new chunkQGBOC5YL_cjs.CompositionSequence(rootSeqSpec, null, rootShape);
1361
+ const composition = new chunk23M5QTWW_cjs.CompositionSequence(rootSeqSpec, null, rootShape);
1219
1362
  await composition.build();
1220
1363
  this._rootSequence = composition;
1221
1364
  if (composition.target) root.addChild(composition.target);
@@ -1224,7 +1367,9 @@ var Movie = class {
1224
1367
  composition.collectAudio(audios, 0);
1225
1368
  if (audios.length > 0) {
1226
1369
  this.audioBuffer = await mixdown(audios, this.duration, audioContext.sampleRate);
1370
+ if (this.audioBuffer) this._mixStats = limitMix(this.audioBuffer, audios);
1227
1371
  }
1372
+ this._audioSources = audios;
1228
1373
  this.timeline.progress(1).progress(0);
1229
1374
  await this._awaitVideoFrames();
1230
1375
  this._updateSpace();
@@ -1262,7 +1407,7 @@ var Movie = class {
1262
1407
  }));
1263
1408
  }
1264
1409
  async snapshot(frame = this.currentFrame, opts = {}) {
1265
- warnUnknownOptions("movie.snapshot()", opts, ["scale", "type", "as"]);
1410
+ chunkUQBGW5DO_cjs.warnUnknownOptions("movie.snapshot()", opts, ["scale", "type", "as"]);
1266
1411
  this._requireReady("snapshot");
1267
1412
  await this.gotoFrame(clampFrame(frame, this.totalFrames), true);
1268
1413
  const scale = opts.scale ?? 1;
@@ -1273,7 +1418,7 @@ var Movie = class {
1273
1418
  return encodeCanvas(out, opts.type ?? "image/png", opts.as ?? "blob");
1274
1419
  }
1275
1420
  async contactSheet(opts = {}) {
1276
- warnUnknownOptions("movie.contactSheet()", opts, ["frames", "times", "count", "columns", "cellWidth", "as"]);
1421
+ chunkUQBGW5DO_cjs.warnUnknownOptions("movie.contactSheet()", opts, ["frames", "times", "count", "columns", "cellWidth", "as"]);
1277
1422
  this._requireReady("contactSheet");
1278
1423
  const frames = (opts.frames ?? opts.times?.map((t) => Math.round(t * this.frameRate)) ?? pickFrames(this.totalFrames, opts.count ?? 6)).map((f) => clampFrame(f, this.totalFrames));
1279
1424
  const LABEL_H = 26;
@@ -1306,12 +1451,21 @@ var Movie = class {
1306
1451
  * the canvas, cut by an edge, empty, or overlapping other text. Seeks there and stays there.
1307
1452
  */
1308
1453
  async inspect(frame = this.currentFrame, opts = { layers: "visible" }) {
1309
- warnUnknownOptions("movie.inspect()", opts, ["layers"]);
1454
+ chunkUQBGW5DO_cjs.warnUnknownOptions("movie.inspect()", opts, ["layers"]);
1310
1455
  this._requireReady("inspect");
1311
1456
  const f = clampFrame(frame, this.totalFrames);
1312
1457
  await this.gotoFrame(f, true);
1313
1458
  return inspectScene(this._rootSequence, f, f / this.frameRate, { width: this.width, height: this.height }, opts);
1314
1459
  }
1460
+ /**
1461
+ * The soundtrack, measured: when every audio layer plays, how loud the mix is over time, and `issues`
1462
+ * (limiting, inaudible layers, sounds cut off by the end). Use it to CHECK sound you cannot hear.
1463
+ */
1464
+ inspectAudio(opts = {}) {
1465
+ chunkUQBGW5DO_cjs.warnUnknownOptions("movie.inspectAudio()", opts, ["window"]);
1466
+ this._requireReady("inspectAudio");
1467
+ return analyzeAudio(this.audioBuffer, this._audioSources, this._mixStats, this.duration, opts);
1468
+ }
1315
1469
  /**
1316
1470
  * Draw the stage now. PixiJS's CullerPlugin only culls inside `app.render()` (the ticker), and we render
1317
1471
  * by hand — while seeking and especially during `render()`, which stops the ticker — so without this the
@@ -1416,7 +1570,7 @@ var Movie = class {
1416
1570
  return this.muted;
1417
1571
  }
1418
1572
  async render(options) {
1419
- warnUnknownOptions("movie.render()", options, ["format", "video", "audio"]);
1573
+ chunkUQBGW5DO_cjs.warnUnknownOptions("movie.render()", options, ["format", "video", "audio"]);
1420
1574
  return await exportFrames(this, options);
1421
1575
  }
1422
1576
  async destroy() {
@@ -1428,6 +1582,8 @@ var Movie = class {
1428
1582
  safeRun(() => this.app?.destroy(true, { children: true, texture: true }));
1429
1583
  this.app = null;
1430
1584
  this.audioBuffer = null;
1585
+ this._audioSources = [];
1586
+ this._mixStats = null;
1431
1587
  safeRun(() => this._audioContext?.close().catch(() => {
1432
1588
  }));
1433
1589
  this._audioContext = null;
@@ -1455,11 +1611,58 @@ function collectVideoSequences(seq, out) {
1455
1611
  collectVideoSequences(child, out);
1456
1612
  }
1457
1613
  }
1614
+ var BY = ["chars", "words", "lines"];
1615
+ var ALIGN = ["left", "center", "right"];
1616
+ var widthOf = (text, style) => PIXI.CanvasTextMetrics.measureText(text, style).width;
1617
+ function measureText(text, style = {}) {
1618
+ const m = PIXI.CanvasTextMetrics.measureText(text, new PIXI.TextStyle(style));
1619
+ return { width: m.width, height: m.height, lines: text.split("\n").length };
1620
+ }
1621
+ function splitText(text, style = {}, opts = {}) {
1622
+ chunkUQBGW5DO_cjs.warnUnknownOptions("splitText()", opts, ["by", "x", "y", "align"]);
1623
+ let by = opts.by ?? "chars";
1624
+ if (!BY.includes(by)) {
1625
+ const g = chunkUQBGW5DO_cjs.suggestName(String(by), BY);
1626
+ console.warn(`pixi-effects: splitText(): by "${by}" is not one of ${BY.map((b) => `"${b}"`).join(", ")}${g ? ` \u2014 did you mean "${g}"?` : ""}; using "chars"`);
1627
+ by = "chars";
1628
+ }
1629
+ let align = opts.align ?? "left";
1630
+ if (!ALIGN.includes(align)) {
1631
+ const g = chunkUQBGW5DO_cjs.suggestName(String(align), ALIGN);
1632
+ console.warn(`pixi-effects: splitText(): align "${align}" is not one of ${ALIGN.join(", ")}${g ? ` \u2014 did you mean "${g}"?` : ""}; using "left"`);
1633
+ align = "left";
1634
+ }
1635
+ const x0 = opts.x ?? 0, y0 = opts.y ?? 0;
1636
+ if (text === "") return [];
1637
+ const ts = new PIXI.TextStyle(style);
1638
+ const lineHeight = PIXI.CanvasTextMetrics.measureText("M", ts).lineHeight ?? PIXI.CanvasTextMetrics.measureText("M", ts).height;
1639
+ const sentinel = widthOf("|", ts);
1640
+ const out = [];
1641
+ text.split("\n").forEach((line, lineNo) => {
1642
+ const lineWidth = widthOf(line, ts);
1643
+ const left = align === "center" ? x0 - lineWidth / 2 : align === "right" ? x0 - lineWidth : x0;
1644
+ const y = y0 + lineNo * lineHeight;
1645
+ const advance = (upTo) => upTo === 0 ? 0 : widthOf(line.slice(0, upTo) + "|", ts) - sentinel;
1646
+ const spans = [];
1647
+ if (by === "lines") {
1648
+ if (line.trim() !== "") spans.push([0, line.length]);
1649
+ } else if (by === "words") {
1650
+ for (const m of line.matchAll(/\S+/g)) spans.push([m.index, m.index + m[0].length]);
1651
+ } else {
1652
+ for (let i = 0; i < line.length; i++) if (!/\s/.test(line[i])) spans.push([i, i + 1]);
1653
+ }
1654
+ for (const [a, b] of spans) {
1655
+ const piece = line.slice(a, b);
1656
+ out.push({ text: piece, x: left + advance(a), y, width: widthOf(piece, ts), index: out.length, line: lineNo });
1657
+ }
1658
+ });
1659
+ return out;
1660
+ }
1458
1661
 
1459
1662
  // src/presets/kenBurns.ts
1460
1663
  var KEN_BURNS_KEYS = ["asset", "name", "at", "duration", "fit", "ease", "motion", "origin", "zoom", "direction", "angle", "from", "to"];
1461
1664
  function kenBurns(opts) {
1462
- warnUnknownOptions("kenBurns()", opts, KEN_BURNS_KEYS);
1665
+ chunkUQBGW5DO_cjs.warnUnknownOptions("kenBurns()", opts, KEN_BURNS_KEYS);
1463
1666
  const {
1464
1667
  asset,
1465
1668
  name,
@@ -1575,7 +1778,7 @@ function parseEase(name) {
1575
1778
  // src/presets/orbit.ts
1576
1779
  var ORBIT_KEYS = ["duration", "degrees", "start", "radius", "center", "width", "height", "fov", "ease", "dollyZoom", "at", "name", "stepsPerSecond"];
1577
1780
  function orbit(opts) {
1578
- warnUnknownOptions("orbit()", opts, ORBIT_KEYS);
1781
+ chunkUQBGW5DO_cjs.warnUnknownOptions("orbit()", opts, ORBIT_KEYS);
1579
1782
  const {
1580
1783
  duration,
1581
1784
  degrees,
@@ -1591,17 +1794,17 @@ function orbit(opts) {
1591
1794
  throw new Error("pixi-effects: orbit() cannot combine `radius` with `dollyZoom` \u2014 the radius follows the animated fov");
1592
1795
  }
1593
1796
  const [cx, cy] = opts.center ?? [width / 2, height / 2];
1594
- const fixedR = opts.radius ?? chunk6BQ6IEW2_cjs.homeDistance(height, fov ?? chunk6BQ6IEW2_cjs.DEFAULT_FOV);
1797
+ const fixedR = opts.radius ?? chunk2RR43OIN_cjs.homeDistance(height, fov ?? chunk2RR43OIN_cjs.DEFAULT_FOV);
1595
1798
  const start = opts.start ?? -degrees / 2;
1596
1799
  const steps = Math.max(1, Math.round(duration * stepsPerSecond));
1597
1800
  const easeFn = parseEase(ease);
1598
1801
  const dollyEase = dolly ? parseEase(dolly.ease ?? ease) : null;
1599
1802
  const pos = (i) => {
1600
1803
  const p = i / steps;
1601
- const a = (start + degrees * easeFn(p)) * chunk6BQ6IEW2_cjs.DEG;
1804
+ const a = (start + degrees * easeFn(p)) * chunk2RR43OIN_cjs.DEG;
1602
1805
  if (!dolly) return { x: cx + fixedR * Math.sin(a), z: fixedR * Math.cos(a) };
1603
1806
  const f = dolly.from + (dolly.to - dolly.from) * dollyEase(p);
1604
- const R = chunk6BQ6IEW2_cjs.homeDistance(height, f);
1807
+ const R = chunk2RR43OIN_cjs.homeDistance(height, f);
1605
1808
  return { x: cx + R * Math.sin(a), z: R * Math.cos(a), fov: f };
1606
1809
  };
1607
1810
  const keyframes = [];
@@ -1630,7 +1833,7 @@ function orbit(opts) {
1630
1833
 
1631
1834
  // src/transforms/withFade.ts
1632
1835
  function withFade(spec, opts) {
1633
- warnUnknownOptions("withFade()", opts, ["in", "out"]);
1836
+ chunkUQBGW5DO_cjs.warnUnknownOptions("withFade()", opts, ["in", "out"]);
1634
1837
  if (opts.in != null && opts.in > 0) {
1635
1838
  spec.initial = { ...spec.initial ?? {}, alpha: 0 };
1636
1839
  const kf = { at: 0, to: { alpha: 1 }, duration: opts.in };
@@ -1650,11 +1853,13 @@ function withFade(spec, opts) {
1650
1853
 
1651
1854
  Object.defineProperty(exports, "registerSequenceType", {
1652
1855
  enumerable: true,
1653
- get: function () { return chunk6BQ6IEW2_cjs.registerSequenceType; }
1856
+ get: function () { return chunk2RR43OIN_cjs.registerSequenceType; }
1654
1857
  });
1655
1858
  exports.Movie = Movie;
1656
1859
  exports.kenBurns = kenBurns;
1860
+ exports.measureText = measureText;
1657
1861
  exports.orbit = orbit;
1862
+ exports.splitText = splitText;
1658
1863
  exports.withFade = withFade;
1659
1864
  //# sourceMappingURL=index.cjs.map
1660
1865
  //# sourceMappingURL=index.cjs.map