Merge pull request #499 from webadderallorg/fix-motion-blur-regressions

fix motion blur regressions
This commit is contained in:
webadderall
2026-05-11 22:13:35 +10:00
committed by GitHub
8 changed files with 170 additions and 68 deletions
@@ -2734,7 +2734,7 @@ export function SettingsPanel({
.maxDirectionalBlurPx
}
min={0}
max={32}
max={96}
step={0.1}
onChange={(value) =>
onZoomMotionBlurTuningChange?.({
@@ -2778,7 +2778,7 @@ export function SettingsPanel({
.maxRadialBlurStrength
}
min={0}
max={0.5}
max={1.5}
step={0.005}
onChange={(value) =>
onZoomMotionBlurTuningChange?.({
+5 -2
View File
@@ -135,6 +135,7 @@ import {
fromFileUrl,
normalizeProjectEditor,
resolveVideoUrl,
stripPersistedDevMotionBlurSettings,
toFileUrl,
validateProjectData,
} from "./projectPersistence";
@@ -1774,7 +1775,7 @@ export default function VideoEditor() {
defaultSourceAudioTrackSettings: SourceAudioTrackSettings;
}>,
) => {
return editor;
return stripPersistedDevMotionBlurSettings(editor);
},
[],
);
@@ -2038,7 +2039,9 @@ export default function VideoEditor() {
const project = candidate;
const sourcePath = fromFileUrl(project.videoPath);
const normalizedEditor = normalizeProjectEditor(project.editor);
const normalizedEditor = normalizeProjectEditor(
stripPersistedDevMotionBlurSettings(project.editor ?? {}),
);
try {
videoPlaybackRef.current?.pause();
@@ -135,6 +135,7 @@ import {
DEFAULT_CURSOR_CLICK_BOUNCE_DURATION,
DEFAULT_CURSOR_MOTION_BLUR,
DEFAULT_CURSOR_SIZE,
DEFAULT_CURSOR_STYLE,
DEFAULT_CURSOR_SMOOTHING,
DEFAULT_CURSOR_SWAY,
DEFAULT_PADDING,
@@ -429,7 +430,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
onAnnotationSizeChange,
cursorTelemetry = [],
showCursor = false,
cursorStyle = "tahoe",
cursorStyle = DEFAULT_CURSOR_STYLE,
cursorSize = DEFAULT_CURSOR_SIZE,
cursorSmoothing = DEFAULT_CURSOR_SMOOTHING,
cursorSpringStiffnessMultiplier = 1,
+27 -1
View File
@@ -1,7 +1,12 @@
import * as fc from "fast-check";
import { describe, expect, it } from "vitest";
import { CURSOR_MOTION_PRESETS } from "./cursorMotionPresets";
import { fromFileUrl, normalizeProjectEditor, toFileUrl } from "./projectPersistence";
import {
fromFileUrl,
normalizeProjectEditor,
stripPersistedDevMotionBlurSettings,
toFileUrl,
} from "./projectPersistence";
describe("Audio path handling", () => {
describe("toFileUrl produces valid file:// URLs for audio paths", () => {
@@ -288,3 +293,24 @@ describe("Motion preset normalization", () => {
expect(result.cursorClickBounceDuration).toBe(focused.cursorClickBounceDuration);
});
});
describe("Dev-only motion blur persistence", () => {
it("strips legacy dev-only blur tuning from project data before normalization", () => {
const defaults = normalizeProjectEditor({} as any);
const stripped = stripPersistedDevMotionBlurSettings({
zoomMotionBlurTuning: {
...defaults.zoomMotionBlurTuning,
maxDirectionalBlurPx: 96,
maxRadialBlurStrength: 1.5,
},
wallpaper: "/wallpapers/wallpaper2.jpg",
} as any);
expect(stripped).not.toHaveProperty("zoomMotionBlurTuning");
const result = normalizeProjectEditor(stripped as any);
expect(result.wallpaper).toBe("/wallpapers/wallpaper2.jpg");
expect(result.zoomMotionBlurTuning).toEqual(defaults.zoomMotionBlurTuning);
});
});
@@ -69,8 +69,8 @@ describe("editorPreferences", () => {
expect(DEFAULT_EDITOR_PREFERENCES.exportQuality).toBe("source");
});
it("defaults cursor preferences to Tahoe at 2.5x with lighter sway", () => {
expect(DEFAULT_EDITOR_PREFERENCES.cursorStyle).toBe("tahoe");
it("defaults cursor preferences to macOS at 2.5x with lighter sway", () => {
expect(DEFAULT_EDITOR_PREFERENCES.cursorStyle).toBe("macos");
expect(DEFAULT_EDITOR_PREFERENCES.cursorSize).toBe(2.5);
expect(DEFAULT_EDITOR_PREFERENCES.cursorSway).toBe(0.25);
});
@@ -79,6 +79,57 @@ describe("editorPreferences", () => {
expect(DEFAULT_EDITOR_PREFERENCES.exportPipelineModel).toBe("modern");
});
it("bakes in the stronger split motion blur defaults", () => {
expect(DEFAULT_EDITOR_PREFERENCES.zoomMotionBlurTuning).toMatchObject({
panVelocityThreshold: 0,
zoomVelocityThreshold: 0,
maxDirectionalBlurPx: 41.8,
maxRadialBlurStrength: 1,
});
});
it("ignores legacy persisted dev-only motion blur overrides", () => {
vi.stubGlobal(
"localStorage",
createStorageMock({
[EDITOR_PREFERENCES_STORAGE_KEY]: JSON.stringify({
zoomMotionBlurTuning: {
panVelocityThreshold: 40,
zoomVelocityThreshold: 0.2,
maxDirectionalBlurPx: 96,
maxRadialBlurStrength: 1.5,
panResponsePerSecond: 22,
zoomResponsePerSecond: 18,
zoomSafeZoneRadiusPx: 40,
},
}),
}),
);
const loaded = loadEditorPreferences();
expect(loaded.zoomMotionBlurTuning).toEqual(DEFAULT_EDITOR_PREFERENCES.zoomMotionBlurTuning);
});
it("does not save dev-only split blur tuning overrides to editor preferences", () => {
const localStorage = createStorageMock();
vi.stubGlobal("localStorage", localStorage);
saveEditorPreferences({
zoomMotionBlurTuning: {
...DEFAULT_EDITOR_PREFERENCES.zoomMotionBlurTuning,
maxDirectionalBlurPx: 96,
maxRadialBlurStrength: 1.5,
},
});
const stored = JSON.parse(
localStorage.getItem(EDITOR_PREFERENCES_STORAGE_KEY) ?? "{}",
) as Record<string, unknown>;
expect(stored).not.toHaveProperty("zoomMotionBlurTuning");
});
it("loads stored editor control preferences", () => {
vi.stubGlobal(
"localStorage",
@@ -212,8 +263,7 @@ describe("editorPreferences", () => {
autoApplyFreshRecordingAutoZooms: false,
});
expect(loadEditorPreferences()).toEqual({
...DEFAULT_EDITOR_PREFERENCES,
expect(loadEditorPreferences()).toMatchObject({
wallpaper: "linear-gradient(to right, #000000, #ffffff)",
shadowIntensity: 0.4,
backgroundBlur: 1.5,
@@ -3,6 +3,7 @@ import {
normalizeExportMp4FrameRate,
normalizeExportPipelineModel,
normalizeProjectEditor,
stripPersistedDevMotionBlurSettings,
type ProjectEditorState,
} from "./projectPersistence";
@@ -267,71 +268,74 @@ function normalizeEditorControls(
raw: Partial<EditorPreferences>,
fallback: EditorPreferences,
): PersistedEditorControls {
const sanitizedRaw = stripPersistedDevMotionBlurSettings(raw);
const candidate: PartialEditorControls = {
wallpaper: raw.wallpaper ?? fallback.wallpaper,
shadowIntensity: raw.shadowIntensity ?? fallback.shadowIntensity,
backgroundBlur: raw.backgroundBlur ?? fallback.backgroundBlur,
zoomMotionBlur: raw.zoomMotionBlur ?? fallback.zoomMotionBlur,
zoomMotionBlurTuning: raw.zoomMotionBlurTuning ?? fallback.zoomMotionBlurTuning,
zoomTemporalMotionBlur: raw.zoomTemporalMotionBlur ?? fallback.zoomTemporalMotionBlur,
zoomMotionBlurSampleCount:
raw.zoomMotionBlurSampleCount ?? fallback.zoomMotionBlurSampleCount,
zoomMotionBlurShutterFraction:
raw.zoomMotionBlurShutterFraction ?? fallback.zoomMotionBlurShutterFraction,
connectZooms: raw.connectZooms ?? fallback.connectZooms,
zoomInDurationMs: raw.zoomInDurationMs ?? fallback.zoomInDurationMs,
zoomInOverlapMs: raw.zoomInOverlapMs ?? fallback.zoomInOverlapMs,
zoomOutDurationMs: raw.zoomOutDurationMs ?? fallback.zoomOutDurationMs,
connectedZoomGapMs: raw.connectedZoomGapMs ?? fallback.connectedZoomGapMs,
connectedZoomDurationMs: raw.connectedZoomDurationMs ?? fallback.connectedZoomDurationMs,
zoomInEasing: raw.zoomInEasing ?? fallback.zoomInEasing,
zoomOutEasing: raw.zoomOutEasing ?? fallback.zoomOutEasing,
connectedZoomEasing: raw.connectedZoomEasing ?? fallback.connectedZoomEasing,
showCursor: raw.showCursor ?? fallback.showCursor,
loopCursor: raw.loopCursor ?? fallback.loopCursor,
cursorStyle: raw.cursorStyle ?? fallback.cursorStyle,
cursorSize: raw.cursorSize ?? fallback.cursorSize,
cursorSmoothing: raw.cursorSmoothing ?? fallback.cursorSmoothing,
wallpaper: sanitizedRaw.wallpaper ?? fallback.wallpaper,
shadowIntensity: sanitizedRaw.shadowIntensity ?? fallback.shadowIntensity,
backgroundBlur: sanitizedRaw.backgroundBlur ?? fallback.backgroundBlur,
zoomMotionBlur: sanitizedRaw.zoomMotionBlur ?? fallback.zoomMotionBlur,
connectZooms: sanitizedRaw.connectZooms ?? fallback.connectZooms,
zoomInDurationMs: sanitizedRaw.zoomInDurationMs ?? fallback.zoomInDurationMs,
zoomInOverlapMs: sanitizedRaw.zoomInOverlapMs ?? fallback.zoomInOverlapMs,
zoomOutDurationMs: sanitizedRaw.zoomOutDurationMs ?? fallback.zoomOutDurationMs,
connectedZoomGapMs:
sanitizedRaw.connectedZoomGapMs ?? fallback.connectedZoomGapMs,
connectedZoomDurationMs:
sanitizedRaw.connectedZoomDurationMs ?? fallback.connectedZoomDurationMs,
zoomInEasing: sanitizedRaw.zoomInEasing ?? fallback.zoomInEasing,
zoomOutEasing: sanitizedRaw.zoomOutEasing ?? fallback.zoomOutEasing,
connectedZoomEasing:
sanitizedRaw.connectedZoomEasing ?? fallback.connectedZoomEasing,
showCursor: sanitizedRaw.showCursor ?? fallback.showCursor,
loopCursor: sanitizedRaw.loopCursor ?? fallback.loopCursor,
cursorStyle: sanitizedRaw.cursorStyle ?? fallback.cursorStyle,
cursorSize: sanitizedRaw.cursorSize ?? fallback.cursorSize,
cursorSmoothing: sanitizedRaw.cursorSmoothing ?? fallback.cursorSmoothing,
cursorSpringStiffnessMultiplier:
raw.cursorSpringStiffnessMultiplier ?? fallback.cursorSpringStiffnessMultiplier,
sanitizedRaw.cursorSpringStiffnessMultiplier ??
fallback.cursorSpringStiffnessMultiplier,
cursorSpringDampingMultiplier:
raw.cursorSpringDampingMultiplier ?? fallback.cursorSpringDampingMultiplier,
sanitizedRaw.cursorSpringDampingMultiplier ??
fallback.cursorSpringDampingMultiplier,
cursorSpringMassMultiplier:
raw.cursorSpringMassMultiplier ?? fallback.cursorSpringMassMultiplier,
sanitizedRaw.cursorSpringMassMultiplier ?? fallback.cursorSpringMassMultiplier,
cameraSpringStiffnessMultiplier:
raw.cameraSpringStiffnessMultiplier ?? fallback.cameraSpringStiffnessMultiplier,
sanitizedRaw.cameraSpringStiffnessMultiplier ??
fallback.cameraSpringStiffnessMultiplier,
cameraSpringDampingMultiplier:
raw.cameraSpringDampingMultiplier ?? fallback.cameraSpringDampingMultiplier,
sanitizedRaw.cameraSpringDampingMultiplier ??
fallback.cameraSpringDampingMultiplier,
cameraSpringMassMultiplier:
raw.cameraSpringMassMultiplier ?? fallback.cameraSpringMassMultiplier,
cursorMotionBlur: raw.cursorMotionBlur ?? fallback.cursorMotionBlur,
cursorClickBounce: raw.cursorClickBounce ?? fallback.cursorClickBounce,
sanitizedRaw.cameraSpringMassMultiplier ?? fallback.cameraSpringMassMultiplier,
cursorMotionBlur: sanitizedRaw.cursorMotionBlur ?? fallback.cursorMotionBlur,
cursorClickBounce: sanitizedRaw.cursorClickBounce ?? fallback.cursorClickBounce,
cursorClickBounceDuration:
raw.cursorClickBounceDuration ?? fallback.cursorClickBounceDuration,
cursorSway: raw.cursorSway ?? fallback.cursorSway,
borderRadius: raw.borderRadius ?? fallback.borderRadius,
padding: raw.padding ?? fallback.padding,
frame: raw.frame !== undefined ? raw.frame : fallback.frame,
webcam: raw.webcam ?? fallback.webcam,
aspectRatio: raw.aspectRatio ?? fallback.aspectRatio,
exportEncodingMode: raw.exportEncodingMode ?? fallback.exportEncodingMode,
sanitizedRaw.cursorClickBounceDuration ?? fallback.cursorClickBounceDuration,
cursorSway: sanitizedRaw.cursorSway ?? fallback.cursorSway,
borderRadius: sanitizedRaw.borderRadius ?? fallback.borderRadius,
padding: sanitizedRaw.padding ?? fallback.padding,
frame: sanitizedRaw.frame !== undefined ? sanitizedRaw.frame : fallback.frame,
webcam: sanitizedRaw.webcam ?? fallback.webcam,
aspectRatio: sanitizedRaw.aspectRatio ?? fallback.aspectRatio,
exportEncodingMode:
sanitizedRaw.exportEncodingMode ?? fallback.exportEncodingMode,
exportBackendPreference:
raw.exportBackendPreference === undefined
sanitizedRaw.exportBackendPreference === undefined
? fallback.exportBackendPreference
: normalizeExportBackendPreference(raw.exportBackendPreference),
: normalizeExportBackendPreference(sanitizedRaw.exportBackendPreference),
exportPipelineModel:
raw.exportPipelineModel === undefined
sanitizedRaw.exportPipelineModel === undefined
? fallback.exportPipelineModel
: normalizeExportPipelineModel(raw.exportPipelineModel),
exportQuality: raw.exportQuality ?? fallback.exportQuality,
: normalizeExportPipelineModel(sanitizedRaw.exportPipelineModel),
exportQuality: sanitizedRaw.exportQuality ?? fallback.exportQuality,
mp4FrameRate:
raw.mp4FrameRate === undefined
sanitizedRaw.mp4FrameRate === undefined
? fallback.mp4FrameRate
: normalizeExportMp4FrameRate(raw.mp4FrameRate),
exportFormat: raw.exportFormat ?? fallback.exportFormat,
gifFrameRate: raw.gifFrameRate ?? fallback.gifFrameRate,
gifLoop: raw.gifLoop ?? fallback.gifLoop,
gifSizePreset: raw.gifSizePreset ?? fallback.gifSizePreset,
: normalizeExportMp4FrameRate(sanitizedRaw.mp4FrameRate),
exportFormat: sanitizedRaw.exportFormat ?? fallback.exportFormat,
gifFrameRate: sanitizedRaw.gifFrameRate ?? fallback.gifFrameRate,
gifLoop: sanitizedRaw.gifLoop ?? fallback.gifLoop,
gifSizePreset: sanitizedRaw.gifSizePreset ?? fallback.gifSizePreset,
};
const normalized = normalizeProjectEditor(candidate);
@@ -442,7 +446,10 @@ export function saveEditorPreferences(preferences: Partial<EditorPreferences>):
try {
const current = loadEditorPreferences();
const merged = normalizeEditorPreferences({ ...current, ...preferences }, current);
globalThis.localStorage.setItem(EDITOR_PREFERENCES_STORAGE_KEY, JSON.stringify(merged));
globalThis.localStorage.setItem(
EDITOR_PREFERENCES_STORAGE_KEY,
JSON.stringify(stripPersistedDevMotionBlurSettings(merged)),
);
} catch {
// Ignore storage failures so editor controls still work.
}
@@ -156,6 +156,21 @@ function clamp(value: number, min: number, max: number) {
return Math.min(max, Math.max(min, value));
}
type PersistedDevMotionBlurSettings = {
zoomMotionBlurTuning?: unknown;
};
export function stripPersistedDevMotionBlurSettings<T extends PersistedDevMotionBlurSettings>(
editor: T,
): Omit<T, keyof PersistedDevMotionBlurSettings> {
const {
zoomMotionBlurTuning: _zoomMotionBlurTuning,
...persistedEditor
} = editor;
return persistedEditor;
}
export function normalizeExportEncodingMode(value: unknown): ExportEncodingMode {
if (value === "fast" || value === "balanced" || value === "quality") {
return value;
@@ -360,10 +375,10 @@ export function normalizeProjectEditor(editor: Partial<ProjectEditorState>): Pro
? clamp(rawZoomMotionBlurTuning.zoomVelocityThreshold, 0, 0.4)
: DEFAULT_ZOOM_MOTION_BLUR_TUNING.zoomVelocityThreshold,
maxDirectionalBlurPx: isFiniteNumber(rawZoomMotionBlurTuning.maxDirectionalBlurPx)
? clamp(rawZoomMotionBlurTuning.maxDirectionalBlurPx, 0, 32)
? clamp(rawZoomMotionBlurTuning.maxDirectionalBlurPx, 0, 96)
: DEFAULT_ZOOM_MOTION_BLUR_TUNING.maxDirectionalBlurPx,
maxRadialBlurStrength: isFiniteNumber(rawZoomMotionBlurTuning.maxRadialBlurStrength)
? clamp(rawZoomMotionBlurTuning.maxRadialBlurStrength, 0, 0.5)
? clamp(rawZoomMotionBlurTuning.maxRadialBlurStrength, 0, 1.5)
: DEFAULT_ZOOM_MOTION_BLUR_TUNING.maxRadialBlurStrength,
panResponsePerSecond: isFiniteNumber(rawZoomMotionBlurTuning.panResponsePerSecond)
? clamp(rawZoomMotionBlurTuning.panResponsePerSecond, 1, 30)
+4 -4
View File
@@ -51,7 +51,7 @@ export interface CursorVisualSettings {
}
export type CursorStyle = "macos" | "tahoe" | "tahoe-inverted" | "dot" | "figma" | (string & {}); // extension-contributed cursor styles
export const DEFAULT_CURSOR_STYLE: CursorStyle = "tahoe";
export const DEFAULT_CURSOR_STYLE: CursorStyle = "macos";
export type EditorEffectSection =
| "scene"
@@ -116,9 +116,9 @@ export interface ZoomMotionBlurTuning {
export const DEFAULT_ZOOM_MOTION_BLUR_TUNING: ZoomMotionBlurTuning = {
panVelocityThreshold: 0,
zoomVelocityThreshold: 0.025,
maxDirectionalBlurPx: 11,
maxRadialBlurStrength: 0.175,
zoomVelocityThreshold: 0,
maxDirectionalBlurPx: 41.8,
maxRadialBlurStrength: 1,
panResponsePerSecond: 11,
zoomResponsePerSecond: 9,
zoomSafeZoneRadiusPx: 6,