Canvas自由绘图Reactjs中撤销和重做功能

3
在使用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} />;
   
}
2个回答

2

选项

您有几个选项。

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

    注意:此方法需要无限(远大于所有可能的笔画)的撤消缓冲区才能正常工作。

  • B 在鼠标抬起后保存画布像素并存储在缓冲区中。不要使用getImageData,因为该缓冲区未经压缩,将很快占用大量内存。而是将像素数据存储为blobDataURL。默认图像格式为PNG,它是无损和压缩的,因此极大地减少了所需的RAM。要执行撤消/重做操作,请清除画布,创建一个图像,并将源设置为适当的撤消位置处的blob或dataURL。当图像加载完成后,将其绘制到画布上。

    注意:blob必须被撤销,因此撤消缓冲区必须确保在失去引用之前,任何已删除的引用都已被撤销。

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

简单的撤消缓冲区对象

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

撤消缓冲区独立于react的状态。

示例代码片段展示了如何使用它。

注意 撤消函数带有参数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>


1

以下是使用变量的最简单解决方案。可以通过以下链接访问CodeSandbox实时工作的解决方案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;

好的,谢谢...但是如果我不想要描边样式被白色遮盖怎么办?我希望它消失。 - Ojay

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接