Compare commits
2 commits
4e5e02c856
...
f5a39aee39
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5a39aee39 | ||
|
|
0cb9d0ebf6 |
17 changed files with 767 additions and 136 deletions
66
docs/one-grid-plan.md
Normal file
66
docs/one-grid-plan.md
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
# The editing grid is the symbol's own frames
|
||||
|
||||
## What was wrong
|
||||
|
||||
Every number a person authors — a span, a `:time :at`, a key, a cut — is in the
|
||||
frame space of the symbol it lives in (`docs/time.md`, and that part is right).
|
||||
The timeline, though, drew its ruler in OUTPUT frames: `clip/output-frames`, the
|
||||
transport's length. In a 12fps project holding 30fps symbols those two spaces sit
|
||||
at a ratio of 2.5, so:
|
||||
|
||||
* a clip at symbol frame 31 was drawn at ruler frame 12.4 — **no clip edge landed
|
||||
on a frame mark**, because almost none of them can;
|
||||
* a gesture measured in ruler frames had to be multiplied into the symbol's
|
||||
frames and rounded (`nest/dragged`), so dragging one ruler frame moved the clip
|
||||
2 or 3 symbol frames — **0.8 or 1.2 ruler frames, never the 1 the pointer
|
||||
said**. That is the jumpiness;
|
||||
* the preview drew the gesture's own number of ruler frames while the commit
|
||||
wrote the rounded one, so **the ghost sat somewhere the settled clip did not**;
|
||||
* before the rounding was added, the fraction went into the document and every
|
||||
later edge edit on that clip was refused for ever (`span/*` refuses a
|
||||
fractional edge, as it should).
|
||||
|
||||
One cause, four symptoms. None of them is an edge case to patch.
|
||||
|
||||
## The model
|
||||
|
||||
1. **A symbol's own frames are the only coordinate anything authored lives in.**
|
||||
Unchanged.
|
||||
2. **The editor edits in the open symbol's frames.** The ruler, the marks, the
|
||||
playhead's position on it, every pointer→frame answer, every drop frame and
|
||||
every drag delta are the open symbol's frames. No multiplication anywhere in
|
||||
the gesture path, so no rounding and nothing fractional to refuse.
|
||||
3. **The output grid is playback's alone** — the clock, the audio mix, export,
|
||||
and the frame the stage draws. Exactly two pure functions cross between them
|
||||
and nothing else does:
|
||||
* `clip/shown-frame clip sid f` — which of `sid`'s frames output frame `f`
|
||||
shows (`cadence/frame`: the latest at or before it).
|
||||
* `clip/first-output-frame clip sid n` — the output frame that first shows
|
||||
symbol frame `n`; the inverse, for seeking from the ruler.
|
||||
4. `nest/inside`, `nest/placement`, `nest/spans` and the gestures take the
|
||||
subject symbol's OWN frame. They used to take an output frame and multiply it
|
||||
secretly, which is what made every caller's units a guess. A caller holding
|
||||
the playhead converts with `clip/shown-frame`, at its own edge, visibly.
|
||||
|
||||
## The gestures
|
||||
|
||||
5. **One pointer→frame function** for the whole timeline, `frame-under`. A drag's
|
||||
delta is the difference of two of its answers, never a pixel ratio rounded
|
||||
separately — so the preview and the commit are the same number by
|
||||
construction.
|
||||
6. **The junction between two clips is one handle with one meaning**: roll. It
|
||||
moves the end of the left clip and the start of the right one together, which
|
||||
is `span/roll`, which is already nothing but `resize-out` then `resize-in`.
|
||||
The three 4px-wide zones it used to pick between — trim-left, roll,
|
||||
trim-right, inside twelve pixels — were the "it just picks one" the handle
|
||||
was accused of. An edge that is not shared still has its own in/out handles.
|
||||
|
||||
## Audio goes with the picture it belongs to
|
||||
|
||||
A take's sound lived inside the take symbol, so placing the take brought it and
|
||||
placing the FACE the take is made of brought nothing. A symbol now says what it
|
||||
sounds like — `:audio`, a sound source — and placing one places a linked audio
|
||||
node beside the clip. Detection sets it on the face it extracts, which is the
|
||||
automatic link; `::ui/link-audio` sets or clears it by hand, which is the manual
|
||||
one. `:linked-to` on the audio node already existed and already follows a moved
|
||||
picture.
|
||||
|
|
@ -64,9 +64,16 @@
|
|||
|
||||
`frozen` is what `flow/freeze/clip` makes: a `:main` that places one symbol per
|
||||
tracked face. `:main` becomes the named symbol — it is what holds the faces in
|
||||
stage pixels, so it is the thing worth placing — and it gets the take's SOUND
|
||||
as an audio node of its own, source frames `range` of footage `footage-id`, so
|
||||
wherever the symbol is placed it is heard.
|
||||
stage pixels, so it is the thing worth placing. Each generated face symbol gets
|
||||
an automatic association with the take's SOUND, source frames `range` of
|
||||
footage `footage-id`. Thus the take is heard through the faces it places, and
|
||||
a face subsequently placed by itself still brings its sound. This is the same
|
||||
shape a future manual symbol/sound association can write; dropping a face is
|
||||
not a special operation.
|
||||
|
||||
A multi-face take consequently reaches the same recording through several
|
||||
symbols. `nest/audio-tracks` collapses simultaneous copies carrying the same
|
||||
`:media-link`; placing those faces at different times still schedules each one.
|
||||
|
||||
The tracking identities, and the analysis they were measured by, come along
|
||||
only when `clip` has no analysis of its own and no face had to be renamed. A
|
||||
|
|
@ -76,20 +83,27 @@
|
|||
[clip frozen label footage-id range]
|
||||
(let [{c :clip ids :ids} (symbols clip frozen [:main] {:main (symbol-id label)})
|
||||
sid (ids :main)
|
||||
faces (mapv ids (sort-by str (keys (:subjects frozen))))
|
||||
;; Older/plain imported clips have no subject table. They are still a
|
||||
;; take, so their wrapper remains the only honest owner of the sound.
|
||||
sound-hosts (if (seq faces) faces [sid])
|
||||
sound {:id :sound :name "sound" :kind :audio :parent nil
|
||||
:z "z-sound" :source {:footage footage-id}
|
||||
;; All automatic copies name one recording. The audio walk
|
||||
;; uses this identity only to avoid mixing that recording once
|
||||
;; per detected face when the complete take is played.
|
||||
:media-link [:footage footage-id range]
|
||||
;; Source frame `start` plays on the symbol's 0.
|
||||
:span range
|
||||
:time {:mode :map :at (- (first range)) :rate 1}}
|
||||
tracked? (and (nil? (:analysis clip))
|
||||
(every? #(= % (ids %)) (keys (:subjects frozen))))]
|
||||
{:sid sid
|
||||
:tracked? tracked?
|
||||
:clip (cond-> (-> c
|
||||
(assoc-in [:symbols sid :name] (str label))
|
||||
(assoc-in [:symbols sid :nodes :sound]
|
||||
{:id :sound :name "sound" :kind :audio :parent nil
|
||||
:z "z-sound" :source {:footage footage-id}
|
||||
;; Source frame `start` plays on the symbol's 0.
|
||||
:span range
|
||||
:time {:mode :map
|
||||
:at (- (first range))
|
||||
:rate 1}}))
|
||||
:clip (cond-> (-> (reduce (fn [document face]
|
||||
(assoc-in document [:symbols face :nodes :sound] sound))
|
||||
c sound-hosts)
|
||||
(assoc-in [:symbols sid :name] (str label)))
|
||||
tracked? (-> (assoc :analysis (:analysis frozen))
|
||||
(update :subjects merge (:subjects frozen))
|
||||
(update :features merge (:features frozen))
|
||||
|
|
|
|||
|
|
@ -13,3 +13,13 @@
|
|||
"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))))))
|
||||
|
||||
(defn reader-frame
|
||||
"The earliest reader frame whose `frame` is at or after native frame n — the
|
||||
inverse of `frame`, as far as it has one.
|
||||
|
||||
`frame` is a floor, so several reader frames can show one native frame and a
|
||||
native frame between two of them is shown by none: this answers with the
|
||||
reader frame that first reaches it, which is what seeking to a mark means."
|
||||
[n grid native]
|
||||
(js/Math.ceil (if (and grid native) (/ (* n grid) native) n)))
|
||||
|
|
|
|||
|
|
@ -84,6 +84,27 @@
|
|||
(defn grid-time [clip sid]
|
||||
{:at 0 :rate (cadence/ratio (:fps clip) (fps clip sid))})
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; the only crossing between the output grid and a symbol's own frames
|
||||
;;
|
||||
;; Everything authored is in a symbol's own frames and every editing gesture is
|
||||
;; too — see `docs/one-grid-plan.md`. The output grid belongs to playback: the
|
||||
;; clock, the audio mix, export, and the frame number the stage is drawing. These
|
||||
;; two functions are the whole of the way between, so a caller that has a
|
||||
;; playhead and needs a document coordinate says so in one visible call instead
|
||||
;; of multiplying by a rate it had to know about.
|
||||
|
||||
(defn shown-frame
|
||||
"Which of symbol `sid`'s own frames the output frame `f` shows."
|
||||
[clip sid f]
|
||||
(cadence/frame f (:fps clip) (fps clip sid)))
|
||||
|
||||
(defn first-output-frame
|
||||
"The output frame that first shows symbol `sid`'s own frame `n`: what to seek
|
||||
to to put the playhead on a mark of `sid`'s ruler."
|
||||
[clip sid n]
|
||||
(cadence/reader-frame n (:fps clip) (fps clip sid)))
|
||||
|
||||
(defn source-time
|
||||
"Derived cel-to-content map. Frame-rate units never enter stored retimes."
|
||||
[clip host n]
|
||||
|
|
|
|||
|
|
@ -39,13 +39,20 @@
|
|||
r))
|
||||
|
||||
(defn inside
|
||||
"Walk row path `path` down from symbol `sid`, whose frame `f` is showing, into
|
||||
the node it ends at. Returns `{:sid :frame :matrix :time}`: the symbol that
|
||||
"Walk row path `path` down from symbol `sid`, whose OWN frame `f` is showing,
|
||||
into the node it ends at. Returns `{:sid :frame :matrix :time}`: the symbol that
|
||||
node places (nil for one that places none), the frame of its own it is
|
||||
showing, the matrix from its coordinates to `sid`'s, and the time map from
|
||||
`sid`'s frames to its own — or nil when a node on the way is not on screen at
|
||||
that frame, where there is no inside to be in.
|
||||
|
||||
`f` IS ONE OF `sid`'S OWN FRAMES, which is the coordinate everything authored
|
||||
and every editing gesture is in — see `docs/one-grid-plan.md`. It used to be an
|
||||
OUTPUT frame, multiplied into `sid`'s space here, which made the units of the
|
||||
argument something each caller had to know without being told and put a
|
||||
2.5-frame step between what the ruler offered and what the document could hold.
|
||||
A caller holding the playhead converts with `clip/shown-frame`.
|
||||
|
||||
THE SAME STEP FOR EVERY NODE. Inside an instance is the symbol it places;
|
||||
inside a shape is where its points and keys are. Either way it is the node's
|
||||
own coordinates and frames, so a shape any depth down is edited through the
|
||||
|
|
@ -84,8 +91,8 @@
|
|||
(cond-> (reduce node/then-time time (map node/time-of chain))
|
||||
inst? (node/then-time (clip/source-time clip sid (get nodes id)))))}
|
||||
(reduced nil))))
|
||||
{:sid sid :frame (js/Math.floor (* f (:rate (clip/grid-time clip sid))))
|
||||
:matrix (node/mat) :time (clip/grid-time clip sid)}
|
||||
{:sid sid :frame (js/Math.floor f)
|
||||
:matrix (node/mat) :time node/same-time}
|
||||
path))
|
||||
|
||||
(defn placement
|
||||
|
|
@ -127,7 +134,12 @@
|
|||
(defn audio-tracks
|
||||
"Flatten audible source intervals through cel and parent clocks.
|
||||
A held visual source is silent. Every returned track carries a source offset,
|
||||
an output interval, and automation mapped into the open symbol's time."
|
||||
an output interval, and automation mapped into the open symbol's time.
|
||||
|
||||
Automatically associated face audio can be reached more than once in a
|
||||
multi-face take. Equal `:media-link`s at the same source and output interval
|
||||
are one recording, not a louder mix; at different placements they remain
|
||||
separate scheduled clips."
|
||||
[clip sid]
|
||||
(letfn [(to-local [m f] (* (:rate m) (- f (:at m))))
|
||||
(to-outer [m f] (+ (:at m) (/ f (:rate m))))
|
||||
|
|
@ -193,7 +205,17 @@
|
|||
periods))))
|
||||
nil))))
|
||||
(sort-by (comp str key) nodes))))]
|
||||
(vec (walk sid (clip/grid-time clip sid) [0 (clip/output-frames clip sid)] [] #{}))))
|
||||
(let [tracks (walk sid (clip/grid-time clip sid)
|
||||
[0 (clip/output-frames clip sid)] [] #{})]
|
||||
(second
|
||||
(reduce (fn [[seen out] track]
|
||||
(let [link (:media-link track)
|
||||
k (when link [link (:source track) (:span track)
|
||||
(select-keys (:time track) [:at :rate :offset])])]
|
||||
(if (and k (contains? seen k))
|
||||
[seen out]
|
||||
[(cond-> seen k (conj k)) (conj out track)])))
|
||||
[#{} []] tracks)))))
|
||||
|
||||
(defn- retime
|
||||
"Node `n` with its own time map replaced by `m`, and nothing else touched: its
|
||||
|
|
@ -353,7 +375,28 @@
|
|||
sids path)]
|
||||
{:sid (last sids)
|
||||
:time (when (every? some? maps)
|
||||
(reduce node/then-time (clip/grid-time clip sid) maps))}))
|
||||
(reduce node/then-time node/same-time maps))}))
|
||||
|
||||
(defn- dragged
|
||||
"Frame `from` of node `id`'s own space, dragged `df` frames of the open
|
||||
symbol: ONE WHOLE FRAME of that space.
|
||||
|
||||
THE ONE PLACE A RULER GESTURE BECOMES A FRAME NUMBER, and the whole of it.
|
||||
`df` counts the open symbol's own frames, which is what the ruler is drawn in,
|
||||
so for a row of the open symbol itself this is `from + df` and nothing happens
|
||||
here at all. It earns its keep for a row reached THROUGH a retimed instance,
|
||||
where one frame of the ruler is a fraction of the node's own: a frame number is
|
||||
the one thing in a document that cannot fall between two frames —
|
||||
`span/resize-out`, `resize-in` and `roll` refuse a fractional edge outright,
|
||||
and `slide` would have let it into `:time :at` and refused every later edge
|
||||
edit on that clip for ever.
|
||||
|
||||
Rounding, not flooring: a drag is a gesture at a position, and the frame it
|
||||
means is the nearer one in both directions. Nothing else belongs here."
|
||||
[here nodes id from df]
|
||||
(let [chain (map #(get nodes %) (reverse (rest (symbol/lineage nodes id))))
|
||||
rate (:rate (reduce node/then-time (:time here) (map node/time-of chain)))]
|
||||
(js/Math.round (+ from (* df rate)))))
|
||||
|
||||
(defn slide
|
||||
"Move the node at row path `path` along its symbol's time by `df` frames of
|
||||
|
|
@ -370,10 +413,23 @@
|
|||
(nil? (get nodes id)) {:refused "nothing to move"}
|
||||
(nil? (:time here)) {:refused "a looping instance is in the way"}
|
||||
:else
|
||||
(let [chain (map #(get nodes %) (reverse (rest (symbol/lineage nodes id))))
|
||||
d (* df (:rate (reduce node/then-time (:time here) (map node/time-of chain))))
|
||||
(let [n (get nodes id)
|
||||
;; Measured from where the node STARTS, so what lands on a whole
|
||||
;; frame is the thing you can see moving. A node with no span is
|
||||
;; on screen throughout and only its `:at` moves.
|
||||
from (or (first (node/placed-span n)) (get-in n [:time :at] 0))
|
||||
d (- (dragged here nodes id from df) from)
|
||||
;; MOVE THE MAP THAT IS THERE; write a fresh one only where there is
|
||||
;; none. The test for "there is one" is `:time` itself, and whether
|
||||
;; it is the affine kind is `node/mapped-time?` — which an absent
|
||||
;; `:mode` satisfies, because `node/time-of` has always read it as
|
||||
;; `:map`. Asking for the key instead said no to every clip
|
||||
;; `span/held` makes, whose `:time` is `{:at f :rate 1}` and nothing
|
||||
;; more, and the else branch then REPLACED that map with one built
|
||||
;; from `d` alone: the first drag of a freshly drawn clip threw away
|
||||
;; its `:at` and jumped it to the head of the lane.
|
||||
shift (fn [n]
|
||||
(if (= :map (get-in n [:time :mode]))
|
||||
(if (and (:time n) (node/mapped-time? n))
|
||||
(update-in n [:time :at] (fnil + 0) d)
|
||||
(assoc n :time {:mode :map :at d :rate 1})))
|
||||
moved (assoc nodes id (shift (get nodes id)))
|
||||
|
|
@ -385,11 +441,20 @@
|
|||
(if (and (= :audio (:kind n)) (= id (:linked-to n)))
|
||||
(assoc ns audio-id (shift n))
|
||||
ns))
|
||||
moved nodes))
|
||||
ps (symbol/problems (assoc (clip/symbol clip (:sid here)) :nodes moved))]
|
||||
(if (seq ps)
|
||||
{:refused (first ps)}
|
||||
{:clip (assoc-in clip [:symbols (:sid here) :nodes] moved)})))))
|
||||
moved nodes))]
|
||||
;; COMMITTED LIKE EVERY OTHER SPAN EDIT. This validated with
|
||||
;; `symbol/problems` and wrote the nodes in itself, which made it a
|
||||
;; SECOND commit path — and the one `span/finish`'s docstring says is
|
||||
;; the only one: "no command can commit an overlap in lane mode, and
|
||||
;; `symbol/overlaps` turning up anything is a bug in a command rather
|
||||
;; than a state to design around". It was that bug. A body drag could
|
||||
;; leave two clips of a lane on screen over the same frames, and from
|
||||
;; there the lane stops behaving: `symbol/children` has two clips
|
||||
;; claiming one frame, so which drawing a polygon lands in and whether
|
||||
;; a boundary can be rolled depend on which of them `some` reaches
|
||||
;; first. `finish` does the same `problems` check and the overlap one
|
||||
;; too, so this is less code and one fewer invariant to remember.
|
||||
(span/finish clip (:sid here) moved id :grow-symbol)))))
|
||||
|
||||
(defn resize-out
|
||||
"Move the right edge of the node at `path` by `df` frames of `open`.
|
||||
|
|
@ -407,10 +472,8 @@
|
|||
(nil? n) {:refused "nothing to resize"}
|
||||
(nil? (:time here)) {:refused "a looping instance is in the way"}
|
||||
:else
|
||||
(let [chain (map #(get nodes %) (reverse (rest (symbol/lineage nodes id))))
|
||||
d (* df (:rate (reduce node/then-time (:time here) (map node/time-of chain))))
|
||||
to (+ (second (node/placed-span n)) d)]
|
||||
(span/resize-out clip sid id to {:ripple? ripple? :extent :grow-symbol})))))
|
||||
(span/resize-out clip sid id (dragged here nodes id (second (node/placed-span n)) df)
|
||||
{:ripple? ripple? :extent :grow-symbol}))))
|
||||
|
||||
(defn resize-in
|
||||
"Move the left edge of the node at `path` by `df` frames of `open`."
|
||||
|
|
@ -424,10 +487,7 @@
|
|||
(nil? n) {:refused "nothing to resize"}
|
||||
(nil? (:time here)) {:refused "a looping instance is in the way"}
|
||||
:else
|
||||
(let [chain (map #(get nodes %) (reverse (rest (symbol/lineage nodes id))))
|
||||
d (* df (:rate (reduce node/then-time (:time here) (map node/time-of chain))))
|
||||
to (+ (first (node/placed-span n)) d)]
|
||||
(span/resize-in clip sid id to)))))
|
||||
(span/resize-in clip sid id (dragged here nodes id (first (node/placed-span n)) df)))))
|
||||
|
||||
(defn roll
|
||||
"Move the shared boundary at `right-path` and the adjacent `left-path`."
|
||||
|
|
@ -443,10 +503,8 @@
|
|||
{:refused "a rolling edit needs adjacent clips in one sequence"}
|
||||
(nil? (:time here)) {:refused "a looping instance is in the way"}
|
||||
:else
|
||||
(let [chain (map #(get nodes %) (reverse (rest (symbol/lineage nodes right-id))))
|
||||
d (* df (:rate (reduce node/then-time (:time here) (map node/time-of chain))))
|
||||
to (+ (first (node/placed-span right)) d)]
|
||||
(span/roll clip sid left-id right-id to)))))
|
||||
(span/roll clip sid left-id right-id
|
||||
(dragged here nodes right-id (first (node/placed-span right)) df)))))
|
||||
|
||||
(defn restack
|
||||
"Put the node at row path `from` just in front of the one at `to` when
|
||||
|
|
|
|||
|
|
@ -161,6 +161,12 @@
|
|||
[f n]
|
||||
(if (and n (> n 1)) (* (js/Math.floor (/ f n)) n) f))
|
||||
|
||||
(def same-time
|
||||
"The identity time map: these frames ARE those frames. What a walk starts from
|
||||
before it has composed anything, and what `time-of` gives a node with no time
|
||||
of its own."
|
||||
{:at 0 :rate 1})
|
||||
|
||||
(defn time-of
|
||||
"A node's own time as the affine map it is: `{:at a :rate r}`, meaning a frame
|
||||
`p` of its parent is frame `r·(p − a)` of its own. THE SAME FOR EVERY NODE. A
|
||||
|
|
@ -179,6 +185,20 @@
|
|||
{:at (- at (/ offset rate)) :rate rate}
|
||||
{:at 0 :rate 1})))
|
||||
|
||||
(defn mapped-time?
|
||||
"Whether `n`'s own time is the affine map `time-of` reads, and therefore
|
||||
whether moving it is a write to `:time :at`.
|
||||
|
||||
AN ABSENT `:mode` IS `:map`, which is what `time-of` has always defaulted it
|
||||
to — and the default is the ordinary case, not an edge one: `span/held` writes
|
||||
`{:at f :rate 1}` with no mode at all, so every clip a drawing creates is in
|
||||
it. Code that asked `(= :map (get-in n [:time :mode]))` instead answered no
|
||||
for those, and `nest/slide` acted on the answer by REPLACING the whole map
|
||||
with a fresh one — so the first drag of a freshly drawn clip discarded its
|
||||
`:at` and teleported it to the head of the lane."
|
||||
[n]
|
||||
(= :map (:mode (:time n) :map)))
|
||||
|
||||
(defn then-time
|
||||
"`outer` then `inner`: the map from `outer`'s parent straight to `inner`'s own
|
||||
frames. Time maps compose like matrices do, which is what makes a nesting of
|
||||
|
|
|
|||
|
|
@ -32,6 +32,22 @@
|
|||
[arthur.footage.store :as store]
|
||||
[re-frame.core :as rf]))
|
||||
|
||||
(defn editing-frame
|
||||
"The frame of the open symbol the playhead is showing: the coordinate every
|
||||
command in here works in, and the only thing `[:playback :frame]` is read for.
|
||||
|
||||
THE ONE CONVERSION, AND IT IS IN ONE DIRECTION. `[:playback :frame]` counts the
|
||||
OUTPUT grid — the clock's, the audio's, export's — and everything authored is in
|
||||
the frames of the symbol it lives in. A 12fps project holding 30fps symbols puts
|
||||
2.5 of those frames in one of these, so a command handed the raw playhead was
|
||||
being handed a number in the wrong units; `clip/shown-frame` is which frame of
|
||||
the symbol that output frame actually shows. See `docs/one-grid-plan.md`.
|
||||
|
||||
`::playback/seek` takes the output frame back, through
|
||||
`clip/first-output-frame`."
|
||||
[db clip]
|
||||
(clip/shown-frame clip (get-in db [:ui :open]) (get-in db [:playback :frame])))
|
||||
|
||||
(rf/reg-event-db
|
||||
::rename-node
|
||||
(fn [db [_ sid id value]]
|
||||
|
|
@ -48,7 +64,14 @@
|
|||
|
||||
The rows above a selection are opened, so one made deep on the stage is seen
|
||||
in the timeline. Not a sound's: its row is always in the audio section, and
|
||||
opening the placement it is heard through would bury it."
|
||||
opening the placement it is heard through would bury it.
|
||||
|
||||
AN EMPTY PATH IS A REAL SELECTION, not a missing one. The open symbol's own
|
||||
lane row names itself with `[]`, and `(pop [])` THROWS — so clicking that row
|
||||
aborted the whole event: nothing was selected, nothing was aimed, and the next
|
||||
polygon went wherever the stale target still pointed, which is most of what
|
||||
made aiming a lane feel random. There is nothing above a selection at the top
|
||||
to open, so the guard is `seq` rather than `some?`."
|
||||
[db selection]
|
||||
(let [[kind sid id path] selection
|
||||
sound? (= :audio (get-in (store/entry (:clip/current db))
|
||||
|
|
@ -56,7 +79,7 @@
|
|||
(cond-> (-> db
|
||||
(assoc-in [:ui :selection] selection)
|
||||
(update :ui dissoc :points :retry))
|
||||
(and (= :node kind) path (not sound?))
|
||||
(and (= :node kind) (seq path) (not sound?))
|
||||
(update-in [:ui :expanded] (fnil into #{}) (rest (reductions conj [] (pop path)))))))
|
||||
|
||||
(defn target-of
|
||||
|
|
@ -173,12 +196,18 @@
|
|||
[db sid result retry]
|
||||
(let [{clip :clip st :store} (store/entry (:clip/current db))
|
||||
path (nth (get-in db [:ui :selection]) 3 nil)
|
||||
{:keys [at rate]} (:time (nest/inside clip st (get-in db [:ui :open])
|
||||
open (get-in db [:ui :open])
|
||||
{:keys [at rate]} (:time (nest/inside clip st open
|
||||
(if (seq path) (pop path) [])
|
||||
(get-in db [:playback :frame])))]
|
||||
(editing-frame db clip)))]
|
||||
(cond-> {:db (apply-command db sid result retry)}
|
||||
(and (:clip result) (:frame result) rate)
|
||||
(assoc :dispatch [::playback/seek (+ at (/ (:frame result) rate))]))))
|
||||
;; Out of the made node's frames into the open symbol's, then out of those
|
||||
;; into the output frame the transport seeks in — the one place that second
|
||||
;; step is needed, because this is the one command that moves the playhead.
|
||||
(assoc :dispatch [::playback/seek
|
||||
(clip/first-output-frame clip open
|
||||
(+ at (/ (:frame result) rate)))]))))
|
||||
|
||||
(defn selection-frame
|
||||
"The playhead as a frame of the symbol that owns `selection`.
|
||||
|
|
@ -232,7 +261,7 @@
|
|||
selection (get-in db [:ui :selection])
|
||||
[_ sid] selection
|
||||
at (selection-frame clip st (get-in db [:ui :open]) selection
|
||||
(get-in db [:playback :frame]))
|
||||
(editing-frame db clip))
|
||||
result (if (integer? at)
|
||||
(span/append-drawing clip sid (random-uuid) (clip/fresh-id clip)
|
||||
{:at at :extent (or extent :keep)})
|
||||
|
|
@ -253,7 +282,7 @@
|
|||
{:clip clip :sid sid :id id
|
||||
:node (get-in clip [:symbols sid :nodes id])
|
||||
:at (when-let [owner-frame (selection-frame clip st (get-in db [:ui :open]) selection
|
||||
(get-in db [:playback :frame]))]
|
||||
(editing-frame db clip))]
|
||||
(span/host-frame clip sid id owner-frame))}))
|
||||
|
||||
(def ^:private no-frame
|
||||
|
|
@ -275,7 +304,7 @@
|
|||
selection (get-in db [:ui :selection])
|
||||
[_ sid] selection
|
||||
at (selection-frame clip st (get-in db [:ui :open]) selection
|
||||
(get-in db [:playback :frame]))
|
||||
(editing-frame db clip))
|
||||
result (if (integer? at)
|
||||
(span/overwrite-drawing clip sid (random-uuid) (clip/fresh-id clip) at
|
||||
{:extent (or extent :keep)
|
||||
|
|
@ -466,13 +495,15 @@
|
|||
(cond
|
||||
(not (integer? frame))
|
||||
{:refused "the playhead is not on one frame of this symbol"}
|
||||
exposed {:clip clip :path (conj (vec path) (:id exposed))}
|
||||
exposed {:clip clip :sid sid :path (conj (vec path) (:id exposed))}
|
||||
:else
|
||||
(let [cel-id (random-uuid)
|
||||
made (span/overwrite-drawing clip sid cel-id (clip/fresh-id clip) frame
|
||||
{:extent :grow-symbol
|
||||
:remainder-id (random-uuid)})]
|
||||
(if (:refused made) made {:clip (:clip made) :path (conj (vec path) cel-id)})))))
|
||||
(if (:refused made)
|
||||
made
|
||||
{:clip (:clip made) :sid sid :path (conj (vec path) cel-id)})))))
|
||||
|
||||
(defn polygon-landing
|
||||
"Choose the document and row path a finished polygon is drawn into.
|
||||
|
|
@ -483,10 +514,10 @@
|
|||
drawing the playhead is on. In an ordinary composition the polygon goes
|
||||
straight into the symbol, as it always did."
|
||||
[clip st db]
|
||||
(let [into (aimed-symbol clip st db (get-in db [:playback :frame]))]
|
||||
(let [into (aimed-symbol clip st db (editing-frame db clip))]
|
||||
(if (and (:sid into) (symbol/lane? (clip/symbol clip (:sid into))))
|
||||
(assoc (into-the-sequence clip db into) :lane? true)
|
||||
{:clip clip :path (:path into) :lane? false})))
|
||||
{:clip clip :sid (:sid into) :path (:path into) :lane? false})))
|
||||
|
||||
(defn beginning-polygon
|
||||
"Enter polygon mode, first materializing a drawing at the playhead when the
|
||||
|
|
@ -497,7 +528,6 @@
|
|||
cancels only the draft; the explicitly started drawing remains."
|
||||
[db]
|
||||
(let [{clip :clip st :store} (store/entry (:clip/current db))
|
||||
open (get-in db [:ui :open])
|
||||
landing (polygon-landing clip st db)
|
||||
source (:clip landing)
|
||||
made? (and source (not (identical? clip source)))
|
||||
|
|
@ -508,8 +538,14 @@
|
|||
made?
|
||||
(-> db
|
||||
(edit/transaction (constantly source))
|
||||
;; THE LANE'S SYMBOL, NOT THE OPEN ONE. The clip just made
|
||||
;; lives in the symbol the lane draws, which is only the open
|
||||
;; symbol when the lane IS what is open. Naming the wrong one
|
||||
;; left a selection that looked up to nothing, so the
|
||||
;; inspector, the breadcrumb and every span command went
|
||||
;; blank on a drawing that had just been created.
|
||||
(assoc-in [:ui :selection]
|
||||
[:node open
|
||||
[:node (:sid landing)
|
||||
(peek (:path landing)) (:path landing)]))
|
||||
|
||||
:else db)]
|
||||
|
|
@ -534,7 +570,7 @@
|
|||
down (:path landing)
|
||||
{:keys [sid frame pts]}
|
||||
(when-not (:refused landing)
|
||||
(nest/drawn-inside source st open down (get-in db [:playback :frame]) draft))]
|
||||
(nest/drawn-inside source st open down (editing-frame db source) draft))]
|
||||
(cond
|
||||
(< (count draft) 6) db
|
||||
(:refused landing) (update db :project merge {:status (:refused landing)})
|
||||
|
|
@ -550,13 +586,19 @@
|
|||
;; log ever has to reproduce a document exactly, this becomes a cofx.
|
||||
:else
|
||||
(let [id (keyword (str "paint-" (random-uuid)))]
|
||||
;; NOTHING IS OPENED HERE. Finishing a shape used to expand every row
|
||||
;; between the open symbol and the new shape, which inside a lane meant
|
||||
;; tearing the lane's one row open into its portal, that clip's
|
||||
;; channels and every shape already in the drawing — a dozen rows, in
|
||||
;; answer to a gesture that said nothing about the outline. Expansion
|
||||
;; is the twist triangle's business and nobody else's; the shape is
|
||||
;; selected and on the stage with handles on it, which is where you
|
||||
;; were looking.
|
||||
(-> db
|
||||
(edit/transaction
|
||||
(fn [_] (paint/new-shape source sid id frame pts (get-in db [:ui :tone]))))
|
||||
(update :ui merge {:tool nil :draft []
|
||||
:selection [:node sid id (conj down id)]})
|
||||
(update-in [:ui :expanded] (fnil into #{})
|
||||
(rest (reductions conj [] down)))))))))
|
||||
:selection [:node sid id (conj down id)]})))))))
|
||||
|
||||
(rf/reg-event-db
|
||||
::set-knob
|
||||
|
|
@ -581,7 +623,7 @@
|
|||
[db where lane?]
|
||||
(let [{document :clip st :store} (store/entry (:clip/current db))
|
||||
open (get-in db [:ui :open])
|
||||
frame (get-in db [:playback :frame])
|
||||
frame (editing-frame db document)
|
||||
into (if (= :top where)
|
||||
(assoc (nest/inside document st open [] frame) :path [])
|
||||
(aimed-symbol document st db frame))
|
||||
|
|
@ -657,6 +699,10 @@
|
|||
is created to receive the drop: the symbol IS the container, so the first drop
|
||||
into one is the same operation as the second.
|
||||
|
||||
`frame` is a frame of the OPEN symbol — the ruler's own unit, which is what the
|
||||
pointer answered with — and `:at` is that frame carried down into the
|
||||
destination's. Nothing on this path crosses the output grid.
|
||||
|
||||
`:clip` is handed back unchanged and is in the result only so the callers that
|
||||
used to be given a document with a freshly made lane in it go on reading one
|
||||
thing."
|
||||
|
|
@ -717,11 +763,14 @@
|
|||
uuid (random-uuid)]
|
||||
(if (:refused where)
|
||||
(-> db (update :ui dissoc :drop) (update :project merge {:status (:refused where)}))
|
||||
;; `:at` IS ALREADY THE DESTINATION SYMBOL'S OWN FRAME, which is what
|
||||
;; `place-sound` positions in. It used to be multiplied by the grid rate
|
||||
;; again on the way in — a second conversion of an already-converted
|
||||
;; number, which dropped a sound 2.5 frames late for every frame it was
|
||||
;; aimed at in a 12fps project.
|
||||
(let [sid (:sid where)
|
||||
seeded (clip/place-sound (:clip where) sid source label length rate
|
||||
(* (:at where)
|
||||
(:rate (clip/grid-time (:clip where) sid)))
|
||||
uuid)]
|
||||
(:at where) uuid)]
|
||||
(landed db where uuid
|
||||
(span/adopt seeded sid uuid (:at where)
|
||||
{:extent :grow-symbol :remainder-id (random-uuid)})))))))
|
||||
|
|
@ -757,7 +806,7 @@
|
|||
(fn [db [_ from to]]
|
||||
(let [{clip :clip st :store} (store/entry (:clip/current db))
|
||||
r (nest/move-node clip st (get-in db [:ui :open]) from to
|
||||
(get-in db [:playback :frame]))]
|
||||
(editing-frame db clip))]
|
||||
(if-let [why (:refused r)]
|
||||
(refused db why)
|
||||
(-> db
|
||||
|
|
@ -825,14 +874,20 @@
|
|||
(update db :ui dissoc :gesture)))))
|
||||
|
||||
(defn record-auto-frame
|
||||
"Buffer the active gesture at outer playback frame `f`, mapped to the node's
|
||||
local frame. Repeated pointer events replace that frame's sample cheaply."
|
||||
"Buffer the active gesture at output playback frame `f`, mapped to the node's
|
||||
local frame. Repeated pointer events replace that frame's sample cheaply.
|
||||
|
||||
`f` is the TRANSPORT's frame here, because this is called from the pointer
|
||||
stream while something plays, and the gesture names the symbol it started in —
|
||||
so the crossing into that symbol's own frames happens here, against that
|
||||
symbol, rather than in the caller against whatever happens to be open."
|
||||
[db f]
|
||||
(let [{:keys [auto-key? open path values] :as g}
|
||||
(get-in db [:ui :gesture])]
|
||||
(if (and auto-key? (seq values))
|
||||
(let [{document :clip st :store} (store/entry (:clip/current db))]
|
||||
(if-let [{:keys [sid id frame]} (nest/placement document st open path f)]
|
||||
(if-let [{:keys [sid id frame]} (nest/placement document st open path
|
||||
(clip/shown-frame document open f))]
|
||||
(assoc-in db [:ui :gesture]
|
||||
(-> g
|
||||
(assoc :sid sid :id id :frame frame)
|
||||
|
|
@ -877,7 +932,7 @@
|
|||
(fn [db [_ from to front?]]
|
||||
(let [{clip :clip st :store} (store/entry (:clip/current db))
|
||||
open (get-in db [:ui :open])
|
||||
f (get-in db [:playback :frame])
|
||||
f (editing-frame db clip)
|
||||
host (pop to)
|
||||
moved (if (= host (pop from))
|
||||
{:clip clip :id (peek from)}
|
||||
|
|
@ -897,7 +952,7 @@
|
|||
(fn [db [_ froms]]
|
||||
(let [{clip :clip st :store} (store/entry (:clip/current db))
|
||||
open (get-in db [:ui :open])
|
||||
f (get-in db [:playback :frame])
|
||||
f (editing-frame db clip)
|
||||
host (pop (first froms))
|
||||
uuid (random-uuid)
|
||||
r (nest/group clip st open froms (clip/fresh-id clip) uuid f)]
|
||||
|
|
|
|||
|
|
@ -80,8 +80,31 @@
|
|||
::frames
|
||||
:<- [::clip]
|
||||
:<- [::open]
|
||||
;; THE TRANSPORT'S LENGTH, in output frames: what the clock counts, what the
|
||||
;; readout reads and what export writes. The timeline's length is
|
||||
;; `::open-frames`, which is a different number whenever the open symbol was
|
||||
;; authored on a different grid — see `docs/one-grid-plan.md`.
|
||||
(fn [[document sid] _] (clip/output-frames document sid)))
|
||||
|
||||
(rf/reg-sub
|
||||
::open-frames
|
||||
:<- [::clip]
|
||||
:<- [::open]
|
||||
;; How many frames the open symbol HAS, in its own frames: the ruler's length,
|
||||
;; because the ruler is the symbol's own frame space and every span drawn
|
||||
;; against it is in that space.
|
||||
(fn [[document sid] _] (or (clip/frames document sid) 0)))
|
||||
|
||||
(rf/reg-sub
|
||||
::open-frame
|
||||
:<- [::clip]
|
||||
:<- [::open]
|
||||
:<- [::playback/frame]
|
||||
;; Where the playhead is IN THE OPEN SYMBOL — the one crossing from the output
|
||||
;; grid for everything that draws or edits in the timeline, and an integer
|
||||
;; frame of the symbol rather than a fraction between two of them.
|
||||
(fn [[document sid f] _] (clip/shown-frame document sid f)))
|
||||
|
||||
(rf/reg-sub
|
||||
::exposure
|
||||
:<- [::symbol]
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
:<- [::selection]
|
||||
:<- [::render/clip-id]
|
||||
:<- [::render/open]
|
||||
:<- [::playback/frame]
|
||||
:<- [::render/open-frame]
|
||||
(fn [[[_ id n] [_ _ _ path] clip-id open f] _]
|
||||
;; `nest/inside` the selected node, from the open symbol: its own frame, the
|
||||
;; matrix from its coordinates to the stage's, and the time map from the open
|
||||
|
|
@ -72,7 +72,7 @@
|
|||
:<- [::render/clip]
|
||||
:<- [::render/clip-id]
|
||||
:<- [::render/open]
|
||||
:<- [::playback/frame]
|
||||
:<- [::render/open-frame]
|
||||
(fn [[{:keys [path]} [_ id n] clip clip-id open f] _]
|
||||
;; `nest/placement` of the TARGET, with the bounds of what it draws — the
|
||||
;; same work `::selected-placement` does, kept separate because the two are
|
||||
|
|
@ -90,7 +90,7 @@
|
|||
:<- [::render/clip]
|
||||
:<- [::render/clip-id]
|
||||
:<- [::render/open]
|
||||
:<- [::playback/frame]
|
||||
:<- [::render/open-frame]
|
||||
(fn [[[_ id n] [_ _ _ path] clip clip-id open f] _]
|
||||
;; `nest/placement` of the selected node, and `:bounds` around what it draws
|
||||
;; in its own coordinates, for the stage's handles. From the clip as it is
|
||||
|
|
|
|||
|
|
@ -16,11 +16,13 @@
|
|||
step's worth of tree-shaking, and a license file, for forty lines.
|
||||
|
||||
Icons are used ONLY where the word is worse than the picture: the transport,
|
||||
where `|<` was ASCII pretending to be a glyph, and the two playback toggles,
|
||||
which are state rather than actions. Everything else in the strip keeps its
|
||||
word, because `insert` and `overwrite` have no pictures and inventing some
|
||||
would be a puzzle rather than a shorthand. Every one carries an `aria-label`
|
||||
at the call site; nothing here is the only statement of what a control does."
|
||||
where `|<` was ASCII pretending to be a glyph, the two playback toggles, which
|
||||
are state rather than actions, and a lane's row in the timeline, where the
|
||||
picture is read while scanning a column of twenty names and the word would
|
||||
not be. Everything else in the strip keeps its word, because `insert` and
|
||||
`overwrite` have no pictures and inventing some would be a puzzle rather than
|
||||
a shorthand. Every one carries an `aria-label` or a text label beside it at
|
||||
the call site; nothing here is the only statement of what a control does."
|
||||
(:require [clojure.string :as str]))
|
||||
|
||||
;; A glyph is its paths on a 12x12 grid. Solid shapes are filled; the two
|
||||
|
|
@ -40,7 +42,12 @@
|
|||
:sound {:fill ["M2.4 4.6h1.8L6.4 2.6v6.8L4.2 7.4H2.4z"]
|
||||
:stroke ["M8 4.1a2.7 2.7 0 0 1 0 3.8"]}
|
||||
:muted {:fill ["M2.4 4.6h1.8L6.4 2.6v6.8L4.2 7.4H2.4z"]
|
||||
:stroke ["M7.9 4.4 10.7 7.6" "M10.7 4.4 7.9 7.6"]}})
|
||||
:stroke ["M7.9 4.4 10.7 7.6" "M10.7 4.4 7.9 7.6"]}
|
||||
;; A strip divided into cels: what a lane IS, drawn rather than named, so one
|
||||
;; row out of a dozen says at a glance that its blocks follow one another in
|
||||
;; time instead of being on screen together. Whole and half coordinates, so
|
||||
;; the three dividers land on pixels at 11px.
|
||||
:lane {:stroke ["M1.5 3.5h9v5h-9z" "M4.5 3.5v5" "M7.5 3.5v5"]}})
|
||||
|
||||
(defn view
|
||||
"The glyph named `k`, sized by CSS and inked in `currentColor` — so a button's
|
||||
|
|
|
|||
|
|
@ -212,7 +212,7 @@
|
|||
;; A span is in the node's OWN frames and `at` is where its frame 0 sits
|
||||
;; in this symbol. See `node/placed-span`.
|
||||
"span" (when start (str start " … " end))
|
||||
"at" (when (= :map (get-in n [:time :mode])) (str (get-in n [:time :at] 0)))]
|
||||
"at" (when (node/mapped-time? n) (str (get-in n [:time :at] 0)))]
|
||||
(when (:paint? n) [drawing-keys sid id n @(rf/subscribe [::sub/selected-local])])
|
||||
[:div.row {:style {:margin-top "6px"}} [:span.dim "channels"]]
|
||||
(let [{:keys [frame]} @(rf/subscribe [::sub/selected-local])]
|
||||
|
|
|
|||
|
|
@ -270,7 +270,10 @@
|
|||
[_ _ _ selected] @(rf/subscribe [::sub/selection])
|
||||
clip-id @(rf/subscribe [::render/clip-id])
|
||||
ctx {:clip-id clip-id
|
||||
:open @(rf/subscribe [::render/open]) :f @(rf/subscribe [::playback/frame])
|
||||
;; The open symbol's OWN frame, which is what `nest/placement`
|
||||
;; walks in: picking and dragging on the stage are edits, and an
|
||||
;; edit's coordinate is the document's.
|
||||
:open @(rf/subscribe [::render/open]) :f @(rf/subscribe [::render/open-frame])
|
||||
:w w :h h}
|
||||
points? @(rf/subscribe [::sub/points])
|
||||
[sid id geom active editable? frame matrix] (when points? (editing))
|
||||
|
|
@ -352,7 +355,9 @@
|
|||
;; reallocates the backing store — so this re-rendering costs nothing per frame.
|
||||
(let [w @(rf/subscribe [::playback/width])
|
||||
h @(rf/subscribe [::playback/height])
|
||||
frame @(rf/subscribe [::playback/frame])
|
||||
;; A drop lands at the playhead IN THE OPEN SYMBOL's frames, the same
|
||||
;; unit a drop on the timeline's ruler lands in.
|
||||
frame @(rf/subscribe [::render/open-frame])
|
||||
zoom @(rf/subscribe [::layout/zoom :stage])]
|
||||
[:div.stage-area
|
||||
[:div.stage-wrap
|
||||
|
|
|
|||
|
|
@ -314,7 +314,11 @@
|
|||
sound-lane? (mapv #(assoc % :sound? true))))))
|
||||
ordered))))]
|
||||
(if (get-in clip [:symbols sid])
|
||||
(walk sid [] 0 #(/ % (:rate (clip/grid-time clip sid))))
|
||||
;; IDENTITY AT THE TOP. The ruler is this symbol's own frames, so a row of
|
||||
;; its own children needs no mapping at all; `->open` only earns its keep
|
||||
;; further down, where a row reached through an instance is in some other
|
||||
;; symbol's frames.
|
||||
(walk sid [] 0 identity)
|
||||
[])))))
|
||||
|
||||
(defn sound-rows
|
||||
|
|
@ -328,12 +332,20 @@
|
|||
inside the symbols this one places, mapped into this ruler."
|
||||
[clip sid expanded]
|
||||
(if-not (get-in clip [:symbols sid]) []
|
||||
(let [lane? (symbol/lane? (clip/symbol clip sid))]
|
||||
(let [lane? (symbol/lane? (clip/symbol clip sid))
|
||||
;; THE MIXER'S INTERVALS ARE IN OUTPUT FRAMES and this ruler is in the
|
||||
;; symbol's own, so these rows -- and only these -- convert.
|
||||
;; `nest/audio-tracks` is the mixer's own flattening and audio is
|
||||
;; measured in seconds, which is why it keeps the transport's grid. A
|
||||
;; sound's bar may land between two marks, and that is honest: a sound
|
||||
;; does not have to start on a frame boundary.
|
||||
own (let [rate (:rate (clip/grid-time clip sid))] #(some-> % (* rate)))
|
||||
own-span (fn [sp] (when-let [[a b] sp] [(own a) (own b)]))]
|
||||
(vec
|
||||
(mapcat
|
||||
(fn [[path tracks]]
|
||||
(let [n (first tracks)
|
||||
span (node/placed-span n)
|
||||
span (own-span (node/placed-span n))
|
||||
select [:node (:owner n) (:id n) path]
|
||||
open? (contains? expanded path)
|
||||
via (when (< 1 (count path)) (str (first path)))
|
||||
|
|
@ -341,14 +353,15 @@
|
|||
:kind :node :node-kind :audio :via via
|
||||
:slides (if via (subvec path 0 1) path)
|
||||
:select select :expandable? true :expanded? open? :span span
|
||||
:keys (vec (distinct (mapcat keyed-frames (vals (:channels n)))))}]
|
||||
:keys (mapv own (distinct (mapcat keyed-frames (vals (:channels n)))))}]
|
||||
(cons (cond-> row
|
||||
(< 1 (count tracks))
|
||||
(assoc :cels (mapv (fn [i track]
|
||||
{:id i :label (node-label (:id track) track)
|
||||
:span (node/placed-span track) :select select})
|
||||
:span (own-span (node/placed-span track))
|
||||
:select select})
|
||||
(range) tracks)))
|
||||
(when open? (channel-rows n path 1 identity span)))))
|
||||
(when open? (channel-rows n path 1 own span)))))
|
||||
(sort-by (comp str key)
|
||||
(group-by :path
|
||||
(remove #(and lane? (= 1 (count (:path %))))
|
||||
|
|
@ -363,17 +376,28 @@
|
|||
(defn- at% [f frames] (str (* 100 (/ (+ f 0.5) (max 1 frames))) "%"))
|
||||
(defn- edge% [f frames] (str (* 100 (/ f (max 1 frames))) "%"))
|
||||
|
||||
(defn- frame-at
|
||||
"Which frame the pointer is over."
|
||||
[^js event frames]
|
||||
(let [box (.getBoundingClientRect (.-currentTarget event))
|
||||
(defn- frame-under
|
||||
"Which frame the pointer is over, measured in `element`'s box.
|
||||
|
||||
THE ONE ANSWER EVERY GESTURE IN THIS PANE IS BUILT FROM, and the only place a
|
||||
pixel becomes a frame. A drop lands on it, a scrub seeks to it, and a drag's
|
||||
distance is the difference between two of its answers -- so the preview and the
|
||||
commit cannot disagree, because they are the same subtraction. The alternative,
|
||||
which this replaces, was a pixel delta scaled and rounded on its own: it
|
||||
answered 1 where the pointer had crossed two marks and the ghost drew one
|
||||
number while the command wrote another.
|
||||
|
||||
Clamped into the ruler: a frame off either end is not a frame, and a gesture
|
||||
dragged past the end and back again should come back to the frame it is over."
|
||||
[^js event frames ^js element]
|
||||
(let [box (.getBoundingClientRect element)
|
||||
x (- (.-clientX event) (.-left box))]
|
||||
(-> (/ (* x frames) (.-width box)) js/Math.floor (max 0) (min (dec frames)))))
|
||||
|
||||
(defn- frame-at-element [^js event frames ^js element]
|
||||
(let [box (.getBoundingClientRect element)
|
||||
x (- (.-clientX event) (.-left box))]
|
||||
(-> (/ (* x frames) (.-width box)) js/Math.floor (max 0) (min (dec frames)))))
|
||||
(defn- frame-at
|
||||
"`frame-under`, in the box of the element whose handler is asking."
|
||||
[^js event frames]
|
||||
(frame-under event frames (.-currentTarget event)))
|
||||
|
||||
(defn- lane-under
|
||||
"The lane track geometrically under a captured pointer, and its selection."
|
||||
|
|
@ -423,8 +447,11 @@
|
|||
;; children, and nothing in a span says.
|
||||
span? (and n (not= :group (:kind n)) (some? (node/placed-span n)))
|
||||
;; Every one of them acts at the playhead, so every one is offered only
|
||||
;; where the playhead is somewhere it means something.
|
||||
owner-frame (ui/selection-frame clip st open selection frame)
|
||||
;; where the playhead is somewhere it means something -- and in the open
|
||||
;; symbol's own frames, which is what the commands take. `frame` and
|
||||
;; `frames` above are the transport's own numbers and are only read out.
|
||||
owner-frame (ui/selection-frame clip st open selection
|
||||
(clip/shown-frame clip open frame))
|
||||
;; TWO COORDINATES AND THEY ARE NOT THE SAME. `host` is the frame of
|
||||
;; whatever the selection is positioned in — its lane, for a cel; the
|
||||
;; symbol, for a free placement — and is what the span commands take.
|
||||
|
|
@ -551,6 +578,7 @@
|
|||
[over-path where] @over]
|
||||
[:div (cond-> {:class (str "tl-label" (when (and select (= select selection)) " on")
|
||||
(when aimed? " aimed")
|
||||
(when lane? " lane")
|
||||
(when (= :ghost kind) " ghost")
|
||||
(when (= path over-path)
|
||||
(case where
|
||||
|
|
@ -615,6 +643,12 @@
|
|||
(.stopPropagation e)
|
||||
(rf/dispatch [::ui/toggle-row path]))}
|
||||
(when expandable? (if expanded? "▾" "▸"))]
|
||||
;; A LANE IS NOT AN INSTANCE WEARING A DIFFERENT HAT, to read. Its one row
|
||||
;; holds blocks that follow one another in time, where every other row
|
||||
;; holds a bar that is simply on screen — so it gets the strip glyph and
|
||||
;; its own ground, and the kind tag below says "lane" rather than naming
|
||||
;; the placement mechanism that happens to carry it.
|
||||
(when lane? [:span.tl-ico {:aria-hidden true} [icon/view :lane]])
|
||||
(if editing?
|
||||
[:input.tl-name-input
|
||||
{:value @draft :auto-focus true :aria-label "lane name"
|
||||
|
|
@ -628,7 +662,9 @@
|
|||
"Escape" (do (.preventDefault e) (reset! renaming nil))
|
||||
nil))}]
|
||||
[:span.name label])
|
||||
(when node? [:span.kind (if via (str "· in " via) (str "·" (name node-kind)))])
|
||||
(cond
|
||||
lane? [:span.kind "·lane"]
|
||||
node? [:span.kind (if via (str "· in " via) (str "·" (name node-kind)))])
|
||||
(when lane?
|
||||
[:button.tl-rename
|
||||
{:title "rename lane (F2)"
|
||||
|
|
@ -660,16 +696,22 @@
|
|||
"×"])]))
|
||||
|
||||
(defn- track-cell
|
||||
"`sliding` is the pointer's side of a bar being dragged, `{:path :x :width
|
||||
:df}`. What it looks like mid-drag is `[:ui :sliding]`, which the clip every
|
||||
row and the stage are drawn from already has in it."
|
||||
"`sliding` is the gesture in flight: `{:row :path :kind :from :df}`, where
|
||||
`:from` is the frame the press was on and `:df` how many frames the pointer has
|
||||
moved since. What it looks like mid-drag is `[:ui :sliding]`, which the clip
|
||||
every row and the stage are drawn from already has in it."
|
||||
[{:keys [path span keys dense? kind node-kind select slides cels lane? of unmapped?]}
|
||||
frames sliding hint {:keys [clip store open frame]}]
|
||||
frames sliding hint {:keys [clip store open frame selection]}]
|
||||
(let [active-row (:row @sliding)
|
||||
slide (fn [^js e]
|
||||
(let [{from :row x0 :x width :width} @sliding]
|
||||
(let [{from :row pressed :from} @sliding]
|
||||
(when (= path from)
|
||||
(let [df (js/Math.round (/ (* frames (- (.-clientX e) x0)) (max 1 width)))
|
||||
;; HOW MANY FRAMES THE POINTER HAS MOVED, as the difference
|
||||
;; of two frames under it -- not a pixel distance scaled and
|
||||
;; rounded by itself. The ghost below and the command on
|
||||
;; release are handed this same number, so what the drag
|
||||
;; shows is what the drag does.
|
||||
(let [df (- (frame-under e frames (.-currentTarget e)) pressed)
|
||||
drag (:drag @sliding)
|
||||
;; TWO INTENTIONS, SAID BY A MODIFIER. An ordinary
|
||||
;; body drag moves a clip in TIME, within its lane
|
||||
|
|
@ -694,7 +736,7 @@
|
|||
[target-el target] (when (and drag (not shift?)) (lane-under e))
|
||||
landing? (some? target)
|
||||
target-frame (when landing?
|
||||
(max 0 (- (frame-at-element e frames target-el)
|
||||
(max 0 (- (frame-under e frames target-el)
|
||||
(:grab drag))))]
|
||||
(when (and drag hint)
|
||||
(reset! hint
|
||||
|
|
@ -766,17 +808,22 @@
|
|||
:on-click actual-select
|
||||
:drag (when drag
|
||||
(assoc drag
|
||||
:grab (- (frame-at-element e frames track)
|
||||
:grab (- (frame-under e frames track)
|
||||
(:in drag))))
|
||||
:ripple? (and (= :out gesture-kind) (.-shiftKey e))
|
||||
:x (.-clientX e) :df 0
|
||||
:width (.-width (.getBoundingClientRect track))})
|
||||
;; The frame pressed, which every later
|
||||
;; answer is measured from. No pixel origin
|
||||
;; and no track width: the box is measured
|
||||
;; when it is asked, so a pane resized or
|
||||
;; zoomed mid-drag cannot skew the gesture.
|
||||
:from (frame-under e frames track) :df 0})
|
||||
(try (.setPointerCapture track (.-pointerId e))
|
||||
(catch :default _ nil))))]
|
||||
[:div.tl-track
|
||||
;; The track, not the bar, holds the pointer while a bar slides, so the drag
|
||||
;; goes on when the bar has slid off the ruler and is no longer drawn.
|
||||
{:on-pointer-move slide
|
||||
{:class (when lane? "lane")
|
||||
:on-pointer-move slide
|
||||
:on-pointer-up (fn [e] (slide e) (done true))
|
||||
:on-pointer-cancel (fn [_] (done false))
|
||||
;; THE CLICK ARRIVES HERE, not on the block it started on, because the
|
||||
|
|
@ -824,6 +871,7 @@
|
|||
(when (= :audio node-kind) " sound")
|
||||
(when unmapped? " unmapped")
|
||||
(when (and select (not unmapped?)) " movable")
|
||||
(when (and select (= select selection)) " on")
|
||||
(when (= path active-row) " sliding"))
|
||||
:title (when unmapped?
|
||||
"inside a held clip · its own frames have no place on this ruler")
|
||||
|
|
@ -856,7 +904,13 @@
|
|||
[:button.tl-cel
|
||||
{:title (str label " · select clip; double-click to edit its symbol"
|
||||
" · shift-drag another clip onto it to nest that clip inside")
|
||||
;; WHICH CLIP IS SELECTED IS THE QUESTION THIS ROW ANSWERS. An
|
||||
;; expanded lane opens exactly the selected clip, so a block that
|
||||
;; cannot say whether it is the one leaves the portal below it
|
||||
;; unexplained. It wears the pick colour rather than the accent every
|
||||
;; span and drop already uses — see `--pick` in app.css.
|
||||
:class (str (when ghost? "ghost")
|
||||
(when (and select (= select selection)) " on")
|
||||
(when (and select (= select (get-in @sliding [:nest :select])))
|
||||
" nest-target"))
|
||||
:style {:position "absolute" :left (edge% in frames)
|
||||
|
|
@ -870,25 +924,39 @@
|
|||
(when el (aset el "arthurCel" {:select select :source source
|
||||
:label label
|
||||
:in (js/Math.floor in)}))))
|
||||
;; `:in` IS A RULER FRAME AND THEREFORE WHOLE, as the ref above
|
||||
;; already says. `:grab` is the pointer's offset into the block,
|
||||
;; `press − in`, and `target-frame` is `move − grab`, so a fractional
|
||||
;; `in` would make every landing frame fractional. A clip of this
|
||||
;; symbol now starts on a whole frame of this ruler by construction --
|
||||
;; they are the same frame space -- and the floor is what still holds
|
||||
;; for a row reached through a retimed instance, where the clip really
|
||||
;; does begin between two of the ruler's frames.
|
||||
:on-pointer-down (when select
|
||||
#(begin! % (nth select 3) :slide select nil
|
||||
{:selection select :label label :in in
|
||||
{:selection select :label label
|
||||
:in (js/Math.floor in)
|
||||
:duration (- out in)}))}
|
||||
[:span.tl-cel-label label]
|
||||
(when select
|
||||
(if joined?
|
||||
;; ONE HANDLE, ONE MEANING: the cut moves. The end of the clip on the
|
||||
;; left and the start of the one on the right are the same frame, so
|
||||
;; a gesture on that frame alters the extent of one and then the
|
||||
;; other -- `span/roll`, which is literally `resize-out` then
|
||||
;; `resize-in`.
|
||||
;;
|
||||
;; It used to pick between three things by where in the handle the
|
||||
;; press landed: trim-left, roll, trim-right, in thirds of twelve
|
||||
;; pixels. Four pixels is not an aim, so a gesture that meant to roll
|
||||
;; trimmed one side instead and the handle looked like it chose for
|
||||
;; itself. Trimming one side alone leaves a GAP, which is a different
|
||||
;; intention and has its own handle on an edge that is not shared.
|
||||
[:span.tl-junction
|
||||
{:title "Left: trim left · center: roll cut · right: trim right"
|
||||
{:title "Drag the cut · the clip before it ends here and the one after it starts here"
|
||||
:on-pointer-down
|
||||
(fn [^js e]
|
||||
(let [box (.getBoundingClientRect (.-currentTarget e))
|
||||
x (/ (- (.-clientX e) (.-left box)) (max 1 (.-width box)))
|
||||
left-path (nth (:select prev) 3)
|
||||
right-path (nth select 3)]
|
||||
(cond
|
||||
(< x 0.34) (begin! e left-path :out (:select prev) nil nil)
|
||||
(> x 0.66) (begin! e right-path :in select nil nil)
|
||||
:else (begin! e right-path :roll select left-path nil))))}]
|
||||
(begin! e (nth select 3) :roll select (nth (:select prev) 3) nil))}]
|
||||
[:span.tl-edge.in {:title "Drag start"
|
||||
:on-pointer-down #(begin! % (nth select 3) :in select nil nil)}]))
|
||||
(when select
|
||||
|
|
@ -927,8 +995,13 @@
|
|||
renaming (r/atom nil)
|
||||
draft (r/atom "")]
|
||||
(let [clip @(rf/subscribe [::render/clip])
|
||||
frames (max 1 (or @(rf/subscribe [::render/frames]) 1))
|
||||
frame @(rf/subscribe [::playback/frame])
|
||||
;; THE RULER IS THE OPEN SYMBOL'S OWN FRAME SPACE. Every span, key and
|
||||
;; cut drawn here is stored in that space, so drawing them against the
|
||||
;; output grid put almost none of them on a mark and left every gesture
|
||||
;; converting and rounding — see `docs/one-grid-plan.md`. The transport
|
||||
;; keeps the output numbers; this pane does not use them.
|
||||
frames (max 1 (or @(rf/subscribe [::render/open-frames]) 1))
|
||||
frame @(rf/subscribe [::render/open-frame])
|
||||
store @(rf/subscribe [::render/store])
|
||||
selection @(rf/subscribe [::sub/selection])
|
||||
target @(rf/subscribe [::sub/target])
|
||||
|
|
@ -941,18 +1014,26 @@
|
|||
;; every placement of a face, because the switch is the face's.
|
||||
tracing {:faces (set (trace/traceable-faces clip open))
|
||||
:on (set (:faces @(rf/subscribe [::render/tracing])))}
|
||||
;; Where a drag out of the pool would land, as a row of its own at the
|
||||
;; top of its section: its own length, starting on the frame it would
|
||||
;; start on. The stage's drop shows it too, at the playhead.
|
||||
;; WHERE THE DROP WILL LAND IS WHERE THE PREVIEW GOES, and what is being
|
||||
;; carried does not come into it. A row under the pointer takes the
|
||||
;; preview as a block in that row -- the same row `ui/drop-destination`
|
||||
;; will resolve on release -- and with no row under the pointer it is a
|
||||
;; row of its own at the top of the section it will appear in.
|
||||
;;
|
||||
;; A SOUND USED TO BE THE EXCEPTION: aimed at a lane it drew no block
|
||||
;; there and a ghost row down in the audio section instead, so dragging
|
||||
;; a sound onto a lane looked exactly like a lane refusing it -- while
|
||||
;; the drop itself worked and put the sound in the lane. One rule
|
||||
;; resolves the drop, so one rule previews it.
|
||||
drop-lane (:target drop)
|
||||
drop-span (when drop [(:frame drop) (+ (:frame drop) (or (:frames drop) 1))])
|
||||
ghost (when (and drop (nil? drop-lane))
|
||||
{:path [::drop] :depth 0 :kind :ghost
|
||||
:label (str "+ " (:label drop))
|
||||
:span [(:frame drop) (+ (:frame drop) (or (:frames drop) 1))]
|
||||
:keys []})
|
||||
lane-ghost (when (and drop drop-lane (not (:sound? drop)))
|
||||
:span drop-span :keys []})
|
||||
lane-ghost (when (and drop drop-lane)
|
||||
{:id ::drop :label (str "+ " (:label drop)) :ghost? true
|
||||
:span [(:frame drop) (+ (:frame drop) (or (:frames drop) 1))]})
|
||||
:span drop-span})
|
||||
chosen (when (= :node (first selection)) (nth selection 3))
|
||||
picture (cond->> (cond->> (rows clip open expanded chosen)
|
||||
lane-ghost
|
||||
|
|
@ -971,7 +1052,14 @@
|
|||
(seq sounds) (-> (conj {:path [::sounds] :kind :section :label "audio"})
|
||||
(into sounds)))
|
||||
;; Roughly ten labels, on a round number of frames.
|
||||
step (* 10 (js/Math.ceil (/ frames 100)))]
|
||||
step (* 10 (js/Math.ceil (/ frames 100)))
|
||||
;; THE OTHER DIRECTION, AND THE ONLY PLACE THIS PANE GOES THERE. The
|
||||
;; ruler is in the open symbol's frames and the transport counts output
|
||||
;; frames, so scrubbing names a mark and seeks to the output frame that
|
||||
;; first shows it. Several marks can share one output frame when the
|
||||
;; symbol was authored on a finer grid; that is what playing it at this
|
||||
;; project's rate means, and the playhead lands where it will really be.
|
||||
seek-to (fn [^js e n] (clip/first-output-frame clip open (frame-at e n)))]
|
||||
[:section.pane.time
|
||||
[transport]
|
||||
[:div.tl-body
|
||||
|
|
@ -1023,7 +1111,7 @@
|
|||
:style {"--tick" (str (* 100 (/ 5 frames)) "%")}}
|
||||
[:div.tl-ruler
|
||||
{:on-pointer-down (fn [^js e]
|
||||
(rf/dispatch [::pb/seek (frame-at e frames)])
|
||||
(rf/dispatch [::pb/seek (seek-to e frames)])
|
||||
(reset! scrubbing true)
|
||||
;; Capture is what keeps a drag scrubbing once it
|
||||
;; leaves the ruler, and it is an ENHANCEMENT: it
|
||||
|
|
@ -1039,7 +1127,7 @@
|
|||
(catch :default _ nil)))
|
||||
:on-pointer-move (fn [^js e]
|
||||
(when @scrubbing
|
||||
(rf/dispatch [::pb/seek (frame-at e frames)])))
|
||||
(rf/dispatch [::pb/seek (seek-to e frames)])))
|
||||
:on-pointer-up (fn [_] (reset! scrubbing false))
|
||||
:on-pointer-cancel (fn [_] (reset! scrubbing false))}
|
||||
(doall
|
||||
|
|
@ -1051,7 +1139,8 @@
|
|||
(with-meta (if (= :section (:kind row))
|
||||
[:div.tl-track.tl-section]
|
||||
[track-cell row frames sliding hint
|
||||
{:clip clip :store store :open open :frame frame}])
|
||||
{:clip clip :store store :open open :frame frame
|
||||
:selection selection}])
|
||||
{:key (str (:path row))})))
|
||||
[:div.tl-empty "nothing in this symbol"])
|
||||
[:div.tl-playhead {:style {:left (at% frame frames)}}]]]
|
||||
|
|
|
|||
|
|
@ -36,8 +36,28 @@
|
|||
(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 #{})))))))
|
||||
;; `nest/inside` and the timeline's rows are in the SYMBOL's own frames --
|
||||
;; see `docs/one-grid-plan.md`. Frame 7 of :main is frame 7 of :main; the
|
||||
;; output frame that shows it is 3, and `shown-frame` is the one function
|
||||
;; that crosses between the two.
|
||||
(is (= 7 (:frame (nest/inside doc store :main [:mark] 7))))
|
||||
(is (= 7 (clip/shown-frame doc :main 3)))
|
||||
(is (= 3 (clip/first-output-frame doc :main 7)))
|
||||
(is (= [0 60] (:span (first (timeline/rows doc :main #{})))))))
|
||||
|
||||
(deftest crossing-to-the-output-grid-and-back-lands-on-the-frame-it-names
|
||||
;; `first-output-frame` is the inverse of `shown-frame` as far as a floor has
|
||||
;; one: seeking to the output frame it names puts the playhead on a frame at or
|
||||
;; after the mark, never before it, and exactly on it whenever an output frame
|
||||
;; shows it at all.
|
||||
(doseq [project [8 12 24 30 60] native [12 24 30 60]]
|
||||
(let [doc (-> footage (assoc :fps project) (assoc-in [:symbols :main :fps] native))]
|
||||
(doseq [n (range 40)]
|
||||
(let [f (clip/first-output-frame doc :main n)]
|
||||
(is (integer? f))
|
||||
(is (>= (clip/shown-frame doc :main f) n)
|
||||
(str n " at " project "/" native))
|
||||
(is (or (zero? f) (< (clip/shown-frame doc :main (dec f)) n))))))))
|
||||
|
||||
(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)
|
||||
|
|
@ -69,6 +89,27 @@
|
|||
[track] (nest/audio-tracks slow :main)]
|
||||
(is (= 0.5 (* (get-in track [:time :rate]) (/ (:fps slow) (:fps track)))))))))
|
||||
|
||||
(deftest generated-faces-own-their-footage-sound
|
||||
(let [face (fn [id] {:id id :fps 30 :frames 20 :nodes {}})
|
||||
frozen {:fps 30 :subjects {:face-1 {} :face-2 {}}
|
||||
:symbols {:main {:id :main :fps 30 :frames 20
|
||||
:nodes {:face-1 {:id :face-1 :kind :instance :z "a1"
|
||||
:source {:symbol :face-1}}
|
||||
:face-2 {:id :face-2 :kind :instance :z "a2"
|
||||
:source {:symbol :face-2}}}}
|
||||
:face-1 (face :face-1)
|
||||
:face-2 (face :face-2)}}
|
||||
{doc :clip take :sid} (bring/take (clip/blank) frozen "take" "video" [3 13])
|
||||
alone (clip/place-symbol doc nil :main :face-1 4 :placed-face nil)]
|
||||
(is (nil? (get-in doc [:symbols take :nodes :sound]))
|
||||
"the wrapper does not own a sound the face would lose")
|
||||
(is (= {:footage "video"}
|
||||
(get-in doc [:symbols :face-1 :nodes :sound :source])))
|
||||
(is (= 1 (count (nest/audio-tracks doc take)))
|
||||
"the same take recording is not mixed once per detected face")
|
||||
(is (= 1 (count (nest/audio-tracks alone :main)))
|
||||
"placing a generated face by itself brings its associated sound")))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; the preserve-snap
|
||||
;;
|
||||
|
|
|
|||
|
|
@ -281,3 +281,78 @@
|
|||
(get-in [:symbols :outer :nodes u]))]
|
||||
(is (empty? (node/problems v)))
|
||||
(is (= [0 36] (node/placed-span v)) "90 frames at 30 are 36 at 12")))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; the ruler IS the symbol's frames
|
||||
|
||||
(defn- on-twos
|
||||
"A 12fps project holding a 30fps lane, which is what changing project fps to
|
||||
12 leaves behind: `docs/time.md` records each symbol's own rate so its timing
|
||||
stays put. Two one-frame held clips in it, at 5 and at 19."
|
||||
[]
|
||||
(let [held (fn [id at] {:id id :kind :instance :z (str "a-" id) :span [0 1]
|
||||
;; As `span/held` writes it: no `:mode`, because
|
||||
;; `node/time-of` reads an absent one as `:map`.
|
||||
:time {:at at :rate 1}
|
||||
:source {:symbol :draw} :playback {:in 0 :speed 0 :end :stop}})]
|
||||
(-> (clip/blank)
|
||||
(assoc :fps 12)
|
||||
(assoc-in [:symbols :draw] {:id :draw :fps 30 :frames 1 :nodes {}})
|
||||
(assoc-in [:symbols :lane] {:id :lane :fps 30 :frames 60 :display :lane
|
||||
:nodes {:a (held :a 5) :b (held :b 19)}}))))
|
||||
|
||||
(deftest a-drag-in-the-open-symbol-is-frame-for-frame
|
||||
;; `df` counts the OPEN SYMBOL's frames, because that is what the ruler is
|
||||
;; drawn in -- see `docs/one-grid-plan.md`. So one frame of the gesture is one
|
||||
;; frame of the document, the project's output rate is not in the arithmetic at
|
||||
;; all, and nothing can land between two frames.
|
||||
(doseq [project [8 12 24 30 60]]
|
||||
(let [c (assoc (on-twos) :fps project)
|
||||
at #(get-in % [:symbols :lane :nodes :b :time :at])
|
||||
span #(node/placed-span (get-in % [:symbols :lane :nodes :b]))]
|
||||
(is (= 20 (at (:clip (nest/slide c :lane [:b] 1)))) (str "at " project "fps"))
|
||||
(is (= 18 (at (:clip (nest/slide c :lane [:b] -1)))))
|
||||
(is (= 24 (at (:clip (nest/slide c :lane [:b] 5)))))
|
||||
(is (= [20 21] (span (:clip (nest/slide c :lane [:b] 1)))))
|
||||
(testing "an edge drag too"
|
||||
(let [grown (nest/resize-out c :lane [:b] 1 false)]
|
||||
(is (nil? (:refused grown)) (:refused grown))
|
||||
(is (= [19 21] (node/placed-span (get-in (:clip grown) [:symbols :lane :nodes :b]))))
|
||||
(is (= [18 20] (node/placed-span
|
||||
(get-in (:clip (nest/resize-in c :lane [:b] -1))
|
||||
[:symbols :lane :nodes :b])))))))))
|
||||
|
||||
(deftest a-drag-through-a-retimed-instance-still-lands-on-a-whole-frame
|
||||
;; The one case where a ruler frame is not a frame of the space being edited:
|
||||
;; a row reached THROUGH an instance somebody deliberately retimed. At double
|
||||
;; speed one frame of the ruler is two inside, and at half speed it is half of
|
||||
;; one -- which has to round, because a frame number cannot fall between two
|
||||
;; frames. That rounding is `nest/dragged` and it is the whole of its job.
|
||||
(let [inner (fn [rate]
|
||||
(-> (on-twos)
|
||||
(assoc-in [:symbols :outer]
|
||||
{:id :outer :fps 30 :frames 60
|
||||
:nodes {:ins {:id :ins :kind :instance :z "a" :span [0 60]
|
||||
:time {:mode :map :at 0 :rate rate}
|
||||
:source {:symbol :lane}
|
||||
:playback {:in 0 :speed 1 :end :stop}}}})))
|
||||
at (fn [rate df]
|
||||
(get-in (:clip (nest/slide (inner rate) :outer [:ins :b] df))
|
||||
[:symbols :lane :nodes :b :time :at]))]
|
||||
(is (= 21 (at 2 1)) "double speed inside: one frame of the ruler is two")
|
||||
(is (= 23 (at 2 2)))
|
||||
(is (= 20 (at 0.5 2)) "half speed: two frames of the ruler are one inside")
|
||||
(is (= 20 (at 0.5 1)) "and one lands on the nearer whole frame, half upwards")
|
||||
(is (every? integer? (map #(at 0.5 %) (range -4 5))))))
|
||||
|
||||
(deftest sliding-a-held-clip-moves-it-rather-than-forgetting-where-it-was
|
||||
;; `span/held` writes `{:at f :rate 1}` with no `:mode`, and the slide used to
|
||||
;; read that as "no time map" and replace the whole thing — so the first drag
|
||||
;; of a freshly drawn clip threw its `:at` away and jumped it to the head of
|
||||
;; the lane. Same grid both sides here, so the only question is the mode.
|
||||
(let [c (assoc (on-twos) :fps 30)
|
||||
slid (:clip (nest/slide c :lane [:b] 3))]
|
||||
(is (= 22 (get-in slid [:symbols :lane :nodes :b :time :at])))
|
||||
(is (= [22 23] (node/placed-span (get-in slid [:symbols :lane :nodes :b]))))
|
||||
(is (= 1 (:rate (get-in slid [:symbols :lane :nodes :b :time])))
|
||||
"and the rest of its map is still there")))
|
||||
|
|
|
|||
|
|
@ -115,3 +115,73 @@
|
|||
lane (first (filter :lane? (timeline/rows doc :main #{} nil)))]
|
||||
(is (= [:vo] (mapv :id (:cels lane))))
|
||||
(is (empty? (timeline/sound-rows doc :main #{})))))
|
||||
|
||||
(deftest aiming-the-open-lanes-own-row-is-a-selection-and-not-a-crash
|
||||
;; `rows` names the open symbol's own lane row `[:node sid nil []]`, and
|
||||
;; `selected` used to open the rows above a selection with `(pop path)` —
|
||||
;; which THROWS on `[]`, aborting the whole event. Nothing was selected,
|
||||
;; nothing was aimed, and the next polygon went wherever the stale target
|
||||
;; still pointed, which is most of what made aiming a lane feel random.
|
||||
(let [doc (fixture/document)
|
||||
id (store/install! {:clip doc :store {}} "aim-the-open-lane")
|
||||
db {:clip/current id :paint/revision 0
|
||||
:ui {:open :main :target {:sid :main :id :a :path [:a]}}
|
||||
:playback {:frame 0}}
|
||||
lane (first (filter :lane? (timeline/rows doc :main #{})))
|
||||
after (ui/aimed db (:select lane))]
|
||||
(is (= [:node :main nil []] (:select lane)))
|
||||
(is (= [:node :main nil []] (get-in after [:ui :selection])))
|
||||
(is (nil? (get-in after [:ui :target]))
|
||||
"the open symbol IS the place, so aiming its own row clears the target")
|
||||
(is (= [] (ui/where-new-goes doc after)))
|
||||
(is (empty? (get-in after [:ui :expanded]))
|
||||
"and there are no rows above the top to open")))
|
||||
|
||||
(deftest finishing-a-polygon-opens-no-rows
|
||||
;; Expansion is the twist triangle's business. Finishing a shape used to open
|
||||
;; every row down to it, which inside a lane meant tearing its one row into a
|
||||
;; portal, that clip's channels and every shape already in the drawing.
|
||||
(let [doc (fixture/document)
|
||||
id (store/install! {:clip doc :store {}} "finish-opens-nothing")
|
||||
db {:clip/current id :paint/revision 0
|
||||
:ui {:open :main :tool :polygon
|
||||
:target {:sid :main :id :a :path [:a]}
|
||||
:draft [10 10 40 10 40 40]}
|
||||
:playback {:frame 1}}]
|
||||
(reset! rf-db/app-db db)
|
||||
(rf/dispatch-sync [::ui/finish-polygon])
|
||||
(let [after @rf-db/app-db
|
||||
[kind sid shape-id path] (get-in after [:ui :selection])]
|
||||
(is (= :node kind))
|
||||
(is (= :drawing-a sid) "the shape went into the drawing the aimed clip places")
|
||||
(is (= [:a shape-id] path))
|
||||
(is (empty? (get-in after [:ui :expanded])))
|
||||
(is (nil? (get-in after [:ui :tool]))))))
|
||||
|
||||
(deftest a-clip-made-for-a-drawing-is-selected-in-the-symbol-it-lives-in
|
||||
;; The clip `beginning-polygon` materializes lives in the symbol the LANE
|
||||
;; draws. Naming the open one instead left a selection that looked up to
|
||||
;; nothing, so the inspector, the breadcrumb and every span command went blank
|
||||
;; on a drawing that had just been created.
|
||||
(let [doc (-> (fixture/document)
|
||||
;; The last of the lane's three clips taken out, so frames 8
|
||||
;; to 12 are a gap and drawing there makes the held clip that
|
||||
;; was missing rather than landing in one that is there.
|
||||
(update-in [:symbols :main :nodes] dissoc :insert)
|
||||
(assoc-in [:symbols :shot]
|
||||
{:id :shot :fps 24 :frames 12
|
||||
:nodes {:girl {:id :girl :kind :instance :z "b" :span [0 12]
|
||||
:time {:mode :map :at 0 :rate 1}
|
||||
:source {:symbol :main}
|
||||
:playback {:in 0 :speed 1 :end :stop}}}}))
|
||||
id (store/install! {:clip doc :store {}} "clip-selected-where-it-lives")
|
||||
db {:clip/current id :paint/revision 0
|
||||
:ui {:open :shot :target {:sid :shot :id :girl :path [:girl]}}
|
||||
:playback {:frame 9}}
|
||||
after (ui/beginning-polygon db)
|
||||
[_ sid clip-id path] (get-in after [:ui :selection])
|
||||
saved (:clip (store/entry id))]
|
||||
(is (= :main sid) "the lane's symbol, not the open one")
|
||||
(is (= [:girl clip-id] path))
|
||||
(is (some? (get-in saved [:symbols :main :nodes clip-id]))
|
||||
"and that is where the node actually is")))
|
||||
|
|
|
|||
|
|
@ -56,6 +56,18 @@
|
|||
--aim: #8d4bd6;
|
||||
--aim-stage: #c79bf2;
|
||||
|
||||
/* A THIRD, and the last. SELECTED IN THE TIMELINE had been --sel-bg, which is
|
||||
#cfe0f5 against spans drawn in #dfe8f4: two pale blues a hair apart, in the
|
||||
one place where which row and which clip is selected decides what the pane
|
||||
is showing — an expanded lane opens exactly the selected clip. The fix is
|
||||
not a stronger blue, which stays in the family the spans already own, but a
|
||||
different hue entirely. Ochre: warm against every cool surface in the pane,
|
||||
clear of --aim's violet, --live's green and the playhead's red, and dark
|
||||
enough at full strength to carry white type on a 21px block. */
|
||||
--pick: #a85c00;
|
||||
--pick-bg: #ffd9a0;
|
||||
--pick-fg: #4a2800;
|
||||
|
||||
/* Auto-key is a recording state, distinct from selection and aiming. */
|
||||
--live: #239447;
|
||||
--live-bg: #e2f5e7;
|
||||
|
|
@ -975,6 +987,14 @@ button.share-button:hover, button.share-button.on { filter: brightness(1.1); }
|
|||
as tall as its rows instead, and at least as tall as the body. */
|
||||
align-items: flex-start;
|
||||
overflow: auto;
|
||||
/* THE GUTTER IS ALWAYS THERE, whether or not there is a scrollbar in it. Every
|
||||
mark in this pane is a percentage of the tracks column, and the column is
|
||||
what is left of the body after the labels -- so a vertical scrollbar
|
||||
appearing took 15px out of the width and moved the ruler, the frame grid,
|
||||
every clip and the playhead sideways, mid-gesture. Expanding a lane's portal
|
||||
adds seven rows at once, which is exactly when a person is looking at the
|
||||
frame they are aiming for. */
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
.tl-labels,
|
||||
|
|
@ -1039,11 +1059,38 @@ button.share-button:hover, button.share-button.on { filter: brightness(1.1); }
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tl-label.on { background: var(--sel-bg); }
|
||||
/* Selected, in the timeline's own colour. The bar down the left edge is the
|
||||
half that survives at a glance: a row 21px tall scrolled past in a column of
|
||||
twenty reads its left edge before it reads its fill. */
|
||||
.tl-label.on {
|
||||
background: var(--pick-bg);
|
||||
box-shadow: inset 3px 0 0 var(--pick);
|
||||
color: var(--pick-fg);
|
||||
}
|
||||
.tl-label.on .kind { color: var(--pick); }
|
||||
.tl-label.aimed { box-shadow: inset 0 0 0 2px var(--aim); }
|
||||
/* Both at once, and they must still be two readable facts: the aim's ring sits
|
||||
inside the pick's edge rather than replacing it. */
|
||||
.tl-label.on.aimed { box-shadow: inset 3px 0 0 var(--pick), inset 0 0 0 2px var(--aim); }
|
||||
.tl-label:hover:not(.on) { background: #fff; }
|
||||
.tl-label .name { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
||||
.tl-label .kind { color: var(--dim); }
|
||||
|
||||
/* A lane's row: the strip glyph, a faint warm ground under the name, and a
|
||||
track behind its blocks — enough that a lane is told from the instance rows
|
||||
around it while scanning, and not so much that it shouts. */
|
||||
.tl-ico {
|
||||
flex: 0 0 12px;
|
||||
line-height: 0;
|
||||
color: var(--dim);
|
||||
}
|
||||
.tl-ico > svg { display: block; width: 11px; height: 11px; }
|
||||
.tl-label.lane { background: #edeae2; }
|
||||
.tl-label.lane .name { font-weight: 600; }
|
||||
.tl-label.lane.on { background: var(--pick-bg); }
|
||||
.tl-label.lane.on .tl-ico { color: var(--pick); }
|
||||
.tl-label.lane:hover:not(.on) { background: #f8f6f0; }
|
||||
.tl-track.lane { background: rgba(0, 0, 0, 0.035); }
|
||||
.tl-name-input { min-width: 0; flex: 1; font: inherit; }
|
||||
.tl-rename { margin-left: auto; padding: 0 3px; border: 0; background: none; color: var(--dim); }
|
||||
.tl-rename:hover { color: var(--fg); }
|
||||
|
|
@ -1138,6 +1185,22 @@ button.share-button:hover, button.share-button.on { filter: brightness(1.1); }
|
|||
.tl-label.drop-front { box-shadow: inset 0 2px 0 var(--sel); }
|
||||
.tl-label.drop-back { box-shadow: inset 0 -2px 0 var(--sel); }
|
||||
|
||||
/* Selected: the clip block and the plain bar alike, in the pick colour. A clip
|
||||
block had no selected state at all, which is why the portal under an expanded
|
||||
lane looked like it opened at random. */
|
||||
.tl-cel.on {
|
||||
background: var(--pick);
|
||||
border-color: var(--pick);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
.tl-cel.on:hover:not(:disabled) { background: var(--pick); filter: brightness(1.12); }
|
||||
.tl-span.on { background: var(--pick-bg); border-color: var(--pick); }
|
||||
.tl-span.on.dense {
|
||||
background: repeating-linear-gradient(
|
||||
-45deg, var(--pick-bg) 0 3px, #f0bf77 3px 6px);
|
||||
}
|
||||
|
||||
/* A node's bar slides it along its symbol's time. */
|
||||
.tl-span.movable { cursor: ew-resize; touch-action: none; }
|
||||
.tl-span.movable.sliding { border-color: var(--sel); }
|
||||
|
|
@ -1155,7 +1218,10 @@ button.share-button:hover, button.share-button.on { filter: brightness(1.1); }
|
|||
.tl-junction {
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
left: 0;
|
||||
/* Centred on the CUT, which is the clips' shared border-box edge. A clip block
|
||||
is a button with a 1px border, so `left: 0` is one pixel inside it and the
|
||||
handle sat a pixel to the right of the line it drags. */
|
||||
left: -1px;
|
||||
bottom: -1px;
|
||||
width: 12px;
|
||||
transform: translateX(-50%);
|
||||
|
|
@ -1163,7 +1229,18 @@ button.share-button:hover, button.share-button.on { filter: brightness(1.1); }
|
|||
touch-action: none;
|
||||
z-index: 3;
|
||||
}
|
||||
.tl-cel-label { display: block; overflow: hidden; text-overflow: ellipsis; pointer-events: none; }
|
||||
/* `nowrap` is load-bearing. A block narrower than its name wrapped it at the
|
||||
hyphen — "symbol-" then "8" — and the button is `overflow: visible` so that
|
||||
second line spilled out below the track as a row of loose digits under the
|
||||
lane, which read as corrupt timing rather than as a long name in a short
|
||||
block. One line, clipped. */
|
||||
.tl-cel-label {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
pointer-events: none;
|
||||
}
|
||||
.tl-span.ghost {
|
||||
background: transparent;
|
||||
border: 1px dashed var(--sel);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue