How to add a corner pin with Claude

Half a second in, the video's four corners glide onto a tilted shape, leaving it as a card turned away from the camera on a cream backdrop, its right edge shorter than its left.

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 corner pin to input.mp4
    Second the corners start to move: 0.5
    Seconds they take to reach the pin: 0.8

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 corner pin 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 corners start to move.
MOVE0.8Seconds they take to reach the pin.
CORNERS(0.10, 0.12), (0.86, 0.20), (0.86, 0.80), (0.10, 0.90)Where the top-left, top-right, bottom-right and bottom-left corners land, as shares of the frame width and height.
BACKDROP(245, 238, 226)Backdrop color.
SHADOW0.40Shadow strength (0-1). 0 removes it.
SHADOW_BLUR, SHADOW_DROP0.03, 0.02Shadow softness and how far it falls below the card, as shares of the frame width.

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
Corner-pin input.mp4 (540x960). Play it full screen until 0.5 seconds, then over 0.8 seconds (ease-out cubic) move its four corners in straight lines to these points, as shares of the frame width and height: top-left (0.10, 0.12), top-right (0.86, 0.20), bottom-right (0.86, 0.80), bottom-left (0.10, 0.90), warping the video with a true perspective transform so it looks like a card turned away from the camera. Hold that pin to the end. Fill the space around the card with solid (245, 238, 226) cream and give the card a soft black shadow at 40% opacity, blurred by 3% of the frame width and shifted down by 2% of the frame width. Keep the card's edges smooth (no jagged or dark fringe). Use Python with Pillow and numpy for the warp, mask and shadow (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 corner-pin.py and run:

python3 corner-pin.py input.mp4 output.mp4
Show the script
#!/usr/bin/env python3
"""Corner pin: the video's four corners glide onto a tilted shape, so it looks like a card turned away in 3D.

Usage: python3 corner-pin.py input.mp4 output.mp4
Needs ffmpeg, Pillow and numpy (pip install pillow numpy).
"""
import json
import subprocess
import sys
from functools import lru_cache

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

START = 0.5                    # second the corners start to move
MOVE = 0.8                     # seconds they take to reach the pin
CORNERS = [(0.10, 0.12),       # where the top-left corner lands, as shares of the frame width and height
           (0.86, 0.20),       # top-right
           (0.86, 0.80),       # bottom-right
           (0.10, 0.90)]       # bottom-left
BACKDROP = (245, 238, 226)     # backdrop color (warm cream)
SHADOW = 0.40                  # shadow strength (0-1)
SHADOW_BLUR = 0.03             # shadow softness as a share of the frame width
SHADOW_DROP = 0.02             # how far the shadow falls below the card, as a share of the frame width


def ease_out_cubic(p):
    p = min(max(p, 0), 1)
    return 1 - (1 - p) ** 3


def perspective_coeffs(dst, src):
    """The 8 numbers Pillow needs to map each output point in the dst quad back to the src quad."""
    rows, rhs = [], []
    for (x, y), (u, v) in zip(dst, src):
        rows += [[x, y, 1, 0, 0, 0, -u * x, -u * y], [0, 0, 0, x, y, 1, -v * x, -v * y]]
        rhs += [u, v]
    return np.linalg.solve(np.array(rows, float), np.array(rhs, float)).tolist()


@lru_cache(maxsize=1)
def layers(p):
    """For progress p: the corner positions, the card's smooth-edged mask and the backdrop with its shadow."""
    full = [(0, 0), (w, 0), (w, h), (0, h)]
    quad = [(fx + (cx * w - fx) * p, fy + (cy * h - fy) * p) for (fx, fy), (cx, cy) in zip(full, CORNERS)]
    big = Image.new("L", (w * 4, h * 4))
    ImageDraw.Draw(big).polygon([(x * 4, y * 4) for x, y in quad], fill=255)
    mask = big.resize((w, h), Image.LANCZOS).filter(ImageFilter.MinFilter(3))  # 1px in, clear of the warp's dark fill

    shadow = Image.new("L", (w, h))
    shadow.paste(round(255 * SHADOW * p), (0, round(w * SHADOW_DROP * p)), mask)
    shadow = shadow.filter(ImageFilter.GaussianBlur(w * SHADOW_BLUR))
    backdrop = Image.new("RGB", (w, h), BACKDROP)
    backdrop.paste((0, 0, 0), (0, 0), shadow)
    return perspective_coeffs(quad, full), mask, backdrop


def pin_frame(video, t):
    """The frame at time t: the video warped onto the corners, over the backdrop and shadow."""
    p = ease_out_cubic((t - START) / MOVE)
    if p == 0:
        return video
    coeffs, mask, backdrop = layers(p)
    warped = video.transform((w, h), Image.PERSPECTIVE, coeffs, Image.BICUBIC)
    img = backdrop.copy()
    img.paste(warped, (0, 0), mask)
    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", "-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)
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(pin_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