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%);
}
}
-
-
-