---
title: "How to add an animated text frame with Claude (tested prompt and script)"
description: "Two white lines race out from opposite corners of an empty box above the bottle, meet to close it, and \"NEW FORMULA\" fades in inside. Install one skill and ask Claude Code, or run the Python script we tested yourself."
url: https://www.recharm.com/effects/animated-text-frame
---

# How to add an animated text frame with Claude

Two white lines race out from opposite corners of an empty box above the bottle, meet to close it, and "NEW FORMULA" fades in inside.

Made by the script on this page, from our own test clip.

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

   ```
   Add an animated text frame to input.mp4
   Text: NEW FORMULA
   ```

## 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 an animated text frame 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.

| Setting   | Default                    | What it does                                           |
| --------- | -------------------------- | ------------------------------------------------------ |
| `TEXT`    | `NEW FORMULA`              | The title inside the frame.                            |
| `START`   | `0.5`                      | Second the frame starts drawing.                       |
| `DRAW`    | `0.8`                      | Seconds the frame takes to close.                      |
| `TEXT_IN` | `1.1`                      | Second the title starts fading in.                     |
| `BOX`     | `(0.08, 0.08, 0.92, 0.20)` | Frame left, top, right, bottom as shares of the frame. |
| `LINE`    | `6`                        | Frame line thickness in pixels.                        |
| `COLOR`   | `(255, 255, 255)`          | Frame and title color.                                 |

## Without the skill

Don’t want to install the skill? Paste this prompt into Claude Code and Claude writes the script itself.

```
Add an animated text frame to input.mp4. A white (255, 255, 255) rectangle outline 6 pixels thick, its corners at 8% and 92% across and 8% and 20% down the frame, draws itself on from 0.5 to 1.3 seconds: two strokes start together, one at the top-left corner going right along the top edge and down the right side, the other at the bottom-right corner going left along the bottom edge and up the left side, each covering half the outline with ease-in-out timing so they meet at the far corners at 1.3 seconds. Nothing is drawn before 0.5 seconds. From 1.1 to 1.5 seconds the title "NEW FORMULA" in bold white, 49 pixels tall (9% of the frame width), fades in centered inside the box (50% across, 14% down). Give the lines and text a soft dark plum (40, 20, 60) shadow blurred by 4 pixels. The frame and title then stay to the end. Use Python with Pillow to draw it and ffmpeg to put it on the video (don't rely on ffmpeg's drawtext filter), keep the audio, and save it as output.mp4.
```

## Run it without Claude

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

```
python3 animated-text-frame.py input.mp4 output.mp4
```

```
#!/usr/bin/env python3
"""Animated text frame: a thin white box draws itself around a title from two opposite corners, then the title fades in.

Usage: python3 animated-text-frame.py input.mp4 output.mp4
Needs ffmpeg and Pillow (pip install pillow).
"""
import json
import math
import subprocess
import sys
from pathlib import Path

from PIL import Image, ImageDraw, ImageFilter, ImageFont

TEXT = "NEW FORMULA"           # the title inside the frame
START = 0.5                    # second the frame starts drawing
DRAW = 0.8                     # seconds the frame takes to close
TEXT_IN = 1.1                  # second the title starts fading in
TEXT_FADE = 0.4                # seconds the title fade takes
BOX = (0.08, 0.08, 0.92, 0.20) # frame left, top, right, bottom (0-1 of the frame)
LINE = 6                       # frame line thickness in pixels
SIZE = 0.09                    # title height as a share of the frame width
COLOR = (255, 255, 255)        # frame and title color
SHADOW = (40, 20, 60)          # soft shadow under the frame and title
FONTS = ["/System/Library/Fonts/Supplemental/Arial Bold.ttf",     # macOS
         "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",  # Linux
         "C:/Windows/Fonts/arialbd.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 trace(path, length):
    """The first `length` pixels of a path given as a list of corner points."""
    points = [path[0]]
    for (x0, y0), (x1, y1) in zip(path, path[1:]):
        seg = math.hypot(x1 - x0, y1 - y0)
        if length >= seg:
            points.append((x1, y1))
            length -= seg
        else:
            points.append((x0 + (x1 - x0) * length / seg, y0 + (y1 - y0) * length / seg))
            break
    return points

def with_shadow(draw_fn):
    """Run draw_fn(draw, fill) on a transparent frame, once blurred in SHADOW and once sharp in COLOR."""
    shadow, img = Image.new("RGBA", (w, h)), Image.new("RGBA", (w, h))
    draw_fn(ImageDraw.Draw(shadow), SHADOW + (200,))
    img.alpha_composite(shadow.filter(ImageFilter.GaussianBlur(4)))
    draw_fn(ImageDraw.Draw(img), COLOR)
    return img

def frame_lines(p):
    """The box drawn p of the way (0-1): one stroke from the top-left corner clockwise, one from the bottom-right."""
    x0, y0, x1, y1 = BOX[0] * w, BOX[1] * h, BOX[2] * w, BOX[3] * h
    half = (x1 - x0) + (y1 - y0)
    strokes = [trace([(x0, y0), (x1, y0), (x1, y1)], p * half),
               trace([(x1, y1), (x0, y1), (x0, y0)], p * half)]

    def draw(d, fill):
        for pts in strokes:
            d.line(pts, fill=fill, width=LINE, joint="curve")
            for x, y in (pts[0], pts[-1]):   # square off the stroke ends and corners
                d.rectangle([x - LINE / 2, y - LINE / 2, x + LINE / 2, y + LINE / 2], fill=fill)
    return with_shadow(draw)

def title():
    font = ImageFont.truetype(FONT_FILE, round(w * SIZE))
    center = (w * (BOX[0] + BOX[2]) / 2, h * (BOX[1] + BOX[3]) / 2)
    return with_shadow(lambda d, fill: d.text(center, TEXT, font=font, anchor="mm", fill=fill))

def ease_in_out(p):
    p = min(max(p, 0), 1)
    return 4 * p ** 3 if p < 0.5 else 1 - (-2 * p + 2) ** 3 / 2

def overlay(t):
    if t < START:
        return blank
    p = ease_in_out((t - START) / DRAW)
    img = closed.copy() if p >= 1 else frame_lines(p)
    a = min(max((t - TEXT_IN) / TEXT_FADE, 0), 1)
    if a > 0:
        faded = text.copy()
        faded.putalpha(text.getchannel("A").point(lambda v: round(v * a)))
        img.alpha_composite(faded)
    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)
blank = Image.new("RGBA", (w, h))
closed = frame_lines(1)
text = title()
render(src, out)
```

## More text and titles

- [Brush stroke text reveal **Brush stroke text reveal**](https://www.recharm.com/effects/brush-stroke-text-reveal)
- [Burning text **Burning text**](https://www.recharm.com/effects/burning-text)
- [Drop shadow text **Drop shadow text**](https://www.recharm.com/effects/drop-shadow-text)
- [Echo text **Echo text**](https://www.recharm.com/effects/echo-text)
- [3D extruded text **3D extruded text**](https://www.recharm.com/effects/extruded-3d-text)

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