fix(avatar-react): keep ambient motion running
This commit is contained in:
parent
175691ab32
commit
70a7ee2ea5
@ -40,6 +40,21 @@ const runtimeEnvironment = () => ({
|
||||
reduceMotion: window.matchMedia('(prefers-reduced-motion: reduce)').matches,
|
||||
})
|
||||
|
||||
const shouldRunFrameLoop = (
|
||||
definition: AvatarDefinition,
|
||||
playback: Readonly<CorePlaybackState>,
|
||||
environment: ReturnType<typeof runtimeEnvironment>
|
||||
) => {
|
||||
if (playback.status === 'playing') return true
|
||||
const motion = definition.expressions[playback.activeExpression]?.motion
|
||||
return (
|
||||
playback.status === 'stopped' &&
|
||||
!environment.reduceMotion &&
|
||||
motion !== undefined &&
|
||||
(motion.eyes !== 'none' || motion.body !== 'none')
|
||||
)
|
||||
}
|
||||
|
||||
export const markAvatarDefinitionValidated = (definition: object) => {
|
||||
validatedDefinitions.add(definition)
|
||||
}
|
||||
@ -331,7 +346,7 @@ export function Avatar({
|
||||
}, [playback.activeExpression, onExpressionChange])
|
||||
|
||||
useEffect(() => {
|
||||
if (playback.status !== 'playing') return
|
||||
if (!shouldRunFrameLoop(definition, playback, runtimeEnvironment())) return
|
||||
let frame = 0
|
||||
const tick = (now: number) => {
|
||||
const current = playbackRef.current
|
||||
@ -351,11 +366,13 @@ export function Avatar({
|
||||
}
|
||||
const frameScene = renderPlaybackFrame(next, now, environment)
|
||||
paintScene(frameScene)
|
||||
if (next.status === 'playing') frame = requestAnimationFrame(tick)
|
||||
if (shouldRunFrameLoop(definition, next, environment)) {
|
||||
frame = requestAnimationFrame(tick)
|
||||
}
|
||||
}
|
||||
frame = requestAnimationFrame(tick)
|
||||
return () => cancelAnimationFrame(frame)
|
||||
}, [definition, playback.status, onAnimationEnd])
|
||||
}, [definition, playback.activeExpression, playback.status, onAnimationEnd])
|
||||
|
||||
const controlled = animation !== undefined || expression !== undefined
|
||||
useImperativeHandle(ref, () => ({
|
||||
|
||||
@ -19,6 +19,11 @@ const expression = {
|
||||
motion: { eyes: 'none', body: 'none' },
|
||||
} as const
|
||||
|
||||
const ambientExpression = {
|
||||
...expression,
|
||||
motion: { eyes: 'none', body: 'shake' },
|
||||
} as const
|
||||
|
||||
const definition: AvatarDefinition = {
|
||||
schema: 'bible-strong/avatar-definition',
|
||||
schemaVersion: 1,
|
||||
@ -28,8 +33,12 @@ const definition: AvatarDefinition = {
|
||||
nodes: [],
|
||||
},
|
||||
colors: { body: '#5b7fe5', eyes: '#111316' },
|
||||
expressions: { neutral: expression, smile: { ...expression, head: { x: 0, y: 10, z: 0 } } },
|
||||
expressionOrder: ['neutral', 'smile'],
|
||||
expressions: {
|
||||
neutral: expression,
|
||||
smile: { ...expression, head: { x: 0, y: 10, z: 0 } },
|
||||
restless: ambientExpression,
|
||||
},
|
||||
expressionOrder: ['neutral', 'smile', 'restless'],
|
||||
animations: {
|
||||
greet: {
|
||||
playbackMode: 'loop',
|
||||
@ -298,6 +307,33 @@ describe('@bible-strong/avatar-react', () => {
|
||||
cancel.mockRestore()
|
||||
})
|
||||
|
||||
it('keeps scheduling frames for a controlled expression with ambient motion', () => {
|
||||
let nextFrame = 0
|
||||
const frames = new Map<number, FrameRequestCallback>()
|
||||
const request = vi.spyOn(window, 'requestAnimationFrame').mockImplementation(callback => {
|
||||
frames.set(++nextFrame, callback)
|
||||
return nextFrame
|
||||
})
|
||||
const cancel = vi.spyOn(window, 'cancelAnimationFrame').mockImplementation(id => {
|
||||
frames.delete(id)
|
||||
})
|
||||
|
||||
render(<Avatar definition={definition} expression="restless" />)
|
||||
|
||||
act(() => {
|
||||
for (let index = 1; index <= 3; index++) {
|
||||
const callback = [...frames.values()].at(-1)
|
||||
frames.clear()
|
||||
callback?.(index * 1_000)
|
||||
}
|
||||
})
|
||||
|
||||
expect(request).toHaveBeenCalledTimes(4)
|
||||
expect(frames).toHaveLength(1)
|
||||
request.mockRestore()
|
||||
cancel.mockRestore()
|
||||
})
|
||||
|
||||
it('fires once-completion exactly once under Strict Mode', () => {
|
||||
let nextFrame = 0
|
||||
const frames = new Map<number, FrameRequestCallback>()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user