How to add a magnifying glass zoom with Claude

A black-rimmed magnifying glass pops up over the bottle's dropper, slides down onto the label and holds there, its glass showing the label's small print twice as large.

Get this effect

  1. Switch to Code in your Claude Desktop App

  2. 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/skills
  3. Ask for the effect

    Add a magnifying glass zoom to input.mp4
    Second the lens pops in: 0.8
    Seconds the lens glides from FROM to TO: 1.2, 2.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 a magnifying glass zoom 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.

SettingDefaultWhat it does
START0.8Second the lens pops in.
GLIDE1.2, 2.4Seconds the lens glides from FROM to TO.
FROM0.62, 0.30Where the lens appears, as shares of the frame width and height.
TO0.50, 0.665Where it comes to rest; here, on the label.
RADIUS0.22Lens radius as a share of the frame width.
MAG2.0How much the lens enlarges what's under it.
RIM(28, 28, 32)Rim and handle color.

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 magnifying glass to input.mp4 with Python, numpy, OpenCV and Pillow, using ffmpeg to read and write the frames. Before 0.8 seconds leave the video untouched. At 0.8 seconds a round lens pops in (scaling from 0 to full size over 0.25 seconds with a slight overshoot) centered at 62% across and 30% down. From 1.2 to 2.4 seconds (smoothstep ease) it glides to 50% across and 66.5% down, onto the label, and stays there to the end. The lens radius is 22% of the frame width (119 px on a 540-wide frame); inside it, show the picture enlarged 2x about the lens center (warpAffine with a scale of 2 around that point), refreshed every frame, and leave everything outside the lens and its frame untouched. Draw the frame with Pillow: a dark (28, 28, 32) rim 3% of the frame width thick just outside the lens radius, a thin gray (120, 120, 128) line inside the rim, a handle in the same dark color 30% of the frame width long and 6.6% wide going down and to the right at 45 degrees with a rounded end, a soft blurred shadow offset 8 px down and right, and a short white glint arc on the upper-left of the glass near its edge. There's no text to draw, but if you add any, draw it with Pillow, not ffmpeg's drawtext. Keep the audio and save it as output.mp4 at 30fps.

Run it without Claude

This is the exact script we tested. Save it as magnifying-glass-zoom.py and run:

python3 magnifying-glass-zoom.py input.mp4 output.mp4
Show the script
#!/usr/bin/env python3
"""Magnifying glass zoom: a round lens on a handle pops in over the bottle, glides down onto the label and shows it at 2x.

Usage: python3 magnifying-glass-zoom.py input.mp4 output.mp4
Needs ffmpeg, OpenCV, Pillow and numpy (pip install opencv-python-headless pillow numpy).
"""
import json
import subprocess
import sys

import cv2
import numpy as np
from PIL import Image, ImageDraw, ImageFilter

START = 0.8               # second the lens pops in
POP = 0.25                # seconds the pop takes
GLIDE = 1.2, 2.4          # seconds the lens glides from FROM to TO
FROM = 0.62, 0.30         # where the lens appears (0-1 across and down the frame)
TO = 0.50, 0.665          # where it comes to rest, here on the label
RADIUS = 0.22             # lens radius as a share of the frame width
MAG = 2.0                 # how much the lens enlarges what's under it
RIM = (28, 28, 32)        # rim and handle color
RIM_WIDTH = 0.03          # rim thickness as a share of the frame width


def ease(p):
    p = min(max(p, 0.0), 1.0)
    return p * p * (3 - 2 * p)


def ease_out_back(p):
    """0 -> 1 with a small overshoot past 1 before settling, which reads as a 'pop'."""
    p = min(max(p, 0.0), 1.0)
    return 1 + 2.70158 * (p - 1) ** 3 + 1.70158 * (p - 1) ** 2


def glass():
    """The lens frame as an RGBA sprite with the lens center at (PAD, PAD): a soft shadow, the dark rim,
    a handle reaching down and right, and a white glint on the glass."""
    s = 4                                                  # draw 4x larger, then shrink, for smooth edges
    big = Image.new("RGBA", (SIZE * s, SIZE * s))
    d = ImageDraw.Draw(big)
    c, r, rw = PAD * s, R * s, round(RIM_WIDTH * w * s)
    hx, hy = c + (r + rw * 0.3) * 0.7071, c + (r + rw * 0.3) * 0.7071
    handle = [(hx, hy), (hx + HANDLE * s * 0.7071, hy + HANDLE * s * 0.7071)]
    shadow = Image.new("RGBA", big.size)
    sd = ImageDraw.Draw(shadow)
    off = 8 * s
    sd.ellipse([c - r - rw + off, c - r - rw + off, c + r + rw + off, c + r + rw + off], outline=(0, 0, 0, 90), width=rw * 2)
    sd.line([(x + off, y + off) for x, y in handle], fill=(0, 0, 0, 90), width=round(rw * 2.2))
    big.alpha_composite(shadow.filter(ImageFilter.GaussianBlur(6 * s)))
    d.line(handle, fill=RIM, width=round(rw * 2.2))
    d.ellipse([handle[1][0] - rw * 1.1, handle[1][1] - rw * 1.1, handle[1][0] + rw * 1.1, handle[1][1] + rw * 1.1], fill=RIM)
    d.ellipse([c - r - rw, c - r - rw, c + r + rw, c + r + rw], outline=RIM, width=rw)
    d.ellipse([c - r - rw * 0.55, c - r - rw * 0.55, c + r + rw * 0.55, c + r + rw * 0.55], outline=(120, 120, 128), width=max(s, rw // 6))
    g = r * 0.86
    d.arc([c - g, c - g, c + g, c + g], 200, 245, fill=(255, 255, 255, 170), width=round(r * 0.05))
    sprite = big.resize((SIZE, SIZE), Image.LANCZOS)
    return np.asarray(sprite, np.float32) / 255


def magnify(img, cx, cy, scale):
    """img with a lens of RADIUS*scale at (cx, cy) showing what's under it MAG times larger."""
    r = R * scale
    zoom = cv2.warpAffine(img, np.float32([[MAG, 0, cx * (1 - MAG)], [0, MAG, cy * (1 - MAG)]]), (w, h),
                          flags=cv2.INTER_CUBIC, borderMode=cv2.BORDER_REFLECT)
    inside = np.clip(r - np.hypot(xs - cx, ys - cy) + 0.5, 0, 1)[..., None]
    out = img * (1 - inside) + zoom * inside
    frame_art = sprite if scale == 1 else cv2.resize(sprite, None, fx=scale, fy=scale, interpolation=cv2.INTER_AREA)
    x0, y0 = round(cx - PAD * scale), round(cy - PAD * scale)
    fh, fw = frame_art.shape[:2]
    ox0, oy0, ox1, oy1 = max(x0, 0), max(y0, 0), min(x0 + fw, w), min(y0 + fh, h)
    art = frame_art[oy0 - y0:oy1 - y0, ox0 - x0:ox1 - x0]
    region = out[oy0:oy1, ox0:ox1]
    a = art[..., 3:]
    out[oy0:oy1, ox0:ox1] = region * (1 - a) + art[..., :3] * 255 * a
    return out


src, out = sys.argv[1:3]
info = json.loads(subprocess.check_output(["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries",
                                           "stream=width,height", "-of", "json", src]))
w, h = info["streams"][0]["width"], info["streams"][0]["height"]
R = RADIUS * w
HANDLE = 0.30 * w                                          # handle length
PAD = round(R + RIM_WIDTH * w + 0.05 * w)                  # room round the lens for the rim and shadow
SIZE = round(PAD + (R + RIM_WIDTH * w + HANDLE) * 0.7071 + 0.12 * w)
sprite = glass()
ys, xs = np.mgrid[0:h, 0:w].astype(np.float32)

reader = subprocess.Popen(["ffmpeg", "-v", "error", "-i", src, "-vf", "fps=30", "-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", "30", "-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 True:
    raw = reader.stdout.read(w * h * 3)
    if len(raw) < w * h * 3:
        break
    img = np.frombuffer(raw, np.uint8).reshape(h, w, 3)
    t = i / 30
    scale = ease_out_back((t - START) / POP) if t >= START else 0
    if scale > 0.02:
        p = ease((t - GLIDE[0]) / (GLIDE[1] - GLIDE[0]))
        cx = w * (FROM[0] + (TO[0] - FROM[0]) * p)
        cy = h * (FROM[1] + (TO[1] - FROM[1]) * p)
        img = magnify(img.astype(np.float32), cx, cy, scale).round().clip(0, 255).astype(np.uint8)
    writer.stdin.write(img.tobytes())
    i += 1
writer.stdin.close()
if reader.wait() or writer.wait():
    sys.exit("ffmpeg failed")

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