# /// script # requires-python = ">=3.10" # dependencies = ["numpy", "opencv-python"] # /// """ @effect kaleidoscope @version 1.0.0 @author artdag @description Kaleidoscope effect. Creates mesmerizing mandala-like patterns by dividing the frame into pie-slice segments and reflecting them. Great for psychedelic visuals. @param segments int @range 3 16 @default 6 Number of symmetry segments. @param rotation float @range 0 360 @default 0 Base rotation angle in degrees. @param rotation_speed float @range -180 180 @default 0 Continuous rotation speed in degrees/second. @param center_x float @range 0 1 @default 0.5 Center X position (0-1). @param center_y float @range 0 1 @default 0.5 Center Y position (0-1). @param zoom float @range 0.5 3.0 @default 1.0 Zoom factor for the source region. @state cumulative_rotation float Tracks rotation over time. @example (effect kaleidoscope :segments 8 :rotation_speed 30) @example ;; Beat-reactive segments (effect kaleidoscope :segments (bind bass :range [4 12]) :zoom 1.5) """ import numpy as np import cv2 def process_frame(frame: np.ndarray, params: dict, state: dict) -> tuple: """ Apply kaleidoscope effect to a video frame. Args: frame: Input frame as numpy array (H, W, 3) RGB uint8 params: Effect parameters - segments: number of segments 3-16 (default 6) - rotation: base rotation degrees (default 0) - rotation_speed: degrees per second (default 0) - center_x: center X 0-1 (default 0.5) - center_y: center Y 0-1 (default 0.5) - zoom: zoom factor 0.5-3 (default 1.0) state: Persistent state dict Returns: Tuple of (processed_frame, new_state) """ segments = max(3, min(int(params.get("segments", 6)), 16)) rotation = params.get("rotation", 0) rotation_speed = params.get("rotation_speed", 0) center_x = params.get("center_x", 0.5) center_y = params.get("center_y", 0.5) zoom = max(0.5, min(params.get("zoom", 1.0), 3.0)) # Get time for animation t = params.get("_time", 0) if state is None: state = {} h, w = frame.shape[:2] # Calculate center in pixels cx = int(w * center_x) cy = int(h * center_y) # Total rotation including time-based animation total_rotation = rotation + rotation_speed * t # Calculate the angle per segment segment_angle = 2 * np.pi / segments # Create coordinate maps y_coords, x_coords = np.mgrid[0:h, 0:w].astype(np.float32) # Translate to center x_centered = x_coords - cx y_centered = y_coords - cy # Convert to polar coordinates r = np.sqrt(x_centered**2 + y_centered**2) theta = np.arctan2(y_centered, x_centered) # Apply rotation theta = theta - np.deg2rad(total_rotation) # Fold angle into first segment and mirror theta_normalized = theta % (2 * np.pi) segment_idx = (theta_normalized / segment_angle).astype(int) theta_in_segment = theta_normalized - segment_idx * segment_angle # Mirror alternating segments mirror_mask = (segment_idx % 2) == 1 theta_in_segment = np.where(mirror_mask, segment_angle - theta_in_segment, theta_in_segment) # Apply zoom r = r / zoom # Convert back to Cartesian (source coordinates) src_x = (r * np.cos(theta_in_segment) + cx).astype(np.float32) src_y = (r * np.sin(theta_in_segment) + cy).astype(np.float32) # Remap result = cv2.remap(frame, src_x, src_y, cv2.INTER_LINEAR, borderMode=cv2.BORDER_REFLECT) return result, state