Home > front end >  How to draw a line in React JS with mouse events
How to draw a line in React JS with mouse events

Time:11-14

I am trying to make a canvas web-app with Vite React (fairly new with React and JS in general). Currently I am using an onDrag event to draw a line in realtime (as the mouse moves), and it works except I do not want the element to be draggable. It is the only solution I have found that is able to track the mouse's location while having the mouse clicked.

As of now, it draws a circle on each location of the mouse while it is dragging. When I drag the second time it picks up the div as it should which is not my intention. I am looking for a more practical way to do this (probably some mouse event I am overlooking?).

The Div:

<div className="canvas" draggable onDrag={(handleOnDrag)}>
   {items.map((item) => (
   <div className="test" style={{
     left: item.x-5   "px",
     top: item.y-5   "px",
   }}
   ></div>
  ))}
</div>

Drag Function:

  function handleOnDrag(e){
    const {clientX, clientY} = e;
    setItems([...items, {x:clientX, y:clientY}])
  }

Picture of Issue

CodePudding user response:

Mouse Events

Colloquially, pressing and moving the mouse can be considered "dragging".

But in web dev, "dragging" refers to the first part of "drag-and-drop" where you drag an object. Therefore, declaring your element as draggable means you can drag the element itself.

Because there is no event type specifically for your usecase, you need to implement it yourself.

As the example on MDN demonstrates, it can be implemented with the events mousedown, mousemove and mouseup. A simple flag for when the button is pressed to draw when moving works:

// When true, moving the mouse draws on the canvas
let isDrawing = false;
let x = 0;
let y = 0;

const myPics = document.getElementById('myPics');
const context = myPics.getContext('2d');

// event.offsetX, event.offsetY gives the (x,y) offset from the edge of the canvas.

// Add the event listeners for mousedown, mousemove, and mouseup
myPics.addEventListener('mousedown', (e) => {
  x = e.offsetX;
  y = e.offsetY;
  isDrawing = true;
});

myPics.addEventListener('mousemove', (e) => {
  if (isDrawing) {
    drawLine(context, x, y, e.offsetX, e.offsetY);
    x = e.offsetX;
    y = e.offsetY;
  }
});

window.addEventListener('mouseup', (e) => {
  if (isDrawing) {
    drawLine(context, x, y, e.offsetX, e.offsetY);
    x = 0;
    y = 0;
    isDrawing = false;
  }
});

function drawLine(context, x1, y1, x2, y2) {
  context.beginPath();
  context.strokeStyle = 'black';
  context.lineWidth = 1;
  context.moveTo(x1, y1);
  context.lineTo(x2, y2);
  context.stroke();
  context.closePath();
}
canvas {
  border: 1px solid black;
  width: 560px;
  height: 360px;
}
<h1>Drawing with mouse events</h1>
<canvas id="myPics" width="560" height="360"></canvas>

Drawing

Let's call "drawing" the manipulation of an actual image instead of a "lookalike" image.

That would mean that your code only gives off the illusion of drawing by mocking a "brush stroke" via elements. This also means that the longer we "draw", the more elements are added to our document:

// Your React drawing app
function App() {
  const [items, setItems] = React.useState([]);
  
  const handleDrag = e => {
    const {clientX, clientY} = e;
    setItems([...items, {x:clientX, y:clientY}]);
  };

  return (
    <div className="canvas" draggable onDrag={handleDrag}>
      {items.map(item => (
        <div className="test"
          style={{ left: `${item.x-5}px`, top: `${item.y-5}px` }}></div>
      ))}
    </div>
  );
}

const root = ReactDOM.createRoot(document.getElementById("app"));
root.render(<App></App>);

// Element counter
const elCount = document.getElementById("element-count");
let count = countElementsOf(document.documentElement);
function countElementsOf(node) {
  return 1   Array.from(node.children)
    .reduce((count, n) =>
      count   countElementsOf(n), 0);
}

const options = { childList: true, subtree: true };
const mutationObserver = new MutationObserver((mutations) => {
  mutations.forEach(m => {
    if (m.type !== "childList") return;
    count  = m.addedNodes.length;
    count -= m.removedNodes.length;
    elCount.value = count;
  });
});
mutationObserver.observe(document.documentElement, options);
/*Any styling for demonstration purposes*/
.canvas {
  position: relative;
  width: 560px;
  height: 360px;
  border: 1px solid black;
}

.test {
  position: absolute;
  border-radius: 9999px;
  width: 10px;
  height: 10px;
  background-color: black;
}
<!--React libraries-->
<script src="https://unpkg.com/react/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom/umd/react-dom.development.js"></script>

<label for="element-count">
  HTML elements on this page: <input id="element-count" type=number value=0 readonly>
</label>

<div id="app"></div> <!--React app-->

As mentioned, the amount of elements on your page increases drastically while "drawing", which eventually impacts performance.

Also note that the individual dots (elements) are not connected, unlike a brush stroke.

We can solve the issues (increasing elements, disconnected "strokes") with the Canvas API, which is intended for drawing on the <canvas> element.

Common ways to draw via the Canvas API are WebGL or with its path-like methods of the 2D-Context. MDN's example uses the path-like approach, which I also personally recommend for beginners.

Refer to the copied snippet from MDN in the first section.

An additional benefit of using <canvas> is: Since it's effectively an image, you can actually copy or save it like any other image.

  • Related