Add S-expression based video effects pipeline with modular effect definitions, constructs, and recipe files. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
122 lines
3.1 KiB
Python
122 lines
3.1 KiB
Python
# /// script
|
|
# requires-python = ">=3.10"
|
|
# dependencies = ["numpy", "opencv-python"]
|
|
# ///
|
|
"""
|
|
@effect swirl
|
|
@version 1.0.0
|
|
@author artdag
|
|
|
|
@description
|
|
Spiral/vortex distortion that twists the image around a center point.
|
|
Creates whirlpool-like effects. Great for psychedelic/hypnotic visuals.
|
|
|
|
@param strength float
|
|
@range -10 10
|
|
@default 1.0
|
|
Swirl strength in radians. Positive = counter-clockwise, negative = clockwise.
|
|
|
|
@param radius float
|
|
@range 0.1 2
|
|
@default 0.5
|
|
Effect radius as fraction of image size. Larger = wider swirl.
|
|
|
|
@param center_x float
|
|
@range 0 1
|
|
@default 0.5
|
|
Horizontal center of swirl (0 = left, 1 = right).
|
|
|
|
@param center_y float
|
|
@range 0 1
|
|
@default 0.5
|
|
Vertical center of swirl (0 = top, 1 = bottom).
|
|
|
|
@param falloff string
|
|
@enum linear quadratic gaussian
|
|
@default quadratic
|
|
How swirl strength decreases from center:
|
|
- linear: constant decrease
|
|
- quadratic: sharper center, softer edges
|
|
- gaussian: smooth bell curve
|
|
|
|
@example
|
|
(effect swirl :strength 2)
|
|
|
|
@example
|
|
;; Reactive swirl
|
|
(effect swirl :strength (bind energy :range [0 5]))
|
|
"""
|
|
|
|
import numpy as np
|
|
import cv2
|
|
|
|
|
|
def process_frame(frame: np.ndarray, params: dict, state: dict) -> tuple:
|
|
"""
|
|
Apply swirl distortion to a video frame.
|
|
|
|
Args:
|
|
frame: Input frame as numpy array (H, W, 3) RGB uint8
|
|
params: Effect parameters
|
|
- strength: swirl amount in radians (default 1.0)
|
|
- radius: effect radius as fraction (default 0.5)
|
|
- center_x: horizontal center 0-1 (default 0.5)
|
|
- center_y: vertical center 0-1 (default 0.5)
|
|
- falloff: linear/quadratic/gaussian (default quadratic)
|
|
state: Persistent state dict (unused)
|
|
|
|
Returns:
|
|
Tuple of (processed_frame, new_state)
|
|
"""
|
|
strength = params.get("strength", 1.0)
|
|
radius_frac = params.get("radius", 0.5)
|
|
center_x = params.get("center_x", 0.5)
|
|
center_y = params.get("center_y", 0.5)
|
|
falloff = params.get("falloff", "quadratic")
|
|
|
|
if strength == 0:
|
|
return frame, state
|
|
|
|
h, w = frame.shape[:2]
|
|
|
|
# Calculate center and radius in pixels
|
|
cx = w * center_x
|
|
cy = h * center_y
|
|
radius = max(w, h) * radius_frac
|
|
|
|
# Create coordinate grids
|
|
y_coords, x_coords = np.mgrid[0:h, 0:w].astype(np.float64)
|
|
|
|
# Calculate distance and angle from center
|
|
dx = x_coords - cx
|
|
dy = y_coords - cy
|
|
dist = np.sqrt(dx**2 + dy**2)
|
|
angle = np.arctan2(dy, dx)
|
|
|
|
# Normalized distance for falloff
|
|
norm_dist = dist / radius
|
|
|
|
# Calculate falloff factor
|
|
if falloff == "linear":
|
|
factor = np.maximum(0, 1 - norm_dist)
|
|
elif falloff == "gaussian":
|
|
factor = np.exp(-norm_dist**2 * 2)
|
|
else: # quadratic
|
|
factor = np.maximum(0, 1 - norm_dist**2)
|
|
|
|
# Apply swirl rotation
|
|
new_angle = angle + strength * factor
|
|
|
|
# Calculate new coordinates
|
|
new_x = (cx + dist * np.cos(new_angle)).astype(np.float32)
|
|
new_y = (cy + dist * np.sin(new_angle)).astype(np.float32)
|
|
|
|
# Remap
|
|
result = cv2.remap(
|
|
frame, new_x, new_y,
|
|
cv2.INTER_LINEAR,
|
|
borderMode=cv2.BORDER_REFLECT
|
|
)
|
|
|
|
return result, state
|