How to add a split-flap text with Claude

A black departures board near the top clatters each tile through the alphabet, and the letters land one by one, left to right, until it reads "BACK IN STOCK".

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 split-flap text to input.mp4
    Lines: ["BACK IN", "STOCK"]

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 split-flap text 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
LINES["BACK IN", "STOCK"]The message, one board row per line.
START0.5Second the blank board fades in.
FLIP0.06Seconds each flap takes to fall.
FIRST_FLIPS, EXTRA_FLIPS8, 2Flips the first tile makes, and how many more each later tile makes.
Y0.20Center of the board, 0 at the top and 1 at the bottom.
TILE, INK(32, 32, 36), (250, 250, 250)Tile and letter colors.

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 split-flap board to input.mp4: "BACK IN" over "STOCK" on two rows of dark gray (32, 32, 36) rounded tiles, one tile per character with the space included, each tile 10.4% of the frame width wide and 15% of the frame width tall with gaps of 0.6% of the frame width, bold white (250, 250, 250) letters 62% of the tile height, and a thin black hinge line across each tile's middle behind the letter. The tiles sit on a near-black (8, 8, 10) rounded board that reaches 4.5% of the frame width past them, and the whole board is centered across and 20% of the way down. The board fades in at 0.5 seconds over 0.2 seconds, then every tile flips through the alphabet in order, 0.06 seconds per flip: the old letter's top half folds down to the hinge, then the new letter's bottom half swings down. The first tile makes 8 flips before it lands on its letter and each later tile, in reading order, makes 2 more, so the letters land left to right and the board reads BACK IN STOCK by 2.5 seconds, then stays. Use Python with Pillow to draw it and ffmpeg to put it on the video (don't rely on ffmpeg's drawtext filter), keep the audio, and save it as output.mp4.

Run it without Claude

This is the exact script we tested. Save it as split-flap-text.py and run:

python3 split-flap-text.py input.mp4 output.mp4
Show the script
#!/usr/bin/env python3
"""Split-flap text: a black departures board flips each tile through the alphabet until it spells the message.

Usage: python3 split-flap-text.py input.mp4 output.mp4
Needs ffmpeg and Pillow (pip install pillow).
"""
import json
import math
import subprocess
import sys
from pathlib import Path

from PIL import Image, ImageDraw, ImageFont

LINES = ["BACK IN", "STOCK"]   # the message, one board row per line
START = 0.5                    # second the blank board fades in
FADE = 0.2                     # seconds the board takes to fade in
FLIP = 0.06                    # seconds each flap takes to fall
FIRST_FLIPS = 8                # flips the first tile makes before it lands
EXTRA_FLIPS = 2                # each later tile makes this many more, so they land left to right
Y = 0.20                       # center of the board (0 = top, 1 = bottom)
TILE_W, TILE_H = 0.104, 0.15   # tile width and height as a share of the frame width
GAP = 0.006                    # space between tiles, share of the frame width
PAD = 0.045                    # board frame around the tiles, share of the frame width
TILE = (32, 32, 36)            # tile color
BOARD = (8, 8, 10)             # color of the board behind the tiles
INK = (250, 250, 250)          # letter color
CHARS = " ABCDEFGHIJKLMNOPQRSTUVWXYZ"  # the order letters come round on each tile
FONTS = ["/System/Library/Fonts/Supplemental/Arial Bold.ttf",     # macOS
         "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",  # Linux
         "C:/Windows/Fonts/arialbd.ttf"]                          # Windows
FONT_FILE = next((f for f in FONTS if Path(f).exists()), None) or sys.exit("No bold font found: add one to FONTS")


def tile(ch):
    """One flap tile showing ch, with the hinge line across the middle behind the letter."""
    img = Image.new("RGBA", (tw, th))
    draw = ImageDraw.Draw(img)
    draw.rounded_rectangle([0, 0, tw - 1, th - 1], radius=tw // 10, fill=TILE)
    draw.line([0, th // 2, tw, th // 2], fill=(0, 0, 0), width=max(2, th // 40))
    draw.text((tw / 2, th / 2), ch, font=font, anchor="mm", fill=INK)
    return img


def flap(old, new, p):
    """Tile partway through a flip from old to new: new's top shows behind old's top flap, which folds
    down to the hinge (p 0 -> 0.5), then new's bottom flap unfolds down over old's bottom (0.5 -> 1)."""
    a, b = tiles[old], tiles[new]
    half = th // 2
    img = Image.new("RGBA", (tw, th))
    img.paste(b.crop((0, 0, tw, half)), (0, 0))
    img.paste(a.crop((0, half, tw, th)), (0, half))
    if p < 0.5:
        fh = max(1, round(half * (1 - 2 * p)))
        img.paste(a.crop((0, 0, tw, half)).resize((tw, fh)), (0, half - fh))
    else:
        fh = max(1, round(half * (2 * p - 1)))
        img.paste(b.crop((0, half, tw, th)).resize((tw, fh)), (0, half))
    return img


def tile_at(k, target, t):
    """What tile k (in reading order) shows at time t: it steps through CHARS and lands on target."""
    flips = FIRST_FLIPS + EXTRA_FLIPS * k
    done = (t - START - FADE) / FLIP   # flips made so far, counting the one under way as a fraction
    if done >= flips:
        return tiles[target]
    i = int(max(done, 0))
    end = CHARS.index(target)
    old = CHARS[(end - flips + i) % len(CHARS)]
    new = CHARS[(end - flips + i + 1) % len(CHARS)]
    return flap(old, new, max(done, 0) - i)


def overlay(t):
    img = Image.new("RGBA", (w, h))
    if t < START:
        return img
    top = h * Y - (len(LINES) * th + (len(LINES) - 1) * gap) / 2
    widest = max(len(line) for line in LINES) * (tw + gap) - gap
    ImageDraw.Draw(img).rounded_rectangle([(w - widest) / 2 - pad, top - pad, (w + widest) / 2 + pad, h * Y * 2 - top + pad],
                                          radius=pad // 2, fill=BOARD)
    k = 0
    for row, line in enumerate(LINES):
        x0 = (w - len(line) * tw - (len(line) - 1) * gap) / 2
        y0 = top + row * (th + gap)
        for col, ch in enumerate(line):
            img.alpha_composite(tile_at(k, ch, t), (round(x0 + col * (tw + gap)), round(y0)))
            k += 1
    fade = min((t - START) / FADE, 1)
    img.putalpha(img.getchannel("A").point(lambda a: round(a * fade)))
    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: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 render(src, out):
    """Draw overlay(t) on every frame of src and save to out, keeping the audio."""
    ff = subprocess.Popen(["ffmpeg", "-y", "-v", "error", "-i", src,
                           "-f", "rawvideo", "-pix_fmt", "rgba", "-s", f"{w}x{h}", "-r", rate, "-i", "-",
                           "-filter_complex", "[0:v][1:v]overlay=shortest=1,format=yuv420p[v]",
                           "-map", "[v]", "-map", "0:a?", "-c:v", "libx264", "-crf", "18", "-c:a", "copy", out],
                          stdin=subprocess.PIPE)
    for i in range(math.ceil(duration * fps)):
        ff.stdin.write(overlay(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)
tw, th, gap, pad = (round(w * s) for s in (TILE_W, TILE_H, GAP, PAD))
font = ImageFont.truetype(FONT_FILE, round(th * 0.62))
tiles = {ch: tile(ch) for ch in CHARS}
render(src, out)

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