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 paper tear transition Length of the transition in seconds: 1.0 Where the tear runs across the frame (0-1); it leans 6% from top to bottom: 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 paper tear 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.0 | Length of the transition in seconds. |
TEAR_X | 0.5 | Where the tear runs across the frame (0-1); it leans 6% from top to bottom. |
RAGGED | 0.03 | How far the tear wanders either side of its line, as a share of the frame width. |
RIM | 0.018 | Average width of the white torn-paper rim on each half, as a share of the frame width. |
PAPER | (250, 248, 242) | Rim color. |
PULL | 0.85 | How far each half slides sideways by the end, as a share of the frame width. |
TWIST | 12 | Degrees each half turns by the end. |
SHADOW | 0.55 | Strength of the shadow the halves cast on clip B (0-1). |
SEED | 3 | Change it for a different tear. |
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 paper tear transition that starts 1 second before A ends, so the result is A's length plus B's length minus 1 second (11 seconds here). Split A's frames along a ragged, near-vertical tear: its line runs from 47% of the frame width at the top to 53% at the bottom and wanders up to 3% of the width either way, with big bumps and small fibrous ones (a fixed random seed, so it doesn't flicker). Each half gets an off-white (250, 248, 242) torn-paper rim along its torn edge, on average 1.8% of the frame width wide and varying by up to 70%, fading in over the first 0.17 seconds. Over the transition, with an ease-in (progress squared), the left half slides left and the right half slides right by up to 85% of the frame width, and each turns by up to 12 degrees about the middle of its own half (the left one counterclockwise, the right one clockwise), so the gap opens wider at the top. Clip B plays underneath from its first frame, darkened by a soft shadow of the halves: blurred by 2% of the frame width, shifted 2% down and 1% right, at 55% strength. Match B's size and frame rate (30fps) to A. Crossfade the audio over the same 1 second. Use Python with numpy and Pillow for the masks and compositing (draw anything with Pillow, not ffmpeg's drawtext) and ffmpeg to read and write the video, keep the audio and save it as output.mp4.Run it without Claude
This is the exact script we tested. Save it as paper-tear-transition.py and run:
python3 paper-tear-transition.py clipA.mp4 clipB.mp4 output.mp4Show the script
#!/usr/bin/env python3
"""Paper tear transition: clip A rips down a ragged line and its two halves pull apart and twist away, revealing clip B.
Usage: python3 paper-tear-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 subprocess
import sys
import numpy as np
from PIL import Image, ImageFilter
D = 1.0 # transition length in seconds
TEAR_X = 0.5 # where the tear runs, as a share of the frame width (it leans 6% across top to bottom)
RAGGED = 0.03 # how far the tear wanders either side of its line, as a share of the frame width
RIM = 0.018 # average width of the white torn-paper rim on each half, as a share of the frame width
PAPER = (250, 248, 242) # rim color (off-white)
PULL = 0.85 # how far each half travels sideways by the end, as a share of the frame width
TWIST = 12 # degrees each half turns by the end
SHADOW = 0.55 # strength of the shadow the halves cast on clip B (0-1)
SEED = 3 # change for a different tear
FPS = 30
def ragged(n, rng):
"""A wandering line of n values around 0 (-1 to 1): big bumps plus small fibrous ones."""
line = np.zeros(n)
for cells, amp in [(4, 0.5), (20, 0.3), (120, 0.2)]:
knots = rng.uniform(-1, 1, cells + 1)
line += amp * np.interp(np.linspace(0, cells, n), np.arange(cells + 1), knots)
return line
def halves():
"""Video and rim masks (0-1) for the left and right halves, split along a ragged near-vertical line."""
rng = np.random.default_rng(SEED)
y = np.arange(h)
tear = w * (TEAR_X + 0.06 * (y / h - 0.5) + RAGGED * ragged(h, rng))[:, None]
rim_left = w * RIM * (1 + 0.7 * ragged(h, rng))[:, None]
rim_right = w * RIM * (1 + 0.7 * ragged(h, rng))[:, None]
x = np.arange(w)[None, :]
left_edge, right_edge = np.clip(tear - x + 0.5, 0, 1), np.clip(x - tear + 0.5, 0, 1)
left_video = np.clip(tear - rim_left - x + 0.5, 0, 1)
right_video = np.clip(x - tear - rim_right + 0.5, 0, 1)
return (left_edge, left_video), (right_edge, right_video)
def ease_in(p):
"""Slow first, then faster, like paper giving way."""
return p * p
def piece(a, edge, video, p, side):
"""One half of frame a as RGBA, white rim on its torn edge, moved and turned for progress p."""
rim = (edge - video) * min(p * 6, 1) # the rim fades in over the first sixth so it doesn't pop
rgb = a * video[..., None] + np.array(PAPER, np.float32) * rim[..., None]
alpha = video + rim
rgba = np.dstack([rgb / np.maximum(alpha, 1e-6)[..., None], alpha * 255])
img = Image.fromarray(np.uint8(np.clip(rgba, 0, 255).round()))
move = ease_in(p)
pivot = (w * (0.25 if side < 0 else 0.75), h * 0.5)
return img.rotate(-side * TWIST * move, Image.BICUBIC, center=pivot, translate=(side * PULL * w * move, 0))
def tear(a, b, p):
"""Frame at progress p (0-1): both halves of A pulled apart over B, with soft shadows."""
pieces = [piece(a, edge, video, p, side) for (edge, video), side in zip(masks, (-1, 1))]
cover = np.maximum(*[np.asarray(pc.getchannel("A"), np.float32) / 255 for pc in pieces])
blur = Image.fromarray(np.uint8(cover * 255)).filter(ImageFilter.GaussianBlur(w * 0.02))
shade = np.roll(np.asarray(blur, np.float32) / 255, (round(w * 0.02), round(w * 0.01)), (0, 1))
img = Image.fromarray(np.uint8(b * (1 - SHADOW * shade[..., None])))
for pc in pieces:
img.paste(pc, (0, 0), pc)
return np.asarray(img)
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 a float 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 chunk := ff.stdout.read(w * h * 3):
yield np.frombuffer(chunk, np.uint8).reshape(h, w, 3).astype(np.float32)
a_path, b_path, out = sys.argv[1:4]
w, h, a_len = probe(a_path)
masks = halves()
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 = tear(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.astype(np.uint8).tobytes())
ff.stdin.close()
if ff.wait():
sys.exit("ffmpeg failed")





