From 9d0ccf3bde683630e53261db6dc80acb7cb775e0 Mon Sep 17 00:00:00 2001 From: Ivan Date: Fri, 3 Apr 2026 00:09:51 +0300 Subject: [PATCH] Add webcam mask shape support --- src/components/video-editor/SettingsPanel.tsx | 86 +++++++++++++++++++ src/components/video-editor/VideoEditor.tsx | 12 +++ src/components/video-editor/VideoPlayback.tsx | 71 +++++++++------ .../video-editor/projectPersistence.test.ts | 9 ++ .../video-editor/projectPersistence.ts | 10 +++ src/components/video-editor/types.ts | 4 + .../video-editor/videoPlayback/layoutUtils.ts | 5 +- src/hooks/useEditorHistory.ts | 4 + src/i18n/locales/en/settings.json | 3 +- src/i18n/locales/es/settings.json | 3 +- src/i18n/locales/zh-CN/settings.json | 3 +- src/lib/compositeLayout.test.ts | 74 +++++++++++++--- src/lib/compositeLayout.ts | 39 +++++++-- src/lib/exporter/frameRenderer.ts | 10 ++- src/lib/exporter/gifExporter.ts | 2 + src/lib/exporter/videoExporter.ts | 2 + src/lib/webcamMaskShapes.ts | 48 +++++++++++ 17 files changed, 330 insertions(+), 55 deletions(-) create mode 100644 src/lib/webcamMaskShapes.ts diff --git a/src/components/video-editor/SettingsPanel.tsx b/src/components/video-editor/SettingsPanel.tsx index f5afe35..50f2d9c 100644 --- a/src/components/video-editor/SettingsPanel.tsx +++ b/src/components/video-editor/SettingsPanel.tsx @@ -51,6 +51,7 @@ import type { FigureData, PlaybackSpeed, WebcamLayoutPreset, + WebcamMaskShape, ZoomDepth, } from "./types"; import { SPEED_OPTIONS } from "./types"; @@ -143,6 +144,8 @@ interface SettingsPanelProps { hasWebcam?: boolean; webcamLayoutPreset?: WebcamLayoutPreset; onWebcamLayoutPresetChange?: (preset: WebcamLayoutPreset) => void; + webcamMaskShape?: import("./types").WebcamMaskShape; + onWebcamMaskShapeChange?: (shape: import("./types").WebcamMaskShape) => void; } export default SettingsPanel; @@ -211,6 +214,8 @@ export function SettingsPanel({ hasWebcam = false, webcamLayoutPreset = "picture-in-picture", onWebcamLayoutPresetChange, + webcamMaskShape = "rectangle", + onWebcamMaskShapeChange, }: SettingsPanelProps) { const t = useScopedT("settings"); const [wallpaperPaths, setWallpaperPaths] = useState([]); @@ -623,6 +628,87 @@ export function SettingsPanel({ + {webcamLayoutPreset === "picture-in-picture" && ( +
+
+ {t("layout.webcamShape")} +
+
+ {( + [ + { value: "rectangle", label: "Rect" }, + { value: "circle", label: "Circle" }, + { value: "square", label: "Square" }, + { value: "rounded", label: "Rounded" }, + ] as Array<{ value: WebcamMaskShape; label: string }> + ).map((shape) => ( + + ))} +
+
+ )} )} diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx index 304d10f..469f892 100644 --- a/src/components/video-editor/VideoEditor.tsx +++ b/src/components/video-editor/VideoEditor.tsx @@ -84,6 +84,7 @@ export default function VideoEditor() { padding, aspectRatio, webcamLayoutPreset, + webcamMaskShape, webcamPosition, } = editorState; @@ -195,6 +196,7 @@ export default function VideoEditor() { annotationRegions: normalizedEditor.annotationRegions, aspectRatio: normalizedEditor.aspectRatio, webcamLayoutPreset: normalizedEditor.webcamLayoutPreset, + webcamMaskShape: normalizedEditor.webcamMaskShape, webcamPosition: normalizedEditor.webcamPosition, }); setExportQuality(normalizedEditor.exportQuality); @@ -264,6 +266,7 @@ export default function VideoEditor() { annotationRegions, aspectRatio, webcamLayoutPreset, + webcamMaskShape, webcamPosition, exportQuality, exportFormat, @@ -287,6 +290,7 @@ export default function VideoEditor() { annotationRegions, aspectRatio, webcamLayoutPreset, + webcamMaskShape, webcamPosition, exportQuality, exportFormat, @@ -380,6 +384,7 @@ export default function VideoEditor() { annotationRegions, aspectRatio, webcamLayoutPreset, + webcamMaskShape, webcamPosition, exportQuality, exportFormat, @@ -434,6 +439,7 @@ export default function VideoEditor() { annotationRegions, aspectRatio, webcamLayoutPreset, + webcamMaskShape, webcamPosition, exportQuality, exportFormat, @@ -1090,6 +1096,7 @@ export default function VideoEditor() { cropRegion, annotationRegions, webcamLayoutPreset, + webcamMaskShape, webcamPosition, previewWidth, previewHeight, @@ -1221,6 +1228,7 @@ export default function VideoEditor() { cropRegion, annotationRegions, webcamLayoutPreset, + webcamMaskShape, webcamPosition, previewWidth, previewHeight, @@ -1289,6 +1297,7 @@ export default function VideoEditor() { isPlaying, aspectRatio, webcamLayoutPreset, + webcamMaskShape, webcamPosition, exportQuality, handleExportSaved, @@ -1473,6 +1482,7 @@ export default function VideoEditor() { videoPath={videoPath || ""} webcamVideoPath={webcamVideoPath || undefined} webcamLayoutPreset={webcamLayoutPreset} + webcamMaskShape={webcamMaskShape} webcamPosition={webcamPosition} onWebcamPositionChange={(pos) => updateState({ webcamPosition: pos })} onWebcamPositionDragEnd={commitState} @@ -1613,6 +1623,8 @@ export default function VideoEditor() { webcamPosition: preset === "vertical-stack" ? null : webcamPosition, }) } + webcamMaskShape={webcamMaskShape} + onWebcamMaskShapeChange={(shape) => pushState({ webcamMaskShape: shape })} videoElement={videoPlaybackRef.current?.video || null} exportQuality={exportQuality} onExportQualityChange={setExportQuality} diff --git a/src/components/video-editor/VideoPlayback.tsx b/src/components/video-editor/VideoPlayback.tsx index 71030bb..3bc4048 100644 --- a/src/components/video-editor/VideoPlayback.tsx +++ b/src/components/video-editor/VideoPlayback.tsx @@ -25,6 +25,7 @@ import { type StyledRenderRect, type WebcamLayoutPreset, } from "@/lib/compositeLayout"; +import { getCssClipPath } from "@/lib/webcamMaskShapes"; import { type AspectRatio, formatAspectRatioForCSS, @@ -63,6 +64,7 @@ interface VideoPlaybackProps { videoPath: string; webcamVideoPath?: string; webcamLayoutPreset: WebcamLayoutPreset; + webcamMaskShape?: import("./types").WebcamMaskShape; webcamPosition?: { cx: number; cy: number } | null; onWebcamPositionChange?: (position: { cx: number; cy: number }) => void; onWebcamPositionDragEnd?: () => void; @@ -111,6 +113,7 @@ const VideoPlayback = forwardRef( videoPath, webcamVideoPath, webcamLayoutPreset, + webcamMaskShape, webcamPosition, onWebcamPositionChange, onWebcamPositionDragEnd, @@ -272,6 +275,7 @@ const VideoPlayback = forwardRef( webcamDimensions, webcamLayoutPreset, webcamPosition, + webcamMaskShape, }); if (result) { @@ -302,6 +306,7 @@ const VideoPlayback = forwardRef( webcamDimensions, webcamLayoutPreset, webcamPosition, + webcamMaskShape, ]); useEffect(() => { @@ -1154,31 +1159,47 @@ const VideoPlayback = forwardRef( : "none", }} /> - {webcamVideoPath && ( -