grand unification of time
This commit is contained in:
parent
a4ce750be2
commit
05878ca48b
40 changed files with 334 additions and 292 deletions
1
.claude/worktrees/unified-time-selection
Submodule
1
.claude/worktrees/unified-time-selection
Submodule
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit e369ad9462ed1816a39b1a0f5c79055a2c013f4f
|
||||||
|
|
@ -52,10 +52,10 @@ overrides and kept-frame sets are all in clip-frame space, so a clip slides on
|
||||||
the timeline without a single stored number changing. Exposure and lead are
|
the timeline without a single stored number changing. Exposure and lead are
|
||||||
transforms *within* clip space:
|
transforms *within* clip space:
|
||||||
|
|
||||||
Detection retains every source frame. A chosen picture fps samples the frozen
|
Detection retains every source frame. Symbols carry their native fps; project
|
||||||
roto in clip time; it changes neither source-frame count nor the audio clock.
|
fps selects the output grid without changing source data or audio speed.
|
||||||
The set of source frames an artist uses as cel tracing references is another
|
See [Time selection](time.md) for boundary sampling and frame units.
|
||||||
selection, independent of the picture fps.
|
Tracing references remain an independent selection.
|
||||||
|
|
||||||
```clojure
|
```clojure
|
||||||
(defn pose-frame [clip cf]
|
(defn pose-frame [clip cf]
|
||||||
|
|
@ -90,8 +90,8 @@ it a name is most of the work: `state` in `app.js` is a clip with its analysis
|
||||||
inlined and its palette global.
|
inlined and its palette global.
|
||||||
|
|
||||||
Cel keys select where drawings begin and how long they hold. The source frames
|
Cel keys select where drawings begin and how long they hold. The source frames
|
||||||
shown beneath a cel while tracing are chosen independently, and picture fps
|
shown beneath a cel while tracing are chosen independently. Project fps controls
|
||||||
only controls which analyzed pose the finished roto displays at a given time.
|
which native frames can appear on the output grid.
|
||||||
|
|
||||||
### Two things called "track"
|
### Two things called "track"
|
||||||
|
|
||||||
|
|
|
||||||
36
docs/time.md
Normal file
36
docs/time.md
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
# Time selection
|
||||||
|
|
||||||
|
Project `:fps` is the playback and export grid. Each symbol has its own native
|
||||||
|
`:fps` and `:frames`; keys, spans, trace choices and corrections stay in that
|
||||||
|
native space. A symbol without an explicit rate inherits the document rate;
|
||||||
|
changing project fps first records that rate so its existing timing stays put.
|
||||||
|
|
||||||
|
An output frame selects the latest native frame at or before its time:
|
||||||
|
`floor(output-frame * native-fps / output-fps)`. Thus 30fps content in a 12fps
|
||||||
|
project reads source frames 0, 2, 5, 7, 10… and retains its duration. A partial
|
||||||
|
last output frame is included. Changing back to 30 restores the original grid.
|
||||||
|
Nothing rewrites or discards the dense measurements.
|
||||||
|
|
||||||
|
The same boundary selection runs when entering a placed symbol. Placement and
|
||||||
|
artistic speed are applied before selection; the stored `:time :rate` and
|
||||||
|
`:playback :speed` never contain a frame-rate conversion. The derived maps used
|
||||||
|
by timeline rows, picking and editing account for the units of each symbol.
|
||||||
|
`clip/frames` is a native length; `clip/output-frames` is a transport/export
|
||||||
|
length. Resolver frame queries return native frames for edits.
|
||||||
|
|
||||||
|
There is one fps control. The old transient picture-fps control and node
|
||||||
|
sample-fps fields are gone. Existing exposure, trace choices and per-instance
|
||||||
|
pose tracks remain available: a pose track can hold a chosen closed-mouth frame
|
||||||
|
without deleting its neighboring measurements. Those choices stay in native
|
||||||
|
frames when output fps changes. Automatic content-aware frame selection is not
|
||||||
|
implemented. An event between output frames appears on the next output frame;
|
||||||
|
it cannot create an extra frame in a 12fps output.
|
||||||
|
|
||||||
|
Audio uses continuous time through the same derived placement maps, without
|
||||||
|
picture floors or holds. Frame-rate units cancel before Web Audio playbackRate
|
||||||
|
is set, so only deliberate speed changes affect pitch and duration. Export and
|
||||||
|
playback use the same output count and resolver.
|
||||||
|
|
||||||
|
Earlier imports with frame-rate conversion baked into stored retimes must be
|
||||||
|
re-imported. There is no second reader for that representation. Source video
|
||||||
|
presentation timestamps are still future work; this model assumes constant fps.
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
# Timing model
|
# Timing model
|
||||||
|
|
||||||
|
[Time selection](time.md) defines the current frame-rate representation.
|
||||||
|
|
||||||
[The Lane Model](lane-model.md) defines the revised target for occurrence timing,
|
[The Lane Model](lane-model.md) defines the revised target for occurrence timing,
|
||||||
source playback, sampling scope, and inverse editing. It supersedes conflicting
|
source playback, sampling scope, and inverse editing. It supersedes conflicting
|
||||||
proposals here; the sections below describe earlier implementation decisions.
|
proposals here; the sections below describe earlier implementation decisions.
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,7 @@
|
||||||
|
|
||||||
(defn- render! [document sid sources store]
|
(defn- render! [document sid sources store]
|
||||||
(let [fps (:fps document)
|
(let [fps (:fps document)
|
||||||
frames (:frames (clip/symbol document sid))
|
frames (clip/output-frames document sid)
|
||||||
tracks (tracks-of document sid)
|
tracks (tracks-of document sid)
|
||||||
output (js/OfflineAudioContext.
|
output (js/OfflineAudioContext.
|
||||||
2 (js/Math.ceil (* (/ frames fps) 44100)) 44100)]
|
2 (js/Math.ceil (* (/ frames fps) 44100)) 44100)]
|
||||||
|
|
@ -121,7 +121,7 @@
|
||||||
(let [[start end] (or (node/placed-span track) [0 frames])
|
(let [[start end] (or (node/placed-span track) [0 frames])
|
||||||
start (max 0 start)
|
start (max 0 start)
|
||||||
end (min frames end)
|
end (min frames end)
|
||||||
{:keys [buffer fps] :or {fps (:fps document)}} (get sources (:source track))
|
{:keys [buffer fps] :or {fps (:fps track)}} (get sources (:source track))
|
||||||
sound (.createBufferSource output)
|
sound (.createBufferSource output)
|
||||||
gain (.createGain output)
|
gain (.createGain output)
|
||||||
pan (.createStereoPanner output)]
|
pan (.createStereoPanner output)]
|
||||||
|
|
@ -130,7 +130,7 @@
|
||||||
(set! (.-loop sound) (boolean (get-in track [:time :loop?])))
|
(set! (.-loop sound) (boolean (get-in track [:time :loop?])))
|
||||||
(automate! (.-playbackRate sound)
|
(automate! (.-playbackRate sound)
|
||||||
(get-in track [:channels [:audio :rate]])
|
(get-in track [:channels [:audio :rate]])
|
||||||
start end (:fps document) (or (get-in track [:time :rate]) 1) 1 store)
|
start end (:fps document) (* (or (get-in track [:time :rate]) 1) (/ (:fps document) fps)) 1 store)
|
||||||
(automate! (.-gain gain)
|
(automate! (.-gain gain)
|
||||||
(get-in track [:channels [:audio :gain]])
|
(get-in track [:channels [:audio :gain]])
|
||||||
start end (:fps document) 1 1 store)
|
start end (:fps document) 1 1 store)
|
||||||
|
|
@ -195,6 +195,6 @@
|
||||||
buffer (wav-url buffer)
|
buffer (wav-url buffer)
|
||||||
(and fallback-url (= sid (clip/opens-on document))) fallback-url
|
(and fallback-url (= sid (clip/opens-on document))) fallback-url
|
||||||
:else (let [rate 44100
|
:else (let [rate 44100
|
||||||
n (max 1 (js/Math.ceil (* rate (/ (clip/frames document sid)
|
n (max 1 (js/Math.ceil (* rate (/ (clip/output-frames document sid)
|
||||||
(:fps document)))))]
|
(:fps document)))))]
|
||||||
(wav-url (.createBuffer (js/OfflineAudioContext. 1 1 rate) 1 n rate))))))))
|
(wav-url (.createBuffer (js/OfflineAudioContext. 1 1 rate) 1 n rate))))))))
|
||||||
|
|
|
||||||
|
|
@ -97,8 +97,3 @@
|
||||||
rather than only inside the scene."
|
rather than only inside the scene."
|
||||||
[f expose]
|
[f expose]
|
||||||
(node/expose f expose))
|
(node/expose f expose))
|
||||||
|
|
||||||
(defn picture-frame
|
|
||||||
"The source pose displayed at f after picture-rate sampling and exposure."
|
|
||||||
[f source-fps picture-fps expose]
|
|
||||||
(node/expose (node/sample-frame f source-fps picture-fps) expose))
|
|
||||||
|
|
|
||||||
|
|
@ -30,8 +30,7 @@
|
||||||
;; serves by hash — and the synthetic take needs a sound of its own so
|
;; 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.
|
;; that the clock has something to run against with no footage ingested.
|
||||||
:audio "/static/arthur/audio.wav"
|
:audio "/static/arthur/audio.wav"
|
||||||
:cid (name label-key)
|
:cid (name label-key)}
|
||||||
:display-fps (:fps clip)}
|
|
||||||
(select-keys clip [:fps :width :height])))
|
(select-keys clip [:fps :width :height])))
|
||||||
|
|
||||||
(def clips
|
(def clips
|
||||||
|
|
@ -72,7 +71,7 @@
|
||||||
:clip (let [c (domain-clip/blank)]
|
:clip (let [c (domain-clip/blank)]
|
||||||
{:fps (:fps c)
|
{:fps (:fps c)
|
||||||
:width (:width c) :height (:height c)
|
:width (:width c) :height (:height c)
|
||||||
:audio nil :display-fps (:fps c)})
|
:audio nil})
|
||||||
|
|
||||||
;; Which ingested footage to detect, and what the last load said. The list
|
;; 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
|
;; comes from the server — tier 3 is the backend's since step 9 — so there is
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@
|
||||||
n))
|
n))
|
||||||
copy (fn [sid]
|
copy (fn [sid]
|
||||||
(-> (clip/symbol other sid)
|
(-> (clip/symbol other sid)
|
||||||
(assoc :id (ids sid))
|
(assoc :id (ids sid) :fps (or (:fps (clip/symbol other sid)) (:fps other)))
|
||||||
(update :nodes #(into {} (map (fn [[id n]] [id (repoint n ids)])) %))))]
|
(update :nodes #(into {} (map (fn [[id n]] [id (repoint n ids)])) %))))]
|
||||||
{:clip (reduce (fn [c sid] (assoc-in c [:symbols (ids sid)] (copy sid))) clip reach)
|
{:clip (reduce (fn [c sid] (assoc-in c [:symbols (ids sid)] (copy sid))) clip reach)
|
||||||
:ids ids}))
|
:ids ids}))
|
||||||
|
|
@ -76,21 +76,6 @@
|
||||||
[clip frozen label footage-id range]
|
[clip frozen label footage-id range]
|
||||||
(let [{c :clip ids :ids} (symbols clip frozen [:main] {:main (symbol-id label)})
|
(let [{c :clip ids :ids} (symbols clip frozen [:main] {:main (symbol-id label)})
|
||||||
sid (ids :main)
|
sid (ids :main)
|
||||||
source-fps (:fps frozen)
|
|
||||||
project-fps (:fps clip)
|
|
||||||
;; The new symbol is played by the receiving project's clock. Keep its
|
|
||||||
;; wall-clock duration by giving it project-rate frames, while its root
|
|
||||||
;; maps those frames back onto the source timeline. A 30fps source in a
|
|
||||||
;; 12fps project therefore has 2.5 source frames per project frame.
|
|
||||||
source-rate (if (and (number? source-fps) (pos? source-fps)
|
|
||||||
(number? project-fps) (pos? project-fps))
|
|
||||||
(/ source-fps project-fps)
|
|
||||||
1)
|
|
||||||
output-frames (max 1 (js/Math.ceil (/ (clip/frames c sid) source-rate)))
|
|
||||||
c (-> c
|
|
||||||
(assoc-in [:symbols sid :frames] output-frames)
|
|
||||||
(update-in [:symbols sid :nodes :root :time]
|
|
||||||
#(assoc (or % {}) :mode :map :at 0 :rate source-rate)))
|
|
||||||
tracked? (and (nil? (:analysis clip))
|
tracked? (and (nil? (:analysis clip))
|
||||||
(every? #(= % (ids %)) (keys (:subjects frozen))))]
|
(every? #(= % (ids %)) (keys (:subjects frozen))))]
|
||||||
{:sid sid
|
{:sid sid
|
||||||
|
|
@ -103,8 +88,8 @@
|
||||||
;; Source frame `start` plays on the symbol's 0.
|
;; Source frame `start` plays on the symbol's 0.
|
||||||
:span range
|
:span range
|
||||||
:time {:mode :map
|
:time {:mode :map
|
||||||
:at (/ (- (first range)) source-rate)
|
:at (- (first range))
|
||||||
:rate source-rate}}))
|
:rate 1}}))
|
||||||
tracked? (-> (assoc :analysis (:analysis frozen))
|
tracked? (-> (assoc :analysis (:analysis frozen))
|
||||||
(update :subjects merge (:subjects frozen))
|
(update :subjects merge (:subjects frozen))
|
||||||
(update :features merge (:features frozen))
|
(update :features merge (:features frozen))
|
||||||
|
|
@ -118,4 +103,5 @@
|
||||||
when there is none. See `clip/place-symbol`."
|
when there is none. See `clip/place-symbol`."
|
||||||
[entry brought store sid host frame uuid point]
|
[entry brought store sid host frame uuid point]
|
||||||
(let [st (merge (:store entry) store)]
|
(let [st (merge (:store entry) store)]
|
||||||
(assoc entry :store st :clip (clip/place-symbol brought st host sid frame uuid point))))
|
(assoc entry :store st :clip (clip/place-symbol brought st host sid
|
||||||
|
(* frame (:rate (clip/grid-time brought host))) uuid point))))
|
||||||
|
|
|
||||||
15
frontend/src/arthur/domain/cadence.cljs
Normal file
15
frontend/src/arthur/domain/cadence.cljs
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
(ns arthur.domain.cadence
|
||||||
|
"Select integer content frames across frame grids. Stored frames never change.")
|
||||||
|
|
||||||
|
(defn ratio [grid native]
|
||||||
|
(if (and grid native (pos? grid) (pos? native)) (/ native grid) 1))
|
||||||
|
|
||||||
|
(defn frame
|
||||||
|
"Latest native frame at or before reader frame f. Never sample the future."
|
||||||
|
[f grid native]
|
||||||
|
(js/Math.floor (if (and grid native) (/ (* f native) grid) f)))
|
||||||
|
|
||||||
|
(defn frames
|
||||||
|
"Reader frames covering a native length, including a partial final frame."
|
||||||
|
[n grid native]
|
||||||
|
(when n (max 1 (js/Math.ceil (/ n (ratio grid native))))))
|
||||||
|
|
@ -33,12 +33,11 @@
|
||||||
them. `unplaced` answers the question a reserved root used to: which symbols
|
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.
|
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
|
:fps is the output grid. A symbol's optional :fps names the native grid its
|
||||||
against its audio, and a nested symbol cannot have one of its own — retiming an
|
frames were authored or measured on; absent means the document's grid."
|
||||||
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 symbol has its own."
|
|
||||||
(:refer-clojure :exclude [symbol])
|
(:refer-clojure :exclude [symbol])
|
||||||
(:require [arthur.domain.channel :as ch]
|
(:require [arthur.domain.cadence :as cadence]
|
||||||
|
[arthur.domain.channel :as ch]
|
||||||
[arthur.domain.feature :as feature]
|
[arthur.domain.feature :as feature]
|
||||||
[arthur.domain.node :as node]
|
[arthur.domain.node :as node]
|
||||||
[arthur.domain.palette :as pal]
|
[arthur.domain.palette :as pal]
|
||||||
|
|
@ -69,6 +68,36 @@
|
||||||
[clip sid]
|
[clip sid]
|
||||||
(:frames (symbol clip sid)))
|
(:frames (symbol clip sid)))
|
||||||
|
|
||||||
|
(defn fps [clip sid] (or (:fps (symbol clip sid)) (:fps clip)))
|
||||||
|
|
||||||
|
(defn set-fps
|
||||||
|
"Change the output grid without rewriting any content's frames."
|
||||||
|
[clip rate]
|
||||||
|
(-> clip
|
||||||
|
(update :symbols #(into {} (map (fn [[sid sym]]
|
||||||
|
[sid (assoc sym :fps (fps clip sid))])) %))
|
||||||
|
(assoc :fps rate)))
|
||||||
|
|
||||||
|
(defn output-frames [clip sid]
|
||||||
|
(cadence/frames (frames clip sid) (:fps clip) (fps clip sid)))
|
||||||
|
|
||||||
|
(defn grid-time [clip sid]
|
||||||
|
{:at 0 :rate (cadence/ratio (:fps clip) (fps clip sid))})
|
||||||
|
|
||||||
|
(defn source-time
|
||||||
|
"Derived cel-to-content map. Frame-rate units never enter stored retimes."
|
||||||
|
[clip host n]
|
||||||
|
(when-let [t (node/source-time n)]
|
||||||
|
(let [r (cadence/ratio (fps clip host) (fps clip (node/source n)))]
|
||||||
|
(-> t (update :rate * r) (update :at / r)))))
|
||||||
|
|
||||||
|
(defn placed-frame [clip host n f]
|
||||||
|
(let [child (node/source n)
|
||||||
|
r (cadence/ratio (fps clip host) (fps clip child))]
|
||||||
|
(some-> (node/placed-frame (update-in n [:playback :speed] #(* (or % 1) r))
|
||||||
|
f (frames clip child))
|
||||||
|
(update :frame js/Math.floor))))
|
||||||
|
|
||||||
(defn stage
|
(defn stage
|
||||||
"A symbol's stage as `[width height]`: its own, or the clip's where it has none.
|
"A symbol's stage as `[width height]`: its own, or the clip's where it has none.
|
||||||
Absent rather than copied in at creation, so a symbol nobody has sized follows
|
Absent rather than copied in at creation, so a symbol nobody has sized follows
|
||||||
|
|
@ -136,7 +165,7 @@
|
||||||
{:name "untitled"
|
{:name "untitled"
|
||||||
:fps 30
|
:fps 30
|
||||||
:width 320 :height 200
|
:width 320 :height 200
|
||||||
:symbols {:main {:id :main :frames blank-frames :nodes {}}}})
|
:symbols {:main {:id :main :fps 30 :frames blank-frames :nodes {}}}})
|
||||||
|
|
||||||
(defn- transform-op
|
(defn- transform-op
|
||||||
"Put a symbol's already resolved mark into its instance's parent space. Its
|
"Put a symbol's already resolved mark into its instance's parent space. Its
|
||||||
|
|
@ -162,23 +191,10 @@
|
||||||
op)))
|
op)))
|
||||||
|
|
||||||
(defn resolver
|
(defn resolver
|
||||||
"Resolve symbol `sid` of a clip, including every symbol its instances place.
|
"Resolve an output frame, selecting native content at each symbol boundary.
|
||||||
|
Every instance owns its cursors and buffers. The IResolver queries return
|
||||||
Each instance owns its own symbol resolver, so two offsets never share a
|
native node frames and world matrices for the last rendered output frame."
|
||||||
channel cursor or point buffer. The returned ops must be drawn before the next
|
[clip sid store palette opts]
|
||||||
frame, as with symbol/resolver.
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
It answers `symbol/world-of` and `symbol/frame-of` for a ROW PATH — the ids
|
|
||||||
from `sid` down through instances, as a timeline row names a node — as of the
|
|
||||||
frame it last resolved: the matrix into `sid`'s coordinates, and the node's
|
|
||||||
own frame. Nil for a node that was not on that frame. It is how something
|
|
||||||
drawn beside the picture, like a tracing photo, rides a node inside it without
|
|
||||||
resolving anything a second time."
|
|
||||||
[clip sid store palette {:keys [picture-fps] :as opts}]
|
|
||||||
(letfn [(build [sid chain pose-tracks]
|
(letfn [(build [sid chain pose-tracks]
|
||||||
(when (some #{sid} chain)
|
(when (some #{sid} chain)
|
||||||
(throw (ex-info "symbol cycle" {:chain (conj chain sid)})))
|
(throw (ex-info "symbol cycle" {:chain (conj chain sid)})))
|
||||||
|
|
@ -188,8 +204,7 @@
|
||||||
rank (symbol/draw-rank nodes (symbol/order nodes))
|
rank (symbol/draw-rank nodes (symbol/order nodes))
|
||||||
ids (sort-by rank (keys nodes))
|
ids (sort-by rank (keys nodes))
|
||||||
own (symbol/resolver sym store palette
|
own (symbol/resolver sym store palette
|
||||||
(assoc opts :pose-tracks pose-tracks
|
(assoc opts :pose-tracks pose-tracks))
|
||||||
:source-fps (:fps clip)))
|
|
||||||
;; Each cel owns its source resolver and mutable buffers.
|
;; Each cel owns its source resolver and mutable buffers.
|
||||||
children (into {}
|
children (into {}
|
||||||
(for [[id n] nodes
|
(for [[id n] nodes
|
||||||
|
|
@ -205,7 +220,7 @@
|
||||||
entered (volatile! {})
|
entered (volatile! {})
|
||||||
step (fn [f]
|
step (fn [f]
|
||||||
(vreset! entered {})
|
(vreset! entered {})
|
||||||
(let [by-id (into {} (map (juxt :node identity)) (own f))]
|
(let [by-id (into {} (map (juxt :node identity)) (own (js/Math.floor f)))]
|
||||||
(into []
|
(into []
|
||||||
(mapcat
|
(mapcat
|
||||||
(fn [id]
|
(fn [id]
|
||||||
|
|
@ -215,7 +230,7 @@
|
||||||
local (symbol/frame-of own id)
|
local (symbol/frame-of own id)
|
||||||
length (frames clip (node/source n))
|
length (frames clip (node/source n))
|
||||||
shown (when (and m (number? local))
|
shown (when (and m (number? local))
|
||||||
(node/placed-frame n local length))
|
(placed-frame clip sid n local))
|
||||||
frame (:frame shown)]
|
frame (:frame shown)]
|
||||||
(if (and frame (<= 0 frame) (< frame length))
|
(if (and frame (<= 0 frame) (< frame length))
|
||||||
(do (vswap! entered assoc id (:symbol shown))
|
(do (vswap! entered assoc id (:symbol shown))
|
||||||
|
|
@ -240,7 +255,13 @@
|
||||||
(when (contains? @entered id)
|
(when (contains? @entered id)
|
||||||
(symbol/frame-of (get children [id (get @entered id)]) (vec more)))
|
(symbol/frame-of (get children [id (get @entered id)]) (vec more)))
|
||||||
(symbol/frame-of own id))))))]
|
(symbol/frame-of own id))))))]
|
||||||
(build sid [] nil)))
|
(let [r (build sid [] nil)]
|
||||||
|
(reify
|
||||||
|
IFn
|
||||||
|
(-invoke [_ f] (r (cadence/frame f (or (:grid-fps opts) (:fps clip)) (fps clip sid))))
|
||||||
|
symbol/IResolver
|
||||||
|
(world-of [_ path] (symbol/world-of r path))
|
||||||
|
(frame-of [_ path] (symbol/frame-of r path))))))
|
||||||
|
|
||||||
(defn center
|
(defn center
|
||||||
"The middle of everything symbol `sid` draws, over all its frames, in its own
|
"The middle of everything symbol `sid` draws, over all its frames, in its own
|
||||||
|
|
@ -254,7 +275,7 @@
|
||||||
Effects' anchor point are set once and left. A symbol that grows later keeps
|
Effects' anchor point are set once and left. A symbol that grows later keeps
|
||||||
its instances' pivots where they were, so nothing on screen moves."
|
its instances' pivots where they were, so nothing on screen moves."
|
||||||
[clip store sid]
|
[clip store sid]
|
||||||
(let [resolve (resolver clip sid store pal/index-of nil)
|
(let [resolve (resolver clip sid store pal/index-of {:grid-fps (fps clip sid)})
|
||||||
bounds (fn [[x0 y0 x1 y1 :as b] x y]
|
bounds (fn [[x0 y0 x1 y1 :as b] x y]
|
||||||
(if b [(min x0 x) (min y0 y) (max x1 x) (max y1 y)] [x y x y]))
|
(if b [(min x0 x) (min y0 y) (max x1 x) (max y1 y)] [x y x y]))
|
||||||
[x0 y0 x1 y1]
|
[x0 y0 x1 y1]
|
||||||
|
|
@ -310,7 +331,7 @@
|
||||||
;; Lexicographic draw order, as `domain/paint` does it: an instance made
|
;; Lexicographic draw order, as `domain/paint` does it: an instance made
|
||||||
;; later sits above one made earlier, and neither has to renumber.
|
;; later sits above one made earlier, and neither has to renumber.
|
||||||
:z (str "z" (js/Date.now) "-" (name sid))
|
:z (str "z" (js/Date.now) "-" (name sid))
|
||||||
:span [0 (:frames target)]
|
:span [0 (cadence/frames (:frames target) (fps clip host) (fps clip sid))]
|
||||||
:time {:mode :map :at frame :rate 1}
|
:time {:mode :map :at frame :rate 1}
|
||||||
:source {:symbol sid}
|
:source {:symbol sid}
|
||||||
:playback {:in 0 :speed 1 :end :stop}
|
:playback {:in 0 :speed 1 :end :stop}
|
||||||
|
|
@ -319,11 +340,8 @@
|
||||||
[:xform :anchor] {:animated? false :value middle}}})))))
|
[:xform :anchor] {:animated? false :value middle}}})))))
|
||||||
|
|
||||||
(defn place-sound
|
(defn place-sound
|
||||||
"An audio node playing `source` — `{:sound id}`, an uploaded file, or
|
"Place a sound at host frame `frame`. Its span uses `source`'s fps when
|
||||||
`{:footage id}`, a video's own sound — inside symbol `host` from `frame` of it.
|
supplied, otherwise the host's. `rate` is a deliberate playback speed."
|
||||||
Timed as an instance is: `:at frame`, its span `length` of its OWN frames, and
|
|
||||||
`rate` of those to one of `host`'s, which is what keeps a 30fps video's sound
|
|
||||||
its own length in a 12fps project. Nothing else: a sound is not in the picture."
|
|
||||||
[clip host source label length rate frame uuid]
|
[clip host source label length rate frame uuid]
|
||||||
(let [end (frames clip host)]
|
(let [end (frames clip host)]
|
||||||
(if (or (nil? end) (nil? frame) (neg? frame) (>= frame end))
|
(if (or (nil? end) (nil? frame) (neg? frame) (>= frame end))
|
||||||
|
|
@ -354,7 +372,7 @@
|
||||||
(if (or (nil? end) (symbol clip sid) (nil? frame) (neg? frame) (>= frame end))
|
(if (or (nil? end) (symbol clip sid) (nil? frame) (neg? frame) (>= frame end))
|
||||||
clip
|
clip
|
||||||
(-> clip
|
(-> clip
|
||||||
(assoc-in [:symbols sid] {:id sid :name (name sid) :frames (- end frame) :nodes {}})
|
(assoc-in [:symbols sid] {:id sid :name (name sid) :fps (fps clip host) :frames (- end frame) :nodes {}})
|
||||||
(place-symbol nil host sid frame uuid nil)))))
|
(place-symbol nil host sid frame uuid nil)))))
|
||||||
|
|
||||||
(defn free-id
|
(defn free-id
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@
|
||||||
(reduce node/then-time {:at 0 :rate 1} (map node/time-of (reverse chain)))
|
(reduce node/then-time {:at 0 :rate 1} (map node/time-of (reverse chain)))
|
||||||
(let [n (get nodes id) t (:time n)]
|
(let [n (get nodes id) t (:time n)]
|
||||||
(when (and n (not (contains? seen id))
|
(when (and n (not (contains? seen id))
|
||||||
(not (:loop? t)) (not (:sample-fps t))
|
(not (:loop? t))
|
||||||
(<= (or (:expose t) 1) 1))
|
(<= (or (:expose t) 1) 1))
|
||||||
(recur (:parent n) (conj seen id) (conj chain n)))))))
|
(recur (:parent n) (conj seen id) (conj chain n)))))))
|
||||||
|
|
||||||
|
|
@ -396,7 +396,7 @@
|
||||||
(when (clip/symbol clip drawing-id) "the new drawing ID is already used"))]
|
(when (clip/symbol clip drawing-id) "the new drawing ID is already used"))]
|
||||||
{:refused why}
|
{:refused why}
|
||||||
(place (assoc-in clip [:symbols drawing-id]
|
(place (assoc-in clip [:symbols drawing-id]
|
||||||
{:id drawing-id :name (name drawing-id) :frames 1 :nodes {}})
|
{:id drawing-id :name (name drawing-id) :fps (clip/fps clip sid) :frames 1 :nodes {}})
|
||||||
sid lane-id id drawing-id at extent true)))
|
sid lane-id id drawing-id at extent true)))
|
||||||
|
|
||||||
(defn reuse-drawing
|
(defn reuse-drawing
|
||||||
|
|
@ -475,7 +475,7 @@
|
||||||
(if (:refused cleared)
|
(if (:refused cleared)
|
||||||
cleared
|
cleared
|
||||||
(place (assoc-in (:clip cleared) [:symbols drawing-id]
|
(place (assoc-in (:clip cleared) [:symbols drawing-id]
|
||||||
{:id drawing-id :name (name drawing-id) :frames 1 :nodes {}})
|
{:id drawing-id :name (name drawing-id) :fps (clip/fps clip sid) :frames 1 :nodes {}})
|
||||||
sid lane-id id drawing-id at extent false))))))
|
sid lane-id id drawing-id at extent false))))))
|
||||||
|
|
||||||
(defn make-unique
|
(defn make-unique
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
clip/<cid>/subject/<subj> 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>/feature/<fid> one feature: area, nodes, params
|
||||||
clip/<cid>/group/<gid> an eye pair and its shared params
|
clip/<cid>/group/<gid> an eye pair and its shared params
|
||||||
clip/<cid>/symbol/<sid> frames, and a palette one day
|
clip/<cid>/symbol/<sid> native frames and fps, optional palette
|
||||||
clip/<cid>/symbol/<sid>/node/<nid> kind, parent, stencil, z, time
|
clip/<cid>/symbol/<sid>/node/<nid> kind, parent, stencil, z, time
|
||||||
clip/<cid>/symbol/<sid>/channel/<nid>/<prop>
|
clip/<cid>/symbol/<sid>/channel/<nid>/<prop>
|
||||||
clip/<cid>/symbol/<sid>/measured/<nid> the channels a re-freeze owns
|
clip/<cid>/symbol/<sid>/measured/<nid> the channels a re-freeze owns
|
||||||
|
|
@ -23,9 +23,8 @@
|
||||||
its own nodes, so the symbol id is a path segment. No symbol has a reserved
|
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.
|
segment: `main` in a path is an id like any other.
|
||||||
|
|
||||||
`:frames` MOVED OFF `timing` onto the symbol. A symbol is a frame space and a
|
The timing leaf holds output fps. Each symbol leaf holds its native fps
|
||||||
clip is a rate, so `timing` holds `:fps` alone. Both used to be in one leaf, which
|
and frame count, so changing the output grid leaves content untouched.
|
||||||
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,
|
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
|
so the cut is chosen so that the things people do simultaneously land on
|
||||||
|
|
@ -145,7 +144,7 @@
|
||||||
;; disagree with itself about; `clip` puts it back.
|
;; disagree with itself about; `clip` puts it back.
|
||||||
(for [[sid sym] (:symbols clip)]
|
(for [[sid sym] (:symbols clip)]
|
||||||
{(at "symbol" (segment sid))
|
{(at "symbol" (segment sid))
|
||||||
(select-keys sym [:name :frames :width :height :palette])})
|
(select-keys sym [:name :frames :fps :width :height :palette])})
|
||||||
(for [[sid sym] (:symbols clip)
|
(for [[sid sym] (:symbols clip)
|
||||||
[id n] (:nodes sym)]
|
[id n] (:nodes sym)]
|
||||||
{(at "symbol" (segment sid) "node" (segment id))
|
{(at "symbol" (segment sid) "node" (segment id))
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@
|
||||||
[clip store sid frame id]
|
[clip store sid frame id]
|
||||||
(let [sym (clip/symbol clip sid)
|
(let [sym (clip/symbol clip sid)
|
||||||
sym (update sym :nodes select-keys (symbol/lineage (:nodes sym) id))
|
sym (update sym :nodes select-keys (symbol/lineage (:nodes sym) id))
|
||||||
r (symbol/resolver sym store pal/index-of {:source-fps (:fps clip)})]
|
r (symbol/resolver sym store pal/index-of nil)]
|
||||||
(r frame)
|
(r frame)
|
||||||
r))
|
r))
|
||||||
|
|
||||||
|
|
@ -67,8 +67,7 @@
|
||||||
;; the hold pins, not on `local`, and inside a playing insert
|
;; the hold pins, not on `local`, and inside a playing insert
|
||||||
;; is its animation at its own in-point and speed.
|
;; is its animation at its own in-point and speed.
|
||||||
shown (when (number? local)
|
shown (when (number? local)
|
||||||
(node/placed-frame (get nodes id) local
|
(clip/placed-frame clip sid (get nodes id) local))
|
||||||
(clip/frames clip (node/source (get nodes id)))))
|
|
||||||
inner (:symbol shown)
|
inner (:symbol shown)
|
||||||
lf (if shown (:frame shown) local)]
|
lf (if shown (:frame shown) local)]
|
||||||
(if (and m (number? local)
|
(if (and m (number? local)
|
||||||
|
|
@ -80,11 +79,12 @@
|
||||||
;; Forward sampling above works for holds too. :time is the
|
;; Forward sampling above works for holds too. :time is the
|
||||||
;; invertible edit map; source in-points and speeds belong in it.
|
;; invertible edit map; source in-points and speeds belong in it.
|
||||||
:time (when (and time (not-any? #(get-in % [:time :loop?]) chain)
|
:time (when (and time (not-any? #(get-in % [:time :loop?]) chain)
|
||||||
(or (not inst?) (node/source-time (get nodes id))))
|
(or (not inst?) (clip/source-time clip sid (get nodes id))))
|
||||||
(cond-> (reduce node/then-time time (map node/time-of chain))
|
(cond-> (reduce node/then-time time (map node/time-of chain))
|
||||||
inst? (node/then-time (node/source-time (get nodes id)))))}
|
inst? (node/then-time (clip/source-time clip sid (get nodes id)))))}
|
||||||
(reduced nil))))
|
(reduced nil))))
|
||||||
{:sid sid :frame f :matrix (node/mat) :time {:at 0 :rate 1}}
|
{:sid sid :frame (js/Math.floor (* f (:rate (clip/grid-time clip sid))))
|
||||||
|
:matrix (node/mat) :time (clip/grid-time clip sid)}
|
||||||
path))
|
path))
|
||||||
|
|
||||||
(defn placement
|
(defn placement
|
||||||
|
|
@ -154,7 +154,10 @@
|
||||||
(let [n (get nodes id)
|
(let [n (get nodes id)
|
||||||
parent (if-let [pid (:parent n)] (get acc pid)
|
parent (if-let [pid (:parent n)] (get acc pid)
|
||||||
{:time outer :bounds bounds})
|
{:time outer :bounds bounds})
|
||||||
m (node/then-time (:time parent) (node/time-of n))]
|
m (node/then-time (:time parent) (node/time-of n))
|
||||||
|
m (if (= :audio (:kind n))
|
||||||
|
(update m :rate * (/ (or (get-in n [:source :fps]) (clip/fps clip sid))
|
||||||
|
(clip/fps clip sid))) m)]
|
||||||
(assoc acc id {:time m :bounds (window m (:span n) (:bounds parent))})))
|
(assoc acc id {:time m :bounds (window m (:span n) (:bounds parent))})))
|
||||||
{} (symbol/order nodes))]
|
{} (symbol/order nodes))]
|
||||||
(mapcat
|
(mapcat
|
||||||
|
|
@ -164,6 +167,7 @@
|
||||||
(case (:kind n)
|
(case (:kind n)
|
||||||
:audio [(-> n
|
:audio [(-> n
|
||||||
(assoc :parent nil :path (conj path id) :owner sid
|
(assoc :parent nil :path (conj path id) :owner sid
|
||||||
|
:fps (or (get-in n [:source :fps]) (clip/fps clip sid))
|
||||||
:span [(to-local m lo) (to-local m hi)]
|
:span [(to-local m lo) (to-local m hi)]
|
||||||
:time (merge (:time n) {:mode :map :at (:at m) :rate (:rate m) :offset 0})
|
:time (merge (:time n) {:mode :map :at (:at m) :rate (:rate m) :offset 0})
|
||||||
:channels (channels (:channels n) m)))]
|
:channels (channels (:channels n) m)))]
|
||||||
|
|
@ -173,7 +177,10 @@
|
||||||
length (clip/frames clip child)]
|
length (clip/frames clip child)]
|
||||||
;; A visual freeze does not emit a sustained audio sample.
|
;; A visual freeze does not emit a sustained audio sample.
|
||||||
(when (and child length (pos? speed))
|
(when (and child length (pos? speed))
|
||||||
(let [source (node/then-time m {:at (- (/ in speed)) :rate speed})
|
(let [source (node/then-time m (clip/source-time clip sid
|
||||||
|
(-> n
|
||||||
|
(assoc-in [:playback :end] :stop)
|
||||||
|
(update :time dissoc :loop?))))
|
||||||
loop? (or (= end :loop) (get-in n [:time :loop?]))
|
loop? (or (= end :loop) (get-in n [:time :loop?]))
|
||||||
periods (if loop?
|
periods (if loop?
|
||||||
(range (js/Math.floor (/ (to-local source lo) length))
|
(range (js/Math.floor (/ (to-local source lo) length))
|
||||||
|
|
@ -185,7 +192,7 @@
|
||||||
periods))))
|
periods))))
|
||||||
nil))))
|
nil))))
|
||||||
(sort-by (comp str key) nodes))))]
|
(sort-by (comp str key) nodes))))]
|
||||||
(vec (walk sid {:at 0 :rate 1} [0 (clip/frames clip sid)] [] #{}))))
|
(vec (walk sid (clip/grid-time clip sid) [0 (clip/output-frames clip sid)] [] #{}))))
|
||||||
|
|
||||||
(defn- retime
|
(defn- retime
|
||||||
"Node `n` with its own time map replaced by `m`, and nothing else touched: its
|
"Node `n` with its own time map replaced by `m`, and nothing else touched: its
|
||||||
|
|
@ -306,17 +313,18 @@
|
||||||
sids (reductions only sid path)
|
sids (reductions only sid path)
|
||||||
;; Every node on the way, outermost first: each instance, after its
|
;; Every node on the way, outermost first: each instance, after its
|
||||||
;; parents in the symbol it is in.
|
;; parents in the symbol it is in.
|
||||||
chain (mapcat (fn [sid id]
|
maps (mapcat (fn [sid id]
|
||||||
(let [nodes (:nodes (clip/symbol clip sid))]
|
(let [nodes (:nodes (clip/symbol clip sid))
|
||||||
(map #(get nodes %) (rseq (symbol/lineage nodes id)))))
|
chain (map #(get nodes %) (rseq (symbol/lineage nodes id)))]
|
||||||
sids path)]
|
(mapcat (fn [n]
|
||||||
{:sid (last sids)
|
(if (get-in n [:time :loop?]) [nil]
|
||||||
:time (when (not-any? #(or (get-in % [:time :loop?])
|
(cond-> [(node/time-of n)]
|
||||||
(and (= :instance (:kind %)) (nil? (node/source-time %)))) chain)
|
(= :instance (:kind n)) (conj (clip/source-time clip sid n)))))
|
||||||
(reduce node/then-time {:at 0 :rate 1}
|
chain)))
|
||||||
(mapcat (fn [n] (cond-> [(node/time-of n)]
|
sids path)]
|
||||||
(= :instance (:kind n)) (conj (node/source-time n))))
|
{:sid (last sids)
|
||||||
chain)))}))
|
:time (when (every? some? maps)
|
||||||
|
(reduce node/then-time (clip/grid-time clip sid) maps))}))
|
||||||
|
|
||||||
(defn slide
|
(defn slide
|
||||||
"Move the node at row path `path` along its symbol's time by `df` frames of
|
"Move the node at row path `path` along its symbol's time by `df` frames of
|
||||||
|
|
@ -402,7 +410,7 @@
|
||||||
start (js/Math.floor (max 0 (apply min (map first spans))))
|
start (js/Math.floor (max 0 (apply min (map first spans))))
|
||||||
end (min (second whole) (apply max (map second spans)))
|
end (min (second whole) (apply max (map second spans)))
|
||||||
made (-> clip
|
made (-> clip
|
||||||
(assoc-in [:symbols sid] {:id sid :name (name sid)
|
(assoc-in [:symbols sid] {:id sid :name (name sid) :fps (clip/fps clip host)
|
||||||
:frames (max 1 (js/Math.ceil (- end start)))
|
:frames (max 1 (js/Math.ceil (- end start)))
|
||||||
:nodes {}})
|
:nodes {}})
|
||||||
(clip/place-symbol store host sid start uuid nil))
|
(clip/place-symbol store host sid start uuid nil))
|
||||||
|
|
|
||||||
|
|
@ -150,30 +150,18 @@
|
||||||
[f n]
|
[f n]
|
||||||
(if (and n (> n 1)) (* (js/Math.floor (/ f n)) n) f))
|
(if (and n (> n 1)) (* (js/Math.floor (/ f n)) n) f))
|
||||||
|
|
||||||
(defn sample-frame
|
|
||||||
"Pick a source frame for a lower picture rate without changing clip time.
|
|
||||||
|
|
||||||
The input is already an integer source frame from the audio clock. Its time is
|
|
||||||
f/source-fps. Quantise that time to the picture grid, then read the latest
|
|
||||||
source frame at or before it. The result is always an integer and never from
|
|
||||||
the future, including when the rates do not divide (30 source → 24 picture)."
|
|
||||||
[f source-fps picture-fps]
|
|
||||||
(if (and source-fps picture-fps
|
|
||||||
(pos? source-fps) (pos? picture-fps)
|
|
||||||
(< picture-fps source-fps))
|
|
||||||
(min f (js/Math.floor
|
|
||||||
(* (js/Math.floor (/ (* f picture-fps) source-fps))
|
|
||||||
(/ source-fps picture-fps))))
|
|
||||||
f))
|
|
||||||
|
|
||||||
(defn time-of
|
(defn time-of
|
||||||
"A node's own time as the affine map it is: `{:at a :rate r}`, meaning a frame
|
"A node's own time as the affine map it is: `{:at a :rate r}`, meaning a frame
|
||||||
`p` of its parent is frame `r·(p − a)` of its own. THE SAME FOR EVERY NODE. A
|
`p` of its parent is frame `r·(p − a)` of its own. THE SAME FOR EVERY NODE. A
|
||||||
node with no time map is `{:at 0 :rate 1}`, reading its parent's frames as its
|
node with no time map is `{:at 0 :rate 1}`, reading its parent's frames as its
|
||||||
own; a mouth lead's `:offset` is folded into `:at`. Cel and picture
|
own; a mouth lead's `:offset` is folded into `:at`. Exposure is a floor, not
|
||||||
sampling are floors, not part of the map, and are left out: this is the map a
|
part of the map, and is left out: this is the map a move preserves and a
|
||||||
move preserves and a timeline row draws with, and `local-frame` is what reads
|
timeline row draws with, and `local-frame` is what reads a frame, the floor and
|
||||||
a frame, floors and the lead in their load-bearing order."
|
the lead in their load-bearing order.
|
||||||
|
|
||||||
|
`:rate` is a RETIME SOMEBODY CHOSE and nothing else — half speed on an insert.
|
||||||
|
Reconciling two frame rates is not a retime and does not belong here: it is a
|
||||||
|
selection, and it lives in `domain/cadence`."
|
||||||
[n]
|
[n]
|
||||||
(let [{:keys [mode at rate offset] :or {mode :map at 0 rate 1 offset 0}} (:time n)]
|
(let [{:keys [mode at rate offset] :or {mode :map at 0 rate 1 offset 0}} (:time n)]
|
||||||
(if (= mode :map)
|
(if (= mode :map)
|
||||||
|
|
@ -258,31 +246,15 @@
|
||||||
(defn finite-number? [v] (and (number? v) (js/Number.isFinite v)))
|
(defn finite-number? [v] (and (number? v) (js/Number.isFinite v)))
|
||||||
|
|
||||||
(defn local-frame
|
(defn local-frame
|
||||||
"Apply a node's time map to the frame it was handed by its parent.
|
"Apply an artistic time map within one frame space: expose, then offset.
|
||||||
|
Frame-rate selection happens at symbol boundaries in domain/clip."
|
||||||
ORDER IS LOAD-BEARING: expose first, then offset. Flooring onto a grid and
|
|
||||||
shifting against the clock do not commute — shift first and the floor discards
|
|
||||||
it on most frames, so the lead slider reads as doing nothing at cels above
|
|
||||||
1, which is indistinguishable from the slider being unwired.
|
|
||||||
|
|
||||||
Composed along the parent chain, outermost first, by symbol/eval-frame. Two
|
|
||||||
rules fall out and they are different rules: cel 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
|
|
||||||
to performance nodes and not to the plate, which is the entire point of it."
|
|
||||||
[n f]
|
[n f]
|
||||||
(let [{:keys [mode source-fps sample-fps] ex :expose :or {mode :map}} (:time n)]
|
(let [{:keys [mode at rate offset] ex :expose :or {mode :map at 0 rate 1}} (:time n)]
|
||||||
(if (= mode :inherit)
|
(if (= mode :inherit)
|
||||||
f
|
f
|
||||||
(do
|
(cond-> (* rate (- f at))
|
||||||
(when (and sample-fps (not (and source-fps (pos? source-fps))))
|
ex (expose ex)
|
||||||
(throw (ex-info "picture sampling needs a positive source fps"
|
offset (+ offset)))))
|
||||||
{:node (:id n) :time (:time n)})))
|
|
||||||
(let [{:keys [at rate offset] :or {at 0 rate 1}} (:time n)]
|
|
||||||
(cond-> (* rate (- f at))
|
|
||||||
sample-fps (sample-frame source-fps sample-fps)
|
|
||||||
ex (expose ex)
|
|
||||||
offset (+ offset)))))))
|
|
||||||
|
|
||||||
;; ---------------------------------------------------------------------------
|
;; ---------------------------------------------------------------------------
|
||||||
;; the transform
|
;; the transform
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@
|
||||||
symbol to BUILD and a lookup to RUN. Asking frame by frame through a fresh one
|
symbol to BUILD and a lookup to RUN. Asking frame by frame through a fresh one
|
||||||
is a resolver per frame, which is what made `pivot` want a separate path for
|
is a resolver per frame, which is what made `pivot` want a separate path for
|
||||||
instances rather than the one walk it is."
|
instances rather than the one walk it is."
|
||||||
[document store n]
|
[document store sid n]
|
||||||
(let [grow (fn [[x0 y0 x1 y1 :as b] x y]
|
(let [grow (fn [[x0 y0 x1 y1 :as b] x y]
|
||||||
(if b [(min x0 x) (min y0 y) (max x1 x) (max y1 y)] [x y x y]))
|
(if b [(min x0 x) (min y0 y) (max x1 x) (max y1 y)] [x y x y]))
|
||||||
at (fn [f p] (ch/value-at (get (node/channels n) p) f store))]
|
at (fn [f p] (ch/value-at (get (node/channels n) p) f store))]
|
||||||
|
|
@ -105,10 +105,10 @@
|
||||||
(let [loop? (get-in n [:time :loop?])
|
(let [loop? (get-in n [:time :loop?])
|
||||||
resolvers (into {} (map (fn [child]
|
resolvers (into {} (map (fn [child]
|
||||||
[child (clip/resolver document child store
|
[child (clip/resolver document child store
|
||||||
pal/index-of nil)]))
|
pal/index-of {:grid-fps (clip/fps document child)})]))
|
||||||
(node/sources n))]
|
(node/sources n))]
|
||||||
(fn [f0]
|
(fn [f0]
|
||||||
(let [shown (node/placed-frame n f0 (clip/frames document (node/source n)))
|
(let [shown (clip/placed-frame document sid n f0)
|
||||||
frames (when shown (clip/frames document (:symbol shown)))
|
frames (when shown (clip/frames document (:symbol shown)))
|
||||||
f (when (number? frames)
|
f (when (number? frames)
|
||||||
(if loop? (mod (:frame shown) frames) (:frame shown)))
|
(if loop? (mod (:frame shown) frames) (:frame shown)))
|
||||||
|
|
@ -151,7 +151,7 @@
|
||||||
|
|
||||||
ALL its frames, not the first, for the reason `clip/center` says: a mouth that
|
ALL its frames, not the first, for the reason `clip/center` says: a mouth that
|
||||||
opens and travels still has its middle where the mouth is."
|
opens and travels still has its middle where the mouth is."
|
||||||
[document store n fs]
|
[document store sid n fs]
|
||||||
(let [bounds (bounds-of document store n)]
|
(let [bounds (bounds-of document store sid n)]
|
||||||
(when-let [[x0 y0 x1 y1] (reduce #(union %1 (bounds %2)) nil fs)]
|
(when-let [[x0 y0 x1 y1] (reduce #(union %1 (bounds %2)) nil fs)]
|
||||||
[(/ (+ x0 x1) 2) (/ (+ y0 y1) 2)])))
|
[(/ (+ x0 x1) 2) (/ (+ y0 y1) 2)])))
|
||||||
|
|
|
||||||
|
|
@ -399,7 +399,7 @@
|
||||||
(defn- base-channel-frame
|
(defn- base-channel-frame
|
||||||
"A trace selects the measured frames its node reads; marked channels read
|
"A trace selects the measured frames its node reads; marked channels read
|
||||||
instance pose choices."
|
instance pose choices."
|
||||||
[choices traces nodes source-fps picture-fps id c lf]
|
[choices traces nodes id c lf]
|
||||||
(cond
|
(cond
|
||||||
(contains? traces id)
|
(contains? traces id)
|
||||||
(trace/held-frame (get traces id) lf)
|
(trace/held-frame (get traces id) lf)
|
||||||
|
|
@ -410,9 +410,9 @@
|
||||||
[:node id]
|
[:node id]
|
||||||
(or (:pose-group (get nodes id)) id))
|
(or (:pose-group (get nodes id)) id))
|
||||||
lf
|
lf
|
||||||
(node/sample-frame lf source-fps picture-fps))
|
(js/Math.floor lf))
|
||||||
|
|
||||||
:else lf))
|
:else (js/Math.floor lf)))
|
||||||
|
|
||||||
(defn- prepared-traces [nodes]
|
(defn- prepared-traces [nodes]
|
||||||
(into {}
|
(into {}
|
||||||
|
|
@ -465,14 +465,14 @@
|
||||||
the only place the space changes.
|
the only place the space changes.
|
||||||
|
|
||||||
This is the definition of what a frame means. `resolver` is what plays it."
|
This is the definition of what a frame means. `resolver` is what plays it."
|
||||||
[sym f store palette {:keys [pose-tracks source-fps picture-fps]}]
|
[sym f store palette {:keys [pose-tracks]}]
|
||||||
(let [nodes (nodes-of sym)
|
(let [nodes (nodes-of sym)
|
||||||
choices (pose/prepare pose-tracks)
|
choices (pose/prepare pose-tracks)
|
||||||
traces (prepared-traces nodes)
|
traces (prepared-traces nodes)
|
||||||
ord (order nodes)]
|
ord (order nodes)]
|
||||||
(eval-into {:read (fn [id path c lf]
|
(eval-into {:read (fn [id path c lf]
|
||||||
(ch/value-at c (base-channel-frame choices traces nodes
|
(ch/value-at c (base-channel-frame choices traces nodes
|
||||||
source-fps picture-fps id c lf)
|
id c lf)
|
||||||
lf store))
|
lf store))
|
||||||
:palette palette
|
:palette palette
|
||||||
:mat-for (fn [_id] (node/mat))
|
:mat-for (fn [_id] (node/mat))
|
||||||
|
|
@ -531,7 +531,7 @@
|
||||||
because a fifth of them later is then a key rather than a nil at every one of
|
because a fifth of them later is then a key rather than a nil at every one of
|
||||||
these call sites — which is what the arity ladder that used to be here was
|
these call sites — which is what the arity ladder that used to be here was
|
||||||
standing in for."
|
standing in for."
|
||||||
[sym store palette {:keys [pose-tracks source-fps picture-fps]}]
|
[sym store palette {:keys [pose-tracks]}]
|
||||||
(let [nodes (nodes-of sym)
|
(let [nodes (nodes-of sym)
|
||||||
choices (pose/prepare pose-tracks)
|
choices (pose/prepare pose-tracks)
|
||||||
traces (prepared-traces nodes)
|
traces (prepared-traces nodes)
|
||||||
|
|
@ -560,7 +560,7 @@
|
||||||
(when-let [cursor (get-in cursors [id path])]
|
(when-let [cursor (get-in cursors [id path])]
|
||||||
(ch/sample! cursor
|
(ch/sample! cursor
|
||||||
(base-channel-frame choices traces nodes
|
(base-channel-frame choices traces nodes
|
||||||
source-fps picture-fps id c lf)
|
id c lf)
|
||||||
lf)))
|
lf)))
|
||||||
:palette palette
|
:palette palette
|
||||||
:mat-for (fn [id] (get mats id))
|
:mat-for (fn [id] (get mats id))
|
||||||
|
|
@ -596,7 +596,7 @@
|
||||||
|
|
||||||
`:width` and `:height` are the symbol's own stage, and are absent until someone
|
`:width` and `:height` are the symbol's own stage, and are absent until someone
|
||||||
sets them: a symbol without them uses the clip's — see `clip/stage`."
|
sets them: a symbol without them uses the clip's — see `clip/stage`."
|
||||||
#{:id :name :frames :width :height :nodes :palette})
|
#{:id :name :frames :fps :width :height :nodes :palette})
|
||||||
|
|
||||||
(defn problems
|
(defn problems
|
||||||
"Human-readable reasons this symbol will not evaluate. Empty means it will.
|
"Human-readable reasons this symbol will not evaluate. Empty means it will.
|
||||||
|
|
@ -642,6 +642,9 @@
|
||||||
(into (when-not (or (nil? (:frames sym)) (and (integer? (:frames sym)) (pos? (:frames sym))))
|
(into (when-not (or (nil? (:frames sym)) (and (integer? (:frames sym)) (pos? (:frames sym))))
|
||||||
[(str ":frames is " (pr-str (:frames sym))
|
[(str ":frames is " (pr-str (:frames sym))
|
||||||
" — a symbol is a frame SPACE, so its length is a positive integer")]))
|
" — a symbol is a frame SPACE, so its length is a positive integer")]))
|
||||||
|
(into (when (and (some? (:fps sym))
|
||||||
|
(not (and (node/finite-number? (:fps sym)) (pos? (:fps sym)))))
|
||||||
|
[":fps must be a positive finite native rate"]))
|
||||||
(into (for [k [:width :height]
|
(into (for [k [:width :height]
|
||||||
:let [v (get sym k)]
|
:let [v (get sym k)]
|
||||||
:when (and (some? v) (not (and (integer? v) (pos? v))))]
|
:when (and (some? v) (not (and (integer? v) (pos? v))))]
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@
|
||||||
[db clip]
|
[db clip]
|
||||||
(cond-> (update db :clip merge (select-keys clip [:width :height]))
|
(cond-> (update db :clip merge (select-keys clip [:width :height]))
|
||||||
(not= (:fps clip) (get-in db [:clip :fps]))
|
(not= (:fps clip) (get-in db [:clip :fps]))
|
||||||
(update :clip merge {:fps (:fps clip) :display-fps (:fps clip)})))
|
(update :clip merge {:fps (:fps clip)})))
|
||||||
|
|
||||||
(defn history
|
(defn history
|
||||||
"Apply `f` to the loaded entry's undo history, which is not an edit: nothing
|
"Apply `f` to the loaded entry's undo history, which is not an edit: nothing
|
||||||
|
|
|
||||||
|
|
@ -117,8 +117,7 @@
|
||||||
(let [{:keys [clip]} (store/entry (:clip/current db))
|
(let [{:keys [clip]} (store/entry (:clip/current db))
|
||||||
{sid :symbol isolate :isolate} (target db)]
|
{sid :symbol isolate :isolate} (target db)]
|
||||||
(export/plan {:clip clip :symbol sid :zoom (get-in db [:export :zoom])
|
(export/plan {:clip clip :symbol sid :zoom (get-in db [:export :zoom])
|
||||||
:isolate isolate
|
:isolate isolate}))))
|
||||||
:picture-fps (get-in db [:clip :display-fps])}))))
|
|
||||||
|
|
||||||
(rf/reg-event-db
|
(rf/reg-event-db
|
||||||
::set-target
|
::set-target
|
||||||
|
|
@ -158,7 +157,6 @@
|
||||||
(:palette db) pal/index-of)
|
(:palette db) pal/index-of)
|
||||||
:ramp (get {:arthur/default pal/rgb} (:palette db) pal/rgb)
|
:ramp (get {:arthur/default pal/rgb} (:palette db) pal/rgb)
|
||||||
:zoom zoom
|
:zoom zoom
|
||||||
:picture-fps (get-in db [:clip :display-fps])
|
|
||||||
:audio-url (:audio entry)
|
:audio-url (:audio entry)
|
||||||
:name (stem (:label entry)
|
:name (stem (:label entry)
|
||||||
(label-of (:clip entry) (get-in db [:ui :open])
|
(label-of (:clip entry) (get-in db [:ui :open])
|
||||||
|
|
|
||||||
|
|
@ -152,7 +152,7 @@
|
||||||
source-blocks (source/pack-subjects (:id (:analysis built)) subjects)
|
source-blocks (source/pack-subjects (:id (:analysis built)) subjects)
|
||||||
_ (mark! "build-clip: pack source blocks")]
|
_ (mark! "build-clip: pack source blocks")]
|
||||||
(assoc (select-keys built [:fps :width :height])
|
(assoc (select-keys built [:fps :width :height])
|
||||||
:display-fps (:fps built)
|
|
||||||
:clip built :store (:store frozen)
|
:clip built :store (:store frozen)
|
||||||
:source-blocks source-blocks
|
:source-blocks source-blocks
|
||||||
:source-inputs (assoc source-inputs :subjects with-presence)
|
:source-inputs (assoc source-inputs :subjects with-presence)
|
||||||
|
|
@ -501,7 +501,7 @@
|
||||||
{:keys [clip sid tracked?]}
|
{:keys [clip sid tracked?]}
|
||||||
(bring/take (:clip (store/entry (:clip/current db))) (:clip built)
|
(bring/take (:clip (store/entry (:clip/current db))) (:clip built)
|
||||||
name footage-id range)
|
name footage-id range)
|
||||||
imported-frames (clip/frames clip sid)
|
imported-frames (clip/output-frames clip sid)
|
||||||
source-fps (get-in built [:clip :fps])
|
source-fps (get-in built [:clip :fps])
|
||||||
db (edit/edit-entry
|
db (edit/edit-entry
|
||||||
db
|
db
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@
|
||||||
the db beside it, because an edit can change it."
|
the db beside it, because an edit can change it."
|
||||||
[db]
|
[db]
|
||||||
(or (some-> (footage/entry (:clip/current db)) :clip
|
(or (some-> (footage/entry (:clip/current db)) :clip
|
||||||
(clip/frames (get-in db [:ui :open])))
|
(clip/output-frames (get-in db [:ui :open])))
|
||||||
1))
|
1))
|
||||||
|
|
||||||
(defn show
|
(defn show
|
||||||
|
|
@ -36,7 +36,7 @@
|
||||||
sid (clip/opens-on (:clip entry))]
|
sid (clip/opens-on (:clip entry))]
|
||||||
(-> db
|
(-> db
|
||||||
(assoc :clip/current id
|
(assoc :clip/current id
|
||||||
:clip (select-keys entry [:fps :width :height :audio :display-fps]))
|
:clip (select-keys entry [:fps :width :height :audio]))
|
||||||
(update :ui merge
|
(update :ui merge
|
||||||
{:open sid :tabs (if sid [sid] [])
|
{:open sid :tabs (if sid [sid] [])
|
||||||
;; From scratch, not merged: the faces switched on were another
|
;; From scratch, not merged: the faces switched on were another
|
||||||
|
|
@ -95,13 +95,6 @@
|
||||||
{:db (assoc-in db [:playback :rate] r)
|
{:db (assoc-in db [:playback :rate] r)
|
||||||
::rate! r}))
|
::rate! r}))
|
||||||
|
|
||||||
(rf/reg-event-db
|
|
||||||
::set-picture-fps
|
|
||||||
(fn [db [_ target]]
|
|
||||||
(if (and (number? target) (pos? target) (<= target (fps db)))
|
|
||||||
(assoc-in db [:clip :display-fps] target)
|
|
||||||
db)))
|
|
||||||
|
|
||||||
;; --- effects: every DOM touch on the audio element is one of these ---
|
;; --- effects: every DOM touch on the audio element is one of these ---
|
||||||
|
|
||||||
(rf/reg-fx ::play! (fn [_] (clock/play!)))
|
(rf/reg-fx ::play! (fn [_] (clock/play!)))
|
||||||
|
|
|
||||||
|
|
@ -120,7 +120,6 @@
|
||||||
;; differs from it, and a collaborator's
|
;; differs from it, and a collaborator's
|
||||||
;; write lands on what does not.
|
;; write lands on what does not.
|
||||||
:synced (project/tier1 (.-leaves clip-json))
|
:synced (project/tier1 (.-leaves clip-json))
|
||||||
:display-fps (:fps built)
|
|
||||||
:clip built :store (:store loaded)
|
:clip built :store (:store loaded)
|
||||||
:footage-id footage-id
|
:footage-id footage-id
|
||||||
:audio (if footage (.-audio footage)
|
:audio (if footage (.-audio footage)
|
||||||
|
|
@ -512,7 +511,6 @@
|
||||||
;; A content id of its own from the start: `::save!` addresses the clip by
|
;; A content id of its own from the start: `::save!` addresses the clip by
|
||||||
;; it, and two untitled documents saved from two tabs are two documents.
|
;; it, and two untitled documents saved from two tabs are two documents.
|
||||||
:cid (str (random-uuid))
|
:cid (str (random-uuid))
|
||||||
:display-fps (:fps c)
|
|
||||||
:fps (:fps c) :width (:width c) :height (:height c)}))
|
:fps (:fps c) :width (:width c) :height (:height c)}))
|
||||||
|
|
||||||
(rf/reg-event-fx
|
(rf/reg-event-fx
|
||||||
|
|
@ -529,7 +527,7 @@
|
||||||
:busy? false :status "new document"}))
|
:busy? false :status "new document"}))
|
||||||
;; The readout goes home with the document; so must the clock, or play
|
;; The readout goes home with the document; so must the clock, or play
|
||||||
;; picks up wherever the last document's audio had got to.
|
;; picks up wherever the last document's audio had got to.
|
||||||
::pb/seek! [(:fps entry) (clip/frames (:clip entry) (clip/opens-on (:clip entry))) 0]
|
::pb/seek! [(:fps entry) (clip/output-frames (:clip entry) (clip/opens-on (:clip entry))) 0]
|
||||||
::pb/pause! nil})))
|
::pb/pause! nil})))
|
||||||
|
|
||||||
(rf/reg-event-fx
|
(rf/reg-event-fx
|
||||||
|
|
@ -594,12 +592,15 @@
|
||||||
(if (or (not (#{:fps :width :height} key))
|
(if (or (not (#{:fps :width :height} key))
|
||||||
(not (and (integer? value) (pos? value))))
|
(not (and (integer? value) (pos? value))))
|
||||||
{}
|
{}
|
||||||
(let [db' (-> (edit/edit db #(assoc % key value))
|
(let [db' (edit/edit db #(if (= key :fps) (clip/set-fps % value) (assoc % key value)))
|
||||||
(assoc-in [:clip key] value)
|
db' (assoc-in db' [:clip key] value)
|
||||||
(cond-> (= key :fps) (assoc-in [:clip :display-fps] value)))
|
frame (min (dec (pb/frames db'))
|
||||||
frame (get-in db [:playback :frame])]
|
(js/Math.floor (* (get-in db [:playback :frame])
|
||||||
|
(/ value (get-in db [:clip :fps])))))]
|
||||||
(cond-> {:db db'}
|
(cond-> {:db db'}
|
||||||
(= key :fps) (assoc ::pb/seek! [value (pb/frames db') frame]))))))
|
(= key :fps) (assoc :db (assoc-in db' [:playback :frame] frame)
|
||||||
|
::pb/seek! [value (pb/frames db') frame]
|
||||||
|
:dispatch [::pb/refresh-clock]))))))
|
||||||
|
|
||||||
(rf/reg-event-db
|
(rf/reg-event-db
|
||||||
::rename-symbol
|
::rename-symbol
|
||||||
|
|
@ -742,7 +743,7 @@
|
||||||
:status (str "opened " name " r" seq)}))
|
:status (str "opened " name " r" seq)}))
|
||||||
::pb/pause! nil
|
::pb/pause! nil
|
||||||
::pb/seek! (let [{c :clip fps :fps} (store/entry clip-id)]
|
::pb/seek! (let [{c :clip fps :fps} (store/entry clip-id)]
|
||||||
[fps (clip/frames c (clip/opens-on c)) 0])}))
|
[fps (clip/output-frames c (clip/opens-on c)) 0])}))
|
||||||
|
|
||||||
(rf/reg-event-fx
|
(rf/reg-event-fx
|
||||||
::failed
|
::failed
|
||||||
|
|
|
||||||
|
|
@ -130,7 +130,7 @@
|
||||||
[clip st open selection frame]
|
[clip st open selection frame]
|
||||||
(let [[_ sid _ path] selection]
|
(let [[_ sid _ path] selection]
|
||||||
(if (or (= sid open) (not (seq path)))
|
(if (or (= sid open) (not (seq path)))
|
||||||
frame
|
(:frame (nest/inside clip st open [] frame))
|
||||||
(:frame (nest/inside clip st open (pop path) frame)))))
|
(:frame (nest/inside clip st open (pop path) frame)))))
|
||||||
|
|
||||||
(rf/reg-event-fx
|
(rf/reg-event-fx
|
||||||
|
|
@ -472,7 +472,7 @@
|
||||||
(-> db
|
(-> db
|
||||||
(update :ui dissoc :drop)
|
(update :ui dissoc :drop)
|
||||||
(edit/edit-entry #(update % :clip clip/place-symbol (:store %)
|
(edit/edit-entry #(update % :clip clip/place-symbol (:store %)
|
||||||
host sid frame uuid point))
|
host sid (* frame (:rate (clip/grid-time (:clip %) host))) uuid point))
|
||||||
(assoc-in [:ui :selection] [:node host uuid [uuid]])))))
|
(assoc-in [:ui :selection] [:node host uuid [uuid]])))))
|
||||||
|
|
||||||
(rf/reg-event-db
|
(rf/reg-event-db
|
||||||
|
|
@ -482,7 +482,7 @@
|
||||||
host (get-in db [:ui :open])]
|
host (get-in db [:ui :open])]
|
||||||
(-> db
|
(-> db
|
||||||
(update :ui dissoc :drop)
|
(update :ui dissoc :drop)
|
||||||
(edit/edit #(clip/place-sound % host source label length rate frame uuid))
|
(edit/edit #(clip/place-sound % host source label length rate (* frame (:rate (clip/grid-time % host))) uuid))
|
||||||
(assoc-in [:ui :selection] [:node host uuid [uuid]])))))
|
(assoc-in [:ui :selection] [:node host uuid [uuid]])))))
|
||||||
|
|
||||||
;; ---------------------------------------------------------------------------
|
;; ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -158,29 +158,24 @@
|
||||||
|
|
||||||
Separate from `run!` so the UI can show the size and length it is about to
|
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."
|
commit to, and so the arithmetic is assertable without a sink."
|
||||||
[{:keys [clip zoom picture-fps] sid :symbol isolate-id :isolate}]
|
[{:keys [clip zoom] sid :symbol isolate-id :isolate}]
|
||||||
(let [sym (some-> (clip/symbol clip sid) (isolate isolate-id))
|
(let [sym (some-> (clip/symbol clip sid) (isolate isolate-id))
|
||||||
[width height] (clip/stage clip sid)
|
[width height] (clip/stage clip sid)
|
||||||
zoom (max 1 (js/Math.floor (or zoom 1)))]
|
zoom (max 1 (js/Math.floor (or zoom 1)))]
|
||||||
(when sym
|
(when sym
|
||||||
{:frames (:frames sym)
|
{:frames (clip/output-frames clip sid)
|
||||||
:fps (:fps clip)
|
:fps (:fps clip)
|
||||||
:zoom zoom
|
:zoom zoom
|
||||||
:width (* width zoom)
|
:width (* width zoom)
|
||||||
:height (* height zoom)
|
:height (* height zoom)
|
||||||
:seconds (/ (:frames sym) (:fps clip))
|
:seconds (/ (clip/output-frames clip sid) (: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 sym) (:fps clip)) picture-fps))
|
|
||||||
(:frames sym))})))
|
|
||||||
|
|
||||||
(defn run!
|
(defn run!
|
||||||
"Render symbol `sid` 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
|
`on-progress` is called with `[done total]` as frames complete, and is where a
|
||||||
UI hangs its readout."
|
UI hangs its readout."
|
||||||
[{:keys [clip store palette ramp zoom picture-fps name audio-url]
|
[{:keys [clip store palette ramp zoom name audio-url]
|
||||||
sid :symbol isolate-id :isolate}
|
sid :symbol isolate-id :isolate}
|
||||||
exporter on-progress]
|
exporter on-progress]
|
||||||
(let [sym (some-> (clip/symbol clip sid) (isolate isolate-id))]
|
(let [sym (some-> (clip/symbol clip sid) (isolate isolate-id))]
|
||||||
|
|
@ -195,7 +190,7 @@
|
||||||
;; inside it still resolve — clip/resolver is the function that knows
|
;; inside it still resolve — clip/resolver is the function that knows
|
||||||
;; how.
|
;; how.
|
||||||
doc (assoc-in clip [:symbols sid] sym)
|
doc (assoc-in clip [:symbols sid] sym)
|
||||||
resolve-frame (clip/resolver doc sid store palette {:picture-fps picture-fps})
|
resolve-frame (clip/resolver doc sid store palette nil)
|
||||||
ras (raster/make width height)
|
ras (raster/make width height)
|
||||||
bg (get palette :bg 0)]
|
bg (get palette :bg 0)]
|
||||||
(-> (audio! doc sid store audio-url)
|
(-> (audio! doc sid store audio-url)
|
||||||
|
|
|
||||||
|
|
@ -728,7 +728,7 @@
|
||||||
at [:symbols sid :nodes id :channels [:xform :anchor]]]
|
at [:symbols sid :nodes id :channels [:xform :anchor]]]
|
||||||
(if (or (get-in c at) (node/measured? n))
|
(if (or (get-in c at) (node/measured? n))
|
||||||
c
|
c
|
||||||
(if-let [p (pick/pivot c store n (range (get-in c [:symbols sid :frames])))]
|
(if-let [p (pick/pivot c store sid n (range (get-in c [:symbols sid :frames])))]
|
||||||
(assoc-in c at (ch/framed p))
|
(assoc-in c at (ch/framed p))
|
||||||
c))))
|
c))))
|
||||||
clip
|
clip
|
||||||
|
|
@ -767,7 +767,7 @@
|
||||||
:features (merged :features) :groups (merged :groups)
|
:features (merged :features) :groups (merged :groups)
|
||||||
:symbols
|
:symbols
|
||||||
(into {:main
|
(into {:main
|
||||||
{:id :main :frames nf
|
{:id :main :fps fps :frames nf
|
||||||
:nodes (into {:root {:id :root :name "clip" :kind :group :z "a1"
|
:nodes (into {:root {:id :root :name "clip" :kind :group :z "a1"
|
||||||
:time {:mode :map :expose expose}}}
|
:time {:mode :map :expose expose}}}
|
||||||
(map-indexed
|
(map-indexed
|
||||||
|
|
@ -777,7 +777,7 @@
|
||||||
:source {:symbol id}}]))
|
:source {:symbol id}}]))
|
||||||
ordered)}}
|
ordered)}}
|
||||||
(map (fn [[id part]]
|
(map (fn [[id part]]
|
||||||
[id (place-in (:symbol part) placement)]))
|
[id (assoc (place-in (:symbol part) placement) :fps fps)]))
|
||||||
parts)}]
|
parts)}]
|
||||||
(doseq [[subject inputs] ordered
|
(doseq [[subject inputs] ordered
|
||||||
[id track] (:presence inputs)]
|
[id track] (:presence inputs)]
|
||||||
|
|
|
||||||
|
|
@ -151,7 +151,7 @@
|
||||||
(throw (ex-info "regeneration needs retained source landmarks"
|
(throw (ex-info "regeneration needs retained source landmarks"
|
||||||
{:subject subject})))
|
{:subject subject})))
|
||||||
base-params (merge take/knobs
|
base-params (merge take/knobs
|
||||||
{:fps (:fps changed)
|
{:fps (or (get-in changed [:symbols subject :fps]) (:fps changed))
|
||||||
:aspect (get-in changed [:analysis :aspect])
|
:aspect (get-in changed [:analysis :aspect])
|
||||||
:analysis (:analysis changed)})
|
:analysis (:analysis changed)})
|
||||||
;; One conditioned anchor for the whole edit, and it is the SUBJECT's.
|
;; One conditioned anchor for the whole edit, and it is the SUBJECT's.
|
||||||
|
|
|
||||||
|
|
@ -119,8 +119,8 @@
|
||||||
|
|
||||||
(defn footage
|
(defn footage
|
||||||
"A real manifest and its detected landmarks through the same measurement and
|
"A real manifest and its detected landmarks through the same measurement and
|
||||||
freeze path as the synthetic take. The source cadence stays in :fps; picture
|
freeze path as the synthetic take. Symbols keep the source fps; the reader
|
||||||
sampling is a root time map applied only after this artifact exists."
|
selects native frames on the project grid."
|
||||||
[manifest {:keys [dimensions detector subjects]}]
|
[manifest {:keys [dimensions detector subjects]}]
|
||||||
(let [[w h] dimensions
|
(let [[w h] dimensions
|
||||||
params (merge knobs
|
params (merge knobs
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@
|
||||||
(rf/reg-sub ::loop? (fn [db _] (get-in db [:playback :loop?])))
|
(rf/reg-sub ::loop? (fn [db _] (get-in db [:playback :loop?])))
|
||||||
(rf/reg-sub ::muted? (fn [db _] (get-in db [:playback :muted?])))
|
(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 ::fps (fn [db _] (get-in db [:clip :fps])))
|
||||||
(rf/reg-sub ::display-fps (fn [db _] (get-in db [:clip :display-fps])))
|
|
||||||
;; The open symbol can have a local stage size; otherwise it follows the project.
|
;; The open symbol can have a local stage size; otherwise it follows the project.
|
||||||
(rf/reg-sub ::stage-size
|
(rf/reg-sub ::stage-size
|
||||||
:<- [:arthur.subs.playback/clip-id]
|
:<- [:arthur.subs.playback/clip-id]
|
||||||
|
|
|
||||||
|
|
@ -64,11 +64,9 @@
|
||||||
|
|
||||||
(rf/reg-sub
|
(rf/reg-sub
|
||||||
::frames
|
::frames
|
||||||
:<- [::symbol]
|
:<- [::clip]
|
||||||
(fn [sym _]
|
:<- [::open]
|
||||||
;; The open symbol's length, read off it rather than copied into the db, so a
|
(fn [[document sid] _] (clip/output-frames document sid)))
|
||||||
;; drop that lengthens it lengthens the transport with it.
|
|
||||||
(:frames sym)))
|
|
||||||
|
|
||||||
(rf/reg-sub
|
(rf/reg-sub
|
||||||
::exposure
|
::exposure
|
||||||
|
|
@ -129,10 +127,9 @@
|
||||||
:<- [::open]
|
:<- [::open]
|
||||||
:<- [::store]
|
:<- [::store]
|
||||||
:<- [::palette]
|
:<- [::palette]
|
||||||
:<- [::playback/display-fps]
|
(fn [[document sid store palette] _]
|
||||||
(fn [[document sid store palette picture-fps] _]
|
|
||||||
(when (and document (clip/symbol document sid))
|
(when (and document (clip/symbol document sid))
|
||||||
(clip/resolver document sid store palette {:picture-fps picture-fps}))))
|
(clip/resolver document sid store palette nil))))
|
||||||
|
|
||||||
(rf/reg-sub
|
(rf/reg-sub
|
||||||
::shown
|
::shown
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@
|
||||||
(when n
|
(when n
|
||||||
(let [st (:store (store/entry clip-id))]
|
(let [st (:store (store/entry clip-id))]
|
||||||
(when-let [pl (nest/placement clip st open (or path [id]) f)]
|
(when-let [pl (nest/placement clip st open (or path [id]) f)]
|
||||||
(assoc pl :node n :bounds ((pick/bounds-of clip st n) (:frame pl))))))))
|
(assoc pl :node n :bounds ((pick/bounds-of clip st (:sid pl) n) (:frame pl))))))))
|
||||||
|
|
||||||
(rf/reg-sub
|
(rf/reg-sub
|
||||||
::settled-clip
|
::settled-clip
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@
|
||||||
(let [{document :clip st :store} (store/entry clip-id)]
|
(let [{document :clip st :store} (store/entry clip-id)]
|
||||||
(reset! carrying (merge {:kind :symbol :sid sid
|
(reset! carrying (merge {:kind :symbol :sid sid
|
||||||
:label (clip/symbol-name document sid)
|
:label (clip/symbol-name document sid)
|
||||||
:frames (clip/frames document sid)
|
:frames (clip/output-frames document sid)
|
||||||
;; A symbol cannot go inside itself or inside
|
;; A symbol cannot go inside itself or inside
|
||||||
;; anything it places. Refused by not ACCEPTING the
|
;; anything it places. Refused by not ACCEPTING the
|
||||||
;; drop, so the pointer says so while it hovers.
|
;; drop, so the pointer says so while it hovers.
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,6 @@
|
||||||
(defn- clip-section []
|
(defn- clip-section []
|
||||||
(let [clip @(rf/subscribe [::render/clip])
|
(let [clip @(rf/subscribe [::render/clip])
|
||||||
fps @(rf/subscribe [::playback/fps])
|
fps @(rf/subscribe [::playback/fps])
|
||||||
picture @(rf/subscribe [::playback/display-fps])
|
|
||||||
open @(rf/subscribe [::render/open])
|
open @(rf/subscribe [::render/open])
|
||||||
frames @(rf/subscribe [::render/frames])
|
frames @(rf/subscribe [::render/frames])
|
||||||
project @(rf/subscribe [::playback/project])
|
project @(rf/subscribe [::playback/project])
|
||||||
|
|
@ -98,18 +97,7 @@
|
||||||
[number-field "height" (:height clip)
|
[number-field "height" (:height clip)
|
||||||
#(rf/dispatch [::project/project-setting :height %]) nil busy?]
|
#(rf/dispatch [::project/project-setting :height %]) nil busy?]
|
||||||
[number-field "project fps" fps
|
[number-field "project fps" fps
|
||||||
#(rf/dispatch [::project/project-setting :fps %]) nil busy?]]
|
#(rf/dispatch [::project/project-setting :fps %]) nil busy?]]]))
|
||||||
;; Sampling the frozen roto at a lower rate. The source track, the duration
|
|
||||||
;; and the audio clock are untouched — a drawing is HELD, the file is never
|
|
||||||
;; short — which is why this is a picture rate and not a playback rate.
|
|
||||||
[:div.row {:style {:margin-top "5px"}}
|
|
||||||
[:span.dim "picture"]
|
|
||||||
(doall
|
|
||||||
(for [r (distinct (filter #(<= % fps) [8 12 15 24 fps]))]
|
|
||||||
^{:key r}
|
|
||||||
[:button {:class (when (= r picture) "on")
|
|
||||||
:on-click #(rf/dispatch [::pb/set-picture-fps r])}
|
|
||||||
(if (= r fps) "source" (str r))]))]]))
|
|
||||||
|
|
||||||
;; ---------------------------------------------------------------------------
|
;; ---------------------------------------------------------------------------
|
||||||
;; a node
|
;; a node
|
||||||
|
|
@ -456,7 +444,7 @@
|
||||||
[section "symbol"
|
[section "symbol"
|
||||||
[facts
|
[facts
|
||||||
"id" (str sid)
|
"id" (str sid)
|
||||||
"length" (str (:frames sym) " frames")
|
"length" (str (:frames sym) " frames at " (clip-domain/fps clip sid) " fps")
|
||||||
"stage" (let [[w h] (clip-domain/stage clip sid)] (str w "×" h))
|
"stage" (let [[w h] (clip-domain/stage clip sid)] (str w "×" h))
|
||||||
"nodes" (str (count (:nodes sym)))]
|
"nodes" (str (count (:nodes sym)))]
|
||||||
[:div.inspector-form
|
[:div.inspector-form
|
||||||
|
|
|
||||||
|
|
@ -294,8 +294,8 @@
|
||||||
[{:keys [id label duration footage? fps]} project-fps rename]
|
[{:keys [id label duration footage? fps]} project-fps rename]
|
||||||
(let [frames (js/Math.ceil (* duration project-fps))
|
(let [frames (js/Math.ceil (* duration project-fps))
|
||||||
[source length rate] (if footage?
|
[source length rate] (if footage?
|
||||||
[{:footage id} (js/Math.round (* duration fps)) (/ fps project-fps)]
|
[{:footage id :fps fps} (js/Math.round (* duration fps)) 1]
|
||||||
[{:sound id} frames 1])]
|
[{:sound id :fps project-fps} frames 1])]
|
||||||
^{:key (str (when footage? "f") id)}
|
^{:key (str (when footage? "f") id)}
|
||||||
[row (merge {:label label
|
[row (merge {:label label
|
||||||
:sub (str (.toFixed duration 1) "s")
|
:sub (str (.toFixed duration 1) "s")
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@
|
||||||
happen and not where they happen again."
|
happen and not where they happen again."
|
||||||
(:require [clojure.string :as str]
|
(:require [clojure.string :as str]
|
||||||
[arthur.domain.node :as node]
|
[arthur.domain.node :as node]
|
||||||
|
[arthur.domain.clip :as clip]
|
||||||
[arthur.domain.nest :as nest]
|
[arthur.domain.nest :as nest]
|
||||||
[arthur.domain.lane :as lane]
|
[arthur.domain.lane :as lane]
|
||||||
[arthur.domain.symbol :as symbol]
|
[arthur.domain.symbol :as symbol]
|
||||||
|
|
@ -65,8 +66,10 @@
|
||||||
(defn- source-frames
|
(defn- source-frames
|
||||||
"How long the thing this node places is, for a placement carrying no span of
|
"How long the thing this node places is, for a placement carrying no span of
|
||||||
its own. 0 for a node that places nothing, or a reference with nothing there."
|
its own. 0 for a node that places nothing, or a reference with nothing there."
|
||||||
[clip n]
|
[clip sid n]
|
||||||
(or (get-in clip [:symbols (node/source n) :frames]) 0))
|
(if-let [{:keys [at rate]} (clip/source-time clip sid n)]
|
||||||
|
(+ at (/ (or (clip/frames clip (node/source n)) 0) rate))
|
||||||
|
(clip/frames clip sid)))
|
||||||
|
|
||||||
(defn- node-label
|
(defn- node-label
|
||||||
"What to call a node in the label column.
|
"What to call a node in the label column.
|
||||||
|
|
@ -126,7 +129,7 @@
|
||||||
(or (node/placed-span
|
(or (node/placed-span
|
||||||
(cond-> n
|
(cond-> n
|
||||||
(and (= :instance (:kind n)) (nil? (:span n)))
|
(and (= :instance (:kind n)) (nil? (:span n)))
|
||||||
(assoc :span [0 (source-frames clip n)])))
|
(assoc :span [0 (source-frames clip sid n)])))
|
||||||
[0 (:frames sym)]))
|
[0 (:frames sym)]))
|
||||||
row {:path rpath
|
row {:path rpath
|
||||||
:depth depth
|
:depth depth
|
||||||
|
|
@ -167,13 +170,13 @@
|
||||||
(-> [row]
|
(-> [row]
|
||||||
(into (channel-rows n rpath (inc depth) self span))
|
(into (channel-rows n rpath (inc depth) self span))
|
||||||
(into (when-let [{:keys [at rate]} (and (= :instance (:kind n))
|
(into (when-let [{:keys [at rate]} (and (= :instance (:kind n))
|
||||||
(node/source-time n))]
|
(clip/source-time clip sid n))]
|
||||||
(when-let [child (node/source n)]
|
(when-let [child (node/source n)]
|
||||||
(walk child rpath (inc depth)
|
(walk child rpath (inc depth)
|
||||||
(comp self #(+ at (/ % rate)))))))))))))
|
(comp self #(+ at (/ % rate)))))))))))))
|
||||||
ordered))))]
|
ordered))))]
|
||||||
(if (get-in clip [:symbols sid])
|
(if (get-in clip [:symbols sid])
|
||||||
(walk sid [] 0 identity)
|
(walk sid [] 0 #(/ % (:rate (clip/grid-time clip sid))))
|
||||||
[])))
|
[])))
|
||||||
|
|
||||||
(defn sound-rows
|
(defn sound-rows
|
||||||
|
|
|
||||||
70
frontend/test/arthur/domain/cadence_test.cljs
Normal file
70
frontend/test/arthur/domain/cadence_test.cljs
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
(ns arthur.domain.cadence-test
|
||||||
|
(:require [cljs.test :refer [deftest is testing]]
|
||||||
|
[arthur.domain.bring :as bring]
|
||||||
|
[arthur.domain.cadence :as cadence]
|
||||||
|
[arthur.domain.channel :as ch]
|
||||||
|
[arthur.domain.clip :as clip]
|
||||||
|
[arthur.domain.leaf :as leaf]
|
||||||
|
[arthur.domain.nest :as nest]
|
||||||
|
[arthur.domain.node :as node]
|
||||||
|
[arthur.domain.palette :as pal]
|
||||||
|
[arthur.domain.pick :as pick]
|
||||||
|
[arthur.domain.pose :as pose]
|
||||||
|
[arthur.ui.timeline :as timeline]))
|
||||||
|
|
||||||
|
(def footage
|
||||||
|
{:fps 30 :width 100 :height 100
|
||||||
|
:symbols {:main {:id :main :fps 30 :frames 60
|
||||||
|
:nodes {:mark {:id :mark :kind :rect :z "a" :pose-group :mouth
|
||||||
|
:channels {[:geom :size] {:animated? true :pose-sampled? true
|
||||||
|
:dense {:store "sizes" :offset 0 :stride 1 :frames 60}}
|
||||||
|
[:style :color] (ch/framed :brow)}}}}}})
|
||||||
|
(def store {"sizes" {:data (js/Float64Array. (clj->js (range 1 61)))}})
|
||||||
|
|
||||||
|
(deftest selection-is-integral-and-never-early
|
||||||
|
(is (= [0 2 5 7 10 12] (mapv #(cadence/frame % 12 30) (range 6))))
|
||||||
|
(doseq [grid [8 12 24 30 60] native [12 24 30 60] f (range 90)]
|
||||||
|
(let [selected (cadence/frame f grid native)]
|
||||||
|
(is (and (integer? selected) (<= selected (/ (* f native) grid))
|
||||||
|
(< (- (/ (* f native) grid) selected) 1))))))
|
||||||
|
|
||||||
|
(deftest changing-output-fps-preserves-dense-content-and-seconds
|
||||||
|
(let [doc (clip/set-fps footage 12)
|
||||||
|
draw (clip/resolver doc :main store pal/index-of nil)]
|
||||||
|
(is (= 24 (clip/output-frames doc :main)))
|
||||||
|
(is (= (:symbols footage) (:symbols doc)))
|
||||||
|
(is (= footage (clip/set-fps doc 30)))
|
||||||
|
(is (= doc (leaf/clip "test" (leaf/leaves "test" doc))))
|
||||||
|
(is (= [1 3 6 8 11 13] (mapv #(:size (first (draw %))) (range 6))))
|
||||||
|
(is (= 7 (:frame (nest/inside doc store :main [:mark] 3))))
|
||||||
|
(is (= [0 24] (:span (first (timeline/rows doc :main #{})))))))
|
||||||
|
|
||||||
|
(deftest imported-footage-uses-selection-for-picture-and-real-speed-for-audio
|
||||||
|
(let [{doc :clip sid :sid} (bring/take (clip/set-fps (clip/blank) 12)
|
||||||
|
footage "take" "video" [15 75])
|
||||||
|
;; A host authored at 12, with a 30fps source starting half a second in.
|
||||||
|
doc (-> doc (assoc-in [:symbols :main :fps] 12)
|
||||||
|
(clip/place-symbol store :main sid 6 :insert nil))
|
||||||
|
n (get-in doc [:symbols :main :nodes :insert])
|
||||||
|
draw (clip/resolver doc :main store pal/index-of nil)
|
||||||
|
[sound] (nest/audio-tracks doc :main)]
|
||||||
|
(is (= 60 (clip/frames doc sid)))
|
||||||
|
(is (= 1 (get-in doc [:symbols sid :nodes :sound :time :rate])))
|
||||||
|
(is (= [0 24] (:span n)))
|
||||||
|
(is (empty? (draw 5)))
|
||||||
|
(is (= 8 (:size (first (draw 9)))))
|
||||||
|
(is (= 7 (:frame (nest/inside doc store :main [:insert :mark] 9))))
|
||||||
|
(is (= [-4 -4 4 4] ((pick/bounds-of doc store :main n) 3)))
|
||||||
|
(is (= [6 30] (node/placed-span sound)))
|
||||||
|
(is (= 15 (node/local-frame sound 6)))
|
||||||
|
(is (= 1 (* (get-in sound [:time :rate]) (/ (:fps doc) (:fps sound)))))
|
||||||
|
(is (= [6 30] (:span (first (timeline/rows doc :main #{[:insert]})))))
|
||||||
|
(testing "an off-grid closed mouth can win without modifying the dense source"
|
||||||
|
(let [pinned (pose/put-cut doc :main :insert :mouth 7 6)
|
||||||
|
r (clip/resolver pinned :main store pal/index-of nil)]
|
||||||
|
(is (= 7 (:size (first (r 9)))))
|
||||||
|
(is (= (get-in doc [:symbols sid]) (get-in pinned [:symbols sid])))))
|
||||||
|
(testing "a deliberate half speed still retimes audio"
|
||||||
|
(let [slow (assoc-in doc [:symbols :main :nodes :insert :playback :speed] 0.5)
|
||||||
|
[track] (nest/audio-tracks slow :main)]
|
||||||
|
(is (= 0.5 (* (get-in track [:time :rate]) (/ (:fps slow) (:fps track)))))))))
|
||||||
|
|
@ -98,7 +98,7 @@
|
||||||
[c st open path f]
|
[c st open path f]
|
||||||
(let [{:keys [sid id world frame]} (nest/placement c st open path f)
|
(let [{:keys [sid id world frame]} (nest/placement c st open path f)
|
||||||
n (get-in c [:symbols sid :nodes id])
|
n (get-in c [:symbols sid :nodes id])
|
||||||
[x0 y0 x1 y1] ((pick/bounds-of c st n) frame)]
|
[x0 y0 x1 y1] ((pick/bounds-of c st sid n) frame)]
|
||||||
{:corners (mapv #(at world %) [[x0 y0] [x1 y0] [x1 y1] [x0 y1]])
|
{:corners (mapv #(at world %) [[x0 y0] [x1 y0] [x1 y1] [x0 y1]])
|
||||||
:pivot (at world (:anchor (gesture/values n frame st)))}))
|
:pivot (at world (:anchor (gesture/values n frame st)))}))
|
||||||
|
|
||||||
|
|
@ -135,7 +135,7 @@
|
||||||
[]
|
[]
|
||||||
(let [c (two-down)]
|
(let [c (two-down)]
|
||||||
(assoc-in c [:symbols :box :nodes :shape :channels [:xform :anchor]]
|
(assoc-in c [:symbols :box :nodes :shape :channels [:xform :anchor]]
|
||||||
(ch/framed (pick/pivot c nil (get-in c [:symbols :box :nodes :shape]) [4])))))
|
(ch/framed (pick/pivot c nil :box (get-in c [:symbols :box :nodes :shape]) [4])))))
|
||||||
|
|
||||||
(deftest dragging-a-corner-away-from-the-middle-makes-it-bigger
|
(deftest dragging-a-corner-away-from-the-middle-makes-it-bigger
|
||||||
(doseq [[what c path f]
|
(doseq [[what c path f]
|
||||||
|
|
@ -287,5 +287,5 @@
|
||||||
(deftest an-instances-box-is-what-its-symbol-draws
|
(deftest an-instances-box-is-what-its-symbol-draws
|
||||||
(let [c (two-down)
|
(let [c (two-down)
|
||||||
{:keys [frame]} (nest/placement c nil :main [u v] 16)]
|
{:keys [frame]} (nest/placement c nil :main [u v] 16)]
|
||||||
(is (= [0 0 10 10] ((pick/bounds-of c nil (get-in c [:symbols :mid :nodes v])) frame)))
|
(is (= [0 0 10 10] ((pick/bounds-of c nil :mid (get-in c [:symbols :mid :nodes v])) frame)))
|
||||||
(is (= [0 0 10 10] ((pick/bounds-of c nil (get-in c [:symbols :box :nodes :shape])) 4)))))
|
(is (= [0 0 10 10] ((pick/bounds-of c nil :box (get-in c [:symbols :box :nodes :shape])) 4)))))
|
||||||
|
|
|
||||||
|
|
@ -82,9 +82,7 @@
|
||||||
:sym/poses symbol}}
|
:sym/poses symbol}}
|
||||||
store {"sizes" {:data values}}
|
store {"sizes" {:data values}}
|
||||||
resolve (clip/resolver document :main store pal/index-of nil)
|
resolve (clip/resolver document :main store pal/index-of nil)
|
||||||
low-resolve (clip/resolver document :main store pal/index-of {:picture-fps 8})
|
at (fn [f] (into {} (map (fn [op] [(:node op) op])) (resolve f)))]
|
||||||
at (fn [f] (into {} (map (fn [op] [(:node op) op])) (resolve f)))
|
|
||||||
low-at (fn [f] (into {} (map (fn [op] [(:node op) op])) (low-resolve f)))]
|
|
||||||
(is (empty? (clip/problems document)))
|
(is (empty? (clip/problems document)))
|
||||||
(is (= document (leaf/clip "stage" (leaf/leaves "stage" document))))
|
(is (= document (leaf/clip "stage" (leaf/leaves "stage" document))))
|
||||||
(is (= 102 (:size (get (at 7) [:first :mouth]))) "eight static frames, plus its correction")
|
(is (= 102 (:size (get (at 7) [:first :mouth]))) "eight static frames, plus its correction")
|
||||||
|
|
@ -97,14 +95,8 @@
|
||||||
(is (nil? (get (at 9) [:first :mouth]))
|
(is (nil? (get (at 9) [:first :mouth]))
|
||||||
"generated visibility is read from the same selected pose")
|
"generated visibility is read from the same selected pose")
|
||||||
(is (some? (get (at 9) [:second :mouth])))
|
(is (some? (get (at 9) [:second :mouth])))
|
||||||
(is (= 105 (:size (get (low-at 7) [:first :brow])))
|
|
||||||
"picture rate samples the generated base and leaves correction time alone")
|
|
||||||
(is (= 22 (:size (get (low-at 8) [:first :mouth])))
|
|
||||||
"an explicit cut occurs at its exact local frame, even off the picture grid")
|
|
||||||
(is (= 8 (:cx (get (low-at 8) [:first :eye])))
|
|
||||||
"authored position ignores the picture grid")
|
|
||||||
(let [sym (get-in document [:symbols :sym/poses])
|
(let [sym (get-in document [:symbols :sym/poses])
|
||||||
opts {:source-fps 30 :picture-fps 8 :pose-tracks {:mouth {0 0, 8 20}}}]
|
opts {:pose-tracks {:mouth {0 0, 8 20}}}]
|
||||||
(is (= (mapv #(select-keys % [:node :cx :size])
|
(is (= (mapv #(select-keys % [:node :cx :size])
|
||||||
(symbol/eval-frame sym 8 store pal/index-of opts))
|
(symbol/eval-frame sym 8 store pal/index-of opts))
|
||||||
(mapv #(select-keys % [:node :cx :size])
|
(mapv #(select-keys % [:node :cx :size])
|
||||||
|
|
@ -249,7 +241,7 @@
|
||||||
made (clip/new-symbol c :outer id 20 u)]
|
made (clip/new-symbol c :outer id 20 u)]
|
||||||
(is (= :symbol-1 id))
|
(is (= :symbol-1 id))
|
||||||
(is (= :symbol-2 (clip/fresh-id made)) "the next one does not collide")
|
(is (= :symbol-2 (clip/fresh-id made)) "the next one does not collide")
|
||||||
(is (= {:id :symbol-1 :name "symbol-1" :frames 180 :nodes {}}
|
(is (= {:id :symbol-1 :name "symbol-1" :fps 30 :frames 180 :nodes {}}
|
||||||
(clip/symbol made :symbol-1))
|
(clip/symbol made :symbol-1))
|
||||||
"empty, and as long as the rest of what it was placed in")
|
"empty, and as long as the rest of what it was placed in")
|
||||||
(is (= {:span [0 180] :time {:mode :map :at 20 :rate 1}}
|
(is (= {:span [0 180] :time {:mode :map :at 20 :rate 1}}
|
||||||
|
|
|
||||||
|
|
@ -165,8 +165,8 @@
|
||||||
(is (= 0 (:frame (nest/inside doc nil :main [:a] 3))))
|
(is (= 0 (:frame (nest/inside doc nil :main [:a] 3))))
|
||||||
(is (nil? (:time (nest/inside doc nil :main [:a] 3))))
|
(is (nil? (:time (nest/inside doc nil :main [:a] 3))))
|
||||||
(is (nil? (nest/inside doc nil :main [:a] 4)))
|
(is (nil? (nest/inside doc nil :main [:a] 4)))
|
||||||
(is (= ((pick/bounds-of doc nil n) 2)
|
(is (= ((pick/bounds-of doc nil :main n) 2)
|
||||||
((pick/bounds-of doc nil (assoc-in n [:playback :in] 5)) 0)))))
|
((pick/bounds-of doc nil :main (assoc-in n [:playback :in] 5)) 0)))))
|
||||||
|
|
||||||
(deftest seeking-and-source-reuse-do-not-share-cursors
|
(deftest seeking-and-source-reuse-do-not-share-cursors
|
||||||
(let [doc (assoc-in (document) [:symbols :main :nodes :b :source :symbol] :drawing-a)
|
(let [doc (assoc-in (document) [:symbols :main :nodes :b :source :symbol] :drawing-a)
|
||||||
|
|
|
||||||
|
|
@ -49,8 +49,8 @@
|
||||||
;; A TIMELINE ID IS A SEGMENT, which is what lets a symbol's nodes be
|
;; A TIMELINE ID IS A SEGMENT, which is what lets a symbol's nodes be
|
||||||
;; addressed by the same path shape as the clip's own. `main` is the root.
|
;; addressed by the same path shape as the clip's own. `main` is the root.
|
||||||
(is (contains? ls "clip/c7/symbol/main"))
|
(is (contains? ls "clip/c7/symbol/main"))
|
||||||
(is (= {:frames 229} (get ls "clip/c7/symbol/main"))
|
(is (= {:frames 229 :fps 30} (get ls "clip/c7/symbol/main"))
|
||||||
"a timeline's leaf is its frame space; :fps is the clip's")
|
"a symbol persists its native grid separately from the output grid")
|
||||||
;; Nodes are local to the face timeline; feature and group ids are clip-wide.
|
;; Nodes are local to the face timeline; feature and group ids are clip-wide.
|
||||||
(is (contains? ls "clip/c7/symbol/face-1/node/mouth"))
|
(is (contains? ls "clip/c7/symbol/face-1/node/mouth"))
|
||||||
(is (contains? ls "clip/c7/symbol/face-1/channel/mouth/geom.pts"))
|
(is (contains? ls "clip/c7/symbol/face-1/channel/mouth/geom.pts"))
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,5 @@
|
||||||
(ns arthur.export-test
|
(ns arthur.export-test
|
||||||
"The frame walk and the arithmetic above the sink.
|
"The output-grid frame walk and duration, using a recording sink."
|
||||||
|
|
||||||
THE SYNC RULE IS THE POINT OF THIS FILE. `arthur.export` states it twice — in
|
|
||||||
its own docstring and in `plan`'s comment — because it is the one failure the
|
|
||||||
export path exists to prevent: a lower picture rate must HOLD each pose across
|
|
||||||
several frames and never drop frames, so the emitted length always matches the
|
|
||||||
audio. Decimating instead gives a file that is silently short, whose sound
|
|
||||||
slides progressively out of sync, and which looks correct in every other
|
|
||||||
respect. Nothing downstream can detect that, so it is asserted here, at both
|
|
||||||
levels: `plan` reports poses separately from frames, and `run!` emits every
|
|
||||||
frame of the frame space whatever the picture rate is.
|
|
||||||
|
|
||||||
The sink is a recording fake. What the walk owes a sink is an ordering and a
|
|
||||||
count, and a fake is the only way to assert on those without also asserting on
|
|
||||||
PNG bytes — which `export.frames-test` already does."
|
|
||||||
(:require [cljs.test :refer [deftest is testing async]]
|
(:require [cljs.test :refer [deftest is testing async]]
|
||||||
[arthur.domain.channel :as ch]
|
[arthur.domain.channel :as ch]
|
||||||
[arthur.domain.clip :as clip]
|
[arthur.domain.clip :as clip]
|
||||||
|
|
@ -85,21 +71,12 @@
|
||||||
(is (= 1 (zoom-of nil)) "an absent zoom is 1:1")
|
(is (= 1 (zoom-of nil)) "an absent zoom is 1:1")
|
||||||
(is (= 1 (zoom-of 1.9)))))
|
(is (= 1 (zoom-of 1.9)))))
|
||||||
|
|
||||||
(deftest a-lower-picture-rate-changes-the-poses-and-not-the-length
|
(deftest a-lower-output-rate-keeps-native-duration
|
||||||
;; THE SYNC RULE, in the arithmetic. 48 frames at 24fps is two seconds; at a
|
(let [doc (assoc-in (a-clip {:frames 48 :fps 24}) [:symbols :main :fps] 24)
|
||||||
;; 12fps picture rate it is still 48 frames and two seconds, holding 24 poses.
|
p (export/plan {:clip (clip/set-fps doc 12) :symbol :main})]
|
||||||
;; If :frames ever tracks :poses here, every export at a reduced picture rate
|
(is (= 24 (:frames p)))
|
||||||
;; comes out half length with the audio sliding off it.
|
(is (= 12 (:fps p)))
|
||||||
(let [p (export/plan {:clip (a-clip {:frames 48 :fps 24}) :symbol :main
|
(is (= 2 (:seconds p)))))
|
||||||
:picture-fps 12})]
|
|
||||||
(is (= 48 (:frames p)) "the frame count does not move")
|
|
||||||
(is (= 2 (:seconds p)) "and neither does the duration")
|
|
||||||
(is (= 24 (:poses p)) "but the picture holds half as many poses"))
|
|
||||||
(testing "a picture rate at or above the clip's rate changes nothing"
|
|
||||||
(doseq [fps [24 48 nil]]
|
|
||||||
(let [p (export/plan {:clip (a-clip {:frames 48 :fps 24}) :symbol :main
|
|
||||||
:picture-fps fps})]
|
|
||||||
(is (= 48 (:poses p)) (str "picture-fps " fps))))))
|
|
||||||
|
|
||||||
(deftest plan-of-a-symbol-that-is-not-there-is-nothing
|
(deftest plan-of-a-symbol-that-is-not-there-is-nothing
|
||||||
(is (nil? (export/plan {:clip (a-clip {}) :symbol :nope :zoom 1}))))
|
(is (nil? (export/plan {:clip (a-clip {}) :symbol :nope :zoom 1}))))
|
||||||
|
|
@ -115,17 +92,12 @@
|
||||||
(done)))
|
(done)))
|
||||||
(.catch (fn [e] (is false (str "threw: " e)) (done))))))
|
(.catch (fn [e] (is false (str "threw: " e)) (done))))))
|
||||||
|
|
||||||
(deftest a-lower-picture-rate-still-emits-every-frame
|
(deftest a-lower-output-rate-emits-the-selected-grid
|
||||||
;; THE SYNC RULE, in the walk — the assertion that matters most in this file.
|
|
||||||
;; The poses repeat; the frames do not thin out.
|
|
||||||
(async done
|
(async done
|
||||||
(-> (run!* (a-clip {:frames 12 :fps 24}) :picture-fps 8)
|
(-> (run!* (clip/set-fps (a-clip {:frames 12 :fps 24}) 8))
|
||||||
(.then (fn [{:keys [frames spec]}]
|
(.then (fn [{:keys [frames spec]}]
|
||||||
(is (= 12 (count frames))
|
(is (= (range 4) (map :i frames)))
|
||||||
"a 12-frame timeline exports 12 frames at any picture rate")
|
(is (= 8 (:fps spec)))
|
||||||
(is (= (range 12) (map :i frames)))
|
|
||||||
(is (= 24 (:fps spec))
|
|
||||||
"and the file's rate is the CLIP's, not the picture rate")
|
|
||||||
(done)))
|
(done)))
|
||||||
(.catch (fn [e] (is false (str "threw: " e)) (done))))))
|
(.catch (fn [e] (is false (str "threw: " e)) (done))))))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -336,14 +336,14 @@
|
||||||
frames (range (get-in c [:symbols sid :frames]))
|
frames (range (get-in c [:symbols sid :frames]))
|
||||||
anchor (:value (get-in n [:channels [:xform :anchor]]))
|
anchor (:value (get-in n [:channels [:xform :anchor]]))
|
||||||
usable (and (not (node/measured? n))
|
usable (and (not (node/measured? n))
|
||||||
(some? (pick/pivot c @store n frames)))]
|
(some? (pick/pivot c @store sid n frames)))]
|
||||||
(is (= usable (some? anchor))
|
(is (= usable (some? anchor))
|
||||||
(str sid "/" id " has a pivot: " (some? anchor)
|
(str sid "/" id " has a pivot: " (some? anchor)
|
||||||
", but a hand can use one: " usable))
|
", but a hand can use one: " usable))
|
||||||
(is (= (nil? (gesture/refusal n)) (not (node/measured? n)))
|
(is (= (nil? (gesture/refusal n)) (not (node/measured? n)))
|
||||||
(str sid "/" id ": `refusal` and `measured?` disagree"))
|
(str sid "/" id ": `refusal` and `measured?` disagree"))
|
||||||
(when anchor
|
(when anchor
|
||||||
(let [bounds (pick/bounds-of c @store n)
|
(let [bounds (pick/bounds-of c @store sid n)
|
||||||
[x0 y0 x1 y1] (reduce #(let [k (bounds %2)]
|
[x0 y0 x1 y1] (reduce #(let [k (bounds %2)]
|
||||||
(cond (nil? %1) k (nil? k) %1
|
(cond (nil? %1) k (nil? k) %1
|
||||||
:else (mapv (fn [op i] (op (nth %1 i) (nth k i)))
|
:else (mapv (fn [op i] (op (nth %1 i) (nth k i)))
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue