@twick/video-editor 0.15.27 → 0.15.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/timeline/timeline-view.d.ts +6 -1
- package/dist/components/track/audio-waveform.d.ts +6 -0
- package/dist/components/track/timeline-media-strip.d.ts +14 -0
- package/dist/components/track/track-base.d.ts +5 -1
- package/dist/components/track/track-element-context-menu.d.ts +16 -0
- package/dist/components/track/track-element.d.ts +6 -0
- package/dist/helpers/types.d.ts +1 -0
- package/dist/hooks/use-marquee-selection.d.ts +2 -1
- package/dist/hooks/use-timeline-drop.d.ts +2 -1
- package/dist/index.js +1369 -180
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1370 -181
- package/dist/index.mjs.map +1 -1
- package/dist/video-editor.css +67 -2
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ const jsxRuntime = require("react/jsx-runtime");
|
|
|
7
7
|
const livePlayer = require("@twick/live-player");
|
|
8
8
|
const timeline = require("@twick/timeline");
|
|
9
9
|
const React = require("react");
|
|
10
|
+
const reactDom = require("react-dom");
|
|
10
11
|
function t(t2, e3, s2) {
|
|
11
12
|
return (e3 = function(t3) {
|
|
12
13
|
var e4 = function(t4, e5) {
|
|
@@ -5945,6 +5946,9 @@ function ea() {
|
|
|
5945
5946
|
function sa() {
|
|
5946
5947
|
return !ta && (!(arguments.length > 0 && void 0 !== arguments[0]) || arguments[0]) && (ta = ea()), ta;
|
|
5947
5948
|
}
|
|
5949
|
+
function ia(t2) {
|
|
5950
|
+
ta = t2;
|
|
5951
|
+
}
|
|
5948
5952
|
const ra = ["filters", "resizeFilter", "src", "crossOrigin", "type"], na = ["cropX", "cropY"];
|
|
5949
5953
|
class oa extends Li {
|
|
5950
5954
|
static getDefaults() {
|
|
@@ -6472,13 +6476,7 @@ function Za(e3, s2) {
|
|
|
6472
6476
|
return tt.setClass(r2, e3), r2;
|
|
6473
6477
|
}
|
|
6474
6478
|
t(Qa, "type", "ColorMatrix"), t(Qa, "defaults", Ja), t(Qa, "uniformLocations", ["uColorMatrix", "uConstants"]), tt.setClass(Qa);
|
|
6475
|
-
Za("Brownie", [0.5997, 0.34553, -0.27082, 0, 0.186, -0.0377, 0.86095, 0.15059, 0, -0.1449, 0.24113, -0.07441, 0.44972, 0, -0.02965, 0, 0, 0, 1, 0]);
|
|
6476
|
-
Za("Vintage", [0.62793, 0.32021, -0.03965, 0, 0.03784, 0.02578, 0.64411, 0.03259, 0, 0.02926, 0.0466, -0.08512, 0.52416, 0, 0.02023, 0, 0, 0, 1, 0]);
|
|
6477
|
-
Za("Kodachrome", [1.12855, -0.39673, -0.03992, 0, 0.24991, -0.16404, 1.08352, -0.05498, 0, 0.09698, -0.16786, -0.56034, 1.60148, 0, 0.13972, 0, 0, 0, 1, 0]);
|
|
6478
|
-
Za("Technicolor", [1.91252, -0.85453, -0.09155, 0, 0.04624, -0.30878, 1.76589, -0.10601, 0, -0.27589, -0.2311, -0.75018, 1.84759, 0, 0.12137, 0, 0, 0, 1, 0]);
|
|
6479
|
-
Za("Polaroid", [1.438, -0.062, -0.062, 0, 0, -0.122, 1.378, -0.122, 0, 0, -0.016, -0.016, 1.483, 0, 0, 0, 0, 0, 1, 0]);
|
|
6480
|
-
Za("Sepia", [0.393, 0.769, 0.189, 0, 0, 0.349, 0.686, 0.168, 0, 0, 0.272, 0.534, 0.131, 0, 0, 0, 0, 0, 1, 0]);
|
|
6481
|
-
Za("BlackWhite", [1.5, 1.5, 1.5, 0, -1, 1.5, 1.5, 1.5, 0, -1, 1.5, 1.5, 1.5, 0, -1, 0, 0, 0, 1, 0]);
|
|
6479
|
+
const $a = Za("Brownie", [0.5997, 0.34553, -0.27082, 0, 0.186, -0.0377, 0.86095, 0.15059, 0, -0.1449, 0.24113, -0.07441, 0.44972, 0, -0.02965, 0, 0, 0, 1, 0]), th = Za("Vintage", [0.62793, 0.32021, -0.03965, 0, 0.03784, 0.02578, 0.64411, 0.03259, 0, 0.02926, 0.0466, -0.08512, 0.52416, 0, 0.02023, 0, 0, 0, 1, 0]), eh = Za("Kodachrome", [1.12855, -0.39673, -0.03992, 0, 0.24991, -0.16404, 1.08352, -0.05498, 0, 0.09698, -0.16786, -0.56034, 1.60148, 0, 0.13972, 0, 0, 0, 1, 0]), sh = Za("Technicolor", [1.91252, -0.85453, -0.09155, 0, 0.04624, -0.30878, 1.76589, -0.10601, 0, -0.27589, -0.2311, -0.75018, 1.84759, 0, 0.12137, 0, 0, 0, 1, 0]), ih = Za("Polaroid", [1.438, -0.062, -0.062, 0, 0, -0.122, 1.378, -0.122, 0, 0, -0.016, -0.016, 1.483, 0, 0, 0, 0, 0, 1, 0]), rh = Za("Sepia", [0.393, 0.769, 0.189, 0, 0, 0.349, 0.686, 0.168, 0, 0, 0.272, 0.534, 0.131, 0, 0, 0, 0, 0, 1, 0]), nh = Za("BlackWhite", [1.5, 1.5, 1.5, 0, -1, 1.5, 1.5, 1.5, 0, -1, 1.5, 1.5, 1.5, 0, -1, 0, 0, 0, 1, 0]);
|
|
6482
6480
|
class oh extends Va {
|
|
6483
6481
|
constructor() {
|
|
6484
6482
|
let t2 = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {};
|
|
@@ -6841,6 +6839,7 @@ class bh extends Va {
|
|
|
6841
6839
|
}
|
|
6842
6840
|
}
|
|
6843
6841
|
t(bh, "type", "Vibrance"), t(bh, "defaults", { vibrance: 0 }), t(bh, "uniformLocations", ["uVibrance"]), tt.setClass(bh);
|
|
6842
|
+
var Sh = Object.freeze({ __proto__: null, BaseFilter: Va, BlackWhite: nh, BlendColor: Ga, BlendImage: Ua, Blur: qa, Brightness: Ka, Brownie: $a, ColorMatrix: Qa, Composed: oh, Contrast: ah, Convolute: ch, Gamma: uh, Grayscale: gh, HueRotation: ph, Invert: mh, Kodachrome: eh, Noise: vh$1, Pixelate: yh, Polaroid: ih, RemoveColor: _h, Resize: xh, Saturation: Ch, Sepia: rh, Technicolor: sh, Vibrance: bh, Vintage: th });
|
|
6844
6843
|
var __defProp2 = Object.defineProperty;
|
|
6845
6844
|
var __defNormalProp2 = (obj, key, value) => key in obj ? __defProp2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
6846
6845
|
var __publicField2 = (obj, key, value) => __defNormalProp2(obj, key + "", value);
|
|
@@ -6901,6 +6900,8 @@ const ELEMENT_TYPES = {
|
|
|
6901
6900
|
RECT: "rect",
|
|
6902
6901
|
/** Circle element type */
|
|
6903
6902
|
CIRCLE: "circle",
|
|
6903
|
+
/** Emoji sticker element type */
|
|
6904
|
+
EMOJI: "emoji",
|
|
6904
6905
|
/** Arrow annotation element type */
|
|
6905
6906
|
ARROW: "arrow",
|
|
6906
6907
|
/** Line annotation / shape element type */
|
|
@@ -7149,7 +7150,25 @@ const rotateControl = new ai({
|
|
|
7149
7150
|
/** Whether to show connection line */
|
|
7150
7151
|
withConnection: true
|
|
7151
7152
|
});
|
|
7152
|
-
|
|
7153
|
+
const COLOR_FILTERS = {
|
|
7154
|
+
SATURATED: "saturated",
|
|
7155
|
+
BRIGHT: "bright",
|
|
7156
|
+
VIBRANT: "vibrant",
|
|
7157
|
+
RETRO: "retro",
|
|
7158
|
+
BLACK_WHITE: "blackWhite",
|
|
7159
|
+
SEPIA: "sepia",
|
|
7160
|
+
COOL: "cool",
|
|
7161
|
+
WARM: "warm",
|
|
7162
|
+
CINEMATIC: "cinematic",
|
|
7163
|
+
SOFT_GLOW: "softGlow",
|
|
7164
|
+
MOODY: "moody",
|
|
7165
|
+
DREAMY: "dreamy",
|
|
7166
|
+
INVERTED: "inverted",
|
|
7167
|
+
VINTAGE: "vintage",
|
|
7168
|
+
DRAMATIC: "dramatic",
|
|
7169
|
+
FADED: "faded"
|
|
7170
|
+
};
|
|
7171
|
+
let LRUCache$1 = class LRUCache {
|
|
7153
7172
|
constructor(maxSize = 100) {
|
|
7154
7173
|
if (maxSize <= 0) {
|
|
7155
7174
|
throw new Error("maxSize must be greater than 0");
|
|
@@ -7209,10 +7228,10 @@ class LRUCache {
|
|
|
7209
7228
|
get size() {
|
|
7210
7229
|
return this.cache.size;
|
|
7211
7230
|
}
|
|
7212
|
-
}
|
|
7213
|
-
class VideoFrameExtractor {
|
|
7231
|
+
};
|
|
7232
|
+
let VideoFrameExtractor$1 = class VideoFrameExtractor {
|
|
7214
7233
|
constructor(options = {}) {
|
|
7215
|
-
this.frameCache = new LRUCache(
|
|
7234
|
+
this.frameCache = new LRUCache$1(
|
|
7216
7235
|
options.maxCacheSize ?? 50
|
|
7217
7236
|
);
|
|
7218
7237
|
this.videoElements = /* @__PURE__ */ new Map();
|
|
@@ -7478,16 +7497,16 @@ class VideoFrameExtractor {
|
|
|
7478
7497
|
this.videoElements.clear();
|
|
7479
7498
|
this.frameCache.clear();
|
|
7480
7499
|
}
|
|
7481
|
-
}
|
|
7482
|
-
let defaultExtractor = null;
|
|
7483
|
-
function getDefaultVideoFrameExtractor(options) {
|
|
7484
|
-
if (!defaultExtractor) {
|
|
7485
|
-
defaultExtractor = new VideoFrameExtractor(options);
|
|
7500
|
+
};
|
|
7501
|
+
let defaultExtractor$1 = null;
|
|
7502
|
+
function getDefaultVideoFrameExtractor$1(options) {
|
|
7503
|
+
if (!defaultExtractor$1) {
|
|
7504
|
+
defaultExtractor$1 = new VideoFrameExtractor$1(options);
|
|
7486
7505
|
}
|
|
7487
|
-
return defaultExtractor;
|
|
7506
|
+
return defaultExtractor$1;
|
|
7488
7507
|
}
|
|
7489
|
-
async function getThumbnailCached(videoUrl, seekTime = 0.1, playbackRate) {
|
|
7490
|
-
const extractor = getDefaultVideoFrameExtractor(
|
|
7508
|
+
async function getThumbnailCached$1(videoUrl, seekTime = 0.1, playbackRate) {
|
|
7509
|
+
const extractor = getDefaultVideoFrameExtractor$1(
|
|
7491
7510
|
void 0
|
|
7492
7511
|
);
|
|
7493
7512
|
return extractor.getFrame(videoUrl, seekTime);
|
|
@@ -7532,6 +7551,275 @@ const getObjectFitSize = (objectFit, elementSize, containerSize) => {
|
|
|
7532
7551
|
};
|
|
7533
7552
|
}
|
|
7534
7553
|
};
|
|
7554
|
+
const {
|
|
7555
|
+
Blur,
|
|
7556
|
+
Brightness,
|
|
7557
|
+
ColorMatrix,
|
|
7558
|
+
Contrast,
|
|
7559
|
+
Grayscale,
|
|
7560
|
+
HueRotation,
|
|
7561
|
+
Invert,
|
|
7562
|
+
Saturation,
|
|
7563
|
+
Sepia
|
|
7564
|
+
} = Sh;
|
|
7565
|
+
let canvas2dFilterBackendInstalled = false;
|
|
7566
|
+
function ensureCanvas2dImageFilterBackend() {
|
|
7567
|
+
if (canvas2dFilterBackendInstalled) return;
|
|
7568
|
+
canvas2dFilterBackendInstalled = true;
|
|
7569
|
+
ia(new Zo());
|
|
7570
|
+
}
|
|
7571
|
+
function getSourceBitmapSize(img) {
|
|
7572
|
+
const el = img.getElement();
|
|
7573
|
+
if (!el) return null;
|
|
7574
|
+
const w2 = "naturalWidth" in el && el.naturalWidth > 0 ? el.naturalWidth : el.width;
|
|
7575
|
+
const h2 = "naturalHeight" in el && el.naturalHeight > 0 ? el.naturalHeight : el.height;
|
|
7576
|
+
if (!w2 || !h2) return null;
|
|
7577
|
+
return { w: w2, h: h2 };
|
|
7578
|
+
}
|
|
7579
|
+
function applyFiltersSafe(img) {
|
|
7580
|
+
ensureCanvas2dImageFilterBackend();
|
|
7581
|
+
img.applyFilters();
|
|
7582
|
+
}
|
|
7583
|
+
const bright = (m2) => Math.min(1, Math.max(-1, (m2 - 1) * 0.48));
|
|
7584
|
+
const contr = (m2) => Math.min(1, Math.max(-1, (m2 - 1) * 0.55));
|
|
7585
|
+
const sat = (m2) => Math.min(1, Math.max(-1, (m2 - 1) * 0.72));
|
|
7586
|
+
const IDENTITY = [
|
|
7587
|
+
1,
|
|
7588
|
+
0,
|
|
7589
|
+
0,
|
|
7590
|
+
0,
|
|
7591
|
+
0,
|
|
7592
|
+
0,
|
|
7593
|
+
1,
|
|
7594
|
+
0,
|
|
7595
|
+
0,
|
|
7596
|
+
0,
|
|
7597
|
+
0,
|
|
7598
|
+
0,
|
|
7599
|
+
1,
|
|
7600
|
+
0,
|
|
7601
|
+
0,
|
|
7602
|
+
0,
|
|
7603
|
+
0,
|
|
7604
|
+
0,
|
|
7605
|
+
1,
|
|
7606
|
+
0
|
|
7607
|
+
];
|
|
7608
|
+
const SEPIA_MATRIX = [
|
|
7609
|
+
0.393,
|
|
7610
|
+
0.769,
|
|
7611
|
+
0.189,
|
|
7612
|
+
0,
|
|
7613
|
+
0,
|
|
7614
|
+
0.349,
|
|
7615
|
+
0.686,
|
|
7616
|
+
0.168,
|
|
7617
|
+
0,
|
|
7618
|
+
0,
|
|
7619
|
+
0.272,
|
|
7620
|
+
0.534,
|
|
7621
|
+
0.131,
|
|
7622
|
+
0,
|
|
7623
|
+
0,
|
|
7624
|
+
0,
|
|
7625
|
+
0,
|
|
7626
|
+
0,
|
|
7627
|
+
1,
|
|
7628
|
+
0
|
|
7629
|
+
];
|
|
7630
|
+
function sepiaMix(strength) {
|
|
7631
|
+
const t2 = Math.min(1, Math.max(0, strength));
|
|
7632
|
+
const m2 = IDENTITY.map((v2, i2) => v2 + (SEPIA_MATRIX[i2] - v2) * t2);
|
|
7633
|
+
return new ColorMatrix({ matrix: m2, colorsOnly: true });
|
|
7634
|
+
}
|
|
7635
|
+
const twickBlurToFabric = (v2) => Math.min(0.22, Math.max(0, v2 * 0.045));
|
|
7636
|
+
function buildFilters(filterType) {
|
|
7637
|
+
switch (filterType) {
|
|
7638
|
+
case COLOR_FILTERS.SATURATED:
|
|
7639
|
+
return {
|
|
7640
|
+
filters: [
|
|
7641
|
+
new Saturation({ saturation: sat(1.4) }),
|
|
7642
|
+
new Contrast({ contrast: contr(1.1) })
|
|
7643
|
+
],
|
|
7644
|
+
opacityFactor: 1
|
|
7645
|
+
};
|
|
7646
|
+
case COLOR_FILTERS.BRIGHT:
|
|
7647
|
+
return {
|
|
7648
|
+
filters: [
|
|
7649
|
+
new Brightness({ brightness: bright(1.3) }),
|
|
7650
|
+
new Contrast({ contrast: contr(1.05) })
|
|
7651
|
+
],
|
|
7652
|
+
opacityFactor: 1
|
|
7653
|
+
};
|
|
7654
|
+
case COLOR_FILTERS.VIBRANT:
|
|
7655
|
+
return {
|
|
7656
|
+
filters: [
|
|
7657
|
+
new Saturation({ saturation: sat(1.6) }),
|
|
7658
|
+
new Brightness({ brightness: bright(1.15) }),
|
|
7659
|
+
new Contrast({ contrast: contr(1.1) })
|
|
7660
|
+
],
|
|
7661
|
+
opacityFactor: 1
|
|
7662
|
+
};
|
|
7663
|
+
case COLOR_FILTERS.RETRO:
|
|
7664
|
+
return {
|
|
7665
|
+
filters: [
|
|
7666
|
+
sepiaMix(0.8),
|
|
7667
|
+
new Contrast({ contrast: contr(1.3) }),
|
|
7668
|
+
new Brightness({ brightness: bright(0.85) }),
|
|
7669
|
+
new Saturation({ saturation: sat(0.8) })
|
|
7670
|
+
],
|
|
7671
|
+
opacityFactor: 1
|
|
7672
|
+
};
|
|
7673
|
+
case COLOR_FILTERS.BLACK_WHITE:
|
|
7674
|
+
return {
|
|
7675
|
+
filters: [
|
|
7676
|
+
new Grayscale(),
|
|
7677
|
+
new Contrast({ contrast: contr(1.25) }),
|
|
7678
|
+
new Brightness({ brightness: bright(1.05) })
|
|
7679
|
+
],
|
|
7680
|
+
opacityFactor: 1
|
|
7681
|
+
};
|
|
7682
|
+
case COLOR_FILTERS.SEPIA:
|
|
7683
|
+
return {
|
|
7684
|
+
filters: [
|
|
7685
|
+
new Sepia(),
|
|
7686
|
+
new Contrast({ contrast: contr(1.08) })
|
|
7687
|
+
],
|
|
7688
|
+
opacityFactor: 1
|
|
7689
|
+
};
|
|
7690
|
+
case COLOR_FILTERS.COOL:
|
|
7691
|
+
return {
|
|
7692
|
+
filters: [
|
|
7693
|
+
new HueRotation({ rotation: 15 / 180 }),
|
|
7694
|
+
new Brightness({ brightness: bright(1.1) }),
|
|
7695
|
+
new Saturation({ saturation: sat(1.3) }),
|
|
7696
|
+
new Contrast({ contrast: contr(1.05) })
|
|
7697
|
+
],
|
|
7698
|
+
opacityFactor: 1
|
|
7699
|
+
};
|
|
7700
|
+
case COLOR_FILTERS.WARM:
|
|
7701
|
+
return {
|
|
7702
|
+
filters: [
|
|
7703
|
+
new HueRotation({ rotation: -15 / 180 }),
|
|
7704
|
+
new Brightness({ brightness: bright(1.15) }),
|
|
7705
|
+
new Saturation({ saturation: sat(1.3) }),
|
|
7706
|
+
new Contrast({ contrast: contr(1.05) })
|
|
7707
|
+
],
|
|
7708
|
+
opacityFactor: 1
|
|
7709
|
+
};
|
|
7710
|
+
case COLOR_FILTERS.CINEMATIC:
|
|
7711
|
+
return {
|
|
7712
|
+
filters: [
|
|
7713
|
+
new Contrast({ contrast: contr(1.4) }),
|
|
7714
|
+
new Brightness({ brightness: bright(0.95) }),
|
|
7715
|
+
new Saturation({ saturation: sat(0.85) }),
|
|
7716
|
+
sepiaMix(0.2)
|
|
7717
|
+
],
|
|
7718
|
+
opacityFactor: 1
|
|
7719
|
+
};
|
|
7720
|
+
case COLOR_FILTERS.SOFT_GLOW:
|
|
7721
|
+
return {
|
|
7722
|
+
filters: [
|
|
7723
|
+
new Brightness({ brightness: bright(1.2) }),
|
|
7724
|
+
new Contrast({ contrast: contr(0.95) }),
|
|
7725
|
+
new Blur({ blur: twickBlurToFabric(1.2) }),
|
|
7726
|
+
new Saturation({ saturation: sat(1.1) })
|
|
7727
|
+
],
|
|
7728
|
+
opacityFactor: 1
|
|
7729
|
+
};
|
|
7730
|
+
case COLOR_FILTERS.MOODY:
|
|
7731
|
+
return {
|
|
7732
|
+
filters: [
|
|
7733
|
+
new Brightness({ brightness: bright(1.05) }),
|
|
7734
|
+
new Contrast({ contrast: contr(1.4) }),
|
|
7735
|
+
new Saturation({ saturation: sat(0.65) }),
|
|
7736
|
+
sepiaMix(0.2)
|
|
7737
|
+
],
|
|
7738
|
+
opacityFactor: 1
|
|
7739
|
+
};
|
|
7740
|
+
case COLOR_FILTERS.DREAMY:
|
|
7741
|
+
return {
|
|
7742
|
+
filters: [
|
|
7743
|
+
new Brightness({ brightness: bright(1.3) }),
|
|
7744
|
+
new Blur({ blur: twickBlurToFabric(2) }),
|
|
7745
|
+
new Saturation({ saturation: sat(1.4) }),
|
|
7746
|
+
new Contrast({ contrast: contr(0.95) })
|
|
7747
|
+
],
|
|
7748
|
+
opacityFactor: 1
|
|
7749
|
+
};
|
|
7750
|
+
case COLOR_FILTERS.INVERTED:
|
|
7751
|
+
return {
|
|
7752
|
+
filters: [
|
|
7753
|
+
new Invert({ invert: true, alpha: false }),
|
|
7754
|
+
new HueRotation({ rotation: 1 })
|
|
7755
|
+
],
|
|
7756
|
+
opacityFactor: 1
|
|
7757
|
+
};
|
|
7758
|
+
case COLOR_FILTERS.VINTAGE:
|
|
7759
|
+
return {
|
|
7760
|
+
filters: [
|
|
7761
|
+
sepiaMix(0.4),
|
|
7762
|
+
new Saturation({ saturation: sat(1.4) }),
|
|
7763
|
+
new Contrast({ contrast: contr(1.2) }),
|
|
7764
|
+
new Brightness({ brightness: bright(1.1) })
|
|
7765
|
+
],
|
|
7766
|
+
opacityFactor: 1
|
|
7767
|
+
};
|
|
7768
|
+
case COLOR_FILTERS.DRAMATIC:
|
|
7769
|
+
return {
|
|
7770
|
+
filters: [
|
|
7771
|
+
new Contrast({ contrast: contr(1.5) }),
|
|
7772
|
+
new Brightness({ brightness: bright(0.9) }),
|
|
7773
|
+
new Saturation({ saturation: sat(1.2) })
|
|
7774
|
+
],
|
|
7775
|
+
opacityFactor: 1
|
|
7776
|
+
};
|
|
7777
|
+
case COLOR_FILTERS.FADED:
|
|
7778
|
+
return {
|
|
7779
|
+
filters: [
|
|
7780
|
+
new Brightness({ brightness: bright(1.2) }),
|
|
7781
|
+
new Saturation({ saturation: sat(0.8) }),
|
|
7782
|
+
new Contrast({ contrast: contr(0.9) })
|
|
7783
|
+
],
|
|
7784
|
+
opacityFactor: 0.9
|
|
7785
|
+
};
|
|
7786
|
+
default:
|
|
7787
|
+
return { filters: [], opacityFactor: 1 };
|
|
7788
|
+
}
|
|
7789
|
+
}
|
|
7790
|
+
function applyFabricMediaColorFilters(img, mediaFilter, elementOpacity) {
|
|
7791
|
+
const key = (mediaFilter == null ? void 0 : mediaFilter.trim()) || "none";
|
|
7792
|
+
if (key === "none") {
|
|
7793
|
+
img.filters = [];
|
|
7794
|
+
img.set("opacity", elementOpacity);
|
|
7795
|
+
applyFiltersSafe(img);
|
|
7796
|
+
return;
|
|
7797
|
+
}
|
|
7798
|
+
const { filters: filterList, opacityFactor } = buildFilters(key);
|
|
7799
|
+
if (filterList.length === 0) {
|
|
7800
|
+
img.filters = [];
|
|
7801
|
+
img.set("opacity", elementOpacity);
|
|
7802
|
+
applyFiltersSafe(img);
|
|
7803
|
+
return;
|
|
7804
|
+
}
|
|
7805
|
+
if (!getSourceBitmapSize(img)) {
|
|
7806
|
+
img.filters = [];
|
|
7807
|
+
img.set("opacity", elementOpacity);
|
|
7808
|
+
return;
|
|
7809
|
+
}
|
|
7810
|
+
img.filters = filterList;
|
|
7811
|
+
img.set("opacity", elementOpacity * opacityFactor);
|
|
7812
|
+
try {
|
|
7813
|
+
applyFiltersSafe(img);
|
|
7814
|
+
} catch {
|
|
7815
|
+
img.filters = [];
|
|
7816
|
+
img.set("opacity", elementOpacity);
|
|
7817
|
+
try {
|
|
7818
|
+
applyFiltersSafe(img);
|
|
7819
|
+
} catch {
|
|
7820
|
+
}
|
|
7821
|
+
}
|
|
7822
|
+
}
|
|
7535
7823
|
const MARGIN = 10;
|
|
7536
7824
|
const addTextElement = ({
|
|
7537
7825
|
element,
|
|
@@ -7622,7 +7910,7 @@ const setImageProps = ({
|
|
|
7622
7910
|
canvasMetadata,
|
|
7623
7911
|
lockAspectRatio = true
|
|
7624
7912
|
}) => {
|
|
7625
|
-
var _a, _b, _c, _d, _e2;
|
|
7913
|
+
var _a, _b, _c, _d, _e2, _f;
|
|
7626
7914
|
const width = (((_a = element.props) == null ? void 0 : _a.width) || 0) * canvasMetadata.scaleX || canvasMetadata.width;
|
|
7627
7915
|
const height = (((_b = element.props) == null ? void 0 : _b.height) || 0) * canvasMetadata.scaleY || canvasMetadata.height;
|
|
7628
7916
|
const { x: x2, y: y2 } = convertToCanvasPosition(
|
|
@@ -7636,11 +7924,15 @@ const setImageProps = ({
|
|
|
7636
7924
|
img.set("height", height);
|
|
7637
7925
|
img.set("left", x2);
|
|
7638
7926
|
img.set("top", y2);
|
|
7639
|
-
img.set("opacity", ((_e2 = element.props) == null ? void 0 : _e2.opacity) ?? 1);
|
|
7640
7927
|
img.set("selectable", true);
|
|
7641
7928
|
img.set("hasControls", true);
|
|
7642
7929
|
img.set("touchAction", "all");
|
|
7643
7930
|
img.set("lockUniScaling", lockAspectRatio);
|
|
7931
|
+
applyFabricMediaColorFilters(
|
|
7932
|
+
img,
|
|
7933
|
+
(_e2 = element.props) == null ? void 0 : _e2.mediaFilter,
|
|
7934
|
+
((_f = element.props) == null ? void 0 : _f.opacity) ?? 1
|
|
7935
|
+
);
|
|
7644
7936
|
};
|
|
7645
7937
|
const addCaptionElement = ({
|
|
7646
7938
|
element,
|
|
@@ -7650,48 +7942,52 @@ const addCaptionElement = ({
|
|
|
7650
7942
|
canvasMetadata,
|
|
7651
7943
|
lockAspectRatio = false
|
|
7652
7944
|
}) => {
|
|
7653
|
-
var _a, _b, _c, _d, _e2, _f, _g, _h2, _i2, _j, _k, _l, _m, _n2, _o2, _p, _q, _r2, _s2, _t2, _u, _v, _w, _x, _y
|
|
7654
|
-
const
|
|
7655
|
-
const
|
|
7945
|
+
var _a, _b, _c, _d, _e2, _f, _g, _h2, _i2, _j, _k, _l, _m, _n2, _o2, _p, _q, _r2, _s2, _t2, _u, _v, _w, _x, _y;
|
|
7946
|
+
const useTrackDefaults = ((_a = element.props) == null ? void 0 : _a.useTrackDefaults) ?? true;
|
|
7947
|
+
const trackColors = captionProps == null ? void 0 : captionProps.colors;
|
|
7948
|
+
const elementColors = (_b = element.props) == null ? void 0 : _b.colors;
|
|
7949
|
+
const resolvedColors = useTrackDefaults ? trackColors : { ...trackColors ?? {}, ...elementColors ?? {} };
|
|
7950
|
+
const captionTextColor = (resolvedColors == null ? void 0 : resolvedColors.text) ?? ((_c = captionProps == null ? void 0 : captionProps.color) == null ? void 0 : _c.text);
|
|
7656
7951
|
const { x: x2, y: y2 } = convertToCanvasPosition(
|
|
7657
|
-
(
|
|
7658
|
-
(
|
|
7952
|
+
(useTrackDefaults ? captionProps == null ? void 0 : captionProps.x : (_d = element.props) == null ? void 0 : _d.x) ?? (captionProps == null ? void 0 : captionProps.x) ?? 0,
|
|
7953
|
+
(useTrackDefaults ? captionProps == null ? void 0 : captionProps.y : (_e2 = element.props) == null ? void 0 : _e2.y) ?? (captionProps == null ? void 0 : captionProps.y) ?? 0,
|
|
7659
7954
|
canvasMetadata
|
|
7660
7955
|
);
|
|
7661
|
-
let width = ((
|
|
7662
|
-
if ((
|
|
7956
|
+
let width = ((_f = element.props) == null ? void 0 : _f.width) ? element.props.width * canvasMetadata.scaleX : canvasMetadata.width - 2 * MARGIN;
|
|
7957
|
+
if ((_g = element.props) == null ? void 0 : _g.maxWidth) {
|
|
7663
7958
|
width = Math.min(width, element.props.maxWidth * canvasMetadata.scaleX);
|
|
7664
7959
|
}
|
|
7665
|
-
const
|
|
7666
|
-
const resolvedFill = (applyToAll ? captionTextColor : ((_h2 = element.props) == null ? void 0 : _h2.fill) ?? (elementColors == null ? void 0 : elementColors.text) ?? captionTextColor) ?? DEFAULT_CAPTION_PROPS.fill;
|
|
7667
|
-
const trackColors = captionProps == null ? void 0 : captionProps.colors;
|
|
7960
|
+
const resolvedFill = (useTrackDefaults ? void 0 : (_h2 = element.props) == null ? void 0 : _h2.fill) ?? captionTextColor ?? DEFAULT_CAPTION_PROPS.fill;
|
|
7668
7961
|
const trackStroke = trackColors == null ? void 0 : trackColors.outlineColor;
|
|
7669
7962
|
const elementStroke = (elementColors == null ? void 0 : elementColors.outlineColor) ?? ((_i2 = element.props) == null ? void 0 : _i2.stroke);
|
|
7670
|
-
const resolvedStroke = (
|
|
7671
|
-
const
|
|
7963
|
+
const resolvedStroke = (useTrackDefaults ? trackStroke : elementStroke ?? trackStroke) ?? void 0;
|
|
7964
|
+
const trackFont = (captionProps == null ? void 0 : captionProps.font) ?? {};
|
|
7965
|
+
const elementFont = ((_j = element.props) == null ? void 0 : _j.font) ?? {};
|
|
7966
|
+
const resolvedFont = useTrackDefaults ? trackFont : { ...trackFont, ...elementFont };
|
|
7967
|
+
const caption = new Uo(((_k = element.props) == null ? void 0 : _k.text) || element.t || "", {
|
|
7672
7968
|
left: x2,
|
|
7673
7969
|
top: y2,
|
|
7674
7970
|
originX: "center",
|
|
7675
7971
|
originY: "center",
|
|
7676
|
-
angle: ((
|
|
7972
|
+
angle: ((_l = element.props) == null ? void 0 : _l.rotation) || 0,
|
|
7677
7973
|
fontSize: Math.round(
|
|
7678
|
-
((
|
|
7974
|
+
((resolvedFont == null ? void 0 : resolvedFont.size) ?? DEFAULT_CAPTION_PROPS.size) * canvasMetadata.scaleX
|
|
7679
7975
|
),
|
|
7680
|
-
fontFamily: (
|
|
7976
|
+
fontFamily: (resolvedFont == null ? void 0 : resolvedFont.family) ?? DEFAULT_CAPTION_PROPS.family,
|
|
7681
7977
|
fill: resolvedFill,
|
|
7682
|
-
fontWeight: (
|
|
7978
|
+
fontWeight: (resolvedFont == null ? void 0 : resolvedFont.weight) ?? DEFAULT_CAPTION_PROPS.fontWeight,
|
|
7683
7979
|
...resolvedStroke ? { stroke: resolvedStroke } : {},
|
|
7684
|
-
opacity: (
|
|
7980
|
+
opacity: (useTrackDefaults ? void 0 : (_m = element.props) == null ? void 0 : _m.opacity) ?? (captionProps == null ? void 0 : captionProps.opacity) ?? 1,
|
|
7685
7981
|
width,
|
|
7686
7982
|
splitByGrapheme: false,
|
|
7687
|
-
textAlign: ((
|
|
7983
|
+
textAlign: ((_n2 = element.props) == null ? void 0 : _n2.textAlign) ?? "center",
|
|
7688
7984
|
shadow: new Ds({
|
|
7689
|
-
offsetX: (
|
|
7690
|
-
offsetY: (
|
|
7691
|
-
blur: (
|
|
7692
|
-
color: (
|
|
7985
|
+
offsetX: (useTrackDefaults ? void 0 : (_p = (_o2 = element.props) == null ? void 0 : _o2.shadowOffset) == null ? void 0 : _p[0]) ?? ((_q = captionProps == null ? void 0 : captionProps.shadowOffset) == null ? void 0 : _q[0]) ?? ((_r2 = DEFAULT_CAPTION_PROPS.shadowOffset) == null ? void 0 : _r2[0]),
|
|
7986
|
+
offsetY: (useTrackDefaults ? void 0 : (_t2 = (_s2 = element.props) == null ? void 0 : _s2.shadowOffset) == null ? void 0 : _t2[1]) ?? ((_u = captionProps == null ? void 0 : captionProps.shadowOffset) == null ? void 0 : _u[1]) ?? ((_v = DEFAULT_CAPTION_PROPS.shadowOffset) == null ? void 0 : _v[1]),
|
|
7987
|
+
blur: (useTrackDefaults ? void 0 : (_w = element.props) == null ? void 0 : _w.shadowBlur) ?? (captionProps == null ? void 0 : captionProps.shadowBlur) ?? DEFAULT_CAPTION_PROPS.shadowBlur,
|
|
7988
|
+
color: (useTrackDefaults ? void 0 : (_x = element.props) == null ? void 0 : _x.shadowColor) ?? (captionProps == null ? void 0 : captionProps.shadowColor) ?? DEFAULT_CAPTION_PROPS.shadowColor
|
|
7693
7989
|
}),
|
|
7694
|
-
strokeWidth: ((
|
|
7990
|
+
strokeWidth: ((useTrackDefaults ? void 0 : (_y = element.props) == null ? void 0 : _y.lineWidth) ?? (captionProps == null ? void 0 : captionProps.lineWidth) ?? DEFAULT_CAPTION_PROPS.lineWidth) * 0.025
|
|
7695
7991
|
});
|
|
7696
7992
|
caption.set("id", element.id);
|
|
7697
7993
|
caption.set("zIndex", index);
|
|
@@ -7718,7 +8014,7 @@ const addVideoElement = async ({
|
|
|
7718
8014
|
}) => {
|
|
7719
8015
|
var _a;
|
|
7720
8016
|
try {
|
|
7721
|
-
const thumbnailUrl = await getThumbnailCached(
|
|
8017
|
+
const thumbnailUrl = await getThumbnailCached$1(
|
|
7722
8018
|
((_a = element == null ? void 0 : element.props) == null ? void 0 : _a.src) || "",
|
|
7723
8019
|
snapTime
|
|
7724
8020
|
);
|
|
@@ -7746,8 +8042,13 @@ const addImageElement = async ({
|
|
|
7746
8042
|
currentFrameEffect,
|
|
7747
8043
|
lockAspectRatio = true
|
|
7748
8044
|
}) => {
|
|
8045
|
+
var _a, _b;
|
|
7749
8046
|
try {
|
|
7750
|
-
const
|
|
8047
|
+
const rawSrc = imageUrl || element.props.src || "";
|
|
8048
|
+
const mediaFilter = (_b = (_a = element.props) == null ? void 0 : _a.mediaFilter) == null ? void 0 : _b.trim();
|
|
8049
|
+
const useFilter = !!mediaFilter && mediaFilter !== "none";
|
|
8050
|
+
const fromUrlOpts = useFilter && /^https?:\/\//i.test(rawSrc) ? { crossOrigin: "anonymous" } : {};
|
|
8051
|
+
const img = await oa.fromURL(rawSrc, fromUrlOpts);
|
|
7751
8052
|
img.set({
|
|
7752
8053
|
originX: "center",
|
|
7753
8054
|
originY: "center",
|
|
@@ -7784,7 +8085,7 @@ const addMediaGroup = ({
|
|
|
7784
8085
|
currentFrameEffect,
|
|
7785
8086
|
lockAspectRatio = true
|
|
7786
8087
|
}) => {
|
|
7787
|
-
var _a, _b, _c, _d, _e2, _f, _g, _h2, _i2, _j, _k, _l, _m, _n2;
|
|
8088
|
+
var _a, _b, _c, _d, _e2, _f, _g, _h2, _i2, _j, _k, _l, _m, _n2, _o2;
|
|
7788
8089
|
let frameSize;
|
|
7789
8090
|
let angle;
|
|
7790
8091
|
let framePosition;
|
|
@@ -7839,9 +8140,13 @@ const addMediaGroup = ({
|
|
|
7839
8140
|
originX: "center",
|
|
7840
8141
|
originY: "center",
|
|
7841
8142
|
scaleX: newSize.width / img.width,
|
|
7842
|
-
scaleY: newSize.height / img.height
|
|
7843
|
-
opacity: ((_n2 = element.props) == null ? void 0 : _n2.opacity) ?? 1
|
|
8143
|
+
scaleY: newSize.height / img.height
|
|
7844
8144
|
});
|
|
8145
|
+
applyFabricMediaColorFilters(
|
|
8146
|
+
img,
|
|
8147
|
+
(_n2 = element.props) == null ? void 0 : _n2.mediaFilter,
|
|
8148
|
+
((_o2 = element.props) == null ? void 0 : _o2.opacity) ?? 1
|
|
8149
|
+
);
|
|
7845
8150
|
const { x: x2, y: y2 } = convertToCanvasPosition(
|
|
7846
8151
|
(framePosition == null ? void 0 : framePosition.x) || 0,
|
|
7847
8152
|
(framePosition == null ? void 0 : framePosition.y) || 0,
|
|
@@ -8327,7 +8632,8 @@ const CaptionElement = {
|
|
|
8327
8632
|
context.canvasMetadata,
|
|
8328
8633
|
context.videoSize
|
|
8329
8634
|
);
|
|
8330
|
-
|
|
8635
|
+
const useTrackDefaults = ((_a = element.props) == null ? void 0 : _a.useTrackDefaults) ?? true;
|
|
8636
|
+
if (useTrackDefaults) {
|
|
8331
8637
|
return {
|
|
8332
8638
|
element,
|
|
8333
8639
|
operation: CANVAS_OPERATIONS.CAPTION_PROPS_UPDATED,
|
|
@@ -8544,6 +8850,72 @@ const EffectElement = {
|
|
|
8544
8850
|
return;
|
|
8545
8851
|
}
|
|
8546
8852
|
};
|
|
8853
|
+
const EmojiElement = {
|
|
8854
|
+
name: ELEMENT_TYPES.EMOJI,
|
|
8855
|
+
async add(params) {
|
|
8856
|
+
var _a;
|
|
8857
|
+
const { element, index, canvas, canvasMetadata, lockAspectRatio } = params;
|
|
8858
|
+
await addImageElement({
|
|
8859
|
+
element,
|
|
8860
|
+
index,
|
|
8861
|
+
canvas,
|
|
8862
|
+
canvasMetadata,
|
|
8863
|
+
lockAspectRatio: lockAspectRatio ?? ((_a = element.props) == null ? void 0 : _a.lockAspectRatio) ?? true
|
|
8864
|
+
});
|
|
8865
|
+
},
|
|
8866
|
+
updateFromFabricObject(object, element, context) {
|
|
8867
|
+
const canvasCenter = getObjectCanvasCenter(object);
|
|
8868
|
+
const { x: x2, y: y2 } = convertToVideoPosition(
|
|
8869
|
+
canvasCenter.x,
|
|
8870
|
+
canvasCenter.y,
|
|
8871
|
+
context.canvasMetadata,
|
|
8872
|
+
context.videoSize
|
|
8873
|
+
);
|
|
8874
|
+
if (object.type === "group") {
|
|
8875
|
+
const scaledW2 = (object.width ?? 0) * (object.scaleX ?? 1);
|
|
8876
|
+
const scaledH2 = (object.height ?? 0) * (object.scaleY ?? 1);
|
|
8877
|
+
const { width: fw, height: fh2 } = convertToVideoDimensions(
|
|
8878
|
+
scaledW2,
|
|
8879
|
+
scaledH2,
|
|
8880
|
+
context.canvasMetadata
|
|
8881
|
+
);
|
|
8882
|
+
const updatedFrameSize = [fw, fh2];
|
|
8883
|
+
const frame2 = element.frame;
|
|
8884
|
+
return {
|
|
8885
|
+
element: {
|
|
8886
|
+
...element,
|
|
8887
|
+
frame: {
|
|
8888
|
+
...frame2,
|
|
8889
|
+
rotation: getObjectCanvasAngle(object),
|
|
8890
|
+
size: updatedFrameSize,
|
|
8891
|
+
x: x2,
|
|
8892
|
+
y: y2
|
|
8893
|
+
}
|
|
8894
|
+
}
|
|
8895
|
+
};
|
|
8896
|
+
}
|
|
8897
|
+
const scaledW = (object.width ?? 0) * (object.scaleX ?? 1);
|
|
8898
|
+
const scaledH = (object.height ?? 0) * (object.scaleY ?? 1);
|
|
8899
|
+
const { width, height } = convertToVideoDimensions(
|
|
8900
|
+
scaledW,
|
|
8901
|
+
scaledH,
|
|
8902
|
+
context.canvasMetadata
|
|
8903
|
+
);
|
|
8904
|
+
return {
|
|
8905
|
+
element: {
|
|
8906
|
+
...element,
|
|
8907
|
+
props: {
|
|
8908
|
+
...element.props,
|
|
8909
|
+
rotation: getObjectCanvasAngle(object),
|
|
8910
|
+
width,
|
|
8911
|
+
height,
|
|
8912
|
+
x: x2,
|
|
8913
|
+
y: y2
|
|
8914
|
+
}
|
|
8915
|
+
}
|
|
8916
|
+
};
|
|
8917
|
+
}
|
|
8918
|
+
};
|
|
8547
8919
|
class ElementController {
|
|
8548
8920
|
constructor() {
|
|
8549
8921
|
__publicField2(this, "elements", /* @__PURE__ */ new Map());
|
|
@@ -8565,6 +8937,7 @@ function registerElements() {
|
|
|
8565
8937
|
elementController.register(RectElement);
|
|
8566
8938
|
elementController.register(CircleElement);
|
|
8567
8939
|
elementController.register(TextElement);
|
|
8940
|
+
elementController.register(EmojiElement);
|
|
8568
8941
|
elementController.register(CaptionElement);
|
|
8569
8942
|
elementController.register(WatermarkElement);
|
|
8570
8943
|
elementController.register(ArrowElement);
|
|
@@ -9149,6 +9522,8 @@ const DEFAULT_ELEMENT_COLORS = {
|
|
|
9149
9522
|
animation: "#4B9B78",
|
|
9150
9523
|
/** Icon element color - bright orchid */
|
|
9151
9524
|
icon: "#A76CD4",
|
|
9525
|
+
/** Emoji element color - warm amber */
|
|
9526
|
+
emoji: "#F59E0B",
|
|
9152
9527
|
/** Circle element color - deep byzantium */
|
|
9153
9528
|
circle: "#703D8B",
|
|
9154
9529
|
/** Effect element color - cyan accent for global effects */
|
|
@@ -9204,6 +9579,7 @@ function useTimelineDrop({
|
|
|
9204
9579
|
zoomLevel,
|
|
9205
9580
|
labelWidth,
|
|
9206
9581
|
trackHeight,
|
|
9582
|
+
separatorHeight = 0,
|
|
9207
9583
|
/** Width of the track content area (timeline minus labels). Used for accurate time mapping. */
|
|
9208
9584
|
trackContentWidth,
|
|
9209
9585
|
onDrop,
|
|
@@ -9221,7 +9597,9 @@ function useTimelineDrop({
|
|
|
9221
9597
|
const viewportLeft = ((_b = (_a = scrollEl == null ? void 0 : scrollEl.getBoundingClientRect) == null ? void 0 : _a.call(scrollEl)) == null ? void 0 : _b.left) ?? rect.left;
|
|
9222
9598
|
const contentX = clientX - viewportLeft + scrollLeft - labelWidth;
|
|
9223
9599
|
const relY = clientY - rect.top;
|
|
9224
|
-
const
|
|
9600
|
+
const rowHeight = trackHeight + separatorHeight;
|
|
9601
|
+
const yFromFirstTrack = Math.max(0, relY - separatorHeight);
|
|
9602
|
+
const rawTrackIndex = Math.floor(yFromFirstTrack / Math.max(1, rowHeight));
|
|
9225
9603
|
const trackIndex = tracks.length === 0 ? 0 : Math.max(0, Math.min(tracks.length - 1, rawTrackIndex));
|
|
9226
9604
|
const pixelsPerSecond = trackContentWidth != null && trackContentWidth > 0 ? trackContentWidth / duration : 100 * zoomLevel;
|
|
9227
9605
|
const timeSec = Math.max(
|
|
@@ -9236,6 +9614,7 @@ function useTimelineDrop({
|
|
|
9236
9614
|
tracks.length,
|
|
9237
9615
|
labelWidth,
|
|
9238
9616
|
trackHeight,
|
|
9617
|
+
separatorHeight,
|
|
9239
9618
|
zoomLevel,
|
|
9240
9619
|
duration,
|
|
9241
9620
|
trackContentWidth
|
|
@@ -19037,66 +19416,766 @@ const setElementColors = (colors) => {
|
|
|
19037
19416
|
...colors
|
|
19038
19417
|
};
|
|
19039
19418
|
};
|
|
19040
|
-
const
|
|
19041
|
-
|
|
19042
|
-
|
|
19043
|
-
|
|
19044
|
-
|
|
19045
|
-
|
|
19046
|
-
|
|
19047
|
-
|
|
19048
|
-
|
|
19049
|
-
|
|
19050
|
-
|
|
19051
|
-
|
|
19052
|
-
|
|
19419
|
+
const VIEWPORT_MARGIN = 8;
|
|
19420
|
+
function clampMenuPosition(clientX, clientY, width, height) {
|
|
19421
|
+
const vw2 = window.innerWidth;
|
|
19422
|
+
const vh3 = window.innerHeight;
|
|
19423
|
+
const maxLeft = Math.max(VIEWPORT_MARGIN, vw2 - width - VIEWPORT_MARGIN);
|
|
19424
|
+
const maxTop = Math.max(VIEWPORT_MARGIN, vh3 - height - VIEWPORT_MARGIN);
|
|
19425
|
+
return {
|
|
19426
|
+
left: Math.min(Math.max(VIEWPORT_MARGIN, clientX), maxLeft),
|
|
19427
|
+
top: Math.min(Math.max(VIEWPORT_MARGIN, clientY), maxTop)
|
|
19428
|
+
};
|
|
19429
|
+
}
|
|
19430
|
+
const TrackElementContextMenu = ({
|
|
19431
|
+
x: x2,
|
|
19432
|
+
y: y2,
|
|
19433
|
+
canSplit,
|
|
19434
|
+
onSplit,
|
|
19435
|
+
onDelete,
|
|
19436
|
+
onClose
|
|
19053
19437
|
}) => {
|
|
19054
|
-
|
|
19055
|
-
const
|
|
19056
|
-
|
|
19057
|
-
|
|
19058
|
-
|
|
19059
|
-
const
|
|
19060
|
-
|
|
19061
|
-
|
|
19062
|
-
|
|
19438
|
+
const menuRef = React.useRef(null);
|
|
19439
|
+
const [position, setPosition] = React.useState(() => ({
|
|
19440
|
+
left: x2,
|
|
19441
|
+
top: y2
|
|
19442
|
+
}));
|
|
19443
|
+
const reposition = React.useCallback(() => {
|
|
19444
|
+
const el = menuRef.current;
|
|
19445
|
+
if (!el) return;
|
|
19446
|
+
const { width, height } = el.getBoundingClientRect();
|
|
19447
|
+
setPosition(clampMenuPosition(x2, y2, width, height));
|
|
19448
|
+
}, [x2, y2]);
|
|
19449
|
+
React.useLayoutEffect(() => {
|
|
19450
|
+
reposition();
|
|
19451
|
+
}, [reposition]);
|
|
19063
19452
|
React.useEffect(() => {
|
|
19064
|
-
|
|
19065
|
-
|
|
19066
|
-
|
|
19067
|
-
|
|
19068
|
-
|
|
19069
|
-
|
|
19070
|
-
|
|
19071
|
-
|
|
19072
|
-
|
|
19073
|
-
|
|
19074
|
-
|
|
19453
|
+
window.addEventListener("resize", reposition);
|
|
19454
|
+
return () => window.removeEventListener("resize", reposition);
|
|
19455
|
+
}, [reposition]);
|
|
19456
|
+
React.useEffect(() => {
|
|
19457
|
+
const handleClickOutside = (e3) => {
|
|
19458
|
+
if (menuRef.current && !menuRef.current.contains(e3.target)) {
|
|
19459
|
+
onClose();
|
|
19460
|
+
}
|
|
19461
|
+
};
|
|
19462
|
+
const handleEscape = (e3) => {
|
|
19463
|
+
if (e3.key === "Escape") onClose();
|
|
19464
|
+
};
|
|
19465
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
19466
|
+
document.addEventListener("keydown", handleEscape);
|
|
19467
|
+
return () => {
|
|
19468
|
+
document.removeEventListener("mousedown", handleClickOutside);
|
|
19469
|
+
document.removeEventListener("keydown", handleEscape);
|
|
19470
|
+
};
|
|
19471
|
+
}, [onClose]);
|
|
19472
|
+
const wrap = (fn2) => {
|
|
19473
|
+
fn2();
|
|
19474
|
+
onClose();
|
|
19475
|
+
};
|
|
19476
|
+
const menu = /* @__PURE__ */ jsxRuntime.jsxs(
|
|
19477
|
+
"div",
|
|
19478
|
+
{
|
|
19479
|
+
ref: menuRef,
|
|
19480
|
+
className: "twick-canvas-context-menu",
|
|
19481
|
+
style: { left: position.left, top: position.top },
|
|
19482
|
+
role: "menu",
|
|
19483
|
+
children: [
|
|
19484
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
19485
|
+
"button",
|
|
19486
|
+
{
|
|
19487
|
+
type: "button",
|
|
19488
|
+
className: "twick-canvas-context-menu-item",
|
|
19489
|
+
onClick: () => wrap(onSplit),
|
|
19490
|
+
disabled: !canSplit,
|
|
19491
|
+
role: "menuitem",
|
|
19492
|
+
style: !canSplit ? { opacity: 0.45, cursor: "not-allowed" } : void 0,
|
|
19493
|
+
children: "Split at playhead"
|
|
19494
|
+
}
|
|
19495
|
+
),
|
|
19496
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "twick-canvas-context-menu-separator", role: "separator" }),
|
|
19497
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
19498
|
+
"button",
|
|
19499
|
+
{
|
|
19500
|
+
type: "button",
|
|
19501
|
+
className: "twick-canvas-context-menu-item twick-canvas-context-menu-item-danger",
|
|
19502
|
+
onClick: () => wrap(onDelete),
|
|
19503
|
+
role: "menuitem",
|
|
19504
|
+
children: "Delete"
|
|
19505
|
+
}
|
|
19506
|
+
)
|
|
19507
|
+
]
|
|
19075
19508
|
}
|
|
19076
|
-
|
|
19077
|
-
|
|
19078
|
-
|
|
19079
|
-
|
|
19080
|
-
|
|
19081
|
-
|
|
19082
|
-
|
|
19083
|
-
|
|
19084
|
-
|
|
19085
|
-
|
|
19509
|
+
);
|
|
19510
|
+
if (typeof document === "undefined") {
|
|
19511
|
+
return null;
|
|
19512
|
+
}
|
|
19513
|
+
return reactDom.createPortal(menu, document.body);
|
|
19514
|
+
};
|
|
19515
|
+
const waveformCache = /* @__PURE__ */ new Map();
|
|
19516
|
+
const buildCacheKey = (src, bucketCount) => `${src}::${bucketCount}`;
|
|
19517
|
+
const getSeed = (src) => {
|
|
19518
|
+
let seed = 2166136261;
|
|
19519
|
+
for (let i2 = 0; i2 < src.length; i2 += 1) {
|
|
19520
|
+
seed ^= src.charCodeAt(i2);
|
|
19521
|
+
seed = Math.imul(seed, 16777619);
|
|
19522
|
+
}
|
|
19523
|
+
return seed >>> 0;
|
|
19524
|
+
};
|
|
19525
|
+
const generateFallbackPeaks = (bucketCount, seed) => {
|
|
19526
|
+
const peaks = new Float32Array(bucketCount);
|
|
19527
|
+
let randomState = seed || 123456789;
|
|
19528
|
+
for (let i2 = 0; i2 < bucketCount; i2 += 1) {
|
|
19529
|
+
randomState = 1103515245 * randomState + 12345 >>> 0;
|
|
19530
|
+
const noise = randomState % 1e3 / 1e3;
|
|
19531
|
+
const shape = 0.35 + 0.65 * Math.abs(Math.sin(i2 * 0.21 + noise * 2.7));
|
|
19532
|
+
peaks[i2] = Math.max(0.08, Math.min(1, shape));
|
|
19533
|
+
}
|
|
19534
|
+
return peaks;
|
|
19535
|
+
};
|
|
19536
|
+
const computePeaks = (channelData, bucketCount) => {
|
|
19537
|
+
const peaks = new Float32Array(bucketCount);
|
|
19538
|
+
const step = Math.max(1, Math.floor(channelData.length / bucketCount));
|
|
19539
|
+
for (let i2 = 0; i2 < bucketCount; i2 += 1) {
|
|
19540
|
+
const start = i2 * step;
|
|
19541
|
+
const end = Math.min(channelData.length, start + step);
|
|
19542
|
+
let peak = 0;
|
|
19543
|
+
for (let j2 = start; j2 < end; j2 += 1) {
|
|
19544
|
+
const sample = Math.abs(channelData[j2]);
|
|
19545
|
+
if (sample > peak) {
|
|
19546
|
+
peak = sample;
|
|
19547
|
+
}
|
|
19548
|
+
}
|
|
19549
|
+
peaks[i2] = peak;
|
|
19550
|
+
}
|
|
19551
|
+
return peaks;
|
|
19552
|
+
};
|
|
19553
|
+
const AudioWaveform = ({ src, widthPx, heightPx, label }) => {
|
|
19554
|
+
const canvasRef = React.useRef(null);
|
|
19555
|
+
const [peaks, setPeaks] = React.useState(null);
|
|
19556
|
+
const bucketCount = React.useMemo(
|
|
19557
|
+
() => Math.max(32, Math.min(2048, Math.floor(widthPx / 3))),
|
|
19558
|
+
[widthPx]
|
|
19559
|
+
);
|
|
19560
|
+
React.useEffect(() => {
|
|
19561
|
+
let isCancelled = false;
|
|
19562
|
+
const controller = new AbortController();
|
|
19563
|
+
const loadWaveform = async () => {
|
|
19564
|
+
if (!src) {
|
|
19565
|
+
setPeaks(generateFallbackPeaks(bucketCount, 1));
|
|
19566
|
+
return;
|
|
19567
|
+
}
|
|
19568
|
+
const cacheKey = buildCacheKey(src, bucketCount);
|
|
19569
|
+
const cached = waveformCache.get(cacheKey);
|
|
19570
|
+
if (cached) {
|
|
19571
|
+
setPeaks(cached);
|
|
19572
|
+
return;
|
|
19573
|
+
}
|
|
19574
|
+
try {
|
|
19575
|
+
const response = await fetch(src, { signal: controller.signal });
|
|
19576
|
+
if (!response.ok) {
|
|
19577
|
+
throw new Error(`Failed to fetch audio (${response.status})`);
|
|
19578
|
+
}
|
|
19579
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
19580
|
+
const audioCtx = new AudioContext();
|
|
19581
|
+
const audioBuffer = await audioCtx.decodeAudioData(arrayBuffer.slice(0));
|
|
19582
|
+
const channelData = audioBuffer.getChannelData(0);
|
|
19583
|
+
const computed = computePeaks(channelData, bucketCount);
|
|
19584
|
+
waveformCache.set(cacheKey, computed);
|
|
19585
|
+
if (!isCancelled) {
|
|
19586
|
+
setPeaks(computed);
|
|
19587
|
+
}
|
|
19588
|
+
await audioCtx.close();
|
|
19589
|
+
} catch {
|
|
19590
|
+
if (!isCancelled) {
|
|
19591
|
+
setPeaks(generateFallbackPeaks(bucketCount, getSeed(src)));
|
|
19086
19592
|
}
|
|
19087
19593
|
}
|
|
19088
|
-
|
|
19089
|
-
|
|
19090
|
-
|
|
19091
|
-
|
|
19092
|
-
|
|
19093
|
-
|
|
19094
|
-
|
|
19095
|
-
|
|
19096
|
-
|
|
19097
|
-
|
|
19098
|
-
|
|
19099
|
-
|
|
19594
|
+
};
|
|
19595
|
+
loadWaveform();
|
|
19596
|
+
return () => {
|
|
19597
|
+
isCancelled = true;
|
|
19598
|
+
controller.abort();
|
|
19599
|
+
};
|
|
19600
|
+
}, [src, bucketCount]);
|
|
19601
|
+
React.useEffect(() => {
|
|
19602
|
+
const canvas = canvasRef.current;
|
|
19603
|
+
if (!canvas) {
|
|
19604
|
+
return;
|
|
19605
|
+
}
|
|
19606
|
+
const dpr = window.devicePixelRatio || 1;
|
|
19607
|
+
canvas.width = Math.max(1, Math.floor(widthPx * dpr));
|
|
19608
|
+
canvas.height = Math.max(1, Math.floor(heightPx * dpr));
|
|
19609
|
+
canvas.style.width = `${widthPx}px`;
|
|
19610
|
+
canvas.style.height = `${heightPx}px`;
|
|
19611
|
+
const context = canvas.getContext("2d");
|
|
19612
|
+
if (!context) {
|
|
19613
|
+
return;
|
|
19614
|
+
}
|
|
19615
|
+
context.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
19616
|
+
context.clearRect(0, 0, widthPx, heightPx);
|
|
19617
|
+
const centerY = Math.floor(heightPx / 2) + 0.5;
|
|
19618
|
+
context.strokeStyle = "rgba(255, 255, 255, 0.18)";
|
|
19619
|
+
context.lineWidth = 1;
|
|
19620
|
+
context.beginPath();
|
|
19621
|
+
context.moveTo(0, centerY);
|
|
19622
|
+
context.lineTo(widthPx, centerY);
|
|
19623
|
+
context.stroke();
|
|
19624
|
+
if (!peaks || peaks.length === 0) {
|
|
19625
|
+
return;
|
|
19626
|
+
}
|
|
19627
|
+
const maxHeight = Math.max(6, heightPx - 10);
|
|
19628
|
+
let peakMax = 1e-3;
|
|
19629
|
+
for (let i2 = 0; i2 < peaks.length; i2 += 1) {
|
|
19630
|
+
peakMax = Math.max(peakMax, peaks[i2]);
|
|
19631
|
+
}
|
|
19632
|
+
const gap = 1;
|
|
19633
|
+
const barWidth = Math.max(1, Math.floor(widthPx / peaks.length) - gap);
|
|
19634
|
+
context.fillStyle = "rgba(255, 255, 255, 0.95)";
|
|
19635
|
+
for (let i2 = 0; i2 < peaks.length; i2 += 1) {
|
|
19636
|
+
const normalized = peaks[i2] / peakMax;
|
|
19637
|
+
const waveHeight = Math.max(2, normalized * maxHeight);
|
|
19638
|
+
const x2 = i2 * (barWidth + gap);
|
|
19639
|
+
if (x2 > widthPx) {
|
|
19640
|
+
break;
|
|
19641
|
+
}
|
|
19642
|
+
const y2 = (heightPx - waveHeight) / 2;
|
|
19643
|
+
context.fillRect(x2, y2, barWidth, waveHeight);
|
|
19644
|
+
}
|
|
19645
|
+
}, [widthPx, heightPx, peaks]);
|
|
19646
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "twick-audio-waveform-root", "aria-label": "Audio waveform", children: [
|
|
19647
|
+
/* @__PURE__ */ jsxRuntime.jsx("canvas", { ref: canvasRef, className: "twick-audio-waveform-canvas" }),
|
|
19648
|
+
label ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "twick-audio-waveform-label", children: label }) : null
|
|
19649
|
+
] });
|
|
19650
|
+
};
|
|
19651
|
+
class LRUCache2 {
|
|
19652
|
+
constructor(maxSize = 100) {
|
|
19653
|
+
if (maxSize <= 0) {
|
|
19654
|
+
throw new Error("maxSize must be greater than 0");
|
|
19655
|
+
}
|
|
19656
|
+
this.maxSize = maxSize;
|
|
19657
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
19658
|
+
}
|
|
19659
|
+
/**
|
|
19660
|
+
* Get a value from the cache.
|
|
19661
|
+
* Moves the item to the end (most recently used).
|
|
19662
|
+
*/
|
|
19663
|
+
get(key) {
|
|
19664
|
+
const value = this.cache.get(key);
|
|
19665
|
+
if (value === void 0) {
|
|
19666
|
+
return void 0;
|
|
19667
|
+
}
|
|
19668
|
+
this.cache.delete(key);
|
|
19669
|
+
this.cache.set(key, value);
|
|
19670
|
+
return value;
|
|
19671
|
+
}
|
|
19672
|
+
/**
|
|
19673
|
+
* Set a value in the cache.
|
|
19674
|
+
* If cache is full, removes the least recently used item.
|
|
19675
|
+
*/
|
|
19676
|
+
set(key, value) {
|
|
19677
|
+
if (this.cache.has(key)) {
|
|
19678
|
+
this.cache.delete(key);
|
|
19679
|
+
} else if (this.cache.size >= this.maxSize) {
|
|
19680
|
+
const firstKey = this.cache.keys().next().value;
|
|
19681
|
+
if (firstKey !== void 0) {
|
|
19682
|
+
this.cache.delete(firstKey);
|
|
19683
|
+
}
|
|
19684
|
+
}
|
|
19685
|
+
this.cache.set(key, value);
|
|
19686
|
+
}
|
|
19687
|
+
/**
|
|
19688
|
+
* Check if a key exists in the cache.
|
|
19689
|
+
*/
|
|
19690
|
+
has(key) {
|
|
19691
|
+
return this.cache.has(key);
|
|
19692
|
+
}
|
|
19693
|
+
/**
|
|
19694
|
+
* Delete a key from the cache.
|
|
19695
|
+
*/
|
|
19696
|
+
delete(key) {
|
|
19697
|
+
return this.cache.delete(key);
|
|
19698
|
+
}
|
|
19699
|
+
/**
|
|
19700
|
+
* Clear all entries from the cache.
|
|
19701
|
+
*/
|
|
19702
|
+
clear() {
|
|
19703
|
+
this.cache.clear();
|
|
19704
|
+
}
|
|
19705
|
+
/**
|
|
19706
|
+
* Get the current size of the cache.
|
|
19707
|
+
*/
|
|
19708
|
+
get size() {
|
|
19709
|
+
return this.cache.size;
|
|
19710
|
+
}
|
|
19711
|
+
}
|
|
19712
|
+
class VideoFrameExtractor2 {
|
|
19713
|
+
constructor(options = {}) {
|
|
19714
|
+
this.frameCache = new LRUCache2(
|
|
19715
|
+
options.maxCacheSize ?? 50
|
|
19716
|
+
);
|
|
19717
|
+
this.videoElements = /* @__PURE__ */ new Map();
|
|
19718
|
+
this.maxVideoElements = options.maxVideoElements ?? 5;
|
|
19719
|
+
this.loadTimeout = options.loadTimeout ?? 15e3;
|
|
19720
|
+
this.jpegQuality = options.jpegQuality ?? 0.8;
|
|
19721
|
+
this.playbackRate = options.playbackRate ?? 1;
|
|
19722
|
+
}
|
|
19723
|
+
/**
|
|
19724
|
+
* Get a frame thumbnail from a video at a specific time.
|
|
19725
|
+
* Uses caching and reuses video elements for optimal performance.
|
|
19726
|
+
* Uses 0.1s instead of 0 when seekTime is 0, since frames at t=0 are often blank.
|
|
19727
|
+
*
|
|
19728
|
+
* @param videoUrl - The URL of the video
|
|
19729
|
+
* @param seekTime - The time in seconds to extract the frame (0 is treated as 0.1)
|
|
19730
|
+
* @returns Promise resolving to a thumbnail image URL (data URL or blob URL)
|
|
19731
|
+
*/
|
|
19732
|
+
async getFrame(videoUrl, seekTime = 0.1) {
|
|
19733
|
+
const effectiveSeekTime = seekTime === 0 ? 0.1 : seekTime;
|
|
19734
|
+
const cacheKey = this.getCacheKey(videoUrl, effectiveSeekTime);
|
|
19735
|
+
const cached = this.frameCache.get(cacheKey);
|
|
19736
|
+
if (cached) {
|
|
19737
|
+
return cached;
|
|
19738
|
+
}
|
|
19739
|
+
const videoState = await this.getVideoElement(videoUrl);
|
|
19740
|
+
const thumbnail = await this.extractFrame(videoState.video, effectiveSeekTime);
|
|
19741
|
+
this.frameCache.set(cacheKey, thumbnail);
|
|
19742
|
+
return thumbnail;
|
|
19743
|
+
}
|
|
19744
|
+
/**
|
|
19745
|
+
* Get or create a video element for the given URL.
|
|
19746
|
+
* Reuses existing elements and manages cleanup.
|
|
19747
|
+
*/
|
|
19748
|
+
async getVideoElement(videoUrl) {
|
|
19749
|
+
let videoState = this.videoElements.get(videoUrl);
|
|
19750
|
+
if (videoState && videoState.isReady) {
|
|
19751
|
+
videoState.lastUsed = Date.now();
|
|
19752
|
+
return videoState;
|
|
19753
|
+
}
|
|
19754
|
+
if (videoState && videoState.isLoading && videoState.loadPromise) {
|
|
19755
|
+
await videoState.loadPromise;
|
|
19756
|
+
if (videoState.isReady) {
|
|
19757
|
+
videoState.lastUsed = Date.now();
|
|
19758
|
+
return videoState;
|
|
19759
|
+
}
|
|
19760
|
+
}
|
|
19761
|
+
if (this.videoElements.size >= this.maxVideoElements) {
|
|
19762
|
+
this.cleanupOldVideoElements();
|
|
19763
|
+
}
|
|
19764
|
+
videoState = await this.createVideoElement(videoUrl);
|
|
19765
|
+
this.videoElements.set(videoUrl, videoState);
|
|
19766
|
+
videoState.lastUsed = Date.now();
|
|
19767
|
+
return videoState;
|
|
19768
|
+
}
|
|
19769
|
+
/**
|
|
19770
|
+
* Create and initialize a new video element.
|
|
19771
|
+
*/
|
|
19772
|
+
async createVideoElement(videoUrl) {
|
|
19773
|
+
const video = document.createElement("video");
|
|
19774
|
+
video.crossOrigin = "anonymous";
|
|
19775
|
+
video.muted = true;
|
|
19776
|
+
video.playsInline = true;
|
|
19777
|
+
video.autoplay = false;
|
|
19778
|
+
video.preload = "auto";
|
|
19779
|
+
video.playbackRate = this.playbackRate;
|
|
19780
|
+
video.style.position = "absolute";
|
|
19781
|
+
video.style.left = "-9999px";
|
|
19782
|
+
video.style.top = "-9999px";
|
|
19783
|
+
video.style.width = "1px";
|
|
19784
|
+
video.style.height = "1px";
|
|
19785
|
+
video.style.opacity = "0";
|
|
19786
|
+
video.style.pointerEvents = "none";
|
|
19787
|
+
video.style.zIndex = "-1";
|
|
19788
|
+
const state = {
|
|
19789
|
+
video,
|
|
19790
|
+
isReady: false,
|
|
19791
|
+
isLoading: true,
|
|
19792
|
+
loadPromise: null,
|
|
19793
|
+
lastUsed: Date.now()
|
|
19794
|
+
};
|
|
19795
|
+
state.loadPromise = new Promise((resolve, reject) => {
|
|
19796
|
+
let timeoutId;
|
|
19797
|
+
const cleanup = () => {
|
|
19798
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
19799
|
+
};
|
|
19800
|
+
const handleError = () => {
|
|
19801
|
+
var _a;
|
|
19802
|
+
cleanup();
|
|
19803
|
+
state.isLoading = false;
|
|
19804
|
+
reject(new Error(`Failed to load video: ${((_a = video.error) == null ? void 0 : _a.message) || "Unknown error"}`));
|
|
19805
|
+
};
|
|
19806
|
+
const handleLoadedMetadata = () => {
|
|
19807
|
+
cleanup();
|
|
19808
|
+
state.isReady = true;
|
|
19809
|
+
state.isLoading = false;
|
|
19810
|
+
resolve();
|
|
19811
|
+
};
|
|
19812
|
+
video.addEventListener("error", handleError, { once: true });
|
|
19813
|
+
video.addEventListener("loadedmetadata", handleLoadedMetadata, { once: true });
|
|
19814
|
+
timeoutId = window.setTimeout(() => {
|
|
19815
|
+
cleanup();
|
|
19816
|
+
state.isLoading = false;
|
|
19817
|
+
reject(new Error("Video loading timed out"));
|
|
19818
|
+
}, this.loadTimeout);
|
|
19819
|
+
video.src = videoUrl;
|
|
19820
|
+
document.body.appendChild(video);
|
|
19821
|
+
});
|
|
19822
|
+
try {
|
|
19823
|
+
await state.loadPromise;
|
|
19824
|
+
} catch (error) {
|
|
19825
|
+
if (video.parentNode) {
|
|
19826
|
+
video.remove();
|
|
19827
|
+
}
|
|
19828
|
+
throw error;
|
|
19829
|
+
}
|
|
19830
|
+
return state;
|
|
19831
|
+
}
|
|
19832
|
+
/**
|
|
19833
|
+
* Extract a frame from a video at the specified time.
|
|
19834
|
+
*/
|
|
19835
|
+
async extractFrame(video, seekTime) {
|
|
19836
|
+
return new Promise((resolve, reject) => {
|
|
19837
|
+
video.pause();
|
|
19838
|
+
const timeThreshold = 0.1;
|
|
19839
|
+
if (Math.abs(video.currentTime - seekTime) < timeThreshold) {
|
|
19840
|
+
try {
|
|
19841
|
+
const canvas = document.createElement("canvas");
|
|
19842
|
+
const width = video.videoWidth || 640;
|
|
19843
|
+
const height = video.videoHeight || 360;
|
|
19844
|
+
canvas.width = width;
|
|
19845
|
+
canvas.height = height;
|
|
19846
|
+
const ctx = canvas.getContext("2d");
|
|
19847
|
+
if (!ctx) {
|
|
19848
|
+
reject(new Error("Failed to get canvas context"));
|
|
19849
|
+
return;
|
|
19850
|
+
}
|
|
19851
|
+
ctx.drawImage(video, 0, 0, width, height);
|
|
19852
|
+
try {
|
|
19853
|
+
const dataUrl = canvas.toDataURL("image/jpeg", this.jpegQuality);
|
|
19854
|
+
resolve(dataUrl);
|
|
19855
|
+
} catch {
|
|
19856
|
+
canvas.toBlob(
|
|
19857
|
+
(blob) => {
|
|
19858
|
+
if (!blob) {
|
|
19859
|
+
reject(new Error("Failed to create Blob"));
|
|
19860
|
+
return;
|
|
19861
|
+
}
|
|
19862
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
19863
|
+
resolve(blobUrl);
|
|
19864
|
+
},
|
|
19865
|
+
"image/jpeg",
|
|
19866
|
+
this.jpegQuality
|
|
19867
|
+
);
|
|
19868
|
+
}
|
|
19869
|
+
return;
|
|
19870
|
+
} catch (err) {
|
|
19871
|
+
reject(new Error(`Error creating thumbnail: ${err}`));
|
|
19872
|
+
return;
|
|
19873
|
+
}
|
|
19874
|
+
}
|
|
19875
|
+
const handleSeeked = () => {
|
|
19876
|
+
try {
|
|
19877
|
+
const canvas = document.createElement("canvas");
|
|
19878
|
+
const width = video.videoWidth || 640;
|
|
19879
|
+
const height = video.videoHeight || 360;
|
|
19880
|
+
canvas.width = width;
|
|
19881
|
+
canvas.height = height;
|
|
19882
|
+
const ctx = canvas.getContext("2d");
|
|
19883
|
+
if (!ctx) {
|
|
19884
|
+
reject(new Error("Failed to get canvas context"));
|
|
19885
|
+
return;
|
|
19886
|
+
}
|
|
19887
|
+
ctx.drawImage(video, 0, 0, width, height);
|
|
19888
|
+
try {
|
|
19889
|
+
const dataUrl = canvas.toDataURL("image/jpeg", this.jpegQuality);
|
|
19890
|
+
resolve(dataUrl);
|
|
19891
|
+
} catch {
|
|
19892
|
+
canvas.toBlob(
|
|
19893
|
+
(blob) => {
|
|
19894
|
+
if (!blob) {
|
|
19895
|
+
reject(new Error("Failed to create Blob"));
|
|
19896
|
+
return;
|
|
19897
|
+
}
|
|
19898
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
19899
|
+
resolve(blobUrl);
|
|
19900
|
+
},
|
|
19901
|
+
"image/jpeg",
|
|
19902
|
+
this.jpegQuality
|
|
19903
|
+
);
|
|
19904
|
+
}
|
|
19905
|
+
} catch (err) {
|
|
19906
|
+
reject(new Error(`Error creating thumbnail: ${err}`));
|
|
19907
|
+
}
|
|
19908
|
+
};
|
|
19909
|
+
video.addEventListener("seeked", handleSeeked, { once: true });
|
|
19910
|
+
const playPromise = video.play();
|
|
19911
|
+
if (playPromise !== void 0) {
|
|
19912
|
+
playPromise.then(() => {
|
|
19913
|
+
video.currentTime = seekTime;
|
|
19914
|
+
}).catch(() => {
|
|
19915
|
+
video.currentTime = seekTime;
|
|
19916
|
+
});
|
|
19917
|
+
} else {
|
|
19918
|
+
video.currentTime = seekTime;
|
|
19919
|
+
}
|
|
19920
|
+
});
|
|
19921
|
+
}
|
|
19922
|
+
/**
|
|
19923
|
+
* Generate cache key for a video URL and seek time.
|
|
19924
|
+
*/
|
|
19925
|
+
getCacheKey(videoUrl, seekTime) {
|
|
19926
|
+
const roundedTime = Math.round(seekTime * 100) / 100;
|
|
19927
|
+
return `${videoUrl}:${roundedTime}`;
|
|
19928
|
+
}
|
|
19929
|
+
/**
|
|
19930
|
+
* Cleanup least recently used video elements.
|
|
19931
|
+
*/
|
|
19932
|
+
cleanupOldVideoElements() {
|
|
19933
|
+
if (this.videoElements.size < this.maxVideoElements) {
|
|
19934
|
+
return;
|
|
19935
|
+
}
|
|
19936
|
+
const entries = Array.from(this.videoElements.entries());
|
|
19937
|
+
entries.sort((a2, b2) => a2[1].lastUsed - b2[1].lastUsed);
|
|
19938
|
+
const toRemove = entries.slice(0, entries.length - this.maxVideoElements + 1);
|
|
19939
|
+
for (const [url, state] of toRemove) {
|
|
19940
|
+
if (state.video.parentNode) {
|
|
19941
|
+
state.video.remove();
|
|
19942
|
+
}
|
|
19943
|
+
this.videoElements.delete(url);
|
|
19944
|
+
}
|
|
19945
|
+
}
|
|
19946
|
+
/**
|
|
19947
|
+
* Clear the frame cache.
|
|
19948
|
+
*/
|
|
19949
|
+
clearCache() {
|
|
19950
|
+
this.frameCache.clear();
|
|
19951
|
+
}
|
|
19952
|
+
/**
|
|
19953
|
+
* Remove a specific video element and clear its cached frames.
|
|
19954
|
+
*/
|
|
19955
|
+
removeVideo(videoUrl) {
|
|
19956
|
+
const state = this.videoElements.get(videoUrl);
|
|
19957
|
+
if (state) {
|
|
19958
|
+
if (state.video.parentNode) {
|
|
19959
|
+
state.video.remove();
|
|
19960
|
+
}
|
|
19961
|
+
this.videoElements.delete(videoUrl);
|
|
19962
|
+
}
|
|
19963
|
+
this.frameCache.clear();
|
|
19964
|
+
}
|
|
19965
|
+
/**
|
|
19966
|
+
* Dispose of all video elements and clear caches.
|
|
19967
|
+
* Removes all video elements from the DOM and clears both the frame cache
|
|
19968
|
+
* and video element cache. Call this when the extractor is no longer needed
|
|
19969
|
+
* to prevent memory leaks.
|
|
19970
|
+
*/
|
|
19971
|
+
dispose() {
|
|
19972
|
+
for (const state of this.videoElements.values()) {
|
|
19973
|
+
if (state.video.parentNode) {
|
|
19974
|
+
state.video.remove();
|
|
19975
|
+
}
|
|
19976
|
+
}
|
|
19977
|
+
this.videoElements.clear();
|
|
19978
|
+
this.frameCache.clear();
|
|
19979
|
+
}
|
|
19980
|
+
}
|
|
19981
|
+
let defaultExtractor = null;
|
|
19982
|
+
function getDefaultVideoFrameExtractor(options) {
|
|
19983
|
+
if (!defaultExtractor) {
|
|
19984
|
+
defaultExtractor = new VideoFrameExtractor2(options);
|
|
19985
|
+
}
|
|
19986
|
+
return defaultExtractor;
|
|
19987
|
+
}
|
|
19988
|
+
async function getThumbnailCached(videoUrl, seekTime = 0.1, playbackRate) {
|
|
19989
|
+
const extractor = getDefaultVideoFrameExtractor(
|
|
19990
|
+
void 0
|
|
19991
|
+
);
|
|
19992
|
+
return extractor.getFrame(videoUrl, seekTime);
|
|
19993
|
+
}
|
|
19994
|
+
const MAX_THUMBNAILS_PER_CLIP = 24;
|
|
19995
|
+
const MIN_THUMBNAIL_WIDTH = 40;
|
|
19996
|
+
const MAX_THUMBNAIL_WIDTH = 96;
|
|
19997
|
+
const videoThumbCache = /* @__PURE__ */ new Map();
|
|
19998
|
+
const inFlightVideoThumbs = /* @__PURE__ */ new Map();
|
|
19999
|
+
const getThumbCacheKey = (src, seekTime) => `${src}:${Math.round(seekTime * 100) / 100}`;
|
|
20000
|
+
const getCachedVideoThumbnail = async (src, seekTime) => {
|
|
20001
|
+
const key = getThumbCacheKey(src, seekTime);
|
|
20002
|
+
const cached = videoThumbCache.get(key);
|
|
20003
|
+
if (cached) {
|
|
20004
|
+
return cached;
|
|
20005
|
+
}
|
|
20006
|
+
const inFlight = inFlightVideoThumbs.get(key);
|
|
20007
|
+
if (inFlight) {
|
|
20008
|
+
return inFlight;
|
|
20009
|
+
}
|
|
20010
|
+
const request = getThumbnailCached(src, seekTime).then((thumb) => {
|
|
20011
|
+
videoThumbCache.set(key, thumb);
|
|
20012
|
+
inFlightVideoThumbs.delete(key);
|
|
20013
|
+
return thumb;
|
|
20014
|
+
}).catch(() => {
|
|
20015
|
+
inFlightVideoThumbs.delete(key);
|
|
20016
|
+
throw new Error("Thumbnail extraction failed");
|
|
20017
|
+
});
|
|
20018
|
+
inFlightVideoThumbs.set(key, request);
|
|
20019
|
+
return request;
|
|
20020
|
+
};
|
|
20021
|
+
const runWithConcurrencyLimit = async (tasks, concurrency) => {
|
|
20022
|
+
const active = /* @__PURE__ */ new Set();
|
|
20023
|
+
for (const task of tasks) {
|
|
20024
|
+
const promise = task();
|
|
20025
|
+
active.add(promise);
|
|
20026
|
+
promise.finally(() => active.delete(promise));
|
|
20027
|
+
if (active.size >= concurrency) {
|
|
20028
|
+
await Promise.race(active);
|
|
20029
|
+
}
|
|
20030
|
+
}
|
|
20031
|
+
await Promise.all(active);
|
|
20032
|
+
};
|
|
20033
|
+
const getThumbnailCount = (widthPx, heightPx) => {
|
|
20034
|
+
const targetThumbWidth = Math.max(
|
|
20035
|
+
MIN_THUMBNAIL_WIDTH,
|
|
20036
|
+
Math.min(MAX_THUMBNAIL_WIDTH, Math.round(heightPx * 1.5))
|
|
20037
|
+
);
|
|
20038
|
+
return Math.max(
|
|
20039
|
+
1,
|
|
20040
|
+
Math.min(MAX_THUMBNAILS_PER_CLIP, Math.ceil(widthPx / targetThumbWidth))
|
|
20041
|
+
);
|
|
20042
|
+
};
|
|
20043
|
+
const ImageTimelineStrip = ({ src, widthPx, heightPx }) => {
|
|
20044
|
+
const count = React.useMemo(() => getThumbnailCount(widthPx, heightPx), [widthPx, heightPx]);
|
|
20045
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "twick-media-strip twick-media-strip-image", "aria-hidden": "true", children: Array.from({ length: count }, (_2, index) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
20046
|
+
"img",
|
|
20047
|
+
{
|
|
20048
|
+
src,
|
|
20049
|
+
className: "twick-media-strip-thumb",
|
|
20050
|
+
alt: "",
|
|
20051
|
+
draggable: false
|
|
20052
|
+
},
|
|
20053
|
+
`${src}-${index}`
|
|
20054
|
+
)) });
|
|
20055
|
+
};
|
|
20056
|
+
const VideoTimelineStrip = ({
|
|
20057
|
+
src,
|
|
20058
|
+
widthPx,
|
|
20059
|
+
heightPx,
|
|
20060
|
+
durationSec,
|
|
20061
|
+
mediaOffsetSec = 0,
|
|
20062
|
+
playbackRate = 1,
|
|
20063
|
+
mediaDurationSec
|
|
20064
|
+
}) => {
|
|
20065
|
+
const count = React.useMemo(() => getThumbnailCount(widthPx, heightPx), [widthPx, heightPx]);
|
|
20066
|
+
const [thumbs, setThumbs] = React.useState({});
|
|
20067
|
+
const slots = React.useMemo(() => {
|
|
20068
|
+
const timelineDuration = Math.max(1e-3, durationSec);
|
|
20069
|
+
return Array.from({ length: count }, (_2, index) => {
|
|
20070
|
+
const progress2 = count === 1 ? 0 : index / (count - 1);
|
|
20071
|
+
const timelineTime = progress2 * timelineDuration;
|
|
20072
|
+
let mediaTime = Math.max(0, mediaOffsetSec + timelineTime * playbackRate);
|
|
20073
|
+
if (typeof mediaDurationSec === "number" && mediaDurationSec > 0) {
|
|
20074
|
+
mediaTime = Math.min(mediaTime, Math.max(0, mediaDurationSec - 0.05));
|
|
20075
|
+
}
|
|
20076
|
+
return mediaTime;
|
|
20077
|
+
});
|
|
20078
|
+
}, [count, durationSec, mediaOffsetSec, playbackRate, mediaDurationSec]);
|
|
20079
|
+
React.useEffect(() => {
|
|
20080
|
+
let cancelled = false;
|
|
20081
|
+
const loadThumbs = async () => {
|
|
20082
|
+
const nextThumbs = {};
|
|
20083
|
+
const tasks = slots.map((seekTime, index) => async () => {
|
|
20084
|
+
try {
|
|
20085
|
+
const thumb = await getCachedVideoThumbnail(src, seekTime);
|
|
20086
|
+
nextThumbs[index] = thumb;
|
|
20087
|
+
} catch {
|
|
20088
|
+
}
|
|
20089
|
+
});
|
|
20090
|
+
await runWithConcurrencyLimit(tasks, 3);
|
|
20091
|
+
if (!cancelled) {
|
|
20092
|
+
setThumbs(nextThumbs);
|
|
20093
|
+
}
|
|
20094
|
+
};
|
|
20095
|
+
loadThumbs();
|
|
20096
|
+
return () => {
|
|
20097
|
+
cancelled = true;
|
|
20098
|
+
};
|
|
20099
|
+
}, [src, slots]);
|
|
20100
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "twick-media-strip twick-media-strip-video", "aria-hidden": "true", children: slots.map((_2, index) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
20101
|
+
"img",
|
|
20102
|
+
{
|
|
20103
|
+
src: thumbs[index],
|
|
20104
|
+
className: "twick-media-strip-thumb",
|
|
20105
|
+
alt: "",
|
|
20106
|
+
draggable: false
|
|
20107
|
+
},
|
|
20108
|
+
`${src}-${index}`
|
|
20109
|
+
)) });
|
|
20110
|
+
};
|
|
20111
|
+
const TrackElementView = ({
|
|
20112
|
+
element,
|
|
20113
|
+
parentWidth,
|
|
20114
|
+
duration,
|
|
20115
|
+
nextStart,
|
|
20116
|
+
prevEnd,
|
|
20117
|
+
selectedItem,
|
|
20118
|
+
selectedIds,
|
|
20119
|
+
onSelection,
|
|
20120
|
+
onDrag,
|
|
20121
|
+
allowOverlap = false,
|
|
20122
|
+
onDragStateChange,
|
|
20123
|
+
elementColors,
|
|
20124
|
+
currentTime = 0,
|
|
20125
|
+
onContextMenuTarget,
|
|
20126
|
+
onDeleteElement,
|
|
20127
|
+
onSplitElement
|
|
20128
|
+
}) => {
|
|
20129
|
+
var _a, _b;
|
|
20130
|
+
const ref = React.useRef(null);
|
|
20131
|
+
const dragType = React.useRef(null);
|
|
20132
|
+
const lastPosRef = React.useRef(null);
|
|
20133
|
+
const [isDragging2, setIsDragging] = React.useState(false);
|
|
20134
|
+
const [clipMenu, setClipMenu] = React.useState(
|
|
20135
|
+
null
|
|
20136
|
+
);
|
|
20137
|
+
const [position, setPosition] = React.useState({
|
|
20138
|
+
start: 0,
|
|
20139
|
+
end: 0
|
|
20140
|
+
});
|
|
20141
|
+
React.useEffect(() => {
|
|
20142
|
+
setPosition({
|
|
20143
|
+
start: element.getStart(),
|
|
20144
|
+
end: element.getEnd()
|
|
20145
|
+
});
|
|
20146
|
+
}, [element.getStart(), element.getEnd(), parentWidth, duration]);
|
|
20147
|
+
const bind = useDrag(({ delta: [dx] }) => {
|
|
20148
|
+
if (!parentWidth) return;
|
|
20149
|
+
if (dx == 0) return;
|
|
20150
|
+
if (!isDragging2) {
|
|
20151
|
+
setIsDragging(true);
|
|
20152
|
+
onDragStateChange == null ? void 0 : onDragStateChange(true, element);
|
|
20153
|
+
}
|
|
20154
|
+
dragType.current = DRAG_TYPE.MOVE;
|
|
20155
|
+
setPosition((prev) => {
|
|
20156
|
+
const span = prev.end - prev.start;
|
|
20157
|
+
let newStart = prev.start + dx / parentWidth * duration;
|
|
20158
|
+
newStart = Math.max(0, Math.min(newStart, prev.end - MIN_DURATION));
|
|
20159
|
+
if (!allowOverlap) {
|
|
20160
|
+
if (prevEnd !== null && newStart < prevEnd) {
|
|
20161
|
+
newStart = prevEnd;
|
|
20162
|
+
} else if (nextStart !== null && !allowOverlap && newStart + span > nextStart) {
|
|
20163
|
+
newStart = nextStart - span;
|
|
20164
|
+
}
|
|
20165
|
+
}
|
|
20166
|
+
newStart = Math.max(0, Math.min(newStart, duration - span));
|
|
20167
|
+
return {
|
|
20168
|
+
start: newStart,
|
|
20169
|
+
end: newStart + span
|
|
20170
|
+
};
|
|
20171
|
+
});
|
|
20172
|
+
});
|
|
20173
|
+
const bindStartHandle = useDrag(({ delta: [dx], event }) => {
|
|
20174
|
+
if (event) {
|
|
20175
|
+
event.stopPropagation();
|
|
20176
|
+
}
|
|
20177
|
+
if (dx === 0) return;
|
|
20178
|
+
if (isDragging2) {
|
|
19100
20179
|
setIsDragging(false);
|
|
19101
20180
|
onDragStateChange == null ? void 0 : onDragStateChange(false, element);
|
|
19102
20181
|
}
|
|
@@ -19107,6 +20186,7 @@ const TrackElementView = ({
|
|
|
19107
20186
|
if (prevEnd !== null && !allowOverlap && newStart < prevEnd) {
|
|
19108
20187
|
newStart = prevEnd;
|
|
19109
20188
|
}
|
|
20189
|
+
newStart = Math.max(0, Math.min(newStart, prev.end - MIN_DURATION));
|
|
19110
20190
|
return {
|
|
19111
20191
|
start: newStart,
|
|
19112
20192
|
end: prev.end
|
|
@@ -19131,6 +20211,7 @@ const TrackElementView = ({
|
|
|
19131
20211
|
newEnd = nextStart;
|
|
19132
20212
|
}
|
|
19133
20213
|
}
|
|
20214
|
+
newEnd = Math.max(prev.start + MIN_DURATION, Math.min(newEnd, duration));
|
|
19134
20215
|
return {
|
|
19135
20216
|
start: prev.start,
|
|
19136
20217
|
end: newEnd
|
|
@@ -19168,7 +20249,7 @@ const TrackElementView = ({
|
|
|
19168
20249
|
};
|
|
19169
20250
|
const getElementColor = (elementType) => {
|
|
19170
20251
|
const colors = elementColors || ELEMENT_COLORS;
|
|
19171
|
-
const key = elementType === timeline.TIMELINE_ELEMENT_TYPE.VIDEO ? "video" : elementType === timeline.TIMELINE_ELEMENT_TYPE.AUDIO ? "audio" : elementType === timeline.TIMELINE_ELEMENT_TYPE.IMAGE ? "image" : elementType === timeline.TIMELINE_ELEMENT_TYPE.TEXT ? "text" : elementType === timeline.TIMELINE_ELEMENT_TYPE.CAPTION ? "caption" : elementType === timeline.TIMELINE_ELEMENT_TYPE.RECT ? "rect" : elementType === timeline.TIMELINE_ELEMENT_TYPE.CIRCLE ? "circle" : elementType === timeline.TIMELINE_ELEMENT_TYPE.ICON ? "icon" : elementType === timeline.TIMELINE_ELEMENT_TYPE.EFFECT ? "effect" : "element";
|
|
20252
|
+
const key = elementType === timeline.TIMELINE_ELEMENT_TYPE.VIDEO ? "video" : elementType === timeline.TIMELINE_ELEMENT_TYPE.AUDIO ? "audio" : elementType === timeline.TIMELINE_ELEMENT_TYPE.IMAGE ? "image" : elementType === timeline.TIMELINE_ELEMENT_TYPE.TEXT ? "text" : elementType === timeline.TIMELINE_ELEMENT_TYPE.CAPTION ? "caption" : elementType === timeline.TIMELINE_ELEMENT_TYPE.RECT ? "rect" : elementType === timeline.TIMELINE_ELEMENT_TYPE.CIRCLE ? "circle" : elementType === timeline.TIMELINE_ELEMENT_TYPE.ICON ? "icon" : elementType === timeline.TIMELINE_ELEMENT_TYPE.EMOJI ? "emoji" : elementType === timeline.TIMELINE_ELEMENT_TYPE.EFFECT ? "effect" : "element";
|
|
19172
20253
|
if (key in colors) {
|
|
19173
20254
|
return colors[key];
|
|
19174
20255
|
}
|
|
@@ -19177,10 +20258,32 @@ const TrackElementView = ({
|
|
|
19177
20258
|
const isSelected = React.useMemo(() => {
|
|
19178
20259
|
return selectedIds.has(element.getId());
|
|
19179
20260
|
}, [selectedIds, element]);
|
|
20261
|
+
const isAudioElement = element.getType() === timeline.TIMELINE_ELEMENT_TYPE.AUDIO;
|
|
20262
|
+
const isVideoElement = element.getType() === timeline.TIMELINE_ELEMENT_TYPE.VIDEO;
|
|
20263
|
+
const isImageElement = element.getType() === timeline.TIMELINE_ELEMENT_TYPE.IMAGE;
|
|
20264
|
+
const elementLabel = element.getType() === timeline.TIMELINE_ELEMENT_TYPE.EFFECT ? ((_b = (_a = element.getProps) == null ? void 0 : _a.call(element)) == null ? void 0 : _b.effectKey) ?? "Effect" : element.getText ? element.getText() : element.getName() || element.getType();
|
|
20265
|
+
const mediaSrc = (isAudioElement || isVideoElement || isImageElement) && element.getSrc ? element.getSrc() : void 0;
|
|
20266
|
+
const mediaOffsetSec = isVideoElement && element.getStartAt ? element.getStartAt() : 0;
|
|
20267
|
+
const playbackRate = isVideoElement && element.getPlaybackRate ? element.getPlaybackRate() : 1;
|
|
20268
|
+
const mediaDurationSec = isVideoElement && element.getMediaDuration ? element.getMediaDuration() : void 0;
|
|
20269
|
+
const elementWidthPx = Math.max(
|
|
20270
|
+
1,
|
|
20271
|
+
(position.end - position.start) / Math.max(duration, MIN_DURATION) * parentWidth
|
|
20272
|
+
);
|
|
19180
20273
|
const hasHandles = (selectedItem == null ? void 0 : selectedItem.getId()) === element.getId();
|
|
20274
|
+
const contextActionsEnabled = Boolean(
|
|
20275
|
+
onDeleteElement && onSplitElement && onContextMenuTarget
|
|
20276
|
+
);
|
|
20277
|
+
const handleContextMenu = (e3) => {
|
|
20278
|
+
if (!contextActionsEnabled) return;
|
|
20279
|
+
e3.preventDefault();
|
|
20280
|
+
e3.stopPropagation();
|
|
20281
|
+
onContextMenuTarget == null ? void 0 : onContextMenuTarget(element);
|
|
20282
|
+
setClipMenu({ x: e3.clientX, y: e3.clientY });
|
|
20283
|
+
};
|
|
19181
20284
|
const motionProps = {
|
|
19182
20285
|
ref,
|
|
19183
|
-
className: `twick-track-element ${isSelected ? "twick-track-element-selected" : "twick-track-element-default"} ${isDragging2 ? "twick-track-element-dragging" : ""}`,
|
|
20286
|
+
className: `twick-track-element ${isSelected ? "twick-track-element-selected" : "twick-track-element-default"} ${isDragging2 ? "twick-track-element-dragging" : ""} ${isAudioElement ? "twick-track-element-audio" : ""}`,
|
|
19184
20287
|
onMouseDown: (e3) => {
|
|
19185
20288
|
if (e3.target === ref.current) {
|
|
19186
20289
|
setLastPos();
|
|
@@ -19198,6 +20301,7 @@ const TrackElementView = ({
|
|
|
19198
20301
|
onSelection(element, e3);
|
|
19199
20302
|
}
|
|
19200
20303
|
},
|
|
20304
|
+
onContextMenu: handleContextMenu,
|
|
19201
20305
|
style: {
|
|
19202
20306
|
backgroundColor: getElementColor(element.getType()),
|
|
19203
20307
|
width: `${(position.end - position.start) / duration * 100}%`,
|
|
@@ -19205,39 +20309,84 @@ const TrackElementView = ({
|
|
|
19205
20309
|
touchAction: "none"
|
|
19206
20310
|
}
|
|
19207
20311
|
};
|
|
19208
|
-
return /* @__PURE__ */ jsxRuntime.
|
|
19209
|
-
|
|
19210
|
-
|
|
20312
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(motion.div, { ...motionProps, children: [
|
|
20313
|
+
clipMenu && contextActionsEnabled ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
20314
|
+
TrackElementContextMenu,
|
|
19211
20315
|
{
|
|
19212
|
-
|
|
19213
|
-
|
|
19214
|
-
|
|
20316
|
+
x: clipMenu.x,
|
|
20317
|
+
y: clipMenu.y,
|
|
20318
|
+
canSplit: timeline.canSplitElement(element, currentTime),
|
|
20319
|
+
onSplit: () => onSplitElement == null ? void 0 : onSplitElement(element, currentTime),
|
|
20320
|
+
onDelete: () => onDeleteElement == null ? void 0 : onDeleteElement(element),
|
|
20321
|
+
onClose: () => setClipMenu(null)
|
|
19215
20322
|
}
|
|
19216
20323
|
) : null,
|
|
19217
|
-
/* @__PURE__ */ jsxRuntime.
|
|
19218
|
-
|
|
19219
|
-
"div",
|
|
19220
|
-
{
|
|
19221
|
-
style: { touchAction: "none", zIndex: isSelected ? 100 : 1 },
|
|
19222
|
-
...bindEndHandle(),
|
|
19223
|
-
className: "twick-track-element-handle twick-track-element-handle-end"
|
|
19224
|
-
}
|
|
19225
|
-
) : null,
|
|
19226
|
-
element.getFrameEffects ? element.getFrameEffects().map((frameEffect) => {
|
|
19227
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
20324
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: { touchAction: "none", height: "100%" }, ...bind(), children: [
|
|
20325
|
+
hasHandles ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
19228
20326
|
"div",
|
|
19229
20327
|
{
|
|
19230
|
-
|
|
19231
|
-
|
|
19232
|
-
|
|
19233
|
-
|
|
19234
|
-
|
|
19235
|
-
|
|
19236
|
-
|
|
19237
|
-
|
|
19238
|
-
|
|
19239
|
-
|
|
19240
|
-
|
|
20328
|
+
style: { touchAction: "none", zIndex: isSelected ? 100 : 1 },
|
|
20329
|
+
...bindStartHandle(),
|
|
20330
|
+
className: "twick-track-element-handle twick-track-element-handle-start"
|
|
20331
|
+
}
|
|
20332
|
+
) : null,
|
|
20333
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
20334
|
+
"div",
|
|
20335
|
+
{
|
|
20336
|
+
className: `twick-track-element-content ${isAudioElement ? "twick-track-element-content-audio" : ""}`,
|
|
20337
|
+
children: isAudioElement ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
20338
|
+
AudioWaveform,
|
|
20339
|
+
{
|
|
20340
|
+
src: mediaSrc,
|
|
20341
|
+
widthPx: elementWidthPx,
|
|
20342
|
+
heightPx: 46,
|
|
20343
|
+
label: elementLabel
|
|
20344
|
+
}
|
|
20345
|
+
) : isVideoElement && mediaSrc ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
20346
|
+
VideoTimelineStrip,
|
|
20347
|
+
{
|
|
20348
|
+
src: mediaSrc,
|
|
20349
|
+
widthPx: elementWidthPx,
|
|
20350
|
+
heightPx: 46,
|
|
20351
|
+
durationSec: Math.max(0, element.getDuration()),
|
|
20352
|
+
mediaOffsetSec,
|
|
20353
|
+
playbackRate,
|
|
20354
|
+
mediaDurationSec
|
|
20355
|
+
}
|
|
20356
|
+
) : isImageElement && mediaSrc ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
20357
|
+
ImageTimelineStrip,
|
|
20358
|
+
{
|
|
20359
|
+
src: mediaSrc,
|
|
20360
|
+
widthPx: elementWidthPx,
|
|
20361
|
+
heightPx: 46
|
|
20362
|
+
}
|
|
20363
|
+
) : elementLabel
|
|
20364
|
+
}
|
|
20365
|
+
),
|
|
20366
|
+
hasHandles ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
20367
|
+
"div",
|
|
20368
|
+
{
|
|
20369
|
+
style: { touchAction: "none", zIndex: isSelected ? 100 : 1 },
|
|
20370
|
+
...bindEndHandle(),
|
|
20371
|
+
className: "twick-track-element-handle twick-track-element-handle-end"
|
|
20372
|
+
}
|
|
20373
|
+
) : null,
|
|
20374
|
+
element.getFrameEffects ? element.getFrameEffects().map((frameEffect) => {
|
|
20375
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
20376
|
+
"div",
|
|
20377
|
+
{
|
|
20378
|
+
className: "twick-track-element-frame-effect",
|
|
20379
|
+
style: {
|
|
20380
|
+
backgroundColor: getElementColor("frameEffect"),
|
|
20381
|
+
width: `${(frameEffect.e - frameEffect.s) / element.getDuration() * 100}%`,
|
|
20382
|
+
left: `${frameEffect.s / element.getDuration() * 100}%`
|
|
20383
|
+
}
|
|
20384
|
+
},
|
|
20385
|
+
frameEffect.s + frameEffect.e
|
|
20386
|
+
);
|
|
20387
|
+
}) : null
|
|
20388
|
+
] })
|
|
20389
|
+
] });
|
|
19241
20390
|
};
|
|
19242
20391
|
const TrackBase = ({
|
|
19243
20392
|
duration,
|
|
@@ -19250,11 +20399,21 @@ const TrackBase = ({
|
|
|
19250
20399
|
onDrag,
|
|
19251
20400
|
allowOverlap = false,
|
|
19252
20401
|
onDragStateChange,
|
|
19253
|
-
elementColors
|
|
20402
|
+
elementColors,
|
|
20403
|
+
currentTime,
|
|
20404
|
+
onContextMenuTarget,
|
|
20405
|
+
onDeleteElement,
|
|
20406
|
+
onSplitElement
|
|
19254
20407
|
}) => {
|
|
19255
20408
|
const trackRef = React.useRef(null);
|
|
19256
20409
|
const trackWidthStyle = `${Math.max(100, duration * zoom * 100)}px`;
|
|
19257
|
-
const elements = track.getElements()
|
|
20410
|
+
const elements = [...track.getElements()].sort((a2, b2) => {
|
|
20411
|
+
const byStart = a2.getStart() - b2.getStart();
|
|
20412
|
+
if (byStart !== 0) return byStart;
|
|
20413
|
+
const byEnd = a2.getEnd() - b2.getEnd();
|
|
20414
|
+
if (byEnd !== 0) return byEnd;
|
|
20415
|
+
return a2.getId().localeCompare(b2.getId());
|
|
20416
|
+
});
|
|
19258
20417
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
19259
20418
|
"div",
|
|
19260
20419
|
{
|
|
@@ -19276,6 +20435,10 @@ const TrackBase = ({
|
|
|
19276
20435
|
onDrag,
|
|
19277
20436
|
onDragStateChange,
|
|
19278
20437
|
elementColors,
|
|
20438
|
+
currentTime,
|
|
20439
|
+
onContextMenuTarget,
|
|
20440
|
+
onDeleteElement,
|
|
20441
|
+
onSplitElement,
|
|
19279
20442
|
nextStart: index < elements.length - 1 ? elements[index + 1].getStart() : null,
|
|
19280
20443
|
prevEnd: index > 0 ? elements[index - 1].getEnd() : 0
|
|
19281
20444
|
},
|
|
@@ -19339,6 +20502,7 @@ function useMarqueeSelection({
|
|
|
19339
20502
|
labelWidth,
|
|
19340
20503
|
trackCount,
|
|
19341
20504
|
trackHeight,
|
|
20505
|
+
separatorHeight = 2,
|
|
19342
20506
|
tracks,
|
|
19343
20507
|
containerRef,
|
|
19344
20508
|
onMarqueeSelect,
|
|
@@ -19392,7 +20556,7 @@ function useMarqueeSelection({
|
|
|
19392
20556
|
const bottom = Math.max(currentMarquee.startY, currentMarquee.endY);
|
|
19393
20557
|
const startTime = Math.max(0, (left - labelWidth) / pixelsPerSecond);
|
|
19394
20558
|
const endTime = Math.min(duration, (right - labelWidth) / pixelsPerSecond);
|
|
19395
|
-
const rowHeight = trackHeight +
|
|
20559
|
+
const rowHeight = trackHeight + separatorHeight;
|
|
19396
20560
|
const startTrackIdx = Math.max(0, Math.floor(top / rowHeight));
|
|
19397
20561
|
const endTrackIdx = Math.min(
|
|
19398
20562
|
trackCount - 1,
|
|
@@ -19421,6 +20585,7 @@ function useMarqueeSelection({
|
|
|
19421
20585
|
labelWidth,
|
|
19422
20586
|
trackCount,
|
|
19423
20587
|
trackHeight,
|
|
20588
|
+
separatorHeight,
|
|
19424
20589
|
tracks,
|
|
19425
20590
|
onMarqueeSelect,
|
|
19426
20591
|
onEmptyClick,
|
|
@@ -19557,7 +20722,7 @@ function getTrackOrSeparatorAt(clientY, containerTop, trackHeight) {
|
|
|
19557
20722
|
return { type: "separator", separatorIndex: index + 1 };
|
|
19558
20723
|
}
|
|
19559
20724
|
const LABEL_WIDTH = 40;
|
|
19560
|
-
const TRACK_HEIGHT =
|
|
20725
|
+
const TRACK_HEIGHT = 52;
|
|
19561
20726
|
const SEPARATOR_HEIGHT = 6;
|
|
19562
20727
|
function TimelineView({
|
|
19563
20728
|
zoomLevel,
|
|
@@ -19580,7 +20745,11 @@ function TimelineView({
|
|
|
19580
20745
|
onDropOnTimeline,
|
|
19581
20746
|
videoResolution,
|
|
19582
20747
|
enableDropOnTimeline = true,
|
|
19583
|
-
chapters = []
|
|
20748
|
+
chapters = [],
|
|
20749
|
+
currentTime = 0,
|
|
20750
|
+
onContextMenuTarget,
|
|
20751
|
+
onDeleteElement,
|
|
20752
|
+
onSplitElement
|
|
19584
20753
|
}) {
|
|
19585
20754
|
const containerRef = React.useRef(null);
|
|
19586
20755
|
const seekContainerRef = React.useRef(null);
|
|
@@ -19696,6 +20865,7 @@ function TimelineView({
|
|
|
19696
20865
|
labelWidth: LABEL_WIDTH,
|
|
19697
20866
|
trackCount: (tracks == null ? void 0 : tracks.length) ?? 0,
|
|
19698
20867
|
trackHeight: TRACK_HEIGHT,
|
|
20868
|
+
separatorHeight: SEPARATOR_HEIGHT,
|
|
19699
20869
|
tracks: tracks ?? [],
|
|
19700
20870
|
containerRef: timelineContentRef,
|
|
19701
20871
|
onMarqueeSelect,
|
|
@@ -19709,6 +20879,7 @@ function TimelineView({
|
|
|
19709
20879
|
zoomLevel,
|
|
19710
20880
|
labelWidth: LABEL_WIDTH,
|
|
19711
20881
|
trackHeight: TRACK_HEIGHT,
|
|
20882
|
+
separatorHeight: SEPARATOR_HEIGHT,
|
|
19712
20883
|
trackContentWidth: timelineWidth - LABEL_WIDTH,
|
|
19713
20884
|
onDrop: onDropOnTimeline ?? (async () => {
|
|
19714
20885
|
}),
|
|
@@ -19804,7 +20975,7 @@ function TimelineView({
|
|
|
19804
20975
|
style: {
|
|
19805
20976
|
position: "absolute",
|
|
19806
20977
|
left: LABEL_WIDTH + preview.timeSec / duration * (timelineWidth - LABEL_WIDTH),
|
|
19807
|
-
top: preview.trackIndex * TRACK_HEIGHT + 2,
|
|
20978
|
+
top: SEPARATOR_HEIGHT + preview.trackIndex * (TRACK_HEIGHT + SEPARATOR_HEIGHT) + 2,
|
|
19808
20979
|
width: preview.widthPct / 100 * (timelineWidth - LABEL_WIDTH),
|
|
19809
20980
|
height: TRACK_HEIGHT - 4
|
|
19810
20981
|
}
|
|
@@ -19849,7 +21020,11 @@ function TimelineView({
|
|
|
19849
21020
|
onDragStateChange: (isDragging2, el) => {
|
|
19850
21021
|
setDraggingElementId(isDragging2 && el ? el.getId() : null);
|
|
19851
21022
|
},
|
|
19852
|
-
elementColors
|
|
21023
|
+
elementColors,
|
|
21024
|
+
currentTime,
|
|
21025
|
+
onContextMenuTarget,
|
|
21026
|
+
onDeleteElement,
|
|
21027
|
+
onSplitElement
|
|
19853
21028
|
}
|
|
19854
21029
|
)
|
|
19855
21030
|
] }),
|
|
@@ -19908,7 +21083,8 @@ const useTimelineManager = () => {
|
|
|
19908
21083
|
if (dragType === DRAG_TYPE.START) {
|
|
19909
21084
|
if (element instanceof timeline.VideoElement || element instanceof timeline.AudioElement) {
|
|
19910
21085
|
const elementProps = element.getProps();
|
|
19911
|
-
const
|
|
21086
|
+
const playbackRate = (elementProps == null ? void 0 : elementProps.playbackRate) || 1;
|
|
21087
|
+
const delta = (updates.start - element.getStart()) * playbackRate;
|
|
19912
21088
|
if (element instanceof timeline.AudioElement) {
|
|
19913
21089
|
element.setStartAt(element.getStartAt() + delta);
|
|
19914
21090
|
} else {
|
|
@@ -20021,6 +21197,38 @@ const useTimelineManager = () => {
|
|
|
20021
21197
|
totalDuration
|
|
20022
21198
|
};
|
|
20023
21199
|
};
|
|
21200
|
+
const useTimelineControl = () => {
|
|
21201
|
+
const { editor, setSelectedItem, selectedIds } = timeline.useTimelineContext();
|
|
21202
|
+
const deleteItem = (item) => {
|
|
21203
|
+
var _a;
|
|
21204
|
+
const tracks = ((_a = editor.getTimelineData()) == null ? void 0 : _a.tracks) ?? [];
|
|
21205
|
+
const toDelete = item !== void 0 ? [item] : timeline.resolveIds(selectedIds, tracks);
|
|
21206
|
+
for (const el of toDelete) {
|
|
21207
|
+
if (el instanceof timeline.Track) {
|
|
21208
|
+
editor.removeTrack(el);
|
|
21209
|
+
} else if (el instanceof timeline.TrackElement) {
|
|
21210
|
+
editor.removeElement(el);
|
|
21211
|
+
}
|
|
21212
|
+
}
|
|
21213
|
+
setSelectedItem(null);
|
|
21214
|
+
};
|
|
21215
|
+
const splitElement = (element, currentTime) => {
|
|
21216
|
+
if (!timeline.canSplitElement(element, currentTime)) return;
|
|
21217
|
+
void editor.splitElement(element, currentTime);
|
|
21218
|
+
};
|
|
21219
|
+
const handleUndo = () => {
|
|
21220
|
+
editor.undo();
|
|
21221
|
+
};
|
|
21222
|
+
const handleRedo = () => {
|
|
21223
|
+
editor.redo();
|
|
21224
|
+
};
|
|
21225
|
+
return {
|
|
21226
|
+
splitElement,
|
|
21227
|
+
deleteItem,
|
|
21228
|
+
handleUndo,
|
|
21229
|
+
handleRedo
|
|
21230
|
+
};
|
|
21231
|
+
};
|
|
20024
21232
|
function useTimelineSelection() {
|
|
20025
21233
|
const { editor, selectedIds, setSelection, setSelectedItem } = timeline.useTimelineContext();
|
|
20026
21234
|
const handleItemSelect = React.useCallback(
|
|
@@ -20101,8 +21309,9 @@ const TimelineManager = ({
|
|
|
20101
21309
|
elementColors
|
|
20102
21310
|
}) => {
|
|
20103
21311
|
var _a, _b;
|
|
20104
|
-
const { playerState } = livePlayer.useLivePlayerContext();
|
|
21312
|
+
const { playerState, currentTime } = livePlayer.useLivePlayerContext();
|
|
20105
21313
|
const { followPlayheadEnabled, editor, videoResolution, setSelectedItem } = timeline.useTimelineContext();
|
|
21314
|
+
const { deleteItem, splitElement } = useTimelineControl();
|
|
20106
21315
|
const {
|
|
20107
21316
|
timelineData,
|
|
20108
21317
|
totalDuration,
|
|
@@ -20123,6 +21332,12 @@ const TimelineManager = ({
|
|
|
20123
21332
|
setPlayheadState(state);
|
|
20124
21333
|
}, []);
|
|
20125
21334
|
const isPlayheadActive = followPlayheadEnabled && playerState === livePlayer.PLAYER_STATE.PLAYING || playheadState.isDragging;
|
|
21335
|
+
const handleContextMenuTarget = React.useCallback(
|
|
21336
|
+
(element) => {
|
|
21337
|
+
setSelectedItem(element);
|
|
21338
|
+
},
|
|
21339
|
+
[setSelectedItem]
|
|
21340
|
+
);
|
|
20126
21341
|
const handleDropOnTimeline = React.useCallback(
|
|
20127
21342
|
async (params) => {
|
|
20128
21343
|
const { track, timeSec, type, url } = params;
|
|
@@ -20170,6 +21385,10 @@ const TimelineManager = ({
|
|
|
20170
21385
|
onEmptyClick: handleEmptyClick,
|
|
20171
21386
|
onMarqueeSelect: handleMarqueeSelect,
|
|
20172
21387
|
elementColors,
|
|
21388
|
+
currentTime,
|
|
21389
|
+
onContextMenuTarget: handleContextMenuTarget,
|
|
21390
|
+
onDeleteElement: (el) => deleteItem(el),
|
|
21391
|
+
onSplitElement: (el, t2) => splitElement(el, t2),
|
|
20173
21392
|
playheadPositionPx: playheadState.positionPx,
|
|
20174
21393
|
isPlayheadActive,
|
|
20175
21394
|
chapters: ((_a = timelineData == null ? void 0 : timelineData.metadata) == null ? void 0 : _a.chapters) ?? [],
|
|
@@ -20253,11 +21472,12 @@ const PlayerControls = ({
|
|
|
20253
21472
|
}
|
|
20254
21473
|
}, [selectedIds.size, onDelete]);
|
|
20255
21474
|
const hasSelection = selectedIds.size > 0;
|
|
21475
|
+
const canSplitSelected = selectedItem instanceof timeline.TrackElement ? timeline.canSplitElement(selectedItem, currentTime) : false;
|
|
20256
21476
|
const handleSplit = React.useCallback(() => {
|
|
20257
|
-
if (selectedItem instanceof timeline.TrackElement && onSplit) {
|
|
21477
|
+
if (selectedItem instanceof timeline.TrackElement && onSplit && canSplitSelected) {
|
|
20258
21478
|
onSplit(selectedItem, currentTime);
|
|
20259
21479
|
}
|
|
20260
|
-
}, [selectedItem, onSplit, currentTime]);
|
|
21480
|
+
}, [selectedItem, onSplit, currentTime, canSplitSelected]);
|
|
20261
21481
|
const handleZoomIn = React.useCallback(() => {
|
|
20262
21482
|
if (setZoomLevel && zoomLevel < MAX_ZOOM) {
|
|
20263
21483
|
setZoomLevel(zoomLevel + ZOOM_STEP);
|
|
@@ -20284,9 +21504,9 @@ const PlayerControls = ({
|
|
|
20284
21504
|
"button",
|
|
20285
21505
|
{
|
|
20286
21506
|
onClick: handleSplit,
|
|
20287
|
-
disabled: !
|
|
21507
|
+
disabled: !canSplitSelected,
|
|
20288
21508
|
title: "Split",
|
|
20289
|
-
className: `control-btn split-btn ${!
|
|
21509
|
+
className: `control-btn split-btn ${!canSplitSelected ? "btn-disabled" : ""}`,
|
|
20290
21510
|
children: /* @__PURE__ */ jsxRuntime.jsx(Scissors, { className: "icon-md" })
|
|
20291
21511
|
}
|
|
20292
21512
|
),
|
|
@@ -20400,37 +21620,6 @@ const usePlayerControl = () => {
|
|
|
20400
21620
|
togglePlayback
|
|
20401
21621
|
};
|
|
20402
21622
|
};
|
|
20403
|
-
const useTimelineControl = () => {
|
|
20404
|
-
const { editor, setSelectedItem, selectedIds } = timeline.useTimelineContext();
|
|
20405
|
-
const deleteItem = (item) => {
|
|
20406
|
-
var _a;
|
|
20407
|
-
const tracks = ((_a = editor.getTimelineData()) == null ? void 0 : _a.tracks) ?? [];
|
|
20408
|
-
const toDelete = item !== void 0 ? [item] : timeline.resolveIds(selectedIds, tracks);
|
|
20409
|
-
for (const el of toDelete) {
|
|
20410
|
-
if (el instanceof timeline.Track) {
|
|
20411
|
-
editor.removeTrack(el);
|
|
20412
|
-
} else if (el instanceof timeline.TrackElement) {
|
|
20413
|
-
editor.removeElement(el);
|
|
20414
|
-
}
|
|
20415
|
-
}
|
|
20416
|
-
setSelectedItem(null);
|
|
20417
|
-
};
|
|
20418
|
-
const splitElement = (element, currentTime) => {
|
|
20419
|
-
editor.splitElement(element, currentTime);
|
|
20420
|
-
};
|
|
20421
|
-
const handleUndo = () => {
|
|
20422
|
-
editor.undo();
|
|
20423
|
-
};
|
|
20424
|
-
const handleRedo = () => {
|
|
20425
|
-
editor.redo();
|
|
20426
|
-
};
|
|
20427
|
-
return {
|
|
20428
|
-
splitElement,
|
|
20429
|
-
deleteItem,
|
|
20430
|
-
handleUndo,
|
|
20431
|
-
handleRedo
|
|
20432
|
-
};
|
|
20433
|
-
};
|
|
20434
21623
|
function shouldIgnoreKeydown() {
|
|
20435
21624
|
const active = document.activeElement;
|
|
20436
21625
|
if (!active) return false;
|