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 an underwater to input.mp4 Second the shot dips underwater: 1.0 Seconds the dip takes to reach full strength: 0.4
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 an underwater 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 | 1.0 | Second the shot dips underwater. |
DIP | 0.4 | Seconds the dip takes to reach full strength. |
WAVE | 9 | How far the ripples push the picture, in pixels. |
WAVELENGTH | 0.22 | Length of one ripple, as a share of the frame height. |
SPEED | 0.8 | Ripples per second passing each point. |
WATER | (10, 120, 135) | Sea green the picture is washed toward. |
TINT | 0.55 | How much of the water color goes in, 0 to 1. |
HAZE | 1.5 | Blur of the murky water, in pixels. |
RAYS | 0.22 | Brightness of the shafts of light from the top, 0 to 1. |
FLICKER | 0.14 | How much the light flickers up and down, 0 to 1. |
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
Make input.mp4 look like it was filmed underwater from 1.0 seconds on. Leave the first second untouched, then ease the effect in over 0.4 seconds (smoothstep) and keep it to the end. For each frame, with Python, numpy and OpenCV: 1) Ripple the picture with cv2.remap: move each pixel up to 9 pixels across and down using sine waves about 0.22 of the frame height long (a sine of y for the sideways push, a sine of x for the up-and-down push, plus a half-strength shorter wave on each), drifting at 0.8 waves per second; reflect at the borders so no black edges show. 2) Blur by a 1.5 pixel Gaussian for murky water. 3) Tint it sea green: take each pixel's gray level, multiply sea green (10, 120, 135) by gray/128, and mix that in at 55%. 4) Darken toward the bottom, from 0% at the top to 35% at the bottom edge. 5) Add soft diagonal shafts of pale aqua light (150, 255, 240) slanting down from the top at about 0.22 strength, swaying slowly and fading out by 85% of the way down. 6) Flicker the whole frame's brightness up and down by about 14% with two sine waves at 2.7 and 6.1 per second. This effect has no text; any text would be drawn with Pillow, not ffmpeg's drawtext filter. Keep the size, frame rate (30fps) and audio, encode as H.264 yuv420p and save it as output.mp4.Run it without Claude
This is the exact script we tested. Save it as underwater.py and run:
python3 underwater.py input.mp4 output.mp4Show the script
#!/usr/bin/env python3
"""Underwater: from one second in, the picture ripples in slow waves, turns sea green and flickers under shafts of light.
Usage: python3 underwater.py input.mp4 output.mp4
Needs ffmpeg, numpy and OpenCV to bend each frame (pip install numpy opencv-python-headless).
"""
import json
import subprocess
import sys
import cv2
import numpy as np
START = 1.0 # second the shot dips underwater
DIP = 0.4 # seconds the dip takes to reach full strength
WAVE = 9 # how far the ripples push the picture, in pixels
WAVELENGTH = 0.22 # length of one ripple, as a share of the frame height
SPEED = 0.8 # ripples per second passing each point
WATER = (10, 120, 135) # sea green the picture is washed toward
TINT = 0.55 # how much of the water color goes in, 0-1
HAZE = 1.5 # blur of the murky water, in pixels
RAYS = 0.22 # brightness of the shafts of light from the top, 0-1
FLICKER = 0.14 # how much the light flickers up and down, 0-1
def strength(t):
"""0 before START, easing up to 1 over DIP seconds."""
u = min(max((t - START) / DIP, 0), 1)
return u * u * (3 - 2 * u)
def ripple_maps(t, k):
"""Where each output pixel reads from: two sets of sine waves, across and down, drifting over time."""
phase = 2 * np.pi * SPEED * t
dx = np.sin(2 * np.pi * ys / (WAVELENGTH * h) + phase) + 0.5 * np.sin(2 * np.pi * xs / (0.6 * WAVELENGTH * h) - 1.3 * phase)
dy = np.sin(2 * np.pi * xs / (WAVELENGTH * h) + 0.8 * phase) + 0.5 * np.sin(2 * np.pi * ys / (0.7 * WAVELENGTH * h) - phase)
return (xs + k * WAVE * dx / 1.5).astype(np.float32), (ys + k * WAVE * dy / 1.5).astype(np.float32)
def light(t):
"""Soft shafts of light slanting down from the top, swaying, fading with depth; plus an overall flicker."""
sway = 0.04 * np.sin(2 * np.pi * 0.3 * t)
u = (xs + 0.35 * ys) / w + sway
shafts = np.clip(np.sin(2 * np.pi * 2.3 * u) * 0.6 + np.sin(2 * np.pi * 5.1 * u + 1.7 * t) * 0.4, 0, 1) ** 2
shafts *= np.clip(1 - ys / (0.85 * h), 0, 1)
flicker = 1 + FLICKER * (0.6 * np.sin(2 * np.pi * 2.7 * t) + 0.4 * np.sin(2 * np.pi * 6.1 * t + 1.0))
return shafts, flicker
def underwater(rgb, t):
k = strength(t)
mx, my = ripple_maps(t, k)
img = cv2.remap(rgb, mx, my, cv2.INTER_LINEAR, borderMode=cv2.BORDER_REFLECT).astype(np.float32)
img = cv2.GaussianBlur(img, (0, 0), max(k * HAZE, 0.01))
gray = img.mean(axis=2, keepdims=True)
water = gray / 128 * np.array(WATER, np.float32) # keep the shading, swap in the sea color
img = img * (1 - k * TINT) + water * k * TINT
depth = 1 - 0.35 * k * (ys / h)[..., None] # darker toward the bottom
shafts, flicker = light(t)
rays = k * RAYS * shafts[..., None] * np.array([150, 255, 240], np.float32)
img = (img * depth + rays) * (1 + k * (flicker - 1))
return np.clip(img, 0, 255).astype(np.uint8)
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)
src, out = sys.argv[1:3]
w, h, rate, fps = probe(src)
ys, xs = np.mgrid[0:h, 0:w].astype(np.float32)
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):
t = i / fps
if t >= START:
raw = underwater(np.frombuffer(raw, np.uint8).reshape(h, w, 3), t).tobytes()
writer.stdin.write(raw)
i += 1
writer.stdin.close()
if reader.wait() or writer.wait():
sys.exit("ffmpeg failed")





