pixi-effects 0.16.2 → 0.17.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 +39 -0
- package/README.md +6 -6
- package/ai/CHAT.md +4 -0
- package/ai/SKILL.md +2 -1
- package/ai/chat-template.html +3 -3
- package/ai/reference/cheatsheet.md +4 -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 +162 -18
- package/ai/tools/render.mjs +21 -7
- package/dist/Composition-HYAOYPAX.js +5 -0
- package/dist/{Composition-VJWY2NDL.js.map → Composition-HYAOYPAX.js.map} +1 -1
- package/dist/Composition-LMVEASHF.cjs +14 -0
- package/dist/{Composition-3WWNVRR5.cjs.map → Composition-LMVEASHF.cjs.map} +1 -1
- package/dist/Controller.cjs +9 -3
- package/dist/Controller.cjs.map +1 -1
- package/dist/Controller.d.cts +1 -1
- package/dist/Controller.d.ts +1 -1
- package/dist/Controller.js +9 -3
- package/dist/Controller.js.map +1 -1
- package/dist/{Movie-oA0SUAyb.d.cts → Movie-CoxF2Z7t.d.cts} +82 -2
- package/dist/{Movie-DilBC348.d.ts → Movie-DHJRrUfI.d.ts} +82 -2
- package/dist/Presenter.d.cts +1 -1
- package/dist/Presenter.d.ts +1 -1
- package/dist/{chunk-2ROVCPSL.cjs → chunk-ETAIVOHP.cjs} +32 -27
- package/dist/chunk-ETAIVOHP.cjs.map +1 -0
- package/dist/{chunk-FJA5VUOF.cjs → chunk-LBJRXU7N.cjs} +15 -3
- package/dist/chunk-LBJRXU7N.cjs.map +1 -0
- package/dist/{chunk-ZNKQH356.js → chunk-ORTYBA42.js} +15 -4
- package/dist/chunk-ORTYBA42.js.map +1 -0
- package/dist/{chunk-XTIHKODH.js → chunk-Q3IVBYT7.js} +10 -5
- package/dist/chunk-Q3IVBYT7.js.map +1 -0
- package/dist/index.cjs +351 -41
- 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 +329 -19
- 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 +78 -4
- 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, L as LayerInfo, e as LoaderOption, M as MotionBlurOptions, f as MotionBlurSpec, g as Movie, h as MovieErrorEvent, i as MovieOptions, O as OnionSkinOptions, P as ProgressEvent, R as RenderOptions, S as SnapshotOptions, j as SoundMeasure, k as StopEvent, l as StopImage, m as StopImagesOptions, T as TimelineData, n as TimelineHtmlOptions, o as TimelineRow, p as TimelineSvgOptions, q as TimelineTransition, V as VolumeEvent, r as dismissLoader, s as failLoader, t as resolveLoader } from './Movie-CoxF2Z7t.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, L as LayerInfo, e as LoaderOption, M as MotionBlurOptions, f as MotionBlurSpec, g as Movie, h as MovieErrorEvent, i as MovieOptions, O as OnionSkinOptions, P as ProgressEvent, R as RenderOptions, S as SnapshotOptions, j as SoundMeasure, k as StopEvent, l as StopImage, m as StopImagesOptions, T as TimelineData, n as TimelineHtmlOptions, o as TimelineRow, p as TimelineSvgOptions, q as TimelineTransition, V as VolumeEvent, r as dismissLoader, s as failLoader, t as resolveLoader } from './Movie-DHJRrUfI.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-Q3IVBYT7.js';
|
|
2
|
+
import { ensureFilterLibrary, homeDistance, DEFAULT_FOV, random, noise, flattenSvgPath, rand, DEG, resolveAt, checkEase, collectPropKeys } from './chunk-ORTYBA42.js';
|
|
3
|
+
export { noise, rand, random, registerSequenceType } from './chunk-ORTYBA42.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,69 @@ 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;
|
|
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 walk2 = (seq, index) => {
|
|
450
|
+
const spec = seq.spec;
|
|
451
|
+
if (spec.type === "text") {
|
|
452
|
+
const raw = spec.style?.fontFamily;
|
|
453
|
+
const family2 = Array.isArray(raw) ? raw.join(", ") : typeof raw === "string" ? raw : "";
|
|
454
|
+
const list = family2.split(",").map((f) => f.trim()).filter(Boolean);
|
|
455
|
+
if (list.length && !list.some(fontAvailable) && !list.some((f) => failed.has(f.replace(/^["']|["']$/g, "")))) missing.push({ layer: spec.name ?? `text#${index}`, family: family2 });
|
|
456
|
+
}
|
|
457
|
+
if (seq.maskSequence) walk2(seq.maskSequence, index);
|
|
458
|
+
(seq._children ?? []).forEach((child, i) => walk2(child, i));
|
|
277
459
|
};
|
|
460
|
+
walk2(root, 0);
|
|
461
|
+
return { missing, failed: [...failed] };
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// src/core/scenes.ts
|
|
465
|
+
function namedScenes(rows) {
|
|
466
|
+
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 }));
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// src/core/onion.ts
|
|
470
|
+
function onionTimes(from, to, count) {
|
|
471
|
+
if (count <= 1) return [from];
|
|
472
|
+
return Array.from({ length: count }, (_, i) => Math.round((from + (to - from) * i / (count - 1)) * 1e6) / 1e6);
|
|
473
|
+
}
|
|
474
|
+
function onionAlphas(count, ramp = 0.25) {
|
|
475
|
+
const weights = Array.from({ length: count }, (_, i) => count === 1 ? 1 : ramp + (1 - ramp) * (i / (count - 1)));
|
|
476
|
+
let sum = 0;
|
|
477
|
+
return weights.map((w) => {
|
|
478
|
+
sum += w;
|
|
479
|
+
return w / sum;
|
|
480
|
+
});
|
|
278
481
|
}
|
|
279
482
|
|
|
280
483
|
// src/core/motionBlur.ts
|
|
@@ -293,6 +496,57 @@ function blurTimes(frame, frameRate, mb, duration) {
|
|
|
293
496
|
return Array.from({ length: mb.samples }, (_, k) => Math.min(Math.max(t + ((k + 0.5) / mb.samples - 0.5) * width, 0), duration));
|
|
294
497
|
}
|
|
295
498
|
|
|
499
|
+
// src/core/renderRange.ts
|
|
500
|
+
function resolveRange(range2, ctx) {
|
|
501
|
+
const { frameRate: fps, totalFrames, duration } = ctx;
|
|
502
|
+
if (range2 === void 0) return { fromFrame: 0, toFrame: totalFrames, fromSec: 0, toSec: duration, partial: false };
|
|
503
|
+
let from, to;
|
|
504
|
+
if (typeof range2 === "string") {
|
|
505
|
+
const rows = (ctx.rows ?? []).filter((r) => r.depth === 0);
|
|
506
|
+
const row = rows.find((r) => r.name === range2);
|
|
507
|
+
if (!row) throw new Error(`pixi-effects: render({ range: "${range2}" }): no layer named "${range2}" at the top level (names: ${rows.map((r) => r.name).join(", ") || "none"})`);
|
|
508
|
+
from = row.start;
|
|
509
|
+
to = row.end;
|
|
510
|
+
} else {
|
|
511
|
+
if (!Array.isArray(range2) || range2.length !== 2 || !range2.every((n) => typeof n === "number" && Number.isFinite(n))) {
|
|
512
|
+
throw new Error("pixi-effects: render({ range }) must be [from, to] in seconds (numbers) or the name of a top-level layer");
|
|
513
|
+
}
|
|
514
|
+
[from, to] = range2;
|
|
515
|
+
}
|
|
516
|
+
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)`);
|
|
517
|
+
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`);
|
|
518
|
+
const fromFrame = Math.round(from * fps);
|
|
519
|
+
const toFrame = Math.min(Math.round(to * fps), totalFrames);
|
|
520
|
+
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)`);
|
|
521
|
+
const partial = fromFrame > 0 || toFrame < totalFrames;
|
|
522
|
+
return { fromFrame, toFrame, fromSec: fromFrame / fps, toSec: toFrame / fps, partial };
|
|
523
|
+
}
|
|
524
|
+
function sliceChannels(channels, sampleRate, fromSec, toSec, opts = {}) {
|
|
525
|
+
const fade = Math.max(0, Math.round((opts.fade ?? 0.01) * sampleRate));
|
|
526
|
+
const from = Math.round(fromSec * sampleRate), len = Math.max(1, Math.round(toSec * sampleRate) - from);
|
|
527
|
+
return channels.map((src) => {
|
|
528
|
+
const out = new Float32Array(len);
|
|
529
|
+
for (let i = 0; i < len; i++) {
|
|
530
|
+
const j = from + i;
|
|
531
|
+
if (j >= 0 && j < src.length) out[i] = src[j];
|
|
532
|
+
}
|
|
533
|
+
const f = Math.min(fade, len >> 1);
|
|
534
|
+
if (opts.fadeIn !== false) for (let i = 0; i < f; i++) out[i] = out[i] * (i / f);
|
|
535
|
+
if (opts.fadeOut !== false) for (let i = 0; i < f; i++) out[len - 1 - i] = out[len - 1 - i] * (i / f);
|
|
536
|
+
return out;
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
function resolveRenderOptions(o) {
|
|
540
|
+
const draft = !!o.draft;
|
|
541
|
+
const scale = o.scale ?? (draft ? 0.5 : 1);
|
|
542
|
+
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})`);
|
|
543
|
+
return {
|
|
544
|
+
scale,
|
|
545
|
+
bitrate: o.video?.bitrate ?? (draft ? "low" : void 0),
|
|
546
|
+
motionBlur: o.motionBlur !== void 0 ? o.motionBlur : draft ? false : void 0
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
|
|
296
550
|
// src/core/Renderer.ts
|
|
297
551
|
var VIDEO_CODEC_BY_FORMAT = {
|
|
298
552
|
mp4: "avc",
|
|
@@ -347,13 +601,22 @@ function makeOutputFormat(name) {
|
|
|
347
601
|
return new MkvOutputFormat();
|
|
348
602
|
}
|
|
349
603
|
}
|
|
604
|
+
function sliceAudio(buffer, fromSec, toSec, fadeIn, fadeOut) {
|
|
605
|
+
const channels = Array.from({ length: buffer.numberOfChannels }, (_, c) => buffer.getChannelData(c));
|
|
606
|
+
const cut = sliceChannels(channels, buffer.sampleRate, fromSec, toSec, { fadeIn, fadeOut });
|
|
607
|
+
const out = new AudioBuffer({ length: cut[0].length, numberOfChannels: cut.length, sampleRate: buffer.sampleRate });
|
|
608
|
+
cut.forEach((data, c) => out.copyToChannel(data, c));
|
|
609
|
+
return out;
|
|
610
|
+
}
|
|
350
611
|
async function exportFrames(movie, options = {}) {
|
|
351
612
|
const fmt = options.format ?? "mp4";
|
|
613
|
+
const eff = resolveRenderOptions(options);
|
|
614
|
+
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
615
|
const opts = {
|
|
353
616
|
format: fmt,
|
|
354
617
|
video: {
|
|
355
618
|
codec: options.video?.codec ?? VIDEO_CODEC_BY_FORMAT[fmt],
|
|
356
|
-
bitrate: qualityMap[
|
|
619
|
+
bitrate: qualityMap[eff.bitrate ?? "high"] ?? QUALITY_HIGH
|
|
357
620
|
},
|
|
358
621
|
audio: {
|
|
359
622
|
codec: options.audio?.codec ?? AUDIO_CODEC_BY_FORMAT[fmt],
|
|
@@ -361,27 +624,30 @@ async function exportFrames(movie, options = {}) {
|
|
|
361
624
|
}
|
|
362
625
|
};
|
|
363
626
|
const container = makeOutputFormat(opts.format);
|
|
364
|
-
|
|
627
|
+
const audio = movie.audioBuffer && range2.partial ? sliceAudio(movie.audioBuffer, range2.fromSec, range2.toSec, range2.fromFrame > 0, range2.toFrame < movie.totalFrames) : movie.audioBuffer;
|
|
628
|
+
if (audio) opts.audio.codec = await chooseAudioCodec(fmt, options.audio?.codec, container, audio, opts.audio.bitrate);
|
|
365
629
|
const output = new Output({
|
|
366
630
|
format: container,
|
|
367
631
|
target: new BufferTarget()
|
|
368
632
|
});
|
|
369
|
-
const mb =
|
|
633
|
+
const mb = eff.motionBlur !== void 0 ? resolveMotionBlur(eff.motionBlur, "movie.render()") : movie.motionBlur ?? null;
|
|
370
634
|
const stage = movie.app.canvas;
|
|
371
635
|
const blurCanvas = mb ? Object.assign(document.createElement("canvas"), { width: stage.width, height: stage.height }) : null;
|
|
372
|
-
const
|
|
636
|
+
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;
|
|
637
|
+
const smallCtx = small?.getContext("2d") ?? null;
|
|
638
|
+
const canvasSource = new CanvasSource(small ?? blurCanvas ?? stage, {
|
|
373
639
|
codec: opts.video.codec,
|
|
374
640
|
bitrate: opts.video.bitrate
|
|
375
641
|
});
|
|
376
642
|
output.addVideoTrack(canvasSource, { frameRate: movie.frameRate });
|
|
377
|
-
if (
|
|
643
|
+
if (audio) {
|
|
378
644
|
const audioSource = new AudioBufferSource({
|
|
379
645
|
codec: opts.audio.codec,
|
|
380
646
|
bitrate: opts.audio.bitrate
|
|
381
|
-
}, { startTimestamp: opts.audio.codec === "aac" ? -2112 /
|
|
647
|
+
}, { startTimestamp: opts.audio.codec === "aac" ? -2112 / audio.sampleRate : 0 });
|
|
382
648
|
output.addAudioTrack(audioSource);
|
|
383
649
|
await output.start();
|
|
384
|
-
await audioSource.add(
|
|
650
|
+
await audioSource.add(audio);
|
|
385
651
|
await audioSource.close();
|
|
386
652
|
} else {
|
|
387
653
|
await output.start();
|
|
@@ -389,13 +655,16 @@ async function exportFrames(movie, options = {}) {
|
|
|
389
655
|
movie.app.ticker.stop();
|
|
390
656
|
const keyframeIntervalFrames = Math.max(1, Math.round(2 * movie.frameRate));
|
|
391
657
|
try {
|
|
392
|
-
|
|
658
|
+
const count = Math.max(1, range2.toFrame - range2.fromFrame);
|
|
659
|
+
for (let frame = range2.fromFrame; frame <= range2.toFrame; frame++) {
|
|
393
660
|
if (mb && blurCanvas) await movie._exposeFrame(frame, mb, blurCanvas, false);
|
|
394
661
|
else await movie.gotoFrame(frame, true);
|
|
395
|
-
|
|
662
|
+
if (small && smallCtx) smallCtx.drawImage(blurCanvas ?? stage, 0, 0, small.width, small.height);
|
|
663
|
+
const rel = frame - range2.fromFrame;
|
|
664
|
+
const isKey = rel === 0 || rel % keyframeIntervalFrames === 0;
|
|
396
665
|
const addOpts = isKey ? { keyFrame: true } : void 0;
|
|
397
|
-
await canvasSource.add(
|
|
398
|
-
const progress = Math.floor(
|
|
666
|
+
await canvasSource.add(rel / movie.frameRate, 1 / movie.frameRate, addOpts);
|
|
667
|
+
const progress = Math.floor(rel / count * 100);
|
|
399
668
|
movie.emit("progress", { progress, frame, totalFrames: movie.totalFrames });
|
|
400
669
|
}
|
|
401
670
|
await canvasSource.close();
|
|
@@ -2039,6 +2308,37 @@ var Movie = class {
|
|
|
2039
2308
|
})));
|
|
2040
2309
|
return new Blob([buildPdf(pages, { title: opts.title })], { type: "application/pdf" });
|
|
2041
2310
|
}
|
|
2311
|
+
async onionSkin(opts = {}) {
|
|
2312
|
+
warnUnknownOptions("movie.onionSkin()", opts, ["from", "to", "count", "scale", "as", "motionBlur"]);
|
|
2313
|
+
this._requireReady("onionSkin");
|
|
2314
|
+
const from = opts.from ?? 0, to = opts.to ?? this.duration, count = opts.count ?? 8, scale = opts.scale ?? 1;
|
|
2315
|
+
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}`);
|
|
2316
|
+
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}`);
|
|
2317
|
+
if (!(scale > 0 && scale <= 4)) throw new Error(`pixi-effects: movie.onionSkin(): scale must be above 0 and at most 4, got ${scale}`);
|
|
2318
|
+
const mb = this._blurFor(opts.motionBlur, "movie.onionSkin()");
|
|
2319
|
+
const blurred = mb ? this._blurCanvas() : null;
|
|
2320
|
+
const out = document.createElement("canvas");
|
|
2321
|
+
out.width = Math.max(1, Math.round(this.width * scale));
|
|
2322
|
+
out.height = Math.max(1, Math.round(this.height * scale));
|
|
2323
|
+
const g = out.getContext("2d");
|
|
2324
|
+
const alphas = onionAlphas(count);
|
|
2325
|
+
const frames = onionTimes(from, to, count).map((t) => clampFrame(t * this.frameRate, this.totalFrames));
|
|
2326
|
+
const back = this.currentFrame, wasAtPoster = this._atPoster;
|
|
2327
|
+
try {
|
|
2328
|
+
for (let i = 0; i < frames.length; i++) {
|
|
2329
|
+
const f = frames[i];
|
|
2330
|
+
if (mb && blurred) await this._quietly(() => this._exposeFrame(f, mb, blurred, true));
|
|
2331
|
+
else await this._quietly(() => this.gotoFrame(f, true));
|
|
2332
|
+
g.globalAlpha = alphas[i];
|
|
2333
|
+
g.drawImage(blurred ?? this.app.canvas, 0, 0, out.width, out.height);
|
|
2334
|
+
}
|
|
2335
|
+
} finally {
|
|
2336
|
+
g.globalAlpha = 1;
|
|
2337
|
+
if (wasAtPoster) await this._showPoster();
|
|
2338
|
+
else await this._quietly(() => this.gotoFrame(back, true));
|
|
2339
|
+
}
|
|
2340
|
+
return encodeCanvas(out, "image/png", opts.as ?? "blob");
|
|
2341
|
+
}
|
|
2042
2342
|
async contactSheet(opts = {}) {
|
|
2043
2343
|
warnUnknownOptions("movie.contactSheet()", opts, ["frames", "times", "count", "columns", "cellWidth", "as", "motionBlur"]);
|
|
2044
2344
|
this._requireReady("contactSheet");
|
|
@@ -2105,10 +2405,19 @@ var Movie = class {
|
|
|
2105
2405
|
* The soundtrack, measured: when every audio layer plays, how loud the mix is over time, and `issues`
|
|
2106
2406
|
* (limiting, inaudible layers, sounds cut off by the end). Use it to CHECK sound you cannot hear.
|
|
2107
2407
|
*/
|
|
2408
|
+
/**
|
|
2409
|
+
* The fonts of the text layers: `missing` lists layers none of whose `fontFamily` entries is available in this browser (they are
|
|
2410
|
+
* drawn in a fallback font), `failed` lists web fonts (`@font-face`) whose file could not be loaded.
|
|
2411
|
+
*/
|
|
2412
|
+
inspectFonts() {
|
|
2413
|
+
this._requireReady("inspectFonts");
|
|
2414
|
+
return inspectFonts(this._rootSequence);
|
|
2415
|
+
}
|
|
2108
2416
|
inspectAudio(opts = {}) {
|
|
2109
|
-
warnUnknownOptions("movie.inspectAudio()", opts, ["window"]);
|
|
2417
|
+
warnUnknownOptions("movie.inspectAudio()", opts, ["window", "scenes"]);
|
|
2110
2418
|
this._requireReady("inspectAudio");
|
|
2111
|
-
|
|
2419
|
+
const scenes = opts.scenes ?? namedScenes(this.timelineData().rows);
|
|
2420
|
+
return analyzeAudio(this.audioBuffer, this._audioSources, this._mixStats, this.duration, { ...opts, scenes });
|
|
2112
2421
|
}
|
|
2113
2422
|
/**
|
|
2114
2423
|
* Draw the stage now. PixiJS's CullerPlugin only culls inside `app.render()` (the ticker), and we render
|
|
@@ -2376,7 +2685,7 @@ var Movie = class {
|
|
|
2376
2685
|
return this.muted;
|
|
2377
2686
|
}
|
|
2378
2687
|
async render(options) {
|
|
2379
|
-
warnUnknownOptions("movie.render()", options, ["format", "video", "audio", "motionBlur"]);
|
|
2688
|
+
warnUnknownOptions("movie.render()", options, ["format", "video", "audio", "motionBlur", "range", "scale", "draft"]);
|
|
2380
2689
|
try {
|
|
2381
2690
|
return await this._quietly(() => exportFrames(this, options));
|
|
2382
2691
|
} catch (err) {
|
|
@@ -2587,6 +2896,7 @@ function clamp(v, lo, hi) {
|
|
|
2587
2896
|
return Math.max(lo, Math.min(hi, v));
|
|
2588
2897
|
}
|
|
2589
2898
|
function parseEase(name) {
|
|
2899
|
+
checkEase(name, "a preset");
|
|
2590
2900
|
return gsap.parseEase(name);
|
|
2591
2901
|
}
|
|
2592
2902
|
|