summaryrefslogtreecommitdiff
path: root/apps/web/src/stores
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2025-08-28 22:39:19 +0200
committerMaze Winther <mazewinther@gmail.com>2025-08-28 22:39:19 +0200
commit2d834a49d7d222d7f70ee6b4cf7ab2827b88bb0e (patch)
treedb8a33eb12c6294cc0000caf05c600fe3c33069e /apps/web/src/stores
parent792fb6cdc06ebcd578d9e27821eb25a7dba9dbd2 (diff)
parenta7d90f7eac90966920c750134d7de9b611b2c38f (diff)
downloadopencut-2d834a49d7d222d7f70ee6b4cf7ab2827b88bb0e.tar.gz
Merge branch 'staging'
Diffstat (limited to 'apps/web/src/stores')
-rw-r--r--apps/web/src/stores/keybindings-store.ts4
-rw-r--r--apps/web/src/stores/media-store.ts78
-rw-r--r--apps/web/src/stores/sounds-store.ts6
-rw-r--r--apps/web/src/stores/text-properties-store.ts33
-rw-r--r--apps/web/src/stores/timeline-store.ts285
5 files changed, 241 insertions, 165 deletions
diff --git a/apps/web/src/stores/keybindings-store.ts b/apps/web/src/stores/keybindings-store.ts
index 4c11cfde..7fa0da5e 100644
--- a/apps/web/src/stores/keybindings-store.ts
+++ b/apps/web/src/stores/keybindings-store.ts
@@ -23,6 +23,8 @@ export const defaultKeybindings: KeybindingConfig = {
n: "toggle-snapping",
"ctrl+a": "select-all",
"ctrl+d": "duplicate-selected",
+ "ctrl+c": "copy-selected",
+ "ctrl+v": "paste-selected",
"ctrl+z": "undo",
"ctrl+shift+z": "redo",
"ctrl+y": "redo",
@@ -162,7 +164,7 @@ export const useKeybindingsStore = create<KeybindingsState>()(
}),
{
name: "opencut-keybindings",
- version: 1,
+ version: 2,
}
)
);
diff --git a/apps/web/src/stores/media-store.ts b/apps/web/src/stores/media-store.ts
index 95676d75..2b7cb62e 100644
--- a/apps/web/src/stores/media-store.ts
+++ b/apps/web/src/stores/media-store.ts
@@ -2,44 +2,21 @@ import { create } from "zustand";
import { storageService } from "@/lib/storage/storage-service";
import { useTimelineStore } from "./timeline-store";
import { generateUUID } from "@/lib/utils";
-
-export type MediaType = "image" | "video" | "audio";
-
-export interface MediaItem {
- id: string;
- name: string;
- type: MediaType;
- file: File;
- url?: string; // Object URL for preview
- thumbnailUrl?: string; // For video thumbnails
- duration?: number; // For video/audio duration
- width?: number; // For video/image width
- height?: number; // For video/image height
- fps?: number; // For video frame rate
- // Ephemeral items are used by timeline directly and should not appear in the media library or be persisted
- ephemeral?: boolean;
- // Text-specific properties
- content?: string; // Text content
- fontSize?: number; // Font size
- fontFamily?: string; // Font family
- color?: string; // Text color
- backgroundColor?: string; // Background color
- textAlign?: "left" | "center" | "right"; // Text alignment
-}
+import { MediaType, MediaFile } from "@/types/media";
interface MediaStore {
- mediaItems: MediaItem[];
+ mediaFiles: MediaFile[];
isLoading: boolean;
- // Actions - now require projectId
- addMediaItem: (
+ // Actions
+ addMediaFile: (
projectId: string,
- item: Omit<MediaItem, "id">
+ file: Omit<MediaFile, "id">
) => Promise<void>;
- removeMediaItem: (projectId: string, id: string) => Promise<void>;
+ removeMediaFile: (projectId: string, id: string) => Promise<void>;
loadProjectMedia: (projectId: string) => Promise<void>;
clearProjectMedia: (projectId: string) => Promise<void>;
- clearAllMedia: () => void; // Clear local state only
+ clearAllMedia: () => void;
}
// Helper function to determine file type
@@ -150,8 +127,7 @@ export const getMediaDuration = (file: File): Promise<number> => {
});
};
-// Helper to get aspect ratio from MediaItem
-export const getMediaAspectRatio = (item: MediaItem): number => {
+export const getMediaAspectRatio = (item: MediaFile): number => {
if (item.width && item.height) {
return item.width / item.height;
}
@@ -159,35 +135,35 @@ export const getMediaAspectRatio = (item: MediaItem): number => {
};
export const useMediaStore = create<MediaStore>((set, get) => ({
- mediaItems: [],
+ mediaFiles: [],
isLoading: false,
- addMediaItem: async (projectId, item) => {
- const newItem: MediaItem = {
- ...item,
+ addMediaFile: async (projectId, file) => {
+ const newItem: MediaFile = {
+ ...file,
id: generateUUID(),
};
// Add to local state immediately for UI responsiveness
set((state) => ({
- mediaItems: [...state.mediaItems, newItem],
+ mediaFiles: [...state.mediaFiles, newItem],
}));
// Save to persistent storage in background
try {
- await storageService.saveMediaItem(projectId, newItem);
+ await storageService.saveMediaFile(projectId, newItem);
} catch (error) {
console.error("Failed to save media item:", error);
// Remove from local state if save failed
set((state) => ({
- mediaItems: state.mediaItems.filter((media) => media.id !== newItem.id),
+ mediaFiles: state.mediaFiles.filter((media) => media.id !== newItem.id),
}));
}
},
- removeMediaItem: async (projectId: string, id: string) => {
+ removeMediaFile: async (projectId: string, id: string) => {
const state = get();
- const item = state.mediaItems.find((media) => media.id === id);
+ const item = state.mediaFiles.find((media) => media.id === id);
// Cleanup object URLs to prevent memory leaks
if (item?.url) {
@@ -199,7 +175,7 @@ export const useMediaStore = create<MediaStore>((set, get) => ({
// 1) Remove from local state immediately
set((state) => ({
- mediaItems: state.mediaItems.filter((media) => media.id !== id),
+ mediaFiles: state.mediaFiles.filter((media) => media.id !== id),
}));
// 2) Cascade into the timeline: remove any elements using this media ID
@@ -238,7 +214,7 @@ export const useMediaStore = create<MediaStore>((set, get) => ({
// 3) Remove from persistent storage
try {
- await storageService.deleteMediaItem(projectId, id);
+ await storageService.deleteMediaFile(projectId, id);
} catch (error) {
console.error("Failed to delete media item:", error);
}
@@ -248,7 +224,7 @@ export const useMediaStore = create<MediaStore>((set, get) => ({
set({ isLoading: true });
try {
- const mediaItems = await storageService.loadAllMediaItems(projectId);
+ const mediaItems = await storageService.loadAllMediaFiles(projectId);
// Regenerate thumbnails for video items
const updatedMediaItems = await Promise.all(
@@ -275,7 +251,7 @@ export const useMediaStore = create<MediaStore>((set, get) => ({
})
);
- set({ mediaItems: updatedMediaItems });
+ set({ mediaFiles: updatedMediaItems });
} catch (error) {
console.error("Failed to load media items:", error);
} finally {
@@ -287,7 +263,7 @@ export const useMediaStore = create<MediaStore>((set, get) => ({
const state = get();
// Cleanup all object URLs
- state.mediaItems.forEach((item) => {
+ state.mediaFiles.forEach((item) => {
if (item.url) {
URL.revokeObjectURL(item.url);
}
@@ -297,13 +273,13 @@ export const useMediaStore = create<MediaStore>((set, get) => ({
});
// Clear local state
- set({ mediaItems: [] });
+ set({ mediaFiles: [] });
// Clear persistent storage
try {
- const mediaIds = state.mediaItems.map((item) => item.id);
+ const mediaIds = state.mediaFiles.map((item) => item.id);
await Promise.all(
- mediaIds.map((id) => storageService.deleteMediaItem(projectId, id))
+ mediaIds.map((id) => storageService.deleteMediaFile(projectId, id))
);
} catch (error) {
console.error("Failed to clear media items from storage:", error);
@@ -314,7 +290,7 @@ export const useMediaStore = create<MediaStore>((set, get) => ({
const state = get();
// Cleanup all object URLs
- state.mediaItems.forEach((item) => {
+ state.mediaFiles.forEach((item) => {
if (item.url) {
URL.revokeObjectURL(item.url);
}
@@ -324,6 +300,6 @@ export const useMediaStore = create<MediaStore>((set, get) => ({
});
// Clear local state
- set({ mediaItems: [] });
+ set({ mediaFiles: [] });
},
}));
diff --git a/apps/web/src/stores/sounds-store.ts b/apps/web/src/stores/sounds-store.ts
index 215253fb..6dffa198 100644
--- a/apps/web/src/stores/sounds-store.ts
+++ b/apps/web/src/stores/sounds-store.ts
@@ -247,7 +247,7 @@ export const useSoundsStore = create<SoundsStore>((set, get) => ({
type: "audio/mpeg",
});
- await useMediaStore.getState().addMediaItem(activeProject.id, {
+ await useMediaStore.getState().addMediaFile(activeProject.id, {
name: sound.name,
type: "audio",
file,
@@ -257,12 +257,12 @@ export const useSoundsStore = create<SoundsStore>((set, get) => ({
const mediaItem = useMediaStore
.getState()
- .mediaItems.find((item) => item.file === file);
+ .mediaFiles.find((item) => item.file === file);
if (!mediaItem) throw new Error("Failed to create media item");
const success = useTimelineStore
.getState()
- .addMediaAtTime(mediaItem, usePlaybackStore.getState().currentTime);
+ .addElementAtTime(mediaItem, usePlaybackStore.getState().currentTime);
if (success) {
return true;
diff --git a/apps/web/src/stores/text-properties-store.ts b/apps/web/src/stores/text-properties-store.ts
new file mode 100644
index 00000000..b3ce3689
--- /dev/null
+++ b/apps/web/src/stores/text-properties-store.ts
@@ -0,0 +1,33 @@
+import { create } from "zustand";
+import { persist } from "zustand/middleware";
+
+export type TextPropertiesTab = "transform" | "style";
+
+export interface TextPropertiesTabMeta {
+ value: TextPropertiesTab;
+ label: string;
+}
+
+export const TEXT_PROPERTIES_TABS: ReadonlyArray<TextPropertiesTabMeta> = [
+ { value: "transform", label: "Transform" },
+ { value: "style", label: "Style" },
+] as const;
+
+export function isTextPropertiesTab(value: string): value is TextPropertiesTab {
+ return TEXT_PROPERTIES_TABS.some((t) => t.value === value);
+}
+
+interface TextPropertiesState {
+ activeTab: TextPropertiesTab;
+ setActiveTab: (tab: TextPropertiesTab) => void;
+}
+
+export const useTextPropertiesStore = create<TextPropertiesState>()(
+ persist(
+ (set) => ({
+ activeTab: "transform",
+ setActiveTab: (tab) => set({ activeTab: tab }),
+ }),
+ { name: "text-properties" }
+ )
+);
diff --git a/apps/web/src/stores/timeline-store.ts b/apps/web/src/stores/timeline-store.ts
index 3eada6de..f0bae00c 100644
--- a/apps/web/src/stores/timeline-store.ts
+++ b/apps/web/src/stores/timeline-store.ts
@@ -10,17 +10,15 @@ import {
ensureMainTrack,
validateElementTrackCompatibility,
} from "@/types/timeline";
-import {
- useMediaStore,
- getMediaAspectRatio,
- type MediaItem,
-} from "./media-store";
+import { useMediaStore, getMediaAspectRatio } from "./media-store";
+import { MediaFile } from "@/types/media";
import { findBestCanvasPreset } from "@/lib/editor-utils";
import { storageService } from "@/lib/storage/storage-service";
import { useProjectStore } from "./project-store";
import { generateUUID } from "@/lib/utils";
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
import { checkElementOverlaps, resolveElementOverlaps } from "@/lib/timeline";
+import { DEFAULT_TEXT_ELEMENT } from "@/constants/text-constants";
// Helper function to manage element naming with suffixes
const getElementNameWithSuffix = (
@@ -43,6 +41,11 @@ interface TimelineStore {
history: TimelineTrack[][];
redoStack: TimelineTrack[][];
+ // Clipboard buffer
+ clipboard: {
+ items: Array<{ trackType: TrackType; element: CreateTimelineElement }>;
+ } | null;
+
// Always returns properly ordered tracks with main track ensured
tracks: TimelineTrack[];
@@ -178,6 +181,10 @@ interface TimelineStore {
loadProjectTimeline: (projectId: string) => Promise<void>;
saveProjectTimeline: (projectId: string) => Promise<void>;
clearTimeline: () => void;
+
+ // Clipboard actions
+ copySelected: () => void;
+ pasteAtTime: (time: number) => void;
updateTextElement: (
trackId: string,
elementId: string,
@@ -207,10 +214,11 @@ interface TimelineStore {
excludeElementId?: string
) => boolean;
findOrCreateTrack: (trackType: TrackType) => string;
- addMediaAtTime: (item: MediaItem, currentTime?: number) => boolean;
- addTextAtTime: (item: TextElement, currentTime?: number) => boolean;
- addMediaToNewTrack: (item: MediaItem) => boolean;
- addTextToNewTrack: (item: TextElement | DragData) => boolean;
+ addElementAtTime: (
+ item: MediaFile | TextElement,
+ currentTime?: number
+ ) => boolean;
+ addElementToNewTrack: (item: MediaFile | TextElement | DragData) => boolean;
}
export const useTimelineStore = create<TimelineStore>((set, get) => {
@@ -254,6 +262,7 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
redoStack: [],
selectedElements: [],
rippleEditingEnabled: false,
+ clipboard: null,
// Snapping settings defaults
snappingEnabled: true,
@@ -494,15 +503,17 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
const newElement: TimelineElement = {
...elementData,
id: generateUUID(),
- startTime: elementData.startTime || 0,
- trimStart: 0,
- trimEnd: 0,
- ...(elementData.type === "media" ? { muted: false } : {}),
+ startTime: elementData.startTime,
+ trimStart: elementData.trimStart ?? 0,
+ trimEnd: elementData.trimEnd ?? 0,
+ ...(elementData.type === "media"
+ ? { muted: elementData.muted ?? false }
+ : {}),
} as TimelineElement;
if (isFirstElement && newElement.type === "media") {
const mediaStore = useMediaStore.getState();
- const mediaItem = mediaStore.mediaItems.find(
+ const mediaItem = mediaStore.mediaFiles.find(
(item) => item.id === newElement.mediaId
);
@@ -1144,7 +1155,7 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
}
try {
- await mediaStore.addMediaItem(
+ await mediaStore.addMediaFile(
projectStore.activeProject.id,
mediaData
);
@@ -1155,7 +1166,7 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
};
}
- const newMediaItem = mediaStore.mediaItems.find(
+ const newMediaItem = mediaStore.mediaFiles.find(
(item) => item.file === newFile
);
@@ -1219,7 +1230,7 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
getProjectThumbnail: async (projectId) => {
try {
const tracks = await storageService.loadTimeline(projectId);
- const mediaItems = await storageService.loadAllMediaItems(projectId);
+ const mediaItems = await storageService.loadAllMediaFiles(projectId);
if (!tracks || !mediaItems.length) return null;
@@ -1230,20 +1241,20 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
if (!firstMediaElement) return null;
- const mediaItem = mediaItems.find(
+ const mediaFile = mediaItems.find(
(item) => item.id === firstMediaElement.mediaId
);
- if (!mediaItem) return null;
+ if (!mediaFile) return null;
- if (mediaItem.type === "video" && mediaItem.file) {
+ if (mediaFile.type === "video" && mediaFile.file) {
const { generateVideoThumbnail } = await import(
"@/stores/media-store"
);
- const { thumbnailUrl } = await generateVideoThumbnail(mediaItem.file);
+ const { thumbnailUrl } = await generateVideoThumbnail(mediaFile.file);
return thumbnailUrl;
}
- if (mediaItem.type === "image" && mediaItem.url) {
- return mediaItem.url;
+ if (mediaFile.type === "image" && mediaFile.url) {
+ return mediaFile.url;
}
return null;
@@ -1348,7 +1359,12 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
clearTimeline: () => {
const defaultTracks = ensureMainTrack([]);
updateTracks(defaultTracks);
- set({ history: [], redoStack: [], selectedElements: [] });
+ set({
+ history: [],
+ redoStack: [],
+ selectedElements: [],
+ clipboard: null,
+ });
},
// Snapping actions
@@ -1401,30 +1417,24 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
return get().addTrack(trackType);
},
- addMediaAtTime: (item, currentTime = 0) => {
- const trackType = item.type === "audio" ? "audio" : "media";
- const duration =
- item.duration || TIMELINE_CONSTANTS.DEFAULT_IMAGE_DURATION;
-
- const tracks = get()._tracks.filter((t) => t.type === trackType);
-
- let targetTrackId = null;
- for (const track of tracks) {
- if (!get().checkElementOverlap(track.id, currentTime, duration)) {
- targetTrackId = track.id;
- break;
- }
- }
-
- if (!targetTrackId) {
- targetTrackId = get().addTrack(trackType);
+ addElementAtTime: (item: MediaFile | TextElement, currentTime = 0) => {
+ if (item.type === "text") {
+ const targetTrackId = get().insertTrackAt("text", 0);
+ get().addElementToTrack(
+ targetTrackId,
+ buildTextElement(item, currentTime)
+ );
+ return true;
}
+ const media = item as MediaFile;
+ const trackType = media.type === "audio" ? "audio" : "media";
+ const targetTrackId = get().insertTrackAt(trackType, 0);
get().addElementToTrack(targetTrackId, {
type: "media",
- mediaId: item.id,
- name: item.name,
- duration,
+ mediaId: media.id,
+ name: media.name,
+ duration: media.duration || TIMELINE_CONSTANTS.DEFAULT_IMAGE_DURATION,
startTime: currentTime,
trimStart: 0,
trimEnd: 0,
@@ -1433,42 +1443,24 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
return true;
},
- addTextAtTime: (item, currentTime = 0) => {
- const targetTrackId = get().insertTrackAt("text", 0);
-
- get().addElementToTrack(targetTrackId, {
- type: "text",
- name: item.name || "Text",
- content: item.content || "Default Text",
- duration: item.duration || TIMELINE_CONSTANTS.DEFAULT_TEXT_DURATION,
- startTime: currentTime,
- trimStart: 0,
- trimEnd: 0,
- fontSize: item.fontSize || 48,
- fontFamily: item.fontFamily || "Arial",
- color: item.color || "#ffffff",
- backgroundColor: item.backgroundColor || "transparent",
- textAlign: item.textAlign || "center",
- fontWeight: item.fontWeight || "normal",
- fontStyle: item.fontStyle || "normal",
- textDecoration: item.textDecoration || "none",
- x: item.x || 0,
- y: item.y || 0,
- rotation: item.rotation || 0,
- opacity: item.opacity !== undefined ? item.opacity : 1,
- });
- return true;
- },
-
- addMediaToNewTrack: (item) => {
- const trackType = item.type === "audio" ? "audio" : "media";
- const targetTrackId = get().findOrCreateTrack(trackType);
+ addElementToNewTrack: (item) => {
+ if (item.type === "text") {
+ const targetTrackId = get().insertTrackAt("text", 0);
+ get().addElementToTrack(
+ targetTrackId,
+ buildTextElement(item as TextElement | DragData, 0)
+ );
+ return true;
+ }
+ const media = item as MediaFile;
+ const trackType = media.type === "audio" ? "audio" : "media";
+ const targetTrackId = get().insertTrackAt(trackType, 0);
get().addElementToTrack(targetTrackId, {
type: "media",
- mediaId: item.id,
- name: item.name,
- duration: item.duration || TIMELINE_CONSTANTS.DEFAULT_IMAGE_DURATION,
+ mediaId: media.id,
+ name: media.name,
+ duration: media.duration || TIMELINE_CONSTANTS.DEFAULT_IMAGE_DURATION,
startTime: 0,
trimStart: 0,
trimEnd: 0,
@@ -1477,40 +1469,113 @@ export const useTimelineStore = create<TimelineStore>((set, get) => {
return true;
},
- addTextToNewTrack: (item) => {
- const targetTrackId = get().insertTrackAt("text", 0);
+ copySelected: () => {
+ const { selectedElements, _tracks } = get();
+ if (selectedElements.length === 0) return;
- get().addElementToTrack(targetTrackId, {
- type: "text",
- name: item.name || "Text",
- content:
- ("content" in item ? item.content : "Default Text") || "Default Text",
- duration: TIMELINE_CONSTANTS.DEFAULT_TEXT_DURATION,
- startTime: 0,
- trimStart: 0,
- trimEnd: 0,
- fontSize: ("fontSize" in item ? item.fontSize : 48) || 48,
- fontFamily:
- ("fontFamily" in item ? item.fontFamily : "Arial") || "Arial",
- color: ("color" in item ? item.color : "#ffffff") || "#ffffff",
- backgroundColor:
- ("backgroundColor" in item ? item.backgroundColor : "transparent") ||
- "transparent",
- textAlign:
- ("textAlign" in item ? item.textAlign : "center") || "center",
- fontWeight:
- ("fontWeight" in item ? item.fontWeight : "normal") || "normal",
- fontStyle:
- ("fontStyle" in item ? item.fontStyle : "normal") || "normal",
- textDecoration:
- ("textDecoration" in item ? item.textDecoration : "none") || "none",
- x: ("x" in item ? item.x : 0) || 0,
- y: ("y" in item ? item.y : 0) || 0,
- rotation: ("rotation" in item ? item.rotation : 0) || 0,
- opacity:
- "opacity" in item && item.opacity !== undefined ? item.opacity : 1,
- });
- return true;
+ const items: Array<{
+ trackType: TrackType;
+ element: CreateTimelineElement;
+ }> = [];
+
+ for (const { trackId, elementId } of selectedElements) {
+ const track = _tracks.find((t) => t.id === trackId);
+ const element = track?.elements.find((e) => e.id === elementId);
+ if (!track || !element) continue;
+
+ // Prepare a creation-friendly copy without id
+ const { id: _id, ...rest } = element as TimelineElement;
+ items.push({
+ trackType: track.type,
+ element: rest as CreateTimelineElement,
+ });
+ }
+
+ set({ clipboard: { items } });
+ },
+
+ pasteAtTime: (time) => {
+ const { clipboard } = get();
+ if (!clipboard || clipboard.items.length === 0) return;
+
+ // Determine reference start time offset based on earliest element in clipboard
+ const minStart = Math.min(
+ ...clipboard.items.map((x) => x.element.startTime)
+ );
+
+ get().pushHistory();
+
+ for (const item of clipboard.items) {
+ const targetTrackId = get().findOrCreateTrack(item.trackType);
+ const relativeOffset = item.element.startTime - minStart;
+ const startTime = Math.max(0, time + relativeOffset);
+
+ // Ensure no overlap on target track
+ const duration =
+ item.element.duration - item.element.trimStart - item.element.trimEnd;
+ const hasOverlap = get().checkElementOverlap(
+ targetTrackId,
+ startTime,
+ duration
+ );
+ if (hasOverlap) {
+ // If overlap, nudge forward slightly until free (simple resolve)
+ let candidate = startTime;
+ let safety = 0;
+ while (
+ get().checkElementOverlap(targetTrackId, candidate, duration) &&
+ safety < 1000
+ ) {
+ candidate += 0.01;
+ safety += 1;
+ }
+ get().addElementToTrack(targetTrackId, {
+ ...item.element,
+ startTime: candidate,
+ });
+ } else {
+ get().addElementToTrack(targetTrackId, {
+ ...item.element,
+ startTime,
+ });
+ }
+ }
},
};
});
+
+function buildTextElement(
+ raw: TextElement | DragData,
+ startTime: number
+): CreateTimelineElement {
+ const t = raw as Partial<TextElement>;
+
+ return {
+ type: "text",
+ name: t.name ?? DEFAULT_TEXT_ELEMENT.name,
+ content: t.content ?? DEFAULT_TEXT_ELEMENT.content,
+ duration: t.duration ?? TIMELINE_CONSTANTS.DEFAULT_TEXT_DURATION,
+ startTime,
+ trimStart: 0,
+ trimEnd: 0,
+ fontSize:
+ typeof t.fontSize === "number"
+ ? t.fontSize
+ : DEFAULT_TEXT_ELEMENT.fontSize,
+ fontFamily: t.fontFamily ?? DEFAULT_TEXT_ELEMENT.fontFamily,
+ color: t.color ?? DEFAULT_TEXT_ELEMENT.color,
+ backgroundColor: t.backgroundColor ?? DEFAULT_TEXT_ELEMENT.backgroundColor,
+ textAlign: t.textAlign ?? DEFAULT_TEXT_ELEMENT.textAlign,
+ fontWeight: t.fontWeight ?? DEFAULT_TEXT_ELEMENT.fontWeight,
+ fontStyle: t.fontStyle ?? DEFAULT_TEXT_ELEMENT.fontStyle,
+ textDecoration: t.textDecoration ?? DEFAULT_TEXT_ELEMENT.textDecoration,
+ x: typeof t.x === "number" ? t.x : DEFAULT_TEXT_ELEMENT.x,
+ y: typeof t.y === "number" ? t.y : DEFAULT_TEXT_ELEMENT.y,
+ rotation:
+ typeof t.rotation === "number"
+ ? t.rotation
+ : DEFAULT_TEXT_ELEMENT.rotation,
+ opacity:
+ typeof t.opacity === "number" ? t.opacity : DEFAULT_TEXT_ELEMENT.opacity,
+ };
+}