---
title: "How to add an ink transition with Claude (tested prompt and script)"
description: "Three deep indigo ink blots with wavy, ragged edges bloom over clip A one after another, and clip B opens up inside them as they spread and merge. Install one skill and ask Claude Code, or run the Python script we tested yourself."
url: https://www.recharm.com/effects/ink-transition
---

# How to add an ink transition with Claude

Three deep indigo ink blots with wavy, ragged edges bloom over clip A one after another, and clip B opens up inside them as they spread and merge.

Made by the script on this page, from our own test clips.

## Get this effect

1. ### Switch to Code in your [Claude Desktop App](https://claude.com/download)

2. ### Install the skill (once)

   ```
   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 an ink transition
   Length of the transition in seconds (the clips overlap by this much): 1.5
   Each blot's center (0-1 across, 0-1 down) and when it starts, as a share of the transition: (0.28, 0.22, 0.00), (0.75, 0.52, 0.15), (0.30, 0.82, 0.30)
   ```

## 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 an ink transition.

## 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                                                                                |
| -------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------- |
| `D`      | `1.5`                                                        | Length of the transition in seconds (the clips overlap by this much).                       |
| `BLOTS`  | `(0.28, 0.22, 0.00), (0.75, 0.52, 0.15), (0.30, 0.82, 0.30)` | Each blot's center (0-1 across, 0-1 down) and when it starts, as a share of the transition. |
| `LOBES`  | `0.30`                                                       | How far the blot outlines bulge in and out (0 makes circles).                               |
| `RAGGED` | `0.025`                                                      | Fine ragged noise on the edges, as a share of the frame width.                              |
| `RIM`    | `0.14`                                                       | Width of the ink band ahead of clip B, as a share of the frame width.                       |
| `INK`    | `(20, 14, 38)`                                               | Ink color, a deep indigo.                                                                   |
| `FPS`    | `30`                                                         | Frame rate of the output.                                                                   |

## Without the skill

Don’t want to install the skill? Paste this prompt into Claude Code and Claude writes the script itself.

```
Join clipA.mp4 and clipB.mp4 with a 1.5 second ink transition that starts 1.5 seconds before A ends, so the result is 10.5 seconds. Three ink blots grow over A, centered at (28% across, 22% down), (75% across, 52% down) and (30% across, 82% down); they start at 0, 0.15 and 0.30 of the way through the transition and each finishes growing at the end of it, with a smoothstep ease (slow, fast, slow). Give each blot a wavy outline: its radius at angle a is r * (1 + 0.3 * f(a)), where f is a sum of sines with 3, 5, 7 and 11 lobes (random phases), scaled to -1..1, and roughen all edges by up to 2.5% of the frame width with smooth noise. Paint the blots in solid ink (20, 14, 38), and show B inside each blot except for a band of ink 14% of the frame width wide around its edge, so the ink leads and B follows. Size the blots so B covers the whole frame exactly when the transition ends; before it show only A, after it only B. Anti-alias the edges over 2 pixels. Fit B to A's size (scale to cover, center crop) at 30fps. Do the per-pixel compositing in Python with numpy and Pillow, piping frames to and from ffmpeg (no ffmpeg drawtext). Crossfade the audio over the same 1.5 seconds. Encode H.264 yuv420p and save it as output.mp4.
```

## Run it without Claude

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

```
python3 ink-transition.py clipA.mp4 clipB.mp4 output.mp4
```

```
#!/usr/bin/env python3
"""Ink transition: three indigo ink blots with ragged edges bloom over clip A, and clip B opens up inside them.

Usage: python3 ink-transition.py clipA.mp4 clipB.mp4 output.mp4  (both clips need an audio track)
Needs ffmpeg, Pillow and numpy (pip install pillow numpy).
"""
import json
import subprocess
import sys

import numpy as np
from PIL import Image

D = 1.5                       # transition length in seconds (the clips overlap by this much)
BLOTS = [(0.28, 0.22, 0.00),  # each blot: center (0-1 across, 0-1 down) and when it starts (share of D)
         (0.75, 0.52, 0.15),
         (0.30, 0.82, 0.30)]
LOBES = 0.30                  # how far the blot outlines bulge in and out (0 = circles)
RAGGED = 0.025                # fine ragged noise on the edges, as a share of the frame width
RIM = 0.14                    # width of the ink band ahead of clip B, as a share of the frame width
INK = (20, 14, 38)            # ink color, a deep indigo
FPS = 30

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):
    """Every frame of a clip, fitted to A's size at 30fps, as a float RGB array."""
    fit = f"scale={w}:{h}:force_original_aspect_ratio=increase,crop={w}:{h},fps={FPS}"
    ff = subprocess.Popen(["ffmpeg", "-v", "error", "-i", path, "-vf", fit, "-f", "rawvideo", "-pix_fmt", "rgb24", "-"],
                          stdout=subprocess.PIPE)
    while True:
        chunk = ff.stdout.read(w * h * 3)
        if not chunk:
            break
        yield np.frombuffer(chunk, np.uint8).reshape(h, w, 3).astype(np.float32)

def blot_shapes(rng):
    """For each blot, every pixel's distance to its center (in pixels), shrunk where the outline bulges out.
    A blot of size r then covers the pixels whose value is below r."""
    y, x = np.mgrid[0:h, 0:w].astype(np.float32)
    grid = Image.fromarray(rng.random((h // 24 + 2, w // 24 + 2)).astype(np.float32))
    ragged = (np.asarray(grid.resize((w, h), Image.BICUBIC), np.float32) - 0.5) * 2 * RAGGED * w
    shapes = []
    for cx, cy, _ in BLOTS:
        dx, dy = x - cx * w, y - cy * h
        angle = np.arctan2(dy, dx)
        wave = sum(rng.uniform(0.4, 1) * np.sin(k * angle + rng.uniform(0, 2 * np.pi)) for k in (3, 5, 7, 11))
        shapes.append(np.hypot(dx, dy) / (1 + LOBES * wave / np.abs(wave).max()) + ragged)
    return shapes

def ink(a, b, p):
    """Frame at progress p (0-1): ink covers A where a blot has spread, and B shows inside the ink band."""
    covered = revealed = np.zeros((h, w), np.float32)
    for (_, _, delay), shape in zip(BLOTS, shapes):
        grow = np.clip((p - delay) / (1 - delay), 0, 1)
        r = reach * grow * grow * (3 - 2 * grow)   # starts slowly, speeds up, then settles
        covered = np.maximum(covered, np.clip((r - shape) / 2, 0, 1))
        revealed = np.maximum(revealed, np.clip((r - RIM * w - shape) / 2, 0, 1))
    covered, revealed = covered[..., None], revealed[..., None]
    return a * (1 - covered) + np.array(INK, np.float32) * (covered - revealed) + b * revealed

a_path, b_path, out = sys.argv[1:4]
w, h, a_len = probe(a_path)
shapes = blot_shapes(np.random.default_rng(7))
reach = np.min(shapes, axis=0).max() + RIM * w + 4   # final blot size: just enough for B to fill the frame
start = round((a_len - D) * FPS)    # first frame of the transition
steps = round(D * FPS)
ff = subprocess.Popen(["ffmpeg", "-y", "-v", "error", "-f", "rawvideo", "-pix_fmt", "rgb24", "-s", f"{w}x{h}",
                       "-r", str(FPS), "-i", "-", "-i", a_path, "-i", b_path,
                       "-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)
b_frames = frames(b_path)
for i, a in enumerate(frames(a_path)):
    if i >= start:
        a = ink(a, next(b_frames), (i - start + 1) / steps)
    ff.stdin.write(np.clip(a, 0, 255).astype(np.uint8).tobytes())
for b in b_frames:
    ff.stdin.write(b.astype(np.uint8).tobytes())
ff.stdin.close()
if ff.wait():
    sys.exit("ffmpeg failed")
```

## More transitions

- [Barn door transition **Barn door transition**](https://www.recharm.com/effects/barn-door)
- [Blinds transition **Blinds transition**](https://www.recharm.com/effects/blinds-transition)
- [Block dissolve **Block dissolve**](https://www.recharm.com/effects/block-dissolve)
- [Blur transition **Blur transition**](https://www.recharm.com/effects/blur-transition)
- [Bounce transition **Bounce transition**](https://www.recharm.com/effects/bounce-transition)

## 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](https://www.recharm.com/get-started)
