You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React中HTML5 Canvas手绘功能的撤销与重做实现求助

实现React Canvas手绘的撤销/重做功能

嘿,这事儿不难!咱们可以通过保存Canvas的状态快照来实现撤销和重做功能。核心思路是维护一个历史记录栈,每次完成一笔绘制就把当前Canvas的状态存进去,撤销/重做时就从栈里取出对应快照恢复到画布上。下面是对你现有代码的改造方案:

步骤拆解

  • 新增状态管理历史记录和当前步骤:用useState维护history(存储Canvas快照数组)和currentStep(当前处于历史记录的哪一步)
  • 绘制完成时更新历史记录:每次松开鼠标完成绘制后,把当前Canvas的状态添加到历史栈中,注意如果是撤销后再绘制,要截断后续的历史记录
  • 实现撤销/重做函数:通过切换currentStep,将对应步骤的快照重新绘制到Canvas上

完整改造代码

import { useRef, useState, useEffect } from 'react';

function App(props) {
  const canvasRef = useRef(null);
  const contextRef = useRef(null);
  const [isDrawing, setIsDrawing] = useState(false);
  // 新增:管理历史记录和当前步骤
  const [history, setHistory] = useState([]);
  const [currentStep, setCurrentStep] = useState(-1);

  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 initialSnapshot = canvas.toDataURL();
    setHistory([initialSnapshot]);
    setCurrentStep(0);
  }, []);

  // 新增:从快照恢复画布状态的工具函数
  const restoreCanvas = (snapshot) => {
    const canvas = canvasRef.current;
    const context = contextRef.current;
    const img = new Image();
    img.onload = () => {
      context.clearRect(0, 0, canvas.width, canvas.height);
      context.drawImage(img, 0, 0);
    };
    img.src = snapshot;
  };

  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 canvas = canvasRef.current;
    const currentSnapshot = canvas.toDataURL();
    
    // 如果当前不是最新步骤(比如撤销后又画了新内容),截断后续历史
    const updatedHistory = history.slice(0, currentStep + 1);
    updatedHistory.push(currentSnapshot);
    setHistory(updatedHistory);
    setCurrentStep(updatedHistory.length - 1);
  };

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

  // 新增:撤销功能
  const handleUndo = () => {
    if (currentStep > 0) {
      const prevStep = currentStep - 1;
      setCurrentStep(prevStep);
      restoreCanvas(history[prevStep]);
    }
  };

  // 新增:重做功能
  const handleRedo = () => {
    if (currentStep < history.length - 1) {
      const nextStep = currentStep + 1;
      setCurrentStep(nextStep);
      restoreCanvas(history[nextStep]);
    }
  };

  return (
    <div>
      {/* 新增撤销/重做按钮 */}
      <div style={{ padding: '10px' }}>
        <button onClick={handleUndo} disabled={currentStep === 0}>撤销</button>
        <button onClick={handleRedo} disabled={currentStep === history.length - 1}>重做</button>
      </div>
      <canvas 
        onMouseDown={startDrawing} 
        onMouseUp={finishDrawing} 
        onMouseMove={draw} 
        ref={canvasRef} 
        style={{ border: '1px solid #ccc' }}
      />
    </div>
  );
}

export default App;

关键细节说明

  • 快照存储:用canvas.toDataURL()把当前画布转换成base64格式的图片字符串,存在history数组里,这种方式简单直接,适合手绘场景
  • 历史截断:当你撤销几步后又开始新的绘制,这时候原来的“未来”历史就没用了,所以要截断history到当前步骤,避免混淆
  • 按钮禁用:给撤销/重做按钮加禁用状态,当已经到历史最开始或最末尾时,按钮不能点击,提升用户体验
  • 高清画布适配:你原来代码里的scale(2,2)是为了高清显示,恢复快照时drawImage会自动适配这个缩放,不用额外处理

内容的提问来源于stack exchange,提问作者Ojay

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.09 17:42:41