How to add a money rain with Claude

Green dollar bills pour down from the top edge and keep falling past the product to the end, spinning and flipping edge-on as they drop.

Get this effect

  1. Switch to Code in your Claude Desktop App

  2. 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/skills
  3. Ask for the effect

    Add a money rain to input.mp4
    Second the first notes drop in at the top edge: 0.3
    Banknotes in the shower: 40

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 a money rain 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.

SettingDefaultWhat it does
START0.3Second the first notes drop in at the top edge.
COUNT40Banknotes in the shower.
SIZE(0.20, 0.30)Note length range, as a share of the frame width.
SPEED(0.30, 0.46)Fall speed range, in frame heights per second.
SPIN90Fastest spin, in degrees per second.
SWAY0.04Side-to-side drift as a share of the frame width.
GREEN, PAPER, INK(70, 150, 80), (246, 248, 236), (25, 80, 40)Note face, border and dollar sign colors.

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 a money rain to input.mp4. Draw a banknote with Pillow: a 300x130 pixel rounded rectangle in off-white (246, 248, 236), a green (70, 150, 80) face inset 9 pixels, a thin dark green (25, 80, 40) line 15 pixels in, a pale green (150, 205, 140) oval 88x80 pixels in the middle with a dark green "$" in bold type, and "100" in off-white in each corner. Rain 40 of these notes down the frame starting at 0.3 seconds: each is scaled to 20% to 30% of the frame width long (bigger notes fall faster, 0.30 to 0.46 frame heights per second, and are drawn in front), placed at random from 5% to 95% across, and queued evenly from 8% to 138% of the frame height above the top edge so they enter one after another. Spin each note at up to 90 degrees per second either way from a random starting angle, squash its height with |cos| over a 1.2 to 2.4 second cycle (never below 12%) so it looks like it flips over, and sway it side to side by 4% of the frame width over 1.8 to 3.2 seconds. A note that falls 15% past the bottom comes back in at the top, so the shower keeps going to the end. Nothing shows before 0.3 seconds and the notes take until about 2 seconds to reach the bottom half. Use Python with numpy and Pillow to draw it and ffmpeg to put it on 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 money-rain.py and run:

python3 money-rain.py input.mp4 output.mp4
Show the script
#!/usr/bin/env python3
"""Money rain: green banknotes shower down from the top of the frame, spinning and flipping as they fall.

Usage: python3 money-rain.py input.mp4 output.mp4
Needs ffmpeg, numpy and Pillow (pip install numpy pillow).
"""
import json
import math
import subprocess
import sys
from pathlib import Path

import numpy as np
from PIL import Image, ImageDraw, ImageFont

START = 0.3                     # second the first notes drop in at the top edge
COUNT = 40                      # banknotes in the shower
SIZE = (0.20, 0.30)             # note length range, as a share of the frame width
SPEED = (0.30, 0.46)            # fall speed range, in frame heights per second (bigger notes fall faster)
SPIN = 90                       # fastest spin, in degrees per second either way
SWAY = 0.04                     # side-to-side drift as a share of the frame width
GREEN = (70, 150, 80)           # note color
PAPER = (246, 248, 236)         # border and numbers
INK = (25, 80, 40)              # dollar sign and inner line
SEED = 11                       # change for a different shower
FONTS = ["/System/Library/Fonts/Supplemental/Arial Bold.ttf",     # macOS
         "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",  # Linux
         "C:/Windows/Fonts/arialbd.ttf"]                          # Windows
FONT_FILE = next((f for f in FONTS if Path(f).exists()), None) or sys.exit("No bold font found: add one to FONTS")


def banknote(w=300, h=130):
    """A flat note: paper border, green face, a pale oval with a dollar sign and 100 in the corners."""
    img = Image.new("RGBA", (w, h))
    draw = ImageDraw.Draw(img)
    draw.rounded_rectangle([0, 0, w - 1, h - 1], radius=6, fill=PAPER)
    draw.rectangle([9, 9, w - 10, h - 10], fill=GREEN)
    draw.rectangle([15, 15, w - 16, h - 16], outline=INK, width=2)
    draw.ellipse([w / 2 - 44, h / 2 - 40, w / 2 + 44, h / 2 + 40], fill=(150, 205, 140), outline=INK, width=3)
    draw.text((w / 2, h / 2 + 2), "$", font=ImageFont.truetype(FONT_FILE, 64), anchor="mm", fill=INK)
    small = ImageFont.truetype(FONT_FILE, 24)
    for x, y, anchor in [(24, 22, "lt"), (w - 24, 22, "rt"), (24, h - 22, "lb"), (w - 24, h - 22, "rb")]:
        draw.text((x, y), "100", font=small, anchor=anchor, fill=PAPER)
    return img


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"])


src, out = sys.argv[1:3]
w, h, rate, fps, duration = probe(src)
note = banknote()
rng = np.random.default_rng(SEED)
notes = []
for k in range(COUNT):                                   # notes queue up evenly above the frame
    depth = rng.uniform(0, 1)                            # 0 = far (small, slow), 1 = near (big, fast)
    length = (SIZE[0] + depth * (SIZE[1] - SIZE[0])) * w
    notes.append({"sprite": note.resize((round(length), round(length * note.height / note.width)), Image.LANCZOS),
                  "x": rng.uniform(0.05, 0.95) * w, "y": -(0.08 + 1.3 * k / COUNT) * h,
                  "speed": (SPEED[0] + depth * (SPEED[1] - SPEED[0])) * h, "spin": rng.uniform(-SPIN, SPIN),
                  "angle": rng.uniform(0, 360), "flip": rng.uniform(1.2, 2.4), "sway": rng.uniform(1.8, 3.2),
                  "phase": rng.uniform(0, 2 * math.pi), "depth": depth})
notes.sort(key=lambda n: n["depth"])                     # draw far notes first so near ones cover them


def draw_notes(img, t):
    """Paste every note at its place at time t onto the frame."""
    age = t - START
    span = h * 1.3                                       # a note that falls off the bottom comes back in at the top
    for n in notes:
        y = n["y"] + n["speed"] * age
        if y > h * 1.15:
            y = (y + 0.15 * h) % span - 0.15 * h
        x = n["x"] + SWAY * w * math.sin(2 * math.pi * age / n["sway"] + n["phase"])
        turn = abs(math.cos(2 * math.pi * age / n["flip"] + n["phase"]))   # flipping over: squashed when edge-on
        sprite = n["sprite"].resize((n["sprite"].width, max(2, round(n["sprite"].height * max(turn, 0.12)))),
                                    Image.LANCZOS)
        sprite = sprite.rotate(n["angle"] + n["spin"] * age, expand=True, resample=Image.BICUBIC)
        img.paste(sprite, (round(x - sprite.width / 2), round(y - sprite.height / 2)), sprite)


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", "18", "-pix_fmt", "yuv420p", "-c:a", "copy", "-shortest", out],
                          stdin=subprocess.PIPE)
i = 0
while raw := reader.stdout.read(w * h * 3):
    t = i / fps
    if t >= START:
        img = Image.frombytes("RGB", (w, h), raw)
        draw_notes(img, t)
        raw = img.tobytes()
    writer.stdin.write(raw)
    i += 1
writer.stdin.close()
if reader.wait() or writer.wait():
    sys.exit("ffmpeg failed")

Find 3x winning ads

Recharm cuts your ad footage into tagged, searchable clips: hooks, testimonials, product shots. Find the shots that win, add the effect with Claude, and ship 3x more ad edits.

Start 14-day free trial