mne_web_dev
mne_web_dev

Reputation: 251

Remove last drawn object from canvas

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:

  1. 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>
    
  2. 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();
        }
    }
    
  3. I also have rectDrawn that is true or false

    const [rectDrawn, setRectDrawn] = React.useState(false);
    
  4. 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

Answers (1)

jsejcksn
jsejcksn

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

Related Questions