# Motion Blur Implementation in OpenScreen: Smooth Pan and Zoom Effects Explained

> Discover how OpenScreen achieves smooth pan and zoom with motion blur. Learn about the PixiJS MotionBlurFilter and custom physics engine for real-time camera velocity blur.

- Repository: [Sid/openscreen](https://github.com/siddharthvaddem/openscreen)
- Tags: deep-dive
- Published: 2026-04-03

---

**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`](https://github.com/siddharthvaddem/openscreen/blob/main/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`](https://github.com/siddharthvaddem/openscreen/blob/main/src/lib/exporter/frameRenderer.ts), the implementation instantiates the filter with zero initial velocity and a conservative kernel size:

```ts
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`](https://github.com/siddharthvaddem/openscreen/blob/main/src/components/video-editor/videoPlayback/zoomTransform.ts). The **`MotionBlurState`** interface stores the previous transform and timing data necessary for velocity calculations:

```ts
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`](https://github.com/siddharthvaddem/openscreen/blob/main/src/components/video-editor/videoPlayback/zoomTransform.ts):

```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:

```ts
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`](https://github.com/siddharthvaddem/openscreen/blob/main/zoomTransform.ts) implements this curve:

```ts
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:

```ts
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:

```tsx
<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`](https://github.com/siddharthvaddem/openscreen/blob/main/VideoPlayback.tsx) stores the current blur amount in a mutable ref:

```tsx
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:

```tsx
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:

```ts
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`](https://github.com/siddharthvaddem/openscreen/blob/main/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`](https://github.com/siddharthvaddem/openscreen/blob/main/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`](https://github.com/siddharthvaddem/openscreen/blob/main/SettingsPanel.tsx) through [`VideoEditor.tsx`](https://github.com/siddharthvaddem/openscreen/blob/main/VideoEditor.tsx) to [`VideoPlayback.tsx`](https://github.com/siddharthvaddem/openscreen/blob/main/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`](https://github.com/siddharthvaddem/openscreen/blob/main/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`](https://github.com/siddharthvaddem/openscreen/blob/main/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`](https://github.com/siddharthvaddem/openscreen/blob/main/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.