macro-clips slice 7: button state transitions

- ButtonClip transitions are now editable in ButtonInstanceDialog
  (from/to/duration/easing) and persisted on the clip via the new
  store.setButtonClipTransitions action; transitions are shared by
  every instance of the button (clip-scoped, not instance-scoped).
- lerpProps exported from interpolate.ts so callers outside the
  sampler can blend two poses.
- useButtonRuntime tracks an active transition per instance plus a
  reactive transitionTick that pokes Vue computeds each frame.
  A self-driving RAF loop runs only while some transition is active
  (idles when none), so animations work paused or playing.
- applyButtonStateChange(id, to, transitions, persistedDisabled)
  is the one entry point for state changes — it looks up a matching
  from→to transition and starts it, or flips state instantly. Pointer
  handlers in CanvasStage and store.actionRuntime.setButtonState both
  go through it.
- CanvasStage blends the two state stacks via sampleStateLayers +
  blendStates: layers matched by name interpolate their sampled props
  with lerpProps; unmatched layers fade in/out. Renderer uses the
  blended layer list in place of the pure state stack during transit.
- Export runtime mirrors the same flow: applyButtonChange starts
  transitions, sampleButtonTransition drives per-frame progress, and
  drawButtonStatesBlended composites via drawLayer(layer, 0, override).
  drawLayer gained an optional propsOverride param for this.
  RUNTIME_VERSION bumped to 6.
- Tests cover applyButtonStateChange matching logic and
  sampleTransition's time → u mapping plus done flag.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
thandClaude Opus 4.7 committed 2026-04-20 21:48:50 +02:00
1 parent f61f90d613
commit 68ea8576b6
8 files changed
+515 -32

No files matched your search

+18 -2
View File
@@ -27,7 +27,10 @@ import {
scriptFramesInRange,
type ActionRuntime,
} from '@/lib/actions'
import { useButtonRuntime } from '@/composables/useButtonRuntime'
import {
applyButtonStateChange,
useButtonRuntime,
} from '@/composables/useButtonRuntime'
const uid = () => Math.random().toString(36).slice(2, 10)
@@ -1204,6 +1207,15 @@ export const useSceneStore = defineStore('scene', {
const clip = this.scene.buttonClips?.find((c) => c.id === id)
if (clip) clip.name = name.trim() || clip.name
},
setButtonClipTransitions(
id: string,
transitions: NonNullable<ButtonClip['transitions']>,
) {
const clip = this.scene.buttonClips?.find((c) => c.id === id)
if (!clip) return
if (transitions.length) clip.transitions = [...transitions]
else delete clip.transitions
},
addButtonInstance(
buttonClipId: string,
clipTrackId: string,
@@ -1577,7 +1589,11 @@ export const useSceneStore = defineStore('scene', {
console.warn('[animatr] unknown button ref:', ref)
return
}
btnRt.setState(id, state)
const inst = instances.find((x) => x.id === id)
const clip = self.scene.buttonClips?.find(
(c) => c.id === inst?.buttonClipId,
)
applyButtonStateChange(id, state, clip?.transitions, inst?.disabled)
},
setButtonDisabled(ref, disabled) {
const id = resolve(ref)