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.d.cts CHANGED
@@ -1,7 +1,7 @@
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, M as Movie, d as MovieOptions, P as ProgressEvent, R as RenderOptions, S as SnapshotOptions, e as SoundMeasure } from './Movie-CADDBfpl.cjs';
2
- import { S as Sequence } from './Base-4vNLl4tH.cjs';
3
- import { S as SequenceSpec, C as CompositionShape, I as ImageSequenceSpec, a as CameraSequenceSpec, b as SequenceCommon } from './types-DrW9QeKU.cjs';
4
- export { A as ArcShapeSpec, c as AssetSpec, d as AudioAssetSpec, e as AudioSequenceSpec, f as AudioSfxSpec, g as ChromaKeyFilterSpec, h as CircleShapeSpec, i as CompositionSequenceSpec, j as CompositionSpec, k as CrossfadeTransition, l as CustomFilterSpec, D as DipTransition, m as DissolveTransition, E as EllipseShapeSpec, n as Expr, F as FilterSpec, G as GradientSpec, o as IrisTransition, K as Keyframe, L as LineShapeSpec, P as PathShapeSpec, p as PolygonShapeSpec, q as PropValue, r as Props, R as RectShapeSpec, s as SfxKnobs, t as SfxOptions, u as SfxPreset, v as SfxVoice, w as ShapeSequenceSpec, x as SlideTransition, T as TextSequenceSpec, y as TransitionCommon, z as TransitionSpec, V as VideoSequenceSpec, W as WipeTransition, Z as ZoomTransition } from './types-DrW9QeKU.cjs';
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, M as Movie, d as MovieErrorEvent, e as MovieOptions, P as ProgressEvent, R as RenderOptions, S as SnapshotOptions, f as SoundMeasure, T as TimelineData, g as TimelineHtmlOptions, h as TimelineRow, i as TimelineSvgOptions, j as TimelineTransition, V as VolumeEvent } from './Movie-CQOhUmko.cjs';
2
+ import { S as Sequence } from './Base-CSIulyIq.cjs';
3
+ import { S as SequenceSpec, C as CompositionShape, I as ImageSequenceSpec, a as CameraSequenceSpec, b as SequenceCommon } from './types-oJijbn-K.cjs';
4
+ export { A as ArcShapeSpec, c as AssetSpec, d as AudioAssetSpec, e as AudioSequenceSpec, f as AudioSfxSpec, g as ChromaKeyFilterSpec, h as CircleShapeSpec, i as CompositionSequenceSpec, j as CompositionSpec, k as CrossfadeTransition, l as CustomFilterSpec, D as DipTransition, m as DissolveTransition, E as EllipseShapeSpec, n as Expr, F as FilterSpec, G as GradientSpec, o as IrisTransition, K as Keyframe, L as LineShapeSpec, P as PathShapeSpec, p as PolygonShapeSpec, q as PropValue, r as Props, R as RectShapeSpec, s as SfxKnobs, t as SfxOptions, u as SfxPreset, v as SfxVoice, w as ShapeSequenceSpec, x as SlideTransition, T as TextSequenceSpec, y as TransitionCommon, z as TransitionSpec, V as VideoSequenceSpec, W as WipeTransition, Z as ZoomTransition } from './types-oJijbn-K.cjs';
5
5
  import 'pixi.js';
6
6
  import 'gsap';
7
7
 
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
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, M as Movie, d as MovieOptions, P as ProgressEvent, R as RenderOptions, S as SnapshotOptions, e as SoundMeasure } from './Movie-Ch2SUI1h.js';
2
- import { S as Sequence } from './Base-CD4AoT5-.js';
3
- import { S as SequenceSpec, C as CompositionShape, I as ImageSequenceSpec, a as CameraSequenceSpec, b as SequenceCommon } from './types-DrW9QeKU.js';
4
- export { A as ArcShapeSpec, c as AssetSpec, d as AudioAssetSpec, e as AudioSequenceSpec, f as AudioSfxSpec, g as ChromaKeyFilterSpec, h as CircleShapeSpec, i as CompositionSequenceSpec, j as CompositionSpec, k as CrossfadeTransition, l as CustomFilterSpec, D as DipTransition, m as DissolveTransition, E as EllipseShapeSpec, n as Expr, F as FilterSpec, G as GradientSpec, o as IrisTransition, K as Keyframe, L as LineShapeSpec, P as PathShapeSpec, p as PolygonShapeSpec, q as PropValue, r as Props, R as RectShapeSpec, s as SfxKnobs, t as SfxOptions, u as SfxPreset, v as SfxVoice, w as ShapeSequenceSpec, x as SlideTransition, T as TextSequenceSpec, y as TransitionCommon, z as TransitionSpec, V as VideoSequenceSpec, W as WipeTransition, Z as ZoomTransition } from './types-DrW9QeKU.js';
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, M as Movie, d as MovieErrorEvent, e as MovieOptions, P as ProgressEvent, R as RenderOptions, S as SnapshotOptions, f as SoundMeasure, T as TimelineData, g as TimelineHtmlOptions, h as TimelineRow, i as TimelineSvgOptions, j as TimelineTransition, V as VolumeEvent } from './Movie-HF3pSGjQ.js';
2
+ import { S as Sequence } from './Base-DVgwyIUj.js';
3
+ import { S as SequenceSpec, C as CompositionShape, I as ImageSequenceSpec, a as CameraSequenceSpec, b as SequenceCommon } from './types-oJijbn-K.js';
4
+ export { A as ArcShapeSpec, c as AssetSpec, d as AudioAssetSpec, e as AudioSequenceSpec, f as AudioSfxSpec, g as ChromaKeyFilterSpec, h as CircleShapeSpec, i as CompositionSequenceSpec, j as CompositionSpec, k as CrossfadeTransition, l as CustomFilterSpec, D as DipTransition, m as DissolveTransition, E as EllipseShapeSpec, n as Expr, F as FilterSpec, G as GradientSpec, o as IrisTransition, K as Keyframe, L as LineShapeSpec, P as PathShapeSpec, p as PolygonShapeSpec, q as PropValue, r as Props, R as RectShapeSpec, s as SfxKnobs, t as SfxOptions, u as SfxPreset, v as SfxVoice, w as ShapeSequenceSpec, x as SlideTransition, T as TextSequenceSpec, y as TransitionCommon, z as TransitionSpec, V as VideoSequenceSpec, W as WipeTransition, Z as ZoomTransition } from './types-oJijbn-K.js';
5
5
  import 'pixi.js';
6
6
  import 'gsap';
7
7
 
package/dist/index.js CHANGED
@@ -1,12 +1,12 @@
1
+ import { CompositionSequence } from './chunk-O4AJYUPK.js';
2
+ import { ensureFilterLibrary, homeDistance, DEFAULT_FOV, DEG, resolveAt, collectPropKeys } from './chunk-FNKSXBKB.js';
3
+ export { registerSequenceType } from './chunk-FNKSXBKB.js';
1
4
  import { warnUnknownOptions, suggestName } from './chunk-DKGG7WD4.js';
2
- import { CompositionSequence } from './chunk-B2FTJLNB.js';
3
- import { homeDistance, DEFAULT_FOV, DEG, resolveAt, collectPropKeys } from './chunk-7IJHJXDI.js';
4
- export { registerSequenceType } from './chunk-7IJHJXDI.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';
7
7
  import { gsap } from 'gsap';
8
8
  import { PixiPlugin } from 'gsap/PixiPlugin';
9
- import { QUALITY_VERY_HIGH, QUALITY_HIGH, QUALITY_MEDIUM, QUALITY_LOW, QUALITY_VERY_LOW, Output, BufferTarget, CanvasSource, AudioBufferSource, Input, ALL_FORMATS, BlobSource, WavOutputFormat, Conversion, VideoSampleSink, MkvOutputFormat, WebMOutputFormat, MovOutputFormat, Mp4OutputFormat } from 'mediabunny';
9
+ import { QUALITY_VERY_HIGH, QUALITY_HIGH, QUALITY_MEDIUM, QUALITY_LOW, QUALITY_VERY_LOW, Output, BufferTarget, CanvasSource, AudioBufferSource, Input, ALL_FORMATS, BlobSource, WavOutputFormat, Conversion, VideoSampleSink, MkvOutputFormat, WebMOutputFormat, MovOutputFormat, Mp4OutputFormat, getFirstEncodableAudioCodec } from 'mediabunny';
10
10
 
11
11
  var registered = false;
12
12
  function ensureLoadersRegistered(audioContext) {
@@ -161,7 +161,7 @@ function fft(re, im) {
161
161
  }
162
162
  var FRAME = 2048;
163
163
  function spectralCentroid(samples, sampleRate, from = 0, to = samples.length) {
164
- let num = 0;
164
+ let num2 = 0;
165
165
  let den = 0;
166
166
  const re = new Float64Array(FRAME);
167
167
  const im = new Float64Array(FRAME);
@@ -176,11 +176,11 @@ function spectralCentroid(samples, sampleRate, from = 0, to = samples.length) {
176
176
  fft(re, im);
177
177
  for (let b = 1; b < FRAME / 2; b++) {
178
178
  const power = re[b] * re[b] + im[b] * im[b];
179
- num += power * (b * sampleRate / FRAME);
179
+ num2 += power * (b * sampleRate / FRAME);
180
180
  den += power;
181
181
  }
182
182
  }
183
- return den > 1e-9 ? Math.round(num / den / 10) * 10 : 0;
183
+ return den > 1e-9 ? Math.round(num2 / den / 10) * 10 : 0;
184
184
  }
185
185
 
186
186
  // src/core/inspectAudio.ts
@@ -288,6 +288,28 @@ var AUDIO_CODEC_BY_FORMAT = {
288
288
  webm: "opus",
289
289
  mkv: "opus"
290
290
  };
291
+ var AUDIO_PREFERENCE = {
292
+ mp4: ["aac", "opus", "mp3", "flac"],
293
+ mov: ["aac", "mp3"],
294
+ webm: ["opus", "vorbis"],
295
+ mkv: ["opus", "vorbis", "aac", "flac", "mp3"]
296
+ };
297
+ async function chooseAudioCodec(fmt, requested, container, audio, bitrate) {
298
+ const supported = container.getSupportedAudioCodecs?.();
299
+ const preferred = AUDIO_PREFERENCE[fmt].filter((c) => !supported || supported.includes(c));
300
+ const canEncode = (list) => getFirstEncodableAudioCodec(list, { numberOfChannels: audio.numberOfChannels ?? 2, sampleRate: audio.sampleRate, bitrate });
301
+ if (requested) {
302
+ if (await canEncode([requested])) return requested;
303
+ const alt = await canEncode(preferred.filter((c) => c !== requested));
304
+ 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)."));
305
+ }
306
+ const chosen = await canEncode(preferred);
307
+ 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.`);
308
+ if (chosen !== preferred[0]) {
309
+ 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.`);
310
+ }
311
+ return chosen;
312
+ }
291
313
  var qualityMap = {
292
314
  "very-low": QUALITY_VERY_LOW,
293
315
  "low": QUALITY_LOW,
@@ -320,8 +342,10 @@ async function exportFrames(movie, options = {}) {
320
342
  bitrate: qualityMap[options.audio?.bitrate ?? "high"] ?? QUALITY_HIGH
321
343
  }
322
344
  };
345
+ const container = makeOutputFormat(opts.format);
346
+ if (movie.audioBuffer) opts.audio.codec = await chooseAudioCodec(fmt, options.audio?.codec, container, movie.audioBuffer, opts.audio.bitrate);
323
347
  const output = new Output({
324
- format: makeOutputFormat(opts.format),
348
+ format: container,
325
349
  target: new BufferTarget()
326
350
  });
327
351
  const canvasSource = new CanvasSource(movie.app.canvas, {
@@ -1120,7 +1144,7 @@ function inspectScene(root, frame, time, canvas, opts = {}) {
1120
1144
  const seen = /* @__PURE__ */ new Map();
1121
1145
  const scene = /* @__PURE__ */ new Map();
1122
1146
  const scaledToNothing = /* @__PURE__ */ new Set();
1123
- const walk = (comp, prefix, insideThreeD, parentVisible, parentScene, parentMoving = false, parentZero = false) => {
1147
+ const walk2 = (comp, prefix, insideThreeD, parentVisible, parentScene, parentMoving = false, parentZero = false) => {
1124
1148
  const blending = /* @__PURE__ */ new Set();
1125
1149
  for (const w of transitionWindowsOf(comp.spec)) {
1126
1150
  const start = (comp.absoluteStart ?? 0) + w.start, end = (comp.absoluteStart ?? 0) + w.end;
@@ -1164,10 +1188,10 @@ function inspectScene(root, frame, time, canvas, opts = {}) {
1164
1188
  const group = parentScene ?? (name !== void 0 && blending.has(name) ? name : void 0);
1165
1189
  if (group) scene.set(info, group);
1166
1190
  if (zero || seq.showsNothingYet) scaledToNothing.add(info);
1167
- if (seq instanceof CompositionSequence) walk(seq, prefix + label + "/", insideThreeD || threeD, visible, group, moving, zero);
1191
+ if (seq instanceof CompositionSequence) walk2(seq, prefix + label + "/", insideThreeD || threeD, visible, group, moving, zero);
1168
1192
  });
1169
1193
  };
1170
- walk(root, "", false, true);
1194
+ walk2(root, "", false, true);
1171
1195
  const issues = [];
1172
1196
  const texts = layers.filter((l) => l.type === "text" && l.visible && l.alpha >= FAINT && l.bounds);
1173
1197
  for (const l of texts) {
@@ -1204,6 +1228,158 @@ function inspectScene(root, frame, time, canvas, opts = {}) {
1204
1228
  return { frame, time, canvas, summary: { layers: layers.length, visible: layers.filter((l) => l.visible).length }, issues, layers: listed };
1205
1229
  }
1206
1230
 
1231
+ // src/core/timelineChart.ts
1232
+ var GROUP_MIN = 4;
1233
+ var MAX_KEYS = 200;
1234
+ var round = (n) => Math.round(n * 1e6) / 1e6;
1235
+ var family = (name) => name.replace(/\d+/g, "#");
1236
+ function detailOf(seq) {
1237
+ const s = seq.spec;
1238
+ if (typeof s.text === "string") return s.text.length > 60 ? s.text.slice(0, 57) + "\u2026" : s.text;
1239
+ if (typeof s.asset === "string") return `asset \u201C${s.asset}\u201D`;
1240
+ if (typeof s.sfx === "string") return `sfx \u201C${s.sfx}\u201D`;
1241
+ if (s.sfx && typeof s.sfx === "object") return "sfx (custom)";
1242
+ return void 0;
1243
+ }
1244
+ function walk(comp, parentStart, parentEnd, depth, prefix, rows, transitions) {
1245
+ const entries = [];
1246
+ comp._children.forEach((seq, i) => {
1247
+ const name = seq.spec.name ?? `${seq.spec.type}#${i}`;
1248
+ const start = parentStart + (seq.at ?? 0);
1249
+ const end = Math.min(parentEnd, seq.duration !== void 0 ? start + seq.duration : parentEnd);
1250
+ const span = end - start;
1251
+ const keys = (seq.spec.keyframes ?? []).map((kf) => round(start + resolveAt(kf.at, span))).sort((a, b) => a - b).slice(0, MAX_KEYS);
1252
+ const row = { path: prefix + name, name, type: seq.spec.type, depth, start: round(start), end: round(end), keys, detail: detailOf(seq) };
1253
+ const inner = [];
1254
+ const isComp = seq instanceof CompositionSequence;
1255
+ if (isComp) walk(seq, start, end, depth + 1, prefix + name + "/", inner, transitions);
1256
+ entries.push({ row, inner, groupable: !isComp });
1257
+ });
1258
+ rows.push(...groupFamilies(entries));
1259
+ for (const w of transitionWindowsOf(comp.spec)) {
1260
+ transitions.push({ from: prefix + w.from, to: prefix + w.to, start: round(parentStart + w.start), end: round(parentStart + w.end) });
1261
+ }
1262
+ }
1263
+ function groupFamilies(entries) {
1264
+ const buckets = /* @__PURE__ */ new Map();
1265
+ for (const e of entries) {
1266
+ if (!e.groupable) continue;
1267
+ const key = e.row.type + "|" + family(e.row.name);
1268
+ (buckets.get(key) ?? buckets.set(key, []).get(key)).push(e.row);
1269
+ }
1270
+ const out = [];
1271
+ const done = /* @__PURE__ */ new Set();
1272
+ for (const e of entries) {
1273
+ const key = e.row.type + "|" + family(e.row.name);
1274
+ const members = e.groupable ? buckets.get(key) : null;
1275
+ if (members && members.length >= GROUP_MIN) {
1276
+ if (done.has(key)) continue;
1277
+ done.add(key);
1278
+ const label = `${family(e.row.name)} \xD7${members.length}`;
1279
+ out.push({
1280
+ path: e.row.path.slice(0, e.row.path.length - e.row.name.length) + label,
1281
+ name: label,
1282
+ type: e.row.type,
1283
+ depth: e.row.depth,
1284
+ start: Math.min(...members.map((x) => x.start)),
1285
+ end: Math.max(...members.map((x) => x.end)),
1286
+ keys: members.flatMap((x) => x.keys).sort((a, b) => a - b).slice(0, MAX_KEYS),
1287
+ parts: members.map((x) => ({ start: x.start, end: x.end }))
1288
+ });
1289
+ } else {
1290
+ out.push(e.row, ...e.inner);
1291
+ continue;
1292
+ }
1293
+ out.push(...e.inner);
1294
+ }
1295
+ return out;
1296
+ }
1297
+ function collectTimeline(root, duration) {
1298
+ const rows = [];
1299
+ const transitions = [];
1300
+ walk(root, 0, duration, 0, "", rows, transitions);
1301
+ return { duration, rows, transitions };
1302
+ }
1303
+ var COLORS = {
1304
+ text: "#4cc9f0",
1305
+ shape: "#ffd166",
1306
+ image: "#7bd88f",
1307
+ video: "#c77dff",
1308
+ audio: "#ff7aa8",
1309
+ composition: "#8ea4c8",
1310
+ three: "#ff9f68",
1311
+ camera: "#b0b0b0"
1312
+ };
1313
+ var esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1314
+ var num = (n) => String(Math.round(n * 100) / 100);
1315
+ function tickStep(duration, chartWidth) {
1316
+ 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;
1317
+ return 7200;
1318
+ }
1319
+ function timelineSvg(data, opts = {}) {
1320
+ const CHART = Math.max(100, opts.chartWidth ?? 950), withLabels = opts.labels !== false;
1321
+ const LABEL = withLabels ? 250 : 0, ROW = 22, TOP = 34, PAD = 16;
1322
+ const W = LABEL + CHART + PAD * 2, H = TOP + data.rows.length * ROW + PAD;
1323
+ const x = (t) => PAD + LABEL + Math.max(0, Math.min(data.duration, t)) / data.duration * CHART;
1324
+ const step = tickStep(data.duration, CHART);
1325
+ const out = [];
1326
+ for (let t = 0; t <= data.duration + 1e-9; t += step) {
1327
+ out.push(`<line class="grid" x1="${num(x(t))}" y1="${TOP - 6}" x2="${num(x(t))}" y2="${H - PAD}"/>`);
1328
+ 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>`);
1329
+ }
1330
+ 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>`);
1331
+ for (const t of data.transitions) {
1332
+ 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>`);
1333
+ }
1334
+ data.rows.forEach((r, i) => {
1335
+ const y = TOP + i * ROW;
1336
+ const color = COLORS[r.type] ?? "#9aa7b8";
1337
+ 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}` : ""}`;
1338
+ out.push(`<g class="row" data-start="${num(r.start)}"><title>${esc(tip)}</title>`);
1339
+ out.push(`<rect class="band" x="${PAD}" y="${y}" width="${LABEL + CHART}" height="${ROW}" ${i % 2 ? 'fill-opacity=".05"' : 'fill-opacity="0"'}/>`);
1340
+ if (withLabels) out.push(`<text class="label" x="${PAD + 6 + r.depth * 14}" y="${y + 15}">${esc(r.name)}</text>`);
1341
+ for (const p of r.parts ?? [{ start: r.start, end: r.end }]) {
1342
+ 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}"/>`);
1343
+ }
1344
+ 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"/>`);
1345
+ out.push("</g>");
1346
+ });
1347
+ 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("");
1348
+ 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}">
1349
+ ${out.join("\n")}
1350
+ ${legend}
1351
+ </svg>`;
1352
+ }
1353
+ function timelineHtml(data, opts = {}) {
1354
+ const W = 250 + 950 + 32;
1355
+ const title = esc(opts.title ?? "Timeline");
1356
+ return `<!doctype html>
1357
+ <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>${title} \u2014 timeline</title>
1358
+ <style>
1359
+ :root { --bg: #f7f5f1; --ink: #1d2433; --dim: #6b7587; --grid: #d9d5cc; --key: #1d2433; --trans: #4cc9f0; }
1360
+ @media (prefers-color-scheme: dark) { :root { --bg: #0e1320; --ink: #e8eefc; --dim: #8fa0bf; --grid: #26304a; --key: #ffffff; --trans: #4cc9f0; } }
1361
+ body { margin: 0; background: var(--bg); color: var(--ink); font: 14px system-ui, sans-serif; }
1362
+ main { padding: 20px 16px 32px; max-width: ${W + 32}px; margin: 0 auto; overflow-x: auto; }
1363
+ h1 { font-size: 18px; font-weight: 600; margin: 0 0 4px; }
1364
+ p { margin: 0 0 14px; color: var(--dim); font-size: 13px; }
1365
+ svg { display: block; }
1366
+ .grid { stroke: var(--grid); stroke-width: 1; }
1367
+ .tick { fill: var(--dim); font-size: 11px; }
1368
+ .label { fill: var(--ink); font-size: 12px; font-family: ui-monospace, Menlo, monospace; }
1369
+ .band { fill: var(--ink); }
1370
+ .key { fill: var(--key); opacity: .85; }
1371
+ .transition { fill: var(--trans); opacity: .16; }
1372
+ .bar { opacity: .88; }
1373
+ .row:hover .bar { opacity: 1; }
1374
+ </style></head>
1375
+ <body><main>
1376
+ <h1>${title}</h1>
1377
+ <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>
1378
+ ${timelineSvg(data)}
1379
+ </main></body></html>
1380
+ `;
1381
+ }
1382
+
1207
1383
  // src/core/frames.ts
1208
1384
  function pickFrames(lastFrame, count) {
1209
1385
  const last = Math.max(0, Math.floor(lastFrame));
@@ -1266,6 +1442,8 @@ var Movie = class {
1266
1442
  audioSource = null;
1267
1443
  gainNode = null;
1268
1444
  _cancelAudioStart = null;
1445
+ /** > 0 while the movie moves its own playhead (playback, render, snapshot, contactSheet): those are not the seeks `seeking` / `seeked` report. */
1446
+ _quiet = 0;
1269
1447
  _volume = 1;
1270
1448
  _muted = false;
1271
1449
  isPlaying = false;
@@ -1359,6 +1537,7 @@ var Movie = class {
1359
1537
  height: options.composition.height ?? this.height,
1360
1538
  duration: options.composition.duration ?? this.duration
1361
1539
  } : void 0;
1540
+ await ensureFilterLibrary(seededComposition);
1362
1541
  const userComposition = seededComposition ? expandTransitions(seededComposition) : void 0;
1363
1542
  const rootSeqSpec = {
1364
1543
  type: "composition",
@@ -1392,12 +1571,32 @@ var Movie = class {
1392
1571
  } catch (cleanupErr) {
1393
1572
  console.warn("pixi-effects: cleanup after init failure also threw:", cleanupErr);
1394
1573
  }
1574
+ this._emitError("init", err);
1395
1575
  throw err;
1396
1576
  }
1397
1577
  }
1398
1578
  async gotoFrame(frame, force = false) {
1399
1579
  if (this._initState !== "ready") return;
1400
1580
  if (!force && this.currentFrame === frame) return;
1581
+ if (this._quiet > 0) return this._goto(frame);
1582
+ const target = Math.max(0, Math.min(frame, this.totalFrames));
1583
+ this.emit("seeking", { frame: target, totalFrames: this.totalFrames });
1584
+ await this._goto(frame);
1585
+ this.emit("seeked", { frame: this.currentFrame, totalFrames: this.totalFrames });
1586
+ }
1587
+ /** Run `fn` with `seeking` / `seeked` silenced: the movie is moving its own playhead, not answering a seek. */
1588
+ async _quietly(fn) {
1589
+ this._quiet++;
1590
+ try {
1591
+ return await fn();
1592
+ } finally {
1593
+ this._quiet--;
1594
+ }
1595
+ }
1596
+ _emitError(where, error) {
1597
+ this.emit("error", { where, message: error instanceof Error ? error.message : String(error), error });
1598
+ }
1599
+ async _goto(frame) {
1401
1600
  this.currentFrame = Math.max(0, Math.min(frame, this.totalFrames));
1402
1601
  this.timeline.time(this.currentFrame / this.frameRate);
1403
1602
  await this._awaitVideoFrames();
@@ -1419,7 +1618,7 @@ var Movie = class {
1419
1618
  async snapshot(frame = this.currentFrame, opts = {}) {
1420
1619
  warnUnknownOptions("movie.snapshot()", opts, ["scale", "type", "as"]);
1421
1620
  this._requireReady("snapshot");
1422
- await this.gotoFrame(clampFrame(frame, this.totalFrames), true);
1621
+ await this._quietly(() => this.gotoFrame(clampFrame(frame, this.totalFrames), true));
1423
1622
  const scale = opts.scale ?? 1;
1424
1623
  const out = document.createElement("canvas");
1425
1624
  out.width = Math.max(1, Math.round(this.width * scale));
@@ -1443,7 +1642,7 @@ var Movie = class {
1443
1642
  try {
1444
1643
  for (let i = 0; i < frames.length; i++) {
1445
1644
  const f = frames[i];
1446
- await this.gotoFrame(f, true);
1645
+ await this._quietly(() => this.gotoFrame(f, true));
1447
1646
  const { x, y } = L.positions[i];
1448
1647
  g.drawImage(this.app.canvas, x, y + LABEL_H, L.cellW, L.cellH);
1449
1648
  g.fillStyle = "#e8ecf8";
@@ -1452,7 +1651,7 @@ var Movie = class {
1452
1651
  g.fillText(`frame ${f} \xB7 ${(f / this.frameRate).toFixed(2)}s`, x + 8, y + LABEL_H / 2);
1453
1652
  }
1454
1653
  } finally {
1455
- await this.gotoFrame(back, true);
1654
+ await this._quietly(() => this.gotoFrame(back, true));
1456
1655
  }
1457
1656
  return encodeCanvas(sheet, "image/png", opts.as ?? "blob");
1458
1657
  }
@@ -1467,6 +1666,24 @@ var Movie = class {
1467
1666
  await this.gotoFrame(f, true);
1468
1667
  return inspectScene(this._rootSequence, f, f / this.frameRate, { width: this.width, height: this.height }, opts);
1469
1668
  }
1669
+ /**
1670
+ * Every layer as a row with absolute start / end / keyframe times (seconds), plus the transition windows. Runs of
1671
+ * similar layers (`pop-1` … `pop-12`) are one row with a `parts` span each. Read it, or draw it with `timelineChart()`.
1672
+ */
1673
+ timelineData() {
1674
+ this._requireReady("timelineData");
1675
+ return collectTimeline(this._rootSequence, this.duration);
1676
+ }
1677
+ /** 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. */
1678
+ timelineChart(opts = {}) {
1679
+ warnUnknownOptions("movie.timelineChart()", opts, ["title"]);
1680
+ return timelineHtml(this.timelineData(), opts);
1681
+ }
1682
+ /** Just the chart: one `<svg>` whose geometry is in `data-*` attributes (a viewer puts a playhead on it: `pixi-effects-view`). */
1683
+ timelineSvg(opts = {}) {
1684
+ warnUnknownOptions("movie.timelineSvg()", opts, ["chartWidth", "labels"]);
1685
+ return timelineSvg(this.timelineData(), opts);
1686
+ }
1470
1687
  /**
1471
1688
  * The soundtrack, measured: when every audio layer plays, how loud the mix is over time, and `issues`
1472
1689
  * (limiting, inaudible layers, sounds cut off by the end). Use it to CHECK sound you cannot hear.
@@ -1517,17 +1734,20 @@ var Movie = class {
1517
1734
  const frame = Math.floor(elapsed * this.frameRate);
1518
1735
  if (frame <= this.totalFrames) {
1519
1736
  inFlight = true;
1520
- this.gotoFrame(frame).catch((err) => {
1737
+ this._goto(frame).catch((err) => {
1521
1738
  console.warn("pixi-effects: gotoFrame failed during playback:", err);
1739
+ this._emitError("playback", err);
1522
1740
  }).finally(() => {
1523
1741
  inFlight = false;
1524
1742
  });
1525
1743
  this._raf = requestAnimationFrame(tick);
1526
1744
  } else {
1527
1745
  this.pause();
1528
- this.gotoFrame(this.totalFrames).catch((err) => {
1746
+ this._goto(this.totalFrames).catch((err) => {
1529
1747
  console.warn("pixi-effects: final gotoFrame failed:", err);
1748
+ this._emitError("playback", err);
1530
1749
  });
1750
+ this.emit("ended");
1531
1751
  }
1532
1752
  };
1533
1753
  this._raf = requestAnimationFrame(tick);
@@ -1571,15 +1791,20 @@ var Movie = class {
1571
1791
  if (wasPlaying) this.emit("pause");
1572
1792
  }
1573
1793
  set volume(v) {
1574
- this._volume = Math.max(0, Math.min(1, v));
1794
+ const next = Math.max(0, Math.min(1, v));
1795
+ const changed = next !== this._volume;
1796
+ this._volume = next;
1575
1797
  if (this.gainNode) this.gainNode.gain.value = this._muted ? 0 : this._volume;
1798
+ if (changed) this.emit("volumechange", { volume: this._volume, muted: this._muted });
1576
1799
  }
1577
1800
  get volume() {
1578
1801
  return this._volume;
1579
1802
  }
1580
1803
  set muted(v) {
1804
+ const changed = !!v !== this._muted;
1581
1805
  this._muted = !!v;
1582
1806
  if (this.gainNode) this.gainNode.gain.value = this._muted ? 0 : this._volume;
1807
+ if (changed) this.emit("volumechange", { volume: this._volume, muted: this._muted });
1583
1808
  }
1584
1809
  get muted() {
1585
1810
  return this._muted;
@@ -1590,7 +1815,12 @@ var Movie = class {
1590
1815
  }
1591
1816
  async render(options) {
1592
1817
  warnUnknownOptions("movie.render()", options, ["format", "video", "audio"]);
1593
- return await exportFrames(this, options);
1818
+ try {
1819
+ return await this._quietly(() => exportFrames(this, options));
1820
+ } catch (err) {
1821
+ this._emitError("render", err);
1822
+ throw err;
1823
+ }
1594
1824
  }
1595
1825
  async destroy() {
1596
1826
  safeRun(() => this.pause());