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

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?

Related

how to access image data on React-three-fiber

I would like to take image data (RGBA) from react-three-fiber
like this code on HTML
var myCanvas = document.createElement("canvas");
myCanvas.width = texture.image.width;
myCanvas.height = texture.image.height;
var myCanvasContext = myCanvas.getContext("2d"); // Get canvas 2d context
myCanvasContext.drawImage(texture.image, 0, 0); // Draw the texture
var texels = myCanvasContext.getImageData(0,0, width, height); // Read the texels/pixels back
However, I cannot access Canvas to drawImage or getImageData.
const ref = React.useRef();
React.useEffect(() => {
if (ref) {
//error
ref.current.getContext("2d");
}
}, []);
<Canvas
dpr={[1, 1.5]} //pixelRatio
ref={ref}
>
<Particles />
</Canvas>
and I tried to use useThree. there is no method to get imageData
const {
gl,
} = useThree();
lastly, I tired to get an image using useLoader as a texture
const map = useLoader(TextureLoader, "/scroll_images/img1.jpg");
so, how can I get the imageData?
In your first example, try useLayoutEffect instead of useEffect. Refs are not actually populated until the first DOM render happened. Unlike useEffect, useLayoutEffect waits until this occurred.
React.useLayoutEffect(() => {
ref.current.getContext("2d"); // <-- hopefully doesnt error
}, []);
In your second example I think its because you need to pass gl={{ preserveDrawingBuffer: true }} prop to your Canvas. Then you can use gl.domElement.toDataURL('image/png') or similar.

implmenting Scorll postion in reactwithout rerending the whole component?

I want to be able to listen to scroll event to get the current value, and if the value reaches a certain threshold render a div the current logic works well with useState but it is rendering every render.
useRef however doesn't seem do be doing what is should, is there any solution to this ? will callback solve this ? if possible could you refactor to a better logic.
const scrollRef = useRef<number>(0);
useEffect(() => {
const listenToScroll = () => {
const winScroll = document.body.scrollTop || document.documentElement.scrollTop;
const height = document.documentElement.scrollHeight - document.documentElement.clientHeight;
const scrolled = winScroll / height;
scrollRef.current = scrolled;
};
const fn = window.addEventListener('scroll', listenToScroll);
return fn;
}, []);

Why does D3 Brush Not Clear Multiple Graphs in React App

I have multiple graphs being displayed on the same page in different elements within the same React Component. Each graph uses the same bit of code, but only the 1st graph clears the Brush after a selection. All graphs work fine in a regular js file without React.
const plotArea = async (props: any) => {
...
// Handler for the end of a brush event from D3.
const brushEnded = (event: any) => {
const s = event.selection;
// Consume the brush action
if (s) {
d3.select('.brush').call(brush.move, null);
}
}
// Create a brush for selecting regions to zoom on.
const brush: any = d3
.brushX()
.extent([
[0, 0],
[width, height - 1],
])
.on('end', brushEnded);
// Zoom brush
svg.append('g').attr('class', 'brush').call(brush);
}
useEffect(() => {
// plotArea() occurs for each graph, there are multiple graphs
plotArea(...);
...
}, []);
When d3 runs the selection in d3.select('.brush').call(brush.move, null);, it does not limit the search to the component. It will search the whole document for a .brush element, and will stop as soon as it finds the first.
As a quick fix, you can save the specific brush of the component, so that you already have the reference and don't need to run a d3.select to get it back:
const plotArea = async (props: any) => {
...
// Create brushElement first
const brushElement = svg.append('g').attr('class', 'brush');
// Handler for the end of a brush event from D3.
const brushEnded = (event: any) => {
const s = event.selection;
// Consume the brush action
if (s) {
brushElement.call(brush.move, null); // Now uses the variable
}
}
// Create a brush for selecting regions to zoom on.
const brush: any = d3
.brushX()
.extent([
[0, 0],
[width, height - 1],
])
.on('end', brushEnded);
brushElement.call(brush);
}

React, insert/append/render existing <HTMLCanvasElement>

In my <App> Context, I have a canvas element (#offScreen) that is already hooked in the requestAnimationFrame loop and appropriately drawing to that canvas, verified by .captureStream to a <video> element.
In my <Canvas> react component, I have the following code (which works, but seems clunky/not the best way to copy an offscreen canvas to the DOM):
NOTE: master is the data object for the <App> Context.
function Canvas({ master, ...rest } = {}) {
const canvasRef = useRef(master.canvas);
const draw = ctx => {
ctx.drawImage(master.canvas, 0, 0);
};
useEffect(() => {
const canvas = canvasRef.current;
const ctx = canvas.getContext("2d");
let animationFrameId;
const render = () => {
draw(ctx)
animationFrameId = window.requestAnimationFrame(render)
}
render();
return () => {
window.cancelAnimationFrame(animationFrameId);
}
}, [ draw ]);
return (
<canvas
ref={ canvasRef }
onMouseDown={ e => console.log(master, e) }
/>
);
};
Edited for clarity based on comments
In my attempts to render the master.canvas directly (e.g. return master.canvas; in <Canvas>), I get some variation of the error "Objects cannot be React children" or I get [object HTMLCanvasElement] verbatim on the screen.
It feels redundant to take the #offScreen canvas and repaint it each frame. Is there, instead, a way to insert or append #offScreen into <Canvas>, so that react is just directly utilizing #offScreen without having to repaint it into the react component canvas via the ref?
Specific Issue: Functionally, I'm rendering a canvas twice--once off screen and once in the react component. How do I (replace/append?) the component's <canvas> element with the offscreen canvas (#offScreen), instead of repainting it like I'm doing now?
For anyone interested, this was actually fairly straightforward, as I overcomplicated it substantially.
export function Canvas({ canvas, ...rest }) {
const container = useRef(null);
useEffect(() => {
container.current.innerHTML = "";
container.current.append(canvas);
}, [ container, canvas ]);
return (
<div ref={ container } />
)
}

How to use DOMRect with useEffect in React?

I am trying to get the x and y of an element in React. I can do it just fine using DOMRect, but not in the first render. That's how my code is right now:
const Circle: React.FC<Props> = ({ children }: Props) => {
const context = useContext(ShuffleMatchContext);
const circle: React.RefObject<HTMLDivElement> = useRef(null);
const { width, height } = useWindowDimensions();
useEffect(() => {
const rect = circle.current?.getBoundingClientRect();
context.setQuestionPosition({
x: rect!.x,
y: rect!.y,
});
}, [width, height]);
return (
<>
<Component
ref={circle}
>
<>{children}</>
</Component>
</>
);
};
export default Circle;
The problem is that on the first render, domRect returns 0 to everything inside it. I assume this behavior happens because, in the first render, you don't have all parent components ready yet. I used a hook called "useWindowDimensions," and in fact, when you resize the screen, domRect returns the expected values. Can anyone help?
You should use useLayoutEffect(). It allows you to get the correct DOM-related values (i.e. the dimensions of a specific element) since it fires synchronously after all DOM mutations.
useLayoutEffect(() => {
const rect = circle.current?.getBoundingClientRect();
context.setQuestionPosition({
x: rect!.x,
y: rect!.y,
});
}, [width, height]);

Resources