---
title: "How to add halftone dots with Claude (tested prompt and script)"
description: "At one second the shot is reprinted like newsprint: navy ink dots on cream paper, merging into solid ink on the dropper cap and shrinking to specks on the bright backdrop. Install one skill and ask Claude Code, or run the Python script we tested yourself."
url: https://www.recharm.com/effects/halftone-dots
---

# How to add halftone dots with Claude

At one second the shot is reprinted like newsprint: navy ink dots on cream paper, merging into solid ink on the dropper cap and shrinking to specks on the bright backdrop.

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

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

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

## More stylize and art

- [Anime style **Anime style**](https://www.recharm.com/effects/anime-style)
- [ASCII art **ASCII art**](https://www.recharm.com/effects/ascii-art)
- [Blueprint **Blueprint**](https://www.recharm.com/effects/blueprint)
- [Charcoal drawing **Charcoal drawing**](https://www.recharm.com/effects/charcoal-drawing)
- [Claymation style **Claymation style**](https://www.recharm.com/effects/claymation-style)

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