mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-11-09 15:33:31 +00:00
Merge pull request #436 from webadderallorg/codex/zoom-motion-blur-refine
feat/zoom motion blur refine
This commit is contained in:
commit
cd95bfda23
16 files changed
+1205
-464
No files matched your search
@@ -29,10 +29,6 @@ import {
|
||||
import {
|
||||
TEMPORAL_MOTION_BLUR_DEFAULT_SAMPLE_COUNT,
|
||||
TEMPORAL_MOTION_BLUR_DEFAULT_SHUTTER_FRACTION,
|
||||
TEMPORAL_MOTION_BLUR_MAX_SAMPLE_COUNT,
|
||||
TEMPORAL_MOTION_BLUR_MAX_SHUTTER_FRACTION,
|
||||
TEMPORAL_MOTION_BLUR_MIN_SAMPLE_COUNT,
|
||||
TEMPORAL_MOTION_BLUR_MIN_SHUTTER_FRACTION,
|
||||
} from "@/lib/exporter/temporalMotionBlur";
|
||||
import type { ExtensionSettingField } from "@/lib/extensions";
|
||||
import { extensionHost, type FrameInstance } from "@/lib/extensions";
|
||||
@@ -68,6 +64,7 @@ import type {
|
||||
WebcamPositionPreset,
|
||||
ZoomDepth,
|
||||
ZoomMode,
|
||||
ZoomMotionBlurTuning,
|
||||
ZoomTransitionEasing,
|
||||
} from "./types";
|
||||
import {
|
||||
@@ -89,6 +86,7 @@ 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";
|
||||
@@ -478,6 +476,8 @@ 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;
|
||||
@@ -520,6 +520,12 @@ interface SettingsPanelProps {
|
||||
onCursorSpringDampingMultiplierChange?: (multiplier: number) => void;
|
||||
cursorSpringMassMultiplier?: number;
|
||||
onCursorSpringMassMultiplierChange?: (multiplier: number) => void;
|
||||
cameraSpringStiffnessMultiplier?: number;
|
||||
onCameraSpringStiffnessMultiplierChange?: (multiplier: number) => void;
|
||||
cameraSpringDampingMultiplier?: number;
|
||||
onCameraSpringDampingMultiplierChange?: (multiplier: number) => void;
|
||||
cameraSpringMassMultiplier?: number;
|
||||
onCameraSpringMassMultiplierChange?: (multiplier: number) => void;
|
||||
zoomClassicMode?: boolean;
|
||||
onZoomClassicModeChange?: (enabled: boolean) => void;
|
||||
cursorMotionBlur?: number;
|
||||
@@ -570,6 +576,8 @@ interface SettingsPanelProps {
|
||||
onClearAutoCaptions?: () => void;
|
||||
onDownloadWhisperSmallModel?: () => void;
|
||||
onDeleteWhisperSmallModel?: () => void;
|
||||
nativeCaptureUnavailableSession?: boolean;
|
||||
onOpenNativeCaptureUnavailableModal?: () => void;
|
||||
}
|
||||
|
||||
const ZOOM_DEPTH_OPTIONS: Array<{ depth: ZoomDepth; label: string }> = [
|
||||
@@ -861,12 +869,8 @@ export function SettingsPanel({
|
||||
onShadowChange,
|
||||
backgroundBlur = 0,
|
||||
onBackgroundBlurChange,
|
||||
zoomTemporalMotionBlur = 0,
|
||||
onZoomTemporalMotionBlurChange,
|
||||
zoomMotionBlurSampleCount = TEMPORAL_MOTION_BLUR_DEFAULT_SAMPLE_COUNT,
|
||||
onZoomMotionBlurSampleCountChange,
|
||||
zoomMotionBlurShutterFraction = TEMPORAL_MOTION_BLUR_DEFAULT_SHUTTER_FRACTION,
|
||||
onZoomMotionBlurShutterFractionChange,
|
||||
zoomMotionBlurTuning = DEFAULT_ZOOM_MOTION_BLUR_TUNING,
|
||||
onZoomMotionBlurTuningChange,
|
||||
connectZooms = true,
|
||||
onConnectZoomsChange,
|
||||
autoApplyFreshRecordingAutoZooms = true,
|
||||
@@ -891,6 +895,12 @@ export function SettingsPanel({
|
||||
onCursorSpringDampingMultiplierChange,
|
||||
cursorSpringMassMultiplier = 1,
|
||||
onCursorSpringMassMultiplierChange,
|
||||
cameraSpringStiffnessMultiplier = 1,
|
||||
onCameraSpringStiffnessMultiplierChange,
|
||||
cameraSpringDampingMultiplier = 1,
|
||||
onCameraSpringDampingMultiplierChange,
|
||||
cameraSpringMassMultiplier = 1,
|
||||
onCameraSpringMassMultiplierChange,
|
||||
zoomClassicMode = false,
|
||||
onZoomClassicModeChange,
|
||||
cursorMotionBlur = DEFAULT_CURSOR_MOTION_BLUR,
|
||||
@@ -939,6 +949,8 @@ export function SettingsPanel({
|
||||
onClearAutoCaptions,
|
||||
onDownloadWhisperSmallModel,
|
||||
onDeleteWhisperSmallModel,
|
||||
nativeCaptureUnavailableSession = false,
|
||||
onOpenNativeCaptureUnavailableModal,
|
||||
}: SettingsPanelProps) {
|
||||
const tSettings = useScopedT("settings");
|
||||
const { locale, setLocale, t } = useI18n();
|
||||
@@ -1467,10 +1479,15 @@ export function SettingsPanel({
|
||||
};
|
||||
|
||||
const resetZoomSection = () => {
|
||||
onZoomTemporalMotionBlurChange?.(initialEditorPreferences.zoomTemporalMotionBlur);
|
||||
onZoomMotionBlurSampleCountChange?.(initialEditorPreferences.zoomMotionBlurSampleCount);
|
||||
onZoomMotionBlurShutterFractionChange?.(
|
||||
initialEditorPreferences.zoomMotionBlurShutterFraction,
|
||||
onZoomMotionBlurTuningChange?.(initialEditorPreferences.zoomMotionBlurTuning);
|
||||
onCameraSpringStiffnessMultiplierChange?.(
|
||||
initialEditorPreferences.cameraSpringStiffnessMultiplier,
|
||||
);
|
||||
onCameraSpringDampingMultiplierChange?.(
|
||||
initialEditorPreferences.cameraSpringDampingMultiplier,
|
||||
);
|
||||
onCameraSpringMassMultiplierChange?.(
|
||||
initialEditorPreferences.cameraSpringMassMultiplier,
|
||||
);
|
||||
onZoomInDurationMsChange?.(initialEditorPreferences.zoomInDurationMs);
|
||||
onZoomOutDurationMsChange?.(initialEditorPreferences.zoomOutDurationMs);
|
||||
@@ -2590,6 +2607,282 @@ export function SettingsPanel({
|
||||
triggerClassName="h-10 w-full justify-start rounded-xl border border-foreground/10 bg-foreground/5 px-3 text-sm text-foreground hover:bg-foreground/10 hover:text-foreground"
|
||||
/>
|
||||
</section>
|
||||
|
||||
{showDevMotionControls ? (
|
||||
<section className="flex flex-col gap-2 rounded-xl border border-[#2563EB]/15 bg-[#2563EB]/5 p-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<SectionLabel>
|
||||
{tSettings("effects.devSection", "Dev")}
|
||||
</SectionLabel>
|
||||
<div className="mt-0.5 text-[10px] text-muted-foreground">
|
||||
{tSettings(
|
||||
"effects.devSectionHint",
|
||||
"Temporary testing controls for native capture and motion tuning.",
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<span className="rounded-full bg-[#2563EB]/10 px-2 py-0.5 text-[10px] font-medium uppercase tracking-wider text-[#2563EB]">
|
||||
DEV
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-foreground/10 bg-background/60 px-3 py-3">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div className="text-[11px] font-medium text-foreground">
|
||||
{tSettings(
|
||||
"effects.nativeCaptureWarningTester",
|
||||
"Native capture warning",
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-0.5 text-[10px] text-muted-foreground">
|
||||
{nativeCaptureUnavailableSession
|
||||
? tSettings(
|
||||
"effects.nativeCaptureWarningTesterUnavailable",
|
||||
"This project is currently marked as native capture unavailable.",
|
||||
)
|
||||
: tSettings(
|
||||
"effects.nativeCaptureWarningTesterAvailable",
|
||||
"This project is not marked as unsupported, but you can still open the modal for UI testing.",
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onOpenNativeCaptureUnavailableModal?.()}
|
||||
className="h-8 shrink-0 border-[#2563EB]/20 bg-[#2563EB]/10 text-[#2563EB] hover:bg-[#2563EB]/15"
|
||||
>
|
||||
{tSettings("effects.openNativeCaptureWarning", "Open warning")}
|
||||
</Button>
|
||||
</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={32}
|
||||
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={0.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">
|
||||
{tSettings("effects.cameraDebugTuning", "Camera Debug Tuning")}
|
||||
</div>
|
||||
<div className="mt-0.5 text-[10px] text-muted-foreground">
|
||||
{tSettings(
|
||||
"effects.cameraDebugTuningHint",
|
||||
"Development-only spring tuning controls for camera motion.",
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<SliderControl
|
||||
label={tSettings(
|
||||
"effects.cameraSpringStiffnessMultiplier",
|
||||
"Camera stiffness",
|
||||
)}
|
||||
value={cameraSpringStiffnessMultiplier}
|
||||
defaultValue={
|
||||
initialEditorPreferences.cameraSpringStiffnessMultiplier
|
||||
}
|
||||
min={0.25}
|
||||
max={3}
|
||||
step={0.01}
|
||||
onChange={(value) =>
|
||||
onCameraSpringStiffnessMultiplierChange?.(value)
|
||||
}
|
||||
formatValue={(value) => `${value.toFixed(2)}×`}
|
||||
parseInput={(text) => parseFloat(text.replace(/×$/, ""))}
|
||||
/>
|
||||
<SliderControl
|
||||
label={tSettings(
|
||||
"effects.cameraSpringDampingMultiplier",
|
||||
"Camera damping",
|
||||
)}
|
||||
value={cameraSpringDampingMultiplier}
|
||||
defaultValue={
|
||||
initialEditorPreferences.cameraSpringDampingMultiplier
|
||||
}
|
||||
min={0.25}
|
||||
max={3}
|
||||
step={0.01}
|
||||
onChange={(value) => onCameraSpringDampingMultiplierChange?.(value)}
|
||||
formatValue={(value) => `${value.toFixed(2)}×`}
|
||||
parseInput={(text) => parseFloat(text.replace(/×$/, ""))}
|
||||
/>
|
||||
<SliderControl
|
||||
label={tSettings(
|
||||
"effects.cameraSpringMassMultiplier",
|
||||
"Camera mass",
|
||||
)}
|
||||
value={cameraSpringMassMultiplier}
|
||||
defaultValue={initialEditorPreferences.cameraSpringMassMultiplier}
|
||||
min={0.25}
|
||||
max={3}
|
||||
step={0.01}
|
||||
onChange={(value) => onCameraSpringMassMultiplierChange?.(value)}
|
||||
formatValue={(value) => `${value.toFixed(2)}×`}
|
||||
parseInput={(text) => parseFloat(text.replace(/×$/, ""))}
|
||||
/>
|
||||
</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.cursorDebugTuning", "Cursor Debug Tuning")}
|
||||
</div>
|
||||
<div className="mt-0.5 text-[10px] text-muted-foreground">
|
||||
{tSettings(
|
||||
"effects.cursorDebugTuningHint",
|
||||
"Development-only spring tuning controls.",
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<SliderControl
|
||||
label={tSettings(
|
||||
"effects.cursorSpringStiffnessMultiplier",
|
||||
"Spring stiffness",
|
||||
)}
|
||||
value={cursorSpringStiffnessMultiplier}
|
||||
defaultValue={
|
||||
initialEditorPreferences.cursorSpringStiffnessMultiplier
|
||||
}
|
||||
min={0.25}
|
||||
max={3}
|
||||
step={0.01}
|
||||
onChange={(value) =>
|
||||
onCursorSpringStiffnessMultiplierChange?.(value)
|
||||
}
|
||||
formatValue={(value) => `${value.toFixed(2)}×`}
|
||||
parseInput={(text) => parseFloat(text.replace(/×$/, ""))}
|
||||
/>
|
||||
<SliderControl
|
||||
label={tSettings(
|
||||
"effects.cursorSpringDampingMultiplier",
|
||||
"Spring damping",
|
||||
)}
|
||||
value={cursorSpringDampingMultiplier}
|
||||
defaultValue={
|
||||
initialEditorPreferences.cursorSpringDampingMultiplier
|
||||
}
|
||||
min={0.25}
|
||||
max={3}
|
||||
step={0.01}
|
||||
onChange={(value) => onCursorSpringDampingMultiplierChange?.(value)}
|
||||
formatValue={(value) => `${value.toFixed(2)}×`}
|
||||
parseInput={(text) => parseFloat(text.replace(/×$/, ""))}
|
||||
/>
|
||||
<SliderControl
|
||||
label={tSettings(
|
||||
"effects.cursorSpringMassMultiplier",
|
||||
"Spring mass",
|
||||
)}
|
||||
value={cursorSpringMassMultiplier}
|
||||
defaultValue={initialEditorPreferences.cursorSpringMassMultiplier}
|
||||
min={0.25}
|
||||
max={3}
|
||||
step={0.01}
|
||||
onChange={(value) => onCursorSpringMassMultiplierChange?.(value)}
|
||||
formatValue={(value) => `${value.toFixed(2)}×`}
|
||||
parseInput={(text) => parseFloat(text.replace(/×$/, ""))}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2710,80 +3003,22 @@ export function SettingsPanel({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{showDevMotionControls ? (
|
||||
<div className="space-y-1.5 rounded-lg border border-[#2563EB]/15 bg-[#2563EB]/5 px-3 py-3">
|
||||
<div>
|
||||
<div className="text-[11px] font-medium text-foreground">
|
||||
{tSettings("effects.exportBlurDebug", "Export Blur Debug")}
|
||||
</div>
|
||||
<div className="mt-0.5 text-[10px] text-muted-foreground">
|
||||
{tSettings(
|
||||
"effects.exportBlurDebugHint",
|
||||
"Development-only temporal blur tuning for export and preview parity checks.",
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<SliderControl
|
||||
label={tSettings("effects.zoomTemporalMotionBlur", "Temporal blur")}
|
||||
value={zoomTemporalMotionBlur}
|
||||
defaultValue={initialEditorPreferences.zoomTemporalMotionBlur}
|
||||
min={0}
|
||||
max={2}
|
||||
step={0.05}
|
||||
onChange={(value) => onZoomTemporalMotionBlurChange?.(value)}
|
||||
formatValue={(value) => `${value.toFixed(2)}×`}
|
||||
parseInput={(text) => parseFloat(text.replace(/×$/, ""))}
|
||||
/>
|
||||
<SliderControl
|
||||
label={tSettings("effects.zoomMotionBlurSampleCount", "Sample count")}
|
||||
value={
|
||||
zoomMotionBlurSampleCount ??
|
||||
TEMPORAL_MOTION_BLUR_DEFAULT_SAMPLE_COUNT
|
||||
}
|
||||
defaultValue={
|
||||
initialEditorPreferences.zoomMotionBlurSampleCount ??
|
||||
TEMPORAL_MOTION_BLUR_DEFAULT_SAMPLE_COUNT
|
||||
}
|
||||
min={TEMPORAL_MOTION_BLUR_MIN_SAMPLE_COUNT}
|
||||
max={TEMPORAL_MOTION_BLUR_MAX_SAMPLE_COUNT}
|
||||
step={2}
|
||||
onChange={(value) =>
|
||||
onZoomMotionBlurSampleCountChange?.(Math.round(value))
|
||||
}
|
||||
formatValue={(value) => `${Math.round(value)} samples`}
|
||||
parseInput={(text) => parseFloat(text.replace(/samples?$/i, "").trim())}
|
||||
/>
|
||||
<SliderControl
|
||||
label={tSettings("effects.zoomMotionBlurShutterFraction", "Shutter")}
|
||||
value={
|
||||
zoomMotionBlurShutterFraction ??
|
||||
TEMPORAL_MOTION_BLUR_DEFAULT_SHUTTER_FRACTION
|
||||
}
|
||||
defaultValue={
|
||||
initialEditorPreferences.zoomMotionBlurShutterFraction ??
|
||||
TEMPORAL_MOTION_BLUR_DEFAULT_SHUTTER_FRACTION
|
||||
}
|
||||
min={TEMPORAL_MOTION_BLUR_MIN_SHUTTER_FRACTION}
|
||||
max={TEMPORAL_MOTION_BLUR_MAX_SHUTTER_FRACTION}
|
||||
step={0.01}
|
||||
onChange={(value) => onZoomMotionBlurShutterFractionChange?.(value)}
|
||||
formatValue={(value) => `${Math.round(value * 100)}%`}
|
||||
parseInput={(text) => parseFloat(text.replace(/%$/, "")) / 100}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<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.exportBlurLocked",
|
||||
"Export blur is fixed for this build.",
|
||||
)}
|
||||
{showDevMotionControls
|
||||
? tSettings(
|
||||
"effects.exportBlurMovedToDev",
|
||||
"Export blur tuning is available in Settings > Dev.",
|
||||
)
|
||||
: tSettings(
|
||||
"effects.exportBlurLocked",
|
||||
"Export blur is fixed for this build.",
|
||||
)}
|
||||
</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>
|
||||
)}
|
||||
</div>
|
||||
{selectedZoomId && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
@@ -3028,75 +3263,13 @@ export function SettingsPanel({
|
||||
}}
|
||||
/>
|
||||
{showDevMotionControls ? (
|
||||
<div className="space-y-1.5 rounded-lg border border-[#2563EB]/15 bg-[#2563EB]/5 px-3 py-3">
|
||||
<div>
|
||||
<div className="text-[11px] font-medium text-foreground">
|
||||
{tSettings(
|
||||
"effects.cursorDebugTuning",
|
||||
"Cursor Debug Tuning",
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-0.5 text-[10px] text-muted-foreground">
|
||||
{tSettings(
|
||||
"effects.cursorDebugTuningHint",
|
||||
"Development-only spring tuning controls.",
|
||||
)}
|
||||
</div>
|
||||
<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.cursorDebugMovedToDev",
|
||||
"Cursor spring tuning is available in Settings > Dev.",
|
||||
)}
|
||||
</div>
|
||||
<SliderControl
|
||||
label={tSettings(
|
||||
"effects.cursorSpringStiffnessMultiplier",
|
||||
"Spring stiffness",
|
||||
)}
|
||||
value={cursorSpringStiffnessMultiplier}
|
||||
defaultValue={
|
||||
initialEditorPreferences.cursorSpringStiffnessMultiplier
|
||||
}
|
||||
min={0.25}
|
||||
max={3}
|
||||
step={0.01}
|
||||
onChange={(value) =>
|
||||
onCursorSpringStiffnessMultiplierChange?.(value)
|
||||
}
|
||||
formatValue={(value) => `${value.toFixed(2)}×`}
|
||||
parseInput={(text) => parseFloat(text.replace(/×$/, ""))}
|
||||
/>
|
||||
<SliderControl
|
||||
label={tSettings(
|
||||
"effects.cursorSpringDampingMultiplier",
|
||||
"Spring damping",
|
||||
)}
|
||||
value={cursorSpringDampingMultiplier}
|
||||
defaultValue={
|
||||
initialEditorPreferences.cursorSpringDampingMultiplier
|
||||
}
|
||||
min={0.25}
|
||||
max={3}
|
||||
step={0.01}
|
||||
onChange={(value) =>
|
||||
onCursorSpringDampingMultiplierChange?.(value)
|
||||
}
|
||||
formatValue={(value) => `${value.toFixed(2)}×`}
|
||||
parseInput={(text) => parseFloat(text.replace(/×$/, ""))}
|
||||
/>
|
||||
<SliderControl
|
||||
label={tSettings(
|
||||
"effects.cursorSpringMassMultiplier",
|
||||
"Spring mass",
|
||||
)}
|
||||
value={cursorSpringMassMultiplier}
|
||||
defaultValue={
|
||||
initialEditorPreferences.cursorSpringMassMultiplier
|
||||
}
|
||||
min={0.25}
|
||||
max={3}
|
||||
step={0.01}
|
||||
onChange={(value) =>
|
||||
onCursorSpringMassMultiplierChange?.(value)
|
||||
}
|
||||
formatValue={(value) => `${value.toFixed(2)}×`}
|
||||
parseInput={(text) => parseFloat(text.replace(/×$/, ""))}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -33,6 +33,14 @@ import { motion } from "motion/react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -164,6 +172,7 @@ import {
|
||||
DEFAULT_ZOOM_IN_DURATION_MS,
|
||||
DEFAULT_ZOOM_IN_EASING,
|
||||
DEFAULT_ZOOM_IN_OVERLAP_MS,
|
||||
DEFAULT_ZOOM_MOTION_BLUR_TUNING,
|
||||
DEFAULT_ZOOM_OUT_DURATION_MS,
|
||||
DEFAULT_ZOOM_OUT_EASING,
|
||||
type EditorEffectSection,
|
||||
@@ -180,6 +189,7 @@ import {
|
||||
type ZoomDepth,
|
||||
type ZoomFocus,
|
||||
type ZoomMode,
|
||||
type ZoomMotionBlurTuning,
|
||||
type ZoomRegion,
|
||||
type ZoomTransitionEasing,
|
||||
} from "./types";
|
||||
@@ -597,6 +607,9 @@ export default function VideoEditor() {
|
||||
);
|
||||
const [backgroundBlur, setBackgroundBlur] = useState(initialEditorPreferences.backgroundBlur);
|
||||
const [zoomMotionBlur, setZoomMotionBlur] = useState(initialEditorPreferences.zoomMotionBlur);
|
||||
const [zoomMotionBlurTuning, setZoomMotionBlurTuning] = useState<ZoomMotionBlurTuning>(
|
||||
initialEditorPreferences.zoomMotionBlurTuning ?? DEFAULT_ZOOM_MOTION_BLUR_TUNING,
|
||||
);
|
||||
const [zoomTemporalMotionBlur, setZoomTemporalMotionBlur] = useState(
|
||||
initialEditorPreferences.zoomTemporalMotionBlur,
|
||||
);
|
||||
@@ -652,9 +665,21 @@ export default function VideoEditor() {
|
||||
const [cursorSpringMassMultiplier, setCursorSpringMassMultiplier] = useState(
|
||||
initialEditorPreferences.cursorSpringMassMultiplier,
|
||||
);
|
||||
const [cameraSpringStiffnessMultiplier, setCameraSpringStiffnessMultiplier] = useState(
|
||||
initialEditorPreferences.cameraSpringStiffnessMultiplier,
|
||||
);
|
||||
const [cameraSpringDampingMultiplier, setCameraSpringDampingMultiplier] = useState(
|
||||
initialEditorPreferences.cameraSpringDampingMultiplier,
|
||||
);
|
||||
const [cameraSpringMassMultiplier, setCameraSpringMassMultiplier] = useState(
|
||||
initialEditorPreferences.cameraSpringMassMultiplier,
|
||||
);
|
||||
const [sessionShowCursorOverride, setSessionShowCursorOverride] = useState<boolean | null>(
|
||||
null,
|
||||
);
|
||||
const [sessionNativeCaptureUnavailable, setSessionNativeCaptureUnavailable] = useState(false);
|
||||
const [nativeCaptureUnavailableModalOpen, setNativeCaptureUnavailableModalOpen] =
|
||||
useState(false);
|
||||
const [zoomSmoothness, setZoomSmoothness] = useState(0.5);
|
||||
const [zoomClassicMode, setZoomClassicMode] = useState(false);
|
||||
const [cursorMotionBlur, setCursorMotionBlur] = useState(
|
||||
@@ -718,6 +743,22 @@ export default function VideoEditor() {
|
||||
const [sourceAudioFallbackPaths, setSourceAudioFallbackPaths] = useState<string[]>([]);
|
||||
const [sourceAudioFallbackStartDelayMsByPath, setSourceAudioFallbackStartDelayMsByPath] =
|
||||
useState<Record<string, number>>({});
|
||||
const applySessionPresentation = useCallback(
|
||||
(
|
||||
session:
|
||||
| {
|
||||
hideOverlayCursorByDefault?: boolean;
|
||||
nativeCaptureUnavailable?: boolean;
|
||||
}
|
||||
| null
|
||||
| undefined,
|
||||
) => {
|
||||
setSessionShowCursorOverride(session?.hideOverlayCursorByDefault ? false : null);
|
||||
setSessionNativeCaptureUnavailable(Boolean(session?.nativeCaptureUnavailable));
|
||||
setNativeCaptureUnavailableModalOpen(Boolean(session?.nativeCaptureUnavailable));
|
||||
},
|
||||
[],
|
||||
);
|
||||
const effectiveShowCursor = sessionShowCursorOverride ?? showCursor;
|
||||
const [aspectRatio, setAspectRatio] = useState<AspectRatio>(
|
||||
initialEditorPreferences.aspectRatio,
|
||||
@@ -838,6 +879,7 @@ export default function VideoEditor() {
|
||||
shadowIntensity,
|
||||
backgroundBlur,
|
||||
zoomMotionBlur,
|
||||
zoomMotionBlurTuning: { ...zoomMotionBlurTuning },
|
||||
zoomTemporalMotionBlur,
|
||||
zoomMotionBlurSampleCount,
|
||||
zoomMotionBlurShutterFraction,
|
||||
@@ -858,6 +900,9 @@ export default function VideoEditor() {
|
||||
cursorSpringStiffnessMultiplier,
|
||||
cursorSpringDampingMultiplier,
|
||||
cursorSpringMassMultiplier,
|
||||
cameraSpringStiffnessMultiplier,
|
||||
cameraSpringDampingMultiplier,
|
||||
cameraSpringMassMultiplier,
|
||||
cursorMotionBlur,
|
||||
cursorClickBounce,
|
||||
cursorClickBounceDuration,
|
||||
@@ -885,6 +930,7 @@ export default function VideoEditor() {
|
||||
shadowIntensity,
|
||||
backgroundBlur,
|
||||
zoomMotionBlur,
|
||||
zoomMotionBlurTuning,
|
||||
zoomTemporalMotionBlur,
|
||||
zoomMotionBlurSampleCount,
|
||||
zoomMotionBlurShutterFraction,
|
||||
@@ -905,6 +951,9 @@ export default function VideoEditor() {
|
||||
cursorSpringStiffnessMultiplier,
|
||||
cursorSpringDampingMultiplier,
|
||||
cursorSpringMassMultiplier,
|
||||
cameraSpringStiffnessMultiplier,
|
||||
cameraSpringDampingMultiplier,
|
||||
cameraSpringMassMultiplier,
|
||||
cursorMotionBlur,
|
||||
cursorClickBounce,
|
||||
cursorClickBounceDuration,
|
||||
@@ -973,6 +1022,7 @@ export default function VideoEditor() {
|
||||
setShadowIntensity(snapshot.shadowIntensity);
|
||||
setBackgroundBlur(snapshot.backgroundBlur);
|
||||
setZoomMotionBlur(snapshot.zoomMotionBlur);
|
||||
setZoomMotionBlurTuning({ ...snapshot.zoomMotionBlurTuning });
|
||||
setZoomTemporalMotionBlur(snapshot.zoomTemporalMotionBlur);
|
||||
setZoomMotionBlurSampleCount(snapshot.zoomMotionBlurSampleCount);
|
||||
setZoomMotionBlurShutterFraction(snapshot.zoomMotionBlurShutterFraction);
|
||||
@@ -993,6 +1043,9 @@ export default function VideoEditor() {
|
||||
setCursorSpringStiffnessMultiplier(snapshot.cursorSpringStiffnessMultiplier);
|
||||
setCursorSpringDampingMultiplier(snapshot.cursorSpringDampingMultiplier);
|
||||
setCursorSpringMassMultiplier(snapshot.cursorSpringMassMultiplier);
|
||||
setCameraSpringStiffnessMultiplier(snapshot.cameraSpringStiffnessMultiplier);
|
||||
setCameraSpringDampingMultiplier(snapshot.cameraSpringDampingMultiplier);
|
||||
setCameraSpringMassMultiplier(snapshot.cameraSpringMassMultiplier);
|
||||
setCursorMotionBlur(snapshot.cursorMotionBlur);
|
||||
setCursorClickBounce(snapshot.cursorClickBounce);
|
||||
setCursorClickBounceDuration(snapshot.cursorClickBounceDuration);
|
||||
@@ -1204,6 +1257,7 @@ export default function VideoEditor() {
|
||||
shadowIntensity,
|
||||
backgroundBlur,
|
||||
zoomMotionBlur,
|
||||
zoomMotionBlurTuning,
|
||||
zoomTemporalMotionBlur,
|
||||
zoomMotionBlurSampleCount,
|
||||
zoomMotionBlurShutterFraction,
|
||||
@@ -1259,6 +1313,9 @@ export default function VideoEditor() {
|
||||
cursorSpringStiffnessMultiplier,
|
||||
cursorSpringDampingMultiplier,
|
||||
cursorSpringMassMultiplier,
|
||||
cameraSpringStiffnessMultiplier,
|
||||
cameraSpringDampingMultiplier,
|
||||
cameraSpringMassMultiplier,
|
||||
zoomSmoothness,
|
||||
zoomClassicMode,
|
||||
cursorMotionBlur,
|
||||
@@ -1337,6 +1394,9 @@ export default function VideoEditor() {
|
||||
cursorSpringDampingMultiplier,
|
||||
cursorSpringMassMultiplier,
|
||||
cursorSpringStiffnessMultiplier,
|
||||
cameraSpringStiffnessMultiplier,
|
||||
cameraSpringDampingMultiplier,
|
||||
cameraSpringMassMultiplier,
|
||||
zoomSmoothness,
|
||||
cursorStyle,
|
||||
cursorSway,
|
||||
@@ -1353,6 +1413,7 @@ export default function VideoEditor() {
|
||||
zoomInEasing,
|
||||
zoomInOverlapMs,
|
||||
zoomMotionBlur,
|
||||
zoomMotionBlurTuning,
|
||||
zoomTemporalMotionBlur,
|
||||
zoomMotionBlurSampleCount,
|
||||
zoomMotionBlurShutterFraction,
|
||||
@@ -1652,15 +1713,16 @@ export default function VideoEditor() {
|
||||
}, [activeEffectSection]);
|
||||
|
||||
const buildPersistedEditorState = useCallback(
|
||||
(
|
||||
editor: Partial<{
|
||||
wallpaper: string;
|
||||
shadowIntensity: number;
|
||||
backgroundBlur: number;
|
||||
zoomMotionBlur: number;
|
||||
zoomTemporalMotionBlur: number;
|
||||
zoomMotionBlurSampleCount: number | null;
|
||||
zoomMotionBlurShutterFraction: number | null;
|
||||
(
|
||||
editor: Partial<{
|
||||
wallpaper: string;
|
||||
shadowIntensity: number;
|
||||
backgroundBlur: number;
|
||||
zoomMotionBlur: number;
|
||||
zoomMotionBlurTuning: ZoomMotionBlurTuning;
|
||||
zoomTemporalMotionBlur: number;
|
||||
zoomMotionBlurSampleCount: number | null;
|
||||
zoomMotionBlurShutterFraction: number | null;
|
||||
connectZooms: boolean;
|
||||
zoomInDurationMs: number;
|
||||
zoomInOverlapMs: number;
|
||||
@@ -1678,6 +1740,9 @@ export default function VideoEditor() {
|
||||
cursorSpringStiffnessMultiplier: number;
|
||||
cursorSpringDampingMultiplier: number;
|
||||
cursorSpringMassMultiplier: number;
|
||||
cameraSpringStiffnessMultiplier: number;
|
||||
cameraSpringDampingMultiplier: number;
|
||||
cameraSpringMassMultiplier: number;
|
||||
zoomSmoothness: number;
|
||||
zoomClassicMode: boolean;
|
||||
cursorMotionBlur: number;
|
||||
@@ -1812,6 +1877,7 @@ export default function VideoEditor() {
|
||||
shadowIntensity,
|
||||
backgroundBlur,
|
||||
zoomMotionBlur,
|
||||
zoomMotionBlurTuning,
|
||||
zoomTemporalMotionBlur,
|
||||
zoomMotionBlurSampleCount,
|
||||
zoomMotionBlurShutterFraction,
|
||||
@@ -1832,6 +1898,9 @@ export default function VideoEditor() {
|
||||
cursorSpringStiffnessMultiplier,
|
||||
cursorSpringDampingMultiplier,
|
||||
cursorSpringMassMultiplier,
|
||||
cameraSpringStiffnessMultiplier,
|
||||
cameraSpringDampingMultiplier,
|
||||
cameraSpringMassMultiplier,
|
||||
zoomSmoothness,
|
||||
zoomClassicMode,
|
||||
cursorMotionBlur,
|
||||
@@ -1868,6 +1937,7 @@ export default function VideoEditor() {
|
||||
shadowIntensity,
|
||||
backgroundBlur,
|
||||
zoomMotionBlur,
|
||||
zoomMotionBlurTuning,
|
||||
zoomTemporalMotionBlur,
|
||||
zoomMotionBlurSampleCount,
|
||||
zoomMotionBlurShutterFraction,
|
||||
@@ -1888,6 +1958,9 @@ export default function VideoEditor() {
|
||||
cursorSpringStiffnessMultiplier,
|
||||
cursorSpringDampingMultiplier,
|
||||
cursorSpringMassMultiplier,
|
||||
cameraSpringStiffnessMultiplier,
|
||||
cameraSpringDampingMultiplier,
|
||||
cameraSpringMassMultiplier,
|
||||
zoomSmoothness,
|
||||
zoomClassicMode,
|
||||
cursorMotionBlur,
|
||||
@@ -2045,16 +2118,19 @@ export default function VideoEditor() {
|
||||
preserveProjectPath: Boolean(path),
|
||||
},
|
||||
);
|
||||
} else {
|
||||
await window.electronAPI.setCurrentVideoPath(sourcePath, {
|
||||
preserveProjectPath: Boolean(path),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
await window.electronAPI.setCurrentVideoPath(sourcePath, {
|
||||
preserveProjectPath: Boolean(path),
|
||||
});
|
||||
}
|
||||
const sessionResult = await window.electronAPI.getCurrentRecordingSession?.();
|
||||
applySessionPresentation(sessionResult?.success ? sessionResult.session : null);
|
||||
|
||||
setWallpaper(normalizedEditor.wallpaper);
|
||||
setWallpaper(normalizedEditor.wallpaper);
|
||||
setShadowIntensity(normalizedEditor.shadowIntensity);
|
||||
setBackgroundBlur(normalizedEditor.backgroundBlur);
|
||||
setZoomMotionBlur(normalizedEditor.zoomMotionBlur);
|
||||
setZoomMotionBlurTuning({ ...normalizedEditor.zoomMotionBlurTuning });
|
||||
setZoomTemporalMotionBlur(normalizedEditor.zoomTemporalMotionBlur);
|
||||
setZoomMotionBlurSampleCount(normalizedEditor.zoomMotionBlurSampleCount);
|
||||
setZoomMotionBlurShutterFraction(normalizedEditor.zoomMotionBlurShutterFraction);
|
||||
@@ -2064,11 +2140,10 @@ export default function VideoEditor() {
|
||||
setZoomOutDurationMs(normalizedEditor.zoomOutDurationMs);
|
||||
setConnectedZoomGapMs(normalizedEditor.connectedZoomGapMs);
|
||||
setConnectedZoomDurationMs(normalizedEditor.connectedZoomDurationMs);
|
||||
setZoomInEasing(normalizedEditor.zoomInEasing);
|
||||
setZoomOutEasing(normalizedEditor.zoomOutEasing);
|
||||
setConnectedZoomEasing(normalizedEditor.connectedZoomEasing);
|
||||
setSessionShowCursorOverride(null);
|
||||
setShowCursor(normalizedEditor.showCursor);
|
||||
setZoomInEasing(normalizedEditor.zoomInEasing);
|
||||
setZoomOutEasing(normalizedEditor.zoomOutEasing);
|
||||
setConnectedZoomEasing(normalizedEditor.connectedZoomEasing);
|
||||
setShowCursor(normalizedEditor.showCursor);
|
||||
setLoopCursor(normalizedEditor.loopCursor);
|
||||
setCursorStyle(normalizedEditor.cursorStyle);
|
||||
setCursorSize(normalizedEditor.cursorSize);
|
||||
@@ -2076,6 +2151,9 @@ export default function VideoEditor() {
|
||||
setCursorSpringStiffnessMultiplier(normalizedEditor.cursorSpringStiffnessMultiplier);
|
||||
setCursorSpringDampingMultiplier(normalizedEditor.cursorSpringDampingMultiplier);
|
||||
setCursorSpringMassMultiplier(normalizedEditor.cursorSpringMassMultiplier);
|
||||
setCameraSpringStiffnessMultiplier(normalizedEditor.cameraSpringStiffnessMultiplier);
|
||||
setCameraSpringDampingMultiplier(normalizedEditor.cameraSpringDampingMultiplier);
|
||||
setCameraSpringMassMultiplier(normalizedEditor.cameraSpringMassMultiplier);
|
||||
setZoomSmoothness(normalizedEditor.zoomSmoothness);
|
||||
setZoomClassicMode(normalizedEditor.zoomClassicMode);
|
||||
setCursorMotionBlur(normalizedEditor.cursorMotionBlur);
|
||||
@@ -2388,9 +2466,7 @@ export default function VideoEditor() {
|
||||
pendingFreshRecordingAutoZoomPathRef.current = autoApplyFreshRecordingAutoZooms
|
||||
? sourceVideoUrl
|
||||
: null;
|
||||
setSessionShowCursorOverride(
|
||||
sessionResult.session.hideOverlayCursorByDefault ? false : null,
|
||||
);
|
||||
applySessionPresentation(sessionResult.session);
|
||||
setWebcam((prev) => ({
|
||||
...prev,
|
||||
enabled: Boolean(sessionResult.session?.webcamPath),
|
||||
@@ -2410,7 +2486,7 @@ export default function VideoEditor() {
|
||||
setCurrentProjectPath(null);
|
||||
setLastSavedSnapshot(null);
|
||||
pendingFreshRecordingAutoZoomPathRef.current = null;
|
||||
setSessionShowCursorOverride(null);
|
||||
applySessionPresentation(null);
|
||||
setWebcam((prev) => ({
|
||||
...prev,
|
||||
enabled: false,
|
||||
@@ -2430,6 +2506,7 @@ export default function VideoEditor() {
|
||||
loadInitialData();
|
||||
}, [
|
||||
applyLoadedProject,
|
||||
applySessionPresentation,
|
||||
autoApplyFreshRecordingAutoZooms,
|
||||
initialEditorPreferences,
|
||||
smokeExportConfig.enabled,
|
||||
@@ -2466,6 +2543,7 @@ export default function VideoEditor() {
|
||||
shadowIntensity,
|
||||
backgroundBlur,
|
||||
zoomMotionBlur,
|
||||
zoomMotionBlurTuning,
|
||||
zoomTemporalMotionBlur,
|
||||
zoomMotionBlurSampleCount,
|
||||
zoomMotionBlurShutterFraction,
|
||||
@@ -2487,6 +2565,9 @@ export default function VideoEditor() {
|
||||
cursorSpringStiffnessMultiplier,
|
||||
cursorSpringDampingMultiplier,
|
||||
cursorSpringMassMultiplier,
|
||||
cameraSpringStiffnessMultiplier,
|
||||
cameraSpringDampingMultiplier,
|
||||
cameraSpringMassMultiplier,
|
||||
cursorMotionBlur,
|
||||
cursorClickBounce,
|
||||
cursorClickBounceDuration,
|
||||
@@ -2513,6 +2594,7 @@ export default function VideoEditor() {
|
||||
shadowIntensity,
|
||||
backgroundBlur,
|
||||
zoomMotionBlur,
|
||||
zoomMotionBlurTuning,
|
||||
zoomTemporalMotionBlur,
|
||||
zoomMotionBlurSampleCount,
|
||||
zoomMotionBlurShutterFraction,
|
||||
@@ -2534,6 +2616,9 @@ export default function VideoEditor() {
|
||||
cursorSpringStiffnessMultiplier,
|
||||
cursorSpringDampingMultiplier,
|
||||
cursorSpringMassMultiplier,
|
||||
cameraSpringStiffnessMultiplier,
|
||||
cameraSpringDampingMultiplier,
|
||||
cameraSpringMassMultiplier,
|
||||
cursorMotionBlur,
|
||||
cursorClickBounce,
|
||||
cursorClickBounceDuration,
|
||||
@@ -3780,7 +3865,7 @@ export default function VideoEditor() {
|
||||
),
|
||||
);
|
||||
},
|
||||
[],
|
||||
[applySessionPresentation],
|
||||
);
|
||||
|
||||
const handleAnnotationDelete = useCallback(
|
||||
@@ -4360,6 +4445,7 @@ export default function VideoEditor() {
|
||||
shadowIntensity: effectiveShadowIntensity,
|
||||
backgroundBlur,
|
||||
zoomMotionBlur,
|
||||
zoomMotionBlurTuning,
|
||||
zoomTemporalMotionBlur,
|
||||
zoomMotionBlurSampleCount,
|
||||
zoomMotionBlurShutterFraction,
|
||||
@@ -4392,6 +4478,9 @@ export default function VideoEditor() {
|
||||
cursorSpringStiffnessMultiplier,
|
||||
cursorSpringDampingMultiplier,
|
||||
cursorSpringMassMultiplier,
|
||||
cameraSpringStiffnessMultiplier,
|
||||
cameraSpringDampingMultiplier,
|
||||
cameraSpringMassMultiplier,
|
||||
zoomSmoothness,
|
||||
zoomClassicMode,
|
||||
cursorMotionBlur,
|
||||
@@ -4539,6 +4628,7 @@ export default function VideoEditor() {
|
||||
shadowIntensity: effectiveShadowIntensity,
|
||||
backgroundBlur,
|
||||
zoomMotionBlur,
|
||||
zoomMotionBlurTuning,
|
||||
zoomTemporalMotionBlur,
|
||||
zoomMotionBlurSampleCount,
|
||||
zoomMotionBlurShutterFraction,
|
||||
@@ -4570,6 +4660,9 @@ export default function VideoEditor() {
|
||||
cursorSpringStiffnessMultiplier,
|
||||
cursorSpringDampingMultiplier,
|
||||
cursorSpringMassMultiplier,
|
||||
cameraSpringStiffnessMultiplier,
|
||||
cameraSpringDampingMultiplier,
|
||||
cameraSpringMassMultiplier,
|
||||
zoomSmoothness,
|
||||
zoomClassicMode,
|
||||
cursorMotionBlur,
|
||||
@@ -4793,6 +4886,7 @@ export default function VideoEditor() {
|
||||
shadowIntensity,
|
||||
backgroundBlur,
|
||||
zoomMotionBlur,
|
||||
zoomMotionBlurTuning,
|
||||
zoomTemporalMotionBlur,
|
||||
zoomMotionBlurSampleCount,
|
||||
zoomMotionBlurShutterFraction,
|
||||
@@ -4813,6 +4907,9 @@ export default function VideoEditor() {
|
||||
cursorSpringStiffnessMultiplier,
|
||||
cursorSpringDampingMultiplier,
|
||||
cursorSpringMassMultiplier,
|
||||
cameraSpringStiffnessMultiplier,
|
||||
cameraSpringDampingMultiplier,
|
||||
cameraSpringMassMultiplier,
|
||||
zoomSmoothness,
|
||||
zoomClassicMode,
|
||||
cursorMotionBlur,
|
||||
@@ -5177,12 +5274,41 @@ export default function VideoEditor() {
|
||||
}}
|
||||
/>
|
||||
);
|
||||
const nativeCaptureUnavailableDialog = (
|
||||
<Dialog
|
||||
open={nativeCaptureUnavailableModalOpen}
|
||||
onOpenChange={setNativeCaptureUnavailableModalOpen}
|
||||
>
|
||||
<DialogContent className="max-w-md bg-editor-dialog border-foreground/10 text-foreground">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{t(
|
||||
"editor.nativeCaptureUnavailable.title",
|
||||
"Nothing’s broken, but we won’t be able to render an animated cursor overlay.",
|
||||
)}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-muted-foreground">
|
||||
{t(
|
||||
"editor.nativeCaptureUnavailable.description",
|
||||
"Your device does not support native capture. This could be for a variety of reasons we haven’t figured out yet. This doesn’t break Recordly, but it does make cursor smoothing impossible.",
|
||||
)}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button onClick={() => setNativeCaptureUnavailableModalOpen(false)}>
|
||||
{t("editor.nativeCaptureUnavailable.confirm", "Okay")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex h-screen items-center justify-center bg-background">
|
||||
<div className="text-foreground">Loading video...</div>
|
||||
{projectBrowser}
|
||||
{nativeCaptureUnavailableDialog}
|
||||
<Toaster className="pointer-events-auto" />
|
||||
</div>
|
||||
);
|
||||
@@ -5202,6 +5328,7 @@ export default function VideoEditor() {
|
||||
</button>
|
||||
</div>
|
||||
{projectBrowser}
|
||||
{nativeCaptureUnavailableDialog}
|
||||
<Toaster className="pointer-events-auto" />
|
||||
</div>
|
||||
);
|
||||
@@ -5790,6 +5917,8 @@ export default function VideoEditor() {
|
||||
onShadowChange={setShadowIntensity}
|
||||
backgroundBlur={backgroundBlur}
|
||||
onBackgroundBlurChange={setBackgroundBlur}
|
||||
zoomMotionBlurTuning={zoomMotionBlurTuning}
|
||||
onZoomMotionBlurTuningChange={setZoomMotionBlurTuning}
|
||||
zoomTemporalMotionBlur={zoomTemporalMotionBlur}
|
||||
onZoomTemporalMotionBlurChange={setZoomTemporalMotionBlur}
|
||||
zoomMotionBlurSampleCount={zoomMotionBlurSampleCount}
|
||||
@@ -5840,6 +5969,16 @@ export default function VideoEditor() {
|
||||
}
|
||||
cursorSpringMassMultiplier={cursorSpringMassMultiplier}
|
||||
onCursorSpringMassMultiplierChange={setCursorSpringMassMultiplier}
|
||||
cameraSpringStiffnessMultiplier={cameraSpringStiffnessMultiplier}
|
||||
onCameraSpringStiffnessMultiplierChange={
|
||||
setCameraSpringStiffnessMultiplier
|
||||
}
|
||||
cameraSpringDampingMultiplier={cameraSpringDampingMultiplier}
|
||||
onCameraSpringDampingMultiplierChange={
|
||||
setCameraSpringDampingMultiplier
|
||||
}
|
||||
cameraSpringMassMultiplier={cameraSpringMassMultiplier}
|
||||
onCameraSpringMassMultiplierChange={setCameraSpringMassMultiplier}
|
||||
zoomClassicMode={zoomClassicMode}
|
||||
onZoomClassicModeChange={setZoomClassicMode}
|
||||
cursorMotionBlur={cursorMotionBlur}
|
||||
@@ -5883,6 +6022,10 @@ export default function VideoEditor() {
|
||||
onClearAutoCaptions={handleClearAutoCaptions}
|
||||
onDownloadWhisperSmallModel={handleDownloadWhisperSmallModel}
|
||||
onDeleteWhisperSmallModel={handleDeleteWhisperSmallModel}
|
||||
nativeCaptureUnavailableSession={sessionNativeCaptureUnavailable}
|
||||
onOpenNativeCaptureUnavailableModal={() =>
|
||||
setNativeCaptureUnavailableModalOpen(true)
|
||||
}
|
||||
onAnnotationContentChange={handleAnnotationContentChange}
|
||||
onAnnotationTypeChange={handleAnnotationTypeChange}
|
||||
onAnnotationStyleChange={handleAnnotationStyleChange}
|
||||
@@ -6046,8 +6189,19 @@ export default function VideoEditor() {
|
||||
cursorSpringMassMultiplier={
|
||||
cursorSpringMassMultiplier
|
||||
}
|
||||
cameraSpringStiffnessMultiplier={
|
||||
cameraSpringStiffnessMultiplier
|
||||
}
|
||||
cameraSpringDampingMultiplier={
|
||||
cameraSpringDampingMultiplier
|
||||
}
|
||||
cameraSpringMassMultiplier={
|
||||
cameraSpringMassMultiplier
|
||||
}
|
||||
zoomSmoothness={zoomSmoothness}
|
||||
zoomClassicMode={zoomClassicMode}
|
||||
zoomMotionBlur={zoomMotionBlur}
|
||||
zoomMotionBlurTuning={zoomMotionBlurTuning}
|
||||
cursorMotionBlur={cursorMotionBlur}
|
||||
cursorClickBounce={cursorClickBounce}
|
||||
cursorClickBounceDuration={
|
||||
@@ -6374,6 +6528,7 @@ export default function VideoEditor() {
|
||||
) : null}
|
||||
|
||||
{projectBrowser}
|
||||
{nativeCaptureUnavailableDialog}
|
||||
|
||||
<Toaster className="pointer-events-auto" />
|
||||
</div>
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
ZOOM_DEPTH_SCALES,
|
||||
type ZoomDepth,
|
||||
type ZoomFocus,
|
||||
type ZoomMotionBlurTuning,
|
||||
type ZoomRegion,
|
||||
type ZoomTransitionEasing,
|
||||
} from "./types";
|
||||
@@ -102,6 +103,8 @@ import {
|
||||
DEFAULT_ZOOM_IN_DURATION_MS,
|
||||
DEFAULT_ZOOM_IN_EASING,
|
||||
DEFAULT_ZOOM_IN_OVERLAP_MS,
|
||||
DEFAULT_ZOOM_MOTION_BLUR,
|
||||
DEFAULT_ZOOM_MOTION_BLUR_TUNING,
|
||||
DEFAULT_ZOOM_OUT_DURATION_MS,
|
||||
DEFAULT_ZOOM_OUT_EASING,
|
||||
getDefaultCaptionFontFamily,
|
||||
@@ -262,8 +265,13 @@ interface VideoPlaybackProps {
|
||||
cursorSpringStiffnessMultiplier?: number;
|
||||
cursorSpringDampingMultiplier?: number;
|
||||
cursorSpringMassMultiplier?: number;
|
||||
cameraSpringStiffnessMultiplier?: number;
|
||||
cameraSpringDampingMultiplier?: number;
|
||||
cameraSpringMassMultiplier?: number;
|
||||
zoomSmoothness?: number;
|
||||
zoomClassicMode?: boolean;
|
||||
zoomMotionBlur?: number;
|
||||
zoomMotionBlurTuning?: ZoomMotionBlurTuning;
|
||||
cursorMotionBlur?: number;
|
||||
cursorClickBounce?: number;
|
||||
cursorClickBounceDuration?: number;
|
||||
@@ -334,8 +342,13 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
|
||||
cursorSpringStiffnessMultiplier = 1,
|
||||
cursorSpringDampingMultiplier = 1,
|
||||
cursorSpringMassMultiplier = 1,
|
||||
cameraSpringStiffnessMultiplier = 1,
|
||||
cameraSpringDampingMultiplier = 1,
|
||||
cameraSpringMassMultiplier = 1,
|
||||
zoomSmoothness = 0.5,
|
||||
zoomClassicMode = false,
|
||||
zoomMotionBlur = DEFAULT_ZOOM_MOTION_BLUR,
|
||||
zoomMotionBlurTuning = DEFAULT_ZOOM_MOTION_BLUR_TUNING,
|
||||
cursorMotionBlur = DEFAULT_CURSOR_MOTION_BLUR,
|
||||
cursorClickBounce = DEFAULT_CURSOR_CLICK_BOUNCE,
|
||||
cursorClickBounceDuration = DEFAULT_CURSOR_CLICK_BOUNCE_DURATION,
|
||||
@@ -426,10 +439,15 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
|
||||
const cursorSpringStiffnessMultiplierRef = useRef(cursorSpringStiffnessMultiplier);
|
||||
const cursorSpringDampingMultiplierRef = useRef(cursorSpringDampingMultiplier);
|
||||
const cursorSpringMassMultiplierRef = useRef(cursorSpringMassMultiplier);
|
||||
const cameraSpringStiffnessMultiplierRef = useRef(cameraSpringStiffnessMultiplier);
|
||||
const cameraSpringDampingMultiplierRef = useRef(cameraSpringDampingMultiplier);
|
||||
const cameraSpringMassMultiplierRef = useRef(cameraSpringMassMultiplier);
|
||||
const cursorMotionBlurRef = useRef(cursorMotionBlur);
|
||||
const cursorClickBounceRef = useRef(cursorClickBounce);
|
||||
const cursorClickBounceDurationRef = useRef(cursorClickBounceDuration);
|
||||
const cursorSwayRef = useRef(cursorSway);
|
||||
const zoomMotionBlurRef = useRef(zoomMotionBlur);
|
||||
const zoomMotionBlurTuningRef = useRef(zoomMotionBlurTuning);
|
||||
const lastEmittedClickTimeMsRef = useRef(-1);
|
||||
|
||||
// Spring animation state for smooth zoom transitions
|
||||
@@ -665,7 +683,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
|
||||
const zoomBlurFilter = zoomBlurFilterRef.current;
|
||||
const motionBlurFilter = motionBlurFilterRef.current;
|
||||
|
||||
if (!app || !videoEffectsContainer || !zoomBlurFilter || !motionBlurFilter) {
|
||||
if (!app || !videoEffectsContainer || !motionBlurFilter || !zoomBlurFilter) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -676,8 +694,8 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
|
||||
const stageWidth = Math.max(1, stageSizeRef.current.width || app.screen.width);
|
||||
const stageHeight = Math.max(1, stageSizeRef.current.height || app.screen.height);
|
||||
|
||||
zoomBlurFilter.resolution = filterResolution;
|
||||
motionBlurFilter.resolution = filterResolution;
|
||||
zoomBlurFilter.resolution = filterResolution;
|
||||
videoEffectsContainer.filterArea = new Rectangle(0, 0, stageWidth, stageHeight);
|
||||
}, []);
|
||||
|
||||
@@ -1158,11 +1176,12 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
|
||||
const zoomBlurFilter = zoomBlurFilterRef.current;
|
||||
const motionBlurFilter = motionBlurFilterRef.current;
|
||||
|
||||
if (!videoEffectsContainer || !zoomBlurFilter || !motionBlurFilter) {
|
||||
if (!videoEffectsContainer || !motionBlurFilter || !zoomBlurFilter) {
|
||||
return;
|
||||
}
|
||||
|
||||
videoEffectsContainer.filters = null;
|
||||
videoEffectsContainer.filters =
|
||||
(zoomMotionBlurRef.current ?? 0) > 0 ? [motionBlurFilter, zoomBlurFilter] : null;
|
||||
motionBlurFilter.velocity = { x: 0, y: 0 };
|
||||
motionBlurFilter.kernelSize = 5;
|
||||
motionBlurFilter.offset = 0;
|
||||
@@ -1250,10 +1269,42 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
|
||||
cursorSpringMassMultiplierRef.current = cursorSpringMassMultiplier;
|
||||
}, [cursorSpringMassMultiplier]);
|
||||
|
||||
useEffect(() => {
|
||||
cameraSpringStiffnessMultiplierRef.current = cameraSpringStiffnessMultiplier;
|
||||
}, [cameraSpringStiffnessMultiplier]);
|
||||
|
||||
useEffect(() => {
|
||||
cameraSpringDampingMultiplierRef.current = cameraSpringDampingMultiplier;
|
||||
}, [cameraSpringDampingMultiplier]);
|
||||
|
||||
useEffect(() => {
|
||||
cameraSpringMassMultiplierRef.current = cameraSpringMassMultiplier;
|
||||
}, [cameraSpringMassMultiplier]);
|
||||
|
||||
useEffect(() => {
|
||||
zoomSmoothnessRef.current = zoomSmoothness;
|
||||
}, [zoomSmoothness]);
|
||||
|
||||
useEffect(() => {
|
||||
zoomMotionBlurRef.current = zoomMotionBlur;
|
||||
|
||||
const videoEffectsContainer = videoEffectsContainerRef.current;
|
||||
const zoomBlurFilter = zoomBlurFilterRef.current;
|
||||
const motionBlurFilter = motionBlurFilterRef.current;
|
||||
|
||||
if (!videoEffectsContainer || !zoomBlurFilter || !motionBlurFilter) {
|
||||
return;
|
||||
}
|
||||
|
||||
motionBlurStateRef.current = createMotionBlurState();
|
||||
videoEffectsContainer.filters =
|
||||
zoomMotionBlur > 0 ? [motionBlurFilter, zoomBlurFilter] : null;
|
||||
}, [videoPath, zoomMotionBlur]);
|
||||
|
||||
useEffect(() => {
|
||||
zoomMotionBlurTuningRef.current = zoomMotionBlurTuning;
|
||||
}, [zoomMotionBlurTuning]);
|
||||
|
||||
useEffect(() => {
|
||||
zoomClassicModeRef.current = zoomClassicMode;
|
||||
}, [zoomClassicMode]);
|
||||
@@ -1335,7 +1386,6 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
|
||||
zoomScale: 1,
|
||||
focusX: DEFAULT_FOCUS.cx,
|
||||
focusY: DEFAULT_FOCUS.cy,
|
||||
motionIntensity: 0,
|
||||
isPlaying: false,
|
||||
motionBlurAmount: 0,
|
||||
motionBlurState: motionBlurStateRef.current,
|
||||
@@ -1531,8 +1581,12 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
|
||||
// same layer in preview and export.
|
||||
const videoEffectsContainer = new Container();
|
||||
videoEffectsContainerRef.current = videoEffectsContainer;
|
||||
zoomBlurFilterRef.current = new ZoomBlurFilter({ strength: 0 });
|
||||
zoomBlurFilterRef.current = new ZoomBlurFilter({ strength: 0, maxKernelSize: 13 });
|
||||
motionBlurFilterRef.current = new MotionBlurFilter([0, 0], 5, 0);
|
||||
videoEffectsContainer.filters = [
|
||||
motionBlurFilterRef.current,
|
||||
zoomBlurFilterRef.current,
|
||||
];
|
||||
cameraContainer.addChild(videoEffectsContainer);
|
||||
syncPreviewMotionBlurQuality();
|
||||
|
||||
@@ -1726,8 +1780,6 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
|
||||
const applyTransform = (
|
||||
transform: { scale: number; x: number; y: number },
|
||||
focus: ZoomFocus,
|
||||
motionIntensity: number,
|
||||
motionVector: { x: number; y: number },
|
||||
) => {
|
||||
const cameraContainer = cameraContainerRef.current;
|
||||
if (!cameraContainer) return;
|
||||
@@ -1744,10 +1796,9 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
|
||||
zoomProgress: state.progress,
|
||||
focusX: focus.cx,
|
||||
focusY: focus.cy,
|
||||
motionIntensity,
|
||||
motionVector,
|
||||
isPlaying: isPlayingRef.current,
|
||||
motionBlurAmount: 0,
|
||||
motionBlurAmount: zoomMotionBlurRef.current,
|
||||
motionBlurTuning: zoomMotionBlurTuningRef.current,
|
||||
transformOverride: transform,
|
||||
motionBlurState: motionBlurStateRef.current,
|
||||
frameTimeMs: performance.now(),
|
||||
@@ -1848,9 +1899,6 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
|
||||
}
|
||||
|
||||
const state = animationStateRef.current;
|
||||
const prevScale = state.appliedScale;
|
||||
const prevX = state.x;
|
||||
const prevY = state.y;
|
||||
|
||||
state.scale = targetScaleFactor;
|
||||
state.focusX = targetFocus.cx;
|
||||
@@ -1880,7 +1928,11 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
|
||||
lastTickTimeRef.current !== null ? now - lastTickTimeRef.current : 1000 / 60;
|
||||
lastTickTimeRef.current = now;
|
||||
|
||||
const zoomSpringConfig = getZoomSpringConfig(zoomSmoothnessRef.current);
|
||||
const zoomSpringConfig = getZoomSpringConfig(zoomSmoothnessRef.current, {
|
||||
stiffnessMultiplier: cameraSpringStiffnessMultiplierRef.current,
|
||||
dampingMultiplier: cameraSpringDampingMultiplierRef.current,
|
||||
massMultiplier: cameraSpringMassMultiplierRef.current,
|
||||
});
|
||||
const useSpring =
|
||||
isPlayingRef.current && !isSeekingRef.current && !zoomClassicModeRef.current;
|
||||
|
||||
@@ -1917,22 +1969,9 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
|
||||
resetSpringState(springYRef.current, appliedY);
|
||||
}
|
||||
|
||||
const motionIntensity = Math.max(
|
||||
Math.abs(appliedScale - prevScale),
|
||||
Math.abs(appliedX - prevX) / Math.max(1, stageSizeRef.current.width),
|
||||
Math.abs(appliedY - prevY) / Math.max(1, stageSizeRef.current.height),
|
||||
);
|
||||
|
||||
const motionVector = {
|
||||
x: appliedX - prevX,
|
||||
y: appliedY - prevY,
|
||||
};
|
||||
|
||||
applyTransform(
|
||||
{ scale: appliedScale, x: appliedX, y: appliedY },
|
||||
targetFocus,
|
||||
motionIntensity,
|
||||
motionVector,
|
||||
);
|
||||
|
||||
applyWebcamBubbleLayout(animationStateRef.current.appliedScale || 1);
|
||||
|
||||
@@ -12,6 +12,7 @@ type PersistedEditorControls = Pick<
|
||||
| "shadowIntensity"
|
||||
| "backgroundBlur"
|
||||
| "zoomMotionBlur"
|
||||
| "zoomMotionBlurTuning"
|
||||
| "zoomTemporalMotionBlur"
|
||||
| "zoomMotionBlurSampleCount"
|
||||
| "zoomMotionBlurShutterFraction"
|
||||
@@ -32,6 +33,9 @@ type PersistedEditorControls = Pick<
|
||||
| "cursorSpringStiffnessMultiplier"
|
||||
| "cursorSpringDampingMultiplier"
|
||||
| "cursorSpringMassMultiplier"
|
||||
| "cameraSpringStiffnessMultiplier"
|
||||
| "cameraSpringDampingMultiplier"
|
||||
| "cameraSpringMassMultiplier"
|
||||
| "cursorMotionBlur"
|
||||
| "cursorClickBounce"
|
||||
| "cursorClickBounceDuration"
|
||||
@@ -89,6 +93,7 @@ export const DEFAULT_EDITOR_PREFERENCES: EditorPreferences = {
|
||||
shadowIntensity: DEFAULT_EDITOR_CONTROLS.shadowIntensity,
|
||||
backgroundBlur: DEFAULT_EDITOR_CONTROLS.backgroundBlur,
|
||||
zoomMotionBlur: DEFAULT_EDITOR_CONTROLS.zoomMotionBlur,
|
||||
zoomMotionBlurTuning: DEFAULT_EDITOR_CONTROLS.zoomMotionBlurTuning,
|
||||
zoomTemporalMotionBlur: DEFAULT_EDITOR_CONTROLS.zoomTemporalMotionBlur,
|
||||
zoomMotionBlurSampleCount: DEFAULT_EDITOR_CONTROLS.zoomMotionBlurSampleCount,
|
||||
zoomMotionBlurShutterFraction: DEFAULT_EDITOR_CONTROLS.zoomMotionBlurShutterFraction,
|
||||
@@ -109,6 +114,9 @@ export const DEFAULT_EDITOR_PREFERENCES: EditorPreferences = {
|
||||
cursorSpringStiffnessMultiplier: DEFAULT_EDITOR_CONTROLS.cursorSpringStiffnessMultiplier,
|
||||
cursorSpringDampingMultiplier: DEFAULT_EDITOR_CONTROLS.cursorSpringDampingMultiplier,
|
||||
cursorSpringMassMultiplier: DEFAULT_EDITOR_CONTROLS.cursorSpringMassMultiplier,
|
||||
cameraSpringStiffnessMultiplier: DEFAULT_EDITOR_CONTROLS.cameraSpringStiffnessMultiplier,
|
||||
cameraSpringDampingMultiplier: DEFAULT_EDITOR_CONTROLS.cameraSpringDampingMultiplier,
|
||||
cameraSpringMassMultiplier: DEFAULT_EDITOR_CONTROLS.cameraSpringMassMultiplier,
|
||||
cursorMotionBlur: DEFAULT_EDITOR_CONTROLS.cursorMotionBlur,
|
||||
cursorClickBounce: DEFAULT_EDITOR_CONTROLS.cursorClickBounce,
|
||||
cursorClickBounceDuration: DEFAULT_EDITOR_CONTROLS.cursorClickBounceDuration,
|
||||
@@ -264,6 +272,7 @@ function normalizeEditorControls(
|
||||
shadowIntensity: raw.shadowIntensity ?? fallback.shadowIntensity,
|
||||
backgroundBlur: raw.backgroundBlur ?? fallback.backgroundBlur,
|
||||
zoomMotionBlur: raw.zoomMotionBlur ?? fallback.zoomMotionBlur,
|
||||
zoomMotionBlurTuning: raw.zoomMotionBlurTuning ?? fallback.zoomMotionBlurTuning,
|
||||
zoomTemporalMotionBlur: raw.zoomTemporalMotionBlur ?? fallback.zoomTemporalMotionBlur,
|
||||
zoomMotionBlurSampleCount:
|
||||
raw.zoomMotionBlurSampleCount ?? fallback.zoomMotionBlurSampleCount,
|
||||
@@ -289,6 +298,12 @@ function normalizeEditorControls(
|
||||
raw.cursorSpringDampingMultiplier ?? fallback.cursorSpringDampingMultiplier,
|
||||
cursorSpringMassMultiplier:
|
||||
raw.cursorSpringMassMultiplier ?? fallback.cursorSpringMassMultiplier,
|
||||
cameraSpringStiffnessMultiplier:
|
||||
raw.cameraSpringStiffnessMultiplier ?? fallback.cameraSpringStiffnessMultiplier,
|
||||
cameraSpringDampingMultiplier:
|
||||
raw.cameraSpringDampingMultiplier ?? fallback.cameraSpringDampingMultiplier,
|
||||
cameraSpringMassMultiplier:
|
||||
raw.cameraSpringMassMultiplier ?? fallback.cameraSpringMassMultiplier,
|
||||
cursorMotionBlur: raw.cursorMotionBlur ?? fallback.cursorMotionBlur,
|
||||
cursorClickBounce: raw.cursorClickBounce ?? fallback.cursorClickBounce,
|
||||
cursorClickBounceDuration:
|
||||
@@ -326,6 +341,7 @@ function normalizeEditorControls(
|
||||
shadowIntensity: normalized.shadowIntensity,
|
||||
backgroundBlur: normalized.backgroundBlur,
|
||||
zoomMotionBlur: normalized.zoomMotionBlur,
|
||||
zoomMotionBlurTuning: normalized.zoomMotionBlurTuning,
|
||||
zoomTemporalMotionBlur: normalized.zoomTemporalMotionBlur,
|
||||
zoomMotionBlurSampleCount: normalized.zoomMotionBlurSampleCount,
|
||||
zoomMotionBlurShutterFraction: normalized.zoomMotionBlurShutterFraction,
|
||||
@@ -346,6 +362,9 @@ function normalizeEditorControls(
|
||||
cursorSpringStiffnessMultiplier: normalized.cursorSpringStiffnessMultiplier,
|
||||
cursorSpringDampingMultiplier: normalized.cursorSpringDampingMultiplier,
|
||||
cursorSpringMassMultiplier: normalized.cursorSpringMassMultiplier,
|
||||
cameraSpringStiffnessMultiplier: normalized.cameraSpringStiffnessMultiplier,
|
||||
cameraSpringDampingMultiplier: normalized.cameraSpringDampingMultiplier,
|
||||
cameraSpringMassMultiplier: normalized.cameraSpringMassMultiplier,
|
||||
cursorMotionBlur: normalized.cursorMotionBlur,
|
||||
cursorClickBounce: normalized.cursorClickBounce,
|
||||
cursorClickBounceDuration: normalized.cursorClickBounceDuration,
|
||||
|
||||
@@ -57,6 +57,7 @@ import {
|
||||
DEFAULT_ZOOM_IN_EASING,
|
||||
DEFAULT_ZOOM_IN_OVERLAP_MS,
|
||||
DEFAULT_ZOOM_MOTION_BLUR,
|
||||
DEFAULT_ZOOM_MOTION_BLUR_TUNING,
|
||||
DEFAULT_ZOOM_OUT_EASING,
|
||||
DEFAULT_ZOOM_SMOOTHNESS,
|
||||
getDefaultCaptionFontFamily,
|
||||
@@ -64,6 +65,7 @@ import {
|
||||
type SpeedRegion,
|
||||
type TrimRegion,
|
||||
type WebcamOverlaySettings,
|
||||
type ZoomMotionBlurTuning,
|
||||
type ZoomRegion,
|
||||
type ZoomTransitionEasing,
|
||||
} from "./types";
|
||||
@@ -78,6 +80,7 @@ export interface ProjectEditorState {
|
||||
shadowIntensity: number;
|
||||
backgroundBlur: number;
|
||||
zoomMotionBlur: number;
|
||||
zoomMotionBlurTuning: ZoomMotionBlurTuning;
|
||||
zoomTemporalMotionBlur: number;
|
||||
zoomMotionBlurSampleCount: number | null;
|
||||
zoomMotionBlurShutterFraction: number | null;
|
||||
@@ -98,6 +101,9 @@ export interface ProjectEditorState {
|
||||
cursorSpringStiffnessMultiplier: number;
|
||||
cursorSpringDampingMultiplier: number;
|
||||
cursorSpringMassMultiplier: number;
|
||||
cameraSpringStiffnessMultiplier: number;
|
||||
cameraSpringDampingMultiplier: number;
|
||||
cameraSpringMassMultiplier: number;
|
||||
zoomSmoothness: number;
|
||||
zoomClassicMode: boolean;
|
||||
cursorMotionBlur: number;
|
||||
@@ -337,6 +343,35 @@ export function normalizeProjectEditor(editor: Partial<ProjectEditorState>): Pro
|
||||
: legacyMotionBlurEnabled
|
||||
? 0.35
|
||||
: DEFAULT_ZOOM_MOTION_BLUR;
|
||||
const rawZoomMotionBlurTuning =
|
||||
(editor as Partial<ProjectEditorState>).zoomMotionBlurTuning &&
|
||||
typeof (editor as Partial<ProjectEditorState>).zoomMotionBlurTuning === "object"
|
||||
? ((editor as Partial<ProjectEditorState>)
|
||||
.zoomMotionBlurTuning as Partial<ZoomMotionBlurTuning>)
|
||||
: {};
|
||||
const normalizedZoomMotionBlurTuning: ZoomMotionBlurTuning = {
|
||||
panVelocityThreshold: isFiniteNumber(rawZoomMotionBlurTuning.panVelocityThreshold)
|
||||
? clamp(rawZoomMotionBlurTuning.panVelocityThreshold, 0, 240)
|
||||
: DEFAULT_ZOOM_MOTION_BLUR_TUNING.panVelocityThreshold,
|
||||
zoomVelocityThreshold: isFiniteNumber(rawZoomMotionBlurTuning.zoomVelocityThreshold)
|
||||
? clamp(rawZoomMotionBlurTuning.zoomVelocityThreshold, 0, 0.4)
|
||||
: DEFAULT_ZOOM_MOTION_BLUR_TUNING.zoomVelocityThreshold,
|
||||
maxDirectionalBlurPx: isFiniteNumber(rawZoomMotionBlurTuning.maxDirectionalBlurPx)
|
||||
? clamp(rawZoomMotionBlurTuning.maxDirectionalBlurPx, 0, 32)
|
||||
: DEFAULT_ZOOM_MOTION_BLUR_TUNING.maxDirectionalBlurPx,
|
||||
maxRadialBlurStrength: isFiniteNumber(rawZoomMotionBlurTuning.maxRadialBlurStrength)
|
||||
? clamp(rawZoomMotionBlurTuning.maxRadialBlurStrength, 0, 0.5)
|
||||
: DEFAULT_ZOOM_MOTION_BLUR_TUNING.maxRadialBlurStrength,
|
||||
panResponsePerSecond: isFiniteNumber(rawZoomMotionBlurTuning.panResponsePerSecond)
|
||||
? clamp(rawZoomMotionBlurTuning.panResponsePerSecond, 1, 30)
|
||||
: DEFAULT_ZOOM_MOTION_BLUR_TUNING.panResponsePerSecond,
|
||||
zoomResponsePerSecond: isFiniteNumber(rawZoomMotionBlurTuning.zoomResponsePerSecond)
|
||||
? clamp(rawZoomMotionBlurTuning.zoomResponsePerSecond, 1, 30)
|
||||
: DEFAULT_ZOOM_MOTION_BLUR_TUNING.zoomResponsePerSecond,
|
||||
zoomSafeZoneRadiusPx: isFiniteNumber(rawZoomMotionBlurTuning.zoomSafeZoneRadiusPx)
|
||||
? clamp(rawZoomMotionBlurTuning.zoomSafeZoneRadiusPx, 0, 80)
|
||||
: DEFAULT_ZOOM_MOTION_BLUR_TUNING.zoomSafeZoneRadiusPx,
|
||||
};
|
||||
const normalizedZoomTemporalMotionBlur = isFiniteNumber(
|
||||
(editor as Partial<ProjectEditorState>).zoomTemporalMotionBlur,
|
||||
)
|
||||
@@ -768,6 +803,7 @@ export function normalizeProjectEditor(editor: Partial<ProjectEditorState>): Pro
|
||||
shadowIntensity: typeof editor.shadowIntensity === "number" ? editor.shadowIntensity : 0.67,
|
||||
backgroundBlur: normalizedBackgroundBlur,
|
||||
zoomMotionBlur: normalizedZoomMotionBlur,
|
||||
zoomMotionBlurTuning: normalizedZoomMotionBlurTuning,
|
||||
zoomTemporalMotionBlur: normalizedZoomTemporalMotionBlur,
|
||||
zoomMotionBlurSampleCount: normalizedZoomMotionBlurSampleCount,
|
||||
zoomMotionBlurShutterFraction: normalizedZoomMotionBlurShutterFraction,
|
||||
@@ -801,6 +837,15 @@ export function normalizeProjectEditor(editor: Partial<ProjectEditorState>): Pro
|
||||
cursorSpringMassMultiplier: isFiniteNumber(editor.cursorSpringMassMultiplier)
|
||||
? clamp(editor.cursorSpringMassMultiplier, 0.25, 3)
|
||||
: DEFAULT_MOTION_PRESET.cursorSpringMassMultiplier,
|
||||
cameraSpringStiffnessMultiplier: isFiniteNumber(editor.cameraSpringStiffnessMultiplier)
|
||||
? clamp(editor.cameraSpringStiffnessMultiplier, 0.25, 3)
|
||||
: 1,
|
||||
cameraSpringDampingMultiplier: isFiniteNumber(editor.cameraSpringDampingMultiplier)
|
||||
? clamp(editor.cameraSpringDampingMultiplier, 0.25, 3)
|
||||
: 1,
|
||||
cameraSpringMassMultiplier: isFiniteNumber(editor.cameraSpringMassMultiplier)
|
||||
? clamp(editor.cameraSpringMassMultiplier, 0.25, 3)
|
||||
: 1,
|
||||
zoomSmoothness: DEFAULT_ZOOM_SMOOTHNESS,
|
||||
zoomClassicMode:
|
||||
typeof editor.zoomClassicMode === "boolean" ? editor.zoomClassicMode : false,
|
||||
|
||||
@@ -100,9 +100,28 @@ export const DEFAULT_CURSOR_SMOOTHING = 0.67;
|
||||
export const DEFAULT_CURSOR_MOTION_BLUR = 0.4;
|
||||
export const DEFAULT_CURSOR_CLICK_BOUNCE = 2.5;
|
||||
export const DEFAULT_CURSOR_CLICK_BOUNCE_DURATION = 350;
|
||||
export const DEFAULT_CURSOR_SWAY = 0.25;
|
||||
export const DEFAULT_CURSOR_SWAY = 0.4;
|
||||
export const DEFAULT_ZOOM_SMOOTHNESS = 0.5;
|
||||
export const DEFAULT_ZOOM_MOTION_BLUR = 0.35;
|
||||
export interface ZoomMotionBlurTuning {
|
||||
panVelocityThreshold: number;
|
||||
zoomVelocityThreshold: number;
|
||||
maxDirectionalBlurPx: number;
|
||||
maxRadialBlurStrength: number;
|
||||
panResponsePerSecond: number;
|
||||
zoomResponsePerSecond: number;
|
||||
zoomSafeZoneRadiusPx: number;
|
||||
}
|
||||
|
||||
export const DEFAULT_ZOOM_MOTION_BLUR_TUNING: ZoomMotionBlurTuning = {
|
||||
panVelocityThreshold: 24,
|
||||
zoomVelocityThreshold: 0.05,
|
||||
maxDirectionalBlurPx: 11,
|
||||
maxRadialBlurStrength: 0.14,
|
||||
panResponsePerSecond: 11,
|
||||
zoomResponsePerSecond: 9,
|
||||
zoomSafeZoneRadiusPx: 6,
|
||||
};
|
||||
export const DEFAULT_ZOOM_IN_DURATION_MS = 1522.575;
|
||||
export const DEFAULT_ZOOM_IN_OVERLAP_MS = 500;
|
||||
export const DEFAULT_ZOOM_OUT_DURATION_MS = 1015.05;
|
||||
@@ -201,9 +220,7 @@ export function mapTimelineTimeToSourceTime(timeMs: number, clips: ClipRegion[])
|
||||
continue;
|
||||
}
|
||||
|
||||
return Math.round(
|
||||
clip.startMs + (roundedTimeMs - clip.startMs) * getSafeClipSpeed(clip),
|
||||
);
|
||||
return Math.round(clip.startMs + (roundedTimeMs - clip.startMs) * getSafeClipSpeed(clip));
|
||||
}
|
||||
|
||||
if (sortedClips.length === 0) {
|
||||
@@ -223,9 +240,7 @@ export function mapSourceTimeToTimelineTime(timeMs: number, clips: ClipRegion[])
|
||||
continue;
|
||||
}
|
||||
|
||||
return Math.round(
|
||||
clip.startMs + (roundedTimeMs - clip.startMs) / getSafeClipSpeed(clip),
|
||||
);
|
||||
return Math.round(clip.startMs + (roundedTimeMs - clip.startMs) / getSafeClipSpeed(clip));
|
||||
}
|
||||
|
||||
if (sortedClips.length === 0) {
|
||||
|
||||
@@ -272,17 +272,20 @@ export function getCursorSpringConfig(
|
||||
}, tuning);
|
||||
}
|
||||
|
||||
export function getZoomSpringConfig(smoothnessFactor = 0.5): SpringConfig {
|
||||
export function getZoomSpringConfig(
|
||||
smoothnessFactor = 0.5,
|
||||
tuning?: CursorSpringTuning,
|
||||
): SpringConfig {
|
||||
const clamped = Math.max(0, Math.min(1, smoothnessFactor));
|
||||
|
||||
if (clamped <= 0) {
|
||||
return {
|
||||
return applyCursorSpringTuning({
|
||||
stiffness: 1000,
|
||||
damping: 100,
|
||||
mass: 1,
|
||||
restDelta: 0.0001,
|
||||
restSpeed: 0.001,
|
||||
};
|
||||
}, tuning);
|
||||
}
|
||||
|
||||
// Map 0-1 slider to the internal 0-2 spring range so that
|
||||
@@ -294,11 +297,11 @@ export function getZoomSpringConfig(smoothnessFactor = 0.5): SpringConfig {
|
||||
// The overshoot clamp in stepSpringValue prevents wobble even at
|
||||
// this low damping, so animations stay fast and responsive.
|
||||
// Higher scaled → lower stiffness + higher mass → slower, floatier settle.
|
||||
return {
|
||||
return applyCursorSpringTuning({
|
||||
stiffness: 100 / scaled,
|
||||
damping: 21,
|
||||
mass: 1.0 * scaled,
|
||||
restDelta: 0.0005,
|
||||
restSpeed: 0.015,
|
||||
};
|
||||
}, tuning);
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { applyZoomTransform, computeZoomTransform, createMotionBlurState } from "./zoomTransform";
|
||||
|
||||
function createStubContainer() {
|
||||
return {
|
||||
scale: { set: () => undefined },
|
||||
position: { set: () => undefined },
|
||||
};
|
||||
}
|
||||
|
||||
describe("applyZoomTransform motion blur routing", () => {
|
||||
it("keeps pure translation on the directional blur path", () => {
|
||||
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, x: 96, y: 24 },
|
||||
frameTimeMs: 1016,
|
||||
});
|
||||
|
||||
expect(
|
||||
Math.hypot(motionBlurFilter.velocity.x, motionBlurFilter.velocity.y),
|
||||
).toBeGreaterThan(0);
|
||||
expect(zoomBlurFilter.strength).toBe(0);
|
||||
});
|
||||
|
||||
it("infers an off-center zoom origin for radial blur", () => {
|
||||
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 stageSize = { width: 1000, height: 800 };
|
||||
const baseMask = { x: 100, y: 100, width: 800, height: 600 };
|
||||
const focusX = 0.2;
|
||||
const focusY = 0.7;
|
||||
|
||||
applyZoomTransform({
|
||||
cameraContainer: createStubContainer() as never,
|
||||
zoomBlurFilter: zoomBlurFilter as never,
|
||||
motionBlurFilter: motionBlurFilter as never,
|
||||
stageSize,
|
||||
baseMask,
|
||||
zoomScale: 1,
|
||||
focusX,
|
||||
focusY,
|
||||
isPlaying: true,
|
||||
motionBlurAmount: 1,
|
||||
motionBlurState,
|
||||
transformOverride: computeZoomTransform({
|
||||
stageSize,
|
||||
baseMask,
|
||||
zoomScale: 1,
|
||||
focusX,
|
||||
focusY,
|
||||
}),
|
||||
frameTimeMs: 1000,
|
||||
});
|
||||
applyZoomTransform({
|
||||
cameraContainer: createStubContainer() as never,
|
||||
zoomBlurFilter: zoomBlurFilter as never,
|
||||
motionBlurFilter: motionBlurFilter as never,
|
||||
stageSize,
|
||||
baseMask,
|
||||
zoomScale: 1.7,
|
||||
focusX,
|
||||
focusY,
|
||||
isPlaying: true,
|
||||
motionBlurAmount: 1,
|
||||
motionBlurState,
|
||||
transformOverride: computeZoomTransform({
|
||||
stageSize,
|
||||
baseMask,
|
||||
zoomScale: 1.7,
|
||||
focusX,
|
||||
focusY,
|
||||
}),
|
||||
frameTimeMs: 1016,
|
||||
});
|
||||
|
||||
expect(Math.abs(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);
|
||||
});
|
||||
});
|
||||
@@ -1,26 +1,17 @@
|
||||
import { Container } from "pixi.js";
|
||||
import { MotionBlurFilter } from "pixi-filters/motion-blur";
|
||||
import { ZoomBlurFilter } from "pixi-filters/zoom-blur";
|
||||
import { DEFAULT_ZOOM_MOTION_BLUR_TUNING, type ZoomMotionBlurTuning } from "../types";
|
||||
|
||||
const PEAK_TRANSLATION_VELOCITY_PPS = 1800;
|
||||
const PAN_VELOCITY_THRESHOLD_PPS = 24;
|
||||
const PEAK_LOG_SCALE_VELOCITY_PER_SECOND = 1.45;
|
||||
const LOG_SCALE_VELOCITY_THRESHOLD = 0.05;
|
||||
const MAX_DIRECTIONAL_BLUR_PX = 7.5;
|
||||
const MAX_ZOOM_BLUR_STRENGTH = 0.14;
|
||||
const PAN_RESPONSE_PER_SECOND = 11;
|
||||
const ZOOM_RESPONSE_PER_SECOND = 9;
|
||||
const MIN_DIRECTIONAL_BLUR_MAGNITUDE = 0.01;
|
||||
const DIRECTIONAL_BLUR_KERNEL_SIZE = 13;
|
||||
const DIRECTIONAL_BLUR_OFFSET_DIVISOR = 32;
|
||||
|
||||
export interface MotionBlurState {
|
||||
lastFrameTimeMs: number;
|
||||
prevCamX: number;
|
||||
prevCamY: number;
|
||||
prevCamScale: number;
|
||||
smoothedPanVelocityX: number;
|
||||
smoothedPanVelocityY: number;
|
||||
smoothedLogScaleVelocity: number;
|
||||
smoothedDirectionalBlur: number;
|
||||
smoothedRadialBlur: number;
|
||||
initialized: boolean;
|
||||
}
|
||||
|
||||
@@ -30,11 +21,6 @@ export function createMotionBlurState(): MotionBlurState {
|
||||
prevCamX: 0,
|
||||
prevCamY: 0,
|
||||
prevCamScale: 1,
|
||||
smoothedPanVelocityX: 0,
|
||||
smoothedPanVelocityY: 0,
|
||||
smoothedLogScaleVelocity: 0,
|
||||
smoothedDirectionalBlur: 0,
|
||||
smoothedRadialBlur: 0,
|
||||
initialized: false,
|
||||
};
|
||||
}
|
||||
@@ -49,10 +35,9 @@ interface TransformParams {
|
||||
zoomProgress?: number;
|
||||
focusX: number;
|
||||
focusY: number;
|
||||
motionIntensity: number;
|
||||
motionVector?: { x: number; y: number };
|
||||
isPlaying: boolean;
|
||||
motionBlurAmount?: number;
|
||||
motionBlurTuning?: ZoomMotionBlurTuning;
|
||||
transformOverride?: AppliedTransform;
|
||||
motionBlurState?: MotionBlurState;
|
||||
frameTimeMs?: number;
|
||||
@@ -64,6 +49,27 @@ interface AppliedTransform {
|
||||
y: number;
|
||||
}
|
||||
|
||||
interface Point2D {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
interface TransformQuad {
|
||||
center: Point2D;
|
||||
corners: [Point2D, Point2D, Point2D, Point2D];
|
||||
diagonal: number;
|
||||
size: Point2D;
|
||||
}
|
||||
|
||||
interface CameraStepAnalysis {
|
||||
mode: "move" | "zoom" | null;
|
||||
moveVelocity: Point2D;
|
||||
moveBlurVelocity: Point2D;
|
||||
moveBlurOffset: number;
|
||||
zoomCenter: Point2D;
|
||||
zoomStrength: number;
|
||||
}
|
||||
|
||||
interface FocusFromTransformGeometry {
|
||||
stageSize: { width: number; height: number };
|
||||
baseMask: { x: number; y: number; width: number; height: number };
|
||||
@@ -103,87 +109,295 @@ function resetMotionEffects(
|
||||
motionBlurState.prevCamX = 0;
|
||||
motionBlurState.prevCamY = 0;
|
||||
motionBlurState.prevCamScale = 1;
|
||||
motionBlurState.smoothedPanVelocityX = 0;
|
||||
motionBlurState.smoothedPanVelocityY = 0;
|
||||
motionBlurState.smoothedLogScaleVelocity = 0;
|
||||
motionBlurState.smoothedDirectionalBlur = 0;
|
||||
motionBlurState.smoothedRadialBlur = 0;
|
||||
motionBlurState.initialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
function mixTowards(current: number, target: number, blendFactor: number) {
|
||||
return current + (target - current) * blendFactor;
|
||||
}
|
||||
|
||||
function computeSmoothingBlend(deltaSeconds: number, responsePerSecond: number) {
|
||||
return 1 - Math.exp(-Math.max(0, deltaSeconds) * responsePerSecond);
|
||||
}
|
||||
|
||||
function remapMotionStrength(value: number, threshold: number, peak: number) {
|
||||
if (!Number.isFinite(value) || value <= threshold) {
|
||||
return 0;
|
||||
function resolveMotionBlurTuning(motionBlurTuning?: ZoomMotionBlurTuning): ZoomMotionBlurTuning {
|
||||
if (!motionBlurTuning) {
|
||||
return DEFAULT_ZOOM_MOTION_BLUR_TUNING;
|
||||
}
|
||||
|
||||
const span = Math.max(0.0001, peak - threshold);
|
||||
return Math.min(1, (value - threshold) / span);
|
||||
}
|
||||
|
||||
function squareEase(value: number) {
|
||||
return value * value;
|
||||
}
|
||||
|
||||
function resolveDirectionalKernelSize(blurStrength: number) {
|
||||
if (blurStrength >= 5.5) {
|
||||
return 13;
|
||||
}
|
||||
|
||||
if (blurStrength >= 3) {
|
||||
return 11;
|
||||
}
|
||||
|
||||
if (blurStrength >= 1.25) {
|
||||
return 7;
|
||||
}
|
||||
|
||||
return 5;
|
||||
}
|
||||
|
||||
function computeZoomBlurGeometry({
|
||||
stageSize,
|
||||
baseMask,
|
||||
targetZoomScale,
|
||||
focusX,
|
||||
focusY,
|
||||
}: {
|
||||
stageSize: { width: number; height: number };
|
||||
baseMask: { x: number; y: number; width: number; height: number };
|
||||
targetZoomScale: number;
|
||||
focusX: number;
|
||||
focusY: number;
|
||||
}) {
|
||||
const safeZoomScale = Math.max(1, targetZoomScale);
|
||||
const visibleWidth = stageSize.width / safeZoomScale;
|
||||
const visibleHeight = stageSize.height / safeZoomScale;
|
||||
const visibleHalfDiagonal = Math.hypot(visibleWidth / 2, visibleHeight / 2);
|
||||
const focusStagePxX = baseMask.x + focusX * baseMask.width;
|
||||
const focusStagePxY = baseMask.y + focusY * baseMask.height;
|
||||
const outerRadius = Math.max(
|
||||
Math.hypot(focusStagePxX, focusStagePxY),
|
||||
Math.hypot(stageSize.width - focusStagePxX, focusStagePxY),
|
||||
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(0, Math.min(Math.max(18, Math.min(radius - 1, visibleHalfDiagonal)), radius - 1)),
|
||||
radius,
|
||||
...DEFAULT_ZOOM_MOTION_BLUR_TUNING,
|
||||
...motionBlurTuning,
|
||||
};
|
||||
}
|
||||
|
||||
function computeTransformQuad(
|
||||
baseMask: { x: number; y: number; width: number; height: number },
|
||||
transform: AppliedTransform,
|
||||
): TransformQuad {
|
||||
const topLeft = {
|
||||
x: transform.x + baseMask.x * transform.scale,
|
||||
y: transform.y + baseMask.y * transform.scale,
|
||||
};
|
||||
const topRight = {
|
||||
x: transform.x + (baseMask.x + baseMask.width) * transform.scale,
|
||||
y: transform.y + baseMask.y * transform.scale,
|
||||
};
|
||||
const bottomRight = {
|
||||
x: transform.x + (baseMask.x + baseMask.width) * transform.scale,
|
||||
y: transform.y + (baseMask.y + baseMask.height) * transform.scale,
|
||||
};
|
||||
const bottomLeft = {
|
||||
x: transform.x + baseMask.x * transform.scale,
|
||||
y: transform.y + (baseMask.y + baseMask.height) * transform.scale,
|
||||
};
|
||||
const center = {
|
||||
x: (topLeft.x + bottomRight.x) * 0.5,
|
||||
y: (topLeft.y + bottomRight.y) * 0.5,
|
||||
};
|
||||
|
||||
return {
|
||||
center,
|
||||
corners: [topLeft, topRight, bottomRight, bottomLeft],
|
||||
diagonal: Math.hypot(bottomRight.x - topLeft.x, bottomRight.y - topLeft.y),
|
||||
size: {
|
||||
x: Math.hypot(topRight.x - topLeft.x, topRight.y - topLeft.y),
|
||||
y: Math.hypot(bottomLeft.x - topLeft.x, bottomLeft.y - topLeft.y),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function crossProduct(a: Point2D, b: Point2D) {
|
||||
return a.x * b.y - a.y * b.x;
|
||||
}
|
||||
|
||||
function clamp(value: number, min: number, max: number) {
|
||||
return Math.min(max, Math.max(min, value));
|
||||
}
|
||||
|
||||
function intersectInfiniteLines(
|
||||
aStart: Point2D,
|
||||
aEnd: Point2D,
|
||||
bStart: Point2D,
|
||||
bEnd: Point2D,
|
||||
): Point2D | null {
|
||||
const aDelta = { x: aEnd.x - aStart.x, y: aEnd.y - aStart.y };
|
||||
const bDelta = { x: bEnd.x - bStart.x, y: bEnd.y - bStart.y };
|
||||
const denominator = crossProduct(aDelta, bDelta);
|
||||
|
||||
if (Math.abs(denominator) < 0.0001) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const originDelta = { x: bStart.x - aStart.x, y: bStart.y - aStart.y };
|
||||
const t = crossProduct(originDelta, bDelta) / denominator;
|
||||
|
||||
return {
|
||||
x: aStart.x + aDelta.x * t,
|
||||
y: aStart.y + aDelta.y * t,
|
||||
};
|
||||
}
|
||||
|
||||
function inferZoomCenterFromQuads(
|
||||
previousQuad: TransformQuad,
|
||||
currentQuad: TransformQuad,
|
||||
stageSize: { width: number; height: number },
|
||||
) {
|
||||
const hasMeaningfulChange =
|
||||
Math.abs(currentQuad.center.x - previousQuad.center.x) > 0.001 ||
|
||||
Math.abs(currentQuad.center.y - previousQuad.center.y) > 0.001 ||
|
||||
Math.abs(currentQuad.diagonal - previousQuad.diagonal) > 0.001;
|
||||
|
||||
if (!hasMeaningfulChange) {
|
||||
return previousQuad.center;
|
||||
}
|
||||
|
||||
const motionSegments: Array<{ start: Point2D; end: Point2D }> = [];
|
||||
|
||||
for (let i = 0; i < previousQuad.corners.length; i += 1) {
|
||||
const start = previousQuad.corners[i];
|
||||
const end = currentQuad.corners[i];
|
||||
if (Math.hypot(end.x - start.x, end.y - start.y) < 0.5) {
|
||||
continue;
|
||||
}
|
||||
motionSegments.push({ start, end });
|
||||
}
|
||||
|
||||
for (let i = 0; i < motionSegments.length; i += 1) {
|
||||
for (let j = i + 1; j < motionSegments.length; j += 1) {
|
||||
const intersection = intersectInfiniteLines(
|
||||
motionSegments[i].start,
|
||||
motionSegments[i].end,
|
||||
motionSegments[j].start,
|
||||
motionSegments[j].end,
|
||||
);
|
||||
|
||||
if (!intersection) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const marginX = stageSize.width;
|
||||
const marginY = stageSize.height;
|
||||
|
||||
return {
|
||||
x: clamp(intersection.x, -marginX, stageSize.width + marginX),
|
||||
y: clamp(intersection.y, -marginY, stageSize.height + marginY),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return currentQuad.center;
|
||||
}
|
||||
|
||||
function resolveFpsScale(deltaSeconds: number) {
|
||||
const fps = 1 / Math.max(1 / 240, deltaSeconds);
|
||||
return fps / 60;
|
||||
}
|
||||
|
||||
function resolveBlurChannels(
|
||||
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),
|
||||
};
|
||||
}
|
||||
|
||||
function computeMoveDelta(previousQuad: TransformQuad, currentQuad: TransformQuad) {
|
||||
return {
|
||||
x: currentQuad.center.x - previousQuad.center.x,
|
||||
y: currentQuad.center.y - previousQuad.center.y,
|
||||
};
|
||||
}
|
||||
|
||||
function computeSizeDelta(previousQuad: TransformQuad, currentQuad: TransformQuad) {
|
||||
return {
|
||||
x: Math.abs(currentQuad.size.x - previousQuad.size.x),
|
||||
y: Math.abs(currentQuad.size.y - previousQuad.size.y),
|
||||
};
|
||||
}
|
||||
|
||||
function computeZoomStrength(previousQuad: TransformQuad, currentQuad: TransformQuad) {
|
||||
return Math.abs(1 - currentQuad.diagonal / Math.max(0.0001, previousQuad.diagonal));
|
||||
}
|
||||
|
||||
function classifyMotionMode(
|
||||
previousQuad: TransformQuad,
|
||||
currentQuad: TransformQuad,
|
||||
motionBlurTuning: ZoomMotionBlurTuning,
|
||||
deltaSeconds: number,
|
||||
) {
|
||||
const moveDelta = computeMoveDelta(previousQuad, currentQuad);
|
||||
const sizeDelta = computeSizeDelta(previousQuad, currentQuad);
|
||||
const moveDistance = Math.hypot(moveDelta.x, moveDelta.y);
|
||||
const zoomDistance = Math.hypot(sizeDelta.x, sizeDelta.y);
|
||||
const moveVelocity = Math.hypot(moveDelta.x, moveDelta.y) / Math.max(0.0001, deltaSeconds);
|
||||
const zoomVelocity =
|
||||
Math.abs(
|
||||
Math.log(
|
||||
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;
|
||||
|
||||
if (!moveActive && !zoomActive) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (moveActive && !zoomActive) {
|
||||
return "move";
|
||||
}
|
||||
|
||||
if (zoomActive && !moveActive) {
|
||||
return "zoom";
|
||||
}
|
||||
|
||||
return zoomDistance > moveDistance ? "zoom" : "move";
|
||||
}
|
||||
|
||||
function createZeroPoint(): Point2D {
|
||||
return { x: 0, y: 0 };
|
||||
}
|
||||
|
||||
function analyzeCameraStep({
|
||||
previousQuad,
|
||||
currentQuad,
|
||||
stageSize,
|
||||
motionBlurAmount,
|
||||
motionBlurTuning,
|
||||
deltaSeconds,
|
||||
}: {
|
||||
previousQuad: TransformQuad;
|
||||
currentQuad: TransformQuad;
|
||||
stageSize: { width: number; height: number };
|
||||
motionBlurAmount: number;
|
||||
motionBlurTuning: ZoomMotionBlurTuning;
|
||||
deltaSeconds: number;
|
||||
}): CameraStepAnalysis {
|
||||
const mode = classifyMotionMode(
|
||||
previousQuad,
|
||||
currentQuad,
|
||||
motionBlurTuning,
|
||||
deltaSeconds,
|
||||
);
|
||||
const moveDelta = computeMoveDelta(previousQuad, currentQuad);
|
||||
const blurChannels = resolveBlurChannels(
|
||||
motionBlurAmount,
|
||||
motionBlurTuning,
|
||||
deltaSeconds,
|
||||
);
|
||||
const moveBlurVelocity = {
|
||||
x: moveDelta.x * blurChannels.motion,
|
||||
y: moveDelta.y * blurChannels.motion,
|
||||
};
|
||||
const moveBlurMagnitude = Math.hypot(moveBlurVelocity.x, moveBlurVelocity.y);
|
||||
|
||||
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,
|
||||
zoomCenter: inferZoomCenterFromQuads(previousQuad, currentQuad, stageSize),
|
||||
zoomStrength:
|
||||
mode === "zoom"
|
||||
? computeZoomStrength(previousQuad, currentQuad) * blurChannels.zoom
|
||||
: 0,
|
||||
};
|
||||
}
|
||||
|
||||
function applyCameraStepBlur({
|
||||
analysis,
|
||||
motionBlurFilter,
|
||||
zoomBlurFilter,
|
||||
}: {
|
||||
analysis: CameraStepAnalysis;
|
||||
motionBlurFilter: MotionBlurFilter;
|
||||
zoomBlurFilter?: ZoomBlurFilter | null;
|
||||
}) {
|
||||
motionBlurFilter.velocity = analysis.moveBlurVelocity;
|
||||
motionBlurFilter.kernelSize = DIRECTIONAL_BLUR_KERNEL_SIZE;
|
||||
motionBlurFilter.offset = analysis.moveBlurOffset;
|
||||
|
||||
if (zoomBlurFilter) {
|
||||
zoomBlurFilter.center = analysis.zoomCenter;
|
||||
zoomBlurFilter.strength = analysis.zoomStrength;
|
||||
zoomBlurFilter.innerRadius = 0;
|
||||
zoomBlurFilter.radius = -1;
|
||||
}
|
||||
}
|
||||
|
||||
export function computeZoomTransform({
|
||||
stageSize,
|
||||
baseMask,
|
||||
@@ -255,10 +469,9 @@ export function applyZoomTransform({
|
||||
zoomProgress = 1,
|
||||
focusX,
|
||||
focusY,
|
||||
motionIntensity: _motionIntensity,
|
||||
motionVector: _motionVector,
|
||||
isPlaying,
|
||||
motionBlurAmount = 0,
|
||||
motionBlurTuning,
|
||||
transformOverride,
|
||||
motionBlurState,
|
||||
frameTimeMs,
|
||||
@@ -289,6 +502,7 @@ export function applyZoomTransform({
|
||||
// Apply position & scale to camera container
|
||||
cameraContainer.scale.set(transform.scale);
|
||||
cameraContainer.position.set(transform.x, transform.y);
|
||||
const resolvedTuning = resolveMotionBlurTuning(motionBlurTuning);
|
||||
|
||||
if (motionBlurState && motionBlurFilter && motionBlurAmount > 0 && isPlaying) {
|
||||
const now = frameTimeMs ?? performance.now();
|
||||
@@ -300,7 +514,7 @@ export function applyZoomTransform({
|
||||
motionBlurState.lastFrameTimeMs = now;
|
||||
motionBlurState.initialized = true;
|
||||
motionBlurFilter.velocity = { x: 0, y: 0 };
|
||||
motionBlurFilter.kernelSize = 5;
|
||||
motionBlurFilter.kernelSize = DIRECTIONAL_BLUR_KERNEL_SIZE;
|
||||
motionBlurFilter.offset = 0;
|
||||
if (zoomBlurFilter) {
|
||||
zoomBlurFilter.strength = 0;
|
||||
@@ -310,104 +524,26 @@ export function applyZoomTransform({
|
||||
const dtSeconds = dtMs / 1000;
|
||||
motionBlurState.lastFrameTimeMs = now;
|
||||
|
||||
const dx = transform.x - motionBlurState.prevCamX;
|
||||
const dy = transform.y - motionBlurState.prevCamY;
|
||||
const previousScale = Math.max(0.0001, motionBlurState.prevCamScale);
|
||||
const scaleRatio = Math.max(0.0001, transform.scale) / previousScale;
|
||||
const logScaleVelocity = Math.log(scaleRatio) / dtSeconds;
|
||||
const previousTransform = {
|
||||
scale: motionBlurState.prevCamScale,
|
||||
x: motionBlurState.prevCamX,
|
||||
y: motionBlurState.prevCamY,
|
||||
};
|
||||
const previousQuad = computeTransformQuad(baseMask, previousTransform);
|
||||
const currentQuad = computeTransformQuad(baseMask, transform);
|
||||
const analysis = analyzeCameraStep({
|
||||
previousQuad,
|
||||
currentQuad,
|
||||
stageSize,
|
||||
motionBlurAmount,
|
||||
motionBlurTuning: resolvedTuning,
|
||||
deltaSeconds: dtSeconds,
|
||||
});
|
||||
|
||||
motionBlurState.prevCamX = transform.x;
|
||||
motionBlurState.prevCamY = transform.y;
|
||||
motionBlurState.prevCamScale = transform.scale;
|
||||
|
||||
const smoothingBlend = computeSmoothingBlend(dtSeconds, PAN_RESPONSE_PER_SECOND);
|
||||
const zoomSmoothingBlend = computeSmoothingBlend(dtSeconds, ZOOM_RESPONSE_PER_SECOND);
|
||||
const rawVelocityX = dx / dtSeconds;
|
||||
const rawVelocityY = dy / dtSeconds;
|
||||
motionBlurState.smoothedPanVelocityX = mixTowards(
|
||||
motionBlurState.smoothedPanVelocityX,
|
||||
rawVelocityX,
|
||||
smoothingBlend,
|
||||
);
|
||||
motionBlurState.smoothedPanVelocityY = mixTowards(
|
||||
motionBlurState.smoothedPanVelocityY,
|
||||
rawVelocityY,
|
||||
smoothingBlend,
|
||||
);
|
||||
motionBlurState.smoothedLogScaleVelocity = mixTowards(
|
||||
motionBlurState.smoothedLogScaleVelocity,
|
||||
logScaleVelocity,
|
||||
zoomSmoothingBlend,
|
||||
);
|
||||
|
||||
const panVelocityX = motionBlurState.smoothedPanVelocityX;
|
||||
const panVelocityY = motionBlurState.smoothedPanVelocityY;
|
||||
const panSpeed = Math.hypot(panVelocityX, panVelocityY);
|
||||
const panStrength = squareEase(
|
||||
remapMotionStrength(
|
||||
panSpeed,
|
||||
PAN_VELOCITY_THRESHOLD_PPS,
|
||||
PEAK_TRANSLATION_VELOCITY_PPS,
|
||||
),
|
||||
);
|
||||
const zoomStrength = squareEase(
|
||||
remapMotionStrength(
|
||||
Math.abs(motionBlurState.smoothedLogScaleVelocity),
|
||||
LOG_SCALE_VELOCITY_THRESHOLD,
|
||||
PEAK_LOG_SCALE_VELOCITY_PER_SECOND,
|
||||
),
|
||||
);
|
||||
|
||||
const targetDirectionalBlur =
|
||||
panStrength *
|
||||
(1 - Math.min(0.82, zoomStrength * 0.85)) *
|
||||
MAX_DIRECTIONAL_BLUR_PX *
|
||||
motionBlurAmount;
|
||||
const targetRadialBlur = zoomStrength * MAX_ZOOM_BLUR_STRENGTH * motionBlurAmount;
|
||||
|
||||
motionBlurState.smoothedDirectionalBlur = mixTowards(
|
||||
motionBlurState.smoothedDirectionalBlur,
|
||||
targetDirectionalBlur,
|
||||
smoothingBlend,
|
||||
);
|
||||
motionBlurState.smoothedRadialBlur = mixTowards(
|
||||
motionBlurState.smoothedRadialBlur,
|
||||
targetRadialBlur,
|
||||
zoomSmoothingBlend,
|
||||
);
|
||||
|
||||
const directionalBlur = motionBlurState.smoothedDirectionalBlur;
|
||||
const radialBlur = motionBlurState.smoothedRadialBlur;
|
||||
const dirMag = Math.hypot(panVelocityX, panVelocityY) || 1;
|
||||
const velocityScale = directionalBlur * 1.1;
|
||||
motionBlurFilter.velocity =
|
||||
directionalBlur > 0.15
|
||||
? {
|
||||
x: (panVelocityX / dirMag) * velocityScale,
|
||||
y: (panVelocityY / dirMag) * velocityScale,
|
||||
}
|
||||
: { x: 0, y: 0 };
|
||||
motionBlurFilter.kernelSize = resolveDirectionalKernelSize(directionalBlur);
|
||||
motionBlurFilter.offset = directionalBlur > 0.45 ? -0.12 : 0;
|
||||
|
||||
if (zoomBlurFilter) {
|
||||
const zoomBlurGeometry = computeZoomBlurGeometry({
|
||||
stageSize,
|
||||
baseMask,
|
||||
targetZoomScale: Math.max(zoomScale, transform.scale, 1),
|
||||
focusX,
|
||||
focusY,
|
||||
});
|
||||
|
||||
zoomBlurFilter.center = {
|
||||
x: zoomBlurGeometry.centerX,
|
||||
y: zoomBlurGeometry.centerY,
|
||||
};
|
||||
zoomBlurFilter.innerRadius = zoomBlurGeometry.innerRadius;
|
||||
zoomBlurFilter.radius = zoomBlurGeometry.radius;
|
||||
zoomBlurFilter.strength =
|
||||
radialBlur * (motionBlurState.smoothedLogScaleVelocity >= 0 ? 0.88 : 1);
|
||||
}
|
||||
applyCameraStepBlur({ analysis, motionBlurFilter, zoomBlurFilter });
|
||||
}
|
||||
} else {
|
||||
resetMotionEffects(zoomBlurFilter, motionBlurFilter, motionBlurState);
|
||||
|
||||
@@ -113,6 +113,11 @@
|
||||
"project": {
|
||||
"untitled": "Untitled"
|
||||
},
|
||||
"nativeCaptureUnavailable": {
|
||||
"title": "Nothing’s broken, but we won’t be able to render an animated cursor overlay.",
|
||||
"description": "Your device does not support native capture. This could be for a variety of reasons we haven’t figured out yet. This doesn’t break Recordly, but it does make cursor smoothing impossible.",
|
||||
"confirm": "Okay"
|
||||
},
|
||||
"exportStatus": {
|
||||
"exporting": "Exporting",
|
||||
"renderingFile": "Rendering your file.",
|
||||
|
||||
@@ -113,6 +113,11 @@
|
||||
"project": {
|
||||
"untitled": "Sin título"
|
||||
},
|
||||
"nativeCaptureUnavailable": {
|
||||
"title": "Nada está roto, pero no podremos renderizar una superposición de cursor animada.",
|
||||
"description": "Tu dispositivo no es compatible con la captura nativa. Esto puede deberse a varias razones que todavía no hemos identificado. Recordly seguirá funcionando, pero hará imposible el suavizado del cursor.",
|
||||
"confirm": "Entendido"
|
||||
},
|
||||
"exportStatus": {
|
||||
"exporting": "Exportando",
|
||||
"renderingFile": "Renderizando tu archivo.",
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
Padding,
|
||||
SpeedRegion,
|
||||
WebcamOverlaySettings,
|
||||
ZoomMotionBlurTuning,
|
||||
ZoomRegion,
|
||||
ZoomTransitionEasing,
|
||||
} from "@/components/video-editor/types";
|
||||
@@ -75,10 +76,9 @@ import { renderAnnotations } from "./annotationRenderer";
|
||||
import { renderCaptions } from "./captionRenderer";
|
||||
import { ForwardFrameSource } from "./forwardFrameSource";
|
||||
import { resolveMediaElementSource } from "./localMediaSource";
|
||||
import {
|
||||
buildTemporalSamplePlanUs,
|
||||
getTemporalMotionBlurConfig,
|
||||
} from "./temporalMotionBlur";
|
||||
import { buildTemporalSamplePlanUs, getTemporalMotionBlurConfig } from "./temporalMotionBlur";
|
||||
|
||||
const TEMPORAL_ZOOM_MOTION_BLUR_ENABLED = false;
|
||||
|
||||
interface FrameRenderConfig {
|
||||
width: number;
|
||||
@@ -90,6 +90,7 @@ interface FrameRenderConfig {
|
||||
shadowIntensity: number;
|
||||
backgroundBlur: number;
|
||||
zoomMotionBlur?: number;
|
||||
zoomMotionBlurTuning?: ZoomMotionBlurTuning;
|
||||
zoomTemporalMotionBlur?: number;
|
||||
zoomMotionBlurSampleCount?: number | null;
|
||||
zoomMotionBlurShutterFraction?: number | null;
|
||||
@@ -123,6 +124,9 @@ interface FrameRenderConfig {
|
||||
cursorSpringStiffnessMultiplier?: number;
|
||||
cursorSpringDampingMultiplier?: number;
|
||||
cursorSpringMassMultiplier?: number;
|
||||
cameraSpringStiffnessMultiplier?: number;
|
||||
cameraSpringDampingMultiplier?: number;
|
||||
cameraSpringMassMultiplier?: number;
|
||||
zoomSmoothness?: number;
|
||||
zoomClassicMode?: boolean;
|
||||
cursorMotionBlur?: number;
|
||||
@@ -229,7 +233,6 @@ export class FrameRenderer {
|
||||
private motionBlurState: MotionBlurState;
|
||||
private layoutCache: LayoutCache | null = null;
|
||||
private currentVideoTime = 0;
|
||||
private lastMotionVector = { x: 0, y: 0 };
|
||||
private springScale: SpringState;
|
||||
private springX: SpringState;
|
||||
private springY: SpringState;
|
||||
@@ -341,7 +344,7 @@ export class FrameRenderer {
|
||||
await this.setupFrame();
|
||||
|
||||
if ((this.config.zoomMotionBlur ?? 0) > 0) {
|
||||
this.zoomBlurFilter = new ZoomBlurFilter({ strength: 0 });
|
||||
this.zoomBlurFilter = new ZoomBlurFilter({ strength: 0, maxKernelSize: 13 });
|
||||
this.motionBlurFilter = new MotionBlurFilter([0, 0], 5, 0);
|
||||
this.videoContainer.filterArea = new Rectangle(
|
||||
0,
|
||||
@@ -349,7 +352,7 @@ export class FrameRenderer {
|
||||
this.config.width,
|
||||
this.config.height,
|
||||
);
|
||||
this.videoContainer.filters = [this.zoomBlurFilter, this.motionBlurFilter];
|
||||
this.videoContainer.filters = [this.motionBlurFilter, this.zoomBlurFilter];
|
||||
} else {
|
||||
this.videoContainer.filters = null;
|
||||
}
|
||||
@@ -1275,7 +1278,10 @@ export class FrameRenderer {
|
||||
}
|
||||
|
||||
const temporalSnapshot =
|
||||
typeof frameDurationUs === "number" && frameDurationUs > 0
|
||||
TEMPORAL_ZOOM_MOTION_BLUR_ENABLED &&
|
||||
(this.config.zoomTemporalMotionBlur ?? 0) > 0 &&
|
||||
typeof frameDurationUs === "number" &&
|
||||
frameDurationUs > 0
|
||||
? await this.renderTemporalMotionBlurFrame(
|
||||
timestamp,
|
||||
cursorTimestamp,
|
||||
@@ -1442,13 +1448,10 @@ export class FrameRenderer {
|
||||
|
||||
const TICKS_PER_FRAME = 1;
|
||||
|
||||
let maxMotionIntensity = 0;
|
||||
for (let i = 0; i < TICKS_PER_FRAME; i++) {
|
||||
const motionIntensity = this.updateAnimationState(timeMs);
|
||||
maxMotionIntensity = Math.max(maxMotionIntensity, motionIntensity);
|
||||
this.updateAnimationState(timeMs);
|
||||
}
|
||||
|
||||
// Apply transform once with maximum motion intensity from all ticks
|
||||
applyZoomTransform({
|
||||
cameraContainer: this.cameraContainer,
|
||||
zoomBlurFilter: this.zoomBlurFilter,
|
||||
@@ -1459,10 +1462,9 @@ export class FrameRenderer {
|
||||
zoomProgress: this.animationState.progress,
|
||||
focusX: this.animationState.focusX,
|
||||
focusY: this.animationState.focusY,
|
||||
motionIntensity: maxMotionIntensity,
|
||||
motionVector: this.lastMotionVector,
|
||||
isPlaying: true,
|
||||
motionBlurAmount: this.config.zoomMotionBlur ?? 0,
|
||||
motionBlurTuning: this.config.zoomMotionBlurTuning,
|
||||
transformOverride: {
|
||||
scale: this.animationState.appliedScale,
|
||||
x: this.animationState.x,
|
||||
@@ -1834,7 +1836,11 @@ export class FrameRenderer {
|
||||
this.lastContentTimeMs !== null ? timeMs - this.lastContentTimeMs : 1000 / 60;
|
||||
this.lastContentTimeMs = timeMs;
|
||||
|
||||
const zoomSpringConfig = getZoomSpringConfig(this.config.zoomSmoothness);
|
||||
const zoomSpringConfig = getZoomSpringConfig(this.config.zoomSmoothness, {
|
||||
stiffnessMultiplier: this.config.cameraSpringStiffnessMultiplier,
|
||||
dampingMultiplier: this.config.cameraSpringDampingMultiplier,
|
||||
massMultiplier: this.config.cameraSpringMassMultiplier,
|
||||
});
|
||||
|
||||
if (this.config.zoomClassicMode) {
|
||||
state.appliedScale = projectedTransform.scale;
|
||||
@@ -1864,11 +1870,6 @@ export class FrameRenderer {
|
||||
);
|
||||
}
|
||||
|
||||
this.lastMotionVector = {
|
||||
x: state.x - prevX,
|
||||
y: state.y - prevY,
|
||||
};
|
||||
|
||||
return Math.max(
|
||||
Math.abs(state.appliedScale - prevScale),
|
||||
Math.abs(state.x - prevX) / Math.max(1, this.layoutCache.stageSize.width),
|
||||
@@ -1919,7 +1920,7 @@ export class FrameRenderer {
|
||||
},
|
||||
);
|
||||
|
||||
const motionIntensity = this.updateAnimationState(timeMs);
|
||||
this.updateAnimationState(timeMs);
|
||||
|
||||
applyZoomTransform({
|
||||
cameraContainer: this.cameraContainer,
|
||||
@@ -1931,10 +1932,9 @@ export class FrameRenderer {
|
||||
zoomProgress: this.animationState.progress,
|
||||
focusX: this.animationState.focusX,
|
||||
focusY: this.animationState.focusY,
|
||||
motionIntensity,
|
||||
motionVector: this.lastMotionVector,
|
||||
isPlaying: true,
|
||||
motionBlurAmount: useVelocityMotionBlur ? (this.config.zoomMotionBlur ?? 0) : 0,
|
||||
motionBlurTuning: this.config.zoomMotionBlurTuning,
|
||||
transformOverride: {
|
||||
scale: this.animationState.appliedScale,
|
||||
x: this.animationState.x,
|
||||
@@ -1972,21 +1972,14 @@ export class FrameRenderer {
|
||||
frameDurationUs: number,
|
||||
layoutCache: LayoutCache,
|
||||
): Promise<RenderSnapshot | null> {
|
||||
if (
|
||||
!this.compositeCanvas ||
|
||||
!this.compositeCtx ||
|
||||
!this.temporalAccumulationCtx
|
||||
) {
|
||||
if (!this.compositeCanvas || !this.compositeCtx || !this.temporalAccumulationCtx) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const blurConfig = getTemporalMotionBlurConfig(
|
||||
this.config.zoomTemporalMotionBlur ?? this.config.zoomMotionBlur,
|
||||
{
|
||||
sampleCount: this.config.zoomMotionBlurSampleCount,
|
||||
shutterFraction: this.config.zoomMotionBlurShutterFraction,
|
||||
},
|
||||
);
|
||||
const blurConfig = getTemporalMotionBlurConfig(this.config.zoomTemporalMotionBlur, {
|
||||
sampleCount: this.config.zoomMotionBlurSampleCount,
|
||||
shutterFraction: this.config.zoomMotionBlurShutterFraction,
|
||||
});
|
||||
if (!blurConfig) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
SpeedRegion,
|
||||
TrimRegion,
|
||||
WebcamOverlaySettings,
|
||||
ZoomMotionBlurTuning,
|
||||
ZoomRegion,
|
||||
ZoomTransitionEasing,
|
||||
} from "@/components/video-editor/types";
|
||||
@@ -42,6 +43,7 @@ interface GifExporterConfig {
|
||||
shadowIntensity: number;
|
||||
backgroundBlur: number;
|
||||
zoomMotionBlur?: number;
|
||||
zoomMotionBlurTuning?: ZoomMotionBlurTuning;
|
||||
zoomTemporalMotionBlur?: number;
|
||||
zoomMotionBlurSampleCount?: number | null;
|
||||
zoomMotionBlurShutterFraction?: number | null;
|
||||
@@ -71,6 +73,9 @@ interface GifExporterConfig {
|
||||
cursorSpringStiffnessMultiplier?: number;
|
||||
cursorSpringDampingMultiplier?: number;
|
||||
cursorSpringMassMultiplier?: number;
|
||||
cameraSpringStiffnessMultiplier?: number;
|
||||
cameraSpringDampingMultiplier?: number;
|
||||
cameraSpringMassMultiplier?: number;
|
||||
zoomSmoothness?: number;
|
||||
zoomClassicMode?: boolean;
|
||||
cursorMotionBlur?: number;
|
||||
@@ -164,6 +169,7 @@ export class GifExporter {
|
||||
shadowIntensity: this.config.shadowIntensity,
|
||||
backgroundBlur: this.config.backgroundBlur,
|
||||
zoomMotionBlur: this.config.zoomMotionBlur,
|
||||
zoomMotionBlurTuning: this.config.zoomMotionBlurTuning,
|
||||
zoomTemporalMotionBlur: this.config.zoomTemporalMotionBlur,
|
||||
zoomMotionBlurSampleCount: this.config.zoomMotionBlurSampleCount,
|
||||
zoomMotionBlurShutterFraction: this.config.zoomMotionBlurShutterFraction,
|
||||
@@ -197,6 +203,9 @@ export class GifExporter {
|
||||
cursorSpringStiffnessMultiplier: this.config.cursorSpringStiffnessMultiplier,
|
||||
cursorSpringDampingMultiplier: this.config.cursorSpringDampingMultiplier,
|
||||
cursorSpringMassMultiplier: this.config.cursorSpringMassMultiplier,
|
||||
cameraSpringStiffnessMultiplier: this.config.cameraSpringStiffnessMultiplier,
|
||||
cameraSpringDampingMultiplier: this.config.cameraSpringDampingMultiplier,
|
||||
cameraSpringMassMultiplier: this.config.cameraSpringMassMultiplier,
|
||||
zoomSmoothness: this.config.zoomSmoothness,
|
||||
zoomClassicMode: this.config.zoomClassicMode,
|
||||
cursorMotionBlur: this.config.cursorMotionBlur,
|
||||
|
||||
@@ -21,6 +21,7 @@ import type {
|
||||
Padding,
|
||||
SpeedRegion,
|
||||
WebcamOverlaySettings,
|
||||
ZoomMotionBlurTuning,
|
||||
ZoomRegion,
|
||||
ZoomTransitionEasing,
|
||||
} from "@/components/video-editor/types";
|
||||
@@ -92,6 +93,9 @@ import {
|
||||
WEBCAM_SHADOW_LAYER_PROFILES,
|
||||
} from "./shadowProfile";
|
||||
import { buildTemporalSamplePlanUs, getTemporalMotionBlurConfig } from "./temporalMotionBlur";
|
||||
|
||||
const TEMPORAL_ZOOM_MOTION_BLUR_ENABLED = false;
|
||||
|
||||
import type { ExportRenderBackend } from "./types";
|
||||
|
||||
interface FrameRenderConfig {
|
||||
@@ -104,6 +108,7 @@ interface FrameRenderConfig {
|
||||
shadowIntensity: number;
|
||||
backgroundBlur: number;
|
||||
zoomMotionBlur?: number;
|
||||
zoomMotionBlurTuning?: ZoomMotionBlurTuning;
|
||||
zoomTemporalMotionBlur?: number;
|
||||
zoomMotionBlurSampleCount?: number | null;
|
||||
zoomMotionBlurShutterFraction?: number | null;
|
||||
@@ -137,6 +142,9 @@ interface FrameRenderConfig {
|
||||
cursorSpringStiffnessMultiplier?: number;
|
||||
cursorSpringDampingMultiplier?: number;
|
||||
cursorSpringMassMultiplier?: number;
|
||||
cameraSpringStiffnessMultiplier?: number;
|
||||
cameraSpringDampingMultiplier?: number;
|
||||
cameraSpringMassMultiplier?: number;
|
||||
cursorMotionBlur?: number;
|
||||
cursorClickBounce?: number;
|
||||
cursorClickBounceDuration?: number;
|
||||
@@ -394,7 +402,6 @@ export class FrameRenderer {
|
||||
private lastContentTimeMs: number | null = null;
|
||||
private layoutCache: LayoutCache | null = null;
|
||||
private currentVideoTime = 0;
|
||||
private lastMotionVector = { x: 0, y: 0 };
|
||||
private cursorOverlay: PixiCursorOverlay | null = null;
|
||||
private lastSyncedWebcamTime: number | null = null;
|
||||
private lastWebcamCacheRefreshTime: number | null = null;
|
||||
@@ -427,8 +434,8 @@ export class FrameRenderer {
|
||||
}
|
||||
|
||||
const activeFilters =
|
||||
this.shouldUseZoomMotionBlur() && this.zoomBlurFilter && this.motionBlurFilter
|
||||
? [this.zoomBlurFilter, this.motionBlurFilter]
|
||||
this.shouldUseZoomMotionBlur() && this.motionBlurFilter && this.zoomBlurFilter
|
||||
? [this.motionBlurFilter, this.zoomBlurFilter]
|
||||
: null;
|
||||
this.videoEffectsContainer.filters = activeFilters;
|
||||
}
|
||||
@@ -541,7 +548,7 @@ export class FrameRenderer {
|
||||
this.setupCaptionResources();
|
||||
|
||||
if (this.shouldUseZoomMotionBlur()) {
|
||||
this.zoomBlurFilter = new ZoomBlurFilter({ strength: 0 });
|
||||
this.zoomBlurFilter = new ZoomBlurFilter({ strength: 0, maxKernelSize: 13 });
|
||||
this.motionBlurFilter = new MotionBlurFilter([0, 0], 5, 0);
|
||||
}
|
||||
|
||||
@@ -2513,7 +2520,7 @@ export class FrameRenderer {
|
||||
);
|
||||
}
|
||||
|
||||
const motionIntensity = this.updateAnimationState(timeMs);
|
||||
this.updateAnimationState(timeMs);
|
||||
|
||||
applyZoomTransform({
|
||||
cameraContainer: this.cameraContainer,
|
||||
@@ -2525,10 +2532,9 @@ export class FrameRenderer {
|
||||
zoomProgress: this.animationState.progress,
|
||||
focusX: this.animationState.focusX,
|
||||
focusY: this.animationState.focusY,
|
||||
motionIntensity,
|
||||
motionVector: this.lastMotionVector,
|
||||
isPlaying: true,
|
||||
motionBlurAmount: useVelocityMotionBlur ? (this.config.zoomMotionBlur ?? 0) : 0,
|
||||
motionBlurTuning: this.config.zoomMotionBlurTuning,
|
||||
transformOverride: {
|
||||
scale: this.animationState.appliedScale,
|
||||
x: this.animationState.x,
|
||||
@@ -2595,13 +2601,10 @@ export class FrameRenderer {
|
||||
return null;
|
||||
}
|
||||
|
||||
const blurConfig = getTemporalMotionBlurConfig(
|
||||
this.config.zoomTemporalMotionBlur ?? this.config.zoomMotionBlur,
|
||||
{
|
||||
sampleCount: this.config.zoomMotionBlurSampleCount,
|
||||
shutterFraction: this.config.zoomMotionBlurShutterFraction,
|
||||
},
|
||||
);
|
||||
const blurConfig = getTemporalMotionBlurConfig(this.config.zoomTemporalMotionBlur, {
|
||||
sampleCount: this.config.zoomMotionBlurSampleCount,
|
||||
shutterFraction: this.config.zoomMotionBlurShutterFraction,
|
||||
});
|
||||
if (!blurConfig) {
|
||||
return null;
|
||||
}
|
||||
@@ -2716,7 +2719,10 @@ export class FrameRenderer {
|
||||
}
|
||||
|
||||
const temporalSnapshot =
|
||||
typeof frameDurationUs === "number" && frameDurationUs > 0
|
||||
TEMPORAL_ZOOM_MOTION_BLUR_ENABLED &&
|
||||
(this.config.zoomTemporalMotionBlur ?? 0) > 0 &&
|
||||
typeof frameDurationUs === "number" &&
|
||||
frameDurationUs > 0
|
||||
? await this.renderTemporalMotionBlurFrame(
|
||||
timestamp,
|
||||
cursorTimestamp,
|
||||
@@ -2761,9 +2767,7 @@ export class FrameRenderer {
|
||||
);
|
||||
}
|
||||
|
||||
let maxMotionIntensity = 0;
|
||||
const motionIntensity = this.updateAnimationState(timeMs);
|
||||
maxMotionIntensity = Math.max(maxMotionIntensity, motionIntensity);
|
||||
this.updateAnimationState(timeMs);
|
||||
|
||||
applyZoomTransform({
|
||||
cameraContainer: this.cameraContainer,
|
||||
@@ -2775,10 +2779,9 @@ export class FrameRenderer {
|
||||
zoomProgress: this.animationState.progress,
|
||||
focusX: this.animationState.focusX,
|
||||
focusY: this.animationState.focusY,
|
||||
motionIntensity: maxMotionIntensity,
|
||||
motionVector: this.lastMotionVector,
|
||||
isPlaying: true,
|
||||
motionBlurAmount: this.config.zoomMotionBlur ?? 0,
|
||||
motionBlurTuning: this.config.zoomMotionBlurTuning,
|
||||
transformOverride: {
|
||||
scale: this.animationState.appliedScale,
|
||||
x: this.animationState.x,
|
||||
@@ -3244,7 +3247,11 @@ export class FrameRenderer {
|
||||
this.lastContentTimeMs !== null ? timeMs - this.lastContentTimeMs : 1000 / 60;
|
||||
this.lastContentTimeMs = timeMs;
|
||||
|
||||
const zoomSpringConfig = getZoomSpringConfig(this.config.zoomSmoothness);
|
||||
const zoomSpringConfig = getZoomSpringConfig(this.config.zoomSmoothness, {
|
||||
stiffnessMultiplier: this.config.cameraSpringStiffnessMultiplier,
|
||||
dampingMultiplier: this.config.cameraSpringDampingMultiplier,
|
||||
massMultiplier: this.config.cameraSpringMassMultiplier,
|
||||
});
|
||||
|
||||
if (this.config.zoomClassicMode) {
|
||||
state.appliedScale = projectedTransform.scale;
|
||||
@@ -3274,11 +3281,6 @@ export class FrameRenderer {
|
||||
);
|
||||
}
|
||||
|
||||
this.lastMotionVector = {
|
||||
x: state.x - previousX,
|
||||
y: state.y - previousY,
|
||||
};
|
||||
|
||||
return Math.max(
|
||||
Math.abs(state.appliedScale - previousScale),
|
||||
Math.abs(state.x - previousX) / Math.max(1, this.layoutCache.stageSize.width),
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
SpeedRegion,
|
||||
TrimRegion,
|
||||
WebcamOverlaySettings,
|
||||
ZoomMotionBlurTuning,
|
||||
ZoomRegion,
|
||||
ZoomTransitionEasing,
|
||||
} from "@/components/video-editor/types";
|
||||
@@ -59,6 +60,7 @@ interface VideoExporterConfig extends ExportConfig {
|
||||
shadowIntensity: number;
|
||||
backgroundBlur: number;
|
||||
zoomMotionBlur?: number;
|
||||
zoomMotionBlurTuning?: ZoomMotionBlurTuning;
|
||||
zoomTemporalMotionBlur?: number;
|
||||
zoomMotionBlurSampleCount?: number | null;
|
||||
zoomMotionBlurShutterFraction?: number | null;
|
||||
@@ -88,6 +90,9 @@ interface VideoExporterConfig extends ExportConfig {
|
||||
cursorSpringStiffnessMultiplier?: number;
|
||||
cursorSpringDampingMultiplier?: number;
|
||||
cursorSpringMassMultiplier?: number;
|
||||
cameraSpringStiffnessMultiplier?: number;
|
||||
cameraSpringDampingMultiplier?: number;
|
||||
cameraSpringMassMultiplier?: number;
|
||||
cursorMotionBlur?: number;
|
||||
cursorClickBounce?: number;
|
||||
cursorClickBounceDuration?: number;
|
||||
@@ -309,6 +314,7 @@ export class ModernVideoExporter {
|
||||
shadowIntensity: this.config.shadowIntensity,
|
||||
backgroundBlur: this.config.backgroundBlur,
|
||||
zoomMotionBlur: this.config.zoomMotionBlur,
|
||||
zoomMotionBlurTuning: this.config.zoomMotionBlurTuning,
|
||||
zoomTemporalMotionBlur: this.config.zoomTemporalMotionBlur,
|
||||
zoomMotionBlurSampleCount: this.config.zoomMotionBlurSampleCount,
|
||||
zoomMotionBlurShutterFraction: this.config.zoomMotionBlurShutterFraction,
|
||||
@@ -342,6 +348,9 @@ export class ModernVideoExporter {
|
||||
cursorSpringStiffnessMultiplier: this.config.cursorSpringStiffnessMultiplier,
|
||||
cursorSpringDampingMultiplier: this.config.cursorSpringDampingMultiplier,
|
||||
cursorSpringMassMultiplier: this.config.cursorSpringMassMultiplier,
|
||||
cameraSpringStiffnessMultiplier: this.config.cameraSpringStiffnessMultiplier,
|
||||
cameraSpringDampingMultiplier: this.config.cameraSpringDampingMultiplier,
|
||||
cameraSpringMassMultiplier: this.config.cameraSpringMassMultiplier,
|
||||
cursorMotionBlur: this.config.cursorMotionBlur,
|
||||
cursorClickBounce: this.config.cursorClickBounce,
|
||||
cursorClickBounceDuration: this.config.cursorClickBounceDuration,
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
SpeedRegion,
|
||||
TrimRegion,
|
||||
WebcamOverlaySettings,
|
||||
ZoomMotionBlurTuning,
|
||||
ZoomRegion,
|
||||
ZoomTransitionEasing,
|
||||
} from "@/components/video-editor/types";
|
||||
@@ -48,6 +49,7 @@ interface VideoExporterConfig extends ExportConfig {
|
||||
shadowIntensity: number;
|
||||
backgroundBlur: number;
|
||||
zoomMotionBlur?: number;
|
||||
zoomMotionBlurTuning?: ZoomMotionBlurTuning;
|
||||
zoomTemporalMotionBlur?: number;
|
||||
zoomMotionBlurSampleCount?: number | null;
|
||||
zoomMotionBlurShutterFraction?: number | null;
|
||||
@@ -77,6 +79,9 @@ interface VideoExporterConfig extends ExportConfig {
|
||||
cursorSpringStiffnessMultiplier?: number;
|
||||
cursorSpringDampingMultiplier?: number;
|
||||
cursorSpringMassMultiplier?: number;
|
||||
cameraSpringStiffnessMultiplier?: number;
|
||||
cameraSpringDampingMultiplier?: number;
|
||||
cameraSpringMassMultiplier?: number;
|
||||
cursorMotionBlur?: number;
|
||||
cursorClickBounce?: number;
|
||||
cursorClickBounceDuration?: number;
|
||||
@@ -198,6 +203,7 @@ export class VideoExporter {
|
||||
shadowIntensity: this.config.shadowIntensity,
|
||||
backgroundBlur: this.config.backgroundBlur,
|
||||
zoomMotionBlur: this.config.zoomMotionBlur,
|
||||
zoomMotionBlurTuning: this.config.zoomMotionBlurTuning,
|
||||
zoomTemporalMotionBlur: this.config.zoomTemporalMotionBlur,
|
||||
zoomMotionBlurSampleCount: this.config.zoomMotionBlurSampleCount,
|
||||
zoomMotionBlurShutterFraction: this.config.zoomMotionBlurShutterFraction,
|
||||
@@ -231,6 +237,9 @@ export class VideoExporter {
|
||||
cursorSpringStiffnessMultiplier: this.config.cursorSpringStiffnessMultiplier,
|
||||
cursorSpringDampingMultiplier: this.config.cursorSpringDampingMultiplier,
|
||||
cursorSpringMassMultiplier: this.config.cursorSpringMassMultiplier,
|
||||
cameraSpringStiffnessMultiplier: this.config.cameraSpringStiffnessMultiplier,
|
||||
cameraSpringDampingMultiplier: this.config.cameraSpringDampingMultiplier,
|
||||
cameraSpringMassMultiplier: this.config.cameraSpringMassMultiplier,
|
||||
cursorMotionBlur: this.config.cursorMotionBlur,
|
||||
cursorClickBounce: this.config.cursorClickBounce,
|
||||
cursorClickBounceDuration: this.config.cursorClickBounceDuration,
|
||||
|
||||
Reference in new issue
Block a user