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 MotionBlurFilter from pixi-filters, initializing it in src/lib/exporter/frameRenderer.ts with a default kernel size of 5 pixels.
  • Velocity-Based Calculation: The system computes pixel-per-second velocity using MotionBlurState in zoomTransform.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.tsx through VideoEditor.tsx to VideoPlayback.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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →