How to add a fire transition with Claude

A ragged wall of orange flames climbs from the bottom of the frame to the top, and clip B is left behind wherever it has burned through clip A.

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 fire transition
    Colors: ember, red, orange, amber, near white

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 fire transition.

Settings you can change

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

SettingDefaultWhat it does
D1.5Length of the transition in seconds, from the first flame tips to the last.
FLAME0.35Flame height above the burning edge, as a share of the frame height.
RAGGED0.04How far the burning edge wobbles up and down.
RISE1.2How fast the flames lick upwards, in frame heights per second.
COLORSember, red, orange, amber, near whiteFlame colors from the tips to the 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 with a 1.5 second fire transition starting 1.5 seconds before the end of A, so the output is 10.5 seconds long. A horizontal burning edge rises from below the frame to above it at a steady speed: its height runs from 1.35 frame heights down to -0.35 (0 is the top, 1 the bottom) over the 1.5 seconds, wobbling up and down by 0.04 frame heights with smooth noise across the width. Below the edge show B, above it show A. On top of the edge draw flames up to 0.35 frame heights tall: smooth random noise that scrolls upward at 1.2 frame heights per second sets each tongue's height between 30% and 120% of that, colored from near white (255, 245, 200) and amber (255, 180, 40) at the edge through orange (250, 100, 10) and red (180, 25, 0) to dark ember (40, 5, 0) at the tips, opaque except the top 30% of each tongue, with a 0.01 frame height white-hot line right on the edge. Darken A to 15% in the 0.03 frame heights just above the edge. Crossfade the audio over the same 1.5 seconds. Match B's size to A, output 30fps H.264 yuv420p. Use Python with numpy to draw the frames and ffmpeg to read and write the video, and save it as output.mp4.

Run it without Claude

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

python3 fire-transition.py clipA.mp4 clipB.mp4 output.mp4
Show the script
#!/usr/bin/env python3
"""Fire transition: a wall of flames rises from the bottom of the frame and burns clip A away to reveal clip B.

Usage: python3 fire-transition.py clipA.mp4 clipB.mp4 output.mp4  (both clips need an audio track)
Needs ffmpeg and numpy (pip install numpy). The flames are drawn from random noise, so no stock fire clip is needed.
"""
import json
import subprocess
import sys

import numpy as np

D = 1.5                  # transition length in seconds, from the first flame tips to the last
FLAME = 0.35             # flame height above the burning edge, as a share of the frame height
RAGGED = 0.04            # how far the burning edge wobbles up and down
CHAR = 0.03              # height of the scorched band just above the edge
RISE = 1.2               # how fast the flame texture climbs, in frame heights per second
HEAT = [0, 0.25, 0.5, 0.75, 1.0]                                                    # flame heat...
COLORS = [(40, 5, 0), (180, 25, 0), (250, 100, 10), (255, 180, 40), (255, 245, 200)]  # ...and its color: ember, red, orange, amber, near white
SEED = 3                 # change for different flames


def noise(x, y, cols, rows, layer):
    """Smooth random values in 0-1 at x, y (0-1 across and down; y may run past 1), on a cols x rows grid."""
    grid = np.random.default_rng([SEED, layer]).random((64, cols + 1))
    gx, gy = x * cols, y * rows
    ix, iy = np.floor(gx).astype(int), np.floor(gy).astype(int)
    fx, fy = gx - ix, gy - iy
    fx, fy = fx * fx * (3 - 2 * fx), fy * fy * (3 - 2 * fy)
    iy0, iy1, ix1 = iy % 64, (iy + 1) % 64, np.minimum(ix + 1, cols)
    top = grid[iy0, ix] * (1 - fx) + grid[iy0, ix1] * fx
    bottom = grid[iy1, ix] * (1 - fx) + grid[iy1, ix1] * fx
    return top * (1 - fy) + bottom * fy


def burn(frame_a, frame_b, t):
    """Clip A burning away to clip B, u = 0 -> 1 through the transition."""
    u = (t - offset) / D
    edge = 1 + FLAME - (1 + 2 * FLAME) * u + RAGGED * (noise(xs[0], t * 0.8, 9, 1, 0) - 0.5)
    d = edge[None, :] - ys                                   # distance above the burning edge (negative: already burned)
    picture = np.where((d < 0)[..., None], frame_b, frame_a * np.clip(d / CHAR, 0.15, 1)[..., None])
    climb = ys + RISE * t                                    # the texture scrolls up, so the flames lick upwards
    flicker = 0.6 * noise(xs, climb, 9, 5, 1) + 0.4 * noise(xs, climb * 1.7, 22, 10, 2)
    reach = FLAME * (0.3 + 0.9 * flicker)                     # each tongue of flame reaches a different height
    heat = np.clip(1 - d / reach, 0, 1) * (d > 0)             # hottest at the edge, cooling towards the tips
    heat = np.maximum(0.85 * heat, np.exp(-(d / 0.01) ** 2))  # white-hot line right on the edge
    color = np.stack([np.interp(heat, HEAT, [c[k] for c in COLORS]) for k in range(3)], axis=2)
    alpha = np.clip(heat / 0.3, 0, 1)[..., None]               # soft only at the very tips
    return picture * (1 - alpha) + color * alpha


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):
    """Decode a clip at the output's size and frame rate, one RGB array per frame."""
    dec = subprocess.Popen(["ffmpeg", "-v", "error", "-i", path, "-vf",
                            f"scale={w}:{h}:force_original_aspect_ratio=increase,crop={w}:{h},fps={fps}",
                            "-f", "rawvideo", "-pix_fmt", "rgb24", "-"], stdout=subprocess.PIPE)
    while raw := dec.stdout.read(w * h * 3):
        yield np.frombuffer(raw, np.uint8).reshape(h, w, 3).astype(np.float32)


a, b, out = sys.argv[1:4]
w, h, len_a = probe(a)
fps = 30
offset = len_a - D
ys, xs = np.mgrid[0:h, 0:w] / np.array([h, w]).reshape(2, 1, 1)
enc = subprocess.Popen(["ffmpeg", "-y", "-v", "error", "-i", a, "-i", b,
                        "-f", "rawvideo", "-pix_fmt", "rgb24", "-s", f"{w}x{h}", "-r", str(fps), "-i", "-",
                        "-filter_complex", f"[0:a][1:a]acrossfade=d={D}[au]",
                        "-map", "2:v", "-map", "[au]", "-c:v", "libx264", "-crf", "18", "-pix_fmt", "yuv420p", "-c:a", "aac", out],
                       stdin=subprocess.PIPE)
clip_a, clip_b = frames(a), frames(b)
i = 0
for frame_a in clip_a:                                       # A on its own, then A burning into the start of B
    t = i / fps
    picture = frame_a if t < offset else burn(frame_a, next(clip_b), t)
    enc.stdin.write(picture.clip(0, 255).astype(np.uint8).tobytes())
    i += 1
for frame_b in clip_b:                                       # the rest of B
    enc.stdin.write(frame_b.astype(np.uint8).tobytes())
enc.stdin.close()
sys.exit("ffmpeg failed" if enc.wait() else 0)

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