import { useRef, useCallback } from "react"; import { useWillUnmount, useForceUpdate } from "hooks"; type AnimationFrameData = { fps: number; deltaTime: number; }; type OnFrameCallback = (frameData: AnimationFrameData) => void; export type UseWindowAnimationFrameOptions = { onFrameInterval?: number; rerenderOnAnimationState?: boolean; }; export type UseWindowAnimationFrameReturnType = { start: VoidFunction; stop: VoidFunction; trigger: VoidFunction; isActive: boolean; }; /** * * @kind 15-Window */ export const useWindowAnimationFrame = ( onFrame: OnFrameCallback, options: UseWindowAnimationFrameOptions = {}, ): UseWindowAnimationFrameReturnType => { const { onFrameInterval = 0, rerenderOnAnimationState = false } = options; const frameIntervalRef = useRef(onFrameInterval); frameIntervalRef.current = onFrameInterval; const onFrameRef = useRef(onFrame); onFrameRef.current = onFrame; const rerender = useForceUpdate(); const intervalStartTimeRef = useRef(0); const animationFrameIdRef = useRef(0); const paintCountRef = useRef(0); const startTimeRef = useRef(0); const thenRef = useRef(0); const deltaTimeRef = useRef(0); const fpsRef = useRef(0); const handleRerenderOnState = () => rerenderOnAnimationState && rerender(); const handleResetAnimationMetaInfo = useCallback(() => { paintCountRef.current = 0; startTimeRef.current = 0; intervalStartTimeRef.current = 0; thenRef.current = 0; deltaTimeRef.current = 0; fpsRef.current = 0; }, []); const handleStopAnimation = () => { const hasAnimation = !!animationFrameIdRef.current; cancelAnimationFrame(animationFrameIdRef.current); animationFrameIdRef.current = 0; if (hasAnimation) handleRerenderOnState(); }; const animate = (now: number) => { const t = now * 0.001; if (!thenRef.current) thenRef.current = t; deltaTimeRef.current = t - thenRef.current; thenRef.current = t; if (!intervalStartTimeRef.current) intervalStartTimeRef.current = t; if (!startTimeRef.current) startTimeRef.current = t; const elapsed = t - startTimeRef.current; paintCountRef.current += 1; const fps = paintCountRef.current / elapsed; fpsRef.current = Number.isFinite(fps) ? Number(fps.toFixed(3)) : 0; const frameData: AnimationFrameData = { fps: fpsRef.current, deltaTime: deltaTimeRef.current, }; if (frameIntervalRef.current) { const diffMs = (now - intervalStartTimeRef.current) * 1000; if (diffMs >= frameIntervalRef.current) { intervalStartTimeRef.current = now; onFrameRef.current?.(frameData); } } else { onFrameRef.current?.(frameData); } animationFrameIdRef.current = requestAnimationFrame(animate); }; const handleStart = () => { if (animationFrameIdRef.current) return; window.addEventListener("blur", handleResetAnimationMetaInfo); animationFrameIdRef.current = requestAnimationFrame(animate); handleRerenderOnState(); }; const handleStop = () => { window.removeEventListener("blur", handleResetAnimationMetaInfo); handleResetAnimationMetaInfo(); handleStopAnimation(); }; const handleTrigger = () => { if (animationFrameIdRef.current) { handleStop(); } else { handleStart(); } }; useWillUnmount(handleStop); return { start: handleStart, stop: handleStop, trigger: handleTrigger, isActive: !!animationFrameIdRef.current, }; };