From 2a0426707a58c16cc60d0f57904e2df288f3f68e Mon Sep 17 00:00:00 2001 From: Olive Vaughn Date: Wed, 30 Sep 2026 01:51:30 -0400 Subject: [PATCH] Hold or tween any keyed channel's gaps, as a drawing's Co-Authored-By: Claude Opus 5.5 --- frontend/src/arthur/domain/node.cljs | 10 +++++++++ frontend/src/arthur/domain/paint.cljs | 10 --------- frontend/src/arthur/events/paint.cljs | 5 ----- frontend/src/arthur/events/project.cljs | 5 +++++ frontend/src/arthur/ui/params.cljs | 25 +++++++++++++++------ frontend/test/arthur/domain/node_test.cljs | 11 ++++++++- frontend/test/arthur/domain/paint_test.cljs | 4 +++- 7 files changed, 46 insertions(+), 24 deletions(-) diff --git a/frontend/src/arthur/domain/node.cljs b/frontend/src/arthur/domain/node.cljs index ad36b3c..5d31ade 100644 --- a/frontend/src/arthur/domain/node.cljs +++ b/frontend/src/arthur/domain/node.cljs @@ -94,6 +94,16 @@ (:segments c) (update :segments dissoc f)) :else (ch/framed v))))) +(defn set-segment-interp + "Choose how channel `path`'s key at `left` leads to the next one: `:hold` cuts + there, `:linear` tweens. The same for a drawing's points as for a transform — + only a gap that exists, between a key and a later one, can be chosen." + [n path left interp] + (let [ks (:keys (get (channels n) path))] + (if (and (contains? ks left) (some #(< left %) (keys ks)) (#{:hold :linear} interp)) + (assoc-in n [:channels path :segments left] interp) + n))) + ;; --------------------------------------------------------------------------- ;; time maps ;; diff --git a/frontend/src/arthur/domain/paint.cljs b/frontend/src/arthur/domain/paint.cljs index 53f1fe4..20fe58a 100644 --- a/frontend/src/arthur/domain/paint.cljs +++ b/frontend/src/arthur/domain/paint.cljs @@ -46,13 +46,3 @@ (if (and points (< (inc i) (count points))) (assoc-in clip path (-> points (assoc i x) (assoc (inc i) y))) clip))) - -(defn set-segment-interp [clip sid id key-frame interp] - (let [node (get-in clip [:symbols sid :nodes id]) - keys (get-in node [:channels geometry :keys])] - (if (and (:paint? node) (contains? keys key-frame) - (some #(< key-frame %) (clojure.core/keys keys)) - (#{:hold :linear} interp)) - (assoc-in clip [:symbols sid :nodes id :channels geometry - :segments key-frame] interp) - clip))) diff --git a/frontend/src/arthur/events/paint.cljs b/frontend/src/arthur/events/paint.cljs index 7bf614b..eb3dc1a 100644 --- a/frontend/src/arthur/events/paint.cljs +++ b/frontend/src/arthur/events/paint.cljs @@ -23,8 +23,3 @@ ::set-vertex (fn [db [_ sid id key-frame vertex point]] (edit/edit db #(paint/set-vertex % sid id key-frame vertex point)))) - -(rf/reg-event-db - ::set-segment-interp - (fn [db [_ sid id key-frame interp]] - (edit/edit db #(paint/set-segment-interp % sid id key-frame interp)))) diff --git a/frontend/src/arthur/events/project.cljs b/frontend/src/arthur/events/project.cljs index f0c9283..6dffcc2 100644 --- a/frontend/src/arthur/events/project.cljs +++ b/frontend/src/arthur/events/project.cljs @@ -624,6 +624,11 @@ (fn [db [_ sid id path frame]] (edit/edit db #(update-in % [:symbols sid :nodes id] node/toggle-key path frame)))) +(rf/reg-event-db + ::set-segment-interp + (fn [db [_ sid id path left interp]] + (edit/edit db #(update-in % [:symbols sid :nodes id] node/set-segment-interp path left interp)))) + (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 85d74f7..6bee115 100644 --- a/frontend/src/arthur/ui/params.cljs +++ b/frontend/src/arthur/ui/params.cljs @@ -123,6 +123,16 @@ :else (let [v (:value ch)] (str "framed · " (if (channel/nothing? v) "absent" (pr-str v)))))) +(defn- segment-select + "Hold or tween, for the gap after the key at `left` — the same choice on a + drawing as on a transform." + [sid id path ch left] + [:select.interp {:value (name (or (channel/segment-interp ch left) :hold)) + :on-change #(rf/dispatch [::project/set-segment-interp + sid id path left (keyword (.. % -target -value))])} + [:option {:value "hold"} "hold"] + [:option {:value "linear"} "tween"]]) + (defn- drawing-keys "The polygon controls: jump to a drawing key, add one here, and choose what the gap after the current one does. Lifted out of the old stage toolbar unchanged — @@ -158,23 +168,23 @@ (when next-k [:div.row {:style {:margin-top "5px"}} [:label.dim (str "key " active " → " next-k " ") - [:select {:value (name (or (channel/segment-interp geom active) :hold)) - :on-change #(rf/dispatch [::paint-events/set-segment-interp - sid id active (keyword (.. % -target -value))])} - [:option {:value "hold"} "hold"] - [:option {:value "linear"} "tween"]]]])])) + [segment-select sid id paint/geometry geom active]]])])) ;; 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. +;; Rotation is shown in degrees. Between two keys, the gap after the one here +;; holds or tweens, as a drawing's does. (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]) + ;; A boolean has nothing between true and false to tween through. + left (when (and keyed? frame (not (boolean? v))) (paint/active-frame ch frame)) + gap? (and left (some #(< left %) (keys (:keys ch)))) put #(rf/dispatch [::project/set-channel sid id path frame %]) field (fn [i x on-number] ^{:key i} @@ -193,7 +203,8 @@ :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)))])) + v))) + (when gap? [segment-select sid id path ch left])])) (defn- node-section [[sid id n]] (let [[start end] (:span n)] diff --git a/frontend/test/arthur/domain/node_test.cljs b/frontend/test/arthur/domain/node_test.cljs index d795949..ca810c8 100644 --- a/frontend/test/arthur/domain/node_test.cljs +++ b/frontend/test/arthur/domain/node_test.cljs @@ -200,4 +200,13 @@ (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"))) + (is (= :hold (get-in (node/toggle-key n [:vis] 0) [:channels [:vis] :interp])) "a boolean holds") + (let [h (node/set-segment-interp c [:xform :pos] 0 :hold)] + (is (= [5 5] (pos h 5)) "a gap set to hold cuts at the next key") + (is (= [10 0] (pos h 10))) + (is (= [7.5 2.5] (pos (node/set-segment-interp h [:xform :pos] 0 :linear) 5)) "and back to a tween") + (is (= h (node/set-segment-interp h [:xform :pos] 10 :hold)) "the last key has no gap after it") + (is (empty? (ch/problems (get-in h [:channels [:xform :pos]]))))) + (let [d (node/toggle-key (node/set-segment-interp c [:xform :pos] 0 :hold) [:xform :pos] 0)] + (is (not (contains? (get-in d [:channels [:xform :pos] :segments]) 0)) + "taking a key off takes its gap's choice with it")))) diff --git a/frontend/test/arthur/domain/paint_test.cljs b/frontend/test/arthur/domain/paint_test.cljs index 169f6a7..1e019ea 100644 --- a/frontend/test/arthur/domain/paint_test.cljs +++ b/frontend/test/arthur/domain/paint_test.cljs @@ -3,6 +3,7 @@ [arthur.demo :as demo] [arthur.domain.channel :as channel] [arthur.domain.leaf :as leaf] + [arthur.domain.node :as node] [arthur.domain.paint :as paint] [arthur.domain.symbol :as symbol])) @@ -17,7 +18,8 @@ c3 (paint/add-key c2 :main :paint-test 15) c4 (paint/set-vertex c3 :main :paint-test 15 0 [34 10]) held (geometry c4) - mixed-clip (paint/set-segment-interp c4 :main :paint-test 9 :linear) + mixed-clip (update-in c4 [:symbols :main :nodes :paint-test] + node/set-segment-interp paint/geometry 9 :linear) mixed (geometry mixed-clip)] (is (= [3 229] (get-in c2 [:symbols :main :nodes :paint-test :span]))) (is (= a (channel/value-at held 8)))