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}
onm ouseDown={handleMouseDown}
onm ouseUp={handleMouseUp}
onm ouseMove={handleMouseMove}
>
Canvas
</canvas>
);
};
export default App;
When I run this code, it produces this output when I click and drag mouse:
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.
CodePudding user response:
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}
onm ouseDown={handleMouseDown}
onm ouseUp={handleMouseUp}
onm ouseMove={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.