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:
1 parent
f61f90d613
commit
68ea8576b6
8 files changed
+515
-32
No files matched your search
+39
-1
@@ -1,6 +1,9 @@
|
||||
import { createApp, h, nextTick, type Component, type Slot } from 'vue'
|
||||
import { createBlankScene, useSceneStore } from '@/stores/scene'
|
||||
import { useButtonRuntime } from '@/composables/useButtonRuntime'
|
||||
import {
|
||||
applyButtonStateChange,
|
||||
useButtonRuntime,
|
||||
} from '@/composables/useButtonRuntime'
|
||||
import {
|
||||
buildMarkerMap,
|
||||
evalAction,
|
||||
@@ -3543,6 +3546,41 @@ export function registerAllTests(): void {
|
||||
expect(rt.getState('b1', false)).toBe('disabled')
|
||||
})
|
||||
|
||||
test('applyButtonStateChange starts a transition only when one matches', () => {
|
||||
const rt = useButtonRuntime()
|
||||
rt.reset()
|
||||
// No transitions defined: state flips instantly, no transition entry.
|
||||
applyButtonStateChange('b1', 'hover', undefined, undefined)
|
||||
expect(rt.getState('b1')).toBe('hover')
|
||||
expect(rt.transition.has('b1')).toBe(false)
|
||||
// With a matching transition: flip state AND start transition.
|
||||
rt.reset()
|
||||
applyButtonStateChange(
|
||||
'b1',
|
||||
'hover',
|
||||
[{ from: 'normal', to: 'hover', duration: 0.2, easing: 'easeOut' }],
|
||||
undefined,
|
||||
)
|
||||
expect(rt.getState('b1')).toBe('hover')
|
||||
expect(rt.transition.has('b1')).toBe(true)
|
||||
rt.endTransition('b1')
|
||||
})
|
||||
|
||||
test('sampleTransition returns null outside of transitions, u∈[0..1] within', () => {
|
||||
const rt = useButtonRuntime()
|
||||
rt.reset()
|
||||
expect(rt.sampleTransition('b1', performance.now())).toBe(null)
|
||||
const t0 = performance.now()
|
||||
rt.startTransition('b1', 'normal', 'hover', 1, 'linear', t0)
|
||||
const mid = rt.sampleTransition('b1', t0 + 500)!
|
||||
expect(mid.done).toBe(false)
|
||||
expect(mid.u > 0.4 && mid.u < 0.6).toBe(true)
|
||||
const end = rt.sampleTransition('b1', t0 + 1100)!
|
||||
expect(end.done).toBe(true)
|
||||
expect(end.u).toBe(1)
|
||||
rt.endTransition('b1')
|
||||
})
|
||||
|
||||
test('store.actionRuntime resolves labels and drives useButtonRuntime', () => {
|
||||
const s = makeFreshStore()
|
||||
const rt = useButtonRuntime()
|
||||
|
||||
Reference in new issue
Block a user