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
@@ -13,7 +13,10 @@
13
13
  * Needs: Node >= 22 (built-in WebSocket), Chrome / Chromium installed (or --chrome PATH / CHROME=PATH). No npm dependencies.
14
14
  * The page must follow ai/template.html: it exposes `window.movie` and sets `window.__ready = true` (and `window.__logs`).
15
15
  *
16
- * Options: --strict (text overlaps and stops where the picture is still changing fail the check; by default they are only listed for review) · --out DIR · --frames N (contact sheet tiles, default 12) · --formats mp4,webm (default mp4) · --no-export ·
16
+ * Options: --draft (the export is a draft: half size, low quality, no motion blur: a much smaller file; the drawing itself is not faster) · --at LIST (pictures at moments you name: 3.5, 50%, f120, title@end → frames/*.png and at.png) ·
17
+ * --query "a=1&b=2" (added to the page address: for a page that reads it, such as one video of a batch) ·
18
+ * --onion A:B (one picture of the movement between A and B seconds, frames overlaid: onion.png) ·
19
+ * --strict (text overlaps and stops where the picture is still changing fail the check; by default they are only listed for review) · --out DIR · --frames N (contact sheet tiles, default 12) · --formats mp4,webm (default mp4) · --no-export ·
17
20
  * --timeout SECONDS (default 240) · --root DIR (static server root; default: the nearest folder above the page with dist/) · --chrome PATH
18
21
  */
19
22
  import { spawn } from 'node:child_process';
@@ -37,7 +40,7 @@ export function findChrome(env = process.env, exists = fs.existsSync, platform =
37
40
  }
38
41
 
39
42
  export function parseArgs(argv) {
40
- const o = { page: null, out: null, frames: 12, formats: ['mp4'], export: true, timeout: 240, root: null, chrome: null, strict: false };
43
+ const o = { page: null, out: null, frames: 12, formats: ['mp4'], export: true, timeout: 240, root: null, chrome: null, strict: false, at: null, draft: false, onion: null, query: null };
41
44
  const need = (i, name) => { if (i + 1 >= argv.length) throw new Error(`${name} needs a value`); return argv[i + 1]; };
42
45
  for (let i = 0; i < argv.length; i++) {
43
46
  const a = argv[i];
@@ -46,6 +49,14 @@ export function parseArgs(argv) {
46
49
  else if (a === '--formats') o.formats = need(i++, a).split(',').map(s => s.trim()).filter(Boolean);
47
50
  else if (a === '--no-export') o.export = false;
48
51
  else if (a === '--strict') o.strict = true;
52
+ else if (a === '--draft') o.draft = true;
53
+ else if (a === '--query') o.query = need(i++, a).replace(/^\?/, '');
54
+ else if (a === '--at') o.at = need(i++, a);
55
+ else if (a === '--onion') {
56
+ const m = need(i++, a).match(/^(\d+(?:\.\d+)?):(\d+(?:\.\d+)?)$/);
57
+ if (!m || !(Number(m[1]) < Number(m[2]))) throw new Error(`--onion must look like 1:3 (seconds, from before to), got "${argv[i]}"`);
58
+ o.onion = [Number(m[1]), Number(m[2])];
59
+ }
49
60
  else if (a === '--timeout') o.timeout = Math.max(10, Number(need(i++, a)) || 240);
50
61
  else if (a === '--root') o.root = need(i++, a);
51
62
  else if (a === '--chrome') o.chrome = need(i++, a);
@@ -56,32 +67,6 @@ export function parseArgs(argv) {
56
67
  return o;
57
68
  }
58
69
 
59
- /** [{ frame, issues[] }] → [{ message, count, firstFrame, lastFrame }]: issues that differ only by numbers are one. */
60
- export function groupIssues(perFrame) {
61
- const groups = new Map();
62
- for (const { frame, issues } of perFrame) {
63
- for (const message of issues) {
64
- const key = message.replace(/-?\d+(\.\d+)?/g, '#');
65
- const g = groups.get(key);
66
- if (g) { g.count++; g.lastFrame = frame; }
67
- else groups.set(key, { message, count: 1, firstFrame: frame, lastFrame: frame });
68
- }
69
- }
70
- return [...groups.values()];
71
- }
72
-
73
- /**
74
- * Layout issues → { problems, review }. Cut off by an edge, outside the canvas and "no size" are exact: problems. Text that
75
- * overlaps text is judged on layout boxes, not ink, so intentional designs trip it (ghost layers, a glow copy under a title,
76
- * per-letter boxes, a wipe between two scenes: 7 of 30 gallery pieces): those are `review` items to look at on the contact
77
- * sheet. `strict` makes every issue a problem.
78
- */
79
- export function splitIssues(groups, strict = false) {
80
- if (strict) return { problems: groups, review: [] };
81
- const overlap = g => /\boverlap\b/.test(g.message);
82
- return { problems: groups.filter(g => !overlap(g)), review: groups.filter(overlap) };
83
- }
84
-
85
70
  const MIME = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript', '.mjs': 'text/javascript', '.css': 'text/css', '.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.webp': 'image/webp', '.svg': 'image/svg+xml', '.mp3': 'audio/mpeg', '.wav': 'audio/wav', '.mp4': 'video/mp4', '.webm': 'video/webm', '.woff2': 'font/woff2', '.woff': 'font/woff', '.ttf': 'font/ttf', '.wasm': 'application/wasm', '.map': 'application/json', '.md': 'text/plain; charset=utf-8', '.txt': 'text/plain; charset=utf-8' };
86
71
 
87
72
  /** A plain static server for `root` (no redirects, so `?query` survives). Resolves { server, port }. */
@@ -94,7 +79,7 @@ export function serve(root, onRequest = null, port = 0) {
94
79
  if (!file.startsWith(root)) { res.writeHead(403).end(); return; }
95
80
  if (fs.existsSync(file) && fs.statSync(file).isDirectory()) file = path.join(file, 'index.html');
96
81
  if (!fs.existsSync(file)) { res.writeHead(404).end('not found'); return; }
97
- res.writeHead(200, { 'content-type': MIME[path.extname(file).toLowerCase()] ?? 'application/octet-stream', 'cache-control': 'no-store' });
82
+ res.writeHead(200, { 'content-type': MIME[path.extname(file).toLowerCase()] ?? 'application/octet-stream', 'cache-control': 'no-store', 'access-control-allow-origin': '*' }); // like GitHub Pages: a page with no origin (a sandboxed iframe) may load these files
98
83
  fs.createReadStream(file).pipe(res);
99
84
  } catch { res.writeHead(500).end(); }
100
85
  });
@@ -137,10 +122,11 @@ export class Cdp {
137
122
  }
138
123
  }
139
124
 
140
- export async function launchChrome(chrome, userDataDir) {
125
+ /** Start a private headless Chrome. `extraArgs`: more command-line flags (the tests use it for `--enable-features=WebMCP`). */
126
+ export async function launchChrome(chrome, userDataDir, extraArgs = []) {
141
127
  const proc = spawn(chrome, [
142
128
  '--headless=new', '--remote-debugging-port=0', `--user-data-dir=${userDataDir}`, '--no-first-run', '--no-default-browser-check',
143
- '--autoplay-policy=no-user-gesture-required', '--ignore-gpu-blocklist', '--enable-unsafe-swiftshader', '--window-size=1400,900', 'about:blank',
129
+ '--autoplay-policy=no-user-gesture-required', '--mute-audio', '--ignore-gpu-blocklist', '--enable-unsafe-swiftshader', '--window-size=1400,900', ...extraArgs, 'about:blank',
144
130
  ], { stdio: ['ignore', 'ignore', 'pipe'] });
145
131
  const endpoint = await new Promise((resolve, reject) => {
146
132
  let buf = '';
@@ -161,24 +147,13 @@ export async function launchChrome(chrome, userDataDir) {
161
147
  const INFO = `(() => { const m = window.movie; if (!m) return { hasMovie: false, logs: window.__logs || [] };
162
148
  const c = document.querySelector('canvas');
163
149
  return { hasMovie: true, logs: window.__logs || [], totalFrames: m.totalFrames, frameRate: m.frameRate, duration: m.duration,
164
- width: m.width ?? (c && c.width), height: m.height ?? (c && c.height), hasAudio: !!m.audioBuffer }; })()`;
165
-
166
- const sweepScript = stride => `(async () => {
167
- const total = movie.totalFrames, frames = new Set([0, total]);
168
- for (let f = 0; f <= total; f += ${stride}) frames.add(Math.min(f, total));
169
- const perFrame = [];
170
- for (const f of [...frames].sort((a, b) => a - b)) {
171
- const r = await movie.inspect(f, { layers: 'none' });
172
- if (r.issues.length) perFrame.push({ frame: f, issues: r.issues });
173
- }
174
- return { checked: frames.size, perFrame };
175
- })()`;
150
+ width: m.width ?? (c && c.width), height: m.height ?? (c && c.height), hasAudio: !!m.audioBuffer, hasReview: typeof m.review === 'function' }; })()`;
176
151
 
177
- const exportScript = format => `(() => {
152
+ const exportScript = (format, draft = false) => `(() => {
178
153
  window.__x = { state: 'running' };
179
154
  (async () => {
180
155
  try {
181
- const blob = await movie.render({ format: ${JSON.stringify(format)} });
156
+ const blob = await movie.render({ format: ${JSON.stringify(format)}${draft ? ', draft: true' : ''} });
182
157
  const info = { bytes: blob.size, type: blob.type };
183
158
  const url = URL.createObjectURL(blob);
184
159
  const v = document.createElement('video'); v.muted = true; v.preload = 'metadata'; v.src = url;
@@ -206,6 +181,43 @@ const exportScript = format => `(() => {
206
181
  return 0;
207
182
  })()`;
208
183
 
184
+ /** The mix as a picture (in the page, no dependency): loudness per window as bars, the peak as a line, scene edges and the start of every sound. */
185
+ const waveformScript = (audio, duration) => `(() => {
186
+ const a = ${JSON.stringify({ windows: audio.windows, scenes: audio.scenes, cues: audio.cues })}, D = ${duration};
187
+ const W = 1200, H = 300, L = 44, R = 12, T = 22, B = 58;
188
+ const c = document.createElement('canvas'); c.width = W; c.height = H;
189
+ const g = c.getContext('2d');
190
+ g.fillStyle = '#10131c'; g.fillRect(0, 0, W, H);
191
+ const x = t => L + (t / D) * (W - L - R);
192
+ const y = db => T + (1 - (Math.max(-60, Math.min(0, db)) + 60) / 60) * (H - T - B);
193
+ g.font = '11px system-ui, sans-serif'; g.textBaseline = 'middle';
194
+ for (const db of [0, -12, -24, -36, -48, -60]) {
195
+ g.strokeStyle = 'rgba(255,255,255,.12)'; g.beginPath(); g.moveTo(L, y(db)); g.lineTo(W - R, y(db)); g.stroke();
196
+ g.fillStyle = '#9aa5c4'; g.fillText(String(db), 8, y(db));
197
+ }
198
+ const bw = Math.max(1, (W - L - R) / Math.max(1, a.windows.length));
199
+ g.fillStyle = '#7fb4ff';
200
+ for (const w of a.windows) { const top = y(w.rmsDb); g.fillRect(x(w.t), top, Math.max(1, bw - 0.5), (H - B) - top); }
201
+ g.strokeStyle = '#f2c14e'; g.lineWidth = 1; g.beginPath();
202
+ a.windows.forEach((w, i) => { const px = x(w.t) + bw / 2, py = y(w.peakDb); if (i) g.lineTo(px, py); else g.moveTo(px, py); });
203
+ g.stroke();
204
+ if (a.scenes.length > 1 && a.scenes.length <= 16) {
205
+ let lastLabel = -1e9;
206
+ a.scenes.forEach(s => {
207
+ g.strokeStyle = 'rgba(255,255,255,.35)'; g.beginPath(); g.moveTo(x(s.from), T - 8); g.lineTo(x(s.from), H - B); g.stroke();
208
+ if (x(s.from) - lastLabel < 70) return; // scenes that start together share one label
209
+ lastLabel = x(s.from);
210
+ g.fillStyle = '#e8ecf8'; g.fillText(s.name.slice(0, 18), x(s.from) + 3, T - 10, Math.max(20, x(s.to) - x(s.from) - 6));
211
+ });
212
+ }
213
+ a.cues.forEach((q, i) => { g.fillStyle = '#ff5a3c'; g.fillRect(x(q.t) - 1, H - B + 2, 2, 8); if (a.cues.length <= 40) g.fillText(String(i + 1), x(q.t) - 3, H - B + 20); });
214
+ g.fillStyle = '#9aa5c4';
215
+ const step = D > 120 ? 30 : D > 40 ? 10 : D > 12 ? 5 : 1;
216
+ for (let t = 0; t <= D + 1e-9; t += step) { g.fillText(t + ' s', x(t) - 8, H - 22); }
217
+ g.fillText('mix level (dBFS): bars = RMS, line = peak; red ticks = when a sound starts', L, H - 7);
218
+ return c.toDataURL('image/png');
219
+ })()`;
220
+
209
221
  // ───────────────────────────── the check ─────────────────────────────
210
222
 
211
223
  export const sleep = ms => new Promise(r => setTimeout(r, ms));
@@ -240,7 +252,7 @@ export async function runCheck(opts, log = console.log) {
240
252
  await cdp.send('Runtime.enable');
241
253
  await cdp.send('Page.enable');
242
254
  const t0 = Date.now();
243
- await cdp.send('Page.navigate', { url: `http://127.0.0.1:${port}/${path.relative(root, pagePath).split(path.sep).join('/')}` });
255
+ await cdp.send('Page.navigate', { url: `http://127.0.0.1:${port}/${path.relative(root, pagePath).split(path.sep).join('/')}${opts.query ? `?${opts.query}` : ''}` });
244
256
 
245
257
  // wait for the movie to be ready (or the page to say it failed)
246
258
  let info = null;
@@ -266,12 +278,25 @@ export async function runCheck(opts, log = console.log) {
266
278
  Object.assign(report, { width: info.width, height: info.height, frameRate: info.frameRate, totalFrames: info.totalFrames, duration: info.duration, hasAudio: info.hasAudio });
267
279
  if (report.logs.length) report.problems.push(`${report.logs.length} console warning(s) / error(s) — each says what to change`);
268
280
 
269
- // layout: inspect over the whole timeline
270
- const stride = Math.max(1, Math.round(info.totalFrames / 60));
271
- const sweep = await cdp.eval(sweepScript(stride));
272
- const grouped = splitIssues(groupIssues(sweep.perFrame), opts.strict);
273
- report.inspect = { checkedFrames: sweep.checked, issues: grouped.problems, review: grouped.review };
274
- if (grouped.problems.length) report.problems.push(`${grouped.problems.length} kind(s) of layout issue from movie.inspect`);
281
+ // the library reviews the movie (layout over the whole timeline, fonts, sound): the same call the Playground and the WebMCP tools make
282
+ if (!info.hasReview) throw new Error("this page's pixi-effects is older than the check tool: use 0.18 or newer (the version in the page's import map)");
283
+
284
+ // a mistake in --at or --onion is a usage error: say so now, not after the slow work
285
+ let atList = null;
286
+ if (opts.at) {
287
+ // the page's error is returned as a value: thrown, it would arrive with the page's stack trace
288
+ const r = JSON.parse(await cdp.eval(`(() => { try { return JSON.stringify({ at: movie.resolveAt(${JSON.stringify(opts.at)}) }); } catch (e) { return JSON.stringify({ error: String(e && e.message || e) }); } })()`));
289
+ if (r.error) throw new Error(r.error.replace(/^pixi-effects: /, ''));
290
+ atList = r.at;
291
+ }
292
+ if (opts.onion && opts.onion[1] > info.duration + 1e-9) throw new Error(`--onion ${opts.onion[0]}:${opts.onion[1]} goes past the end: the movie is ${info.duration} s`);
293
+
294
+ const rv = await cdp.eval(`movie.review(${JSON.stringify({ strict: !!opts.strict })})`);
295
+ report.inspect = { checkedFrames: rv.frames, issues: rv.problems, review: rv.review };
296
+ if (rv.problems.length) report.problems.push(`${rv.problems.length} kind(s) of layout issue from movie.inspect`);
297
+ // fonts: a web font whose file did not load is a problem (the library lists a layer with no available font for review)
298
+ report.fonts = rv.fonts;
299
+ if (rv.fonts.failed.length) report.problems.push(`${rv.fonts.failed.length} web font(s) failed to load: ${rv.fonts.failed.join(', ')} (the layers that use them are drawn in a fallback)`);
275
300
  left();
276
301
 
277
302
  // contact sheet
@@ -280,6 +305,32 @@ export async function runCheck(opts, log = console.log) {
280
305
  fs.writeFileSync(sheetFile, Buffer.from(sheet.slice(sheet.indexOf(',') + 1), 'base64'));
281
306
  report.files.sheet = shown(sheetFile);
282
307
 
308
+ // --at: the pictures at moments you name (seconds, 50%, f120, title@end), one file each and one sheet
309
+ if (opts.at) {
310
+ const wanted = atList;
311
+ const framesDir = path.join(outDir, 'frames');
312
+ fs.mkdirSync(framesDir, { recursive: true });
313
+ for (const w of wanted) {
314
+ const png = await cdp.eval(`movie.snapshot(${w.frame}, { as: 'dataURL' })`);
315
+ fs.writeFileSync(path.join(framesDir, `${w.label}.png`), Buffer.from(png.slice(png.indexOf(',') + 1), 'base64'));
316
+ }
317
+ const atSheet = await cdp.eval(`movie.contactSheet({ frames: ${JSON.stringify(wanted.map(w => w.frame))}, as: 'dataURL' })`);
318
+ const atFile = path.join(outDir, 'at.png');
319
+ fs.writeFileSync(atFile, Buffer.from(atSheet.slice(atSheet.indexOf(',') + 1), 'base64'));
320
+ report.files.at = shown(atFile);
321
+ report.files.frames = shown(framesDir);
322
+ report.at = wanted;
323
+ }
324
+
325
+ // --onion A:B: one picture of the movement between A and B seconds (8 frames overlaid, the later the stronger)
326
+ if (opts.onion) {
327
+ const [from, to] = opts.onion;
328
+ const onion = await cdp.eval(`movie.onionSkin({ from: ${from}, to: ${to}, count: 8, as: 'dataURL' })`);
329
+ const onionFile = path.join(outDir, 'onion.png');
330
+ fs.writeFileSync(onionFile, Buffer.from(onion.slice(onion.indexOf(',') + 1), 'base64'));
331
+ report.files.onion = shown(onionFile);
332
+ }
333
+
283
334
  // a presentation (composition.stops): one picture of every stop, in order, so the pages and steps can be read at a glance
284
335
  const stops = await cdp.eval(`(() => { const s = movie.stops || []; return s.length ? { count: s.length, pages: movie.pageCount, items: s.map(x => ({ at: Math.round(x.at * 100) / 100, frame: x.frame, page: x.page, pageStart: x.pageStart, notes: !!x.notes, pdf: x.pdf })) } : null; })()`).catch(() => null);
285
336
  if (stops) {
@@ -316,7 +367,15 @@ export async function runCheck(opts, log = console.log) {
316
367
  }
317
368
 
318
369
  // audio
319
- report.audio = info.hasAudio ? await cdp.eval('movie.inspectAudio()') : null;
370
+ report.audio = info.hasAudio ? rv.audio : null;
371
+ if (report.audio) {
372
+ const wave = await cdp.eval(waveformScript(report.audio, info.duration)).catch(() => null);
373
+ if (wave) {
374
+ const waveFile = path.join(outDir, 'waveform.png');
375
+ fs.writeFileSync(waveFile, Buffer.from(wave.slice(wave.indexOf(',') + 1), 'base64'));
376
+ report.files.waveform = shown(waveFile);
377
+ }
378
+ }
320
379
  if (report.audio?.issues?.length) report.problems.push(`${report.audio.issues.length} audio issue(s) from movie.inspectAudio`);
321
380
 
322
381
  // export, decoded again
@@ -324,7 +383,7 @@ export async function runCheck(opts, log = console.log) {
324
383
  if (opts.export) {
325
384
  for (const format of opts.formats) {
326
385
  left();
327
- await cdp.eval(exportScript(format));
386
+ await cdp.eval(exportScript(format, opts.draft));
328
387
  let state;
329
388
  for (;;) {
330
389
  left();
@@ -332,7 +391,7 @@ export async function runCheck(opts, log = console.log) {
332
391
  state = await cdp.eval('window.__x && window.__x.state');
333
392
  if (state && state !== 'running') break;
334
393
  }
335
- const ex = { format };
394
+ const ex = { format, draft: !!opts.draft };
336
395
  if (state === 'error') {
337
396
  ex.error = await cdp.eval('window.__x.message');
338
397
  report.problems.push(`export ${format} failed: ${ex.error}`);
@@ -382,7 +441,7 @@ function finish(report, outDir, log) {
382
441
  if (g.length > 8) L.push(` … ${g.length - 8} more (report.json)`);
383
442
  const rv = report.inspect.review;
384
443
  if (rv.length) {
385
- L.push(` review ${rv.length} text overlap(s) — often intentional (ghost / glow copies, per-letter boxes, a wipe); check them on the contact sheet, or use --strict to fail on them:`);
444
+ L.push(` review ${rv.length} to look at — text overlaps are often intentional (ghost / glow copies, per-letter boxes, a wipe), and a font that is not available is drawn in a fallback; check them on the contact sheet, or use --strict to fail on them:`);
386
445
  for (const i of rv.slice(0, 4)) L.push(line(i));
387
446
  if (rv.length > 4) L.push(` … ${rv.length - 4} more (report.json)`);
388
447
  }
@@ -398,13 +457,14 @@ function finish(report, outDir, log) {
398
457
  }
399
458
  if (report.audio !== undefined) {
400
459
  const a = report.audio;
401
- L.push(a ? ` audio ${a.sources.length} source(s) · mix peak ${a.peakDb} dBFS · ${a.issues.length ? a.issues.join(' | ') : 'no issues (movie.inspectAudio)'}` : ' audio none');
460
+ L.push(a ? ` audio ${a.sources.length} source(s) · mix peak ${a.peakDb} dBFS · ${a.loudness?.integratedLufs ?? 'n/a'} LUFS · true peak ${a.loudness?.truePeakDb ?? 'n/a'} dBTP · ${a.issues.length ? a.issues.join(' | ') : 'no issues (movie.inspectAudio)'}` : ' audio none');
461
+ for (const n of a?.notes ?? []) L.push(` note ${n}`);
402
462
  }
403
463
  for (const ex of report.exports ?? []) {
404
464
  if (ex.error) { L.push(` export ${ex.format}: FAILED — ${ex.error}`); continue; }
405
465
  const v = ex.video?.error ? `video: ${ex.video.error}` : `video ${ex.video.duration.toFixed(2)} s ${ex.video.width}×${ex.video.height}`;
406
466
  const au = ex.audio ? (ex.audio.error ? `audio: ${ex.audio.error}` : `audio ${ex.audio.duration} s, peak ${ex.audio.peakDb} dBFS, rms/s [${ex.audio.rmsDbPerSecond.join(' ')}]`) : 'no audio track';
407
- L.push(` export ${ex.format} ok · ${kb(ex.bytes)} · ${v} · ${au}`);
467
+ L.push(` export ${ex.format}${ex.draft ? ' (draft)' : ''} ok · ${kb(ex.bytes)} · ${v} · ${au}`);
408
468
  }
409
469
  L.push(` files ${Object.values(report.files).join(' ')}`);
410
470
  L.push(report.problems.length ? `RESULT: ${report.problems.length} PROBLEM(S)\n - ${report.problems.join('\n - ')}` : 'RESULT: OK — nothing to fix. Look at the contact sheet before you call it done.');
@@ -419,7 +479,7 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
419
479
  if (typeof WebSocket === 'undefined') throw new Error('Node >= 22 is needed (built-in WebSocket)');
420
480
  const opts = parseArgs(process.argv.slice(2));
421
481
  if (!opts.page) {
422
- console.error('usage: node ai/tools/check.mjs <page.html> [--out DIR] [--frames N] [--formats mp4,webm] [--no-export] [--timeout S] [--root DIR] [--chrome PATH]');
482
+ console.error('usage: node ai/tools/check.mjs <page.html> [--out DIR] [--frames N] [--formats mp4,webm] [--no-export] [--draft] [--at 3.5,title@end] [--onion 1:3] [--query a=1&b=2] [--timeout S] [--root DIR] [--chrome PATH]');
423
483
  process.exit(2);
424
484
  }
425
485
  const report = await runCheck(opts);
@@ -13,6 +13,8 @@
13
13
  *
14
14
  * Options: -o, --out FILE (default ./<page name>.<format>; the container follows the extension: mp4 webm mov mkv) ·
15
15
  * --format mp4|webm|mov|mkv|pdf (a .pdf is the deck as pages: one picture per page of a movie with `stops`; --all-stops makes a page of every stop) · --quality very-low|low|medium|high|very-high (video and audio bitrate, default high) ·
16
+ * --range A:B (only seconds A to B; 2: runs to the end, :5 starts at the beginning) · --scene NAME (the span of a top-level layer: a movie's scene) · --scale S (output size, 0 < S <= 1: smaller picture, smaller file) ·
17
+ * --draft (for looking: half size, low quality, no motion blur; a --quality you give still wins) ·
16
18
  * --motion-blur SAMPLES (2-64: each frame is drawn that many times over the shutter and averaged; overrides the page's own motionBlur) ·
17
19
  * --shutter FRACTION (with --motion-blur: how long the shutter is open, 0-1 of a frame, default 0.5) ·
18
20
  * --video-codec C · --audio-codec C · --query "a=1&b=2" (added to the page URL) · --fail-on-warn (exit 1 when the page
@@ -41,7 +43,7 @@ export function defaultOutput(page, format) {
41
43
  }
42
44
 
43
45
  export function parseRenderArgs(argv) {
44
- const o = { page: null, out: null, format: null, quality: 'high', videoCodec: null, audioCodec: null, allStops: false, motionBlur: null, shutter: null, query: null, failOnWarn: false, quiet: false, timeout: 900, root: null, chrome: null };
46
+ const o = { range: null, scene: null, scale: null, draft: false, qualityGiven: false, page: null, out: null, format: null, quality: 'high', videoCodec: null, audioCodec: null, allStops: false, motionBlur: null, shutter: null, query: null, failOnWarn: false, quiet: false, timeout: 900, root: null, chrome: null };
45
47
  const need = (i, name) => { if (i + 1 >= argv.length) throw new Error(`${name} needs a value`); return argv[i + 1]; };
46
48
  for (let i = 0; i < argv.length; i++) {
47
49
  const a = argv[i];
@@ -50,6 +52,7 @@ export function parseRenderArgs(argv) {
50
52
  o.format = need(i++, a).toLowerCase();
51
53
  if (!FORMATS.includes(o.format)) throw new Error(`--format must be mp4, webm, mov, mkv or pdf (got "${o.format}")`);
52
54
  } else if (a === '--quality') {
55
+ o.qualityGiven = true;
53
56
  o.quality = need(i++, a);
54
57
  if (!QUALITIES.includes(o.quality)) throw new Error(`--quality must be very-low, low, medium, high or very-high (got "${o.quality}")`);
55
58
  } else if (a === '--video-codec') o.videoCodec = need(i++, a);
@@ -60,7 +63,17 @@ export function parseRenderArgs(argv) {
60
63
  } else if (a === '--shutter') {
61
64
  o.shutter = Number(need(i++, a));
62
65
  if (!(o.shutter > 0 && o.shutter <= 1)) throw new Error(`--shutter must be above 0 and at most 1 (0.5 is a 180° shutter), got "${argv[i]}"`);
63
- } else if (a === '--query') o.query = need(i++, a).replace(/^\?/, '');
66
+ } else if (a === '--range') {
67
+ const m = need(i++, a).match(/^(\d+(?:\.\d+)?)?:(\d+(?:\.\d+)?)?$/);
68
+ if (!m || (m[1] === undefined && m[2] === undefined)) throw new Error(`--range must look like 2:5 (seconds; 2: runs to the end, :5 starts at the beginning), got "${argv[i]}"`);
69
+ o.range = [m[1] === undefined ? null : Number(m[1]), m[2] === undefined ? null : Number(m[2])];
70
+ if (o.range[0] !== null && o.range[1] !== null && !(o.range[0] < o.range[1])) throw new Error(`--range ${argv[i]}: it must start before it ends (2:5 is seconds 2 to 5)`);
71
+ } else if (a === '--scene') o.scene = need(i++, a);
72
+ else if (a === '--scale') {
73
+ o.scale = Number(need(i++, a));
74
+ if (!(o.scale > 0 && o.scale <= 1)) throw new Error(`--scale must be above 0 and at most 1 (0.5 is half size), got "${argv[i]}"`);
75
+ } else if (a === '--draft') o.draft = true;
76
+ else if (a === '--query') o.query = need(i++, a).replace(/^\?/, '');
64
77
  else if (a === '--all-stops') o.allStops = true;
65
78
  else if (a === '--fail-on-warn') o.failOnWarn = true;
66
79
  else if (a === '--quiet') o.quiet = true;
@@ -74,6 +87,8 @@ export function parseRenderArgs(argv) {
74
87
  if (o.shutter !== null && o.motionBlur === null) throw new Error('--shutter goes with --motion-blur SAMPLES');
75
88
  if (o.allStops && o.format !== 'pdf' && !(o.out && formatFromPath(o.out) === 'pdf')) throw new Error('--all-stops goes with a PDF (-o deck.pdf)');
76
89
  o.format ??= (o.out ? formatFromPath(o.out) : null) ?? 'mp4';
90
+ if (o.range && o.scene !== null) throw new Error('--range and --scene cannot be used together: a scene is a range');
91
+ if (o.format === 'pdf' && (o.range || o.scene !== null || o.scale !== null || o.draft)) throw new Error('--range, --scene, --scale and --draft are not for a PDF (a PDF is the pages of a deck)');
77
92
  return o;
78
93
  }
79
94
 
@@ -81,16 +96,16 @@ const UPLOAD = '/__pixi_effects_out';
81
96
  const kb = n => (n >= 1048576 ? `${(n / 1048576).toFixed(2)} MB` : `${Math.max(1, Math.round(n / 1024))} KB`);
82
97
 
83
98
  /** The page's side: render, and PUT the Blob to our server (so a long video never goes through base64). */
84
- const renderScript = (format, quality, videoCodec, audioCodec, motionBlur, shutter, allStops) => `(() => {
99
+ const renderScript = (format, quality, videoCodec, audioCodec, motionBlur, shutter, allStops, part = {}) => `(() => {
85
100
  window.__r = { state: 'running', progress: 0 };
86
101
  movie.on('progress', e => { window.__r.progress = e.progress; });
87
102
  (async () => {
88
103
  try {
89
- const video = { bitrate: ${JSON.stringify(quality)}${videoCodec ? `, codec: ${JSON.stringify(videoCodec)}` : ''} };
104
+ const video = { ${[part.draft && !part.qualityGiven ? null : `bitrate: ${JSON.stringify(quality)}`, videoCodec ? `codec: ${JSON.stringify(videoCodec)}` : null].filter(Boolean).join(', ')} };
90
105
  const audio = { bitrate: ${JSON.stringify(quality)}${audioCodec ? `, codec: ${JSON.stringify(audioCodec)}` : ''} };
91
106
  const blob = ${JSON.stringify(format)} === 'pdf'
92
107
  ? await movie.exportPDF({ which: ${allStops ? "'stops'" : "'pages'"}, title: document.title })
93
- : await movie.render({ format: ${JSON.stringify(format)}, video, audio${motionBlur ? `, motionBlur: { samples: ${motionBlur}${shutter ? `, shutter: ${shutter}` : ''} }` : ''} });
108
+ : await movie.render({ format: ${JSON.stringify(format)}, video, audio${part.range ? `, range: [${part.range[0] ?? 0}, ${part.range[1] ?? 'movie.duration'}]` : ''}${part.scene ? `, range: ${JSON.stringify(part.scene)}` : ''}${part.scale ? `, scale: ${part.scale}` : ''}${part.draft ? ', draft: true' : ''}${motionBlur ? `, motionBlur: { samples: ${motionBlur}${shutter ? `, shutter: ${shutter}` : ''} }` : ''} });
94
109
  const res = await fetch(${JSON.stringify(UPLOAD)}, { method: 'PUT', body: blob });
95
110
  if (!res.ok) throw new Error('could not hand the file to the command (' + res.status + ')');
96
111
  window.__r = { state: 'done', bytes: blob.size, type: blob.type };
@@ -156,7 +171,7 @@ export async function runRender(opts, log = console.log, progress = () => {}) {
156
171
  }
157
172
  Object.assign(result, { duration: info.duration, frames: info.totalFrames, width: info.width, height: info.height, frameRate: info.frameRate });
158
173
 
159
- await cdp.eval(renderScript(opts.format, opts.quality, opts.videoCodec, opts.audioCodec, opts.motionBlur, opts.shutter, opts.allStops));
174
+ await cdp.eval(renderScript(opts.format, opts.quality, opts.videoCodec, opts.audioCodec, opts.motionBlur, opts.shutter, opts.allStops, { range: opts.range, scene: opts.scene, scale: opts.scale, draft: opts.draft, qualityGiven: opts.qualityGiven }));
160
175
  let state;
161
176
  for (;;) {
162
177
  left();
@@ -190,7 +205,7 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
190
205
  if (typeof WebSocket === 'undefined') throw new Error('Node >= 22 is needed (built-in WebSocket)');
191
206
  const opts = parseRenderArgs(process.argv.slice(2));
192
207
  if (!opts.page) {
193
- console.error('usage: node ai/tools/render.mjs <page.html> [-o out.mp4] [--format mp4|webm|mov|mkv|pdf] [--quality very-low|low|medium|high|very-high] [--query a=1] [--fail-on-warn] [--quiet] [--timeout S] [--root DIR] [--chrome PATH]');
208
+ console.error('usage: node ai/tools/render.mjs <page.html> [-o out.mp4] [--format mp4|webm|mov|mkv|pdf] [--range A:B | --scene NAME] [--scale S] [--draft] [--quality very-low|low|medium|high|very-high] [--query a=1] [--fail-on-warn] [--quiet] [--timeout S] [--root DIR] [--chrome PATH]');
194
209
  process.exit(2);
195
210
  }
196
211
  let last = -1;
@@ -0,0 +1,14 @@
1
+ 'use strict';
2
+
3
+ var chunkJW67MDHQ_cjs = require('./chunk-JW67MDHQ.cjs');
4
+ require('./chunk-ANO5R2E6.cjs');
5
+ require('./chunk-UQBGW5DO.cjs');
6
+
7
+
8
+
9
+ Object.defineProperty(exports, "CompositionSequence", {
10
+ enumerable: true,
11
+ get: function () { return chunkJW67MDHQ_cjs.CompositionSequence; }
12
+ });
13
+ //# sourceMappingURL=Composition-6JMXWNVV.cjs.map
14
+ //# sourceMappingURL=Composition-6JMXWNVV.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-3WWNVRR5.cjs"}
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-6JMXWNVV.cjs"}
@@ -0,0 +1,5 @@
1
+ export { CompositionSequence } from './chunk-UXDR3ZDU.js';
2
+ import './chunk-SG3Y66FN.js';
3
+ import './chunk-DKGG7WD4.js';
4
+ //# sourceMappingURL=Composition-MLBHYYFF.js.map
5
+ //# sourceMappingURL=Composition-MLBHYYFF.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-VJWY2NDL.js"}
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-MLBHYYFF.js"}
@@ -1,4 +1,4 @@
1
- import { f as Movie } from './Movie-oA0SUAyb.cjs';
1
+ import { h as Movie } from './Movie-K2YuVAdg.cjs';
2
2
  import 'pixi.js';
3
3
  import 'gsap';
4
4
  import './types-SS51lP6O.cjs';
@@ -1,4 +1,4 @@
1
- import { f as Movie } from './Movie-DilBC348.js';
1
+ import { h as Movie } from './Movie-DlHQ9C02.js';
2
2
  import 'pixi.js';
3
3
  import 'gsap';
4
4
  import './types-SS51lP6O.js';
@@ -5,6 +5,12 @@ import { U as Stop, e as AssetSpec, c as CompositionSpec } from './types-SS51lP6
5
5
  interface AudioInspectOptions {
6
6
  /** Width of each loudness window in seconds. Default 0.1. */
7
7
  window?: number;
8
+ /** The scenes to measure one by one (`movie.inspectAudio()` fills this from the movie's named top-level compositions). Default: the whole movie as one. */
9
+ scenes?: Array<{
10
+ name: string;
11
+ start: number;
12
+ end: number;
13
+ }>;
8
14
  }
9
15
  /** One sound measured ON ITS OWN (not mixed), so overlapping layers do not blur it. */
10
16
  interface SoundMeasure {
@@ -44,8 +50,76 @@ interface AudioReport {
44
50
  peakDb: number;
45
51
  brightnessHz: number;
46
52
  }>;
47
- /** Problems worth fixing: limiting, inaudible layers, sounds cut off by the end of the movie. Read this first. */
53
+ /** Problems worth fixing: limiting, clipping, a true peak above 0 dBTP, inaudible layers, sounds cut off by the end of the movie. Read this first. */
48
54
  issues: string[];
55
+ /** The mix as a whole (ITU-R BS.1770 loudness; `integratedLufs` is null for a mix shorter than 0.4 s or with nothing above −70 LUFS). */
56
+ loudness: {
57
+ integratedLufs: number | null;
58
+ truePeakDb: number;
59
+ clippedSamples: number;
60
+ silences: Array<{
61
+ from: number;
62
+ to: number;
63
+ }>;
64
+ };
65
+ /** The same measure for each scene (`silent`: nothing above −60 dBFS). */
66
+ scenes: Array<{
67
+ name: string;
68
+ from: number;
69
+ to: number;
70
+ lufs: number | null;
71
+ peakDb: number;
72
+ silent: boolean;
73
+ }>;
74
+ /** When each sound starts: what a waveform marks. */
75
+ cues: Array<{
76
+ t: number;
77
+ name: string;
78
+ }>;
79
+ /** Advice, not faults: a mix that is quiet or loud for web video, little headroom, silent stretches. */
80
+ notes: string[];
81
+ }
82
+
83
+ interface FontReport {
84
+ /** Text layers none of whose fonts is available here: they are drawn in a fallback. */
85
+ missing: Array<{
86
+ layer: string;
87
+ family: string;
88
+ }>;
89
+ /** Web fonts (`@font-face`) whose file could not be loaded and that a text layer uses. */
90
+ failed: string[];
91
+ /** Web fonts whose file could not be loaded but no text layer uses (an unused face: worth a look, not a fault). */
92
+ failedUnused: string[];
93
+ }
94
+
95
+ /** One kind of issue, with how many frames it was seen in. Issues that differ only by numbers are one kind. */
96
+ interface IssueGroup {
97
+ message: string;
98
+ count: number;
99
+ firstFrame: number;
100
+ lastFrame: number;
101
+ }
102
+ interface ReviewOptions {
103
+ /** Moments to resolve and report: `3.5`, `50%`, `f120`, `title@end` (comma-separated). */
104
+ at?: string;
105
+ /** Every layout issue is a problem (by default text overlaps are only listed for review). */
106
+ strict?: boolean;
107
+ }
108
+ interface ReviewReport {
109
+ /** How many frames were inspected. */
110
+ frames: number;
111
+ /** Text cut off by an edge, outside the canvas, or with no size (with `strict`: every issue). */
112
+ problems: IssueGroup[];
113
+ /** Text overlaps and fonts that are not available: often intentional, look at them on the contact sheet. */
114
+ review: IssueGroup[];
115
+ fonts: FontReport;
116
+ /** The sound as numbers (`movie.inspectAudio()`), or null for a movie with no audio. */
117
+ audio: AudioReport | null;
118
+ /** The moments of `at`, as frames with a label that is safe as a file name. */
119
+ at: Array<{
120
+ label: string;
121
+ frame: number;
122
+ }>;
49
123
  }
50
124
 
51
125
  interface Rect {
@@ -107,6 +181,8 @@ interface TimelineRow {
107
181
  start: number;
108
182
  end: number;
109
183
  }>;
184
+ /** With `parts`: the name of each layer the row stands for, in the same order (so `render({ range: 'pop-3' })` can find it). */
185
+ partNames?: string[];
110
186
  /** A short text for the tooltip (a text layer's words, an asset name). */
111
187
  detail?: string;
112
188
  }
@@ -196,6 +272,15 @@ interface RenderOptions {
196
272
  codec?: string;
197
273
  bitrate?: 'very-low' | 'low' | 'medium' | 'high' | 'very-high';
198
274
  };
275
+ /**
276
+ * Only part of the movie: `[from, to]` in seconds, or the name of a top-level layer (its start to its end). The file starts at 0
277
+ * and its sound is cut the same way (with a 10 ms fade on a cut edge). Past the end, backwards or an unknown name is an error.
278
+ */
279
+ range?: [number, number] | string;
280
+ /** Output size relative to the canvas, above 0 and at most 1. Each frame is drawn at full size and copied smaller before it is encoded: the file and the encoding get smaller, the drawing is not faster. */
281
+ scale?: number;
282
+ /** For looking, not for delivering: `scale: 0.5`, `video.bitrate: 'low'` and no motion blur. Anything you set yourself wins. */
283
+ draft?: boolean;
199
284
  }
200
285
  interface SnapshotOptions {
201
286
  /** Motion blur for this picture (see `MovieOptions.motionBlur`); overrides the movie's own setting, `false` turns it off. */
@@ -248,6 +333,19 @@ interface ContactSheetOptions {
248
333
  cellWidth?: number;
249
334
  as?: 'blob' | 'dataURL';
250
335
  }
336
+ interface OnionSkinOptions {
337
+ /** Where the movement starts, in seconds. Default 0. */
338
+ from?: number;
339
+ /** Where it ends, in seconds. Default: the end of the movie. */
340
+ to?: number;
341
+ /** How many frames are overlaid, 1–64, spread evenly from `from` to `to`. Default 8. */
342
+ count?: number;
343
+ /** Output size relative to the canvas, above 0 and at most 4. Default 1. */
344
+ scale?: number;
345
+ /** Motion blur for these pictures (see `MovieOptions.motionBlur`); overrides the movie's own setting, `false` turns it off. */
346
+ motionBlur?: MotionBlurSpec;
347
+ as?: 'blob' | 'dataURL';
348
+ }
251
349
  interface FrameEvent {
252
350
  frame: number;
253
351
  totalFrames: number;
@@ -419,6 +517,17 @@ declare class Movie {
419
517
  motionBlur?: MotionBlurSpec;
420
518
  onImage?: (image: StopImage) => void;
421
519
  }): Promise<Blob>;
520
+ /**
521
+ * One picture of a movement: `count` frames from `from` to `to` drawn one over the other, each later one stronger, so a thing that
522
+ * moves leaves a trail (its path and its easing: even steps = constant speed, bunched = slow) and what stays still stays itself.
523
+ * The movie is left where it was.
524
+ */
525
+ onionSkin(opts?: OnionSkinOptions & {
526
+ as?: 'blob';
527
+ }): Promise<Blob>;
528
+ onionSkin(opts: OnionSkinOptions & {
529
+ as: 'dataURL';
530
+ }): Promise<string>;
422
531
  /**
423
532
  * Several frames on ONE image, each labelled with its frame number and time — the cheapest way to
424
533
  * check a whole animation by eye. Restores the current frame afterwards.
@@ -447,6 +556,21 @@ declare class Movie {
447
556
  * The soundtrack, measured: when every audio layer plays, how loud the mix is over time, and `issues`
448
557
  * (limiting, inaudible layers, sounds cut off by the end). Use it to CHECK sound you cannot hear.
449
558
  */
559
+ /** 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. */
560
+ resolveAt(list: string): Array<{
561
+ label: string;
562
+ frame: number;
563
+ }>;
564
+ /**
565
+ * One call that looks at everything `pixi-effects-check` looks at inside the page: layout issues over the whole timeline (every
566
+ * 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.
567
+ */
568
+ review(opts?: ReviewOptions): Promise<ReviewReport>;
569
+ /**
570
+ * The fonts of the text layers: `missing` lists layers none of whose `fontFamily` entries is available in this browser (they are
571
+ * drawn in a fallback font), `failed` lists web fonts (`@font-face`) whose file could not be loaded.
572
+ */
573
+ inspectFonts(): FontReport;
450
574
  inspectAudio(opts?: AudioInspectOptions): AudioReport;
451
575
  /**
452
576
  * Draw the stage now. PixiJS's CullerPlugin only culls inside `app.render()` (the ticker), and we render
@@ -509,4 +633,4 @@ declare class Movie {
509
633
  destroy(): Promise<void>;
510
634
  }
511
635
 
512
- export { type AudioInspectOptions as A, type ContactSheetOptions as C, type FrameEvent as F, type InspectOptions as I, type LayerInfo as L, type MotionBlurOptions as M, type ProgressEvent as P, type RenderOptions as R, type SnapshotOptions as S, type TimelineData as T, type VolumeEvent as V, type AudioReport as a, type AudioSourceReport as b, type InspectReport as c, type LoaderOption as d, type MotionBlurSpec as e, Movie as f, type MovieErrorEvent as g, type MovieOptions as h, type SoundMeasure as i, type StopEvent as j, type StopImage as k, type StopImagesOptions as l, type TimelineHtmlOptions as m, type TimelineRow as n, type TimelineSvgOptions as o, type TimelineTransition as p, dismissLoader as q, failLoader as r, resolveLoader as s };
636
+ export { type AudioInspectOptions as A, type ContactSheetOptions as C, type FontReport as F, type InspectOptions as I, type LayerInfo as L, type MotionBlurOptions as M, type OnionSkinOptions as O, type ProgressEvent as P, type RenderOptions as R, type SnapshotOptions as S, type TimelineData as T, type VolumeEvent as V, type AudioReport as a, type AudioSourceReport as b, type FrameEvent as c, type InspectReport as d, type IssueGroup as e, type LoaderOption as f, type MotionBlurSpec as g, Movie as h, type MovieErrorEvent as i, type MovieOptions as j, type ReviewOptions as k, type ReviewReport as l, type SoundMeasure as m, type StopEvent as n, type StopImage as o, type StopImagesOptions as p, type TimelineHtmlOptions as q, type TimelineRow as r, type TimelineSvgOptions as s, type TimelineTransition as t, dismissLoader as u, failLoader as v, resolveLoader as w };