Search code examples
javascripthtmlreactjsreact-hooksmatter.js

React won't draw anything in the canvas in useEffect


I building a react website with matter.js. I am using the useEffect hook to render stuff to the canvas with matter.js (I found most of this code here). However, when I try to draw anything else to the canvas, nothing appears. Everything matter.js-related works.

const scene = useRef()
const isDragging = useRef(false)
const engine = useRef(Engine.create())

useEffect(() => {
  const cw = 1100;
  const ch = 700;
  const render = Render.create({
    canvas: scene.current,
    engine: engine.current,
    options: {
      width: cw,
      height: ch,
      wireframes: false,
      background: 'transparent'
    }
  })

  console.log("gravity " + engine.current.gravity.y + "x : " + engine.current.gravity.x)

  let mouse = Mouse.create(render.canvas);
  let mouseConstraint = MouseConstraint.create(engine.current, {
    mouse: mouse,
    constraint: {
      render: {
        visible: false
      }
    }
  })
  render.mouse = mouse;

  World.add(engine.current.world, [
    Bodies.rectangle(cw / 2, 0, cw, 20, {
      isStatic: true,
      density: 1
    }),
    Bodies.rectangle(cw / 2, ch, cw, 20, {
      isStatic: true,
      density: 1
    }),
    Bodies.rectangle(0, ch / 2, 20, ch, {
      isStatic: true,
      density: 1
    }),
    Bodies.rectangle(cw, ch / 2, 20, ch, {
      isStatic: true,
      density: 1
    }),

    mouseConstraint,
  ])

  Runner.run(engine.current)
  Render.run(render)

  Events.on(mouseConstraint, "mousedown", function(event) {
    handleSelections(mouseConstraint.body)
  })
  Events.on(mouseConstraint, "startdrag", function(event) {
    isDragging.current = true
  })
  Events.on(mouseConstraint, "enddrag", function() {
    isDragging.current = false
  })
  Events.on(engine.current, 'afterUpdate', function() {
    countTen.current = countTen.current + 1;

    if (countTen.current == 10) {
      countTen.current = 0;

      if (selectedObjectRef.current != null) {

        setGraphingPos(selectedObjectRef.current.velocity.y * -1);

        setTicks(ticks + 1)
      }
    }
  })

  // ******************* This part doesn't work **************************
  scene.current.getContext('2d').beginPath();
  scene.current.getContext('2d').arc(100, 100, 20, 0, 2 * Math.PI, false);
  scene.current.getContext('2d').fillStyle = 'red';
  scene.current.getContext('2d').fill();
  // **********************************************************************
  return () => {
    Render.stop(render)
    World.clear(engine.current.world)
    Engine.clear(engine.current)
    render.canvas.remove()
    render.canvas = null
    render.context = null
    render.textures = {}
  }
}, [])
<canvas ref={scene} onClick={ handleMouseDown} className='main-canvas'></canvas>

Any kind of help is much appreciated!


Solution

  • Currently, you're drawing one time, up front, on the same canvas that MJS wipes per frame. So, you draw your circle, then MJS wipes the canvas immediately when it renders its first frame. You never attempt to draw again as the engine and renderer run onward.

    I see a few solutions (at least!). Which is most appropriate depends on your specific use case.

    1. Draw on the canvas on each frame inside the afterRender callback for the render object. This is the simplest and most direct solution from where you are now and I provide an example below.
    2. Add a second transparent canvas on top of the one you're providing to MJS. This is done with absolute positioning. Now you can do whatever you want in this overlay without interference from MJS.
    3. Run MJS headlessly, which gives you maximum control, as the MJS built-in renderer is mostly intended for protoyping (it's a physics engine, not a graphics engine). Once you're headless, you can render whatever you want, whenever you want, however you want. I have many demos of headless rendering in my other answers, both to the plain DOM and with React and p5.js.

    As an aside, this doesn't appear related to React or useEffect in any way; the same problem would arise even if you were injecting a plain canvas in vanilla JS.

    Also, keep in mind that whatever you draw is totally unrelated to MJS other than the fact that it happens to be on the same canvas. Don't expect physics to work on it, unless you're using coordinates from a body MJS knows about.

    Although you haven't provided a full component, here's a minimal proof-of-concept of the afterRender approach mentioned above:

    const {useEffect, useRef} = React;
    const {Bodies, Engine, Events, Render, Runner, Composite} = Matter;
    
    const Scene = () => {
      const canvasRef = useRef();
    
      useEffect(() => {
        const cw = 200;
        const ch = 200;
        const engine = Engine.create();
        const ctx = canvasRef.current.getContext("2d");
        const render = Render.create({
          canvas: canvasRef.current,
          engine,
          options: {
            width: cw,
            height: ch,
            wireframes: false,
            background: "transparent",
          }
        });
        const handleAfterRender = () => {
          const x = Math.cos(Date.now() / 300) * 80 + 100;
          const y = Math.sin(Date.now() / 299) * 80 + 100;
          ctx.beginPath();
          ctx.arc(x, y, 20, 0, 2 * Math.PI);
          ctx.fillStyle = "red";
          ctx.fill();
        };
        Events.on(render, "afterRender", handleAfterRender);
        Composite.add(engine.world, [
          Bodies.rectangle(cw / 2, 0, cw, 20, {
            isStatic: true,
            density: 1,
          }),
          Bodies.rectangle(cw / 2, ch, cw, 20, {
            isStatic: true,
            density: 1,
          }),
          Bodies.rectangle(0, ch / 2, 20, ch, {
            isStatic: true,
            density: 1,
          }),
          Bodies.rectangle(cw, ch / 2, 20, ch, {
            isStatic: true,
            density: 1,
          }),
        ]);
        Runner.run(engine);
        Render.run(render);
    
        return () => {
          Events.off("afterRender", handleAfterRender);
          Render.stop(render);
          Composite.clear(engine.world);
          Engine.clear(engine);
          render.canvas.remove();
          render.canvas = null;
          render.context = null;
          render.textures = {};
        };
      }, []);
      return <canvas ref={canvasRef}></canvas>;
    };
    
    const root = document.querySelector("#app");
    ReactDOM.createRoot(root).render(<Scene />);
    <script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
    <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.20.0/matter.min.js"></script>
    <div id="app"></div>