pixi-effects 0.38.1 → 0.40.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 +28 -0
- package/README.md +16 -30
- package/ai/chat-template.html +3 -3
- package/ai/tools/check.mjs +109 -13
- package/ai/tools/gltfProbe.mjs +122 -0
- package/ai/tools/media.mjs +129 -10
- package/ai/tools/pixi-effects.mjs +1 -1
- package/ai/tools/render.mjs +34 -8
- package/dist/{Base-BKKwxavW.d.cts → Base-DQWIJ6bJ.d.cts} +1 -1
- package/dist/{Base-ioh_AjLB.d.ts → Base-Jgkud1Aw.d.ts} +1 -1
- package/dist/Composition-C3UEVHQG.cjs +16 -0
- package/dist/{Composition-23KGIJX4.cjs.map → Composition-C3UEVHQG.cjs.map} +1 -1
- package/dist/Composition-KRWH72MA.js +7 -0
- package/dist/{Composition-5SQXSZNA.js.map → Composition-KRWH72MA.js.map} +1 -1
- package/dist/Controller.d.cts +2 -2
- package/dist/Controller.d.ts +2 -2
- package/dist/{Movie-BtwFI2ne.d.ts → Movie-CG6Jy1tx.d.ts} +6 -1
- package/dist/{Movie-CSWUTnMd.d.cts → Movie-CMq4KFmc.d.cts} +6 -1
- package/dist/{Spans-WJFPHMSQ.cjs → Spans-APNJHCYO.cjs} +9 -9
- package/dist/{Spans-WJFPHMSQ.cjs.map → Spans-APNJHCYO.cjs.map} +1 -1
- package/dist/{Spans-4IBQASGK.js → Spans-TWO4VN5H.js} +4 -4
- package/dist/{Spans-4IBQASGK.js.map → Spans-TWO4VN5H.js.map} +1 -1
- package/dist/{chunk-4CSL5QSL.js → chunk-26Z544SH.js} +24 -13
- package/dist/chunk-26Z544SH.js.map +1 -0
- package/dist/{chunk-FE42PKHP.cjs → chunk-27PYJ4JY.cjs} +83 -72
- package/dist/chunk-27PYJ4JY.cjs.map +1 -0
- package/dist/{chunk-7NXKBRT7.cjs → chunk-7EI2N7WO.cjs} +27 -7
- package/dist/{chunk-7NXKBRT7.cjs.map → chunk-7EI2N7WO.cjs.map} +1 -1
- package/dist/{chunk-XOCRS5MW.js → chunk-CT5CZVT5.js} +15 -8
- package/dist/chunk-CT5CZVT5.js.map +1 -0
- package/dist/{chunk-KNMDDUA3.cjs → chunk-LEL7UNYQ.cjs} +50 -43
- package/dist/chunk-LEL7UNYQ.cjs.map +1 -0
- package/dist/{chunk-UDTLYYGY.js → chunk-LMWXR5OC.js} +27 -7
- package/dist/{chunk-UDTLYYGY.js.map → chunk-LMWXR5OC.js.map} +1 -1
- package/dist/index.cjs +33 -33
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +3 -3
- package/dist/loader.css +1 -1
- package/dist/present.cjs +3 -3
- package/dist/present.d.cts +2 -2
- package/dist/present.d.ts +2 -2
- package/dist/present.js +3 -3
- package/dist/three.cjs +505 -12
- package/dist/three.cjs.map +1 -1
- package/dist/three.d.cts +139 -7
- package/dist/three.d.ts +139 -7
- package/dist/three.js +501 -10
- package/dist/three.js.map +1 -1
- package/dist/{types-BXi-0iZS.d.cts → types-DIh93lFb.d.cts} +82 -1
- package/dist/{types-BXi-0iZS.d.ts → types-DIh93lFb.d.ts} +82 -1
- package/docs/api.md +21 -11
- package/docs/dsl.md +73 -5
- package/docs/media.md +17 -2
- package/llms-full.txt +188 -23
- package/llms.txt +1 -1
- package/package.json +11 -12
- package/skills/pixi-effects/SKILL.md +5 -3
- package/skills/pixi-effects/reference/cheatsheet.md +2 -2
- package/skills/pixi-effects/reference/pitfalls.md +1 -0
- package/skills/pixi-effects/reference/recipes.md +69 -0
- package/skills/pixi-effects/template.html +9 -8
- package/dist/Composition-23KGIJX4.cjs +0 -16
- package/dist/Composition-5SQXSZNA.js +0 -7
- package/dist/chunk-4CSL5QSL.js.map +0 -1
- package/dist/chunk-FE42PKHP.cjs.map +0 -1
- package/dist/chunk-KNMDDUA3.cjs.map +0 -1
- package/dist/chunk-XOCRS5MW.js.map +0 -1
package/ai/tools/media.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* pixi-effects probe | trim | frames | sound — look at, listen to and cut footage, with no ffmpeg installed.
|
|
4
4
|
*
|
|
5
|
-
* npx pixi-effects probe <file|url> [--keyframes] [--json] what is in a file: container, duration, tracks, codecs, size, notes
|
|
5
|
+
* npx pixi-effects probe <file|url> [--keyframes] [--json] what is in a file: container, duration, tracks, codecs, size, notes (a .glb / .gltf model: clips and size)
|
|
6
6
|
* npx pixi-effects trim <file|url> <start> <end> -o <out> [--exact] [--force] [--json]
|
|
7
7
|
* cut [start, end] (seconds, or 1:02.5): by COPY when it can (no re-encode, no loss),
|
|
8
8
|
* and it says the real times; --exact re-encodes so the start is the frame asked for
|
|
@@ -22,6 +22,7 @@ import os from 'node:os';
|
|
|
22
22
|
import path from 'node:path';
|
|
23
23
|
import { createRequire } from 'node:module';
|
|
24
24
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
25
|
+
import { gltfText, isGltfFile, probeGltf } from './gltfProbe.mjs';
|
|
25
26
|
|
|
26
27
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
27
28
|
const PACKAGE_ROOT = path.resolve(HERE, '../..');
|
|
@@ -38,7 +39,7 @@ export const USAGE = `usage:
|
|
|
38
39
|
|
|
39
40
|
/** How each command reads its command line: the positional words, and each option with the kind of value it takes. */
|
|
40
41
|
const COMMANDS = {
|
|
41
|
-
probe: { positional: ['file'], flags: { '--keyframes': 'bool', '--json': 'bool' } },
|
|
42
|
+
probe: { positional: ['file'], flags: { '--keyframes': 'bool', '--expect': 'str', '--json': 'bool' } },
|
|
42
43
|
trim: { positional: ['file', 'start', 'end'], flags: { '-o': 'str', '--out': 'str', '--exact': 'bool', '--force': 'bool', '--json': 'bool', '--chrome': 'str' } },
|
|
43
44
|
audio: { positional: ['file'], flags: { '-o': 'str', '--out': 'str', '--force': 'bool', '--json': 'bool' } },
|
|
44
45
|
concat: { positional: ['file', 'file'], variadic: true, flags: { '-o': 'str', '--out': 'str', '--force': 'bool', '--json': 'bool' } },
|
|
@@ -157,7 +158,7 @@ async function openInput(file) {
|
|
|
157
158
|
}
|
|
158
159
|
|
|
159
160
|
/** What is in a file: container, duration, size, one row per track, key packet times with `keyframes`, and notes about what an author would trip on. */
|
|
160
|
-
export async function probeFile(file, { keyframes = false } = {}) {
|
|
161
|
+
export async function probeFile(file, { keyframes = false, exact = false } = {}) {
|
|
161
162
|
const { mb, input, format } = await openInput(file);
|
|
162
163
|
const notes = [];
|
|
163
164
|
let duration;
|
|
@@ -184,6 +185,13 @@ export async function probeFile(file, { keyframes = false } = {}) {
|
|
|
184
185
|
first: round(first, 4), end: round(end, 6), frameGap: round(median, 6),
|
|
185
186
|
stored: { width: await track.getCodedWidth(), height: await track.getCodedHeight() },
|
|
186
187
|
});
|
|
188
|
+
const cs = await track.getColorSpace().catch(() => null); // what the file says about its colours (a decoder reads this, not the encoder's intent)
|
|
189
|
+
if (cs && (cs.primaries || cs.transfer || cs.matrix)) video[video.length - 1].color = { primaries: cs.primaries ?? null, transfer: cs.transfer ?? null, matrix: cs.matrix ?? null, fullRange: !!cs.fullRange };
|
|
190
|
+
if (exact) { // every packet, not the first 300: the real frame count and the bitrate of the whole file
|
|
191
|
+
const all = await track.computePacketStats(Infinity);
|
|
192
|
+
video[video.length - 1].frames = all.packetCount;
|
|
193
|
+
video[video.length - 1].bitrateAll = Math.round(all.averageBitrate);
|
|
194
|
+
}
|
|
187
195
|
if (first > 0.05) notes.push(`the video starts at ${round(first, 3)} s (its first frame is not at 0, so the sound can start before the picture; frames before ${round(first, 3)} s show the first frame)`);
|
|
188
196
|
if (rotation) notes.push(`rotated ${rotation}° clockwise: stored ${await track.getCodedWidth()}×${await track.getCodedHeight()}, shown ${await track.getDisplayWidth()}×${await track.getDisplayHeight()} (frames and anything that places the picture use the shown size; whether the picture then reads upright depends on how the file was made, so look at a frame)`);
|
|
189
197
|
if (fpsVaries) notes.push(`variable frame rate (the gaps between frames range from ${round(gaps[0] * 1000, 1)} to ${round(gaps[gaps.length - 1] * 1000, 1)} ms): a clip from a phone or a screen recording; cut and sample by time, not by frame number`);
|
|
@@ -209,17 +217,125 @@ const kb = (n) => (n >= 1048576 ? `${(n / 1048576).toFixed(2)} MB` : `${Math.max
|
|
|
209
217
|
|
|
210
218
|
function probeText(r, keyframes) {
|
|
211
219
|
const L = [`${r.file}`, ` ${r.container} · ${r.duration} s${r.bytes != null ? ` · ${kb(r.bytes)}` : ''}`];
|
|
212
|
-
for (const v of r.video) L.push(` video ${v.codec} ${v.width}×${v.height}${v.rotation ? ` shown (stored ${v.stored.width}×${v.stored.height}, turned ${v.rotation}° clockwise)` : ''} · ${v.fps}${v.fpsVaries ? ' (varies)' : ''} fps${v.bitrate ? ` · ${Math.round(v.bitrate / 1000)} kb/s` : ''}`);
|
|
220
|
+
for (const v of r.video) L.push(` video ${v.codec} ${v.width}×${v.height}${v.rotation ? ` shown (stored ${v.stored.width}×${v.stored.height}, turned ${v.rotation}° clockwise)` : ''} · ${v.fps}${v.fpsVaries ? ' (varies)' : ''} fps${v.bitrate ? ` · ${Math.round(v.bitrate / 1000)} kb/s` : ''}${v.color ? ` · colour ${v.color.matrix ?? '?'} (transfer ${v.color.transfer ?? '?'}, ${v.color.fullRange ? 'full' : 'limited'} range)` : ''}`);
|
|
213
221
|
for (const a of r.audio) L.push(` audio ${a.codec} ${a.sampleRate} Hz · ${a.channels} ch${a.bitrate ? ` · ${Math.round(a.bitrate / 1000)} kb/s` : ''}`);
|
|
214
222
|
if (keyframes && r.video.length) L.push(` key packets at: ${r.keyframes.slice(0, 12).join(', ')}${r.keyframes.length > 12 ? `, … (${r.keyframes.length})` : ''} s`);
|
|
215
223
|
for (const n of r.notes) L.push(` note ${n}`);
|
|
216
224
|
return L.join('\n');
|
|
217
225
|
}
|
|
218
226
|
|
|
227
|
+
// ───────────────────────────── probe --expect ─────────────────────────────
|
|
228
|
+
|
|
229
|
+
const EXPECT_KEYS = ['size', 'width', 'height', 'fps', 'frames', 'duration', 'bitrate', 'video', 'audio', 'container', 'matrix', 'transfer'];
|
|
230
|
+
const EXPECT_NUMERIC = ['width', 'height', 'fps', 'frames', 'duration', 'bitrate'];
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* `size=3840x2160 fps=30 frames=300 bitrate>=35M duration<=60s audio=none video=avc container=mp4` → [{ key, op, raw }].
|
|
234
|
+
* Words are separated by spaces or commas; the operators are = != >= <= > <; a bitrate may end in k or M, a duration in s.
|
|
235
|
+
*/
|
|
236
|
+
export function parseExpect(text) {
|
|
237
|
+
const items = [];
|
|
238
|
+
for (const word of String(text ?? '').split(/[\s,]+/).filter(Boolean)) {
|
|
239
|
+
const m = word.match(/^([A-Za-z]+)(>=|<=|!=|=|>|<)(.*)$/);
|
|
240
|
+
if (!m) {
|
|
241
|
+
const name = word.match(/^[A-Za-z]+/)?.[0];
|
|
242
|
+
if (name && EXPECT_KEYS.includes(name.toLowerCase())) throw new UsageError(`probe --expect: ${name}: write a comparison such as ${name}=30 or ${name}>=30 (words: ${EXPECT_KEYS.join(' ')})`);
|
|
243
|
+
throw new UsageError(`probe --expect: cannot read "${word}": write name, operator and value, for example frames=300 or bitrate>=35M (words: ${EXPECT_KEYS.join(' ')})`);
|
|
244
|
+
}
|
|
245
|
+
const key = m[1].toLowerCase(), op = m[2], raw = m[3];
|
|
246
|
+
if (!EXPECT_KEYS.includes(key)) {
|
|
247
|
+
const guess = nearest(key, EXPECT_KEYS);
|
|
248
|
+
throw new UsageError(`probe --expect: unknown "${m[1]}"${guess ? `; did you mean "${guess}"?` : ''} (words: ${EXPECT_KEYS.join(' ')})`);
|
|
249
|
+
}
|
|
250
|
+
if (!raw) throw new UsageError(`probe --expect: ${key}${op} needs a value`);
|
|
251
|
+
if (!EXPECT_NUMERIC.includes(key) && op !== '=' && op !== '!=') throw new UsageError(`probe --expect: ${key} takes only = or != (got ${op})`);
|
|
252
|
+
if (EXPECT_NUMERIC.includes(key) && !Number.isFinite(expectNumber(key, raw))) throw new UsageError(`probe --expect: ${key} wants a number${key === 'bitrate' ? ' (35M, 800k or bits a second)' : key === 'duration' ? ' of seconds (10 or 10s)' : ''}, got "${raw}"`);
|
|
253
|
+
items.push({ key, op, raw });
|
|
254
|
+
}
|
|
255
|
+
if (!items.length) throw new UsageError(`probe --expect: nothing to check: write what the file should be, for example size=3840x2160 fps=30 frames=300 (words: ${EXPECT_KEYS.join(' ')})`);
|
|
256
|
+
return items;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** The number of an expectation's value: bits a second for a bitrate (k, M), seconds for a duration (an s is allowed). */
|
|
260
|
+
function expectNumber(key, raw) {
|
|
261
|
+
const m = String(raw).trim().match(/^(\d+(?:\.\d+)?|\.\d+)\s*([kKmM]?)\s*(?:s)?$/);
|
|
262
|
+
if (!m) return NaN;
|
|
263
|
+
const unit = m[2].toLowerCase();
|
|
264
|
+
if (unit && key !== 'bitrate') return NaN;
|
|
265
|
+
return Number(m[1]) * (unit === 'k' ? 1e3 : unit === 'm' ? 1e6 : 1);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const compare = (got, op, want, tol = 0) => {
|
|
269
|
+
switch (op) {
|
|
270
|
+
case '=': return Math.abs(got - want) <= tol;
|
|
271
|
+
case '!=': return Math.abs(got - want) > tol;
|
|
272
|
+
case '>': return got > want + tol;
|
|
273
|
+
case '<': return got < want - tol;
|
|
274
|
+
case '>=': return got >= want - tol;
|
|
275
|
+
case '<=': return got <= want + tol;
|
|
276
|
+
}
|
|
277
|
+
return false;
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
/** Each expectation against the numbers of a `probeFile(file, { exact: true })` report: [{ key, op, want, got, ok, line }]. */
|
|
281
|
+
export function checkExpect(report, items) {
|
|
282
|
+
const v = report.video?.[0], a = report.audio?.[0];
|
|
283
|
+
const mbps = n => `${Math.round(n / 1e5) / 10} Mbps`;
|
|
284
|
+
return items.map(({ key, op, raw }) => {
|
|
285
|
+
let got, ok, shown;
|
|
286
|
+
if (['size', 'width', 'height', 'fps', 'frames', 'duration', 'bitrate', 'video', 'matrix', 'transfer'].includes(key) && !v) {
|
|
287
|
+
got = null; ok = false; shown = 'no video';
|
|
288
|
+
} else if (key === 'size') {
|
|
289
|
+
got = `${v.width}x${v.height}`; ok = (got === raw.toLowerCase().replace('×', 'x')) === (op === '='); shown = got;
|
|
290
|
+
} else if (key === 'width' || key === 'height' || key === 'frames') {
|
|
291
|
+
got = v[key]; if (got === undefined) { ok = false; shown = 'not measured'; } else { ok = compare(got, op, expectNumber(key, raw)); shown = String(got); }
|
|
292
|
+
} else if (key === 'fps') {
|
|
293
|
+
got = v.fps; ok = compare(got, op, expectNumber(key, raw), 0.01); shown = `${got}`;
|
|
294
|
+
} else if (key === 'duration') {
|
|
295
|
+
const frame = v.frameGap || 1 / (v.fps || 30);
|
|
296
|
+
got = v.end; ok = compare(got, op, expectNumber(key, raw), frame / 2); shown = `${got} s`;
|
|
297
|
+
} else if (key === 'bitrate') {
|
|
298
|
+
got = v.bitrateAll ?? v.bitrate; ok = compare(got, op, expectNumber(key, raw)); shown = mbps(got);
|
|
299
|
+
} else if (key === 'video') {
|
|
300
|
+
got = v.codec; ok = (String(got).toLowerCase() === raw.toLowerCase()) === (op === '='); shown = String(got);
|
|
301
|
+
} else if (key === 'matrix' || key === 'transfer') {
|
|
302
|
+
got = v.color?.[key] ?? null; shown = got ?? 'not tagged'; ok = got !== null && ((String(got).toLowerCase() === raw.toLowerCase()) === (op === '='));
|
|
303
|
+
} else if (key === 'audio') {
|
|
304
|
+
const r = raw.toLowerCase();
|
|
305
|
+
got = a ? a.codec : 'none';
|
|
306
|
+
ok = (r === 'none' ? !a : r === 'any' ? !!a : !!a && String(a.codec).toLowerCase() === r) === (op === '=');
|
|
307
|
+
shown = a ? String(a.codec) : 'none';
|
|
308
|
+
} else {
|
|
309
|
+
got = String(report.container ?? '').toLowerCase(); ok = (got === raw.toLowerCase()) === (op === '='); shown = got;
|
|
310
|
+
}
|
|
311
|
+
return { key, op, want: raw, got, ok, line: ` ${ok ? 'ok ' : 'FAIL'} ${key.padEnd(9)} ${shown} (wanted ${key === 'bitrate' ? bitrateWanted(op, raw) : `${op}${raw}`})` };
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function bitrateWanted(op, raw) { const n = expectNumber('bitrate', raw); return `${op}${Math.round(n / 1e5) / 10} Mbps`; }
|
|
316
|
+
|
|
219
317
|
commands.probe = async ({ file, opts }, io) => {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
318
|
+
if (isGltfFile(file)) { // a model, not footage: read in Node, no container library, no browser
|
|
319
|
+
if (opts.expect !== undefined || opts.keyframes) throw new UsageError('probe on a glTF model takes no --expect or --keyframes (they are for video and audio files); it prints the clips and the size');
|
|
320
|
+
const r = await probeGltf(file);
|
|
321
|
+
io.log(opts.json ? JSON.stringify(r, null, 2) : gltfText(r));
|
|
322
|
+
return 0;
|
|
323
|
+
}
|
|
324
|
+
const items = opts.expect !== undefined ? parseExpect(opts.expect) : null; // a bad expectation is a usage error before any file is read
|
|
325
|
+
const r = await probeFile(file, { keyframes: !!opts.keyframes, exact: !!items });
|
|
326
|
+
const results = items ? checkExpect(r, items) : null;
|
|
327
|
+
if (results) r.expect = results.map(({ line, ...rest }) => rest);
|
|
328
|
+
if (opts.json) io.log(JSON.stringify(r, null, 2));
|
|
329
|
+
else {
|
|
330
|
+
const L = [probeText(r, !!opts.keyframes)];
|
|
331
|
+
if (results) {
|
|
332
|
+
L.push('', ...results.map(x => x.line));
|
|
333
|
+
const bad = results.filter(x => !x.ok).length;
|
|
334
|
+
L.push(bad ? `RESULT: ${bad} of ${results.length} expectations do not hold` : `RESULT: all ${results.length} expectations hold`);
|
|
335
|
+
}
|
|
336
|
+
io.log(L.join('\n'));
|
|
337
|
+
}
|
|
338
|
+
return results && results.some(x => !x.ok) ? 1 : 0;
|
|
223
339
|
};
|
|
224
340
|
|
|
225
341
|
const OUTPUT_FORMATS = { '.mp4': 'Mp4OutputFormat', '.m4v': 'Mp4OutputFormat', '.m4a': 'Mp4OutputFormat', '.mov': 'MovOutputFormat', '.webm': 'WebMOutputFormat', '.mkv': 'MkvOutputFormat', '.mka': 'MkvOutputFormat' };
|
|
@@ -470,6 +586,9 @@ const AT_HELP = 'use seconds (3.5), a percentage (50%), a frame (f120), start, e
|
|
|
470
586
|
*/
|
|
471
587
|
export function parseTimes(list, { duration, fps, first = 0, last: lastFrame }) {
|
|
472
588
|
const out = [], seen = new Set(), last = Math.max(first, lastFrame ?? duration - 1 / fps);
|
|
589
|
+
// A frame is shown from its own time until the next frame's: a time asked for a FRAME (f12, start, end) rounds UP to four decimals, never down (1/30 s as 0.0333 is
|
|
590
|
+
// before frame 1 and gives frame 0). A time asked in seconds is the instant asked for, and keeps the plain rounding.
|
|
591
|
+
const onFrame = t => Math.ceil(t * 1e4 - 1e-7) / 1e4 + 0; // + 0: Math.ceil(-0.00000001) is -0
|
|
473
592
|
const add = (t, label) => { if (!seen.has(label)) { seen.add(label); out.push({ t, label }); } };
|
|
474
593
|
const inside = (t, raw) => { if (t > duration + 1e-6) throw new Error(`${raw} s is past the end of the file (${round(duration, 3)} s)`); return Math.min(t, last > t ? t : Math.max(t, 0)); };
|
|
475
594
|
for (const raw of String(list).split(',').map(x => x.trim()).filter(Boolean)) {
|
|
@@ -480,12 +599,12 @@ export function parseTimes(list, { duration, fps, first = 0, last: lastFrame })
|
|
|
480
599
|
const n = Math.ceil(duration / step);
|
|
481
600
|
if (n > MAX_FRAMES) throw new Error(`--at "${raw}": that is ${n} frames of a ${round(duration, 1)} s file, too many (at most ${MAX_FRAMES}): a larger step, or a range of times`);
|
|
482
601
|
for (let k = 0; k * step < duration - 1e-6; k++) add(round(k * step, 4), `${round(k * step, 2).toFixed(2)}s`);
|
|
483
|
-
} else if (/^start$/i.test(raw)) add(
|
|
484
|
-
else if (/^end$/i.test(raw)) add(
|
|
602
|
+
} else if (/^start$/i.test(raw)) add(onFrame(first), 'start');
|
|
603
|
+
else if (/^end$/i.test(raw)) add(onFrame(last), 'end');
|
|
485
604
|
else if ((m = raw.match(/^(\d+(?:\.\d+)?)%$/))) {
|
|
486
605
|
if (Number(m[1]) > 100) throw new Error(`--at "${raw}": a percentage is 0–100`);
|
|
487
606
|
add(round(Math.min(last, duration * Number(m[1]) / 100), 4), `${m[1]}pct`);
|
|
488
|
-
} else if ((m = raw.match(/^f(\d+)$/i))) add(
|
|
607
|
+
} else if ((m = raw.match(/^f(\d+)$/i))) add(onFrame(inside(Number(m[1]) / fps, `frame ${m[1]} is ${round(Number(m[1]) / fps, 3)}`)), `f${m[1]}`);
|
|
489
608
|
else if (/^\d+(\.\d+)?$/.test(raw)) add(round(inside(Number(raw), raw), 4), `${Number(raw).toFixed(2)}s`);
|
|
490
609
|
else throw new Error(`--at: cannot read "${raw}": ${AT_HELP}`);
|
|
491
610
|
}
|
|
@@ -22,7 +22,7 @@ const USAGE = `usage:
|
|
|
22
22
|
pixi-effects check <page.html> [--at 3,6] [--out dir] review a video page: warnings, layout, sound, contact sheet, real export
|
|
23
23
|
pixi-effects render <page.html> -o <file.mp4> export the video
|
|
24
24
|
pixi-effects view <page.html> look at it in a browser, with a timeline
|
|
25
|
-
pixi-effects probe <file|url> what is in a video or audio file (codecs, size, duration), with no ffmpeg
|
|
25
|
+
pixi-effects probe <file|url> what is in a video or audio file (codecs, size, duration), with no ffmpeg; or a .glb / .gltf model (its clips and its size)
|
|
26
26
|
pixi-effects trim <file|url> <start> <end> -o <out> cut a file (by copy when it can) and say the real times
|
|
27
27
|
pixi-effects frames <file|url> [--at 1,50%,end] [--sheet] pictures of a video at those times
|
|
28
28
|
pixi-effects sound <file|url> [--waveform] loudness, peak, silences of a file's sound
|
package/ai/tools/render.mjs
CHANGED
|
@@ -44,7 +44,7 @@ export function defaultOutput(page, format) {
|
|
|
44
44
|
return path.resolve(path.basename(page).replace(/\.[^.]+$/, '') + '.' + format);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
export const USAGE = '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] [--var name=value] [--size 1080x1920] [--strict] [--quiet] [--timeout S] [--root DIR] [--chrome PATH]';
|
|
47
|
+
export const USAGE = 'usage: node ai/tools/render.mjs <page.html> [-o out.mp4] [--format mp4|webm|mov|mkv|wav|ogg|pdf] [--range A:B | --scene NAME] [--scale S] [--draft] [--quality very-low|low|medium|high|very-high] [--video-codec avc|hevc|vp9|av1|vp8] [--video-bitrate 8M|high] [--audio-codec C] [--motion-blur SAMPLES] [--shutter FRACTION] [--all-stops] [--query a=1] [--var name=value] [--size 1080x1920] [--strict] [--quiet] [--timeout S] [--root DIR] [--chrome PATH] [--help]\n(the header of ai/tools/render.mjs explains each option)';
|
|
48
48
|
|
|
49
49
|
export function parseRenderArgs(argv) {
|
|
50
50
|
const o = { range: null, scene: null, scale: null, draft: false, qualityGiven: false, page: null, out: null, format: null, quality: 'high', videoCodec: null, videoBitrate: null, audioCodec: null, allStops: false, motionBlur: null, shutter: null, query: null, vars: [], size: null, strict: false, quiet: false, timeout: 900, root: null, chrome: null };
|
|
@@ -146,6 +146,34 @@ export function fileSeconds(r, opts) {
|
|
|
146
146
|
return r.duration;
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
/**
|
|
150
|
+
* The progress of a render. A terminal gets one line that is rewritten; a command whose stderr is not a terminal (an agent, a script, CI) used to get NOTHING,
|
|
151
|
+
* so a render that took minutes (a heavy shader, a busy GPU) looked hung: it gets a line at once and then one every `every` ms. `--quiet` gets none.
|
|
152
|
+
*/
|
|
153
|
+
export function makeProgress({ quiet = false, isTTY = false, write, now = Date.now, every = 15000 }) {
|
|
154
|
+
if (quiet) return () => {};
|
|
155
|
+
let last = -1, start = null, lastAt = 0;
|
|
156
|
+
return p => {
|
|
157
|
+
const pct = Math.min(100, Math.round(p));
|
|
158
|
+
if (isTTY) { if (pct !== last) { last = pct; write(`\rrendering ${pct}%`); } return; }
|
|
159
|
+
const t = now();
|
|
160
|
+
if (start === null) { start = t; lastAt = t; write(`rendering ${pct}%\n`); return; }
|
|
161
|
+
if (t - lastAt >= every) { lastAt = t; write(`rendering ${pct}% (${Math.round((t - start) / 1000)} s)\n`); }
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** The real bitrate of a written file, and a sentence when `--video-bitrate` asked for far more than the encoder spent (55M gave 17.8 Mbps on a mostly black picture: it is not a target). */
|
|
166
|
+
export function bitrateLine(bytes, seconds, asked) {
|
|
167
|
+
const mbps = seconds > 0 ? Math.round(bytes * 8 / seconds / 1e5) / 10 : null;
|
|
168
|
+
let note = '';
|
|
169
|
+
const m = typeof asked === 'string' ? asked.trim().match(/^(\d+(?:\.\d+)?)\s*([kKmM]?)$/) : null;
|
|
170
|
+
if (m && mbps !== null) {
|
|
171
|
+
const askedMbps = Number(m[1]) * ({ k: 1e3, m: 1e6 }[m[2].toLowerCase()] ?? 1) / 1e6;
|
|
172
|
+
if (askedMbps > 0 && mbps < askedMbps * 0.5) note = `asked ${asked}, got ${mbps} Mbps: the encoder used fewer bits than asked (a quiet or mostly black picture needs fewer; grain and fine detail need more). Read the real rate with probe, and ask for more if a platform needs it`;
|
|
173
|
+
}
|
|
174
|
+
return { mbps, note };
|
|
175
|
+
}
|
|
176
|
+
|
|
149
177
|
export async function runRender(opts, log = console.log, progress = () => {}) {
|
|
150
178
|
const pagePath = path.resolve(opts.page);
|
|
151
179
|
if (!fs.existsSync(pagePath)) throw new Error(`page not found: ${opts.page}`);
|
|
@@ -248,12 +276,7 @@ if (process.argv[1] && import.meta.url === pathToFileURL(fs.realpathSync(process
|
|
|
248
276
|
console.error(USAGE);
|
|
249
277
|
process.exit(2);
|
|
250
278
|
}
|
|
251
|
-
|
|
252
|
-
const progress = p => {
|
|
253
|
-
if (opts.quiet || !process.stderr.isTTY) return;
|
|
254
|
-
const pct = Math.min(100, Math.round(p));
|
|
255
|
-
if (pct !== last) { last = pct; process.stderr.write(`\rrendering ${pct}%`); }
|
|
256
|
-
};
|
|
279
|
+
const progress = makeProgress({ quiet: opts.quiet, isTTY: !!process.stderr.isTTY, write: s => process.stderr.write(s) });
|
|
257
280
|
const r = await runRender(opts, console.log, progress);
|
|
258
281
|
if (!opts.quiet && process.stderr.isTTY) process.stderr.write('\r \r');
|
|
259
282
|
for (const w of r.logs) console.error(`page: ${w.length > 300 ? w.slice(0, 297) + '…' : w}`);
|
|
@@ -261,7 +284,10 @@ if (process.argv[1] && import.meta.url === pathToFileURL(fs.realpathSync(process
|
|
|
261
284
|
console.error(`pixi-effects render failed: ${r.problems.join('; ') || 'no file written'}`);
|
|
262
285
|
process.exit(1);
|
|
263
286
|
}
|
|
264
|
-
|
|
287
|
+
const picture = !SOUND_ONLY.includes(opts.format) && opts.format !== 'pdf';
|
|
288
|
+
const rate = picture ? bitrateLine(r.bytes, fileSeconds(r, opts), opts.videoBitrate) : { mbps: null, note: '' };
|
|
289
|
+
console.log(`${shown(r.file)} ${kb(r.bytes)} · ${fileSeconds(r, opts)} s${rate.mbps !== null ? ` · ${rate.mbps} Mbps` : ''} · ${SOUND_ONLY.includes(opts.format) ? 'sound only' : `${r.width}×${r.height} @ ${r.frameRate} fps`} · ${opts.format} · rendered in ${r.seconds} s`);
|
|
290
|
+
if (rate.note) console.error(`note: ${rate.note}`);
|
|
265
291
|
process.exit(opts.strict && r.logs.length ? 1 : 0);
|
|
266
292
|
} catch (e) {
|
|
267
293
|
console.error(`pixi-effects render failed: ${e.message}`);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Container } from 'pixi.js';
|
|
2
|
-
import {
|
|
2
|
+
import { S as SequenceSpec, C as CompositionShape } from './types-DIh93lFb.cjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Resolves a routed keyframe path to a concrete tween target.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Container } from 'pixi.js';
|
|
2
|
-
import {
|
|
2
|
+
import { S as SequenceSpec, C as CompositionShape } from './types-DIh93lFb.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Resolves a routed keyframe path to a concrete tween target.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var chunk27PYJ4JY_cjs = require('./chunk-27PYJ4JY.cjs');
|
|
4
|
+
require('./chunk-7EI2N7WO.cjs');
|
|
5
|
+
require('./chunk-JN7JHNZR.cjs');
|
|
6
|
+
require('./chunk-3SAS3ZE5.cjs');
|
|
7
|
+
require('./chunk-TWZNOUM7.cjs');
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
Object.defineProperty(exports, "CompositionSequence", {
|
|
12
|
+
enumerable: true,
|
|
13
|
+
get: function () { return chunk27PYJ4JY_cjs.CompositionSequence; }
|
|
14
|
+
});
|
|
15
|
+
//# sourceMappingURL=Composition-C3UEVHQG.cjs.map
|
|
16
|
+
//# sourceMappingURL=Composition-C3UEVHQG.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-C3UEVHQG.cjs"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { CompositionSequence } from './chunk-26Z544SH.js';
|
|
2
|
+
import './chunk-LMWXR5OC.js';
|
|
3
|
+
import './chunk-VNP7QCJE.js';
|
|
4
|
+
import './chunk-32MMNDDN.js';
|
|
5
|
+
import './chunk-BXOCEZ4U.js';
|
|
6
|
+
//# sourceMappingURL=Composition-KRWH72MA.js.map
|
|
7
|
+
//# sourceMappingURL=Composition-KRWH72MA.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-KRWH72MA.js"}
|
package/dist/Controller.d.cts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { M as Movie } from './Movie-
|
|
1
|
+
import { M as Movie } from './Movie-CMq4KFmc.cjs';
|
|
2
2
|
import 'pixi.js';
|
|
3
3
|
import './soundEntry-DT13DNt8.cjs';
|
|
4
|
-
import './types-
|
|
4
|
+
import './types-DIh93lFb.cjs';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* The look of the bar. Each key sets a CSS custom property on the bar (so you can also set `--mc-accent` … in your own CSS, on the
|
package/dist/Controller.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { M as Movie } from './Movie-
|
|
1
|
+
import { M as Movie } from './Movie-CG6Jy1tx.js';
|
|
2
2
|
import 'pixi.js';
|
|
3
3
|
import './soundEntry-DT13DNt8.js';
|
|
4
|
-
import './types-
|
|
4
|
+
import './types-DIh93lFb.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* The look of the bar. Each key sets a CSS custom property on the bar (so you can also set `--mc-accent` … in your own CSS, on the
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Application } from 'pixi.js';
|
|
2
2
|
import { A as AudioReport, a as AudioInspectOptions } from './soundEntry-DT13DNt8.js';
|
|
3
|
-
import {
|
|
3
|
+
import { b as Stop, A as AssetSpec, a as CompositionSpec } from './types-DIh93lFb.js';
|
|
4
4
|
|
|
5
5
|
type EaseFn = (p: number) => number;
|
|
6
6
|
|
|
@@ -149,6 +149,8 @@ interface CanvasRow {
|
|
|
149
149
|
ok: boolean;
|
|
150
150
|
/** Why not, in a sentence (empty when ok). */
|
|
151
151
|
problem: string;
|
|
152
|
+
/** Which kind of layer drew it: a `canvas` layer or a `three` layer (both are held to "a function of the time"). */
|
|
153
|
+
kind: 'canvas' | 'three';
|
|
152
154
|
}
|
|
153
155
|
|
|
154
156
|
/** One kind of issue, with how many frames it was seen in. Issues that differ only by numbers are one kind. */
|
|
@@ -208,6 +210,9 @@ interface LayerInfo {
|
|
|
208
210
|
depthBlur?: number;
|
|
209
211
|
/** Light (threeD layers that receive lights): how lit the layer is this frame, 0..1+ (the brightness of the light falling on its middle; 1 = as with no light, 0 = black). Left out when there is no light. */
|
|
210
212
|
light?: number;
|
|
213
|
+
/** A `three` layer: the renderer it draws with (`'webgl'` / `'webgpu'`) and the backend under it (`'webgl2'`: three's WebGPURenderer found no WebGPU and used its own WebGL2 backend: the same picture). */
|
|
214
|
+
renderer?: 'webgl' | 'webgpu';
|
|
215
|
+
backend?: 'webgl' | 'webgpu' | 'webgl2';
|
|
211
216
|
}
|
|
212
217
|
interface InspectReport {
|
|
213
218
|
frame: number;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Application } from 'pixi.js';
|
|
2
2
|
import { A as AudioReport, a as AudioInspectOptions } from './soundEntry-DT13DNt8.cjs';
|
|
3
|
-
import {
|
|
3
|
+
import { b as Stop, A as AssetSpec, a as CompositionSpec } from './types-DIh93lFb.cjs';
|
|
4
4
|
|
|
5
5
|
type EaseFn = (p: number) => number;
|
|
6
6
|
|
|
@@ -149,6 +149,8 @@ interface CanvasRow {
|
|
|
149
149
|
ok: boolean;
|
|
150
150
|
/** Why not, in a sentence (empty when ok). */
|
|
151
151
|
problem: string;
|
|
152
|
+
/** Which kind of layer drew it: a `canvas` layer or a `three` layer (both are held to "a function of the time"). */
|
|
153
|
+
kind: 'canvas' | 'three';
|
|
152
154
|
}
|
|
153
155
|
|
|
154
156
|
/** One kind of issue, with how many frames it was seen in. Issues that differ only by numbers are one kind. */
|
|
@@ -208,6 +210,9 @@ interface LayerInfo {
|
|
|
208
210
|
depthBlur?: number;
|
|
209
211
|
/** Light (threeD layers that receive lights): how lit the layer is this frame, 0..1+ (the brightness of the light falling on its middle; 1 = as with no light, 0 = black). Left out when there is no light. */
|
|
210
212
|
light?: number;
|
|
213
|
+
/** A `three` layer: the renderer it draws with (`'webgl'` / `'webgpu'`) and the backend under it (`'webgl2'`: three's WebGPURenderer found no WebGPU and used its own WebGL2 backend: the same picture). */
|
|
214
|
+
renderer?: 'webgl' | 'webgpu';
|
|
215
|
+
backend?: 'webgl' | 'webgpu' | 'webgl2';
|
|
211
216
|
}
|
|
212
217
|
interface InspectReport {
|
|
213
218
|
frame: number;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
4
|
-
var
|
|
3
|
+
var chunk27PYJ4JY_cjs = require('./chunk-27PYJ4JY.cjs');
|
|
4
|
+
var chunk7EI2N7WO_cjs = require('./chunk-7EI2N7WO.cjs');
|
|
5
5
|
require('./chunk-JN7JHNZR.cjs');
|
|
6
6
|
require('./chunk-3SAS3ZE5.cjs');
|
|
7
7
|
require('./chunk-TWZNOUM7.cjs');
|
|
@@ -12,12 +12,12 @@ var strip = (props) => {
|
|
|
12
12
|
const { visibleChars: _gone, ...rest } = props;
|
|
13
13
|
return rest;
|
|
14
14
|
};
|
|
15
|
-
var SpansSequence = class extends
|
|
15
|
+
var SpansSequence = class extends chunk27PYJ4JY_cjs.CompositionSequence {
|
|
16
16
|
_meta;
|
|
17
17
|
_count = { value: 0, visibleChars: 0 };
|
|
18
18
|
constructor(spec, parent, root) {
|
|
19
19
|
super(spec, parent, root);
|
|
20
|
-
this._meta =
|
|
20
|
+
this._meta = chunk7EI2N7WO_cjs.spansMeta.get(spec);
|
|
21
21
|
}
|
|
22
22
|
/** `visibleChars` is the group's own counter, not a property to write on the container. */
|
|
23
23
|
displayProps() {
|
|
@@ -33,8 +33,8 @@ var SpansSequence = class extends chunkFE42PKHP_cjs.CompositionSequence {
|
|
|
33
33
|
if (!this._meta?.typed) return;
|
|
34
34
|
const scope = this.scope();
|
|
35
35
|
const first = this.spec.initial?.visibleChars;
|
|
36
|
-
this._count.visibleChars = first === void 0 ? 0 :
|
|
37
|
-
|
|
36
|
+
this._count.visibleChars = first === void 0 ? 0 : chunk7EI2N7WO_cjs.resolveNumber(first, scope);
|
|
37
|
+
chunk7EI2N7WO_cjs.bindCounterKeyframes(timeline, this._count, "visibleChars", () => this._distribute(), this.spec.keyframes ?? [], this.duration, offset + this.at, scope);
|
|
38
38
|
this._distribute();
|
|
39
39
|
}
|
|
40
40
|
/** Every piece shows its part of the first N characters of the whole text. */
|
|
@@ -43,7 +43,7 @@ var SpansSequence = class extends chunkFE42PKHP_cjs.CompositionSequence {
|
|
|
43
43
|
const total = Math.max(0, Math.floor(this._count.visibleChars));
|
|
44
44
|
let i = 0;
|
|
45
45
|
for (const child of this._children) {
|
|
46
|
-
if (!(child instanceof
|
|
46
|
+
if (!(child instanceof chunk7EI2N7WO_cjs.TextSequence)) continue;
|
|
47
47
|
const p = pieces[i++];
|
|
48
48
|
if (p) child.setVisibleChars(Math.min(p.length, Math.max(0, total - p.start)));
|
|
49
49
|
}
|
|
@@ -51,5 +51,5 @@ var SpansSequence = class extends chunkFE42PKHP_cjs.CompositionSequence {
|
|
|
51
51
|
};
|
|
52
52
|
|
|
53
53
|
exports.SpansSequence = SpansSequence;
|
|
54
|
-
//# sourceMappingURL=Spans-
|
|
55
|
-
//# sourceMappingURL=Spans-
|
|
54
|
+
//# sourceMappingURL=Spans-APNJHCYO.cjs.map
|
|
55
|
+
//# sourceMappingURL=Spans-APNJHCYO.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/sequences/Spans.ts"],"names":["CompositionSequence","spansMeta","resolveNumber","bindCounterKeyframes","TextSequence"],"mappings":";;;;;;;;;AAQA,IAAM,KAAA,GAAQ,CAAC,KAAA,KAAgD;AAC7D,EAAA,IAAI,CAAC,KAAA,IAAS,EAAE,cAAA,IAAkB,QAAQ,OAAO,KAAA;AACjD,EAAA,MAAM,EAAE,YAAA,EAAc,KAAA,EAAO,GAAG,MAAK,GAAI,KAAA;AACzC,EAAA,OAAO,IAAA;AACT,CAAA;AAEO,IAAM,aAAA,GAAN,cAA4BA,qCAAA,CAAoB;AAAA,EACpC,KAAA;AAAA,EACA,MAAA,GAAuB,EAAE,KAAA,EAAO,CAAA,EAAG,cAAc,CAAA,EAAE;AAAA,EAEpE,WAAA,CAAY,IAAA,EAAoB,MAAA,EAAiC,IAAA,EAAwB;AACvF,IAAA,KAAA,CAAM,IAAA,EAAM,QAAQ,IAAI,CAAA;AACxB,IAAA,IAAA,CAAK,KAAA,GAAQC,2BAAA,CAAU,GAAA,CAAI,IAAI,CAAA;AAAA,EACjC;AAAA;AAAA,EAGmB,YAAA,GAAyH;AAC1I,IAAA,MAAM,CAAA,GAAI,MAAM,YAAA,EAAa;AAC7B,IAAA,IAAI,CAAC,IAAA,CAAK,KAAA,EAAO,KAAA,EAAO,OAAO,CAAA;AAC/B,IAAA,OAAO;AAAA,MACL,OAAA,EAAS,KAAA,CAAM,CAAA,CAAE,OAA4B,CAAA;AAAA,MAC7C,SAAA,EAAW,EAAE,SAAA,EAAW,GAAA,CAAI,SAAO,EAAE,GAAG,EAAA,EAAI,GAAA,EAAK,KAAA,CAAM,EAAA,CAAG,GAAG,CAAA,EAAG,EAAA,EAAI,KAAA,CAAM,EAAA,CAAG,EAAE,CAAA,EAAG,MAAM,KAAA,CAAM,EAAA,CAAG,IAAI,CAAA,EAAE,CAAE;AAAA,KAC3G;AAAA,EACF;AAAA,EAES,YAAA,CAAa,QAAA,EAAoB,MAAA,GAAS,CAAA,EAAS;AAC1D,IAAA,KAAA,CAAM,YAAA,CAAa,UAAU,MAAM,CAAA;AACnC,IAAA,IAAI,CAAC,IAAA,CAAK,KAAA,EAAO,KAAA,EAAO;AACxB,IAAA,MAAM,KAAA,GAAQ,KAAK,KAAA,EAAM;AACzB,IAAA,MAAM,KAAA,GAAS,IAAA,CAAK,IAAA,CAAK,OAAA,EAAiD,YAAA;AAC1E,IAAA,IAAA,CAAK,OAAO,YAAA,GAAe,KAAA,KAAU,SAAY,CAAA,GAAIC,+BAAA,CAAc,OAAO,KAAK,CAAA;AAC/E,IAAAC,sCAAA,CAAqB,UAAU,IAAA,CAAK,MAAA,EAAQ,gBAAgB,MAAM,IAAA,CAAK,aAAY,EAAG,IAAA,CAAK,IAAA,CAAK,SAAA,IAAa,EAAC,EAAG,IAAA,CAAK,UAAW,MAAA,GAAS,IAAA,CAAK,IAAI,KAAK,CAAA;AACxJ,IAAA,IAAA,CAAK,WAAA,EAAY;AAAA,EACnB;AAAA;AAAA,EAGQ,WAAA,GAAoB;AAC1B,IAAA,MAAM,MAAA,GAAS,KAAK,KAAA,CAAO,MAAA;AAC3B,IAAA,MAAM,KAAA,GAAQ,KAAK,GAAA,CAAI,CAAA,EAAG,KAAK,KAAA,CAAM,IAAA,CAAK,MAAA,CAAO,YAAY,CAAC,CAAA;AAC9D,IAAA,IAAI,CAAA,GAAI,CAAA;AACR,IAAA,KAAA,MAAW,KAAA,IAAS,KAAK,SAAA,EAAW;AAClC,MAAA,IAAI,EAAE,iBAAiBC,8BAAA,CAAA,EAAe;AACtC,MAAA,MAAM,CAAA,GAAI,OAAO,CAAA,EAAG,CAAA;AACpB,MAAA,IAAI,CAAA,EAAG,KAAA,CAAM,eAAA,CAAgB,IAAA,CAAK,IAAI,CAAA,CAAE,MAAA,EAAQ,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,KAAA,GAAQ,CAAA,CAAE,KAAK,CAAC,CAAC,CAAA;AAAA,IAC/E;AAAA,EACF;AACF","file":"Spans-
|
|
1
|
+
{"version":3,"sources":["../src/sequences/Spans.ts"],"names":["CompositionSequence","spansMeta","resolveNumber","bindCounterKeyframes","TextSequence"],"mappings":";;;;;;;;;AAQA,IAAM,KAAA,GAAQ,CAAC,KAAA,KAAgD;AAC7D,EAAA,IAAI,CAAC,KAAA,IAAS,EAAE,cAAA,IAAkB,QAAQ,OAAO,KAAA;AACjD,EAAA,MAAM,EAAE,YAAA,EAAc,KAAA,EAAO,GAAG,MAAK,GAAI,KAAA;AACzC,EAAA,OAAO,IAAA;AACT,CAAA;AAEO,IAAM,aAAA,GAAN,cAA4BA,qCAAA,CAAoB;AAAA,EACpC,KAAA;AAAA,EACA,MAAA,GAAuB,EAAE,KAAA,EAAO,CAAA,EAAG,cAAc,CAAA,EAAE;AAAA,EAEpE,WAAA,CAAY,IAAA,EAAoB,MAAA,EAAiC,IAAA,EAAwB;AACvF,IAAA,KAAA,CAAM,IAAA,EAAM,QAAQ,IAAI,CAAA;AACxB,IAAA,IAAA,CAAK,KAAA,GAAQC,2BAAA,CAAU,GAAA,CAAI,IAAI,CAAA;AAAA,EACjC;AAAA;AAAA,EAGmB,YAAA,GAAyH;AAC1I,IAAA,MAAM,CAAA,GAAI,MAAM,YAAA,EAAa;AAC7B,IAAA,IAAI,CAAC,IAAA,CAAK,KAAA,EAAO,KAAA,EAAO,OAAO,CAAA;AAC/B,IAAA,OAAO;AAAA,MACL,OAAA,EAAS,KAAA,CAAM,CAAA,CAAE,OAA4B,CAAA;AAAA,MAC7C,SAAA,EAAW,EAAE,SAAA,EAAW,GAAA,CAAI,SAAO,EAAE,GAAG,EAAA,EAAI,GAAA,EAAK,KAAA,CAAM,EAAA,CAAG,GAAG,CAAA,EAAG,EAAA,EAAI,KAAA,CAAM,EAAA,CAAG,EAAE,CAAA,EAAG,MAAM,KAAA,CAAM,EAAA,CAAG,IAAI,CAAA,EAAE,CAAE;AAAA,KAC3G;AAAA,EACF;AAAA,EAES,YAAA,CAAa,QAAA,EAAoB,MAAA,GAAS,CAAA,EAAS;AAC1D,IAAA,KAAA,CAAM,YAAA,CAAa,UAAU,MAAM,CAAA;AACnC,IAAA,IAAI,CAAC,IAAA,CAAK,KAAA,EAAO,KAAA,EAAO;AACxB,IAAA,MAAM,KAAA,GAAQ,KAAK,KAAA,EAAM;AACzB,IAAA,MAAM,KAAA,GAAS,IAAA,CAAK,IAAA,CAAK,OAAA,EAAiD,YAAA;AAC1E,IAAA,IAAA,CAAK,OAAO,YAAA,GAAe,KAAA,KAAU,SAAY,CAAA,GAAIC,+BAAA,CAAc,OAAO,KAAK,CAAA;AAC/E,IAAAC,sCAAA,CAAqB,UAAU,IAAA,CAAK,MAAA,EAAQ,gBAAgB,MAAM,IAAA,CAAK,aAAY,EAAG,IAAA,CAAK,IAAA,CAAK,SAAA,IAAa,EAAC,EAAG,IAAA,CAAK,UAAW,MAAA,GAAS,IAAA,CAAK,IAAI,KAAK,CAAA;AACxJ,IAAA,IAAA,CAAK,WAAA,EAAY;AAAA,EACnB;AAAA;AAAA,EAGQ,WAAA,GAAoB;AAC1B,IAAA,MAAM,MAAA,GAAS,KAAK,KAAA,CAAO,MAAA;AAC3B,IAAA,MAAM,KAAA,GAAQ,KAAK,GAAA,CAAI,CAAA,EAAG,KAAK,KAAA,CAAM,IAAA,CAAK,MAAA,CAAO,YAAY,CAAC,CAAA;AAC9D,IAAA,IAAI,CAAA,GAAI,CAAA;AACR,IAAA,KAAA,MAAW,KAAA,IAAS,KAAK,SAAA,EAAW;AAClC,MAAA,IAAI,EAAE,iBAAiBC,8BAAA,CAAA,EAAe;AACtC,MAAA,MAAM,CAAA,GAAI,OAAO,CAAA,EAAG,CAAA;AACpB,MAAA,IAAI,CAAA,EAAG,KAAA,CAAM,eAAA,CAAgB,IAAA,CAAK,IAAI,CAAA,CAAE,MAAA,EAAQ,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,KAAA,GAAQ,CAAA,CAAE,KAAK,CAAC,CAAC,CAAA;AAAA,IAC/E;AAAA,EACF;AACF","file":"Spans-APNJHCYO.cjs","sourcesContent":["// The sequence behind a text layer with `spans`: a composition of ordinary text layers (see src/text/spans.ts) that also owns the typewriter\n// (`visibleChars`) of the whole text: it counts over every character of every piece, and tells each piece how many of its own to show.\nimport { CompositionSequence } from './Composition';\nimport { TextSequence, bindCounterKeyframes, resolveNumber, type CounterState } from './Text';\nimport { spansMeta, type SpansMeta } from '../text/spans';\nimport type { CompositionShape, SequenceSpec, Props } from '../types';\nimport type { Timeline } from '../core/timelineEngine';\n\nconst strip = (props: Props | undefined): Props | undefined => {\n if (!props || !('visibleChars' in props)) return props;\n const { visibleChars: _gone, ...rest } = props as Record<string, unknown>;\n return rest as unknown as Props;\n};\n\nexport class SpansSequence extends CompositionSequence {\n private readonly _meta: SpansMeta | undefined;\n private readonly _count: CounterState = { value: 0, visibleChars: 0 };\n\n constructor(spec: SequenceSpec, parent: CompositionShape | null, root: CompositionShape) {\n super(spec, parent, root);\n this._meta = spansMeta.get(spec);\n }\n\n /** `visibleChars` is the group's own counter, not a property to write on the container. */\n protected override displayProps(): { initial: CompositionSequence['spec']['initial']; keyframes: CompositionSequence['spec']['keyframes'] } {\n const p = super.displayProps();\n if (!this._meta?.typed) return p;\n return {\n initial: strip(p.initial as Props | undefined) as never,\n keyframes: p.keyframes?.map(kf => ({ ...kf, set: strip(kf.set), to: strip(kf.to), from: strip(kf.from) })),\n };\n }\n\n override bindTimeline(timeline: Timeline, offset = 0): void {\n super.bindTimeline(timeline, offset);\n if (!this._meta?.typed) return;\n const scope = this.scope();\n const first = (this.spec.initial as Record<string, unknown> | undefined)?.visibleChars;\n this._count.visibleChars = first === undefined ? 0 : resolveNumber(first, scope); // like a text: it starts at 0 once it is animated\n bindCounterKeyframes(timeline, this._count, 'visibleChars', () => this._distribute(), this.spec.keyframes ?? [], this.duration!, offset + this.at, scope);\n this._distribute();\n }\n\n /** Every piece shows its part of the first N characters of the whole text. */\n private _distribute(): void {\n const pieces = this._meta!.pieces;\n const total = Math.max(0, Math.floor(this._count.visibleChars));\n let i = 0;\n for (const child of this._children) {\n if (!(child instanceof TextSequence)) continue;\n const p = pieces[i++];\n if (p) child.setVisibleChars(Math.min(p.length, Math.max(0, total - p.start)));\n }\n }\n}\n"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { CompositionSequence } from './chunk-
|
|
2
|
-
import { spansMeta, resolveNumber, bindCounterKeyframes, TextSequence } from './chunk-
|
|
1
|
+
import { CompositionSequence } from './chunk-26Z544SH.js';
|
|
2
|
+
import { spansMeta, resolveNumber, bindCounterKeyframes, TextSequence } from './chunk-LMWXR5OC.js';
|
|
3
3
|
import './chunk-VNP7QCJE.js';
|
|
4
4
|
import './chunk-32MMNDDN.js';
|
|
5
5
|
import './chunk-BXOCEZ4U.js';
|
|
@@ -49,5 +49,5 @@ var SpansSequence = class extends CompositionSequence {
|
|
|
49
49
|
};
|
|
50
50
|
|
|
51
51
|
export { SpansSequence };
|
|
52
|
-
//# sourceMappingURL=Spans-
|
|
53
|
-
//# sourceMappingURL=Spans-
|
|
52
|
+
//# sourceMappingURL=Spans-TWO4VN5H.js.map
|
|
53
|
+
//# sourceMappingURL=Spans-TWO4VN5H.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/sequences/Spans.ts"],"names":[],"mappings":";;;;;;;AAQA,IAAM,KAAA,GAAQ,CAAC,KAAA,KAAgD;AAC7D,EAAA,IAAI,CAAC,KAAA,IAAS,EAAE,cAAA,IAAkB,QAAQ,OAAO,KAAA;AACjD,EAAA,MAAM,EAAE,YAAA,EAAc,KAAA,EAAO,GAAG,MAAK,GAAI,KAAA;AACzC,EAAA,OAAO,IAAA;AACT,CAAA;AAEO,IAAM,aAAA,GAAN,cAA4B,mBAAA,CAAoB;AAAA,EACpC,KAAA;AAAA,EACA,MAAA,GAAuB,EAAE,KAAA,EAAO,CAAA,EAAG,cAAc,CAAA,EAAE;AAAA,EAEpE,WAAA,CAAY,IAAA,EAAoB,MAAA,EAAiC,IAAA,EAAwB;AACvF,IAAA,KAAA,CAAM,IAAA,EAAM,QAAQ,IAAI,CAAA;AACxB,IAAA,IAAA,CAAK,KAAA,GAAQ,SAAA,CAAU,GAAA,CAAI,IAAI,CAAA;AAAA,EACjC;AAAA;AAAA,EAGmB,YAAA,GAAyH;AAC1I,IAAA,MAAM,CAAA,GAAI,MAAM,YAAA,EAAa;AAC7B,IAAA,IAAI,CAAC,IAAA,CAAK,KAAA,EAAO,KAAA,EAAO,OAAO,CAAA;AAC/B,IAAA,OAAO;AAAA,MACL,OAAA,EAAS,KAAA,CAAM,CAAA,CAAE,OAA4B,CAAA;AAAA,MAC7C,SAAA,EAAW,EAAE,SAAA,EAAW,GAAA,CAAI,SAAO,EAAE,GAAG,EAAA,EAAI,GAAA,EAAK,KAAA,CAAM,EAAA,CAAG,GAAG,CAAA,EAAG,EAAA,EAAI,KAAA,CAAM,EAAA,CAAG,EAAE,CAAA,EAAG,MAAM,KAAA,CAAM,EAAA,CAAG,IAAI,CAAA,EAAE,CAAE;AAAA,KAC3G;AAAA,EACF;AAAA,EAES,YAAA,CAAa,QAAA,EAAoB,MAAA,GAAS,CAAA,EAAS;AAC1D,IAAA,KAAA,CAAM,YAAA,CAAa,UAAU,MAAM,CAAA;AACnC,IAAA,IAAI,CAAC,IAAA,CAAK,KAAA,EAAO,KAAA,EAAO;AACxB,IAAA,MAAM,KAAA,GAAQ,KAAK,KAAA,EAAM;AACzB,IAAA,MAAM,KAAA,GAAS,IAAA,CAAK,IAAA,CAAK,OAAA,EAAiD,YAAA;AAC1E,IAAA,IAAA,CAAK,OAAO,YAAA,GAAe,KAAA,KAAU,SAAY,CAAA,GAAI,aAAA,CAAc,OAAO,KAAK,CAAA;AAC/E,IAAA,oBAAA,CAAqB,UAAU,IAAA,CAAK,MAAA,EAAQ,gBAAgB,MAAM,IAAA,CAAK,aAAY,EAAG,IAAA,CAAK,IAAA,CAAK,SAAA,IAAa,EAAC,EAAG,IAAA,CAAK,UAAW,MAAA,GAAS,IAAA,CAAK,IAAI,KAAK,CAAA;AACxJ,IAAA,IAAA,CAAK,WAAA,EAAY;AAAA,EACnB;AAAA;AAAA,EAGQ,WAAA,GAAoB;AAC1B,IAAA,MAAM,MAAA,GAAS,KAAK,KAAA,CAAO,MAAA;AAC3B,IAAA,MAAM,KAAA,GAAQ,KAAK,GAAA,CAAI,CAAA,EAAG,KAAK,KAAA,CAAM,IAAA,CAAK,MAAA,CAAO,YAAY,CAAC,CAAA;AAC9D,IAAA,IAAI,CAAA,GAAI,CAAA;AACR,IAAA,KAAA,MAAW,KAAA,IAAS,KAAK,SAAA,EAAW;AAClC,MAAA,IAAI,EAAE,iBAAiB,YAAA,CAAA,EAAe;AACtC,MAAA,MAAM,CAAA,GAAI,OAAO,CAAA,EAAG,CAAA;AACpB,MAAA,IAAI,CAAA,EAAG,KAAA,CAAM,eAAA,CAAgB,IAAA,CAAK,IAAI,CAAA,CAAE,MAAA,EAAQ,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,KAAA,GAAQ,CAAA,CAAE,KAAK,CAAC,CAAC,CAAA;AAAA,IAC/E;AAAA,EACF;AACF","file":"Spans-
|
|
1
|
+
{"version":3,"sources":["../src/sequences/Spans.ts"],"names":[],"mappings":";;;;;;;AAQA,IAAM,KAAA,GAAQ,CAAC,KAAA,KAAgD;AAC7D,EAAA,IAAI,CAAC,KAAA,IAAS,EAAE,cAAA,IAAkB,QAAQ,OAAO,KAAA;AACjD,EAAA,MAAM,EAAE,YAAA,EAAc,KAAA,EAAO,GAAG,MAAK,GAAI,KAAA;AACzC,EAAA,OAAO,IAAA;AACT,CAAA;AAEO,IAAM,aAAA,GAAN,cAA4B,mBAAA,CAAoB;AAAA,EACpC,KAAA;AAAA,EACA,MAAA,GAAuB,EAAE,KAAA,EAAO,CAAA,EAAG,cAAc,CAAA,EAAE;AAAA,EAEpE,WAAA,CAAY,IAAA,EAAoB,MAAA,EAAiC,IAAA,EAAwB;AACvF,IAAA,KAAA,CAAM,IAAA,EAAM,QAAQ,IAAI,CAAA;AACxB,IAAA,IAAA,CAAK,KAAA,GAAQ,SAAA,CAAU,GAAA,CAAI,IAAI,CAAA;AAAA,EACjC;AAAA;AAAA,EAGmB,YAAA,GAAyH;AAC1I,IAAA,MAAM,CAAA,GAAI,MAAM,YAAA,EAAa;AAC7B,IAAA,IAAI,CAAC,IAAA,CAAK,KAAA,EAAO,KAAA,EAAO,OAAO,CAAA;AAC/B,IAAA,OAAO;AAAA,MACL,OAAA,EAAS,KAAA,CAAM,CAAA,CAAE,OAA4B,CAAA;AAAA,MAC7C,SAAA,EAAW,EAAE,SAAA,EAAW,GAAA,CAAI,SAAO,EAAE,GAAG,EAAA,EAAI,GAAA,EAAK,KAAA,CAAM,EAAA,CAAG,GAAG,CAAA,EAAG,EAAA,EAAI,KAAA,CAAM,EAAA,CAAG,EAAE,CAAA,EAAG,MAAM,KAAA,CAAM,EAAA,CAAG,IAAI,CAAA,EAAE,CAAE;AAAA,KAC3G;AAAA,EACF;AAAA,EAES,YAAA,CAAa,QAAA,EAAoB,MAAA,GAAS,CAAA,EAAS;AAC1D,IAAA,KAAA,CAAM,YAAA,CAAa,UAAU,MAAM,CAAA;AACnC,IAAA,IAAI,CAAC,IAAA,CAAK,KAAA,EAAO,KAAA,EAAO;AACxB,IAAA,MAAM,KAAA,GAAQ,KAAK,KAAA,EAAM;AACzB,IAAA,MAAM,KAAA,GAAS,IAAA,CAAK,IAAA,CAAK,OAAA,EAAiD,YAAA;AAC1E,IAAA,IAAA,CAAK,OAAO,YAAA,GAAe,KAAA,KAAU,SAAY,CAAA,GAAI,aAAA,CAAc,OAAO,KAAK,CAAA;AAC/E,IAAA,oBAAA,CAAqB,UAAU,IAAA,CAAK,MAAA,EAAQ,gBAAgB,MAAM,IAAA,CAAK,aAAY,EAAG,IAAA,CAAK,IAAA,CAAK,SAAA,IAAa,EAAC,EAAG,IAAA,CAAK,UAAW,MAAA,GAAS,IAAA,CAAK,IAAI,KAAK,CAAA;AACxJ,IAAA,IAAA,CAAK,WAAA,EAAY;AAAA,EACnB;AAAA;AAAA,EAGQ,WAAA,GAAoB;AAC1B,IAAA,MAAM,MAAA,GAAS,KAAK,KAAA,CAAO,MAAA;AAC3B,IAAA,MAAM,KAAA,GAAQ,KAAK,GAAA,CAAI,CAAA,EAAG,KAAK,KAAA,CAAM,IAAA,CAAK,MAAA,CAAO,YAAY,CAAC,CAAA;AAC9D,IAAA,IAAI,CAAA,GAAI,CAAA;AACR,IAAA,KAAA,MAAW,KAAA,IAAS,KAAK,SAAA,EAAW;AAClC,MAAA,IAAI,EAAE,iBAAiB,YAAA,CAAA,EAAe;AACtC,MAAA,MAAM,CAAA,GAAI,OAAO,CAAA,EAAG,CAAA;AACpB,MAAA,IAAI,CAAA,EAAG,KAAA,CAAM,eAAA,CAAgB,IAAA,CAAK,IAAI,CAAA,CAAE,MAAA,EAAQ,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,KAAA,GAAQ,CAAA,CAAE,KAAK,CAAC,CAAC,CAAA;AAAA,IAC/E;AAAA,EACF;AACF","file":"Spans-TWO4VN5H.js","sourcesContent":["// The sequence behind a text layer with `spans`: a composition of ordinary text layers (see src/text/spans.ts) that also owns the typewriter\n// (`visibleChars`) of the whole text: it counts over every character of every piece, and tells each piece how many of its own to show.\nimport { CompositionSequence } from './Composition';\nimport { TextSequence, bindCounterKeyframes, resolveNumber, type CounterState } from './Text';\nimport { spansMeta, type SpansMeta } from '../text/spans';\nimport type { CompositionShape, SequenceSpec, Props } from '../types';\nimport type { Timeline } from '../core/timelineEngine';\n\nconst strip = (props: Props | undefined): Props | undefined => {\n if (!props || !('visibleChars' in props)) return props;\n const { visibleChars: _gone, ...rest } = props as Record<string, unknown>;\n return rest as unknown as Props;\n};\n\nexport class SpansSequence extends CompositionSequence {\n private readonly _meta: SpansMeta | undefined;\n private readonly _count: CounterState = { value: 0, visibleChars: 0 };\n\n constructor(spec: SequenceSpec, parent: CompositionShape | null, root: CompositionShape) {\n super(spec, parent, root);\n this._meta = spansMeta.get(spec);\n }\n\n /** `visibleChars` is the group's own counter, not a property to write on the container. */\n protected override displayProps(): { initial: CompositionSequence['spec']['initial']; keyframes: CompositionSequence['spec']['keyframes'] } {\n const p = super.displayProps();\n if (!this._meta?.typed) return p;\n return {\n initial: strip(p.initial as Props | undefined) as never,\n keyframes: p.keyframes?.map(kf => ({ ...kf, set: strip(kf.set), to: strip(kf.to), from: strip(kf.from) })),\n };\n }\n\n override bindTimeline(timeline: Timeline, offset = 0): void {\n super.bindTimeline(timeline, offset);\n if (!this._meta?.typed) return;\n const scope = this.scope();\n const first = (this.spec.initial as Record<string, unknown> | undefined)?.visibleChars;\n this._count.visibleChars = first === undefined ? 0 : resolveNumber(first, scope); // like a text: it starts at 0 once it is animated\n bindCounterKeyframes(timeline, this._count, 'visibleChars', () => this._distribute(), this.spec.keyframes ?? [], this.duration!, offset + this.at, scope);\n this._distribute();\n }\n\n /** Every piece shows its part of the first N characters of the whole text. */\n private _distribute(): void {\n const pieces = this._meta!.pieces;\n const total = Math.max(0, Math.floor(this._count.visibleChars));\n let i = 0;\n for (const child of this._children) {\n if (!(child instanceof TextSequence)) continue;\n const p = pieces[i++];\n if (p) child.setVisibleChars(Math.min(p.length, Math.max(0, total - p.start)));\n }\n }\n}\n"]}
|