How to add a bullet time with Claude

Golden droplets burst out of the serum bottle and hang frozen in mid-air for three seconds while the camera keeps circling it, then rain down out of the shot.

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 bullet time to input.mp4
    Bottle: three boxes

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 bullet time 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
BURST0.5Second the droplets burst out.
FREEZE1.0Second they stop in mid-air.
RESUME4.0Second they start falling again.
ORBIT11Degrees a second the camera circles the bottle; match your footage so the droplets stay put in space.
ORIGIN(0.5, 0.47)Where the splash starts in the first frame (0-1 across and down).
DROPS60Number of droplets.
COLOR(245, 175, 20)Droplet color.
SEED7Change it for a different splash.
BOTTLEthree boxesThe product's outline (0-1 of the frame), which droplets behind it are hidden by.

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 bullet time splash to input.mp4 (540x960, 30fps, 6 seconds, a camera slowly orbiting a serum bottle that stands at the center). Use Python with numpy and Pillow for the droplets and ffmpeg to read and write the video. Make 60 golden (245, 175, 20) droplets with numpy's default_rng(7): each gets a random direction round the bottle (angle 0 to 360 degrees), an outward speed of 150 to 420 pixels a second, an upward speed of 350 to 950 pixels a second and a radius of 5 to 14 pixels. They burst at 0.5 seconds from a point at the bottle's center line 47% of the way down the frame and fly as 3D points (x right, y down, z toward the camera) with gravity of 1600 pixels a second squared. At 1.0 seconds stop their clock so they hang frozen in mid-air until 4.0 seconds, then let the clock run again so they fall out of the bottom of the frame (gone by 5.5 seconds). The whole time, turn the droplet cloud round the vertical axis through the burst point by 11 degrees a second (x' = x cos a - z sin a, z' = x sin a + z cos a, front moving left) to match the camera orbit, so the frozen droplets slide past each other as the camera circles, and project with perspective scale 1400 / (1400 - z'). Draw each droplet as a glossy sphere sprite: lit from the top left, darker at the rim and with a small white glint; while they fly, add three faint (30% opacity) copies 1/120 of a second apart behind each one as a short trail. Hide droplets behind the bottle (z' < 0) where the bottle is: rounded boxes from 34% to 66% across and 41% to 78% down (body), 40-60% across and 32-41% down (collar) and 44-56% across and 22-32% down (dropper). The bottle drifts sideways as the camera orbits, so follow it: each frame, find how far the patch 36-64% across and 20-44% down of the first frame has slid sideways (best match within 60 pixels) and move the burst point and the bottle boxes with it. Nothing is drawn before 0.5 seconds and the footage itself is not retimed, cropped or recolored. Don't add any text (if you do, draw it with Pillow, not ffmpeg's drawtext). Keep the size, frame rate and audio, and save it as output.mp4.

Run it without Claude

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

python3 bullet-time.py input.mp4 output.mp4
Show the script
#!/usr/bin/env python3
"""Bullet time: a splash of golden droplets bursts round the bottle and freezes in mid-air while the camera circles it.

Usage: python3 bullet-time.py input.mp4 output.mp4
Needs ffmpeg, Pillow and numpy (pip install pillow numpy). Made for a shot that orbits a still product.
"""
import json
import math
import subprocess
import sys

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

BURST = 0.5               # second the droplets burst out
FREEZE = 1.0              # second they stop in mid-air
RESUME = 4.0              # second they start falling again
ORBIT = 11                # degrees a second the camera circles the bottle (match your footage)
ORIGIN = (0.5, 0.47)      # where the splash starts in the first frame (0-1 across and down)
DROPS = 60                # number of droplets
COLOR = (245, 175, 20)    # droplet color
SEED = 7                  # change for a different splash
GRAVITY = 1600            # pixels a second squared
FOCAL = 1400              # perspective: smaller exaggerates depth
BOTTLE = [(0.34, 0.41, 0.66, 0.78), (0.40, 0.32, 0.60, 0.41), (0.44, 0.22, 0.56, 0.32)]  # body, collar, dropper
TRACK = (0.36, 0.20, 0.64, 0.44)  # part of the first frame followed to keep the splash on the bottle


def splash():
    """Each droplet's 3D start velocity (x right, y down, z toward the camera) and radius, in pixels."""
    rng = np.random.default_rng(SEED)
    angle = rng.uniform(0, 2 * math.pi, DROPS)
    out = rng.uniform(150, 420, DROPS)
    up = rng.uniform(350, 950, DROPS)
    vel = np.stack([np.cos(angle) * out, -up, np.sin(angle) * out], axis=1)
    return vel, rng.uniform(5, 14, DROPS)


def sprite(size=96):
    """A glossy golden sphere: lit from the top left, darker at the rim, with a white glint."""
    y, x = (np.mgrid[0:size, 0:size] + 0.5) / size * 2 - 1
    d = np.sqrt(x * x + y * y)
    z = np.sqrt(np.clip(1 - d * d, 0, 1))
    light = np.clip(-0.45 * x - 0.55 * y + 0.7 * z, 0, 1)
    glint = np.exp(-((x + 0.38) ** 2 + (y + 0.42) ** 2) / 0.02)
    rgb = np.clip(np.array(COLOR) * (0.45 + 0.75 * light)[..., None] + 255 * glint[..., None], 0, 255)
    alpha = np.clip((1 - d) * size / 2, 0, 1) * 255
    return Image.fromarray(np.dstack([rgb, alpha]).astype(np.uint8))


def flight_time(t):
    """Seconds of flight the droplets have had at output time t: the clock stops from FREEZE to RESUME."""
    if t < FREEZE:
        return t - BURST
    return FREEZE - BURST + max(0.0, t - RESUME)


def positions(t):
    """Screen offsets from the origin, depth scale and z of every droplet at time t, seen by the orbiting camera."""
    s = flight_time(t)
    p = vel * s + np.array([0, GRAVITY * s * s / 2, 0])
    a = math.radians(ORBIT * t)
    x = p[:, 0] * math.cos(a) - p[:, 2] * math.sin(a)
    z = p[:, 0] * math.sin(a) + p[:, 2] * math.cos(a)
    scale = FOCAL / (FOCAL - z)
    return x * scale, p[:, 1] * scale, scale, z


def layer(t, ox, oy, behind):
    """The droplets behind the bottle (or in front of it) at time t, with a faint trail while they fly."""
    img = Image.new("RGBA", (w, h))
    moving = t < FREEZE or t >= RESUME
    trail = [t - k / 120 for k in (3, 2, 1) if t - k / 120 >= BURST] if moving else []
    for when in trail + [t]:
        x, y, scale, z = positions(when)
        for i in np.argsort(z):
            if (z[i] < 0) != behind or z[i] > FOCAL / 2:  # skip droplets that reach the camera
                continue
            d = max(2, round(2 * radius[i] * scale[i]))
            drop = ball.resize((d, d), Image.LANCZOS)
            if when < t:
                drop.putalpha(drop.getchannel("A").point(lambda a: a * 0.3))
            img.alpha_composite(drop, (round(ox + x[i] - d / 2), round(oy + y[i] - d / 2)))
    if behind:
        img.putalpha(Image.composite(Image.new("L", (w, h)), img.getchannel("A"), bottle_mask(ox, oy)))
    return img


def bottle_mask(ox, oy):
    """The bottle's outline where it is now, which hides droplets that fly behind it."""
    m = Image.new("L", (w, h))
    draw = ImageDraw.Draw(m)
    dx, dy = ox - ORIGIN[0] * w, oy - ORIGIN[1] * h
    for x0, y0, x1, y1 in BOTTLE:
        draw.rounded_rectangle([x0 * w + dx, y0 * h + dy, x1 * w + dx, y1 * h + dy], radius=w * 0.03, fill=255)
    return m.filter(ImageFilter.GaussianBlur(2))


def track_box():
    return round(TRACK[0] * w), round(TRACK[1] * h), round(TRACK[2] * w), round(TRACK[3] * h)


def track(gray, template, search=60):
    """How far the bottle has slid sideways since the first frame: slide the first frame's patch to fit."""
    x0, y0, x1, y1 = track_box()
    errors = {dx: np.abs(gray[y0:y1:2, x0 + dx:x1 + dx:2] - template).mean()
              for dx in range(max(-search, -x0), min(search, w - x1) + 1)}
    return min(errors, key=errors.get)


def probe(path):
    info = json.loads(subprocess.check_output(["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries",
                                               "stream=width,height,r_frame_rate", "-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)


def render(src, out):
    """Read src frame by frame, follow the bottle, draw the droplets on it and save to out, keeping the audio."""
    dec = subprocess.Popen(["ffmpeg", "-v", "error", "-i", src, "-f", "rawvideo", "-pix_fmt", "rgb24", "-"],
                           stdout=subprocess.PIPE)
    enc = subprocess.Popen(["ffmpeg", "-y", "-v", "error", "-f", "rawvideo", "-pix_fmt", "rgb24", "-s", f"{w}x{h}",
                            "-r", rate, "-i", "-", "-i", src, "-map", "0:v", "-map", "1:a?",
                            "-c:v", "libx264", "-crf", "18", "-pix_fmt", "yuv420p", "-c:a", "copy", out],
                           stdin=subprocess.PIPE)
    x0, y0, x1, y1 = track_box()
    template, i = None, 0
    while raw := dec.stdout.read(w * h * 3):
        frame = np.frombuffer(raw, np.uint8).reshape(h, w, 3)
        gray = frame.mean(axis=2)
        if template is None:
            template = gray[y0:y1:2, x0:x1:2]
        t = i / fps
        img = Image.fromarray(frame).convert("RGBA")
        if t >= BURST:
            ox, oy = ORIGIN[0] * w + track(gray, template), ORIGIN[1] * h
            img.alpha_composite(layer(t, ox, oy, behind=True))
            img.alpha_composite(layer(t, ox, oy, behind=False))
        enc.stdin.write(img.convert("RGB").tobytes())
        i += 1
    enc.stdin.close()
    if dec.wait() or enc.wait():
        sys.exit("ffmpeg failed")


src, out = sys.argv[1:3]
w, h, rate, fps = probe(src)
vel, radius = splash()
ball = sprite()
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