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>
109 lines
2.8 KiB
Python
109 lines
2.8 KiB
Python
# /// script
|
|
# requires-python = ">=3.10"
|
|
# dependencies = ["numpy", "opencv-python"]
|
|
# ///
|
|
"""
|
|
@effect wave
|
|
@version 1.0.0
|
|
@author artdag
|
|
|
|
@description
|
|
Sine wave displacement distortion. Creates wavy, liquid-like warping.
|
|
Great for psychedelic and underwater effects synced to music.
|
|
|
|
@param amplitude float
|
|
@range 0 100
|
|
@default 10
|
|
Wave height in pixels. Bind to bass for punchy distortion.
|
|
|
|
@param wavelength float
|
|
@range 10 500
|
|
@default 50
|
|
Distance between wave peaks in pixels.
|
|
|
|
@param speed float
|
|
@range 0 10
|
|
@default 1
|
|
Wave animation speed. Uses state to track phase over time.
|
|
|
|
@param direction string
|
|
@enum horizontal vertical both
|
|
@default horizontal
|
|
Wave direction:
|
|
- horizontal: waves move left-right
|
|
- vertical: waves move up-down
|
|
- both: waves in both directions
|
|
|
|
@state phase float
|
|
Tracks wave animation phase across frames.
|
|
|
|
@example
|
|
(effect wave :amplitude 20 :wavelength 100)
|
|
|
|
@example
|
|
;; Bass-reactive waves
|
|
(effect wave :amplitude (bind bass :range [0 50] :transform sqrt))
|
|
"""
|
|
|
|
import numpy as np
|
|
import cv2
|
|
|
|
|
|
def process_frame(frame: np.ndarray, params: dict, state: dict) -> tuple:
|
|
"""
|
|
Apply wave distortion to a video frame.
|
|
|
|
Args:
|
|
frame: Input frame as numpy array (H, W, 3) RGB uint8
|
|
params: Effect parameters
|
|
- amplitude: wave height in pixels (default 10)
|
|
- wavelength: distance between peaks (default 50)
|
|
- speed: animation speed (default 1)
|
|
- direction: horizontal/vertical/both (default horizontal)
|
|
state: Persistent state dict
|
|
- phase: current wave phase
|
|
|
|
Returns:
|
|
Tuple of (processed_frame, new_state)
|
|
"""
|
|
amplitude = params.get("amplitude", 10)
|
|
wavelength = params.get("wavelength", 50)
|
|
speed = params.get("speed", 1)
|
|
direction = params.get("direction", "horizontal")
|
|
|
|
if state is None:
|
|
state = {}
|
|
|
|
if amplitude == 0:
|
|
return frame, state
|
|
|
|
h, w = frame.shape[:2]
|
|
|
|
# Update phase for animation
|
|
phase = state.get("phase", 0)
|
|
phase += speed * 0.1
|
|
state["phase"] = phase
|
|
|
|
# Create coordinate maps
|
|
map_x = np.tile(np.arange(w, dtype=np.float32), (h, 1))
|
|
map_y = np.tile(np.arange(h, dtype=np.float32).reshape(-1, 1), (1, w))
|
|
|
|
if direction in ("horizontal", "both"):
|
|
# Horizontal waves: displace X based on Y
|
|
wave = np.sin(2 * np.pi * map_y / wavelength + phase) * amplitude
|
|
map_x = map_x + wave
|
|
|
|
if direction in ("vertical", "both"):
|
|
# Vertical waves: displace Y based on X
|
|
wave = np.sin(2 * np.pi * map_x / wavelength + phase) * amplitude
|
|
map_y = map_y + wave
|
|
|
|
# Apply distortion
|
|
result = cv2.remap(
|
|
frame, map_x, map_y,
|
|
cv2.INTER_LINEAR,
|
|
borderMode=cv2.BORDER_REFLECT
|
|
)
|
|
|
|
return result, state
|