Files
OpenCut/apps/web/src/lib/timeline/track-utils.ts
T

245 lines
5.1 KiB
TypeScript
Raw Normal View History

2026-01-16 18:07:28 +01:00
import type {
TrackType,
TimelineTrack,
ElementType,
VideoTrack,
AudioTrack,
StickerTrack,
TextTrack,
} from "@/types/timeline";
2026-01-05 01:35:10 +01:00
import {
TRACK_COLORS,
TRACK_HEIGHTS,
TRACK_GAP,
} from "@/constants/timeline-constants";
2025-11-26 08:47:03 +01:00
import { generateUUID } from "@/lib/utils";
2026-01-16 18:07:28 +01:00
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";
}
2026-01-05 01:35:10 +01:00
export function getTrackColor({ type }: { type: TrackType }) {
2025-11-26 08:47:03 +01:00
return TRACK_COLORS[type];
}
2026-01-15 11:05:17 +01:00
export function getTrackClasses({ type }: { type: TrackType }) {
const colors = TRACK_COLORS[type];
2026-01-20 22:14:49 +01:00
return `${colors.background}`.trim();
2026-01-15 11:05:17 +01:00
}
2025-11-26 08:47:03 +01:00
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(
2026-01-05 01:35:10 +01:00
(sum, track) => sum + getTrackHeight({ type: track.type }) + TRACK_GAP,
2025-11-26 08:47:03 +01:00
0,
);
}
export function getTotalTracksHeight({
tracks,
}: {
tracks: Array<{ type: TrackType }>;
}): number {
const tracksHeight = tracks.reduce(
(sum, track) => sum + getTrackHeight({ type: track.type }),
0,
);
2026-01-05 01:35:10 +01:00
const gapsHeight = Math.max(0, tracks.length - 1) * TRACK_GAP;
2025-11-26 08:47:03 +01:00
return tracksHeight + gapsHeight;
}
2026-01-18 05:44:25 +01:00
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}`);
}
}
2026-01-20 22:14:49 +01:00
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;
}
2026-01-16 18:07:28 +01:00
export function isMainTrack(track: TimelineTrack): track is VideoTrack {
return track.type === "video" && track.isMain === true;
}
2025-11-26 08:47:03 +01:00
export function getMainTrack({
tracks,
}: {
tracks: TimelineTrack[];
}): TimelineTrack | null {
2026-01-16 18:07:28 +01:00
return tracks.find((track) => isMainTrack(track)) ?? null;
2025-11-26 08:47:03 +01:00
}
export function ensureMainTrack({
tracks,
}: {
tracks: TimelineTrack[];
}): TimelineTrack[] {
2026-01-16 18:07:28 +01:00
const hasMainTrack = tracks.some((track) => isMainTrack(track));
2025-11-26 08:47:03 +01:00
if (!hasMainTrack) {
const mainTrack: TimelineTrack = {
id: generateUUID(),
name: "Main Track",
2026-01-15 11:05:17 +01:00
type: "video",
2025-11-26 08:47:03 +01:00
elements: [],
muted: false,
isMain: true,
2026-01-18 05:44:25 +01:00
hidden: false,
2025-11-26 08:47:03 +01:00
};
return [mainTrack, ...tracks];
}
return tracks;
}
export function canElementGoOnTrack({
elementType,
trackType,
}: {
2026-01-15 11:05:17 +01:00
elementType: ElementType;
2025-11-26 08:47:03 +01:00
trackType: TrackType;
}): boolean {
2026-01-05 01:35:10 +01:00
if (elementType === "text") return trackType === "text";
if (elementType === "audio") return trackType === "audio";
2026-01-15 11:05:17 +01:00
if (elementType === "sticker") return trackType === "sticker";
2026-01-05 01:35:10 +01:00
if (elementType === "video" || elementType === "image") {
2026-01-15 11:05:17 +01:00
return trackType === "video";
2025-11-26 08:47:03 +01:00
}
return false;
}
export function validateElementTrackCompatibility({
element,
track,
}: {
2026-01-15 11:05:17 +01:00
element: { type: ElementType };
2025-11-26 08:47:03 +01:00
track: { type: TrackType };
}): { isValid: boolean; errorMessage?: string } {
const isValid = canElementGoOnTrack({
elementType: element.type,
trackType: track.type,
});
if (!isValid) {
2026-01-05 01:35:10 +01:00
return {
isValid: false,
errorMessage: `${element.type} elements cannot be placed on ${track.type} tracks`,
};
2025-11-26 08:47:03 +01:00
}
return { isValid: true };
}