From a5c39a9a95bf0f00784688fe8643982b3cd27c5a Mon Sep 17 00:00:00 2001 From: Pranav-IIITM Date: Sat, 23 May 2026 09:11:21 +0530 Subject: [PATCH] feat: add real-time crop and aspect ratio preview overlay in VideoPreview (#891) --- src/components/VideoPreview.tsx | 146 +++++++++++++++++++++++++++++--- 1 file changed, 135 insertions(+), 11 deletions(-) diff --git a/src/components/VideoPreview.tsx b/src/components/VideoPreview.tsx index 856ac383..c2199987 100644 --- a/src/components/VideoPreview.tsx +++ b/src/components/VideoPreview.tsx @@ -2,6 +2,7 @@ import { useEffect, useRef, RefObject } from "react"; import { EditRecipe } from "@/lib/types"; +import { getPresetById } from "@/lib/presets"; interface Props { file: File | null; @@ -9,7 +10,7 @@ interface Props { recipe: EditRecipe; } -export default function VideoPreview({ file, videoRef ,recipe }: Props) { +export default function VideoPreview({ file, videoRef, recipe }: Props) { const urlRef = useRef(null); useEffect(() => { @@ -35,18 +36,141 @@ export default function VideoPreview({ file, videoRef ,recipe }: Props) { if (!videoRef.current || !recipe) return; videoRef.current.playbackRate = recipe.speed; }, [recipe, videoRef]); + + // Calculate aspect ratio overlay dimensions + const getOverlayStyles = () => { + if (!file || !recipe.preset || recipe.preset === "custom") return null; + + const preset = getPresetById(recipe.preset); + if (!preset) return null; + + const targetAspect = preset.width / preset.height; + const previewAspect = 16 / 9; // Fixed aspect ratio of preview container + + if (recipe.framing === "fill") { + // FILL mode: show dashed box where content will be cropped + if (targetAspect > previewAspect) { + // Target is wider: crop top/bottom + const scaleFactor = targetAspect / previewAspect; + const visibleHeightPercent = (1 / scaleFactor) * 100; + const topPercent = (100 - visibleHeightPercent) / 2; + return { + type: "fill", + style: { + top: `${topPercent}%`, + left: 0, + width: "100%", + height: `${visibleHeightPercent}%`, + }, + }; + } else if (targetAspect < previewAspect) { + // Target is narrower: crop left/right + const scaleFactor = previewAspect / targetAspect; + const visibleWidthPercent = (1 / scaleFactor) * 100; + const leftPercent = (100 - visibleWidthPercent) / 2; + return { + type: "fill", + style: { + top: 0, + left: `${leftPercent}%`, + width: `${visibleWidthPercent}%`, + height: "100%", + }, + }; + } + // Target aspect matches preview aspect: no overlay needed + return null; + } else { + // FIT mode: show letterbox/pillarbox bars + if (targetAspect > previewAspect) { + // Target is wider (e.g., 47:10 vs 16:9): add letterbox (top/bottom bars) + const scaleFactor = targetAspect / previewAspect; + const barHeightPercent = ((scaleFactor - 1) / (2 * scaleFactor)) * 100; + return { + type: "fit", + bars: [ + { position: "top", sizePercent: barHeightPercent }, + { position: "bottom", sizePercent: barHeightPercent }, + ], + }; + } else if (targetAspect < previewAspect) { + // Target is narrower (e.g., 9:16 vs 16:9): add pillarbox (left/right bars) + const scaleFactor = previewAspect / targetAspect; + const barWidthPercent = ((scaleFactor - 1) / (2 * scaleFactor)) * 100; + return { + type: "fit", + bars: [ + { position: "left", sizePercent: barWidthPercent }, + { position: "right", sizePercent: barWidthPercent }, + ], + }; + } + // Target aspect matches preview aspect: no overlay needed + return null; + } + }; + + const overlayInfo = getOverlayStyles(); + return ( -
- +
+ ref={videoRef} + controls + className="w-full h-full object-contain" + playsInline + muted={!recipe?.keepAudio} + > + + + + {/* Aspect ratio preview overlay */} + {overlayInfo && ( +
+ {overlayInfo.type === "fill" && ( +
+ )} + {overlayInfo.type === "fit" && + overlayInfo.bars?.map((bar, idx) => ( +
+ ))} +
+ )}
); } \ No newline at end of file