Implements ascii_fx_zone effect that allows applying arbitrary sexp effects to each character cell via cell_effect lambdas. Each cell is rendered as a small image that effects can operate on. Key changes: - New ascii_fx_zone effect with cell_effect parameter for per-cell transforms - Zone context (row, col, lum, sat, hue, etc.) available in cell_effect lambdas - Effects are now loaded explicitly from recipe declarations, not auto-loaded - Added effects_registry to plan for explicit effect dependency tracking - Updated effect definition syntax across all sexp effects - New run_staged.py for executing staged recipes - Example recipes demonstrating alternating rotation and blur/rgb_split patterns Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
159 lines
7.9 KiB
Common Lisp
159 lines
7.9 KiB
Common Lisp
;; cycle-effects-preset construct
|
|
;; Data-driven effect cycling using preset definitions
|
|
;;
|
|
;; Preset format (flat, no nested :params):
|
|
;; {:effect "brightness" :amount {:bind "bass" :range [-80 80]}}
|
|
;; {:effect "blur" :radius 5}
|
|
;;
|
|
;; Binding specs {:bind "analyzer" :range [min max]} are resolved to actual bindings
|
|
|
|
(define-construct cycle-effects-preset
|
|
:params (
|
|
(preset :type any :desc "List of effect preset definitions")
|
|
(videos :type any :desc "List of video source nodes")
|
|
(video_infos :type any :desc "List of video info analysis results")
|
|
(beats :type any :desc "Beat analysis data with :times")
|
|
(beats_per_segment :type int :default 4 :desc "Number of beats per segment")
|
|
)
|
|
(let [num-effects (len preset)
|
|
num-videos (len videos)
|
|
;; Extract durations from video-info analysis results
|
|
durations (map (fn [info] (get info :duration)) video_infos)
|
|
times (get beats :times)
|
|
grouped (chunk-every times beats_per_segment)
|
|
|
|
;; Resolve a param value - if it's a binding spec dict, create actual Binding
|
|
;; Note: pass the analyzer NAME (string) to bind, not the data - it will be
|
|
;; looked up at execution time in analysis_data
|
|
resolve-param (fn [param-value]
|
|
(if (dict? param-value)
|
|
(let [bind-name (get param-value :bind)]
|
|
(if bind-name
|
|
(let [range-spec (get param-value :range (list 0 1))]
|
|
(bind bind-name :range range-spec))
|
|
param-value))
|
|
param-value))
|
|
|
|
;; Process effect spec - resolve any binding specs in params
|
|
;; Effect spec is flat: {:effect "name" :param1 val1 :param2 {:bind ...}}
|
|
make-effect (fn [effect-spec]
|
|
(let [effect-name (get effect-spec :effect)]
|
|
;; Build effect dict with resolved params
|
|
;; Since we can't iterate dict keys, we check known params
|
|
;; Only include params that exist (non-nil) in the spec
|
|
(let [result {:effect effect-name}
|
|
;; Check each known param
|
|
amount (get effect-spec :amount nil)
|
|
degrees (get effect-spec :degrees nil)
|
|
speed (get effect-spec :speed nil)
|
|
level (get effect-spec :level nil)
|
|
levels (get effect-spec :levels nil)
|
|
radius (get effect-spec :radius nil)
|
|
intensity (get effect-spec :intensity nil)
|
|
contrast (get effect-spec :contrast nil)
|
|
brightness (get effect-spec :brightness nil)
|
|
strength (get effect-spec :strength nil)
|
|
amplitude (get effect-spec :amplitude nil)
|
|
wavelength (get effect-spec :wavelength nil)
|
|
frequency (get effect-spec :frequency nil)
|
|
segments-p (get effect-spec :segments nil)
|
|
rotation_speed (get effect-spec :rotation_speed nil)
|
|
factor (get effect-spec :factor nil)
|
|
angle (get effect-spec :angle nil)
|
|
direction (get effect-spec :direction nil)
|
|
block_size (get effect-spec :block_size nil)
|
|
char_size (get effect-spec :char_size nil)
|
|
color_mode (get effect-spec :color_mode nil)
|
|
low (get effect-spec :low nil)
|
|
high (get effect-spec :high nil)
|
|
thickness (get effect-spec :thickness nil)
|
|
glow_radius (get effect-spec :glow_radius nil)
|
|
glow_intensity (get effect-spec :glow_intensity nil)
|
|
line_spacing (get effect-spec :line_spacing nil)
|
|
vignette_amount (get effect-spec :vignette_amount nil)
|
|
spacing (get effect-spec :spacing nil)
|
|
offset_x (get effect-spec :offset_x nil)
|
|
num_echoes (get effect-spec :num_echoes nil)
|
|
decay (get effect-spec :decay nil)
|
|
persistence (get effect-spec :persistence nil)
|
|
rows (get effect-spec :rows nil)
|
|
cols (get effect-spec :cols nil)
|
|
threshold_low (get effect-spec :threshold_low nil)
|
|
threshold_high (get effect-spec :threshold_high nil)
|
|
corruption (get effect-spec :corruption nil)]
|
|
;; Only add non-nil params to result
|
|
;; Use cond to build up the dict (since we can't dynamically add keys)
|
|
;; This is ugly but necessary without dict iteration
|
|
{:effect effect-name
|
|
:amount (if (nil? amount) nil (resolve-param amount))
|
|
:degrees (if (nil? degrees) nil (resolve-param degrees))
|
|
:speed speed
|
|
:level level
|
|
:levels levels
|
|
:radius (if (nil? radius) nil (resolve-param radius))
|
|
:intensity (if (nil? intensity) nil (resolve-param intensity))
|
|
:contrast (if (nil? contrast) nil (resolve-param contrast))
|
|
:brightness (if (nil? brightness) nil (resolve-param brightness))
|
|
:strength (if (nil? strength) nil (resolve-param strength))
|
|
:amplitude (if (nil? amplitude) nil (resolve-param amplitude))
|
|
:wavelength wavelength
|
|
:frequency frequency
|
|
:segments segments-p
|
|
:rotation_speed rotation_speed
|
|
:factor (if (nil? factor) nil (resolve-param factor))
|
|
:angle (if (nil? angle) nil (resolve-param angle))
|
|
:direction direction
|
|
:block_size (if (nil? block_size) nil (resolve-param block_size))
|
|
:char_size char_size
|
|
:color_mode color_mode
|
|
:low low
|
|
:high high
|
|
:thickness thickness
|
|
:glow_radius glow_radius
|
|
:glow_intensity glow_intensity
|
|
:line_spacing line_spacing
|
|
:vignette_amount (if (nil? vignette_amount) nil (resolve-param vignette_amount))
|
|
:spacing spacing
|
|
:offset_x (if (nil? offset_x) nil (resolve-param offset_x))
|
|
:num_echoes num_echoes
|
|
:decay decay
|
|
:persistence persistence
|
|
:rows rows
|
|
:cols cols
|
|
:threshold_low threshold_low
|
|
:threshold_high threshold_high
|
|
:corruption (if (nil? corruption) nil (resolve-param corruption))})))
|
|
|
|
find-valid-video (fn [preferred-idx seg-duration]
|
|
(cond
|
|
(>= (nth durations preferred-idx) seg-duration) preferred-idx
|
|
(>= (nth durations (mod (+ preferred-idx 1) num-videos)) seg-duration) (mod (+ preferred-idx 1) num-videos)
|
|
(>= (nth durations (mod (+ preferred-idx 2) num-videos)) seg-duration) (mod (+ preferred-idx 2) num-videos)
|
|
:else nil))]
|
|
|
|
(nth
|
|
(reduce
|
|
(fn [state group]
|
|
(let [acc (first state)
|
|
segments (nth state 1)
|
|
audio-start (first group)
|
|
audio-end (last group)
|
|
seg-duration (- audio-end audio-start)
|
|
vid-idx (find-valid-video (mod acc num-videos) seg-duration)]
|
|
(if (nil? vid-idx)
|
|
(list (inc acc) segments)
|
|
(let [src (nth videos vid-idx)
|
|
src-duration (nth durations vid-idx)
|
|
wrapped-start (mod audio-start src-duration)
|
|
effect-idx (mod acc num-effects)
|
|
effect-spec (nth preset effect-idx)
|
|
fx (make-effect effect-spec)
|
|
segment (dict :source src
|
|
:start wrapped-start
|
|
:duration seg-duration
|
|
:effects (list fx))]
|
|
(list (inc acc) (append segments segment))))))
|
|
(list 0 (list))
|
|
grouped)
|
|
1)))
|