---
title: "How to add a camera shutter transition with Claude (tested prompt and script)"
description: "Six dark gray aperture blades twist inward over clip A, leaving a shrinking hexagon until the lens is shut, then twist back open to reveal clip B. Install one skill and ask Claude Code, or run the Python script we tested yourself."
url: https://www.recharm.com/effects/camera-shutter-transition
---

# How to add a camera shutter transition with Claude

Six dark gray aperture blades twist inward over clip A, leaving a shrinking hexagon until the lens is shut, then twist back open to reveal clip B.

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 camera shutter transition
   Seconds the blades take to shut over clip A: 0.35
   Seconds the shutter stays closed between the clips: 0.1
   ```

## 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 camera shutter 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                                                     |
| -------------- | ---------------------------- | ---------------------------------------------------------------- |
| `CLOSE`        | `0.35`                       | Seconds the blades take to shut over clip A.                     |
| `SHUT`         | `0.1`                        | Seconds the shutter stays closed between the clips.              |
| `OPEN`         | `0.35`                       | Seconds the blades take to open onto clip B.                     |
| `BLADES`       | `6`                          | Number of blades; the opening is a polygon with this many sides. |
| `TWIST`        | `60`                         | Degrees the blades turn between fully open and shut.             |
| `BLADE_COLORS` | `(52, 53, 58), (40, 41, 46)` | Alternating blade shades.                                        |
| `SEAM`         | `(88, 90, 98)`               | Color of the lines where the blades overlap.                     |
| `RIM`          | `(150, 152, 160)`            | Color of the edge round the opening.                             |

## 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 0.8 second camera shutter (aperture) transition starting 0.8 seconds before A ends (so the result is 11.2 seconds for two 6 second clips). Build the transition frames with Python, numpy and Pillow. The picture shows through a regular hexagon centered in the frame whose corners sit at radius r = o * R from the center, where R = (half the frame diagonal + 20) / cos(30 degrees) so the fully open hexagon clears the corners, and whose corners are at angles turn + i*60 degrees with turn = 60 * (1 - o) degrees, so the blades twist as they close. Everything outside the hexagon is covered by six opaque blades: blade i fills the area between hexagon edge i and the two neighboring edges' lines carried outward past their corners (a pinwheel), shaded alternately RGB (52, 53, 58) and (40, 41, 46), with a 2 pixel (88, 90, 98) line along each of those outward lines and a 2 pixel (150, 152, 160) outline round the hexagon. Draw the blades at twice the frame size and scale them down for smooth edges. Over the first 0.35 seconds the blades close over clip A (o = 1 - s(p), p from 0 to 1, with smoothstep s(p) = p*p*(3-2p)); for the next 0.1 seconds the shutter stays fully closed; over the last 0.35 seconds they open onto clip B (o = s(p)), with B playing from its first frame at the start of the transition. Don't use ffmpeg's drawtext. Use ffmpeg for the rest: match B's size and frame rate to A (30fps), crossfade the audio over the same 0.8 seconds, keep the audio and save it as output.mp4 (H.264, yuv420p).
```

## Run it without Claude

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

```
python3 camera-shutter-transition.py clipA.mp4 clipB.mp4 output.mp4
```

```
#!/usr/bin/env python3
"""Camera shutter: six gray aperture blades twist shut over clip A, then twist open onto clip B.

Usage: python3 camera-shutter-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 math
import subprocess
import sys

import numpy as np
from PIL import Image, ImageDraw

CLOSE = 0.35                 # seconds the blades take to shut over clip A
SHUT = 0.1                   # seconds the shutter stays closed
OPEN = 0.35                  # seconds the blades take to open onto clip B
BLADES = 6                   # number of blades (the opening is a polygon with this many sides)
TWIST = 60                   # degrees the blades turn between fully open and shut
BLADE_COLORS = [(52, 53, 58), (40, 41, 46)]   # alternating blade shades
SEAM = (88, 90, 98)          # lines where the blades overlap
RIM = (150, 152, 160)        # bright edge round the opening
FPS = 30

a_path, b_path, out_path = sys.argv[1:4]
info = json.loads(subprocess.run(
    ["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries", "stream=width,height:format=duration",
     "-of", "json", a_path], capture_output=True, check=True).stdout)
w, h = info["streams"][0]["width"], info["streams"][0]["height"]
D = CLOSE + SHUT + OPEN
offset = float(info["format"]["duration"]) - D
n = round(D * FPS)
# fully open, the polygon's inner circle clears the frame's corners
widest = (math.hypot(w, h) / 2 + 20) / math.cos(math.pi / BLADES)

def frames(path, start):
    """The n frames from `start`, fitted to A's size, as RGB arrays."""
    raw = subprocess.run(
        ["ffmpeg", "-v", "error", "-ss", str(start), "-i", path, "-frames:v", str(n),
         "-vf", f"scale={w}:{h}:force_original_aspect_ratio=increase,crop={w}:{h},fps={FPS}",
         "-f", "rawvideo", "-pix_fmt", "rgb24", "-"], capture_output=True, check=True).stdout
    return np.frombuffer(raw, np.uint8).reshape(-1, h, w, 3)

def smoothstep(p):
    return p * p * (3 - 2 * p)

def openness(t):
    """1 = shutter fully open, 0 = shut."""
    if t < CLOSE:
        return 1 - smoothstep(t / CLOSE)
    if t < CLOSE + SHUT:
        return 0.0
    return smoothstep((t - CLOSE - SHUT) / OPEN)

def blades(o):
    """The blades as an RGBA layer, drawn at twice the size and scaled down for smooth edges."""
    s = 2
    img = Image.new("RGBA", (w * s, h * s))
    draw = ImageDraw.Draw(img)
    cx, cy, r = w * s / 2, h * s / 2, o * widest * s
    turn = math.radians(TWIST * (1 - o))
    corners = [(cx + r * math.cos(turn + 2 * math.pi * i / BLADES), cy + r * math.sin(turn + 2 * math.pi * i / BLADES))
               for i in range(BLADES)]
    far = 4 * max(w, h) * s
    # each blade fills the space between its edge of the opening and the two neighboring edges carried outward
    seams = []
    for i in range(BLADES):
        (x0, y0), (x1, y1) = corners[i], corners[(i + 1) % BLADES]
        ang = turn + 2 * math.pi * (i + 0.5) / BLADES + math.pi / 2         # direction of this edge
        prev = ang - 2 * math.pi / BLADES                                   # direction of the edge before
        tip = (x1 + far * math.cos(ang), y1 + far * math.sin(ang))
        tail = (x0 + far * math.cos(prev), y0 + far * math.sin(prev))
        draw.polygon([(x0, y0), (x1, y1), tip, tail], fill=BLADE_COLORS[i % 2])
        seams.append([(x1, y1), tip])
    for seam in seams:
        draw.line(seam, fill=SEAM, width=2 * s)
    if r > 1:
        draw.line(corners + corners[:1], fill=RIM, width=2 * s, joint="curve")
    return img.resize((w, h), Image.LANCZOS)

def shutter_frame(picture, o):
    layer = np.asarray(blades(o), np.float32)
    alpha = layer[:, :, 3:] / 255
    return (picture * (1 - alpha) + layer[:, :, :3] * alpha).astype(np.uint8).tobytes()

tail, head = frames(a_path, offset), frames(b_path, 0)
middle = b"".join(shutter_frame(tail[i] if i / FPS < CLOSE + SHUT else head[i], openness(i / FPS)) for i in range(n))

# A up to the shutter, then the shutter frames from stdin, then the rest of B; the sound crossfades underneath
subprocess.run(
    ["ffmpeg", "-y", "-v", "error", "-i", a_path, "-i", b_path,
     "-f", "rawvideo", "-pix_fmt", "rgb24", "-s", f"{w}x{h}", "-r", str(FPS), "-i", "-",
     "-filter_complex",
     f"[0:v]fps={FPS},trim=end_frame={round(offset * FPS)},setpts=PTS-STARTPTS,format=yuv420p,setsar=1[a];"
     f"[2:v]format=yuv420p,setsar=1[m];"
     f"[1:v]scale={w}:{h}:force_original_aspect_ratio=increase,crop={w}:{h},fps={FPS},"
     f"trim=start_frame={n},setpts=PTS-STARTPTS,format=yuv420p,setsar=1[b];"
     f"[a][m][b]concat=n=3:v=1:a=0[v];[0:a][1:a]acrossfade=d={D}[au]",
     "-map", "[v]", "-map", "[au]", "-c:v", "libx264", "-crf", "18", "-pix_fmt", "yuv420p", "-c:a", "aac", out_path],
    input=middle, check=True)
```

## 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)
