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 halftone dots to input.mp4 Second the picture turns into dots: 1.0 Pixels between dot centers: 10
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 halftone dots 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 picture turns into dots. |
CELL | 10 | Pixels between dot centers. Bigger gives a coarser, more graphic print. |
ANGLE | 45 | Angle of the dot rows in degrees. |
PAPER | (245, 240, 225) | Paper color. |
INK | (25, 25, 40) | Dot color. |
CONTRAST | (2, 98) | Brightness percentiles that become solid ink and bare paper. |
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
Give input.mp4 (540x960) a halftone print look from 1.0 seconds to the end; before 1.0 seconds leave it untouched. For each frame from 1.0 s: turn it to grayscale, box-blur it with a 5 pixel radius, and stretch its brightness so the 2nd percentile is solid ink and the 98th percentile is bare paper (darkness 0 to 1). Fill the frame with cream paper (245, 240, 225) and draw dark navy (25, 25, 40) dots on a square grid with 10 pixels between dot centers, the grid turned 45 degrees. Each dot's radius is 7.2 pixels times the square root of the darkness at its center, so dark areas get big dots that merge and bright areas tiny specks; give the dots a 1 pixel soft edge. No other colors should be left. Use Python with numpy and Pillow for the frames and ffmpeg to read and write the video (don't use ffmpeg's drawtext if you add any text), keep the size, 30fps, the 6 second length and the audio, and save it as output.mp4.Run it without Claude
This is the exact script we tested. Save it as halftone-dots.py and run:
python3 halftone-dots.py input.mp4 output.mp4Show the script
#!/usr/bin/env python3
"""Halftone dots: at one second the shot is reprinted as rows of ink dots on cream paper, big dots where it's dark.
Usage: python3 halftone-dots.py input.mp4 output.mp4
Needs ffmpeg, Pillow and numpy (pip install pillow numpy).
"""
import json
import subprocess
import sys
import numpy as np
from PIL import Image, ImageFilter
START = 1.0 # second the picture turns into dots
CELL = 10 # pixels between dot centers
ANGLE = 45 # screen angle in degrees, as on newsprint
PAPER = (245, 240, 225) # paper color
INK = (25, 25, 40) # dot color
CONTRAST = (2, 98) # percentiles of the frame's brightness mapped to paper white and solid ink
def halftone(img):
"""Reprint one frame (h, w, 3) as dots: each dot's area matches how dark the picture is under it."""
gray = Image.fromarray(img.astype(np.uint8)).convert("L").filter(ImageFilter.BoxBlur(CELL // 2))
luma = np.asarray(gray, np.float32)
lo, hi = np.percentile(luma, CONTRAST)
dark = 1 - np.clip((luma - lo) / max(hi - lo, 1), 0, 1)
# nearest dot center to every pixel, on a grid turned by ANGLE
cu, cv = (np.floor(u / CELL) + 0.5) * CELL, (np.floor(v / CELL) + 0.5) * CELL
cx = np.clip(np.round(cu * cos + cv * sin), 0, w - 1).astype(int)
cy = np.clip(np.round(-cu * sin + cv * cos), 0, h - 1).astype(int)
radius = CELL * 0.72 * np.sqrt(dark[cy, cx]) # 0.72 cells lets the darkest dots merge
ink = np.clip(radius - np.hypot(u - cu, v - cv) + 0.5, 0, 1)[..., None] # 1px soft edge
return paper * (1 - ink) + ink_color * ink
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)
y, x = np.mgrid[0:h, 0:w].astype(np.float32)
cos, sin = np.cos(np.radians(ANGLE)), np.sin(np.radians(ANGLE))
u, v = x * cos - y * sin, x * sin + y * cos # pixel positions in the turned grid
paper, ink_color = np.array(PAPER, np.float32), np.array(INK, 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):
img = np.frombuffer(raw, np.uint8).reshape(h, w, 3).astype(np.float32)
if i / fps >= START:
img = halftone(img)
writer.stdin.write(np.clip(img, 0, 255).astype(np.uint8).tobytes())
i += 1
writer.stdin.close()
if reader.wait() or writer.wait():
sys.exit("ffmpeg failed")





