@pixerate/editor 0.115.0 → 0.117.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/THIRD_PARTY_NOTICES.md +3 -0
- package/dist/animation-editor/index.cjs +54 -54
- package/dist/animation-editor/index.d.cts +2 -2
- package/dist/animation-editor/index.d.ts +2 -2
- package/dist/animation-editor/index.js +2 -2
- package/dist/{chunk-I7FQBWMQ.cjs → chunk-2J3LS7JZ.cjs} +333 -333
- package/dist/{chunk-I7FQBWMQ.cjs.map → chunk-2J3LS7JZ.cjs.map} +1 -1
- package/dist/{chunk-TFOPHZ3U.cjs → chunk-7ISPWEJA.cjs} +302 -5
- package/dist/chunk-7ISPWEJA.cjs.map +1 -0
- package/dist/{chunk-VXR7XWMF.js → chunk-7LNZAWSJ.js} +437 -41
- package/dist/chunk-7LNZAWSJ.js.map +1 -0
- package/dist/{chunk-DFQEX3AM.cjs → chunk-BYAWOAST.cjs} +223 -30
- package/dist/chunk-BYAWOAST.cjs.map +1 -0
- package/dist/chunk-COAMRKVI.cjs +159 -0
- package/dist/chunk-COAMRKVI.cjs.map +1 -0
- package/dist/{chunk-ST7E3NEE.cjs → chunk-EBSYZRSW.cjs} +230 -732
- package/dist/chunk-EBSYZRSW.cjs.map +1 -0
- package/dist/chunk-EJVDBITD.cjs +501 -0
- package/dist/chunk-EJVDBITD.cjs.map +1 -0
- package/dist/{chunk-LPLCWZTM.cjs → chunk-FODKPD44.cjs} +555 -159
- package/dist/chunk-FODKPD44.cjs.map +1 -0
- package/dist/{chunk-XK5LZKBD.js → chunk-HKDTM6VC.js} +298 -1
- package/dist/chunk-HKDTM6VC.js.map +1 -0
- package/dist/chunk-IPFRWILU.js +1908 -0
- package/dist/chunk-IPFRWILU.js.map +1 -0
- package/dist/chunk-IVSNUXW7.js +3339 -0
- package/dist/chunk-IVSNUXW7.js.map +1 -0
- package/dist/{chunk-LKCYANMW.js → chunk-JHXMNOMF.js} +198 -5
- package/dist/chunk-JHXMNOMF.js.map +1 -0
- package/dist/{chunk-FV5AXTSF.cjs → chunk-JSSBHMMU.cjs} +36 -188
- package/dist/chunk-JSSBHMMU.cjs.map +1 -0
- package/dist/chunk-L2T6EJ5S.cjs +3339 -0
- package/dist/chunk-L2T6EJ5S.cjs.map +1 -0
- package/dist/chunk-LF4WOZXZ.js +159 -0
- package/dist/chunk-LF4WOZXZ.js.map +1 -0
- package/dist/{chunk-26PGXHY7.cjs → chunk-M7O4BJRH.cjs} +36 -36
- package/dist/{chunk-26PGXHY7.cjs.map → chunk-M7O4BJRH.cjs.map} +1 -1
- package/dist/chunk-N3467VD6.js +501 -0
- package/dist/chunk-N3467VD6.js.map +1 -0
- package/dist/{chunk-XICA4PVO.js → chunk-R2JE6SA7.js} +17 -17
- package/dist/chunk-SMJGE7HM.cjs +1908 -0
- package/dist/chunk-SMJGE7HM.cjs.map +1 -0
- package/dist/{chunk-MMVHBKMM.js → chunk-T2P5TIZ6.js} +2 -2
- package/dist/{chunk-C6C5PDKU.js → chunk-VD2UDVF7.js} +102 -604
- package/dist/chunk-VD2UDVF7.js.map +1 -0
- package/dist/{chunk-FGW5OWJ4.js → chunk-VJUML33I.js} +4 -156
- package/dist/chunk-VJUML33I.js.map +1 -0
- package/dist/export/index.cjs +4 -2
- package/dist/export/index.cjs.map +1 -1
- package/dist/export/index.js +5 -3
- package/dist/{export-BJE8T0YA.d.cts → export-DMOuzgZZ.d.cts} +9 -4
- package/dist/{export-Con3UKkm.d.ts → export-Dul1SbPv.d.ts} +9 -4
- package/dist/export-Dv0-ZVrV.d.cts +501 -0
- package/dist/export-Dv0-ZVrV.d.ts +501 -0
- package/dist/{retouch-Bxkuc0KH.d.cts → flow-4-OC-UkQ.d.cts} +582 -2
- package/dist/{retouch-Do6tBs2H.d.ts → flow-Cb7lkPm8.d.ts} +582 -2
- package/dist/image-editor/index.cjs +1345 -1095
- package/dist/image-editor/index.cjs.map +1 -1
- package/dist/image-editor/index.d.cts +8 -8
- package/dist/image-editor/index.d.ts +8 -8
- package/dist/image-editor/index.js +300 -50
- package/dist/image-editor/index.js.map +1 -1
- package/dist/index.cjs +4 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/kernel-wasm-WYCGNZR4.js +10 -0
- package/dist/kernel-wasm-WYCGNZR4.js.map +1 -0
- package/dist/kernel-wasm-YPVAEMVN.cjs +10 -0
- package/dist/kernel-wasm-YPVAEMVN.cjs.map +1 -0
- package/dist/paint/index.cjs +170 -4
- package/dist/paint/index.cjs.map +1 -1
- package/dist/paint/index.d.cts +225 -3
- package/dist/paint/index.d.ts +225 -3
- package/dist/paint/index.js +171 -5
- package/dist/paint-editor/index.cjs +4735 -890
- package/dist/paint-editor/index.cjs.map +1 -1
- package/dist/paint-editor/index.d.cts +1526 -278
- package/dist/paint-editor/index.d.ts +1526 -278
- package/dist/paint-editor/index.js +4561 -716
- package/dist/paint-editor/index.js.map +1 -1
- package/dist/raster/index.cjs +7 -4
- package/dist/raster/index.cjs.map +1 -1
- package/dist/raster/index.d.cts +2 -2
- package/dist/raster/index.d.ts +2 -2
- package/dist/raster/index.js +36 -33
- package/dist/{render-CrmHOgpo.d.ts → render-B05K68_h.d.ts} +9 -1
- package/dist/{render-B1K_OoLy.d.cts → render-DnAbIKcs.d.cts} +9 -1
- package/dist/{svg-BnRj0q8i.d.cts → svg-CVdFc80-.d.cts} +2 -2
- package/dist/{svg-CmmBFfGT.d.ts → svg-DlEuhwoX.d.ts} +2 -2
- package/dist/{types-1SUQKDAw.d.ts → types-CDLGhAZp.d.ts} +40 -2
- package/dist/{types-yINF17Ta.d.cts → types-Tfly4Aya.d.cts} +40 -2
- package/dist/vector/index.cjs +6536 -4525
- package/dist/vector/index.cjs.map +1 -1
- package/dist/vector/index.d.cts +426 -32
- package/dist/vector/index.d.ts +426 -32
- package/dist/vector/index.js +11397 -9386
- package/dist/vector/index.js.map +1 -1
- package/dist/video-editor/index.cjs +502 -2165
- package/dist/video-editor/index.cjs.map +1 -1
- package/dist/video-editor/index.d.cts +3 -501
- package/dist/video-editor/index.d.ts +3 -501
- package/dist/video-editor/index.js +151 -1814
- package/dist/video-editor/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/pixerate-image-editor/references/photoshop-files.md +7 -3
- package/skills/pixerate-vector/SKILL.md +3 -0
- package/dist/chunk-C6C5PDKU.js.map +0 -1
- package/dist/chunk-DFQEX3AM.cjs.map +0 -1
- package/dist/chunk-FGW5OWJ4.js.map +0 -1
- package/dist/chunk-FV5AXTSF.cjs.map +0 -1
- package/dist/chunk-GX332W2C.js +0 -181
- package/dist/chunk-GX332W2C.js.map +0 -1
- package/dist/chunk-LKCYANMW.js.map +0 -1
- package/dist/chunk-LPLCWZTM.cjs.map +0 -1
- package/dist/chunk-MUUXNRU4.cjs +0 -181
- package/dist/chunk-MUUXNRU4.cjs.map +0 -1
- package/dist/chunk-ST7E3NEE.cjs.map +0 -1
- package/dist/chunk-TCKIXI46.cjs +0 -1136
- package/dist/chunk-TCKIXI46.cjs.map +0 -1
- package/dist/chunk-TFOPHZ3U.cjs.map +0 -1
- package/dist/chunk-UBIXVXPR.js +0 -1136
- package/dist/chunk-UBIXVXPR.js.map +0 -1
- package/dist/chunk-VXR7XWMF.js.map +0 -1
- package/dist/chunk-XK5LZKBD.js.map +0 -1
- /package/dist/{chunk-XICA4PVO.js.map → chunk-R2JE6SA7.js.map} +0 -0
- /package/dist/{chunk-MMVHBKMM.js.map → chunk-T2P5TIZ6.js.map} +0 -0
|
@@ -1,3 +1,125 @@
|
|
|
1
|
+
import {
|
|
2
|
+
VIDEO_BLEND_MODES,
|
|
3
|
+
VIDEO_CLIP_ANIMATION_DURATION,
|
|
4
|
+
VIDEO_CLIP_ANIMATION_EFFECTS,
|
|
5
|
+
VIDEO_CLIP_TYPES,
|
|
6
|
+
VIDEO_EASE_NAMES,
|
|
7
|
+
VIDEO_FADE_CURVES,
|
|
8
|
+
VIDEO_FITS,
|
|
9
|
+
VIDEO_GRADE_CONTROLS,
|
|
10
|
+
VIDEO_GRADE_PRESETS,
|
|
11
|
+
VIDEO_GRADE_PRESET_NAMES,
|
|
12
|
+
VIDEO_KEYFRAME_LIMITS,
|
|
13
|
+
VIDEO_KEYFRAME_PROPERTIES,
|
|
14
|
+
VIDEO_LUT_MAX_SIZE,
|
|
15
|
+
VIDEO_PLACEMENT_MODES,
|
|
16
|
+
VIDEO_TIME_EPSILON,
|
|
17
|
+
VIDEO_TRANSFORM_PROPERTIES,
|
|
18
|
+
VIDEO_TRANSITION_TYPES,
|
|
19
|
+
VIDEO_WIPE_DIRECTIONS,
|
|
20
|
+
VideoLutCache,
|
|
21
|
+
allVideoClipIds,
|
|
22
|
+
allVideoTracks,
|
|
23
|
+
applyVideoGrade,
|
|
24
|
+
applyVideoLut,
|
|
25
|
+
applyVideoLutToPixels,
|
|
26
|
+
checkVideoTimeRemap,
|
|
27
|
+
drawVideoFrame,
|
|
28
|
+
ffmpegFilterValue,
|
|
29
|
+
findVideoClip,
|
|
30
|
+
findVideoGaps,
|
|
31
|
+
findVideoSequenceClip,
|
|
32
|
+
findVideoSequenceCycle,
|
|
33
|
+
formatVideoSeconds,
|
|
34
|
+
formatVideoTimecode,
|
|
35
|
+
formatVideoTimecodeFrames,
|
|
36
|
+
isVideoDropFrameRate,
|
|
37
|
+
isVideoKeyframes,
|
|
38
|
+
nextFreeVideoSlot,
|
|
39
|
+
normalizeVideoGrade,
|
|
40
|
+
normalizeVideoGradeLut,
|
|
41
|
+
parseVideoCubeLut,
|
|
42
|
+
parseVideoLoudnessMeasurement,
|
|
43
|
+
placeVideoRect,
|
|
44
|
+
removeVideoKey,
|
|
45
|
+
retimeVideoAnimatable,
|
|
46
|
+
setVideoKey,
|
|
47
|
+
snapToVideoFrame,
|
|
48
|
+
snapVideoTime,
|
|
49
|
+
sortVideoKeys,
|
|
50
|
+
sortedVideoClips,
|
|
51
|
+
splitVideoAnimatable,
|
|
52
|
+
toVideoEdl,
|
|
53
|
+
toVideoFfmpeg,
|
|
54
|
+
toVideoLoudnessMeasure,
|
|
55
|
+
videoAssetHasSound,
|
|
56
|
+
videoAssetMap,
|
|
57
|
+
videoClipAfter,
|
|
58
|
+
videoClipAnimated,
|
|
59
|
+
videoClipAnimationProperties,
|
|
60
|
+
videoClipAnimationSpans,
|
|
61
|
+
videoClipBefore,
|
|
62
|
+
videoClipEnd,
|
|
63
|
+
videoClipHandles,
|
|
64
|
+
videoClipOverhang,
|
|
65
|
+
videoClipSoundGain,
|
|
66
|
+
videoClipSourceRange,
|
|
67
|
+
videoClipSourceTime,
|
|
68
|
+
videoClipSpeed,
|
|
69
|
+
videoClipSpeedAt,
|
|
70
|
+
videoClipsAt,
|
|
71
|
+
videoContactSheetSvg,
|
|
72
|
+
videoEaseBezier,
|
|
73
|
+
videoEaseProgress,
|
|
74
|
+
videoFadeCurveGain,
|
|
75
|
+
videoFrameAt,
|
|
76
|
+
videoFrameDuration,
|
|
77
|
+
videoFrameLeafLayers,
|
|
78
|
+
videoFrameToSvg,
|
|
79
|
+
videoFrames,
|
|
80
|
+
videoFreezeFrameEdit,
|
|
81
|
+
videoGradeCssFilter,
|
|
82
|
+
videoGradeFfmpegFilters,
|
|
83
|
+
videoGradeGainsFilterId,
|
|
84
|
+
videoGradeLook,
|
|
85
|
+
videoGradePreset,
|
|
86
|
+
videoGradeStepMatrix,
|
|
87
|
+
videoGradeStepSvgValues,
|
|
88
|
+
videoGradeSteps,
|
|
89
|
+
videoGradeSvgDefs,
|
|
90
|
+
videoGradeSvgFilter,
|
|
91
|
+
videoHourFrames,
|
|
92
|
+
videoIrisRadius,
|
|
93
|
+
videoKeyframeBreakpoints,
|
|
94
|
+
videoKeyframeFfmpegExpr,
|
|
95
|
+
videoKeysOf,
|
|
96
|
+
videoLutFromFunction,
|
|
97
|
+
videoLutSvgApproximation,
|
|
98
|
+
videoLutSvgPrimitives,
|
|
99
|
+
videoLutToCube,
|
|
100
|
+
videoOverlap,
|
|
101
|
+
videoProjectDuration,
|
|
102
|
+
videoProjectSvg,
|
|
103
|
+
videoSequenceChildren,
|
|
104
|
+
videoSequenceDuration,
|
|
105
|
+
videoSequenceMap,
|
|
106
|
+
videoShellQuote,
|
|
107
|
+
videoSlowMotionRamp,
|
|
108
|
+
videoSnapPoints,
|
|
109
|
+
videoSpeedRampEdit,
|
|
110
|
+
videoSpeedRampKeys,
|
|
111
|
+
videoTextStyle,
|
|
112
|
+
videoTimeRemapBreakpoints,
|
|
113
|
+
videoTimeRemapSpan,
|
|
114
|
+
videoTimecodeFrames,
|
|
115
|
+
videoTitleFrame,
|
|
116
|
+
videoTrackSyncLocked,
|
|
117
|
+
videoTransformAt,
|
|
118
|
+
videoTransitionAudioCurve,
|
|
119
|
+
videoTransitionEase,
|
|
120
|
+
videoValueAt,
|
|
121
|
+
videoWipeRect
|
|
122
|
+
} from "../chunk-IPFRWILU.js";
|
|
1
123
|
import {
|
|
2
124
|
namedWithSuggestion
|
|
3
125
|
} from "../chunk-INZJALPY.js";
|
|
@@ -11,708 +133,6 @@ import "../chunk-AGR4P4UZ.js";
|
|
|
11
133
|
import "../chunk-ZRJMJIQU.js";
|
|
12
134
|
import "../chunk-TGD52FEZ.js";
|
|
13
135
|
|
|
14
|
-
// src/video-editor/types.ts
|
|
15
|
-
var VIDEO_FITS = ["contain", "cover", "fill", "none"];
|
|
16
|
-
var VIDEO_TRANSITION_TYPES = ["crossfade", "dip", "wipe", "push", "slide", "iris"];
|
|
17
|
-
var VIDEO_WIPE_DIRECTIONS = ["left", "right", "up", "down"];
|
|
18
|
-
var videoTransitionAudioCurve = (t) => t.audioCurve ?? (t.type === "dip" ? "linear" : "equal-power");
|
|
19
|
-
var VIDEO_BLEND_MODES = ["normal", "multiply", "screen", "overlay", "darken", "lighten", "color-dodge", "color-burn", "hard-light", "soft-light", "difference", "exclusion", "add"];
|
|
20
|
-
var VIDEO_CLIP_ANIMATION_EFFECTS = ["fade", "rise", "drop", "slide-left", "slide-right", "zoom", "pop"];
|
|
21
|
-
var VIDEO_FADE_CURVES = ["linear", "equal-power", "s-curve", "exponential", "logarithmic"];
|
|
22
|
-
var VIDEO_PLACEMENT_MODES = ["refuse", "overwrite", "insert"];
|
|
23
|
-
var VIDEO_CLIP_TYPES = ["media", "text", "color", "sequence"];
|
|
24
|
-
|
|
25
|
-
// src/video-editor/keyframes.ts
|
|
26
|
-
var VIDEO_EASE_NAMES = ["linear", "hold", "ease-in", "ease-out", "ease-in-out"];
|
|
27
|
-
var VIDEO_KEYFRAME_PROPERTIES = ["opacity", "volume", "x", "y", "scale", "rotation"];
|
|
28
|
-
var VIDEO_TRANSFORM_PROPERTIES = ["x", "y", "scale", "rotation"];
|
|
29
|
-
var VIDEO_KEYFRAME_LIMITS = {
|
|
30
|
-
opacity: { default: 1, min: 0, max: 1 },
|
|
31
|
-
volume: { default: 1, min: 0, max: 4 },
|
|
32
|
-
x: { default: 0 },
|
|
33
|
-
y: { default: 0 },
|
|
34
|
-
scale: { default: 1, min: 0 },
|
|
35
|
-
rotation: { default: 0 }
|
|
36
|
-
};
|
|
37
|
-
var NAMED = {
|
|
38
|
-
"ease-in": [0.42, 0, 1, 1],
|
|
39
|
-
"ease-out": [0, 0, 0.58, 1],
|
|
40
|
-
"ease-in-out": [0.42, 0, 0.58, 1]
|
|
41
|
-
};
|
|
42
|
-
var isVideoKeyframes = (v) => typeof v === "object" && v !== null && Array.isArray(v.keys);
|
|
43
|
-
function videoEaseBezier(ease) {
|
|
44
|
-
if (ease === void 0 || ease === "linear" || ease === "hold") return null;
|
|
45
|
-
return Array.isArray(ease) ? ease : NAMED[ease];
|
|
46
|
-
}
|
|
47
|
-
var bez = (a, b, s) => 3 * a * (1 - s) * (1 - s) * s + 3 * b * (1 - s) * s * s + s * s * s;
|
|
48
|
-
var bezSlope = (a, b, s) => 3 * a * (1 - s) * (1 - s) + 6 * (b - a) * (1 - s) * s + 3 * (1 - b) * s * s;
|
|
49
|
-
function bezierParam(x1, x2, x) {
|
|
50
|
-
let s = x;
|
|
51
|
-
for (let i = 0; i < 8; i++) {
|
|
52
|
-
const err = bez(x1, x2, s) - x;
|
|
53
|
-
if (Math.abs(err) < 1e-7) return s;
|
|
54
|
-
const d = bezSlope(x1, x2, s);
|
|
55
|
-
if (Math.abs(d) < 1e-6) break;
|
|
56
|
-
s -= err / d;
|
|
57
|
-
if (s < 0 || s > 1) break;
|
|
58
|
-
}
|
|
59
|
-
let lo = 0;
|
|
60
|
-
let hi = 1;
|
|
61
|
-
s = x;
|
|
62
|
-
for (let i = 0; i < 60; i++) {
|
|
63
|
-
const v = bez(x1, x2, s);
|
|
64
|
-
if (Math.abs(v - x) < 1e-9) break;
|
|
65
|
-
if (v < x) lo = s;
|
|
66
|
-
else hi = s;
|
|
67
|
-
s = (lo + hi) / 2;
|
|
68
|
-
}
|
|
69
|
-
return s;
|
|
70
|
-
}
|
|
71
|
-
function videoEaseProgress(ease, p) {
|
|
72
|
-
if (p <= 0) return 0;
|
|
73
|
-
if (p >= 1) return ease === "hold" ? 0 : 1;
|
|
74
|
-
if (ease === "hold") return 0;
|
|
75
|
-
const b = videoEaseBezier(ease);
|
|
76
|
-
if (!b) return p;
|
|
77
|
-
return bez(b[1], b[3], bezierParam(b[0], b[2], p));
|
|
78
|
-
}
|
|
79
|
-
function keyBefore(keys, t) {
|
|
80
|
-
let lo = 0;
|
|
81
|
-
let hi = keys.length - 1;
|
|
82
|
-
if (t < keys[0].t) return -1;
|
|
83
|
-
while (lo < hi) {
|
|
84
|
-
const mid = lo + hi + 1 >> 1;
|
|
85
|
-
if (keys[mid].t <= t) lo = mid;
|
|
86
|
-
else hi = mid - 1;
|
|
87
|
-
}
|
|
88
|
-
return lo;
|
|
89
|
-
}
|
|
90
|
-
function videoValueAt(prop, clipTime, fallback = 0) {
|
|
91
|
-
if (prop === void 0) return fallback;
|
|
92
|
-
if (typeof prop === "number") return prop;
|
|
93
|
-
const { keys } = prop;
|
|
94
|
-
if (!keys.length) return fallback;
|
|
95
|
-
const i = keyBefore(keys, clipTime);
|
|
96
|
-
if (i < 0) return keys[0].v;
|
|
97
|
-
if (i >= keys.length - 1) return keys[keys.length - 1].v;
|
|
98
|
-
const a = keys[i];
|
|
99
|
-
const b = keys[i + 1];
|
|
100
|
-
if (a.ease === "hold") return a.v;
|
|
101
|
-
return a.v + (b.v - a.v) * videoEaseProgress(a.ease, (clipTime - a.t) / (b.t - a.t));
|
|
102
|
-
}
|
|
103
|
-
var videoKeysOf = (prop) => isVideoKeyframes(prop) ? prop.keys : [];
|
|
104
|
-
function retimeVideoAnimatable(prop, dt, k = 1) {
|
|
105
|
-
if (!isVideoKeyframes(prop)) return prop;
|
|
106
|
-
return { keys: prop.keys.map((key) => ({ ...key, t: key.t * k + dt })) };
|
|
107
|
-
}
|
|
108
|
-
function splitEase(ease, p) {
|
|
109
|
-
if (ease === "hold") return ["hold", "hold"];
|
|
110
|
-
const b = videoEaseBezier(ease);
|
|
111
|
-
if (!b) return [ease, ease];
|
|
112
|
-
const s = bezierParam(b[0], b[2], p);
|
|
113
|
-
const lerp = (u, v, k) => [u[0] + (v[0] - u[0]) * k, u[1] + (v[1] - u[1]) * k];
|
|
114
|
-
const P0 = [0, 0];
|
|
115
|
-
const P1 = [b[0], b[1]];
|
|
116
|
-
const P2 = [b[2], b[3]];
|
|
117
|
-
const P3 = [1, 1];
|
|
118
|
-
const A = lerp(P0, P1, s);
|
|
119
|
-
const B = lerp(P1, P2, s);
|
|
120
|
-
const C = lerp(P2, P3, s);
|
|
121
|
-
const AB = lerp(A, B, s);
|
|
122
|
-
const BC = lerp(B, C, s);
|
|
123
|
-
const M = lerp(AB, BC, s);
|
|
124
|
-
const r = (v) => Math.round(v * 1e6) / 1e6;
|
|
125
|
-
const left = Math.abs(M[1]) < 1e-9 ? "linear" : [r(A[0] / M[0]), r(A[1] / M[1]), r(AB[0] / M[0]), r(AB[1] / M[1])];
|
|
126
|
-
const dx = 1 - M[0];
|
|
127
|
-
const dy = 1 - M[1];
|
|
128
|
-
const right = Math.abs(dy) < 1e-9 ? "linear" : [r((BC[0] - M[0]) / dx), r((BC[1] - M[1]) / dy), r((C[0] - M[0]) / dx), r((C[1] - M[1]) / dy)];
|
|
129
|
-
return [left, right];
|
|
130
|
-
}
|
|
131
|
-
function splitVideoAnimatable(prop, at) {
|
|
132
|
-
if (!isVideoKeyframes(prop)) return [prop, prop];
|
|
133
|
-
const keys = prop.keys;
|
|
134
|
-
const eps = 1e-6;
|
|
135
|
-
const v = videoValueAt(prop, at);
|
|
136
|
-
const i = keyBefore(keys, at);
|
|
137
|
-
const exact = i >= 0 && Math.abs(keys[i].t - at) < eps;
|
|
138
|
-
let leftEase;
|
|
139
|
-
let rightEase;
|
|
140
|
-
if (exact) rightEase = keys[i].ease;
|
|
141
|
-
else if (i >= 0 && i < keys.length - 1) {
|
|
142
|
-
const a = keys[i];
|
|
143
|
-
[leftEase, rightEase] = splitEase(a.ease, (at - a.t) / (keys[i + 1].t - a.t));
|
|
144
|
-
}
|
|
145
|
-
const before = keys.slice(0, exact ? i : i + 1).map((k) => ({ ...k }));
|
|
146
|
-
if (before.length && leftEase !== void 0) before[before.length - 1] = withEase(before[before.length - 1], leftEase);
|
|
147
|
-
const left = [...before, { t: at, v }];
|
|
148
|
-
const right = [withEase({ t: 0, v }, rightEase), ...keys.slice(i + 1).map((k) => ({ ...k, t: k.t - at }))];
|
|
149
|
-
return [{ keys: left }, { keys: right }];
|
|
150
|
-
}
|
|
151
|
-
var withEase = (key, ease) => {
|
|
152
|
-
const { ease: _old, ...rest } = key;
|
|
153
|
-
return ease === void 0 || ease === "linear" ? rest : { ...rest, ease };
|
|
154
|
-
};
|
|
155
|
-
function sortVideoKeys(keys) {
|
|
156
|
-
const sorted = [...keys].sort((a, b) => a.t - b.t);
|
|
157
|
-
for (let i = 1; i < sorted.length; i++) if (Math.abs(sorted[i].t - sorted[i - 1].t) < 1e-6) throw new Error(`Two keys are at ${sorted[i].t} ms; give each key its own time.`);
|
|
158
|
-
return sorted;
|
|
159
|
-
}
|
|
160
|
-
function setVideoKey(prop, key, fallback) {
|
|
161
|
-
const keys = isVideoKeyframes(prop) ? prop.keys.filter((k) => Math.abs(k.t - key.t) >= 1e-6) : [];
|
|
162
|
-
if (!isVideoKeyframes(prop)) {
|
|
163
|
-
const v = prop ?? fallback;
|
|
164
|
-
if (key.t !== 0) keys.push({ t: 0, v });
|
|
165
|
-
}
|
|
166
|
-
return { keys: sortVideoKeys([...keys, key]) };
|
|
167
|
-
}
|
|
168
|
-
function removeVideoKey(prop, t) {
|
|
169
|
-
const keys = prop.keys.filter((k) => Math.abs(k.t - t) >= 1e-6);
|
|
170
|
-
return keys.length ? { keys } : prop.keys[0].v;
|
|
171
|
-
}
|
|
172
|
-
var fmt = (v) => {
|
|
173
|
-
const r = Math.round(v * 1e4) / 1e4;
|
|
174
|
-
return Object.is(r, -0) ? "0" : String(r);
|
|
175
|
-
};
|
|
176
|
-
var signed = (v) => fmt(v).startsWith("-") ? fmt(v) : `+${fmt(v)}`;
|
|
177
|
-
function videoKeyframeBreakpoints(prop, step) {
|
|
178
|
-
const { keys } = prop;
|
|
179
|
-
const points = [];
|
|
180
|
-
const holds = /* @__PURE__ */ new Set();
|
|
181
|
-
for (let i = 0; i < keys.length; i++) {
|
|
182
|
-
const a = keys[i];
|
|
183
|
-
points.push([a.t, a.v]);
|
|
184
|
-
const b = keys[i + 1];
|
|
185
|
-
if (!b) break;
|
|
186
|
-
if (a.ease === "hold") {
|
|
187
|
-
holds.add(points.length - 1);
|
|
188
|
-
continue;
|
|
189
|
-
}
|
|
190
|
-
if (!videoEaseBezier(a.ease)) continue;
|
|
191
|
-
const n2 = Math.max(1, Math.ceil((b.t - a.t) / step));
|
|
192
|
-
for (let k = 1; k < n2; k++) {
|
|
193
|
-
const t = a.t + (b.t - a.t) * k / n2;
|
|
194
|
-
points.push([t, videoValueAt(prop, t)]);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
return { points, holds };
|
|
198
|
-
}
|
|
199
|
-
function videoKeyframeFfmpegExpr(prop, time, fallback, step) {
|
|
200
|
-
if (!isVideoKeyframes(prop)) return fmt(prop ?? fallback);
|
|
201
|
-
const { points, holds } = videoKeyframeBreakpoints(prop, step);
|
|
202
|
-
if (points.length === 1 || points.every(([, v]) => v === points[0][1])) return fmt(points[0][1]);
|
|
203
|
-
let expr = fmt(points[points.length - 1][1]);
|
|
204
|
-
for (let i = points.length - 2; i >= 0; i--) {
|
|
205
|
-
const [t0, v0] = points[i];
|
|
206
|
-
const [t1, v1] = points[i + 1];
|
|
207
|
-
const seg = holds.has(i) || v0 === v1 ? fmt(v0) : `${fmt(v0)}${signed((v1 - v0) / (t1 - t0))}*(${time}${signed(-t0)})`;
|
|
208
|
-
expr = `if(lt(${time},${fmt(t1)}),${seg},${expr})`;
|
|
209
|
-
}
|
|
210
|
-
return `if(lt(${time},${fmt(points[0][0])}),${fmt(points[0][1])},${expr})`;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// src/video-editor/lut.ts
|
|
214
|
-
var VIDEO_LUT_MAX_SIZE = 129;
|
|
215
|
-
function parseVideoCubeLut(text) {
|
|
216
|
-
let size = 0;
|
|
217
|
-
let size1 = 0;
|
|
218
|
-
let title;
|
|
219
|
-
const domainMin = [0, 0, 0];
|
|
220
|
-
const domainMax = [1, 1, 1];
|
|
221
|
-
let data = null;
|
|
222
|
-
let rows = 0;
|
|
223
|
-
const lines = text.split(/\r\n|\r|\n/);
|
|
224
|
-
const fail2 = (line, message) => {
|
|
225
|
-
throw new Error(`Line ${line + 1} of the .cube file: ${message}.`);
|
|
226
|
-
};
|
|
227
|
-
const numbers = (parts, line) => parts.map((p) => {
|
|
228
|
-
const v = Number(p);
|
|
229
|
-
if (!Number.isFinite(v)) fail2(line, `"${p}" is not a number`);
|
|
230
|
-
return v;
|
|
231
|
-
});
|
|
232
|
-
for (let i = 0; i < lines.length; i++) {
|
|
233
|
-
const line = lines[i].replace(/#.*/, "").trim();
|
|
234
|
-
if (!line) continue;
|
|
235
|
-
const parts = line.split(/\s+/);
|
|
236
|
-
const key = parts[0];
|
|
237
|
-
if (/^[A-Za-z]/.test(key)) {
|
|
238
|
-
if (rows) fail2(i, `"${key}" comes after the table; keywords go before it`);
|
|
239
|
-
if (key === "TITLE") title = line.slice(5).trim().replace(/^"|"$/g, "");
|
|
240
|
-
else if (key === "LUT_3D_SIZE") {
|
|
241
|
-
size = Number(parts[1]);
|
|
242
|
-
if (!Number.isInteger(size) || size < 2 || size > VIDEO_LUT_MAX_SIZE) fail2(i, `LUT_3D_SIZE must be a whole number from 2 to ${VIDEO_LUT_MAX_SIZE}, not "${parts[1] ?? ""}"`);
|
|
243
|
-
} else if (key === "LUT_1D_SIZE") size1 = Number(parts[1]) || 1;
|
|
244
|
-
else if (key === "DOMAIN_MIN" || key === "DOMAIN_MAX") {
|
|
245
|
-
const v2 = numbers(parts.slice(1), i);
|
|
246
|
-
if (v2.length !== 3) fail2(i, `${key} needs 3 numbers`);
|
|
247
|
-
(key === "DOMAIN_MIN" ? domainMin : domainMax).splice(0, 3, ...v2);
|
|
248
|
-
} else if (key === "LUT_3D_INPUT_RANGE") {
|
|
249
|
-
const v2 = numbers(parts.slice(1), i);
|
|
250
|
-
if (v2.length !== 2 || !(v2[1] > v2[0])) fail2(i, "LUT_3D_INPUT_RANGE needs a minimum and a larger maximum");
|
|
251
|
-
domainMin.fill(v2[0]);
|
|
252
|
-
domainMax.fill(v2[1]);
|
|
253
|
-
}
|
|
254
|
-
continue;
|
|
255
|
-
}
|
|
256
|
-
if (!size) fail2(i, size1 ? "this is a 1D LUT (LUT_1D_SIZE); only 3D LUTs (LUT_3D_SIZE) are supported" : "the table starts before LUT_3D_SIZE");
|
|
257
|
-
data ??= new Float32Array(size * size * size * 3);
|
|
258
|
-
const v = numbers(parts, i);
|
|
259
|
-
if (v.length !== 3) fail2(i, `a table row needs 3 numbers, not ${v.length}`);
|
|
260
|
-
if (rows >= size ** 3) fail2(i, `the table has more than the ${size ** 3} rows LUT_3D_SIZE ${size} needs`);
|
|
261
|
-
data.set(v, rows * 3);
|
|
262
|
-
rows++;
|
|
263
|
-
}
|
|
264
|
-
if (!size) throw new Error(size1 ? "The .cube file is a 1D LUT (LUT_1D_SIZE); only 3D LUTs (LUT_3D_SIZE) are supported." : "The .cube file has no LUT_3D_SIZE line, so it is not a 3D LUT.");
|
|
265
|
-
if (rows !== size ** 3) throw new Error(`The .cube file has ${rows} table rows; LUT_3D_SIZE ${size} needs ${size ** 3}.`);
|
|
266
|
-
for (let c = 0; c < 3; c++) if (!(domainMax[c] > domainMin[c])) throw new Error("The .cube file's DOMAIN_MAX must be larger than its DOMAIN_MIN on every channel.");
|
|
267
|
-
return { ...title ? { title } : {}, size, domainMin, domainMax, data };
|
|
268
|
-
}
|
|
269
|
-
function videoLutFromFunction(size, fn, title) {
|
|
270
|
-
const n2 = size - 1;
|
|
271
|
-
const data = new Float32Array(size ** 3 * 3);
|
|
272
|
-
let i = 0;
|
|
273
|
-
for (let b = 0; b < size; b++)
|
|
274
|
-
for (let g = 0; g < size; g++)
|
|
275
|
-
for (let r = 0; r < size; r++) {
|
|
276
|
-
data.set(fn([r / n2, g / n2, b / n2]), i);
|
|
277
|
-
i += 3;
|
|
278
|
-
}
|
|
279
|
-
return { ...title ? { title } : {}, size, domainMin: [0, 0, 0], domainMax: [1, 1, 1], data };
|
|
280
|
-
}
|
|
281
|
-
function videoLutToCube(lut) {
|
|
282
|
-
const f = (v) => (Math.round(v * 1e6) / 1e6).toString();
|
|
283
|
-
const head = [...lut.title ? [`TITLE "${lut.title.replace(/"/g, "'")}"`] : [], `LUT_3D_SIZE ${lut.size}`];
|
|
284
|
-
if (lut.domainMin.some((v) => v !== 0) || lut.domainMax.some((v) => v !== 1)) head.push(`DOMAIN_MIN ${lut.domainMin.map(f).join(" ")}`, `DOMAIN_MAX ${lut.domainMax.map(f).join(" ")}`);
|
|
285
|
-
const rows = [];
|
|
286
|
-
for (let i = 0; i < lut.data.length; i += 3) rows.push(`${f(lut.data[i])} ${f(lut.data[i + 1])} ${f(lut.data[i + 2])}`);
|
|
287
|
-
return `${head.join("\n")}
|
|
288
|
-
${rows.join("\n")}
|
|
289
|
-
`;
|
|
290
|
-
}
|
|
291
|
-
var clamp01 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
|
|
292
|
-
function applyVideoLut(lut, rgb, out = [0, 0, 0]) {
|
|
293
|
-
const { size, data, domainMin, domainMax } = lut;
|
|
294
|
-
const n2 = size - 1;
|
|
295
|
-
const x = clamp01((rgb[0] - domainMin[0]) / (domainMax[0] - domainMin[0])) * n2;
|
|
296
|
-
const y = clamp01((rgb[1] - domainMin[1]) / (domainMax[1] - domainMin[1])) * n2;
|
|
297
|
-
const z = clamp01((rgb[2] - domainMin[2]) / (domainMax[2] - domainMin[2])) * n2;
|
|
298
|
-
const r0 = Math.min(n2 - 1, Math.floor(x));
|
|
299
|
-
const g0 = Math.min(n2 - 1, Math.floor(y));
|
|
300
|
-
const b0 = Math.min(n2 - 1, Math.floor(z));
|
|
301
|
-
const fr = x - r0;
|
|
302
|
-
const fg = y - g0;
|
|
303
|
-
const fb = z - b0;
|
|
304
|
-
const sr = 3;
|
|
305
|
-
const sg = 3 * size;
|
|
306
|
-
const sb = 3 * size * size;
|
|
307
|
-
const o = r0 * sr + g0 * sg + b0 * sb;
|
|
308
|
-
let a, c, w0, w1, w2, w3;
|
|
309
|
-
if (fr > fg) {
|
|
310
|
-
if (fg > fb) [a, c, w0, w1, w2, w3] = [sr, sr + sg, 1 - fr, fr - fg, fg - fb, fb];
|
|
311
|
-
else if (fr > fb) [a, c, w0, w1, w2, w3] = [sr, sr + sb, 1 - fr, fr - fb, fb - fg, fg];
|
|
312
|
-
else [a, c, w0, w1, w2, w3] = [sb, sb + sr, 1 - fb, fb - fr, fr - fg, fg];
|
|
313
|
-
} else if (fb > fg) [a, c, w0, w1, w2, w3] = [sb, sb + sg, 1 - fb, fb - fg, fg - fr, fr];
|
|
314
|
-
else if (fb > fr) [a, c, w0, w1, w2, w3] = [sg, sg + sb, 1 - fg, fg - fb, fb - fr, fr];
|
|
315
|
-
else [a, c, w0, w1, w2, w3] = [sg, sg + sr, 1 - fg, fg - fr, fr - fb, fb];
|
|
316
|
-
const d = sr + sg + sb;
|
|
317
|
-
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]);
|
|
318
|
-
return out;
|
|
319
|
-
}
|
|
320
|
-
function applyVideoLutToPixels(lut, data, strength = 1) {
|
|
321
|
-
const s = clamp01(strength);
|
|
322
|
-
if (!s) return;
|
|
323
|
-
const rgb = [0, 0, 0];
|
|
324
|
-
const out = [0, 0, 0];
|
|
325
|
-
for (let i = 0; i < data.length; i += 4) {
|
|
326
|
-
if (!data[i + 3]) continue;
|
|
327
|
-
rgb[0] = data[i] / 255;
|
|
328
|
-
rgb[1] = data[i + 1] / 255;
|
|
329
|
-
rgb[2] = data[i + 2] / 255;
|
|
330
|
-
applyVideoLut(lut, rgb, out);
|
|
331
|
-
for (let k = 0; k < 3; k++) data[i + k] = Math.round((rgb[k] + (out[k] - rgb[k]) * s) * 255);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
var approximations = /* @__PURE__ */ new WeakMap();
|
|
335
|
-
function videoLutSvgApproximation(lut) {
|
|
336
|
-
const cached = approximations.get(lut);
|
|
337
|
-
if (cached) return cached;
|
|
338
|
-
const points = 17;
|
|
339
|
-
const tables = [[], [], []];
|
|
340
|
-
for (let i = 0; i < points; i++) {
|
|
341
|
-
const v = i / (points - 1);
|
|
342
|
-
const out = applyVideoLut(lut, [v, v, v]);
|
|
343
|
-
for (let k = 0; k < 3; k++) tables[k].push(out[k]);
|
|
344
|
-
}
|
|
345
|
-
const curve = (k, v) => {
|
|
346
|
-
const x = clamp01(v) * (points - 1);
|
|
347
|
-
const i = Math.min(points - 2, Math.floor(x));
|
|
348
|
-
return tables[k][i] + (tables[k][i + 1] - tables[k][i]) * (x - i);
|
|
349
|
-
};
|
|
350
|
-
const ata = Array.from({ length: 4 }, () => [0, 0, 0, 0]);
|
|
351
|
-
const atb = Array.from({ length: 3 }, () => [0, 0, 0, 0]);
|
|
352
|
-
const grid = 9;
|
|
353
|
-
for (let b = 0; b < grid; b++)
|
|
354
|
-
for (let g = 0; g < grid; g++)
|
|
355
|
-
for (let r = 0; r < grid; r++) {
|
|
356
|
-
const x = [r / (grid - 1), g / (grid - 1), b / (grid - 1)];
|
|
357
|
-
const z = [curve(0, x[0]), curve(1, x[1]), curve(2, x[2]), 1];
|
|
358
|
-
const y = applyVideoLut(lut, x);
|
|
359
|
-
for (let i = 0; i < 4; i++) {
|
|
360
|
-
for (let j = 0; j < 4; j++) ata[i][j] += z[i] * z[j];
|
|
361
|
-
for (let k = 0; k < 3; k++) atb[k][i] += z[i] * y[k];
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
const matrix = atb.map((rhs) => solve4(ata, rhs));
|
|
365
|
-
const result = { tables, matrix };
|
|
366
|
-
approximations.set(lut, result);
|
|
367
|
-
return result;
|
|
368
|
-
}
|
|
369
|
-
function solve4(a, b) {
|
|
370
|
-
const m = a.map((row, i) => [...row, b[i]]);
|
|
371
|
-
for (let col = 0; col < 4; col++) {
|
|
372
|
-
let pivot = col;
|
|
373
|
-
for (let r = col + 1; r < 4; r++) if (Math.abs(m[r][col]) > Math.abs(m[pivot][col])) pivot = r;
|
|
374
|
-
if (Math.abs(m[pivot][col]) < 1e-12) continue;
|
|
375
|
-
[m[col], m[pivot]] = [m[pivot], m[col]];
|
|
376
|
-
for (let r = 0; r < 4; r++) {
|
|
377
|
-
if (r === col) continue;
|
|
378
|
-
const f = m[r][col] / m[col][col];
|
|
379
|
-
for (let c = col; c < 5; c++) m[r][c] -= f * m[col][c];
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
return m.map((row, i) => Math.abs(row[i]) < 1e-12 ? 0 : row[4] / row[i]);
|
|
383
|
-
}
|
|
384
|
-
var VideoLutCache = class {
|
|
385
|
-
constructor(loader = async (src) => {
|
|
386
|
-
const res = await fetch(src);
|
|
387
|
-
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`.trim());
|
|
388
|
-
return res.text();
|
|
389
|
-
}) {
|
|
390
|
-
this.loader = loader;
|
|
391
|
-
}
|
|
392
|
-
loader;
|
|
393
|
-
ready = /* @__PURE__ */ new Map();
|
|
394
|
-
pending = /* @__PURE__ */ new Map();
|
|
395
|
-
failed = /* @__PURE__ */ new Map();
|
|
396
|
-
listeners = /* @__PURE__ */ new Set();
|
|
397
|
-
/** The LUT if loaded, without starting to load it. */
|
|
398
|
-
peek(src) {
|
|
399
|
-
return this.ready.get(src);
|
|
400
|
-
}
|
|
401
|
-
/** The LUT if loaded; otherwise `undefined`, and loading starts (once) unless it failed. */
|
|
402
|
-
get(src) {
|
|
403
|
-
const lut = this.ready.get(src);
|
|
404
|
-
if (!lut && !this.failed.has(src)) void this.load(src).catch(() => {
|
|
405
|
-
});
|
|
406
|
-
return lut;
|
|
407
|
-
}
|
|
408
|
-
/** Why loading a LUT failed, if it did. */
|
|
409
|
-
error(src) {
|
|
410
|
-
return this.failed.get(src);
|
|
411
|
-
}
|
|
412
|
-
/** The LUT, loading and reading it if needed. Rejects with a readable error. */
|
|
413
|
-
load(src) {
|
|
414
|
-
const ready = this.ready.get(src);
|
|
415
|
-
if (ready) return Promise.resolve(ready);
|
|
416
|
-
const failed = this.failed.get(src);
|
|
417
|
-
if (failed) return Promise.reject(new Error(failed));
|
|
418
|
-
let pending = this.pending.get(src);
|
|
419
|
-
if (!pending) {
|
|
420
|
-
pending = Promise.resolve().then(() => this.loader(src)).then((v) => typeof v === "string" ? parseVideoCubeLut(v) : v).then(
|
|
421
|
-
(lut) => {
|
|
422
|
-
this.pending.delete(src);
|
|
423
|
-
this.ready.set(src, lut);
|
|
424
|
-
this.emit();
|
|
425
|
-
return lut;
|
|
426
|
-
},
|
|
427
|
-
(e) => {
|
|
428
|
-
this.pending.delete(src);
|
|
429
|
-
const message = `The LUT "${src}" could not be read: ${e instanceof Error ? e.message : String(e)}`;
|
|
430
|
-
this.failed.set(src, message);
|
|
431
|
-
this.emit();
|
|
432
|
-
throw new Error(message);
|
|
433
|
-
}
|
|
434
|
-
);
|
|
435
|
-
this.pending.set(src, pending);
|
|
436
|
-
}
|
|
437
|
-
return pending;
|
|
438
|
-
}
|
|
439
|
-
/** Uses a LUT the host already has (`.cube` text or a parsed LUT), under `src`. */
|
|
440
|
-
set(src, lut) {
|
|
441
|
-
const parsed = typeof lut === "string" ? parseVideoCubeLut(lut) : lut;
|
|
442
|
-
this.ready.set(src, parsed);
|
|
443
|
-
this.failed.delete(src);
|
|
444
|
-
this.emit();
|
|
445
|
-
return parsed;
|
|
446
|
-
}
|
|
447
|
-
/** Forgets a failure so the next `get` tries again. */
|
|
448
|
-
retry(src) {
|
|
449
|
-
if (this.failed.delete(src)) this.emit();
|
|
450
|
-
}
|
|
451
|
-
subscribe(fn) {
|
|
452
|
-
this.listeners.add(fn);
|
|
453
|
-
return () => {
|
|
454
|
-
this.listeners.delete(fn);
|
|
455
|
-
};
|
|
456
|
-
}
|
|
457
|
-
emit() {
|
|
458
|
-
for (const l of [...this.listeners]) l();
|
|
459
|
-
}
|
|
460
|
-
};
|
|
461
|
-
|
|
462
|
-
// src/video-editor/grade.ts
|
|
463
|
-
var VIDEO_GRADE_CONTROLS = [
|
|
464
|
-
{ key: "exposure", label: "Exposure", min: -3, max: 3, step: 0.1, description: "Exposure in stops: +1 doubles every channel, -1 halves it (default 0)." },
|
|
465
|
-
{ key: "contrast", label: "Contrast", min: -1, max: 1, step: 0.05, description: "Contrast around mid grey, -1 (flat grey) to 1 (twice as steep); default 0." },
|
|
466
|
-
{ key: "saturation", label: "Saturation", min: -1, max: 1, step: 0.05, description: "Saturation, -1 (black and white) to 1 (twice as saturated); default 0." },
|
|
467
|
-
{ key: "hue", label: "Hue", min: -180, max: 180, step: 1, description: "Hue rotation in degrees (default 0)." },
|
|
468
|
-
{ key: "temperature", label: "Temperature", min: -1, max: 1, step: 0.05, description: "White balance, -1 (cool, bluer) to 1 (warm, more orange); default 0." },
|
|
469
|
-
{ key: "tint", label: "Tint", min: -1, max: 1, step: 0.05, description: "White balance, -1 (greener) to 1 (more magenta); default 0." }
|
|
470
|
-
];
|
|
471
|
-
var VIDEO_GRADE_PRESETS = {
|
|
472
|
-
warm: { temperature: 0.4, saturation: 0.1 },
|
|
473
|
-
cool: { temperature: -0.4, tint: -0.05 },
|
|
474
|
-
punchy: { contrast: 0.25, saturation: 0.3 },
|
|
475
|
-
faded: { exposure: 0.15, contrast: -0.3, saturation: -0.3 },
|
|
476
|
-
"black-and-white": { saturation: -1, contrast: 0.15 },
|
|
477
|
-
"bleach-bypass": { saturation: -0.6, contrast: 0.4 },
|
|
478
|
-
"day-for-night": { exposure: -1.5, saturation: -0.4, temperature: -0.6 }
|
|
479
|
-
};
|
|
480
|
-
var VIDEO_GRADE_PRESET_NAMES = Object.keys(VIDEO_GRADE_PRESETS);
|
|
481
|
-
var clampTo = (c, v) => Math.min(c.max, Math.max(c.min, v));
|
|
482
|
-
function normalizeVideoGradeLut(lut) {
|
|
483
|
-
if (!lut || typeof lut.src !== "string" || !lut.src) return void 0;
|
|
484
|
-
const strength = typeof lut.strength === "number" && Number.isFinite(lut.strength) ? Math.round(Math.min(1, Math.max(0, lut.strength)) * 1e3) / 1e3 : 1;
|
|
485
|
-
if (!strength) return void 0;
|
|
486
|
-
return { src: lut.src, ...strength !== 1 ? { strength } : {}, ...lut.name ? { name: lut.name } : {} };
|
|
487
|
-
}
|
|
488
|
-
function normalizeVideoGrade(grade) {
|
|
489
|
-
if (!grade) return void 0;
|
|
490
|
-
const out = {};
|
|
491
|
-
for (const c of VIDEO_GRADE_CONTROLS) {
|
|
492
|
-
const v = grade[c.key];
|
|
493
|
-
if (typeof v === "number" && Number.isFinite(v) && v !== 0) out[c.key] = Math.round(clampTo(c, v) * 1e3) / 1e3;
|
|
494
|
-
}
|
|
495
|
-
const lut = normalizeVideoGradeLut(grade.lut);
|
|
496
|
-
if (lut) out.lut = lut;
|
|
497
|
-
return Object.keys(out).length ? out : void 0;
|
|
498
|
-
}
|
|
499
|
-
function videoGradePreset(name, amount = 1) {
|
|
500
|
-
const preset = VIDEO_GRADE_PRESETS[name];
|
|
501
|
-
if (!preset) throw new Error(`There is no grade preset "${name}"; the presets are ${VIDEO_GRADE_PRESET_NAMES.join(", ")}.`);
|
|
502
|
-
const k = Math.min(1, Math.max(0, amount));
|
|
503
|
-
return normalizeVideoGrade(Object.fromEntries(Object.entries(preset).map(([key, v]) => [key, v * k])));
|
|
504
|
-
}
|
|
505
|
-
function videoGradeLook(current, name, amount = 1) {
|
|
506
|
-
return normalizeVideoGrade({ ...videoGradePreset(name, amount), ...current?.lut ? { lut: current.lut } : {} });
|
|
507
|
-
}
|
|
508
|
-
function videoGradeSteps(grade) {
|
|
509
|
-
const g = normalizeVideoGrade(grade);
|
|
510
|
-
if (!g) return [];
|
|
511
|
-
const steps = [];
|
|
512
|
-
if (g.exposure) steps.push({ kind: "brightness", amount: 2 ** g.exposure });
|
|
513
|
-
if (g.contrast) steps.push({ kind: "contrast", amount: 1 + g.contrast });
|
|
514
|
-
if (g.saturation) steps.push({ kind: "saturate", amount: 1 + g.saturation });
|
|
515
|
-
if (g.hue) steps.push({ kind: "hue-rotate", degrees: g.hue });
|
|
516
|
-
if (g.temperature || g.tint) {
|
|
517
|
-
const t = g.temperature ?? 0;
|
|
518
|
-
steps.push({ kind: "gains", r: 1 + 0.25 * t, g: 1 - 0.25 * (g.tint ?? 0), b: 1 - 0.25 * t });
|
|
519
|
-
}
|
|
520
|
-
return steps;
|
|
521
|
-
}
|
|
522
|
-
function videoGradeStepMatrix(step) {
|
|
523
|
-
switch (step.kind) {
|
|
524
|
-
case "brightness":
|
|
525
|
-
return [
|
|
526
|
-
[step.amount, 0, 0, 0],
|
|
527
|
-
[0, step.amount, 0, 0],
|
|
528
|
-
[0, 0, step.amount, 0]
|
|
529
|
-
];
|
|
530
|
-
case "contrast": {
|
|
531
|
-
const c = step.amount;
|
|
532
|
-
const o = 0.5 - 0.5 * c;
|
|
533
|
-
return [
|
|
534
|
-
[c, 0, 0, o],
|
|
535
|
-
[0, c, 0, o],
|
|
536
|
-
[0, 0, c, o]
|
|
537
|
-
];
|
|
538
|
-
}
|
|
539
|
-
case "gains":
|
|
540
|
-
return [
|
|
541
|
-
[step.r, 0, 0, 0],
|
|
542
|
-
[0, step.g, 0, 0],
|
|
543
|
-
[0, 0, step.b, 0]
|
|
544
|
-
];
|
|
545
|
-
case "saturate": {
|
|
546
|
-
const s = step.amount;
|
|
547
|
-
return [
|
|
548
|
-
[0.213 + 0.787 * s, 0.715 - 0.715 * s, 0.072 - 0.072 * s, 0],
|
|
549
|
-
[0.213 - 0.213 * s, 0.715 + 0.285 * s, 0.072 - 0.072 * s, 0],
|
|
550
|
-
[0.213 - 0.213 * s, 0.715 - 0.715 * s, 0.072 + 0.928 * s, 0]
|
|
551
|
-
];
|
|
552
|
-
}
|
|
553
|
-
case "hue-rotate": {
|
|
554
|
-
const a = step.degrees * Math.PI / 180;
|
|
555
|
-
const cos = Math.cos(a);
|
|
556
|
-
const sin = Math.sin(a);
|
|
557
|
-
return [
|
|
558
|
-
[0.213 + cos * 0.787 - sin * 0.213, 0.715 - cos * 0.715 - sin * 0.715, 0.072 - cos * 0.072 + sin * 0.928, 0],
|
|
559
|
-
[0.213 - cos * 0.213 + sin * 0.143, 0.715 + cos * 0.285 + sin * 0.14, 0.072 - cos * 0.072 - sin * 0.283, 0],
|
|
560
|
-
[0.213 - cos * 0.213 - sin * 0.787, 0.715 - cos * 0.715 + sin * 0.715, 0.072 + cos * 0.928 + sin * 0.072, 0]
|
|
561
|
-
];
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
function applyVideoGrade(grade, rgb, lut) {
|
|
566
|
-
let v = rgb.map((x) => x / 255);
|
|
567
|
-
for (const step of videoGradeSteps(grade)) {
|
|
568
|
-
const m = videoGradeStepMatrix(step);
|
|
569
|
-
v = m.map((row) => Math.min(1, Math.max(0, row[0] * v[0] + row[1] * v[1] + row[2] * v[2] + row[3])));
|
|
570
|
-
}
|
|
571
|
-
const setting = normalizeVideoGradeLut(grade?.lut);
|
|
572
|
-
if (setting && lut) {
|
|
573
|
-
const out = applyVideoLut(lut, v);
|
|
574
|
-
const s = setting.strength ?? 1;
|
|
575
|
-
v = v.map((x, k) => x + (out[k] - x) * s);
|
|
576
|
-
}
|
|
577
|
-
return v.map((x) => Math.round(x * 255));
|
|
578
|
-
}
|
|
579
|
-
var fmt2 = (v) => String(Math.round(v * 1e5) / 1e5);
|
|
580
|
-
function videoGradeStepSvgValues(step) {
|
|
581
|
-
return videoGradeStepMatrix(step).map((row) => [row[0], row[1], row[2], 0, row[3]].map(fmt2).join(" ")).concat("0 0 0 1 0").join(" ");
|
|
582
|
-
}
|
|
583
|
-
function videoLutSvgPrimitives(lut, strength = 1) {
|
|
584
|
-
const { tables, matrix } = videoLutSvgApproximation(lut);
|
|
585
|
-
const table = (k) => tables[k].map(fmt2).join(" ");
|
|
586
|
-
const values = matrix.map((row) => [row[0], row[1], row[2], 0, row[3]].map(fmt2).join(" ")).concat("0 0 0 1 0").join(" ");
|
|
587
|
-
const s = Math.min(1, Math.max(0, strength));
|
|
588
|
-
return `<feComponentTransfer result="lutIn"><feFuncA type="identity"/></feComponentTransfer><feComponentTransfer><feFuncR type="table" tableValues="${table(0)}"/><feFuncG type="table" tableValues="${table(1)}"/><feFuncB type="table" tableValues="${table(2)}"/></feComponentTransfer><feColorMatrix type="matrix" values="${values}"/>` + (s < 1 ? `<feComposite in2="lutIn" operator="arithmetic" k1="0" k2="${fmt2(s)}" k3="${fmt2(1 - s)}" k4="0"/>` : "");
|
|
589
|
-
}
|
|
590
|
-
function videoGradeSvgFilter(id, grade, lut) {
|
|
591
|
-
const steps = videoGradeSteps(grade);
|
|
592
|
-
const setting = normalizeVideoGradeLut(grade?.lut);
|
|
593
|
-
return `<filter id="${id}" color-interpolation-filters="sRGB" x="0" y="0" width="100%" height="100%">${steps.map((s) => `<feColorMatrix type="matrix" values="${videoGradeStepSvgValues(s)}"/>`).join("")}${setting && lut ? videoLutSvgPrimitives(lut, setting.strength) : ""}</filter>`;
|
|
594
|
-
}
|
|
595
|
-
var videoGradeGainsFilterId = (step) => `pixerate-wb-${[step.r, step.g, step.b].map((v) => Math.round(v * 1e3)).join("-")}`;
|
|
596
|
-
function videoGradeCssFilter(grade) {
|
|
597
|
-
return videoGradeSteps(grade).map((s) => s.kind === "brightness" ? `brightness(${fmt2(s.amount)})` : s.kind === "contrast" ? `contrast(${fmt2(s.amount)})` : s.kind === "saturate" ? `saturate(${fmt2(s.amount)})` : s.kind === "hue-rotate" ? `hue-rotate(${fmt2(s.degrees)}deg)` : `url(#${videoGradeGainsFilterId(s)})`).join(" ");
|
|
598
|
-
}
|
|
599
|
-
function videoGradeSvgDefs(grade) {
|
|
600
|
-
return videoGradeSteps(grade).filter((s) => s.kind === "gains").map((s) => {
|
|
601
|
-
const id = videoGradeGainsFilterId(s);
|
|
602
|
-
return { id, markup: `<filter id="${id}" color-interpolation-filters="sRGB"><feColorMatrix type="matrix" values="${videoGradeStepSvgValues(s)}"/></filter>` };
|
|
603
|
-
});
|
|
604
|
-
}
|
|
605
|
-
function videoGradeFfmpegFilters(grade) {
|
|
606
|
-
return videoGradeSteps(grade).map((step) => {
|
|
607
|
-
const m = videoGradeStepMatrix(step);
|
|
608
|
-
if (step.kind === "brightness" || step.kind === "contrast") {
|
|
609
|
-
const lut = (i) => `clip(val*${fmt2(m[i][i])}+${fmt2(m[i][3] * 255)},0,255)`;
|
|
610
|
-
return `lutrgb=r='${lut(0)}':g='${lut(1)}':b='${lut(2)}'`;
|
|
611
|
-
}
|
|
612
|
-
const k = ["r", "g", "b"];
|
|
613
|
-
return `colorchannelmixer=${m.flatMap((row, i) => row.slice(0, 3).map((v, j) => `${k[i]}${k[j]}=${fmt2(v)}`)).join(":")}`;
|
|
614
|
-
});
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
// src/video-editor/remap.ts
|
|
618
|
-
function videoClipSourceTime(clip, clipTime) {
|
|
619
|
-
const base = clip.in ?? 0;
|
|
620
|
-
return clip.timeRemap ? base + videoValueAt(clip.timeRemap, clipTime, 0) : base + clipTime * (clip.speed ?? 1);
|
|
621
|
-
}
|
|
622
|
-
function videoClipSpeedAt(clip, clipTime) {
|
|
623
|
-
if (clip.type !== "media") return 1;
|
|
624
|
-
if (!clip.timeRemap) return clip.speed ?? 1;
|
|
625
|
-
const t = Math.min(Math.max(clipTime, 0), Math.max(0, clip.duration - 1));
|
|
626
|
-
return Math.max(0, videoClipSourceTime(clip, t + 1) - videoClipSourceTime(clip, t));
|
|
627
|
-
}
|
|
628
|
-
function videoSpeedRampKeys(segments) {
|
|
629
|
-
if (!segments.length) throw new Error("Give at least one stretch { duration, speed }.");
|
|
630
|
-
const keys = [];
|
|
631
|
-
let t = 0;
|
|
632
|
-
let v = 0;
|
|
633
|
-
segments.forEach((s, i) => {
|
|
634
|
-
if (!(s.duration > 0)) throw new Error(`Stretch ${i + 1} needs a duration above 0 ms.`);
|
|
635
|
-
for (const [name, x] of [["speed", s.speed], ["toSpeed", s.toSpeed]])
|
|
636
|
-
if (x !== void 0 && !(x >= 0 && x <= 20)) throw new Error(`Stretch ${i + 1}'s ${name} must be from 0 (a freeze) to 20; got ${x}.`);
|
|
637
|
-
const a = s.speed;
|
|
638
|
-
const b = s.toSpeed ?? s.speed;
|
|
639
|
-
const avg = (a + b) / 2;
|
|
640
|
-
let ease = "linear";
|
|
641
|
-
if (a !== b && avg > 0) {
|
|
642
|
-
ease = [1 / 3, 2 * a / (3 * (a + b)), 2 / 3, 1 - 2 * b / (3 * (a + b))].map((n2) => Math.round(n2 * 1e6) / 1e6);
|
|
643
|
-
}
|
|
644
|
-
keys.push({ t, v, ...ease === "linear" ? {} : { ease } });
|
|
645
|
-
t += s.duration;
|
|
646
|
-
v += avg * s.duration;
|
|
647
|
-
});
|
|
648
|
-
keys.push({ t, v });
|
|
649
|
-
return { keys: mergeLinear(keys), duration: t, source: v };
|
|
650
|
-
}
|
|
651
|
-
function mergeLinear(keys) {
|
|
652
|
-
const out = [keys[0]];
|
|
653
|
-
for (let i = 1; i < keys.length; i++) {
|
|
654
|
-
const prev = out[out.length - 1];
|
|
655
|
-
const k = keys[i];
|
|
656
|
-
const before = out.length > 1 ? out[out.length - 2] : null;
|
|
657
|
-
if (before && !before.ease && !prev.ease) {
|
|
658
|
-
const s1 = (prev.v - before.v) / (prev.t - before.t);
|
|
659
|
-
const s2 = (k.v - prev.v) / (k.t - prev.t);
|
|
660
|
-
if (Math.abs(s1 - s2) < 1e-9) {
|
|
661
|
-
out[out.length - 1] = k;
|
|
662
|
-
continue;
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
out.push(k);
|
|
666
|
-
}
|
|
667
|
-
return out;
|
|
668
|
-
}
|
|
669
|
-
function checkVideoTimeRemap(remap, path = "timeRemap") {
|
|
670
|
-
const { keys } = remap;
|
|
671
|
-
if (!keys.length) throw new Error(`${path} needs at least one key.`);
|
|
672
|
-
keys.forEach((k, i) => {
|
|
673
|
-
if (k.v < 0) throw new Error(`${path}.keys[${i}].v must be at least 0 (ms after the in point).`);
|
|
674
|
-
const next = keys[i + 1];
|
|
675
|
-
if (!next) return;
|
|
676
|
-
if (next.v < k.v - 1e-6) throw new Error(`${path}.keys[${i + 1}] goes back in the source (${next.v} ms after ${k.v} ms); a ramp only plays forwards, so make each value at least the one before.`);
|
|
677
|
-
if (!videoEaseBezier(k.ease)) return;
|
|
678
|
-
let prev = k.v;
|
|
679
|
-
for (let n2 = 1; n2 <= 32; n2++) {
|
|
680
|
-
const v = videoValueAt(remap, k.t + (next.t - k.t) * n2 / 32);
|
|
681
|
-
if (v < prev - 1e-3) throw new Error(`${path}.keys[${i}].ease overshoots, which would play backwards; use a curve that only moves forwards (or linear).`);
|
|
682
|
-
prev = v;
|
|
683
|
-
}
|
|
684
|
-
});
|
|
685
|
-
}
|
|
686
|
-
function videoTimeRemapSpan(remap) {
|
|
687
|
-
return { from: remap.keys[0].v, to: remap.keys[remap.keys.length - 1].v };
|
|
688
|
-
}
|
|
689
|
-
function videoTimeRemapBreakpoints(remap, step) {
|
|
690
|
-
const { points, holds } = videoKeyframeBreakpoints(remap, step);
|
|
691
|
-
const out = [];
|
|
692
|
-
points.forEach(([t, v], i) => {
|
|
693
|
-
out.push({ t, v });
|
|
694
|
-
if (holds.has(i) && points[i + 1]) out.push({ t: points[i + 1][0], v });
|
|
695
|
-
});
|
|
696
|
-
return out;
|
|
697
|
-
}
|
|
698
|
-
function videoSpeedRampEdit(clip, segments, ripple = true) {
|
|
699
|
-
const ramp = videoSpeedRampKeys(segments);
|
|
700
|
-
const offset = videoClipSourceTime(clip, 0) - (clip.in ?? 0);
|
|
701
|
-
return { op: "set_time_remap", clip: clip.id, keys: ramp.keys.map((k) => ({ ...k, v: k.v + offset })), duration: ramp.duration, ripple };
|
|
702
|
-
}
|
|
703
|
-
function videoFreezeFrameEdit(clip, at, duration, ripple = true) {
|
|
704
|
-
if (at < 0 || at > clip.duration) throw new Error(`${Math.round(at)} ms is outside clip "${clip.id}" (0\u2013${clip.duration} ms of clip time).`);
|
|
705
|
-
const remap = clip.timeRemap ?? { keys: [{ t: 0, v: 0 }, { t: clip.duration, v: clip.duration * (clip.speed ?? 1) }] };
|
|
706
|
-
const [left, right] = splitVideoAnimatable(remap, at);
|
|
707
|
-
const keys = [...left.keys.slice(0, -1), { ...left.keys[left.keys.length - 1], ease: "linear" }, ...right.keys.map((k) => ({ ...k, t: k.t + at + duration }))];
|
|
708
|
-
return { op: "set_time_remap", clip: clip.id, keys, duration: clip.duration + duration, ripple };
|
|
709
|
-
}
|
|
710
|
-
var videoSlowMotionRamp = (duration) => [
|
|
711
|
-
{ duration: Math.max(1, duration / 2), speed: 1 },
|
|
712
|
-
{ duration: 500, speed: 1, toSpeed: 0.3 },
|
|
713
|
-
{ duration: 1500, speed: 0.3 }
|
|
714
|
-
];
|
|
715
|
-
|
|
716
136
|
// src/video-editor/parse.ts
|
|
717
137
|
var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
718
138
|
var fail = (path, what) => {
|
|
@@ -745,7 +165,7 @@ var compact = (o) => Object.fromEntries(Object.entries(o).filter(([, v]) => v !=
|
|
|
745
165
|
function parseVideoEase(v, path) {
|
|
746
166
|
if (v === void 0) return void 0;
|
|
747
167
|
if (Array.isArray(v)) {
|
|
748
|
-
if (v.length !== 4 || !v.every((
|
|
168
|
+
if (v.length !== 4 || !v.every((n) => typeof n === "number" && Number.isFinite(n))) fail(path, "must be four numbers [x1, y1, x2, y2] (CSS cubic-bezier)");
|
|
749
169
|
if (v[0] < 0 || v[0] > 1 || v[2] < 0 || v[2] > 1) fail(path, "must have x1 and x2 between 0 and 1");
|
|
750
170
|
return [v[0], v[1], v[2], v[3]];
|
|
751
171
|
}
|
|
@@ -1026,258 +446,6 @@ function createVideoProject(options = {}) {
|
|
|
1026
446
|
});
|
|
1027
447
|
}
|
|
1028
448
|
|
|
1029
|
-
// src/video-editor/timeline.ts
|
|
1030
|
-
var VIDEO_TIME_EPSILON = 0.5;
|
|
1031
|
-
var videoClipEnd = (clip) => clip.start + clip.duration;
|
|
1032
|
-
var videoTrackSyncLocked = (track) => track.syncLocked !== false;
|
|
1033
|
-
var videoClipSpeed = (clip) => clip.type === "media" ? clip.speed ?? 1 : 1;
|
|
1034
|
-
function videoClipSourceRange(clip) {
|
|
1035
|
-
if (clip.type === "sequence") return { in: clip.in ?? 0, out: (clip.in ?? 0) + clip.duration };
|
|
1036
|
-
if (clip.type !== "media") return null;
|
|
1037
|
-
const start = clip.in ?? 0;
|
|
1038
|
-
if (clip.timeRemap?.keys.length) return { in: start + clip.timeRemap.keys[0].v, out: start + clip.timeRemap.keys[clip.timeRemap.keys.length - 1].v };
|
|
1039
|
-
return { in: start, out: start + clip.duration * videoClipSpeed(clip) };
|
|
1040
|
-
}
|
|
1041
|
-
var videoFrameDuration = (fps) => 1e3 / fps;
|
|
1042
|
-
function snapToVideoFrame(time, fps) {
|
|
1043
|
-
const frame = Math.round(time * fps / 1e3);
|
|
1044
|
-
return Math.round(frame * 1e3 / fps * 1e3) / 1e3;
|
|
1045
|
-
}
|
|
1046
|
-
var videoFrames = (time, fps) => Math.round(time * fps / 1e3);
|
|
1047
|
-
function isVideoDropFrameRate(fps) {
|
|
1048
|
-
const nominal = Math.round(fps);
|
|
1049
|
-
return (nominal === 30 || nominal === 60) && Math.abs(fps - nominal * 1e3 / 1001) < 5e-3;
|
|
1050
|
-
}
|
|
1051
|
-
var exactVideoRate = (fps) => {
|
|
1052
|
-
const nominal = Math.round(fps);
|
|
1053
|
-
return nominal !== fps && Math.abs(fps - nominal * 1e3 / 1001) < 5e-3 ? nominal * 1e3 / 1001 : fps;
|
|
1054
|
-
};
|
|
1055
|
-
var videoTimecodeFrames = (time, fps) => Math.max(0, Math.round(time * exactVideoRate(fps) / 1e3));
|
|
1056
|
-
function formatVideoTimecode(time, fps, dropFrame = isVideoDropFrameRate(fps)) {
|
|
1057
|
-
return formatVideoTimecodeFrames(videoTimecodeFrames(time, fps), fps, dropFrame);
|
|
1058
|
-
}
|
|
1059
|
-
function formatVideoTimecodeFrames(frames, fps, dropFrame = isVideoDropFrameRate(fps)) {
|
|
1060
|
-
const base = Math.round(fps);
|
|
1061
|
-
let total = Math.max(0, Math.round(frames));
|
|
1062
|
-
const drop = dropFrame && isVideoDropFrameRate(fps);
|
|
1063
|
-
if (drop) {
|
|
1064
|
-
const skip = base / 15;
|
|
1065
|
-
const perMinute = base * 60 - skip;
|
|
1066
|
-
const perTen = perMinute * 10 + skip;
|
|
1067
|
-
const tens = Math.floor(total / perTen);
|
|
1068
|
-
const rest = total % perTen;
|
|
1069
|
-
total += skip * 9 * tens + (rest > skip ? skip * Math.floor((rest - skip) / perMinute) : 0);
|
|
1070
|
-
}
|
|
1071
|
-
const ff = total % base;
|
|
1072
|
-
const secs = Math.floor(total / base);
|
|
1073
|
-
const pad = (n2) => String(n2).padStart(2, "0");
|
|
1074
|
-
return `${pad(Math.floor(secs / 3600))}:${pad(Math.floor(secs / 60) % 60)}:${pad(secs % 60)}${drop ? ";" : ":"}${pad(ff)}`;
|
|
1075
|
-
}
|
|
1076
|
-
var videoHourFrames = (fps, dropFrame = isVideoDropFrameRate(fps)) => {
|
|
1077
|
-
const base = Math.round(fps);
|
|
1078
|
-
return dropFrame && isVideoDropFrameRate(fps) ? base * 3600 - base / 15 * 54 : base * 3600;
|
|
1079
|
-
};
|
|
1080
|
-
var formatVideoSeconds = (time) => `${(time / 1e3).toFixed(2)} s`;
|
|
1081
|
-
var videoAssetHasSound = (asset) => !!asset && (asset.kind === "audio" || asset.kind === "video" && asset.hasAudio !== false);
|
|
1082
|
-
function videoAssetMap(project) {
|
|
1083
|
-
return new Map(project.assets.map((a) => [a.id, a]));
|
|
1084
|
-
}
|
|
1085
|
-
function videoSequenceMap(project) {
|
|
1086
|
-
return new Map((project.sequences ?? []).map((q) => [q.id, q]));
|
|
1087
|
-
}
|
|
1088
|
-
function videoSequenceDuration(sequence) {
|
|
1089
|
-
let end = 0;
|
|
1090
|
-
for (const track of sequence.tracks) for (const clip of track.clips) end = Math.max(end, videoClipEnd(clip));
|
|
1091
|
-
return end;
|
|
1092
|
-
}
|
|
1093
|
-
var videoSequenceChildren = (sequence) => [...new Set(sequence.tracks.flatMap((t) => t.clips.flatMap((c) => c.type === "sequence" ? [c.sequence] : [])))];
|
|
1094
|
-
function findVideoSequenceCycle(project, id) {
|
|
1095
|
-
const sequences = videoSequenceMap(project);
|
|
1096
|
-
const walk = (at, path) => {
|
|
1097
|
-
const seq = sequences.get(at);
|
|
1098
|
-
if (!seq) return null;
|
|
1099
|
-
for (const child of videoSequenceChildren(seq)) {
|
|
1100
|
-
if (child === id) return [...path, child];
|
|
1101
|
-
if (path.includes(child)) continue;
|
|
1102
|
-
const found = walk(child, [...path, child]);
|
|
1103
|
-
if (found) return found;
|
|
1104
|
-
}
|
|
1105
|
-
return null;
|
|
1106
|
-
};
|
|
1107
|
-
return walk(id, [id]);
|
|
1108
|
-
}
|
|
1109
|
-
function allVideoTracks(project) {
|
|
1110
|
-
return [...project.tracks.map((track) => ({ track })), ...(project.sequences ?? []).flatMap((q) => q.tracks.map((track) => ({ sequence: q.id, track })))];
|
|
1111
|
-
}
|
|
1112
|
-
function sortedVideoClips(track) {
|
|
1113
|
-
return [...track.clips].sort((a, b) => a.start - b.start || a.duration - b.duration);
|
|
1114
|
-
}
|
|
1115
|
-
function findVideoClip(project, id) {
|
|
1116
|
-
for (let trackIndex = 0; trackIndex < project.tracks.length; trackIndex++) {
|
|
1117
|
-
const track = project.tracks[trackIndex];
|
|
1118
|
-
const index = track.clips.findIndex((c) => c.id === id);
|
|
1119
|
-
if (index >= 0) return { track, trackIndex, clip: track.clips[index], index };
|
|
1120
|
-
}
|
|
1121
|
-
return null;
|
|
1122
|
-
}
|
|
1123
|
-
function allVideoClipIds(project) {
|
|
1124
|
-
return allVideoTracks(project).flatMap(({ track }) => track.clips.map((c) => c.id));
|
|
1125
|
-
}
|
|
1126
|
-
function findVideoSequenceClip(project, id) {
|
|
1127
|
-
for (const sequence of project.sequences ?? [])
|
|
1128
|
-
for (const track of sequence.tracks) {
|
|
1129
|
-
const clip = track.clips.find((c) => c.id === id);
|
|
1130
|
-
if (clip) return { sequence, track, clip };
|
|
1131
|
-
}
|
|
1132
|
-
return null;
|
|
1133
|
-
}
|
|
1134
|
-
function videoProjectDuration(project) {
|
|
1135
|
-
let end = 0;
|
|
1136
|
-
for (const track of project.tracks) for (const clip of track.clips) end = Math.max(end, videoClipEnd(clip));
|
|
1137
|
-
return end;
|
|
1138
|
-
}
|
|
1139
|
-
function videoClipBefore(track, clip) {
|
|
1140
|
-
let best = null;
|
|
1141
|
-
for (const c of track.clips) if (c !== clip && Math.abs(videoClipEnd(c) - clip.start) <= VIDEO_TIME_EPSILON) best = c;
|
|
1142
|
-
return best;
|
|
1143
|
-
}
|
|
1144
|
-
function videoClipAfter(track, clip) {
|
|
1145
|
-
for (const c of track.clips) if (c !== clip && Math.abs(c.start - videoClipEnd(clip)) <= VIDEO_TIME_EPSILON) return c;
|
|
1146
|
-
return null;
|
|
1147
|
-
}
|
|
1148
|
-
function videoClipHandles(clip, assets, sequences) {
|
|
1149
|
-
if (clip.type === "sequence") {
|
|
1150
|
-
const seq = sequences?.get(clip.sequence);
|
|
1151
|
-
const range2 = videoClipSourceRange(clip);
|
|
1152
|
-
return { before: range2.in, after: seq ? Math.max(0, videoSequenceDuration(seq) - range2.out) : Infinity };
|
|
1153
|
-
}
|
|
1154
|
-
if (clip.type !== "media") return { before: Infinity, after: Infinity };
|
|
1155
|
-
const asset = assets.get(clip.asset);
|
|
1156
|
-
const speed = videoClipSpeed(clip);
|
|
1157
|
-
const range = videoClipSourceRange(clip);
|
|
1158
|
-
const before = range.in / speed;
|
|
1159
|
-
if (!asset || asset.kind === "image" || asset.duration === void 0) return { before: asset?.kind === "image" ? Infinity : before, after: Infinity };
|
|
1160
|
-
return { before, after: Math.max(0, (asset.duration - range.out) / speed) };
|
|
1161
|
-
}
|
|
1162
|
-
function videoClipOverhang(track, clip) {
|
|
1163
|
-
const lead = clip.transition && clip.transition.type !== "dip" && videoClipBefore(track, clip) ? clip.transition.duration / 2 : 0;
|
|
1164
|
-
const next = videoClipAfter(track, clip);
|
|
1165
|
-
const tail = next?.transition && next.transition.type !== "dip" ? next.transition.duration / 2 : 0;
|
|
1166
|
-
return { lead, tail };
|
|
1167
|
-
}
|
|
1168
|
-
function videoSnapPoints(project, options = {}) {
|
|
1169
|
-
const exclude = new Set(options.exclude ?? []);
|
|
1170
|
-
const points = /* @__PURE__ */ new Set([0]);
|
|
1171
|
-
for (const track of project.tracks)
|
|
1172
|
-
for (const clip of track.clips) {
|
|
1173
|
-
if (exclude.has(clip.id)) continue;
|
|
1174
|
-
points.add(clip.start);
|
|
1175
|
-
points.add(videoClipEnd(clip));
|
|
1176
|
-
}
|
|
1177
|
-
for (const marker of project.markers ?? []) points.add(marker.time);
|
|
1178
|
-
if (options.playhead !== void 0) points.add(options.playhead);
|
|
1179
|
-
return [...points].sort((a, b) => a - b);
|
|
1180
|
-
}
|
|
1181
|
-
function snapVideoTime(time, points, threshold) {
|
|
1182
|
-
let best = null;
|
|
1183
|
-
for (const p of points) if (Math.abs(p - time) <= threshold && (best === null || Math.abs(p - time) < Math.abs(best - time))) best = p;
|
|
1184
|
-
return best === null ? { time, snapped: null } : { time: best, snapped: best };
|
|
1185
|
-
}
|
|
1186
|
-
function findVideoGaps(project, trackId) {
|
|
1187
|
-
const gaps = [];
|
|
1188
|
-
for (const track of project.tracks) {
|
|
1189
|
-
if (trackId !== void 0 && track.id !== trackId) continue;
|
|
1190
|
-
let at = 0;
|
|
1191
|
-
for (const clip of sortedVideoClips(track)) {
|
|
1192
|
-
if (clip.start - at > VIDEO_TIME_EPSILON) gaps.push({ track: track.id, start: at, end: clip.start });
|
|
1193
|
-
at = Math.max(at, videoClipEnd(clip));
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
|
-
return gaps;
|
|
1197
|
-
}
|
|
1198
|
-
function videoOverlap(track, start, end, ignore = /* @__PURE__ */ new Set()) {
|
|
1199
|
-
for (const c of sortedVideoClips(track)) {
|
|
1200
|
-
if (ignore.has(c.id)) continue;
|
|
1201
|
-
if (c.start < end - VIDEO_TIME_EPSILON && videoClipEnd(c) > start + VIDEO_TIME_EPSILON) return c;
|
|
1202
|
-
}
|
|
1203
|
-
return null;
|
|
1204
|
-
}
|
|
1205
|
-
function nextFreeVideoSlot(track, from, duration, ignore = /* @__PURE__ */ new Set()) {
|
|
1206
|
-
let at = from;
|
|
1207
|
-
for (const c of sortedVideoClips(track)) {
|
|
1208
|
-
if (ignore.has(c.id)) continue;
|
|
1209
|
-
if (videoClipEnd(c) <= at + VIDEO_TIME_EPSILON) continue;
|
|
1210
|
-
if (c.start >= at + duration - VIDEO_TIME_EPSILON) break;
|
|
1211
|
-
at = videoClipEnd(c);
|
|
1212
|
-
}
|
|
1213
|
-
return at;
|
|
1214
|
-
}
|
|
1215
|
-
function videoClipsAt(project, time) {
|
|
1216
|
-
const out = [];
|
|
1217
|
-
for (const track of project.tracks) for (const clip of track.clips) if (time >= clip.start && time < videoClipEnd(clip)) out.push({ track: track.id, clip });
|
|
1218
|
-
return out;
|
|
1219
|
-
}
|
|
1220
|
-
|
|
1221
|
-
// src/video-editor/clip-animation.ts
|
|
1222
|
-
var VIDEO_CLIP_ANIMATION_DURATION = 500;
|
|
1223
|
-
var EFFECTS = {
|
|
1224
|
-
fade: { easeIn: "linear", easeOut: "linear" },
|
|
1225
|
-
rise: { y: 0.06, easeIn: "ease-out", easeOut: "ease-in" },
|
|
1226
|
-
drop: { y: -0.06, easeIn: "ease-out", easeOut: "ease-in" },
|
|
1227
|
-
"slide-left": { x: 0.15, easeIn: "ease-out", easeOut: "ease-in" },
|
|
1228
|
-
"slide-right": { x: -0.15, easeIn: "ease-out", easeOut: "ease-in" },
|
|
1229
|
-
zoom: { scale: 0.6, easeIn: "ease-out", easeOut: "ease-in" },
|
|
1230
|
-
// Overshoots past full size, then settles (a "back" curve).
|
|
1231
|
-
pop: { scale: 0.3, easeIn: [0.34, 1.56, 0.64, 1], easeOut: [0.36, 0, 0.66, -0.56] }
|
|
1232
|
-
};
|
|
1233
|
-
var PROPS = ["opacity", "x", "y", "scale"];
|
|
1234
|
-
function videoClipAnimationProperties(animation) {
|
|
1235
|
-
const spec = EFFECTS[animation.effect];
|
|
1236
|
-
return PROPS.filter((p) => p === "opacity" || spec[p] !== void 0);
|
|
1237
|
-
}
|
|
1238
|
-
function videoClipAnimationSpans(clip) {
|
|
1239
|
-
const a = clip.animateIn ? clip.animateIn.duration ?? VIDEO_CLIP_ANIMATION_DURATION : 0;
|
|
1240
|
-
const b = clip.animateOut ? clip.animateOut.duration ?? VIDEO_CLIP_ANIMATION_DURATION : 0;
|
|
1241
|
-
const k = a + b > clip.duration ? clip.duration / (a + b) : 1;
|
|
1242
|
-
return { in: a * k, out: b * k };
|
|
1243
|
-
}
|
|
1244
|
-
function videoClipAnimated(clip, project) {
|
|
1245
|
-
if (!clip.animateIn && !clip.animateOut) return clip;
|
|
1246
|
-
const spans = videoClipAnimationSpans(clip);
|
|
1247
|
-
const out = { ...clip, transform: { ...clip.transform } };
|
|
1248
|
-
for (const prop of PROPS) {
|
|
1249
|
-
const current = prop === "opacity" ? clip.opacity : clip.transform?.[prop];
|
|
1250
|
-
if (isVideoKeyframes(current)) continue;
|
|
1251
|
-
const base = current ?? (prop === "opacity" || prop === "scale" ? 1 : 0);
|
|
1252
|
-
const away = (spec) => {
|
|
1253
|
-
if (prop === "opacity") return 0;
|
|
1254
|
-
const v = spec[prop];
|
|
1255
|
-
if (v === void 0) return void 0;
|
|
1256
|
-
return prop === "scale" ? base * v : base + v * (prop === "x" ? project.width : project.height);
|
|
1257
|
-
};
|
|
1258
|
-
const keys = [];
|
|
1259
|
-
if (clip.animateIn) {
|
|
1260
|
-
const spec = EFFECTS[clip.animateIn.effect];
|
|
1261
|
-
const from = away(spec);
|
|
1262
|
-
if (from !== void 0) keys.push({ t: 0, v: from, ease: spec.easeIn }, { t: spans.in, v: base });
|
|
1263
|
-
}
|
|
1264
|
-
if (clip.animateOut) {
|
|
1265
|
-
const spec = EFFECTS[clip.animateOut.effect];
|
|
1266
|
-
const to = away(spec);
|
|
1267
|
-
if (to !== void 0) {
|
|
1268
|
-
const start = clip.duration - spans.out;
|
|
1269
|
-
if (!keys.length || keys[keys.length - 1].t < start) keys.push({ t: start, v: base, ease: spec.easeOut });
|
|
1270
|
-
else keys[keys.length - 1] = { ...keys[keys.length - 1], ease: spec.easeOut };
|
|
1271
|
-
keys.push({ t: clip.duration, v: to });
|
|
1272
|
-
}
|
|
1273
|
-
}
|
|
1274
|
-
if (!keys.length) continue;
|
|
1275
|
-
if (prop === "opacity") out.opacity = { keys };
|
|
1276
|
-
else out.transform[prop] = { keys };
|
|
1277
|
-
}
|
|
1278
|
-
return out;
|
|
1279
|
-
}
|
|
1280
|
-
|
|
1281
449
|
// src/video-editor/check.ts
|
|
1282
450
|
function videoClipKeyframes(clip) {
|
|
1283
451
|
const out = [
|
|
@@ -1455,7 +623,7 @@ var refuse = (message) => {
|
|
|
1455
623
|
};
|
|
1456
624
|
function freshId(prefix, taken) {
|
|
1457
625
|
const used = new Set(taken);
|
|
1458
|
-
for (let
|
|
626
|
+
for (let n = 1; ; n++) if (!used.has(`${prefix}${n}`)) return `${prefix}${n}`;
|
|
1459
627
|
}
|
|
1460
628
|
var slug = (s) => s.replace(/^.*[\\/]/, "").replace(/\.[a-z0-9]+(\?.*)?$/i, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 32);
|
|
1461
629
|
function mergeSpans(spans) {
|
|
@@ -1736,8 +904,8 @@ var Editor = class {
|
|
|
1736
904
|
const ids = this.p.tracks.map((t) => t.id);
|
|
1737
905
|
if (e.track.id && ids.includes(e.track.id)) refuse(`There is already a track "${e.track.id}".`);
|
|
1738
906
|
const id = e.track.id ?? freshId(e.track.kind === "video" ? "v" : "a", ids);
|
|
1739
|
-
const
|
|
1740
|
-
const track = { name: `${e.track.kind === "video" ? "Video" : "Audio"} ${
|
|
907
|
+
const n = this.p.tracks.filter((t) => t.kind === e.track.kind).length + 1;
|
|
908
|
+
const track = { name: `${e.track.kind === "video" ? "Video" : "Audio"} ${n}`, ...e.track, id, clips: [] };
|
|
1741
909
|
const at = e.at ?? (e.track.kind === "video" ? 0 : this.p.tracks.length);
|
|
1742
910
|
if (at > this.p.tracks.length) refuse(`"at" is ${at} but there are only ${this.p.tracks.length} tracks; use 0\u2013${this.p.tracks.length}.`);
|
|
1743
911
|
this.p.tracks.splice(at, 0, track);
|
|
@@ -2188,169 +1356,11 @@ function applyVideoEdits(project, edits) {
|
|
|
2188
1356
|
return { ok: true, project: next, issues, outcomes: editor.outcomes };
|
|
2189
1357
|
}
|
|
2190
1358
|
|
|
2191
|
-
// src/video-editor/evaluate.ts
|
|
2192
|
-
function videoTransformAt(transform, clipTime = 0) {
|
|
2193
|
-
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) };
|
|
2194
|
-
}
|
|
2195
|
-
function placeVideoRect(frame, size, fit = "contain", transform = {}, clipTime = 0) {
|
|
2196
|
-
let w = size.width;
|
|
2197
|
-
let h = size.height;
|
|
2198
|
-
if (fit === "fill") {
|
|
2199
|
-
w = frame.width;
|
|
2200
|
-
h = frame.height;
|
|
2201
|
-
} else if (fit !== "none") {
|
|
2202
|
-
const k = fit === "cover" ? Math.max(frame.width / w, frame.height / h) : Math.min(frame.width / w, frame.height / h);
|
|
2203
|
-
w *= k;
|
|
2204
|
-
h *= k;
|
|
2205
|
-
}
|
|
2206
|
-
const t = videoTransformAt(transform, clipTime);
|
|
2207
|
-
w *= t.scale;
|
|
2208
|
-
h *= t.scale;
|
|
2209
|
-
const cx = frame.width / 2 + t.x;
|
|
2210
|
-
const cy = frame.height / 2 + t.y;
|
|
2211
|
-
return { x: cx - w / 2, y: cy - h / 2, width: w, height: h };
|
|
2212
|
-
}
|
|
2213
|
-
function videoTextStyle(clip) {
|
|
2214
|
-
return { text: clip.text, color: clip.color ?? "#ffffff", fontSize: clip.fontSize ?? 64, fontFamily: clip.fontFamily ?? "sans-serif", fontWeight: clip.fontWeight ?? 700, ...clip.background ? { background: clip.background } : {} };
|
|
2215
|
-
}
|
|
2216
|
-
function videoTitleFrame(project, clip, clipTime = 0) {
|
|
2217
|
-
const layer = { clip: clip.id, track: "", type: "text", opacity: 1, rect: placeVideoRect(project, project, "fill", clip.transform, clipTime), rotation: videoTransformAt(clip.transform, clipTime).rotation, text: videoTextStyle(clip) };
|
|
2218
|
-
return { time: clip.start, width: project.width, height: project.height, background: "rgba(0,0,0,0)", layers: [layer], audio: [] };
|
|
2219
|
-
}
|
|
2220
|
-
var clamp012 = (v) => Math.min(1, Math.max(0, v));
|
|
2221
|
-
var videoTransitionEase = (progress) => (1 - Math.cos(Math.PI * clamp012(progress))) / 2;
|
|
2222
|
-
function videoFadeCurveGain(curve, x) {
|
|
2223
|
-
const t = clamp012(x);
|
|
2224
|
-
switch (curve) {
|
|
2225
|
-
case "equal-power":
|
|
2226
|
-
return Math.sin(t * Math.PI / 2);
|
|
2227
|
-
case "s-curve":
|
|
2228
|
-
return (1 - Math.cos(t * Math.PI)) / 2;
|
|
2229
|
-
case "exponential":
|
|
2230
|
-
return t * t * t;
|
|
2231
|
-
case "logarithmic":
|
|
2232
|
-
return 1 - (1 - t) * (1 - t);
|
|
2233
|
-
default:
|
|
2234
|
-
return t;
|
|
2235
|
-
}
|
|
2236
|
-
}
|
|
2237
|
-
function fadeGain(clip, time, sound = false) {
|
|
2238
|
-
let g = 1;
|
|
2239
|
-
const shape = sound && (clip.type === "media" || clip.type === "sequence") ? clip : void 0;
|
|
2240
|
-
if (clip.fadeIn) g *= videoFadeCurveGain(shape?.fadeInCurve, (time - clip.start) / clip.fadeIn);
|
|
2241
|
-
if (clip.fadeOut) g *= videoFadeCurveGain(shape?.fadeOutCurve, (videoClipEnd(clip) - time) / clip.fadeOut);
|
|
2242
|
-
return g;
|
|
2243
|
-
}
|
|
2244
|
-
function videoClipSoundGain(clip, clipTime) {
|
|
2245
|
-
const volume = clip.type === "media" || clip.type === "sequence" ? videoValueAt(clip.volume, clipTime, 1) : 1;
|
|
2246
|
-
return volume * fadeGain(clip, clip.start + Math.min(Math.max(clipTime, 0), clip.duration), true);
|
|
2247
|
-
}
|
|
2248
|
-
function activeOnTrack(track, time) {
|
|
2249
|
-
const out = [];
|
|
2250
|
-
for (const clip of sortedVideoClips(track)) {
|
|
2251
|
-
const before = clip.transition ? videoClipBefore(track, clip) : null;
|
|
2252
|
-
const lead = before ? clip.transition.duration / 2 : 0;
|
|
2253
|
-
const next = videoClipAfter(track, clip);
|
|
2254
|
-
const tail = next?.transition ? next.transition.duration / 2 : 0;
|
|
2255
|
-
if (time < clip.start - lead || time >= videoClipEnd(clip) + tail) continue;
|
|
2256
|
-
const a = { clip };
|
|
2257
|
-
if (lead && time < clip.start + lead) a.incoming = { progress: clamp012((time - (clip.start - lead)) / (2 * lead)), type: clip.transition.type, transition: clip.transition };
|
|
2258
|
-
if (tail && time >= videoClipEnd(clip) - tail) a.outgoing = { progress: clamp012((time - (videoClipEnd(clip) - tail)) / (2 * tail)), type: next.transition.type, next };
|
|
2259
|
-
out.push(a);
|
|
2260
|
-
}
|
|
2261
|
-
return out;
|
|
2262
|
-
}
|
|
2263
|
-
var sourceTimeAt = (clip, time) => clip.type === "media" ? videoClipSourceTime(clip, time - clip.start) : 0;
|
|
2264
|
-
function videoFrameAt(project, time) {
|
|
2265
|
-
const { layers, audio } = tracksAt(project, project.tracks, time, { assets: videoAssetMap(project), sequences: videoSequenceMap(project), within: [], prefix: "" });
|
|
2266
|
-
return { time, width: project.width, height: project.height, background: project.background ?? "#000000", layers, audio };
|
|
2267
|
-
}
|
|
2268
|
-
function tracksAt(project, tracks, time, scope) {
|
|
2269
|
-
const { assets, prefix } = scope;
|
|
2270
|
-
const layers = [];
|
|
2271
|
-
const audio = [];
|
|
2272
|
-
for (let ti = tracks.length - 1; ti >= 0; ti--) {
|
|
2273
|
-
const track = tracks[ti];
|
|
2274
|
-
for (const { clip, incoming, outgoing } of activeOnTrack(track, time)) {
|
|
2275
|
-
if (incoming?.type === "dip" && incoming.progress < 0.5) continue;
|
|
2276
|
-
if (outgoing?.type === "dip" && outgoing.progress >= 0.5) continue;
|
|
2277
|
-
const inside = Math.min(Math.max(time, clip.start), videoClipEnd(clip));
|
|
2278
|
-
const fade = fadeGain(clip, inside);
|
|
2279
|
-
const level = (t, transition, into) => videoFadeCurveGain(videoTransitionAudioCurve(transition), t.type === "dip" ? Math.abs(1 - 2 * t.progress) : into ? t.progress : 1 - t.progress);
|
|
2280
|
-
const mix = (incoming ? level(incoming, incoming.transition, true) : 1) * (outgoing ? level(outgoing, outgoing.next.transition, false) : 1);
|
|
2281
|
-
const asset = clip.type === "media" ? assets.get(clip.asset) : void 0;
|
|
2282
|
-
const sourceTime = clip.type === "sequence" ? (clip.in ?? 0) + time - clip.start : sourceTimeAt(clip, time);
|
|
2283
|
-
const clipTime = time - clip.start;
|
|
2284
|
-
let nested = null;
|
|
2285
|
-
if (clip.type === "sequence") {
|
|
2286
|
-
const seq = scope.sequences.get(clip.sequence);
|
|
2287
|
-
if (!seq || scope.within.includes(seq.id)) continue;
|
|
2288
|
-
nested = tracksAt(project, seq.tracks, sourceTime, { ...scope, within: [...scope.within, seq.id], prefix: `${prefix}${clip.id}/` });
|
|
2289
|
-
}
|
|
2290
|
-
const audible = videoAssetHasSound(asset);
|
|
2291
|
-
if (audible && clip.type === "media" && !clip.muted && !track.muted) {
|
|
2292
|
-
const gain = videoValueAt(clip.volume, clipTime, 1) * (track.volume ?? 1) * fadeGain(clip, inside, true) * mix;
|
|
2293
|
-
audio.push({ clip: prefix + clip.id, track: track.id, asset: asset.id, src: asset.src, sourceTime, gain, speed: videoClipSpeedAt(clip, clipTime) });
|
|
2294
|
-
}
|
|
2295
|
-
if (nested && clip.type === "sequence" && !clip.muted && !track.muted) {
|
|
2296
|
-
const gain = videoValueAt(clip.volume, clipTime, 1) * (track.volume ?? 1) * fadeGain(clip, inside, true) * mix;
|
|
2297
|
-
for (const a of nested.audio) audio.push({ ...a, gain: a.gain * gain });
|
|
2298
|
-
}
|
|
2299
|
-
if (track.kind !== "video" || track.hidden) continue;
|
|
2300
|
-
if (clip.type === "media" && (!asset || asset.kind === "audio")) continue;
|
|
2301
|
-
if (nested && !nested.layers.length) continue;
|
|
2302
|
-
const shown = videoClipAnimated(clip, project);
|
|
2303
|
-
let opacity = videoValueAt(shown.opacity, clipTime, 1) * fade;
|
|
2304
|
-
let wipe;
|
|
2305
|
-
let dip = null;
|
|
2306
|
-
let iris;
|
|
2307
|
-
let shift = 0;
|
|
2308
|
-
let shiftDirection = "right";
|
|
2309
|
-
if (incoming) {
|
|
2310
|
-
if (incoming.type === "crossfade") opacity *= incoming.progress;
|
|
2311
|
-
else if (incoming.type === "wipe") wipe = { direction: incoming.transition.direction ?? "right", progress: incoming.progress };
|
|
2312
|
-
else if (incoming.type === "iris") iris = { progress: incoming.progress };
|
|
2313
|
-
else if (incoming.type === "push" || incoming.type === "slide") {
|
|
2314
|
-
shiftDirection = incoming.transition.direction ?? "right";
|
|
2315
|
-
shift = videoTransitionEase(incoming.progress) - 1;
|
|
2316
|
-
} else dip = { color: incoming.transition.color ?? "#000000", opacity: (1 - incoming.progress) * 2 };
|
|
2317
|
-
}
|
|
2318
|
-
if (outgoing?.type === "dip") dip = { color: outgoing.next.transition.color ?? "#000000", opacity: outgoing.progress * 2 };
|
|
2319
|
-
if (outgoing?.type === "push") {
|
|
2320
|
-
shiftDirection = outgoing.next.transition.direction ?? "right";
|
|
2321
|
-
shift = videoTransitionEase(outgoing.progress);
|
|
2322
|
-
}
|
|
2323
|
-
const size = clip.type === "media" ? { width: asset.width ?? project.width, height: asset.height ?? project.height } : { width: project.width, height: project.height };
|
|
2324
|
-
const fit = clip.fit ?? (clip.type === "media" || clip.type === "sequence" ? "contain" : "fill");
|
|
2325
|
-
const placed = placeVideoRect(project, size, clip.type === "text" ? "fill" : fit, shown.transform, clipTime);
|
|
2326
|
-
const across = shiftDirection === "left" || shiftDirection === "right";
|
|
2327
|
-
const sign = shiftDirection === "left" || shiftDirection === "up" ? -1 : 1;
|
|
2328
|
-
const rect = shift ? { ...placed, ...across ? { x: placed.x + sign * shift * project.width } : { y: placed.y + sign * shift * project.height } } : placed;
|
|
2329
|
-
const layer = { clip: prefix + clip.id, track: track.id, type: clip.type, opacity: clamp012(opacity), rect, rotation: videoValueAt(shown.transform?.rotation, clipTime, 0) };
|
|
2330
|
-
if (wipe) layer.wipe = wipe;
|
|
2331
|
-
if (iris) layer.iris = iris;
|
|
2332
|
-
if (clip.blend && clip.blend !== "normal") layer.blend = clip.blend;
|
|
2333
|
-
const grade = normalizeVideoGrade(clip.grade);
|
|
2334
|
-
if (grade) layer.grade = grade;
|
|
2335
|
-
if (clip.type === "media") Object.assign(layer, { asset: asset.id, assetKind: asset.kind, src: asset.src, sourceTime });
|
|
2336
|
-
if (clip.type === "sequence") Object.assign(layer, { sequence: clip.sequence, sourceTime, layers: nested.layers });
|
|
2337
|
-
if (clip.type === "text") layer.text = videoTextStyle(clip);
|
|
2338
|
-
if (clip.type === "color") layer.color = clip.color;
|
|
2339
|
-
if (layer.opacity > 0) layers.push(layer);
|
|
2340
|
-
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 });
|
|
2341
|
-
}
|
|
2342
|
-
}
|
|
2343
|
-
return { layers, audio };
|
|
2344
|
-
}
|
|
2345
|
-
function videoFrameLeafLayers(layers) {
|
|
2346
|
-
return layers.flatMap((l) => l.layers ? videoFrameLeafLayers(l.layers) : [l]);
|
|
2347
|
-
}
|
|
2348
|
-
|
|
2349
1359
|
// src/video-editor/audio.ts
|
|
2350
1360
|
var VIDEO_PEAK_BLOCK = 32;
|
|
2351
1361
|
function buildVideoAudioPeaks(channels, sampleRate, blockSize = VIDEO_PEAK_BLOCK) {
|
|
2352
1362
|
if (!(sampleRate > 0)) throw new Error("The sample rate must be above 0.");
|
|
2353
|
-
const length = channels.reduce((
|
|
1363
|
+
const length = channels.reduce((n, c) => Math.max(n, c.length), 0);
|
|
2354
1364
|
const blocks = Math.max(1, Math.ceil(length / blockSize));
|
|
2355
1365
|
const level0 = new Float32Array(blocks * 2);
|
|
2356
1366
|
for (let b = 0; b < blocks; b++) {
|
|
@@ -2371,12 +1381,12 @@ function buildVideoAudioPeaks(channels, sampleRate, blockSize = VIDEO_PEAK_BLOCK
|
|
|
2371
1381
|
const levels = [level0];
|
|
2372
1382
|
while (levels[levels.length - 1].length > 2) {
|
|
2373
1383
|
const below = levels[levels.length - 1];
|
|
2374
|
-
const
|
|
2375
|
-
const up = new Float32Array(Math.ceil(
|
|
1384
|
+
const n = below.length / 2;
|
|
1385
|
+
const up = new Float32Array(Math.ceil(n / 4) * 2);
|
|
2376
1386
|
for (let j = 0; j < up.length / 2; j++) {
|
|
2377
1387
|
let lo = Infinity;
|
|
2378
1388
|
let hi = -Infinity;
|
|
2379
|
-
for (let i = 4 * j; i < Math.min(
|
|
1389
|
+
for (let i = 4 * j; i < Math.min(n, 4 * j + 4); i++) {
|
|
2380
1390
|
lo = Math.min(lo, below[2 * i]);
|
|
2381
1391
|
hi = Math.max(hi, below[2 * i + 1]);
|
|
2382
1392
|
}
|
|
@@ -2417,10 +1427,10 @@ function videoAudioPeakRange(peaks, from, to) {
|
|
|
2417
1427
|
return min === Infinity ? { min: 0, max: 0 } : { min, max };
|
|
2418
1428
|
}
|
|
2419
1429
|
function videoAudioPeakSpans(peaks, from, to, columns) {
|
|
2420
|
-
const
|
|
2421
|
-
const out = new Float32Array(
|
|
2422
|
-
const step = (to - from) / Math.max(1,
|
|
2423
|
-
for (let c = 0; c <
|
|
1430
|
+
const n = Math.max(0, Math.floor(columns));
|
|
1431
|
+
const out = new Float32Array(n * 2);
|
|
1432
|
+
const step = (to - from) / Math.max(1, n);
|
|
1433
|
+
for (let c = 0; c < n; c++) {
|
|
2424
1434
|
const { min, max } = videoAudioPeakRange(peaks, from + c * step, from + (c + 1) * step);
|
|
2425
1435
|
out[2 * c] = min;
|
|
2426
1436
|
out[2 * c + 1] = max;
|
|
@@ -2428,12 +1438,12 @@ function videoAudioPeakSpans(peaks, from, to, columns) {
|
|
|
2428
1438
|
return out;
|
|
2429
1439
|
}
|
|
2430
1440
|
function videoClipWaveform(clip, peaks, columns) {
|
|
2431
|
-
const
|
|
2432
|
-
const out = new Float32Array(
|
|
1441
|
+
const n = Math.max(0, Math.floor(columns));
|
|
1442
|
+
const out = new Float32Array(n * 2);
|
|
2433
1443
|
const speed = clip.speed ?? 1;
|
|
2434
1444
|
const start = clip.in ?? 0;
|
|
2435
|
-
const step = clip.duration / Math.max(1,
|
|
2436
|
-
for (let c = 0; c <
|
|
1445
|
+
const step = clip.duration / Math.max(1, n);
|
|
1446
|
+
for (let c = 0; c < n; c++) {
|
|
2437
1447
|
const { min, max } = videoAudioPeakRange(peaks, start + c * step * speed, start + (c + 1) * step * speed);
|
|
2438
1448
|
const gain = videoClipSoundGain(clip, (c + 0.5) * step);
|
|
2439
1449
|
out[2 * c] = min * gain;
|
|
@@ -2442,16 +1452,16 @@ function videoClipWaveform(clip, peaks, columns) {
|
|
|
2442
1452
|
return out;
|
|
2443
1453
|
}
|
|
2444
1454
|
function videoWaveformPath(spans, width, height) {
|
|
2445
|
-
const
|
|
2446
|
-
if (!
|
|
1455
|
+
const n = spans.length / 2;
|
|
1456
|
+
if (!n || width <= 0 || height <= 0) return "";
|
|
2447
1457
|
const mid = height / 2;
|
|
2448
1458
|
const y = (v) => (mid - Math.max(-1, Math.min(1, v)) * mid).toFixed(1);
|
|
2449
|
-
const x = (i) => ((i + 0.5) * (width /
|
|
1459
|
+
const x = (i) => ((i + 0.5) * (width / n)).toFixed(1);
|
|
2450
1460
|
const floor = 0.5 / mid;
|
|
2451
1461
|
const top = [];
|
|
2452
1462
|
const bottom = [];
|
|
2453
|
-
for (let i = 0; i <
|
|
2454
|
-
for (let i =
|
|
1463
|
+
for (let i = 0; i < n; i++) top.push(`${i ? "L" : "M"}${x(i)} ${y(Math.max(spans[2 * i + 1], floor))}`);
|
|
1464
|
+
for (let i = n - 1; i >= 0; i--) bottom.push(`L${x(i)} ${y(Math.min(spans[2 * i], -floor))}`);
|
|
2455
1465
|
return `${top.join("")}${bottom.join("")}Z`;
|
|
2456
1466
|
}
|
|
2457
1467
|
function videoClipWaveformPath(clip, peaks, width, height) {
|
|
@@ -2708,7 +1718,7 @@ function summarizeVideoScopes(pixels, options = {}) {
|
|
|
2708
1718
|
const w = videoLumaWaveform(pixels, { ...options, columns, levels: 256 });
|
|
2709
1719
|
const waveform = Array.from({ length: w.columns }, (_, c) => {
|
|
2710
1720
|
const counts = w.data.subarray(c * 256, c * 256 + 256);
|
|
2711
|
-
const
|
|
1721
|
+
const n = counts.reduce((a, b) => a + b, 0);
|
|
2712
1722
|
let lo = 0;
|
|
2713
1723
|
while (lo < 255 && !counts[lo]) lo++;
|
|
2714
1724
|
let hi = 255;
|
|
@@ -2717,7 +1727,7 @@ function summarizeVideoScopes(pixels, options = {}) {
|
|
|
2717
1727
|
let median = 0;
|
|
2718
1728
|
for (let i = 0; i < 256; i++) {
|
|
2719
1729
|
seen += counts[i];
|
|
2720
|
-
if (seen >=
|
|
1730
|
+
if (seen >= n / 2 && n) {
|
|
2721
1731
|
median = i;
|
|
2722
1732
|
break;
|
|
2723
1733
|
}
|
|
@@ -3027,7 +2037,7 @@ function videoTimelineTranscript(project, options = {}) {
|
|
|
3027
2037
|
return out;
|
|
3028
2038
|
}
|
|
3029
2039
|
var normalizeVideoWord = (word) => word.toLowerCase().replace(/^[^\p{L}\p{N}']+|[^\p{L}\p{N}']+$/gu, "");
|
|
3030
|
-
var rangeOf = (words, i,
|
|
2040
|
+
var rangeOf = (words, i, n) => ({ from: words[i].start, to: words[i + n - 1].end, text: words.slice(i, i + n).map((w) => w.word).join(" ") });
|
|
3031
2041
|
function findVideoTranscriptText(words, query) {
|
|
3032
2042
|
const q = query.split(/\s+/).map(normalizeVideoWord).filter(Boolean);
|
|
3033
2043
|
if (!q.length) throw new Error("Search for at least one word.");
|
|
@@ -3101,679 +2111,6 @@ function videoWordAt(words, time) {
|
|
|
3101
2111
|
return -1;
|
|
3102
2112
|
}
|
|
3103
2113
|
|
|
3104
|
-
// src/video-editor/render.ts
|
|
3105
|
-
var esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
3106
|
-
var n = (v) => String(Math.round(v * 100) / 100);
|
|
3107
|
-
var n4 = (v) => String(Math.round(v * 1e4) / 1e4);
|
|
3108
|
-
var videoIrisRadius = (frame, progress) => progress * Math.hypot(frame.width / 2, frame.height / 2);
|
|
3109
|
-
function videoWipeRect(rect, wipe) {
|
|
3110
|
-
const { direction, progress } = wipe;
|
|
3111
|
-
const w = rect.width * progress;
|
|
3112
|
-
const h = rect.height * progress;
|
|
3113
|
-
if (direction === "right") return { ...rect, width: w };
|
|
3114
|
-
if (direction === "left") return { ...rect, x: rect.x + rect.width - w, width: w };
|
|
3115
|
-
if (direction === "down") return { ...rect, height: h };
|
|
3116
|
-
return { ...rect, y: rect.y + rect.height - h, height: h };
|
|
3117
|
-
}
|
|
3118
|
-
function videoFrameToSvg(frame, options = {}, assets = []) {
|
|
3119
|
-
const defs = [];
|
|
3120
|
-
const posters = new Map(assets.map((a) => [a.id, a.poster]));
|
|
3121
|
-
const draw = (layers, prefix) => layers.map((layer, index) => {
|
|
3122
|
-
const i = `${prefix}${index}`;
|
|
3123
|
-
const { rect } = layer;
|
|
3124
|
-
const attrs = [];
|
|
3125
|
-
const style = [];
|
|
3126
|
-
if (layer.opacity < 1) attrs.push(`opacity="${n(layer.opacity)}"`);
|
|
3127
|
-
if (layer.blend) style.push(`mix-blend-mode:${layer.blend === "add" ? "plus-lighter" : layer.blend}`);
|
|
3128
|
-
if (layer.layers) style.push("isolation:isolate");
|
|
3129
|
-
if (style.length) attrs.push(`style="${style.join(";")}"`);
|
|
3130
|
-
if (layer.grade) {
|
|
3131
|
-
defs.push(videoGradeSvgFilter(`g${i}`, layer.grade, layer.grade.lut ? options.lutFor?.(layer.grade.lut.src) : null));
|
|
3132
|
-
attrs.push(`filter="url(#g${i})"`);
|
|
3133
|
-
}
|
|
3134
|
-
if (layer.rotation) attrs.push(`transform="rotate(${n(layer.rotation)} ${n(rect.x + rect.width / 2)} ${n(rect.y + rect.height / 2)})"`);
|
|
3135
|
-
if (layer.wipe) {
|
|
3136
|
-
const r = videoWipeRect(rect, layer.wipe);
|
|
3137
|
-
defs.push(`<clipPath id="w${i}"><rect x="${n(r.x)}" y="${n(r.y)}" width="${n(r.width)}" height="${n(r.height)}"/></clipPath>`);
|
|
3138
|
-
attrs.push(`clip-path="url(#w${i})"`);
|
|
3139
|
-
}
|
|
3140
|
-
const box = `x="${n(rect.x)}" y="${n(rect.y)}" width="${n(rect.width)}" height="${n(rect.height)}"`;
|
|
3141
|
-
let inner = "";
|
|
3142
|
-
if (layer.layers) {
|
|
3143
|
-
inner = `<g transform="translate(${n(rect.x)} ${n(rect.y)}) scale(${n4(rect.width / frame.width)} ${n4(rect.height / frame.height)})">${draw(layer.layers, `${i}-`)}</g>`;
|
|
3144
|
-
} else if (layer.type === "color") inner = `<rect ${box} fill="${esc(layer.color ?? "#000000")}"/>`;
|
|
3145
|
-
else if (layer.type === "text" && layer.text) {
|
|
3146
|
-
const t = layer.text;
|
|
3147
|
-
const lines = t.text.split("\n");
|
|
3148
|
-
const lh = t.fontSize * 1.2;
|
|
3149
|
-
const cx = rect.x + rect.width / 2;
|
|
3150
|
-
const cy = rect.y + rect.height / 2;
|
|
3151
|
-
const top = cy - lh * (lines.length - 1) / 2;
|
|
3152
|
-
if (t.background) {
|
|
3153
|
-
const w = Math.max(...lines.map((l) => l.length)) * t.fontSize * 0.6 + t.fontSize;
|
|
3154
|
-
const h = lh * lines.length + t.fontSize * 0.4;
|
|
3155
|
-
inner += `<rect x="${n(cx - w / 2)}" y="${n(cy - h / 2)}" width="${n(w)}" height="${n(h)}" fill="${esc(t.background)}"/>`;
|
|
3156
|
-
}
|
|
3157
|
-
inner += `<text x="${n(cx)}" text-anchor="middle" dominant-baseline="central" font-family="${esc(t.fontFamily)}" font-size="${n(t.fontSize)}" font-weight="${t.fontWeight}" fill="${esc(t.color)}">${lines.map((l, k) => `<tspan x="${n(cx)}" y="${n(top + k * lh)}">${esc(l)}</tspan>`).join("")}</text>`;
|
|
3158
|
-
} else {
|
|
3159
|
-
const href = options.imageFor ? options.imageFor(layer, frame) : layer.assetKind === "image" ? layer.src ?? null : posters.get(layer.asset ?? "") ?? null;
|
|
3160
|
-
if (href) inner = `<image href="${esc(href)}" ${box} preserveAspectRatio="none"/>`;
|
|
3161
|
-
else {
|
|
3162
|
-
const label = `${layer.asset ?? layer.clip} \xB7 ${formatVideoTimecode(layer.sourceTime ?? 0, options.fps ?? 30)}`;
|
|
3163
|
-
const size = Math.max(10, Math.min(rect.height / 8, rect.width / Math.max(8, label.length * 0.6)));
|
|
3164
|
-
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>`;
|
|
3165
|
-
}
|
|
3166
|
-
}
|
|
3167
|
-
let item = attrs.length ? `<g ${attrs.join(" ")}>${inner}</g>` : inner;
|
|
3168
|
-
if (layer.iris) {
|
|
3169
|
-
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>`);
|
|
3170
|
-
item = `<g clip-path="url(#i${i})">${item}</g>`;
|
|
3171
|
-
}
|
|
3172
|
-
return item;
|
|
3173
|
-
}).join("");
|
|
3174
|
-
const body = draw(frame.layers, "");
|
|
3175
|
-
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${n(frame.width)} ${n(frame.height)}" width="${n(frame.width)}" height="${n(frame.height)}">${defs.length ? `<defs>${defs.join("")}</defs>` : ""}<rect width="100%" height="100%" fill="${esc(frame.background)}"/>${body}</svg>`;
|
|
3176
|
-
}
|
|
3177
|
-
function videoProjectSvg(project, time, options = {}) {
|
|
3178
|
-
return videoFrameToSvg(videoFrameAt(project, time), { fps: project.fps, ...options }, project.assets);
|
|
3179
|
-
}
|
|
3180
|
-
function videoContactSheetSvg(project, times, options = {}) {
|
|
3181
|
-
const columns = options.columns ?? Math.ceil(Math.sqrt(times.length));
|
|
3182
|
-
const w = options.width ?? 320;
|
|
3183
|
-
const h = w * project.height / project.width;
|
|
3184
|
-
const label = 18;
|
|
3185
|
-
const gap = 8;
|
|
3186
|
-
const rows = Math.ceil(times.length / columns);
|
|
3187
|
-
const W = columns * w + (columns + 1) * gap;
|
|
3188
|
-
const H = rows * (h + label) + (rows + 1) * gap;
|
|
3189
|
-
const cells = times.map((t, i) => {
|
|
3190
|
-
const x = gap + i % columns * (w + gap);
|
|
3191
|
-
const y = gap + Math.floor(i / columns) * (h + label + gap);
|
|
3192
|
-
const svg = videoProjectSvg(project, t, options).replace(/^<svg xmlns="http:\/\/www.w3.org\/2000\/svg"/, `<svg x="${n(x)}" y="${n(y)}" width="${n(w)}" height="${n(h)}"`).replace(/ width="[\d.]+" height="[\d.]+">/, ">");
|
|
3193
|
-
return `${svg}<text x="${n(x)}" y="${n(y + h + 14)}" font-family="sans-serif" font-size="12" fill="#0f172a">${(t / 1e3).toFixed(2)} s \xB7 ${formatVideoTimecode(t, project.fps)}</text>`;
|
|
3194
|
-
});
|
|
3195
|
-
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>`;
|
|
3196
|
-
}
|
|
3197
|
-
function paintVideoLayer(ctx, frame, layer, sourceFor, options) {
|
|
3198
|
-
const { rect } = layer;
|
|
3199
|
-
if (layer.iris) {
|
|
3200
|
-
ctx.beginPath();
|
|
3201
|
-
ctx.arc(frame.width / 2, frame.height / 2, videoIrisRadius(frame, layer.iris.progress), 0, 2 * Math.PI);
|
|
3202
|
-
ctx.clip();
|
|
3203
|
-
}
|
|
3204
|
-
if (layer.grade) {
|
|
3205
|
-
const defs = videoGradeSvgDefs(layer.grade);
|
|
3206
|
-
if (options.defineSvgFilter) for (const d of defs) options.defineSvgFilter(d.id, d.markup);
|
|
3207
|
-
const css = videoGradeCssFilter(layer.grade);
|
|
3208
|
-
ctx.filter = options.defineSvgFilter ? css || "none" : css.replace(/\s*url\([^)]*\)/g, "").trim() || "none";
|
|
3209
|
-
}
|
|
3210
|
-
if (layer.rotation) {
|
|
3211
|
-
ctx.translate(rect.x + rect.width / 2, rect.y + rect.height / 2);
|
|
3212
|
-
ctx.rotate(layer.rotation * Math.PI / 180);
|
|
3213
|
-
ctx.translate(-(rect.x + rect.width / 2), -(rect.y + rect.height / 2));
|
|
3214
|
-
}
|
|
3215
|
-
if (layer.wipe) {
|
|
3216
|
-
const r = videoWipeRect(rect, layer.wipe);
|
|
3217
|
-
ctx.beginPath();
|
|
3218
|
-
ctx.rect(r.x, r.y, r.width, r.height);
|
|
3219
|
-
ctx.clip();
|
|
3220
|
-
}
|
|
3221
|
-
if (layer.type === "color") {
|
|
3222
|
-
ctx.fillStyle = layer.color ?? "#000000";
|
|
3223
|
-
ctx.fillRect(rect.x, rect.y, rect.width, rect.height);
|
|
3224
|
-
} else if (layer.type === "text" && layer.text) {
|
|
3225
|
-
const t = layer.text;
|
|
3226
|
-
const lines = t.text.split("\n");
|
|
3227
|
-
const lh = t.fontSize * 1.2;
|
|
3228
|
-
const cx = rect.x + rect.width / 2;
|
|
3229
|
-
const top = rect.y + rect.height / 2 - lh * (lines.length - 1) / 2;
|
|
3230
|
-
ctx.font = `${t.fontWeight} ${t.fontSize}px ${t.fontFamily}`;
|
|
3231
|
-
ctx.textAlign = "center";
|
|
3232
|
-
ctx.textBaseline = "middle";
|
|
3233
|
-
if (t.background) {
|
|
3234
|
-
const w = Math.max(...lines.map((l) => ctx.measureText(l).width)) + t.fontSize;
|
|
3235
|
-
const h = lh * lines.length + t.fontSize * 0.4;
|
|
3236
|
-
ctx.fillStyle = t.background;
|
|
3237
|
-
ctx.fillRect(cx - w / 2, rect.y + rect.height / 2 - h / 2, w, h);
|
|
3238
|
-
}
|
|
3239
|
-
ctx.fillStyle = t.color;
|
|
3240
|
-
lines.forEach((l, k) => ctx.fillText(l, cx, top + k * lh));
|
|
3241
|
-
} else {
|
|
3242
|
-
const source = sourceFor(layer);
|
|
3243
|
-
if (source) ctx.drawImage(source, rect.x, rect.y, rect.width, rect.height);
|
|
3244
|
-
else {
|
|
3245
|
-
ctx.fillStyle = "#334155";
|
|
3246
|
-
ctx.fillRect(rect.x, rect.y, rect.width, rect.height);
|
|
3247
|
-
ctx.strokeStyle = "#94a3b8";
|
|
3248
|
-
ctx.lineWidth = 2;
|
|
3249
|
-
ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);
|
|
3250
|
-
}
|
|
3251
|
-
}
|
|
3252
|
-
}
|
|
3253
|
-
function drawVideoFrame(ctx, frame, sourceFor, scale = 1, options = {}) {
|
|
3254
|
-
ctx.save();
|
|
3255
|
-
ctx.setTransform(scale, 0, 0, scale, 0, 0);
|
|
3256
|
-
ctx.globalAlpha = 1;
|
|
3257
|
-
ctx.fillStyle = frame.background;
|
|
3258
|
-
ctx.fillRect(0, 0, frame.width, frame.height);
|
|
3259
|
-
drawVideoLayers(ctx, frame, frame.layers, sourceFor, scale, options, 0);
|
|
3260
|
-
ctx.restore();
|
|
3261
|
-
}
|
|
3262
|
-
function drawVideoLayers(ctx, frame, layers, sourceFor, scale, options, depth) {
|
|
3263
|
-
const cw = Math.max(1, Math.round(frame.width * scale));
|
|
3264
|
-
const ch = Math.max(1, Math.round(frame.height * scale));
|
|
3265
|
-
for (const layer of layers) {
|
|
3266
|
-
let source = sourceFor;
|
|
3267
|
-
if (layer.layers) {
|
|
3268
|
-
const group = options.scratch?.(cw, ch, 2 * depth);
|
|
3269
|
-
if (!group) {
|
|
3270
|
-
drawVideoLayers(ctx, frame, layer.layers, sourceFor, scale, options, depth + 1);
|
|
3271
|
-
continue;
|
|
3272
|
-
}
|
|
3273
|
-
const g = group.ctx;
|
|
3274
|
-
g.save();
|
|
3275
|
-
g.setTransform(1, 0, 0, 1, 0, 0);
|
|
3276
|
-
g.clearRect(0, 0, cw, ch);
|
|
3277
|
-
g.setTransform(scale, 0, 0, scale, 0, 0);
|
|
3278
|
-
g.globalAlpha = 1;
|
|
3279
|
-
g.globalCompositeOperation = "source-over";
|
|
3280
|
-
drawVideoLayers(g, frame, layer.layers, sourceFor, scale, options, depth + 1);
|
|
3281
|
-
g.restore();
|
|
3282
|
-
source = (l) => l === layer ? group.canvas : sourceFor(l);
|
|
3283
|
-
}
|
|
3284
|
-
const setting = layer.grade?.lut;
|
|
3285
|
-
const lut = setting ? options.lutFor?.(setting.src) : null;
|
|
3286
|
-
const scratch = lut ? options.scratch?.(cw, ch, 2 * depth + 1) : null;
|
|
3287
|
-
ctx.save();
|
|
3288
|
-
ctx.setTransform(scale, 0, 0, scale, 0, 0);
|
|
3289
|
-
if (lut && scratch) {
|
|
3290
|
-
const s = scratch.ctx;
|
|
3291
|
-
s.save();
|
|
3292
|
-
s.setTransform(1, 0, 0, 1, 0, 0);
|
|
3293
|
-
s.clearRect(0, 0, cw, ch);
|
|
3294
|
-
s.setTransform(scale, 0, 0, scale, 0, 0);
|
|
3295
|
-
s.globalAlpha = 1;
|
|
3296
|
-
s.globalCompositeOperation = "source-over";
|
|
3297
|
-
paintVideoLayer(s, frame, layer, source, options);
|
|
3298
|
-
s.restore();
|
|
3299
|
-
const img = s.getImageData(0, 0, cw, ch);
|
|
3300
|
-
applyVideoLutToPixels(lut, img.data, setting.strength ?? 1);
|
|
3301
|
-
s.putImageData(img, 0, 0);
|
|
3302
|
-
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
3303
|
-
ctx.globalAlpha = layer.opacity;
|
|
3304
|
-
if (layer.blend && layer.blend !== "normal") ctx.globalCompositeOperation = layer.blend === "add" ? "lighter" : layer.blend;
|
|
3305
|
-
ctx.drawImage(scratch.canvas, 0, 0, cw, ch);
|
|
3306
|
-
} else {
|
|
3307
|
-
ctx.globalAlpha = layer.opacity;
|
|
3308
|
-
if (layer.blend && layer.blend !== "normal") ctx.globalCompositeOperation = layer.blend === "add" ? "lighter" : layer.blend;
|
|
3309
|
-
paintVideoLayer(ctx, frame, layer, source, options);
|
|
3310
|
-
}
|
|
3311
|
-
ctx.restore();
|
|
3312
|
-
}
|
|
3313
|
-
}
|
|
3314
|
-
|
|
3315
|
-
// src/video-editor/export.ts
|
|
3316
|
-
var reelName = (asset) => (asset?.name ?? asset?.id ?? "AX").replace(/\.[a-z0-9]+$/i, "").replace(/[^A-Za-z0-9]/g, "").toUpperCase().slice(0, 8) || "AX";
|
|
3317
|
-
function toVideoEdl(project, options = {}) {
|
|
3318
|
-
const track = options.track !== void 0 ? project.tracks.find((t) => t.id === options.track) : [...project.tracks].reverse().find((t) => t.kind === "video");
|
|
3319
|
-
if (!track) throw new Error(options.track !== void 0 ? `There is no track "${options.track}".` : "The project has no video track.");
|
|
3320
|
-
const fps = project.fps;
|
|
3321
|
-
const dropFrame = (options.dropFrame ?? true) && isVideoDropFrameRate(fps);
|
|
3322
|
-
const tc = (ms) => formatVideoTimecode(ms, fps, dropFrame);
|
|
3323
|
-
const rec0 = options.recordStart !== void 0 ? videoTimecodeFrames(options.recordStart, fps) : videoHourFrames(fps, dropFrame);
|
|
3324
|
-
const rtc = (ms) => formatVideoTimecodeFrames(rec0 + videoTimecodeFrames(ms, fps), fps, dropFrame);
|
|
3325
|
-
const assets = videoAssetMap(project);
|
|
3326
|
-
const channel = track.kind === "video" ? "V" : "A";
|
|
3327
|
-
const lines = [`TITLE: ${(options.title ?? project.name ?? "Untitled").toUpperCase()}`, `FCM: ${dropFrame ? "DROP FRAME" : "NON-DROP FRAME"}`, ""];
|
|
3328
|
-
let event = 0;
|
|
3329
|
-
const num2 = () => String(++event).padStart(3, "0");
|
|
3330
|
-
const sourceAt = (clip, time) => clip.type === "media" ? (clip.in ?? 0) + (time - clip.start) * videoClipSpeed(clip) : time - clip.start;
|
|
3331
|
-
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)}`;
|
|
3332
|
-
for (const clip of sortedVideoClips(track)) {
|
|
3333
|
-
const asset = clip.type === "media" ? assets.get(clip.asset) : void 0;
|
|
3334
|
-
const reel = clip.type === "media" ? reelName(asset) : "BL";
|
|
3335
|
-
const end = videoClipEnd(clip);
|
|
3336
|
-
const before = clip.transition ? videoClipBefore(track, clip) : null;
|
|
3337
|
-
const half = before && clip.transition.type !== "dip" ? clip.transition.duration / 2 : 0;
|
|
3338
|
-
const next = sortedVideoClips(track).find((c) => c !== clip && Math.abs(c.start - end) < 0.5 && c.transition && c.transition.type !== "dip");
|
|
3339
|
-
const recOut = next ? end - next.transition.duration / 2 : end;
|
|
3340
|
-
const recIn = clip.start - half;
|
|
3341
|
-
const n2 = num2();
|
|
3342
|
-
if (before) {
|
|
3343
|
-
const t = clip.transition;
|
|
3344
|
-
const frames = String(Math.round(t.duration * fps / 1e3)).padStart(3, "0");
|
|
3345
|
-
const outReel = before.type === "media" ? reelName(assets.get(before.asset)) : "BL";
|
|
3346
|
-
const at = sourceAt(before, recIn);
|
|
3347
|
-
lines.push(row(n2, outReel, "C", at, at, recIn, recIn));
|
|
3348
|
-
lines.push(row(n2, reel, `${t.type === "crossfade" || t.type === "dip" ? "D" : "W001"} ${frames}`, sourceAt(clip, recIn), sourceAt(clip, recOut), recIn, recOut));
|
|
3349
|
-
} else lines.push(row(n2, reel, "C", sourceAt(clip, recIn), sourceAt(clip, recOut), recIn, recOut));
|
|
3350
|
-
if (clip.type === "media" && videoClipSpeed(clip) !== 1) lines.push(`M2 ${reel.padEnd(8)} ${(videoClipSpeed(clip) * fps).toFixed(1).padStart(6)} ${tc(sourceAt(clip, recIn))}`);
|
|
3351
|
-
if (clip.type === "media") lines.push(`* FROM CLIP NAME: ${asset?.name ?? asset?.src ?? clip.asset}`);
|
|
3352
|
-
if (clip.type === "text") lines.push(`* TITLE: ${clip.text.replace(/\n/g, " ")}`);
|
|
3353
|
-
if (clip.type === "color") lines.push(`* COLOR: ${clip.color}`);
|
|
3354
|
-
if (clip.type === "sequence") lines.push(`* COMPOUND CLIP: ${clip.name ?? clip.sequence} (sequence ${clip.sequence} from ${tc(clip.in ?? 0)}, not flattened)`);
|
|
3355
|
-
lines.push("");
|
|
3356
|
-
}
|
|
3357
|
-
return lines.join("\n");
|
|
3358
|
-
}
|
|
3359
|
-
function loudnormFilter(target = {}, measure = false) {
|
|
3360
|
-
const I = target.integrated ?? -14;
|
|
3361
|
-
const TP = target.truePeak ?? -1;
|
|
3362
|
-
const LRA = target.range ?? 11;
|
|
3363
|
-
if (!(I >= -70 && I <= -5)) throw new Error(`The loudness target must be between -70 and -5 LUFS (got ${I}); -14 suits streaming, -23 broadcast.`);
|
|
3364
|
-
if (!(TP >= -9 && TP <= 0)) throw new Error(`The true peak ceiling must be between -9 and 0 dBTP (got ${TP}); -1 is usual.`);
|
|
3365
|
-
if (!(LRA >= 1 && LRA <= 50)) throw new Error(`The loudness range must be between 1 and 50 LU (got ${LRA}).`);
|
|
3366
|
-
const parts = [`loudnorm=I=${I}:TP=${TP}:LRA=${LRA}`];
|
|
3367
|
-
const m = target.measured;
|
|
3368
|
-
if (m && !measure) {
|
|
3369
|
-
for (const [k, v] of Object.entries(m)) if (!Number.isFinite(v)) throw new Error(`The measured ${k} is not a number (${v}); measure again, or leave "measured" out for a one-pass normalisation.`);
|
|
3370
|
-
parts.push(`measured_I=${m.integrated}:measured_TP=${m.truePeak}:measured_LRA=${m.range}:measured_thresh=${m.threshold}:offset=${m.offset}:linear=true`);
|
|
3371
|
-
}
|
|
3372
|
-
if (measure) parts.push("print_format=json");
|
|
3373
|
-
return `${parts.join(":")},aresample=48000`;
|
|
3374
|
-
}
|
|
3375
|
-
function parseVideoLoudnessMeasurement(output) {
|
|
3376
|
-
const blocks = output.match(/\{[^{}]*"input_i"[^{}]*\}/g);
|
|
3377
|
-
if (!blocks) throw new Error("No loudness measurement found: pass the stderr of the command from toVideoLoudnessMeasure.");
|
|
3378
|
-
const json = JSON.parse(blocks[blocks.length - 1]);
|
|
3379
|
-
const n2 = (k) => Number(json[k]);
|
|
3380
|
-
const m = { integrated: n2("input_i"), truePeak: n2("input_tp"), range: n2("input_lra"), threshold: n2("input_thresh"), offset: n2("target_offset") };
|
|
3381
|
-
if (!Number.isFinite(m.integrated)) throw new Error("The mix is silent, so its loudness cannot be normalised.");
|
|
3382
|
-
return m;
|
|
3383
|
-
}
|
|
3384
|
-
var sec = (ms) => String(Math.round(ms) / 1e3);
|
|
3385
|
-
var fmtMs = (v) => String(Math.round(v * 100) / 100);
|
|
3386
|
-
var FFMPEG_BLEND = {
|
|
3387
|
-
multiply: "multiply",
|
|
3388
|
-
screen: "screen",
|
|
3389
|
-
overlay: "overlay",
|
|
3390
|
-
darken: "darken",
|
|
3391
|
-
lighten: "lighten",
|
|
3392
|
-
"color-dodge": "dodge",
|
|
3393
|
-
"color-burn": "burn",
|
|
3394
|
-
"hard-light": "hardlight",
|
|
3395
|
-
"soft-light": "softlight",
|
|
3396
|
-
difference: "difference",
|
|
3397
|
-
exclusion: "exclusion",
|
|
3398
|
-
add: "addition"
|
|
3399
|
-
};
|
|
3400
|
-
var AFADE_CURVES = { linear: "tri", "equal-power": "qsin", "s-curve": "hsin", exponential: "cub", logarithmic: "ipar" };
|
|
3401
|
-
var afadeCurve = (curve) => curve && curve !== "linear" ? `:curve=${AFADE_CURVES[curve]}` : "";
|
|
3402
|
-
var even = (v) => Math.max(2, 2 * Math.round(v / 2));
|
|
3403
|
-
var ffmpegFilterValue = (s) => s.replace(/[\\':%]/g, (c) => `\\${c}`).replace(/[\\'[\],;]/g, (c) => `\\${c}`);
|
|
3404
|
-
var color = (c) => {
|
|
3405
|
-
const hex = /^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i.exec(c);
|
|
3406
|
-
if (!hex) return c.replace(/[^A-Za-z0-9@.]/g, "");
|
|
3407
|
-
const h = hex[1].length === 3 ? [...hex[1]].map((x) => x + x).join("") : hex[1];
|
|
3408
|
-
return `0x${h.slice(0, 6)}${h.length === 8 ? `@${(parseInt(h.slice(6), 16) / 255).toFixed(3)}` : ""}`;
|
|
3409
|
-
};
|
|
3410
|
-
var videoShellQuote = (s) => /^[\w@%+=:,./-]+$/.test(s) ? s : `'${s.replace(/'/g, `'\\''`)}'`;
|
|
3411
|
-
function atempo(speed) {
|
|
3412
|
-
const out = [];
|
|
3413
|
-
let s = speed;
|
|
3414
|
-
while (s > 2) {
|
|
3415
|
-
out.push("atempo=2");
|
|
3416
|
-
s /= 2;
|
|
3417
|
-
}
|
|
3418
|
-
while (s < 0.5) {
|
|
3419
|
-
out.push("atempo=0.5");
|
|
3420
|
-
s /= 0.5;
|
|
3421
|
-
}
|
|
3422
|
-
if (Math.abs(s - 1) > 1e-6) out.push(`atempo=${Math.round(s * 1e6) / 1e6}`);
|
|
3423
|
-
return out;
|
|
3424
|
-
}
|
|
3425
|
-
function toVideoFfmpeg(project, options = {}) {
|
|
3426
|
-
return buildFfmpeg(project, options, false);
|
|
3427
|
-
}
|
|
3428
|
-
function toVideoLoudnessMeasure(project, options = {}) {
|
|
3429
|
-
return buildFfmpeg(project, { ...options, loudness: { ...options.loudness, measured: void 0 } }, true);
|
|
3430
|
-
}
|
|
3431
|
-
function buildFfmpeg(project, options, measure) {
|
|
3432
|
-
const notes = [];
|
|
3433
|
-
const assets = videoAssetMap(project);
|
|
3434
|
-
const sequences = videoSequenceMap(project);
|
|
3435
|
-
const D = videoProjectDuration(project);
|
|
3436
|
-
if (D <= 0) throw new Error("The project has no clips, so there is nothing to render.");
|
|
3437
|
-
const W = even(project.width);
|
|
3438
|
-
const H = even(project.height);
|
|
3439
|
-
const fps = project.fps;
|
|
3440
|
-
const inputs = [];
|
|
3441
|
-
const graph = measure ? [] : [`color=c=${color(project.background ?? "#000000")}:s=${W}x${H}:r=${fps}:d=${sec(D)},format=yuva420p[base]`];
|
|
3442
|
-
let usesDrawtext = false;
|
|
3443
|
-
const animated = /* @__PURE__ */ new Set();
|
|
3444
|
-
const ramped = /* @__PURE__ */ new Set();
|
|
3445
|
-
const rampedPicture = (remap, lead, end) => {
|
|
3446
|
-
const pts = videoTimeRemapBreakpoints(remap, 2e3 / fps);
|
|
3447
|
-
const first = pts[0];
|
|
3448
|
-
const last = pts[pts.length - 1];
|
|
3449
|
-
const s = `(T*1000+${fmtMs(first.v)})`;
|
|
3450
|
-
let e = fmtMs(last.t - first.t);
|
|
3451
|
-
for (let i = pts.length - 2; i >= 0; i--) {
|
|
3452
|
-
const a = pts[i];
|
|
3453
|
-
const b = pts[i + 1];
|
|
3454
|
-
if (b.v - a.v < 1e-6) continue;
|
|
3455
|
-
e = `if(lt(${s},${fmtMs(b.v)}),${fmtMs(a.t - first.t)}+(${s}-${fmtMs(a.v)})*${fmtMs((b.t - a.t) / (b.v - a.v))},${e})`;
|
|
3456
|
-
}
|
|
3457
|
-
const before = lead + first.t;
|
|
3458
|
-
const after = Math.max(0, end - last.t);
|
|
3459
|
-
return ["setpts=PTS-STARTPTS", `setpts='(${e})/1000/TB'`, `fps=${fps}`, ...before > 0.5 || after > 0.5 ? [`tpad=start_mode=clone:start_duration=${sec(before)}:stop_mode=clone:stop_duration=${sec(after + 1e3 / fps)}`] : [`tpad=stop_mode=clone:stop_duration=${sec(1e3 / fps)}`], `trim=duration=${sec(lead + end)}`];
|
|
3460
|
-
};
|
|
3461
|
-
const rampedSound = (remap, input2, output, lead, end) => {
|
|
3462
|
-
const pts = videoTimeRemapBreakpoints(remap, Math.max(80, 2e3 / fps));
|
|
3463
|
-
const first = pts[0];
|
|
3464
|
-
const last = pts[pts.length - 1];
|
|
3465
|
-
const n2 = ak;
|
|
3466
|
-
const fmtA = "aformat=sample_fmts=fltp:sample_rates=48000:channel_layouts=stereo";
|
|
3467
|
-
const silence = (ms) => `anullsrc=r=48000:cl=stereo,atrim=duration=${sec(ms)},${fmtA}`;
|
|
3468
|
-
const pieces = [];
|
|
3469
|
-
const out = [];
|
|
3470
|
-
const parts = [];
|
|
3471
|
-
if (lead + first.t > 0.5) parts.push(silence(lead + first.t));
|
|
3472
|
-
const played = [];
|
|
3473
|
-
for (let i = 0; i + 1 < pts.length; i++) {
|
|
3474
|
-
const dt = pts[i + 1].t - pts[i].t;
|
|
3475
|
-
if (dt < 0.5) continue;
|
|
3476
|
-
const speed = (pts[i + 1].v - pts[i].v) / dt;
|
|
3477
|
-
if (speed < 0.1) parts.push(silence(dt));
|
|
3478
|
-
else {
|
|
3479
|
-
played.push({ from: pts[i].v - first.v, to: pts[i + 1].v - first.v, speed });
|
|
3480
|
-
parts.push(`#${played.length - 1}`);
|
|
3481
|
-
}
|
|
3482
|
-
}
|
|
3483
|
-
if (end - last.t > 0.5) parts.push(silence(end - last.t));
|
|
3484
|
-
if (played.length) out.push(`[${input2}]asplit=${played.length}${played.map((_, k2) => `[r${n2}s${k2}]`).join("")}`);
|
|
3485
|
-
parts.forEach((p, k2) => {
|
|
3486
|
-
const label = `[r${n2}p${k2}]`;
|
|
3487
|
-
if (p.startsWith("#")) {
|
|
3488
|
-
const piece = played[Number(p.slice(1))];
|
|
3489
|
-
out.push(`[r${n2}s${p.slice(1)}]atrim=start=${sec(piece.from)}:end=${sec(piece.to)},asetpts=PTS-STARTPTS,${atempo(piece.speed).join(",") || "anull"},${fmtA}${label}`);
|
|
3490
|
-
} else out.push(`${p}${label}`);
|
|
3491
|
-
pieces.push(label);
|
|
3492
|
-
});
|
|
3493
|
-
out.push(`${pieces.join("")}concat=n=${pieces.length}:v=0:a=1${output}`);
|
|
3494
|
-
return out;
|
|
3495
|
-
};
|
|
3496
|
-
const blended = /* @__PURE__ */ new Set();
|
|
3497
|
-
let k = 0;
|
|
3498
|
-
const step = 2e3 / fps;
|
|
3499
|
-
const expr = (prop, clipTime, fallback, what) => {
|
|
3500
|
-
if (isVideoKeyframes(prop)) animated.add(what);
|
|
3501
|
-
return videoKeyframeFfmpegExpr(prop, clipTime, fallback, step);
|
|
3502
|
-
};
|
|
3503
|
-
const input = (clip, asset, lead, tail) => {
|
|
3504
|
-
const speed = videoClipSpeed(clip);
|
|
3505
|
-
const src = options.resolve ? options.resolve(asset) : asset.src;
|
|
3506
|
-
const span = clip.duration + lead + tail;
|
|
3507
|
-
if (asset.kind === "image") inputs.push(["-loop", "1", "-framerate", String(fps), "-t", sec(span), "-i", src]);
|
|
3508
|
-
else if (clip.timeRemap) {
|
|
3509
|
-
const { from, to } = videoTimeRemapSpan(clip.timeRemap);
|
|
3510
|
-
inputs.push(["-ss", sec((clip.in ?? 0) + from), "-t", sec(to - from + 1e3 / fps), "-i", src]);
|
|
3511
|
-
} else inputs.push(["-ss", sec((clip.in ?? 0) - lead * speed), "-t", sec(span * speed), "-i", src]);
|
|
3512
|
-
return inputs.length - 1;
|
|
3513
|
-
};
|
|
3514
|
-
let ak = 0;
|
|
3515
|
-
let q = 0;
|
|
3516
|
-
const compose = (tracks, base, length, scope) => {
|
|
3517
|
-
let cur2 = base;
|
|
3518
|
-
const sounds = [];
|
|
3519
|
-
for (const track of [...tracks].reverse()) {
|
|
3520
|
-
for (const raw of sortedVideoClips(track)) {
|
|
3521
|
-
const clip = videoClipAnimated(raw, project);
|
|
3522
|
-
const { lead, tail } = videoClipOverhang(track, clip);
|
|
3523
|
-
const t0 = clip.start - lead;
|
|
3524
|
-
const span = clip.duration + lead + tail;
|
|
3525
|
-
const end = videoClipEnd(clip);
|
|
3526
|
-
const asset = clip.type === "media" ? assets.get(clip.asset) : void 0;
|
|
3527
|
-
if (clip.type === "media" && !asset) {
|
|
3528
|
-
notes.push(`Clip "${clip.id}" was left out: its asset "${clip.asset}" is missing.`);
|
|
3529
|
-
continue;
|
|
3530
|
-
}
|
|
3531
|
-
const shows = scope.picture && track.kind === "video" && !track.hidden;
|
|
3532
|
-
let compound = null;
|
|
3533
|
-
if (clip.type === "sequence") {
|
|
3534
|
-
const seq = sequences.get(clip.sequence);
|
|
3535
|
-
if (!seq || scope.within.includes(seq.id)) {
|
|
3536
|
-
notes.push(`Compound clip "${clip.id}" was left out: its sequence "${clip.sequence}" ${seq ? "contains itself" : "is missing"}.`);
|
|
3537
|
-
continue;
|
|
3538
|
-
}
|
|
3539
|
-
const from = (clip.in ?? 0) - lead;
|
|
3540
|
-
if (from < -0.5) notes.push(`Compound clip "${clip.id}"'s transition needs ${Math.round(-from)} ms of sequence "${seq.id}" before its start, which is not there; it shows nothing then.`);
|
|
3541
|
-
const length2 = Math.max(videoSequenceDuration(seq), from + span);
|
|
3542
|
-
const hears = scope.sound && !clip.muted && !track.muted;
|
|
3543
|
-
const n2 = q++;
|
|
3544
|
-
const nestedBase = shows ? `nb${n2}` : null;
|
|
3545
|
-
if (nestedBase) graph.push(`color=c=black@0:s=${W}x${H}:r=${fps}:d=${sec(length2)},format=yuva420p[${nestedBase}]`);
|
|
3546
|
-
const sub = compose(seq.tracks, nestedBase, length2, { within: [...scope.within, seq.id], picture: shows, sound: hears });
|
|
3547
|
-
compound = { picture: null, sound: null };
|
|
3548
|
-
if (sub.video) {
|
|
3549
|
-
graph.push(`[${sub.video}]trim=start=${sec(Math.max(0, from))}:duration=${sec(span)},setpts=PTS-STARTPTS[sq${n2}]`);
|
|
3550
|
-
compound.picture = `[sq${n2}]`;
|
|
3551
|
-
}
|
|
3552
|
-
if (sub.audio.length) {
|
|
3553
|
-
graph.push(`${sub.audio.join("")}amix=inputs=${sub.audio.length}:normalize=0:duration=longest,apad,atrim=start=${sec(Math.max(0, from))}:end=${sec(from + span)}[sm${n2}]`);
|
|
3554
|
-
compound.sound = `[sm${n2}]`;
|
|
3555
|
-
}
|
|
3556
|
-
}
|
|
3557
|
-
let index;
|
|
3558
|
-
if (shows && asset && asset.kind !== "audio") index = input(clip, asset, lead, tail);
|
|
3559
|
-
const audible = clip.type === "sequence" ? !!compound?.sound : videoAssetHasSound(asset);
|
|
3560
|
-
if (scope.sound && audible && (clip.type === "media" || clip.type === "sequence") && !clip.muted && !track.muted) {
|
|
3561
|
-
const trackGain = track.volume ?? 1;
|
|
3562
|
-
const volume = isVideoKeyframes(clip.volume) ? `volume='${trackGain}*(${expr(clip.volume, `(t*1000${lead ? `-${Math.round(lead)}` : ""})`, 1, "volume")})':eval=frame` : `volume=${(clip.volume ?? 1) * trackGain}`;
|
|
3563
|
-
const ramp = clip.type === "media" ? clip.timeRemap : void 0;
|
|
3564
|
-
const ai = compound ? -1 : index ?? input(clip, asset, lead, tail);
|
|
3565
|
-
let source2 = compound?.sound ?? `[${ai}:a]`;
|
|
3566
|
-
if (ramp) {
|
|
3567
|
-
source2 = `[r${ak}]`;
|
|
3568
|
-
graph.push(...rampedSound(ramp, `${ai}:a`, source2, lead, clip.duration + tail));
|
|
3569
|
-
ramped.add("sound");
|
|
3570
|
-
}
|
|
3571
|
-
const chain2 = [...ramp ? [] : ["asetpts=PTS-STARTPTS", ...atempo(videoClipSpeed(clip))], volume];
|
|
3572
|
-
const before2 = clip.transition ? videoClipBefore(track, clip) : null;
|
|
3573
|
-
const next2 = sortedVideoClips(track).find((c) => c !== clip && Math.abs(c.start - end) < 0.5 && c.transition);
|
|
3574
|
-
const inCurve = clip.transition ? afadeCurve(videoTransitionAudioCurve(clip.transition)) : "";
|
|
3575
|
-
const outCurve = next2 ? afadeCurve(videoTransitionAudioCurve(next2.transition)) : "";
|
|
3576
|
-
if (lead) chain2.push(`afade=t=in:st=0:d=${sec(2 * lead)}${inCurve}`);
|
|
3577
|
-
if (tail) chain2.push(`afade=t=out:st=${sec(span - 2 * tail)}:d=${sec(2 * tail)}${outCurve}`);
|
|
3578
|
-
if (before2 && clip.transition.type === "dip") chain2.push(`afade=t=in:st=0:d=${sec(clip.transition.duration / 2)}${inCurve}`);
|
|
3579
|
-
if (next2?.transition.type === "dip") chain2.push(`afade=t=out:st=${sec(span - next2.transition.duration / 2)}:d=${sec(next2.transition.duration / 2)}${outCurve}`);
|
|
3580
|
-
if (clip.fadeIn) chain2.push(`afade=t=in:st=${sec(lead)}:d=${sec(clip.fadeIn)}${afadeCurve(clip.fadeInCurve)}`);
|
|
3581
|
-
if (clip.fadeOut) chain2.push(`afade=t=out:st=${sec(lead + clip.duration - clip.fadeOut)}:d=${sec(clip.fadeOut)}${afadeCurve(clip.fadeOutCurve)}`);
|
|
3582
|
-
chain2.push(`adelay=${Math.round(t0)}:all=1`);
|
|
3583
|
-
graph.push(`${source2}${chain2.join(",")}[a${ak}]`);
|
|
3584
|
-
sounds.push(`[a${ak++}]`);
|
|
3585
|
-
}
|
|
3586
|
-
if (!shows || compound && !compound.picture) continue;
|
|
3587
|
-
const enable = `enable='between(t,${sec(t0)},${sec(t0 + span)})'`;
|
|
3588
|
-
const next = `v${k++}`;
|
|
3589
|
-
const onTimeline = `(t*1000-${Math.round(clip.start)})`;
|
|
3590
|
-
const onChain = (v) => `(${v}*1000${lead ? `-${Math.round(lead)}` : ""})`;
|
|
3591
|
-
const tf = clip.transform;
|
|
3592
|
-
const moving = VIDEO_TRANSFORM_PROPERTIES.some((p) => isVideoKeyframes(tf?.[p]));
|
|
3593
|
-
if (clip.type === "text" && options.titleImage) {
|
|
3594
|
-
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 })]);
|
|
3595
|
-
index = inputs.length - 1;
|
|
3596
|
-
} else if (clip.type === "text") {
|
|
3597
|
-
usesDrawtext = true;
|
|
3598
|
-
if (scope.within.length) notes.push(`Title "${clip.id}" is inside a compound clip and drawn with drawtext, which leaves the sequence's transparent areas transparent, so it shows only over pictures in the sequence; pass titleImage to draw it anywhere.`);
|
|
3599
|
-
const op = isVideoKeyframes(clip.opacity) ? `clip(${expr(clip.opacity, onTimeline, 1, "opacity")},0,1)` : String(clip.opacity ?? 1);
|
|
3600
|
-
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("*");
|
|
3601
|
-
const coord = (axis) => {
|
|
3602
|
-
const v = tf?.[axis];
|
|
3603
|
-
const base2 = `(${axis === "x" ? "w-text_w" : "h-text_h"})/2`;
|
|
3604
|
-
return isVideoKeyframes(v) ? `${axis}='${base2}+(${expr(v, onTimeline, 0, "position")})'` : `${axis}=${base2}+${v ?? 0}`;
|
|
3605
|
-
};
|
|
3606
|
-
const fontSize = clip.fontSize ?? 64;
|
|
3607
|
-
const opts = [
|
|
3608
|
-
`text=${ffmpegFilterValue(clip.text)}`,
|
|
3609
|
-
options.fontFile ? `fontfile=${ffmpegFilterValue(options.fontFile)}` : `font=${ffmpegFilterValue(clip.fontFamily ?? "sans-serif")}`,
|
|
3610
|
-
isVideoKeyframes(tf?.scale) ? `fontsize='${fontSize}*(${expr(tf.scale, onTimeline, 1, "scale")})'` : `fontsize=${Math.round(fontSize * (tf?.scale ?? 1))}`,
|
|
3611
|
-
`fontcolor=${color(clip.color ?? "#ffffff")}`,
|
|
3612
|
-
coord("x"),
|
|
3613
|
-
coord("y"),
|
|
3614
|
-
`alpha='${alpha}'`,
|
|
3615
|
-
...clip.background ? ["box=1", `boxcolor=${color(clip.background)}`, `boxborderw=${Math.round((clip.fontSize ?? 64) / 2)}`] : [],
|
|
3616
|
-
`enable='between(t,${sec(clip.start)},${sec(end)})'`
|
|
3617
|
-
];
|
|
3618
|
-
if (tf?.rotation) notes.push(`Title "${clip.id}" is drawn without its rotation (drawtext cannot rotate).`);
|
|
3619
|
-
if (videoGradeFfmpegFilters(clip.grade).length || normalizeVideoGradeLut(clip.grade?.lut)) notes.push(`Title "${clip.id}" is drawn without its grade (drawtext draws straight onto the picture); pass titleImage to grade titles.`);
|
|
3620
|
-
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.`);
|
|
3621
|
-
graph.push(`[${cur2}]drawtext=${opts.join(":")}[${next}]`);
|
|
3622
|
-
cur2 = next;
|
|
3623
|
-
continue;
|
|
3624
|
-
}
|
|
3625
|
-
const size = asset ? { width: asset.width ?? project.width, height: asset.height ?? project.height } : { width: project.width, height: project.height };
|
|
3626
|
-
const placed = clip.type === "text" && !moving ? void 0 : tf;
|
|
3627
|
-
const still = Object.fromEntries(Object.entries(placed ?? {}).filter(([, v]) => !isVideoKeyframes(v)));
|
|
3628
|
-
const rect = placeVideoRect(project, size, clip.type === "text" ? "fill" : clip.fit ?? (clip.type === "media" || clip.type === "sequence" ? "contain" : "fill"), still);
|
|
3629
|
-
const chain = [];
|
|
3630
|
-
let source;
|
|
3631
|
-
if (clip.type === "color") source = `color=c=${color(clip.color)}:s=${even(rect.width)}x${even(rect.height)}:r=${fps}:d=${sec(span)}`;
|
|
3632
|
-
else if (compound?.picture) {
|
|
3633
|
-
source = compound.picture;
|
|
3634
|
-
chain.push(`scale=${even(rect.width)}:${even(rect.height)}`);
|
|
3635
|
-
} else {
|
|
3636
|
-
source = `[${index}:v]`;
|
|
3637
|
-
if (clip.type === "media" && clip.timeRemap) chain.push(...rampedPicture(clip.timeRemap, lead, clip.duration + tail), `scale=${even(rect.width)}:${even(rect.height)}`);
|
|
3638
|
-
else chain.push(`setpts=${videoClipSpeed(clip) !== 1 ? `(PTS-STARTPTS)/${videoClipSpeed(clip)}` : "PTS-STARTPTS"}`, `fps=${fps}`, `scale=${even(rect.width)}:${even(rect.height)}`);
|
|
3639
|
-
}
|
|
3640
|
-
const grade = videoGradeFfmpegFilters(clip.grade);
|
|
3641
|
-
if (grade.length) chain.push("format=rgba", ...grade);
|
|
3642
|
-
const lut = normalizeVideoGradeLut(clip.grade?.lut);
|
|
3643
|
-
if (lut) {
|
|
3644
|
-
chain.push("format=gbrap");
|
|
3645
|
-
const path = options.resolveLut ? options.resolveLut(lut.src) : lut.src;
|
|
3646
|
-
if (/^(blob|data|https?):/i.test(path)) notes.push(`ffmpeg's lut3d reads only local files, and clip "${clip.id}"'s LUT is "${path.slice(0, 80)}"; save it and pass resolveLut to give its path.`);
|
|
3647
|
-
const file = ffmpegFilterValue(path);
|
|
3648
|
-
const into2 = `q${next}`;
|
|
3649
|
-
graph.push(`${source.startsWith("[") ? source : `${source},`}${chain.join(",")}[${into2}]`);
|
|
3650
|
-
const lut3d = `lut3d=file=${file}:interp=tetrahedral`;
|
|
3651
|
-
if ((lut.strength ?? 1) >= 1) graph.push(`[${into2}]${lut3d}[r${next}]`);
|
|
3652
|
-
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}]`);
|
|
3653
|
-
source = `[r${next}]`;
|
|
3654
|
-
chain.length = 0;
|
|
3655
|
-
}
|
|
3656
|
-
chain.push("format=yuva420p");
|
|
3657
|
-
if (isVideoKeyframes(clip.opacity)) chain.push(`geq=lum='lum(X,Y)':cb='cb(X,Y)':cr='cr(X,Y)':a='alpha(X,Y)*clip(${expr(clip.opacity, onChain("T"), 1, "opacity")},0,1)'`);
|
|
3658
|
-
else if (clip.opacity !== void 0 && clip.opacity < 1) chain.push(`colorchannelmixer=aa=${clip.opacity}`);
|
|
3659
|
-
if (clip.fadeIn) chain.push(`fade=t=in:st=${sec(lead)}:d=${sec(clip.fadeIn)}:alpha=1`);
|
|
3660
|
-
if (clip.fadeOut) chain.push(`fade=t=out:st=${sec(lead + clip.duration - clip.fadeOut)}:d=${sec(clip.fadeOut)}:alpha=1`);
|
|
3661
|
-
if (lead && clip.transition.type === "crossfade") chain.push(`fade=t=in:st=0:d=${sec(2 * lead)}:alpha=1`);
|
|
3662
|
-
if (lead && clip.transition.type === "wipe") {
|
|
3663
|
-
const d = sec(2 * lead);
|
|
3664
|
-
const p = `min(1,T/${d})`;
|
|
3665
|
-
const dir = clip.transition.direction ?? "right";
|
|
3666
|
-
const test = dir === "right" ? `lt(X,W*${p})` : dir === "left" ? `gt(X,W*(1-${p}))` : dir === "down" ? `lt(Y,H*${p})` : `gt(Y,H*(1-${p}))`;
|
|
3667
|
-
chain.push(`geq=lum='lum(X,Y)':cb='cb(X,Y)':cr='cr(X,Y)':a='if(${test},alpha(X,Y),0)'`);
|
|
3668
|
-
}
|
|
3669
|
-
if (lead && clip.transition.type === "iris") {
|
|
3670
|
-
const r = videoIrisRadius({ width: W, height: H }, 1);
|
|
3671
|
-
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)'`);
|
|
3672
|
-
}
|
|
3673
|
-
const rotation = placed?.rotation;
|
|
3674
|
-
if (isVideoKeyframes(rotation)) chain.push(`rotate=a='(${expr(rotation, onChain("t"), 0, "rotation")})*PI/180':c=black@0:ow='hypot(iw,ih)':oh=ow`);
|
|
3675
|
-
else if (rotation) chain.push(`rotate=${rotation}*PI/180:c=black@0:ow=rotw(${rotation}*PI/180):oh=roth(${rotation}*PI/180)`);
|
|
3676
|
-
if (isVideoKeyframes(placed?.scale)) {
|
|
3677
|
-
const s = expr(placed.scale, onChain("t"), 1, "scale");
|
|
3678
|
-
chain.push(`scale=w='max(2,trunc(iw*(${s})/2)*2)':h='max(2,trunc(ih*(${s})/2)*2)':eval=frame`);
|
|
3679
|
-
}
|
|
3680
|
-
chain.push(`setpts=PTS+${sec(t0)}/TB`);
|
|
3681
|
-
const cx = rect.x + rect.width / 2;
|
|
3682
|
-
const cy = rect.y + rect.height / 2;
|
|
3683
|
-
const shifts = { x: [], y: [] };
|
|
3684
|
-
const shiftBy = (direction, from, d, entering) => {
|
|
3685
|
-
const dir = direction ?? "right";
|
|
3686
|
-
const axis = dir === "left" || dir === "right" ? "x" : "y";
|
|
3687
|
-
const size2 = axis === "x" ? W : H;
|
|
3688
|
-
const sign = dir === "left" || dir === "up" ? -1 : 1;
|
|
3689
|
-
const ease = `(1-cos(PI*clip((t-${sec(from)})/${sec(d)},0,1)))/2`;
|
|
3690
|
-
shifts[axis].push(`${sign * size2}*(${ease}${entering ? "-1" : ""})`);
|
|
3691
|
-
};
|
|
3692
|
-
const into = lead ? clip.transition.type : void 0;
|
|
3693
|
-
if (into === "push" || into === "slide") shiftBy(clip.transition.direction, clip.start - lead, 2 * lead, true);
|
|
3694
|
-
const after = tail ? sortedVideoClips(track).find((c) => c !== clip && Math.abs(c.start - end) < 0.5 && c.transition) : void 0;
|
|
3695
|
-
if (after?.transition.type === "push") shiftBy(after.transition.direction, end - tail, 2 * tail, false);
|
|
3696
|
-
const at = (axis, c) => {
|
|
3697
|
-
const v = placed?.[axis];
|
|
3698
|
-
const extra = shifts[axis].map((s) => `+${s}`).join("");
|
|
3699
|
-
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}'`;
|
|
3700
|
-
};
|
|
3701
|
-
graph.push(`${source.startsWith("[") ? source : `${source},`}${chain.join(",")}[l${next}]`);
|
|
3702
|
-
if (clip.blend && clip.blend !== "normal") {
|
|
3703
|
-
graph.push(`color=c=black@0:s=${W}x${H}:r=${fps}:d=${sec(length)},format=yuva420p[t${next}]`);
|
|
3704
|
-
const clear = scope.within.length > 0;
|
|
3705
|
-
graph.push(`[t${next}][l${next}]overlay=${at("x", cx)}:${at("y", cy)}:eof_action=pass:${enable},format=gbrap,split${clear ? `=3` : ""}[f${next}][g${next}]${clear ? `[h${next}]` : ""}`);
|
|
3706
|
-
graph.push(`[${cur2}]format=gbrap,split${clear ? `=3` : ""}[c${next}][d${next}]${clear ? `[e${next}]` : ""}`);
|
|
3707
|
-
const layerFirst = clip.blend === "color-dodge" || clip.blend === "color-burn";
|
|
3708
|
-
graph.push(`${layerFirst ? `[f${next}][c${next}]` : `[c${next}][f${next}]`}blend=all_mode=${FFMPEG_BLEND[clip.blend]}[b${next}]`);
|
|
3709
|
-
graph.push(`[g${next}]alphaextract[m${next}]`);
|
|
3710
|
-
if (clear) {
|
|
3711
|
-
graph.push(`[e${next}]alphaextract[ea${next}]`);
|
|
3712
|
-
graph.push(`[b${next}][ea${next}]alphamerge[bb${next}]`);
|
|
3713
|
-
graph.push(`[h${next}]format=gbrp[ho${next}]`);
|
|
3714
|
-
graph.push(`[ho${next}][bb${next}]overlay=format=gbrp[bm${next}]`);
|
|
3715
|
-
graph.push(`[bm${next}][m${next}]alphamerge[x${next}]`);
|
|
3716
|
-
} else graph.push(`[b${next}][m${next}]alphamerge[x${next}]`);
|
|
3717
|
-
graph.push(`[d${next}][x${next}]overlay=format=gbrp,format=yuva420p[${next}]`);
|
|
3718
|
-
blended.add(clip.blend);
|
|
3719
|
-
} else graph.push(`[${cur2}][l${next}]overlay=${at("x", cx)}:${at("y", cy)}:eof_action=pass:${enable}[${next}]`);
|
|
3720
|
-
cur2 = next;
|
|
3721
|
-
const before = clip.transition ? videoClipBefore(track, clip) : null;
|
|
3722
|
-
if (before && clip.transition.type === "dip") {
|
|
3723
|
-
const d = clip.transition.duration;
|
|
3724
|
-
const dip = `d${k}`;
|
|
3725
|
-
graph.push(`color=c=${color(clip.transition.color ?? "#000000")}:s=${W}x${H}:r=${fps}:d=${sec(d)},format=yuva420p,fade=t=in:st=0:d=${sec(d / 2)}:alpha=1,fade=t=out:st=${sec(d / 2)}:d=${sec(d / 2)}:alpha=1,setpts=PTS+${sec(clip.start - d / 2)}/TB[l${dip}]`);
|
|
3726
|
-
graph.push(`[${cur2}][l${dip}]overlay=eof_action=pass:enable='between(t,${sec(clip.start - d / 2)},${sec(clip.start + d / 2)})'[${dip}]`);
|
|
3727
|
-
cur2 = dip;
|
|
3728
|
-
}
|
|
3729
|
-
}
|
|
3730
|
-
}
|
|
3731
|
-
return { video: cur2, audio: sounds };
|
|
3732
|
-
};
|
|
3733
|
-
const top = compose(project.tracks, measure ? null : "base", D, { within: [], picture: !measure, sound: true });
|
|
3734
|
-
const audio = top.audio;
|
|
3735
|
-
const cur = top.video;
|
|
3736
|
-
if (!measure) graph.push(`[${cur}]format=yuv420p[vout]`);
|
|
3737
|
-
if (measure && !audio.length) throw new Error("Nothing on the timeline makes a sound, so there is no loudness to measure.");
|
|
3738
|
-
const loudness = options.loudness || measure ? `,${loudnormFilter(options.loudness, measure)}` : "";
|
|
3739
|
-
if (audio.length) graph.push(`${audio.join("")}amix=inputs=${audio.length}:normalize=0:duration=longest,apad,atrim=0:${sec(D)}${loudness}[aout]`);
|
|
3740
|
-
if (options.loudness && !audio.length) notes.push("Nothing makes a sound, so there was no loudness to normalise.");
|
|
3741
|
-
else if (options.loudness && !options.loudness.measured)
|
|
3742
|
-
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.");
|
|
3743
|
-
if (allVideoTracks(project).some(({ track: t }) => t.clips.some((c) => c.type === "media" && assets.get(c.asset)?.kind === "video" && assets.get(c.asset)?.hasAudio === void 0)))
|
|
3744
|
-
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.');
|
|
3745
|
-
if (animated.size)
|
|
3746
|
-
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)" : ""}.`);
|
|
3747
|
-
if (blended.has("soft-light")) notes.push("ffmpeg's soft-light blend uses a slightly different formula from the browser's, so the render can differ a little from the preview.");
|
|
3748
|
-
if (ramped.has("sound")) notes.push("Sound through a speed ramp plays each stretch at its own speed, so a gliding change is heard in small steps (about every 80 ms), and freezes are silent.");
|
|
3749
|
-
if (usesDrawtext) notes.push("Titles use drawtext, which needs an ffmpeg built with libfreetype (Homebrew's is not: check `ffmpeg -filters | grep drawtext`); pass titleImage to render them as images instead.");
|
|
3750
|
-
const filterComplex = graph.join(";");
|
|
3751
|
-
if (measure) {
|
|
3752
|
-
const args2 = ["-hide_banner", "-nostats", ...inputs.flat(), "-filter_complex", filterComplex, "-map", "[aout]", "-f", "null", "-"];
|
|
3753
|
-
return { args: args2, command: ["ffmpeg", ...args2].map(videoShellQuote).join(" "), filterComplex, duration: D, notes };
|
|
3754
|
-
}
|
|
3755
|
-
const args = [
|
|
3756
|
-
"-y",
|
|
3757
|
-
...inputs.flat(),
|
|
3758
|
-
"-filter_complex",
|
|
3759
|
-
filterComplex,
|
|
3760
|
-
"-map",
|
|
3761
|
-
"[vout]",
|
|
3762
|
-
...audio.length ? ["-map", "[aout]", "-c:a", options.audioCodec ?? "aac"] : ["-an"],
|
|
3763
|
-
"-c:v",
|
|
3764
|
-
options.videoCodec ?? "libx264",
|
|
3765
|
-
...(options.videoCodec ?? "libx264") === "libx264" ? ["-crf", String(options.crf ?? 18), "-preset", "medium"] : [],
|
|
3766
|
-
"-pix_fmt",
|
|
3767
|
-
"yuv420p",
|
|
3768
|
-
"-r",
|
|
3769
|
-
String(fps),
|
|
3770
|
-
"-t",
|
|
3771
|
-
sec(D),
|
|
3772
|
-
options.output ?? "output.mp4"
|
|
3773
|
-
];
|
|
3774
|
-
return { args, command: ["ffmpeg", ...args].map(videoShellQuote).join(" "), filterComplex, duration: D, notes };
|
|
3775
|
-
}
|
|
3776
|
-
|
|
3777
2114
|
// src/video-editor/proxy.ts
|
|
3778
2115
|
var VIDEO_PROXY_SCALES = [0.5, 0.25];
|
|
3779
2116
|
function videoAssetProxy(asset, scale) {
|
|
@@ -3884,7 +2221,7 @@ function planVideoPrefetch(project, time, options = {}) {
|
|
|
3884
2221
|
return out;
|
|
3885
2222
|
}
|
|
3886
2223
|
function videoCacheEvictions(entries, options) {
|
|
3887
|
-
let total = entries.reduce((
|
|
2224
|
+
let total = entries.reduce((n, e) => n + e.bytes, 0);
|
|
3888
2225
|
if (total <= options.maxBytes) return [];
|
|
3889
2226
|
const dir = options.direction ?? 1;
|
|
3890
2227
|
const behind = options.behind ?? 2;
|
|
@@ -5038,7 +3375,7 @@ function describeVideoSequence(project, sequence, tracks = true) {
|
|
|
5038
3375
|
id: sequence.id,
|
|
5039
3376
|
...sequence.name ? { name: sequence.name } : {},
|
|
5040
3377
|
duration: videoSequenceDuration(sequence),
|
|
5041
|
-
clipCount: sequence.tracks.reduce((
|
|
3378
|
+
clipCount: sequence.tracks.reduce((n, t) => n + t.clips.length, 0),
|
|
5042
3379
|
nests: videoSequenceChildren(sequence),
|
|
5043
3380
|
usedBy: videoSequenceUsers(project, sequence.id),
|
|
5044
3381
|
...cycle ? { cycle } : {},
|
|
@@ -5627,7 +3964,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
5627
3964
|
annotations: { title: "Find filler words", ...READ },
|
|
5628
3965
|
handler: ({ track, fillers }) => {
|
|
5629
3966
|
const found = findVideoFillers(timelineWords(track), fillers);
|
|
5630
|
-
return { fillers: found.map(RANGE_OUT), total: round(found.reduce((
|
|
3967
|
+
return { fillers: found.map(RANGE_OUT), total: round(found.reduce((n, r) => n + r.to - r.from, 0)) };
|
|
5631
3968
|
}
|
|
5632
3969
|
};
|
|
5633
3970
|
const findPauses = {
|
|
@@ -5642,7 +3979,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
5642
3979
|
annotations: { title: "Find pauses between words", ...READ },
|
|
5643
3980
|
handler: ({ track, min, keep }) => {
|
|
5644
3981
|
const found = findVideoTranscriptPauses(timelineWords(track), { min, keep, fps: project().fps });
|
|
5645
|
-
return { pauses: found.map(RANGE_OUT), total: round(found.reduce((
|
|
3982
|
+
return { pauses: found.map(RANGE_OUT), total: round(found.reduce((n, r) => n + r.to - r.from, 0)) };
|
|
5646
3983
|
}
|
|
5647
3984
|
};
|
|
5648
3985
|
const removeRanges = {
|
|
@@ -5958,7 +4295,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
5958
4295
|
const r = soundRange(args);
|
|
5959
4296
|
const peaks = await controller.audioPeaks.load(r.asset);
|
|
5960
4297
|
const silences = findVideoAudioSilences(peaks, r.toSource(r.from), r.toSource(r.to), { thresholdDb: args.thresholdDb, minDuration: args.minDuration }).map((x) => ({ start: round(r.toOut(x.start)), end: round(r.toOut(x.end)) }));
|
|
5961
|
-
return { asset: r.asset.id, from: r.from, to: r.to, silences, total: round(silences.reduce((
|
|
4298
|
+
return { asset: r.asset.id, from: r.from, to: r.to, silences, total: round(silences.reduce((n, x) => n + x.end - x.start, 0)) };
|
|
5962
4299
|
}
|
|
5963
4300
|
};
|
|
5964
4301
|
const pixelsAt = async (time) => {
|