Model head anchors and independent pose timing
This commit is contained in:
parent
a45e89f4e4
commit
49ece8dee6
17 changed files with 521 additions and 215 deletions
|
|
@ -60,7 +60,8 @@
|
|||
;; leaf carries structure and no geometry.
|
||||
(let [ls (leaf/leaves :c1 @take/clip)
|
||||
n (get ls "clip/c1/timeline/main/node/mouth")]
|
||||
(is (= {:id :mouth :name "mouth" :kind :poly :parent :head :z "a1"} n))
|
||||
(is (= {:id :mouth :name "mouth" :kind :poly :parent :head :z "a1"
|
||||
:pose-group :mouth} n))
|
||||
(is (nil? (:channels n)))
|
||||
(is (:animated? (get ls "clip/c1/timeline/main/channel/mouth/geom.pts")))))
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,10 @@
|
|||
[arthur.domain.channel :as ch]
|
||||
[arthur.domain.clip :as clip]
|
||||
[arthur.domain.leaf :as leaf]
|
||||
[arthur.domain.node :as node]))
|
||||
[arthur.domain.node :as node]
|
||||
[arthur.domain.pose :as pose]
|
||||
[arthur.domain.palette :as pal]
|
||||
[arthur.domain.timeline :as timeline]))
|
||||
|
||||
(def source
|
||||
{:name "source" :fps 30 :width 320 :height 200
|
||||
|
|
@ -40,6 +43,97 @@
|
|||
(is (= [[[:right :mark] 150]] (at 5)))
|
||||
(is (= document (leaf/clip "stage" (leaf/leaves "stage" document))))))
|
||||
|
||||
(deftest a-placement-holds-and-cuts-each-generated-shape-independently
|
||||
(let [values (js/Int16Array. (clj->js (range 2 32)))
|
||||
visible (ch/keyed {0 true 20 true 21 false})
|
||||
dense {:animated? true :interp :hold
|
||||
:dense {:store "sizes" :offset 0 :stride 1 :frames 30}
|
||||
:pose-sampled? true}
|
||||
shape (fn [id z group]
|
||||
{:id id :kind :rect :parent :root :z z :pose-group group
|
||||
:channels {[:xform :pos] (ch/keyed {0 [0 0] 8 [8 0]})
|
||||
[:geom :size] dense
|
||||
[:vis] (assoc visible :pose-sampled? true)
|
||||
[:style :color] (ch/framed :brow)}})
|
||||
symbol {:id :sym/poses :frames 30
|
||||
:nodes {:root {:id :root :kind :group :z "a1"}
|
||||
:mouth (shape :mouth "a1" :mouth)
|
||||
:mouth-detail (shape :mouth-detail "a2" :mouth)
|
||||
:eye (shape :eye "a3" :eye)
|
||||
:brow (shape :brow "a4" :brow)}}
|
||||
document {:fps 30 :width 320 :height 200
|
||||
:timelines
|
||||
{:main {:id :main :frames 30
|
||||
:nodes {:root {:id :root :kind :group :z "a1"}
|
||||
:first {:id :first :kind :symbol :of :sym/poses
|
||||
:parent :root :z "a1"
|
||||
:playback {:tracks {:mouth {0 0, 8 20, 9 21}
|
||||
[:node :mouth-detail] {0 0, 8 4}
|
||||
:eye {0 0, 4 4}}}}
|
||||
:second {:id :second :kind :symbol :of :sym/poses
|
||||
:parent :root :z "a2"
|
||||
:playback {:tracks {:mouth {0 0, 8 8}}}}}}
|
||||
:sym/poses symbol}}
|
||||
resolve (clip/resolver document {"sizes" {:data values}})
|
||||
low-resolve (clip/resolver document {"sizes" {:data values}}
|
||||
pal/index-of :main {:picture-fps 8})
|
||||
at (fn [f] (into {} (map (fn [op] [(:node op) op])) (resolve f)))
|
||||
low-at (fn [f] (into {} (map (fn [op] [(:node op) op])) (low-resolve f)))]
|
||||
(is (empty? (clip/problems document)))
|
||||
(is (= document (leaf/clip "stage" (leaf/leaves "stage" document))))
|
||||
(is (= 2 (:size (get (at 7) [:first :mouth]))) "eight static frames")
|
||||
(is (= 22 (:size (get (at 8) [:first :mouth]))) "cut to source pose 20")
|
||||
(is (= 6 (:size (get (at 8) [:first :mouth-detail])))
|
||||
"one node may depart from its shared mouth group")
|
||||
(is (= 10 (:size (get (at 8) [:second :mouth]))) "other instance chooses pose 8")
|
||||
(is (= 6 (:size (get (at 7) [:first :eye]))) "eye has its own timing")
|
||||
(is (= 8 (:cx (get (at 8) [:first :eye]))) "authored position still reads stage time")
|
||||
(is (nil? (get (at 9) [:first :mouth]))
|
||||
"generated visibility is read from the same selected pose")
|
||||
(is (some? (get (at 9) [:second :mouth])))
|
||||
(is (= 5 (:size (get (low-at 7) [:first :brow])))
|
||||
"picture rate samples only generated motion")
|
||||
(is (= 22 (:size (get (low-at 8) [:first :mouth])))
|
||||
"an explicit cut occurs at its exact local frame, even off the picture grid")
|
||||
(is (= 8 (:cx (get (low-at 8) [:first :eye])))
|
||||
"authored position ignores the picture grid")
|
||||
(let [tl (get-in document [:timelines :sym/poses])
|
||||
opts {:source-fps 30 :picture-fps 8}]
|
||||
(is (= (mapv #(select-keys % [:node :cx :size])
|
||||
(timeline/eval-frame tl 8 {"sizes" {:data values}}
|
||||
pal/index-of {:mouth {0 0, 8 20}} opts))
|
||||
(mapv #(select-keys % [:node :cx :size])
|
||||
((timeline/resolver tl {"sizes" {:data values}}
|
||||
pal/index-of {:mouth {0 0, 8 20}} opts) 8)))
|
||||
"pure evaluation and playback apply the same pose choice"))))
|
||||
|
||||
(deftest stage-pose-edits-preserve-earlier-motion-and-survive-save
|
||||
(let [document (-> source
|
||||
(assoc-in [:timelines :main :nodes :placed]
|
||||
{:id :placed :kind :symbol :of :sym/test :parent :root
|
||||
:z "a2"})
|
||||
(assoc-in [:timelines :sym/test]
|
||||
{:id :sym/test :frames 4
|
||||
:nodes {:root {:id :root :kind :group :z "a1"}
|
||||
:mark {:id :mark :kind :rect :parent :root
|
||||
:z "a1" :pose-group :mark
|
||||
:channels {[:geom :size]
|
||||
{:animated? true :interp :hold
|
||||
:keys {0 2 1 3 2 4 3 5}
|
||||
:pose-sampled? true}}}}})
|
||||
(pose/put-cut :placed :mark 2 3))
|
||||
cuts (get-in document [:timelines :main :nodes :placed :playback :tracks :mark])]
|
||||
(is (= {2 3} cuts))
|
||||
(is (= 1 (pose/source-frame (pose/prepare {:mark cuts}) :mark 1 1))
|
||||
"before the first cut, dense motion continues")
|
||||
(is (= 3 (pose/source-frame (pose/prepare {:mark cuts}) :mark 2 2)))
|
||||
(is (= document (leaf/clip "stage" (leaf/leaves "stage" document))))
|
||||
(is (nil? (get-in (pose/remove-cut document :placed :mark 2)
|
||||
[:timelines :main :nodes :placed :playback :tracks :mark])))
|
||||
(is (seq (clip/problems (assoc-in document
|
||||
[:timelines :main :nodes :placed :playback :tracks :mark]
|
||||
{4 3}))))))
|
||||
|
||||
(defn- uuid-of
|
||||
"The uuid the layout authors for the placement whose handle is `id`.
|
||||
|
||||
|
|
|
|||
|
|
@ -25,12 +25,8 @@
|
|||
:channels {[:geom :pts] (ch/framed pts) [:style :color] (ch/framed color)}})
|
||||
|
||||
(defn- a-timeline
|
||||
"One square under a `:root` group.
|
||||
|
||||
The group is not decoration: a reduced picture rate is applied by writing a time
|
||||
map onto the node called `:root` — `export/sampled` and `subs/render` both do it,
|
||||
which is what keeps the export's poses identical to the preview's — so a timeline
|
||||
without one is not a shape this tool produces."
|
||||
"One authored square under a `:root` group. Picture sampling now applies to
|
||||
marked generated channels in the shared resolver, leaving this square alone."
|
||||
[frames]
|
||||
{:frames frames
|
||||
:nodes {:root {:id :root :kind :group :z "a1"}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
(defn- params-at [overrides]
|
||||
(merge take/knobs
|
||||
{:name "addr" :fps fps :aspect 1 :stage [320 200]
|
||||
:expose 1 :head :as-filmed}
|
||||
:expose 1 :head :free}
|
||||
overrides))
|
||||
|
||||
(defn- freeze-at
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
[arthur.domain.channel :as ch]
|
||||
[arthur.domain.clip :as clip]
|
||||
[arthur.domain.geom :as geom]
|
||||
[arthur.domain.leaf :as leaf]
|
||||
[arthur.domain.node :as node]
|
||||
[arthur.domain.palette :as pal]
|
||||
[arthur.domain.raster :as raster]
|
||||
|
|
@ -77,10 +78,11 @@
|
|||
;; before the data is trusted — which is what it is for. It checks the clip's
|
||||
;; fields, every timeline in it and the tracking identities, so it is the whole
|
||||
;; of what a save would refuse.
|
||||
(doseq [mode [:as-filmed :locked]]
|
||||
(let [c (freeze/head-mode {:mode mode} @frozen)]
|
||||
(is (empty? (clip/problems c)) (str mode ": " (pr-str (clip/problems c))))))
|
||||
(let [c (freeze/head-mode {:mode :per-plate :kept #{0 12 40 88 150}} @frozen)]
|
||||
(doseq [spec [{:mode :free} {:mode :anchored :anchors {0 0}}]]
|
||||
(let [c (freeze/head-mode spec @frozen)]
|
||||
(is (empty? (clip/problems c)) (str spec ": " (pr-str (clip/problems c))))))
|
||||
(let [c (freeze/head-mode {:mode :anchored
|
||||
:anchors {0 12, 40 88, 150 150}} @frozen)]
|
||||
(is (empty? (clip/problems c)) (pr-str (clip/problems c)))))
|
||||
|
||||
(deftest the-tree-is-the-one-the-model-specifies
|
||||
|
|
@ -196,7 +198,7 @@
|
|||
;; of the two says it is. A test that recomputed the chain would only be
|
||||
;; checking arithmetic against itself; this checks `node/local!`, `node/world!`
|
||||
;; and `emit` as well.
|
||||
(let [c (freeze/head-mode {:mode :as-filmed} @frozen)
|
||||
(let [c (freeze/head-mode {:mode :free} @frozen)
|
||||
res (timeline/resolver (clip/root c) @store pal/index-of)
|
||||
k (first (:value (chan :face [:xform :scale])))
|
||||
anc (:value (chan :face [:xform :anchor]))
|
||||
|
|
@ -234,47 +236,48 @@
|
|||
"], the composition says [" wx " " wy "]"))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; the three modes are the three channel shapes
|
||||
;; one dense measurement, with optional held anchor frames
|
||||
|
||||
(deftest the-three-head-modes-are-the-three-channel-shapes
|
||||
(let [kept #{0 12 40 88 150}
|
||||
of (fn [c path] (get-in (clip/nodes c) [:head :channels path]))]
|
||||
(testing "locked is framed identity"
|
||||
(let [sc (freeze/head-mode {:mode :locked} @frozen)]
|
||||
(is (= [:framed :framed :framed]
|
||||
(mapv #(ch/describe (of sc %))
|
||||
[[:xform :pos] [:xform :rot] [:xform :scale]])))
|
||||
(is (= [0.0 0.0] (:value (of sc [:xform :pos]))))
|
||||
(is (= 0.0 (:value (of sc [:xform :rot]))))
|
||||
(is (= [1.0 1.0] (:value (of sc [:xform :scale]))))))
|
||||
(testing "as filmed is dense"
|
||||
(let [sc (freeze/head-mode {:mode :as-filmed} @frozen)]
|
||||
(is (= [:dense :dense :dense]
|
||||
(mapv #(ch/describe (of sc %))
|
||||
[[:xform :pos] [:xform :rot] [:xform :scale]])))))
|
||||
(testing "per plate is keyed at exactly the kept frames"
|
||||
(let [sc (freeze/head-mode {:mode :per-plate :kept kept} @frozen)]
|
||||
(doseq [path [[:xform :pos] [:xform :rot] [:xform :scale]]]
|
||||
(is (= :keyed (ch/describe (of sc path))))
|
||||
(is (= (sort kept) (sort (keys (:keys (of sc path))))))
|
||||
;; A dense read is a VIEW into tier 2. Storing one in the document would
|
||||
;; be storing a value that changes when a re-freeze rewrites the array
|
||||
;; under it, so the keys hold plain data.
|
||||
(doseq [[_ v] (:keys (of sc path))]
|
||||
(is (or (number? v) (vector? v)) (str path " key is " (pr-str v)))))
|
||||
;; And the keys are the dense track sampled at those frames, which is the
|
||||
;; whole of what "per plate" means.
|
||||
(is (= (mapv #(ch/value-at (get-in (node :head) [:measured [:xform :rot]]) % @store)
|
||||
(sort kept))
|
||||
(mapv (:keys (of sc [:xform :rot])) (sort kept))))))))
|
||||
(deftest head-anchors-select-measured-frames-without-copying-channels
|
||||
(let [free (freeze/head-mode {:mode :free} @frozen)
|
||||
one (freeze/head-mode {:mode :anchored :anchors {0 12}} @frozen)
|
||||
keyed (freeze/head-mode {:mode :anchored
|
||||
:anchors {0 12, 40 88, 150 150}} @frozen)
|
||||
of (fn [c path] (get-in (clip/nodes c) [:head :channels path]))]
|
||||
(doseq [c [free one keyed]
|
||||
path [[:xform :pos] [:xform :rot] [:xform :scale]]]
|
||||
(is (= :dense (ch/describe (of c path))))
|
||||
(is (= (of free path) (of c path)) "anchor edits do not copy measurements"))
|
||||
(is (nil? (get-in (clip/nodes free) [:head :anchors])))
|
||||
(is (= {0 12} (get-in (clip/nodes one) [:head :anchors])))
|
||||
(is (= {0 12, 40 88, 150 150}
|
||||
(get-in (clip/nodes keyed) [:head :anchors])))
|
||||
(is (= keyed (leaf/clip "head" (leaf/leaves "head" keyed)))
|
||||
"anchor source addresses survive the document round trip")))
|
||||
|
||||
(deftest head-anchor-keys-hold-the-whole-measured-transform
|
||||
(let [free (timeline/resolver (clip/root
|
||||
(freeze/head-mode {:mode :free} @frozen))
|
||||
@store pal/index-of)
|
||||
held (timeline/resolver (clip/root
|
||||
(freeze/head-mode {:mode :anchored
|
||||
:anchors {0 12, 40 88}} @frozen))
|
||||
@store pal/index-of)
|
||||
world (fn [resolver frame]
|
||||
(resolver frame)
|
||||
(vec (array-seq (timeline/world-of resolver :head))))]
|
||||
(is (= (world free 12) (world held 0)))
|
||||
(is (= (world free 12) (world held 38)))
|
||||
(is (= (world free 88) (world held 40)))
|
||||
(is (= (world free 88) (world held 100)))))
|
||||
|
||||
(deftest switching-modes-rewrites-the-head-and-nothing-else
|
||||
;; It has to be impossible for the toggle to move something a hand placed, and
|
||||
;; it has to be a DOCUMENT edit: tier 1, undoable, syncable, instant, and not a
|
||||
;; reason to re-analyse.
|
||||
(let [a (freeze/head-mode {:mode :as-filmed} @frozen)
|
||||
b (freeze/head-mode {:mode :locked} @frozen)
|
||||
c (freeze/head-mode {:mode :per-plate :kept #{0 40}} @frozen)]
|
||||
(let [a (freeze/head-mode {:mode :free} @frozen)
|
||||
b (freeze/head-mode {:mode :anchored :anchors {0 0}} @frozen)
|
||||
c (freeze/head-mode {:mode :anchored :anchors {0 0, 40 40}} @frozen)]
|
||||
(doseq [x [b c]]
|
||||
(is (= (get (clip/nodes a) :face) (get (clip/nodes x) :face))
|
||||
":face moved")
|
||||
|
|
@ -288,13 +291,14 @@
|
|||
;; channels, so the clip's own fields and its other timelines are untouched.
|
||||
(is (= (dissoc a :timelines) (dissoc x :timelines))))))
|
||||
|
||||
(deftest a-head-mode-that-is-not-one-of-the-three-is-refused
|
||||
(is (thrown-with-msg? ExceptionInfo #"not one of the three channel shapes"
|
||||
(deftest invalid-head-anchor-maps-are-refused
|
||||
(is (thrown-with-msg? ExceptionInfo #"free or anchored"
|
||||
(freeze/head-mode {:mode :stabilised} @frozen)))
|
||||
;; The kept-frame set belongs to the plate strip, not to measurement, so freeze
|
||||
;; cannot invent one.
|
||||
(is (thrown-with-msg? ExceptionInfo #"kept-frame set"
|
||||
(freeze/head-mode {:mode :per-plate} @frozen))))
|
||||
(doseq [anchors [nil {} {12 12} {0 take/frames} {0 0, 10 -1}]]
|
||||
(is (thrown-with-msg? ExceptionInfo #"frame-zero key"
|
||||
(freeze/head-mode {:mode :anchored :anchors anchors} @frozen))))
|
||||
(is (thrown-with-msg? ExceptionInfo #"has no anchors"
|
||||
(freeze/head-mode {:mode :free :anchors {0 0}} @frozen))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; the face: authored, and what makes makeXform deletable
|
||||
|
|
@ -597,7 +601,7 @@
|
|||
;; from different beats are genuinely different mouths and frames inside one
|
||||
;; beat are not. This is the numeric half of step 5's done-criterion; the other
|
||||
;; half is a picture and lives in test/browser/take.mjs.
|
||||
(let [locked (freeze/head-mode {:mode :locked} @frozen)
|
||||
(let [locked (freeze/head-mode {:mode :anchored :anchors {0 0}} @frozen)
|
||||
shot (fn [f]
|
||||
(let [r (raster/make W H)
|
||||
mouth (filter #(= :mouth (:node %)) (ops-at (clip/root locked) f))]
|
||||
|
|
@ -614,6 +618,6 @@
|
|||
(is (< (differ (shot 10) (shot 12)) 200)
|
||||
"a held pose is moving more than the detector noise it should have lost"))
|
||||
;; As filmed, the head carries it around the stage as well.
|
||||
(let [filmed (freeze/head-mode {:mode :as-filmed} @frozen)]
|
||||
(let [filmed (freeze/head-mode {:mode :free} @frozen)]
|
||||
(is (> (count (remove true? (map = (render filmed 10) (render filmed 120)))) 300)
|
||||
"the head does not move across the take")))
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
(delay
|
||||
(let [p (merge take/knobs
|
||||
{:name "regen" :fps 30 :aspect 1 :stage [320 200]
|
||||
:expose 1 :head :as-filmed
|
||||
:expose 1 :head :free
|
||||
:analysis (address/analysis
|
||||
{:detector "synth" :version "mulberry32"
|
||||
:seed 9 :frames frames :fps 30 :aspect 1})})]
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
(take/build
|
||||
(merge take/knobs
|
||||
{:name "regen" :fps 30 :aspect 1 :stage [320 200]
|
||||
:expose 1 :head :as-filmed
|
||||
:expose 1 :head :free
|
||||
:analysis (get-in @initial [:clip :analysis])}
|
||||
change)
|
||||
@inputs))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue