How to add a prize wheel spin with Claude

A rainbow eight-prize wheel in front of the product whirls into a blur, slows to a stop with red "30% OFF" under the arrow, and a yellow "You won 30% off" banner pops in above it.

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 prize wheel spin to input.mp4
    Prizes: 30% OFF, FREE GIFT, 10% OFF, FREE SHIP, 20% OFF, 2 FOR 1, 5% OFF, 50% OFF
    Colors: red, white, purple, yellow, teal, pink, navy, orange
    Banner: YOU WON, 30% OFF

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 prize wheel spin 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
PRIZES30% OFF, FREE GIFT, 10% OFF, FREE SHIP, 20% OFF, 2 FOR 1, 5% OFF, 50% OFFSlice labels clockwise, starting with the one the wheel lands on.
COLORSred, white, purple, yellow, teal, pink, navy, orangeOne RGB color per slice.
SPIN_START, SPIN_END0.3, 3.8Seconds the wheel spins.
TURNS5Full turns before it lands.
SIZE0.86Wheel width as a share of the frame width.
X, Y0.5, 0.65Wheel center, 0-1 across and down the frame.
BANNER_AT4.0Second the banner pops in.
BANNERYOU WON, 30% OFFThe banner's two lines.

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 prize wheel spin to input.mp4. Draw a wheel 86% of the frame width across, centered at 50% across and 65% down, with a soft dark drop shadow: a dark (30, 20, 50) rim 6% of the wheel's width thick with 24 small cream (255, 245, 200) dots round it, eight equal slices with thin white dividers and a white hub (18% of the wheel's width) with a dark outline. Clockwise from the top the slices are red (230, 30, 45) "30% / OFF", white "FREE / GIFT", purple (120, 60, 220) "10% / OFF", yellow (255, 214, 0) "FREE / SHIP", teal (0, 170, 160) "20% / OFF", pink (255, 60, 150) "2 FOR / 1", navy (30, 40, 90) "5% / OFF" and orange (255, 140, 0) "50% / OFF", each label in bold on two lines at 66% of the radius, upright when its slice is at the top, dark text on light slices and white on dark ones. Put a white downward triangle with a dark outline, 11% of the frame width, at 12 o'clock over the rim as the pointer; it doesn't move. The wheel starts turned 160 degrees counterclockwise from its landing position, holds until 0.3 seconds, then spins clockwise 5 full turns plus those 160 degrees, ending at 3.8 seconds with the red 30% OFF slice under the pointer; ease it out with a cubic curve so it starts fast and slows the whole way, and motion-blur each frame by averaging the wheel at several angles across the frame's turn. At 4.0 seconds pop in a yellow (255, 214, 0) rounded banner 84% of the frame width wide and 14% of the frame height tall, centered at 13% down, with a dark outline and "YOU WON" over "30% OFF" in dark bold text; it scales up over 0.35 seconds with a slight overshoot and stays. Use Python with Pillow to draw the wheel, labels and banner and ffmpeg to put them on the video (don't rely on ffmpeg's drawtext filter), keep the audio, and save it as output.mp4.

Run it without Claude

This is the exact script we tested. Save it as prize-wheel-spin.py and run:

python3 prize-wheel-spin.py input.mp4 output.mp4
Show the script
#!/usr/bin/env python3
"""Prize wheel spin: an eight-prize wheel whirls, slows to a stop on "30% OFF" and a "You won" banner pops in.

Usage: python3 prize-wheel-spin.py input.mp4 output.mp4
Needs ffmpeg, Pillow and numpy (pip install pillow numpy).
"""
import json
import math
import subprocess
import sys
from pathlib import Path

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

PRIZES = [("30%", "OFF"), ("FREE", "GIFT"), ("10%", "OFF"), ("FREE", "SHIP"),   # clockwise from the winner
          ("20%", "OFF"), ("2 FOR", "1"), ("5%", "OFF"), ("50%", "OFF")]
COLORS = [(230, 30, 45), (255, 255, 255), (120, 60, 220), (255, 214, 0),        # one per prize
          (0, 170, 160), (255, 60, 150), (30, 40, 90), (255, 140, 0)]
SPIN_START, SPIN_END = 0.3, 3.8   # seconds the wheel spins; it slows down the whole way
TURNS = 5                          # full turns before it lands
SIZE = 0.86                        # wheel width as a share of the frame width
X, Y = 0.5, 0.65                   # wheel center (0-1 across and down the frame)
BANNER_AT = 4.0                    # second the "You won" banner pops in
BANNER = ("YOU WON", "30% OFF")    # banner's two lines
BANNER_COLOR = (255, 214, 0)
INK = (30, 20, 50)                 # dark text and rim color
FONTS = ["/System/Library/Fonts/Supplemental/Arial Bold.ttf",     # macOS
         "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",  # Linux
         "C:/Windows/Fonts/arialbd.ttf"]                          # Windows
FONT_FILE = next((f for f in FONTS if Path(f).exists()), None) or sys.exit("No bold font found: add one to FONTS")


def font(size):
    return ImageFont.truetype(FONT_FILE, round(size))


def wheel():
    """The wheel with the winning prize at 12 o'clock, every label upright when its slice is at the top."""
    d = round(w * SIZE)
    r, step = d / 2, 360 / len(PRIZES)
    img = Image.new("RGBA", (d, d))
    draw = ImageDraw.Draw(img)
    draw.ellipse([0, 0, d - 1, d - 1], fill=INK)
    inner = [d * 0.06, d * 0.06, d * 0.94, d * 0.94]
    for i, color in enumerate(COLORS):
        mid = -90 + i * step                            # Pillow angles run clockwise from 3 o'clock
        draw.pieslice(inner, mid - step / 2, mid + step / 2, fill=color, outline="white", width=3)
    for i in range(24):                                 # light bulbs round the rim
        a, b = math.radians(i * 15 + 7.5), d * 0.012
        bx, by = r + r * 0.94 * math.sin(a), r - r * 0.94 * math.cos(a)
        draw.ellipse([bx - b, by - b, bx + b, by + b], fill=(255, 245, 200))
    for i, ((top, bottom), color) in enumerate(zip(PRIZES, COLORS)):
        ink = INK if sum(color) > 450 else "white"
        label = Image.new("RGBA", (round(d * 0.3), round(d * 0.2)))
        ld = ImageDraw.Draw(label)
        ld.text((label.width / 2, label.height * 0.32), top, font=font(d * 0.072), anchor="mm", fill=ink)
        ld.text((label.width / 2, label.height * 0.75), bottom, font=font(d * 0.054), anchor="mm", fill=ink)
        label = label.rotate(-i * step, resample=Image.BICUBIC, expand=True)
        a = math.radians(i * step)
        cx, cy = r + r * 0.66 * math.sin(a), r - r * 0.66 * math.cos(a)
        img.alpha_composite(label, (round(cx - label.width / 2), round(cy - label.height / 2)))
    draw.ellipse([r - d * 0.09, r - d * 0.09, r + d * 0.09, r + d * 0.09], fill="white", outline=INK, width=round(d * 0.02))
    return img


def angle(t):
    """Clockwise turn in degrees: starts fast, eases out (cubic) and lands with the winner back at the top."""
    total = TURNS * 360 + 160
    p = min(max((t - SPIN_START) / (SPIN_END - SPIN_START), 0), 1)
    return total * (1 - (1 - p) ** 3) - total


def spun(t):
    """The wheel at time t, motion-blurred by averaging enough in-between angles to cover one frame's turn."""
    a0, a1 = angle(t), angle(t + 1 / fps)
    n = max(1, math.ceil(abs(a1 - a0) / 3))
    layers = [np.asarray(disc.rotate(-(a0 + (a1 - a0) * k / n), resample=Image.BICUBIC), np.float32) for k in range(n)]
    return Image.fromarray(np.mean(layers, axis=0).astype(np.uint8))


def pointer():
    """A white arrow at 12 o'clock pointing down into the wheel."""
    s = round(w * 0.11)
    img = Image.new("RGBA", (s, s))
    ImageDraw.Draw(img).polygon([(s * 0.1, s * 0.05), (s * 0.9, s * 0.05), (s / 2, s * 0.95)],
                                fill="white", outline=INK, width=max(2, s // 14))
    return img


def banner():
    bw, bh = round(w * 0.84), round(h * 0.14)
    img = Image.new("RGBA", (bw, bh))
    draw = ImageDraw.Draw(img)
    draw.rounded_rectangle([0, 0, bw - 1, bh - 1], radius=bh * 0.25, fill=BANNER_COLOR, outline=INK, width=4)
    draw.text((bw / 2, bh * 0.3), BANNER[0], font=font(bh * 0.26), anchor="mm", fill=INK)
    draw.text((bw / 2, bh * 0.66), BANNER[1], font=font(bh * 0.42), anchor="mm", fill=INK)
    return img


def shadow():
    img = Image.new("RGBA", (w, h))
    d = disc.width
    ImageDraw.Draw(img).ellipse([w * X - d / 2, h * Y - d / 2 + d * 0.04, w * X + d / 2, h * Y + d / 2 + d * 0.04], fill=(0, 0, 0, 120))
    return img.filter(ImageFilter.GaussianBlur(d * 0.03))


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), 1)
    return 1 + 2.70158 * (p - 1) ** 3 + 1.70158 * (p - 1) ** 2


def overlay(t):
    img = base.copy()
    img.alpha_composite(spun(t), (round(w * X - disc.width / 2), round(h * Y - disc.height / 2)))
    img.alpha_composite(arrow, (round(w * X - arrow.width / 2), round(h * Y - disc.height / 2 - arrow.height * 0.45)))
    scale = ease_out_back((t - BANNER_AT) / 0.35) if t >= BANNER_AT else 0
    if scale > 0.02:
        b = won.resize((round(won.width * scale), max(1, round(won.height * scale))), Image.LANCZOS)
        img.alpha_composite(b, (round(w / 2 - b.width / 2), round(h * 0.13 - b.height / 2)))
    return img


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"])


src, out = sys.argv[1:3]
w, h, duration = probe(src)
fps = 30
disc, arrow, won = wheel(), pointer(), banner()
base = shadow()
# overlay frames come in on stdin and go over the video, which keeps its own audio
ff = subprocess.Popen(["ffmpeg", "-y", "-v", "error", "-i", src,
                       "-f", "rawvideo", "-pix_fmt", "rgba", "-s", f"{w}x{h}", "-r", str(fps), "-i", "-",
                       "-filter_complex", f"[0:v]fps={fps}[v0];[v0][1:v]overlay=shortest=1,format=yuv420p[v]",
                       "-map", "[v]", "-map", "0:a?", "-c:v", "libx264", "-crf", "18", "-c:a", "copy", out],
                      stdin=subprocess.PIPE)
for i in range(math.ceil(duration * fps)):
    ff.stdin.write(overlay(i / fps).tobytes())
ff.stdin.close()
if ff.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