A held cel is a one-frame symbol that loops
Holding a drawing and looping a one-frame symbol are the same picture, and the second is a case the model already carries, so the first is a special case kept for nothing. Speed 0 goes; a cel becomes a one-frame symbol with `:end :loop` and a span. Looping and span are already properties of the instance and `:frames` already belongs to the symbol, so nothing moves — a mode is deleted. Two spellings of looping collapse to one, `:time :loop?` giving way to `:playback :end :loop`, and `extend-hold` collapses into `resize-out`, since it exists only to refuse anything that is not frozen before editing a span. What it buys is one rule where there were three refusals. `nest/inside` has no invertible clock for a hold, for `:end :hold`, or for a loop, so shift-to-reparent refuses all three — which is most of what anybody would drag onto. Resolving the move with the destination's map at the CURRENT frame covers every one: a loop is affine within the period the frame falls in, a one-frame loop is that rule with a period of one — which lands exactly where the nesting notes argued it should from first principles — and `:end :hold` is affine in the played part and frozen in the tail. The trap is written down beside it, because it would be found the hard way: `audio-tracks` expands a loop into one walk per period, and is saved today only by the `(pos? speed)` guard that a held cel fails. Make every drawing a loop and a drawing held for 120 frames becomes 120 walks emitting any nested sound 120 times. An audibility precheck has to land in the same change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
6e7827e03f
commit
d029908f0b
1 changed files with 81 additions and 4 deletions
|
|
@ -123,6 +123,82 @@ people, and it does not make the symbol a different kind of thing. The thing
|
|||
to hold the line on is that nothing outside the timeline, and the commit
|
||||
path's overlap check, is allowed to read it.
|
||||
|
||||
## Tear out the held cel
|
||||
|
||||
A held cel is a 1-frame symbol shown for many frames. A 1-frame symbol with
|
||||
`:end :loop`, lengthened, is the same picture by a different route — and the
|
||||
second route is a case the model already has, so keeping the first one is
|
||||
keeping a special case for free.
|
||||
|
||||
**What is already true**, so that nothing has to move: `:span` is on the node,
|
||||
in its own frames; `:time` (`:at`, `:rate`) is on the node; looping is on the
|
||||
node. The SYMBOL owns only `:frames`, the authored window. So looping and span
|
||||
are instance properties already, and this change is about deleting a mode, not
|
||||
relocating a field.
|
||||
|
||||
**What has to be decided and collapsed:**
|
||||
|
||||
- There are two spellings of looping — `:time :loop?` and `:playback :end
|
||||
:loop` — and both are read, in `node/placed-frame` and in
|
||||
`nest/audio-tracks`. Keep one. `:playback {:in :speed :end}` already says
|
||||
what happens at the ends, so `:end :loop` is the one to keep and
|
||||
`:time :loop?` is the one to delete.
|
||||
- `:playback :speed 0` stops being produced. Make it illegal in
|
||||
`node/problems` rather than legal-but-unused, so a frozen clock has exactly
|
||||
one spelling: a 1-frame loop.
|
||||
- `lane/extend-hold` exists only because holds were special — it refuses
|
||||
anything whose speed is not 0 and then edits a span. Once a hold is a loop,
|
||||
lengthening one IS `resize-out`, and the command collapses into it.
|
||||
- `cel` survives as the word for the creation policy — a new empty symbol is
|
||||
one frame — but stops naming a playback mode.
|
||||
|
||||
**What it buys, and this is the point:** one rule for nesting, which settles
|
||||
the refusal that blocks shift-to-reparent today. `nest/inside` currently has
|
||||
no `:time` for a hold, for `:end :hold`, or for a loop, and so refuses all
|
||||
three. The general rule that covers all of them: **resolve the move with the
|
||||
destination's map at the CURRENT frame — the affine piece the current frame
|
||||
falls in.**
|
||||
|
||||
- A loop of length L is affine within the period the current frame is in.
|
||||
Timing is preserved inside that period and repeats after it, which is what
|
||||
looping means.
|
||||
- A 1-frame loop — the ex-hold — has a period of length 1, so the map within
|
||||
it is trivially invertible and lands the moved node on frame 0, aligned to
|
||||
the current frame. That is exactly the answer `docs/lane-nesting-notes.md`
|
||||
argues for from first principles, arrived at here as an instance of the
|
||||
general rule instead of a special case.
|
||||
- `:end :hold` is affine in the played part and frozen in the tail, which the
|
||||
same sentence covers.
|
||||
|
||||
**The trap, which must be handled in the same change.** `nest/audio-tracks`
|
||||
expands a loop into one walk PER PERIOD:
|
||||
|
||||
periods (range (floor (/ (to-local source lo) length))
|
||||
(ceil (/ (to-local source hi) length)))
|
||||
|
||||
Today a held cel is skipped entirely — `(pos? speed)` is the guard, and the
|
||||
comment says a visual freeze does not emit a sustained audio sample. Turn
|
||||
every drawing into a 1-frame loop and that guard stops firing: a drawing held
|
||||
for 120 frames becomes 120 recursive walks, and any sound inside it is emitted
|
||||
120 times. That is both a wrong mix and a performance cliff on the most common
|
||||
node in the document. Required with this change: a cheap `symbol/audible?`
|
||||
precheck so a source with no audio anywhere inside it is never period-expanded,
|
||||
and a cap or a different formulation for the ones that are.
|
||||
|
||||
Also worth knowing before the change: `ui/timeline`'s own docstring already
|
||||
says a looping instance draws only its first pass. With every drawing a loop,
|
||||
that sentence now describes every drawing — harmless, since one pass of a
|
||||
1-frame symbol is the whole of it, but the docstring should stop sounding like
|
||||
a limitation.
|
||||
|
||||
**Dropping into a 1-frame symbol.** The window is authored and crops what it
|
||||
holds, so a 10-frame symbol dropped into a 1-frame drawing shows its frame 0
|
||||
and nothing else. That is consistent — `:frames` is the shot length and
|
||||
`:extent :grow-symbol` is the opt-in — but it is probably not what somebody
|
||||
dragging means. Offer the growth through the `:required-frames` retry the
|
||||
model already uses: the command reports what it would need, the UI offers one
|
||||
button.
|
||||
|
||||
## Order of work
|
||||
|
||||
Each step compiles, passes `npm test`, and leaves the editor usable.
|
||||
|
|
@ -183,7 +259,8 @@ to keep.
|
|||
2. **Does a lane row's edge drag trim the placing instance's span, or ripple
|
||||
the children?** Same handle, two commands; the row is now an instance, so
|
||||
it has a span of its own for the first time.
|
||||
3. **The held destination for shift-to-reparent** is still refused —
|
||||
`nest/inside` has no invertible clock for a hold, which is most of what
|
||||
anybody would try to nest into. `docs/lane-nesting-notes.md` argues the
|
||||
refusal is stronger than the facts require and says what would settle it.
|
||||
3. **Does tearing out the held cel come before or after the lane work?** It
|
||||
is independent of it — `nest` never knew about lanes — and it is what makes
|
||||
shift-to-reparent work on the thing people would actually drag onto. Doing
|
||||
it first means the lane work lands on a model with one playback mode fewer;
|
||||
doing it after means two changes to `lane_test`'s fixtures instead of one.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue