@pixerate/editor 0.79.0 → 0.81.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/README.md +13 -3
- package/dist/agent/index.cjs +102 -6
- package/dist/agent/index.cjs.map +1 -1
- package/dist/agent/index.d.cts +4 -2
- package/dist/agent/index.d.ts +4 -2
- package/dist/agent/index.js +101 -5
- package/dist/agent/index.js.map +1 -1
- package/dist/{builtins-BKYlHH1s.d.ts → builtins-CV1fDj64.d.ts} +5 -1
- package/dist/{builtins-1kqwLUnb.d.cts → builtins-c4w0eAYl.d.cts} +5 -1
- package/dist/{chunk-2CJ3LPYQ.js → chunk-424H3P4I.js} +13 -295
- package/dist/chunk-424H3P4I.js.map +1 -0
- package/dist/{chunk-UN33YOXW.cjs → chunk-PA5S7KPC.cjs} +293 -2
- package/dist/chunk-PA5S7KPC.cjs.map +1 -0
- package/dist/{chunk-IB3ZHQAX.js → chunk-PTYWCFFW.js} +29 -4
- package/dist/chunk-PTYWCFFW.js.map +1 -0
- package/dist/{chunk-JQ3BB3E6.js → chunk-PXDEFEG2.js} +293 -2
- package/dist/chunk-PXDEFEG2.js.map +1 -0
- package/dist/{chunk-YU4WQE5M.cjs → chunk-TAPXWP2B.cjs} +95 -377
- package/dist/chunk-TAPXWP2B.cjs.map +1 -0
- package/dist/{chunk-HXDMZQTD.cjs → chunk-TPOT575P.cjs} +29 -4
- package/dist/{chunk-IB3ZHQAX.js.map → chunk-TPOT575P.cjs.map} +1 -1
- package/dist/commands/index.cjs +2 -2
- package/dist/commands/index.d.cts +1 -1
- package/dist/commands/index.d.ts +1 -1
- package/dist/commands/index.js +1 -1
- package/dist/generation/index.cjs +932 -791
- package/dist/generation/index.cjs.map +1 -1
- package/dist/generation/index.d.cts +266 -240
- package/dist/generation/index.d.ts +266 -240
- package/dist/generation/index.js +926 -785
- package/dist/generation/index.js.map +1 -1
- package/dist/image-editor/index.cjs +266 -2
- package/dist/image-editor/index.cjs.map +1 -1
- package/dist/image-editor/index.d.cts +12 -1
- package/dist/image-editor/index.d.ts +12 -1
- package/dist/image-editor/index.js +265 -1
- package/dist/image-editor/index.js.map +1 -1
- package/dist/index.cjs +3 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +16 -16
- package/dist/spreadsheet/index.cjs +3 -3
- package/dist/spreadsheet/index.cjs.map +1 -1
- package/dist/spreadsheet/index.js +16 -16
- package/dist/video-editor/index.cjs +461 -101
- package/dist/video-editor/index.cjs.map +1 -1
- package/dist/video-editor/index.d.cts +67 -3
- package/dist/video-editor/index.d.ts +67 -3
- package/dist/video-editor/index.js +426 -66
- package/dist/video-editor/index.js.map +1 -1
- package/package.json +3 -1
- package/skills/README.md +55 -0
- package/skills/pixerate-agent-tools/SKILL.md +75 -0
- package/skills/pixerate-animation-editor/SKILL.md +155 -0
- package/skills/pixerate-animation-editor/references/commands.md +123 -0
- package/skills/pixerate-canvas/SKILL.md +230 -0
- package/skills/pixerate-chart/SKILL.md +190 -0
- package/skills/pixerate-diagram/SKILL.md +134 -0
- package/skills/pixerate-generation/SKILL.md +109 -0
- package/skills/pixerate-image-editor/SKILL.md +139 -0
- package/skills/pixerate-parameters/SKILL.md +83 -0
- package/skills/pixerate-rich-text/SKILL.md +160 -0
- package/skills/pixerate-spreadsheet/SKILL.md +146 -0
- package/skills/pixerate-vector/SKILL.md +191 -0
- package/skills/pixerate-vector/references/commands.md +138 -0
- package/skills/pixerate-video-editor/SKILL.md +173 -0
- package/dist/chunk-2CJ3LPYQ.js.map +0 -1
- package/dist/chunk-HXDMZQTD.cjs.map +0 -1
- package/dist/chunk-JQ3BB3E6.js.map +0 -1
- package/dist/chunk-UN33YOXW.cjs.map +0 -1
- package/dist/chunk-YU4WQE5M.cjs.map +0 -1
|
@@ -15,6 +15,7 @@ require('../chunk-2FHLHZAL.cjs');
|
|
|
15
15
|
var VIDEO_FITS = ["contain", "cover", "fill", "none"];
|
|
16
16
|
var VIDEO_TRANSITION_TYPES = ["crossfade", "dip", "wipe"];
|
|
17
17
|
var VIDEO_WIPE_DIRECTIONS = ["left", "right", "up", "down"];
|
|
18
|
+
var VIDEO_PLACEMENT_MODES = ["refuse", "overwrite", "insert"];
|
|
18
19
|
var VIDEO_CLIP_TYPES = ["media", "text", "color"];
|
|
19
20
|
|
|
20
21
|
// src/video-editor/keyframes.ts
|
|
@@ -348,6 +349,7 @@ function parseVideoTrack(v, path = "track") {
|
|
|
348
349
|
muted: bool(v.muted, `${path}.muted`),
|
|
349
350
|
hidden: bool(v.hidden, `${path}.hidden`),
|
|
350
351
|
locked: bool(v.locked, `${path}.locked`),
|
|
352
|
+
syncLocked: bool(v.syncLocked, `${path}.syncLocked`),
|
|
351
353
|
volume: num(v.volume, `${path}.volume`, { optional: true, min: 0, max: 4 }),
|
|
352
354
|
clips: (_nullishCoalesce(v.clips, () => ( []))).map((c, i) => parseVideoClip(c, `${path}.clips[${i}]`))
|
|
353
355
|
});
|
|
@@ -411,6 +413,7 @@ function createVideoProject(options = {}) {
|
|
|
411
413
|
// src/video-editor/timeline.ts
|
|
412
414
|
var VIDEO_TIME_EPSILON = 0.5;
|
|
413
415
|
var videoClipEnd = (clip) => clip.start + clip.duration;
|
|
416
|
+
var videoTrackSyncLocked = (track) => track.syncLocked !== false;
|
|
414
417
|
var videoClipSpeed = (clip) => clip.type === "media" ? _nullishCoalesce(clip.speed, () => ( 1)) : 1;
|
|
415
418
|
function videoClipSourceRange(clip) {
|
|
416
419
|
if (clip.type !== "media") return null;
|
|
@@ -632,6 +635,7 @@ function checkVideoProject(project) {
|
|
|
632
635
|
}
|
|
633
636
|
|
|
634
637
|
// src/video-editor/edit.ts
|
|
638
|
+
var TRACK_FIELD_KEYS = ["name", "muted", "hidden", "locked", "syncLocked", "volume"];
|
|
635
639
|
var VIDEO_EDIT_OPS = [
|
|
636
640
|
"set_settings",
|
|
637
641
|
"add_asset",
|
|
@@ -648,6 +652,12 @@ var VIDEO_EDIT_OPS = [
|
|
|
648
652
|
"delete_clips",
|
|
649
653
|
"update_clip",
|
|
650
654
|
"set_transition",
|
|
655
|
+
"slip_clip",
|
|
656
|
+
"roll_edit",
|
|
657
|
+
"slide_clip",
|
|
658
|
+
"lift_range",
|
|
659
|
+
"extract_range",
|
|
660
|
+
"split_at",
|
|
651
661
|
"set_keyframes",
|
|
652
662
|
"close_gaps",
|
|
653
663
|
"add_marker",
|
|
@@ -667,6 +677,15 @@ function freshId(prefix, taken) {
|
|
|
667
677
|
for (let n2 = 1; ; n2++) if (!used.has(`${prefix}${n2}`)) return `${prefix}${n2}`;
|
|
668
678
|
}
|
|
669
679
|
var slug = (s) => s.replace(/^.*[\\/]/, "").replace(/\.[a-z0-9]+(\?.*)?$/i, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 32);
|
|
680
|
+
function mergeSpans(spans) {
|
|
681
|
+
const out = [];
|
|
682
|
+
for (const [from, to] of [...spans].sort((a, b) => a[0] - b[0])) {
|
|
683
|
+
const last = out[out.length - 1];
|
|
684
|
+
if (last && from <= last[1] + VIDEO_TIME_EPSILON) last[1] = Math.max(last[1], to);
|
|
685
|
+
else out.push([from, to]);
|
|
686
|
+
}
|
|
687
|
+
return out;
|
|
688
|
+
}
|
|
670
689
|
function clipAnimatables(clip) {
|
|
671
690
|
const c = clip;
|
|
672
691
|
const out = [
|
|
@@ -683,6 +702,7 @@ function retimeClipKeys(clip, fn) {
|
|
|
683
702
|
}
|
|
684
703
|
}
|
|
685
704
|
var OPTIONAL_CLIP_FIELDS = ["name", "opacity", "transform", "fit", "fadeIn", "fadeOut", "transition", "in", "speed", "volume", "muted", "color", "fontSize", "fontFamily", "fontWeight", "background"];
|
|
705
|
+
var TIME_FIELDS = { slip_clip: ["delta"], roll_edit: ["delta"], slide_clip: ["delta"], lift_range: ["from", "to"], extract_range: ["from", "to"], split_at: ["at"] };
|
|
686
706
|
var Editor = (_class = class {
|
|
687
707
|
constructor(p) {;_class.prototype.__init.call(this);
|
|
688
708
|
this.p = p;
|
|
@@ -714,7 +734,7 @@ var Editor = (_class = class {
|
|
|
714
734
|
const hit = videoOverlap(track, start, start + duration, ignore);
|
|
715
735
|
if (hit) {
|
|
716
736
|
const free = nextFreeVideoSlot(track, start, duration, ignore);
|
|
717
|
-
refuse(`${what} at ${formatVideoSeconds(start)}\u2013${formatVideoSeconds(start + duration)} would overlap clip "${hit.id}" (${formatVideoSeconds(hit.start)}\u2013${formatVideoSeconds(videoClipEnd(hit))}) on track "${track.id}"; the next free place is ${formatVideoSeconds(free)}, or
|
|
737
|
+
refuse(`${what} at ${formatVideoSeconds(start)}\u2013${formatVideoSeconds(start + duration)} would overlap clip "${hit.id}" (${formatVideoSeconds(hit.start)}\u2013${formatVideoSeconds(videoClipEnd(hit))}) on track "${track.id}"; the next free place is ${formatVideoSeconds(free)}, or use mode "insert" to push later clips along or "overwrite" to replace what is there.`);
|
|
718
738
|
}
|
|
719
739
|
}
|
|
720
740
|
kindFits(track, clip) {
|
|
@@ -726,8 +746,160 @@ var Editor = (_class = class {
|
|
|
726
746
|
shift(track, from, delta, except = /* @__PURE__ */ new Set()) {
|
|
727
747
|
if (!delta) return;
|
|
728
748
|
for (const c of track.clips) if (!except.has(c.id) && c.start >= from - VIDEO_TIME_EPSILON) c.start = Math.max(0, c.start + delta);
|
|
749
|
+
track.clips.sort((a, b) => a.start - b.start);
|
|
750
|
+
}
|
|
751
|
+
get frame() {
|
|
752
|
+
return 1e3 / this.p.fps;
|
|
753
|
+
}
|
|
754
|
+
ids() {
|
|
755
|
+
return this.p.tracks.flatMap((t) => t.clips.map((c) => c.id));
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* The tracks a ripple on `sources` moves, top to bottom: the sources, and every unlocked
|
|
759
|
+
* sync-locked track when one of the sources is sync-locked.
|
|
760
|
+
*/
|
|
761
|
+
rippleTracks(sources) {
|
|
762
|
+
const set = new Set(sources);
|
|
763
|
+
if ([...set].some(videoTrackSyncLocked)) {
|
|
764
|
+
for (const t of this.p.tracks) if (videoTrackSyncLocked(t) && !t.locked) set.add(t);
|
|
765
|
+
}
|
|
766
|
+
return this.p.tracks.filter((t) => set.has(t));
|
|
767
|
+
}
|
|
768
|
+
/** The tracks a range edit works on: the named ones (unlocked), or every unlocked track. */
|
|
769
|
+
rangeTracks(ids) {
|
|
770
|
+
if (ids) {
|
|
771
|
+
if (!ids.length) refuse('"tracks" is empty; name at least one track, or leave it out for every unlocked track.');
|
|
772
|
+
const tracks = [...new Set(ids)].map((id) => this.track(id));
|
|
773
|
+
tracks.forEach((t) => this.unlocked(t));
|
|
774
|
+
return this.p.tracks.filter((t) => tracks.includes(t));
|
|
775
|
+
}
|
|
776
|
+
const open = this.p.tracks.filter((t) => !t.locked);
|
|
777
|
+
if (!open.length) refuse("Every track is locked; unlock one (update_track with locked false) first.");
|
|
778
|
+
return open;
|
|
779
|
+
}
|
|
780
|
+
range(from, to) {
|
|
781
|
+
if (!(to - from > VIDEO_TIME_EPSILON)) refuse(`"to" (${formatVideoSeconds(to)}) must be after "from" (${formatVideoSeconds(from)}).`);
|
|
782
|
+
}
|
|
783
|
+
/** Moves a media clip's in point by `delta` of timeline time (stills, titles and colours have none). */
|
|
784
|
+
moveIn(clip, delta) {
|
|
785
|
+
if (clip.type !== "media" || _optionalChain([videoAssetMap, 'call', _8 => _8(this.p), 'access', _9 => _9.get, 'call', _10 => _10(clip.asset), 'optionalAccess', _11 => _11.kind]) === "image") return;
|
|
786
|
+
clip.in = Math.max(0, (_nullishCoalesce(clip.in, () => ( 0))) + delta * videoClipSpeed(clip));
|
|
787
|
+
if (clip.in < 1e-9) delete clip.in;
|
|
788
|
+
}
|
|
789
|
+
/** Moves a clip's head `delta` later in time (start, in point and keys); the caller sets start and duration. */
|
|
790
|
+
trimHead(clip, delta) {
|
|
791
|
+
this.moveIn(clip, delta);
|
|
792
|
+
retimeClipKeys(clip, (v) => retimeVideoAnimatable(v, -delta));
|
|
793
|
+
}
|
|
794
|
+
/** Cuts `clip` at `at` (inside it): the left part keeps the id, the right part is returned. */
|
|
795
|
+
cut(track, clip, at, id) {
|
|
796
|
+
const end = videoClipEnd(clip);
|
|
797
|
+
const right = { ...structuredClone(clip), id: _nullishCoalesce(id, () => ( freshId(`${clip.id}-`, this.ids()))), start: at, duration: end - at };
|
|
798
|
+
delete right.transition;
|
|
799
|
+
delete right.fadeIn;
|
|
800
|
+
const leftValues = clipAnimatables(clip);
|
|
801
|
+
clipAnimatables(right).forEach((r, i) => {
|
|
802
|
+
const [l, rr] = splitVideoAnimatable(leftValues[i].get(), at - clip.start);
|
|
803
|
+
if (isVideoKeyframes(l)) leftValues[i].set(l);
|
|
804
|
+
if (isVideoKeyframes(rr)) r.set(rr);
|
|
805
|
+
});
|
|
806
|
+
this.moveIn(right, at - clip.start);
|
|
807
|
+
clip.duration = at - clip.start;
|
|
808
|
+
delete clip.fadeOut;
|
|
809
|
+
track.clips.push(right);
|
|
810
|
+
track.clips.sort((a, b) => a.start - b.start);
|
|
811
|
+
return right;
|
|
812
|
+
}
|
|
813
|
+
/** Cuts every clip on `tracks` that runs across `at`; returns the new right-hand parts. */
|
|
814
|
+
cutAcross(tracks, at) {
|
|
815
|
+
const made = [];
|
|
816
|
+
for (const t of tracks) for (const c of [...t.clips]) if (c.start < at - VIDEO_TIME_EPSILON && videoClipEnd(c) > at + VIDEO_TIME_EPSILON) made.push(this.cut(t, c, at));
|
|
817
|
+
return made;
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* Opens `amount` of empty time at `at` on `tracks`: clips across `at` are cut there and everything
|
|
821
|
+
* from `at` on moves later. Returns the clips made by cutting.
|
|
822
|
+
*/
|
|
823
|
+
open(tracks, at, amount, except = /* @__PURE__ */ new Set()) {
|
|
824
|
+
const made = this.cutAcross(tracks, at).map((c) => c.id);
|
|
825
|
+
for (const t of tracks) this.shift(t, at, amount, except);
|
|
826
|
+
return made;
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* Closes `[from, to)` on `tracks`, pulling everything after it earlier. Refuses, before moving
|
|
830
|
+
* anything, when a track still has a clip in that stretch (it would be pulled out of sync).
|
|
831
|
+
*/
|
|
832
|
+
close(tracks, from, to) {
|
|
833
|
+
for (const t of tracks) {
|
|
834
|
+
const hit = videoOverlap(t, from, to);
|
|
835
|
+
if (hit)
|
|
836
|
+
refuse(
|
|
837
|
+
`Rippling out ${formatVideoSeconds(from)}\u2013${formatVideoSeconds(to)} would pull track "${t.id}" out of sync: clip "${hit.id}" (${formatVideoSeconds(hit.start)}\u2013${formatVideoSeconds(videoClipEnd(hit))}) is in that stretch. Extract the range across "${t.id}" too (extract_range with its id in "tracks"), turn its sync lock off (update_track with syncLocked false) or lock it, or lift instead of rippling.`
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
for (const t of tracks) this.shift(t, to, from - to);
|
|
841
|
+
}
|
|
842
|
+
/**
|
|
843
|
+
* Empties `[from, to)` on one track, leaving a gap: clips inside go, clips across an edge are
|
|
844
|
+
* trimmed back to it, and a clip across both is cut in two around the gap.
|
|
845
|
+
*/
|
|
846
|
+
clear(track, from, to, report) {
|
|
847
|
+
for (const c of [...track.clips]) {
|
|
848
|
+
const start = c.start;
|
|
849
|
+
const end = videoClipEnd(c);
|
|
850
|
+
if (end <= from + VIDEO_TIME_EPSILON || start >= to - VIDEO_TIME_EPSILON) continue;
|
|
851
|
+
const coversHead = start >= from - VIDEO_TIME_EPSILON;
|
|
852
|
+
const coversTail = end <= to + VIDEO_TIME_EPSILON;
|
|
853
|
+
if (coversHead && coversTail) {
|
|
854
|
+
track.clips = track.clips.filter((x) => x !== c);
|
|
855
|
+
report.removed.push(c.id);
|
|
856
|
+
continue;
|
|
857
|
+
}
|
|
858
|
+
if (!coversHead && !coversTail) report.created.push(this.cut(track, c, to).id);
|
|
859
|
+
else if (coversHead) {
|
|
860
|
+
this.trimHead(c, to - start);
|
|
861
|
+
c.start = to;
|
|
862
|
+
c.duration = end - to;
|
|
863
|
+
delete c.transition;
|
|
864
|
+
report.trimmed.push(c.id);
|
|
865
|
+
continue;
|
|
866
|
+
}
|
|
867
|
+
c.duration = from - start;
|
|
868
|
+
if (!report.trimmed.includes(c.id)) report.trimmed.push(c.id);
|
|
869
|
+
}
|
|
870
|
+
track.clips.sort((a, b) => a.start - b.start);
|
|
871
|
+
}
|
|
872
|
+
/** Clamps a slip, roll or slide to `[lo, hi]`, refusing when it cannot move that way at all. */
|
|
873
|
+
clamp(delta, lo, hi, why) {
|
|
874
|
+
const applied = Math.min(Math.max(delta, Math.min(0, lo)), Math.max(0, hi));
|
|
875
|
+
if (Math.abs(delta) > VIDEO_TIME_EPSILON && Math.abs(applied) <= VIDEO_TIME_EPSILON) refuse(why());
|
|
876
|
+
return Math.abs(applied) <= VIDEO_TIME_EPSILON ? 0 : applied;
|
|
877
|
+
}
|
|
878
|
+
/** Places a clip on a track by `mode`, returning what it cut, trimmed, removed and rippled. */
|
|
879
|
+
place(track, clip, mode, outcome) {
|
|
880
|
+
if (mode === "insert") {
|
|
881
|
+
const tracks = this.rippleTracks([track]);
|
|
882
|
+
outcome.created = this.open(tracks, clip.start, clip.duration);
|
|
883
|
+
outcome.rippled = tracks.map((t) => t.id);
|
|
884
|
+
} else if (mode === "overwrite") {
|
|
885
|
+
const report = { removed: [], trimmed: [], created: [] };
|
|
886
|
+
this.clear(track, clip.start, videoClipEnd(clip), report);
|
|
887
|
+
Object.assign(outcome, report);
|
|
888
|
+
} else this.fits(track, clip.start, clip.duration, /* @__PURE__ */ new Set([clip.id]), `Clip "${clip.id}"`);
|
|
889
|
+
track.clips.push(clip);
|
|
890
|
+
track.clips.sort((a, b) => a.start - b.start);
|
|
891
|
+
}
|
|
892
|
+
placement(mode, ripple) {
|
|
893
|
+
if (mode !== void 0 && !VIDEO_PLACEMENT_MODES.includes(mode)) refuse(`"mode" must be ${VIDEO_PLACEMENT_MODES.map((m) => `"${m}"`).join(", ")}.`);
|
|
894
|
+
if (ripple && mode !== void 0 && mode !== "insert") refuse(`"ripple" means mode "insert", so it cannot go with mode "${mode}"; pass one or the other.`);
|
|
895
|
+
return _nullishCoalesce(mode, () => ( (ripple ? "insert" : "refuse")));
|
|
729
896
|
}
|
|
730
897
|
run(e) {
|
|
898
|
+
for (const k of _nullishCoalesce(TIME_FIELDS[e.op], () => ( []))) {
|
|
899
|
+
const v = e[k];
|
|
900
|
+
if (typeof v !== "number" || !Number.isFinite(v)) refuse(`"${k}" must be a number of milliseconds.`);
|
|
901
|
+
if (k !== "delta" && v < 0) refuse(`"${k}" must be a time of at least 0.`);
|
|
902
|
+
}
|
|
731
903
|
switch (e.op) {
|
|
732
904
|
case "set_settings": {
|
|
733
905
|
const { op: _op, ...settings } = e;
|
|
@@ -773,7 +945,7 @@ var Editor = (_class = class {
|
|
|
773
945
|
}
|
|
774
946
|
case "update_track": {
|
|
775
947
|
const track = this.track(e.track);
|
|
776
|
-
for (const k of Object.keys(_nullishCoalesce(e.set, () => ( {})))) if (!
|
|
948
|
+
for (const k of Object.keys(_nullishCoalesce(e.set, () => ( {})))) if (!TRACK_FIELD_KEYS.includes(k)) refuse(`"${k}" cannot be set on a track; use ${TRACK_FIELD_KEYS.join(", ")}.`);
|
|
777
949
|
Object.assign(track, e.set);
|
|
778
950
|
for (const k of _nullishCoalesce(e.remove, () => ( []))) delete track[k];
|
|
779
951
|
return this.outcomes.push({ op: e.op, track: track.id });
|
|
@@ -794,6 +966,7 @@ var Editor = (_class = class {
|
|
|
794
966
|
case "add_clip": {
|
|
795
967
|
const track = this.track(e.track);
|
|
796
968
|
this.unlocked(track);
|
|
969
|
+
const mode = this.placement(e.mode, e.ripple);
|
|
797
970
|
const draft = { ...e.clip, type: _nullishCoalesce(e.clip.type, () => ( "media")) };
|
|
798
971
|
this.kindFits(track, draft);
|
|
799
972
|
const ids = this.p.tracks.flatMap((t) => t.clips.map((c) => c.id));
|
|
@@ -811,14 +984,9 @@ var Editor = (_class = class {
|
|
|
811
984
|
const end = sortedVideoClips(track).reduce((m, c) => Math.max(m, videoClipEnd(c)), 0);
|
|
812
985
|
draft.start ??= end;
|
|
813
986
|
const clip = parseVideoClip(draft, "clip");
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
this.shift(track, clip.start, clip.duration);
|
|
818
|
-
} else this.fits(track, clip.start, clip.duration, /* @__PURE__ */ new Set(), `Clip "${clip.id}"`);
|
|
819
|
-
track.clips.push(clip);
|
|
820
|
-
track.clips.sort((a, b) => a.start - b.start);
|
|
821
|
-
return this.outcomes.push({ op: e.op, clip: clip.id, track: track.id });
|
|
987
|
+
const outcome = { op: e.op, clip: clip.id, track: track.id };
|
|
988
|
+
this.place(track, clip, mode, outcome);
|
|
989
|
+
return this.outcomes.push(outcome);
|
|
822
990
|
}
|
|
823
991
|
case "move_clip": {
|
|
824
992
|
const from = this.clip(e.clip);
|
|
@@ -826,15 +994,14 @@ var Editor = (_class = class {
|
|
|
826
994
|
const to = e.track !== void 0 ? this.track(e.track) : from.track;
|
|
827
995
|
this.unlocked(to);
|
|
828
996
|
if (to !== from.track) this.kindFits(to, from.clip);
|
|
997
|
+
const mode = this.placement(e.mode);
|
|
829
998
|
const start = _nullishCoalesce(e.start, () => ( from.clip.start));
|
|
830
|
-
|
|
999
|
+
if (start < 0) refuse("A clip cannot start before 0.");
|
|
1000
|
+
from.track.clips.splice(from.index, 1);
|
|
831
1001
|
from.clip.start = start;
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
}
|
|
836
|
-
to.clips.sort((a, b) => a.start - b.start);
|
|
837
|
-
return this.outcomes.push({ op: e.op, clip: from.clip.id, track: to.id });
|
|
1002
|
+
const outcome = { op: e.op, clip: from.clip.id, track: to.id };
|
|
1003
|
+
this.place(to, from.clip, mode, outcome);
|
|
1004
|
+
return this.outcomes.push(outcome);
|
|
838
1005
|
}
|
|
839
1006
|
case "trim_clip": {
|
|
840
1007
|
const { track, clip } = this.clip(e.clip);
|
|
@@ -854,8 +1021,17 @@ var Editor = (_class = class {
|
|
|
854
1021
|
}
|
|
855
1022
|
retimeClipKeys(clip, (v) => retimeVideoAnimatable(v, -delta));
|
|
856
1023
|
if (e.ripple) {
|
|
857
|
-
|
|
858
|
-
|
|
1024
|
+
const tracks = this.rippleTracks([track]);
|
|
1025
|
+
if (delta > 0) {
|
|
1026
|
+
clip.start += delta;
|
|
1027
|
+
clip.duration -= delta;
|
|
1028
|
+
this.close(tracks, clip.start - delta, clip.start);
|
|
1029
|
+
} else if (delta < 0) {
|
|
1030
|
+
const created = this.open(tracks, clip.start, -delta, /* @__PURE__ */ new Set([clip.id]));
|
|
1031
|
+
clip.duration -= delta;
|
|
1032
|
+
return this.outcomes.push({ op: e.op, clip: clip.id, track: track.id, rippled: tracks.map((t) => t.id), ...created.length ? { created } : {} });
|
|
1033
|
+
}
|
|
1034
|
+
return this.outcomes.push({ op: e.op, clip: clip.id, track: track.id, rippled: tracks.map((t) => t.id) });
|
|
859
1035
|
} else {
|
|
860
1036
|
if (e.to < 0) refuse("A clip cannot start before 0.");
|
|
861
1037
|
if (delta < 0) this.fits(track, e.to, -delta, /* @__PURE__ */ new Set([clip.id]), `Extending "${clip.id}"`);
|
|
@@ -868,11 +1044,22 @@ var Editor = (_class = class {
|
|
|
868
1044
|
if (clip.type === "media") {
|
|
869
1045
|
const asset = assets.get(clip.asset);
|
|
870
1046
|
const out = (_nullishCoalesce(clip.in, () => ( 0))) + duration * speed;
|
|
871
|
-
if (_optionalChain([asset, 'optionalAccess',
|
|
1047
|
+
if (_optionalChain([asset, 'optionalAccess', _12 => _12.duration]) !== void 0 && out > asset.duration + VIDEO_TIME_EPSILON)
|
|
872
1048
|
refuse(`"${clip.id}" can end at most at ${formatVideoSeconds(clip.start + (asset.duration - (_nullishCoalesce(clip.in, () => ( 0)))) / speed)}, where "${asset.id}" runs out.`);
|
|
873
1049
|
}
|
|
874
|
-
if (e.ripple)
|
|
875
|
-
|
|
1050
|
+
if (e.ripple) {
|
|
1051
|
+
const tracks = this.rippleTracks([track]);
|
|
1052
|
+
let created = [];
|
|
1053
|
+
if (e.to < end) {
|
|
1054
|
+
clip.duration = duration;
|
|
1055
|
+
this.close(tracks, e.to, end);
|
|
1056
|
+
} else if (e.to > end) {
|
|
1057
|
+
created = this.open(tracks, end, e.to - end, /* @__PURE__ */ new Set([clip.id]));
|
|
1058
|
+
clip.duration = duration;
|
|
1059
|
+
}
|
|
1060
|
+
return this.outcomes.push({ op: e.op, clip: clip.id, track: track.id, rippled: tracks.map((t) => t.id), ...created.length ? { created } : {} });
|
|
1061
|
+
}
|
|
1062
|
+
if (e.to > end) this.fits(track, end, e.to - end, /* @__PURE__ */ new Set([clip.id]), `Extending "${clip.id}"`);
|
|
876
1063
|
clip.duration = duration;
|
|
877
1064
|
}
|
|
878
1065
|
return this.outcomes.push({ op: e.op, clip: clip.id, track: track.id });
|
|
@@ -883,37 +1070,25 @@ var Editor = (_class = class {
|
|
|
883
1070
|
const end = videoClipEnd(clip);
|
|
884
1071
|
const minimum = 1e3 / this.p.fps;
|
|
885
1072
|
if (e.at < clip.start + minimum - VIDEO_TIME_EPSILON || e.at > end - minimum + VIDEO_TIME_EPSILON) refuse(`${formatVideoSeconds(e.at)} is not inside "${clip.id}" (${formatVideoSeconds(clip.start)}\u2013${formatVideoSeconds(end)}), at least a frame from either end.`);
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
const left = e.at - clip.start;
|
|
890
|
-
const right = { ...structuredClone(clip), id, start: e.at, duration: end - e.at };
|
|
891
|
-
delete right.transition;
|
|
892
|
-
delete right.fadeIn;
|
|
893
|
-
const leftValues = clipAnimatables(clip);
|
|
894
|
-
clipAnimatables(right).forEach((r, i) => {
|
|
895
|
-
const [l, rr] = splitVideoAnimatable(leftValues[i].get(), left);
|
|
896
|
-
if (isVideoKeyframes(l)) leftValues[i].set(l);
|
|
897
|
-
if (isVideoKeyframes(rr)) r.set(rr);
|
|
898
|
-
});
|
|
899
|
-
if (clip.type === "media") {
|
|
900
|
-
right.in = (_nullishCoalesce(clip.in, () => ( 0))) + left * videoClipSpeed(clip);
|
|
901
|
-
}
|
|
902
|
-
clip.duration = left;
|
|
903
|
-
delete clip.fadeOut;
|
|
904
|
-
track.clips.push(right);
|
|
905
|
-
track.clips.sort((a, b) => a.start - b.start);
|
|
906
|
-
return this.outcomes.push({ op: e.op, clips: [clip.id, id], clip: id, track: track.id });
|
|
1073
|
+
if (e.id !== void 0 && this.ids().includes(e.id)) refuse(`There is already a clip "${e.id}".`);
|
|
1074
|
+
const right = this.cut(track, clip, e.at, e.id);
|
|
1075
|
+
return this.outcomes.push({ op: e.op, clips: [clip.id, right.id], clip: right.id, track: track.id });
|
|
907
1076
|
}
|
|
908
1077
|
case "delete_clips": {
|
|
909
1078
|
const found = [...new Set(e.clips)].map((id) => this.clip(id));
|
|
910
1079
|
for (const f of found) this.unlocked(f.track);
|
|
911
|
-
for (const track of new Set(found.map((f) => f.track)))
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
1080
|
+
for (const track of new Set(found.map((f) => f.track))) track.clips = track.clips.filter((c) => !found.some((f) => f.clip === c));
|
|
1081
|
+
if (!e.ripple) return this.outcomes.push({ op: e.op, clips: found.map((f) => f.clip.id) });
|
|
1082
|
+
const rippled = /* @__PURE__ */ new Set();
|
|
1083
|
+
const groups = [...new Set(found.map((f) => f.track))].filter((t) => !videoTrackSyncLocked(t)).map((t) => [t]);
|
|
1084
|
+
if (found.some((f) => videoTrackSyncLocked(f.track))) groups.push(found.filter((f) => videoTrackSyncLocked(f.track)).map((f) => f.track));
|
|
1085
|
+
for (const sources of groups) {
|
|
1086
|
+
const tracks = this.rippleTracks(sources);
|
|
1087
|
+
tracks.forEach((t) => rippled.add(t));
|
|
1088
|
+
const spans = mergeSpans(found.filter((f) => sources.includes(f.track)).map((f) => [f.clip.start, videoClipEnd(f.clip)]));
|
|
1089
|
+
for (const [from, to] of spans.reverse()) this.close(tracks, from, to);
|
|
915
1090
|
}
|
|
916
|
-
return this.outcomes.push({ op: e.op, clips: found.map((f) => f.clip.id) });
|
|
1091
|
+
return this.outcomes.push({ op: e.op, clips: found.map((f) => f.clip.id), rippled: this.p.tracks.filter((t) => rippled.has(t)).map((t) => t.id) });
|
|
917
1092
|
}
|
|
918
1093
|
case "update_clip": {
|
|
919
1094
|
const at = this.clip(e.clip);
|
|
@@ -925,7 +1100,7 @@ var Editor = (_class = class {
|
|
|
925
1100
|
let stretch = 1;
|
|
926
1101
|
if (at.clip.type === "media" && !("duration" in set)) {
|
|
927
1102
|
const was = videoClipSpeed(at.clip);
|
|
928
|
-
const now = "speed" in set ? set.speed : _optionalChain([e, 'access',
|
|
1103
|
+
const now = "speed" in set ? set.speed : _optionalChain([e, 'access', _13 => _13.remove, 'optionalAccess', _14 => _14.includes, 'call', _15 => _15("speed")]) ? 1 : was;
|
|
929
1104
|
if (now !== was) {
|
|
930
1105
|
set.duration = at.clip.duration * was / now;
|
|
931
1106
|
stretch = was / now;
|
|
@@ -948,6 +1123,102 @@ var Editor = (_class = class {
|
|
|
948
1123
|
else clip.transition = parseVideoTransition(e.transition, "transition");
|
|
949
1124
|
return this.outcomes.push({ op: e.op, clip: clip.id, track: track.id });
|
|
950
1125
|
}
|
|
1126
|
+
case "slip_clip": {
|
|
1127
|
+
const { track, clip } = this.clip(e.clip);
|
|
1128
|
+
this.unlocked(track);
|
|
1129
|
+
if (clip.type !== "media") refuse(`Clip "${clip.id}" is a ${clip.type === "text" ? "title" : "colour"}, so it has no source to slip; only video and audio clips slip.`);
|
|
1130
|
+
const asset = this.asset(clip.asset);
|
|
1131
|
+
if (asset.kind === "image") refuse(`Clip "${clip.id}" is a still ("${asset.id}"), so it has no source to slip; only video and audio clips slip.`);
|
|
1132
|
+
const handles = videoClipHandles(clip, videoAssetMap(this.p));
|
|
1133
|
+
const { lead, tail } = videoClipOverhang(track, clip);
|
|
1134
|
+
const delta = this.clamp(
|
|
1135
|
+
e.delta,
|
|
1136
|
+
lead - handles.before,
|
|
1137
|
+
handles.after - tail,
|
|
1138
|
+
() => e.delta > 0 ? `"${clip.id}" already plays to the end of "${asset.id}"${tail ? ` (less the ${tail} ms the transition after it needs)` : ""}, so it cannot slip later; slip it earlier (a negative delta).` : `"${clip.id}" already starts at the beginning of "${asset.id}"${lead ? ` (plus the ${lead} ms its transition needs)` : ""}, so it cannot slip earlier; slip it later (a positive delta).`
|
|
1139
|
+
);
|
|
1140
|
+
this.moveIn(clip, delta);
|
|
1141
|
+
return this.outcomes.push({ op: e.op, clip: clip.id, track: track.id, delta });
|
|
1142
|
+
}
|
|
1143
|
+
case "roll_edit": {
|
|
1144
|
+
const left = this.clip(e.left);
|
|
1145
|
+
const right = this.clip(e.right);
|
|
1146
|
+
if (left.track !== right.track) refuse(`"${e.left}" is on track "${left.track.id}" and "${e.right}" on "${right.track.id}"; a roll moves the cut between two clips on one track.`);
|
|
1147
|
+
const track = left.track;
|
|
1148
|
+
this.unlocked(track);
|
|
1149
|
+
const L = left.clip;
|
|
1150
|
+
const R = right.clip;
|
|
1151
|
+
if (Math.abs(videoClipEnd(L) - R.start) > VIDEO_TIME_EPSILON) {
|
|
1152
|
+
if (Math.abs(videoClipEnd(R) - L.start) <= VIDEO_TIME_EPSILON) refuse(`"${R.id}" comes before "${L.id}"; pass left "${R.id}" and right "${L.id}".`);
|
|
1153
|
+
refuse(`"${L.id}" ends at ${formatVideoSeconds(videoClipEnd(L))} but "${R.id}" starts at ${formatVideoSeconds(R.start)}; a roll needs two clips that meet at a cut (use trim_clip to move one edge).`);
|
|
1154
|
+
}
|
|
1155
|
+
const assets = videoAssetMap(this.p);
|
|
1156
|
+
const lo = Math.max(-(L.duration - this.frame), -(videoClipHandles(R, assets).before - videoClipOverhang(track, R).lead));
|
|
1157
|
+
const hi = Math.min(R.duration - this.frame, videoClipHandles(L, assets).after - videoClipOverhang(track, L).tail);
|
|
1158
|
+
const delta = this.clamp(
|
|
1159
|
+
e.delta,
|
|
1160
|
+
lo,
|
|
1161
|
+
hi,
|
|
1162
|
+
() => e.delta > 0 ? `The cut cannot move later: ${R.duration - this.frame < 1 ? `"${R.id}" is already a frame long` : `"${L.id}" has no media after its out point`}. Roll it earlier (a negative delta).` : `The cut cannot move earlier: ${L.duration - this.frame < 1 ? `"${L.id}" is already a frame long` : `"${R.id}" has no media before its in point`}. Roll it later (a positive delta).`
|
|
1163
|
+
);
|
|
1164
|
+
L.duration += delta;
|
|
1165
|
+
R.start += delta;
|
|
1166
|
+
R.duration -= delta;
|
|
1167
|
+
this.trimHead(R, delta);
|
|
1168
|
+
return this.outcomes.push({ op: e.op, clip: R.id, clips: [L.id, R.id], track: track.id, delta });
|
|
1169
|
+
}
|
|
1170
|
+
case "slide_clip": {
|
|
1171
|
+
const { track, clip } = this.clip(e.clip);
|
|
1172
|
+
this.unlocked(track);
|
|
1173
|
+
const before = videoClipBefore(track, clip);
|
|
1174
|
+
const after = videoClipAfter(track, clip);
|
|
1175
|
+
if (!before && !after) refuse(`"${clip.id}" has no clip right before or after it on track "${track.id}", so there is nothing to slide against; use move_clip.`);
|
|
1176
|
+
const assets = videoAssetMap(this.p);
|
|
1177
|
+
const end = videoClipEnd(clip);
|
|
1178
|
+
const others = track.clips.filter((c) => c !== clip);
|
|
1179
|
+
const room = {
|
|
1180
|
+
earlier: before ? before.duration - this.frame : clip.start - others.filter((c) => videoClipEnd(c) <= clip.start + VIDEO_TIME_EPSILON).reduce((m, c) => Math.max(m, videoClipEnd(c)), 0),
|
|
1181
|
+
later: after ? after.duration - this.frame : others.filter((c) => c.start >= end - VIDEO_TIME_EPSILON).reduce((m, c) => Math.min(m, c.start - end), Infinity)
|
|
1182
|
+
};
|
|
1183
|
+
const lo = -Math.min(room.earlier, after ? videoClipHandles(after, assets).before - videoClipOverhang(track, after).lead : Infinity);
|
|
1184
|
+
const hi = Math.min(room.later, before ? videoClipHandles(before, assets).after - videoClipOverhang(track, before).tail : Infinity);
|
|
1185
|
+
const delta = this.clamp(
|
|
1186
|
+
e.delta,
|
|
1187
|
+
lo,
|
|
1188
|
+
hi,
|
|
1189
|
+
() => e.delta > 0 ? `"${clip.id}" cannot slide later: ${room.later < 1 ? after ? `"${after.id}" is already a frame long` : "the next clip is right after it" : `"${before.id}" has no media after its out point to fill in behind it`}. Slide it earlier (a negative delta).` : `"${clip.id}" cannot slide earlier: ${room.earlier < 1 ? before ? `"${before.id}" is already a frame long` : "it is against the clip before it or the start" : `"${after.id}" has no media before its in point to fill in behind it`}. Slide it later (a positive delta).`
|
|
1190
|
+
);
|
|
1191
|
+
clip.start += delta;
|
|
1192
|
+
if (before) before.duration += delta;
|
|
1193
|
+
if (after) {
|
|
1194
|
+
after.start += delta;
|
|
1195
|
+
after.duration -= delta;
|
|
1196
|
+
this.trimHead(after, delta);
|
|
1197
|
+
}
|
|
1198
|
+
track.clips.sort((a, b) => a.start - b.start);
|
|
1199
|
+
return this.outcomes.push({ op: e.op, clip: clip.id, clips: [_optionalChain([before, 'optionalAccess', _16 => _16.id]), clip.id, _optionalChain([after, 'optionalAccess', _17 => _17.id])].filter((x) => !!x), track: track.id, delta });
|
|
1200
|
+
}
|
|
1201
|
+
case "lift_range":
|
|
1202
|
+
case "extract_range": {
|
|
1203
|
+
this.range(e.from, e.to);
|
|
1204
|
+
const tracks = this.rangeTracks(e.tracks);
|
|
1205
|
+
const report = { removed: [], trimmed: [], created: [] };
|
|
1206
|
+
for (const t of tracks) this.clear(t, e.from, e.to, report);
|
|
1207
|
+
const outcome = { op: e.op, ...report };
|
|
1208
|
+
if (e.op === "extract_range") {
|
|
1209
|
+
const rippled = this.rippleTracks(tracks);
|
|
1210
|
+
this.close(rippled, e.from, e.to);
|
|
1211
|
+
outcome.rippled = rippled.map((t) => t.id);
|
|
1212
|
+
}
|
|
1213
|
+
return this.outcomes.push(outcome);
|
|
1214
|
+
}
|
|
1215
|
+
case "split_at": {
|
|
1216
|
+
const tracks = this.rangeTracks(e.tracks);
|
|
1217
|
+
const lefts = tracks.flatMap((t) => t.clips.filter((c) => c.start < e.at - VIDEO_TIME_EPSILON && videoClipEnd(c) > e.at + VIDEO_TIME_EPSILON).map((c) => c.id));
|
|
1218
|
+
if (!lefts.length) refuse(`No clip runs across ${formatVideoSeconds(e.at)} on ${e.tracks ? `track${tracks.length > 1 ? "s" : ""} ${tracks.map((t) => `"${t.id}"`).join(", ")}` : "the unlocked tracks"}, so there is nothing to split there.`);
|
|
1219
|
+
const created = this.cutAcross(tracks, e.at).map((c) => c.id);
|
|
1220
|
+
return this.outcomes.push({ op: e.op, clips: lefts, created });
|
|
1221
|
+
}
|
|
951
1222
|
case "set_keyframes": {
|
|
952
1223
|
const { track, clip } = this.clip(e.clip);
|
|
953
1224
|
this.unlocked(track);
|
|
@@ -956,7 +1227,7 @@ var Editor = (_class = class {
|
|
|
956
1227
|
const limits = VIDEO_KEYFRAME_LIMITS[e.property];
|
|
957
1228
|
const transform = VIDEO_TRANSFORM_PROPERTIES.includes(e.property);
|
|
958
1229
|
const c = clip;
|
|
959
|
-
const current = transform ? _optionalChain([c, 'access',
|
|
1230
|
+
const current = transform ? _optionalChain([c, 'access', _18 => _18.transform, 'optionalAccess', _19 => _19[e.property]]) : c[e.property];
|
|
960
1231
|
let value;
|
|
961
1232
|
if (e.keys === null) {
|
|
962
1233
|
const v = videoValueAt(current, 0, limits.default);
|
|
@@ -1038,7 +1309,7 @@ function applyVideoEdits(project, edits) {
|
|
|
1038
1309
|
editor.run(edit);
|
|
1039
1310
|
} catch (error) {
|
|
1040
1311
|
const message = error.message;
|
|
1041
|
-
return { ok: false, error: `${list.length > 1 ? `op ${i + 1} (${_optionalChain([edit, 'optionalAccess',
|
|
1312
|
+
return { ok: false, error: `${list.length > 1 ? `op ${i + 1} (${_optionalChain([edit, 'optionalAccess', _20 => _20.op])}): ` : ""}${message}${/Nothing was changed\.$/.test(message) ? "" : " Nothing was changed."}` };
|
|
1042
1313
|
}
|
|
1043
1314
|
}
|
|
1044
1315
|
let next;
|
|
@@ -1057,7 +1328,7 @@ function applyVideoEdits(project, edits) {
|
|
|
1057
1328
|
|
|
1058
1329
|
// src/video-editor/evaluate.ts
|
|
1059
1330
|
function videoTransformAt(transform, clipTime = 0) {
|
|
1060
|
-
return { x: videoValueAt(_optionalChain([transform, 'optionalAccess',
|
|
1331
|
+
return { x: videoValueAt(_optionalChain([transform, 'optionalAccess', _21 => _21.x]), clipTime, 0), y: videoValueAt(_optionalChain([transform, 'optionalAccess', _22 => _22.y]), clipTime, 0), scale: videoValueAt(_optionalChain([transform, 'optionalAccess', _23 => _23.scale]), clipTime, 1), rotation: videoValueAt(_optionalChain([transform, 'optionalAccess', _24 => _24.rotation]), clipTime, 0) };
|
|
1061
1332
|
}
|
|
1062
1333
|
function placeVideoRect(frame, size, fit = "contain", transform = {}, clipTime = 0) {
|
|
1063
1334
|
let w = size.width;
|
|
@@ -1097,7 +1368,7 @@ function activeOnTrack(track, time) {
|
|
|
1097
1368
|
const before = clip.transition ? videoClipBefore(track, clip) : null;
|
|
1098
1369
|
const lead = before ? clip.transition.duration / 2 : 0;
|
|
1099
1370
|
const next = videoClipAfter(track, clip);
|
|
1100
|
-
const tail = _optionalChain([next, 'optionalAccess',
|
|
1371
|
+
const tail = _optionalChain([next, 'optionalAccess', _25 => _25.transition]) ? next.transition.duration / 2 : 0;
|
|
1101
1372
|
if (time < clip.start - lead || time >= videoClipEnd(clip) + tail) continue;
|
|
1102
1373
|
const a = { clip };
|
|
1103
1374
|
if (lead && time < clip.start + lead) a.incoming = { progress: clamp01((time - (clip.start - lead)) / (2 * lead)), type: clip.transition.type, transition: clip.transition };
|
|
@@ -1114,8 +1385,8 @@ function videoFrameAt(project, time) {
|
|
|
1114
1385
|
for (let ti = project.tracks.length - 1; ti >= 0; ti--) {
|
|
1115
1386
|
const track = project.tracks[ti];
|
|
1116
1387
|
for (const { clip, incoming, outgoing } of activeOnTrack(track, time)) {
|
|
1117
|
-
if (_optionalChain([incoming, 'optionalAccess',
|
|
1118
|
-
if (_optionalChain([outgoing, 'optionalAccess',
|
|
1388
|
+
if (_optionalChain([incoming, 'optionalAccess', _26 => _26.type]) === "dip" && incoming.progress < 0.5) continue;
|
|
1389
|
+
if (_optionalChain([outgoing, 'optionalAccess', _27 => _27.type]) === "dip" && outgoing.progress >= 0.5) continue;
|
|
1119
1390
|
const fade = fadeGain(clip, Math.min(Math.max(time, clip.start), videoClipEnd(clip)));
|
|
1120
1391
|
const level = (t, into) => t.type === "dip" ? Math.abs(1 - 2 * t.progress) : into ? t.progress : 1 - t.progress;
|
|
1121
1392
|
const mix = (incoming ? level(incoming, true) : 1) * (outgoing ? level(outgoing, false) : 1);
|
|
@@ -1137,11 +1408,11 @@ function videoFrameAt(project, time) {
|
|
|
1137
1408
|
else if (incoming.type === "wipe") wipe = { direction: _nullishCoalesce(incoming.transition.direction, () => ( "right")), progress: incoming.progress };
|
|
1138
1409
|
else dip = { color: _nullishCoalesce(incoming.transition.color, () => ( "#000000")), opacity: (1 - incoming.progress) * 2 };
|
|
1139
1410
|
}
|
|
1140
|
-
if (_optionalChain([outgoing, 'optionalAccess',
|
|
1411
|
+
if (_optionalChain([outgoing, 'optionalAccess', _28 => _28.type]) === "dip") dip = { color: _nullishCoalesce(outgoing.next.transition.color, () => ( "#000000")), opacity: outgoing.progress * 2 };
|
|
1141
1412
|
const size = clip.type === "media" ? { width: _nullishCoalesce(asset.width, () => ( project.width)), height: _nullishCoalesce(asset.height, () => ( project.height)) } : { width: project.width, height: project.height };
|
|
1142
1413
|
const fit = _nullishCoalesce(clip.fit, () => ( (clip.type === "media" ? "contain" : "fill")));
|
|
1143
1414
|
const rect = placeVideoRect(project, size, clip.type === "text" ? "fill" : fit, clip.transform, clipTime);
|
|
1144
|
-
const layer = { clip: clip.id, track: track.id, type: clip.type, opacity: clamp01(opacity), rect, rotation: videoValueAt(_optionalChain([clip, 'access',
|
|
1415
|
+
const layer = { clip: clip.id, track: track.id, type: clip.type, opacity: clamp01(opacity), rect, rotation: videoValueAt(_optionalChain([clip, 'access', _29 => _29.transform, 'optionalAccess', _30 => _30.rotation]), clipTime, 0) };
|
|
1145
1416
|
if (wipe) layer.wipe = wipe;
|
|
1146
1417
|
if (clip.type === "media") Object.assign(layer, { asset: asset.id, assetKind: asset.kind, src: asset.src, sourceTime });
|
|
1147
1418
|
if (clip.type === "text") layer.text = videoTextStyle(clip);
|
|
@@ -1286,7 +1557,7 @@ function drawVideoFrame(ctx, frame, sourceFor, scale = 1) {
|
|
|
1286
1557
|
}
|
|
1287
1558
|
|
|
1288
1559
|
// src/video-editor/export.ts
|
|
1289
|
-
var reelName = (asset) => (_nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess',
|
|
1560
|
+
var reelName = (asset) => (_nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess', _31 => _31.name]), () => ( _optionalChain([asset, 'optionalAccess', _32 => _32.id]))), () => ( "AX"))).replace(/\.[a-z0-9]+$/i, "").replace(/[^A-Za-z0-9]/g, "").toUpperCase().slice(0, 8) || "AX";
|
|
1290
1561
|
function toVideoEdl(project, options = {}) {
|
|
1291
1562
|
const track = options.track !== void 0 ? project.tracks.find((t) => t.id === options.track) : [...project.tracks].reverse().find((t) => t.kind === "video");
|
|
1292
1563
|
if (!track) throw new Error(options.track !== void 0 ? `There is no track "${options.track}".` : "The project has no video track.");
|
|
@@ -1319,7 +1590,7 @@ function toVideoEdl(project, options = {}) {
|
|
|
1319
1590
|
lines.push(row(n2, reel, `${t.type === "wipe" ? "W001" : "D"} ${frames}`, sourceAt(clip, recIn), sourceAt(clip, recOut), recIn, recOut));
|
|
1320
1591
|
} else lines.push(row(n2, reel, "C", sourceAt(clip, recIn), sourceAt(clip, recOut), recIn, recOut));
|
|
1321
1592
|
if (clip.type === "media" && videoClipSpeed(clip) !== 1) lines.push(`M2 ${reel.padEnd(8)} ${(videoClipSpeed(clip) * fps).toFixed(1).padStart(6)} ${tc(sourceAt(clip, recIn))}`);
|
|
1322
|
-
if (clip.type === "media") lines.push(`* FROM CLIP NAME: ${_nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess',
|
|
1593
|
+
if (clip.type === "media") lines.push(`* FROM CLIP NAME: ${_nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess', _33 => _33.name]), () => ( _optionalChain([asset, 'optionalAccess', _34 => _34.src]))), () => ( clip.asset))}`);
|
|
1323
1594
|
if (clip.type === "text") lines.push(`* TITLE: ${clip.text.replace(/\n/g, " ")}`);
|
|
1324
1595
|
if (clip.type === "color") lines.push(`* COLOR: ${clip.color}`);
|
|
1325
1596
|
lines.push("");
|
|
@@ -1401,7 +1672,7 @@ function toVideoFfmpeg(project, options = {}) {
|
|
|
1401
1672
|
if (lead) chain2.push(`afade=t=in:st=0:d=${sec(2 * lead)}`);
|
|
1402
1673
|
if (tail) chain2.push(`afade=t=out:st=${sec(span - 2 * tail)}:d=${sec(2 * tail)}`);
|
|
1403
1674
|
if (before2 && clip.transition.type === "dip") chain2.push(`afade=t=in:st=0:d=${sec(clip.transition.duration / 2)}`);
|
|
1404
|
-
const after = sortedVideoClips(track).find((c) => c !== clip && Math.abs(c.start - end) < 0.5 && _optionalChain([c, 'access',
|
|
1675
|
+
const after = sortedVideoClips(track).find((c) => c !== clip && Math.abs(c.start - end) < 0.5 && _optionalChain([c, 'access', _35 => _35.transition, 'optionalAccess', _36 => _36.type]) === "dip");
|
|
1405
1676
|
if (after) chain2.push(`afade=t=out:st=${sec(span - after.transition.duration / 2)}:d=${sec(after.transition.duration / 2)}`);
|
|
1406
1677
|
if (clip.fadeIn) chain2.push(`afade=t=in:st=${sec(lead)}:d=${sec(clip.fadeIn)}`);
|
|
1407
1678
|
if (clip.fadeOut) chain2.push(`afade=t=out:st=${sec(lead + clip.duration - clip.fadeOut)}:d=${sec(clip.fadeOut)}`);
|
|
@@ -1415,7 +1686,7 @@ function toVideoFfmpeg(project, options = {}) {
|
|
|
1415
1686
|
const onTimeline = `(t*1000-${Math.round(clip.start)})`;
|
|
1416
1687
|
const onChain = (v) => `(${v}*1000${lead ? `-${Math.round(lead)}` : ""})`;
|
|
1417
1688
|
const tf = clip.transform;
|
|
1418
|
-
const moving = VIDEO_TRANSFORM_PROPERTIES.some((p) => isVideoKeyframes(_optionalChain([tf, 'optionalAccess',
|
|
1689
|
+
const moving = VIDEO_TRANSFORM_PROPERTIES.some((p) => isVideoKeyframes(_optionalChain([tf, 'optionalAccess', _37 => _37[p]])));
|
|
1419
1690
|
if (clip.type === "text" && options.titleImage) {
|
|
1420
1691
|
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 })]);
|
|
1421
1692
|
index = inputs.length - 1;
|
|
@@ -1424,7 +1695,7 @@ function toVideoFfmpeg(project, options = {}) {
|
|
|
1424
1695
|
const op = isVideoKeyframes(clip.opacity) ? `clip(${expr(clip.opacity, onTimeline, 1, "opacity")},0,1)` : String(_nullishCoalesce(clip.opacity, () => ( 1)));
|
|
1425
1696
|
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("*");
|
|
1426
1697
|
const coord = (axis) => {
|
|
1427
|
-
const v = _optionalChain([tf, 'optionalAccess',
|
|
1698
|
+
const v = _optionalChain([tf, 'optionalAccess', _38 => _38[axis]]);
|
|
1428
1699
|
const base = `(${axis === "x" ? "w-text_w" : "h-text_h"})/2`;
|
|
1429
1700
|
return isVideoKeyframes(v) ? `${axis}='${base}+(${expr(v, onTimeline, 0, "position")})'` : `${axis}=${base}+${_nullishCoalesce(v, () => ( 0))}`;
|
|
1430
1701
|
};
|
|
@@ -1432,7 +1703,7 @@ function toVideoFfmpeg(project, options = {}) {
|
|
|
1432
1703
|
const opts = [
|
|
1433
1704
|
`text=${ffmpegFilterValue(clip.text)}`,
|
|
1434
1705
|
options.fontFile ? `fontfile=${ffmpegFilterValue(options.fontFile)}` : `font=${ffmpegFilterValue(_nullishCoalesce(clip.fontFamily, () => ( "sans-serif")))}`,
|
|
1435
|
-
isVideoKeyframes(_optionalChain([tf, 'optionalAccess',
|
|
1706
|
+
isVideoKeyframes(_optionalChain([tf, 'optionalAccess', _39 => _39.scale])) ? `fontsize='${fontSize}*(${expr(tf.scale, onTimeline, 1, "scale")})'` : `fontsize=${Math.round(fontSize * (_nullishCoalesce(_optionalChain([tf, 'optionalAccess', _40 => _40.scale]), () => ( 1))))}`,
|
|
1436
1707
|
`fontcolor=${color(_nullishCoalesce(clip.color, () => ( "#ffffff")))}`,
|
|
1437
1708
|
coord("x"),
|
|
1438
1709
|
coord("y"),
|
|
@@ -1440,7 +1711,7 @@ function toVideoFfmpeg(project, options = {}) {
|
|
|
1440
1711
|
...clip.background ? ["box=1", `boxcolor=${color(clip.background)}`, `boxborderw=${Math.round((_nullishCoalesce(clip.fontSize, () => ( 64))) / 2)}`] : [],
|
|
1441
1712
|
`enable='between(t,${sec(clip.start)},${sec(end)})'`
|
|
1442
1713
|
];
|
|
1443
|
-
if (_optionalChain([tf, 'optionalAccess',
|
|
1714
|
+
if (_optionalChain([tf, 'optionalAccess', _41 => _41.rotation])) notes.push(`Title "${clip.id}" is drawn without its rotation (drawtext cannot rotate).`);
|
|
1444
1715
|
graph.push(`[${cur}]drawtext=${opts.join(":")}[${next}]`);
|
|
1445
1716
|
cur = next;
|
|
1446
1717
|
continue;
|
|
@@ -1456,7 +1727,7 @@ function toVideoFfmpeg(project, options = {}) {
|
|
|
1456
1727
|
source = `[${index}:v]`;
|
|
1457
1728
|
chain.push(`setpts=${videoClipSpeed(clip) !== 1 ? `(PTS-STARTPTS)/${videoClipSpeed(clip)}` : "PTS-STARTPTS"}`, `fps=${fps}`, `scale=${even(rect.width)}:${even(rect.height)}`);
|
|
1458
1729
|
}
|
|
1459
|
-
if (isVideoKeyframes(_optionalChain([placed, 'optionalAccess',
|
|
1730
|
+
if (isVideoKeyframes(_optionalChain([placed, 'optionalAccess', _42 => _42.scale]))) {
|
|
1460
1731
|
const s = expr(placed.scale, onChain("t"), 1, "scale");
|
|
1461
1732
|
chain.push(`scale=w='max(2,trunc(${even(rect.width)}*(${s})/2)*2)':h='max(2,trunc(${even(rect.height)}*(${s})/2)*2)':eval=frame`);
|
|
1462
1733
|
}
|
|
@@ -1473,14 +1744,14 @@ function toVideoFfmpeg(project, options = {}) {
|
|
|
1473
1744
|
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}))`;
|
|
1474
1745
|
chain.push(`geq=lum='lum(X,Y)':cb='cb(X,Y)':cr='cr(X,Y)':a='if(${test},alpha(X,Y),0)'`);
|
|
1475
1746
|
}
|
|
1476
|
-
const rotation = _optionalChain([placed, 'optionalAccess',
|
|
1747
|
+
const rotation = _optionalChain([placed, 'optionalAccess', _43 => _43.rotation]);
|
|
1477
1748
|
if (isVideoKeyframes(rotation)) chain.push(`rotate=a='(${expr(rotation, onChain("t"), 0, "rotation")})*PI/180':c=none:ow='hypot(iw,ih)':oh=ow`);
|
|
1478
1749
|
else if (rotation) chain.push(`rotate=${rotation}*PI/180:c=none:ow=rotw(${rotation}*PI/180):oh=roth(${rotation}*PI/180)`);
|
|
1479
1750
|
chain.push(`setpts=PTS+${sec(t0)}/TB`);
|
|
1480
1751
|
const cx = rect.x + rect.width / 2;
|
|
1481
1752
|
const cy = rect.y + rect.height / 2;
|
|
1482
1753
|
const at = (axis, c) => {
|
|
1483
|
-
const v = _optionalChain([placed, 'optionalAccess',
|
|
1754
|
+
const v = _optionalChain([placed, 'optionalAccess', _44 => _44[axis]]);
|
|
1484
1755
|
return isVideoKeyframes(v) ? `${axis}='${Math.round(axis === "x" ? project.width / 2 : project.height / 2)}+(${expr(v, onTimeline, 0, "position")})-${axis === "x" ? "w" : "h"}/2'` : `${axis}='${Math.round(c)}-${axis === "x" ? "w" : "h"}/2'`;
|
|
1485
1756
|
};
|
|
1486
1757
|
graph.push(`${source.startsWith("[") ? source : `${source},`}${chain.join(",")}[l${next}]`);
|
|
@@ -1498,7 +1769,7 @@ function toVideoFfmpeg(project, options = {}) {
|
|
|
1498
1769
|
}
|
|
1499
1770
|
graph.push(`[${cur}]format=yuv420p[vout]`);
|
|
1500
1771
|
if (audio.length) graph.push(`${audio.join("")}amix=inputs=${audio.length}:normalize=0:duration=longest,apad,atrim=0:${sec(D)}[aout]`);
|
|
1501
|
-
if (project.tracks.some((t) => t.clips.some((c) => c.type === "media" && _optionalChain([assets, 'access',
|
|
1772
|
+
if (project.tracks.some((t) => t.clips.some((c) => c.type === "media" && _optionalChain([assets, 'access', _45 => _45.get, 'call', _46 => _46(c.asset), 'optionalAccess', _47 => _47.kind]) === "video" && _optionalChain([assets, 'access', _48 => _48.get, 'call', _49 => _49(c.asset), 'optionalAccess', _50 => _50.hasAudio]) === void 0)))
|
|
1502
1773
|
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.');
|
|
1503
1774
|
if (animated.size)
|
|
1504
1775
|
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)" : ""}.`);
|
|
@@ -1532,7 +1803,7 @@ var VideoPreviewEngine = (_class2 = class {
|
|
|
1532
1803
|
this.source = source;
|
|
1533
1804
|
this.options = options;
|
|
1534
1805
|
this.canvas = canvas;
|
|
1535
|
-
this.ctx = options.context !== void 0 ? options.context : _nullishCoalesce(_optionalChain([canvas, 'access',
|
|
1806
|
+
this.ctx = options.context !== void 0 ? options.context : _nullishCoalesce(_optionalChain([canvas, 'access', _51 => _51.getContext, 'optionalCall', _52 => _52("2d")]), () => ( null));
|
|
1536
1807
|
this.unsubscribe = source.subscribe(() => this.update());
|
|
1537
1808
|
this.update();
|
|
1538
1809
|
}
|
|
@@ -1580,8 +1851,8 @@ var VideoPreviewEngine = (_class2 = class {
|
|
|
1580
1851
|
entry.media.pause();
|
|
1581
1852
|
entry.media.removeEventListener("seeked", entry.onReady);
|
|
1582
1853
|
entry.media.removeEventListener("loadeddata", entry.onReady);
|
|
1583
|
-
_optionalChain([entry, 'access',
|
|
1584
|
-
_optionalChain([entry, 'access',
|
|
1854
|
+
_optionalChain([entry, 'access', _53 => _53.media, 'access', _54 => _54.removeAttribute, 'optionalCall', _55 => _55("src")]);
|
|
1855
|
+
_optionalChain([entry, 'access', _56 => _56.media, 'access', _57 => _57.load, 'optionalCall', _58 => _58()]);
|
|
1585
1856
|
this.media.delete(entry.clip);
|
|
1586
1857
|
}
|
|
1587
1858
|
/** Brings the media elements in step with the editor and draws the frame. */
|
|
@@ -1614,7 +1885,7 @@ var VideoPreviewEngine = (_class2 = class {
|
|
|
1614
1885
|
for (const layer of frame.layers) {
|
|
1615
1886
|
if (layer.type !== "media" || layer.assetKind !== "video" || !layer.src) continue;
|
|
1616
1887
|
const sound = sounding.get(layer.clip);
|
|
1617
|
-
sync(layer.clip, layer.src, "video", _nullishCoalesce(layer.sourceTime, () => ( 0)), _nullishCoalesce(_optionalChain([sound, 'optionalAccess',
|
|
1888
|
+
sync(layer.clip, layer.src, "video", _nullishCoalesce(layer.sourceTime, () => ( 0)), _nullishCoalesce(_optionalChain([sound, 'optionalAccess', _59 => _59.speed]), () => ( 1)), sound ? sound.gain : null);
|
|
1618
1889
|
sounding.delete(layer.clip);
|
|
1619
1890
|
}
|
|
1620
1891
|
for (const a of sounding.values()) sync(a.clip, a.src, "audio", a.sourceTime, a.speed, a.gain);
|
|
@@ -1647,7 +1918,7 @@ var VideoPreviewEngine = (_class2 = class {
|
|
|
1647
1918
|
if (this.canvas.width !== w) this.canvas.width = w;
|
|
1648
1919
|
if (this.canvas.height !== h) this.canvas.height = h;
|
|
1649
1920
|
drawVideoFrame(this.ctx, frame, this.sourceFor, resolution);
|
|
1650
|
-
_optionalChain([this, 'access',
|
|
1921
|
+
_optionalChain([this, 'access', _60 => _60.options, 'access', _61 => _61.onFrame, 'optionalCall', _62 => _62(frame)]);
|
|
1651
1922
|
}
|
|
1652
1923
|
/** Stops following the editor and releases every media element. */
|
|
1653
1924
|
destroy() {
|
|
@@ -1754,7 +2025,7 @@ var VideoEditorController = (_class3 = class {
|
|
|
1754
2025
|
this.applying = false;
|
|
1755
2026
|
}
|
|
1756
2027
|
this.notify();
|
|
1757
|
-
_optionalChain([this, 'access',
|
|
2028
|
+
_optionalChain([this, 'access', _63 => _63.onChange, 'optionalCall', _64 => _64(project)]);
|
|
1758
2029
|
}
|
|
1759
2030
|
commit(label, next) {
|
|
1760
2031
|
const before = this.project;
|
|
@@ -1818,7 +2089,7 @@ var VideoEditorController = (_class3 = class {
|
|
|
1818
2089
|
}
|
|
1819
2090
|
/** Selects clips (unknown ids are dropped). `add` toggles them into the current selection. */
|
|
1820
2091
|
select(clips, options = {}) {
|
|
1821
|
-
const known = _nullishCoalesce(_optionalChain([clips, 'optionalAccess',
|
|
2092
|
+
const known = _nullishCoalesce(_optionalChain([clips, 'optionalAccess', _65 => _65.filter, 'call', _66 => _66((id) => findVideoClip(this.project, id))]), () => ( []));
|
|
1822
2093
|
let next;
|
|
1823
2094
|
if (options.add) {
|
|
1824
2095
|
next = [...this.selection];
|
|
@@ -1914,7 +2185,17 @@ var CLIP_ID = { type: "string", minLength: 1, description: 'A clip id from get_v
|
|
|
1914
2185
|
var TRACK_ID = { type: "string", minLength: 1, description: 'A track id from get_video_project (e.g. "v1" or "a1").' };
|
|
1915
2186
|
var ASSET_ID = { type: "string", minLength: 1, description: "An asset id from get_video_project." };
|
|
1916
2187
|
var COLOR = (what) => ({ type: "string", minLength: 1, description: `${what}: a CSS colour such as "#ffcc00".` });
|
|
1917
|
-
var RIPPLE = {
|
|
2188
|
+
var RIPPLE = {
|
|
2189
|
+
type: "boolean",
|
|
2190
|
+
description: "Push (or pull) later material along by the change, keeping the cuts after it in sync (default false: refuse if it would overlap). When the track is sync-locked (the default), every other unlocked sync-locked track moves too: clips across the edit point are cut when time opens, and closing time is refused if another sync-locked track has a clip in that stretch."
|
|
2191
|
+
};
|
|
2192
|
+
var MODE = {
|
|
2193
|
+
type: "string",
|
|
2194
|
+
enum: [...VIDEO_PLACEMENT_MODES],
|
|
2195
|
+
description: 'What to do with clips already where it lands: "refuse" (default) fails with the next free place; "overwrite" cuts, trims or removes what is underneath on that track; "insert" cuts everything across the start and pushes later material along by its length on this track and every sync-locked track.'
|
|
2196
|
+
};
|
|
2197
|
+
var DELTA = (what) => ({ type: "number", description: `${what}, in milliseconds of timeline time: positive is later, negative earlier. It is clamped to what the media and neighbours allow; the result says how far it went.` });
|
|
2198
|
+
var RANGE_TRACKS = { type: "array", items: TRACK_ID, minItems: 1, uniqueItems: true, description: "Tracks to work on (default: every unlocked track). Locked tracks are refused." };
|
|
1918
2199
|
var TRANSFORM = object(
|
|
1919
2200
|
{
|
|
1920
2201
|
x: { type: "number", description: "Offset of the centre to the right of the frame's centre, in pixels (default 0)." },
|
|
@@ -1982,6 +2263,7 @@ var TRACK_FIELDS = {
|
|
|
1982
2263
|
muted: { type: "boolean", description: "Silence every clip on it." },
|
|
1983
2264
|
hidden: { type: "boolean", description: "Leave its pictures out of the frame." },
|
|
1984
2265
|
locked: { type: "boolean", description: "Refuse edits to its clips." },
|
|
2266
|
+
syncLocked: { type: "boolean", description: "Move with ripple edits on other sync-locked tracks so it stays in sync (default true). Turn off for a track that should stay put, e.g. a music bed." },
|
|
1985
2267
|
volume: { type: "number", minimum: 0, maximum: 4, description: "Gain for every clip on it (1 = unchanged)." }
|
|
1986
2268
|
};
|
|
1987
2269
|
var ASSET_FIELDS = {
|
|
@@ -2042,6 +2324,7 @@ function describeVideoProject(project) {
|
|
|
2042
2324
|
...t.muted ? { muted: true } : {},
|
|
2043
2325
|
...t.hidden ? { hidden: true } : {},
|
|
2044
2326
|
...t.locked ? { locked: true } : {},
|
|
2327
|
+
...t.syncLocked === false ? { syncLocked: false } : {},
|
|
2045
2328
|
...t.volume !== void 0 ? { volume: t.volume } : {},
|
|
2046
2329
|
clips: sortedVideoClips(t).map((c) => ({ ...c, end: videoClipEnd(c), ...c.type === "media" ? { source: videoClipSourceRange(c) } : {} }))
|
|
2047
2330
|
})),
|
|
@@ -2070,9 +2353,15 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
2070
2353
|
}
|
|
2071
2354
|
return null;
|
|
2072
2355
|
};
|
|
2356
|
+
const placed = (o) => ({
|
|
2357
|
+
..._optionalChain([o, 'access', _67 => _67.created, 'optionalAccess', _68 => _68.length]) ? { created: o.created } : {},
|
|
2358
|
+
..._optionalChain([o, 'access', _69 => _69.trimmed, 'optionalAccess', _70 => _70.length]) ? { trimmed: o.trimmed } : {},
|
|
2359
|
+
..._optionalChain([o, 'access', _71 => _71.removed, 'optionalAccess', _72 => _72.length]) ? { removed: o.removed } : {},
|
|
2360
|
+
...o.rippled ? { rippled: o.rippled } : {}
|
|
2361
|
+
});
|
|
2073
2362
|
const keysOut = (p, clip, property) => {
|
|
2074
2363
|
const c = clipOut(p, clip);
|
|
2075
|
-
const value = VIDEO_TRANSFORM_PROPERTIES.includes(property) ? _optionalChain([c, 'optionalAccess',
|
|
2364
|
+
const value = VIDEO_TRANSFORM_PROPERTIES.includes(property) ? _optionalChain([c, 'optionalAccess', _73 => _73.transform, 'optionalAccess', _74 => _74[property]]) : _optionalChain([c, 'optionalAccess', _75 => _75[property]]);
|
|
2076
2365
|
return typeof value === "object" ? { keys: value.keys } : { keys: [], value: _nullishCoalesce(value, () => ( null)) };
|
|
2077
2366
|
};
|
|
2078
2367
|
const getProject = {
|
|
@@ -2239,8 +2528,8 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
2239
2528
|
const updateTrack = {
|
|
2240
2529
|
name: "update_video_track",
|
|
2241
2530
|
title: "Change a track",
|
|
2242
|
-
description: "Renames, mutes, hides, locks or sets the volume of a track (one undo step). Returns the track's settings.",
|
|
2243
|
-
inputSchema: object({ track: TRACK_ID, set: object(TRACK_FIELDS, [], "Fields to replace."), remove: { type: "array", items: { type: "string", enum: ["name", "muted", "hidden", "locked", "volume"] }, description: "Fields to clear." } }, ["track"]),
|
|
2531
|
+
description: "Renames, mutes, hides, locks, sync-locks or sets the volume of a track (one undo step). Returns the track's settings.",
|
|
2532
|
+
inputSchema: object({ track: TRACK_ID, set: object(TRACK_FIELDS, [], "Fields to replace."), remove: { type: "array", items: { type: "string", enum: ["name", "muted", "hidden", "locked", "syncLocked", "volume"] }, description: "Fields to clear." } }, ["track"]),
|
|
2244
2533
|
annotations: { title: "Change a track", ...WRITE_IDEMPOTENT },
|
|
2245
2534
|
handler: ({ track, set, remove }) => apply({ op: "update_track", track, set, remove }, "Change track", (r) => {
|
|
2246
2535
|
const { clips, ...t } = r.project.tracks.find((x) => x.id === track);
|
|
@@ -2266,20 +2555,20 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
2266
2555
|
const addClip = {
|
|
2267
2556
|
name: "add_video_clip",
|
|
2268
2557
|
title: "Add a clip",
|
|
2269
|
-
description: 'Puts a clip on a track (one undo step): media (a stretch of an asset from its "in" point), a title or a colour. With no start it goes at the end of the track
|
|
2270
|
-
inputSchema: object({ track: TRACK_ID, clip: NEW_CLIP, ripple:
|
|
2558
|
+
description: 'Puts a clip on a track (one undo step): media (a stretch of an asset from its "in" point), a title or a colour. With no start it goes at the end of the track. Where it lands on other clips, mode decides: refuse (default, with the next free place), overwrite (replace what is underneath) or insert (push later material along on every sync-locked track). Returns the clip with its end and source range, plus any clips it cut (created), trimmed or removed and the tracks it rippled.',
|
|
2559
|
+
inputSchema: object({ track: TRACK_ID, clip: NEW_CLIP, mode: MODE, ripple: { type: "boolean", description: 'Same as mode "insert" (kept for older callers; default false).' } }, ["track", "clip"]),
|
|
2271
2560
|
annotations: { title: "Add a clip", ...WRITE },
|
|
2272
|
-
handler: ({ track, clip, ripple }) => apply({ op: "add_clip", track, clip, ripple }, "Add clip", (r) => ({ clip: clipOut(r.project, r.outcomes[0].clip) }))
|
|
2561
|
+
handler: ({ track, clip, mode, ripple }) => apply({ op: "add_clip", track, clip, mode, ripple }, mode === "overwrite" ? "Overwrite clip" : mode === "insert" || ripple ? "Insert clip" : "Add clip", (r) => ({ clip: clipOut(r.project, r.outcomes[0].clip), ...placed(r.outcomes[0]) }))
|
|
2273
2562
|
};
|
|
2274
2563
|
const moveClip = {
|
|
2275
2564
|
name: "move_video_clip",
|
|
2276
2565
|
title: "Move a clip",
|
|
2277
|
-
description: "Moves a clip to a new start and/or another track of the same kind (one undo step)
|
|
2278
|
-
inputSchema: object({ clip: CLIP_ID, start: MS("New start on the timeline (default: unchanged)"), track: { ...TRACK_ID, description: "Track to move it to (default: its own)." } }, ["clip"]),
|
|
2279
|
-
annotations: { title: "Move a clip", ...
|
|
2280
|
-
handler: ({ clip, start, track }) => {
|
|
2566
|
+
description: "Moves a clip to a new start and/or another track of the same kind (one undo step), leaving a gap where it was. Where it lands on other clips, mode decides: refuse (default, naming the clip and the next free place), overwrite or insert. Returns the clip, plus any clips it cut (created), trimmed or removed and the tracks it rippled.",
|
|
2567
|
+
inputSchema: object({ clip: CLIP_ID, start: MS("New start on the timeline (default: unchanged)"), track: { ...TRACK_ID, description: "Track to move it to (default: its own)." }, mode: MODE }, ["clip"]),
|
|
2568
|
+
annotations: { title: "Move a clip", ...WRITE },
|
|
2569
|
+
handler: ({ clip, start, track, mode }) => {
|
|
2281
2570
|
if (start === void 0 && track === void 0) throw new Error('Give "start", "track" or both.');
|
|
2282
|
-
return apply({ op: "move_clip", clip, start, track }, "Move clip", (r) => ({ clip: clipOut(r.project, clip) }));
|
|
2571
|
+
return apply({ op: "move_clip", clip, start, track, mode }, mode === "overwrite" ? "Overwrite clip" : mode === "insert" ? "Insert clip" : "Move clip", (r) => ({ clip: clipOut(r.project, clip), ...placed(r.outcomes[0]) }));
|
|
2283
2572
|
}
|
|
2284
2573
|
};
|
|
2285
2574
|
const trimClip = {
|
|
@@ -2288,7 +2577,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
2288
2577
|
description: 'Moves one edge of a clip to a timeline time (one undo step): "start" also moves its source in point, "end" its out point. Refused past the ends of the source (the error says how far it can go) or below one frame. With ripple the clip keeps its start and the later clips on the track move by the change, so no gap or overlap opens. Returns the clip.',
|
|
2289
2578
|
inputSchema: object({ clip: CLIP_ID, edge: { type: "string", enum: ["start", "end"], description: "Which edge to move." }, to: MS("Where that edge goes on the timeline"), ripple: RIPPLE }, ["clip", "edge", "to"]),
|
|
2290
2579
|
annotations: { title: "Trim a clip", ...WRITE_IDEMPOTENT },
|
|
2291
|
-
handler: ({ clip, edge, to, ripple }) => apply({ op: "trim_clip", clip, edge, to, ripple }, "Trim clip", (r) => ({ clip: clipOut(r.project, clip) }))
|
|
2580
|
+
handler: ({ clip, edge, to, ripple }) => apply({ op: "trim_clip", clip, edge, to, ripple }, ripple ? "Ripple trim" : "Trim clip", (r) => ({ clip: clipOut(r.project, clip), ...placed(r.outcomes[0]) }))
|
|
2292
2581
|
};
|
|
2293
2582
|
const splitClip = {
|
|
2294
2583
|
name: "split_video_clip",
|
|
@@ -2301,10 +2590,13 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
2301
2590
|
const deleteClips = {
|
|
2302
2591
|
name: "delete_video_clips",
|
|
2303
2592
|
title: "Delete clips",
|
|
2304
|
-
description: "Removes clips (one undo step). With ripple the later
|
|
2305
|
-
inputSchema: object(
|
|
2593
|
+
description: "Removes clips (one undo step). With ripple the later material moves up to close the gaps they leave: on every sync-locked track when they were on one (refused, naming the clip, if another sync-locked track has something in a gap), else on their own track. Returns the ids removed and the tracks rippled.",
|
|
2594
|
+
inputSchema: object(
|
|
2595
|
+
{ clips: { type: "array", items: CLIP_ID, minItems: 1, description: "Clip ids." }, ripple: { type: "boolean", description: "Close the gaps they leave, on every sync-locked track (default false)." } },
|
|
2596
|
+
["clips"]
|
|
2597
|
+
),
|
|
2306
2598
|
annotations: { title: "Delete clips", ...DESTRUCTIVE },
|
|
2307
|
-
handler: ({ clips, ripple }) => apply({ op: "delete_clips", clips, ripple }, ripple ? "Ripple delete" : "Delete clips", () => ({ removed: clips }))
|
|
2599
|
+
handler: ({ clips, ripple }) => apply({ op: "delete_clips", clips, ripple }, ripple ? "Ripple delete" : "Delete clips", (r) => ({ removed: clips, ...r.outcomes[0].rippled ? { rippled: r.outcomes[0].rippled } : {} }))
|
|
2308
2600
|
};
|
|
2309
2601
|
const updateClip = {
|
|
2310
2602
|
name: "update_video_clip",
|
|
@@ -2337,6 +2629,66 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
2337
2629
|
annotations: { title: "Set a transition", ...WRITE_IDEMPOTENT },
|
|
2338
2630
|
handler: ({ clip, transition }) => apply({ op: "set_transition", clip, transition }, transition ? "Set transition" : "Remove transition", (r) => ({ clip: clipOut(r.project, clip) }))
|
|
2339
2631
|
};
|
|
2632
|
+
const slipClip = {
|
|
2633
|
+
name: "slip_video_clip",
|
|
2634
|
+
title: "Slip a clip",
|
|
2635
|
+
description: "Shows a different stretch of a video or audio clip's source without moving it on the timeline: its in and out points both move by delta (one undo step). Clamped to the media past either end, keeping what its transitions need. Returns the clip (with its new source range) and the delta applied.",
|
|
2636
|
+
inputSchema: object({ clip: CLIP_ID, delta: DELTA("How far to move the source (positive shows later media)") }, ["clip", "delta"]),
|
|
2637
|
+
annotations: { title: "Slip a clip", ...WRITE },
|
|
2638
|
+
handler: ({ clip, delta }) => apply({ op: "slip_clip", clip, delta }, "Slip clip", (r) => ({ clip: clipOut(r.project, clip), delta: r.outcomes[0].delta }))
|
|
2639
|
+
};
|
|
2640
|
+
const rollEdit = {
|
|
2641
|
+
name: "roll_video_edit",
|
|
2642
|
+
title: "Roll a cut",
|
|
2643
|
+
description: "Moves the cut between two clips that meet on one track (one undo step): the left one's out point and the right one's in point move together, so nothing else moves and no gap opens. Clamped to each clip's media and at least a frame of each. Returns both clips, the new cut time and the delta applied.",
|
|
2644
|
+
inputSchema: object({ left: { ...CLIP_ID, description: "The clip before the cut (from get_video_project)." }, right: { ...CLIP_ID, description: 'The clip after the cut, starting where "left" ends.' }, delta: DELTA("How far to move the cut") }, ["left", "right", "delta"]),
|
|
2645
|
+
annotations: { title: "Roll a cut", ...WRITE },
|
|
2646
|
+
handler: ({ left, right, delta }) => apply({ op: "roll_edit", left, right, delta }, "Roll edit", (r) => {
|
|
2647
|
+
const after = clipOut(r.project, right);
|
|
2648
|
+
return { left: clipOut(r.project, left), right: after, cut: after.start, delta: r.outcomes[0].delta };
|
|
2649
|
+
})
|
|
2650
|
+
};
|
|
2651
|
+
const slideClip = {
|
|
2652
|
+
name: "slide_video_clip",
|
|
2653
|
+
title: "Slide a clip",
|
|
2654
|
+
description: "Moves a clip along its track keeping its content, trimming the clips either side so the stretch stays gapless (one undo step): the one before gets longer as the one after gets shorter, or the other way. Without a neighbour on one side it moves through the gap there. Clamped to the neighbours' media and at least a frame of each. Returns the clip, its neighbours and the delta applied.",
|
|
2655
|
+
inputSchema: object({ clip: CLIP_ID, delta: DELTA("How far to move it") }, ["clip", "delta"]),
|
|
2656
|
+
annotations: { title: "Slide a clip", ...WRITE },
|
|
2657
|
+
handler: ({ clip, delta }) => apply({ op: "slide_clip", clip, delta }, "Slide clip", (r) => {
|
|
2658
|
+
const ids = r.outcomes[0].clips;
|
|
2659
|
+
const i = ids.indexOf(clip);
|
|
2660
|
+
return { clip: clipOut(r.project, clip), ...i > 0 ? { before: clipOut(r.project, ids[0]) } : {}, ...i < ids.length - 1 ? { after: clipOut(r.project, ids[ids.length - 1]) } : {}, delta: r.outcomes[0].delta };
|
|
2661
|
+
})
|
|
2662
|
+
};
|
|
2663
|
+
const rangeArgs = object({ from: MS("Start of the range on the timeline"), to: MS('End of the range on the timeline (after "from")'), tracks: RANGE_TRACKS }, ["from", "to"]);
|
|
2664
|
+
const rangeOut = (o) => ({ removed: _nullishCoalesce(o.removed, () => ( [])), trimmed: _nullishCoalesce(o.trimmed, () => ( [])), created: _nullishCoalesce(o.created, () => ( [])), ...o.rippled ? { rippled: o.rippled } : {} });
|
|
2665
|
+
const liftRange = {
|
|
2666
|
+
name: "lift_video_range",
|
|
2667
|
+
title: "Lift a range",
|
|
2668
|
+
description: "Empties a stretch of time on some or all tracks, leaving a gap (one undo step): clips inside are deleted, clips across an edge are trimmed to it, and a clip across both is cut in two. Returns the clips removed, trimmed and created (right-hand parts of cuts).",
|
|
2669
|
+
inputSchema: rangeArgs,
|
|
2670
|
+
annotations: { title: "Lift a range", ...DESTRUCTIVE },
|
|
2671
|
+
handler: ({ from, to, tracks }) => apply({ op: "lift_range", from, to, tracks }, "Lift", (r) => rangeOut(r.outcomes[0]))
|
|
2672
|
+
};
|
|
2673
|
+
const extractRange = {
|
|
2674
|
+
name: "extract_video_range",
|
|
2675
|
+
title: "Extract a range",
|
|
2676
|
+
description: 'Removes a stretch of time and closes it up (one undo step): like lift, then everything after it moves earlier by its length on those tracks and every sync-locked track. Refused, naming the clip, when a sync-locked track left out of "tracks" has something in the range. Returns the clips removed, trimmed and created and the tracks that moved.',
|
|
2677
|
+
inputSchema: rangeArgs,
|
|
2678
|
+
annotations: { title: "Extract a range", ...DESTRUCTIVE },
|
|
2679
|
+
handler: ({ from, to, tracks }) => apply({ op: "extract_range", from, to, tracks }, "Extract", (r) => rangeOut(r.outcomes[0]))
|
|
2680
|
+
};
|
|
2681
|
+
const splitAt = {
|
|
2682
|
+
name: "split_video_at",
|
|
2683
|
+
title: "Cut across tracks",
|
|
2684
|
+
description: "Cuts every clip that runs across a time on some or all tracks (a razor through the timeline), as one undo step. Each left part keeps its id, fade in and transition; each right part gets a new id and the fade out. Refused when no clip runs across that time. Returns the pairs { left, right }.",
|
|
2685
|
+
inputSchema: object({ at: MS("Where to cut, on the timeline"), tracks: RANGE_TRACKS }, ["at"]),
|
|
2686
|
+
annotations: { title: "Cut across tracks", ...WRITE },
|
|
2687
|
+
handler: ({ at, tracks }) => apply({ op: "split_at", at, tracks }, "Split", (r) => {
|
|
2688
|
+
const o = r.outcomes[0];
|
|
2689
|
+
return { cuts: o.clips.map((left, i) => ({ left: clipOut(r.project, left), right: clipOut(r.project, o.created[i]) })) };
|
|
2690
|
+
})
|
|
2691
|
+
};
|
|
2340
2692
|
const setKeyframes = {
|
|
2341
2693
|
name: "set_video_keyframes",
|
|
2342
2694
|
title: "Animate a clip property",
|
|
@@ -2379,7 +2731,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
2379
2731
|
};
|
|
2380
2732
|
const EDIT_OP = {
|
|
2381
2733
|
type: "object",
|
|
2382
|
-
description: `One edit: { "op": one of ${VIDEO_EDIT_OPS.filter((o) => o !== "replace_project").join(", ")}, \u2026its fields }. Fields match the single tools: add_clip { track, clip, ripple }, move_clip { clip, start, track }, trim_clip { clip, edge, to, ripple }, split_clip { clip, at, id }, delete_clips { clips, ripple }, update_clip { clip, set, remove }, set_transition { clip, transition }, set_keyframes { clip, property, keys (null removes) }, close_gaps { track, from }, add_asset { asset }, update_asset { asset, set, remove }, remove_asset { asset }, add_track { track: { kind, id, \u2026 }, at }, update_track { track, set, remove }, remove_track { track }, move_track { track, to }, add_marker { marker: { time, label } }, update_marker { marker, set }, remove_marker { marker }, set_settings { name, width, height, fps, background }.`,
|
|
2734
|
+
description: `One edit: { "op": one of ${VIDEO_EDIT_OPS.filter((o) => o !== "replace_project").join(", ")}, \u2026its fields }. Fields match the single tools: add_clip { track, clip, mode, ripple }, move_clip { clip, start, track, mode }, trim_clip { clip, edge, to, ripple }, split_clip { clip, at, id }, delete_clips { clips, ripple }, update_clip { clip, set, remove }, set_transition { clip, transition }, set_keyframes { clip, property, keys (null removes) }, slip_clip { clip, delta }, roll_edit { left, right, delta }, slide_clip { clip, delta }, lift_range { from, to, tracks }, extract_range { from, to, tracks }, split_at { at, tracks }, close_gaps { track, from }, add_asset { asset }, update_asset { asset, set, remove }, remove_asset { asset }, add_track { track: { kind, id, \u2026 }, at }, update_track { track, set, remove }, remove_track { track }, move_track { track, to }, add_marker { marker: { time, label } }, update_marker { marker, set }, remove_marker { marker }, set_settings { name, width, height, fps, background }.`,
|
|
2383
2735
|
properties: { op: { type: "string", enum: VIDEO_EDIT_OPS.filter((o) => o !== "replace_project"), description: "Which edit." } },
|
|
2384
2736
|
required: ["op"]
|
|
2385
2737
|
};
|
|
@@ -2420,7 +2772,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
2420
2772
|
}
|
|
2421
2773
|
};
|
|
2422
2774
|
const reads = [getProject, getFrame, findGaps, checkProject, renderFrame, exportFfmpeg, exportEdl, seek];
|
|
2423
|
-
const writes = [setSettings, addAsset, updateAsset, removeAsset, addTrack, updateTrack, removeTrack, moveTrack, addClip, moveClip, trimClip, splitClip, deleteClips, updateClip, setTransition, setKeyframes, removeKeyframes, closeGaps, addMarker, removeMarker, editVideo, undo, redo];
|
|
2775
|
+
const writes = [setSettings, addAsset, updateAsset, removeAsset, addTrack, updateTrack, removeTrack, moveTrack, addClip, moveClip, trimClip, splitClip, deleteClips, updateClip, setTransition, setKeyframes, removeKeyframes, slipClip, rollEdit, slideClip, liftRange, extractRange, splitAt, closeGaps, addMarker, removeMarker, editVideo, undo, redo];
|
|
2424
2776
|
return {
|
|
2425
2777
|
id: _nullishCoalesce(options.id, () => ( "video")),
|
|
2426
2778
|
namespace: options.namespace,
|
|
@@ -2462,7 +2814,7 @@ function videoClipLabel(clip, asset) {
|
|
|
2462
2814
|
if (clip.name) return clip.name;
|
|
2463
2815
|
if (clip.type === "text") return clip.text.split("\n")[0];
|
|
2464
2816
|
if (clip.type === "color") return clip.color;
|
|
2465
|
-
const base = _nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess',
|
|
2817
|
+
const base = _nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess', _76 => _76.name]), () => ( _optionalChain([asset, 'optionalAccess', _77 => _77.src, 'access', _78 => _78.replace, 'call', _79 => _79(/^.*[\\/]/, "")]))), () => ( clip.asset));
|
|
2466
2818
|
const speed = videoClipSpeed(clip);
|
|
2467
2819
|
return speed !== 1 ? `${base} (${Math.round(speed * 100)}%)` : base;
|
|
2468
2820
|
}
|
|
@@ -2479,7 +2831,8 @@ function planVideoClipDrag(project, input) {
|
|
|
2479
2831
|
const start = Math.max(0, raw + (shifts.length ? shifts.reduce((a, b) => Math.abs(b) < Math.abs(a) ? b : a) : 0));
|
|
2480
2832
|
const target = _nullishCoalesce(input.track, () => ( track.id));
|
|
2481
2833
|
const moved = Math.abs(start - clip.start) > 1e-6 || target !== track.id;
|
|
2482
|
-
|
|
2834
|
+
const mode = input.placement && input.placement !== "refuse" ? { mode: input.placement } : {};
|
|
2835
|
+
return { start, duration: clip.duration, track: target, edit: moved ? { op: "move_clip", clip: clip.id, start, ...target !== track.id ? { track: target } : {}, ...mode } : null };
|
|
2483
2836
|
}
|
|
2484
2837
|
const minimum = 1e3 / project.fps;
|
|
2485
2838
|
if (input.mode === "trim-start") {
|
|
@@ -2497,13 +2850,17 @@ function videoDropTracks(project, clipId) {
|
|
|
2497
2850
|
if (!at) return [];
|
|
2498
2851
|
return project.tracks.filter((t) => t.kind === at.track.kind && !t.locked).map((t) => t.id);
|
|
2499
2852
|
}
|
|
2853
|
+
function videoDragLabel(edit) {
|
|
2854
|
+
if (edit.op === "move_clip") return edit.mode === "insert" ? "Insert clip" : edit.mode === "overwrite" ? "Overwrite clip" : "Move clip";
|
|
2855
|
+
return edit.op === "trim_clip" && edit.ripple ? "Ripple trim" : "Trim clip";
|
|
2856
|
+
}
|
|
2500
2857
|
function videoAssetDropEdit(project, assetId, options = {}) {
|
|
2501
2858
|
const asset = videoAssetMap(project).get(assetId);
|
|
2502
2859
|
if (!asset) return null;
|
|
2503
2860
|
const kind = asset.kind === "audio" ? "audio" : "video";
|
|
2504
|
-
const track = _nullishCoalesce(options.track, () => ( _optionalChain([project, 'access',
|
|
2861
|
+
const track = _nullishCoalesce(options.track, () => ( _optionalChain([project, 'access', _80 => _80.tracks, 'access', _81 => _81.find, 'call', _82 => _82((t) => t.kind === kind && !t.locked), 'optionalAccess', _83 => _83.id])));
|
|
2505
2862
|
if (!track) return null;
|
|
2506
|
-
return { op: "add_clip", track, clip: { type: "media", asset: asset.id, ...options.start !== void 0 ? { start: options.start } : {} }, ...options.ripple ? { ripple: true } : {} };
|
|
2863
|
+
return { op: "add_clip", track, clip: { type: "media", asset: asset.id, ...options.start !== void 0 ? { start: options.start } : {} }, ...options.ripple ? { ripple: true } : {}, ...options.mode && options.mode !== "refuse" ? { mode: options.mode } : {} };
|
|
2507
2864
|
}
|
|
2508
2865
|
function videoAssetKindOf(typeOrName) {
|
|
2509
2866
|
const s = typeOrName.toLowerCase().replace(/[?#].*$/, "");
|
|
@@ -2550,15 +2907,15 @@ var KEY_LABELS = {
|
|
|
2550
2907
|
scale: { label: "Scale", step: 0.05 },
|
|
2551
2908
|
rotation: { label: "Rotation", step: 1 }
|
|
2552
2909
|
};
|
|
2553
|
-
var animatableOf = (clip, property) => VIDEO_TRANSFORM_PROPERTIES.includes(property) ? _optionalChain([clip, 'access',
|
|
2910
|
+
var animatableOf = (clip, property) => VIDEO_TRANSFORM_PROPERTIES.includes(property) ? _optionalChain([clip, 'access', _84 => _84.transform, 'optionalAccess', _85 => _85[property]]) : property === "volume" ? clip.type === "media" ? clip.volume : void 0 : clip.opacity;
|
|
2554
2911
|
var playheadInClip = (clip, time) => Math.round(Math.min(clip.duration, Math.max(0, time - clip.start)));
|
|
2555
2912
|
function videoKeyframeRows(project, clipId, time) {
|
|
2556
2913
|
const at = findVideoClip(project, clipId);
|
|
2557
2914
|
if (!at) return [];
|
|
2558
2915
|
const { clip } = at;
|
|
2559
2916
|
const asset = clip.type === "media" ? videoAssetMap(project).get(clip.asset) : void 0;
|
|
2560
|
-
const visual = !(_optionalChain([asset, 'optionalAccess',
|
|
2561
|
-
const audible = clip.type === "media" && (_optionalChain([asset, 'optionalAccess',
|
|
2917
|
+
const visual = !(_optionalChain([asset, 'optionalAccess', _86 => _86.kind]) === "audio" || at.track.kind === "audio");
|
|
2918
|
+
const audible = clip.type === "media" && (_optionalChain([asset, 'optionalAccess', _87 => _87.kind]) === "audio" || _optionalChain([asset, 'optionalAccess', _88 => _88.kind]) === "video" && asset.hasAudio !== false);
|
|
2562
2919
|
const properties = [...visual ? ["opacity"] : [], ...audible ? ["volume"] : [], ...visual ? VIDEO_TRANSFORM_PROPERTIES : []];
|
|
2563
2920
|
const clipTime = playheadInClip(clip, time);
|
|
2564
2921
|
return properties.map((property) => {
|
|
@@ -2700,5 +3057,8 @@ function videoKeyframeToggleEdit(project, clipId, property, time) {
|
|
|
2700
3057
|
|
|
2701
3058
|
|
|
2702
3059
|
|
|
2703
|
-
|
|
3060
|
+
|
|
3061
|
+
|
|
3062
|
+
|
|
3063
|
+
exports.VIDEO_CLIP_TYPES = VIDEO_CLIP_TYPES; exports.VIDEO_DEFAULT_STILL_DURATION = VIDEO_DEFAULT_STILL_DURATION; exports.VIDEO_DEFAULT_TITLE_DURATION = VIDEO_DEFAULT_TITLE_DURATION; exports.VIDEO_EASE_NAMES = VIDEO_EASE_NAMES; exports.VIDEO_EDIT_OPS = VIDEO_EDIT_OPS; exports.VIDEO_FITS = VIDEO_FITS; exports.VIDEO_ISSUE_CODES = VIDEO_ISSUE_CODES; exports.VIDEO_KEYFRAME_LIMITS = VIDEO_KEYFRAME_LIMITS; exports.VIDEO_KEYFRAME_PROPERTIES = VIDEO_KEYFRAME_PROPERTIES; exports.VIDEO_PLACEMENT_MODES = VIDEO_PLACEMENT_MODES; exports.VIDEO_TIME_EPSILON = VIDEO_TIME_EPSILON; exports.VIDEO_TRANSFORM_PROPERTIES = VIDEO_TRANSFORM_PROPERTIES; exports.VIDEO_TRANSITION_TYPES = VIDEO_TRANSITION_TYPES; exports.VIDEO_WIPE_DIRECTIONS = VIDEO_WIPE_DIRECTIONS; exports.VideoEditorController = VideoEditorController; exports.VideoPreviewEngine = VideoPreviewEngine; exports.allVideoClipIds = allVideoClipIds; exports.applyVideoEdits = applyVideoEdits; exports.checkVideoProject = checkVideoProject; exports.createVideoEditorToolProvider = createVideoEditorToolProvider; exports.createVideoProject = createVideoProject; exports.describeVideoProject = describeVideoProject; exports.drawVideoFrame = drawVideoFrame; exports.ffmpegFilterValue = ffmpegFilterValue; exports.findVideoClip = findVideoClip; exports.findVideoGaps = findVideoGaps; exports.formatVideoSeconds = formatVideoSeconds; exports.formatVideoTimecode = formatVideoTimecode; exports.isVideoKeyframes = isVideoKeyframes; exports.nextFreeVideoSlot = nextFreeVideoSlot; exports.parseVideoAnimatable = parseVideoAnimatable; exports.parseVideoAsset = parseVideoAsset; exports.parseVideoClip = parseVideoClip; exports.parseVideoKeyframes = parseVideoKeyframes; exports.parseVideoMarker = parseVideoMarker; exports.parseVideoProject = parseVideoProject; exports.parseVideoTrack = parseVideoTrack; exports.parseVideoTransform = parseVideoTransform; exports.parseVideoTransition = parseVideoTransition; exports.placeVideoRect = placeVideoRect; exports.planVideoClipDrag = planVideoClipDrag; exports.probeVideoMedia = probeVideoMedia; exports.removeVideoKey = removeVideoKey; exports.retimeVideoAnimatable = retimeVideoAnimatable; exports.setVideoKey = setVideoKey; exports.snapToVideoFrame = snapToVideoFrame; exports.snapVideoTime = snapVideoTime; exports.sortVideoKeys = sortVideoKeys; exports.sortedVideoClips = sortedVideoClips; exports.splitVideoAnimatable = splitVideoAnimatable; exports.toVideoEdl = toVideoEdl; exports.toVideoFfmpeg = toVideoFfmpeg; exports.videoAssetDropEdit = videoAssetDropEdit; exports.videoAssetKindOf = videoAssetKindOf; exports.videoAssetMap = videoAssetMap; exports.videoClipAfter = videoClipAfter; exports.videoClipBefore = videoClipBefore; exports.videoClipEnd = videoClipEnd; exports.videoClipHandles = videoClipHandles; exports.videoClipKeyframes = videoClipKeyframes; exports.videoClipLabel = videoClipLabel; exports.videoClipOverhang = videoClipOverhang; exports.videoClipSourceRange = videoClipSourceRange; exports.videoClipSpeed = videoClipSpeed; exports.videoClipTint = videoClipTint; exports.videoClipsAt = videoClipsAt; exports.videoContactSheetSvg = videoContactSheetSvg; exports.videoDragLabel = videoDragLabel; exports.videoDropTracks = videoDropTracks; exports.videoEaseBezier = videoEaseBezier; exports.videoEaseProgress = videoEaseProgress; exports.videoFrameAt = videoFrameAt; exports.videoFrameDuration = videoFrameDuration; exports.videoFrameToSvg = videoFrameToSvg; exports.videoFrames = videoFrames; exports.videoIssueKey = videoIssueKey; exports.videoKeyframeBreakpoints = videoKeyframeBreakpoints; exports.videoKeyframeFfmpegExpr = videoKeyframeFfmpegExpr; exports.videoKeyframeRows = videoKeyframeRows; exports.videoKeyframeToggleEdit = videoKeyframeToggleEdit; exports.videoKeyframeValueEdit = videoKeyframeValueEdit; exports.videoKeysOf = videoKeysOf; exports.videoOverlap = videoOverlap; exports.videoProjectDuration = videoProjectDuration; exports.videoProjectSvg = videoProjectSvg; exports.videoRulerTicks = videoRulerTicks; exports.videoSnapPoints = videoSnapPoints; exports.videoTextStyle = videoTextStyle; exports.videoTitleFrame = videoTitleFrame; exports.videoTrackSyncLocked = videoTrackSyncLocked; exports.videoTransformAt = videoTransformAt; exports.videoValueAt = videoValueAt; exports.videoWipeRect = videoWipeRect;
|
|
2704
3064
|
//# sourceMappingURL=index.cjs.map
|