Skip to content

feat(motion): motion deck for firing clips without changing the selection - #51

Merged
rosspeili merged 6 commits into
ARPAHLS:mainfrom
AUDOSt0ck1ng:feat/motion-deck
Aug 16, 2026
Merged

feat(motion): motion deck for firing clips without changing the selection#51
rosspeili merged 6 commits into
ARPAHLS:mainfrom
AUDOSt0ck1ng:feat/motion-deck

Conversation

@AUDOSt0ck1ng

@AUDOSt0ck1ng AUDOSt0ck1ng commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator

Settings → Motion. The user assembles a list of cards, each holding one clip and any number of chords; pressing a chord — or clicking the card — plays that clip once and hands the stage back to the Animations selection, without touching animationId.

Rebased onto main now that #50 has landed.

The design, what it takes from the issue as written, where it diverges, and what switching animation folders does: #24 (comment)

Two commits

  1. feat(stage)animation.play takes a mode (select / once) and animation.stop ends a one-shot early. Not specific to this feature: the agent bus ([Feature]: Local agent/event bus so external tools can drive the avatar #6) and keyword triggers ([Feature]: Keyword → animation mapping table #7) are meant to drive the same commands. Verified to build and pass on its own.
  2. feat(motion) — the deck, confined to src/motion-deck/. Outside that folder it is four lines in userSettings.js and nine in AvatarStage.jsx; the folder's README carries the removal steps and the decisions worth not re-litigating.

Notes for review

  • Nothing prunes the deck. Switching Directories → Animations replaces the catalog, and the autosave would persist a pruned deck within its debounce — "point it at another folder to look" would cost the whole table with no undo. Unresolved cards are shown as unavailable and keep their chords reserved. Clear unavailable is the only sweep and refuses to run against an empty catalog, since a custom folder is scanned asynchronously and every card is briefly unresolvable at launch.
  • Cards self-heal by name. A custom folder's ids hash the absolute path, so moving or renaming it invalidates all of them while the files are untouched. A unique name match only — two folders can each hold a wave.vrma.
  • Binding a taken chord moves it rather than warning or blocking, so no stored binding is ever ambiguous.
  • Deriving library ids from file content rather than path would fix the id fragility at the source. Left out deliberately — it shifts existing animationId values and deserves its own change.

lint, test (93, of which 21 are new) and build pass. Manually tested on the desktop shell: binding and firing, folder switches in both directions, folder rename, reset, and the app's own keyboard not being swallowed while a binding is armed.

Refs #24

🤖 Generated with Claude Code

@AUDOSt0ck1ng

Copy link
Copy Markdown
Collaborator Author

This PR is based on #50
The differences will decrease after PR 50 merges.

@AUDOSt0ck1ng

Copy link
Copy Markdown
Collaborator Author

@rosspeili

Copy link
Copy Markdown
Contributor

@AUDOSt0ck1ng this looks good on the vid, I wonder if we could make the cards like a carousel with arrows or highlighted dots instead of scrolling, eg. in case someone has dozens of animations? or what do you think about one single card that can hold all pairs just name + hotkey for each line, not sure, just an idea.

Since this was done before 50 was merged, let's merge once this is rebased on main, and I would add: docs/using-the-app.md still says there’s no hotkey set, worth a one-line update when this lands.

Other than that this looks dope and unlocks a bunch of potential new issues towards this direction. Thanks <3

@AUDOSt0ck1ng
AUDOSt0ck1ng marked this pull request as draft August 15, 2026 13:43
AUDOSt0ck1ng and others added 3 commits August 15, 2026 21:49
`animation.play` takes a `mode`. `select` is the default and behaves as
before: it changes what the stage is set to, and that is what config.yaml
stores. `once` overlays a single pass and leaves `animationId` alone, so a
gesture cannot cost the user the Default sequence or loop they chose.
`animation.stop` ends one early.

`once` is refused for anything that cannot end on its own — the Default
sequence loops for as long as it is selected, so there is no frame at
which the selection could come back.

The overlay is transient state in AvatarStage, never persisted, and is
dropped when the model or the animation catalog changes: the mixer
belongs to the VRM and the url belongs to the catalog. Resuming skips a
sequence's intro when the selection underneath has not changed, or the
avatar would greet you again after every gesture; a pick made while a
gesture runs is a fresh start and still plays in full.

Also detach a one-shot's `finished` listener whichever generation it
belongs to. A superseded one used to return early and leave its closure
on the mixer for the life of the model — reachable only from the
Animations menu before, reachable from every key press now.

This layer is not specific to any one trigger. The Motion Deck is its
first consumer, and the local agent bus and keyword triggers are meant to
drive the same commands rather than grow their own path to the mixer.

Refs #6, #7, ARPAHLS#24

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…tion

Settings → Motion. The user assembles a list of cards, each holding one
clip and any number of chords; pressing a chord — or clicking the card —
plays that clip once and hands the stage back to the Animations
selection. Refs ARPAHLS#24.

Bindings are authored rather than derived. The issue asks for a bind
control on every row of the animation catalog, but a table of the catalog
is a projection of whichever folder is currently mounted, so it
structurally cannot hold a binding for a clip that folder does not
contain. An authored list holds cards from several folders at once and
does not care which one is mounted; it also gives the agent bus and
keyword triggers a stable unit to point at later.

Which is why nothing prunes the deck. Switching Directories → Animations
replaces the whole catalog, and the autosave would persist a pruned deck
within its debounce — "point it at another folder to look" would cost the
table with no undo. Cards resolve against the live catalog when drawn and
when fired; an unresolved one is shown as unavailable, keeps its chords
reserved, and comes back when its folder does. Clear unavailable is the
only sweep, and it refuses to run against an empty catalog, since a
custom folder is scanned asynchronously and every card is briefly
unresolvable at launch.

Cards remember their clip's name and re-point themselves when the id
misses and exactly one clip matches — a custom folder's ids hash the
absolute path, so moving or renaming it invalidates all of them while the
files are untouched. A unique match only: two folders can each hold a
wave.vrma.

Binding a chord that is taken moves it rather than warning or blocking:
two cards holding one chord has no defined behaviour, and refusing throws
away the key the user just pressed. Chords are keyed off event.code, so
the same physical key records and fires under one name. Only single clips
can be added, since a sequence has no last frame to hand the stage back
on, and keys are live only while the window has focus.

The feature is confined to src/motion-deck/, which carries a README with
the removal steps; outside it, four lines in userSettings.js and nine in
AvatarStage.jsx.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Section 9 said no hotkey was set; Settings → Motion now binds keys, though
only while the window has focus.

Refs ARPAHLS#24

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@AUDOSt0ck1ng

AUDOSt0ck1ng commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

@rosspeili
Do you mean like this:
image

AUDOSt0ck1ng and others added 3 commits August 15, 2026 22:24
Review on ARPAHLS#24 asked for either a carousel or a single card holding every
name/hotkey pair. Rows, not a carousel: paging hides the deck behind an
interaction, and the panel exists to be read down — name against chord is
the only pairing on it worth scanning. A card each spent a bordered block
and two stacked lines on what fits in one, so a deck of twenty was taller
than the drawer it sits in.

The list scrolls itself rather than the drawer, so Add motion stays where
it was under a deck of any length, and past five rows the deck grows a
filter box for reaching one row to edit without scrolling to it.

Filtering never reindexes. Every action addresses a card by its position
in the deck, so the filtered list carries the original index rather than
its own — otherwise the first row of a filtered view would fire, rebind
or delete whichever card happens to sit at position zero.

Focusing the filter cancels an armed recording. The keydown listener
swallows everything while recording, so a filter box next to the row
being bound would otherwise eat the first letter typed and bind it.

The unavailable sentence a card could afford does not fit a row: the row
carries a badge, and the explanation moves next to Clear unavailable,
where it is said once instead of per card.

Refs ARPAHLS#24

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The drawer is ~237px wide, which the row layout was written without
measuring. Three things came out of measuring it.

The bind slot dropped to a second line on every row, however much space
was free: a wrap container that is itself a flex item resolves an `auto`
basis to min-content — one chip's width — so `flex-basis: max-content`
is what makes the chips sit on the name's line.

Chips are smaller (10px, 2px 5px) and the bind slot is a bare `+` rather
than `+ Key`. At this width the five characters cost more of the name
than the word was worth, and the slot reads as one by sitting among the
chords; the recording state still spells out "Press a key…".

The name gets a 110px floor and the chords give way, rather than the two
shrinking together — that split truncated names to "ba…" while the chords
wrapped anyway, which lost both. So a clip with two chords, or one long
one, is two lines, and a long name still ends in an ellipsis with the
full text on hover. One line was the aim, not a requirement worth
truncating a name over.

Screenshots of the panel for docs/screenshots/, in the 8x settings block.

Refs ARPAHLS#24

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The two screenshots landed in docs/screenshots/ without anything pointing
at them. Both docs now do, the way Directories is shown in each.

Section 7 of the walkthrough had no Motion subsection at all — it covered
Overlay, Snap, Directories, VRoid Hub and System, and the panel between
Directories and VRoid Hub was reachable only through a line in the
keyboard notes. It now sits in the drawer's own order.

Refs ARPAHLS#24

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@rosspeili

Copy link
Copy Markdown
Contributor

This looks really good @AUDOSt0ck1ng, thanks <3 merging as is.

@rosspeili
rosspeili merged commit 9af12ef into ARPAHLS:main Aug 16, 2026
1 check passed
rosspeili added a commit that referenced this pull request Aug 16, 2026
Fold Unreleased into 0.7.0 (Motion Deck, stage commands, custom VRMA, env posters), point download links and citation metadata at the new installer, and refresh the roadmap for shipped #24/#50/#51 work.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants