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 pinwheel wipe Length of the transition in seconds: 1.0 Number of blades on the pinwheel: 4
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 pinwheel wipe.
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.0 | Length of the transition in seconds. |
BLADES | 4 | Number of blades on the pinwheel. |
TWIST | 70 | How far each blade's edge bends between the center and the frame's corners, in degrees. |
LINE | 4 | Width of the white line along each blade's edge in pixels; 0 for none. |
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 second pinwheel wipe starting 1 second before A ends (output 11 seconds for two 6 second clips). Split the frame into 4 equal blades around its center, the first starting at twelve o'clock. Each blade's edge curves: its angle shifts clockwise by 70 degrees from the center out to the frame's corners. Over the second, every blade of B grows clockwise from its leading edge until it covers its whole quarter, eased in and out (smoothstep), so all four sweep at once. Draw a 4 pixel pure white (255,255,255) line along every blade edge while the wipe runs, and smooth the edges over about a pixel. Fit B to A's size at 30fps. Build the mask per frame with numpy and composite with Python, piping frames to ffmpeg. Crossfade the audio over the same 1 second and keep it. Save it as output.mp4 (H.264, yuv420p).Run it without Claude
This is the exact script we tested. Save it as pinwheel-wipe.py and run:
python3 pinwheel-wipe.py clipA.mp4 clipB.mp4 output.mp4Show the script
#!/usr/bin/env python3
"""Pinwheel wipe: four curved blades of clip B sweep clockwise around the center of clip A until B fills the frame.
Usage: python3 pinwheel-wipe.py clipA.mp4 clipB.mp4 output.mp4 (both clips need an audio track)
Needs ffmpeg and numpy (pip install numpy).
"""
import json
import math
import subprocess
import sys
import numpy as np
D = 1.0 # transition length in seconds
BLADES = 4 # blades on the pinwheel
TWIST = 70 # how far each blade's edge bends between the center and the frame's corners, in degrees
LINE = 4 # width of the white line along each blade's edge, in pixels (0 for none)
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 not chunk:
break
yield np.frombuffer(chunk, np.uint8).reshape(h, w, 3)
def blade_layout():
"""For every pixel: where it sits within its blade's slice (0-1, clockwise from the blade's leading edge,
bent by TWIST further out), and how many pixels one whole slice spans at its distance from the center."""
y, x = np.mgrid[0:h, 0:w].astype(np.float32)
dx, dy = x + 0.5 - w / 2, y + 0.5 - h / 2
r = np.hypot(dx, dy)
angle = np.arctan2(dx, -dy) - math.radians(TWIST) * r / r.max() # 0 at twelve o'clock, clockwise
slice_angle = 2 * math.pi / BLADES
return (angle / slice_angle) % 1, np.maximum(r, 1) * slice_angle
def pinwheel_wipe(a, b, p):
"""Mix of A and B at progress p (0-1): each blade of B covers the first p of its slice (easing in and out),
with a white line along its edges, all smoothed over about a pixel."""
eased = p * p * (3 - 2 * p)
inside = place < eased
edge = np.where(inside, np.minimum(place, eased - place), -np.minimum(place - eased, 1 - place)) * span
mask = np.clip(0.5 + edge, 0, 1)[..., None]
line = np.clip(LINE / 2 + 0.5 - np.abs(edge), 0, 1)[..., None]
mixed = a * (1 - mask) + b * mask
return (mixed * (1 - line) + 255 * line).astype(np.uint8)
a_path, b_path, out = sys.argv[1:4]
w, h, a_len = probe(a_path)
place, span = blade_layout()
start = round((a_len - D) * FPS) # first frame of the transition
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 = pinwheel_wipe(a, next(b_frames), (i - start + 1) / (round(D * FPS) + 1))
ff.stdin.write(a.tobytes())
for b in b_frames:
ff.stdin.write(b.tobytes())
ff.stdin.close()
if ff.wait():
sys.exit("ffmpeg failed")





