diff --git a/frontend/src/arthur/domain/node.cljs b/frontend/src/arthur/domain/node.cljs index 2472da3..ad36b3c 100644 --- a/frontend/src/arthur/domain/node.cljs +++ b/frontend/src/arthur/domain/node.cljs @@ -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 ;; diff --git a/frontend/src/arthur/events/project.cljs b/frontend/src/arthur/events/project.cljs index f150477..8b239c9 100644 --- a/frontend/src/arthur/events/project.cljs +++ b/frontend/src/arthur/events/project.cljs @@ -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]} _] diff --git a/frontend/src/arthur/ui/params.cljs b/frontend/src/arthur/ui/params.cljs index f419b41..2a2cc95 100644 --- a/frontend/src/arthur/ui/params.cljs +++ b/frontend/src/arthur/ui/params.cljs @@ -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 diff --git a/frontend/test/arthur/domain/node_test.cljs b/frontend/test/arthur/domain/node_test.cljs index 15aa746..d795949 100644 --- a/frontend/test/arthur/domain/node_test.cljs +++ b/frontend/test/arthur/domain/node_test.cljs @@ -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"))) diff --git a/static/arthur/app.css b/static/arthur/app.css index 7bbd72a..0081793 100644 --- a/static/arthur/app.css +++ b/static/arthur/app.css @@ -782,3 +782,10 @@ button.share-button:hover, button.share-button.on { filter: brightness(1.1); } padding: 8px 10px; border-top: 1px solid var(--hair); } + +/* A transform or visibility channel: its key button, then a field per component. */ +.facts dd.channel { display: flex; gap: 3px; align-items: center; overflow: visible; } +.facts dd.channel input[type=number] { width: 0; flex: 1; min-width: 0; } +.facts dd.channel .key { padding: 0 3px; color: var(--dim); } +.facts dd.channel .key.keyed { color: var(--fg); } +.facts dd.channel .key.on { color: var(--sel); }