Add instance playback controls to the inspector

This commit is contained in:
Olive Vaughn 2026-10-04 02:12:06 -04:00
parent 52f25e0bb1
commit 1c520e6a68
3 changed files with 94 additions and 0 deletions

View file

@ -757,6 +757,32 @@
(let [put (if (get-in db [:ui :auto-key?]) node/set-keyed-channel node/set-channel)] (let [put (if (get-in db [:ui :auto-key?]) node/set-keyed-channel node/set-channel)]
(edit/edit db #(update-in % [:symbols sid :nodes id] put path frame value))))) (edit/edit db #(update-in % [:symbols sid :nodes id] put path frame value)))))
(rf/reg-event-db
::instance-playback
(fn [db [_ sid id field value]]
(let [valid? (case field
:mode (#{:once :loop :frame} value)
(:in :speed) (and (node/finite-number? value) (<= 0 value))
false)]
(if-not valid?
db
(edit/edit
db
(fn [document]
(let [path [:symbols sid :nodes id]
n (get-in document path)]
(if-not (= :instance (:kind n))
document
(let [{:keys [speed]} (node/playback-of n)
updated (if (= :mode field)
(-> n
(update :time dissoc :loop?)
(assoc-in [:playback :end] (if (= :loop value) :loop :stop))
(assoc-in [:playback :speed]
(if (= :frame value) 0 (if (pos? speed) speed 1))))
(assoc-in n [:playback field] value))]
(assoc-in document path updated))))))))))
(defn- seed-palette-choice [document sid id] (defn- seed-palette-choice [document sid id]
(if (get-in document [:symbols sid :nodes id :channels [:palette]]) (if (get-in document [:symbols sid :nodes id :channels [:palette]])
document document

View file

@ -236,6 +236,34 @@
:on-click #(rf/dispatch [::project/set-channel sid id [:style :color] frame i])}]))] :on-click #(rf/dispatch [::project/set-channel sid id [:style :color] frame i])}]))]
(when keyed? [:span.dim "hold"])])) (when keyed? [:span.dim "hold"])]))
(defn- instance-playback [[sid id n]]
(let [{:keys [in speed end]} (node/playback-of n)
mode (cond (zero? speed) :frame
(or (= :loop end) (get-in n [:time :loop?])) :loop
:else :once)
change! (fn [field value]
(rf/dispatch [::project/instance-playback sid id field value]))]
[section "playback"
[:div.inspector-form
[:label.inspector-field "mode"
[:select {:value (name mode)
:aria-label "instance playback mode"
:on-change #(change! :mode (keyword (.. % -target -value)))}
[:option {:value "once"} "play once"]
[:option {:value "loop"} "loop"]
[:option {:value "frame"} "hold frame"]]]
[:label.inspector-field "start frame"
[number-input {:min 0 :step 1 :value in
:aria-label "instance start frame"
:parse #(js/parseInt % 10)
:on-number #(change! :in %)}]]
[:label.inspector-field "speed"
[number-input {:min 0 :step 0.1 :value speed
:disabled (= :frame mode)
:aria-label "instance playback speed"
:parse js/parseFloat
:on-number #(change! :speed %)}]]]]))
(defn- node-section [[sid id n]] (defn- node-section [[sid id n]]
(let [[start end] (:span n) (let [[start end] (:span n)
auto-key? @(rf/subscribe [::sub/auto-key?]) auto-key? @(rf/subscribe [::sub/auto-key?])
@ -787,6 +815,7 @@
[palette-placement-section node] [palette-placement-section node]
[clip-section]) [clip-section])
(when (and node (not palette-placement?)) [node-section node]) (when (and node (not palette-placement?)) [node-section node])
(when (and placed (not palette-placement?)) [instance-playback node])
(when (and node (not palette-placement?)) [palette/remap-section]) (when (and node (not palette-placement?)) [palette/remap-section])
(when (and placed (not palette-placement?)) [symbol-section placed "source symbol"]) (when (and placed (not palette-placement?)) [symbol-section placed "source symbol"])
(when (and node (not palette-placement?)) ^{:key (str (first node) "/" (second node))} (when (and node (not palette-placement?)) ^{:key (str (first node) "/" (second node))}

View file

@ -0,0 +1,39 @@
(ns arthur.events.instance-playback-test
(:require [cljs.test :refer [deftest is]]
[arthur.domain.clip :as clip]
[arthur.events.project :as project]
[arthur.footage.store :as store]
[re-frame.core :as rf]
[re-frame.db :as rf-db]))
(deftest playback-controls-change-only-the-selected-instance
(let [n {:id :wheel :kind :instance :source {:symbol :spin}
:span [0 240] :playback {:in 0 :speed 2 :end :stop
:tracks {:mouth []}}}
document (-> (clip/blank)
(assoc-in [:symbols :spin] {:frames 24 :fps 30 :nodes {}})
(assoc-in [:symbols :main :nodes :wheel] n)
(assoc-in [:symbols :main :nodes :other] (assoc n :id :other)))
id (store/install! {:clip document :store {}} "instance-playback-controls")
current #(get-in (:clip (store/entry (:clip/current @rf-db/app-db)))
[:symbols :main :nodes :wheel])]
(reset! rf-db/app-db {:clip/current id :paint/revision 0})
(rf/dispatch-sync [::project/instance-playback :main :wheel :mode :loop])
(is (= 0 (:frame (clip/placed-frame document :main (current) 24))))
(is (= :loop (get-in (current) [:playback :end])))
(is (= 2 (get-in (current) [:playback :speed])))
(rf/dispatch-sync [::project/instance-playback :main :wheel :in 5])
(rf/dispatch-sync [::project/instance-playback :main :wheel :mode :frame])
(is (= 5 (:frame (clip/placed-frame document :main (current) 100))))
(rf/dispatch-sync [::project/instance-playback :main :wheel :mode :once])
(is (nil? (clip/placed-frame document :main (current) 100)))
(is (= [0 240] (:span (current))))
(is (= {:mouth []} (get-in (current) [:playback :tracks])))
(let [before (current)]
(doseq [[field value] [[:in -1] [:speed js/Infinity] [:mode :invalid]]]
(rf/dispatch-sync [::project/instance-playback :main :wheel field value]))
(is (= before (current))))
(is (= n (assoc (get-in (:clip (store/entry (:clip/current @rf-db/app-db)))
[:symbols :main :nodes :other]) :id :wheel)))
(is (= 24 (get-in (:clip (store/entry (:clip/current @rf-db/app-db)))
[:symbols :spin :frames])))))