Get this effect
Switch to Code in your Claude Desktop App
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/skillsAsk for the effect
Add floating bubbles to input.mp4 Second the bubbles start to fade in, and how many seconds the fade takes: 0.5, 0.5 Number of bubbles: 16
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 floating bubbles 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 |
|---|---|---|
START, FADE | 0.5, 0.5 | Second the bubbles start to fade in, and how many seconds the fade takes. |
COUNT | 16 | Number of bubbles. |
RADIUS | (22, 64) | Smallest and largest bubble radius, in pixels. |
RISE | (45, 110) | Slowest and fastest upward drift, in pixels per second; small bubbles rise faster. |
SWAY | 26 | How far each bubble sways side to side, in pixels. |
SEED | 4 | Change for a different arrangement of bubbles. |
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 floating soap bubbles to input.mp4 (540x960): 16 see-through bubbles with radii between 22 and 64 pixels, scattered at random across the frame. Draw each bubble with numpy: nearly clear in the middle (opacity 0.04, rising to 0.49 at the edge as 0.04 + 0.45 x (distance/radius)^8), a thin bright rim at 0.96 of the radius (opacity 0.7, a Gaussian 0.04 of the radius wide), and a pastel rainbow sheen on the film and rim: color = 255 x (0.5 + 0.5 x (0.5 + 0.5 x cos(2 pi (p + k/3)))) for the red, green and blue channels (k = 0, 1, 2), where p = a random hue per bubble + 0.35 x cos(angle - 0.8) + 0.6 x distance/radius. Add a pure white (255, 255, 255) glint: an ellipse 0.17 x 0.11 of the radius centered 0.4 radius left and 0.45 radius above the center at full opacity, plus a smaller dot at half strength at the bottom right. Blend the bubbles over the video normally (not added or screened). Each bubble rises at a speed between 110 px/s (smallest) and 45 px/s (largest), wrapping from the top back to the bottom, and sways 26 pixels side to side over 2.5 to 5 seconds. Leave the frames before 0.5 seconds untouched, fade the bubbles in from 0.5 to 1.0 seconds and keep them to the end. Use Python with numpy and ffmpeg (don't use ffmpeg's drawtext filter, draw any text with Pillow), keep the audio, and save it as output.mp4.Run it without Claude
This is the exact script we tested. Save it as floating-bubbles.py and run:
python3 floating-bubbles.py input.mp4 output.mp4Show the script
#!/usr/bin/env python3
"""Floating bubbles: see-through soap bubbles with a rainbow sheen and a white glint drift up across the frame.
Usage: python3 floating-bubbles.py input.mp4 output.mp4
Needs ffmpeg and numpy (pip install numpy).
"""
import json
import subprocess
import sys
import numpy as np
START = 0.5 # second the bubbles start to fade in
FADE = 0.5 # seconds the fade-in takes
COUNT = 16 # number of bubbles
RADIUS = (22, 64) # smallest and largest bubble radius, in pixels
RISE = (45, 110) # slowest and fastest upward drift, in pixels per second (small bubbles rise faster)
SWAY = 26 # how far each bubble sways side to side, in pixels
SEED = 4 # change for a different arrangement
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 bubble(r, hue):
"""One bubble as a color and an opacity per pixel: a nearly clear middle, a thin rim with a rainbow
sheen that shifts around the bubble, and a white glint at the top left."""
y, x = np.mgrid[-r:r + 1, -r:r + 1].astype(np.float32)
d = np.hypot(x, y) / r
angle = np.arctan2(y, x)
phase = hue + 0.35 * np.cos(angle - 0.8) + 0.6 * d # thin-film colors shift around and across the rim
rainbow = 0.5 + 0.5 * np.cos(2 * np.pi * (phase[..., None] + np.array([0, 1 / 3, 2 / 3])))
color = 255 * (0.5 + 0.5 * rainbow) # pastel, like light on a soap film
film = 0.04 + 0.45 * d ** 8 # clear in the middle, stronger toward the edge
rim = 0.7 * np.exp(-((d - 0.96) / 0.04) ** 2) # the thin bright outline
alpha = np.where(d <= 1, film, 0) + rim
glint = np.exp(-(((x + 0.4 * r) / (0.17 * r)) ** 2 + ((y + 0.45 * r) / (0.11 * r)) ** 2))
glint += 0.5 * np.exp(-(((x - 0.45 * r) / (0.09 * r)) ** 2 + ((y - 0.5 * r) / (0.09 * r)) ** 2))
color = color * (1 - glint[..., None]) + 255 * glint[..., None]
alpha = np.clip(np.maximum(alpha, glint), 0, 1)
return color.astype(np.float32), alpha[..., None].astype(np.float32)
def bubbles(t):
"""The bubble layer at time t: premultiplied color and opacity per pixel, or None before START."""
fade = np.clip((t - START) / FADE, 0, 1)
if fade == 0:
return None
color = np.zeros((h, w, 3), np.float32)
alpha = np.zeros((h, w, 1), np.float32)
for b in floating:
r = b["r"]
cx = round(b["x"] + SWAY * np.sin(2 * np.pi * t / b["sway"] + b["phase"]))
cy = round((b["y"] - b["rise"] * t) % (h + 2 * r) - r) # wraps from the top back to the bottom
x0, y0, x1, y1 = max(cx - r, 0), max(cy - r, 0), min(cx + r + 1, w), min(cy + r + 1, h)
if x0 < x1 and y0 < y1:
sy, sx = slice(y0 - (cy - r), y1 - (cy - r)), slice(x0 - (cx - r), x1 - (cx - r))
a = b["alpha"][sy, sx]
color[y0:y1, x0:x1] = color[y0:y1, x0:x1] * (1 - a) + b["color"][sy, sx] * a # premultiplied "over"
alpha[y0:y1, x0:x1] = alpha[y0:y1, x0:x1] * (1 - a) + a
return color * fade, alpha * fade
src, out = sys.argv[1:3]
w, h, rate, fps = probe(src)
rng = np.random.default_rng(SEED)
floating = []
for _ in range(COUNT):
size = rng.uniform()
r = round(RADIUS[0] + (RADIUS[1] - RADIUS[0]) * size)
sprite_color, sprite_alpha = bubble(r, rng.uniform())
floating.append({"r": r, "x": rng.uniform(r, w - r), "y": rng.uniform(0, h + 2 * r),
"rise": RISE[1] - (RISE[1] - RISE[0]) * size, "sway": rng.uniform(2.5, 5),
"phase": rng.uniform(0, 2 * np.pi), "color": sprite_color, "alpha": sprite_alpha})
reader = subprocess.Popen(["ffmpeg", "-v", "error", "-i", src, "-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", rate, "-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)
i = 0
while raw := reader.stdout.read(w * h * 3):
img = np.frombuffer(raw, np.uint8).reshape(h, w, 3)
layer = bubbles(i / fps)
if layer is not None:
color, alpha = layer
img = (img * (1 - alpha) + color).astype(np.uint8)
writer.stdin.write(img.tobytes())
i += 1
writer.stdin.close()
if reader.wait() or writer.wait():
sys.exit("ffmpeg failed")





