Get this effect
Switch to Code in your Claude Desktop App
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/skillsAsk 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.
| Setting | Default | What it does |
|---|---|---|
D | 1.2 | Length of the transition 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 the wave front, in pixels. |
EDGE | 60 | Width of the soft edge where clip A turns into clip B, in pixels. |
SHINE | 28 | Brightness of the light and dark bands on the ripples. |
FPS | 30 | Output 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.mp4Show 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")





