summaryrefslogtreecommitdiff
path: root/apps/web/src/lib/timeline/element-utils.ts
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-03-25 16:47:22 +0100
committerMaze Winther <mazewinther@gmail.com>2026-03-29 15:48:22 +0200
commit8db3bead13441a5882d8689dfc6a9f09c82375d9 (patch)
treee9d0cafa93904ffcbec3188c37961bcc16c6422d /apps/web/src/lib/timeline/element-utils.ts
parent39ea298a9ccba3104c549c0ca079c231981811f5 (diff)
downloadopencut-8db3bead13441a5882d8689dfc6a9f09c82375d9.tar.gz
feat: masks, properties refactor, shaders, storage migrations, and more
Diffstat (limited to 'apps/web/src/lib/timeline/element-utils.ts')
-rw-r--r--apps/web/src/lib/timeline/element-utils.ts241
1 files changed, 126 insertions, 115 deletions
diff --git a/apps/web/src/lib/timeline/element-utils.ts b/apps/web/src/lib/timeline/element-utils.ts
index 11564376..c44ae044 100644
--- a/apps/web/src/lib/timeline/element-utils.ts
+++ b/apps/web/src/lib/timeline/element-utils.ts
@@ -1,30 +1,33 @@
-import { DEFAULT_TEXT_ELEMENT } from "@/constants/text-constants";
+import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
import {
- DEFAULT_BLEND_MODE,
- DEFAULT_OPACITY,
- DEFAULT_TRANSFORM,
- TIMELINE_CONSTANTS,
-} from "@/constants/timeline-constants";
-import type {
- CreateEffectElement,
- CreateTimelineElement,
- CreateVideoElement,
- CreateImageElement,
- CreateStickerElement,
- CreateUploadAudioElement,
- CreateLibraryAudioElement,
- TextBackground,
- TextElement,
- TimelineElement,
- TimelineTrack,
- AudioElement,
- VideoElement,
- ImageElement,
- VisualElement,
- UploadAudioElement,
-} from "@/types/timeline";
-import type { MediaType } from "@/types/assets";
+ MASKABLE_ELEMENT_TYPES,
+ RETIMABLE_ELEMENT_TYPES,
+ VISUAL_ELEMENT_TYPES,
+ type CreateEffectElement,
+ type CreateGraphicElement,
+ type CreateTimelineElement,
+ type CreateVideoElement,
+ type CreateImageElement,
+ type CreateStickerElement,
+ type CreateUploadAudioElement,
+ type CreateLibraryAudioElement,
+ type TextBackground,
+ type TextElement,
+ type TimelineElement,
+ type TimelineTrack,
+ type AudioElement,
+ type VideoElement,
+ type ImageElement,
+ type MaskableElement,
+ type RetimableElement,
+ type VisualElement,
+ type UploadAudioElement,
+} from "@/lib/timeline";
+import { DEFAULTS } from "@/lib/timeline/defaults";
+import type { MediaType } from "@/lib/media/types";
import { buildDefaultEffectInstance } from "@/lib/effects";
+import { buildDefaultGraphicInstance } from "@/lib/graphics";
+import type { ParamValues } from "@/lib/params";
import { capitalizeFirstLetter } from "@/utils/string";
export function canElementHaveAudio(
@@ -36,12 +39,19 @@ export function canElementHaveAudio(
export function isVisualElement(
element: TimelineElement,
): element is VisualElement {
- return (
- element.type === "video" ||
- element.type === "image" ||
- element.type === "text" ||
- element.type === "sticker"
- );
+ return (VISUAL_ELEMENT_TYPES as readonly string[]).includes(element.type);
+}
+
+export function isMaskableElement(
+ element: TimelineElement,
+): element is MaskableElement {
+ return (MASKABLE_ELEMENT_TYPES as readonly string[]).includes(element.type);
+}
+
+export function isRetimableElement(
+ element: TimelineElement,
+): element is RetimableElement {
+ return (RETIMABLE_ELEMENT_TYPES as readonly string[]).includes(element.type);
}
export function canElementBeHidden(
@@ -50,6 +60,14 @@ export function canElementBeHidden(
return isVisualElement(element);
}
+export function hasElementEffects({
+ element,
+}: {
+ element: TimelineElement;
+}): boolean {
+ return isVisualElement(element) && (element.effects?.length ?? 0) > 0;
+}
+
export function hasMediaId(
element: TimelineElement,
): element is UploadAudioElement | VideoElement | ImageElement {
@@ -68,55 +86,6 @@ export function requiresMediaId({
);
}
-export function checkElementOverlaps({
- elements,
-}: {
- elements: TimelineElement[];
-}): boolean {
- const sortedElements = [...elements].sort(
- (a, b) => a.startTime - b.startTime,
- );
-
- for (let i = 0; i < sortedElements.length - 1; i++) {
- const current = sortedElements[i];
- const next = sortedElements[i + 1];
-
- const currentEnd = current.startTime + current.duration;
-
- if (currentEnd > next.startTime) return true;
- }
-
- return false;
-}
-
-export function resolveElementOverlaps({
- elements,
-}: {
- elements: TimelineElement[];
-}): TimelineElement[] {
- const sortedElements = [...elements].sort(
- (a, b) => a.startTime - b.startTime,
- );
- const resolvedElements: TimelineElement[] = [];
-
- for (let i = 0; i < sortedElements.length; i++) {
- const current = { ...sortedElements[i] };
-
- if (resolvedElements.length > 0) {
- const previous = resolvedElements[resolvedElements.length - 1];
- const previousEnd = previous.startTime + previous.duration;
-
- if (current.startTime < previousEnd) {
- current.startTime = previousEnd;
- }
- }
-
- resolvedElements.push(current);
- }
-
- return resolvedElements;
-}
-
export function wouldElementOverlap({
elements,
startTime,
@@ -138,9 +107,8 @@ export function wouldElementOverlap({
function buildTextBackground(
raw: Partial<TextBackground> | undefined,
): TextBackground {
- const color = raw?.color ?? DEFAULT_TEXT_ELEMENT.background.color;
- const enabled =
- typeof raw?.enabled === "boolean" ? raw.enabled : color !== "transparent";
+ const color = raw?.color ?? DEFAULTS.text.element.background.color;
+ const enabled = raw?.enabled ?? color !== "transparent";
return {
enabled,
color,
@@ -163,28 +131,25 @@ export function buildTextElement({
return {
type: "text",
- name: t.name ?? DEFAULT_TEXT_ELEMENT.name,
- content: t.content ?? DEFAULT_TEXT_ELEMENT.content,
+ name: t.name ?? DEFAULTS.text.element.name,
+ content: t.content ?? DEFAULTS.text.element.content,
duration: t.duration ?? TIMELINE_CONSTANTS.DEFAULT_ELEMENT_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,
+ fontSize: t.fontSize ?? DEFAULTS.text.element.fontSize,
+ fontFamily: t.fontFamily ?? DEFAULTS.text.element.fontFamily,
+ color: t.color ?? DEFAULTS.text.element.color,
background: buildTextBackground(t.background),
- 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,
- letterSpacing: t.letterSpacing ?? DEFAULT_TEXT_ELEMENT.letterSpacing,
- lineHeight: t.lineHeight ?? DEFAULT_TEXT_ELEMENT.lineHeight,
- transform: t.transform ?? DEFAULT_TEXT_ELEMENT.transform,
- opacity: t.opacity ?? DEFAULT_TEXT_ELEMENT.opacity,
- blendMode: t.blendMode ?? DEFAULT_BLEND_MODE,
+ textAlign: t.textAlign ?? DEFAULTS.text.element.textAlign,
+ fontWeight: t.fontWeight ?? DEFAULTS.text.element.fontWeight,
+ fontStyle: t.fontStyle ?? DEFAULTS.text.element.fontStyle,
+ textDecoration: t.textDecoration ?? DEFAULTS.text.element.textDecoration,
+ letterSpacing: t.letterSpacing ?? DEFAULTS.text.element.letterSpacing,
+ lineHeight: t.lineHeight ?? DEFAULTS.text.element.lineHeight,
+ transform: t.transform ?? DEFAULTS.text.element.transform,
+ opacity: t.opacity ?? DEFAULTS.text.element.opacity,
+ blendMode: t.blendMode ?? DEFAULTS.element.blendMode,
};
}
@@ -214,10 +179,14 @@ export function buildStickerElement({
stickerId,
name,
startTime,
+ intrinsicWidth,
+ intrinsicHeight,
}: {
stickerId: string;
name?: string;
startTime: number;
+ intrinsicWidth?: number;
+ intrinsicHeight?: number;
}): CreateStickerElement {
const stickerNameFromId =
stickerId.split(":").slice(1).pop()?.replaceAll("-", " ") ?? stickerId;
@@ -225,17 +194,52 @@ export function buildStickerElement({
type: "sticker",
name: name ?? stickerNameFromId,
stickerId,
+ intrinsicWidth,
+ intrinsicHeight,
+ duration: TIMELINE_CONSTANTS.DEFAULT_ELEMENT_DURATION,
+ startTime,
+ trimStart: 0,
+ trimEnd: 0,
+ transform: {
+ ...DEFAULTS.element.transform,
+ position: { ...DEFAULTS.element.transform.position },
+ },
+ opacity: DEFAULTS.element.opacity,
+ blendMode: DEFAULTS.element.blendMode,
+ };
+}
+
+export function buildGraphicElement({
+ definitionId,
+ name,
+ startTime,
+ params,
+}: {
+ definitionId: string;
+ name?: string;
+ startTime: number;
+ params?: Partial<ParamValues>;
+}): CreateGraphicElement {
+ const instance = buildDefaultGraphicInstance({ definitionId });
+ return {
+ type: "graphic",
+ name: name ?? capitalizeFirstLetter({ string: instance.definitionId }),
+ definitionId: instance.definitionId,
+ params: { ...instance.params, ...(params ?? {}) } as ParamValues,
duration: TIMELINE_CONSTANTS.DEFAULT_ELEMENT_DURATION,
startTime,
trimStart: 0,
trimEnd: 0,
- transform: { ...DEFAULT_TRANSFORM },
- opacity: DEFAULT_OPACITY,
- blendMode: DEFAULT_BLEND_MODE,
+ transform: {
+ ...DEFAULTS.element.transform,
+ position: { ...DEFAULTS.element.transform.position },
+ },
+ opacity: DEFAULTS.element.opacity,
+ blendMode: DEFAULTS.element.blendMode,
};
}
-export function buildVideoElement({
+function buildVideoElement({
mediaId,
name,
duration,
@@ -257,13 +261,17 @@ export function buildVideoElement({
sourceDuration: duration,
muted: false,
hidden: false,
- transform: { ...DEFAULT_TRANSFORM },
- opacity: DEFAULT_OPACITY,
- blendMode: DEFAULT_BLEND_MODE,
+ transform: {
+ ...DEFAULTS.element.transform,
+ position: { ...DEFAULTS.element.transform.position },
+ },
+ opacity: DEFAULTS.element.opacity,
+ blendMode: DEFAULTS.element.blendMode,
+ volume: DEFAULTS.element.volume,
};
}
-export function buildImageElement({
+function buildImageElement({
mediaId,
name,
duration,
@@ -283,13 +291,16 @@ export function buildImageElement({
trimStart: 0,
trimEnd: 0,
hidden: false,
- transform: { ...DEFAULT_TRANSFORM },
- opacity: DEFAULT_OPACITY,
- blendMode: DEFAULT_BLEND_MODE,
+ transform: {
+ ...DEFAULTS.element.transform,
+ position: { ...DEFAULTS.element.transform.position },
+ },
+ opacity: DEFAULTS.element.opacity,
+ blendMode: DEFAULTS.element.blendMode,
};
}
-export function buildUploadAudioElement({
+function buildUploadAudioElement({
mediaId,
name,
duration,
@@ -312,7 +323,7 @@ export function buildUploadAudioElement({
trimStart: 0,
trimEnd: 0,
sourceDuration: duration,
- volume: 1,
+ volume: DEFAULTS.element.volume,
muted: false,
};
if (buffer) {
@@ -375,7 +386,7 @@ export function buildLibraryAudioElement({
trimStart: 0,
trimEnd: 0,
sourceDuration: duration,
- volume: 1,
+ volume: DEFAULTS.element.volume,
muted: false,
};
if (buffer) {
@@ -407,7 +418,7 @@ export function getElementsAtTime({
return result;
}
-export function collectFontFamilies({
+export function getElementFontFamilies({
tracks,
}: {
tracks: TimelineTrack[];