@pixerate/editor 0.123.0 → 0.124.1
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/dist/agent/index.cjs +2 -2
- package/dist/agent/index.cjs.map +1 -1
- package/dist/agent/index.js +3 -3
- package/dist/animation/index.d.cts +2 -1
- package/dist/animation/index.d.ts +2 -1
- package/dist/animation-editor/index.cjs.map +1 -1
- package/dist/animation-editor/index.d.cts +11 -733
- package/dist/animation-editor/index.d.ts +11 -733
- package/dist/animation-editor/index.js.map +1 -1
- package/dist/{bezier-C-W9LFyD.d.cts → bezier-CBzKKDjl.d.cts} +2 -66
- package/dist/{bezier-DQZE5dxA.d.ts → bezier-CWv6LQal.d.ts} +2 -66
- package/dist/{brushstrokeRender-M4S65DHT.cjs → brushstrokeRender-EELTV7U4.cjs} +8 -8
- package/dist/{brushstrokeRender-M4S65DHT.cjs.map → brushstrokeRender-EELTV7U4.cjs.map} +1 -1
- package/dist/{brushstrokeRender-CDDRSV2W.js → brushstrokeRender-LRV7GXGD.js} +3 -3
- package/dist/canvas/index.cjs +6 -6
- package/dist/canvas/index.d.cts +10 -6
- package/dist/canvas/index.d.ts +10 -6
- package/dist/canvas/index.js +1 -1
- package/dist/{chunk-SMJGE7HM.cjs → chunk-4QQZV5Y5.cjs} +512 -39
- package/dist/chunk-4QQZV5Y5.cjs.map +1 -0
- package/dist/{chunk-TVXFHE4T.js → chunk-64PYNEHJ.js} +45 -12
- package/dist/chunk-64PYNEHJ.js.map +1 -0
- package/dist/{chunk-2IW3BWRT.cjs → chunk-7LQO6OHH.cjs} +2 -2
- package/dist/{chunk-2IW3BWRT.cjs.map → chunk-7LQO6OHH.cjs.map} +1 -1
- package/dist/chunk-CN4PJHUG.cjs +234 -0
- package/dist/chunk-CN4PJHUG.cjs.map +1 -0
- package/dist/chunk-FHYDQOPC.cjs +215 -0
- package/dist/chunk-FHYDQOPC.cjs.map +1 -0
- package/dist/{chunk-KMBC4HNV.js → chunk-J67OLQUQ.js} +2 -2
- package/dist/chunk-JRXJTPES.js +215 -0
- package/dist/chunk-JRXJTPES.js.map +1 -0
- package/dist/{chunk-IPFRWILU.js → chunk-ORHMVUR6.js} +488 -15
- package/dist/chunk-ORHMVUR6.js.map +1 -0
- package/dist/{chunk-6CJRDJCU.cjs → chunk-PEHYWUVV.cjs} +176 -111
- package/dist/chunk-PEHYWUVV.cjs.map +1 -0
- package/dist/chunk-QEG3GRN2.js +234 -0
- package/dist/chunk-QEG3GRN2.js.map +1 -0
- package/dist/{chunk-6ZBR4J62.js → chunk-WATAQ2Z2.js} +79 -14
- package/dist/chunk-WATAQ2Z2.js.map +1 -0
- package/dist/{chunk-RLRUAPBJ.cjs → chunk-Z6OUCS2K.cjs} +47 -14
- package/dist/chunk-Z6OUCS2K.cjs.map +1 -0
- package/dist/effects/index.cjs +605 -0
- package/dist/effects/index.cjs.map +1 -0
- package/dist/effects/index.d.cts +163 -0
- package/dist/effects/index.d.ts +163 -0
- package/dist/effects/index.js +605 -0
- package/dist/effects/index.js.map +1 -0
- package/dist/evaluate-2S3vSPLy.d.cts +55 -0
- package/dist/evaluate-CfmT6xA3.d.ts +55 -0
- package/dist/{export-DfjzHAJI.d.ts → export-BGtXgMJ3.d.ts} +536 -128
- package/dist/{export-Dv0-ZVrV.d.cts → export-BdfY_lH0.d.ts} +392 -5
- package/dist/{export-Dv0-ZVrV.d.ts → export-CbpjwxaI.d.cts} +392 -5
- package/dist/{export-lU5qSpHE.d.cts → export-WmYoWmLM.d.cts} +536 -128
- package/dist/flair-CxB3fOcU.d.ts +739 -0
- package/dist/flair-D7BxyBwL.d.cts +739 -0
- package/dist/gradient-KDfCvhuj.d.cts +66 -0
- package/dist/gradient-KDfCvhuj.d.ts +66 -0
- package/dist/helpers-777mC6dc.d.cts +253 -0
- package/dist/helpers-DX2_7L5t.d.ts +253 -0
- package/dist/image-editor/index.cjs +1296 -805
- package/dist/image-editor/index.cjs.map +1 -1
- package/dist/image-editor/index.d.cts +39 -182
- package/dist/image-editor/index.d.ts +39 -182
- package/dist/image-editor/index.js +497 -6
- package/dist/image-editor/index.js.map +1 -1
- package/dist/{opentype-p8Xxf-Wh.d.ts → opentype-14PDI5D2.d.ts} +1 -1
- package/dist/{opentype-BV7wWatY.d.cts → opentype-D80AVmMu.d.cts} +1 -1
- package/dist/paint/index.cjs +2 -2
- package/dist/paint/index.d.cts +32 -10
- package/dist/paint/index.d.ts +32 -10
- package/dist/paint/index.js +1 -1
- package/dist/paint-editor/index.cjs +10 -8
- package/dist/paint-editor/index.cjs.map +1 -1
- package/dist/paint-editor/index.d.cts +11 -5
- package/dist/paint-editor/index.d.ts +11 -5
- package/dist/paint-editor/index.js +4 -2
- package/dist/paint-editor/index.js.map +1 -1
- package/dist/parameters/index.d.cts +4 -184
- package/dist/parameters/index.d.ts +4 -184
- package/dist/{pathfinder-IxcyhO00.d.cts → pathfinder-C0UoFypx.d.cts} +1 -1
- package/dist/{pathfinder-StVChJOT.d.ts → pathfinder-DjISxkUv.d.ts} +1 -1
- package/dist/{player-DrcP2xqj.d.cts → player-CBtEoU2x.d.cts} +2 -54
- package/dist/{player-DV4Jujdq.d.ts → player-cGLrngL_.d.ts} +2 -54
- package/dist/{svg-BtND4TJu.d.cts → svg-BDrjRnL1.d.cts} +3 -2
- package/dist/{svg-BfVXG7hL.d.ts → svg-C-vZ3q50.d.ts} +3 -2
- package/dist/tools/index.cjs +2 -2
- package/dist/tools/index.cjs.map +1 -1
- package/dist/tools/index.js +3 -3
- package/dist/tools-C7JtLYzo.d.ts +126 -0
- package/dist/tools-DHKGHiSK.d.cts +126 -0
- package/dist/{types-fuXEme9j.d.ts → types-BcX7sCsn.d.ts} +2 -2
- package/dist/{types-DJF3Ghn5.d.cts → types-CznabG7m.d.cts} +2 -2
- package/dist/types-D3-J3TFf.d.ts +185 -0
- package/dist/types-DMcf_z4J.d.cts +185 -0
- package/dist/vector/index.d.cts +10 -10
- package/dist/vector/index.d.ts +10 -10
- package/dist/video-editor/index.cjs +749 -277
- package/dist/video-editor/index.cjs.map +1 -1
- package/dist/video-editor/index.d.cts +265 -138
- package/dist/video-editor/index.d.ts +265 -138
- package/dist/video-editor/index.js +490 -18
- package/dist/video-editor/index.js.map +1 -1
- package/dist/{warp-BjmAgQwq.d.cts → warp-CH9MM435.d.cts} +1 -1
- package/dist/{warp-Ca5fedzS.d.ts → warp-k_PdtXOG.d.ts} +1 -1
- package/package.json +6 -1
- package/skills/pixerate-image-editor/SKILL.md +7 -0
- package/skills/pixerate-image-editor/references/provider-effects.md +35 -0
- package/skills/pixerate-video-editor/SKILL.md +39 -1
- package/dist/chunk-6CJRDJCU.cjs.map +0 -1
- package/dist/chunk-6ZBR4J62.js.map +0 -1
- package/dist/chunk-IPFRWILU.js.map +0 -1
- package/dist/chunk-RLRUAPBJ.cjs.map +0 -1
- package/dist/chunk-SMJGE7HM.cjs.map +0 -1
- package/dist/chunk-TVXFHE4T.js.map +0 -1
- /package/dist/{brushstrokeRender-CDDRSV2W.js.map → brushstrokeRender-LRV7GXGD.js.map} +0 -0
- /package/dist/{chunk-KMBC4HNV.js.map → chunk-J67OLQUQ.js.map} +0 -0
|
@@ -1,13 +1,25 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EffectControlError,
|
|
3
|
+
EffectRendererPool,
|
|
4
|
+
EffectUnsupportedError,
|
|
5
|
+
EffectsSlot,
|
|
6
|
+
createEffectPixels,
|
|
7
|
+
effectValueErrors
|
|
8
|
+
} from "./chunk-QEG3GRN2.js";
|
|
9
|
+
import {
|
|
10
|
+
validateJsonSchema
|
|
11
|
+
} from "./chunk-4FX5ACVG.js";
|
|
12
|
+
|
|
1
13
|
// src/video-editor/types.ts
|
|
2
14
|
var VIDEO_FITS = ["contain", "cover", "fill", "none"];
|
|
3
|
-
var VIDEO_TRANSITION_TYPES = ["crossfade", "dip", "wipe", "push", "slide", "iris"];
|
|
15
|
+
var VIDEO_TRANSITION_TYPES = ["crossfade", "dip", "wipe", "push", "slide", "iris", "effect"];
|
|
4
16
|
var VIDEO_WIPE_DIRECTIONS = ["left", "right", "up", "down"];
|
|
5
17
|
var videoTransitionAudioCurve = (t) => t.audioCurve ?? (t.type === "dip" ? "linear" : "equal-power");
|
|
6
18
|
var VIDEO_BLEND_MODES = ["normal", "multiply", "screen", "overlay", "darken", "lighten", "color-dodge", "color-burn", "hard-light", "soft-light", "difference", "exclusion", "add"];
|
|
7
19
|
var VIDEO_CLIP_ANIMATION_EFFECTS = ["fade", "rise", "drop", "slide-left", "slide-right", "zoom", "pop"];
|
|
8
20
|
var VIDEO_FADE_CURVES = ["linear", "equal-power", "s-curve", "exponential", "logarithmic"];
|
|
9
21
|
var VIDEO_PLACEMENT_MODES = ["refuse", "overwrite", "insert"];
|
|
10
|
-
var VIDEO_CLIP_TYPES = ["media", "text", "color", "sequence"];
|
|
22
|
+
var VIDEO_CLIP_TYPES = ["media", "text", "color", "sequence", "effect"];
|
|
11
23
|
|
|
12
24
|
// src/video-editor/keyframes.ts
|
|
13
25
|
var VIDEO_EASE_NAMES = ["linear", "hold", "ease-in", "ease-out", "ease-in-out"];
|
|
@@ -953,6 +965,11 @@ function videoClipAnimated(clip, project) {
|
|
|
953
965
|
}
|
|
954
966
|
|
|
955
967
|
// src/video-editor/evaluate.ts
|
|
968
|
+
function videoEffectValuesAt(values, clipTime) {
|
|
969
|
+
const out = {};
|
|
970
|
+
for (const [k, v] of Object.entries(values ?? {})) out[k] = isVideoKeyframes(v) ? videoValueAt(v, clipTime, 0) : v;
|
|
971
|
+
return out;
|
|
972
|
+
}
|
|
956
973
|
function videoTransformAt(transform, clipTime = 0) {
|
|
957
974
|
return { x: videoValueAt(transform?.x, clipTime, 0), y: videoValueAt(transform?.y, clipTime, 0), scale: videoValueAt(transform?.scale, clipTime, 1), rotation: videoValueAt(transform?.rotation, clipTime, 0) };
|
|
958
975
|
}
|
|
@@ -1068,12 +1085,14 @@ function tracksAt(project, tracks, time, scope) {
|
|
|
1068
1085
|
let wipe;
|
|
1069
1086
|
let dip = null;
|
|
1070
1087
|
let iris;
|
|
1088
|
+
let effectIn;
|
|
1071
1089
|
let shift = 0;
|
|
1072
1090
|
let shiftDirection = "right";
|
|
1073
1091
|
if (incoming) {
|
|
1074
1092
|
if (incoming.type === "crossfade") opacity *= incoming.progress;
|
|
1075
1093
|
else if (incoming.type === "wipe") wipe = { direction: incoming.transition.direction ?? "right", progress: incoming.progress };
|
|
1076
1094
|
else if (incoming.type === "iris") iris = { progress: incoming.progress };
|
|
1095
|
+
else if (incoming.type === "effect") effectIn = incoming;
|
|
1077
1096
|
else if (incoming.type === "push" || incoming.type === "slide") {
|
|
1078
1097
|
shiftDirection = incoming.transition.direction ?? "right";
|
|
1079
1098
|
shift = videoTransitionEase(incoming.progress) - 1;
|
|
@@ -1100,14 +1119,279 @@ function tracksAt(project, tracks, time, scope) {
|
|
|
1100
1119
|
if (clip.type === "sequence") Object.assign(layer, { sequence: clip.sequence, sourceTime, layers: nested.layers });
|
|
1101
1120
|
if (clip.type === "text") layer.text = videoTextStyle(clip);
|
|
1102
1121
|
if (clip.type === "color") layer.color = clip.color;
|
|
1122
|
+
if (clip.type === "effect") layer.generator = { effect: clip.effect, ...clip.name ? { name: clip.name } : {}, values: videoEffectValuesAt(clip.values, clipTime), time: Math.max(0, clipTime) };
|
|
1123
|
+
const effects = (clip.effects ?? []).filter((e) => e.enabled !== false).map((e) => ({ id: e.id, effect: e.effect, ...e.name ? { name: e.name } : {}, opacity: clamp012(videoValueAt(e.opacity, clipTime, 1)), values: videoEffectValuesAt(e.values, clipTime), time: clipTime })).filter((e) => e.opacity > 0);
|
|
1124
|
+
if (effects.length) layer.effects = effects;
|
|
1125
|
+
if (effectIn) {
|
|
1126
|
+
const before = videoClipBefore(track, clip);
|
|
1127
|
+
const i = before ? layers.findIndex((l) => l.clip === prefix + before.id && l.track === track.id) : -1;
|
|
1128
|
+
const from = i >= 0 ? layers.splice(i, 1)[0] : null;
|
|
1129
|
+
const t = effectIn.transition;
|
|
1130
|
+
layer.transitionEffect = { effect: t.effect.effect, ...t.effect.name ? { name: t.effect.name } : {}, values: videoEffectValuesAt(t.effect.values, 0), progress: effectIn.progress, time: effectIn.progress * t.duration, from };
|
|
1131
|
+
}
|
|
1103
1132
|
if (layer.opacity > 0) layers.push(layer);
|
|
1104
1133
|
if (dip && dip.opacity > 0) layers.push({ clip: `transition:${prefix}${incoming ? clip.id : outgoing.next.id}`, track: track.id, type: "color", color: dip.color, opacity: clamp012(dip.opacity), rect: { x: 0, y: 0, width: project.width, height: project.height }, rotation: 0 });
|
|
1105
1134
|
}
|
|
1106
1135
|
}
|
|
1107
1136
|
return { layers, audio };
|
|
1108
1137
|
}
|
|
1138
|
+
function videoLayersWithoutTransitionEffects(layers) {
|
|
1139
|
+
return layers.flatMap((l) => {
|
|
1140
|
+
const t = l.transitionEffect;
|
|
1141
|
+
if (!t) return [l];
|
|
1142
|
+
const { transitionEffect: _t, ...rest } = l;
|
|
1143
|
+
const self = { ...rest, opacity: rest.opacity * t.progress };
|
|
1144
|
+
return t.from ? [t.from, self] : [self];
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1109
1147
|
function videoFrameLeafLayers(layers) {
|
|
1110
|
-
return layers.flatMap((l) => l.layers ? videoFrameLeafLayers(l.layers) : [l]);
|
|
1148
|
+
return layers.flatMap((l) => [...l.transitionEffect?.from ? videoFrameLeafLayers([l.transitionEffect.from]) : [], ...l.layers ? videoFrameLeafLayers(l.layers) : [l]]);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// src/video-editor/effects.ts
|
|
1152
|
+
function videoProjectEffects(project) {
|
|
1153
|
+
const out = [];
|
|
1154
|
+
for (const { track } of allVideoTracks(project)) for (const clip of track.clips) out.push(...videoClipEffectUses(clip));
|
|
1155
|
+
return out;
|
|
1156
|
+
}
|
|
1157
|
+
var refFields = (r) => ({ effect: r.effect, ...r.name ? { name: r.name } : {}, ...r.provider ? { provider: r.provider } : {} });
|
|
1158
|
+
function videoClipEffectUses(clip) {
|
|
1159
|
+
const out = [];
|
|
1160
|
+
if (clip.type === "effect") out.push({ clip: clip.id, kind: "generator", ...refFields(clip), enabled: true });
|
|
1161
|
+
if (clip.transition?.type === "effect" && clip.transition.effect) out.push({ clip: clip.id, kind: "transition", ...refFields(clip.transition.effect), enabled: true });
|
|
1162
|
+
for (const e of clip.effects ?? []) out.push({ clip: clip.id, kind: "clip", instance: e.id, ...refFields(e), enabled: e.enabled !== false });
|
|
1163
|
+
return out;
|
|
1164
|
+
}
|
|
1165
|
+
var videoLayersUseEffects = (layers) => layers.some((l) => !!(l.effects?.length || l.generator || l.transitionEffect || l.layers && videoLayersUseEffects(l.layers)));
|
|
1166
|
+
var videoEffectFrameTime = (time, fps) => Math.floor(time * fps / 1e3 + 1e-6) * 1e3 / fps;
|
|
1167
|
+
var PLACEMENT = /* @__PURE__ */ new Set(["opacity", "rect", "rotation", "blend", "wipe", "iris", "grade", "track"]);
|
|
1168
|
+
var round = (_k, v) => typeof v === "number" ? Math.round(v * 100) / 100 : v;
|
|
1169
|
+
function videoEffectLayerKey(layer, width, height) {
|
|
1170
|
+
const content = layer.transitionEffect ? layer : Object.fromEntries(Object.entries(layer).filter(([k]) => !PLACEMENT.has(k)));
|
|
1171
|
+
return `${width}x${height}:${JSON.stringify(content, round)}`;
|
|
1172
|
+
}
|
|
1173
|
+
function mixEffectPixels(a, b, k) {
|
|
1174
|
+
if (k >= 1) return b;
|
|
1175
|
+
const out = createEffectPixels(a.width, a.height);
|
|
1176
|
+
for (let i = 0; i < out.data.length; i++) out.data[i] = a.data[i] + (b.data[i] - a.data[i]) * k;
|
|
1177
|
+
return out;
|
|
1178
|
+
}
|
|
1179
|
+
function overEffectPixels(base, top, opacity = 1) {
|
|
1180
|
+
const out = createEffectPixels(base.width, base.height);
|
|
1181
|
+
const o = out.data;
|
|
1182
|
+
const b = base.data;
|
|
1183
|
+
const t = top.data;
|
|
1184
|
+
for (let i = 0; i < o.length; i += 4) {
|
|
1185
|
+
const ta = t[i + 3] / 255 * opacity;
|
|
1186
|
+
const ba = b[i + 3] / 255;
|
|
1187
|
+
const a = ta + ba * (1 - ta);
|
|
1188
|
+
o[i + 3] = Math.round(a * 255);
|
|
1189
|
+
if (a <= 0) continue;
|
|
1190
|
+
for (let c = 0; c < 3; c++) o[i + c] = (t[i + c] * ta + b[i + c] * ba * (1 - ta)) / a;
|
|
1191
|
+
}
|
|
1192
|
+
return out;
|
|
1193
|
+
}
|
|
1194
|
+
function defaultToImage(p) {
|
|
1195
|
+
const data = new Uint8ClampedArray(p.data);
|
|
1196
|
+
if (typeof ImageData === "undefined") return null;
|
|
1197
|
+
const image = new ImageData(data, p.width, p.height);
|
|
1198
|
+
if (typeof OffscreenCanvas !== "undefined") {
|
|
1199
|
+
const c = new OffscreenCanvas(p.width, p.height);
|
|
1200
|
+
c.getContext("2d")?.putImageData(image, 0, 0);
|
|
1201
|
+
return c;
|
|
1202
|
+
}
|
|
1203
|
+
if (typeof document !== "undefined") {
|
|
1204
|
+
const c = document.createElement("canvas");
|
|
1205
|
+
c.width = p.width;
|
|
1206
|
+
c.height = p.height;
|
|
1207
|
+
c.getContext("2d")?.putImageData(image, 0, 0);
|
|
1208
|
+
return c;
|
|
1209
|
+
}
|
|
1210
|
+
return null;
|
|
1211
|
+
}
|
|
1212
|
+
var VideoEffectsEngine = class {
|
|
1213
|
+
slot;
|
|
1214
|
+
cache = /* @__PURE__ */ new Map();
|
|
1215
|
+
pending = /* @__PURE__ */ new Map();
|
|
1216
|
+
failures = /* @__PURE__ */ new Map();
|
|
1217
|
+
pool;
|
|
1218
|
+
descriptions = /* @__PURE__ */ new Map();
|
|
1219
|
+
listeners = /* @__PURE__ */ new Set();
|
|
1220
|
+
maxEntries;
|
|
1221
|
+
toImage;
|
|
1222
|
+
generation = 0;
|
|
1223
|
+
detach;
|
|
1224
|
+
constructor(options = {}) {
|
|
1225
|
+
this.slot = options.slot ?? new EffectsSlot();
|
|
1226
|
+
this.maxEntries = options.maxEntries ?? 120;
|
|
1227
|
+
this.toImage = options.toImage ?? defaultToImage;
|
|
1228
|
+
this.pool = new EffectRendererPool({ maxRenderers: options.maxRenderers });
|
|
1229
|
+
this.detach = this.slot.subscribe(() => {
|
|
1230
|
+
this.reset();
|
|
1231
|
+
this.notify();
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
get provider() {
|
|
1235
|
+
return this.slot.provider;
|
|
1236
|
+
}
|
|
1237
|
+
/** True when a provider is registered, so effects can be drawn. */
|
|
1238
|
+
get available() {
|
|
1239
|
+
return !!this.slot.provider;
|
|
1240
|
+
}
|
|
1241
|
+
/** The drawable picture for `key`, or `undefined` when it is not rendered (yet). */
|
|
1242
|
+
get(key) {
|
|
1243
|
+
const e = this.cache.get(key);
|
|
1244
|
+
if (!e) return void 0;
|
|
1245
|
+
this.cache.delete(key);
|
|
1246
|
+
this.cache.set(key, e);
|
|
1247
|
+
if (e.image === void 0) e.image = this.toImage(e.pixels);
|
|
1248
|
+
return e.image;
|
|
1249
|
+
}
|
|
1250
|
+
/** The rendered pixels for `key`. */
|
|
1251
|
+
pixels(key) {
|
|
1252
|
+
return this.cache.get(key)?.pixels;
|
|
1253
|
+
}
|
|
1254
|
+
/** Why the render for `key` failed, if it did (it is not retried until the cache is cleared). */
|
|
1255
|
+
failure(key) {
|
|
1256
|
+
return this.failures.get(key);
|
|
1257
|
+
}
|
|
1258
|
+
/** Every failure so far, newest last. */
|
|
1259
|
+
failuresList() {
|
|
1260
|
+
return [...this.failures].map(([key, error]) => ({ key, error }));
|
|
1261
|
+
}
|
|
1262
|
+
/** Whether `key` is being rendered. */
|
|
1263
|
+
isPending(key) {
|
|
1264
|
+
return this.pending.has(key);
|
|
1265
|
+
}
|
|
1266
|
+
/**
|
|
1267
|
+
* Starts rendering `job` into `key` unless it is cached, pending or failed. Returns true while
|
|
1268
|
+
* the picture is on its way (so the frame is waiting for it).
|
|
1269
|
+
*/
|
|
1270
|
+
request(key, job) {
|
|
1271
|
+
if (this.cache.has(key)) return false;
|
|
1272
|
+
if (this.pending.has(key)) return true;
|
|
1273
|
+
if (this.failures.has(key) || !this.slot.provider) return false;
|
|
1274
|
+
const generation = this.generation;
|
|
1275
|
+
const run = this.run(job).then((pixels) => {
|
|
1276
|
+
if (generation !== this.generation) return;
|
|
1277
|
+
this.cache.set(key, { pixels });
|
|
1278
|
+
while (this.cache.size > this.maxEntries) this.cache.delete(this.cache.keys().next().value);
|
|
1279
|
+
}).catch((error) => {
|
|
1280
|
+
if (generation !== this.generation) return;
|
|
1281
|
+
this.failures.set(key, error instanceof Error ? error : new Error(String(error)));
|
|
1282
|
+
}).finally(() => {
|
|
1283
|
+
if (this.pending.get(key) === run) this.pending.delete(key);
|
|
1284
|
+
if (generation === this.generation) this.notify();
|
|
1285
|
+
});
|
|
1286
|
+
this.pending.set(key, run);
|
|
1287
|
+
return true;
|
|
1288
|
+
}
|
|
1289
|
+
/** Resolves when nothing is being rendered. */
|
|
1290
|
+
async idle() {
|
|
1291
|
+
while (this.pending.size) await Promise.all([...this.pending.values()]);
|
|
1292
|
+
}
|
|
1293
|
+
/** Renders a job now, without the cache. Rejects with the provider's error. */
|
|
1294
|
+
async run(job) {
|
|
1295
|
+
const provider = this.slot.require();
|
|
1296
|
+
if (job.kind === "transition") {
|
|
1297
|
+
const out = await this.render(provider, job.effect, job.from.width, job.from.height, { time: job.time, progress: job.progress, values: job.values, inputs: { from: job.from, to: job.to } });
|
|
1298
|
+
return out;
|
|
1299
|
+
}
|
|
1300
|
+
let cur;
|
|
1301
|
+
if (job.kind === "generator") cur = await this.render(provider, job.effect, job.width, job.height, { time: job.time, values: job.values });
|
|
1302
|
+
else cur = job.input;
|
|
1303
|
+
return this.stack(provider, cur, job.effects);
|
|
1304
|
+
}
|
|
1305
|
+
async stack(provider, input, effects) {
|
|
1306
|
+
let cur = input;
|
|
1307
|
+
for (const e of effects) {
|
|
1308
|
+
const d = await this.describe(provider, e.effect);
|
|
1309
|
+
if (d.inputs.some((i) => i.name === "source")) {
|
|
1310
|
+
const out = await this.render(provider, e.effect, cur.width, cur.height, { time: e.time, values: e.values, inputs: { source: cur } });
|
|
1311
|
+
cur = mixEffectPixels(cur, out, e.opacity);
|
|
1312
|
+
} else {
|
|
1313
|
+
const out = await this.render(provider, e.effect, cur.width, cur.height, { time: e.time, values: e.values });
|
|
1314
|
+
cur = overEffectPixels(cur, out, e.opacity);
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
return cur;
|
|
1318
|
+
}
|
|
1319
|
+
/** The provider's description of an effect, fetched once. */
|
|
1320
|
+
describe(provider, id) {
|
|
1321
|
+
let d = this.descriptions.get(id);
|
|
1322
|
+
if (!d) {
|
|
1323
|
+
d = Promise.resolve().then(() => provider.describe(id));
|
|
1324
|
+
d.catch(() => this.descriptions.delete(id));
|
|
1325
|
+
this.descriptions.set(id, d);
|
|
1326
|
+
}
|
|
1327
|
+
return d;
|
|
1328
|
+
}
|
|
1329
|
+
/** Renders through the pool: a few live renderers, one render at a time on each. */
|
|
1330
|
+
render(provider, id, width, height, request) {
|
|
1331
|
+
return this.pool.render(provider, id, { width, height }, request);
|
|
1332
|
+
}
|
|
1333
|
+
/** Forgets every picture and failure and disposes the renderers (e.g. after the provider changes). */
|
|
1334
|
+
reset() {
|
|
1335
|
+
this.generation++;
|
|
1336
|
+
this.cache.clear();
|
|
1337
|
+
this.pending.clear();
|
|
1338
|
+
this.failures.clear();
|
|
1339
|
+
this.descriptions.clear();
|
|
1340
|
+
this.pool.clear();
|
|
1341
|
+
}
|
|
1342
|
+
subscribe(listener) {
|
|
1343
|
+
this.listeners.add(listener);
|
|
1344
|
+
return () => this.listeners.delete(listener);
|
|
1345
|
+
}
|
|
1346
|
+
notify() {
|
|
1347
|
+
for (const l of [...this.listeners]) l();
|
|
1348
|
+
}
|
|
1349
|
+
destroy() {
|
|
1350
|
+
this.reset();
|
|
1351
|
+
this.detach();
|
|
1352
|
+
this.listeners.clear();
|
|
1353
|
+
}
|
|
1354
|
+
};
|
|
1355
|
+
var VIDEO_EFFECT_PLACES = {
|
|
1356
|
+
/** A clip's effect stack: filters change the picture, overlays draw over it. */
|
|
1357
|
+
clip: ["filter", "overlay"],
|
|
1358
|
+
transition: ["transition"],
|
|
1359
|
+
/** An effect clip's picture. */
|
|
1360
|
+
generator: ["generator", "title", "overlay"]
|
|
1361
|
+
};
|
|
1362
|
+
function checkVideoEffectPlace(d, place) {
|
|
1363
|
+
const allowed = VIDEO_EFFECT_PLACES[place];
|
|
1364
|
+
if (d.kind === "motion" || !d.uses.some((u) => allowed.includes(u))) {
|
|
1365
|
+
const where = place === "clip" ? "on a clip" : place === "transition" ? "as a transition" : "as an effect clip";
|
|
1366
|
+
const instead = d.uses.includes("transition") ? "set_video_effect_transition" : d.uses.some((u) => VIDEO_EFFECT_PLACES.generator.includes(u)) ? "add_video_effect_clip" : d.uses.some((u) => u === "filter") ? "apply_effect" : null;
|
|
1367
|
+
throw new EffectUnsupportedError(
|
|
1368
|
+
`"${d.id}" (${d.name}) is a ${d.uses.join(" / ")} effect and cannot be used ${where}, which takes ${allowed.join(" or ")} effects${instead ? `; use ${instead} instead` : ""}. search_effects with use "${allowed[0]}" lists ones that fit.`,
|
|
1369
|
+
d.id
|
|
1370
|
+
);
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
function checkVideoEffectValues(d, values, options = {}) {
|
|
1374
|
+
const plain = {};
|
|
1375
|
+
const errors = [];
|
|
1376
|
+
for (const [k, v] of Object.entries(values ?? {})) {
|
|
1377
|
+
if (!isVideoKeyframes(v)) {
|
|
1378
|
+
plain[k] = v;
|
|
1379
|
+
continue;
|
|
1380
|
+
}
|
|
1381
|
+
const schema = d.controls.properties[k];
|
|
1382
|
+
const type = schema?.type;
|
|
1383
|
+
if (!options.keyframes) errors.push({ path: `/${k}`, message: "cannot be keyframed here; give a plain value." });
|
|
1384
|
+
else if (!schema) errors.push({ path: `/${k}`, message: `is not a control of "${d.id}"; its controls are ${Object.keys(d.controls.properties).join(", ") || "none"}.` });
|
|
1385
|
+
else if (type !== "number" && type !== "integer") errors.push({ path: `/${k}`, message: "only number controls can be keyframed." });
|
|
1386
|
+
else for (const [i, key] of v.keys.entries()) for (const e of validateJsonSchema(key.v, schema)) errors.push({ path: `/${k}/keys/${i}/v`, message: e.message });
|
|
1387
|
+
}
|
|
1388
|
+
for (const e of effectValueErrors(d, plain)) {
|
|
1389
|
+
const name = e.path.split("/")[1];
|
|
1390
|
+
const about = name && d.controls.properties[name]?.description;
|
|
1391
|
+
errors.push(about ? { path: e.path, message: `${e.message} (${about})` } : e);
|
|
1392
|
+
}
|
|
1393
|
+
if (errors.length) throw new EffectControlError(d.id, errors);
|
|
1394
|
+
return { ...options.base ?? d.defaults, ...values ?? {} };
|
|
1111
1395
|
}
|
|
1112
1396
|
|
|
1113
1397
|
// src/video-editor/render.ts
|
|
@@ -1127,7 +1411,7 @@ function videoWipeRect(rect, wipe) {
|
|
|
1127
1411
|
function videoFrameToSvg(frame, options = {}, assets = []) {
|
|
1128
1412
|
const defs = [];
|
|
1129
1413
|
const posters = new Map(assets.map((a) => [a.id, a.poster]));
|
|
1130
|
-
const draw = (layers, prefix) => layers.map((layer, index) => {
|
|
1414
|
+
const draw = (layers, prefix) => videoLayersWithoutTransitionEffects(layers).map((layer, index) => {
|
|
1131
1415
|
const i = `${prefix}${index}`;
|
|
1132
1416
|
const { rect } = layer;
|
|
1133
1417
|
const attrs = [];
|
|
@@ -1168,7 +1452,7 @@ function videoFrameToSvg(frame, options = {}, assets = []) {
|
|
|
1168
1452
|
const href = options.imageFor ? options.imageFor(layer, frame) : layer.assetKind === "image" ? layer.src ?? null : posters.get(layer.asset ?? "") ?? null;
|
|
1169
1453
|
if (href) inner = `<image href="${esc(href)}" ${box} preserveAspectRatio="none"/>`;
|
|
1170
1454
|
else {
|
|
1171
|
-
const label = `${layer.asset ?? layer.clip} \xB7 ${formatVideoTimecode(layer.sourceTime ?? 0, options.fps ?? 30)}`;
|
|
1455
|
+
const label = layer.generator ? `Effect: ${layer.generator.name ?? layer.generator.effect}` : `${layer.asset ?? layer.clip} \xB7 ${formatVideoTimecode(layer.sourceTime ?? 0, options.fps ?? 30)}`;
|
|
1172
1456
|
const size = Math.max(10, Math.min(rect.height / 8, rect.width / Math.max(8, label.length * 0.6)));
|
|
1173
1457
|
inner = `<rect ${box} fill="#334155" stroke="#94a3b8" stroke-width="2"/><text x="${n(rect.x + rect.width / 2)}" y="${n(rect.y + rect.height / 2)}" text-anchor="middle" dominant-baseline="central" font-family="sans-serif" font-size="${n(size)}" fill="#e2e8f0">${esc(label)}</text>`;
|
|
1174
1458
|
}
|
|
@@ -1256,6 +1540,14 @@ function paintVideoLayer(ctx, frame, layer, sourceFor, options) {
|
|
|
1256
1540
|
ctx.strokeStyle = "#94a3b8";
|
|
1257
1541
|
ctx.lineWidth = 2;
|
|
1258
1542
|
ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);
|
|
1543
|
+
if (layer.generator) {
|
|
1544
|
+
const label = `Effect: ${layer.generator.name ?? layer.generator.effect}`;
|
|
1545
|
+
ctx.font = `600 ${Math.max(12, Math.min(rect.height / 10, rect.width / Math.max(8, label.length * 0.6)))}px sans-serif`;
|
|
1546
|
+
ctx.textAlign = "center";
|
|
1547
|
+
ctx.textBaseline = "middle";
|
|
1548
|
+
ctx.fillStyle = "#e2e8f0";
|
|
1549
|
+
ctx.fillText(label, rect.x + rect.width / 2, rect.y + rect.height / 2);
|
|
1550
|
+
}
|
|
1259
1551
|
}
|
|
1260
1552
|
}
|
|
1261
1553
|
}
|
|
@@ -1265,18 +1557,118 @@ function drawVideoFrame(ctx, frame, sourceFor, scale = 1, options = {}) {
|
|
|
1265
1557
|
ctx.globalAlpha = 1;
|
|
1266
1558
|
ctx.fillStyle = frame.background;
|
|
1267
1559
|
ctx.fillRect(0, 0, frame.width, frame.height);
|
|
1268
|
-
|
|
1560
|
+
const state = { missing: 0, waiting: 0 };
|
|
1561
|
+
const counted = (layer) => {
|
|
1562
|
+
const source = sourceFor(layer);
|
|
1563
|
+
if (!source && layer.type === "media") state.missing++;
|
|
1564
|
+
return source;
|
|
1565
|
+
};
|
|
1566
|
+
drawVideoLayers(ctx, frame, frame.layers, counted, scale, options, 0, state);
|
|
1269
1567
|
ctx.restore();
|
|
1270
1568
|
}
|
|
1271
|
-
|
|
1569
|
+
var unplaced = (layer) => {
|
|
1570
|
+
const { rotation: _r, wipe: _w, iris: _i, grade: _g, blend: _b, effects: _e, transitionEffect: _t, ...rest } = layer;
|
|
1571
|
+
return { ...rest, opacity: 1, rotation: 0 };
|
|
1572
|
+
};
|
|
1573
|
+
var readPixels = (s, width, height) => {
|
|
1574
|
+
const img = s.ctx.getImageData(0, 0, width, height);
|
|
1575
|
+
return { width, height, data: img.data };
|
|
1576
|
+
};
|
|
1577
|
+
function begin(s, width, height, scale, dx = 0, dy = 0) {
|
|
1578
|
+
const g = s.ctx;
|
|
1579
|
+
g.save();
|
|
1580
|
+
g.setTransform(1, 0, 0, 1, 0, 0);
|
|
1581
|
+
g.clearRect(0, 0, width, height);
|
|
1582
|
+
g.setTransform(scale, 0, 0, scale, dx, dy);
|
|
1583
|
+
g.globalAlpha = 1;
|
|
1584
|
+
g.globalCompositeOperation = "source-over";
|
|
1585
|
+
return g;
|
|
1586
|
+
}
|
|
1587
|
+
function effectPicture(frame, layer, source, scale, options, depth, state) {
|
|
1588
|
+
const engine = options.effects;
|
|
1589
|
+
if (!engine?.available) return null;
|
|
1590
|
+
const { rect } = layer;
|
|
1591
|
+
const w = Math.max(1, Math.round(rect.width * scale));
|
|
1592
|
+
const h = Math.max(1, Math.round(rect.height * scale));
|
|
1593
|
+
const key = videoEffectLayerKey(layer, w, h);
|
|
1594
|
+
const hit = engine.get(key);
|
|
1595
|
+
if (hit !== void 0 || engine.failure(key)) return hit ?? null;
|
|
1596
|
+
let job;
|
|
1597
|
+
if (layer.generator) job = { kind: "generator", width: w, height: h, ...layer.generator, effects: layer.effects ?? [] };
|
|
1598
|
+
else {
|
|
1599
|
+
const s = options.scratch?.(w, h, 1e3 + 4 * depth);
|
|
1600
|
+
if (!s) return null;
|
|
1601
|
+
const missing = state.missing;
|
|
1602
|
+
const g = begin(s, w, h, scale, -rect.x * scale, -rect.y * scale);
|
|
1603
|
+
const bare = unplaced(layer);
|
|
1604
|
+
paintVideoLayer(g, frame, bare, (l) => source(l === bare ? layer : l), options);
|
|
1605
|
+
g.restore();
|
|
1606
|
+
if (state.missing !== missing) return null;
|
|
1607
|
+
job = { kind: "stack", input: readPixels(s, w, h), effects: layer.effects ?? [] };
|
|
1608
|
+
}
|
|
1609
|
+
if (engine.request(key, job)) {
|
|
1610
|
+
state.waiting++;
|
|
1611
|
+
options.onEffectWaiting?.();
|
|
1612
|
+
}
|
|
1613
|
+
return null;
|
|
1614
|
+
}
|
|
1615
|
+
function drawEffectTransition(ctx, frame, layer, sourceFor, scale, options, depth, state) {
|
|
1616
|
+
const engine = options.effects;
|
|
1617
|
+
const t = layer.transitionEffect;
|
|
1618
|
+
if (!engine?.available) return false;
|
|
1272
1619
|
const cw = Math.max(1, Math.round(frame.width * scale));
|
|
1273
1620
|
const ch = Math.max(1, Math.round(frame.height * scale));
|
|
1274
|
-
|
|
1621
|
+
const key = videoEffectLayerKey(layer, cw, ch);
|
|
1622
|
+
const hit = engine.get(key);
|
|
1623
|
+
if (hit) {
|
|
1624
|
+
ctx.save();
|
|
1625
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
1626
|
+
ctx.globalAlpha = 1;
|
|
1627
|
+
ctx.globalCompositeOperation = "source-over";
|
|
1628
|
+
ctx.drawImage(hit, 0, 0, cw, ch);
|
|
1629
|
+
ctx.restore();
|
|
1630
|
+
return true;
|
|
1631
|
+
}
|
|
1632
|
+
if (hit === null || engine.failure(key) || engine.isPending(key)) {
|
|
1633
|
+
if (engine.isPending(key)) {
|
|
1634
|
+
state.waiting++;
|
|
1635
|
+
options.onEffectWaiting?.();
|
|
1636
|
+
}
|
|
1637
|
+
return false;
|
|
1638
|
+
}
|
|
1639
|
+
const a = options.scratch?.(cw, ch, 1e3 + 4 * depth + 1);
|
|
1640
|
+
const b = options.scratch?.(cw, ch, 1e3 + 4 * depth + 2);
|
|
1641
|
+
if (!a || !b) return false;
|
|
1642
|
+
const before = { ...state };
|
|
1643
|
+
const ga = begin(a, cw, ch, scale);
|
|
1644
|
+
drawVideoLayers(ga, frame, t.from ? [t.from] : [], sourceFor, scale, options, depth + 1, state);
|
|
1645
|
+
ga.restore();
|
|
1646
|
+
const { transitionEffect: _t, ...incoming } = layer;
|
|
1647
|
+
const gb = begin(b, cw, ch, scale);
|
|
1648
|
+
drawVideoLayers(gb, frame, [incoming], sourceFor, scale, options, depth + 1, state);
|
|
1649
|
+
gb.restore();
|
|
1650
|
+
if (state.missing !== before.missing || state.waiting !== before.waiting) return false;
|
|
1651
|
+
const job = { kind: "transition", effect: t.effect, values: t.values, progress: t.progress, time: t.time, from: readPixels(a, cw, ch), to: readPixels(b, cw, ch) };
|
|
1652
|
+
if (engine.request(key, job)) {
|
|
1653
|
+
state.waiting++;
|
|
1654
|
+
options.onEffectWaiting?.();
|
|
1655
|
+
}
|
|
1656
|
+
return false;
|
|
1657
|
+
}
|
|
1658
|
+
function drawVideoLayers(ctx, frame, layers, sourceFor, scale, options, depth, state) {
|
|
1659
|
+
const cw = Math.max(1, Math.round(frame.width * scale));
|
|
1660
|
+
const ch = Math.max(1, Math.round(frame.height * scale));
|
|
1661
|
+
for (const original of layers) {
|
|
1662
|
+
let layer = original;
|
|
1663
|
+
if (layer.transitionEffect) {
|
|
1664
|
+
if (!drawEffectTransition(ctx, frame, layer, sourceFor, scale, options, depth, state)) drawVideoLayers(ctx, frame, videoLayersWithoutTransitionEffects([layer]), sourceFor, scale, options, depth, state);
|
|
1665
|
+
continue;
|
|
1666
|
+
}
|
|
1275
1667
|
let source = sourceFor;
|
|
1276
1668
|
if (layer.layers) {
|
|
1277
1669
|
const group = options.scratch?.(cw, ch, 2 * depth);
|
|
1278
1670
|
if (!group) {
|
|
1279
|
-
drawVideoLayers(ctx, frame, layer.layers, sourceFor, scale, options, depth + 1);
|
|
1671
|
+
drawVideoLayers(ctx, frame, layer.layers, sourceFor, scale, options, depth + 1, state);
|
|
1280
1672
|
continue;
|
|
1281
1673
|
}
|
|
1282
1674
|
const g = group.ctx;
|
|
@@ -1286,9 +1678,19 @@ function drawVideoLayers(ctx, frame, layers, sourceFor, scale, options, depth) {
|
|
|
1286
1678
|
g.setTransform(scale, 0, 0, scale, 0, 0);
|
|
1287
1679
|
g.globalAlpha = 1;
|
|
1288
1680
|
g.globalCompositeOperation = "source-over";
|
|
1289
|
-
drawVideoLayers(g, frame, layer.layers, sourceFor, scale, options, depth + 1);
|
|
1681
|
+
drawVideoLayers(g, frame, layer.layers, sourceFor, scale, options, depth + 1, state);
|
|
1290
1682
|
g.restore();
|
|
1291
|
-
|
|
1683
|
+
const self = layer;
|
|
1684
|
+
source = (l) => l === self ? group.canvas : sourceFor(l);
|
|
1685
|
+
}
|
|
1686
|
+
if (layer.effects?.length || layer.generator) {
|
|
1687
|
+
const picture = effectPicture(frame, layer, source, scale, options, depth, state);
|
|
1688
|
+
if (picture) {
|
|
1689
|
+
const { layers: _l, text: _t, color: _c, generator: _g, effects: _e, ...rest } = layer;
|
|
1690
|
+
const drawn = { ...rest, type: "media" };
|
|
1691
|
+
layer = drawn;
|
|
1692
|
+
source = (l) => l === drawn ? picture : sourceFor(l);
|
|
1693
|
+
}
|
|
1292
1694
|
}
|
|
1293
1695
|
const setting = layer.grade?.lut;
|
|
1294
1696
|
const lut = setting ? options.lutFor?.(setting.src) : null;
|
|
@@ -1320,6 +1722,41 @@ function drawVideoLayers(ctx, frame, layers, sourceFor, scale, options, depth) {
|
|
|
1320
1722
|
ctx.restore();
|
|
1321
1723
|
}
|
|
1322
1724
|
}
|
|
1725
|
+
async function renderVideoFrameExact(ctx, project, time, sourceFor, options = {}) {
|
|
1726
|
+
const frame = videoFrameAt(project, videoEffectFrameTime(time, project.fps));
|
|
1727
|
+
const engine = options.effects;
|
|
1728
|
+
const before = new Set(engine?.failuresList().map((f) => f.error) ?? []);
|
|
1729
|
+
let complete = false;
|
|
1730
|
+
for (let pass = 0; pass < (options.maxPasses ?? 8); pass++) {
|
|
1731
|
+
let waiting = 0;
|
|
1732
|
+
let missing = 0;
|
|
1733
|
+
drawVideoFrame(
|
|
1734
|
+
ctx,
|
|
1735
|
+
frame,
|
|
1736
|
+
(layer) => {
|
|
1737
|
+
if (layer.type === "effect") return null;
|
|
1738
|
+
const s = sourceFor(layer);
|
|
1739
|
+
if (!s) missing++;
|
|
1740
|
+
return s;
|
|
1741
|
+
},
|
|
1742
|
+
options.scale ?? 1,
|
|
1743
|
+
{
|
|
1744
|
+
...options,
|
|
1745
|
+
onEffectWaiting: () => {
|
|
1746
|
+
waiting++;
|
|
1747
|
+
options.onEffectWaiting?.();
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
);
|
|
1751
|
+
if (!waiting) {
|
|
1752
|
+
complete = missing === 0;
|
|
1753
|
+
break;
|
|
1754
|
+
}
|
|
1755
|
+
await engine.idle();
|
|
1756
|
+
}
|
|
1757
|
+
const failures = (engine?.failuresList() ?? []).map((f) => f.error).filter((e) => !before.has(e));
|
|
1758
|
+
return { frame, complete: complete && !failures.length, failures };
|
|
1759
|
+
}
|
|
1323
1760
|
|
|
1324
1761
|
// src/video-editor/export.ts
|
|
1325
1762
|
var reelName = (asset) => (asset?.name ?? asset?.id ?? "AX").replace(/\.[a-z0-9]+$/i, "").replace(/[^A-Za-z0-9]/g, "").toUpperCase().slice(0, 8) || "AX";
|
|
@@ -1360,11 +1797,17 @@ function toVideoEdl(project, options = {}) {
|
|
|
1360
1797
|
if (clip.type === "media") lines.push(`* FROM CLIP NAME: ${asset?.name ?? asset?.src ?? clip.asset}`);
|
|
1361
1798
|
if (clip.type === "text") lines.push(`* TITLE: ${clip.text.replace(/\n/g, " ")}`);
|
|
1362
1799
|
if (clip.type === "color") lines.push(`* COLOR: ${clip.color}`);
|
|
1800
|
+
if (clip.type === "effect") lines.push(`* EFFECT CLIP: ${clip.name ?? clip.effect} (${clip.effect})`);
|
|
1801
|
+
if (clip.transition?.type === "effect") lines.push(`* EFFECT TRANSITION: ${clip.transition.effect?.name ?? clip.transition.effect?.effect}`);
|
|
1802
|
+
for (const e of clip.effects ?? []) if (e.enabled !== false) lines.push(`* EFFECT: ${e.name ?? e.effect} (${e.effect})`);
|
|
1363
1803
|
if (clip.type === "sequence") lines.push(`* COMPOUND CLIP: ${clip.name ?? clip.sequence} (sequence ${clip.sequence} from ${tc(clip.in ?? 0)}, not flattened)`);
|
|
1364
1804
|
lines.push("");
|
|
1365
1805
|
}
|
|
1366
1806
|
return lines.join("\n");
|
|
1367
1807
|
}
|
|
1808
|
+
function videoFfmpegUnsupportedEffects(project) {
|
|
1809
|
+
return allVideoTracks(project).filter(({ track }) => track.kind === "video" && !track.hidden).flatMap(({ track }) => track.clips.flatMap(videoClipEffectUses)).filter((u) => u.enabled);
|
|
1810
|
+
}
|
|
1368
1811
|
function loudnormFilter(target = {}, measure = false) {
|
|
1369
1812
|
const I = target.integrated ?? -14;
|
|
1370
1813
|
const TP = target.truePeak ?? -1;
|
|
@@ -1447,7 +1890,21 @@ function buildFfmpeg(project, options, measure) {
|
|
|
1447
1890
|
const H = even(project.height);
|
|
1448
1891
|
const fps = project.fps;
|
|
1449
1892
|
const inputs = [];
|
|
1450
|
-
const
|
|
1893
|
+
const frames = measure ? void 0 : options.frames;
|
|
1894
|
+
if (!measure && !frames) {
|
|
1895
|
+
const effects = videoFfmpegUnsupportedEffects(project);
|
|
1896
|
+
if (effects.length) {
|
|
1897
|
+
const list = effects.slice(0, 5).map((u) => `${u.kind === "generator" ? "effect clip" : u.kind === "transition" ? "the transition into" : "an effect on"} "${u.clip}" (${u.name ?? u.effect})`).join(", ");
|
|
1898
|
+
throw new Error(
|
|
1899
|
+
`ffmpeg cannot draw provider effects, and this project uses ${effects.length}: ${list}${effects.length > 5 ? ", ..." : ""}. Render the picture frame by frame with renderVideoFrameExact (the preview's drawing, with the effects provider) to PNGs or raw RGBA and pass them as frames: { input: "frames/%06d.png" } (or { input: "pipe:0", format: "rawvideo" }); ffmpeg still mixes the sound. Or turn the effects off.`
|
|
1900
|
+
);
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
if (frames) {
|
|
1904
|
+
if (frames.format === "rawvideo") inputs.push(["-f", "rawvideo", "-pix_fmt", "rgba", "-s", `${W}x${H}`, "-framerate", String(fps), "-i", frames.input]);
|
|
1905
|
+
else inputs.push(["-framerate", String(fps), "-i", frames.input]);
|
|
1906
|
+
}
|
|
1907
|
+
const graph = measure || frames ? [] : [`color=c=${color(project.background ?? "#000000")}:s=${W}x${H}:r=${fps}:d=${sec(D)},format=yuva420p[base]`];
|
|
1451
1908
|
let usesDrawtext = false;
|
|
1452
1909
|
const animated = /* @__PURE__ */ new Set();
|
|
1453
1910
|
const ramped = /* @__PURE__ */ new Set();
|
|
@@ -1739,10 +2196,11 @@ function buildFfmpeg(project, options, measure) {
|
|
|
1739
2196
|
}
|
|
1740
2197
|
return { video: cur2, audio: sounds };
|
|
1741
2198
|
};
|
|
1742
|
-
const top = compose(project.tracks, measure ? null : "base", D, { within: [], picture: !measure, sound: true });
|
|
2199
|
+
const top = compose(project.tracks, measure || frames ? null : "base", D, { within: [], picture: !measure && !frames, sound: true });
|
|
1743
2200
|
const audio = top.audio;
|
|
1744
2201
|
const cur = top.video;
|
|
1745
|
-
if (
|
|
2202
|
+
if (frames) graph.push(`[0:v]scale=${W}:${H},format=yuv420p,trim=duration=${sec(D)}[vout]`);
|
|
2203
|
+
else if (!measure) graph.push(`[${cur}]format=yuv420p[vout]`);
|
|
1746
2204
|
if (measure && !audio.length) throw new Error("Nothing on the timeline makes a sound, so there is no loudness to measure.");
|
|
1747
2205
|
const loudness = options.loudness || measure ? `,${loudnormFilter(options.loudness, measure)}` : "";
|
|
1748
2206
|
if (audio.length) graph.push(`${audio.join("")}amix=inputs=${audio.length}:normalize=0:duration=longest,apad,atrim=0:${sec(D)}${loudness}[aout]`);
|
|
@@ -1883,6 +2341,7 @@ export {
|
|
|
1883
2341
|
videoClipAnimationProperties,
|
|
1884
2342
|
videoClipAnimationSpans,
|
|
1885
2343
|
videoClipAnimated,
|
|
2344
|
+
videoEffectValuesAt,
|
|
1886
2345
|
videoTransformAt,
|
|
1887
2346
|
placeVideoRect,
|
|
1888
2347
|
videoTextStyle,
|
|
@@ -1891,18 +2350,32 @@ export {
|
|
|
1891
2350
|
videoFadeCurveGain,
|
|
1892
2351
|
videoClipSoundGain,
|
|
1893
2352
|
videoFrameAt,
|
|
2353
|
+
videoLayersWithoutTransitionEffects,
|
|
1894
2354
|
videoFrameLeafLayers,
|
|
2355
|
+
videoProjectEffects,
|
|
2356
|
+
videoClipEffectUses,
|
|
2357
|
+
videoLayersUseEffects,
|
|
2358
|
+
videoEffectFrameTime,
|
|
2359
|
+
videoEffectLayerKey,
|
|
2360
|
+
mixEffectPixels,
|
|
2361
|
+
overEffectPixels,
|
|
2362
|
+
VideoEffectsEngine,
|
|
2363
|
+
VIDEO_EFFECT_PLACES,
|
|
2364
|
+
checkVideoEffectPlace,
|
|
2365
|
+
checkVideoEffectValues,
|
|
1895
2366
|
videoIrisRadius,
|
|
1896
2367
|
videoWipeRect,
|
|
1897
2368
|
videoFrameToSvg,
|
|
1898
2369
|
videoProjectSvg,
|
|
1899
2370
|
videoContactSheetSvg,
|
|
1900
2371
|
drawVideoFrame,
|
|
2372
|
+
renderVideoFrameExact,
|
|
1901
2373
|
toVideoEdl,
|
|
2374
|
+
videoFfmpegUnsupportedEffects,
|
|
1902
2375
|
parseVideoLoudnessMeasurement,
|
|
1903
2376
|
ffmpegFilterValue,
|
|
1904
2377
|
videoShellQuote,
|
|
1905
2378
|
toVideoFfmpeg,
|
|
1906
2379
|
toVideoLoudnessMeasure
|
|
1907
2380
|
};
|
|
1908
|
-
//# sourceMappingURL=chunk-
|
|
2381
|
+
//# sourceMappingURL=chunk-ORHMVUR6.js.map
|