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.
- package/CHANGELOG.md +55 -0
- package/README.md +7 -7
- package/ai/CHAT.md +7 -0
- package/ai/SKILL.md +3 -2
- package/ai/chat-template.html +8 -4
- package/ai/reference/cheatsheet.md +6 -2
- package/ai/reference/pitfalls.md +3 -0
- package/ai/reference/recipes.md +67 -0
- package/ai/template.html +5 -5
- package/ai/tools/check.mjs +119 -59
- package/ai/tools/render.mjs +22 -7
- package/dist/Composition-6JMXWNVV.cjs +14 -0
- package/dist/{Composition-3WWNVRR5.cjs.map → Composition-6JMXWNVV.cjs.map} +1 -1
- package/dist/Composition-MLBHYYFF.js +5 -0
- package/dist/{Composition-VJWY2NDL.js.map → Composition-MLBHYYFF.js.map} +1 -1
- package/dist/Controller.d.cts +1 -1
- package/dist/Controller.d.ts +1 -1
- package/dist/{Movie-DilBC348.d.ts → Movie-DlHQ9C02.d.ts} +126 -2
- package/dist/{Movie-oA0SUAyb.d.cts → Movie-K2YuVAdg.d.cts} +126 -2
- package/dist/Presenter.d.cts +1 -1
- package/dist/Presenter.d.ts +1 -1
- package/dist/{chunk-FJA5VUOF.cjs → chunk-ANO5R2E6.cjs} +25 -3
- package/dist/chunk-ANO5R2E6.cjs.map +1 -0
- package/dist/{chunk-2ROVCPSL.cjs → chunk-JW67MDHQ.cjs} +32 -27
- package/dist/chunk-JW67MDHQ.cjs.map +1 -0
- package/dist/{chunk-ZNKQH356.js → chunk-SG3Y66FN.js} +25 -4
- package/dist/chunk-SG3Y66FN.js.map +1 -0
- package/dist/{chunk-XTIHKODH.js → chunk-UXDR3ZDU.js} +10 -5
- package/dist/chunk-UXDR3ZDU.js.map +1 -0
- package/dist/index.cjs +464 -43
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +442 -21
- package/dist/index.js.map +1 -1
- package/dist/loader.css +1 -1
- package/dist/three.cjs +5 -5
- package/dist/three.js +1 -1
- package/docs/dsl.md +2 -1
- package/llms-full.txt +81 -5
- package/llms.txt +1 -1
- package/package.json +6 -3
- package/dist/Composition-3WWNVRR5.cjs +0 -14
- package/dist/Composition-VJWY2NDL.js +0 -5
- package/dist/chunk-2ROVCPSL.cjs.map +0 -1
- package/dist/chunk-FJA5VUOF.cjs.map +0 -1
- package/dist/chunk-XTIHKODH.js.map +0 -1
- package/dist/chunk-ZNKQH356.js.map +0 -1
package/ai/tools/check.mjs
CHANGED
|
@@ -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: --
|
|
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
|
-
|
|
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
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
if (
|
|
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 ?
|
|
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}
|
|
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);
|
package/ai/tools/render.mjs
CHANGED
|
@@ -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 === '--
|
|
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)}
|
|
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-
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-6JMXWNVV.cjs"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-MLBHYYFF.js"}
|
package/dist/Controller.d.cts
CHANGED
package/dist/Controller.d.ts
CHANGED
|
@@ -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
|
|
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 };
|