pixi-effects 0.6.0 → 0.8.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 +29 -0
- package/README.md +18 -11
- package/ai/SKILL.md +1 -1
- package/ai/reference/cheatsheet.md +9 -1
- package/ai/template.html +4 -3
- package/ai/tools/check.mjs +14 -5
- package/ai/tools/render.mjs +202 -0
- package/ai/tools/view.mjs +269 -0
- package/dist/{Base-4vNLl4tH.d.cts → Base-CSIulyIq.d.cts} +1 -1
- package/dist/{Base-CD4AoT5-.d.ts → Base-DVgwyIUj.d.ts} +1 -1
- package/dist/Composition-CM62XJKT.cjs +14 -0
- package/dist/{Composition-Y5VZSDDP.cjs.map → Composition-CM62XJKT.cjs.map} +1 -1
- package/dist/Composition-RJDSQYTV.js +5 -0
- package/dist/{Composition-7SO5WYHU.js.map → Composition-RJDSQYTV.js.map} +1 -1
- package/dist/Controller.cjs +122 -20
- package/dist/Controller.cjs.map +1 -1
- package/dist/Controller.d.cts +45 -3
- package/dist/Controller.d.ts +45 -3
- package/dist/Controller.js +122 -21
- package/dist/Controller.js.map +1 -1
- package/dist/{Movie-Ch2SUI1h.d.ts → Movie-CQOhUmko.d.cts} +75 -2
- package/dist/{Movie-CADDBfpl.d.cts → Movie-HF3pSGjQ.d.ts} +75 -2
- package/dist/{chunk-KQM2ZZMK.cjs → chunk-AGMNSJJH.cjs} +175 -29
- package/dist/chunk-AGMNSJJH.cjs.map +1 -0
- package/dist/{chunk-7IJHJXDI.js → chunk-FNKSXBKB.js} +136 -10
- package/dist/chunk-FNKSXBKB.js.map +1 -0
- package/dist/{chunk-B2FTJLNB.js → chunk-O4AJYUPK.js} +3 -3
- package/dist/{chunk-B2FTJLNB.js.map → chunk-O4AJYUPK.js.map} +1 -1
- package/dist/{chunk-4AGJF5RK.cjs → chunk-PDBDDGG5.cjs} +21 -21
- package/dist/{chunk-4AGJF5RK.cjs.map → chunk-PDBDDGG5.cjs.map} +1 -1
- package/dist/index.cjs +255 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +248 -18
- package/dist/index.js.map +1 -1
- package/dist/three.cjs +6 -5
- package/dist/three.cjs.map +1 -1
- package/dist/three.d.cts +2 -2
- package/dist/three.d.ts +2 -2
- package/dist/three.js +2 -1
- package/dist/three.js.map +1 -1
- package/dist/{types-DrW9QeKU.d.ts → types-oJijbn-K.d.cts} +14 -1
- package/dist/{types-DrW9QeKU.d.cts → types-oJijbn-K.d.ts} +14 -1
- package/docs/api.md +57 -0
- package/docs/dsl.md +60 -1
- package/llms-full.txt +127 -3
- package/package.json +7 -4
- package/dist/Composition-7SO5WYHU.js +0 -4
- package/dist/Composition-Y5VZSDDP.cjs +0 -13
- package/dist/chunk-7IJHJXDI.js.map +0 -1
- package/dist/chunk-KQM2ZZMK.cjs.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var chunkPDBDDGG5_cjs = require('./chunk-PDBDDGG5.cjs');
|
|
4
|
+
var chunkAGMNSJJH_cjs = require('./chunk-AGMNSJJH.cjs');
|
|
3
5
|
var chunkUQBGW5DO_cjs = require('./chunk-UQBGW5DO.cjs');
|
|
4
|
-
var chunk4AGJF5RK_cjs = require('./chunk-4AGJF5RK.cjs');
|
|
5
|
-
var chunkKQM2ZZMK_cjs = require('./chunk-KQM2ZZMK.cjs');
|
|
6
6
|
var PIXI = require('pixi.js');
|
|
7
7
|
var gsap = require('gsap');
|
|
8
8
|
var PixiPlugin = require('gsap/PixiPlugin');
|
|
@@ -181,7 +181,7 @@ function fft(re, im) {
|
|
|
181
181
|
}
|
|
182
182
|
var FRAME = 2048;
|
|
183
183
|
function spectralCentroid(samples, sampleRate, from = 0, to = samples.length) {
|
|
184
|
-
let
|
|
184
|
+
let num2 = 0;
|
|
185
185
|
let den = 0;
|
|
186
186
|
const re = new Float64Array(FRAME);
|
|
187
187
|
const im = new Float64Array(FRAME);
|
|
@@ -196,11 +196,11 @@ function spectralCentroid(samples, sampleRate, from = 0, to = samples.length) {
|
|
|
196
196
|
fft(re, im);
|
|
197
197
|
for (let b = 1; b < FRAME / 2; b++) {
|
|
198
198
|
const power = re[b] * re[b] + im[b] * im[b];
|
|
199
|
-
|
|
199
|
+
num2 += power * (b * sampleRate / FRAME);
|
|
200
200
|
den += power;
|
|
201
201
|
}
|
|
202
202
|
}
|
|
203
|
-
return den > 1e-9 ? Math.round(
|
|
203
|
+
return den > 1e-9 ? Math.round(num2 / den / 10) * 10 : 0;
|
|
204
204
|
}
|
|
205
205
|
|
|
206
206
|
// src/core/inspectAudio.ts
|
|
@@ -308,6 +308,28 @@ var AUDIO_CODEC_BY_FORMAT = {
|
|
|
308
308
|
webm: "opus",
|
|
309
309
|
mkv: "opus"
|
|
310
310
|
};
|
|
311
|
+
var AUDIO_PREFERENCE = {
|
|
312
|
+
mp4: ["aac", "opus", "mp3", "flac"],
|
|
313
|
+
mov: ["aac", "mp3"],
|
|
314
|
+
webm: ["opus", "vorbis"],
|
|
315
|
+
mkv: ["opus", "vorbis", "aac", "flac", "mp3"]
|
|
316
|
+
};
|
|
317
|
+
async function chooseAudioCodec(fmt, requested, container, audio, bitrate) {
|
|
318
|
+
const supported = container.getSupportedAudioCodecs?.();
|
|
319
|
+
const preferred = AUDIO_PREFERENCE[fmt].filter((c) => !supported || supported.includes(c));
|
|
320
|
+
const canEncode = (list) => mediabunny.getFirstEncodableAudioCodec(list, { numberOfChannels: audio.numberOfChannels ?? 2, sampleRate: audio.sampleRate, bitrate });
|
|
321
|
+
if (requested) {
|
|
322
|
+
if (await canEncode([requested])) return requested;
|
|
323
|
+
const alt = await canEncode(preferred.filter((c) => c !== requested));
|
|
324
|
+
throw new Error(`pixi-effects: this browser cannot encode "${requested}" audio for ${fmt}` + (alt ? `; "${alt}" would work: render({ format: '${fmt}', audio: { codec: '${alt}' } }), or leave audio.codec out and the best one is chosen.` : "; it has no audio encoder for this format at all (try another browser, or a movie without sound)."));
|
|
325
|
+
}
|
|
326
|
+
const chosen = await canEncode(preferred);
|
|
327
|
+
if (!chosen) throw new Error(`pixi-effects: this browser has no audio encoder for ${fmt} (it tried ${preferred.join(", ")}): try another browser, or render a movie without sound.`);
|
|
328
|
+
if (chosen !== preferred[0]) {
|
|
329
|
+
console.warn(`pixi-effects: this browser cannot encode ${preferred[0].toUpperCase()} audio (Chrome on Linux has no AAC encoder), so the ${fmt} carries ${chosen} instead; current players play it. A webm export needs no AAC.`);
|
|
330
|
+
}
|
|
331
|
+
return chosen;
|
|
332
|
+
}
|
|
311
333
|
var qualityMap = {
|
|
312
334
|
"very-low": mediabunny.QUALITY_VERY_LOW,
|
|
313
335
|
"low": mediabunny.QUALITY_LOW,
|
|
@@ -340,8 +362,10 @@ async function exportFrames(movie, options = {}) {
|
|
|
340
362
|
bitrate: qualityMap[options.audio?.bitrate ?? "high"] ?? mediabunny.QUALITY_HIGH
|
|
341
363
|
}
|
|
342
364
|
};
|
|
365
|
+
const container = makeOutputFormat(opts.format);
|
|
366
|
+
if (movie.audioBuffer) opts.audio.codec = await chooseAudioCodec(fmt, options.audio?.codec, container, movie.audioBuffer, opts.audio.bitrate);
|
|
343
367
|
const output = new mediabunny.Output({
|
|
344
|
-
format:
|
|
368
|
+
format: container,
|
|
345
369
|
target: new mediabunny.BufferTarget()
|
|
346
370
|
});
|
|
347
371
|
const canvasSource = new mediabunny.CanvasSource(movie.app.canvas, {
|
|
@@ -819,16 +843,16 @@ function expandTransitions(spec) {
|
|
|
819
843
|
if (toUses > 1) {
|
|
820
844
|
throw new Error(`pixi-effects: ${tag} sequence "${t.to}" is used as \`to\` in more than one transition`);
|
|
821
845
|
}
|
|
822
|
-
const tStart =
|
|
846
|
+
const tStart = chunkAGMNSJJH_cjs.resolveAt(t.at, parentDuration);
|
|
823
847
|
const tEnd = tStart + t.duration;
|
|
824
848
|
const fromSeq = fromEntry.seq;
|
|
825
|
-
const fromStart =
|
|
849
|
+
const fromStart = chunkAGMNSJJH_cjs.resolveAt(fromSeq.at, parentDuration);
|
|
826
850
|
const fromEnd = fromStart + (fromSeq.duration ?? parentDuration);
|
|
827
851
|
if (tStart < fromStart || tEnd > fromEnd) {
|
|
828
852
|
throw new Error(`pixi-effects: ${tag} window [${tStart}, ${tEnd}] is not covered by \`from\` "${t.from}" (lives [${fromStart}, ${fromEnd}], so it ends at ${fromEnd})`);
|
|
829
853
|
}
|
|
830
854
|
const toSeq = toEntry.seq;
|
|
831
|
-
const toStart =
|
|
855
|
+
const toStart = chunkAGMNSJJH_cjs.resolveAt(toSeq.at, parentDuration);
|
|
832
856
|
const toEnd = toStart + (toSeq.duration ?? parentDuration);
|
|
833
857
|
if (tStart < toStart || tEnd > toEnd) {
|
|
834
858
|
throw new Error(`pixi-effects: ${tag} window [${tStart}, ${tEnd}] is not covered by \`to\` "${t.to}" (lives [${toStart}, ${toEnd}], so it starts at ${toStart})`);
|
|
@@ -1140,7 +1164,7 @@ function inspectScene(root, frame, time, canvas, opts = {}) {
|
|
|
1140
1164
|
const seen = /* @__PURE__ */ new Map();
|
|
1141
1165
|
const scene = /* @__PURE__ */ new Map();
|
|
1142
1166
|
const scaledToNothing = /* @__PURE__ */ new Set();
|
|
1143
|
-
const
|
|
1167
|
+
const walk2 = (comp, prefix, insideThreeD, parentVisible, parentScene, parentMoving = false, parentZero = false) => {
|
|
1144
1168
|
const blending = /* @__PURE__ */ new Set();
|
|
1145
1169
|
for (const w of transitionWindowsOf(comp.spec)) {
|
|
1146
1170
|
const start = (comp.absoluteStart ?? 0) + w.start, end = (comp.absoluteStart ?? 0) + w.end;
|
|
@@ -1168,7 +1192,7 @@ function inspectScene(root, frame, time, canvas, opts = {}) {
|
|
|
1168
1192
|
onCanvas = inter === 0 && area > 0 ? "none" : inter >= area - 1e-6 ? "full" : "partial";
|
|
1169
1193
|
if (area === 0) onCanvas = "none";
|
|
1170
1194
|
}
|
|
1171
|
-
const keys =
|
|
1195
|
+
const keys = chunkAGMNSJJH_cjs.collectPropKeys(seq.spec);
|
|
1172
1196
|
const moving = parentMoving || (seq.spec.keyframes ?? []).length > 0 && (keys.has("x") || keys.has("y"));
|
|
1173
1197
|
const scale = t?.scale;
|
|
1174
1198
|
const zero = parentZero || !!scale && (scale.x === 0 || scale.y === 0);
|
|
@@ -1184,10 +1208,10 @@ function inspectScene(root, frame, time, canvas, opts = {}) {
|
|
|
1184
1208
|
const group = parentScene ?? (name !== void 0 && blending.has(name) ? name : void 0);
|
|
1185
1209
|
if (group) scene.set(info, group);
|
|
1186
1210
|
if (zero || seq.showsNothingYet) scaledToNothing.add(info);
|
|
1187
|
-
if (seq instanceof
|
|
1211
|
+
if (seq instanceof chunkPDBDDGG5_cjs.CompositionSequence) walk2(seq, prefix + label + "/", insideThreeD || threeD, visible, group, moving, zero);
|
|
1188
1212
|
});
|
|
1189
1213
|
};
|
|
1190
|
-
|
|
1214
|
+
walk2(root, "", false, true);
|
|
1191
1215
|
const issues = [];
|
|
1192
1216
|
const texts = layers.filter((l) => l.type === "text" && l.visible && l.alpha >= FAINT && l.bounds);
|
|
1193
1217
|
for (const l of texts) {
|
|
@@ -1224,6 +1248,158 @@ function inspectScene(root, frame, time, canvas, opts = {}) {
|
|
|
1224
1248
|
return { frame, time, canvas, summary: { layers: layers.length, visible: layers.filter((l) => l.visible).length }, issues, layers: listed };
|
|
1225
1249
|
}
|
|
1226
1250
|
|
|
1251
|
+
// src/core/timelineChart.ts
|
|
1252
|
+
var GROUP_MIN = 4;
|
|
1253
|
+
var MAX_KEYS = 200;
|
|
1254
|
+
var round = (n) => Math.round(n * 1e6) / 1e6;
|
|
1255
|
+
var family = (name) => name.replace(/\d+/g, "#");
|
|
1256
|
+
function detailOf(seq) {
|
|
1257
|
+
const s = seq.spec;
|
|
1258
|
+
if (typeof s.text === "string") return s.text.length > 60 ? s.text.slice(0, 57) + "\u2026" : s.text;
|
|
1259
|
+
if (typeof s.asset === "string") return `asset \u201C${s.asset}\u201D`;
|
|
1260
|
+
if (typeof s.sfx === "string") return `sfx \u201C${s.sfx}\u201D`;
|
|
1261
|
+
if (s.sfx && typeof s.sfx === "object") return "sfx (custom)";
|
|
1262
|
+
return void 0;
|
|
1263
|
+
}
|
|
1264
|
+
function walk(comp, parentStart, parentEnd, depth, prefix, rows, transitions) {
|
|
1265
|
+
const entries = [];
|
|
1266
|
+
comp._children.forEach((seq, i) => {
|
|
1267
|
+
const name = seq.spec.name ?? `${seq.spec.type}#${i}`;
|
|
1268
|
+
const start = parentStart + (seq.at ?? 0);
|
|
1269
|
+
const end = Math.min(parentEnd, seq.duration !== void 0 ? start + seq.duration : parentEnd);
|
|
1270
|
+
const span = end - start;
|
|
1271
|
+
const keys = (seq.spec.keyframes ?? []).map((kf) => round(start + chunkAGMNSJJH_cjs.resolveAt(kf.at, span))).sort((a, b) => a - b).slice(0, MAX_KEYS);
|
|
1272
|
+
const row = { path: prefix + name, name, type: seq.spec.type, depth, start: round(start), end: round(end), keys, detail: detailOf(seq) };
|
|
1273
|
+
const inner = [];
|
|
1274
|
+
const isComp = seq instanceof chunkPDBDDGG5_cjs.CompositionSequence;
|
|
1275
|
+
if (isComp) walk(seq, start, end, depth + 1, prefix + name + "/", inner, transitions);
|
|
1276
|
+
entries.push({ row, inner, groupable: !isComp });
|
|
1277
|
+
});
|
|
1278
|
+
rows.push(...groupFamilies(entries));
|
|
1279
|
+
for (const w of transitionWindowsOf(comp.spec)) {
|
|
1280
|
+
transitions.push({ from: prefix + w.from, to: prefix + w.to, start: round(parentStart + w.start), end: round(parentStart + w.end) });
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
function groupFamilies(entries) {
|
|
1284
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
1285
|
+
for (const e of entries) {
|
|
1286
|
+
if (!e.groupable) continue;
|
|
1287
|
+
const key = e.row.type + "|" + family(e.row.name);
|
|
1288
|
+
(buckets.get(key) ?? buckets.set(key, []).get(key)).push(e.row);
|
|
1289
|
+
}
|
|
1290
|
+
const out = [];
|
|
1291
|
+
const done = /* @__PURE__ */ new Set();
|
|
1292
|
+
for (const e of entries) {
|
|
1293
|
+
const key = e.row.type + "|" + family(e.row.name);
|
|
1294
|
+
const members = e.groupable ? buckets.get(key) : null;
|
|
1295
|
+
if (members && members.length >= GROUP_MIN) {
|
|
1296
|
+
if (done.has(key)) continue;
|
|
1297
|
+
done.add(key);
|
|
1298
|
+
const label = `${family(e.row.name)} \xD7${members.length}`;
|
|
1299
|
+
out.push({
|
|
1300
|
+
path: e.row.path.slice(0, e.row.path.length - e.row.name.length) + label,
|
|
1301
|
+
name: label,
|
|
1302
|
+
type: e.row.type,
|
|
1303
|
+
depth: e.row.depth,
|
|
1304
|
+
start: Math.min(...members.map((x) => x.start)),
|
|
1305
|
+
end: Math.max(...members.map((x) => x.end)),
|
|
1306
|
+
keys: members.flatMap((x) => x.keys).sort((a, b) => a - b).slice(0, MAX_KEYS),
|
|
1307
|
+
parts: members.map((x) => ({ start: x.start, end: x.end }))
|
|
1308
|
+
});
|
|
1309
|
+
} else {
|
|
1310
|
+
out.push(e.row, ...e.inner);
|
|
1311
|
+
continue;
|
|
1312
|
+
}
|
|
1313
|
+
out.push(...e.inner);
|
|
1314
|
+
}
|
|
1315
|
+
return out;
|
|
1316
|
+
}
|
|
1317
|
+
function collectTimeline(root, duration) {
|
|
1318
|
+
const rows = [];
|
|
1319
|
+
const transitions = [];
|
|
1320
|
+
walk(root, 0, duration, 0, "", rows, transitions);
|
|
1321
|
+
return { duration, rows, transitions };
|
|
1322
|
+
}
|
|
1323
|
+
var COLORS = {
|
|
1324
|
+
text: "#4cc9f0",
|
|
1325
|
+
shape: "#ffd166",
|
|
1326
|
+
image: "#7bd88f",
|
|
1327
|
+
video: "#c77dff",
|
|
1328
|
+
audio: "#ff7aa8",
|
|
1329
|
+
composition: "#8ea4c8",
|
|
1330
|
+
three: "#ff9f68",
|
|
1331
|
+
camera: "#b0b0b0"
|
|
1332
|
+
};
|
|
1333
|
+
var esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1334
|
+
var num = (n) => String(Math.round(n * 100) / 100);
|
|
1335
|
+
function tickStep(duration, chartWidth) {
|
|
1336
|
+
for (const s of [0.04, 0.1, 0.2, 0.5, 1, 2, 5, 10, 15, 30, 60, 120, 300, 600, 1200, 3600]) if (s / duration * chartWidth >= 70) return s;
|
|
1337
|
+
return 7200;
|
|
1338
|
+
}
|
|
1339
|
+
function timelineSvg(data, opts = {}) {
|
|
1340
|
+
const CHART = Math.max(100, opts.chartWidth ?? 950), withLabels = opts.labels !== false;
|
|
1341
|
+
const LABEL = withLabels ? 250 : 0, ROW = 22, TOP = 34, PAD = 16;
|
|
1342
|
+
const W = LABEL + CHART + PAD * 2, H = TOP + data.rows.length * ROW + PAD;
|
|
1343
|
+
const x = (t) => PAD + LABEL + Math.max(0, Math.min(data.duration, t)) / data.duration * CHART;
|
|
1344
|
+
const step = tickStep(data.duration, CHART);
|
|
1345
|
+
const out = [];
|
|
1346
|
+
for (let t = 0; t <= data.duration + 1e-9; t += step) {
|
|
1347
|
+
out.push(`<line class="grid" x1="${num(x(t))}" y1="${TOP - 6}" x2="${num(x(t))}" y2="${H - PAD}"/>`);
|
|
1348
|
+
out.push(`<text class="tick" x="${num(x(t))}" y="${TOP - 12}" text-anchor="middle">${num(t)}${t === 0 || Math.abs(t - data.duration) < 1e-9 ? " s" : ""}</text>`);
|
|
1349
|
+
}
|
|
1350
|
+
if (Math.abs(data.duration % step) > 1e-9) out.push(`<text class="tick" x="${num(x(data.duration))}" y="${TOP - 12}" text-anchor="middle">${num(data.duration)} s</text>`);
|
|
1351
|
+
for (const t of data.transitions) {
|
|
1352
|
+
out.push(`<g><title>transition ${esc(t.from)} \u2192 ${esc(t.to)} (${num(t.start)}\u2013${num(t.end)} s)</title><rect class="transition" x="${num(x(t.start))}" y="${TOP - 4}" width="${num(Math.max(2, x(t.end) - x(t.start)))}" height="${H - TOP - PAD + 4}"/></g>`);
|
|
1353
|
+
}
|
|
1354
|
+
data.rows.forEach((r, i) => {
|
|
1355
|
+
const y = TOP + i * ROW;
|
|
1356
|
+
const color = COLORS[r.type] ?? "#9aa7b8";
|
|
1357
|
+
const tip = `${r.path} \xB7 ${r.type} \xB7 ${num(r.start)}\u2013${num(r.end)} s (${num(r.end - r.start)} s)${r.keys.length ? ` \xB7 ${r.keys.length} keyframe${r.keys.length > 1 ? "s" : ""}` : ""}${r.detail ? ` \xB7 ${r.detail}` : ""}`;
|
|
1358
|
+
out.push(`<g class="row" data-start="${num(r.start)}"><title>${esc(tip)}</title>`);
|
|
1359
|
+
out.push(`<rect class="band" x="${PAD}" y="${y}" width="${LABEL + CHART}" height="${ROW}" ${i % 2 ? 'fill-opacity=".05"' : 'fill-opacity="0"'}/>`);
|
|
1360
|
+
if (withLabels) out.push(`<text class="label" x="${PAD + 6 + r.depth * 14}" y="${y + 15}">${esc(r.name)}</text>`);
|
|
1361
|
+
for (const p of r.parts ?? [{ start: r.start, end: r.end }]) {
|
|
1362
|
+
out.push(`<rect class="bar" x="${num(x(p.start))}" y="${y + 4}" width="${num(Math.max(2, x(p.end) - x(p.start)))}" height="${ROW - 8}" rx="3" fill="${color}"/>`);
|
|
1363
|
+
}
|
|
1364
|
+
for (const k of r.keys) out.push(`<path class="key" d="M ${num(x(k))} ${y + 7} l 3 4 l -3 4 l -3 -4 z"/>`);
|
|
1365
|
+
out.push("</g>");
|
|
1366
|
+
});
|
|
1367
|
+
const legend = Object.entries(COLORS).map(([type, c], i) => `<g transform="translate(${PAD + i * 108},${H + 10})"><rect width="12" height="12" rx="3" fill="${c}"/><text x="18" y="10" class="tick">${type}</text></g>`).join("");
|
|
1368
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H + 32}" viewBox="0 0 ${W} ${H + 32}" role="img" aria-label="Timeline of ${data.rows.length} rows" data-duration="${num(data.duration)}" data-x0="${num(x(0))}" data-x1="${num(x(data.duration))}" data-top="${TOP - 6}" data-bottom="${H - PAD}" data-row="${ROW}">
|
|
1369
|
+
${out.join("\n")}
|
|
1370
|
+
${legend}
|
|
1371
|
+
</svg>`;
|
|
1372
|
+
}
|
|
1373
|
+
function timelineHtml(data, opts = {}) {
|
|
1374
|
+
const W = 250 + 950 + 32;
|
|
1375
|
+
const title = esc(opts.title ?? "Timeline");
|
|
1376
|
+
return `<!doctype html>
|
|
1377
|
+
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>${title} \u2014 timeline</title>
|
|
1378
|
+
<style>
|
|
1379
|
+
:root { --bg: #f7f5f1; --ink: #1d2433; --dim: #6b7587; --grid: #d9d5cc; --key: #1d2433; --trans: #4cc9f0; }
|
|
1380
|
+
@media (prefers-color-scheme: dark) { :root { --bg: #0e1320; --ink: #e8eefc; --dim: #8fa0bf; --grid: #26304a; --key: #ffffff; --trans: #4cc9f0; } }
|
|
1381
|
+
body { margin: 0; background: var(--bg); color: var(--ink); font: 14px system-ui, sans-serif; }
|
|
1382
|
+
main { padding: 20px 16px 32px; max-width: ${W + 32}px; margin: 0 auto; overflow-x: auto; }
|
|
1383
|
+
h1 { font-size: 18px; font-weight: 600; margin: 0 0 4px; }
|
|
1384
|
+
p { margin: 0 0 14px; color: var(--dim); font-size: 13px; }
|
|
1385
|
+
svg { display: block; }
|
|
1386
|
+
.grid { stroke: var(--grid); stroke-width: 1; }
|
|
1387
|
+
.tick { fill: var(--dim); font-size: 11px; }
|
|
1388
|
+
.label { fill: var(--ink); font-size: 12px; font-family: ui-monospace, Menlo, monospace; }
|
|
1389
|
+
.band { fill: var(--ink); }
|
|
1390
|
+
.key { fill: var(--key); opacity: .85; }
|
|
1391
|
+
.transition { fill: var(--trans); opacity: .16; }
|
|
1392
|
+
.bar { opacity: .88; }
|
|
1393
|
+
.row:hover .bar { opacity: 1; }
|
|
1394
|
+
</style></head>
|
|
1395
|
+
<body><main>
|
|
1396
|
+
<h1>${title}</h1>
|
|
1397
|
+
<p>${data.rows.length} row${data.rows.length === 1 ? "" : "s"} \xB7 ${num(data.duration)} s \xB7 \u25C6 keyframe start \xB7 shaded band: a transition \xB7 hover a bar for details</p>
|
|
1398
|
+
${timelineSvg(data)}
|
|
1399
|
+
</main></body></html>
|
|
1400
|
+
`;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1227
1403
|
// src/core/frames.ts
|
|
1228
1404
|
function pickFrames(lastFrame, count) {
|
|
1229
1405
|
const last = Math.max(0, Math.floor(lastFrame));
|
|
@@ -1286,6 +1462,8 @@ var Movie = class {
|
|
|
1286
1462
|
audioSource = null;
|
|
1287
1463
|
gainNode = null;
|
|
1288
1464
|
_cancelAudioStart = null;
|
|
1465
|
+
/** > 0 while the movie moves its own playhead (playback, render, snapshot, contactSheet): those are not the seeks `seeking` / `seeked` report. */
|
|
1466
|
+
_quiet = 0;
|
|
1289
1467
|
_volume = 1;
|
|
1290
1468
|
_muted = false;
|
|
1291
1469
|
isPlaying = false;
|
|
@@ -1379,6 +1557,7 @@ var Movie = class {
|
|
|
1379
1557
|
height: options.composition.height ?? this.height,
|
|
1380
1558
|
duration: options.composition.duration ?? this.duration
|
|
1381
1559
|
} : void 0;
|
|
1560
|
+
await chunkAGMNSJJH_cjs.ensureFilterLibrary(seededComposition);
|
|
1382
1561
|
const userComposition = seededComposition ? expandTransitions(seededComposition) : void 0;
|
|
1383
1562
|
const rootSeqSpec = {
|
|
1384
1563
|
type: "composition",
|
|
@@ -1388,7 +1567,7 @@ var Movie = class {
|
|
|
1388
1567
|
...userComposition
|
|
1389
1568
|
};
|
|
1390
1569
|
if (userComposition) carryTransitionWindows(userComposition, rootSeqSpec);
|
|
1391
|
-
const composition = new
|
|
1570
|
+
const composition = new chunkPDBDDGG5_cjs.CompositionSequence(rootSeqSpec, null, rootShape);
|
|
1392
1571
|
await composition.build();
|
|
1393
1572
|
this._rootSequence = composition;
|
|
1394
1573
|
if (composition.target) root.addChild(composition.target);
|
|
@@ -1412,12 +1591,32 @@ var Movie = class {
|
|
|
1412
1591
|
} catch (cleanupErr) {
|
|
1413
1592
|
console.warn("pixi-effects: cleanup after init failure also threw:", cleanupErr);
|
|
1414
1593
|
}
|
|
1594
|
+
this._emitError("init", err);
|
|
1415
1595
|
throw err;
|
|
1416
1596
|
}
|
|
1417
1597
|
}
|
|
1418
1598
|
async gotoFrame(frame, force = false) {
|
|
1419
1599
|
if (this._initState !== "ready") return;
|
|
1420
1600
|
if (!force && this.currentFrame === frame) return;
|
|
1601
|
+
if (this._quiet > 0) return this._goto(frame);
|
|
1602
|
+
const target = Math.max(0, Math.min(frame, this.totalFrames));
|
|
1603
|
+
this.emit("seeking", { frame: target, totalFrames: this.totalFrames });
|
|
1604
|
+
await this._goto(frame);
|
|
1605
|
+
this.emit("seeked", { frame: this.currentFrame, totalFrames: this.totalFrames });
|
|
1606
|
+
}
|
|
1607
|
+
/** Run `fn` with `seeking` / `seeked` silenced: the movie is moving its own playhead, not answering a seek. */
|
|
1608
|
+
async _quietly(fn) {
|
|
1609
|
+
this._quiet++;
|
|
1610
|
+
try {
|
|
1611
|
+
return await fn();
|
|
1612
|
+
} finally {
|
|
1613
|
+
this._quiet--;
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
_emitError(where, error) {
|
|
1617
|
+
this.emit("error", { where, message: error instanceof Error ? error.message : String(error), error });
|
|
1618
|
+
}
|
|
1619
|
+
async _goto(frame) {
|
|
1421
1620
|
this.currentFrame = Math.max(0, Math.min(frame, this.totalFrames));
|
|
1422
1621
|
this.timeline.time(this.currentFrame / this.frameRate);
|
|
1423
1622
|
await this._awaitVideoFrames();
|
|
@@ -1439,7 +1638,7 @@ var Movie = class {
|
|
|
1439
1638
|
async snapshot(frame = this.currentFrame, opts = {}) {
|
|
1440
1639
|
chunkUQBGW5DO_cjs.warnUnknownOptions("movie.snapshot()", opts, ["scale", "type", "as"]);
|
|
1441
1640
|
this._requireReady("snapshot");
|
|
1442
|
-
await this.gotoFrame(clampFrame(frame, this.totalFrames), true);
|
|
1641
|
+
await this._quietly(() => this.gotoFrame(clampFrame(frame, this.totalFrames), true));
|
|
1443
1642
|
const scale = opts.scale ?? 1;
|
|
1444
1643
|
const out = document.createElement("canvas");
|
|
1445
1644
|
out.width = Math.max(1, Math.round(this.width * scale));
|
|
@@ -1463,7 +1662,7 @@ var Movie = class {
|
|
|
1463
1662
|
try {
|
|
1464
1663
|
for (let i = 0; i < frames.length; i++) {
|
|
1465
1664
|
const f = frames[i];
|
|
1466
|
-
await this.gotoFrame(f, true);
|
|
1665
|
+
await this._quietly(() => this.gotoFrame(f, true));
|
|
1467
1666
|
const { x, y } = L.positions[i];
|
|
1468
1667
|
g.drawImage(this.app.canvas, x, y + LABEL_H, L.cellW, L.cellH);
|
|
1469
1668
|
g.fillStyle = "#e8ecf8";
|
|
@@ -1472,7 +1671,7 @@ var Movie = class {
|
|
|
1472
1671
|
g.fillText(`frame ${f} \xB7 ${(f / this.frameRate).toFixed(2)}s`, x + 8, y + LABEL_H / 2);
|
|
1473
1672
|
}
|
|
1474
1673
|
} finally {
|
|
1475
|
-
await this.gotoFrame(back, true);
|
|
1674
|
+
await this._quietly(() => this.gotoFrame(back, true));
|
|
1476
1675
|
}
|
|
1477
1676
|
return encodeCanvas(sheet, "image/png", opts.as ?? "blob");
|
|
1478
1677
|
}
|
|
@@ -1487,6 +1686,24 @@ var Movie = class {
|
|
|
1487
1686
|
await this.gotoFrame(f, true);
|
|
1488
1687
|
return inspectScene(this._rootSequence, f, f / this.frameRate, { width: this.width, height: this.height }, opts);
|
|
1489
1688
|
}
|
|
1689
|
+
/**
|
|
1690
|
+
* Every layer as a row with absolute start / end / keyframe times (seconds), plus the transition windows. Runs of
|
|
1691
|
+
* similar layers (`pop-1` … `pop-12`) are one row with a `parts` span each. Read it, or draw it with `timelineChart()`.
|
|
1692
|
+
*/
|
|
1693
|
+
timelineData() {
|
|
1694
|
+
this._requireReady("timelineData");
|
|
1695
|
+
return collectTimeline(this._rootSequence, this.duration);
|
|
1696
|
+
}
|
|
1697
|
+
/** The timeline as one self-contained HTML page (an inline SVG, no scripts): layers as bars on a time axis, ◆ keyframes, transition bands. Open it in a browser. */
|
|
1698
|
+
timelineChart(opts = {}) {
|
|
1699
|
+
chunkUQBGW5DO_cjs.warnUnknownOptions("movie.timelineChart()", opts, ["title"]);
|
|
1700
|
+
return timelineHtml(this.timelineData(), opts);
|
|
1701
|
+
}
|
|
1702
|
+
/** Just the chart: one `<svg>` whose geometry is in `data-*` attributes (a viewer puts a playhead on it: `pixi-effects-view`). */
|
|
1703
|
+
timelineSvg(opts = {}) {
|
|
1704
|
+
chunkUQBGW5DO_cjs.warnUnknownOptions("movie.timelineSvg()", opts, ["chartWidth", "labels"]);
|
|
1705
|
+
return timelineSvg(this.timelineData(), opts);
|
|
1706
|
+
}
|
|
1490
1707
|
/**
|
|
1491
1708
|
* The soundtrack, measured: when every audio layer plays, how loud the mix is over time, and `issues`
|
|
1492
1709
|
* (limiting, inaudible layers, sounds cut off by the end). Use it to CHECK sound you cannot hear.
|
|
@@ -1537,17 +1754,20 @@ var Movie = class {
|
|
|
1537
1754
|
const frame = Math.floor(elapsed * this.frameRate);
|
|
1538
1755
|
if (frame <= this.totalFrames) {
|
|
1539
1756
|
inFlight = true;
|
|
1540
|
-
this.
|
|
1757
|
+
this._goto(frame).catch((err) => {
|
|
1541
1758
|
console.warn("pixi-effects: gotoFrame failed during playback:", err);
|
|
1759
|
+
this._emitError("playback", err);
|
|
1542
1760
|
}).finally(() => {
|
|
1543
1761
|
inFlight = false;
|
|
1544
1762
|
});
|
|
1545
1763
|
this._raf = requestAnimationFrame(tick);
|
|
1546
1764
|
} else {
|
|
1547
1765
|
this.pause();
|
|
1548
|
-
this.
|
|
1766
|
+
this._goto(this.totalFrames).catch((err) => {
|
|
1549
1767
|
console.warn("pixi-effects: final gotoFrame failed:", err);
|
|
1768
|
+
this._emitError("playback", err);
|
|
1550
1769
|
});
|
|
1770
|
+
this.emit("ended");
|
|
1551
1771
|
}
|
|
1552
1772
|
};
|
|
1553
1773
|
this._raf = requestAnimationFrame(tick);
|
|
@@ -1591,15 +1811,20 @@ var Movie = class {
|
|
|
1591
1811
|
if (wasPlaying) this.emit("pause");
|
|
1592
1812
|
}
|
|
1593
1813
|
set volume(v) {
|
|
1594
|
-
|
|
1814
|
+
const next = Math.max(0, Math.min(1, v));
|
|
1815
|
+
const changed = next !== this._volume;
|
|
1816
|
+
this._volume = next;
|
|
1595
1817
|
if (this.gainNode) this.gainNode.gain.value = this._muted ? 0 : this._volume;
|
|
1818
|
+
if (changed) this.emit("volumechange", { volume: this._volume, muted: this._muted });
|
|
1596
1819
|
}
|
|
1597
1820
|
get volume() {
|
|
1598
1821
|
return this._volume;
|
|
1599
1822
|
}
|
|
1600
1823
|
set muted(v) {
|
|
1824
|
+
const changed = !!v !== this._muted;
|
|
1601
1825
|
this._muted = !!v;
|
|
1602
1826
|
if (this.gainNode) this.gainNode.gain.value = this._muted ? 0 : this._volume;
|
|
1827
|
+
if (changed) this.emit("volumechange", { volume: this._volume, muted: this._muted });
|
|
1603
1828
|
}
|
|
1604
1829
|
get muted() {
|
|
1605
1830
|
return this._muted;
|
|
@@ -1610,7 +1835,12 @@ var Movie = class {
|
|
|
1610
1835
|
}
|
|
1611
1836
|
async render(options) {
|
|
1612
1837
|
chunkUQBGW5DO_cjs.warnUnknownOptions("movie.render()", options, ["format", "video", "audio"]);
|
|
1613
|
-
|
|
1838
|
+
try {
|
|
1839
|
+
return await this._quietly(() => exportFrames(this, options));
|
|
1840
|
+
} catch (err) {
|
|
1841
|
+
this._emitError("render", err);
|
|
1842
|
+
throw err;
|
|
1843
|
+
}
|
|
1614
1844
|
}
|
|
1615
1845
|
async destroy() {
|
|
1616
1846
|
safeRun(() => this.pause());
|
|
@@ -1833,17 +2063,17 @@ function orbit(opts) {
|
|
|
1833
2063
|
throw new Error("pixi-effects: orbit() cannot combine `radius` with `dollyZoom` \u2014 the radius follows the animated fov");
|
|
1834
2064
|
}
|
|
1835
2065
|
const [cx, cy] = opts.center ?? [width / 2, height / 2];
|
|
1836
|
-
const fixedR = opts.radius ??
|
|
2066
|
+
const fixedR = opts.radius ?? chunkAGMNSJJH_cjs.homeDistance(height, fov ?? chunkAGMNSJJH_cjs.DEFAULT_FOV);
|
|
1837
2067
|
const start = opts.start ?? -degrees / 2;
|
|
1838
2068
|
const steps = Math.max(1, Math.round(duration * stepsPerSecond));
|
|
1839
2069
|
const easeFn = parseEase(ease);
|
|
1840
2070
|
const dollyEase = dolly ? parseEase(dolly.ease ?? ease) : null;
|
|
1841
2071
|
const pos = (i) => {
|
|
1842
2072
|
const p = i / steps;
|
|
1843
|
-
const a = (start + degrees * easeFn(p)) *
|
|
2073
|
+
const a = (start + degrees * easeFn(p)) * chunkAGMNSJJH_cjs.DEG;
|
|
1844
2074
|
if (!dolly) return { x: cx + fixedR * Math.sin(a), z: fixedR * Math.cos(a) };
|
|
1845
2075
|
const f = dolly.from + (dolly.to - dolly.from) * dollyEase(p);
|
|
1846
|
-
const R =
|
|
2076
|
+
const R = chunkAGMNSJJH_cjs.homeDistance(height, f);
|
|
1847
2077
|
return { x: cx + R * Math.sin(a), z: R * Math.cos(a), fov: f };
|
|
1848
2078
|
};
|
|
1849
2079
|
const keyframes = [];
|
|
@@ -1892,7 +2122,7 @@ function withFade(spec, opts) {
|
|
|
1892
2122
|
|
|
1893
2123
|
Object.defineProperty(exports, "registerSequenceType", {
|
|
1894
2124
|
enumerable: true,
|
|
1895
|
-
get: function () { return
|
|
2125
|
+
get: function () { return chunkAGMNSJJH_cjs.registerSequenceType; }
|
|
1896
2126
|
});
|
|
1897
2127
|
exports.Movie = Movie;
|
|
1898
2128
|
exports.kenBurns = kenBurns;
|