Motion Blur Implementation in OpenScreen: Smooth Pan and Zoom Effects Explained
OpenScreen generates cinematic motion blur during pan and zoom operations by combining a PixiJS MotionBlurFilter with a custom physics engine that calculates blur intensity from real-time camera velocity.
OpenScreen is an open-source video editing application that produces fluid pan-and-zoom animations through an adaptive motion blur system. The implementation calculates pixel-per-second velocity from camera transformations and applies corresponding blur filters to create natural, speed-responsive visual effects. By leveraging PixiJS filters and a dedicated state management system in src/components/video-editor/videoPlayback/zoomTransform.ts, OpenScreen delivers professional-grade motion smoothing without manual keyframing.
Filter Setup and Initialization
When the rendering pipeline initializes, OpenScreen attaches a PixiJS MotionBlurFilter alongside a standard BlurFilter to the video container. This setup occurs in the FrameRenderer class, which prepares the filter chain before any frames are processed.
In src/lib/exporter/frameRenderer.ts, the implementation instantiates the filter with zero initial velocity and a conservative kernel size:
this.blurFilter = new BlurFilter();
this.motionBlurFilter = new MotionBlurFilter([0, 0], 5, 0);
this.videoContainer.filters = [this.blurFilter, this.motionBlurFilter];
The MotionBlurFilter starts with a velocity vector of [0, 0], a kernel size of 5 pixels, and zero offset. The standard BlurFilter handles static background blur independently, while the motion blur filter dynamically adjusts to camera movement during playback and export.
Tracking Camera Velocity with MotionBlurState
The system tracks camera movement history using a lightweight state object defined in src/components/video-editor/videoPlayback/zoomTransform.ts. The MotionBlurState interface stores the previous transform and timing data necessary for velocity calculations:
export interface MotionBlurState {
lastFrameTimeMs: number;
prevCamX: number;
prevCamY: number;
prevCamScale: number;
initialized: boolean;
}
export function createMotionBlurState(): MotionBlurState { … }
Each frame render updates this state, enabling the system to compute deltas between the current and previous camera positions. The state remains uninitialized until the first frame is processed, ensuring accurate velocity calculations from the start of the animation.
Computing Motion Blur Intensity
OpenScreen calculates blur intensity based on pixel-per-second velocity derived from translational and scale changes. The implementation uses physics-based constants to normalize speed into visual blur amounts.
Velocity Thresholds and Constants
The calculation relies on three key thresholds defined in src/components/video-editor/videoPlayback/zoomTransform.ts:
const PEAK_VELOCITY_PPS = 1400; // pixels per second at max blur
const MAX_BLUR_PX = 14; // maximum blur radius
const VELOCITY_THRESHOLD_PPS = 12; // below this no blur
- PEAK_VELOCITY_PPS: The speed at which maximum blur intensity is reached
- MAX_BLUR_PX: The upper limit for blur radius in pixels
- VELOCITY_THRESHOLD_PPS: The minimum speed required to trigger any blur effect
The Blur Factor Algorithm
For each frame, the system calculates velocity by dividing position deltas (dx, dy, dScale) by elapsed time (dtSeconds). It then normalizes speed against the peak velocity and applies a quadratic response curve for natural falloff:
const normalised = Math.min(1, speed / PEAK_VELOCITY_PPS);
const targetBlur = speed < VELOCITY_THRESHOLD_PPS
? 0
: normalised * normalised * MAX_BLUR_PX * amountResponse;
The speed value combines translational velocity with scale-change velocity. When the calculated speed falls below VELOCITY_THRESHOLD_PPS, the blur effect immediately drops to zero to prevent unwanted artifacts on static frames.
Non-Linear Response Curves
To provide intuitive user control, OpenScreen applies a non-linear response curve to the UI slider value (0-1). The getMotionBlurAmountResponse function in zoomTransform.ts implements this curve:
function getMotionBlurAmountResponse(motionBlurAmount: number) {
const clampedAmount = Math.min(1, Math.max(0, motionBlurAmount));
return clampedAmount * (1 + (MAX_AMOUNT_BOOST - 1) * clampedAmount);
}
This quadratic function ensures that higher slider values produce disproportionately stronger blur effects, giving users fine control at low intensities while allowing dramatic motion smoothing at high settings.
Applying Real-Time Filter Updates
The applyZoomTransform function serves as the central hub for motion blur application, executing on every rendered frame to update filter parameters based on calculated velocity.
Dynamic Filter Configuration
When motion blur is enabled (motionBlurAmount > 0 and isPlaying), the function updates three critical MotionBlurFilter properties:
motionBlurFilter.velocity = targetBlur > 0
? { x: (velocityX / dirMag) * velocityScale,
y: (velocityY / dirMag) * velocityScale }
: { x: 0, y: 0 };
motionBlurFilter.kernelSize = targetBlur > 8 ? 15 : targetBlur > 4 ? 11 : 7;
motionBlurFilter.offset = targetBlur > 0.5 ? -0.2 : 0;
The velocity vector normalizes direction and scales by intensity. The kernel size switches between discrete values (15, 11, or 7) based on blur magnitude for performance optimization. An offset of -0.2 applies subtle directional bias during high-motion scenarios. Simultaneously, the standard BlurFilter resets to zero to prevent double-blurring.
If motion blur is disabled, the system clears filter parameters and marks the MotionBlurState as uninitialized, ensuring clean state transitions when toggling the effect.
UI Integration and Configuration
OpenScreen exposes motion blur control through a React-based component hierarchy that propagates user settings from the settings panel to the rendering engine.
Settings Panel Control
The SettingsPanel component renders a normalized slider (0-1) that updates the global motion blur amount:
<Slider
value={[motionBlurAmount]}
onValueChange={v => onMotionBlurChange(v[0])}
// UI displays “off” when value === 0
/>
VideoPlayback Ref Management
To prevent React re-renders from disrupting the animation loop, VideoPlayback.tsx stores the current blur amount in a mutable ref:
const motionBlurAmountRef = useRef(motionBlurAmount);
useEffect(() => {
motionBlurAmountRef.current = motionBlurAmount;
}, [motionBlurAmount]);
applyZoomTransform({
…,
motionBlurAmount: motionBlurAmountRef.current,
});
This pattern ensures that the applyZoomTransform function always accesses the latest user-selected value during the requestAnimationFrame loop without triggering component re-renders.
Usage Examples
Enabling Motion Blur in a Video Editor Instance
To implement motion blur in a custom OpenScreen integration, pass the motionBlurAmount prop to the VideoEditor component:
import { VideoEditor } from "@/components/video-editor/VideoEditor";
function Demo() {
const [motionBlur, setMotionBlur] = useState(0.5); // 0‑1
return (
<VideoEditor
videoPath="/my‑video.mp4"
motionBlurAmount={motionBlur}
onMotionBlurChange={setMotionBlur}
/>
);
}
Exporting with Motion Blur
The FrameRenderer reuses the same filter configuration during video export, ensuring the final rendered video matches the preview exactly:
this.motionBlurFilter = new MotionBlurFilter([0, 0], 5, 0);
this.videoContainer.filters = [this.blurFilter, this.motionBlurFilter];
applyZoomTransform({
…,
motionBlurAmount: this.config.motionBlurAmount ?? 0,
});
Summary
- PixiJS Integration: OpenScreen utilizes the
MotionBlurFilterfrom pixi-filters, initializing it insrc/lib/exporter/frameRenderer.tswith a default kernel size of 5 pixels. - Velocity-Based Calculation: The system computes pixel-per-second velocity using
MotionBlurStateinzoomTransform.ts, applying thresholds at 12 PPS (minimum) and 1400 PPS (maximum). - Adaptive Parameters: Filter updates include dynamic kernel sizing (7/11/15), directional velocity vectors, and offset adjustments calculated every frame within
applyZoomTransform. - UI Propagation: User controls flow from
SettingsPanel.tsxthroughVideoEditor.tsxtoVideoPlayback.tsx, using refs to maintain performance during real-time preview. - Export Parity: The same filter chain and calculation logic apply to both live preview and final video export, ensuring consistent visual quality.
Frequently Asked Questions
What graphics library does OpenScreen use to render motion blur?
OpenScreen implements motion blur using PixiJS and the MotionBlurFilter from the @pixi/filter-motion-blur package. The filter is instantiated in src/lib/exporter/frameRenderer.ts and attached to the video container's filter array alongside a standard BlurFilter.
How does the motion blur intensity adjust to camera speed?
The system calculates pixel-per-second velocity by comparing current and previous camera positions against elapsed time. When velocity exceeds VELOCITY_THRESHOLD_PPS (12 pixels per second), the blur intensity scales quadratically from 0 to MAX_BLUR_PX (14 pixels) based on the ratio of current speed to PEAK_VELOCITY_PPS (1400 pixels per second).
Can users control the strength of the motion blur effect?
Yes. Users adjust blur intensity via a slider in SettingsPanel.tsx that accepts values from 0 to 1. This value passes through a non-linear response curve in getMotionBlurAmountResponse before being applied to the filter calculations, allowing fine-grained control at low values and dramatic effects at high values.
Does motion blur impact video export performance in OpenScreen?
The motion blur implementation uses the same MotionBlurFilter instance and calculation logic during both preview and export phases in src/lib/exporter/frameRenderer.ts. While the effect requires per-frame velocity calculations, the dynamic kernel sizing (switching between 7, 11, or 15 based on blur magnitude) optimizes performance by reducing computational load during subtle movements.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →