Files
test/effects/color_cycle.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

105 lines
2.8 KiB
Python

# /// script
# requires-python = ">=3.10"
# dependencies = ["numpy", "opencv-python"]
# ///
"""
@effect color_cycle
@version 1.0.0
@author artdag
@description
Color cycle effect. Shifts all hues over time creating psychedelic
rainbow cycling. Great for trippy visuals.
@param speed float
@range 0 10
@default 1
Cycle speed (rotations per second).
@param offset float
@range 0 360
@default 0
Initial hue offset in degrees.
@param saturation_boost float
@range 0 2
@default 1
Saturation multiplier.
@param mode string
@enum all highlights shadows midtones
@default all
Which tones to affect.
@example
(effect color_cycle :speed 0.5)
@example
;; Beat-synced color shift
(effect color_cycle :offset (bind beat_position :range [0 360]))
"""
import numpy as np
import cv2
def process_frame(frame: np.ndarray, params: dict, state: dict) -> tuple:
"""
Apply color cycle effect to a video frame.
Args:
frame: Input frame as numpy array (H, W, 3) RGB uint8
params: Effect parameters
- speed: rotations per second (default 1)
- offset: initial hue offset (default 0)
- saturation_boost: saturation multiplier (default 1)
- mode: which tones to affect (default all)
state: Persistent state dict
Returns:
Tuple of (processed_frame, new_state)
"""
speed = params.get("speed", 1)
offset = params.get("offset", 0)
saturation_boost = max(0, min(params.get("saturation_boost", 1), 2))
mode = params.get("mode", "all")
t = params.get("_time", 0)
if state is None:
state = {}
# Calculate hue shift
hue_shift = int((offset + speed * t * 360) % 360)
# Convert to HSV (OpenCV uses BGR, our frame is RGB)
frame_bgr = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)
hsv = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2HSV).astype(np.float32)
if mode == "all":
# Shift all hues
hsv[:, :, 0] = (hsv[:, :, 0] + hue_shift / 2) % 180
hsv[:, :, 1] = np.clip(hsv[:, :, 1] * saturation_boost, 0, 255)
else:
# Calculate luminance mask
lum = hsv[:, :, 2] / 255.0
if mode == "highlights":
mask = np.clip((lum - 0.67) * 3, 0, 1)
elif mode == "shadows":
mask = np.clip(1 - lum * 3, 0, 1)
else: # midtones
shadow_mask = np.clip(1 - lum * 3, 0, 1)
highlight_mask = np.clip((lum - 0.67) * 3, 0, 1)
mask = 1 - shadow_mask - highlight_mask
# Apply selective hue shift
shifted_hue = (hsv[:, :, 0] + hue_shift / 2) % 180
hsv[:, :, 0] = hsv[:, :, 0] * (1 - mask) + shifted_hue * mask
# Convert back
hsv = np.clip(hsv, 0, 255).astype(np.uint8)
result_bgr = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)
result = cv2.cvtColor(result_bgr, cv2.COLOR_BGR2RGB)
return result, state