diff --git a/frontend/src/arthur/db.cljs b/frontend/src/arthur/db.cljs index c83fff4..8fe4358 100644 --- a/frontend/src/arthur/db.cljs +++ b/frontend/src/arthur/db.cljs @@ -162,6 +162,7 @@ :time-view :timeline :tone :skin-base :tool nil + :auto-key? false :draft [] :knobs {} :trace {:faces #{} :opacity trace/opacity-default} diff --git a/frontend/src/arthur/domain/gesture.cljs b/frontend/src/arthur/domain/gesture.cljs index ec27248..1a52d69 100644 --- a/frontend/src/arthur/domain/gesture.cljs +++ b/frontend/src/arthur/domain/gesture.cljs @@ -7,10 +7,10 @@ is in the placement's matrices, so a shape five symbols down moves under the pointer like one on top. - THE KEYING RULE IS `node/set-channel`, the inspector's: a channel with keys - gets one on the node's own frame, and one without has its one value changed. - After Effects' stopwatch โ€” there is no mode to be in, and nothing snaps back - on the next frame as an unkeyed change does in Blender." + The normal keying rule is `node/set-channel`, the inspector's: a channel with + keys gets one on the node's own frame, and one without has its one value + changed. Auto-key deliberately replaces that rule with `set-keyed-channel`, + so touching an otherwise static transform starts its animation at this frame." (:require [arthur.domain.channel :as ch] [arthur.domain.node :as node])) @@ -78,7 +78,10 @@ (defn apply-values "Clip with channel values `vs`, `{path value}`, written into node `id` of - symbol `sid` on the node's own frame `f`, by the keying rule above." - [clip sid id f vs] - (update-in clip [:symbols sid :nodes id] - #(reduce-kv (fn [n path v] (node/set-channel n path f v)) % vs))) + symbol `sid` on the node's own frame `f`, by the keying rule above. The sixth + argument arms auto-key; the five-argument form retains the normal rule." + ([clip sid id f vs] (apply-values clip sid id f vs false)) + ([clip sid id f vs auto-key?] + (let [put (if auto-key? node/set-keyed-channel node/set-channel)] + (update-in clip [:symbols sid :nodes id] + #(reduce-kv (fn [n path v] (put n path f v)) % vs))))) diff --git a/frontend/src/arthur/domain/node.cljs b/frontend/src/arthur/domain/node.cljs index 77694ad..1d5a474 100644 --- a/frontend/src/arthur/domain/node.cljs +++ b/frontend/src/arthur/domain/node.cljs @@ -104,6 +104,17 @@ (assoc-in n [:channels path] (if (:keys c) (assoc-in c [:keys f] v) (ch/framed v))))) +(defn set-keyed-channel + "Write `v` as a key at `f`, starting an animated channel when needed. This is + the auto-key counterpart to `set-channel`; an existing channel keeps its + interpolation and segment choices." + [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/keyed {f v} (if (boolean? v) :hold :linear)))))) + (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 diff --git a/frontend/src/arthur/events/project.cljs b/frontend/src/arthur/events/project.cljs index 3d33ada..ab6b630 100644 --- a/frontend/src/arthur/events/project.cljs +++ b/frontend/src/arthur/events/project.cljs @@ -635,7 +635,8 @@ ::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)))) + (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))))) (rf/reg-event-db ::toggle-key diff --git a/frontend/src/arthur/events/ui.cljs b/frontend/src/arthur/events/ui.cljs index 6e6efd5..50575c7 100644 --- a/frontend/src/arthur/events/ui.cljs +++ b/frontend/src/arthur/events/ui.cljs @@ -621,6 +621,11 @@ (fn [db [_ scope id knob value]] (assoc-in db [:ui :knobs [scope id knob]] value))) +(rf/reg-event-db + ::toggle-auto-key + (fn [db _] + (update-in db [:ui :auto-key?] not))) + ;; --------------------------------------------------------------------------- ;; a new symbol @@ -754,14 +759,19 @@ ;; A transform in the middle of a drag on the stage, `{:sid :id :frame ;; :values}`, drawn by `::render/clip` as `::sliding` is; nil when abandoned. (fn [db [_ g]] - (if g (assoc-in db [:ui :gesture] g) (update db :ui dissoc :gesture)))) + (if g + (assoc-in db [:ui :gesture] + (assoc g :auto-key? (boolean (get-in db [:ui :auto-key?])))) + (update db :ui dissoc :gesture)))) (rf/reg-event-db ::transform ;; The drag let go: one edit, so one undo step and one write to collaborators. (fn [db [_ {:keys [sid id frame values]}]] (cond-> (update db :ui dissoc :gesture) - (seq values) (edit/edit #(gesture/apply-values % sid id frame values))))) + (seq values) (edit/edit #(gesture/apply-values + % sid id frame values + (boolean (get-in db [:ui :auto-key?]))))))) (rf/reg-event-db ::delete-selected diff --git a/frontend/src/arthur/subs/render.cljs b/frontend/src/arthur/subs/render.cljs index 7f69a51..f76c45e 100644 --- a/frontend/src/arthur/subs/render.cljs +++ b/frontend/src/arthur/subs/render.cljs @@ -46,8 +46,8 @@ (let [c (:clip (footage/entry id))] (or (when-let [{:keys [path df]} sliding] (:clip (nest/slide c open path df))) - (when-let [{:keys [sid id frame values]} gesture] - (when c (gesture/apply-values c sid id frame values))) + (when-let [{:keys [sid id frame values auto-key?]} gesture] + (when c (gesture/apply-values c sid id frame values auto-key?))) c)))) (rf/reg-sub diff --git a/frontend/src/arthur/subs/ui.cljs b/frontend/src/arthur/subs/ui.cljs index 059e218..4086778 100644 --- a/frontend/src/arthur/subs/ui.cljs +++ b/frontend/src/arthur/subs/ui.cljs @@ -17,6 +17,7 @@ (rf/reg-sub ::lane-retry (fn [db _] (get-in db [:ui :lane-retry]))) (rf/reg-sub ::tone (fn [db _] (get-in db [:ui :tone]))) (rf/reg-sub ::tool (fn [db _] (get-in db [:ui :tool]))) +(rf/reg-sub ::auto-key? (fn [db _] (boolean (get-in db [:ui :auto-key?])))) (rf/reg-sub ::draft (fn [db _] (get-in db [:ui :draft]))) (rf/reg-sub ::convert (fn [db _] (get-in db [:ui :convert]))) (rf/reg-sub ::drop (fn [db _] (get-in db [:ui :drop]))) diff --git a/frontend/src/arthur/ui/palette.cljs b/frontend/src/arthur/ui/palette.cljs index 2e59458..79cd841 100644 --- a/frontend/src/arthur/ui/palette.cljs +++ b/frontend/src/arthur/ui/palette.cljs @@ -42,11 +42,19 @@ (defn bar [] (let [tone @(rf/subscribe [::sub/tone]) tool @(rf/subscribe [::sub/tool]) + auto? @(rf/subscribe [::sub/auto-key?]) draft @(rf/subscribe [::sub/draft])] [:div.palette-bar [:div.swatches (doall (map #(swatch % tone) (range slots)))] [:span.dim (name tone)] [:span {:style {:flex 1}}] + [:button.auto-key {:class (when auto? "on") + :aria-pressed auto? + :title (if auto? + "auto-key is on โ€” edited parameters get a key at this frame" + "auto-key edited parameters at this frame") + :on-click #(rf/dispatch [::ui/toggle-auto-key])} + "โ—† auto key"] (if (= :polygon tool) [:<> [:span.dim (str (quot (count draft) 2) " points")] diff --git a/frontend/src/arthur/ui/params.cljs b/frontend/src/arthur/ui/params.cljs index 9bf21b1..cc67730 100644 --- a/frontend/src/arthur/ui/params.cljs +++ b/frontend/src/arthur/ui/params.cljs @@ -167,7 +167,7 @@ ;; 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] +(defn- channel-control [sid id path ch frame auto-key?] (let [keyed? (some? (:keys ch)) ;; No store: the call site below hands this only channels that are not ;; `:dense`, which are the only ones with anything in tier 2 to read. @@ -184,7 +184,7 @@ :value (if deg? (/ (js/Math.round (* x (/ 18000 js/Math.PI))) 100) x) :parse js/parseFloat :on-number #(on-number (if deg? (* % (/ js/Math.PI 180)) %))}])] - [:dd.channel + [:dd.channel {:class (when auto-key? "live")} [: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") @@ -199,7 +199,8 @@ (when gap? [segment-select sid id path ch left])])) (defn- node-section [[sid id n]] - (let [[start end] (:span n)] + (let [[start end] (:span n) + auto-key? @(rf/subscribe [::sub/auto-key?])] [section (str (name (:kind n)) " ยท in " (name sid)) [facts "name" (or (:name n) (brief id)) @@ -221,7 +222,7 @@ [:<> [:dt (str/join " " (map name path))] (if (and (contains? (node/defaults-of n) path) (not (:dense ch))) - [channel-control sid id path ch frame] + [channel-control sid id path ch frame auto-key?] [:dd (channel-state ch)])]))])])) ;; --------------------------------------------------------------------------- diff --git a/frontend/test/arthur/domain/node_test.cljs b/frontend/test/arthur/domain/node_test.cljs index c7cc882..5c19862 100644 --- a/frontend/test/arthur/domain/node_test.cljs +++ b/frontend/test/arthur/domain/node_test.cljs @@ -216,3 +216,13 @@ (let [d (node/toggle-key (node/set-segment-interp c [:xform :pos] 0 :hold) [:xform :pos] 0 nil)] (is (not (contains? (get-in d [:channels [:xform :pos] :segments]) 0)) "taking a key off takes its gap's choice with it")))) + +(deftest auto-keying-a-channel-from-a-parameter-edit + (let [n {:id :x :kind :group} + keyed (node/set-keyed-channel n [:xform :rot] 7 1.25) + moved (node/set-keyed-channel keyed [:xform :rot] 11 2.5) + visible (node/set-keyed-channel n [:vis] 7 false)] + (is (= {7 1.25, 11 2.5} (get-in moved [:channels [:xform :rot] :keys]))) + (is (= :linear (get-in moved [:channels [:xform :rot] :interp]))) + (is (= :hold (get-in visible [:channels [:vis] :interp])) + "boolean parameters do not tween"))) diff --git a/static/arthur/app.css b/static/arthur/app.css index 7c02fa8..3adc3c1 100644 --- a/static/arthur/app.css +++ b/static/arthur/app.css @@ -56,6 +56,10 @@ --aim: #8d4bd6; --aim-stage: #c79bf2; + /* Auto-key is a recording state, distinct from selection and aiming. */ + --live: #239447; + --live-bg: #e2f5e7; + /* A keyframe is a dot and a dot is ink. Flash draws them black and so does this; the frames a node exists over are a pale tint behind them. */ --key: #1f1f1f; @@ -748,6 +752,13 @@ button.share-button:hover, button.share-button.on { filter: brightness(1.1); } border-bottom: 1px solid var(--line); } +.palette-bar .auto-key.on { + color: #145c2b; + background: var(--live-bg); + border-color: var(--live); + box-shadow: 0 0 7px rgba(35, 148, 71, .72), inset 0 0 0 1px rgba(255, 255, 255, .7); +} + .swatches { display: flex; gap: 3px; } /* Circles. A palette entry is one indivisible tone, not an area of coverage, and @@ -1283,6 +1294,14 @@ button.share-button:hover, button.share-button.on { filter: brightness(1.1); } .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); } +.facts dd.channel.live { + margin: -2px; + padding: 2px; + border-radius: 3px; + background: var(--live-bg); + box-shadow: 0 0 0 1px rgba(35, 148, 71, .45), 0 0 6px rgba(35, 148, 71, .28); +} +.facts dd.channel.live input { border-color: var(--live); } /* The selected node's transform box, in stage pixels (the SVG's viewBox). */ .paint-overlay .handles .box { fill: none; stroke: #e6ca8b; stroke-width: 0.5; stroke-dasharray: 2 1; pointer-events: none; }