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 portal transition Length of the transition in seconds: 1.2 Where in clip A the portal opens, as 0-1 across and down; OpenCV tracks it from there: (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 portal 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. |
ANCHOR | (0.5, 0.5) | Where in clip A the portal opens, as 0-1 across and down; OpenCV tracks it from there. |
OPEN_SIZE | 0.22 | Radius the ring opens to before the push, as a share of the frame width. |
OPEN_TIME | 0.35 | Share of the transition spent opening the ring. |
PUSH | 1.8 | How far the camera pushes into clip A by the end. |
B_START_ZOOM | 1.5 | How close clip B starts inside the portal before settling to full frame. |
CORE, GLOW | (255, 238, 200), (255, 120, 20) | Colors of the ring's hot core and the glow around it. |
SPIN | 10 | Number of bright streaks spinning around the ring. |
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 portal transition over the last 1.2 seconds of A (output 10.8 seconds long). Fit B to A's size and frame rate. At A's 4.8 second mark a glowing ring opens on the center of the frame and is pinned to the scene with OpenCV optical flow (track corners within 20% of the frame width of the point, move it by their median flow). Over the first 35% of the transition the ring grows to a radius of 22% of the frame width; inside the ring show B, scaled 1.5x around the ring's center. Over the remaining 65%, the ring's center eases to the frame center, its radius grows (squared ease) to 1.15x the distance to the farthest corner, A zooms in around the ring's center up to 1.8x, and B eases from 1.5x to 1.0x so it fills the frame exactly when the transition ends. Draw the ring with numpy, painted over the picture (blend toward the color, don't add): with a ring width of 0.8% of the frame width plus 3% of the radius, a soft orange (255, 120, 20) glow with a Gaussian falloff of 3x the ring width either side of the circle's edge, then a hot core (255, 238, 200) with a falloff of 0.6x the ring width, both brightened and dimmed by 0.7 + 0.3 * cos(10 * angle - 14 * seconds) so 10 bright streaks spin around it. Fade the ring in over the first 8% and out over the last 25% of the push. Crossfade the audio over the same 1.2 seconds. Use Python with OpenCV and numpy and ffmpeg to encode, keep the audio, and save it as output.mp4.Run it without Claude
This is the exact script we tested. Save it as portal-transition.py and run:
python3 portal-transition.py clipA.mp4 clipB.mp4 output.mp4Show the script
#!/usr/bin/env python3
"""Portal transition: a glowing orange ring opens on the subject of clip A and the camera pushes through it into clip B.
Usage: python3 portal-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). OpenCV tracks the spot the
portal opens on, so the ring stays pinned to the scene while clip A's camera moves.
"""
import json
import subprocess
import sys
import cv2
import numpy as np
D = 1.2 # transition length in seconds (the last D seconds of A overlap the first of B)
ANCHOR = (0.5, 0.5) # where the portal opens in A's first transition frame (0-1 across and down)
OPEN_SIZE = 0.22 # radius the ring opens to before the push, as a share of the frame width
OPEN_TIME = 0.35 # share of the transition spent opening the ring
PUSH = 1.8 # how far the camera pushes into A (1.8 = 80% closer by the end)
B_START_ZOOM = 1.5 # B starts this much closer inside the portal and settles to 1.0
CORE = (255, 238, 200) # color of the ring's hot core
GLOW = (255, 120, 20) # color of the glow around it
SPIN = 10 # bright streaks around the ring, which spin as it grows
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):
"""Yield the clip's frames as RGB arrays, fitted to A's size and frame rate."""
ff = subprocess.Popen(["ffmpeg", "-v", "error", "-i", path, "-vf",
f"scale={w}:{h}:force_original_aspect_ratio=increase,crop={w}:{h},fps={rate}",
"-f", "rawvideo", "-pix_fmt", "rgb24", "-"], stdout=subprocess.PIPE)
while buf := ff.stdout.read(w * h * 3):
yield np.frombuffer(buf, np.uint8).reshape(h, w, 3)
ff.wait()
def smooth(p):
p = min(max(p, 0.0), 1.0)
return p * p * (3 - 2 * p)
def track(prev, cur, point):
"""Move point by the median optical flow of the corners around it in clip A."""
g0, g1 = cv2.cvtColor(prev, cv2.COLOR_RGB2GRAY), cv2.cvtColor(cur, cv2.COLOR_RGB2GRAY)
box = np.zeros_like(g0)
cv2.circle(box, (round(point[0]), round(point[1])), round(w * 0.2), 255, -1)
pts = cv2.goodFeaturesToTrack(g0, 60, 0.01, 6, mask=box)
if pts is None:
return point
new, ok, _ = cv2.calcOpticalFlowPyrLK(g0, g1, pts, None, winSize=(21, 21), maxLevel=3)
moved = (new - pts)[ok.ravel() == 1]
return point + np.median(moved.reshape(-1, 2), axis=0) if len(moved) else point
def portal(a, b, p, anchor, t):
"""One transition frame, p going 0 -> 1: the ring opens on the anchor, then the camera pushes through it."""
push = max(p - OPEN_TIME, 0) / (1 - OPEN_TIME)
e = smooth(push)
cx, cy = anchor * (1 - e) + np.array([w / 2, h / 2]) * e # the portal drifts to the frame center
far = max(np.hypot(x, y) for x in (cx, w - cx) for y in (cy, h - cy))
r = w * OPEN_SIZE * smooth(p / OPEN_TIME) + (far * 1.15 - w * OPEN_SIZE) * push ** 2
s = 1 + (PUSH - 1) * push ** 2 # push into A around the portal
a = cv2.warpAffine(a, np.float32([[s, 0, cx - s * cx], [0, s, cy - s * cy]]), (w, h), borderMode=cv2.BORDER_REFLECT)
sb = B_START_ZOOM + (1 - B_START_ZOOM) * e # B, centered in the portal, settles to full frame
b = cv2.warpAffine(b, np.float32([[sb, 0, cx - sb * w / 2], [0, sb, cy - sb * h / 2]]), (w, h), borderMode=cv2.BORDER_REFLECT)
dist = np.hypot(xs - cx, ys - cy)
inside = np.clip(r - dist + 0.5, 0, 1)[..., None]
width = w * 0.008 + r * 0.03
spin = 0.7 + 0.3 * np.cos(SPIN * np.arctan2(ys - cy, xs - cx) - 14 * t)
fade = smooth(p / 0.08) * (1 - smooth((push - 0.75) / 0.25)) # ring fades in, and out as it leaves the frame
glow = (np.exp(-((dist - r) / (width * 3)) ** 2) * (0.55 + 0.45 * spin) * fade)[..., None]
core = (np.exp(-((dist - r) / (width * 0.6)) ** 2) * spin * fade)[..., None]
out = a * (1 - inside) + b * inside
out = out * (1 - glow) + np.float32(GLOW) * glow # paint the glow, then the core over it
out = out * (1 - core) + np.float32(CORE) * core
return np.clip(out, 0, 255).astype(np.uint8)
def render(a_path, b_path, out):
ff = subprocess.Popen(["ffmpeg", "-y", "-v", "error", "-f", "rawvideo", "-pix_fmt", "rgb24", "-s", f"{w}x{h}",
"-r", rate, "-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)
first = round((duration - D) * fps)
clip_b = frames(b_path)
anchor, prev = np.array([w * ANCHOR[0], h * ANCHOR[1]]), None
for i, a in enumerate(frames(a_path)):
if i >= first:
if prev is not None:
anchor = track(prev, a, anchor)
prev = a
p = (i - first) / round(D * fps)
a = portal(a.astype(np.float32), next(clip_b).astype(np.float32), p, anchor, i / fps)
ff.stdin.write(a.tobytes())
for b in clip_b:
ff.stdin.write(b.tobytes())
ff.stdin.close()
if ff.wait():
sys.exit("ffmpeg failed")
a_path, b_path, out = sys.argv[1:4]
w, h, rate, fps, duration = probe(a_path)
ys, xs = np.mgrid[0:h, 0:w].astype(np.float32)
render(a_path, b_path, out)





