@pixerate/editor 0.160.0 → 0.162.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/dist/agent/index.cjs +36 -35
- package/dist/agent/index.js +7 -6
- package/dist/animation/index.cjs +5 -4
- package/dist/animation/index.js +4 -3
- package/dist/animation-editor/index.cjs +62 -61
- package/dist/animation-editor/index.js +6 -5
- package/dist/canvas/index.cjs +33 -32
- package/dist/canvas/index.js +6 -5
- package/dist/chart/index.cjs +88 -87
- package/dist/chart/index.js +7 -6
- package/dist/{chunk-QMI5HZWA.cjs → chunk-236TKNUR.cjs} +3 -3
- package/dist/{chunk-6UPGM3JB.js → chunk-2TKHLX6H.js} +1 -1
- package/dist/{chunk-3YDQLQH2.cjs → chunk-32MMQYCQ.cjs} +81 -81
- package/dist/{chunk-MHEXLG3M.cjs → chunk-3GJH2MGH.cjs} +37 -37
- package/dist/{chunk-AKDWAWSY.cjs → chunk-3IHNXRUV.cjs} +45 -45
- package/dist/{chunk-VKF3PWKR.cjs → chunk-3OSXHLIT.cjs} +259 -28
- package/dist/{chunk-TVO6W2Q6.cjs → chunk-4NGVHHJB.cjs} +17 -17
- package/dist/chunk-4VU4CP7R.js +769 -0
- package/dist/chunk-6DM3PUQD.cjs +767 -0
- package/dist/{chunk-DBIVICAO.js → chunk-7CKPUUN4.js} +1 -1
- package/dist/{chunk-57JLBGJ5.cjs → chunk-7YBDRJXT.cjs} +8 -8
- package/dist/{chunk-KY3FYKP3.js → chunk-BQWGN5WH.js} +1 -1
- package/dist/{chunk-LEUZ4FRC.cjs → chunk-CIPIESX4.cjs} +3 -3
- package/dist/chunk-CJE5MOIS.js +347 -0
- package/dist/{chunk-5QBRAIVK.js → chunk-CX6JLDFC.js} +3 -3
- package/dist/{chunk-R3QY7PPL.js → chunk-DTSTNRPH.js} +1 -1
- package/dist/{chunk-6LUYFHHN.cjs → chunk-EQ46F75O.cjs} +51 -51
- package/dist/{chunk-UKCJ43YL.cjs → chunk-ES3UEVUT.cjs} +130 -898
- package/dist/{chunk-UM3RPYC5.js → chunk-EVPZ5IKN.js} +1 -1
- package/dist/{chunk-XNQ5EFB4.cjs → chunk-EY22AY6R.cjs} +16 -16
- package/dist/chunk-FKUKJ3XA.cjs +347 -0
- package/dist/{chunk-YABNLNY7.js → chunk-GLMP22FT.js} +3 -3
- package/dist/{chunk-SUF6WRXR.cjs → chunk-GTELFJRE.cjs} +2 -2
- package/dist/{chunk-72BGL3I2.js → chunk-GWV4T5NZ.js} +1 -1
- package/dist/{chunk-AY65VQSU.js → chunk-H3OEM3Q2.js} +1 -1
- package/dist/{chunk-4YWAEV4W.cjs → chunk-HDGBM7EA.cjs} +26 -26
- package/dist/{chunk-H5XX4KHB.js → chunk-HLOKB7WW.js} +5 -3
- package/dist/{chunk-WNAAWLZ5.js → chunk-KGTF55R3.js} +2 -2
- package/dist/{chunk-WPPJH6X4.cjs → chunk-LL3H5XUH.cjs} +6 -4
- package/dist/{chunk-4T44CJ5K.cjs → chunk-NA4UVVJ4.cjs} +15 -15
- package/dist/{chunk-Q6PA362P.js → chunk-NO5QQDHZ.js} +2 -2
- package/dist/{chunk-AA34BA7G.js → chunk-QYZGBVNC.js} +30 -798
- package/dist/chunk-S6X2QRVU.cjs +769 -0
- package/dist/{chunk-SSCL7QQ4.js → chunk-XDCFJEU7.js} +2 -2
- package/dist/{chunk-IQ6KKXOU.js → chunk-YN4AYYBJ.js} +243 -12
- package/dist/chunk-ZGOHKS3X.js +767 -0
- package/dist/color/index.cjs +6 -2
- package/dist/color/index.d.cts +1 -0
- package/dist/color/index.d.ts +1 -0
- package/dist/color/index.js +28 -24
- package/dist/color/suggest.d.cts +12 -0
- package/dist/color/suggest.d.ts +12 -0
- package/dist/effects/index.cjs +44 -771
- package/dist/effects/index.js +34 -761
- package/dist/effects/ui.d.cts +20 -3
- package/dist/effects/ui.d.ts +20 -3
- package/dist/export/index.cjs +6 -5
- package/dist/export/index.js +5 -4
- package/dist/extensions/index.cjs +9 -8
- package/dist/extensions/index.js +8 -7
- package/dist/gradient/index.cjs +28 -26
- package/dist/gradient/index.js +7 -5
- package/dist/image-editor/index.cjs +638 -637
- package/dist/image-editor/index.js +5 -4
- package/dist/index.cjs +15 -14
- package/dist/index.js +9 -8
- package/dist/paint-editor/index.cjs +17 -16
- package/dist/paint-editor/index.js +6 -5
- package/dist/typography/accessibility.d.cts +44 -0
- package/dist/typography/accessibility.d.ts +44 -0
- package/dist/typography/catalog.d.cts +98 -0
- package/dist/typography/catalog.d.ts +98 -0
- package/dist/typography/index.cjs +1099 -0
- package/dist/typography/index.d.cts +16 -0
- package/dist/typography/index.d.ts +16 -0
- package/dist/typography/index.js +1099 -0
- package/dist/typography/metrics.d.cts +31 -0
- package/dist/typography/metrics.d.ts +31 -0
- package/dist/typography/picker.d.cts +132 -0
- package/dist/typography/picker.d.ts +132 -0
- package/dist/typography/registry.d.cts +66 -0
- package/dist/typography/registry.d.ts +66 -0
- package/dist/typography/scale.d.cts +62 -0
- package/dist/typography/scale.d.ts +62 -0
- package/dist/typography/segment.d.cts +26 -0
- package/dist/typography/segment.d.ts +26 -0
- package/dist/typography/stack.d.cts +26 -0
- package/dist/typography/stack.d.ts +26 -0
- package/dist/typography/tools.d.cts +34 -0
- package/dist/typography/tools.d.ts +34 -0
- package/dist/typography/types.d.cts +96 -0
- package/dist/typography/types.d.ts +96 -0
- package/dist/typography/weight.d.cts +13 -0
- package/dist/typography/weight.d.ts +13 -0
- package/dist/vector/index.cjs +950 -949
- package/dist/vector/index.js +5 -4
- package/dist/video-editor/audio.d.cts +9 -0
- package/dist/video-editor/audio.d.ts +9 -0
- package/dist/video-editor/check.d.cts +1 -1
- package/dist/video-editor/check.d.ts +1 -1
- package/dist/video-editor/controller.d.cts +47 -0
- package/dist/video-editor/controller.d.ts +47 -0
- package/dist/video-editor/dialogue-tools.d.cts +19 -0
- package/dist/video-editor/dialogue-tools.d.ts +19 -0
- package/dist/video-editor/effect-authoring.d.cts +54 -0
- package/dist/video-editor/effect-authoring.d.ts +54 -0
- package/dist/video-editor/evaluate.d.cts +11 -0
- package/dist/video-editor/evaluate.d.ts +11 -0
- package/dist/video-editor/export.d.cts +7 -1
- package/dist/video-editor/export.d.ts +7 -1
- package/dist/video-editor/index.cjs +1857 -531
- package/dist/video-editor/index.d.cts +7 -0
- package/dist/video-editor/index.d.ts +7 -0
- package/dist/video-editor/index.js +1516 -190
- package/dist/video-editor/lut.d.cts +12 -2
- package/dist/video-editor/lut.d.ts +12 -2
- package/dist/video-editor/mask.d.cts +48 -0
- package/dist/video-editor/mask.d.ts +48 -0
- package/dist/video-editor/mix.d.cts +107 -0
- package/dist/video-editor/mix.d.ts +107 -0
- package/dist/video-editor/music-tools.d.cts +19 -0
- package/dist/video-editor/music-tools.d.ts +19 -0
- package/dist/video-editor/parse.d.cts +3 -1
- package/dist/video-editor/parse.d.ts +3 -1
- package/dist/video-editor/plan.d.cts +75 -0
- package/dist/video-editor/plan.d.ts +75 -0
- package/dist/video-editor/render.d.cts +1 -1
- package/dist/video-editor/render.d.ts +1 -1
- package/dist/video-editor/timeline.d.cts +2 -0
- package/dist/video-editor/timeline.d.ts +2 -0
- package/dist/video-editor/tools.d.cts +90 -0
- package/dist/video-editor/tools.d.ts +90 -0
- package/dist/video-editor/transcript-controller.d.cts +106 -0
- package/dist/video-editor/transcript-controller.d.ts +106 -0
- package/dist/video-editor/transcript.d.cts +40 -0
- package/dist/video-editor/transcript.d.ts +40 -0
- package/dist/video-editor/types.d.cts +62 -2
- package/dist/video-editor/types.d.ts +62 -2
- package/dist/video-editor/ui.d.cts +21 -1
- package/dist/video-editor/ui.d.ts +21 -1
- package/package.json +11 -1
- package/skills/README.md +3 -0
- package/skills/pixerate-bgm-alignment/SKILL.md +82 -0
- package/skills/pixerate-talking-head-edit/SKILL.md +80 -0
- package/skills/pixerate-typography/SKILL.md +91 -0
- package/skills/pixerate-video-editor/SKILL.md +40 -2
|
@@ -19,9 +19,11 @@ var VIDEO_WIPE_DIRECTIONS = ["left", "right", "up", "down"];
|
|
|
19
19
|
var videoTransitionAudioCurve = (t) => _nullishCoalesce(t.audioCurve, () => ( (t.type === "dip" ? "linear" : "equal-power")));
|
|
20
20
|
var VIDEO_BLEND_MODES = ["normal", "multiply", "screen", "overlay", "darken", "lighten", "color-dodge", "color-burn", "hard-light", "soft-light", "difference", "exclusion", "add"];
|
|
21
21
|
var VIDEO_CLIP_ANIMATION_EFFECTS = ["fade", "rise", "drop", "slide-left", "slide-right", "zoom", "pop"];
|
|
22
|
+
var VIDEO_MASK_SHAPES = ["rectangle", "ellipse", "linear", "polygon"];
|
|
22
23
|
var VIDEO_FADE_CURVES = ["linear", "equal-power", "s-curve", "exponential", "logarithmic"];
|
|
24
|
+
var VIDEO_ADJUSTMENT_SCOPES = ["underneath", "all", "track"];
|
|
23
25
|
var VIDEO_PLACEMENT_MODES = ["refuse", "overwrite", "insert"];
|
|
24
|
-
var VIDEO_CLIP_TYPES = ["media", "text", "color", "sequence", "effect"];
|
|
26
|
+
var VIDEO_CLIP_TYPES = ["media", "text", "color", "sequence", "effect", "adjustment"];
|
|
25
27
|
|
|
26
28
|
// src/video-editor/keyframes.ts
|
|
27
29
|
var VIDEO_EASE_NAMES = ["linear", "hold", "ease-in", "ease-out", "ease-in-out"];
|
|
@@ -318,9 +320,25 @@ function applyVideoLut(lut, rgb, out = [0, 0, 0]) {
|
|
|
318
320
|
for (let k = 0; k < 3; k++) out[k] = clamp01(w0 * data[o + k] + w1 * data[o + a + k] + w2 * data[o + c + k] + w3 * data[o + d + k]);
|
|
319
321
|
return out;
|
|
320
322
|
}
|
|
321
|
-
function
|
|
323
|
+
function videoSkinToneWeight(r, g, b) {
|
|
324
|
+
const R = r * 255;
|
|
325
|
+
const G = g * 255;
|
|
326
|
+
const B = b * 255;
|
|
327
|
+
const y = 0.299 * R + 0.587 * G + 0.114 * B;
|
|
328
|
+
if (y < 20 || y > 250) return 0;
|
|
329
|
+
const cb = 128 - 0.168736 * R - 0.331264 * G + 0.5 * B;
|
|
330
|
+
const cr = 128 + 0.5 * R - 0.418688 * G - 0.081312 * B;
|
|
331
|
+
const d = Math.hypot((cb - 108) / 20, (cr - 154) / 18);
|
|
332
|
+
let w = d <= 1 ? 1 : d >= 1.5 ? 0 : 1 - smooth((d - 1) / 0.5);
|
|
333
|
+
if (y < 40) w *= (y - 20) / 20;
|
|
334
|
+
else if (y > 235) w *= (250 - y) / 15;
|
|
335
|
+
return w;
|
|
336
|
+
}
|
|
337
|
+
var smooth = (t) => t * t * (3 - 2 * t);
|
|
338
|
+
function applyVideoLutToPixels(lut, data, strength = 1, preserveSkinTones = 0) {
|
|
322
339
|
const s = clamp01(strength);
|
|
323
340
|
if (!s) return;
|
|
341
|
+
const skin = clamp01(preserveSkinTones);
|
|
324
342
|
const rgb = [0, 0, 0];
|
|
325
343
|
const out = [0, 0, 0];
|
|
326
344
|
for (let i = 0; i < data.length; i += 4) {
|
|
@@ -329,7 +347,8 @@ function applyVideoLutToPixels(lut, data, strength = 1) {
|
|
|
329
347
|
rgb[1] = data[i + 1] / 255;
|
|
330
348
|
rgb[2] = data[i + 2] / 255;
|
|
331
349
|
applyVideoLut(lut, rgb, out);
|
|
332
|
-
|
|
350
|
+
const k = skin ? s * (1 - skin * videoSkinToneWeight(rgb[0], rgb[1], rgb[2])) : s;
|
|
351
|
+
for (let c = 0; c < 3; c++) data[i + c] = Math.round((rgb[c] + (out[c] - rgb[c]) * k) * 255);
|
|
333
352
|
}
|
|
334
353
|
}
|
|
335
354
|
var approximations = /* @__PURE__ */ new WeakMap();
|
|
@@ -484,7 +503,8 @@ function normalizeVideoGradeLut(lut) {
|
|
|
484
503
|
if (!lut || typeof lut.src !== "string" || !lut.src) return void 0;
|
|
485
504
|
const strength = typeof lut.strength === "number" && Number.isFinite(lut.strength) ? Math.round(Math.min(1, Math.max(0, lut.strength)) * 1e3) / 1e3 : 1;
|
|
486
505
|
if (!strength) return void 0;
|
|
487
|
-
|
|
506
|
+
const skin = typeof lut.preserveSkinTones === "number" && Number.isFinite(lut.preserveSkinTones) ? Math.round(Math.min(1, Math.max(0, lut.preserveSkinTones)) * 1e3) / 1e3 : 0;
|
|
507
|
+
return { src: lut.src, ...strength !== 1 ? { strength } : {}, ...lut.name ? { name: lut.name } : {}, ...skin ? { preserveSkinTones: skin } : {} };
|
|
488
508
|
}
|
|
489
509
|
function normalizeVideoGrade(grade) {
|
|
490
510
|
if (!grade) return void 0;
|
|
@@ -572,7 +592,7 @@ function applyVideoGrade(grade, rgb, lut) {
|
|
|
572
592
|
const setting = normalizeVideoGradeLut(_optionalChain([grade, 'optionalAccess', _3 => _3.lut]));
|
|
573
593
|
if (setting && lut) {
|
|
574
594
|
const out = applyVideoLut(lut, v);
|
|
575
|
-
const s = _nullishCoalesce(setting.strength, () => ( 1));
|
|
595
|
+
const s = (_nullishCoalesce(setting.strength, () => ( 1))) * (1 - (_nullishCoalesce(setting.preserveSkinTones, () => ( 0))) * videoSkinToneWeight(v[0], v[1], v[2]));
|
|
576
596
|
v = v.map((x, k) => x + (out[k] - x) * s);
|
|
577
597
|
}
|
|
578
598
|
return v.map((x) => Math.round(x * 255));
|
|
@@ -717,6 +737,7 @@ var videoSlowMotionRamp = (duration) => [
|
|
|
717
737
|
// src/video-editor/timeline.ts
|
|
718
738
|
var VIDEO_TIME_EPSILON = 0.5;
|
|
719
739
|
var videoClipEnd = (clip) => clip.start + clip.duration;
|
|
740
|
+
var videoClipTypeLabel = (clip) => ({ text: "title", color: "colour", effect: "effect clip", adjustment: "adjustment clip", sequence: "compound clip", media: "media clip" })[clip.type];
|
|
720
741
|
var videoTrackSyncLocked = (track) => track.syncLocked !== false;
|
|
721
742
|
var videoClipSpeed = (clip) => clip.type === "media" ? _nullishCoalesce(clip.speed, () => ( 1)) : 1;
|
|
722
743
|
function videoClipSourceRange(clip) {
|
|
@@ -966,6 +987,84 @@ function videoClipAnimated(clip, project) {
|
|
|
966
987
|
return out;
|
|
967
988
|
}
|
|
968
989
|
|
|
990
|
+
// src/video-editor/mask.ts
|
|
991
|
+
var VIDEO_MASK_DEFAULTS = { x: 0.5, y: 0.5, width: 0.5, height: 0.5, rotation: 0, feather: 0, cornerRadius: 0 };
|
|
992
|
+
var ELLIPSE_SEGMENTS = 64;
|
|
993
|
+
var CORNER_SEGMENTS = 8;
|
|
994
|
+
function normalizeVideoClipMask(mask) {
|
|
995
|
+
const d = VIDEO_MASK_DEFAULTS;
|
|
996
|
+
const finite = (v, fallback) => typeof v === "number" && Number.isFinite(v) ? v : fallback;
|
|
997
|
+
return {
|
|
998
|
+
shape: mask.shape,
|
|
999
|
+
x: finite(mask.x, d.x),
|
|
1000
|
+
y: finite(mask.y, d.y),
|
|
1001
|
+
width: Math.max(0, finite(mask.width, d.width)),
|
|
1002
|
+
height: Math.max(0, finite(mask.height, d.height)),
|
|
1003
|
+
rotation: finite(mask.rotation, d.rotation),
|
|
1004
|
+
feather: Math.max(0, finite(mask.feather, d.feather)),
|
|
1005
|
+
cornerRadius: Math.min(1, Math.max(0, finite(mask.cornerRadius, d.cornerRadius))),
|
|
1006
|
+
inverted: mask.inverted === true,
|
|
1007
|
+
...mask.points ? { points: mask.points } : {}
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
function videoMaskPolygon(mask, rect) {
|
|
1011
|
+
const m = normalizeVideoClipMask(mask);
|
|
1012
|
+
const cx = rect.x + m.x * rect.width;
|
|
1013
|
+
const cy = rect.y + m.y * rect.height;
|
|
1014
|
+
const a = m.rotation * Math.PI / 180;
|
|
1015
|
+
const cos = Math.cos(a);
|
|
1016
|
+
const sin = Math.sin(a);
|
|
1017
|
+
const turn = (dx, dy) => [cx + dx * cos - dy * sin, cy + dx * sin + dy * cos];
|
|
1018
|
+
if (m.shape === "linear") {
|
|
1019
|
+
const R = 4 * (Math.hypot(rect.width, rect.height) + Math.hypot(cx - rect.x, cy - rect.y)) + 1;
|
|
1020
|
+
return [turn(-R, 0), turn(R, 0), turn(R, -R), turn(-R, -R)];
|
|
1021
|
+
}
|
|
1022
|
+
if (m.shape === "polygon") {
|
|
1023
|
+
return (_nullishCoalesce(m.points, () => ( []))).map((p) => turn((p.x - m.x) * rect.width, (p.y - m.y) * rect.height));
|
|
1024
|
+
}
|
|
1025
|
+
const hw = m.width * rect.width / 2;
|
|
1026
|
+
const hh = m.height * rect.height / 2;
|
|
1027
|
+
if (m.shape === "ellipse") {
|
|
1028
|
+
return Array.from({ length: ELLIPSE_SEGMENTS }, (_, i) => {
|
|
1029
|
+
const t = i / ELLIPSE_SEGMENTS * 2 * Math.PI;
|
|
1030
|
+
return turn(hw * Math.cos(t), hh * Math.sin(t));
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
const r = m.cornerRadius * Math.min(hw, hh);
|
|
1034
|
+
if (r <= 0) return [turn(-hw, -hh), turn(hw, -hh), turn(hw, hh), turn(-hw, hh)];
|
|
1035
|
+
const corners = [
|
|
1036
|
+
[-hw + r, -hh + r, Math.PI],
|
|
1037
|
+
[hw - r, -hh + r, 1.5 * Math.PI],
|
|
1038
|
+
[hw - r, hh - r, 0],
|
|
1039
|
+
[-hw + r, hh - r, 0.5 * Math.PI]
|
|
1040
|
+
];
|
|
1041
|
+
const out = [];
|
|
1042
|
+
for (const [x, y, start] of corners)
|
|
1043
|
+
for (let i = 0; i <= CORNER_SEGMENTS; i++) {
|
|
1044
|
+
const t = start + i / CORNER_SEGMENTS * (Math.PI / 2);
|
|
1045
|
+
out.push(turn(x + r * Math.cos(t), y + r * Math.sin(t)));
|
|
1046
|
+
}
|
|
1047
|
+
return out;
|
|
1048
|
+
}
|
|
1049
|
+
function videoLayerMask(mask, rect) {
|
|
1050
|
+
const m = normalizeVideoClipMask(mask);
|
|
1051
|
+
return { points: videoMaskPolygon(mask, rect), feather: m.feather, inverted: m.inverted };
|
|
1052
|
+
}
|
|
1053
|
+
function videoMaskSvgPath(mask) {
|
|
1054
|
+
const n2 = (v) => String(Math.round(v * 100) / 100);
|
|
1055
|
+
return mask.points.map(([x, y], i) => `${i ? "L" : "M"}${n2(x)} ${n2(y)}`).join("") + "Z";
|
|
1056
|
+
}
|
|
1057
|
+
function videoMaskContains(mask, x, y) {
|
|
1058
|
+
let inside = false;
|
|
1059
|
+
const p = mask.points;
|
|
1060
|
+
for (let i = 0, j = p.length - 1; i < p.length; j = i++) {
|
|
1061
|
+
const [xi, yi] = p[i];
|
|
1062
|
+
const [xj, yj] = p[j];
|
|
1063
|
+
if (yi > y !== yj > y && x < (xj - xi) * (y - yi) / (yj - yi) + xi) inside = !inside;
|
|
1064
|
+
}
|
|
1065
|
+
return mask.inverted ? !inside : inside;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
969
1068
|
// src/video-editor/evaluate.ts
|
|
970
1069
|
function videoEffectValuesAt(values, clipTime) {
|
|
971
1070
|
const out = {};
|
|
@@ -1050,8 +1149,9 @@ function videoFrameAt(project, time) {
|
|
|
1050
1149
|
}
|
|
1051
1150
|
function tracksAt(project, tracks, time, scope) {
|
|
1052
1151
|
const { assets, prefix } = scope;
|
|
1053
|
-
|
|
1152
|
+
let layers = [];
|
|
1054
1153
|
const audio = [];
|
|
1154
|
+
const deferred = [];
|
|
1055
1155
|
for (let ti = tracks.length - 1; ti >= 0; ti--) {
|
|
1056
1156
|
const track = tracks[ti];
|
|
1057
1157
|
for (const { clip, incoming, outgoing } of activeOnTrack(track, time)) {
|
|
@@ -1081,6 +1181,7 @@ function tracksAt(project, tracks, time, scope) {
|
|
|
1081
1181
|
}
|
|
1082
1182
|
if (track.kind !== "video" || track.hidden) continue;
|
|
1083
1183
|
if (clip.type === "media" && (!asset || asset.kind === "audio")) continue;
|
|
1184
|
+
if (clip.type === "adjustment" && !normalizeVideoGrade(clip.grade) && !clip.mask && !(_nullishCoalesce(clip.effects, () => ( []))).some((e) => e.enabled !== false)) continue;
|
|
1084
1185
|
if (nested && !nested.layers.length) continue;
|
|
1085
1186
|
const shown = videoClipAnimated(clip, project);
|
|
1086
1187
|
let opacity = videoValueAt(shown.opacity, clipTime, 1) * fade;
|
|
@@ -1124,6 +1225,22 @@ function tracksAt(project, tracks, time, scope) {
|
|
|
1124
1225
|
if (clip.type === "effect") layer.generator = { effect: clip.effect, ...clip.name ? { name: clip.name } : {}, values: videoEffectValuesAt(clip.values, clipTime), time: Math.max(0, clipTime) };
|
|
1125
1226
|
const effects = (_nullishCoalesce(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);
|
|
1126
1227
|
if (effects.length) layer.effects = effects;
|
|
1228
|
+
if (clip.type === "adjustment") {
|
|
1229
|
+
layer.rect = { x: 0, y: 0, width: project.width, height: project.height };
|
|
1230
|
+
layer.rotation = 0;
|
|
1231
|
+
const adjScope = _nullishCoalesce(clip.scope, () => ( "underneath"));
|
|
1232
|
+
layer.adjustment = { scope: adjScope, ...clip.targetTrack ? { track: clip.targetTrack } : {} };
|
|
1233
|
+
if (clip.mask) layer.mask = videoLayerMask(clip.mask, layer.rect);
|
|
1234
|
+
const keepBase = layer.opacity < 1 || !!layer.blend || !!layer.mask || !!layer.wipe || !!layer.iris;
|
|
1235
|
+
if (layer.opacity <= 0) continue;
|
|
1236
|
+
if (adjScope === "underneath") {
|
|
1237
|
+
if (!layers.length) continue;
|
|
1238
|
+
layer.layers = keepBase ? [...layers] : layers.splice(0);
|
|
1239
|
+
layers.push(layer);
|
|
1240
|
+
} else deferred.push({ layer, keepBase });
|
|
1241
|
+
continue;
|
|
1242
|
+
}
|
|
1243
|
+
if (clip.mask) layer.mask = videoLayerMask(clip.mask, layer.rect);
|
|
1127
1244
|
if (effectIn) {
|
|
1128
1245
|
const before = videoClipBefore(track, clip);
|
|
1129
1246
|
const i = before ? layers.findIndex((l) => l.clip === prefix + before.id && l.track === track.id) : -1;
|
|
@@ -1135,6 +1252,22 @@ function tracksAt(project, tracks, time, scope) {
|
|
|
1135
1252
|
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 });
|
|
1136
1253
|
}
|
|
1137
1254
|
}
|
|
1255
|
+
for (const { layer, keepBase } of deferred) {
|
|
1256
|
+
if (layer.adjustment.scope === "all") {
|
|
1257
|
+
if (!layers.length) continue;
|
|
1258
|
+
layer.layers = [...layers];
|
|
1259
|
+
layers = keepBase ? [...layers, layer] : [layer];
|
|
1260
|
+
continue;
|
|
1261
|
+
}
|
|
1262
|
+
const target = layer.adjustment.track;
|
|
1263
|
+
const first = layers.findIndex((l) => l.track === target);
|
|
1264
|
+
if (first < 0) continue;
|
|
1265
|
+
let last = first;
|
|
1266
|
+
while (last + 1 < layers.length && layers[last + 1].track === target) last++;
|
|
1267
|
+
const run = layers.slice(first, last + 1);
|
|
1268
|
+
layer.layers = run;
|
|
1269
|
+
layers = [...layers.slice(0, first), ...keepBase ? run : [], layer, ...layers.slice(last + 1)];
|
|
1270
|
+
}
|
|
1138
1271
|
return { layers, audio };
|
|
1139
1272
|
}
|
|
1140
1273
|
function videoLayersWithoutTransitionEffects(layers) {
|
|
@@ -1166,7 +1299,7 @@ function videoClipEffectUses(clip) {
|
|
|
1166
1299
|
}
|
|
1167
1300
|
var videoLayersUseEffects = (layers) => layers.some((l) => !!(_optionalChain([l, 'access', _31 => _31.effects, 'optionalAccess', _32 => _32.length]) || l.generator || l.transitionEffect || l.layers && videoLayersUseEffects(l.layers)));
|
|
1168
1301
|
var videoEffectFrameTime = (time, fps) => Math.floor(time * fps / 1e3 + 1e-6) * 1e3 / fps;
|
|
1169
|
-
var PLACEMENT = /* @__PURE__ */ new Set(["opacity", "rect", "rotation", "blend", "wipe", "iris", "grade", "track"]);
|
|
1302
|
+
var PLACEMENT = /* @__PURE__ */ new Set(["opacity", "rect", "rotation", "blend", "wipe", "iris", "grade", "track", "mask"]);
|
|
1170
1303
|
var round = (_k, v) => typeof v === "number" ? Math.round(v * 100) / 100 : v;
|
|
1171
1304
|
function videoEffectLayerKey(layer, width, height) {
|
|
1172
1305
|
const content = layer.transitionEffect ? layer : Object.fromEntries(Object.entries(layer).filter(([k]) => !PLACEMENT.has(k)));
|
|
@@ -1459,6 +1592,14 @@ function videoFrameToSvg(frame, options = {}, assets = []) {
|
|
|
1459
1592
|
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>`;
|
|
1460
1593
|
}
|
|
1461
1594
|
}
|
|
1595
|
+
if (layer.mask) {
|
|
1596
|
+
const m = layer.mask;
|
|
1597
|
+
const blur = m.feather > 0 ? `<filter id="mf${i}" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="${n(m.feather / 2)}"/></filter>` : "";
|
|
1598
|
+
if (blur) defs.push(blur);
|
|
1599
|
+
const shape = `<path d="${videoMaskSvgPath(m)}" fill="${m.inverted ? "black" : "white"}"${blur ? ` filter="url(#mf${i})"` : ""}/>`;
|
|
1600
|
+
defs.push(`<mask id="m${i}" maskUnits="userSpaceOnUse" x="-100000" y="-100000" width="200000" height="200000">${m.inverted ? '<rect x="-100000" y="-100000" width="200000" height="200000" fill="white"/>' : ""}${shape}</mask>`);
|
|
1601
|
+
inner = `<g mask="url(#m${i})">${inner}</g>`;
|
|
1602
|
+
}
|
|
1462
1603
|
let item = attrs.length ? `<g ${attrs.join(" ")}>${inner}</g>` : inner;
|
|
1463
1604
|
if (layer.iris) {
|
|
1464
1605
|
defs.push(`<clipPath id="i${i}"><circle cx="${n(frame.width / 2)}" cy="${n(frame.height / 2)}" r="${n(videoIrisRadius(frame, layer.iris.progress))}"/></clipPath>`);
|
|
@@ -1489,6 +1630,18 @@ function videoContactSheetSvg(project, times, options = {}) {
|
|
|
1489
1630
|
});
|
|
1490
1631
|
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${n(W)} ${n(H)}" width="${n(W)}" height="${n(H)}"><rect width="100%" height="100%" fill="#f8fafc"/>${cells.join("")}</svg>`;
|
|
1491
1632
|
}
|
|
1633
|
+
function applyVideoMask(g, mask, scale) {
|
|
1634
|
+
if (!g.moveTo || !g.lineTo || !g.closePath || !g.fill || mask.points.length < 3) return false;
|
|
1635
|
+
g.globalAlpha = 1;
|
|
1636
|
+
g.globalCompositeOperation = mask.inverted ? "destination-out" : "destination-in";
|
|
1637
|
+
g.filter = mask.feather > 0 ? `blur(${Math.max(0.5, mask.feather * scale / 2)}px)` : "none";
|
|
1638
|
+
g.fillStyle = "#000000";
|
|
1639
|
+
g.beginPath();
|
|
1640
|
+
mask.points.forEach(([x, y], i) => i ? g.lineTo(x, y) : g.moveTo(x, y));
|
|
1641
|
+
g.closePath();
|
|
1642
|
+
g.fill();
|
|
1643
|
+
return true;
|
|
1644
|
+
}
|
|
1492
1645
|
function paintVideoLayer(ctx, frame, layer, sourceFor, options) {
|
|
1493
1646
|
const { rect } = layer;
|
|
1494
1647
|
if (layer.iris) {
|
|
@@ -1696,10 +1849,10 @@ function drawVideoLayers(ctx, frame, layers, sourceFor, scale, options, depth, s
|
|
|
1696
1849
|
}
|
|
1697
1850
|
const setting = _optionalChain([layer, 'access', _70 => _70.grade, 'optionalAccess', _71 => _71.lut]);
|
|
1698
1851
|
const lut = setting ? _optionalChain([options, 'access', _72 => _72.lutFor, 'optionalCall', _73 => _73(setting.src)]) : null;
|
|
1699
|
-
const scratch = lut ? _optionalChain([options, 'access', _74 => _74.scratch, 'optionalCall', _75 => _75(cw, ch, 2 * depth + 1)]) : null;
|
|
1852
|
+
const scratch = lut || layer.mask ? _optionalChain([options, 'access', _74 => _74.scratch, 'optionalCall', _75 => _75(cw, ch, 2 * depth + 1)]) : null;
|
|
1700
1853
|
ctx.save();
|
|
1701
1854
|
ctx.setTransform(scale, 0, 0, scale, 0, 0);
|
|
1702
|
-
if (
|
|
1855
|
+
if (scratch) {
|
|
1703
1856
|
const s = scratch.ctx;
|
|
1704
1857
|
s.save();
|
|
1705
1858
|
s.setTransform(1, 0, 0, 1, 0, 0);
|
|
@@ -1709,9 +1862,23 @@ function drawVideoLayers(ctx, frame, layers, sourceFor, scale, options, depth, s
|
|
|
1709
1862
|
s.globalCompositeOperation = "source-over";
|
|
1710
1863
|
paintVideoLayer(s, frame, layer, source, options);
|
|
1711
1864
|
s.restore();
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1865
|
+
if (lut) {
|
|
1866
|
+
const img = s.getImageData(0, 0, cw, ch);
|
|
1867
|
+
applyVideoLutToPixels(lut, img.data, _nullishCoalesce(setting.strength, () => ( 1)), _nullishCoalesce(setting.preserveSkinTones, () => ( 0)));
|
|
1868
|
+
s.putImageData(img, 0, 0);
|
|
1869
|
+
}
|
|
1870
|
+
if (layer.mask) {
|
|
1871
|
+
s.save();
|
|
1872
|
+
s.setTransform(scale, 0, 0, scale, 0, 0);
|
|
1873
|
+
const { rect } = layer;
|
|
1874
|
+
if (layer.rotation) {
|
|
1875
|
+
s.translate(rect.x + rect.width / 2, rect.y + rect.height / 2);
|
|
1876
|
+
s.rotate(layer.rotation * Math.PI / 180);
|
|
1877
|
+
s.translate(-(rect.x + rect.width / 2), -(rect.y + rect.height / 2));
|
|
1878
|
+
}
|
|
1879
|
+
applyVideoMask(s, layer.mask, scale);
|
|
1880
|
+
s.restore();
|
|
1881
|
+
}
|
|
1715
1882
|
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
1716
1883
|
ctx.globalAlpha = layer.opacity;
|
|
1717
1884
|
if (layer.blend && layer.blend !== "normal") ctx.globalCompositeOperation = layer.blend === "add" ? "lighter" : layer.blend;
|
|
@@ -1778,6 +1945,7 @@ function toVideoEdl(project, options = {}) {
|
|
|
1778
1945
|
const sourceAt = (clip, time) => clip.type === "media" ? (_nullishCoalesce(clip.in, () => ( 0))) + (time - clip.start) * videoClipSpeed(clip) : time - clip.start;
|
|
1779
1946
|
const row = (n2, reel, kind, srcIn, srcOut, recIn, recOut) => `${n2} ${reel.padEnd(8)} ${channel.padEnd(5)} ${kind.padEnd(8)} ${tc(srcIn)} ${tc(srcOut)} ${rtc(recIn)} ${rtc(recOut)}`;
|
|
1780
1947
|
for (const clip of sortedVideoClips(track)) {
|
|
1948
|
+
if (clip.type === "adjustment") continue;
|
|
1781
1949
|
const asset = clip.type === "media" ? assets.get(clip.asset) : void 0;
|
|
1782
1950
|
const reel = clip.type === "media" ? reelName(asset) : "BL";
|
|
1783
1951
|
const end = videoClipEnd(clip);
|
|
@@ -1810,6 +1978,27 @@ function toVideoEdl(project, options = {}) {
|
|
|
1810
1978
|
function videoFfmpegUnsupportedEffects(project) {
|
|
1811
1979
|
return allVideoTracks(project).filter(({ track }) => track.kind === "video" && !track.hidden).flatMap(({ track }) => track.clips.flatMap(videoClipEffectUses)).filter((u) => u.enabled);
|
|
1812
1980
|
}
|
|
1981
|
+
function videoFfmpegUnsupportedFeatures(project) {
|
|
1982
|
+
const out = [];
|
|
1983
|
+
for (const { track } of allVideoTracks(project)) {
|
|
1984
|
+
if (track.kind !== "video" || track.hidden) continue;
|
|
1985
|
+
for (const clip of track.clips) {
|
|
1986
|
+
if (clip.mask) out.push(`clip "${clip.id}" has a mask`);
|
|
1987
|
+
if (clip.type !== "adjustment") continue;
|
|
1988
|
+
const why = [];
|
|
1989
|
+
if ((_nullishCoalesce(clip.scope, () => ( "underneath"))) === "track") why.push("changes one track only");
|
|
1990
|
+
if (clip.opacity !== void 0 && clip.opacity !== 1) why.push("is not at full opacity");
|
|
1991
|
+
if (clip.fadeIn || clip.fadeOut) why.push("fades");
|
|
1992
|
+
if (clip.blend && clip.blend !== "normal") why.push("blends");
|
|
1993
|
+
if (clip.transition) why.push("has a transition");
|
|
1994
|
+
if (clip.animateIn || clip.animateOut) why.push("is animated");
|
|
1995
|
+
const lut = normalizeVideoGradeLut(_optionalChain([clip, 'access', _96 => _96.grade, 'optionalAccess', _97 => _97.lut]));
|
|
1996
|
+
if (lut && (_nullishCoalesce(lut.strength, () => ( 1))) < 1) why.push("mixes its LUT at part strength");
|
|
1997
|
+
if (why.length) out.push(`adjustment clip "${clip.id}" ${why.join(", ")}`);
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
return out;
|
|
2001
|
+
}
|
|
1813
2002
|
function loudnormFilter(target = {}, measure = false) {
|
|
1814
2003
|
const I = _nullishCoalesce(target.integrated, () => ( -14));
|
|
1815
2004
|
const TP = _nullishCoalesce(target.truePeak, () => ( -1));
|
|
@@ -1902,6 +2091,13 @@ function buildFfmpeg(project, options, measure) {
|
|
|
1902
2091
|
);
|
|
1903
2092
|
}
|
|
1904
2093
|
}
|
|
2094
|
+
if (!measure && !frames) {
|
|
2095
|
+
const features = videoFfmpegUnsupportedFeatures(project);
|
|
2096
|
+
if (features.length)
|
|
2097
|
+
throw new Error(
|
|
2098
|
+
`ffmpeg's filters cannot draw ${features.slice(0, 5).join("; ")}${features.length > 5 ? "; ..." : ""}. Render the picture frame by frame with renderVideoFrameExact (the preview's drawing) to PNGs or raw RGBA and pass them as frames: { input: "frames/%06d.png" }; ffmpeg still mixes the sound. Or remove the mask, or keep adjustment clips to a full-opacity grade over what is underneath or the whole frame.`
|
|
2099
|
+
);
|
|
2100
|
+
}
|
|
1905
2101
|
if (frames) {
|
|
1906
2102
|
if (frames.format === "rawvideo") inputs.push(["-f", "rawvideo", "-pix_fmt", "rgba", "-s", `${W}x${H}`, "-framerate", String(fps), "-i", frames.input]);
|
|
1907
2103
|
else inputs.push(["-framerate", String(fps), "-i", frames.input]);
|
|
@@ -1981,11 +2177,33 @@ function buildFfmpeg(project, options, measure) {
|
|
|
1981
2177
|
};
|
|
1982
2178
|
let ak = 0;
|
|
1983
2179
|
let q = 0;
|
|
2180
|
+
const adjust = (cur2, clip) => {
|
|
2181
|
+
const enable = `enable='between(t,${sec(clip.start)},${sec(videoClipEnd(clip))})'`;
|
|
2182
|
+
const steps = videoGradeFfmpegFilters(clip.grade).map((f) => `${f}:${enable}`);
|
|
2183
|
+
const lut = normalizeVideoGradeLut(_optionalChain([clip, 'access', _98 => _98.grade, 'optionalAccess', _99 => _99.lut]));
|
|
2184
|
+
if (lut) {
|
|
2185
|
+
const path = options.resolveLut ? options.resolveLut(lut.src) : lut.src;
|
|
2186
|
+
if (/^(blob|data|https?):/i.test(path)) notes.push(`ffmpeg's lut3d reads only local files, and adjustment clip "${clip.id}"'s LUT is "${path.slice(0, 80)}"; save it and pass resolveLut to give its path.`);
|
|
2187
|
+
steps.push("format=gbrap", `lut3d=file=${ffmpegFilterValue(path)}:interp=tetrahedral:${enable}`);
|
|
2188
|
+
if (lut.preserveSkinTones) notes.push(`Adjustment clip "${clip.id}"'s LUT protects skin tones in the preview, which ffmpeg's lut3d cannot; render frames with renderVideoFrameExact to keep it.`);
|
|
2189
|
+
}
|
|
2190
|
+
if (!steps.length) return cur2;
|
|
2191
|
+
const next = `adj${k++}`;
|
|
2192
|
+
graph.push(`[${cur2}]format=rgba,${steps.join(",")},format=yuva420p[${next}]`);
|
|
2193
|
+
return next;
|
|
2194
|
+
};
|
|
1984
2195
|
const compose = (tracks, base, length, scope) => {
|
|
1985
2196
|
let cur2 = base;
|
|
1986
2197
|
const sounds = [];
|
|
2198
|
+
const adjustAll = [];
|
|
1987
2199
|
for (const track of [...tracks].reverse()) {
|
|
1988
2200
|
for (const raw of sortedVideoClips(track)) {
|
|
2201
|
+
if (raw.type === "adjustment") {
|
|
2202
|
+
if (!scope.picture || track.kind !== "video" || track.hidden) continue;
|
|
2203
|
+
if ((_nullishCoalesce(raw.scope, () => ( "underneath"))) === "all") adjustAll.push(raw);
|
|
2204
|
+
else if (cur2) cur2 = adjust(cur2, raw);
|
|
2205
|
+
continue;
|
|
2206
|
+
}
|
|
1989
2207
|
const clip = videoClipAnimated(raw, project);
|
|
1990
2208
|
const { lead, tail } = videoClipOverhang(track, clip);
|
|
1991
2209
|
const t0 = clip.start - lead;
|
|
@@ -2024,13 +2242,13 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2024
2242
|
}
|
|
2025
2243
|
let index;
|
|
2026
2244
|
if (shows && asset && asset.kind !== "audio") index = input(clip, asset, lead, tail);
|
|
2027
|
-
const audible = clip.type === "sequence" ? !!_optionalChain([compound, 'optionalAccess',
|
|
2245
|
+
const audible = clip.type === "sequence" ? !!_optionalChain([compound, 'optionalAccess', _100 => _100.sound]) : videoAssetHasSound(asset);
|
|
2028
2246
|
if (scope.sound && audible && (clip.type === "media" || clip.type === "sequence") && !clip.muted && !track.muted) {
|
|
2029
2247
|
const trackGain = _nullishCoalesce(track.volume, () => ( 1));
|
|
2030
2248
|
const volume = isVideoKeyframes(clip.volume) ? `volume='${trackGain}*(${expr(clip.volume, `(t*1000${lead ? `-${Math.round(lead)}` : ""})`, 1, "volume")})':eval=frame` : `volume=${(_nullishCoalesce(clip.volume, () => ( 1))) * trackGain}`;
|
|
2031
2249
|
const ramp = clip.type === "media" ? clip.timeRemap : void 0;
|
|
2032
2250
|
const ai = compound ? -1 : _nullishCoalesce(index, () => ( input(clip, asset, lead, tail)));
|
|
2033
|
-
let source2 = _nullishCoalesce(_optionalChain([compound, 'optionalAccess',
|
|
2251
|
+
let source2 = _nullishCoalesce(_optionalChain([compound, 'optionalAccess', _101 => _101.sound]), () => ( `[${ai}:a]`));
|
|
2034
2252
|
if (ramp) {
|
|
2035
2253
|
source2 = `[r${ak}]`;
|
|
2036
2254
|
graph.push(...rampedSound(ramp, `${ai}:a`, source2, lead, clip.duration + tail));
|
|
@@ -2044,7 +2262,7 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2044
2262
|
if (lead) chain2.push(`afade=t=in:st=0:d=${sec(2 * lead)}${inCurve}`);
|
|
2045
2263
|
if (tail) chain2.push(`afade=t=out:st=${sec(span - 2 * tail)}:d=${sec(2 * tail)}${outCurve}`);
|
|
2046
2264
|
if (before2 && clip.transition.type === "dip") chain2.push(`afade=t=in:st=0:d=${sec(clip.transition.duration / 2)}${inCurve}`);
|
|
2047
|
-
if (_optionalChain([next2, 'optionalAccess',
|
|
2265
|
+
if (_optionalChain([next2, 'optionalAccess', _102 => _102.transition, 'access', _103 => _103.type]) === "dip") chain2.push(`afade=t=out:st=${sec(span - next2.transition.duration / 2)}:d=${sec(next2.transition.duration / 2)}${outCurve}`);
|
|
2048
2266
|
if (clip.fadeIn) chain2.push(`afade=t=in:st=${sec(lead)}:d=${sec(clip.fadeIn)}${afadeCurve(clip.fadeInCurve)}`);
|
|
2049
2267
|
if (clip.fadeOut) chain2.push(`afade=t=out:st=${sec(lead + clip.duration - clip.fadeOut)}:d=${sec(clip.fadeOut)}${afadeCurve(clip.fadeOutCurve)}`);
|
|
2050
2268
|
chain2.push(`adelay=${Math.round(t0)}:all=1`);
|
|
@@ -2057,7 +2275,7 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2057
2275
|
const onTimeline = `(t*1000-${Math.round(clip.start)})`;
|
|
2058
2276
|
const onChain = (v) => `(${v}*1000${lead ? `-${Math.round(lead)}` : ""})`;
|
|
2059
2277
|
const tf = clip.transform;
|
|
2060
|
-
const moving = VIDEO_TRANSFORM_PROPERTIES.some((p) => isVideoKeyframes(_optionalChain([tf, 'optionalAccess',
|
|
2278
|
+
const moving = VIDEO_TRANSFORM_PROPERTIES.some((p) => isVideoKeyframes(_optionalChain([tf, 'optionalAccess', _104 => _104[p]])));
|
|
2061
2279
|
if (clip.type === "text" && options.titleImage) {
|
|
2062
2280
|
inputs.push(["-loop", "1", "-framerate", String(fps), "-t", sec(span), "-i", options.titleImage(moving ? { ...clip, transform: void 0 } : clip, { width: project.width, height: project.height })]);
|
|
2063
2281
|
index = inputs.length - 1;
|
|
@@ -2067,7 +2285,7 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2067
2285
|
const op = isVideoKeyframes(clip.opacity) ? `clip(${expr(clip.opacity, onTimeline, 1, "opacity")},0,1)` : String(_nullishCoalesce(clip.opacity, () => ( 1)));
|
|
2068
2286
|
const alpha = [op, clip.fadeIn ? `min(1,(t-${sec(clip.start)})/${sec(clip.fadeIn)})` : "", clip.fadeOut ? `min(1,(${sec(end)}-t)/${sec(clip.fadeOut)})` : ""].filter(Boolean).join("*");
|
|
2069
2287
|
const coord = (axis) => {
|
|
2070
|
-
const v = _optionalChain([tf, 'optionalAccess',
|
|
2288
|
+
const v = _optionalChain([tf, 'optionalAccess', _105 => _105[axis]]);
|
|
2071
2289
|
const base2 = `(${axis === "x" ? "w-text_w" : "h-text_h"})/2`;
|
|
2072
2290
|
return isVideoKeyframes(v) ? `${axis}='${base2}+(${expr(v, onTimeline, 0, "position")})'` : `${axis}=${base2}+${_nullishCoalesce(v, () => ( 0))}`;
|
|
2073
2291
|
};
|
|
@@ -2075,7 +2293,7 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2075
2293
|
const opts = [
|
|
2076
2294
|
`text=${ffmpegFilterValue(clip.text)}`,
|
|
2077
2295
|
options.fontFile ? `fontfile=${ffmpegFilterValue(options.fontFile)}` : `font=${ffmpegFilterValue(_nullishCoalesce(clip.fontFamily, () => ( "sans-serif")))}`,
|
|
2078
|
-
isVideoKeyframes(_optionalChain([tf, 'optionalAccess',
|
|
2296
|
+
isVideoKeyframes(_optionalChain([tf, 'optionalAccess', _106 => _106.scale])) ? `fontsize='${fontSize}*(${expr(tf.scale, onTimeline, 1, "scale")})'` : `fontsize=${Math.round(fontSize * (_nullishCoalesce(_optionalChain([tf, 'optionalAccess', _107 => _107.scale]), () => ( 1))))}`,
|
|
2079
2297
|
`fontcolor=${color(_nullishCoalesce(clip.color, () => ( "#ffffff")))}`,
|
|
2080
2298
|
coord("x"),
|
|
2081
2299
|
coord("y"),
|
|
@@ -2083,8 +2301,8 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2083
2301
|
...clip.background ? ["box=1", `boxcolor=${color(clip.background)}`, `boxborderw=${Math.round((_nullishCoalesce(clip.fontSize, () => ( 64))) / 2)}`] : [],
|
|
2084
2302
|
`enable='between(t,${sec(clip.start)},${sec(end)})'`
|
|
2085
2303
|
];
|
|
2086
|
-
if (_optionalChain([tf, 'optionalAccess',
|
|
2087
|
-
if (videoGradeFfmpegFilters(clip.grade).length || normalizeVideoGradeLut(_optionalChain([clip, 'access',
|
|
2304
|
+
if (_optionalChain([tf, 'optionalAccess', _108 => _108.rotation])) notes.push(`Title "${clip.id}" is drawn without its rotation (drawtext cannot rotate).`);
|
|
2305
|
+
if (videoGradeFfmpegFilters(clip.grade).length || normalizeVideoGradeLut(_optionalChain([clip, 'access', _109 => _109.grade, 'optionalAccess', _110 => _110.lut]))) notes.push(`Title "${clip.id}" is drawn without its grade (drawtext draws straight onto the picture); pass titleImage to grade titles.`);
|
|
2088
2306
|
if (clip.blend && clip.blend !== "normal") notes.push(`Title "${clip.id}" is drawn without its blend mode (drawtext draws straight onto the picture); pass titleImage to blend titles.`);
|
|
2089
2307
|
graph.push(`[${cur2}]drawtext=${opts.join(":")}[${next}]`);
|
|
2090
2308
|
cur2 = next;
|
|
@@ -2097,7 +2315,7 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2097
2315
|
const chain = [];
|
|
2098
2316
|
let source;
|
|
2099
2317
|
if (clip.type === "color") source = `color=c=${color(clip.color)}:s=${even(rect.width)}x${even(rect.height)}:r=${fps}:d=${sec(span)}`;
|
|
2100
|
-
else if (_optionalChain([compound, 'optionalAccess',
|
|
2318
|
+
else if (_optionalChain([compound, 'optionalAccess', _111 => _111.picture])) {
|
|
2101
2319
|
source = compound.picture;
|
|
2102
2320
|
chain.push(`scale=${even(rect.width)}:${even(rect.height)}`);
|
|
2103
2321
|
} else {
|
|
@@ -2107,7 +2325,7 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2107
2325
|
}
|
|
2108
2326
|
const grade = videoGradeFfmpegFilters(clip.grade);
|
|
2109
2327
|
if (grade.length) chain.push("format=rgba", ...grade);
|
|
2110
|
-
const lut = normalizeVideoGradeLut(_optionalChain([clip, 'access',
|
|
2328
|
+
const lut = normalizeVideoGradeLut(_optionalChain([clip, 'access', _112 => _112.grade, 'optionalAccess', _113 => _113.lut]));
|
|
2111
2329
|
if (lut) {
|
|
2112
2330
|
chain.push("format=gbrap");
|
|
2113
2331
|
const path = options.resolveLut ? options.resolveLut(lut.src) : lut.src;
|
|
@@ -2116,6 +2334,7 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2116
2334
|
const into2 = `q${next}`;
|
|
2117
2335
|
graph.push(`${source.startsWith("[") ? source : `${source},`}${chain.join(",")}[${into2}]`);
|
|
2118
2336
|
const lut3d = `lut3d=file=${file}:interp=tetrahedral`;
|
|
2337
|
+
if (lut.preserveSkinTones) notes.push(`Clip "${clip.id}"'s LUT protects skin tones in the preview, which ffmpeg's lut3d cannot; render frames with renderVideoFrameExact to keep it.`);
|
|
2119
2338
|
if ((_nullishCoalesce(lut.strength, () => ( 1))) >= 1) graph.push(`[${into2}]${lut3d}[r${next}]`);
|
|
2120
2339
|
else graph.push(`[${into2}]split[o${next}][p${next}]`, `[p${next}]${lut3d}[s${next}]`, `[s${next}][o${next}]blend=all_mode=normal:all_opacity=${lut.strength}[r${next}]`);
|
|
2121
2340
|
source = `[r${next}]`;
|
|
@@ -2138,10 +2357,10 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2138
2357
|
const r = videoIrisRadius({ width: W, height: H }, 1);
|
|
2139
2358
|
chain.push(`geq=lum='lum(X,Y)':cb='cb(X,Y)':cr='cr(X,Y)':a='if(lte(hypot(X+${Math.round(rect.x)}-${W / 2},Y+${Math.round(rect.y)}-${H / 2}),${Math.round(r)}*min(1,T/${sec(2 * lead)})),alpha(X,Y),0)'`);
|
|
2140
2359
|
}
|
|
2141
|
-
const rotation = _optionalChain([placed, 'optionalAccess',
|
|
2360
|
+
const rotation = _optionalChain([placed, 'optionalAccess', _114 => _114.rotation]);
|
|
2142
2361
|
if (isVideoKeyframes(rotation)) chain.push(`rotate=a='(${expr(rotation, onChain("t"), 0, "rotation")})*PI/180':c=black@0:ow='hypot(iw,ih)':oh=ow`);
|
|
2143
2362
|
else if (rotation) chain.push(`rotate=${rotation}*PI/180:c=black@0:ow=rotw(${rotation}*PI/180):oh=roth(${rotation}*PI/180)`);
|
|
2144
|
-
if (isVideoKeyframes(_optionalChain([placed, 'optionalAccess',
|
|
2363
|
+
if (isVideoKeyframes(_optionalChain([placed, 'optionalAccess', _115 => _115.scale]))) {
|
|
2145
2364
|
const s = expr(placed.scale, onChain("t"), 1, "scale");
|
|
2146
2365
|
chain.push(`scale=w='max(2,trunc(iw*(${s})/2)*2)':h='max(2,trunc(ih*(${s})/2)*2)':eval=frame`);
|
|
2147
2366
|
}
|
|
@@ -2160,9 +2379,9 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2160
2379
|
const into = lead ? clip.transition.type : void 0;
|
|
2161
2380
|
if (into === "push" || into === "slide") shiftBy(clip.transition.direction, clip.start - lead, 2 * lead, true);
|
|
2162
2381
|
const after = tail ? sortedVideoClips(track).find((c) => c !== clip && Math.abs(c.start - end) < 0.5 && c.transition) : void 0;
|
|
2163
|
-
if (_optionalChain([after, 'optionalAccess',
|
|
2382
|
+
if (_optionalChain([after, 'optionalAccess', _116 => _116.transition, 'access', _117 => _117.type]) === "push") shiftBy(after.transition.direction, end - tail, 2 * tail, false);
|
|
2164
2383
|
const at = (axis, c) => {
|
|
2165
|
-
const v = _optionalChain([placed, 'optionalAccess',
|
|
2384
|
+
const v = _optionalChain([placed, 'optionalAccess', _118 => _118[axis]]);
|
|
2166
2385
|
const extra = shifts[axis].map((s) => `+${s}`).join("");
|
|
2167
2386
|
return isVideoKeyframes(v) ? `${axis}='${Math.round(axis === "x" ? project.width / 2 : project.height / 2)}+(${expr(v, onTimeline, 0, "position")})-${axis === "x" ? "w" : "h"}/2${extra}'` : `${axis}='${Math.round(c)}-${axis === "x" ? "w" : "h"}/2${extra}'`;
|
|
2168
2387
|
};
|
|
@@ -2196,6 +2415,7 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2196
2415
|
}
|
|
2197
2416
|
}
|
|
2198
2417
|
}
|
|
2418
|
+
for (const clip of adjustAll) if (cur2) cur2 = adjust(cur2, clip);
|
|
2199
2419
|
return { video: cur2, audio: sounds };
|
|
2200
2420
|
};
|
|
2201
2421
|
const top = compose(project.tracks, measure || frames ? null : "base", D, { within: [], picture: !measure && !frames, sound: true });
|
|
@@ -2209,7 +2429,7 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2209
2429
|
if (options.loudness && !audio.length) notes.push("Nothing makes a sound, so there was no loudness to normalise.");
|
|
2210
2430
|
else if (options.loudness && !options.loudness.measured)
|
|
2211
2431
|
notes.push("Loudness is normalised in one pass, which adjusts the level as it goes; for a fixed gain that keeps the dynamics, run toVideoLoudnessMeasure first and pass its result as loudness.measured.");
|
|
2212
|
-
if (allVideoTracks(project).some(({ track: t }) => t.clips.some((c) => c.type === "media" && _optionalChain([assets, 'access',
|
|
2432
|
+
if (allVideoTracks(project).some(({ track: t }) => t.clips.some((c) => c.type === "media" && _optionalChain([assets, 'access', _119 => _119.get, 'call', _120 => _120(c.asset), 'optionalAccess', _121 => _121.kind]) === "video" && _optionalChain([assets, 'access', _122 => _122.get, 'call', _123 => _123(c.asset), 'optionalAccess', _124 => _124.hasAudio]) === void 0)))
|
|
2213
2433
|
notes.push('Video assets are assumed to have sound; set "hasAudio": false on any that do not, or ffmpeg will stop at a missing audio stream.');
|
|
2214
2434
|
if (animated.size)
|
|
2215
2435
|
notes.push(`Keyframed ${[...animated].sort().join(", ")} ${animated.size > 1 ? "are" : "is"} rendered as straight lines between keys, with eased segments sampled every two frames${animated.has("scale") ? "; animated scale needs ffmpeg 5 or later (scale with eval=frame)" : ""}.`);
|
|
@@ -2379,4 +2599,15 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2379
2599
|
|
|
2380
2600
|
|
|
2381
2601
|
|
|
2382
|
-
|
|
2602
|
+
|
|
2603
|
+
|
|
2604
|
+
|
|
2605
|
+
|
|
2606
|
+
|
|
2607
|
+
|
|
2608
|
+
|
|
2609
|
+
|
|
2610
|
+
|
|
2611
|
+
|
|
2612
|
+
|
|
2613
|
+
exports.VIDEO_FITS = VIDEO_FITS; exports.VIDEO_TRANSITION_TYPES = VIDEO_TRANSITION_TYPES; exports.VIDEO_WIPE_DIRECTIONS = VIDEO_WIPE_DIRECTIONS; exports.videoTransitionAudioCurve = videoTransitionAudioCurve; exports.VIDEO_BLEND_MODES = VIDEO_BLEND_MODES; exports.VIDEO_CLIP_ANIMATION_EFFECTS = VIDEO_CLIP_ANIMATION_EFFECTS; exports.VIDEO_MASK_SHAPES = VIDEO_MASK_SHAPES; exports.VIDEO_FADE_CURVES = VIDEO_FADE_CURVES; exports.VIDEO_ADJUSTMENT_SCOPES = VIDEO_ADJUSTMENT_SCOPES; exports.VIDEO_PLACEMENT_MODES = VIDEO_PLACEMENT_MODES; exports.VIDEO_CLIP_TYPES = VIDEO_CLIP_TYPES; exports.VIDEO_EASE_NAMES = VIDEO_EASE_NAMES; exports.VIDEO_KEYFRAME_PROPERTIES = VIDEO_KEYFRAME_PROPERTIES; exports.VIDEO_TRANSFORM_PROPERTIES = VIDEO_TRANSFORM_PROPERTIES; exports.VIDEO_KEYFRAME_LIMITS = VIDEO_KEYFRAME_LIMITS; exports.isVideoKeyframes = isVideoKeyframes; exports.videoEaseBezier = videoEaseBezier; exports.videoEaseProgress = videoEaseProgress; exports.videoValueAt = videoValueAt; exports.videoKeysOf = videoKeysOf; exports.retimeVideoAnimatable = retimeVideoAnimatable; exports.splitVideoAnimatable = splitVideoAnimatable; exports.sortVideoKeys = sortVideoKeys; exports.setVideoKey = setVideoKey; exports.removeVideoKey = removeVideoKey; exports.videoKeyframeBreakpoints = videoKeyframeBreakpoints; exports.videoKeyframeFfmpegExpr = videoKeyframeFfmpegExpr; exports.VIDEO_LUT_MAX_SIZE = VIDEO_LUT_MAX_SIZE; exports.parseVideoCubeLut = parseVideoCubeLut; exports.videoLutFromFunction = videoLutFromFunction; exports.videoLutToCube = videoLutToCube; exports.applyVideoLut = applyVideoLut; exports.videoSkinToneWeight = videoSkinToneWeight; exports.applyVideoLutToPixels = applyVideoLutToPixels; exports.videoLutSvgApproximation = videoLutSvgApproximation; exports.VideoLutCache = VideoLutCache; exports.VIDEO_GRADE_CONTROLS = VIDEO_GRADE_CONTROLS; exports.VIDEO_GRADE_PRESETS = VIDEO_GRADE_PRESETS; exports.VIDEO_GRADE_PRESET_NAMES = VIDEO_GRADE_PRESET_NAMES; exports.normalizeVideoGradeLut = normalizeVideoGradeLut; exports.normalizeVideoGrade = normalizeVideoGrade; exports.videoGradePreset = videoGradePreset; exports.videoGradeLook = videoGradeLook; exports.videoGradeSteps = videoGradeSteps; exports.videoGradeStepMatrix = videoGradeStepMatrix; exports.applyVideoGrade = applyVideoGrade; exports.videoGradeStepSvgValues = videoGradeStepSvgValues; exports.videoLutSvgPrimitives = videoLutSvgPrimitives; exports.videoGradeSvgFilter = videoGradeSvgFilter; exports.videoGradeGainsFilterId = videoGradeGainsFilterId; exports.videoGradeCssFilter = videoGradeCssFilter; exports.videoGradeSvgDefs = videoGradeSvgDefs; exports.videoGradeFfmpegFilters = videoGradeFfmpegFilters; exports.videoClipSourceTime = videoClipSourceTime; exports.videoClipSpeedAt = videoClipSpeedAt; exports.videoSpeedRampKeys = videoSpeedRampKeys; exports.checkVideoTimeRemap = checkVideoTimeRemap; exports.videoTimeRemapSpan = videoTimeRemapSpan; exports.videoTimeRemapBreakpoints = videoTimeRemapBreakpoints; exports.videoSpeedRampEdit = videoSpeedRampEdit; exports.videoFreezeFrameEdit = videoFreezeFrameEdit; exports.videoSlowMotionRamp = videoSlowMotionRamp; exports.VIDEO_TIME_EPSILON = VIDEO_TIME_EPSILON; exports.videoClipEnd = videoClipEnd; exports.videoClipTypeLabel = videoClipTypeLabel; exports.videoTrackSyncLocked = videoTrackSyncLocked; exports.videoClipSpeed = videoClipSpeed; exports.videoClipSourceRange = videoClipSourceRange; exports.videoFrameDuration = videoFrameDuration; exports.snapToVideoFrame = snapToVideoFrame; exports.videoFrames = videoFrames; exports.isVideoDropFrameRate = isVideoDropFrameRate; exports.videoTimecodeFrames = videoTimecodeFrames; exports.formatVideoTimecode = formatVideoTimecode; exports.formatVideoTimecodeFrames = formatVideoTimecodeFrames; exports.videoHourFrames = videoHourFrames; exports.formatVideoSeconds = formatVideoSeconds; exports.videoAssetHasSound = videoAssetHasSound; exports.videoAssetMap = videoAssetMap; exports.videoSequenceMap = videoSequenceMap; exports.videoSequenceDuration = videoSequenceDuration; exports.videoSequenceChildren = videoSequenceChildren; exports.findVideoSequenceCycle = findVideoSequenceCycle; exports.allVideoTracks = allVideoTracks; exports.sortedVideoClips = sortedVideoClips; exports.findVideoClip = findVideoClip; exports.allVideoClipIds = allVideoClipIds; exports.findVideoSequenceClip = findVideoSequenceClip; exports.videoProjectDuration = videoProjectDuration; exports.videoClipBefore = videoClipBefore; exports.videoClipAfter = videoClipAfter; exports.videoClipHandles = videoClipHandles; exports.videoClipOverhang = videoClipOverhang; exports.videoSnapPoints = videoSnapPoints; exports.snapVideoTime = snapVideoTime; exports.findVideoGaps = findVideoGaps; exports.videoOverlap = videoOverlap; exports.nextFreeVideoSlot = nextFreeVideoSlot; exports.videoClipsAt = videoClipsAt; exports.VIDEO_CLIP_ANIMATION_DURATION = VIDEO_CLIP_ANIMATION_DURATION; exports.videoClipAnimationProperties = videoClipAnimationProperties; exports.videoClipAnimationSpans = videoClipAnimationSpans; exports.videoClipAnimated = videoClipAnimated; exports.VIDEO_MASK_DEFAULTS = VIDEO_MASK_DEFAULTS; exports.normalizeVideoClipMask = normalizeVideoClipMask; exports.videoMaskPolygon = videoMaskPolygon; exports.videoLayerMask = videoLayerMask; exports.videoMaskSvgPath = videoMaskSvgPath; exports.videoMaskContains = videoMaskContains; exports.videoEffectValuesAt = videoEffectValuesAt; exports.videoTransformAt = videoTransformAt; exports.placeVideoRect = placeVideoRect; exports.videoTextStyle = videoTextStyle; exports.videoTitleFrame = videoTitleFrame; exports.videoTransitionEase = videoTransitionEase; exports.videoFadeCurveGain = videoFadeCurveGain; exports.videoClipSoundGain = videoClipSoundGain; exports.videoFrameAt = videoFrameAt; exports.videoLayersWithoutTransitionEffects = videoLayersWithoutTransitionEffects; exports.videoFrameLeafLayers = videoFrameLeafLayers; exports.videoProjectEffects = videoProjectEffects; exports.videoClipEffectUses = videoClipEffectUses; exports.videoLayersUseEffects = videoLayersUseEffects; exports.videoEffectFrameTime = videoEffectFrameTime; exports.videoEffectLayerKey = videoEffectLayerKey; exports.mixEffectPixels = mixEffectPixels; exports.overEffectPixels = overEffectPixels; exports.VideoEffectsEngine = VideoEffectsEngine; exports.VIDEO_EFFECT_PLACES = VIDEO_EFFECT_PLACES; exports.checkVideoEffectPlace = checkVideoEffectPlace; exports.checkVideoEffectValues = checkVideoEffectValues; exports.videoIrisRadius = videoIrisRadius; exports.videoWipeRect = videoWipeRect; exports.videoFrameToSvg = videoFrameToSvg; exports.videoProjectSvg = videoProjectSvg; exports.videoContactSheetSvg = videoContactSheetSvg; exports.drawVideoFrame = drawVideoFrame; exports.renderVideoFrameExact = renderVideoFrameExact; exports.toVideoEdl = toVideoEdl; exports.videoFfmpegUnsupportedEffects = videoFfmpegUnsupportedEffects; exports.videoFfmpegUnsupportedFeatures = videoFfmpegUnsupportedFeatures; exports.parseVideoLoudnessMeasurement = parseVideoLoudnessMeasurement; exports.ffmpegFilterValue = ffmpegFilterValue; exports.videoShellQuote = videoShellQuote; exports.toVideoFfmpeg = toVideoFfmpeg; exports.toVideoLoudnessMeasure = toVideoLoudnessMeasure;
|