From 3ea6b002547beb58ac7cd54b9c95c1755ead5ad1 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 25 Jun 2025 22:55:25 +0200 Subject: [PATCH] fix: lock aspect ratio in preview --- .../src/components/editor/preview-panel.tsx | 540 ++++++++++-------- 1 file changed, 290 insertions(+), 250 deletions(-) diff --git a/apps/web/src/components/editor/preview-panel.tsx b/apps/web/src/components/editor/preview-panel.tsx index fafb26b..5ee5748 100644 --- a/apps/web/src/components/editor/preview-panel.tsx +++ b/apps/web/src/components/editor/preview-panel.tsx @@ -1,250 +1,290 @@ -"use client"; - -import { - useTimelineStore, - type TimelineClip, - type TimelineTrack, -} from "@/stores/timeline-store"; -import { useMediaStore, type MediaItem } from "@/stores/media-store"; -import { usePlaybackStore } from "@/stores/playback-store"; -import { VideoPlayer } from "@/components/ui/video-player"; -import { Button } from "@/components/ui/button"; -import { Play, Pause, Volume2, VolumeX } from "lucide-react"; -import { useState, useRef } from "react"; - -// Debug flag - set to false to hide active clips info -const SHOW_DEBUG_INFO = process.env.NODE_ENV === "development"; - -interface ActiveClip { - clip: TimelineClip; - track: TimelineTrack; - mediaItem: MediaItem | null; -} - -export function PreviewPanel() { - const { tracks } = useTimelineStore(); - const { mediaItems } = useMediaStore(); - const { isPlaying, toggle, currentTime, muted, toggleMute, volume } = - usePlaybackStore(); - const [canvasSize, setCanvasSize] = useState({ width: 1920, height: 1080 }); - const [showDebug, setShowDebug] = useState(SHOW_DEBUG_INFO); - const previewRef = useRef(null); - - // Get active clips at current time - const getActiveClips = (): ActiveClip[] => { - const activeClips: ActiveClip[] = []; - - tracks.forEach((track) => { - track.clips.forEach((clip) => { - const clipStart = clip.startTime; - const clipEnd = - clip.startTime + (clip.duration - clip.trimStart - clip.trimEnd); - - if (currentTime >= clipStart && currentTime < clipEnd) { - const mediaItem = - clip.mediaId === "test" - ? null // Test clips don't have a real media item - : mediaItems.find((item) => item.id === clip.mediaId) || null; - - activeClips.push({ clip, track, mediaItem }); - } - }); - }); - - return activeClips; - }; - - const activeClips = getActiveClips(); - const aspectRatio = canvasSize.width / canvasSize.height; - - // Render a clip - const renderClip = (clipData: ActiveClip, index: number) => { - const { clip, mediaItem } = clipData; - - // Test clips - if (!mediaItem || clip.mediaId === "test") { - return ( -
-
-
🎬
-

{clip.name}

-
-
- ); - } - - // Video clips - if (mediaItem.type === "video") { - return ( -
- -
- ); - } - - // Image clips - if (mediaItem.type === "image") { - return ( -
- {mediaItem.name} -
- ); - } - - // Audio clips (visual representation) - if (mediaItem.type === "audio") { - return ( -
-
-
🎵
-

{mediaItem.name}

-
-
- ); - } - - return null; - }; - - // Canvas presets - const canvasPresets = [ - { name: "16:9 HD", width: 1920, height: 1080 }, - { name: "16:9 4K", width: 3840, height: 2160 }, - { name: "9:16 Mobile", width: 1080, height: 1920 }, - { name: "1:1 Square", width: 1080, height: 1080 }, - { name: "4:3 Standard", width: 1440, height: 1080 }, - ]; - - return ( -
- {/* Controls */} -
- Canvas: - - - {/* Debug Toggle - Only show in development */} - {SHOW_DEBUG_INFO && ( - - )} - - - - -
- - {/* Preview Area */} -
-
- {activeClips.length === 0 ? ( -
- {tracks.length === 0 - ? "Drop media to start editing" - : "No clips at current time"} -
- ) : ( - activeClips.map((clipData, index) => renderClip(clipData, index)) - )} -
-
- - {/* Debug Info Panel - Conditionally rendered */} - {showDebug && ( -
-
- Debug: Active Clips ({activeClips.length}) -
-
- {activeClips.map((clipData, index) => ( -
- - {index + 1} - - {clipData.clip.name} - - ({clipData.mediaItem?.type || "test"}) - -
- ))} - {activeClips.length === 0 && ( - No active clips - )} -
-
- )} -
- ); -} +"use client"; + +import { + useTimelineStore, + type TimelineClip, + type TimelineTrack, +} from "@/stores/timeline-store"; +import { useMediaStore, type MediaItem } from "@/stores/media-store"; +import { usePlaybackStore } from "@/stores/playback-store"; +import { VideoPlayer } from "@/components/ui/video-player"; +import { Button } from "@/components/ui/button"; +import { Play, Pause, Volume2, VolumeX } from "lucide-react"; +import { useState, useRef, useEffect } from "react"; + +// Debug flag - set to false to hide active clips info +const SHOW_DEBUG_INFO = process.env.NODE_ENV === "development"; + +interface ActiveClip { + clip: TimelineClip; + track: TimelineTrack; + mediaItem: MediaItem | null; +} + +export function PreviewPanel() { + const { tracks } = useTimelineStore(); + const { mediaItems } = useMediaStore(); + const { isPlaying, toggle, currentTime, muted, toggleMute, volume } = + usePlaybackStore(); + const [canvasSize, setCanvasSize] = useState({ width: 1920, height: 1080 }); + const [showDebug, setShowDebug] = useState(SHOW_DEBUG_INFO); + const previewRef = useRef(null); + const containerRef = useRef(null); + const [previewDimensions, setPreviewDimensions] = useState({ + width: 0, + height: 0, + }); + + // Calculate optimal preview size that fits in container while maintaining aspect ratio + useEffect(() => { + const updatePreviewSize = () => { + if (!containerRef.current) return; + + const container = containerRef.current.getBoundingClientRect(); + const targetRatio = canvasSize.width / canvasSize.height; + const containerRatio = container.width / container.height; + + let width, height; + + if (containerRatio > targetRatio) { + // Container is wider - constrain by height + height = container.height; + width = height * targetRatio; + } else { + // Container is taller - constrain by width + width = container.width; + height = width / targetRatio; + } + + setPreviewDimensions({ width, height }); + }; + + updatePreviewSize(); + + const resizeObserver = new ResizeObserver(updatePreviewSize); + if (containerRef.current) { + resizeObserver.observe(containerRef.current); + } + + return () => resizeObserver.disconnect(); + }, [canvasSize.width, canvasSize.height]); + + // Get active clips at current time + const getActiveClips = (): ActiveClip[] => { + const activeClips: ActiveClip[] = []; + + tracks.forEach((track) => { + track.clips.forEach((clip) => { + const clipStart = clip.startTime; + const clipEnd = + clip.startTime + (clip.duration - clip.trimStart - clip.trimEnd); + + if (currentTime >= clipStart && currentTime < clipEnd) { + const mediaItem = + clip.mediaId === "test" + ? null // Test clips don't have a real media item + : mediaItems.find((item) => item.id === clip.mediaId) || null; + + activeClips.push({ clip, track, mediaItem }); + } + }); + }); + + return activeClips; + }; + + const activeClips = getActiveClips(); + + // Render a clip + const renderClip = (clipData: ActiveClip, index: number) => { + const { clip, mediaItem } = clipData; + + // Test clips + if (!mediaItem || clip.mediaId === "test") { + return ( +
+
+
🎬
+

{clip.name}

+
+
+ ); + } + + // Video clips + if (mediaItem.type === "video") { + return ( +
+ +
+ ); + } + + // Image clips + if (mediaItem.type === "image") { + return ( +
+ {mediaItem.name} +
+ ); + } + + // Audio clips (visual representation) + if (mediaItem.type === "audio") { + return ( +
+
+
🎵
+

{mediaItem.name}

+
+
+ ); + } + + return null; + }; + + // Canvas presets + const canvasPresets = [ + { name: "16:9 HD", width: 1920, height: 1080 }, + { name: "16:9 4K", width: 3840, height: 2160 }, + { name: "9:16 Mobile", width: 1080, height: 1920 }, + { name: "1:1 Square", width: 1080, height: 1080 }, + { name: "4:3 Standard", width: 1440, height: 1080 }, + ]; + + return ( +
+ {/* Controls */} +
+ Canvas: + + + {/* Debug Toggle - Only show in development */} + {SHOW_DEBUG_INFO && ( + + )} + + + + +
+ + {/* Preview Area */} +
+
+ {activeClips.length === 0 ? ( +
+ {tracks.length === 0 + ? "Drop media to start editing" + : "No clips at current time"} +
+ ) : ( + activeClips.map((clipData, index) => renderClip(clipData, index)) + )} +
+
+ + {/* Debug Info Panel - Conditionally rendered */} + {showDebug && ( +
+
+ Debug: Active Clips ({activeClips.length}) +
+
+ {activeClips.map((clipData, index) => ( +
+ + {index + 1} + + {clipData.clip.name} + + ({clipData.mediaItem?.type || "test"}) + +
+ ))} + {activeClips.length === 0 && ( + No active clips + )} +
+
+ )} +
+ ); +}