From 60e186234b34f58dd9766d30fa851df0acfd5daf Mon Sep 17 00:00:00 2001 From: webadderall <131426131+webadderall@users.noreply.github.com> Date: Wed, 26 Aug 2026 21:23:03 +1000 Subject: [PATCH] Polish editor preview and motion blur --- src/components/video-editor/VideoEditor.tsx | 4 +- .../video-editor/cursorMotionPresets.ts | 8 +- src/components/video-editor/types.ts | 6 +- .../videoPlayback/cursorRenderer.ts | 24 +++--- .../videoPlayback/zoomTransform.test.ts | 19 ++++- .../videoPlayback/zoomTransform.ts | 85 +++++++++++-------- src/index.css | 5 +- 7 files changed, 91 insertions(+), 60 deletions(-) diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx index c2e16ed6..b19280bc 100644 --- a/src/components/video-editor/VideoEditor.tsx +++ b/src/components/video-editor/VideoEditor.tsx @@ -5573,7 +5573,7 @@ export default function VideoEditor() { zoomInEasing={zoomInEasing} zoomOutEasing={zoomOutEasing} connectedZoomEasing={connectedZoomEasing} - borderRadius={borderRadius} + borderRadius={0} padding={padding} frame={frame} cropRegion={cropRegion} @@ -5911,7 +5911,7 @@ export default function VideoEditor() { )}
diff --git a/src/components/video-editor/cursorMotionPresets.ts b/src/components/video-editor/cursorMotionPresets.ts index 8563d40e..04e9745d 100644 --- a/src/components/video-editor/cursorMotionPresets.ts +++ b/src/components/video-editor/cursorMotionPresets.ts @@ -1,4 +1,8 @@ -import { DEFAULT_ZOOM_IN_DURATION_MS, DEFAULT_ZOOM_OUT_DURATION_MS } from "./types"; +import { + DEFAULT_CURSOR_MOTION_BLUR, + DEFAULT_ZOOM_IN_DURATION_MS, + DEFAULT_ZOOM_OUT_DURATION_MS, +} from "./types"; export type CursorMotionPresetId = "focused" | "smooth"; @@ -35,7 +39,7 @@ const SHARED_CURSOR_PRESET_VALUES = { cursorSize: 2.5, cursorSmoothing: 0.67, cursorSpringMassMultiplier: 1.29, - cursorMotionBlur: 0.4, + cursorMotionBlur: DEFAULT_CURSOR_MOTION_BLUR, cursorClickBounce: 3.5, cursorClickBounceDuration: 350, } as const; diff --git a/src/components/video-editor/types.ts b/src/components/video-editor/types.ts index 98eefb3f..75fd2dc9 100644 --- a/src/components/video-editor/types.ts +++ b/src/components/video-editor/types.ts @@ -147,7 +147,7 @@ export interface WebcamOverlaySettings { export const DEFAULT_CURSOR_SIZE = 3.0; export const DEFAULT_CURSOR_SMOOTHING = 0.67; -export const DEFAULT_CURSOR_MOTION_BLUR = 0.4; +export const DEFAULT_CURSOR_MOTION_BLUR = 0.6; export const DEFAULT_CURSOR_CLICK_BOUNCE = 2.5; export const DEFAULT_CURSOR_CLICK_BOUNCE_DURATION = 350; export const DEFAULT_CURSOR_SWAY = 0.4; @@ -435,11 +435,11 @@ export interface AnnotationTextStyle { } function getDefaultAnnotationFontFamily() { - return '"SF Pro Display", "SF Pro Text", Helvetica, sans-serif'; + return '"SF Pro Display", "SF Pro Text", "Helvetica Neue", sans-serif'; } export function getDefaultCaptionFontFamily() { - return '"SF Pro Text", "SF Pro Display", Helvetica, sans-serif'; + return '"SF Pro Text", "SF Pro Display", "Helvetica Neue", sans-serif'; } export interface AnnotationRegion { diff --git a/src/components/video-editor/videoPlayback/cursorRenderer.ts b/src/components/video-editor/videoPlayback/cursorRenderer.ts index c10b915d..22156e24 100644 --- a/src/components/video-editor/videoPlayback/cursorRenderer.ts +++ b/src/components/video-editor/videoPlayback/cursorRenderer.ts @@ -27,6 +27,7 @@ import { stepSpringValue, } from "./motionSmoothing"; import { cursorSetAssets, getCursorStyleSizeMultiplier } from "./uploadedCursorAssets"; +import { computeDirectionalMotionBlur } from "./zoomTransform"; type CursorAssetKey = NonNullable; type StatefulCursorStyle = Extract; @@ -112,7 +113,9 @@ export interface CursorRenderConfig { } const MIN_CURSOR_VIEWPORT_SCALE = 0; -const CURSOR_MOTION_BLUR_BASE_MULTIPLIER = 0.08; +// Preserve the cursor's stronger visual response while sharing the camera's +// frame-rate normalization, kernel, and directional offset formula. +const CURSOR_DIRECTIONAL_BLUR_STRENGTH = 4.8; const CURSOR_TIME_DISCONTINUITY_MS = 100; const CURSOR_SWAY_SMOOTHING_MULTIPLIER = 0.7; const CURSOR_SWAY_SMOOTHING_OFFSET = 0.18; @@ -1569,17 +1572,16 @@ export class PixiCursorOverlay { const deltaMs = Math.max(1, timeMs - this.lastRenderedTimeMs); const dx = px - this.lastRenderedPoint.px; const dy = py - this.lastRenderedPoint.py; - const velocityScale = - (1000 / deltaMs) * this.config.motionBlur * CURSOR_MOTION_BLUR_BASE_MULTIPLIER; - const velocity = { - x: dx * velocityScale, - y: dy * velocityScale, - }; - const magnitude = Math.hypot(velocity.x, velocity.y); + const blur = computeDirectionalMotionBlur( + { x: dx, y: dy }, + this.config.motionBlur, + deltaMs / 1000, + CURSOR_DIRECTIONAL_BLUR_STRENGTH, + ); - this.cursorMotionBlurFilter.velocity = magnitude > 0.05 ? velocity : { x: 0, y: 0 }; - this.cursorMotionBlurFilter.kernelSize = magnitude > 3 ? 9 : magnitude > 1 ? 7 : 5; - this.cursorMotionBlurFilter.offset = magnitude > 0.5 ? -0.25 : 0; + this.cursorMotionBlurFilter.velocity = blur.velocity; + this.cursorMotionBlurFilter.kernelSize = blur.kernelSize; + this.cursorMotionBlurFilter.offset = blur.offset; } reset(): void { diff --git a/src/components/video-editor/videoPlayback/zoomTransform.test.ts b/src/components/video-editor/videoPlayback/zoomTransform.test.ts index 6031977a..faf5f6e1 100644 --- a/src/components/video-editor/videoPlayback/zoomTransform.test.ts +++ b/src/components/video-editor/videoPlayback/zoomTransform.test.ts @@ -1,5 +1,10 @@ import { describe, expect, it } from "vitest"; -import { applyZoomTransform, computeZoomTransform, createMotionBlurState } from "./zoomTransform"; +import { + applyZoomTransform, + computeDirectionalMotionBlur, + computeZoomTransform, + createMotionBlurState, +} from "./zoomTransform"; function createStubContainer() { return { @@ -8,6 +13,18 @@ function createStubContainer() { }; } +describe("computeDirectionalMotionBlur", () => { + it("normalizes cursor and camera motion to the same 60fps baseline", () => { + const at60Fps = computeDirectionalMotionBlur({ x: 12, y: 0 }, 0.5, 1 / 60); + const at30Fps = computeDirectionalMotionBlur({ x: 12, y: 0 }, 0.5, 1 / 30); + + expect(at60Fps.velocity).toEqual({ x: 6, y: 0 }); + expect(at30Fps.velocity).toEqual({ x: 3, y: 0 }); + expect(at60Fps.kernelSize).toBe(13); + expect(at60Fps.offset).toBeCloseTo(-6 / 32, 6); + }); +}); + describe("applyZoomTransform motion blur routing", () => { it("keeps pure translation on the directional blur path", () => { const motionBlurState = createMotionBlurState(); diff --git a/src/components/video-editor/videoPlayback/zoomTransform.ts b/src/components/video-editor/videoPlayback/zoomTransform.ts index 65fe8ce7..26ff84a5 100644 --- a/src/components/video-editor/videoPlayback/zoomTransform.ts +++ b/src/components/video-editor/videoPlayback/zoomTransform.ts @@ -7,6 +7,13 @@ const MIN_DIRECTIONAL_BLUR_MAGNITUDE = 0.01; const DIRECTIONAL_BLUR_KERNEL_SIZE = 13; const DIRECTIONAL_BLUR_OFFSET_DIVISOR = 32; +export interface DirectionalMotionBlur { + velocity: { x: number; y: number }; + magnitude: number; + kernelSize: number; + offset: number; +} + export interface MotionBlurState { lastFrameTimeMs: number; prevCamX: number; @@ -247,25 +254,40 @@ function resolveFpsScale(deltaSeconds: number) { return fps / 60; } -function resolveBlurChannels( +export function computeDirectionalMotionBlur( + delta: { x: number; y: number }, + motionBlurAmount: number, + deltaSeconds: number, + strengthMultiplier = 1, +): DirectionalMotionBlur { + const scale = motionBlurAmount * resolveFpsScale(deltaSeconds) * strengthMultiplier; + const velocity = { + x: delta.x * scale, + y: delta.y * scale, + }; + const magnitude = Math.hypot(velocity.x, velocity.y); + const isActive = magnitude >= MIN_DIRECTIONAL_BLUR_MAGNITUDE; + + return { + velocity: isActive ? velocity : createZeroPoint(), + magnitude: isActive ? magnitude : 0, + kernelSize: DIRECTIONAL_BLUR_KERNEL_SIZE, + offset: isActive ? -magnitude / DIRECTIONAL_BLUR_OFFSET_DIVISOR : 0, + }; +} + +function resolveZoomBlurStrength( motionBlurAmount: number, motionBlurTuning: ZoomMotionBlurTuning, deltaSeconds: number, ) { const fpsScale = resolveFpsScale(deltaSeconds); - - return { - motion: - motionBlurAmount * - fpsScale * - (motionBlurTuning.maxDirectionalBlurPx / - DEFAULT_ZOOM_MOTION_BLUR_TUNING.maxDirectionalBlurPx), - zoom: - motionBlurAmount * - fpsScale * - (motionBlurTuning.maxRadialBlurStrength / - DEFAULT_ZOOM_MOTION_BLUR_TUNING.maxRadialBlurStrength), - }; + return ( + motionBlurAmount * + fpsScale * + (motionBlurTuning.maxRadialBlurStrength / + DEFAULT_ZOOM_MOTION_BLUR_TUNING.maxRadialBlurStrength) + ); } function computeMoveDelta(previousQuad: TransformQuad, currentQuad: TransformQuad) { @@ -340,40 +362,29 @@ function analyzeCameraStep({ motionBlurTuning: ZoomMotionBlurTuning; deltaSeconds: number; }): CameraStepAnalysis { - const mode = classifyMotionMode( - previousQuad, - currentQuad, - motionBlurTuning, - deltaSeconds, - ); + const mode = classifyMotionMode(previousQuad, currentQuad, motionBlurTuning, deltaSeconds); const moveDelta = computeMoveDelta(previousQuad, currentQuad); - const blurChannels = resolveBlurChannels( + const zoomBlurStrength = resolveZoomBlurStrength( motionBlurAmount, motionBlurTuning, deltaSeconds, ); - const moveBlurVelocity = { - x: moveDelta.x * blurChannels.motion, - y: moveDelta.y * blurChannels.motion, - }; - const moveBlurMagnitude = Math.hypot(moveBlurVelocity.x, moveBlurVelocity.y); + const directionalBlur = computeDirectionalMotionBlur( + moveDelta, + motionBlurAmount, + deltaSeconds, + motionBlurTuning.maxDirectionalBlurPx / + DEFAULT_ZOOM_MOTION_BLUR_TUNING.maxDirectionalBlurPx, + ); return { mode, moveVelocity: moveDelta, - moveBlurVelocity: - mode === "move" && moveBlurMagnitude >= MIN_DIRECTIONAL_BLUR_MAGNITUDE - ? moveBlurVelocity - : createZeroPoint(), - moveBlurOffset: - mode === "move" && moveBlurMagnitude >= MIN_DIRECTIONAL_BLUR_MAGNITUDE - ? -moveBlurMagnitude / DIRECTIONAL_BLUR_OFFSET_DIVISOR - : 0, + moveBlurVelocity: mode === "move" ? directionalBlur.velocity : createZeroPoint(), + moveBlurOffset: mode === "move" ? directionalBlur.offset : 0, zoomCenter: inferZoomCenterFromQuads(previousQuad, currentQuad, stageSize), zoomStrength: - mode === "zoom" - ? computeZoomStrength(previousQuad, currentQuad) * blurChannels.zoom - : 0, + mode === "zoom" ? computeZoomStrength(previousQuad, currentQuad) * zoomBlurStrength : 0, }; } diff --git a/src/index.css b/src/index.css index a5ae2368..afbb98aa 100644 --- a/src/index.css +++ b/src/index.css @@ -29,7 +29,7 @@ } :root { - --app-font-sans: "SF Pro Display", "SF Pro Text", Helvetica, sans-serif; + --app-font-sans: "SF Pro Display", "SF Pro Text", "Helvetica Neue", sans-serif; --brand-accent: #2563eb; --brand-accent-rgb: 37, 99, 235; --background: 0 0% 100%; @@ -268,6 +268,3 @@ transform: translateX(270%); } } - - -