# /// 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