feat: replace motion blur toggle with intensity slider

Motion blur was a boolean switch (on/off). This changes it to a slider
from 0 (off) to 1 (full intensity), with 0.35 as the recommended sweet
spot per feedback on PR #207.

- EditorState/ProjectEditorState: motionBlurEnabled:bool → motionBlurAmount:number
- SettingsPanel: Switch → Slider (0–1, step 0.01); shows 'off' or value
- VideoPlayback/zoomTransform: scale blur by amount instead of boolean gate
- FrameRenderer/VideoExporter/GifExporter: propagate numeric amount
- projectPersistence: backward-compat loader (old true → 0.35, false → 0)
This commit is contained in:
Etienne Lescot
2026-03-16 11:17:09 +01:00
parent 9d71f509b8
commit dd84edaf41
8 changed files with 55 additions and 42 deletions
+20 -11
View File
@@ -92,8 +92,8 @@ interface SettingsPanelProps {
onShadowCommit?: () => void;
showBlur?: boolean;
onBlurChange?: (showBlur: boolean) => void;
motionBlurEnabled?: boolean;
onMotionBlurChange?: (enabled: boolean) => void;
motionBlurAmount?: number;
onMotionBlurChange?: (amount: number) => void;
borderRadius?: number;
onBorderRadiusChange?: (radius: number) => void;
onBorderRadiusCommit?: () => void;
@@ -157,7 +157,7 @@ export function SettingsPanel({
onShadowCommit,
showBlur,
onBlurChange,
motionBlurEnabled = false,
motionBlurAmount = 0,
onMotionBlurChange,
borderRadius = 0,
onBorderRadiusChange,
@@ -574,14 +574,6 @@ export function SettingsPanel({
</AccordionTrigger>
<AccordionContent className="pb-3">
<div className="grid grid-cols-2 gap-2 mb-3">
<div className="flex items-center justify-between p-2 rounded-lg bg-white/5 border border-white/5">
<div className="text-[10px] font-medium text-slate-300">Motion Blur</div>
<Switch
checked={motionBlurEnabled}
onCheckedChange={onMotionBlurChange}
className="data-[state=checked]:bg-[#34B27B] scale-90"
/>
</div>
<div className="flex items-center justify-between p-2 rounded-lg bg-white/5 border border-white/5">
<div className="text-[10px] font-medium text-slate-300">Blur BG</div>
<Switch
@@ -593,6 +585,23 @@ export function SettingsPanel({
</div>
<div className="grid grid-cols-2 gap-2">
<div className="p-2 rounded-lg bg-white/5 border border-white/5">
<div className="flex items-center justify-between mb-1">
<div className="text-[10px] font-medium text-slate-300">Motion Blur</div>
<span className="text-[10px] text-slate-500 font-mono">
{motionBlurAmount === 0 ? "off" : motionBlurAmount.toFixed(2)}
</span>
</div>
<Slider
value={[motionBlurAmount]}
onValueChange={(values) => onMotionBlurChange?.(values[0])}
onValueCommit={(values) => onMotionBlurChange?.(values[0])}
min={0}
max={1}
step={0.01}
className="w-full [&_[role=slider]]:bg-[#34B27B] [&_[role=slider]]:border-[#34B27B] [&_[role=slider]]:h-3 [&_[role=slider]]:w-3"
/>
</div>
<div className="p-2 rounded-lg bg-white/5 border border-white/5">
<div className="flex items-center justify-between mb-1">
<div className="text-[10px] font-medium text-slate-300">Shadow</div>
+12 -12
View File
@@ -70,7 +70,7 @@ export default function VideoEditor() {
wallpaper,
shadowIntensity,
showBlur,
motionBlurEnabled,
motionBlurAmount,
borderRadius,
padding,
aspectRatio,
@@ -139,7 +139,7 @@ export default function VideoEditor() {
wallpaper: normalizedEditor.wallpaper,
shadowIntensity: normalizedEditor.shadowIntensity,
showBlur: normalizedEditor.showBlur,
motionBlurEnabled: normalizedEditor.motionBlurEnabled,
motionBlurAmount: normalizedEditor.motionBlurAmount,
borderRadius: normalizedEditor.borderRadius,
padding: normalizedEditor.padding,
cropRegion: normalizedEditor.cropRegion,
@@ -198,7 +198,7 @@ export default function VideoEditor() {
wallpaper,
shadowIntensity,
showBlur,
motionBlurEnabled,
motionBlurAmount,
borderRadius,
padding,
cropRegion,
@@ -220,7 +220,7 @@ export default function VideoEditor() {
wallpaper,
shadowIntensity,
showBlur,
motionBlurEnabled,
motionBlurAmount,
borderRadius,
padding,
cropRegion,
@@ -294,7 +294,7 @@ export default function VideoEditor() {
wallpaper,
shadowIntensity,
showBlur,
motionBlurEnabled,
motionBlurAmount,
borderRadius,
padding,
cropRegion,
@@ -347,7 +347,7 @@ export default function VideoEditor() {
wallpaper,
shadowIntensity,
showBlur,
motionBlurEnabled,
motionBlurAmount,
borderRadius,
padding,
cropRegion,
@@ -933,7 +933,7 @@ export default function VideoEditor() {
showShadow: shadowIntensity > 0,
shadowIntensity,
showBlur,
motionBlurEnabled,
motionBlurAmount,
borderRadius,
padding,
videoPadding: padding,
@@ -1060,7 +1060,7 @@ export default function VideoEditor() {
showShadow: shadowIntensity > 0,
shadowIntensity,
showBlur,
motionBlurEnabled,
motionBlurAmount,
borderRadius,
padding,
cropRegion,
@@ -1121,7 +1121,7 @@ export default function VideoEditor() {
speedRegions,
shadowIntensity,
showBlur,
motionBlurEnabled,
motionBlurAmount,
borderRadius,
padding,
cropRegion,
@@ -1270,7 +1270,7 @@ export default function VideoEditor() {
showShadow={shadowIntensity > 0}
shadowIntensity={shadowIntensity}
showBlur={showBlur}
motionBlurEnabled={motionBlurEnabled}
motionBlurAmount={motionBlurAmount}
borderRadius={borderRadius}
padding={padding}
cropRegion={cropRegion}
@@ -1369,8 +1369,8 @@ export default function VideoEditor() {
onShadowCommit={commitState}
showBlur={showBlur}
onBlurChange={(v) => pushState({ showBlur: v })}
motionBlurEnabled={motionBlurEnabled}
onMotionBlurChange={(v) => pushState({ motionBlurEnabled: v })}
motionBlurAmount={motionBlurAmount}
onMotionBlurChange={(v) => pushState({ motionBlurAmount: v })}
borderRadius={borderRadius}
onBorderRadiusChange={(v) => updateState({ borderRadius: v })}
onBorderRadiusCommit={commitState}
@@ -70,7 +70,7 @@ interface VideoPlaybackProps {
showShadow?: boolean;
shadowIntensity?: number;
showBlur?: boolean;
motionBlurEnabled?: boolean;
motionBlurAmount?: number;
borderRadius?: number;
padding?: number;
cropRegion?: import("./types").CropRegion;
@@ -113,7 +113,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
showShadow,
shadowIntensity = 0,
showBlur,
motionBlurEnabled = false,
motionBlurAmount = 0,
borderRadius = 0,
padding = 50,
cropRegion,
@@ -128,7 +128,6 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
},
ref,
) => {
const ZOOM_MOTION_BLUR_AMOUNT = 0.35;
const videoRef = useRef<HTMLVideoElement | null>(null);
const containerRef = useRef<HTMLDivElement | null>(null);
const appRef = useRef<Application | null>(null);
@@ -169,7 +168,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
const layoutVideoContentRef = useRef<(() => void) | null>(null);
const trimRegionsRef = useRef<TrimRegion[]>([]);
const speedRegionsRef = useRef<SpeedRegion[]>([]);
const motionBlurEnabledRef = useRef(motionBlurEnabled);
const motionBlurAmountRef = useRef(motionBlurAmount);
const motionBlurStateRef = useRef<MotionBlurState>(createMotionBlurState());
const onTimeUpdateRef = useRef(onTimeUpdate);
const onPlayStateChangeRef = useRef(onPlayStateChange);
@@ -400,8 +399,8 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
}, [speedRegions]);
useEffect(() => {
motionBlurEnabledRef.current = motionBlurEnabled;
}, [motionBlurEnabled]);
motionBlurAmountRef.current = motionBlurAmount;
}, [motionBlurAmount]);
useEffect(() => {
onTimeUpdateRef.current = onTimeUpdate;
@@ -475,7 +474,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
focusY: DEFAULT_FOCUS.cy,
motionIntensity: 0,
isPlaying: false,
motionBlurAmount: motionBlurEnabledRef.current ? ZOOM_MOTION_BLUR_AMOUNT : 0,
motionBlurAmount: motionBlurAmountRef.current,
});
requestAnimationFrame(() => {
@@ -739,7 +738,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
motionIntensity,
motionVector,
isPlaying: isPlayingRef.current,
motionBlurAmount: motionBlurEnabledRef.current ? ZOOM_MOTION_BLUR_AMOUNT : 0,
motionBlurAmount: motionBlurAmountRef.current,
transformOverride: transform,
motionBlurState: motionBlurStateRef.current,
frameTimeMs: performance.now(),
@@ -28,7 +28,7 @@ export interface ProjectEditorState {
wallpaper: string;
shadowIntensity: number;
showBlur: boolean;
motionBlurEnabled: boolean;
motionBlurAmount: number;
borderRadius: number;
padding: number;
cropRegion: CropRegion;
@@ -302,8 +302,13 @@ export function normalizeProjectEditor(editor: Partial<ProjectEditorState>): Pro
wallpaper: typeof editor.wallpaper === "string" ? editor.wallpaper : WALLPAPER_PATHS[0],
shadowIntensity: typeof editor.shadowIntensity === "number" ? editor.shadowIntensity : 0,
showBlur: typeof editor.showBlur === "boolean" ? editor.showBlur : false,
motionBlurEnabled:
typeof editor.motionBlurEnabled === "boolean" ? editor.motionBlurEnabled : false,
motionBlurAmount: isFiniteNumber(editor.motionBlurAmount)
? clamp(editor.motionBlurAmount, 0, 1)
: typeof (editor as { motionBlurEnabled?: unknown }).motionBlurEnabled === "boolean"
? (editor as { motionBlurEnabled?: boolean }).motionBlurEnabled
? 0.35
: 0
: 0,
borderRadius: typeof editor.borderRadius === "number" ? editor.borderRadius : 0,
padding: isFiniteNumber(editor.padding) ? clamp(editor.padding, 0, 100) : 50,
cropRegion: {
+2 -2
View File
@@ -20,7 +20,7 @@ export interface EditorState {
wallpaper: string;
shadowIntensity: number;
showBlur: boolean;
motionBlurEnabled: boolean;
motionBlurAmount: number;
borderRadius: number;
padding: number;
aspectRatio: AspectRatio;
@@ -35,7 +35,7 @@ export const INITIAL_EDITOR_STATE: EditorState = {
wallpaper: "/wallpapers/wallpaper1.jpg",
shadowIntensity: 0,
showBlur: false,
motionBlurEnabled: false,
motionBlurAmount: 0,
borderRadius: 0,
padding: 50,
aspectRatio: "16:9",
+2 -2
View File
@@ -40,7 +40,7 @@ interface FrameRenderConfig {
showShadow: boolean;
shadowIntensity: number;
showBlur: boolean;
motionBlurEnabled?: boolean;
motionBlurAmount?: number;
borderRadius?: number;
padding?: number;
cropRegion: CropRegion;
@@ -351,7 +351,7 @@ export class FrameRenderer {
focusY: this.animationState.focusY,
motionIntensity: maxMotionIntensity,
isPlaying: true,
motionBlurAmount: this.config.motionBlurEnabled ? 0.35 : 0,
motionBlurAmount: this.config.motionBlurAmount ?? 0,
motionBlurState: this.motionBlurState,
frameTimeMs: timeMs,
});
+2 -2
View File
@@ -32,7 +32,7 @@ interface GifExporterConfig {
showShadow: boolean;
shadowIntensity: number;
showBlur: boolean;
motionBlurEnabled?: boolean;
motionBlurAmount?: number;
borderRadius?: number;
padding?: number;
videoPadding?: number;
@@ -106,7 +106,7 @@ export class GifExporter {
showShadow: this.config.showShadow,
shadowIntensity: this.config.shadowIntensity,
showBlur: this.config.showBlur,
motionBlurEnabled: this.config.motionBlurEnabled,
motionBlurAmount: this.config.motionBlurAmount,
borderRadius: this.config.borderRadius,
padding: this.config.padding,
cropRegion: this.config.cropRegion,
+2 -2
View File
@@ -20,7 +20,7 @@ interface VideoExporterConfig extends ExportConfig {
showShadow: boolean;
shadowIntensity: number;
showBlur: boolean;
motionBlurEnabled?: boolean;
motionBlurAmount?: number;
borderRadius?: number;
padding?: number;
videoPadding?: number;
@@ -70,7 +70,7 @@ export class VideoExporter {
showShadow: this.config.showShadow,
shadowIntensity: this.config.shadowIntensity,
showBlur: this.config.showBlur,
motionBlurEnabled: this.config.motionBlurEnabled,
motionBlurAmount: this.config.motionBlurAmount,
borderRadius: this.config.borderRadius,
padding: this.config.padding,
cropRegion: this.config.cropRegion,