How to add a torn paper edge with Claude

Half a second in, all four edges of the video rip inward into a jagged white paper rim, leaving the shot like a torn scrap laid on brown kraft paper.

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 torn paper edge to input.mp4
    Second the edges start to tear: 0.5
    Seconds the tear takes to reach full depth: 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.

Add a torn paper edge 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.5Second the edges start to tear.
TEAR0.5Seconds the tear takes to reach full depth.
BORDER0.06Average depth of the backdrop strip, as a share of the frame width.
RAGGED0.45How much that depth wanders along each edge (0 is a straight edge).
RIM0.025Average width of the white torn-paper rim, as a share of the frame width.
PAPER, BACKDROP(252, 250, 245), (196, 158, 108)Rim and backdrop colors.
SHADOW0.35Strength of the paper's shadow on the backdrop (0-1).
SEED7Change 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
Give input.mp4 a torn paper edge. Play it untouched until 0.5 seconds, then over 0.5 seconds (ease-out) tear all four edges inward and hold that to the end. Outside the tear is a kraft-brown (196, 158, 108) backdrop with faint grain. Its edge sits on average 6% of the frame width in from each side, wandering up to 45% either way along the edge with big bumps and small fibrous ones (a fixed random seed, so it doesn't flicker). Just inside it is a ragged off-white (252, 250, 245) paper rim, on average 2.5% of the frame width wide and varying by up to 60%, and inside the rim the video plays unscaled. The paper casts a soft shadow onto the backdrop: blurred by 1.2% of the frame width, shifted 1.2% down and 0.8% right, at 35% strength. Use Python with numpy and Pillow for the masks (draw anything with Pillow, not ffmpeg's drawtext) and ffmpeg to read and write the video, keep the audio and the original size, and save it as output.mp4.

Run it without Claude

This is the exact script we tested. Save it as torn-paper-edge.py and run:

python3 torn-paper-edge.py input.mp4 output.mp4
Show the script
#!/usr/bin/env python3
"""Torn paper edge: the video's edges tear away into a ragged white paper rim over a kraft-paper backdrop.

Usage: python3 torn-paper-edge.py input.mp4 output.mp4
Needs ffmpeg, Pillow and numpy (pip install pillow numpy).
"""
import json
import subprocess
import sys

import numpy as np
from PIL import Image, ImageFilter

START = 0.5                  # second the edges start to tear
TEAR = 0.5                   # seconds the tear takes to reach full depth
BORDER = 0.06                # average depth of the backdrop strip, as a share of the frame width
RAGGED = 0.45                # how much that depth wanders along each edge (0 = straight)
RIM = 0.025                  # average width of the white torn-paper rim, as a share of the frame width
PAPER = (252, 250, 245)      # rim color (off-white)
BACKDROP = (196, 158, 108)   # backdrop color (kraft brown)
SHADOW = 0.35                # strength of the shadow the paper casts on the backdrop (0-1)
SEED = 7                     # change for a different tear


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 [(5, 0.5), (22, 0.3), (110, 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 tear_lines():
    """For each side (left, right, top, bottom): how deep the backdrop and the end of the white rim reach."""
    rng = np.random.default_rng(SEED)
    sides = {}
    for side, n in [("left", h), ("right", h), ("top", w), ("bottom", w)]:
        paper = w * BORDER * (1 + RAGGED * ragged(n, rng))
        rim = w * RIM * (1 + 0.6 * ragged(n, rng))
        sides[side] = (paper, paper + rim)
    return sides


def coverage(depth):
    """0-1 mask of what lies inside the four edges, each `depth[side]` pixels in, with soft 1px edges."""
    x, y = np.arange(w)[None, :], np.arange(h)[:, None]
    return np.clip(np.minimum.reduce([x - depth["left"][:, None], (w - 1 - x) - depth["right"][:, None],
                                      y - depth["top"][None, :], (h - 1 - y) - depth["bottom"][None, :]]) + 0.5, 0, 1)


def layers(p):
    """Paper mask, video mask and shadowed backdrop with the tear at progress p (0-1)."""
    paper = coverage({s: p * lines[s][0] for s in lines})
    video = coverage({s: p * lines[s][1] for s in lines})
    blur = Image.fromarray(np.uint8(paper * 255)).filter(ImageFilter.GaussianBlur(w * 0.012))
    shade = np.roll(np.asarray(blur, np.float32) / 255, (round(w * 0.012), round(w * 0.008)), (0, 1))
    return paper[..., None], video[..., None], backdrop * (1 - SHADOW * shade[..., None])


def torn_frame(video, t):
    p = min(max((t - START) / TEAR, 0), 1)
    if p == 0:
        return video
    p = 1 - (1 - p) ** 3  # ease out
    paper, inside, bg = final if p == 1 else layers(p)
    img = bg * (1 - paper) + np.array(PAPER, np.float32) * paper
    img = img * (1 - inside) + np.asarray(video, np.float32) * inside
    return Image.fromarray(np.uint8(img.round()))


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)


src, out = sys.argv[1:3]
w, h, rate, fps = probe(src)
lines = tear_lines()
grain = np.asarray(Image.effect_noise((w, h), 40).filter(ImageFilter.GaussianBlur(1)), np.float32)
backdrop = np.array(BACKDROP, np.float32) + (grain[..., None] - 128) * 0.15
final = layers(1)
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 chunk := reader.stdout.read(w * h * 3):
    writer.stdin.write(torn_frame(Image.frombytes("RGB", (w, h), chunk), i / fps).tobytes())
    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