diff options
| author | Maze <167211895+mazeincoding@users.noreply.github.com> | 2026-01-31 00:20:04 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-01-31 00:20:04 +0100 |
| commit | 7bf0984698e43b9aa2dd879f65951318ed410390 (patch) | |
| tree | ccf2cd590a47d550152afc07129cb55c71a9ec35 /apps/web/src/lib/timeline/track-utils.ts | |
| parent | 0173db99448d1c4c6415dcf15272e17551b48b04 (diff) | |
| download | opencut-7bf0984698e43b9aa2dd879f65951318ed410390.tar.gz | |
codebase overhaul (#697)
Diffstat (limited to 'apps/web/src/lib/timeline/track-utils.ts')
| -rw-r--r-- | apps/web/src/lib/timeline/track-utils.ts | 244 |
1 files changed, 244 insertions, 0 deletions
diff --git a/apps/web/src/lib/timeline/track-utils.ts b/apps/web/src/lib/timeline/track-utils.ts new file mode 100644 index 00000000..b6261569 --- /dev/null +++ b/apps/web/src/lib/timeline/track-utils.ts @@ -0,0 +1,244 @@ +import type { + TrackType, + TimelineTrack, + ElementType, + VideoTrack, + AudioTrack, + StickerTrack, + TextTrack, +} from "@/types/timeline"; +import { + TRACK_COLORS, + TRACK_HEIGHTS, + TRACK_GAP, +} from "@/constants/timeline-constants"; +import { generateUUID } from "@/utils/id"; + +export function canTracktHaveAudio( + track: TimelineTrack, +): track is VideoTrack | AudioTrack { + return track.type === "audio" || track.type === "video"; +} + +export function canTrackBeHidden( + track: TimelineTrack, +): track is VideoTrack | TextTrack | StickerTrack { + return track.type !== "audio"; +} + +export function getTrackColor({ type }: { type: TrackType }) { + return TRACK_COLORS[type]; +} + +export function getTrackClasses({ type }: { type: TrackType }) { + const colors = TRACK_COLORS[type]; + return `${colors.background}`.trim(); +} + +export function getTrackHeight({ type }: { type: TrackType }): number { + return TRACK_HEIGHTS[type]; +} + +export function getCumulativeHeightBefore({ + tracks, + trackIndex, +}: { + tracks: Array<{ type: TrackType }>; + trackIndex: number; +}): number { + return tracks + .slice(0, trackIndex) + .reduce( + (sum, track) => sum + getTrackHeight({ type: track.type }) + TRACK_GAP, + 0, + ); +} + +export function getTotalTracksHeight({ + tracks, +}: { + tracks: Array<{ type: TrackType }>; +}): number { + const tracksHeight = tracks.reduce( + (sum, track) => sum + getTrackHeight({ type: track.type }), + 0, + ); + const gapsHeight = Math.max(0, tracks.length - 1) * TRACK_GAP; + return tracksHeight + gapsHeight; +} + +export function buildEmptyTrack({ + id, + type, + name, +}: { + id: string; + type: TrackType; + name?: string; +}): TimelineTrack { + const trackName = + name ?? + (type === "video" + ? "Video track" + : type === "text" + ? "Text track" + : type === "audio" + ? "Audio track" + : type === "sticker" + ? "Sticker track" + : "Track"); + + switch (type) { + case "video": + return { + id, + name: trackName, + type: "video", + elements: [], + hidden: false, + muted: false, + isMain: false, + }; + case "text": + return { + id, + name: trackName, + type: "text", + elements: [], + hidden: false, + }; + case "sticker": + return { + id, + name: trackName, + type: "sticker", + elements: [], + hidden: false, + }; + case "audio": + return { + id, + name: trackName, + type: "audio", + elements: [], + muted: false, + }; + default: + throw new Error(`Unsupported track type: ${type}`); + } +} + +export function getDefaultInsertIndexForTrack({ + tracks, + trackType, +}: { + tracks: TimelineTrack[]; + trackType: TrackType; +}): number { + if (trackType === "audio") { + return tracks.length; + } + + const mainTrackIndex = tracks.findIndex((track) => isMainTrack(track)); + if (mainTrackIndex >= 0) { + return mainTrackIndex; + } + + const firstAudioTrackIndex = tracks.findIndex( + (track) => track.type === "audio", + ); + if (firstAudioTrackIndex >= 0) { + return firstAudioTrackIndex; + } + + return tracks.length; +} + +export function getHighestInsertIndexForTrack({ + tracks, + trackType, +}: { + tracks: TimelineTrack[]; + trackType: TrackType; +}): number { + const mainTrackIndex = tracks.findIndex((track) => isMainTrack(track)); + + if (trackType === "audio") { + return mainTrackIndex >= 0 ? mainTrackIndex + 1 : tracks.length; + } + + return 0; +} + +export function isMainTrack(track: TimelineTrack): track is VideoTrack { + return track.type === "video" && track.isMain === true; +} + +export function getMainTrack({ + tracks, +}: { + tracks: TimelineTrack[]; +}): TimelineTrack | null { + return tracks.find((track) => isMainTrack(track)) ?? null; +} + +export function ensureMainTrack({ + tracks, +}: { + tracks: TimelineTrack[]; +}): TimelineTrack[] { + const hasMainTrack = tracks.some((track) => isMainTrack(track)); + + if (!hasMainTrack) { + const mainTrack: TimelineTrack = { + id: generateUUID(), + name: "Main Track", + type: "video", + elements: [], + muted: false, + isMain: true, + hidden: false, + }; + return [mainTrack, ...tracks]; + } + + return tracks; +} + +export function canElementGoOnTrack({ + elementType, + trackType, +}: { + elementType: ElementType; + trackType: TrackType; +}): boolean { + if (elementType === "text") return trackType === "text"; + if (elementType === "audio") return trackType === "audio"; + if (elementType === "sticker") return trackType === "sticker"; + if (elementType === "video" || elementType === "image") { + return trackType === "video"; + } + return false; +} + +export function validateElementTrackCompatibility({ + element, + track, +}: { + element: { type: ElementType }; + track: { type: TrackType }; +}): { isValid: boolean; errorMessage?: string } { + const isValid = canElementGoOnTrack({ + elementType: element.type, + trackType: track.type, + }); + + if (!isValid) { + return { + isValid: false, + errorMessage: `${element.type} elements cannot be placed on ${track.type} tracks`, + }; + } + + return { isValid: true }; +} |
