【问题标题】:Canvas Freehand drawing Undo and Redo functionality in ReactjsReactjs 中的 Canvas 手绘撤消和重做功能
【发布时间】:2020-10-30 15:20:00
【问题描述】:

在我尝试使用使用 React 实现的 HTML5 画布创建手绘图之后,我想分别在单击撤消和重做按钮时添加撤消和重做功能。如有任何帮助,我将不胜感激。

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} />;
   
}

【问题讨论】:

    标签: javascript reactjs html5-canvas


    【解决方案1】:

    这是最简单的变量解决方案。代码沙盒解决方案可实时运行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;
    

    【讨论】:

    • 很好,谢谢...但是如果我不希望笔触样式被白色遮盖怎么办。我希望它消失
    【解决方案2】:

    选项

    您有多种选择。

    • A 在鼠标抬起后,将用于渲染每个笔划的所有点保存在缓冲区(数组)中。要撤消清除画布并将所有笔划重画到适当的撤消位置。要重做,只需在撤消缓冲区中绘制下一个笔划。

      注意此方法需要无限(远大于所有可能的笔划)撤消缓冲区,否则将不起作用。

    • B 鼠标向上保存画布像素并存储在缓冲区中。不要使用getImageData,因为缓冲区是未压缩的,会很快消耗大量内存。而是将像素数据存储为blobDataURL。默认图像格式是 PNG,它是无损压缩的,因此大大减少了所需的 RAM。要撤消/重做,请清除画布,创建图像并将源设置为相应撤消位置的 blob 或 dataURL。加载图像后,将其绘制到画布上。

      注意必须撤销 Blob,因此撤消缓冲区必须确保在丢失引用之前撤销所有已删除的引用。

    • C 上述两种方法的组合。保存笔画,并经常保存像素。

    简单的撤消缓冲区对象

    您可以实现一个通用的撤消缓冲区对象来存储任何数据。

    撤消缓冲区独立于反应的状态

    示例 sn-p 显示了它是如何使用的。

    注意 undo 函数接受参数all 如果这是真的,那么调用undo 会将所有缓冲区从第一次更新返回到当前position - 1。如果您需要重建图像,则需要这样做。

    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;
    }
    

    示例

    使用上面的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>

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-10-03
      • 2011-09-18
      • 2012-06-22
      • 2012-11-14
      • 2013-01-26
      • 2013-01-27
      相关资源
      最近更新 更多