Key a node's transform and visibility from the inspector

Each of vis, anchor, pos, rot, scale and skew is a row: ◆ keys the channel on
the node's own frame, or takes the key there off, and a typed value is written
on blur or Enter — a key on a keyed channel, the one value on one that is not.
Rotation reads in degrees. Dense and other channels keep their readout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Olive Vaughn 2026-09-30 01:11:16 -04:00
parent 38850a29ba
commit 7246534505
5 changed files with 109 additions and 7 deletions

View file

@ -70,6 +70,30 @@
[n]
(merge defaults (:channels n)))
(defn set-channel
"Write `v` into channel `path`: a key on the node's own frame `f` when the
channel is keyed, its one value when it is not."
[n path f v]
(let [c (get (channels n) path)]
(assoc-in n [:channels path]
(if (:keys c) (assoc-in c [:keys f] v) (ch/framed v)))))
(defn toggle-key
"Key channel `path` on the node's own frame `f` with the value it has there, or
take the key there off. The first key starts the channel animating and taking
the last one off leaves it that one value. A boolean holds; anything else tweens."
[n path f]
(let [c (get (channels n) path)
v (ch/value-at c f)
ks (dissoc (:keys c) f)]
(assoc-in n [:channels path]
(cond
(not (:keys c)) (ch/keyed {f v} (if (boolean? v) :hold :linear))
(not (contains? (:keys c) f)) (assoc-in c [:keys f] v)
(seq ks) (cond-> (assoc c :keys ks)
(:segments c) (update :segments dissoc f))
:else (ch/framed v)))))
;; ---------------------------------------------------------------------------
;; time maps
;;

View file

@ -25,6 +25,7 @@
[arthur.domain.bring :as bring]
[arthur.domain.clip :as clip]
[arthur.domain.leaf :as leaf]
[arthur.domain.node :as node]
[arthur.events.edit :as edit]
[arthur.audio.mix :as mix]
[arthur.demo.stage :as stage]
@ -579,6 +580,17 @@
:label (or (:label clip) (name id))
:clip clip}}))))
(rf/reg-event-db
::set-channel
;; `frame` is the node's own, as for a drawing key.
(fn [db [_ sid id path frame value]]
(edit/edit db #(update-in % [:symbols sid :nodes id] node/set-channel path frame value))))
(rf/reg-event-db
::toggle-key
(fn [db [_ sid id path frame]]
(edit/edit db #(update-in % [:symbols sid :nodes id] node/toggle-key path frame))))
(rf/reg-event-fx
::list
(fn [{:keys [db]} _]

View file

@ -122,6 +122,44 @@
[:option {:value "hold"} "hold"]
[:option {:value "linear"} "tween"]]]])]))
;; The transform and visibility, which every node has, as one row each: ◆ keys
;; the channel here or takes the key here off, and an edit writes a key on a
;; keyed channel or the one value on one that is not. `frame` is the node's own,
;; nil when it is not on screen, where a keyed channel has no here to write to.
;; Rotation is shown in degrees.
(defn- channel-control [sid id path ch frame]
(let [keyed? (some? (:keys ch))
v (channel/value-at ch (or frame 0))
off? (and keyed? (nil? frame))
deg? (= path [:xform :rot])
put #(rf/dispatch [::project/set-channel sid id path frame %])
;; Written on blur or Enter rather than per keystroke, so a typed
;; number is one edit. Keyed by what it shows, so a seek or an undo
;; that changes the value puts the new one in the box.
field (fn [i x on-number]
(let [shown (if deg? (/ (js/Math.round (* x (/ 18000 js/Math.PI))) 100) x)]
^{:key (str i " " shown)}
[:input {:type "number" :step "any" :disabled off?
:default-value shown
:on-key-down #(when (= "Enter" (.-key %)) (.. % -target blur))
:on-blur (fn [e]
(let [n (js/parseFloat (.. e -target -value))]
(when-not (or (js/isNaN n) (= n shown))
(on-number (if deg? (* n (/ js/Math.PI 180)) n)))))}]))]
[:dd.channel
[:button.key {:class (cond (contains? (:keys ch) frame) "on" keyed? "keyed")
:disabled (nil? frame)
:title (if keyed? (str (count (:keys ch)) " keys") "key this here")
:on-click #(rf/dispatch [::project/toggle-key sid id path frame])}
"◆"]
(cond
(boolean? v) [:input {:type "checkbox" :checked v :disabled off?
:on-change #(put (.. % -target -checked))}]
(number? v) (field 0 v put)
:else (doall (map-indexed (fn [i x] (field i x #(put (assoc (vec v) i %))))
v)))]))
(defn- node-section [[sid id n]]
(let [[start end] (:span n)]
[section (str (name (:kind n)) " · in " (name sid))
@ -137,13 +175,16 @@
"at" (when (= :map (get-in n [:time :mode])) (str (get-in n [:time :at] 0)))]
(when (:paint? n) [drawing-keys sid id n @(rf/subscribe [::sub/selected-local])])
[:div.row {:style {:margin-top "6px"}} [:span.dim "channels"]]
[:dl.facts
(doall
(for [[path ch] (sort-by (comp str key) (node/channels n))]
^{:key (str path)}
[:<>
[:dt (str/join " " (map name path))]
[:dd (channel-state ch)]]))]]))
(let [{:keys [frame]} @(rf/subscribe [::sub/selected-local])]
[:dl.facts
(doall
(for [[path ch] (sort-by (comp str key) (node/channels n))]
^{:key (str path)}
[:<>
[:dt (str/join " " (map name path))]
(if (and (contains? node/defaults path) (not (:dense ch)))
[channel-control sid id path ch frame]
[:dd (channel-state ch)])]))])]))
;; ---------------------------------------------------------------------------
;; a symbol

View file

@ -183,3 +183,21 @@
(is (seq (node/problems {:id :x :kind :poly :z "a1"
:channels {[:geom :pts] {:animated? true}}}))
"and a channel that is malformed in itself"))
(deftest keying-a-channel-from-the-inspector
(let [n {:id :x :kind :group}
a (node/set-channel n [:xform :rot] 3 1.0)
b (node/toggle-key a [:xform :rot] 3)
c (-> b (node/set-channel [:xform :pos] 9 [5 5])
(node/toggle-key [:xform :pos] 0)
(node/set-channel [:xform :pos] 10 [10 0]))
rot #(ch/value-at (get (node/channels %1) [:xform :rot]) %2)
pos #(ch/value-at (get (node/channels %1) [:xform :pos]) %2)]
(is (= 1.0 (rot a 50)) "an unkeyed channel is its one value")
(is (= {3 1.0} (get-in b [:channels [:xform :rot] :keys])) "the first key is its value here")
(is (= [7.5 2.5] (pos c 5)) "an edit on a keyed channel keys it, and keys tween")
(is (= 1.0 (rot (node/set-channel b [:xform :rot] 8 2.0) 3)) "without moving the key before it")
(let [d (node/toggle-key b [:xform :rot] 3)]
(is (not (:animated? (get-in d [:channels [:xform :rot]]))) "the last key off is one value again")
(is (= 1.0 (rot d 0))))
(is (= :hold (get-in (node/toggle-key n [:vis] 0) [:channels [:vis] :interp])) "a boolean holds")))