Search code examples
javascriptreactjshtml5-canvas

HTML canvas keep on rendering line, even after I cleared it


I am trying my hand on building a drawing app in React using HTML Canvas element.

import React, { useEffect, useLayoutEffect, useRef, useState } from "react";
///*[EN ROUGH]*/import rough from 'roughjs';

///*[EN ROUGH]*/const generator = rough.generator();

const createElement = (x1, y1, x2, y2, type) => {
  return { x1, y1, x2, y2, type };
};

const drawElement = (context, element) => {
  if (element.type === "connect") {
     context.moveTo(element.x1, element.y1);
     context.lineTo(element.x2, element.y2);
     context.stroke();
    ///*[EN ROUGH]*/const line = generator.line(element.x1, element.y1, element.x2, element.y2);
    ///*[EN ROUGH]*/context.draw(line);

  }
};

function App() {
  const [elements, setElements] = useState([]);
  const [drawing, setDrawing] = useState(false);

  useLayoutEffect(() => {
    const canvas = document.getElementById("canvas");
    const context = canvas.getContext("2d");

    context.clearRect(0, 0, canvas.width, canvas.height);
    ///*[EN ROUGH]*/const context = rough.canvas(canvas);

    //redraw all the components on re-render
    elements.forEach((element) => {
      drawElement(context, element);
    });
  });

  const handleMouseDown = (event) => {
    setDrawing(true);
    const { clientX, clientY } = event;

    const element = createElement(
      clientX,
      clientY,
      clientX,
      clientY,
      "connect"
    );
    setElements((prevState) => [...prevState, element]);
  };

  const handleMouseMove = (event) => {
    if (!drawing) return;
    const { clientX, clientY } = event;
    //index of the last element that was selected
    const lastElemIndex = elements.length - 1;
    const { x1, y1 } = elements[lastElemIndex];
    const updatedElement = createElement(x1, y1, clientX, clientY, "connect");

    //overwrite the previous element with the new x2, y2 coordinates of mouse move
    const elementsCopy = [...elements];
    elementsCopy[lastElemIndex] = updatedElement;
    setElements(elementsCopy);
  };

  const handleMouseUp = () => {
    setDrawing(false);
  };

  return (
    <canvas
      id="canvas"
      width={window.innerWidth}
      height={window.innerHeight}
      onMouseDown={handleMouseDown}
      onMouseUp={handleMouseUp}
      onMouseMove={handleMouseMove}
    >
      Canvas
    </canvas>
  );
};

export default App;

When I run this code, it produces this output when I click and drag mouse:

enter image description here

When I enable RoughJS to draw instead of normal canvas, the line works fine. RoughJS code can be enabled by removing comments EN ROUGH. I am totally lost why it's happening.


Solution

  • Try context.beginPath() to start a new path, something Rough is probably doing on your behalf when you use it:

      if (element.type === "connect") {
        context.beginPath(); // <-- Added
        // ...
    

    Full example (click and drag to create a line--the white screen may make it hard to tell it's running):

    const { useEffect, useLayoutEffect, useRef, useState } = React;
    ///*[EN ROUGH]*/import rough from 'roughjs';
    
    ///*[EN ROUGH]*/const generator = rough.generator();
    
    const createElement = (x1, y1, x2, y2, type) => {
      return { x1, y1, x2, y2, type };
    };
    
    const drawElement = (context, element) => {
      if (element.type === "connect") {
        context.beginPath(); // <-- Added
        context.moveTo(element.x1, element.y1);
        context.lineTo(element.x2, element.y2);
        context.stroke();
        ///*[EN ROUGH]*/const line = generator.line(element.x1, element.y1, element.x2, element.y2);
        ///*[EN ROUGH]*/context.draw(line);
    
      }
    };
    
    function App() {
      const [elements, setElements] = useState([]);
      const [drawing, setDrawing] = useState(false);
    
      useLayoutEffect(() => {
        const canvas = document.getElementById("canvas");
        const context = canvas.getContext("2d");
        context.clearRect(0, 0, canvas.width, canvas.height);
        ///*[EN ROUGH]*/const context = rough.canvas(canvas);
    
        //redraw all the components on re-render
        elements.forEach((element) => {
          drawElement(context, element);
        });
      });
    
      const handleMouseDown = (event) => {
        setDrawing(true);
        const { clientX, clientY } = event;
    
        const element = createElement(
          clientX,
          clientY,
          clientX,
          clientY,
          "connect"
        );
        setElements((prevState) => [...prevState, element]);
      };
    
      const handleMouseMove = (event) => {
        if (!drawing) return;
        const { clientX, clientY } = event;
        //index of the last element that was selected
        const lastElemIndex = elements.length - 1;
        const { x1, y1 } = elements[lastElemIndex];
        const updatedElement = createElement(x1, y1, clientX, clientY, "connect");
    
        //overwrite the previous element with the new x2, y2 coordinates of mouse move
        setElements(prev => {
          const elementsCopy = [...prev];
          elementsCopy[lastElemIndex] = updatedElement;
          return elementsCopy;
        });
      };
    
      const handleMouseUp = () => {
        setDrawing(false);
      };
    
      return (
        <canvas
          id="canvas"
          width={window.innerWidth}
          height={window.innerHeight}
          onMouseDown={handleMouseDown}
          onMouseUp={handleMouseUp}
          onMouseMove={handleMouseMove}
        >
          The canvas element is unsupported by this browser
        </canvas>
      );
    };
    
    ReactDOM.createRoot(document.querySelector("#app")).render(<App />);
    <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>
    <div id="app"></div>

    Note I've used the callback version of setElements when reading the previous state to compute the new state.

    Also, the text inside the <canvas> tags is fallback text that's displayed when the canvas isn't supported by the browser, so I've picked something a bit more descriptive.