# /// script # requires-python = ">=3.10" # dependencies = ["numpy", "opencv-python"] # /// """ @effect tile_grid @version 1.0.0 @author artdag @description Tile Grid effect. Repeats the frame in a grid pattern creating a mosaic by tiling scaled-down copies. Great for psychedelic visuals. @param rows int @range 1 10 @default 2 Number of rows in grid. @param cols int @range 1 10 @default 2 Number of columns in grid. @param gap int @range 0 50 @default 0 Gap between tiles in pixels. @param gap_color list @default [0, 0, 0] RGB color for gaps. @param rotation_per_tile float @range -180 180 @default 0 Rotation increment per tile in degrees. @param alternate_flip bool @default false Flip alternating tiles horizontally. @example (effect tile_grid :rows 3 :cols 3) @example ;; Rotating tiles (effect tile_grid :rows 2 :cols 2 :rotation_per_tile 90) @example ;; Beat-reactive grid (effect tile_grid :rows (bind bass :range [2 6]) :cols (bind bass :range [2 6])) """ import numpy as np import cv2 def process_frame(frame: np.ndarray, params: dict, state: dict) -> tuple: """ Apply tile grid effect to a video frame. Args: frame: Input frame as numpy array (H, W, 3) RGB uint8 params: Effect parameters - rows: number of rows (default 2) - cols: number of columns (default 2) - gap: gap between tiles (default 0) - gap_color: RGB tuple (default [0,0,0]) - rotation_per_tile: rotation increment (default 0) - alternate_flip: flip alternating tiles (default False) state: Persistent state dict Returns: Tuple of (processed_frame, new_state) """ rows = max(1, min(int(params.get("rows", 2)), 10)) cols = max(1, min(int(params.get("cols", 2)), 10)) gap = max(0, int(params.get("gap", 0))) gap_color = params.get("gap_color", [0, 0, 0]) rotation_per_tile = params.get("rotation_per_tile", 0) alternate_flip = params.get("alternate_flip", False) if state is None: state = {} h, w = frame.shape[:2] # Calculate tile size tile_w = (w - gap * (cols - 1)) // cols tile_h = (h - gap * (rows - 1)) // rows if tile_w <= 0 or tile_h <= 0: return frame, state # Scale down the original frame to tile size tile = cv2.resize(frame, (tile_w, tile_h), interpolation=cv2.INTER_LINEAR) # Create result with gap color if isinstance(gap_color, (list, tuple)) and len(gap_color) >= 3: result = np.full((h, w, 3), gap_color[:3], dtype=np.uint8) else: result = np.zeros((h, w, 3), dtype=np.uint8) # Place tiles tile_idx = 0 for row in range(rows): for col in range(cols): y = row * (tile_h + gap) x = col * (tile_w + gap) current_tile = tile.copy() # Apply rotation if specified if rotation_per_tile != 0: angle = rotation_per_tile * tile_idx center = (tile_w // 2, tile_h // 2) M = cv2.getRotationMatrix2D(center, angle, 1.0) current_tile = cv2.warpAffine(current_tile, M, (tile_w, tile_h), borderMode=cv2.BORDER_REFLECT) # Apply flip for alternating tiles if alternate_flip and (row + col) % 2 == 1: current_tile = cv2.flip(current_tile, 1) # Place tile y_end = min(y + tile_h, h) x_end = min(x + tile_w, w) tile_crop_h = y_end - y tile_crop_w = x_end - x result[y:y_end, x:x_end] = current_tile[:tile_crop_h, :tile_crop_w] tile_idx += 1 return result, state