---
title: "How to add a pinwheel wipe with Claude (tested prompt and script)"
description: "Four curved blades edged in white spin clockwise out from the center, each one widening until clip B fills the frame. Install one skill and ask Claude Code, or run the Python script we tested yourself."
url: https://www.recharm.com/effects/pinwheel-wipe
---

# How to add a pinwheel wipe with Claude

Four curved blades edged in white spin clockwise out from the center, each one widening until clip B fills the frame.

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 a pinwheel wipe
   Length of the transition in seconds: 1.0
   Number of blades on the pinwheel: 4
   ```

## 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 pinwheel wipe.

## 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.0`   | Length of the transition in seconds.                                                    |
| `BLADES` | `4`     | Number of blades on the pinwheel.                                                       |
| `TWIST`  | `70`    | How far each blade's edge bends between the center and the frame's corners, in degrees. |
| `LINE`   | `4`     | Width of the white line along each blade's edge in pixels; 0 for none.                  |

## 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 second pinwheel wipe starting 1 second before A ends (output 11 seconds for two 6 second clips). Split the frame into 4 equal blades around its center, the first starting at twelve o'clock. Each blade's edge curves: its angle shifts clockwise by 70 degrees from the center out to the frame's corners. Over the second, every blade of B grows clockwise from its leading edge until it covers its whole quarter, eased in and out (smoothstep), so all four sweep at once. Draw a 4 pixel pure white (255,255,255) line along every blade edge while the wipe runs, and smooth the edges over about a pixel. Fit B to A's size at 30fps. Build the mask per frame with numpy and composite with Python, piping frames to ffmpeg. Crossfade the audio over the same 1 second and keep it. Save it as output.mp4 (H.264, yuv420p).
```

## Run it without Claude

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

```
python3 pinwheel-wipe.py clipA.mp4 clipB.mp4 output.mp4
```

```
#!/usr/bin/env python3
"""Pinwheel wipe: four curved blades of clip B sweep clockwise around the center of clip A until B fills the frame.

Usage: python3 pinwheel-wipe.py clipA.mp4 clipB.mp4 output.mp4  (both clips need an audio track)
Needs ffmpeg and numpy (pip install numpy).
"""
import json
import math
import subprocess
import sys

import numpy as np

D = 1.0          # transition length in seconds
BLADES = 4       # blades on the pinwheel
TWIST = 70       # how far each blade's edge bends between the center and the frame's corners, in degrees
LINE = 4         # width of the white line along each blade's edge, in pixels (0 for none)
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 an 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)

def blade_layout():
    """For every pixel: where it sits within its blade's slice (0-1, clockwise from the blade's leading edge,
    bent by TWIST further out), and how many pixels one whole slice spans at its distance from the center."""
    y, x = np.mgrid[0:h, 0:w].astype(np.float32)
    dx, dy = x + 0.5 - w / 2, y + 0.5 - h / 2
    r = np.hypot(dx, dy)
    angle = np.arctan2(dx, -dy) - math.radians(TWIST) * r / r.max()    # 0 at twelve o'clock, clockwise
    slice_angle = 2 * math.pi / BLADES
    return (angle / slice_angle) % 1, np.maximum(r, 1) * slice_angle

def pinwheel_wipe(a, b, p):
    """Mix of A and B at progress p (0-1): each blade of B covers the first p of its slice (easing in and out),
    with a white line along its edges, all smoothed over about a pixel."""
    eased = p * p * (3 - 2 * p)
    inside = place < eased
    edge = np.where(inside, np.minimum(place, eased - place), -np.minimum(place - eased, 1 - place)) * span
    mask = np.clip(0.5 + edge, 0, 1)[..., None]
    line = np.clip(LINE / 2 + 0.5 - np.abs(edge), 0, 1)[..., None]
    mixed = a * (1 - mask) + b * mask
    return (mixed * (1 - line) + 255 * line).astype(np.uint8)

a_path, b_path, out = sys.argv[1:4]
w, h, a_len = probe(a_path)
place, span = blade_layout()
start = round((a_len - D) * FPS)    # first frame of the transition
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 = pinwheel_wipe(a, next(b_frames), (i - start + 1) / (round(D * FPS) + 1))
    ff.stdin.write(a.tobytes())
for b in b_frames:
    ff.stdin.write(b.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)
