pixi-effects 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +55 -0
- package/README.md +16 -6
- package/ai/SKILL.md +46 -0
- package/ai/reference/cheatsheet.md +121 -0
- package/ai/reference/pitfalls.md +75 -0
- package/ai/reference/recipes.md +375 -0
- package/ai/template.html +82 -0
- package/ai/tools/save-image.py +24 -0
- package/dist/{Base-Bh_VSusi.d.ts → Base-5R7C5pHR.d.cts} +7 -1
- package/dist/{Base-B1Tdn0Cv.d.cts → Base-DhY9oMxr.d.ts} +7 -1
- package/dist/Composition-D7RCAGEA.cjs +13 -0
- package/dist/{Composition-BI5HZJFL.cjs.map → Composition-D7RCAGEA.cjs.map} +1 -1
- package/dist/Composition-RGTRZ2JX.js +4 -0
- package/dist/{Composition-IO7ZN32J.js.map → Composition-RGTRZ2JX.js.map} +1 -1
- package/dist/Controller.d.cts +2 -2
- package/dist/Controller.d.ts +2 -2
- package/dist/Movie-CghFXprP.d.cts +179 -0
- package/dist/Movie-DRZtIGU7.d.ts +179 -0
- package/dist/{chunk-7OIWYXGV.cjs → chunk-6BQ6IEW2.cjs} +363 -91
- package/dist/chunk-6BQ6IEW2.cjs.map +1 -0
- package/dist/{chunk-H55V3U56.js → chunk-A574IA4F.js} +68 -13
- package/dist/chunk-A574IA4F.js.map +1 -0
- package/dist/{chunk-64IHCYYN.cjs → chunk-QGBOC5YL.cjs} +76 -21
- package/dist/chunk-QGBOC5YL.cjs.map +1 -0
- package/dist/{chunk-VJCDG6YG.js → chunk-ZQX7WIYL.js} +359 -93
- package/dist/chunk-ZQX7WIYL.js.map +1 -0
- package/dist/index.cjs +366 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +61 -31
- package/dist/index.d.ts +61 -31
- package/dist/index.js +363 -22
- package/dist/index.js.map +1 -1
- package/dist/three.cjs +5 -5
- package/dist/three.d.cts +2 -2
- package/dist/three.d.ts +2 -2
- package/dist/three.js +1 -1
- package/dist/{types-CNBilhpz.d.cts → types-CRE9WKd4.d.cts} +59 -6
- package/dist/{types-CNBilhpz.d.ts → types-CRE9WKd4.d.ts} +59 -6
- package/docs/api.md +333 -0
- package/docs/dsl.md +1028 -0
- package/llms-full.txt +2001 -0
- package/llms.txt +30 -0
- package/package.json +5 -3
- package/dist/Composition-BI5HZJFL.cjs +0 -13
- package/dist/Composition-IO7ZN32J.js +0 -4
- package/dist/Movie-CcR6h2jO.d.cts +0 -82
- package/dist/Movie-D-n8glA6.d.ts +0 -82
- package/dist/chunk-64IHCYYN.cjs.map +0 -1
- package/dist/chunk-7OIWYXGV.cjs.map +0 -1
- package/dist/chunk-H55V3U56.js.map +0 -1
- package/dist/chunk-VJCDG6YG.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { CompositionSequence } from './chunk-
|
|
2
|
-
import { resolveAt } from './chunk-
|
|
3
|
-
export { registerSequenceType } from './chunk-
|
|
1
|
+
import { CompositionSequence } from './chunk-A574IA4F.js';
|
|
2
|
+
import { homeDistance, DEFAULT_FOV, DEG, resolveAt, collectPropKeys } from './chunk-ZQX7WIYL.js';
|
|
3
|
+
export { registerSequenceType } from './chunk-ZQX7WIYL.js';
|
|
4
4
|
import * as PIXI from 'pixi.js';
|
|
5
|
-
import { extensions, CullerPlugin, Application, Container, Rectangle, Assets, ExtensionType, Filter, UniformGroup, GpuProgram, GlProgram, defaultFilterVert } from 'pixi.js';
|
|
5
|
+
import { extensions, CullerPlugin, Application, Container, Rectangle, Culler, Assets, ExtensionType, Filter, UniformGroup, GpuProgram, GlProgram, defaultFilterVert } from 'pixi.js';
|
|
6
6
|
import { gsap } from 'gsap';
|
|
7
7
|
import { PixiPlugin } from 'gsap/PixiPlugin';
|
|
8
8
|
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';
|
|
@@ -501,6 +501,15 @@ var TransitionMaskFilter = class extends Filter {
|
|
|
501
501
|
};
|
|
502
502
|
|
|
503
503
|
// src/core/Transitions.ts
|
|
504
|
+
var windows = /* @__PURE__ */ new WeakMap();
|
|
505
|
+
function transitionWindowsOf(spec) {
|
|
506
|
+
return windows.get(spec) ?? [];
|
|
507
|
+
}
|
|
508
|
+
function carryTransitionWindows(from, to) {
|
|
509
|
+
const w = windows.get(from);
|
|
510
|
+
if (w) windows.set(to, w);
|
|
511
|
+
return to;
|
|
512
|
+
}
|
|
504
513
|
function expandTransitions(spec) {
|
|
505
514
|
const out = {
|
|
506
515
|
...spec,
|
|
@@ -611,29 +620,37 @@ function expandTransitions(spec) {
|
|
|
611
620
|
if (tStart < toStart || tEnd > toEnd) {
|
|
612
621
|
throw new Error(`pixi-effects: ${tag} window [${tStart}, ${tEnd}] is not covered by \`to\` "${t.to}" (lives [${toStart}, ${toEnd}], so it starts at ${toStart})`);
|
|
613
622
|
}
|
|
614
|
-
|
|
623
|
+
const record = windows.get(out) ?? [];
|
|
624
|
+
record.push({ from: t.from, to: t.to, start: tStart, end: tEnd });
|
|
625
|
+
windows.set(out, record);
|
|
626
|
+
const abs = { ...t, at: tStart };
|
|
627
|
+
const nFrom = keyframeCount(fromEntry.seq);
|
|
628
|
+
const nTo = keyframeCount(toEntry.seq);
|
|
629
|
+
switch (abs.kind) {
|
|
615
630
|
case "crossfade":
|
|
616
|
-
expandCrossfade(out,
|
|
631
|
+
expandCrossfade(out, abs, fromEntry.seq, toEntry.seq);
|
|
617
632
|
break;
|
|
618
633
|
case "wipe":
|
|
619
|
-
expandMask(out,
|
|
634
|
+
expandMask(out, abs, fromEntry.seq, toEntry.seq, i, wipeMode(abs.direction), abs.smoothing);
|
|
620
635
|
break;
|
|
621
636
|
case "iris":
|
|
622
|
-
expandMask(out,
|
|
637
|
+
expandMask(out, abs, fromEntry.seq, toEntry.seq, i, abs.mode === "out" ? "iris-out" : "iris-in", abs.smoothing);
|
|
623
638
|
break;
|
|
624
639
|
case "slide":
|
|
625
|
-
expandSlide(out,
|
|
640
|
+
expandSlide(out, abs, fromEntry.seq, toEntry.seq);
|
|
626
641
|
break;
|
|
627
642
|
case "dip":
|
|
628
|
-
expandDip(out,
|
|
643
|
+
expandDip(out, abs, fromEntry.seq, toEntry.seq);
|
|
629
644
|
break;
|
|
630
645
|
case "zoom":
|
|
631
|
-
expandZoom(out,
|
|
646
|
+
expandZoom(out, abs, fromEntry.seq, toEntry.seq);
|
|
632
647
|
break;
|
|
633
648
|
case "dissolve":
|
|
634
|
-
expandDissolve(out,
|
|
649
|
+
expandDissolve(out, abs, fromEntry.seq, toEntry.seq, i);
|
|
635
650
|
break;
|
|
636
651
|
}
|
|
652
|
+
rebaseNewKeyframes(fromEntry.seq, nFrom, fromStart);
|
|
653
|
+
rebaseNewKeyframes(toEntry.seq, nTo, toStart);
|
|
637
654
|
}
|
|
638
655
|
delete out.transitions;
|
|
639
656
|
return out;
|
|
@@ -665,6 +682,17 @@ function wrapAsFullComposition(seq, compW, compH) {
|
|
|
665
682
|
sequences: [inner]
|
|
666
683
|
};
|
|
667
684
|
}
|
|
685
|
+
function keyframeCount(seq) {
|
|
686
|
+
return seq.keyframes?.length ?? 0;
|
|
687
|
+
}
|
|
688
|
+
function rebaseNewKeyframes(seq, from, seqStart) {
|
|
689
|
+
const kfs = seq.keyframes;
|
|
690
|
+
if (!kfs) return;
|
|
691
|
+
for (let i = from; i < kfs.length; i++) {
|
|
692
|
+
const kf = kfs[i];
|
|
693
|
+
if (typeof kf.at === "number") kfs[i] = { ...kf, at: kf.at - seqStart };
|
|
694
|
+
}
|
|
695
|
+
}
|
|
668
696
|
function ensureKeyframes(seq) {
|
|
669
697
|
const s = seq;
|
|
670
698
|
if (!s.keyframes) s.keyframes = [];
|
|
@@ -881,6 +909,181 @@ function rejectReservedNameCollision(filters, seqName) {
|
|
|
881
909
|
}
|
|
882
910
|
}
|
|
883
911
|
|
|
912
|
+
// src/core/inspect.ts
|
|
913
|
+
var FAINT = 0.3;
|
|
914
|
+
var intersection = (a, b) => {
|
|
915
|
+
const w = Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x);
|
|
916
|
+
const h = Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y);
|
|
917
|
+
return w > 0 && h > 0 ? w * h : 0;
|
|
918
|
+
};
|
|
919
|
+
var clip = (a, b) => {
|
|
920
|
+
const x = Math.max(a.x, b.x), y = Math.max(a.y, b.y);
|
|
921
|
+
const w = Math.min(a.x + a.width, b.x + b.width) - x, h = Math.min(a.y + a.height, b.y + b.height) - y;
|
|
922
|
+
return w > 0 && h > 0 ? { x, y, width: w, height: h } : null;
|
|
923
|
+
};
|
|
924
|
+
function inspectScene(root, frame, time, canvas, opts = {}) {
|
|
925
|
+
const layers = [];
|
|
926
|
+
const view = { x: 0, y: 0, width: canvas.width, height: canvas.height };
|
|
927
|
+
const seen = /* @__PURE__ */ new Map();
|
|
928
|
+
const scene = /* @__PURE__ */ new Map();
|
|
929
|
+
const walk = (comp, prefix, insideThreeD, parentVisible, parentScene) => {
|
|
930
|
+
const blending = /* @__PURE__ */ new Set();
|
|
931
|
+
for (const w of transitionWindowsOf(comp.spec)) {
|
|
932
|
+
const start = (comp.absoluteStart ?? 0) + w.start, end = (comp.absoluteStart ?? 0) + w.end;
|
|
933
|
+
if (time >= start - 1e-9 && time <= end + 1e-9) {
|
|
934
|
+
blending.add(w.from);
|
|
935
|
+
blending.add(w.to);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
comp.layers().forEach(({ seq, display, threeD }, i) => {
|
|
939
|
+
const t = seq.target;
|
|
940
|
+
const name = seq.spec.name;
|
|
941
|
+
const label = name ?? `${seq.spec.type}#${i}`;
|
|
942
|
+
const alpha = t?.alpha ?? 1;
|
|
943
|
+
const alive = threeD ? display.visible : t?.renderable ?? true;
|
|
944
|
+
const visible = parentVisible && alive && alpha > 0;
|
|
945
|
+
let bounds = null;
|
|
946
|
+
if (!insideThreeD) {
|
|
947
|
+
const b = display.getBounds();
|
|
948
|
+
bounds = { x: b.x, y: b.y, width: b.width, height: b.height };
|
|
949
|
+
}
|
|
950
|
+
let onCanvas = null;
|
|
951
|
+
if (bounds) {
|
|
952
|
+
const inter = intersection(bounds, view);
|
|
953
|
+
const area = bounds.width * bounds.height;
|
|
954
|
+
onCanvas = inter === 0 && area > 0 ? "none" : inter >= area - 1e-6 ? "full" : "partial";
|
|
955
|
+
if (area === 0) onCanvas = "none";
|
|
956
|
+
}
|
|
957
|
+
const keys = collectPropKeys(seq.spec);
|
|
958
|
+
const moving = (seq.spec.keyframes ?? []).length > 0 && (keys.has("x") || keys.has("y"));
|
|
959
|
+
const info = { path: prefix + label, name, type: seq.spec.type, threeD, visible, alpha, moving, bounds, onCanvas };
|
|
960
|
+
layers.push(info);
|
|
961
|
+
let shown = bounds ? clip(bounds, view) : null;
|
|
962
|
+
const maskTarget = seq.maskSequence?.target;
|
|
963
|
+
if (shown && maskTarget && !seq.spec.maskInverted) {
|
|
964
|
+
const m = maskTarget.getBounds();
|
|
965
|
+
shown = clip(shown, { x: m.x, y: m.y, width: m.width, height: m.height });
|
|
966
|
+
}
|
|
967
|
+
seen.set(info, shown);
|
|
968
|
+
const group = parentScene ?? (name !== void 0 && blending.has(name) ? name : void 0);
|
|
969
|
+
if (group) scene.set(info, group);
|
|
970
|
+
if (seq instanceof CompositionSequence) walk(seq, prefix + label + "/", insideThreeD || threeD, visible, group);
|
|
971
|
+
});
|
|
972
|
+
};
|
|
973
|
+
walk(root, "", false, true);
|
|
974
|
+
const issues = [];
|
|
975
|
+
const texts = layers.filter((l) => l.type === "text" && l.visible && l.alpha >= FAINT && l.bounds);
|
|
976
|
+
for (const l of texts) {
|
|
977
|
+
const b = l.bounds;
|
|
978
|
+
const who = `text layer "${l.path}"`;
|
|
979
|
+
if (b.width < 1 || b.height < 1) {
|
|
980
|
+
issues.push(`${who} has no size (empty text, or not drawn yet)`);
|
|
981
|
+
} else if (l.moving || scene.has(l)) ; else if (l.onCanvas === "none") {
|
|
982
|
+
issues.push(`${who} is entirely outside the canvas (at ${Math.round(b.x)},${Math.round(b.y)}, ${Math.round(b.width)}\xD7${Math.round(b.height)})`);
|
|
983
|
+
} else if (l.onCanvas === "partial") {
|
|
984
|
+
const over = [];
|
|
985
|
+
if (b.x < 0) over.push(`${Math.round(-b.x)}px beyond the left edge`);
|
|
986
|
+
if (b.x + b.width > canvas.width) over.push(`${Math.round(b.x + b.width - canvas.width)}px beyond the right edge`);
|
|
987
|
+
if (b.y < 0) over.push(`${Math.round(-b.y)}px beyond the top edge`);
|
|
988
|
+
if (b.y + b.height > canvas.height) over.push(`${Math.round(b.y + b.height - canvas.height)}px beyond the bottom edge`);
|
|
989
|
+
issues.push(`${who} is cut off by the canvas edge: ${over.join(", ")}`);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
for (let i = 0; i < texts.length; i++) {
|
|
993
|
+
for (let j = i + 1; j < texts.length; j++) {
|
|
994
|
+
const a = seen.get(texts[i]), b = seen.get(texts[j]);
|
|
995
|
+
if (!a || !b) continue;
|
|
996
|
+
const sa = scene.get(texts[i]), sb = scene.get(texts[j]);
|
|
997
|
+
if (sa && sb && sa !== sb) continue;
|
|
998
|
+
const smaller = Math.min(a.width * a.height, b.width * b.height);
|
|
999
|
+
if (smaller <= 0) continue;
|
|
1000
|
+
const share = intersection(a, b) / smaller;
|
|
1001
|
+
if (share > 0.25) issues.push(`text layers "${texts[i].path}" and "${texts[j].path}" overlap by ${Math.round(share * 100)}% of the smaller one`);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
const mode = opts.layers ?? "all";
|
|
1005
|
+
const listed = mode === "none" ? [] : mode === "visible" ? layers.filter((l) => l.visible) : layers;
|
|
1006
|
+
return { frame, time, canvas, summary: { layers: layers.length, visible: layers.filter((l) => l.visible).length }, issues, layers: listed };
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
// src/core/frames.ts
|
|
1010
|
+
function pickFrames(lastFrame, count) {
|
|
1011
|
+
const last = Math.max(0, Math.floor(lastFrame));
|
|
1012
|
+
const n = Math.max(1, Math.floor(count));
|
|
1013
|
+
if (n === 1) return [0];
|
|
1014
|
+
const out = [];
|
|
1015
|
+
for (let i = 0; i < n; i++) {
|
|
1016
|
+
const f = Math.round(last * i / (n - 1));
|
|
1017
|
+
if (out[out.length - 1] !== f) out.push(f);
|
|
1018
|
+
}
|
|
1019
|
+
return out;
|
|
1020
|
+
}
|
|
1021
|
+
function sheetLayout(n, columns, cellW, srcW, srcH, labelH) {
|
|
1022
|
+
const cols = Math.max(1, Math.min(Math.floor(columns), n));
|
|
1023
|
+
const rows = Math.ceil(n / cols);
|
|
1024
|
+
const cellH = Math.round(cellW * srcH / srcW);
|
|
1025
|
+
const positions = Array.from({ length: n }, (_, i) => ({ x: i % cols * cellW, y: Math.floor(i / cols) * (cellH + labelH) }));
|
|
1026
|
+
return { cols, rows, cellW, cellH, width: cols * cellW, height: rows * (cellH + labelH), positions };
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
// src/core/options.ts
|
|
1030
|
+
function distance(a, b) {
|
|
1031
|
+
const dp = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
|
|
1032
|
+
for (let j = 1; j <= b.length; j++) dp[0][j] = j;
|
|
1033
|
+
for (let i = 1; i <= a.length; i++) {
|
|
1034
|
+
for (let j = 1; j <= b.length; j++) {
|
|
1035
|
+
dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
1036
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) dp[i][j] = Math.min(dp[i][j], dp[i - 2][j - 2] + 1);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
return dp[a.length][b.length];
|
|
1040
|
+
}
|
|
1041
|
+
var isSubsequence = (small, big) => {
|
|
1042
|
+
let i = 0;
|
|
1043
|
+
for (const ch of big) if (i < small.length && ch === small[i]) i++;
|
|
1044
|
+
return i === small.length;
|
|
1045
|
+
};
|
|
1046
|
+
var COMMON_ALIASES = {
|
|
1047
|
+
fps: "frameRate",
|
|
1048
|
+
framerate: "frameRate",
|
|
1049
|
+
frame_rate: "frameRate",
|
|
1050
|
+
bg: "background",
|
|
1051
|
+
backgroundcolor: "background",
|
|
1052
|
+
bgcolor: "background",
|
|
1053
|
+
seconds: "duration",
|
|
1054
|
+
length: "duration",
|
|
1055
|
+
time: "duration",
|
|
1056
|
+
w: "width",
|
|
1057
|
+
h: "height",
|
|
1058
|
+
cols: "columns",
|
|
1059
|
+
sweep: "degrees",
|
|
1060
|
+
angle: "degrees"
|
|
1061
|
+
};
|
|
1062
|
+
function suggestName(name, valid) {
|
|
1063
|
+
const n = name.toLowerCase();
|
|
1064
|
+
const alias = COMMON_ALIASES[n];
|
|
1065
|
+
if (alias && valid.includes(alias)) return alias;
|
|
1066
|
+
let best = null;
|
|
1067
|
+
for (const v of valid) {
|
|
1068
|
+
const lv = v.toLowerCase();
|
|
1069
|
+
const d = distance(n, lv);
|
|
1070
|
+
const close = d <= Math.max(1, Math.floor(Math.min(n.length, lv.length) / 3));
|
|
1071
|
+
const abbreviation = n.length >= 3 && isSubsequence(n, lv) || lv.length >= 2 && n.length > lv.length && n.includes(lv);
|
|
1072
|
+
if (!close && !abbreviation) continue;
|
|
1073
|
+
const score = close ? d : 3 + (lv.length - n.length) / 100;
|
|
1074
|
+
if (!best || score < best.score) best = { v, score };
|
|
1075
|
+
}
|
|
1076
|
+
return best ? best.v : null;
|
|
1077
|
+
}
|
|
1078
|
+
function warnUnknownOptions(where, opts, valid) {
|
|
1079
|
+
if (!opts || typeof opts !== "object") return;
|
|
1080
|
+
for (const key of Object.keys(opts)) {
|
|
1081
|
+
if (valid.includes(key)) continue;
|
|
1082
|
+
const guess = suggestName(key, valid);
|
|
1083
|
+
console.warn(`pixi-effects: ${where}: unknown option "${key}"${guess ? ` \u2014 did you mean "${guess}"?` : ""} (valid: ${valid.join(", ")})`);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
884
1087
|
// src/core/Movie.ts
|
|
885
1088
|
extensions.add(CullerPlugin);
|
|
886
1089
|
gsap.registerPlugin(PixiPlugin);
|
|
@@ -939,6 +1142,7 @@ var Movie = class {
|
|
|
939
1142
|
return this._audioContext;
|
|
940
1143
|
}
|
|
941
1144
|
async init(options = {}) {
|
|
1145
|
+
warnUnknownOptions("movie.init()", options, ["width", "height", "duration", "frameRate", "background", "canvas", "assets", "composition"]);
|
|
942
1146
|
this._initState = "pending";
|
|
943
1147
|
try {
|
|
944
1148
|
this.width = options.width ?? 1920;
|
|
@@ -990,6 +1194,7 @@ var Movie = class {
|
|
|
990
1194
|
duration: this.duration,
|
|
991
1195
|
...userComposition
|
|
992
1196
|
};
|
|
1197
|
+
if (userComposition) carryTransitionWindows(userComposition, rootSeqSpec);
|
|
993
1198
|
const composition = new CompositionSequence(rootSeqSpec, null, rootShape);
|
|
994
1199
|
await composition.build();
|
|
995
1200
|
this._rootSequence = composition;
|
|
@@ -1003,7 +1208,7 @@ var Movie = class {
|
|
|
1003
1208
|
this.timeline.progress(1).progress(0);
|
|
1004
1209
|
await this._awaitVideoFrames();
|
|
1005
1210
|
this._updateSpace();
|
|
1006
|
-
this.
|
|
1211
|
+
this._renderNow();
|
|
1007
1212
|
this._initState = "ready";
|
|
1008
1213
|
this.emit("ready");
|
|
1009
1214
|
} catch (err) {
|
|
@@ -1022,7 +1227,7 @@ var Movie = class {
|
|
|
1022
1227
|
this.timeline.time(this.currentFrame / this.frameRate);
|
|
1023
1228
|
await this._awaitVideoFrames();
|
|
1024
1229
|
this._updateSpace();
|
|
1025
|
-
this.
|
|
1230
|
+
this._renderNow();
|
|
1026
1231
|
this.emit("frame", { frame: this.currentFrame, totalFrames: this.totalFrames });
|
|
1027
1232
|
}
|
|
1028
1233
|
async _awaitVideoFrames() {
|
|
@@ -1036,6 +1241,76 @@ var Movie = class {
|
|
|
1036
1241
|
return v.awaitFrameAt(local);
|
|
1037
1242
|
}));
|
|
1038
1243
|
}
|
|
1244
|
+
async snapshot(frame = this.currentFrame, opts = {}) {
|
|
1245
|
+
warnUnknownOptions("movie.snapshot()", opts, ["scale", "type", "as"]);
|
|
1246
|
+
this._requireReady("snapshot");
|
|
1247
|
+
await this.gotoFrame(clampFrame(frame, this.totalFrames), true);
|
|
1248
|
+
const scale = opts.scale ?? 1;
|
|
1249
|
+
const out = document.createElement("canvas");
|
|
1250
|
+
out.width = Math.max(1, Math.round(this.width * scale));
|
|
1251
|
+
out.height = Math.max(1, Math.round(this.height * scale));
|
|
1252
|
+
out.getContext("2d").drawImage(this.app.canvas, 0, 0, out.width, out.height);
|
|
1253
|
+
return encodeCanvas(out, opts.type ?? "image/png", opts.as ?? "blob");
|
|
1254
|
+
}
|
|
1255
|
+
async contactSheet(opts = {}) {
|
|
1256
|
+
warnUnknownOptions("movie.contactSheet()", opts, ["frames", "times", "count", "columns", "cellWidth", "as"]);
|
|
1257
|
+
this._requireReady("contactSheet");
|
|
1258
|
+
const frames = (opts.frames ?? opts.times?.map((t) => Math.round(t * this.frameRate)) ?? pickFrames(this.totalFrames, opts.count ?? 6)).map((f) => clampFrame(f, this.totalFrames));
|
|
1259
|
+
const LABEL_H = 26;
|
|
1260
|
+
const L = sheetLayout(frames.length, opts.columns ?? 3, opts.cellWidth ?? 480, this.width, this.height, LABEL_H);
|
|
1261
|
+
const sheet = document.createElement("canvas");
|
|
1262
|
+
sheet.width = L.width;
|
|
1263
|
+
sheet.height = L.height;
|
|
1264
|
+
const g = sheet.getContext("2d");
|
|
1265
|
+
g.fillStyle = "#10131c";
|
|
1266
|
+
g.fillRect(0, 0, L.width, L.height);
|
|
1267
|
+
const back = this.currentFrame;
|
|
1268
|
+
try {
|
|
1269
|
+
for (let i = 0; i < frames.length; i++) {
|
|
1270
|
+
const f = frames[i];
|
|
1271
|
+
await this.gotoFrame(f, true);
|
|
1272
|
+
const { x, y } = L.positions[i];
|
|
1273
|
+
g.drawImage(this.app.canvas, x, y + LABEL_H, L.cellW, L.cellH);
|
|
1274
|
+
g.fillStyle = "#e8ecf8";
|
|
1275
|
+
g.font = "600 15px system-ui, sans-serif";
|
|
1276
|
+
g.textBaseline = "middle";
|
|
1277
|
+
g.fillText(`frame ${f} \xB7 ${(f / this.frameRate).toFixed(2)}s`, x + 8, y + LABEL_H / 2);
|
|
1278
|
+
}
|
|
1279
|
+
} finally {
|
|
1280
|
+
await this.gotoFrame(back, true);
|
|
1281
|
+
}
|
|
1282
|
+
return encodeCanvas(sheet, "image/png", opts.as ?? "blob");
|
|
1283
|
+
}
|
|
1284
|
+
/**
|
|
1285
|
+
* Where every layer is drawn at `frame` (canvas pixels, visibility) plus `issues` — text that is off
|
|
1286
|
+
* the canvas, cut by an edge, empty, or overlapping other text. Seeks there and stays there.
|
|
1287
|
+
*/
|
|
1288
|
+
async inspect(frame = this.currentFrame, opts = { layers: "visible" }) {
|
|
1289
|
+
warnUnknownOptions("movie.inspect()", opts, ["layers"]);
|
|
1290
|
+
this._requireReady("inspect");
|
|
1291
|
+
const f = clampFrame(frame, this.totalFrames);
|
|
1292
|
+
await this.gotoFrame(f, true);
|
|
1293
|
+
return inspectScene(this._rootSequence, f, f / this.frameRate, { width: this.width, height: this.height }, opts);
|
|
1294
|
+
}
|
|
1295
|
+
/**
|
|
1296
|
+
* Draw the stage now. PixiJS's CullerPlugin only culls inside `app.render()` (the ticker), and we render
|
|
1297
|
+
* by hand — while seeking and especially during `render()`, which stops the ticker — so without this the
|
|
1298
|
+
* `culled` flags are whatever the last tick left: a layer that is now on screen can stay culled and be
|
|
1299
|
+
* missing from the frame or from the exported video. Cull again (updating transforms: the timeline just
|
|
1300
|
+
* moved things) right before drawing.
|
|
1301
|
+
*/
|
|
1302
|
+
_renderNow() {
|
|
1303
|
+
const app = this.app;
|
|
1304
|
+
if (!app?.renderer) return;
|
|
1305
|
+
this._rootSequence?.syncFrame();
|
|
1306
|
+
Culler.shared.cull(app.stage, app.renderer.screen, false);
|
|
1307
|
+
app.renderer.render({ container: app.stage });
|
|
1308
|
+
}
|
|
1309
|
+
_requireReady(what) {
|
|
1310
|
+
if (this._initState !== "ready" || !this._rootSequence || !this.app) {
|
|
1311
|
+
throw new Error(`pixi-effects: movie.${what}() needs a ready movie \u2014 await movie.init(...) first`);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1039
1314
|
/** Projects every `threeD` layer for the current frame (see src/space). */
|
|
1040
1315
|
_updateSpace() {
|
|
1041
1316
|
if (!this._rootSequence || !this.app) return;
|
|
@@ -1121,6 +1396,7 @@ var Movie = class {
|
|
|
1121
1396
|
return this.muted;
|
|
1122
1397
|
}
|
|
1123
1398
|
async render(options) {
|
|
1399
|
+
warnUnknownOptions("movie.render()", options, ["format", "video", "audio"]);
|
|
1124
1400
|
return await exportFrames(this, options);
|
|
1125
1401
|
}
|
|
1126
1402
|
async destroy() {
|
|
@@ -1138,6 +1414,11 @@ var Movie = class {
|
|
|
1138
1414
|
this._initState = "destroyed";
|
|
1139
1415
|
}
|
|
1140
1416
|
};
|
|
1417
|
+
var clampFrame = (f, last) => Math.max(0, Math.min(Math.round(f), last));
|
|
1418
|
+
function encodeCanvas(canvas, type, as) {
|
|
1419
|
+
if (as === "dataURL") return Promise.resolve(canvas.toDataURL(type));
|
|
1420
|
+
return new Promise((resolve, reject) => canvas.toBlob((b) => b ? resolve(b) : reject(new Error("pixi-effects: could not encode the image")), type));
|
|
1421
|
+
}
|
|
1141
1422
|
function safeRun(fn) {
|
|
1142
1423
|
try {
|
|
1143
1424
|
fn();
|
|
@@ -1156,7 +1437,9 @@ function collectVideoSequences(seq, out) {
|
|
|
1156
1437
|
}
|
|
1157
1438
|
|
|
1158
1439
|
// src/presets/kenBurns.ts
|
|
1440
|
+
var KEN_BURNS_KEYS = ["asset", "name", "at", "duration", "fit", "ease", "motion", "origin", "zoom", "direction", "angle", "from", "to"];
|
|
1159
1441
|
function kenBurns(opts) {
|
|
1442
|
+
warnUnknownOptions("kenBurns()", opts, KEN_BURNS_KEYS);
|
|
1160
1443
|
const {
|
|
1161
1444
|
asset,
|
|
1162
1445
|
name,
|
|
@@ -1166,7 +1449,6 @@ function kenBurns(opts) {
|
|
|
1166
1449
|
ease = "sine.inOut"
|
|
1167
1450
|
} = opts;
|
|
1168
1451
|
const baseScale = fit;
|
|
1169
|
-
const baseAt = at ?? 0;
|
|
1170
1452
|
let initial;
|
|
1171
1453
|
let keyframes;
|
|
1172
1454
|
if (opts.motion === "still") {
|
|
@@ -1197,7 +1479,7 @@ function kenBurns(opts) {
|
|
|
1197
1479
|
scale: initialScale
|
|
1198
1480
|
};
|
|
1199
1481
|
keyframes = [
|
|
1200
|
-
{ at:
|
|
1482
|
+
{ at: 0, to: { scale: `${baseScale} * ${toZ}` }, duration, ease }
|
|
1201
1483
|
];
|
|
1202
1484
|
} else if (opts.motion === "rotation") {
|
|
1203
1485
|
const angle = clamp(opts.angle ?? 8, 0, 30);
|
|
@@ -1228,7 +1510,7 @@ function kenBurns(opts) {
|
|
|
1228
1510
|
const tHere = duration * tFrac;
|
|
1229
1511
|
const tPrev = duration * ((i - 1) / (sampleCount - 1));
|
|
1230
1512
|
keyframes.push({
|
|
1231
|
-
at:
|
|
1513
|
+
at: tPrev,
|
|
1232
1514
|
to: { rotation: angleHere, scale: scaleAt(angleHere) },
|
|
1233
1515
|
duration: tHere - tPrev,
|
|
1234
1516
|
ease: i === 1 ? ease : "none"
|
|
@@ -1249,7 +1531,7 @@ function kenBurns(opts) {
|
|
|
1249
1531
|
scale: scaleExpr
|
|
1250
1532
|
};
|
|
1251
1533
|
keyframes = [
|
|
1252
|
-
{ at:
|
|
1534
|
+
{ at: 0, to: { x: xExpr(to[0]), y: yExpr(to[1]) }, duration, ease }
|
|
1253
1535
|
];
|
|
1254
1536
|
}
|
|
1255
1537
|
const spec = {
|
|
@@ -1266,13 +1548,72 @@ function kenBurns(opts) {
|
|
|
1266
1548
|
function clamp(v, lo, hi) {
|
|
1267
1549
|
return Math.max(lo, Math.min(hi, v));
|
|
1268
1550
|
}
|
|
1551
|
+
function parseEase(name) {
|
|
1552
|
+
return gsap.parseEase(name);
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
// src/presets/orbit.ts
|
|
1556
|
+
var ORBIT_KEYS = ["duration", "degrees", "start", "radius", "center", "width", "height", "fov", "ease", "dollyZoom", "at", "name", "stepsPerSecond"];
|
|
1557
|
+
function orbit(opts) {
|
|
1558
|
+
warnUnknownOptions("orbit()", opts, ORBIT_KEYS);
|
|
1559
|
+
const {
|
|
1560
|
+
duration,
|
|
1561
|
+
degrees,
|
|
1562
|
+
width = 1280,
|
|
1563
|
+
height = 720,
|
|
1564
|
+
fov,
|
|
1565
|
+
ease = "sine.inOut",
|
|
1566
|
+
stepsPerSecond = 10
|
|
1567
|
+
} = opts;
|
|
1568
|
+
if (!(duration > 0)) throw new Error(`pixi-effects: orbit duration must be > 0 (got ${duration})`);
|
|
1569
|
+
const dolly = opts.dollyZoom;
|
|
1570
|
+
if (dolly && opts.radius !== void 0) {
|
|
1571
|
+
throw new Error("pixi-effects: orbit() cannot combine `radius` with `dollyZoom` \u2014 the radius follows the animated fov");
|
|
1572
|
+
}
|
|
1573
|
+
const [cx, cy] = opts.center ?? [width / 2, height / 2];
|
|
1574
|
+
const fixedR = opts.radius ?? homeDistance(height, fov ?? DEFAULT_FOV);
|
|
1575
|
+
const start = opts.start ?? -degrees / 2;
|
|
1576
|
+
const steps = Math.max(1, Math.round(duration * stepsPerSecond));
|
|
1577
|
+
const easeFn = parseEase(ease);
|
|
1578
|
+
const dollyEase = dolly ? parseEase(dolly.ease ?? ease) : null;
|
|
1579
|
+
const pos = (i) => {
|
|
1580
|
+
const p = i / steps;
|
|
1581
|
+
const a = (start + degrees * easeFn(p)) * DEG;
|
|
1582
|
+
if (!dolly) return { x: cx + fixedR * Math.sin(a), z: fixedR * Math.cos(a) };
|
|
1583
|
+
const f = dolly.from + (dolly.to - dolly.from) * dollyEase(p);
|
|
1584
|
+
const R = homeDistance(height, f);
|
|
1585
|
+
return { x: cx + R * Math.sin(a), z: R * Math.cos(a), fov: f };
|
|
1586
|
+
};
|
|
1587
|
+
const keyframes = [];
|
|
1588
|
+
for (let i = 0; i < steps; i++) {
|
|
1589
|
+
keyframes.push({ at: duration * i / steps, to: pos(i + 1), duration: duration / steps, ease: "none" });
|
|
1590
|
+
}
|
|
1591
|
+
const p0 = pos(0);
|
|
1592
|
+
const spec = {
|
|
1593
|
+
type: "camera",
|
|
1594
|
+
duration,
|
|
1595
|
+
initial: {
|
|
1596
|
+
x: p0.x,
|
|
1597
|
+
y: cy,
|
|
1598
|
+
z: p0.z,
|
|
1599
|
+
lookAtX: cx,
|
|
1600
|
+
lookAtY: cy,
|
|
1601
|
+
lookAtZ: 0,
|
|
1602
|
+
...p0.fov !== void 0 ? { fov: p0.fov } : fov !== void 0 ? { fov } : {}
|
|
1603
|
+
},
|
|
1604
|
+
keyframes
|
|
1605
|
+
};
|
|
1606
|
+
if (opts.at !== void 0) spec.at = opts.at;
|
|
1607
|
+
if (opts.name !== void 0) spec.name = opts.name;
|
|
1608
|
+
return spec;
|
|
1609
|
+
}
|
|
1269
1610
|
|
|
1270
1611
|
// src/transforms/withFade.ts
|
|
1271
1612
|
function withFade(spec, opts) {
|
|
1272
|
-
|
|
1613
|
+
warnUnknownOptions("withFade()", opts, ["in", "out"]);
|
|
1273
1614
|
if (opts.in != null && opts.in > 0) {
|
|
1274
1615
|
spec.initial = { ...spec.initial ?? {}, alpha: 0 };
|
|
1275
|
-
const kf = { at, to: { alpha: 1 }, duration: opts.in };
|
|
1616
|
+
const kf = { at: 0, to: { alpha: 1 }, duration: opts.in };
|
|
1276
1617
|
spec.keyframes = [...spec.keyframes ?? [], kf];
|
|
1277
1618
|
}
|
|
1278
1619
|
if (opts.out != null && opts.out > 0) {
|
|
@@ -1281,12 +1622,12 @@ function withFade(spec, opts) {
|
|
|
1281
1622
|
"withFade(spec, { out }) requires spec.duration to be set so the fade-out can be anchored to the sequence end. Set spec.duration before passing the spec in."
|
|
1282
1623
|
);
|
|
1283
1624
|
}
|
|
1284
|
-
const kf = { at:
|
|
1625
|
+
const kf = { at: spec.duration - opts.out, to: { alpha: 0 }, duration: opts.out };
|
|
1285
1626
|
spec.keyframes = [...spec.keyframes ?? [], kf];
|
|
1286
1627
|
}
|
|
1287
1628
|
return spec;
|
|
1288
1629
|
}
|
|
1289
1630
|
|
|
1290
|
-
export { Movie, kenBurns, withFade };
|
|
1631
|
+
export { Movie, kenBurns, orbit, withFade };
|
|
1291
1632
|
//# sourceMappingURL=index.js.map
|
|
1292
1633
|
//# sourceMappingURL=index.js.map
|