Symbols, not timelines; no symbol is special
Everything that holds nodes is a symbol (domain/timeline -> domain/symbol, :timelines -> :symbols) and a node that places one is :kind :instance. The reserved :main root is gone: which symbol is on screen is editor state ([:ui :open]), every domain function that needs a symbol is told which, and a document opens on the longest symbol nothing else places. Saved projects move to schema 2 through migration 0007, which rewrites leaf paths, instance kinds and the feature :symbol key; the client refuses a schema it does not read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
179770d7d4
commit
5dff490162
61 changed files with 1587 additions and 1431 deletions
|
|
@ -91,18 +91,15 @@
|
|||
(.setValueAtTime param (* factor v) (/ f fps)))))))
|
||||
|
||||
(defn tracks-of
|
||||
"The audio nodes of one of the clip's timelines.
|
||||
"The audio nodes of one of the clip's symbols. Any symbol may carry its own
|
||||
sound, and playback mixes the open one's."
|
||||
[document sid]
|
||||
(filter #(= :audio (:kind %)) (vals (:nodes (clip/symbol document sid)))))
|
||||
|
||||
A timeline parameter rather than always the root, because a symbol is a
|
||||
timeline and may carry its own sound. `:main` is the clip's own, which is what
|
||||
playback mixes."
|
||||
[document tid]
|
||||
(filter #(= :audio (:kind %)) (vals (:nodes (clip/timeline document tid)))))
|
||||
|
||||
(defn- render! [document tid sources store]
|
||||
(defn- render! [document sid sources store]
|
||||
(let [fps (:fps document)
|
||||
frames (:frames (clip/timeline document tid))
|
||||
tracks (tracks-of document tid)
|
||||
frames (:frames (clip/symbol document sid))
|
||||
tracks (tracks-of document sid)
|
||||
output (js/OfflineAudioContext.
|
||||
2 (js/Math.ceil (* (/ frames fps) 44100)) 44100)]
|
||||
(doseq [track tracks]
|
||||
|
|
@ -133,20 +130,20 @@
|
|||
(.startRendering output)))
|
||||
|
||||
(defn buffer!
|
||||
"Promise of the `AudioBuffer` one timeline's audio tracks mix down to, or nil
|
||||
"Promise of the `AudioBuffer` one symbol's audio tracks mix down to, or nil
|
||||
when it has none.
|
||||
|
||||
The raw product. `mix!` packages it as a WAV URL for the transport and
|
||||
`export/frames` packages it as WAV bytes in an archive; a muxer would take it as
|
||||
it is, which is why this is the function the others are written in terms of."
|
||||
([document tid] (buffer! document tid nil))
|
||||
([document tid store]
|
||||
(let [tracks (tracks-of document tid)]
|
||||
([document sid] (buffer! document sid nil))
|
||||
([document sid store]
|
||||
(let [tracks (tracks-of document sid)]
|
||||
(if (empty? tracks)
|
||||
(js/Promise.resolve nil)
|
||||
(-> (js/Promise.all
|
||||
(into-array (map source! (distinct (map #(get-in % [:source :footage]) tracks)))))
|
||||
(.then (fn [pairs] (render! document tid (into {} (array-seq pairs)) store))))))))
|
||||
(.then (fn [pairs] (render! document sid (into {} (array-seq pairs)) store))))))))
|
||||
|
||||
(defn decode!
|
||||
"Promise of the `AudioBuffer` behind a URL. What a clip whose audio is a plain
|
||||
|
|
@ -162,9 +159,9 @@
|
|||
(.decodeAudioData (js/OfflineAudioContext. 1 1 44100) bytes)))))
|
||||
|
||||
(defn mix!
|
||||
"Promise of a mixed WAV URL, or the original URL for a clip without audio
|
||||
tracks. Each track can be trimmed and faded independently of its linked picture."
|
||||
([document fallback-url] (mix! document fallback-url nil))
|
||||
([document fallback-url store]
|
||||
(-> (buffer! document clip/root-id store)
|
||||
(.then (fn [buffer] (if buffer (wav-url buffer) fallback-url))))))
|
||||
"Promise of a mixed WAV URL for symbol `sid`, or the original URL when it has
|
||||
no audio tracks. Each track can be trimmed and faded independently of its
|
||||
linked picture."
|
||||
[document sid fallback-url store]
|
||||
(-> (buffer! document sid store)
|
||||
(.then (fn [buffer] (if buffer (wav-url buffer) fallback-url)))))
|
||||
|
|
|
|||
|
|
@ -20,10 +20,8 @@
|
|||
|
||||
Read OFF the clip rather than written again beside it: copying a number by hand
|
||||
into this table is how it comes to disagree with the document it describes.
|
||||
`:frames` comes from the ROOT TIMELINE and `:fps` from the clip, which is the
|
||||
split `arthur.domain.clip` exists to make — a timeline is a frame space, a clip
|
||||
is a rate — and an earlier version of this docstring noted that they sat on one
|
||||
map \"only because there is one clip per scene today\". They do not any more."
|
||||
There is no `:frames` here, because a length belongs to a symbol and which
|
||||
symbol is open is the editor's state — see `events/playback/frames`."
|
||||
[label-key label clip store]
|
||||
(merge {:label label :clip clip :store store
|
||||
;; A static asset since step 9, and not the repo root's `audio.wav`.
|
||||
|
|
@ -32,8 +30,7 @@
|
|||
;; that the clock has something to run against with no footage ingested.
|
||||
:audio "/static/arthur/audio.wav"
|
||||
:cid (name label-key)
|
||||
:display-fps (:fps clip)
|
||||
:frames (domain-clip/frames clip)}
|
||||
:display-fps (:fps clip)}
|
||||
(select-keys clip [:fps :width :height])))
|
||||
|
||||
(def clips
|
||||
|
|
@ -72,7 +69,7 @@
|
|||
;; transform on a node, so nothing downstream of the freeze knows the frame
|
||||
;; size — and it is why ui/player no longer hardcodes 320x200.
|
||||
:clip (let [c (domain-clip/blank)]
|
||||
{:fps (:fps c) :frames (domain-clip/frames c)
|
||||
{:fps (:fps c)
|
||||
:width (:width c) :height (:height c)
|
||||
:audio nil :display-fps (:fps c)})
|
||||
|
||||
|
|
@ -106,11 +103,12 @@
|
|||
;; machinery that would share it.
|
||||
;; --- export ---
|
||||
;;
|
||||
;; The REQUEST and its progress, never the frames. Which timeline to write and
|
||||
;; The REQUEST and its progress, never the frames. Which symbol to write and
|
||||
;; at what integer zoom is authored state like anything else; the megabytes the
|
||||
;; render produces are handed straight to a download and never enter the db.
|
||||
;; `:isolate` is the placement to render alone, or nil for the whole timeline.
|
||||
:export {:timeline :main :isolate nil :zoom 4 :busy? false :done 0 :total 0
|
||||
;; `:isolate` is the placement to render alone, or nil for the whole symbol;
|
||||
;; `:symbol` nil means whichever symbol is open.
|
||||
:export {:symbol nil :isolate nil :zoom 4 :busy? false :done 0 :total 0
|
||||
:status nil}
|
||||
|
||||
:playback {:frame 0
|
||||
|
|
@ -134,20 +132,28 @@
|
|||
;; names, so a pane dispatches on it rather than on which of several
|
||||
;; "selected-x" keys happens to be non-nil:
|
||||
;;
|
||||
;; [:node <timeline> <node>] a shape or a placement
|
||||
;; [:timeline <id>] a timeline, root or library
|
||||
;; [:node <symbol> <node>] a shape or an instance
|
||||
;; [:symbol <id>] a symbol
|
||||
;; [:subject <id>] [:feature <id>] [:group <id>] a tracked object
|
||||
;;
|
||||
;; `:draft` is the polygon being clicked out, flat [x y x y …] as geometry is
|
||||
;; stored everywhere. `:expanded` holds timeline row PATHS — a path and not a
|
||||
;; node id, because one symbol placed twice is two rows that open separately.
|
||||
;;
|
||||
;; `:open` is the symbol on screen — the one the stage draws, the timeline
|
||||
;; lists, the transport plays and a new shape goes into — and `:tabs` the
|
||||
;; symbols open beside it. Editor state and not the document's, because no
|
||||
;; symbol is special to the document: which one you are looking at is a fact
|
||||
;; about you.
|
||||
;;
|
||||
;; `:knobs` holds a generated setting's value WHILE THE REGENERATION IS IN
|
||||
;; FLIGHT, keyed by [scope id knob]. Moving a slider dispatches a preview that
|
||||
;; re-freezes blocks asynchronously, so until it lands the clip still reports
|
||||
;; the old value — and a slider reading from the clip would spring back under
|
||||
;; the user's finger on every frame of the drag.
|
||||
:ui {:selection nil
|
||||
:ui {:open nil
|
||||
:tabs []
|
||||
:selection nil
|
||||
:tone :skin-base
|
||||
:tool nil
|
||||
:draft []
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
validates would not be the one that renders, and the model would be validated
|
||||
against a scene nobody ever looked at."
|
||||
(:require [arthur.domain.clip :as domain-clip]
|
||||
[arthur.domain.timeline :as timeline]
|
||||
[arthur.domain.symbol :as symbol]
|
||||
[cljs.reader :as reader]
|
||||
[shadow.resource :as rc]))
|
||||
|
||||
|
|
@ -14,15 +14,15 @@
|
|||
|
||||
(def clip (reader/read-string source))
|
||||
|
||||
(def timeline
|
||||
"The clip's root timeline: what an evaluator takes. `clip` is the document."
|
||||
(domain-clip/root clip))
|
||||
(def main
|
||||
"The scene's one symbol: what an evaluator takes. `clip` is the document."
|
||||
(domain-clip/symbol clip :main))
|
||||
|
||||
(def fps (:fps clip))
|
||||
(def frames (domain-clip/frames clip))
|
||||
(def frames (domain-clip/frames clip :main))
|
||||
|
||||
(defn ops-at
|
||||
"Draw ops for one frame, via the specification path. The page uses
|
||||
`timeline/resolver` instead; this is here for the REPL."
|
||||
`symbol/resolver` instead; this is here for the REPL."
|
||||
[f]
|
||||
(timeline/eval-frame timeline f))
|
||||
(symbol/eval-frame main f))
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@
|
|||
:width 320
|
||||
:height 200
|
||||
|
||||
:timelines
|
||||
:symbols
|
||||
{:main
|
||||
{:id :main
|
||||
:frames 229
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@
|
|||
(let [{:keys [name width height frames symbol instances audio scale]} layout
|
||||
default-anchor (or (:anchor layout)
|
||||
[(/ (:width source) 2) (/ (:height source) 2)])
|
||||
original (get-in source [:timelines :main])
|
||||
original (get-in source [:symbols :main])
|
||||
;; Authored id -> uuid, so the `:linked-to` in the EDN resolves to the
|
||||
;; identity the document uses. Built before either pass because the audio
|
||||
;; nodes refer to the instances.
|
||||
|
|
@ -49,7 +49,7 @@
|
|||
{:root {:id :root :name "stage" :kind :group :z "a1"}}
|
||||
(map (fn [{:keys [uuid name z span at in center anchor drift phase]}]
|
||||
(let [anchor (or anchor default-anchor)]
|
||||
[uuid {:id uuid :name name :kind :symbol :of symbol
|
||||
[uuid {:id uuid :name name :kind :instance :of symbol
|
||||
:parent :root :z z :span span
|
||||
:time {:mode :map :at at :in in :rate 1}
|
||||
:channels {[:xform :pos] (if drift
|
||||
|
|
@ -68,6 +68,6 @@
|
|||
pan (assoc [:audio :pan] pan))}])
|
||||
audio))]
|
||||
(assoc source :name name :width width :height height
|
||||
:timelines (assoc (:timelines source)
|
||||
:symbols (assoc (:symbols source)
|
||||
:main {:id :main :frames frames :nodes nodes}
|
||||
symbol (assoc original :id symbol)))))
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@
|
|||
:fps fps
|
||||
:width 320
|
||||
:height 200
|
||||
:timelines
|
||||
:symbols
|
||||
{:main
|
||||
{:id :main
|
||||
:frames frames
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
│
|
||||
FREEZE ──▶ channels on nodes
|
||||
│
|
||||
timeline/resolver ──▶ raster
|
||||
symbol/resolver ──▶ raster
|
||||
|
||||
— and the order of that diagram is the whole argument for the stage split. The
|
||||
anchor fit is knob-free. Conditioning smooths its four parameters. The rings are
|
||||
|
|
|
|||
|
|
@ -1,47 +1,43 @@
|
|||
(ns arthur.domain.clip
|
||||
"A CLIP: the unit of work, and a library of timelines.
|
||||
"A CLIP: the unit of work, and a library of symbols.
|
||||
|
||||
{:name \"take\"
|
||||
:fps 30
|
||||
:width 320 :height 200
|
||||
:analysis {...}
|
||||
:subjects {...} :features {...} :groups {...}
|
||||
:timelines {:main {:id :main :frames 229 :nodes {...}}}}
|
||||
:symbols {:main {:id :main :frames 229 :nodes {...}}}}
|
||||
|
||||
Every field here is a fact about the clip and NOT about a bag of nodes, which is
|
||||
the cut this namespace exists to make. Before it, one map carried both: `:fps`,
|
||||
the stage dimensions, the analysis record and the tracking identities sat beside
|
||||
`:nodes`, and `arthur.db` said of it — correctly — that they \"sit on the scene
|
||||
map only because there is one clip per scene today\". The cost of leaving them
|
||||
together was not untidiness. It was that a SYMBOL had nowhere to live: a library
|
||||
timeline is a bag of nodes with a frame space and nothing else, so under the old
|
||||
shape it would have had to be a clip with seven meaningless fields, or a second
|
||||
structure with the same `:nodes` key that every walk had to be taught about.
|
||||
`:nodes`. The cost of leaving them together was not untidiness. It was that a
|
||||
SYMBOL had nowhere to live: a symbol is a bag of nodes with a frame space and
|
||||
nothing else, so under the old shape it would have had to be a clip with seven
|
||||
meaningless fields.
|
||||
|
||||
Now there is one node-holding type — `arthur.domain.timeline` — and a clip holds
|
||||
a MAP of them. A `:kind :symbol` instance names a timeline in `:timelines`,
|
||||
and the clip resolver gives each placement its own reading heads.
|
||||
Now there is one node-holding type — `arthur.domain.symbol` — and a clip holds
|
||||
a MAP of them. A `:kind :instance` node places one symbol inside another, and
|
||||
the clip resolver gives each instance its own reading heads.
|
||||
|
||||
THE ROOT TIMELINE HAS A RESERVED ID, `:main`, rather than the clip carrying a
|
||||
pointer to it. A pointer is a field that can be wrong — it can name a timeline
|
||||
that is not there, and then every reader needs a fallback — where a reserved name
|
||||
can only be absent, which `problems` reports once. Flash reserves `_root` the
|
||||
same way and for the same reason. Nothing else about `:main` is special: it is an
|
||||
ordinary entry in the map, and a symbol is another one.
|
||||
NO SYMBOL IS SPECIAL. There is no reserved root and no pointer to one: which
|
||||
symbol is on screen is the editor's state, not the document's, and every
|
||||
function here that needs a symbol is told which. A new document has one symbol
|
||||
called `:main` because it has to be called something, and that is all the name
|
||||
means — it can be renamed, placed inside another symbol or deleted like any of
|
||||
them. `unplaced` answers the question a reserved root used to: which symbols
|
||||
nothing else places, and so which ones a person opening the document wants.
|
||||
|
||||
WHY :fps IS HERE AND :frames IS NOT. A rate is how fast the whole clip plays
|
||||
against its audio, and a nested timeline cannot have one of its own — retiming an
|
||||
against its audio, and a nested symbol cannot have one of its own — retiming an
|
||||
instance is `:rate` on its `:time` map, which is a factor and not a rate. A
|
||||
frame COUNT is a property of a frame space, so every timeline has its own."
|
||||
frame COUNT is a property of a frame space, so every symbol has its own."
|
||||
(:refer-clojure :exclude [symbol])
|
||||
(:require [arthur.domain.feature :as feature]
|
||||
[arthur.domain.node :as node]
|
||||
[arthur.domain.palette :as pal]
|
||||
[arthur.domain.pose :as pose]
|
||||
[arthur.domain.timeline :as timeline]))
|
||||
|
||||
(def ^:const root-id
|
||||
"The reserved id of the timeline a clip plays. See the namespace docstring."
|
||||
:main)
|
||||
[arthur.domain.symbol :as symbol]))
|
||||
|
||||
(def clip-keys
|
||||
"Every top-level field of a clip, and the reason `arthur.domain.leaf` refuses
|
||||
|
|
@ -50,38 +46,56 @@
|
|||
that loses something on every round trip, which is the one bug a persistence
|
||||
layer must not be able to have. Add the field here and to `leaf/leaves` and
|
||||
`leaf/clip` in the same commit."
|
||||
#{:name :fps :analysis :subjects :features :groups :width :height :timelines})
|
||||
#{:name :fps :analysis :subjects :features :groups :width :height :symbols})
|
||||
|
||||
(defn timeline
|
||||
"One of the clip's timelines, by id."
|
||||
[clip id]
|
||||
(get-in clip [:timelines id]))
|
||||
|
||||
(defn root
|
||||
"The timeline the clip plays."
|
||||
[clip]
|
||||
(timeline clip root-id))
|
||||
(defn symbol
|
||||
"One of the clip's symbols, by id."
|
||||
[clip sid]
|
||||
(get-in clip [:symbols sid]))
|
||||
|
||||
(defn frames
|
||||
"The clip's length, which is its root timeline's frame space and is not written
|
||||
down twice. Reading it off the root is what stops the two from disagreeing."
|
||||
"A symbol's length. Read off the symbol, never copied beside it."
|
||||
[clip sid]
|
||||
(:frames (symbol clip sid)))
|
||||
|
||||
(defn update-symbol
|
||||
"Apply f to one symbol in place."
|
||||
[clip sid f & args]
|
||||
(apply update-in clip [:symbols sid] f args))
|
||||
|
||||
(defn places
|
||||
"The ids of the symbols `sid` places, directly."
|
||||
[clip sid]
|
||||
(into #{} (keep (fn [n] (when (= :instance (:kind n)) (:of n))))
|
||||
(vals (:nodes (symbol clip sid)))))
|
||||
|
||||
(defn contains-symbol?
|
||||
"Whether `inner` is `outer` or is placed anywhere inside it. Placing `outer`
|
||||
into `inner` when this is true is a cycle."
|
||||
[clip outer inner]
|
||||
(let [seen (volatile! #{})]
|
||||
(letfn [(walk [sid]
|
||||
(or (= sid inner)
|
||||
(when-not (@seen sid)
|
||||
(vswap! seen conj sid)
|
||||
(some walk (places clip sid)))))]
|
||||
(boolean (walk outer)))))
|
||||
|
||||
(defn unplaced
|
||||
"The symbols no other symbol places, sorted by id. What to open when a
|
||||
document is opened."
|
||||
[clip]
|
||||
(:frames (root clip)))
|
||||
(let [placed (into #{} (mapcat #(places clip %)) (keys (:symbols clip)))]
|
||||
(vec (sort-by str (remove placed (keys (:symbols clip)))))))
|
||||
|
||||
(defn update-timeline
|
||||
"Apply f to one timeline in place."
|
||||
[clip id f & args]
|
||||
(apply update-in clip [:timelines id] f args))
|
||||
|
||||
(defn update-root [clip f & args]
|
||||
(apply update-timeline clip root-id f args))
|
||||
|
||||
(defn nodes
|
||||
"The root timeline's nodes. A convenience for the many callers that mean the
|
||||
root and would otherwise spell it out; anything that could mean a symbol says
|
||||
which timeline instead."
|
||||
(defn opens-on
|
||||
"The symbol a document opens on: the longest one nothing else places, ties
|
||||
broken by id. The symbol that contains everything else is the longest of the
|
||||
unplaced ones in every document made so far, and a reserved name is what this
|
||||
replaces."
|
||||
[clip]
|
||||
(:nodes (root clip)))
|
||||
(first (sort-by (fn [sid] [(- (or (frames clip sid) 0)) (str sid)])
|
||||
(unplaced clip))))
|
||||
|
||||
(def ^:const blank-frames
|
||||
"How long a new document is before anything says otherwise. Four seconds at 30,
|
||||
|
|
@ -89,9 +103,9 @@
|
|||
120)
|
||||
|
||||
(defn blank
|
||||
"A new, empty document.
|
||||
"A new, empty document: one empty symbol.
|
||||
|
||||
`:nodes` is empty rather than seeded with a layer, because an empty timeline is
|
||||
`:nodes` is empty rather than seeded with a layer, because an empty symbol is
|
||||
a true statement and a layer nobody asked for is one more thing to delete. The
|
||||
tracking maps are present and empty for the same reason `clip-keys` exists: a
|
||||
field that is sometimes absent is a field every reader needs a fallback for."
|
||||
|
|
@ -100,39 +114,39 @@
|
|||
:fps 30
|
||||
:width 320 :height 200
|
||||
:subjects {} :features {} :groups {}
|
||||
:timelines {root-id {:id root-id :frames blank-frames :nodes {}}}})
|
||||
:symbols {:main {:id :main :frames blank-frames :nodes {}}}})
|
||||
|
||||
(defn place-symbol
|
||||
"An instance of library timeline `tid`, on the root timeline, at `frame`.
|
||||
"An instance of symbol `sid`, inside symbol `into`, at `frame` of `into`.
|
||||
|
||||
THE UUID IS AN ARGUMENT. A placement's identity is the key it has in the node
|
||||
map — it is what `:linked-to`, an export target and a saved leaf all name — so
|
||||
generating one in here would make this function's result depend on when it was
|
||||
called, and this namespace is the pure one. `demo/stage_8625.edn` authors its
|
||||
placements' uuids by hand for the same reason, in more words.
|
||||
called, and this namespace is the pure one.
|
||||
|
||||
The instance's own time starts where it was dropped: `:at frame` with `:in 0`
|
||||
means local frame 0 of the symbol plays on `frame` of the stage, which is what
|
||||
dragging something onto a playhead is asking for. `:span` runs to the end of
|
||||
the root's frame space rather than to the symbol's length, because a symbol
|
||||
shorter than the space it is placed in should hold its last frame rather than
|
||||
disappear."
|
||||
[clip tid frame uuid [x y]]
|
||||
(let [target (timeline clip tid)
|
||||
end (frames clip)]
|
||||
(if (or (nil? target) (= root-id tid) (nil? frame) (neg? frame) (>= frame end))
|
||||
means local frame 0 of the symbol plays on `frame` of `into`, which is what
|
||||
dragging something onto a playhead is asking for.
|
||||
|
||||
Refused, returning the clip unchanged, when it would make a cycle: a symbol
|
||||
cannot be placed inside itself or inside anything it places."
|
||||
[clip into sid frame uuid [x y]]
|
||||
(let [target (symbol clip sid)
|
||||
end (frames clip into)]
|
||||
(if (or (nil? target) (nil? end) (nil? frame) (neg? frame) (>= frame end)
|
||||
(contains-symbol? clip sid into))
|
||||
clip
|
||||
(update-root
|
||||
clip assoc-in [:nodes uuid]
|
||||
(update-symbol
|
||||
clip into assoc-in [:nodes uuid]
|
||||
{:id uuid
|
||||
:name (name tid)
|
||||
:kind :symbol
|
||||
:of tid
|
||||
:name (name sid)
|
||||
:kind :instance
|
||||
:of sid
|
||||
:parent nil
|
||||
;; Lexicographic draw order, as `domain/paint` does it: a placement made
|
||||
;; later sits above one made earlier, and neither has to renumber.
|
||||
:z (str "z" (js/Date.now) "-" (name tid))
|
||||
:span [frame end]
|
||||
:z (str "z" (js/Date.now) "-" (name sid))
|
||||
:span [frame (min end (+ frame (:frames target)))]
|
||||
:time {:mode :map :at frame :in 0 :rate 1}
|
||||
:channels {[:xform :pos] {:animated? false :value [x y]}}}))))
|
||||
|
||||
|
|
@ -158,35 +172,30 @@
|
|||
op)))
|
||||
|
||||
(defn resolver
|
||||
"Resolve a clip, including each library timeline placed by a symbol instance.
|
||||
"Resolve symbol `sid` of a clip, including every symbol its instances place.
|
||||
|
||||
Each instance owns its own timeline resolver, so two offsets never share a
|
||||
Each instance owns its own symbol resolver, so two offsets never share a
|
||||
channel cursor or point buffer. The returned ops must be drawn before the next
|
||||
frame, as with timeline/resolver.
|
||||
frame, as with symbol/resolver.
|
||||
|
||||
`root` is which timeline to resolve AS the root, and it defaults to the clip's.
|
||||
Passing a symbol's id is the whole of \"render that symbol\": a library timeline
|
||||
and the clip's own are the same type, so a symbol resolves by being rooted
|
||||
rather than by a second code path — which is the return on collapsing the two
|
||||
into `domain/timeline`. Its frame space is its own `:frames`, and nested symbols
|
||||
inside it still resolve, because this is the function that knows how to do that."
|
||||
([clip store] (resolver clip store pal/index-of root-id))
|
||||
([clip store palette] (resolver clip store palette root-id))
|
||||
([clip store palette root] (resolver clip store palette root nil))
|
||||
([clip store palette root {:keys [picture-fps] :as opts}]
|
||||
(letfn [(build [tid chain pose-tracks]
|
||||
(when (some #{tid} chain)
|
||||
(throw (ex-info "symbol timeline cycle" {:chain (conj chain tid)})))
|
||||
(let [tl (or (timeline clip tid)
|
||||
(throw (ex-info "symbol names a missing timeline" {:timeline tid})))
|
||||
nodes (:nodes tl)
|
||||
rank (timeline/draw-rank nodes (timeline/order nodes))
|
||||
Any symbol can be resolved and none is the default: the frame space is the
|
||||
resolved symbol's own `:frames`, and nested instances inside it still resolve,
|
||||
because this is the function that knows how to do that."
|
||||
([clip store palette sid] (resolver clip store palette sid nil))
|
||||
([clip store palette sid {:keys [picture-fps] :as opts}]
|
||||
(letfn [(build [sid chain pose-tracks]
|
||||
(when (some #{sid} chain)
|
||||
(throw (ex-info "symbol cycle" {:chain (conj chain sid)})))
|
||||
(let [sym (or (symbol clip sid)
|
||||
(throw (ex-info "an instance names a missing symbol" {:symbol sid})))
|
||||
nodes (:nodes sym)
|
||||
rank (symbol/draw-rank nodes (symbol/order nodes))
|
||||
ids (sort-by rank (keys nodes))
|
||||
own (timeline/resolver tl store palette pose-tracks
|
||||
own (symbol/resolver sym store palette pose-tracks
|
||||
(assoc opts :source-fps (:fps clip)))
|
||||
children (into {}
|
||||
(for [[id n] nodes :when (= :symbol (:kind n))]
|
||||
[id (build (:of n) (conj chain tid)
|
||||
(for [[id n] nodes :when (= :instance (:kind n))]
|
||||
[id (build (:of n) (conj chain sid)
|
||||
(get-in n [:playback :tracks]))]))]
|
||||
(fn [f]
|
||||
(let [by-id (into {} (map (juxt :node identity)) (own f))]
|
||||
|
|
@ -194,10 +203,10 @@
|
|||
(mapcat
|
||||
(fn [id]
|
||||
(let [n (get nodes id)]
|
||||
(if (= :symbol (:kind n))
|
||||
(let [m (timeline/world-of own id)
|
||||
local (timeline/frame-of own id)
|
||||
target (timeline clip (:of n))
|
||||
(if (= :instance (:kind n))
|
||||
(let [m (symbol/world-of own id)
|
||||
local (symbol/frame-of own id)
|
||||
target (symbol clip (:of n))
|
||||
length (:frames target)
|
||||
frame (when (and m (number? local))
|
||||
(if (get-in n [:time :loop?])
|
||||
|
|
@ -208,7 +217,7 @@
|
|||
[]))
|
||||
(when-let [op (get by-id id)] [op]))))
|
||||
ids))))))]
|
||||
(build root [] nil))))
|
||||
(build sid [] nil))))
|
||||
|
||||
(defn problems
|
||||
"Human-readable reasons this clip will not evaluate or save."
|
||||
|
|
@ -217,28 +226,26 @@
|
|||
(concat
|
||||
(for [k (remove clip-keys (keys clip))]
|
||||
(str "clip has a field with no leaf to save it in: " (pr-str k)))
|
||||
(when-not (map? (:timelines clip))
|
||||
[":timelines must be a map of id -> timeline"])
|
||||
(when (and (map? (:timelines clip)) (nil? (root clip)))
|
||||
[(str "no " (pr-str root-id) " timeline — a clip plays the one with the reserved id")])
|
||||
(when-not (map? (:symbols clip))
|
||||
[":symbols must be a map of id -> symbol"])
|
||||
(when-not (or (nil? (:fps clip)) (and (number? (:fps clip)) (pos? (:fps clip))))
|
||||
[(str ":fps is " (pr-str (:fps clip)) " — a rate is a positive number")])
|
||||
(for [[id tl] (:timelines clip)
|
||||
:when (not= id (:id tl))]
|
||||
(str "timeline under key " (pr-str id) " has :id " (pr-str (:id tl))))
|
||||
(for [[id tl] (:timelines clip)
|
||||
p (timeline/problems tl)]
|
||||
(str "timeline " (pr-str id) ": " p))
|
||||
(for [[tid tl] (:timelines clip)
|
||||
[id n] (:nodes tl)
|
||||
:when (and (= :symbol (:kind n))
|
||||
(not (contains? (:timelines clip) (:of n))))]
|
||||
(str "timeline " (pr-str tid) " symbol " (pr-str id)
|
||||
" names missing timeline " (pr-str (:of n))))
|
||||
(for [[tid tl] (:timelines clip)
|
||||
[id n] (:nodes tl)
|
||||
:when (= :symbol (:kind n))
|
||||
:let [target (get-in clip [:timelines (:of n)])
|
||||
(for [[id sym] (:symbols clip)
|
||||
:when (not= id (:id sym))]
|
||||
(str "symbol under key " (pr-str id) " has :id " (pr-str (:id sym))))
|
||||
(for [[id sym] (:symbols clip)
|
||||
p (symbol/problems sym)]
|
||||
(str "symbol " (pr-str id) ": " p))
|
||||
(for [[sid sym] (:symbols clip)
|
||||
[id n] (:nodes sym)
|
||||
:when (and (= :instance (:kind n))
|
||||
(not (contains? (:symbols clip) (:of n))))]
|
||||
(str "symbol " (pr-str sid) " instance " (pr-str id)
|
||||
" names missing symbol " (pr-str (:of n))))
|
||||
(for [[sid sym] (:symbols clip)
|
||||
[id n] (:nodes sym)
|
||||
:when (= :instance (:kind n))
|
||||
:let [target (get-in clip [:symbols (:of n)])
|
||||
active (filter (fn [node]
|
||||
(some :pose-sampled? (vals (:channels node))))
|
||||
(vals (:nodes target)))
|
||||
|
|
@ -247,11 +254,11 @@
|
|||
(map #(vector :node (:id %)) active)))]
|
||||
p (pose/problems (get-in n [:playback :tracks])
|
||||
(:frames target) groups)]
|
||||
(str "timeline " (pr-str tid) " symbol " (pr-str id) ": " p))
|
||||
(for [[tid tl] (:timelines clip)
|
||||
[id n] (:nodes tl)
|
||||
(str "symbol " (pr-str sid) " instance " (pr-str id) ": " p))
|
||||
(for [[sid sym] (:symbols clip)
|
||||
[id n] (:nodes sym)
|
||||
:when (and (= :audio (:kind n)) (:linked-to n)
|
||||
(not (contains? (:nodes tl) (:linked-to n))))]
|
||||
(str "timeline " (pr-str tid) " audio " (pr-str id)
|
||||
(not (contains? (:nodes sym) (:linked-to n))))]
|
||||
(str "symbol " (pr-str sid) " audio " (pr-str id)
|
||||
" links to missing node " (pr-str (:linked-to n))))
|
||||
(feature/problems clip))))
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
(ns arthur.domain.feature
|
||||
"Tracked subjects, feature ownership, and eye-pair settings.
|
||||
Features name their timeline explicitly; node ids are local to that timeline."
|
||||
Features name their symbol explicitly; node ids are local to that symbol."
|
||||
(:require [arthur.domain.params :as params]))
|
||||
|
||||
(defn owned
|
||||
|
|
@ -44,14 +44,14 @@
|
|||
clip))
|
||||
|
||||
(defn problems
|
||||
"Check tracked identities and timeline-local node ownership."
|
||||
"Check tracked identities and symbol-local node ownership."
|
||||
[clip]
|
||||
(let [subjects (:subjects clip)
|
||||
features (:features clip)
|
||||
groups (:groups clip)
|
||||
memberships (mapcat (comp :members val) groups)
|
||||
node-owners (for [[_ f] features n (:nodes f)]
|
||||
[(:timeline f) n])]
|
||||
[(:symbol f) n])]
|
||||
(vec
|
||||
(concat
|
||||
(for [[id s] subjects :when (not= id (:id s))]
|
||||
|
|
@ -60,8 +60,8 @@
|
|||
:when (not (params/valid-settings? :subject (or (:params s) {})))]
|
||||
(str "subject " (pr-str id) " has invalid settings"))
|
||||
(for [[id _] subjects
|
||||
:when (not (seq (get-in clip [:timelines id :nodes :head :measured])))]
|
||||
(str "subject " (pr-str id) " has no measured head in its timeline"))
|
||||
:when (not (seq (get-in clip [:symbols id :nodes :head :measured])))]
|
||||
(str "subject " (pr-str id) " has no measured head in its symbol"))
|
||||
(for [[id f] features :when (not= id (:id f))]
|
||||
(str "feature " (pr-str id) " has a different :id"))
|
||||
(for [[id f] features :when (not (contains? subjects (:subject f)))]
|
||||
|
|
@ -72,10 +72,10 @@
|
|||
:when (not (params/valid-settings? (:area f) (or (:params f) {})))]
|
||||
(str "feature " (pr-str id) " has invalid settings for " (pr-str (:area f))))
|
||||
(for [[id f] features
|
||||
:when (not (contains? (:timelines clip) (:timeline f)))]
|
||||
(str "feature " (pr-str id) " names a missing timeline"))
|
||||
:when (not (contains? (:symbols clip) (:symbol f)))]
|
||||
(str "feature " (pr-str id) " names a missing symbol"))
|
||||
(for [[id f] features node-id (:nodes f)
|
||||
:let [owned-nodes (get-in clip [:timelines (:timeline f) :nodes])]
|
||||
:let [owned-nodes (get-in clip [:symbols (:symbol f) :nodes])]
|
||||
:when (not (contains? owned-nodes node-id))]
|
||||
(str "feature " (pr-str id) " refers to missing node " (pr-str node-id)))
|
||||
(for [[id n] (frequencies node-owners) :when (> n 1)]
|
||||
|
|
|
|||
|
|
@ -11,21 +11,21 @@
|
|||
clip/<cid>/timing fps
|
||||
clip/<cid>/stage width, height
|
||||
clip/<cid>/source the analysis record this came out of
|
||||
clip/<cid>/subject/<sid> a tracked subject and its params
|
||||
clip/<cid>/subject/<subj> a tracked subject and its params
|
||||
clip/<cid>/feature/<fid> one feature: area, nodes, params
|
||||
clip/<cid>/group/<gid> an eye pair and its shared params
|
||||
clip/<cid>/timeline/<tid> frames, and a palette one day
|
||||
clip/<cid>/timeline/<tid>/node/<nid> kind, parent, stencil, z, time
|
||||
clip/<cid>/timeline/<tid>/channel/<nid>/<prop>
|
||||
clip/<cid>/timeline/<tid>/measured/<nid> the channels a re-freeze owns
|
||||
clip/<cid>/symbol/<sid> frames, and a palette one day
|
||||
clip/<cid>/symbol/<sid>/node/<nid> kind, parent, stencil, z, time
|
||||
clip/<cid>/symbol/<sid>/channel/<nid>/<prop>
|
||||
clip/<cid>/symbol/<sid>/measured/<nid> the channels a re-freeze owns
|
||||
|
||||
WHY NODES SIT UNDER A TIMELINE. A clip holds a library of timelines. Its root
|
||||
and each symbol have their own nodes, so the timeline id is a path segment.
|
||||
The root is `main`, and a symbol's nodes use the same path shape.
|
||||
WHY NODES SIT UNDER A SYMBOL. A clip holds a library of symbols and each has
|
||||
its own nodes, so the symbol id is a path segment. No symbol has a reserved
|
||||
segment: `main` in a path is an id like any other.
|
||||
|
||||
`:frames` MOVED OFF `timing` onto the timeline. A timeline is a frame space and a
|
||||
`:frames` MOVED OFF `timing` onto the symbol. A symbol is a frame space and a
|
||||
clip is a rate, so `timing` holds `:fps` alone. Both used to be in one leaf, which
|
||||
is how a nested timeline's length would have had nowhere to go.
|
||||
is how a nested symbol's length would have had nowhere to go.
|
||||
|
||||
WHY THESE BOUNDARIES. Last-writer-wins only clobbers when its unit is too big,
|
||||
so the cut is chosen so that the things people do simultaneously land on
|
||||
|
|
@ -56,7 +56,7 @@
|
|||
it is one character rather than a scheme."
|
||||
(:require [arthur.domain.clip :as clip]
|
||||
[arthur.domain.sha256 :as sha]
|
||||
[arthur.domain.timeline :as timeline]
|
||||
[arthur.domain.symbol :as symbol]
|
||||
[clojure.string :as str]))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
|
@ -124,11 +124,11 @@
|
|||
(when (seq unknown)
|
||||
(throw (ex-info "the clip has a field with no leaf to save it in; see arthur.domain.clip/clip-keys"
|
||||
{:unknown (vec (sort-by str unknown))}))))
|
||||
(doseq [[id tl] (:timelines clip)]
|
||||
(let [unknown (remove timeline/timeline-keys (keys tl))]
|
||||
(doseq [[id sym] (:symbols clip)]
|
||||
(let [unknown (remove symbol/symbol-keys (keys sym))]
|
||||
(when (seq unknown)
|
||||
(throw (ex-info "a timeline has a field with no leaf to save it in; see arthur.domain.timeline/timeline-keys"
|
||||
{:timeline id :unknown (vec (sort-by str unknown))})))))
|
||||
(throw (ex-info "a symbol has a field with no leaf to save it in; see arthur.domain.symbol/symbol-keys"
|
||||
{:symbol id :unknown (vec (sort-by str unknown))})))))
|
||||
(let [at (fn [& parts] (str/join "/" (into ["clip" (segment cid)] parts)))
|
||||
some-leaf (fn [path v] (when (seq v) {path v}))]
|
||||
(apply merge
|
||||
|
|
@ -140,30 +140,30 @@
|
|||
(for [[id v] (:subjects clip)] {(at "subject" (segment id)) v})
|
||||
(for [[id v] (:features clip)] {(at "feature" (segment id)) v})
|
||||
(for [[id v] (:groups clip)] {(at "group" (segment id)) v})
|
||||
;; The timeline's own facts. `:id` is the path segment, so writing it
|
||||
;; The symbol's own facts. `:id` is the path segment, so writing it
|
||||
;; into the value as well would be the one field a rename could
|
||||
;; disagree with itself about; `clip` puts it back.
|
||||
(for [[tid tl] (:timelines clip)]
|
||||
{(at "timeline" (segment tid))
|
||||
(select-keys tl [:frames :palette])})
|
||||
(for [[tid tl] (:timelines clip)
|
||||
[id n] (:nodes tl)]
|
||||
{(at "timeline" (segment tid) "node" (segment id))
|
||||
(for [[sid sym] (:symbols clip)]
|
||||
{(at "symbol" (segment sid))
|
||||
(select-keys sym [:frames :palette])})
|
||||
(for [[sid sym] (:symbols clip)
|
||||
[id n] (:nodes sym)]
|
||||
{(at "symbol" (segment sid) "node" (segment id))
|
||||
(apply dissoc n node-channel-keys)})
|
||||
(for [[tid tl] (:timelines clip)
|
||||
[id n] (:nodes tl)
|
||||
(for [[sid sym] (:symbols clip)
|
||||
[id n] (:nodes sym)
|
||||
:when (seq (:measured n))]
|
||||
{(at "timeline" (segment tid) "measured" (segment id)) (:measured n)})
|
||||
(for [[tid tl] (:timelines clip)
|
||||
[id n] (:nodes tl)
|
||||
{(at "symbol" (segment sid) "measured" (segment id)) (:measured n)})
|
||||
(for [[sid sym] (:symbols clip)
|
||||
[id n] (:nodes sym)
|
||||
[prop ch] (:channels n)]
|
||||
{(at "timeline" (segment tid) "channel" (segment id) (prop->path prop)) ch})))))
|
||||
{(at "symbol" (segment sid) "channel" (segment id) (prop->path prop)) ch})))))
|
||||
|
||||
(defn clip
|
||||
"The inverse of `leaves`, for one clip. Paths belonging to another clip are
|
||||
ignored, so a project's whole leaf map can be handed straight in.
|
||||
|
||||
A timeline's `:id` is restored from its path segment rather than read out of the
|
||||
A symbol's `:id` is restored from its path segment rather than read out of the
|
||||
value, which is why `leaves` does not write it: a segment and a field that both
|
||||
claim to be the id are two places for one fact."
|
||||
[cid leaves]
|
||||
|
|
@ -173,14 +173,14 @@
|
|||
(let [[_ found kind a b c] (str/split path #"/")]
|
||||
(if-not (= want found)
|
||||
acc
|
||||
(if (= "timeline" kind)
|
||||
(let [tid (unsegment a)
|
||||
acc (assoc-in acc [:timelines tid :id] tid)]
|
||||
(if (= "symbol" kind)
|
||||
(let [sid (unsegment a)
|
||||
acc (assoc-in acc [:symbols sid :id] sid)]
|
||||
(case b
|
||||
nil (update-in acc [:timelines tid] merge v)
|
||||
"node" (update-in acc [:timelines tid :nodes (unsegment c)] merge v)
|
||||
"measured" (assoc-in acc [:timelines tid :nodes (unsegment c) :measured] v)
|
||||
"channel" (assoc-in acc [:timelines tid :nodes (unsegment c)
|
||||
nil (update-in acc [:symbols sid] merge v)
|
||||
"node" (update-in acc [:symbols sid :nodes (unsegment c)] merge v)
|
||||
"measured" (assoc-in acc [:symbols sid :nodes (unsegment c) :measured] v)
|
||||
"channel" (assoc-in acc [:symbols sid :nodes (unsegment c)
|
||||
:channels (path->prop (nth (str/split path #"/") 6))]
|
||||
v)
|
||||
(throw (ex-info "not a leaf path" {:path path}))))
|
||||
|
|
@ -210,20 +210,20 @@
|
|||
content-addressed is that it does not have to travel with tier 1 to be found."
|
||||
[leaves]
|
||||
(let [parts (into {} (map (juxt identity #(vec (str/split % #"/")))) (keys leaves))
|
||||
;; A node leaf, by (clip, timeline, node). Under a timeline id, because a
|
||||
;; symbol and the root may both hold a `:mouth` and a channel of one is not
|
||||
;; a channel of the other.
|
||||
;; A node leaf, by (clip, symbol, node). Under a symbol id, because two
|
||||
;; symbols may both hold a `:mouth` and a channel of one is not a channel
|
||||
;; of the other.
|
||||
nodes (into #{} (keep (fn [[_ p]]
|
||||
(when (and (= 6 (count p)) (= "timeline" (nth p 2))
|
||||
(when (and (= 6 (count p)) (= "symbol" (nth p 2))
|
||||
(= "node" (nth p 4)))
|
||||
[(nth p 1) (nth p 3) (nth p 5)])))
|
||||
parts)
|
||||
;; Which segment index holds the kind, and what shapes are legal.
|
||||
legal? (fn [p]
|
||||
(and (= "clip" (first p)) (second p)
|
||||
(if (= "timeline" (nth p 2 nil))
|
||||
(if (= "symbol" (nth p 2 nil))
|
||||
(case (count p)
|
||||
4 true ; the timeline itself
|
||||
4 true ; the symbol itself
|
||||
6 (#{"node" "measured"} (nth p 4))
|
||||
7 (= "channel" (nth p 4))
|
||||
false)
|
||||
|
|
@ -238,13 +238,13 @@
|
|||
:when (not (legal? p))]
|
||||
(str (pr-str path) " is not a leaf path"))
|
||||
(for [[path p] (sort-by key parts)
|
||||
:when (and (legal? p) (= "timeline" (nth p 2 nil)) (>= (count p) 6)
|
||||
:when (and (legal? p) (= "symbol" (nth p 2 nil)) (>= (count p) 6)
|
||||
(#{"channel" "measured"} (nth p 4))
|
||||
(not (contains? nodes [(nth p 1) (nth p 3) (nth p 5)])))]
|
||||
(str (pr-str path) " addresses a node with no node leaf"))
|
||||
(for [[path p] (sort-by key parts)
|
||||
:let [v (get leaves path)]
|
||||
:when (and (legal? p) (= "timeline" (nth p 2 nil)) (= 7 (count p))
|
||||
:when (and (legal? p) (= "symbol" (nth p 2 nil)) (= 7 (count p))
|
||||
(:dense v) (not (sha/key? (:store (:dense v)))))]
|
||||
(str (pr-str path) " names tier 2 as " (pr-str (:store (:dense v)))
|
||||
" — a dense channel in a saved document names a content address"))))))
|
||||
|
|
|
|||
|
|
@ -21,9 +21,9 @@
|
|||
"`:bitmap` is in the vocabulary and not implemented; it is
|
||||
here so that a scene that names one fails as \"not implemented\" rather than as
|
||||
\"not a kind\"."
|
||||
#{:poly :disc :rect :group :bitmap :symbol :audio})
|
||||
#{:poly :disc :rect :group :bitmap :instance :audio})
|
||||
|
||||
(def implemented-kinds #{:poly :disc :rect :group :symbol :audio})
|
||||
(def implemented-kinds #{:poly :disc :rect :group :instance :audio})
|
||||
|
||||
(def xform-paths
|
||||
"In composition order, which is also the order they have to be sampled in.
|
||||
|
|
@ -45,7 +45,7 @@
|
|||
change to this spec silently change what gets drawn."
|
||||
(let [base (into #{[:vis]} xform-paths)]
|
||||
{:group base
|
||||
:symbol base
|
||||
:instance base
|
||||
:audio (into base [[:audio :gain] [:audio :pan] [:audio :rate]])
|
||||
:poly (into base [[:geom :pts] [:style :color]])
|
||||
;; A disc's radius is framed in practice — iris size is a knob, not a
|
||||
|
|
@ -111,7 +111,7 @@
|
|||
it on most frames, so the lead slider reads as doing nothing at exposures above
|
||||
1, which is indistinguishable from the slider being unwired.
|
||||
|
||||
Composed along the parent chain, outermost first, by timeline/eval-frame. Two
|
||||
Composed along the parent chain, outermost first, by symbol/eval-frame. Two
|
||||
rules fall out and they are different rules: exposure INHERITS STRICTLY,
|
||||
because a head cutting on odd frames against a mouth cutting on even ones reads
|
||||
as two performances; offset is PER-NODE by design, because mouth lead applies
|
||||
|
|
@ -122,13 +122,13 @@
|
|||
(if (= mode :inherit)
|
||||
f
|
||||
(do
|
||||
(when (and (not (#{:symbol :audio} (:kind n))) rate (not= rate 1.0) (not= rate 1))
|
||||
(throw (ex-info "time map :rate belongs to a symbol or audio instance"
|
||||
(when (and (not (#{:instance :audio} (: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 (#{:symbol :audio} (:kind n))
|
||||
(cond-> (if (#{:instance :audio} (:kind n))
|
||||
(+ (or in 0) (* (or rate 1) (- f (or at 0))))
|
||||
f)
|
||||
sample-fps (sample-frame source-fps sample-fps)
|
||||
|
|
@ -265,10 +265,10 @@
|
|||
(not (contains? implemented-kinds k)))
|
||||
(conj (str ":kind " k " is in the vocabulary but not implemented"))
|
||||
|
||||
(and (= k :symbol) (nil? (:of n))) (conj "a symbol instance needs :of")
|
||||
(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 (#{:symbol :audio} k) (some? (get-in n [:time :rate]))
|
||||
(and (#{:instance :audio} k) (some? (get-in n [:time :rate]))
|
||||
(not (pos? (get-in n [:time :rate]))))
|
||||
(conj "an instance's :rate must be positive")
|
||||
(nil? (:z n)) (conj "no :z — draw order is authored per scene, not implied by the tree")
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
(ns arthur.domain.paint
|
||||
"Small authored polygon operations. Paint nodes read timeline frames directly;
|
||||
the roto root's exposure and picture sampling must not quantise a hand edit."
|
||||
"Small authored polygon operations, each on a named symbol. Paint nodes read
|
||||
their symbol's frames directly; a roto instance's exposure and picture sampling
|
||||
must not quantise a hand edit."
|
||||
(:require [arthur.domain.channel :as channel]))
|
||||
|
||||
(def geometry [:geom :pts])
|
||||
|
||||
(defn shapes [clip]
|
||||
(->> (get-in clip [:timelines :main :nodes])
|
||||
(defn shapes [clip sid]
|
||||
(->> (get-in clip [:symbols sid :nodes])
|
||||
(filter (fn [[_ node]] (:paint? node)))
|
||||
(sort-by (comp :z val))
|
||||
vec))
|
||||
|
|
@ -15,21 +16,21 @@
|
|||
(let [frames (sort (keys (:keys ch)))]
|
||||
(or (last (take-while #(<= % frame) frames)) (first frames))))
|
||||
|
||||
(defn new-shape [clip id frame points color]
|
||||
(let [end (get-in clip [:timelines :main :frames])
|
||||
(defn new-shape [clip sid id frame points color]
|
||||
(let [end (get-in clip [:symbols sid :frames])
|
||||
z (str "z" (js/Date.now) "-" (name id))]
|
||||
(if (and (<= 0 frame) (< frame end) (>= (count points) 6)
|
||||
(even? (count points)))
|
||||
(assoc-in clip [:timelines :main :nodes id]
|
||||
{:id id :name (str "shape " (inc (count (shapes clip))))
|
||||
(assoc-in clip [:symbols sid :nodes id]
|
||||
{:id id :name (str "shape " (inc (count (shapes clip sid))))
|
||||
:kind :poly :paint? true :parent nil :z z
|
||||
:span [frame end]
|
||||
:channels {geometry (channel/keyed {frame points})
|
||||
[:style :color] (channel/framed color)}})
|
||||
clip)))
|
||||
|
||||
(defn add-key [clip id frame]
|
||||
(let [path [:timelines :main :nodes id]
|
||||
(defn add-key [clip sid id frame]
|
||||
(let [path [:symbols sid :nodes id]
|
||||
node (get-in clip path)
|
||||
ch (get-in node [:channels geometry])
|
||||
[start end] (:span node)]
|
||||
|
|
@ -38,20 +39,20 @@
|
|||
(vec (channel/value-at ch frame)))
|
||||
clip)))
|
||||
|
||||
(defn set-vertex [clip id key-frame vertex [x y]]
|
||||
(let [path [:timelines :main :nodes id :channels geometry :keys key-frame]
|
||||
(defn set-vertex [clip sid id key-frame vertex [x y]]
|
||||
(let [path [:symbols sid :nodes id :channels geometry :keys key-frame]
|
||||
points (get-in clip path)
|
||||
i (* 2 vertex)]
|
||||
(if (and points (< (inc i) (count points)))
|
||||
(assoc-in clip path (-> points (assoc i x) (assoc (inc i) y)))
|
||||
clip)))
|
||||
|
||||
(defn set-segment-interp [clip id key-frame interp]
|
||||
(let [node (get-in clip [:timelines :main :nodes id])
|
||||
(defn set-segment-interp [clip sid id key-frame interp]
|
||||
(let [node (get-in clip [:symbols sid :nodes id])
|
||||
keys (get-in node [:channels geometry :keys])]
|
||||
(if (and (:paint? node) (contains? keys key-frame)
|
||||
(some #(< key-frame %) (clojure.core/keys keys))
|
||||
(#{:hold :linear} interp))
|
||||
(assoc-in clip [:timelines :main :nodes id :channels geometry
|
||||
(assoc-in clip [:symbols sid :nodes id :channels geometry
|
||||
:segments key-frame] interp)
|
||||
clip)))
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@
|
|||
(defn encoder
|
||||
"(fn [raster ramp] -> promise of PNG bytes), for one stage size and one zoom.
|
||||
|
||||
Built once per export rather than per frame, in the shape `timeline/resolver`
|
||||
Built once per export rather than per frame, in the shape `symbol/resolver`
|
||||
already uses: everything that does not change frame to frame is held here. What
|
||||
that buys is the scanline scratch, which at zoom 6 is seven megabytes — a
|
||||
per-frame allocation of that size is the one thing that would make a long export
|
||||
|
|
|
|||
|
|
@ -32,16 +32,17 @@
|
|||
default-frame))
|
||||
|
||||
(defn put-cut
|
||||
"Set one held pose on a symbol instance. Earlier motion stays untouched."
|
||||
[clip instance group at source]
|
||||
(let [node (get-in clip [:timelines :main :nodes instance])
|
||||
symbol (get-in clip [:timelines (:of node)])
|
||||
length (:frames symbol)
|
||||
"Set one held pose on an instance inside symbol `sid`. Earlier motion stays
|
||||
untouched."
|
||||
[clip sid instance group at source]
|
||||
(let [node (get-in clip [:symbols sid :nodes instance])
|
||||
placed (get-in clip [:symbols (:of node)])
|
||||
length (:frames placed)
|
||||
active (filter (fn [n] (some :pose-sampled? (vals (:channels n))))
|
||||
(vals (:nodes symbol)))
|
||||
(vals (:nodes placed)))
|
||||
groups (set (map #(or (:pose-group %) (:id %)) active))
|
||||
ids (set (map :id active))]
|
||||
(when-not (and (= :symbol (:kind node))
|
||||
(when-not (and (= :instance (:kind node))
|
||||
(or (contains? groups group)
|
||||
(and (vector? group) (= 2 (count group))
|
||||
(= :node (first group))
|
||||
|
|
@ -50,17 +51,17 @@
|
|||
(integer? source) (<= 0 source) (< source length))
|
||||
(throw (ex-info "invalid stage pose cut"
|
||||
{:instance instance :group group :at at :source source})))
|
||||
(update-in clip [:timelines :main :nodes instance :playback :tracks group]
|
||||
(update-in clip [:symbols sid :nodes instance :playback :tracks group]
|
||||
#(assoc (or % {}) at source))))
|
||||
|
||||
(defn remove-cut
|
||||
"Remove a cut; an empty track again follows the normal generated motion."
|
||||
[clip instance group at]
|
||||
(let [path [:timelines :main :nodes instance :playback :tracks group]]
|
||||
[clip sid instance group at]
|
||||
(let [path [:symbols sid :nodes instance :playback :tracks group]]
|
||||
(if-let [entries (get-in clip path)]
|
||||
(if-let [remaining (not-empty (dissoc entries at))]
|
||||
(assoc-in clip path remaining)
|
||||
(update-in clip [:timelines :main :nodes instance :playback :tracks]
|
||||
(update-in clip [:symbols sid :nodes instance :playback :tracks]
|
||||
dissoc group))
|
||||
clip)))
|
||||
|
||||
|
|
|
|||
|
|
@ -29,6 +29,12 @@
|
|||
(:require [arthur.domain.leaf :as leaf]
|
||||
[arthur.domain.wire :as wire]))
|
||||
|
||||
(def schema-version
|
||||
"The stored document format this client reads and writes. 2 is symbols: leaf
|
||||
paths say `symbol`, a placing node is `:kind :instance`, and no symbol id is
|
||||
reserved. `clips/migrations/0007` moved every saved project from 1."
|
||||
2)
|
||||
|
||||
(defn block-keys
|
||||
"Every tier-2 key a leaf map names, in a stable order."
|
||||
[leaves]
|
||||
|
|
@ -53,8 +59,8 @@
|
|||
round-trip a clip through `JSON.parse(JSON.stringify(...))` and be running the
|
||||
same conversion the network runs, rather than a CLJS-shaped rehearsal of it. The
|
||||
one thing a keywordising `js->clj` would quietly break is the leaf paths —
|
||||
`:clip/c1/timeline/main/node/mouth` is a keyword whose `name` is
|
||||
\"c1/timeline/main/node/mouth\", so the
|
||||
`:clip/c1/symbol/main/node/mouth` is a keyword whose `name` is
|
||||
\"c1/symbol/main/node/mouth\", so the
|
||||
\"clip/\" would be lost on the way back in.
|
||||
|
||||
Refuses a document `domain/leaf` calls unaddressable, which is where a hand-made
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@
|
|||
edge landing exactly on a pixel boundary resolves consistently.
|
||||
|
||||
Flat and preallocated because this is the per-frame path: fixed topology means
|
||||
a node's vertex count is known at freeze time, so timeline/resolver hands the same
|
||||
a node's vertex count is known at freeze time, so symbol/resolver hands the same
|
||||
buffer back every frame and a frame allocates nothing. At 30fps per-frame
|
||||
allocation is the only thing that will make this stutter.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,38 +1,36 @@
|
|||
(ns arthur.domain.timeline
|
||||
"A TIMELINE: an ordered bag of nodes in its own frame space, and the two ways to
|
||||
(ns arthur.domain.symbol
|
||||
"A SYMBOL: an ordered bag of nodes in its own frame space, and the two ways to
|
||||
evaluate it at a frame.
|
||||
|
||||
{:id :main :frames 229 :nodes {id -> node} :palette nil}
|
||||
|
||||
That is the whole type, and EVERYTHING THAT HOLDS NODES IS ONE OF THESE. A
|
||||
clip's root timeline is one; a symbol in the library is one; a `:kind :symbol`
|
||||
node is an INSTANCE of one. An earlier arrangement had the clip's node tree and
|
||||
a library symbol as two structures with the same fields and never said they were
|
||||
the same thing — the clip map carried `:fps`, `:width`, `:height`, `:analysis`
|
||||
and the tracking identities alongside `:nodes`, so a symbol had nowhere to live
|
||||
that was not a clip with seven meaningless fields. Flash's `_root` is a
|
||||
MovieClip and After Effects' pre-comp is just a layer; collapsing them is what
|
||||
makes nesting arbitrary and free rather than a feature to be added.
|
||||
That is the whole type, and EVERYTHING THAT HOLDS NODES IS ONE OF THESE. What
|
||||
a document opens on is a symbol; what a `:kind :instance` node places is a
|
||||
symbol; there is no second structure. An earlier arrangement had a root node
|
||||
tree and a library entry as two structures with the same fields and never said
|
||||
they were the same thing. Flash's `_root` is a MovieClip and After Effects'
|
||||
pre-comp is just a layer; collapsing them is what makes nesting arbitrary and
|
||||
free rather than a feature to be added.
|
||||
|
||||
The clip-level facts are in `arthur.domain.clip`. A timeline has a FRAME SPACE,
|
||||
The clip-level facts are in `arthur.domain.clip`. A symbol has a FRAME SPACE,
|
||||
not a rate and not a size: `:fps` is the clip's, because a rate is a fact about
|
||||
how fast the whole thing plays, and a nested timeline cannot have its own.
|
||||
how fast the whole thing plays, and a nested symbol cannot have its own.
|
||||
|
||||
TWO AXES OF NESTING, and conflating them is why \"nested\" and \"flat with parent
|
||||
pointers\" sound contradictory when they are not. Parent/child is transform
|
||||
composition WITHIN one timeline and is stored flat with pointers. Instance is a
|
||||
timeline inside another timeline and is stored by reference into the library.
|
||||
Each timeline is flat; timelines nest. Every argument for flat storage —
|
||||
composition WITHIN one symbol and is stored flat with pointers. Instance is a
|
||||
symbol inside another symbol and is stored by reference into the library.
|
||||
Each symbol is flat; symbols nest. Every argument for flat storage —
|
||||
addressability, one-field reparenting, structural sharing, per-node sync leaves —
|
||||
is about the first axis and is untouched by the second.
|
||||
|
||||
Two ways to evaluate one at a frame:
|
||||
|
||||
(eval-frame tl f store) THE SPECIFICATION. Allocating, order-free,
|
||||
(eval-frame sym f store) THE SPECIFICATION. Allocating, order-free,
|
||||
obviously correct. Use it in tests and for a
|
||||
one-off render.
|
||||
|
||||
(resolver tl store) -> (fn [f] ops). What playback uses. Caches the
|
||||
(resolver sym store) -> (fn [f] ops). What playback uses. Caches the
|
||||
topological order and the z paths, holds one
|
||||
CURSOR per channel and one PREALLOCATED point
|
||||
buffer per node, so a frame allocates the op
|
||||
|
|
@ -42,7 +40,7 @@
|
|||
read and where points are written. That is deliberate: two independent
|
||||
implementations of frame evaluation would drift, and the drift would look like
|
||||
a rendering bug rather than like two functions disagreeing. What differs
|
||||
between them is exactly the part that can be wrong, and timeline-test asserts
|
||||
between them is exactly the part that can be wrong, and symbol-test asserts
|
||||
they agree frame for frame in forward, backward and random order.
|
||||
|
||||
The output is a list of DRAW OPS, and it is the boundary with the rasteriser:
|
||||
|
|
@ -76,12 +74,12 @@
|
|||
(when-let [p (:parent (get nodes i))]
|
||||
(if (contains? nodes p)
|
||||
p
|
||||
(throw (ex-info "node's :parent is not in the timeline"
|
||||
(throw (ex-info "node's :parent is not in the symbol"
|
||||
{:node i :parent p})))))
|
||||
chain (into [] (comp (take-while some?) (take (inc (count nodes))))
|
||||
(iterate up id))]
|
||||
(when (> (count chain) (count nodes))
|
||||
(throw (ex-info "parent cycle in timeline" {:node id :chain chain})))
|
||||
(throw (ex-info "parent cycle in symbol" {:node id :chain chain})))
|
||||
chain))
|
||||
|
||||
(defn depth
|
||||
|
|
@ -129,9 +127,9 @@
|
|||
"id -> its position in draw order.
|
||||
|
||||
Computed ONCE. Draw order is a function of the z paths, which are structural —
|
||||
they change when the timeline changes and never because the playhead moved — so
|
||||
they change when the symbol changes and never because the playhead moved — so
|
||||
sorting ops by z on every frame was re-deriving a constant thirty times a
|
||||
second. Here it is derived when the timeline is, and a frame sorts small integers.
|
||||
second. Here it is derived when the symbol is, and a frame sorts small integers.
|
||||
|
||||
`sort-by` is stable and `ord` is topological, so nodes sharing a z path keep
|
||||
parent-before-child order without a tiebreak field on every op."
|
||||
|
|
@ -146,9 +144,9 @@
|
|||
"Tone keyword -> the index the raster writes, in a given palette.
|
||||
|
||||
`palette` is a map of tone -> index. It is a PARAMETER, not a global: a tone
|
||||
names which mark this is, and which ramp it is read in belongs to the timeline
|
||||
names which mark this is, and which ramp it is read in belongs to the symbol
|
||||
the node sits in, so resolution cannot reach for one ambient answer. Today
|
||||
there is one palette and it is passed in anyway; when timelines carry a
|
||||
there is one palette and it is passed in anyway; when symbols carry a
|
||||
`:palette` channel, the walk carries the palette in scope exactly as it already
|
||||
carries the parent transform and the local frame.
|
||||
|
||||
|
|
@ -229,7 +227,7 @@
|
|||
flow/freeze writes it KEYED, because a threshold crossing is a handful of
|
||||
transitions and hold is the default, and because a human has to be able to fix
|
||||
one frame of it. When something does want a dense one it will land here loudly
|
||||
instead of blanking the timeline.
|
||||
instead of blanking the symbol.
|
||||
|
||||
Absence is not a boolean and is not an error: a subject that is not on the
|
||||
frame has nothing to show."
|
||||
|
|
@ -271,13 +269,13 @@
|
|||
:rd rd})))))))
|
||||
|
||||
(defn- emit
|
||||
"Emit geometry in the timeline's space. Rect sizes stay fractional until
|
||||
"Emit geometry in the symbol's space. Rect sizes stay fractional until
|
||||
rasterization, so enclosing symbol transforms can still scale them."
|
||||
[{:keys [palette buf-for]} n {:keys [m rd]} base]
|
||||
(let [colour #(colour-index palette (rd [:style :color]))]
|
||||
(case (:kind n)
|
||||
:group nil
|
||||
:symbol nil
|
||||
:instance nil
|
||||
:audio nil
|
||||
|
||||
:poly
|
||||
|
|
@ -311,20 +309,20 @@
|
|||
{:node (:id n) :kind (:kind n)})))))
|
||||
|
||||
(defn- nodes-of
|
||||
"The timeline's node map, REFUSING a map that has none.
|
||||
"The symbol's node map, REFUSING a map that has none.
|
||||
|
||||
A clip and a timeline both have an `:id` and both are maps, so handing a CLIP to
|
||||
A clip and a symbol both have an `:id` and both are maps, so handing a CLIP to
|
||||
an evaluator is the one mistake this type split makes easy — and the result is
|
||||
not an error, it is `(:nodes clip)` being nil and a frame resolving to no ops at
|
||||
all. That reads as a black stage, or, in a benchmark, as \"0 nodes\" and a
|
||||
flattering number. It happened once while the split was being made, which is why
|
||||
this is a guard and not a comment."
|
||||
[tl]
|
||||
(let [nodes (:nodes tl)]
|
||||
[sym]
|
||||
(let [nodes (:nodes sym)]
|
||||
(when-not (map? nodes)
|
||||
(throw (ex-info (str "not a timeline: :nodes is " (pr-str nodes)
|
||||
" — a clip is not a timeline, its `:timelines` hold them")
|
||||
{:keys (vec (sort-by str (keys tl)))})))
|
||||
(throw (ex-info (str "not a symbol: :nodes is " (pr-str nodes)
|
||||
" — a clip is not a symbol, its `:symbols` hold them")
|
||||
{:keys (vec (sort-by str (keys sym)))})))
|
||||
nodes))
|
||||
|
||||
(defn- channel-frame
|
||||
|
|
@ -386,18 +384,18 @@
|
|||
;; the specification
|
||||
|
||||
(defn eval-frame
|
||||
"Timeline at frame f -> draw ops in z order. Pure, and allocates freely.
|
||||
"Symbol at frame f -> draw ops in z order. Pure, and allocates freely.
|
||||
|
||||
`f` is in THIS timeline's frame space. At the clip's root that is clip frames;
|
||||
inside an instance it is the instance's own space, and the instance boundary is
|
||||
`f` is in THIS symbol's frame space. For the symbol on screen that is the
|
||||
transport's frame; inside an instance it is the instance's own space, and the instance boundary is
|
||||
the only place the space changes.
|
||||
|
||||
This is the definition of what a frame means. `resolver` is what plays it."
|
||||
([tl f] (eval-frame tl f nil pal/index-of))
|
||||
([tl f store] (eval-frame tl f store pal/index-of))
|
||||
([tl f store palette] (eval-frame tl f store palette nil nil))
|
||||
([tl f store palette pose-tracks opts]
|
||||
(let [nodes (nodes-of tl)
|
||||
([sym f] (eval-frame sym f nil pal/index-of))
|
||||
([sym f store] (eval-frame sym f store pal/index-of))
|
||||
([sym f store palette] (eval-frame sym f store palette nil nil))
|
||||
([sym f store palette pose-tracks opts]
|
||||
(let [nodes (nodes-of sym)
|
||||
choices (pose/prepare pose-tracks)
|
||||
anchors (prepared-anchors nodes)
|
||||
{:keys [source-fps picture-fps]} opts
|
||||
|
|
@ -456,12 +454,12 @@
|
|||
The op maps themselves are allocated fresh, and deliberately: there are a dozen
|
||||
of them per frame against hundreds of points, so pooling them would buy
|
||||
nothing and cost the ability to hand an op list around as plain data."
|
||||
([tl] (resolver tl nil pal/index-of nil nil))
|
||||
([tl store] (resolver tl store pal/index-of nil nil))
|
||||
([tl store palette] (resolver tl store palette nil nil))
|
||||
([tl store palette pose-tracks] (resolver tl store palette pose-tracks nil))
|
||||
([tl store palette pose-tracks {:keys [source-fps picture-fps]}]
|
||||
(let [nodes (nodes-of tl)
|
||||
([sym] (resolver sym nil pal/index-of nil nil))
|
||||
([sym store] (resolver sym store pal/index-of nil nil))
|
||||
([sym store palette] (resolver sym store palette nil nil))
|
||||
([sym store palette pose-tracks] (resolver sym store palette pose-tracks nil))
|
||||
([sym store palette pose-tracks {:keys [source-fps picture-fps]}]
|
||||
(let [nodes (nodes-of sym)
|
||||
choices (pose/prepare pose-tracks)
|
||||
anchors (prepared-anchors nodes)
|
||||
ord (order nodes)
|
||||
|
|
@ -507,20 +505,20 @@
|
|||
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(def timeline-keys
|
||||
"Every field a timeline may carry, and the reason `arthur.domain.leaf` refuses
|
||||
(def symbol-keys
|
||||
"Every field a symbol may carry, and the reason `arthur.domain.leaf` refuses
|
||||
one it does not know: a field added without a leaf to save it in is a field that
|
||||
saves silently and comes back missing.
|
||||
|
||||
`:palette` is in the vocabulary and nothing writes one yet. A timeline is where
|
||||
a ramp belongs — `domain/timeline` takes the palette as a PARAMETER rather than
|
||||
reaching for a global precisely so that a nested timeline can carry its own —
|
||||
`:palette` is in the vocabulary and nothing writes one yet. A symbol is where
|
||||
a ramp belongs — `domain/symbol` takes the palette as a PARAMETER rather than
|
||||
reaching for a global precisely so that a nested symbol can carry its own —
|
||||
and leaving the field out would make the first one a migration instead of a
|
||||
write."
|
||||
#{:id :frames :nodes :palette})
|
||||
|
||||
(defn problems
|
||||
"Human-readable reasons this timeline will not evaluate. Empty means it will.
|
||||
"Human-readable reasons this symbol will not evaluate. Empty means it will.
|
||||
|
||||
Node structure only. The tracking identities — subjects, features, groups — are
|
||||
the CLIP's and are checked by `arthur.domain.clip/problems`, which is not a
|
||||
|
|
@ -530,8 +528,8 @@
|
|||
Total by construction — it reports a cycle rather than looping on one — because
|
||||
its whole job is to be safe to run over authored data before that data is
|
||||
trusted."
|
||||
[tl]
|
||||
(let [nodes (:nodes tl)]
|
||||
[sym]
|
||||
(let [nodes (:nodes sym)]
|
||||
(if-not (map? nodes)
|
||||
[":nodes must be a map of id -> node"]
|
||||
(-> []
|
||||
|
|
@ -541,11 +539,11 @@
|
|||
(into (for [[id n] nodes
|
||||
:when (and (:parent n) (not (contains? nodes (:parent n))))]
|
||||
(str "node " (pr-str id) " has :parent " (pr-str (:parent n))
|
||||
" which is not in the timeline")))
|
||||
" which is not in the symbol")))
|
||||
(into (for [[id n] nodes
|
||||
:when (and (:stencil n) (not (contains? nodes (:stencil n))))]
|
||||
(str "node " (pr-str id) " has :stencil " (pr-str (:stencil n))
|
||||
" which is not in the timeline")))
|
||||
" which is not in the symbol")))
|
||||
(into (for [[id n] nodes
|
||||
p (node/problems n)]
|
||||
(str "node " (pr-str id) ": " p)))
|
||||
|
|
@ -554,19 +552,19 @@
|
|||
:let [anchors (:anchors n)]
|
||||
:when (some? anchors)
|
||||
:when (not (and (map? anchors) (contains? anchors 0)
|
||||
(integer? (:frames tl))
|
||||
(integer? (:frames sym))
|
||||
(every? #(and (integer? %) (<= 0 %)
|
||||
(< % (:frames tl)))
|
||||
(< % (:frames sym)))
|
||||
(concat (keys anchors) (vals anchors)))
|
||||
(seq (:measured n))
|
||||
(= (:channels n) (:measured n))))]
|
||||
(str "node " (pr-str id)
|
||||
": :anchors must start at frame 0, name valid measured frames, and read that node's own measured channels")))
|
||||
(into (for [k (remove timeline-keys (keys tl))]
|
||||
(str "timeline has a field with no leaf to save it in: " (pr-str k))))
|
||||
(into (when-not (or (nil? (:frames tl)) (and (integer? (:frames tl)) (pos? (:frames tl))))
|
||||
[(str ":frames is " (pr-str (:frames tl))
|
||||
" — a timeline is a frame SPACE, so its length is a positive integer")]))
|
||||
(into (for [k (remove symbol-keys (keys sym))]
|
||||
(str "symbol has a field with no leaf to save it in: " (pr-str k))))
|
||||
(into (when-not (or (nil? (:frames sym)) (and (integer? (:frames sym)) (pos? (:frames sym))))
|
||||
[(str ":frames is " (pr-str (:frames sym))
|
||||
" — a symbol is a frame SPACE, so its length is a positive integer")]))
|
||||
(into (try
|
||||
(doall (map #(depth nodes %) (keys nodes)))
|
||||
nil
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
"Export, as intents and one effect.
|
||||
|
||||
The walk is not an event and must not become one: it is a promise chain that
|
||||
runs for as long as the timeline is long, and re-frame events are the wrong unit
|
||||
runs for as long as the symbol is long, and re-frame events are the wrong unit
|
||||
for something with a middle. So `::start` collects what the render needs out of
|
||||
the db and hands it to an fx, and the fx dispatches progress back — the same
|
||||
arrangement `events/project`'s save uses, and for the same reason.
|
||||
|
|
@ -44,84 +44,88 @@
|
|||
(defn target-value
|
||||
"An export target as a `<select>` option value.
|
||||
|
||||
Two kinds, told apart by a leading letter: `t:<timeline>` is a whole timeline,
|
||||
`n:<timeline>:<node>` is one placement inside one. The parts are joined with `:`
|
||||
because neither a timeline id nor a uuid contains one.
|
||||
Two kinds, told apart by a leading letter: `s:<symbol>` is a whole symbol,
|
||||
`n:<symbol>:<node>` is one instance inside one. The parts are joined with `:`
|
||||
because neither a symbol id nor a uuid contains one.
|
||||
|
||||
IT CARRIES THE NAMESPACE. `(name :sym/face-8625)` is \"face-8625\", and a value
|
||||
written that way cannot be read back: `keyword` on it gives `:face-8625`, which
|
||||
is not a key in `:timelines`, so the plan silently becomes nil and the export
|
||||
throws \"there is no such timeline\" from inside re-frame's `:do-fx`. That
|
||||
is not a key in `:symbols`, so the plan silently becomes nil and the export
|
||||
throws \"there is no such symbol\" from inside re-frame's `:do-fx`. That
|
||||
presented as the tab locking up rather than as an error — see `::run!` below for
|
||||
the other half of why — and it is the reason this is a named pair of functions
|
||||
with a test rather than `name` and `keyword` at the two ends of a select."
|
||||
[{:keys [timeline isolate]}]
|
||||
(let [tl (subs (str (or timeline :main)) 1)]
|
||||
(if isolate (str "n:" tl ":" isolate) (str "t:" tl))))
|
||||
[{sid :symbol isolate :isolate}]
|
||||
(let [s (subs (str sid) 1)]
|
||||
(if isolate (str "n:" s ":" isolate) (str "s:" s))))
|
||||
|
||||
(defn target-id
|
||||
"The inverse of `target-value`. `keyword` splits on the `/` itself, so a
|
||||
namespaced timeline id survives; a placement comes back a uuid, which is what
|
||||
namespaced symbol id survives; an instance comes back a uuid, which is what
|
||||
the node map is keyed by."
|
||||
[v]
|
||||
(let [[kind tl node] (str/split v #":")]
|
||||
(cond-> {:timeline (keyword tl)}
|
||||
(let [[kind s node] (str/split v #":")]
|
||||
(cond-> {:symbol (keyword s)}
|
||||
(= "n" kind) (assoc :isolate (uuid node)))))
|
||||
|
||||
(defn targets
|
||||
"Everything an export can be pointed at, in the order the picker lists them.
|
||||
|
||||
THREE KINDS, and the distinction is the point. `:main` is the clip. A symbol
|
||||
timeline is the DRAWING — one file however many times it is placed, in its own
|
||||
frame space. A placement is that drawing WHERE IT SITS: the stage's length and
|
||||
rate, with the other placements removed, which is why seven instances of one
|
||||
symbol are seven different exports rather than seven copies of one.
|
||||
TWO KINDS, and the distinction is the point. A symbol is the DRAWING — one file
|
||||
however many times it is placed, in its own frame space. An instance is that
|
||||
drawing WHERE IT SITS in the open symbol: that symbol's length and rate, with
|
||||
the other instances removed, which is why seven instances of one symbol are
|
||||
seven different exports rather than seven copies of one.
|
||||
|
||||
Placements are ordered and labelled by `:name`, never by id: a uuid sorts at
|
||||
Instances are ordered and labelled by `:name`, never by id: a uuid sorts at
|
||||
random and means nothing to read."
|
||||
[clip]
|
||||
(let [libs (cons :main (sort-by str (remove #{:main} (keys (:timelines clip)))))
|
||||
placements (->> (get-in clip [:timelines :main :nodes])
|
||||
(filter (comp #{:symbol} :kind val))
|
||||
(sort-by (fn [[id n]] [(or (:name n) "") (str id)])))]
|
||||
(into (mapv (fn [tid]
|
||||
{:timeline tid
|
||||
:label (if (= :main tid) "main (the clip)" (name tid))})
|
||||
libs)
|
||||
[clip open]
|
||||
(let [instances (->> (get-in clip [:symbols open :nodes])
|
||||
(filter (comp #{:instance} :kind val))
|
||||
(sort-by (fn [[id n]] [(or (:name n) "") (str id)])))]
|
||||
(into (mapv (fn [sid] {:symbol sid :label (name sid)})
|
||||
(sort-by str (keys (:symbols clip))))
|
||||
(mapv (fn [[id n]]
|
||||
{:timeline :main :isolate id
|
||||
{:symbol open :isolate id
|
||||
:label (or (:name n) (str id))})
|
||||
placements))))
|
||||
instances))))
|
||||
|
||||
(defn target
|
||||
"What the export is pointed at. A nil symbol is whichever one is open, so a
|
||||
new document exports what is on screen without anyone choosing."
|
||||
[db]
|
||||
(let [{sid :symbol isolate :isolate} (:export db)]
|
||||
{:symbol (or sid (get-in db [:ui :open])) :isolate isolate}))
|
||||
|
||||
(defn- label-of
|
||||
"The label of the target `db` currently points at, for the filename."
|
||||
[clip {:keys [timeline isolate]}]
|
||||
(:label (or (first (filter #(and (= timeline (:timeline %))
|
||||
(= isolate (:isolate %)))
|
||||
(targets clip)))
|
||||
{:label (some-> timeline name)})))
|
||||
[clip open {sid :symbol isolate :isolate}]
|
||||
(:label (or (first (filter #(and (= sid (:symbol %)) (= isolate (:isolate %)))
|
||||
(targets clip open)))
|
||||
{:label (some-> sid name)})))
|
||||
|
||||
(rf/reg-sub ::state (fn [db _] (:export db)))
|
||||
(rf/reg-sub ::state (fn [db _] (assoc (:export db) :target (target db))))
|
||||
|
||||
(rf/reg-sub
|
||||
::targets
|
||||
(fn [db _]
|
||||
(targets (:clip (store/entry (:clip/current db))))))
|
||||
(targets (:clip (store/entry (:clip/current db))) (get-in db [:ui :open]))))
|
||||
|
||||
(rf/reg-sub
|
||||
::plan
|
||||
(fn [db _]
|
||||
(let [{:keys [clip]} (store/entry (:clip/current db))
|
||||
{:keys [timeline zoom isolate]} (:export db)]
|
||||
(export/plan {:clip clip :timeline timeline :zoom zoom :isolate isolate
|
||||
{sid :symbol isolate :isolate} (target db)]
|
||||
(export/plan {:clip clip :symbol sid :zoom (get-in db [:export :zoom])
|
||||
:isolate isolate
|
||||
:picture-fps (get-in db [:clip :display-fps])}))))
|
||||
|
||||
(rf/reg-event-db
|
||||
::set-target
|
||||
;; Both keys always, so switching from a placement back to a whole timeline
|
||||
;; Both keys always, so switching from an instance back to a whole symbol
|
||||
;; clears the isolate rather than leaving it to filter the new target.
|
||||
(fn [db [_ {:keys [timeline isolate]}]]
|
||||
(update db :export merge {:timeline (or timeline :main) :isolate isolate})))
|
||||
(fn [db [_ {sid :symbol isolate :isolate}]]
|
||||
(update db :export merge {:symbol sid :isolate isolate})))
|
||||
|
||||
(rf/reg-event-db
|
||||
::set-zoom
|
||||
|
|
@ -134,16 +138,17 @@
|
|||
{}
|
||||
(let [id (:clip/current db)
|
||||
entry (store/entry id)
|
||||
{:keys [timeline zoom isolate]} (:export db)]
|
||||
{sid :symbol isolate :isolate} (target db)
|
||||
zoom (get-in db [:export :zoom])]
|
||||
{:db (update db :export merge {:busy? true :done 0
|
||||
:total (:frames (export/plan
|
||||
{:clip (:clip entry)
|
||||
:timeline timeline
|
||||
:symbol sid
|
||||
:isolate isolate
|
||||
:zoom zoom}))
|
||||
:status "rendering…"})
|
||||
::run! {:clip (:clip entry)
|
||||
:timeline timeline
|
||||
:symbol sid
|
||||
:isolate isolate
|
||||
:store (:store entry)
|
||||
;; The same palette and ramp the preview resolves and blits
|
||||
|
|
@ -156,8 +161,8 @@
|
|||
:picture-fps (get-in db [:clip :display-fps])
|
||||
:audio-url (:audio entry)
|
||||
:name (stem (:label entry)
|
||||
(label-of (:clip entry)
|
||||
{:timeline timeline :isolate isolate}))}}))))
|
||||
(label-of (:clip entry) (get-in db [:ui :open])
|
||||
{:symbol sid :isolate isolate}))}}))))
|
||||
|
||||
(rf/reg-event-db
|
||||
::progress
|
||||
|
|
@ -197,7 +202,7 @@
|
|||
::run!
|
||||
(fn [spec]
|
||||
;; THE CALL IS GUARDED because `export/run!` validates its request BEFORE it
|
||||
;; returns a promise, so a bad timeline id throws synchronously — here, inside
|
||||
;; returns a promise, so a bad symbol id throws synchronously — here, inside
|
||||
;; re-frame's `:do-fx` interceptor. An uncaught throw there never reaches the
|
||||
;; `.catch` below, so `::failed` never dispatches and `:busy?` stays true: the
|
||||
;; button sits disabled on \"rendering…\" and the readout on \"frame 0 /\"
|
||||
|
|
|
|||
|
|
@ -142,7 +142,6 @@
|
|||
source-blocks (source/pack-subjects (:id (:analysis built)) subjects)
|
||||
_ (mark! "build-clip: pack source blocks")]
|
||||
(assoc (select-keys built [:fps :width :height])
|
||||
:frames (clip/frames built)
|
||||
:display-fps (:fps built)
|
||||
:clip built :store (:store frozen)
|
||||
:source-blocks source-blocks
|
||||
|
|
@ -389,11 +388,7 @@
|
|||
::loaded
|
||||
(fn [{:keys [db]} [_ id summary]]
|
||||
(let [clip (store/entry id)]
|
||||
{:db (-> db
|
||||
(assoc :clip/current id
|
||||
:clip (select-keys clip [:fps :frames :width :height :audio :display-fps])
|
||||
:footage (assoc (:footage db) :id id :label (:label clip)
|
||||
:loading? false :status summary))
|
||||
(assoc-in [:playback :frame] 0)
|
||||
(assoc-in [:playback :playing?] false))
|
||||
{:db (-> (pb/show db id)
|
||||
(assoc :footage (assoc (:footage db) :id id :label (:label clip)
|
||||
:loading? false :status summary)))
|
||||
::pb/pause! nil})))
|
||||
|
|
|
|||
|
|
@ -7,20 +7,20 @@
|
|||
|
||||
(rf/reg-event-db
|
||||
::new-shape
|
||||
(fn [db [_ id points color]]
|
||||
(edit/edit db #(paint/new-shape % id (get-in db [:playback :frame]) points color))))
|
||||
(fn [db [_ sid id points color]]
|
||||
(edit/edit db #(paint/new-shape % sid id (get-in db [:playback :frame]) points color))))
|
||||
|
||||
(rf/reg-event-db
|
||||
::add-key
|
||||
(fn [db [_ id]]
|
||||
(edit/edit db #(paint/add-key % id (get-in db [:playback :frame])))))
|
||||
(fn [db [_ sid id]]
|
||||
(edit/edit db #(paint/add-key % sid id (get-in db [:playback :frame])))))
|
||||
|
||||
(rf/reg-event-db
|
||||
::set-vertex
|
||||
(fn [db [_ id key-frame vertex point]]
|
||||
(edit/edit db #(paint/set-vertex % id key-frame vertex point))))
|
||||
(fn [db [_ sid id key-frame vertex point]]
|
||||
(edit/edit db #(paint/set-vertex % sid id key-frame vertex point))))
|
||||
|
||||
(rf/reg-event-db
|
||||
::set-segment-interp
|
||||
(fn [db [_ id key-frame interp]]
|
||||
(edit/edit db #(paint/set-segment-interp % id key-frame interp))))
|
||||
(fn [db [_ sid id key-frame interp]]
|
||||
(edit/edit db #(paint/set-segment-interp % sid id key-frame interp))))
|
||||
|
|
|
|||
|
|
@ -11,11 +11,33 @@
|
|||
a small app-db. If global interceptors are added later they are added to a
|
||||
chain these events are excluded from, not to `reg-global-interceptor`."
|
||||
(:require [arthur.clock :as clock]
|
||||
[arthur.domain.clip :as clip]
|
||||
[arthur.footage.store :as footage]
|
||||
[re-frame.core :as rf]))
|
||||
|
||||
(defn- fps [db] (get-in db [:clip :fps]))
|
||||
(defn- frames [db] (get-in db [:clip :frames]))
|
||||
|
||||
(defn frames
|
||||
"The open symbol's length. Derived from the document every time, never kept in
|
||||
the db beside it, because an edit can change it."
|
||||
[db]
|
||||
(or (some-> (footage/entry (:clip/current db)) :clip
|
||||
(clip/frames (get-in db [:ui :open])))
|
||||
1))
|
||||
|
||||
(defn show
|
||||
"Put loaded clip `id` on screen, open on the symbol it opens on, with the
|
||||
playhead home. What every way of loading a document ends in, so that none of
|
||||
them can forget which symbol is open."
|
||||
[db id]
|
||||
(let [entry (footage/entry id)
|
||||
sid (clip/opens-on (:clip entry))]
|
||||
(-> db
|
||||
(assoc :clip/current id
|
||||
:clip (select-keys entry [:fps :width :height :audio :display-fps]))
|
||||
(update :ui merge {:open sid :tabs (if sid [sid] [])})
|
||||
(assoc-in [:playback :frame] 0)
|
||||
(assoc-in [:playback :playing?] false))))
|
||||
|
||||
(rf/reg-event-db
|
||||
::tick
|
||||
|
|
@ -99,21 +121,15 @@
|
|||
;; Changing the clip changes the resolver, the frame count and the rate all
|
||||
;; at once, so the playhead goes home rather than being left pointing at a
|
||||
;; frame the new clip may not have.
|
||||
(let [{:keys [fps frames] :as clip} (footage/entry id)]
|
||||
{:db (-> db
|
||||
(assoc :clip/current id)
|
||||
;; The stage travels with the clip: two clips may be different
|
||||
;; sizes, and the raster the loop paints into is the clip's, not
|
||||
;; the app's.
|
||||
(assoc :clip (select-keys clip [:fps :frames :width :height :audio :display-fps]))
|
||||
;; The document's identity goes with it. A built-in scene has no
|
||||
;; project on the server, so this CLEARS the id rather than keeping
|
||||
;; the last one — saving a fixture must create a document of its
|
||||
;; own, not overwrite whatever happened to be open before it.
|
||||
(assoc :project {:id nil :cid (:cid clip) :name (:label clip)
|
||||
:seq nil :busy? false
|
||||
:status "built-in example · not a saved project"})
|
||||
(assoc-in [:playback :frame] 0)
|
||||
(assoc-in [:playback :playing?] false))
|
||||
(let [{:keys [label cid]} (footage/entry id)
|
||||
db (-> (show db id)
|
||||
;; The document's identity goes with it. A built-in scene has no
|
||||
;; project on the server, so this CLEARS the id rather than
|
||||
;; keeping the last one — saving a fixture must create a
|
||||
;; document of its own, not overwrite whatever was open before.
|
||||
(assoc :project {:id nil :cid cid :name label
|
||||
:seq nil :busy? false
|
||||
:status "built-in example · not a saved project"}))]
|
||||
{:db db
|
||||
::pause! nil
|
||||
::seek! [fps frames 0]})))
|
||||
::seek! [(fps db) (frames db) 0]})))
|
||||
|
|
|
|||
|
|
@ -109,13 +109,13 @@
|
|||
built (:clip loaded)]
|
||||
(let [entry (merge (select-keys built [:fps :width :height])
|
||||
{:label (str (or (.-name clip-json) cid) " (saved)")
|
||||
:cid cid :frames (clip/frames built)
|
||||
:cid cid
|
||||
:display-fps (:fps built)
|
||||
:clip built :store (:store loaded)
|
||||
:footage-id footage-id
|
||||
:audio (if footage (.-audio footage)
|
||||
"/static/arthur/audio.wav")})]
|
||||
(-> (mix/mix! built (:audio entry) (:store entry))
|
||||
(-> (mix/mix! built (clip/opens-on built) (:audio entry) (:store entry))
|
||||
(.then (fn [audio] (assoc entry :audio audio)))))))))))
|
||||
|
||||
(rf/reg-fx
|
||||
|
|
@ -188,6 +188,11 @@
|
|||
(.then (fn [^js row] (http/GET (str "/api/projects/" (.-id row)))))
|
||||
(.then (fn [^js loaded]
|
||||
(let [^js clip-json (first (array-seq (.-clips loaded)))]
|
||||
(when (not= project/schema-version (.-schema_version loaded))
|
||||
(throw (ex-info (str "that project is stored as schema "
|
||||
(.-schema_version loaded) " and this client reads "
|
||||
project/schema-version)
|
||||
{})))
|
||||
(when-not clip-json
|
||||
(throw (ex-info "that project has no clips" {})))
|
||||
(-> (opened-entry! clip-json)
|
||||
|
|
@ -213,9 +218,9 @@
|
|||
(.then (fn [entry]
|
||||
(let [built (stage/compose (:clip entry))
|
||||
entry (assoc entry :clip built :label (:name built)
|
||||
:cid "stage-8625" :frames (clip/frames built)
|
||||
:cid "stage-8625"
|
||||
:width (:width built) :height (:height built))]
|
||||
(-> (mix/mix! built (:audio entry) (:store entry))
|
||||
(-> (mix/mix! built (clip/opens-on built) (:audio entry) (:store entry))
|
||||
(.then (fn [audio]
|
||||
(rf/dispatch
|
||||
[::stage-opened
|
||||
|
|
@ -379,7 +384,6 @@
|
|||
;; it, and two untitled documents saved from two tabs are two documents.
|
||||
:cid (str (random-uuid))
|
||||
:display-fps (:fps c)
|
||||
:frames (clip/frames c)
|
||||
:fps (:fps c) :width (:width c) :height (:height c)}))
|
||||
|
||||
(rf/reg-event-fx
|
||||
|
|
@ -390,14 +394,10 @@
|
|||
;; for, and the built-in scenes are rows in the media pool like anything else.
|
||||
(let [entry (blank-entry)
|
||||
id (store/install! entry "new")]
|
||||
{:db (-> db
|
||||
(assoc :clip/current id
|
||||
:clip (select-keys entry [:fps :frames :width :height :audio :display-fps]))
|
||||
{:db (-> (assoc db :ui (:ui db/default))
|
||||
(pb/show id)
|
||||
(assoc :project {:id nil :cid (:cid entry) :name nil :seq nil
|
||||
:busy? false :status "new document"})
|
||||
(assoc :ui (:ui db/default))
|
||||
(assoc-in [:playback :frame] 0)
|
||||
(assoc-in [:playback :playing?] false))
|
||||
:busy? false :status "new document"}))
|
||||
::pb/pause! nil})))
|
||||
|
||||
(rf/reg-event-fx
|
||||
|
|
@ -447,15 +447,11 @@
|
|||
(rf/reg-event-fx
|
||||
::stage-opened
|
||||
(fn [{:keys [db]} [_ clip-id]]
|
||||
(let [entry (store/entry clip-id)]
|
||||
{:db (-> db
|
||||
(assoc :clip/current clip-id
|
||||
:clip (select-keys entry [:fps :frames :width :height :audio :display-fps]))
|
||||
(assoc :project {:id nil :cid nil :name nil :seq nil
|
||||
:busy? false :status "loaded 8625 stage study"})
|
||||
(assoc-in [:playback :frame] 0)
|
||||
(assoc-in [:playback :playing?] false))
|
||||
::pb/seek! [(:fps entry) (:frames entry) 0]})))
|
||||
(let [db (-> (pb/show db clip-id)
|
||||
(assoc :project {:id nil :cid nil :name nil :seq nil
|
||||
:busy? false :status "loaded 8625 stage study"}))]
|
||||
{:db db
|
||||
::pb/seek! [(get-in db [:clip :fps]) (pb/frames db) 0]})))
|
||||
|
||||
(rf/reg-event-db
|
||||
::saved
|
||||
|
|
@ -469,17 +465,13 @@
|
|||
(rf/reg-event-fx
|
||||
::opened
|
||||
(fn [{:keys [db]} [_ clip-id project-id name seq]]
|
||||
(let [clip (store/entry clip-id)]
|
||||
{:db (-> db
|
||||
(assoc :clip/current clip-id
|
||||
:clip (select-keys clip [:fps :frames :width :height :audio :display-fps]))
|
||||
(update :project merge
|
||||
{:id project-id :name name :seq seq :cid (:cid clip)
|
||||
:busy? false
|
||||
:status (str "opened " name " r" seq)})
|
||||
(assoc-in [:playback :frame] 0)
|
||||
(assoc-in [:playback :playing?] false))
|
||||
::pb/pause! nil})))
|
||||
{:db (-> (pb/show db clip-id)
|
||||
(update :project merge
|
||||
{:id project-id :name name :seq seq
|
||||
:cid (:cid (store/entry clip-id))
|
||||
:busy? false
|
||||
:status (str "opened " name " r" seq)}))
|
||||
::pb/pause! nil}))
|
||||
|
||||
(rf/reg-event-db
|
||||
::failed
|
||||
|
|
|
|||
|
|
@ -58,9 +58,10 @@
|
|||
;; out: a node's id is its identity in the saved document, so the pure
|
||||
;; layer must be handed one rather than invent one. If replaying the event
|
||||
;; log ever has to reproduce a document exactly, this becomes a cofx.
|
||||
(let [id (keyword (str "paint-" (random-uuid)))]
|
||||
{:db (update db :ui merge {:tool nil :draft [] :selection [:node :main id]})
|
||||
:dispatch [::paint/new-shape id draft (get-in db [:ui :tone])]})))))
|
||||
(let [id (keyword (str "paint-" (random-uuid)))
|
||||
sid (get-in db [:ui :open])]
|
||||
{:db (update db :ui merge {:tool nil :draft [] :selection [:node sid id]})
|
||||
:dispatch [::paint/new-shape sid id draft (get-in db [:ui :tone])]})))))
|
||||
|
||||
(rf/reg-event-db
|
||||
::set-knob
|
||||
|
|
@ -72,8 +73,9 @@
|
|||
|
||||
(rf/reg-event-db
|
||||
::place-symbol
|
||||
(fn [db [_ tid [x y]]]
|
||||
(let [uuid (random-uuid)]
|
||||
(fn [db [_ sid [x y]]]
|
||||
(let [uuid (random-uuid)
|
||||
into (get-in db [:ui :open])]
|
||||
(-> db
|
||||
(edit/edit #(clip/place-symbol % tid (get-in db [:playback :frame]) uuid [x y]))
|
||||
(assoc-in [:ui :selection] [:node :main uuid])))))
|
||||
(edit/edit #(clip/place-symbol % into sid (get-in db [:playback :frame]) uuid [x y]))
|
||||
(assoc-in [:ui :selection] [:node into uuid])))))
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
reachable and neither should be the other's special case.
|
||||
|
||||
What is genuinely shared is everything above the sink, and it is most of the
|
||||
work: rooting the resolver at the chosen timeline, generated-channel picture
|
||||
work: rooting the resolver at the chosen symbol, generated-channel picture
|
||||
sampling,
|
||||
the raster, the frame loop, the audio mix, the progress reporting and the
|
||||
yielding that lets the page paint. So `run!` owns all of that and calls three
|
||||
|
|
@ -20,7 +20,7 @@
|
|||
|
||||
TWO RULES THE WALK ENFORCES, both about sync:
|
||||
|
||||
Every frame of the timeline's frame space is emitted, at the CLIP's rate. A
|
||||
Every frame of the symbol's frame space is emitted, at the CLIP's rate. A
|
||||
lower picture rate holds a pose across several frames — it never drops them —
|
||||
so the exported duration matches the audio no matter what the picture rate is.
|
||||
Decimating instead is how an export silently runs short and the sound slides
|
||||
|
|
@ -37,14 +37,14 @@
|
|||
[arthur.domain.raster :as raster]))
|
||||
|
||||
(defprotocol Exporter
|
||||
"A sink for a rendered timeline. Implementations live under `arthur.export.*`.
|
||||
"A sink for a rendered symbol. Implementations live under `arthur.export.*`.
|
||||
|
||||
Called in this order, once, per export: `begin!`, then `frame!` for every frame
|
||||
in order from 0, then `finish!`. Any of them may return a promise and the walk
|
||||
waits for it, which is what keeps a slow encoder from being fed faster than it
|
||||
drains and what gives the page a chance to paint between frames.
|
||||
|
||||
`Exporter` rather than `IExporter`, which is what `domain/timeline`'s
|
||||
`Exporter` rather than `IExporter`, which is what `domain/symbol`'s
|
||||
`IResolver` would suggest, because it names a role a thing plays rather than a
|
||||
capability a value has."
|
||||
|
||||
|
|
@ -60,7 +60,7 @@
|
|||
:fps frames per second of the finished file — the CLIP's rate
|
||||
:frames how many frames will arrive
|
||||
:ramp index -> [r g b], the palette to expand through
|
||||
:audio an AudioBuffer, or nil when the timeline has no sound
|
||||
:audio an AudioBuffer, or nil when the symbol has no sound
|
||||
|
||||
The ramp and the audio are here rather than on `frame!` because neither
|
||||
changes across an export, and a muxer has to declare its tracks before it
|
||||
|
|
@ -71,7 +71,7 @@
|
|||
|
||||
THE RASTER IS REUSED and must be consumed before this returns (or before the
|
||||
promise it returns settles). The walk hands back the same buffer every frame,
|
||||
for the same reason `timeline/resolver` reuses its point buffers: a 900-frame
|
||||
for the same reason `symbol/resolver` reuses its point buffers: a 900-frame
|
||||
export that allocates a stage per frame is a tab that swaps. A sink that wants
|
||||
to keep pixels has to copy or encode them here.")
|
||||
|
||||
|
|
@ -92,7 +92,7 @@
|
|||
not the thing that was asked for.
|
||||
|
||||
Siblings go. That is the whole point: what comes out is one placement, where it
|
||||
sits, on the timeline it sits on."
|
||||
sits, in the symbol it sits in."
|
||||
[nodes id]
|
||||
(let [up (loop [i id acc #{}]
|
||||
(if (or (nil? i) (contains? acc i))
|
||||
|
|
@ -114,7 +114,7 @@
|
|||
k))))
|
||||
|
||||
(defn isolate
|
||||
"The timeline with only `id` and its kin kept. `nil` leaves it alone.
|
||||
"The symbol with only `id` and its kin kept. `nil` leaves it alone.
|
||||
|
||||
The FRAME SPACE IS UNTOUCHED, which is what makes this different from exporting
|
||||
the symbol a placement plays. Rooting at `:sym/face-8625` renders the drawing in
|
||||
|
|
@ -122,10 +122,10 @@
|
|||
renders the STAGE — its length, its rate, the placement's span, drift and scale
|
||||
— with the other six removed. The first is the drawing; the second is that face
|
||||
on the stage, and they are different deliverables."
|
||||
[tl id]
|
||||
(if (and id (get-in tl [:nodes id]))
|
||||
(update tl :nodes select-keys (kin (:nodes tl) id))
|
||||
tl))
|
||||
[sym id]
|
||||
(if (and id (get-in sym [:nodes id]))
|
||||
(update sym :nodes select-keys (kin (:nodes sym) id))
|
||||
sym))
|
||||
|
||||
(defn- yield!
|
||||
"Hand the event loop a turn between frames.
|
||||
|
|
@ -140,64 +140,64 @@
|
|||
(defn audio!
|
||||
"Promise of the AudioBuffer to export alongside the picture, or nil.
|
||||
|
||||
A timeline's own placed audio tracks win. Failing that, the ROOT timeline — and
|
||||
only the root — falls back to the clip's audio file, which is where a take's
|
||||
sound lives before anyone has placed a track. A symbol exports silence rather
|
||||
than the whole clip's soundtrack, because a symbol's frame space is its own and
|
||||
the clip's audio is not a fact about it."
|
||||
[clip-doc tid store fallback-url]
|
||||
(-> (mix/buffer! clip-doc tid store)
|
||||
A symbol's own placed audio tracks win. Failing that, the symbol the document
|
||||
OPENS ON — and only that one — falls back to the clip's audio file, which is
|
||||
where a take's sound lives before anyone has placed a track. Any other symbol
|
||||
exports silence rather than the whole clip's soundtrack, because its frame
|
||||
space is its own and the clip's audio is not a fact about it."
|
||||
[clip-doc sid store fallback-url]
|
||||
(-> (mix/buffer! clip-doc sid store)
|
||||
(.then (fn [buffer]
|
||||
(cond
|
||||
buffer buffer
|
||||
(and (= tid clip/root-id) fallback-url) (mix/decode! fallback-url)
|
||||
(and (= sid (clip/opens-on clip-doc)) fallback-url) (mix/decode! fallback-url)
|
||||
:else nil)))))
|
||||
|
||||
(defn plan
|
||||
"What an export of `tid` will produce, without producing any of it.
|
||||
"What an export of `sid` will produce, without producing any of it.
|
||||
|
||||
Separate from `run!` so the UI can show the size and length it is about to
|
||||
commit to, and so the arithmetic is assertable without a sink."
|
||||
[{:keys [clip timeline zoom picture-fps] isolate-id :isolate}]
|
||||
(let [tl (some-> (clip/timeline clip timeline) (isolate isolate-id))
|
||||
[{:keys [clip zoom picture-fps] sid :symbol isolate-id :isolate}]
|
||||
(let [sym (some-> (clip/symbol clip sid) (isolate isolate-id))
|
||||
zoom (max 1 (js/Math.floor (or zoom 1)))]
|
||||
(when tl
|
||||
{:frames (:frames tl)
|
||||
(when sym
|
||||
{:frames (:frames sym)
|
||||
:fps (:fps clip)
|
||||
:zoom zoom
|
||||
:width (* (:width clip) zoom)
|
||||
:height (* (:height clip) zoom)
|
||||
:seconds (/ (:frames tl) (:fps clip))
|
||||
:seconds (/ (:frames sym) (:fps clip))
|
||||
;; The unedited picture-grid count. A per-instance pose track can add or
|
||||
;; remove changes, so this is only the grid's nominal count.
|
||||
:poses (if (and picture-fps (< picture-fps (:fps clip)))
|
||||
(js/Math.ceil (* (/ (:frames tl) (:fps clip)) picture-fps))
|
||||
(:frames tl))})))
|
||||
(js/Math.ceil (* (/ (:frames sym) (:fps clip)) picture-fps))
|
||||
(:frames sym))})))
|
||||
|
||||
(defn run!
|
||||
"Render `timeline` into `exporter`. Promise of `{:filename :blob}`.
|
||||
"Render symbol `sid` into `exporter`. Promise of `{:filename :blob}`.
|
||||
|
||||
`on-progress` is called with `[done total]` as frames complete, and is where a
|
||||
UI hangs its readout."
|
||||
[{:keys [clip timeline store palette ramp zoom picture-fps name audio-url]
|
||||
isolate-id :isolate}
|
||||
[{:keys [clip store palette ramp zoom picture-fps name audio-url]
|
||||
sid :symbol isolate-id :isolate}
|
||||
exporter on-progress]
|
||||
(let [tl (some-> (clip/timeline clip timeline) (isolate isolate-id))]
|
||||
(when-not tl
|
||||
(throw (ex-info "there is no such timeline to export"
|
||||
{:timeline timeline
|
||||
:timelines (vec (sort-by str (keys (:timelines clip))))})))
|
||||
(let [{:keys [frames fps zoom]} (plan {:clip clip :timeline timeline :zoom zoom
|
||||
(let [sym (some-> (clip/symbol clip sid) (isolate isolate-id))]
|
||||
(when-not sym
|
||||
(throw (ex-info "there is no such symbol to export"
|
||||
{:symbol sid
|
||||
:symbols (vec (sort-by str (keys (:symbols clip))))})))
|
||||
(let [{:keys [frames fps zoom]} (plan {:clip clip :symbol sid :zoom zoom
|
||||
:isolate isolate-id})
|
||||
;; Rooted at the chosen timeline, so exporting a symbol is exporting a
|
||||
;; clip whose root that symbol is. Nested symbols inside it still
|
||||
;; resolve — clip/resolver is the function that knows how.
|
||||
doc (assoc-in clip [:timelines timeline] tl)
|
||||
resolve-frame (clip/resolver doc store palette timeline
|
||||
;; Rooted at the chosen symbol, as the stage is. Nested instances
|
||||
;; inside it still resolve — clip/resolver is the function that knows
|
||||
;; how.
|
||||
doc (assoc-in clip [:symbols sid] sym)
|
||||
resolve-frame (clip/resolver doc store palette sid
|
||||
{:picture-fps picture-fps})
|
||||
ras (raster/make (:width clip) (:height clip))
|
||||
bg (get palette :bg 0)]
|
||||
(-> (audio! doc timeline store audio-url)
|
||||
(-> (audio! doc sid store audio-url)
|
||||
(.then (fn [audio]
|
||||
(js/Promise.resolve
|
||||
(begin! exporter {:name name :width (:width clip)
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@
|
|||
plate, which a human draws, is worth decimating. Sparse visibility keys capture
|
||||
decisions about the mouth cavity, blink and teeth without thinning geometry."
|
||||
(:require [arthur.domain.channel :as ch]
|
||||
[arthur.domain.clip :as clip]
|
||||
[arthur.domain.feature :as feature]
|
||||
[arthur.domain.geom :as geom]
|
||||
[arthur.domain.ring :as ring]
|
||||
|
|
@ -262,7 +261,7 @@
|
|||
not. Two faces in one shot were filmed together and are posed apart: choosing
|
||||
frame 12 for the second face must leave the first one running, and it does,
|
||||
because an anchor map lives on that subject's own head node and
|
||||
`domain/timeline` reads anchors off whatever node carries them."
|
||||
`domain/symbol` reads anchors off whatever node carries them."
|
||||
[{:keys [subject mode anchors]} {:keys [clip]}]
|
||||
(when-not (contains? head-modes mode)
|
||||
(throw (ex-info "head mode must be free or anchored"
|
||||
|
|
@ -274,14 +273,14 @@
|
|||
{:subject subject :subjects (vec (sort-by str (keys (:subjects clip))))})))
|
||||
(reduce
|
||||
(fn [c sid]
|
||||
(let [frames (get-in c [:timelines sid :frames])]
|
||||
(let [frames (get-in c [:symbols sid :frames])]
|
||||
(when (and (= mode :anchored)
|
||||
(not (and (map? anchors) (contains? anchors 0)
|
||||
(every? #(and (integer? %) (<= 0 %) (< % frames))
|
||||
(concat (keys anchors) (vals anchors))))))
|
||||
(throw (ex-info "anchored head needs a frame-zero key and valid source frames"
|
||||
{:subject sid :anchors anchors :frames frames})))
|
||||
(update-in c [:timelines sid :nodes :head]
|
||||
(update-in c [:symbols sid :nodes :head]
|
||||
(fn [n]
|
||||
(cond-> (assoc n :channels (:measured n))
|
||||
(= mode :anchored) (assoc :anchors anchors)
|
||||
|
|
@ -654,7 +653,7 @@
|
|||
;; the clip
|
||||
|
||||
(defn- subject-part
|
||||
"A subject's drawing, metadata and blocks. Node names are timeline-local."
|
||||
"A subject's drawing, metadata and blocks. Node names are symbol-local."
|
||||
[params subject {:keys [outer eyes brows teeth] :as inputs}]
|
||||
(let [own (partial feature/owned subject)
|
||||
areas (cond-> [:mouth] (and eyes brows) (into [:eye :brow]) teeth (conj :teeth))
|
||||
|
|
@ -666,13 +665,13 @@
|
|||
:eye-l [:eye [:eye-l :eye-l-in :iris-l :pupil-l]]
|
||||
:brow-r [:brow [:brow-r]] :brow-l [:brow [:brow-l]]})
|
||||
teeth (assoc :teeth [:teeth [:teeth]]))]
|
||||
{:timeline {:id subject :frames (count outer)
|
||||
{:symbol {:id subject :frames (count outer)
|
||||
:nodes (into {:head {:id :head :name "head" :kind :group :z "a1"
|
||||
:measured (:measured head)}}
|
||||
(mapcat :nodes) parts)}
|
||||
:features (into {} (map (fn [[role [area nodes]]]
|
||||
[(own role) {:id (own role) :subject subject
|
||||
:timeline subject :area area
|
||||
:symbol subject :area area
|
||||
:nodes nodes :params {}}])) features)
|
||||
:groups (if (and eyes brows)
|
||||
{(own :eyes) {:id (own :eyes) :kind :eye-pair :subject subject
|
||||
|
|
@ -681,12 +680,13 @@
|
|||
:store (into (:store head) (mapcat :store) parts)}))
|
||||
|
||||
(defn clip
|
||||
"Subject-id -> conditioned measurements becomes a library of face timelines.
|
||||
"Subject-id -> conditioned measurements becomes one symbol per face, and a
|
||||
symbol called :main that places them.
|
||||
|
||||
:main holds exposure and a shared source-to-stage placement. Each subject is
|
||||
placed by an ordinary symbol instance, so pose choices and transforms have
|
||||
their existing instance scope. Features name local nodes in that subject's
|
||||
timeline; block descriptors still name globally distinct features.
|
||||
placed by an ordinary instance, so pose choices and transforms have their
|
||||
existing instance scope. Features name local nodes in that subject's symbol;
|
||||
block descriptors still name globally distinct features.
|
||||
|
||||
Subjects share a source frame space. :head and :anchors may be overridden
|
||||
per subject; all other freeze settings come from params."
|
||||
|
|
@ -697,7 +697,7 @@
|
|||
(throw (ex-info "a freeze needs subjects with ids distinct from :main, :root and :face" {})))
|
||||
(let [ordered (sort-by (comp str key) subjects)
|
||||
parts (mapv (fn [[id inputs]] [id (subject-part params id inputs)]) ordered)
|
||||
lengths (distinct (map #(get-in % [1 :timeline :frames]) parts))
|
||||
lengths (distinct (map #(get-in % [1 :symbol :frames]) parts))
|
||||
_ (when-not (and (= 1 (count lengths)) (pos? (first lengths)))
|
||||
(throw (ex-info "subjects need the same positive frame count"
|
||||
{:frames (vec lengths)})))
|
||||
|
|
@ -707,9 +707,9 @@
|
|||
:width (first stage) :height (second stage)
|
||||
:subjects (into {} (map (fn [[id _]] [id {:id id :params {}}])) ordered)
|
||||
:features (merged :features) :groups (merged :groups)
|
||||
:timelines
|
||||
(into {clip/root-id
|
||||
{:id clip/root-id :frames nf
|
||||
:symbols
|
||||
(into {:main
|
||||
{:id :main :frames nf
|
||||
:nodes (into {:root {:id :root :name "clip" :kind :group :z "a1"
|
||||
:time {:mode :map :expose expose}}
|
||||
:face {:id :face :name "source placement" :kind :group
|
||||
|
|
@ -717,10 +717,10 @@
|
|||
:channels (face-placement params subjects)}}
|
||||
(map-indexed
|
||||
(fn [i [id _]]
|
||||
[id {:id id :kind :symbol :of id :parent :face
|
||||
[id {:id id :kind :instance :of id :parent :face
|
||||
:z (str "a" i)}]))
|
||||
ordered)}}
|
||||
(map (fn [[id part]] [id (:timeline part)])) parts)}]
|
||||
(map (fn [[id part]] [id (:symbol part)])) parts)}]
|
||||
(doseq [[subject inputs] ordered
|
||||
[id track] (:presence inputs)]
|
||||
(when-not (and (= nf (count track))
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@
|
|||
:when (:generated channel)]
|
||||
[id prop channel])]
|
||||
(-> (reduce (fn [entry [id prop channel]]
|
||||
(let [at [:clip :timelines (get-in entry [:clip :features fid :timeline])
|
||||
(let [at [:clip :symbols (get-in entry [:clip :features fid :symbol])
|
||||
:nodes id :channels prop]
|
||||
old (get-in entry at)]
|
||||
(assoc-in entry at
|
||||
|
|
@ -103,7 +103,7 @@
|
|||
by hand."
|
||||
[entry params base subject]
|
||||
(let [baked (freeze/head-part subject params @base)
|
||||
at [:clip :timelines subject :nodes :head]
|
||||
at [:clip :symbols subject :nodes :head]
|
||||
old (get-in entry at)
|
||||
measured (:measured baked)]
|
||||
(cond-> (-> entry
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@
|
|||
Everything here returns a promise of a PARSED JS VALUE, not of CLJS data, and
|
||||
that is deliberate: a leaf is transit, and `domain/project` reads it straight out
|
||||
of the response object. A keywordising `js->clj` on the way past would turn the
|
||||
leaf path \"clip/c1/timeline/main/node/mouth\" into a keyword whose name is
|
||||
\"c1/timeline/main/node/mouth\",
|
||||
leaf path \"clip/c1/symbol/main/node/mouth\" into a keyword whose name is
|
||||
\"c1/symbol/main/node/mouth\",
|
||||
losing the prefix — a corruption that only shows up on the way back in.
|
||||
|
||||
CSRF IS NOT EXEMPTED. The page renders `{% csrf_token %}`, so Django sets its
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@
|
|||
(rf/reg-sub ::muted? (fn [db _] (get-in db [:playback :muted?])))
|
||||
(rf/reg-sub ::fps (fn [db _] (get-in db [:clip :fps])))
|
||||
(rf/reg-sub ::display-fps (fn [db _] (get-in db [:clip :display-fps])))
|
||||
(rf/reg-sub ::frames (fn [db _] (get-in db [:clip :frames])))
|
||||
;; The stage, in pixels. On the clip because project dimensions are independent
|
||||
;; of the footage — see flow/freeze/face-placement — so the canvas and the raster
|
||||
;; take their size from the document rather than from a constant.
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@
|
|||
playhead is not an input, so moving it cannot invalidate this."
|
||||
(:require [arthur.domain.clip :as clip]
|
||||
[arthur.domain.palette :as pal]
|
||||
[arthur.domain.timeline :as timeline]
|
||||
[arthur.footage.store :as footage]
|
||||
[arthur.subs.playback :as playback]
|
||||
[re-frame.core :as rf]))
|
||||
|
|
@ -23,27 +22,38 @@
|
|||
:<- [::paint-revision]
|
||||
(fn [[id _] _] (:clip (footage/entry id))))
|
||||
|
||||
(rf/reg-sub ::open (fn [db _] (get-in db [:ui :open])))
|
||||
|
||||
(rf/reg-sub
|
||||
::timeline
|
||||
::symbol
|
||||
:<- [::clip]
|
||||
(fn [clip _] (some-> clip clip/root)))
|
||||
:<- [::open]
|
||||
(fn [[clip sid] _] (some-> clip (clip/symbol sid))))
|
||||
|
||||
(rf/reg-sub
|
||||
::frames
|
||||
:<- [::symbol]
|
||||
(fn [sym _]
|
||||
;; The open symbol's length, read off it rather than copied into the db, so a
|
||||
;; drop that lengthens it lengthens the transport with it.
|
||||
(:frames sym)))
|
||||
|
||||
(rf/reg-sub
|
||||
::exposure
|
||||
:<- [::timeline]
|
||||
(fn [tl _]
|
||||
;; Exposure lives on the timeline's root node and is INHERITED, so reading it
|
||||
:<- [::symbol]
|
||||
(fn [sym _]
|
||||
;; Exposure lives on the symbol's root node and is INHERITED, so reading it
|
||||
;; there is reading it everywhere. The transport shows it so that `exposure 2`
|
||||
;; is visibly doing something at the transport rather than only inside the
|
||||
;; document.
|
||||
(or (get-in tl [:nodes :root :time :expose]) 1)))
|
||||
(or (get-in sym [:nodes :root :time :expose]) 1)))
|
||||
|
||||
(rf/reg-sub
|
||||
::palette
|
||||
(fn [db _]
|
||||
;; A NAME resolves to a ramp. One today; when timelines carry a `:palette`
|
||||
;; channel this becomes the project's table and the walk carries the ramp in
|
||||
;; scope, which is why domain/timeline takes the palette as a parameter rather
|
||||
;; scope, which is why domain/symbol takes the palette as a parameter rather
|
||||
;; than reaching for a global.
|
||||
(get {:arthur/default pal/index-of} (:palette db) pal/index-of)))
|
||||
|
||||
|
|
@ -68,11 +78,10 @@
|
|||
(rf/reg-sub
|
||||
::resolver
|
||||
:<- [::clip]
|
||||
:<- [::timeline]
|
||||
:<- [::open]
|
||||
:<- [::store]
|
||||
:<- [::palette]
|
||||
:<- [::playback/display-fps]
|
||||
(fn [[document tl store palette picture-fps] _]
|
||||
(when tl (clip/resolver (assoc-in document [:timelines :main] tl)
|
||||
store palette clip/root-id
|
||||
{:picture-fps picture-fps}))))
|
||||
(fn [[document sid store palette picture-fps] _]
|
||||
(when (and document (clip/symbol document sid))
|
||||
(clip/resolver document store palette sid {:picture-fps picture-fps}))))
|
||||
|
|
|
|||
|
|
@ -23,6 +23,6 @@
|
|||
;; the node alone because every caller that wants the node also wants to know
|
||||
;; where it lives — an edit names the timeline, and a bare node has forgotten.
|
||||
(when (and clip (= :node (first selection)))
|
||||
(let [[_ tid id] selection]
|
||||
(when-let [n (get-in clip [:timelines tid :nodes id])]
|
||||
[tid id n])))))
|
||||
(let [[_ sid id] selection]
|
||||
(when-let [n (get-in clip [:symbols sid :nodes id])]
|
||||
[sid id n])))))
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"The right pane: what the selection is, and what can be changed about it.
|
||||
|
||||
Sections rather than a mode switch. The clip's facts are always true, so the
|
||||
clip section is always there; the node and timeline sections appear when
|
||||
clip section is always there; the node and symbol sections appear when
|
||||
something of that kind is selected; the tracking section appears when the clip
|
||||
has analysis in it. Nothing here computes — every control dispatches an intent
|
||||
and every readout comes off a subscription."
|
||||
|
|
@ -45,12 +45,14 @@
|
|||
(let [clip @(rf/subscribe [::render/clip])
|
||||
fps @(rf/subscribe [::playback/fps])
|
||||
picture @(rf/subscribe [::playback/display-fps])
|
||||
frames @(rf/subscribe [::playback/frames])
|
||||
open @(rf/subscribe [::render/open])
|
||||
frames @(rf/subscribe [::render/frames])
|
||||
expose @(rf/subscribe [::render/exposure])]
|
||||
[section "clip"
|
||||
[facts
|
||||
"name" (:name clip)
|
||||
"stage" (str (:width clip) "×" (:height clip))
|
||||
"open" (some-> open name)
|
||||
"length" (str frames " frames")
|
||||
"source" (str fps " fps")
|
||||
"expose" (str "on " expose "s")]
|
||||
|
|
@ -84,7 +86,7 @@
|
|||
gap after the current one does. Lifted out of the old stage toolbar unchanged —
|
||||
a drawing key is a parameter of the shape, and this is where the shape's
|
||||
parameters are."
|
||||
[id n frame]
|
||||
[sid id n frame]
|
||||
(let [geom (get-in n [:channels paint/geometry])
|
||||
active (when geom (paint/active-frame geom frame))
|
||||
ks (when geom (sort (keys (:keys geom))))
|
||||
|
|
@ -94,7 +96,7 @@
|
|||
[:div.row {:style {:margin "5px 0"}}
|
||||
[:button {:disabled (or (< frame start) (>= frame end)
|
||||
(contains? (:keys geom) frame))
|
||||
:on-click #(rf/dispatch [::paint-events/add-key id])}
|
||||
:on-click #(rf/dispatch [::paint-events/add-key sid id])}
|
||||
"drawing key here"]]
|
||||
(when (seq ks)
|
||||
[:div.row
|
||||
|
|
@ -110,22 +112,22 @@
|
|||
[:label.dim (str "key " active " → " next-k " ")
|
||||
[:select {:value (name (or (channel/segment-interp geom active) :hold))
|
||||
:on-change #(rf/dispatch [::paint-events/set-segment-interp
|
||||
id active (keyword (.. % -target -value))])}
|
||||
sid id active (keyword (.. % -target -value))])}
|
||||
[:option {:value "hold"} "hold"]
|
||||
[:option {:value "linear"} "tween"]]]])]))
|
||||
|
||||
(defn- node-section [[tid id n]]
|
||||
(defn- node-section [[sid id n]]
|
||||
(let [frame @(rf/subscribe [::playback/frame])
|
||||
[start end] (:span n)]
|
||||
[section (str (name (:kind n)) (when (not= :main tid) (str " · " (name tid))))
|
||||
[section (str (name (:kind n)) " · in " (name sid))
|
||||
[facts
|
||||
"name" (or (:name n) (brief id))
|
||||
"id" (brief id)
|
||||
;; Which library timeline a placement plays. The one fact that makes a
|
||||
;; symbol instance legible as an instance rather than as a node.
|
||||
"of" (when (= :symbol (:kind n)) (str (:of n)))
|
||||
;; 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)))
|
||||
"span" (when start (str start " … " end))]
|
||||
(when (:paint? n) [drawing-keys id n frame])
|
||||
(when (:paint? n) [drawing-keys sid id n frame])
|
||||
[:div.row {:style {:margin-top "6px"}} [:span.dim "channels"]]
|
||||
[:dl.facts
|
||||
(doall
|
||||
|
|
@ -136,16 +138,15 @@
|
|||
[:dd (channel-state ch)]]))]]))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; a timeline
|
||||
;; a symbol
|
||||
|
||||
(defn- timeline-section [tid]
|
||||
(let [tl @(rf/subscribe [::render/clip])
|
||||
tl (get-in tl [:timelines tid])]
|
||||
[section "timeline"
|
||||
(defn- symbol-section [sid]
|
||||
(let [sym (get-in @(rf/subscribe [::render/clip]) [:symbols sid])]
|
||||
[section "symbol"
|
||||
[facts
|
||||
"id" (str tid)
|
||||
"length" (str (:frames tl) " frames")
|
||||
"nodes" (str (count (:nodes tl)))]]))
|
||||
"id" (str sid)
|
||||
"length" (str (:frames sym) " frames")
|
||||
"nodes" (str (count (:nodes sym)))]]))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; tracked objects
|
||||
|
|
@ -245,5 +246,5 @@
|
|||
[:div {:style {:min-height 0}}
|
||||
[clip-section]
|
||||
(when node [node-section node])
|
||||
(when (= :timeline (first selection)) [timeline-section (second selection)])
|
||||
(when (= :symbol (first selection)) [symbol-section (second selection)])
|
||||
(when tracked? [tracking-section])]]))
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@
|
|||
:palette @(rf/subscribe [::render/palette])
|
||||
:ramp @(rf/subscribe [::render/ramp])
|
||||
:fps @(rf/subscribe [::sub/fps])
|
||||
:frames @(rf/subscribe [::sub/frames])
|
||||
:frames @(rf/subscribe [::render/frames])
|
||||
:width @(rf/subscribe [::sub/width])
|
||||
:height @(rf/subscribe [::sub/height])
|
||||
:frame @(rf/subscribe [::sub/frame])
|
||||
|
|
|
|||
|
|
@ -8,14 +8,14 @@
|
|||
the confusion this pane should be ending.
|
||||
|
||||
A SYMBOL is one timeline out of the open document's library — the thing a
|
||||
`:kind :symbol` node is an instance of. Dragging one onto the stage places an
|
||||
`:kind :instance` node is an instance of. Dragging one onto the stage places an
|
||||
instance. Open a different project and this is a different list, because a
|
||||
library belongs to the document that holds it.
|
||||
|
||||
FOOTAGE is source media on the server: uploads that have been probed,
|
||||
transcoded and cut into frames. Running one through detection produces a
|
||||
document whose roto IS a symbol — `flow/freeze/clip` gives each tracked subject
|
||||
its own library timeline and places it on `:main` with one instance — so
|
||||
its own symbol and places one instance of each in `:main` — so
|
||||
footage becomes a row in the group above it by way of being detected.
|
||||
|
||||
THE WHOLE PANE IS THE DROP TARGET. A video dropped anywhere in it uploads and
|
||||
|
|
@ -53,25 +53,25 @@
|
|||
(defn- symbols []
|
||||
(let [clip @(rf/subscribe [::render/clip])
|
||||
selection @(rf/subscribe [::sub/selection])
|
||||
library (sort-by str (remove #{:main} (keys (:timelines clip))))]
|
||||
library (sort-by str (keys (:symbols clip)))]
|
||||
(group "symbols"
|
||||
[:div.dim "timelines in this document"]
|
||||
[:div.dim "in this document"]
|
||||
(if (empty? library)
|
||||
[:div.dim "none yet"]
|
||||
(doall
|
||||
(for [tid library
|
||||
:let [tl (get-in clip [:timelines tid])]]
|
||||
^{:key tid}
|
||||
[item {:label (name tid)
|
||||
:sub (str (:frames tl) "f · " (count (:nodes tl)) " nodes")
|
||||
:on? (= selection [:timeline tid])
|
||||
(for [sid library
|
||||
:let [sym (get-in clip [:symbols sid])]]
|
||||
^{:key sid}
|
||||
[item {:label (name sid)
|
||||
:sub (str (:frames sym) "f · " (count (:nodes sym)) " nodes")
|
||||
:on? (= selection [:symbol sid])
|
||||
:draggable true
|
||||
:on-drag-start
|
||||
(fn [^js event]
|
||||
(.setData (.-dataTransfer event) "text/plain"
|
||||
(str "symbol:" (subs (str tid) 1)))
|
||||
(str "symbol:" (subs (str sid) 1)))
|
||||
(set! (.. event -dataTransfer -effectAllowed) "copy"))
|
||||
:on-click #(rf/dispatch [::ui/select [:timeline tid]])}])))
|
||||
:on-click #(rf/dispatch [::ui/select [:symbol sid]])}])))
|
||||
(when (seq library)
|
||||
[:div.dim "drag onto the stage to place"]))))
|
||||
|
||||
|
|
|
|||
|
|
@ -47,15 +47,17 @@
|
|||
(defonce ^:private dragging (atom nil))
|
||||
|
||||
(defn- editing
|
||||
"The selected node when it is a polygon on the root timeline, as
|
||||
`[id node geom active-key editable?]`. Nothing else is vertex-editable yet."
|
||||
[clip frame]
|
||||
(let [[tid id n] @(rf/subscribe [::sub/selected-node])]
|
||||
(when (and (= :main tid) (:paint? n))
|
||||
"The selected node when it is a polygon in the open symbol, as
|
||||
`[sid id node geom active-key editable?]`. Nothing else is vertex-editable yet:
|
||||
a shape inside an instance is drawn through that instance's transform, and
|
||||
handles in the open symbol's space would be in the wrong place."
|
||||
[frame]
|
||||
(let [[sid id n] @(rf/subscribe [::sub/selected-node])]
|
||||
(when (and (= @(rf/subscribe [::render/open]) sid) (:paint? n))
|
||||
(let [geom (get-in n [:channels paint/geometry])
|
||||
active (when geom (paint/active-frame geom frame))
|
||||
[start end] (:span n)]
|
||||
[id n geom active
|
||||
[sid id n geom active
|
||||
;; A frame between two drawing keys with a tween running has no vertices
|
||||
;; of its own to move: what is on screen there is interpolated, and
|
||||
;; dragging it would silently edit the key behind it instead.
|
||||
|
|
@ -69,7 +71,7 @@
|
|||
tool @(rf/subscribe [::sub/tool])
|
||||
draft @(rf/subscribe [::sub/draft])
|
||||
drawing? (= :polygon tool)
|
||||
[id _ geom active editable?] (editing clip frame)
|
||||
[sid id _ geom active editable?] (editing frame)
|
||||
pts (when geom (channel/value-at geom frame))]
|
||||
[:svg {:class (str "paint-overlay" (when drawing? " drawing"))
|
||||
:width (* zoom w) :height (* zoom h)
|
||||
|
|
@ -79,9 +81,9 @@
|
|||
(let [[x y] (stage-point event w h)]
|
||||
(rf/dispatch [::ui/add-draft-point x y]))))
|
||||
:on-pointer-move (fn [event]
|
||||
(when-let [[node key-frame vertex] @dragging]
|
||||
(when-let [[sid node key-frame vertex] @dragging]
|
||||
(rf/dispatch [::paint-events/set-vertex
|
||||
node key-frame vertex
|
||||
sid node key-frame vertex
|
||||
(stage-point event w h)])))
|
||||
:on-pointer-up (fn [_] (reset! dragging nil))
|
||||
:on-pointer-cancel (fn [_] (reset! dragging nil))}
|
||||
|
|
@ -104,10 +106,10 @@
|
|||
(.preventDefault event)
|
||||
(.setPointerCapture (.-currentTarget event)
|
||||
(.-pointerId event))
|
||||
(reset! dragging [id active i]))}])))])]))
|
||||
(reset! dragging [sid id active i]))}])))])]))
|
||||
|
||||
(defn- dropped-symbol
|
||||
"The library timeline a drag out of the media pool is carrying, or nil.
|
||||
"The symbol a drag out of the media pool is carrying, or nil.
|
||||
|
||||
`text/plain` with a prefix rather than a custom MIME type: the payload is one
|
||||
short string, every browser agrees about this type, and a drag that arrives
|
||||
|
|
@ -130,8 +132,8 @@
|
|||
(.preventDefault event)))
|
||||
:on-drop (fn [^js event]
|
||||
(.preventDefault event)
|
||||
(when-let [tid (dropped-symbol event)]
|
||||
(rf/dispatch [::ui/place-symbol tid (stage-point event w h)])))}
|
||||
(when-let [sid (dropped-symbol event)]
|
||||
(rf/dispatch [::ui/place-symbol sid (stage-point event w h)])))}
|
||||
[:canvas.stage {:ref #(player/set-canvas! %)
|
||||
:width w :height h
|
||||
:style {:width (str (* zoom w) "px")
|
||||
|
|
|
|||
|
|
@ -1,22 +1,24 @@
|
|||
(ns arthur.ui.timeline
|
||||
"The bottom pane: the transport, a ruler, and a row per node.
|
||||
"The bottom pane: the transport, a ruler, and a row per node of the open symbol.
|
||||
|
||||
`rows` is the whole of the interesting part and it is a PURE function of the
|
||||
clip and the set of open paths. It flattens the document's two axes of nesting
|
||||
— parent/child within a timeline, and instance into a library timeline — into
|
||||
clip, the open symbol and the set of open paths. It flattens the document's two
|
||||
axes of nesting — parent/child within a symbol, and instance of another
|
||||
symbol — into
|
||||
one depth-tagged list, which is what lets the labels column and the tracks
|
||||
column render from the same vector and therefore stay aligned without measuring
|
||||
anything.
|
||||
|
||||
EVERY FRAME NUMBER A ROW CARRIES IS IN ROOT FRAME SPACE. A symbol's keys are
|
||||
its own timeline's, and drawing them against the stage's ruler unmapped would
|
||||
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
|
||||
put a key under the wrong frame — silently, and most convincingly when the
|
||||
instance starts at 0. So the walk carries a `->root` function and composes one
|
||||
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`.
|
||||
|
||||
WHAT IT DOES NOT DO YET: a looping instance repeats its timeline, and only the
|
||||
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
|
||||
happen and not where they happen again."
|
||||
(:require [clojure.string :as str]
|
||||
|
|
@ -35,7 +37,7 @@
|
|||
|
||||
(defn- local->parent
|
||||
"The inverse of `node/local-frame`: where a frame of this node's OWN time sits
|
||||
on the timeline it lives in.
|
||||
in the symbol it lives in.
|
||||
|
||||
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,
|
||||
|
|
@ -52,7 +54,7 @@
|
|||
identity
|
||||
(fn [f]
|
||||
(let [f (- f (or offset 0))]
|
||||
(if (and (#{:symbol :audio} (:kind n)) (not (zero? rate)))
|
||||
(if (and (#{:instance :audio} (:kind n)) (not (zero? rate)))
|
||||
(js/Math.round (+ at (/ (- f in) rate)))
|
||||
f))))))
|
||||
|
||||
|
|
@ -69,7 +71,7 @@
|
|||
(or (:name n)
|
||||
(if (keyword? id) (subs (str id) 1) (subs (str id) 0 8))))
|
||||
|
||||
(defn- channel-rows [n path depth ->root span]
|
||||
(defn- channel-rows [n path depth ->open span]
|
||||
(for [[cpath ch] (sort-by (comp str key) (node/channels n))]
|
||||
{:path (conj path cpath)
|
||||
:depth depth
|
||||
|
|
@ -77,15 +79,16 @@
|
|||
:kind :channel
|
||||
:select nil
|
||||
:span (when (or (:dense ch) (seq (:keys ch))) span)
|
||||
:keys (mapv ->root (keyed-frames ch))
|
||||
:keys (mapv ->open (keyed-frames ch))
|
||||
:dense? (boolean (:dense ch))}))
|
||||
|
||||
(defn rows
|
||||
"The visible rows, outermost first. `expanded` is a set of row paths."
|
||||
[clip expanded]
|
||||
(letfn [(walk [tid path depth ->root]
|
||||
(let [tl (get-in clip [:timelines tid])
|
||||
ordered (->> (:nodes tl)
|
||||
"The visible rows of symbol `sid`, outermost first. `expanded` is a set of row
|
||||
paths."
|
||||
[clip sid expanded]
|
||||
(letfn [(walk [sid path depth ->open]
|
||||
(let [sym (get-in clip [:symbols sid])
|
||||
ordered (->> (:nodes sym)
|
||||
;; Front-most at the top, as a layer list is drawn
|
||||
;; everywhere. `:z` is the lexicographic draw key;
|
||||
;; the id breaks ties so the order is stable.
|
||||
|
|
@ -99,18 +102,18 @@
|
|||
channels (node/channels n)
|
||||
;; The span is in the PARENT's space and the channels
|
||||
;; are in the node's own, so they take different
|
||||
;; mappings. `self` is also what the symbol's target
|
||||
;; timeline is resolved in — `clip/resolver` roots the
|
||||
;; mappings. `self` is also what the instance's
|
||||
;; symbol is resolved in — `clip/resolver` roots the
|
||||
;; child at this node's local frame — so the nested
|
||||
;; walk carries it down unchanged.
|
||||
self (comp ->root (local->parent n))
|
||||
span (mapv ->root (or (:span n) [0 (:frames tl)]))
|
||||
self (comp ->open (local->parent n))
|
||||
span (mapv ->open (or (:span n) [0 (:frames sym)]))
|
||||
row {:path rpath
|
||||
:depth depth
|
||||
:label (node-label id n)
|
||||
:kind :node
|
||||
:node-kind (:kind n)
|
||||
:select [:node tid id]
|
||||
:select [:node sid id]
|
||||
:expandable? true
|
||||
:expanded? open?
|
||||
:span span
|
||||
|
|
@ -123,11 +126,11 @@
|
|||
[row]
|
||||
(-> [row]
|
||||
(into (channel-rows n rpath (inc depth) self span))
|
||||
(into (when (= :symbol (:kind n))
|
||||
(into (when (= :instance (:kind n))
|
||||
(walk (:of n) rpath (inc depth) self)))))))
|
||||
ordered))))]
|
||||
(if (get-in clip [:timelines :main])
|
||||
(walk :main [] 0 identity)
|
||||
(if (get-in clip [:symbols sid])
|
||||
(walk sid [] 0 identity)
|
||||
[])))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
|
@ -153,7 +156,7 @@
|
|||
(let [playing? @(rf/subscribe [::playback/playing?])
|
||||
rate @(rf/subscribe [::playback/rate])
|
||||
frame @(rf/subscribe [::playback/frame])
|
||||
frames @(rf/subscribe [::playback/frames])
|
||||
frames @(rf/subscribe [::render/frames])
|
||||
{:keys [fps drop]} @player/meter]
|
||||
[:div.pane-head
|
||||
[:button {:on-click #(rf/dispatch [::pb/toggle])} (if playing? "pause" "play")]
|
||||
|
|
@ -214,11 +217,11 @@
|
|||
(defn view []
|
||||
(r/with-let [scrubbing (r/atom false)]
|
||||
(let [clip @(rf/subscribe [::render/clip])
|
||||
frames (max 1 (or @(rf/subscribe [::playback/frames]) 1))
|
||||
frames (max 1 (or @(rf/subscribe [::render/frames]) 1))
|
||||
frame @(rf/subscribe [::playback/frame])
|
||||
selection @(rf/subscribe [::sub/selection])
|
||||
expanded @(rf/subscribe [::sub/expanded])
|
||||
visible (rows clip expanded)
|
||||
visible (rows clip @(rf/subscribe [::render/open]) expanded)
|
||||
;; Roughly ten labels, on a round number of frames.
|
||||
step (* 10 (js/Math.ceil (/ frames 100)))]
|
||||
[:section.pane.time
|
||||
|
|
@ -262,5 +265,5 @@
|
|||
(if (seq visible)
|
||||
(doall (for [row visible]
|
||||
^{:key (str (:path row))} [track-cell row frames]))
|
||||
[:div.tl-empty "nothing on this timeline"])
|
||||
[:div.tl-empty "nothing in this symbol"])
|
||||
[:div.tl-playhead {:style {:left (at% frame frames)}}]]]])))
|
||||
|
|
|
|||
|
|
@ -12,17 +12,17 @@
|
|||
[re-frame.core :as rf]))
|
||||
|
||||
(defn- exporter []
|
||||
(let [{:keys [timeline zoom isolate busy? done total]} @(rf/subscribe [::export/state])
|
||||
(let [{:keys [target zoom busy? done total]} @(rf/subscribe [::export/state])
|
||||
targets @(rf/subscribe [::export/targets])]
|
||||
[:<>
|
||||
;; ONE SELECT over everything exportable: the clip, each symbol in its
|
||||
;; library, and each placement on the stage. They are one list because they
|
||||
;; ONE SELECT over everything exportable: each symbol, and each instance
|
||||
;; in the open one. They are one list because they
|
||||
;; are one kind of request — render this, alone.
|
||||
;;
|
||||
;; The option VALUE goes through `export/target-value`, never `name`: a
|
||||
;; symbol timeline is `:sym/face-8625` and a placement is a uuid, and writing
|
||||
;; symbol id is `:sym/face-8625` and an instance is a uuid, and writing
|
||||
;; either through `name` loses what identifies it.
|
||||
[:select {:value (export/target-value {:timeline (or timeline :main) :isolate isolate})
|
||||
[:select {:value (export/target-value target)
|
||||
:disabled busy?
|
||||
:title "what to render"
|
||||
:on-change #(rf/dispatch [::export/set-target
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue