3
3
MotionConfigContext ,
4
4
type ValueAnimationTransition ,
5
5
} from 'motion/react' ;
6
+ import { type ObservablePoint } from 'pixi.js' ;
6
7
import {
7
8
type ComponentProps ,
8
9
createElement ,
@@ -13,8 +14,8 @@ import {
13
14
useImperativeHandle ,
14
15
useRef ,
15
16
} from 'react' ;
16
- import { useCompareEffect } from './useCompareEffect.js' ;
17
17
import { useLatestFunction } from './useLatestFunction.js' ;
18
+ import { usePointCompareEffect , usePointCompareMemo } from './usePointCompare.js' ;
18
19
19
20
import type { PixiElements } from 'typedefs/PixiElements.js' ;
20
21
@@ -64,6 +65,9 @@ export type PixiMotionComponent<TagName extends SupportedElements> = (
64
65
props : WithMotionProps < ComponentProps < TagName > > ,
65
66
) => JSX . Element ;
66
67
68
+ /**
69
+ * filter out property keys from transitions so that we don't accidentally pass an empty transition to motion
70
+ */
67
71
const filterTransition = ( transition : ValueAnimationTransition | undefined ) =>
68
72
{
69
73
if ( transition === undefined ) return undefined ;
@@ -130,12 +134,46 @@ export function createMotionComponent<TagName extends SupportedElements>(
130
134
) ;
131
135
132
136
/**
133
- * get the initial state for a property
137
+ * get the instant state for a property
134
138
*
135
- * the priority matters here: initial always wins, then prefer props over animate
139
+ * on first render, this is the initial values
140
+ * if the value passed directly to a prop ever changes, it will become the instant value for future renders
136
141
*/
137
- const useInitialState = ( key : SupportedProps ) =>
138
- useRef ( initial ?. [ key ] ?? props [ key ] ?? animate ?. [ key ] ) . current ;
142
+ const useInstantState = ( key : SupportedProps ) =>
143
+ {
144
+ const instantValue = props [ key ] ;
145
+ // the priority matters here: initial always wins, then prefer props over animate
146
+ const initialValue = useRef ( initial ?. [ key ] ?? instantValue ?? animate ?. [ key ] ) . current ;
147
+
148
+ return usePointCompareMemo ( ( ) =>
149
+ {
150
+ if ( firstRenderRef . current ) return initialValue ;
151
+
152
+ return instantValue ;
153
+ } , instantValue ) ;
154
+ } ;
155
+
156
+ /**
157
+ * manage running animations so they don't continue to run after a value changes
158
+ */
159
+ const runningAnimations = useRef <
160
+ Partial <
161
+ Record <
162
+ SupportedProps ,
163
+ ReturnType < typeof motionAnimate > [ ]
164
+ >
165
+ >
166
+ > ( { } ) ;
167
+ const saveAnimation = useCallback ( ( key : SupportedProps , value : ReturnType < typeof motionAnimate > ) =>
168
+ {
169
+ runningAnimations . current [ key ] ??= [ ] ;
170
+ runningAnimations . current [ key ] . push ( value ) ;
171
+ } , [ ] ) ;
172
+ const clear = useCallback ( ( key : SupportedProps ) =>
173
+ {
174
+ runningAnimations . current [ key ] ?. forEach ( ( animation ) => animation . stop ( ) ) ;
175
+ runningAnimations . current [ key ] = [ ] ;
176
+ } , [ ] ) ;
139
177
140
178
/**
141
179
* animate to a certain state
@@ -150,59 +188,56 @@ export function createMotionComponent<TagName extends SupportedElements>(
150
188
151
189
if ( typeof value === 'object' )
152
190
{
191
+ const nestedValue = ref . current ?. [ key ] as ObservablePoint ;
153
192
const { x, y } = value ;
154
193
194
+ if ( ! nestedValue ) return ;
195
+
155
196
if ( x !== undefined )
156
197
{
157
- motionAnimate ( ref . current [ key ] , { x } , transition ) ;
198
+ saveAnimation ( key , motionAnimate ( nestedValue , {
199
+ x : [
200
+ nestedValue . x ,
201
+ x
202
+ ]
203
+ } , transition ) ) ;
158
204
}
159
205
if ( y !== undefined )
160
206
{
161
- motionAnimate ( ref . current [ key ] , { y } , transition ) ;
207
+ saveAnimation ( key , motionAnimate ( nestedValue , {
208
+ y : [
209
+ nestedValue . y , y
210
+ ]
211
+ } , transition ) ) ;
162
212
}
163
213
}
164
214
else
165
215
{
166
- motionAnimate ( ref . current , { [ key ] : value } , transition ) ;
216
+ saveAnimation ( key , motionAnimate ( ref . current , { [ key ] : value } , transition ) ) ;
167
217
}
168
218
} ,
169
219
[ getTransitionDetails ] ,
170
220
) ;
171
221
172
- /**
173
- * instantly jump to a certain state, interrupting any running animations
174
- */
175
- const set = useCallback (
176
- < T extends SupportedProps > ( key : T , value ?: SupportedValues [ T ] ) =>
177
- {
178
- if ( value === undefined ) return ;
179
- if ( firstRenderRef . current ) return ;
180
- if ( ! ref . current ) return ;
181
-
182
- // @ts -expect-error propably not possible to narrow, but types will catch it
183
- ref . current [ key ] = value ;
184
- } ,
185
- [ ] ,
186
- ) ;
187
-
188
- const initialProps : Partial <
222
+ const propsToPass = useRef < Partial <
189
223
Record < SupportedProps , SupportedValues [ SupportedProps ] >
190
- > = { } ;
224
+ > > ( { } ) ;
191
225
192
226
for ( const key of supportedProps )
193
227
{
194
228
/**
195
- * track our initial values, which never change after mount
229
+ * track our initial/instant values
196
230
*/
197
- initialProps [ key ] = useInitialState ( key ) ;
231
+ propsToPass . current [ key ] = useInstantState ( key ) ;
198
232
/**
199
- * instantly jump to a new value when that prop changes
233
+ * stop our animations when values jump
200
234
*/
201
- useCompareEffect ( ( ) => set ( key ) , [ set , props [ key ] ] ) ;
235
+ usePointCompareEffect ( ( ) =>
236
+ ( ) => clear ( key ) , props [ key ] ) ;
202
237
/**
203
238
* animate our values when they change
204
239
*/
205
- useCompareEffect ( ( ) => to ( key , animate ?. [ key ] ) , [ to , animate ?. [ key ] ] ) ;
240
+ usePointCompareEffect ( ( ) => to ( key , animate ?. [ key ] ) , animate ?. [ key ] ) ;
206
241
}
207
242
208
243
/**
@@ -215,7 +250,7 @@ export function createMotionComponent<TagName extends SupportedElements>(
215
250
216
251
return createElement ( Component , {
217
252
...props ,
218
- ...initialProps ,
253
+ ...propsToPass . current ,
219
254
ref,
220
255
} ) ;
221
256
} ;
0 commit comments