Skip to content

Commit c8cf052

Browse files
Merge pull request #221 from EtienneLescot/feat/motion-blur-slider
feat: replace motion blur toggle with intensity slider
2 parents 9d71f50 + 446e3a3 commit c8cf052

9 files changed

Lines changed: 72 additions & 50 deletions

File tree

src/components/video-editor/SettingsPanel.tsx

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,9 @@ interface SettingsPanelProps {
9292
onShadowCommit?: () => void;
9393
showBlur?: boolean;
9494
onBlurChange?: (showBlur: boolean) => void;
95-
motionBlurEnabled?: boolean;
96-
onMotionBlurChange?: (enabled: boolean) => void;
95+
motionBlurAmount?: number;
96+
onMotionBlurChange?: (amount: number) => void;
97+
onMotionBlurCommit?: () => void;
9798
borderRadius?: number;
9899
onBorderRadiusChange?: (radius: number) => void;
99100
onBorderRadiusCommit?: () => void;
@@ -157,8 +158,9 @@ export function SettingsPanel({
157158
onShadowCommit,
158159
showBlur,
159160
onBlurChange,
160-
motionBlurEnabled = false,
161+
motionBlurAmount = 0,
161162
onMotionBlurChange,
163+
onMotionBlurCommit,
162164
borderRadius = 0,
163165
onBorderRadiusChange,
164166
onBorderRadiusCommit,
@@ -574,14 +576,6 @@ export function SettingsPanel({
574576
</AccordionTrigger>
575577
<AccordionContent className="pb-3">
576578
<div className="grid grid-cols-2 gap-2 mb-3">
577-
<div className="flex items-center justify-between p-2 rounded-lg bg-white/5 border border-white/5">
578-
<div className="text-[10px] font-medium text-slate-300">Motion Blur</div>
579-
<Switch
580-
checked={motionBlurEnabled}
581-
onCheckedChange={onMotionBlurChange}
582-
className="data-[state=checked]:bg-[#34B27B] scale-90"
583-
/>
584-
</div>
585579
<div className="flex items-center justify-between p-2 rounded-lg bg-white/5 border border-white/5">
586580
<div className="text-[10px] font-medium text-slate-300">Blur BG</div>
587581
<Switch
@@ -593,6 +587,23 @@ export function SettingsPanel({
593587
</div>
594588

595589
<div className="grid grid-cols-2 gap-2">
590+
<div className="p-2 rounded-lg bg-white/5 border border-white/5">
591+
<div className="flex items-center justify-between mb-1">
592+
<div className="text-[10px] font-medium text-slate-300">Motion Blur</div>
593+
<span className="text-[10px] text-slate-500 font-mono">
594+
{motionBlurAmount === 0 ? "off" : motionBlurAmount.toFixed(2)}
595+
</span>
596+
</div>
597+
<Slider
598+
value={[motionBlurAmount]}
599+
onValueChange={(values) => onMotionBlurChange?.(values[0])}
600+
onValueCommit={() => onMotionBlurCommit?.()}
601+
min={0}
602+
max={1}
603+
step={0.01}
604+
className="w-full [&_[role=slider]]:bg-[#34B27B] [&_[role=slider]]:border-[#34B27B] [&_[role=slider]]:h-3 [&_[role=slider]]:w-3"
605+
/>
606+
</div>
596607
<div className="p-2 rounded-lg bg-white/5 border border-white/5">
597608
<div className="flex items-center justify-between mb-1">
598609
<div className="text-[10px] font-medium text-slate-300">Shadow</div>

src/components/video-editor/VideoEditor.tsx

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export default function VideoEditor() {
7070
wallpaper,
7171
shadowIntensity,
7272
showBlur,
73-
motionBlurEnabled,
73+
motionBlurAmount,
7474
borderRadius,
7575
padding,
7676
aspectRatio,
@@ -139,7 +139,7 @@ export default function VideoEditor() {
139139
wallpaper: normalizedEditor.wallpaper,
140140
shadowIntensity: normalizedEditor.shadowIntensity,
141141
showBlur: normalizedEditor.showBlur,
142-
motionBlurEnabled: normalizedEditor.motionBlurEnabled,
142+
motionBlurAmount: normalizedEditor.motionBlurAmount,
143143
borderRadius: normalizedEditor.borderRadius,
144144
padding: normalizedEditor.padding,
145145
cropRegion: normalizedEditor.cropRegion,
@@ -198,7 +198,7 @@ export default function VideoEditor() {
198198
wallpaper,
199199
shadowIntensity,
200200
showBlur,
201-
motionBlurEnabled,
201+
motionBlurAmount,
202202
borderRadius,
203203
padding,
204204
cropRegion,
@@ -220,7 +220,7 @@ export default function VideoEditor() {
220220
wallpaper,
221221
shadowIntensity,
222222
showBlur,
223-
motionBlurEnabled,
223+
motionBlurAmount,
224224
borderRadius,
225225
padding,
226226
cropRegion,
@@ -294,7 +294,7 @@ export default function VideoEditor() {
294294
wallpaper,
295295
shadowIntensity,
296296
showBlur,
297-
motionBlurEnabled,
297+
motionBlurAmount,
298298
borderRadius,
299299
padding,
300300
cropRegion,
@@ -347,7 +347,7 @@ export default function VideoEditor() {
347347
wallpaper,
348348
shadowIntensity,
349349
showBlur,
350-
motionBlurEnabled,
350+
motionBlurAmount,
351351
borderRadius,
352352
padding,
353353
cropRegion,
@@ -933,7 +933,7 @@ export default function VideoEditor() {
933933
showShadow: shadowIntensity > 0,
934934
shadowIntensity,
935935
showBlur,
936-
motionBlurEnabled,
936+
motionBlurAmount,
937937
borderRadius,
938938
padding,
939939
videoPadding: padding,
@@ -1060,7 +1060,7 @@ export default function VideoEditor() {
10601060
showShadow: shadowIntensity > 0,
10611061
shadowIntensity,
10621062
showBlur,
1063-
motionBlurEnabled,
1063+
motionBlurAmount,
10641064
borderRadius,
10651065
padding,
10661066
cropRegion,
@@ -1121,7 +1121,7 @@ export default function VideoEditor() {
11211121
speedRegions,
11221122
shadowIntensity,
11231123
showBlur,
1124-
motionBlurEnabled,
1124+
motionBlurAmount,
11251125
borderRadius,
11261126
padding,
11271127
cropRegion,
@@ -1270,7 +1270,7 @@ export default function VideoEditor() {
12701270
showShadow={shadowIntensity > 0}
12711271
shadowIntensity={shadowIntensity}
12721272
showBlur={showBlur}
1273-
motionBlurEnabled={motionBlurEnabled}
1273+
motionBlurAmount={motionBlurAmount}
12741274
borderRadius={borderRadius}
12751275
padding={padding}
12761276
cropRegion={cropRegion}
@@ -1369,8 +1369,9 @@ export default function VideoEditor() {
13691369
onShadowCommit={commitState}
13701370
showBlur={showBlur}
13711371
onBlurChange={(v) => pushState({ showBlur: v })}
1372-
motionBlurEnabled={motionBlurEnabled}
1373-
onMotionBlurChange={(v) => pushState({ motionBlurEnabled: v })}
1372+
motionBlurAmount={motionBlurAmount}
1373+
onMotionBlurChange={(v) => updateState({ motionBlurAmount: v })}
1374+
onMotionBlurCommit={commitState}
13741375
borderRadius={borderRadius}
13751376
onBorderRadiusChange={(v) => updateState({ borderRadius: v })}
13761377
onBorderRadiusCommit={commitState}

src/components/video-editor/VideoPlayback.tsx

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ interface VideoPlaybackProps {
7070
showShadow?: boolean;
7171
shadowIntensity?: number;
7272
showBlur?: boolean;
73-
motionBlurEnabled?: boolean;
73+
motionBlurAmount?: number;
7474
borderRadius?: number;
7575
padding?: number;
7676
cropRegion?: import("./types").CropRegion;
@@ -113,7 +113,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
113113
showShadow,
114114
shadowIntensity = 0,
115115
showBlur,
116-
motionBlurEnabled = false,
116+
motionBlurAmount = 0,
117117
borderRadius = 0,
118118
padding = 50,
119119
cropRegion,
@@ -128,7 +128,6 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
128128
},
129129
ref,
130130
) => {
131-
const ZOOM_MOTION_BLUR_AMOUNT = 0.35;
132131
const videoRef = useRef<HTMLVideoElement | null>(null);
133132
const containerRef = useRef<HTMLDivElement | null>(null);
134133
const appRef = useRef<Application | null>(null);
@@ -169,7 +168,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
169168
const layoutVideoContentRef = useRef<(() => void) | null>(null);
170169
const trimRegionsRef = useRef<TrimRegion[]>([]);
171170
const speedRegionsRef = useRef<SpeedRegion[]>([]);
172-
const motionBlurEnabledRef = useRef(motionBlurEnabled);
171+
const motionBlurAmountRef = useRef(motionBlurAmount);
173172
const motionBlurStateRef = useRef<MotionBlurState>(createMotionBlurState());
174173
const onTimeUpdateRef = useRef(onTimeUpdate);
175174
const onPlayStateChangeRef = useRef(onPlayStateChange);
@@ -400,8 +399,8 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
400399
}, [speedRegions]);
401400

402401
useEffect(() => {
403-
motionBlurEnabledRef.current = motionBlurEnabled;
404-
}, [motionBlurEnabled]);
402+
motionBlurAmountRef.current = motionBlurAmount;
403+
}, [motionBlurAmount]);
405404

406405
useEffect(() => {
407406
onTimeUpdateRef.current = onTimeUpdate;
@@ -475,7 +474,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
475474
focusY: DEFAULT_FOCUS.cy,
476475
motionIntensity: 0,
477476
isPlaying: false,
478-
motionBlurAmount: motionBlurEnabledRef.current ? ZOOM_MOTION_BLUR_AMOUNT : 0,
477+
motionBlurAmount: motionBlurAmountRef.current,
479478
});
480479

481480
requestAnimationFrame(() => {
@@ -739,7 +738,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
739738
motionIntensity,
740739
motionVector,
741740
isPlaying: isPlayingRef.current,
742-
motionBlurAmount: motionBlurEnabledRef.current ? ZOOM_MOTION_BLUR_AMOUNT : 0,
741+
motionBlurAmount: motionBlurAmountRef.current,
743742
transformOverride: transform,
744743
motionBlurState: motionBlurStateRef.current,
745744
frameTimeMs: performance.now(),

src/components/video-editor/projectPersistence.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export interface ProjectEditorState {
2828
wallpaper: string;
2929
shadowIntensity: number;
3030
showBlur: boolean;
31-
motionBlurEnabled: boolean;
31+
motionBlurAmount: number;
3232
borderRadius: number;
3333
padding: number;
3434
cropRegion: CropRegion;
@@ -302,8 +302,13 @@ export function normalizeProjectEditor(editor: Partial<ProjectEditorState>): Pro
302302
wallpaper: typeof editor.wallpaper === "string" ? editor.wallpaper : WALLPAPER_PATHS[0],
303303
shadowIntensity: typeof editor.shadowIntensity === "number" ? editor.shadowIntensity : 0,
304304
showBlur: typeof editor.showBlur === "boolean" ? editor.showBlur : false,
305-
motionBlurEnabled:
306-
typeof editor.motionBlurEnabled === "boolean" ? editor.motionBlurEnabled : false,
305+
motionBlurAmount: isFiniteNumber(editor.motionBlurAmount)
306+
? clamp(editor.motionBlurAmount, 0, 1)
307+
: typeof (editor as { motionBlurEnabled?: unknown }).motionBlurEnabled === "boolean"
308+
? (editor as { motionBlurEnabled?: boolean }).motionBlurEnabled
309+
? 0.35
310+
: 0
311+
: 0,
307312
borderRadius: typeof editor.borderRadius === "number" ? editor.borderRadius : 0,
308313
padding: isFiniteNumber(editor.padding) ? clamp(editor.padding, 0, 100) : 50,
309314
cropRegion: {

src/components/video-editor/videoPlayback/zoomTransform.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
import { BlurFilter, Container } from "pixi.js";
22
import { MotionBlurFilter } from "pixi-filters/motion-blur";
33

4-
const PEAK_VELOCITY_PPS = 2000;
5-
const MAX_BLUR_PX = 8;
6-
const VELOCITY_THRESHOLD_PPS = 15;
4+
const PEAK_VELOCITY_PPS = 1400;
5+
const MAX_BLUR_PX = 14;
6+
const VELOCITY_THRESHOLD_PPS = 12;
7+
const MAX_AMOUNT_BOOST = 2.2;
8+
9+
function getMotionBlurAmountResponse(motionBlurAmount: number) {
10+
const clampedAmount = Math.min(1, Math.max(0, motionBlurAmount));
11+
// Keep the low end usable while giving the top of the slider substantially more headroom.
12+
return clampedAmount * (1 + (MAX_AMOUNT_BOOST - 1) * clampedAmount);
13+
}
714

815
export interface MotionBlurState {
916
lastFrameTimeMs: number;
@@ -185,6 +192,7 @@ export function applyZoomTransform({
185192
const dtMs = Math.min(80, Math.max(1, now - motionBlurState.lastFrameTimeMs));
186193
const dtSeconds = dtMs / 1000;
187194
motionBlurState.lastFrameTimeMs = now;
195+
const amountResponse = getMotionBlurAmountResponse(motionBlurAmount);
188196

189197
// Camera displacement this frame (stage-px)
190198
const dx = transform.x - motionBlurState.prevCamX;
@@ -204,17 +212,15 @@ export function applyZoomTransform({
204212

205213
const normalised = Math.min(1, speed / PEAK_VELOCITY_PPS);
206214
const targetBlur =
207-
speed < VELOCITY_THRESHOLD_PPS
208-
? 0
209-
: normalised * normalised * MAX_BLUR_PX * motionBlurAmount;
215+
speed < VELOCITY_THRESHOLD_PPS ? 0 : normalised * normalised * MAX_BLUR_PX * amountResponse;
210216

211217
const dirMag = Math.sqrt(velocityX * velocityX + velocityY * velocityY) || 1;
212-
const velocityScale = targetBlur * 1.2;
218+
const velocityScale = targetBlur * 2.4;
213219
motionBlurFilter.velocity =
214220
targetBlur > 0
215221
? { x: (velocityX / dirMag) * velocityScale, y: (velocityY / dirMag) * velocityScale }
216222
: { x: 0, y: 0 };
217-
motionBlurFilter.kernelSize = targetBlur > 4 ? 11 : targetBlur > 1.5 ? 9 : 5;
223+
motionBlurFilter.kernelSize = targetBlur > 8 ? 15 : targetBlur > 4 ? 11 : 7;
218224
motionBlurFilter.offset = targetBlur > 0.5 ? -0.2 : 0;
219225

220226
if (blurFilter) {

src/hooks/useEditorHistory.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export interface EditorState {
2020
wallpaper: string;
2121
shadowIntensity: number;
2222
showBlur: boolean;
23-
motionBlurEnabled: boolean;
23+
motionBlurAmount: number;
2424
borderRadius: number;
2525
padding: number;
2626
aspectRatio: AspectRatio;
@@ -35,7 +35,7 @@ export const INITIAL_EDITOR_STATE: EditorState = {
3535
wallpaper: "/wallpapers/wallpaper1.jpg",
3636
shadowIntensity: 0,
3737
showBlur: false,
38-
motionBlurEnabled: false,
38+
motionBlurAmount: 0,
3939
borderRadius: 0,
4040
padding: 50,
4141
aspectRatio: "16:9",

src/lib/exporter/frameRenderer.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ interface FrameRenderConfig {
4040
showShadow: boolean;
4141
shadowIntensity: number;
4242
showBlur: boolean;
43-
motionBlurEnabled?: boolean;
43+
motionBlurAmount?: number;
4444
borderRadius?: number;
4545
padding?: number;
4646
cropRegion: CropRegion;
@@ -351,7 +351,7 @@ export class FrameRenderer {
351351
focusY: this.animationState.focusY,
352352
motionIntensity: maxMotionIntensity,
353353
isPlaying: true,
354-
motionBlurAmount: this.config.motionBlurEnabled ? 0.35 : 0,
354+
motionBlurAmount: this.config.motionBlurAmount ?? 0,
355355
motionBlurState: this.motionBlurState,
356356
frameTimeMs: timeMs,
357357
});

src/lib/exporter/gifExporter.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ interface GifExporterConfig {
3232
showShadow: boolean;
3333
shadowIntensity: number;
3434
showBlur: boolean;
35-
motionBlurEnabled?: boolean;
35+
motionBlurAmount?: number;
3636
borderRadius?: number;
3737
padding?: number;
3838
videoPadding?: number;
@@ -106,7 +106,7 @@ export class GifExporter {
106106
showShadow: this.config.showShadow,
107107
shadowIntensity: this.config.shadowIntensity,
108108
showBlur: this.config.showBlur,
109-
motionBlurEnabled: this.config.motionBlurEnabled,
109+
motionBlurAmount: this.config.motionBlurAmount,
110110
borderRadius: this.config.borderRadius,
111111
padding: this.config.padding,
112112
cropRegion: this.config.cropRegion,

src/lib/exporter/videoExporter.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ interface VideoExporterConfig extends ExportConfig {
2020
showShadow: boolean;
2121
shadowIntensity: number;
2222
showBlur: boolean;
23-
motionBlurEnabled?: boolean;
23+
motionBlurAmount?: number;
2424
borderRadius?: number;
2525
padding?: number;
2626
videoPadding?: number;
@@ -70,7 +70,7 @@ export class VideoExporter {
7070
showShadow: this.config.showShadow,
7171
shadowIntensity: this.config.shadowIntensity,
7272
showBlur: this.config.showBlur,
73-
motionBlurEnabled: this.config.motionBlurEnabled,
73+
motionBlurAmount: this.config.motionBlurAmount,
7474
borderRadius: this.config.borderRadius,
7575
padding: this.config.padding,
7676
cropRegion: this.config.cropRegion,

0 commit comments

Comments
 (0)