One time map for every node; move and group nodes between symbols
Every node now has the same time map into its parent, local = rate·(parent − at), with its span and keys in its own frames: what instances had, made the rule. A shape without a time map reads as it always did, so no data changes. The per-kind branches, the span-start term and the rate refusal are gone; node/time-of, then-time and invert-time compose it like the matrix. clip/move-node puts a node into another symbol without changing the picture or the timing — its matrix becomes a :pinv, its time a new :at and :rate, and its channels, keys and span are untouched — and clip/group makes a new symbol around side-by-side nodes. Generated parts, split stencils, cycles and looping instances are refused with the reason. Nested sounds use the same map. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
6bec121108
commit
eafbe6c4d2
7 changed files with 360 additions and 91 deletions
|
|
@ -293,7 +293,8 @@
|
|||
(assoc-in [:symbols sid :nodes :sound]
|
||||
{:id :sound :name "sound" :kind :audio :parent nil
|
||||
:z "z-sound" :source {:footage footage-id}
|
||||
:span range :time {:mode :map :at 0 :rate 1}}))
|
||||
;; Source frame `start` plays on the symbol's 0.
|
||||
:span range :time {:mode :map :at (- (first range)) :rate 1}}))
|
||||
tracked? (-> (assoc :analysis (:analysis take))
|
||||
(update :subjects merge (:subjects take))
|
||||
(update :features merge (:features take))
|
||||
|
|
@ -304,31 +305,29 @@
|
|||
and, recursively, those inside the instances it places.
|
||||
|
||||
A sound inside a placed symbol is heard where the instance puts it, so each one
|
||||
is carried OUT through the instance's time map — `:at` and `:rate`, and clipped
|
||||
to the instance's own span — until it is in the frames of the symbol being
|
||||
played. Keyed automation moves with it. What comes back is what a mixer that
|
||||
only knows flat tracks can play as it is."
|
||||
is carried OUT through the instance's time map — the same map a timeline row
|
||||
draws with — and cut to the instance's own span, until it is in the frames of
|
||||
the symbol being played. Keyed automation moves with it. What comes back is
|
||||
what a mixer that only knows flat tracks can play as it is."
|
||||
[clip sid]
|
||||
(let [sym (symbol clip sid)]
|
||||
(into (vec (filter #(= :audio (:kind %)) (vals (:nodes sym))))
|
||||
(mapcat
|
||||
(fn [inst]
|
||||
(let [{:keys [mode at rate] :or {at 0 rate 1}} (:time inst)
|
||||
inner (symbol clip (:of inst))
|
||||
[in out] (or (:span inst) [0 (:frames inner)])
|
||||
in (if (= mode :map) in 0)
|
||||
->outer (fn [x] (+ at (/ (- x in) rate)))]
|
||||
(let [outer (node/time-of inst)
|
||||
->outer (fn [x] (+ (:at outer) (/ x (:rate outer))))
|
||||
[in out] (or (:span inst) [0 (frames clip (:of inst))])]
|
||||
(keep (fn [a]
|
||||
(let [{a-at :at a-rate :rate :or {a-at 0 a-rate 1}} (:time a)
|
||||
[s0 s1] (or (:span a) [0 (:frames inner)])
|
||||
x0 (max a-at in)
|
||||
x1 (min (+ a-at (/ (- s1 s0) a-rate)) out)]
|
||||
(let [[p0 p1] (or (node/placed-span a) [in out])
|
||||
x0 (max p0 in)
|
||||
x1 (min p1 out)
|
||||
own (node/time-of a)
|
||||
->own (fn [x] (* (:rate own) (- x (:at own))))
|
||||
world (node/then-time outer own)]
|
||||
(when (< x0 x1)
|
||||
(-> a
|
||||
(assoc :span [(+ s0 (* a-rate (- x0 a-at)))
|
||||
(+ s0 (* a-rate (- x1 a-at)))]
|
||||
:time {:mode :map :at (->outer x0)
|
||||
:rate (* a-rate rate)})
|
||||
(assoc :span [(->own x0) (->own x1)]
|
||||
:time {:mode :map :at (:at world) :rate (:rate world)})
|
||||
(update :channels
|
||||
(fn [chs]
|
||||
(into {} (map (fn [[p ch]]
|
||||
|
|
@ -387,6 +386,177 @@
|
|||
[(aget out 0) (aget out 1)]))
|
||||
(partition 2 pts)))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; reparenting
|
||||
;;
|
||||
;; ONE NESTING, AS FAR AS A PERSON IS CONCERNED. Putting a node inside another
|
||||
;; symbol is how things are grouped: the symbol is a shared timeline, and its
|
||||
;; instance is the handle that moves, retimes and transforms everything in it
|
||||
;; together. Parent pointers inside a symbol stay — the roto rig is built on them
|
||||
;; — but they are not something the timeline hands out.
|
||||
;;
|
||||
;; A MOVE CHANGES NEITHER THE PICTURE NOR THE TIMING. Space: the node keeps its
|
||||
;; own channels and gains a `:pinv`, the matrix from where it was to where it
|
||||
;; goes, which is Blender's parent-inverse and what `node/world!` has always
|
||||
;; applied. Time: its frames shift by the difference between the two symbols'
|
||||
;; time maps — its keys and span if it is a shape, only its `:at` if it is an
|
||||
;; instance, whose span is in its own frames and does not move at all.
|
||||
|
||||
(defn- time-down
|
||||
"The time map from symbol `sid` down through the instances `path` names: the
|
||||
frame of the last one's symbol for each frame of `sid`. Each step is the
|
||||
instance's own map after its parents' in that symbol, so the map is the one
|
||||
the stage reads with, floors aside. Nil through a looping instance, whose
|
||||
frames come round again and do not map one to one."
|
||||
[clip sid path]
|
||||
(:down
|
||||
(reduce (fn [{:keys [sid down]} id]
|
||||
(let [nodes (:nodes (symbol clip sid))
|
||||
chain (map #(get nodes %) (rseq (symbol/lineage nodes id)))]
|
||||
(if (some #(get-in % [:time :loop?]) chain)
|
||||
(reduced {:down nil})
|
||||
{:sid (:of (get nodes id))
|
||||
:down (reduce node/then-time down (map node/time-of chain))})))
|
||||
{:sid sid :down {:at 0 :rate 1}}
|
||||
path)))
|
||||
|
||||
(defn- retime
|
||||
"Node `n` with its own time map replaced by `m`, and nothing else touched: its
|
||||
span and keys are in its own frames, which a move does not change."
|
||||
[n {:keys [at rate]}]
|
||||
(assoc n :time (merge (:time n) {:mode :map :at at :rate rate :offset 0})))
|
||||
|
||||
(defn- subtree
|
||||
"`id` and every node whose parent chain reaches it."
|
||||
[nodes id]
|
||||
(into #{} (filter #(some #{id} (symbol/lineage nodes %))) (keys nodes)))
|
||||
|
||||
(defn- transplant
|
||||
"Move node `id` from symbol `host`, where frame `frame` is showing, into symbol
|
||||
`target`, keeping where it is on screen and when. `carry` is the matrix from
|
||||
`host`'s coordinates to `target`'s, and `back` the time map from `target`'s
|
||||
frames to `host`'s.
|
||||
|
||||
THE ONE RULE, for space and time alike: the node's new map is its old one
|
||||
under what it leaves — its parents here, and the way from here to there — so
|
||||
the picture and the timing through it do not change. For space that is a
|
||||
`:pinv`, Blender's parent-inverse; for time it is a new `:at` and `:rate`. Its
|
||||
channels, keys and span are untouched, and its children keep their parent
|
||||
pointers and come with it. `{:clip}` or `{:refused why}`."
|
||||
[clip store host frame id target carry back]
|
||||
(let [nodes (:nodes (symbol clip host))
|
||||
n (get nodes id)
|
||||
moving (subtree nodes id)
|
||||
;; Its parents in this symbol, outermost first.
|
||||
chain (map #(get nodes %) (reverse (rest (symbol/lineage nodes id))))
|
||||
parent (when-let [p (:parent n)]
|
||||
(let [r (symbol/resolver (symbol clip host) store pal/index-of nil
|
||||
{:source-fps (:fps clip)})]
|
||||
(r frame)
|
||||
(some-> (symbol/world-of r p) js/Float64Array.from)))]
|
||||
(cond
|
||||
(= host target) {:refused "it is already there"}
|
||||
(and (= :instance (:kind n)) (contains-symbol? clip (:of n) target))
|
||||
{:refused "a symbol cannot go inside itself"}
|
||||
(some (fn [m] (or (:measured (get nodes m))
|
||||
(some #(or (:dense %) (:generated %)) (vals (:channels (get nodes m))))))
|
||||
moving)
|
||||
{:refused "generated parts stay with their take — move the instance that places it"}
|
||||
(some (fn [[k m]] (and (:stencil m)
|
||||
(not= (contains? moving k) (contains? moving (:stencil m)))))
|
||||
nodes)
|
||||
{:refused "a stencil and what it clips have to move together"}
|
||||
(and (:parent n) (nil? parent))
|
||||
{:refused "its parent is not on screen at this frame"}
|
||||
(some #(get-in % [:time :loop?]) chain)
|
||||
{:refused "a looping parent is in the way"}
|
||||
:else
|
||||
(let [taken (:nodes (symbol clip target))
|
||||
ids (into {} (map (fn [m] [m (free-id #(contains? taken %) m)])) moving)
|
||||
pinv (reduce #(node/mul! (node/mat) %1 %2) carry (keep identity [parent (node/pinv n)]))
|
||||
;; back · parents · own: target frames to the node's own.
|
||||
time (reduce node/then-time back (concat (map node/time-of chain)
|
||||
[(node/time-of n)]))
|
||||
moved (for [m moving
|
||||
:let [x (get nodes m)]]
|
||||
(cond-> (-> x
|
||||
(assoc :id (ids m))
|
||||
(update :parent #(get ids %)))
|
||||
(:stencil x) (update :stencil ids)
|
||||
(= m id) (-> (retime time)
|
||||
(assoc :pinv (vec (array-seq pinv))
|
||||
:z (str "z" (js/Date.now) "-" (ids m))))))]
|
||||
{:clip (-> clip
|
||||
(update-symbol host update :nodes #(apply dissoc % moving))
|
||||
(update-symbol target update :nodes (fnil into {})
|
||||
(map (juxt :id identity)) moved))}))))
|
||||
|
||||
(defn move-node
|
||||
"Move the node at row path `from` — its last id is the node, the rest the
|
||||
instances down to where it lives — into the symbol placed by the instance at
|
||||
row path `to`, or to the top of `open` when `to` is empty. Row paths start at
|
||||
`open`, and `f` is its current frame, at which both have to be on screen.
|
||||
`{:clip}`, or `{:refused why}`."
|
||||
[clip store open from to f]
|
||||
(let [here (inside clip store open (pop from) f)
|
||||
there (inside clip store open to f)
|
||||
a (time-down clip open (pop from))
|
||||
b (time-down clip open to)
|
||||
inv (some-> there :matrix invert)]
|
||||
(cond
|
||||
(nil? (get-in clip [:symbols (:sid here) :nodes (peek from)]))
|
||||
{:refused "nothing to move"}
|
||||
(or (nil? here) (nil? there)) {:refused "both have to be on screen at this frame"}
|
||||
(not (and a b)) {:refused "a looping instance is in the way"}
|
||||
(nil? inv) {:refused "the target is scaled to nothing"}
|
||||
:else (transplant clip store (:sid here) (:frame here) (peek from) (:sid there)
|
||||
(node/mul! (node/mat) inv (:matrix here))
|
||||
(node/then-time (node/invert-time b) a)))))
|
||||
|
||||
(defn group
|
||||
"Put the nodes at row paths `froms`, all side by side in one symbol, into a
|
||||
NEW symbol `sid`, placed where they were by instance `uuid`. `{:clip}` or
|
||||
`{:refused why}`.
|
||||
|
||||
The new symbol starts where the earliest of them starts and ends where the
|
||||
last one ends, so its instance's bar on the timeline covers exactly theirs.
|
||||
Its instance sits at the identity, so nothing moves, and pivots about the
|
||||
middle of what it now holds."
|
||||
[clip store open froms sid uuid f]
|
||||
(let [host-path (pop (first froms))
|
||||
{host :sid frame :frame} (inside clip store open host-path f)
|
||||
nodes (:nodes (symbol clip host))
|
||||
whole [0 (frames clip host)]
|
||||
spans (for [from froms
|
||||
:let [n (get nodes (peek from))]]
|
||||
(or (node/placed-span n)
|
||||
(when (= :instance (:kind n))
|
||||
(node/placed-span (assoc n :span [0 (frames clip (:of n))])))
|
||||
whole))
|
||||
start (max 0 (apply min (map first spans)))
|
||||
end (min (second whole) (apply max (map second spans)))]
|
||||
(cond
|
||||
(nil? host) {:refused "they have to be on screen at this frame"}
|
||||
(not-every? #(= host-path (pop %)) froms) {:refused "only things side by side can be grouped"}
|
||||
(some #(nil? (get nodes (peek %))) froms) {:refused "nothing to group"}
|
||||
:else
|
||||
(let [made (-> clip
|
||||
(assoc-in [:symbols sid] {:id sid :name (name sid)
|
||||
:frames (max 1 (js/Math.ceil (- end start)))
|
||||
:nodes {}})
|
||||
(place-symbol store host sid (js/Math.floor start) uuid nil))
|
||||
moved (reduce (fn [acc from]
|
||||
(let [r (transplant (:clip acc) store host frame (peek from) sid
|
||||
(node/mat)
|
||||
(node/invert-time
|
||||
(node/time-of (get-in (:clip acc) [:symbols host :nodes uuid]))))]
|
||||
(if (:refused r) (reduced r) r)))
|
||||
{:clip made} froms)]
|
||||
(cond-> moved
|
||||
(:clip moved) (update :clip assoc-in
|
||||
[:symbols host :nodes uuid :channels [:xform :anchor] :value]
|
||||
(center (:clip moved) store sid)))))))
|
||||
|
||||
(defn- transform-op
|
||||
"Put a symbol's already resolved mark into its instance's parent space."
|
||||
[op m path]
|
||||
|
|
|
|||
|
|
@ -103,27 +103,42 @@
|
|||
(/ source-fps picture-fps))))
|
||||
f))
|
||||
|
||||
(def timed-kinds
|
||||
"Kinds that place something with a time of its own — a symbol, a sound — and
|
||||
so read their `:span` in THAT time rather than in their parent's."
|
||||
#{:instance :audio})
|
||||
(defn time-of
|
||||
"A node's own time as the affine map it is: `{:at a :rate r}`, meaning a frame
|
||||
`p` of its parent is frame `r·(p − a)` of its own. THE SAME FOR EVERY NODE. A
|
||||
node with no time map is `{:at 0 :rate 1}`, reading its parent's frames as its
|
||||
own; a mouth lead's `:offset` is folded into `:at`. Exposure and picture
|
||||
sampling are floors, not part of the map, and are left out: this is the map a
|
||||
move preserves and a timeline row draws with, and `local-frame` is what reads
|
||||
a frame, floors and the lead in their load-bearing order."
|
||||
[n]
|
||||
(let [{:keys [mode at rate offset] :or {at 0 rate 1 offset 0}} (:time n)]
|
||||
(if (= mode :map)
|
||||
{:at (- at (/ offset rate)) :rate rate}
|
||||
{:at 0 :rate 1})))
|
||||
|
||||
(defn then-time
|
||||
"`outer` then `inner`: the map from `outer`'s parent straight to `inner`'s own
|
||||
frames. Time maps compose like matrices do, which is what makes a nesting of
|
||||
any depth one map."
|
||||
[{a1 :at r1 :rate} {a2 :at r2 :rate}]
|
||||
{:at (+ a1 (/ a2 r1)) :rate (* r1 r2)})
|
||||
|
||||
(defn invert-time [{:keys [at rate]}]
|
||||
{:at (- (* at rate)) :rate (/ 1 rate)})
|
||||
|
||||
(defn placed-span
|
||||
"Where a node exists, as `[in out)` in its PARENT's frames, or nil for always.
|
||||
|
||||
TWO MEANINGS OF `:span`, and this is the one place that knows both. A shape has
|
||||
no time of its own, so its `:span` is already in its parent's frames. An
|
||||
instance or a sound HAS a time of its own, and its `:span` is in that time: which
|
||||
frames of what it places are played, from its own 0 however late it enters. Where
|
||||
that first frame lands in the parent is `:at`, a separate fact — so moving an
|
||||
instance along its parent is one write to `:at`, and the span, which says what
|
||||
the instance IS, does not change when it is moved."
|
||||
A `:span` is in the node's OWN frames — which of its frames exist — for every
|
||||
node alike, and its time map says where they land in the parent. For a node
|
||||
with no time map the two are the same frames, so a shape's span reads as it
|
||||
always did. Moving a node along its parent is then one write to `:at`, and the
|
||||
span, which says what the node IS, does not change when it is moved."
|
||||
[n]
|
||||
(when-let [[in out] (:span n)]
|
||||
(if (timed-kinds (:kind n))
|
||||
(let [{:keys [at rate] :or {at 0 rate 1}} (:time n)]
|
||||
[at (+ at (/ (- out in) rate))])
|
||||
[in out])))
|
||||
(let [{:keys [at rate]} (time-of n)]
|
||||
[(+ at (/ in rate)) (+ at (/ out rate))])))
|
||||
|
||||
(defn local-frame
|
||||
"Apply a node's time map to the frame it was handed by its parent.
|
||||
|
|
@ -139,25 +154,18 @@
|
|||
as two performances; offset is PER-NODE by design, because mouth lead applies
|
||||
to performance nodes and not to the plate, which is the entire point of it."
|
||||
[n f]
|
||||
(let [{:keys [mode offset rate at source-fps sample-fps]
|
||||
ex :expose :or {mode :inherit}} (:time n)]
|
||||
(let [{:keys [mode source-fps sample-fps] ex :expose :or {mode :inherit}} (:time n)]
|
||||
(if (= mode :inherit)
|
||||
f
|
||||
(do
|
||||
(when (and (not (timed-kinds (:kind n))) rate (not= rate 1.0) (not= rate 1))
|
||||
(throw (ex-info "time map :rate belongs to an instance or an audio node"
|
||||
{:node (:id n) :time (:time n)})))
|
||||
(when (and sample-fps (not (and source-fps (pos? source-fps))))
|
||||
(throw (ex-info "picture sampling needs a positive source fps"
|
||||
{:node (:id n) :time (:time n)})))
|
||||
(cond-> (if (timed-kinds (:kind n))
|
||||
;; `:at` is where the span's first frame lands, so the parent's
|
||||
;; `at` is this node's `in`.
|
||||
(+ (or (first (:span n)) 0) (* (or rate 1) (- f (or at 0))))
|
||||
f)
|
||||
sample-fps (sample-frame source-fps sample-fps)
|
||||
ex (expose ex)
|
||||
offset (+ offset))))))
|
||||
(let [{:keys [at rate offset] :or {at 0 rate 1}} (:time n)]
|
||||
(cond-> (* rate (- f at))
|
||||
sample-fps (sample-frame source-fps sample-fps)
|
||||
ex (expose ex)
|
||||
offset (+ offset)))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; the transform
|
||||
|
|
@ -292,9 +300,9 @@
|
|||
(and (= k :instance) (nil? (:of n))) (conj "an instance needs :of")
|
||||
(and (= k :audio) (nil? (get-in n [:source :footage])))
|
||||
(conj "an audio instance needs :source :footage")
|
||||
(and (#{:instance :audio} k) (some? (get-in n [:time :rate]))
|
||||
(and (some? (get-in n [:time :rate]))
|
||||
(not (pos? (get-in n [:time :rate]))))
|
||||
(conj "an instance's :rate must be positive")
|
||||
(conj ":time :rate must be positive")
|
||||
(nil? (:z n)) (conj "no :z — draw order is authored per scene, not implied by the tree")
|
||||
(and (:span n) (not= 2 (count (:span n))))
|
||||
(conj ":span must be [in out]")
|
||||
|
|
|
|||
|
|
@ -126,11 +126,10 @@
|
|||
;; Which symbol an instance places. The one fact that makes an instance
|
||||
;; legible as an instance rather than as a node.
|
||||
"of" (when (= :instance (:kind n)) (str (:of n)))
|
||||
;; An instance's span is in its OWN frames and `at` is where it sits in
|
||||
;; this symbol; a shape's span is in this symbol's frames. See
|
||||
;; `node/placed-span`.
|
||||
;; A span is in the node's OWN frames and `at` is where its frame 0 sits
|
||||
;; in this symbol. See `node/placed-span`.
|
||||
"span" (when start (str start " … " end))
|
||||
"at" (when (node/timed-kinds (:kind n)) (str (get-in n [:time :at] 0)))]
|
||||
"at" (when (= :map (get-in n [:time :mode])) (str (get-in n [:time :at] 0)))]
|
||||
(when (:paint? n) [drawing-keys sid id n frame])
|
||||
[:div.row {:style {:margin-top "6px"}} [:span.dim "channels"]]
|
||||
[:dl.facts
|
||||
|
|
|
|||
|
|
@ -9,14 +9,12 @@
|
|||
column render from the same vector and therefore stay aligned without measuring
|
||||
anything.
|
||||
|
||||
EVERY FRAME NUMBER A ROW CARRIES IS IN THE OPEN SYMBOL'S FRAME SPACE. An
|
||||
instance's keys are in its own symbol's, and drawing them against the ruler
|
||||
unmapped would
|
||||
EVERY FRAME NUMBER A ROW CARRIES IS IN THE OPEN SYMBOL'S FRAME SPACE. A node's
|
||||
keys are in its own frames, and drawing them against the ruler unmapped would
|
||||
put a key under the wrong frame — silently, and most convincingly when the
|
||||
instance starts at 0. So the walk carries a `->open` function and composes one
|
||||
more mapping into it at each instance. The mapping is the inverse of
|
||||
`node/local-frame`: `local = in + rate·(parent - at)`, so
|
||||
`parent = at + (local - in)/rate`.
|
||||
node starts at 0. So the walk carries a `->open` function and composes one more
|
||||
mapping into it at each level: the inverse of the node's time map,
|
||||
`local = rate·(parent − at)`, so `parent = at + local/rate`.
|
||||
|
||||
WHAT IT DOES NOT DO YET: a looping instance repeats its symbol, and only the
|
||||
first pass is drawn. An expanded loop therefore shows keys where they first
|
||||
|
|
@ -37,28 +35,22 @@
|
|||
;; the rows
|
||||
|
||||
(defn- local->parent
|
||||
"The inverse of `node/local-frame`: where a frame of this node's OWN time sits
|
||||
in the symbol it lives in.
|
||||
"The inverse of a node's time map: where a frame of its OWN time sits in the
|
||||
symbol it lives in. See `node/time-of`.
|
||||
|
||||
Two frame spaces meet at every node and mixing them up is the bug this exists
|
||||
to prevent. A node's `:span` is checked against the frame its PARENT hands it,
|
||||
but its channels are read at `local-frame` of that — so a placement `:at 48`
|
||||
whose scale is keyed at 0 has that key on stage frame 48, and drawing it at 0
|
||||
puts every placement's keys in the same place however staggered they are.
|
||||
to prevent: a placement `:at 48` whose scale is keyed at 0 has that key on
|
||||
frame 48 of the symbol it is in, and drawing it at 0 puts every placement's
|
||||
keys in the same place however staggered they are.
|
||||
|
||||
Exposure is not inverted, because it is a floor and has no inverse: a key on a
|
||||
frame the exposure grid never samples is still authored on that frame, and that
|
||||
is where the row should show it."
|
||||
[n]
|
||||
(let [{:keys [mode offset at rate] :or {mode :inherit at 0 rate 1}} (:time n)
|
||||
in (or (first (:span n)) 0)]
|
||||
(if (= mode :inherit)
|
||||
(let [{:keys [at rate]} (node/time-of n)]
|
||||
(if (and (zero? at) (= 1 rate))
|
||||
identity
|
||||
(fn [f]
|
||||
(let [f (- f (or offset 0))]
|
||||
(if (and (node/timed-kinds (:kind n)) (not (zero? rate)))
|
||||
(js/Math.round (+ at (/ (- f in) rate)))
|
||||
f))))))
|
||||
(fn [f] (js/Math.round (+ at (/ f rate)))))))
|
||||
|
||||
(defn- keyed-frames [ch] (some-> (:keys ch) keys sort))
|
||||
|
||||
|
|
|
|||
|
|
@ -257,9 +257,9 @@
|
|||
(deftest an-instance-span-is-in-its-own-frames
|
||||
(let [n {:id :i :kind :instance :of :x :z "a1" :span [3 13]
|
||||
:time {:mode :map :at 40 :rate 2}}]
|
||||
(is (= [40 45] (node/placed-span n)) "ten own frames at double rate is five")
|
||||
(is (= 3 (node/local-frame n 40)) "the parent's :at is the span's first frame")
|
||||
(is (= 11 (node/local-frame n 44)))
|
||||
(is (= [41.5 46.5] (node/placed-span n)) "own frames 3 to 13, at double rate, from 40")
|
||||
(is (= 0 (node/local-frame n 40)) "the parent's :at is where its own frame 0 lands")
|
||||
(is (= 8 (node/local-frame n 44)))
|
||||
(is (= [2 9] (node/placed-span {:kind :poly :span [2 9]}))
|
||||
"a shape has no time of its own, so its span is already the parent's")
|
||||
(is (seq (node/problems (assoc-in n [:time :in] 3)))
|
||||
|
|
@ -311,7 +311,7 @@
|
|||
|
||||
(deftest a-placed-symbols-sound-is-heard-where-it-is-placed
|
||||
(let [voice {:id :v :kind :audio :source {:footage "f"} :z "a1"
|
||||
:span [10 40] :time {:mode :map :at 0 :rate 1}
|
||||
:span [10 40] :time {:mode :map :at -10 :rate 1}
|
||||
:channels {[:audio :gain] (ch/keyed {0 0.0 5 1.0})}}
|
||||
c (-> (clip/blank)
|
||||
(assoc-in [:symbols :talk] {:id :talk :frames 30 :nodes {:v voice}})
|
||||
|
|
@ -351,3 +351,95 @@
|
|||
(is (= [55 35] (clip/center grown nil :box)) "the symbol's middle moved")
|
||||
(is (= [25 35] (get-in grown [:symbols :main :nodes u :channels [:xform :anchor] :value]))
|
||||
"the instance's did not")))))
|
||||
|
||||
(defn- picture
|
||||
"What `sid` draws at each of `fs`, without the node paths a move changes:
|
||||
per frame, the sorted marks with their points rounded to a thousandth."
|
||||
[c sid fs]
|
||||
(let [resolve (clip/resolver c nil pal/index-of sid)
|
||||
round #(/ (js/Math.round (* 1000 %)) 1000)]
|
||||
(mapv (fn [f]
|
||||
(sort-by str (map (fn [op]
|
||||
[(:color op) (mapv round (take (* 2 (:n op)) (array-seq (:pts op))))])
|
||||
(resolve f))))
|
||||
fs)))
|
||||
|
||||
(def ^:private a-uuid #uuid "00000000-0000-4000-8000-0000000000e1")
|
||||
(def ^:private b-uuid #uuid "00000000-0000-4000-8000-0000000000e2")
|
||||
|
||||
(defn- studio
|
||||
"`main` holds a keyed shape and a moved, turned, doubled instance of `box`,
|
||||
placed at frame 10; `box` holds a shape of its own."
|
||||
[]
|
||||
(let [tri (fn [id x keyed]
|
||||
{:id id :kind :poly :z "a1" :paint? true :span [4 60]
|
||||
:channels {[:geom :pts] (ch/keyed (into {} (map (fn [[f dx]] [f [x 10 (+ x dx) 10 x 40]])) keyed))
|
||||
[:style :color] (ch/framed :brow)}})]
|
||||
(-> (clip/blank)
|
||||
(assoc-in [:symbols :main :nodes :tri] (tri :tri 100 {4 20 30 40}))
|
||||
(assoc-in [:symbols :box] {:id :box :frames 50 :nodes {:inner (tri :inner 5 {0 10})}})
|
||||
(clip/place-symbol nil :main :box 10 a-uuid nil)
|
||||
(update-in [:symbols :main :nodes a-uuid :channels] merge
|
||||
{[:xform :pos] (ch/framed [30 20])
|
||||
[:xform :rot] (ch/framed 0.5)
|
||||
[:xform :scale] (ch/framed [2 2])}))))
|
||||
|
||||
(deftest moving-a-node-into-a-symbol-changes-nothing-on-screen
|
||||
;; From frame 10, where the instance starts: inside a symbol a node exists only
|
||||
;; while that symbol is on screen, so moving the shape in cuts off 4–9.
|
||||
(let [c (studio)
|
||||
fs [10 16 29 30 45 59 60 70]
|
||||
{moved :clip :as r} (clip/move-node c nil :main [:tri] [a-uuid] 16)]
|
||||
(is (nil? (:refused r)) (:refused r))
|
||||
(is (contains? (get-in moved [:symbols :box :nodes]) :tri) "it is in the symbol now")
|
||||
(is (not (contains? (get-in moved [:symbols :main :nodes]) :tri)) "and not beside it")
|
||||
(is (= [4 60] (get-in moved [:symbols :box :nodes :tri :span]))
|
||||
"its span and keys are its own and do not change")
|
||||
(is (= -10 (get-in moved [:symbols :box :nodes :tri :time :at]))
|
||||
"its time map takes up the ten frames the instance starts late")
|
||||
(is (= (picture c :main fs) (picture moved :main fs)) "and the picture is the same, frame for frame")
|
||||
(testing "moving it back out is also invisible"
|
||||
(let [{back :clip} (clip/move-node moved nil :main [a-uuid :tri] [] 16)]
|
||||
(is (= (picture c :main fs) (picture back :main fs)))))))
|
||||
|
||||
(deftest moving-an-instance-moves-only-its-at
|
||||
(let [c (-> (studio)
|
||||
(assoc-in [:symbols :holder] {:id :holder :frames 100 :nodes {}})
|
||||
(clip/place-symbol nil :main :holder 3 b-uuid nil))
|
||||
fs [0 10 16 40 59]
|
||||
{moved :clip :as r} (clip/move-node c nil :main [a-uuid] [b-uuid] 16)
|
||||
n (get-in moved [:symbols :holder :nodes a-uuid])]
|
||||
(is (nil? (:refused r)) (:refused r))
|
||||
(is (= 7 (get-in n [:time :at])) "placed at 10 in main is at 7 inside something placed at 3")
|
||||
(is (= [0 50] (:span n)) "its own frames do not move")
|
||||
(is (= (picture c :main fs) (picture moved :main fs)))))
|
||||
|
||||
(deftest grouping-makes-a-symbol-around-them-and-changes-nothing-on-screen
|
||||
(let [c (studio)
|
||||
fs [0 4 10 16 30 59 60]
|
||||
{grouped :clip :as r} (clip/group c nil :main [[:tri] [a-uuid]] :group-1 b-uuid 16)
|
||||
inst (get-in grouped [:symbols :main :nodes b-uuid])]
|
||||
(is (nil? (:refused r)) (:refused r))
|
||||
(is (= #{:tri a-uuid} (set (keys (get-in grouped [:symbols :group-1 :nodes])))))
|
||||
(is (= [b-uuid] (keys (get-in grouped [:symbols :main :nodes]))) "one instance where they were")
|
||||
(is (= 4 (get-in inst [:time :at])) "starting where the earliest of them starts")
|
||||
(is (= 56 (clip/frames grouped :group-1)) "and lasting until the last one ends")
|
||||
(is (= (picture c :main fs) (picture grouped :main fs)))
|
||||
(is (empty? (clip/problems grouped)))))
|
||||
|
||||
(deftest what-cannot-move-says-why
|
||||
(let [c (studio)]
|
||||
(is (:refused (clip/move-node c nil :main [a-uuid] [a-uuid] 16)) "not into itself")
|
||||
(is (:refused (clip/move-node c nil :main [:tri] [a-uuid] 2)) "not while the target is off screen")
|
||||
(let [roto (assoc-in c [:symbols :main :nodes :tri :channels [:geom :pts] :generated] {:by :roto})]
|
||||
(is (re-find #"generated" (:refused (clip/move-node roto nil :main [:tri] [a-uuid] 16)))))))
|
||||
|
||||
(deftest moving-into-a-retimed-instance-keeps-the-timing
|
||||
(let [c (assoc-in (studio) [:symbols :main :nodes a-uuid :time :rate] 2)
|
||||
fs [10 12 17 20 29 34]
|
||||
{moved :clip :as r} (clip/move-node c nil :main [:tri] [a-uuid] 16)]
|
||||
(is (nil? (:refused r)) (:refused r))
|
||||
(is (= {:at -20 :rate 0.5}
|
||||
(select-keys (get-in moved [:symbols :box :nodes :tri :time]) [:at :rate]))
|
||||
"half speed inside something at double speed, so it runs as it did")
|
||||
(is (= (picture c :main fs) (picture moved :main fs)))))
|
||||
|
|
|
|||
|
|
@ -132,16 +132,16 @@
|
|||
(is (= (vec (range 8)) (mapv #(node/local-frame {:id :x} %) (range 8))))
|
||||
(is (= (vec (range 8)) (mapv #(node/local-frame {:id :x :time {:mode :inherit}} %) (range 8)))))
|
||||
|
||||
(deftest a-retimed-instance-is-refused-rather-than-ignored
|
||||
;; :rate is a symbol instance's timing and symbols are out of scope. Silently
|
||||
;; dropping it would be a retimed blink playing at the wrong speed, which looks
|
||||
;; like a bad blink and not like a missing feature.
|
||||
(is (thrown-with-msg? ExceptionInfo #":rate"
|
||||
(node/local-frame {:id :x :time {:mode :map :rate 0.5}} 0)))
|
||||
(deftest every-node-has-the-same-time-map
|
||||
;; One rule, whatever the node is: `local = rate · (parent − at)`.
|
||||
(is (= 5 (node/local-frame {:id :x :time {:mode :map :rate 0.5}} 10)) "a shape, at half speed")
|
||||
(is (= 5 (node/local-frame {:id :x :kind :instance :time {:mode :map :rate 0.5}} 10)) "an instance, the same")
|
||||
(is (= 3 (node/local-frame {:id :x :time {:mode :map :at 7}} 10)) "and :at is where its frame 0 lands")
|
||||
(is (= 4 (node/local-frame {:id :x :time {:mode :map :expose 2 :rate 1.0}} 5))
|
||||
"rate 1.0 is the identity and is allowed, because it appears in the spec's example"))
|
||||
|
||||
;; ---- the shape ----
|
||||
"exposure still floors on its own clock")
|
||||
(let [m {:at 3 :rate 2}]
|
||||
(is (= {:at 0 :rate 1} (node/then-time m (node/invert-time m)))
|
||||
"a map followed by its inverse is nothing")))
|
||||
|
||||
(deftest transform-channels-default-to-the-identity
|
||||
(let [chs (node/channels {:id :x :kind :group})]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue