'Performant way of getting mouse position every frame for canvas animation in React?

I have an animated background using canvas and requestAnimationFrame in my React app and I am trying to have its moving particles interact with the mouse pointer, but every solution I try ranges from significantly slowing down the animation the moment I start moving the mouse to pretty much crashing the browser.

The structure of the animated background component goes something like this:

<BackgroundParentComponent> // Gets mounted only once
    <Canvas> // Reutilizable canvas, updates every frame.
        // v - dozens of moving particles (just canvas drawing logic, no JSX return).
        // Each particle calculates its next frame updated state every frame.          
        {particlesArray.map(particle => <Particle/>} 
    <Canvas/>
<BackgroundParentComponent />

I have tried moving the event listeners to every level of the component structure, calling them with a custom hook with an useRef to hold the value without rerendering, throttling the mouse event listener so that it does not fire that often... nothing seems to help. This is my custom hook right now:

const useMousePosition = () => {
  const mousePosition = useRef({ x: null, y: null });

  useEffect(() => {
    window.addEventListener('mousemove', throttle(200, (event) => {
      mousePosition.current = { x: event.x, y: event.y };
    }))

  });

  useEffect(() => {
    window.addEventListener('mouseout', throttle(500, () => {
      mousePosition.current = { x: null, y: null };
    }));
  });
  return mousePosition.current;
}

const throttle = (delay: number, fn: (...args: any[]) => void) => {
  let shouldWait = false;
  return (...args: any[]) => {
    if (shouldWait) return;
    fn(...args);
    shouldWait = true;
    setTimeout(() => shouldWait = false, delay);
    return;
    // return fn(...args);
  }
}

For reference, my canvas component responsible of the animation looks roughly like this:

const AnimatedCanvas = ({ children, dimensions }) => {
  const canvasRef = useRef(null);
  const [renderingContext, setRenderingContext] = useState(null);
  const [frameCount, setFrameCount] = useState(0);

  // Initialize Canvas
  useEffect(() => {
    if (!canvasRef.current) return;
    const canvas = canvasRef.current;
    canvas.width = dimensions.width;
    canvas.height = dimensions.height;
    const canvas2DContext = canvas.getContext('2d');
    setRenderingContext(canvas2DContext);
  }, [dimensions]);

  // make component re-render every frame
  useEffect(() => {
    const frameId = requestAnimationFrame(() => {
      setFrameCount(frameCount + 1);
    });
    return () => {cancelAnimationFrame(frameId)};
  }, [frameCount, setFrameCount]);

  // clear canvas with each render to erase previous frame
  if (renderingContext !== null) {
    renderingContext.clearRect(0, 0, dimensions.width, dimensions.height);
  }

  return (
    <Canvas2dContext.Provider value={renderingContext}>
      <FrameContext.Provider value={frameCount}>
        <canvas ref={canvasRef}>
          {children}
        </canvas>
      </FrameContext.Provider>
    </Canvas2dContext.Provider>
  );
};

The mapped <Particle/> components are are fed to the above canvas component as children:

const Particle = (props) => {
  const canvas = useContext(Canvas2dContext);
  useContext(FrameContext); // only here to force the  force that the particle re-render each frame after the canvas is cleared.
  // lots of state calculating logic here 
  // This is where I need to know mouse position every (or every few) frames in order to modify each particle's behaviour when near the pointer.
  canvas.beginPath();
  // canvas drawing logic

  return null;
}

Just to clarify, the animation is always moving regardless of the mouse being idle, I've seen other solutions that only work for animations triggered exclusively by mouse movement.

Is there any performant way of accessing the mouse position each frame in the Particle mapped components without choking the browser? Is there a better way of handling this type of interactive animation with React?



Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source