---
title: "How to add a tunnel effect with Claude (tested prompt and script)"
description: "At one second the picture curls round into a turning tunnel lined with mirrored copies of the shot, rings of it rushing toward you out of a dark center, then it unrolls back to the normal shot at five seconds. Install one skill and ask Claude Code, or run the Python script we tested yourself."
url: https://www.recharm.com/effects/tunnel-effect
---

# How to add a tunnel effect with Claude

At one second the picture curls round into a turning tunnel lined with mirrored copies of the shot, rings of it rushing toward you out of a dark center, then it unrolls back to the normal shot at five seconds.

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 a tunnel effect to input.mp4
   Seconds the picture starts curling into the tunnel and is back to normal: 1.0, 5.0
   Seconds the curl in and the unroll out each take: 0.6
   ```

## 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 tunnel effect 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, END` | `1.0, 5.0`    | Seconds the picture starts curling into the tunnel and is back to normal.        |
| `MORPH`      | `0.6`         | Seconds the curl in and the unroll out each take.                                |
| `DEPTH`      | `0.5`         | How stretched the walls are. Smaller pulls the picture deeper into the distance. |
| `SPEED`      | `0.9`         | How fast you fly down the tunnel, in picture heights per second.                 |
| `SPIN`       | `0.08`        | How fast the tunnel turns, in turns per second. 0 stops it.                      |
| `WRAPS`      | `2`           | How many mirrored copies of the picture run round the wall.                      |
| `FOG`        | `0.3`         | Size of the dark far end, as a share of half the frame's short side.             |
| `FOG_COLOR`  | `(10, 6, 24)` | Color the far end fades into.                                                    |

## Without the skill

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

```
Add a tunnel effect to input.mp4 with Python and numpy, decoding and encoding the frames with ffmpeg. Measure each pixel from the frame center: its distance r as a share of half the frame's short side (r = 1 at 270 px on a 540x960 frame) and its angle a from straight up, in turns of half a circle (a = atan2(x - w/2, h/2 - y) / pi, from -1 to 1). In the full tunnel, the pixel shows the input frame at x = fold(2 x (a + 0.16 x (t - 1))) x (width - 1) and y = fold(0.5 / r + 0.9 x (t - 1)) x (height - 1), where t is the time in seconds and fold(v) = |((v + 1) mod 2) - 1| (a 0 -> 1 -> 0 zigzag, so the picture repeats mirrored with no seams), sampled with bilinear blending. So the picture runs twice round the wall, mirrored, turns 0.08 turns per second, and stretches into the distance toward the center while you fly forward. Fade the far end into a dark (10, 6, 24): mix that color in by 1 - min(r / 0.3, 1)^1.5. Blend between normal and tunnel by moving each pixel's sample position from its own position to the tunnel position, and the fog with it, by a strength that eases (smoothstep) from 0 at 1.0s to 1 at 1.6s, stays 1 until 4.4s and eases back to 0 at 5.0s; before 1.0s and after 5.0s the frames are untouched. Keep the size, 30fps and the audio, and save it as output.mp4.
```

## Run it without Claude

This is the exact script we tested. Save it as `tunnel-effect.py` and run:

```
python3 tunnel-effect.py input.mp4 output.mp4
```

```
#!/usr/bin/env python3
"""Tunnel effect: at 1 second the picture curls round into a tunnel that stretches away to a dark center, flies forward through it, then unrolls back at 5 seconds.

Usage: python3 tunnel-effect.py input.mp4 output.mp4
Needs ffmpeg and numpy (pip install numpy).
"""
import json
import subprocess
import sys

import numpy as np

START = 1.0             # second the picture starts curling into the tunnel
END = 5.0               # second it is back to normal
MORPH = 0.6             # seconds the curl in (and the unroll out) takes
FPS = 30                # output frame rate
DEPTH = 0.5             # how stretched the walls are: smaller pulls the picture deeper into the distance
SPEED = 0.9             # how fast you fly down the tunnel (picture heights per second)
SPIN = 0.08             # how fast the tunnel turns (turns per second)
WRAPS = 2               # how many times the picture runs round the tunnel wall (each one mirrored)
FOG = 0.3               # radius of the dark center, as a share of half the frame's short side
FOG_COLOR = (10, 6, 24) # color the far end of the tunnel fades into

def probe(path):
    info = json.loads(subprocess.check_output(["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries",
                                               "stream=width,height:format=duration", "-of", "json", path]))
    return info["streams"][0]["width"], info["streams"][0]["height"], float(info["format"]["duration"])

def frames(path):
    """Decode a clip as RGB frames at FPS."""
    ff = subprocess.Popen(["ffmpeg", "-v", "error", "-i", path, "-vf", f"fps={FPS}",
                           "-f", "rawvideo", "-pix_fmt", "rgb24", "-"], stdout=subprocess.PIPE)
    while True:
        data = ff.stdout.read(w * h * 3)
        if len(data) < w * h * 3:
            return
        yield np.frombuffer(data, np.uint8).reshape(h, w, 3)

def fold(x):
    """0 -> 1 -> 0 every 2 units, so the picture repeats mirrored with no seams."""
    return np.abs((x + 1) % 2 - 1)

def strength(t):
    """How far into the tunnel the picture is at time t (0 = normal, 1 = full tunnel), eased in and out."""
    p = max(min((t - START) / MORPH, (END - t) / MORPH, 1), 0)
    return p * p * (3 - 2 * p)

def sample(frame, sx, sy):
    """Read the frame at fractional pixel positions, blending the four nearest pixels."""
    x0, y0 = np.floor(sx).astype(int), np.floor(sy).astype(int)
    fx, fy = (sx - x0)[..., None], (sy - y0)[..., None]
    x1, y1 = np.minimum(x0 + 1, w - 1), np.minimum(y0 + 1, h - 1)
    top = frame[y0, x0] * (1 - fx) + frame[y0, x1] * fx
    bottom = frame[y1, x0] * (1 - fx) + frame[y1, x1] * fx
    return top * (1 - fy) + bottom * fy

def tunnel(frame, t, e):
    """One frame at tunnel strength e: each pixel's angle round the center picks a point across the picture
    and its distance picks a point down it (depth / distance, so it stretches toward the center)."""
    angle = WRAPS * (np.arctan2(xs - w / 2, h / 2 - ys) / np.pi + 2 * SPIN * (t - START))
    tx = fold(angle) * (w - 1)
    ty = fold(DEPTH / np.maximum(radius, 1e-3) + SPEED * (t - START)) * (h - 1)
    sx = np.clip((1 - e) * xs + e * tx, 0, w - 1)
    sy = np.clip((1 - e) * ys + e * ty, 0, h - 1)
    fog = e * (1 - np.clip(radius / FOG, 0, 1) ** 1.5)[..., None]
    return sample(frame.astype(np.float32), sx, sy) * (1 - fog) + np.array(FOG_COLOR, np.float32) * fog

src, out = sys.argv[1:3]
w, h, duration = probe(src)
ys, xs = np.mgrid[0:h, 0:w].astype(np.float32)
radius = np.hypot(xs - w / 2, ys - h / 2) / (min(w, h) / 2)

enc = subprocess.Popen(["ffmpeg", "-y", "-v", "error", "-f", "rawvideo", "-pix_fmt", "rgb24", "-s", f"{w}x{h}",
                        "-r", str(FPS), "-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)
for i, frame in enumerate(frames(src)):
    e = strength(i / FPS)
    enc.stdin.write((tunnel(frame, i / FPS, e).astype(np.uint8) if e > 0 else frame).tobytes())
enc.stdin.close()
if enc.wait():
    sys.exit("ffmpeg failed")
```

## More distortion and glitch

- [Anaglyph 3D **Anaglyph 3D**](https://www.recharm.com/effects/anaglyph-3d)
- [Black hole **Black hole**](https://www.recharm.com/effects/black-hole)
- [Bulge and pinch **Bulge and pinch**](https://www.recharm.com/effects/bulge-pinch)
- [Datamosh **Datamosh**](https://www.recharm.com/effects/datamosh)
- [Disintegration **Disintegration**](https://www.recharm.com/effects/disintegration)

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