---
title: "How to add a smoke transition with Claude (tested prompt and script)"
description: "Billowing pale gray smoke rises up the first shot until it hides the whole frame, then drifts upward and clears from the bottom to uncover the second shot. Install one skill and ask Claude Code, or run the Python script we tested yourself."
url: https://www.recharm.com/effects/smoke-transition
---

# How to add a smoke transition with Claude

Billowing pale gray smoke rises up the first shot until it hides the whole frame, then drifts upward and clears from the bottom to uncover the second shot.

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 smoke transition
   Length of the transition in seconds; the cut to B happens unseen at the midpoint: 2.0
   Share of the transition the frame stays fully covered around the cut: 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 smoke 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`     | `2.0`             | Length of the transition in seconds; the cut to B happens unseen at the midpoint. |
| `HOLD`  | `0.1`             | Share of the transition the frame stays fully covered around the cut.             |
| `COLOR` | `(244, 245, 248)` | Brightest smoke color.                                                            |
| `SHADE` | `0.28`            | How much darker the shadowed folds of the smoke are (0 = flat color).             |
| `SOFT`  | `0.18`            | Width of the wispy smoke edge (0 = hard edge).                                    |
| `RISE`  | `260`             | How fast the smoke billows upward, in pixels per second.                          |
| `BLOB`  | `140`             | Rough size of the smoke puffs, in pixels.                                         |

## 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 (both 540x960, 6 seconds) with a 2 second smoke transition, using Python with numpy and ffmpeg to read and write the frames. Make the smoke from soft cloudy noise (random noise low-pass filtered so the puffs are about 140 pixels across) that scrolls upward at 260 pixels per second, shaded from (244, 245, 248) in the thin folds down to about 28% darker in the thick ones so it looks rounded. The transition runs from 4.0 to 6.0 seconds of the output: the smoke rises from the bottom edge and fills the frame bottom first, with ragged wispy edges, until it covers 100% of the picture from 4.9 to 5.1 seconds; the cut from A to B happens hidden behind it at 5.0 seconds; then it clears bottom first, so the bottom of clip B is clear again by 5.55 seconds while smoke still hangs at the top, and it is gone at 6.0 seconds. B then plays to its end, for a 10 second video at 30fps. Crossfade the audio over the same 2 seconds, don't use ffmpeg's drawtext filter, and save it as output.mp4.
```

## Run it without Claude

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

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

```
#!/usr/bin/env python3
"""Smoke transition: pale smoke rolls up from the bottom of clip A until it fills the frame, then lifts away upward to show clip B.

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

import numpy as np

D = 2.0                     # transition length in seconds; the cut to B happens unseen at the midpoint
HOLD = 0.1                  # share of D the frame stays fully covered around the cut
COLOR = (244, 245, 248)     # brightest smoke color, a near-white
SHADE = 0.28                # how much darker the shadowed folds of the smoke are (0 = flat color)
SOFT = 0.18                 # width of the wispy smoke edge (0 = hard edge)
RISE = 260                  # how fast the smoke billows upward, in pixels per second
BLOB = 140                  # rough size of the smoke puffs, in pixels
SEED = 5                    # change for a different smoke pattern
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 chunk := ff.stdout.read(w * h * 3):
        yield np.frombuffer(chunk, np.uint8).reshape(h, w, 3).astype(np.float32)

def clouds(rng, height, width, size):
    """Soft cloudy noise (0-1) that wraps top to bottom: random noise with its fine detail filtered out."""
    fy = np.fft.fftfreq(height)[:, None]
    fx = np.fft.fftfreq(width)[None, :]
    f = np.hypot(fx, fy) * size
    field = np.fft.ifft2(np.fft.fft2(rng.standard_normal((height, width))) * np.exp(-f ** 2) / np.maximum(f, 0.3)).real
    return ((field - field.min()) / (field.max() - field.min())).astype(np.float32)

def smoke(base, p):
    """Frame at progress p (0-1): smoke thickens from the bottom up, holds, then thins away from the bottom up."""
    rows = (np.arange(h) - round(RISE * p * D)) % puffs.shape[0]   # the puffs drift upward as they billow
    density, shade = puffs[rows], folds[rows]
    if p < 0.5:
        amount = min(p / (0.5 - HOLD / 2), 1)
        threshold = 0.55 * density + 0.45 * (1 - y)                # lower rows fill first
    else:
        amount = min((1 - p) / (0.5 - HOLD / 2), 1)
        threshold = 0.55 * density + 0.45 * y                      # lower rows clear first, as the smoke lifts
    reach = amount * (1 + SOFT)
    alpha = np.clip((reach - threshold) / SOFT, 0, 1)[..., None]
    color = np.array(COLOR, np.float32) * (1 - SHADE * shade)[..., None]
    return base * (1 - alpha) + color * alpha

a_path, b_path, out = sys.argv[1:4]
w, h, a_len = probe(a_path)
rng = np.random.default_rng(SEED)
puffs = clouds(rng, 2 * h, w, BLOB)
folds = clouds(rng, 2 * h, w, BLOB / 2)
y = np.linspace(0, 1, h, dtype=np.float32)[:, None]
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:
        p = (i - start + 0.5) / steps
        b = next(b_frames)
        a = smoke(a if p < 0.5 else b, p)
    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)
