How to add a cloud transition with Claude

The first shot sinks and zooms as if the camera were climbing, white clouds stream down from the top until the frame is all cloud, then they part from the middle outward to reveal the second shot.

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

    Join clipA.mp4 and clipB.mp4 with a cloud 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 cloud transition.

Settings you can change

Ask Claude to change any of these, or edit them at the top of the script.

SettingDefaultWhat it does
D2.0Length of the transition in seconds; the cut to B happens unseen at the midpoint.
HOLD0.1Share of the transition the frame stays fully covered around the cut.
LIGHT(248, 249, 251)Sunlit cloud color.
SHADOW(206, 212, 222)Cloud color in the shadowed folds.
RISE900How fast the clouds stream down past the camera, in pixels per second.
CLIMB0.18How far clip A sinks as the camera rises, as a share of the frame height.
PUSH1.25Zoom on A as it disappears and on B as it first shows; B settles back to 1.
BLOB150Rough size of the cloud puffs, in pixels.
SOFT0.14Width of the feathered cloud edge (0 = hard edge).

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
Join clipA.mp4 and clipB.mp4 (both 540x960, 6 seconds) with a 2 second cloud transition, using Python with numpy and ffmpeg to read and write the frames. Make the clouds from soft cloudy noise (random noise low-pass filtered so the puffs are about 150 pixels across) that streams downward at 900 pixels per second and swells to 1.6 times its size over the transition, so it feels like flying up through it; shade it from sunlit (248, 249, 251) to a cool gray (206, 212, 222) in the folds, with a feathered edge. The transition runs from 4.0 to 6.0 seconds of the output. From 4.0 to 4.9 seconds the cloud bank comes down from the top edge and fills the frame top first while clip A zooms in around its center from 1.0 to 1.25 and slides down by 18% of the frame height (smoothstep easing), as if the camera were rising. The frame is fully covered from 4.9 to 5.1 seconds and the cut from A to B happens hidden behind it at 5.0 seconds. From 5.1 to 6.0 seconds a hole opens in the middle of the frame and widens out to the corners, while clip B settles from 1.25 zoom back to 1.0 (smoothstep), so at 5.45 seconds the center of B is clear and the top and bottom edges are still cloud. 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 cloud-transition.py and run:

python3 cloud-transition.py clipA.mp4 clipB.mp4 output.mp4
Show the script
#!/usr/bin/env python3
"""Cloud transition: the camera climbs out of clip A into a white cloud bank, then the clouds part from the middle to reveal clip B.

Usage: python3 cloud-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 white around the cut
LIGHT = (248, 249, 251)     # sunlit cloud color
SHADOW = (206, 212, 222)    # cloud color in the shadowed folds, a cool gray
RISE = 900                  # how fast the clouds stream down past the camera, in pixels per second
CLIMB = 0.18                # how far clip A sinks as the camera rises, as a share of the frame height
PUSH = 1.25                 # zoom at the moment A disappears, and B's zoom when it first shows (settles to 1)
BLOB = 150                  # rough size of the cloud puffs, in pixels
SOFT = 0.14                 # width of the feathered cloud edge (0 = hard edge)
SEED = 7                    # change for a different cloud 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 at the edges: 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 camera(img, zoom, drop):
    """The frame scaled up by zoom around its center and moved down by drop pixels (the camera rising past it)."""
    rows = np.clip(((ys - h / 2 - drop) / zoom + h / 2).astype(int), 0, h - 1)
    cols = np.clip(((xs - w / 2) / zoom + w / 2).astype(int), 0, w - 1)
    return img[rows][:, cols]


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


def cloud_layer(p):
    """Cloud density (0-1) and shading at progress p: the puffs stream downward and swell as the camera flies in."""
    swell = 1 + 0.6 * p
    rows = ((ys - h / 2) / swell + h / 2 - RISE * p * D).astype(int) % puffs.shape[0]
    cols = ((xs - w / 2) / swell + w / 2).astype(int) % puffs.shape[1]
    return puffs[rows][:, cols], folds[rows][:, cols]


def frame_at(a, b, p):
    """Frame at progress p (0-1): clouds roll in from the top over A, hold, then part from the center over B."""
    density, shade = cloud_layer(p)
    if p < 0.5:
        amount = min(p / (0.5 - HOLD / 2), 1)
        k = ease(amount)
        base = camera(a, 1 + (PUSH - 1) * k, CLIMB * h * k)
        threshold = 0.5 * density + 0.5 * y                          # the cloud bank comes down from above
    else:
        amount = min((1 - p) / (0.5 - HOLD / 2), 1)
        base = camera(b, 1 + (PUSH - 1) * ease(amount), 0)
        threshold = 0.5 * density + 0.5 * (1 - radius)                # a hole opens in the middle and widens
    alpha = np.clip((amount * (1 + SOFT) - threshold) / SOFT, 0, 1)[..., None]
    color = light + (shadow - light) * 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) ** 2
light, shadow = np.array(LIGHT, np.float32), np.array(SHADOW, np.float32)
ys, xs = np.arange(h, dtype=np.float32), np.arange(w, dtype=np.float32)
y = np.linspace(0, 1, h, dtype=np.float32)[:, None] * np.ones((1, w), np.float32)
radius = np.hypot((xs[None, :] - w / 2) / (w / 2), (ys[:, None] - h / 2) / (h / 2)) / np.sqrt(2)
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 = frame_at(a, next(b_frames), (i - start + 0.5) / 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")

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