How to add a retro grid visualizer with Claude

The clip shrinks into a glowing square card above a synthwave sunset: a striped yellow-to-pink sun sits on the horizon, a magenta grid floor rolls one row toward you every beat, and a row of cyan bars along the horizon leaps with the kick and hi-hats.

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 retro grid visualizer to input.mp4
    Where the sky meets the floor, as a share of the frame height: 0.62
    Sun diameter as a share of the frame width: 0.64

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 retro grid visualizer 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
HORIZON0.62Where the sky meets the floor, as a share of the frame height.
SUN_SIZE0.64Sun diameter as a share of the frame width.
SUN_TOP, SUN_LOW(255, 215, 70), (255, 60, 140)Sun color at its top and at the horizon.
GRID(255, 40, 200)Grid line color.
BEAT0.5Seconds for the grid to scroll one row toward the viewer; 60 / BPM keeps it on the beat.
BARS28Number of bars along the horizon, low notes on the left.
BAR_COLOR(60, 235, 255)Bar and card border color.
MAX_HEIGHT0.10Tallest bar as a share of the frame height.
RANGE36Decibels between an empty bar and a full one.
FALL0.80Share of its height a bar keeps each frame as the sound drops.
CARD0.60Width of the square video card as a share of the frame width.

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
Turn input.mp4 into a synthwave music visualizer driven by its own audio, on every frame from start to end. Build the scene in Python with numpy and Pillow: the horizon sits 62% of the way down. The sky above is a vertical gradient from (12, 4, 35) at the top to (110, 25, 120) at the horizon; the floor below runs from (45, 8, 75) at the horizon to (14, 2, 30) at the bottom. A half sun 64% of the frame width across sits on the horizon, centered across, colored from (255, 215, 70) at its top to (255, 60, 140) at the horizon, with 6 horizontal stripes cut through its lower half that get thicker toward the horizon, and a soft glow around it. The floor has a magenta (255, 40, 200) perspective grid with a 3-pixel line along the horizon: 11 lines fanning out from the center of the horizon to the bottom edge, spaced 32% of the frame width apart at the bottom, and horizontal rows where a row at distance z sits (bottom - horizon) / z below the horizon, rows at z = n - shift for n = 1, 2, 3..., with shift = (time / 0.5) mod 1 so the grid scrolls exactly one row toward the viewer every 0.5 seconds; rows are max(2, round(6 / z)) pixels thick. Along the horizon stand 28 cyan (60, 235, 255) bars with rounded tops, spread from 4% to 96% across, each half its slot wide, standing on the horizon and at most 10% of the frame height tall, with a faint 30%-opacity reflection 0.4 times their height on the floor below. Bar heights come from an FFT of the audio: for each video frame take a 4096-sample Hann window centered on that frame's time, split 60 Hz to 12 kHz into 28 log-spaced bands (low notes on the left), convert each band's average magnitude to decibels, and scale each band so its 99th-percentile level over the clip is full height and 36 dB below that is empty; bars rise instantly and fall back by keeping 80% of their height per frame. Give the grid and bars an 8-pixel glow. Put the clip's center square (full width, centered vertically) scaled to 60% of the frame width in a card centered across with its top 6% down, with rounded corners and a 4-pixel cyan (60, 235, 255) glowing border. Pipe raw frames from and back to ffmpeg. Add no text (any text should be drawn with Pillow, not ffmpeg's drawtext). Keep the size, a 30fps frame rate, the 6 second length and the audio, encode H.264 yuv420p and save it as output.mp4.

Run it without Claude

This is the exact script we tested. Save it as retro-grid-visualizer.py and run:

python3 retro-grid-visualizer.py input.mp4 output.mp4
Show the script
#!/usr/bin/env python3
"""Retro grid visualizer: a neon grid floor scrolls toward a striped sunset sun while cyan bars on the horizon pulse to the track.

Usage: python3 retro-grid-visualizer.py input.mp4 output.mp4
The clip plays in a framed square card above the sun. Needs ffmpeg, numpy and Pillow (pip install numpy pillow).
"""
import json
import math
import subprocess
import sys

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

HORIZON = 0.62                  # where the sky meets the floor (0-1 down the frame)
SKY_TOP, SKY_LOW = (12, 4, 35), (110, 25, 120)    # sky color at the top and at the horizon
FLOOR_FAR, FLOOR_NEAR = (45, 8, 75), (14, 2, 30)  # floor color at the horizon and at the bottom
SUN_SIZE = 0.64                 # sun diameter as a share of the frame width
SUN_TOP, SUN_LOW = (255, 215, 70), (255, 60, 140)  # sun color at its top and at the horizon
GRID = (255, 40, 200)           # grid line color
COLUMNS = 11                    # grid lines fanning out from the vanishing point
BEAT = 0.5                      # seconds for the grid to scroll one row toward the viewer (120 BPM)
BARS = 28                       # spectrum bars along the horizon, low notes on the left
BAR_COLOR = (60, 235, 255)      # bar color
MAX_HEIGHT = 0.10               # tallest bar as a share of the frame height
LOW_HZ, HIGH_HZ = 60, 12000     # frequency range the bars cover
RANGE = 36                      # decibels between an empty bar and a full one
FALL = 0.80                     # share of its height a bar keeps each frame as the sound drops
CARD = 0.60                     # width of the square video card as a share of the frame width
CARD_TOP = 0.06                 # top of the card (0-1 down the frame)
GLOW = 8                        # glow blur radius in pixels
RATE = 48000                    # audio sample rate used for the analysis
FPS = 30                        # output frame rate


def gradient(top, bottom, height):
    """A vertical color ramp, `height` rows tall and one pixel wide."""
    mix = np.linspace(0, 1, height)[:, None]
    return (np.array(top) * (1 - mix) + np.array(bottom) * mix)[:, None, :]


def backdrop():
    """The still part of the scene: sky, floor and the striped sun sitting on the horizon."""
    sky = np.broadcast_to(gradient(SKY_TOP, SKY_LOW, hy), (hy, w, 3))
    floor = np.broadcast_to(gradient(FLOOR_FAR, FLOOR_NEAR, h - hy), (h - hy, w, 3))
    img = Image.fromarray(np.concatenate([sky, floor]).astype(np.uint8))
    r = round(w * SUN_SIZE / 2)
    sun = Image.fromarray(np.broadcast_to(gradient(SUN_TOP, SUN_LOW, r), (r, 2 * r, 3)).astype(np.uint8))
    mask = Image.new("L", (2 * r, r))
    draw = ImageDraw.Draw(mask)
    draw.ellipse([0, 0, 2 * r, 2 * r], fill=255)
    for i in range(6):  # stripes cut through the lower half, thicker toward the horizon
        y = r * (0.5 + i * 0.09)
        draw.rectangle([0, y, 2 * r, y + 2 + i * 1.6], fill=0)
    layer = Image.new("RGBA", (w, h))
    layer.paste(sun, (w // 2 - r, hy - r), mask)
    glow = layer.filter(ImageFilter.GaussianBlur(GLOW * 3))
    glow.putalpha(glow.getchannel("A").point(lambda a: a // 2))
    return Image.alpha_composite(Image.alpha_composite(img.convert("RGBA"), glow), layer)


def spectrum(src):
    """Height (0-1) of every bar on every frame, from an FFT of the audio around that frame."""
    pcm = subprocess.check_output(["ffmpeg", "-v", "error", "-i", src, "-vn", "-ac", "1", "-ar", str(RATE),
                                   "-f", "f32le", "-"])
    audio = np.frombuffer(pcm, np.float32)
    size = 4096
    window = np.hanning(size)
    freqs = np.fft.rfftfreq(size, 1 / RATE)
    edges = np.geomspace(LOW_HZ, HIGH_HZ, BARS + 1)
    bands = [(freqs >= lo) & (freqs < hi) for lo, hi in zip(edges, edges[1:])]
    padded = np.concatenate([np.zeros(size), audio, np.zeros(size)])
    levels = []
    for i in range(frames):
        mid = size + round(i / FPS * RATE)
        mag = np.abs(np.fft.rfft(padded[mid - size // 2:mid + size // 2] * window))
        levels.append([20 * np.log10(mag[b].mean() + 1e-9) for b in bands])
    db = np.array(levels)
    top = np.percentile(db, 99, axis=0)
    heights = np.clip((db - (top - RANGE)) / RANGE, 0, 1)
    for i in range(1, frames):  # rise at once, fall back gently
        heights[i] = np.maximum(heights[i], heights[i - 1] * FALL)
    return heights


def neon(t, levels):
    """The moving lines: grid rows sliding toward the viewer, and the bars with a faint reflection on the floor."""
    img = Image.new("RGBA", (w, h))
    draw = ImageDraw.Draw(img)
    depth = h - hy
    for k in range(-COLUMNS // 2 + 1, COLUMNS // 2 + 1):
        draw.line([(w / 2, hy), (w / 2 + k * w * 0.32, h)], fill=GRID, width=3)
    shift = (t / BEAT) % 1
    for n in range(1, 40):  # a row at distance z sits depth / z below the horizon
        z = n - shift
        if z > 0.95:
            draw.line([(0, hy + depth / z), (w, hy + depth / z)], fill=GRID, width=max(2, round(6 / z)))
    draw.line([(0, hy), (w, hy)], fill=GRID, width=3)
    slot = 0.92 * w / BARS
    bar_w = slot * 0.5
    for i, level in enumerate(levels):
        x0 = 0.04 * w + i * slot + (slot - bar_w) / 2
        tall = max(bar_w, level * MAX_HEIGHT * h)
        draw.rounded_rectangle([x0, hy - tall, x0 + bar_w, hy - 2], radius=bar_w / 2, fill=BAR_COLOR)
        draw.rectangle([x0, hy + 3, x0 + bar_w, hy + 3 + tall * 0.4], fill=BAR_COLOR + (70,))
    return Image.alpha_composite(img.filter(ImageFilter.GaussianBlur(GLOW)), img)


def card(frame):
    """The clip's center square, scaled into the card."""
    side = round(w * CARD)
    sq = min(w, h)
    crop = frame.crop(((w - sq) // 2, (h - sq) // 2, (w + sq) // 2, (h + sq) // 2)).resize((side, side), Image.LANCZOS)
    mask = Image.new("L", (side, side))
    ImageDraw.Draw(mask).rounded_rectangle([0, 0, side - 1, side - 1], radius=side // 14, fill=255)
    return crop, mask


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]))
    s = info["streams"][0]
    return s["width"], s["height"], float(info["format"]["duration"])


def render(src, out, heights):
    """Build the scene for every frame of src, the clip in its card, and save to out with the audio."""
    reader = subprocess.Popen(["ffmpeg", "-v", "error", "-i", src, "-vf", f"fps={FPS}", "-f", "rawvideo", "-pix_fmt", "rgb24", "-"],
                              stdout=subprocess.PIPE)
    writer = subprocess.Popen(["ffmpeg", "-y", "-v", "error", "-f", "rawvideo", "-pix_fmt", "rgb24", "-s", f"{w}x{h}",
                               "-r", str(FPS), "-i", "-", "-i", src, "-map", "0:v", "-map", "1:a?", "-c:v", "libx264",
                               "-crf", "18", "-pix_fmt", "yuv420p", "-c:a", "copy", "-shortest", out],
                              stdin=subprocess.PIPE)
    still = backdrop()
    side = round(w * CARD)
    x, y = (w - side) // 2, round(h * CARD_TOP)
    border = Image.new("RGBA", (w, h))
    ImageDraw.Draw(border).rounded_rectangle([x - 4, y - 4, x + side + 3, y + side + 3], radius=side // 14 + 4,
                                             outline=BAR_COLOR, width=4)
    border = Image.alpha_composite(border.filter(ImageFilter.GaussianBlur(GLOW)), border)
    for i, levels in enumerate(heights):
        buf = reader.stdout.read(w * h * 3)
        if len(buf) < w * h * 3:
            break
        scene = Image.alpha_composite(still, neon(i / FPS, levels))
        scene = Image.alpha_composite(scene, border).convert("RGB")
        clip, mask = card(Image.frombytes("RGB", (w, h), buf))
        scene.paste(clip, (x, y), mask)
        writer.stdin.write(scene.tobytes())
    writer.stdin.close()
    reader.stdout.close()
    if writer.wait():
        sys.exit("ffmpeg failed")


src, out = sys.argv[1:3]
w, h, duration = probe(src)
hy = round(h * HORIZON)
frames = math.ceil(duration * FPS)
render(src, out, spectrum(src))

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