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.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { A as AudioInspectOptions, a as AudioReport, b as AudioSourceReport, C as ContactSheetOptions, F as FrameEvent, I as InspectOptions,
|
|
1
|
+
export { A as AudioInspectOptions, a as AudioReport, b as AudioSourceReport, C as ContactSheetOptions, F as FontReport, c as FrameEvent, I as InspectOptions, d as InspectReport, e as IssueGroup, L as LayerInfo, f as LoaderOption, M as MotionBlurOptions, g as MotionBlurSpec, h as Movie, i as MovieErrorEvent, j as MovieOptions, O as OnionSkinOptions, P as ProgressEvent, R as RenderOptions, k as ReviewOptions, l as ReviewReport, S as SnapshotOptions, m as SoundMeasure, n as StopEvent, o as StopImage, p as StopImagesOptions, T as TimelineData, q as TimelineHtmlOptions, r as TimelineRow, s as TimelineSvgOptions, t as TimelineTransition, V as VolumeEvent, u as dismissLoader, v as failLoader, w as resolveLoader } from './Movie-K2YuVAdg.cjs';
|
|
2
2
|
import { S as SequenceSpec, C as CompositionShape, I as ImageSequenceSpec, a as CameraSequenceSpec, K as Keyframe, T as TextSequenceSpec, M as MusicOptions, P as Props, b as TransitionSpec, c as CompositionSpec, d as SequenceCommon } from './types-SS51lP6O.cjs';
|
|
3
3
|
export { A as ArcShapeSpec, e as AssetSpec, f as AudioAssetSpec, g as AudioMusicSpec, h as AudioSequenceSpec, i as AudioSfxSpec, j as ChromaKeyFilterSpec, k as CircleShapeSpec, l as CompositionSequenceSpec, m as CrossfadeTransition, n as CustomFilterSpec, D as DipTransition, o as DissolveTransition, E as EllipseShapeSpec, p as Expr, F as FilterSpec, G as GradientSpec, q as IrisTransition, L as LineShapeSpec, r as MusicDrum, s as MusicDrums, t as MusicInstrument, u as MusicLevel, v as MusicTrack, N as NullSequenceSpec, w as PathShapeSpec, x as PolygonShapeSpec, y as PropValue, R as RectShapeSpec, z as SfxKnobs, B as SfxOptions, H as SfxPreset, J as SfxVoice, O as ShapeSequenceSpec, Q as SlideTransition, U as Stop, V as StopSpec, W as TransitionCommon, X as VideoSequenceSpec, Y as WipeTransition, Z as ZoomTransition, _ as normalizeStops, $ as pictureStops } from './types-SS51lP6O.cjs';
|
|
4
4
|
import { S as Sequence } from './Base-OHqp8qvX.cjs';
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { A as AudioInspectOptions, a as AudioReport, b as AudioSourceReport, C as ContactSheetOptions, F as FrameEvent, I as InspectOptions,
|
|
1
|
+
export { A as AudioInspectOptions, a as AudioReport, b as AudioSourceReport, C as ContactSheetOptions, F as FontReport, c as FrameEvent, I as InspectOptions, d as InspectReport, e as IssueGroup, L as LayerInfo, f as LoaderOption, M as MotionBlurOptions, g as MotionBlurSpec, h as Movie, i as MovieErrorEvent, j as MovieOptions, O as OnionSkinOptions, P as ProgressEvent, R as RenderOptions, k as ReviewOptions, l as ReviewReport, S as SnapshotOptions, m as SoundMeasure, n as StopEvent, o as StopImage, p as StopImagesOptions, T as TimelineData, q as TimelineHtmlOptions, r as TimelineRow, s as TimelineSvgOptions, t as TimelineTransition, V as VolumeEvent, u as dismissLoader, v as failLoader, w as resolveLoader } from './Movie-DlHQ9C02.js';
|
|
2
2
|
import { S as SequenceSpec, C as CompositionShape, I as ImageSequenceSpec, a as CameraSequenceSpec, K as Keyframe, T as TextSequenceSpec, M as MusicOptions, P as Props, b as TransitionSpec, c as CompositionSpec, d as SequenceCommon } from './types-SS51lP6O.js';
|
|
3
3
|
export { A as ArcShapeSpec, e as AssetSpec, f as AudioAssetSpec, g as AudioMusicSpec, h as AudioSequenceSpec, i as AudioSfxSpec, j as ChromaKeyFilterSpec, k as CircleShapeSpec, l as CompositionSequenceSpec, m as CrossfadeTransition, n as CustomFilterSpec, D as DipTransition, o as DissolveTransition, E as EllipseShapeSpec, p as Expr, F as FilterSpec, G as GradientSpec, q as IrisTransition, L as LineShapeSpec, r as MusicDrum, s as MusicDrums, t as MusicInstrument, u as MusicLevel, v as MusicTrack, N as NullSequenceSpec, w as PathShapeSpec, x as PolygonShapeSpec, y as PropValue, R as RectShapeSpec, z as SfxKnobs, B as SfxOptions, H as SfxPreset, J as SfxVoice, O as ShapeSequenceSpec, Q as SlideTransition, U as Stop, V as StopSpec, W as TransitionCommon, X as VideoSequenceSpec, Y as WipeTransition, Z as ZoomTransition, _ as normalizeStops, $ as pictureStops } from './types-SS51lP6O.js';
|
|
4
4
|
import { S as Sequence } from './Base-BQUodYZK.js';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { CompositionSequence } from './chunk-
|
|
2
|
-
import { ensureFilterLibrary, homeDistance, DEFAULT_FOV, random, noise, flattenSvgPath, rand, DEG, resolveAt, collectPropKeys } from './chunk-
|
|
3
|
-
export { noise, rand, random, registerSequenceType } from './chunk-
|
|
1
|
+
import { CompositionSequence } from './chunk-UXDR3ZDU.js';
|
|
2
|
+
import { ensureFilterLibrary, homeDistance, DEFAULT_FOV, random, noise, flattenSvgPath, rand, DEG, resolveAt, checkEase, collectPropKeys } from './chunk-SG3Y66FN.js';
|
|
3
|
+
export { noise, rand, random, registerSequenceType } from './chunk-SG3Y66FN.js';
|
|
4
4
|
import { warnUnknownOptions, suggestName } from './chunk-DKGG7WD4.js';
|
|
5
5
|
import * as PIXI from 'pixi.js';
|
|
6
6
|
import { extensions, CullerPlugin, Application, Container, Rectangle, Culler, CanvasTextMetrics, TextStyle, Assets, ExtensionType, Filter, UniformGroup, GpuProgram, GlProgram, defaultFilterVert } from 'pixi.js';
|
|
@@ -183,6 +183,118 @@ function spectralCentroid(samples, sampleRate, from = 0, to = samples.length) {
|
|
|
183
183
|
return den > 1e-9 ? Math.round(num3 / den / 10) * 10 : 0;
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
+
// src/audio/loudness.ts
|
|
187
|
+
var LOUDNESS_NOTES = { quietBelow: -24, loudAbove: -9, headroomDb: -1 };
|
|
188
|
+
function kWeighting(fs) {
|
|
189
|
+
const f0 = 1681.974450955533, G = 3.999843853973347, Q = 0.7071752369554196;
|
|
190
|
+
const K = Math.tan(Math.PI * f0 / fs), Vh = 10 ** (G / 20), Vb = Vh ** 0.4996667741545416;
|
|
191
|
+
const a0 = 1 + K / Q + K * K;
|
|
192
|
+
const shelf = {
|
|
193
|
+
b: [(Vh + Vb * K / Q + K * K) / a0, 2 * (K * K - Vh) / a0, (Vh - Vb * K / Q + K * K) / a0],
|
|
194
|
+
a: [1, 2 * (K * K - 1) / a0, (1 - K / Q + K * K) / a0]
|
|
195
|
+
};
|
|
196
|
+
const f1 = 38.13547087602444, Q1 = 0.5003270373238773, K1 = Math.tan(Math.PI * f1 / fs), a1 = 1 + K1 / Q1 + K1 * K1;
|
|
197
|
+
const high = { b: [1, -2, 1], a: [1, 2 * (K1 * K1 - 1) / a1, (1 - K1 / Q1 + K1 * K1) / a1] };
|
|
198
|
+
return [shelf, high];
|
|
199
|
+
}
|
|
200
|
+
function filter(x, stages) {
|
|
201
|
+
let cur = x;
|
|
202
|
+
for (const { b, a } of stages) {
|
|
203
|
+
const out = new Float32Array(cur.length);
|
|
204
|
+
let x1 = 0, x2 = 0, y1 = 0, y2 = 0;
|
|
205
|
+
for (let i = 0; i < cur.length; i++) {
|
|
206
|
+
const xi = cur[i], y = b[0] * xi + b[1] * x1 + b[2] * x2 - a[1] * y1 - a[2] * y2;
|
|
207
|
+
x2 = x1;
|
|
208
|
+
x1 = xi;
|
|
209
|
+
y2 = y1;
|
|
210
|
+
y1 = y;
|
|
211
|
+
out[i] = y;
|
|
212
|
+
}
|
|
213
|
+
cur = out;
|
|
214
|
+
}
|
|
215
|
+
return cur;
|
|
216
|
+
}
|
|
217
|
+
var toDb = (x) => Math.round(200 * Math.log10(Math.max(x, 1e-6))) / 10;
|
|
218
|
+
var TP_HALF = 12;
|
|
219
|
+
var TP_BLOCK = 32;
|
|
220
|
+
var TP_KERNELS = [0.25, 0.5, 0.75].map((p) => Float64Array.from({ length: 2 * TP_HALF }, (_, j) => {
|
|
221
|
+
const k = j - TP_HALF + 1, x = k - p, s = x === 0 ? 1 : Math.sin(Math.PI * x) / (Math.PI * x);
|
|
222
|
+
return s * (0.5 + 0.5 * Math.cos(Math.PI * x / (TP_HALF + 1)));
|
|
223
|
+
}));
|
|
224
|
+
var TP_GAIN = Math.max(...TP_KERNELS.map((k) => k.reduce((a, v) => a + Math.abs(v), 0)));
|
|
225
|
+
function truePeak(chans) {
|
|
226
|
+
let peak = 0;
|
|
227
|
+
for (const c of chans) {
|
|
228
|
+
const n = c.length;
|
|
229
|
+
const blocks = Math.ceil(n / TP_BLOCK), blockMax = new Float32Array(blocks + 2);
|
|
230
|
+
for (let i = 0; i < n; i++) {
|
|
231
|
+
const a = Math.abs(c[i]), b = (i / TP_BLOCK | 0) + 1;
|
|
232
|
+
if (a > blockMax[b]) blockMax[b] = a;
|
|
233
|
+
if (a > peak) peak = a;
|
|
234
|
+
}
|
|
235
|
+
const padded = new Float32Array(n + 2 * TP_HALF);
|
|
236
|
+
padded.set(c, TP_HALF);
|
|
237
|
+
for (let i = 0; i < n; i++) {
|
|
238
|
+
const b = (i / TP_BLOCK | 0) + 1;
|
|
239
|
+
const local = Math.max(blockMax[b - 1], blockMax[b], blockMax[b + 1]);
|
|
240
|
+
if (local * TP_GAIN <= peak) continue;
|
|
241
|
+
for (let pi = 0; pi < 3; pi++) {
|
|
242
|
+
const k = TP_KERNELS[pi];
|
|
243
|
+
let v = 0;
|
|
244
|
+
for (let j = 0; j < 2 * TP_HALF; j++) v += padded[i + 1 + j] * k[j];
|
|
245
|
+
const a = Math.abs(v);
|
|
246
|
+
if (a > peak) peak = a;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return peak;
|
|
251
|
+
}
|
|
252
|
+
function measureLoudness(channels, sampleRate, opts = {}) {
|
|
253
|
+
let samplePeak = 0, clipped = 0;
|
|
254
|
+
for (const c of channels) for (let i = 0; i < c.length; i++) {
|
|
255
|
+
const a = Math.abs(c[i]);
|
|
256
|
+
if (a > samplePeak) samplePeak = a;
|
|
257
|
+
if (a >= 1) clipped++;
|
|
258
|
+
}
|
|
259
|
+
const stages = kWeighting(sampleRate);
|
|
260
|
+
const weighted = channels.map((c) => filter(c, stages));
|
|
261
|
+
const block = Math.round(0.4 * sampleRate), hop = Math.round(0.1 * sampleRate);
|
|
262
|
+
const energies = [];
|
|
263
|
+
for (let s = 0; s + block <= (weighted[0]?.length ?? 0); s += hop) {
|
|
264
|
+
let sum = 0;
|
|
265
|
+
for (const w of weighted) {
|
|
266
|
+
let z = 0;
|
|
267
|
+
for (let i = s; i < s + block; i++) z += w[i] * w[i];
|
|
268
|
+
sum += z / block;
|
|
269
|
+
}
|
|
270
|
+
energies.push(sum);
|
|
271
|
+
}
|
|
272
|
+
const lufs = (e) => -0.691 + 10 * Math.log10(e);
|
|
273
|
+
const absolute = energies.filter((e) => e > 0 && lufs(e) > -70);
|
|
274
|
+
let integrated = null;
|
|
275
|
+
if (absolute.length) {
|
|
276
|
+
const relative = lufs(absolute.reduce((a, b) => a + b, 0) / absolute.length) - 10;
|
|
277
|
+
const gated = absolute.filter((e) => lufs(e) > relative);
|
|
278
|
+
if (gated.length) integrated = Math.round(10 * lufs(gated.reduce((a, b) => a + b, 0) / gated.length)) / 10;
|
|
279
|
+
}
|
|
280
|
+
return { integratedLufs: integrated, truePeakDb: opts.truePeak === false ? null : toDb(truePeak(channels)), samplePeakDb: toDb(samplePeak), clippedSamples: clipped };
|
|
281
|
+
}
|
|
282
|
+
function findSilences(channels, sampleRate, opts = {}) {
|
|
283
|
+
const min = Math.round((opts.minLength ?? 1) * sampleRate), limit = 10 ** ((opts.thresholdDb ?? -60) / 20);
|
|
284
|
+
const n = channels[0]?.length ?? 0, out = [];
|
|
285
|
+
let start = -1;
|
|
286
|
+
const quiet = (i) => channels.every((c) => Math.abs(c[i]) < limit);
|
|
287
|
+
for (let i = 0; i <= n; i++) {
|
|
288
|
+
const q = i < n && quiet(i);
|
|
289
|
+
if (q && start < 0) start = i;
|
|
290
|
+
if (!q && start >= 0) {
|
|
291
|
+
if (i - start >= min) out.push({ from: Math.round(start / sampleRate * 1e3) / 1e3, to: Math.round(i / sampleRate * 1e3) / 1e3 });
|
|
292
|
+
start = -1;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return out;
|
|
296
|
+
}
|
|
297
|
+
|
|
186
298
|
// src/core/inspectAudio.ts
|
|
187
299
|
var INAUDIBLE_DB = -50;
|
|
188
300
|
var db = (x) => Math.round(10 * Math.max(-120, 20 * Math.log10(x))) / 10;
|
|
@@ -208,7 +320,19 @@ function measure(chans, sampleRate) {
|
|
|
208
320
|
}
|
|
209
321
|
function analyzeAudio(mix, sources, stats, movieDuration, opts = {}) {
|
|
210
322
|
if (!mix) {
|
|
211
|
-
return {
|
|
323
|
+
return {
|
|
324
|
+
duration: movieDuration,
|
|
325
|
+
sampleRate: 0,
|
|
326
|
+
peakDb: -120,
|
|
327
|
+
peakAt: 0,
|
|
328
|
+
sources: [],
|
|
329
|
+
windows: [],
|
|
330
|
+
loudness: { integratedLufs: null, truePeakDb: -120, clippedSamples: 0, silences: [] },
|
|
331
|
+
scenes: [],
|
|
332
|
+
cues: [],
|
|
333
|
+
notes: [],
|
|
334
|
+
issues: ['no audio: the movie has no audio layers (sound effects need no files: { type: "audio", sfx: "pop", at: 1 })']
|
|
335
|
+
};
|
|
212
336
|
}
|
|
213
337
|
const sr = mix.sampleRate;
|
|
214
338
|
const chans = Array.from({ length: mix.numberOfChannels }, (_, c) => mix.getChannelData(c));
|
|
@@ -266,6 +390,24 @@ function analyzeAudio(mix, sources, stats, movieDuration, opts = {}) {
|
|
|
266
390
|
}
|
|
267
391
|
return { layer, source, start: s.start, end: s.end, peakDb, sound };
|
|
268
392
|
});
|
|
393
|
+
const whole = measureLoudness(chans, sr);
|
|
394
|
+
const silences = findSilences(chans, sr, { minLength: 1 });
|
|
395
|
+
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`);
|
|
396
|
+
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`);
|
|
397
|
+
const notes = [];
|
|
398
|
+
if (whole.integratedLufs !== null) {
|
|
399
|
+
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`);
|
|
400
|
+
else if (whole.integratedLufs > LOUDNESS_NOTES.loudAbove) notes.push(`the mix is loud: ${whole.integratedLufs} LUFS (typical web video \u221214 to \u221216)`);
|
|
401
|
+
}
|
|
402
|
+
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)`);
|
|
403
|
+
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)`);
|
|
404
|
+
if (silences.length > 3) notes.push(`and ${silences.length - 3} more silent stretch(es) (loudness.silences lists them all)`);
|
|
405
|
+
const sceneList = (opts.scenes && opts.scenes.length ? opts.scenes : [{ name: "movie", start: 0, end: movieDuration }]).map((sc) => {
|
|
406
|
+
const a = Math.max(0, Math.round(sc.start * sr)), b = Math.min(mix.length, Math.round(sc.end * sr));
|
|
407
|
+
const part = chans.map((c) => c.subarray(a, Math.max(a, b)));
|
|
408
|
+
const m = measureLoudness(part, sr, { truePeak: false });
|
|
409
|
+
return { name: sc.name, from: sc.start, to: sc.end, lufs: m.integratedLufs, peakDb: m.samplePeakDb, silent: m.samplePeakDb < -60 };
|
|
410
|
+
});
|
|
269
411
|
return {
|
|
270
412
|
duration: movieDuration,
|
|
271
413
|
sampleRate: sr,
|
|
@@ -273,8 +415,158 @@ function analyzeAudio(mix, sources, stats, movieDuration, opts = {}) {
|
|
|
273
415
|
peakAt: stats?.peakAt ?? 0,
|
|
274
416
|
sources: list,
|
|
275
417
|
windows: windows2,
|
|
276
|
-
issues
|
|
418
|
+
issues,
|
|
419
|
+
loudness: { integratedLufs: whole.integratedLufs, truePeakDb: whole.truePeakDb ?? -120, clippedSamples: whole.clippedSamples, silences },
|
|
420
|
+
scenes: sceneList,
|
|
421
|
+
cues: list.map((s) => ({ t: s.start, name: `${s.layer} (${s.source})` })),
|
|
422
|
+
notes
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// src/text/fonts.ts
|
|
427
|
+
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"]);
|
|
428
|
+
function fontAvailable(family2) {
|
|
429
|
+
const f = family2.trim().replace(/^["']|["']$/g, "");
|
|
430
|
+
if (!f || GENERIC.has(f.toLowerCase())) return true;
|
|
431
|
+
const g = document.createElement("canvas").getContext("2d");
|
|
432
|
+
if (!g) return true;
|
|
433
|
+
const sample = "mmmmmmmmmmlliWWWW 0123 \u3042\u3044\u3046";
|
|
434
|
+
const width = (stack) => {
|
|
435
|
+
g.font = `72px ${stack}`;
|
|
436
|
+
return g.measureText(sample).width;
|
|
277
437
|
};
|
|
438
|
+
const q = JSON.stringify(f);
|
|
439
|
+
return width(`${q}, monospace`) !== width("monospace") || width(`${q}, serif`) !== width("serif");
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// src/core/inspectFonts.ts
|
|
443
|
+
function inspectFonts(root) {
|
|
444
|
+
const failed = /* @__PURE__ */ new Set();
|
|
445
|
+
if (typeof document !== "undefined" && document.fonts) document.fonts.forEach((face) => {
|
|
446
|
+
if (face.status === "error") failed.add(face.family.replace(/^["']|["']$/g, ""));
|
|
447
|
+
});
|
|
448
|
+
const missing = [];
|
|
449
|
+
const used = /* @__PURE__ */ new Set();
|
|
450
|
+
const walk2 = (seq, index) => {
|
|
451
|
+
const spec = seq.spec;
|
|
452
|
+
if (spec.type === "text") {
|
|
453
|
+
const raw = spec.style?.fontFamily;
|
|
454
|
+
const family2 = Array.isArray(raw) ? raw.join(", ") : typeof raw === "string" ? raw : "";
|
|
455
|
+
const list = family2.split(",").map((f) => f.trim()).filter(Boolean);
|
|
456
|
+
for (const f of list) used.add(f.replace(/^["']|["']$/g, "").toLowerCase());
|
|
457
|
+
if (list.length && !list.some(fontAvailable) && !list.some((f) => failed.has(f.replace(/^["']|["']$/g, "")))) missing.push({ layer: spec.name ?? `text#${index}`, family: family2 });
|
|
458
|
+
}
|
|
459
|
+
if (seq.maskSequence) walk2(seq.maskSequence, index);
|
|
460
|
+
(seq._children ?? []).forEach((child, i) => walk2(child, i));
|
|
461
|
+
};
|
|
462
|
+
walk2(root, 0);
|
|
463
|
+
const all = [...failed];
|
|
464
|
+
return { missing, failed: all.filter((f) => used.has(f.toLowerCase())), failedUnused: all.filter((f) => !used.has(f.toLowerCase())) };
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// src/core/scenes.ts
|
|
468
|
+
function namedScenes(rows) {
|
|
469
|
+
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 }));
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// src/core/review.ts
|
|
473
|
+
function sampleFrames({ totalFrames, frameRate, scenes = [], step = 0.25, cap = 240 }) {
|
|
474
|
+
const last = totalFrames;
|
|
475
|
+
const clamp2 = (f) => Math.max(0, Math.min(last, Math.round(f)));
|
|
476
|
+
const edges = /* @__PURE__ */ new Set([0, last]);
|
|
477
|
+
for (const s of scenes) {
|
|
478
|
+
edges.add(clamp2(s.start * frameRate));
|
|
479
|
+
edges.add(clamp2(s.end * frameRate - 1));
|
|
480
|
+
}
|
|
481
|
+
const room = Math.max(0, cap - edges.size);
|
|
482
|
+
const seconds = totalFrames / frameRate;
|
|
483
|
+
const wanted = Math.floor(seconds / step) + 1;
|
|
484
|
+
const stride = wanted <= room ? step : seconds / Math.max(1, room);
|
|
485
|
+
const frames = new Set(edges);
|
|
486
|
+
for (let t = stride; frames.size < cap && t < seconds; t += stride) frames.add(clamp2(t * frameRate));
|
|
487
|
+
return [...frames].sort((a, b) => a - b);
|
|
488
|
+
}
|
|
489
|
+
var AT_HELP = "use seconds (3.5), a percentage (50%), a frame (f120) or a layer's start / mid / end (name@end)";
|
|
490
|
+
function resolveAtList(list, ctx) {
|
|
491
|
+
const { frameRate: fps, totalFrames, duration, rows } = ctx;
|
|
492
|
+
const out = [], used = /* @__PURE__ */ new Set();
|
|
493
|
+
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]);
|
|
494
|
+
for (const raw of String(list).split(",").map((s) => s.trim()).filter(Boolean)) {
|
|
495
|
+
let frame, label, m;
|
|
496
|
+
if (m = raw.match(/^(.+)@(start|mid|end)$/)) {
|
|
497
|
+
const row = named.find((r) => r.name === m[1]);
|
|
498
|
+
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" : ""})`);
|
|
499
|
+
const t = m[2] === "start" ? row.start : m[2] === "mid" ? (row.start + row.end) / 2 : row.end - 1 / fps;
|
|
500
|
+
frame = Math.round(t * fps);
|
|
501
|
+
label = `${m[1]}-${m[2]}`;
|
|
502
|
+
} else if (m = raw.match(/^(\d+(?:\.\d+)?)%$/)) {
|
|
503
|
+
frame = Math.round(Number(m[1]) / 100 * totalFrames);
|
|
504
|
+
label = `${m[1]}pct`;
|
|
505
|
+
} else if (m = raw.match(/^f(\d+)$/i)) {
|
|
506
|
+
frame = Number(m[1]);
|
|
507
|
+
label = `f${m[1]}`;
|
|
508
|
+
} else if (/^\d+(\.\d+)?$/.test(raw)) {
|
|
509
|
+
frame = Math.round(Number(raw) * fps);
|
|
510
|
+
label = `${Number(raw).toFixed(2)}s`;
|
|
511
|
+
} else throw new Error(`--at: cannot read "${raw}": ${AT_HELP}`);
|
|
512
|
+
if (frame > totalFrames || frame < 0) throw new Error(`--at "${raw}" is past the end: the movie is ${duration} s (${totalFrames} frames)`);
|
|
513
|
+
label = label.replace(/[\\/:*?"<>|]+/g, "-");
|
|
514
|
+
let unique = label, n = 2;
|
|
515
|
+
while (used.has(unique)) unique = `${label}-${n++}`;
|
|
516
|
+
used.add(unique);
|
|
517
|
+
out.push({ label: unique, frame });
|
|
518
|
+
}
|
|
519
|
+
return out;
|
|
520
|
+
}
|
|
521
|
+
function groupIssues(perFrame) {
|
|
522
|
+
const groups = /* @__PURE__ */ new Map();
|
|
523
|
+
for (const { frame, issues } of perFrame) {
|
|
524
|
+
for (const message of issues) {
|
|
525
|
+
const key = message.replace(/-?\d+(\.\d+)?/g, "#");
|
|
526
|
+
const g = groups.get(key);
|
|
527
|
+
if (g) {
|
|
528
|
+
g.count++;
|
|
529
|
+
g.lastFrame = frame;
|
|
530
|
+
} else groups.set(key, { message, count: 1, firstFrame: frame, lastFrame: frame });
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
return [...groups.values()];
|
|
534
|
+
}
|
|
535
|
+
function splitIssues(groups, strict = false) {
|
|
536
|
+
if (strict) return { problems: groups, review: [] };
|
|
537
|
+
const overlap = (g) => /\boverlap\b/.test(g.message);
|
|
538
|
+
return { problems: groups.filter((g) => !overlap(g)), review: groups.filter(overlap) };
|
|
539
|
+
}
|
|
540
|
+
async function reviewMovie(movie, opts = {}) {
|
|
541
|
+
const rows = movie.timelineData().rows;
|
|
542
|
+
const at = opts.at ? resolveAtList(opts.at, { frameRate: movie.frameRate, totalFrames: movie.totalFrames, duration: movie.duration, rows }) : [];
|
|
543
|
+
const frames = sampleFrames({ totalFrames: movie.totalFrames, frameRate: movie.frameRate, scenes: namedScenes(rows) });
|
|
544
|
+
const perFrame = [];
|
|
545
|
+
for (const frame of frames) {
|
|
546
|
+
const r = await movie.inspect(frame, { layers: "none" });
|
|
547
|
+
if (r.issues.length) perFrame.push({ frame, issues: r.issues });
|
|
548
|
+
}
|
|
549
|
+
const grouped = splitIssues(groupIssues(perFrame), !!opts.strict);
|
|
550
|
+
const fonts = movie.inspectFonts();
|
|
551
|
+
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 });
|
|
552
|
+
for (const m of fonts.missing) {
|
|
553
|
+
(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 });
|
|
554
|
+
}
|
|
555
|
+
return { frames: frames.length, problems: grouped.problems, review: grouped.review, fonts, audio: movie.audioBuffer ? movie.inspectAudio() : null, at };
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// src/core/onion.ts
|
|
559
|
+
function onionTimes(from, to, count) {
|
|
560
|
+
if (count <= 1) return [from];
|
|
561
|
+
return Array.from({ length: count }, (_, i) => Math.round((from + (to - from) * i / (count - 1)) * 1e6) / 1e6);
|
|
562
|
+
}
|
|
563
|
+
function onionAlphas(count, ramp = 0.25) {
|
|
564
|
+
const weights = Array.from({ length: count }, (_, i) => count === 1 ? 1 : ramp + (1 - ramp) * (i / (count - 1)));
|
|
565
|
+
let sum = 0;
|
|
566
|
+
return weights.map((w) => {
|
|
567
|
+
sum += w;
|
|
568
|
+
return w / sum;
|
|
569
|
+
});
|
|
278
570
|
}
|
|
279
571
|
|
|
280
572
|
// src/core/motionBlur.ts
|
|
@@ -293,6 +585,57 @@ function blurTimes(frame, frameRate, mb, duration) {
|
|
|
293
585
|
return Array.from({ length: mb.samples }, (_, k) => Math.min(Math.max(t + ((k + 0.5) / mb.samples - 0.5) * width, 0), duration));
|
|
294
586
|
}
|
|
295
587
|
|
|
588
|
+
// src/core/renderRange.ts
|
|
589
|
+
function resolveRange(range2, ctx) {
|
|
590
|
+
const { frameRate: fps, totalFrames, duration } = ctx;
|
|
591
|
+
if (range2 === void 0) return { fromFrame: 0, toFrame: totalFrames, fromSec: 0, toSec: duration, partial: false };
|
|
592
|
+
let from, to;
|
|
593
|
+
if (typeof range2 === "string") {
|
|
594
|
+
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]);
|
|
595
|
+
const row = rows.find((r) => r.name === range2);
|
|
596
|
+
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" : ""})`);
|
|
597
|
+
from = row.start;
|
|
598
|
+
to = row.end;
|
|
599
|
+
} else {
|
|
600
|
+
if (!Array.isArray(range2) || range2.length !== 2 || !range2.every((n) => typeof n === "number" && Number.isFinite(n))) {
|
|
601
|
+
throw new Error("pixi-effects: render({ range }) must be [from, to] in seconds (numbers) or the name of a top-level layer");
|
|
602
|
+
}
|
|
603
|
+
[from, to] = range2;
|
|
604
|
+
}
|
|
605
|
+
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)`);
|
|
606
|
+
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`);
|
|
607
|
+
const fromFrame = Math.round(from * fps);
|
|
608
|
+
const toFrame = Math.min(Math.round(to * fps), totalFrames);
|
|
609
|
+
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)`);
|
|
610
|
+
const partial = fromFrame > 0 || toFrame < totalFrames;
|
|
611
|
+
return { fromFrame, toFrame, fromSec: fromFrame / fps, toSec: toFrame / fps, partial };
|
|
612
|
+
}
|
|
613
|
+
function sliceChannels(channels, sampleRate, fromSec, toSec, opts = {}) {
|
|
614
|
+
const fade = Math.max(0, Math.round((opts.fade ?? 0.01) * sampleRate));
|
|
615
|
+
const from = Math.round(fromSec * sampleRate), len = Math.max(1, Math.round(toSec * sampleRate) - from);
|
|
616
|
+
return channels.map((src) => {
|
|
617
|
+
const out = new Float32Array(len);
|
|
618
|
+
for (let i = 0; i < len; i++) {
|
|
619
|
+
const j = from + i;
|
|
620
|
+
if (j >= 0 && j < src.length) out[i] = src[j];
|
|
621
|
+
}
|
|
622
|
+
const f = Math.min(fade, len >> 1);
|
|
623
|
+
if (opts.fadeIn !== false) for (let i = 0; i < f; i++) out[i] = out[i] * (i / f);
|
|
624
|
+
if (opts.fadeOut !== false) for (let i = 0; i < f; i++) out[len - 1 - i] = out[len - 1 - i] * (i / f);
|
|
625
|
+
return out;
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
function resolveRenderOptions(o) {
|
|
629
|
+
const draft = !!o.draft;
|
|
630
|
+
const scale = o.scale ?? (draft ? 0.5 : 1);
|
|
631
|
+
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})`);
|
|
632
|
+
return {
|
|
633
|
+
scale,
|
|
634
|
+
bitrate: o.video?.bitrate ?? (draft ? "low" : void 0),
|
|
635
|
+
motionBlur: o.motionBlur !== void 0 ? o.motionBlur : draft ? false : void 0
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
|
|
296
639
|
// src/core/Renderer.ts
|
|
297
640
|
var VIDEO_CODEC_BY_FORMAT = {
|
|
298
641
|
mp4: "avc",
|
|
@@ -347,13 +690,22 @@ function makeOutputFormat(name) {
|
|
|
347
690
|
return new MkvOutputFormat();
|
|
348
691
|
}
|
|
349
692
|
}
|
|
693
|
+
function sliceAudio(buffer, fromSec, toSec, fadeIn, fadeOut) {
|
|
694
|
+
const channels = Array.from({ length: buffer.numberOfChannels }, (_, c) => buffer.getChannelData(c));
|
|
695
|
+
const cut = sliceChannels(channels, buffer.sampleRate, fromSec, toSec, { fadeIn, fadeOut });
|
|
696
|
+
const out = new AudioBuffer({ length: cut[0].length, numberOfChannels: cut.length, sampleRate: buffer.sampleRate });
|
|
697
|
+
cut.forEach((data, c) => out.copyToChannel(data, c));
|
|
698
|
+
return out;
|
|
699
|
+
}
|
|
350
700
|
async function exportFrames(movie, options = {}) {
|
|
351
701
|
const fmt = options.format ?? "mp4";
|
|
702
|
+
const eff = resolveRenderOptions(options);
|
|
703
|
+
const range2 = resolveRange(options.range, { frameRate: movie.frameRate, totalFrames: movie.totalFrames, duration: movie.duration, rows: typeof options.range === "string" ? movie.timelineData().rows : void 0 });
|
|
352
704
|
const opts = {
|
|
353
705
|
format: fmt,
|
|
354
706
|
video: {
|
|
355
707
|
codec: options.video?.codec ?? VIDEO_CODEC_BY_FORMAT[fmt],
|
|
356
|
-
bitrate: qualityMap[
|
|
708
|
+
bitrate: qualityMap[eff.bitrate ?? "high"] ?? QUALITY_HIGH
|
|
357
709
|
},
|
|
358
710
|
audio: {
|
|
359
711
|
codec: options.audio?.codec ?? AUDIO_CODEC_BY_FORMAT[fmt],
|
|
@@ -361,27 +713,30 @@ async function exportFrames(movie, options = {}) {
|
|
|
361
713
|
}
|
|
362
714
|
};
|
|
363
715
|
const container = makeOutputFormat(opts.format);
|
|
364
|
-
|
|
716
|
+
const audio = movie.audioBuffer && range2.partial ? sliceAudio(movie.audioBuffer, range2.fromSec, range2.toSec, range2.fromFrame > 0, range2.toFrame < movie.totalFrames) : movie.audioBuffer;
|
|
717
|
+
if (audio) opts.audio.codec = await chooseAudioCodec(fmt, options.audio?.codec, container, audio, opts.audio.bitrate);
|
|
365
718
|
const output = new Output({
|
|
366
719
|
format: container,
|
|
367
720
|
target: new BufferTarget()
|
|
368
721
|
});
|
|
369
|
-
const mb =
|
|
722
|
+
const mb = eff.motionBlur !== void 0 ? resolveMotionBlur(eff.motionBlur, "movie.render()") : movie.motionBlur ?? null;
|
|
370
723
|
const stage = movie.app.canvas;
|
|
371
724
|
const blurCanvas = mb ? Object.assign(document.createElement("canvas"), { width: stage.width, height: stage.height }) : null;
|
|
372
|
-
const
|
|
725
|
+
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;
|
|
726
|
+
const smallCtx = small?.getContext("2d") ?? null;
|
|
727
|
+
const canvasSource = new CanvasSource(small ?? blurCanvas ?? stage, {
|
|
373
728
|
codec: opts.video.codec,
|
|
374
729
|
bitrate: opts.video.bitrate
|
|
375
730
|
});
|
|
376
731
|
output.addVideoTrack(canvasSource, { frameRate: movie.frameRate });
|
|
377
|
-
if (
|
|
732
|
+
if (audio) {
|
|
378
733
|
const audioSource = new AudioBufferSource({
|
|
379
734
|
codec: opts.audio.codec,
|
|
380
735
|
bitrate: opts.audio.bitrate
|
|
381
|
-
}, { startTimestamp: opts.audio.codec === "aac" ? -2112 /
|
|
736
|
+
}, { startTimestamp: opts.audio.codec === "aac" ? -2112 / audio.sampleRate : 0 });
|
|
382
737
|
output.addAudioTrack(audioSource);
|
|
383
738
|
await output.start();
|
|
384
|
-
await audioSource.add(
|
|
739
|
+
await audioSource.add(audio);
|
|
385
740
|
await audioSource.close();
|
|
386
741
|
} else {
|
|
387
742
|
await output.start();
|
|
@@ -389,18 +744,21 @@ async function exportFrames(movie, options = {}) {
|
|
|
389
744
|
movie.app.ticker.stop();
|
|
390
745
|
const keyframeIntervalFrames = Math.max(1, Math.round(2 * movie.frameRate));
|
|
391
746
|
try {
|
|
392
|
-
|
|
747
|
+
const count = Math.max(1, range2.toFrame - range2.fromFrame);
|
|
748
|
+
for (let frame = range2.fromFrame; frame <= range2.toFrame; frame++) {
|
|
393
749
|
if (mb && blurCanvas) await movie._exposeFrame(frame, mb, blurCanvas, false);
|
|
394
750
|
else await movie.gotoFrame(frame, true);
|
|
395
|
-
|
|
751
|
+
if (small && smallCtx) smallCtx.drawImage(blurCanvas ?? stage, 0, 0, small.width, small.height);
|
|
752
|
+
const rel = frame - range2.fromFrame;
|
|
753
|
+
const isKey = rel === 0 || rel % keyframeIntervalFrames === 0;
|
|
396
754
|
const addOpts = isKey ? { keyFrame: true } : void 0;
|
|
397
|
-
await canvasSource.add(
|
|
398
|
-
const progress = Math.floor(
|
|
755
|
+
await canvasSource.add(rel / movie.frameRate, 1 / movie.frameRate, addOpts);
|
|
756
|
+
const progress = Math.floor(rel / count * 100);
|
|
399
757
|
movie.emit("progress", { progress, frame, totalFrames: movie.totalFrames });
|
|
400
758
|
}
|
|
401
759
|
await canvasSource.close();
|
|
402
760
|
await output.finalize();
|
|
403
|
-
if (mb) await movie.gotoFrame(
|
|
761
|
+
if (mb) await movie.gotoFrame(range2.toFrame, true);
|
|
404
762
|
return new Blob([output.target.buffer], { type: output.format.mimeType });
|
|
405
763
|
} finally {
|
|
406
764
|
movie.app.ticker.start();
|
|
@@ -1316,7 +1674,8 @@ function groupFamilies(entries) {
|
|
|
1316
1674
|
start: Math.min(...members.map((x) => x.start)),
|
|
1317
1675
|
end: Math.max(...members.map((x) => x.end)),
|
|
1318
1676
|
keys: members.flatMap((x) => x.keys).sort((a, b) => a - b).slice(0, MAX_KEYS),
|
|
1319
|
-
parts: members.map((x) => ({ start: x.start, end: x.end }))
|
|
1677
|
+
parts: members.map((x) => ({ start: x.start, end: x.end })),
|
|
1678
|
+
partNames: members.map((x) => x.name)
|
|
1320
1679
|
});
|
|
1321
1680
|
} else {
|
|
1322
1681
|
out.push(e.row, ...e.inner);
|
|
@@ -2039,6 +2398,37 @@ var Movie = class {
|
|
|
2039
2398
|
})));
|
|
2040
2399
|
return new Blob([buildPdf(pages, { title: opts.title })], { type: "application/pdf" });
|
|
2041
2400
|
}
|
|
2401
|
+
async onionSkin(opts = {}) {
|
|
2402
|
+
warnUnknownOptions("movie.onionSkin()", opts, ["from", "to", "count", "scale", "as", "motionBlur"]);
|
|
2403
|
+
this._requireReady("onionSkin");
|
|
2404
|
+
const from = opts.from ?? 0, to = opts.to ?? this.duration, count = opts.count ?? 8, scale = opts.scale ?? 1;
|
|
2405
|
+
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}`);
|
|
2406
|
+
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}`);
|
|
2407
|
+
if (!(scale > 0 && scale <= 4)) throw new Error(`pixi-effects: movie.onionSkin(): scale must be above 0 and at most 4, got ${scale}`);
|
|
2408
|
+
const mb = this._blurFor(opts.motionBlur, "movie.onionSkin()");
|
|
2409
|
+
const blurred = mb ? this._blurCanvas() : null;
|
|
2410
|
+
const out = document.createElement("canvas");
|
|
2411
|
+
out.width = Math.max(1, Math.round(this.width * scale));
|
|
2412
|
+
out.height = Math.max(1, Math.round(this.height * scale));
|
|
2413
|
+
const g = out.getContext("2d");
|
|
2414
|
+
const alphas = onionAlphas(count);
|
|
2415
|
+
const frames = onionTimes(from, to, count).map((t) => clampFrame(t * this.frameRate, this.totalFrames));
|
|
2416
|
+
const back = this.currentFrame, wasAtPoster = this._atPoster;
|
|
2417
|
+
try {
|
|
2418
|
+
for (let i = 0; i < frames.length; i++) {
|
|
2419
|
+
const f = frames[i];
|
|
2420
|
+
if (mb && blurred) await this._quietly(() => this._exposeFrame(f, mb, blurred, true));
|
|
2421
|
+
else await this._quietly(() => this.gotoFrame(f, true));
|
|
2422
|
+
g.globalAlpha = alphas[i];
|
|
2423
|
+
g.drawImage(blurred ?? this.app.canvas, 0, 0, out.width, out.height);
|
|
2424
|
+
}
|
|
2425
|
+
} finally {
|
|
2426
|
+
g.globalAlpha = 1;
|
|
2427
|
+
if (wasAtPoster) await this._showPoster();
|
|
2428
|
+
else await this._quietly(() => this.gotoFrame(back, true));
|
|
2429
|
+
}
|
|
2430
|
+
return encodeCanvas(out, "image/png", opts.as ?? "blob");
|
|
2431
|
+
}
|
|
2042
2432
|
async contactSheet(opts = {}) {
|
|
2043
2433
|
warnUnknownOptions("movie.contactSheet()", opts, ["frames", "times", "count", "columns", "cellWidth", "as", "motionBlur"]);
|
|
2044
2434
|
this._requireReady("contactSheet");
|
|
@@ -2105,10 +2495,40 @@ var Movie = class {
|
|
|
2105
2495
|
* The soundtrack, measured: when every audio layer plays, how loud the mix is over time, and `issues`
|
|
2106
2496
|
* (limiting, inaudible layers, sounds cut off by the end). Use it to CHECK sound you cannot hear.
|
|
2107
2497
|
*/
|
|
2498
|
+
/** 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. */
|
|
2499
|
+
resolveAt(list) {
|
|
2500
|
+
this._requireReady("resolveAt");
|
|
2501
|
+
return resolveAtList(list, { frameRate: this.frameRate, totalFrames: this.totalFrames, duration: this.duration, rows: this.timelineData().rows });
|
|
2502
|
+
}
|
|
2503
|
+
/**
|
|
2504
|
+
* One call that looks at everything `pixi-effects-check` looks at inside the page: layout issues over the whole timeline (every
|
|
2505
|
+
* 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.
|
|
2506
|
+
*/
|
|
2507
|
+
async review(opts = {}) {
|
|
2508
|
+
warnUnknownOptions("movie.review()", opts, ["at", "strict"]);
|
|
2509
|
+
this._requireReady("review");
|
|
2510
|
+
const back = this.currentFrame, wasAtPoster = this._atPoster;
|
|
2511
|
+
try {
|
|
2512
|
+
return await reviewMovie(this, opts);
|
|
2513
|
+
} finally {
|
|
2514
|
+
this.currentFrame = back;
|
|
2515
|
+
if (wasAtPoster) await this._showPoster();
|
|
2516
|
+
else await this._quietly(() => this.gotoFrame(back, true));
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
/**
|
|
2520
|
+
* The fonts of the text layers: `missing` lists layers none of whose `fontFamily` entries is available in this browser (they are
|
|
2521
|
+
* drawn in a fallback font), `failed` lists web fonts (`@font-face`) whose file could not be loaded.
|
|
2522
|
+
*/
|
|
2523
|
+
inspectFonts() {
|
|
2524
|
+
this._requireReady("inspectFonts");
|
|
2525
|
+
return inspectFonts(this._rootSequence);
|
|
2526
|
+
}
|
|
2108
2527
|
inspectAudio(opts = {}) {
|
|
2109
|
-
warnUnknownOptions("movie.inspectAudio()", opts, ["window"]);
|
|
2528
|
+
warnUnknownOptions("movie.inspectAudio()", opts, ["window", "scenes"]);
|
|
2110
2529
|
this._requireReady("inspectAudio");
|
|
2111
|
-
|
|
2530
|
+
const scenes = opts.scenes ?? namedScenes(this.timelineData().rows);
|
|
2531
|
+
return analyzeAudio(this.audioBuffer, this._audioSources, this._mixStats, this.duration, { ...opts, scenes });
|
|
2112
2532
|
}
|
|
2113
2533
|
/**
|
|
2114
2534
|
* Draw the stage now. PixiJS's CullerPlugin only culls inside `app.render()` (the ticker), and we render
|
|
@@ -2376,7 +2796,7 @@ var Movie = class {
|
|
|
2376
2796
|
return this.muted;
|
|
2377
2797
|
}
|
|
2378
2798
|
async render(options) {
|
|
2379
|
-
warnUnknownOptions("movie.render()", options, ["format", "video", "audio", "motionBlur"]);
|
|
2799
|
+
warnUnknownOptions("movie.render()", options, ["format", "video", "audio", "motionBlur", "range", "scale", "draft"]);
|
|
2380
2800
|
try {
|
|
2381
2801
|
return await this._quietly(() => exportFrames(this, options));
|
|
2382
2802
|
} catch (err) {
|
|
@@ -2587,6 +3007,7 @@ function clamp(v, lo, hi) {
|
|
|
2587
3007
|
return Math.max(lo, Math.min(hi, v));
|
|
2588
3008
|
}
|
|
2589
3009
|
function parseEase(name) {
|
|
3010
|
+
checkEase(name, "a preset");
|
|
2590
3011
|
return gsap.parseEase(name);
|
|
2591
3012
|
}
|
|
2592
3013
|
|