From 8db3bead13441a5882d8689dfc6a9f09c82375d9 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 25 Mar 2026 16:47:22 +0100 Subject: feat: masks, properties refactor, shaders, storage migrations, and more --- apps/web/src/lib/timeline/element-utils.ts | 241 +++++++++++++++-------------- 1 file changed, 126 insertions(+), 115 deletions(-) (limited to 'apps/web/src/lib/timeline/element-utils.ts') 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 | 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; +}): 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[]; -- cgit v1.3.1