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 camera shutter transition Seconds the blades take to shut over clip A: 0.35 Seconds the shutter stays closed between the clips: 0.1
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 camera shutter 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 |
|---|---|---|
CLOSE | 0.35 | Seconds the blades take to shut over clip A. |
SHUT | 0.1 | Seconds the shutter stays closed between the clips. |
OPEN | 0.35 | Seconds the blades take to open onto clip B. |
BLADES | 6 | Number of blades; the opening is a polygon with this many sides. |
TWIST | 60 | Degrees the blades turn between fully open and shut. |
BLADE_COLORS | (52, 53, 58), (40, 41, 46) | Alternating blade shades. |
SEAM | (88, 90, 98) | Color of the lines where the blades overlap. |
RIM | (150, 152, 160) | Color of the edge round the opening. |
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 0.8 second camera shutter (aperture) transition starting 0.8 seconds before A ends (so the result is 11.2 seconds for two 6 second clips). Build the transition frames with Python, numpy and Pillow. The picture shows through a regular hexagon centered in the frame whose corners sit at radius r = o * R from the center, where R = (half the frame diagonal + 20) / cos(30 degrees) so the fully open hexagon clears the corners, and whose corners are at angles turn + i*60 degrees with turn = 60 * (1 - o) degrees, so the blades twist as they close. Everything outside the hexagon is covered by six opaque blades: blade i fills the area between hexagon edge i and the two neighboring edges' lines carried outward past their corners (a pinwheel), shaded alternately RGB (52, 53, 58) and (40, 41, 46), with a 2 pixel (88, 90, 98) line along each of those outward lines and a 2 pixel (150, 152, 160) outline round the hexagon. Draw the blades at twice the frame size and scale them down for smooth edges. Over the first 0.35 seconds the blades close over clip A (o = 1 - s(p), p from 0 to 1, with smoothstep s(p) = p*p*(3-2p)); for the next 0.1 seconds the shutter stays fully closed; over the last 0.35 seconds they open onto clip B (o = s(p)), with B playing from its first frame at the start of the transition. Don't use ffmpeg's drawtext. Use ffmpeg for the rest: match B's size and frame rate to A (30fps), crossfade the audio over the same 0.8 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 camera-shutter-transition.py and run:
python3 camera-shutter-transition.py clipA.mp4 clipB.mp4 output.mp4Show the script
#!/usr/bin/env python3
"""Camera shutter: six gray aperture blades twist shut over clip A, then twist open onto clip B.
Usage: python3 camera-shutter-transition.py clipA.mp4 clipB.mp4 output.mp4 (both clips need an audio track)
Needs ffmpeg, Pillow and numpy (pip install pillow numpy).
"""
import json
import math
import subprocess
import sys
import numpy as np
from PIL import Image, ImageDraw
CLOSE = 0.35 # seconds the blades take to shut over clip A
SHUT = 0.1 # seconds the shutter stays closed
OPEN = 0.35 # seconds the blades take to open onto clip B
BLADES = 6 # number of blades (the opening is a polygon with this many sides)
TWIST = 60 # degrees the blades turn between fully open and shut
BLADE_COLORS = [(52, 53, 58), (40, 41, 46)] # alternating blade shades
SEAM = (88, 90, 98) # lines where the blades overlap
RIM = (150, 152, 160) # bright edge round the opening
FPS = 30
a_path, b_path, out_path = sys.argv[1:4]
info = json.loads(subprocess.run(
["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries", "stream=width,height:format=duration",
"-of", "json", a_path], capture_output=True, check=True).stdout)
w, h = info["streams"][0]["width"], info["streams"][0]["height"]
D = CLOSE + SHUT + OPEN
offset = float(info["format"]["duration"]) - D
n = round(D * FPS)
# fully open, the polygon's inner circle clears the frame's corners
widest = (math.hypot(w, h) / 2 + 20) / math.cos(math.pi / BLADES)
def frames(path, start):
"""The n frames from `start`, fitted to A's size, as RGB arrays."""
raw = subprocess.run(
["ffmpeg", "-v", "error", "-ss", str(start), "-i", path, "-frames:v", str(n),
"-vf", f"scale={w}:{h}:force_original_aspect_ratio=increase,crop={w}:{h},fps={FPS}",
"-f", "rawvideo", "-pix_fmt", "rgb24", "-"], capture_output=True, check=True).stdout
return np.frombuffer(raw, np.uint8).reshape(-1, h, w, 3)
def smoothstep(p):
return p * p * (3 - 2 * p)
def openness(t):
"""1 = shutter fully open, 0 = shut."""
if t < CLOSE:
return 1 - smoothstep(t / CLOSE)
if t < CLOSE + SHUT:
return 0.0
return smoothstep((t - CLOSE - SHUT) / OPEN)
def blades(o):
"""The blades as an RGBA layer, drawn at twice the size and scaled down for smooth edges."""
s = 2
img = Image.new("RGBA", (w * s, h * s))
draw = ImageDraw.Draw(img)
cx, cy, r = w * s / 2, h * s / 2, o * widest * s
turn = math.radians(TWIST * (1 - o))
corners = [(cx + r * math.cos(turn + 2 * math.pi * i / BLADES), cy + r * math.sin(turn + 2 * math.pi * i / BLADES))
for i in range(BLADES)]
far = 4 * max(w, h) * s
# each blade fills the space between its edge of the opening and the two neighboring edges carried outward
seams = []
for i in range(BLADES):
(x0, y0), (x1, y1) = corners[i], corners[(i + 1) % BLADES]
ang = turn + 2 * math.pi * (i + 0.5) / BLADES + math.pi / 2 # direction of this edge
prev = ang - 2 * math.pi / BLADES # direction of the edge before
tip = (x1 + far * math.cos(ang), y1 + far * math.sin(ang))
tail = (x0 + far * math.cos(prev), y0 + far * math.sin(prev))
draw.polygon([(x0, y0), (x1, y1), tip, tail], fill=BLADE_COLORS[i % 2])
seams.append([(x1, y1), tip])
for seam in seams:
draw.line(seam, fill=SEAM, width=2 * s)
if r > 1:
draw.line(corners + corners[:1], fill=RIM, width=2 * s, joint="curve")
return img.resize((w, h), Image.LANCZOS)
def shutter_frame(picture, o):
layer = np.asarray(blades(o), np.float32)
alpha = layer[:, :, 3:] / 255
return (picture * (1 - alpha) + layer[:, :, :3] * alpha).astype(np.uint8).tobytes()
tail, head = frames(a_path, offset), frames(b_path, 0)
middle = b"".join(shutter_frame(tail[i] if i / FPS < CLOSE + SHUT else head[i], openness(i / FPS)) for i in range(n))
# A up to the shutter, then the shutter frames from stdin, then the rest of B; the sound crossfades underneath
subprocess.run(
["ffmpeg", "-y", "-v", "error", "-i", a_path, "-i", b_path,
"-f", "rawvideo", "-pix_fmt", "rgb24", "-s", f"{w}x{h}", "-r", str(FPS), "-i", "-",
"-filter_complex",
f"[0:v]fps={FPS},trim=end_frame={round(offset * FPS)},setpts=PTS-STARTPTS,format=yuv420p,setsar=1[a];"
f"[2:v]format=yuv420p,setsar=1[m];"
f"[1:v]scale={w}:{h}:force_original_aspect_ratio=increase,crop={w}:{h},fps={FPS},"
f"trim=start_frame={n},setpts=PTS-STARTPTS,format=yuv420p,setsar=1[b];"
f"[a][m][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_path],
input=middle, check=True)





