@anton.bobrov/react-vevet-hooks
Version:
A collection of custom React hooks designed to seamlessly integrate with the `Vevet` library
108 lines (89 loc) • 2.99 kB
text/typescript
import { useEvent, useDeepCompareMemoize } from '@anton.bobrov/react-hooks';
import { useCallback, useEffect, useRef, useState } from 'react';
import { AnimationFrame, NAnimationFrame } from 'vevet';
export interface IUseAnimationFrameOnFrameProps {
/** The multiplier for frames per second (FPS). */
fpsMultiplier: number;
/** Real-time FPS */
computedFPS: number;
}
export type TUseAnimationFrameOnFrame = (
props: IUseAnimationFrameOnFrameProps,
) => void;
export interface IUseAnimationFrameProps
extends Pick<NAnimationFrame.IChangeableProps, 'fps' | 'autoFpsFrames'> {
/** Event triggered when the animation starts playing */
onPlay?: () => void;
/** Event triggered when the animation is paused */
onPause?: () => void;
/** Event triggered on each animation frame */
onFrame: TUseAnimationFrameOnFrame;
}
/**
* Custom React hook that integrates with `vevet`'s `AnimationFrame` class.
*
* This hook creates an animation frame instance, allowing for
* customizable animation properties and callbacks for play, pause,
* and frame events. It handles the lifecycle of the animation instance,
* ensuring proper cleanup on component unmount.
*
* @example
* const MyComponent = () => {
* const { play, pause } = useAnimationFrame({
* onPlay: () => console.log('Animation started'),
* onPause: () => console.log('Animation paused'),
* onFrame: ({ fpsMultiplier }) => {
* console.log('Frame updated, FPS Multiplier:', fpsMultiplier);
* },
* });
*
* return (
* <div>
* <button onClick={play}>Play</button>
* <button onClick={pause}>Pause</button>
* </div>
* );
* };
*/
export function useAnimationFrame({
onPlay: onPlayProp,
onPause: onPauseProp,
onFrame: onFrameProp,
...changeableProps
}: IUseAnimationFrameProps) {
const [frame, setFrame] = useState<AnimationFrame | null>(null);
const initialChangeablePropsRef = useRef(changeableProps);
const onPlay = useEvent(onPlayProp);
const onPause = useEvent(onPauseProp);
const onFrame = useEvent(onFrameProp);
useEffect(() => {
const instance = new AnimationFrame({
...initialChangeablePropsRef.current,
isEnabled: false,
});
setFrame(instance);
if (onPlay) {
instance.addCallback('play', onPlay);
}
if (onPause) {
instance.addCallback('pause', onPause);
}
instance.addCallback('frame', () =>
onFrame({
fpsMultiplier: instance.fpsMultiplier,
computedFPS: instance.computedFPS,
}),
);
return () => instance.destroy();
}, [onFrame, onPause, onPlay]);
useEffect(() => {
if (!frame) {
return;
}
frame.changeProps(changeableProps);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [frame, useDeepCompareMemoize(changeableProps)]);
const play = useCallback(() => frame?.play(), [frame]);
const pause = useCallback(() => frame?.pause(), [frame]);
return { frame, play, pause };
}