---
title: "How to add a lens distortion transition with Claude (tested prompt and script)"
description: "Clip A swells into a fisheye bulge that blows up the middle of the frame, then clip B flattens back out of it. Install one skill and ask Claude Code, or run the Python script we tested yourself."
url: https://www.recharm.com/effects/lens-distortion-transition
---

# How to add a lens distortion transition with Claude

Clip A swells into a fisheye bulge that blows up the middle of the frame, then clip B flattens back out of it.

Made by the script on this page, from our own test clips.

## Get this effect

1. ### Switch to Code in your [Claude Desktop App](https://claude.com/download)

2. ### Install the skill (once)

   ```
   Install the Recharm video effects skill: download https://recharm-growth-staging.s3.us-east-2.amazonaws.com/website/downloads/recharm-video-effects.zip and unzip it into ~/.claude/skills
   ```

3. ### Ask for the effect

   ```
   Join clipA.mp4 and clipB.mp4 with a lens distortion transition
   Length of the transition in seconds: 0.8
   How much the center is magnified at the peak (1.5 = 2.5x); the corners stay put: 1.5
   ```

## Several videos, several effects, one ask

Put your clips in the folder, mention them with @, and ask for everything at once. Claude runs one tested script after another on each clip, then joins them.

Put a sale badge on **@clip1.mp4**, give **@clip2.mp4** a VHS look, then join them with a lens distortion transition.

## Settings you can change

Ask Claude to change any of these, or edit them at the top of the script.

| Setting | Default | What it does                                                                     |
| ------- | ------- | -------------------------------------------------------------------------------- |
| `D`     | `0.8`   | Length of the transition in seconds.                                             |
| `BULGE` | `1.5`   | How much the center is magnified at the peak (1.5 = 2.5x); the corners stay put. |
| `SWAP`  | `0.2`   | Share of the transition, around the peak, over which A dissolves into B.         |

## Without the skill

Don’t want to install the skill? Paste this prompt into Claude Code and Claude writes the script itself.

```
Join clipA.mp4 and clipB.mp4 with a 0.8 second lens distortion transition over the last 0.8 seconds of A and the first 0.8 seconds of B. Use Python with OpenCV (cv2.remap) to bend each transition frame like a fisheye lens about the frame's center: an output pixel at radius r (0 at the center, 1 at the corner) samples the source at radius r * (1 + k*r^2) / (1 + k), so the corners stay put and the center is magnified 1 + k times. k rises from 0 to 1.5 and back to 0 with a cosine ease (k = 1.5 * (1 - cos(2*pi*p)) / 2, p from 0 to 1 across the transition). Bulge both clips the same way and dissolve from A to B over the middle 20% of the transition (p 0.4 to 0.6). Before the transition show A unchanged, after it B unchanged. Crossfade the audio over the same 0.8 seconds. Match B's size and frame rate to A, and save it as output.mp4.
```

## Run it without Claude

This is the exact script we tested. Save it as `lens-distortion-transition.py` and run:

```
python3 lens-distortion-transition.py clipA.mp4 clipB.mp4 output.mp4
```

```
#!/usr/bin/env python3
"""Lens distortion transition: clip A bulges out like a fisheye lens, then clip B flattens back from the bulge, over 0.8s.

Usage: python3 lens-distortion-transition.py clipA.mp4 clipB.mp4 output.mp4  (both clips need an audio track)
Needs ffmpeg, OpenCV and numpy (pip install opencv-python-headless numpy).
"""
import json
import math
import subprocess
import sys

import cv2
import numpy as np

D = 0.8          # transition length in seconds
BULGE = 1.5      # how much the center is magnified at the peak (1.5 = 2.5x); the corners stay put
SWAP = 0.2       # share of the transition, around the peak, over which A dissolves into B

def probe(path):
    info = json.loads(subprocess.check_output(["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries",
                                               "stream=width,height,r_frame_rate:format=duration", "-of", "json", path]))
    s = info["streams"][0]
    num, den = s["r_frame_rate"].split("/")
    return s["width"], s["height"], s["r_frame_rate"], float(num) / float(den), float(info["format"]["duration"])

def frames(path, start):
    """The transition's frames from path, starting at `start` seconds, fitted to A's size and frame rate."""
    raw = subprocess.check_output(["ffmpeg", "-v", "error", "-ss", f"{start:.3f}", "-i", path, "-frames:v", str(n),
                                   "-vf", f"scale={w}:{h}:force_original_aspect_ratio=increase,crop={w}:{h},fps={rate}",
                                   "-f", "rawvideo", "-pix_fmt", "rgb24", "-"])
    return [np.frombuffer(raw[i * w * h * 3:(i + 1) * w * h * 3], np.uint8).reshape(h, w, 3) for i in range(n)]

def bulged(img, k):
    """img seen through a fisheye: a pixel at radius r (1 = corner) samples the source at r * (1 + k r^2) / (1 + k)."""
    scale = (1 + k * r2) / (1 + k)
    return cv2.remap(img, (cx + dx * scale).astype(np.float32), (cy + dy * scale).astype(np.float32),
                     cv2.INTER_LINEAR, borderMode=cv2.BORDER_REFLECT)

def lens_frame(i):
    """Frame i: the bulge swells to BULGE and back (cosine ease), A dissolving into B around the peak."""
    p = i / (n - 1)
    k = BULGE * (1 - math.cos(2 * math.pi * p)) / 2
    mix = min(max((p - 0.5) / SWAP + 0.5, 0), 1)
    a, b = bulged(a_frames[i], k), bulged(b_frames[i], k)
    return cv2.addWeighted(a, 1 - mix, b, mix, 0)

a_path, b_path, out = sys.argv[1:4]
w, h, rate, fps, a_len = probe(a_path)
n = round(D * fps)
offset = a_len - D
a_frames, b_frames = frames(a_path, offset), frames(b_path, 0)

# every output pixel's offset from the center, and its squared radius as a share of the corner distance
cx, cy = (w - 1) / 2, (h - 1) / 2
ys, xs = np.mgrid[0:h, 0:w].astype(np.float32)
dx, dy = xs - cx, ys - cy
r2 = (dx ** 2 + dy ** 2) / (cx ** 2 + cy ** 2)

# A up to the transition, the bulged frames, then B from where the transition ends; the sound crossfades over D
ff = subprocess.Popen(["ffmpeg", "-y", "-v", "error", "-i", a_path, "-i", b_path,
                       "-f", "rawvideo", "-pix_fmt", "rgb24", "-s", f"{w}x{h}", "-r", rate, "-i", "-",
                       "-filter_complex",
                       f"[0:v]trim=end={offset:.4f},setpts=PTS-STARTPTS,fps={rate},format=yuv420p,setsar=1[a];"
                       f"[1:v]scale={w}:{h}:force_original_aspect_ratio=increase,crop={w}:{h},fps={rate},"
                       f"trim=start={D},setpts=PTS-STARTPTS,format=yuv420p,setsar=1[b];"
                       f"[2:v]format=yuv420p,setsar=1[s];"
                       f"[a][s][b]concat=n=3:v=1:a=0[v];[0:a][1:a]acrossfade=d={D}[au]",
                       "-map", "[v]", "-map", "[au]", "-c:v", "libx264", "-crf", "18", "-pix_fmt", "yuv420p",
                       "-c:a", "aac", out], stdin=subprocess.PIPE)
for i in range(n):
    ff.stdin.write(lens_frame(i).tobytes())
ff.stdin.close()
if ff.wait():
    sys.exit("ffmpeg failed")
```

## More transitions

- [Barn door transition **Barn door transition**](https://www.recharm.com/effects/barn-door)
- [Blinds transition **Blinds transition**](https://www.recharm.com/effects/blinds-transition)
- [Block dissolve **Block dissolve**](https://www.recharm.com/effects/block-dissolve)
- [Blur transition **Blur transition**](https://www.recharm.com/effects/blur-transition)
- [Bounce transition **Bounce transition**](https://www.recharm.com/effects/bounce-transition)

## Find 3x _winning ads_

Recharm cuts your ad footage into tagged, searchable clips: hooks, testimonials, product shots. Find the shots that win, add the effect with Claude, and ship 3x more ad edits.

[Start 14-day free trial](https://www.recharm.com/get-started)
