A lane's drawings were going to be one instance whose source was a KEYED
channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of
a row are that channel's keys. Two things followed from it, and both were
wrong.
The first is that playback meant whichever shape the channel happened to have.
A framed source played its symbol; a keyed source froze the selected frame.
So `node/placed-at` read animation out of storage, and adding an ordinary key
to a still turned it into an animation — the last-key bug, which was not a bug
in the code so much as the rule working as written. But WHICH drawing is used
and HOW time runs inside it are independent questions, and all four combinations
are ordinary: hold one drawing, play one animation, cut between held drawings,
cut between playing ones.
So an occurrence names one symbol in `:source {:symbol ...}` and says how its
source time advances in `:playback {:in :speed :end}` — `source = in + speed *
f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named
rather than guessed. `node/placed-frame` samples it forwards, which works for
holds too, and `node/source-time` is the separate, invertible edit map, nil
where inversion is meaningless. The two were one function before, and a hold
had to lie about one of them.
The second is that a keyed source only looked necessary because an occurrence
was assumed to need a ROW. It does not. A lane is a group with `:layout
:sequence`, its occurrences are ordinary instances in the same flat node map,
and `timeline/rows` draws them as cel blocks on the lane's own row: twelve
exposures, one row, each cel still separately selectable and addressable. The
vertical growth that justified the keyed source is a presentation question, and
it is answered in the view.
`arthur.domain.sequence` holds the first commands over that shape — add lane,
append drawing, extend hold — each one history step, each refusing rather than
half-applying. Extending a hold leaves the lane's keys at their authored times,
because you are adjusting drawings underneath timed motion; a correction owned
by an occurrence travels with it. Ownership does that work, so no key needs a
flag saying what it follows. Ripple past the symbol's end is refused with the
frame count it would need, and `:extent :grow-symbol` is the caller saying yes.
`clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty
maps write no leaf, so a blank document could not survive its own round trip —
`leaf/leaves` promises exactness and was the only honest side of that.
Documents are schema 3. A version 2 document is not read; nothing here converts
one. `docs/lane-model.md` is the design, and says which of its parts are built.
392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor
through create, hold, explicit overflow and undo.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
91 lines
4.6 KiB
Markdown
91 lines
4.6 KiB
Markdown
# Timing model
|
|
|
|
[The Lane Model](lane-model.md) defines the revised target for occurrence timing,
|
|
source playback, sampling scope, and inverse editing. It supersedes conflicting
|
|
proposals here; the sections below describe earlier implementation decisions.
|
|
|
|
The source footage, authored drawings, generated face motion, and stage placement
|
|
have different frame decisions. They share a clock but do not share one kept-frame
|
|
list. `timing-handoff.md` records earlier implementation notes.
|
|
|
|
## Frame spaces
|
|
|
|
- A source frame addresses a decoded image and its measured face data. Keep the
|
|
source cadence and, for variable-rate video, its presentation timestamp.
|
|
- A timeline frame addresses authored keys in the clip or symbol's local space.
|
|
- A stage frame is mapped through the symbol instance's offset and rate before
|
|
local frame decisions are read. Moving a placement does not rewrite its keys.
|
|
|
|
The analyzed source poses remain dense. A lower picture rate or a skipped pose
|
|
never removes source data or shortens audio.
|
|
|
|
## Head placement
|
|
|
|
Analysis fits each source frame's rigid landmarks into one common head-local
|
|
space. Its inverse is the measured head transform, stored densely on `:head`.
|
|
The head node has one optional anchor map:
|
|
|
|
```clojure
|
|
;; no :anchors free movement: read measured frame f at f
|
|
:anchors {0 12} ; one lock: use frame 12's transform throughout
|
|
:anchors {0 12, 40 42} ; keyed locks: switch to frame 42 at local frame 40
|
|
```
|
|
|
|
A key is `(local change frame -> measured source frame)`. Its value holds to the
|
|
next key. The map chooses position, rotation and scale together. Frame zero must
|
|
have a key when the map exists. The dense transform blocks remain intact, so
|
|
editing anchors is a small document change and re-freezing can replace the
|
|
measurements without losing the anchor choices.
|
|
|
|
A source image used for tracing should be registered with that image's measured
|
|
stabilizing transform, then the selected head transform, then the authored
|
|
`:face` placement. This makes the photo and head-local vectors share the same
|
|
orientation and position. Tracing-photo selection is a separate editor address;
|
|
it does not choose the head anchor.
|
|
|
|
The prototype stabilizes into the shot's mean rigid pose and uses an early
|
|
closed-mouth frame for raster framing. Those are internal analysis and framing
|
|
choices. The authored head-anchor map above controls which measured head pose is
|
|
shown over each range. It is independent of plate drawing starts.
|
|
|
|
## Performance poses
|
|
|
|
Generated mouth, eye, and brow channels can be sampled at a lower picture rate
|
|
without retiming authored keys. The normal rule picks the latest available pose
|
|
at or before a picture-grid time. A future performance policy may add important
|
|
closed-mouth poses and store manual keeps/drops separately from the rate's
|
|
proposal. Related parts should share a selected pose by default: a mouth outline,
|
|
interior, teeth and generated visibility must not disagree about its frame.
|
|
|
|
## Stage placement
|
|
|
|
A symbol placement has optional pose-cut tracks, separate from head anchors:
|
|
|
|
```clojure
|
|
:playback {:tracks {:mouth {0 12, 8 27}
|
|
:eye-r {0 0, 4 6}}}
|
|
```
|
|
|
|
These maps are also `(local change frame -> source pose frame)`. They select which
|
|
baked/generated shape pose appears on that placement. Before the first explicit
|
|
cut, normal generated motion continues. Cuts hold, without interpolation, until
|
|
the next cut. A `[:node id]` track can override one shape in a shared group.
|
|
Authored cels, transforms, and audio remain on their normal local time.
|
|
|
|
The current implementation reads retained frozen channels. A separate resolved
|
|
geometry bake is not implemented; when added, it must preserve addressable
|
|
candidate poses so stage cuts can still select any of them.
|
|
|
|
## Ownership
|
|
|
|
| Choice | Owner | Current state |
|
|
| --- | --- | --- |
|
|
| Source frames and timestamps | Footage/analysis | Constant-rate frame indexing exists; variable timestamps remain future work |
|
|
| Head anchor map | `:head` node | Implemented, stored with the node |
|
|
| Trace frames (photo address) and origin | Face symbol's `:head` `:trace`, written through `:anchors` | Implemented, see `domain/trace` |
|
|
| Showing the tracing photo, and its opacity | Face instance's `:underlay` | Implemented; a drawing aid, not keyed |
|
|
| Generated picture-rate proposal and closure protection | Roto clip/symbol | Generated-only picture sampling exists; closure protection remains future work |
|
|
| Stage pose cuts | Symbol instance | Implemented, stored with the instance |
|
|
|
|
Preview and export use the same resolver for generated picture sampling and stage
|
|
cuts. Export still emits every timeline frame at the clip's audio rate.
|