How to add a funhouse mirror with Claude

For four seconds the bottle bulges fat in one band and pinches thin in the next, an hourglass shape that keeps rolling down the frame, then it settles back to its true shape.

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 funhouse mirror to input.mp4
    Second the mirror warp starts: 1.0
    Second it's gone again: 5.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 a funhouse mirror 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
START1.0Second the mirror warp starts.
END5.0Second it's gone again.
EASE0.5Seconds it takes to build up at the start and fade out at the end.
BULGE0.8Extra width in the fattest band. 0.8 is 80% wider.
BANDS2.0Fat-and-thin waves from the top of the frame to the bottom.
ROLL2.5Seconds for the bands to roll down one wave. Bigger is slower.

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 funhouse mirror effect to input.mp4 from 1.0 to 5.0 seconds; before 1.0 and after 5.0 seconds the video is unchanged. Strength k eases from 0 to 1 over the first 0.5 seconds and back to 0 over the last 0.5 seconds (smoothstep k = u²(3 − 2u), where u = min(t − 1, 5 − t, 0.5) / 0.5). Each row is stretched sideways around the frame's vertical center line by its own amount: output pixel (x, y) shows the input pixel at column w/2 + (x − w/2) / s on the same row, with s = 1 + 0.8 × k × (1 + sin(2π × (2 × y / h − (t − 1) / 2.5))) / 2, where w and h are the frame width and height. So there are 2 fat-and-thin waves from top to bottom, the fattest rows are 1.8x wider at full strength, the thinnest keep their width, and the bands roll down one wave every 2.5 seconds. Only columns move, never rows, and the center column stays put. Use Python with OpenCV (cv2.remap, cubic interpolation) and ffmpeg, keep the size, 30fps and the audio, and save it as output.mp4.

Run it without Claude

This is the exact script we tested. Save it as funhouse-mirror.py and run:

python3 funhouse-mirror.py input.mp4 output.mp4
Show the script
#!/usr/bin/env python3
"""Funhouse mirror: the picture swells fat and pinches thin in bands that roll down the frame, like a carnival mirror.

Usage: python3 funhouse-mirror.py input.mp4 output.mp4
Needs ffmpeg, OpenCV and numpy (pip install opencv-python-headless numpy).
"""
import json
import math
import subprocess
import sys

import cv2
import numpy as np

START = 1.0      # second the mirror warp starts
END = 5.0        # second it's gone again
EASE = 0.5       # seconds it takes to build up at the start and fade out at the end
BULGE = 0.8      # extra width in the fattest band (0.8 = 80% wider); the thinnest band keeps its width
BANDS = 2.0      # fat-and-thin waves from the top of the frame to the bottom
ROLL = 2.5       # seconds for the bands to roll down one wave


def strength(t):
    """0 outside the effect, easing up to 1 and back down over EASE seconds at each end."""
    u = max(min(t - START, END - t, EASE) / EASE, 0)
    return u * u * (3 - 2 * u)


def source_columns(t):
    """For every pixel, the input column it shows: each row is scaled across the frame's center line
    by its own amount, between 1 (thin bands) and 1 + BULGE (fat bands)."""
    wave = np.sin(2 * math.pi * (BANDS * (rows[:, :1] + 0.5) / h - (t - START) / ROLL))
    scale = 1 + strength(t) * BULGE * (1 + wave) / 2
    return (w / 2 + (cols - w / 2) / scale).astype(np.float32)


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)
rows, cols = np.mgrid[0:h, 0:w].astype(np.float32) + 0.5
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 True:
    buf = reader.stdout.read(w * h * 3)
    if len(buf) < w * h * 3:
        break
    img = np.frombuffer(buf, np.uint8).reshape(h, w, 3)
    t = i / fps
    if START < t < END:
        img = cv2.remap(img, source_columns(t) - 0.5, rows - 0.5, cv2.INTER_CUBIC, borderMode=cv2.BORDER_REFLECT)
    writer.stdin.write(img.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