Add auto-key mode to main toolbar
This commit is contained in:
parent
3879d76d57
commit
34f62ab007
11 changed files with 82 additions and 17 deletions
|
|
@ -162,6 +162,7 @@
|
||||||
:time-view :timeline
|
:time-view :timeline
|
||||||
:tone :skin-base
|
:tone :skin-base
|
||||||
:tool nil
|
:tool nil
|
||||||
|
:auto-key? false
|
||||||
:draft []
|
:draft []
|
||||||
:knobs {}
|
:knobs {}
|
||||||
:trace {:faces #{} :opacity trace/opacity-default}
|
:trace {:faces #{} :opacity trace/opacity-default}
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,10 @@
|
||||||
is in the placement's matrices, so a shape five symbols down moves under the
|
is in the placement's matrices, so a shape five symbols down moves under the
|
||||||
pointer like one on top.
|
pointer like one on top.
|
||||||
|
|
||||||
THE KEYING RULE IS `node/set-channel`, the inspector's: a channel with keys
|
The normal keying rule is `node/set-channel`, the inspector's: a channel with
|
||||||
gets one on the node's own frame, and one without has its one value changed.
|
keys gets one on the node's own frame, and one without has its one value
|
||||||
After Effects' stopwatch — there is no mode to be in, and nothing snaps back
|
changed. Auto-key deliberately replaces that rule with `set-keyed-channel`,
|
||||||
on the next frame as an unkeyed change does in Blender."
|
so touching an otherwise static transform starts its animation at this frame."
|
||||||
(:require [arthur.domain.channel :as ch]
|
(:require [arthur.domain.channel :as ch]
|
||||||
[arthur.domain.node :as node]))
|
[arthur.domain.node :as node]))
|
||||||
|
|
||||||
|
|
@ -78,7 +78,10 @@
|
||||||
|
|
||||||
(defn apply-values
|
(defn apply-values
|
||||||
"Clip with channel values `vs`, `{path value}`, written into node `id` of
|
"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."
|
symbol `sid` on the node's own frame `f`, by the keying rule above. The sixth
|
||||||
[clip sid id f vs]
|
argument arms auto-key; the five-argument form retains the normal rule."
|
||||||
(update-in clip [:symbols sid :nodes id]
|
([clip sid id f vs] (apply-values clip sid id f vs false))
|
||||||
#(reduce-kv (fn [n path v] (node/set-channel n path f v)) % vs)))
|
([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)))))
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,17 @@
|
||||||
(assoc-in n [:channels path]
|
(assoc-in n [:channels path]
|
||||||
(if (:keys c) (assoc-in c [:keys f] v) (ch/framed v)))))
|
(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
|
(defn toggle-key
|
||||||
"Key channel `path` on the node's own frame `f` with the value it has there, or
|
"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
|
take the key there off. The first key starts the channel animating and taking
|
||||||
|
|
|
||||||
|
|
@ -635,7 +635,8 @@
|
||||||
::set-channel
|
::set-channel
|
||||||
;; `frame` is the node's own, as for a drawing key.
|
;; `frame` is the node's own, as for a drawing key.
|
||||||
(fn [db [_ sid id path frame value]]
|
(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
|
(rf/reg-event-db
|
||||||
::toggle-key
|
::toggle-key
|
||||||
|
|
|
||||||
|
|
@ -621,6 +621,11 @@
|
||||||
(fn [db [_ scope id knob value]]
|
(fn [db [_ scope id knob value]]
|
||||||
(assoc-in db [:ui :knobs [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
|
;; a new symbol
|
||||||
|
|
||||||
|
|
@ -754,14 +759,19 @@
|
||||||
;; A transform in the middle of a drag on the stage, `{:sid :id :frame
|
;; 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.
|
;; :values}`, drawn by `::render/clip` as `::sliding` is; nil when abandoned.
|
||||||
(fn [db [_ g]]
|
(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
|
(rf/reg-event-db
|
||||||
::transform
|
::transform
|
||||||
;; The drag let go: one edit, so one undo step and one write to collaborators.
|
;; The drag let go: one edit, so one undo step and one write to collaborators.
|
||||||
(fn [db [_ {:keys [sid id frame values]}]]
|
(fn [db [_ {:keys [sid id frame values]}]]
|
||||||
(cond-> (update db :ui dissoc :gesture)
|
(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
|
(rf/reg-event-db
|
||||||
::delete-selected
|
::delete-selected
|
||||||
|
|
|
||||||
|
|
@ -46,8 +46,8 @@
|
||||||
(let [c (:clip (footage/entry id))]
|
(let [c (:clip (footage/entry id))]
|
||||||
(or (when-let [{:keys [path df]} sliding]
|
(or (when-let [{:keys [path df]} sliding]
|
||||||
(:clip (nest/slide c open path df)))
|
(:clip (nest/slide c open path df)))
|
||||||
(when-let [{:keys [sid id frame values]} gesture]
|
(when-let [{:keys [sid id frame values auto-key?]} gesture]
|
||||||
(when c (gesture/apply-values c sid id frame values)))
|
(when c (gesture/apply-values c sid id frame values auto-key?)))
|
||||||
c))))
|
c))))
|
||||||
|
|
||||||
(rf/reg-sub
|
(rf/reg-sub
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
(rf/reg-sub ::lane-retry (fn [db _] (get-in db [:ui :lane-retry])))
|
(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 ::tone (fn [db _] (get-in db [:ui :tone])))
|
||||||
(rf/reg-sub ::tool (fn [db _] (get-in db [:ui :tool])))
|
(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 ::draft (fn [db _] (get-in db [:ui :draft])))
|
||||||
(rf/reg-sub ::convert (fn [db _] (get-in db [:ui :convert])))
|
(rf/reg-sub ::convert (fn [db _] (get-in db [:ui :convert])))
|
||||||
(rf/reg-sub ::drop (fn [db _] (get-in db [:ui :drop])))
|
(rf/reg-sub ::drop (fn [db _] (get-in db [:ui :drop])))
|
||||||
|
|
|
||||||
|
|
@ -42,11 +42,19 @@
|
||||||
(defn bar []
|
(defn bar []
|
||||||
(let [tone @(rf/subscribe [::sub/tone])
|
(let [tone @(rf/subscribe [::sub/tone])
|
||||||
tool @(rf/subscribe [::sub/tool])
|
tool @(rf/subscribe [::sub/tool])
|
||||||
|
auto? @(rf/subscribe [::sub/auto-key?])
|
||||||
draft @(rf/subscribe [::sub/draft])]
|
draft @(rf/subscribe [::sub/draft])]
|
||||||
[:div.palette-bar
|
[:div.palette-bar
|
||||||
[:div.swatches (doall (map #(swatch % tone) (range slots)))]
|
[:div.swatches (doall (map #(swatch % tone) (range slots)))]
|
||||||
[:span.dim (name tone)]
|
[:span.dim (name tone)]
|
||||||
[:span {:style {:flex 1}}]
|
[: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)
|
(if (= :polygon tool)
|
||||||
[:<>
|
[:<>
|
||||||
[:span.dim (str (quot (count draft) 2) " points")]
|
[:span.dim (str (quot (count draft) 2) " points")]
|
||||||
|
|
|
||||||
|
|
@ -167,7 +167,7 @@
|
||||||
;; Rotation is shown in degrees. Between two keys, the gap after the one here
|
;; Rotation is shown in degrees. Between two keys, the gap after the one here
|
||||||
;; holds or tweens, as a drawing's does.
|
;; 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))
|
(let [keyed? (some? (:keys ch))
|
||||||
;; No store: the call site below hands this only channels that are not
|
;; 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.
|
;; `: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)
|
:value (if deg? (/ (js/Math.round (* x (/ 18000 js/Math.PI))) 100) x)
|
||||||
:parse js/parseFloat
|
:parse js/parseFloat
|
||||||
:on-number #(on-number (if deg? (* % (/ js/Math.PI 180)) %))}])]
|
: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")
|
[:button.key {:class (cond (contains? (:keys ch) frame) "on" keyed? "keyed")
|
||||||
:disabled (nil? frame)
|
:disabled (nil? frame)
|
||||||
:title (if keyed? (str (count (:keys ch)) " keys") "key this here")
|
:title (if keyed? (str (count (:keys ch)) " keys") "key this here")
|
||||||
|
|
@ -199,7 +199,8 @@
|
||||||
(when gap? [segment-select sid id path ch left])]))
|
(when gap? [segment-select sid id path ch left])]))
|
||||||
|
|
||||||
(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?])]
|
||||||
[section (str (name (:kind n)) " · in " (name sid))
|
[section (str (name (:kind n)) " · in " (name sid))
|
||||||
[facts
|
[facts
|
||||||
"name" (or (:name n) (brief id))
|
"name" (or (:name n) (brief id))
|
||||||
|
|
@ -221,7 +222,7 @@
|
||||||
[:<>
|
[:<>
|
||||||
[:dt (str/join " " (map name path))]
|
[:dt (str/join " " (map name path))]
|
||||||
(if (and (contains? (node/defaults-of n) path) (not (:dense ch)))
|
(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)])]))])]))
|
[:dd (channel-state ch)])]))])]))
|
||||||
|
|
||||||
;; ---------------------------------------------------------------------------
|
;; ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -216,3 +216,13 @@
|
||||||
(let [d (node/toggle-key (node/set-segment-interp c [:xform :pos] 0 :hold) [:xform :pos] 0 nil)]
|
(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))
|
(is (not (contains? (get-in d [:channels [:xform :pos] :segments]) 0))
|
||||||
"taking a key off takes its gap's choice with it"))))
|
"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")))
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,10 @@
|
||||||
--aim: #8d4bd6;
|
--aim: #8d4bd6;
|
||||||
--aim-stage: #c79bf2;
|
--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
|
/* 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. */
|
this; the frames a node exists over are a pale tint behind them. */
|
||||||
--key: #1f1f1f;
|
--key: #1f1f1f;
|
||||||
|
|
@ -748,6 +752,13 @@ button.share-button:hover, button.share-button.on { filter: brightness(1.1); }
|
||||||
border-bottom: 1px solid var(--line);
|
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; }
|
.swatches { display: flex; gap: 3px; }
|
||||||
|
|
||||||
/* Circles. A palette entry is one indivisible tone, not an area of coverage, and
|
/* 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 { padding: 0 3px; color: var(--dim); }
|
||||||
.facts dd.channel .key.keyed { color: var(--fg); }
|
.facts dd.channel .key.keyed { color: var(--fg); }
|
||||||
.facts dd.channel .key.on { color: var(--sel); }
|
.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). */
|
/* 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; }
|
.paint-overlay .handles .box { fill: none; stroke: #e6ca8b; stroke-width: 0.5; stroke-dasharray: 2 1; pointer-events: none; }
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue