export type VideoCropRect = { x: number; y: number; width: number; height: number; }; export type VideoCropHandle = "nw" | "n" | "ne" | "e" | "se" | "s" | "sw" | "w"; export const DEFAULT_VIDEO_CROP_RECT: VideoCropRect = { x: 0.1, y: 0.1, width: 0.8, height: 0.8, }; const MIN_VIDEO_CROP_SIZE = 0.05; function clampUnit(value: number) { return Math.min(1, Math.max(0, value)); } function roundUnit(value: number) { return Number(clampUnit(value).toFixed(6)); } export function normalizeVideoCropRect(value: unknown): VideoCropRect { if (!value || typeof value !== "object") return DEFAULT_VIDEO_CROP_RECT; const candidate = value as Partial; const x = Number(candidate.x); const y = Number(candidate.y); const width = Number(candidate.width); const height = Number(candidate.height); if (![x, y, width, height].every(Number.isFinite)) return DEFAULT_VIDEO_CROP_RECT; const normalizedX = clampUnit(x); const normalizedY = clampUnit(y); return { x: roundUnit(Math.min(normalizedX, 1 - MIN_VIDEO_CROP_SIZE)), y: roundUnit(Math.min(normalizedY, 1 - MIN_VIDEO_CROP_SIZE)), width: roundUnit(Math.max(MIN_VIDEO_CROP_SIZE, Math.min(width, 1 - normalizedX))), height: roundUnit(Math.max(MIN_VIDEO_CROP_SIZE, Math.min(height, 1 - normalizedY))), }; } export function moveVideoCropRect(rect: VideoCropRect, deltaX: number, deltaY: number) { const normalized = normalizeVideoCropRect(rect); return { ...normalized, x: roundUnit(Math.max(0, Math.min(1 - normalized.width, normalized.x + deltaX))), y: roundUnit(Math.max(0, Math.min(1 - normalized.height, normalized.y + deltaY))), }; } export function resizeVideoCropRect( rect: VideoCropRect, handle: VideoCropHandle, pointX: number, pointY: number, ) { const normalized = normalizeVideoCropRect(rect); let left = normalized.x; let top = normalized.y; let right = normalized.x + normalized.width; let bottom = normalized.y + normalized.height; const x = clampUnit(pointX); const y = clampUnit(pointY); if (handle.includes("w")) left = Math.min(x, right - MIN_VIDEO_CROP_SIZE); if (handle.includes("e")) right = Math.max(x, left + MIN_VIDEO_CROP_SIZE); if (handle.includes("n")) top = Math.min(y, bottom - MIN_VIDEO_CROP_SIZE); if (handle.includes("s")) bottom = Math.max(y, top + MIN_VIDEO_CROP_SIZE); return normalizeVideoCropRect({ x: left, y: top, width: right - left, height: bottom - top, }); } export function getVideoCropOutputDimensions( rect: VideoCropRect, sourceWidth: number, sourceHeight: number, ) { const normalized = normalizeVideoCropRect(rect); return { width: Math.max(2, Math.round(Math.max(2, sourceWidth) * normalized.width)), height: Math.max(2, Math.round(Math.max(2, sourceHeight) * normalized.height)), }; }