Reputation: 251
I have a task where I need to place rectangle on area where I clicked on cavnas (PDF). I am using React and after I upload pdf file by using react-pdf module that file is being translated into canvas element. I want to remove previously drawn rectangle after I click multiple times so that rectangle is going to change place it is not going to be repeated on screen. What I tried so far is this:
After I choose pdf file that file is being translated into canvas and viewed on page using react-pdf module that I mentioned earlier
<Document
className={classes.pdf_document}
file={file}
onLoadSuccess={handleOnPdfLoad}
>
<Page
onClick={drawRectangle}
width={400}
pageNumber={currentPage}>
</Page>
</Document>
drawRectangle function is drawing red rectangle on clicked area
const setCoordinatesOnClick = (e) => {
const rect = e.target.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const marker = e.target.getContext("2d");
const drawRect = () => {
marker.beginPath();
marker.lineWidth = "3";
marker.strokeStyle = "red";
marker.strokeRect(x, y, 70, 50);
marker.stroke();
}
if (!rectDrawn) {
drawRect();
setRectDrawn(true);
} else {
marker.clearRect(0, 0, e.target.width, e.target.height);
drawRect();
}
}
I also have rectDrawn that is true or false
const [rectDrawn, setRectDrawn] = React.useState(false);
When marker.clearRect happens red rectangle reappears on newly clicked area but I loose all other pdf data on that canvas ( text and everything else ) it just becomes blank.
Upvotes: 4
Views: 1199
Reputation: 33721
Here's a self-contained and commented example demonstrating how to get a rectangle of ImageData
from a canvas, store it in React state, and then restore it to the canvas in-place:
I used TypeScript when creating the example, but I manually removed all of the type information in the snippet below in case it might be confusing to you (your question didn't indicate that you are using TypeScript). However, in case you're interested in the typed version, you can view it at this TS Playground link.
<div id="root"></div><script src="https://unpkg.com/[email protected]/umd/react.development.js"></script><script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script><script src="https://unpkg.com/@babel/[email protected]/babel.min.js"></script>
<script type="text/babel" data-type="module" data-presets="env,react">
const {useEffect, useRef, useState} = React;
/** This function is just for having an example image for this demo */
async function loadInitialImageData (ctx) {
const img = new Image();
img.crossOrigin = 'anonymous';
img.src = 'https://i.imgur.com/KeiVCph.jpg'; // 720px x 764px
img.addEventListener('load', () => ctx.drawImage(img, 0, 0, 360, 382));
}
// Reusable utility/helper functions:
/** For making sure the context isn't `null` when trying to access it */
function assertIsContext (ctx) {
if (!ctx) throw new Error('Canvas context not found');
}
/**
* Calculate left (x), and top (y) coordinates from a mouse click event
* on a `<canvas>` element. If `centered` is `true` (default), the center of
* the reactangle will be at the mouse click position, but if `centered` is
* `false`, the top left corner of the rect will be at the click position
*/
function getXYCoords (ev, {h = 0, w = 0, centered = true} = {}) {
const ctx = ev.target.getContext('2d');
assertIsContext(ctx);
const rect = ctx.canvas.getBoundingClientRect();
const scaleX = ctx.canvas.width / rect.width;
const scaleY = ctx.canvas.height / rect.height;
const x = (ev.clientX - rect.left - (centered ? (w / 2) : 0)) * scaleX;
const y = (ev.clientY - rect.top - (centered ? (h / 2) : 0)) * scaleY;
return [x, y];
}
/**
* Draw the actual rectangle outline.
* The stroke is always drawn on the **outside** of the rectangle:
* This is, unfortunately, not configurable.
*/
function strokeRect (ctx, options): void {
ctx.lineWidth = options.lineWidth;
ctx.strokeStyle = options.strokeStyle;
ctx.strokeRect(...options.dimensions);
}
/**
* Calculates dimensions of a rectangle including optional XY offset values.
* This is to accommodate for the fact that strokes are always drawn on the
* outside of a rectangle.
*/
function getOffsetRect (x, y, w, h, xOffset = 0, yOffset = 0) {
x -= xOffset;
y -= yOffset;
w += xOffset * 2;
h += yOffset * 2;
return [x, y, w, h];
}
/** Example component for this demo */
function Example () {
// This might be useful to you, but is only used here when initially loading the demo image
const canvasRef = useRef(null);
// This will hold a closure (function) that will restore the original image data.
// Initalize with empty function:
const [restoreImageData, setRestoreImageData] = useState(() => () => {});
// This one-time call to `useEffect` is just for having an example image for this demo
useEffect(() => {
const ctx = canvasRef.current?.getContext('2d') ?? null;
assertIsContext(ctx);
loadInitialImageData(ctx);
}, []);
// This is where all the magic happens:
const handleClick = (ev) => {
const ctx = ev.target.getContext('2d');
assertIsContext(ctx);
// You defined these width and height values statically in your question,
// but you could also store these in React state to use them dynamically:
const w = 70;
const h = 50;
// Use the helper function to get XY coordinates:
const [x, y] = getXYCoords(ev, {h, w});
// Again, these are static in your question, but could be in React state:
const lineWidth = 3;
const strokeRectOpts = {
lineWidth,
strokeStyle: 'red',
dimensions: [x, y, w, h],
};
// Use a helper function again to calculate the offset rectangle dimensions:
const expanded = getOffsetRect(x, y, w, h, lineWidth, lineWidth);
// Restore the previous image data from the offset rectangle
restoreImageData();
// Get the new image data from the offset rectangle:
const imageData = ctx.getImageData(...expanded);
// Use the image data in a closure which will restore it when invoked later,
// and put it into React state:
setRestoreImageData(() => () => ctx.putImageData(imageData, expanded[0], expanded[1]));
// Finally, draw the rectangle stroke:
strokeRect(ctx, strokeRectOpts);
};
return (
<div style={{border: '1px solid black', display: 'inline-block'}}>
<canvas
ref={canvasRef}
onClick={handleClick}
width="360"
height="382"
></canvas>
</div>
);
}
ReactDOM.render(<Example />, document.getElementById('root'));
</script>
Upvotes: 1