pixi-effects 0.6.0 → 0.8.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 +29 -0
  2. package/README.md +18 -11
  3. package/ai/SKILL.md +1 -1
  4. package/ai/reference/cheatsheet.md +9 -1
  5. package/ai/template.html +4 -3
  6. package/ai/tools/check.mjs +14 -5
  7. package/ai/tools/render.mjs +202 -0
  8. package/ai/tools/view.mjs +269 -0
  9. package/dist/{Base-4vNLl4tH.d.cts → Base-CSIulyIq.d.cts} +1 -1
  10. package/dist/{Base-CD4AoT5-.d.ts → Base-DVgwyIUj.d.ts} +1 -1
  11. package/dist/Composition-CM62XJKT.cjs +14 -0
  12. package/dist/{Composition-Y5VZSDDP.cjs.map → Composition-CM62XJKT.cjs.map} +1 -1
  13. package/dist/Composition-RJDSQYTV.js +5 -0
  14. package/dist/{Composition-7SO5WYHU.js.map → Composition-RJDSQYTV.js.map} +1 -1
  15. package/dist/Controller.cjs +122 -20
  16. package/dist/Controller.cjs.map +1 -1
  17. package/dist/Controller.d.cts +45 -3
  18. package/dist/Controller.d.ts +45 -3
  19. package/dist/Controller.js +122 -21
  20. package/dist/Controller.js.map +1 -1
  21. package/dist/{Movie-Ch2SUI1h.d.ts → Movie-CQOhUmko.d.cts} +75 -2
  22. package/dist/{Movie-CADDBfpl.d.cts → Movie-HF3pSGjQ.d.ts} +75 -2
  23. package/dist/{chunk-KQM2ZZMK.cjs → chunk-AGMNSJJH.cjs} +175 -29
  24. package/dist/chunk-AGMNSJJH.cjs.map +1 -0
  25. package/dist/{chunk-7IJHJXDI.js → chunk-FNKSXBKB.js} +136 -10
  26. package/dist/chunk-FNKSXBKB.js.map +1 -0
  27. package/dist/{chunk-B2FTJLNB.js → chunk-O4AJYUPK.js} +3 -3
  28. package/dist/{chunk-B2FTJLNB.js.map → chunk-O4AJYUPK.js.map} +1 -1
  29. package/dist/{chunk-4AGJF5RK.cjs → chunk-PDBDDGG5.cjs} +21 -21
  30. package/dist/{chunk-4AGJF5RK.cjs.map → chunk-PDBDDGG5.cjs.map} +1 -1
  31. package/dist/index.cjs +255 -25
  32. package/dist/index.cjs.map +1 -1
  33. package/dist/index.d.cts +4 -4
  34. package/dist/index.d.ts +4 -4
  35. package/dist/index.js +248 -18
  36. package/dist/index.js.map +1 -1
  37. package/dist/three.cjs +6 -5
  38. package/dist/three.cjs.map +1 -1
  39. package/dist/three.d.cts +2 -2
  40. package/dist/three.d.ts +2 -2
  41. package/dist/three.js +2 -1
  42. package/dist/three.js.map +1 -1
  43. package/dist/{types-DrW9QeKU.d.ts → types-oJijbn-K.d.cts} +14 -1
  44. package/dist/{types-DrW9QeKU.d.cts → types-oJijbn-K.d.ts} +14 -1
  45. package/docs/api.md +57 -0
  46. package/docs/dsl.md +60 -1
  47. package/llms-full.txt +127 -3
  48. package/package.json +7 -4
  49. package/dist/Composition-7SO5WYHU.js +0 -4
  50. package/dist/Composition-Y5VZSDDP.cjs +0 -13
  51. package/dist/chunk-7IJHJXDI.js.map +0 -1
  52. package/dist/chunk-KQM2ZZMK.cjs.map +0 -1
package/dist/index.cjs CHANGED
@@ -1,8 +1,8 @@
1
1
  'use strict';
2
2
 
3
+ var chunkPDBDDGG5_cjs = require('./chunk-PDBDDGG5.cjs');
4
+ var chunkAGMNSJJH_cjs = require('./chunk-AGMNSJJH.cjs');
3
5
  var chunkUQBGW5DO_cjs = require('./chunk-UQBGW5DO.cjs');
4
- var chunk4AGJF5RK_cjs = require('./chunk-4AGJF5RK.cjs');
5
- var chunkKQM2ZZMK_cjs = require('./chunk-KQM2ZZMK.cjs');
6
6
  var PIXI = require('pixi.js');
7
7
  var gsap = require('gsap');
8
8
  var PixiPlugin = require('gsap/PixiPlugin');
@@ -181,7 +181,7 @@ function fft(re, im) {
181
181
  }
182
182
  var FRAME = 2048;
183
183
  function spectralCentroid(samples, sampleRate, from = 0, to = samples.length) {
184
- let num = 0;
184
+ let num2 = 0;
185
185
  let den = 0;
186
186
  const re = new Float64Array(FRAME);
187
187
  const im = new Float64Array(FRAME);
@@ -196,11 +196,11 @@ function spectralCentroid(samples, sampleRate, from = 0, to = samples.length) {
196
196
  fft(re, im);
197
197
  for (let b = 1; b < FRAME / 2; b++) {
198
198
  const power = re[b] * re[b] + im[b] * im[b];
199
- num += power * (b * sampleRate / FRAME);
199
+ num2 += power * (b * sampleRate / FRAME);
200
200
  den += power;
201
201
  }
202
202
  }
203
- return den > 1e-9 ? Math.round(num / den / 10) * 10 : 0;
203
+ return den > 1e-9 ? Math.round(num2 / den / 10) * 10 : 0;
204
204
  }
205
205
 
206
206
  // src/core/inspectAudio.ts
@@ -308,6 +308,28 @@ var AUDIO_CODEC_BY_FORMAT = {
308
308
  webm: "opus",
309
309
  mkv: "opus"
310
310
  };
311
+ var AUDIO_PREFERENCE = {
312
+ mp4: ["aac", "opus", "mp3", "flac"],
313
+ mov: ["aac", "mp3"],
314
+ webm: ["opus", "vorbis"],
315
+ mkv: ["opus", "vorbis", "aac", "flac", "mp3"]
316
+ };
317
+ async function chooseAudioCodec(fmt, requested, container, audio, bitrate) {
318
+ const supported = container.getSupportedAudioCodecs?.();
319
+ const preferred = AUDIO_PREFERENCE[fmt].filter((c) => !supported || supported.includes(c));
320
+ const canEncode = (list) => mediabunny.getFirstEncodableAudioCodec(list, { numberOfChannels: audio.numberOfChannels ?? 2, sampleRate: audio.sampleRate, bitrate });
321
+ if (requested) {
322
+ if (await canEncode([requested])) return requested;
323
+ const alt = await canEncode(preferred.filter((c) => c !== requested));
324
+ throw new Error(`pixi-effects: this browser cannot encode "${requested}" audio for ${fmt}` + (alt ? `; "${alt}" would work: render({ format: '${fmt}', audio: { codec: '${alt}' } }), or leave audio.codec out and the best one is chosen.` : "; it has no audio encoder for this format at all (try another browser, or a movie without sound)."));
325
+ }
326
+ const chosen = await canEncode(preferred);
327
+ if (!chosen) throw new Error(`pixi-effects: this browser has no audio encoder for ${fmt} (it tried ${preferred.join(", ")}): try another browser, or render a movie without sound.`);
328
+ if (chosen !== preferred[0]) {
329
+ console.warn(`pixi-effects: this browser cannot encode ${preferred[0].toUpperCase()} audio (Chrome on Linux has no AAC encoder), so the ${fmt} carries ${chosen} instead; current players play it. A webm export needs no AAC.`);
330
+ }
331
+ return chosen;
332
+ }
311
333
  var qualityMap = {
312
334
  "very-low": mediabunny.QUALITY_VERY_LOW,
313
335
  "low": mediabunny.QUALITY_LOW,
@@ -340,8 +362,10 @@ async function exportFrames(movie, options = {}) {
340
362
  bitrate: qualityMap[options.audio?.bitrate ?? "high"] ?? mediabunny.QUALITY_HIGH
341
363
  }
342
364
  };
365
+ const container = makeOutputFormat(opts.format);
366
+ if (movie.audioBuffer) opts.audio.codec = await chooseAudioCodec(fmt, options.audio?.codec, container, movie.audioBuffer, opts.audio.bitrate);
343
367
  const output = new mediabunny.Output({
344
- format: makeOutputFormat(opts.format),
368
+ format: container,
345
369
  target: new mediabunny.BufferTarget()
346
370
  });
347
371
  const canvasSource = new mediabunny.CanvasSource(movie.app.canvas, {
@@ -819,16 +843,16 @@ function expandTransitions(spec) {
819
843
  if (toUses > 1) {
820
844
  throw new Error(`pixi-effects: ${tag} sequence "${t.to}" is used as \`to\` in more than one transition`);
821
845
  }
822
- const tStart = chunkKQM2ZZMK_cjs.resolveAt(t.at, parentDuration);
846
+ const tStart = chunkAGMNSJJH_cjs.resolveAt(t.at, parentDuration);
823
847
  const tEnd = tStart + t.duration;
824
848
  const fromSeq = fromEntry.seq;
825
- const fromStart = chunkKQM2ZZMK_cjs.resolveAt(fromSeq.at, parentDuration);
849
+ const fromStart = chunkAGMNSJJH_cjs.resolveAt(fromSeq.at, parentDuration);
826
850
  const fromEnd = fromStart + (fromSeq.duration ?? parentDuration);
827
851
  if (tStart < fromStart || tEnd > fromEnd) {
828
852
  throw new Error(`pixi-effects: ${tag} window [${tStart}, ${tEnd}] is not covered by \`from\` "${t.from}" (lives [${fromStart}, ${fromEnd}], so it ends at ${fromEnd})`);
829
853
  }
830
854
  const toSeq = toEntry.seq;
831
- const toStart = chunkKQM2ZZMK_cjs.resolveAt(toSeq.at, parentDuration);
855
+ const toStart = chunkAGMNSJJH_cjs.resolveAt(toSeq.at, parentDuration);
832
856
  const toEnd = toStart + (toSeq.duration ?? parentDuration);
833
857
  if (tStart < toStart || tEnd > toEnd) {
834
858
  throw new Error(`pixi-effects: ${tag} window [${tStart}, ${tEnd}] is not covered by \`to\` "${t.to}" (lives [${toStart}, ${toEnd}], so it starts at ${toStart})`);
@@ -1140,7 +1164,7 @@ function inspectScene(root, frame, time, canvas, opts = {}) {
1140
1164
  const seen = /* @__PURE__ */ new Map();
1141
1165
  const scene = /* @__PURE__ */ new Map();
1142
1166
  const scaledToNothing = /* @__PURE__ */ new Set();
1143
- const walk = (comp, prefix, insideThreeD, parentVisible, parentScene, parentMoving = false, parentZero = false) => {
1167
+ const walk2 = (comp, prefix, insideThreeD, parentVisible, parentScene, parentMoving = false, parentZero = false) => {
1144
1168
  const blending = /* @__PURE__ */ new Set();
1145
1169
  for (const w of transitionWindowsOf(comp.spec)) {
1146
1170
  const start = (comp.absoluteStart ?? 0) + w.start, end = (comp.absoluteStart ?? 0) + w.end;
@@ -1168,7 +1192,7 @@ function inspectScene(root, frame, time, canvas, opts = {}) {
1168
1192
  onCanvas = inter === 0 && area > 0 ? "none" : inter >= area - 1e-6 ? "full" : "partial";
1169
1193
  if (area === 0) onCanvas = "none";
1170
1194
  }
1171
- const keys = chunkKQM2ZZMK_cjs.collectPropKeys(seq.spec);
1195
+ const keys = chunkAGMNSJJH_cjs.collectPropKeys(seq.spec);
1172
1196
  const moving = parentMoving || (seq.spec.keyframes ?? []).length > 0 && (keys.has("x") || keys.has("y"));
1173
1197
  const scale = t?.scale;
1174
1198
  const zero = parentZero || !!scale && (scale.x === 0 || scale.y === 0);
@@ -1184,10 +1208,10 @@ function inspectScene(root, frame, time, canvas, opts = {}) {
1184
1208
  const group = parentScene ?? (name !== void 0 && blending.has(name) ? name : void 0);
1185
1209
  if (group) scene.set(info, group);
1186
1210
  if (zero || seq.showsNothingYet) scaledToNothing.add(info);
1187
- if (seq instanceof chunk4AGJF5RK_cjs.CompositionSequence) walk(seq, prefix + label + "/", insideThreeD || threeD, visible, group, moving, zero);
1211
+ if (seq instanceof chunkPDBDDGG5_cjs.CompositionSequence) walk2(seq, prefix + label + "/", insideThreeD || threeD, visible, group, moving, zero);
1188
1212
  });
1189
1213
  };
1190
- walk(root, "", false, true);
1214
+ walk2(root, "", false, true);
1191
1215
  const issues = [];
1192
1216
  const texts = layers.filter((l) => l.type === "text" && l.visible && l.alpha >= FAINT && l.bounds);
1193
1217
  for (const l of texts) {
@@ -1224,6 +1248,158 @@ function inspectScene(root, frame, time, canvas, opts = {}) {
1224
1248
  return { frame, time, canvas, summary: { layers: layers.length, visible: layers.filter((l) => l.visible).length }, issues, layers: listed };
1225
1249
  }
1226
1250
 
1251
+ // src/core/timelineChart.ts
1252
+ var GROUP_MIN = 4;
1253
+ var MAX_KEYS = 200;
1254
+ var round = (n) => Math.round(n * 1e6) / 1e6;
1255
+ var family = (name) => name.replace(/\d+/g, "#");
1256
+ function detailOf(seq) {
1257
+ const s = seq.spec;
1258
+ if (typeof s.text === "string") return s.text.length > 60 ? s.text.slice(0, 57) + "\u2026" : s.text;
1259
+ if (typeof s.asset === "string") return `asset \u201C${s.asset}\u201D`;
1260
+ if (typeof s.sfx === "string") return `sfx \u201C${s.sfx}\u201D`;
1261
+ if (s.sfx && typeof s.sfx === "object") return "sfx (custom)";
1262
+ return void 0;
1263
+ }
1264
+ function walk(comp, parentStart, parentEnd, depth, prefix, rows, transitions) {
1265
+ const entries = [];
1266
+ comp._children.forEach((seq, i) => {
1267
+ const name = seq.spec.name ?? `${seq.spec.type}#${i}`;
1268
+ const start = parentStart + (seq.at ?? 0);
1269
+ const end = Math.min(parentEnd, seq.duration !== void 0 ? start + seq.duration : parentEnd);
1270
+ const span = end - start;
1271
+ const keys = (seq.spec.keyframes ?? []).map((kf) => round(start + chunkAGMNSJJH_cjs.resolveAt(kf.at, span))).sort((a, b) => a - b).slice(0, MAX_KEYS);
1272
+ const row = { path: prefix + name, name, type: seq.spec.type, depth, start: round(start), end: round(end), keys, detail: detailOf(seq) };
1273
+ const inner = [];
1274
+ const isComp = seq instanceof chunkPDBDDGG5_cjs.CompositionSequence;
1275
+ if (isComp) walk(seq, start, end, depth + 1, prefix + name + "/", inner, transitions);
1276
+ entries.push({ row, inner, groupable: !isComp });
1277
+ });
1278
+ rows.push(...groupFamilies(entries));
1279
+ for (const w of transitionWindowsOf(comp.spec)) {
1280
+ transitions.push({ from: prefix + w.from, to: prefix + w.to, start: round(parentStart + w.start), end: round(parentStart + w.end) });
1281
+ }
1282
+ }
1283
+ function groupFamilies(entries) {
1284
+ const buckets = /* @__PURE__ */ new Map();
1285
+ for (const e of entries) {
1286
+ if (!e.groupable) continue;
1287
+ const key = e.row.type + "|" + family(e.row.name);
1288
+ (buckets.get(key) ?? buckets.set(key, []).get(key)).push(e.row);
1289
+ }
1290
+ const out = [];
1291
+ const done = /* @__PURE__ */ new Set();
1292
+ for (const e of entries) {
1293
+ const key = e.row.type + "|" + family(e.row.name);
1294
+ const members = e.groupable ? buckets.get(key) : null;
1295
+ if (members && members.length >= GROUP_MIN) {
1296
+ if (done.has(key)) continue;
1297
+ done.add(key);
1298
+ const label = `${family(e.row.name)} \xD7${members.length}`;
1299
+ out.push({
1300
+ path: e.row.path.slice(0, e.row.path.length - e.row.name.length) + label,
1301
+ name: label,
1302
+ type: e.row.type,
1303
+ depth: e.row.depth,
1304
+ start: Math.min(...members.map((x) => x.start)),
1305
+ end: Math.max(...members.map((x) => x.end)),
1306
+ keys: members.flatMap((x) => x.keys).sort((a, b) => a - b).slice(0, MAX_KEYS),
1307
+ parts: members.map((x) => ({ start: x.start, end: x.end }))
1308
+ });
1309
+ } else {
1310
+ out.push(e.row, ...e.inner);
1311
+ continue;
1312
+ }
1313
+ out.push(...e.inner);
1314
+ }
1315
+ return out;
1316
+ }
1317
+ function collectTimeline(root, duration) {
1318
+ const rows = [];
1319
+ const transitions = [];
1320
+ walk(root, 0, duration, 0, "", rows, transitions);
1321
+ return { duration, rows, transitions };
1322
+ }
1323
+ var COLORS = {
1324
+ text: "#4cc9f0",
1325
+ shape: "#ffd166",
1326
+ image: "#7bd88f",
1327
+ video: "#c77dff",
1328
+ audio: "#ff7aa8",
1329
+ composition: "#8ea4c8",
1330
+ three: "#ff9f68",
1331
+ camera: "#b0b0b0"
1332
+ };
1333
+ var esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1334
+ var num = (n) => String(Math.round(n * 100) / 100);
1335
+ function tickStep(duration, chartWidth) {
1336
+ for (const s of [0.04, 0.1, 0.2, 0.5, 1, 2, 5, 10, 15, 30, 60, 120, 300, 600, 1200, 3600]) if (s / duration * chartWidth >= 70) return s;
1337
+ return 7200;
1338
+ }
1339
+ function timelineSvg(data, opts = {}) {
1340
+ const CHART = Math.max(100, opts.chartWidth ?? 950), withLabels = opts.labels !== false;
1341
+ const LABEL = withLabels ? 250 : 0, ROW = 22, TOP = 34, PAD = 16;
1342
+ const W = LABEL + CHART + PAD * 2, H = TOP + data.rows.length * ROW + PAD;
1343
+ const x = (t) => PAD + LABEL + Math.max(0, Math.min(data.duration, t)) / data.duration * CHART;
1344
+ const step = tickStep(data.duration, CHART);
1345
+ const out = [];
1346
+ for (let t = 0; t <= data.duration + 1e-9; t += step) {
1347
+ out.push(`<line class="grid" x1="${num(x(t))}" y1="${TOP - 6}" x2="${num(x(t))}" y2="${H - PAD}"/>`);
1348
+ out.push(`<text class="tick" x="${num(x(t))}" y="${TOP - 12}" text-anchor="middle">${num(t)}${t === 0 || Math.abs(t - data.duration) < 1e-9 ? " s" : ""}</text>`);
1349
+ }
1350
+ if (Math.abs(data.duration % step) > 1e-9) out.push(`<text class="tick" x="${num(x(data.duration))}" y="${TOP - 12}" text-anchor="middle">${num(data.duration)} s</text>`);
1351
+ for (const t of data.transitions) {
1352
+ out.push(`<g><title>transition ${esc(t.from)} \u2192 ${esc(t.to)} (${num(t.start)}\u2013${num(t.end)} s)</title><rect class="transition" x="${num(x(t.start))}" y="${TOP - 4}" width="${num(Math.max(2, x(t.end) - x(t.start)))}" height="${H - TOP - PAD + 4}"/></g>`);
1353
+ }
1354
+ data.rows.forEach((r, i) => {
1355
+ const y = TOP + i * ROW;
1356
+ const color = COLORS[r.type] ?? "#9aa7b8";
1357
+ const tip = `${r.path} \xB7 ${r.type} \xB7 ${num(r.start)}\u2013${num(r.end)} s (${num(r.end - r.start)} s)${r.keys.length ? ` \xB7 ${r.keys.length} keyframe${r.keys.length > 1 ? "s" : ""}` : ""}${r.detail ? ` \xB7 ${r.detail}` : ""}`;
1358
+ out.push(`<g class="row" data-start="${num(r.start)}"><title>${esc(tip)}</title>`);
1359
+ out.push(`<rect class="band" x="${PAD}" y="${y}" width="${LABEL + CHART}" height="${ROW}" ${i % 2 ? 'fill-opacity=".05"' : 'fill-opacity="0"'}/>`);
1360
+ if (withLabels) out.push(`<text class="label" x="${PAD + 6 + r.depth * 14}" y="${y + 15}">${esc(r.name)}</text>`);
1361
+ for (const p of r.parts ?? [{ start: r.start, end: r.end }]) {
1362
+ out.push(`<rect class="bar" x="${num(x(p.start))}" y="${y + 4}" width="${num(Math.max(2, x(p.end) - x(p.start)))}" height="${ROW - 8}" rx="3" fill="${color}"/>`);
1363
+ }
1364
+ for (const k of r.keys) out.push(`<path class="key" d="M ${num(x(k))} ${y + 7} l 3 4 l -3 4 l -3 -4 z"/>`);
1365
+ out.push("</g>");
1366
+ });
1367
+ const legend = Object.entries(COLORS).map(([type, c], i) => `<g transform="translate(${PAD + i * 108},${H + 10})"><rect width="12" height="12" rx="3" fill="${c}"/><text x="18" y="10" class="tick">${type}</text></g>`).join("");
1368
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H + 32}" viewBox="0 0 ${W} ${H + 32}" role="img" aria-label="Timeline of ${data.rows.length} rows" data-duration="${num(data.duration)}" data-x0="${num(x(0))}" data-x1="${num(x(data.duration))}" data-top="${TOP - 6}" data-bottom="${H - PAD}" data-row="${ROW}">
1369
+ ${out.join("\n")}
1370
+ ${legend}
1371
+ </svg>`;
1372
+ }
1373
+ function timelineHtml(data, opts = {}) {
1374
+ const W = 250 + 950 + 32;
1375
+ const title = esc(opts.title ?? "Timeline");
1376
+ return `<!doctype html>
1377
+ <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>${title} \u2014 timeline</title>
1378
+ <style>
1379
+ :root { --bg: #f7f5f1; --ink: #1d2433; --dim: #6b7587; --grid: #d9d5cc; --key: #1d2433; --trans: #4cc9f0; }
1380
+ @media (prefers-color-scheme: dark) { :root { --bg: #0e1320; --ink: #e8eefc; --dim: #8fa0bf; --grid: #26304a; --key: #ffffff; --trans: #4cc9f0; } }
1381
+ body { margin: 0; background: var(--bg); color: var(--ink); font: 14px system-ui, sans-serif; }
1382
+ main { padding: 20px 16px 32px; max-width: ${W + 32}px; margin: 0 auto; overflow-x: auto; }
1383
+ h1 { font-size: 18px; font-weight: 600; margin: 0 0 4px; }
1384
+ p { margin: 0 0 14px; color: var(--dim); font-size: 13px; }
1385
+ svg { display: block; }
1386
+ .grid { stroke: var(--grid); stroke-width: 1; }
1387
+ .tick { fill: var(--dim); font-size: 11px; }
1388
+ .label { fill: var(--ink); font-size: 12px; font-family: ui-monospace, Menlo, monospace; }
1389
+ .band { fill: var(--ink); }
1390
+ .key { fill: var(--key); opacity: .85; }
1391
+ .transition { fill: var(--trans); opacity: .16; }
1392
+ .bar { opacity: .88; }
1393
+ .row:hover .bar { opacity: 1; }
1394
+ </style></head>
1395
+ <body><main>
1396
+ <h1>${title}</h1>
1397
+ <p>${data.rows.length} row${data.rows.length === 1 ? "" : "s"} \xB7 ${num(data.duration)} s \xB7 \u25C6 keyframe start \xB7 shaded band: a transition \xB7 hover a bar for details</p>
1398
+ ${timelineSvg(data)}
1399
+ </main></body></html>
1400
+ `;
1401
+ }
1402
+
1227
1403
  // src/core/frames.ts
1228
1404
  function pickFrames(lastFrame, count) {
1229
1405
  const last = Math.max(0, Math.floor(lastFrame));
@@ -1286,6 +1462,8 @@ var Movie = class {
1286
1462
  audioSource = null;
1287
1463
  gainNode = null;
1288
1464
  _cancelAudioStart = null;
1465
+ /** > 0 while the movie moves its own playhead (playback, render, snapshot, contactSheet): those are not the seeks `seeking` / `seeked` report. */
1466
+ _quiet = 0;
1289
1467
  _volume = 1;
1290
1468
  _muted = false;
1291
1469
  isPlaying = false;
@@ -1379,6 +1557,7 @@ var Movie = class {
1379
1557
  height: options.composition.height ?? this.height,
1380
1558
  duration: options.composition.duration ?? this.duration
1381
1559
  } : void 0;
1560
+ await chunkAGMNSJJH_cjs.ensureFilterLibrary(seededComposition);
1382
1561
  const userComposition = seededComposition ? expandTransitions(seededComposition) : void 0;
1383
1562
  const rootSeqSpec = {
1384
1563
  type: "composition",
@@ -1388,7 +1567,7 @@ var Movie = class {
1388
1567
  ...userComposition
1389
1568
  };
1390
1569
  if (userComposition) carryTransitionWindows(userComposition, rootSeqSpec);
1391
- const composition = new chunk4AGJF5RK_cjs.CompositionSequence(rootSeqSpec, null, rootShape);
1570
+ const composition = new chunkPDBDDGG5_cjs.CompositionSequence(rootSeqSpec, null, rootShape);
1392
1571
  await composition.build();
1393
1572
  this._rootSequence = composition;
1394
1573
  if (composition.target) root.addChild(composition.target);
@@ -1412,12 +1591,32 @@ var Movie = class {
1412
1591
  } catch (cleanupErr) {
1413
1592
  console.warn("pixi-effects: cleanup after init failure also threw:", cleanupErr);
1414
1593
  }
1594
+ this._emitError("init", err);
1415
1595
  throw err;
1416
1596
  }
1417
1597
  }
1418
1598
  async gotoFrame(frame, force = false) {
1419
1599
  if (this._initState !== "ready") return;
1420
1600
  if (!force && this.currentFrame === frame) return;
1601
+ if (this._quiet > 0) return this._goto(frame);
1602
+ const target = Math.max(0, Math.min(frame, this.totalFrames));
1603
+ this.emit("seeking", { frame: target, totalFrames: this.totalFrames });
1604
+ await this._goto(frame);
1605
+ this.emit("seeked", { frame: this.currentFrame, totalFrames: this.totalFrames });
1606
+ }
1607
+ /** Run `fn` with `seeking` / `seeked` silenced: the movie is moving its own playhead, not answering a seek. */
1608
+ async _quietly(fn) {
1609
+ this._quiet++;
1610
+ try {
1611
+ return await fn();
1612
+ } finally {
1613
+ this._quiet--;
1614
+ }
1615
+ }
1616
+ _emitError(where, error) {
1617
+ this.emit("error", { where, message: error instanceof Error ? error.message : String(error), error });
1618
+ }
1619
+ async _goto(frame) {
1421
1620
  this.currentFrame = Math.max(0, Math.min(frame, this.totalFrames));
1422
1621
  this.timeline.time(this.currentFrame / this.frameRate);
1423
1622
  await this._awaitVideoFrames();
@@ -1439,7 +1638,7 @@ var Movie = class {
1439
1638
  async snapshot(frame = this.currentFrame, opts = {}) {
1440
1639
  chunkUQBGW5DO_cjs.warnUnknownOptions("movie.snapshot()", opts, ["scale", "type", "as"]);
1441
1640
  this._requireReady("snapshot");
1442
- await this.gotoFrame(clampFrame(frame, this.totalFrames), true);
1641
+ await this._quietly(() => this.gotoFrame(clampFrame(frame, this.totalFrames), true));
1443
1642
  const scale = opts.scale ?? 1;
1444
1643
  const out = document.createElement("canvas");
1445
1644
  out.width = Math.max(1, Math.round(this.width * scale));
@@ -1463,7 +1662,7 @@ var Movie = class {
1463
1662
  try {
1464
1663
  for (let i = 0; i < frames.length; i++) {
1465
1664
  const f = frames[i];
1466
- await this.gotoFrame(f, true);
1665
+ await this._quietly(() => this.gotoFrame(f, true));
1467
1666
  const { x, y } = L.positions[i];
1468
1667
  g.drawImage(this.app.canvas, x, y + LABEL_H, L.cellW, L.cellH);
1469
1668
  g.fillStyle = "#e8ecf8";
@@ -1472,7 +1671,7 @@ var Movie = class {
1472
1671
  g.fillText(`frame ${f} \xB7 ${(f / this.frameRate).toFixed(2)}s`, x + 8, y + LABEL_H / 2);
1473
1672
  }
1474
1673
  } finally {
1475
- await this.gotoFrame(back, true);
1674
+ await this._quietly(() => this.gotoFrame(back, true));
1476
1675
  }
1477
1676
  return encodeCanvas(sheet, "image/png", opts.as ?? "blob");
1478
1677
  }
@@ -1487,6 +1686,24 @@ var Movie = class {
1487
1686
  await this.gotoFrame(f, true);
1488
1687
  return inspectScene(this._rootSequence, f, f / this.frameRate, { width: this.width, height: this.height }, opts);
1489
1688
  }
1689
+ /**
1690
+ * Every layer as a row with absolute start / end / keyframe times (seconds), plus the transition windows. Runs of
1691
+ * similar layers (`pop-1` … `pop-12`) are one row with a `parts` span each. Read it, or draw it with `timelineChart()`.
1692
+ */
1693
+ timelineData() {
1694
+ this._requireReady("timelineData");
1695
+ return collectTimeline(this._rootSequence, this.duration);
1696
+ }
1697
+ /** The timeline as one self-contained HTML page (an inline SVG, no scripts): layers as bars on a time axis, ◆ keyframes, transition bands. Open it in a browser. */
1698
+ timelineChart(opts = {}) {
1699
+ chunkUQBGW5DO_cjs.warnUnknownOptions("movie.timelineChart()", opts, ["title"]);
1700
+ return timelineHtml(this.timelineData(), opts);
1701
+ }
1702
+ /** Just the chart: one `<svg>` whose geometry is in `data-*` attributes (a viewer puts a playhead on it: `pixi-effects-view`). */
1703
+ timelineSvg(opts = {}) {
1704
+ chunkUQBGW5DO_cjs.warnUnknownOptions("movie.timelineSvg()", opts, ["chartWidth", "labels"]);
1705
+ return timelineSvg(this.timelineData(), opts);
1706
+ }
1490
1707
  /**
1491
1708
  * The soundtrack, measured: when every audio layer plays, how loud the mix is over time, and `issues`
1492
1709
  * (limiting, inaudible layers, sounds cut off by the end). Use it to CHECK sound you cannot hear.
@@ -1537,17 +1754,20 @@ var Movie = class {
1537
1754
  const frame = Math.floor(elapsed * this.frameRate);
1538
1755
  if (frame <= this.totalFrames) {
1539
1756
  inFlight = true;
1540
- this.gotoFrame(frame).catch((err) => {
1757
+ this._goto(frame).catch((err) => {
1541
1758
  console.warn("pixi-effects: gotoFrame failed during playback:", err);
1759
+ this._emitError("playback", err);
1542
1760
  }).finally(() => {
1543
1761
  inFlight = false;
1544
1762
  });
1545
1763
  this._raf = requestAnimationFrame(tick);
1546
1764
  } else {
1547
1765
  this.pause();
1548
- this.gotoFrame(this.totalFrames).catch((err) => {
1766
+ this._goto(this.totalFrames).catch((err) => {
1549
1767
  console.warn("pixi-effects: final gotoFrame failed:", err);
1768
+ this._emitError("playback", err);
1550
1769
  });
1770
+ this.emit("ended");
1551
1771
  }
1552
1772
  };
1553
1773
  this._raf = requestAnimationFrame(tick);
@@ -1591,15 +1811,20 @@ var Movie = class {
1591
1811
  if (wasPlaying) this.emit("pause");
1592
1812
  }
1593
1813
  set volume(v) {
1594
- this._volume = Math.max(0, Math.min(1, v));
1814
+ const next = Math.max(0, Math.min(1, v));
1815
+ const changed = next !== this._volume;
1816
+ this._volume = next;
1595
1817
  if (this.gainNode) this.gainNode.gain.value = this._muted ? 0 : this._volume;
1818
+ if (changed) this.emit("volumechange", { volume: this._volume, muted: this._muted });
1596
1819
  }
1597
1820
  get volume() {
1598
1821
  return this._volume;
1599
1822
  }
1600
1823
  set muted(v) {
1824
+ const changed = !!v !== this._muted;
1601
1825
  this._muted = !!v;
1602
1826
  if (this.gainNode) this.gainNode.gain.value = this._muted ? 0 : this._volume;
1827
+ if (changed) this.emit("volumechange", { volume: this._volume, muted: this._muted });
1603
1828
  }
1604
1829
  get muted() {
1605
1830
  return this._muted;
@@ -1610,7 +1835,12 @@ var Movie = class {
1610
1835
  }
1611
1836
  async render(options) {
1612
1837
  chunkUQBGW5DO_cjs.warnUnknownOptions("movie.render()", options, ["format", "video", "audio"]);
1613
- return await exportFrames(this, options);
1838
+ try {
1839
+ return await this._quietly(() => exportFrames(this, options));
1840
+ } catch (err) {
1841
+ this._emitError("render", err);
1842
+ throw err;
1843
+ }
1614
1844
  }
1615
1845
  async destroy() {
1616
1846
  safeRun(() => this.pause());
@@ -1833,17 +2063,17 @@ function orbit(opts) {
1833
2063
  throw new Error("pixi-effects: orbit() cannot combine `radius` with `dollyZoom` \u2014 the radius follows the animated fov");
1834
2064
  }
1835
2065
  const [cx, cy] = opts.center ?? [width / 2, height / 2];
1836
- const fixedR = opts.radius ?? chunkKQM2ZZMK_cjs.homeDistance(height, fov ?? chunkKQM2ZZMK_cjs.DEFAULT_FOV);
2066
+ const fixedR = opts.radius ?? chunkAGMNSJJH_cjs.homeDistance(height, fov ?? chunkAGMNSJJH_cjs.DEFAULT_FOV);
1837
2067
  const start = opts.start ?? -degrees / 2;
1838
2068
  const steps = Math.max(1, Math.round(duration * stepsPerSecond));
1839
2069
  const easeFn = parseEase(ease);
1840
2070
  const dollyEase = dolly ? parseEase(dolly.ease ?? ease) : null;
1841
2071
  const pos = (i) => {
1842
2072
  const p = i / steps;
1843
- const a = (start + degrees * easeFn(p)) * chunkKQM2ZZMK_cjs.DEG;
2073
+ const a = (start + degrees * easeFn(p)) * chunkAGMNSJJH_cjs.DEG;
1844
2074
  if (!dolly) return { x: cx + fixedR * Math.sin(a), z: fixedR * Math.cos(a) };
1845
2075
  const f = dolly.from + (dolly.to - dolly.from) * dollyEase(p);
1846
- const R = chunkKQM2ZZMK_cjs.homeDistance(height, f);
2076
+ const R = chunkAGMNSJJH_cjs.homeDistance(height, f);
1847
2077
  return { x: cx + R * Math.sin(a), z: R * Math.cos(a), fov: f };
1848
2078
  };
1849
2079
  const keyframes = [];
@@ -1892,7 +2122,7 @@ function withFade(spec, opts) {
1892
2122
 
1893
2123
  Object.defineProperty(exports, "registerSequenceType", {
1894
2124
  enumerable: true,
1895
- get: function () { return chunkKQM2ZZMK_cjs.registerSequenceType; }
2125
+ get: function () { return chunkAGMNSJJH_cjs.registerSequenceType; }
1896
2126
  });
1897
2127
  exports.Movie = Movie;
1898
2128
  exports.kenBurns = kenBurns;