---
title: "How to add a zoom into screen with Claude (tested prompt and script)"
description: "A black phone playing clip B rises over clip A, then the camera pushes into its screen until B fills the whole frame. Install one skill and ask Claude Code, or run the Python script we tested yourself."
url: https://www.recharm.com/effects/zoom-into-screen
---

# How to add a zoom into screen with Claude

A black phone playing clip B rises over clip A, then the camera pushes into its screen until B fills the whole frame.

Made by the script on this page, from our own test clips.

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

   ```
   Join clipA.mp4 and clipB.mp4 with a zoom into screen
   Seconds the phone takes to rise from below the frame: 0.5
   Seconds the phone sits still before the push: 0.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.

Put a sale badge on **@clip1.mp4**, give **@clip2.mp4** a VHS look, then join them with a zoom into screen.

## 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                                                                            |
| -------- | --------------- | --------------------------------------------------------------------------------------- |
| `RISE`   | `0.5`           | Seconds the phone takes to rise from below the frame.                                   |
| `HOLD`   | `0.4`           | Seconds the phone sits still before the push.                                           |
| `PUSH`   | `1.1`           | Seconds the camera takes to push into the screen.                                       |
| `SCREEN` | `0.44`          | Screen width at rest as a share of the frame width; the screen keeps the video's shape. |
| `BEZEL`  | `0.026`         | Bezel thickness at rest as a share of the frame width.                                  |
| `RADIUS` | `0.06`          | Screen corner radius at rest as a share of the frame width.                             |
| `BODY`   | `(18, 18, 22)`  | Phone body color.                                                                       |
| `RIM`    | `(92, 92, 104)` | Color of the thin edge around the body and of the side buttons.                         |
| `SHADOW` | `0.55`          | Shadow strength (0-1).                                                                  |

## Without the skill

Don’t want to install the skill? Paste this prompt into Claude Code and Claude writes the script itself.

```
Join clipA.mp4 and clipB.mp4 (both 540x960 at 30fps, 6 seconds) with a 2-second zoom-into-screen transition over the last 2 seconds of A, so the result is 10 seconds: A alone until 4.0s, then B playing from its start on a phone screen, and B alone from 6.0s (B at 2.0s) to its end. The phone: its screen is 44% of the frame width with the clip's 9:16 shape (238x422 pixels at rest), centered in the frame (x=151 to 389, y=269 to 691), with corners rounded to 32 pixels, and clip B plays on it scaled to fit exactly, uncropped. Around the screen draw a near-black (18, 18, 22) body with a 14-pixel bezel on every side, edged by a thin gray-blue (92, 92, 104) rim, with small gray-blue side buttons (two volume buttons on the left, power on the right) and a soft black shadow under it at 55% opacity, shifted down 16 pixels and blurred about 22 pixels. From 4.0s to 4.5s the phone rises from fully below the frame into its centered spot (cubic ease-out), clip A still playing behind it. It holds until 4.9s. From 4.9s to 6.0s the camera pushes in (cubic ease-in-out): the screen grows about the frame's center until it is exactly 540x960, its corner radius shrinking to 0 as it does, while clip A behind the phone is zoomed in by the same factor about the frame center (1.0x at 4.9s, 2.27x at 6.0s), so the phone and the scene move together like one camera. Draw the phone with Pillow (draw anything with Pillow, not ffmpeg's drawtext), scale with OpenCV and read and write the video with ffmpeg. Crossfade the audio over the same 2 seconds, and save it as output.mp4.
```

## Run it without Claude

This is the exact script we tested. Save it as `zoom-into-screen.py` and run:

```
python3 zoom-into-screen.py clipA.mp4 clipB.mp4 output.mp4
```

```
#!/usr/bin/env python3
"""Zoom into screen: a phone playing clip B rises over clip A, then the camera pushes into its screen until B fills the frame.

Usage: python3 zoom-into-screen.py clipA.mp4 clipB.mp4 output.mp4  (both clips need an audio track)
Needs ffmpeg, OpenCV, numpy and Pillow (pip install opencv-python-headless numpy pillow).
"""
import json
import subprocess
import sys

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

RISE = 0.5               # seconds the phone takes to rise from below the frame
HOLD = 0.4               # seconds the phone sits still before the push
PUSH = 1.1               # seconds the camera takes to push into the screen
SCREEN = 0.44            # screen width as a share of the frame width at rest (the screen keeps the video's shape)
BEZEL = 0.026            # bezel thickness as a share of the frame width at rest
RADIUS = 0.06            # screen corner radius as a share of the frame width at rest
BODY = (18, 18, 22)      # phone body color (near-black)
RIM = (92, 92, 104)      # thin metal edge around the body and the side buttons
SHADOW = 0.55            # shadow strength (0-1)

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 frames(path, start):
    """The transition's frames from path, starting at `start` seconds, fitted to A's size and frame rate."""
    raw = subprocess.check_output(["ffmpeg", "-v", "error", "-ss", f"{start:.3f}", "-i", path, "-frames:v", str(n),
                                   "-vf", f"scale={w}:{h}:force_original_aspect_ratio=increase,crop={w}:{h},fps={rate}",
                                   "-f", "rawvideo", "-pix_fmt", "rgb24", "-"])
    return np.frombuffer(raw, np.uint8)[:n * h * w * 3].reshape(n, h, w, 3)

def ease_out(p):
    return 1 - (1 - min(max(p, 0), 1)) ** 3

def ease_in_out(p):
    p = min(max(p, 0), 1)
    return 4 * p ** 3 if p < 0.5 else 1 - (-2 * p + 2) ** 3 / 2

def phone_body():
    """The phone at full size (its screen exactly w x h at (pad, pad)) with its shadow, as an RGBA array."""
    k = 1 / SCREEN  # the phone is drawn at the size it has when the screen fills the frame
    bezel, radius = round(w * BEZEL * k), round(w * RADIUS * k)
    x0, y0, x1, y1 = pad - bezel, pad - bezel, pad + w - 1 + bezel, pad + h - 1 + bezel
    layer = Image.new("RGBA", (w + 2 * pad, h + 2 * pad))
    shadow = Image.new("L", layer.size)
    drop = w * 0.03 * k
    ImageDraw.Draw(shadow).rounded_rectangle([x0, y0 + drop, x1, y1 + drop], radius + bezel, fill=round(255 * SHADOW))
    layer.paste((0, 0, 0, 255), (0, 0), shadow.filter(ImageFilter.GaussianBlur(w * 0.04 * k)))
    draw = ImageDraw.Draw(layer)
    button, ph = round(w * 0.008 * k), y1 - y0
    draw.rounded_rectangle([x0 - button, y0 + ph * 0.17, x0 + 2, y0 + ph * 0.24], button, fill=RIM)  # volume up
    draw.rounded_rectangle([x0 - button, y0 + ph * 0.27, x0 + 2, y0 + ph * 0.34], button, fill=RIM)  # volume down
    draw.rounded_rectangle([x1 - 2, y0 + ph * 0.22, x1 + button, y0 + ph * 0.32], button, fill=RIM)  # power
    draw.rounded_rectangle([x0, y0, x1, y1], radius + bezel, fill=RIM)
    draw.rounded_rectangle([x0 + 3, y0 + 3, x1 - 3, y1 - 3], radius + bezel - 3, fill=BODY)
    return np.asarray(layer, np.float32), radius

def zoomed(img, z):
    """img with the camera pushed in z times on its center."""
    cw, ch = w / z, h / z
    x0, y0 = (w - cw) / 2, (h - ch) / 2
    m = np.float32([[z, 0, -x0 * z], [0, z, -y0 * z]])
    return cv2.warpAffine(img, m, (w, h), flags=cv2.INTER_LINEAR)

def screen_frame(i):
    """Frame i of the transition: A behind, the phone playing B, risen by RISE and pushed into after HOLD."""
    t = i / fps
    rise = ease_out(t / RISE)
    push = ease_in_out((t - RISE - HOLD) / PUSH)
    scale = SCREEN + (1 - SCREEN) * push        # screen width as a share of the frame width
    lift = (1 - rise) * h * (0.5 + SCREEN / 2 + 0.1)  # starts fully below the frame

    phone = body.copy()
    mask = Image.new("L", (w, h))
    ImageDraw.Draw(mask).rounded_rectangle([0, 0, w - 1, h - 1], round(corner * (1 - push)), fill=255)
    m = np.asarray(mask, np.float32)[..., None] / 255
    screen = phone[pad:pad + h, pad:pad + w]
    screen[..., :3] = b_frames[i] * m + screen[..., :3] * (1 - m)
    screen[..., 3:] = 255 * m + screen[..., 3:] * (1 - m)

    # shrink the phone so its screen is `scale` of the frame wide, then place it centered and lifted by `lift`
    size = (round(phone.shape[1] * scale), round(phone.shape[0] * scale))
    small = cv2.resize(phone, size, interpolation=cv2.INTER_AREA if scale < 1 else cv2.INTER_LINEAR)
    move = np.float32([[1, 0, (w - size[0]) / 2], [0, 1, (h - size[1]) / 2 + lift]])
    layer = cv2.warpAffine(small, move, (w, h), flags=cv2.INTER_LINEAR)
    alpha = layer[..., 3:] / 255
    back = zoomed(a_frames[i], scale / SCREEN).astype(np.float32)
    return (layer[..., :3] * alpha + back * (1 - alpha)).clip(0, 255).astype(np.uint8)

a_path, b_path, out = sys.argv[1:4]
w, h, rate, fps, a_len = probe(a_path)
D = RISE + HOLD + PUSH
n = round(D * fps)
offset = a_len - D
a_frames, b_frames = frames(a_path, offset), frames(b_path, 0)
pad = round(w * 0.15 / SCREEN)
body, corner = phone_body()

# A up to the phone, the transition frames, then B from where the push lands; the sound crossfades over D
ff = subprocess.Popen(["ffmpeg", "-y", "-v", "error", "-i", a_path, "-i", b_path,
                       "-f", "rawvideo", "-pix_fmt", "rgb24", "-s", f"{w}x{h}", "-r", rate, "-i", "-",
                       "-filter_complex",
                       f"[0:v]trim=end={offset:.4f},setpts=PTS-STARTPTS,fps={rate},format=yuv420p,setsar=1[a];"
                       f"[1:v]scale={w}:{h}:force_original_aspect_ratio=increase,crop={w}:{h},fps={rate},"
                       f"trim=start={D},setpts=PTS-STARTPTS,format=yuv420p,setsar=1[b];"
                       f"[2:v]format=yuv420p,setsar=1[c];"
                       f"[a][c][b]concat=n=3:v=1:a=0[v];[0:a][1:a]acrossfade=d={D}[au]",
                       "-map", "[v]", "-map", "[au]", "-c:v", "libx264", "-crf", "18", "-pix_fmt", "yuv420p",
                       "-c:a", "aac", out], stdin=subprocess.PIPE)
for i in range(n):
    ff.stdin.write(screen_frame(i).tobytes())
ff.stdin.close()
if ff.wait():
    sys.exit("ffmpeg failed")
```

## More transitions

- [Barn door transition **Barn door transition**](https://www.recharm.com/effects/barn-door)
- [Blinds transition **Blinds transition**](https://www.recharm.com/effects/blinds-transition)
- [Block dissolve **Block dissolve**](https://www.recharm.com/effects/block-dissolve)
- [Blur transition **Blur transition**](https://www.recharm.com/effects/blur-transition)
- [Bounce transition **Bounce transition**](https://www.recharm.com/effects/bounce-transition)

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