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
相关产品推荐
相关产品推荐

