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 doodle transition Transition length in seconds: 1.2 Share of the transition spent scribbling before the strokes thicken to fill the frame: 0.6
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 doodle 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 | Transition length in seconds. |
DRAW | 0.6 | Share of the transition spent scribbling before the strokes thicken to fill the frame. |
PASSES | 6 | Zigzag strokes from the top of the frame to the bottom. |
STROKE | 0.09 | Stroke width while scribbling, as a share of the frame height. |
RIM | 0.014 | Width of the marker edge, as a share of the frame height. |
JITTER | 0.03 | How far the hand wobbles off a straight line. |
MARKER | (255, 214, 0) | Marker color. |
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 doodle transition starting 1.2 seconds before A ends, so the output is A's length plus B's minus 1.2 seconds. A single scribbled line is drawn over A as if by hand: a zigzag through 7 points that alternate between 10% left of the frame and 10% right of it, at heights of 1/12, 3/12, 5/12 ... 13/12 of the frame height, each pass sagging down 35% of a pass spacing at its middle and wobbling up and down by up to 3% of the frame height. Over the first 0.72 seconds the line is drawn from its start to its end at a steady speed, 9% of the frame height thick with round ends. Inside the line show clip B (playing from its start), and around it draw a yellow (255, 214, 0) marker edge 1.4% of the frame height wide on each side. Over the last 0.48 seconds the line thickens (easing in) until it covers the whole frame, then B plays on alone. Match B's size and frame rate to A and crossfade the audio over the same 1.2 seconds. Use Python with Pillow to draw the line and numpy and ffmpeg to composite (don't rely on ffmpeg's drawtext filter), and save it as output.mp4.Run it without Claude
This is the exact script we tested. Save it as doodle-transition.py and run:
python3 doodle-transition.py clipA.mp4 clipB.mp4 output.mp4Show the script
#!/usr/bin/env python3
"""Doodle transition: a yellow marker scribbles back and forth down clip A, clip B shows inside the strokes, then they thicken until B fills the frame.
Usage: python3 doodle-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, ImageDraw, ImageFilter
D = 1.2 # transition length in seconds
DRAW = 0.6 # share of D spent scribbling; the strokes then thicken to fill the frame
PASSES = 6 # zigzag strokes from the top of the frame to the bottom
STROKE = 0.09 # stroke width while scribbling, as a share of the frame height
RIM = 0.014 # width of the marker edge around each stroke, as a share of the frame height
JITTER = 0.03 # how far the hand wobbles off a straight line, as a share of the frame height
MARKER = (255, 214, 0) # marker color, a bright yellow
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 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 True:
chunk = ff.stdout.read(w * h * 3)
if not chunk:
break
yield np.frombuffer(chunk, np.uint8).reshape(h, w, 3).astype(np.float32)
def scribble():
"""Points of one zigzag line, left and right across the frame and a little further down each pass, wobbling like a hand."""
rng = np.random.default_rng(7)
corners = [(-0.1 * w if i % 2 == 0 else 1.1 * w, (i + 0.5) * h / PASSES) for i in range(PASSES + 1)]
points = []
for (x0, y0), (x1, y1) in zip(corners, corners[1:]):
phase, waves = rng.uniform(0, 2 * np.pi), rng.uniform(1.5, 2.5)
for s in np.linspace(0, 1, 40, endpoint=False):
bow = np.sin(s * np.pi) * h / PASSES * 0.35 # each pass sags a little, like a quick arm swing
wobble = JITTER * h * np.sin(s * waves * 2 * np.pi + phase) * np.sin(s * np.pi)
points.append((x0 + (x1 - x0) * s, y0 + (y1 - y0) * s + bow + wobble))
points.append(corners[-1])
return points
def drawn_part(length):
"""The scribble's points up to `length` pixels along it, so the line appears as if drawn."""
part, done = [line[0]], 0.0
for a, b in zip(line, line[1:]):
seg = np.hypot(b[0] - a[0], b[1] - a[1])
if done + seg >= length:
k = (length - done) / seg
part.append((a[0] + (b[0] - a[0]) * k, a[1] + (b[1] - a[1]) * k))
break
part.append(b)
done += seg
return part
def stroke_mask(points, width):
"""The line as a soft-edged 0-1 mask, drawn at twice the size and shrunk for smooth edges."""
img = Image.new("L", (w * 2, h * 2))
draw = ImageDraw.Draw(img)
big = [(x * 2, y * 2) for x, y in points]
draw.line(big, fill=255, width=round(width * 2), joint="curve")
for x, y in (big[0], big[-1]):
draw.ellipse([x - width, y - width, x + width, y + width], fill=255) # round pen tips
img = img.resize((w, h), Image.LANCZOS).filter(ImageFilter.GaussianBlur(0.6))
return np.asarray(img, np.float32)[..., None] / 255
def doodle(a, b, p):
"""Frame at progress p (0-1): the scribble drawn so far, B inside it and a yellow marker edge round it."""
drawn = min(p / DRAW, 1)
fill = max(p - DRAW, 0) / (1 - DRAW)
width = STROKE * h + fill * fill * 2.2 * h / PASSES # the strokes swell until they cover the gaps between them
points = drawn_part(drawn * total)
edge = stroke_mask(points, width + 2 * RIM * h)
inside = stroke_mask(points, width)
return a * (1 - edge) + np.array(MARKER, np.float32) * (edge - inside) + b * inside
a_path, b_path, out = sys.argv[1:4]
w, h, a_len = probe(a_path)
line = scribble()
total = sum(np.hypot(b[0] - a[0], b[1] - a[1]) for a, b in zip(line, line[1:]))
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:
p = (i - start + 1) / steps
a = next(b_frames) if p >= 1 else doodle(a, next(b_frames), p)
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")





