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
Add raindrops on glass to input.mp4 Second the first drop lands: 0.5 Seconds over which the drops land, one after another: 1.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.
Add raindrops on glass to @clip1.mp4, put a sale badge on @clip2.mp4, give @clip3.mp4 a VHS look, then join all three with crossfades.
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 |
|---|---|---|
START | 0.5 | Second the first drop lands. |
LANDING | 1.0 | Seconds over which the drops land, one after another. |
COUNT | 220 | Drops sitting on the glass. |
RADIUS | (3, 15) | Radius of the smallest and biggest drops, in pixels. |
TRICKLE | 6 | Big drops that slide down the glass. |
TRICKLE_START | 1.6 | Second the first one starts to slide; the rest follow every 0.4 seconds. |
SPEED | (90, 160) | Slide speed of the slowest and fastest trickling drop, in pixels per second. |
LENS | 2.2 | How much of the scene each drop shows, flipped. Higher is a wider view. |
SEED | 3 | Change for a different arrangement of drops. |
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
Add raindrops on glass to input.mp4, as if the camera were behind a wet window. Leave the video unchanged before 0.5 seconds. From 0.5 to 1.5 seconds, 220 drops land one after another at random places, each spreading to full size over 0.08 seconds and then staying put. Their radii run from 3 to 15 pixels, mostly small, and each is 1.1 times taller than wide with a slightly lumpy outline. Draw every drop as a lens rather than a flat shape: inside it, show the picture around it flipped upside down and left to right, taken from an area 2.2 times the drop's size. Darken the upper rim of each drop to 46% at its edge, brighten its lower rim by up to 70 levels, and put a pure white (255, 255, 255) glint in its top-left, centered 35% of the radius left and 42% up from the middle with a radius of 20% of the drop's. Also place 6 big drops (radius 13 to 15 pixels), spread evenly across the width, starting between 5% and 35% down the frame. At 1.6 seconds the first starts sliding down at 90 to 160 pixels per second, with the rest following every 0.4 seconds; each wobbles 4 pixels side to side, stretches up to 1.4 times taller than wide, and leaves a line of small beads behind it, one every 14 pixels and 22 to 42% of its radius. Don't dim or tint the rest of the picture. Use Python with numpy to draw the drops and ffmpeg to read and write the video (don't rely on ffmpeg's drawtext filter), keep the audio, and save it as output.mp4.Run it without Claude
This is the exact script we tested. Save it as raindrops-on-glass.py and run:
python3 raindrops-on-glass.py input.mp4 output.mp4Show the script
#!/usr/bin/env python3
"""Raindrops on glass: water drops land on an invisible pane in front of the picture, each one a tiny upside-down lens, and a few trickle down.
Usage: python3 raindrops-on-glass.py input.mp4 output.mp4
Needs ffmpeg and numpy (pip install numpy).
"""
import json
import subprocess
import sys
import numpy as np
START = 0.5 # second the first drop lands
LANDING = 1.0 # seconds over which the drops land, one after another
COUNT = 220 # drops sitting on the glass
RADIUS = (3, 15) # radius of the smallest and biggest drops, in pixels
TRICKLE = 6 # big drops that slide down the glass
TRICKLE_START = 1.6 # second the first one starts to slide (the rest follow every 0.4 seconds)
SPEED = (90, 160) # slide speed of the slowest and fastest trickling drop, in pixels per second
LENS = 2.2 # how much of the scene each drop shows, flipped (higher = wider view)
SEED = 3 # change for a different arrangement
def probe(path):
info = json.loads(subprocess.check_output(["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries",
"stream=width,height,r_frame_rate", "-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)
def drops_at(t):
"""(x, y, radius, height/width, lumpiness) of every drop on the glass at time t."""
out = []
for x, y, r, lands, lump in sitting:
grow = (t - lands) / 0.08 # a drop spreads to full size in 0.08 seconds as it lands
if grow > 0:
out.append((x, y, r * min(grow, 1), 1.1, lump))
for i, (x, y, r, speed) in enumerate(sliders):
slide = max(t - (TRICKLE_START + 0.4 * i), 0) # they sit from START like the rest, then let go
y_now = y + speed * slide
out.append((x + 4 * np.sin(y_now / 31), y_now, r, 1.1 + min(slide, 0.3), 0)) # stretches as it runs
# it leaves a line of small beads behind it, one every 14 pixels
for k, bead_y in enumerate(np.arange(y + 14, y_now - r * 1.4, 14)):
out.append((x + 4 * np.sin(bead_y / 31), bead_y, r * (0.22 + 0.1 * (k % 3)), 1.2, 0))
return out
def glass(img, t):
"""Draw every drop as a little lens: a flipped, wider view of what's around it, a shadowed top edge,
a bright bottom edge where the light gathers, and a white glint."""
src = img.astype(np.float32)
res = src.copy()
for cx, cy, r, tall, lump in drops_at(t):
x0, x1 = int(max(cx - r * 1.2 - 1, 0)), int(min(cx + r * 1.2 + 2, w))
y0, y1 = int(max(cy - r * tall * 1.2 - 1, 0)), int(min(cy + r * tall * 1.2 + 2, h))
if x0 >= x1 or y0 >= y1 or r < 1:
continue
yy, xx = np.mgrid[y0:y1, x0:x1].astype(np.float32)
dx, dy = (xx - cx) / r, (yy - cy) / (r * tall)
angle = np.arctan2(dy, dx)
d = np.hypot(dx, dy) / (1 + 0.12 * lump * np.sin(3 * angle + 7 * lump)) # real drops aren't perfect circles
inside = np.clip((1 - d) * r, 0, 1)[..., None] # 1 inside, a one-pixel soft edge
sx = np.clip(cx - dx * r * LENS, 0, w - 1).astype(int) # flipped: left shows right, top shows bottom
sy = np.clip(cy - dy * r * tall * LENS, 0, h - 1).astype(int)
rim = d[..., None] ** 4
lens = src[sy, sx] * (1.06 - 0.6 * rim * (dy[..., None] < 0.2)) # shadow round the top of the rim
lens += 70 * rim * np.clip(dy[..., None] * 2, 0, 1) # light gathers at the bottom edge
glint = np.clip(1 - np.hypot(dx + 0.35, dy + 0.42) / 0.2, 0, 1)[..., None] ** 0.5 # top-left glint
lens = lens * (1 - glint) + 255 * glint
res[y0:y1, x0:x1] = res[y0:y1, x0:x1] * (1 - inside) + lens * inside
return res.clip(0, 255).astype(np.uint8)
src, out = sys.argv[1:3]
w, h, rate, fps = probe(src)
rng = np.random.default_rng(SEED)
size = rng.uniform(size=COUNT) ** 2 # mostly small drops, a few big ones
sitting = [(rng.uniform(0, w), rng.uniform(0, h), RADIUS[0] + (RADIUS[1] - RADIUS[0]) * s,
START + rng.uniform(0, LANDING), rng.uniform()) for s in size]
sliders = [(w * (i + 0.5) / TRICKLE + rng.uniform(-20, 20), rng.uniform(0.05, 0.35) * h,
RADIUS[1] * rng.uniform(0.85, 1.0), rng.uniform(*SPEED)) for i in range(TRICKLE)]
reader = subprocess.Popen(["ffmpeg", "-v", "error", "-i", src, "-f", "rawvideo", "-pix_fmt", "rgb24", "-"],
stdout=subprocess.PIPE)
writer = subprocess.Popen(["ffmpeg", "-y", "-v", "error", "-f", "rawvideo", "-pix_fmt", "rgb24", "-s", f"{w}x{h}",
"-r", rate, "-i", "-", "-i", src, "-map", "0:v", "-map", "1:a?",
"-c:v", "libx264", "-crf", "16", "-pix_fmt", "yuv420p", "-c:a", "copy", "-shortest", out],
stdin=subprocess.PIPE)
i = 0
while raw := reader.stdout.read(w * h * 3):
img = np.frombuffer(raw, np.uint8).reshape(h, w, 3)
t = i / fps
writer.stdin.write((glass(img, t) if t >= START else img).tobytes())
i += 1
writer.stdin.close()
if reader.wait() or writer.wait():
sys.exit("ffmpeg failed")





