@twick/video-editor 0.14.6 → 0.14.8

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/index.mjs CHANGED
@@ -3,8 +3,8 @@ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { en
3
3
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
4
  import { jsxs, jsx } from "react/jsx-runtime";
5
5
  import { useLivePlayerContext, PLAYER_STATE, LivePlayer } from "@twick/live-player";
6
- import { useTimelineContext, TIMELINE_ACTION, getCurrentElements, ElementDeserializer, Track, getDecimalNumber, VideoElement, AudioElement, TrackElement } from "@twick/timeline";
7
- import React, { useState, useRef, useEffect, useCallback, forwardRef, createElement, createContext, useContext, useId, useLayoutEffect, useMemo, useInsertionEffect, Fragment, Component } from "react";
6
+ import { useTimelineContext, TIMELINE_ACTION, getCurrentElements, CaptionElement, ElementDeserializer, Track, getDecimalNumber, VideoElement, AudioElement, TrackElement, ValidationError, VALIDATION_ERROR_CODE } from "@twick/timeline";
7
+ import React, { useState, useRef, useEffect, useMemo, forwardRef, createElement, createContext, useContext, useId, useCallback, useLayoutEffect, useInsertionEffect, Fragment, Component } from "react";
8
8
  function t(t2, e3, s2) {
9
9
  return (e3 = function(t3) {
10
10
  var e4 = function(t4, e5) {
@@ -6851,7 +6851,9 @@ const CANVAS_OPERATIONS = {
6851
6851
  /** An item has been selected on the canvas */
6852
6852
  ITEM_SELECTED: "ITEM_SELECTED",
6853
6853
  /** An item has been updated/modified on the canvas */
6854
- ITEM_UPDATED: "ITEM_UPDATED"
6854
+ ITEM_UPDATED: "ITEM_UPDATED",
6855
+ /** Caption properties have been updated */
6856
+ CAPTION_PROPS_UPDATED: "CAPTION_PROPS_UPDATED"
6855
6857
  };
6856
6858
  const ELEMENT_TYPES = {
6857
6859
  /** Text element type */
@@ -6863,7 +6865,9 @@ const ELEMENT_TYPES = {
6863
6865
  /** Video element type */
6864
6866
  VIDEO: "video",
6865
6867
  /** Rectangle element type */
6866
- RECT: "rect"
6868
+ RECT: "rect",
6869
+ /** Circle element type */
6870
+ CIRCLE: "circle"
6867
6871
  };
6868
6872
  const isBrowser$2 = typeof window !== "undefined";
6869
6873
  const isCanvasSupported = isBrowser$2 && !!window.HTMLCanvasElement;
@@ -7223,33 +7227,33 @@ const addCaptionElement = ({
7223
7227
  captionProps,
7224
7228
  canvasMetadata
7225
7229
  }) => {
7226
- var _a2, _b, _c, _d, _e2, _f, _g, _h, _i2, _j, _k, _l, _m, _n2, _o2, _p, _q, _r2, _s2, _t2, _u, _v, _w, _x, _y, _z, _A;
7230
+ var _a2, _b, _c, _d, _e2, _f, _g, _h, _i2, _j, _k, _l, _m, _n2, _o2, _p, _q, _r2, _s2, _t2, _u, _v, _w, _x, _y, _z, _A, _B, _C, _D, _E, _F, _G;
7227
7231
  const { x: x2, y: y2 } = convertToCanvasPosition(
7228
- ((_b = (_a2 = element.props) == null ? void 0 : _a2.pos) == null ? void 0 : _b.x) || ((_c = captionProps == null ? void 0 : captionProps.pos) == null ? void 0 : _c.x) || 0,
7229
- ((_e2 = (_d = element.props) == null ? void 0 : _d.pos) == null ? void 0 : _e2.y) || ((_f = captionProps == null ? void 0 : captionProps.pos) == null ? void 0 : _f.y) || 0,
7232
+ ((captionProps == null ? void 0 : captionProps.applyToAll) ? captionProps == null ? void 0 : captionProps.x : (_a2 = element.props) == null ? void 0 : _a2.x) ?? 0,
7233
+ ((captionProps == null ? void 0 : captionProps.applyToAll) ? captionProps == null ? void 0 : captionProps.y : (_b = element.props) == null ? void 0 : _b.y) ?? 0,
7230
7234
  canvasMetadata
7231
7235
  );
7232
- const caption = new ko(((_g = element.props) == null ? void 0 : _g.text) || element.t || "", {
7236
+ const caption = new ko(((_c = element.props) == null ? void 0 : _c.text) || element.t || "", {
7233
7237
  left: x2,
7234
7238
  top: y2,
7235
7239
  originX: "center",
7236
7240
  originY: "center",
7237
- angle: ((_h = element.props) == null ? void 0 : _h.rotation) || 0,
7241
+ angle: ((_d = element.props) == null ? void 0 : _d.rotation) || 0,
7238
7242
  fontSize: Math.round(
7239
- (((_j = (_i2 = element.props) == null ? void 0 : _i2.font) == null ? void 0 : _j.size) || ((_k = captionProps.font) == null ? void 0 : _k.size) || DEFAULT_CAPTION_PROPS.size) * canvasMetadata.scaleX
7243
+ (((captionProps == null ? void 0 : captionProps.applyToAll) ? (_e2 = captionProps == null ? void 0 : captionProps.font) == null ? void 0 : _e2.size : ((_g = (_f = element.props) == null ? void 0 : _f.font) == null ? void 0 : _g.size) ?? ((_h = captionProps == null ? void 0 : captionProps.font) == null ? void 0 : _h.size)) ?? DEFAULT_CAPTION_PROPS.size) * canvasMetadata.scaleX
7240
7244
  ),
7241
- fontFamily: ((_m = (_l = element.props) == null ? void 0 : _l.font) == null ? void 0 : _m.family) || ((_n2 = captionProps.font) == null ? void 0 : _n2.family) || DEFAULT_CAPTION_PROPS.family,
7242
- fill: ((_o2 = element.props) == null ? void 0 : _o2.fill) || ((_p = captionProps.color) == null ? void 0 : _p.text) || DEFAULT_CAPTION_PROPS.fill,
7243
- fontWeight: DEFAULT_CAPTION_PROPS.fontWeight,
7244
- stroke: ((_q = element.props) == null ? void 0 : _q.stroke) || DEFAULT_CAPTION_PROPS.stroke,
7245
- opacity: ((_r2 = element.props) == null ? void 0 : _r2.opacity) ?? 1,
7245
+ fontFamily: ((captionProps == null ? void 0 : captionProps.applyToAll) ? (_i2 = captionProps == null ? void 0 : captionProps.font) == null ? void 0 : _i2.family : ((_k = (_j = element.props) == null ? void 0 : _j.font) == null ? void 0 : _k.family) ?? ((_l = captionProps == null ? void 0 : captionProps.font) == null ? void 0 : _l.family)) ?? DEFAULT_CAPTION_PROPS.family,
7246
+ fill: ((captionProps == null ? void 0 : captionProps.applyToAll) ? (_m = captionProps.color) == null ? void 0 : _m.text : ((_n2 = element.props) == null ? void 0 : _n2.fill) ?? ((_o2 = captionProps.color) == null ? void 0 : _o2.text)) ?? DEFAULT_CAPTION_PROPS.fill,
7247
+ fontWeight: ((captionProps == null ? void 0 : captionProps.applyToAll) ? (_p = captionProps == null ? void 0 : captionProps.font) == null ? void 0 : _p.weight : ((_q = element.props) == null ? void 0 : _q.fontWeight) ?? ((_r2 = captionProps == null ? void 0 : captionProps.font) == null ? void 0 : _r2.weight)) ?? DEFAULT_CAPTION_PROPS.fontWeight,
7248
+ stroke: ((captionProps == null ? void 0 : captionProps.applyToAll) ? captionProps == null ? void 0 : captionProps.stroke : ((_s2 = element.props) == null ? void 0 : _s2.stroke) ?? (captionProps == null ? void 0 : captionProps.stroke)) ?? DEFAULT_CAPTION_PROPS.stroke,
7249
+ opacity: ((captionProps == null ? void 0 : captionProps.applyToAll) ? captionProps == null ? void 0 : captionProps.opacity : ((_t2 = element.props) == null ? void 0 : _t2.opacity) ?? (captionProps == null ? void 0 : captionProps.opacity)) ?? 1,
7246
7250
  shadow: new ds({
7247
- offsetX: ((_t2 = (_s2 = element.props) == null ? void 0 : _s2.shadowOffset) == null ? void 0 : _t2[0]) || ((_u = DEFAULT_CAPTION_PROPS.shadowOffset) == null ? void 0 : _u[0]),
7248
- offsetY: ((_w = (_v = element.props) == null ? void 0 : _v.shadowOffset) == null ? void 0 : _w[1]) || ((_x = DEFAULT_CAPTION_PROPS.shadowOffset) == null ? void 0 : _x[1]),
7249
- blur: ((_y = element.props) == null ? void 0 : _y.shadowBlur) || DEFAULT_CAPTION_PROPS.shadowBlur,
7250
- color: ((_z = element.props) == null ? void 0 : _z.shadowColor) || DEFAULT_CAPTION_PROPS.shadowColor
7251
+ offsetX: ((captionProps == null ? void 0 : captionProps.applyToAll) ? (_u = captionProps == null ? void 0 : captionProps.shadowOffset) == null ? void 0 : _u[0] : ((_w = (_v = element.props) == null ? void 0 : _v.shadowOffset) == null ? void 0 : _w[0]) ?? ((_x = captionProps == null ? void 0 : captionProps.shadowOffset) == null ? void 0 : _x[0])) ?? ((_y = DEFAULT_CAPTION_PROPS.shadowOffset) == null ? void 0 : _y[0]),
7252
+ offsetY: ((captionProps == null ? void 0 : captionProps.applyToAll) ? (_z = captionProps == null ? void 0 : captionProps.shadowOffset) == null ? void 0 : _z[1] : ((_B = (_A = element.props) == null ? void 0 : _A.shadowOffset) == null ? void 0 : _B[1]) ?? ((_C = captionProps == null ? void 0 : captionProps.shadowOffset) == null ? void 0 : _C[1])) ?? ((_D = DEFAULT_CAPTION_PROPS.shadowOffset) == null ? void 0 : _D[1]),
7253
+ blur: ((captionProps == null ? void 0 : captionProps.applyToAll) ? captionProps == null ? void 0 : captionProps.shadowBlur : ((_E = element.props) == null ? void 0 : _E.shadowBlur) ?? (captionProps == null ? void 0 : captionProps.shadowBlur)) ?? DEFAULT_CAPTION_PROPS.shadowBlur,
7254
+ color: ((captionProps == null ? void 0 : captionProps.applyToAll) ? captionProps == null ? void 0 : captionProps.shadowColor : ((_F = element.props) == null ? void 0 : _F.shadowColor) ?? (captionProps == null ? void 0 : captionProps.shadowColor)) ?? DEFAULT_CAPTION_PROPS.shadowColor
7251
7255
  }),
7252
- strokeWidth: ((_A = element.props) == null ? void 0 : _A.lineWidth) || DEFAULT_CAPTION_PROPS.lineWidth
7256
+ strokeWidth: ((captionProps == null ? void 0 : captionProps.applyToAll) ? captionProps == null ? void 0 : captionProps.lineWidth : ((_G = element.props) == null ? void 0 : _G.lineWidth) ?? (captionProps == null ? void 0 : captionProps.lineWidth)) ?? DEFAULT_CAPTION_PROPS.lineWidth
7253
7257
  });
7254
7258
  caption.set("id", element.id);
7255
7259
  caption.set("zIndex", index);
@@ -7476,6 +7480,40 @@ const addRectElement = ({
7476
7480
  canvas.add(rect);
7477
7481
  return rect;
7478
7482
  };
7483
+ const addCircleElement = ({
7484
+ element,
7485
+ index,
7486
+ canvas,
7487
+ canvasMetadata
7488
+ }) => {
7489
+ var _a2, _b, _c, _d, _e2, _f;
7490
+ const { x: x2, y: y2 } = convertToCanvasPosition(
7491
+ ((_a2 = element.props) == null ? void 0 : _a2.x) || 0,
7492
+ ((_b = element.props) == null ? void 0 : _b.y) || 0,
7493
+ canvasMetadata
7494
+ );
7495
+ const circle = new qn({
7496
+ left: x2,
7497
+ // X-coordinate on the canvas
7498
+ top: y2,
7499
+ // Y-coordinate on the canvas
7500
+ radius: (((_c = element.props) == null ? void 0 : _c.radius) || 0) * canvasMetadata.scaleX,
7501
+ fill: ((_d = element.props) == null ? void 0 : _d.fill) || "#000000",
7502
+ stroke: ((_e2 = element.props) == null ? void 0 : _e2.stroke) || "#000000",
7503
+ strokeWidth: (((_f = element.props) == null ? void 0 : _f.lineWidth) || 0) * canvasMetadata.scaleX,
7504
+ originX: "center",
7505
+ originY: "center"
7506
+ });
7507
+ circle.controls.mt = disabledControl;
7508
+ circle.controls.mb = disabledControl;
7509
+ circle.controls.ml = disabledControl;
7510
+ circle.controls.mr = disabledControl;
7511
+ circle.controls.mtr = disabledControl;
7512
+ circle.set("id", element.id);
7513
+ circle.set("zIndex", index);
7514
+ canvas.add(circle);
7515
+ return circle;
7516
+ };
7479
7517
  const addBackgroundColor = ({
7480
7518
  element,
7481
7519
  index,
@@ -7517,6 +7555,7 @@ const useTwickCanvas = ({
7517
7555
  const twickCanvasRef = useRef(null);
7518
7556
  const videoSizeRef = useRef({ width: 1, height: 1 });
7519
7557
  const canvasResolutionRef = useRef({ width: 1, height: 1 });
7558
+ const captionPropsRef = useRef(null);
7520
7559
  const canvasMetadataRef = useRef({
7521
7560
  width: 0,
7522
7561
  height: 0,
@@ -7574,7 +7613,7 @@ const useTwickCanvas = ({
7574
7613
  }
7575
7614
  };
7576
7615
  const handleMouseUp = (event) => {
7577
- var _a2, _b;
7616
+ var _a2, _b, _c;
7578
7617
  if (event.target) {
7579
7618
  const object = event.target;
7580
7619
  const elementId = object.get("id");
@@ -7601,20 +7640,29 @@ const useTwickCanvas = ({
7601
7640
  videoSizeRef.current
7602
7641
  );
7603
7642
  if (elementMap.current[elementId].type === "caption") {
7604
- elementMap.current[elementId] = {
7605
- ...elementMap.current[elementId],
7606
- props: {
7607
- ...elementMap.current[elementId].props,
7608
- pos: {
7643
+ if ((_c = captionPropsRef.current) == null ? void 0 : _c.applyToAll) {
7644
+ onCanvasOperation == null ? void 0 : onCanvasOperation(CANVAS_OPERATIONS.CAPTION_PROPS_UPDATED, {
7645
+ element: elementMap.current[elementId],
7646
+ props: {
7647
+ ...captionPropsRef.current,
7609
7648
  x: x2,
7610
7649
  y: y2
7611
7650
  }
7612
- }
7613
- };
7614
- onCanvasOperation == null ? void 0 : onCanvasOperation(
7615
- CANVAS_OPERATIONS.ITEM_UPDATED,
7616
- elementMap.current[elementId]
7617
- );
7651
+ });
7652
+ } else {
7653
+ elementMap.current[elementId] = {
7654
+ ...elementMap.current[elementId],
7655
+ props: {
7656
+ ...elementMap.current[elementId].props,
7657
+ x: x2,
7658
+ y: y2
7659
+ }
7660
+ };
7661
+ onCanvasOperation == null ? void 0 : onCanvasOperation(
7662
+ CANVAS_OPERATIONS.ITEM_UPDATED,
7663
+ elementMap.current[elementId]
7664
+ );
7665
+ }
7618
7666
  } else {
7619
7667
  if ((object == null ? void 0 : object.type) === "group") {
7620
7668
  const currentFrameEffect = elementFrameMap.current[elementId];
@@ -7678,6 +7726,22 @@ const useTwickCanvas = ({
7678
7726
  y: y2
7679
7727
  }
7680
7728
  };
7729
+ } else if ((object == null ? void 0 : object.type) === "circle") {
7730
+ const radius = Number(
7731
+ (elementMap.current[elementId].props.radius * object.scaleX).toFixed(2)
7732
+ );
7733
+ elementMap.current[elementId] = {
7734
+ ...elementMap.current[elementId],
7735
+ props: {
7736
+ ...elementMap.current[elementId].props,
7737
+ rotation: object.angle,
7738
+ radius,
7739
+ height: radius * 2,
7740
+ width: radius * 2,
7741
+ x: x2,
7742
+ y: y2
7743
+ }
7744
+ };
7681
7745
  } else {
7682
7746
  elementMap.current[elementId] = {
7683
7747
  ...elementMap.current[elementId],
@@ -7720,6 +7784,7 @@ const useTwickCanvas = ({
7720
7784
  twickCanvas.renderAll();
7721
7785
  }
7722
7786
  }
7787
+ captionPropsRef.current = captionProps;
7723
7788
  await Promise.all(
7724
7789
  elements.map(async (element, index) => {
7725
7790
  try {
@@ -7758,7 +7823,10 @@ const useTwickCanvas = ({
7758
7823
  }
7759
7824
  switch (element.type) {
7760
7825
  case ELEMENT_TYPES.VIDEO:
7761
- const currentFrameEffect = getCurrentFrameEffect(element, seekTime || 0);
7826
+ const currentFrameEffect = getCurrentFrameEffect(
7827
+ element,
7828
+ seekTime || 0
7829
+ );
7762
7830
  elementFrameMap.current[element.id] = currentFrameEffect;
7763
7831
  const snapTime = ((seekTime || 0) - ((element == null ? void 0 : element.s) || 0)) * (((_a2 = element == null ? void 0 : element.props) == null ? void 0 : _a2.playbackRate) || 1) + (((_b = element == null ? void 0 : element.props) == null ? void 0 : _b.time) || 0);
7764
7832
  await addVideoElement({
@@ -7802,6 +7870,14 @@ const useTwickCanvas = ({
7802
7870
  canvasMetadata: canvasMetadataRef.current
7803
7871
  });
7804
7872
  break;
7873
+ case ELEMENT_TYPES.CIRCLE:
7874
+ await addCircleElement({
7875
+ element,
7876
+ index,
7877
+ canvas: twickCanvas,
7878
+ canvasMetadata: canvasMetadataRef.current
7879
+ });
7880
+ break;
7805
7881
  case ELEMENT_TYPES.TEXT:
7806
7882
  await addTextElement({
7807
7883
  element,
@@ -7837,25 +7913,39 @@ const usePlayerManager = ({
7837
7913
  videoProps
7838
7914
  }) => {
7839
7915
  const [projectData, setProjectData] = useState(null);
7840
- const { timelineAction, setTimelineAction, setSelectedItem, editor, changeLog } = useTimelineContext();
7916
+ const {
7917
+ timelineAction,
7918
+ setTimelineAction,
7919
+ setSelectedItem,
7920
+ editor,
7921
+ changeLog
7922
+ } = useTimelineContext();
7841
7923
  const currentChangeLog = useRef(changeLog);
7924
+ const prevSeekTime = useRef(0);
7842
7925
  const [playerUpdating, setPlayerUpdating] = useState(false);
7843
7926
  const handleCanvasReady = (canvas) => {
7844
7927
  console.log("canvas ready", canvas);
7845
7928
  };
7846
7929
  const handleCanvasOperation = (operation, data) => {
7847
- const element = ElementDeserializer.fromJSON(data);
7848
- switch (operation) {
7849
- case CANVAS_OPERATIONS.ITEM_SELECTED:
7850
- setSelectedItem(element);
7851
- break;
7852
- case CANVAS_OPERATIONS.ITEM_UPDATED:
7853
- if (element) {
7854
- const updatedElement = editor.updateElement(element);
7855
- currentChangeLog.current = currentChangeLog.current + 1;
7856
- setSelectedItem(updatedElement);
7857
- }
7858
- break;
7930
+ if (operation === CANVAS_OPERATIONS.CAPTION_PROPS_UPDATED) {
7931
+ const subtitlesTrack = editor.getSubtiltesTrack();
7932
+ subtitlesTrack == null ? void 0 : subtitlesTrack.setProps(data.props);
7933
+ setSelectedItem(data.element);
7934
+ editor.refresh();
7935
+ } else {
7936
+ const element = ElementDeserializer.fromJSON(data);
7937
+ switch (operation) {
7938
+ case CANVAS_OPERATIONS.ITEM_SELECTED:
7939
+ setSelectedItem(element);
7940
+ break;
7941
+ case CANVAS_OPERATIONS.ITEM_UPDATED:
7942
+ if (element) {
7943
+ const updatedElement = editor.updateElement(element);
7944
+ currentChangeLog.current = currentChangeLog.current + 1;
7945
+ setSelectedItem(updatedElement);
7946
+ }
7947
+ break;
7948
+ }
7859
7949
  }
7860
7950
  };
7861
7951
  const { twickCanvas, buildCanvas, setCanvasElements } = useTwickCanvas({
@@ -7864,17 +7954,25 @@ const usePlayerManager = ({
7864
7954
  });
7865
7955
  const updateCanvas = (seekTime) => {
7866
7956
  var _a2;
7867
- if (changeLog === currentChangeLog.current) {
7957
+ if (changeLog === currentChangeLog.current && seekTime === prevSeekTime.current) {
7868
7958
  return;
7869
7959
  }
7960
+ prevSeekTime.current = seekTime;
7870
7961
  const elements = getCurrentElements(
7871
7962
  seekTime,
7872
7963
  ((_a2 = editor.getTimelineData()) == null ? void 0 : _a2.tracks) ?? []
7873
7964
  );
7965
+ let captionProps = {};
7966
+ (elements || []).forEach((element) => {
7967
+ if (element instanceof CaptionElement) {
7968
+ const track = editor.getTrackById(element.getTrackId());
7969
+ captionProps = (track == null ? void 0 : track.getProps()) ?? {};
7970
+ }
7971
+ });
7874
7972
  setCanvasElements({
7875
7973
  elements,
7876
7974
  seekTime,
7877
- captionProps: {},
7975
+ captionProps,
7878
7976
  cleanAndAdd: true
7879
7977
  });
7880
7978
  currentChangeLog.current = changeLog;
@@ -7887,7 +7985,7 @@ const usePlayerManager = ({
7887
7985
  }
7888
7986
  };
7889
7987
  useEffect(() => {
7890
- var _a2, _b, _c, _d;
7988
+ var _a2, _b, _c, _d, _e2;
7891
7989
  switch (timelineAction.type) {
7892
7990
  case TIMELINE_ACTION.UPDATE_PLAYER_DATA:
7893
7991
  if (videoProps) {
@@ -7901,6 +7999,11 @@ const usePlayerManager = ({
7901
7999
  }
7902
8000
  };
7903
8001
  setProjectData(_latestProjectData);
8002
+ if (((_e2 = timelineAction.payload) == null ? void 0 : _e2.version) === 1) {
8003
+ setTimeout(() => {
8004
+ setPlayerUpdating(false);
8005
+ });
8006
+ }
7904
8007
  } else {
7905
8008
  setTimelineAction(TIMELINE_ACTION.ON_PLAYER_UPDATED, null);
7906
8009
  }
@@ -9353,11 +9456,10 @@ function SeekTrack({
9353
9456
  duration,
9354
9457
  zoom = 1,
9355
9458
  onSeek,
9356
- timelineCount = 0
9459
+ timelineCount = 0,
9460
+ timelineTickConfigs
9357
9461
  }) {
9358
- const canvasRef = useRef(null);
9359
9462
  const containerRef = useRef(null);
9360
- const [containerWidth, setContainerWidth] = useState(0);
9361
9463
  const [isDragging2, setIsDragging] = useState(false);
9362
9464
  const [seekPosition, setSeekPosition] = useState(0);
9363
9465
  const pixelsPerSecond = 100 * zoom;
@@ -9368,61 +9470,62 @@ function SeekTrack({
9368
9470
  setSeekPosition(currentTime * pixelsPerSecond);
9369
9471
  }
9370
9472
  }, [currentTime, pixelsPerSecond, isDragging2]);
9371
- const drawTimeline = useCallback(() => {
9372
- const canvas = canvasRef.current;
9373
- if (!canvas) return;
9374
- const ctx = canvas.getContext("2d", { alpha: false });
9375
- if (!ctx) return;
9376
- const dpr = window.devicePixelRatio || 1;
9377
- const displayWidth = Math.max(totalWidth, containerWidth);
9378
- canvas.width = displayWidth * dpr;
9379
- canvas.height = 32 * dpr;
9380
- canvas.style.width = `${displayWidth}px`;
9381
- canvas.style.height = "32px";
9382
- ctx.scale(dpr, dpr);
9383
- ctx.fillStyle = "#0f0f0f";
9384
- ctx.fillRect(0, 0, displayWidth, 32);
9385
- for (let i2 = 0; i2 <= Math.ceil(duration * 10); i2++) {
9386
- const time2 = i2 * 0.1;
9387
- const x2 = Math.floor(time2 * pixelsPerSecond) + 0.5;
9388
- const isSecond = i2 % 10 === 0;
9389
- ctx.beginPath();
9390
- ctx.moveTo(x2, 0);
9391
- ctx.lineTo(x2, isSecond ? 24 : 12);
9392
- ctx.strokeStyle = isSecond ? "rgba(255, 255, 255, 0.7)" : "rgba(255, 255, 255, 0.3)";
9393
- ctx.lineWidth = 1;
9394
- ctx.stroke();
9395
- if (isSecond) {
9396
- ctx.font = "bold 10px system-ui, sans-serif";
9397
- ctx.fillStyle = "rgba(255, 255, 255, 0.7)";
9398
- ctx.textAlign = "center";
9399
- ctx.textBaseline = "bottom";
9400
- if (time2 === 0) ;
9401
- else {
9402
- ctx.fillText(`${Math.floor(time2)}s`, x2, 32);
9473
+ const { majorIntervalSec, minorIntervalSec } = useMemo(() => {
9474
+ if (timelineTickConfigs && timelineTickConfigs.length > 0) {
9475
+ const sortedConfigs = [...timelineTickConfigs].sort((a2, b2) => a2.durationThreshold - b2.durationThreshold);
9476
+ for (const config of sortedConfigs) {
9477
+ if (duration < config.durationThreshold) {
9478
+ return {
9479
+ majorIntervalSec: config.majorInterval,
9480
+ minorIntervalSec: config.minorTicks > 0 ? config.majorInterval / (config.minorTicks + 1) : config.majorInterval
9481
+ };
9403
9482
  }
9404
9483
  }
9484
+ const lastConfig = sortedConfigs[sortedConfigs.length - 1];
9485
+ return {
9486
+ majorIntervalSec: lastConfig.majorInterval,
9487
+ minorIntervalSec: lastConfig.minorTicks > 0 ? lastConfig.majorInterval / (lastConfig.minorTicks + 1) : lastConfig.majorInterval
9488
+ };
9405
9489
  }
9406
- }, [duration, pixelsPerSecond, totalWidth, containerWidth]);
9407
- useEffect(() => {
9408
- if (containerRef.current) {
9409
- setContainerWidth(containerRef.current.clientWidth);
9490
+ let major = 1;
9491
+ let minors = 5;
9492
+ if (duration < 10) {
9493
+ major = 1;
9494
+ minors = 10;
9495
+ } else if (duration < 30) {
9496
+ major = 5;
9497
+ minors = 5;
9498
+ } else if (duration < 120) {
9499
+ major = 10;
9500
+ minors = 5;
9501
+ } else if (duration < 300) {
9502
+ major = 30;
9503
+ minors = 6;
9504
+ } else if (duration < 900) {
9505
+ major = 60;
9506
+ minors = 6;
9507
+ } else if (duration < 1800) {
9508
+ major = 120;
9509
+ minors = 4;
9510
+ } else if (duration < 3600) {
9511
+ major = 300;
9512
+ minors = 5;
9513
+ } else if (duration < 7200) {
9514
+ major = 600;
9515
+ minors = 10;
9516
+ } else {
9517
+ major = 1800;
9518
+ minors = 6;
9410
9519
  }
9411
- const handleResize = () => {
9412
- if (containerRef.current) {
9413
- setContainerWidth(containerRef.current.clientWidth);
9414
- }
9520
+ return {
9521
+ majorIntervalSec: major,
9522
+ minorIntervalSec: minors > 0 ? major / (minors + 1) : major
9415
9523
  };
9416
- window.addEventListener("resize", handleResize);
9417
- return () => window.removeEventListener("resize", handleResize);
9418
- }, []);
9419
- useEffect(() => {
9420
- drawTimeline();
9421
- }, [drawTimeline, duration, zoom, containerWidth]);
9524
+ }, [duration, timelineTickConfigs]);
9422
9525
  const handleSeek = (clientX) => {
9423
9526
  if (!containerRef.current) return;
9424
9527
  const rect = containerRef.current.getBoundingClientRect();
9425
- const x2 = clientX - rect.left;
9528
+ const x2 = clientX - rect.left + (containerRef.current.scrollLeft || 0);
9426
9529
  const newTime = Math.max(0, Math.min(duration, x2 / pixelsPerSecond));
9427
9530
  onSeek(newTime);
9428
9531
  };
@@ -9433,7 +9536,7 @@ function SeekTrack({
9433
9536
  setIsDragging(active);
9434
9537
  if (!containerRef.current) return;
9435
9538
  const rect = containerRef.current.getBoundingClientRect();
9436
- const xPos = x2 - rect.left;
9539
+ const xPos = x2 - rect.left + (containerRef.current.scrollLeft || 0);
9437
9540
  const newTime = Math.max(0, Math.min(duration, xPos / pixelsPerSecond));
9438
9541
  setSeekPosition(xPos);
9439
9542
  onSeek(newTime);
@@ -9442,24 +9545,94 @@ function SeekTrack({
9442
9545
  "div",
9443
9546
  {
9444
9547
  ref: containerRef,
9445
- className: "twick-seek-track-container",
9548
+ className: "twick-seek-track-container-no-scrollbar",
9446
9549
  onClick: (e3) => handleSeek(e3.clientX),
9550
+ style: {
9551
+ overflowX: "auto",
9552
+ overflowY: "hidden",
9553
+ position: "relative",
9554
+ scrollbarWidth: "none",
9555
+ // Firefox
9556
+ msOverflowStyle: "none"
9557
+ // IE/Edge
9558
+ },
9447
9559
  children: [
9448
- /* @__PURE__ */ jsx(
9449
- "canvas",
9450
- {
9451
- ref: canvasRef,
9452
- className: "twick-seek-track-canvas",
9453
- style: { minWidth: "100%" }
9560
+ (() => {
9561
+ const ticks = [];
9562
+ const labels = [];
9563
+ const epsilon = 1e-6;
9564
+ const tickPositions = /* @__PURE__ */ new Set();
9565
+ for (let t2 = 0; t2 <= duration + epsilon; t2 += minorIntervalSec) {
9566
+ tickPositions.add(Math.round(t2 * 1e3) / 1e3);
9454
9567
  }
9455
- ),
9568
+ tickPositions.forEach((t2) => {
9569
+ const left = t2 * pixelsPerSecond;
9570
+ const isMajor = Math.abs(t2 / majorIntervalSec - Math.round(t2 / majorIntervalSec)) < 1e-3;
9571
+ ticks.push(
9572
+ /* @__PURE__ */ jsx(
9573
+ "div",
9574
+ {
9575
+ style: {
9576
+ position: "absolute",
9577
+ left,
9578
+ top: 0,
9579
+ width: "1px",
9580
+ height: isMajor ? "12px" : "8px",
9581
+ backgroundColor: isMajor ? "rgba(255,255,255,0.5)" : "rgba(255,255,255,0.2)",
9582
+ pointerEvents: "none"
9583
+ }
9584
+ },
9585
+ `tick-${t2}`
9586
+ )
9587
+ );
9588
+ if (isMajor && t2 > epsilon) {
9589
+ labels.push(
9590
+ /* @__PURE__ */ jsx(
9591
+ "div",
9592
+ {
9593
+ style: {
9594
+ position: "absolute",
9595
+ left,
9596
+ bottom: "6px",
9597
+ transform: "translateX(-50%)",
9598
+ color: "rgba(255,255,255,0.7)",
9599
+ font: "bold 10px system-ui, sans-serif",
9600
+ pointerEvents: "none",
9601
+ textShadow: "1px 1px 2px rgba(0,0,0,0.8)"
9602
+ },
9603
+ children: `${Math.floor(t2)}s`
9604
+ },
9605
+ `lbl-${t2}`
9606
+ )
9607
+ );
9608
+ }
9609
+ });
9610
+ return /* @__PURE__ */ jsxs(
9611
+ "div",
9612
+ {
9613
+ style: {
9614
+ overflow: "hidden",
9615
+ position: "relative",
9616
+ width: `${Math.max(1, Math.round(totalWidth))}px`,
9617
+ height: "32px",
9618
+ backgroundColor: "#0f0f0f"
9619
+ },
9620
+ children: [
9621
+ ticks,
9622
+ labels
9623
+ ]
9624
+ }
9625
+ );
9626
+ })(),
9456
9627
  /* @__PURE__ */ jsxs(
9457
9628
  "div",
9458
9629
  {
9459
9630
  ...bind(),
9460
9631
  className: "twick-seek-track-playhead",
9461
9632
  style: {
9633
+ position: "absolute",
9462
9634
  left: seekPosition,
9635
+ top: 0,
9463
9636
  touchAction: "none",
9464
9637
  transition: isDragging2 ? "none" : "left 0.1s linear"
9465
9638
  },
@@ -9483,7 +9656,8 @@ const SeekControl = ({
9483
9656
  duration,
9484
9657
  zoom,
9485
9658
  timelineCount,
9486
- onSeek
9659
+ onSeek,
9660
+ timelineTickConfigs
9487
9661
  }) => {
9488
9662
  const { currentTime } = useLivePlayerContext();
9489
9663
  return /* @__PURE__ */ jsx(
@@ -9493,7 +9667,8 @@ const SeekControl = ({
9493
9667
  currentTime,
9494
9668
  zoom,
9495
9669
  onSeek,
9496
- timelineCount
9670
+ timelineCount,
9671
+ timelineTickConfigs
9497
9672
  }
9498
9673
  );
9499
9674
  };
@@ -16937,6 +17112,90 @@ const DRAG_TYPE = {
16937
17112
  END: "end"
16938
17113
  };
16939
17114
  const DEFAULT_TIMELINE_ZOOM = 1.5;
17115
+ const DEFAULT_TIMELINE_ZOOM_CONFIG = {
17116
+ /** Minimum zoom level (10%) */
17117
+ min: 0.1,
17118
+ /** Maximum zoom level (300%) */
17119
+ max: 3,
17120
+ /** Zoom step increment/decrement (10%) */
17121
+ step: 0.1,
17122
+ /** Default zoom level (150%) */
17123
+ default: 1.5
17124
+ };
17125
+ const DEFAULT_TIMELINE_TICK_CONFIGS = [
17126
+ {
17127
+ durationThreshold: 10,
17128
+ // < 10 seconds
17129
+ majorInterval: 1,
17130
+ // 1s major ticks
17131
+ minorTicks: 10
17132
+ // 0.1s minor ticks (10 minors between majors)
17133
+ },
17134
+ {
17135
+ durationThreshold: 30,
17136
+ // < 30 seconds
17137
+ majorInterval: 5,
17138
+ // 5s major ticks
17139
+ minorTicks: 5
17140
+ // 1s minor ticks (5 minors between majors)
17141
+ },
17142
+ {
17143
+ durationThreshold: 120,
17144
+ // < 2 minutes
17145
+ majorInterval: 10,
17146
+ // 10s major ticks
17147
+ minorTicks: 5
17148
+ // 2s minor ticks (5 minors between majors)
17149
+ },
17150
+ {
17151
+ durationThreshold: 300,
17152
+ // < 5 minutes
17153
+ majorInterval: 30,
17154
+ // 30s major ticks
17155
+ minorTicks: 6
17156
+ // 5s minor ticks (6 minors between majors)
17157
+ },
17158
+ {
17159
+ durationThreshold: 900,
17160
+ // < 15 minutes
17161
+ majorInterval: 60,
17162
+ // 1m major ticks
17163
+ minorTicks: 6
17164
+ // 10s minor ticks (6 minors between majors)
17165
+ },
17166
+ {
17167
+ durationThreshold: 1800,
17168
+ // < 30 minutes
17169
+ majorInterval: 120,
17170
+ // 2m major ticks
17171
+ minorTicks: 4
17172
+ // 30s minor ticks (4 minors between majors)
17173
+ },
17174
+ {
17175
+ durationThreshold: 3600,
17176
+ // < 1 hour
17177
+ majorInterval: 300,
17178
+ // 5m major ticks
17179
+ minorTicks: 5
17180
+ // 1m minor ticks (5 minors between majors)
17181
+ },
17182
+ {
17183
+ durationThreshold: 7200,
17184
+ // < 2 hours
17185
+ majorInterval: 600,
17186
+ // 10m major ticks
17187
+ minorTicks: 10
17188
+ // 1m minor ticks (10 minors between majors)
17189
+ },
17190
+ {
17191
+ durationThreshold: Infinity,
17192
+ // >= 2 hours
17193
+ majorInterval: 1800,
17194
+ // 30m major ticks
17195
+ minorTicks: 6
17196
+ // 5m minor ticks (6 minors between majors)
17197
+ }
17198
+ ];
16940
17199
  const DEFAULT_ELEMENT_COLORS = {
16941
17200
  /** Fragment element color - deep charcoal matching UI background */
16942
17201
  fragment: "#1A1A1A",
@@ -17032,7 +17291,8 @@ const TrackElementView = ({
17032
17291
  selectedItem,
17033
17292
  onSelection,
17034
17293
  onDrag,
17035
- allowOverlap = false
17294
+ allowOverlap = false,
17295
+ elementColors
17036
17296
  }) => {
17037
17297
  const ref = useRef(null);
17038
17298
  const dragType = useRef(null);
@@ -17132,8 +17392,9 @@ const TrackElementView = ({
17132
17392
  }
17133
17393
  };
17134
17394
  const getElementColor = (elementType) => {
17135
- if (elementType in ELEMENT_COLORS) {
17136
- return ELEMENT_COLORS[elementType];
17395
+ const colors = elementColors || ELEMENT_COLORS;
17396
+ if (elementType in colors) {
17397
+ return colors[elementType];
17137
17398
  }
17138
17399
  return ELEMENT_COLORS.element;
17139
17400
  };
@@ -17209,7 +17470,8 @@ const TrackBase = ({
17209
17470
  selectedItem,
17210
17471
  onItemSelection,
17211
17472
  onDrag,
17212
- allowOverlap = false
17473
+ allowOverlap = false,
17474
+ elementColors
17213
17475
  }) => {
17214
17476
  const trackRef = useRef(null);
17215
17477
  const trackWidthStyle = `${Math.max(100, duration * zoom * 100)}px`;
@@ -17232,6 +17494,7 @@ const TrackBase = ({
17232
17494
  selectedItem,
17233
17495
  onSelection: onItemSelection,
17234
17496
  onDrag,
17497
+ elementColors,
17235
17498
  nextStart: index < elements.length - 1 ? elements[index + 1].getStart() : null,
17236
17499
  prevEnd: index > 0 ? elements[index - 1].getEnd() : 0
17237
17500
  },
@@ -17249,7 +17512,8 @@ function TimelineView({
17249
17512
  onAddTrack,
17250
17513
  onReorder,
17251
17514
  onSelectionChange,
17252
- onElementDrag
17515
+ onElementDrag,
17516
+ elementColors
17253
17517
  }) {
17254
17518
  const containerRef = useRef(null);
17255
17519
  const seekContainerRef = useRef(null);
@@ -17361,7 +17625,8 @@ function TimelineView({
17361
17625
  allowOverlap: false,
17362
17626
  trackWidth: timelineWidth - labelWidth,
17363
17627
  onItemSelection: handleItemSelection,
17364
- onDrag: onElementDrag
17628
+ onDrag: onElementDrag,
17629
+ elementColors
17365
17630
  }
17366
17631
  )
17367
17632
  ] }, track.getId())) })
@@ -17424,7 +17689,9 @@ const useTimelineManager = () => {
17424
17689
  };
17425
17690
  };
17426
17691
  const TimelineManager = ({
17427
- trackZoom
17692
+ trackZoom,
17693
+ timelineTickConfigs,
17694
+ elementColors
17428
17695
  }) => {
17429
17696
  var _a2;
17430
17697
  const { timelineData, totalDuration, selectedItem, onAddTrack, onReorder, onElementDrag, onSeek, onSelectionChange } = useTimelineManager();
@@ -17442,13 +17709,15 @@ const TimelineManager = ({
17442
17709
  onElementDrag,
17443
17710
  onSeek,
17444
17711
  onSelectionChange,
17712
+ elementColors,
17445
17713
  seekTrack: /* @__PURE__ */ jsx(
17446
17714
  SeekControl,
17447
17715
  {
17448
17716
  duration: totalDuration,
17449
17717
  zoom: trackZoom,
17450
17718
  onSeek,
17451
- timelineCount: ((_a2 = timelineData == null ? void 0 : timelineData.tracks) == null ? void 0 : _a2.length) ?? 0
17719
+ timelineCount: ((_a2 = timelineData == null ? void 0 : timelineData.tracks) == null ? void 0 : _a2.length) ?? 0,
17720
+ timelineTickConfigs
17452
17721
  }
17453
17722
  )
17454
17723
  }
@@ -17476,9 +17745,6 @@ const UndoRedoControls = ({ canUndo, canRedo, onUndo, onRedo }) => {
17476
17745
  )
17477
17746
  ] });
17478
17747
  };
17479
- const MAX_ZOOM = 3;
17480
- const MIN_ZOOM = 0.5;
17481
- const ZOOM_STEP = 0.25;
17482
17748
  const PlayerControls = ({
17483
17749
  selectedItem,
17484
17750
  duration,
@@ -17493,8 +17759,12 @@ const PlayerControls = ({
17493
17759
  onDelete,
17494
17760
  zoomLevel = 1,
17495
17761
  setZoomLevel,
17496
- className = ""
17762
+ className = "",
17763
+ zoomConfig = DEFAULT_TIMELINE_ZOOM_CONFIG
17497
17764
  }) => {
17765
+ const MAX_ZOOM = zoomConfig.max;
17766
+ const MIN_ZOOM = zoomConfig.min;
17767
+ const ZOOM_STEP = zoomConfig.step;
17498
17768
  const formatTime = useCallback((time2) => {
17499
17769
  const minutes = Math.floor(time2 / 60);
17500
17770
  const seconds = Math.floor(time2 % 60);
@@ -17651,7 +17921,8 @@ const useTimelineControl = () => {
17651
17921
  };
17652
17922
  const ControlManager = ({
17653
17923
  trackZoom,
17654
- setTrackZoom
17924
+ setTrackZoom,
17925
+ zoomConfig
17655
17926
  }) => {
17656
17927
  const { currentTime, playerState } = useLivePlayerContext();
17657
17928
  const { togglePlayback } = usePlayerControl();
@@ -17672,7 +17943,8 @@ const ControlManager = ({
17672
17943
  onUndo: handleUndo,
17673
17944
  onRedo: handleRedo,
17674
17945
  zoomLevel: trackZoom,
17675
- setZoomLevel: setTrackZoom
17946
+ setZoomLevel: setTrackZoom,
17947
+ zoomConfig
17676
17948
  }
17677
17949
  ) });
17678
17950
  };
@@ -17683,7 +17955,16 @@ const VideoEditor = ({
17683
17955
  editorConfig,
17684
17956
  defaultPlayControls = true
17685
17957
  }) => {
17686
- const [trackZoom, setTrackZoom] = useState(DEFAULT_TIMELINE_ZOOM);
17958
+ const zoomConfig = editorConfig.timelineZoomConfig ?? DEFAULT_TIMELINE_ZOOM_CONFIG;
17959
+ const timelineTickConfigs = (editorConfig == null ? void 0 : editorConfig.timelineTickConfigs) ?? DEFAULT_TIMELINE_TICK_CONFIGS;
17960
+ const elementColors = useMemo(
17961
+ () => ({
17962
+ ...DEFAULT_ELEMENT_COLORS,
17963
+ ...(editorConfig == null ? void 0 : editorConfig.elementColors) || {}
17964
+ }),
17965
+ [editorConfig == null ? void 0 : editorConfig.elementColors]
17966
+ );
17967
+ const [trackZoom, setTrackZoom] = useState(zoomConfig.default);
17687
17968
  const useMemoizedPlayerManager = useMemo(
17688
17969
  () => /* @__PURE__ */ jsx(
17689
17970
  PlayerManager,
@@ -17703,11 +17984,73 @@ const VideoEditor = ({
17703
17984
  ] }),
17704
17985
  bottomPanel ? bottomPanel : null,
17705
17986
  /* @__PURE__ */ jsxs("div", { className: "twick-editor-timeline-section", children: [
17706
- defaultPlayControls ? /* @__PURE__ */ jsx(ControlManager, { trackZoom, setTrackZoom }) : null,
17707
- /* @__PURE__ */ jsx(TimelineManager, { trackZoom })
17987
+ defaultPlayControls ? /* @__PURE__ */ jsx(
17988
+ ControlManager,
17989
+ {
17990
+ trackZoom,
17991
+ setTrackZoom,
17992
+ zoomConfig
17993
+ }
17994
+ ) : null,
17995
+ /* @__PURE__ */ jsx(
17996
+ TimelineManager,
17997
+ {
17998
+ trackZoom,
17999
+ timelineTickConfigs,
18000
+ elementColors
18001
+ }
18002
+ )
17708
18003
  ] })
17709
18004
  ] });
17710
18005
  };
18006
+ const useEditorManager = () => {
18007
+ const { editor, selectedItem, setSelectedItem } = useTimelineContext();
18008
+ const { getCurrentTime } = useLivePlayerContext();
18009
+ const addElement = async (element) => {
18010
+ const currentTime = getCurrentTime();
18011
+ element.setStart(currentTime);
18012
+ try {
18013
+ if (selectedItem instanceof Track) {
18014
+ const result = await editor.addElementToTrack(selectedItem, element);
18015
+ if (result) {
18016
+ setSelectedItem(element);
18017
+ }
18018
+ } else {
18019
+ const newTrack = editor.addTrack(`Track_${Date.now()}`);
18020
+ const result = await editor.addElementToTrack(newTrack, element);
18021
+ if (result) {
18022
+ setSelectedItem(element);
18023
+ }
18024
+ }
18025
+ } catch (error) {
18026
+ if (error instanceof ValidationError) {
18027
+ if (error.errors.includes(VALIDATION_ERROR_CODE.COLLISION_ERROR)) {
18028
+ try {
18029
+ const newTrack = editor.addTrack(`Track_${Date.now()}`);
18030
+ const result = await editor.addElementToTrack(newTrack, element);
18031
+ if (result) {
18032
+ setSelectedItem(element);
18033
+ }
18034
+ } catch (error2) {
18035
+ }
18036
+ } else {
18037
+ console.log("TIMELINE ERROR: ", error.errors);
18038
+ }
18039
+ } else {
18040
+ console.log("TIMELINE ERROR: ", error);
18041
+ }
18042
+ }
18043
+ };
18044
+ const updateElement = (element) => {
18045
+ const updatedElement = editor.updateElement(element);
18046
+ editor.refresh();
18047
+ setSelectedItem(updatedElement);
18048
+ };
18049
+ return {
18050
+ addElement,
18051
+ updateElement
18052
+ };
18053
+ };
17711
18054
  class BaseMediaManager {
17712
18055
  }
17713
18056
  class BrowserMediaManager extends BaseMediaManager {
@@ -18055,7 +18398,9 @@ export {
18055
18398
  BaseMediaManager,
18056
18399
  BrowserMediaManager,
18057
18400
  DEFAULT_ELEMENT_COLORS,
18401
+ DEFAULT_TIMELINE_TICK_CONFIGS,
18058
18402
  DEFAULT_TIMELINE_ZOOM,
18403
+ DEFAULT_TIMELINE_ZOOM_CONFIG,
18059
18404
  DRAG_TYPE,
18060
18405
  INITIAL_TIMELINE_DATA,
18061
18406
  MIN_DURATION,
@@ -18066,6 +18411,7 @@ export {
18066
18411
  VideoEditor as default,
18067
18412
  getAnimationGif,
18068
18413
  setElementColors,
18414
+ useEditorManager,
18069
18415
  usePlayerControl,
18070
18416
  useTimelineControl
18071
18417
  };