Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion src/components/video-editor/SettingsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -51,6 +51,7 @@ import type {
FigureData,
PlaybackSpeed,
WebcamLayoutPreset,
WebcamMaskShape,
ZoomDepth,
} from "./types";
import { SPEED_OPTIONS } from "./types";
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<string[]>([]);
Expand Down Expand Up @@ -623,6 +628,33 @@ export function SettingsPanel({
</SelectContent>
</Select>
</div>
<div
className={`mt-2 p-2 rounded-lg bg-white/5 border border-white/5 ${webcamLayoutPreset === "vertical-stack" ? "opacity-40 pointer-events-none" : ""}`}
>
<div className="text-[10px] font-medium text-slate-300 mb-1.5">
{t("layout.shape")}
</div>
<Select
value={
webcamLayoutPreset === "vertical-stack"
? "rounded-rectangle"
: webcamMaskShape
}
onValueChange={(value: WebcamMaskShape) => onWebcamMaskShapeChange?.(value)}
disabled={webcamLayoutPreset === "vertical-stack"}
>
<SelectTrigger className="h-8 bg-black/20 border-white/10 text-xs">
<SelectValue placeholder={t("layout.selectShape")} />
</SelectTrigger>
<SelectContent>
{WEBCAM_MASK_SHAPES.map((shape) => (
<SelectItem key={shape} value={shape} className="text-xs">
{shape === "circle" ? t("layout.circle") : t("layout.roundedRectangle")}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</AccordionContent>
</AccordionItem>
)}
Expand Down
12 changes: 12 additions & 0 deletions src/components/video-editor/VideoEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export default function VideoEditor() {
padding,
aspectRatio,
webcamLayoutPreset,
webcamMaskShape,
webcamPosition,
} = editorState;

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -264,6 +266,7 @@ export default function VideoEditor() {
annotationRegions,
aspectRatio,
webcamLayoutPreset,
webcamMaskShape,
webcamPosition,
exportQuality,
exportFormat,
Expand All @@ -287,6 +290,7 @@ export default function VideoEditor() {
annotationRegions,
aspectRatio,
webcamLayoutPreset,
webcamMaskShape,
webcamPosition,
exportQuality,
exportFormat,
Expand Down Expand Up @@ -380,6 +384,7 @@ export default function VideoEditor() {
annotationRegions,
aspectRatio,
webcamLayoutPreset,
webcamMaskShape,
webcamPosition,
exportQuality,
exportFormat,
Expand Down Expand Up @@ -434,6 +439,7 @@ export default function VideoEditor() {
annotationRegions,
aspectRatio,
webcamLayoutPreset,
webcamMaskShape,
webcamPosition,
exportQuality,
exportFormat,
Expand Down Expand Up @@ -1090,6 +1096,7 @@ export default function VideoEditor() {
cropRegion,
annotationRegions,
webcamLayoutPreset,
webcamMaskShape,
webcamPosition,
previewWidth,
previewHeight,
Expand Down Expand Up @@ -1221,6 +1228,7 @@ export default function VideoEditor() {
cropRegion,
annotationRegions,
webcamLayoutPreset,
webcamMaskShape,
webcamPosition,
previewWidth,
previewHeight,
Expand Down Expand Up @@ -1289,6 +1297,7 @@ export default function VideoEditor() {
isPlaying,
aspectRatio,
webcamLayoutPreset,
webcamMaskShape,
webcamPosition,
exportQuality,
handleExportSaved,
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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}
Expand Down
8 changes: 7 additions & 1 deletion src/components/video-editor/VideoPlayback.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
type Size,
type StyledRenderRect,
type WebcamLayoutPreset,
type WebcamMaskShape,
} from "@/lib/compositeLayout";
import {
type AspectRatio,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -111,6 +113,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
videoPath,
webcamVideoPath,
webcamLayoutPreset,
webcamMaskShape = "rounded-rectangle",
webcamPosition,
onWebcamPositionChange,
onWebcamPositionDragEnd,
Expand Down Expand Up @@ -271,6 +274,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
padding,
webcamDimensions,
webcamLayoutPreset,
webcamMaskShape,
webcamPosition,
});

Expand Down Expand Up @@ -301,6 +305,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
padding,
webcamDimensions,
webcamLayoutPreset,
webcamMaskShape,
webcamPosition,
]);

Expand Down Expand Up @@ -1164,7 +1169,8 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
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,
Expand Down
4 changes: 3 additions & 1 deletion src/components/video-editor/projectPersistence.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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,
Expand Down
9 changes: 8 additions & 1 deletion src/components/video-editor/projectPersistence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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;
Expand All @@ -44,6 +46,7 @@ export interface ProjectEditorState {
annotationRegions: AnnotationRegion[];
aspectRatio: AspectRatio;
webcamLayoutPreset: WebcamLayoutPreset;
webcamMaskShape: WebcamMaskShape;
webcamPosition: WebcamPosition | null;
exportQuality: ExportQuality;
exportFormat: ExportFormat;
Expand Down Expand Up @@ -352,6 +355,10 @@ export function normalizeProjectEditor(editor: Partial<ProjectEditorState>): 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" &&
Expand Down
5 changes: 3 additions & 2 deletions src/components/video-editor/types.ts
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
4 changes: 4 additions & 0 deletions src/components/video-editor/videoPlayback/layoutUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
type Size,
type StyledRenderRect,
type WebcamLayoutPreset,
type WebcamMaskShape,
} from "@/lib/compositeLayout";
import type { CropRegion } from "../types";

Expand All @@ -20,6 +21,7 @@ interface LayoutParams {
padding?: number;
webcamDimensions?: Size | null;
webcamLayoutPreset?: WebcamLayoutPreset;
webcamMaskShape?: WebcamMaskShape;
webcamPosition?: { cx: number; cy: number } | null;
}

Expand All @@ -46,6 +48,7 @@ export function layoutVideoContent(params: LayoutParams): LayoutResult | null {
padding = 0,
webcamDimensions,
webcamLayoutPreset,
webcamMaskShape,
webcamPosition,
} = params;

Expand Down Expand Up @@ -93,6 +96,7 @@ export function layoutVideoContent(params: LayoutParams): LayoutResult | null {
screenSize: { width: croppedVideoWidth, height: croppedVideoHeight },
webcamSize: webcamDimensions,
layoutPreset: webcamLayoutPreset,
webcamMaskShape,
webcamPosition,
});

Expand Down
4 changes: 4 additions & 0 deletions src/hooks/useEditorHistory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -31,6 +33,7 @@ export interface EditorState {
padding: number;
aspectRatio: AspectRatio;
webcamLayoutPreset: WebcamLayoutPreset;
webcamMaskShape: WebcamMaskShape;
webcamPosition: WebcamPosition | null;
}

Expand All @@ -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,
};

Expand Down
6 changes: 5 additions & 1 deletion src/i18n/locales/en/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
6 changes: 5 additions & 1 deletion src/i18n/locales/es/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
6 changes: 5 additions & 1 deletion src/i18n/locales/zh-CN/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@
"title": "布局",
"preset": "预设",
"selectPreset": "选择预设",
"shape": "形状",
"selectShape": "选择形状",
"pictureInPicture": "画中画",
"verticalStack": "垂直堆叠"
"verticalStack": "垂直堆叠",
"roundedRectangle": "圆角矩形",
"circle": "圆形"
},
"effects": {
"title": "视频效果",
Expand Down
Loading