From 7f00d95681fcea56b97d14c934a680cd5d23b401 Mon Sep 17 00:00:00 2001 From: webadderall <131426131+webadderall@users.noreply.github.com> Date: Tue, 5 May 2026 17:29:17 +1000 Subject: [PATCH] fix presets and motion blur, add dev sliders --- src/components/video-editor/SettingsPanel.tsx | 152 ++++++++++++++++-- src/components/video-editor/VideoEditor.tsx | 65 +++++++- .../video-editor/cursorMotionPresets.ts | 4 +- .../video-editor/projectPersistence.ts | 40 ++++- src/lib/exporter/modernFrameRenderer.ts | 10 -- 5 files changed, 241 insertions(+), 30 deletions(-) diff --git a/src/components/video-editor/SettingsPanel.tsx b/src/components/video-editor/SettingsPanel.tsx index c857f942..478bc261 100644 --- a/src/components/video-editor/SettingsPanel.tsx +++ b/src/components/video-editor/SettingsPanel.tsx @@ -29,6 +29,10 @@ import { import { TEMPORAL_MOTION_BLUR_DEFAULT_SAMPLE_COUNT, TEMPORAL_MOTION_BLUR_DEFAULT_SHUTTER_FRACTION, + TEMPORAL_MOTION_BLUR_MAX_SAMPLE_COUNT, + TEMPORAL_MOTION_BLUR_MAX_SHUTTER_FRACTION, + TEMPORAL_MOTION_BLUR_MIN_SAMPLE_COUNT, + TEMPORAL_MOTION_BLUR_MIN_SHUTTER_FRACTION, } from "@/lib/exporter/temporalMotionBlur"; import type { ExtensionSettingField } from "@/lib/extensions"; import { extensionHost, type FrameInstance } from "@/lib/extensions"; @@ -85,7 +89,6 @@ import { DEFAULT_WEBCAM_SHADOW, DEFAULT_WEBCAM_SIZE, DEFAULT_ZOOM_IN_DURATION_MS, - DEFAULT_ZOOM_SMOOTHNESS, DEFAULT_ZOOM_OUT_DURATION_MS, } from "./types"; import { fromCursorSwaySliderValue, toCursorSwaySliderValue } from "./videoPlayback/cursorSway"; @@ -470,6 +473,7 @@ interface SettingsPanelProps { onShadowChange?: (intensity: number) => void; backgroundBlur?: number; onBackgroundBlurChange?: (amount: number) => void; + zoomTemporalMotionBlur?: number; onZoomTemporalMotionBlurChange?: (amount: number) => void; zoomMotionBlurSampleCount?: number | null; onZoomMotionBlurSampleCountChange?: (count: number | null) => void; @@ -849,8 +853,11 @@ export function SettingsPanel({ onShadowChange, backgroundBlur = 0, onBackgroundBlurChange, + zoomTemporalMotionBlur = 0, onZoomTemporalMotionBlurChange, + zoomMotionBlurSampleCount = TEMPORAL_MOTION_BLUR_DEFAULT_SAMPLE_COUNT, onZoomMotionBlurSampleCountChange, + zoomMotionBlurShutterFraction = TEMPORAL_MOTION_BLUR_DEFAULT_SHUTTER_FRACTION, onZoomMotionBlurShutterFractionChange, connectZooms = true, onConnectZoomsChange, @@ -1124,6 +1131,7 @@ export function SettingsPanel({ () => ({ ...builtInCursorPreviewUrls, ...extensionCursorPreviewUrls }), [builtInCursorPreviewUrls, extensionCursorPreviewUrls], ); + const showDevMotionControls = import.meta.env.DEV; const cursorStyleOptions = useMemo( () => [ ...BUILTIN_CURSOR_STYLE_OPTIONS, @@ -1448,9 +1456,9 @@ export function SettingsPanel({ const resetZoomSection = () => { onZoomTemporalMotionBlurChange?.(initialEditorPreferences.zoomTemporalMotionBlur); - onZoomMotionBlurSampleCountChange?.(TEMPORAL_MOTION_BLUR_DEFAULT_SAMPLE_COUNT); + onZoomMotionBlurSampleCountChange?.(initialEditorPreferences.zoomMotionBlurSampleCount); onZoomMotionBlurShutterFractionChange?.( - TEMPORAL_MOTION_BLUR_DEFAULT_SHUTTER_FRACTION, + initialEditorPreferences.zoomMotionBlurShutterFraction, ); onZoomInDurationMsChange?.(initialEditorPreferences.zoomInDurationMs); onZoomOutDurationMsChange?.(initialEditorPreferences.zoomOutDurationMs); @@ -2690,14 +2698,79 @@ export function SettingsPanel({ )} )} -
-
- {tSettings("effects.exportBlurLocked", "Export blur is fixed for this build.")} + {showDevMotionControls ? ( +
+
+
+ {tSettings("effects.exportBlurDebug", "Export Blur Debug")} +
+
+ {tSettings( + "effects.exportBlurDebugHint", + "Development-only temporal blur tuning for export and preview parity checks.", + )} +
+
+ onZoomTemporalMotionBlurChange?.(value)} + formatValue={(value) => `${value.toFixed(2)}×`} + parseInput={(text) => parseFloat(text.replace(/×$/, ""))} + /> + + onZoomMotionBlurSampleCountChange?.(Math.round(value)) + } + formatValue={(value) => `${Math.round(value)} samples`} + parseInput={(text) => parseFloat(text.replace(/samples?$/i, "").trim())} + /> + onZoomMotionBlurShutterFractionChange?.(value)} + formatValue={(value) => `${Math.round(value * 100)}%`} + parseInput={(text) => parseFloat(text.replace(/%$/, "")) / 100} + />
-
- {`${TEMPORAL_MOTION_BLUR_DEFAULT_SAMPLE_COUNT} samples · ${Math.round(TEMPORAL_MOTION_BLUR_DEFAULT_SHUTTER_FRACTION * 100)}% shutter`} + ) : ( +
+
+ {tSettings( + "effects.exportBlurLocked", + "Export blur is fixed for this build.", + )} +
+
+ {`${TEMPORAL_MOTION_BLUR_DEFAULT_SAMPLE_COUNT} samples · ${Math.round(TEMPORAL_MOTION_BLUR_DEFAULT_SHUTTER_FRACTION * 100)}% shutter`} +
-
+ )} {selectedZoomId && (
{renderExtensionPanelsForSections("cursor")} diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx index e534d1fa..6ff79072 100644 --- a/src/components/video-editor/VideoEditor.tsx +++ b/src/components/video-editor/VideoEditor.tsx @@ -597,6 +597,15 @@ export default function VideoEditor() { ); const [backgroundBlur, setBackgroundBlur] = useState(initialEditorPreferences.backgroundBlur); const [zoomMotionBlur, setZoomMotionBlur] = useState(initialEditorPreferences.zoomMotionBlur); + const [zoomTemporalMotionBlur, setZoomTemporalMotionBlur] = useState( + initialEditorPreferences.zoomTemporalMotionBlur, + ); + const [zoomMotionBlurSampleCount, setZoomMotionBlurSampleCount] = useState( + initialEditorPreferences.zoomMotionBlurSampleCount, + ); + const [zoomMotionBlurShutterFraction, setZoomMotionBlurShutterFraction] = useState< + number | null + >(initialEditorPreferences.zoomMotionBlurShutterFraction); const [autoApplyFreshRecordingAutoZooms, setAutoApplyFreshRecordingAutoZooms] = useState( initialEditorPreferences.autoApplyFreshRecordingAutoZooms, ); @@ -829,10 +838,9 @@ export default function VideoEditor() { shadowIntensity, backgroundBlur, zoomMotionBlur, - zoomTemporalMotionBlur: zoomMotionBlur, - zoomMotionBlurSampleCount: initialEditorPreferences.zoomMotionBlurSampleCount, - zoomMotionBlurShutterFraction: - initialEditorPreferences.zoomMotionBlurShutterFraction, + zoomTemporalMotionBlur, + zoomMotionBlurSampleCount, + zoomMotionBlurShutterFraction, connectZooms, zoomInDurationMs, zoomInOverlapMs, @@ -877,8 +885,9 @@ export default function VideoEditor() { shadowIntensity, backgroundBlur, zoomMotionBlur, - initialEditorPreferences.zoomMotionBlurSampleCount, - initialEditorPreferences.zoomMotionBlurShutterFraction, + zoomTemporalMotionBlur, + zoomMotionBlurSampleCount, + zoomMotionBlurShutterFraction, connectZooms, zoomInDurationMs, zoomInOverlapMs, @@ -964,6 +973,9 @@ export default function VideoEditor() { setShadowIntensity(snapshot.shadowIntensity); setBackgroundBlur(snapshot.backgroundBlur); setZoomMotionBlur(snapshot.zoomMotionBlur); + setZoomTemporalMotionBlur(snapshot.zoomTemporalMotionBlur); + setZoomMotionBlurSampleCount(snapshot.zoomMotionBlurSampleCount); + setZoomMotionBlurShutterFraction(snapshot.zoomMotionBlurShutterFraction); setConnectZooms(snapshot.connectZooms); setZoomInDurationMs(snapshot.zoomInDurationMs); setZoomInOverlapMs(snapshot.zoomInOverlapMs); @@ -1192,6 +1204,9 @@ export default function VideoEditor() { shadowIntensity, backgroundBlur, zoomMotionBlur, + zoomTemporalMotionBlur, + zoomMotionBlurSampleCount, + zoomMotionBlurShutterFraction, connectZooms, zoomInDurationMs, zoomInOverlapMs, @@ -1338,6 +1353,9 @@ export default function VideoEditor() { zoomInEasing, zoomInOverlapMs, zoomMotionBlur, + zoomTemporalMotionBlur, + zoomMotionBlurSampleCount, + zoomMotionBlurShutterFraction, zoomOutDurationMs, zoomOutEasing, zoomRegions, @@ -1640,6 +1658,9 @@ export default function VideoEditor() { shadowIntensity: number; backgroundBlur: number; zoomMotionBlur: number; + zoomTemporalMotionBlur: number; + zoomMotionBlurSampleCount: number | null; + zoomMotionBlurShutterFraction: number | null; connectZooms: boolean; zoomInDurationMs: number; zoomInOverlapMs: number; @@ -1791,6 +1812,9 @@ export default function VideoEditor() { shadowIntensity, backgroundBlur, zoomMotionBlur, + zoomTemporalMotionBlur, + zoomMotionBlurSampleCount, + zoomMotionBlurShutterFraction, connectZooms, zoomInDurationMs, zoomInOverlapMs, @@ -1844,6 +1868,9 @@ export default function VideoEditor() { shadowIntensity, backgroundBlur, zoomMotionBlur, + zoomTemporalMotionBlur, + zoomMotionBlurSampleCount, + zoomMotionBlurShutterFraction, connectZooms, zoomInDurationMs, zoomInOverlapMs, @@ -2025,6 +2052,9 @@ export default function VideoEditor() { setShadowIntensity(normalizedEditor.shadowIntensity); setBackgroundBlur(normalizedEditor.backgroundBlur); setZoomMotionBlur(normalizedEditor.zoomMotionBlur); + setZoomTemporalMotionBlur(normalizedEditor.zoomTemporalMotionBlur); + setZoomMotionBlurSampleCount(normalizedEditor.zoomMotionBlurSampleCount); + setZoomMotionBlurShutterFraction(normalizedEditor.zoomMotionBlurShutterFraction); setConnectZooms(normalizedEditor.connectZooms); setZoomInDurationMs(normalizedEditor.zoomInDurationMs); setZoomInOverlapMs(normalizedEditor.zoomInOverlapMs); @@ -2431,6 +2461,9 @@ export default function VideoEditor() { shadowIntensity, backgroundBlur, zoomMotionBlur, + zoomTemporalMotionBlur, + zoomMotionBlurSampleCount, + zoomMotionBlurShutterFraction, autoApplyFreshRecordingAutoZooms, connectZooms, zoomInDurationMs, @@ -2475,6 +2508,9 @@ export default function VideoEditor() { shadowIntensity, backgroundBlur, zoomMotionBlur, + zoomTemporalMotionBlur, + zoomMotionBlurSampleCount, + zoomMotionBlurShutterFraction, autoApplyFreshRecordingAutoZooms, connectZooms, zoomInDurationMs, @@ -4313,6 +4349,9 @@ export default function VideoEditor() { shadowIntensity: effectiveShadowIntensity, backgroundBlur, zoomMotionBlur, + zoomTemporalMotionBlur, + zoomMotionBlurSampleCount, + zoomMotionBlurShutterFraction, connectZooms, zoomInDurationMs, zoomInOverlapMs, @@ -4489,6 +4528,9 @@ export default function VideoEditor() { shadowIntensity: effectiveShadowIntensity, backgroundBlur, zoomMotionBlur, + zoomTemporalMotionBlur, + zoomMotionBlurSampleCount, + zoomMotionBlurShutterFraction, connectZooms, zoomInDurationMs, zoomInOverlapMs, @@ -4740,6 +4782,9 @@ export default function VideoEditor() { shadowIntensity, backgroundBlur, zoomMotionBlur, + zoomTemporalMotionBlur, + zoomMotionBlurSampleCount, + zoomMotionBlurShutterFraction, connectZooms, zoomInDurationMs, zoomInOverlapMs, @@ -5721,6 +5766,14 @@ export default function VideoEditor() { onShadowChange={setShadowIntensity} backgroundBlur={backgroundBlur} onBackgroundBlurChange={setBackgroundBlur} + zoomTemporalMotionBlur={zoomTemporalMotionBlur} + onZoomTemporalMotionBlurChange={setZoomTemporalMotionBlur} + zoomMotionBlurSampleCount={zoomMotionBlurSampleCount} + onZoomMotionBlurSampleCountChange={setZoomMotionBlurSampleCount} + zoomMotionBlurShutterFraction={zoomMotionBlurShutterFraction} + onZoomMotionBlurShutterFractionChange={ + setZoomMotionBlurShutterFraction + } autoApplyFreshRecordingAutoZooms={autoApplyFreshRecordingAutoZooms} onAutoApplyFreshRecordingAutoZoomsChange={ setAutoApplyFreshRecordingAutoZooms diff --git a/src/components/video-editor/cursorMotionPresets.ts b/src/components/video-editor/cursorMotionPresets.ts index 8e06809b..f38079ae 100644 --- a/src/components/video-editor/cursorMotionPresets.ts +++ b/src/components/video-editor/cursorMotionPresets.ts @@ -32,8 +32,8 @@ export const CURSOR_MOTION_PRESETS: Record): ProjectEditorState { + const normalizeTemporalBlurSampleCount = (value: unknown): number => { + if (!isFiniteNumber(value)) { + return TEMPORAL_MOTION_BLUR_DEFAULT_SAMPLE_COUNT; + } + + const roundedValue = Math.round(value); + const clampedValue = clamp( + roundedValue, + TEMPORAL_MOTION_BLUR_MIN_SAMPLE_COUNT, + TEMPORAL_MOTION_BLUR_MAX_SAMPLE_COUNT, + ); + + if (clampedValue % 2 === 1) { + return clampedValue; + } + + return clampedValue >= TEMPORAL_MOTION_BLUR_MAX_SAMPLE_COUNT + ? clampedValue - 1 + : clampedValue + 1; + }; + const validAspectRatios = new Set(ASPECT_RATIOS); const legacyMotionBlurEnabled = (editor as Partial<{ motionBlurEnabled: boolean }>) .motionBlurEnabled; @@ -323,9 +348,18 @@ export function normalizeProjectEditor(editor: Partial): Pro : legacyShowBlur ? 2 : 0; - const normalizedZoomMotionBlurSampleCount = TEMPORAL_MOTION_BLUR_DEFAULT_SAMPLE_COUNT; - const normalizedZoomMotionBlurShutterFraction = - TEMPORAL_MOTION_BLUR_DEFAULT_SHUTTER_FRACTION; + const normalizedZoomMotionBlurSampleCount = normalizeTemporalBlurSampleCount( + (editor as Partial).zoomMotionBlurSampleCount, + ); + const normalizedZoomMotionBlurShutterFraction = isFiniteNumber( + (editor as Partial).zoomMotionBlurShutterFraction, + ) + ? clamp( + (editor as Partial).zoomMotionBlurShutterFraction as number, + TEMPORAL_MOTION_BLUR_MIN_SHUTTER_FRACTION, + TEMPORAL_MOTION_BLUR_MAX_SHUTTER_FRACTION, + ) + : TEMPORAL_MOTION_BLUR_DEFAULT_SHUTTER_FRACTION; const normalizedZoomInDurationMs = isFiniteNumber(editor.zoomInDurationMs) ? clamp(editor.zoomInDurationMs, 60, 4000) : DEFAULT_MOTION_PRESET.zoomInDurationMs; diff --git a/src/lib/exporter/modernFrameRenderer.ts b/src/lib/exporter/modernFrameRenderer.ts index 570409be..161d26bc 100644 --- a/src/lib/exporter/modernFrameRenderer.ts +++ b/src/lib/exporter/modernFrameRenderer.ts @@ -2638,16 +2638,6 @@ export class FrameRenderer { return null; } - if (resolvedSnapshot !== lastSnapshot) { - await this.renderSceneSample( - Math.round(resolvedSnapshot.timeMs * 1000), - Math.round(resolvedSnapshot.cursorTimeMs * 1000), - Math.round(resolvedSnapshot.backgroundTimelineTimeMs * 1000), - layoutCache, - true, - false, - ); - } this.updateCaptionLayer(resolvedSnapshot.timeMs); const hasOverlayCanvasWork =