arthur/docs/timing-model.md
Your Name 3d3c1bbca0 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

4.6 KiB

Timing model

The Lane Model 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:

;; 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:

: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.