152 lines
4.7 KiB
TypeScript
152 lines
4.7 KiB
TypeScript
import {
|
|
DEFAULT_SCENE_ACCENT_COLOR,
|
|
DEFAULT_SCENE_BACKGROUND_COLOR,
|
|
} from "@/lib/studio-color-palettes";
|
|
import type { Layer, Scene, SceneTransition } from "@/lib/studio-types";
|
|
import { DEFAULT_SCENE_DURATION } from "@/lib/studio-types";
|
|
|
|
const SCENE_TRANSITIONS: SceneTransition[] = [
|
|
"none",
|
|
"fade",
|
|
"slide-left",
|
|
"zoom",
|
|
];
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
}
|
|
|
|
function parseLayer(value: unknown): Layer | null {
|
|
if (!isRecord(value)) return null;
|
|
if (typeof value.id !== "string" || typeof value.type !== "string") return null;
|
|
if (
|
|
typeof value.x !== "number" ||
|
|
typeof value.y !== "number" ||
|
|
typeof value.width !== "number" ||
|
|
typeof value.height !== "number"
|
|
) {
|
|
return null;
|
|
}
|
|
return {
|
|
id: value.id,
|
|
type: value.type as Layer["type"],
|
|
name: typeof value.name === "string" ? value.name : undefined,
|
|
visible: typeof value.visible === "boolean" ? value.visible : undefined,
|
|
x: value.x,
|
|
y: value.y,
|
|
width: value.width,
|
|
height: value.height,
|
|
rotation: typeof value.rotation === "number" ? value.rotation : 0,
|
|
opacity: typeof value.opacity === "number" ? value.opacity : 1,
|
|
zIndex: typeof value.zIndex === "number" ? value.zIndex : 0,
|
|
props: isRecord(value.props) ? value.props : {},
|
|
};
|
|
}
|
|
|
|
function parseScene(value: unknown): Scene | null {
|
|
if (!isRecord(value)) return null;
|
|
if (typeof value.id !== "string" || typeof value.name !== "string") return null;
|
|
if (!Array.isArray(value.layers)) return null;
|
|
|
|
const layers = value.layers
|
|
.map(parseLayer)
|
|
.filter((layer): layer is Layer => layer !== null);
|
|
|
|
const transitionType =
|
|
typeof value.transitionType === "string" &&
|
|
SCENE_TRANSITIONS.includes(value.transitionType as SceneTransition)
|
|
? (value.transitionType as SceneTransition)
|
|
: "none";
|
|
|
|
return {
|
|
id: value.id,
|
|
name: value.name,
|
|
duration:
|
|
typeof value.duration === "number" ? value.duration : DEFAULT_SCENE_DURATION,
|
|
layers,
|
|
transitionType,
|
|
};
|
|
}
|
|
|
|
export function parseVideoScenes(value: unknown): Scene[] {
|
|
if (!Array.isArray(value)) return [];
|
|
return value.map(parseScene).filter((scene): scene is Scene => scene !== null);
|
|
}
|
|
|
|
export function isVideoSceneDataEmpty(sceneData: Record<string, unknown>): boolean {
|
|
const scenes = parseVideoScenes(sceneData.scenes);
|
|
return scenes.length === 0;
|
|
}
|
|
|
|
/** Omit generated thumbnails — they are re-created in the editor */
|
|
export function scenesForPersistence(scenes: Scene[]): Scene[] {
|
|
return scenes.map((scene) => {
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- strip before save
|
|
const { thumbnailUrl, ...rest } = scene;
|
|
return rest;
|
|
});
|
|
}
|
|
|
|
export interface VideoPersistedSceneData {
|
|
scenes: Scene[];
|
|
activeSceneId: string;
|
|
currentTime?: number;
|
|
pxPerSecond?: number;
|
|
audioFileName?: string | null;
|
|
audioSrc?: string | null;
|
|
audioVolume?: number;
|
|
sceneBackgroundColor?: string;
|
|
sceneAccentColor?: string;
|
|
}
|
|
|
|
export function buildVideoSceneDataPayload(
|
|
input: VideoPersistedSceneData
|
|
): Record<string, unknown> {
|
|
return {
|
|
scenes: scenesForPersistence(input.scenes),
|
|
activeSceneId: input.activeSceneId,
|
|
currentTime: input.currentTime,
|
|
pxPerSecond: input.pxPerSecond,
|
|
audioFileName: input.audioFileName,
|
|
audioSrc: input.audioSrc,
|
|
audioVolume: input.audioVolume,
|
|
sceneBackgroundColor: input.sceneBackgroundColor,
|
|
sceneAccentColor: input.sceneAccentColor,
|
|
};
|
|
}
|
|
|
|
export function parseVideoSceneData(
|
|
sceneData: Record<string, unknown>
|
|
): VideoPersistedSceneData | null {
|
|
const scenes = parseVideoScenes(sceneData.scenes);
|
|
if (scenes.length === 0) return null;
|
|
|
|
const activeSceneId =
|
|
typeof sceneData.activeSceneId === "string" &&
|
|
scenes.some((scene) => scene.id === sceneData.activeSceneId)
|
|
? sceneData.activeSceneId
|
|
: scenes[0].id;
|
|
|
|
return {
|
|
scenes,
|
|
activeSceneId,
|
|
currentTime:
|
|
typeof sceneData.currentTime === "number" ? sceneData.currentTime : 0,
|
|
pxPerSecond:
|
|
typeof sceneData.pxPerSecond === "number" ? sceneData.pxPerSecond : undefined,
|
|
audioFileName:
|
|
typeof sceneData.audioFileName === "string" ? sceneData.audioFileName : null,
|
|
audioSrc: typeof sceneData.audioSrc === "string" ? sceneData.audioSrc : null,
|
|
audioVolume:
|
|
typeof sceneData.audioVolume === "number" ? sceneData.audioVolume : 100,
|
|
sceneBackgroundColor:
|
|
typeof sceneData.sceneBackgroundColor === "string"
|
|
? sceneData.sceneBackgroundColor
|
|
: DEFAULT_SCENE_BACKGROUND_COLOR,
|
|
sceneAccentColor:
|
|
typeof sceneData.sceneAccentColor === "string"
|
|
? sceneData.sceneAccentColor
|
|
: DEFAULT_SCENE_ACCENT_COLOR,
|
|
};
|
|
}
|