fix presets and motion blur, add dev sliders

This commit is contained in:
webadderall
2026-05-05 17:29:17 +10:00
parent f287dd3132
commit 7f00d95681
5 changed files with 241 additions and 30 deletions
+143 -9
View File
@@ -29,6 +29,10 @@ 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";
@@ -85,7 +89,6 @@ import {
DEFAULT_WEBCAM_SHADOW,
DEFAULT_WEBCAM_SIZE,
DEFAULT_ZOOM_IN_DURATION_MS,
DEFAULT_ZOOM_SMOOTHNESS,
DEFAULT_ZOOM_OUT_DURATION_MS,
} from "./types";
import { fromCursorSwaySliderValue, toCursorSwaySliderValue } from "./videoPlayback/cursorSway";
@@ -470,6 +473,7 @@ interface SettingsPanelProps {
onShadowChange?: (intensity: number) => void;
backgroundBlur?: number;
onBackgroundBlurChange?: (amount: number) => void;
zoomTemporalMotionBlur?: number;
onZoomTemporalMotionBlurChange?: (amount: number) => void;
zoomMotionBlurSampleCount?: number | null;
onZoomMotionBlurSampleCountChange?: (count: number | null) => void;
@@ -849,8 +853,11 @@ 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,
connectZooms = true,
onConnectZoomsChange,
@@ -1124,6 +1131,7 @@ export function SettingsPanel({
() => ({ ...builtInCursorPreviewUrls, ...extensionCursorPreviewUrls }),
[builtInCursorPreviewUrls, extensionCursorPreviewUrls],
);
const showDevMotionControls = import.meta.env.DEV;
const cursorStyleOptions = useMemo<CursorStyleOption[]>(
() => [
...BUILTIN_CURSOR_STYLE_OPTIONS,
@@ -1448,9 +1456,9 @@ export function SettingsPanel({
const resetZoomSection = () => {
onZoomTemporalMotionBlurChange?.(initialEditorPreferences.zoomTemporalMotionBlur);
onZoomMotionBlurSampleCountChange?.(TEMPORAL_MOTION_BLUR_DEFAULT_SAMPLE_COUNT);
onZoomMotionBlurSampleCountChange?.(initialEditorPreferences.zoomMotionBlurSampleCount);
onZoomMotionBlurShutterFractionChange?.(
TEMPORAL_MOTION_BLUR_DEFAULT_SHUTTER_FRACTION,
initialEditorPreferences.zoomMotionBlurShutterFraction,
);
onZoomInDurationMsChange?.(initialEditorPreferences.zoomInDurationMs);
onZoomOutDurationMsChange?.(initialEditorPreferences.zoomOutDurationMs);
@@ -2690,14 +2698,79 @@ export function SettingsPanel({
)}
</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 ? (
<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="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 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.",
)}
</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={() => {
@@ -2941,6 +3014,67 @@ export function SettingsPanel({
return parseFloat(text.replace(/×$/, ""));
}}
/>
{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>
<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>
{renderExtensionPanelsForSections("cursor")}
</section>
+59 -6
View File
@@ -597,6 +597,15 @@ export default function VideoEditor() {
);
const [backgroundBlur, setBackgroundBlur] = useState(initialEditorPreferences.backgroundBlur);
const [zoomMotionBlur, setZoomMotionBlur] = useState(initialEditorPreferences.zoomMotionBlur);
const [zoomTemporalMotionBlur, setZoomTemporalMotionBlur] = useState(
initialEditorPreferences.zoomTemporalMotionBlur,
);
const [zoomMotionBlurSampleCount, setZoomMotionBlurSampleCount] = useState<number | null>(
initialEditorPreferences.zoomMotionBlurSampleCount,
);
const [zoomMotionBlurShutterFraction, setZoomMotionBlurShutterFraction] = useState<
number | null
>(initialEditorPreferences.zoomMotionBlurShutterFraction);
const [autoApplyFreshRecordingAutoZooms, setAutoApplyFreshRecordingAutoZooms] = useState(
initialEditorPreferences.autoApplyFreshRecordingAutoZooms,
);
@@ -829,10 +838,9 @@ export default function VideoEditor() {
shadowIntensity,
backgroundBlur,
zoomMotionBlur,
zoomTemporalMotionBlur: zoomMotionBlur,
zoomMotionBlurSampleCount: initialEditorPreferences.zoomMotionBlurSampleCount,
zoomMotionBlurShutterFraction:
initialEditorPreferences.zoomMotionBlurShutterFraction,
zoomTemporalMotionBlur,
zoomMotionBlurSampleCount,
zoomMotionBlurShutterFraction,
connectZooms,
zoomInDurationMs,
zoomInOverlapMs,
@@ -877,8 +885,9 @@ export default function VideoEditor() {
shadowIntensity,
backgroundBlur,
zoomMotionBlur,
initialEditorPreferences.zoomMotionBlurSampleCount,
initialEditorPreferences.zoomMotionBlurShutterFraction,
zoomTemporalMotionBlur,
zoomMotionBlurSampleCount,
zoomMotionBlurShutterFraction,
connectZooms,
zoomInDurationMs,
zoomInOverlapMs,
@@ -964,6 +973,9 @@ export default function VideoEditor() {
setShadowIntensity(snapshot.shadowIntensity);
setBackgroundBlur(snapshot.backgroundBlur);
setZoomMotionBlur(snapshot.zoomMotionBlur);
setZoomTemporalMotionBlur(snapshot.zoomTemporalMotionBlur);
setZoomMotionBlurSampleCount(snapshot.zoomMotionBlurSampleCount);
setZoomMotionBlurShutterFraction(snapshot.zoomMotionBlurShutterFraction);
setConnectZooms(snapshot.connectZooms);
setZoomInDurationMs(snapshot.zoomInDurationMs);
setZoomInOverlapMs(snapshot.zoomInOverlapMs);
@@ -1192,6 +1204,9 @@ export default function VideoEditor() {
shadowIntensity,
backgroundBlur,
zoomMotionBlur,
zoomTemporalMotionBlur,
zoomMotionBlurSampleCount,
zoomMotionBlurShutterFraction,
connectZooms,
zoomInDurationMs,
zoomInOverlapMs,
@@ -1338,6 +1353,9 @@ export default function VideoEditor() {
zoomInEasing,
zoomInOverlapMs,
zoomMotionBlur,
zoomTemporalMotionBlur,
zoomMotionBlurSampleCount,
zoomMotionBlurShutterFraction,
zoomOutDurationMs,
zoomOutEasing,
zoomRegions,
@@ -1640,6 +1658,9 @@ export default function VideoEditor() {
shadowIntensity: number;
backgroundBlur: number;
zoomMotionBlur: number;
zoomTemporalMotionBlur: number;
zoomMotionBlurSampleCount: number | null;
zoomMotionBlurShutterFraction: number | null;
connectZooms: boolean;
zoomInDurationMs: number;
zoomInOverlapMs: number;
@@ -1791,6 +1812,9 @@ export default function VideoEditor() {
shadowIntensity,
backgroundBlur,
zoomMotionBlur,
zoomTemporalMotionBlur,
zoomMotionBlurSampleCount,
zoomMotionBlurShutterFraction,
connectZooms,
zoomInDurationMs,
zoomInOverlapMs,
@@ -1844,6 +1868,9 @@ export default function VideoEditor() {
shadowIntensity,
backgroundBlur,
zoomMotionBlur,
zoomTemporalMotionBlur,
zoomMotionBlurSampleCount,
zoomMotionBlurShutterFraction,
connectZooms,
zoomInDurationMs,
zoomInOverlapMs,
@@ -2025,6 +2052,9 @@ export default function VideoEditor() {
setShadowIntensity(normalizedEditor.shadowIntensity);
setBackgroundBlur(normalizedEditor.backgroundBlur);
setZoomMotionBlur(normalizedEditor.zoomMotionBlur);
setZoomTemporalMotionBlur(normalizedEditor.zoomTemporalMotionBlur);
setZoomMotionBlurSampleCount(normalizedEditor.zoomMotionBlurSampleCount);
setZoomMotionBlurShutterFraction(normalizedEditor.zoomMotionBlurShutterFraction);
setConnectZooms(normalizedEditor.connectZooms);
setZoomInDurationMs(normalizedEditor.zoomInDurationMs);
setZoomInOverlapMs(normalizedEditor.zoomInOverlapMs);
@@ -2431,6 +2461,9 @@ export default function VideoEditor() {
shadowIntensity,
backgroundBlur,
zoomMotionBlur,
zoomTemporalMotionBlur,
zoomMotionBlurSampleCount,
zoomMotionBlurShutterFraction,
autoApplyFreshRecordingAutoZooms,
connectZooms,
zoomInDurationMs,
@@ -2475,6 +2508,9 @@ export default function VideoEditor() {
shadowIntensity,
backgroundBlur,
zoomMotionBlur,
zoomTemporalMotionBlur,
zoomMotionBlurSampleCount,
zoomMotionBlurShutterFraction,
autoApplyFreshRecordingAutoZooms,
connectZooms,
zoomInDurationMs,
@@ -4313,6 +4349,9 @@ export default function VideoEditor() {
shadowIntensity: effectiveShadowIntensity,
backgroundBlur,
zoomMotionBlur,
zoomTemporalMotionBlur,
zoomMotionBlurSampleCount,
zoomMotionBlurShutterFraction,
connectZooms,
zoomInDurationMs,
zoomInOverlapMs,
@@ -4489,6 +4528,9 @@ export default function VideoEditor() {
shadowIntensity: effectiveShadowIntensity,
backgroundBlur,
zoomMotionBlur,
zoomTemporalMotionBlur,
zoomMotionBlurSampleCount,
zoomMotionBlurShutterFraction,
connectZooms,
zoomInDurationMs,
zoomInOverlapMs,
@@ -4740,6 +4782,9 @@ export default function VideoEditor() {
shadowIntensity,
backgroundBlur,
zoomMotionBlur,
zoomTemporalMotionBlur,
zoomMotionBlurSampleCount,
zoomMotionBlurShutterFraction,
connectZooms,
zoomInDurationMs,
zoomInOverlapMs,
@@ -5721,6 +5766,14 @@ export default function VideoEditor() {
onShadowChange={setShadowIntensity}
backgroundBlur={backgroundBlur}
onBackgroundBlurChange={setBackgroundBlur}
zoomTemporalMotionBlur={zoomTemporalMotionBlur}
onZoomTemporalMotionBlurChange={setZoomTemporalMotionBlur}
zoomMotionBlurSampleCount={zoomMotionBlurSampleCount}
onZoomMotionBlurSampleCountChange={setZoomMotionBlurSampleCount}
zoomMotionBlurShutterFraction={zoomMotionBlurShutterFraction}
onZoomMotionBlurShutterFractionChange={
setZoomMotionBlurShutterFraction
}
autoApplyFreshRecordingAutoZooms={autoApplyFreshRecordingAutoZooms}
onAutoApplyFreshRecordingAutoZoomsChange={
setAutoApplyFreshRecordingAutoZooms
@@ -32,8 +32,8 @@ export const CURSOR_MOTION_PRESETS: Record<CursorMotionPresetId, CursorMotionPre
id: "focused",
label: "Focused",
zoomSmoothness: 0.5,
zoomInDurationMs: 100,
zoomOutDurationMs: 100,
zoomInDurationMs: 200,
zoomOutDurationMs: 200,
...SHARED_CURSOR_PRESET_VALUES,
cursorSpringStiffnessMultiplier: 1.35,
cursorSpringDampingMultiplier: 0.79,
@@ -12,6 +12,10 @@ import { isValidMp4FrameRate } from "@/lib/exporter";
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 { DEFAULT_WALLPAPER_PATH } from "@/lib/wallpapers";
import { ASPECT_RATIOS, type AspectRatio, isCustomAspectRatio } from "@/utils/aspectRatioUtils";
@@ -300,6 +304,27 @@ export function validateProjectData(candidate: unknown): candidate is EditorProj
}
export function normalizeProjectEditor(editor: Partial<ProjectEditorState>): ProjectEditorState {
const normalizeTemporalBlurSampleCount = (value: unknown): number => {
if (!isFiniteNumber(value)) {
return TEMPORAL_MOTION_BLUR_DEFAULT_SAMPLE_COUNT;
}
const roundedValue = Math.round(value);
const clampedValue = clamp(
roundedValue,
TEMPORAL_MOTION_BLUR_MIN_SAMPLE_COUNT,
TEMPORAL_MOTION_BLUR_MAX_SAMPLE_COUNT,
);
if (clampedValue % 2 === 1) {
return clampedValue;
}
return clampedValue >= TEMPORAL_MOTION_BLUR_MAX_SAMPLE_COUNT
? clampedValue - 1
: clampedValue + 1;
};
const validAspectRatios = new Set<AspectRatio>(ASPECT_RATIOS);
const legacyMotionBlurEnabled = (editor as Partial<{ motionBlurEnabled: boolean }>)
.motionBlurEnabled;
@@ -323,9 +348,18 @@ export function normalizeProjectEditor(editor: Partial<ProjectEditorState>): Pro
: legacyShowBlur
? 2
: 0;
const normalizedZoomMotionBlurSampleCount = TEMPORAL_MOTION_BLUR_DEFAULT_SAMPLE_COUNT;
const normalizedZoomMotionBlurShutterFraction =
TEMPORAL_MOTION_BLUR_DEFAULT_SHUTTER_FRACTION;
const normalizedZoomMotionBlurSampleCount = normalizeTemporalBlurSampleCount(
(editor as Partial<ProjectEditorState>).zoomMotionBlurSampleCount,
);
const normalizedZoomMotionBlurShutterFraction = isFiniteNumber(
(editor as Partial<ProjectEditorState>).zoomMotionBlurShutterFraction,
)
? clamp(
(editor as Partial<ProjectEditorState>).zoomMotionBlurShutterFraction as number,
TEMPORAL_MOTION_BLUR_MIN_SHUTTER_FRACTION,
TEMPORAL_MOTION_BLUR_MAX_SHUTTER_FRACTION,
)
: TEMPORAL_MOTION_BLUR_DEFAULT_SHUTTER_FRACTION;
const normalizedZoomInDurationMs = isFiniteNumber(editor.zoomInDurationMs)
? clamp(editor.zoomInDurationMs, 60, 4000)
: DEFAULT_MOTION_PRESET.zoomInDurationMs;
-10
View File
@@ -2638,16 +2638,6 @@ export class FrameRenderer {
return null;
}
if (resolvedSnapshot !== lastSnapshot) {
await this.renderSceneSample(
Math.round(resolvedSnapshot.timeMs * 1000),
Math.round(resolvedSnapshot.cursorTimeMs * 1000),
Math.round(resolvedSnapshot.backgroundTimelineTimeMs * 1000),
layoutCache,
true,
false,
);
}
this.updateCaptionLayer(resolvedSnapshot.timeMs);
const hasOverlayCanvasWork =