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/dist/index.cjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
4
|
-
var
|
|
3
|
+
var chunkJW67MDHQ_cjs = require('./chunk-JW67MDHQ.cjs');
|
|
4
|
+
var chunkANO5R2E6_cjs = require('./chunk-ANO5R2E6.cjs');
|
|
5
5
|
var chunkUQBGW5DO_cjs = require('./chunk-UQBGW5DO.cjs');
|
|
6
6
|
var PIXI = require('pixi.js');
|
|
7
7
|
var gsap = require('gsap');
|
|
@@ -203,6 +203,118 @@ function spectralCentroid(samples, sampleRate, from = 0, to = samples.length) {
|
|
|
203
203
|
return den > 1e-9 ? Math.round(num3 / den / 10) * 10 : 0;
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
+
// src/audio/loudness.ts
|
|
207
|
+
var LOUDNESS_NOTES = { quietBelow: -24, loudAbove: -9, headroomDb: -1 };
|
|
208
|
+
function kWeighting(fs) {
|
|
209
|
+
const f0 = 1681.974450955533, G = 3.999843853973347, Q = 0.7071752369554196;
|
|
210
|
+
const K = Math.tan(Math.PI * f0 / fs), Vh = 10 ** (G / 20), Vb = Vh ** 0.4996667741545416;
|
|
211
|
+
const a0 = 1 + K / Q + K * K;
|
|
212
|
+
const shelf = {
|
|
213
|
+
b: [(Vh + Vb * K / Q + K * K) / a0, 2 * (K * K - Vh) / a0, (Vh - Vb * K / Q + K * K) / a0],
|
|
214
|
+
a: [1, 2 * (K * K - 1) / a0, (1 - K / Q + K * K) / a0]
|
|
215
|
+
};
|
|
216
|
+
const f1 = 38.13547087602444, Q1 = 0.5003270373238773, K1 = Math.tan(Math.PI * f1 / fs), a1 = 1 + K1 / Q1 + K1 * K1;
|
|
217
|
+
const high = { b: [1, -2, 1], a: [1, 2 * (K1 * K1 - 1) / a1, (1 - K1 / Q1 + K1 * K1) / a1] };
|
|
218
|
+
return [shelf, high];
|
|
219
|
+
}
|
|
220
|
+
function filter(x, stages) {
|
|
221
|
+
let cur = x;
|
|
222
|
+
for (const { b, a } of stages) {
|
|
223
|
+
const out = new Float32Array(cur.length);
|
|
224
|
+
let x1 = 0, x2 = 0, y1 = 0, y2 = 0;
|
|
225
|
+
for (let i = 0; i < cur.length; i++) {
|
|
226
|
+
const xi = cur[i], y = b[0] * xi + b[1] * x1 + b[2] * x2 - a[1] * y1 - a[2] * y2;
|
|
227
|
+
x2 = x1;
|
|
228
|
+
x1 = xi;
|
|
229
|
+
y2 = y1;
|
|
230
|
+
y1 = y;
|
|
231
|
+
out[i] = y;
|
|
232
|
+
}
|
|
233
|
+
cur = out;
|
|
234
|
+
}
|
|
235
|
+
return cur;
|
|
236
|
+
}
|
|
237
|
+
var toDb = (x) => Math.round(200 * Math.log10(Math.max(x, 1e-6))) / 10;
|
|
238
|
+
var TP_HALF = 12;
|
|
239
|
+
var TP_BLOCK = 32;
|
|
240
|
+
var TP_KERNELS = [0.25, 0.5, 0.75].map((p) => Float64Array.from({ length: 2 * TP_HALF }, (_, j) => {
|
|
241
|
+
const k = j - TP_HALF + 1, x = k - p, s = x === 0 ? 1 : Math.sin(Math.PI * x) / (Math.PI * x);
|
|
242
|
+
return s * (0.5 + 0.5 * Math.cos(Math.PI * x / (TP_HALF + 1)));
|
|
243
|
+
}));
|
|
244
|
+
var TP_GAIN = Math.max(...TP_KERNELS.map((k) => k.reduce((a, v) => a + Math.abs(v), 0)));
|
|
245
|
+
function truePeak(chans) {
|
|
246
|
+
let peak = 0;
|
|
247
|
+
for (const c of chans) {
|
|
248
|
+
const n = c.length;
|
|
249
|
+
const blocks = Math.ceil(n / TP_BLOCK), blockMax = new Float32Array(blocks + 2);
|
|
250
|
+
for (let i = 0; i < n; i++) {
|
|
251
|
+
const a = Math.abs(c[i]), b = (i / TP_BLOCK | 0) + 1;
|
|
252
|
+
if (a > blockMax[b]) blockMax[b] = a;
|
|
253
|
+
if (a > peak) peak = a;
|
|
254
|
+
}
|
|
255
|
+
const padded = new Float32Array(n + 2 * TP_HALF);
|
|
256
|
+
padded.set(c, TP_HALF);
|
|
257
|
+
for (let i = 0; i < n; i++) {
|
|
258
|
+
const b = (i / TP_BLOCK | 0) + 1;
|
|
259
|
+
const local = Math.max(blockMax[b - 1], blockMax[b], blockMax[b + 1]);
|
|
260
|
+
if (local * TP_GAIN <= peak) continue;
|
|
261
|
+
for (let pi = 0; pi < 3; pi++) {
|
|
262
|
+
const k = TP_KERNELS[pi];
|
|
263
|
+
let v = 0;
|
|
264
|
+
for (let j = 0; j < 2 * TP_HALF; j++) v += padded[i + 1 + j] * k[j];
|
|
265
|
+
const a = Math.abs(v);
|
|
266
|
+
if (a > peak) peak = a;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return peak;
|
|
271
|
+
}
|
|
272
|
+
function measureLoudness(channels, sampleRate, opts = {}) {
|
|
273
|
+
let samplePeak = 0, clipped = 0;
|
|
274
|
+
for (const c of channels) for (let i = 0; i < c.length; i++) {
|
|
275
|
+
const a = Math.abs(c[i]);
|
|
276
|
+
if (a > samplePeak) samplePeak = a;
|
|
277
|
+
if (a >= 1) clipped++;
|
|
278
|
+
}
|
|
279
|
+
const stages = kWeighting(sampleRate);
|
|
280
|
+
const weighted = channels.map((c) => filter(c, stages));
|
|
281
|
+
const block = Math.round(0.4 * sampleRate), hop = Math.round(0.1 * sampleRate);
|
|
282
|
+
const energies = [];
|
|
283
|
+
for (let s = 0; s + block <= (weighted[0]?.length ?? 0); s += hop) {
|
|
284
|
+
let sum = 0;
|
|
285
|
+
for (const w of weighted) {
|
|
286
|
+
let z = 0;
|
|
287
|
+
for (let i = s; i < s + block; i++) z += w[i] * w[i];
|
|
288
|
+
sum += z / block;
|
|
289
|
+
}
|
|
290
|
+
energies.push(sum);
|
|
291
|
+
}
|
|
292
|
+
const lufs = (e) => -0.691 + 10 * Math.log10(e);
|
|
293
|
+
const absolute = energies.filter((e) => e > 0 && lufs(e) > -70);
|
|
294
|
+
let integrated = null;
|
|
295
|
+
if (absolute.length) {
|
|
296
|
+
const relative = lufs(absolute.reduce((a, b) => a + b, 0) / absolute.length) - 10;
|
|
297
|
+
const gated = absolute.filter((e) => lufs(e) > relative);
|
|
298
|
+
if (gated.length) integrated = Math.round(10 * lufs(gated.reduce((a, b) => a + b, 0) / gated.length)) / 10;
|
|
299
|
+
}
|
|
300
|
+
return { integratedLufs: integrated, truePeakDb: opts.truePeak === false ? null : toDb(truePeak(channels)), samplePeakDb: toDb(samplePeak), clippedSamples: clipped };
|
|
301
|
+
}
|
|
302
|
+
function findSilences(channels, sampleRate, opts = {}) {
|
|
303
|
+
const min = Math.round((opts.minLength ?? 1) * sampleRate), limit = 10 ** ((opts.thresholdDb ?? -60) / 20);
|
|
304
|
+
const n = channels[0]?.length ?? 0, out = [];
|
|
305
|
+
let start = -1;
|
|
306
|
+
const quiet = (i) => channels.every((c) => Math.abs(c[i]) < limit);
|
|
307
|
+
for (let i = 0; i <= n; i++) {
|
|
308
|
+
const q = i < n && quiet(i);
|
|
309
|
+
if (q && start < 0) start = i;
|
|
310
|
+
if (!q && start >= 0) {
|
|
311
|
+
if (i - start >= min) out.push({ from: Math.round(start / sampleRate * 1e3) / 1e3, to: Math.round(i / sampleRate * 1e3) / 1e3 });
|
|
312
|
+
start = -1;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return out;
|
|
316
|
+
}
|
|
317
|
+
|
|
206
318
|
// src/core/inspectAudio.ts
|
|
207
319
|
var INAUDIBLE_DB = -50;
|
|
208
320
|
var db = (x) => Math.round(10 * Math.max(-120, 20 * Math.log10(x))) / 10;
|
|
@@ -228,7 +340,19 @@ function measure(chans, sampleRate) {
|
|
|
228
340
|
}
|
|
229
341
|
function analyzeAudio(mix, sources, stats, movieDuration, opts = {}) {
|
|
230
342
|
if (!mix) {
|
|
231
|
-
return {
|
|
343
|
+
return {
|
|
344
|
+
duration: movieDuration,
|
|
345
|
+
sampleRate: 0,
|
|
346
|
+
peakDb: -120,
|
|
347
|
+
peakAt: 0,
|
|
348
|
+
sources: [],
|
|
349
|
+
windows: [],
|
|
350
|
+
loudness: { integratedLufs: null, truePeakDb: -120, clippedSamples: 0, silences: [] },
|
|
351
|
+
scenes: [],
|
|
352
|
+
cues: [],
|
|
353
|
+
notes: [],
|
|
354
|
+
issues: ['no audio: the movie has no audio layers (sound effects need no files: { type: "audio", sfx: "pop", at: 1 })']
|
|
355
|
+
};
|
|
232
356
|
}
|
|
233
357
|
const sr = mix.sampleRate;
|
|
234
358
|
const chans = Array.from({ length: mix.numberOfChannels }, (_, c) => mix.getChannelData(c));
|
|
@@ -286,6 +410,24 @@ function analyzeAudio(mix, sources, stats, movieDuration, opts = {}) {
|
|
|
286
410
|
}
|
|
287
411
|
return { layer, source, start: s.start, end: s.end, peakDb, sound };
|
|
288
412
|
});
|
|
413
|
+
const whole = measureLoudness(chans, sr);
|
|
414
|
+
const silences = findSilences(chans, sr, { minLength: 1 });
|
|
415
|
+
if (whole.clippedSamples > 0) issues.push(`${whole.clippedSamples} sample(s) of the mix are at full scale (clipped) \u2014 lower the volume of the layers that play loudest`);
|
|
416
|
+
if (whole.truePeakDb > 0) issues.push(`the mix's true peak is ${whole.truePeakDb} dBTP: it will distort when the video is encoded \u2014 lower the volume`);
|
|
417
|
+
const notes = [];
|
|
418
|
+
if (whole.integratedLufs !== null) {
|
|
419
|
+
if (whole.integratedLufs < LOUDNESS_NOTES.quietBelow) notes.push(`the mix is quiet for web video: ${whole.integratedLufs} LUFS (typical \u221214 to \u221216); raise the volume if that is not on purpose`);
|
|
420
|
+
else if (whole.integratedLufs > LOUDNESS_NOTES.loudAbove) notes.push(`the mix is loud: ${whole.integratedLufs} LUFS (typical web video \u221214 to \u221216)`);
|
|
421
|
+
}
|
|
422
|
+
if (whole.truePeakDb > LOUDNESS_NOTES.headroomDb && whole.truePeakDb <= 0) notes.push(`the true peak is ${whole.truePeakDb} dBTP: little headroom left for encoding (keep it below ${LOUDNESS_NOTES.headroomDb} dBTP)`);
|
|
423
|
+
for (const q of silences.slice(0, 3)) notes.push(`the mix is silent from ${q.from} s to ${q.to} s (${Math.round((q.to - q.from) * 10) / 10} s)`);
|
|
424
|
+
if (silences.length > 3) notes.push(`and ${silences.length - 3} more silent stretch(es) (loudness.silences lists them all)`);
|
|
425
|
+
const sceneList = (opts.scenes && opts.scenes.length ? opts.scenes : [{ name: "movie", start: 0, end: movieDuration }]).map((sc) => {
|
|
426
|
+
const a = Math.max(0, Math.round(sc.start * sr)), b = Math.min(mix.length, Math.round(sc.end * sr));
|
|
427
|
+
const part = chans.map((c) => c.subarray(a, Math.max(a, b)));
|
|
428
|
+
const m = measureLoudness(part, sr, { truePeak: false });
|
|
429
|
+
return { name: sc.name, from: sc.start, to: sc.end, lufs: m.integratedLufs, peakDb: m.samplePeakDb, silent: m.samplePeakDb < -60 };
|
|
430
|
+
});
|
|
289
431
|
return {
|
|
290
432
|
duration: movieDuration,
|
|
291
433
|
sampleRate: sr,
|
|
@@ -293,8 +435,158 @@ function analyzeAudio(mix, sources, stats, movieDuration, opts = {}) {
|
|
|
293
435
|
peakAt: stats?.peakAt ?? 0,
|
|
294
436
|
sources: list,
|
|
295
437
|
windows: windows2,
|
|
296
|
-
issues
|
|
438
|
+
issues,
|
|
439
|
+
loudness: { integratedLufs: whole.integratedLufs, truePeakDb: whole.truePeakDb ?? -120, clippedSamples: whole.clippedSamples, silences },
|
|
440
|
+
scenes: sceneList,
|
|
441
|
+
cues: list.map((s) => ({ t: s.start, name: `${s.layer} (${s.source})` })),
|
|
442
|
+
notes
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// src/text/fonts.ts
|
|
447
|
+
var GENERIC = /* @__PURE__ */ new Set(["serif", "sans-serif", "monospace", "cursive", "fantasy", "system-ui", "ui-serif", "ui-sans-serif", "ui-monospace", "ui-rounded", "emoji", "math", "fangsong", "-apple-system", "blinkmacsystemfont"]);
|
|
448
|
+
function fontAvailable(family2) {
|
|
449
|
+
const f = family2.trim().replace(/^["']|["']$/g, "");
|
|
450
|
+
if (!f || GENERIC.has(f.toLowerCase())) return true;
|
|
451
|
+
const g = document.createElement("canvas").getContext("2d");
|
|
452
|
+
if (!g) return true;
|
|
453
|
+
const sample = "mmmmmmmmmmlliWWWW 0123 \u3042\u3044\u3046";
|
|
454
|
+
const width = (stack) => {
|
|
455
|
+
g.font = `72px ${stack}`;
|
|
456
|
+
return g.measureText(sample).width;
|
|
297
457
|
};
|
|
458
|
+
const q = JSON.stringify(f);
|
|
459
|
+
return width(`${q}, monospace`) !== width("monospace") || width(`${q}, serif`) !== width("serif");
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// src/core/inspectFonts.ts
|
|
463
|
+
function inspectFonts(root) {
|
|
464
|
+
const failed = /* @__PURE__ */ new Set();
|
|
465
|
+
if (typeof document !== "undefined" && document.fonts) document.fonts.forEach((face) => {
|
|
466
|
+
if (face.status === "error") failed.add(face.family.replace(/^["']|["']$/g, ""));
|
|
467
|
+
});
|
|
468
|
+
const missing = [];
|
|
469
|
+
const used = /* @__PURE__ */ new Set();
|
|
470
|
+
const walk2 = (seq, index) => {
|
|
471
|
+
const spec = seq.spec;
|
|
472
|
+
if (spec.type === "text") {
|
|
473
|
+
const raw = spec.style?.fontFamily;
|
|
474
|
+
const family2 = Array.isArray(raw) ? raw.join(", ") : typeof raw === "string" ? raw : "";
|
|
475
|
+
const list = family2.split(",").map((f) => f.trim()).filter(Boolean);
|
|
476
|
+
for (const f of list) used.add(f.replace(/^["']|["']$/g, "").toLowerCase());
|
|
477
|
+
if (list.length && !list.some(fontAvailable) && !list.some((f) => failed.has(f.replace(/^["']|["']$/g, "")))) missing.push({ layer: spec.name ?? `text#${index}`, family: family2 });
|
|
478
|
+
}
|
|
479
|
+
if (seq.maskSequence) walk2(seq.maskSequence, index);
|
|
480
|
+
(seq._children ?? []).forEach((child, i) => walk2(child, i));
|
|
481
|
+
};
|
|
482
|
+
walk2(root, 0);
|
|
483
|
+
const all = [...failed];
|
|
484
|
+
return { missing, failed: all.filter((f) => used.has(f.toLowerCase())), failedUnused: all.filter((f) => !used.has(f.toLowerCase())) };
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// src/core/scenes.ts
|
|
488
|
+
function namedScenes(rows) {
|
|
489
|
+
return rows.filter((r) => r.depth === 0 && r.type === "composition" && !/[#×]/.test(r.name) && r.end - r.start >= 1).map((r) => ({ name: r.name, start: r.start, end: r.end }));
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// src/core/review.ts
|
|
493
|
+
function sampleFrames({ totalFrames, frameRate, scenes = [], step = 0.25, cap = 240 }) {
|
|
494
|
+
const last = totalFrames;
|
|
495
|
+
const clamp2 = (f) => Math.max(0, Math.min(last, Math.round(f)));
|
|
496
|
+
const edges = /* @__PURE__ */ new Set([0, last]);
|
|
497
|
+
for (const s of scenes) {
|
|
498
|
+
edges.add(clamp2(s.start * frameRate));
|
|
499
|
+
edges.add(clamp2(s.end * frameRate - 1));
|
|
500
|
+
}
|
|
501
|
+
const room = Math.max(0, cap - edges.size);
|
|
502
|
+
const seconds = totalFrames / frameRate;
|
|
503
|
+
const wanted = Math.floor(seconds / step) + 1;
|
|
504
|
+
const stride = wanted <= room ? step : seconds / Math.max(1, room);
|
|
505
|
+
const frames = new Set(edges);
|
|
506
|
+
for (let t = stride; frames.size < cap && t < seconds; t += stride) frames.add(clamp2(t * frameRate));
|
|
507
|
+
return [...frames].sort((a, b) => a - b);
|
|
508
|
+
}
|
|
509
|
+
var AT_HELP = "use seconds (3.5), a percentage (50%), a frame (f120) or a layer's start / mid / end (name@end)";
|
|
510
|
+
function resolveAtList(list, ctx) {
|
|
511
|
+
const { frameRate: fps, totalFrames, duration, rows } = ctx;
|
|
512
|
+
const out = [], used = /* @__PURE__ */ new Set();
|
|
513
|
+
const named = rows.flatMap((r) => r.parts && r.partNames ? r.partNames.map((name, i) => ({ name, start: r.parts[i].start, end: r.parts[i].end })) : [r]);
|
|
514
|
+
for (const raw of String(list).split(",").map((s) => s.trim()).filter(Boolean)) {
|
|
515
|
+
let frame, label, m;
|
|
516
|
+
if (m = raw.match(/^(.+)@(start|mid|end)$/)) {
|
|
517
|
+
const row = named.find((r) => r.name === m[1]);
|
|
518
|
+
if (!row) throw new Error(`--at "${raw}": no layer named "${m[1]}" (names: ${named.slice(0, 40).map((r) => r.name).join(", ")}${named.length > 40 ? ", \u2026" : ""})`);
|
|
519
|
+
const t = m[2] === "start" ? row.start : m[2] === "mid" ? (row.start + row.end) / 2 : row.end - 1 / fps;
|
|
520
|
+
frame = Math.round(t * fps);
|
|
521
|
+
label = `${m[1]}-${m[2]}`;
|
|
522
|
+
} else if (m = raw.match(/^(\d+(?:\.\d+)?)%$/)) {
|
|
523
|
+
frame = Math.round(Number(m[1]) / 100 * totalFrames);
|
|
524
|
+
label = `${m[1]}pct`;
|
|
525
|
+
} else if (m = raw.match(/^f(\d+)$/i)) {
|
|
526
|
+
frame = Number(m[1]);
|
|
527
|
+
label = `f${m[1]}`;
|
|
528
|
+
} else if (/^\d+(\.\d+)?$/.test(raw)) {
|
|
529
|
+
frame = Math.round(Number(raw) * fps);
|
|
530
|
+
label = `${Number(raw).toFixed(2)}s`;
|
|
531
|
+
} else throw new Error(`--at: cannot read "${raw}": ${AT_HELP}`);
|
|
532
|
+
if (frame > totalFrames || frame < 0) throw new Error(`--at "${raw}" is past the end: the movie is ${duration} s (${totalFrames} frames)`);
|
|
533
|
+
label = label.replace(/[\\/:*?"<>|]+/g, "-");
|
|
534
|
+
let unique = label, n = 2;
|
|
535
|
+
while (used.has(unique)) unique = `${label}-${n++}`;
|
|
536
|
+
used.add(unique);
|
|
537
|
+
out.push({ label: unique, frame });
|
|
538
|
+
}
|
|
539
|
+
return out;
|
|
540
|
+
}
|
|
541
|
+
function groupIssues(perFrame) {
|
|
542
|
+
const groups = /* @__PURE__ */ new Map();
|
|
543
|
+
for (const { frame, issues } of perFrame) {
|
|
544
|
+
for (const message of issues) {
|
|
545
|
+
const key = message.replace(/-?\d+(\.\d+)?/g, "#");
|
|
546
|
+
const g = groups.get(key);
|
|
547
|
+
if (g) {
|
|
548
|
+
g.count++;
|
|
549
|
+
g.lastFrame = frame;
|
|
550
|
+
} else groups.set(key, { message, count: 1, firstFrame: frame, lastFrame: frame });
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
return [...groups.values()];
|
|
554
|
+
}
|
|
555
|
+
function splitIssues(groups, strict = false) {
|
|
556
|
+
if (strict) return { problems: groups, review: [] };
|
|
557
|
+
const overlap = (g) => /\boverlap\b/.test(g.message);
|
|
558
|
+
return { problems: groups.filter((g) => !overlap(g)), review: groups.filter(overlap) };
|
|
559
|
+
}
|
|
560
|
+
async function reviewMovie(movie, opts = {}) {
|
|
561
|
+
const rows = movie.timelineData().rows;
|
|
562
|
+
const at = opts.at ? resolveAtList(opts.at, { frameRate: movie.frameRate, totalFrames: movie.totalFrames, duration: movie.duration, rows }) : [];
|
|
563
|
+
const frames = sampleFrames({ totalFrames: movie.totalFrames, frameRate: movie.frameRate, scenes: namedScenes(rows) });
|
|
564
|
+
const perFrame = [];
|
|
565
|
+
for (const frame of frames) {
|
|
566
|
+
const r = await movie.inspect(frame, { layers: "none" });
|
|
567
|
+
if (r.issues.length) perFrame.push({ frame, issues: r.issues });
|
|
568
|
+
}
|
|
569
|
+
const grouped = splitIssues(groupIssues(perFrame), !!opts.strict);
|
|
570
|
+
const fonts = movie.inspectFonts();
|
|
571
|
+
for (const f of fonts.failedUnused) grouped.review.push({ message: `web font "${f}" failed to load, but no text layer uses it (a broken or unused @font-face)`, count: 1, firstFrame: 0, lastFrame: 0 });
|
|
572
|
+
for (const m of fonts.missing) {
|
|
573
|
+
(opts.strict ? grouped.problems : grouped.review).push({ message: `layer "${m.layer}": none of the fonts "${m.family}" is available here (it is drawn in a fallback font)`, count: 1, firstFrame: 0, lastFrame: 0 });
|
|
574
|
+
}
|
|
575
|
+
return { frames: frames.length, problems: grouped.problems, review: grouped.review, fonts, audio: movie.audioBuffer ? movie.inspectAudio() : null, at };
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// src/core/onion.ts
|
|
579
|
+
function onionTimes(from, to, count) {
|
|
580
|
+
if (count <= 1) return [from];
|
|
581
|
+
return Array.from({ length: count }, (_, i) => Math.round((from + (to - from) * i / (count - 1)) * 1e6) / 1e6);
|
|
582
|
+
}
|
|
583
|
+
function onionAlphas(count, ramp = 0.25) {
|
|
584
|
+
const weights = Array.from({ length: count }, (_, i) => count === 1 ? 1 : ramp + (1 - ramp) * (i / (count - 1)));
|
|
585
|
+
let sum = 0;
|
|
586
|
+
return weights.map((w) => {
|
|
587
|
+
sum += w;
|
|
588
|
+
return w / sum;
|
|
589
|
+
});
|
|
298
590
|
}
|
|
299
591
|
|
|
300
592
|
// src/core/motionBlur.ts
|
|
@@ -313,6 +605,57 @@ function blurTimes(frame, frameRate, mb, duration) {
|
|
|
313
605
|
return Array.from({ length: mb.samples }, (_, k) => Math.min(Math.max(t + ((k + 0.5) / mb.samples - 0.5) * width, 0), duration));
|
|
314
606
|
}
|
|
315
607
|
|
|
608
|
+
// src/core/renderRange.ts
|
|
609
|
+
function resolveRange(range2, ctx) {
|
|
610
|
+
const { frameRate: fps, totalFrames, duration } = ctx;
|
|
611
|
+
if (range2 === void 0) return { fromFrame: 0, toFrame: totalFrames, fromSec: 0, toSec: duration, partial: false };
|
|
612
|
+
let from, to;
|
|
613
|
+
if (typeof range2 === "string") {
|
|
614
|
+
const rows = (ctx.rows ?? []).filter((r) => r.depth === 0).flatMap((r) => r.parts && r.partNames ? r.partNames.map((name, i) => ({ name, start: r.parts[i].start, end: r.parts[i].end })) : [r]);
|
|
615
|
+
const row = rows.find((r) => r.name === range2);
|
|
616
|
+
if (!row) throw new Error(`pixi-effects: render({ range: "${range2}" }): no layer named "${range2}" at the top level (names: ${rows.slice(0, 40).map((r) => r.name).join(", ") || "none"}${rows.length > 40 ? ", \u2026" : ""})`);
|
|
617
|
+
from = row.start;
|
|
618
|
+
to = row.end;
|
|
619
|
+
} else {
|
|
620
|
+
if (!Array.isArray(range2) || range2.length !== 2 || !range2.every((n) => typeof n === "number" && Number.isFinite(n))) {
|
|
621
|
+
throw new Error("pixi-effects: render({ range }) must be [from, to] in seconds (numbers) or the name of a top-level layer");
|
|
622
|
+
}
|
|
623
|
+
[from, to] = range2;
|
|
624
|
+
}
|
|
625
|
+
if (from < 0 || to <= from) throw new Error(`pixi-effects: render({ range: [${from}, ${to}] }): a range must start before it ends (from >= 0, from < to)`);
|
|
626
|
+
if (to > duration + 0.5 / fps) throw new Error(`pixi-effects: render({ range: [${from}, ${to}] }) goes past the end: the movie is ${duration} s long`);
|
|
627
|
+
const fromFrame = Math.round(from * fps);
|
|
628
|
+
const toFrame = Math.min(Math.round(to * fps), totalFrames);
|
|
629
|
+
if (toFrame <= fromFrame) throw new Error(`pixi-effects: render({ range: [${from}, ${to}] }) is shorter than one frame (${(1 / fps).toFixed(4)} s at ${fps} fps)`);
|
|
630
|
+
const partial = fromFrame > 0 || toFrame < totalFrames;
|
|
631
|
+
return { fromFrame, toFrame, fromSec: fromFrame / fps, toSec: toFrame / fps, partial };
|
|
632
|
+
}
|
|
633
|
+
function sliceChannels(channels, sampleRate, fromSec, toSec, opts = {}) {
|
|
634
|
+
const fade = Math.max(0, Math.round((opts.fade ?? 0.01) * sampleRate));
|
|
635
|
+
const from = Math.round(fromSec * sampleRate), len = Math.max(1, Math.round(toSec * sampleRate) - from);
|
|
636
|
+
return channels.map((src) => {
|
|
637
|
+
const out = new Float32Array(len);
|
|
638
|
+
for (let i = 0; i < len; i++) {
|
|
639
|
+
const j = from + i;
|
|
640
|
+
if (j >= 0 && j < src.length) out[i] = src[j];
|
|
641
|
+
}
|
|
642
|
+
const f = Math.min(fade, len >> 1);
|
|
643
|
+
if (opts.fadeIn !== false) for (let i = 0; i < f; i++) out[i] = out[i] * (i / f);
|
|
644
|
+
if (opts.fadeOut !== false) for (let i = 0; i < f; i++) out[len - 1 - i] = out[len - 1 - i] * (i / f);
|
|
645
|
+
return out;
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
function resolveRenderOptions(o) {
|
|
649
|
+
const draft = !!o.draft;
|
|
650
|
+
const scale = o.scale ?? (draft ? 0.5 : 1);
|
|
651
|
+
if (!(typeof scale === "number" && scale > 0 && scale <= 1)) throw new Error(`pixi-effects: render({ scale }): scale must be above 0 and at most 1 (got ${scale})`);
|
|
652
|
+
return {
|
|
653
|
+
scale,
|
|
654
|
+
bitrate: o.video?.bitrate ?? (draft ? "low" : void 0),
|
|
655
|
+
motionBlur: o.motionBlur !== void 0 ? o.motionBlur : draft ? false : void 0
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
|
|
316
659
|
// src/core/Renderer.ts
|
|
317
660
|
var VIDEO_CODEC_BY_FORMAT = {
|
|
318
661
|
mp4: "avc",
|
|
@@ -367,13 +710,22 @@ function makeOutputFormat(name) {
|
|
|
367
710
|
return new mediabunny.MkvOutputFormat();
|
|
368
711
|
}
|
|
369
712
|
}
|
|
713
|
+
function sliceAudio(buffer, fromSec, toSec, fadeIn, fadeOut) {
|
|
714
|
+
const channels = Array.from({ length: buffer.numberOfChannels }, (_, c) => buffer.getChannelData(c));
|
|
715
|
+
const cut = sliceChannels(channels, buffer.sampleRate, fromSec, toSec, { fadeIn, fadeOut });
|
|
716
|
+
const out = new AudioBuffer({ length: cut[0].length, numberOfChannels: cut.length, sampleRate: buffer.sampleRate });
|
|
717
|
+
cut.forEach((data, c) => out.copyToChannel(data, c));
|
|
718
|
+
return out;
|
|
719
|
+
}
|
|
370
720
|
async function exportFrames(movie, options = {}) {
|
|
371
721
|
const fmt = options.format ?? "mp4";
|
|
722
|
+
const eff = resolveRenderOptions(options);
|
|
723
|
+
const range2 = resolveRange(options.range, { frameRate: movie.frameRate, totalFrames: movie.totalFrames, duration: movie.duration, rows: typeof options.range === "string" ? movie.timelineData().rows : void 0 });
|
|
372
724
|
const opts = {
|
|
373
725
|
format: fmt,
|
|
374
726
|
video: {
|
|
375
727
|
codec: options.video?.codec ?? VIDEO_CODEC_BY_FORMAT[fmt],
|
|
376
|
-
bitrate: qualityMap[
|
|
728
|
+
bitrate: qualityMap[eff.bitrate ?? "high"] ?? mediabunny.QUALITY_HIGH
|
|
377
729
|
},
|
|
378
730
|
audio: {
|
|
379
731
|
codec: options.audio?.codec ?? AUDIO_CODEC_BY_FORMAT[fmt],
|
|
@@ -381,27 +733,30 @@ async function exportFrames(movie, options = {}) {
|
|
|
381
733
|
}
|
|
382
734
|
};
|
|
383
735
|
const container = makeOutputFormat(opts.format);
|
|
384
|
-
|
|
736
|
+
const audio = movie.audioBuffer && range2.partial ? sliceAudio(movie.audioBuffer, range2.fromSec, range2.toSec, range2.fromFrame > 0, range2.toFrame < movie.totalFrames) : movie.audioBuffer;
|
|
737
|
+
if (audio) opts.audio.codec = await chooseAudioCodec(fmt, options.audio?.codec, container, audio, opts.audio.bitrate);
|
|
385
738
|
const output = new mediabunny.Output({
|
|
386
739
|
format: container,
|
|
387
740
|
target: new mediabunny.BufferTarget()
|
|
388
741
|
});
|
|
389
|
-
const mb =
|
|
742
|
+
const mb = eff.motionBlur !== void 0 ? resolveMotionBlur(eff.motionBlur, "movie.render()") : movie.motionBlur ?? null;
|
|
390
743
|
const stage = movie.app.canvas;
|
|
391
744
|
const blurCanvas = mb ? Object.assign(document.createElement("canvas"), { width: stage.width, height: stage.height }) : null;
|
|
392
|
-
const
|
|
745
|
+
const small = eff.scale < 1 ? Object.assign(document.createElement("canvas"), { width: Math.max(2, Math.round(stage.width * eff.scale / 2) * 2), height: Math.max(2, Math.round(stage.height * eff.scale / 2) * 2) }) : null;
|
|
746
|
+
const smallCtx = small?.getContext("2d") ?? null;
|
|
747
|
+
const canvasSource = new mediabunny.CanvasSource(small ?? blurCanvas ?? stage, {
|
|
393
748
|
codec: opts.video.codec,
|
|
394
749
|
bitrate: opts.video.bitrate
|
|
395
750
|
});
|
|
396
751
|
output.addVideoTrack(canvasSource, { frameRate: movie.frameRate });
|
|
397
|
-
if (
|
|
752
|
+
if (audio) {
|
|
398
753
|
const audioSource = new mediabunny.AudioBufferSource({
|
|
399
754
|
codec: opts.audio.codec,
|
|
400
755
|
bitrate: opts.audio.bitrate
|
|
401
|
-
}, { startTimestamp: opts.audio.codec === "aac" ? -2112 /
|
|
756
|
+
}, { startTimestamp: opts.audio.codec === "aac" ? -2112 / audio.sampleRate : 0 });
|
|
402
757
|
output.addAudioTrack(audioSource);
|
|
403
758
|
await output.start();
|
|
404
|
-
await audioSource.add(
|
|
759
|
+
await audioSource.add(audio);
|
|
405
760
|
await audioSource.close();
|
|
406
761
|
} else {
|
|
407
762
|
await output.start();
|
|
@@ -409,18 +764,21 @@ async function exportFrames(movie, options = {}) {
|
|
|
409
764
|
movie.app.ticker.stop();
|
|
410
765
|
const keyframeIntervalFrames = Math.max(1, Math.round(2 * movie.frameRate));
|
|
411
766
|
try {
|
|
412
|
-
|
|
767
|
+
const count = Math.max(1, range2.toFrame - range2.fromFrame);
|
|
768
|
+
for (let frame = range2.fromFrame; frame <= range2.toFrame; frame++) {
|
|
413
769
|
if (mb && blurCanvas) await movie._exposeFrame(frame, mb, blurCanvas, false);
|
|
414
770
|
else await movie.gotoFrame(frame, true);
|
|
415
|
-
|
|
771
|
+
if (small && smallCtx) smallCtx.drawImage(blurCanvas ?? stage, 0, 0, small.width, small.height);
|
|
772
|
+
const rel = frame - range2.fromFrame;
|
|
773
|
+
const isKey = rel === 0 || rel % keyframeIntervalFrames === 0;
|
|
416
774
|
const addOpts = isKey ? { keyFrame: true } : void 0;
|
|
417
|
-
await canvasSource.add(
|
|
418
|
-
const progress = Math.floor(
|
|
775
|
+
await canvasSource.add(rel / movie.frameRate, 1 / movie.frameRate, addOpts);
|
|
776
|
+
const progress = Math.floor(rel / count * 100);
|
|
419
777
|
movie.emit("progress", { progress, frame, totalFrames: movie.totalFrames });
|
|
420
778
|
}
|
|
421
779
|
await canvasSource.close();
|
|
422
780
|
await output.finalize();
|
|
423
|
-
if (mb) await movie.gotoFrame(
|
|
781
|
+
if (mb) await movie.gotoFrame(range2.toFrame, true);
|
|
424
782
|
return new Blob([output.target.buffer], { type: output.format.mimeType });
|
|
425
783
|
} finally {
|
|
426
784
|
movie.app.ticker.start();
|
|
@@ -867,16 +1225,16 @@ function expandTransitions(spec) {
|
|
|
867
1225
|
if (toUses > 1) {
|
|
868
1226
|
throw new Error(`pixi-effects: ${tag} sequence "${t.to}" is used as \`to\` in more than one transition`);
|
|
869
1227
|
}
|
|
870
|
-
const tStart =
|
|
1228
|
+
const tStart = chunkANO5R2E6_cjs.resolveAt(t.at, parentDuration);
|
|
871
1229
|
const tEnd = tStart + t.duration;
|
|
872
1230
|
const fromSeq = fromEntry.seq;
|
|
873
|
-
const fromStart =
|
|
1231
|
+
const fromStart = chunkANO5R2E6_cjs.resolveAt(fromSeq.at, parentDuration);
|
|
874
1232
|
const fromEnd = fromStart + (fromSeq.duration ?? parentDuration);
|
|
875
1233
|
if (tStart < fromStart - EPS || tEnd > fromEnd + EPS) {
|
|
876
1234
|
throw new Error(`pixi-effects: ${tag} window [${tStart}, ${tEnd}] is not covered by \`from\` "${t.from}" (lives [${fromStart}, ${fromEnd}], so it ends at ${fromEnd})`);
|
|
877
1235
|
}
|
|
878
1236
|
const toSeq = toEntry.seq;
|
|
879
|
-
const toStart =
|
|
1237
|
+
const toStart = chunkANO5R2E6_cjs.resolveAt(toSeq.at, parentDuration);
|
|
880
1238
|
const toEnd = toStart + (toSeq.duration ?? parentDuration);
|
|
881
1239
|
if (tStart < toStart - EPS || tEnd > toEnd + EPS) {
|
|
882
1240
|
throw new Error(`pixi-effects: ${tag} window [${tStart}, ${tEnd}] is not covered by \`to\` "${t.to}" (lives [${toStart}, ${toEnd}], so it starts at ${toStart})`);
|
|
@@ -1216,9 +1574,9 @@ function inspectScene(root, frame, time, canvas, opts = {}) {
|
|
|
1216
1574
|
onCanvas = inter === 0 && area > 0 ? "none" : inter >= area - 1e-6 ? "full" : "partial";
|
|
1217
1575
|
if (area === 0) onCanvas = "none";
|
|
1218
1576
|
}
|
|
1219
|
-
const keys =
|
|
1577
|
+
const keys = chunkANO5R2E6_cjs.collectPropKeys(seq.spec);
|
|
1220
1578
|
const carried = carriers.some((c) => {
|
|
1221
|
-
const k =
|
|
1579
|
+
const k = chunkANO5R2E6_cjs.collectPropKeys(c.spec);
|
|
1222
1580
|
return (c.spec.keyframes ?? []).length > 0 && (k.has("x") || k.has("y") || k.has("rotation") || k.has("scale"));
|
|
1223
1581
|
});
|
|
1224
1582
|
const moving = parentMoving || carried || (seq.spec.keyframes ?? []).length > 0 && (keys.has("x") || keys.has("y"));
|
|
@@ -1239,7 +1597,7 @@ function inspectScene(root, frame, time, canvas, opts = {}) {
|
|
|
1239
1597
|
const group = parentScene ?? (name !== void 0 && blending.has(name) ? name : void 0);
|
|
1240
1598
|
if (group) scene.set(info, group);
|
|
1241
1599
|
if (zero || seq.showsNothingYet) scaledToNothing.add(info);
|
|
1242
|
-
if (seq instanceof
|
|
1600
|
+
if (seq instanceof chunkJW67MDHQ_cjs.CompositionSequence) walk2(seq, prefix + label + "/", insideThreeD || threeD, visible, group, moving, zero);
|
|
1243
1601
|
});
|
|
1244
1602
|
};
|
|
1245
1603
|
walk2(root, "", false, true);
|
|
@@ -1300,10 +1658,10 @@ function walk(comp, parentStart, parentEnd, depth, prefix, rows, transitions) {
|
|
|
1300
1658
|
const start = parentStart + (seq.at ?? 0);
|
|
1301
1659
|
const end = Math.min(parentEnd, seq.duration !== void 0 ? start + seq.duration : parentEnd);
|
|
1302
1660
|
const span = end - start;
|
|
1303
|
-
const keys = (seq.spec.keyframes ?? []).map((kf) => round(start +
|
|
1661
|
+
const keys = (seq.spec.keyframes ?? []).map((kf) => round(start + chunkANO5R2E6_cjs.resolveAt(kf.at, span))).sort((a, b) => a - b).slice(0, MAX_KEYS);
|
|
1304
1662
|
const row = { path: prefix + name, name, type: seq.spec.type, depth, start: round(start), end: round(end), keys, detail: detailOf(seq) };
|
|
1305
1663
|
const inner = [];
|
|
1306
|
-
const isComp = seq instanceof
|
|
1664
|
+
const isComp = seq instanceof chunkJW67MDHQ_cjs.CompositionSequence;
|
|
1307
1665
|
if (isComp) walk(seq, start, end, depth + 1, prefix + name + "/", inner, transitions);
|
|
1308
1666
|
entries.push({ row, inner, groupable: !isComp });
|
|
1309
1667
|
});
|
|
@@ -1336,7 +1694,8 @@ function groupFamilies(entries) {
|
|
|
1336
1694
|
start: Math.min(...members.map((x) => x.start)),
|
|
1337
1695
|
end: Math.max(...members.map((x) => x.end)),
|
|
1338
1696
|
keys: members.flatMap((x) => x.keys).sort((a, b) => a - b).slice(0, MAX_KEYS),
|
|
1339
|
-
parts: members.map((x) => ({ start: x.start, end: x.end }))
|
|
1697
|
+
parts: members.map((x) => ({ start: x.start, end: x.end })),
|
|
1698
|
+
partNames: members.map((x) => x.name)
|
|
1340
1699
|
});
|
|
1341
1700
|
} else {
|
|
1342
1701
|
out.push(e.row, ...e.inner);
|
|
@@ -1851,7 +2210,7 @@ var Movie = class {
|
|
|
1851
2210
|
height: options.composition.height ?? this.height,
|
|
1852
2211
|
duration: options.composition.duration ?? this.duration
|
|
1853
2212
|
} : void 0;
|
|
1854
|
-
await
|
|
2213
|
+
await chunkANO5R2E6_cjs.ensureFilterLibrary(seededComposition);
|
|
1855
2214
|
const userComposition = seededComposition ? expandTransitions(seededComposition) : void 0;
|
|
1856
2215
|
const rootSeqSpec = {
|
|
1857
2216
|
type: "composition",
|
|
@@ -1861,7 +2220,7 @@ var Movie = class {
|
|
|
1861
2220
|
...userComposition
|
|
1862
2221
|
};
|
|
1863
2222
|
if (userComposition) carryTransitionWindows(userComposition, rootSeqSpec);
|
|
1864
|
-
const composition = new
|
|
2223
|
+
const composition = new chunkJW67MDHQ_cjs.CompositionSequence(rootSeqSpec, null, rootShape);
|
|
1865
2224
|
await composition.build();
|
|
1866
2225
|
this._rootSequence = composition;
|
|
1867
2226
|
if (composition.target) root.addChild(composition.target);
|
|
@@ -2059,6 +2418,37 @@ var Movie = class {
|
|
|
2059
2418
|
})));
|
|
2060
2419
|
return new Blob([buildPdf(pages, { title: opts.title })], { type: "application/pdf" });
|
|
2061
2420
|
}
|
|
2421
|
+
async onionSkin(opts = {}) {
|
|
2422
|
+
chunkUQBGW5DO_cjs.warnUnknownOptions("movie.onionSkin()", opts, ["from", "to", "count", "scale", "as", "motionBlur"]);
|
|
2423
|
+
this._requireReady("onionSkin");
|
|
2424
|
+
const from = opts.from ?? 0, to = opts.to ?? this.duration, count = opts.count ?? 8, scale = opts.scale ?? 1;
|
|
2425
|
+
if (!(Number.isInteger(count) && count >= 1 && count <= 64)) throw new Error(`pixi-effects: movie.onionSkin(): count must be a whole number from 1 to 64, got ${count}`);
|
|
2426
|
+
if (!(from >= 0 && to <= this.duration + 1e-9 && (count === 1 || from < to))) throw new Error(`pixi-effects: movie.onionSkin(): from must be before to, inside the movie (0 to ${this.duration} s), got ${from} to ${to}`);
|
|
2427
|
+
if (!(scale > 0 && scale <= 4)) throw new Error(`pixi-effects: movie.onionSkin(): scale must be above 0 and at most 4, got ${scale}`);
|
|
2428
|
+
const mb = this._blurFor(opts.motionBlur, "movie.onionSkin()");
|
|
2429
|
+
const blurred = mb ? this._blurCanvas() : null;
|
|
2430
|
+
const out = document.createElement("canvas");
|
|
2431
|
+
out.width = Math.max(1, Math.round(this.width * scale));
|
|
2432
|
+
out.height = Math.max(1, Math.round(this.height * scale));
|
|
2433
|
+
const g = out.getContext("2d");
|
|
2434
|
+
const alphas = onionAlphas(count);
|
|
2435
|
+
const frames = onionTimes(from, to, count).map((t) => clampFrame(t * this.frameRate, this.totalFrames));
|
|
2436
|
+
const back = this.currentFrame, wasAtPoster = this._atPoster;
|
|
2437
|
+
try {
|
|
2438
|
+
for (let i = 0; i < frames.length; i++) {
|
|
2439
|
+
const f = frames[i];
|
|
2440
|
+
if (mb && blurred) await this._quietly(() => this._exposeFrame(f, mb, blurred, true));
|
|
2441
|
+
else await this._quietly(() => this.gotoFrame(f, true));
|
|
2442
|
+
g.globalAlpha = alphas[i];
|
|
2443
|
+
g.drawImage(blurred ?? this.app.canvas, 0, 0, out.width, out.height);
|
|
2444
|
+
}
|
|
2445
|
+
} finally {
|
|
2446
|
+
g.globalAlpha = 1;
|
|
2447
|
+
if (wasAtPoster) await this._showPoster();
|
|
2448
|
+
else await this._quietly(() => this.gotoFrame(back, true));
|
|
2449
|
+
}
|
|
2450
|
+
return encodeCanvas(out, "image/png", opts.as ?? "blob");
|
|
2451
|
+
}
|
|
2062
2452
|
async contactSheet(opts = {}) {
|
|
2063
2453
|
chunkUQBGW5DO_cjs.warnUnknownOptions("movie.contactSheet()", opts, ["frames", "times", "count", "columns", "cellWidth", "as", "motionBlur"]);
|
|
2064
2454
|
this._requireReady("contactSheet");
|
|
@@ -2125,10 +2515,40 @@ var Movie = class {
|
|
|
2125
2515
|
* The soundtrack, measured: when every audio layer plays, how loud the mix is over time, and `issues`
|
|
2126
2516
|
* (limiting, inaudible layers, sounds cut off by the end). Use it to CHECK sound you cannot hear.
|
|
2127
2517
|
*/
|
|
2518
|
+
/** 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. */
|
|
2519
|
+
resolveAt(list) {
|
|
2520
|
+
this._requireReady("resolveAt");
|
|
2521
|
+
return resolveAtList(list, { frameRate: this.frameRate, totalFrames: this.totalFrames, duration: this.duration, rows: this.timelineData().rows });
|
|
2522
|
+
}
|
|
2523
|
+
/**
|
|
2524
|
+
* One call that looks at everything `pixi-effects-check` looks at inside the page: layout issues over the whole timeline (every
|
|
2525
|
+
* 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.
|
|
2526
|
+
*/
|
|
2527
|
+
async review(opts = {}) {
|
|
2528
|
+
chunkUQBGW5DO_cjs.warnUnknownOptions("movie.review()", opts, ["at", "strict"]);
|
|
2529
|
+
this._requireReady("review");
|
|
2530
|
+
const back = this.currentFrame, wasAtPoster = this._atPoster;
|
|
2531
|
+
try {
|
|
2532
|
+
return await reviewMovie(this, opts);
|
|
2533
|
+
} finally {
|
|
2534
|
+
this.currentFrame = back;
|
|
2535
|
+
if (wasAtPoster) await this._showPoster();
|
|
2536
|
+
else await this._quietly(() => this.gotoFrame(back, true));
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
/**
|
|
2540
|
+
* The fonts of the text layers: `missing` lists layers none of whose `fontFamily` entries is available in this browser (they are
|
|
2541
|
+
* drawn in a fallback font), `failed` lists web fonts (`@font-face`) whose file could not be loaded.
|
|
2542
|
+
*/
|
|
2543
|
+
inspectFonts() {
|
|
2544
|
+
this._requireReady("inspectFonts");
|
|
2545
|
+
return inspectFonts(this._rootSequence);
|
|
2546
|
+
}
|
|
2128
2547
|
inspectAudio(opts = {}) {
|
|
2129
|
-
chunkUQBGW5DO_cjs.warnUnknownOptions("movie.inspectAudio()", opts, ["window"]);
|
|
2548
|
+
chunkUQBGW5DO_cjs.warnUnknownOptions("movie.inspectAudio()", opts, ["window", "scenes"]);
|
|
2130
2549
|
this._requireReady("inspectAudio");
|
|
2131
|
-
|
|
2550
|
+
const scenes = opts.scenes ?? namedScenes(this.timelineData().rows);
|
|
2551
|
+
return analyzeAudio(this.audioBuffer, this._audioSources, this._mixStats, this.duration, { ...opts, scenes });
|
|
2132
2552
|
}
|
|
2133
2553
|
/**
|
|
2134
2554
|
* Draw the stage now. PixiJS's CullerPlugin only culls inside `app.render()` (the ticker), and we render
|
|
@@ -2396,7 +2816,7 @@ var Movie = class {
|
|
|
2396
2816
|
return this.muted;
|
|
2397
2817
|
}
|
|
2398
2818
|
async render(options) {
|
|
2399
|
-
chunkUQBGW5DO_cjs.warnUnknownOptions("movie.render()", options, ["format", "video", "audio", "motionBlur"]);
|
|
2819
|
+
chunkUQBGW5DO_cjs.warnUnknownOptions("movie.render()", options, ["format", "video", "audio", "motionBlur", "range", "scale", "draft"]);
|
|
2400
2820
|
try {
|
|
2401
2821
|
return await this._quietly(() => exportFrames(this, options));
|
|
2402
2822
|
} catch (err) {
|
|
@@ -2607,6 +3027,7 @@ function clamp(v, lo, hi) {
|
|
|
2607
3027
|
return Math.max(lo, Math.min(hi, v));
|
|
2608
3028
|
}
|
|
2609
3029
|
function parseEase(name) {
|
|
3030
|
+
chunkANO5R2E6_cjs.checkEase(name, "a preset");
|
|
2610
3031
|
return gsap.gsap.parseEase(name);
|
|
2611
3032
|
}
|
|
2612
3033
|
|
|
@@ -2629,17 +3050,17 @@ function orbit(opts) {
|
|
|
2629
3050
|
throw new Error("pixi-effects: orbit() cannot combine `radius` with `dollyZoom` \u2014 the radius follows the animated fov");
|
|
2630
3051
|
}
|
|
2631
3052
|
const [cx, cy] = opts.center ?? [width / 2, height / 2];
|
|
2632
|
-
const fixedR = opts.radius ??
|
|
3053
|
+
const fixedR = opts.radius ?? chunkANO5R2E6_cjs.homeDistance(height, fov ?? chunkANO5R2E6_cjs.DEFAULT_FOV);
|
|
2633
3054
|
const start = opts.start ?? -degrees / 2;
|
|
2634
3055
|
const steps = Math.max(1, Math.round(duration * stepsPerSecond));
|
|
2635
3056
|
const easeFn = parseEase(ease);
|
|
2636
3057
|
const dollyEase = dolly ? parseEase(dolly.ease ?? ease) : null;
|
|
2637
3058
|
const pos = (i) => {
|
|
2638
3059
|
const p = i / steps;
|
|
2639
|
-
const a = (start + degrees * easeFn(p)) *
|
|
3060
|
+
const a = (start + degrees * easeFn(p)) * chunkANO5R2E6_cjs.DEG;
|
|
2640
3061
|
if (!dolly) return { x: cx + fixedR * Math.sin(a), z: fixedR * Math.cos(a) };
|
|
2641
3062
|
const f = dolly.from + (dolly.to - dolly.from) * dollyEase(p);
|
|
2642
|
-
const R =
|
|
3063
|
+
const R = chunkANO5R2E6_cjs.homeDistance(height, f);
|
|
2643
3064
|
return { x: cx + R * Math.sin(a), z: R * Math.cos(a), fov: f };
|
|
2644
3065
|
};
|
|
2645
3066
|
const keyframes = [];
|
|
@@ -2694,7 +3115,7 @@ function particles(opts) {
|
|
|
2694
3115
|
const round3 = (v) => Math.round(v * 1e6) / 1e6;
|
|
2695
3116
|
const out = [];
|
|
2696
3117
|
for (let i = 0; i < count; i++) {
|
|
2697
|
-
const r =
|
|
3118
|
+
const r = chunkANO5R2E6_cjs.random(seed * 1000003 + i * 7919 + 17);
|
|
2698
3119
|
const pick = ([a2, b]) => a2 + r() * (b - a2);
|
|
2699
3120
|
const born = at + r() * emit;
|
|
2700
3121
|
const lifetime = pick(life);
|
|
@@ -2707,7 +3128,7 @@ function particles(opts) {
|
|
|
2707
3128
|
const turn = spin ? pick(spin) : 0;
|
|
2708
3129
|
const startAngle = spin ? r() * 360 : 0;
|
|
2709
3130
|
const swayAmp = sway?.amp ?? 0, swayFreq = sway?.freq ?? 1;
|
|
2710
|
-
const n0 = sway ?
|
|
3131
|
+
const n0 = sway ? chunkANO5R2E6_cjs.noise(0, seed * 1000003 + i) : 0;
|
|
2711
3132
|
const at_ = (t) => {
|
|
2712
3133
|
const axis = (p0, v0, acc) => {
|
|
2713
3134
|
if (drag > 0) {
|
|
@@ -2716,7 +3137,7 @@ function particles(opts) {
|
|
|
2716
3137
|
}
|
|
2717
3138
|
return p0 + v0 * t + 0.5 * acc * t * t;
|
|
2718
3139
|
};
|
|
2719
|
-
const sw = sway ? swayAmp * (
|
|
3140
|
+
const sw = sway ? swayAmp * (chunkANO5R2E6_cjs.noise(t * swayFreq, seed * 1000003 + i) - n0) : 0;
|
|
2720
3141
|
return { x: axis(x0, vx, wind) + sw, y: axis(y0, vy, gravity) };
|
|
2721
3142
|
};
|
|
2722
3143
|
const kfs = [];
|
|
@@ -2778,7 +3199,7 @@ function followPath(opts) {
|
|
|
2778
3199
|
if (steps > MAX_STEPS) throw new Error(`followPath(): ${steps} samples is too many (max ${MAX_STEPS}); lower frameRate or duration`);
|
|
2779
3200
|
const segments = [];
|
|
2780
3201
|
let total = 0;
|
|
2781
|
-
for (const line of
|
|
3202
|
+
for (const line of chunkANO5R2E6_cjs.flattenSvgPath(d)) {
|
|
2782
3203
|
const p = line.closed ? [...line.pts, line.pts[0], line.pts[1]] : line.pts;
|
|
2783
3204
|
for (let i = 2; i < p.length; i += 2) {
|
|
2784
3205
|
const len = Math.hypot(p[i] - p[i - 2], p[i + 1] - p[i - 1]);
|
|
@@ -2931,7 +3352,7 @@ function distances(n, from, grid, seed) {
|
|
|
2931
3352
|
if (n === 0) return [];
|
|
2932
3353
|
if (from === "random") {
|
|
2933
3354
|
const order = Array.from({ length: n }, (_, i) => i);
|
|
2934
|
-
const r =
|
|
3355
|
+
const r = chunkANO5R2E6_cjs.random(seed);
|
|
2935
3356
|
for (let i = n - 1; i > 0; i--) {
|
|
2936
3357
|
const j = Math.floor(r() * (i + 1));
|
|
2937
3358
|
[order[i], order[j]] = [order[j], order[i]];
|
|
@@ -2993,7 +3414,7 @@ function wiggle(opts) {
|
|
|
2993
3414
|
to[n] = p.around;
|
|
2994
3415
|
return;
|
|
2995
3416
|
}
|
|
2996
|
-
const off = (
|
|
3417
|
+
const off = (chunkANO5R2E6_cjs.rand(seed * 7919 + pi * 104729 + s * 31 + 1) * 2 - 1) * p.amp;
|
|
2997
3418
|
to[n] = typeof p.around === "number" ? p.around + off : `(${p.around}) ${off < 0 ? "-" : "+"} ${Math.abs(off)}`;
|
|
2998
3419
|
});
|
|
2999
3420
|
out.push({ at: at + s * stepLen, to, duration: stepLen, ease });
|
|
@@ -3458,19 +3879,19 @@ function withFade(spec, opts) {
|
|
|
3458
3879
|
|
|
3459
3880
|
Object.defineProperty(exports, "noise", {
|
|
3460
3881
|
enumerable: true,
|
|
3461
|
-
get: function () { return
|
|
3882
|
+
get: function () { return chunkANO5R2E6_cjs.noise; }
|
|
3462
3883
|
});
|
|
3463
3884
|
Object.defineProperty(exports, "rand", {
|
|
3464
3885
|
enumerable: true,
|
|
3465
|
-
get: function () { return
|
|
3886
|
+
get: function () { return chunkANO5R2E6_cjs.rand; }
|
|
3466
3887
|
});
|
|
3467
3888
|
Object.defineProperty(exports, "random", {
|
|
3468
3889
|
enumerable: true,
|
|
3469
|
-
get: function () { return
|
|
3890
|
+
get: function () { return chunkANO5R2E6_cjs.random; }
|
|
3470
3891
|
});
|
|
3471
3892
|
Object.defineProperty(exports, "registerSequenceType", {
|
|
3472
3893
|
enumerable: true,
|
|
3473
|
-
get: function () { return
|
|
3894
|
+
get: function () { return chunkANO5R2E6_cjs.registerSequenceType; }
|
|
3474
3895
|
});
|
|
3475
3896
|
exports.Movie = Movie;
|
|
3476
3897
|
exports.animateText = animateText;
|