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 frozen text to input.mp4 Text: ICE COLD
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 frozen text 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 |
|---|---|---|
TEXT | ICE COLD | The headline. |
START | 0.5 | Second the ice starts creeping in from the left. |
FREEZE | 1.0 | Seconds the ice takes to cover the whole headline. |
TOP, BOTTOM | (225, 245, 255), (70, 150, 220) | Ice colors at the top and bottom of the letters. |
OUTLINE | (15, 50, 100) | Dark blue rim that keeps the letters readable on light footage. |
ICICLES | 9 | Icicles hanging off the bottom of the letters. |
CRACKS | 14 | White cracks through the ice. |
TWINKLE | 1.2 | Seconds between flashes of each sparkle. |
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 frozen text to input.mp4: "ICE COLD" in Arial Black (or another heavy bold font) at an 81 pixel font size (15% of the frame width), centered horizontally 15% of the way down. Make the letters look like ice: a 4 pixel dark blue (15, 50, 100) outline; a fill that runs in a vertical gradient from (225, 245, 255) at the top of the outlined text to (70, 150, 220) at the bottom, mottled by about 8% with random noise blurred by 2 pixels so it looks frosted; and 14 jagged white cracks clipped to the letters (2 pixels wide, 4 segments of about 14 pixels each, each with a thin dark blue line 1 pixel down and right of it). Behind the letters, so their outline stays unbroken, hang 9 icicles spread evenly along the feet of the upright strokes (not under the round bottoms of C and O): light blue (130, 190, 235) downward-pointing triangles with no outline, 6 to 12 pixels wide, starting 6 pixels above the bottom of the outline and reaching 10 to 32 pixels below it. Nothing shows before 0.5 seconds. From 0.5 to 1.5 seconds the ice sweeps in from the left: a soft, ragged edge about 32 pixels wide (6% of the frame width) moves at a steady speed from just left of the text to just past its right edge, revealing the letters behind it, and the letters glow white right at the moving edge. From 1.5 seconds three four-pointed white star glints (up to 49 pixels across, 9% of the frame width, with a soft glow) flash at about (135, 137), (314, 161) and (412, 133) pixels, that is 18%/35%, 60%/70% and 83%/30% across and down the outlined text's box: each one's size follows the positive half of a sine wave with a 1.2 second period, the first peaking at 1.8 seconds and each of the others 0.4 seconds after the one before (1.8, 2.2 and 2.6 seconds, repeating every 1.2 seconds). Use Python with Pillow to draw it and ffmpeg to put it on the video (don't rely on ffmpeg's drawtext filter), keep the audio, and save it as output.mp4.Run it without Claude
This is the exact script we tested. Save it as frozen-text.py and run:
python3 frozen-text.py input.mp4 output.mp4Show the script
#!/usr/bin/env python3
"""Frozen text: ice creeps across "ICE COLD" from left to right, leaving frosted blue letters with white cracks and glints.
Usage: python3 frozen-text.py input.mp4 output.mp4
Needs ffmpeg, Pillow and numpy (pip install pillow numpy).
"""
import json
import math
import subprocess
import sys
from pathlib import Path
import numpy as np
from PIL import Image, ImageDraw, ImageFilter, ImageFont
TEXT = "ICE COLD" # the headline
START = 0.5 # second the ice starts creeping in from the left
FREEZE = 1.0 # seconds the ice takes to cover the whole headline
Y = 0.15 # text center, 0-1 down the frame
SIZE = 0.15 # font size as a share of the frame width
TOP = (225, 245, 255) # ice color at the top of the letters
BOTTOM = (70, 150, 220) # ice color at the bottom of the letters
OUTLINE = (15, 50, 100) # dark blue rim that keeps the letters readable on light footage
ICICLES = 9 # icicles hanging off the bottom of the letters
ICICLE_LENGTH = 0.06 # longest icicle as a share of the frame width
ICICLE = (130, 190, 235) # icicle color
CRACKS = 14 # number of white cracks through the ice
GLINTS = [(0.18, 0.35), (0.60, 0.70), (0.83, 0.30)] # sparkle spots, 0-1 across and down the text box
GLINT_SIZE = 0.09 # widest sparkle as a share of the frame width
TWINKLE = 1.2 # seconds between flashes of each sparkle; each one flashes 0.4s after the last
SEED = 5 # change for a different frost and crack pattern
FONTS = ["/System/Library/Fonts/Supplemental/Arial Black.ttf", # macOS
"/System/Library/Fonts/Supplemental/Arial Bold.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", # Linux
"C:/Windows/Fonts/ariblk.ttf"] # Windows
FONT_FILE = next((f for f in FONTS if Path(f).exists()), None) or sys.exit("No bold font found: add one to FONTS")
def ice_letters():
"""The finished frozen headline on a transparent frame, plus its bounding box."""
rng = np.random.default_rng(SEED)
font = ImageFont.truetype(FONT_FILE, round(w * SIZE))
shape = Image.new("L", (w, h))
ImageDraw.Draw(shape).text((w / 2, h * Y), TEXT, font=font, anchor="mm", fill=255)
rim = Image.new("L", (w, h))
ImageDraw.Draw(rim).text((w / 2, h * Y), TEXT, font=font, anchor="mm", fill=255, stroke_width=4)
x0, y0, x1, y1 = rim.getbbox()
# ice body: a light-to-deep blue gradient, mottled with soft noise so it looks frosted
ramp = np.linspace(0, 1, y1 - y0)[:, None, None]
body = np.zeros((h, w, 3))
body[y0:y1] = np.array(TOP) * (1 - ramp) + np.array(BOTTOM) * ramp
body *= 1 + 0.08 * soft_noise(rng, 2)[..., None]
ice = Image.fromarray(body.clip(0, 255).astype(np.uint8)).convert("RGBA")
# cracks: jagged white lines with a dark edge, kept inside the letters
cracks = Image.new("RGBA", (w, h))
draw = ImageDraw.Draw(cracks)
for _ in range(CRACKS):
x, y = rng.uniform(x0, x1), rng.uniform(y0, y1)
angle = rng.uniform(0, 2 * math.pi)
points = [(x, y)]
for _ in range(4):
angle += rng.uniform(-0.8, 0.8)
x, y = x + math.cos(angle) * w * 0.025, y + math.sin(angle) * w * 0.025
points.append((x, y))
draw.line([(px + 1, py + 1) for px, py in points], fill=OUTLINE + (150,), width=1)
draw.line(points, fill=(255, 255, 255, 255), width=2)
ice.alpha_composite(cracks)
ice.putalpha(shape)
# icicles: hanging from the feet of the upright strokes, tucked behind the letters so their outline stays clean
letters = Image.new("RGBA", (w, h))
draw = ImageDraw.Draw(letters)
rows = np.asarray(shape) > 128
under = np.nonzero(rows[y1 - 7] & rows[y1 - 4 - round(w * SIZE * 0.3)])[0]
for x in (rng.choice(part) for part in np.array_split(under, ICICLES)): # spread along the text
half, length = rng.uniform(3, 6), rng.uniform(0.3, 1) * w * ICICLE_LENGTH
draw.polygon([(x - half, y1 - 6), (x + half, y1 - 6), (x, y1 + length)], fill=ICICLE)
rim_layer = Image.new("RGBA", (w, h), OUTLINE)
rim_layer.putalpha(rim)
letters.alpha_composite(rim_layer)
letters.alpha_composite(ice)
return letters, (x0, y0, x1, y1)
def soft_noise(rng, blur):
"""Blurred random noise scaled to mean 0 and spread 1, the same size as the frame."""
n = np.asarray(Image.fromarray(rng.integers(0, 256, (h, w), dtype=np.uint8)).filter(ImageFilter.GaussianBlur(blur)), np.float32)
return (n - n.mean()) / n.std()
def glint(size):
"""A four-pointed white star with a soft glow."""
star = Image.new("L", (size, size))
draw = ImageDraw.Draw(star)
c, arm, waist = size / 2, size / 2, size / 14
draw.polygon([(c, c - arm), (c + waist, c - waist), (c + arm, c), (c + waist, c + waist),
(c, c + arm), (c - waist, c + waist), (c - arm, c), (c - waist, c - waist)], fill=255)
draw.ellipse([c - size / 7, c - size / 7, c + size / 7, c + size / 7], fill=255)
img = Image.new("RGBA", (size, size), (255, 255, 255))
img.putalpha(Image.fromarray(np.maximum(np.asarray(star), np.asarray(star.filter(ImageFilter.GaussianBlur(3))))))
return img
def frost_mask(t):
"""How frozen each pixel is at t (0-1): a ragged edge that sweeps left to right."""
x0, _, x1, _ = box
front = x0 - edge_width + (x1 - x0 + 2 * edge_width) * (t - START) / FREEZE
return np.clip((front - np.arange(w)[None, :] + ragged) / edge_width, 0, 1)
def overlay(t):
if t < START:
return blank
img = letters.copy()
if t < START + FREEZE:
frozen = frost_mask(t)
frost = np.asarray(letters.getchannel("A"), np.float32) * frozen * (1 - frozen) * 4 # whitest at the front
white = Image.new("RGBA", (w, h), (255, 255, 255))
white.putalpha(Image.fromarray(frost.astype(np.uint8)))
img.alpha_composite(white)
img.putalpha(Image.fromarray(np.minimum(np.asarray(letters.getchannel("A")), frozen * 255).astype(np.uint8)))
return img
x0, y0, x1, y1 = box
for i, (gx, gy) in enumerate(GLINTS):
twinkle = math.sin(2 * math.pi * (t - START - FREEZE - 0.4 * i) / TWINKLE)
size = round(w * GLINT_SIZE * max(twinkle, 0))
if size > 4:
star = sparkle.resize((size, size), Image.LANCZOS)
img.alpha_composite(star, (round(x0 + gx * (x1 - x0) - size / 2), round(y0 + gy * (y1 - y0) - size / 2)))
return img
def probe(path):
info = json.loads(subprocess.check_output(["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries",
"stream=width,height,r_frame_rate:format=duration", "-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), float(info["format"]["duration"])
def render(src, out):
"""Draw overlay(t) on every frame of src and save to out, keeping the audio."""
ff = subprocess.Popen(["ffmpeg", "-y", "-v", "error", "-i", src,
"-f", "rawvideo", "-pix_fmt", "rgba", "-s", f"{w}x{h}", "-r", rate, "-i", "-",
"-filter_complex", "[0:v][1:v]overlay=shortest=1,format=yuv420p[v]",
"-map", "[v]", "-map", "0:a?", "-c:v", "libx264", "-crf", "18", "-c:a", "copy", out],
stdin=subprocess.PIPE)
for i in range(math.ceil(duration * fps)):
ff.stdin.write(overlay(i / fps).tobytes())
ff.stdin.close()
if ff.wait():
sys.exit("ffmpeg failed")
src, out = sys.argv[1:3]
w, h, rate, fps, duration = probe(src)
letters, box = ice_letters()
edge_width = w * 0.06
ragged = soft_noise(np.random.default_rng(SEED), 4) * edge_width / 2
sparkle = glint(round(w * GLINT_SIZE))
blank = Image.new("RGBA", (w, h))
render(src, out)





