Удалить последний нарисованный объект с холста

#html #reactjs #canvas

#HTML #reactjs #холст

Вопрос:

У меня есть задача, в которой мне нужно поместить прямоугольник в область, где я нажал на cavnas (PDF). Я использую React, и после того, как я загружаю PDF-файл с помощью модуля react-pdf, этот файл переводится в элемент canvas. Я хочу удалить ранее нарисованный прямоугольник после того, как я щелкну несколько раз, чтобы прямоугольник поменял место, и он не будет повторяться на экране. То, что я пробовал до сих пор, это:

  1. После того, как я выбираю pdf-файл, этот файл переводится в canvas и просматривается на странице с помощью модуля react-pdf, о котором я упоминал ранее
                             <Document
                                className={classes.pdf_document}
                                file={file}
                                onLoadSuccess={handleOnPdfLoad}                      
                            >
                                <Page
                                    onClick={drawRectangle}
                                    width={400}
                                    pageNumber={currentPage}>
                                </Page>
                            </Document>
     
  2. Функция DrawRectangle рисует красный прямоугольник в области щелчка
     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. У меня также есть rectDrawn, который является истинным или ложным
     const [rectDrawn, setRectDrawn] = React.useState(false);
     
  4. Когда marker.clearRect происходит, красный прямоугольник снова появляется во вновь нажатой области, но я теряю все остальные данные pdf на этом холсте (текст и все остальное), он просто становится пустым.

Ответ №1:

Вот автономный и прокомментированный пример, демонстрирующий, как получить прямоугольник ImageData из холста, сохранить его в состоянии реакции, а затем восстановить его на холсте на месте:

При создании примера я использовал TypeScript, но я вручную удалил всю информацию о типе из приведенного ниже фрагмента на случай, если это может вас смутить (в вашем вопросе не указано, что вы используете TypeScript). Однако, если вас интересует типизированная версия, вы можете просмотреть ее по этой ссылке TS Playground.

 <div id="root"></div><script src="https://unpkg.com/react@17.0.2/umd/react.development.js"></script><script src="https://unpkg.com/react-dom@17.0.2/umd/react-dom.development.js"></script><script src="https://unpkg.com/@babel/standalone@7.16.4/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> 

Комментарии:

1. Спасибо за ваш ответ, но если вы сможете уточнить его немного подробнее, я был бы более чем благодарен. Заранее благодарю вас.

2. @mne_web_dev Я написал для вас рабочий пример с комментариями и добавил его в свой ответ.