mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-25 15:25:44 +00:00
Merge pull request #839 from webadderallorg/codex/editor-visual-polish
Polish editor preview and motion blur
This commit is contained in:
@@ -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() {
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="flex items-center justify-self-end pr-3"
|
||||
className="flex items-center justify-self-end"
|
||||
style={{ WebkitAppRegion: "no-drag" } as React.CSSProperties}
|
||||
>
|
||||
<Popover open={presetPopoverOpen} onOpenChange={setPresetPopoverOpen}>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
stepSpringValue,
|
||||
} from "./motionSmoothing";
|
||||
import { cursorSetAssets, getCursorStyleSizeMultiplier } from "./uploadedCursorAssets";
|
||||
import { computeDirectionalMotionBlur } from "./zoomTransform";
|
||||
|
||||
type CursorAssetKey = NonNullable<CursorTelemetryPoint["cursorType"]>;
|
||||
type StatefulCursorStyle = Extract<CursorStyle, "macos" | "tahoe" | "tahoe-inverted">;
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+1
-4
@@ -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%);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user