Files
test/effects/wave.py
gilesb 406cc7c0c7 Initial commit: video effects processing system
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>
2026-01-19 12:34:45 +00:00

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