-
-
Notifications
You must be signed in to change notification settings - Fork 1
/
useAnimationFunction.ts
140 lines (129 loc) · 4.23 KB
/
useAnimationFunction.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
import { useEffect } from "react";
import { isSameObject } from "../../core/utils";
import {
TypedKeyframeEffectOptions,
createAnimation,
_cancel,
_waitFor,
_finish,
_pause,
_play,
_reverse,
_setRate,
_setTime,
WaitingAnimationEventName,
} from "../../core/waapi";
import type { BaseAnimationHandle } from "./useAnimation";
import { useStatic } from "./useStatic";
import { useLatestRef } from "./useLatestRef";
/**
* Handle of {@link useAnimationFunction}.
* @typeParam Args - argument type
*/
export interface AnimationFunctionHandle<Args = void>
extends BaseAnimationHandle<Args> {}
export interface AnimationFunctionOptions extends TypedKeyframeEffectOptions {}
/**
* Non nullable [ComputedEffectTiming](https://developer.mozilla.org/en-US/docs/Web/API/AnimationEffect/getComputedTiming)
*/
export type ComputedTimingContext = Required<{
[key in keyof ComputedEffectTiming]: NonNullable<ComputedEffectTiming[key]>;
}>;
/**
* An argument of {@link useAnimationFunction}.
* In this callback you can update any state or ref in JS.
* - `ctx`: current animation state
* - `args`: any argument passed from play
*/
export type AnimationFunction<Args = void> = Args extends void
? (ctx: ComputedTimingContext) => void
: (ctx: ComputedTimingContext, args: Args) => void;
const bindUpdateFunction = <Args>(
animation: Animation,
getUpdateFunction: () => AnimationFunction<Args>,
args: Args
) => {
const update = () => {
const timing = animation.effect?.getComputedTiming();
if (!timing) return;
const progress = timing.progress;
if (progress != null) {
getUpdateFunction()(timing as ComputedTimingContext, args);
}
if (animation.playState === "running") {
requestAnimationFrame(update);
}
};
animation.ready.then(update);
};
/**
* Same as {@link useAnimation}, but it drives function not React element. See {@link AnimationFunctionHandle}.
* @typeParam Args - argument type
*/
export const useAnimationFunction = <Args = void>(
onUpdate: AnimationFunction<Args>,
options?: AnimationFunctionOptions
): AnimationFunctionHandle<Args> => {
const onUpdateRef = useLatestRef(onUpdate);
const optionsRef = useLatestRef(options);
const [handle, cleanup] = useStatic(
(): [AnimationFunctionHandle<Args>, () => void] => {
const getOnUpdate = () => onUpdateRef.current;
let cache: [Animation, AnimationFunctionOptions | undefined] | undefined;
const initAnimation = (opts: { args?: Args } = {}): Animation => {
const options = optionsRef.current;
if (cache) {
const [prevAnimation, prevOptions] = cache;
// Reuse animation if possible
if (isSameObject(options, prevOptions)) {
if (prevAnimation.playState !== "running") {
bindUpdateFunction(prevAnimation, getOnUpdate, opts.args!);
}
return prevAnimation;
}
prevAnimation.cancel();
}
const animation = createAnimation(null, null, options);
bindUpdateFunction(animation, getOnUpdate, opts.args!);
cache = [animation, options];
return animation;
};
const getAnimation = () => cache?.[0];
const externalHandle: AnimationFunctionHandle<Args> = {
play: (...opts) => {
_play(initAnimation(opts[0] as { args?: Args }), opts[0]);
return externalHandle;
},
reverse: () => {
_reverse(initAnimation());
return externalHandle;
},
cancel: () => {
_cancel(getAnimation());
return externalHandle;
},
finish: () => {
_finish(getAnimation());
return externalHandle;
},
pause: () => {
_pause(getAnimation());
return externalHandle;
},
setTime: (time) => {
_setTime(getAnimation(), time);
return externalHandle;
},
setPlaybackRate: (rate) => {
_setRate(getAnimation(), rate);
return externalHandle;
},
waitFor: (event: WaitingAnimationEventName) =>
_waitFor(getAnimation(), event).then(() => externalHandle),
};
return [externalHandle, externalHandle.cancel];
}
);
useEffect(() => cleanup, []);
return handle;
};