How to add a sci-fi HUD overlay with Claude

At half a second mint-green corner brackets slide in, a ringed crosshair with two spinning arcs closes onto the center, and "TARGET LOCKED", a running timecode and range readouts appear, as if seen through a visor.

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 sci-fi HUD overlay to input.mp4
    Label: TARGET LOCKED

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 sci-fi hud overlay 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
START0.5Second the HUD starts booting up.
BOOT0.5Seconds the brackets and reticle take to slide into place.
COLOR(80, 255, 230)Line and text color.
RETICLE0.20Reticle ring radius as a share of the frame width.
SPIN90Degrees per second the reticle's outer arcs turn.
LABELTARGET LOCKEDText under the reticle.

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 a sci-fi HUD overlay to input.mp4, all lines and text in (80, 255, 230) with a soft 4-pixel dark (0, 20, 30) shadow behind them. Before 0.5 seconds the video is untouched. From 0.5s, over 0.5 seconds with an ease-out: four L-shaped corner brackets (arms 70 pixels long, 3 pixels thick, corners 28 pixels in from each frame edge) slide in from 40 pixels further out, and a targeting reticle centered on the frame shrinks from 1.6x into place. The reticle is a 2-pixel ring with a radius of 20% of the frame width, four crosshair ticks from 0.75 to 1.2 times the radius on each side, a small 16-pixel cross in the middle, and two 60-degree arcs 5 pixels thick at 1.12 times the radius, opposite each other, turning clockwise at 90 degrees per second. Once it's in place (1.0s) show readouts in a monospaced font: "TARGET LOCKED" at 5.5% of the frame width tall, centered 34 pixels below the bottom crosshair tick, in a 2-pixel outlined box with a dark (0, 20, 30) fill at 67% opacity; a running "REC mm:ss:ff" timecode in the top-left corner and "SYS ONLINE" in the top-right corner, 14 pixels inside the brackets; and "RANGE 2.30 M" over "AZ 134.0" in the bottom-left corner, both numbers changing slowly. Text other than the label is 4% of the frame width tall. Draw it with Python and Pillow (don't use ffmpeg's drawtext filter), composite it with ffmpeg, keep the audio, and save it as output.mp4 at 30fps.

Run it without Claude

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

python3 hud-overlay.py input.mp4 output.mp4
Show the script
#!/usr/bin/env python3
"""HUD overlay: thin corner brackets, a spinning targeting reticle and live readouts boot up over the video.

Usage: python3 hud-overlay.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

START = 0.5                 # second the HUD starts booting up
BOOT = 0.5                  # seconds the brackets and reticle take to slide and shrink into place
COLOR = (80, 255, 230)      # HUD line and text color
LINE = 3                    # line thickness in pixels
INSET = 28                  # corner brackets' distance from the frame edge, in pixels
ARM = 70                    # length of each bracket arm, in pixels
RETICLE = 0.20              # reticle ring radius as a share of the frame width, centered on the frame
SPIN = 90                   # degrees per second the reticle's outer arcs turn
LABEL = "TARGET LOCKED"     # text under the reticle
FONTS = ["/System/Library/Fonts/Menlo.ttc",                         # macOS
         "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",     # Linux
         "C:/Windows/Fonts/consola.ttf"]                            # Windows
FONT_FILE = next((f for f in FONTS if Path(f).exists()), None) or sys.exit("No monospaced font found: add one to FONTS")


def brackets(draw, p):
    """L-shaped brackets in the four corners, sliding in from 40px further out as p goes 0 -> 1."""
    off = round(40 * (1 - p))
    for sx, sy in [(1, 1), (-1, 1), (1, -1), (-1, -1)]:
        x = (INSET - off) if sx > 0 else w - 1 - INSET + off
        y = (INSET - off) if sy > 0 else h - 1 - INSET + off
        draw.line([(x, y + sy * ARM), (x, y), (x + sx * ARM, y)], fill=COLOR, width=LINE, joint="curve")


def reticle(draw, t, p):
    """A ring with crosshair ticks and two spinning arcs, shrinking from 1.6x its size as p goes 0 -> 1."""
    cx, cy = w / 2, h / 2
    r = w * RETICLE * (1.6 - 0.6 * p)
    draw.ellipse([cx - r, cy - r, cx + r, cy + r], outline=COLOR, width=2)
    for dx, dy in [(1, 0), (-1, 0), (0, 1), (0, -1)]:
        draw.line([(cx + dx * r * 0.75, cy + dy * r * 0.75), (cx + dx * r * 1.2, cy + dy * r * 1.2)], fill=COLOR, width=LINE)
    draw.line([(cx - 8, cy), (cx + 8, cy)], fill=COLOR, width=2)
    draw.line([(cx, cy - 8), (cx, cy + 8)], fill=COLOR, width=2)
    R = r * 1.12
    turn = SPIN * (t - START)
    for a in (turn, turn + 180):
        draw.arc([cx - R, cy - R, cx + R, cy + R], a, a + 60, fill=COLOR, width=LINE + 2)


def readouts(draw, t):
    """Timecode top left, status top right, the label under the reticle and changing numbers bottom left."""
    frames = round(t * 30)
    draw.text((INSET + 14, INSET + 14), f"REC {frames // 30 // 60:02d}:{frames // 30 % 60:02d}:{frames % 30:02d}",
              font=small, fill=COLOR)
    draw.text((w - INSET - 14, INSET + 14), "SYS ONLINE", font=small, fill=COLOR, anchor="ra")
    y = h / 2 + w * RETICLE * 1.2 + 34
    box = draw.textbbox((w / 2, y), LABEL, font=big, anchor="mm")
    draw.rectangle([box[0] - 12, box[1] - 8, box[2] + 12, box[3] + 8], fill=(0, 20, 30, 170), outline=COLOR, width=2)
    draw.text((w / 2, y), LABEL, font=big, fill=COLOR, anchor="mm")
    draw.text((INSET + 14, h - INSET - 14), f"RANGE {2.4 - 0.1 * t:.2f} M\nAZ {134 + 7 * math.sin(t * 2):6.1f}",
              font=small, fill=COLOR, anchor="ld", spacing=6)


def overlay(t):
    img = Image.new("RGBA", (w, h))
    if t < START:
        return img
    p = ease_out(min((t - START) / BOOT, 1))
    draw = ImageDraw.Draw(img)
    brackets(draw, p)
    reticle(draw, t, p)
    if p >= 1:
        readouts(draw, t)
    shadow = Image.new("RGBA", (w, h), (0, 20, 30))
    shadow.putalpha(img.getchannel("A").filter(ImageFilter.GaussianBlur(4)).point(lambda a: min(255, a * 2)))
    shadow.alpha_composite(img)
    return shadow


def ease_out(p):
    return 1 - (1 - p) ** 3


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)
small = ImageFont.truetype(FONT_FILE, round(w * 0.04))
big = ImageFont.truetype(FONT_FILE, round(w * 0.055))
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