Make a symbol from a dropped video

Dropping footage on the stage or the timeline asks which frames and what name:
a dialog plays the video with start and end handles that seek it. Detection
runs on that range only (decoding stops at the end, frames before the start
are skipped) and the range is part of the analysis address, so a partial
analysis is never served as a whole one. The frozen take comes in as one
named symbol, carrying its sound as an audio node, placed where it was
dropped; tracking and tuning come with it when the document has no analysis
of its own.

clip/adopt copies symbols between documents, renaming ids that collide, and
clip/audio-tracks carries sounds out of nested instances so a placed symbol
is heard where it is placed.

Fixes the stage going blank after a conversion: ::store recomputed only when
the clip id changed, so blocks merged into the loaded entry were invisible to
the resolver. And the paint loop now schedules its next frame before
painting and reports a frame it cannot draw instead of stopping.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Olive Vaughn 2026-09-29 13:21:08 -04:00
parent 270c5a4369
commit 41b4bdf110
18 changed files with 562 additions and 120 deletions

View file

@ -654,3 +654,74 @@ input[type="range"] { width: 100%; accent-color: var(--sel); }
}
.tl-empty { padding: 9px; color: var(--dim); }
/* --------------------------------------------------------------------------
the video -> symbol dialog */
.convert-scrim {
position: fixed;
inset: 0;
z-index: 20;
display: grid;
place-items: center;
background: rgba(0, 0, 0, .35);
}
.convert {
width: min(560px, calc(100vw - 32px));
background: var(--pane);
border: 1px solid var(--line);
border-radius: 3px;
box-shadow: 0 6px 24px rgba(0, 0, 0, .3);
}
.convert-body { display: grid; gap: 8px; padding: 10px; }
.convert-video {
width: 100%;
max-height: 46vh;
background: var(--stage);
border-radius: 2px;
}
/* The range: the kept frames lit, a handle at each end. */
.convert-range {
position: relative;
height: 18px;
margin: 0 7px;
background: var(--sunk);
border: 1px solid var(--hair);
border-radius: 2px;
touch-action: none;
}
.convert-kept {
position: absolute;
top: 0;
bottom: 0;
background: var(--sel-bg);
border-left: 1px solid var(--sel);
border-right: 1px solid var(--sel);
}
.convert-handle {
position: absolute;
top: -3px;
bottom: -3px;
width: 10px;
margin-left: -5px;
background: var(--sel);
border-radius: 2px;
cursor: ew-resize;
}
.convert-name { display: flex; align-items: center; gap: 8px; }
.convert-name input { flex: 1; }
.convert-actions {
display: flex;
justify-content: flex-end;
gap: 6px;
padding: 8px 10px;
border-top: 1px solid var(--hair);
}