From 989a0813f9bc5d14b17796f653634fe2b2edb71f Mon Sep 17 00:00:00 2001 From: webadderall <131426131+webadderall@users.noreply.github.com> Date: Tue, 15 Sep 2026 11:20:16 +1000 Subject: [PATCH] Fix gap annotation compositing and late audio sync; retire temporal blur --- .../audio/useAudioPreviewSync.test.ts | 10 + .../video-editor/audio/useAudioPreviewSync.ts | 1 + .../video-editor/editorPreferences.ts | 9 - .../export/buildExportRenderOptions.ts | 3 - .../useEditorPreferencesPersistence.ts | 6 - .../presets/useVideoEditorPresets.ts | 6 - .../project/useProjectLibraryController.ts | 9 - .../project/useProjectLifecycle.ts | 3 - .../project/useProjectSnapshotModel.ts | 6 - .../video-editor/projectPersistence.test.ts | 13 ++ .../video-editor/projectPersistence.ts | 52 ----- .../video-editor/state/useAppearanceState.ts | 15 -- src/i18n/locales/de/settings.json | 4 - src/i18n/locales/en/settings.json | 4 - src/i18n/locales/es/settings.json | 4 - src/i18n/locales/fr/settings.json | 4 - src/i18n/locales/it/settings.json | 4 - src/i18n/locales/ko/settings.json | 4 - src/i18n/locales/nl/settings.json | 4 - src/i18n/locales/pt-BR/settings.json | 4 - src/i18n/locales/ru/settings.json | 4 - src/i18n/locales/zh-CN/settings.json | 4 - src/i18n/locales/zh-TW/settings.json | 4 - src/lib/exporter/frameRenderer.ts | 215 +----------------- src/lib/exporter/gifExporter.ts | 6 - src/lib/exporter/modernFrameRenderer.test.ts | 35 +-- src/lib/exporter/modernFrameRenderer.ts | 7 +- src/lib/exporter/modernVideoExporter.ts | 6 - src/lib/exporter/temporalMotionBlur.test.ts | 78 ------- src/lib/exporter/temporalMotionBlur.ts | 134 ----------- src/lib/exporter/videoExporter.ts | 6 - 31 files changed, 46 insertions(+), 618 deletions(-) delete mode 100644 src/lib/exporter/temporalMotionBlur.test.ts delete mode 100644 src/lib/exporter/temporalMotionBlur.ts diff --git a/src/components/video-editor/audio/useAudioPreviewSync.test.ts b/src/components/video-editor/audio/useAudioPreviewSync.test.ts index 9b03c595..46e8df34 100644 --- a/src/components/video-editor/audio/useAudioPreviewSync.test.ts +++ b/src/components/video-editor/audio/useAudioPreviewSync.test.ts @@ -35,6 +35,15 @@ afterEach(() => { describe("source preview playback ownership", () => { it.each([ + { + name: "sub-second offset", + muted: false, + playing: true, + rate: 1, + time: 0.5, + delay: 0, + plays: true, + }, { name: "playing clip", muted: false, @@ -145,5 +154,6 @@ describe("source preview playback ownership", () => { harness.effects.at(-1)?.(); await Promise.resolve(); expect(audio.play).toHaveBeenCalledTimes(plays ? 1 : 0); + if (plays) expect(audio.currentTime).toBeCloseTo(time - delay / 1000); }); }); diff --git a/src/components/video-editor/audio/useAudioPreviewSync.ts b/src/components/video-editor/audio/useAudioPreviewSync.ts index af64230b..2b3743c5 100644 --- a/src/components/video-editor/audio/useAudioPreviewSync.ts +++ b/src/components/video-editor/audio/useAudioPreviewSync.ts @@ -239,6 +239,7 @@ export function useAudioPreviewSync({ sourceAudioResourceVersion, ); latestAudio.load(); + lastSourceAudioSyncTimeRef.current = null; setSourceLoadVersion((version) => version + 1); } catch (error) { const latestAudio = existing.get(audioPath); diff --git a/src/components/video-editor/editorPreferences.ts b/src/components/video-editor/editorPreferences.ts index 6fee386f..bca8b266 100644 --- a/src/components/video-editor/editorPreferences.ts +++ b/src/components/video-editor/editorPreferences.ts @@ -17,9 +17,6 @@ type PersistedEditorControls = Pick< | "backgroundBlur" | "zoomMotionBlur" | "zoomMotionBlurTuning" - | "zoomTemporalMotionBlur" - | "zoomMotionBlurSampleCount" - | "zoomMotionBlurShutterFraction" | "connectZooms" | "zoomInDurationMs" | "zoomInOverlapMs" @@ -108,9 +105,6 @@ export const DEFAULT_EDITOR_PREFERENCES: EditorPreferences = { backgroundBlur: DEFAULT_EDITOR_CONTROLS.backgroundBlur, zoomMotionBlur: DEFAULT_EDITOR_CONTROLS.zoomMotionBlur, zoomMotionBlurTuning: DEFAULT_EDITOR_CONTROLS.zoomMotionBlurTuning, - zoomTemporalMotionBlur: DEFAULT_EDITOR_CONTROLS.zoomTemporalMotionBlur, - zoomMotionBlurSampleCount: DEFAULT_EDITOR_CONTROLS.zoomMotionBlurSampleCount, - zoomMotionBlurShutterFraction: DEFAULT_EDITOR_CONTROLS.zoomMotionBlurShutterFraction, connectZooms: DEFAULT_EDITOR_CONTROLS.connectZooms, zoomInDurationMs: DEFAULT_EDITOR_CONTROLS.zoomInDurationMs, zoomInOverlapMs: DEFAULT_EDITOR_CONTROLS.zoomInOverlapMs, @@ -378,9 +372,6 @@ function normalizeEditorControls( backgroundBlur: normalized.backgroundBlur, zoomMotionBlur: normalized.zoomMotionBlur, zoomMotionBlurTuning: normalized.zoomMotionBlurTuning, - zoomTemporalMotionBlur: normalized.zoomTemporalMotionBlur, - zoomMotionBlurSampleCount: normalized.zoomMotionBlurSampleCount, - zoomMotionBlurShutterFraction: normalized.zoomMotionBlurShutterFraction, connectZooms: normalized.connectZooms, zoomInDurationMs: normalized.zoomInDurationMs, zoomInOverlapMs: normalized.zoomInOverlapMs, diff --git a/src/components/video-editor/export/buildExportRenderOptions.ts b/src/components/video-editor/export/buildExportRenderOptions.ts index 0f240e36..ae836c5a 100644 --- a/src/components/video-editor/export/buildExportRenderOptions.ts +++ b/src/components/video-editor/export/buildExportRenderOptions.ts @@ -42,9 +42,6 @@ export function buildExportRenderOptions({ backgroundBlur: appearance.backgroundBlur, zoomMotionBlur: appearance.zoomMotionBlur, zoomMotionBlurTuning: appearance.zoomMotionBlurTuning, - zoomTemporalMotionBlur: appearance.zoomTemporalMotionBlur, - zoomMotionBlurSampleCount: appearance.zoomMotionBlurSampleCount, - zoomMotionBlurShutterFraction: appearance.zoomMotionBlurShutterFraction, connectZooms: appearance.connectZooms, zoomInDurationMs: appearance.zoomInDurationMs, zoomInOverlapMs: appearance.zoomInOverlapMs, diff --git a/src/components/video-editor/presets/useEditorPreferencesPersistence.ts b/src/components/video-editor/presets/useEditorPreferencesPersistence.ts index 2d44dfa0..7691af80 100644 --- a/src/components/video-editor/presets/useEditorPreferencesPersistence.ts +++ b/src/components/video-editor/presets/useEditorPreferencesPersistence.ts @@ -26,9 +26,6 @@ export function useEditorPreferencesPersistence({ backgroundBlur: appearance.backgroundBlur, zoomMotionBlur: appearance.zoomMotionBlur, zoomMotionBlurTuning: appearance.zoomMotionBlurTuning, - zoomTemporalMotionBlur: appearance.zoomTemporalMotionBlur, - zoomMotionBlurSampleCount: appearance.zoomMotionBlurSampleCount, - zoomMotionBlurShutterFraction: appearance.zoomMotionBlurShutterFraction, autoApplyFreshRecordingAutoZooms: appearance.autoApplyFreshRecordingAutoZooms, connectZooms: appearance.connectZooms, zoomInDurationMs: appearance.zoomInDurationMs, @@ -81,9 +78,6 @@ export function useEditorPreferencesPersistence({ appearance.backgroundBlur, appearance.zoomMotionBlur, appearance.zoomMotionBlurTuning, - appearance.zoomTemporalMotionBlur, - appearance.zoomMotionBlurSampleCount, - appearance.zoomMotionBlurShutterFraction, appearance.autoApplyFreshRecordingAutoZooms, appearance.connectZooms, appearance.zoomInDurationMs, diff --git a/src/components/video-editor/presets/useVideoEditorPresets.ts b/src/components/video-editor/presets/useVideoEditorPresets.ts index 5474422a..a68560ee 100644 --- a/src/components/video-editor/presets/useVideoEditorPresets.ts +++ b/src/components/video-editor/presets/useVideoEditorPresets.ts @@ -43,9 +43,6 @@ export function useVideoEditorPresets({ backgroundBlur: appearance.backgroundBlur, zoomMotionBlur: appearance.zoomMotionBlur, zoomMotionBlurTuning: { ...appearance.zoomMotionBlurTuning }, - zoomTemporalMotionBlur: appearance.zoomTemporalMotionBlur, - zoomMotionBlurSampleCount: appearance.zoomMotionBlurSampleCount, - zoomMotionBlurShutterFraction: appearance.zoomMotionBlurShutterFraction, connectZooms: appearance.connectZooms, zoomInDurationMs: appearance.zoomInDurationMs, zoomInOverlapMs: appearance.zoomInOverlapMs, @@ -111,9 +108,6 @@ export function useVideoEditorPresets({ appearance.setBackgroundBlur(snapshot.backgroundBlur); appearance.setZoomMotionBlur(snapshot.zoomMotionBlur); appearance.setZoomMotionBlurTuning({ ...snapshot.zoomMotionBlurTuning }); - appearance.setZoomTemporalMotionBlur(snapshot.zoomTemporalMotionBlur); - appearance.setZoomMotionBlurSampleCount(snapshot.zoomMotionBlurSampleCount); - appearance.setZoomMotionBlurShutterFraction(snapshot.zoomMotionBlurShutterFraction); appearance.setConnectZooms(snapshot.connectZooms); appearance.setZoomInDurationMs(snapshot.zoomInDurationMs); appearance.setZoomInOverlapMs(snapshot.zoomInOverlapMs); diff --git a/src/components/video-editor/project/useProjectLibraryController.ts b/src/components/video-editor/project/useProjectLibraryController.ts index 0ae7493b..92a6578b 100644 --- a/src/components/video-editor/project/useProjectLibraryController.ts +++ b/src/components/video-editor/project/useProjectLibraryController.ts @@ -68,9 +68,6 @@ export function useProjectLibraryController({ zoomInOverlapMs, zoomMotionBlur, zoomMotionBlurTuning, - zoomTemporalMotionBlur, - zoomMotionBlurSampleCount, - zoomMotionBlurShutterFraction, zoomOutDurationMs, zoomOutEasing, zoomClassicMode, @@ -156,9 +153,6 @@ export function useProjectLibraryController({ backgroundBlur, zoomMotionBlur, zoomMotionBlurTuning, - zoomTemporalMotionBlur, - zoomMotionBlurSampleCount, - zoomMotionBlurShutterFraction, connectZooms, zoomInDurationMs, zoomInOverlapMs, @@ -327,9 +321,6 @@ export function useProjectLibraryController({ zoomInOverlapMs, zoomMotionBlur, zoomMotionBlurTuning, - zoomTemporalMotionBlur, - zoomMotionBlurSampleCount, - zoomMotionBlurShutterFraction, zoomOutDurationMs, zoomOutEasing, zoomRegions, diff --git a/src/components/video-editor/project/useProjectLifecycle.ts b/src/components/video-editor/project/useProjectLifecycle.ts index c2e0b3b5..1f0a0f32 100644 --- a/src/components/video-editor/project/useProjectLifecycle.ts +++ b/src/components/video-editor/project/useProjectLifecycle.ts @@ -131,9 +131,6 @@ export function useProjectLifecycle(input: Input) { appearance.setBackgroundBlur(editor.backgroundBlur); appearance.setZoomMotionBlur(editor.zoomMotionBlur); appearance.setZoomMotionBlurTuning({ ...editor.zoomMotionBlurTuning }); - appearance.setZoomTemporalMotionBlur(editor.zoomTemporalMotionBlur); - appearance.setZoomMotionBlurSampleCount(editor.zoomMotionBlurSampleCount); - appearance.setZoomMotionBlurShutterFraction(editor.zoomMotionBlurShutterFraction); appearance.setConnectZooms(editor.connectZooms); appearance.setZoomInDurationMs(editor.zoomInDurationMs); appearance.setZoomInOverlapMs(editor.zoomInOverlapMs); diff --git a/src/components/video-editor/project/useProjectSnapshotModel.ts b/src/components/video-editor/project/useProjectSnapshotModel.ts index 9031a54c..e138cad1 100644 --- a/src/components/video-editor/project/useProjectSnapshotModel.ts +++ b/src/components/video-editor/project/useProjectSnapshotModel.ts @@ -79,9 +79,6 @@ export function useProjectSnapshotModel({ backgroundBlur: appearance.backgroundBlur, zoomMotionBlur: appearance.zoomMotionBlur, zoomMotionBlurTuning: appearance.zoomMotionBlurTuning, - zoomTemporalMotionBlur: appearance.zoomTemporalMotionBlur, - zoomMotionBlurSampleCount: appearance.zoomMotionBlurSampleCount, - zoomMotionBlurShutterFraction: appearance.zoomMotionBlurShutterFraction, connectZooms: appearance.connectZooms, zoomInDurationMs: appearance.zoomInDurationMs, zoomInOverlapMs: appearance.zoomInOverlapMs, @@ -144,9 +141,6 @@ export function useProjectSnapshotModel({ appearance.backgroundBlur, appearance.zoomMotionBlur, appearance.zoomMotionBlurTuning, - appearance.zoomTemporalMotionBlur, - appearance.zoomMotionBlurSampleCount, - appearance.zoomMotionBlurShutterFraction, appearance.connectZooms, appearance.zoomInDurationMs, appearance.zoomInOverlapMs, diff --git a/src/components/video-editor/projectPersistence.test.ts b/src/components/video-editor/projectPersistence.test.ts index 039476bb..7b585cd3 100644 --- a/src/components/video-editor/projectPersistence.test.ts +++ b/src/components/video-editor/projectPersistence.test.ts @@ -34,6 +34,19 @@ describe("resolveVideoUrl", () => { }); describe("normalizeProjectEditor", () => { + it("ignores retired temporal blur fields when opening an older project", () => { + const savedEditor = { + zoomMotionBlur: 0.6, + zoomTemporalMotionBlur: 0.35, + zoomMotionBlurSampleCount: 13, + zoomMotionBlurShutterFraction: 0.94, + }; + const normalized = normalizeProjectEditor(savedEditor); + expect(normalized.zoomMotionBlur).toBe(0.6); + for (const field of ["zoomTemporalMotionBlur", "zoomMotionBlurSampleCount", "zoomMotionBlurShutterFraction"]) { + expect(normalized).not.toHaveProperty(field); + } + }); it("defaults to 8% on macOS and square corners elsewhere", () => { expect(getDefaultBorderRadiusPercent("MacIntel")).toBe(8); expect(getDefaultBorderRadiusPercent("Win32")).toBe(0); diff --git a/src/components/video-editor/projectPersistence.ts b/src/components/video-editor/projectPersistence.ts index 0d797c67..75154893 100644 --- a/src/components/video-editor/projectPersistence.ts +++ b/src/components/video-editor/projectPersistence.ts @@ -10,14 +10,6 @@ import type { GifSizePreset, } from "@/lib/exporter"; import { isValidMp4FrameRate } from "@/lib/exporter/types"; -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 { DEFAULT_WALLPAPER_PATH } from "@/lib/wallpapers"; import { ASPECT_RATIOS, type AspectRatio, isCustomAspectRatio } from "@/utils/aspectRatioUtils"; import { CURSOR_MOTION_PRESETS, resolveCursorMotionPresetId } from "./cursorMotionPresets"; @@ -104,9 +96,6 @@ export interface ProjectEditorState { backgroundBlur: number; zoomMotionBlur: number; zoomMotionBlurTuning: ZoomMotionBlurTuning; - zoomTemporalMotionBlur: number; - zoomMotionBlurSampleCount: number | null; - zoomMotionBlurShutterFraction: number | null; connectZooms: boolean; zoomInDurationMs: number; zoomInOverlapMs: number; @@ -354,27 +343,6 @@ export function validateProjectData(candidate: unknown): candidate is EditorProj } export function normalizeProjectEditor(editor: Partial): 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; @@ -415,11 +383,6 @@ export function normalizeProjectEditor(editor: Partial): Pro ? clamp(rawZoomMotionBlurTuning.zoomSafeZoneRadiusPx, 0, 80) : DEFAULT_ZOOM_MOTION_BLUR_TUNING.zoomSafeZoneRadiusPx, }; - const normalizedZoomTemporalMotionBlur = isFiniteNumber( - (editor as Partial).zoomTemporalMotionBlur, - ) - ? clamp((editor as Partial).zoomTemporalMotionBlur as number, 0, 2) - : normalizedZoomMotionBlur; const normalizedBackgroundBlur = isFiniteNumber( (editor as Partial).backgroundBlur, ) @@ -427,18 +390,6 @@ export function normalizeProjectEditor(editor: Partial): Pro : legacyShowBlur ? 2 : 0; - 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; @@ -915,9 +866,6 @@ export function normalizeProjectEditor(editor: Partial): Pro backgroundBlur: normalizedBackgroundBlur, zoomMotionBlur: normalizedZoomMotionBlur, zoomMotionBlurTuning: normalizedZoomMotionBlurTuning, - zoomTemporalMotionBlur: normalizedZoomTemporalMotionBlur, - zoomMotionBlurSampleCount: normalizedZoomMotionBlurSampleCount, - zoomMotionBlurShutterFraction: normalizedZoomMotionBlurShutterFraction, connectZooms: typeof editor.connectZooms === "boolean" ? editor.connectZooms : true, zoomInDurationMs: normalizedMotionPreset.zoomInDurationMs, zoomInOverlapMs: normalizedZoomInOverlapMs, diff --git a/src/components/video-editor/state/useAppearanceState.ts b/src/components/video-editor/state/useAppearanceState.ts index 7335fe7b..6dbfd4b1 100644 --- a/src/components/video-editor/state/useAppearanceState.ts +++ b/src/components/video-editor/state/useAppearanceState.ts @@ -31,15 +31,6 @@ export function useAppearanceState(preferences: EditorPreferences) { const [zoomMotionBlurTuning, setZoomMotionBlurTuning] = useState( preferences.zoomMotionBlurTuning ?? DEFAULT_ZOOM_MOTION_BLUR_TUNING, ); - const [zoomTemporalMotionBlur, setZoomTemporalMotionBlur] = useState( - preferences.zoomTemporalMotionBlur, - ); - const [zoomMotionBlurSampleCount, setZoomMotionBlurSampleCount] = useState( - preferences.zoomMotionBlurSampleCount, - ); - const [zoomMotionBlurShutterFraction, setZoomMotionBlurShutterFraction] = useState< - number | null - >(preferences.zoomMotionBlurShutterFraction); const [autoApplyFreshRecordingAutoZooms, setAutoApplyFreshRecordingAutoZooms] = useState( preferences.autoApplyFreshRecordingAutoZooms, ); @@ -135,12 +126,6 @@ export function useAppearanceState(preferences: EditorPreferences) { setZoomMotionBlur, zoomMotionBlurTuning, setZoomMotionBlurTuning, - zoomTemporalMotionBlur, - setZoomTemporalMotionBlur, - zoomMotionBlurSampleCount, - setZoomMotionBlurSampleCount, - zoomMotionBlurShutterFraction, - setZoomMotionBlurShutterFraction, autoApplyFreshRecordingAutoZooms, setAutoApplyFreshRecordingAutoZooms, connectZooms, diff --git a/src/i18n/locales/de/settings.json b/src/i18n/locales/de/settings.json index 6d5bac29..d0837fae 100644 --- a/src/i18n/locales/de/settings.json +++ b/src/i18n/locales/de/settings.json @@ -46,10 +46,6 @@ }, "backgroundBlur": "Unschärfe", "zoomMotionBlur": "Zoom-Bewegungsunschärfe", - "temporalZoomMotionBlur": "Zeitliche Zoom-Bewegungsunschärfe", - "temporalZoomMotionBlurDescription": "Steuert das Verschlussfenster und die Bildausschnitte, die vom neueren Zoom-Bewegungsunschärfe-Durchlauf verwendet werden.", - "zoomMotionBlurSamples": "Unschärfe-Samples", - "zoomMotionBlurShutter": "Verschluss", "auto": "Auto", "connectZooms": "Zooms verbinden", "connectZoomsDescription": "Füge aufeinanderfolgende Zoom-Bereiche zu einer kontinuierlichen Kamerabewegung zusammen.", diff --git a/src/i18n/locales/en/settings.json b/src/i18n/locales/en/settings.json index 31953838..596109cb 100644 --- a/src/i18n/locales/en/settings.json +++ b/src/i18n/locales/en/settings.json @@ -52,10 +52,6 @@ }, "backgroundBlur": "Blur", "zoomMotionBlur": "Zoom Motion Blur", - "temporalZoomMotionBlur": "Temporal Zoom Blur", - "temporalZoomMotionBlurDescription": "Control the shutter window and frame samples used by the newer zoom blur pass.", - "zoomMotionBlurSamples": "Blur Samples", - "zoomMotionBlurShutter": "Shutter", "auto": "Auto", "connectZooms": "Connect Zooms", "connectZoomsDescription": "Smooth consecutive zoom regions into a continuous camera move.", diff --git a/src/i18n/locales/es/settings.json b/src/i18n/locales/es/settings.json index 3690c13c..3510d6bb 100644 --- a/src/i18n/locales/es/settings.json +++ b/src/i18n/locales/es/settings.json @@ -46,10 +46,6 @@ }, "backgroundBlur": "Desenfoque", "zoomMotionBlur": "Desenfoque de movimiento del zoom", - "temporalZoomMotionBlur": "Temporal Zoom Blur", - "temporalZoomMotionBlurDescription": "Control the shutter window and frame samples used by the newer zoom blur pass.", - "zoomMotionBlurSamples": "Blur Samples", - "zoomMotionBlurShutter": "Shutter", "auto": "Auto", "connectZooms": "Conectar zooms", "connectZoomsDescription": "Suaviza regiones de zoom consecutivas convirtiéndolas en un movimiento continuo de cámara.", diff --git a/src/i18n/locales/fr/settings.json b/src/i18n/locales/fr/settings.json index 59b1e770..d7e4e675 100644 --- a/src/i18n/locales/fr/settings.json +++ b/src/i18n/locales/fr/settings.json @@ -46,10 +46,6 @@ }, "backgroundBlur": "Flou", "zoomMotionBlur": "Flou de mouvement du zoom", - "temporalZoomMotionBlur": "Temporal Zoom Blur", - "temporalZoomMotionBlurDescription": "Control the shutter window and frame samples used by the newer zoom blur pass.", - "zoomMotionBlurSamples": "Blur Samples", - "zoomMotionBlurShutter": "Shutter", "auto": "Auto", "connectZooms": "Relier les zooms", "connectZoomsDescription": "Lisse les zones de zoom consécutives pour créer un mouvement de caméra continu.", diff --git a/src/i18n/locales/it/settings.json b/src/i18n/locales/it/settings.json index 4a00e1db..8027ac02 100644 --- a/src/i18n/locales/it/settings.json +++ b/src/i18n/locales/it/settings.json @@ -46,10 +46,6 @@ }, "backgroundBlur": "Sfocatura", "zoomMotionBlur": "Motion blur dello zoom", - "temporalZoomMotionBlur": "Sfocatura zoom temporale", - "temporalZoomMotionBlurDescription": "Controlla la finestra dell'otturatore e i campioni di frame usati dal nuovo passaggio di sfocatura zoom.", - "zoomMotionBlurSamples": "Campioni di sfocatura", - "zoomMotionBlurShutter": "Otturatore", "auto": "Auto", "connectZooms": "Connetti zoom", "connectZoomsDescription": "Unisce regioni di zoom consecutive in un movimento di camera continuo.", diff --git a/src/i18n/locales/ko/settings.json b/src/i18n/locales/ko/settings.json index 1c95bf2a..b2063381 100644 --- a/src/i18n/locales/ko/settings.json +++ b/src/i18n/locales/ko/settings.json @@ -46,10 +46,6 @@ }, "backgroundBlur": "블러", "zoomMotionBlur": "확대 모션 블러", - "temporalZoomMotionBlur": "Temporal Zoom Blur", - "temporalZoomMotionBlurDescription": "Control the shutter window and frame samples used by the newer zoom blur pass.", - "zoomMotionBlurSamples": "Blur Samples", - "zoomMotionBlurShutter": "Shutter", "auto": "Auto", "connectZooms": "확대 구간 연결", "connectZoomsDescription": "연속된 확대 구간을 하나의 부드러운 카메라 이동으로 연결합니다.", diff --git a/src/i18n/locales/nl/settings.json b/src/i18n/locales/nl/settings.json index 31eeec7a..4fda9180 100644 --- a/src/i18n/locales/nl/settings.json +++ b/src/i18n/locales/nl/settings.json @@ -46,10 +46,6 @@ }, "backgroundBlur": "Vervaging", "zoomMotionBlur": "Zoom-bewegingsonscherpte", - "temporalZoomMotionBlur": "Temporal Zoom Blur", - "temporalZoomMotionBlurDescription": "Control the shutter window and frame samples used by the newer zoom blur pass.", - "zoomMotionBlurSamples": "Blur Samples", - "zoomMotionBlurShutter": "Shutter", "auto": "Auto", "connectZooms": "Zooms verbinden", "connectZoomsDescription": "Maak opeenvolgende zoomgebieden vloeiend tot een doorlopende camerabeweging.", diff --git a/src/i18n/locales/pt-BR/settings.json b/src/i18n/locales/pt-BR/settings.json index 2e4bcf62..1cb1b72e 100644 --- a/src/i18n/locales/pt-BR/settings.json +++ b/src/i18n/locales/pt-BR/settings.json @@ -46,10 +46,6 @@ }, "backgroundBlur": "Desfoque", "zoomMotionBlur": "Desfoque de movimento do zoom", - "temporalZoomMotionBlur": "Temporal Zoom Blur", - "temporalZoomMotionBlurDescription": "Control the shutter window and frame samples used by the newer zoom blur pass.", - "zoomMotionBlurSamples": "Blur Samples", - "zoomMotionBlurShutter": "Shutter", "auto": "Auto", "connectZooms": "Conectar zooms", "connectZoomsDescription": "Suaviza regiões de zoom consecutivas em um movimento contínuo de câmera.", diff --git a/src/i18n/locales/ru/settings.json b/src/i18n/locales/ru/settings.json index 0d65dd89..7ec21ff1 100644 --- a/src/i18n/locales/ru/settings.json +++ b/src/i18n/locales/ru/settings.json @@ -46,10 +46,6 @@ }, "backgroundBlur": "Размытие", "zoomMotionBlur": "Размытие при зуме", - "temporalZoomMotionBlur": "Временное размытие зума", - "temporalZoomMotionBlurDescription": "Настройка плавности зума по времени и кадрам.", - "zoomMotionBlurSamples": "Образцы размытия", - "zoomMotionBlurShutter": "Выдержка", "auto": "Авто", "connectZooms": "Связать зумы", "connectZoomsDescription": "Плавный переход между зумами.", diff --git a/src/i18n/locales/zh-CN/settings.json b/src/i18n/locales/zh-CN/settings.json index e07f4cf9..053d1095 100644 --- a/src/i18n/locales/zh-CN/settings.json +++ b/src/i18n/locales/zh-CN/settings.json @@ -46,10 +46,6 @@ }, "backgroundBlur": "模糊", "zoomMotionBlur": "缩放运动模糊", - "temporalZoomMotionBlur": "Temporal Zoom Blur", - "temporalZoomMotionBlurDescription": "Control the shutter window and frame samples used by the newer zoom blur pass.", - "zoomMotionBlurSamples": "Blur Samples", - "zoomMotionBlurShutter": "Shutter", "auto": "Auto", "connectZooms": "连接缩放", "connectZoomsDescription": "将连续的缩放区域平滑连接为一次连续的镜头移动。", diff --git a/src/i18n/locales/zh-TW/settings.json b/src/i18n/locales/zh-TW/settings.json index 1e4d6e6d..7de88520 100644 --- a/src/i18n/locales/zh-TW/settings.json +++ b/src/i18n/locales/zh-TW/settings.json @@ -46,10 +46,6 @@ }, "backgroundBlur": "模糊", "zoomMotionBlur": "縮放動態模糊", - "temporalZoomMotionBlur": "Temporal Zoom Blur", - "temporalZoomMotionBlurDescription": "Control the shutter window and frame samples used by the newer zoom blur pass.", - "zoomMotionBlurSamples": "Blur Samples", - "zoomMotionBlurShutter": "Shutter", "auto": "Auto", "connectZooms": "連接縮放", "connectZoomsDescription": "將連續的縮放區域平滑串接成一段連續的鏡頭移動。", diff --git a/src/lib/exporter/frameRenderer.ts b/src/lib/exporter/frameRenderer.ts index 0749d724..640bd39a 100644 --- a/src/lib/exporter/frameRenderer.ts +++ b/src/lib/exporter/frameRenderer.ts @@ -75,9 +75,7 @@ import { renderAnnotations } from "./annotationRenderer"; import { renderCaptions } from "./captionRenderer"; import { ForwardFrameSource } from "./forwardFrameSource"; import { resolveMediaElementSource } from "./localMediaSource"; -import { buildTemporalSamplePlanUs, getTemporalMotionBlurConfig } from "./temporalMotionBlur"; -const TEMPORAL_ZOOM_MOTION_BLUR_ENABLED = false; interface FrameRenderConfig { timelineEffects?: boolean; @@ -91,9 +89,6 @@ interface FrameRenderConfig { backgroundBlur: number; zoomMotionBlur?: number; zoomMotionBlurTuning?: ZoomMotionBlurTuning; - zoomTemporalMotionBlur?: number; - zoomMotionBlurSampleCount?: number | null; - zoomMotionBlurShutterFraction?: number | null; connectZooms?: boolean; zoomInDurationMs?: number; zoomInOverlapMs?: number; @@ -197,11 +192,6 @@ interface LayoutCache { }; } -interface RenderSnapshot { - timeMs: number; - sceneTransform: { scale: number; x: number; y: number }; -} - function createAnimationState(): AnimationState { return { scale: 1, @@ -245,8 +235,6 @@ export class FrameRenderer { private shadowCtx: CanvasRenderingContext2D | null = null; private compositeCanvas: HTMLCanvasElement | null = null; private compositeCtx: CanvasRenderingContext2D | null = null; - private temporalAccumulationCanvas: HTMLCanvasElement | null = null; - private temporalAccumulationCtx: CanvasRenderingContext2D | null = null; private backgroundForwardFrameSource: ForwardFrameSource | null = null; private backgroundForwardFrameSourceUrl: string | null = null; private backgroundForwardFrameDurationSec: number | null = null; @@ -470,19 +458,6 @@ export class FrameRenderer { throw new Error("Failed to get 2D context for composite canvas"); } - this.temporalAccumulationCanvas = document.createElement("canvas"); - this.temporalAccumulationCanvas.width = this.config.width; - this.temporalAccumulationCanvas.height = this.config.height; - this.temporalAccumulationCtx = configureHighQuality2DContext( - this.temporalAccumulationCanvas.getContext("2d", { - willReadFrequently: true, - }), - ); - - if (!this.temporalAccumulationCtx) { - throw new Error("Failed to get 2D context for temporal accumulation canvas"); - } - // Setup shadow canvas if needed if (this.config.showShadow) { this.shadowCanvas = document.createElement("canvas"); @@ -1407,7 +1382,7 @@ export class FrameRenderer { videoFrame: VideoFrame | null, timestamp: number, cursorTimestamp = timestamp, - frameDurationUs?: number, + _frameDurationUs?: number, backgroundTimelineTimestamp = timestamp, ): Promise { if (!this.app || !this.videoContainer || !this.cameraContainer || !this.compositeCtx) { @@ -1449,62 +1424,6 @@ export class FrameRenderer { return; } - const temporalSnapshot = - TEMPORAL_ZOOM_MOTION_BLUR_ENABLED && - (this.config.zoomTemporalMotionBlur ?? 0) > 0 && - typeof frameDurationUs === "number" && - frameDurationUs > 0 - ? await this.renderTemporalMotionBlurFrame( - timestamp, - cursorTimestamp, - backgroundTimelineTimestamp, - frameDurationUs, - layoutCache, - ) - : null; - - if (temporalSnapshot) { - if ( - this.config.annotationRegions && - this.config.annotationRegions.length > 0 && - this.compositeCtx - ) { - const scaleX = this.config.width / BASE_PREVIEW_WIDTH; - const scaleY = this.config.height / BASE_PREVIEW_HEIGHT; - const scaleFactor = (scaleX + scaleY) / 2; - - await renderAnnotations( - this.compositeCtx, - this.config.annotationRegions, - this.config.width, - this.config.height, - temporalSnapshot.timeMs, - scaleFactor, - undefined, - temporalSnapshot.sceneTransform, - this.layoutCache?.maskRect, - ); - } - - if ( - this.config.autoCaptions && - this.config.autoCaptions.length > 0 && - this.config.autoCaptionSettings && - this.compositeCtx - ) { - renderCaptions( - this.compositeCtx, - this.config.autoCaptions, - this.config.autoCaptionSettings, - this.config.width, - this.config.height, - timestamp / 1000, - ); - } - - return; - } - if (this.webcamForwardFrameSource || this.webcamVideoElement) { const targetTime = Math.max(0, this.currentVideoTime); await this.syncWebcamFrame(targetTime); @@ -1757,136 +1676,6 @@ export class FrameRenderer { ); } - private async renderSceneSample( - timestamp: number, - cursorTimestamp: number, - backgroundTimelineTimestamp: number, - layoutCache: LayoutCache, - useVelocityMotionBlur: boolean, - ): Promise { - if (!this.app || !this.cameraContainer) { - throw new Error("Renderer not initialized"); - } - - this.currentVideoTime = timestamp / 1_000_000; - - if (this.webcamForwardFrameSource || this.webcamVideoElement) { - await this.syncWebcamFrame(Math.max(0, this.currentVideoTime)); - } - - if (this.backgroundForwardFrameSource || this.backgroundVideoElement) { - await this.syncBackgroundFrame(Math.max(0, backgroundTimelineTimestamp / 1_000_000)); - } - - const timeMs = this.config.timelineEffects - ? backgroundTimelineTimestamp / 1000 - : timestamp / 1000; - const cursorTimeMs = cursorTimestamp / 1000; - - if (this.cursorOverlay) { - this.cursorOverlay.update( - this.config.cursorTelemetry ?? [], - cursorTimeMs, - layoutCache.maskRect, - this.config.showCursor ?? true, - false, - ); - } - - this.updateAnimationState(timeMs, cursorTimeMs); - - applyZoomTransform({ - cameraContainer: this.cameraContainer, - zoomBlurFilter: this.zoomBlurFilter, - motionBlurFilter: this.motionBlurFilter, - stageSize: layoutCache.stageSize, - baseMask: layoutCache.maskRect, - zoomScale: this.animationState.scale, - zoomProgress: this.animationState.progress, - focusX: this.animationState.focusX, - focusY: this.animationState.focusY, - isPlaying: true, - motionBlurAmount: useVelocityMotionBlur ? (this.config.zoomMotionBlur ?? 0) : 0, - motionBlurTuning: this.config.zoomMotionBlurTuning, - transformOverride: { - scale: this.animationState.appliedScale, - x: this.animationState.x, - y: this.animationState.y, - }, - motionBlurState: this.motionBlurState, - frameTimeMs: timeMs, - }); - - this.app.renderer.render(this.app.stage); - this.compositeWithShadows(); - - return { - timeMs, - sceneTransform: { - scale: this.animationState.appliedScale, - x: this.animationState.x, - y: this.animationState.y, - }, - }; - } - - private async renderTemporalMotionBlurFrame( - timestamp: number, - cursorTimestamp: number, - backgroundTimelineTimestamp: number, - frameDurationUs: number, - layoutCache: LayoutCache, - ): Promise { - if (!this.compositeCanvas || !this.compositeCtx || !this.temporalAccumulationCtx) { - return null; - } - - const blurConfig = getTemporalMotionBlurConfig(this.config.zoomTemporalMotionBlur, { - sampleCount: this.config.zoomMotionBlurSampleCount, - shutterFraction: this.config.zoomMotionBlurShutterFraction, - }); - if (!blurConfig) { - return null; - } - - const samplePlan = buildTemporalSamplePlanUs(frameDurationUs, blurConfig); - - this.temporalAccumulationCtx.clearRect(0, 0, this.config.width, this.config.height); - - let centerSnapshot: RenderSnapshot | null = null; - let lastSnapshot: RenderSnapshot | null = null; - - for (const { offsetUs: sampleOffsetUs, weight } of samplePlan) { - const sampleTimestamp = Math.max(0, timestamp + sampleOffsetUs); - const sampleCursorTimestamp = Math.max(0, cursorTimestamp + sampleOffsetUs); - const sampleBackgroundTimelineTimestamp = Math.max( - 0, - backgroundTimelineTimestamp + sampleOffsetUs, - ); - const snapshot = await this.renderSceneSample( - sampleTimestamp, - sampleCursorTimestamp, - sampleBackgroundTimelineTimestamp, - layoutCache, - false, - ); - lastSnapshot = snapshot; - if (Math.abs(sampleOffsetUs) < 0.0001) { - centerSnapshot = snapshot; - } - - this.temporalAccumulationCtx.save(); - this.temporalAccumulationCtx.globalCompositeOperation = "lighter"; - this.temporalAccumulationCtx.globalAlpha = weight; - this.temporalAccumulationCtx.drawImage(this.compositeCanvas, 0, 0); - this.temporalAccumulationCtx.restore(); - } - this.compositeCtx.clearRect(0, 0, this.config.width, this.config.height); - this.compositeCtx.drawImage(this.temporalAccumulationCanvas!, 0, 0); - - return centerSnapshot ?? lastSnapshot; - } - private compositeWithShadows(includeWebcam = true): void { if (!this.compositeCanvas || !this.compositeCtx || !this.app) return; @@ -2202,8 +1991,6 @@ export class FrameRenderer { this.shadowCtx = null; this.compositeCanvas = null; this.compositeCtx = null; - this.temporalAccumulationCanvas = null; - this.temporalAccumulationCtx = null; this.backgroundCtx = null; this.closeBackgroundDecodedFrame(); this.backgroundForwardFrameSource?.cancel(); diff --git a/src/lib/exporter/gifExporter.ts b/src/lib/exporter/gifExporter.ts index c7a0214d..2df73000 100644 --- a/src/lib/exporter/gifExporter.ts +++ b/src/lib/exporter/gifExporter.ts @@ -46,9 +46,6 @@ interface GifExporterConfig { backgroundBlur: number; zoomMotionBlur?: number; zoomMotionBlurTuning?: ZoomMotionBlurTuning; - zoomTemporalMotionBlur?: number; - zoomMotionBlurSampleCount?: number | null; - zoomMotionBlurShutterFraction?: number | null; connectZooms?: boolean; zoomInDurationMs?: number; zoomInOverlapMs?: number; @@ -149,9 +146,6 @@ export function buildGifFrameRendererConfig( backgroundBlur: config.backgroundBlur, zoomMotionBlur: config.zoomMotionBlur, zoomMotionBlurTuning: config.zoomMotionBlurTuning, - zoomTemporalMotionBlur: config.zoomTemporalMotionBlur, - zoomMotionBlurSampleCount: config.zoomMotionBlurSampleCount, - zoomMotionBlurShutterFraction: config.zoomMotionBlurShutterFraction, connectZooms: config.connectZooms, zoomInDurationMs: config.zoomInDurationMs, zoomInOverlapMs: config.zoomInOverlapMs, diff --git a/src/lib/exporter/modernFrameRenderer.test.ts b/src/lib/exporter/modernFrameRenderer.test.ts index 5c473f0e..79c2caa1 100644 --- a/src/lib/exporter/modernFrameRenderer.test.ts +++ b/src/lib/exporter/modernFrameRenderer.test.ts @@ -202,28 +202,32 @@ function createRenderer() { }); } -it("renders the background during a timeline gap without source layers", async () => { +it("bypasses blur annotation compositing during gaps and clears stale composite frames", async () => { const renderer = createRenderer(); const camera = { visible: true }; - const output = createMockCanvas(); - output.width = 1920; - output.height = 1080; - const annotations = vi.fn(); - const renderOutput = vi.fn(async () => {}); + const canvas = createMockCanvas(); + const render = vi.fn(); + const compose = vi.fn(); + const webcam = { visible: true }; + const captions = { visible: true }; Object.assign(renderer, { - app: {}, + app: { canvas, render }, videoContainer: {}, videoMaskGraphics: {}, cameraContainer: camera, - ensureExportCompositeCanvas: () => ({ canvas: output, context: output.context }), - updateAnnotationLayer: annotations, - renderOutput, + webcamRootContainer: webcam, + captionContainer: captions, + hasActiveBlurAnnotations: () => true, + composeBlurAnnotationFrame: compose, + outputCanvasOverride: createMockCanvas(), }); await renderer.renderFrame(null, 0, 0, 33333, 1500000); expect(camera.visible).toBe(false); - expect(output.context.fillRect).not.toHaveBeenCalled(); - expect(annotations).not.toHaveBeenCalled(); - expect(renderOutput).toHaveBeenCalledWith(1500); + expect(webcam.visible).toBe(false); + expect(captions.visible).toBe(false); + expect(compose).not.toHaveBeenCalled(); + expect(render).toHaveBeenCalledOnce(); + expect(renderer.getCanvas()).toBe(canvas); }); describe("ModernFrameRenderer Pixi lifecycle", () => { @@ -813,9 +817,7 @@ describe("ModernFrameRenderer webcam export fallback", () => { }); describe("ModernFrameRenderer frame sequencing", () => { - it.each([ - 0, 1, - ])("resumes visual layers after a background gap with temporal blur set to %s", async (temporalBlur) => { + it("resumes visual layers after a background gap", async () => { const renderer = createRenderer(); const sceneCanvas = createMockCanvas(); const gapCanvas = createMockCanvas(); @@ -834,7 +836,6 @@ describe("ModernFrameRenderer frame sequencing", () => { width: 1920, height: 1080, timelineEffects: true, - zoomTemporalMotionBlur: temporalBlur, zoomMotionBlur: 0.5, }, app: { canvas: sceneCanvas, render }, diff --git a/src/lib/exporter/modernFrameRenderer.ts b/src/lib/exporter/modernFrameRenderer.ts index 40dc9978..629f2521 100644 --- a/src/lib/exporter/modernFrameRenderer.ts +++ b/src/lib/exporter/modernFrameRenderer.ts @@ -108,9 +108,6 @@ interface FrameRenderConfig { backgroundBlur: number; zoomMotionBlur?: number; zoomMotionBlurTuning?: ZoomMotionBlurTuning; - zoomTemporalMotionBlur?: number; - zoomMotionBlurSampleCount?: number | null; - zoomMotionBlurShutterFraction?: number | null; connectZooms?: boolean; zoomInDurationMs?: number; zoomInOverlapMs?: number; @@ -2867,7 +2864,9 @@ export class FrameRenderer { } if (this.webcamRootContainer) this.webcamRootContainer.visible = false; if (this.captionContainer) this.captionContainer.visible = false; - await this.renderOutput(backgroundTimelineTimestamp / 1000); + // Gap frames must bypass canvas annotation compositing as well as Pixi layers. + this.outputCanvasOverride = null; + this.app.render(); return; } if (this.captionContainer) this.captionContainer.visible = true; diff --git a/src/lib/exporter/modernVideoExporter.ts b/src/lib/exporter/modernVideoExporter.ts index 86b7ecc9..81137b26 100644 --- a/src/lib/exporter/modernVideoExporter.ts +++ b/src/lib/exporter/modernVideoExporter.ts @@ -104,9 +104,6 @@ interface VideoExporterConfig extends ExportConfig { backgroundBlur: number; zoomMotionBlur?: number; zoomMotionBlurTuning?: ZoomMotionBlurTuning; - zoomTemporalMotionBlur?: number; - zoomMotionBlurSampleCount?: number | null; - zoomMotionBlurShutterFraction?: number | null; connectZooms?: boolean; zoomInDurationMs?: number; zoomInOverlapMs?: number; @@ -615,9 +612,6 @@ export class ModernVideoExporter { backgroundBlur: this.config.backgroundBlur, zoomMotionBlur: this.config.zoomMotionBlur, zoomMotionBlurTuning: this.config.zoomMotionBlurTuning, - zoomTemporalMotionBlur: this.config.zoomTemporalMotionBlur, - zoomMotionBlurSampleCount: this.config.zoomMotionBlurSampleCount, - zoomMotionBlurShutterFraction: this.config.zoomMotionBlurShutterFraction, connectZooms: this.config.connectZooms, zoomInDurationMs: this.config.zoomInDurationMs, zoomInOverlapMs: this.config.zoomInOverlapMs, diff --git a/src/lib/exporter/temporalMotionBlur.test.ts b/src/lib/exporter/temporalMotionBlur.test.ts deleted file mode 100644 index b2a37f31..00000000 --- a/src/lib/exporter/temporalMotionBlur.test.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { - buildTemporalSampleOffsetsUs, - buildTemporalSamplePlanUs, - getTemporalMotionBlurConfig, -} from "./temporalMotionBlur"; - -describe("temporalMotionBlur", () => { - it("disables temporal blur for zero strength", () => { - expect(getTemporalMotionBlurConfig(0)).toBeNull(); - expect(getTemporalMotionBlurConfig(undefined)).toBeNull(); - }); - - it("uses an odd number of centered samples", () => { - const config = getTemporalMotionBlurConfig(1); - - expect(config).not.toBeNull(); - expect(config?.sampleCount).toBe(5); - expect(config?.sampleCount % 2).toBe(1); - expect(config?.shutterFraction).toBeGreaterThan(0.24); - }); - - it("caps the automatic sample budget to keep exports responsive", () => { - const config = getTemporalMotionBlurConfig(2); - - expect(config).not.toBeNull(); - expect(config?.sampleCount).toBe(5); - }); - - it("builds symmetric shutter offsets around the frame center", () => { - const offsets = buildTemporalSampleOffsetsUs(33_333.333, { - sampleCount: 5, - shutterFraction: 0.9, - }); - - expect(offsets).toHaveLength(5); - expect(offsets[2]).toBeCloseTo(0, 6); - expect(offsets[0]).toBeCloseTo(-offsets[4], 6); - expect(offsets[1]).toBeCloseTo(-offsets[3], 6); - }); - - it("accepts explicit shutter and odd sample overrides", () => { - const config = getTemporalMotionBlurConfig(0.35, { - sampleCount: 60, - shutterFraction: 3, - }); - - expect(config).not.toBeNull(); - expect(config?.sampleCount).toBe(61); - expect(config?.shutterFraction).toBeCloseTo(3, 6); - }); - - it("allows experimental multi-frame shutter windows", () => { - const offsets = buildTemporalSampleOffsetsUs(33_333.333, { - sampleCount: 61, - shutterFraction: 3, - }); - - expect(offsets).toHaveLength(61); - expect(offsets[30]).toBeCloseTo(0, 6); - expect(offsets[0]).toBeCloseTo(-50_000, 0); - expect(offsets[60]).toBeCloseTo(50_000, 0); - }); - - it("builds normalized sample weights with a center bias", () => { - const config = getTemporalMotionBlurConfig(1.2); - expect(config).not.toBeNull(); - - const plan = buildTemporalSamplePlanUs(33_333.333, config!); - const totalWeight = plan.reduce((sum, sample) => sum + sample.weight, 0); - const centerSample = plan[Math.floor(plan.length / 2)]; - const edgeSample = plan[0]; - - expect(totalWeight).toBeCloseTo(1, 6); - expect(centerSample?.weight ?? 0).toBeGreaterThan(edgeSample?.weight ?? 0); - expect(plan.map((sample) => sample.offsetUs)).toContain(0); - }); -}); diff --git a/src/lib/exporter/temporalMotionBlur.ts b/src/lib/exporter/temporalMotionBlur.ts deleted file mode 100644 index 69ec5e24..00000000 --- a/src/lib/exporter/temporalMotionBlur.ts +++ /dev/null @@ -1,134 +0,0 @@ -interface TemporalMotionBlurConfig { - sampleCount: number; - shutterFraction: number; - weightCurvePower: number; -} - -interface TemporalMotionBlurOverrides { - sampleCount?: number | null; - shutterFraction?: number | null; -} - -interface TemporalMotionBlurSample { - offsetUs: number; - weight: number; -} - -const MIN_BLUR_AMOUNT = 0.001; -export const TEMPORAL_MOTION_BLUR_MIN_SHUTTER_FRACTION = 0.18; -export const TEMPORAL_MOTION_BLUR_MAX_SHUTTER_FRACTION = 3; -export const TEMPORAL_MOTION_BLUR_MIN_SAMPLE_COUNT = 3; -export const TEMPORAL_MOTION_BLUR_MAX_SAMPLE_COUNT = 61; -export const TEMPORAL_MOTION_BLUR_DEFAULT_SAMPLE_COUNT = 13; -export const TEMPORAL_MOTION_BLUR_DEFAULT_SHUTTER_FRACTION = 0.94; -const TEMPORAL_MOTION_BLUR_AUTO_MIN_SHUTTER_FRACTION = 0.24; -const TEMPORAL_MOTION_BLUR_AUTO_MAX_SHUTTER_FRACTION = 0.62; -const TEMPORAL_MOTION_BLUR_AUTO_MAX_SAMPLE_COUNT = 5; -const TEMPORAL_MOTION_BLUR_WEIGHT_FLOOR = 0.22; -const MAX_BLUR_AMOUNT = 2; - -function normalizeTemporalMotionBlurSampleCount(value: number | null | undefined): number | null { - if (!Number.isFinite(value)) { - return null; - } - - const roundedValue = Math.round(value ?? 0); - const clampedValue = Math.min( - TEMPORAL_MOTION_BLUR_MAX_SAMPLE_COUNT, - Math.max(TEMPORAL_MOTION_BLUR_MIN_SAMPLE_COUNT, roundedValue), - ); - - if (clampedValue % 2 === 1) { - return clampedValue; - } - - return clampedValue >= TEMPORAL_MOTION_BLUR_MAX_SAMPLE_COUNT - ? clampedValue - 1 - : clampedValue + 1; -} - -export function getTemporalMotionBlurConfig( - amount: number | null | undefined, - overrides: TemporalMotionBlurOverrides = {}, -): TemporalMotionBlurConfig | null { - const resolvedAmount = Number.isFinite(amount) ? Math.max(0, amount ?? 0) : 0; - if (resolvedAmount < MIN_BLUR_AMOUNT) { - return null; - } - - const normalizedAmount = Math.min(1, resolvedAmount / MAX_BLUR_AMOUNT); - const sampleStepCount = Math.round( - normalizedAmount * - ((TEMPORAL_MOTION_BLUR_AUTO_MAX_SAMPLE_COUNT - TEMPORAL_MOTION_BLUR_MIN_SAMPLE_COUNT) / - 2), - ); - const defaultSampleCount = TEMPORAL_MOTION_BLUR_MIN_SAMPLE_COUNT + sampleStepCount * 2; - const sampleCount = - normalizeTemporalMotionBlurSampleCount(overrides.sampleCount) ?? defaultSampleCount; - const shutterFraction = Number.isFinite(overrides.shutterFraction) - ? Math.min( - TEMPORAL_MOTION_BLUR_MAX_SHUTTER_FRACTION, - Math.max( - TEMPORAL_MOTION_BLUR_MIN_SHUTTER_FRACTION, - overrides.shutterFraction ?? TEMPORAL_MOTION_BLUR_AUTO_MIN_SHUTTER_FRACTION, - ), - ) - : TEMPORAL_MOTION_BLUR_AUTO_MIN_SHUTTER_FRACTION + - normalizedAmount * - (TEMPORAL_MOTION_BLUR_AUTO_MAX_SHUTTER_FRACTION - - TEMPORAL_MOTION_BLUR_AUTO_MIN_SHUTTER_FRACTION); - - return { - sampleCount, - shutterFraction, - weightCurvePower: 1.2 + normalizedAmount * 0.9, - }; -} - -export function buildTemporalSampleOffsetsUs( - frameDurationUs: number, - config: TemporalMotionBlurConfig, -): number[] { - const safeFrameDurationUs = Math.max(1, frameDurationUs); - const safeSampleCount = Math.max(1, Math.floor(config.sampleCount)); - if (safeSampleCount === 1) { - return [0]; - } - - const shutterWindowUs = - safeFrameDurationUs * - Math.max(0, Math.min(TEMPORAL_MOTION_BLUR_MAX_SHUTTER_FRACTION, config.shutterFraction)); - const startOffsetUs = -shutterWindowUs / 2; - const stepUs = shutterWindowUs / (safeSampleCount - 1); - - return Array.from({ length: safeSampleCount }, (_, index) => startOffsetUs + stepUs * index); -} - -export function buildTemporalSamplePlanUs( - frameDurationUs: number, - config: TemporalMotionBlurConfig, -): TemporalMotionBlurSample[] { - const offsetsUs = buildTemporalSampleOffsetsUs(frameDurationUs, config); - if (offsetsUs.length === 1) { - return [{ offsetUs: 0, weight: 1 }]; - } - - const centerIndex = (offsetsUs.length - 1) / 2; - const rawWeights = offsetsUs.map((_offsetUs, index) => { - const normalizedDistance = Math.abs(index - centerIndex) / Math.max(1, centerIndex); - const taperedWeight = Math.cos(normalizedDistance * (Math.PI / 2)); - return ( - TEMPORAL_MOTION_BLUR_WEIGHT_FLOOR + - (1 - TEMPORAL_MOTION_BLUR_WEIGHT_FLOOR) * - Math.pow(Math.max(0, taperedWeight), config.weightCurvePower) - ); - }); - const totalWeight = rawWeights.reduce((sum, weight) => sum + weight, 0) || 1; - - return offsetsUs.map((offsetUs, index) => ({ - offsetUs, - weight: rawWeights[index]! / totalWeight, - })); -} - -export type { TemporalMotionBlurConfig, TemporalMotionBlurOverrides, TemporalMotionBlurSample }; diff --git a/src/lib/exporter/videoExporter.ts b/src/lib/exporter/videoExporter.ts index d8f1c6c0..2eaa18f7 100644 --- a/src/lib/exporter/videoExporter.ts +++ b/src/lib/exporter/videoExporter.ts @@ -55,9 +55,6 @@ interface VideoExporterConfig extends ExportConfig { backgroundBlur: number; zoomMotionBlur?: number; zoomMotionBlurTuning?: ZoomMotionBlurTuning; - zoomTemporalMotionBlur?: number; - zoomMotionBlurSampleCount?: number | null; - zoomMotionBlurShutterFraction?: number | null; connectZooms?: boolean; zoomInDurationMs?: number; zoomInOverlapMs?: number; @@ -228,9 +225,6 @@ export class VideoExporter { backgroundBlur: this.config.backgroundBlur, zoomMotionBlur: this.config.zoomMotionBlur, zoomMotionBlurTuning: this.config.zoomMotionBlurTuning, - zoomTemporalMotionBlur: this.config.zoomTemporalMotionBlur, - zoomMotionBlurSampleCount: this.config.zoomMotionBlurSampleCount, - zoomMotionBlurShutterFraction: this.config.zoomMotionBlurShutterFraction, connectZooms: this.config.connectZooms, zoomInDurationMs: this.config.zoomInDurationMs, zoomInOverlapMs: this.config.zoomInOverlapMs,