;; cycle-crossfade template ;; ;; Generalized cycling zoom-crossfade for any number of video layers. ;; Cycles through videos with smooth zoom-based crossfade transitions. ;; ;; Parameters: ;; beat-data - beat analysis node (drives timing) ;; input-videos - list of video nodes to cycle through ;; init-clen - initial cycle length in beats ;; ;; NOTE: The parameter is named "input-videos" (not "videos") because ;; template substitution replaces param names everywhere in the AST. ;; The planner's _expand_slice_on injects env["videos"] at plan time, ;; so (len videos) inside the lambda references that injected value. (deftemplate cycle-crossfade (beat-data input-videos init-clen) (slice-on beat-data :videos input-videos :init {:cycle 0 :beat 0 :clen init-clen} :fn (lambda [acc i start end] (let [beat (get acc "beat") clen (get acc "clen") active (get acc "cycle") n (len videos) phase3 (* beat 3) wt (lambda [p] (let [prev (mod (+ p (- n 1)) n)] (if (= active p) (if (< phase3 clen) 1.0 (if (< phase3 (* clen 2)) (- 1.0 (* (/ (- phase3 clen) clen) 1.0)) 0.0)) (if (= active prev) (if (< phase3 clen) 0.0 (if (< phase3 (* clen 2)) (* (/ (- phase3 clen) clen) 1.0) 1.0)) 0.0)))) zm (lambda [p] (let [prev (mod (+ p (- n 1)) n)] (if (= active p) ;; Active video: normal -> zoom out during transition -> tiny (if (< phase3 clen) 1.0 (if (< phase3 (* clen 2)) (+ 1.0 (* (/ (- phase3 clen) clen) 1.0)) 0.1)) (if (= active prev) ;; Incoming video: tiny -> zoom in during transition -> normal (if (< phase3 clen) 0.1 (if (< phase3 (* clen 2)) (+ 0.1 (* (/ (- phase3 clen) clen) 0.9)) 1.0)) 0.1)))) new-acc (if (< (+ beat 1) clen) (dict :cycle active :beat (+ beat 1) :clen clen) (dict :cycle (mod (+ active 1) n) :beat 0 :clen (+ 40 (mod (* i 7) 41))))] {:layers (map (lambda [p] {:video p :effects [{:effect zoom :amount (zm p)}]}) (range 0 n)) :compose {:effect blend_multi :mode "alpha" :weights (map (lambda [p] (wt p)) (range 0 n))} :acc new-acc}))))