How to add a tile flip transition with Claude

Clip A breaks into a 4-by-6 grid of tiles that each turn over on their vertical center line in a diagonal wave from the top-left corner, flashing a dark gap as they go edge-on and landing with clip B on their backs, over 1.2 seconds.

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

    Join clipA.mp4 and clipB.mp4 with a tile flip transition
    Whole transition in seconds, from the first tile starting to the last tile landing: 1.2
    Seconds one tile takes to turn over: 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.

Put a sale badge on @clip1.mp4, give @clip2.mp4 a VHS look, then join them with a tile flip transition.

Settings you can change

Ask Claude to change any of these, or edit them at the top of the script.

SettingDefaultWhat it does
D1.2Whole transition in seconds, from the first tile starting to the last tile landing.
FLIP0.5Seconds one tile takes to turn over.
COLS, ROWS4, 6Grid size; the wave runs from the top-left tile to the bottom-right one.
FPS30Output frame rate.
BACKGROUND(14, 14, 18)Color seen behind a tile while it turns.
TAPER0.25How much a tile's far edge shrinks as it turns (0 = flat squeeze, no perspective).
SHADE0.5How much a tile darkens as it turns edge-on (0 = not at all).

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
Join clipA.mp4 and clipB.mp4 (both 540x960, 30fps) with a 1.2 second tile flip transition that starts 1.2 seconds before A ends, so the output is 10.8 seconds long. Split the frame into a grid of 4 columns by 6 rows (135x160 pixel tiles, from the top-left corner). Each tile turns over 180 degrees about its own vertical center line in 0.5 seconds with a cubic ease-in-out: for the first 90 degrees it shows its piece of clip A, squeezed horizontally to cos(angle) of its width and centered in its cell, then it shows the same piece of clip B coming back out the other way. Give it a little perspective: the edge turning away shrinks to 1 - 0.25*|sin(angle)| of the tile height, centered vertically, and darken the tile by 1 - 0.5*|sin(angle)|. The space a turning tile uncovers is dark gray RGB (14, 14, 18). The tile in column c and row r (both from 0) starts its flip (c + r) * 0.0875 seconds after the transition starts, so the top-left tile goes first and the bottom-right tile lands exactly at the end. Crossfade the audio over the same 1.2 seconds. Use Python with OpenCV and numpy for the frames and ffmpeg to decode and encode, keep the audio and save it as output.mp4 (H.264, yuv420p, 30fps).

Run it without Claude

This is the exact script we tested. Save it as tile-flip-transition.py and run:

python3 tile-flip-transition.py clipA.mp4 clipB.mp4 output.mp4
Show the script
#!/usr/bin/env python3
"""Tile flip transition: clip A splits into a grid of tiles that flip over in a diagonal wave, with clip B on their backs.

Usage: python3 tile-flip-transition.py clipA.mp4 clipB.mp4 output.mp4  (both clips need an audio track)
Needs ffmpeg, OpenCV and numpy (pip install opencv-python-headless numpy).
"""
import json
import math
import subprocess
import sys
from itertools import islice

import cv2
import numpy as np

D = 1.2                    # whole transition in seconds, first tile starting to last tile landing
FLIP = 0.5                 # seconds one tile takes to turn over
COLS, ROWS = 4, 6          # grid size; the wave runs from the top-left tile to the bottom-right one
FPS = 30                   # output frame rate
BACKGROUND = (14, 14, 18)  # color seen behind a tile while it turns
TAPER = 0.25               # how much a tile's far edge shrinks as it turns (0 = flat squeeze, no perspective)
SHADE = 0.5                # how much a tile darkens as it turns edge-on (0 = not at all)


def probe(path):
    info = json.loads(subprocess.check_output(["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries",
                                               "stream=width,height:format=duration", "-of", "json", path]))
    return info["streams"][0]["width"], info["streams"][0]["height"], float(info["format"]["duration"])


def frames(path):
    """Decode a clip as w x h RGB frames at FPS, cropping it to fill the frame if its shape differs."""
    ff = subprocess.Popen(["ffmpeg", "-v", "error", "-i", path, "-vf",
                           f"scale={w}:{h}:force_original_aspect_ratio=increase,crop={w}:{h},fps={FPS}",
                           "-f", "rawvideo", "-pix_fmt", "rgb24", "-"], stdout=subprocess.PIPE)
    while True:
        data = ff.stdout.read(w * h * 3)
        if len(data) < w * h * 3:
            return
        yield np.frombuffer(data, np.uint8).reshape(h, w, 3)


def ease(p):
    """Slow start, fast middle, slow landing (cubic ease-in-out)."""
    return 4 * p ** 3 if p < 0.5 else 1 - (-2 * p + 2) ** 3 / 2


def turned(tile, angle):
    """tile turned `angle` degrees (-90 to 90) about its vertical center line, drawn on the background."""
    th, tw = tile.shape[:2]
    a = math.radians(angle)
    half = tw / 2 * math.cos(a)
    far = 1 - TAPER * abs(math.sin(a))                 # the edge turning away from us gets shorter
    left, right = (1, far) if angle > 0 else (far, 1)
    corners = [(tw / 2 - half, th / 2 * (1 - left)), (tw / 2 + half, th / 2 * (1 - right)),
               (tw / 2 + half, th / 2 * (1 + right)), (tw / 2 - half, th / 2 * (1 + left))]
    m = cv2.getPerspectiveTransform(np.float32([(0, 0), (tw, 0), (tw, th), (0, th)]), np.float32(corners))
    lit = (tile * (1 - SHADE * abs(math.sin(a)))).astype(np.uint8)
    return cv2.warpPerspective(lit, m, (tw, th), flags=cv2.INTER_LINEAR, borderValue=BACKGROUND)


def tile_flip(a, b, t):
    """One transition frame, t seconds in: each tile starts its flip a little after the one up and to its left."""
    out = np.empty_like(a)
    wave = (D - FLIP) / (COLS + ROWS - 2)              # delay between neighbouring diagonals
    for r in range(ROWS):
        for c in range(COLS):
            ys = slice(r * h // ROWS, (r + 1) * h // ROWS)
            xs = slice(c * w // COLS, (c + 1) * w // COLS)
            angle = 180 * ease(min(max((t - (r + c) * wave) / FLIP, 0), 1))
            # past 90 degrees we see the tile's back, which is B the right way round
            out[ys, xs] = turned(a[ys, xs], angle) if angle < 90 else turned(b[ys, xs], angle - 180)
    return out


A, B, out = sys.argv[1:4]
w, h, length = probe(A)
cut_in = round((length - D) * FPS)   # first frame of the transition
n = round(D * FPS)                   # frames in the transition

enc = subprocess.Popen(["ffmpeg", "-y", "-v", "error", "-f", "rawvideo", "-pix_fmt", "rgb24", "-s", f"{w}x{h}",
                        "-r", str(FPS), "-i", "-", "-i", A, "-i", B,
                        "-filter_complex", f"[1:a][2:a]acrossfade=d={D}[au]", "-map", "0:v", "-map", "[au]",
                        "-c:v", "libx264", "-crf", "18", "-pix_fmt", "yuv420p", "-c:a", "aac", out],
                       stdin=subprocess.PIPE)
clip_a, clip_b = frames(A), frames(B)
for a in islice(clip_a, cut_in):
    enc.stdin.write(a.tobytes())
for i, (a, b) in enumerate(zip(islice(clip_a, n), islice(clip_b, n))):
    enc.stdin.write(tile_flip(a, b, i / FPS).tobytes())
for b in clip_b:
    enc.stdin.write(b.tobytes())
enc.stdin.close()
if enc.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