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.d.cts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export { A as AudioInspectOptions, a as AudioReport, b as AudioSourceReport, C as ContactSheetOptions, F as FrameEvent, I as InspectOptions, c as InspectReport, L as LayerInfo, M as Movie, d as MovieOptions, P as ProgressEvent, R as RenderOptions, S as SnapshotOptions,
|
|
2
|
-
import { S as Sequence } from './Base-
|
|
3
|
-
import { S as SequenceSpec, C as CompositionShape, I as ImageSequenceSpec, a as CameraSequenceSpec, b as SequenceCommon } from './types-
|
|
4
|
-
export { A as ArcShapeSpec, c as AssetSpec, d as AudioAssetSpec, e as AudioSequenceSpec, f as AudioSfxSpec, g as ChromaKeyFilterSpec, h as CircleShapeSpec, i as CompositionSequenceSpec, j as CompositionSpec, k as CrossfadeTransition, l as CustomFilterSpec, D as DipTransition, m as DissolveTransition, E as EllipseShapeSpec, n as Expr, F as FilterSpec, G as GradientSpec, o as IrisTransition, K as Keyframe, L as LineShapeSpec, P as PathShapeSpec, p as PolygonShapeSpec, q as PropValue, r as Props, R as RectShapeSpec, s as SfxKnobs, t as SfxOptions, u as SfxPreset, v as SfxVoice, w as ShapeSequenceSpec, x as SlideTransition, T as TextSequenceSpec, y as TransitionCommon, z as TransitionSpec, V as VideoSequenceSpec, W as WipeTransition, Z as ZoomTransition } from './types-
|
|
1
|
+
export { A as AudioInspectOptions, a as AudioReport, b as AudioSourceReport, C as ContactSheetOptions, F as FrameEvent, I as InspectOptions, c as InspectReport, L as LayerInfo, M as Movie, d as MovieErrorEvent, e as MovieOptions, P as ProgressEvent, R as RenderOptions, S as SnapshotOptions, f as SoundMeasure, T as TimelineData, g as TimelineHtmlOptions, h as TimelineRow, i as TimelineSvgOptions, j as TimelineTransition, V as VolumeEvent } from './Movie-CQOhUmko.cjs';
|
|
2
|
+
import { S as Sequence } from './Base-CSIulyIq.cjs';
|
|
3
|
+
import { S as SequenceSpec, C as CompositionShape, I as ImageSequenceSpec, a as CameraSequenceSpec, b as SequenceCommon } from './types-oJijbn-K.cjs';
|
|
4
|
+
export { A as ArcShapeSpec, c as AssetSpec, d as AudioAssetSpec, e as AudioSequenceSpec, f as AudioSfxSpec, g as ChromaKeyFilterSpec, h as CircleShapeSpec, i as CompositionSequenceSpec, j as CompositionSpec, k as CrossfadeTransition, l as CustomFilterSpec, D as DipTransition, m as DissolveTransition, E as EllipseShapeSpec, n as Expr, F as FilterSpec, G as GradientSpec, o as IrisTransition, K as Keyframe, L as LineShapeSpec, P as PathShapeSpec, p as PolygonShapeSpec, q as PropValue, r as Props, R as RectShapeSpec, s as SfxKnobs, t as SfxOptions, u as SfxPreset, v as SfxVoice, w as ShapeSequenceSpec, x as SlideTransition, T as TextSequenceSpec, y as TransitionCommon, z as TransitionSpec, V as VideoSequenceSpec, W as WipeTransition, Z as ZoomTransition } from './types-oJijbn-K.cjs';
|
|
5
5
|
import 'pixi.js';
|
|
6
6
|
import 'gsap';
|
|
7
7
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export { A as AudioInspectOptions, a as AudioReport, b as AudioSourceReport, C as ContactSheetOptions, F as FrameEvent, I as InspectOptions, c as InspectReport, L as LayerInfo, M as Movie, d as MovieOptions, P as ProgressEvent, R as RenderOptions, S as SnapshotOptions,
|
|
2
|
-
import { S as Sequence } from './Base-
|
|
3
|
-
import { S as SequenceSpec, C as CompositionShape, I as ImageSequenceSpec, a as CameraSequenceSpec, b as SequenceCommon } from './types-
|
|
4
|
-
export { A as ArcShapeSpec, c as AssetSpec, d as AudioAssetSpec, e as AudioSequenceSpec, f as AudioSfxSpec, g as ChromaKeyFilterSpec, h as CircleShapeSpec, i as CompositionSequenceSpec, j as CompositionSpec, k as CrossfadeTransition, l as CustomFilterSpec, D as DipTransition, m as DissolveTransition, E as EllipseShapeSpec, n as Expr, F as FilterSpec, G as GradientSpec, o as IrisTransition, K as Keyframe, L as LineShapeSpec, P as PathShapeSpec, p as PolygonShapeSpec, q as PropValue, r as Props, R as RectShapeSpec, s as SfxKnobs, t as SfxOptions, u as SfxPreset, v as SfxVoice, w as ShapeSequenceSpec, x as SlideTransition, T as TextSequenceSpec, y as TransitionCommon, z as TransitionSpec, V as VideoSequenceSpec, W as WipeTransition, Z as ZoomTransition } from './types-
|
|
1
|
+
export { A as AudioInspectOptions, a as AudioReport, b as AudioSourceReport, C as ContactSheetOptions, F as FrameEvent, I as InspectOptions, c as InspectReport, L as LayerInfo, M as Movie, d as MovieErrorEvent, e as MovieOptions, P as ProgressEvent, R as RenderOptions, S as SnapshotOptions, f as SoundMeasure, T as TimelineData, g as TimelineHtmlOptions, h as TimelineRow, i as TimelineSvgOptions, j as TimelineTransition, V as VolumeEvent } from './Movie-HF3pSGjQ.js';
|
|
2
|
+
import { S as Sequence } from './Base-DVgwyIUj.js';
|
|
3
|
+
import { S as SequenceSpec, C as CompositionShape, I as ImageSequenceSpec, a as CameraSequenceSpec, b as SequenceCommon } from './types-oJijbn-K.js';
|
|
4
|
+
export { A as ArcShapeSpec, c as AssetSpec, d as AudioAssetSpec, e as AudioSequenceSpec, f as AudioSfxSpec, g as ChromaKeyFilterSpec, h as CircleShapeSpec, i as CompositionSequenceSpec, j as CompositionSpec, k as CrossfadeTransition, l as CustomFilterSpec, D as DipTransition, m as DissolveTransition, E as EllipseShapeSpec, n as Expr, F as FilterSpec, G as GradientSpec, o as IrisTransition, K as Keyframe, L as LineShapeSpec, P as PathShapeSpec, p as PolygonShapeSpec, q as PropValue, r as Props, R as RectShapeSpec, s as SfxKnobs, t as SfxOptions, u as SfxPreset, v as SfxVoice, w as ShapeSequenceSpec, x as SlideTransition, T as TextSequenceSpec, y as TransitionCommon, z as TransitionSpec, V as VideoSequenceSpec, W as WipeTransition, Z as ZoomTransition } from './types-oJijbn-K.js';
|
|
5
5
|
import 'pixi.js';
|
|
6
6
|
import 'gsap';
|
|
7
7
|
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
+
import { CompositionSequence } from './chunk-O4AJYUPK.js';
|
|
2
|
+
import { ensureFilterLibrary, homeDistance, DEFAULT_FOV, DEG, resolveAt, collectPropKeys } from './chunk-FNKSXBKB.js';
|
|
3
|
+
export { registerSequenceType } from './chunk-FNKSXBKB.js';
|
|
1
4
|
import { warnUnknownOptions, suggestName } from './chunk-DKGG7WD4.js';
|
|
2
|
-
import { CompositionSequence } from './chunk-B2FTJLNB.js';
|
|
3
|
-
import { homeDistance, DEFAULT_FOV, DEG, resolveAt, collectPropKeys } from './chunk-7IJHJXDI.js';
|
|
4
|
-
export { registerSequenceType } from './chunk-7IJHJXDI.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';
|
|
7
7
|
import { gsap } from 'gsap';
|
|
8
8
|
import { PixiPlugin } from 'gsap/PixiPlugin';
|
|
9
|
-
import { QUALITY_VERY_HIGH, QUALITY_HIGH, QUALITY_MEDIUM, QUALITY_LOW, QUALITY_VERY_LOW, Output, BufferTarget, CanvasSource, AudioBufferSource, Input, ALL_FORMATS, BlobSource, WavOutputFormat, Conversion, VideoSampleSink, MkvOutputFormat, WebMOutputFormat, MovOutputFormat, Mp4OutputFormat } from 'mediabunny';
|
|
9
|
+
import { QUALITY_VERY_HIGH, QUALITY_HIGH, QUALITY_MEDIUM, QUALITY_LOW, QUALITY_VERY_LOW, Output, BufferTarget, CanvasSource, AudioBufferSource, Input, ALL_FORMATS, BlobSource, WavOutputFormat, Conversion, VideoSampleSink, MkvOutputFormat, WebMOutputFormat, MovOutputFormat, Mp4OutputFormat, getFirstEncodableAudioCodec } from 'mediabunny';
|
|
10
10
|
|
|
11
11
|
var registered = false;
|
|
12
12
|
function ensureLoadersRegistered(audioContext) {
|
|
@@ -161,7 +161,7 @@ function fft(re, im) {
|
|
|
161
161
|
}
|
|
162
162
|
var FRAME = 2048;
|
|
163
163
|
function spectralCentroid(samples, sampleRate, from = 0, to = samples.length) {
|
|
164
|
-
let
|
|
164
|
+
let num2 = 0;
|
|
165
165
|
let den = 0;
|
|
166
166
|
const re = new Float64Array(FRAME);
|
|
167
167
|
const im = new Float64Array(FRAME);
|
|
@@ -176,11 +176,11 @@ function spectralCentroid(samples, sampleRate, from = 0, to = samples.length) {
|
|
|
176
176
|
fft(re, im);
|
|
177
177
|
for (let b = 1; b < FRAME / 2; b++) {
|
|
178
178
|
const power = re[b] * re[b] + im[b] * im[b];
|
|
179
|
-
|
|
179
|
+
num2 += power * (b * sampleRate / FRAME);
|
|
180
180
|
den += power;
|
|
181
181
|
}
|
|
182
182
|
}
|
|
183
|
-
return den > 1e-9 ? Math.round(
|
|
183
|
+
return den > 1e-9 ? Math.round(num2 / den / 10) * 10 : 0;
|
|
184
184
|
}
|
|
185
185
|
|
|
186
186
|
// src/core/inspectAudio.ts
|
|
@@ -288,6 +288,28 @@ var AUDIO_CODEC_BY_FORMAT = {
|
|
|
288
288
|
webm: "opus",
|
|
289
289
|
mkv: "opus"
|
|
290
290
|
};
|
|
291
|
+
var AUDIO_PREFERENCE = {
|
|
292
|
+
mp4: ["aac", "opus", "mp3", "flac"],
|
|
293
|
+
mov: ["aac", "mp3"],
|
|
294
|
+
webm: ["opus", "vorbis"],
|
|
295
|
+
mkv: ["opus", "vorbis", "aac", "flac", "mp3"]
|
|
296
|
+
};
|
|
297
|
+
async function chooseAudioCodec(fmt, requested, container, audio, bitrate) {
|
|
298
|
+
const supported = container.getSupportedAudioCodecs?.();
|
|
299
|
+
const preferred = AUDIO_PREFERENCE[fmt].filter((c) => !supported || supported.includes(c));
|
|
300
|
+
const canEncode = (list) => getFirstEncodableAudioCodec(list, { numberOfChannels: audio.numberOfChannels ?? 2, sampleRate: audio.sampleRate, bitrate });
|
|
301
|
+
if (requested) {
|
|
302
|
+
if (await canEncode([requested])) return requested;
|
|
303
|
+
const alt = await canEncode(preferred.filter((c) => c !== requested));
|
|
304
|
+
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)."));
|
|
305
|
+
}
|
|
306
|
+
const chosen = await canEncode(preferred);
|
|
307
|
+
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.`);
|
|
308
|
+
if (chosen !== preferred[0]) {
|
|
309
|
+
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.`);
|
|
310
|
+
}
|
|
311
|
+
return chosen;
|
|
312
|
+
}
|
|
291
313
|
var qualityMap = {
|
|
292
314
|
"very-low": QUALITY_VERY_LOW,
|
|
293
315
|
"low": QUALITY_LOW,
|
|
@@ -320,8 +342,10 @@ async function exportFrames(movie, options = {}) {
|
|
|
320
342
|
bitrate: qualityMap[options.audio?.bitrate ?? "high"] ?? QUALITY_HIGH
|
|
321
343
|
}
|
|
322
344
|
};
|
|
345
|
+
const container = makeOutputFormat(opts.format);
|
|
346
|
+
if (movie.audioBuffer) opts.audio.codec = await chooseAudioCodec(fmt, options.audio?.codec, container, movie.audioBuffer, opts.audio.bitrate);
|
|
323
347
|
const output = new Output({
|
|
324
|
-
format:
|
|
348
|
+
format: container,
|
|
325
349
|
target: new BufferTarget()
|
|
326
350
|
});
|
|
327
351
|
const canvasSource = new CanvasSource(movie.app.canvas, {
|
|
@@ -1120,7 +1144,7 @@ function inspectScene(root, frame, time, canvas, opts = {}) {
|
|
|
1120
1144
|
const seen = /* @__PURE__ */ new Map();
|
|
1121
1145
|
const scene = /* @__PURE__ */ new Map();
|
|
1122
1146
|
const scaledToNothing = /* @__PURE__ */ new Set();
|
|
1123
|
-
const
|
|
1147
|
+
const walk2 = (comp, prefix, insideThreeD, parentVisible, parentScene, parentMoving = false, parentZero = false) => {
|
|
1124
1148
|
const blending = /* @__PURE__ */ new Set();
|
|
1125
1149
|
for (const w of transitionWindowsOf(comp.spec)) {
|
|
1126
1150
|
const start = (comp.absoluteStart ?? 0) + w.start, end = (comp.absoluteStart ?? 0) + w.end;
|
|
@@ -1164,10 +1188,10 @@ function inspectScene(root, frame, time, canvas, opts = {}) {
|
|
|
1164
1188
|
const group = parentScene ?? (name !== void 0 && blending.has(name) ? name : void 0);
|
|
1165
1189
|
if (group) scene.set(info, group);
|
|
1166
1190
|
if (zero || seq.showsNothingYet) scaledToNothing.add(info);
|
|
1167
|
-
if (seq instanceof CompositionSequence)
|
|
1191
|
+
if (seq instanceof CompositionSequence) walk2(seq, prefix + label + "/", insideThreeD || threeD, visible, group, moving, zero);
|
|
1168
1192
|
});
|
|
1169
1193
|
};
|
|
1170
|
-
|
|
1194
|
+
walk2(root, "", false, true);
|
|
1171
1195
|
const issues = [];
|
|
1172
1196
|
const texts = layers.filter((l) => l.type === "text" && l.visible && l.alpha >= FAINT && l.bounds);
|
|
1173
1197
|
for (const l of texts) {
|
|
@@ -1204,6 +1228,158 @@ function inspectScene(root, frame, time, canvas, opts = {}) {
|
|
|
1204
1228
|
return { frame, time, canvas, summary: { layers: layers.length, visible: layers.filter((l) => l.visible).length }, issues, layers: listed };
|
|
1205
1229
|
}
|
|
1206
1230
|
|
|
1231
|
+
// src/core/timelineChart.ts
|
|
1232
|
+
var GROUP_MIN = 4;
|
|
1233
|
+
var MAX_KEYS = 200;
|
|
1234
|
+
var round = (n) => Math.round(n * 1e6) / 1e6;
|
|
1235
|
+
var family = (name) => name.replace(/\d+/g, "#");
|
|
1236
|
+
function detailOf(seq) {
|
|
1237
|
+
const s = seq.spec;
|
|
1238
|
+
if (typeof s.text === "string") return s.text.length > 60 ? s.text.slice(0, 57) + "\u2026" : s.text;
|
|
1239
|
+
if (typeof s.asset === "string") return `asset \u201C${s.asset}\u201D`;
|
|
1240
|
+
if (typeof s.sfx === "string") return `sfx \u201C${s.sfx}\u201D`;
|
|
1241
|
+
if (s.sfx && typeof s.sfx === "object") return "sfx (custom)";
|
|
1242
|
+
return void 0;
|
|
1243
|
+
}
|
|
1244
|
+
function walk(comp, parentStart, parentEnd, depth, prefix, rows, transitions) {
|
|
1245
|
+
const entries = [];
|
|
1246
|
+
comp._children.forEach((seq, i) => {
|
|
1247
|
+
const name = seq.spec.name ?? `${seq.spec.type}#${i}`;
|
|
1248
|
+
const start = parentStart + (seq.at ?? 0);
|
|
1249
|
+
const end = Math.min(parentEnd, seq.duration !== void 0 ? start + seq.duration : parentEnd);
|
|
1250
|
+
const span = end - start;
|
|
1251
|
+
const keys = (seq.spec.keyframes ?? []).map((kf) => round(start + resolveAt(kf.at, span))).sort((a, b) => a - b).slice(0, MAX_KEYS);
|
|
1252
|
+
const row = { path: prefix + name, name, type: seq.spec.type, depth, start: round(start), end: round(end), keys, detail: detailOf(seq) };
|
|
1253
|
+
const inner = [];
|
|
1254
|
+
const isComp = seq instanceof CompositionSequence;
|
|
1255
|
+
if (isComp) walk(seq, start, end, depth + 1, prefix + name + "/", inner, transitions);
|
|
1256
|
+
entries.push({ row, inner, groupable: !isComp });
|
|
1257
|
+
});
|
|
1258
|
+
rows.push(...groupFamilies(entries));
|
|
1259
|
+
for (const w of transitionWindowsOf(comp.spec)) {
|
|
1260
|
+
transitions.push({ from: prefix + w.from, to: prefix + w.to, start: round(parentStart + w.start), end: round(parentStart + w.end) });
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
function groupFamilies(entries) {
|
|
1264
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
1265
|
+
for (const e of entries) {
|
|
1266
|
+
if (!e.groupable) continue;
|
|
1267
|
+
const key = e.row.type + "|" + family(e.row.name);
|
|
1268
|
+
(buckets.get(key) ?? buckets.set(key, []).get(key)).push(e.row);
|
|
1269
|
+
}
|
|
1270
|
+
const out = [];
|
|
1271
|
+
const done = /* @__PURE__ */ new Set();
|
|
1272
|
+
for (const e of entries) {
|
|
1273
|
+
const key = e.row.type + "|" + family(e.row.name);
|
|
1274
|
+
const members = e.groupable ? buckets.get(key) : null;
|
|
1275
|
+
if (members && members.length >= GROUP_MIN) {
|
|
1276
|
+
if (done.has(key)) continue;
|
|
1277
|
+
done.add(key);
|
|
1278
|
+
const label = `${family(e.row.name)} \xD7${members.length}`;
|
|
1279
|
+
out.push({
|
|
1280
|
+
path: e.row.path.slice(0, e.row.path.length - e.row.name.length) + label,
|
|
1281
|
+
name: label,
|
|
1282
|
+
type: e.row.type,
|
|
1283
|
+
depth: e.row.depth,
|
|
1284
|
+
start: Math.min(...members.map((x) => x.start)),
|
|
1285
|
+
end: Math.max(...members.map((x) => x.end)),
|
|
1286
|
+
keys: members.flatMap((x) => x.keys).sort((a, b) => a - b).slice(0, MAX_KEYS),
|
|
1287
|
+
parts: members.map((x) => ({ start: x.start, end: x.end }))
|
|
1288
|
+
});
|
|
1289
|
+
} else {
|
|
1290
|
+
out.push(e.row, ...e.inner);
|
|
1291
|
+
continue;
|
|
1292
|
+
}
|
|
1293
|
+
out.push(...e.inner);
|
|
1294
|
+
}
|
|
1295
|
+
return out;
|
|
1296
|
+
}
|
|
1297
|
+
function collectTimeline(root, duration) {
|
|
1298
|
+
const rows = [];
|
|
1299
|
+
const transitions = [];
|
|
1300
|
+
walk(root, 0, duration, 0, "", rows, transitions);
|
|
1301
|
+
return { duration, rows, transitions };
|
|
1302
|
+
}
|
|
1303
|
+
var COLORS = {
|
|
1304
|
+
text: "#4cc9f0",
|
|
1305
|
+
shape: "#ffd166",
|
|
1306
|
+
image: "#7bd88f",
|
|
1307
|
+
video: "#c77dff",
|
|
1308
|
+
audio: "#ff7aa8",
|
|
1309
|
+
composition: "#8ea4c8",
|
|
1310
|
+
three: "#ff9f68",
|
|
1311
|
+
camera: "#b0b0b0"
|
|
1312
|
+
};
|
|
1313
|
+
var esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1314
|
+
var num = (n) => String(Math.round(n * 100) / 100);
|
|
1315
|
+
function tickStep(duration, chartWidth) {
|
|
1316
|
+
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;
|
|
1317
|
+
return 7200;
|
|
1318
|
+
}
|
|
1319
|
+
function timelineSvg(data, opts = {}) {
|
|
1320
|
+
const CHART = Math.max(100, opts.chartWidth ?? 950), withLabels = opts.labels !== false;
|
|
1321
|
+
const LABEL = withLabels ? 250 : 0, ROW = 22, TOP = 34, PAD = 16;
|
|
1322
|
+
const W = LABEL + CHART + PAD * 2, H = TOP + data.rows.length * ROW + PAD;
|
|
1323
|
+
const x = (t) => PAD + LABEL + Math.max(0, Math.min(data.duration, t)) / data.duration * CHART;
|
|
1324
|
+
const step = tickStep(data.duration, CHART);
|
|
1325
|
+
const out = [];
|
|
1326
|
+
for (let t = 0; t <= data.duration + 1e-9; t += step) {
|
|
1327
|
+
out.push(`<line class="grid" x1="${num(x(t))}" y1="${TOP - 6}" x2="${num(x(t))}" y2="${H - PAD}"/>`);
|
|
1328
|
+
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>`);
|
|
1329
|
+
}
|
|
1330
|
+
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>`);
|
|
1331
|
+
for (const t of data.transitions) {
|
|
1332
|
+
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>`);
|
|
1333
|
+
}
|
|
1334
|
+
data.rows.forEach((r, i) => {
|
|
1335
|
+
const y = TOP + i * ROW;
|
|
1336
|
+
const color = COLORS[r.type] ?? "#9aa7b8";
|
|
1337
|
+
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}` : ""}`;
|
|
1338
|
+
out.push(`<g class="row" data-start="${num(r.start)}"><title>${esc(tip)}</title>`);
|
|
1339
|
+
out.push(`<rect class="band" x="${PAD}" y="${y}" width="${LABEL + CHART}" height="${ROW}" ${i % 2 ? 'fill-opacity=".05"' : 'fill-opacity="0"'}/>`);
|
|
1340
|
+
if (withLabels) out.push(`<text class="label" x="${PAD + 6 + r.depth * 14}" y="${y + 15}">${esc(r.name)}</text>`);
|
|
1341
|
+
for (const p of r.parts ?? [{ start: r.start, end: r.end }]) {
|
|
1342
|
+
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}"/>`);
|
|
1343
|
+
}
|
|
1344
|
+
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"/>`);
|
|
1345
|
+
out.push("</g>");
|
|
1346
|
+
});
|
|
1347
|
+
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("");
|
|
1348
|
+
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}">
|
|
1349
|
+
${out.join("\n")}
|
|
1350
|
+
${legend}
|
|
1351
|
+
</svg>`;
|
|
1352
|
+
}
|
|
1353
|
+
function timelineHtml(data, opts = {}) {
|
|
1354
|
+
const W = 250 + 950 + 32;
|
|
1355
|
+
const title = esc(opts.title ?? "Timeline");
|
|
1356
|
+
return `<!doctype html>
|
|
1357
|
+
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>${title} \u2014 timeline</title>
|
|
1358
|
+
<style>
|
|
1359
|
+
:root { --bg: #f7f5f1; --ink: #1d2433; --dim: #6b7587; --grid: #d9d5cc; --key: #1d2433; --trans: #4cc9f0; }
|
|
1360
|
+
@media (prefers-color-scheme: dark) { :root { --bg: #0e1320; --ink: #e8eefc; --dim: #8fa0bf; --grid: #26304a; --key: #ffffff; --trans: #4cc9f0; } }
|
|
1361
|
+
body { margin: 0; background: var(--bg); color: var(--ink); font: 14px system-ui, sans-serif; }
|
|
1362
|
+
main { padding: 20px 16px 32px; max-width: ${W + 32}px; margin: 0 auto; overflow-x: auto; }
|
|
1363
|
+
h1 { font-size: 18px; font-weight: 600; margin: 0 0 4px; }
|
|
1364
|
+
p { margin: 0 0 14px; color: var(--dim); font-size: 13px; }
|
|
1365
|
+
svg { display: block; }
|
|
1366
|
+
.grid { stroke: var(--grid); stroke-width: 1; }
|
|
1367
|
+
.tick { fill: var(--dim); font-size: 11px; }
|
|
1368
|
+
.label { fill: var(--ink); font-size: 12px; font-family: ui-monospace, Menlo, monospace; }
|
|
1369
|
+
.band { fill: var(--ink); }
|
|
1370
|
+
.key { fill: var(--key); opacity: .85; }
|
|
1371
|
+
.transition { fill: var(--trans); opacity: .16; }
|
|
1372
|
+
.bar { opacity: .88; }
|
|
1373
|
+
.row:hover .bar { opacity: 1; }
|
|
1374
|
+
</style></head>
|
|
1375
|
+
<body><main>
|
|
1376
|
+
<h1>${title}</h1>
|
|
1377
|
+
<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>
|
|
1378
|
+
${timelineSvg(data)}
|
|
1379
|
+
</main></body></html>
|
|
1380
|
+
`;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1207
1383
|
// src/core/frames.ts
|
|
1208
1384
|
function pickFrames(lastFrame, count) {
|
|
1209
1385
|
const last = Math.max(0, Math.floor(lastFrame));
|
|
@@ -1266,6 +1442,8 @@ var Movie = class {
|
|
|
1266
1442
|
audioSource = null;
|
|
1267
1443
|
gainNode = null;
|
|
1268
1444
|
_cancelAudioStart = null;
|
|
1445
|
+
/** > 0 while the movie moves its own playhead (playback, render, snapshot, contactSheet): those are not the seeks `seeking` / `seeked` report. */
|
|
1446
|
+
_quiet = 0;
|
|
1269
1447
|
_volume = 1;
|
|
1270
1448
|
_muted = false;
|
|
1271
1449
|
isPlaying = false;
|
|
@@ -1359,6 +1537,7 @@ var Movie = class {
|
|
|
1359
1537
|
height: options.composition.height ?? this.height,
|
|
1360
1538
|
duration: options.composition.duration ?? this.duration
|
|
1361
1539
|
} : void 0;
|
|
1540
|
+
await ensureFilterLibrary(seededComposition);
|
|
1362
1541
|
const userComposition = seededComposition ? expandTransitions(seededComposition) : void 0;
|
|
1363
1542
|
const rootSeqSpec = {
|
|
1364
1543
|
type: "composition",
|
|
@@ -1392,12 +1571,32 @@ var Movie = class {
|
|
|
1392
1571
|
} catch (cleanupErr) {
|
|
1393
1572
|
console.warn("pixi-effects: cleanup after init failure also threw:", cleanupErr);
|
|
1394
1573
|
}
|
|
1574
|
+
this._emitError("init", err);
|
|
1395
1575
|
throw err;
|
|
1396
1576
|
}
|
|
1397
1577
|
}
|
|
1398
1578
|
async gotoFrame(frame, force = false) {
|
|
1399
1579
|
if (this._initState !== "ready") return;
|
|
1400
1580
|
if (!force && this.currentFrame === frame) return;
|
|
1581
|
+
if (this._quiet > 0) return this._goto(frame);
|
|
1582
|
+
const target = Math.max(0, Math.min(frame, this.totalFrames));
|
|
1583
|
+
this.emit("seeking", { frame: target, totalFrames: this.totalFrames });
|
|
1584
|
+
await this._goto(frame);
|
|
1585
|
+
this.emit("seeked", { frame: this.currentFrame, totalFrames: this.totalFrames });
|
|
1586
|
+
}
|
|
1587
|
+
/** Run `fn` with `seeking` / `seeked` silenced: the movie is moving its own playhead, not answering a seek. */
|
|
1588
|
+
async _quietly(fn) {
|
|
1589
|
+
this._quiet++;
|
|
1590
|
+
try {
|
|
1591
|
+
return await fn();
|
|
1592
|
+
} finally {
|
|
1593
|
+
this._quiet--;
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
_emitError(where, error) {
|
|
1597
|
+
this.emit("error", { where, message: error instanceof Error ? error.message : String(error), error });
|
|
1598
|
+
}
|
|
1599
|
+
async _goto(frame) {
|
|
1401
1600
|
this.currentFrame = Math.max(0, Math.min(frame, this.totalFrames));
|
|
1402
1601
|
this.timeline.time(this.currentFrame / this.frameRate);
|
|
1403
1602
|
await this._awaitVideoFrames();
|
|
@@ -1419,7 +1618,7 @@ var Movie = class {
|
|
|
1419
1618
|
async snapshot(frame = this.currentFrame, opts = {}) {
|
|
1420
1619
|
warnUnknownOptions("movie.snapshot()", opts, ["scale", "type", "as"]);
|
|
1421
1620
|
this._requireReady("snapshot");
|
|
1422
|
-
await this.gotoFrame(clampFrame(frame, this.totalFrames), true);
|
|
1621
|
+
await this._quietly(() => this.gotoFrame(clampFrame(frame, this.totalFrames), true));
|
|
1423
1622
|
const scale = opts.scale ?? 1;
|
|
1424
1623
|
const out = document.createElement("canvas");
|
|
1425
1624
|
out.width = Math.max(1, Math.round(this.width * scale));
|
|
@@ -1443,7 +1642,7 @@ var Movie = class {
|
|
|
1443
1642
|
try {
|
|
1444
1643
|
for (let i = 0; i < frames.length; i++) {
|
|
1445
1644
|
const f = frames[i];
|
|
1446
|
-
await this.gotoFrame(f, true);
|
|
1645
|
+
await this._quietly(() => this.gotoFrame(f, true));
|
|
1447
1646
|
const { x, y } = L.positions[i];
|
|
1448
1647
|
g.drawImage(this.app.canvas, x, y + LABEL_H, L.cellW, L.cellH);
|
|
1449
1648
|
g.fillStyle = "#e8ecf8";
|
|
@@ -1452,7 +1651,7 @@ var Movie = class {
|
|
|
1452
1651
|
g.fillText(`frame ${f} \xB7 ${(f / this.frameRate).toFixed(2)}s`, x + 8, y + LABEL_H / 2);
|
|
1453
1652
|
}
|
|
1454
1653
|
} finally {
|
|
1455
|
-
await this.gotoFrame(back, true);
|
|
1654
|
+
await this._quietly(() => this.gotoFrame(back, true));
|
|
1456
1655
|
}
|
|
1457
1656
|
return encodeCanvas(sheet, "image/png", opts.as ?? "blob");
|
|
1458
1657
|
}
|
|
@@ -1467,6 +1666,24 @@ var Movie = class {
|
|
|
1467
1666
|
await this.gotoFrame(f, true);
|
|
1468
1667
|
return inspectScene(this._rootSequence, f, f / this.frameRate, { width: this.width, height: this.height }, opts);
|
|
1469
1668
|
}
|
|
1669
|
+
/**
|
|
1670
|
+
* Every layer as a row with absolute start / end / keyframe times (seconds), plus the transition windows. Runs of
|
|
1671
|
+
* similar layers (`pop-1` … `pop-12`) are one row with a `parts` span each. Read it, or draw it with `timelineChart()`.
|
|
1672
|
+
*/
|
|
1673
|
+
timelineData() {
|
|
1674
|
+
this._requireReady("timelineData");
|
|
1675
|
+
return collectTimeline(this._rootSequence, this.duration);
|
|
1676
|
+
}
|
|
1677
|
+
/** 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. */
|
|
1678
|
+
timelineChart(opts = {}) {
|
|
1679
|
+
warnUnknownOptions("movie.timelineChart()", opts, ["title"]);
|
|
1680
|
+
return timelineHtml(this.timelineData(), opts);
|
|
1681
|
+
}
|
|
1682
|
+
/** Just the chart: one `<svg>` whose geometry is in `data-*` attributes (a viewer puts a playhead on it: `pixi-effects-view`). */
|
|
1683
|
+
timelineSvg(opts = {}) {
|
|
1684
|
+
warnUnknownOptions("movie.timelineSvg()", opts, ["chartWidth", "labels"]);
|
|
1685
|
+
return timelineSvg(this.timelineData(), opts);
|
|
1686
|
+
}
|
|
1470
1687
|
/**
|
|
1471
1688
|
* The soundtrack, measured: when every audio layer plays, how loud the mix is over time, and `issues`
|
|
1472
1689
|
* (limiting, inaudible layers, sounds cut off by the end). Use it to CHECK sound you cannot hear.
|
|
@@ -1517,17 +1734,20 @@ var Movie = class {
|
|
|
1517
1734
|
const frame = Math.floor(elapsed * this.frameRate);
|
|
1518
1735
|
if (frame <= this.totalFrames) {
|
|
1519
1736
|
inFlight = true;
|
|
1520
|
-
this.
|
|
1737
|
+
this._goto(frame).catch((err) => {
|
|
1521
1738
|
console.warn("pixi-effects: gotoFrame failed during playback:", err);
|
|
1739
|
+
this._emitError("playback", err);
|
|
1522
1740
|
}).finally(() => {
|
|
1523
1741
|
inFlight = false;
|
|
1524
1742
|
});
|
|
1525
1743
|
this._raf = requestAnimationFrame(tick);
|
|
1526
1744
|
} else {
|
|
1527
1745
|
this.pause();
|
|
1528
|
-
this.
|
|
1746
|
+
this._goto(this.totalFrames).catch((err) => {
|
|
1529
1747
|
console.warn("pixi-effects: final gotoFrame failed:", err);
|
|
1748
|
+
this._emitError("playback", err);
|
|
1530
1749
|
});
|
|
1750
|
+
this.emit("ended");
|
|
1531
1751
|
}
|
|
1532
1752
|
};
|
|
1533
1753
|
this._raf = requestAnimationFrame(tick);
|
|
@@ -1571,15 +1791,20 @@ var Movie = class {
|
|
|
1571
1791
|
if (wasPlaying) this.emit("pause");
|
|
1572
1792
|
}
|
|
1573
1793
|
set volume(v) {
|
|
1574
|
-
|
|
1794
|
+
const next = Math.max(0, Math.min(1, v));
|
|
1795
|
+
const changed = next !== this._volume;
|
|
1796
|
+
this._volume = next;
|
|
1575
1797
|
if (this.gainNode) this.gainNode.gain.value = this._muted ? 0 : this._volume;
|
|
1798
|
+
if (changed) this.emit("volumechange", { volume: this._volume, muted: this._muted });
|
|
1576
1799
|
}
|
|
1577
1800
|
get volume() {
|
|
1578
1801
|
return this._volume;
|
|
1579
1802
|
}
|
|
1580
1803
|
set muted(v) {
|
|
1804
|
+
const changed = !!v !== this._muted;
|
|
1581
1805
|
this._muted = !!v;
|
|
1582
1806
|
if (this.gainNode) this.gainNode.gain.value = this._muted ? 0 : this._volume;
|
|
1807
|
+
if (changed) this.emit("volumechange", { volume: this._volume, muted: this._muted });
|
|
1583
1808
|
}
|
|
1584
1809
|
get muted() {
|
|
1585
1810
|
return this._muted;
|
|
@@ -1590,7 +1815,12 @@ var Movie = class {
|
|
|
1590
1815
|
}
|
|
1591
1816
|
async render(options) {
|
|
1592
1817
|
warnUnknownOptions("movie.render()", options, ["format", "video", "audio"]);
|
|
1593
|
-
|
|
1818
|
+
try {
|
|
1819
|
+
return await this._quietly(() => exportFrames(this, options));
|
|
1820
|
+
} catch (err) {
|
|
1821
|
+
this._emitError("render", err);
|
|
1822
|
+
throw err;
|
|
1823
|
+
}
|
|
1594
1824
|
}
|
|
1595
1825
|
async destroy() {
|
|
1596
1826
|
safeRun(() => this.pause());
|