Add composable ASCII art with per-cell effects and explicit effect loading
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>
This commit is contained in:
69
effects/ascii_alternating_fx.sexp
Normal file
69
effects/ascii_alternating_fx.sexp
Normal file
@@ -0,0 +1,69 @@
|
||||
;; ASCII with Alternating Effects - Checkerboard of blur and RGB split
|
||||
;;
|
||||
;; Demonstrates using existing sexp effects within cell_effect lambdas.
|
||||
;; Even cells get blur, odd cells get RGB split - creating a checkerboard pattern.
|
||||
|
||||
(recipe "ascii_alternating_fx"
|
||||
:version "1.0"
|
||||
:description "ASCII art with alternating blur and RGB split effects per cell"
|
||||
:encoding (:codec "libx264" :crf 20 :preset "medium" :audio-codec "aac" :fps 30)
|
||||
|
||||
:params (
|
||||
(cols :type int :default 40 :range [20 100]
|
||||
:desc "Number of character columns")
|
||||
(blur_amount :type float :default 3 :range [1 10]
|
||||
:desc "Blur radius for blur cells")
|
||||
(rgb_offset :type int :default 3 :range [1 10]
|
||||
:desc "RGB split offset for split cells")
|
||||
)
|
||||
|
||||
;; Registry
|
||||
(effect ascii_fx_zone :path "../sexp_effects/effects/ascii_fx_zone.sexp")
|
||||
(analyzer energy :path "../../artdag-analyzers/energy/analyzer.py")
|
||||
|
||||
;; Source files
|
||||
(def video (source :path "../monday.webm"))
|
||||
(def audio (source :path "../dizzy.mp3"))
|
||||
|
||||
;; Stage 1: Analysis
|
||||
(stage :analyze
|
||||
:outputs [energy-data]
|
||||
(def audio-clip (-> audio (segment :start 60 :duration 10)))
|
||||
(def energy-data (-> audio-clip (analyze energy))))
|
||||
|
||||
;; Stage 2: Process - apply effect with alternating cell effects
|
||||
(stage :process
|
||||
:requires [:analyze]
|
||||
:inputs [energy-data]
|
||||
:outputs [result audio-clip]
|
||||
(def clip (-> video (segment :start 0 :duration 10)))
|
||||
(def audio-clip (-> audio (segment :start 60 :duration 10)))
|
||||
|
||||
;; Apply effect with cell_effect lambda
|
||||
;; Checkerboard: (row + col) even = blur, odd = rgb_split
|
||||
(def result (-> clip
|
||||
(effect ascii_fx_zone
|
||||
:cols cols
|
||||
:char_size (bind energy-data values :range [12 24])
|
||||
:color_mode "color"
|
||||
:background "black"
|
||||
;; Pass params to zone dict
|
||||
:energy (bind energy-data values :range [0 1])
|
||||
:blur_amount blur_amount
|
||||
:rgb_offset rgb_offset
|
||||
;; Cell effect: alternate between blur and rgb_split
|
||||
;; Uses existing sexp effects - each cell is just a small frame
|
||||
:cell_effect (lambda [cell zone]
|
||||
(if (= (mod (+ (get zone "row") (get zone "col")) 2) 0)
|
||||
;; Even cells: blur scaled by energy
|
||||
(blur cell (* (get zone "blur_amount") (get zone "energy")))
|
||||
;; Odd cells: rgb split scaled by energy
|
||||
(rgb_split cell
|
||||
(* (get zone "rgb_offset") (get zone "energy"))
|
||||
0)))))))
|
||||
|
||||
;; Stage 3: Output
|
||||
(stage :output
|
||||
:requires [:process]
|
||||
:inputs [result audio-clip]
|
||||
(mux result audio-clip)))
|
||||
64
effects/ascii_alternating_rotate.sexp
Normal file
64
effects/ascii_alternating_rotate.sexp
Normal file
@@ -0,0 +1,64 @@
|
||||
;; ASCII with Alternating Rotation Directions
|
||||
;;
|
||||
;; Checkerboard pattern: even cells rotate clockwise, odd cells rotate counter-clockwise
|
||||
;; Rotation amount scaled by energy and position (more at top-right)
|
||||
|
||||
(recipe "ascii_alternating_rotate"
|
||||
:version "1.0"
|
||||
:description "ASCII art with alternating rotation directions per cell"
|
||||
:encoding (:codec "libx264" :crf 20 :preset "medium" :audio-codec "aac" :fps 30)
|
||||
|
||||
:params (
|
||||
(cols :type int :default 50 :range [20 100]
|
||||
:desc "Number of character columns")
|
||||
(rotation_scale :type float :default 60 :range [0 180]
|
||||
:desc "Max rotation in degrees")
|
||||
)
|
||||
|
||||
;; Registry
|
||||
(effect ascii_fx_zone :path "../sexp_effects/effects/ascii_fx_zone.sexp")
|
||||
;; Effects used in cell_effect lambda
|
||||
(effect rotate :path "../sexp_effects/effects/rotate.sexp")
|
||||
(analyzer energy :path "../../artdag-analyzers/energy/analyzer.py")
|
||||
|
||||
;; Source files
|
||||
(def video (source :path "../monday.webm"))
|
||||
(def audio (source :path "../dizzy.mp3"))
|
||||
|
||||
;; Stage 1: Analysis
|
||||
(stage :analyze
|
||||
:outputs [energy-data]
|
||||
(def audio-clip (-> audio (segment :start 60 :duration 10)))
|
||||
(def energy-data (-> audio-clip (analyze energy))))
|
||||
|
||||
;; Stage 2: Process
|
||||
(stage :process
|
||||
:requires [:analyze]
|
||||
:inputs [energy-data]
|
||||
:outputs [result audio-clip]
|
||||
(def clip (-> video (segment :start 0 :duration 10)))
|
||||
(def audio-clip (-> audio (segment :start 60 :duration 10)))
|
||||
|
||||
(def result (-> clip
|
||||
(effect ascii_fx_zone
|
||||
:cols cols
|
||||
:char_size (bind energy-data values :range [10 20])
|
||||
:color_mode "color"
|
||||
:background "black"
|
||||
:energy (bind energy-data values :range [0 1])
|
||||
:rotation_scale rotation_scale
|
||||
;; Alternating rotation: even cells clockwise, odd cells counter-clockwise
|
||||
;; Scaled by energy * position (more at top-right)
|
||||
:cell_effect (lambda [cell zone]
|
||||
(rotate cell
|
||||
(* (if (= (mod (+ (get zone "row") (get zone "col")) 2) 0) 1 -1)
|
||||
(* (get zone "energy")
|
||||
(get zone "rotation_scale")
|
||||
(* 1.5 (+ (get zone "col-norm")
|
||||
(- 1 (get zone "row-norm"))))))))))))
|
||||
|
||||
;; Stage 3: Output
|
||||
(stage :output
|
||||
:requires [:process]
|
||||
:inputs [result audio-clip]
|
||||
(mux result audio-clip)))
|
||||
89
effects/ascii_art_fx_staged.sexp
Normal file
89
effects/ascii_art_fx_staged.sexp
Normal file
@@ -0,0 +1,89 @@
|
||||
;; ASCII art FX effect with staged execution and per-character effects
|
||||
;;
|
||||
;; Run with --list-params to see all available parameters:
|
||||
;; python3 run_staged.py effects/ascii_art_fx_staged.sexp --list-params
|
||||
|
||||
(recipe "ascii_art_fx_staged"
|
||||
:version "1.0"
|
||||
:description "ASCII art FX with per-character effects"
|
||||
:encoding (:codec "libx264" :crf 20 :preset "medium" :audio-codec "aac" :fps 30)
|
||||
|
||||
:params (
|
||||
;; Colors
|
||||
(color_mode :type string :default "color"
|
||||
:desc "Character color: color, mono, invert, or any color name/hex")
|
||||
(background_color :type string :default "black"
|
||||
:desc "Background color name or hex value")
|
||||
(invert_colors :type int :default 0 :range [0 1]
|
||||
:desc "Swap foreground and background colors")
|
||||
|
||||
;; Character sizing
|
||||
(char_size :type int :default 12 :range [4 32]
|
||||
:desc "Base character cell size in pixels")
|
||||
|
||||
;; Per-character effects
|
||||
(char_jitter :type float :default 0 :range [0 20]
|
||||
:desc "Position jitter amount in pixels")
|
||||
(char_scale :type float :default 1.0 :range [0.5 2.0]
|
||||
:desc "Character scale factor")
|
||||
(char_rotation :type float :default 0 :range [0 180]
|
||||
:desc "Rotation amount in degrees")
|
||||
(char_hue_shift :type float :default 0 :range [0 360]
|
||||
:desc "Hue shift in degrees")
|
||||
|
||||
;; Modulation sources
|
||||
(jitter_source :type string :default "none"
|
||||
:choices [none luminance inv_luminance saturation position_x position_y random center_dist]
|
||||
:desc "What drives jitter modulation")
|
||||
(scale_source :type string :default "none"
|
||||
:choices [none luminance inv_luminance saturation position_x position_y random center_dist]
|
||||
:desc "What drives scale modulation")
|
||||
(rotation_source :type string :default "none"
|
||||
:choices [none luminance inv_luminance saturation position_x position_y random center_dist]
|
||||
:desc "What drives rotation modulation")
|
||||
(hue_source :type string :default "none"
|
||||
:choices [none luminance inv_luminance saturation position_x position_y random center_dist]
|
||||
:desc "What drives hue shift modulation")
|
||||
)
|
||||
|
||||
;; Registry
|
||||
(effect ascii_art_fx :path "../sexp_effects/effects/ascii_art_fx.sexp")
|
||||
(analyzer energy :path "../../artdag-analyzers/energy/analyzer.py")
|
||||
|
||||
;; Source files (not parameterized for now)
|
||||
(def video (source :path "../monday.webm"))
|
||||
(def audio (source :path "../dizzy.mp3"))
|
||||
|
||||
;; Stage 1: Analysis
|
||||
(stage :analyze
|
||||
:outputs [energy-data]
|
||||
(def audio-clip (-> audio (segment :start 60 :duration 10)))
|
||||
(def energy-data (-> audio-clip (analyze energy))))
|
||||
|
||||
;; Stage 2: Process - apply effect
|
||||
(stage :process
|
||||
:requires [:analyze]
|
||||
:inputs [energy-data]
|
||||
:outputs [result audio-clip]
|
||||
(def clip (-> video (segment :start 0 :duration 10)))
|
||||
(def audio-clip (-> audio (segment :start 60 :duration 10)))
|
||||
(def result (-> clip
|
||||
(effect ascii_art_fx
|
||||
:char_size (bind energy-data values :range [8 24])
|
||||
:color_mode color_mode
|
||||
:background_color background_color
|
||||
:invert_colors invert_colors
|
||||
:char_jitter char_jitter
|
||||
:char_scale char_scale
|
||||
:char_rotation char_rotation
|
||||
:char_hue_shift char_hue_shift
|
||||
:jitter_source jitter_source
|
||||
:scale_source scale_source
|
||||
:rotation_source rotation_source
|
||||
:hue_source hue_source))))
|
||||
|
||||
;; Stage 3: Output
|
||||
(stage :output
|
||||
:requires [:process]
|
||||
:inputs [result audio-clip]
|
||||
(mux result audio-clip)))
|
||||
59
effects/ascii_art_staged.sexp
Normal file
59
effects/ascii_art_staged.sexp
Normal file
@@ -0,0 +1,59 @@
|
||||
;; ASCII art effect with staged execution
|
||||
;;
|
||||
;; Stages:
|
||||
;; :analyze - Run energy analysis on audio (cacheable)
|
||||
;; :process - Segment media and apply effect
|
||||
;; :output - Mux video with audio
|
||||
;;
|
||||
;; Usage: python3 run_staged.py effects/ascii_art_staged.sexp
|
||||
;;
|
||||
;; Parameters:
|
||||
;; color_mode: coloring mode ("color", "green", "white", default: "color")
|
||||
;; char_size is bound to energy (wobbles with overall loudness)
|
||||
|
||||
(recipe "ascii_art_staged"
|
||||
:version "1.0"
|
||||
:description "ASCII art effect with staged execution"
|
||||
:encoding (:codec "libx264" :crf 20 :preset "medium" :audio-codec "aac" :fps 30)
|
||||
|
||||
;; Registry: effects and analyzers
|
||||
(effect ascii_art :path "../sexp_effects/effects/ascii_art.sexp")
|
||||
(analyzer energy :path "../../artdag-analyzers/energy/analyzer.py")
|
||||
|
||||
;; Pre-stage definitions (available to all stages)
|
||||
(def color_mode "color")
|
||||
(def background_color "black")
|
||||
(def invert_colors 0) ;; 0=false, 1=true
|
||||
(def video (source :path "../monday.webm"))
|
||||
(def audio (source :path "../dizzy.mp3"))
|
||||
|
||||
;; Stage 1: Analysis - extract energy from audio
|
||||
;; This stage is expensive but cacheable - rerun with same input skips this
|
||||
(stage :analyze
|
||||
:outputs [energy-data]
|
||||
;; Audio from 60s where it's louder
|
||||
(def audio-clip (-> audio (segment :start 60 :duration 10)))
|
||||
(def energy-data (-> audio-clip (analyze energy))))
|
||||
|
||||
;; Stage 2: Process - apply ASCII art effect with energy binding
|
||||
(stage :process
|
||||
:requires [:analyze]
|
||||
:inputs [energy-data]
|
||||
:outputs [result audio-clip]
|
||||
;; Video segment
|
||||
(def clip (-> video (segment :start 0 :duration 10)))
|
||||
;; Audio clip for muxing (same segment as analysis)
|
||||
(def audio-clip (-> audio (segment :start 60 :duration 10)))
|
||||
;; Apply effect with char_size bound to energy
|
||||
(def result (-> clip
|
||||
(effect ascii_art
|
||||
:char_size (bind energy-data values :range [2 32])
|
||||
:color_mode color_mode
|
||||
:background_color background_color
|
||||
:invert_colors invert_colors))))
|
||||
|
||||
;; Stage 3: Output - combine video and audio
|
||||
(stage :output
|
||||
:requires [:process]
|
||||
:inputs [result audio-clip]
|
||||
(mux result audio-clip)))
|
||||
64
effects/ascii_cell_effect_staged.sexp
Normal file
64
effects/ascii_cell_effect_staged.sexp
Normal file
@@ -0,0 +1,64 @@
|
||||
;; ASCII Cell Effect - Demonstrates arbitrary per-cell effects via lambda
|
||||
;;
|
||||
;; Each character cell is a mini-frame that can have any effects applied.
|
||||
;; The lambda receives the cell image and zone context (including bound analysis data).
|
||||
|
||||
(recipe "ascii_cell_effect_staged"
|
||||
:version "1.0"
|
||||
:description "ASCII art with lambda-driven per-cell effects"
|
||||
:encoding (:codec "libx264" :crf 20 :preset "medium" :audio-codec "aac" :fps 30)
|
||||
|
||||
:params (
|
||||
(cols :type int :default 60 :range [20 200]
|
||||
:desc "Number of character columns")
|
||||
(rotation_scale :type float :default 45 :range [0 90]
|
||||
:desc "Max rotation in degrees at top-right corner")
|
||||
)
|
||||
|
||||
;; Registry
|
||||
(effect ascii_fx_zone :path "../sexp_effects/effects/ascii_fx_zone.sexp")
|
||||
(analyzer energy :path "../../artdag-analyzers/energy/analyzer.py")
|
||||
|
||||
;; Source files
|
||||
(def video (source :path "../monday.webm"))
|
||||
(def audio (source :path "../dizzy.mp3"))
|
||||
|
||||
;; Stage 1: Analysis
|
||||
(stage :analyze
|
||||
:outputs [energy-data]
|
||||
(def audio-clip (-> audio (segment :start 60 :duration 10)))
|
||||
(def energy-data (-> audio-clip (analyze energy))))
|
||||
|
||||
;; Stage 2: Process - apply effect with cell_effect lambda
|
||||
(stage :process
|
||||
:requires [:analyze]
|
||||
:inputs [energy-data]
|
||||
:outputs [result audio-clip]
|
||||
(def clip (-> video (segment :start 0 :duration 10)))
|
||||
(def audio-clip (-> audio (segment :start 60 :duration 10)))
|
||||
|
||||
;; Apply effect with cell_effect lambda
|
||||
;; The lambda receives (cell zone) where:
|
||||
;; cell = the rendered character as a small image
|
||||
;; zone = dict with row, col, lum, sat, hue, energy, rotation_scale, etc.
|
||||
(def result (-> clip
|
||||
(effect ascii_fx_zone
|
||||
:cols cols
|
||||
:char_size (bind energy-data values :range [10 20])
|
||||
:color_mode "color"
|
||||
:background "black"
|
||||
;; Pass bound values so they're available in zone dict
|
||||
:energy (bind energy-data values :range [0 1])
|
||||
:rotation_scale rotation_scale
|
||||
;; Cell effect lambda: rotate each cell based on energy * position
|
||||
:cell_effect (lambda [cell zone]
|
||||
(rotate-cell cell
|
||||
(* (* (get zone "energy") (get zone "rotation_scale"))
|
||||
(* 1.5 (+ (get zone "col-norm")
|
||||
(- 1 (get zone "row-norm")))))))))))
|
||||
|
||||
;; Stage 3: Output
|
||||
(stage :output
|
||||
:requires [:process]
|
||||
:inputs [result audio-clip]
|
||||
(mux result audio-clip)))
|
||||
66
effects/ascii_fx_zone_staged.sexp
Normal file
66
effects/ascii_fx_zone_staged.sexp
Normal file
@@ -0,0 +1,66 @@
|
||||
;; ASCII FX Zone effect with per-zone expression-driven effects
|
||||
;;
|
||||
;; Uses energy analysis to drive rotation based on position:
|
||||
;; - Bottom-left = 0 rotation
|
||||
;; - Top-right = max rotation (scaled by energy)
|
||||
|
||||
(recipe "ascii_fx_zone_staged"
|
||||
:version "1.0"
|
||||
:description "ASCII art with per-zone expression-driven effects"
|
||||
:encoding (:codec "libx264" :crf 20 :preset "medium" :audio-codec "aac" :fps 30)
|
||||
|
||||
:params (
|
||||
(cols :type int :default 80 :range [20 200]
|
||||
:desc "Number of character columns")
|
||||
(color_mode :type string :default "color"
|
||||
:desc "Character color: color, mono, invert, or any color name/hex")
|
||||
(background :type string :default "black"
|
||||
:desc "Background color name or hex value")
|
||||
(rotation_scale :type float :default 30 :range [0 90]
|
||||
:desc "Max rotation in degrees at top-right corner")
|
||||
)
|
||||
|
||||
;; Registry
|
||||
(effect ascii_fx_zone :path "../sexp_effects/effects/ascii_fx_zone.sexp")
|
||||
(analyzer energy :path "../../artdag-analyzers/energy/analyzer.py")
|
||||
|
||||
;; Source files
|
||||
(def video (source :path "../monday.webm"))
|
||||
(def audio (source :path "../dizzy.mp3"))
|
||||
|
||||
;; Stage 1: Analysis
|
||||
(stage :analyze
|
||||
:outputs [energy-data]
|
||||
(def audio-clip (-> audio (segment :start 60 :duration 10)))
|
||||
(def energy-data (-> audio-clip (analyze energy))))
|
||||
|
||||
;; Stage 2: Process - apply effect with zone expressions
|
||||
(stage :process
|
||||
:requires [:analyze]
|
||||
:inputs [energy-data]
|
||||
:outputs [result audio-clip]
|
||||
(def clip (-> video (segment :start 0 :duration 10)))
|
||||
(def audio-clip (-> audio (segment :start 60 :duration 10)))
|
||||
|
||||
;; Apply effect with lambdas
|
||||
;; Lambda receives zone dict: {row, col, row-norm, col-norm, lum, sat, hue, r, g, b, char}
|
||||
;; Plus any extra params like energy, rotation_scale
|
||||
(def result (-> clip
|
||||
(effect ascii_fx_zone
|
||||
:char_size (bind energy-data values :range [8 24])
|
||||
:color_mode color_mode
|
||||
:background background
|
||||
;; Pass energy as extra param so lambda can access it via zone dict
|
||||
:energy (bind energy-data values :range [0 1])
|
||||
:rotation_scale rotation_scale
|
||||
;; Rotation: energy * scale * position (bottom-left=0, top-right=3)
|
||||
:char_rotation (lambda [z]
|
||||
(* (* (get z "energy") (get z "rotation_scale"))
|
||||
(* 1.5 (+ (get z "col-norm")
|
||||
(- 1 (get z "row-norm"))))))))))
|
||||
|
||||
;; Stage 3: Output
|
||||
(stage :output
|
||||
:requires [:process]
|
||||
:inputs [result audio-clip]
|
||||
(mux result audio-clip)))
|
||||
Reference in New Issue
Block a user