Files
test/effects/trails.py
gilesb 406cc7c0c7 Initial commit: video effects processing system
Add S-expression based video effects pipeline with modular effect
definitions, constructs, and recipe files.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 12:34:45 +00:00

96 lines
2.6 KiB
Python

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