Split clips from timelines
This commit is contained in:
parent
27bfe18bee
commit
9778b9023b
31 changed files with 794 additions and 444 deletions
|
|
@ -6,34 +6,37 @@
|
|||
affordable over authored data, and cheap writes, since every edit `assoc`es
|
||||
into this map and every mounted layer-2 sub compares the result.
|
||||
|
||||
So the scene is here (it is a document — a human placed every node) and dense
|
||||
So the clip is here (it is a document — a human placed every node) and dense
|
||||
channel blocks are not; they live behind a handle in `store`. The hand-written
|
||||
demo scene has no dense blocks and its store is empty; the swarm and the take
|
||||
demo clip has no dense blocks and its store is empty; the swarm and the take
|
||||
are entirely dense."
|
||||
(:require [arthur.demo :as demo]
|
||||
[arthur.domain.clip :as domain-clip]
|
||||
[arthur.demo.swarm :as swarm]
|
||||
[arthur.demo.take :as take]))
|
||||
|
||||
(defn- clip
|
||||
"A scene plus the clip-level facts the transport and the stage need.
|
||||
(defn- entry
|
||||
"A clip plus what the transport and the stage read off it.
|
||||
|
||||
Read OFF the scene rather than written again beside it. `:fps`, `:frames` and the
|
||||
stage dimensions belong to the CLIP and not to the timeline — a timeline has a
|
||||
frame space, not a rate and not a size — and they sit on the scene map only
|
||||
because there is one clip per scene today. Copying them by hand into this table
|
||||
is how one of them comes to disagree with the scene it describes."
|
||||
[label-key label scene store]
|
||||
(merge {:label label :scene scene :store store
|
||||
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."
|
||||
[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`.
|
||||
;; That file is `extract.sh`'s output — tier 3, which the backend now
|
||||
;; serves by hash — and the synthetic take needs a sound of its own so
|
||||
;; that the clock has something to run against with no footage ingested.
|
||||
:audio "/static/arthur/audio.wav"
|
||||
:cid (name label-key)
|
||||
:display-fps (:fps scene)}
|
||||
(select-keys scene [:fps :frames :width :height])))
|
||||
:display-fps (:fps clip)
|
||||
:frames (domain-clip/frames clip)}
|
||||
(select-keys clip [:fps :width :height])))
|
||||
|
||||
(def scenes
|
||||
(def clips
|
||||
"The hand-made clips, selectable from the transport.
|
||||
|
||||
`:swarm` is the load test: a hundred and twenty nodes, entirely dense. The two
|
||||
|
|
@ -41,14 +44,14 @@
|
|||
`:head` written as a dense track in one and as framed identity in the other, so
|
||||
the button that switches between them switches a document field and nothing
|
||||
else."
|
||||
{:demo (clip :demo "demo" demo/scene nil)
|
||||
:swarm (clip :swarm "swarm" @swarm/scene @swarm/store)
|
||||
:take (clip :take "take" @take/scene @take/store)
|
||||
:take-locked (clip :take-locked "locked" @take/locked @take/store)})
|
||||
{:demo (entry :demo "demo" demo/clip nil)
|
||||
:swarm (entry :swarm "swarm" @swarm/clip @swarm/store)
|
||||
:take (entry :take "take" @take/clip @take/store)
|
||||
:take-locked (entry :take-locked "locked" @take/locked @take/store)})
|
||||
|
||||
(def default
|
||||
{;; --- the document ---
|
||||
:scene/current :take
|
||||
:clip/current :take
|
||||
:palette :arthur/default ; a NAME; the ramp itself is project data
|
||||
|
||||
;; --- the clip ---
|
||||
|
|
@ -57,7 +60,7 @@
|
|||
;; footage's. That is what deleting `makeXform` buys — the framing became a
|
||||
;; 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 (select-keys (:take scenes) [:fps :frames :width :height :audio :display-fps])
|
||||
:clip (select-keys (:take clips) [:fps :frames :width :height :audio :display-fps])
|
||||
|
||||
;; Which ingested footage to detect, and what the last load said. The list
|
||||
;; comes from the server — tier 3 is the backend's since step 9 — so there is
|
||||
|
|
|
|||
|
|
@ -1,23 +1,28 @@
|
|||
(ns arthur.demo
|
||||
"The hand-written scene from port-plan step 2, and nothing else.
|
||||
"The hand-written clip from port-plan step 2, and nothing else.
|
||||
|
||||
The EDN is a resource rather than a literal in this file so that the test and
|
||||
the page read the SAME bytes. If the scene were written twice, the one the test
|
||||
validates would not be the one that renders, and the model would be validated
|
||||
against a scene nobody ever looked at."
|
||||
(:require [arthur.domain.scene :as scene]
|
||||
(:require [arthur.domain.clip :as domain-clip]
|
||||
[arthur.domain.timeline :as timeline]
|
||||
[cljs.reader :as reader]
|
||||
[shadow.resource :as rc]))
|
||||
|
||||
(def source (rc/inline "arthur/demo/scene.edn"))
|
||||
|
||||
(def scene (reader/read-string source))
|
||||
(def clip (reader/read-string source))
|
||||
|
||||
(def fps (:fps scene))
|
||||
(def frames (:frames scene))
|
||||
(def timeline
|
||||
"The clip's root timeline: what an evaluator takes. `clip` is the document."
|
||||
(domain-clip/root clip))
|
||||
|
||||
(def fps (:fps clip))
|
||||
(def frames (domain-clip/frames clip))
|
||||
|
||||
(defn ops-at
|
||||
"Draw ops for one frame, via the specification path. The page uses
|
||||
`scene/resolver` instead; this is here for the REPL."
|
||||
`timeline/resolver` instead; this is here for the REPL."
|
||||
[f]
|
||||
(scene/eval-frame scene f))
|
||||
(timeline/eval-frame timeline f))
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@
|
|||
;; 229 frames at 30fps is 7.63s, which covers audio.wav (7.601s) with a frame to
|
||||
;; spare. fps belongs to the CLIP rather than to the timeline — a timeline has a
|
||||
;; frame space, not a rate — and it is here only because there is one clip.
|
||||
:frames 229
|
||||
:fps 30
|
||||
;; The STAGE, in pixels. The project's dimensions, not the footage's — which is
|
||||
;; what makes `makeXform` deletable: placement is a transform on a node and the
|
||||
|
|
@ -33,7 +32,11 @@
|
|||
:width 320
|
||||
:height 200
|
||||
|
||||
:nodes
|
||||
:timelines
|
||||
{:main
|
||||
{:id :main
|
||||
:frames 229
|
||||
:nodes
|
||||
{;; The clip root. EXPOSURE LIVES HERE and is inherited, because
|
||||
;; docs/design.md is emphatic that everything rides one grid: a head cutting on
|
||||
;; odd frames against a mouth cutting on even ones reads as two performances.
|
||||
|
|
@ -96,4 +99,4 @@
|
|||
{:id :pupil :name "pupil" :kind :rect :parent :iris :stencil :iris :z "a3"
|
||||
:channels
|
||||
{[:geom :size] {:animated? false :value 5.0}
|
||||
[:style :color] {:animated? false :value :pupil}}}}}
|
||||
[:style :color] {:animated? false :value :pupil}}}}}}}
|
||||
|
|
|
|||
|
|
@ -147,19 +147,22 @@
|
|||
(def store
|
||||
(delay (merge (orbit-blocks) (shape-blocks))))
|
||||
|
||||
(def scene
|
||||
(def clip
|
||||
(delay
|
||||
{:name "swarm"
|
||||
:frames frames
|
||||
:fps fps
|
||||
:width 320
|
||||
:height 200
|
||||
:nodes
|
||||
(into {:root {:id :root :kind :group :parent nil :z "a1"
|
||||
:timelines
|
||||
{:main
|
||||
{:id :main
|
||||
:frames frames
|
||||
:nodes
|
||||
(into {:root {:id :root :kind :group :parent nil :z "a1"
|
||||
;; On 2s, like everything else. A hundred and twenty shapes
|
||||
;; cutting on one grid reads as animation; the same shapes on
|
||||
;; their own grids read as a screensaver, which is the whole
|
||||
;; argument for exposure inheriting strictly.
|
||||
:time {:mode :map :expose 2}}}
|
||||
(concat (map (juxt :id identity) (map orbit-node (range n-orbits)))
|
||||
(map (juxt :id identity) (map shape-node (range n-shapes)))))}))
|
||||
(concat (map (juxt :id identity) (map orbit-node (range n-orbits)))
|
||||
(map (juxt :id identity) (map shape-node (range n-shapes)))))}}}))
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
│
|
||||
FREEZE ──▶ channels on nodes
|
||||
│
|
||||
scene/resolver ──▶ raster
|
||||
timeline/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
|
||||
|
|
@ -21,7 +21,7 @@
|
|||
anything that reads a source pixel, because that part takes the landmarks and
|
||||
the frames and never the transform.
|
||||
|
||||
TWO SCENES, ONE STORE. `:take` carries the head as filmed and `:take-locked`
|
||||
TWO CLIPS, ONE STORE. `:take` carries the head as filmed and `:take-locked`
|
||||
carries it locked, and they are the same dense blocks with one node's channels
|
||||
written two ways. That is the claim \"stabilisation is a channel, not a mode\"
|
||||
made checkable by eye: switching between them is a document edit, tier 1, and
|
||||
|
|
@ -91,7 +91,7 @@
|
|||
|
||||
(def store (delay (:store @frozen)))
|
||||
|
||||
(def scene (delay (:scene @frozen)))
|
||||
(def clip (delay (:clip @frozen)))
|
||||
|
||||
(def locked
|
||||
"The same blocks, with `:head` written as framed identity instead."
|
||||
|
|
|
|||
110
frontend/src/arthur/domain/clip.cljs
Normal file
110
frontend/src/arthur/domain/clip.cljs
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
(ns arthur.domain.clip
|
||||
"A CLIP: the unit of work, and a library of timelines.
|
||||
|
||||
{:name \"take\"
|
||||
:fps 30
|
||||
:width 320 :height 200
|
||||
:analysis {...}
|
||||
:subjects {...} :features {...} :groups {...}
|
||||
:timelines {: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.
|
||||
|
||||
Now there is one node-holding type — `arthur.domain.timeline` — and a clip holds
|
||||
a MAP of them. `:kind :symbol` is still unimplemented and this is the shape it
|
||||
was waiting for: an instance names a timeline in `:timelines`, and the resolver
|
||||
recurses into a type it already knows how to evaluate.
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
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."
|
||||
(:require [arthur.domain.feature :as feature]
|
||||
[arthur.domain.timeline :as timeline]))
|
||||
|
||||
(def ^:const root-id
|
||||
"The reserved id of the timeline a clip plays. See the namespace docstring."
|
||||
:main)
|
||||
|
||||
(def clip-keys
|
||||
"Every top-level field of a clip, and the reason `arthur.domain.leaf` refuses
|
||||
one it does not know: a field added to the clip without a leaf to save it in is
|
||||
a field that saves silently and comes back missing. The failure is a document
|
||||
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})
|
||||
|
||||
(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 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."
|
||||
[clip]
|
||||
(:frames (root 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."
|
||||
[clip]
|
||||
(:nodes (root clip)))
|
||||
|
||||
(defn problems
|
||||
"Human-readable reasons this clip will not evaluate or will not save. Empty
|
||||
means it will.
|
||||
|
||||
The tracking identities are checked HERE and not in `domain/timeline`, because a
|
||||
feature names nodes and only the root timeline's nodes were tracked into: a
|
||||
library symbol is drawn, not detected. So `feature/problems` is asked about the
|
||||
root, once, rather than about every timeline."
|
||||
[clip]
|
||||
(vec
|
||||
(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 (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))
|
||||
(when (map? (root clip))
|
||||
(feature/problems clip (nodes clip))))))
|
||||
|
|
@ -1,22 +1,28 @@
|
|||
(ns arthur.domain.feature
|
||||
"Stable tracked identities and explicit eye-pair settings associations.
|
||||
These maps are document data. Rendering only reads the nodes and channels."
|
||||
|
||||
These maps are the CLIP's — `:subjects`, `:features`, `:groups` — and not a
|
||||
timeline's, because they describe what a camera saw and a library symbol is
|
||||
drawn rather than detected. Rendering never reads them; it reads nodes and
|
||||
channels. `problems` therefore takes the clip AND the node map to check
|
||||
references against, rather than reaching for `(:nodes clip)`: a clip holds
|
||||
several timelines and only the root one was tracked into."
|
||||
(:require [arthur.domain.params :as params]))
|
||||
|
||||
(defn group-for [scene feature-id]
|
||||
(defn group-for [clip feature-id]
|
||||
(first (filter (fn [[_ group]] (some #{feature-id} (:members group)))
|
||||
(:groups scene))))
|
||||
(:groups clip))))
|
||||
|
||||
(defn effective-params
|
||||
"Resolve static settings for one feature. A future parameter channel can
|
||||
replace a scalar at this boundary without changing feature or pair identity."
|
||||
[scene feature-id]
|
||||
(let [{:keys [subject area params] :as feature} (get-in scene [:features feature-id])
|
||||
[_ group] (group-for scene feature-id)]
|
||||
[clip feature-id]
|
||||
(let [{:keys [subject area params] :as feature} (get-in clip [:features feature-id])
|
||||
[_ group] (group-for clip feature-id)]
|
||||
(when-not feature
|
||||
(throw (ex-info "unknown feature" {:feature feature-id})))
|
||||
(merge (params/for-area :subject)
|
||||
(get-in scene [:subjects subject :params])
|
||||
(get-in clip [:subjects subject :params])
|
||||
(params/for-area area)
|
||||
(:params group)
|
||||
params)))
|
||||
|
|
@ -24,26 +30,30 @@
|
|||
(defn remove-from-pair
|
||||
"Keep the eye's current settings when its association is removed. Empty pairs
|
||||
are removed; a one-eye pair remains valid and can acquire a partner later."
|
||||
[scene feature-id]
|
||||
(if-let [[group-id group] (group-for scene feature-id)]
|
||||
(let [area (get-in scene [:features feature-id :area])
|
||||
values (select-keys (effective-params scene feature-id)
|
||||
[clip feature-id]
|
||||
(if-let [[group-id group] (group-for clip feature-id)]
|
||||
(let [area (get-in clip [:features feature-id :area])
|
||||
values (select-keys (effective-params clip feature-id)
|
||||
(keys (params/for-area area)))
|
||||
members (vec (remove #{feature-id} (:members group)))]
|
||||
(-> scene
|
||||
(-> clip
|
||||
(assoc-in [:features feature-id :params] values)
|
||||
(update :groups (fn [groups]
|
||||
(if (seq members)
|
||||
(assoc-in groups [group-id :members] members)
|
||||
(dissoc groups group-id))))))
|
||||
scene))
|
||||
clip))
|
||||
|
||||
(defn problems
|
||||
"Check identity references and pair membership before storing a scene."
|
||||
[scene]
|
||||
(let [subjects (:subjects scene)
|
||||
features (:features scene)
|
||||
groups (:groups scene)
|
||||
"Check identity references and pair membership before storing a clip.
|
||||
|
||||
`nodes` is the node map a feature's `:nodes` are resolved against — the root
|
||||
timeline's, passed in rather than looked up, so this namespace does not have to
|
||||
know which timeline a clip plays."
|
||||
[clip nodes]
|
||||
(let [subjects (:subjects clip)
|
||||
features (:features clip)
|
||||
groups (:groups clip)
|
||||
memberships (mapcat (comp :members val) groups)
|
||||
node-owners (mapcat (comp :nodes val) features)]
|
||||
(vec
|
||||
|
|
@ -63,7 +73,7 @@
|
|||
: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 node-id (:nodes f)
|
||||
:when (not (contains? (:nodes scene) node-id))]
|
||||
:when (not (contains? 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)]
|
||||
(str "node " (pr-str id) " belongs to more than one feature"))
|
||||
|
|
|
|||
|
|
@ -8,15 +8,28 @@
|
|||
once.
|
||||
|
||||
clip/<cid>/name a label
|
||||
clip/<cid>/timing fps, frames
|
||||
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>/feature/<fid> one feature: area, nodes, params
|
||||
clip/<cid>/group/<gid> an eye pair and its shared params
|
||||
clip/<cid>/node/<nid> one node: kind, parent, stencil, z, time
|
||||
clip/<cid>/channel/<nid>/<prop> one channel
|
||||
clip/<cid>/measured/<nid> the measured channels a re-freeze owns
|
||||
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
|
||||
|
||||
WHY NODES SIT UNDER A TIMELINE. They did not until the clip and the timeline came
|
||||
apart, and the flat `clip/<cid>/node/<nid>` was the persistence half of the same
|
||||
conflation: it could only ever address the nodes of the one bag a clip had. A
|
||||
library symbol is a timeline, an instance references one, and both need their
|
||||
nodes addressed — so the timeline id is a segment, the root is `main`, and a
|
||||
symbol's nodes are reachable by the same path shape as the clip's own. Adding it
|
||||
later would have meant rewriting every stored path.
|
||||
|
||||
`:frames` MOVED OFF `timing` onto the timeline. A timeline 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.
|
||||
|
||||
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
|
||||
|
|
@ -43,7 +56,9 @@
|
|||
— docs/architecture.md draws one as `:eye-r/iris` — is written `eye-r~iris`.
|
||||
`~` is then refused inside a name, which is the whole of the escaping and is why
|
||||
it is one character rather than a scheme."
|
||||
(:require [arthur.domain.sha256 :as sha]
|
||||
(:require [arthur.domain.clip :as clip]
|
||||
[arthur.domain.sha256 :as sha]
|
||||
[arthur.domain.timeline :as timeline]
|
||||
[clojure.string :as str]))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
|
@ -80,69 +95,89 @@
|
|||
;; ---------------------------------------------------------------------------
|
||||
;; the split
|
||||
|
||||
(def scene-keys
|
||||
"Every top-level field of a scene, and the reason `leaves` refuses one it does
|
||||
not know: a field added to the scene without a leaf is a field that saves
|
||||
silently and comes back missing. The failure is a document 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 `leaves` and `scene` in the same commit."
|
||||
#{:name :frames :fps :analysis :subjects :features :groups :width :height :nodes})
|
||||
|
||||
(def ^:private node-channel-keys #{:channels :measured})
|
||||
|
||||
(defn leaves
|
||||
"One clip's scene -> path -> value.
|
||||
"One clip -> path -> value.
|
||||
|
||||
A leaf whose value would be empty is OMITTED rather than written as `{}`, and
|
||||
that is what makes the round trip exact: the demo scene has no `:fps` and the
|
||||
root node has no `:channels`, and a codec that invented them would hand back a
|
||||
scene that is not `=` to the one it was given."
|
||||
[cid scene]
|
||||
(let [unknown (remove scene-keys (keys scene))]
|
||||
that is what makes the round trip exact: the demo clip has no `:fps` and its root
|
||||
node has no `:channels`, and a codec that invented them would hand back a clip
|
||||
that is not `=` to the one it was given."
|
||||
[cid clip]
|
||||
(let [unknown (remove clip/clip-keys (keys clip))]
|
||||
(when (seq unknown)
|
||||
(throw (ex-info "the scene has a field with no leaf to save it in; see arthur.domain.leaf/scene-keys"
|
||||
(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))]
|
||||
(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))})))))
|
||||
(let [at (fn [& parts] (str/join "/" (into ["clip" (segment cid)] parts)))
|
||||
some-leaf (fn [path v] (when (seq v) {path v}))]
|
||||
(apply merge
|
||||
(some-leaf (at "name") (select-keys scene [:name]))
|
||||
(some-leaf (at "timing") (select-keys scene [:fps :frames]))
|
||||
(some-leaf (at "stage") (select-keys scene [:width :height]))
|
||||
(some-leaf (at "source") (:analysis scene))
|
||||
(some-leaf (at "name") (select-keys clip [:name]))
|
||||
(some-leaf (at "timing") (select-keys clip [:fps]))
|
||||
(some-leaf (at "stage") (select-keys clip [:width :height]))
|
||||
(some-leaf (at "source") (:analysis clip))
|
||||
(concat
|
||||
(for [[id v] (:subjects scene)] {(at "subject" (segment id)) v})
|
||||
(for [[id v] (:features scene)] {(at "feature" (segment id)) v})
|
||||
(for [[id v] (:groups scene)] {(at "group" (segment id)) v})
|
||||
(for [[id n] (:nodes scene)] {(at "node" (segment id))
|
||||
(apply dissoc n node-channel-keys)})
|
||||
(for [[id n] (:nodes scene)
|
||||
:when (seq (:measured n))] {(at "measured" (segment id)) (:measured n)})
|
||||
(for [[id n] (:nodes scene)
|
||||
(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
|
||||
;; 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))
|
||||
(apply dissoc n node-channel-keys)})
|
||||
(for [[tid tl] (:timelines clip)
|
||||
[id n] (:nodes tl)
|
||||
:when (seq (:measured n))]
|
||||
{(at "timeline" (segment tid) "measured" (segment id)) (:measured n)})
|
||||
(for [[tid tl] (:timelines clip)
|
||||
[id n] (:nodes tl)
|
||||
[prop ch] (:channels n)]
|
||||
{(at "channel" (segment id) (prop->path prop)) ch})))))
|
||||
{(at "timeline" (segment tid) "channel" (segment id) (prop->path prop)) ch})))))
|
||||
|
||||
(defn scene
|
||||
(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."
|
||||
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
|
||||
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]
|
||||
(let [want (segment cid)]
|
||||
(reduce
|
||||
(fn [acc [path v]]
|
||||
(let [[kind a b] (drop 2 (str/split path #"/"))]
|
||||
(if-not (= want (second (str/split path #"/")))
|
||||
(let [[_ found kind a b c] (str/split path #"/")]
|
||||
(if-not (= want found)
|
||||
acc
|
||||
(case kind
|
||||
"name" (merge acc v)
|
||||
"timing" (merge acc v)
|
||||
"stage" (merge acc v)
|
||||
"source" (assoc acc :analysis v)
|
||||
"subject" (assoc-in acc [:subjects (unsegment a)] v)
|
||||
"feature" (assoc-in acc [:features (unsegment a)] v)
|
||||
"group" (assoc-in acc [:groups (unsegment a)] v)
|
||||
"node" (update-in acc [:nodes (unsegment a)] merge v)
|
||||
"measured" (assoc-in acc [:nodes (unsegment a) :measured] v)
|
||||
"channel" (assoc-in acc [:nodes (unsegment a) :channels (path->prop b)] v)
|
||||
(throw (ex-info "not a leaf path" {:path path}))))))
|
||||
(if (= "timeline" kind)
|
||||
(let [tid (unsegment a)
|
||||
acc (assoc-in acc [:timelines tid :id] tid)]
|
||||
(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)
|
||||
:channels (path->prop (nth (str/split path #"/") 6))]
|
||||
v)
|
||||
(throw (ex-info "not a leaf path" {:path path}))))
|
||||
(case kind
|
||||
"name" (merge acc v)
|
||||
"timing" (merge acc v)
|
||||
"stage" (merge acc v)
|
||||
"source" (assoc acc :analysis v)
|
||||
"subject" (assoc-in acc [:subjects (unsegment a)] v)
|
||||
"feature" (assoc-in acc [:features (unsegment a)] v)
|
||||
"group" (assoc-in acc [:groups (unsegment a)] v)
|
||||
(throw (ex-info "not a leaf path" {:path path})))))))
|
||||
{}
|
||||
;; Sorted, so `node` lands before `channel` and `measured` under one id and
|
||||
;; the node map is merged INTO rather than over. `update-in ... merge` makes
|
||||
|
|
@ -159,25 +194,42 @@
|
|||
on the machine that produced it, and the whole point of tier 2 being
|
||||
content-addressed is that it does not have to travel with tier 1 to be found."
|
||||
[leaves]
|
||||
(let [nodes (into #{} (keep (fn [path]
|
||||
(let [[_ cid kind id] (str/split path #"/")]
|
||||
(when (= "node" kind) [cid id]))))
|
||||
(keys 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.
|
||||
nodes (into #{} (keep (fn [[_ p]]
|
||||
(when (and (= 6 (count p)) (= "timeline" (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))
|
||||
(case (count p)
|
||||
4 true ; the timeline itself
|
||||
6 (#{"node" "measured"} (nth p 4))
|
||||
7 (= "channel" (nth p 4))
|
||||
false)
|
||||
(case (count p)
|
||||
;; The clip's own facts carry no id.
|
||||
3 (#{"name" "timing" "stage" "source"} (nth p 2))
|
||||
4 (#{"subject" "feature" "group"} (nth p 2))
|
||||
false))))]
|
||||
(vec
|
||||
(concat
|
||||
(for [[path v] (sort-by key leaves)
|
||||
:let [[root cid kind id prop] (str/split path #"/")]
|
||||
:when (or (not= "clip" root) (nil? cid)
|
||||
(not (#{"name" "timing" "stage" "source" "subject" "feature"
|
||||
"group" "node" "channel" "measured"} kind)))]
|
||||
(for [[path p] (sort-by key parts)
|
||||
:when (not (legal? p))]
|
||||
(str (pr-str path) " is not a leaf path"))
|
||||
(for [[path _] (sort-by key leaves)
|
||||
:let [[_ cid kind id] (str/split path #"/")]
|
||||
:when (and (#{"channel" "measured"} kind) (not (contains? nodes [cid id])))]
|
||||
(for [[path p] (sort-by key parts)
|
||||
:when (and (legal? p) (= "timeline" (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 v] (sort-by key leaves)
|
||||
:let [[_ _ kind] (str/split path #"/")]
|
||||
:when (and (= "channel" kind) (:dense v)
|
||||
(not (sha/key? (:store (:dense v)))))]
|
||||
(for [[path p] (sort-by key parts)
|
||||
:let [v (get leaves path)]
|
||||
:when (and (legal? p) (= "timeline" (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"))))))
|
||||
|
|
|
|||
|
|
@ -109,7 +109,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 nesting chain, outermost first, by scene/eval-frame. Two
|
||||
Composed along the parent chain, outermost first, by timeline/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
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
(ns arthur.domain.project
|
||||
"A clip <-> the document that travels. The tier split, as a pair of functions.
|
||||
|
||||
`save` takes what `flow/freeze` produced — `{:scene ... :store ...}` — and
|
||||
`save` takes what `flow/freeze` produced — `{:clip ... :store ...}` — and
|
||||
returns two things that are allowed on the wire for different reasons:
|
||||
|
||||
:leaves TIER 1. The document. Nodes, channels, subjects, features, groups,
|
||||
time maps, the analysis record. Kilobytes, and every byte of it
|
||||
authored or authorable.
|
||||
:leaves TIER 1. The document. Timelines, nodes, channels, subjects,
|
||||
features, groups, time maps, the analysis record. Kilobytes, and
|
||||
every byte of it authored or authorable.
|
||||
|
||||
:blocks TIER 2. The dense blocks the document NAMES, each with the
|
||||
descriptor its key is the hash of. Megabytes, content-addressed,
|
||||
|
|
@ -57,11 +57,11 @@
|
|||
\"clip/\" would be lost on the way back in.
|
||||
|
||||
Refuses a document `domain/leaf` calls unaddressable, which is where a hand-made
|
||||
scene with placeholder store keys — `demo/swarm`'s \"swarm/pos\" — stops rather
|
||||
clip with placeholder store keys — `demo/swarm`'s \"swarm/pos\" — stops rather
|
||||
than being uploaded as a project that means something only on the machine that
|
||||
made it."
|
||||
[cid {:keys [scene store]}]
|
||||
(let [leaves (leaf/leaves cid scene)
|
||||
[cid {:keys [clip store]}]
|
||||
(let [leaves (leaf/leaves cid clip)
|
||||
ps (leaf/problems leaves)]
|
||||
(when (seq ps)
|
||||
(throw (ex-info (str "this clip cannot be saved: " (first ps))
|
||||
|
|
@ -83,13 +83,13 @@
|
|||
(block-keys leaves)))})))
|
||||
|
||||
(defn load
|
||||
"The parsed response -> `{:scene :store}`, which is what `flow/freeze` returns
|
||||
"The parsed response -> `{:clip :store}`, which is what `flow/freeze` returns
|
||||
and therefore what the player already knows how to play."
|
||||
[cid ^js doc]
|
||||
(let [leaves (.-leaves doc)
|
||||
tier1 (into {} (map (fn [path] [path (wire/decode-json (aget leaves path))]))
|
||||
(js-keys leaves))]
|
||||
{:scene (leaf/scene cid tier1)
|
||||
{:clip (leaf/clip cid tier1)
|
||||
:store (into {}
|
||||
(map (fn [^js b]
|
||||
[(.-key b)
|
||||
|
|
|
|||
|
|
@ -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 scene/resolver hands the same
|
||||
a node's vertex count is known at freeze time, so timeline/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,12 +1,38 @@
|
|||
(ns arthur.domain.scene
|
||||
"The scene: a flat map of id -> node, and the two ways to evaluate it at a
|
||||
frame.
|
||||
(ns arthur.domain.timeline
|
||||
"A TIMELINE: an ordered bag of nodes in its own frame space, and the two ways to
|
||||
evaluate it at a frame.
|
||||
|
||||
(eval-frame scene f store) THE SPECIFICATION. Allocating, order-free,
|
||||
{: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.
|
||||
|
||||
The clip-level facts are in `arthur.domain.clip`. A timeline 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.
|
||||
|
||||
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 —
|
||||
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,
|
||||
obviously correct. Use it in tests and for a
|
||||
one-off render.
|
||||
|
||||
(resolver scene store) -> (fn [f] ops). What playback uses. Caches the
|
||||
(resolver tl 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
|
||||
|
|
@ -16,8 +42,8 @@
|
|||
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 scene-test asserts they
|
||||
agree frame for frame in forward, backward and random order.
|
||||
between them is exactly the part that can be wrong, and timeline-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:
|
||||
ops carry palette indices and raster-space points, and the rasteriser knows
|
||||
|
|
@ -28,7 +54,6 @@
|
|||
vector of numbers, and they read the same way, which is what makes freezing
|
||||
fill in the same channel rather than convert into a second format."
|
||||
(:require [arthur.domain.channel :as ch]
|
||||
[arthur.domain.feature :as feature]
|
||||
[arthur.domain.node :as node]
|
||||
[arthur.domain.palette :as pal]))
|
||||
|
||||
|
|
@ -50,12 +75,12 @@
|
|||
(when-let [p (:parent (get nodes i))]
|
||||
(if (contains? nodes p)
|
||||
p
|
||||
(throw (ex-info "node's :parent is not in the scene"
|
||||
(throw (ex-info "node's :parent is not in the timeline"
|
||||
{: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 scene" {:node id :chain chain})))
|
||||
(throw (ex-info "parent cycle in timeline" {:node id :chain chain})))
|
||||
chain))
|
||||
|
||||
(defn depth
|
||||
|
|
@ -103,9 +128,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 scene changes and never because the playhead moved — so
|
||||
they change when the timeline 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 scene is, and a frame sorts small integers.
|
||||
second. Here it is derived when the timeline 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."
|
||||
|
|
@ -203,7 +228,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 scene.
|
||||
instead of blanking the timeline.
|
||||
|
||||
Absence is not a boolean and is not an error: a subject that is not on the
|
||||
frame has nothing to show."
|
||||
|
|
@ -291,6 +316,23 @@
|
|||
(throw (ex-info "node kind is not implemented"
|
||||
{:node (:id n) :kind (:kind n)})))))
|
||||
|
||||
(defn- nodes-of
|
||||
"The timeline'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
|
||||
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)]
|
||||
(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)))})))
|
||||
nodes))
|
||||
|
||||
(defn- eval-into
|
||||
"One frame, as a fold over the nodes in topological order.
|
||||
|
||||
|
|
@ -327,13 +369,17 @@
|
|||
;; the specification
|
||||
|
||||
(defn eval-frame
|
||||
"Scene at clip frame f -> draw ops in z order. Pure, and allocates freely.
|
||||
"Timeline 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
|
||||
the only place the space changes.
|
||||
|
||||
This is the definition of what a frame means. `resolver` is what plays it."
|
||||
([scene f] (eval-frame scene f nil pal/index-of))
|
||||
([scene f store] (eval-frame scene f store pal/index-of))
|
||||
([scene f store palette]
|
||||
(let [nodes (:nodes scene)
|
||||
([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]
|
||||
(let [nodes (nodes-of tl)
|
||||
ord (order nodes)]
|
||||
(eval-into {:read (fn [_id _path c lf] (ch/value-at c lf store))
|
||||
:palette palette
|
||||
|
|
@ -385,10 +431,10 @@
|
|||
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."
|
||||
([scene] (resolver scene nil pal/index-of))
|
||||
([scene store] (resolver scene store pal/index-of))
|
||||
([scene store palette]
|
||||
(let [nodes (:nodes scene)
|
||||
([tl] (resolver tl nil pal/index-of))
|
||||
([tl store] (resolver tl store pal/index-of))
|
||||
([tl store palette]
|
||||
(let [nodes (nodes-of tl)
|
||||
ord (order nodes)
|
||||
rank (draw-rank nodes ord)
|
||||
cursors (into {}
|
||||
|
|
@ -427,14 +473,31 @@
|
|||
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(def timeline-keys
|
||||
"Every field a timeline 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 —
|
||||
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 scene will not evaluate. Empty means it will.
|
||||
"Human-readable reasons this timeline 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
|
||||
layering nicety: a feature names nodes, and a library symbol's nodes are not
|
||||
the ones a face was tracked into.
|
||||
|
||||
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."
|
||||
[scene]
|
||||
(let [nodes (:nodes scene)]
|
||||
[tl]
|
||||
(let [nodes (:nodes tl)]
|
||||
(if-not (map? nodes)
|
||||
[":nodes must be a map of id -> node"]
|
||||
(-> []
|
||||
|
|
@ -444,15 +507,19 @@
|
|||
(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 scene")))
|
||||
" which is not in the timeline")))
|
||||
(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 scene")))
|
||||
" which is not in the timeline")))
|
||||
(into (for [[id n] nodes
|
||||
p (node/problems n)]
|
||||
(str "node " (pr-str id) ": " p)))
|
||||
(into (feature/problems scene))
|
||||
(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 (try
|
||||
(doall (map #(depth nodes %) (keys nodes)))
|
||||
nil
|
||||
|
|
@ -4,7 +4,8 @@
|
|||
The frames come from the server by URL since step 9 — see `flow/ingest` — and the
|
||||
detector's identity comes from the server too, because it goes into the content
|
||||
address of every block this produces."
|
||||
(:require [arthur.events.playback :as pb]
|
||||
(:require [arthur.domain.clip :as clip]
|
||||
[arthur.events.playback :as pb]
|
||||
[arthur.flow.detect :as detect]
|
||||
[arthur.flow.ingest :as ingest]
|
||||
[arthur.flow.measure.interior :as interior]
|
||||
|
|
@ -65,10 +66,11 @@
|
|||
:dimensions dimensions :interior interior
|
||||
:presence (:presence manifest)
|
||||
:detector detector})
|
||||
scene (:scene frozen)]
|
||||
(assoc (select-keys scene [:fps :frames :width :height])
|
||||
:display-fps (:fps scene)
|
||||
:scene scene :store (:store frozen)
|
||||
built (:clip frozen)]
|
||||
(assoc (select-keys built [:fps :width :height])
|
||||
:frames (clip/frames built)
|
||||
:display-fps (:fps built)
|
||||
:clip built :store (:store frozen)
|
||||
;; No cache-buster. The audio is a blob named by the hash of its own
|
||||
;; bytes, so re-extracting gives it a different URL rather than
|
||||
;; overwriting this one — which is what the `?v=` here used to work
|
||||
|
|
@ -156,7 +158,7 @@
|
|||
(fn [{:keys [db]} [_ id summary]]
|
||||
(let [clip (store/entry id)]
|
||||
{:db (-> db
|
||||
(assoc :scene/current id
|
||||
(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))
|
||||
|
|
|
|||
|
|
@ -94,14 +94,14 @@
|
|||
{:db (assoc-in db [:playback :muted?] on?) ::mute! on?})))
|
||||
|
||||
(rf/reg-event-fx
|
||||
::select-scene
|
||||
::select-clip
|
||||
(fn [{:keys [db]} [_ id]]
|
||||
;; 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 :scene/current id)
|
||||
(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.
|
||||
|
|
|
|||
|
|
@ -21,7 +21,8 @@
|
|||
|
||||
Nothing here touches app-db except through events. The promise chain lives in an
|
||||
fx, which is the only thing in this namespace that is not pure."
|
||||
(:require [arthur.domain.project :as project]
|
||||
(:require [arthur.domain.clip :as clip]
|
||||
[arthur.domain.project :as project]
|
||||
[arthur.events.playback :as pb]
|
||||
[arthur.footage.store :as store]
|
||||
[arthur.flow.address :as address]
|
||||
|
|
@ -67,7 +68,7 @@
|
|||
(rf/reg-fx
|
||||
::save!
|
||||
(fn [{:keys [id cid label clip]}]
|
||||
(let [analysis (:analysis (:scene clip))
|
||||
(let [analysis (:analysis (:clip clip))
|
||||
doc (project/save cid clip)]
|
||||
(-> (ensure-project! id label)
|
||||
(.then (fn [pid]
|
||||
|
|
@ -112,20 +113,21 @@
|
|||
(into-array (map #(http/GET (str "/api/blocks/" %))
|
||||
(array-seq (.-blocks clip-json)))))
|
||||
(.then (fn [blocks]
|
||||
(let [doc #js {:leaves (.-leaves clip-json) :blocks blocks}
|
||||
cid (.-cid clip-json)
|
||||
clip (project/load cid doc)
|
||||
scene (:scene clip)
|
||||
entry (merge
|
||||
(select-keys scene [:fps :frames :width :height])
|
||||
(let [doc #js {:leaves (.-leaves clip-json) :blocks blocks}
|
||||
cid (.-cid clip-json)
|
||||
loaded' (project/load cid doc)
|
||||
built (:clip loaded')
|
||||
entry (merge
|
||||
(select-keys built [:fps :width :height])
|
||||
{:label (str (or (.-name clip-json) cid) " (saved)")
|
||||
:cid cid
|
||||
:display-fps (:fps scene)
|
||||
:scene scene
|
||||
:store (:store clip)
|
||||
:frames (clip/frames built)
|
||||
:display-fps (:fps built)
|
||||
:clip built
|
||||
:store (:store loaded')
|
||||
;; The audio is the clip's, and a
|
||||
;; document does not carry it: tier 3
|
||||
;; is by hash and the scene names the
|
||||
;; is by hash and the clip names the
|
||||
;; analysis, not the sound. Until the
|
||||
;; footage id is in the document, the
|
||||
;; synthetic take's is the one that
|
||||
|
|
@ -146,7 +148,7 @@
|
|||
(rf/reg-event-fx
|
||||
::save
|
||||
(fn [{:keys [db]} _]
|
||||
(let [id (:scene/current db)
|
||||
(let [id (:clip/current db)
|
||||
clip (store/entry id)]
|
||||
(if (or (:busy? (:project db)) (nil? clip))
|
||||
{}
|
||||
|
|
@ -179,7 +181,7 @@
|
|||
(fn [{:keys [db]} [_ clip-id project-id name seq]]
|
||||
(let [clip (store/entry clip-id)]
|
||||
{:db (-> db
|
||||
(assoc :scene/current clip-id
|
||||
(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)
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@
|
|||
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.geom :as geom]
|
||||
[arthur.domain.ring :as ring]
|
||||
[arthur.flow.address :as address]))
|
||||
|
|
@ -276,11 +277,11 @@
|
|||
vectors. It may not store what `value-at` handed it: a wide dense read is a
|
||||
VIEW into the block, and a view into tier 2 sitting in the document is a value
|
||||
that changes when a re-freeze rewrites the array under it."
|
||||
[{:keys [mode kept]} {:keys [scene store]}]
|
||||
[{:keys [mode kept]} {:keys [clip store]}]
|
||||
(when-not (contains? head-modes mode)
|
||||
(throw (ex-info "head mode is not one of the three channel shapes"
|
||||
{:mode mode :modes head-modes})))
|
||||
(let [base (get-in scene [:nodes :head :measured])
|
||||
(let [base (get-in (clip/root clip) [:nodes :head :measured])
|
||||
at (fn [path f]
|
||||
(let [v (ch/value-at (get base path) f store)
|
||||
n (:stride (:dense (get base path)))]
|
||||
|
|
@ -292,7 +293,7 @@
|
|||
(when (and (= mode :per-plate) (empty? kept))
|
||||
(throw (ex-info "the per-plate head mode needs a kept-frame set; it is the plate strip's, not measurement's"
|
||||
{:mode mode})))
|
||||
(assoc-in scene [:nodes :head :channels]
|
||||
(assoc-in clip [:timelines clip/root-id :nodes :head :channels]
|
||||
(case mode
|
||||
;; No `:generated` on the locked shape, and that is not an
|
||||
;; oversight: nothing generated this identity. It is a decision,
|
||||
|
|
@ -655,8 +656,7 @@
|
|||
roto (fn [by] (prov by {:verts verts :contour-avg contour-avg}))
|
||||
features (when (and eyes brows) (feature-parts absent? obs params inputs))
|
||||
interior (when teeth (interior-part params absent? obs teeth))
|
||||
scene {:name name
|
||||
:frames nf
|
||||
built {:name name
|
||||
:fps fps
|
||||
;; Tier 1 says which analysis its channels came out of, in full.
|
||||
;; The id alone would make the document unreadable the first time
|
||||
|
|
@ -688,8 +688,16 @@
|
|||
;; face-placement: nothing below here knows the frame size.
|
||||
:width (first stage)
|
||||
:height (second stage)
|
||||
:nodes
|
||||
(merge
|
||||
;; ONE TIMELINE, and the frame count is ITS. A clip is a rate and
|
||||
;; a timeline is a frame space — see arthur.domain.clip — so `nf`
|
||||
;; lands here and `:fps` above, and the library a symbol will live
|
||||
;; in is this same map with a second entry.
|
||||
:timelines
|
||||
{clip/root-id
|
||||
{:id clip/root-id
|
||||
:frames nf
|
||||
:nodes
|
||||
(merge
|
||||
{:root
|
||||
{:id :root :name "clip" :kind :group :parent nil :z "a1"
|
||||
:time {:mode :map :expose expose}}
|
||||
|
|
@ -721,7 +729,7 @@
|
|||
[:vis] (visibility params inputs
|
||||
(prov :roto/mouth-aperture
|
||||
{:aperture-cut aperture-cut}))}}}
|
||||
(:nodes features) (:nodes interior))}
|
||||
(:nodes features) (:nodes interior))}}}
|
||||
;; Tier 2, behind a handle, and now behind a content address: every key is
|
||||
;; a sha256 over the analysis, the settings and the absence data that
|
||||
;; produced the bytes under it. Nothing above this line changed when they
|
||||
|
|
@ -729,8 +737,8 @@
|
|||
store (merge (stored rings pos rot scale)
|
||||
(:store features) (:store interior))]
|
||||
(doseq [id (keys presence)]
|
||||
(when-not (contains? (:features scene) id)
|
||||
(throw (ex-info "presence track names no feature in this scene"
|
||||
{:feature id :features (keys (:features scene))}))))
|
||||
(when-not (contains? (:features built) id)
|
||||
(throw (ex-info "presence track names no feature in this clip"
|
||||
{:feature id :features (keys (:features built))}))))
|
||||
{:store store
|
||||
:scene (head-mode {:mode head :kept kept} {:scene scene :store store})}))
|
||||
:clip (head-mode {:mode head :kept kept} {:clip built :store store})}))
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@
|
|||
|
||||
Two things arrive this way and they are the same kind of thing: footage that has
|
||||
been detected and frozen, and a project opened from the server. Both are
|
||||
`{:scene ... :store ...}` — which is what `flow/freeze` returns — plus the clip
|
||||
facts the transport needs, and both hold typed arrays that have no business being
|
||||
in a map every mounted subscription compares."
|
||||
`{:clip ... :store ...}` — which is what `flow/freeze` returns — plus what the
|
||||
transport reads off the clip, and both hold typed arrays that have no business
|
||||
being in a map every mounted subscription compares."
|
||||
(:require [arthur.db :as db]))
|
||||
|
||||
(defonce ^:private loaded (atom nil))
|
||||
|
|
@ -27,4 +27,4 @@
|
|||
(defn entry [id]
|
||||
(if (= id (:id @loaded))
|
||||
@loaded
|
||||
(get db/scenes id)))
|
||||
(get db/clips id)))
|
||||
|
|
|
|||
|
|
@ -7,47 +7,55 @@
|
|||
CLOSURE that produces geometry at a frame. So a scene edit costs one
|
||||
recomputation here and a frame costs a lookup and a blit — and, crucially, the
|
||||
playhead is not an input, so moving it cannot invalidate this."
|
||||
(:require [arthur.domain.palette :as pal]
|
||||
[arthur.domain.scene :as scene]
|
||||
(: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]))
|
||||
|
||||
(rf/reg-sub ::scene-id (fn [db _] (:scene/current db)))
|
||||
(rf/reg-sub ::clip-id (fn [db _] (:clip/current db)))
|
||||
|
||||
(rf/reg-sub
|
||||
::base-scene
|
||||
:<- [::scene-id]
|
||||
(fn [id _] (:scene (footage/entry id))))
|
||||
::clip
|
||||
:<- [::clip-id]
|
||||
(fn [id _] (:clip (footage/entry id))))
|
||||
|
||||
(rf/reg-sub
|
||||
::scene
|
||||
:<- [::base-scene]
|
||||
::timeline
|
||||
:<- [::clip]
|
||||
:<- [::playback/display-fps]
|
||||
(fn [[scene picture-fps] _]
|
||||
;; This changes only the scene's root time map. The dense source track stays
|
||||
;; at its native rate, and the audio clock still advances through source time.
|
||||
(if (and scene picture-fps (< picture-fps (:fps scene)))
|
||||
(-> scene
|
||||
(assoc-in [:nodes :root :time :source-fps] (:fps scene))
|
||||
(assoc-in [:nodes :root :time :sample-fps] picture-fps))
|
||||
scene)))
|
||||
(fn [[clip picture-fps] _]
|
||||
;; The ROOT timeline, with picture sampling written onto its root node's time
|
||||
;; map. Only that node's time map changes: the dense source track stays at its
|
||||
;; native rate and the audio clock still advances through source time.
|
||||
;;
|
||||
;; `:fps` is read off the CLIP and `:frames` off the timeline, which is the
|
||||
;; whole reason the two came apart — the source cadence is a fact about how fast
|
||||
;; the clip plays against its audio, and a nested timeline will not have one.
|
||||
(when-let [tl (clip/root clip)]
|
||||
(if (and picture-fps (< picture-fps (:fps clip)))
|
||||
(-> tl
|
||||
(assoc-in [:nodes :root :time :source-fps] (:fps clip))
|
||||
(assoc-in [:nodes :root :time :sample-fps] picture-fps))
|
||||
tl))))
|
||||
|
||||
(rf/reg-sub
|
||||
::exposure
|
||||
:<- [::scene]
|
||||
(fn [scene _]
|
||||
;; Exposure lives on the clip root 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 scene.
|
||||
(or (get-in scene [:nodes :root :time :expose]) 1)))
|
||||
:<- [::timeline]
|
||||
(fn [tl _]
|
||||
;; Exposure lives on the timeline'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)))
|
||||
|
||||
(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/scene takes the palette as a parameter rather
|
||||
;; scope, which is why domain/timeline takes the palette as a parameter rather
|
||||
;; than reaching for a global.
|
||||
(get {:arthur/default pal/index-of} (:palette db) pal/index-of)))
|
||||
|
||||
|
|
@ -62,7 +70,7 @@
|
|||
|
||||
(rf/reg-sub
|
||||
::store
|
||||
:<- [::scene-id]
|
||||
:<- [::clip-id]
|
||||
(fn [id _]
|
||||
;; Tier 2, behind a handle, and never in app-db itself — what is in the db is
|
||||
;; the id of the clip whose blocks these are. The hand-written demo has none;
|
||||
|
|
@ -71,8 +79,8 @@
|
|||
|
||||
(rf/reg-sub
|
||||
::resolver
|
||||
:<- [::scene]
|
||||
:<- [::timeline]
|
||||
:<- [::store]
|
||||
:<- [::palette]
|
||||
(fn [[scene store palette] _]
|
||||
(scene/resolver scene store palette)))
|
||||
(fn [[tl store palette] _]
|
||||
(when tl (timeline/resolver tl store palette))))
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
canvas from an animation frame; it is a sink, not a view that re-renders, and
|
||||
the actual content of the folklore about re-frame and canvas is that expensive
|
||||
work must not live in a layer-2 sub. The resolver is a layer-3 sub over the
|
||||
scene, so the playhead moving cannot invalidate it.
|
||||
timeline, so the playhead moving cannot invalidate it.
|
||||
|
||||
The one dispatch is `::playback/tick`, and it is deliberately NOT what the
|
||||
picture waits on: the frame is painted from the clock directly, and the tick
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@
|
|||
frames @(rf/subscribe [::sub/frames])
|
||||
fps @(rf/subscribe [::sub/fps])
|
||||
picture-fps @(rf/subscribe [::sub/display-fps])
|
||||
current @(rf/subscribe [::render/scene-id])
|
||||
current @(rf/subscribe [::render/clip-id])
|
||||
expose @(rf/subscribe [::render/exposure])
|
||||
{:keys [id label loading? status available chosen]} @(rf/subscribe [::sub/footage])
|
||||
{project-name :name :keys [busy?] project-status :status
|
||||
|
|
@ -55,14 +55,14 @@
|
|||
:on-click #(rf/dispatch [::pb/toggle-mute])} "mute"]
|
||||
[:span.gap]
|
||||
(doall
|
||||
(for [[id {:keys [label]}] db/scenes]
|
||||
(for [[id {:keys [label]}] db/clips]
|
||||
^{:key id}
|
||||
[:button {:class (when (= id @(rf/subscribe [::render/scene-id])) "on")
|
||||
:on-click #(rf/dispatch [::pb/select-scene id])}
|
||||
[:button {:class (when (= id @(rf/subscribe [::render/clip-id])) "on")
|
||||
:on-click #(rf/dispatch [::pb/select-clip id])}
|
||||
label]))
|
||||
(when id
|
||||
[:button {:class (when (= id @(rf/subscribe [::render/scene-id])) "on")
|
||||
:on-click #(rf/dispatch [::pb/select-scene id])}
|
||||
[:button {:class (when (= id @(rf/subscribe [::render/clip-id])) "on")
|
||||
:on-click #(rf/dispatch [::pb/select-clip id])}
|
||||
(or label "footage")])
|
||||
[:button {:disabled (or loading? (nil? chosen))
|
||||
:on-click #(rf/dispatch [::footage/load])}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue