Merge pull request #221 from EtienneLescot/feat/motion-blur-slider
feat: replace motion blur toggle with intensity slider
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user