fix: address PR feedback regressions

This commit is contained in:
webadderall
2026-05-05 16:17:47 +10:00
parent 2c0c436267
commit b773f98ad0
12 changed files with 209 additions and 61 deletions
@@ -1448,9 +1448,9 @@ export function SettingsPanel({
const resetZoomSection = () => {
onZoomTemporalMotionBlurChange?.(initialEditorPreferences.zoomTemporalMotionBlur);
onZoomMotionBlurSampleCountChange?.(initialEditorPreferences.zoomMotionBlurSampleCount);
onZoomMotionBlurSampleCountChange?.(TEMPORAL_MOTION_BLUR_DEFAULT_SAMPLE_COUNT);
onZoomMotionBlurShutterFractionChange?.(
initialEditorPreferences.zoomMotionBlurShutterFraction,
TEMPORAL_MOTION_BLUR_DEFAULT_SHUTTER_FRACTION,
);
onZoomInDurationMsChange?.(initialEditorPreferences.zoomInDurationMs);
onZoomOutDurationMsChange?.(initialEditorPreferences.zoomOutDurationMs);
@@ -1481,7 +1481,6 @@ export function SettingsPanel({
MOTION_PRESET_ORDER.find((presetId) => {
const preset = CURSOR_MOTION_PRESETS[presetId];
return (
preset.zoomSmoothness === DEFAULT_ZOOM_SMOOTHNESS &&
preset.zoomInDurationMs === zoomInDurationMs &&
preset.zoomOutDurationMs === zoomOutDurationMs &&
preset.cursorSize === cursorSize &&
+90 -12
View File
@@ -77,6 +77,7 @@ import {
getAspectRatioLabel,
getAspectRatioValue,
} from "@/utils/aspectRatioUtils";
import { cn } from "@/lib/utils";
import { ExtensionIcon } from "./ExtensionIcon";
const PhCursorFill = (props: { className?: string; weight?: "fill" | "regular" }) => (
@@ -103,7 +104,10 @@ import { resolveAutoCaptionSourcePath } from "./autoCaptionSource";
import { CropControl } from "./CropControl";
import { ExportSettingsMenu } from "./ExportSettingsMenu";
import ExtensionManager from "./ExtensionManager";
import { loadEditorPreferences, saveEditorPreferences } from "./editorPreferences";
import {
loadEditorPreferences,
saveEditorPreferences,
} from "./editorPreferences";
import ProjectBrowserDialog, { type ProjectLibraryEntry } from "./ProjectBrowserDialog";
import {
createProjectData,
@@ -622,6 +626,18 @@ export default function VideoEditor() {
const [cursorSmoothing, setCursorSmoothing] = useState(
initialEditorPreferences.cursorSmoothing,
);
const [cursorSpringStiffnessMultiplier, setCursorSpringStiffnessMultiplier] = useState(
initialEditorPreferences.cursorSpringStiffnessMultiplier,
);
const [cursorSpringDampingMultiplier, setCursorSpringDampingMultiplier] = useState(
initialEditorPreferences.cursorSpringDampingMultiplier,
);
const [cursorSpringMassMultiplier, setCursorSpringMassMultiplier] = useState(
initialEditorPreferences.cursorSpringMassMultiplier,
);
const [sessionShowCursorOverride, setSessionShowCursorOverride] = useState<boolean | null>(
null,
);
const [zoomSmoothness, setZoomSmoothness] = useState(0.5);
const [zoomClassicMode, setZoomClassicMode] = useState(false);
const [cursorMotionBlur, setCursorMotionBlur] = useState(
@@ -685,6 +701,7 @@ export default function VideoEditor() {
const [sourceAudioFallbackPaths, setSourceAudioFallbackPaths] = useState<string[]>([]);
const [sourceAudioFallbackStartDelayMsByPath, setSourceAudioFallbackStartDelayMsByPath] =
useState<Record<string, number>>({});
const effectiveShowCursor = sessionShowCursorOverride ?? showCursor;
const [aspectRatio, setAspectRatio] = useState<AspectRatio>(
initialEditorPreferences.aspectRatio,
);
@@ -917,10 +934,13 @@ export default function VideoEditor() {
previewWidth,
previewHeight,
cursorTelemetry,
showCursor,
effectiveShowCursor,
cursorStyle,
cursorSize,
cursorSmoothing,
cursorSpringStiffnessMultiplier,
cursorSpringDampingMultiplier,
cursorSpringMassMultiplier,
zoomSmoothness,
zoomClassicMode,
cursorMotionBlur,
@@ -996,6 +1016,9 @@ export default function VideoEditor() {
cursorMotionBlur,
cursorSize,
cursorSmoothing,
cursorSpringDampingMultiplier,
cursorSpringMassMultiplier,
cursorSpringStiffnessMultiplier,
zoomSmoothness,
cursorStyle,
cursorSway,
@@ -1004,7 +1027,7 @@ export default function VideoEditor() {
padding,
resolvedWebcamVideoUrl,
shadowIntensity,
showCursor,
effectiveShowCursor,
speedRegions,
wallpaper,
webcam,
@@ -1032,6 +1055,11 @@ export default function VideoEditor() {
}));
}, []);
const handleShowCursorChange = useCallback((nextShowCursor: boolean) => {
setSessionShowCursorOverride(null);
setShowCursor(nextShowCursor);
}, []);
const remountPreview = useCallback(() => {
setIsPreviewReady(false);
setPreviewVersion((version) => version + 1);
@@ -1323,6 +1351,9 @@ export default function VideoEditor() {
cursorStyle: CursorStyle;
cursorSize: number;
cursorSmoothing: number;
cursorSpringStiffnessMultiplier: number;
cursorSpringDampingMultiplier: number;
cursorSpringMassMultiplier: number;
zoomSmoothness: number;
zoomClassicMode: boolean;
cursorMotionBlur: number;
@@ -1471,6 +1502,9 @@ export default function VideoEditor() {
cursorStyle,
cursorSize,
cursorSmoothing,
cursorSpringStiffnessMultiplier,
cursorSpringDampingMultiplier,
cursorSpringMassMultiplier,
zoomSmoothness,
zoomClassicMode,
cursorMotionBlur,
@@ -1521,6 +1555,9 @@ export default function VideoEditor() {
cursorStyle,
cursorSize,
cursorSmoothing,
cursorSpringStiffnessMultiplier,
cursorSpringDampingMultiplier,
cursorSpringMassMultiplier,
zoomSmoothness,
zoomClassicMode,
cursorMotionBlur,
@@ -1694,11 +1731,19 @@ export default function VideoEditor() {
setZoomInEasing(normalizedEditor.zoomInEasing);
setZoomOutEasing(normalizedEditor.zoomOutEasing);
setConnectedZoomEasing(normalizedEditor.connectedZoomEasing);
setSessionShowCursorOverride(null);
setShowCursor(normalizedEditor.showCursor);
setLoopCursor(normalizedEditor.loopCursor);
setCursorStyle(normalizedEditor.cursorStyle);
setCursorSize(normalizedEditor.cursorSize);
setCursorSmoothing(normalizedEditor.cursorSmoothing);
setCursorSpringStiffnessMultiplier(
normalizedEditor.cursorSpringStiffnessMultiplier,
);
setCursorSpringDampingMultiplier(
normalizedEditor.cursorSpringDampingMultiplier,
);
setCursorSpringMassMultiplier(normalizedEditor.cursorSpringMassMultiplier);
setZoomSmoothness(normalizedEditor.zoomSmoothness);
setZoomClassicMode(normalizedEditor.zoomClassicMode);
setCursorMotionBlur(normalizedEditor.cursorMotionBlur);
@@ -2005,9 +2050,9 @@ export default function VideoEditor() {
pendingFreshRecordingAutoZoomPathRef.current = autoApplyFreshRecordingAutoZooms
? sourceVideoUrl
: null;
if (sessionResult.session.hideOverlayCursorByDefault) {
setShowCursor(false);
}
setSessionShowCursorOverride(
sessionResult.session.hideOverlayCursorByDefault ? false : null,
);
setWebcam((prev) => ({
...prev,
enabled: Boolean(sessionResult.session?.webcamPath),
@@ -2027,6 +2072,7 @@ export default function VideoEditor() {
setCurrentProjectPath(null);
setLastSavedSnapshot(null);
pendingFreshRecordingAutoZoomPathRef.current = null;
setSessionShowCursorOverride(null);
setWebcam((prev) => ({
...prev,
enabled: false,
@@ -2097,6 +2143,9 @@ export default function VideoEditor() {
cursorStyle,
cursorSize,
cursorSmoothing,
cursorSpringStiffnessMultiplier,
cursorSpringDampingMultiplier,
cursorSpringMassMultiplier,
cursorMotionBlur,
cursorClickBounce,
cursorClickBounceDuration,
@@ -2138,6 +2187,9 @@ export default function VideoEditor() {
cursorStyle,
cursorSize,
cursorSmoothing,
cursorSpringStiffnessMultiplier,
cursorSpringDampingMultiplier,
cursorSpringMassMultiplier,
cursorMotionBlur,
cursorClickBounce,
cursorClickBounceDuration,
@@ -3975,10 +4027,13 @@ export default function VideoEditor() {
autoCaptionSettings,
zoomRegions: effectiveZoomRegions,
cursorTelemetry: effectiveCursorTelemetry,
showCursor,
effectiveShowCursor,
cursorStyle,
cursorSize,
cursorSmoothing,
cursorSpringStiffnessMultiplier,
cursorSpringDampingMultiplier,
cursorSpringMassMultiplier,
zoomSmoothness,
zoomClassicMode,
cursorMotionBlur,
@@ -4147,10 +4202,13 @@ export default function VideoEditor() {
autoCaptionSettings,
zoomRegions: effectiveZoomRegions,
cursorTelemetry: effectiveCursorTelemetry,
showCursor,
effectiveShowCursor,
cursorStyle,
cursorSize,
cursorSmoothing,
cursorSpringStiffnessMultiplier,
cursorSpringDampingMultiplier,
cursorSpringMassMultiplier,
zoomSmoothness,
zoomClassicMode,
cursorMotionBlur,
@@ -4383,11 +4441,14 @@ export default function VideoEditor() {
zoomInEasing,
zoomOutEasing,
connectedZoomEasing,
showCursor,
effectiveShowCursor,
cursorStyle,
effectiveCursorTelemetry,
cursorSize,
cursorSmoothing,
cursorSpringStiffnessMultiplier,
cursorSpringDampingMultiplier,
cursorSpringMassMultiplier,
zoomSmoothness,
zoomClassicMode,
cursorMotionBlur,
@@ -5255,8 +5316,8 @@ export default function VideoEditor() {
onZoomOutEasingChange={setZoomOutEasing}
connectedZoomEasing={connectedZoomEasing}
onConnectedZoomEasingChange={setConnectedZoomEasing}
showCursor={showCursor}
onShowCursorChange={setShowCursor}
showCursor={effectiveShowCursor}
onShowCursorChange={handleShowCursorChange}
loopCursor={loopCursor}
onLoopCursorChange={setLoopCursor}
cursorStyle={cursorStyle}
@@ -5265,6 +5326,16 @@ export default function VideoEditor() {
onCursorSizeChange={setCursorSize}
cursorSmoothing={cursorSmoothing}
onCursorSmoothingChange={setCursorSmoothing}
cursorSpringStiffnessMultiplier={cursorSpringStiffnessMultiplier}
onCursorSpringStiffnessMultiplierChange={
setCursorSpringStiffnessMultiplier
}
cursorSpringDampingMultiplier={cursorSpringDampingMultiplier}
onCursorSpringDampingMultiplierChange={
setCursorSpringDampingMultiplier
}
cursorSpringMassMultiplier={cursorSpringMassMultiplier}
onCursorSpringMassMultiplierChange={setCursorSpringMassMultiplier}
zoomClassicMode={zoomClassicMode}
onZoomClassicModeChange={setZoomClassicMode}
cursorMotionBlur={cursorMotionBlur}
@@ -5455,10 +5526,17 @@ export default function VideoEditor() {
}
onAnnotationSizeChange={handleAnnotationSizeChange}
cursorTelemetry={effectiveCursorTelemetry}
showCursor={showCursor}
showCursor={effectiveShowCursor}
cursorStyle={cursorStyle}
cursorSize={cursorSize}
cursorSmoothing={cursorSmoothing}
cursorSpringStiffnessMultiplier={
cursorSpringStiffnessMultiplier
}
cursorSpringDampingMultiplier={
cursorSpringDampingMultiplier
}
cursorSpringMassMultiplier={cursorSpringMassMultiplier}
zoomSmoothness={zoomSmoothness}
zoomClassicMode={zoomClassicMode}
cursorMotionBlur={cursorMotionBlur}
@@ -1102,7 +1102,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
return;
}
videoEffectsContainer.filters = null;
videoEffectsContainer.filters = [zoomBlurFilter, motionBlurFilter];
motionBlurFilter.velocity = { x: 0, y: 0 };
motionBlurFilter.kernelSize = 5;
motionBlurFilter.offset = 0;
@@ -1443,6 +1443,10 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
videoEffectsContainerRef.current = videoEffectsContainer;
zoomBlurFilterRef.current = new ZoomBlurFilter({ strength: 0 });
motionBlurFilterRef.current = new MotionBlurFilter([0, 0], 5, 0);
videoEffectsContainer.filters = [
zoomBlurFilterRef.current,
motionBlurFilterRef.current,
];
cameraContainer.addChild(videoEffectsContainer);
syncPreviewMotionBlurQuality();
@@ -443,14 +443,16 @@ export function loadEditorPresets(): EditorPreset[] {
}
}
export function saveEditorPresets(presets: EditorPreset[]): void {
export function saveEditorPresets(presets: EditorPreset[]): boolean {
if (typeof globalThis.localStorage === "undefined") {
return;
return false;
}
try {
globalThis.localStorage.setItem(EDITOR_PRESETS_STORAGE_KEY, JSON.stringify(presets));
return true;
} catch {
// Ignore storage failures so editor controls still work.
return false;
}
}
@@ -1476,21 +1476,18 @@ const TimelineEditor = forwardRef<TimelineEditorHandle, TimelineEditorProps>(
}
const startPos = Math.max(0, Math.min(startMs, totalMs));
const activeClip = clipRegions.find(
(clip) => startPos >= clip.startMs && startPos < clip.endMs,
);
const nextClip = clipRegions.find((clip) => clip.startMs > startPos);
const activeClip =
clipRegions.length === 0
? { startMs: 0, endMs: totalMs }
: clipRegions.find((clip) => startPos >= clip.startMs && startPos < clip.endMs);
if (!activeClip) {
return false;
}
const sorted = [...zoomRegions].sort((a, b) => a.startMs - b.startMs);
const nextRegion = sorted.find((region) => region.startMs > startPos);
const gapToNextClipEdge = clipRegions.length === 0
? totalMs - startPos
: activeClip
? activeClip.endMs - startPos
: nextClip
? nextClip.startMs - startPos
: 0;
const gapToNextRegion = nextRegion ? nextRegion.startMs - startPos : totalMs - startPos;
const gapToNextClipEdge = activeClip.endMs - startPos;
const gapToNextRegion = nextRegion ? nextRegion.startMs - startPos : gapToNextClipEdge;
const availableDuration = Math.min(gapToNextClipEdge, gapToNextRegion);
const isOverlapping = sorted.some(
@@ -72,9 +72,9 @@ describe("computeCursorFollowFocus", () => {
{ snapToEdgesRatio: 0.25 },
);
expect(firstShift.cx).toBeCloseTo(0.7, 6);
expect(firstShift.cx).toBeCloseTo(0.575, 6);
expect(firstShift.cy).toBeCloseTo(0.5, 6);
expect(secondShift.cx).toBeCloseTo(0.7, 6);
expect(secondShift.cx).toBeCloseTo(0.595, 6);
expect(secondShift.cy).toBeCloseTo(0.5, 6);
});
@@ -107,4 +107,45 @@ describe("computeCursorFollowFocus", () => {
expect(clampedFocus).toEqual({ cx: 0.75, cy: 0.75 });
});
it("reinitializes focus when time goes backward during zoom-out", () => {
const state = createCursorFollowCameraState();
const cursorSamples = [
{ timeMs: 0, cx: 0.5, cy: 0.5, interactionType: "move" as const },
{ timeMs: 100, cx: 0.8, cy: 0.5, interactionType: "move" as const },
];
computeCursorFollowFocus(
state,
cursorSamples,
100,
2,
1,
{ cx: 0.5, cy: 0.5 },
{ snapToEdgesRatio: 0.25 },
);
const frozenFocus = computeCursorFollowFocus(
state,
cursorSamples,
140,
2,
0.8,
{ cx: 0.5, cy: 0.5 },
{ snapToEdgesRatio: 0.25 },
);
const rewoundFocus = computeCursorFollowFocus(
state,
cursorSamples,
0,
2,
0.8,
{ cx: 0.5, cy: 0.5 },
{ snapToEdgesRatio: 0.25 },
);
expect(frozenFocus.cx).toBeCloseTo(0.675, 6);
expect(rewoundFocus).toEqual({ cx: 0.5, cy: 0.5 });
});
});
@@ -100,15 +100,15 @@ function recenterFocusWhenCursorLeavesSafeZone(
let nextFocusY = currentFocus.cy;
if (cursorFocus.cx < safeLeft) {
nextFocusX = cursorFocus.cx;
nextFocusX = cursorFocus.cx + halfSpan - safeZoneInset;
} else if (cursorFocus.cx > safeRight) {
nextFocusX = cursorFocus.cx;
nextFocusX = cursorFocus.cx - halfSpan + safeZoneInset;
}
if (cursorFocus.cy < safeTop) {
nextFocusY = cursorFocus.cy;
nextFocusY = cursorFocus.cy + halfSpan - safeZoneInset;
} else if (cursorFocus.cy > safeBottom) {
nextFocusY = cursorFocus.cy;
nextFocusY = cursorFocus.cy - halfSpan + safeZoneInset;
}
return clampFocusToScale(
@@ -157,18 +157,21 @@ export function computeCursorFollowFocus(
: clampedRegionFocus;
}
const timeWentBackwards = state.initialized && timeMs + 0.5 < state.lastTimeMs;
if (timeWentBackwards) {
state.reachedFullZoom = false;
}
// Track when zoom reaches full strength
if (zoomStrength >= 0.99) {
state.reachedFullZoom = true;
}
// Zooming out: was fully zoomed but strength is now dropping — freeze camera
if (state.reachedFullZoom && zoomStrength < 0.99) {
if (!timeWentBackwards && state.reachedFullZoom && zoomStrength < 0.99) {
return { cx: state.frozenFocusX, cy: state.frozenFocusY };
}
const timeWentBackwards = state.initialized && timeMs + 0.5 < state.lastTimeMs;
if (!state.initialized || !state.wasZoomed || timeWentBackwards) {
const initialFocus = recenterFocusWhenCursorLeavesSafeZone(
clampedRegionFocus,
@@ -174,12 +174,13 @@ function computeZoomBlurGeometry({
Math.hypot(focusStagePxX, stageSize.height - focusStagePxY),
Math.hypot(stageSize.width - focusStagePxX, stageSize.height - focusStagePxY),
);
const radius = Math.max(outerRadius, 1);
return {
centerX: focusStagePxX,
centerY: focusStagePxY,
innerRadius: Math.max(18, Math.min(outerRadius - 1, visibleHalfDiagonal)),
radius: Math.max(outerRadius, 1),
innerRadius: Math.max(0, Math.min(Math.max(18, Math.min(radius - 1, visibleHalfDiagonal)), radius - 1)),
radius,
};
}
+21 -14
View File
@@ -1103,6 +1103,8 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
}
}
hideEditorOverlayCursorByDefault.current = true;
const wantsAudioCapture = microphoneEnabled || systemAudioEnabled;
const browserCaptureSource = await resolveBrowserCaptureSource(selectedSource);
@@ -1258,20 +1260,25 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
stream.current.addTrack(micAudioTrack);
}
} else {
const mediaStream = await mediaDevices.getDisplayMedia({
audio: false,
video: {
displaySurface: selectedSource.id?.startsWith("window:")
? "window"
: "monitor",
width: { ideal: TARGET_WIDTH, max: TARGET_WIDTH },
height: { ideal: TARGET_HEIGHT, max: TARGET_HEIGHT },
frameRate: { ideal: TARGET_FRAME_RATE, max: TARGET_FRAME_RATE },
cursor: "never",
},
selfBrowserSurface: "exclude",
surfaceSwitching: "exclude",
});
const mediaStream = useLinuxPortal
? await mediaDevices.getDisplayMedia({
audio: false,
video: {
displaySurface: selectedSource.id?.startsWith("window:")
? "window"
: "monitor",
width: { ideal: TARGET_WIDTH, max: TARGET_WIDTH },
height: { ideal: TARGET_HEIGHT, max: TARGET_HEIGHT },
frameRate: { ideal: TARGET_FRAME_RATE, max: TARGET_FRAME_RATE },
cursor: "never",
},
selfBrowserSurface: "exclude",
surfaceSwitching: "exclude",
})
: await mediaDevices.getUserMedia({
audio: false,
video: browserScreenVideoConstraints,
});
stream.current = mediaStream;
videoTrack = mediaStream.getVideoTracks()[0];
+13
View File
@@ -234,6 +234,7 @@ interface CaptionRenderState {
interface RenderSnapshot {
timeMs: number;
cursorTimeMs: number;
backgroundTimelineTimeMs: number;
sceneTransform: { scale: number; x: number; y: number };
zoom: { scale: number; focusX: number; focusY: number; progress: number };
}
@@ -2550,6 +2551,7 @@ export class FrameRenderer {
return {
timeMs,
cursorTimeMs,
backgroundTimelineTimeMs: backgroundTimelineTimestamp / 1000,
sceneTransform: {
scale: this.animationState.appliedScale,
x: this.animationState.x,
@@ -2635,6 +2637,17 @@ export class FrameRenderer {
return null;
}
if (resolvedSnapshot !== lastSnapshot) {
await this.renderSceneSample(
Math.round(resolvedSnapshot.timeMs * 1000),
Math.round(resolvedSnapshot.cursorTimeMs * 1000),
Math.round(resolvedSnapshot.backgroundTimelineTimeMs * 1000),
layoutCache,
true,
false,
);
}
const hasOverlayCanvasWork =
(this.config.annotationRegions?.length ?? 0) > 0 ||
Boolean(this.captionCanvas && this.captionSprite?.visible);