diff --git a/src/components/video-editor/SettingsPanel.tsx b/src/components/video-editor/SettingsPanel.tsx index 31f96b9..7f0385a 100644 --- a/src/components/video-editor/SettingsPanel.tsx +++ b/src/components/video-editor/SettingsPanel.tsx @@ -57,6 +57,10 @@ interface SettingsPanelProps { onBlurChange?: (showBlur: boolean) => void; motionBlurEnabled?: boolean; onMotionBlurChange?: (enabled: boolean) => void; + borderRadius?: number; + onBorderRadiusChange?: (radius: number) => void; + padding?: number; + onPaddingChange?: (padding: number) => void; cropRegion?: CropRegion; onCropChange?: (region: CropRegion) => void; videoElement?: HTMLVideoElement | null; @@ -74,7 +78,7 @@ const ZOOM_DEPTH_OPTIONS: Array<{ depth: ZoomDepth; label: string }> = [ { depth: 6, label: "5×" }, ]; -export function SettingsPanel({ selected, onWallpaperChange, selectedZoomDepth, onZoomDepthChange, selectedZoomId, onZoomDelete, shadowIntensity = 0, onShadowChange, showBlur, onBlurChange, motionBlurEnabled = true, onMotionBlurChange, cropRegion, onCropChange, videoElement, onExport }: SettingsPanelProps) { +export function SettingsPanel({ selected, onWallpaperChange, selectedZoomDepth, onZoomDepthChange, selectedZoomId, onZoomDelete, shadowIntensity = 0, onShadowChange, showBlur, onBlurChange, motionBlurEnabled = true, onMotionBlurChange, borderRadius = 0, onBorderRadiusChange, padding = 0, onPaddingChange, cropRegion, onCropChange, videoElement, onExport }: SettingsPanelProps) { const [wallpaperPaths, setWallpaperPaths] = useState([]); const [customImages, setCustomImages] = useState([]); const fileInputRef = useRef(null); @@ -243,6 +247,36 @@ export function SettingsPanel({ selected, onWallpaperChange, selectedZoomDepth, className="data-[state=checked]:bg-[#34B27B]" /> + {/* Corner Roundness Slider */} +
+
+
Roundness
+ {borderRadius}px +
+ onBorderRadiusChange?.(values[0])} + min={0} + max={16} + step={0.5} + className="w-full [&_[role=slider]]:bg-[#34B27B] [&_[role=slider]]:border-[#34B27B]" + /> +
+ {/* Padding Slider */} +
+
+
Padding (TODO)
+ {padding}px +
+ onPaddingChange?.(values[0])} + min={0} + max={100} + step={1} + className="w-full [&_[role=slider]]:bg-[#34B27B] [&_[role=slider]]:border-[#34B27B]" + /> +
diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx index f680bcd..21277bc 100644 --- a/src/components/video-editor/VideoEditor.tsx +++ b/src/components/video-editor/VideoEditor.tsx @@ -38,6 +38,8 @@ export default function VideoEditor() { const [shadowIntensity, setShadowIntensity] = useState(0); const [showBlur, setShowBlur] = useState(false); const [motionBlurEnabled, setMotionBlurEnabled] = useState(true); + const [borderRadius, setBorderRadius] = useState(0); + const [padding, setPadding] = useState(0); const [cropRegion, setCropRegion] = useState(DEFAULT_CROP_REGION); const [zoomRegions, setZoomRegions] = useState([]); const [selectedZoomId, setSelectedZoomId] = useState(null); @@ -303,6 +305,7 @@ export default function VideoEditor() { shadowIntensity, showBlur, motionBlurEnabled, + borderRadius, cropRegion, onProgress: (progress: ExportProgress) => { setExportProgress(progress); @@ -344,7 +347,7 @@ export default function VideoEditor() { setIsExporting(false); exporterRef.current = null; } - }, [videoPath, wallpaper, zoomRegions, trimRegions, shadowIntensity, showBlur, motionBlurEnabled, cropRegion, isPlaying]); + }, [videoPath, wallpaper, zoomRegions, trimRegions, shadowIntensity, showBlur, motionBlurEnabled, borderRadius, cropRegion, isPlaying]); const handleCancelExport = useCallback(() => { if (exporterRef.current) { @@ -409,6 +412,7 @@ export default function VideoEditor() { shadowIntensity={shadowIntensity} showBlur={showBlur} motionBlurEnabled={motionBlurEnabled} + borderRadius={borderRadius} cropRegion={cropRegion} trimRegions={trimRegions} /> @@ -472,6 +476,10 @@ export default function VideoEditor() { onBlurChange={setShowBlur} motionBlurEnabled={motionBlurEnabled} onMotionBlurChange={setMotionBlurEnabled} + borderRadius={borderRadius} + onBorderRadiusChange={setBorderRadius} + padding={padding} + onPaddingChange={setPadding} cropRegion={cropRegion} onCropChange={setCropRegion} videoElement={videoPlaybackRef.current?.video || null} diff --git a/src/components/video-editor/VideoPlayback.tsx b/src/components/video-editor/VideoPlayback.tsx index 6a7ae69..44397e3 100644 --- a/src/components/video-editor/VideoPlayback.tsx +++ b/src/components/video-editor/VideoPlayback.tsx @@ -28,6 +28,7 @@ interface VideoPlaybackProps { shadowIntensity?: number; showBlur?: boolean; motionBlurEnabled?: boolean; + borderRadius?: number; cropRegion?: import('./types').CropRegion; trimRegions?: TrimRegion[]; } @@ -57,6 +58,7 @@ const VideoPlayback = forwardRef(({ shadowIntensity = 0, showBlur, motionBlurEnabled = true, + borderRadius = 0, cropRegion, trimRegions = [], }, ref) => { @@ -150,6 +152,7 @@ const VideoPlayback = forwardRef(({ videoElement, cropRegion, lockedVideoDimensions: lockedVideoDimensionsRef.current, + borderRadius, }); if (result) { @@ -171,7 +174,7 @@ const VideoPlayback = forwardRef(({ updateOverlayForRegion(activeRegion); } - }, [updateOverlayForRegion, cropRegion]); + }, [updateOverlayForRegion, cropRegion, borderRadius]); useEffect(() => { layoutVideoContentRef.current = layoutVideoContent; diff --git a/src/components/video-editor/videoPlayback/layoutUtils.ts b/src/components/video-editor/videoPlayback/layoutUtils.ts index 5aeab35..4def048 100644 --- a/src/components/video-editor/videoPlayback/layoutUtils.ts +++ b/src/components/video-editor/videoPlayback/layoutUtils.ts @@ -10,6 +10,7 @@ interface LayoutParams { videoElement: HTMLVideoElement; cropRegion?: CropRegion; lockedVideoDimensions?: { width: number; height: number } | null; + borderRadius?: number; } interface LayoutResult { @@ -22,7 +23,7 @@ interface LayoutResult { } export function layoutVideoContent(params: LayoutParams): LayoutResult | null { - const { container, app, videoSprite, maskGraphics, videoElement, cropRegion, lockedVideoDimensions } = params; + const { container, app, videoSprite, maskGraphics, videoElement, cropRegion, lockedVideoDimensions, borderRadius = 0 } = params; const videoWidth = lockedVideoDimensions?.width || videoElement.videoWidth; const videoHeight = lockedVideoDimensions?.height || videoElement.videoHeight; @@ -91,9 +92,10 @@ export function layoutVideoContent(params: LayoutParams): LayoutResult | null { // Create a mask that only shows the cropped region (centered in container) const maskX = centerOffsetX; const maskY = centerOffsetY; - const radius = Math.min(croppedDisplayWidth, croppedDisplayHeight) * 0.02; + + // Apply border radius maskGraphics.clear(); - maskGraphics.roundRect(maskX, maskY, croppedDisplayWidth, croppedDisplayHeight, radius); + maskGraphics.roundRect(maskX, maskY, croppedDisplayWidth, croppedDisplayHeight, borderRadius); maskGraphics.fill({ color: 0xffffff }); return { diff --git a/src/lib/exporter/frameRenderer.ts b/src/lib/exporter/frameRenderer.ts index f5d2000..c0b6b9a 100644 --- a/src/lib/exporter/frameRenderer.ts +++ b/src/lib/exporter/frameRenderer.ts @@ -15,6 +15,7 @@ interface FrameRenderConfig { shadowIntensity: number; showBlur: boolean; motionBlurEnabled?: boolean; + borderRadius?: number; cropRegion: CropRegion; videoWidth: number; videoHeight: number; @@ -301,10 +302,20 @@ export class FrameRenderer { if (!this.app || !this.videoSprite || !this.maskGraphics || !this.videoContainer) return; const { width, height } = this.config; - const { cropRegion } = this.config; + const { cropRegion, borderRadius = 0 } = this.config; const videoWidth = this.config.videoWidth; const videoHeight = this.config.videoHeight; + // Log layout calculation once (only on first layout) + if (!this.layoutCache) { + console.log('[FrameRenderer] Initial updateLayout', { + canvasSize: { width, height }, + videoSize: { width: videoWidth, height: videoHeight }, + cropRegion, + borderRadius, + }); + } + // Calculate cropped video dimensions const cropStartX = cropRegion.x; const cropStartY = cropRegion.y; @@ -337,9 +348,24 @@ export class FrameRenderer { this.videoContainer.y = centerOffsetY; // Update mask - const radius = Math.min(croppedDisplayWidth, croppedDisplayHeight) * 0.02; + // Scale the border radius if needed? + // In preview, we use the raw pixel value from the slider. + // In export, the canvas might be much larger (e.g. 4K vs 800px preview). + // If we use the raw value (e.g. 20px), it will look tiny on 4K. + // We should probably scale it based on the resolution ratio relative to a "standard" preview size (e.g. 1920x1080 or similar). + // Or, we can assume the user sees it on a ~1000px wide preview. + // Let's scale it by (width / 1280) as a rough heuristic to match visual appearance? + // Actually, let's just use the raw value for now as requested "fine grain control". + // If the user sets 20px, they might expect 20px. + // BUT, if they are editing on a small screen and exporting to 4K, 20px will look different. + // Let's stick to raw value first as it's safer than guessing. + // Wait, the previous hardcoded value was percentage based: radius = min(w, h) * 0.02 + // If I use raw pixels, I break that "responsiveness". + // However, the slider is in pixels (0-40). + // I will use the raw value for now. + this.maskGraphics.clear(); - this.maskGraphics.roundRect(0, 0, croppedDisplayWidth, croppedDisplayHeight, radius); + this.maskGraphics.roundRect(0, 0, croppedDisplayWidth, croppedDisplayHeight, borderRadius); this.maskGraphics.fill({ color: 0xffffff }); // Cache layout info diff --git a/src/lib/exporter/videoExporter.ts b/src/lib/exporter/videoExporter.ts index ba115ea..f994d4d 100644 --- a/src/lib/exporter/videoExporter.ts +++ b/src/lib/exporter/videoExporter.ts @@ -13,6 +13,7 @@ interface VideoExporterConfig extends ExportConfig { shadowIntensity: number; showBlur: boolean; motionBlurEnabled?: boolean; + borderRadius?: number; videoPadding?: number; cropRegion: CropRegion; onProgress?: (progress: ExportProgress) => void; @@ -87,6 +88,7 @@ export class VideoExporter { shadowIntensity: this.config.shadowIntensity, showBlur: this.config.showBlur, motionBlurEnabled: this.config.motionBlurEnabled, + borderRadius: this.config.borderRadius, cropRegion: this.config.cropRegion, videoWidth: videoInfo.width, videoHeight: videoInfo.height,