From 2977fbcc9c0355f3b7e4f25d0d646cbbae848fb6 Mon Sep 17 00:00:00 2001 From: webadderall <131426131+webadderall@users.noreply.github.com> Date: Fri, 8 May 2026 14:27:30 +1000 Subject: [PATCH] Polish timeline markers and normalize motion presets --- src/components/video-editor/SettingsPanel.tsx | 33 +++---- src/components/video-editor/audio.test.ts | 55 ++++++++++++ .../video-editor/cursorMotionPresets.ts | 46 +++++++++- .../video-editor/projectPersistence.ts | 86 +++++++++++-------- .../video-editor/timeline/TimelineEditor.tsx | 3 +- 5 files changed, 168 insertions(+), 55 deletions(-) diff --git a/src/components/video-editor/SettingsPanel.tsx b/src/components/video-editor/SettingsPanel.tsx index 5e594d95..c0492821 100644 --- a/src/components/video-editor/SettingsPanel.tsx +++ b/src/components/video-editor/SettingsPanel.tsx @@ -45,7 +45,11 @@ import { useI18n, useScopedT } from "../../contexts/I18nContext"; import type { AppLocale } from "../../i18n/config"; import { SUPPORTED_LOCALES } from "../../i18n/config"; import { AnnotationSettingsPanel } from "./AnnotationSettingsPanel"; -import { CURSOR_MOTION_PRESETS, type CursorMotionPresetId } from "./cursorMotionPresets"; +import { + CURSOR_MOTION_PRESETS, + getMatchingCursorMotionPresetId, + type CursorMotionPresetId, +} from "./cursorMotionPresets"; import { loadEditorPreferences, saveEditorPreferences } from "./editorPreferences"; import { SliderControl } from "./SliderControl"; import { KeyboardShortcutsDialog } from "./TutorialHelp"; @@ -1515,21 +1519,18 @@ export function SettingsPanel({ const activeMotionPresetId = useMemo(() => { return ( - MOTION_PRESET_ORDER.find((presetId) => { - const preset = CURSOR_MOTION_PRESETS[presetId]; - return ( - preset.zoomInDurationMs === zoomInDurationMs && - preset.zoomOutDurationMs === zoomOutDurationMs && - preset.cursorSize === cursorSize && - preset.cursorSmoothing === cursorSmoothing && - preset.cursorSpringStiffnessMultiplier === cursorSpringStiffnessMultiplier && - preset.cursorSpringDampingMultiplier === cursorSpringDampingMultiplier && - preset.cursorSpringMassMultiplier === cursorSpringMassMultiplier && - preset.cursorMotionBlur === cursorMotionBlur && - preset.cursorClickBounce === cursorClickBounce && - preset.cursorClickBounceDuration === cursorClickBounceDuration - ); - }) ?? null + getMatchingCursorMotionPresetId({ + zoomInDurationMs, + zoomOutDurationMs, + cursorSize, + cursorSmoothing, + cursorSpringStiffnessMultiplier, + cursorSpringDampingMultiplier, + cursorSpringMassMultiplier, + cursorMotionBlur, + cursorClickBounce, + cursorClickBounceDuration, + }) ?? "focused" ); }, [ cursorClickBounce, diff --git a/src/components/video-editor/audio.test.ts b/src/components/video-editor/audio.test.ts index 60a46f8a..b0510396 100644 --- a/src/components/video-editor/audio.test.ts +++ b/src/components/video-editor/audio.test.ts @@ -1,5 +1,6 @@ import * as fc from "fast-check"; import { describe, expect, it } from "vitest"; +import { CURSOR_MOTION_PRESETS } from "./cursorMotionPresets"; import { fromFileUrl, normalizeProjectEditor, toFileUrl } from "./projectPersistence"; describe("Audio path handling", () => { @@ -235,3 +236,57 @@ describe("Audio region normalization", () => { }); }); }); + +describe("Motion preset normalization", () => { + it("preserves the smooth preset when the saved values match it exactly", () => { + const smooth = CURSOR_MOTION_PRESETS.smooth; + const result = normalizeProjectEditor({ + zoomInDurationMs: smooth.zoomInDurationMs, + zoomOutDurationMs: smooth.zoomOutDurationMs, + cursorSize: smooth.cursorSize, + cursorSmoothing: smooth.cursorSmoothing, + cursorSpringStiffnessMultiplier: smooth.cursorSpringStiffnessMultiplier, + cursorSpringDampingMultiplier: smooth.cursorSpringDampingMultiplier, + cursorSpringMassMultiplier: smooth.cursorSpringMassMultiplier, + cursorMotionBlur: smooth.cursorMotionBlur, + cursorClickBounce: smooth.cursorClickBounce, + cursorClickBounceDuration: smooth.cursorClickBounceDuration, + } as any); + + expect(result.zoomInDurationMs).toBe(smooth.zoomInDurationMs); + expect(result.zoomOutDurationMs).toBe(smooth.zoomOutDurationMs); + expect(result.cursorSpringStiffnessMultiplier).toBe( + smooth.cursorSpringStiffnessMultiplier, + ); + expect(result.cursorSpringDampingMultiplier).toBe(smooth.cursorSpringDampingMultiplier); + }); + + it("falls back to focused when the saved values do not match a supported preset", () => { + const focused = CURSOR_MOTION_PRESETS.focused; + const result = normalizeProjectEditor({ + zoomInDurationMs: 275, + zoomOutDurationMs: 410, + cursorSize: 3.25, + cursorSmoothing: 0.52, + cursorSpringStiffnessMultiplier: 1.1, + cursorSpringDampingMultiplier: 1.05, + cursorSpringMassMultiplier: 1.6, + cursorMotionBlur: 0.9, + cursorClickBounce: 2.25, + cursorClickBounceDuration: 280, + } as any); + + expect(result.zoomInDurationMs).toBe(focused.zoomInDurationMs); + expect(result.zoomOutDurationMs).toBe(focused.zoomOutDurationMs); + expect(result.cursorSize).toBe(focused.cursorSize); + expect(result.cursorSmoothing).toBe(focused.cursorSmoothing); + expect(result.cursorSpringStiffnessMultiplier).toBe( + focused.cursorSpringStiffnessMultiplier, + ); + expect(result.cursorSpringDampingMultiplier).toBe(focused.cursorSpringDampingMultiplier); + expect(result.cursorSpringMassMultiplier).toBe(focused.cursorSpringMassMultiplier); + expect(result.cursorMotionBlur).toBe(focused.cursorMotionBlur); + expect(result.cursorClickBounce).toBe(focused.cursorClickBounce); + expect(result.cursorClickBounceDuration).toBe(focused.cursorClickBounceDuration); + }); +}); diff --git a/src/components/video-editor/cursorMotionPresets.ts b/src/components/video-editor/cursorMotionPresets.ts index 0338fddf..8563d40e 100644 --- a/src/components/video-editor/cursorMotionPresets.ts +++ b/src/components/video-editor/cursorMotionPresets.ts @@ -18,6 +18,19 @@ export interface CursorMotionPreset { cursorClickBounceDuration: number; } +export interface CursorMotionPresetSelectionInput { + zoomInDurationMs: number; + zoomOutDurationMs: number; + cursorSize: number; + cursorSmoothing: number; + cursorSpringStiffnessMultiplier: number; + cursorSpringDampingMultiplier: number; + cursorSpringMassMultiplier: number; + cursorMotionBlur: number; + cursorClickBounce: number; + cursorClickBounceDuration: number; +} + const SHARED_CURSOR_PRESET_VALUES = { cursorSize: 2.5, cursorSmoothing: 0.67, @@ -48,4 +61,35 @@ export const CURSOR_MOTION_PRESETS: Record): Pro ? "tahoe-inverted" : editor.cursorStyle : DEFAULT_CURSOR_STYLE; - - return { - wallpaper: typeof editor.wallpaper === "string" ? editor.wallpaper : DEFAULT_WALLPAPER_PATH, - shadowIntensity: typeof editor.shadowIntensity === "number" ? editor.shadowIntensity : 0.67, - backgroundBlur: normalizedBackgroundBlur, - zoomMotionBlur: normalizedZoomMotionBlur, - zoomMotionBlurTuning: normalizedZoomMotionBlurTuning, - zoomTemporalMotionBlur: normalizedZoomTemporalMotionBlur, - zoomMotionBlurSampleCount: normalizedZoomMotionBlurSampleCount, - zoomMotionBlurShutterFraction: normalizedZoomMotionBlurShutterFraction, - connectZooms: typeof editor.connectZooms === "boolean" ? editor.connectZooms : true, + const normalizedMotionValues = { zoomInDurationMs: normalizedZoomInDurationMs, - zoomInOverlapMs: normalizedZoomInOverlapMs, zoomOutDurationMs: normalizedZoomOutDurationMs, - connectedZoomGapMs: normalizedConnectedZoomGapMs, - connectedZoomDurationMs: normalizedConnectedZoomDurationMs, - zoomInEasing: normalizeZoomTransitionEasing(editor.zoomInEasing, DEFAULT_ZOOM_IN_EASING), - zoomOutEasing: normalizeZoomTransitionEasing(editor.zoomOutEasing, DEFAULT_ZOOM_OUT_EASING), - connectedZoomEasing: normalizeZoomTransitionEasing( - editor.connectedZoomEasing, - DEFAULT_CONNECTED_ZOOM_EASING, - ), - showCursor: typeof editor.showCursor === "boolean" ? editor.showCursor : true, - loopCursor: typeof editor.loopCursor === "boolean" ? editor.loopCursor : false, - cursorStyle: normalizedCursorStyle, cursorSize: isFiniteNumber(editor.cursorSize) ? clamp(editor.cursorSize, 0.5, 10) : DEFAULT_MOTION_PRESET.cursorSize, @@ -837,18 +815,6 @@ export function normalizeProjectEditor(editor: Partial): Pro cursorSpringMassMultiplier: isFiniteNumber(editor.cursorSpringMassMultiplier) ? clamp(editor.cursorSpringMassMultiplier, 0.25, 3) : DEFAULT_MOTION_PRESET.cursorSpringMassMultiplier, - cameraSpringStiffnessMultiplier: isFiniteNumber(editor.cameraSpringStiffnessMultiplier) - ? clamp(editor.cameraSpringStiffnessMultiplier, 0.25, 3) - : 1, - cameraSpringDampingMultiplier: isFiniteNumber(editor.cameraSpringDampingMultiplier) - ? clamp(editor.cameraSpringDampingMultiplier, 0.25, 3) - : 1, - cameraSpringMassMultiplier: isFiniteNumber(editor.cameraSpringMassMultiplier) - ? clamp(editor.cameraSpringMassMultiplier, 0.25, 3) - : 1, - zoomSmoothness: DEFAULT_ZOOM_SMOOTHNESS, - zoomClassicMode: - typeof editor.zoomClassicMode === "boolean" ? editor.zoomClassicMode : false, cursorMotionBlur: isFiniteNumber((editor as Partial).cursorMotionBlur) ? clamp((editor as Partial).cursorMotionBlur as number, 0, 2) : DEFAULT_MOTION_PRESET.cursorMotionBlur, @@ -864,6 +830,54 @@ export function normalizeProjectEditor(editor: Partial): Pro 500, ) : DEFAULT_MOTION_PRESET.cursorClickBounceDuration, + }; + const normalizedMotionPreset = + CURSOR_MOTION_PRESETS[resolveCursorMotionPresetId(normalizedMotionValues)]; + + return { + wallpaper: typeof editor.wallpaper === "string" ? editor.wallpaper : DEFAULT_WALLPAPER_PATH, + shadowIntensity: typeof editor.shadowIntensity === "number" ? editor.shadowIntensity : 0.67, + 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, + zoomOutDurationMs: normalizedMotionPreset.zoomOutDurationMs, + connectedZoomGapMs: normalizedConnectedZoomGapMs, + connectedZoomDurationMs: normalizedConnectedZoomDurationMs, + zoomInEasing: normalizeZoomTransitionEasing(editor.zoomInEasing, DEFAULT_ZOOM_IN_EASING), + zoomOutEasing: normalizeZoomTransitionEasing(editor.zoomOutEasing, DEFAULT_ZOOM_OUT_EASING), + connectedZoomEasing: normalizeZoomTransitionEasing( + editor.connectedZoomEasing, + DEFAULT_CONNECTED_ZOOM_EASING, + ), + showCursor: typeof editor.showCursor === "boolean" ? editor.showCursor : true, + loopCursor: typeof editor.loopCursor === "boolean" ? editor.loopCursor : false, + cursorStyle: normalizedCursorStyle, + cursorSize: normalizedMotionPreset.cursorSize, + cursorSmoothing: normalizedMotionPreset.cursorSmoothing, + cursorSpringStiffnessMultiplier: normalizedMotionPreset.cursorSpringStiffnessMultiplier, + cursorSpringDampingMultiplier: normalizedMotionPreset.cursorSpringDampingMultiplier, + cursorSpringMassMultiplier: normalizedMotionPreset.cursorSpringMassMultiplier, + cameraSpringStiffnessMultiplier: isFiniteNumber(editor.cameraSpringStiffnessMultiplier) + ? clamp(editor.cameraSpringStiffnessMultiplier, 0.25, 3) + : 1, + cameraSpringDampingMultiplier: isFiniteNumber(editor.cameraSpringDampingMultiplier) + ? clamp(editor.cameraSpringDampingMultiplier, 0.25, 3) + : 1, + cameraSpringMassMultiplier: isFiniteNumber(editor.cameraSpringMassMultiplier) + ? clamp(editor.cameraSpringMassMultiplier, 0.25, 3) + : 1, + zoomSmoothness: DEFAULT_ZOOM_SMOOTHNESS, + zoomClassicMode: + typeof editor.zoomClassicMode === "boolean" ? editor.zoomClassicMode : false, + cursorMotionBlur: normalizedMotionPreset.cursorMotionBlur, + cursorClickBounce: normalizedMotionPreset.cursorClickBounce, + cursorClickBounceDuration: normalizedMotionPreset.cursorClickBounceDuration, cursorSway: isFiniteNumber((editor as Partial).cursorSway) ? clamp((editor as Partial).cursorSway as number, 0, 2) : DEFAULT_CURSOR_SWAY, diff --git a/src/components/video-editor/timeline/TimelineEditor.tsx b/src/components/video-editor/timeline/TimelineEditor.tsx index 740d01cb..f58353a2 100644 --- a/src/components/video-editor/timeline/TimelineEditor.tsx +++ b/src/components/video-editor/timeline/TimelineEditor.tsx @@ -585,7 +585,7 @@ function ClipMarkerOverlay({ videoDurationMs }: { videoDurationMs: number }) { bottom: "7.5%", [sideProperty]: `${offset}px`, background: - "linear-gradient(to bottom, transparent 0%, rgba(255,255,255,0.10) 35%, rgba(255,255,255,0.10) 65%, transparent 100%)", + "linear-gradient(to bottom, transparent 0%, rgba(255,255,255,0.32) 35%, rgba(255,255,255,0.32) 65%, transparent 100%)", }} /> ))} @@ -858,7 +858,6 @@ function Timeline({ onMouseMove={handleTimelineMouseMove} onMouseLeave={handleTimelineMouseLeave} > -