# /// script # requires-python = ">=3.10" # dependencies = ["numpy"] # /// """ @effect scanlines @version 1.0.0 @author artdag @description VHS/CRT-style scan line shift. Horizontally displaces alternating lines to create analog video distortion. Great for retro/glitch aesthetics. @param amplitude float @range 0 100 @default 10 Maximum shift amount in pixels. Bind to bass for reactive glitch. @param frequency float @range 1 100 @default 10 Lines per cycle (affects pattern density). @param randomness float @range 0 1 @default 0.5 0 = regular sine pattern, 1 = fully random shifts. @param line_gap int @range 1 20 @default 1 Only shift every Nth line (1 = all lines). @param seed int @default 42 Random seed for deterministic patterns. @state rng DeterministicRNG Random number generator for reproducible results. @example (effect scanlines :amplitude 20) @example ;; Heavy glitch on bass (effect scanlines :amplitude (bind bass :range [0 50]) :randomness 0.8) @example ;; Reproducible scanlines (effect scanlines :amplitude 30 :randomness 0.7 :seed 999) """ import numpy as np from pathlib import Path import sys # Import DeterministicRNG from same directory _effects_dir = Path(__file__).parent if str(_effects_dir) not in sys.path: sys.path.insert(0, str(_effects_dir)) from random import DeterministicRNG def process_frame(frame: np.ndarray, params: dict, state: dict) -> tuple: """ Apply scan line shift to a video frame. Args: frame: Input frame as numpy array (H, W, 3) RGB uint8 params: Effect parameters - amplitude: max shift in pixels (default 10) - frequency: lines per cycle (default 10) - randomness: 0-1 random vs sine (default 0.5) - line_gap: shift every Nth line (default 1) - seed: random seed (default 42) state: Persistent state dict - rng: DeterministicRNG instance Returns: Tuple of (processed_frame, new_state) """ amplitude = params.get("amplitude", 10) frequency = params.get("frequency", 10) or 1 randomness = params.get("randomness", 0.5) line_gap = max(1, int(params.get("line_gap", 1))) seed = int(params.get("seed", 42)) if state is None: state = {} # Initialize RNG if "rng" not in state: state["rng"] = DeterministicRNG(seed) rng = state["rng"] if amplitude == 0: return frame, state h, w = frame.shape[:2] result = frame.copy() for y in range(0, h, line_gap): # Calculate shift amount if randomness >= 1.0: shift = int(rng.uniform(-amplitude, amplitude)) elif randomness <= 0: shift = int(amplitude * np.sin(2 * np.pi * y / frequency)) else: sine_shift = amplitude * np.sin(2 * np.pi * y / frequency) rand_shift = rng.uniform(-amplitude, amplitude) shift = int(sine_shift * (1 - randomness) + rand_shift * randomness) if shift != 0: result[y] = np.roll(result[y], shift, axis=0) return result, state