diff --git a/src/components/video-editor/CropControl.tsx b/src/components/video-editor/CropControl.tsx index 07e769d..faedf1c 100644 --- a/src/components/video-editor/CropControl.tsx +++ b/src/components/video-editor/CropControl.tsx @@ -1,6 +1,7 @@ import { useEffect, useRef, useState } from "react"; import { cn } from "@/lib/utils"; import { type AspectRatio } from "@/utils/aspectRatioUtils"; +import { DEFAULT_SOURCE_DIMENSIONS } from "./editorDefaults"; interface CropRegion { x: number; // 0-1 normalized @@ -32,8 +33,8 @@ export function CropControl({ videoElement, cropRegion, onCropChange }: CropCont const ctx = canvas.getContext("2d", { alpha: false }); if (!ctx) return; - canvas.width = videoElement.videoWidth || 1920; - canvas.height = videoElement.videoHeight || 1080; + canvas.width = videoElement.videoWidth || DEFAULT_SOURCE_DIMENSIONS.width; + canvas.height = videoElement.videoHeight || DEFAULT_SOURCE_DIMENSIONS.height; const draw = () => { if (videoElement.readyState >= 2) { diff --git a/src/components/video-editor/SettingsPanel.tsx b/src/components/video-editor/SettingsPanel.tsx index 3bd8750..571b587 100644 --- a/src/components/video-editor/SettingsPanel.tsx +++ b/src/components/video-editor/SettingsPanel.tsx @@ -54,6 +54,14 @@ import { AnnotationSettingsPanel } from "./AnnotationSettingsPanel"; import { BlurSettingsPanel } from "./BlurSettingsPanel"; import { CropControl } from "./CropControl"; import { parseCustomPlaybackSpeedInput } from "./customPlaybackSpeed"; +import { + DEFAULT_CURSOR_SETTINGS, + DEFAULT_EDITOR_LAYOUT_SETTINGS, + DEFAULT_EXPORT_SETTINGS, + DEFAULT_GIF_SETTINGS, + DEFAULT_SOURCE_DIMENSIONS, + DEFAULT_WEBCAM_SETTINGS, +} from "./editorDefaults"; import { KeyboardShortcutsHelp } from "./KeyboardShortcutsHelp"; import type { AnnotationRegion, @@ -71,7 +79,6 @@ import type { ZoomFocusMode, } from "./types"; import { - DEFAULT_WEBCAM_SIZE_PRESET, MAX_ZOOM_SCALE, MIN_ZOOM_SCALE, ROTATION_3D_PRESET_ORDER, @@ -383,24 +390,24 @@ export function SettingsPanel({ borderRadius = 0, onBorderRadiusChange, onBorderRadiusCommit, - padding = 50, + padding = DEFAULT_EDITOR_LAYOUT_SETTINGS.padding, onPaddingChange, onPaddingCommit, cropRegion, onCropChange, aspectRatio, videoElement, - exportQuality = "good", + exportQuality = DEFAULT_EXPORT_SETTINGS.quality, onExportQualityChange, - exportFormat = "mp4", + exportFormat = DEFAULT_EXPORT_SETTINGS.format, onExportFormatChange, - gifFrameRate = 15, + gifFrameRate = DEFAULT_GIF_SETTINGS.frameRate, onGifFrameRateChange, - gifLoop = true, + gifLoop = DEFAULT_GIF_SETTINGS.loop, onGifLoopChange, - gifSizePreset = "medium", + gifSizePreset = DEFAULT_GIF_SETTINGS.sizePreset, onGifSizePresetChange, - gifOutputDimensions = { width: 1280, height: 720 }, + gifOutputDimensions = DEFAULT_GIF_SETTINGS.outputDimensions, onExport, unsavedExport, onSaveUnsavedExport, @@ -422,25 +429,25 @@ export function SettingsPanel({ onSpeedChange, onSpeedDelete, hasWebcam = false, - webcamLayoutPreset = "picture-in-picture", + webcamLayoutPreset = DEFAULT_WEBCAM_SETTINGS.layoutPreset, onWebcamLayoutPresetChange, - webcamMaskShape = "rectangle", + webcamMaskShape = DEFAULT_WEBCAM_SETTINGS.maskShape, onWebcamMaskShapeChange, - webcamSizePreset = DEFAULT_WEBCAM_SIZE_PRESET, + webcamSizePreset = DEFAULT_WEBCAM_SETTINGS.sizePreset, onWebcamSizePresetChange, onWebcamSizePresetCommit, onSaveDiagnostic, - showCursor = true, + showCursor = DEFAULT_CURSOR_SETTINGS.show, onShowCursorChange, - cursorSize = 3.0, + cursorSize = DEFAULT_CURSOR_SETTINGS.size, onCursorSizeChange, - cursorSmoothing = 0.67, + cursorSmoothing = DEFAULT_CURSOR_SETTINGS.smoothing, onCursorSmoothingChange, - cursorMotionBlur = 0.35, + cursorMotionBlur = DEFAULT_CURSOR_SETTINGS.motionBlur, onCursorMotionBlurChange, - cursorClickBounce = 2.5, + cursorClickBounce = DEFAULT_CURSOR_SETTINGS.clickBounce, onCursorClickBounceChange, - cursorClipToBounds = false, + cursorClipToBounds = DEFAULT_CURSOR_SETTINGS.clipToBounds, onCursorClipToBoundsChange, hasCursorData = false, showCursorSettings = true, @@ -479,8 +486,8 @@ export function SettingsPanel({ const [cropAspectRatio, setCropAspectRatio] = useState(""); const isPortraitCanvas = isPortraitAspectRatio(aspectRatio); - const videoWidth = videoElement?.videoWidth || 1920; - const videoHeight = videoElement?.videoHeight || 1080; + const videoWidth = videoElement?.videoWidth || DEFAULT_SOURCE_DIMENSIONS.width; + const videoHeight = videoElement?.videoHeight || DEFAULT_SOURCE_DIMENSIONS.height; const handleCropNumericChange = useCallback( (field: "x" | "y" | "width" | "height", pixelValue: number) => { diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx index ce6314f..b44d9b6 100644 --- a/src/components/video-editor/VideoEditor.tsx +++ b/src/components/video-editor/VideoEditor.tsx @@ -49,6 +49,12 @@ import { isPortraitAspectRatio, } from "@/utils/aspectRatioUtils"; import { ExportDialog } from "./ExportDialog"; +import { + DEFAULT_CURSOR_SETTINGS, + DEFAULT_EXPORT_SETTINGS, + DEFAULT_GIF_SETTINGS, + DEFAULT_SOURCE_DIMENSIONS, +} from "./editorDefaults"; import PlaybackControls from "./PlaybackControls"; import { createProjectData, @@ -71,11 +77,6 @@ import { DEFAULT_ANNOTATION_SIZE, DEFAULT_ANNOTATION_STYLE, DEFAULT_BLUR_DATA, - DEFAULT_CURSOR_CLICK_BOUNCE, - DEFAULT_CURSOR_CLIP_TO_BOUNDS, - DEFAULT_CURSOR_MOTION_BLUR, - DEFAULT_CURSOR_SIZE, - DEFAULT_CURSOR_SMOOTHING, DEFAULT_FIGURE_DATA, DEFAULT_PLAYBACK_SPEED, DEFAULT_ZOOM_DEPTH, @@ -203,11 +204,15 @@ export default function VideoEditor() { const [exportError, setExportError] = useState(null); const [showExportDialog, setShowExportDialog] = useState(false); const [showNewRecordingDialog, setShowNewRecordingDialog] = useState(false); - const [exportQuality, setExportQuality] = useState("good"); - const [exportFormat, setExportFormat] = useState("mp4"); - const [gifFrameRate, setGifFrameRate] = useState(15); - const [gifLoop, setGifLoop] = useState(true); - const [gifSizePreset, setGifSizePreset] = useState("medium"); + const [exportQuality, setExportQuality] = useState( + DEFAULT_EXPORT_SETTINGS.quality, + ); + const [exportFormat, setExportFormat] = useState(DEFAULT_EXPORT_SETTINGS.format); + const [gifFrameRate, setGifFrameRate] = useState(DEFAULT_GIF_SETTINGS.frameRate); + const [gifLoop, setGifLoop] = useState(DEFAULT_GIF_SETTINGS.loop); + const [gifSizePreset, setGifSizePreset] = useState( + DEFAULT_GIF_SETTINGS.sizePreset, + ); const [exportedFilePath, setExportedFilePath] = useState(null); const [lastSavedSnapshot, setLastSavedSnapshot] = useState(null); const [unsavedExport, setUnsavedExport] = useState<{ @@ -238,12 +243,14 @@ export default function VideoEditor() { }, [cursorRecordingData, cursorTelemetry]); // Cursor & motion blur visual settings (non-undoable preferences) - const [showCursor, setShowCursor] = useState(true); - const [cursorSize, setCursorSize] = useState(DEFAULT_CURSOR_SIZE); - const [cursorSmoothing, setCursorSmoothing] = useState(DEFAULT_CURSOR_SMOOTHING); - const [cursorMotionBlur, setCursorMotionBlur] = useState(DEFAULT_CURSOR_MOTION_BLUR); - const [cursorClickBounce, setCursorClickBounce] = useState(DEFAULT_CURSOR_CLICK_BOUNCE); - const [cursorClipToBounds, setCursorClipToBounds] = useState(DEFAULT_CURSOR_CLIP_TO_BOUNDS); + const [showCursor, setShowCursor] = useState(DEFAULT_CURSOR_SETTINGS.show); + const [cursorSize, setCursorSize] = useState(DEFAULT_CURSOR_SETTINGS.size); + const [cursorSmoothing, setCursorSmoothing] = useState(DEFAULT_CURSOR_SETTINGS.smoothing); + const [cursorMotionBlur, setCursorMotionBlur] = useState(DEFAULT_CURSOR_SETTINGS.motionBlur); + const [cursorClickBounce, setCursorClickBounce] = useState(DEFAULT_CURSOR_SETTINGS.clickBounce); + const [cursorClipToBounds, setCursorClipToBounds] = useState( + DEFAULT_CURSOR_SETTINGS.clipToBounds, + ); const [nativePlatform, setNativePlatform] = useState(null); const [recordingCursorCaptureMode, setRecordingCursorCaptureMode] = useState(null); @@ -1575,8 +1582,8 @@ export default function VideoEditor() { videoPlaybackRef.current?.pause(); } - const sourceWidth = video.videoWidth || 1920; - const sourceHeight = video.videoHeight || 1080; + const sourceWidth = video.videoWidth || DEFAULT_SOURCE_DIMENSIONS.width; + const sourceHeight = video.videoHeight || DEFAULT_SOURCE_DIMENSIONS.height; const effectiveSourceDimensions = calculateEffectiveSourceDimensions( sourceWidth, sourceHeight, @@ -1590,8 +1597,8 @@ export default function VideoEditor() { // Get preview CONTAINER dimensions for scaling const playbackRef = videoPlaybackRef.current; const containerElement = playbackRef?.containerRef?.current; - const previewWidth = containerElement?.clientWidth || 1920; - const previewHeight = containerElement?.clientHeight || 1080; + const previewWidth = containerElement?.clientWidth || DEFAULT_SOURCE_DIMENSIONS.width; + const previewHeight = containerElement?.clientHeight || DEFAULT_SOURCE_DIMENSIONS.height; if (settings.format === "gif" && settings.gifConfig) { // GIF Export @@ -1845,8 +1852,8 @@ export default function VideoEditor() { } // Build export settings from current state - const sourceWidth = video.videoWidth || 1920; - const sourceHeight = video.videoHeight || 1080; + const sourceWidth = video.videoWidth || DEFAULT_SOURCE_DIMENSIONS.width; + const sourceHeight = video.videoHeight || DEFAULT_SOURCE_DIMENSIONS.height; const effectiveSourceDimensions = calculateEffectiveSourceDimensions( sourceWidth, sourceHeight, @@ -2050,8 +2057,10 @@ export default function VideoEditor() { aspectRatio: aspectRatio === "native" ? getNativeAspectRatioValue( - videoPlaybackRef.current?.video?.videoWidth || 1920, - videoPlaybackRef.current?.video?.videoHeight || 1080, + videoPlaybackRef.current?.video?.videoWidth || + DEFAULT_SOURCE_DIMENSIONS.width, + videoPlaybackRef.current?.video?.videoHeight || + DEFAULT_SOURCE_DIMENSIONS.height, cropRegion, ) : getAspectRatioValue(aspectRatio), @@ -2217,21 +2226,27 @@ export default function VideoEditor() { onGifSizePresetChange={setGifSizePreset} gifOutputDimensions={calculateOutputDimensions( calculateEffectiveSourceDimensions( - videoPlaybackRef.current?.video?.videoWidth || 1920, - videoPlaybackRef.current?.video?.videoHeight || 1080, + videoPlaybackRef.current?.video?.videoWidth || + DEFAULT_SOURCE_DIMENSIONS.width, + videoPlaybackRef.current?.video?.videoHeight || + DEFAULT_SOURCE_DIMENSIONS.height, cropRegion, ).width, calculateEffectiveSourceDimensions( - videoPlaybackRef.current?.video?.videoWidth || 1920, - videoPlaybackRef.current?.video?.videoHeight || 1080, + videoPlaybackRef.current?.video?.videoWidth || + DEFAULT_SOURCE_DIMENSIONS.width, + videoPlaybackRef.current?.video?.videoHeight || + DEFAULT_SOURCE_DIMENSIONS.height, cropRegion, ).height, gifSizePreset, GIF_SIZE_PRESETS, aspectRatio === "native" ? getNativeAspectRatioValue( - videoPlaybackRef.current?.video?.videoWidth || 1920, - videoPlaybackRef.current?.video?.videoHeight || 1080, + videoPlaybackRef.current?.video?.videoWidth || + DEFAULT_SOURCE_DIMENSIONS.width, + videoPlaybackRef.current?.video?.videoHeight || + DEFAULT_SOURCE_DIMENSIONS.height, cropRegion, ) : getAspectRatioValue(aspectRatio), diff --git a/src/components/video-editor/VideoPlayback.tsx b/src/components/video-editor/VideoPlayback.tsx index 45ca9df..5b25ced 100644 --- a/src/components/video-editor/VideoPlayback.tsx +++ b/src/components/video-editor/VideoPlayback.tsx @@ -49,15 +49,16 @@ import { getNativeAspectRatioValue, } from "@/utils/aspectRatioUtils"; import { AnnotationOverlay } from "./AnnotationOverlay"; +import { + DEFAULT_CURSOR_SETTINGS, + DEFAULT_EDITOR_LAYOUT_SETTINGS, + DEFAULT_SOURCE_DIMENSIONS, +} from "./editorDefaults"; import { type AnnotationRegion, type BlurData, type CursorTelemetryPoint, computeRotation3DContainScale, - DEFAULT_CURSOR_CLICK_BOUNCE, - DEFAULT_CURSOR_MOTION_BLUR, - DEFAULT_CURSOR_SIZE, - DEFAULT_CURSOR_SMOOTHING, DEFAULT_ROTATION_3D, isRotation3DIdentity, lerpRotation3D, @@ -244,7 +245,7 @@ const VideoPlayback = forwardRef( showBlur, motionBlurAmount = 0, borderRadius = 0, - padding = 50, + padding = DEFAULT_EDITOR_LAYOUT_SETTINGS.padding, cropRegion, trimRegions = [], speedRegions = [], @@ -265,11 +266,11 @@ const VideoPlayback = forwardRef( cursorTelemetry = [], cursorClickTimestamps = [], showCursor = false, - cursorSize = DEFAULT_CURSOR_SIZE, - cursorSmoothing = DEFAULT_CURSOR_SMOOTHING, - cursorMotionBlur = DEFAULT_CURSOR_MOTION_BLUR, - cursorClickBounce = DEFAULT_CURSOR_CLICK_BOUNCE, - cursorClipToBounds = false, + cursorSize = DEFAULT_CURSOR_SETTINGS.size, + cursorSmoothing = DEFAULT_CURSOR_SETTINGS.smoothing, + cursorMotionBlur = DEFAULT_CURSOR_SETTINGS.motionBlur, + cursorClickBounce = DEFAULT_CURSOR_SETTINGS.clickBounce, + cursorClipToBounds = DEFAULT_CURSOR_SETTINGS.clipToBounds, }, ref, ) => { @@ -1824,8 +1825,8 @@ const VideoPlayback = forwardRef( aspectRatio, aspectRatio === "native" ? getNativeAspectRatioValue( - lockedVideoDimensionsRef.current?.width || 1920, - lockedVideoDimensionsRef.current?.height || 1080, + lockedVideoDimensionsRef.current?.width || DEFAULT_SOURCE_DIMENSIONS.width, + lockedVideoDimensionsRef.current?.height || DEFAULT_SOURCE_DIMENSIONS.height, cropRegion, ) : undefined, diff --git a/src/components/video-editor/editorDefaults.test.ts b/src/components/video-editor/editorDefaults.test.ts new file mode 100644 index 0000000..8b515f1 --- /dev/null +++ b/src/components/video-editor/editorDefaults.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, it } from "vitest"; +import { INITIAL_EDITOR_STATE } from "@/hooks/useEditorHistory"; +import { DEFAULT_PREFS } from "@/lib/userPreferences"; +import { + DEFAULT_EDITOR_APPEARANCE_SETTINGS, + DEFAULT_EDITOR_LAYOUT_SETTINGS, + DEFAULT_EXPORT_SETTINGS, + DEFAULT_GIF_SETTINGS, + DEFAULT_WEBCAM_SETTINGS, +} from "./editorDefaults"; +import { normalizeProjectEditor } from "./projectPersistence"; + +describe("editor defaults SSOT", () => { + it("keeps history defaults aligned with editor defaults", () => { + expect(INITIAL_EDITOR_STATE).toMatchObject({ + ...DEFAULT_EDITOR_APPEARANCE_SETTINGS, + padding: DEFAULT_EDITOR_LAYOUT_SETTINGS.padding, + aspectRatio: DEFAULT_EDITOR_LAYOUT_SETTINGS.aspectRatio, + cropRegion: DEFAULT_EDITOR_LAYOUT_SETTINGS.cropRegion, + wallpaper: DEFAULT_EDITOR_LAYOUT_SETTINGS.wallpaper, + webcamLayoutPreset: DEFAULT_WEBCAM_SETTINGS.layoutPreset, + webcamMaskShape: DEFAULT_WEBCAM_SETTINGS.maskShape, + webcamSizePreset: DEFAULT_WEBCAM_SETTINGS.sizePreset, + webcamPosition: DEFAULT_WEBCAM_SETTINGS.position, + }); + }); + + it("keeps user preference defaults aligned with editor and export defaults", () => { + expect(DEFAULT_PREFS).toMatchObject({ + padding: DEFAULT_EDITOR_LAYOUT_SETTINGS.padding, + aspectRatio: DEFAULT_EDITOR_LAYOUT_SETTINGS.aspectRatio, + exportQuality: DEFAULT_EXPORT_SETTINGS.quality, + exportFormat: DEFAULT_EXPORT_SETTINGS.format, + }); + }); + + it("keeps project fallback normalization aligned with editor defaults", () => { + expect(normalizeProjectEditor({})).toMatchObject({ + ...DEFAULT_EDITOR_APPEARANCE_SETTINGS, + padding: DEFAULT_EDITOR_LAYOUT_SETTINGS.padding, + cropRegion: DEFAULT_EDITOR_LAYOUT_SETTINGS.cropRegion, + wallpaper: DEFAULT_EDITOR_LAYOUT_SETTINGS.wallpaper, + aspectRatio: DEFAULT_EDITOR_LAYOUT_SETTINGS.aspectRatio, + webcamLayoutPreset: DEFAULT_WEBCAM_SETTINGS.layoutPreset, + webcamMaskShape: DEFAULT_WEBCAM_SETTINGS.maskShape, + webcamSizePreset: DEFAULT_WEBCAM_SETTINGS.sizePreset, + webcamPosition: DEFAULT_WEBCAM_SETTINGS.position, + exportQuality: DEFAULT_EXPORT_SETTINGS.quality, + exportFormat: DEFAULT_EXPORT_SETTINGS.format, + gifFrameRate: DEFAULT_GIF_SETTINGS.frameRate, + gifLoop: DEFAULT_GIF_SETTINGS.loop, + gifSizePreset: DEFAULT_GIF_SETTINGS.sizePreset, + }); + }); +}); diff --git a/src/components/video-editor/editorDefaults.ts b/src/components/video-editor/editorDefaults.ts new file mode 100644 index 0000000..ac6ab61 --- /dev/null +++ b/src/components/video-editor/editorDefaults.ts @@ -0,0 +1,95 @@ +import type { ExportFormat, ExportQuality, GifFrameRate, GifSizePreset } from "@/lib/exporter"; +import { DEFAULT_WALLPAPER } from "@/lib/wallpaper"; +import type { AspectRatio } from "@/utils/aspectRatioUtils"; +import { + type CursorVisualSettings, + DEFAULT_CROP_REGION, + DEFAULT_CURSOR_CLICK_BOUNCE, + DEFAULT_CURSOR_CLIP_TO_BOUNDS, + DEFAULT_CURSOR_MOTION_BLUR, + DEFAULT_CURSOR_SIZE, + DEFAULT_CURSOR_SMOOTHING, + DEFAULT_WEBCAM_LAYOUT_PRESET, + DEFAULT_WEBCAM_MASK_SHAPE, + DEFAULT_WEBCAM_POSITION, + DEFAULT_WEBCAM_SIZE_PRESET, + type WebcamLayoutPreset, + type WebcamMaskShape, + type WebcamPosition, + type WebcamSizePreset, +} from "./types"; + +export const DEFAULT_SOURCE_DIMENSIONS = { + width: 1920, + height: 1080, +} as const; + +export const DEFAULT_GIF_OUTPUT_DIMENSIONS = { + width: 1280, + height: 720, +} as const; + +export const DEFAULT_EDITOR_APPEARANCE_SETTINGS: { + shadowIntensity: number; + showBlur: boolean; + motionBlurAmount: number; + borderRadius: number; +} = { + shadowIntensity: 0, + showBlur: false, + motionBlurAmount: 0, + borderRadius: 0, +}; + +export const DEFAULT_EDITOR_LAYOUT_SETTINGS: { + padding: number; + aspectRatio: AspectRatio; + cropRegion: typeof DEFAULT_CROP_REGION; + wallpaper: string; +} = { + padding: 50, + aspectRatio: "16:9", + cropRegion: DEFAULT_CROP_REGION, + wallpaper: DEFAULT_WALLPAPER, +}; + +export const DEFAULT_WEBCAM_SETTINGS = { + layoutPreset: DEFAULT_WEBCAM_LAYOUT_PRESET, + maskShape: DEFAULT_WEBCAM_MASK_SHAPE, + sizePreset: DEFAULT_WEBCAM_SIZE_PRESET, + position: DEFAULT_WEBCAM_POSITION, +} as const satisfies { + layoutPreset: WebcamLayoutPreset; + maskShape: WebcamMaskShape; + sizePreset: WebcamSizePreset; + position: WebcamPosition | null; +}; + +export const DEFAULT_CURSOR_SETTINGS: CursorVisualSettings & { show: boolean } = { + show: true, + size: DEFAULT_CURSOR_SIZE, + smoothing: DEFAULT_CURSOR_SMOOTHING, + motionBlur: DEFAULT_CURSOR_MOTION_BLUR, + clickBounce: DEFAULT_CURSOR_CLICK_BOUNCE, + clipToBounds: DEFAULT_CURSOR_CLIP_TO_BOUNDS, +}; + +export const DEFAULT_EXPORT_SETTINGS: { + quality: ExportQuality; + format: ExportFormat; +} = { + quality: "good", + format: "mp4", +}; + +export const DEFAULT_GIF_SETTINGS: { + frameRate: GifFrameRate; + loop: boolean; + sizePreset: GifSizePreset; + outputDimensions: typeof DEFAULT_GIF_OUTPUT_DIMENSIONS; +} = { + frameRate: 15, + loop: true, + sizePreset: "medium", + outputDimensions: DEFAULT_GIF_OUTPUT_DIMENSIONS, +}; diff --git a/src/components/video-editor/projectPersistence.ts b/src/components/video-editor/projectPersistence.ts index 7360142..f16d29e 100644 --- a/src/components/video-editor/projectPersistence.ts +++ b/src/components/video-editor/projectPersistence.ts @@ -4,6 +4,13 @@ import type { ProjectMedia } from "@/lib/recordingSession"; import { normalizeProjectMedia } from "@/lib/recordingSession"; import { DEFAULT_WALLPAPER, WALLPAPER_PATHS } from "@/lib/wallpaper"; import { ASPECT_RATIOS, type AspectRatio, isPortraitAspectRatio } from "@/utils/aspectRatioUtils"; +import { + DEFAULT_EDITOR_APPEARANCE_SETTINGS, + DEFAULT_EDITOR_LAYOUT_SETTINGS, + DEFAULT_EXPORT_SETTINGS, + DEFAULT_GIF_SETTINGS, + DEFAULT_WEBCAM_SETTINGS, +} from "./editorDefaults"; import { type AnnotationRegion, type CropRegion, @@ -15,14 +22,10 @@ import { DEFAULT_BLUR_DATA, DEFAULT_BLUR_FREEHAND_POINTS, DEFAULT_BLUR_INTENSITY, - DEFAULT_CROP_REGION, DEFAULT_FIGURE_DATA, DEFAULT_PLAYBACK_SPEED, - DEFAULT_WEBCAM_LAYOUT_PRESET, - DEFAULT_WEBCAM_MASK_SHAPE, - DEFAULT_WEBCAM_POSITION, - DEFAULT_WEBCAM_SIZE_PRESET, DEFAULT_ZOOM_DEPTH, + DEFAULT_ZOOM_MOTION_BLUR, MAX_BLUR_BLOCK_SIZE, MAX_BLUR_INTENSITY, MAX_PLAYBACK_SPEED, @@ -104,13 +107,13 @@ function computeNormalizedWebcamLayoutPreset( case "vertical-stack": return isPortraitAspectRatio(normalizedAspectRatio) ? webcamLayoutPreset - : DEFAULT_WEBCAM_LAYOUT_PRESET; + : DEFAULT_WEBCAM_SETTINGS.layoutPreset; case "dual-frame": return isPortraitAspectRatio(normalizedAspectRatio) - ? DEFAULT_WEBCAM_LAYOUT_PRESET + ? DEFAULT_WEBCAM_SETTINGS.layoutPreset : webcamLayoutPreset; default: - return DEFAULT_WEBCAM_LAYOUT_PRESET; + return DEFAULT_WEBCAM_SETTINGS.layoutPreset; } } @@ -211,7 +214,7 @@ export function normalizeProjectEditor(editor: Partial): Pro editor.aspectRatio as AspectRatio, ) ? (editor.aspectRatio as AspectRatio) - : "16:9"; + : DEFAULT_EDITOR_LAYOUT_SETTINGS.aspectRatio; const normalizedWebcamLayoutPreset = computeNormalizedWebcamLayoutPreset( editor.webcamLayoutPreset, normalizedAspectRatio, @@ -226,7 +229,7 @@ export function normalizeProjectEditor(editor: Partial): Pro cx: clamp((editor.webcamPosition as WebcamPosition).cx, 0, 1), cy: clamp((editor.webcamPosition as WebcamPosition).cy, 0, 1), } - : DEFAULT_WEBCAM_POSITION; + : DEFAULT_WEBCAM_SETTINGS.position; const normalizedZoomRegions: ZoomRegion[] = Array.isArray(editor.zoomRegions) ? editor.zoomRegions @@ -413,16 +416,16 @@ export function normalizeProjectEditor(editor: Partial): Pro const rawCropX = isFiniteNumber(editor.cropRegion?.x) ? editor.cropRegion.x - : DEFAULT_CROP_REGION.x; + : DEFAULT_EDITOR_LAYOUT_SETTINGS.cropRegion.x; const rawCropY = isFiniteNumber(editor.cropRegion?.y) ? editor.cropRegion.y - : DEFAULT_CROP_REGION.y; + : DEFAULT_EDITOR_LAYOUT_SETTINGS.cropRegion.y; const rawCropWidth = isFiniteNumber(editor.cropRegion?.width) ? editor.cropRegion.width - : DEFAULT_CROP_REGION.width; + : DEFAULT_EDITOR_LAYOUT_SETTINGS.cropRegion.width; const rawCropHeight = isFiniteNumber(editor.cropRegion?.height) ? editor.cropRegion.height - : DEFAULT_CROP_REGION.height; + : DEFAULT_EDITOR_LAYOUT_SETTINGS.cropRegion.height; const cropX = clamp(rawCropX, 0, 1); const cropY = clamp(rawCropY, 0, 1); @@ -433,18 +436,29 @@ export function normalizeProjectEditor(editor: Partial): Pro wallpaper: typeof editor.wallpaper === "string" ? normalizeWallpaperValue(editor.wallpaper) - : DEFAULT_WALLPAPER, - shadowIntensity: typeof editor.shadowIntensity === "number" ? editor.shadowIntensity : 0, - showBlur: typeof editor.showBlur === "boolean" ? editor.showBlur : false, + : DEFAULT_EDITOR_LAYOUT_SETTINGS.wallpaper, + shadowIntensity: + typeof editor.shadowIntensity === "number" + ? editor.shadowIntensity + : DEFAULT_EDITOR_APPEARANCE_SETTINGS.shadowIntensity, + showBlur: + typeof editor.showBlur === "boolean" + ? editor.showBlur + : DEFAULT_EDITOR_APPEARANCE_SETTINGS.showBlur, motionBlurAmount: isFiniteNumber(editor.motionBlurAmount) ? clamp(editor.motionBlurAmount, 0, 1) : typeof (editor as { motionBlurEnabled?: unknown }).motionBlurEnabled === "boolean" ? (editor as { motionBlurEnabled?: boolean }).motionBlurEnabled - ? 0.35 - : 0 - : 0, - borderRadius: typeof editor.borderRadius === "number" ? editor.borderRadius : 0, - padding: isFiniteNumber(editor.padding) ? clamp(editor.padding, 0, 100) : 50, + ? DEFAULT_ZOOM_MOTION_BLUR + : DEFAULT_EDITOR_APPEARANCE_SETTINGS.motionBlurAmount + : DEFAULT_EDITOR_APPEARANCE_SETTINGS.motionBlurAmount, + borderRadius: + typeof editor.borderRadius === "number" + ? editor.borderRadius + : DEFAULT_EDITOR_APPEARANCE_SETTINGS.borderRadius, + padding: isFiniteNumber(editor.padding) + ? clamp(editor.padding, 0, 100) + : DEFAULT_EDITOR_LAYOUT_SETTINGS.padding, cropRegion: { x: cropX, y: cropY, @@ -463,31 +477,31 @@ export function normalizeProjectEditor(editor: Partial): Pro editor.webcamMaskShape === "square" || editor.webcamMaskShape === "rounded" ? editor.webcamMaskShape - : DEFAULT_WEBCAM_MASK_SHAPE, + : DEFAULT_WEBCAM_SETTINGS.maskShape, webcamSizePreset: typeof editor.webcamSizePreset === "number" && isFiniteNumber(editor.webcamSizePreset) ? Math.max(10, Math.min(50, editor.webcamSizePreset)) - : DEFAULT_WEBCAM_SIZE_PRESET, + : DEFAULT_WEBCAM_SETTINGS.sizePreset, webcamPosition: normalizedWebcamPosition, exportQuality: editor.exportQuality === "medium" || editor.exportQuality === "source" ? editor.exportQuality - : "good", - exportFormat: editor.exportFormat === "gif" ? "gif" : "mp4", + : DEFAULT_EXPORT_SETTINGS.quality, + exportFormat: editor.exportFormat === "gif" ? "gif" : DEFAULT_EXPORT_SETTINGS.format, gifFrameRate: editor.gifFrameRate === 15 || editor.gifFrameRate === 20 || editor.gifFrameRate === 25 || editor.gifFrameRate === 30 ? editor.gifFrameRate - : 15, - gifLoop: typeof editor.gifLoop === "boolean" ? editor.gifLoop : true, + : DEFAULT_GIF_SETTINGS.frameRate, + gifLoop: typeof editor.gifLoop === "boolean" ? editor.gifLoop : DEFAULT_GIF_SETTINGS.loop, gifSizePreset: editor.gifSizePreset === "medium" || editor.gifSizePreset === "large" || editor.gifSizePreset === "original" ? editor.gifSizePreset - : "medium", + : DEFAULT_GIF_SETTINGS.sizePreset, }; } diff --git a/src/components/video-editor/types.ts b/src/components/video-editor/types.ts index cf426d0..fce4a19 100644 --- a/src/components/video-editor/types.ts +++ b/src/components/video-editor/types.ts @@ -188,6 +188,7 @@ export interface CursorVisualSettings { smoothing: number; motionBlur: number; clickBounce: number; + clipToBounds: boolean; } export const DEFAULT_CURSOR_SIZE = 3.0; diff --git a/src/hooks/useEditorHistory.ts b/src/hooks/useEditorHistory.ts index bd410da..b6525c1 100644 --- a/src/hooks/useEditorHistory.ts +++ b/src/hooks/useEditorHistory.ts @@ -1,4 +1,9 @@ import { useCallback, useRef, useState } from "react"; +import { + DEFAULT_EDITOR_APPEARANCE_SETTINGS, + DEFAULT_EDITOR_LAYOUT_SETTINGS, + DEFAULT_WEBCAM_SETTINGS, +} from "@/components/video-editor/editorDefaults"; import type { AnnotationRegion, CropRegion, @@ -10,14 +15,7 @@ import type { WebcamSizePreset, ZoomRegion, } from "@/components/video-editor/types"; -import { - DEFAULT_CROP_REGION, - DEFAULT_WEBCAM_LAYOUT_PRESET, - DEFAULT_WEBCAM_MASK_SHAPE, - DEFAULT_WEBCAM_POSITION, - DEFAULT_WEBCAM_SIZE_PRESET, -} from "@/components/video-editor/types"; -import { DEFAULT_WALLPAPER } from "@/lib/wallpaper"; +import { DEFAULT_CROP_REGION } from "@/components/video-editor/types"; import type { AspectRatio } from "@/utils/aspectRatioUtils"; // Undoable state — selection IDs are intentionally excluded (undoing a @@ -47,17 +45,17 @@ export const INITIAL_EDITOR_STATE: EditorState = { speedRegions: [], annotationRegions: [], cropRegion: DEFAULT_CROP_REGION, - wallpaper: DEFAULT_WALLPAPER, - shadowIntensity: 0, - showBlur: false, - motionBlurAmount: 0, - borderRadius: 0, - padding: 50, - aspectRatio: "16:9", - webcamLayoutPreset: DEFAULT_WEBCAM_LAYOUT_PRESET, - webcamMaskShape: DEFAULT_WEBCAM_MASK_SHAPE, - webcamSizePreset: DEFAULT_WEBCAM_SIZE_PRESET, - webcamPosition: DEFAULT_WEBCAM_POSITION, + wallpaper: DEFAULT_EDITOR_LAYOUT_SETTINGS.wallpaper, + shadowIntensity: DEFAULT_EDITOR_APPEARANCE_SETTINGS.shadowIntensity, + showBlur: DEFAULT_EDITOR_APPEARANCE_SETTINGS.showBlur, + motionBlurAmount: DEFAULT_EDITOR_APPEARANCE_SETTINGS.motionBlurAmount, + borderRadius: DEFAULT_EDITOR_APPEARANCE_SETTINGS.borderRadius, + padding: DEFAULT_EDITOR_LAYOUT_SETTINGS.padding, + aspectRatio: DEFAULT_EDITOR_LAYOUT_SETTINGS.aspectRatio, + webcamLayoutPreset: DEFAULT_WEBCAM_SETTINGS.layoutPreset, + webcamMaskShape: DEFAULT_WEBCAM_SETTINGS.maskShape, + webcamSizePreset: DEFAULT_WEBCAM_SETTINGS.sizePreset, + webcamPosition: DEFAULT_WEBCAM_SETTINGS.position, }; type StateUpdate = Partial | ((prev: EditorState) => Partial); diff --git a/src/hooks/useScreenRecorder.ts b/src/hooks/useScreenRecorder.ts index 08d062c..4ad11c6 100644 --- a/src/hooks/useScreenRecorder.ts +++ b/src/hooks/useScreenRecorder.ts @@ -989,8 +989,8 @@ export function useScreenRecorder(): UseScreenRecorderReturn { enabled: webcamEnabled, deviceId: webcamDeviceId, deviceName: webcamDeviceName, - width: WEBCAM_TARGET_WIDTH, - height: WEBCAM_TARGET_HEIGHT, + width: 0, + height: 0, fps: WEBCAM_TARGET_FRAME_RATE, }, cursor: { diff --git a/src/lib/userPreferences.ts b/src/lib/userPreferences.ts index 5b7bc86..28a4506 100644 --- a/src/lib/userPreferences.ts +++ b/src/lib/userPreferences.ts @@ -1,3 +1,7 @@ +import { + DEFAULT_EDITOR_LAYOUT_SETTINGS, + DEFAULT_EXPORT_SETTINGS, +} from "@/components/video-editor/editorDefaults"; import type { ExportFormat, ExportQuality } from "@/lib/exporter"; import type { AspectRatio } from "@/utils/aspectRatioUtils"; @@ -27,11 +31,11 @@ export interface UserPreferences { exportFolder: string | null; } -const DEFAULT_PREFS: UserPreferences = { - padding: 50, - aspectRatio: "16:9", - exportQuality: "good", - exportFormat: "mp4", +export const DEFAULT_PREFS: UserPreferences = { + padding: DEFAULT_EDITOR_LAYOUT_SETTINGS.padding, + aspectRatio: DEFAULT_EDITOR_LAYOUT_SETTINGS.aspectRatio, + exportQuality: DEFAULT_EXPORT_SETTINGS.quality, + exportFormat: DEFAULT_EXPORT_SETTINGS.format, exportFolder: null, };