summaryrefslogtreecommitdiff
path: root/apps/web/src/lib/timeline/track-utils.ts
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/lib/timeline/track-utils.ts')
-rw-r--r--apps/web/src/lib/timeline/track-utils.ts594
1 files changed, 297 insertions, 297 deletions
diff --git a/apps/web/src/lib/timeline/track-utils.ts b/apps/web/src/lib/timeline/track-utils.ts
index cde2c814..0034c72e 100644
--- a/apps/web/src/lib/timeline/track-utils.ts
+++ b/apps/web/src/lib/timeline/track-utils.ts
@@ -1,297 +1,297 @@
-import type {
- TrackType,
- TimelineTrack,
- ElementType,
- VideoTrack,
- AudioTrack,
- GraphicTrack,
- TextTrack,
- EffectTrack,
- TimelineElement,
-} from "@/lib/timeline";
-import {
- ELEMENT_TRACK_MAP,
- TRACK_CONFIG,
- ELEMENT_TYPE_CONFIG,
- 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 | GraphicTrack | EffectTrack {
- return track.type !== "audio";
-}
-
-export function getElementClasses({ type }: { type: TrackType }) {
- return ELEMENT_TYPE_CONFIG[type].background.trim();
-}
-
-export function getTrackHeight({ type }: { type: TrackType }): number {
- return TRACK_CONFIG[type].height;
-}
-
-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 ?? TRACK_CONFIG[type].defaultName;
-
- 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 "graphic":
- return {
- id,
- name: trackName,
- type: "graphic",
- elements: [],
- hidden: false,
- };
- case "audio":
- return {
- id,
- name: trackName,
- type: "audio",
- elements: [],
- muted: false,
- };
- case "effect":
- return {
- id,
- name: trackName,
- type: "effect",
- elements: [],
- hidden: 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;
- }
-
- if (trackType === "effect") {
- return 0;
- }
-
- 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 {
- return ELEMENT_TRACK_MAP[elementType] === trackType;
-}
-
-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 };
-}
-
-export function getEarliestMainTrackElement({
- tracks,
- excludeElementId,
-}: {
- tracks: TimelineTrack[];
- excludeElementId?: string;
-}): TimelineElement | null {
- const mainTrack = getMainTrack({ tracks });
- if (!mainTrack) {
- return null;
- }
-
- const elements = mainTrack.elements.filter(
- (element) => !excludeElementId || element.id !== excludeElementId,
- );
-
- if (elements.length === 0) {
- return null;
- }
-
- return elements.reduce((earliest, element) =>
- element.startTime < earliest.startTime ? element : earliest,
- );
-}
-
-export function enforceMainTrackStart({
- tracks,
- targetTrackId,
- requestedStartTime,
- excludeElementId,
-}: {
- tracks: TimelineTrack[];
- targetTrackId: string;
- requestedStartTime: number;
- excludeElementId?: string;
-}): number {
- const mainTrack = getMainTrack({ tracks });
- if (!mainTrack || mainTrack.id !== targetTrackId) {
- return requestedStartTime;
- }
-
- const earliestElement = getEarliestMainTrackElement({
- tracks,
- excludeElementId,
- });
-
- if (!earliestElement) {
- return 0;
- }
-
- // main track must always start at time 0; if this element would
- // become the earliest, pin it to the start
- if (requestedStartTime <= earliestElement.startTime) {
- return 0;
- }
-
- return requestedStartTime;
-}
+import type {
+ TrackType,
+ TimelineTrack,
+ ElementType,
+ VideoTrack,
+ AudioTrack,
+ GraphicTrack,
+ TextTrack,
+ EffectTrack,
+ TimelineElement,
+} from "@/lib/timeline";
+import {
+ ELEMENT_TRACK_MAP,
+ TRACK_CONFIG,
+ ELEMENT_TYPE_CONFIG,
+ 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 | GraphicTrack | EffectTrack {
+ return track.type !== "audio";
+}
+
+export function getElementClasses({ type }: { type: TrackType }) {
+ return ELEMENT_TYPE_CONFIG[type].background.trim();
+}
+
+export function getTrackHeight({ type }: { type: TrackType }): number {
+ return TRACK_CONFIG[type].height;
+}
+
+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 ?? TRACK_CONFIG[type].defaultName;
+
+ 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 "graphic":
+ return {
+ id,
+ name: trackName,
+ type: "graphic",
+ elements: [],
+ hidden: false,
+ };
+ case "audio":
+ return {
+ id,
+ name: trackName,
+ type: "audio",
+ elements: [],
+ muted: false,
+ };
+ case "effect":
+ return {
+ id,
+ name: trackName,
+ type: "effect",
+ elements: [],
+ hidden: 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;
+ }
+
+ if (trackType === "effect") {
+ return 0;
+ }
+
+ 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 {
+ return ELEMENT_TRACK_MAP[elementType] === trackType;
+}
+
+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 };
+}
+
+export function getEarliestMainTrackElement({
+ tracks,
+ excludeElementId,
+}: {
+ tracks: TimelineTrack[];
+ excludeElementId?: string;
+}): TimelineElement | null {
+ const mainTrack = getMainTrack({ tracks });
+ if (!mainTrack) {
+ return null;
+ }
+
+ const elements = mainTrack.elements.filter(
+ (element) => !excludeElementId || element.id !== excludeElementId,
+ );
+
+ if (elements.length === 0) {
+ return null;
+ }
+
+ return elements.reduce((earliest, element) =>
+ element.startTime < earliest.startTime ? element : earliest,
+ );
+}
+
+export function enforceMainTrackStart({
+ tracks,
+ targetTrackId,
+ requestedStartTime,
+ excludeElementId,
+}: {
+ tracks: TimelineTrack[];
+ targetTrackId: string;
+ requestedStartTime: number;
+ excludeElementId?: string;
+}): number {
+ const mainTrack = getMainTrack({ tracks });
+ if (!mainTrack || mainTrack.id !== targetTrackId) {
+ return requestedStartTime;
+ }
+
+ const earliestElement = getEarliestMainTrackElement({
+ tracks,
+ excludeElementId,
+ });
+
+ if (!earliestElement) {
+ return 0;
+ }
+
+ // main track must always start at time 0; if this element would
+ // become the earliest, pin it to the start
+ if (requestedStartTime <= earliestElement.startTime) {
+ return 0;
+ }
+
+ return requestedStartTime;
+}