@anton.bobrov/react-vevet-hooks
Version:
A collection of custom React hooks designed to seamlessly integrate with the `Vevet` library
108 lines (86 loc) • 2.85 kB
text/typescript
import { objectKeys, useEvent } from '@anton.bobrov/react-hooks';
import { useCallback, useRef } from 'react';
import { lerp } from 'vevet';
import { useAnimationFrame } from './useAnimationFrame';
export type TUseAnimationFrameSyncData = Record<string, number>;
export interface IUseAnimationFrameSyncProps<
T extends TUseAnimationFrameSyncData,
> {
/** The initial data for the animation frame, with properties to animate. */
data: T;
/** Callback function to be called on each update with the current interpolated values. */
onUpdate: (data: T) => void;
/** Callback function to be called on each update with the target values. */
onSet?: (data: T) => void;
/** The easing factor for the interpolation. Defaults to 0.1. */
ease?: number;
}
/**
* Custom React hook that synchronizes animation frame data using linear interpolation.
*
* This hook allows you to smoothly animate properties by specifying their initial values.
* It uses linear interpolation to transition between current and target values.
* The animation frame will automatically stop once all values reach their targets.
*
* @example
* const { set } = useAnimationFrameSync({
* data: { x: 0, y: 0 },
* onUpdate: ({ x, y }) => console.log(x, y),
* });
*
* set('x', 1);
* set('y', 0.5);
* set('y', 0.5, true); // instant change
*/
export function useAnimationFrameSync<T extends TUseAnimationFrameSyncData>({
data: initialData,
onUpdate: onUpdateProp,
onSet: onSetProp,
ease: easeProp = 0.1,
}: IUseAnimationFrameSyncProps<T>) {
const onUpdate = useEvent(onUpdateProp);
const onSet = useEvent(onSetProp);
const dataRef = useRef({
moment: { ...initialData },
target: { ...initialData },
});
const props = objectKeys(initialData);
const { moment, target } = dataRef.current;
const render = useEvent((ease: number) => {
props.forEach((prop) => {
// @ts-ignore
moment[prop] = lerp(moment[prop], target[prop], ease);
});
onUpdate(moment);
});
const { play, pause } = useAnimationFrame({
onFrame: ({ fpsMultiplier }) => {
const ease = easeProp * fpsMultiplier;
render(ease);
const undone = props.filter((prop) => moment[prop] !== target[prop]);
const isInterpolated = undone.length === 0;
if (isInterpolated) {
pause();
}
},
});
const set = useCallback(
(prop: keyof T, value: number, isInstant?: boolean) => {
// @ts-ignore
target[prop] = value;
// instant change
if (isInstant) {
// @ts-ignore
moment[prop] = value;
render(0);
} else {
// interpolated change
play();
}
onSet?.(target);
},
[moment, onSet, play, render, target],
);
const getMoment = useCallback(() => moment, [moment]);
return { set, getMoment };
}