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 cube transition Length of the transition in seconds: 0.8 Camera distance from the cube's center in cube widths; smaller means stronger perspective: 2.0
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 cube 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 | 0.8 | Length of the transition in seconds. |
CAMERA | 2.0 | Camera distance from the cube's center in cube widths; smaller means stronger perspective. |
PULL | 0.15 | How far the cube pulls back mid-turn, as a share of its size. |
SHADE | 0.45 | How much a side darkens as it turns away from the camera. |
BACKGROUND | (14, 14, 18) | Color behind the cube. |
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 3D cube transition, overlapping the clips by 0.8 seconds (the output is 11.2 seconds). Over the last 0.8 seconds of A, put A's frames on the front side of a box as wide and deep as the frame (540 pixels) and as tall as it (960), and the first 0.8 seconds of B on its right side, then turn the box 90 degrees about its vertical axis so the right side comes to the front, with a cubic ease-in-out (slow start, fast middle, slow landing). Project it in perspective from a camera 2 frame widths from the box's center, with the focal length set so the front side exactly fills the frame before the turn, and shrink the projection about the frame center by 15% at the middle of the turn (scale = 1 - 0.15 * sin(pi * eased progress)). Darken each side as it turns away (brightness 1 - 0.45 * (1 - cos) of the angle between it and the camera) and fill the space around the box with dark gray (14, 14, 18). Warp the sides with OpenCV's warpPerspective using Python 3, OpenCV and numpy, and use ffmpeg for the rest. Before the transition the output is A untouched, after it B carries on from 0.8 seconds in. Crossfade the audio over the same 0.8 seconds. Match B's size and frame rate to A and save it as output.mp4 (H.264, yuv420p, 30fps).Run it without Claude
This is the exact script we tested. Save it as cube-transition.py and run:
python3 cube-transition.py clipA.mp4 clipB.mp4 output.mp4Show the script
#!/usr/bin/env python3
"""Cube transition: clip A and clip B sit on two sides of a 3D cube that turns left to bring B to the front.
Usage: python3 cube-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).
"""
import json
import math
import subprocess
import sys
import cv2
import numpy as np
D = 0.8 # transition length in seconds
CAMERA = 2.0 # camera distance from the cube's center, in cube widths (smaller = stronger perspective)
PULL = 0.15 # how far the cube pulls back mid-turn, as a share of its size
SHADE = 0.45 # how much a side darkens as it turns away from the camera
BACKGROUND = (14, 14, 18) # color behind the cube
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, start):
"""The transition's frames from path, starting at `start` seconds, fitted to A's size and frame rate."""
raw = subprocess.check_output(["ffmpeg", "-v", "error", "-ss", f"{start:.3f}", "-i", path, "-frames:v", str(n),
"-vf", f"scale={w}:{h}:force_original_aspect_ratio=increase,crop={w}:{h},fps={rate}",
"-f", "rawvideo", "-pix_fmt", "rgb24", "-"])
return np.frombuffer(raw, np.uint8)[:n * h * w * 3].reshape(n, h, w, 3)
def ease(p):
"""Slow start, fast middle, slow landing (cubic ease-in-out)."""
return 4 * p ** 3 if p < 0.5 else 1 - (-2 * p + 2) ** 3 / 2
def project(points, angle, scale):
"""Screen positions of 3D points on a cube of width w centered at the origin, turned `angle` radians about
its vertical axis, seen by a camera that shows the front side exactly filling the frame when angle is 0."""
cam = CAMERA * w
focal = (cam - w / 2) * scale
out = []
for x, y, z in points:
x, z = x * math.cos(angle) - z * math.sin(angle), x * math.sin(angle) + z * math.cos(angle)
out.append((w / 2 + focal * x / (cam - z), h / 2 + focal * y / (cam - z)))
return np.float32(out)
def paint(canvas, img, corners, light):
"""Warp img onto the four screen corners (top-left, top-right, bottom-right, bottom-left), shaded by light."""
src = np.float32([[0, 0], [w, 0], [w, h], [0, h]])
m = cv2.getPerspectiveTransform(src, corners)
face = cv2.warpPerspective(img, m, (w, h), flags=cv2.INTER_LINEAR).astype(np.float32) * light
mask = cv2.warpPerspective(np.ones((h, w), np.float32), m, (w, h), flags=cv2.INTER_LINEAR)[..., None]
canvas[:] = canvas * (1 - mask) + face * mask
def cube_frame(i):
"""Frame i of the transition: A on the cube's front, B on its right side, turned a quarter turn in all."""
p = ease(i / (n - 1))
angle = p * math.pi / 2
scale = 1 - PULL * math.sin(p * math.pi)
x, y, z = w / 2, h / 2, w / 2
front = project([(-x, -y, z), (x, -y, z), (x, y, z), (-x, y, z)], angle, scale)
right = project([(x, -y, z), (x, -y, -z), (x, y, -z), (x, y, z)], angle, scale)
canvas = np.empty((h, w, 3), np.float32)
canvas[:] = BACKGROUND
if math.cos(angle) > 1e-3:
paint(canvas, a_frames[i], front, 1 - SHADE * (1 - math.cos(angle)))
if math.sin(angle) > 1e-3:
paint(canvas, b_frames[i], right, 1 - SHADE * (1 - math.sin(angle)))
return canvas.clip(0, 255).astype(np.uint8)
a_path, b_path, out = sys.argv[1:4]
w, h, rate, fps, a_len = probe(a_path)
n = round(D * fps)
offset = a_len - D
a_frames, b_frames = frames(a_path, offset), frames(b_path, 0)
# A up to the transition, the cube frames, then B from where the transition ends; the sound crossfades over D
ff = subprocess.Popen(["ffmpeg", "-y", "-v", "error", "-i", a_path, "-i", b_path,
"-f", "rawvideo", "-pix_fmt", "rgb24", "-s", f"{w}x{h}", "-r", rate, "-i", "-",
"-filter_complex",
f"[0:v]trim=end={offset:.4f},setpts=PTS-STARTPTS,fps={rate},format=yuv420p,setsar=1[a];"
f"[1:v]scale={w}:{h}:force_original_aspect_ratio=increase,crop={w}:{h},fps={rate},"
f"trim=start={D},setpts=PTS-STARTPTS,format=yuv420p,setsar=1[b];"
f"[2:v]format=yuv420p,setsar=1[c];"
f"[a][c][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], stdin=subprocess.PIPE)
for i in range(n):
ff.stdin.write(cube_frame(i).tobytes())
ff.stdin.close()
if ff.wait():
sys.exit("ffmpeg failed")





