How to add a ripple transition with Claude

Rings of water ripples spread out from the middle of clip A, bending the picture with light and dark bands, and clip B shows inside the widening wave until it fills the frame.

Get this effect

  1. Switch to Code in your Claude Desktop App

  2. Install the skill (once)

    Show the prompt
    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 ripple transition
    Length of the transition in seconds: 1.2
    Where the ripples start, 0-1 across and down the frame: 0.5, 0.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 ripple transition.

Settings you can change

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

SettingDefaultWhat it does
D1.2Length of the transition in seconds.
X, Y0.5, 0.5Where the ripples start, 0-1 across and down the frame.
WAVELENGTH70Distance between ripple rings in pixels.
AMP14Furthest a ripple bends the picture, in pixels.
AHEAD60How far the ripples reach ahead of the wave front, in pixels.
BEHIND200How far the trailing rings reach behind the wave front, in pixels.
EDGE60Width of the soft edge where clip A turns into clip B, in pixels.
SHINE28Brightness of the light and dark bands on the ripples.
FPS30Output frame rate.

Without the skill

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

Show the prompt
Join clipA.mp4 and clipB.mp4 with a 1.2 second ripple transition that starts 1.2 seconds before clipA.mp4 ends, so the output is A's length plus B's length minus 1.2 seconds (10.8 seconds for two 6 second clips; B starts from its first frame when the transition starts). Use Python with numpy and OpenCV (cv2.remap), working frame by frame at 30fps and fitting B to A's size first. On each transition frame, with progress p from 1/36 to 1: a wave front grows from the center of the frame at radius R = p * (distance from the center to a corner + 60) pixels. For every pixel at distance r from the center, let d = r - R, strength = sin(pi * p) * exp(-(d / s)^2) with s = 60 pixels when d > 0 (ahead of the front) and 200 pixels behind it, and phase = 2 * pi * d / 70. Bend both clips radially by sampling each pixel from 14 * strength * sin(phase) pixels closer to the center (reflect at the borders). Show B inside the front and A outside it, mixing with weight clip(0.5 - d / 60, 0, 1) for B, then add 28 * strength * cos(phase) to all three channels so the rings get light and dark bands like water. Leave every frame outside the transition untouched, so clip A is clean until 4.8 seconds and clip B is clean from 6.0 seconds. Crossfade the audio over the same 1.2 seconds. Keep the audio and save it as output.mp4 (H.264, yuv420p).

Run it without Claude

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

python3 ripple-transition.py clipA.mp4 clipB.mp4 output.mp4
Show the script
#!/usr/bin/env python3
"""Ripple transition: rings of water ripples spread out from the middle of clip A, bending the picture, and clip B shows inside the widening wave.

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

import cv2
import numpy as np

D = 1.2            # transition length in seconds
X, Y = 0.5, 0.5    # where the ripples start (0-1 across and down the frame)
WAVELENGTH = 70    # distance between ripple rings in pixels
AMP = 14           # furthest a ripple bends the picture, in pixels
AHEAD = 60         # how far the ripples reach ahead of the wave front, in pixels
BEHIND = 200       # how far the trailing rings reach behind it, in pixels
EDGE = 60          # width of the soft edge where A turns into B, in pixels
SHINE = 28         # brightness of the light and dark bands on the ripples
FPS = 30


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


def frames(path):
    """Every frame of a clip, fitted to A's size at 30fps, as an RGB array."""
    fit = f"scale={w}:{h}:force_original_aspect_ratio=increase,crop={w}:{h},fps={FPS}"
    ff = subprocess.Popen(["ffmpeg", "-v", "error", "-i", path, "-vf", fit, "-f", "rawvideo", "-pix_fmt", "rgb24", "-"],
                          stdout=subprocess.PIPE)
    while True:
        chunk = ff.stdout.read(w * h * 3)
        if len(chunk) < w * h * 3:
            return
        yield np.frombuffer(chunk, np.uint8).reshape(h, w, 3)


def ripple(a, b, p):
    """Frame at progress p (0-1): a ring-shaped wave grows from (X, Y), B inside it and A outside, both bent by the ripples."""
    d = r - p * reach                                                    # distance past the wave front
    strength = np.sin(np.pi * p) * np.exp(-(d / np.where(d > 0, AHEAD, BEHIND)) ** 2)
    phase = 2 * np.pi * d / WAVELENGTH
    push = AMP * strength * np.sin(phase)                                # outward (+) or inward (-) bend
    map_x = (xs - push * ux).astype(np.float32)
    map_y = (ys - push * uy).astype(np.float32)
    bent_a = cv2.remap(a, map_x, map_y, cv2.INTER_LINEAR, borderMode=cv2.BORDER_REFLECT)
    bent_b = cv2.remap(b, map_x, map_y, cv2.INTER_LINEAR, borderMode=cv2.BORDER_REFLECT)
    inside = np.clip(0.5 - d / EDGE, 0, 1)[..., None]
    out = bent_a * (1 - inside) + bent_b * inside
    return out + (SHINE * strength * np.cos(phase))[..., None]           # light on one slope of each ring, shade on the other


a_path, b_path, out = sys.argv[1:4]
w, h, a_len = probe(a_path)
ys, xs = np.mgrid[0:h, 0:w].astype(np.float32)
dx, dy = xs - X * w, ys - Y * h
r = np.hypot(dx, dy)
ux, uy = dx / np.maximum(r, 1), dy / np.maximum(r, 1)
reach = np.hypot(max(X, 1 - X) * w, max(Y, 1 - Y) * h) + EDGE           # the front passes the farthest corner at the end
start = round((a_len - D) * FPS)    # first frame of the transition
steps = round(D * FPS)
ff = subprocess.Popen(["ffmpeg", "-y", "-v", "error", "-f", "rawvideo", "-pix_fmt", "rgb24", "-s", f"{w}x{h}",
                       "-r", str(FPS), "-i", "-", "-i", a_path, "-i", b_path,
                       "-filter_complex", f"[1:a][2:a]acrossfade=d={D}[au]", "-map", "0:v", "-map", "[au]",
                       "-c:v", "libx264", "-crf", "18", "-pix_fmt", "yuv420p", "-c:a", "aac", out], stdin=subprocess.PIPE)
b_frames = frames(b_path)
for i, a in enumerate(frames(a_path)):
    if i >= start:
        a = ripple(a, next(b_frames), (i - start + 1) / steps)
    ff.stdin.write(np.clip(a, 0, 255).astype(np.uint8).tobytes())
for b in b_frames:
    ff.stdin.write(b.tobytes())
ff.stdin.close()
if ff.wait():
    sys.exit("ffmpeg failed")

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