---
title: "How to add floating bubbles with Claude (tested prompt and script)"
description: "Clear soap bubbles, each ringed with a thin rainbow rim and a white glint at its top left, fade in over the bottle and rise up the frame, swaying gently side to side. Install one skill and ask Claude Code, or run the Python script we tested yourself."
url: https://www.recharm.com/effects/floating-bubbles
---

# How to add floating bubbles with Claude

Clear soap bubbles, each ringed with a thin rainbow rim and a white glint at its top left, fade in over the bottle and rise up the frame, swaying gently side to side.

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

```
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.mp4
```

```
#!/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")
```

## More overlays and light

- [Anamorphic streak **Anamorphic streak**](https://www.recharm.com/effects/anamorphic-streak)
- [Aurora **Aurora**](https://www.recharm.com/effects/aurora)
- [Blood splatter **Blood splatter**](https://www.recharm.com/effects/blood-splatter)
- [Bokeh lights **Bokeh lights**](https://www.recharm.com/effects/bokeh-lights)
- [Camera flash **Camera flash**](https://www.recharm.com/effects/camera-flash)

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