grand unification of time
This commit is contained in:
parent
a4ce750be2
commit
05878ca48b
40 changed files with 334 additions and 292 deletions
70
frontend/test/arthur/domain/cadence_test.cljs
Normal file
70
frontend/test/arthur/domain/cadence_test.cljs
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
(ns arthur.domain.cadence-test
|
||||
(:require [cljs.test :refer [deftest is testing]]
|
||||
[arthur.domain.bring :as bring]
|
||||
[arthur.domain.cadence :as cadence]
|
||||
[arthur.domain.channel :as ch]
|
||||
[arthur.domain.clip :as clip]
|
||||
[arthur.domain.leaf :as leaf]
|
||||
[arthur.domain.nest :as nest]
|
||||
[arthur.domain.node :as node]
|
||||
[arthur.domain.palette :as pal]
|
||||
[arthur.domain.pick :as pick]
|
||||
[arthur.domain.pose :as pose]
|
||||
[arthur.ui.timeline :as timeline]))
|
||||
|
||||
(def footage
|
||||
{:fps 30 :width 100 :height 100
|
||||
:symbols {:main {:id :main :fps 30 :frames 60
|
||||
:nodes {:mark {:id :mark :kind :rect :z "a" :pose-group :mouth
|
||||
:channels {[:geom :size] {:animated? true :pose-sampled? true
|
||||
:dense {:store "sizes" :offset 0 :stride 1 :frames 60}}
|
||||
[:style :color] (ch/framed :brow)}}}}}})
|
||||
(def store {"sizes" {:data (js/Float64Array. (clj->js (range 1 61)))}})
|
||||
|
||||
(deftest selection-is-integral-and-never-early
|
||||
(is (= [0 2 5 7 10 12] (mapv #(cadence/frame % 12 30) (range 6))))
|
||||
(doseq [grid [8 12 24 30 60] native [12 24 30 60] f (range 90)]
|
||||
(let [selected (cadence/frame f grid native)]
|
||||
(is (and (integer? selected) (<= selected (/ (* f native) grid))
|
||||
(< (- (/ (* f native) grid) selected) 1))))))
|
||||
|
||||
(deftest changing-output-fps-preserves-dense-content-and-seconds
|
||||
(let [doc (clip/set-fps footage 12)
|
||||
draw (clip/resolver doc :main store pal/index-of nil)]
|
||||
(is (= 24 (clip/output-frames doc :main)))
|
||||
(is (= (:symbols footage) (:symbols doc)))
|
||||
(is (= footage (clip/set-fps doc 30)))
|
||||
(is (= doc (leaf/clip "test" (leaf/leaves "test" doc))))
|
||||
(is (= [1 3 6 8 11 13] (mapv #(:size (first (draw %))) (range 6))))
|
||||
(is (= 7 (:frame (nest/inside doc store :main [:mark] 3))))
|
||||
(is (= [0 24] (:span (first (timeline/rows doc :main #{})))))))
|
||||
|
||||
(deftest imported-footage-uses-selection-for-picture-and-real-speed-for-audio
|
||||
(let [{doc :clip sid :sid} (bring/take (clip/set-fps (clip/blank) 12)
|
||||
footage "take" "video" [15 75])
|
||||
;; A host authored at 12, with a 30fps source starting half a second in.
|
||||
doc (-> doc (assoc-in [:symbols :main :fps] 12)
|
||||
(clip/place-symbol store :main sid 6 :insert nil))
|
||||
n (get-in doc [:symbols :main :nodes :insert])
|
||||
draw (clip/resolver doc :main store pal/index-of nil)
|
||||
[sound] (nest/audio-tracks doc :main)]
|
||||
(is (= 60 (clip/frames doc sid)))
|
||||
(is (= 1 (get-in doc [:symbols sid :nodes :sound :time :rate])))
|
||||
(is (= [0 24] (:span n)))
|
||||
(is (empty? (draw 5)))
|
||||
(is (= 8 (:size (first (draw 9)))))
|
||||
(is (= 7 (:frame (nest/inside doc store :main [:insert :mark] 9))))
|
||||
(is (= [-4 -4 4 4] ((pick/bounds-of doc store :main n) 3)))
|
||||
(is (= [6 30] (node/placed-span sound)))
|
||||
(is (= 15 (node/local-frame sound 6)))
|
||||
(is (= 1 (* (get-in sound [:time :rate]) (/ (:fps doc) (:fps sound)))))
|
||||
(is (= [6 30] (:span (first (timeline/rows doc :main #{[:insert]})))))
|
||||
(testing "an off-grid closed mouth can win without modifying the dense source"
|
||||
(let [pinned (pose/put-cut doc :main :insert :mouth 7 6)
|
||||
r (clip/resolver pinned :main store pal/index-of nil)]
|
||||
(is (= 7 (:size (first (r 9)))))
|
||||
(is (= (get-in doc [:symbols sid]) (get-in pinned [:symbols sid])))))
|
||||
(testing "a deliberate half speed still retimes audio"
|
||||
(let [slow (assoc-in doc [:symbols :main :nodes :insert :playback :speed] 0.5)
|
||||
[track] (nest/audio-tracks slow :main)]
|
||||
(is (= 0.5 (* (get-in track [:time :rate]) (/ (:fps slow) (:fps track)))))))))
|
||||
|
|
@ -98,7 +98,7 @@
|
|||
[c st open path f]
|
||||
(let [{:keys [sid id world frame]} (nest/placement c st open path f)
|
||||
n (get-in c [:symbols sid :nodes id])
|
||||
[x0 y0 x1 y1] ((pick/bounds-of c st n) frame)]
|
||||
[x0 y0 x1 y1] ((pick/bounds-of c st sid n) frame)]
|
||||
{:corners (mapv #(at world %) [[x0 y0] [x1 y0] [x1 y1] [x0 y1]])
|
||||
:pivot (at world (:anchor (gesture/values n frame st)))}))
|
||||
|
||||
|
|
@ -135,7 +135,7 @@
|
|||
[]
|
||||
(let [c (two-down)]
|
||||
(assoc-in c [:symbols :box :nodes :shape :channels [:xform :anchor]]
|
||||
(ch/framed (pick/pivot c nil (get-in c [:symbols :box :nodes :shape]) [4])))))
|
||||
(ch/framed (pick/pivot c nil :box (get-in c [:symbols :box :nodes :shape]) [4])))))
|
||||
|
||||
(deftest dragging-a-corner-away-from-the-middle-makes-it-bigger
|
||||
(doseq [[what c path f]
|
||||
|
|
@ -287,5 +287,5 @@
|
|||
(deftest an-instances-box-is-what-its-symbol-draws
|
||||
(let [c (two-down)
|
||||
{:keys [frame]} (nest/placement c nil :main [u v] 16)]
|
||||
(is (= [0 0 10 10] ((pick/bounds-of c nil (get-in c [:symbols :mid :nodes v])) frame)))
|
||||
(is (= [0 0 10 10] ((pick/bounds-of c nil (get-in c [:symbols :box :nodes :shape])) 4)))))
|
||||
(is (= [0 0 10 10] ((pick/bounds-of c nil :mid (get-in c [:symbols :mid :nodes v])) frame)))
|
||||
(is (= [0 0 10 10] ((pick/bounds-of c nil :box (get-in c [:symbols :box :nodes :shape])) 4)))))
|
||||
|
|
|
|||
|
|
@ -82,9 +82,7 @@
|
|||
:sym/poses symbol}}
|
||||
store {"sizes" {:data values}}
|
||||
resolve (clip/resolver document :main store pal/index-of nil)
|
||||
low-resolve (clip/resolver document :main store pal/index-of {: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)))]
|
||||
at (fn [f] (into {} (map (fn [op] [(:node op) op])) (resolve f)))]
|
||||
(is (empty? (clip/problems document)))
|
||||
(is (= document (leaf/clip "stage" (leaf/leaves "stage" document))))
|
||||
(is (= 102 (:size (get (at 7) [:first :mouth]))) "eight static frames, plus its correction")
|
||||
|
|
@ -97,14 +95,8 @@
|
|||
(is (nil? (get (at 9) [:first :mouth]))
|
||||
"generated visibility is read from the same selected pose")
|
||||
(is (some? (get (at 9) [:second :mouth])))
|
||||
(is (= 105 (:size (get (low-at 7) [:first :brow])))
|
||||
"picture rate samples the generated base and leaves correction time alone")
|
||||
(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 [sym (get-in document [:symbols :sym/poses])
|
||||
opts {:source-fps 30 :picture-fps 8 :pose-tracks {:mouth {0 0, 8 20}}}]
|
||||
opts {:pose-tracks {:mouth {0 0, 8 20}}}]
|
||||
(is (= (mapv #(select-keys % [:node :cx :size])
|
||||
(symbol/eval-frame sym 8 store pal/index-of opts))
|
||||
(mapv #(select-keys % [:node :cx :size])
|
||||
|
|
@ -249,7 +241,7 @@
|
|||
made (clip/new-symbol c :outer id 20 u)]
|
||||
(is (= :symbol-1 id))
|
||||
(is (= :symbol-2 (clip/fresh-id made)) "the next one does not collide")
|
||||
(is (= {:id :symbol-1 :name "symbol-1" :frames 180 :nodes {}}
|
||||
(is (= {:id :symbol-1 :name "symbol-1" :fps 30 :frames 180 :nodes {}}
|
||||
(clip/symbol made :symbol-1))
|
||||
"empty, and as long as the rest of what it was placed in")
|
||||
(is (= {:span [0 180] :time {:mode :map :at 20 :rate 1}}
|
||||
|
|
|
|||
|
|
@ -165,8 +165,8 @@
|
|||
(is (= 0 (:frame (nest/inside doc nil :main [:a] 3))))
|
||||
(is (nil? (:time (nest/inside doc nil :main [:a] 3))))
|
||||
(is (nil? (nest/inside doc nil :main [:a] 4)))
|
||||
(is (= ((pick/bounds-of doc nil n) 2)
|
||||
((pick/bounds-of doc nil (assoc-in n [:playback :in] 5)) 0)))))
|
||||
(is (= ((pick/bounds-of doc nil :main n) 2)
|
||||
((pick/bounds-of doc nil :main (assoc-in n [:playback :in] 5)) 0)))))
|
||||
|
||||
(deftest seeking-and-source-reuse-do-not-share-cursors
|
||||
(let [doc (assoc-in (document) [:symbols :main :nodes :b :source :symbol] :drawing-a)
|
||||
|
|
|
|||
|
|
@ -49,8 +49,8 @@
|
|||
;; A TIMELINE ID IS A SEGMENT, which is what lets a symbol's nodes be
|
||||
;; addressed by the same path shape as the clip's own. `main` is the root.
|
||||
(is (contains? ls "clip/c7/symbol/main"))
|
||||
(is (= {:frames 229} (get ls "clip/c7/symbol/main"))
|
||||
"a timeline's leaf is its frame space; :fps is the clip's")
|
||||
(is (= {:frames 229 :fps 30} (get ls "clip/c7/symbol/main"))
|
||||
"a symbol persists its native grid separately from the output grid")
|
||||
;; Nodes are local to the face timeline; feature and group ids are clip-wide.
|
||||
(is (contains? ls "clip/c7/symbol/face-1/node/mouth"))
|
||||
(is (contains? ls "clip/c7/symbol/face-1/channel/mouth/geom.pts"))
|
||||
|
|
|
|||
|
|
@ -1,19 +1,5 @@
|
|||
(ns arthur.export-test
|
||||
"The frame walk and the arithmetic above the sink.
|
||||
|
||||
THE SYNC RULE IS THE POINT OF THIS FILE. `arthur.export` states it twice — in
|
||||
its own docstring and in `plan`'s comment — because it is the one failure the
|
||||
export path exists to prevent: a lower picture rate must HOLD each pose across
|
||||
several frames and never drop frames, so the emitted length always matches the
|
||||
audio. Decimating instead gives a file that is silently short, whose sound
|
||||
slides progressively out of sync, and which looks correct in every other
|
||||
respect. Nothing downstream can detect that, so it is asserted here, at both
|
||||
levels: `plan` reports poses separately from frames, and `run!` emits every
|
||||
frame of the frame space whatever the picture rate is.
|
||||
|
||||
The sink is a recording fake. What the walk owes a sink is an ordering and a
|
||||
count, and a fake is the only way to assert on those without also asserting on
|
||||
PNG bytes — which `export.frames-test` already does."
|
||||
"The output-grid frame walk and duration, using a recording sink."
|
||||
(:require [cljs.test :refer [deftest is testing async]]
|
||||
[arthur.domain.channel :as ch]
|
||||
[arthur.domain.clip :as clip]
|
||||
|
|
@ -85,21 +71,12 @@
|
|||
(is (= 1 (zoom-of nil)) "an absent zoom is 1:1")
|
||||
(is (= 1 (zoom-of 1.9)))))
|
||||
|
||||
(deftest a-lower-picture-rate-changes-the-poses-and-not-the-length
|
||||
;; THE SYNC RULE, in the arithmetic. 48 frames at 24fps is two seconds; at a
|
||||
;; 12fps picture rate it is still 48 frames and two seconds, holding 24 poses.
|
||||
;; If :frames ever tracks :poses here, every export at a reduced picture rate
|
||||
;; comes out half length with the audio sliding off it.
|
||||
(let [p (export/plan {:clip (a-clip {:frames 48 :fps 24}) :symbol :main
|
||||
:picture-fps 12})]
|
||||
(is (= 48 (:frames p)) "the frame count does not move")
|
||||
(is (= 2 (:seconds p)) "and neither does the duration")
|
||||
(is (= 24 (:poses p)) "but the picture holds half as many poses"))
|
||||
(testing "a picture rate at or above the clip's rate changes nothing"
|
||||
(doseq [fps [24 48 nil]]
|
||||
(let [p (export/plan {:clip (a-clip {:frames 48 :fps 24}) :symbol :main
|
||||
:picture-fps fps})]
|
||||
(is (= 48 (:poses p)) (str "picture-fps " fps))))))
|
||||
(deftest a-lower-output-rate-keeps-native-duration
|
||||
(let [doc (assoc-in (a-clip {:frames 48 :fps 24}) [:symbols :main :fps] 24)
|
||||
p (export/plan {:clip (clip/set-fps doc 12) :symbol :main})]
|
||||
(is (= 24 (:frames p)))
|
||||
(is (= 12 (:fps p)))
|
||||
(is (= 2 (:seconds p)))))
|
||||
|
||||
(deftest plan-of-a-symbol-that-is-not-there-is-nothing
|
||||
(is (nil? (export/plan {:clip (a-clip {}) :symbol :nope :zoom 1}))))
|
||||
|
|
@ -115,17 +92,12 @@
|
|||
(done)))
|
||||
(.catch (fn [e] (is false (str "threw: " e)) (done))))))
|
||||
|
||||
(deftest a-lower-picture-rate-still-emits-every-frame
|
||||
;; THE SYNC RULE, in the walk — the assertion that matters most in this file.
|
||||
;; The poses repeat; the frames do not thin out.
|
||||
(deftest a-lower-output-rate-emits-the-selected-grid
|
||||
(async done
|
||||
(-> (run!* (a-clip {:frames 12 :fps 24}) :picture-fps 8)
|
||||
(-> (run!* (clip/set-fps (a-clip {:frames 12 :fps 24}) 8))
|
||||
(.then (fn [{:keys [frames spec]}]
|
||||
(is (= 12 (count frames))
|
||||
"a 12-frame timeline exports 12 frames at any picture rate")
|
||||
(is (= (range 12) (map :i frames)))
|
||||
(is (= 24 (:fps spec))
|
||||
"and the file's rate is the CLIP's, not the picture rate")
|
||||
(is (= (range 4) (map :i frames)))
|
||||
(is (= 8 (:fps spec)))
|
||||
(done)))
|
||||
(.catch (fn [e] (is false (str "threw: " e)) (done))))))
|
||||
|
||||
|
|
|
|||
|
|
@ -336,14 +336,14 @@
|
|||
frames (range (get-in c [:symbols sid :frames]))
|
||||
anchor (:value (get-in n [:channels [:xform :anchor]]))
|
||||
usable (and (not (node/measured? n))
|
||||
(some? (pick/pivot c @store n frames)))]
|
||||
(some? (pick/pivot c @store sid n frames)))]
|
||||
(is (= usable (some? anchor))
|
||||
(str sid "/" id " has a pivot: " (some? anchor)
|
||||
", but a hand can use one: " usable))
|
||||
(is (= (nil? (gesture/refusal n)) (not (node/measured? n)))
|
||||
(str sid "/" id ": `refusal` and `measured?` disagree"))
|
||||
(when anchor
|
||||
(let [bounds (pick/bounds-of c @store n)
|
||||
(let [bounds (pick/bounds-of c @store sid n)
|
||||
[x0 y0 x1 y1] (reduce #(let [k (bounds %2)]
|
||||
(cond (nil? %1) k (nil? k) %1
|
||||
:else (mapv (fn [op i] (op (nth %1 i) (nth k i)))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue