mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-11-10 16:04:38 +00:00
Make motion blur streakier and simplify its controls
This commit is contained in:
1 parent
bb268188dc
commit
1e6be1295d
8 files changed
+69
-187
No files matched your search
@@ -22,10 +22,6 @@ import { Switch } from "@/components/ui/switch";
|
||||
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { getAssetPath, getRenderableVideoUrl, getWallpaperThumbnailUrl } from "@/lib/assetPath";
|
||||
import {
|
||||
TEMPORAL_MOTION_BLUR_DEFAULT_SAMPLE_COUNT,
|
||||
TEMPORAL_MOTION_BLUR_DEFAULT_SHUTTER_FRACTION,
|
||||
} from "@/lib/exporter/temporalMotionBlur";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { BuiltInWallpaper } from "@/lib/wallpapers";
|
||||
import {
|
||||
@@ -64,7 +60,6 @@ import type {
|
||||
WebcamPositionPreset,
|
||||
ZoomDepth,
|
||||
ZoomMode,
|
||||
ZoomMotionBlurTuning,
|
||||
ZoomTransitionEasing,
|
||||
} from "./types";
|
||||
import {
|
||||
@@ -78,7 +73,6 @@ import {
|
||||
DEFAULT_CURSOR_CLICK_EFFECT_DURATION_MS,
|
||||
DEFAULT_CURSOR_CLICK_EFFECT_OPACITY,
|
||||
DEFAULT_CURSOR_CLICK_EFFECT_SCALE,
|
||||
DEFAULT_CURSOR_MOTION_BLUR,
|
||||
DEFAULT_CURSOR_SIZE,
|
||||
DEFAULT_CURSOR_STYLE,
|
||||
DEFAULT_CURSOR_SWAY,
|
||||
@@ -92,7 +86,6 @@ import {
|
||||
DEFAULT_WEBCAM_SHADOW,
|
||||
DEFAULT_WEBCAM_SIZE,
|
||||
DEFAULT_ZOOM_IN_DURATION_MS,
|
||||
DEFAULT_ZOOM_MOTION_BLUR_TUNING,
|
||||
DEFAULT_ZOOM_OUT_DURATION_MS,
|
||||
} from "./types";
|
||||
import { fromCursorSwaySliderValue, toCursorSwaySliderValue } from "./videoPlayback/cursorSway";
|
||||
@@ -548,14 +541,6 @@ interface SettingsPanelProps {
|
||||
onShadowChange?: (intensity: number) => void;
|
||||
backgroundBlur?: number;
|
||||
onBackgroundBlurChange?: (amount: number) => void;
|
||||
zoomMotionBlurTuning?: ZoomMotionBlurTuning;
|
||||
onZoomMotionBlurTuningChange?: (tuning: ZoomMotionBlurTuning) => void;
|
||||
zoomTemporalMotionBlur?: number;
|
||||
onZoomTemporalMotionBlurChange?: (amount: number) => void;
|
||||
zoomMotionBlurSampleCount?: number | null;
|
||||
onZoomMotionBlurSampleCountChange?: (count: number | null) => void;
|
||||
zoomMotionBlurShutterFraction?: number | null;
|
||||
onZoomMotionBlurShutterFractionChange?: (fraction: number | null) => void;
|
||||
connectZooms?: boolean;
|
||||
onConnectZoomsChange?: (enabled: boolean) => void;
|
||||
autoApplyFreshRecordingAutoZooms?: boolean;
|
||||
@@ -600,8 +585,6 @@ interface SettingsPanelProps {
|
||||
onCameraSpringMassMultiplierChange?: (multiplier: number) => void;
|
||||
zoomClassicMode?: boolean;
|
||||
onZoomClassicModeChange?: (enabled: boolean) => void;
|
||||
cursorMotionBlur?: number;
|
||||
onCursorMotionBlurChange?: (amount: number) => void;
|
||||
cursorClickEffect?: CursorClickEffectStyle;
|
||||
onCursorClickEffectChange?: (effect: CursorClickEffectStyle) => void;
|
||||
cursorClickEffectColor?: string;
|
||||
@@ -1009,8 +992,6 @@ export function SettingsPanel({
|
||||
onShadowChange,
|
||||
backgroundBlur = 0,
|
||||
onBackgroundBlurChange,
|
||||
zoomMotionBlurTuning = DEFAULT_ZOOM_MOTION_BLUR_TUNING,
|
||||
onZoomMotionBlurTuningChange,
|
||||
connectZooms = true,
|
||||
onConnectZoomsChange,
|
||||
autoApplyFreshRecordingAutoZooms = true,
|
||||
@@ -1043,8 +1024,6 @@ export function SettingsPanel({
|
||||
onCameraSpringMassMultiplierChange,
|
||||
zoomClassicMode = false,
|
||||
onZoomClassicModeChange,
|
||||
cursorMotionBlur = DEFAULT_CURSOR_MOTION_BLUR,
|
||||
onCursorMotionBlurChange,
|
||||
cursorClickEffect = DEFAULT_CURSOR_CLICK_EFFECT,
|
||||
onCursorClickEffectChange,
|
||||
cursorClickEffectColor = DEFAULT_CURSOR_CLICK_EFFECT_COLOR,
|
||||
@@ -1546,7 +1525,6 @@ export function SettingsPanel({
|
||||
};
|
||||
|
||||
const resetZoomSection = () => {
|
||||
onZoomMotionBlurTuningChange?.(initialEditorPreferences.zoomMotionBlurTuning);
|
||||
onCameraSpringStiffnessMultiplierChange?.(
|
||||
initialEditorPreferences.cameraSpringStiffnessMultiplier,
|
||||
);
|
||||
@@ -1572,7 +1550,6 @@ export function SettingsPanel({
|
||||
initialEditorPreferences.cursorSpringDampingMultiplier,
|
||||
);
|
||||
onCursorSpringMassMultiplierChange?.(initialEditorPreferences.cursorSpringMassMultiplier);
|
||||
onCursorMotionBlurChange?.(initialEditorPreferences.cursorMotionBlur);
|
||||
onCursorClickEffectChange?.(initialEditorPreferences.cursorClickEffect);
|
||||
onCursorClickEffectColorChange?.(initialEditorPreferences.cursorClickEffectColor);
|
||||
onCursorClickEffectScaleChange?.(initialEditorPreferences.cursorClickEffectScale);
|
||||
@@ -1593,7 +1570,6 @@ export function SettingsPanel({
|
||||
cursorSpringStiffnessMultiplier,
|
||||
cursorSpringDampingMultiplier,
|
||||
cursorSpringMassMultiplier,
|
||||
cursorMotionBlur,
|
||||
cursorClickBounce,
|
||||
cursorClickBounceDuration,
|
||||
}) ?? "focused"
|
||||
@@ -1601,7 +1577,6 @@ export function SettingsPanel({
|
||||
}, [
|
||||
cursorClickBounce,
|
||||
cursorClickBounceDuration,
|
||||
cursorMotionBlur,
|
||||
cursorSize,
|
||||
cursorSmoothing,
|
||||
cursorSpringDampingMultiplier,
|
||||
@@ -1620,7 +1595,6 @@ export function SettingsPanel({
|
||||
onCursorSpringStiffnessMultiplierChange?.(preset.cursorSpringStiffnessMultiplier);
|
||||
onCursorSpringDampingMultiplierChange?.(preset.cursorSpringDampingMultiplier);
|
||||
onCursorSpringMassMultiplierChange?.(preset.cursorSpringMassMultiplier);
|
||||
onCursorMotionBlurChange?.(preset.cursorMotionBlur);
|
||||
onCursorClickBounceChange?.(preset.cursorClickBounce);
|
||||
onCursorClickBounceDurationChange?.(preset.cursorClickBounceDuration);
|
||||
};
|
||||
@@ -2719,104 +2693,6 @@ export function SettingsPanel({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5 rounded-lg border border-foreground/10 bg-background/60 px-3 py-3">
|
||||
<div>
|
||||
<div className="text-[11px] font-medium text-foreground">
|
||||
{tSettings("effects.motionBlurDebug", "Motion Blur Debug")}
|
||||
</div>
|
||||
<div className="mt-0.5 text-[10px] text-muted-foreground">
|
||||
{tSettings(
|
||||
"effects.motionBlurDebugHint",
|
||||
"Development-only tuning for the split move-vs-zoom blur path. Pan controls drive the streak filter, and zoom controls drive the focus-centered zoom filter.",
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<SliderControl
|
||||
label={tSettings("effects.motionBlurPanThreshold", "Pan threshold")}
|
||||
value={zoomMotionBlurTuning.panVelocityThreshold}
|
||||
defaultValue={
|
||||
initialEditorPreferences.zoomMotionBlurTuning
|
||||
.panVelocityThreshold
|
||||
}
|
||||
min={0}
|
||||
max={240}
|
||||
step={1}
|
||||
onChange={(value) =>
|
||||
onZoomMotionBlurTuningChange?.({
|
||||
...zoomMotionBlurTuning,
|
||||
panVelocityThreshold: value,
|
||||
})
|
||||
}
|
||||
formatValue={(value) => `${Math.round(value)} px/s`}
|
||||
parseInput={(text) =>
|
||||
parseFloat(text.replace(/px\/s$/i, "").trim())
|
||||
}
|
||||
/>
|
||||
<SliderControl
|
||||
label={tSettings("effects.motionBlurPanStrength", "Pan max blur")}
|
||||
value={zoomMotionBlurTuning.maxDirectionalBlurPx}
|
||||
defaultValue={
|
||||
initialEditorPreferences.zoomMotionBlurTuning
|
||||
.maxDirectionalBlurPx
|
||||
}
|
||||
min={0}
|
||||
max={96}
|
||||
step={0.1}
|
||||
onChange={(value) =>
|
||||
onZoomMotionBlurTuningChange?.({
|
||||
...zoomMotionBlurTuning,
|
||||
maxDirectionalBlurPx: value,
|
||||
})
|
||||
}
|
||||
formatValue={(value) => `${value.toFixed(1)} px`}
|
||||
parseInput={(text) => parseFloat(text.replace(/px$/i, "").trim())}
|
||||
/>
|
||||
<SliderControl
|
||||
label={tSettings(
|
||||
"effects.motionBlurZoomThreshold",
|
||||
"Zoom threshold",
|
||||
)}
|
||||
value={zoomMotionBlurTuning.zoomVelocityThreshold}
|
||||
defaultValue={
|
||||
initialEditorPreferences.zoomMotionBlurTuning
|
||||
.zoomVelocityThreshold
|
||||
}
|
||||
min={0}
|
||||
max={0.4}
|
||||
step={0.005}
|
||||
onChange={(value) =>
|
||||
onZoomMotionBlurTuningChange?.({
|
||||
...zoomMotionBlurTuning,
|
||||
zoomVelocityThreshold: value,
|
||||
})
|
||||
}
|
||||
formatValue={(value) => value.toFixed(3)}
|
||||
parseInput={(text) => parseFloat(text)}
|
||||
/>
|
||||
<SliderControl
|
||||
label={tSettings(
|
||||
"effects.motionBlurZoomStrength",
|
||||
"Zoom blur strength",
|
||||
)}
|
||||
value={zoomMotionBlurTuning.maxRadialBlurStrength}
|
||||
defaultValue={
|
||||
initialEditorPreferences.zoomMotionBlurTuning
|
||||
.maxRadialBlurStrength
|
||||
}
|
||||
min={0}
|
||||
max={1.5}
|
||||
step={0.005}
|
||||
onChange={(value) =>
|
||||
onZoomMotionBlurTuningChange?.({
|
||||
...zoomMotionBlurTuning,
|
||||
maxRadialBlurStrength: value,
|
||||
})
|
||||
}
|
||||
formatValue={(value) => value.toFixed(3)}
|
||||
parseInput={(text) => parseFloat(text)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5 rounded-lg border border-foreground/10 bg-background/60 px-3 py-3">
|
||||
<div>
|
||||
<div className="text-[11px] font-medium text-foreground">
|
||||
@@ -3061,19 +2937,6 @@ export function SettingsPanel({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{showDevMotionControls ? (
|
||||
<div className="rounded-lg border border-foreground/10 bg-foreground/[0.03] px-3 py-2">
|
||||
<div className="text-[10px] text-muted-foreground">
|
||||
{tSettings(
|
||||
"effects.exportBlurMovedToDev",
|
||||
"Export blur tuning is available in Settings > Dev.",
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-1 text-[12px] font-medium text-foreground">
|
||||
{`${TEMPORAL_MOTION_BLUR_DEFAULT_SAMPLE_COUNT} samples · ${Math.round(TEMPORAL_MOTION_BLUR_DEFAULT_SHUTTER_FRACTION * 100)}% shutter`}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{selectedZoomId && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
@@ -3412,17 +3275,6 @@ export function SettingsPanel({
|
||||
formatValue={(v) => `${v.toFixed(2)}×`}
|
||||
parseInput={(text) => parseFloat(text.replace(/×$/, ""))}
|
||||
/>
|
||||
<SliderControl
|
||||
label={tSettings("effects.cursorMotionBlur")}
|
||||
value={cursorMotionBlur}
|
||||
defaultValue={DEFAULT_CURSOR_MOTION_BLUR}
|
||||
min={0}
|
||||
max={2}
|
||||
step={0.05}
|
||||
onChange={(v) => onCursorMotionBlurChange?.(v)}
|
||||
formatValue={(v) => `${v.toFixed(2)}×`}
|
||||
parseInput={(text) => parseFloat(text.replace(/×$/, ""))}
|
||||
/>
|
||||
<CursorClickEffectCards
|
||||
title={tSettings(
|
||||
"effects.cursorClickEffects.title",
|
||||
|
||||
@@ -6372,16 +6372,6 @@ export default function VideoEditor() {
|
||||
onShadowChange={setShadowIntensity}
|
||||
backgroundBlur={backgroundBlur}
|
||||
onBackgroundBlurChange={setBackgroundBlur}
|
||||
zoomMotionBlurTuning={zoomMotionBlurTuning}
|
||||
onZoomMotionBlurTuningChange={setZoomMotionBlurTuning}
|
||||
zoomTemporalMotionBlur={zoomTemporalMotionBlur}
|
||||
onZoomTemporalMotionBlurChange={setZoomTemporalMotionBlur}
|
||||
zoomMotionBlurSampleCount={zoomMotionBlurSampleCount}
|
||||
onZoomMotionBlurSampleCountChange={setZoomMotionBlurSampleCount}
|
||||
zoomMotionBlurShutterFraction={zoomMotionBlurShutterFraction}
|
||||
onZoomMotionBlurShutterFractionChange={
|
||||
setZoomMotionBlurShutterFraction
|
||||
}
|
||||
autoApplyFreshRecordingAutoZooms={autoApplyFreshRecordingAutoZooms}
|
||||
onAutoApplyFreshRecordingAutoZoomsChange={
|
||||
setAutoApplyFreshRecordingAutoZooms
|
||||
@@ -6436,8 +6426,6 @@ export default function VideoEditor() {
|
||||
onCameraSpringMassMultiplierChange={setCameraSpringMassMultiplier}
|
||||
zoomClassicMode={zoomClassicMode}
|
||||
onZoomClassicModeChange={setZoomClassicMode}
|
||||
cursorMotionBlur={cursorMotionBlur}
|
||||
onCursorMotionBlurChange={setCursorMotionBlur}
|
||||
cursorClickEffect={cursorClickEffect}
|
||||
cursorClickEffectColor={cursorClickEffectColor}
|
||||
onCursorClickEffectChange={setCursorClickEffect}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as fc from "fast-check";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { CURSOR_MOTION_PRESETS } from "./cursorMotionPresets";
|
||||
import { DEFAULT_CURSOR_MOTION_BLUR } from "./types";
|
||||
import {
|
||||
fromFileUrl,
|
||||
normalizeProjectEditor,
|
||||
@@ -253,7 +254,6 @@ describe("Motion preset normalization", () => {
|
||||
cursorSpringStiffnessMultiplier: smooth.cursorSpringStiffnessMultiplier,
|
||||
cursorSpringDampingMultiplier: smooth.cursorSpringDampingMultiplier,
|
||||
cursorSpringMassMultiplier: smooth.cursorSpringMassMultiplier,
|
||||
cursorMotionBlur: smooth.cursorMotionBlur,
|
||||
cursorClickBounce: smooth.cursorClickBounce,
|
||||
cursorClickBounceDuration: smooth.cursorClickBounceDuration,
|
||||
} as any);
|
||||
@@ -288,7 +288,7 @@ describe("Motion preset normalization", () => {
|
||||
);
|
||||
expect(result.cursorSpringDampingMultiplier).toBe(focused.cursorSpringDampingMultiplier);
|
||||
expect(result.cursorSpringMassMultiplier).toBe(focused.cursorSpringMassMultiplier);
|
||||
expect(result.cursorMotionBlur).toBe(focused.cursorMotionBlur);
|
||||
expect(result.cursorMotionBlur).toBe(DEFAULT_CURSOR_MOTION_BLUR);
|
||||
expect(result.cursorClickBounce).toBe(focused.cursorClickBounce);
|
||||
expect(result.cursorClickBounceDuration).toBe(focused.cursorClickBounceDuration);
|
||||
});
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
DEFAULT_CURSOR_MOTION_BLUR,
|
||||
DEFAULT_ZOOM_IN_DURATION_MS,
|
||||
DEFAULT_ZOOM_OUT_DURATION_MS,
|
||||
} from "./types";
|
||||
import { DEFAULT_ZOOM_IN_DURATION_MS, DEFAULT_ZOOM_OUT_DURATION_MS } from "./types";
|
||||
|
||||
export type CursorMotionPresetId = "focused" | "smooth";
|
||||
|
||||
@@ -17,7 +13,6 @@ export interface CursorMotionPreset {
|
||||
cursorSpringStiffnessMultiplier: number;
|
||||
cursorSpringDampingMultiplier: number;
|
||||
cursorSpringMassMultiplier: number;
|
||||
cursorMotionBlur: number;
|
||||
cursorClickBounce: number;
|
||||
cursorClickBounceDuration: number;
|
||||
}
|
||||
@@ -30,7 +25,6 @@ export interface CursorMotionPresetSelectionInput {
|
||||
cursorSpringStiffnessMultiplier: number;
|
||||
cursorSpringDampingMultiplier: number;
|
||||
cursorSpringMassMultiplier: number;
|
||||
cursorMotionBlur: number;
|
||||
cursorClickBounce: number;
|
||||
cursorClickBounceDuration: number;
|
||||
}
|
||||
@@ -39,7 +33,6 @@ const SHARED_CURSOR_PRESET_VALUES = {
|
||||
cursorSize: 2.5,
|
||||
cursorSmoothing: 0.67,
|
||||
cursorSpringMassMultiplier: 1.29,
|
||||
cursorMotionBlur: DEFAULT_CURSOR_MOTION_BLUR,
|
||||
cursorClickBounce: 3.5,
|
||||
cursorClickBounceDuration: 350,
|
||||
} as const;
|
||||
@@ -80,7 +73,6 @@ export function getMatchingCursorMotionPresetId(
|
||||
preset.cursorSpringStiffnessMultiplier === values.cursorSpringStiffnessMultiplier &&
|
||||
preset.cursorSpringDampingMultiplier === values.cursorSpringDampingMultiplier &&
|
||||
preset.cursorSpringMassMultiplier === values.cursorSpringMassMultiplier &&
|
||||
preset.cursorMotionBlur === values.cursorMotionBlur &&
|
||||
preset.cursorClickBounce === values.cursorClickBounce &&
|
||||
preset.cursorClickBounceDuration === values.cursorClickBounceDuration
|
||||
) {
|
||||
|
||||
@@ -46,6 +46,7 @@ import {
|
||||
DEFAULT_CURSOR_CLICK_EFFECT_DURATION_MS,
|
||||
DEFAULT_CURSOR_CLICK_EFFECT_OPACITY,
|
||||
DEFAULT_CURSOR_CLICK_EFFECT_SCALE,
|
||||
DEFAULT_CURSOR_MOTION_BLUR,
|
||||
DEFAULT_CURSOR_STYLE,
|
||||
DEFAULT_CURSOR_SWAY,
|
||||
DEFAULT_FIGURE_DATA,
|
||||
@@ -851,9 +852,6 @@ export function normalizeProjectEditor(editor: Partial<ProjectEditorState>): Pro
|
||||
cursorSpringMassMultiplier: isFiniteNumber(editor.cursorSpringMassMultiplier)
|
||||
? clamp(editor.cursorSpringMassMultiplier, 0.25, 3)
|
||||
: DEFAULT_MOTION_PRESET.cursorSpringMassMultiplier,
|
||||
cursorMotionBlur: isFiniteNumber((editor as Partial<ProjectEditorState>).cursorMotionBlur)
|
||||
? clamp((editor as Partial<ProjectEditorState>).cursorMotionBlur as number, 0, 2)
|
||||
: DEFAULT_MOTION_PRESET.cursorMotionBlur,
|
||||
cursorClickBounce: isFiniteNumber((editor as Partial<ProjectEditorState>).cursorClickBounce)
|
||||
? clamp((editor as Partial<ProjectEditorState>).cursorClickBounce as number, 0, 5)
|
||||
: DEFAULT_MOTION_PRESET.cursorClickBounce,
|
||||
@@ -942,7 +940,7 @@ export function normalizeProjectEditor(editor: Partial<ProjectEditorState>): Pro
|
||||
zoomSmoothness: DEFAULT_ZOOM_SMOOTHNESS,
|
||||
zoomClassicMode:
|
||||
typeof editor.zoomClassicMode === "boolean" ? editor.zoomClassicMode : false,
|
||||
cursorMotionBlur: normalizedMotionPreset.cursorMotionBlur,
|
||||
cursorMotionBlur: DEFAULT_CURSOR_MOTION_BLUR,
|
||||
cursorClickBounce: normalizedMotionPreset.cursorClickBounce,
|
||||
cursorClickBounceDuration: normalizedMotionPreset.cursorClickBounceDuration,
|
||||
cursorSway: isFiniteNumber((editor as Partial<ProjectEditorState>).cursorSway)
|
||||
|
||||
@@ -114,7 +114,7 @@ export interface CursorRenderConfig {
|
||||
const MIN_CURSOR_VIEWPORT_SCALE = 0;
|
||||
// Keep the cursor visually responsive while sharing the camera's frame-rate
|
||||
// normalization, kernel, and directional offset formula.
|
||||
const CURSOR_DIRECTIONAL_BLUR_STRENGTH = 3.2;
|
||||
const CURSOR_DIRECTIONAL_BLUR_STRENGTH = 2;
|
||||
const CURSOR_TIME_DISCONTINUITY_MS = 100;
|
||||
const CURSOR_SWAY_SMOOTHING_MULTIPLIER = 0.7;
|
||||
const CURSOR_SWAY_SMOOTHING_OFFSET = 0.18;
|
||||
|
||||
@@ -132,10 +132,53 @@ describe("applyZoomTransform motion blur routing", () => {
|
||||
frameTimeMs: 1016,
|
||||
});
|
||||
|
||||
expect(Math.abs(zoomBlurFilter.strength)).toBeGreaterThan(0);
|
||||
expect(zoomBlurFilter.strength).toBeGreaterThan(0);
|
||||
expect(Math.hypot(motionBlurFilter.velocity.x, motionBlurFilter.velocity.y)).toBe(0);
|
||||
expect(zoomBlurFilter.center.x).toBeCloseTo(stageSize.width / 2, 0);
|
||||
expect(zoomBlurFilter.center.y).toBeCloseTo(stageSize.height / 2, 0);
|
||||
expect(zoomBlurFilter.radius).toBe(-1);
|
||||
});
|
||||
|
||||
it("keeps a zoom on the radial path even when its center moves farther than its size", () => {
|
||||
const motionBlurState = createMotionBlurState();
|
||||
const motionBlurFilter = {
|
||||
velocity: { x: 0, y: 0 },
|
||||
kernelSize: 5,
|
||||
offset: 0,
|
||||
};
|
||||
const zoomBlurFilter = {
|
||||
strength: 0,
|
||||
center: { x: 0, y: 0 },
|
||||
innerRadius: 0,
|
||||
radius: -1,
|
||||
};
|
||||
const sharedParams = {
|
||||
cameraContainer: createStubContainer() as never,
|
||||
zoomBlurFilter: zoomBlurFilter as never,
|
||||
motionBlurFilter: motionBlurFilter as never,
|
||||
stageSize: { width: 1280, height: 720 },
|
||||
baseMask: { x: 80, y: 60, width: 1120, height: 600 },
|
||||
zoomScale: 1,
|
||||
focusX: 0.5,
|
||||
focusY: 0.5,
|
||||
isPlaying: true,
|
||||
motionBlurAmount: 1,
|
||||
motionBlurState,
|
||||
};
|
||||
|
||||
applyZoomTransform({
|
||||
...sharedParams,
|
||||
transformOverride: { scale: 1, x: 0, y: 0 },
|
||||
frameTimeMs: 1000,
|
||||
});
|
||||
applyZoomTransform({
|
||||
...sharedParams,
|
||||
transformOverride: { scale: 1.01, x: 120, y: 0 },
|
||||
frameTimeMs: 1016,
|
||||
});
|
||||
|
||||
expect(zoomBlurFilter.strength).toBeGreaterThan(0);
|
||||
expect(Math.hypot(motionBlurFilter.velocity.x, motionBlurFilter.velocity.y)).toBe(0);
|
||||
expect(zoomBlurFilter.innerRadius).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -6,6 +6,10 @@ import { DEFAULT_ZOOM_MOTION_BLUR_TUNING, type ZoomMotionBlurTuning } from "../t
|
||||
const MIN_DIRECTIONAL_BLUR_MAGNITUDE = 0.01;
|
||||
const DIRECTIONAL_BLUR_KERNEL_SIZE = 13;
|
||||
const DIRECTIONAL_BLUR_OFFSET_DIVISOR = 32;
|
||||
const SPATIAL_PAN_SHUTTER_MULTIPLIER = 4;
|
||||
// Spatial equivalent of a longer exposure. This extends the one-pass zoom
|
||||
// trail without multiplying scene renders like temporal accumulation does.
|
||||
const SPATIAL_ZOOM_SHUTTER_MULTIPLIER = 4;
|
||||
|
||||
export interface DirectionalMotionBlur {
|
||||
velocity: { x: number; y: number };
|
||||
@@ -285,6 +289,7 @@ function resolveZoomBlurStrength(
|
||||
return (
|
||||
motionBlurAmount *
|
||||
fpsScale *
|
||||
SPATIAL_ZOOM_SHUTTER_MULTIPLIER *
|
||||
(motionBlurTuning.maxRadialBlurStrength /
|
||||
DEFAULT_ZOOM_MOTION_BLUR_TUNING.maxRadialBlurStrength)
|
||||
);
|
||||
@@ -305,6 +310,9 @@ function computeSizeDelta(previousQuad: TransformQuad, currentQuad: TransformQua
|
||||
}
|
||||
|
||||
function computeZoomStrength(previousQuad: TransformQuad, currentQuad: TransformQuad) {
|
||||
// Keep the spatial approximation sampling inward. Sampling outward during
|
||||
// expansion can leave the filter's captured texture and produce transparent
|
||||
// or black frames; true directionality requires temporal/overscanned input.
|
||||
return Math.abs(1 - currentQuad.diagonal / Math.max(0.0001, previousQuad.diagonal));
|
||||
}
|
||||
|
||||
@@ -325,22 +333,22 @@ function classifyMotionMode(
|
||||
Math.max(0.0001, currentQuad.diagonal) / Math.max(0.0001, previousQuad.diagonal),
|
||||
),
|
||||
) / Math.max(0.0001, deltaSeconds);
|
||||
const moveActive = moveVelocity >= motionBlurTuning.panVelocityThreshold;
|
||||
const zoomActive = zoomVelocity >= motionBlurTuning.zoomVelocityThreshold;
|
||||
const moveActive = moveDistance > 0.001 && moveVelocity > motionBlurTuning.panVelocityThreshold;
|
||||
const zoomActive =
|
||||
zoomDistance > 0.001 && zoomVelocity > motionBlurTuning.zoomVelocityThreshold;
|
||||
|
||||
if (!moveActive && !zoomActive) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (moveActive && !zoomActive) {
|
||||
return "move";
|
||||
}
|
||||
|
||||
if (zoomActive && !moveActive) {
|
||||
// Any meaningful scale change is a zoom. The inferred zoom center already
|
||||
// accounts for the translation caused by an off-center zoom, so routing it
|
||||
// through pan blur would turn a straight radial zoom into a directional smear.
|
||||
if (zoomActive) {
|
||||
return "zoom";
|
||||
}
|
||||
|
||||
return zoomDistance > moveDistance ? "zoom" : "move";
|
||||
return "move";
|
||||
}
|
||||
|
||||
function createZeroPoint(): Point2D {
|
||||
@@ -373,8 +381,9 @@ function analyzeCameraStep({
|
||||
moveDelta,
|
||||
motionBlurAmount,
|
||||
deltaSeconds,
|
||||
motionBlurTuning.maxDirectionalBlurPx /
|
||||
DEFAULT_ZOOM_MOTION_BLUR_TUNING.maxDirectionalBlurPx,
|
||||
SPATIAL_PAN_SHUTTER_MULTIPLIER *
|
||||
(motionBlurTuning.maxDirectionalBlurPx /
|
||||
DEFAULT_ZOOM_MOTION_BLUR_TUNING.maxDirectionalBlurPx),
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
Reference in new issue
Block a user