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 a 2.5D parallax photo to input.mp4 Top of the stand, 0-1 down the frame; the product and everything below this line move as the near layer: 0.75 Backdrop scale and slide (share of the frame width, negative is left) by the last frame: 1.08, -0.04
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 2.5d parallax photo 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 |
|---|---|---|
STAGE | 0.75 | Top of the stand, 0-1 down the frame; the product and everything below this line move as the near layer. |
FAR_ZOOM, FAR_PAN | 1.08, -0.04 | Backdrop scale and slide (share of the frame width, negative is left) by the last frame. |
NEAR_ZOOM, NEAR_PAN | 1.16, 0.07 | Product and stand scale and slide by the last frame. The bigger the gap from the far values, the deeper it looks. |
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
Turn the first frame of input.mp4 (540x960, 30fps, 6 seconds, a serum bottle standing on an acrylic block whose top edge is 75% down the frame) into a 2.5D parallax photo. Take the first frame as the photo and split it into two layers. Near layer: the bottle cut out with rembg (u2net model, its mask as the alpha), plus every row from 75% down to the bottom (the block and floor), fading in over 6 pixels centered on that line. Far layer: the photo with the bottle's hole filled: grow the rembg mask (pixels above 40) with a 15x15 dilation and fill it with OpenCV's Telea inpainting, radius 9. Animate with p = smoothstep(t / 6) = x * x * (3 - 2 * x), so frame 0 is the untouched photo and the move eases in and out over the whole 6 seconds. Far layer: scale by 1 + 0.08p about the frame center, then shift left by 0.04p of the frame width. Near layer: scale by 1 + 0.16p about the point at the horizontal center and 75% down, then shift right by 0.07p of the frame width. Draw the far layer, then the near layer over it, using bicubic resampling. Use Python with Pillow and numpy and ffmpeg to read and write the video, keep the size, 30fps, 6-second length and the input's audio, and save it as output.mp4.Run it without Claude
This is the exact script we tested. Save it as 2-5d-parallax-photo.py and run:
python3 2-5d-parallax-photo.py input.mp4 output.mp4Show the script
#!/usr/bin/env python3
"""2.5D parallax photo: a still product photo splits into a near and a far layer that slide and zoom by different amounts.
Usage: python3 2-5d-parallax-photo.py input.mp4 output.mp4
The photo is the input's first frame; the output keeps the input's length and audio.
Needs ffmpeg, Pillow, numpy, OpenCV (to fill the hole behind the product) and rembg (to cut the product out)
(pip install pillow numpy opencv-python-headless "rembg[cpu]"; rembg downloads its u2net model on first use).
"""
import json
import subprocess
import sys
import cv2
import numpy as np
from PIL import Image
from rembg import new_session, remove
STAGE = 0.75 # top of the stand, 0-1 down the frame: the product and everything below it form the near layer
FAR_ZOOM = 1.08 # backdrop scale on the last frame (about the frame center)
FAR_PAN = -0.04 # backdrop slide by the last frame, as a share of the frame width (negative = left)
NEAR_ZOOM = 1.16 # product and stand scale on the last frame (about the stand's top, centered)
NEAR_PAN = 0.07 # product and stand slide by the last frame, as a share of the frame width (positive = right)
def smoothstep(p):
"""0 -> 1, starting and stopping gently, like a camera move."""
p = min(max(p, 0), 1)
return p * p * (3 - 2 * p)
def split_layers(photo):
"""The near layer (product cut out, plus the stand and floor below STAGE) and the far layer (backdrop with the product's hole filled in)."""
mask = np.asarray(remove(photo, session=new_session("u2net"), only_mask=True)).astype(np.float32)
rows = np.clip((np.arange(h) - h * STAGE) / 6 + 0.5, 0, 1)[:, None] * 255 # 6-pixel fade at the stage line
near = photo.convert("RGBA")
near.putalpha(Image.fromarray(np.maximum(mask, rows).astype(np.uint8)))
hole = cv2.dilate((mask > 40).astype(np.uint8) * 255, np.ones((15, 15), np.uint8))
far = Image.fromarray(cv2.inpaint(np.asarray(photo), hole, 9, cv2.INPAINT_TELEA))
return near, far
def move(layer, zoom, pan, cx, cy):
"""Scale a layer by zoom about (cx, cy), then slide it pan pixels across."""
return layer.transform((w, h), Image.AFFINE, (1 / zoom, 0, cx - (cx + pan) / zoom, 0, 1 / zoom, cy - cy / zoom),
Image.BICUBIC)
def compose(t):
"""One output frame: the backdrop drifts one way while the nearer product and stand swing the other way and grow faster."""
p = smoothstep(t / duration)
frame = move(far, 1 + (FAR_ZOOM - 1) * p, FAR_PAN * w * p, w / 2, h / 2).convert("RGBA")
frame.alpha_composite(move(near, 1 + (NEAR_ZOOM - 1) * p, NEAR_PAN * w * p, w / 2, h * STAGE))
return frame.convert("RGB")
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"])
def first_frame(path):
raw = subprocess.check_output(["ffmpeg", "-v", "error", "-i", path, "-frames:v", "1",
"-f", "rawvideo", "-pix_fmt", "rgb24", "-"])
return Image.frombytes("RGB", (w, h), raw)
def render(src, out):
"""Write compose(t) for every frame of src's length to out, with src's audio."""
ff = 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)
for i in range(round(duration * fps)):
ff.stdin.write(compose(i / fps).tobytes())
ff.stdin.close()
if ff.wait():
sys.exit("ffmpeg failed")
src, out = sys.argv[1:3]
w, h, rate, fps, duration = probe(src)
near, far = split_layers(first_frame(src))
render(src, out)





