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>
111 lines
2.8 KiB
Python
111 lines
2.8 KiB
Python
# /// script
|
|
# requires-python = ">=3.10"
|
|
# dependencies = ["numpy", "opencv-python"]
|
|
# ///
|
|
"""
|
|
@effect rotate
|
|
@version 1.0.0
|
|
@author artdag
|
|
|
|
@description
|
|
Rotates the frame by a specified angle. Supports two modes:
|
|
- Static: rotate by fixed angle
|
|
- Continuous: rotate by speed (degrees per frame), accumulating over time
|
|
|
|
The effect uses state to track cumulative rotation for smooth continuous motion.
|
|
|
|
@param angle float
|
|
@range -360 360
|
|
@default 0
|
|
Rotation angle in degrees. Positive = counter-clockwise.
|
|
|
|
@param speed float
|
|
@range -30 30
|
|
@default 0
|
|
Rotation speed in degrees per frame (for continuous rotation).
|
|
When non-zero, angle is ignored and rotation accumulates.
|
|
|
|
@param auto_zoom bool
|
|
@default false
|
|
If true, automatically zooms to crop out black corners.
|
|
|
|
@state cumulative_angle float
|
|
Tracks total rotation for continuous mode. Persists across frames.
|
|
|
|
@example
|
|
(effect rotate :angle 45)
|
|
|
|
@example
|
|
;; Continuous spin at 5 degrees per frame
|
|
(effect rotate :speed 5)
|
|
|
|
@example
|
|
;; Beat-reactive rotation
|
|
(effect rotate :angle (bind bass :range [0 90]))
|
|
"""
|
|
|
|
import numpy as np
|
|
import cv2
|
|
import math
|
|
|
|
|
|
def process_frame(frame: np.ndarray, params: dict, state: dict) -> tuple:
|
|
"""
|
|
Rotate a video frame.
|
|
|
|
Args:
|
|
frame: Input frame as numpy array (H, W, 3) RGB uint8
|
|
params: Effect parameters
|
|
- angle: rotation angle in degrees (default 0)
|
|
- speed: rotation speed per frame for continuous mode (default 0)
|
|
- auto_zoom: zoom to hide black corners (default False)
|
|
state: Persistent state dict
|
|
- cumulative_angle: total rotation for continuous mode
|
|
|
|
Returns:
|
|
Tuple of (processed_frame, new_state)
|
|
"""
|
|
angle = params.get("angle", 0)
|
|
speed = params.get("speed", 0)
|
|
auto_zoom = params.get("auto_zoom", False)
|
|
|
|
# Initialize state
|
|
if state is None:
|
|
state = {}
|
|
|
|
# Handle continuous rotation mode
|
|
if speed != 0:
|
|
cumulative = state.get("cumulative_angle", 0)
|
|
cumulative += speed
|
|
state["cumulative_angle"] = cumulative
|
|
angle = cumulative
|
|
|
|
if angle == 0:
|
|
return frame, state
|
|
|
|
h, w = frame.shape[:2]
|
|
center = (w / 2, h / 2)
|
|
|
|
# Calculate zoom factor to hide black corners
|
|
scale = 1.0
|
|
if auto_zoom:
|
|
angle_rad = math.radians(abs(angle) % 90)
|
|
if w >= h:
|
|
scale = math.cos(angle_rad) + (w / h) * math.sin(angle_rad)
|
|
else:
|
|
scale = math.cos(angle_rad) + (h / w) * math.sin(angle_rad)
|
|
scale = max(1.0, scale)
|
|
|
|
# Get rotation matrix
|
|
rotation_matrix = cv2.getRotationMatrix2D(center, angle, scale)
|
|
|
|
# Apply rotation
|
|
result = cv2.warpAffine(
|
|
frame, rotation_matrix, (w, h),
|
|
flags=cv2.INTER_LINEAR,
|
|
borderMode=cv2.BORDER_CONSTANT,
|
|
borderValue=(0, 0, 0)
|
|
)
|
|
|
|
return result, state
|