mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-27 00:05:39 +00:00
Polish timeline markers and normalize motion presets
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<CursorMotionPresetId, CursorMotionPre
|
||||
cursorSpringStiffnessMultiplier: 0.92,
|
||||
cursorSpringDampingMultiplier: 1.36,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export function getMatchingCursorMotionPresetId(
|
||||
values: CursorMotionPresetSelectionInput,
|
||||
): CursorMotionPresetId | null {
|
||||
for (const presetId of Object.keys(CURSOR_MOTION_PRESETS) as CursorMotionPresetId[]) {
|
||||
const preset = CURSOR_MOTION_PRESETS[presetId];
|
||||
if (
|
||||
preset.zoomInDurationMs === values.zoomInDurationMs &&
|
||||
preset.zoomOutDurationMs === values.zoomOutDurationMs &&
|
||||
preset.cursorSize === values.cursorSize &&
|
||||
preset.cursorSmoothing === values.cursorSmoothing &&
|
||||
preset.cursorSpringStiffnessMultiplier === values.cursorSpringStiffnessMultiplier &&
|
||||
preset.cursorSpringDampingMultiplier === values.cursorSpringDampingMultiplier &&
|
||||
preset.cursorSpringMassMultiplier === values.cursorSpringMassMultiplier &&
|
||||
preset.cursorMotionBlur === values.cursorMotionBlur &&
|
||||
preset.cursorClickBounce === values.cursorClickBounce &&
|
||||
preset.cursorClickBounceDuration === values.cursorClickBounceDuration
|
||||
) {
|
||||
return presetId;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function resolveCursorMotionPresetId(
|
||||
values: CursorMotionPresetSelectionInput,
|
||||
fallback: CursorMotionPresetId = "focused",
|
||||
): CursorMotionPresetId {
|
||||
return getMatchingCursorMotionPresetId(values) ?? fallback;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
} from "@/lib/exporter/temporalMotionBlur";
|
||||
import { DEFAULT_WALLPAPER_PATH } from "@/lib/wallpapers";
|
||||
import { ASPECT_RATIOS, type AspectRatio, isCustomAspectRatio } from "@/utils/aspectRatioUtils";
|
||||
import { CURSOR_MOTION_PRESETS } from "./cursorMotionPresets";
|
||||
import { CURSOR_MOTION_PRESETS, resolveCursorMotionPresetId } from "./cursorMotionPresets";
|
||||
import {
|
||||
type AnnotationRegion,
|
||||
type AudioRegion,
|
||||
@@ -797,31 +797,9 @@ export function normalizeProjectEditor(editor: Partial<ProjectEditorState>): 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<ProjectEditorState>): 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<ProjectEditorState>).cursorMotionBlur)
|
||||
? clamp((editor as Partial<ProjectEditorState>).cursorMotionBlur as number, 0, 2)
|
||||
: DEFAULT_MOTION_PRESET.cursorMotionBlur,
|
||||
@@ -864,6 +830,54 @@ export function normalizeProjectEditor(editor: Partial<ProjectEditorState>): 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<ProjectEditorState>).cursorSway)
|
||||
? clamp((editor as Partial<ProjectEditorState>).cursorSway as number, 0, 2)
|
||||
: DEFAULT_CURSOR_SWAY,
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
<div className="absolute inset-0 bg-[linear-gradient(to_right,hsl(var(--foreground)/0.03)_1px,transparent_1px)] bg-[length:20px_100%] pointer-events-none" />
|
||||
<TimelineAxis videoDurationMs={videoDurationMs} currentTimeMs={currentTimeMs} />
|
||||
<PlaybackCursor
|
||||
currentTimeMs={currentTimeMs}
|
||||
|
||||
Reference in New Issue
Block a user