pixi-effects 0.16.3 → 0.18.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 (48) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +7 -7
  3. package/ai/CHAT.md +7 -0
  4. package/ai/SKILL.md +3 -2
  5. package/ai/chat-template.html +8 -4
  6. package/ai/reference/cheatsheet.md +6 -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 +119 -59
  11. package/ai/tools/render.mjs +22 -7
  12. package/dist/Composition-6JMXWNVV.cjs +14 -0
  13. package/dist/{Composition-3WWNVRR5.cjs.map → Composition-6JMXWNVV.cjs.map} +1 -1
  14. package/dist/Composition-MLBHYYFF.js +5 -0
  15. package/dist/{Composition-VJWY2NDL.js.map → Composition-MLBHYYFF.js.map} +1 -1
  16. package/dist/Controller.d.cts +1 -1
  17. package/dist/Controller.d.ts +1 -1
  18. package/dist/{Movie-DilBC348.d.ts → Movie-DlHQ9C02.d.ts} +126 -2
  19. package/dist/{Movie-oA0SUAyb.d.cts → Movie-K2YuVAdg.d.cts} +126 -2
  20. package/dist/Presenter.d.cts +1 -1
  21. package/dist/Presenter.d.ts +1 -1
  22. package/dist/{chunk-FJA5VUOF.cjs → chunk-ANO5R2E6.cjs} +25 -3
  23. package/dist/chunk-ANO5R2E6.cjs.map +1 -0
  24. package/dist/{chunk-2ROVCPSL.cjs → chunk-JW67MDHQ.cjs} +32 -27
  25. package/dist/chunk-JW67MDHQ.cjs.map +1 -0
  26. package/dist/{chunk-ZNKQH356.js → chunk-SG3Y66FN.js} +25 -4
  27. package/dist/chunk-SG3Y66FN.js.map +1 -0
  28. package/dist/{chunk-XTIHKODH.js → chunk-UXDR3ZDU.js} +10 -5
  29. package/dist/chunk-UXDR3ZDU.js.map +1 -0
  30. package/dist/index.cjs +464 -43
  31. package/dist/index.cjs.map +1 -1
  32. package/dist/index.d.cts +1 -1
  33. package/dist/index.d.ts +1 -1
  34. package/dist/index.js +442 -21
  35. package/dist/index.js.map +1 -1
  36. package/dist/loader.css +1 -1
  37. package/dist/three.cjs +5 -5
  38. package/dist/three.js +1 -1
  39. package/docs/dsl.md +2 -1
  40. package/llms-full.txt +81 -5
  41. package/llms.txt +1 -1
  42. package/package.json +6 -3
  43. package/dist/Composition-3WWNVRR5.cjs +0 -14
  44. package/dist/Composition-VJWY2NDL.js +0 -5
  45. package/dist/chunk-2ROVCPSL.cjs.map +0 -1
  46. package/dist/chunk-FJA5VUOF.cjs.map +0 -1
  47. package/dist/chunk-XTIHKODH.js.map +0 -1
  48. package/dist/chunk-ZNKQH356.js.map +0 -1
package/dist/index.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
- var chunk2ROVCPSL_cjs = require('./chunk-2ROVCPSL.cjs');
4
- var chunkFJA5VUOF_cjs = require('./chunk-FJA5VUOF.cjs');
3
+ var chunkJW67MDHQ_cjs = require('./chunk-JW67MDHQ.cjs');
4
+ var chunkANO5R2E6_cjs = require('./chunk-ANO5R2E6.cjs');
5
5
  var chunkUQBGW5DO_cjs = require('./chunk-UQBGW5DO.cjs');
6
6
  var PIXI = require('pixi.js');
7
7
  var gsap = require('gsap');
@@ -203,6 +203,118 @@ function spectralCentroid(samples, sampleRate, from = 0, to = samples.length) {
203
203
  return den > 1e-9 ? Math.round(num3 / den / 10) * 10 : 0;
204
204
  }
205
205
 
206
+ // src/audio/loudness.ts
207
+ var LOUDNESS_NOTES = { quietBelow: -24, loudAbove: -9, headroomDb: -1 };
208
+ function kWeighting(fs) {
209
+ const f0 = 1681.974450955533, G = 3.999843853973347, Q = 0.7071752369554196;
210
+ const K = Math.tan(Math.PI * f0 / fs), Vh = 10 ** (G / 20), Vb = Vh ** 0.4996667741545416;
211
+ const a0 = 1 + K / Q + K * K;
212
+ const shelf = {
213
+ b: [(Vh + Vb * K / Q + K * K) / a0, 2 * (K * K - Vh) / a0, (Vh - Vb * K / Q + K * K) / a0],
214
+ a: [1, 2 * (K * K - 1) / a0, (1 - K / Q + K * K) / a0]
215
+ };
216
+ const f1 = 38.13547087602444, Q1 = 0.5003270373238773, K1 = Math.tan(Math.PI * f1 / fs), a1 = 1 + K1 / Q1 + K1 * K1;
217
+ const high = { b: [1, -2, 1], a: [1, 2 * (K1 * K1 - 1) / a1, (1 - K1 / Q1 + K1 * K1) / a1] };
218
+ return [shelf, high];
219
+ }
220
+ function filter(x, stages) {
221
+ let cur = x;
222
+ for (const { b, a } of stages) {
223
+ const out = new Float32Array(cur.length);
224
+ let x1 = 0, x2 = 0, y1 = 0, y2 = 0;
225
+ for (let i = 0; i < cur.length; i++) {
226
+ const xi = cur[i], y = b[0] * xi + b[1] * x1 + b[2] * x2 - a[1] * y1 - a[2] * y2;
227
+ x2 = x1;
228
+ x1 = xi;
229
+ y2 = y1;
230
+ y1 = y;
231
+ out[i] = y;
232
+ }
233
+ cur = out;
234
+ }
235
+ return cur;
236
+ }
237
+ var toDb = (x) => Math.round(200 * Math.log10(Math.max(x, 1e-6))) / 10;
238
+ var TP_HALF = 12;
239
+ var TP_BLOCK = 32;
240
+ var TP_KERNELS = [0.25, 0.5, 0.75].map((p) => Float64Array.from({ length: 2 * TP_HALF }, (_, j) => {
241
+ const k = j - TP_HALF + 1, x = k - p, s = x === 0 ? 1 : Math.sin(Math.PI * x) / (Math.PI * x);
242
+ return s * (0.5 + 0.5 * Math.cos(Math.PI * x / (TP_HALF + 1)));
243
+ }));
244
+ var TP_GAIN = Math.max(...TP_KERNELS.map((k) => k.reduce((a, v) => a + Math.abs(v), 0)));
245
+ function truePeak(chans) {
246
+ let peak = 0;
247
+ for (const c of chans) {
248
+ const n = c.length;
249
+ const blocks = Math.ceil(n / TP_BLOCK), blockMax = new Float32Array(blocks + 2);
250
+ for (let i = 0; i < n; i++) {
251
+ const a = Math.abs(c[i]), b = (i / TP_BLOCK | 0) + 1;
252
+ if (a > blockMax[b]) blockMax[b] = a;
253
+ if (a > peak) peak = a;
254
+ }
255
+ const padded = new Float32Array(n + 2 * TP_HALF);
256
+ padded.set(c, TP_HALF);
257
+ for (let i = 0; i < n; i++) {
258
+ const b = (i / TP_BLOCK | 0) + 1;
259
+ const local = Math.max(blockMax[b - 1], blockMax[b], blockMax[b + 1]);
260
+ if (local * TP_GAIN <= peak) continue;
261
+ for (let pi = 0; pi < 3; pi++) {
262
+ const k = TP_KERNELS[pi];
263
+ let v = 0;
264
+ for (let j = 0; j < 2 * TP_HALF; j++) v += padded[i + 1 + j] * k[j];
265
+ const a = Math.abs(v);
266
+ if (a > peak) peak = a;
267
+ }
268
+ }
269
+ }
270
+ return peak;
271
+ }
272
+ function measureLoudness(channels, sampleRate, opts = {}) {
273
+ let samplePeak = 0, clipped = 0;
274
+ for (const c of channels) for (let i = 0; i < c.length; i++) {
275
+ const a = Math.abs(c[i]);
276
+ if (a > samplePeak) samplePeak = a;
277
+ if (a >= 1) clipped++;
278
+ }
279
+ const stages = kWeighting(sampleRate);
280
+ const weighted = channels.map((c) => filter(c, stages));
281
+ const block = Math.round(0.4 * sampleRate), hop = Math.round(0.1 * sampleRate);
282
+ const energies = [];
283
+ for (let s = 0; s + block <= (weighted[0]?.length ?? 0); s += hop) {
284
+ let sum = 0;
285
+ for (const w of weighted) {
286
+ let z = 0;
287
+ for (let i = s; i < s + block; i++) z += w[i] * w[i];
288
+ sum += z / block;
289
+ }
290
+ energies.push(sum);
291
+ }
292
+ const lufs = (e) => -0.691 + 10 * Math.log10(e);
293
+ const absolute = energies.filter((e) => e > 0 && lufs(e) > -70);
294
+ let integrated = null;
295
+ if (absolute.length) {
296
+ const relative = lufs(absolute.reduce((a, b) => a + b, 0) / absolute.length) - 10;
297
+ const gated = absolute.filter((e) => lufs(e) > relative);
298
+ if (gated.length) integrated = Math.round(10 * lufs(gated.reduce((a, b) => a + b, 0) / gated.length)) / 10;
299
+ }
300
+ return { integratedLufs: integrated, truePeakDb: opts.truePeak === false ? null : toDb(truePeak(channels)), samplePeakDb: toDb(samplePeak), clippedSamples: clipped };
301
+ }
302
+ function findSilences(channels, sampleRate, opts = {}) {
303
+ const min = Math.round((opts.minLength ?? 1) * sampleRate), limit = 10 ** ((opts.thresholdDb ?? -60) / 20);
304
+ const n = channels[0]?.length ?? 0, out = [];
305
+ let start = -1;
306
+ const quiet = (i) => channels.every((c) => Math.abs(c[i]) < limit);
307
+ for (let i = 0; i <= n; i++) {
308
+ const q = i < n && quiet(i);
309
+ if (q && start < 0) start = i;
310
+ if (!q && start >= 0) {
311
+ if (i - start >= min) out.push({ from: Math.round(start / sampleRate * 1e3) / 1e3, to: Math.round(i / sampleRate * 1e3) / 1e3 });
312
+ start = -1;
313
+ }
314
+ }
315
+ return out;
316
+ }
317
+
206
318
  // src/core/inspectAudio.ts
207
319
  var INAUDIBLE_DB = -50;
208
320
  var db = (x) => Math.round(10 * Math.max(-120, 20 * Math.log10(x))) / 10;
@@ -228,7 +340,19 @@ function measure(chans, sampleRate) {
228
340
  }
229
341
  function analyzeAudio(mix, sources, stats, movieDuration, opts = {}) {
230
342
  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 })'] };
343
+ return {
344
+ duration: movieDuration,
345
+ sampleRate: 0,
346
+ peakDb: -120,
347
+ peakAt: 0,
348
+ sources: [],
349
+ windows: [],
350
+ loudness: { integratedLufs: null, truePeakDb: -120, clippedSamples: 0, silences: [] },
351
+ scenes: [],
352
+ cues: [],
353
+ notes: [],
354
+ issues: ['no audio: the movie has no audio layers (sound effects need no files: { type: "audio", sfx: "pop", at: 1 })']
355
+ };
232
356
  }
233
357
  const sr = mix.sampleRate;
234
358
  const chans = Array.from({ length: mix.numberOfChannels }, (_, c) => mix.getChannelData(c));
@@ -286,6 +410,24 @@ function analyzeAudio(mix, sources, stats, movieDuration, opts = {}) {
286
410
  }
287
411
  return { layer, source, start: s.start, end: s.end, peakDb, sound };
288
412
  });
413
+ const whole = measureLoudness(chans, sr);
414
+ const silences = findSilences(chans, sr, { minLength: 1 });
415
+ 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`);
416
+ 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`);
417
+ const notes = [];
418
+ if (whole.integratedLufs !== null) {
419
+ 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`);
420
+ else if (whole.integratedLufs > LOUDNESS_NOTES.loudAbove) notes.push(`the mix is loud: ${whole.integratedLufs} LUFS (typical web video \u221214 to \u221216)`);
421
+ }
422
+ 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)`);
423
+ 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)`);
424
+ if (silences.length > 3) notes.push(`and ${silences.length - 3} more silent stretch(es) (loudness.silences lists them all)`);
425
+ const sceneList = (opts.scenes && opts.scenes.length ? opts.scenes : [{ name: "movie", start: 0, end: movieDuration }]).map((sc) => {
426
+ const a = Math.max(0, Math.round(sc.start * sr)), b = Math.min(mix.length, Math.round(sc.end * sr));
427
+ const part = chans.map((c) => c.subarray(a, Math.max(a, b)));
428
+ const m = measureLoudness(part, sr, { truePeak: false });
429
+ return { name: sc.name, from: sc.start, to: sc.end, lufs: m.integratedLufs, peakDb: m.samplePeakDb, silent: m.samplePeakDb < -60 };
430
+ });
289
431
  return {
290
432
  duration: movieDuration,
291
433
  sampleRate: sr,
@@ -293,8 +435,158 @@ function analyzeAudio(mix, sources, stats, movieDuration, opts = {}) {
293
435
  peakAt: stats?.peakAt ?? 0,
294
436
  sources: list,
295
437
  windows: windows2,
296
- issues
438
+ issues,
439
+ loudness: { integratedLufs: whole.integratedLufs, truePeakDb: whole.truePeakDb ?? -120, clippedSamples: whole.clippedSamples, silences },
440
+ scenes: sceneList,
441
+ cues: list.map((s) => ({ t: s.start, name: `${s.layer} (${s.source})` })),
442
+ notes
443
+ };
444
+ }
445
+
446
+ // src/text/fonts.ts
447
+ 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"]);
448
+ function fontAvailable(family2) {
449
+ const f = family2.trim().replace(/^["']|["']$/g, "");
450
+ if (!f || GENERIC.has(f.toLowerCase())) return true;
451
+ const g = document.createElement("canvas").getContext("2d");
452
+ if (!g) return true;
453
+ const sample = "mmmmmmmmmmlliWWWW 0123 \u3042\u3044\u3046";
454
+ const width = (stack) => {
455
+ g.font = `72px ${stack}`;
456
+ return g.measureText(sample).width;
297
457
  };
458
+ const q = JSON.stringify(f);
459
+ return width(`${q}, monospace`) !== width("monospace") || width(`${q}, serif`) !== width("serif");
460
+ }
461
+
462
+ // src/core/inspectFonts.ts
463
+ function inspectFonts(root) {
464
+ const failed = /* @__PURE__ */ new Set();
465
+ if (typeof document !== "undefined" && document.fonts) document.fonts.forEach((face) => {
466
+ if (face.status === "error") failed.add(face.family.replace(/^["']|["']$/g, ""));
467
+ });
468
+ const missing = [];
469
+ const used = /* @__PURE__ */ new Set();
470
+ const walk2 = (seq, index) => {
471
+ const spec = seq.spec;
472
+ if (spec.type === "text") {
473
+ const raw = spec.style?.fontFamily;
474
+ const family2 = Array.isArray(raw) ? raw.join(", ") : typeof raw === "string" ? raw : "";
475
+ const list = family2.split(",").map((f) => f.trim()).filter(Boolean);
476
+ for (const f of list) used.add(f.replace(/^["']|["']$/g, "").toLowerCase());
477
+ if (list.length && !list.some(fontAvailable) && !list.some((f) => failed.has(f.replace(/^["']|["']$/g, "")))) missing.push({ layer: spec.name ?? `text#${index}`, family: family2 });
478
+ }
479
+ if (seq.maskSequence) walk2(seq.maskSequence, index);
480
+ (seq._children ?? []).forEach((child, i) => walk2(child, i));
481
+ };
482
+ walk2(root, 0);
483
+ const all = [...failed];
484
+ return { missing, failed: all.filter((f) => used.has(f.toLowerCase())), failedUnused: all.filter((f) => !used.has(f.toLowerCase())) };
485
+ }
486
+
487
+ // src/core/scenes.ts
488
+ function namedScenes(rows) {
489
+ 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 }));
490
+ }
491
+
492
+ // src/core/review.ts
493
+ function sampleFrames({ totalFrames, frameRate, scenes = [], step = 0.25, cap = 240 }) {
494
+ const last = totalFrames;
495
+ const clamp2 = (f) => Math.max(0, Math.min(last, Math.round(f)));
496
+ const edges = /* @__PURE__ */ new Set([0, last]);
497
+ for (const s of scenes) {
498
+ edges.add(clamp2(s.start * frameRate));
499
+ edges.add(clamp2(s.end * frameRate - 1));
500
+ }
501
+ const room = Math.max(0, cap - edges.size);
502
+ const seconds = totalFrames / frameRate;
503
+ const wanted = Math.floor(seconds / step) + 1;
504
+ const stride = wanted <= room ? step : seconds / Math.max(1, room);
505
+ const frames = new Set(edges);
506
+ for (let t = stride; frames.size < cap && t < seconds; t += stride) frames.add(clamp2(t * frameRate));
507
+ return [...frames].sort((a, b) => a - b);
508
+ }
509
+ var AT_HELP = "use seconds (3.5), a percentage (50%), a frame (f120) or a layer's start / mid / end (name@end)";
510
+ function resolveAtList(list, ctx) {
511
+ const { frameRate: fps, totalFrames, duration, rows } = ctx;
512
+ const out = [], used = /* @__PURE__ */ new Set();
513
+ const named = rows.flatMap((r) => r.parts && r.partNames ? r.partNames.map((name, i) => ({ name, start: r.parts[i].start, end: r.parts[i].end })) : [r]);
514
+ for (const raw of String(list).split(",").map((s) => s.trim()).filter(Boolean)) {
515
+ let frame, label, m;
516
+ if (m = raw.match(/^(.+)@(start|mid|end)$/)) {
517
+ const row = named.find((r) => r.name === m[1]);
518
+ if (!row) throw new Error(`--at "${raw}": no layer named "${m[1]}" (names: ${named.slice(0, 40).map((r) => r.name).join(", ")}${named.length > 40 ? ", \u2026" : ""})`);
519
+ const t = m[2] === "start" ? row.start : m[2] === "mid" ? (row.start + row.end) / 2 : row.end - 1 / fps;
520
+ frame = Math.round(t * fps);
521
+ label = `${m[1]}-${m[2]}`;
522
+ } else if (m = raw.match(/^(\d+(?:\.\d+)?)%$/)) {
523
+ frame = Math.round(Number(m[1]) / 100 * totalFrames);
524
+ label = `${m[1]}pct`;
525
+ } else if (m = raw.match(/^f(\d+)$/i)) {
526
+ frame = Number(m[1]);
527
+ label = `f${m[1]}`;
528
+ } else if (/^\d+(\.\d+)?$/.test(raw)) {
529
+ frame = Math.round(Number(raw) * fps);
530
+ label = `${Number(raw).toFixed(2)}s`;
531
+ } else throw new Error(`--at: cannot read "${raw}": ${AT_HELP}`);
532
+ if (frame > totalFrames || frame < 0) throw new Error(`--at "${raw}" is past the end: the movie is ${duration} s (${totalFrames} frames)`);
533
+ label = label.replace(/[\\/:*?"<>|]+/g, "-");
534
+ let unique = label, n = 2;
535
+ while (used.has(unique)) unique = `${label}-${n++}`;
536
+ used.add(unique);
537
+ out.push({ label: unique, frame });
538
+ }
539
+ return out;
540
+ }
541
+ function groupIssues(perFrame) {
542
+ const groups = /* @__PURE__ */ new Map();
543
+ for (const { frame, issues } of perFrame) {
544
+ for (const message of issues) {
545
+ const key = message.replace(/-?\d+(\.\d+)?/g, "#");
546
+ const g = groups.get(key);
547
+ if (g) {
548
+ g.count++;
549
+ g.lastFrame = frame;
550
+ } else groups.set(key, { message, count: 1, firstFrame: frame, lastFrame: frame });
551
+ }
552
+ }
553
+ return [...groups.values()];
554
+ }
555
+ function splitIssues(groups, strict = false) {
556
+ if (strict) return { problems: groups, review: [] };
557
+ const overlap = (g) => /\boverlap\b/.test(g.message);
558
+ return { problems: groups.filter((g) => !overlap(g)), review: groups.filter(overlap) };
559
+ }
560
+ async function reviewMovie(movie, opts = {}) {
561
+ const rows = movie.timelineData().rows;
562
+ const at = opts.at ? resolveAtList(opts.at, { frameRate: movie.frameRate, totalFrames: movie.totalFrames, duration: movie.duration, rows }) : [];
563
+ const frames = sampleFrames({ totalFrames: movie.totalFrames, frameRate: movie.frameRate, scenes: namedScenes(rows) });
564
+ const perFrame = [];
565
+ for (const frame of frames) {
566
+ const r = await movie.inspect(frame, { layers: "none" });
567
+ if (r.issues.length) perFrame.push({ frame, issues: r.issues });
568
+ }
569
+ const grouped = splitIssues(groupIssues(perFrame), !!opts.strict);
570
+ const fonts = movie.inspectFonts();
571
+ for (const f of fonts.failedUnused) grouped.review.push({ message: `web font "${f}" failed to load, but no text layer uses it (a broken or unused @font-face)`, count: 1, firstFrame: 0, lastFrame: 0 });
572
+ for (const m of fonts.missing) {
573
+ (opts.strict ? grouped.problems : grouped.review).push({ message: `layer "${m.layer}": none of the fonts "${m.family}" is available here (it is drawn in a fallback font)`, count: 1, firstFrame: 0, lastFrame: 0 });
574
+ }
575
+ return { frames: frames.length, problems: grouped.problems, review: grouped.review, fonts, audio: movie.audioBuffer ? movie.inspectAudio() : null, at };
576
+ }
577
+
578
+ // src/core/onion.ts
579
+ function onionTimes(from, to, count) {
580
+ if (count <= 1) return [from];
581
+ return Array.from({ length: count }, (_, i) => Math.round((from + (to - from) * i / (count - 1)) * 1e6) / 1e6);
582
+ }
583
+ function onionAlphas(count, ramp = 0.25) {
584
+ const weights = Array.from({ length: count }, (_, i) => count === 1 ? 1 : ramp + (1 - ramp) * (i / (count - 1)));
585
+ let sum = 0;
586
+ return weights.map((w) => {
587
+ sum += w;
588
+ return w / sum;
589
+ });
298
590
  }
299
591
 
300
592
  // src/core/motionBlur.ts
@@ -313,6 +605,57 @@ function blurTimes(frame, frameRate, mb, duration) {
313
605
  return Array.from({ length: mb.samples }, (_, k) => Math.min(Math.max(t + ((k + 0.5) / mb.samples - 0.5) * width, 0), duration));
314
606
  }
315
607
 
608
+ // src/core/renderRange.ts
609
+ function resolveRange(range2, ctx) {
610
+ const { frameRate: fps, totalFrames, duration } = ctx;
611
+ if (range2 === void 0) return { fromFrame: 0, toFrame: totalFrames, fromSec: 0, toSec: duration, partial: false };
612
+ let from, to;
613
+ if (typeof range2 === "string") {
614
+ const rows = (ctx.rows ?? []).filter((r) => r.depth === 0).flatMap((r) => r.parts && r.partNames ? r.partNames.map((name, i) => ({ name, start: r.parts[i].start, end: r.parts[i].end })) : [r]);
615
+ const row = rows.find((r) => r.name === range2);
616
+ if (!row) throw new Error(`pixi-effects: render({ range: "${range2}" }): no layer named "${range2}" at the top level (names: ${rows.slice(0, 40).map((r) => r.name).join(", ") || "none"}${rows.length > 40 ? ", \u2026" : ""})`);
617
+ from = row.start;
618
+ to = row.end;
619
+ } else {
620
+ if (!Array.isArray(range2) || range2.length !== 2 || !range2.every((n) => typeof n === "number" && Number.isFinite(n))) {
621
+ throw new Error("pixi-effects: render({ range }) must be [from, to] in seconds (numbers) or the name of a top-level layer");
622
+ }
623
+ [from, to] = range2;
624
+ }
625
+ 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)`);
626
+ 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`);
627
+ const fromFrame = Math.round(from * fps);
628
+ const toFrame = Math.min(Math.round(to * fps), totalFrames);
629
+ 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)`);
630
+ const partial = fromFrame > 0 || toFrame < totalFrames;
631
+ return { fromFrame, toFrame, fromSec: fromFrame / fps, toSec: toFrame / fps, partial };
632
+ }
633
+ function sliceChannels(channels, sampleRate, fromSec, toSec, opts = {}) {
634
+ const fade = Math.max(0, Math.round((opts.fade ?? 0.01) * sampleRate));
635
+ const from = Math.round(fromSec * sampleRate), len = Math.max(1, Math.round(toSec * sampleRate) - from);
636
+ return channels.map((src) => {
637
+ const out = new Float32Array(len);
638
+ for (let i = 0; i < len; i++) {
639
+ const j = from + i;
640
+ if (j >= 0 && j < src.length) out[i] = src[j];
641
+ }
642
+ const f = Math.min(fade, len >> 1);
643
+ if (opts.fadeIn !== false) for (let i = 0; i < f; i++) out[i] = out[i] * (i / f);
644
+ if (opts.fadeOut !== false) for (let i = 0; i < f; i++) out[len - 1 - i] = out[len - 1 - i] * (i / f);
645
+ return out;
646
+ });
647
+ }
648
+ function resolveRenderOptions(o) {
649
+ const draft = !!o.draft;
650
+ const scale = o.scale ?? (draft ? 0.5 : 1);
651
+ 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})`);
652
+ return {
653
+ scale,
654
+ bitrate: o.video?.bitrate ?? (draft ? "low" : void 0),
655
+ motionBlur: o.motionBlur !== void 0 ? o.motionBlur : draft ? false : void 0
656
+ };
657
+ }
658
+
316
659
  // src/core/Renderer.ts
317
660
  var VIDEO_CODEC_BY_FORMAT = {
318
661
  mp4: "avc",
@@ -367,13 +710,22 @@ function makeOutputFormat(name) {
367
710
  return new mediabunny.MkvOutputFormat();
368
711
  }
369
712
  }
713
+ function sliceAudio(buffer, fromSec, toSec, fadeIn, fadeOut) {
714
+ const channels = Array.from({ length: buffer.numberOfChannels }, (_, c) => buffer.getChannelData(c));
715
+ const cut = sliceChannels(channels, buffer.sampleRate, fromSec, toSec, { fadeIn, fadeOut });
716
+ const out = new AudioBuffer({ length: cut[0].length, numberOfChannels: cut.length, sampleRate: buffer.sampleRate });
717
+ cut.forEach((data, c) => out.copyToChannel(data, c));
718
+ return out;
719
+ }
370
720
  async function exportFrames(movie, options = {}) {
371
721
  const fmt = options.format ?? "mp4";
722
+ const eff = resolveRenderOptions(options);
723
+ const range2 = resolveRange(options.range, { frameRate: movie.frameRate, totalFrames: movie.totalFrames, duration: movie.duration, rows: typeof options.range === "string" ? movie.timelineData().rows : void 0 });
372
724
  const opts = {
373
725
  format: fmt,
374
726
  video: {
375
727
  codec: options.video?.codec ?? VIDEO_CODEC_BY_FORMAT[fmt],
376
- bitrate: qualityMap[options.video?.bitrate ?? "high"] ?? mediabunny.QUALITY_HIGH
728
+ bitrate: qualityMap[eff.bitrate ?? "high"] ?? mediabunny.QUALITY_HIGH
377
729
  },
378
730
  audio: {
379
731
  codec: options.audio?.codec ?? AUDIO_CODEC_BY_FORMAT[fmt],
@@ -381,27 +733,30 @@ async function exportFrames(movie, options = {}) {
381
733
  }
382
734
  };
383
735
  const container = makeOutputFormat(opts.format);
384
- if (movie.audioBuffer) opts.audio.codec = await chooseAudioCodec(fmt, options.audio?.codec, container, movie.audioBuffer, opts.audio.bitrate);
736
+ const audio = movie.audioBuffer && range2.partial ? sliceAudio(movie.audioBuffer, range2.fromSec, range2.toSec, range2.fromFrame > 0, range2.toFrame < movie.totalFrames) : movie.audioBuffer;
737
+ if (audio) opts.audio.codec = await chooseAudioCodec(fmt, options.audio?.codec, container, audio, opts.audio.bitrate);
385
738
  const output = new mediabunny.Output({
386
739
  format: container,
387
740
  target: new mediabunny.BufferTarget()
388
741
  });
389
- const mb = options.motionBlur !== void 0 ? resolveMotionBlur(options.motionBlur, "movie.render()") : movie.motionBlur ?? null;
742
+ const mb = eff.motionBlur !== void 0 ? resolveMotionBlur(eff.motionBlur, "movie.render()") : movie.motionBlur ?? null;
390
743
  const stage = movie.app.canvas;
391
744
  const blurCanvas = mb ? Object.assign(document.createElement("canvas"), { width: stage.width, height: stage.height }) : null;
392
- const canvasSource = new mediabunny.CanvasSource(blurCanvas ?? stage, {
745
+ 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;
746
+ const smallCtx = small?.getContext("2d") ?? null;
747
+ const canvasSource = new mediabunny.CanvasSource(small ?? blurCanvas ?? stage, {
393
748
  codec: opts.video.codec,
394
749
  bitrate: opts.video.bitrate
395
750
  });
396
751
  output.addVideoTrack(canvasSource, { frameRate: movie.frameRate });
397
- if (movie.audioBuffer) {
752
+ if (audio) {
398
753
  const audioSource = new mediabunny.AudioBufferSource({
399
754
  codec: opts.audio.codec,
400
755
  bitrate: opts.audio.bitrate
401
- }, { startTimestamp: opts.audio.codec === "aac" ? -2112 / movie.audioBuffer.sampleRate : 0 });
756
+ }, { startTimestamp: opts.audio.codec === "aac" ? -2112 / audio.sampleRate : 0 });
402
757
  output.addAudioTrack(audioSource);
403
758
  await output.start();
404
- await audioSource.add(movie.audioBuffer);
759
+ await audioSource.add(audio);
405
760
  await audioSource.close();
406
761
  } else {
407
762
  await output.start();
@@ -409,18 +764,21 @@ async function exportFrames(movie, options = {}) {
409
764
  movie.app.ticker.stop();
410
765
  const keyframeIntervalFrames = Math.max(1, Math.round(2 * movie.frameRate));
411
766
  try {
412
- for (let frame = 0; frame <= movie.totalFrames; frame++) {
767
+ const count = Math.max(1, range2.toFrame - range2.fromFrame);
768
+ for (let frame = range2.fromFrame; frame <= range2.toFrame; frame++) {
413
769
  if (mb && blurCanvas) await movie._exposeFrame(frame, mb, blurCanvas, false);
414
770
  else await movie.gotoFrame(frame, true);
415
- const isKey = frame === 0 || frame % keyframeIntervalFrames === 0;
771
+ if (small && smallCtx) smallCtx.drawImage(blurCanvas ?? stage, 0, 0, small.width, small.height);
772
+ const rel = frame - range2.fromFrame;
773
+ const isKey = rel === 0 || rel % keyframeIntervalFrames === 0;
416
774
  const addOpts = isKey ? { keyFrame: true } : void 0;
417
- await canvasSource.add(frame / movie.frameRate, 1 / movie.frameRate, addOpts);
418
- const progress = Math.floor(frame / movie.totalFrames * 100);
775
+ await canvasSource.add(rel / movie.frameRate, 1 / movie.frameRate, addOpts);
776
+ const progress = Math.floor(rel / count * 100);
419
777
  movie.emit("progress", { progress, frame, totalFrames: movie.totalFrames });
420
778
  }
421
779
  await canvasSource.close();
422
780
  await output.finalize();
423
- if (mb) await movie.gotoFrame(movie.totalFrames, true);
781
+ if (mb) await movie.gotoFrame(range2.toFrame, true);
424
782
  return new Blob([output.target.buffer], { type: output.format.mimeType });
425
783
  } finally {
426
784
  movie.app.ticker.start();
@@ -867,16 +1225,16 @@ function expandTransitions(spec) {
867
1225
  if (toUses > 1) {
868
1226
  throw new Error(`pixi-effects: ${tag} sequence "${t.to}" is used as \`to\` in more than one transition`);
869
1227
  }
870
- const tStart = chunkFJA5VUOF_cjs.resolveAt(t.at, parentDuration);
1228
+ const tStart = chunkANO5R2E6_cjs.resolveAt(t.at, parentDuration);
871
1229
  const tEnd = tStart + t.duration;
872
1230
  const fromSeq = fromEntry.seq;
873
- const fromStart = chunkFJA5VUOF_cjs.resolveAt(fromSeq.at, parentDuration);
1231
+ const fromStart = chunkANO5R2E6_cjs.resolveAt(fromSeq.at, parentDuration);
874
1232
  const fromEnd = fromStart + (fromSeq.duration ?? parentDuration);
875
1233
  if (tStart < fromStart - EPS || tEnd > fromEnd + EPS) {
876
1234
  throw new Error(`pixi-effects: ${tag} window [${tStart}, ${tEnd}] is not covered by \`from\` "${t.from}" (lives [${fromStart}, ${fromEnd}], so it ends at ${fromEnd})`);
877
1235
  }
878
1236
  const toSeq = toEntry.seq;
879
- const toStart = chunkFJA5VUOF_cjs.resolveAt(toSeq.at, parentDuration);
1237
+ const toStart = chunkANO5R2E6_cjs.resolveAt(toSeq.at, parentDuration);
880
1238
  const toEnd = toStart + (toSeq.duration ?? parentDuration);
881
1239
  if (tStart < toStart - EPS || tEnd > toEnd + EPS) {
882
1240
  throw new Error(`pixi-effects: ${tag} window [${tStart}, ${tEnd}] is not covered by \`to\` "${t.to}" (lives [${toStart}, ${toEnd}], so it starts at ${toStart})`);
@@ -1216,9 +1574,9 @@ function inspectScene(root, frame, time, canvas, opts = {}) {
1216
1574
  onCanvas = inter === 0 && area > 0 ? "none" : inter >= area - 1e-6 ? "full" : "partial";
1217
1575
  if (area === 0) onCanvas = "none";
1218
1576
  }
1219
- const keys = chunkFJA5VUOF_cjs.collectPropKeys(seq.spec);
1577
+ const keys = chunkANO5R2E6_cjs.collectPropKeys(seq.spec);
1220
1578
  const carried = carriers.some((c) => {
1221
- const k = chunkFJA5VUOF_cjs.collectPropKeys(c.spec);
1579
+ const k = chunkANO5R2E6_cjs.collectPropKeys(c.spec);
1222
1580
  return (c.spec.keyframes ?? []).length > 0 && (k.has("x") || k.has("y") || k.has("rotation") || k.has("scale"));
1223
1581
  });
1224
1582
  const moving = parentMoving || carried || (seq.spec.keyframes ?? []).length > 0 && (keys.has("x") || keys.has("y"));
@@ -1239,7 +1597,7 @@ function inspectScene(root, frame, time, canvas, opts = {}) {
1239
1597
  const group = parentScene ?? (name !== void 0 && blending.has(name) ? name : void 0);
1240
1598
  if (group) scene.set(info, group);
1241
1599
  if (zero || seq.showsNothingYet) scaledToNothing.add(info);
1242
- if (seq instanceof chunk2ROVCPSL_cjs.CompositionSequence) walk2(seq, prefix + label + "/", insideThreeD || threeD, visible, group, moving, zero);
1600
+ if (seq instanceof chunkJW67MDHQ_cjs.CompositionSequence) walk2(seq, prefix + label + "/", insideThreeD || threeD, visible, group, moving, zero);
1243
1601
  });
1244
1602
  };
1245
1603
  walk2(root, "", false, true);
@@ -1300,10 +1658,10 @@ function walk(comp, parentStart, parentEnd, depth, prefix, rows, transitions) {
1300
1658
  const start = parentStart + (seq.at ?? 0);
1301
1659
  const end = Math.min(parentEnd, seq.duration !== void 0 ? start + seq.duration : parentEnd);
1302
1660
  const span = end - start;
1303
- const keys = (seq.spec.keyframes ?? []).map((kf) => round(start + chunkFJA5VUOF_cjs.resolveAt(kf.at, span))).sort((a, b) => a - b).slice(0, MAX_KEYS);
1661
+ const keys = (seq.spec.keyframes ?? []).map((kf) => round(start + chunkANO5R2E6_cjs.resolveAt(kf.at, span))).sort((a, b) => a - b).slice(0, MAX_KEYS);
1304
1662
  const row = { path: prefix + name, name, type: seq.spec.type, depth, start: round(start), end: round(end), keys, detail: detailOf(seq) };
1305
1663
  const inner = [];
1306
- const isComp = seq instanceof chunk2ROVCPSL_cjs.CompositionSequence;
1664
+ const isComp = seq instanceof chunkJW67MDHQ_cjs.CompositionSequence;
1307
1665
  if (isComp) walk(seq, start, end, depth + 1, prefix + name + "/", inner, transitions);
1308
1666
  entries.push({ row, inner, groupable: !isComp });
1309
1667
  });
@@ -1336,7 +1694,8 @@ function groupFamilies(entries) {
1336
1694
  start: Math.min(...members.map((x) => x.start)),
1337
1695
  end: Math.max(...members.map((x) => x.end)),
1338
1696
  keys: members.flatMap((x) => x.keys).sort((a, b) => a - b).slice(0, MAX_KEYS),
1339
- parts: members.map((x) => ({ start: x.start, end: x.end }))
1697
+ parts: members.map((x) => ({ start: x.start, end: x.end })),
1698
+ partNames: members.map((x) => x.name)
1340
1699
  });
1341
1700
  } else {
1342
1701
  out.push(e.row, ...e.inner);
@@ -1851,7 +2210,7 @@ var Movie = class {
1851
2210
  height: options.composition.height ?? this.height,
1852
2211
  duration: options.composition.duration ?? this.duration
1853
2212
  } : void 0;
1854
- await chunkFJA5VUOF_cjs.ensureFilterLibrary(seededComposition);
2213
+ await chunkANO5R2E6_cjs.ensureFilterLibrary(seededComposition);
1855
2214
  const userComposition = seededComposition ? expandTransitions(seededComposition) : void 0;
1856
2215
  const rootSeqSpec = {
1857
2216
  type: "composition",
@@ -1861,7 +2220,7 @@ var Movie = class {
1861
2220
  ...userComposition
1862
2221
  };
1863
2222
  if (userComposition) carryTransitionWindows(userComposition, rootSeqSpec);
1864
- const composition = new chunk2ROVCPSL_cjs.CompositionSequence(rootSeqSpec, null, rootShape);
2223
+ const composition = new chunkJW67MDHQ_cjs.CompositionSequence(rootSeqSpec, null, rootShape);
1865
2224
  await composition.build();
1866
2225
  this._rootSequence = composition;
1867
2226
  if (composition.target) root.addChild(composition.target);
@@ -2059,6 +2418,37 @@ var Movie = class {
2059
2418
  })));
2060
2419
  return new Blob([buildPdf(pages, { title: opts.title })], { type: "application/pdf" });
2061
2420
  }
2421
+ async onionSkin(opts = {}) {
2422
+ chunkUQBGW5DO_cjs.warnUnknownOptions("movie.onionSkin()", opts, ["from", "to", "count", "scale", "as", "motionBlur"]);
2423
+ this._requireReady("onionSkin");
2424
+ const from = opts.from ?? 0, to = opts.to ?? this.duration, count = opts.count ?? 8, scale = opts.scale ?? 1;
2425
+ 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}`);
2426
+ 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}`);
2427
+ if (!(scale > 0 && scale <= 4)) throw new Error(`pixi-effects: movie.onionSkin(): scale must be above 0 and at most 4, got ${scale}`);
2428
+ const mb = this._blurFor(opts.motionBlur, "movie.onionSkin()");
2429
+ const blurred = mb ? this._blurCanvas() : null;
2430
+ const out = document.createElement("canvas");
2431
+ out.width = Math.max(1, Math.round(this.width * scale));
2432
+ out.height = Math.max(1, Math.round(this.height * scale));
2433
+ const g = out.getContext("2d");
2434
+ const alphas = onionAlphas(count);
2435
+ const frames = onionTimes(from, to, count).map((t) => clampFrame(t * this.frameRate, this.totalFrames));
2436
+ const back = this.currentFrame, wasAtPoster = this._atPoster;
2437
+ try {
2438
+ for (let i = 0; i < frames.length; i++) {
2439
+ const f = frames[i];
2440
+ if (mb && blurred) await this._quietly(() => this._exposeFrame(f, mb, blurred, true));
2441
+ else await this._quietly(() => this.gotoFrame(f, true));
2442
+ g.globalAlpha = alphas[i];
2443
+ g.drawImage(blurred ?? this.app.canvas, 0, 0, out.width, out.height);
2444
+ }
2445
+ } finally {
2446
+ g.globalAlpha = 1;
2447
+ if (wasAtPoster) await this._showPoster();
2448
+ else await this._quietly(() => this.gotoFrame(back, true));
2449
+ }
2450
+ return encodeCanvas(out, "image/png", opts.as ?? "blob");
2451
+ }
2062
2452
  async contactSheet(opts = {}) {
2063
2453
  chunkUQBGW5DO_cjs.warnUnknownOptions("movie.contactSheet()", opts, ["frames", "times", "count", "columns", "cellWidth", "as", "motionBlur"]);
2064
2454
  this._requireReady("contactSheet");
@@ -2125,10 +2515,40 @@ var Movie = class {
2125
2515
  * The soundtrack, measured: when every audio layer plays, how loud the mix is over time, and `issues`
2126
2516
  * (limiting, inaudible layers, sounds cut off by the end). Use it to CHECK sound you cannot hear.
2127
2517
  */
2518
+ /** The pictures to look at for moments such as `3.5`, `50%`, `f120`, `title@end` (comma-separated). A mistake says what could not be read. */
2519
+ resolveAt(list) {
2520
+ this._requireReady("resolveAt");
2521
+ return resolveAtList(list, { frameRate: this.frameRate, totalFrames: this.totalFrames, duration: this.duration, rows: this.timelineData().rows });
2522
+ }
2523
+ /**
2524
+ * One call that looks at everything `pixi-effects-check` looks at inside the page: layout issues over the whole timeline (every
2525
+ * scene's first and last frame and a frame every 0.25 s), fonts and the sound. See `ReviewReport`. Page warnings are in `window.__logs`. The movie is left as it was.
2526
+ */
2527
+ async review(opts = {}) {
2528
+ chunkUQBGW5DO_cjs.warnUnknownOptions("movie.review()", opts, ["at", "strict"]);
2529
+ this._requireReady("review");
2530
+ const back = this.currentFrame, wasAtPoster = this._atPoster;
2531
+ try {
2532
+ return await reviewMovie(this, opts);
2533
+ } finally {
2534
+ this.currentFrame = back;
2535
+ if (wasAtPoster) await this._showPoster();
2536
+ else await this._quietly(() => this.gotoFrame(back, true));
2537
+ }
2538
+ }
2539
+ /**
2540
+ * The fonts of the text layers: `missing` lists layers none of whose `fontFamily` entries is available in this browser (they are
2541
+ * drawn in a fallback font), `failed` lists web fonts (`@font-face`) whose file could not be loaded.
2542
+ */
2543
+ inspectFonts() {
2544
+ this._requireReady("inspectFonts");
2545
+ return inspectFonts(this._rootSequence);
2546
+ }
2128
2547
  inspectAudio(opts = {}) {
2129
- chunkUQBGW5DO_cjs.warnUnknownOptions("movie.inspectAudio()", opts, ["window"]);
2548
+ chunkUQBGW5DO_cjs.warnUnknownOptions("movie.inspectAudio()", opts, ["window", "scenes"]);
2130
2549
  this._requireReady("inspectAudio");
2131
- return analyzeAudio(this.audioBuffer, this._audioSources, this._mixStats, this.duration, opts);
2550
+ const scenes = opts.scenes ?? namedScenes(this.timelineData().rows);
2551
+ return analyzeAudio(this.audioBuffer, this._audioSources, this._mixStats, this.duration, { ...opts, scenes });
2132
2552
  }
2133
2553
  /**
2134
2554
  * Draw the stage now. PixiJS's CullerPlugin only culls inside `app.render()` (the ticker), and we render
@@ -2396,7 +2816,7 @@ var Movie = class {
2396
2816
  return this.muted;
2397
2817
  }
2398
2818
  async render(options) {
2399
- chunkUQBGW5DO_cjs.warnUnknownOptions("movie.render()", options, ["format", "video", "audio", "motionBlur"]);
2819
+ chunkUQBGW5DO_cjs.warnUnknownOptions("movie.render()", options, ["format", "video", "audio", "motionBlur", "range", "scale", "draft"]);
2400
2820
  try {
2401
2821
  return await this._quietly(() => exportFrames(this, options));
2402
2822
  } catch (err) {
@@ -2607,6 +3027,7 @@ function clamp(v, lo, hi) {
2607
3027
  return Math.max(lo, Math.min(hi, v));
2608
3028
  }
2609
3029
  function parseEase(name) {
3030
+ chunkANO5R2E6_cjs.checkEase(name, "a preset");
2610
3031
  return gsap.gsap.parseEase(name);
2611
3032
  }
2612
3033
 
@@ -2629,17 +3050,17 @@ function orbit(opts) {
2629
3050
  throw new Error("pixi-effects: orbit() cannot combine `radius` with `dollyZoom` \u2014 the radius follows the animated fov");
2630
3051
  }
2631
3052
  const [cx, cy] = opts.center ?? [width / 2, height / 2];
2632
- const fixedR = opts.radius ?? chunkFJA5VUOF_cjs.homeDistance(height, fov ?? chunkFJA5VUOF_cjs.DEFAULT_FOV);
3053
+ const fixedR = opts.radius ?? chunkANO5R2E6_cjs.homeDistance(height, fov ?? chunkANO5R2E6_cjs.DEFAULT_FOV);
2633
3054
  const start = opts.start ?? -degrees / 2;
2634
3055
  const steps = Math.max(1, Math.round(duration * stepsPerSecond));
2635
3056
  const easeFn = parseEase(ease);
2636
3057
  const dollyEase = dolly ? parseEase(dolly.ease ?? ease) : null;
2637
3058
  const pos = (i) => {
2638
3059
  const p = i / steps;
2639
- const a = (start + degrees * easeFn(p)) * chunkFJA5VUOF_cjs.DEG;
3060
+ const a = (start + degrees * easeFn(p)) * chunkANO5R2E6_cjs.DEG;
2640
3061
  if (!dolly) return { x: cx + fixedR * Math.sin(a), z: fixedR * Math.cos(a) };
2641
3062
  const f = dolly.from + (dolly.to - dolly.from) * dollyEase(p);
2642
- const R = chunkFJA5VUOF_cjs.homeDistance(height, f);
3063
+ const R = chunkANO5R2E6_cjs.homeDistance(height, f);
2643
3064
  return { x: cx + R * Math.sin(a), z: R * Math.cos(a), fov: f };
2644
3065
  };
2645
3066
  const keyframes = [];
@@ -2694,7 +3115,7 @@ function particles(opts) {
2694
3115
  const round3 = (v) => Math.round(v * 1e6) / 1e6;
2695
3116
  const out = [];
2696
3117
  for (let i = 0; i < count; i++) {
2697
- const r = chunkFJA5VUOF_cjs.random(seed * 1000003 + i * 7919 + 17);
3118
+ const r = chunkANO5R2E6_cjs.random(seed * 1000003 + i * 7919 + 17);
2698
3119
  const pick = ([a2, b]) => a2 + r() * (b - a2);
2699
3120
  const born = at + r() * emit;
2700
3121
  const lifetime = pick(life);
@@ -2707,7 +3128,7 @@ function particles(opts) {
2707
3128
  const turn = spin ? pick(spin) : 0;
2708
3129
  const startAngle = spin ? r() * 360 : 0;
2709
3130
  const swayAmp = sway?.amp ?? 0, swayFreq = sway?.freq ?? 1;
2710
- const n0 = sway ? chunkFJA5VUOF_cjs.noise(0, seed * 1000003 + i) : 0;
3131
+ const n0 = sway ? chunkANO5R2E6_cjs.noise(0, seed * 1000003 + i) : 0;
2711
3132
  const at_ = (t) => {
2712
3133
  const axis = (p0, v0, acc) => {
2713
3134
  if (drag > 0) {
@@ -2716,7 +3137,7 @@ function particles(opts) {
2716
3137
  }
2717
3138
  return p0 + v0 * t + 0.5 * acc * t * t;
2718
3139
  };
2719
- const sw = sway ? swayAmp * (chunkFJA5VUOF_cjs.noise(t * swayFreq, seed * 1000003 + i) - n0) : 0;
3140
+ const sw = sway ? swayAmp * (chunkANO5R2E6_cjs.noise(t * swayFreq, seed * 1000003 + i) - n0) : 0;
2720
3141
  return { x: axis(x0, vx, wind) + sw, y: axis(y0, vy, gravity) };
2721
3142
  };
2722
3143
  const kfs = [];
@@ -2778,7 +3199,7 @@ function followPath(opts) {
2778
3199
  if (steps > MAX_STEPS) throw new Error(`followPath(): ${steps} samples is too many (max ${MAX_STEPS}); lower frameRate or duration`);
2779
3200
  const segments = [];
2780
3201
  let total = 0;
2781
- for (const line of chunkFJA5VUOF_cjs.flattenSvgPath(d)) {
3202
+ for (const line of chunkANO5R2E6_cjs.flattenSvgPath(d)) {
2782
3203
  const p = line.closed ? [...line.pts, line.pts[0], line.pts[1]] : line.pts;
2783
3204
  for (let i = 2; i < p.length; i += 2) {
2784
3205
  const len = Math.hypot(p[i] - p[i - 2], p[i + 1] - p[i - 1]);
@@ -2931,7 +3352,7 @@ function distances(n, from, grid, seed) {
2931
3352
  if (n === 0) return [];
2932
3353
  if (from === "random") {
2933
3354
  const order = Array.from({ length: n }, (_, i) => i);
2934
- const r = chunkFJA5VUOF_cjs.random(seed);
3355
+ const r = chunkANO5R2E6_cjs.random(seed);
2935
3356
  for (let i = n - 1; i > 0; i--) {
2936
3357
  const j = Math.floor(r() * (i + 1));
2937
3358
  [order[i], order[j]] = [order[j], order[i]];
@@ -2993,7 +3414,7 @@ function wiggle(opts) {
2993
3414
  to[n] = p.around;
2994
3415
  return;
2995
3416
  }
2996
- const off = (chunkFJA5VUOF_cjs.rand(seed * 7919 + pi * 104729 + s * 31 + 1) * 2 - 1) * p.amp;
3417
+ const off = (chunkANO5R2E6_cjs.rand(seed * 7919 + pi * 104729 + s * 31 + 1) * 2 - 1) * p.amp;
2997
3418
  to[n] = typeof p.around === "number" ? p.around + off : `(${p.around}) ${off < 0 ? "-" : "+"} ${Math.abs(off)}`;
2998
3419
  });
2999
3420
  out.push({ at: at + s * stepLen, to, duration: stepLen, ease });
@@ -3458,19 +3879,19 @@ function withFade(spec, opts) {
3458
3879
 
3459
3880
  Object.defineProperty(exports, "noise", {
3460
3881
  enumerable: true,
3461
- get: function () { return chunkFJA5VUOF_cjs.noise; }
3882
+ get: function () { return chunkANO5R2E6_cjs.noise; }
3462
3883
  });
3463
3884
  Object.defineProperty(exports, "rand", {
3464
3885
  enumerable: true,
3465
- get: function () { return chunkFJA5VUOF_cjs.rand; }
3886
+ get: function () { return chunkANO5R2E6_cjs.rand; }
3466
3887
  });
3467
3888
  Object.defineProperty(exports, "random", {
3468
3889
  enumerable: true,
3469
- get: function () { return chunkFJA5VUOF_cjs.random; }
3890
+ get: function () { return chunkANO5R2E6_cjs.random; }
3470
3891
  });
3471
3892
  Object.defineProperty(exports, "registerSequenceType", {
3472
3893
  enumerable: true,
3473
- get: function () { return chunkFJA5VUOF_cjs.registerSequenceType; }
3894
+ get: function () { return chunkANO5R2E6_cjs.registerSequenceType; }
3474
3895
  });
3475
3896
  exports.Movie = Movie;
3476
3897
  exports.animateText = animateText;