How to add a burning text with Claude

Charred "HOT DROP" letters with a glowing orange rim catch fire, and tongues of flame flicker up from the top of every letter.

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

    Add a burning text to input.mp4
    Text: HOT DROP

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.

Add a burning text to @clip1.mp4, put a sale badge on @clip2.mp4, give @clip3.mp4 a VHS look, then join all three with crossfades.

Settings you can change

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

SettingDefaultWhat it does
TEXTHOT DROPThe headline.
START0.5Second the letters appear and catch fire.
IGNITE0.6Seconds the flames take to grow to full height.
FLAME0.12Tallest flame tongue, as a share of the frame height.
RISE1.4How fast the flames lick upwards, in frame heights per second.
LETTERS(30, 18, 12)Charred letter color.
SEED7Change for a different pattern of flames.

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
Add burning text to input.mp4: "HOT DROP" in near-black (30, 18, 12) Arial Black (or another heavy bold font) at an 81 pixel font size (15% of the frame width), centered horizontally 20% of the way down, with a 3 pixel orange (255, 120, 20) outline blurred by 1.5 pixels so the letters glow at the edges. Nothing shows before 0.5 seconds; at 0.5 seconds the letters appear and flames start rising from the top edge of every letter, growing linearly to full height by 1.1 seconds and burning to the end. Draw the flames with numpy: for each pixel, measure how far it sits above the nearest letter pixel below it in the same column; flame tongues reach up to 12% of the frame height (115 pixels), each column's height set by smooth random noise (about 30 cells across the frame) that scrolls upward at 1.4 frame heights per second so the flames flicker and lick upwards, with the tips leaning up to 2% of the frame width left and right. Heat is 1 at the letter edge and 0 at the tip; color it from near white (255, 240, 190) through amber (255, 180, 40), orange (250, 100, 10) and red (190, 30, 0) to dark ember (60, 8, 0), soft only at the tips, never hotter than the pixel just below so no specks float free, and blur the flames by 1.5 pixels. Draw the letters on top of the flames, and keep fire off the rest of the frame. Use Python with Pillow to draw the text (don't rely on ffmpeg's drawtext filter), numpy for the flames and ffmpeg to put it on the video, keep the audio, and save it as output.mp4.

Run it without Claude

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

python3 burning-text.py input.mp4 output.mp4
Show the script
#!/usr/bin/env python3
"""Burning text: charred letters with a glowing ember rim catch fire, and flames flicker up from their tops.

Usage: python3 burning-text.py input.mp4 output.mp4
Needs ffmpeg, Pillow and numpy (pip install pillow numpy). The flames are drawn from random noise, so no stock fire clip is needed.
"""
import json
import math
import subprocess
import sys
from pathlib import Path

import numpy as np
from PIL import Image, ImageDraw, ImageFilter, ImageFont

TEXT = "HOT DROP"          # the headline
START = 0.5                # second the letters appear
IGNITE = 0.6               # seconds the flames take to grow to full height after START
Y = 0.2                    # text center, 0-1 down the frame
SIZE = 0.15                # font size as a share of the frame width
LETTERS = (30, 18, 12)     # charred letter color
RIM = (255, 120, 20)       # ember glow around the letters
FLAME = 0.12               # tallest flame tongue, as a share of the frame height
RISE = 1.4                 # how fast the flame texture climbs, in frame heights per second
SWAY = 0.04                # how far the flame tips lean left and right, as a share of the frame width
HEAT = [0, 0.25, 0.5, 0.75, 1.0]                                                    # flame heat...
COLORS = [(60, 8, 0), (190, 30, 0), (250, 100, 10), (255, 180, 40), (255, 240, 190)]  # ...and its color: ember, red, orange, amber, near white
SEED = 7                   # change for different flames
FONTS = ["/System/Library/Fonts/Supplemental/Arial Black.ttf",    # macOS
         "/System/Library/Fonts/Supplemental/Arial Bold.ttf",
         "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",  # Linux
         "C:/Windows/Fonts/ariblk.ttf"]                           # Windows
FONT_FILE = next((f for f in FONTS if Path(f).exists()), None) or sys.exit("No bold font found: add one to FONTS")


def letters():
    """The charred letters with their ember rim (RGBA), and their shape as a 0/1 mask."""
    font = ImageFont.truetype(FONT_FILE, round(w * SIZE))
    shape = Image.new("L", (w, h))
    ImageDraw.Draw(shape).text((w / 2, h * Y), TEXT, font=font, anchor="mm", fill=255)
    rim = Image.new("L", (w, h))
    ImageDraw.Draw(rim).text((w / 2, h * Y), TEXT, font=font, anchor="mm", fill=255, stroke_width=3)
    img = Image.new("RGBA", (w, h), RIM)
    img.putalpha(rim.filter(ImageFilter.GaussianBlur(1.5)))
    fill = Image.new("RGBA", (w, h), LETTERS)
    fill.putalpha(shape)
    img.alpha_composite(fill)
    return img, np.asarray(shape) > 127


def height_above(mask):
    """For every pixel, how far up it sits from the nearest letter below it, in frame heights (0 on a letter,
    infinite with no letter below)."""
    d = np.full(mask.shape, np.inf, np.float32)
    for y in range(h - 2, -1, -1):
        d[y] = np.where(mask[y], 0, d[y + 1] + 1)
    return d / h


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 flames(t):
    """The fire above the letters at time t, as an RGBA image."""
    grow = min(max((t - START) / IGNITE, 0), 1)
    climb = ys + RISE * t                                     # the texture scrolls up, so the flames lick upwards
    flicker = 0.6 * noise(xs, climb, 30, 6, 1) + 0.4 * noise(xs, climb * 1.7, 70, 14, 2)
    reach = FLAME * grow * (0.25 + 1.2 * flicker ** 2)        # each tongue of flame reaches a different height
    lean = SWAY * w * (noise(xs, climb * 0.6, 6, 3, 3) - 0.5) * np.clip((Y * h - ys * h) / (FLAME * h), 0, 1)
    cols = np.clip(np.round(px + lean), 0, w - 1).astype(int)  # higher up, read the height from a column off to the side
    d = np.take_along_axis(above, cols, axis=1)
    air = (d > 0) & np.isfinite(d)                            # above a letter, not on one
    heat = np.where(air, np.clip(1 - d / np.maximum(reach, 1e-6), 0, 1), 1)
    heat = np.minimum.accumulate(heat[::-1], axis=0)[::-1] * air  # never hotter than just below: no floating specks
    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)                         # soft only at the very tips
    fire = Image.fromarray(np.dstack([color, alpha * 255]).astype(np.uint8))
    return fire.filter(ImageFilter.GaussianBlur(1.5))


def overlay(t):
    if t < START:
        return blank
    img = flames(t)
    img.alpha_composite(text)
    return img


def probe(path):
    info = json.loads(subprocess.check_output(["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries",
                                               "stream=width,height,r_frame_rate:format=duration", "-of", "json", path]))
    s = info["streams"][0]
    num, den = s["r_frame_rate"].split("/")
    return s["width"], s["height"], s["r_frame_rate"], float(num) / float(den), float(info["format"]["duration"])


def render(src, out):
    """Draw overlay(t) on every frame of src and save to out, keeping the audio."""
    ff = subprocess.Popen(["ffmpeg", "-y", "-v", "error", "-i", src,
                           "-f", "rawvideo", "-pix_fmt", "rgba", "-s", f"{w}x{h}", "-r", rate, "-i", "-",
                           "-filter_complex", "[0:v][1:v]overlay=shortest=1,format=yuv420p[v]",
                           "-map", "[v]", "-map", "0:a?", "-c:v", "libx264", "-crf", "18", "-c:a", "copy", out],
                          stdin=subprocess.PIPE)
    for i in range(math.ceil(duration * fps)):
        ff.stdin.write(overlay(i / fps).tobytes())
    ff.stdin.close()
    if ff.wait():
        sys.exit("ffmpeg failed")


src, out = sys.argv[1:3]
w, h, rate, fps, duration = probe(src)
ys, xs = np.mgrid[0:h, 0:w] / np.array([h, w]).reshape(2, 1, 1)
px = xs * w
text, mask = letters()
above = height_above(mask)
blank = Image.new("RGBA", (w, h))
render(src, out)

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