Skip to content

Commit ff7d025

Browse files
RJWadleytrezy
authored andcommitted
refactor: handle running animations, refactor manual prop application
1 parent 712c13b commit ff7d025

6 files changed

+95
-71
lines changed

package-lock.json

+4-20
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@
9595
"@vitest/browser": "^2.0.4",
9696
"husky": "^8.0.0",
9797
"jsdom": "^25.0.0",
98-
"pixi.js": "^8.9.0",
98+
"pixi.js": "8.2.6",
9999
"playwright": "^1.45.3",
100100
"react": "^19.0.0",
101101
"react-dom": "^19.0.0",

src/motion/createMotionComponent.ts

+67-32
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
MotionConfigContext,
44
type ValueAnimationTransition,
55
} from 'motion/react';
6+
import { type ObservablePoint } from 'pixi.js';
67
import {
78
type ComponentProps,
89
createElement,
@@ -13,8 +14,8 @@ import {
1314
useImperativeHandle,
1415
useRef,
1516
} from 'react';
16-
import { useCompareEffect } from './useCompareEffect.js';
1717
import { useLatestFunction } from './useLatestFunction.js';
18+
import { usePointCompareEffect, usePointCompareMemo } from './usePointCompare.js';
1819

1920
import type { PixiElements } from 'typedefs/PixiElements.js';
2021

@@ -64,6 +65,9 @@ export type PixiMotionComponent<TagName extends SupportedElements> = (
6465
props: WithMotionProps<ComponentProps<TagName>>,
6566
) => JSX.Element;
6667

68+
/**
69+
* filter out property keys from transitions so that we don't accidentally pass an empty transition to motion
70+
*/
6771
const filterTransition = (transition: ValueAnimationTransition | undefined) =>
6872
{
6973
if (transition === undefined) return undefined;
@@ -130,12 +134,46 @@ export function createMotionComponent<TagName extends SupportedElements>(
130134
);
131135

132136
/**
133-
* get the initial state for a property
137+
* get the instant state for a property
134138
*
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
136141
*/
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+
}, []);
139177

140178
/**
141179
* animate to a certain state
@@ -150,59 +188,56 @@ export function createMotionComponent<TagName extends SupportedElements>(
150188

151189
if (typeof value === 'object')
152190
{
191+
const nestedValue = ref.current?.[key] as ObservablePoint;
153192
const { x, y } = value;
154193

194+
if (!nestedValue) return;
195+
155196
if (x !== undefined)
156197
{
157-
motionAnimate(ref.current[key], { x }, transition);
198+
saveAnimation(key, motionAnimate(nestedValue, {
199+
x: [
200+
nestedValue.x,
201+
x
202+
]
203+
}, transition));
158204
}
159205
if (y !== undefined)
160206
{
161-
motionAnimate(ref.current[key], { y }, transition);
207+
saveAnimation(key, motionAnimate(nestedValue, {
208+
y: [
209+
nestedValue.y, y
210+
]
211+
}, transition));
162212
}
163213
}
164214
else
165215
{
166-
motionAnimate(ref.current, { [key]: value }, transition);
216+
saveAnimation(key, motionAnimate(ref.current, { [key]: value }, transition));
167217
}
168218
},
169219
[getTransitionDetails],
170220
);
171221

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<
189223
Record<SupportedProps, SupportedValues[SupportedProps]>
190-
> = {};
224+
>>({});
191225

192226
for (const key of supportedProps)
193227
{
194228
/**
195-
* track our initial values, which never change after mount
229+
* track our initial/instant values
196230
*/
197-
initialProps[key] = useInitialState(key);
231+
propsToPass.current[key] = useInstantState(key);
198232
/**
199-
* instantly jump to a new value when that prop changes
233+
* stop our animations when values jump
200234
*/
201-
useCompareEffect(() => set(key), [set, props[key]]);
235+
usePointCompareEffect(() =>
236+
() => clear(key), props[key]);
202237
/**
203238
* animate our values when they change
204239
*/
205-
useCompareEffect(() => to(key, animate?.[key]), [to, animate?.[key]]);
240+
usePointCompareEffect(() => to(key, animate?.[key]), animate?.[key]);
206241
}
207242

208243
/**
@@ -215,7 +250,7 @@ export function createMotionComponent<TagName extends SupportedElements>(
215250

216251
return createElement(Component, {
217252
...props,
218-
...initialProps,
253+
...propsToPass.current,
219254
ref,
220255
});
221256
};

src/motion/useCompareEffect.ts

-17
This file was deleted.

src/motion/useLatestFunction.ts

-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { useCallback, useRef } from 'react';
22

3-
// biome-ignore lint/suspicious/noExplicitAny: generics
43
export function useLatestFunction<T extends(...args: any) => any>(thing: T)
54
{
65
const latestFunctionRef = useRef(thing);

src/motion/usePointCompare.ts

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { useEffect, useMemo } from 'react';
2+
3+
import type { ObservablePoint, PointData } from 'pixi.js';
4+
5+
type NumberPoint = number | ObservablePoint | PointData | undefined;
6+
7+
const toDependencyArray = (point: NumberPoint) => [
8+
typeof point === 'object' ? point.x : null,
9+
typeof point === 'object' ? point.y : null,
10+
typeof point !== 'object' ? point : null,
11+
];
12+
13+
export const usePointCompareEffect = (
14+
callback: VoidFunction,
15+
point: NumberPoint
16+
) =>
17+
useEffect(callback, toDependencyArray(point));
18+
19+
export const usePointCompareMemo = <MemoType>(
20+
memo: () => MemoType,
21+
point: NumberPoint
22+
) =>
23+
useMemo(memo, toDependencyArray(point));

0 commit comments

Comments
 (0)