arthur/docs/timing-model.md

94 lines
4.7 KiB
Markdown
Raw Normal View History

# Timing model
2026-10-01 01:47:08 -04:00
[Time selection](time.md) defines the current frame-rate representation.
An occurrence is a node, with a clock of its own 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>
2026-09-30 15:20:23 -04:00
[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
The face owns its placement, not the take that holds it The source-to-stage mapping moves off :main's :face group and onto each face's own :place, above its head. `face-placement` computes exactly what it computed before, over every subject together, so two faces filmed side by side keep their filmed relation — it is written into each face instead of onto a group above them all. Same transform, same subtree, one level lower, and the composite is identical to the pixel: a digest over every op :main emits across the whole take is unchanged either way. THE OWNER IS THE POINT. A face carrying its own mapping is the right size wherever it is put — dropped into another symbol, or opened in its own tab to be drawn over — and the take that holds it needs to know nothing. On a group above the instances the scale belonged to the take, so a face taken out of it had no size at all and drew at a fraction of a pixel. The pool's thumbnails drop the workaround that knew about this: a symbol is rendered rooted at itself again, because a face now carries the placement that makes that honest, so the pool needs to know nothing about where a symbol happens to be used. `domain/node` and `arthur.export` leave its requires with it. The tests here were reading the placement off :main. The photo registration test changes shape rather than location: its premise was that face-1's head is its own root, so a photo sitting where it was filmed was image pixels over image height and nothing else. The head still cancels — that is what the test is about — but it now cancels against the face's own placement, which is why the photo comes with the face into its own tab instead of sitting at a fraction of a pixel beside it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-01 01:25:37 -04:00
`:place` placement the face carries. 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 | The face's `:plate` `:time :holds`, and its `:head` `:reads` | Implemented, see `docs/tracing-symbol-plan.md` |
| Showing a tracing layer, and its opacity | Editor state, `[:ui :tracing]` | Implemented; a drawing aid, never saved or 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.