简体   繁体   English

Reactjs 中的 Canvas Freehand 绘图撤消和重做功能

[英]Canvas Freehand drawing Undo and Redo functionality in Reactjs

After my attempt to creating a Freehand drawing using HTML5 canvas implemented using React, I want to proceed to Add an undo and redo functionality onclick of the undo and redo button respectively.在我尝试使用使用 React 实现的 HTML5 画布创建手绘图后,我想继续添加撤消和重做功能,分别单击撤消和重做按钮。 I'll be grateful for any help rendered.我将不胜感激所提供的任何帮助。

function App(props) {
    const canvasRef = useRef(null);
    const contextRef = useRef(null);
    const [isDrawing, setIsDrawing] = useState(false);

    useEffect(() => {
        const canvas = canvasRef.current;
        canvas.width = window.innerWidth * 2;
        canvas.height = window.innerHeight * 2;
        canvas.style.width = `${window.innerWidth}px`;
        canvas.style.height = `${window.innerHeight}px`;

        const context = canvas.getContext('2d');
        context.scale(2, 2);
        context.lineCap = 'round';
        context.strokeStyle = 'black';
        context.lineWidth = 5;
        contextRef.current = context;
    }, []);

    const startDrawing = ({ nativeEvent }) => {
        const { offsetX, offsetY } = nativeEvent;
        contextRef.current.beginPath();
        contextRef.current.moveTo(offsetX, offsetY);
        setIsDrawing(true);
    };

    const finishDrawing = () => {
        contextRef.current.closePath();
        setIsDrawing(false);
    };

    const draw = ({ nativeEvent }) => {
        if (!isDrawing) {
            return;
        }
        const { offsetX, offsetY } = nativeEvent;
        contextRef.current.lineTo(offsetX, offsetY);
        contextRef.current.stroke();
    };

    return <canvas onMouseDown={startDrawing} onMouseUp={finishDrawing} onMouseMove={draw} ref={canvasRef} />;
   
}

Here is the simplest solution with variables.这是最简单的变量解决方案。 Code sandbox solution to work on live https://codesandbox.io/s/suspicious-breeze-lmlcq .代码沙箱解决方案可用于实时https://codesandbox.io/s/suspicious-breeze-lmlcq

import React, { useEffect, useRef, useState } from "react";
import "./styles.css";

function App(props) {
  const canvasRef = useRef(null);
  const contextRef = useRef(null);
  const [undoSteps, setUndoSteps] = useState({});
  const [redoStep, setRedoStep] = useState({});

  const [undo, setUndo] = useState(0);
  const [redo, setRedo] = useState(0);
  const [isDrawing, setIsDrawing] = useState(false);

  useEffect(() => {
    const canvas = canvasRef.current;
    canvas.width = window.innerWidth * 2;
    canvas.height = window.innerHeight * 2;
    canvas.style.width = `${window.innerWidth}px`;
    canvas.style.height = `${window.innerHeight}px`;

    const context = canvas.getContext("2d");
    context.scale(2, 2);
    context.lineCap = "round";
    context.strokeStyle = "black";
    context.lineWidth = 5;
    contextRef.current = context;
  }, []);

  const startDrawing = ({ nativeEvent }) => {
    const { offsetX, offsetY } = nativeEvent;

    contextRef.current.beginPath();
    contextRef.current.moveTo(offsetX, offsetY);
    const temp = {
      ...undoSteps,
      [undo + 1]: []
    };
    temp[undo + 1].push({ offsetX, offsetY });
    setUndoSteps(temp);
    setUndo(undo + 1);
    setIsDrawing(true);
  };

  const finishDrawing = () => {
    contextRef.current.closePath();
    setIsDrawing(false);
  };

  const draw = ({ nativeEvent }) => {
    if (!isDrawing) {
      return;
    }
    const { offsetX, offsetY } = nativeEvent;
    contextRef.current.lineTo(offsetX, offsetY);
    contextRef.current.stroke();
    const temp = {
      ...undoSteps
    };
    temp[undo].push({ offsetX, offsetY });
    setUndoSteps(temp);
  };

  const undoLastOperation = () => {
    if (undo > 0) {
      const data = undoSteps[undo];
      contextRef.current.strokeStyle = "white";
      contextRef.current.beginPath();
      contextRef.current.lineWidth = 5;
      contextRef.current.moveTo(data[0].offsetX, data[0].offsetY);
      data.forEach((item, index) => {
        if (index !== 0) {
          contextRef.current.lineTo(item.offsetX, item.offsetY);
          contextRef.current.stroke();
        }
      });
      contextRef.current.closePath();
      contextRef.current.strokeStyle = "black";
      const temp = {
        ...undoSteps,
        [undo]: []
      };
      const te = {
        ...redoStep,
        [redo + 1]: [...data]
      };
      setUndo(undo - 1);
      setRedo(redo + 1);
      setRedoStep(te);
      setUndoSteps(temp);
    }
  };

  const redoLastOperation = () => {
    if (redo > 0) {
      const data = redoStep[redo];
      contextRef.current.strokeStyle = "black";
      contextRef.current.beginPath();
      contextRef.current.lineWidth = 5;
      contextRef.current.moveTo(data[0].offsetX, data[0].offsetY);
      data.forEach((item, index) => {
        if (index !== 0) {
          contextRef.current.lineTo(item.offsetX, item.offsetY);
          contextRef.current.stroke();
        }
      });
      contextRef.current.closePath();
      const temp = {
        ...redoStep,
        [redo]: []
      };
      setUndo(undo + 1);
      setRedo(redo - 1);
      setRedoStep(temp);
      setUndoSteps({
        ...undoSteps,
        [undo + 1]: [...data]
      });
    }
  };

  return (
    <>
      <p>check</p>
      <button type="button"  disabled={ undo === 0} onClick={undoLastOperation}>
        Undo
      </button>
      &nbsp;
      <button type="button"  disabled={ redo === 0} onClick={redoLastOperation}>
        Redo
      </button>
      <canvas
        onMouseDown={startDrawing}
        onMouseUp={finishDrawing}
        onMouseMove={draw}
        ref={canvasRef}
      ></canvas>
    </>
  );
}

export default App;

Options选项

You have several options.您有多种选择。

  • A Save all points used to render each stroke in a buffer (array) after on mouse up. A在鼠标抬起后,将用于渲染每个笔划的所有点保存在缓冲区(数组)中。 To undo clear the canvas and redraw all strokes up to the appropriate undo position.撤消清除画布并将所有笔画重绘到适当的撤消位置。 To redo just draw the next stroke in the undo buffer.要重做,只需在撤消缓冲区中绘制下一个笔划。

    Note This approach requires an infinite (well larger than all possible strokes) undo buffer or it will not work.注意此方法需要无限(远大于所有可能笔划)的撤消缓冲区,否则将无法工作。

  • B On mouse up save the canvas pixels and store in a buffer. B在鼠标向上保存画布像素并存储在缓冲区中。 Do not use getImageData as the buffer is uncompressed and will quickly consume a lot of memory.不要使用getImageData,因为缓冲区是未压缩的,会很快消耗大量内存。 Rather store the pixel data as a blob or DataURL .而是将像素数据存储为blobDataURL The default image format is PNG which is lossless and compressed and thus greatly reduces the RAM needed.默认图像格式是 PNG,它是无损压缩的,因此大大减少了所需的 RAM。 To undo / redo, clear the canvas, create an image and set the source to the blob or dataURL at the appropriate undo position.要撤消/重做,请清除画布,创建图像并将源设置为适当撤消位置的 blob 或 dataURL。 When the image has loaded draw it to the canvas.加载图像后,将其绘制到画布上。

    Note that blobs must be revoked and as such the undo buffer must ensure any deleted references are revoked before you lose the reference.请注意,必须撤销 blob,因此撤消缓冲区必须确保在丢失引用之前撤销所有已删除的引用。

  • C A combination of the two methods above. C以上两种方法的结合。 Save strokes and every so often save the pixels.保存笔画,每隔一段时间保存像素。

Simple undo buffer object简单的撤销缓冲对象

You can implement a generic undo buffer object that will store any data.您可以实现一个通用的撤消缓冲区对象来存储任何数据。

The undo buffer is independent of react's state撤消缓冲区独立于反应的状态

The example snippet shows how it is used.示例代码段显示了它是如何使用的。

Note That the undo function takes the argument all If this is true then calling undo returns all buffers from the first update to the current position - 1 .注意undo 函数接受参数all如果这是真的,那么调用 undo 会将所有缓冲区从第一次更新返回到当前position - 1 This is needed if you need to reconstruct the image.如果您需要重建图像,则需要这样做。

function UndoBuffer(maxUndos = Infinity) {
    const buffer = [];
    var position = 0;
    const API = {
        get canUndo() { return position > 0 },
        get canRedo() { return position < buffer.length },
        update(data) {
            if (position === maxUndos) { 
                buffer.shift();
                position--;
            }
            if (position < buffer.length) { buffer.length = position }
            buffer.push(data);
            position ++;
        },
        undo(all = true) {
            if (API.canUndo) { 
                if (all) {
                    const buf = [...buffer];
                    buf.length = --position;
                    return buf;
                }
                return buffer[--position];
            }
        },
        redo() {
            if (API.canRedo) { return buffer[position++] }
        },
    };
    return API;
}

Example例子

Using above UndoBuffer to implement undo and redo using buffered strokes.使用上面的UndoBuffer来实现使用缓冲笔画的撤销和重做。

 const ctx = canvas.getContext("2d"); undo.addEventListener("click", undoDrawing); redo.addEventListener("click", redoDrawing); const undoBuffer = UndoBuffer(); updateUndo(); function createImage(w, h){ const can = document.createElement("canvas"); can.width = w; can.height = h; can.ctx = can.getContext("2d"); return can; } const drawing = createImage(canvas.width, canvas.height); const mouse = {x : 0, y : 0, button : false, target: canvas}; function mouseEvents(e){ var updateTarget = false if (mouse.target === e.target || mouse.button) { mouse.x = e.pageX; mouse.y = e.pageY; if (e.type === "mousedown") { mouse.button = true } updateTarget = true; } if (e.type === "mouseup" && mouse.button) { mouse.button = false; updateTarget = true; } updateTarget && update(e.type); } ["down", "up", "move"].forEach(name => document.addEventListener("mouse" + name, mouseEvents)); const stroke = []; function drawStroke(ctx, stroke, r = false) { var i = 0; ctx.lineWidth = 5; ctx.lineCap = ctx.lineJoin = "round"; ctx.strokeStyle = "black"; ctx.beginPath(); while (i < stroke.length) { ctx.lineTo(stroke[i++],stroke[i++]) } ctx.stroke(); } function updateView() { ctx.globalCompositeOperation = "copy"; ctx.drawImage(drawing, 0, 0); ctx.globalCompositeOperation = "source-over"; } function update(event) { var i = 0; if (mouse.button) { updateView() stroke.push(mouse.x - 1, mouse.y - 29); drawStroke(ctx, stroke); } if (event === "mouseup") { drawing.ctx.globalCompositeOperation = "copy"; drawing.ctx.drawImage(canvas, 0, 0); drawing.ctx.globalCompositeOperation = "source-over"; addUndoable(stroke); stroke.length = 0; } } function updateUndo() { undo.disabled = !undoBuffer.canUndo; redo.disabled = !undoBuffer.canRedo; } function undoDrawing() { drawing.ctx.clearRect(0, 0, drawing.width, drawing.height); undoBuffer.undo(true).forEach(stroke => drawStroke(drawing.ctx, stroke, true)); updateView(); updateUndo(); } function redoDrawing() { drawStroke(drawing.ctx, undoBuffer.redo()); updateView(); updateUndo(); } function addUndoable(data) { undoBuffer.update([...data]); updateUndo(); } function UndoBuffer(maxUndos = Infinity) { const buffer = []; var position = 0; const API = { get canUndo() { return position > 0 }, get canRedo() { return position < buffer.length }, update(data) { if (position === maxUndos) { buffer.shift(); position--; } if (position < buffer.length) { buffer.length = position } buffer.push(data); position ++; }, reset() { position = buffer.length = 0 }, undo(all = true) { if (API.canUndo) { if (all) { const buf = [...buffer]; buf.length = --position; return buf; } return buffer[--position]; } }, redo() { if (API.canRedo) { return buffer[position++] } }, }; return API; }
 canvas { position : absolute; top : 28px; left : 0px; border: 1px solid black; } button { position : absolute; top: 4px; } #undo { left: 4px; } #redo { left: 60px; }
 <canvas id="canvas"></canvas> <button id="undo">Undo</button> <button id="redo">Redo</button>

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM