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 a heat haze to input.mp4 Second the haze starts: 1.0 Seconds it takes to build up to full strength; it stays after: 0.5
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 heat haze 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 haze starts. |
EASE | 0.5 | Seconds it takes to build up to full strength; it stays after. |
AMOUNT | 11 | How far the picture bends sideways at the bottom of the frame, in pixels. |
CALM_TOP | 0.15 | Top share of the frame left still; the haze grows from there to the bottom. |
CELL | (36, 14) | Size of the ripples (width, height) in pixels. |
RISE | 90 | Pixels per second the ripples climb up the frame. |
SEED | 7 | Random seed for the ripple pattern. |
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 heat haze to input.mp4. Leave the first 1.0 seconds untouched, then ease the haze in over 0.5 seconds (smoothstep, k = u²(3 − 2u) with u = (t − 1.0) / 0.5 clamped to 0-1) and keep it at full strength to the end. Build two smooth noise layers, each the frame width wide and twice the frame height tall: random values from -1 to 1 on a coarse grid with one value every 36 pixels across and every 14 pixels down, scaled up to full size with cubic interpolation (use a fixed random seed). At time t (s = t − 1.0) read layer A shifted up by 90 × s pixels and layer B shifted up by 153 × s pixels, wrapping around, so the ripples climb at two speeds. The haze weight down the frame is 0 in the top 15% and grows to 1 at the bottom edge as ((y/height − 0.15) / 0.85)^0.8. Each output pixel samples the input at x + dx, y + dy, with dx = k × 11 × weight × (0.65 × A + 0.35 × B) and dy = k × 11 × 0.3 × weight × (A − B), using bilinear sampling (OpenCV remap) and mirrored edges. Use Python with numpy and OpenCV for the pixels and ffmpeg to decode and encode, keep the size, 30fps and the audio, and save it as output.mp4.Run it without Claude
This is the exact script we tested. Save it as heat-haze.py and run:
python3 heat-haze.py input.mp4 output.mp4Show the script
#!/usr/bin/env python3
"""Heat haze: from 1 second the air shimmers, wobbly ripples rising up the frame and bending the picture most near the bottom.
Usage: python3 heat-haze.py input.mp4 output.mp4
Needs ffmpeg, numpy and OpenCV (pip install numpy opencv-python-headless).
"""
import json
import subprocess
import sys
import cv2
import numpy as np
START = 1.0 # second the haze starts
EASE = 0.5 # seconds it takes to build up to full strength (it stays after)
FPS = 30 # output frame rate
AMOUNT = 11 # how far the picture bends sideways at the bottom of the frame, in pixels
CALM_TOP = 0.15 # top share of the frame left still; the haze grows from there to the bottom
CELL = (36, 14) # size of the ripples (width, height) in pixels: short, wide cells make wavy sideways bands
RISE = 90 # pixels per second the ripples climb up the frame
SEED = 7 # change for a different (but repeatable) ripple pattern
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]))
return info["streams"][0]["width"], info["streams"][0]["height"], float(info["format"]["duration"])
def frames(path):
"""Decode a clip as RGB frames at FPS."""
ff = subprocess.Popen(["ffmpeg", "-v", "error", "-i", path, "-vf", f"fps={FPS}",
"-f", "rawvideo", "-pix_fmt", "rgb24", "-"], stdout=subprocess.PIPE)
while True:
data = ff.stdout.read(w * h * 3)
if len(data) < w * h * 3:
return
yield np.frombuffer(data, np.uint8).reshape(h, w, 3)
def noise(rng, tall):
"""Smooth random values from -1 to 1, w wide and `tall` high: a coarse random grid scaled up with cubic smoothing."""
grid = rng.uniform(-1, 1, (tall // CELL[1] + 2, w // CELL[0] + 2)).astype(np.float32)
big = cv2.resize(grid, (w + 2 * CELL[0], tall + 2 * CELL[1]), interpolation=cv2.INTER_CUBIC)
return big[CELL[1]:CELL[1] + tall, CELL[0]:CELL[0] + w]
def haze(frame, t, k):
"""Bend one frame at strength k (0-1): two ripple layers climb at different speeds so the shimmer boils
rather than slides, and each pixel is pushed sideways (and a little up and down) by their sum."""
s = t - START
rows = np.arange(h)
a = layer_a[(rows + round(RISE * s)) % len(layer_a)]
b = layer_b[(rows + round(RISE * 1.7 * s)) % len(layer_b)]
dx = k * AMOUNT * weight * (0.65 * a + 0.35 * b)
dy = k * AMOUNT * 0.3 * weight * (a - b)
return cv2.remap(frame, grid_x + dx, grid_y + dy, cv2.INTER_LINEAR, borderMode=cv2.BORDER_REFLECT)
src, out = sys.argv[1:3]
w, h, duration = probe(src)
rng = np.random.default_rng(SEED)
layer_a, layer_b = noise(rng, 2 * h), noise(rng, 2 * h)
grid_x, grid_y = np.meshgrid(np.arange(w, dtype=np.float32), np.arange(h, dtype=np.float32))
weight = (np.clip((grid_y / h - CALM_TOP) / (1 - CALM_TOP), 0, 1) ** 0.8).astype(np.float32)
enc = 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)
for i, frame in enumerate(frames(src)):
t = i / FPS
u = min(max((t - START) / EASE, 0), 1)
k = u * u * (3 - 2 * u)
enc.stdin.write((haze(frame, t, k) if k > 0 else frame).tobytes())
enc.stdin.close()
if enc.wait():
sys.exit("ffmpeg failed")





