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>
This commit is contained in:
164
effects/beam.py
Normal file
164
effects/beam.py
Normal file
@@ -0,0 +1,164 @@
|
||||
# /// 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
|
||||
Reference in New Issue
Block a user