pixi-effects 0.16.2 → 0.17.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 (52) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +6 -6
  3. package/ai/CHAT.md +4 -0
  4. package/ai/SKILL.md +2 -1
  5. package/ai/chat-template.html +3 -3
  6. package/ai/reference/cheatsheet.md +4 -2
  7. package/ai/reference/pitfalls.md +3 -0
  8. package/ai/reference/recipes.md +67 -0
  9. package/ai/template.html +5 -5
  10. package/ai/tools/check.mjs +162 -18
  11. package/ai/tools/render.mjs +21 -7
  12. package/dist/Composition-HYAOYPAX.js +5 -0
  13. package/dist/{Composition-VJWY2NDL.js.map → Composition-HYAOYPAX.js.map} +1 -1
  14. package/dist/Composition-LMVEASHF.cjs +14 -0
  15. package/dist/{Composition-3WWNVRR5.cjs.map → Composition-LMVEASHF.cjs.map} +1 -1
  16. package/dist/Controller.cjs +9 -3
  17. package/dist/Controller.cjs.map +1 -1
  18. package/dist/Controller.d.cts +1 -1
  19. package/dist/Controller.d.ts +1 -1
  20. package/dist/Controller.js +9 -3
  21. package/dist/Controller.js.map +1 -1
  22. package/dist/{Movie-oA0SUAyb.d.cts → Movie-CoxF2Z7t.d.cts} +82 -2
  23. package/dist/{Movie-DilBC348.d.ts → Movie-DHJRrUfI.d.ts} +82 -2
  24. package/dist/Presenter.d.cts +1 -1
  25. package/dist/Presenter.d.ts +1 -1
  26. package/dist/{chunk-2ROVCPSL.cjs → chunk-ETAIVOHP.cjs} +32 -27
  27. package/dist/chunk-ETAIVOHP.cjs.map +1 -0
  28. package/dist/{chunk-FJA5VUOF.cjs → chunk-LBJRXU7N.cjs} +15 -3
  29. package/dist/chunk-LBJRXU7N.cjs.map +1 -0
  30. package/dist/{chunk-ZNKQH356.js → chunk-ORTYBA42.js} +15 -4
  31. package/dist/chunk-ORTYBA42.js.map +1 -0
  32. package/dist/{chunk-XTIHKODH.js → chunk-Q3IVBYT7.js} +10 -5
  33. package/dist/chunk-Q3IVBYT7.js.map +1 -0
  34. package/dist/index.cjs +351 -41
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.d.cts +1 -1
  37. package/dist/index.d.ts +1 -1
  38. package/dist/index.js +329 -19
  39. package/dist/index.js.map +1 -1
  40. package/dist/loader.css +1 -1
  41. package/dist/three.cjs +5 -5
  42. package/dist/three.js +1 -1
  43. package/docs/dsl.md +2 -1
  44. package/llms-full.txt +78 -4
  45. package/llms.txt +1 -1
  46. package/package.json +6 -3
  47. package/dist/Composition-3WWNVRR5.cjs +0 -14
  48. package/dist/Composition-VJWY2NDL.js +0 -5
  49. package/dist/chunk-2ROVCPSL.cjs.map +0 -1
  50. package/dist/chunk-FJA5VUOF.cjs.map +0 -1
  51. package/dist/chunk-XTIHKODH.js.map +0 -1
  52. package/dist/chunk-ZNKQH356.js.map +0 -1
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- export { A as AudioInspectOptions, a as AudioReport, b as AudioSourceReport, C as ContactSheetOptions, F as FrameEvent, I as InspectOptions, c as InspectReport, L as LayerInfo, d as LoaderOption, M as MotionBlurOptions, e as MotionBlurSpec, f as Movie, g as MovieErrorEvent, h as MovieOptions, P as ProgressEvent, R as RenderOptions, S as SnapshotOptions, i as SoundMeasure, j as StopEvent, k as StopImage, l as StopImagesOptions, T as TimelineData, m as TimelineHtmlOptions, n as TimelineRow, o as TimelineSvgOptions, p as TimelineTransition, V as VolumeEvent, q as dismissLoader, r as failLoader, s as resolveLoader } from './Movie-oA0SUAyb.cjs';
1
+ export { A as AudioInspectOptions, a as AudioReport, b as AudioSourceReport, C as ContactSheetOptions, F as FontReport, c as FrameEvent, I as InspectOptions, d as InspectReport, L as LayerInfo, e as LoaderOption, M as MotionBlurOptions, f as MotionBlurSpec, g as Movie, h as MovieErrorEvent, i as MovieOptions, O as OnionSkinOptions, P as ProgressEvent, R as RenderOptions, S as SnapshotOptions, j as SoundMeasure, k as StopEvent, l as StopImage, m as StopImagesOptions, T as TimelineData, n as TimelineHtmlOptions, o as TimelineRow, p as TimelineSvgOptions, q as TimelineTransition, V as VolumeEvent, r as dismissLoader, s as failLoader, t as resolveLoader } from './Movie-CoxF2Z7t.cjs';
2
2
  import { S as SequenceSpec, C as CompositionShape, I as ImageSequenceSpec, a as CameraSequenceSpec, K as Keyframe, T as TextSequenceSpec, M as MusicOptions, P as Props, b as TransitionSpec, c as CompositionSpec, d as SequenceCommon } from './types-SS51lP6O.cjs';
3
3
  export { A as ArcShapeSpec, e as AssetSpec, f as AudioAssetSpec, g as AudioMusicSpec, h as AudioSequenceSpec, i as AudioSfxSpec, j as ChromaKeyFilterSpec, k as CircleShapeSpec, l as CompositionSequenceSpec, m as CrossfadeTransition, n as CustomFilterSpec, D as DipTransition, o as DissolveTransition, E as EllipseShapeSpec, p as Expr, F as FilterSpec, G as GradientSpec, q as IrisTransition, L as LineShapeSpec, r as MusicDrum, s as MusicDrums, t as MusicInstrument, u as MusicLevel, v as MusicTrack, N as NullSequenceSpec, w as PathShapeSpec, x as PolygonShapeSpec, y as PropValue, R as RectShapeSpec, z as SfxKnobs, B as SfxOptions, H as SfxPreset, J as SfxVoice, O as ShapeSequenceSpec, Q as SlideTransition, U as Stop, V as StopSpec, W as TransitionCommon, X as VideoSequenceSpec, Y as WipeTransition, Z as ZoomTransition, _ as normalizeStops, $ as pictureStops } from './types-SS51lP6O.cjs';
4
4
  import { S as Sequence } from './Base-OHqp8qvX.cjs';
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { A as AudioInspectOptions, a as AudioReport, b as AudioSourceReport, C as ContactSheetOptions, F as FrameEvent, I as InspectOptions, c as InspectReport, L as LayerInfo, d as LoaderOption, M as MotionBlurOptions, e as MotionBlurSpec, f as Movie, g as MovieErrorEvent, h as MovieOptions, P as ProgressEvent, R as RenderOptions, S as SnapshotOptions, i as SoundMeasure, j as StopEvent, k as StopImage, l as StopImagesOptions, T as TimelineData, m as TimelineHtmlOptions, n as TimelineRow, o as TimelineSvgOptions, p as TimelineTransition, V as VolumeEvent, q as dismissLoader, r as failLoader, s as resolveLoader } from './Movie-DilBC348.js';
1
+ export { A as AudioInspectOptions, a as AudioReport, b as AudioSourceReport, C as ContactSheetOptions, F as FontReport, c as FrameEvent, I as InspectOptions, d as InspectReport, L as LayerInfo, e as LoaderOption, M as MotionBlurOptions, f as MotionBlurSpec, g as Movie, h as MovieErrorEvent, i as MovieOptions, O as OnionSkinOptions, P as ProgressEvent, R as RenderOptions, S as SnapshotOptions, j as SoundMeasure, k as StopEvent, l as StopImage, m as StopImagesOptions, T as TimelineData, n as TimelineHtmlOptions, o as TimelineRow, p as TimelineSvgOptions, q as TimelineTransition, V as VolumeEvent, r as dismissLoader, s as failLoader, t as resolveLoader } from './Movie-DHJRrUfI.js';
2
2
  import { S as SequenceSpec, C as CompositionShape, I as ImageSequenceSpec, a as CameraSequenceSpec, K as Keyframe, T as TextSequenceSpec, M as MusicOptions, P as Props, b as TransitionSpec, c as CompositionSpec, d as SequenceCommon } from './types-SS51lP6O.js';
3
3
  export { A as ArcShapeSpec, e as AssetSpec, f as AudioAssetSpec, g as AudioMusicSpec, h as AudioSequenceSpec, i as AudioSfxSpec, j as ChromaKeyFilterSpec, k as CircleShapeSpec, l as CompositionSequenceSpec, m as CrossfadeTransition, n as CustomFilterSpec, D as DipTransition, o as DissolveTransition, E as EllipseShapeSpec, p as Expr, F as FilterSpec, G as GradientSpec, q as IrisTransition, L as LineShapeSpec, r as MusicDrum, s as MusicDrums, t as MusicInstrument, u as MusicLevel, v as MusicTrack, N as NullSequenceSpec, w as PathShapeSpec, x as PolygonShapeSpec, y as PropValue, R as RectShapeSpec, z as SfxKnobs, B as SfxOptions, H as SfxPreset, J as SfxVoice, O as ShapeSequenceSpec, Q as SlideTransition, U as Stop, V as StopSpec, W as TransitionCommon, X as VideoSequenceSpec, Y as WipeTransition, Z as ZoomTransition, _ as normalizeStops, $ as pictureStops } from './types-SS51lP6O.js';
4
4
  import { S as Sequence } from './Base-BQUodYZK.js';
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
- import { CompositionSequence } from './chunk-XTIHKODH.js';
2
- import { ensureFilterLibrary, homeDistance, DEFAULT_FOV, random, noise, flattenSvgPath, rand, DEG, resolveAt, collectPropKeys } from './chunk-ZNKQH356.js';
3
- export { noise, rand, random, registerSequenceType } from './chunk-ZNKQH356.js';
1
+ import { CompositionSequence } from './chunk-Q3IVBYT7.js';
2
+ import { ensureFilterLibrary, homeDistance, DEFAULT_FOV, random, noise, flattenSvgPath, rand, DEG, resolveAt, checkEase, collectPropKeys } from './chunk-ORTYBA42.js';
3
+ export { noise, rand, random, registerSequenceType } from './chunk-ORTYBA42.js';
4
4
  import { warnUnknownOptions, suggestName } from './chunk-DKGG7WD4.js';
5
5
  import * as PIXI from 'pixi.js';
6
6
  import { extensions, CullerPlugin, Application, Container, Rectangle, Culler, CanvasTextMetrics, TextStyle, Assets, ExtensionType, Filter, UniformGroup, GpuProgram, GlProgram, defaultFilterVert } from 'pixi.js';
@@ -183,6 +183,118 @@ function spectralCentroid(samples, sampleRate, from = 0, to = samples.length) {
183
183
  return den > 1e-9 ? Math.round(num3 / den / 10) * 10 : 0;
184
184
  }
185
185
 
186
+ // src/audio/loudness.ts
187
+ var LOUDNESS_NOTES = { quietBelow: -24, loudAbove: -9, headroomDb: -1 };
188
+ function kWeighting(fs) {
189
+ const f0 = 1681.974450955533, G = 3.999843853973347, Q = 0.7071752369554196;
190
+ const K = Math.tan(Math.PI * f0 / fs), Vh = 10 ** (G / 20), Vb = Vh ** 0.4996667741545416;
191
+ const a0 = 1 + K / Q + K * K;
192
+ const shelf = {
193
+ b: [(Vh + Vb * K / Q + K * K) / a0, 2 * (K * K - Vh) / a0, (Vh - Vb * K / Q + K * K) / a0],
194
+ a: [1, 2 * (K * K - 1) / a0, (1 - K / Q + K * K) / a0]
195
+ };
196
+ const f1 = 38.13547087602444, Q1 = 0.5003270373238773, K1 = Math.tan(Math.PI * f1 / fs), a1 = 1 + K1 / Q1 + K1 * K1;
197
+ const high = { b: [1, -2, 1], a: [1, 2 * (K1 * K1 - 1) / a1, (1 - K1 / Q1 + K1 * K1) / a1] };
198
+ return [shelf, high];
199
+ }
200
+ function filter(x, stages) {
201
+ let cur = x;
202
+ for (const { b, a } of stages) {
203
+ const out = new Float32Array(cur.length);
204
+ let x1 = 0, x2 = 0, y1 = 0, y2 = 0;
205
+ for (let i = 0; i < cur.length; i++) {
206
+ const xi = cur[i], y = b[0] * xi + b[1] * x1 + b[2] * x2 - a[1] * y1 - a[2] * y2;
207
+ x2 = x1;
208
+ x1 = xi;
209
+ y2 = y1;
210
+ y1 = y;
211
+ out[i] = y;
212
+ }
213
+ cur = out;
214
+ }
215
+ return cur;
216
+ }
217
+ var toDb = (x) => Math.round(200 * Math.log10(Math.max(x, 1e-6))) / 10;
218
+ var TP_HALF = 12;
219
+ var TP_BLOCK = 32;
220
+ var TP_KERNELS = [0.25, 0.5, 0.75].map((p) => Float64Array.from({ length: 2 * TP_HALF }, (_, j) => {
221
+ const k = j - TP_HALF + 1, x = k - p, s = x === 0 ? 1 : Math.sin(Math.PI * x) / (Math.PI * x);
222
+ return s * (0.5 + 0.5 * Math.cos(Math.PI * x / (TP_HALF + 1)));
223
+ }));
224
+ var TP_GAIN = Math.max(...TP_KERNELS.map((k) => k.reduce((a, v) => a + Math.abs(v), 0)));
225
+ function truePeak(chans) {
226
+ let peak = 0;
227
+ for (const c of chans) {
228
+ const n = c.length;
229
+ const blocks = Math.ceil(n / TP_BLOCK), blockMax = new Float32Array(blocks + 2);
230
+ for (let i = 0; i < n; i++) {
231
+ const a = Math.abs(c[i]), b = (i / TP_BLOCK | 0) + 1;
232
+ if (a > blockMax[b]) blockMax[b] = a;
233
+ if (a > peak) peak = a;
234
+ }
235
+ const padded = new Float32Array(n + 2 * TP_HALF);
236
+ padded.set(c, TP_HALF);
237
+ for (let i = 0; i < n; i++) {
238
+ const b = (i / TP_BLOCK | 0) + 1;
239
+ const local = Math.max(blockMax[b - 1], blockMax[b], blockMax[b + 1]);
240
+ if (local * TP_GAIN <= peak) continue;
241
+ for (let pi = 0; pi < 3; pi++) {
242
+ const k = TP_KERNELS[pi];
243
+ let v = 0;
244
+ for (let j = 0; j < 2 * TP_HALF; j++) v += padded[i + 1 + j] * k[j];
245
+ const a = Math.abs(v);
246
+ if (a > peak) peak = a;
247
+ }
248
+ }
249
+ }
250
+ return peak;
251
+ }
252
+ function measureLoudness(channels, sampleRate, opts = {}) {
253
+ let samplePeak = 0, clipped = 0;
254
+ for (const c of channels) for (let i = 0; i < c.length; i++) {
255
+ const a = Math.abs(c[i]);
256
+ if (a > samplePeak) samplePeak = a;
257
+ if (a >= 1) clipped++;
258
+ }
259
+ const stages = kWeighting(sampleRate);
260
+ const weighted = channels.map((c) => filter(c, stages));
261
+ const block = Math.round(0.4 * sampleRate), hop = Math.round(0.1 * sampleRate);
262
+ const energies = [];
263
+ for (let s = 0; s + block <= (weighted[0]?.length ?? 0); s += hop) {
264
+ let sum = 0;
265
+ for (const w of weighted) {
266
+ let z = 0;
267
+ for (let i = s; i < s + block; i++) z += w[i] * w[i];
268
+ sum += z / block;
269
+ }
270
+ energies.push(sum);
271
+ }
272
+ const lufs = (e) => -0.691 + 10 * Math.log10(e);
273
+ const absolute = energies.filter((e) => e > 0 && lufs(e) > -70);
274
+ let integrated = null;
275
+ if (absolute.length) {
276
+ const relative = lufs(absolute.reduce((a, b) => a + b, 0) / absolute.length) - 10;
277
+ const gated = absolute.filter((e) => lufs(e) > relative);
278
+ if (gated.length) integrated = Math.round(10 * lufs(gated.reduce((a, b) => a + b, 0) / gated.length)) / 10;
279
+ }
280
+ return { integratedLufs: integrated, truePeakDb: opts.truePeak === false ? null : toDb(truePeak(channels)), samplePeakDb: toDb(samplePeak), clippedSamples: clipped };
281
+ }
282
+ function findSilences(channels, sampleRate, opts = {}) {
283
+ const min = Math.round((opts.minLength ?? 1) * sampleRate), limit = 10 ** ((opts.thresholdDb ?? -60) / 20);
284
+ const n = channels[0]?.length ?? 0, out = [];
285
+ let start = -1;
286
+ const quiet = (i) => channels.every((c) => Math.abs(c[i]) < limit);
287
+ for (let i = 0; i <= n; i++) {
288
+ const q = i < n && quiet(i);
289
+ if (q && start < 0) start = i;
290
+ if (!q && start >= 0) {
291
+ if (i - start >= min) out.push({ from: Math.round(start / sampleRate * 1e3) / 1e3, to: Math.round(i / sampleRate * 1e3) / 1e3 });
292
+ start = -1;
293
+ }
294
+ }
295
+ return out;
296
+ }
297
+
186
298
  // src/core/inspectAudio.ts
187
299
  var INAUDIBLE_DB = -50;
188
300
  var db = (x) => Math.round(10 * Math.max(-120, 20 * Math.log10(x))) / 10;
@@ -208,7 +320,19 @@ function measure(chans, sampleRate) {
208
320
  }
209
321
  function analyzeAudio(mix, sources, stats, movieDuration, opts = {}) {
210
322
  if (!mix) {
211
- 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 })'] };
323
+ return {
324
+ duration: movieDuration,
325
+ sampleRate: 0,
326
+ peakDb: -120,
327
+ peakAt: 0,
328
+ sources: [],
329
+ windows: [],
330
+ loudness: { integratedLufs: null, truePeakDb: -120, clippedSamples: 0, silences: [] },
331
+ scenes: [],
332
+ cues: [],
333
+ notes: [],
334
+ issues: ['no audio: the movie has no audio layers (sound effects need no files: { type: "audio", sfx: "pop", at: 1 })']
335
+ };
212
336
  }
213
337
  const sr = mix.sampleRate;
214
338
  const chans = Array.from({ length: mix.numberOfChannels }, (_, c) => mix.getChannelData(c));
@@ -266,6 +390,24 @@ function analyzeAudio(mix, sources, stats, movieDuration, opts = {}) {
266
390
  }
267
391
  return { layer, source, start: s.start, end: s.end, peakDb, sound };
268
392
  });
393
+ const whole = measureLoudness(chans, sr);
394
+ const silences = findSilences(chans, sr, { minLength: 1 });
395
+ if (whole.clippedSamples > 0) issues.push(`${whole.clippedSamples} sample(s) of the mix are at full scale (clipped) \u2014 lower the volume of the layers that play loudest`);
396
+ if (whole.truePeakDb > 0) issues.push(`the mix's true peak is ${whole.truePeakDb} dBTP: it will distort when the video is encoded \u2014 lower the volume`);
397
+ const notes = [];
398
+ if (whole.integratedLufs !== null) {
399
+ if (whole.integratedLufs < LOUDNESS_NOTES.quietBelow) notes.push(`the mix is quiet for web video: ${whole.integratedLufs} LUFS (typical \u221214 to \u221216); raise the volume if that is not on purpose`);
400
+ else if (whole.integratedLufs > LOUDNESS_NOTES.loudAbove) notes.push(`the mix is loud: ${whole.integratedLufs} LUFS (typical web video \u221214 to \u221216)`);
401
+ }
402
+ if (whole.truePeakDb > LOUDNESS_NOTES.headroomDb && whole.truePeakDb <= 0) notes.push(`the true peak is ${whole.truePeakDb} dBTP: little headroom left for encoding (keep it below ${LOUDNESS_NOTES.headroomDb} dBTP)`);
403
+ for (const q of silences.slice(0, 3)) notes.push(`the mix is silent from ${q.from} s to ${q.to} s (${Math.round((q.to - q.from) * 10) / 10} s)`);
404
+ if (silences.length > 3) notes.push(`and ${silences.length - 3} more silent stretch(es) (loudness.silences lists them all)`);
405
+ const sceneList = (opts.scenes && opts.scenes.length ? opts.scenes : [{ name: "movie", start: 0, end: movieDuration }]).map((sc) => {
406
+ const a = Math.max(0, Math.round(sc.start * sr)), b = Math.min(mix.length, Math.round(sc.end * sr));
407
+ const part = chans.map((c) => c.subarray(a, Math.max(a, b)));
408
+ const m = measureLoudness(part, sr, { truePeak: false });
409
+ return { name: sc.name, from: sc.start, to: sc.end, lufs: m.integratedLufs, peakDb: m.samplePeakDb, silent: m.samplePeakDb < -60 };
410
+ });
269
411
  return {
270
412
  duration: movieDuration,
271
413
  sampleRate: sr,
@@ -273,8 +415,69 @@ function analyzeAudio(mix, sources, stats, movieDuration, opts = {}) {
273
415
  peakAt: stats?.peakAt ?? 0,
274
416
  sources: list,
275
417
  windows: windows2,
276
- issues
418
+ issues,
419
+ loudness: { integratedLufs: whole.integratedLufs, truePeakDb: whole.truePeakDb ?? -120, clippedSamples: whole.clippedSamples, silences },
420
+ scenes: sceneList,
421
+ cues: list.map((s) => ({ t: s.start, name: `${s.layer} (${s.source})` })),
422
+ notes
423
+ };
424
+ }
425
+
426
+ // src/text/fonts.ts
427
+ var GENERIC = /* @__PURE__ */ new Set(["serif", "sans-serif", "monospace", "cursive", "fantasy", "system-ui", "ui-serif", "ui-sans-serif", "ui-monospace", "ui-rounded", "emoji", "math", "fangsong", "-apple-system", "blinkmacsystemfont"]);
428
+ function fontAvailable(family2) {
429
+ const f = family2.trim().replace(/^["']|["']$/g, "");
430
+ if (!f || GENERIC.has(f.toLowerCase())) return true;
431
+ const g = document.createElement("canvas").getContext("2d");
432
+ if (!g) return true;
433
+ const sample = "mmmmmmmmmmlliWWWW 0123 \u3042\u3044\u3046";
434
+ const width = (stack) => {
435
+ g.font = `72px ${stack}`;
436
+ return g.measureText(sample).width;
437
+ };
438
+ const q = JSON.stringify(f);
439
+ return width(`${q}, monospace`) !== width("monospace") || width(`${q}, serif`) !== width("serif");
440
+ }
441
+
442
+ // src/core/inspectFonts.ts
443
+ function inspectFonts(root) {
444
+ const failed = /* @__PURE__ */ new Set();
445
+ if (typeof document !== "undefined" && document.fonts) document.fonts.forEach((face) => {
446
+ if (face.status === "error") failed.add(face.family.replace(/^["']|["']$/g, ""));
447
+ });
448
+ const missing = [];
449
+ const walk2 = (seq, index) => {
450
+ const spec = seq.spec;
451
+ if (spec.type === "text") {
452
+ const raw = spec.style?.fontFamily;
453
+ const family2 = Array.isArray(raw) ? raw.join(", ") : typeof raw === "string" ? raw : "";
454
+ const list = family2.split(",").map((f) => f.trim()).filter(Boolean);
455
+ if (list.length && !list.some(fontAvailable) && !list.some((f) => failed.has(f.replace(/^["']|["']$/g, "")))) missing.push({ layer: spec.name ?? `text#${index}`, family: family2 });
456
+ }
457
+ if (seq.maskSequence) walk2(seq.maskSequence, index);
458
+ (seq._children ?? []).forEach((child, i) => walk2(child, i));
277
459
  };
460
+ walk2(root, 0);
461
+ return { missing, failed: [...failed] };
462
+ }
463
+
464
+ // src/core/scenes.ts
465
+ function namedScenes(rows) {
466
+ return rows.filter((r) => r.depth === 0 && r.type === "composition" && !/[#×]/.test(r.name) && r.end - r.start >= 1).map((r) => ({ name: r.name, start: r.start, end: r.end }));
467
+ }
468
+
469
+ // src/core/onion.ts
470
+ function onionTimes(from, to, count) {
471
+ if (count <= 1) return [from];
472
+ return Array.from({ length: count }, (_, i) => Math.round((from + (to - from) * i / (count - 1)) * 1e6) / 1e6);
473
+ }
474
+ function onionAlphas(count, ramp = 0.25) {
475
+ const weights = Array.from({ length: count }, (_, i) => count === 1 ? 1 : ramp + (1 - ramp) * (i / (count - 1)));
476
+ let sum = 0;
477
+ return weights.map((w) => {
478
+ sum += w;
479
+ return w / sum;
480
+ });
278
481
  }
279
482
 
280
483
  // src/core/motionBlur.ts
@@ -293,6 +496,57 @@ function blurTimes(frame, frameRate, mb, duration) {
293
496
  return Array.from({ length: mb.samples }, (_, k) => Math.min(Math.max(t + ((k + 0.5) / mb.samples - 0.5) * width, 0), duration));
294
497
  }
295
498
 
499
+ // src/core/renderRange.ts
500
+ function resolveRange(range2, ctx) {
501
+ const { frameRate: fps, totalFrames, duration } = ctx;
502
+ if (range2 === void 0) return { fromFrame: 0, toFrame: totalFrames, fromSec: 0, toSec: duration, partial: false };
503
+ let from, to;
504
+ if (typeof range2 === "string") {
505
+ const rows = (ctx.rows ?? []).filter((r) => r.depth === 0);
506
+ const row = rows.find((r) => r.name === range2);
507
+ if (!row) throw new Error(`pixi-effects: render({ range: "${range2}" }): no layer named "${range2}" at the top level (names: ${rows.map((r) => r.name).join(", ") || "none"})`);
508
+ from = row.start;
509
+ to = row.end;
510
+ } else {
511
+ if (!Array.isArray(range2) || range2.length !== 2 || !range2.every((n) => typeof n === "number" && Number.isFinite(n))) {
512
+ throw new Error("pixi-effects: render({ range }) must be [from, to] in seconds (numbers) or the name of a top-level layer");
513
+ }
514
+ [from, to] = range2;
515
+ }
516
+ if (from < 0 || to <= from) throw new Error(`pixi-effects: render({ range: [${from}, ${to}] }): a range must start before it ends (from >= 0, from < to)`);
517
+ if (to > duration + 0.5 / fps) throw new Error(`pixi-effects: render({ range: [${from}, ${to}] }) goes past the end: the movie is ${duration} s long`);
518
+ const fromFrame = Math.round(from * fps);
519
+ const toFrame = Math.min(Math.round(to * fps), totalFrames);
520
+ if (toFrame <= fromFrame) throw new Error(`pixi-effects: render({ range: [${from}, ${to}] }) is shorter than one frame (${(1 / fps).toFixed(4)} s at ${fps} fps)`);
521
+ const partial = fromFrame > 0 || toFrame < totalFrames;
522
+ return { fromFrame, toFrame, fromSec: fromFrame / fps, toSec: toFrame / fps, partial };
523
+ }
524
+ function sliceChannels(channels, sampleRate, fromSec, toSec, opts = {}) {
525
+ const fade = Math.max(0, Math.round((opts.fade ?? 0.01) * sampleRate));
526
+ const from = Math.round(fromSec * sampleRate), len = Math.max(1, Math.round(toSec * sampleRate) - from);
527
+ return channels.map((src) => {
528
+ const out = new Float32Array(len);
529
+ for (let i = 0; i < len; i++) {
530
+ const j = from + i;
531
+ if (j >= 0 && j < src.length) out[i] = src[j];
532
+ }
533
+ const f = Math.min(fade, len >> 1);
534
+ if (opts.fadeIn !== false) for (let i = 0; i < f; i++) out[i] = out[i] * (i / f);
535
+ if (opts.fadeOut !== false) for (let i = 0; i < f; i++) out[len - 1 - i] = out[len - 1 - i] * (i / f);
536
+ return out;
537
+ });
538
+ }
539
+ function resolveRenderOptions(o) {
540
+ const draft = !!o.draft;
541
+ const scale = o.scale ?? (draft ? 0.5 : 1);
542
+ if (!(typeof scale === "number" && scale > 0 && scale <= 1)) throw new Error(`pixi-effects: render({ scale }): scale must be above 0 and at most 1 (got ${scale})`);
543
+ return {
544
+ scale,
545
+ bitrate: o.video?.bitrate ?? (draft ? "low" : void 0),
546
+ motionBlur: o.motionBlur !== void 0 ? o.motionBlur : draft ? false : void 0
547
+ };
548
+ }
549
+
296
550
  // src/core/Renderer.ts
297
551
  var VIDEO_CODEC_BY_FORMAT = {
298
552
  mp4: "avc",
@@ -347,13 +601,22 @@ function makeOutputFormat(name) {
347
601
  return new MkvOutputFormat();
348
602
  }
349
603
  }
604
+ function sliceAudio(buffer, fromSec, toSec, fadeIn, fadeOut) {
605
+ const channels = Array.from({ length: buffer.numberOfChannels }, (_, c) => buffer.getChannelData(c));
606
+ const cut = sliceChannels(channels, buffer.sampleRate, fromSec, toSec, { fadeIn, fadeOut });
607
+ const out = new AudioBuffer({ length: cut[0].length, numberOfChannels: cut.length, sampleRate: buffer.sampleRate });
608
+ cut.forEach((data, c) => out.copyToChannel(data, c));
609
+ return out;
610
+ }
350
611
  async function exportFrames(movie, options = {}) {
351
612
  const fmt = options.format ?? "mp4";
613
+ const eff = resolveRenderOptions(options);
614
+ const range2 = resolveRange(options.range, { frameRate: movie.frameRate, totalFrames: movie.totalFrames, duration: movie.duration, rows: typeof options.range === "string" ? movie.timelineData().rows : void 0 });
352
615
  const opts = {
353
616
  format: fmt,
354
617
  video: {
355
618
  codec: options.video?.codec ?? VIDEO_CODEC_BY_FORMAT[fmt],
356
- bitrate: qualityMap[options.video?.bitrate ?? "high"] ?? QUALITY_HIGH
619
+ bitrate: qualityMap[eff.bitrate ?? "high"] ?? QUALITY_HIGH
357
620
  },
358
621
  audio: {
359
622
  codec: options.audio?.codec ?? AUDIO_CODEC_BY_FORMAT[fmt],
@@ -361,27 +624,30 @@ async function exportFrames(movie, options = {}) {
361
624
  }
362
625
  };
363
626
  const container = makeOutputFormat(opts.format);
364
- if (movie.audioBuffer) opts.audio.codec = await chooseAudioCodec(fmt, options.audio?.codec, container, movie.audioBuffer, opts.audio.bitrate);
627
+ const audio = movie.audioBuffer && range2.partial ? sliceAudio(movie.audioBuffer, range2.fromSec, range2.toSec, range2.fromFrame > 0, range2.toFrame < movie.totalFrames) : movie.audioBuffer;
628
+ if (audio) opts.audio.codec = await chooseAudioCodec(fmt, options.audio?.codec, container, audio, opts.audio.bitrate);
365
629
  const output = new Output({
366
630
  format: container,
367
631
  target: new BufferTarget()
368
632
  });
369
- const mb = options.motionBlur !== void 0 ? resolveMotionBlur(options.motionBlur, "movie.render()") : movie.motionBlur ?? null;
633
+ const mb = eff.motionBlur !== void 0 ? resolveMotionBlur(eff.motionBlur, "movie.render()") : movie.motionBlur ?? null;
370
634
  const stage = movie.app.canvas;
371
635
  const blurCanvas = mb ? Object.assign(document.createElement("canvas"), { width: stage.width, height: stage.height }) : null;
372
- const canvasSource = new CanvasSource(blurCanvas ?? stage, {
636
+ const small = eff.scale < 1 ? Object.assign(document.createElement("canvas"), { width: Math.max(2, Math.round(stage.width * eff.scale / 2) * 2), height: Math.max(2, Math.round(stage.height * eff.scale / 2) * 2) }) : null;
637
+ const smallCtx = small?.getContext("2d") ?? null;
638
+ const canvasSource = new CanvasSource(small ?? blurCanvas ?? stage, {
373
639
  codec: opts.video.codec,
374
640
  bitrate: opts.video.bitrate
375
641
  });
376
642
  output.addVideoTrack(canvasSource, { frameRate: movie.frameRate });
377
- if (movie.audioBuffer) {
643
+ if (audio) {
378
644
  const audioSource = new AudioBufferSource({
379
645
  codec: opts.audio.codec,
380
646
  bitrate: opts.audio.bitrate
381
- }, { startTimestamp: opts.audio.codec === "aac" ? -2112 / movie.audioBuffer.sampleRate : 0 });
647
+ }, { startTimestamp: opts.audio.codec === "aac" ? -2112 / audio.sampleRate : 0 });
382
648
  output.addAudioTrack(audioSource);
383
649
  await output.start();
384
- await audioSource.add(movie.audioBuffer);
650
+ await audioSource.add(audio);
385
651
  await audioSource.close();
386
652
  } else {
387
653
  await output.start();
@@ -389,13 +655,16 @@ async function exportFrames(movie, options = {}) {
389
655
  movie.app.ticker.stop();
390
656
  const keyframeIntervalFrames = Math.max(1, Math.round(2 * movie.frameRate));
391
657
  try {
392
- for (let frame = 0; frame <= movie.totalFrames; frame++) {
658
+ const count = Math.max(1, range2.toFrame - range2.fromFrame);
659
+ for (let frame = range2.fromFrame; frame <= range2.toFrame; frame++) {
393
660
  if (mb && blurCanvas) await movie._exposeFrame(frame, mb, blurCanvas, false);
394
661
  else await movie.gotoFrame(frame, true);
395
- const isKey = frame === 0 || frame % keyframeIntervalFrames === 0;
662
+ if (small && smallCtx) smallCtx.drawImage(blurCanvas ?? stage, 0, 0, small.width, small.height);
663
+ const rel = frame - range2.fromFrame;
664
+ const isKey = rel === 0 || rel % keyframeIntervalFrames === 0;
396
665
  const addOpts = isKey ? { keyFrame: true } : void 0;
397
- await canvasSource.add(frame / movie.frameRate, 1 / movie.frameRate, addOpts);
398
- const progress = Math.floor(frame / movie.totalFrames * 100);
666
+ await canvasSource.add(rel / movie.frameRate, 1 / movie.frameRate, addOpts);
667
+ const progress = Math.floor(rel / count * 100);
399
668
  movie.emit("progress", { progress, frame, totalFrames: movie.totalFrames });
400
669
  }
401
670
  await canvasSource.close();
@@ -2039,6 +2308,37 @@ var Movie = class {
2039
2308
  })));
2040
2309
  return new Blob([buildPdf(pages, { title: opts.title })], { type: "application/pdf" });
2041
2310
  }
2311
+ async onionSkin(opts = {}) {
2312
+ warnUnknownOptions("movie.onionSkin()", opts, ["from", "to", "count", "scale", "as", "motionBlur"]);
2313
+ this._requireReady("onionSkin");
2314
+ const from = opts.from ?? 0, to = opts.to ?? this.duration, count = opts.count ?? 8, scale = opts.scale ?? 1;
2315
+ if (!(Number.isInteger(count) && count >= 1 && count <= 64)) throw new Error(`pixi-effects: movie.onionSkin(): count must be a whole number from 1 to 64, got ${count}`);
2316
+ if (!(from >= 0 && to <= this.duration + 1e-9 && (count === 1 || from < to))) throw new Error(`pixi-effects: movie.onionSkin(): from must be before to, inside the movie (0 to ${this.duration} s), got ${from} to ${to}`);
2317
+ if (!(scale > 0 && scale <= 4)) throw new Error(`pixi-effects: movie.onionSkin(): scale must be above 0 and at most 4, got ${scale}`);
2318
+ const mb = this._blurFor(opts.motionBlur, "movie.onionSkin()");
2319
+ const blurred = mb ? this._blurCanvas() : null;
2320
+ const out = document.createElement("canvas");
2321
+ out.width = Math.max(1, Math.round(this.width * scale));
2322
+ out.height = Math.max(1, Math.round(this.height * scale));
2323
+ const g = out.getContext("2d");
2324
+ const alphas = onionAlphas(count);
2325
+ const frames = onionTimes(from, to, count).map((t) => clampFrame(t * this.frameRate, this.totalFrames));
2326
+ const back = this.currentFrame, wasAtPoster = this._atPoster;
2327
+ try {
2328
+ for (let i = 0; i < frames.length; i++) {
2329
+ const f = frames[i];
2330
+ if (mb && blurred) await this._quietly(() => this._exposeFrame(f, mb, blurred, true));
2331
+ else await this._quietly(() => this.gotoFrame(f, true));
2332
+ g.globalAlpha = alphas[i];
2333
+ g.drawImage(blurred ?? this.app.canvas, 0, 0, out.width, out.height);
2334
+ }
2335
+ } finally {
2336
+ g.globalAlpha = 1;
2337
+ if (wasAtPoster) await this._showPoster();
2338
+ else await this._quietly(() => this.gotoFrame(back, true));
2339
+ }
2340
+ return encodeCanvas(out, "image/png", opts.as ?? "blob");
2341
+ }
2042
2342
  async contactSheet(opts = {}) {
2043
2343
  warnUnknownOptions("movie.contactSheet()", opts, ["frames", "times", "count", "columns", "cellWidth", "as", "motionBlur"]);
2044
2344
  this._requireReady("contactSheet");
@@ -2105,10 +2405,19 @@ var Movie = class {
2105
2405
  * The soundtrack, measured: when every audio layer plays, how loud the mix is over time, and `issues`
2106
2406
  * (limiting, inaudible layers, sounds cut off by the end). Use it to CHECK sound you cannot hear.
2107
2407
  */
2408
+ /**
2409
+ * The fonts of the text layers: `missing` lists layers none of whose `fontFamily` entries is available in this browser (they are
2410
+ * drawn in a fallback font), `failed` lists web fonts (`@font-face`) whose file could not be loaded.
2411
+ */
2412
+ inspectFonts() {
2413
+ this._requireReady("inspectFonts");
2414
+ return inspectFonts(this._rootSequence);
2415
+ }
2108
2416
  inspectAudio(opts = {}) {
2109
- warnUnknownOptions("movie.inspectAudio()", opts, ["window"]);
2417
+ warnUnknownOptions("movie.inspectAudio()", opts, ["window", "scenes"]);
2110
2418
  this._requireReady("inspectAudio");
2111
- return analyzeAudio(this.audioBuffer, this._audioSources, this._mixStats, this.duration, opts);
2419
+ const scenes = opts.scenes ?? namedScenes(this.timelineData().rows);
2420
+ return analyzeAudio(this.audioBuffer, this._audioSources, this._mixStats, this.duration, { ...opts, scenes });
2112
2421
  }
2113
2422
  /**
2114
2423
  * Draw the stage now. PixiJS's CullerPlugin only culls inside `app.render()` (the ticker), and we render
@@ -2376,7 +2685,7 @@ var Movie = class {
2376
2685
  return this.muted;
2377
2686
  }
2378
2687
  async render(options) {
2379
- warnUnknownOptions("movie.render()", options, ["format", "video", "audio", "motionBlur"]);
2688
+ warnUnknownOptions("movie.render()", options, ["format", "video", "audio", "motionBlur", "range", "scale", "draft"]);
2380
2689
  try {
2381
2690
  return await this._quietly(() => exportFrames(this, options));
2382
2691
  } catch (err) {
@@ -2587,6 +2896,7 @@ function clamp(v, lo, hi) {
2587
2896
  return Math.max(lo, Math.min(hi, v));
2588
2897
  }
2589
2898
  function parseEase(name) {
2899
+ checkEase(name, "a preset");
2590
2900
  return gsap.parseEase(name);
2591
2901
  }
2592
2902