Merge pull request #221 from EtienneLescot/feat/motion-blur-slider

feat: replace motion blur toggle with intensity slider
This commit is contained in:
Sid
2026-03-16 08:27:38 -07:00
committed by GitHub
9 changed files with 72 additions and 50 deletions
+22 -11
View File
@@ -92,8 +92,9 @@ interface SettingsPanelProps {
onShadowCommit?: () => void;
showBlur?: boolean;
onBlurChange?: (showBlur: boolean) => void;
motionBlurEnabled?: boolean;
onMotionBlurChange?: (enabled: boolean) => void;
motionBlurAmount?: number;
onMotionBlurChange?: (amount: number) => void;
onMotionBlurCommit?: () => void;
borderRadius?: number;
onBorderRadiusChange?: (radius: number) => void;
onBorderRadiusCommit?: () => void;
@@ -157,8 +158,9 @@ export function SettingsPanel({
onShadowCommit,
showBlur,
onBlurChange,
motionBlurEnabled = false,
motionBlurAmount = 0,
onMotionBlurChange,
onMotionBlurCommit,
borderRadius = 0,
onBorderRadiusChange,
onBorderRadiusCommit,
@@ -574,14 +576,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 +587,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={() => onMotionBlurCommit?.()}
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>
+13 -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,9 @@ export default function VideoEditor() {
onShadowCommit={commitState}
showBlur={showBlur}
onBlurChange={(v) => pushState({ showBlur: v })}
motionBlurEnabled={motionBlurEnabled}
onMotionBlurChange={(v) => pushState({ motionBlurEnabled: v })}
motionBlurAmount={motionBlurAmount}
onMotionBlurChange={(v) => updateState({ motionBlurAmount: v })}
onMotionBlurCommit={commitState}
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: {
@@ -1,9 +1,16 @@
import { BlurFilter, Container } from "pixi.js";
import { MotionBlurFilter } from "pixi-filters/motion-blur";
const PEAK_VELOCITY_PPS = 2000;
const MAX_BLUR_PX = 8;
const VELOCITY_THRESHOLD_PPS = 15;
const PEAK_VELOCITY_PPS = 1400;
const MAX_BLUR_PX = 14;
const VELOCITY_THRESHOLD_PPS = 12;
const MAX_AMOUNT_BOOST = 2.2;
function getMotionBlurAmountResponse(motionBlurAmount: number) {
const clampedAmount = Math.min(1, Math.max(0, motionBlurAmount));
// Keep the low end usable while giving the top of the slider substantially more headroom.
return clampedAmount * (1 + (MAX_AMOUNT_BOOST - 1) * clampedAmount);
}
export interface MotionBlurState {
lastFrameTimeMs: number;
@@ -185,6 +192,7 @@ export function applyZoomTransform({
const dtMs = Math.min(80, Math.max(1, now - motionBlurState.lastFrameTimeMs));
const dtSeconds = dtMs / 1000;
motionBlurState.lastFrameTimeMs = now;
const amountResponse = getMotionBlurAmountResponse(motionBlurAmount);
// Camera displacement this frame (stage-px)
const dx = transform.x - motionBlurState.prevCamX;
@@ -204,17 +212,15 @@ export function applyZoomTransform({
const normalised = Math.min(1, speed / PEAK_VELOCITY_PPS);
const targetBlur =
speed < VELOCITY_THRESHOLD_PPS
? 0
: normalised * normalised * MAX_BLUR_PX * motionBlurAmount;
speed < VELOCITY_THRESHOLD_PPS ? 0 : normalised * normalised * MAX_BLUR_PX * amountResponse;
const dirMag = Math.sqrt(velocityX * velocityX + velocityY * velocityY) || 1;
const velocityScale = targetBlur * 1.2;
const velocityScale = targetBlur * 2.4;
motionBlurFilter.velocity =
targetBlur > 0
? { x: (velocityX / dirMag) * velocityScale, y: (velocityY / dirMag) * velocityScale }
: { x: 0, y: 0 };
motionBlurFilter.kernelSize = targetBlur > 4 ? 11 : targetBlur > 1.5 ? 9 : 5;
motionBlurFilter.kernelSize = targetBlur > 8 ? 15 : targetBlur > 4 ? 11 : 7;
motionBlurFilter.offset = targetBlur > 0.5 ? -0.2 : 0;
if (blurFilter) {
+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,