# /// script # requires-python = ">=3.10" # dependencies = ["numpy"] # /// """ @effect trails @version 1.0.0 @author artdag @description Trails effect. Creates persistent motion trails by blending current frame with previous frames. Like echo but with configurable blend. @param persistence float @range 0 0.99 @default 0.8 How much of previous frame remains (0 = none, 0.99 = very long trails). @param blend_mode string @enum blend add screen lighten darken @default blend How to combine frames. @param fade_color list @default [0, 0, 0] Color to fade toward. @state trail_buffer ndarray Accumulated trail buffer. @example (effect trails :persistence 0.85) @example ;; Long bright trails (effect trails :persistence 0.9 :blend_mode "add") """ import numpy as np def process_frame(frame: np.ndarray, params: dict, state: dict) -> tuple: """ Apply trails effect to a video frame. Args: frame: Input frame as numpy array (H, W, 3) RGB uint8 params: Effect parameters - persistence: trail length 0-0.99 (default 0.8) - blend_mode: how to combine (default blend) - fade_color: color to fade to (default black) state: Persistent state dict Returns: Tuple of (processed_frame, new_state) """ persistence = max(0, min(params.get("persistence", 0.8), 0.99)) blend_mode = params.get("blend_mode", "blend") fade_color = params.get("fade_color", [0, 0, 0]) if state is None: state = {} # Initialize trail buffer if "trail_buffer" not in state or state["trail_buffer"].shape != frame.shape: state["trail_buffer"] = frame.astype(np.float32) buffer = state["trail_buffer"] current = frame.astype(np.float32) # Get fade color if isinstance(fade_color, (list, tuple)) and len(fade_color) >= 3: fade = np.array(fade_color[:3], dtype=np.float32) else: fade = np.array([0, 0, 0], dtype=np.float32) # Blend buffer toward fade color faded_buffer = buffer * persistence + fade * (1 - persistence) # Combine with current frame based on blend mode if blend_mode == "add": result = faded_buffer + current elif blend_mode == "screen": result = 255 - ((255 - faded_buffer) * (255 - current) / 255) elif blend_mode == "lighten": result = np.maximum(faded_buffer, current) elif blend_mode == "darken": result = np.minimum(faded_buffer, current) else: # blend result = faded_buffer + current * (1 - persistence) # Update buffer state["trail_buffer"] = np.clip(result, 0, 255) return np.clip(result, 0, 255).astype(np.uint8), state