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>
165 lines
4.1 KiB
Python
165 lines
4.1 KiB
Python
# /// script
|
|
# requires-python = ">=3.10"
|
|
# dependencies = ["numpy"]
|
|
# ///
|
|
"""
|
|
@effect beam
|
|
@version 1.0.0
|
|
@author artdag
|
|
|
|
@description
|
|
Beam effect. Creates animated light beams / lasers from a starting
|
|
point to an ending point with glow effect.
|
|
|
|
@param start_x float
|
|
@range 0 1
|
|
@default 0
|
|
Beam start X position (0-1).
|
|
|
|
@param start_y float
|
|
@range 0 1
|
|
@default 0.5
|
|
Beam start Y position (0-1).
|
|
|
|
@param end_x float
|
|
@range 0 1
|
|
@default 1
|
|
Beam end X position (0-1).
|
|
|
|
@param end_y float
|
|
@range 0 1
|
|
@default 0.5
|
|
Beam end Y position (0-1).
|
|
|
|
@param thickness float
|
|
@range 1 50
|
|
@default 5
|
|
Beam core thickness in pixels.
|
|
|
|
@param glow_radius float
|
|
@range 0 100
|
|
@default 20
|
|
Outer glow size in pixels.
|
|
|
|
@param color list
|
|
@default [0, 255, 255]
|
|
Beam color RGB (default cyan).
|
|
|
|
@param intensity float
|
|
@range 0 2
|
|
@default 1.0
|
|
Beam brightness.
|
|
|
|
@param pulse bool
|
|
@default false
|
|
Enable pulsing animation.
|
|
|
|
@param pulse_speed float
|
|
@range 0.1 10
|
|
@default 2.0
|
|
Pulse animation speed.
|
|
|
|
@example
|
|
(effect beam :start_x 0 :start_y 0.5 :end_x 1 :end_y 0.5)
|
|
|
|
@example
|
|
;; Reactive laser
|
|
(effect beam :intensity (bind bass :range [0.5 2]) :color [255 0 0])
|
|
"""
|
|
|
|
import numpy as np
|
|
|
|
|
|
def process_frame(frame: np.ndarray, params: dict, state: dict) -> tuple:
|
|
"""
|
|
Apply beam effect to a video frame.
|
|
|
|
Args:
|
|
frame: Input frame as numpy array (H, W, 3) RGB uint8
|
|
params: Effect parameters
|
|
state: Persistent state dict
|
|
|
|
Returns:
|
|
Tuple of (processed_frame, new_state)
|
|
"""
|
|
start_x = params.get("start_x", 0)
|
|
start_y = params.get("start_y", 0.5)
|
|
end_x = params.get("end_x", 1)
|
|
end_y = params.get("end_y", 0.5)
|
|
thickness = params.get("thickness", 5)
|
|
glow_radius = params.get("glow_radius", 20)
|
|
color = params.get("color", [0, 255, 255])
|
|
intensity = params.get("intensity", 1.0)
|
|
pulse = params.get("pulse", False)
|
|
pulse_speed = params.get("pulse_speed", 2.0)
|
|
t = params.get("_time", 0)
|
|
|
|
if state is None:
|
|
state = {}
|
|
|
|
h, w = frame.shape[:2]
|
|
result = frame.copy().astype(np.float32)
|
|
|
|
# Calculate beam endpoints in pixels
|
|
x1, y1 = int(start_x * w), int(start_y * h)
|
|
x2, y2 = int(end_x * w), int(end_y * h)
|
|
|
|
# Apply pulse modulation
|
|
if pulse:
|
|
pulse_mod = 0.5 + 0.5 * np.sin(t * pulse_speed * 2 * np.pi)
|
|
intensity = intensity * pulse_mod
|
|
|
|
# Create coordinate grids
|
|
y_coords, x_coords = np.mgrid[0:h, 0:w].astype(np.float32)
|
|
|
|
# Calculate distance from each pixel to the line segment
|
|
line_vec = np.array([x2 - x1, y2 - y1], dtype=np.float32)
|
|
line_len = np.sqrt(line_vec[0]**2 + line_vec[1]**2)
|
|
|
|
if line_len < 1:
|
|
return frame, state
|
|
|
|
line_unit = line_vec / line_len
|
|
|
|
# Vector from start to each pixel
|
|
px = x_coords - x1
|
|
py = y_coords - y1
|
|
|
|
# Project onto line
|
|
proj_len = px * line_unit[0] + py * line_unit[1]
|
|
proj_len = np.clip(proj_len, 0, line_len)
|
|
|
|
# Closest point on line
|
|
closest_x = x1 + proj_len * line_unit[0]
|
|
closest_y = y1 + proj_len * line_unit[1]
|
|
|
|
# Distance to closest point
|
|
dist = np.sqrt((x_coords - closest_x)**2 + (y_coords - closest_y)**2)
|
|
|
|
# Get beam color
|
|
if isinstance(color, (list, tuple)) and len(color) >= 3:
|
|
beam_color = np.array(color[:3], dtype=np.float32)
|
|
else:
|
|
beam_color = np.array([0, 255, 255], dtype=np.float32)
|
|
|
|
# Core beam (bright center)
|
|
core_mask = dist < thickness
|
|
core_intensity = intensity * (1 - dist[core_mask] / max(1, thickness))
|
|
for c in range(3):
|
|
result[core_mask, c] = np.clip(
|
|
result[core_mask, c] + beam_color[c] * core_intensity,
|
|
0, 255
|
|
)
|
|
|
|
# Glow (fading outer region)
|
|
glow_mask = (dist >= thickness) & (dist < thickness + glow_radius)
|
|
glow_dist = dist[glow_mask] - thickness
|
|
glow_intensity = intensity * 0.5 * (1 - glow_dist / max(1, glow_radius)) ** 2
|
|
for c in range(3):
|
|
result[glow_mask, c] = np.clip(
|
|
result[glow_mask, c] + beam_color[c] * glow_intensity,
|
|
0, 255
|
|
)
|
|
|
|
return result.astype(np.uint8), state
|