diff --git a/src/components/video-editor/SettingsPanel.tsx b/src/components/video-editor/SettingsPanel.tsx index f5afe35a..255d2080 100644 --- a/src/components/video-editor/SettingsPanel.tsx +++ b/src/components/video-editor/SettingsPanel.tsx @@ -35,7 +35,7 @@ import { Switch } from "@/components/ui/switch"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { useScopedT } from "@/contexts/I18nContext"; import { getAssetPath } from "@/lib/assetPath"; -import { WEBCAM_LAYOUT_PRESETS } from "@/lib/compositeLayout"; +import { WEBCAM_LAYOUT_PRESETS, WEBCAM_MASK_SHAPES } from "@/lib/compositeLayout"; import type { ExportFormat, ExportQuality, GifFrameRate, GifSizePreset } from "@/lib/exporter"; import { GIF_FRAME_RATES, GIF_SIZE_PRESETS } from "@/lib/exporter"; import { cn } from "@/lib/utils"; @@ -51,6 +51,7 @@ import type { FigureData, PlaybackSpeed, WebcamLayoutPreset, + WebcamMaskShape, ZoomDepth, } from "./types"; import { SPEED_OPTIONS } from "./types"; @@ -142,7 +143,9 @@ interface SettingsPanelProps { onSpeedDelete?: (id: string) => void; hasWebcam?: boolean; webcamLayoutPreset?: WebcamLayoutPreset; + webcamMaskShape?: WebcamMaskShape; onWebcamLayoutPresetChange?: (preset: WebcamLayoutPreset) => void; + onWebcamMaskShapeChange?: (shape: WebcamMaskShape) => void; } export default SettingsPanel; @@ -210,7 +213,9 @@ export function SettingsPanel({ onSpeedDelete, hasWebcam = false, webcamLayoutPreset = "picture-in-picture", + webcamMaskShape = "rounded-rectangle", onWebcamLayoutPresetChange, + onWebcamMaskShapeChange, }: SettingsPanelProps) { const t = useScopedT("settings"); const [wallpaperPaths, setWallpaperPaths] = useState([]); @@ -623,6 +628,33 @@ export function SettingsPanel({ +
+
+ {t("layout.shape")} +
+ +
)} diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx index 304d10f4..8238c03a 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} @@ -1607,12 +1617,14 @@ export default function VideoEditor() { aspectRatio={aspectRatio} hasWebcam={Boolean(webcamVideoPath)} webcamLayoutPreset={webcamLayoutPreset} + webcamMaskShape={webcamMaskShape} onWebcamLayoutPresetChange={(preset) => pushState({ webcamLayoutPreset: preset, webcamPosition: preset === "vertical-stack" ? null : webcamPosition, }) } + 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 71030bb8..b3c33da8 100644 --- a/src/components/video-editor/VideoPlayback.tsx +++ b/src/components/video-editor/VideoPlayback.tsx @@ -24,6 +24,7 @@ import { type Size, type StyledRenderRect, type WebcamLayoutPreset, + type WebcamMaskShape, } from "@/lib/compositeLayout"; import { type AspectRatio, @@ -63,6 +64,7 @@ interface VideoPlaybackProps { videoPath: string; webcamVideoPath?: string; webcamLayoutPreset: WebcamLayoutPreset; + webcamMaskShape?: 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 = "rounded-rectangle", webcamPosition, onWebcamPositionChange, onWebcamPositionDragEnd, @@ -271,6 +274,7 @@ const VideoPlayback = forwardRef( padding, webcamDimensions, webcamLayoutPreset, + webcamMaskShape, webcamPosition, }); @@ -301,6 +305,7 @@ const VideoPlayback = forwardRef( padding, webcamDimensions, webcamLayoutPreset, + webcamMaskShape, webcamPosition, ]); @@ -1164,7 +1169,8 @@ const VideoPlayback = forwardRef( top: webcamLayout?.y ?? 0, width: webcamLayout?.width ?? 0, height: webcamLayout?.height ?? 0, - borderRadius: webcamLayout?.borderRadius ?? 0, + borderRadius: + webcamLayout?.shape === "circle" ? "50%" : (webcamLayout?.borderRadius ?? 0), boxShadow: webcamCssBoxShadow, zIndex: 20, opacity: webcamLayout ? 1 : 0, diff --git a/src/components/video-editor/projectPersistence.test.ts b/src/components/video-editor/projectPersistence.test.ts index 13ea358f..4422fa5e 100644 --- a/src/components/video-editor/projectPersistence.test.ts +++ b/src/components/video-editor/projectPersistence.test.ts @@ -20,7 +20,7 @@ describe("projectPersistence media compatibility", () => { }); }); - it("creates version 2 projects with explicit media", () => { + it("creates version 3 projects with explicit media", () => { const project = createProjectData( { screenVideoPath: "/tmp/screen.webm", @@ -40,6 +40,8 @@ describe("projectPersistence media compatibility", () => { annotationRegions: [], aspectRatio: "16:9", webcamLayoutPreset: "picture-in-picture", + webcamMaskShape: "rounded-rectangle", + webcamPosition: null, exportQuality: "good", exportFormat: "mp4", gifFrameRate: 15, diff --git a/src/components/video-editor/projectPersistence.ts b/src/components/video-editor/projectPersistence.ts index 99f1bbaa..85da0f93 100644 --- a/src/components/video-editor/projectPersistence.ts +++ b/src/components/video-editor/projectPersistence.ts @@ -12,11 +12,13 @@ import { DEFAULT_FIGURE_DATA, DEFAULT_PLAYBACK_SPEED, DEFAULT_WEBCAM_LAYOUT_PRESET, + DEFAULT_WEBCAM_MASK_SHAPE, DEFAULT_WEBCAM_POSITION, DEFAULT_ZOOM_DEPTH, type SpeedRegion, type TrimRegion, type WebcamLayoutPreset, + type WebcamMaskShape, type WebcamPosition, type ZoomRegion, } from "./types"; @@ -28,7 +30,7 @@ export const WALLPAPER_PATHS = Array.from( (_, i) => `/wallpapers/wallpaper${i + 1}.jpg`, ); -export const PROJECT_VERSION = 2; +export const PROJECT_VERSION = 3; export interface ProjectEditorState { wallpaper: string; @@ -44,6 +46,7 @@ export interface ProjectEditorState { annotationRegions: AnnotationRegion[]; aspectRatio: AspectRatio; webcamLayoutPreset: WebcamLayoutPreset; + webcamMaskShape: WebcamMaskShape; webcamPosition: WebcamPosition | null; exportQuality: ExportQuality; exportFormat: ExportFormat; @@ -352,6 +355,10 @@ export function normalizeProjectEditor(editor: Partial): Pro editor.webcamLayoutPreset === "picture-in-picture" ? editor.webcamLayoutPreset : DEFAULT_WEBCAM_LAYOUT_PRESET, + webcamMaskShape: + editor.webcamMaskShape === "circle" || editor.webcamMaskShape === "rounded-rectangle" + ? editor.webcamMaskShape + : DEFAULT_WEBCAM_MASK_SHAPE, webcamPosition: editor.webcamPosition && typeof editor.webcamPosition === "object" && diff --git a/src/components/video-editor/types.ts b/src/components/video-editor/types.ts index ce49f8e4..abed0b40 100644 --- a/src/components/video-editor/types.ts +++ b/src/components/video-editor/types.ts @@ -1,9 +1,10 @@ -import type { WebcamLayoutPreset } from "@/lib/compositeLayout"; +import type { WebcamLayoutPreset, WebcamMaskShape } from "@/lib/compositeLayout"; export type ZoomDepth = 1 | 2 | 3 | 4 | 5 | 6; -export type { WebcamLayoutPreset }; +export type { WebcamLayoutPreset, WebcamMaskShape }; export const DEFAULT_WEBCAM_LAYOUT_PRESET: WebcamLayoutPreset = "picture-in-picture"; +export const DEFAULT_WEBCAM_MASK_SHAPE: WebcamMaskShape = "rounded-rectangle"; export interface WebcamPosition { cx: number; // normalized horizontal center (0-1) diff --git a/src/components/video-editor/videoPlayback/layoutUtils.ts b/src/components/video-editor/videoPlayback/layoutUtils.ts index 13d46318..54397ebb 100644 --- a/src/components/video-editor/videoPlayback/layoutUtils.ts +++ b/src/components/video-editor/videoPlayback/layoutUtils.ts @@ -5,6 +5,7 @@ import { type Size, type StyledRenderRect, type WebcamLayoutPreset, + type WebcamMaskShape, } from "@/lib/compositeLayout"; import type { CropRegion } from "../types"; @@ -20,6 +21,7 @@ interface LayoutParams { padding?: number; webcamDimensions?: Size | null; webcamLayoutPreset?: WebcamLayoutPreset; + webcamMaskShape?: WebcamMaskShape; webcamPosition?: { cx: number; cy: number } | null; } @@ -46,6 +48,7 @@ export function layoutVideoContent(params: LayoutParams): LayoutResult | null { padding = 0, webcamDimensions, webcamLayoutPreset, + webcamMaskShape, webcamPosition, } = params; @@ -93,6 +96,7 @@ export function layoutVideoContent(params: LayoutParams): LayoutResult | null { screenSize: { width: croppedVideoWidth, height: croppedVideoHeight }, webcamSize: webcamDimensions, layoutPreset: webcamLayoutPreset, + webcamMaskShape, webcamPosition, }); diff --git a/src/hooks/useEditorHistory.ts b/src/hooks/useEditorHistory.ts index ea155c8d..d261c1f8 100644 --- a/src/hooks/useEditorHistory.ts +++ b/src/hooks/useEditorHistory.ts @@ -5,12 +5,14 @@ import type { SpeedRegion, TrimRegion, WebcamLayoutPreset, + WebcamMaskShape, WebcamPosition, ZoomRegion, } from "@/components/video-editor/types"; import { DEFAULT_CROP_REGION, DEFAULT_WEBCAM_LAYOUT_PRESET, + DEFAULT_WEBCAM_MASK_SHAPE, DEFAULT_WEBCAM_POSITION, } from "@/components/video-editor/types"; import type { AspectRatio } from "@/utils/aspectRatioUtils"; @@ -31,6 +33,7 @@ export interface EditorState { padding: number; aspectRatio: AspectRatio; webcamLayoutPreset: WebcamLayoutPreset; + webcamMaskShape: WebcamMaskShape; webcamPosition: WebcamPosition | null; } @@ -48,6 +51,7 @@ export const INITIAL_EDITOR_STATE: EditorState = { padding: 50, aspectRatio: "16:9", webcamLayoutPreset: DEFAULT_WEBCAM_LAYOUT_PRESET, + webcamMaskShape: DEFAULT_WEBCAM_MASK_SHAPE, webcamPosition: DEFAULT_WEBCAM_POSITION, }; diff --git a/src/i18n/locales/en/settings.json b/src/i18n/locales/en/settings.json index 36b7462a..95695561 100644 --- a/src/i18n/locales/en/settings.json +++ b/src/i18n/locales/en/settings.json @@ -16,8 +16,12 @@ "title": "Layout", "preset": "Preset", "selectPreset": "Select preset", + "shape": "Shape", + "selectShape": "Select shape", "pictureInPicture": "Picture in Picture", - "verticalStack": "Vertical Stack" + "verticalStack": "Vertical Stack", + "roundedRectangle": "Rounded Rectangle", + "circle": "Circle" }, "effects": { "title": "Video Effects", diff --git a/src/i18n/locales/es/settings.json b/src/i18n/locales/es/settings.json index 46744801..3f3c1c8d 100644 --- a/src/i18n/locales/es/settings.json +++ b/src/i18n/locales/es/settings.json @@ -16,8 +16,12 @@ "title": "Diseño", "preset": "Predefinido", "selectPreset": "Seleccionar predefinido", + "shape": "Forma", + "selectShape": "Seleccionar forma", "pictureInPicture": "Imagen en imagen", - "verticalStack": "Apilado vertical" + "verticalStack": "Apilado vertical", + "roundedRectangle": "Rectángulo redondeado", + "circle": "Círculo" }, "effects": { "title": "Efectos de video", diff --git a/src/i18n/locales/zh-CN/settings.json b/src/i18n/locales/zh-CN/settings.json index 41bf55b3..20386585 100644 --- a/src/i18n/locales/zh-CN/settings.json +++ b/src/i18n/locales/zh-CN/settings.json @@ -16,8 +16,12 @@ "title": "布局", "preset": "预设", "selectPreset": "选择预设", + "shape": "形状", + "selectShape": "选择形状", "pictureInPicture": "画中画", - "verticalStack": "垂直堆叠" + "verticalStack": "垂直堆叠", + "roundedRectangle": "圆角矩形", + "circle": "圆形" }, "effects": { "title": "视频效果", diff --git a/src/lib/compositeLayout.test.ts b/src/lib/compositeLayout.test.ts index 5dc5339f..204cc85e 100644 --- a/src/lib/compositeLayout.test.ts +++ b/src/lib/compositeLayout.test.ts @@ -33,7 +33,7 @@ describe("computeCompositeLayout", () => { ).toBeLessThanOrEqual(1920); }); - it("centers the combined screen and webcam stack in vertical stack mode", () => { + it("fills the canvas with the combined screen and webcam stack in vertical stack mode", () => { const layout = computeCompositeLayout({ canvasSize: { width: 1920, height: 1080 }, maxContentSize: { width: 1536, height: 864 }, @@ -44,21 +44,22 @@ describe("computeCompositeLayout", () => { expect(layout).not.toBeNull(); expect(layout?.screenRect).toEqual({ - x: 576, - y: 108, - width: 768, - height: 432, + x: 0, + y: 0, + width: 1920, + height: 0, }); expect(layout?.webcamRect).toEqual({ - x: 576, - y: 540, - width: 768, - height: 432, + x: 0, + y: 0, + width: 1920, + height: 1080, borderRadius: 0, + shape: "rounded-rectangle", }); }); - it("keeps the screen centered and omits the webcam when dimensions are unavailable", () => { + it("fills the canvas and omits the webcam when vertical stack dimensions are unavailable", () => { const layout = computeCompositeLayout({ canvasSize: { width: 1920, height: 1080 }, maxContentSize: { width: 1536, height: 864 }, @@ -68,11 +69,26 @@ describe("computeCompositeLayout", () => { expect(layout).not.toBeNull(); expect(layout?.screenRect).toEqual({ - x: 192, - y: 108, - width: 1536, - height: 864, + x: 0, + y: 0, + width: 1920, + height: 1080, }); expect(layout?.webcamRect).toBeNull(); }); + + it("uses a square webcam rect for circle masks", () => { + const layout = computeCompositeLayout({ + canvasSize: { width: 1920, height: 1080 }, + screenSize: { width: 1920, height: 1080 }, + webcamSize: { width: 1280, height: 720 }, + webcamMaskShape: "circle", + }); + + expect(layout).not.toBeNull(); + expect(layout?.webcamRect).not.toBeNull(); + expect(layout?.webcamRect?.width).toBe(layout?.webcamRect?.height); + expect(layout?.webcamRect?.shape).toBe("circle"); + expect(layout?.webcamRect?.borderRadius).toBe((layout?.webcamRect?.width ?? 0) / 2); + }); }); diff --git a/src/lib/compositeLayout.ts b/src/lib/compositeLayout.ts index 5feca50e..da29aecc 100644 --- a/src/lib/compositeLayout.ts +++ b/src/lib/compositeLayout.ts @@ -7,6 +7,7 @@ export interface RenderRect { export interface StyledRenderRect extends RenderRect { borderRadius: number; + shape: WebcamMaskShape; } export interface Size { @@ -15,6 +16,7 @@ export interface Size { } export type WebcamLayoutPreset = "picture-in-picture" | "vertical-stack"; +export type WebcamMaskShape = "rounded-rectangle" | "circle"; export interface WebcamLayoutShadow { color: string; @@ -103,6 +105,19 @@ export const WEBCAM_LAYOUT_PRESETS = Object.entries(WEBCAM_LAYOUT_PRESET_MAP).ma }), ); +export const WEBCAM_MASK_SHAPES: WebcamMaskShape[] = ["rounded-rectangle", "circle"]; + +export function resolveWebcamMaskShape( + shape: WebcamMaskShape = "rounded-rectangle", + layoutPreset: WebcamLayoutPreset = "picture-in-picture", +): WebcamMaskShape { + if (layoutPreset === "vertical-stack") { + return "rounded-rectangle"; + } + + return shape === "circle" ? "circle" : "rounded-rectangle"; +} + export function getWebcamLayoutPresetDefinition( preset: WebcamLayoutPreset = "picture-in-picture", ): WebcamLayoutPresetDefinition { @@ -124,6 +139,7 @@ export function computeCompositeLayout(params: { screenSize: Size; webcamSize?: Size | null; layoutPreset?: WebcamLayoutPreset; + webcamMaskShape?: WebcamMaskShape; webcamPosition?: { cx: number; cy: number } | null; }): WebcamCompositeLayout | null { const { @@ -132,6 +148,7 @@ export function computeCompositeLayout(params: { screenSize, webcamSize, layoutPreset = "picture-in-picture", + webcamMaskShape = "rounded-rectangle", webcamPosition, } = params; const { width: canvasWidth, height: canvasHeight } = canvasSize; @@ -139,6 +156,7 @@ export function computeCompositeLayout(params: { const webcamWidth = webcamSize?.width; const webcamHeight = webcamSize?.height; const preset = getWebcamLayoutPresetDefinition(layoutPreset); + const resolvedMaskShape = resolveWebcamMaskShape(webcamMaskShape, layoutPreset); if (canvasWidth <= 0 || canvasHeight <= 0 || screenWidth <= 0 || screenHeight <= 0) { return null; @@ -175,6 +193,7 @@ export function computeCompositeLayout(params: { width: resolvedWebcamWidth, height: resolvedWebcamHeight, borderRadius: 0, + shape: "rounded-rectangle", }, screenCover: true, }; @@ -198,8 +217,11 @@ export function computeCompositeLayout(params: { const maxWidth = Math.max(transform.minSize, canvasWidth * transform.maxStageFraction); const maxHeight = Math.max(transform.minSize, canvasHeight * transform.maxStageFraction); const scale = Math.min(maxWidth / webcamWidth, maxHeight / webcamHeight); - const width = Math.round(webcamWidth * scale); - const height = Math.round(webcamHeight * scale); + const scaledWidth = Math.round(webcamWidth * scale); + const scaledHeight = Math.round(webcamHeight * scale); + const size = Math.min(scaledWidth, scaledHeight); + const width = resolvedMaskShape === "circle" ? size : scaledWidth; + const height = resolvedMaskShape === "circle" ? size : scaledHeight; let webcamX: number; let webcamY: number; @@ -224,13 +246,17 @@ export function computeCompositeLayout(params: { y: webcamY, width, height, - borderRadius: Math.min( - preset.borderRadius.max, - Math.max( - preset.borderRadius.min, - Math.round(Math.min(width, height) * preset.borderRadius.fraction), - ), - ), + borderRadius: + resolvedMaskShape === "circle" + ? Math.round(size / 2) + : Math.min( + preset.borderRadius.max, + Math.max( + preset.borderRadius.min, + Math.round(Math.min(width, height) * preset.borderRadius.fraction), + ), + ), + shape: resolvedMaskShape, }, }; } diff --git a/src/lib/exporter/frameRenderer.ts b/src/lib/exporter/frameRenderer.ts index 4a9b2bdc..df2c507a 100644 --- a/src/lib/exporter/frameRenderer.ts +++ b/src/lib/exporter/frameRenderer.ts @@ -13,6 +13,7 @@ import type { CropRegion, SpeedRegion, WebcamLayoutPreset, + WebcamMaskShape, ZoomDepth, ZoomRegion, } from "@/components/video-editor/types"; @@ -61,6 +62,7 @@ interface FrameRenderConfig { videoHeight: number; webcamSize?: Size | null; webcamLayoutPreset?: WebcamLayoutPreset; + webcamMaskShape?: WebcamMaskShape; webcamPosition?: { cx: number; cy: number } | null; annotationRegions?: AnnotationRegion[]; speedRegions?: SpeedRegion[]; @@ -440,6 +442,7 @@ export class FrameRenderer { screenSize: { width: croppedVideoWidth, height: croppedVideoHeight }, webcamSize: webcamFrame ? this.config.webcamSize : null, layoutPreset: this.config.webcamLayoutPreset, + webcamMaskShape: this.config.webcamMaskShape, webcamPosition: this.config.webcamPosition, }); if (!compositeLayout) return; diff --git a/src/lib/exporter/gifExporter.ts b/src/lib/exporter/gifExporter.ts index 8aac0b56..db06a0ef 100644 --- a/src/lib/exporter/gifExporter.ts +++ b/src/lib/exporter/gifExporter.ts @@ -5,6 +5,7 @@ import type { SpeedRegion, TrimRegion, WebcamLayoutPreset, + WebcamMaskShape, ZoomRegion, } from "@/components/video-editor/types"; import { AsyncVideoFrameQueue } from "./asyncVideoFrameQueue"; @@ -41,6 +42,7 @@ interface GifExporterConfig { videoPadding?: number; cropRegion: CropRegion; webcamLayoutPreset?: WebcamLayoutPreset; + webcamMaskShape?: WebcamMaskShape; webcamPosition?: { cx: number; cy: number } | null; annotationRegions?: AnnotationRegion[]; previewWidth?: number; @@ -141,6 +143,7 @@ export class GifExporter { videoHeight: videoInfo.height, webcamSize: webcamInfo ? { width: webcamInfo.width, height: webcamInfo.height } : null, webcamLayoutPreset: this.config.webcamLayoutPreset, + webcamMaskShape: this.config.webcamMaskShape, webcamPosition: this.config.webcamPosition, annotationRegions: this.config.annotationRegions, speedRegions: this.config.speedRegions, diff --git a/src/lib/exporter/videoExporter.ts b/src/lib/exporter/videoExporter.ts index 1b75a2e8..25da2137 100644 --- a/src/lib/exporter/videoExporter.ts +++ b/src/lib/exporter/videoExporter.ts @@ -4,6 +4,7 @@ import type { SpeedRegion, TrimRegion, WebcamLayoutPreset, + WebcamMaskShape, ZoomRegion, } from "@/components/video-editor/types"; import { AsyncVideoFrameQueue } from "./asyncVideoFrameQueue"; @@ -32,6 +33,7 @@ interface VideoExporterConfig extends ExportConfig { videoPadding?: number; cropRegion: CropRegion; webcamLayoutPreset?: WebcamLayoutPreset; + webcamMaskShape?: WebcamMaskShape; webcamPosition?: { cx: number; cy: number } | null; annotationRegions?: AnnotationRegion[]; previewWidth?: number; @@ -134,6 +136,7 @@ export class VideoExporter { videoHeight: videoInfo.height, webcamSize: webcamInfo ? { width: webcamInfo.width, height: webcamInfo.height } : null, webcamLayoutPreset: this.config.webcamLayoutPreset, + webcamMaskShape: this.config.webcamMaskShape, webcamPosition: this.config.webcamPosition, annotationRegions: this.config.annotationRegions, speedRegions: this.config.speedRegions,